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 reader.read(pck->header.godotVersionMajor);
75 reader.read(pck->header.godotVersionMinor);
76 reader.read(pck->header.godotVersionPatch);
77
78 pck->header.flags = FLAG_DIR_NONE;
79 std::size_t extraEntryContentsOffset = 0;
80 if (pck->header.packVersion > 1) {
81 pck->header.flags = reader.read<FlagsDirV2>();
82 extraEntryContentsOffset = reader.read<uint64_t>();
83 }
84
85 if (pck->header.flags & FLAG_DIR_ENCRYPTED) {
86 // File directory is encrypted
87 return nullptr;
88 }
89 if (pck->header.flags & FLAG_DIR_RELATIVE_FILE_DATA) {
90 extraEntryContentsOffset += pck->startOffset;
91 pck->header.flags = static_cast<FlagsDirV2>(pck->header.flags & ~FLAG_DIR_RELATIVE_FILE_DATA);
92 }
93
94 // Reserved
95 reader.skip_in<int32_t>(16);
96
97 // Directory
98 auto fileCount = reader.read<uint32_t>();
99 for (uint32_t i = 0; i < fileCount; i++) {
100 Entry entry = createNewEntry();
101
102 auto entryPath = pck->cleanEntryPath(reader.read_string(reader.read<uint32_t>()));
103 if (entryPath.starts_with(PCK_PATH_PREFIX)) {
104 entryPath = entryPath.substr(PCK_PATH_PREFIX.length());
105 }
106
107 entry.offset = reader.read<uint64_t>() + extraEntryContentsOffset;
108 entry.length = reader.read<uint64_t>();
109 entry.extraData = reader.read_bytes(16);
110
111 if (pck->header.packVersion > 1) {
112 entry.flags = reader.read<uint32_t>();
113 if (entry.flags & FLAG_FILE_REMOVED) {
114 continue;
115 }
116 }
117
118 pck->entries.emplace(entryPath, entry);
119
120 if (callback) {
121 callback(entryPath, entry);
122 }
123 }
124
125 // File data
126 pck->dataOffset = reader.tell_in();
127
128 return packFile;
129}
130
131std::optional<std::vector<std::byte>> PCK::readEntry(const std::string& path_) const {
132 auto path = this->cleanEntryPath(path_);
133 auto entry = this->findEntry(path);
134 if (!entry) {
135 return std::nullopt;
136 }
137 if (entry->unbaked) {
138 return readUnbakedEntry(*entry);
139 }
140
141 // It's baked into the file on disk
142 if (entry->flags & FLAG_FILE_ENCRYPTED) {
143 // File is encrypted
144 return std::nullopt;
145 }
146
147 FileStream stream{this->fullFilePath};
148 if (!stream) {
149 return std::nullopt;
150 }
151 stream.seek_in_u(entry->offset);
152 return stream.read_bytes(entry->length);
153}
154
155void PCK::addEntryInternal(Entry& entry, const std::string& path, std::vector<std::byte>& buffer, EntryOptions options) {
156 entry.length = buffer.size();
157
158 const auto md5 = crypto::computeMD5(buffer);
159 entry.extraData = {md5.begin(), md5.end()};
160
161 // Offset will be reset when it's baked
162 entry.offset = 0;
163}
164
165bool PCK::bake(const std::string& outputDir_, BakeOptions options, const EntryCallback& callback) {
166 // Get the proper file output folder
167 std::string outputDir = this->getBakeOutputDir(outputDir_);
168 std::string outputPath = outputDir + '/' + this->getFilename();
169
170 // Reconstruct data for ease of access
171 std::vector<std::pair<std::string, Entry*>> entriesToBake;
172 this->runForAllEntriesInternal([&entriesToBake](const std::string& path, Entry& entry) {
173 entriesToBake.emplace_back(path, &entry);
174 });
175
176 // Read data before overwriting, we don't know if we're writing to ourself
177 std::vector<std::byte> fileData;
178 for (auto& [path, entry] : entriesToBake) {
179 if (auto binData = this->readEntry(path)) {
180 entry->offset = fileData.size();
181
182 fileData.insert(fileData.end(), binData->begin(), binData->end());
183 const auto padding = math::paddingForAlignment(PCK_FILE_DATA_PADDING, static_cast<int>(entry->length));
184 for (int i = 0; i < padding; i++) {
185 fileData.push_back(static_cast<std::byte>(0));
186 }
187 } else {
188 entry->offset = 0;
189 entry->length = 0;
190 }
191 }
192
193 // If this is an embedded pck, read the executable data first
194 std::vector<std::byte> exeData;
195 if (this->startOffset > 0) {
196 FileStream stream{this->fullFilePath};
197 if (!stream) {
198 return false;
199 }
200 stream.seek_in(0);
201 exeData = stream.read_bytes(this->startOffset);
202 }
203
204 // Write data
205 {
206 FileStream stream{outputPath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
207 stream.seek_out(0);
208
209 if (!exeData.empty()) {
210 stream.write(exeData);
211 }
212
213 // Signature
214 stream.write(PCK_SIGNATURE);
215
216 // Header
217 stream.write(this->header.packVersion);
218 stream.write(this->header.godotVersionMajor);
219 stream.write(this->header.godotVersionMinor);
220 stream.write(this->header.godotVersionPatch);
221
222 if (this->header.packVersion > 1) {
223 stream.write(this->header.flags);
224 stream.write<uint64_t>(0);
225 }
226
227 // Reserved
228 stream.write(std::array<int32_t, 16>{});
229
230 // Directory start
231 stream.write(static_cast<uint32_t>(entriesToBake.size()));
232
233 // Dry-run to get the length of the directory section
234 this->dataOffset = stream.tell_out();
235 for (const auto& path : std::views::keys(entriesToBake)) {
236 const auto entryPath = std::string{PCK_PATH_PREFIX} + path;
237 const auto padding = math::paddingForAlignment(PCK_DIRECTORY_STRING_PADDING, static_cast<int>(entryPath.length()));
238 this->dataOffset +=
239 sizeof(uint32_t) + // Path length
240 entryPath.length() + padding + // Path
241 (sizeof(std::size_t) * 2) + // Offset, Length
242 (sizeof(std::byte) * 16); // MD5
243
244 if (this->header.packVersion > 1) {
245 this->dataOffset += sizeof(uint32_t); // Flags
246 }
247 }
248
249 // Directory
250 for (const auto& [path, entry] : entriesToBake) {
251 const auto entryPath = std::string{PCK_PATH_PREFIX} + path;
252 const auto padding = math::paddingForAlignment(PCK_DIRECTORY_STRING_PADDING, static_cast<int>(entryPath.length()));
253 stream.write(static_cast<uint32_t>(entryPath.length() + padding));
254 stream.write(entryPath, false, entryPath.length() + padding);
255
256 entry->offset += this->dataOffset;
257 stream.write(entry->offset);
258 stream.write(entry->length);
259 stream.write(entry->extraData);
260
261 if (this->header.packVersion > 1) {
262 stream.write(entry->flags);
263 }
264
265 if (callback) {
266 callback(path, *entry);
267 }
268 }
269
270 // File data
271 stream.write(fileData);
272
273 // Write offset to start
274 if (this->startOffset > 0) {
275 stream.write(stream.tell_out() - startOffset);
276 stream.write(PCK_SIGNATURE);
277 }
278 }
279
280 // Clean up
281 this->mergeUnbakedEntries();
282 PackFile::setFullFilePath(outputDir);
283 return true;
284}
285
287 using enum Attribute;
288 return LENGTH | PCK_MD5;
289}
290
291PCK::operator std::string() const {
292 auto out = PackFile::operator std::string() +
293 " | Version v" + std::to_string(this->header.packVersion) +
294 " | Godot Version v" + std::to_string(this->header.godotVersionMajor) + '.' + std::to_string(this->header.godotVersionMinor) + '.' + std::to_string(this->header.godotVersionPatch);
295 if (this->startOffset > 0) {
296 out += " | Embedded";
297 }
298 if (this->header.flags & FLAG_DIR_ENCRYPTED) {
299 out += " | Encrypted";
300 }
301 return out;
302}
303
304uint32_t PCK::getVersion() const {
305 return this->header.packVersion;
306}
307
308void PCK::setVersion(uint32_t version) {
309 if (version == 1 || version == 2) {
310 this->header.packVersion = version;
311 }
312}
313
314std::tuple<uint32_t, uint32_t, uint32_t> PCK::getGodotVersion() const {
316}
317
318void PCK::setGodotVersion(uint32_t major, uint32_t minor, uint32_t patch) {
319 this->header.godotVersionMajor = major;
320 this->header.godotVersionMinor = minor;
321 this->header.godotVersionPatch = patch;
322}
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:318
void addEntryInternal(Entry &entry, const std::string &path, std::vector< std::byte > &buffer, EntryOptions options) override
Definition: PCK.cpp:155
std::size_t startOffset
Definition: PCK.h:77
std::tuple< uint32_t, uint32_t, uint32_t > getGodotVersion() const
Definition: PCK.cpp:314
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:131
uint32_t getVersion() const
Returns 1 for v1, 2 for v2.
Definition: PCK.cpp:304
Attribute getSupportedEntryAttributes() const override
Returns a list of supported entry attributes Mostly for GUI programs that show entries and their meta...
Definition: PCK.cpp:286
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:165
@ 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:308
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:30
void mergeUnbakedEntries()
Definition: PackFile.cpp:656
std::optional< Entry > findEntry(const std::string &path_, bool includeUnbaked=true) const
Try to find an entry given the file path.
Definition: PackFile.cpp:163
std::string fullFilePath
Definition: PackFile.h:219
void runForAllEntriesInternal(const std::function< void(const std::string &, Entry &)> &operation, bool includeUnbaked=true)
Definition: PackFile.cpp:541
std::string getFilename() const
/home/user/pak01_dir.vpk -> pak01_dir.vpk
Definition: PackFile.cpp:591
std::string getBakeOutputDir(const std::string &outputDir) const
Definition: PackFile.cpp:640
void setFullFilePath(const std::string &outputDir)
Definition: PackFile.cpp:672
std::string cleanEntryPath(const std::string &path) const
Definition: PackFile.cpp:677
static Entry createNewEntry()
Definition: PackFile.cpp:686
static std::optional< std::vector< std::byte > > readUnbakedEntry(const Entry &entry)
Definition: PackFile.cpp:690
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:57
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