1 /*
2 * Copyright (C) 2002-2020 by the Widelands Development Team
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 *
18 */
19
20 #include "io/filesystem/zip_filesystem.h"
21
22 #include <cassert>
23 #include <cerrno>
24 #include <cstring>
25 #include <memory>
26
27 #include <boost/format.hpp>
28
29 #include "base/wexception.h"
30 #include "io/filesystem/filesystem_exceptions.h"
31 #include "io/filesystem/zip_exceptions.h"
32 #include "io/streamread.h"
33 #include "io/streamwrite.h"
34
ZipFile(const std::string & zipfile)35 ZipFilesystem::ZipFile::ZipFile(const std::string& zipfile)
36 : state_(State::kIdle),
37 path_(zipfile),
38 basename_(fs_filename(zipfile.c_str())),
39 write_handle_(nullptr),
40 read_handle_(nullptr) {
41 }
42
~ZipFile()43 ZipFilesystem::ZipFile::~ZipFile() {
44 close();
45 }
46
close()47 void ZipFilesystem::ZipFile::close() {
48 if (state_ == State::kZipping) {
49 zipClose(write_handle_, nullptr);
50 } else if (state_ == State::kUnzipping) {
51 unzClose(read_handle_);
52 }
53 state_ = State::kIdle;
54 }
55
open_for_zip()56 void ZipFilesystem::ZipFile::open_for_zip() {
57 if (state_ == State::kZipping) {
58 return;
59 }
60
61 close();
62
63 write_handle_ = zipOpen(path_.c_str(), APPEND_STATUS_ADDINZIP);
64 if (!write_handle_) {
65 // Couldn't open for append, so create new.
66 write_handle_ = zipOpen(path_.c_str(), APPEND_STATUS_CREATE);
67 }
68
69 state_ = State::kZipping;
70 }
71
open_for_unzip()72 void ZipFilesystem::ZipFile::open_for_unzip() {
73 if (state_ == State::kUnzipping) {
74 return;
75 }
76
77 close();
78
79 read_handle_ = unzOpen(path_.c_str());
80 if (!read_handle_) {
81 throw FileTypeError("ZipFilesystem::open_for_unzip", path_, "not a .zip file");
82 }
83
84 std::string first_entry;
85 size_t longest_prefix = 0;
86 unz_file_info file_info;
87 char filename_inzip[256];
88 for (;;) {
89 unzGetCurrentFileInfo(
90 read_handle_, &file_info, filename_inzip, sizeof(filename_inzip), nullptr, 0, nullptr, 0);
91 if (first_entry.empty()) {
92 first_entry = filename_inzip;
93 longest_prefix = first_entry.size();
94 } else {
95 const std::string entry = filename_inzip;
96 size_t pos = 0;
97 while (pos < longest_prefix && pos < entry.size() && first_entry[pos] == entry[pos]) {
98 ++pos;
99 }
100 longest_prefix = pos;
101 }
102
103 if (unzGoToNextFile(read_handle_) == UNZ_END_OF_LIST_OF_FILE) {
104 break;
105 }
106 }
107 common_prefix_ = first_entry.substr(0, longest_prefix);
108
109 state_ = State::kUnzipping;
110 }
111
strip_basename(const std::string & filename)112 std::string ZipFilesystem::ZipFile::strip_basename(const std::string& filename) {
113 return filename.substr(common_prefix_.size());
114 }
115
write_handle()116 const zipFile& ZipFilesystem::ZipFile::write_handle() {
117 open_for_zip();
118 return write_handle_;
119 }
120
read_handle()121 const unzFile& ZipFilesystem::ZipFile::read_handle() {
122 open_for_unzip();
123 return read_handle_;
124 }
125
path() const126 const std::string& ZipFilesystem::ZipFile::path() const {
127 return path_;
128 }
129
130 /**
131 * Initialize the real file-system
132 */
ZipFilesystem(const std::string & zipfile)133 ZipFilesystem::ZipFilesystem(const std::string& zipfile)
134 : zip_file_(new ZipFile(zipfile)), basedir_in_zip_file_() {
135 // TODO(unknown): check OS permissions on whether the file is writable
136 }
137
ZipFilesystem(const std::shared_ptr<ZipFile> & shared_data,const std::string & basedir_in_zip_file)138 ZipFilesystem::ZipFilesystem(const std::shared_ptr<ZipFile>& shared_data,
139 const std::string& basedir_in_zip_file)
140 : zip_file_(shared_data), basedir_in_zip_file_(basedir_in_zip_file) {
141 }
142
~ZipFilesystem()143 ZipFilesystem::~ZipFilesystem() {
144 }
145
146 /**
147 * Return true if this directory is writable.
148 */
is_writable() const149 bool ZipFilesystem::is_writable() const {
150 return true; // should be checked in constructor
151 }
152
153 /**
154 * Returns the number of files found, and stores the filenames (without the
155 * pathname) in the results. There doesn't seem to be an even remotely
156 * cross-platform way of doing this
157 */
list_directory(const std::string & path_in) const158 FilenameSet ZipFilesystem::list_directory(const std::string& path_in) const {
159 assert(path_in.size()); // prevent invalid read below
160
161 std::string path = basedir_in_zip_file_;
162 if (*path_in.begin() != '/') {
163 path += "/";
164 }
165 path += path_in;
166 if (*path.rbegin() != '/') {
167 path += '/';
168 }
169 if (*path.begin() == '/') {
170 path = path.substr(1);
171 }
172
173 unzCloseCurrentFile(zip_file_->read_handle());
174 unzGoToFirstFile(zip_file_->read_handle());
175
176 unz_file_info file_info;
177 char filename_inzip[256];
178 std::set<std::string> results;
179 for (;;) {
180 unzGetCurrentFileInfo(zip_file_->read_handle(), &file_info, filename_inzip,
181 sizeof(filename_inzip), nullptr, 0, nullptr, 0);
182
183 std::string complete_filename = zip_file_->strip_basename(filename_inzip);
184 std::string filename = fs_filename(complete_filename.c_str());
185 std::string filepath =
186 complete_filename.substr(0, complete_filename.size() - filename.size());
187
188 // TODO(unknown): Something strange is going on with regard to the leading slash!
189 // This is just an ugly workaround and does not solve the real
190 // problem (which remains undiscovered)
191 if (('/' + path == filepath || path == filepath || path.length() == 1) && filename.size()) {
192 results.insert(complete_filename.substr(basedir_in_zip_file_.size()));
193 }
194
195 if (unzGoToNextFile(zip_file_->read_handle()) == UNZ_END_OF_LIST_OF_FILE) {
196 break;
197 }
198 }
199 return results;
200 }
201
202 /**
203 * Returns true if the given file exists, and false if it doesn't.
204 * Also returns false if the pathname is invalid
205 */
file_exists(const std::string & path) const206 bool ZipFilesystem::file_exists(const std::string& path) const {
207 try {
208 unzGoToFirstFile(zip_file_->read_handle());
209 } catch (...) {
210 // The zip file could not be opened. I guess this means 'path' does not
211 // exist.
212 return false;
213 }
214 unz_file_info file_info;
215 char filename_inzip[256];
216 memset(filename_inzip, ' ', 256);
217
218 std::string path_in = basedir_in_zip_file_ + "/" + path;
219
220 if (*path_in.begin() == '/') {
221 path_in = path_in.substr(1);
222 }
223
224 assert(path_in.size());
225
226 for (;;) {
227 const int32_t success =
228 unzGetCurrentFileInfo(zip_file_->read_handle(), &file_info, filename_inzip,
229 sizeof(filename_inzip), nullptr, 0, nullptr, 0);
230
231 // Handle corrupt files
232 if (success != UNZ_OK) {
233 return false;
234 }
235
236 std::string complete_filename = zip_file_->strip_basename(filename_inzip);
237
238 if (*complete_filename.rbegin() == '/') {
239 complete_filename.resize(complete_filename.size() - 1);
240 }
241 if (path_in == complete_filename) {
242 return true;
243 }
244 if (unzGoToNextFile(zip_file_->read_handle()) == UNZ_END_OF_LIST_OF_FILE) {
245 break;
246 }
247 }
248 return false;
249 }
250
251 /**
252 * Returns true if the given file is a directory, and false if it doesn't.
253 * Also returns false if the pathname is invalid
254 */
is_directory(const std::string & path) const255 bool ZipFilesystem::is_directory(const std::string& path) const {
256 if (!file_exists(path)) {
257 return false;
258 }
259
260 unz_file_info file_info;
261 char filename_inzip[256];
262
263 unzGetCurrentFileInfo(zip_file_->read_handle(), &file_info, filename_inzip,
264 sizeof(filename_inzip), nullptr, 0, nullptr, 0);
265
266 return filename_inzip[strlen(filename_inzip) - 1] == '/';
267 }
268
269 /**
270 * Create a sub filesystem out of this filesystem
271 */
make_sub_file_system(const std::string & path)272 FileSystem* ZipFilesystem::make_sub_file_system(const std::string& path) {
273 if (path == ".") {
274 return new ZipFilesystem(zip_file_, basedir_in_zip_file_);
275 }
276 if (!file_exists(path)) {
277 throw wexception(
278 "ZipFilesystem::make_sub_file_system: The path '%s' does not exist in zip file '%s'.",
279 (basedir_in_zip_file_.empty() ? path : basedir_in_zip_file_ + "/" + path).c_str(),
280 zip_file_->path().c_str());
281 }
282 if (!is_directory(path)) {
283 throw wexception(
284 "ZipFilesystem::make_sub_file_system: The path '%s' needs to be a directory in zip file "
285 "'%s'.",
286 (basedir_in_zip_file_.empty() ? path : basedir_in_zip_file_ + "/" + path).c_str(),
287 zip_file_->path().c_str());
288 }
289
290 std::string localpath = path;
291 if (*localpath.begin() == '/') {
292 localpath = localpath.substr(1);
293 }
294 return new ZipFilesystem(zip_file_, basedir_in_zip_file_ + "/" + localpath);
295 }
296
297 /**
298 * Make a new Subfilesystem in this
299 * \throw ZipOperationError
300 */
301 // TODO(unknown): type should be recognized automatically,
302 // see Filesystem::create
create_sub_file_system(const std::string & path,Type const type)303 FileSystem* ZipFilesystem::create_sub_file_system(const std::string& path, Type const type) {
304 if (file_exists(path)) {
305 throw wexception(
306 "ZipFilesystem::create_sub_file_system: Path '%s' already exists in zip file %s.",
307 (basedir_in_zip_file_.empty() ? path : basedir_in_zip_file_ + "/" + path).c_str(),
308 zip_file_->path().c_str());
309 }
310
311 if (type != FileSystem::DIR) {
312 throw ZipOperationError("ZipFilesystem::create_sub_file_system", path, zip_file_->path(),
313 "can not create ZipFilesystem inside another ZipFilesystem");
314 }
315
316 ensure_directory_exists(path);
317
318 std::string localpath = path;
319 if (*localpath.begin() == '/') {
320 localpath = localpath.substr(1);
321 }
322 return new ZipFilesystem(zip_file_, basedir_in_zip_file_ + "/" + localpath);
323 }
324 /**
325 * Remove a number of files
326 * \throw ZipOperationError
327 */
fs_unlink(const std::string & filename)328 void ZipFilesystem::fs_unlink(const std::string& filename) {
329 throw ZipOperationError("ZipFilesystem::unlink", filename, zip_file_->path(),
330 "unlinking is not supported inside zipfiles");
331 }
332
333 /**
334 * Create this directory if it doesn't exist, throws an error
335 * if the dir can't be created or if a file with this name exists
336 */
ensure_directory_exists(const std::string & dirname)337 void ZipFilesystem::ensure_directory_exists(const std::string& dirname) {
338 if (file_exists(dirname) && is_directory(dirname)) {
339 return;
340 }
341
342 make_directory(dirname);
343 }
344
345 /**
346 * Create this directory, throw an error if it already exists or
347 * if a file is in the way or if the creation fails.
348 *
349 * Pleas note, this function does not honor parents,
350 * make_directory("onedir/otherdir/onemoredir") will fail
351 * if either ondir or otherdir is missing
352 */
make_directory(const std::string & dirname)353 void ZipFilesystem::make_directory(const std::string& dirname) {
354 zip_fileinfo zi;
355
356 zi.tmz_date.tm_sec = zi.tmz_date.tm_min = zi.tmz_date.tm_hour = zi.tmz_date.tm_mday =
357 zi.tmz_date.tm_mon = zi.tmz_date.tm_year = 0;
358 zi.dosDate = 0;
359 zi.internal_fa = 0;
360 zi.external_fa = 0;
361
362 std::string complete_filename = basedir_in_zip_file_;
363 complete_filename += "/";
364 complete_filename += dirname;
365
366 assert(dirname.size());
367 if (*complete_filename.rbegin() != '/') {
368 complete_filename += '/';
369 }
370
371 switch (zipOpenNewFileInZip3(zip_file_->write_handle(), complete_filename.c_str(), &zi, nullptr,
372 0, nullptr, 0, nullptr /* comment*/, Z_DEFLATED, Z_BEST_COMPRESSION,
373 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, nullptr, 0)) {
374 case ZIP_OK:
375 break;
376 case ZIP_ERRNO:
377 throw FileError("ZipFilesystem::make_directory", complete_filename, strerror(errno));
378 default:
379 throw FileError("ZipFilesystem::make_directory", complete_filename);
380 }
381 zipCloseFileInZip(zip_file_->write_handle());
382 }
383
384 /**
385 * Read the given file into alloced memory; called by FileRead::open.
386 * \throw FileNotFoundError if the file couldn't be opened.
387 */
load(const std::string & fname,size_t & length)388 void* ZipFilesystem::load(const std::string& fname, size_t& length) {
389 if (!file_exists(fname.c_str()) || is_directory(fname.c_str())) {
390 throw ZipOperationError(
391 "ZipFilesystem::load", fname, zip_file_->path(), "could not open file from zipfile");
392 }
393
394 char buffer[1024];
395 size_t totallen = 0;
396 unzOpenCurrentFile(zip_file_->read_handle());
397 for (;;) {
398 const int32_t len = unzReadCurrentFile(zip_file_->read_handle(), buffer, sizeof(buffer));
399 if (len == 0) {
400 break;
401 }
402 if (len < 0) {
403 unzCloseCurrentFile(zip_file_->read_handle());
404 const std::string errormessage = (boost::format("read error %i") % len).str();
405 throw ZipOperationError(
406 "ZipFilesystem::load", fname, zip_file_->path(), errormessage.c_str());
407 }
408
409 totallen += len;
410 }
411 unzCloseCurrentFile(zip_file_->read_handle());
412
413 void* const result = malloc(totallen + 1);
414 if (!result) {
415 throw std::bad_alloc();
416 }
417 unzOpenCurrentFile(zip_file_->read_handle());
418 unzReadCurrentFile(zip_file_->read_handle(), result, totallen);
419 unzCloseCurrentFile(zip_file_->read_handle());
420
421 static_cast<uint8_t*>(result)[totallen] = 0;
422 length = totallen;
423
424 return result;
425 }
426
427 /**
428 * Write the given block of memory to the repository.
429 * Throws an exception if it fails.
430 */
write(const std::string & fname,void const * const data,int32_t const length)431 void ZipFilesystem::write(const std::string& fname, void const* const data, int32_t const length) {
432 std::string filename = fname;
433 std::replace(filename.begin(), filename.end(), '\\', '/');
434
435 zip_fileinfo zi;
436 zi.tmz_date.tm_sec = zi.tmz_date.tm_min = zi.tmz_date.tm_hour = zi.tmz_date.tm_mday =
437 zi.tmz_date.tm_mon = zi.tmz_date.tm_year = 0;
438 zi.dosDate = 0;
439 zi.internal_fa = 0;
440 zi.external_fa = 0;
441
442 std::string complete_filename = basedir_in_zip_file_ + "/" + filename;
443
444 // create file
445 switch (zipOpenNewFileInZip3(zip_file_->write_handle(), complete_filename.c_str(), &zi, nullptr,
446 0, nullptr, 0, nullptr /* comment*/, Z_DEFLATED, Z_BEST_COMPRESSION,
447 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, nullptr, 0)) {
448 case ZIP_OK:
449 break;
450 default:
451 throw ZipOperationError("ZipFilesystem::write", complete_filename, zip_file_->path());
452 }
453
454 switch (zipWriteInFileInZip(zip_file_->write_handle(), data, length)) {
455 case ZIP_OK:
456 break;
457 case ZIP_ERRNO:
458 throw FileError(
459 "ZipFilesystem::write", complete_filename,
460 (boost::format("in path '%s'', Error") % zip_file_->path() % strerror(errno)).str());
461 default:
462 throw FileError("ZipFilesystem::write", complete_filename,
463 (boost::format("in path '%s'") % zip_file_->path()).str());
464 }
465
466 zipCloseFileInZip(zip_file_->write_handle());
467 }
468
open_stream_read(const std::string & fname)469 StreamRead* ZipFilesystem::open_stream_read(const std::string& fname) {
470 if (!file_exists(fname.c_str()) || is_directory(fname.c_str())) {
471 throw ZipOperationError(
472 "ZipFilesystem::load", fname, zip_file_->path(), "could not open file from zipfile");
473 }
474
475 int32_t method;
476 int result = unzOpenCurrentFile3(zip_file_->read_handle(), &method, nullptr, 1, nullptr);
477 switch (result) {
478 case ZIP_OK:
479 break;
480 default:
481 throw ZipOperationError(
482 "ZipFilesystem: Failed to open streamwrite", fname, zip_file_->path());
483 }
484 return new ZipStreamRead(zip_file_);
485 }
486
open_stream_write(const std::string & fname)487 StreamWrite* ZipFilesystem::open_stream_write(const std::string& fname) {
488 zip_fileinfo zi;
489
490 zi.tmz_date.tm_sec = zi.tmz_date.tm_min = zi.tmz_date.tm_hour = zi.tmz_date.tm_mday =
491 zi.tmz_date.tm_mon = zi.tmz_date.tm_year = 0;
492 zi.dosDate = 0;
493 zi.internal_fa = 0;
494 zi.external_fa = 0;
495
496 std::string complete_filename = basedir_in_zip_file_ + "/" + fname;
497 // create file
498 switch (zipOpenNewFileInZip3(zip_file_->write_handle(), complete_filename.c_str(), &zi, nullptr,
499 0, nullptr, 0, nullptr /* comment*/, Z_DEFLATED, Z_BEST_COMPRESSION,
500 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, nullptr, 0)) {
501 case ZIP_OK:
502 break;
503 default:
504 throw ZipOperationError(
505 "ZipFilesystem: Failed to open streamwrite", complete_filename, zip_file_->path());
506 }
507 return new ZipStreamWrite(zip_file_);
508 }
509
fs_rename(const std::string &,const std::string &)510 void ZipFilesystem::fs_rename(const std::string&, const std::string&) {
511 throw wexception("rename inside zip FS is not implemented yet");
512 }
513
disk_space()514 unsigned long long ZipFilesystem::disk_space() {
515 return 0;
516 }
517
get_basename()518 std::string ZipFilesystem::get_basename() {
519 return zip_file_->path();
520 }
521
ZipStreamRead(const std::shared_ptr<ZipFile> & shared_data)522 ZipFilesystem::ZipStreamRead::ZipStreamRead(const std::shared_ptr<ZipFile>& shared_data)
523 : zip_file_(shared_data) {
524 }
525
~ZipStreamRead()526 ZipFilesystem::ZipStreamRead::~ZipStreamRead() {
527 }
528
data(void * read_data,size_t bufsize)529 size_t ZipFilesystem::ZipStreamRead::data(void* read_data, size_t bufsize) {
530 int copied = unzReadCurrentFile(zip_file_->read_handle(), read_data, bufsize);
531 if (copied < 0) {
532 throw DataError("Failed to read from zip file %s", zip_file_->path().c_str());
533 }
534 if (copied == 0) {
535 throw DataError("End of file reached while reading zip %s", zip_file_->path().c_str());
536 }
537 return copied;
538 }
539
end_of_file() const540 bool ZipFilesystem::ZipStreamRead::end_of_file() const {
541 return unzReadCurrentFile(zip_file_->read_handle(), nullptr, 1) == 0;
542 }
543
ZipStreamWrite(const std::shared_ptr<ZipFile> & shared_data)544 ZipFilesystem::ZipStreamWrite::ZipStreamWrite(const std::shared_ptr<ZipFile>& shared_data)
545 : zip_file_(shared_data) {
546 }
547
~ZipStreamWrite()548 ZipFilesystem::ZipStreamWrite::~ZipStreamWrite() {
549 }
550
data(const void * const write_data,const size_t size)551 void ZipFilesystem::ZipStreamWrite::data(const void* const write_data, const size_t size) {
552 int result = zipWriteInFileInZip(zip_file_->write_handle(), write_data, size);
553 switch (result) {
554 case ZIP_OK:
555 break;
556 default:
557 throw wexception("Failed to write into zipfile %s", zip_file_->path().c_str());
558 }
559 }
560