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