Skip to content

Instantly share code, notes, and snippets.

@ssokolow
Last active January 8, 2022 17:45
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ssokolow/7368450647df37c40830 to your computer and use it in GitHub Desktop.
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
#!/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