SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
PCK.cpp
Go to the documentation of this file.
1#include <vpkpp/format/PCK.h>
2
3#include <filesystem>
4#include <format>
5#include <ranges>
6
7#include <FileStream.h>
9
10using namespace sourcepp;
11using namespace vpkpp;
12
14constexpr int PCK_FILE_DATA_PADDING = 16;
15
16std::unique_ptr<PackFile> PCK::create(const std::string& path, uint32_t version, uint32_t godotMajorVersion, uint32_t godotMinorVersion, uint32_t godotPatchVersion) {
17 if (version != 1 && version != 2) {
18 return nullptr;
19 }
20
21 {
22 FileStream stream{path, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
23
24 stream
25 .write(PCK_SIGNATURE)
26 .write(version)
27 .write(godotMajorVersion)
28 .write(godotMinorVersion)
29 .write(godotPatchVersion);
30
31 if (version > 1) {
32 stream
33 .write(FLAG_DIR_NONE)
34 .write<uint64_t>(0);
35 }
36
37 stream
38 .write(std::array<int32_t, 16>{})
39 .write<uint32_t>(0);
40 }
41 return PCK::open(path);
42}
43
44std::unique_ptr<PackFile> PCK::open(const std::string& path, const EntryCallback& callback) {
45 if (!std::filesystem::exists(path)) {
46 // File does not exist
47 return nullptr;
48 }
49
50 auto* pck = new PCK{path};
51 auto packFile = std::unique_ptr<PackFile>(pck);
52
53 FileStream reader{pck->fullFilePath};
54 reader.seek_in(0);
55
56 if (auto signature = reader.read<uint32_t>(); signature != PCK_SIGNATURE) {
57 // PCK might be embedded
58 reader.seek_in(sizeof(uint32_t), std::ios::end);
59 if (auto endSignature = reader.read<uint32_t>(); endSignature != PCK_SIGNATURE) {
60 return nullptr;
61 }
62
63 reader.seek_in(-static_cast<int64_t>(sizeof(uint32_t) + sizeof(uint64_t)), std::ios::cur);
64 auto distanceIntoFile = reader.read<uint64_t>();
65
66 reader.seek_in(-static_cast<int64_t>(distanceIntoFile + sizeof(uint64_t)), std::ios::cur);
67 if (auto startSignature = reader.read<uint32_t>(); startSignature != PCK_SIGNATURE) {
68 return nullptr;
69 }
70
71 pck->startOffset = reader.tell_in() - sizeof(uint32_t);
72 }
73
74 reader.read(pck->header.packVersion);
75 if (pck->header.packVersion < 1 || pck->header.packVersion > 2) {
76 // We don't support v3 yet
77 return nullptr;
78 }
79
80 reader.read(pck->header.godotVersionMajor);
81 reader.read(pck->header.godotVersionMinor);
82 reader.read(pck->header.godotVersionPatch);
83
84 pck->header.flags = FLAG_DIR_NONE;
85 std::size_t extraEntryContentsOffset = 0;
86 if (pck->header.packVersion > 1) {
87 pck->header.flags = reader.read<FlagsDirV2>();
88 extraEntryContentsOffset = reader.read<uint64_t>();
89 }
90
91 if (pck->header.flags & FLAG_DIR_ENCRYPTED) {
92 // File directory is encrypted
93 return nullptr;
94 }
95 if (pck->header.flags & FLAG_DIR_RELATIVE_FILE_DATA) {
96 extraEntryContentsOffset += pck->startOffset;
97 pck->header.flags = static_cast<FlagsDirV2>(pck->header.flags & ~FLAG_DIR_RELATIVE_FILE_DATA);
98 }
99
100 // Reserved
101 reader.skip_in<int32_t>(16);
102
103 // Directory
104 auto fileCount = reader.read<uint32_t>();
105 for (uint32_t i = 0; i < fileCount; i++) {
106 Entry entry = createNewEntry();
107
108 auto entryPath = pck->cleanEntryPath(reader.read_string(reader.read<uint32_t>()));
109 if (entryPath.starts_with(PCK_PATH_PREFIX)) {
110 entryPath = entryPath.substr(PCK_PATH_PREFIX.length());
111 }
112
113 entry.offset = reader.read<uint64_t>() + extraEntryContentsOffset;
114 entry.length = reader.read<uint64_t>();
115 entry.extraData = reader.read_bytes(16);
116
117 if (pck->header.packVersion > 1) {
118 entry.flags = reader.read<uint32_t>();
119 if (entry.flags & FLAG_FILE_REMOVED) {
120 continue;
121 }
122 }
123
124 pck->entries.emplace(entryPath, entry);
125
126 if (callback) {
127 callback(entryPath, entry);
128 }
129 }
130
131 // File data
132 pck->dataOffset = reader.tell_in();
133
134 return packFile;
135}
136
137std::optional<std::vector<std::byte>> PCK::readEntry(const std::string& path_) const {
138 auto path = this->cleanEntryPath(path_);
139 auto entry = this->findEntry(path);
140 if (!entry) {
141 return std::nullopt;
142 }
143 if (entry->unbaked) {
144 return readUnbakedEntry(*entry);
145 }
146
147 // It's baked into the file on disk
148 if (entry->flags & FLAG_FILE_ENCRYPTED) {
149 // File is encrypted
150 return std::nullopt;
151 }
152
153 FileStream stream{this->fullFilePath};
154 if (!stream) {
155 return std::nullopt;
156 }
157 stream.seek_in_u(entry->offset);
158 return stream.read_bytes(entry->length);
159}
160
161void PCK::addEntryInternal(Entry& entry, const std::string& path, std::vector<std::byte>& buffer, EntryOptions options) {
162 entry.length = buffer.size();
163
164 const auto md5 = crypto::computeMD5(buffer);
165 entry.extraData = {md5.begin(), md5.end()};
166
167 // Offset will be reset when it's baked
168 entry.offset = 0;
169}
170
171bool PCK::bake(const std::string& outputDir_, BakeOptions options, const EntryCallback& callback) {
172 // Get the proper file output folder
173 std::string outputDir = this->getBakeOutputDir(outputDir_);
174 std::string outputPath = outputDir + '/' + this->getFilename();
175
176 // Reconstruct data for ease of access
177 std::vector<std::pair<std::string, Entry*>> entriesToBake;
178 this->runForAllEntriesInternal([&entriesToBake](const std::string& path, Entry& entry) {
179 entriesToBake.emplace_back(path, &entry);
180 });
181
182 // Read data before overwriting, we don't know if we're writing to ourself
183 std::vector<std::byte> fileData;
184 for (auto& [path, entry] : entriesToBake) {
185 if (auto binData = this->readEntry(path)) {
186 entry->offset = fileData.size();
187
188 fileData.insert(fileData.end(), binData->begin(), binData->end());
189 const auto padding = math::paddingForAlignment(PCK_FILE_DATA_PADDING, static_cast<int>(entry->length));
190 for (int i = 0; i < padding; i++) {
191 fileData.push_back(static_cast<std::byte>(0));
192 }
193 } else {
194 entry->offset = 0;
195 entry->length = 0;
196 }
197 }
198
199 // If this is an embedded pck, read the executable data first
200 std::vector<std::byte> exeData;
201 if (this->startOffset > 0) {
202 FileStream stream{this->fullFilePath};
203 if (!stream) {
204 return false;
205 }
206 stream.seek_in(0);
207 exeData = stream.read_bytes(this->startOffset);
208 }
209
210 // Write data
211 {
212 FileStream stream{outputPath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
213 stream.seek_out(0);
214
215 if (!exeData.empty()) {
216 stream.write(exeData);
217 }
218
219 // Signature
220 stream.write(PCK_SIGNATURE);
221
222 // Header
223 stream.write(this->header.packVersion);
224 stream.write(this->header.godotVersionMajor);
225 stream.write(this->header.godotVersionMinor);
226 stream.write(this->header.godotVersionPatch);
227
228 if (this->header.packVersion > 1) {
229 stream.write(this->header.flags);
230 stream.write<uint64_t>(0);
231 }
232
233 // Reserved
234 stream.write(std::array<int32_t, 16>{});
235
236 // Directory start
237 stream.write(static_cast<uint32_t>(entriesToBake.size()));
238
239 // Dry-run to get the length of the directory section
240 this->dataOffset = stream.tell_out();
241 for (const auto& path : std::views::keys(entriesToBake)) {
242 const auto entryPath = std::string{PCK_PATH_PREFIX} + path;
243 const auto padding = math::paddingForAlignment(PCK_DIRECTORY_STRING_PADDING, static_cast<int>(entryPath.length()));
244 this->dataOffset +=
245 sizeof(uint32_t) + // Path length
246 entryPath.length() + padding + // Path
247 (sizeof(std::size_t) * 2) + // Offset, Length
248 (sizeof(std::byte) * 16); // MD5
249
250 if (this->header.packVersion > 1) {
251 this->dataOffset += sizeof(uint32_t); // Flags
252 }
253 }
254
255 // Directory
256 for (const auto& [path, entry] : entriesToBake) {
257 auto entryPath = path;
258 if (this->header.godotVersionMajor <= 4 && this->header.godotVersionMinor <= 3) {
259 entryPath = std::string{PCK_PATH_PREFIX} + entryPath; // NOLINT(*-inefficient-string-concatenation)
260 }
261 const auto padding = math::paddingForAlignment(PCK_DIRECTORY_STRING_PADDING, static_cast<int>(entryPath.length()));
262 stream.write(static_cast<uint32_t>(entryPath.length() + padding));
263 stream.write(entryPath, false, entryPath.length() + padding);
264
265 entry->offset += this->dataOffset;
266 stream.write(entry->offset);
267 stream.write(entry->length);
268 stream.write(entry->extraData);
269
270 if (this->header.packVersion > 1) {
271 stream.write(entry->flags);
272 }
273
274 if (callback) {
275 callback(path, *entry);
276 }
277 }
278
279 // File data
280 stream.write(fileData);
281
282 // Write offset to start
283 if (this->startOffset > 0) {
284 stream.write(stream.tell_out() - startOffset);
285 stream.write(PCK_SIGNATURE);
286 }
287 }
288
289 // Clean up
290 this->mergeUnbakedEntries();
291 PackFile::setFullFilePath(outputDir);
292 return true;
293}
294
296 using enum Attribute;
297 return LENGTH | PCK_MD5;
298}
299
300PCK::operator std::string() const {
301 auto out = PackFile::operator std::string() + std::format(" | Version v{} | Godot Version v{}.{}.{}", this->header.packVersion, this->header.godotVersionMajor, this->header.godotVersionMinor, this->header.godotVersionPatch);
302 if (this->startOffset > 0) {
303 out += " | Embedded";
304 }
305 if (this->header.flags & FLAG_DIR_ENCRYPTED) {
306 out += " | Encrypted";
307 }
308 return out;
309}
310
311uint32_t PCK::getVersion() const {
312 return this->header.packVersion;
313}
314
315void PCK::setVersion(uint32_t version) {
316 if (version == 1 || version == 2) {
317 this->header.packVersion = version;
318 }
319}
320
321std::tuple<uint32_t, uint32_t, uint32_t> PCK::getGodotVersion() const {
322 return {this->header.godotVersionMajor, this->header.godotVersionMinor, this->header.godotVersionPatch};
323}
324
325void PCK::setGodotVersion(uint32_t major, uint32_t minor, uint32_t patch) {
326 this->header.godotVersionMajor = major;
327 this->header.godotVersionMinor = minor;
328 this->header.godotVersionPatch = patch;
329}
constexpr int PCK_DIRECTORY_STRING_PADDING
Definition PCK.cpp:13
constexpr int PCK_FILE_DATA_PADDING
Definition PCK.cpp:14
This class represents the metadata that a file has inside a PackFile.
Definition Entry.h:14
bool unbaked
Used to check if entry is saved to disk.
Definition Entry.h:43
uint32_t flags
Format-specific flags (PCK: File flags, VPK: Internal parser state, ZIP: Compression method / strengt...
Definition Entry.h:19
uint64_t offset
Offset, format-specific meaning - 0 if unused, or if the offset genuinely is 0.
Definition Entry.h:33
uint64_t length
Length in bytes (in formats with compression, this is the uncompressed length).
Definition Entry.h:26
std::vector< std::byte > extraData
Format-specific (PCK: MD5 hash, VPK: Preloaded data).
Definition Entry.h:36
void setGodotVersion(uint32_t major, uint32_t minor=0, uint32_t patch=0)
Definition PCK.cpp:325
void addEntryInternal(Entry &entry, const std::string &path, std::vector< std::byte > &buffer, EntryOptions options) override
Definition PCK.cpp:161
std::size_t startOffset
Definition PCK.h:73
std::tuple< uint32_t, uint32_t, uint32_t > getGodotVersion() const
Definition PCK.cpp:321
Header header
Definition PCK.h:71
static std::unique_ptr< PackFile > open(const std::string &path, const EntryCallback &callback=nullptr)
Open a PCK file (potentially embedded in an executable).
Definition PCK.h:78
std::optional< std::vector< std::byte > > readEntry(const std::string &path_) const override
Try to read the entry's data to a bytebuffer.
Definition PCK.cpp:137
uint32_t getVersion() const
Returns 1 for v1, 2 for v2.
Definition PCK.cpp:311
Attribute getSupportedEntryAttributes() const override
Returns a list of supported entry attributes Mostly for GUI programs that show entries and their meta...
Definition PCK.cpp:295
@ FLAG_FILE_ENCRYPTED
Definition PCK.h:25
@ FLAG_FILE_REMOVED
Definition PCK.h:26
std::size_t dataOffset
Definition PCK.h:74
static std::unique_ptr< PackFile > create(const std::string &path, uint32_t version=2, uint32_t godotMajorVersion=0, uint32_t godotMinorVersion=0, uint32_t godotPatchVersion=0)
Create a new PCK file.
Definition PCK.cpp:16
void setVersion(uint32_t version)
Change the version of the PCK. Valid values are 1 and 2.
Definition PCK.cpp:315
FlagsDirV2
Definition PCK.h:17
@ FLAG_DIR_ENCRYPTED
Definition PCK.h:19
@ FLAG_DIR_NONE
Definition PCK.h:18
@ FLAG_DIR_RELATIVE_FILE_DATA
Definition PCK.h:20
EntryCallbackBase< void > EntryCallback
Definition PackFile.h:38
void mergeUnbakedEntries()
Definition PackFile.cpp:685
std::optional< Entry > findEntry(const std::string &path_, bool includeUnbaked=true) const
Try to find an entry given the file path.
Definition PackFile.cpp:172
std::string fullFilePath
Definition PackFile.h:231
void runForAllEntriesInternal(const std::function< void(const std::string &, Entry &)> &operation, bool includeUnbaked=true)
Definition PackFile.cpp:571
bool bake()
If output folder is an empty string, it will overwrite the original.
Definition PackFile.cpp:369
std::string getFilename() const
/home/user/pak01_dir.vpk -> pak01_dir.vpk
Definition PackFile.cpp:621
std::string getBakeOutputDir(const std::string &outputDir) const
Definition PackFile.cpp:670
void setFullFilePath(const std::string &outputDir)
Definition PackFile.cpp:701
std::string cleanEntryPath(const std::string &path) const
Definition PackFile.cpp:706
static Entry createNewEntry()
Definition PackFile.cpp:715
static std::optional< std::vector< std::byte > > readUnbakedEntry(const Entry &entry)
Definition PackFile.cpp:719
std::array< std::byte, 16 > computeMD5(std::span< const std::byte > buffer)
Definition MD5.cpp:8
constexpr uint16_t paddingForAlignment(uint16_t alignment, uint64_t n)
Definition Math.h:61
Attribute
Definition Attribute.h:7
constexpr auto PCK_SIGNATURE
Definition PCK.h:11
constexpr std::string_view PCK_PATH_PREFIX
Definition PCK.h:12