├── .gitignore ├── LICENSE ├── README.md └── gogextract.py /.gitignore: -------------------------------------------------------------------------------- 1 | extract 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Gabriel Huber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GOG Extract 2 | 3 | Script for unpacking GOG Linux installers. 4 | 5 | Explanation of how it works is in my [blog post]. 6 | 7 | ## Usage 8 | 9 | `gogextract.py ` 10 | 11 | Output files will be named `unpacker.sh`, `mojosetup.tar.gz` and `data.zip`. 12 | 13 | ## License 14 | 15 | [MIT](LICENSE) 16 | 17 | [blog post]: https://yepoleb.github.io/blog/2016/10/09/how-the-gog-linux-installers-work/ 18 | -------------------------------------------------------------------------------- /gogextract.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import io 3 | import re 4 | import shutil 5 | import os 6 | from os import path 7 | import sys 8 | 9 | FILESIZE_RE = re.compile(r'filesizes="(\d+?)"') 10 | OFFSET_RE = re.compile(r'offset=`head -n (\d+?) "\$0"') 11 | 12 | if len(sys.argv) == 2: 13 | input_path = sys.argv[1] 14 | output_path = "./" 15 | elif len(sys.argv) == 3: 16 | input_path = sys.argv[1] 17 | output_path = sys.argv[2] 18 | else: 19 | print("Usage: {} ".format(sys.argv[0])) 20 | exit(1) 21 | 22 | game_bin = open(input_path, "rb") 23 | os.makedirs(output_path, exist_ok=True) 24 | 25 | # Read the first 10kb so we can determine the script line number 26 | beginning = game_bin.read(10240).decode("utf-8", errors="ignore") 27 | offset_match = OFFSET_RE.search(beginning) 28 | script_lines = int(offset_match.group(1)) 29 | 30 | # Read the number of lines to determine the script size 31 | game_bin.seek(0, io.SEEK_SET) 32 | for l in range(0, script_lines): 33 | game_bin.readline() 34 | script_size = game_bin.tell() 35 | print("Makeself script size:", script_size) 36 | 37 | # Read the script 38 | game_bin.seek(0, io.SEEK_SET) 39 | script_bin = game_bin.read(script_size) 40 | with open(path.join(output_path, "unpacker.sh"), "wb") as script_f: 41 | script_f.write(script_bin) 42 | script = script_bin.decode("utf-8") 43 | 44 | # Filesize is for the MojoSetup archive, not the actual game data 45 | filesize_match = FILESIZE_RE.search(script) 46 | filesize = int(filesize_match.group(1)) 47 | print("MojoSetup archive size:", filesize) 48 | 49 | # Extract the setup archive 50 | game_bin.seek(script_size, io.SEEK_SET) 51 | with open(path.join(output_path, "mojosetup.tar.gz"), "wb") as setup_f: 52 | setup_f.write(game_bin.read(filesize)) 53 | 54 | # Extract the game data archive 55 | dataoffset = script_size + filesize 56 | game_bin.seek(dataoffset, io.SEEK_SET) 57 | with open(path.join(output_path, "data.zip"), "wb") as datafile: 58 | shutil.copyfileobj(game_bin, datafile) 59 | --------------------------------------------------------------------------------