Module refinery.units.formats.archive.xtinno

Expand source code Browse git
from __future__ import annotations

from refinery.lib import json
from refinery.lib.inno.archive import (
    InnoArchive,
    InnoChecksumMismatch,
    SetupFileFlags,
    is_inno_setup,
)
from refinery.lib.mime import FileMagicInfo as magic
from refinery.lib.structures import struct_to_json
from refinery.units import RefineryPartialResult
from refinery.units.formats.archive import ArchiveUnit


class _ps:
    """
    Note: This unit generates 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 _expand_constants(inno: InnoArchive, doc):
    """
    Expands the Inno Setup constants in every string of the given JSON document.
    """
    if isinstance(doc, dict):
        return {key: _expand_constants(inno, val) for key, val in doc.items()}
    if isinstance(doc, list):
        return [_expand_constants(inno, entry) for entry in doc]
    if not isinstance(doc, str):
        return doc
    try:
        return inno.expand_constants(doc)
    except Exception:
        return doc


class xtinno(ArchiveUnit, _ps, docs='{0}{p}{PathExtractorUnit}{p}{_ps}'):
    """
    Extract files from InnoSetup archives.
    """
    def unpack(self, data: bytearray):
        inno = InnoArchive(data, self)

        _pwd: bytes = self.args.pwd
        password = _pwd.decode(self.codec) if _pwd 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')

        if t_setup := inno.streams.TSetup.data:
            yield self._pack('meta/setup.bin', None, t_setup)
            doc = struct_to_json(inno.setup_info)
            yield self._pack('meta/setup.template', None, json.dumps(doc, tojson=json.bytes_as_array))
            doc = _expand_constants(inno, doc)
            yield self._pack('meta/setup.json', None, json.dumps(doc, tojson=json.bytes_as_array))

        if t_data := inno.streams.TData.data:
            yield self._pack('meta/files.bin', None, t_data)
            doc = struct_to_json(inno.setup_data)
            yield self._pack('meta/files.template', None, json.dumps(doc, tojson=json.bytes_as_array))
            doc = _expand_constants(inno, doc)
            yield self._pack('meta/files.json', None, json.dumps(doc, tojson=json.bytes_as_array))

        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: ps.disassembly().encode(self.codec) if (ps := i.ifps) else B'')

        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, read=inno.extractor(file, password)):
                if pwd is None:
                    inno.guess_password(10)
                try:
                    return inno.check_file(file, read())
                except InnoChecksumMismatch as mismatch:
                    raise RefineryPartialResult(str(mismatch), mismatch.data) from mismatch

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

    @classmethod
    def handles(cls, data):
        return is_inno_setup(data)

Classes

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

Extract files from InnoSetup archives.

This unit extracts items with an associated virtual path from a container; each extracted item is emitted as a separate chunk with a corresponding meta variable named "path".

Positional arguments to PathExtractorUnit are patterns to filter the extracted items. Use the -x flag to add an exclusion pattern. To extract all files with a foo or bar extension, but none that has the word "temp" in its path:

PathExtractorUnit .foo .bar -x temp

To view only the paths of all chunks, use the listing switch:

emit data | ... | PathExtractorUnit -l

Otherwise, extracted items are written to the standard output port and usually require a frame to properly process. In order to dump all extracted data to disk, the following pipeline can be used:

emit data | ... | PathExtractorUnit [| dump extracted/{path} ]

The value {path} is a placeholder which is substituted by the virtual path of the extracted item. When several items have the same path, each of them gets a number appended to its path in the order in which PathExtractorUnit lists them, and a pattern that matches the path before the number selects all of them. When using PathExtractorUnit to unpack a file on disk, the following pattern can be useful:

ef pack.bin [| PathExtractorUnit -j | d2p ]

The unit ef is also a path extractor. By specifying -j (or --join), the paths of extracted items are combined. Here, d2p is a shortcut for dump {path}. It deconflicts the joined paths with the local file system: If pack.bin contains items one.txt and two.txt, the following local file tree would be the result:

pack.bin
pack/one.txt
pack/two.txt

Finally, the -d (or --drop) switch can be used to not create (or alter) the path metadata at all, which is useful in cases where path metadata from a previous unit should be preserved.

Note: This unit generates 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, _ps, docs='{0}{p}{PathExtractorUnit}{p}{_ps}'):
    """
    Extract files from InnoSetup archives.
    """
    def unpack(self, data: bytearray):
        inno = InnoArchive(data, self)

        _pwd: bytes = self.args.pwd
        password = _pwd.decode(self.codec) if _pwd 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')

        if t_setup := inno.streams.TSetup.data:
            yield self._pack('meta/setup.bin', None, t_setup)
            doc = struct_to_json(inno.setup_info)
            yield self._pack('meta/setup.template', None, json.dumps(doc, tojson=json.bytes_as_array))
            doc = _expand_constants(inno, doc)
            yield self._pack('meta/setup.json', None, json.dumps(doc, tojson=json.bytes_as_array))

        if t_data := inno.streams.TData.data:
            yield self._pack('meta/files.bin', None, t_data)
            doc = struct_to_json(inno.setup_data)
            yield self._pack('meta/files.template', None, json.dumps(doc, tojson=json.bytes_as_array))
            doc = _expand_constants(inno, doc)
            yield self._pack('meta/files.json', None, json.dumps(doc, tojson=json.bytes_as_array))

        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: ps.disassembly().encode(self.codec) if (ps := i.ifps) else B'')

        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, read=inno.extractor(file, password)):
                if pwd is None:
                    inno.guess_password(10)
                try:
                    return inno.check_file(file, read())
                except InnoChecksumMismatch as mismatch:
                    raise RefineryPartialResult(str(mismatch), mismatch.data) from mismatch

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

    @classmethod
    def handles(cls, data):
        return is_inno_setup(data)

Ancestors

Subclasses

Class variables

var reverse

The type of the None singleton.

Methods

def unpack(self, data)
Expand source code Browse git
def unpack(self, data: bytearray):
    inno = InnoArchive(data, self)

    _pwd: bytes = self.args.pwd
    password = _pwd.decode(self.codec) if _pwd 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')

    if t_setup := inno.streams.TSetup.data:
        yield self._pack('meta/setup.bin', None, t_setup)
        doc = struct_to_json(inno.setup_info)
        yield self._pack('meta/setup.template', None, json.dumps(doc, tojson=json.bytes_as_array))
        doc = _expand_constants(inno, doc)
        yield self._pack('meta/setup.json', None, json.dumps(doc, tojson=json.bytes_as_array))

    if t_data := inno.streams.TData.data:
        yield self._pack('meta/files.bin', None, t_data)
        doc = struct_to_json(inno.setup_data)
        yield self._pack('meta/files.template', None, json.dumps(doc, tojson=json.bytes_as_array))
        doc = _expand_constants(inno, doc)
        yield self._pack('meta/files.json', None, json.dumps(doc, tojson=json.bytes_as_array))

    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: ps.disassembly().encode(self.codec) if (ps := i.ifps) else B'')

    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, read=inno.extractor(file, password)):
            if pwd is None:
                inno.guess_password(10)
            try:
                return inno.check_file(file, read())
            except InnoChecksumMismatch as mismatch:
                raise RefineryPartialResult(str(mismatch), mismatch.data) from mismatch

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

Inherited members