Created
August 27, 2026 20:57
-
-
Save UserUnknownFactor/9a6f607340ed3ff91cd5e1cf139b4ce0 to your computer and use it in GitHub Desktop.
Unpacks NSISBI Unity Editor installers with file mask support
This file contains hidden or 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
| import argparse | |
| import io | |
| import logging | |
| import lzma | |
| import mmap | |
| from pathlib import Path | |
| import re | |
| import struct | |
| from typing import BinaryIO, List, Optional, Tuple | |
| from colorama import Fore, Style, init as colorama_init | |
| # --------------------------------------------------------------------------- | |
| # Console | |
| # --------------------------------------------------------------------------- | |
| colorama_init(autoreset=True) | |
| log = logging.getLogger("nsis-extract") | |
| log.setLevel(logging.DEBUG) | |
| _console = logging.StreamHandler() | |
| _console.setFormatter(logging.Formatter("%(message)s")) | |
| log.addHandler(_console) | |
| log.propagate = False | |
| def status(message: str, *args) -> None: | |
| if args: | |
| message = message % args | |
| print(f"{Fore.CYAN}>{Style.RESET_ALL} {message}") | |
| def success(message: str, *args) -> None: | |
| if args: | |
| message = message % args | |
| print(f"{Fore.GREEN}√{Style.RESET_ALL} {message}") | |
| def warning(message: str, *args) -> None: | |
| if args: | |
| message = message % args | |
| print(f"{Fore.YELLOW}!{Style.RESET_ALL} {message}") | |
| def error(message: str, *args) -> None: | |
| if args: | |
| message = message % args | |
| print(f"{Fore.RED}x{Style.RESET_ALL} {message}") | |
| def debug(message: str, *args) -> None: | |
| if not VERBOSE: | |
| return | |
| if args: | |
| message = message % args | |
| print( | |
| f"{Style.DIM}{Fore.WHITE}· {message}" | |
| f"{Style.RESET_ALL}" | |
| ) | |
| VERBOSE = False | |
| # --------------------------------------------------------------------------- | |
| # NSIS constants | |
| # --------------------------------------------------------------------------- | |
| NSIS_HEADER_MAGIC = b"NullsoftInst" | |
| DEFAULT_FILTER_REGEX = r"windowsstandalonesupport/Variations" | |
| EW_NOP = 2 | |
| EW_CREATEDIR = 11 | |
| EW_EXTRACTFILE = 20 | |
| EW_ASSIGNVAR = 25 | |
| NS_3_CODE_SHELL = 2 | |
| NS_3_CODE_VAR = 3 | |
| NS_3_CODE_SKIP = 4 | |
| VAR_R4 = 14 | |
| VAR_INSTDIR = 21 | |
| VAR_OUTDIR = 22 | |
| SPECIAL_EXTRACT_VAR = 10 # $R0 | |
| # --------------------------------------------------------------------------- | |
| # Struct definitions | |
| # --------------------------------------------------------------------------- | |
| NSIS_FIRST_HEADER_FMT = "<II12sII" | |
| NSIS_FIRST_HEADER_SIZE = struct.calcsize(NSIS_FIRST_HEADER_FMT) | |
| NSIS_BLOCK_FMT = "<II" | |
| NSIS_BLOCK_SIZE = struct.calcsize(NSIS_BLOCK_FMT) | |
| class NsisFirstHeader: | |
| def __init__(self, data: bytes): | |
| ( | |
| self.flags, | |
| self.siginfo, | |
| self.magic, | |
| self.length_of_header, | |
| self.length_of_all_following_data, | |
| ) = struct.unpack(NSIS_FIRST_HEADER_FMT, data) | |
| def is_nsisbi(self) -> bool: | |
| return (self.flags & 0x30) != 0 | |
| def has_crc(self) -> bool: | |
| return (self.flags & 0x04) != 0 | |
| class NsisBlock: | |
| def __init__(self, offset: int, num: int): | |
| self.offset = offset | |
| self.num = num | |
| class NsisCommand: | |
| def __init__(self, cmd_id: int, params: List[int]): | |
| self.cmd_id = cmd_id | |
| self.params = params | |
| # --------------------------------------------------------------------------- | |
| # Basic helpers | |
| # --------------------------------------------------------------------------- | |
| def clear_int_flag(i: int) -> Tuple[int, bool]: | |
| has_flag = (i & 0x80000000) != 0 | |
| return i & 0x7FFFFFFF, has_flag | |
| def to_signed(val: int) -> int: | |
| return val - 0x100000000 if val >= 0x80000000 else val | |
| def read_u32(stream: BinaryIO) -> int: | |
| data = stream.read(4) | |
| if len(data) < 4: | |
| raise EOFError("Unexpected end of stream") | |
| return struct.unpack("<I", data)[0] | |
| def read_u16_from_bytes(data: bytes, offset: int) -> int: | |
| return struct.unpack_from("<H", data, offset)[0] | |
| def normalize_path(in_path: str) -> str: | |
| cleaned = in_path.replace("\\", "/") | |
| while cleaned.startswith("/"): | |
| cleaned = cleaned[1:] | |
| return cleaned | |
| def strip_instdir(path: str) -> str: | |
| path = normalize_path(path) | |
| upper = path.upper() | |
| if upper == "$INSTDIR": | |
| return "" | |
| prefix = "$INSTDIR/" | |
| if upper.startswith(prefix): | |
| return path[len(prefix):] | |
| return path | |
| def join_nsis_path(base: str, suffix: str) -> str: | |
| base = normalize_path(base).rstrip("/") | |
| suffix = normalize_path(suffix).lstrip("/") | |
| if not base: | |
| return suffix | |
| if not suffix: | |
| return base | |
| return f"{base}/{suffix}" | |
| def human_size(size: int) -> str: | |
| value = float(size) | |
| for unit in ("B", "KB", "MB", "GB"): | |
| if value < 1024.0 or unit == "GB": | |
| if unit == "B": | |
| return f"{int(value)} {unit}" | |
| return f"{value:.1f} {unit}" | |
| value /= 1024.0 | |
| return f"{size} B" | |
| def is_absolute_nsis_path(path: str) -> bool: | |
| """ | |
| Determines whether an NSIS path string is absolute. | |
| Absolute means: | |
| - starts with a known variable ($INSTDIR, $TEMP, $PLUGINSDIR, $EXEDIR, ...) | |
| - has a drive letter (e.g. C:\\) | |
| Relative means: | |
| - starts with \\ or / but NO drive letter (e.g. \\windowsstandalonesupport) | |
| - has no leading separator at all | |
| """ | |
| if not path: | |
| return False | |
| upper = path.upper() | |
| # Known NSIS variable prefixes | |
| for var in ("$INSTDIR", "$TEMP", "$PLUGINSDIR", "$EXEDIR", "$APPDATA", | |
| "$COMMONAPPDATA", "$DESKTOP", "$DOCUMENTS", "$MUSIC", | |
| "$PICTURES", "$PROFILES", "$PROGRAMFILES", "$PROGRAMFILESX86", | |
| "$SYSTEM", "$SYSDIR", "$TEMP", "$USERPROFILE", "$WINDIR"): | |
| if upper.startswith(var): | |
| return True | |
| # Drive letter: X:\ | |
| if len(path) >= 3 and path[1] == ":" and path[2] in ("\\", "/"): | |
| return True | |
| # Bare drive letter without path: X: | |
| if len(path) >= 2 and path[1] == ":": | |
| return True | |
| return False | |
| # --------------------------------------------------------------------------- | |
| # NSIS strings and variables | |
| # --------------------------------------------------------------------------- | |
| def decode_ns3_unicode_number(n: int) -> int: | |
| return (n & 0x7F) | (((n >> 8) & 0x7F) << 7) | |
| def decode_ns_number(c0: int, c1: int) -> int: | |
| return (c0 & 0x7F) | ((c1 & 0x7F) << 7) | |
| def read_string(block: bytes, is_unicode: bool, pos: int, variables: dict) -> str: | |
| """ | |
| Reads an NSIS string from the string table and evaluates any variable | |
| codes using the provided variables dictionary to simulate runtime state. | |
| """ | |
| if pos < 0: | |
| return "" | |
| if is_unicode: | |
| offset = pos * 2 | |
| if offset >= len(block): | |
| return "" | |
| result: List[str] = [] | |
| for _ in range(0xFFFF): | |
| if offset + 2 > len(block): | |
| break | |
| c = read_u16_from_bytes(block, offset) | |
| offset += 2 | |
| if c == 0: | |
| break | |
| if c > NS_3_CODE_SKIP: | |
| result.append(chr(c)) | |
| continue | |
| if offset + 2 > len(block): | |
| break | |
| n = read_u16_from_bytes(block, offset) | |
| offset += 2 | |
| if n == 0: | |
| break | |
| if c == NS_3_CODE_SKIP: | |
| result.append(chr(n)) | |
| continue | |
| if c == NS_3_CODE_VAR: | |
| index = decode_ns3_unicode_number(n) | |
| result.append(variables.get(index, "")) | |
| continue | |
| if c == NS_3_CODE_SHELL: | |
| index1 = n & 0xFF | |
| index2 = n >> 8 | |
| result.append(f"$SHELL_{index1}_{index2}") | |
| continue | |
| lang_index = decode_ns3_unicode_number(n) | |
| result.append(f"$(LSTR_{lang_index})") | |
| return "".join(result) | |
| # ANSI NSIS 3.x logic | |
| offset = pos | |
| if offset >= len(block): | |
| return "" | |
| result_bytes = bytearray() | |
| for _ in range(0xFFFF): | |
| if offset >= len(block): | |
| break | |
| c = block[offset] | |
| offset += 1 | |
| if c == 0: | |
| break | |
| if c > NS_3_CODE_SKIP: | |
| result_bytes.append(c) | |
| continue | |
| if offset >= len(block): | |
| break | |
| c0 = block[offset] | |
| offset += 1 | |
| if c0 == 0: | |
| break | |
| if c == NS_3_CODE_SKIP: | |
| result_bytes.append(c0) | |
| continue | |
| if offset >= len(block): | |
| break | |
| c1 = block[offset] | |
| offset += 1 | |
| if c1 == 0: | |
| break | |
| if c == NS_3_CODE_VAR: | |
| index = decode_ns_number(c0, c1) | |
| val = variables.get(index, "") | |
| result_bytes.extend(val.encode("latin-1", errors="replace")) | |
| continue | |
| if c == NS_3_CODE_SHELL: | |
| result_bytes.extend(f"$SHELL_{c0}_{c1}".encode("ascii")) | |
| continue | |
| lang_index = decode_ns_number(c0, c1) | |
| result_bytes.extend(f"$(LSTR_{lang_index})".encode("ascii")) | |
| try: | |
| return result_bytes.decode("utf-8") | |
| except UnicodeDecodeError: | |
| return result_bytes.decode("latin-1", errors="replace") | |
| def is_var_string( | |
| strings: bytes, | |
| is_unicode: bool, | |
| pos: int, | |
| wanted_var: int, | |
| ) -> bool: | |
| """Checks the raw string table bytes to see if it is exactly a specific variable.""" | |
| if pos < 0: | |
| return False | |
| if is_unicode: | |
| offset = pos * 2 | |
| if offset + 6 > len(strings): | |
| return False | |
| code = read_u16_from_bytes(strings, offset) | |
| n = read_u16_from_bytes(strings, offset + 2) | |
| end = read_u16_from_bytes(strings, offset + 4) | |
| if code != NS_3_CODE_VAR or n == 0 or end != 0: | |
| return False | |
| return decode_ns3_unicode_number(n) == wanted_var | |
| # ANSI NSIS 3.x logic | |
| offset = pos | |
| if offset + 4 > len(strings): | |
| return False | |
| if strings[offset] != NS_3_CODE_VAR: | |
| return False | |
| c0 = strings[offset + 1] | |
| c1 = strings[offset + 2] | |
| end = strings[offset + 3] | |
| if c0 == 0 or c1 == 0 or end != 0: | |
| return False | |
| return decode_ns_number(c0, c1) == wanted_var | |
| # --------------------------------------------------------------------------- | |
| # LZMA | |
| # --------------------------------------------------------------------------- | |
| def decompress_lzma( | |
| data: bytes, | |
| unpacked_size: Optional[int] = None, | |
| ) -> bytes: | |
| if not data: | |
| return b"" | |
| if len(data) < 5: | |
| raise lzma.LZMAError("NSIS LZMA stream is too short") | |
| props = data[:5] | |
| payload = data[5:] | |
| if unpacked_size is not None and unpacked_size >= 0: | |
| size_bytes = struct.pack("<Q", unpacked_size) | |
| else: | |
| size_bytes = b"\xff" * 8 | |
| alone_data = props + size_bytes + payload | |
| try: | |
| return lzma.decompress(alone_data, format=lzma.FORMAT_ALONE) | |
| except lzma.LZMAError: | |
| pass | |
| props_byte = props[0] | |
| lc = props_byte % 9 | |
| remainder = props_byte // 9 | |
| lp = remainder % 5 | |
| pb = remainder // 5 | |
| if pb > 4: | |
| raise lzma.LZMAError(f"Invalid LZMA properties: 0x{props_byte:02X}") | |
| dict_size = struct.unpack_from("<I", props, 1)[0] | |
| filters = [ | |
| { | |
| "id": lzma.FILTER_LZMA1, | |
| "lc": lc, | |
| "lp": lp, | |
| "pb": pb, | |
| "dict_size": dict_size, | |
| } | |
| ] | |
| return lzma.decompress(payload, format=lzma.FORMAT_RAW, filters=filters) | |
| # --------------------------------------------------------------------------- | |
| # Header discovery | |
| # --------------------------------------------------------------------------- | |
| def find_nsis_header(mmap_data: mmap.mmap) -> int: | |
| idx = mmap_data.find(NSIS_HEADER_MAGIC) | |
| if idx == -1 or idx < 8: | |
| raise ValueError("NSIS header not found") | |
| return idx - 8 | |
| # --------------------------------------------------------------------------- | |
| # Special filename recovery | |
| # --------------------------------------------------------------------------- | |
| def recover_extract_filename_id( | |
| commands: List[NsisCommand], | |
| command_index: int, | |
| strings_data: bytes, | |
| is_unicode: bool, | |
| original_name_id: int, | |
| ) -> int: | |
| # Heuristic: Find if an NSIS macro populated $R4 just before calling `File` on $R0 | |
| if not is_var_string(strings_data, is_unicode, original_name_id, SPECIAL_EXTRACT_VAR): | |
| return original_name_id | |
| back_offset = 28 | |
| if command_index > 1 and commands[command_index - 1].cmd_id == EW_NOP: | |
| back_offset -= 2 | |
| if command_index <= back_offset: | |
| return original_name_id | |
| candidate = commands[command_index - back_offset] | |
| if candidate.cmd_id != EW_ASSIGNVAR: | |
| return original_name_id | |
| p = candidate.params | |
| if len(p) < 4: | |
| return original_name_id | |
| if p[0] == VAR_R4 and p[2] == 0 and p[3] == 0: | |
| return p[1] | |
| return original_name_id | |
| # --------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------- | |
| def main() -> None: | |
| global VERBOSE | |
| parser = argparse.ArgumentParser(description="Extract files from NSISBI installers") | |
| parser.add_argument("file", type=Path, help="Unity Editor installer") | |
| parser.add_argument("out_dir", type=Path, help="Output directory") | |
| parser.add_argument("-r", "--regex", default=DEFAULT_FILTER_REGEX, help="Path filter regex") | |
| parser.add_argument("-a", "--all", action="store_true", help="Extract all files") | |
| parser.add_argument("-v", "--verbose", action="store_true", help="Show parser details") | |
| args = parser.parse_args() | |
| VERBOSE = args.verbose | |
| if not args.file.is_file(): | |
| error("File not found: %s", args.file) | |
| return | |
| file_filter = None | |
| if not args.all: | |
| try: | |
| file_filter = re.compile(args.regex, re.IGNORECASE) | |
| except re.error as e: | |
| error("Invalid regex: %s", e) | |
| return | |
| args.out_dir.mkdir(parents=True, exist_ok=True) | |
| status( | |
| "%s %s→%s %s", | |
| Fore.WHITE + Style.BRIGHT, | |
| args.file.name, | |
| Style.RESET_ALL + Fore.CYAN, | |
| args.out_dir, | |
| ) | |
| with open(args.file, "rb") as f: | |
| mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) | |
| try: | |
| try: | |
| header_offset = find_nsis_header(mm) | |
| except ValueError as e: | |
| error("%s", e) | |
| return | |
| debug("NSIS header @ 0x%X", header_offset) | |
| if header_offset + NSIS_FIRST_HEADER_SIZE > len(mm): | |
| error("Truncated NSIS header") | |
| return | |
| first_header = NsisFirstHeader(mm[header_offset: header_offset + NSIS_FIRST_HEADER_SIZE]) | |
| debug( | |
| "NSISBI=%s CRC=%s header=%d bytes", | |
| first_header.is_nsisbi(), | |
| first_header.has_crc(), | |
| first_header.length_of_header, | |
| ) | |
| pos = header_offset + NSIS_FIRST_HEADER_SIZE | |
| if first_header.is_nsisbi(): | |
| pos += 8 | |
| if pos + 4 > len(mm): | |
| error("Truncated compressed header") | |
| return | |
| compressed_header_len_raw = struct.unpack_from("<I", mm, pos)[0] | |
| pos += 4 | |
| (compressed_header_len, header_is_compressed) = clear_int_flag(compressed_header_len_raw) | |
| if pos + compressed_header_len > len(mm): | |
| error("NSIS header extends past end of file") | |
| return | |
| data_offset = pos + compressed_header_len | |
| debug( | |
| "Header storage=%s, size=%s", | |
| "LZMA" if header_is_compressed else "raw", | |
| human_size(compressed_header_len), | |
| ) | |
| header_data_slice = mm[pos: pos + compressed_header_len] | |
| try: | |
| if header_is_compressed: | |
| decompressed_header = decompress_lzma( | |
| header_data_slice, unpacked_size=first_header.length_of_header | |
| ) | |
| else: | |
| decompressed_header = bytes(header_data_slice) | |
| except lzma.LZMAError as e: | |
| error("Could not decompress NSIS header: %s", e) | |
| return | |
| if len(decompressed_header) < 4 + 8 * NSIS_BLOCK_SIZE: | |
| error("Invalid NSIS header") | |
| return | |
| header_io = io.BytesIO(decompressed_header) | |
| _header_flags = read_u32(header_io) | |
| blocks: List[NsisBlock] = [] | |
| for _ in range(8): | |
| block_bytes = header_io.read(NSIS_BLOCK_SIZE) | |
| if len(block_bytes) != NSIS_BLOCK_SIZE: | |
| error("Truncated NSIS block table") | |
| return | |
| offset_b, num_b = struct.unpack(NSIS_BLOCK_FMT, block_bytes) | |
| blocks.append(NsisBlock(offset_b, num_b)) | |
| entries_block = blocks[2] | |
| strings_block = blocks[3] | |
| lang_block = blocks[4] | |
| if not (0 <= entries_block.offset <= len(decompressed_header) and 0 <= strings_block.offset <= len(decompressed_header)): | |
| error("Invalid NSIS block offsets") | |
| return | |
| if strings_block.offset <= lang_block.offset <= len(decompressed_header): | |
| strings_data = decompressed_header[strings_block.offset: lang_block.offset] | |
| else: | |
| strings_data = decompressed_header[strings_block.offset:] | |
| if len(strings_data) < 2: | |
| error("Invalid NSIS string table") | |
| return | |
| is_unicode = read_u16_from_bytes(strings_data, 0) == 0 | |
| debug( | |
| "Strings=%s, commands=%d", | |
| "Unicode" if is_unicode else "ANSI", | |
| entries_block.num, | |
| ) | |
| cmd_io = io.BytesIO(decompressed_header[entries_block.offset:]) | |
| num_params = 6 + (2 if first_header.is_nsisbi() else 0) | |
| commands: List[NsisCommand] = [] | |
| for command_index in range(entries_block.num): | |
| try: | |
| cmd_id = read_u32(cmd_io) | |
| params = [read_u32(cmd_io) for _ in range(num_params)] | |
| except EOFError: | |
| warning("Command table ended early at %d/%d", command_index, entries_block.num) | |
| break | |
| commands.append(NsisCommand(cmd_id, params)) | |
| # Initialize variables state mimicking NSIS defaults | |
| variables = { | |
| VAR_INSTDIR: "$INSTDIR", | |
| 25: "$TEMP", # VAR_TEMP | |
| 26: "$PLUGINSDIR", # VAR_PLUGINSDIR | |
| 23: "$EXEDIR", # VAR_EXEDIR | |
| } | |
| # Track the last ABSOLUTE outdir. Relative SetOutPath calls | |
| # (leading "\" with no drive letter) resolve against this base, | |
| base_outdir = "$INSTDIR" | |
| extracted_count = 0 | |
| skipped_count = 0 | |
| indirect_count = 0 | |
| extracted_bytes = 0 | |
| for command_index, command in enumerate(commands): | |
| cmd_id = command.cmd_id | |
| params = command.params | |
| # ------------------------------------------------------------ | |
| # EW_ASSIGNVAR (StrCpy etc) | |
| # ------------------------------------------------------------ | |
| if cmd_id == EW_ASSIGNVAR: | |
| dst_var = params[0] | |
| src_text = read_string(strings_data, is_unicode, params[1], variables) | |
| # Implement substring copying (MaxLen and StartOffset) | |
| max_len = to_signed(params[2]) | |
| start_offset = to_signed(params[3]) | |
| if start_offset != 0 or max_len != 0: | |
| if start_offset > 0: | |
| src_text = src_text[start_offset:] | |
| elif start_offset < 0: | |
| src_text = src_text[start_offset:] | |
| if max_len > 0: | |
| src_text = src_text[:max_len] | |
| elif max_len < 0: | |
| src_text = src_text[:max_len] | |
| variables[dst_var] = src_text | |
| continue | |
| # ------------------------------------------------------------ | |
| # EW_CREATEDIR (SetOutPath) | |
| # ------------------------------------------------------------ | |
| if cmd_id == EW_CREATEDIR: | |
| if params[1] == 0: | |
| continue | |
| out_path = read_string(strings_data, is_unicode, params[0], variables) | |
| if is_absolute_nsis_path(out_path): | |
| base_outdir = out_path | |
| variables[VAR_OUTDIR] = out_path | |
| else: | |
| # Relative path resolves against the last absolute base | |
| variables[VAR_OUTDIR] = join_nsis_path(base_outdir, out_path) | |
| debug("OutPath → %s", variables[VAR_OUTDIR]) | |
| continue | |
| # ------------------------------------------------------------ | |
| # EW_EXTRACTFILE | |
| # ------------------------------------------------------------ | |
| if cmd_id != EW_EXTRACTFILE: | |
| continue | |
| original_name_str_id = params[1] | |
| name_str_id = recover_extract_filename_id( | |
| commands, | |
| command_index, | |
| strings_data, | |
| is_unicode, | |
| original_name_str_id, | |
| ) | |
| if name_str_id != original_name_str_id: | |
| indirect_count += 1 | |
| # Evaluates string using current memory state | |
| name_str = read_string(strings_data, is_unicode, name_str_id, variables) | |
| if not name_str: | |
| warning("Skipped unnamed file at command %d", command_index) | |
| continue | |
| # Check if the filename itself is absolute | |
| is_absolute = is_absolute_nsis_path(name_str) | |
| if is_absolute: | |
| full_path = name_str | |
| else: | |
| current_outdir = variables.get(VAR_OUTDIR, "") | |
| full_path = join_nsis_path(current_outdir, name_str) | |
| normalized_path = normalize_path(strip_instdir(full_path)) | |
| # Prevent arbitrary writes across directories/drives | |
| if len(normalized_path) > 1 and normalized_path[1] == ':': | |
| normalized_path = normalized_path[2:] | |
| normalized_path = normalized_path.lstrip("/") | |
| if normalized_path.startswith("../") or normalized_path.startswith("..\\"): | |
| warning("Attempted path traversal, skipping: %s", normalized_path) | |
| continue | |
| if file_filter and not file_filter.search(normalized_path): | |
| skipped_count += 1 | |
| continue | |
| file_size_offset = data_offset + params[2] | |
| if file_size_offset + 4 > len(mm): | |
| error("Invalid data offset for %s", normalized_path) | |
| continue | |
| raw_file_size = struct.unpack_from("<I", mm, file_size_offset)[0] | |
| (file_size, is_compressed) = clear_int_flag(raw_file_size) | |
| file_data_offset = file_size_offset + 4 | |
| if file_data_offset + file_size > len(mm): | |
| error("Payload extends past EOF: %s", normalized_path) | |
| continue | |
| out_file_abs = args.out_dir / Path(normalized_path) | |
| out_file_abs.parent.mkdir(parents=True, exist_ok=True) | |
| if file_size == 0: | |
| out_file_abs.write_bytes(b"") | |
| success( | |
| "%s %s%s%s", | |
| normalized_path, | |
| Style.DIM, | |
| "0 B", | |
| Style.RESET_ALL, | |
| ) | |
| extracted_count += 1 | |
| continue | |
| file_raw_bytes = mm[file_data_offset: file_data_offset + file_size] | |
| if not is_compressed: | |
| output_data = bytes(file_raw_bytes) | |
| else: | |
| try: | |
| output_data = decompress_lzma(file_raw_bytes, unpacked_size=None) | |
| except lzma.LZMAError as e: | |
| error("%s — %s", normalized_path, e) | |
| continue | |
| out_file_abs.write_bytes(output_data) | |
| extracted_count += 1 | |
| extracted_bytes += len(output_data) | |
| print( | |
| f"{Fore.GREEN} +{Style.RESET_ALL} " | |
| f"{normalized_path} " | |
| f"{Style.DIM}{human_size(len(output_data))}" | |
| f"{Style.RESET_ALL}" | |
| ) | |
| print() | |
| success( | |
| "%d files %s•%s %s", | |
| extracted_count, | |
| Style.DIM, | |
| Style.RESET_ALL, | |
| human_size(extracted_bytes), | |
| ) | |
| if VERBOSE: | |
| debug("Filtered=%d, indirect names=%d", skipped_count, indirect_count) | |
| finally: | |
| mm.close() | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment