You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
540 lines
12 KiB
540 lines
12 KiB
/* |
|
* Copyright (C) 2014-2018 Daniel Scharrer |
|
* |
|
* This software is provided 'as-is', without any express or implied |
|
* warranty. In no event will the author(s) be held liable for any damages |
|
* arising from the use of this software. |
|
* |
|
* Permission is granted to anyone to use this software for any purpose, |
|
* including commercial applications, and to alter it and redistribute it |
|
* freely, subject to the following restrictions: |
|
* |
|
* 1. The origin of this software must not be misrepresented; you must not |
|
* claim that you wrote the original software. If you use this software |
|
* in a product, an acknowledgment in the product documentation would be |
|
* appreciated but is not required. |
|
* 2. Altered source versions must be plainly marked as such, and must not be |
|
* misrepresented as being the original software. |
|
* 3. This notice may not be removed or altered from any source distribution. |
|
*/ |
|
|
|
#include "cli/gog.hpp" |
|
|
|
#include <stddef.h> |
|
#include <cstring> |
|
#include <sstream> |
|
#include <iomanip> |
|
#include <iostream> |
|
#include <signal.h> |
|
|
|
#include <boost/foreach.hpp> |
|
#include <boost/algorithm/string/predicate.hpp> |
|
#include <boost/filesystem/operations.hpp> |
|
|
|
#include "cli/extract.hpp" |
|
|
|
#include "crypto/md5.hpp" |
|
|
|
#include "loader/offsets.hpp" |
|
|
|
#include "setup/data.hpp" |
|
#include "setup/info.hpp" |
|
#include "setup/registry.hpp" |
|
|
|
#include "stream/slice.hpp" |
|
|
|
#include "util/console.hpp" |
|
#include "util/boostfs_compat.hpp" |
|
#include "util/fstream.hpp" |
|
#include "util/log.hpp" |
|
#include "util/process.hpp" |
|
|
|
namespace fs = boost::filesystem; |
|
|
|
namespace gog { |
|
|
|
std::string get_game_id(const setup::info & info) { |
|
|
|
std::string id; |
|
|
|
const char * prefix = "SOFTWARE\\GOG.com\\Games\\"; |
|
size_t prefix_length = std::strlen(prefix); |
|
|
|
BOOST_FOREACH(const setup::registry_entry & entry, info.registry_entries) { |
|
|
|
if(!boost::istarts_with(entry.key, prefix)) { |
|
continue; |
|
} |
|
|
|
if(entry.key.find('\\', prefix_length) != std::string::npos) { |
|
continue; |
|
} |
|
|
|
if(boost::iequals(entry.name, "gameID")) { |
|
return entry.value; |
|
} |
|
|
|
if(id.empty()) { |
|
id = entry.key.substr(prefix_length); |
|
} |
|
|
|
} |
|
|
|
return id; |
|
} |
|
|
|
namespace { |
|
|
|
std::string get_verb(const extract_options & o) { |
|
const char * verb = "inspect"; |
|
if(o.extract) { |
|
verb = "extract"; |
|
} else if(o.test) { |
|
verb = "test"; |
|
} else if(o.list) { |
|
verb = "list the contents of"; |
|
} |
|
return verb; |
|
} |
|
|
|
volatile sig_atomic_t quit_requested = 0; |
|
void quit_handler(int /* ignored */) { |
|
quit_requested = 1; |
|
} |
|
|
|
bool process_file_unrar(const fs::path & file, const extract_options & o, |
|
const std::string & password) { |
|
|
|
std::vector<const char *> args; |
|
args.push_back("unrar"); |
|
|
|
if(o.extract) { |
|
args.push_back("x"); |
|
} else if(o.test) { |
|
args.push_back("t"); |
|
} else if(o.silent) { |
|
args.push_back("lb"); |
|
} else { |
|
args.push_back("l"); |
|
} |
|
|
|
args.push_back("-p-"); |
|
std::string pwarg; |
|
if(!password.empty()) { |
|
pwarg = "-p" + password; |
|
args.push_back(pwarg.c_str()); |
|
} |
|
|
|
args.push_back("-idc"); // Disable copyright header |
|
|
|
if(!progress::is_enabled()) { |
|
args.push_back("-idp"); // Disable progress display |
|
} |
|
|
|
if(o.filenames.is_lowercase()) { |
|
args.push_back("-cl"); // Connvert filenames to lowercase |
|
} |
|
|
|
if(!o.list) { |
|
args.push_back("-idq"); // Disable file list |
|
} |
|
|
|
args.push_back("-o+"); // Overwrite existing files |
|
|
|
if(o.preserve_file_times) { |
|
args.push_back("-tsmca"); // Restore file times |
|
} else { |
|
args.push_back("-tsm0c0a0"); // Don't restore file times |
|
} |
|
|
|
args.push_back("-y"); // Enable batch mode |
|
|
|
args.push_back("--"); |
|
|
|
std::string filename = file.string(); |
|
args.push_back(filename.c_str()); |
|
|
|
std::string dir = o.output_dir.string(); |
|
if(!dir.empty()) { |
|
if(dir[dir.length() - 1] != '/' && dir[dir.length() - 1] != '\\') { |
|
#if defined(_WIN32) |
|
dir += '\\'; |
|
#else |
|
dir += '/'; |
|
#endif |
|
} |
|
args.push_back(dir.c_str()); |
|
} |
|
|
|
args.push_back(NULL); |
|
|
|
int ret = util::run(&args.front()); |
|
if(ret < 0 && !quit_requested) { |
|
args[0] = "rar"; |
|
ret = util::run(&args.front()); |
|
if(ret < 0 && !quit_requested) { |
|
return false; |
|
} |
|
} |
|
|
|
if(ret > 0) { |
|
throw std::runtime_error("Could not " + get_verb(o) + " \"" + file.string() |
|
+ "\": unrar failed"); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
bool process_file_unar(const fs::path & file, const extract_options & o, |
|
const std::string & password) { |
|
|
|
std::string dir = o.output_dir.string(); |
|
|
|
std::vector<const char *> args; |
|
if(o.extract) { |
|
args.push_back("unar"); |
|
|
|
args.push_back("-f"); // Overwrite existing files |
|
|
|
args.push_back("-D"); // Don't create directory |
|
|
|
if(!dir.empty()) { |
|
args.push_back("-o"); |
|
args.push_back(dir.c_str()); |
|
} |
|
|
|
if(!o.list) { |
|
args.push_back("-q"); // Disable file list |
|
} |
|
|
|
} else { |
|
args.push_back("lsar"); |
|
|
|
if(o.test) { |
|
args.push_back("-t"); |
|
} |
|
} |
|
|
|
if(!password.empty()) { |
|
args.push_back("-p"); |
|
args.push_back(password.c_str()); |
|
} |
|
|
|
args.push_back("--"); |
|
|
|
std::string filename = file.string(); |
|
args.push_back(filename.c_str()); |
|
|
|
args.push_back(NULL); |
|
|
|
int ret = util::run(&args.front()); |
|
if(ret < 0 && !quit_requested) { |
|
return false; |
|
} |
|
|
|
if(ret > 0) { |
|
throw std::runtime_error("Could not " + get_verb(o) + " \"" + file.string() |
|
+ "\": unar failed"); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
bool process_rar_file(const fs::path & file, const extract_options & o, const std::string & password) { |
|
return process_file_unrar(file, o, password) || process_file_unar(file, o, password); |
|
} |
|
|
|
char hex_char(int c) { |
|
if(c < 10) { |
|
return char('0' + c); |
|
} else { |
|
return char('a' + (c - 10)); |
|
} |
|
} |
|
|
|
class temporary_directory { |
|
|
|
fs::path path; |
|
|
|
public: |
|
|
|
explicit temporary_directory(const fs::path & base) { |
|
try { |
|
size_t tmpnum = 0; |
|
std::ostringstream oss; |
|
do { |
|
oss.str(std::string()); |
|
oss << "innoextract-tmp-" << tmpnum++; |
|
path = base / oss.str(); |
|
} while(fs::exists(path)); |
|
fs::create_directories(path); |
|
} catch(...) { |
|
path = fs::path(); |
|
throw std::runtime_error("Could not create temporary directory!"); |
|
} |
|
} |
|
|
|
~temporary_directory() { |
|
if(!path.empty()) { |
|
try { |
|
fs::remove_all(path); |
|
} catch(...) { |
|
log_error << "Could not remove temporary directory " << path << '!'; |
|
} |
|
} |
|
} |
|
|
|
const fs::path & get() { return path; } |
|
|
|
}; |
|
|
|
void process_rar_files(const std::vector<fs::path> & files, |
|
const extract_options & o, const setup::info & info) { |
|
|
|
if((!o.list && !o.test && !o.extract) || files.empty()) { |
|
return; |
|
} |
|
|
|
// Calculate password from the GOG.com game ID |
|
std::string password = get_game_id(info); |
|
if(!password.empty()) { |
|
crypto::md5 md5; |
|
md5.init(); |
|
md5.update(password.c_str(), password.length()); |
|
char hash[16]; |
|
md5.finalize(hash); |
|
password.resize(size_t(boost::size(hash) * 2)); |
|
for(size_t i = 0; i < size_t(boost::size(hash)); i++) { |
|
password[2 * i + 0] = hex_char(((unsigned char)hash[i]) / 16); |
|
password[2 * i + 1] = hex_char(((unsigned char)hash[i]) % 16); |
|
} |
|
} |
|
|
|
if((!o.extract && !o.test && o.list) || files.size() == 1) { |
|
|
|
// When listing contents or for single-file archives, pass the bin file to unrar |
|
|
|
bool ok = true; |
|
BOOST_FOREACH(const fs::path & file, files) { |
|
if(!process_rar_file(file, o, password)) { |
|
ok = false; |
|
} |
|
} |
|
|
|
if(ok) { |
|
return; |
|
} |
|
|
|
} else { |
|
|
|
/* |
|
* When extracting multi-part archives we need to create symlinks with special |
|
* names so that unrar will find all the parts of the archive. |
|
*/ |
|
|
|
typedef void(*signal_handler /* … */)(int /* … */); |
|
#ifdef SIGINT |
|
signal_handler old_sigint_handler = signal(SIGINT, quit_handler); |
|
#endif |
|
#ifdef SIGTERM |
|
signal_handler old_sigterm_handler = signal(SIGTERM, quit_handler); |
|
#endif |
|
#ifdef SIGHUP |
|
signal_handler old_sighup_handler = signal(SIGHUP, quit_handler); |
|
#endif |
|
|
|
temporary_directory tmpdir(o.output_dir); |
|
|
|
fs::path first_file; |
|
try { |
|
|
|
fs::path here = fs::current_path(); |
|
|
|
std::string basename = util::as_string(files.front().stem()); |
|
if(boost::ends_with(basename, "-1")) { |
|
basename.resize(basename.length() - 2); |
|
} |
|
|
|
size_t i = 0; |
|
std::ostringstream oss; |
|
BOOST_FOREACH(const fs::path & file, files) { |
|
|
|
oss.str(std::string()); |
|
oss << basename << ".r" << std::setfill('0') << std::setw(2) << i; |
|
fs::path symlink = tmpdir.get() / oss.str(); |
|
|
|
if(file.root_path().empty()) { |
|
fs::create_symlink(here / file, symlink); |
|
} else { |
|
fs::create_symlink(file, symlink); |
|
} |
|
|
|
if(i == 0) { |
|
first_file = symlink; |
|
} |
|
|
|
i++; |
|
} |
|
|
|
} catch(...) { |
|
throw std::runtime_error("Could not " + get_verb(o) |
|
+ " \"" + files.front().string() |
|
+ "\": unable to create .r?? symlinks"); |
|
} |
|
|
|
if(process_rar_file(first_file, o, password)) { |
|
return; |
|
} |
|
|
|
#ifdef SIGHUP |
|
signal(SIGHUP, old_sighup_handler); |
|
#endif |
|
#ifdef SIGTERM |
|
signal(SIGTERM, old_sigterm_handler); |
|
#endif |
|
#ifdef SIGINT |
|
signal(SIGINT, old_sigint_handler); |
|
#endif |
|
if(quit_requested) { |
|
throw std::runtime_error("Aborted!"); |
|
} |
|
|
|
} |
|
|
|
throw std::runtime_error("Could not " + get_verb(o) + " \"" + files.front().string() |
|
+ "\": install `unrar` or `unar`"); |
|
} |
|
|
|
void process_bin_files(const std::vector<fs::path> & files, const extract_options & o, |
|
const setup::info & info) { |
|
|
|
util::ifstream ifs(files.front(), std::ios_base::in | std::ios_base::binary); |
|
if(!ifs.is_open()) { |
|
throw std::runtime_error("Could not open file \"" + files.front().string() + '"'); |
|
} |
|
|
|
char magic[4]; |
|
if(!ifs.read(magic, std::streamsize(boost::size(magic))).fail()) { |
|
|
|
if(std::memcmp(magic, "Rar!", 4) == 0) { |
|
ifs.close(); |
|
process_rar_files(files, o, info); |
|
return; |
|
} |
|
|
|
if(std::memcmp(magic, "MZ", 2) == 0) { |
|
loader::offsets offsets; |
|
offsets.load(ifs); |
|
if(offsets.header_offset != 0) { |
|
ifs.close(); |
|
extract_options new_options = o; |
|
new_options.gog = false; |
|
new_options.warn_unused = false; |
|
std::cout << '\n'; |
|
process_file(files.front(), new_options); |
|
return; |
|
} |
|
} |
|
|
|
} |
|
|
|
throw std::runtime_error("Could not " + get_verb(o) + " \"" + files.front().string() |
|
+ "\": unknown filetype"); |
|
} |
|
|
|
size_t probe_bin_file_series(const extract_options & o, const setup::info & info, const fs::path & dir, |
|
const std::string & basename, size_t format = 0, size_t start = 0) { |
|
|
|
size_t count = 0; |
|
|
|
std::vector<fs::path> files; |
|
|
|
for(size_t i = start;; i++) { |
|
|
|
fs::path file; |
|
if(format == 0) { |
|
file = dir / basename; |
|
} else { |
|
file = dir / stream::slice_reader::slice_filename(basename, i, format); |
|
} |
|
|
|
try { |
|
if(!fs::is_regular_file(file)) { |
|
break; |
|
} |
|
} catch(...) { |
|
break; |
|
} |
|
|
|
if(o.gog) { |
|
files.push_back(file); |
|
} else { |
|
log_warning << file.filename() << " is not part of the installer!"; |
|
count++; |
|
} |
|
|
|
if(format == 0) { |
|
break; |
|
} |
|
|
|
} |
|
|
|
if(!files.empty()) { |
|
process_bin_files(files, o, info); |
|
} |
|
|
|
return count; |
|
} |
|
|
|
} // anonymous namespace |
|
|
|
void probe_bin_files(const extract_options & o, const setup::info & info, |
|
const fs::path & setup_file, bool external) { |
|
|
|
boost::filesystem::path dir = setup_file.parent_path(); |
|
std::string basename = util::as_string(setup_file.stem()); |
|
|
|
size_t bin_count = 0; |
|
bin_count += probe_bin_file_series(o, info, dir, basename + ".bin"); |
|
bin_count += probe_bin_file_series(o, info, dir, basename + "-0" + ".bin"); |
|
|
|
|
|
size_t max_slice = 0; |
|
if(external) { |
|
BOOST_FOREACH(const setup::data_entry & location, info.data_entries) { |
|
max_slice = std::max(max_slice, location.chunk.first_slice); |
|
max_slice = std::max(max_slice, location.chunk.last_slice); |
|
} |
|
} |
|
|
|
size_t slice = 0; |
|
size_t format = 1; |
|
if(external && info.header.slices_per_disk == 1) { |
|
slice = max_slice + 1; |
|
} |
|
bin_count += probe_bin_file_series(o, info, dir, basename, format, slice); |
|
|
|
slice = 0; |
|
format = 2; |
|
if(external && info.header.slices_per_disk != 1) { |
|
slice = max_slice + 1; |
|
format = info.header.slices_per_disk; |
|
} |
|
bin_count += probe_bin_file_series(o, info, dir, basename, format, slice); |
|
|
|
if(bin_count) { |
|
const char * verb = "inspecting"; |
|
if(o.extract) { |
|
verb = "extracting"; |
|
} else if(o.test) { |
|
verb = "testing"; |
|
} else if(o.list) { |
|
verb = "listing the contents of"; |
|
} |
|
std::cerr << color::yellow << "Use the --gog option to try " << verb << " " |
|
<< (bin_count > 1 ? "these files" : "this file") << ".\n" << color::reset; |
|
} |
|
|
|
} |
|
|
|
} // namespace gog
|
|
|