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