Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/Common/File/VFS/ZipFileReader.cpp
5650 views
1
#include <cctype>
2
#include <set>
3
#include <algorithm> // for sort
4
#include <cstdio>
5
#include <cstring>
6
7
#ifdef SHARED_LIBZIP
8
#include <zip.h>
9
#else
10
#include "ext/libzip/zip.h"
11
#endif
12
13
#include "Common/Common.h"
14
#include "Common/Log.h"
15
#include "Common/File/VFS/ZipFileReader.h"
16
#include "Common/StringUtils.h"
17
18
ZipContainer::ZipContainer() noexcept : sourceData_(nullptr), zip_(nullptr) {}
19
20
ZipContainer::ZipContainer(const Path &path) : sourceData_(new SourceData {path, nullptr}), zip_(nullptr) {
21
zip_source_t *source = zip_source_function_create(SourceCallback, sourceData_, nullptr);
22
if (source != nullptr && (zip_ = zip_open_from_source(source, ZIP_RDONLY, nullptr)) == nullptr) {
23
zip_source_free(source);
24
}
25
}
26
27
ZipContainer::ZipContainer(ZipContainer &&other) noexcept {
28
*this = std::move(other);
29
}
30
31
ZipContainer &ZipContainer::operator=(ZipContainer &&other) noexcept {
32
sourceData_ = other.sourceData_;
33
zip_ = other.zip_;
34
other.sourceData_ = nullptr;
35
other.zip_ = nullptr;
36
return *this;
37
}
38
39
ZipContainer::~ZipContainer() {
40
close();
41
}
42
43
void ZipContainer::close() noexcept {
44
if (zip_ != nullptr) {
45
zip_close(zip_);
46
zip_ = nullptr;
47
}
48
if (sourceData_ != nullptr) {
49
delete sourceData_;
50
sourceData_ = nullptr;
51
}
52
}
53
54
ZipContainer::operator zip_t *() const noexcept {
55
return zip_;
56
}
57
58
zip_int64_t ZipContainer::SourceCallback(void *userdata, void *data, zip_uint64_t len, zip_source_cmd_t cmd) {
59
SourceData &sourceData = *(SourceData *)userdata;
60
61
switch (cmd) {
62
case ZIP_SOURCE_SUPPORTS:
63
return zip_source_make_command_bitmap(
64
ZIP_SOURCE_SUPPORTS,
65
ZIP_SOURCE_OPEN,
66
ZIP_SOURCE_READ,
67
ZIP_SOURCE_CLOSE,
68
ZIP_SOURCE_STAT,
69
ZIP_SOURCE_ERROR,
70
ZIP_SOURCE_SEEK,
71
ZIP_SOURCE_TELL,
72
-1);
73
74
case ZIP_SOURCE_OPEN:
75
{
76
FILE *newFile = File::OpenCFile(sourceData.path, "rb");
77
if (newFile == nullptr) {
78
return -1;
79
}
80
sourceData.file = newFile;
81
return 0;
82
}
83
84
case ZIP_SOURCE_READ:
85
{
86
size_t n = fread(data, 1, len, sourceData.file);
87
return ferror(sourceData.file) ? -1 : n;
88
}
89
90
case ZIP_SOURCE_CLOSE:
91
fclose(sourceData.file);
92
sourceData.file = nullptr;
93
return 0;
94
95
case ZIP_SOURCE_STAT:
96
if (sourceData.file == nullptr) {
97
zip_stat_t *stat = (zip_stat_t *)data;
98
zip_stat_init(stat);
99
stat->valid = 0;
100
} else {
101
int64_t pos = File::Ftell(sourceData.file);
102
if (pos == -1) {
103
return -1;
104
}
105
if (File::Fseek(sourceData.file, 0, SEEK_END) != 0) {
106
return -1;
107
}
108
int64_t size = File::Ftell(sourceData.file);
109
if (size != pos && File::Fseek(sourceData.file, pos, SEEK_SET) != 0) {
110
return -1;
111
}
112
zip_stat_t *stat = (zip_stat_t *)data;
113
zip_stat_init(stat);
114
stat->valid = ZIP_STAT_SIZE;
115
stat->size = size;
116
}
117
return 0;
118
119
case ZIP_SOURCE_ERROR:
120
return ZIP_ER_INTERNAL;
121
122
case ZIP_SOURCE_SEEK:
123
{
124
zip_source_args_seek_t *args = (zip_source_args_seek_t *)data;
125
if (args == nullptr) {
126
return -1;
127
}
128
return File::Fseek(sourceData.file, args->offset, args->whence) ? -1 : 0;
129
}
130
131
case ZIP_SOURCE_TELL:
132
return File::Ftell(sourceData.file);
133
134
default:
135
return -1;
136
}
137
}
138
139
ZipFileReader *ZipFileReader::Create(const Path &zipFile, std::string_view inZipPath, bool logErrors) {
140
// The inZipPath is supposed to be a folder, and internally in this class, we suffix
141
// folder paths with '/', matching how the zip library works.
142
std::string path(inZipPath);
143
if (!path.empty() && path.back() != '/') {
144
path.push_back('/');
145
}
146
147
ZipContainer zip(zipFile);
148
if (zip == nullptr) {
149
if (logErrors) {
150
ERROR_LOG(Log::IO, "Failed to open %s as a zip file", zipFile.c_str());
151
}
152
return nullptr;
153
}
154
155
return new ZipFileReader(std::move(zip), zipFile, path);
156
}
157
158
ZipFileReader::~ZipFileReader() {
159
std::lock_guard<std::mutex> guard(lock_);
160
zip_file_.close();
161
}
162
163
uint8_t *ZipFileReader::ReadFile(std::string_view path, size_t *size) {
164
std::string temp_path = join(inZipPath_, path);
165
166
std::lock_guard<std::mutex> guard(lock_);
167
// Figure out the file size first. TODO: Can this part be done without locking the mutex?
168
struct zip_stat zstat;
169
int retval = zip_stat(zip_file_, temp_path.c_str(), ZIP_FL_NOCASE | ZIP_FL_UNCHANGED, &zstat);
170
if (retval != 0) {
171
ERROR_LOG(Log::IO, "Error opening %s from ZIP", temp_path.c_str());
172
return 0;
173
}
174
zip_file *file = zip_fopen_index(zip_file_, zstat.index, ZIP_FL_NOCASE | ZIP_FL_UNCHANGED);
175
if (!file) {
176
ERROR_LOG(Log::IO, "Error opening %s from ZIP", temp_path.c_str());
177
return 0;
178
}
179
uint8_t *contents = new uint8_t[zstat.size + 1];
180
zip_fread(file, contents, zstat.size);
181
zip_fclose(file);
182
contents[zstat.size] = 0;
183
184
*size = zstat.size;
185
return contents;
186
}
187
188
bool ZipFileReader::GetFileListing(std::string_view orig_path, std::vector<File::FileInfo> *listing, const char *filter = 0) {
189
std::string path = join(inZipPath_, orig_path);
190
if (!path.empty() && path.back() != '/') {
191
path.push_back('/');
192
}
193
194
std::set<std::string> filters;
195
std::string tmp;
196
if (filter) {
197
while (*filter) {
198
if (*filter == ':') {
199
filters.emplace("." + tmp);
200
tmp.clear();
201
} else {
202
tmp.push_back(*filter);
203
}
204
filter++;
205
}
206
}
207
208
if (tmp.size())
209
filters.emplace("." + tmp);
210
211
// We just loop through the whole ZIP file and deduce what files are in this directory, and what subdirectories there are.
212
std::set<std::string> files;
213
std::set<std::string> directories;
214
bool success = GetZipListings(path, files, directories);
215
if (!success) {
216
// This means that no file prefix matched the path.
217
return false;
218
}
219
220
listing->clear();
221
222
// INFO_LOG(Log::IO, "Zip: Listing '%s'", orig_path);
223
224
const std::string relativePath = path.substr(inZipPath_.size());
225
226
listing->reserve(directories.size() + files.size());
227
for (const auto &dir : directories) {
228
File::FileInfo info;
229
info.name = dir;
230
231
// Remove the "inzip" part of the fullname.
232
info.fullName = Path(relativePath + dir);
233
info.exists = true;
234
info.isWritable = false;
235
info.isDirectory = true;
236
// INFO_LOG(Log::IO, "Found file: %s (%s)", info.name.c_str(), info.fullName.c_str());
237
listing->push_back(info);
238
}
239
240
for (const auto &fiter : files) {
241
File::FileInfo info;
242
info.name = fiter;
243
info.fullName = Path(relativePath + fiter);
244
info.exists = true;
245
info.isWritable = false;
246
info.isDirectory = false;
247
std::string ext = info.fullName.GetFileExtension();
248
if (filter) {
249
if (filters.find(ext) == filters.end()) {
250
continue;
251
}
252
}
253
// INFO_LOG(Log::IO, "Found dir: %s (%s)", info.name.c_str(), info.fullName.c_str());
254
listing->push_back(info);
255
}
256
257
std::sort(listing->begin(), listing->end());
258
return true;
259
}
260
261
// path here is from the root, so inZipPath needs to already be added.
262
bool ZipFileReader::GetZipListings(const std::string &path, std::set<std::string> &files, std::set<std::string> &directories) {
263
_dbg_assert_(path.empty() || path.back() == '/');
264
265
std::lock_guard<std::mutex> guard(lock_);
266
int numFiles = zip_get_num_files(zip_file_);
267
bool anyPrefixMatched = false;
268
for (int i = 0; i < numFiles; i++) {
269
const char* name = zip_get_name(zip_file_, i, 0);
270
if (!name)
271
continue; // shouldn't happen, I think
272
if (startsWith(name, path)) {
273
if (strlen(name) == path.size()) {
274
// Don't want to return the same folder.
275
continue;
276
}
277
const char *slashPos = strchr(name + path.size(), '/');
278
if (slashPos != 0) {
279
anyPrefixMatched = true;
280
// A directory. Let's pick off the only part we care about.
281
size_t offset = path.size();
282
std::string dirName = std::string(name + offset, slashPos - (name + offset));
283
// We might get a lot of these if the tree is deep. The std::set deduplicates.
284
directories.insert(dirName);
285
} else {
286
anyPrefixMatched = true;
287
// It's a file.
288
const char *fn = name + path.size();
289
files.emplace(fn);
290
}
291
}
292
}
293
return anyPrefixMatched;
294
}
295
296
bool ZipFileReader::GetFileInfo(std::string_view path, File::FileInfo *info) {
297
struct zip_stat zstat;
298
std::string temp_path = join(inZipPath_, path);
299
300
// Clear some things to start.
301
info->isDirectory = false;
302
info->isWritable = false;
303
info->size = 0;
304
305
{
306
std::lock_guard<std::mutex> guard(lock_);
307
if (0 != zip_stat(zip_file_, temp_path.c_str(), ZIP_FL_NOCASE | ZIP_FL_UNCHANGED, &zstat)) {
308
// ZIP files do not have real directories, so we'll end up here if we
309
// try to stat one. For now that's fine.
310
info->exists = false;
311
return false;
312
}
313
}
314
315
// Zips usually don't contain directory entries, but they may.
316
if ((zstat.valid & ZIP_STAT_NAME) != 0 && zstat.name) {
317
info->isDirectory = zstat.name[strlen(zstat.name) - 1] == '/';
318
}
319
if ((zstat.valid & ZIP_STAT_SIZE) != 0) {
320
info->size = zstat.size;
321
}
322
323
info->fullName = Path(path);
324
info->exists = true;
325
return true;
326
}
327
328
class ZipFileReaderFileReference : public VFSFileReference {
329
public:
330
int zi;
331
};
332
333
class ZipFileReaderOpenFile : public VFSOpenFile {
334
public:
335
~ZipFileReaderOpenFile() {
336
// Needs to be closed properly and unlocked.
337
_dbg_assert_(zf == nullptr);
338
}
339
ZipFileReaderFileReference *reference;
340
zip_file_t *zf = nullptr;
341
};
342
343
VFSFileReference *ZipFileReader::GetFile(std::string_view path) {
344
std::string p(path);
345
int zi = zip_name_locate(zip_file_, p.c_str(), ZIP_FL_NOCASE); // this is EXPENSIVE
346
if (zi < 0) {
347
// Not found.
348
return nullptr;
349
}
350
ZipFileReaderFileReference *ref = new ZipFileReaderFileReference();
351
ref->zi = zi;
352
return ref;
353
}
354
355
bool ZipFileReader::GetFileInfo(VFSFileReference *vfsReference, File::FileInfo *fileInfo) {
356
ZipFileReaderFileReference *reference = (ZipFileReaderFileReference *)vfsReference;
357
// If you crash here, you called this while having the lock held by having the file open.
358
// Don't do that, check the info before you open the file.
359
zip_stat_t zstat;
360
if (zip_stat_index(zip_file_, reference->zi, 0, &zstat) != 0)
361
return false;
362
*fileInfo = File::FileInfo{};
363
fileInfo->size = 0;
364
if (zstat.valid & ZIP_STAT_SIZE)
365
fileInfo->size = zstat.size;
366
return zstat.size;
367
}
368
369
void ZipFileReader::ReleaseFile(VFSFileReference *vfsReference) {
370
ZipFileReaderFileReference *reference = (ZipFileReaderFileReference *)vfsReference;
371
// Don't do anything other than deleting it.
372
delete reference;
373
}
374
375
VFSOpenFile *ZipFileReader::OpenFileForRead(VFSFileReference *vfsReference, size_t *size) {
376
ZipFileReaderFileReference *reference = (ZipFileReaderFileReference *)vfsReference;
377
ZipFileReaderOpenFile *openFile = new ZipFileReaderOpenFile();
378
openFile->reference = reference;
379
*size = 0;
380
// We only allow one file to be open for read concurrently. It's possible that this can be improved,
381
// especially if we only access by index like this.
382
lock_.lock();
383
zip_stat_t zstat;
384
if (zip_stat_index(zip_file_, reference->zi, 0, &zstat) != 0) {
385
lock_.unlock();
386
delete openFile;
387
return nullptr;
388
}
389
390
openFile->zf = zip_fopen_index(zip_file_, reference->zi, 0);
391
if (!openFile->zf) {
392
WARN_LOG(Log::G3D, "File with index %d not found in zip", reference->zi);
393
lock_.unlock();
394
delete openFile;
395
return nullptr;
396
}
397
398
*size = zstat.size;
399
// Intentionally leaving the mutex locked, will be closed in CloseFile.
400
return openFile;
401
}
402
403
void ZipFileReader::Rewind(VFSOpenFile *vfsOpenFile) {
404
ZipFileReaderOpenFile *file = (ZipFileReaderOpenFile *)vfsOpenFile;
405
_assert_(file);
406
// Unless the zip file is compressed, can't seek directly, so we re-open.
407
// This version of libzip doesn't even have zip_file_is_seekable(), should probably upgrade.
408
zip_fclose(file->zf);
409
file->zf = zip_fopen_index(zip_file_, file->reference->zi, 0);
410
_dbg_assert_(file->zf != nullptr);
411
}
412
413
size_t ZipFileReader::Read(VFSOpenFile *vfsOpenFile, void *buffer, size_t length) {
414
ZipFileReaderOpenFile *file = (ZipFileReaderOpenFile *)vfsOpenFile;
415
_assert_(file);
416
_dbg_assert_(file->zf != nullptr);
417
return zip_fread(file->zf, buffer, length);
418
}
419
420
void ZipFileReader::CloseFile(VFSOpenFile *vfsOpenFile) {
421
ZipFileReaderOpenFile *file = (ZipFileReaderOpenFile *)vfsOpenFile;
422
_assert_(file);
423
_dbg_assert_(file->zf != nullptr);
424
zip_fclose(file->zf);
425
file->zf = nullptr;
426
vfsOpenFile = nullptr;
427
lock_.unlock();
428
delete file;
429
}
430
431
bool ReadSingleFileFromZip(Path zipFile, const char *path, std::string *data, std::mutex *mutex) {
432
ZipContainer zip(zipFile);
433
if (zip == nullptr) {
434
return false;
435
}
436
437
struct zip_stat zstat;
438
if (zip_stat(zip, path, ZIP_FL_NOCASE | ZIP_FL_UNCHANGED, &zstat) != 0) {
439
return false;
440
}
441
zip_file *file = zip_fopen_index(zip, zstat.index, ZIP_FL_UNCHANGED);
442
if (!file) {
443
return false;
444
}
445
if (mutex) {
446
mutex->lock();
447
}
448
data->resize(zstat.size);
449
if (zip_fread(file, data->data(), zstat.size) != zstat.size) {
450
if (mutex) {
451
mutex->unlock();
452
}
453
data->resize(0);
454
zip_fclose(file);
455
return false;
456
}
457
if (mutex) {
458
mutex->unlock();
459
}
460
zip_fclose(file);
461
return true;
462
}
463
464