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