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

/*
* 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