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