Last active
January 8, 2022 17:45
-
-
Save ssokolow/7368450647df37c40830 to your computer and use it in GitHub Desktop.
innoextract+unrar wrapper for unpacking GOG.com installers using password-protected RAR files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python2 | |
# -*- coding: utf-8 -*- | |
"""Unpack wrapper for GOG.com installers using password-protected RAR files""" | |
from __future__ import (absolute_import, division, print_function, | |
with_statement, unicode_literals) | |
__author__ = "Stephan Sokolow (deitarion/SSokolow)" | |
__appname__ = "gog_unrar.py" | |
__version__ = "0.1rc1" | |
__license__ = "MIT" | |
import errno, hashlib, os, re, shutil, subprocess, tempfile | |
def call_cleanly(args, target=None, *call_args, **call_kwargs): | |
"""@note: Make sure C{args} contains no relative paths.""" | |
if not which(args[0]): | |
raise OSError(errno.ENOENT, "%s not found in PATH" % args[0]) | |
targetdir = target or tempfile.mkdtemp(prefix='gog_unrar-') | |
try: | |
subprocess.check_call(args, cwd=targetdir, *call_args, **call_kwargs) | |
except (OSError, subprocess.CalledProcessError): | |
if target is None: | |
shutil.rmtree(targetdir) | |
raise | |
return targetdir | |
def check_file_type(path): | |
"""Take a path, examine the file header, and return 'exe', 'rar', or | |
raise an exception if the file is unrecognized. | |
""" | |
with open(path, 'rb') as fobj: | |
header = fobj.read(64) | |
if header.startswith(b'\x4d\x5a'): # InnoSetup EXE | |
return 'exe' | |
elif header.startswith(b'\x52\x61\x72\x21\x1a\x07\x00'): # RAR file | |
return 'rar' | |
# TODO: Find the proper exception type for this | |
raise Exception("Unrecognized file type: %s" % path) | |
def get_id(path): | |
"""Given the path to a GOG installer's EXE or first BIN file, return a | |
tuple of the path to the first BIN file and the game's GOG ID. | |
""" | |
fbase = os.path.splitext(path)[0] | |
ftype = check_file_type(path) | |
if ftype == 'exe': | |
bin_path = fbase + '-1.bin' # TODO: Make this more robust | |
exe_path, other_path = path, bin_path | |
elif ftype == 'rar': | |
exe_path = fbase[:-2] + '.exe' # TODO: Make this more robust | |
bin_path, other_path = path, exe_path | |
# TODO: If pointed at *-2.bin, this may not behave as expected. | |
ftype_other = check_file_type(other_path) | |
if ftype_other == ftype: | |
# TODO: Find the proper exception type for this | |
raise Exception("Could not predict exe/rar path from rar/exe path: " | |
"%s -> %s", (path, other_path)) | |
exe_path = os.path.abspath(exe_path) | |
try: | |
print("Extracting game ID from InnoSetup EXE...") | |
tmpdir = call_cleanly(['innoextract', '-s', exe_path]) | |
gameids = [os.path.splitext(x)[0] | |
for x in os.listdir(os.path.join(tmpdir, 'tmp')) | |
if re.match(r'\d+\.ini', x)] | |
finally: | |
shutil.rmtree(tmpdir) | |
if gameids: | |
assert gameids[0] | |
return bin_path, gameids[0] | |
else: | |
raise NotImplementedError("TODO: Fall back to urllib2-based lookup") | |
def which(name, flags=os.X_OK): | |
"""Search PATH for executable files with the given name. | |
On newer versions of MS-Windows, the PATHEXT environment variable will be | |
set to the list of file extensions for files considered executable. This | |
will normally include things like ".EXE". This fuction will also find files | |
with the given name ending with any of these extensions. | |
On MS-Windows the only flag that has any meaning is os.F_OK. Any other | |
flags will be ignored. | |
Source: Twisted-2.4.0/TwistedCore-2.4.0/twisted/python/procutils.py | |
Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
Copyright (c) 2007, 2014 Stephan Sokolow. | |
Released under the MIT license. | |
See LICENSE from the Twisted 2.4.0 tarball for details. | |
https://twistedmatrix.com/Releases/Twisted/2.4/Twisted-2.4.0.tar.bz2 | |
@type name: C{str} | |
@param name: The name for which to search. | |
@type flags: C{int} | |
@param flags: Arguments to L{os.access}. | |
@rtype: C{list} | |
@param: A list of the full paths to files found, in the | |
order in which they were found. | |
""" | |
result = [] | |
exts = [x for x in os.environ.get('PATHEXT', '').split(os.pathsep) if x] | |
for bindir in os.environ['PATH'].split(os.pathsep): | |
binpath = os.path.join(os.path.expanduser(bindir), name) | |
if os.access(binpath, flags): | |
result.append(binpath) | |
for ext in exts: | |
pext = binpath + ext | |
if os.access(pext, flags): | |
result.append(pext) | |
return result | |
def main(): | |
"""The main entry point, compatible with setuptools entry points.""" | |
# pylint: disable=bad-continuation | |
from optparse import OptionParser | |
parser = OptionParser(version="%%prog v%s" % __version__, | |
usage="%prog [options] <path to EXE or BIN file> ...", | |
description=__doc__.replace('\r\n', '\n').split('\n--snip--\n')[0]) | |
parser.add_option('-t', '--target', action="store", dest="target", | |
default='.', help="Set the target directory for extraction") | |
opts, args = parser.parse_args() | |
if not args: | |
parser.print_usage() | |
for path in args: | |
binp, game_id = get_id(path) | |
binp, passwd = os.path.abspath(binp), hashlib.md5(game_id).hexdigest() | |
call_cleanly(['unrar', 'x', '-p%s' % passwd, binp], target=opts.target) | |
if __name__ == '__main__': | |
main() | |
# vim: set sw=4 sts=4 expandtab : |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment