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