SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
VPK.cpp
Go to the documentation of this file.
1#include <vpkpp/format/VPK.h>
2
3#include <cstdio>
4#include <filesystem>
5#include <format>
6
7#define CRYPTOPP_ENABLE_NAMESPACE_WEAK 1
8#include <cryptopp/md5.h>
9#include <FileStream.h>
10#include <kvpp/kvpp.h>
12#include <sourcepp/crypto/MD5.h>
13#include <sourcepp/crypto/RSA.h>
15#include <sourcepp/FS.h>
16#include <sourcepp/String.h>
17#include <vpkpp/format/FPX.h>
18
19#ifdef VPKPP_SUPPORT_VPK_V54
20#include <zstd.h>
21#endif
22
23using namespace kvpp;
24using namespace sourcepp;
25using namespace vpkpp;
26
28constexpr uint32_t VPK_FLAG_REUSING_CHUNK = 0x1;
29
30namespace {
31
32std::string removeVPKAndOrDirSuffix(const std::string& path, bool isFPX) {
33 std::string filename = path;
34 if (filename.length() >= 4 && filename.substr(filename.length() - 4) == (isFPX ? FPX_EXTENSION : VPK_EXTENSION)) {
35 filename = filename.substr(0, filename.length() - 4);
36 }
37
38 // This indicates it's a dir VPK, but some people ignore this convention...
39 // It should fail later if it's not a proper dir VPK
40 if (filename.length() >= 4 && filename.substr(filename.length() - 4) == (isFPX ? FPX_DIR_SUFFIX : VPK_DIR_SUFFIX)) {
41 filename = filename.substr(0, filename.length() - 4);
42 }
43
44 return filename;
45}
46
47bool isFPX(const VPK* vpk) {
48 return dynamic_cast<const FPX*>(vpk);
49}
50
51} // namespace
52
53std::unique_ptr<PackFile> VPK::create(const std::string& path, uint32_t version) {
54 if (version != 0 && version != 1 && version != 2 && version != 54) {
55 return nullptr;
56 }
57
58 {
59 FileStream stream{path, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
60
61 if (version > 0) {
63 header1.signature = VPK_SIGNATURE;
64 header1.version = version;
65 header1.treeSize = 1;
66 stream.write(header1);
67 }
68 if (version > 1) {
70 header2.fileDataSectionSize = 0;
71 header2.archiveMD5SectionSize = 0;
72 header2.otherMD5SectionSize = 0;
73 header2.signatureSectionSize = 0;
74 stream.write(header2);
75 }
76
77 stream.write('\0');
78 }
79 return VPK::open(path);
80}
81
82std::unique_ptr<PackFile> VPK::open(const std::string& path, const EntryCallback& callback) {
83 std::unique_ptr<PackFile> vpk;
84
85 // Try loading the directory VPK first if this is a numbered archive and the dir exists
86 if (path.length() >= 8) {
87 auto dirPath = path.substr(0, path.length() - 8) + "_dir.vpk";
88 auto pathEnd = path.substr(path.length() - 8, path.length());
89 if (string::matches(pathEnd, "_%d%d%d.vpk") && std::filesystem::exists(dirPath)) {
90 vpk = VPK::openInternal(dirPath, callback);
91 if (vpk) {
92 return vpk;
93 }
94 }
95 }
96
97 return VPK::openInternal(path, callback);
98}
99
100std::unique_ptr<PackFile> VPK::openInternal(const std::string& path, const EntryCallback& callback) {
101 if (!std::filesystem::exists(path)) {
102 // File does not exist
103 return nullptr;
104 }
105
106 auto* vpk = new VPK{path};
107 auto packFile = std::unique_ptr<PackFile>{vpk};
108
109 FileStream reader{vpk->fullFilePath};
110 reader.seek_in(0);
111 reader.read(vpk->header1);
112 if (vpk->header1.signature != VPK_SIGNATURE) {
113 reader.seek_in(3, std::ios::end);
114 if (reader.read<char>() == '\0' && reader.read<char>() == '\0' && reader.read<char>() == '\0') {
115 // hack: if file is 9 bytes long it's probably an empty VTMB VPK and we should bail so that code can pick it up
116 // either way a 9 byte long VPK should not have any files in it
117 if (std::filesystem::file_size(vpk->fullFilePath) == 9) {
118 return nullptr;
119 }
120
121 // File is one of those shitty ancient VPKs
122 vpk->header1.signature = VPK_SIGNATURE;
123 vpk->header1.version = 0;
124 vpk->header1.treeSize = 0;
125
126 reader.seek_in(0);
127 } else {
128 // File is not a VPK
129 return nullptr;
130 }
131 }
132 if (vpk->hasExtendedHeader()) {
133 reader.read(vpk->header2);
134 } else if (vpk->header1.version != 0 && vpk->header1.version != 1) {
135 // Apex Legends, Titanfall, etc. are not supported
136 return nullptr;
137 }
138
139 // Extensions
140 while (true) {
141 std::string extension;
142 reader.read(extension);
143 if (extension.empty())
144 break;
145
146 // Directories
147 while (true) {
148 std::string directory;
149 reader.read(directory);
150 if (directory.empty())
151 break;
152
153 std::string fullDir;
154 if (directory == " ") {
155 fullDir = "";
156 } else {
157 fullDir = directory;
158 }
159
160 // Files
161 while (true) {
162 std::string entryName;
163 reader.read(entryName);
164 if (entryName.empty())
165 break;
166
167 Entry entry = createNewEntry();
168
169 std::string entryPath;
170 if (extension == " ") {
171 entryPath = fullDir.empty() ? "" : fullDir + '/';
172 entryPath += entryName;
173 } else {
174 entryPath = fullDir.empty() ? "" : fullDir + '/';
175 entryPath += entryName + '.';
176 entryPath += extension;
177 }
178 entryPath = vpk->cleanEntryPath(entryPath);
179
180 reader.read(entry.crc32);
181 auto preloadedDataSize = reader.read<uint16_t>();
182 entry.archiveIndex = reader.read<uint16_t>();
183 entry.offset = reader.read<uint32_t>();
184 entry.length = reader.read<uint32_t>();
185
186 if (vpk->hasCompression()) {
187 entry.compressedLength = reader.read<uint32_t>();
188 }
189
190 if (reader.read<uint16_t>() != VPK_ENTRY_TERM) {
191 // Invalid terminator!
192 return nullptr;
193 }
194
195 if (preloadedDataSize > 0) {
196 entry.extraData = reader.read_bytes(preloadedDataSize);
197 entry.length += preloadedDataSize;
198 }
199
200 if (entry.archiveIndex != VPK_DIR_INDEX && std::cmp_greater(entry.archiveIndex, vpk->numArchives)) {
201 vpk->numArchives = static_cast<int32_t>(entry.archiveIndex);
202 }
203
204
205 vpk->entries.emplace(entryPath, entry);
206
207 if (callback) {
208 callback(entryPath, entry);
209 }
210 }
211 }
212 }
213
214 // If there are no archives, -1 will be incremented to 0
215 vpk->numArchives++;
216
217 // VPK v1 has nothing else for us
218 if (!vpk->hasExtendedHeader()) {
219 return packFile;
220 }
221
222 // Skip over file data, if any
223 reader.seek_in(vpk->header2.fileDataSectionSize, std::ios::cur);
224
225 if (vpk->header2.archiveMD5SectionSize % sizeof(MD5Entry) != 0) {
226 return nullptr;
227 }
228
229 vpk->md5Entries.clear();
230 unsigned int entryNum = vpk->header2.archiveMD5SectionSize / sizeof(MD5Entry);
231 for (unsigned int i = 0; i < entryNum; i++) {
232 vpk->md5Entries.push_back(reader.read<MD5Entry>());
233 }
234
235 if (vpk->header2.otherMD5SectionSize != 48) {
236 // This should always be 48
237 return packFile;
238 }
239
240 vpk->footer2.treeChecksum = reader.read_bytes<16>();
241 vpk->footer2.md5EntriesChecksum = reader.read_bytes<16>();
242 vpk->footer2.wholeFileChecksum = reader.read_bytes<16>();
243
244 if (!vpk->header2.signatureSectionSize) {
245 return packFile;
246 }
247
248 auto publicKeySize = reader.read<int32_t>();
249 if (vpk->header2.signatureSectionSize == 20 && publicKeySize == VPK_SIGNATURE) {
250 // CS2 beta VPK, ignore it
251 return packFile;
252 }
253
254 vpk->footer2.publicKey = reader.read_bytes(publicKeySize);
255 vpk->footer2.signature = reader.read_bytes(reader.read<int32_t>());
256
257 return packFile;
258}
259
260std::vector<std::string> VPK::verifyEntryChecksums() const {
261 return this->verifyEntryChecksumsUsingCRC32();
262}
263
265 return this->hasExtendedHeader();
266}
267
269 // File checksums aren't in v1
270 if (!this->hasExtendedHeader()) {
271 return true;
272 }
273
274 FileStream stream{this->getFilepath().data()};
275
276 stream.seek_in(this->getHeaderLength());
277 if (this->footer2.treeChecksum != crypto::computeMD5(stream.read_bytes(this->header1.treeSize))) {
278 return false;
279 }
280
281 stream.seek_in(this->getHeaderLength() + this->header1.treeSize + this->header2.fileDataSectionSize);
282 if (this->footer2.md5EntriesChecksum != crypto::computeMD5(stream.read_bytes(this->header2.archiveMD5SectionSize))) {
283 return false;
284 }
285
286 stream.seek_in(0);
287 if (this->footer2.wholeFileChecksum != crypto::computeMD5(stream.read_bytes(this->getHeaderLength() + this->header1.treeSize + this->header2.fileDataSectionSize + this->header2.archiveMD5SectionSize + this->header2.otherMD5SectionSize - sizeof(this->footer2.wholeFileChecksum)))) {
288 return false;
289 }
290
291 return true;
292}
293
295 if (!this->hasExtendedHeader()) {
296 return false;
297 }
298 if (this->footer2.publicKey.empty() || this->footer2.signature.empty()) {
299 return false;
300 }
301 return true;
302}
303
305 // Signatures aren't in v1
306 if (!this->hasExtendedHeader()) {
307 return true;
308 }
309
310 if (this->footer2.publicKey.empty() || this->footer2.signature.empty()) {
311 return true;
312 }
313 auto dirFileBuffer = fs::readFileBuffer(this->getFilepath().data());
314 const auto signatureSectionSize = this->footer2.publicKey.size() + this->footer2.signature.size() + sizeof(uint32_t) * 2;
315 if (dirFileBuffer.size() <= signatureSectionSize) {
316 return false;
317 }
318 for (int i = 0; i < signatureSectionSize; i++) {
319 dirFileBuffer.pop_back();
320 }
321 return crypto::verifySHA256PublicKey(dirFileBuffer, this->footer2.publicKey, this->footer2.signature);
322}
323
324// NOLINTNEXTLINE(*-no-recursion)
325std::optional<std::vector<std::byte>> VPK::readEntry(const std::string& path_) const {
326 auto path = this->cleanEntryPath(path_);
327 auto entry = this->findEntry(path);
328 if (!entry) {
329 return std::nullopt;
330 }
331 if (entry->unbaked) {
332 return readUnbakedEntry(*entry);
333 }
334
335 const auto entryLength = (this->hasCompression() && entry->compressedLength) ? entry->compressedLength : entry->length;
336 if (entryLength == 0) {
337 return std::vector<std::byte>{};
338 }
339 std::vector out(entryLength, static_cast<std::byte>(0));
340
341 if (!entry->extraData.empty()) {
342 std::copy(entry->extraData.begin(), entry->extraData.end(), out.begin());
343 }
344 if (entryLength != entry->extraData.size()) {
345 if (entry->archiveIndex != VPK_DIR_INDEX) {
346 // Stored in a numbered archive
347 FileStream stream{this->getTruncatedFilepath() + '_' + string::padNumber(entry->archiveIndex, 3) + std::string{::isFPX(this) ? FPX_EXTENSION : VPK_EXTENSION}};
348 if (!stream) {
349 return std::nullopt;
350 }
351 stream.seek_in_u(entry->offset);
352 auto bytes = stream.read_bytes(entryLength - entry->extraData.size());
353 std::copy(bytes.begin(), bytes.end(), out.begin() + static_cast<long long>(entry->extraData.size()));
354 } else {
355 // Stored in this directory VPK
356 FileStream stream{this->fullFilePath};
357 if (!stream) {
358 return std::nullopt;
359 }
360 stream.seek_in_u(this->getHeaderLength() + this->header1.treeSize + entry->offset);
361 auto bytes = stream.read_bytes(entry->length - entry->extraData.size());
362 std::copy(bytes.begin(), bytes.end(), out.begin() + static_cast<long long>(entry->extraData.size()));
363 }
364 }
365
366#ifndef VPKPP_SUPPORT_VPK_V54
367 return out;
368#else
369 if (!this->hasCompression() || !entry->compressedLength) {
370 return out;
371 }
372
373 const auto decompressionDict = this->readEntry(this->getTruncatedFilestem() + ".dict");
374 if (!decompressionDict) {
375 return std::nullopt;
376 }
377
378 std::unique_ptr<ZSTD_DDict, void(*)(void*)> dDict{
379 ZSTD_createDDict(decompressionDict->data(), decompressionDict->size()),
380 [](void* dDict_) { ZSTD_freeDDict(static_cast<ZSTD_DDict*>(dDict_)); },
381 };
382 if (!dDict) {
383 return std::nullopt;
384 }
385
386 std::unique_ptr<ZSTD_DCtx, void(*)(void*)> dCtx{
387 ZSTD_createDCtx(),
388 [](void* dCtx_) { ZSTD_freeDCtx(static_cast<ZSTD_DCtx*>(dCtx_)); },
389 };
390 if (!dCtx) {
391 return std::nullopt;
392 }
393
394 std::vector<std::byte> decompressedData;
395 decompressedData.resize(entry->length);
396
397 if (ZSTD_isError(ZSTD_decompress_usingDDict(dCtx.get(), decompressedData.data(), decompressedData.size(), out.data(), out.size(), dDict.get()))) {
398 return {};
399 }
400 return decompressedData;
401#endif
402}
403
404void VPK::addEntryInternal(Entry& entry, const std::string& path, std::vector<std::byte>& buffer, EntryOptions options) {
405 if (this->hasCompression()) {
406 // I don't feel like getting this to work right now
407 options.vpk_preloadBytes = 0;
408 }
409
410 entry.crc32 = crypto::computeCRC32(buffer);
411 entry.length = buffer.size();
412
413 // Offset will be reset when it's baked, assuming we're not replacing an existing chunk (when flags = 1)
414 // Compressed entries will not replace existing chunks, since their size is unknown
415 entry.flags = 0;
416 entry.offset = 0;
418 if (!options.vpk_saveToDirectory && !this->freedChunks.empty() && !this->hasCompression()) {
419 int64_t bestChunkIndex = -1;
420 std::size_t currentChunkGap = SIZE_MAX;
421 for (int64_t i = 0; i < this->freedChunks.size(); i++) {
422 if (
423 (bestChunkIndex < 0 && this->freedChunks[i].length >= entry.length) ||
424 (bestChunkIndex >= 0 && this->freedChunks[i].length >= entry.length && (this->freedChunks[i].length - entry.length) < currentChunkGap)
425 ) {
426 bestChunkIndex = i;
427 currentChunkGap = this->freedChunks[i].length - entry.length;
428 }
429 }
430 if (bestChunkIndex >= 0) {
432 entry.offset = this->freedChunks[bestChunkIndex].offset;
433 entry.archiveIndex = this->freedChunks[bestChunkIndex].archiveIndex;
434 this->freedChunks.erase(this->freedChunks.begin() + bestChunkIndex);
435 if (currentChunkGap < SIZE_MAX && currentChunkGap > 0) {
436 // Add the remaining free space as a free chunk
437 this->freedChunks.push_back({entry.offset + entry.length, currentChunkGap, entry.archiveIndex});
438 }
439 }
440 }
441
442 if (options.vpk_preloadBytes > 0) {
443 const auto clampedPreloadBytes = std::clamp<uint16_t>(options.vpk_preloadBytes, 0, buffer.size() > VPK_MAX_PRELOAD_BYTES ? VPK_MAX_PRELOAD_BYTES : static_cast<uint16_t>(buffer.size()));
444 entry.extraData.resize(clampedPreloadBytes);
445 std::memcpy(entry.extraData.data(), buffer.data(), clampedPreloadBytes);
446 buffer.erase(buffer.begin(), buffer.begin() + clampedPreloadBytes);
447 }
448
449 // Now that archive index is calculated for this entry, check if it needs to be incremented
450 if (!options.vpk_saveToDirectory && !(entry.flags & VPK_FLAG_REUSING_CHUNK)) {
451 entry.offset = this->currentlyFilledChunkSize;
452 this->currentlyFilledChunkSize += static_cast<int>(buffer.size());
453 if (this->currentlyFilledChunkSize > this->chunkSize) {
454 this->currentlyFilledChunkSize = 0;
455 this->numArchives++;
456 }
457 }
458}
459
460bool VPK::removeEntry(const std::string& filename_) {
461 const auto filename = this->cleanEntryPath(filename_);
462 if (const auto entry = this->findEntry(filename); entry && (!entry->unbaked || entry->flags & VPK_FLAG_REUSING_CHUNK)) {
463 this->freedChunks.push_back({entry->offset, entry->length, entry->archiveIndex});
464 }
465 return PackFile::removeEntry(filename);
466}
467
468std::size_t VPK::removeDirectory(const std::string& dirName_) {
469 auto dirName = this->cleanEntryPath(dirName_);
470 if (!dirName.empty()) {
471 dirName += '/';
472 }
473 this->runForAllEntries([this, &dirName](const std::string& path, const Entry& entry) {
474 if (path.starts_with(dirName) && (!entry.unbaked || entry.flags & VPK_FLAG_REUSING_CHUNK)) {
475 this->freedChunks.push_back({entry.offset, entry.length, entry.archiveIndex});
476 }
477 });
478 return PackFile::removeDirectory(dirName_);
479}
480
481bool VPK::bake(const std::string& outputDir_, BakeOptions options, const EntryCallback& callback) {
482 // Get the proper file output folder
483 std::string outputDir = this->getBakeOutputDir(outputDir_);
484 std::string outputPath = outputDir + '/' + this->getFilename();
485
486#ifdef VPKPP_SUPPORT_VPK_V54
487 // Store compression dictionary
488 std::optional<std::vector<std::byte>> compressionDict;
489 std::unique_ptr<ZSTD_CDict, void(*)(void*)> cDict{nullptr, nullptr};
490 std::unique_ptr<ZSTD_CCtx, void(*)(void*)> cCtx{nullptr, nullptr};
491 if (this->hasCompression()) {
492 compressionDict = this->readEntry(this->getTruncatedFilestem() + ".dict");
493 if (!compressionDict) {
494 return false;
495 }
496
497 cDict = {
498 ZSTD_createCDict(compressionDict->data(), compressionDict->size(), options.zip_compressionStrength),
499 [](void* cDict_) { ZSTD_freeCDict(static_cast<ZSTD_CDict*>(cDict_)); },
500 };
501 if (!cDict) {
502 return false;
503 }
504
505 cCtx = {
506 ZSTD_createCCtx(),
507 [](void* cCtx_) { ZSTD_freeCCtx(static_cast<ZSTD_CCtx*>(cCtx_)); },
508 };
509 if (!cCtx) {
510 return false;
511 }
512 }
513#endif
514
515 // Reconstruct data so we're not looping over it a ton of times
516 std::unordered_map<std::string, std::unordered_map<std::string, std::vector<std::pair<std::string, Entry*>>>> temp;
517 this->runForAllEntriesInternal([&temp](const std::string& path, Entry& entry) {
518 const auto fsPath = std::filesystem::path{path};
519 auto extension = fsPath.extension().string();
520 if (extension.starts_with('.')) {
521 extension = extension.substr(1);
522 }
523 const auto parentDir = fsPath.parent_path().string();
524
525 if (extension.empty()) {
526 extension = " ";
527 }
528 if (!temp.contains(extension)) {
529 temp[extension] = {};
530 }
531 if (!temp.at(extension).contains(parentDir)) {
532 temp.at(extension)[parentDir] = {};
533 }
534 temp.at(extension).at(parentDir).emplace_back(path, &entry);
535 });
536
537 // Temporarily store baked file data that's stored in the directory VPK since it's getting overwritten
538 std::vector<std::byte> dirVPKEntryData;
539 std::size_t newDirEntryOffset = 0;
540 this->runForAllEntriesInternal([this, &dirVPKEntryData, &newDirEntryOffset](const std::string& path, Entry& entry) {
541 if (entry.archiveIndex != VPK_DIR_INDEX || entry.length == entry.extraData.size()) {
542 return;
543 }
544
545 auto binData = this->readEntry(path);
546 if (!binData) {
547 return;
548 }
549 dirVPKEntryData.reserve(dirVPKEntryData.size() + entry.length - entry.extraData.size());
550 dirVPKEntryData.insert(dirVPKEntryData.end(), binData->begin() + static_cast<std::vector<std::byte>::difference_type>(entry.extraData.size()), binData->end());
551
552 entry.offset = newDirEntryOffset;
553 newDirEntryOffset += entry.length - entry.extraData.size();
554 }, false);
555
556 // Helper
557 const auto getArchiveFilename = [this](const std::string& filename_, uint32_t archiveIndex) {
558 std::string out{filename_ + '_' + string::padNumber(archiveIndex, 3) + std::string{::isFPX(this) ? FPX_EXTENSION : VPK_EXTENSION}};
560 return out;
561 };
562
563 // Copy external binary blobs to the new dir
564 if (!outputDir_.empty()) {
565 for (uint32_t archiveIndex = 0; archiveIndex < this->numArchives; archiveIndex++) {
566 std::string from = getArchiveFilename(this->getTruncatedFilepath(), archiveIndex);
567 if (!std::filesystem::exists(from)) {
568 continue;
569 }
570 std::string dest = getArchiveFilename(outputDir + '/' + this->getTruncatedFilestem(), archiveIndex);
571 if (from == dest) {
572 continue;
573 }
574 std::filesystem::copy_file(from, dest, std::filesystem::copy_options::overwrite_existing);
575 }
576 }
577
578 FileStream outDir{outputPath, FileStream::OPT_READ | FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
579 outDir.seek_in(0);
580 outDir.seek_out(0);
581
582 // Dummy header
583 if (this->header1.version > 0) {
584 outDir.write(this->header1);
585 if (this->hasExtendedHeader()) {
586 outDir.write(this->header2);
587 }
588 }
589
590 // File tree data
591 for (auto& [ext, dirs] : temp) {
592 outDir.write(ext);
593
594 for (auto& [dir, tempEntries] : dirs) {
595 outDir.write(!dir.empty() ? dir : " ");
596
597 for (auto& [path, entry] : tempEntries) {
598 // Calculate entry offset if it's unbaked and upload the data
599 if (entry->unbaked) {
600 auto entryData = readUnbakedEntry(*entry);
601 if (!entryData) {
602 continue;
603 }
604
605 if (entry->length == entry->extraData.size() && !this->hasCompression()) {
606 // Override the archive index, no need for an archive VPK
608 entry->offset = dirVPKEntryData.size();
609 } else if (entry->archiveIndex != VPK_DIR_INDEX && (entry->flags & VPK_FLAG_REUSING_CHUNK)) {
610 // The entry is replacing pre-existing data in a VPK archive - it's not compressed
611 auto archiveFilename = getArchiveFilename(::removeVPKAndOrDirSuffix(outputPath, ::isFPX(this)), entry->archiveIndex);
612 FileStream stream{archiveFilename, FileStream::OPT_READ | FileStream::OPT_WRITE | FileStream::OPT_CREATE_IF_NONEXISTENT};
613 stream.seek_out_u(entry->offset);
614 stream.write(*entryData);
615 } else if (entry->archiveIndex != VPK_DIR_INDEX) {
616 // The entry is being appended to a newly created VPK archive
617 auto archiveFilename = getArchiveFilename(::removeVPKAndOrDirSuffix(outputPath, ::isFPX(this)), entry->archiveIndex);
618 entry->offset = std::filesystem::exists(archiveFilename) ? std::filesystem::file_size(archiveFilename) : 0;
619 FileStream stream{archiveFilename, FileStream::OPT_APPEND | FileStream::OPT_CREATE_IF_NONEXISTENT};
620#ifndef VPKPP_SUPPORT_VPK_V54
621 stream.write(*entryData);
622#else
623 if (!this->hasCompression() || path == this->getTruncatedFilestem() + ".dict") {
624 stream.write(*entryData);
625 } else {
626 std::vector<std::byte> compressedData;
627 compressedData.resize(ZSTD_compressBound(entryData->size()));
628 auto compressedSize = ZSTD_compress_usingCDict(cCtx.get(), compressedData.data(), compressedData.size(), entryData->data(), entryData->size(), cDict.get());
629 if (ZSTD_isError(compressedSize) || compressedData.size() < compressedSize) {
630 return false;
631 }
632 stream.write(std::span{compressedData.data(), compressedSize});
633 entry->compressedLength = compressedSize;
634 }
635#endif
636 } else {
637 // The entry will be added to the directory VPK
638 entry->offset = dirVPKEntryData.size();
639#ifndef VPKPP_SUPPORT_VPK_V54
640 dirVPKEntryData.insert(dirVPKEntryData.end(), entryData->data(), entryData->data() + entryData->size());
641#else
642 if (!this->hasCompression() || path == this->getTruncatedFilestem() + ".dict") {
643 dirVPKEntryData.insert(dirVPKEntryData.end(), entryData->data(), entryData->data() + entryData->size());
644 } else {
645 std::vector<std::byte> compressedData;
646 compressedData.resize(ZSTD_compressBound(entryData->size()));
647 auto compressedSize = ZSTD_compress_usingCDict(cCtx.get(), compressedData.data(), compressedData.size(), entryData->data(), entryData->size(), cDict.get());
648 if (ZSTD_isError(compressedSize) || compressedData.size() < compressedSize) {
649 return false;
650 }
651 dirVPKEntryData.insert(dirVPKEntryData.end(), compressedData.data(), compressedData.data() + compressedSize);
652 entry->compressedLength = compressedSize;
653 }
654#endif
655 }
656
657 // Clear flags
658 entry->flags = 0;
659 }
660
661 outDir.write(std::filesystem::path{path}.stem().string());
662 outDir.write(entry->crc32);
663 outDir.write<uint16_t>(entry->extraData.size());
664 outDir.write<uint16_t>(entry->archiveIndex);
665 outDir.write<uint32_t>(entry->offset);
666 outDir.write<uint32_t>(entry->length - entry->extraData.size());
667
668 if (this->hasCompression()) {
669 outDir.write<uint32_t>(entry->compressedLength - entry->extraData.size());
670 }
671
672 outDir.write(VPK_ENTRY_TERM);
673
674 if (!entry->extraData.empty()) {
675 outDir.write(entry->extraData);
676 }
677
678 if (callback) {
679 callback(path, *entry);
680 }
681 }
682 outDir.write('\0');
683 }
684 outDir.write('\0');
685 }
686 outDir.write('\0');
687
688 // Put files copied from the dir archive back
689 if (!dirVPKEntryData.empty()) {
690 outDir.write(dirVPKEntryData);
691 }
692
693 // Merge unbaked into baked entries
694 this->mergeUnbakedEntries();
695
696 // Calculate Header1
697 this->header1.treeSize = outDir.tell_out() - dirVPKEntryData.size() - this->getHeaderLength();
698
699 // Non-v1 stuff
700 if (this->hasExtendedHeader()) {
701 // Calculate hashes for all entries
702 this->md5Entries.clear();
703 if (options.vpk_generateMD5Entries) {
704 this->runForAllEntries([this](const std::string& path, const Entry& entry) {
705 const auto binData = this->readEntry(path);
706 if (!binData) {
707 return;
708 }
709 const MD5Entry md5Entry{
710 .archiveIndex = entry.archiveIndex,
711 .offset = static_cast<uint32_t>(entry.offset),
712 .length = static_cast<uint32_t>(entry.length - entry.extraData.size()),
713 .checksum = crypto::computeMD5(*binData),
714 };
715 this->md5Entries.push_back(md5Entry);
716 }, false);
717 }
718
719 // Calculate Header2
720 this->header2.fileDataSectionSize = dirVPKEntryData.size();
721 this->header2.archiveMD5SectionSize = this->md5Entries.size() * sizeof(MD5Entry);
722 this->header2.otherMD5SectionSize = 48;
723 this->header2.signatureSectionSize = 0;
724
725 // Calculate Footer2
726 CryptoPP::Weak::MD5 wholeFileChecksumMD5;
727 {
728 // Only the tree is updated in the file right now
729 wholeFileChecksumMD5.Update(reinterpret_cast<const CryptoPP::byte*>(&this->header1), sizeof(Header1));
730 wholeFileChecksumMD5.Update(reinterpret_cast<const CryptoPP::byte*>(&this->header2), sizeof(Header2));
731 }
732 {
733 outDir.seek_in(sizeof(Header1) + sizeof(Header2));
734 std::vector<std::byte> treeData = outDir.read_bytes(this->header1.treeSize);
735 wholeFileChecksumMD5.Update(reinterpret_cast<const CryptoPP::byte*>(treeData.data()), treeData.size());
736 this->footer2.treeChecksum = crypto::computeMD5(treeData);
737 }
738 if (!dirVPKEntryData.empty()) {
739 wholeFileChecksumMD5.Update(reinterpret_cast<const CryptoPP::byte*>(dirVPKEntryData.data()), dirVPKEntryData.size());
740 }
741 {
742 wholeFileChecksumMD5.Update(reinterpret_cast<const CryptoPP::byte*>(this->md5Entries.data()), this->md5Entries.size() * sizeof(MD5Entry));
743 CryptoPP::Weak::MD5 md5EntriesChecksumMD5;
744 md5EntriesChecksumMD5.Update(reinterpret_cast<const CryptoPP::byte*>(this->md5Entries.data()), this->md5Entries.size() * sizeof(MD5Entry));
745 md5EntriesChecksumMD5.Final(reinterpret_cast<CryptoPP::byte*>(this->footer2.md5EntriesChecksum.data()));
746 }
747 wholeFileChecksumMD5.Update(reinterpret_cast<const CryptoPP::byte*>(this->footer2.treeChecksum.data()), this->footer2.treeChecksum.size());
748 wholeFileChecksumMD5.Update(reinterpret_cast<const CryptoPP::byte*>(this->footer2.md5EntriesChecksum.data()), this->footer2.md5EntriesChecksum.size());
749 wholeFileChecksumMD5.Final(reinterpret_cast<CryptoPP::byte*>(this->footer2.wholeFileChecksum.data()));
750
751 // We can't recalculate the signature without the private key
752 this->footer2.publicKey.clear();
753 this->footer2.signature.clear();
754 }
755
756 // Ancient crap VPK with no header
757 if (this->header1.version == 0) {
758 PackFile::setFullFilePath(outputDir);
759 return true;
760 }
761
762 // Write new headers
763 outDir.seek_out(0);
764 outDir.write(this->header1);
765
766 // MD5 hashes, file signature
767 if (!this->hasExtendedHeader()) {
768 PackFile::setFullFilePath(outputDir);
769 return true;
770 }
771
772 outDir.write(this->header2);
773
774 // Add MD5 hashes
775 outDir.seek_out_u(sizeof(Header1) + sizeof(Header2) + this->header1.treeSize + dirVPKEntryData.size());
776 outDir.write(this->md5Entries);
777 outDir.write(this->footer2.treeChecksum);
778 outDir.write(this->footer2.md5EntriesChecksum);
779 outDir.write(this->footer2.wholeFileChecksum);
780
781 // The signature section is not present
782 PackFile::setFullFilePath(outputDir);
783 return true;
784}
785
786std::string VPK::getTruncatedFilestem() const {
787 std::string filestem = this->getFilestem();
788 // This indicates it's a dir VPK, but some people ignore this convention...
789 if (filestem.length() >= 4 && filestem.substr(filestem.length() - 4) == (::isFPX(this) ? FPX_DIR_SUFFIX : VPK_DIR_SUFFIX)) {
790 filestem = filestem.substr(0, filestem.length() - 4);
791 }
792 return filestem;
793}
794
799
800VPK::operator std::string() const {
801 return PackFile::operator std::string() + std::format(" | Version v{}", this->header1.version);
802}
803
804bool VPK::generateKeyPairFiles(const std::string& name) {
805 const auto [privateKey, publicKey] = crypto::computeSHA256KeyPair(1024);
806 {
807 auto privateKeyPath = name + ".privatekey.vdf";
808 FileStream stream{privateKeyPath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
809
810 std::string output;
811 // Template size, remove %s and %s, add key sizes, add null terminator size
812 output.resize(VPK_KEYPAIR_PRIVATE_KEY_TEMPLATE.size() - 4 + privateKey.size() + publicKey.size() + 1);
813 if (std::sprintf(output.data(), VPK_KEYPAIR_PRIVATE_KEY_TEMPLATE.data(), privateKey.data(), publicKey.data()) < 0) {
814 return false;
815 }
816 output.pop_back();
817 stream.write(output, false);
818 }
819 {
820 auto publicKeyPath = name + ".publickey.vdf";
821 FileStream stream{publicKeyPath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
822
823 std::string output;
824 // Template size, remove %s, add key size, add null terminator size
825 output.resize(VPK_KEYPAIR_PUBLIC_KEY_TEMPLATE.size() - 2 + publicKey.size() + 1);
826 if (std::sprintf(output.data(), VPK_KEYPAIR_PUBLIC_KEY_TEMPLATE.data(), publicKey.data()) < 0) {
827 return false;
828 }
829 output.pop_back();
830 stream.write(output, false);
831 }
832 return true;
833}
834
835bool VPK::sign(const std::string& filename_) {
836 if (!this->hasExtendedHeader() || !std::filesystem::exists(filename_) || std::filesystem::is_directory(filename_)) {
837 return false;
838 }
839
840 const KV1 fileKV{fs::readFileText(filename_)};
841
842 const auto privateKeyHex = fileKV["private_key"]["rsa_private_key"].getValue();
843 if (privateKeyHex.empty()) {
844 return false;
845 }
846 const auto publicKeyHex = fileKV["private_key"]["public_key"]["rsa_public_key"].getValue();
847 if (publicKeyHex.empty()) {
848 return false;
849 }
850
851 return this->sign(crypto::decodeHexString(privateKeyHex), crypto::decodeHexString(publicKeyHex));
852}
853
854bool VPK::sign(const std::vector<std::byte>& privateKey, const std::vector<std::byte>& publicKey) {
855 if (!this->hasExtendedHeader()) {
856 return false;
857 }
858
859 this->header2.signatureSectionSize = this->footer2.publicKey.size() + this->footer2.signature.size() + sizeof(uint32_t) * 2;
860 {
861 FileStream stream{std::string{this->getFilepath()}, FileStream::OPT_READ | FileStream::OPT_WRITE};
862 stream.seek_out(sizeof(Header1));
863 stream.write(this->header2);
864 }
865
866 auto dirFileBuffer = fs::readFileBuffer(std::string{this->getFilepath()});
867 if (dirFileBuffer.size() <= this->header2.signatureSectionSize) {
868 return false;
869 }
870 for (int i = 0; i < this->header2.signatureSectionSize; i++) {
871 dirFileBuffer.pop_back();
872 }
873 this->footer2.publicKey = publicKey;
874 this->footer2.signature = crypto::signDataWithSHA256PrivateKey(dirFileBuffer, privateKey);
875
876 {
877 FileStream stream{std::string{this->getFilepath()}, FileStream::OPT_READ | FileStream::OPT_WRITE};
878 stream.seek_out(this->getHeaderLength() + this->header1.treeSize + this->header2.fileDataSectionSize + this->header2.archiveMD5SectionSize + this->header2.otherMD5SectionSize);
879 stream.write(static_cast<uint32_t>(this->footer2.publicKey.size()));
880 stream.write(this->footer2.publicKey);
881 stream.write(static_cast<uint32_t>(this->footer2.signature.size()));
882 stream.write(this->footer2.signature);
883 }
884 return true;
885}
886
887uint32_t VPK::getVersion() const {
888 return this->header1.version;
889}
890
891void VPK::setVersion(uint32_t version) {
892 // Version must be supported, we cannot be an FPX, and version must be different
893 if ((version != 0 && version != 1 && version != 2 && version != 54) || ::isFPX(this) || version == this->header1.version) {
894 return;
895 }
896 this->header1.version = version;
897
898 // Clearing these isn't necessary, but might as well
899 this->header2 = {};
900 this->footer2 = {};
901 this->md5Entries.clear();
902}
903
904uint32_t VPK::getChunkSize() const {
905 return this->chunkSize;
906}
907
908void VPK::setChunkSize(uint32_t newChunkSize) {
909 this->chunkSize = newChunkSize;
910}
911
913 return this->header1.version == 2 || this->header1.version == 54;
914}
915
917 return this->header1.version == 54;
918}
919
920uint32_t VPK::getHeaderLength() const {
921 if (!this->hasExtendedHeader()) {
922 return sizeof(Header1);
923 }
924 return sizeof(Header1) + sizeof(Header2);
925}
constexpr uint32_t VPK_FLAG_REUSING_CHUNK
Runtime-only flag that indicates a file is going to be written to an existing archive file.
Definition VPK.cpp:28
std::string_view getValue() const
Get the value associated with the element.
Definition KV1.h:31
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 compressedLength
If the format supports compression, this is the compressed length.
Definition Entry.h:30
uint32_t archiveIndex
Which external archive this entry is in.
Definition Entry.h:23
uint32_t crc32
CRC32 checksum - 0 if unused.
Definition Entry.h:40
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
EntryCallbackBase< void > EntryCallback
Definition PackFile.h:38
virtual std::size_t removeDirectory(const std::string &dirName_)
Remove a directory.
Definition PackFile.cpp:351
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
std::vector< std::string > verifyEntryChecksumsUsingCRC32() const
Definition PackFile.cpp:656
void runForAllEntriesInternal(const std::function< void(const std::string &, Entry &)> &operation, bool includeUnbaked=true)
Definition PackFile.cpp:571
std::string getFilestem() const
/home/user/pak01_dir.vpk -> pak01_dir
Definition PackFile.cpp:630
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
std::string getTruncatedFilepath() const
/home/user/pak01_dir.vpk -> /home/user/pak01
Definition PackFile.cpp:617
void runForAllEntries(const EntryCallback &operation, bool includeUnbaked=true) const
Run a callback for each entry in the pack file.
Definition PackFile.cpp:529
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
virtual bool removeEntry(const std::string &path_)
Remove an entry.
Definition PackFile.cpp:334
std::string_view getFilepath() const
/home/user/pak01_dir.vpk
Definition PackFile.cpp:613
static std::optional< std::vector< std::byte > > readUnbakedEntry(const Entry &entry)
Definition PackFile.cpp:719
Footer2 footer2
Definition VPK.h:147
Attribute getSupportedEntryAttributes() const override
Returns a list of supported entry attributes Mostly for GUI programs that show entries and their meta...
Definition VPK.cpp:795
static std::unique_ptr< PackFile > create(const std::string &path, uint32_t version=2)
Create a new directory VPK file - should end in "_dir.vpk"! This is not enforced but STRONGLY recomme...
Definition VPK.cpp:53
std::size_t removeDirectory(const std::string &dirName_) override
Remove a directory.
Definition VPK.cpp:468
void setChunkSize(uint32_t newChunkSize)
Set the VPK chunk size in bytes (size of generated archives when baking).
Definition VPK.cpp:908
uint32_t getHeaderLength() const
Definition VPK.cpp:920
bool hasCompression() const
Definition VPK.cpp:916
bool verifyPackFileSignature() const override
Verify the file signature, returns true on success Will return true if there is no signature ability ...
Definition VPK.cpp:304
uint32_t getChunkSize() const
Get the VPK chunk size in bytes (size of generated archives when baking).
Definition VPK.cpp:904
std::vector< std::string > verifyEntryChecksums() const override
Verify the checksums of each file, if a file fails the check its path will be added to the vector If ...
Definition VPK.cpp:260
uint32_t getVersion() const
Returns 1 for v1, 2 for v2.
Definition VPK.cpp:887
uint32_t currentlyFilledChunkSize
Definition VPK.h:140
bool hasPackFileSignature() const override
Returns true if the file is signed.
Definition VPK.cpp:294
bool hasExtendedHeader() const
Definition VPK.cpp:912
static bool generateKeyPairFiles(const std::string &name)
Generate keypair files, which can be used to sign a VPK Input is a truncated file path,...
Definition VPK.cpp:804
std::vector< FreedChunk > freedChunks
Definition VPK.h:143
bool hasPackFileChecksum() const override
Returns true if the entire file has a checksum.
Definition VPK.cpp:264
bool verifyPackFileChecksum() const override
Verify the checksum of the entire file, returns true on success Will return true if there is no check...
Definition VPK.cpp:268
void setVersion(uint32_t version)
Change the version of the VPK. Valid values are 1 and 2.
Definition VPK.cpp:891
std::optional< std::vector< std::byte > > readEntry(const std::string &path_) const override
Try to read the entry's data to a bytebuffer.
Definition VPK.cpp:325
bool sign(const std::string &filename_)
Sign the VPK with the given private key KeyValues file. (See below comment).
Definition VPK.cpp:835
bool removeEntry(const std::string &filename_) override
Remove an entry.
Definition VPK.cpp:460
int32_t numArchives
Definition VPK.h:139
Header2 header2
Definition VPK.h:146
std::string getTruncatedFilestem() const override
/home/user/pak01_dir.vpk -> pak01
Definition VPK.cpp:786
std::vector< MD5Entry > md5Entries
Definition VPK.h:149
void addEntryInternal(Entry &entry, const std::string &path, std::vector< std::byte > &buffer, EntryOptions options) override
Definition VPK.cpp:404
static std::unique_ptr< PackFile > open(const std::string &path, const EntryCallback &callback=nullptr)
Open a VPK file.
Definition VPK.cpp:82
static std::unique_ptr< PackFile > openInternal(const std::string &path, const EntryCallback &callback=nullptr)
Definition VPK.cpp:100
uint32_t chunkSize
Definition VPK.h:141
Header1 header1
Definition VPK.h:145
Definition DMX.h:13
std::vector< std::byte > signDataWithSHA256PrivateKey(std::span< const std::byte > buffer, std::span< const std::byte > privateKey)
Definition RSA.cpp:36
std::array< std::byte, 16 > computeMD5(std::span< const std::byte > buffer)
Definition MD5.cpp:8
bool verifySHA256PublicKey(std::span< const std::byte > buffer, std::span< const std::byte > publicKey, std::span< const std::byte > signature)
Definition RSA.cpp:30
std::pair< std::string, std::string > computeSHA256KeyPair(uint16_t size=2048)
Definition RSA.cpp:9
uint32_t computeCRC32(std::span< const std::byte > buffer)
Definition CRC32.cpp:7
std::vector< std::byte > decodeHexString(std::string_view hex)
Definition String.cpp:10
std::string readFileText(const std::filesystem::path &filepath, std::size_t startOffset=0)
Definition FS.cpp:22
std::vector< std::byte > readFileBuffer(const std::filesystem::path &filepath, std::size_t startOffset=0)
Definition FS.cpp:7
std::string padNumber(int64_t number, int width)
Definition String.cpp:222
void normalizeSlashes(std::string &path, bool stripSlashPrefix=false, bool stripSlashSuffix=true)
Definition String.cpp:226
bool matches(std::string_view in, std::string_view search)
A very basic regex-like pattern checker for ASCII strings.
Definition String.cpp:25
constexpr uint32_t VPK_SIGNATURE
Definition VPK.h:11
constexpr std::string_view VPK_DIR_SUFFIX
Definition VPK.h:14
constexpr std::string_view VPK_KEYPAIR_PUBLIC_KEY_TEMPLATE
Definition VPK.h:17
Attribute
Definition Attribute.h:7
constexpr uint16_t VPK_ENTRY_TERM
Definition VPK.h:13
constexpr std::string_view FPX_DIR_SUFFIX
Definition FPX.h:10
constexpr std::string_view VPK_EXTENSION
Definition VPK.h:15
constexpr std::string_view VPK_KEYPAIR_PRIVATE_KEY_TEMPLATE
Definition VPK.h:18
constexpr std::string_view FPX_EXTENSION
Definition FPX.h:11
constexpr uint16_t VPK_DIR_INDEX
Definition VPK.h:12
constexpr uint16_t VPK_MAX_PRELOAD_BYTES
Maximum preload data size in bytes.
Definition VPK.h:21
bool vpk_generateMD5Entries
VPK - Generate MD5 hashes for each file (VPK v2 only).
Definition Options.h:31
int16_t zip_compressionStrength
BSP/VPK/ZIP - Compression strength.
Definition Options.h:25
uint16_t vpk_preloadBytes
VPK - The amount in bytes of the file to preload. Maximum is controlled by VPK_MAX_PRELOAD_BYTES (for...
Definition Options.h:42
bool vpk_saveToDirectory
VPK - Save this entry to the directory VPK.
Definition Options.h:45