SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
ZIP.cpp
Go to the documentation of this file.
1#include <vpkpp/format/ZIP.h>
2
3#include <cstring>
4#include <ctime>
5#include <filesystem>
6
7#include <mz.h>
8#include <mz_os.h>
9#include <mz_strm.h>
10#include <mz_strm_os.h>
11#include <mz_zip.h>
12#include <mz_zip_rw.h>
13
14#include <FileStream.h>
16#include <sourcepp/String.h>
17
18using namespace sourcepp;
19using namespace vpkpp;
20
21ZIP::ZIP(const std::string& fullFilePath_)
22 : PackFile(fullFilePath_)
23 , tempZIPPath((std::filesystem::temp_directory_path() / (string::generateUUIDv4() + ".zip")).string()) {}
24
26 this->closeZIP();
27}
28
29std::unique_ptr<PackFile> ZIP::create(const std::string& path) {
30 {
31 FileStream stream{path, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
32
33 // I just created an empty zip with Windows and pasted the result because minizip-ng wasn't behaving
34 stream
35 .write('P')
36 .write('K')
37 .write<uint8_t>(5)
38 .write<uint8_t>(6)
39 .write(std::array<uint16_t, 4>{})
40 .write(std::array<uint32_t, 2>{})
41 .write<uint16_t>(0);
42 }
43 return ZIP::open(path);
44}
45
46std::unique_ptr<PackFile> ZIP::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* zip = new ZIP{path};
53 auto packFile = std::unique_ptr<PackFile>(zip);
54
55 if (!zip->openZIP(zip->fullFilePath)) {
56 return nullptr;
57 }
58
59 for (auto code = mz_zip_goto_first_entry(zip->zipHandle); code == MZ_OK; code = mz_zip_goto_next_entry(zip->zipHandle)) {
60 if (mz_zip_entry_is_dir(zip->zipHandle) == MZ_OK) {
61 continue;
62 }
63
64 mz_zip_file* fileInfo = nullptr;
65 if (mz_zip_entry_get_info(zip->zipHandle, &fileInfo)) {
66 return nullptr;
67 }
68
69 auto entryPath = zip->cleanEntryPath(fileInfo->filename);
70
71 Entry entry = createNewEntry();
72 entry.flags = fileInfo->compression_method << 16;
73 entry.length = fileInfo->uncompressed_size;
74 entry.compressedLength = fileInfo->compressed_size;
75 entry.crc32 = fileInfo->crc;
76
77 zip->entries.insert(entryPath, entry);
78
79 if (callback) {
80 callback(entryPath, entry);
81 }
82 }
83
84 return packFile;
85}
86
87std::vector<std::string> ZIP::verifyEntryChecksums() const {
88 return this->verifyEntryChecksumsUsingCRC32();
89}
90
91std::optional<std::vector<std::byte>> ZIP::readEntry(const std::string& path_) const {
92 const auto path = this->cleanEntryPath(path_);
93 const auto entry = this->findEntry(path);
94 if (!entry) {
95 return std::nullopt;
96 }
97 if (entry->unbaked) {
98 return readUnbakedEntry(*entry);
99 }
100
101 // It's baked into the file on disk
102 if (!this->streamOpen || !this->zipOpen) {
103 return std::nullopt;
104 }
105 if (mz_zip_locate_entry(this->zipHandle, path.c_str(), !this->isCaseSensitive()) != MZ_OK) {
106 return std::nullopt;
107 }
108 if (mz_zip_entry_read_open(this->zipHandle, 0, nullptr) != MZ_OK) {
109 return std::nullopt;
110 }
111 std::vector<std::byte> out;
112 out.resize(entry->length);
113 mz_zip_entry_read(this->zipHandle, out.data(), static_cast<int>(entry->length));
114 mz_zip_entry_close(this->zipHandle);
115 return out;
116}
117
118bool ZIP::isReadOnly() const noexcept {
119 uint8_t xzpVersion = 0;
120 mz_zip_get_valve_xzp_version(this->zipHandle, &xzpVersion);
121 return xzpVersion == 2 || xzpVersion == 3;
122}
123
124void ZIP::addEntryInternal(Entry& entry, const std::string& path, std::vector<std::byte>& buffer, EntryOptions options) {
126 entry.length = buffer.size();
127 entry.compressedLength = 0;
128 entry.crc32 = crypto::computeCRC32(buffer);
129}
130
131bool ZIP::bake(const std::string& outputDir_, BakeOptions options, const EntryCallback& callback) {
132 // Get the proper file output folder
133 const std::string outputDir = this->getBakeOutputDir(outputDir_);
134 const std::string outputPath = outputDir + '/' + this->getFilename();
135
136 // Use temp folder so we can read from the current ZIP
137 if (!this->bakeTempZip(this->tempZIPPath, options, callback)) {
138 return false;
139 }
140 this->mergeUnbakedEntries();
141
142 // Close our ZIP and reopen it
143 this->closeZIP();
144 std::filesystem::rename(this->tempZIPPath, outputPath);
145 if (!this->openZIP(outputPath)) {
146 return false;
147 }
148 PackFile::setFullFilePath(outputDir);
149 return true;
150}
151
153 using enum Attribute;
154 return LENGTH | CRC32;
155}
156
157EntryCompressionType ZIP::getEntryCompressionType(const std::string& path_) const {
158 const auto path = this->cleanEntryPath(path_);
159 if (this->entries.count(path)) {
160 return static_cast<EntryCompressionType>(this->entries.at(path).flags >> 16);
161 }
163}
164
166 const auto path = this->cleanEntryPath(path_);
167 if (this->entries.count(path)) {
168 this->entries.at(path).flags = (static_cast<uint32_t>(type) << 16) | (this->entries.at(path).flags & 0xffff);
169 }
170}
171
172int16_t ZIP::getEntryCompressionStrength(const std::string& path_) const {
173 const auto path = this->cleanEntryPath(path_);
174 if (this->entries.count(path)) {
175 return static_cast<int16_t>(this->entries.at(path).flags & 0xffff);
176 }
177 return 0;
178}
179
180void ZIP::setEntryCompressionStrength(const std::string& path_, int16_t strength) {
181 const auto path = this->cleanEntryPath(path_);
182 if (this->entries.count(path)) {
183 this->entries.at(path).flags = (this->entries.at(path).flags & 0xffff0000) | strength;
184 }
185}
186
187bool ZIP::bakeTempZip(const std::string& writeZipPath, BakeOptions options, const EntryCallback& callback) const {
188 void* writeStreamHandle = mz_stream_os_create();
189 if (mz_stream_open(writeStreamHandle, writeZipPath.c_str(), MZ_OPEN_MODE_CREATE | MZ_OPEN_MODE_WRITE)) {
190 return false;
191 }
192
193 void* writeZipHandle = mz_zip_writer_create();
194 if (mz_zip_writer_open(writeZipHandle, writeStreamHandle, 0)) {
195 return false;
196 }
197
198 mz_zip_set_version_madeby(writeZipHandle, MZ_VERSION_MADEBY);
199
200 const auto modifiedTime = std::time(nullptr);
201 const auto modifiedTimeDOS = mz_zip_time_t_to_dos_date(modifiedTime);
202
203 const bool overrideCompression = options.zip_compressionTypeOverride != EntryCompressionType::NO_OVERRIDE;
204
205 bool noneFailed = true;
206 this->runForAllEntries([this, &options, &callback, writeZipHandle, modifiedTime, modifiedTimeDOS, overrideCompression, &noneFailed](const std::string& path, const Entry& entry) {
207 const auto binData = this->readEntry(path);
208 if (!binData) {
209 return;
210 }
211
212 if (overrideCompression) {
213 mz_zip_writer_set_compress_method(writeZipHandle, static_cast<uint16_t>(options.zip_compressionTypeOverride));
214 mz_zip_writer_set_compress_level(writeZipHandle, options.zip_compressionStrength);
215 } else {
216 mz_zip_writer_set_compress_method(writeZipHandle, entry.flags >> 16);
217 mz_zip_writer_set_compress_level(writeZipHandle, (entry.flags & 0xffff) > 0 ? static_cast<int16_t>(entry.flags & 0xffff) : options.zip_compressionStrength);
218 }
219
220 mz_zip_entry fileInfo{};
221 fileInfo.filename = path.c_str();
222 fileInfo.filename_size = path.length();
223 fileInfo.version_madeby = MZ_VERSION_MADEBY;
224 fileInfo.creation_date = modifiedTime;
225 fileInfo.modified_date = modifiedTimeDOS;
226 fileInfo.compression_method = overrideCompression ? static_cast<uint16_t>(options.zip_compressionTypeOverride) : entry.flags >> 16;
227 fileInfo.flag = MZ_ZIP_FLAG_DATA_DESCRIPTOR | MZ_ZIP_FLAG_UTF8;
228 fileInfo.uncompressed_size = static_cast<int64_t>(entry.length);
229 fileInfo.compressed_size = static_cast<int64_t>(entry.compressedLength);
230 fileInfo.crc = entry.crc32;
231 if (mz_zip_writer_add_buffer(writeZipHandle, binData->data(), static_cast<int>(binData->size()), &fileInfo)) {
232 noneFailed = false;
233 return;
234 }
235
236 if (callback) {
237 callback(path, entry);
238 }
239 });
240
241 if (mz_zip_writer_close(writeZipHandle)) {
242 return false;
243 }
244 mz_zip_writer_delete(&writeZipHandle);
245
246 if (mz_stream_close(writeStreamHandle)) {
247 return false;
248 }
249 mz_stream_delete(&writeStreamHandle);
250
251 return noneFailed;
252}
253
254bool ZIP::openZIP(std::string_view path) {
255 this->streamHandle = mz_stream_os_create();
256 if (mz_stream_open(this->streamHandle, path.data(), MZ_OPEN_MODE_READ) != MZ_OK) {
257 mz_stream_delete(&this->streamHandle);
258 return false;
259 }
260 this->streamOpen = true;
261
262 this->zipHandle = mz_zip_create();
263 if (mz_zip_open(this->zipHandle, this->streamHandle, MZ_OPEN_MODE_READ) != MZ_OK) {
264 mz_zip_delete(&this->zipHandle);
265 return false; // No need to delete the stream, it's done when we destruct
266 }
267 this->zipOpen = true;
268
269 return true;
270}
271
273 if (this->zipOpen) {
274 mz_zip_close(this->zipHandle);
275 mz_zip_delete(&this->zipHandle);
276 this->zipOpen = false;
277 }
278 if (this->streamOpen) {
279 mz_stream_close(this->streamHandle);
280 mz_stream_delete(&this->streamHandle);
281 this->streamOpen = false;
282 }
283}
This class represents the metadata that a file has inside a PackFile.
Definition Entry.h:14
uint32_t flags
Format-specific flags (PCK: File flags, VPK: Internal parser state, ZIP: Compression method / strengt...
Definition Entry.h:19
uint64_t compressedLength
If the format supports compression, this is the compressed length.
Definition Entry.h:30
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
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
virtual operator std::string() const
Definition PackFile.cpp:615
EntryTrie entries
Definition PackFile.h:233
std::vector< std::string > verifyEntryChecksumsUsingCRC32() const
Definition PackFile.cpp:629
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 runForAllEntries(const EntryCallback &operation, bool includeUnbaked=true) const
Run a callback for each entry in the pack file.
Definition PackFile.cpp:502
void setFullFilePath(const std::string &outputDir)
Definition PackFile.cpp:674
std::string cleanEntryPath(const std::string &path) const
Definition PackFile.cpp:679
PackFile(const PackFile &other)=delete
static Entry createNewEntry()
Definition PackFile.cpp:688
static std::optional< std::vector< std::byte > > readUnbakedEntry(const Entry &entry)
Definition PackFile.cpp:692
void addEntryInternal(Entry &entry, const std::string &path, std::vector< std::byte > &buffer, EntryOptions options) override
Definition ZIP.cpp:124
Attribute getSupportedEntryAttributes() const override
Returns a list of supported entry attributes Mostly for GUI programs that show entries and their meta...
Definition ZIP.cpp:152
ZIP(const std::string &fullFilePath_)
Definition ZIP.cpp:21
const std::string tempZIPPath
Definition ZIP.h:70
void setEntryCompressionType(const std::string &path_, EntryCompressionType type)
Definition ZIP.cpp:165
void setEntryCompressionStrength(const std::string &path_, int16_t strength)
Definition ZIP.cpp:180
void closeZIP()
Definition ZIP.cpp:272
void * streamHandle
Definition ZIP.h:72
static std::unique_ptr< PackFile > open(const std::string &path, const EntryCallback &callback=nullptr)
Open a ZIP file.
Definition ZIP.cpp:46
bool isReadOnly() const noexcept override
Definition ZIP.cpp:118
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 ZIP.cpp:87
int16_t getEntryCompressionStrength(const std::string &path_) const
Definition ZIP.cpp:172
bool streamOpen
Definition ZIP.h:73
bool openZIP(std::string_view path)
Definition ZIP.cpp:254
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 ZIP.cpp:131
bool zipOpen
Definition ZIP.h:76
std::optional< std::vector< std::byte > > readEntry(const std::string &path_) const override
Try to read the entry's data to a bytebuffer.
Definition ZIP.cpp:91
void * zipHandle
Definition ZIP.h:75
~ZIP() override
Definition ZIP.cpp:25
bool bakeTempZip(const std::string &writeZipPath, BakeOptions options, const EntryCallback &callback) const
Definition ZIP.cpp:187
static std::unique_ptr< PackFile > create(const std::string &path)
Create a ZIP file.
Definition ZIP.cpp:29
EntryCompressionType getEntryCompressionType(const std::string &path_) const
Definition ZIP.cpp:157
uint32_t computeCRC32(std::span< const std::byte > buffer)
Definition CRC32.cpp:7
Attribute
Definition Attribute.h:7
EntryCompressionType
Definition Options.h:7
EntryCompressionType zip_compressionTypeOverride
BSP/ZIP - Override compression type of all stored entries.
Definition Options.h:21
int16_t zip_compressionStrength
BSP/VPK/ZIP - Compression strength.
Definition Options.h:24
EntryCompressionType zip_compressionType
BSP/ZIP - The compression format.
Definition Options.h:35
int16_t zip_compressionStrength
BSP/ZIP - The compression strength.
Definition Options.h:38