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