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