SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
PAK.cpp
Go to the documentation of this file.
1// ReSharper disable CppRedundantQualifier
2
3#include <vpkpp/format/PAK.h>
4
5#include <filesystem>
6
7#include <FileStream.h>
8#include <sourcepp/String.h>
9
10using namespace sourcepp;
11using namespace vpkpp;
12
13namespace {
14
15std::vector<std::byte> decompressDaikatanaEntryData(std::span<const std::byte> inputData_) {
16 BufferStreamReadOnly inStream{inputData_};
17
18 std::vector<std::byte> out;
19 BufferStream outStream{out};
20
21 while (inStream.tell() < inStream.size()) {
22 if (
23 const auto code = inStream.read<uint8_t>();
24 code < 64
25 ) {
26 // Uncompressed block
27 outStream << inStream.read_span<std::byte>(code + 1);
28 } else if (code < 128) {
29 // Run length zero block
30 outStream.pad(code - 62);
31 } else if (code < 192) {
32 // Run length encoded block
33 const auto value = inStream.read<uint8_t>();
34 outStream.pad(code - 126, value);
35 } else if (code < 254) {
36 // Reference previous data
37 const auto offset = inStream.read<uint8_t>();
38 outStream << outStream.at_bytes_u(code - 190, outStream.size() - offset - 2);
39 } else if (code == 254) {
40 // ReSharper disable once CppRedundantControlFlowJump
41 continue;
42 } else {
43 break;
44 }
45 }
46
47 out.resize(outStream.size());
48 return out;
49}
50
51} // namespace
52
53std::unique_ptr<PackFile> PAK::create(const std::string& path, Type type) {
54 {
55 FileStream stream{path, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
56 switch (type) {
57 case Type::PAK:
58 case Type::DAIKATANA:
59 stream << PAK_SIGNATURE;
60 break;
61 case Type::SIN:
62 stream << PAK_SIN_SIGNATURE;
63 break;
66 break;
67 case Type::HROT:
68 stream << PAK_HROT_SIGNATURE;
69 break;
70 }
71 stream
72 .write<uint32_t>(0)
73 .write<uint32_t>(0);
74 }
75 return PAK::open(path);
76}
77
78std::unique_ptr<PackFile> PAK::open(const std::string& path, const EntryCallback& callback) {
79 if (!std::filesystem::exists(path)) {
80 // File does not exist
81 return nullptr;
82 }
83
84 auto* pak = new PAK{path};
85 auto packFile = std::unique_ptr<PackFile>(pak);
86
87 FileStream reader{pak->fullFilePath};
88 reader.seek_in(0);
89
90 // Get total file size
91 const auto pakFileLength = std::filesystem::file_size(path);
92
93 if (const auto signature = reader.read<uint32_t>(); signature == PAK_SIGNATURE) {
94 pak->type = Type::PAK;
95 } else if (signature == PAK_SIN_SIGNATURE) {
96 pak->type = Type::SIN;
97 } else if (signature == PAK_SIN_RELOADED_SIGNATURE) {
98 pak->type = Type::SIN_RELOADED;
99 } else if (signature == PAK_HROT_SIGNATURE) {
100 pak->type = Type::HROT;
101 } else {
102 // File is not a PAK
103 return nullptr;
104 }
105
106 if (pak->type == Type::SIN_RELOADED) {
107 reader.skip_in<int32_t>(); // reserved
108 const auto directoryOffset = reader.read<uint64_t>();
109 const auto namesOffset = reader.read<uint64_t>();
110 const auto fileCount = reader.read<uint32_t>();
111 const auto namesLength = reader.read<uint32_t>();
112
113 // read names buffer
114 std::string namesBuffer;
115 reader.seek_in_u(namesOffset);
116 reader.read(namesBuffer, namesLength, false);
117
118 // read directory
119 reader.seek_in_u(directoryOffset);
120 for (uint32_t i = 0; i < fileCount; i++) {
121 Entry entry = createNewEntry();
122
123 const auto filePos = reader.read<uint64_t>();
124 const auto fileLen = reader.read<uint32_t>();
125 const auto nameOfs = reader.read<uint32_t>();
126
127 auto entryPath = pak->cleanEntryPath(namesBuffer.c_str() + nameOfs);
128
129 entry.offset = filePos;
130 entry.length = fileLen;
131
132 pak->entries.emplace(entryPath, entry);
133
134 if (callback) {
135 callback(entryPath, entry);
136 }
137 }
138 } else {
139 // Directory offset and size
140 const auto directoryOffset = reader.read<uint32_t>();
141 const auto directorySize = reader.read<uint32_t>();
142
143 // Check if it's a Daikatana PAK
144 bool isDaikatana = directorySize % 64 != 0;
145 const auto entrySize = sizeof(uint32_t) * 2 + pak->getFilenameLength();
146 const auto dkEntrySize = sizeof(uint32_t) * 4 + pak->getFilenameLength();
147
148 // Check again if it's a Daikatana PAK for this edge case
149 if (directorySize % 576 == 0) {
150 const auto numEntries = directorySize / dkEntrySize;
151
152 for (size_t i = 0; i < numEntries; i++) {
153 // Check filename
154 auto entryPath = pak->cleanEntryPath(reader.read_string(pak->getFilenameLength()));
155
156 size_t j;
157 for (j = 0; j < pak->getFilenameLength(); j++) {
158 if (entryPath[j] == '\0' || !string::isAlphaNumeric(entryPath[j])) {
159 break;
160 }
161 }
162 if (j == 0 || !string::isAlphaNumeric(entryPath)) {
163 isDaikatana = false;
164 break;
165 }
166
167 const auto offset = reader.read<uint32_t>();
168 auto length = reader.read<uint32_t>();
169
170 // check compression
171 const auto compressedLength = reader.read<uint32_t>();
172 const auto compressedType = reader.read<uint32_t>();
173 if (compressedType != 0 && compressedType != 1) {
174 isDaikatana = false;
175 break;
176 }
177
178 // check offset
179 if (compressedType == 1) {
180 length = compressedLength;
181 }
182 if (offset + length > pakFileLength) {
183 isDaikatana = false;
184 break;
185 }
186 }
187 }
188
189 // Set the type if we truly think it's a Daikatana PAK
190 if (isDaikatana) {
191 pak->type = Type::DAIKATANA;
192 }
193
194 // File count
195 const auto fileCount = directorySize / (isDaikatana ? dkEntrySize : entrySize);
196
197 reader.seek_in(directoryOffset);
198 for (uint32_t i = 0; i < fileCount; i++) {
199 Entry entry = createNewEntry();
200
201 auto entryPath = pak->cleanEntryPath(reader.read_string(pak->getFilenameLength()));
202
203 entry.offset = reader.read<uint32_t>();
204 entry.length = reader.read<uint32_t>();
205
206 // Read extra data
207 if (isDaikatana) {
208 const auto compressedLength = reader.read<uint32_t>();
209 if (
210 const auto compressedType = reader.read<uint32_t>();
211 compressedType == 1
212 ) {
213 entry.compressedLength = compressedLength;
214 }
215 }
216
217 pak->entries.emplace(entryPath, entry);
218
219 if (callback) {
220 callback(entryPath, entry);
221 }
222 }
223 }
224
225 return packFile;
226}
227
228std::optional<std::vector<std::byte>> PAK::readEntry(const std::string& path_) const {
229 const auto path = this->cleanEntryPath(path_);
230 const auto entry = this->findEntry(path);
231 if (!entry) {
232 return std::nullopt;
233 }
234 if (entry->unbaked) {
235 return readUnbakedEntry(*entry);
236 }
237
238 // It's baked into the file on disk
239 FileStream stream{this->fullFilePath};
240 if (!stream) {
241 return std::nullopt;
242 }
243 stream.seek_in_u(entry->offset);
244
245 // Check if we need to decompress the Daikatana PAK file
246 if (this->type == Type::DAIKATANA && entry->compressedLength > 0) {
247 return ::decompressDaikatanaEntryData(stream.read_bytes(entry->compressedLength));
248 }
249
250 return stream.read_bytes(entry->length);
251}
252
253void PAK::addEntryInternal(Entry& entry, const std::string&, std::vector<std::byte>& buffer, EntryOptions) {
254 entry.length = buffer.size();
255
256 // Offset will be reset when it's baked
257 entry.offset = 0;
258}
259
260bool PAK::bake(const std::string& outputDir_, BakeOptions, const EntryCallback& callback) {
261 // todo: Daikatana PAKs are not currently handled
262 if (this->type == Type::DAIKATANA) {
263 return false;
264 }
265
266 // Get the proper file output folder
267 const std::string outputDir = this->getBakeOutputDir(outputDir_);
268 const std::string outputPath = outputDir + '/' + this->getFilename();
269
270 // Reconstruct data for ease of access
271 std::vector<std::pair<std::string, Entry*>> entriesToBake;
272 this->runForAllEntriesInternal([&entriesToBake](const std::string& path, Entry& entry) {
273 entriesToBake.emplace_back(path, &entry);
274 });
275
276 // Read data before overwriting, we don't know if we're writing to ourself
277 std::vector<std::byte> fileData;
278 std::vector<std::byte> nameData;
279 std::vector<uint32_t> nameOffsets;
280 for (auto& [path, entry] : entriesToBake) {
281 if (auto binData = this->readEntry(path)) {
282 entry->offset = fileData.size();
283
284 fileData.insert(fileData.end(), binData->begin(), binData->end());
285
286 if (this->type == Type::SIN_RELOADED) {
287 const auto oldSize = nameData.size();
288 nameData.resize(oldSize + path.size() + 1);
289 std::memcpy(&nameData[oldSize], path.data(), path.size());
290 nameOffsets.push_back(oldSize);
291 }
292 } else {
293 entry->offset = 0;
294 entry->length = 0;
295 }
296 }
297
298 {
299 FileStream stream{outputPath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
300 stream.seek_out(0);
301
302 // Signature
303 stream.write<uint32_t>(this->getSignature());
304
305 if (this->type == Type::SIN_RELOADED) {
306 // Basically a version identifier, but not used...?
307 stream.write<int32_t>(0);
308
309 // Offset of directory
310 static constexpr auto HEADER_OFFSET = sizeof(uint32_t) * 3 + sizeof(uint64_t) * 2;
311 stream.write<uint64_t>(HEADER_OFFSET + fileData.size());
312
313 // Offset of names
314 const auto numFiles = entriesToBake.size();
315 const auto directorySize = (sizeof(uint64_t) + sizeof(uint32_t) * 2) * numFiles;
316 stream.write<uint64_t>(HEADER_OFFSET + fileData.size() + directorySize);
317
318 // Number of files
319 stream.write<uint32_t>(numFiles);
320
321 // Size of names
322 stream.write<uint32_t>(nameData.size());
323
324 // File data
325 stream.write(fileData);
326
327 // Directory
328 size_t i = 0;
329 for (const auto& [path, entry] : entriesToBake) {
330 stream
331 .write<uint64_t>(entry->offset + HEADER_OFFSET)
332 .write<uint32_t>(entry->length)
333 .write<uint32_t>(nameOffsets[i++]);
334
335 if (callback) {
336 callback(path, *entry);
337 }
338 }
339
340 // Names
341 stream.write(nameData);
342 } else {
343 // Offset and size of directory
344 static constexpr auto HEADER_OFFSET = sizeof(uint32_t) * 3;
345 stream
346 .write<uint32_t>(HEADER_OFFSET + fileData.size())
347 .write<uint32_t>(entriesToBake.size() * (sizeof(uint32_t) * 2 + this->getFilenameLength()));
348
349 // File data
350 stream.write(fileData);
351
352 // Directory
353 for (const auto& [path, entry] : entriesToBake) {
354 stream
355 .write(path, true, this->getFilenameLength())
356 .write<uint32_t>(entry->offset + HEADER_OFFSET)
357 .write<uint32_t>(entry->length);
358
359 if (callback) {
360 callback(path, *entry);
361 }
362 }
363 }
364 }
365
366 // Clean up
367 this->mergeUnbakedEntries();
368 PackFile::setFullFilePath(outputDir);
369 return true;
370}
371
373 using enum Attribute;
374 return LENGTH;
375}
376
378 return this->type;
379}
380
381void PAK::setType(Type type_) {
382 this->type = type_;
383}
384
385uint32_t PAK::getSignature() const {
386 switch (this->type) {
387 case Type::PAK:
388 case Type::DAIKATANA:
389 return PAK_SIGNATURE;
390 case Type::SIN:
391 return PAK_SIN_SIGNATURE;
394 case Type::HROT:
395 return PAK_HROT_SIGNATURE;
396 }
397 return PAK_SIGNATURE;
398}
399
400uint8_t PAK::getFilenameLength() const {
401 switch (this->type) {
402 case Type::PAK:
403 case Type::DAIKATANA:
405 case Type::SIN:
408 return 0; // variable
409 case Type::HROT:
411 }
413}
This class represents the metadata that a file has inside a PackFile.
Definition Entry.h:14
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
uint64_t length
Length in bytes (in formats with compression, this is the uncompressed length).
Definition Entry.h:26
static std::unique_ptr< PackFile > create(const std::string &path, Type type=Type::PAK)
Create a PAK file.
Definition PAK.cpp:53
static std::unique_ptr< PackFile > open(const std::string &path, const EntryCallback &callback=nullptr)
Open a PAK file.
Definition PAK.cpp: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 PAK.cpp:228
Type getType() const
Definition PAK.cpp:377
Attribute getSupportedEntryAttributes() const override
Returns a list of supported entry attributes Mostly for GUI programs that show entries and their meta...
Definition PAK.cpp:372
uint8_t getFilenameLength() const
Definition PAK.cpp:400
uint32_t getSignature() const
Definition PAK.cpp:385
Type type
Definition PAK.h:60
void addEntryInternal(Entry &entry, const std::string &path, std::vector< std::byte > &buffer, EntryOptions options) override
Definition PAK.cpp:253
void setType(Type type_)
Definition PAK.cpp:381
EntryCallbackBase< void > EntryCallback
Definition PackFile.h:40
void mergeUnbakedEntries()
Definition PackFile.cpp:692
std::optional< Entry > findEntry(const std::string &path_, bool includeUnbaked=true) const
Try to find an entry given the file path.
Definition PackFile.cpp:179
std::string fullFilePath
Definition PackFile.h:233
void runForAllEntriesInternal(const std::function< void(const std::string &, Entry &)> &operation, bool includeUnbaked=true)
Definition PackFile.cpp:578
bool bake()
If output folder is an empty string, it will overwrite the original.
Definition PackFile.cpp:376
std::string getFilename() const
/home/user/pak01_dir.vpk -> pak01_dir.vpk
Definition PackFile.cpp:628
std::string getBakeOutputDir(const std::string &outputDir) const
Definition PackFile.cpp:677
void setFullFilePath(const std::string &outputDir)
Definition PackFile.cpp:708
std::string cleanEntryPath(const std::string &path) const
Definition PackFile.cpp:713
static Entry createNewEntry()
Definition PackFile.cpp:722
static std::optional< std::vector< std::byte > > readUnbakedEntry(const Entry &entry)
Definition PackFile.cpp:726
bool isAlphaNumeric(char c)
If a char is a letter (a-z, A-Z) or a numerical character (0-9).
Definition String.cpp:47
constexpr auto PAK_SIN_RELOADED_SIGNATURE
Definition PAK.h:17
constexpr auto PAK_HROT_SIGNATURE
Definition PAK.h:20
constexpr auto PAK_SIN_SIGNATURE
Definition PAK.h:15
constexpr auto PAK_SIGNATURE
Definition PAK.h:12
Attribute
Definition Attribute.h:7
constexpr uint8_t PAK_SIN_FILENAME_MAX_SIZE
Definition PAK.h:14
constexpr uint8_t PAK_HROT_FILENAME_MAX_SIZE
Definition PAK.h:19
constexpr uint8_t PAK_FILENAME_MAX_SIZE
Definition PAK.h:11