Module refinery.units.formats.archive.xtinno

Expand source code Browse git
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations

import re

from refinery.units.formats.archive import ArchiveUnit

from refinery.lib.mime import FileMagicInfo as magic
from refinery.lib.json import BytesAsArrayEncoder
from refinery.lib.inno.archive import InnoArchive, InvalidPassword, SetupFileFlags


class xtinno(ArchiveUnit):
    """
    Extract files from InnoSetup archives. The unit extracts the following synthetic metadata files
    under the "meta" directory:

    - `setup.bin` contains the raw bytes for the setup metadata
    - `setup.template` contains the raw and unprocessed metadata in JSON format
    - `setup.json` contains the setup metadata with all format fields expanded

    Similarly, there are `files.bin`, `files.template`, and `files.json` that contain the metadata
    of the archived files. The files that are extracted under the "embedded" directory are usually
    parts of the InnoSetup installer and not user data. All archived files are extracted within the
    directory named "data".
    """
    def unpack(self, data: bytearray):
        def post_process_json(doc):
            if isinstance(doc, dict):
                return {key: post_process_json(val) for key, val in doc.items()}
            if isinstance(doc, list):
                return [post_process_json(entry) for entry in doc]
            if not isinstance(doc, str):
                return doc
            try:
                return inno.emulator.reset().expand_constant(doc)
            except Exception:
                return doc

        inno = InnoArchive(data, self)

        password: bytes = self.args.pwd
        password = password.decode(self.codec) if password else None

        if any(file.encrypted for file in inno.files) and password is None:
            self.log_info('some files are password-protected and no password was given')

        with BytesAsArrayEncoder as encoder:
            yield self._pack('meta/setup.bin', None, inno.streams.TSetup.data)
            doc = inno.setup_info.json()
            yield self._pack('meta/setup.template', None, encoder.dumps(doc).encode(self.codec))
            doc = post_process_json(doc)
            yield self._pack('meta/setup.json', None, encoder.dumps(doc).encode(self.codec))

        with BytesAsArrayEncoder as encoder:
            yield self._pack('meta/files.bin', None, inno.streams.TData.data)
            doc = inno.setup_data.json()
            yield self._pack('meta/files.template', None, encoder.dumps(doc).encode(self.codec))
            doc = post_process_json(doc)
            yield self._pack('meta/files.json', None, encoder.dumps(doc).encode(self.codec))

        def _uninstaller(i=inno):
            return i.read_stream(i.streams.Uninstaller)
        yield self._pack('embedded/uninstaller.exe', None, _uninstaller)

        if license := inno.setup_info.Header.get_license():
            yield self._pack('embedded/license.rtf', None, license.encode(self.codec))

        if script := inno.setup_info.Header.get_script():
            yield self._pack('embedded/script.bin', None, script)
            yield self._pack('embedded/script.ps', None,
                lambda i=inno: i.ifps.disassembly().encode(self.codec))

        if dll := inno.setup_info.get_decompress_dll():
            yield self._pack(F'embedded/decompress.{magic(dll).extension}', None, dll)

        if dll := inno.setup_info.get_decryption_dll():
            yield self._pack(F'embedded/decryption.{magic(dll).extension}', None, dll)

        for size, images in (
            ('small', inno.setup_info.get_wizard_images_small()),
            ('large', inno.setup_info.get_wizard_images_large()),
        ):
            _formatting = len(str(len(images) + 1))
            for k, img in enumerate(images, 1):
                yield self._pack(F'embedded/images/{size}{k:0{_formatting}d}.{magic(img).extension}', None, img)

        for file in inno.files:
            if file.dupe:
                continue

            def _read(inno=inno, file=file, pwd=password):
                if pwd is None:
                    inno.guess_password(10)
                if self.leniency > 0:
                    return inno.read_file(file, pwd)
                try:
                    return inno.read_file_and_check(file, pwd)
                except InvalidPassword:
                    raise
                except Exception as E:
                    raise ValueError(F'{E!s} [ignore this check with -L]') from E

            yield self._pack(file.path, file.date, _read,
                tags=[t.name for t in SetupFileFlags if t & file.tags])

    @classmethod
    def handles(self, data):
        if data[:2] != B'MZ':
            return False
        if re.search(re.escape(InnoArchive.ChunkPrefix), data) is None:
            return False
        return bool(
            re.search(BR'Inno Setup Setup Data \(\d+\.\d+\.', data))

Classes

class xtinno (*paths, list=False, join_path=False, drop_path=False, fuzzy=0, exact=False, regex=False, path=b'path', date=b'date', pwd=b'')

Extract files from InnoSetup archives. The unit extracts the following synthetic metadata files under the "meta" directory:

  • setup.bin contains the raw bytes for the setup metadata
  • setup.template contains the raw and unprocessed metadata in JSON format
  • setup.json contains the setup metadata with all format fields expanded

Similarly, there are files.bin, files.template, and files.json that contain the metadata of the archived files. The files that are extracted under the "embedded" directory are usually parts of the InnoSetup installer and not user data. All archived files are extracted within the directory named "data".

Expand source code Browse git
class xtinno(ArchiveUnit):
    """
    Extract files from InnoSetup archives. The unit extracts the following synthetic metadata files
    under the "meta" directory:

    - `setup.bin` contains the raw bytes for the setup metadata
    - `setup.template` contains the raw and unprocessed metadata in JSON format
    - `setup.json` contains the setup metadata with all format fields expanded

    Similarly, there are `files.bin`, `files.template`, and `files.json` that contain the metadata
    of the archived files. The files that are extracted under the "embedded" directory are usually
    parts of the InnoSetup installer and not user data. All archived files are extracted within the
    directory named "data".
    """
    def unpack(self, data: bytearray):
        def post_process_json(doc):
            if isinstance(doc, dict):
                return {key: post_process_json(val) for key, val in doc.items()}
            if isinstance(doc, list):
                return [post_process_json(entry) for entry in doc]
            if not isinstance(doc, str):
                return doc
            try:
                return inno.emulator.reset().expand_constant(doc)
            except Exception:
                return doc

        inno = InnoArchive(data, self)

        password: bytes = self.args.pwd
        password = password.decode(self.codec) if password else None

        if any(file.encrypted for file in inno.files) and password is None:
            self.log_info('some files are password-protected and no password was given')

        with BytesAsArrayEncoder as encoder:
            yield self._pack('meta/setup.bin', None, inno.streams.TSetup.data)
            doc = inno.setup_info.json()
            yield self._pack('meta/setup.template', None, encoder.dumps(doc).encode(self.codec))
            doc = post_process_json(doc)
            yield self._pack('meta/setup.json', None, encoder.dumps(doc).encode(self.codec))

        with BytesAsArrayEncoder as encoder:
            yield self._pack('meta/files.bin', None, inno.streams.TData.data)
            doc = inno.setup_data.json()
            yield self._pack('meta/files.template', None, encoder.dumps(doc).encode(self.codec))
            doc = post_process_json(doc)
            yield self._pack('meta/files.json', None, encoder.dumps(doc).encode(self.codec))

        def _uninstaller(i=inno):
            return i.read_stream(i.streams.Uninstaller)
        yield self._pack('embedded/uninstaller.exe', None, _uninstaller)

        if license := inno.setup_info.Header.get_license():
            yield self._pack('embedded/license.rtf', None, license.encode(self.codec))

        if script := inno.setup_info.Header.get_script():
            yield self._pack('embedded/script.bin', None, script)
            yield self._pack('embedded/script.ps', None,
                lambda i=inno: i.ifps.disassembly().encode(self.codec))

        if dll := inno.setup_info.get_decompress_dll():
            yield self._pack(F'embedded/decompress.{magic(dll).extension}', None, dll)

        if dll := inno.setup_info.get_decryption_dll():
            yield self._pack(F'embedded/decryption.{magic(dll).extension}', None, dll)

        for size, images in (
            ('small', inno.setup_info.get_wizard_images_small()),
            ('large', inno.setup_info.get_wizard_images_large()),
        ):
            _formatting = len(str(len(images) + 1))
            for k, img in enumerate(images, 1):
                yield self._pack(F'embedded/images/{size}{k:0{_formatting}d}.{magic(img).extension}', None, img)

        for file in inno.files:
            if file.dupe:
                continue

            def _read(inno=inno, file=file, pwd=password):
                if pwd is None:
                    inno.guess_password(10)
                if self.leniency > 0:
                    return inno.read_file(file, pwd)
                try:
                    return inno.read_file_and_check(file, pwd)
                except InvalidPassword:
                    raise
                except Exception as E:
                    raise ValueError(F'{E!s} [ignore this check with -L]') from E

            yield self._pack(file.path, file.date, _read,
                tags=[t.name for t in SetupFileFlags if t & file.tags])

    @classmethod
    def handles(self, data):
        if data[:2] != B'MZ':
            return False
        if re.search(re.escape(InnoArchive.ChunkPrefix), data) is None:
            return False
        return bool(
            re.search(BR'Inno Setup Setup Data \(\d+\.\d+\.', data))

Ancestors

Class variables

var required_dependencies
var optional_dependencies

Methods

def unpack(self, data)
Expand source code Browse git
def unpack(self, data: bytearray):
    def post_process_json(doc):
        if isinstance(doc, dict):
            return {key: post_process_json(val) for key, val in doc.items()}
        if isinstance(doc, list):
            return [post_process_json(entry) for entry in doc]
        if not isinstance(doc, str):
            return doc
        try:
            return inno.emulator.reset().expand_constant(doc)
        except Exception:
            return doc

    inno = InnoArchive(data, self)

    password: bytes = self.args.pwd
    password = password.decode(self.codec) if password else None

    if any(file.encrypted for file in inno.files) and password is None:
        self.log_info('some files are password-protected and no password was given')

    with BytesAsArrayEncoder as encoder:
        yield self._pack('meta/setup.bin', None, inno.streams.TSetup.data)
        doc = inno.setup_info.json()
        yield self._pack('meta/setup.template', None, encoder.dumps(doc).encode(self.codec))
        doc = post_process_json(doc)
        yield self._pack('meta/setup.json', None, encoder.dumps(doc).encode(self.codec))

    with BytesAsArrayEncoder as encoder:
        yield self._pack('meta/files.bin', None, inno.streams.TData.data)
        doc = inno.setup_data.json()
        yield self._pack('meta/files.template', None, encoder.dumps(doc).encode(self.codec))
        doc = post_process_json(doc)
        yield self._pack('meta/files.json', None, encoder.dumps(doc).encode(self.codec))

    def _uninstaller(i=inno):
        return i.read_stream(i.streams.Uninstaller)
    yield self._pack('embedded/uninstaller.exe', None, _uninstaller)

    if license := inno.setup_info.Header.get_license():
        yield self._pack('embedded/license.rtf', None, license.encode(self.codec))

    if script := inno.setup_info.Header.get_script():
        yield self._pack('embedded/script.bin', None, script)
        yield self._pack('embedded/script.ps', None,
            lambda i=inno: i.ifps.disassembly().encode(self.codec))

    if dll := inno.setup_info.get_decompress_dll():
        yield self._pack(F'embedded/decompress.{magic(dll).extension}', None, dll)

    if dll := inno.setup_info.get_decryption_dll():
        yield self._pack(F'embedded/decryption.{magic(dll).extension}', None, dll)

    for size, images in (
        ('small', inno.setup_info.get_wizard_images_small()),
        ('large', inno.setup_info.get_wizard_images_large()),
    ):
        _formatting = len(str(len(images) + 1))
        for k, img in enumerate(images, 1):
            yield self._pack(F'embedded/images/{size}{k:0{_formatting}d}.{magic(img).extension}', None, img)

    for file in inno.files:
        if file.dupe:
            continue

        def _read(inno=inno, file=file, pwd=password):
            if pwd is None:
                inno.guess_password(10)
            if self.leniency > 0:
                return inno.read_file(file, pwd)
            try:
                return inno.read_file_and_check(file, pwd)
            except InvalidPassword:
                raise
            except Exception as E:
                raise ValueError(F'{E!s} [ignore this check with -L]') from E

        yield self._pack(file.path, file.date, _read,
            tags=[t.name for t in SetupFileFlags if t & file.tags])

Inherited members