1 /*
2  * Copyright (C) 2011-2020 Daniel Scharrer
3  *
4  * This software is provided 'as-is', without any express or implied
5  * warranty.  In no event will the author(s) be held liable for any damages
6  * arising from the use of this software.
7  *
8  * Permission is granted to anyone to use this software for any purpose,
9  * including commercial applications, and to alter it and redistribute it
10  * freely, subject to the following restrictions:
11  *
12  * 1. The origin of this software must not be misrepresented; you must not
13  *    claim that you wrote the original software. If you use this software
14  *    in a product, an acknowledgment in the product documentation would be
15  *    appreciated but is not required.
16  * 2. Altered source versions must be plainly marked as such, and must not be
17  *    misrepresented as being the original software.
18  * 3. This notice may not be removed or altered from any source distribution.
19  */
20 
21 #include "cli/extract.hpp"
22 
23 #include <algorithm>
24 #include <cmath>
25 #include <iomanip>
26 #include <iostream>
27 #include <map>
28 #include <sstream>
29 #include <vector>
30 #include <limits>
31 
32 #include <boost/foreach.hpp>
33 #include <boost/noncopyable.hpp>
34 #include <boost/scoped_ptr.hpp>
35 #include <boost/unordered_map.hpp>
36 #include <boost/algorithm/string/case_conv.hpp>
37 #include <boost/filesystem/operations.hpp>
38 #include <boost/ptr_container/ptr_map.hpp>
39 #include <boost/ptr_container/ptr_vector.hpp>
40 #include <boost/range/size.hpp>
41 
42 #include <boost/version.hpp>
43 #if BOOST_VERSION >= 104800
44 #include <boost/container/flat_map.hpp>
45 #endif
46 
47 #include "cli/debug.hpp"
48 #include "cli/gog.hpp"
49 #include "cli/goggalaxy.hpp"
50 
51 #include "crypto/checksum.hpp"
52 #include "crypto/hasher.hpp"
53 
54 #include "loader/offsets.hpp"
55 
56 #include "setup/data.hpp"
57 #include "setup/directory.hpp"
58 #include "setup/expression.hpp"
59 #include "setup/file.hpp"
60 #include "setup/info.hpp"
61 #include "setup/language.hpp"
62 
63 #include "stream/chunk.hpp"
64 #include "stream/file.hpp"
65 #include "stream/slice.hpp"
66 
67 #include "util/boostfs_compat.hpp"
68 #include "util/console.hpp"
69 #include "util/encoding.hpp"
70 #include "util/fstream.hpp"
71 #include "util/load.hpp"
72 #include "util/log.hpp"
73 #include "util/output.hpp"
74 #include "util/time.hpp"
75 
76 namespace fs = boost::filesystem;
77 
78 namespace {
79 
80 template <typename Entry>
81 class processed_item {
82 
83 	std::string path_;
84 	const Entry * entry_;
85 
86 public:
87 
processed_item(const std::string & path,const Entry * entry)88 	processed_item(const std::string & path, const Entry * entry)
89 		: path_(path), entry_(entry) { }
90 
has_entry() const91 	bool has_entry() const { return entry_ != NULL; }
entry() const92 	const Entry & entry() const { return *entry_; }
path() const93 	const std::string & path() const { return path_; }
94 
set_entry(const Entry * entry)95 	void set_entry(const Entry * entry) { entry_ = entry; }
set_path(const std::string & path)96 	void set_path(const std::string & path) { path_ = path; }
97 
98 };
99 
100 class processed_file : public processed_item<setup::file_entry> {
101 
102 public:
103 
processed_file(const setup::file_entry * entry,const std::string & path)104 	processed_file(const setup::file_entry * entry, const std::string & path)
105 		: processed_item<setup::file_entry>(path, entry) { }
106 
is_multipart() const107 	bool is_multipart() const { return !entry().additional_locations.empty(); }
108 
109 };
110 
111 class processed_directory : public processed_item<setup::directory_entry> {
112 
113 	bool implied_;
114 
115 public:
116 
processed_directory(const std::string & path)117 	explicit processed_directory(const std::string & path)
118 		: processed_item<setup::directory_entry>(path, NULL), implied_(false) { }
119 
implied() const120 	bool implied() const { return implied_; }
121 
set_implied(bool implied)122 	void set_implied(bool implied) { implied_ = implied; }
123 
124 };
125 
126 class file_output : private boost::noncopyable {
127 
128 	fs::path path_;
129 	const processed_file * file_;
130 	util::fstream stream_;
131 
132 	crypto::hasher checksum_;
133 	boost::uint64_t checksum_position_;
134 
135 	boost::uint64_t position_;
136 	boost::uint64_t total_written_;
137 
138 	bool write_;
139 
140 public:
141 
file_output(const fs::path & dir,const processed_file * f,bool write)142 	explicit file_output(const fs::path & dir, const processed_file * f, bool write)
143 		: path_(dir / f->path())
144 		, file_(f)
145 		, checksum_(f->entry().checksum.type)
146 		, checksum_position_(f->entry().checksum.type == crypto::None ? boost::uint64_t(-1) : 0)
147 		, position_(0)
148 		, total_written_(0)
149 		, write_(write)
150 	{
151 		if(write_) {
152 			try {
153 				std::ios_base::openmode flags = std::ios_base::out | std::ios_base::binary | std::ios_base::trunc;
154 				if(file_->is_multipart()) {
155 					flags |= std::ios_base::in;
156 				}
157 				stream_.open(path_, flags);
158 				if(!stream_.is_open()) {
159 					throw std::exception();
160 				}
161 			} catch(...) {
162 				throw std::runtime_error("Coul not open output file \"" + path_.string() + '"');
163 			}
164 		}
165 	}
166 
write(const char * data,size_t n)167 	bool write(const char * data, size_t n) {
168 
169 		if(write_) {
170 			stream_.write(data, std::streamsize(n));
171 		}
172 
173 		if(checksum_position_ == position_) {
174 			checksum_.update(data, n);
175 			checksum_position_ += n;
176 		}
177 
178 		position_ += n;
179 		total_written_ += n;
180 
181 		return !write_ || !stream_.fail();
182 	}
183 
seek(boost::uint64_t new_position)184 	void seek(boost::uint64_t new_position) {
185 
186 		if(new_position == position_) {
187 			return;
188 		}
189 
190 		debug("seeking output from " << print_hex(position_) << " to " << print_hex(new_position));
191 
192 		if(!write_) {
193 			position_ = new_position;
194 			return;
195 		}
196 
197 		const boost::uint64_t max = boost::uint64_t(std::numeric_limits<util::fstream::off_type>::max() / 4);
198 
199 		if(new_position <= max) {
200 			stream_.seekp(util::fstream::off_type(new_position), std::ios_base::beg);
201 		} else {
202 			util::fstream::off_type sign = (new_position > position_) ? 1 : -1;
203 			boost::uint64_t diff = (new_position > position_) ? new_position - position_ : position_ - new_position;
204 			while(diff > 0) {
205 				stream_.seekp(sign * util::fstream::off_type(std::min(diff, max)), std::ios_base::cur);
206 				diff -= std::min(diff, max);
207 			}
208 		}
209 
210 		position_ = new_position;
211 
212 	}
213 
close()214 	void close() {
215 
216 		if(write_) {
217 			stream_.close();
218 		}
219 
220 	}
221 
path() const222 	const fs::path & path() const { return path_; }
file() const223 	const processed_file * file() const { return file_; }
224 
is_complete() const225 	bool is_complete() const {
226 		return total_written_ == file_->entry().size;
227 	}
228 
has_checksum() const229 	bool has_checksum() const {
230 		return checksum_position_ == file_->entry().size;
231 	}
232 
calculate_checksum()233 	bool calculate_checksum() {
234 
235 		if(has_checksum()) {
236 			return true;
237 		}
238 
239 		if(!write_) {
240 			return false;
241 		}
242 
243 		debug("calculating output checksum for " << path_);
244 
245 		const boost::uint64_t max = boost::uint64_t(std::numeric_limits<util::fstream::off_type>::max() / 4);
246 
247 		boost::uint64_t diff = checksum_position_;
248 		stream_.seekg(util::fstream::off_type(std::min(diff, max)), std::ios_base::beg);
249 		diff -= std::min(diff, max);
250 		while(diff > 0) {
251 			stream_.seekg(util::fstream::off_type(std::min(diff, max)), std::ios_base::cur);
252 			diff -= std::min(diff, max);
253 		}
254 
255 		while(!stream_.eof()) {
256 			char buffer[8192];
257 			std::streamsize n = stream_.read(buffer, sizeof(buffer)).gcount();
258 			checksum_.update(buffer, size_t(n));
259 			checksum_position_ += boost::uint64_t(n);
260 		}
261 
262 		if(!has_checksum()) {
263 			log_warning << "Could not read back " << path_ << " to calculate output checksum for multi-part file";
264 			return false;
265 		}
266 
267 		return true;
268 	}
269 
checksum()270 	crypto::checksum checksum() {
271 		return checksum_.finalize();
272 	}
273 
274 };
275 
276 class path_filter {
277 
278 	typedef std::pair<bool, std::string> Filter;
279 	std::vector<Filter> includes;
280 
281 public:
282 
path_filter(const extract_options & o)283 	explicit path_filter(const extract_options & o) {
284 		BOOST_FOREACH(const std::string & include, o.include) {
285 			if(!include.empty() && include[0] == setup::path_sep) {
286 				includes.push_back(Filter(true, boost::to_lower_copy(include) + setup::path_sep));
287 			} else {
288 				includes.push_back(Filter(false, setup::path_sep + boost::to_lower_copy(include)
289 				                                 + setup::path_sep));
290 			}
291 		}
292 	}
293 
match(const std::string & path) const294 	bool match(const std::string & path) const {
295 
296 		if(includes.empty()) {
297 			return true;
298 		}
299 
300 		BOOST_FOREACH(const Filter & i, includes) {
301 			if(i.first) {
302 				if(!i.second.compare(1, i.second.size() - 1,
303 				                     path + setup::path_sep, 0, i.second.size() - 1)) {
304 					return true;
305 				}
306 			} else {
307 				if((setup::path_sep + path + setup::path_sep).find(i.second) != std::string::npos) {
308 					return true;
309 				}
310 			}
311 		}
312 
313 		return false;
314 	}
315 
316 };
317 
print_filter_info(const setup::item & item,bool temp)318 void print_filter_info(const setup::item & item, bool temp) {
319 
320 	bool first = true;
321 
322 	if(!item.languages.empty()) {
323 		std::cout << " [";
324 		first = false;
325 		std::cout << color::green << item.languages << color::reset;
326 	}
327 
328 	if(temp) {
329 		std::cout << (first ? " [" : ", ");
330 		first = false;
331 		std::cout << color::cyan << "temp" << color::reset;
332 
333 	}
334 
335 	if(!first) {
336 		std::cout << "]";
337 	}
338 
339 }
340 
print_filter_info(const setup::file_entry & file)341 void print_filter_info(const setup::file_entry & file) {
342 	bool is_temp = !!(file.options & setup::file_entry::DeleteAfterInstall);
343 	print_filter_info(file, is_temp);
344 }
345 
print_filter_info(const setup::directory_entry & dir)346 void print_filter_info(const setup::directory_entry & dir) {
347 	bool is_temp = !!(dir.options & setup::directory_entry::DeleteAfterInstall);
348 	print_filter_info(dir, is_temp);
349 }
350 
print_size_info(const stream::file & file,boost::uint64_t size)351 void print_size_info(const stream::file & file, boost::uint64_t size) {
352 
353 	if(logger::debug) {
354 		std::cout << " @ " << print_hex(file.offset);
355 	}
356 
357 	std::cout << " (" << color::dim_cyan << print_bytes(size ? size : file.size) << color::reset << ")";
358 }
359 
print_checksum_info(const stream::file & file,const crypto::checksum * checksum)360 void print_checksum_info(const stream::file & file, const crypto::checksum * checksum) {
361 
362 	if(!checksum || checksum->type == crypto::None) {
363 		checksum = &file.checksum;
364 	}
365 
366 	std::cout << color::dim_magenta << *checksum << color::reset;
367 }
368 
prompt_overwrite()369 bool prompt_overwrite() {
370 	return true; // TODO the user always overwrites
371 }
372 
handle_collision(const setup::file_entry & oldfile,const setup::data_entry & olddata,const setup::file_entry & newfile,const setup::data_entry & newdata)373 const char * handle_collision(const setup::file_entry & oldfile, const setup::data_entry & olddata,
374                               const setup::file_entry & newfile, const setup::data_entry & newdata) {
375 
376 	bool allow_timestamp = true;
377 
378 	if(!(newfile.options & setup::file_entry::IgnoreVersion)) {
379 
380 		bool version_info_valid = !!(newdata.options & setup::data_entry::VersionInfoValid);
381 
382 		if(olddata.options & setup::data_entry::VersionInfoValid) {
383 			allow_timestamp = false;
384 
385 			if(!version_info_valid || olddata.file_version > newdata.file_version) {
386 				if(!(newfile.options & setup::file_entry::PromptIfOlder) || !prompt_overwrite()) {
387 					return "old version";
388 				}
389 			} else if(newdata.file_version == olddata.file_version
390 				   && !(newfile.options & setup::file_entry::OverwriteSameVersion)) {
391 
392 				if((newfile.options & setup::file_entry::ReplaceSameVersionIfContentsDiffer)
393 				   && olddata.file.checksum == newdata.file.checksum) {
394 					return "duplicate (checksum)";
395 				}
396 
397 				if(!(newfile.options & setup::file_entry::CompareTimeStamp)) {
398 					return "duplicate (version)";
399 				}
400 
401 				allow_timestamp = true;
402 			}
403 
404 		} else if(version_info_valid) {
405 			allow_timestamp = false;
406 		}
407 
408 	}
409 
410 	if(allow_timestamp && (newfile.options & setup::file_entry::CompareTimeStamp)) {
411 
412 		if(newdata.timestamp == olddata.timestamp
413 		   && newdata.timestamp_nsec == olddata.timestamp_nsec) {
414 			return "duplicate (modification time)";
415 		}
416 
417 
418 		if(newdata.timestamp < olddata.timestamp
419 		   || (newdata.timestamp == olddata.timestamp
420 		       && newdata.timestamp_nsec < olddata.timestamp_nsec)) {
421 			if(!(newfile.options & setup::file_entry::PromptIfOlder) || !prompt_overwrite()) {
422 				return "old version (modification time)";
423 			}
424 		}
425 
426 	}
427 
428 	if((newfile.options & setup::file_entry::ConfirmOverwrite) && !prompt_overwrite()) {
429 		return "user chose not to overwrite";
430 	}
431 
432 	if(oldfile.attributes != boost::uint32_t(-1)
433 	   && (oldfile.attributes & setup::file_entry::ReadOnly) != 0) {
434 		if(!(newfile.options & setup::file_entry::OverwriteReadOnly) && !prompt_overwrite()) {
435 			return "user chose not to overwrite read-only file";
436 		}
437 	}
438 
439 	return NULL; // overwrite old file
440 }
441 
442 typedef boost::unordered_map<std::string, processed_file> FilesMap;
443 #if BOOST_VERSION >= 104800
444 typedef boost::container::flat_map<std::string, processed_directory> DirectoriesMap;
445 #else
446 typedef std::map<std::string, processed_directory> DirectoriesMap;
447 #endif
448 typedef boost::unordered_map<std::string, std::vector<processed_file> > CollisionMap;
449 
parent_dir(const std::string & path)450 std::string parent_dir(const std::string & path) {
451 
452 	size_t pos = path.find_last_of(setup::path_sep);
453 	if(pos == std::string::npos) {
454 		return std::string();
455 	}
456 
457 	return path.substr(0, pos);
458 }
459 
insert_dirs(DirectoriesMap & processed_directories,const path_filter & includes,const std::string & internal_path,std::string & path,bool implied)460 bool insert_dirs(DirectoriesMap & processed_directories, const path_filter & includes,
461                  const std::string & internal_path, std::string & path, bool implied) {
462 
463 	std::string dir = parent_dir(path);
464 	std::string internal_dir = parent_dir(internal_path);
465 
466 	if(internal_dir.empty()) {
467 		return false;
468 	}
469 
470 	if(implied || includes.match(internal_dir)) {
471 
472 		std::pair<DirectoriesMap::iterator, bool> existing = processed_directories.insert(
473 			std::make_pair(internal_dir, processed_directory(dir))
474 		);
475 
476 		if(implied) {
477 			existing.first->second.set_implied(true);
478 		}
479 
480 		if(!existing.second) {
481 			if(existing.first->second.path() != dir) {
482 				// Existing dir case differs, fix path
483 				if(existing.first->second.path().length() == dir.length()) {
484 					path.replace(0, dir.length(), existing.first->second.path());
485 				} else {
486 					path = existing.first->second.path() + path.substr(dir.length());
487 				}
488 				return true;
489 			} else {
490 				return false;
491 			}
492 		}
493 
494 		implied = true;
495 	}
496 
497 	size_t oldlength = dir.length();
498 	if(insert_dirs(processed_directories, includes, internal_dir, dir, implied)) {
499 		// Existing dir case differs, fix path
500 		if(dir.length() == oldlength) {
501 			path.replace(0, dir.length(), dir);
502 		} else {
503 			path = dir + path.substr(oldlength);
504 		}
505 		// Also fix previously inserted directory
506 		DirectoriesMap::iterator inserted = processed_directories.find(internal_dir);
507 		if(inserted != processed_directories.end()) {
508 			inserted->second.set_path(dir);
509 		}
510 		return true;
511 	}
512 
513 	return false;
514 }
515 
rename_collision(const extract_options & o,FilesMap & processed_files,const std::string & path,const processed_file & other,bool common_component,bool common_language,bool common_arch,bool first)516 bool rename_collision(const extract_options & o, FilesMap & processed_files, const std::string & path,
517                       const processed_file & other, bool common_component, bool common_language,
518                       bool common_arch, bool first) {
519 
520 	const setup::file_entry & file = other.entry();
521 
522 	bool require_number_suffix = !first || (o.collisions == RenameAllCollisions);
523 	std::ostringstream oss;
524 	const setup::file_entry::flags arch_flags = setup::file_entry::Bits32 | setup::file_entry::Bits64;
525 
526 	if(!common_component && !file.components.empty()) {
527 		if(setup::is_simple_expression(file.components)) {
528 			require_number_suffix = false;
529 			oss << '#' << file.components;
530 		}
531 	}
532 	if(!common_language && !file.languages.empty()) {
533 		if(setup::is_simple_expression(file.languages)) {
534 			require_number_suffix = false;
535 			if(file.languages != o.default_language) {
536 				oss << '@' << file.languages;
537 			}
538 		}
539 	}
540 	if(!common_arch && (file.options & arch_flags) == setup::file_entry::Bits32) {
541 		require_number_suffix = false;
542 		oss << "@32bit";
543 	} else if(!common_arch && (file.options & arch_flags) == setup::file_entry::Bits64) {
544 		require_number_suffix = false;
545 		oss << "@64bit";
546 	}
547 
548 	size_t i = 0;
549 	std::string suffix = oss.str();
550 	if(require_number_suffix) {
551 		oss << '$' << i++;
552 	}
553 	for(;;) {
554 		std::pair<FilesMap::iterator, bool> insertion = processed_files.insert(std::make_pair(
555 			path + oss.str(), processed_file(&file, other.path() + oss.str())
556 		));
557 		if(insertion.second) {
558 			// Found an available name and inserted
559 			return true;
560 		}
561 		if(&insertion.first->second.entry() == &file) {
562 			// File already has the desired name, abort
563 			return false;
564 		}
565 		oss.str(suffix);
566 		oss << '$' << i++;
567 	}
568 
569 }
570 
rename_collisions(const extract_options & o,FilesMap & processed_files,const CollisionMap & collisions)571 void rename_collisions(const extract_options & o, FilesMap & processed_files,
572                        const CollisionMap & collisions) {
573 
574 	BOOST_FOREACH(const CollisionMap::value_type & collision, collisions) {
575 
576 		const std::string & path = collision.first;
577 
578 		const processed_file & base = processed_files.find(path)->second;
579 		const setup::file_entry & file = base.entry();
580 		const setup::file_entry::flags arch_flags = setup::file_entry::Bits32 | setup::file_entry::Bits64;
581 
582 		bool common_component = true;
583 		bool common_language = true;
584 		bool common_arch = true;
585 		BOOST_FOREACH(const processed_file & other, collision.second) {
586 			common_component = common_component && other.entry().components == file.components;
587 			common_language = common_language && other.entry().languages == file.languages;
588 			common_arch = common_arch && (other.entry().options & arch_flags) == (file.options & arch_flags);
589 		}
590 
591 		bool ignore_component = common_component || o.collisions != RenameAllCollisions;
592 		if(rename_collision(o, processed_files, path, base,
593 		                    ignore_component, common_language, common_arch, true)) {
594 			processed_files.erase(path);
595 		}
596 
597 		BOOST_FOREACH(const processed_file & other, collision.second) {
598 			rename_collision(o, processed_files, path, other,
599 			                 common_component, common_language, common_arch, false);
600 		}
601 
602 	}
603 }
604 
print_file_info(const extract_options & o,const setup::info & info)605 bool print_file_info(const extract_options & o, const setup::info & info) {
606 
607 	if(!o.quiet) {
608 		const std::string & name = info.header.app_versioned_name.empty()
609 		                           ? info.header.app_name : info.header.app_versioned_name;
610 		const char * verb = "Inspecting";
611 		if(o.extract) {
612 			verb = "Extracting";
613 		} else if(o.test) {
614 			verb = "Testing";
615 		} else if(o.list) {
616 			verb = "Listing";
617 		}
618 		std::cout << verb << " \"" << color::green << name << color::reset
619 		          << "\" - setup data version " << color::white << info.version << color::reset
620 		          << std::endl;
621 	}
622 
623 	#ifdef DEBUG
624 	if(logger::debug) {
625 		std::cout << '\n';
626 		print_info(info);
627 		std::cout << '\n';
628 	}
629 	#endif
630 
631 	bool multiple_sections = (o.list_languages + o.gog_game_id + o.list + o.show_password > 1);
632 	if(!o.quiet && multiple_sections) {
633 		std::cout << '\n';
634 	}
635 
636 	if(o.list_languages) {
637 		if(o.silent) {
638 			BOOST_FOREACH(const setup::language_entry & language, info.languages) {
639 				std::cout << language.name <<' ' << language.language_name << '\n';
640 			}
641 		} else {
642 			if(multiple_sections) {
643 				std::cout << "Languages:\n";
644 			}
645 			BOOST_FOREACH(const setup::language_entry & language, info.languages) {
646 				std::cout << " - " << color::green << language.name << color::reset;
647 				if(!language.language_name.empty()) {
648 					std::cout << ": " << color::white << language.language_name << color::reset;
649 				}
650 				std::cout << '\n';
651 			}
652 			if(info.languages.empty()) {
653 				std::cout << " (none)\n";
654 			}
655 		}
656 		if((o.silent || !o.quiet) && multiple_sections) {
657 			std::cout << '\n';
658 		}
659 	}
660 
661 	if(o.gog_game_id) {
662 		std::string id = gog::get_game_id(info);
663 		if(id.empty()) {
664 			if(!o.quiet) {
665 				std::cout << "No GOG.com game ID found!\n";
666 			}
667 		} else if(!o.silent) {
668 			std::cout << "GOG.com game ID is " << color::cyan << id << color::reset << '\n';
669 		} else {
670 			std::cout << id << '\n';
671 		}
672 		if((o.silent || !o.quiet) && multiple_sections) {
673 			std::cout << '\n';
674 		}
675 	}
676 
677 	if(o.show_password) {
678 		if(info.header.options & setup::header::Password) {
679 			if(o.silent) {
680 				std::cout << info.header.password << '\n';
681 			} else {
682 				std::cout << "Password hash: " << color::yellow << info.header.password << color::reset << '\n';
683 			}
684 			if(o.silent) {
685 				std::cout << print_hex(info.header.password_salt) << '\n';
686 			} else if(!info.header.password_salt.empty()) {
687 				std::cout << "Password salt: " << color::yellow
688 				          << print_hex(info.header.password_salt) << color::reset;
689 				if(!o.quiet) {
690 					std::cout << " (hex bytes, prepended to password)";
691 				}
692 				std::cout << '\n';
693 			}
694 			if(o.silent) {
695 				std::cout << util::encoding_name(info.codepage) << '\n';
696 			} else {
697 				std::cout << "Password encoding: " << color::yellow
698 				          << util::encoding_name(info.codepage) << color::reset << '\n';
699 			}
700 		} else if(!o.quiet) {
701 			std::cout << "Setup is not passworded!\n";
702 		}
703 		if((o.silent || !o.quiet) && multiple_sections) {
704 			std::cout << '\n';
705 		}
706 	}
707 
708 	return multiple_sections;
709 }
710 
711 struct processed_entries {
712 
713 	FilesMap files;
714 
715 	DirectoriesMap directories;
716 
717 };
718 
filter_entries(const extract_options & o,const setup::info & info)719 processed_entries filter_entries(const extract_options & o, const setup::info & info) {
720 
721 	processed_entries processed;
722 
723 	#if BOOST_VERSION >= 105000
724 	processed.files.reserve(info.files.size());
725 	#endif
726 
727 	#if BOOST_VERSION >= 104800
728 	processed.directories.reserve(info.directories.size()
729 	                              + size_t(std::log(double(info.files.size()))));
730 	#endif
731 
732 	CollisionMap collisions;
733 
734 	path_filter includes(o);
735 
736 	// Filter the directories to be created
737 	BOOST_FOREACH(const setup::directory_entry & directory, info.directories) {
738 
739 		if(!o.extract_temp && (directory.options & setup::directory_entry::DeleteAfterInstall)) {
740 			continue; // Ignore temporary dirs
741 		}
742 
743 		if(!directory.languages.empty()) {
744 			if(!o.language.empty() && !setup::expression_match(o.language, directory.languages)) {
745 				continue; // Ignore other languages
746 			}
747 		} else if(o.language_only) {
748 			continue; // Ignore language-agnostic dirs
749 		}
750 
751 		std::string path = o.filenames.convert(directory.name);
752 		if(path.empty()) {
753 			continue; // Don't know what to do with this
754 		}
755 		std::string internal_path = boost::algorithm::to_lower_copy(path);
756 
757 		bool path_included = includes.match(internal_path);
758 
759 		insert_dirs(processed.directories, includes, internal_path, path, path_included);
760 
761 		DirectoriesMap::iterator it;
762 		if(path_included) {
763 			std::pair<DirectoriesMap::iterator, bool> existing = processed.directories.insert(
764 				std::make_pair(internal_path, processed_directory(path))
765 			);
766 			it = existing.first;
767 		} else {
768 			it = processed.directories.find(internal_path);
769 			if(it == processed.directories.end()) {
770 				continue;
771 			}
772 		}
773 
774 		it->second.set_entry(&directory);
775 	}
776 
777 	// Filter the files to be extracted
778 	BOOST_FOREACH(const setup::file_entry & file, info.files) {
779 
780 		if(file.location >= info.data_entries.size()) {
781 			continue; // Ignore external files (copy commands)
782 		}
783 
784 		if(!o.extract_temp && (file.options & setup::file_entry::DeleteAfterInstall)) {
785 			continue; // Ignore temporary files
786 		}
787 
788 		if(!file.languages.empty()) {
789 			if(!o.language.empty() && !setup::expression_match(o.language, file.languages)) {
790 				continue; // Ignore other languages
791 			}
792 		} else if(o.language_only) {
793 			continue; // Ignore language-agnostic files
794 		}
795 
796 		std::string path = o.filenames.convert(file.destination);
797 		if(path.empty()) {
798 			continue; // Internal file, not extracted
799 		}
800 		std::string internal_path = boost::algorithm::to_lower_copy(path);
801 
802 		bool path_included = includes.match(internal_path);
803 
804 		insert_dirs(processed.directories, includes, internal_path, path, path_included);
805 
806 		if(!path_included) {
807 			continue; // Ignore excluded file
808 		}
809 
810 		std::pair<FilesMap::iterator, bool> insertion = processed.files.insert(std::make_pair(
811 			internal_path, processed_file(&file, path)
812 		));
813 
814 		if(!insertion.second) {
815 			// Collision!
816 			processed_file & existing = insertion.first->second;
817 
818 			if(o.collisions == ErrorOnCollisions) {
819 				throw std::runtime_error("Collision: " + path);
820 			} else if(o.collisions == RenameAllCollisions) {
821 				collisions[internal_path].push_back(processed_file(&file, path));
822 			} else {
823 
824 				const setup::data_entry & newdata = info.data_entries[file.location];
825 				const setup::data_entry & olddata = info.data_entries[existing.entry().location];
826 				const char * skip = handle_collision(existing.entry(), olddata, file, newdata);
827 
828 				if(!o.default_language.empty()) {
829 					bool oldlang = setup::expression_match(o.default_language, file.languages);
830 					bool newlang = setup::expression_match(o.default_language, existing.entry().languages);
831 					if(oldlang && !newlang) {
832 						skip = NULL;
833 					} else if(!oldlang && newlang) {
834 						skip = "overwritten";
835 					}
836 				}
837 
838 				if(o.collisions == RenameCollisions) {
839 					const setup::file_entry & clobberedfile = skip ? file : existing.entry();
840 					const std::string & clobberedpath = skip ? path : existing.path();
841 					collisions[internal_path].push_back(processed_file(&clobberedfile, clobberedpath));
842 				} else if(!o.silent) {
843 					std::cout << " - ";
844 					const std::string & clobberedpath = skip ? path : existing.path();
845 					std::cout << '"' << color::dim_yellow << clobberedpath << color::reset << '"';
846 					print_filter_info(skip ? file : existing.entry());
847 					if(o.list_sizes) {
848 						print_size_info(skip ? newdata.file : olddata.file, skip ? file.size : existing.entry().size);
849 					}
850 					if(o.list_checksums) {
851 						std::cout << ' ';
852 						print_checksum_info(skip ? newdata.file : olddata.file,
853 						                    skip ? &file.checksum : &existing.entry().checksum);
854 					}
855 					std::cout << " - " << (skip ? skip : "overwritten") << '\n';
856 				}
857 
858 				if(!skip) {
859 					existing.set_entry(&file);
860 					if(file.type != setup::file_entry::UninstExe) {
861 						// Old file is "deleted" first → use case from new file
862 						existing.set_path(path);
863 					}
864 				}
865 
866 			}
867 
868 		}
869 
870 	}
871 
872 	if(o.collisions == RenameCollisions || o.collisions == RenameAllCollisions) {
873 		rename_collisions(o, processed.files, collisions);
874 	}
875 
876 	return processed;
877 }
878 
create_output_directory(const extract_options & o)879 void create_output_directory(const extract_options & o) {
880 
881 	try {
882 		if(!o.output_dir.empty() && !fs::exists(o.output_dir)) {
883 			fs::create_directory(o.output_dir);
884 		}
885 	} catch(...) {
886 		throw std::runtime_error("Could not create output directory \"" + o.output_dir.string() + '"');
887 	}
888 
889 }
890 
891 } // anonymous namespace
892 
process_file(const fs::path & installer,const extract_options & o)893 void process_file(const fs::path & installer, const extract_options & o) {
894 
895 	bool is_directory;
896 	try {
897 		is_directory = fs::is_directory(installer);
898 	} catch(...) {
899 		throw std::runtime_error("Could not open file \"" + installer.string()
900 		                         + "\": access denied");
901 	}
902 	if(is_directory) {
903 		throw std::runtime_error("Input file \"" + installer.string() + "\" is a directory!");
904 	}
905 
906 	util::ifstream ifs;
907 	try {
908 		ifs.open(installer, std::ios_base::in | std::ios_base::binary);
909 		if(!ifs.is_open()) {
910 			throw std::exception();
911 		}
912 	} catch(...) {
913 		throw std::runtime_error("Could not open file \"" + installer.string() + '"');
914 	}
915 
916 	loader::offsets offsets;
917 	offsets.load(ifs);
918 
919 	#ifdef DEBUG
920 	if(logger::debug) {
921 		print_offsets(offsets);
922 		std::cout << '\n';
923 	}
924 	#endif
925 
926 	if(o.data_version)  {
927 		setup::version version;
928 		ifs.seekg(offsets.header_offset);
929 		version.load(ifs);
930 		if(o.silent) {
931 			std::cout << version << '\n';
932 		} else {
933 			std::cout << color::white << version << color::reset << '\n';
934 		}
935 		return;
936 	}
937 
938 	#ifdef DEBUG
939 	if(o.dump_headers)  {
940 		create_output_directory(o);
941 		dump_headers(ifs, offsets, o);
942 		return;
943 	}
944 	#endif
945 
946 	setup::info::entry_types entries = 0;
947 	if(o.list || o.test || o.extract || (o.gog_galaxy && o.list_languages)) {
948 		entries |= setup::info::Files;
949 		entries |= setup::info::Directories;
950 		entries |= setup::info::DataEntries;
951 	}
952 	if(o.list_languages) {
953 		entries |= setup::info::Languages;
954 	}
955 	if(o.gog_game_id || o.gog) {
956 		entries |= setup::info::RegistryEntries;
957 	}
958 	if(!o.extract_unknown) {
959 		entries |= setup::info::NoUnknownVersion;
960 	}
961 #ifdef DEBUG
962 	if(logger::debug) {
963 		entries = setup::info::entry_types::all();
964 	}
965 #endif
966 
967 	ifs.seekg(offsets.header_offset);
968 	setup::info info;
969 	try {
970 		info.load(ifs, entries, o.codepage);
971 	} catch(const setup::version_error &) {
972 		fs::path headerfile = installer;
973 		headerfile.replace_extension(".0");
974 		if(offsets.header_offset == 0 && headerfile != installer && fs::exists(headerfile)) {
975 			log_info << "Opening \"" << color::cyan << headerfile.string() << color::reset << '"';
976 			process_file(headerfile, o);
977 			return;
978 		}
979 		if(offsets.found_magic) {
980 			if(offsets.header_offset == 0) {
981 				throw format_error("Could not determine location of setup headers!");
982 			} else {
983 				throw format_error("Could not determine setup data version!");
984 			}
985 		}
986 		throw;
987 	} catch(const std::exception & e) {
988 		std::ostringstream oss;
989 		oss << "Stream error while parsing setup headers!\n";
990 		oss << " ├─ detected setup version: " << info.version << '\n';
991 		oss << " └─ error reason: " << e.what();
992 		throw format_error(oss.str());
993 	}
994 
995 	if(o.gog_galaxy && (o.list || o.test || o.extract || o.list_languages)) {
996 		gog::parse_galaxy_files(info, o.gog);
997 	}
998 
999 	bool multiple_sections = print_file_info(o, info);
1000 
1001 	std::string password;
1002 	if(o.password.empty()) {
1003 		if(!o.quiet && (o.list || o.test || o.extract) && (info.header.options & setup::header::EncryptionUsed)) {
1004 			log_warning << "Setup contains encrypted files, use the --password option to extract them";
1005 		}
1006 	} else {
1007 		util::from_utf8(o.password, password, info.codepage);
1008 		if(info.header.options & setup::header::Password) {
1009 			crypto::hasher checksum(info.header.password.type);
1010 			checksum.update(info.header.password_salt.c_str(), info.header.password_salt.length());
1011 			checksum.update(password.c_str(), password.length());
1012 			if(checksum.finalize() != info.header.password) {
1013 				if(o.check_password) {
1014 					throw std::runtime_error("Incorrect password provided");
1015 				}
1016 				log_error << "Incorrect password provided";
1017 				password.clear();
1018 			}
1019 		}
1020 		#if !INNOEXTRACT_HAVE_ARC4
1021 		if((o.extract || o.test) && (info.header.options & setup::header::EncryptionUsed)) {
1022 			log_warning << "ARC4 decryption not supported in this build, skipping compressed chunks";
1023 		}
1024 		password.clear();
1025 		#endif
1026 	}
1027 
1028 	if(!o.list && !o.test && !o.extract) {
1029 		return;
1030 	}
1031 
1032 	if(!o.silent && multiple_sections) {
1033 		std::cout << "Files:\n";
1034 	}
1035 
1036 	processed_entries processed = filter_entries(o, info);
1037 
1038 	if(o.extract) {
1039 		create_output_directory(o);
1040 	}
1041 
1042 	if(o.list || o.extract) {
1043 
1044 		BOOST_FOREACH(const DirectoriesMap::value_type & i, processed.directories) {
1045 
1046 			const std::string & path = i.second.path();
1047 
1048 			if(o.list && !i.second.implied()) {
1049 
1050 				if(!o.silent) {
1051 
1052 					std::cout << " - ";
1053 					std::cout << '"' << color::dim_white << path << setup::path_sep << color::reset << '"';
1054 					if(i.second.has_entry()) {
1055 						print_filter_info(i.second.entry());
1056 					}
1057 					std::cout << '\n';
1058 
1059 				} else {
1060 					std::cout << color::dim_white << path << setup::path_sep << color::reset << '\n';
1061 				}
1062 
1063 			}
1064 
1065 			if(o.extract) {
1066 				fs::path dir = o.output_dir / path;
1067 				try {
1068 					fs::create_directory(dir);
1069 				} catch(...) {
1070 					throw std::runtime_error("Could not create directory \"" + dir.string() + '"');
1071 				}
1072 			}
1073 
1074 		}
1075 
1076 	}
1077 
1078 	typedef std::pair<const processed_file *, boost::uint64_t> output_location;
1079 	std::vector< std::vector<output_location> > files_for_location;
1080 	files_for_location.resize(info.data_entries.size());
1081 	BOOST_FOREACH(const FilesMap::value_type & i, processed.files) {
1082 		const processed_file & file = i.second;
1083 		files_for_location[file.entry().location].push_back(output_location(&file, 0));
1084 		if(o.test || o.extract) {
1085 			boost::uint64_t offset = info.data_entries[file.entry().location].uncompressed_size;
1086 			boost::uint32_t sort_slice = info.data_entries[file.entry().location].chunk.first_slice;
1087 			boost::uint32_t sort_offset = info.data_entries[file.entry().location].chunk.sort_offset;
1088 			BOOST_FOREACH(boost::uint32_t location, file.entry().additional_locations) {
1089 				setup::data_entry & data = info.data_entries[location];
1090 				files_for_location[location].push_back(output_location(&file, offset));
1091 				offset += data.uncompressed_size;
1092 				if(data.chunk.first_slice > sort_slice ||
1093 				   (data.chunk.first_slice == sort_slice && data.chunk.sort_offset > sort_offset)) {
1094 					sort_slice = data.chunk.first_slice;
1095 					sort_offset = data.chunk.sort_offset;
1096 				} else if(data.chunk.first_slice == sort_slice && data.chunk.sort_offset == data.chunk.offset) {
1097 					data.chunk.sort_offset = ++sort_offset;
1098 				} else {
1099 					// Could not reorder chunk - no point in trying to reordder the remaining chunks
1100 					sort_slice = boost::uint32_t(-1);
1101 				}
1102 			}
1103 		}
1104 	}
1105 
1106 	boost::uint64_t total_size = 0;
1107 
1108 	typedef std::map<stream::file, size_t> Files;
1109 	typedef std::map<stream::chunk, Files> Chunks;
1110 	Chunks chunks;
1111 	for(size_t i = 0; i < info.data_entries.size(); i++) {
1112 		if(!files_for_location[i].empty()) {
1113 			setup::data_entry & location = info.data_entries[i];
1114 			chunks[location.chunk][location.file] = i;
1115 			total_size += location.uncompressed_size;
1116 		}
1117 	}
1118 
1119 	boost::scoped_ptr<stream::slice_reader> slice_reader;
1120 	if(o.extract || o.test) {
1121 		if(offsets.data_offset) {
1122 			slice_reader.reset(new stream::slice_reader(&ifs, offsets.data_offset));
1123 		} else {
1124 			fs::path dir = installer.parent_path();
1125 			std::string basename = util::as_string(installer.stem());
1126 			std::string basename2 = info.header.base_filename;
1127 			// Prevent access to unexpected files
1128 			std::replace(basename2.begin(), basename2.end(), '/', '_');
1129 			std::replace(basename2.begin(), basename2.end(), '\\', '_');
1130 			// Older Inno Setup versions used the basename stored in the headers, change our default accordingly
1131 			if(info.version < INNO_VERSION(4, 1, 7) && !basename2.empty()) {
1132 				std::swap(basename2, basename);
1133 			}
1134 			slice_reader.reset(new stream::slice_reader(dir, basename, basename2, info.header.slices_per_disk));
1135 		}
1136 	}
1137 
1138 	progress extract_progress(total_size);
1139 
1140 	typedef boost::ptr_map<const processed_file *, file_output> multi_part_outputs;
1141 	multi_part_outputs multi_outputs;
1142 
1143 	BOOST_FOREACH(const Chunks::value_type & chunk, chunks) {
1144 
1145 		debug("[starting " << chunk.first.compression << " chunk @ slice " << chunk.first.first_slice
1146 		      << " + " << print_hex(offsets.data_offset) << " + " << print_hex(chunk.first.offset)
1147 		      << ']');
1148 
1149 		stream::chunk_reader::pointer chunk_source;
1150 		if((o.extract || o.test) && (chunk.first.encryption == stream::Plaintext || !password.empty())) {
1151 			chunk_source = stream::chunk_reader::get(*slice_reader, chunk.first, password);
1152 		}
1153 		boost::uint64_t offset = 0;
1154 
1155 		BOOST_FOREACH(const Files::value_type & location, chunk.second) {
1156 			const stream::file & file = location.first;
1157 			const std::vector<output_location> & output_locations = files_for_location[location.second];
1158 
1159 			if(file.offset > offset) {
1160 				debug("discarding " << print_bytes(file.offset - offset)
1161 				      << " @ " << print_hex(offset));
1162 				if(chunk_source.get()) {
1163 					util::discard(*chunk_source, file.offset - offset);
1164 				}
1165 			}
1166 
1167 			// Print filename and size
1168 			if(o.list) {
1169 
1170 				extract_progress.clear(DeferredClear);
1171 
1172 				if(!o.silent) {
1173 
1174 					bool named = false;
1175 					boost::uint64_t size = 0;
1176 					const crypto::checksum * checksum = NULL;
1177 					BOOST_FOREACH(const output_location & output, output_locations) {
1178 						if(output.second != 0) {
1179 							continue;
1180 						}
1181 						if(output.first->entry().size != 0) {
1182 							if(size != 0 && size != output.first->entry().size) {
1183 								log_warning << "Mismatched output sizes";
1184 							}
1185 							size = output.first->entry().size;
1186 						}
1187 						if(output.first->entry().checksum.type != crypto::None) {
1188 							if(checksum && *checksum != output.first->entry().checksum) {
1189 								log_warning << "Mismatched output checksums";
1190 							}
1191 							checksum = &output.first->entry().checksum;
1192 						}
1193 						if(named) {
1194 							std::cout << ", ";
1195 						} else {
1196 							std::cout << " - ";
1197 							named = true;
1198 						}
1199 						if(chunk.first.encryption != stream::Plaintext) {
1200 							if(password.empty()) {
1201 								std::cout << '"' << color::dim_yellow << output.first->path() << color::reset << '"';
1202 							} else {
1203 								std::cout << '"' << color::yellow << output.first->path() << color::reset << '"';
1204 							}
1205 						} else {
1206 							std::cout << '"' << color::white << output.first->path() << color::reset << '"';
1207 						}
1208 						print_filter_info(output.first->entry());
1209 					}
1210 
1211 					if(named) {
1212 						if(o.list_sizes) {
1213 							print_size_info(file, size);
1214 						}
1215 						if(o.list_checksums) {
1216 							std::cout << ' ';
1217 							print_checksum_info(file, checksum);
1218 						}
1219 						if(chunk.first.encryption != stream::Plaintext && password.empty()) {
1220 							std::cout << " - encrypted";
1221 						}
1222 						std::cout << '\n';
1223 					}
1224 
1225 				} else {
1226 					BOOST_FOREACH(const output_location & output, output_locations) {
1227 						if(output.second == 0) {
1228 							const processed_file * fileinfo = output.first;
1229 							if(o.list_sizes) {
1230 								boost::uint64_t size = fileinfo->entry().size;
1231 								std::cout << color::dim_cyan << (size != 0 ? size : file.size) << color::reset << ' ';
1232 							}
1233 							if(o.list_checksums) {
1234 								print_checksum_info(file, &fileinfo->entry().checksum);
1235 								std::cout << ' ';
1236 							}
1237 							std::cout << color::white << fileinfo->path() << color::reset << '\n';
1238 						}
1239 					}
1240 				}
1241 
1242 				bool updated = extract_progress.update(0, true);
1243 				if(!updated && (o.extract || o.test)) {
1244 					std::cout.flush();
1245 				}
1246 
1247 			}
1248 
1249 			// Seek to the correct position within the chunk
1250 			if(chunk_source.get() && file.offset < offset) {
1251 				std::ostringstream oss;
1252 				oss << "Bad offset while extracting files: file start (" << file.offset
1253 				    << ") is before end of previous file (" << offset << ")!";
1254 				throw format_error(oss.str());
1255 			}
1256 			offset = file.offset + file.size;
1257 
1258 			if(!chunk_source.get()) {
1259 				continue; // Not extracting/testing this file
1260 			}
1261 
1262 			crypto::checksum checksum;
1263 
1264 			// Open input file
1265 			stream::file_reader::pointer file_source;
1266 			file_source = stream::file_reader::get(*chunk_source, file, &checksum);
1267 
1268 			// Open output files
1269 			boost::ptr_vector<file_output> single_outputs;
1270 			std::vector<file_output *> outputs;
1271 			BOOST_FOREACH(const output_location & output_loc, output_locations) {
1272 				const processed_file * fileinfo = output_loc.first;
1273 				try {
1274 
1275 					if(!o.extract && fileinfo->entry().checksum.type == crypto::None) {
1276 						continue;
1277 					}
1278 
1279 					// Re-use existing file output for multi-part files
1280 					file_output * output = NULL;
1281 					if(fileinfo->is_multipart()) {
1282 						multi_part_outputs::iterator it = multi_outputs.find(fileinfo);
1283 						if(it != multi_outputs.end()) {
1284 							output = it->second;
1285 						}
1286 					}
1287 
1288 					if(!output) {
1289 						output = new file_output(o.output_dir, fileinfo, o.extract);
1290 						if(fileinfo->is_multipart()) {
1291 							multi_outputs.insert(fileinfo, output);
1292 						} else {
1293 							single_outputs.push_back(output);
1294 						}
1295 					}
1296 
1297 					outputs.push_back(output);
1298 
1299 					output->seek(output_loc.second);
1300 
1301 				} catch(boost::bad_pointer &) {
1302 					// should never happen
1303 					std::terminate();
1304 				}
1305 			}
1306 
1307 			// Copy data
1308 			boost::uint64_t output_size = 0;
1309 			while(!file_source->eof()) {
1310 				char buffer[8192 * 10];
1311 				std::streamsize buffer_size = std::streamsize(boost::size(buffer));
1312 				std::streamsize n = file_source->read(buffer, buffer_size).gcount();
1313 				if(n > 0) {
1314 					BOOST_FOREACH(file_output * output, outputs) {
1315 						bool success = output->write(buffer, size_t(n));
1316 						if(!success) {
1317 							throw std::runtime_error("Error writing file \"" + output->path().string() + '"');
1318 						}
1319 					}
1320 					extract_progress.update(boost::uint64_t(n));
1321 					output_size += boost::uint64_t(n);
1322 				}
1323 			}
1324 
1325 			const setup::data_entry & data = info.data_entries[location.second];
1326 
1327 			if(output_size != data.uncompressed_size) {
1328 				log_warning << "Unexpected output file size: " << output_size << " != " << data.uncompressed_size;
1329 			}
1330 
1331 			util::time filetime = data.timestamp;
1332 			if(o.extract && o.preserve_file_times && o.local_timestamps && !(data.options & data.TimeStampInUTC)) {
1333 				filetime = util::to_local_time(filetime);
1334 			}
1335 
1336 			BOOST_FOREACH(file_output * output, outputs) {
1337 
1338 				if(output->file()->is_multipart() && !output->is_complete()) {
1339 					continue;
1340 				}
1341 
1342 				// Verify output checksum if available
1343 				if(output->file()->entry().checksum.type != crypto::None && output->calculate_checksum()) {
1344 					crypto::checksum output_checksum = output->checksum();
1345 					if(output_checksum != output->file()->entry().checksum) {
1346 						log_warning << "Output checksum mismatch for " << output->file()->path() << ":\n"
1347 						            << " ├─ actual:   " << output_checksum << '\n'
1348 						            << " └─ expected: " << output->file()->entry().checksum;
1349 						if(o.test) {
1350 							throw std::runtime_error("Integrity test failed!");
1351 						}
1352 					}
1353 				}
1354 
1355 				// Adjust file timestamps
1356 				if(o.extract && o.preserve_file_times) {
1357 					output->close();
1358 					if(!util::set_file_time(output->path(), filetime, data.timestamp_nsec)) {
1359 						log_warning << "Error setting timestamp on file " << output->path();
1360 					}
1361 				}
1362 
1363 				if(output->file()->is_multipart()) {
1364 					debug("[finalizing multi-part file]");
1365 					multi_outputs.erase(output->file());
1366 				}
1367 
1368 			}
1369 
1370 			// Verify checksums
1371 			if(checksum != file.checksum) {
1372 				log_warning << "Checksum mismatch:\n"
1373 				            << " ├─ actual:   " << checksum << '\n'
1374 				            << " └─ expected: " << file.checksum;
1375 				if(o.test) {
1376 					throw std::runtime_error("Integrity test failed!");
1377 				}
1378 			}
1379 
1380 		}
1381 
1382 		#ifdef DEBUG
1383 		if(offset < chunk.first.size) {
1384 			debug("discarding " << print_bytes(chunk.first.size - offset)
1385 			      << " at end of chunk @ " << print_hex(offset));
1386 		}
1387 		#endif
1388 	}
1389 
1390 	extract_progress.clear();
1391 
1392 	if(!multi_outputs.empty()) {
1393 		log_warning << "Incomplete multi-part files";
1394 	}
1395 
1396 	if(o.warn_unused || o.gog) {
1397 		gog::probe_bin_files(o, info, installer, offsets.data_offset == 0);
1398 	}
1399 
1400 }
1401