Module refinery.units.formats.archive.xtinno

InnoSetup parsing based on innoextract source code and its Python port in Malcat. The Malcat implementation served as the initial template but was re-written to work with refinery's data structures.

Expand source code Browse git
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
InnoSetup parsing based on [innoextract][IE] source code and its Python port in [Malcat][MC].
The [Malcat][MC] implementation served as the initial template but was re-written to work
with refinery's data structures.

[IE]: https://constexpr.org/innoextract/
[MC]: https://malcat.fr/
"""
from __future__ import annotations

import enum
import re
import struct
import functools
import dataclasses
import codecs

import lzma
import zlib
import bz2

from datetime import datetime, timezone
from hashlib import sha256, sha1, md5

from refinery.units.formats.archive import ArchiveUnit
from refinery.units.formats.pe.perc import perc
from refinery.lib.structures import Struct, StructReader

from refinery.lib.tools import exception_to_string, one
from refinery.lib.lcid import LCID, DEFAULT_CODEPAGE
from refinery.lib.types import ByteStr
from refinery.lib.json import BytesAsStringEncoder

from refinery.units.crypto.cipher.rc4 import rc4
from refinery.units.crypto.cipher.chacha import xchacha
from refinery.units.crypto.keyderive.pbkdf2 import pbkdf2

from typing import TYPE_CHECKING, NamedTuple

if TYPE_CHECKING:
    from typing import (
        List,
        Optional,
        Type,
        TypeVar,
    )
    _T = TypeVar('_T')
    _E = TypeVar('_T', bound=enum.IntEnum)


class InvalidPassword(ValueError):
    def __init__(self, password: Optional[str] = None):
        if password is None:
            super().__init__('A password is required and none was given.')
        else:
            super().__init__('The given password is not correct.')


class IncorrectVersion(RuntimeError):
    pass


class FileChunkOutOfBounds(LookupError):
    pass


class IVF(enum.IntFlag):
    NoFlag   = 0b0000 # noqa
    Legacy   = 0b0001 # noqa
    Bits16   = 0b0010 # noqa
    UTF_16   = 0b0100 # noqa
    InnoSX   = 0b1000 # noqa
    Legacy32 = 0b0001
    Legacy16 = 0b0011
    IsLegacy = 0b0011


def _enum(options: Type[_E], value: int, default: _E):
    try:
        return options(value)
    except ValueError:
        return default


class InnoVersion(NamedTuple):
    major: int
    minor: int
    patch: int
    build: int = 0
    flags: IVF = IVF.NoFlag

    @property
    def semver(self):
        return (self.major, self.minor, self.patch, self.build)

    @property
    def unicode(self):
        return self.flags & IVF.UTF_16 == IVF.UTF_16

    @property
    def legacy(self):
        return self.flags & IVF.Legacy

    @property
    def ascii(self):
        return self.flags & IVF.UTF_16 == IVF.NoFlag

    @property
    def isx(self):
        return bool(self.flags & IVF.InnoSX)

    @property
    def bits(self):
        return 0x10 if self.flags & IVF.Bits16 else 0x20

    @classmethod
    def ParseLegacy(cls, dfn: bytes):
        v, s, _ = dfn.partition(B'\x1A')
        if s and (m := re.fullmatch(BR'i(\d+)\.(\d+)\.(\d+)--(16|32)', v)):
            major = int(m[1])
            minor = int(m[2])
            build = int(m[3])
            flags = IVF.Legacy16 if m[3] == B'16' else IVF.Legacy32
            return cls(major, minor, build, 0, flags)
        raise ValueError(dfn)

    @classmethod
    def Parse(cls, dfn: bytes):
        versions: List[InnoVersion] = []
        for match in [m.groups() for m in re.finditer(rb'(.*?)\((\d+(?:\.\d+){2,3})(?:.*?\(([uU])\))?', dfn)]:
            sv = tuple(map(int, match[1].split(B'.')))
            sv = (sv + (0,))[:4]
            vf = IVF.NoFlag
            if sv >= (6, 3, 0) or match[2]:
                vf |= IVF.UTF_16
            if any(isx in match[0] for isx in (B'My Inno Setup Extensions', B'with ISX')):
                vf |= IVF.InnoSX
            versions.append(InnoVersion(*sv, vf))
        if len(versions) == 1:
            return versions[0]
        if len(versions) == 2:
            a, b = versions
            return InnoVersion(*max(a.semver, b.semver), a.flags | b.flags)
        raise ValueError(dfn)

    def __str__(self):
        v = F'v{self.major}.{self.minor}.{self.patch:02d}.{self.build}'
        a = R'a'
        u = R'u'
        if self.flags & IVF.InnoSX:
            a = R''
            v = F'{v}x'
        t = u if self.flags & IVF.UTF_16 else a
        v = F'{v}{t}'
        if b := {
            IVF.Legacy16: '16',
            IVF.Legacy32: '32',
        }.get(self.flags & IVF.IsLegacy):
            v = F'{v}/{b}'
        return v

    def __repr__(self):
        return str(self)

    def is_ambiguous(self):
        try:
            return _IS_AMBIGUOUS[self]
        except KeyError:
            return True


_I = InnoVersion

_FILE_TIME_1970_01_01 = 116444736000000000
_DEFAULT_INNO_VERSION = _I(5, 0, 0, 0, IVF.UTF_16)

_IS_AMBIGUOUS = {
    _I(1, 2, 10, 0, IVF.Legacy16): False,
    _I(1, 2, 10, 0, IVF.Legacy32): False,
    _I(1, 3,  3, 0, IVF.NoFlag): False, # noqa
    _I(1, 3,  9, 0, IVF.NoFlag): False, # noqa
    _I(1, 3, 10, 0, IVF.NoFlag): False, # noqa
    _I(1, 3, 10, 0, IVF.InnoSX): False, # noqa
    _I(1, 3, 12, 1, IVF.InnoSX): False, # noqa
    _I(1, 3, 21, 0, IVF.NoFlag): True,  # noqa
    _I(1, 3, 21, 0, IVF.InnoSX): True,  # noqa
    _I(1, 3, 24, 0, IVF.NoFlag): False, # noqa
    _I(1, 3, 24, 0, IVF.InnoSX): False, # noqa
    _I(1, 3, 25, 0, IVF.NoFlag): False, # noqa
    _I(1, 3, 25, 0, IVF.InnoSX): False, # noqa
    _I(2, 0,  0, 0, IVF.NoFlag): False, # noqa
    _I(2, 0,  1, 0, IVF.NoFlag): True,  # noqa
    _I(2, 0,  2, 0, IVF.NoFlag): False, # noqa
    _I(2, 0,  5, 0, IVF.NoFlag): False, # noqa
    _I(2, 0,  6, 0, IVF.NoFlag): False, # noqa
    _I(2, 0,  6, 0, IVF.InnoSX): False, # noqa
    _I(2, 0,  7, 0, IVF.NoFlag): False, # noqa
    _I(2, 0,  8, 0, IVF.NoFlag): False, # noqa
    _I(2, 0,  8, 0, IVF.InnoSX): False, # noqa
    _I(2, 0, 10, 0, IVF.InnoSX): False, # noqa
    _I(2, 0, 11, 0, IVF.NoFlag): False, # noqa
    _I(2, 0, 11, 0, IVF.InnoSX): False, # noqa
    _I(2, 0, 17, 0, IVF.NoFlag): False, # noqa
    _I(2, 0, 17, 0, IVF.InnoSX): False, # noqa
    _I(2, 0, 18, 0, IVF.NoFlag): False, # noqa
    _I(2, 0, 18, 0, IVF.InnoSX): False, # noqa
    _I(3, 0,  0, 0, IVF.NoFlag): False, # noqa
    _I(3, 0,  1, 0, IVF.NoFlag): False, # noqa
    _I(3, 0,  1, 0, IVF.InnoSX): False, # noqa
    _I(3, 0,  3, 0, IVF.NoFlag): True,  # noqa
    _I(3, 0,  3, 0, IVF.InnoSX): True,  # noqa
    _I(3, 0,  4, 0, IVF.NoFlag): False, # noqa
    _I(3, 0,  4, 0, IVF.InnoSX): False, # noqa
    _I(3, 0,  5, 0, IVF.NoFlag): False, # noqa
    _I(3, 0,  6, 1, IVF.InnoSX): False, # noqa
    _I(4, 0,  0, 0, IVF.NoFlag): False, # noqa
    _I(4, 0,  1, 0, IVF.NoFlag): False, # noqa
    _I(4, 0,  3, 0, IVF.NoFlag): False, # noqa
    _I(4, 0,  5, 0, IVF.NoFlag): False, # noqa
    _I(4, 0,  9, 0, IVF.NoFlag): False, # noqa
    _I(4, 0, 10, 0, IVF.NoFlag): False, # noqa
    _I(4, 0, 11, 0, IVF.NoFlag): False, # noqa
    _I(4, 1,  0, 0, IVF.NoFlag): False, # noqa
    _I(4, 1,  2, 0, IVF.NoFlag): False, # noqa
    _I(4, 1,  3, 0, IVF.NoFlag): False, # noqa
    _I(4, 1,  4, 0, IVF.NoFlag): False, # noqa
    _I(4, 1,  5, 0, IVF.NoFlag): False, # noqa
    _I(4, 1,  6, 0, IVF.NoFlag): False, # noqa
    _I(4, 1,  8, 0, IVF.NoFlag): False, # noqa
    _I(4, 2,  0, 0, IVF.NoFlag): False, # noqa
    _I(4, 2,  1, 0, IVF.NoFlag): False, # noqa
    _I(4, 2,  2, 0, IVF.NoFlag): False, # noqa
    _I(4, 2,  3, 0, IVF.NoFlag): True,  # noqa
    _I(4, 2,  4, 0, IVF.NoFlag): False, # noqa
    _I(4, 2,  5, 0, IVF.NoFlag): False, # noqa
    _I(4, 2,  6, 0, IVF.NoFlag): False, # noqa
    _I(5, 0,  0, 0, IVF.NoFlag): False, # noqa
    _I(5, 0,  1, 0, IVF.NoFlag): False, # noqa
    _I(5, 0,  3, 0, IVF.NoFlag): False, # noqa
    _I(5, 0,  4, 0, IVF.NoFlag): False, # noqa
    _I(5, 1,  0, 0, IVF.NoFlag): False, # noqa
    _I(5, 1,  2, 0, IVF.NoFlag): False, # noqa
    _I(5, 1,  7, 0, IVF.NoFlag): False, # noqa
    _I(5, 1, 10, 0, IVF.NoFlag): False, # noqa
    _I(5, 1, 13, 0, IVF.NoFlag): False, # noqa
    _I(5, 2,  0, 0, IVF.NoFlag): False, # noqa
    _I(5, 2,  1, 0, IVF.NoFlag): False, # noqa
    _I(5, 2,  3, 0, IVF.NoFlag): False, # noqa
    _I(5, 2,  5, 0, IVF.NoFlag): False, # noqa
    _I(5, 2,  5, 0, IVF.UTF_16): False, # noqa
    _I(5, 3,  0, 0, IVF.NoFlag): False, # noqa
    _I(5, 3,  0, 0, IVF.UTF_16): False, # noqa
    _I(5, 3,  3, 0, IVF.NoFlag): False, # noqa
    _I(5, 3,  3, 0, IVF.UTF_16): False, # noqa
    _I(5, 3,  5, 0, IVF.NoFlag): False, # noqa
    _I(5, 3,  5, 0, IVF.UTF_16): False, # noqa
    _I(5, 3,  6, 0, IVF.NoFlag): False, # noqa
    _I(5, 3,  6, 0, IVF.UTF_16): False, # noqa
    _I(5, 3,  7, 0, IVF.NoFlag): False, # noqa
    _I(5, 3,  7, 0, IVF.UTF_16): False, # noqa
    _I(5, 3,  8, 0, IVF.NoFlag): False, # noqa
    _I(5, 3,  8, 0, IVF.UTF_16): False, # noqa
    _I(5, 3,  9, 0, IVF.NoFlag): False, # noqa
    _I(5, 3,  9, 0, IVF.UTF_16): False, # noqa
    _I(5, 3, 10, 0, IVF.NoFlag): True,  # noqa
    _I(5, 3, 10, 0, IVF.UTF_16): True,  # noqa
    _I(5, 3, 10, 1, IVF.NoFlag): False, # noqa
    _I(5, 3, 10, 1, IVF.UTF_16): False, # noqa
    _I(5, 4,  2, 0, IVF.NoFlag): True,  # noqa
    _I(5, 4,  2, 0, IVF.UTF_16): True,  # noqa
    _I(5, 4,  2, 1, IVF.NoFlag): False, # noqa
    _I(5, 4,  2, 1, IVF.UTF_16): False, # noqa
    _I(5, 5,  0, 0, IVF.NoFlag): True,  # noqa
    _I(5, 5,  0, 0, IVF.UTF_16): True,  # noqa
    _I(5, 5,  0, 1, IVF.NoFlag): False, # noqa
    _I(5, 5,  0, 1, IVF.UTF_16): False, # noqa
    _I(5, 5,  6, 0, IVF.NoFlag): False, # noqa
    _I(5, 5,  6, 0, IVF.UTF_16): False, # noqa
    _I(5, 5,  7, 0, IVF.NoFlag): True,  # noqa
    _I(5, 5,  7, 0, IVF.UTF_16): True,  # noqa
    _I(5, 5,  7, 1, IVF.NoFlag): True,  # noqa
    _I(5, 5,  7, 1, IVF.UTF_16): True,  # noqa
    _I(5, 6,  0, 0, IVF.NoFlag): False, # noqa
    _I(5, 6,  0, 0, IVF.UTF_16): False, # noqa
    _I(5, 6,  2, 0, IVF.NoFlag): False, # noqa
    _I(5, 6,  2, 0, IVF.UTF_16): False, # noqa
    _I(6, 0,  0, 0, IVF.UTF_16): False, # noqa
    _I(6, 1,  0, 0, IVF.UTF_16): False, # noqa
    _I(6, 3,  0, 0, IVF.UTF_16): False, # noqa
    _I(6, 4,  0, 0, IVF.UTF_16): False, # noqa
    _I(6, 4,  0, 1, IVF.UTF_16): False, # noqa
}

_VERSIONS = sorted(_IS_AMBIGUOUS)


class JsonStruct(Struct):
    def json(self):
        def _json(v):
            if isinstance(v, list):
                return [_json(x) for x in v]
            if isinstance(v, dict):
                return {x: _json(y) for x, y in v.items()}
            if isinstance(v, JsonStruct):
                return v.json()
            if isinstance(v, enum.IntFlag):
                return [option.name for option in v.__class__ if v & option == option]
            if isinstance(v, enum.IntEnum):
                return v.name
            if isinstance(v, memoryview):
                return codecs.decode(v, 'latin1')
            return v
        return {
            k: _json(v) for k, v in self.__dict__.items()
            if not k.startswith('_')
        }


class InnoStruct(JsonStruct):
    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, codec: str = 'latin1'):
        if version.unicode:
            self._read_string = functools.partial(
                reader.read_length_prefixed_utf16, bytecount=True)
        else:
            def _read():
                data = reader.read_length_prefixed()
                try:
                    return codecs.decode(data, codec)
                except (LookupError, UnicodeDecodeError):
                    # TODO
                    return codecs.decode(data, 'latin1')
            self._read_string = _read


class CheckSumType(enum.IntEnum):
    Missing = 0 # noqa
    Adler32 = 1 # noqa
    CRC32   = 2 # noqa
    MD5     = 3 # noqa
    SHA1    = 4 # noqa
    SHA256  = 5 # noqa

    def strong(self):
        return self.value >= 3


class Flags(enum.IntFlag):
    Empty                       = 0           # noqa
    DisableStartupPrompt        = enum.auto() # noqa
    Uninstallable               = enum.auto() # noqa
    CreateAppDir                = enum.auto() # noqa
    DisableDirPage              = enum.auto() # noqa
    DisableDirExistsWarning     = enum.auto() # noqa
    DisableProgramGroupPage     = enum.auto() # noqa
    AllowNoIcons                = enum.auto() # noqa
    AlwaysRestart               = enum.auto() # noqa
    BackSolid                   = enum.auto() # noqa
    AlwaysUsePersonalGroup      = enum.auto() # noqa
    WindowVisible               = enum.auto() # noqa
    WindowShowCaption           = enum.auto() # noqa
    WindowResizable             = enum.auto() # noqa
    WindowStartMaximized        = enum.auto() # noqa
    EnableDirDoesntExistWarning = enum.auto() # noqa
    DisableAppendDir            = enum.auto() # noqa
    Password                    = enum.auto() # noqa
    AllowRootDirectory          = enum.auto() # noqa
    DisableFinishedPage         = enum.auto() # noqa
    AdminPrivilegesRequired     = enum.auto() # noqa
    AlwaysCreateUninstallIcon   = enum.auto() # noqa
    OverwriteUninstRegEntries   = enum.auto() # noqa
    ChangesAssociations         = enum.auto() # noqa
    CreateUninstallRegKey       = enum.auto() # noqa
    UsePreviousAppDir           = enum.auto() # noqa
    BackColorHorizontal         = enum.auto() # noqa
    UsePreviousGroup            = enum.auto() # noqa
    UpdateUninstallLogAppName   = enum.auto() # noqa
    UsePreviousSetupType        = enum.auto() # noqa
    DisableReadyMemo            = enum.auto() # noqa
    AlwaysShowComponentsList    = enum.auto() # noqa
    FlatComponentsList          = enum.auto() # noqa
    ShowComponentSizes          = enum.auto() # noqa
    UsePreviousTasks            = enum.auto() # noqa
    DisableReadyPage            = enum.auto() # noqa
    AlwaysShowDirOnReadyPage    = enum.auto() # noqa
    AlwaysShowGroupOnReadyPage  = enum.auto() # noqa
    BzipUsed                    = enum.auto() # noqa
    AllowUNCPath                = enum.auto() # noqa
    UserInfoPage                = enum.auto() # noqa
    UsePreviousUserInfo         = enum.auto() # noqa
    UninstallRestartComputer    = enum.auto() # noqa
    RestartIfNeededByRun        = enum.auto() # noqa
    ShowTasksTreeLines          = enum.auto() # noqa
    ShowLanguageDialog          = enum.auto() # noqa
    DetectLanguageUsingLocale   = enum.auto() # noqa
    AllowCancelDuringInstall    = enum.auto() # noqa
    WizardImageStretch          = enum.auto() # noqa
    AppendDefaultDirName        = enum.auto() # noqa
    AppendDefaultGroupName      = enum.auto() # noqa
    EncryptionUsed              = enum.auto() # noqa
    ChangesEnvironment          = enum.auto() # noqa
    ShowUndisplayableLanguages  = enum.auto() # noqa
    SetupLogging                = enum.auto() # noqa
    SignedUninstaller           = enum.auto() # noqa
    UsePreviousLanguage         = enum.auto() # noqa
    DisableWelcomePage          = enum.auto() # noqa
    CloseApplications           = enum.auto() # noqa
    RestartApplications         = enum.auto() # noqa
    AllowNetworkDrive           = enum.auto() # noqa
    ForceCloseApplications      = enum.auto() # noqa
    AppNameHasConsts            = enum.auto() # noqa
    UsePreviousPrivileges       = enum.auto() # noqa
    WizardResizable             = enum.auto() # noqa
    UninstallLogging            = enum.auto() # noqa


class AutoBool(enum.IntEnum):
    Auto = 0
    No = 1
    Yes = 2

    @classmethod
    def From(cls, b: bool):
        return AutoBool.Yes if b else AutoBool.No


class WizardStyle(enum.IntEnum):
    Classic = 0
    Modern = 1


class StoredAlphaFormat(enum.IntEnum):
    AlphaIgnored = 0
    AlphaDefined = 1
    AlphaPremultiplied = 2


class UninstallLogMode(enum.IntEnum):
    Append = 0
    New = 1
    Overwrite = 2


class SetupStyle(enum.IntEnum):
    Classic = 0
    Modern = 1


class PrivilegesRequired(enum.IntEnum):
    Nothing = 0
    PowerUser = 1
    Admin = 2
    Lowest = 3


class PrivilegesRequiredOverrideAllowed(enum.IntEnum):
    CommandLine = 0
    Dialog = 1


class LanguageDetection(enum.IntEnum):
    UI = 0
    Locale = 1
    Nothing = 2


class CompressionMethod(enum.IntEnum):
    Store = 0
    ZLib = 1
    Bzip2 = 2
    Lzma1 = 3
    Lzma2 = 4

    def legacy_check(self, max: int, ver: str):
        if self.value > max:
            raise ValueError(F'Compression method {self.value} cannot be represented before version {ver}.')
        return self

    def legacy_conversion_pre_4_2_5(self):
        return self.legacy_check(2, '4.2.5').__class__(self.value + 1)

    def legacy_conversion_pre_4_2_6(self):
        if self == CompressionMethod.Store:
            return self
        return self.legacy_check(2, '4.2.6').__class__(self.value + 1)

    def legacy_conversion_pre_5_3_9(self):
        return self.legacy_check(3, '5.3.9')


class Architecture(enum.IntFlag):
    Unknown = 0b00000 # noqa
    X86     = 0b00001 # noqa
    AMD64   = 0b00010 # noqa
    IA64    = 0b00100 # noqa
    ARM64   = 0b01000 # noqa
    All     = 0b01111 # noqa


class PasswordType(enum.IntEnum):
    CRC32     = 0           # noqa
    Nothing   = 0           # noqa
    MD5       = enum.auto() # noqa
    SHA1      = enum.auto() # noqa
    XChaCha20 = enum.auto() # noqa


class SetupTypeEnum(enum.IntEnum):
    User = 0
    DefaultFull = 1
    DefaultCompact = 2
    DefaultCustom = 3


class SetupFlags(enum.IntFlag):
    Fixed                     = 0b00001 # noqa
    Restart                   = 0b00010 # noqa
    DisableNoUninstallWarning = 0b00100 # noqa
    Exclusive                 = 0b01000 # noqa
    DontInheritCheck          = 0b10000 # noqa


class StreamCompressionMethod(enum.IntEnum):
    Store = 0
    ZLib = 1
    LZMA1 = 2


class StreamHeader(InnoStruct):
    def __init__(self, reader: StructReader[memoryview], name: str, version: InnoVersion):
        super().__init__(reader, version)
        self.Name = name
        self.HeaderCrc = reader.u32()
        self.CompressedSize = size = reader.u32()
        if version >= (4, 0, 9):
            self.StoredSize = self.CompressedSize
            if not reader.u8():
                self.Compression = StreamCompressionMethod.Store
            elif version >= (4, 1, 6):
                self.Compression = StreamCompressionMethod.LZMA1
            else:
                self.Compression = StreamCompressionMethod.ZLib
        else:
            self.UncompresedSize = reader.u32()
            if size == 0xFFFFFFFF:
                self.StoredSize = self.UncompresedSize
                self.Compression = StreamCompressionMethod.Store
            else:
                self.StoredSize = size
                self.Compression = StreamCompressionMethod.ZLib
            # Add the size of a CRC32 checksum for each 4KiB subblock
            block_count, _r = divmod(self.StoredSize, 4096)
            block_count += int(bool(_r))
            self.StoredSize += 4 * block_count


class CrcCompressedBlock(JsonStruct):
    def __init__(self, reader: StructReader[memoryview], size: int):
        self.BlockCrc = reader.u32()
        self.BlockData = reader.read(size)


class TSetupOffsets(Struct):
    def __init__(self, reader: StructReader[memoryview]):
        self.id = reader.read(12)
        self.version = reader.u32()
        self.total_size = reader.u32()
        self.exe_offset = reader.u32()
        self.exe_uncompressed_size = reader.u32()
        self.exe_crc = reader.u32()
        self.setup0_offset = reader.u32()
        self.setup1_offset = reader.u32()
        self.offsets_crc = reader.u32()
        self.base = min(
            self.exe_offset,
            self.setup0_offset,
            self.setup1_offset,
        )
        self.setup0 = self.setup0_offset - self.base
        self.setup1 = self.setup1_offset - self.base


@dataclasses.dataclass
class InnoFile:
    reader: StructReader[ByteStr]
    version: InnoVersion
    meta: SetupDataEntry
    path: str = ""
    dupe: bool = False
    setup: Optional[SetupFile] = None
    compression_method: Optional[CompressionMethod] = None
    password_hash: bytes = B''
    password_salt: bytes = B''
    password_type: PasswordType = PasswordType.Nothing

    @property
    def tags(self):
        if s := self.setup:
            return s.Flags
        else:
            return SetupFileFlags.Empty

    @property
    def unicode(self):
        return self.version.unicode

    @property
    def compression(self):
        if self.meta.Flags & SetupDataEntryFlags.ChunkCompressed:
            return self.compression_method
        return CompressionMethod.Store

    @property
    def offset(self):
        return self.meta.Offset

    @property
    def size(self):
        return self.meta.FileSize

    @property
    def date(self):
        return self.meta.FileTime

    @property
    def chunk_offset(self):
        return self.meta.ChunkOffset

    @property
    def chunk_length(self):
        return self.meta.ChunkSize

    @property
    def checksum(self):
        return self.meta.Checksum

    @property
    def checksum_type(self):
        return self.meta.ChecksumType

    @property
    def encrypted(self):
        return bool(self.meta.Flags & SetupDataEntryFlags.ChunkEncrypted)

    @property
    def filtered(self):
        return bool(self.meta.Flags & SetupDataEntryFlags.CallInstructionOptimized)

    def check(self, data: ByteStr):
        t = self.checksum_type
        if t == CheckSumType.Missing:
            return None
        if t == CheckSumType.Adler32:
            return zlib.adler32(data) & 0xFFFFFFFF
        if t == CheckSumType.CRC32:
            return zlib.crc32(data) & 0xFFFFFFFF
        if t == CheckSumType.MD5:
            return md5(data).digest()
        if t == CheckSumType.SHA1:
            return sha1(data).digest()
        if t == CheckSumType.SHA256:
            return sha256(data).digest()
        raise ValueError(F'Unknown checksum type: {t!r}')


@dataclasses.dataclass
class InnoStream:
    header: StreamHeader
    blocks: list[CrcCompressedBlock] = dataclasses.field(default_factory=list)
    data: Optional[bytearray] = None

    @property
    def compression(self):
        return self.header.Compression

    @property
    def name(self):
        return self.header.Name


class InstallMode(enum.IntEnum):
    Normal     = enum.auto() # noqa
    Silent     = enum.auto() # noqa
    VerySilent = enum.auto() # noqa


class SetupHeader(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion):
        super().__init__(reader, version)

        def read_string():
            return reader.read_length_prefixed()

        if version < (1, 3, 0):
            # skip uncompressed size
            reader.u32()

        if True:
            self.AppName = read_string()
            self.AppVersionedName = read_string()
        if version >= (1, 3, 0):
            self.AppId = read_string()
        if True:
            self.AppCopyright = read_string()
        if version >= (1, 3, 0):
            self.AppPublisher = read_string()
            self.AppPublisherUrl = read_string()
        if version >= (5, 1, 13):
            self.AppSupportPhone = read_string()
        if version >= (1, 3, 0):
            self.AppSupportUrl = read_string()
            self.AppUpdatesUrl = read_string()
            self.AppVersion = read_string()
        if True:
            self.DefaultDirName = read_string()
            self.DefaultGroupName = read_string()
        if version < (3, 0, 0):
            self.UninstallIconName = reader.read_length_prefixed(encoding='cp1252')
        if True:
            self.BaseFilename = read_string()
        if (1, 3, 0) <= version < (5, 2, 5):
            self._license = reader.read_length_prefixed_ascii()
            self.InfoHead = reader.read_length_prefixed_ascii()
            self.InfoTail = reader.read_length_prefixed_ascii()
        if version >= (1, 3, 3):
            self.UninstallFilesDir = read_string()
        if version >= (1, 3, 6):
            self.UninstallName = read_string()
            self.UninstallIcon = read_string()
        if version >= (1, 3, 14):
            self.AppMutex = read_string()
        if version >= (3, 0, 0):
            self.DefaultUsername = read_string()
            self.DefaultOrganisation = read_string()
        if version >= (4, 0, 0) or version.isx and version >= (3, 0, 6, 1):
            self.DefaultSerial = read_string()
        if (4, 0, 0) <= version < (5, 2, 5) or version.isx and version >= (1, 3, 24):
            self.CompiledCode = read_string()
        if version >= (4, 2, 4):
            self.AppReadmeFile = read_string()
            self.AppContact = read_string()
            self.AppComment = read_string()
            self.AppModifyPath = read_string()
        if version >= (5, 3, 8):
            self.CreateUninstallRegistryKey = read_string()
        if version >= (5, 3, 10):
            self.Uninstallable = read_string()
        if version >= (5, 5, 0):
            self.CloseApplicationsFilter = read_string()
        if version >= (5, 5, 6):
            self.SetupMutex = read_string()
        if version >= (5, 6, 1):
            self.ChangesEnvironment = read_string()
            self.ChangesAssociations = read_string()
        if version >= (6, 3, 0):
            self.ArchitecturesAllowed32 = read_string()
            self.ArchitecturesAllowed64 = read_string()
        if version >= (5, 2, 5):
            self._license = reader.read_length_prefixed_ascii()
            self.InfoHead = reader.read_length_prefixed_ascii()
            self.InfoTail = reader.read_length_prefixed_ascii()
        if version >= (5, 2, 1) and version < (5, 3, 10):
            self.UninstallerSignature = read_string()
        if version >= (5, 2, 5):
            self.CompiledCode = read_string()

        if self.CompiledCode and self.CompiledCode[:4] != B'IFPS':
            raise ValueError('Invalid signature in compiled code.')

        if version >= (2, 0, 6) and version.ascii:
            self.Charset = reader.read_integer(0x100)
        else:
            self.Charset = [False] * 0x100

        if version >= (4, 0, 0):
            self.LanguageCount = reader.u32()
        elif version >= (2, 0, 1):
            self.LanguageCount = 1
        else:
            self.LanguageCount = 0

        if version >= (4, 2, 1):
            self.MessageCount = reader.u32()
        else:
            self.MessageCount = 0

        if version >= (4, 1, 0):
            self.PermissionCount = reader.u32()
        else:
            self.PermissionCount = 0

        if version >= (2, 0, 0) or version.isx:
            self.TypeCount = reader.u32()
            self.ComponentCount = reader.u32()
        else:
            self.TypeCount = 0
            self.ComponentCount = 0

        if version >= (2, 0, 0) or version.isx and version >= (1, 3, 17):
            self.TaskCount = reader.u32()
        else:
            self.TaskCount = 0

        self.DirectoryCount = reader.u32()
        self.FileCount = reader.u32()
        self.DataEntryCount = reader.u32()
        self.IconCount = reader.u32()
        self.IniEntryCount = reader.u32()
        self.RegistryEntryCount = reader.u32()
        self.DeleteEntryCount = reader.u32()
        self.UninstallDeleteEntryCount = reader.u32()
        self.RunEntryCount = reader.u32()
        self.UninstallRunEntryCount = reader.u32()

        if version < (1, 3, 0):
            _license_len = reader.u32()
            _infhead_len = reader.u32()
            _inftail_len = reader.u32()

        self.MinimumWindowsVersion = WindowsVersion(reader, version)
        self.MaximumWindowsVersion = WindowsVersion(reader, version)

        self.BackColor1 = reader.u32() if (0, 0, 0) <= version < (6, 4, 0, 1) else 0
        self.BackColor2 = reader.u32() if (1, 3, 3) <= version < (6, 4, 0, 1) else 0

        if version < (5, 5, 7):
            self.ImageBackColor = reader.u32()
        if (2, 0, 0) <= version < (5, 0, 4) or version.isx:
            self.SmallImageBackColor = reader.u32()
        if version >= (6, 0, 0):
            self.WizardStyle = WizardStyle(reader.u8())
            self.WizardResizePercentX = reader.u32()
            self.WizardResizePercentY = reader.u32()
        else:
            self.WizardStyle = WizardStyle.Classic
            self.WizardResizePercentX = 0
            self.WizardResizePercentY = 0

        if version >= (5, 5, 7):
            self.StoredAlphaFormat = StoredAlphaFormat(reader.u8())
        else:
            self.StoredAlphaFormat = StoredAlphaFormat.AlphaIgnored

        if version >= (6, 4, 0):
            self.PasswordType = PasswordType.XChaCha20
            self.PasswordHash = reader.read(4)
        elif version >= (5, 3, 9):
            self.PasswordType = PasswordType.SHA1
            self.PasswordHash = reader.read(20)
        elif version >= (4, 2, 0):
            self.PasswordType = PasswordType.MD5
            self.PasswordHash = reader.read(16)
        else:
            self.PasswordType = PasswordType.CRC32
            self.PasswordHash = reader.u32()

        if version >= (6, 4, 0):
            self.PasswordSalt = reader.read(44)
        elif version >= (4, 2, 2):
            self.PasswordSalt = reader.read(8)
        else:
            self.PasswordSalt = None

        if version >= (4, 0, 0):
            self.ExtraDiskSpace = reader.i64()
            self.SlicesPerDisk = reader.u32()
        else:
            self.ExtraDiskSpace = reader.u32()
            self.SlicesPerDisk = 1

        if (2, 0, 0) <= version < (5, 0, 0):
            self.InstallMode = _enum(InstallMode, reader.u8(), InstallMode.Normal)
        else:
            self.InstallMode = InstallMode.Normal
        if version >= (1, 3, 0):
            self.UninstallLogMode = UninstallLogMode(reader.u8())
        else:
            self.UninstallLogMode = UninstallLogMode.New

        if version >= (5, 0, 0):
            self.SetupStyle = SetupStyle.Modern
        elif (2, 0, 0) <= version or version.isx and version >= (1, 3, 13):
            self.SetupStyle = SetupStyle(reader.u8())
        else:
            self.SetupStyle = SetupStyle.Classic

        if version >= (1, 3, 6):
            self.DirExistsWarning = AutoBool(reader.u8())
        else:
            self.DirExistsWarning = AutoBool.Auto
        if version.isx and (2, 0, 10) <= version < (3, 0, 0):
            self.CodeLineOffset = reader.u32()

        self.Flags = Flags.Empty

        if (3, 0, 0) <= version < (3, 0, 3):
            val = AutoBool(reader.u8())
            if val == AutoBool.Auto:
                self.Flags |= Flags.RestartIfNeededByRun
            elif val == AutoBool.Yes:
                self.Flags |= Flags.AlwaysRestart

        if version >= (3, 0, 4) or version.isx and version >= (3, 0, 3):
            self.PrivilegesRequired = PrivilegesRequired(reader.u8())
        if version >= (5, 7, 0):
            self.PrivilegesRequiredOverrideAllowed = PrivilegesRequiredOverrideAllowed(reader.u8())
        if version >= (4, 0, 10):
            self.ShowLanguageDialog = AutoBool(reader.u8())
            self.LanguageDetection = LanguageDetection(reader.u8())

        if version >= (4, 1, 5):
            method = CompressionMethod(reader.u8())
            if version < (4, 2, 5):
                method = method.legacy_conversion_pre_4_2_5()
            elif version < (4, 2, 6):
                method = method.legacy_conversion_pre_4_2_5()
            elif version < (5, 3, 9):
                method = method.legacy_conversion_pre_5_3_9()
            self.CompressionMethod = method

        if version >= (6, 3, 0):
            self.ArchitecturesAllowed = Architecture.Unknown
            self.ArchitecturesInstalled64 = Architecture.Unknown
        elif version >= (5, 1, 0):
            self.ArchitecturesAllowed = Architecture(reader.u8())
            self.ArchitecturesInstalled64 = Architecture(reader.u8())
        else:
            self.ArchitecturesAllowed = Architecture.All
            self.ArchitecturesInstalled64 = Architecture.All

        if (5, 2, 1) <= version < (5, 3, 10):
            self.UninstallerOriginalSize = reader.u32()
            self.UninstallheaderCrc = reader.u32()
        if version >= (5, 3, 3):
            self.DisableDirPage = AutoBool(reader.u8())
            self.DisableProgramGroupPage = AutoBool(reader.u8())
        if version >= (5, 5, 0):
            self.UninstallDisplaySize = reader.u64()
        elif version >= (5, 3, 6):
            self.UninstallDisplaySize = reader.u32()
        else:
            self.UninstallDisplaySize = 0

        flags = []
        flags.append(Flags.DisableStartupPrompt)
        if version < (5, 3, 10):
            flags.append(Flags.Uninstallable)
        flags.append(Flags.CreateAppDir)
        if version < (5, 3, 3):
            flags.append(Flags.DisableDirPage)
        if version < (1, 3, 6):
            flags.append(Flags.DisableDirExistsWarning)
        if version < (5, 3, 3):
            flags.append(Flags.DisableProgramGroupPage)
        flags.append(Flags.AllowNoIcons)
        if version < (3, 0, 0) or version >= (3, 0, 3):
            flags.append(Flags.AlwaysRestart)
        if version < (1, 3, 3):
            flags.append(Flags.BackSolid)
        flags.append(Flags.AlwaysUsePersonalGroup)
        flags.append(Flags.WindowVisible)
        flags.append(Flags.WindowShowCaption)
        flags.append(Flags.WindowResizable)
        flags.append(Flags.WindowStartMaximized)
        flags.append(Flags.EnableDirDoesntExistWarning)
        if version < (4, 1, 2):
            flags.append(Flags.DisableAppendDir)
        flags.append(Flags.Password)
        flags.append(Flags.AllowRootDirectory)
        flags.append(Flags.DisableFinishedPage)

        if version.bits > 16:
            if version < (3, 0, 4):
                flags.append(Flags.AdminPrivilegesRequired)
            if version < (3, 0, 0):
                flags.append(Flags.AlwaysCreateUninstallIcon)
            if version < (1, 3, 6):
                flags.append(Flags.OverwriteUninstRegEntries)
            if version < (5, 6, 1):
                flags.append(Flags.ChangesAssociations)

        if version < (5, 3, 8):
            flags.append(Flags.CreateUninstallRegKey)

        flags.append(Flags.UsePreviousAppDir)
        flags.append(Flags.BackColorHorizontal)
        flags.append(Flags.UsePreviousGroup)
        flags.append(Flags.UpdateUninstallLogAppName)
        flags.append(Flags.UsePreviousSetupType)
        flags.append(Flags.DisableReadyMemo)
        flags.append(Flags.AlwaysShowComponentsList)
        flags.append(Flags.FlatComponentsList)
        flags.append(Flags.ShowComponentSizes)
        flags.append(Flags.UsePreviousTasks)
        flags.append(Flags.DisableReadyPage)
        flags.append(Flags.AlwaysShowDirOnReadyPage)
        flags.append(Flags.AlwaysShowGroupOnReadyPage)
        if version < (4, 1, 5):
            flags.append(Flags.BzipUsed)
        flags.append(Flags.AllowUNCPath)
        flags.append(Flags.UserInfoPage)
        flags.append(Flags.UsePreviousUserInfo)
        flags.append(Flags.UninstallRestartComputer)
        flags.append(Flags.RestartIfNeededByRun)
        flags.append(Flags.ShowTasksTreeLines)
        if version < (4, 0, 10):
            flags.append(Flags.ShowLanguageDialog)
        if version >= (4, 0, 1) and version < (4, 0, 10):
            flags.append(Flags.DetectLanguageUsingLocale)
        if version >= (4, 0, 9):
            flags.append(Flags.AllowCancelDuringInstall)
        if version >= (4, 1, 3):
            flags.append(Flags.WizardImageStretch)
        if version >= (4, 1, 8):
            flags.append(Flags.AppendDefaultDirName)
            flags.append(Flags.AppendDefaultGroupName)
        if version >= (4, 2, 2):
            flags.append(Flags.EncryptionUsed)
        if version >= (5, 0, 4) and version < (5, 6, 1):
            flags.append(Flags.ChangesEnvironment)
        if version >= (5, 1, 7) and version.ascii:
            flags.append(Flags.ShowUndisplayableLanguages)
        if version >= (5, 1, 13):
            flags.append(Flags.SetupLogging)
        if version >= (5, 2, 1):
            flags.append(Flags.SignedUninstaller)
        if version >= (5, 3, 8):
            flags.append(Flags.UsePreviousLanguage)
        if version >= (5, 3, 9):
            flags.append(Flags.DisableWelcomePage)
        if version >= (5, 5, 0):
            flags.append(Flags.CloseApplications)
            flags.append(Flags.RestartApplications)
            flags.append(Flags.AllowNetworkDrive)
        if version >= (5, 5, 7):
            flags.append(Flags.ForceCloseApplications)
        if version >= (6, 0, 0):
            flags.append(Flags.AppNameHasConsts)
            flags.append(Flags.UsePreviousPrivileges)
            flags.append(Flags.WizardResizable)
        if version >= (6, 3, 0):
            flags.append(Flags.UninstallLogging)

        flagsize, _r = divmod(len(flags), 8)
        flagsize += int(bool(_r))
        bytecheck = bytes(reader.peek(flagsize + 1 + 4 + 1))

        if bytecheck[0] == 0:
            if bytecheck[~0] != 0 or bytecheck[~3:~0] == B'\0\0\0':
                reader.u8()

        for flag in flags:
            if reader.read_bit():
                self.Flags |= flag

        if version < (3, 0, 4):
            self.PrivilegesRequired = PrivilegesRequired.Admin if (
                self.Flags & Flags.AdminPrivilegesRequired
            ) else PrivilegesRequired.Nothing

        if version < (4, 0, 10):
            self.ShowLanguageDialog = AutoBool.From(
                self.Flags & Flags.ShowLanguageDialog)
            self.LanguageDetection = LanguageDetection.Locale if (
                self.Flags & Flags.DetectLanguageUsingLocale
            ) else LanguageDetection.UI

        if version < (4, 1, 5):
            self.CompressionMethod = CompressionMethod.Bzip2 if (
                self.Flags & Flags.BzipUsed
            ) else CompressionMethod.ZLib

        if version < (5, 3, 3):
            self.DisableDirPage = AutoBool.From(self.Flags & Flags.DisableDirPage)
            self.DisableProgramGroupPage = AutoBool.From(self.Flags & Flags.DisableProgramGroupPage)

        if version < (1, 3, 0):
            def _read_ascii(n: int):
                return codecs.decode(reader.read(_license_len), 'cp1252')
            self._license = _read_ascii(_license_len)
            self.InfoHead = _read_ascii(_infhead_len)
            self.InfoTail = _read_ascii(_inftail_len)

        reader.byte_align()

        if flagsize == 3:
            reader.u8()

    def get_license(self):
        return self._license

    def recode_strings(self, codec: str):
        for coded_string_attribute in [
            'AppComment',
            'AppContact',
            'AppCopyright',
            'AppId',
            'AppModifyPath',
            'AppMutex',
            'AppName',
            'AppPublisher',
            'AppPublisherUrl',
            'AppReadmeFile',
            'AppSupportPhone',
            'AppSupportUrl',
            'AppUpdatesUrl',
            'AppVersion',
            'AppVersionedName',
            'BaseFilename',
            'ChangesAssociations',
            'ChangesEnvironment',
            'CloseApplicationsFilter',
            'CreateUninstallRegistryKey',
            'DefaultDirName',
            'DefaultGroupName',
            'DefaultOrganisation',
            'DefaultSerial',
            'DefaultUsername',
            'SetupMutex',
            'Uninstallable',
            'UninstallFilesDir',
            'UninstallIcon',
            'UninstallName',
        ]:
            try:
                value: bytes = getattr(self, coded_string_attribute)
            except AttributeError:
                continue
            if not isinstance(value, (bytes, bytearray, memoryview)):
                raise RuntimeError(F'Attempting to decode {coded_string_attribute} which was already decoded.')
            setattr(self, coded_string_attribute, codecs.decode(value, codec))


class Version(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion):
        super().__init__(reader, version)
        self.Build = reader.u16() if version >= (1, 3, 19) else 0
        self.Minor = reader.u8()
        self.Major = reader.u8()


class WindowsVersion(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion):
        super().__init__(reader, version)
        self.WindowsVersion = Version(reader, version)
        self.NtVersion = Version(reader, version)
        (
            self.ServicePackMinor,
            self.ServicePackMajor,
        ) = reader.read_struct('BB') if version >= (1, 3, 19) else (0, 0)


class LanguageId(JsonStruct):
    def __init__(self, reader: StructReader[memoryview]):
        self.Value = reader.i32()
        self.Name = LCID.get(self.Value, None)


class SetupLanguage(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, _: TSetup):
        super().__init__(reader, version)
        read_string = self._read_string

        self.Name = read_string()
        LanguageName = reader.read_length_prefixed()

        self.DialogFont = read_string()
        self.TitleFont = read_string()
        self.WelcomeFont = read_string()
        self.CopyrightFont = read_string()
        self.Data = reader.read_length_prefixed()

        if version >= (4, 0, 1):
            self.LicenseText = reader.read_length_prefixed_ascii()
            self.InfoBefore = reader.read_length_prefixed_ascii()
            self.InfoAfter = reader.read_length_prefixed_ascii()

        self.LanguageId = LanguageId(reader)

        if version < (4, 2, 2):
            self.Codepage = DEFAULT_CODEPAGE.get(self.LanguageId.Value, 'cp1252')
        elif version.ascii:
            cp = reader.u32() or 1252
            self.Codepage = F'cp{cp}'
        else:
            if version < (5, 3, 0):
                reader.u32()
            self.Codepage = 'utf-16le'

        if version >= (4, 2, 2):
            self.LanguageName = codecs.decode(LanguageName, 'utf-16le')
        else:
            self.LanguageName = codecs.decode(LanguageName, self.Codepage)

        self.DialogFontSize = reader.u32()

        if version < (4, 1, 0):
            self.DialogFontStandardHeight = reader.u32()

        self.TitleFontSize = reader.u32()
        self.WelcomeFontSize = reader.u32()
        self.CopyrightFontSize = reader.u32()

        if version >= (5, 2, 3):
            self.RightToLeft = reader.u8()


class SetupMessage(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        self.EncodedName = self._read_string()
        value = reader.read_length_prefixed()
        language = reader.i32()
        try:
            codec = parent.Languages[language].Codepage
        except IndexError:
            pass
        try:
            self.Value = codecs.decode(value, codec)
        except LookupError:
            # TODO: This is a fallback
            self.Value = codecs.decode(value, 'latin1')


class SetupType(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        read_string = self._read_string
        self.Name = read_string()
        self.Description = read_string()
        if version >= (4, 0, 1):
            self.Languages = read_string()
        if version >= (4, 0, 0) or version.isx and version >= (1, 3, 24):
            self.Check = read_string()
        self.MinimumWindowsVersion = WindowsVersion(reader, version)
        self.MaximumWindowsVersion = WindowsVersion(reader, version)
        self.CustsomTypeCode = reader.u8()
        if version >= (4, 0, 3):
            self.SetupType = SetupTypeEnum(reader.u8())
        else:
            self.SetupType = SetupTypeEnum.User
        if version >= (4, 0, 0):
            self.Size = reader.u64()
        else:
            self.Size = reader.u32()


class SetupComponent(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        read_string = self._read_string
        self.Name = read_string()
        self.Description = read_string()
        self.Types = read_string()
        if version >= (4, 0, 1):
            self.Languages = read_string()
        if version >= (4, 0, 0) or version.isx and version >= (1, 3, 24):
            self.Check = read_string()
        if version >= (4, 0, 0):
            self.ExtraDiskSpace = reader.u64()
        if version >= (4, 0, 0) or version.isx and version >= (3, 0, 3):
            self.Level = reader.u32()
        else:
            self.Level = 0
        if version >= (4, 0, 0) or version.isx and version >= (3, 0, 4):
            self.Used = bool(reader.u8())
        else:
            self.Used = True
        if True:
            self.MinimumWindowsVersion = WindowsVersion(reader, version)
            self.MaximumWindowsVersion = WindowsVersion(reader, version)
            self.Flags = SetupFlags(reader.u8())
        if version >= (4, 0, 0):
            self.Size = reader.u64()
        elif version >= (2, 0, 0) or version.isx and version >= (1, 3, 24):
            self.Size = reader.u32()


class SetupTaskFlags(enum.IntFlag):
    Empty            = 0           # noqa
    Exclusive        = enum.auto() # noqa
    Unchecked        = enum.auto() # noqa
    Restart          = enum.auto() # noqa
    CheckedOne       = enum.auto() # noqa
    DontInheritCheck = enum.auto() # noqa


class SetupTask(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        read_string = self._read_string

        self.Name = read_string()
        self.Description = read_string()
        self.GroupDescription = read_string()
        self.Components = read_string()

        if version >= (4, 0, 1):
            self.Languages = read_string()
        if version >= (4, 0, 0) or version.isx and version >= (1, 3, 24):
            self.Check = read_string()
        if version >= (4, 0, 0) or version.isx and version >= (3, 0, 3):
            self.Level = reader.u32()
        else:
            self.Level = 0
        if version >= (4, 0, 0) or version.isx and version >= (3, 0, 4):
            self.Used = bool(reader.u8())
        else:
            self.Used = True
        if True:
            self.MinimumWindowsVersion = WindowsVersion(reader, version)
            self.MaximumWindowsVersion = WindowsVersion(reader, version)

        self.Flags = SetupTaskFlags.Empty

        def flagbit(f):
            self.Flags |= f if reader.read_bit() else 0

        if True:
            flagbit(SetupTaskFlags.Exclusive)
            flagbit(SetupTaskFlags.Unchecked)
        if version >= (2, 0, 5):
            flagbit(SetupTaskFlags.Restart)
        if version >= (2, 0, 6):
            flagbit(SetupTaskFlags.CheckedOne)
        if version >= (4, 2, 3):
            flagbit(SetupTaskFlags.DontInheritCheck)

        reader.byte_align()


class SetupCondition(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        read_string = self._read_string
        if version >= (2, 0, 0) or version.isx and version >= (1, 3, 8):
            self.Components = read_string()
        if version >= (2, 0, 0) or version.isx and version >= (1, 3, 17):
            self.Tasks = read_string()
        if version >= (4, 0, 1):
            self.Languages = read_string()
        if version >= (4, 0, 0) or version.isx and version >= (1, 3, 24):
            self.Check = read_string()
        else:
            self.Check = None
        if version >= (4, 1, 0):
            self.AfterInstall = read_string()
            self.BeforeInstall = read_string()


class SetupDirectoryFlags(enum.IntFlag):
    Empty                = 0           # noqa
    NeverUninstall       = enum.auto() # noqa
    DeleteAfterInstall   = enum.auto() # noqa
    AlwaysUninstall      = enum.auto() # noqa
    SetNtfsCompression   = enum.auto() # noqa
    UnsetNtfsCompression = enum.auto() # noqa


class SetupDirectory(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        read_string = self._read_string

        if version < (1, 3, 0):
            self.UncompressedSize = reader.u32()
        if True:
            self.Name = read_string()
            self.Condition = SetupCondition(reader, version, parent)

        if (4, 0, 11) <= version < (4, 1, 0):
            self.Permissions = read_string()
        if version >= (2, 0, 11):
            self.Attributes = reader.u32()

        self.MinimumWindowsVersion = WindowsVersion(reader, version)
        self.MaximumWindowsVersion = WindowsVersion(reader, version)

        if version >= (4, 1, 0):
            self.Permissions = reader.u16()

        self.Flags = SetupDirectoryFlags.Empty

        def flagbit(f):
            self.Flags |= f if reader.read_bit() else 0
        flagbit(SetupDirectoryFlags.NeverUninstall)
        flagbit(SetupDirectoryFlags.DeleteAfterInstall)
        flagbit(SetupDirectoryFlags.AlwaysUninstall)
        flagbit(SetupDirectoryFlags.SetNtfsCompression)
        flagbit(SetupDirectoryFlags.UnsetNtfsCompression)
        reader.byte_align()


class SetupPermission(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        self.Permission = reader.read_length_prefixed()


class SetupFileFlags(enum.IntFlag):
    Empty                              = 0           # noqa
    ConfirmOverwrite                   = enum.auto() # noqa
    NeverUninstall                     = enum.auto() # noqa
    RestartReplace                     = enum.auto() # noqa
    DeleteAfterInstall                 = enum.auto() # noqa
    RegisterServer                     = enum.auto() # noqa
    RegisterTypeLib                    = enum.auto() # noqa
    SharedFile                         = enum.auto() # noqa
    IsReadmeFile                       = enum.auto() # noqa
    CompareTimeStamp                   = enum.auto() # noqa
    FontIsNotTrueType                  = enum.auto() # noqa
    SkipIfSourceDoesntExist            = enum.auto() # noqa
    OverwriteReadOnly                  = enum.auto() # noqa
    OverwriteSameVersion               = enum.auto() # noqa
    CustomDestName                     = enum.auto() # noqa
    OnlyIfDestFileExists               = enum.auto() # noqa
    NoRegError                         = enum.auto() # noqa
    UninsRestartDelete                 = enum.auto() # noqa
    OnlyIfDoesntExist                  = enum.auto() # noqa
    IgnoreVersion                      = enum.auto() # noqa
    PromptIfOlder                      = enum.auto() # noqa
    DontCopy                           = enum.auto() # noqa
    UninsRemoveReadOnly                = enum.auto() # noqa
    RecurseSubDirsExternal             = enum.auto() # noqa
    ReplaceSameVersionIfContentsDiffer = enum.auto() # noqa
    DontVerifyChecksum                 = enum.auto() # noqa
    UninsNoSharedFilePrompt            = enum.auto() # noqa
    CreateAllSubDirs                   = enum.auto() # noqa
    Bits32                             = enum.auto() # noqa
    Bits64                             = enum.auto() # noqa
    ExternalSizePreset                 = enum.auto() # noqa
    SetNtfsCompression                 = enum.auto() # noqa
    UnsetNtfsCompression               = enum.auto() # noqa
    GacInstall                         = enum.auto() # noqa


class SetupFileType(enum.IntEnum):
    UserFile = 0
    UninstExe = 1
    RegSvrExe = 2


class SetupFileCopyMode(enum.IntEnum):
        Normal                  = enum.auto() # noqa
        IfDoesntExist           = enum.auto() # noqa
        AlwaysOverwrite         = enum.auto() # noqa
        AlwaysSkipIfSameOrOlder = enum.auto() # noqa


class SetupFile(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        read_string = self._read_string

        if version < (1, 3, 0):
            self.UncompressedSize = reader.u32()
        if True:
            self.Source = read_string()
            self.Destination = read_string()
            self.InstallFontName = read_string()
        if version >= (5, 2, 5):
            self.StrongAssemblyName = read_string()

        self.Condition = SetupCondition(reader, version, parent)

        self.MinimumWindowsVersion = WindowsVersion(reader, version)
        self.MaximumWindowsVersion = WindowsVersion(reader, version)

        self.Location = reader.u32()
        self.Attributes = reader.u32()
        self.ExternalSize = reader.u64() if version >= (4, 0, 0) else reader.u32()

        self.Flags = SetupFileFlags.Empty

        if version < (3, 0, 5):
            copy = _enum(SetupFileCopyMode, reader.u8(), SetupFileCopyMode.Normal)
            if copy == SetupFileCopyMode.AlwaysSkipIfSameOrOlder:
                pass
            elif copy == SetupFileCopyMode.Normal:
                self.Flags |= SetupFileFlags.PromptIfOlder
            elif copy == SetupFileCopyMode.IfDoesntExist:
                self.Flags |= SetupFileFlags.OnlyIfDoesntExist | SetupFileFlags.PromptIfOlder
            elif copy == SetupFileCopyMode.AlwaysOverwrite:
                self.Flags |= SetupFileFlags.IgnoreVersion | SetupFileFlags.PromptIfOlder

        if version >= (4, 1, 0):
            self.Permissions = reader.u16()

        def flagbit(f):
            self.Flags |= f if reader.read_bit() else 0

        flagstart = reader.tell()

        if True:
            flagbit(SetupFileFlags.ConfirmOverwrite)
            flagbit(SetupFileFlags.NeverUninstall)
            flagbit(SetupFileFlags.RestartReplace)
            flagbit(SetupFileFlags.DeleteAfterInstall)
        if version.bits > 16:
            flagbit(SetupFileFlags.RegisterServer)
            flagbit(SetupFileFlags.RegisterTypeLib)
            flagbit(SetupFileFlags.SharedFile)
        if version < (2, 0, 0) and not version.isx:
            flagbit(SetupFileFlags.IsReadmeFile)
        if True:
            flagbit(SetupFileFlags.CompareTimeStamp)
            flagbit(SetupFileFlags.FontIsNotTrueType)
        if version >= (1, 2, 5):
            flagbit(SetupFileFlags.SkipIfSourceDoesntExist)
        if version >= (1, 2, 6):
            flagbit(SetupFileFlags.OverwriteReadOnly)
        if version >= (1, 3, 21):
            flagbit(SetupFileFlags.OverwriteSameVersion)
            flagbit(SetupFileFlags.CustomDestName)
        if version >= (1, 3, 25):
            flagbit(SetupFileFlags.OnlyIfDestFileExists)
        if version >= (2, 0, 5):
            flagbit(SetupFileFlags.NoRegError)
        if version >= (3, 0, 1):
            flagbit(SetupFileFlags.UninsRestartDelete)
        if version >= (3, 0, 5):
            flagbit(SetupFileFlags.OnlyIfDoesntExist)
            flagbit(SetupFileFlags.IgnoreVersion)
            flagbit(SetupFileFlags.PromptIfOlder)
        if version >= (4, 0, 0) or version.isx and version >= (3, 0, 6, 1):
            flagbit(SetupFileFlags.DontCopy)
        if version >= (4, 0, 5):
            flagbit(SetupFileFlags.UninsRemoveReadOnly)
        if version >= (4, 1, 8):
            flagbit(SetupFileFlags.RecurseSubDirsExternal)
        if version >= (4, 2, 1):
            flagbit(SetupFileFlags.ReplaceSameVersionIfContentsDiffer)
        if version >= (4, 2, 5):
            flagbit(SetupFileFlags.DontVerifyChecksum)
        if version >= (5, 0, 3):
            flagbit(SetupFileFlags.UninsNoSharedFilePrompt)
        if version >= (5, 1, 0):
            flagbit(SetupFileFlags.CreateAllSubDirs)
        if version >= (5, 1, 2):
            flagbit(SetupFileFlags.Bits32)
            flagbit(SetupFileFlags.Bits64)
        if version >= (5, 2, 0):
            flagbit(SetupFileFlags.ExternalSizePreset)
            flagbit(SetupFileFlags.SetNtfsCompression)
            flagbit(SetupFileFlags.UnsetNtfsCompression)
        if version >= (5, 2, 5):
            flagbit(SetupFileFlags.GacInstall)

        reader.byte_align()

        if reader.tell() - flagstart == 3:
            reader.u8()

        self.Type = SetupFileType(reader.u8())


class TSetup(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion):
        super().__init__(reader, version)
        self.Header = h = SetupHeader(reader, version)

        def _array(count: int, parser: Type[_T]) -> List[_T]:
            return [parser(reader, version, self) for _ in range(count)]

        self.Languages = _array(h.LanguageCount, SetupLanguage)
        _default_codec = 'cp1252'

        if version.unicode:
            self.Codec = 'utf-16le'
        elif not self.Languages:
            self.Codec = _default_codec
        else:
            self.Codec = self.Languages[0].Codepage
            if any(language.Codepage == _default_codec for language in self.Languages):
                self.Codec = _default_codec

        if version.ascii:
            h.recode_strings(self.Codec)
        else:
            h.recode_strings('latin1')

        # if version < INNO_VERSION(4, 0, 0):
        #  load_wizard_and_decompressor

        self.Messages    = _array(h.MessageCount,    SetupMessage)     # noqa
        self.Permissions = _array(h.PermissionCount, SetupPermission)  # noqa
        self.Types       = _array(h.TypeCount,       SetupType)        # noqa
        self.Components  = _array(h.ComponentCount,  SetupComponent)   # noqa
        self.Tasks       = _array(h.TaskCount,       SetupTask)        # noqa
        self.Directories = _array(h.DirectoryCount,  SetupDirectory)   # noqa
        self.Files       = _array(h.FileCount,       SetupFile)        # noqa

        # self.Icons                  = _array(h.IconCount,                 SetupIcon)
        # self.IniEntries             = _array(h.IniEntryCount,             SetupIniEntry)
        # self.RegistryEntries        = _array(h.RegistryEntryCount,        SetupRegistryEntry)
        # self.DeleteEntries          = _array(h.DeleteEntryCount,          SetupDeleteEntry)
        # self.UninstallDeleteEntries = _array(h.UninstallDeleteEntryCount, SetupUninstallDeleteEntry)
        # self.RunEntries             = _array(h.RunEntryCount,             SetupRunEntry)
        # self.UninstallRunEntries    = _array(h.UninstallRunEntryCount,    SetupUninstallRunEntry)

        # if version >= INNO_VERSION(4, 0, 0):
        #  load_wizard_and_decompressor


class SetupDataEntryFlags(enum.IntFlag):
    Empty                    = 0           # noqa    
    VersionInfoValid         = enum.auto() # noqa
    VersionInfoNotValid      = enum.auto() # noqa
    BZipped                  = enum.auto() # noqa
    TimeStampInUTC           = enum.auto() # noqa
    IsUninstallerExe         = enum.auto() # noqa
    CallInstructionOptimized = enum.auto() # noqa
    Touch                    = enum.auto() # noqa
    ChunkEncrypted           = enum.auto() # noqa
    ChunkCompressed          = enum.auto() # noqa
    SolidBreak               = enum.auto() # noqa
    Sign                     = enum.auto() # noqa
    SignOnce                 = enum.auto() # noqa


class SetupSignMode(enum.IntEnum):
    NoSetting  = 0  # noqa
    Yes        = 1  # noqa
    Once       = 2  # noqa
    Check      = 3  # noqa


class SetupDataEntry(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion):
        super().__init__(reader, version)
        self.FirstSlice = reader.u32()
        self.LastSlice = reader.u32()
        self.ChunkOffset = reader.u32()
        if version >= (4, 0, 1):
            self.Offset = reader.u64()
        self.FileSize = reader.u64()
        self.ChunkSize = reader.u64()

        if version >= (6, 4, 0):
            self.ChecksumType = CheckSumType.SHA256
            self.Checksum = bytes(reader.read(32))
        elif version >= (5, 3, 9):
            self.ChecksumType = CheckSumType.SHA1
            self.Checksum = bytes(reader.read(20))
        elif version >= (4, 2, 0):
            self.ChecksumType = CheckSumType.MD5
            self.Checksum = bytes(reader.read(16))
        elif version >= (4, 0, 1):
            self.ChecksumType = CheckSumType.CRC32
            self.Checksum = reader.u32()
        else:
            self.ChecksumType = CheckSumType.Adler32
            self.Checksum = reader.u32()

        ft = reader.u64()
        ts = datetime.fromtimestamp(
            (ft - _FILE_TIME_1970_01_01) / 10000000, timezone.utc)
        self.FileTime = ts

        self.FileVersionMs = reader.u32()
        self.FileVersionLs = reader.u32()

        self.Flags = 0

        def flagbit(f):
            self.Flags |= f if reader.read_bit() else 0

        flagbit(SetupDataEntryFlags.VersionInfoNotValid)
        flagbit(SetupDataEntryFlags.VersionInfoValid)

        if version < (4, 0, 1):
            flagbit(SetupDataEntryFlags.BZipped)
        if version >= (4, 0, 10):
            flagbit(SetupDataEntryFlags.TimeStampInUTC)
        if version >= (4, 1, 0):
            flagbit(SetupDataEntryFlags.IsUninstallerExe)
        if version >= (4, 1, 8):
            flagbit(SetupDataEntryFlags.CallInstructionOptimized)
        if version >= (4, 2, 0):
            flagbit(SetupDataEntryFlags.Touch)
        if version >= (4, 2, 2):
            flagbit(SetupDataEntryFlags.ChunkEncrypted)
        if version >= (4, 2, 5):
            flagbit(SetupDataEntryFlags.ChunkCompressed)
        if version >= (5, 1, 13):
            flagbit(SetupDataEntryFlags.SolidBreak)
        if version >= (5, 5, 7) and version < (6, 3, 0):
            flagbit(SetupDataEntryFlags.Sign)
            flagbit(SetupDataEntryFlags.SignOnce)

        reader.byte_align()

        if version >= (6, 3, 0):
            self.SignMode = SetupSignMode(reader.u8())
        elif self.Flags & SetupDataEntryFlags.SignOnce:
            self.SignMode = SetupSignMode.Once
        elif self.Flags & SetupDataEntryFlags.Sign:
            self.SignMode = SetupSignMode.Yes
        else:
            self.SignMode = SetupSignMode.NoSetting


class TData(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion):
        super().__init__(reader, version)
        self.DataEntries: list[SetupDataEntry] = []
        while not reader.eof:
            self.DataEntries.append(SetupDataEntry(reader, version))


class InnoParseResult(NamedTuple):
    version: InnoVersion
    streams: List[InnoStream]
    files: List[InnoFile]
    encrypted_file: Optional[InnoFile]
    warnings: int
    failures: List[str]
    stream0: TSetup
    stream1: TData

    def ok(self):
        return self.warnings == 0 and not self.failures


class xtinno(ArchiveUnit):
    """
    Extract files from InnoSetup archives.
    """

    _STREAM_NAMES = 'meta/TSetup', 'meta/TData', 'meta/Uninstaller'
    _ISCRIPT_NAME = 'meta/script'
    _LICENSE_NAME = 'meta/license.rtf'
    _OFFSETS_PATH = 'RCDATA/11111/0'
    _CHUNK_PREFIX = b'zlb\x1a'
    _MAX_ATTEMPTS = 200_000

    def unpack(self, data: bytearray):
        try:
            file_metadata = one(data | perc(self._OFFSETS_PATH))
        except Exception as E:
            raise ValueError(F'Could not find TSetupOffsets PE resource at {self._OFFSETS_PATH}') from E

        meta = TSetupOffsets(file_metadata)
        view = memoryview(data)
        base = meta.base
        inno = StructReader(view[base:base + meta.total_size])

        self._decompressed = {}
        password = self.args.pwd or None

        blobsize = meta.setup0 - meta.setup1
        inno.seek(meta.setup1)
        blobs = StructReader(inno.read(blobsize))

        header = bytes(inno.read(16))

        try:
            version = InnoVersion.ParseLegacy(header)
        except ValueError:
            header += bytes(inno.read(64 - 16))
            try:
                version = InnoVersion.Parse(header)
            except ValueError:
                name, _, _rest = header.partition(b'\0')
                method = 'broken'
                if any(_rest):
                    header = name.hex()
                else:
                    header = name.decode('latin1')
                if self.leniency < 1:
                    raise ValueError(F'unable to parse header identifier "{header}"')
                version = _DEFAULT_INNO_VERSION
            else:
                header, _, _ = header.partition(B'\0')
                header = header.decode('latin1')
                method = 'modern'
        else:
            header, _, _ = header.partition(B'\x1A')
            header = header.decode('latin1')
            method = 'legacy'

        self.log_info(F'inno {version!s} via {method} header: {header}')

        class _notok(object):
            def __init__(self, e: Exception):
                self.failures = [str(e)]

            def ok(self):
                return False

        def _parse(v: InnoVersion):
            try:
                inno.seekset(inno_start)
                if v.legacy:
                    inno.seekrel(-48)
                r = self._try_parse_as(inno, blobs, v, password)
            except Exception as e:
                self.log_info(F'exception while parsing as {v!s}: {exception_to_string(e)}')
                return _notok(e)
            else:
                results[v] = r
                return r

        inno_start = inno.tell()
        best_parse = None
        best_score = 0
        success = False
        results: dict[InnoVersion, InnoParseResult] = {}

        if not version.is_ambiguous():
            index = _VERSIONS.index(version)
        else:
            try:
                index = max(k for k, v in enumerate(_VERSIONS) if v <= version)
            except Exception:
                index = 0

        upper = index + 1
        lower = index

        while lower > 0 and _IS_AMBIGUOUS[_VERSIONS[lower - 1]]:
            lower -= 1

        versions = [version] + _VERSIONS[lower:index] + _VERSIONS[upper:] + _VERSIONS[:lower]

        for v in versions:
            result = _parse(v)
            if success := result.ok():
                break
            if not result.failures and (best_parse is None or result.warnings < best_score):
                best_score = best_score
                best_parse = result

        if not success:
            if best_parse is not None:
                result = best_parse
                self.log_warn(F'using parse result for {result.version!s} with {result.warnings} warnings')
            else:
                result = min(results.values(), key=lambda result: len(result.failures))
                self.log_warn(F'using parse result for {result.version!s} with {len(result.failures)} failures')
                for k, failure in enumerate(result.failures, 1):
                    self.log_info(F'failure {k}: {failure}')

        version = result.version
        codec = result.stream0.Codec
        stream0 = result.stream0
        stream1 = result.stream1
        files = result.files
        streams = result.streams
        encrypted_file = result.encrypted_file

        if version.unicode:
            codec = 'latin1'
        self.log_info(
            F'inno {version!s} '
            F'compression:{stream0.Header.CompressionMethod.name} '
            F'codepage:{codec} '
            F'password:{stream0.Header.PasswordType.name} '
        )

        if stream0.Header.CompiledCode:
            from refinery.units.formats.ifps import ifps
            script = stream0.Header.CompiledCode
            yield self._pack(F'{self._ISCRIPT_NAME}.ps', None, script | ifps(codec) | bytes)
            yield self._pack(F'{self._ISCRIPT_NAME}.bin', None, script)

        if encrypted_file is not None:
            assert password is None, (
                F'An encrypted test file was chosen even though a password was provided: {password!r}')
            self.log_info(F'guessing password using encrypted file: {encrypted_file.path}')
            try:
                def _pwd(s: str):
                    if not s:
                        return False
                    if re.search(r'\{\w+\}', s):
                        return False
                    if re.search(R'^\w{2,12}://', s):
                        return False
                    return True
                from refinery.units.formats.ifpsstr import ifpsstr
                from itertools import combinations
                strings = script | ifpsstr(codec) | {str}
                strings = [s for s in strings if _pwd(s)]
                total = 0
                for k in range(1, 10):
                    for parts in combinations(strings, k):
                        if total > self._MAX_ATTEMPTS:
                            break
                        total += 1
                        string = ''.join(parts)
                        try:
                            plaintext = self._read_file_and_check(encrypted_file, password=string)
                            if encrypted_file.check(plaintext) != encrypted_file.checksum:
                                continue
                            password = string
                            break
                        except Exception:
                            continue
                    if total > self._MAX_ATTEMPTS:
                        break
            except Exception as e:
                self.log_info(F'failed to extract strings from IFPS: {exception_to_string(e)}')

        yield self._pack(
            streams[2].name, None, lambda s=streams[2]: self._read_stream(s))

        yield self._pack(streams[0].name, None, streams[0].data)

        with BytesAsStringEncoder as encoder:
            yield self._pack(F'{streams[0].name}.json', None,
                encoder.dumps(stream0.json()).encode(self.codec))
            yield self._pack(self._LICENSE_NAME, None,
                stream0.Header.get_license().encode(self.codec))

        yield self._pack(streams[1].name, None, streams[1].data)

        with BytesAsStringEncoder as encoder:
            yield self._pack(F'{streams[1].name}.json', None,
                encoder.dumps(stream1.json()).encode(self.codec))

        for file in files:
            if file.dupe:
                continue
            yield self._pack(
                file.path,
                file.date,
                lambda f=file: self._read_file_and_check(f, password=password),
                tags=[t.name for t in SetupFileFlags if t & file.tags],
            )

    def _try_parse_as(
        self,
        inno: StructReader,
        blobs: StructReader,
        version: InnoVersion,
        password: Optional[str] = None,
        max_failures: int = 5
    ):
        streams: List[InnoStream] = []
        files: List[InnoFile] = []
        encrypted_file = None
        warnings = 0

        for name in self._STREAM_NAMES:
            stream = InnoStream(StreamHeader(inno, name, version))
            streams.append(stream)
            to_read = stream.header.StoredSize
            while to_read > 4:
                block = CrcCompressedBlock(inno, min(to_read - 4, 0x1000))
                stream.blocks.append(block)
                to_read -= len(block)

        self.log_debug(F'{version!s} parsing stream 1 (TData)')
        stream1 = TData(memoryview(self._read_stream(streams[1])), version)

        for meta in stream1.DataEntries:
            file = InnoFile(blobs, version, meta)
            files.append(file)
            if password or not file.encrypted or not file.size:
                continue
            if encrypted_file is None or file.size < encrypted_file.size:
                encrypted_file = file

        self.log_debug(F'{version!s} parsing stream 0 (TSetup)')
        stream0 = TSetup(memoryview(self._read_stream(streams[0])), version)
        path_dedup: dict[str, list[SetupFile]] = {}

        for file in files:
            file.compression_method = stream0.Header.CompressionMethod
            file.password_hash = stream0.Header.PasswordHash
            file.password_type = stream0.Header.PasswordType
            file.password_salt = stream0.Header.PasswordSalt

        for sf in stream0.Files:
            sf: SetupFile
            location = sf.Location
            if location == 0xFFFFFFFF or sf.Type != SetupFileType.UserFile or sf.Source:
                msg = F'skipping file: offset=0x{location:08X} type={sf.Type.name}'
                if sf.Source:
                    msg = F'{msg} src={sf.Source}'
                self.log_debug(msg)
                continue
            if location >= len(files):
                self.log_warn(F'parsed {len(file)} entries, ignoring invalid setup reference to entry {location + 1}')
                continue
            path = sf.Destination.replace('\\', '/')
            if condition := sf.Condition.Check:
                condition = condition.replace(' ', '-')
                path = F'{condition}/{path}'
            path = F'data/{path}'
            path_dedup.setdefault(path, []).append(sf)
            files[location].setup = sf
            files[location].path = path

        for path, infos in path_dedup.items():
            if len(infos) == 1:
                files[infos[0].Location].path = path
                continue
            bycheck = {}
            for info in infos:
                file = files[info.Location]
                if not file.checksum_type.strong():
                    bycheck.clear()
                    break
                dkey = (file.checksum, file.size)
                if dkey in bycheck:
                    self.log_debug(F'skipping exact duplicate: {path}')
                    file.dupe = True
                    continue
                bycheck[dkey] = info
            if bycheck:
                if len(bycheck) == 1:
                    file.path = path
                    continue
                infos = list(bycheck.values())
            for k, info in enumerate(infos):
                files[info.Location].path = F'{path}[{k}]'

        _width = len(str(len(files)))

        for k, file in enumerate(files):
            if file.dupe:
                continue
            if not file.path:
                self.log_debug(F'file {k} does not have a path')
                file.path = F'raw/FileData{k:0{_width}d}'

        warnings = sum(1 for file in files if file.setup is None)
        failures = []
        nonempty = [f for f in files if f.size > 0]

        self._decompressed.clear()

        for file in nonempty:
            if len(failures) >= max_failures:
                break
            if file.setup is None:
                failures.append(F'file {file.path} had no associated metadata')
                continue
            if file.chunk_length < 0x10000:
                try:
                    data = self._read_file(file)
                except InvalidPassword:
                    continue
                except Exception as e:
                    failures.append(F'extraction error for {file.path}: {e!s}')
                    continue
                if file.check(data) != file.checksum:
                    failures.append(F'invalid checksum for {file.path}')

        return InnoParseResult(
            version,
            streams,
            files,
            encrypted_file,
            warnings,
            failures,
            stream0,
            stream1,
        )

    def _read_stream(self, stream: InnoStream):
        if stream.data is not None:
            return stream.data
        result = bytearray()
        it = iter(stream.blocks)
        if stream.compression == StreamCompressionMethod.Store:
            class _dummy(self):
                def decompress(self, b):
                    return b
            dec = _dummy()
        elif stream.compression == StreamCompressionMethod.LZMA1:
            import lzma
            first = next(it).BlockData
            prop, first = first[:5], first[5:]
            filter = lzma._decode_filter_properties(lzma.FILTER_LZMA1, prop)
            dec = lzma.LZMADecompressor(lzma.FORMAT_RAW, filters=[filter])
            result.extend(dec.decompress(first))
        elif stream.compression == StreamCompressionMethod.ZLib:
            import zlib
            dec = zlib.decompressobj()
        for block in it:
            result.extend(dec.decompress(block.BlockData))
        stream.data = result
        return result

    def _read_chunk(self, file: InnoFile, password: Optional[str] = None):
        reader = file.reader
        offset = file.chunk_offset
        length = file.chunk_length
        method = file.compression

        if offset + length > len(reader):
            span = F'0x{offset:X}-0x{offset + length:X}'
            raise LookupError(
                F'File data spans 0x{len(file.reader):X} bytes, but the file {file.path} is located at {span}.')

        reader.seek(offset)
        prefix = reader.read(4)

        if prefix != self._CHUNK_PREFIX:
            raise ValueError(F'Error reading chunk at offset 0x{offset:X}; invalid magic {prefix.hex()}.')

        if file.encrypted:
            if file.password_type == PasswordType.Nothing:
                raise RuntimeError(F'File {file.path} is encrypted, but no password type was set.')
            if password is None:
                raise InvalidPassword
            if file.password_type == PasswordType.XChaCha20:
                salt, iterations, nonce = struct.unpack('=16sI24s', file.password_salt)
                key = password.encode('utf8') | pbkdf2(32, salt, iterations, 'SHA256') | bytes
                test_nonce = list(struct.unpack('6I', nonce))
                test_nonce[2] = ~test_nonce[2]
                test_nonce = struct.pack('6I', test_nonce)
                if B'\0\0\0\0' | xchacha(key, nonce=test_nonce) | bytes != file.password_hash:
                    raise InvalidPassword(password)
                decryptor = xchacha(key, nonce=nonce)
            else:
                password_bytes = password.encode(
                    'utf-16le' if file.unicode else 'utf8')
                algorithm = {
                    PasswordType.SHA1: sha1,
                    PasswordType.MD5 : md5,
                }[file.password_type]
                hash = algorithm(b'PasswordCheckHash' + file.password_salt)
                hash.update(password_bytes)
                if hash.digest() != file.password_hash:
                    raise InvalidPassword(password)
                hash = algorithm(reader.read(8))
                hash.update(password_bytes)
                decryptor = rc4(hash.digest(), discard=1000)

        data = reader.read_exactly(length)

        if file.encrypted:
            data = data | decryptor | bytearray

        if method is None:
            return chunk

        try:
            if method == CompressionMethod.Store:
                chunk = data
            elif method == CompressionMethod.Lzma1:
                props = lzma._decode_filter_properties(lzma.FILTER_LZMA1, data[0:5])
                dec = lzma.LZMADecompressor(lzma.FORMAT_RAW, filters=[props])
                chunk = dec.decompress(data[5:])
            elif method == CompressionMethod.Lzma2:
                props = lzma._decode_filter_properties(lzma.FILTER_LZMA2, data[0:1])
                dec = lzma.LZMADecompressor(lzma.FORMAT_RAW, filters=[props])
                chunk = dec.decompress(data[1:])
            elif method == CompressionMethod.Bzip2:
                chunk = bz2.decompress(data)
            elif method == CompressionMethod.ZLib:
                chunk = zlib.decompress(data)
        except Exception as E:
            if not file.encrypted:
                raise
            raise InvalidPassword(password) from E

        return chunk

    def _read_file(
        self,
        file: InnoFile,
        password: Optional[str] = None,
    ):
        offset = file.chunk_offset
        length = file.chunk_length

        try:
            chunk = self._decompressed[offset, length]
        except KeyError:
            chunk = self._decompressed[offset, length] = self._read_chunk(file, password)

        view = memoryview(chunk)
        data = view[file.offset:file.offset + file.size]

        if file.filtered:
            if file.version >= (5, 2, 0):
                flip = (file.version >= (5, 3, 9))
                data = self._filter_new(data, flip_high_byte=flip)
            else:
                data = self._filter_old(data)

        return data

    def _read_file_and_check(
        self,
        file: InnoFile,
        password: Optional[str] = None,
    ):
        data = self._read_file(file, password)

        if not self.leniency and (cs := file.check(data)) is not None and cs != file.checksum:
            if isinstance(cs, int):
                computed = F'{cs:08X}'
                expected = F'{file.checksum:08X}'
            else:
                computed = cs.hex().upper()
                expected = file.checksum.hex().upper()
            raise ValueError(F'checksum error; computed:{computed} expected:{expected} [ignore this check with -L]')

        return data

    @ArchiveUnit.Requires('numpy', 'speed', 'default', 'extended')
    def _numpy():
        import numpy
        return numpy

    def _filter_new(self, data: ByteStr, flip_high_byte=False):
        try:
            np = self._numpy
        except ImportError:
            return self._filter_new_fallback(data, flip_high_byte)
        u08 = np.uint8
        u32 = np.uint32
        ab0 = bytearray()
        ab1 = bytearray()
        ab2 = bytearray()
        ab3 = bytearray()
        positions = []
        if isinstance(data, bytearray):
            out = data
        else:
            out = bytearray(data)
        mem = memoryview(out)
        for k in range(0, len(mem), 0x10000):
            for match in re.finditer(B'(?s)[\xE8\xE9]....', mem[k:k + 0x10000], flags=re.DOTALL):
                a = match.start() + k
                top = mem[a + 4]
                if top != 0x00 and top != 0xFF:
                    continue
                ab0.append(mem[a + 1])
                ab1.append(mem[a + 2])
                ab2.append(mem[a + 3])
                ab3.append(top)
                positions.append(a + 5)
        ab0 = np.frombuffer(ab0, dtype=u08)
        ab1 = np.frombuffer(ab1, dtype=u08)
        low = np.frombuffer(ab2, dtype=u08).astype(u32)
        msb = np.frombuffer(ab3, dtype=u08)
        sub = np.fromiter(positions, dtype=u32)
        low <<= 8
        low += ab1
        low <<= 8
        low += ab0
        low -= sub
        low &= 0xFFFFFF
        if flip_high_byte:
            flips = low >> 23
            keeps = 1 - flips
            keeps *= msb
            msb ^= 0xFF
            msb *= flips
            msb += keeps
        low += (msb.astype(u32) << 24)
        addresses = low.tobytes()
        for k, offset in enumerate(positions):
            out[offset - 4:offset] = addresses[k * 4:(k + 1) * 4]
        return out

    def _filter_new_fallback(self, data: ByteStr, flip_high_byte=False):
        block_size = 0x10000
        out = bytearray(data)
        i = 0
        while len(data) - i >= 5:
            c = out[i]
            block_size_left = block_size - (i % block_size)
            i += 1
            if (c == 0xE8 or c == 0xE9) and block_size_left >= 5:
                address = out[i:i + 4]
                i += 4
                if address[3] == 0 or address[3] == 0xFF:
                    rel = address[0] | address[1] << 8 | address[2] << 16
                    rel -= i & 0xFFFFFF
                    out[i - 4] = rel & 0xFF
                    out[i - 3] = (rel >> 8) & 0xFF
                    out[i - 2] = (rel >> 16) & 0xFF
                    if flip_high_byte and (rel & 0x800000) != 0:
                        out[i - 1] = (~out[i - 1]) & 0xFF
        return out

    @staticmethod
    def _filter_old(data: ByteStr):
        if not isinstance(data, bytearray):
            data = bytearray(data)
        addr_bytes_left = 0
        addr_offset = 5
        addr = 0
        for i, c in enumerate(data):
            if addr_bytes_left == 0:
                if c == 0xE8 or c == 0xE9:
                    addr = (~addr_offset + 1) & 0xFFFFFFFF
                    addr_bytes_left = 4
            else:
                addr = (addr + c) & 0xFFFFFFFF
                c = addr & 0xFF
                addr = addr >> 8
                addr_bytes_left -= 1
            data[i] = c
        return data

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

Classes

class InvalidPassword (password=None)

Inappropriate argument value (of correct type).

Expand source code Browse git
class InvalidPassword(ValueError):
    def __init__(self, password: Optional[str] = None):
        if password is None:
            super().__init__('A password is required and none was given.')
        else:
            super().__init__('The given password is not correct.')

Ancestors

  • builtins.ValueError
  • builtins.Exception
  • builtins.BaseException
class IncorrectVersion (*args, **kwargs)

Unspecified run-time error.

Expand source code Browse git
class IncorrectVersion(RuntimeError):
    pass

Ancestors

  • builtins.RuntimeError
  • builtins.Exception
  • builtins.BaseException
class FileChunkOutOfBounds (*args, **kwargs)

Base class for lookup errors.

Expand source code Browse git
class FileChunkOutOfBounds(LookupError):
    pass

Ancestors

  • builtins.LookupError
  • builtins.Exception
  • builtins.BaseException
class IVF (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class IVF(enum.IntFlag):
    NoFlag   = 0b0000 # noqa
    Legacy   = 0b0001 # noqa
    Bits16   = 0b0010 # noqa
    UTF_16   = 0b0100 # noqa
    InnoSX   = 0b1000 # noqa
    Legacy32 = 0b0001
    Legacy16 = 0b0011
    IsLegacy = 0b0011

Ancestors

  • enum.IntFlag
  • builtins.int
  • enum.Flag
  • enum.Enum

Class variables

var NoFlag
var Legacy
var Bits16
var UTF_16
var InnoSX
var Legacy32
var Legacy16
var IsLegacy
class InnoVersion (major, minor, patch, build=0, flags=IVF.NoFlag)

InnoVersion(major, minor, patch, build, flags)

Expand source code Browse git
class InnoVersion(NamedTuple):
    major: int
    minor: int
    patch: int
    build: int = 0
    flags: IVF = IVF.NoFlag

    @property
    def semver(self):
        return (self.major, self.minor, self.patch, self.build)

    @property
    def unicode(self):
        return self.flags & IVF.UTF_16 == IVF.UTF_16

    @property
    def legacy(self):
        return self.flags & IVF.Legacy

    @property
    def ascii(self):
        return self.flags & IVF.UTF_16 == IVF.NoFlag

    @property
    def isx(self):
        return bool(self.flags & IVF.InnoSX)

    @property
    def bits(self):
        return 0x10 if self.flags & IVF.Bits16 else 0x20

    @classmethod
    def ParseLegacy(cls, dfn: bytes):
        v, s, _ = dfn.partition(B'\x1A')
        if s and (m := re.fullmatch(BR'i(\d+)\.(\d+)\.(\d+)--(16|32)', v)):
            major = int(m[1])
            minor = int(m[2])
            build = int(m[3])
            flags = IVF.Legacy16 if m[3] == B'16' else IVF.Legacy32
            return cls(major, minor, build, 0, flags)
        raise ValueError(dfn)

    @classmethod
    def Parse(cls, dfn: bytes):
        versions: List[InnoVersion] = []
        for match in [m.groups() for m in re.finditer(rb'(.*?)\((\d+(?:\.\d+){2,3})(?:.*?\(([uU])\))?', dfn)]:
            sv = tuple(map(int, match[1].split(B'.')))
            sv = (sv + (0,))[:4]
            vf = IVF.NoFlag
            if sv >= (6, 3, 0) or match[2]:
                vf |= IVF.UTF_16
            if any(isx in match[0] for isx in (B'My Inno Setup Extensions', B'with ISX')):
                vf |= IVF.InnoSX
            versions.append(InnoVersion(*sv, vf))
        if len(versions) == 1:
            return versions[0]
        if len(versions) == 2:
            a, b = versions
            return InnoVersion(*max(a.semver, b.semver), a.flags | b.flags)
        raise ValueError(dfn)

    def __str__(self):
        v = F'v{self.major}.{self.minor}.{self.patch:02d}.{self.build}'
        a = R'a'
        u = R'u'
        if self.flags & IVF.InnoSX:
            a = R''
            v = F'{v}x'
        t = u if self.flags & IVF.UTF_16 else a
        v = F'{v}{t}'
        if b := {
            IVF.Legacy16: '16',
            IVF.Legacy32: '32',
        }.get(self.flags & IVF.IsLegacy):
            v = F'{v}/{b}'
        return v

    def __repr__(self):
        return str(self)

    def is_ambiguous(self):
        try:
            return _IS_AMBIGUOUS[self]
        except KeyError:
            return True

Ancestors

  • builtins.tuple

Static methods

def ParseLegacy(dfn)
Expand source code Browse git
@classmethod
def ParseLegacy(cls, dfn: bytes):
    v, s, _ = dfn.partition(B'\x1A')
    if s and (m := re.fullmatch(BR'i(\d+)\.(\d+)\.(\d+)--(16|32)', v)):
        major = int(m[1])
        minor = int(m[2])
        build = int(m[3])
        flags = IVF.Legacy16 if m[3] == B'16' else IVF.Legacy32
        return cls(major, minor, build, 0, flags)
    raise ValueError(dfn)
def Parse(dfn)
Expand source code Browse git
@classmethod
def Parse(cls, dfn: bytes):
    versions: List[InnoVersion] = []
    for match in [m.groups() for m in re.finditer(rb'(.*?)\((\d+(?:\.\d+){2,3})(?:.*?\(([uU])\))?', dfn)]:
        sv = tuple(map(int, match[1].split(B'.')))
        sv = (sv + (0,))[:4]
        vf = IVF.NoFlag
        if sv >= (6, 3, 0) or match[2]:
            vf |= IVF.UTF_16
        if any(isx in match[0] for isx in (B'My Inno Setup Extensions', B'with ISX')):
            vf |= IVF.InnoSX
        versions.append(InnoVersion(*sv, vf))
    if len(versions) == 1:
        return versions[0]
    if len(versions) == 2:
        a, b = versions
        return InnoVersion(*max(a.semver, b.semver), a.flags | b.flags)
    raise ValueError(dfn)

Instance variables

var major

Alias for field number 0

var minor

Alias for field number 1

var patch

Alias for field number 2

var build

Alias for field number 3

var flags

Alias for field number 4

var semver
Expand source code Browse git
@property
def semver(self):
    return (self.major, self.minor, self.patch, self.build)
var unicode
Expand source code Browse git
@property
def unicode(self):
    return self.flags & IVF.UTF_16 == IVF.UTF_16
var legacy
Expand source code Browse git
@property
def legacy(self):
    return self.flags & IVF.Legacy
var ascii
Expand source code Browse git
@property
def ascii(self):
    return self.flags & IVF.UTF_16 == IVF.NoFlag
var isx
Expand source code Browse git
@property
def isx(self):
    return bool(self.flags & IVF.InnoSX)
var bits
Expand source code Browse git
@property
def bits(self):
    return 0x10 if self.flags & IVF.Bits16 else 0x20

Methods

def is_ambiguous(self)
Expand source code Browse git
def is_ambiguous(self):
    try:
        return _IS_AMBIGUOUS[self]
    except KeyError:
        return True
class JsonStruct (reader, *args, **kwargs)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class JsonStruct(Struct):
    def json(self):
        def _json(v):
            if isinstance(v, list):
                return [_json(x) for x in v]
            if isinstance(v, dict):
                return {x: _json(y) for x, y in v.items()}
            if isinstance(v, JsonStruct):
                return v.json()
            if isinstance(v, enum.IntFlag):
                return [option.name for option in v.__class__ if v & option == option]
            if isinstance(v, enum.IntEnum):
                return v.name
            if isinstance(v, memoryview):
                return codecs.decode(v, 'latin1')
            return v
        return {
            k: _json(v) for k, v in self.__dict__.items()
            if not k.startswith('_')
        }

Ancestors

Subclasses

Methods

def json(self)
Expand source code Browse git
def json(self):
    def _json(v):
        if isinstance(v, list):
            return [_json(x) for x in v]
        if isinstance(v, dict):
            return {x: _json(y) for x, y in v.items()}
        if isinstance(v, JsonStruct):
            return v.json()
        if isinstance(v, enum.IntFlag):
            return [option.name for option in v.__class__ if v & option == option]
        if isinstance(v, enum.IntEnum):
            return v.name
        if isinstance(v, memoryview):
            return codecs.decode(v, 'latin1')
        return v
    return {
        k: _json(v) for k, v in self.__dict__.items()
        if not k.startswith('_')
    }
class InnoStruct (reader, version, codec='latin1')

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class InnoStruct(JsonStruct):
    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, codec: str = 'latin1'):
        if version.unicode:
            self._read_string = functools.partial(
                reader.read_length_prefixed_utf16, bytecount=True)
        else:
            def _read():
                data = reader.read_length_prefixed()
                try:
                    return codecs.decode(data, codec)
                except (LookupError, UnicodeDecodeError):
                    # TODO
                    return codecs.decode(data, 'latin1')
            self._read_string = _read

Ancestors

Subclasses

class CheckSumType (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class CheckSumType(enum.IntEnum):
    Missing = 0 # noqa
    Adler32 = 1 # noqa
    CRC32   = 2 # noqa
    MD5     = 3 # noqa
    SHA1    = 4 # noqa
    SHA256  = 5 # noqa

    def strong(self):
        return self.value >= 3

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var Missing
var Adler32
var CRC32
var MD5
var SHA1
var SHA256

Methods

def strong(self)
Expand source code Browse git
def strong(self):
    return self.value >= 3
class Flags (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class Flags(enum.IntFlag):
    Empty                       = 0           # noqa
    DisableStartupPrompt        = enum.auto() # noqa
    Uninstallable               = enum.auto() # noqa
    CreateAppDir                = enum.auto() # noqa
    DisableDirPage              = enum.auto() # noqa
    DisableDirExistsWarning     = enum.auto() # noqa
    DisableProgramGroupPage     = enum.auto() # noqa
    AllowNoIcons                = enum.auto() # noqa
    AlwaysRestart               = enum.auto() # noqa
    BackSolid                   = enum.auto() # noqa
    AlwaysUsePersonalGroup      = enum.auto() # noqa
    WindowVisible               = enum.auto() # noqa
    WindowShowCaption           = enum.auto() # noqa
    WindowResizable             = enum.auto() # noqa
    WindowStartMaximized        = enum.auto() # noqa
    EnableDirDoesntExistWarning = enum.auto() # noqa
    DisableAppendDir            = enum.auto() # noqa
    Password                    = enum.auto() # noqa
    AllowRootDirectory          = enum.auto() # noqa
    DisableFinishedPage         = enum.auto() # noqa
    AdminPrivilegesRequired     = enum.auto() # noqa
    AlwaysCreateUninstallIcon   = enum.auto() # noqa
    OverwriteUninstRegEntries   = enum.auto() # noqa
    ChangesAssociations         = enum.auto() # noqa
    CreateUninstallRegKey       = enum.auto() # noqa
    UsePreviousAppDir           = enum.auto() # noqa
    BackColorHorizontal         = enum.auto() # noqa
    UsePreviousGroup            = enum.auto() # noqa
    UpdateUninstallLogAppName   = enum.auto() # noqa
    UsePreviousSetupType        = enum.auto() # noqa
    DisableReadyMemo            = enum.auto() # noqa
    AlwaysShowComponentsList    = enum.auto() # noqa
    FlatComponentsList          = enum.auto() # noqa
    ShowComponentSizes          = enum.auto() # noqa
    UsePreviousTasks            = enum.auto() # noqa
    DisableReadyPage            = enum.auto() # noqa
    AlwaysShowDirOnReadyPage    = enum.auto() # noqa
    AlwaysShowGroupOnReadyPage  = enum.auto() # noqa
    BzipUsed                    = enum.auto() # noqa
    AllowUNCPath                = enum.auto() # noqa
    UserInfoPage                = enum.auto() # noqa
    UsePreviousUserInfo         = enum.auto() # noqa
    UninstallRestartComputer    = enum.auto() # noqa
    RestartIfNeededByRun        = enum.auto() # noqa
    ShowTasksTreeLines          = enum.auto() # noqa
    ShowLanguageDialog          = enum.auto() # noqa
    DetectLanguageUsingLocale   = enum.auto() # noqa
    AllowCancelDuringInstall    = enum.auto() # noqa
    WizardImageStretch          = enum.auto() # noqa
    AppendDefaultDirName        = enum.auto() # noqa
    AppendDefaultGroupName      = enum.auto() # noqa
    EncryptionUsed              = enum.auto() # noqa
    ChangesEnvironment          = enum.auto() # noqa
    ShowUndisplayableLanguages  = enum.auto() # noqa
    SetupLogging                = enum.auto() # noqa
    SignedUninstaller           = enum.auto() # noqa
    UsePreviousLanguage         = enum.auto() # noqa
    DisableWelcomePage          = enum.auto() # noqa
    CloseApplications           = enum.auto() # noqa
    RestartApplications         = enum.auto() # noqa
    AllowNetworkDrive           = enum.auto() # noqa
    ForceCloseApplications      = enum.auto() # noqa
    AppNameHasConsts            = enum.auto() # noqa
    UsePreviousPrivileges       = enum.auto() # noqa
    WizardResizable             = enum.auto() # noqa
    UninstallLogging            = enum.auto() # noqa

Ancestors

  • enum.IntFlag
  • builtins.int
  • enum.Flag
  • enum.Enum

Class variables

var Empty
var DisableStartupPrompt
var Uninstallable
var CreateAppDir
var DisableDirPage
var DisableDirExistsWarning
var DisableProgramGroupPage
var AllowNoIcons
var AlwaysRestart
var BackSolid
var AlwaysUsePersonalGroup
var WindowVisible
var WindowShowCaption
var WindowResizable
var WindowStartMaximized
var EnableDirDoesntExistWarning
var DisableAppendDir
var Password
var AllowRootDirectory
var DisableFinishedPage
var AdminPrivilegesRequired
var AlwaysCreateUninstallIcon
var OverwriteUninstRegEntries
var ChangesAssociations
var CreateUninstallRegKey
var UsePreviousAppDir
var BackColorHorizontal
var UsePreviousGroup
var UpdateUninstallLogAppName
var UsePreviousSetupType
var DisableReadyMemo
var AlwaysShowComponentsList
var FlatComponentsList
var ShowComponentSizes
var UsePreviousTasks
var DisableReadyPage
var AlwaysShowDirOnReadyPage
var AlwaysShowGroupOnReadyPage
var BzipUsed
var AllowUNCPath
var UserInfoPage
var UsePreviousUserInfo
var UninstallRestartComputer
var RestartIfNeededByRun
var ShowTasksTreeLines
var ShowLanguageDialog
var DetectLanguageUsingLocale
var AllowCancelDuringInstall
var WizardImageStretch
var AppendDefaultDirName
var AppendDefaultGroupName
var EncryptionUsed
var ChangesEnvironment
var ShowUndisplayableLanguages
var SetupLogging
var SignedUninstaller
var UsePreviousLanguage
var DisableWelcomePage
var CloseApplications
var RestartApplications
var AllowNetworkDrive
var ForceCloseApplications
var AppNameHasConsts
var UsePreviousPrivileges
var WizardResizable
var UninstallLogging
class AutoBool (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class AutoBool(enum.IntEnum):
    Auto = 0
    No = 1
    Yes = 2

    @classmethod
    def From(cls, b: bool):
        return AutoBool.Yes if b else AutoBool.No

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var Auto
var No
var Yes

Static methods

def From(b)
Expand source code Browse git
@classmethod
def From(cls, b: bool):
    return AutoBool.Yes if b else AutoBool.No
class WizardStyle (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class WizardStyle(enum.IntEnum):
    Classic = 0
    Modern = 1

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var Classic
var Modern
class StoredAlphaFormat (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class StoredAlphaFormat(enum.IntEnum):
    AlphaIgnored = 0
    AlphaDefined = 1
    AlphaPremultiplied = 2

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var AlphaIgnored
var AlphaDefined
var AlphaPremultiplied
class UninstallLogMode (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class UninstallLogMode(enum.IntEnum):
    Append = 0
    New = 1
    Overwrite = 2

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var Append
var New
var Overwrite
class SetupStyle (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class SetupStyle(enum.IntEnum):
    Classic = 0
    Modern = 1

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var Classic
var Modern
class PrivilegesRequired (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class PrivilegesRequired(enum.IntEnum):
    Nothing = 0
    PowerUser = 1
    Admin = 2
    Lowest = 3

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var Nothing
var PowerUser
var Admin
var Lowest
class PrivilegesRequiredOverrideAllowed (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class PrivilegesRequiredOverrideAllowed(enum.IntEnum):
    CommandLine = 0
    Dialog = 1

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var CommandLine
var Dialog
class LanguageDetection (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class LanguageDetection(enum.IntEnum):
    UI = 0
    Locale = 1
    Nothing = 2

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var UI
var Locale
var Nothing
class CompressionMethod (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class CompressionMethod(enum.IntEnum):
    Store = 0
    ZLib = 1
    Bzip2 = 2
    Lzma1 = 3
    Lzma2 = 4

    def legacy_check(self, max: int, ver: str):
        if self.value > max:
            raise ValueError(F'Compression method {self.value} cannot be represented before version {ver}.')
        return self

    def legacy_conversion_pre_4_2_5(self):
        return self.legacy_check(2, '4.2.5').__class__(self.value + 1)

    def legacy_conversion_pre_4_2_6(self):
        if self == CompressionMethod.Store:
            return self
        return self.legacy_check(2, '4.2.6').__class__(self.value + 1)

    def legacy_conversion_pre_5_3_9(self):
        return self.legacy_check(3, '5.3.9')

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var Store
var ZLib
var Bzip2
var Lzma1
var Lzma2

Methods

def legacy_check(self, max, ver)
Expand source code Browse git
def legacy_check(self, max: int, ver: str):
    if self.value > max:
        raise ValueError(F'Compression method {self.value} cannot be represented before version {ver}.')
    return self
def legacy_conversion_pre_4_2_5(self)
Expand source code Browse git
def legacy_conversion_pre_4_2_5(self):
    return self.legacy_check(2, '4.2.5').__class__(self.value + 1)
def legacy_conversion_pre_4_2_6(self)
Expand source code Browse git
def legacy_conversion_pre_4_2_6(self):
    if self == CompressionMethod.Store:
        return self
    return self.legacy_check(2, '4.2.6').__class__(self.value + 1)
def legacy_conversion_pre_5_3_9(self)
Expand source code Browse git
def legacy_conversion_pre_5_3_9(self):
    return self.legacy_check(3, '5.3.9')
class Architecture (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class Architecture(enum.IntFlag):
    Unknown = 0b00000 # noqa
    X86     = 0b00001 # noqa
    AMD64   = 0b00010 # noqa
    IA64    = 0b00100 # noqa
    ARM64   = 0b01000 # noqa
    All     = 0b01111 # noqa

Ancestors

  • enum.IntFlag
  • builtins.int
  • enum.Flag
  • enum.Enum

Class variables

var Unknown
var X86
var AMD64
var IA64
var ARM64
var All
class PasswordType (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class PasswordType(enum.IntEnum):
    CRC32     = 0           # noqa
    Nothing   = 0           # noqa
    MD5       = enum.auto() # noqa
    SHA1      = enum.auto() # noqa
    XChaCha20 = enum.auto() # noqa

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var CRC32
var Nothing
var MD5
var SHA1
var XChaCha20
class SetupTypeEnum (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class SetupTypeEnum(enum.IntEnum):
    User = 0
    DefaultFull = 1
    DefaultCompact = 2
    DefaultCustom = 3

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var User
var DefaultFull
var DefaultCompact
var DefaultCustom
class SetupFlags (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class SetupFlags(enum.IntFlag):
    Fixed                     = 0b00001 # noqa
    Restart                   = 0b00010 # noqa
    DisableNoUninstallWarning = 0b00100 # noqa
    Exclusive                 = 0b01000 # noqa
    DontInheritCheck          = 0b10000 # noqa

Ancestors

  • enum.IntFlag
  • builtins.int
  • enum.Flag
  • enum.Enum

Class variables

var Fixed
var Restart
var DisableNoUninstallWarning
var Exclusive
var DontInheritCheck
class StreamCompressionMethod (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class StreamCompressionMethod(enum.IntEnum):
    Store = 0
    ZLib = 1
    LZMA1 = 2

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var Store
var ZLib
var LZMA1
class StreamHeader (reader, name, version)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class StreamHeader(InnoStruct):
    def __init__(self, reader: StructReader[memoryview], name: str, version: InnoVersion):
        super().__init__(reader, version)
        self.Name = name
        self.HeaderCrc = reader.u32()
        self.CompressedSize = size = reader.u32()
        if version >= (4, 0, 9):
            self.StoredSize = self.CompressedSize
            if not reader.u8():
                self.Compression = StreamCompressionMethod.Store
            elif version >= (4, 1, 6):
                self.Compression = StreamCompressionMethod.LZMA1
            else:
                self.Compression = StreamCompressionMethod.ZLib
        else:
            self.UncompresedSize = reader.u32()
            if size == 0xFFFFFFFF:
                self.StoredSize = self.UncompresedSize
                self.Compression = StreamCompressionMethod.Store
            else:
                self.StoredSize = size
                self.Compression = StreamCompressionMethod.ZLib
            # Add the size of a CRC32 checksum for each 4KiB subblock
            block_count, _r = divmod(self.StoredSize, 4096)
            block_count += int(bool(_r))
            self.StoredSize += 4 * block_count

Ancestors

class CrcCompressedBlock (reader, size)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class CrcCompressedBlock(JsonStruct):
    def __init__(self, reader: StructReader[memoryview], size: int):
        self.BlockCrc = reader.u32()
        self.BlockData = reader.read(size)

Ancestors

class TSetupOffsets (reader)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class TSetupOffsets(Struct):
    def __init__(self, reader: StructReader[memoryview]):
        self.id = reader.read(12)
        self.version = reader.u32()
        self.total_size = reader.u32()
        self.exe_offset = reader.u32()
        self.exe_uncompressed_size = reader.u32()
        self.exe_crc = reader.u32()
        self.setup0_offset = reader.u32()
        self.setup1_offset = reader.u32()
        self.offsets_crc = reader.u32()
        self.base = min(
            self.exe_offset,
            self.setup0_offset,
            self.setup1_offset,
        )
        self.setup0 = self.setup0_offset - self.base
        self.setup1 = self.setup1_offset - self.base

Ancestors

class InnoFile (reader, version, meta, path='', dupe=False, setup=None, compression_method=None, password_hash=b'', password_salt=b'', password_type=PasswordType.CRC32)

InnoFile(reader: 'StructReader[ByteStr]', version: 'InnoVersion', meta: 'SetupDataEntry', path: 'str' = '', dupe: 'bool' = False, setup: 'Optional[SetupFile]' = None, compression_method: 'Optional[CompressionMethod]' = None, password_hash: 'bytes' = b'', password_salt: 'bytes' = b'', password_type: 'PasswordType' = )

Expand source code Browse git
class InnoFile:
    reader: StructReader[ByteStr]
    version: InnoVersion
    meta: SetupDataEntry
    path: str = ""
    dupe: bool = False
    setup: Optional[SetupFile] = None
    compression_method: Optional[CompressionMethod] = None
    password_hash: bytes = B''
    password_salt: bytes = B''
    password_type: PasswordType = PasswordType.Nothing

    @property
    def tags(self):
        if s := self.setup:
            return s.Flags
        else:
            return SetupFileFlags.Empty

    @property
    def unicode(self):
        return self.version.unicode

    @property
    def compression(self):
        if self.meta.Flags & SetupDataEntryFlags.ChunkCompressed:
            return self.compression_method
        return CompressionMethod.Store

    @property
    def offset(self):
        return self.meta.Offset

    @property
    def size(self):
        return self.meta.FileSize

    @property
    def date(self):
        return self.meta.FileTime

    @property
    def chunk_offset(self):
        return self.meta.ChunkOffset

    @property
    def chunk_length(self):
        return self.meta.ChunkSize

    @property
    def checksum(self):
        return self.meta.Checksum

    @property
    def checksum_type(self):
        return self.meta.ChecksumType

    @property
    def encrypted(self):
        return bool(self.meta.Flags & SetupDataEntryFlags.ChunkEncrypted)

    @property
    def filtered(self):
        return bool(self.meta.Flags & SetupDataEntryFlags.CallInstructionOptimized)

    def check(self, data: ByteStr):
        t = self.checksum_type
        if t == CheckSumType.Missing:
            return None
        if t == CheckSumType.Adler32:
            return zlib.adler32(data) & 0xFFFFFFFF
        if t == CheckSumType.CRC32:
            return zlib.crc32(data) & 0xFFFFFFFF
        if t == CheckSumType.MD5:
            return md5(data).digest()
        if t == CheckSumType.SHA1:
            return sha1(data).digest()
        if t == CheckSumType.SHA256:
            return sha256(data).digest()
        raise ValueError(F'Unknown checksum type: {t!r}')

Class variables

var reader
var version
var meta
var path
var dupe
var setup
var compression_method
var password_hash
var password_salt
var password_type

Instance variables

var tags
Expand source code Browse git
@property
def tags(self):
    if s := self.setup:
        return s.Flags
    else:
        return SetupFileFlags.Empty
var unicode
Expand source code Browse git
@property
def unicode(self):
    return self.version.unicode
var compression
Expand source code Browse git
@property
def compression(self):
    if self.meta.Flags & SetupDataEntryFlags.ChunkCompressed:
        return self.compression_method
    return CompressionMethod.Store
var offset
Expand source code Browse git
@property
def offset(self):
    return self.meta.Offset
var size
Expand source code Browse git
@property
def size(self):
    return self.meta.FileSize
var date
Expand source code Browse git
@property
def date(self):
    return self.meta.FileTime
var chunk_offset
Expand source code Browse git
@property
def chunk_offset(self):
    return self.meta.ChunkOffset
var chunk_length
Expand source code Browse git
@property
def chunk_length(self):
    return self.meta.ChunkSize
var checksum
Expand source code Browse git
@property
def checksum(self):
    return self.meta.Checksum
var checksum_type
Expand source code Browse git
@property
def checksum_type(self):
    return self.meta.ChecksumType
var encrypted
Expand source code Browse git
@property
def encrypted(self):
    return bool(self.meta.Flags & SetupDataEntryFlags.ChunkEncrypted)
var filtered
Expand source code Browse git
@property
def filtered(self):
    return bool(self.meta.Flags & SetupDataEntryFlags.CallInstructionOptimized)

Methods

def check(self, data)
Expand source code Browse git
def check(self, data: ByteStr):
    t = self.checksum_type
    if t == CheckSumType.Missing:
        return None
    if t == CheckSumType.Adler32:
        return zlib.adler32(data) & 0xFFFFFFFF
    if t == CheckSumType.CRC32:
        return zlib.crc32(data) & 0xFFFFFFFF
    if t == CheckSumType.MD5:
        return md5(data).digest()
    if t == CheckSumType.SHA1:
        return sha1(data).digest()
    if t == CheckSumType.SHA256:
        return sha256(data).digest()
    raise ValueError(F'Unknown checksum type: {t!r}')
class InnoStream (header, blocks=<factory>, data=None)

InnoStream(header: 'StreamHeader', blocks: 'list[CrcCompressedBlock]' = , data: 'Optional[bytearray]' = None)

Expand source code Browse git
class InnoStream:
    header: StreamHeader
    blocks: list[CrcCompressedBlock] = dataclasses.field(default_factory=list)
    data: Optional[bytearray] = None

    @property
    def compression(self):
        return self.header.Compression

    @property
    def name(self):
        return self.header.Name

Class variables

var header
var blocks
var data

Instance variables

var compression
Expand source code Browse git
@property
def compression(self):
    return self.header.Compression
var name
Expand source code Browse git
@property
def name(self):
    return self.header.Name
class InstallMode (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class InstallMode(enum.IntEnum):
    Normal     = enum.auto() # noqa
    Silent     = enum.auto() # noqa
    VerySilent = enum.auto() # noqa

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var Normal
var Silent
var VerySilent
class SetupHeader (reader, version)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class SetupHeader(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion):
        super().__init__(reader, version)

        def read_string():
            return reader.read_length_prefixed()

        if version < (1, 3, 0):
            # skip uncompressed size
            reader.u32()

        if True:
            self.AppName = read_string()
            self.AppVersionedName = read_string()
        if version >= (1, 3, 0):
            self.AppId = read_string()
        if True:
            self.AppCopyright = read_string()
        if version >= (1, 3, 0):
            self.AppPublisher = read_string()
            self.AppPublisherUrl = read_string()
        if version >= (5, 1, 13):
            self.AppSupportPhone = read_string()
        if version >= (1, 3, 0):
            self.AppSupportUrl = read_string()
            self.AppUpdatesUrl = read_string()
            self.AppVersion = read_string()
        if True:
            self.DefaultDirName = read_string()
            self.DefaultGroupName = read_string()
        if version < (3, 0, 0):
            self.UninstallIconName = reader.read_length_prefixed(encoding='cp1252')
        if True:
            self.BaseFilename = read_string()
        if (1, 3, 0) <= version < (5, 2, 5):
            self._license = reader.read_length_prefixed_ascii()
            self.InfoHead = reader.read_length_prefixed_ascii()
            self.InfoTail = reader.read_length_prefixed_ascii()
        if version >= (1, 3, 3):
            self.UninstallFilesDir = read_string()
        if version >= (1, 3, 6):
            self.UninstallName = read_string()
            self.UninstallIcon = read_string()
        if version >= (1, 3, 14):
            self.AppMutex = read_string()
        if version >= (3, 0, 0):
            self.DefaultUsername = read_string()
            self.DefaultOrganisation = read_string()
        if version >= (4, 0, 0) or version.isx and version >= (3, 0, 6, 1):
            self.DefaultSerial = read_string()
        if (4, 0, 0) <= version < (5, 2, 5) or version.isx and version >= (1, 3, 24):
            self.CompiledCode = read_string()
        if version >= (4, 2, 4):
            self.AppReadmeFile = read_string()
            self.AppContact = read_string()
            self.AppComment = read_string()
            self.AppModifyPath = read_string()
        if version >= (5, 3, 8):
            self.CreateUninstallRegistryKey = read_string()
        if version >= (5, 3, 10):
            self.Uninstallable = read_string()
        if version >= (5, 5, 0):
            self.CloseApplicationsFilter = read_string()
        if version >= (5, 5, 6):
            self.SetupMutex = read_string()
        if version >= (5, 6, 1):
            self.ChangesEnvironment = read_string()
            self.ChangesAssociations = read_string()
        if version >= (6, 3, 0):
            self.ArchitecturesAllowed32 = read_string()
            self.ArchitecturesAllowed64 = read_string()
        if version >= (5, 2, 5):
            self._license = reader.read_length_prefixed_ascii()
            self.InfoHead = reader.read_length_prefixed_ascii()
            self.InfoTail = reader.read_length_prefixed_ascii()
        if version >= (5, 2, 1) and version < (5, 3, 10):
            self.UninstallerSignature = read_string()
        if version >= (5, 2, 5):
            self.CompiledCode = read_string()

        if self.CompiledCode and self.CompiledCode[:4] != B'IFPS':
            raise ValueError('Invalid signature in compiled code.')

        if version >= (2, 0, 6) and version.ascii:
            self.Charset = reader.read_integer(0x100)
        else:
            self.Charset = [False] * 0x100

        if version >= (4, 0, 0):
            self.LanguageCount = reader.u32()
        elif version >= (2, 0, 1):
            self.LanguageCount = 1
        else:
            self.LanguageCount = 0

        if version >= (4, 2, 1):
            self.MessageCount = reader.u32()
        else:
            self.MessageCount = 0

        if version >= (4, 1, 0):
            self.PermissionCount = reader.u32()
        else:
            self.PermissionCount = 0

        if version >= (2, 0, 0) or version.isx:
            self.TypeCount = reader.u32()
            self.ComponentCount = reader.u32()
        else:
            self.TypeCount = 0
            self.ComponentCount = 0

        if version >= (2, 0, 0) or version.isx and version >= (1, 3, 17):
            self.TaskCount = reader.u32()
        else:
            self.TaskCount = 0

        self.DirectoryCount = reader.u32()
        self.FileCount = reader.u32()
        self.DataEntryCount = reader.u32()
        self.IconCount = reader.u32()
        self.IniEntryCount = reader.u32()
        self.RegistryEntryCount = reader.u32()
        self.DeleteEntryCount = reader.u32()
        self.UninstallDeleteEntryCount = reader.u32()
        self.RunEntryCount = reader.u32()
        self.UninstallRunEntryCount = reader.u32()

        if version < (1, 3, 0):
            _license_len = reader.u32()
            _infhead_len = reader.u32()
            _inftail_len = reader.u32()

        self.MinimumWindowsVersion = WindowsVersion(reader, version)
        self.MaximumWindowsVersion = WindowsVersion(reader, version)

        self.BackColor1 = reader.u32() if (0, 0, 0) <= version < (6, 4, 0, 1) else 0
        self.BackColor2 = reader.u32() if (1, 3, 3) <= version < (6, 4, 0, 1) else 0

        if version < (5, 5, 7):
            self.ImageBackColor = reader.u32()
        if (2, 0, 0) <= version < (5, 0, 4) or version.isx:
            self.SmallImageBackColor = reader.u32()
        if version >= (6, 0, 0):
            self.WizardStyle = WizardStyle(reader.u8())
            self.WizardResizePercentX = reader.u32()
            self.WizardResizePercentY = reader.u32()
        else:
            self.WizardStyle = WizardStyle.Classic
            self.WizardResizePercentX = 0
            self.WizardResizePercentY = 0

        if version >= (5, 5, 7):
            self.StoredAlphaFormat = StoredAlphaFormat(reader.u8())
        else:
            self.StoredAlphaFormat = StoredAlphaFormat.AlphaIgnored

        if version >= (6, 4, 0):
            self.PasswordType = PasswordType.XChaCha20
            self.PasswordHash = reader.read(4)
        elif version >= (5, 3, 9):
            self.PasswordType = PasswordType.SHA1
            self.PasswordHash = reader.read(20)
        elif version >= (4, 2, 0):
            self.PasswordType = PasswordType.MD5
            self.PasswordHash = reader.read(16)
        else:
            self.PasswordType = PasswordType.CRC32
            self.PasswordHash = reader.u32()

        if version >= (6, 4, 0):
            self.PasswordSalt = reader.read(44)
        elif version >= (4, 2, 2):
            self.PasswordSalt = reader.read(8)
        else:
            self.PasswordSalt = None

        if version >= (4, 0, 0):
            self.ExtraDiskSpace = reader.i64()
            self.SlicesPerDisk = reader.u32()
        else:
            self.ExtraDiskSpace = reader.u32()
            self.SlicesPerDisk = 1

        if (2, 0, 0) <= version < (5, 0, 0):
            self.InstallMode = _enum(InstallMode, reader.u8(), InstallMode.Normal)
        else:
            self.InstallMode = InstallMode.Normal
        if version >= (1, 3, 0):
            self.UninstallLogMode = UninstallLogMode(reader.u8())
        else:
            self.UninstallLogMode = UninstallLogMode.New

        if version >= (5, 0, 0):
            self.SetupStyle = SetupStyle.Modern
        elif (2, 0, 0) <= version or version.isx and version >= (1, 3, 13):
            self.SetupStyle = SetupStyle(reader.u8())
        else:
            self.SetupStyle = SetupStyle.Classic

        if version >= (1, 3, 6):
            self.DirExistsWarning = AutoBool(reader.u8())
        else:
            self.DirExistsWarning = AutoBool.Auto
        if version.isx and (2, 0, 10) <= version < (3, 0, 0):
            self.CodeLineOffset = reader.u32()

        self.Flags = Flags.Empty

        if (3, 0, 0) <= version < (3, 0, 3):
            val = AutoBool(reader.u8())
            if val == AutoBool.Auto:
                self.Flags |= Flags.RestartIfNeededByRun
            elif val == AutoBool.Yes:
                self.Flags |= Flags.AlwaysRestart

        if version >= (3, 0, 4) or version.isx and version >= (3, 0, 3):
            self.PrivilegesRequired = PrivilegesRequired(reader.u8())
        if version >= (5, 7, 0):
            self.PrivilegesRequiredOverrideAllowed = PrivilegesRequiredOverrideAllowed(reader.u8())
        if version >= (4, 0, 10):
            self.ShowLanguageDialog = AutoBool(reader.u8())
            self.LanguageDetection = LanguageDetection(reader.u8())

        if version >= (4, 1, 5):
            method = CompressionMethod(reader.u8())
            if version < (4, 2, 5):
                method = method.legacy_conversion_pre_4_2_5()
            elif version < (4, 2, 6):
                method = method.legacy_conversion_pre_4_2_5()
            elif version < (5, 3, 9):
                method = method.legacy_conversion_pre_5_3_9()
            self.CompressionMethod = method

        if version >= (6, 3, 0):
            self.ArchitecturesAllowed = Architecture.Unknown
            self.ArchitecturesInstalled64 = Architecture.Unknown
        elif version >= (5, 1, 0):
            self.ArchitecturesAllowed = Architecture(reader.u8())
            self.ArchitecturesInstalled64 = Architecture(reader.u8())
        else:
            self.ArchitecturesAllowed = Architecture.All
            self.ArchitecturesInstalled64 = Architecture.All

        if (5, 2, 1) <= version < (5, 3, 10):
            self.UninstallerOriginalSize = reader.u32()
            self.UninstallheaderCrc = reader.u32()
        if version >= (5, 3, 3):
            self.DisableDirPage = AutoBool(reader.u8())
            self.DisableProgramGroupPage = AutoBool(reader.u8())
        if version >= (5, 5, 0):
            self.UninstallDisplaySize = reader.u64()
        elif version >= (5, 3, 6):
            self.UninstallDisplaySize = reader.u32()
        else:
            self.UninstallDisplaySize = 0

        flags = []
        flags.append(Flags.DisableStartupPrompt)
        if version < (5, 3, 10):
            flags.append(Flags.Uninstallable)
        flags.append(Flags.CreateAppDir)
        if version < (5, 3, 3):
            flags.append(Flags.DisableDirPage)
        if version < (1, 3, 6):
            flags.append(Flags.DisableDirExistsWarning)
        if version < (5, 3, 3):
            flags.append(Flags.DisableProgramGroupPage)
        flags.append(Flags.AllowNoIcons)
        if version < (3, 0, 0) or version >= (3, 0, 3):
            flags.append(Flags.AlwaysRestart)
        if version < (1, 3, 3):
            flags.append(Flags.BackSolid)
        flags.append(Flags.AlwaysUsePersonalGroup)
        flags.append(Flags.WindowVisible)
        flags.append(Flags.WindowShowCaption)
        flags.append(Flags.WindowResizable)
        flags.append(Flags.WindowStartMaximized)
        flags.append(Flags.EnableDirDoesntExistWarning)
        if version < (4, 1, 2):
            flags.append(Flags.DisableAppendDir)
        flags.append(Flags.Password)
        flags.append(Flags.AllowRootDirectory)
        flags.append(Flags.DisableFinishedPage)

        if version.bits > 16:
            if version < (3, 0, 4):
                flags.append(Flags.AdminPrivilegesRequired)
            if version < (3, 0, 0):
                flags.append(Flags.AlwaysCreateUninstallIcon)
            if version < (1, 3, 6):
                flags.append(Flags.OverwriteUninstRegEntries)
            if version < (5, 6, 1):
                flags.append(Flags.ChangesAssociations)

        if version < (5, 3, 8):
            flags.append(Flags.CreateUninstallRegKey)

        flags.append(Flags.UsePreviousAppDir)
        flags.append(Flags.BackColorHorizontal)
        flags.append(Flags.UsePreviousGroup)
        flags.append(Flags.UpdateUninstallLogAppName)
        flags.append(Flags.UsePreviousSetupType)
        flags.append(Flags.DisableReadyMemo)
        flags.append(Flags.AlwaysShowComponentsList)
        flags.append(Flags.FlatComponentsList)
        flags.append(Flags.ShowComponentSizes)
        flags.append(Flags.UsePreviousTasks)
        flags.append(Flags.DisableReadyPage)
        flags.append(Flags.AlwaysShowDirOnReadyPage)
        flags.append(Flags.AlwaysShowGroupOnReadyPage)
        if version < (4, 1, 5):
            flags.append(Flags.BzipUsed)
        flags.append(Flags.AllowUNCPath)
        flags.append(Flags.UserInfoPage)
        flags.append(Flags.UsePreviousUserInfo)
        flags.append(Flags.UninstallRestartComputer)
        flags.append(Flags.RestartIfNeededByRun)
        flags.append(Flags.ShowTasksTreeLines)
        if version < (4, 0, 10):
            flags.append(Flags.ShowLanguageDialog)
        if version >= (4, 0, 1) and version < (4, 0, 10):
            flags.append(Flags.DetectLanguageUsingLocale)
        if version >= (4, 0, 9):
            flags.append(Flags.AllowCancelDuringInstall)
        if version >= (4, 1, 3):
            flags.append(Flags.WizardImageStretch)
        if version >= (4, 1, 8):
            flags.append(Flags.AppendDefaultDirName)
            flags.append(Flags.AppendDefaultGroupName)
        if version >= (4, 2, 2):
            flags.append(Flags.EncryptionUsed)
        if version >= (5, 0, 4) and version < (5, 6, 1):
            flags.append(Flags.ChangesEnvironment)
        if version >= (5, 1, 7) and version.ascii:
            flags.append(Flags.ShowUndisplayableLanguages)
        if version >= (5, 1, 13):
            flags.append(Flags.SetupLogging)
        if version >= (5, 2, 1):
            flags.append(Flags.SignedUninstaller)
        if version >= (5, 3, 8):
            flags.append(Flags.UsePreviousLanguage)
        if version >= (5, 3, 9):
            flags.append(Flags.DisableWelcomePage)
        if version >= (5, 5, 0):
            flags.append(Flags.CloseApplications)
            flags.append(Flags.RestartApplications)
            flags.append(Flags.AllowNetworkDrive)
        if version >= (5, 5, 7):
            flags.append(Flags.ForceCloseApplications)
        if version >= (6, 0, 0):
            flags.append(Flags.AppNameHasConsts)
            flags.append(Flags.UsePreviousPrivileges)
            flags.append(Flags.WizardResizable)
        if version >= (6, 3, 0):
            flags.append(Flags.UninstallLogging)

        flagsize, _r = divmod(len(flags), 8)
        flagsize += int(bool(_r))
        bytecheck = bytes(reader.peek(flagsize + 1 + 4 + 1))

        if bytecheck[0] == 0:
            if bytecheck[~0] != 0 or bytecheck[~3:~0] == B'\0\0\0':
                reader.u8()

        for flag in flags:
            if reader.read_bit():
                self.Flags |= flag

        if version < (3, 0, 4):
            self.PrivilegesRequired = PrivilegesRequired.Admin if (
                self.Flags & Flags.AdminPrivilegesRequired
            ) else PrivilegesRequired.Nothing

        if version < (4, 0, 10):
            self.ShowLanguageDialog = AutoBool.From(
                self.Flags & Flags.ShowLanguageDialog)
            self.LanguageDetection = LanguageDetection.Locale if (
                self.Flags & Flags.DetectLanguageUsingLocale
            ) else LanguageDetection.UI

        if version < (4, 1, 5):
            self.CompressionMethod = CompressionMethod.Bzip2 if (
                self.Flags & Flags.BzipUsed
            ) else CompressionMethod.ZLib

        if version < (5, 3, 3):
            self.DisableDirPage = AutoBool.From(self.Flags & Flags.DisableDirPage)
            self.DisableProgramGroupPage = AutoBool.From(self.Flags & Flags.DisableProgramGroupPage)

        if version < (1, 3, 0):
            def _read_ascii(n: int):
                return codecs.decode(reader.read(_license_len), 'cp1252')
            self._license = _read_ascii(_license_len)
            self.InfoHead = _read_ascii(_infhead_len)
            self.InfoTail = _read_ascii(_inftail_len)

        reader.byte_align()

        if flagsize == 3:
            reader.u8()

    def get_license(self):
        return self._license

    def recode_strings(self, codec: str):
        for coded_string_attribute in [
            'AppComment',
            'AppContact',
            'AppCopyright',
            'AppId',
            'AppModifyPath',
            'AppMutex',
            'AppName',
            'AppPublisher',
            'AppPublisherUrl',
            'AppReadmeFile',
            'AppSupportPhone',
            'AppSupportUrl',
            'AppUpdatesUrl',
            'AppVersion',
            'AppVersionedName',
            'BaseFilename',
            'ChangesAssociations',
            'ChangesEnvironment',
            'CloseApplicationsFilter',
            'CreateUninstallRegistryKey',
            'DefaultDirName',
            'DefaultGroupName',
            'DefaultOrganisation',
            'DefaultSerial',
            'DefaultUsername',
            'SetupMutex',
            'Uninstallable',
            'UninstallFilesDir',
            'UninstallIcon',
            'UninstallName',
        ]:
            try:
                value: bytes = getattr(self, coded_string_attribute)
            except AttributeError:
                continue
            if not isinstance(value, (bytes, bytearray, memoryview)):
                raise RuntimeError(F'Attempting to decode {coded_string_attribute} which was already decoded.')
            setattr(self, coded_string_attribute, codecs.decode(value, codec))

Ancestors

Methods

def get_license(self)
Expand source code Browse git
def get_license(self):
    return self._license
def recode_strings(self, codec)
Expand source code Browse git
def recode_strings(self, codec: str):
    for coded_string_attribute in [
        'AppComment',
        'AppContact',
        'AppCopyright',
        'AppId',
        'AppModifyPath',
        'AppMutex',
        'AppName',
        'AppPublisher',
        'AppPublisherUrl',
        'AppReadmeFile',
        'AppSupportPhone',
        'AppSupportUrl',
        'AppUpdatesUrl',
        'AppVersion',
        'AppVersionedName',
        'BaseFilename',
        'ChangesAssociations',
        'ChangesEnvironment',
        'CloseApplicationsFilter',
        'CreateUninstallRegistryKey',
        'DefaultDirName',
        'DefaultGroupName',
        'DefaultOrganisation',
        'DefaultSerial',
        'DefaultUsername',
        'SetupMutex',
        'Uninstallable',
        'UninstallFilesDir',
        'UninstallIcon',
        'UninstallName',
    ]:
        try:
            value: bytes = getattr(self, coded_string_attribute)
        except AttributeError:
            continue
        if not isinstance(value, (bytes, bytearray, memoryview)):
            raise RuntimeError(F'Attempting to decode {coded_string_attribute} which was already decoded.')
        setattr(self, coded_string_attribute, codecs.decode(value, codec))
class Version (reader, version)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class Version(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion):
        super().__init__(reader, version)
        self.Build = reader.u16() if version >= (1, 3, 19) else 0
        self.Minor = reader.u8()
        self.Major = reader.u8()

Ancestors

class WindowsVersion (reader, version)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class WindowsVersion(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion):
        super().__init__(reader, version)
        self.WindowsVersion = Version(reader, version)
        self.NtVersion = Version(reader, version)
        (
            self.ServicePackMinor,
            self.ServicePackMajor,
        ) = reader.read_struct('BB') if version >= (1, 3, 19) else (0, 0)

Ancestors

class LanguageId (reader)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class LanguageId(JsonStruct):
    def __init__(self, reader: StructReader[memoryview]):
        self.Value = reader.i32()
        self.Name = LCID.get(self.Value, None)

Ancestors

class SetupLanguage (reader, version, _)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class SetupLanguage(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, _: TSetup):
        super().__init__(reader, version)
        read_string = self._read_string

        self.Name = read_string()
        LanguageName = reader.read_length_prefixed()

        self.DialogFont = read_string()
        self.TitleFont = read_string()
        self.WelcomeFont = read_string()
        self.CopyrightFont = read_string()
        self.Data = reader.read_length_prefixed()

        if version >= (4, 0, 1):
            self.LicenseText = reader.read_length_prefixed_ascii()
            self.InfoBefore = reader.read_length_prefixed_ascii()
            self.InfoAfter = reader.read_length_prefixed_ascii()

        self.LanguageId = LanguageId(reader)

        if version < (4, 2, 2):
            self.Codepage = DEFAULT_CODEPAGE.get(self.LanguageId.Value, 'cp1252')
        elif version.ascii:
            cp = reader.u32() or 1252
            self.Codepage = F'cp{cp}'
        else:
            if version < (5, 3, 0):
                reader.u32()
            self.Codepage = 'utf-16le'

        if version >= (4, 2, 2):
            self.LanguageName = codecs.decode(LanguageName, 'utf-16le')
        else:
            self.LanguageName = codecs.decode(LanguageName, self.Codepage)

        self.DialogFontSize = reader.u32()

        if version < (4, 1, 0):
            self.DialogFontStandardHeight = reader.u32()

        self.TitleFontSize = reader.u32()
        self.WelcomeFontSize = reader.u32()
        self.CopyrightFontSize = reader.u32()

        if version >= (5, 2, 3):
            self.RightToLeft = reader.u8()

Ancestors

class SetupMessage (reader, version, parent)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class SetupMessage(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        self.EncodedName = self._read_string()
        value = reader.read_length_prefixed()
        language = reader.i32()
        try:
            codec = parent.Languages[language].Codepage
        except IndexError:
            pass
        try:
            self.Value = codecs.decode(value, codec)
        except LookupError:
            # TODO: This is a fallback
            self.Value = codecs.decode(value, 'latin1')

Ancestors

class SetupType (reader, version, parent)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class SetupType(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        read_string = self._read_string
        self.Name = read_string()
        self.Description = read_string()
        if version >= (4, 0, 1):
            self.Languages = read_string()
        if version >= (4, 0, 0) or version.isx and version >= (1, 3, 24):
            self.Check = read_string()
        self.MinimumWindowsVersion = WindowsVersion(reader, version)
        self.MaximumWindowsVersion = WindowsVersion(reader, version)
        self.CustsomTypeCode = reader.u8()
        if version >= (4, 0, 3):
            self.SetupType = SetupTypeEnum(reader.u8())
        else:
            self.SetupType = SetupTypeEnum.User
        if version >= (4, 0, 0):
            self.Size = reader.u64()
        else:
            self.Size = reader.u32()

Ancestors

class SetupComponent (reader, version, parent)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class SetupComponent(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        read_string = self._read_string
        self.Name = read_string()
        self.Description = read_string()
        self.Types = read_string()
        if version >= (4, 0, 1):
            self.Languages = read_string()
        if version >= (4, 0, 0) or version.isx and version >= (1, 3, 24):
            self.Check = read_string()
        if version >= (4, 0, 0):
            self.ExtraDiskSpace = reader.u64()
        if version >= (4, 0, 0) or version.isx and version >= (3, 0, 3):
            self.Level = reader.u32()
        else:
            self.Level = 0
        if version >= (4, 0, 0) or version.isx and version >= (3, 0, 4):
            self.Used = bool(reader.u8())
        else:
            self.Used = True
        if True:
            self.MinimumWindowsVersion = WindowsVersion(reader, version)
            self.MaximumWindowsVersion = WindowsVersion(reader, version)
            self.Flags = SetupFlags(reader.u8())
        if version >= (4, 0, 0):
            self.Size = reader.u64()
        elif version >= (2, 0, 0) or version.isx and version >= (1, 3, 24):
            self.Size = reader.u32()

Ancestors

class SetupTaskFlags (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class SetupTaskFlags(enum.IntFlag):
    Empty            = 0           # noqa
    Exclusive        = enum.auto() # noqa
    Unchecked        = enum.auto() # noqa
    Restart          = enum.auto() # noqa
    CheckedOne       = enum.auto() # noqa
    DontInheritCheck = enum.auto() # noqa

Ancestors

  • enum.IntFlag
  • builtins.int
  • enum.Flag
  • enum.Enum

Class variables

var Empty
var Exclusive
var Unchecked
var Restart
var CheckedOne
var DontInheritCheck
class SetupTask (reader, version, parent)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class SetupTask(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        read_string = self._read_string

        self.Name = read_string()
        self.Description = read_string()
        self.GroupDescription = read_string()
        self.Components = read_string()

        if version >= (4, 0, 1):
            self.Languages = read_string()
        if version >= (4, 0, 0) or version.isx and version >= (1, 3, 24):
            self.Check = read_string()
        if version >= (4, 0, 0) or version.isx and version >= (3, 0, 3):
            self.Level = reader.u32()
        else:
            self.Level = 0
        if version >= (4, 0, 0) or version.isx and version >= (3, 0, 4):
            self.Used = bool(reader.u8())
        else:
            self.Used = True
        if True:
            self.MinimumWindowsVersion = WindowsVersion(reader, version)
            self.MaximumWindowsVersion = WindowsVersion(reader, version)

        self.Flags = SetupTaskFlags.Empty

        def flagbit(f):
            self.Flags |= f if reader.read_bit() else 0

        if True:
            flagbit(SetupTaskFlags.Exclusive)
            flagbit(SetupTaskFlags.Unchecked)
        if version >= (2, 0, 5):
            flagbit(SetupTaskFlags.Restart)
        if version >= (2, 0, 6):
            flagbit(SetupTaskFlags.CheckedOne)
        if version >= (4, 2, 3):
            flagbit(SetupTaskFlags.DontInheritCheck)

        reader.byte_align()

Ancestors

class SetupCondition (reader, version, parent)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class SetupCondition(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        read_string = self._read_string
        if version >= (2, 0, 0) or version.isx and version >= (1, 3, 8):
            self.Components = read_string()
        if version >= (2, 0, 0) or version.isx and version >= (1, 3, 17):
            self.Tasks = read_string()
        if version >= (4, 0, 1):
            self.Languages = read_string()
        if version >= (4, 0, 0) or version.isx and version >= (1, 3, 24):
            self.Check = read_string()
        else:
            self.Check = None
        if version >= (4, 1, 0):
            self.AfterInstall = read_string()
            self.BeforeInstall = read_string()

Ancestors

class SetupDirectoryFlags (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class SetupDirectoryFlags(enum.IntFlag):
    Empty                = 0           # noqa
    NeverUninstall       = enum.auto() # noqa
    DeleteAfterInstall   = enum.auto() # noqa
    AlwaysUninstall      = enum.auto() # noqa
    SetNtfsCompression   = enum.auto() # noqa
    UnsetNtfsCompression = enum.auto() # noqa

Ancestors

  • enum.IntFlag
  • builtins.int
  • enum.Flag
  • enum.Enum

Class variables

var Empty
var NeverUninstall
var DeleteAfterInstall
var AlwaysUninstall
var SetNtfsCompression
var UnsetNtfsCompression
class SetupDirectory (reader, version, parent)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class SetupDirectory(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        read_string = self._read_string

        if version < (1, 3, 0):
            self.UncompressedSize = reader.u32()
        if True:
            self.Name = read_string()
            self.Condition = SetupCondition(reader, version, parent)

        if (4, 0, 11) <= version < (4, 1, 0):
            self.Permissions = read_string()
        if version >= (2, 0, 11):
            self.Attributes = reader.u32()

        self.MinimumWindowsVersion = WindowsVersion(reader, version)
        self.MaximumWindowsVersion = WindowsVersion(reader, version)

        if version >= (4, 1, 0):
            self.Permissions = reader.u16()

        self.Flags = SetupDirectoryFlags.Empty

        def flagbit(f):
            self.Flags |= f if reader.read_bit() else 0
        flagbit(SetupDirectoryFlags.NeverUninstall)
        flagbit(SetupDirectoryFlags.DeleteAfterInstall)
        flagbit(SetupDirectoryFlags.AlwaysUninstall)
        flagbit(SetupDirectoryFlags.SetNtfsCompression)
        flagbit(SetupDirectoryFlags.UnsetNtfsCompression)
        reader.byte_align()

Ancestors

class SetupPermission (reader, version, parent)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class SetupPermission(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        self.Permission = reader.read_length_prefixed()

Ancestors

class SetupFileFlags (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class SetupFileFlags(enum.IntFlag):
    Empty                              = 0           # noqa
    ConfirmOverwrite                   = enum.auto() # noqa
    NeverUninstall                     = enum.auto() # noqa
    RestartReplace                     = enum.auto() # noqa
    DeleteAfterInstall                 = enum.auto() # noqa
    RegisterServer                     = enum.auto() # noqa
    RegisterTypeLib                    = enum.auto() # noqa
    SharedFile                         = enum.auto() # noqa
    IsReadmeFile                       = enum.auto() # noqa
    CompareTimeStamp                   = enum.auto() # noqa
    FontIsNotTrueType                  = enum.auto() # noqa
    SkipIfSourceDoesntExist            = enum.auto() # noqa
    OverwriteReadOnly                  = enum.auto() # noqa
    OverwriteSameVersion               = enum.auto() # noqa
    CustomDestName                     = enum.auto() # noqa
    OnlyIfDestFileExists               = enum.auto() # noqa
    NoRegError                         = enum.auto() # noqa
    UninsRestartDelete                 = enum.auto() # noqa
    OnlyIfDoesntExist                  = enum.auto() # noqa
    IgnoreVersion                      = enum.auto() # noqa
    PromptIfOlder                      = enum.auto() # noqa
    DontCopy                           = enum.auto() # noqa
    UninsRemoveReadOnly                = enum.auto() # noqa
    RecurseSubDirsExternal             = enum.auto() # noqa
    ReplaceSameVersionIfContentsDiffer = enum.auto() # noqa
    DontVerifyChecksum                 = enum.auto() # noqa
    UninsNoSharedFilePrompt            = enum.auto() # noqa
    CreateAllSubDirs                   = enum.auto() # noqa
    Bits32                             = enum.auto() # noqa
    Bits64                             = enum.auto() # noqa
    ExternalSizePreset                 = enum.auto() # noqa
    SetNtfsCompression                 = enum.auto() # noqa
    UnsetNtfsCompression               = enum.auto() # noqa
    GacInstall                         = enum.auto() # noqa

Ancestors

  • enum.IntFlag
  • builtins.int
  • enum.Flag
  • enum.Enum

Class variables

var Empty
var ConfirmOverwrite
var NeverUninstall
var RestartReplace
var DeleteAfterInstall
var RegisterServer
var RegisterTypeLib
var SharedFile
var IsReadmeFile
var CompareTimeStamp
var FontIsNotTrueType
var SkipIfSourceDoesntExist
var OverwriteReadOnly
var OverwriteSameVersion
var CustomDestName
var OnlyIfDestFileExists
var NoRegError
var UninsRestartDelete
var OnlyIfDoesntExist
var IgnoreVersion
var PromptIfOlder
var DontCopy
var UninsRemoveReadOnly
var RecurseSubDirsExternal
var ReplaceSameVersionIfContentsDiffer
var DontVerifyChecksum
var UninsNoSharedFilePrompt
var CreateAllSubDirs
var Bits32
var Bits64
var ExternalSizePreset
var SetNtfsCompression
var UnsetNtfsCompression
var GacInstall
class SetupFileType (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class SetupFileType(enum.IntEnum):
    UserFile = 0
    UninstExe = 1
    RegSvrExe = 2

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var UserFile
var UninstExe
var RegSvrExe
class SetupFileCopyMode (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class SetupFileCopyMode(enum.IntEnum):
        Normal                  = enum.auto() # noqa
        IfDoesntExist           = enum.auto() # noqa
        AlwaysOverwrite         = enum.auto() # noqa
        AlwaysSkipIfSameOrOlder = enum.auto() # noqa

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var Normal
var IfDoesntExist
var AlwaysOverwrite
var AlwaysSkipIfSameOrOlder
class SetupFile (reader, version, parent)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class SetupFile(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion, parent: TSetup):
        super().__init__(reader, version, parent.Codec)
        read_string = self._read_string

        if version < (1, 3, 0):
            self.UncompressedSize = reader.u32()
        if True:
            self.Source = read_string()
            self.Destination = read_string()
            self.InstallFontName = read_string()
        if version >= (5, 2, 5):
            self.StrongAssemblyName = read_string()

        self.Condition = SetupCondition(reader, version, parent)

        self.MinimumWindowsVersion = WindowsVersion(reader, version)
        self.MaximumWindowsVersion = WindowsVersion(reader, version)

        self.Location = reader.u32()
        self.Attributes = reader.u32()
        self.ExternalSize = reader.u64() if version >= (4, 0, 0) else reader.u32()

        self.Flags = SetupFileFlags.Empty

        if version < (3, 0, 5):
            copy = _enum(SetupFileCopyMode, reader.u8(), SetupFileCopyMode.Normal)
            if copy == SetupFileCopyMode.AlwaysSkipIfSameOrOlder:
                pass
            elif copy == SetupFileCopyMode.Normal:
                self.Flags |= SetupFileFlags.PromptIfOlder
            elif copy == SetupFileCopyMode.IfDoesntExist:
                self.Flags |= SetupFileFlags.OnlyIfDoesntExist | SetupFileFlags.PromptIfOlder
            elif copy == SetupFileCopyMode.AlwaysOverwrite:
                self.Flags |= SetupFileFlags.IgnoreVersion | SetupFileFlags.PromptIfOlder

        if version >= (4, 1, 0):
            self.Permissions = reader.u16()

        def flagbit(f):
            self.Flags |= f if reader.read_bit() else 0

        flagstart = reader.tell()

        if True:
            flagbit(SetupFileFlags.ConfirmOverwrite)
            flagbit(SetupFileFlags.NeverUninstall)
            flagbit(SetupFileFlags.RestartReplace)
            flagbit(SetupFileFlags.DeleteAfterInstall)
        if version.bits > 16:
            flagbit(SetupFileFlags.RegisterServer)
            flagbit(SetupFileFlags.RegisterTypeLib)
            flagbit(SetupFileFlags.SharedFile)
        if version < (2, 0, 0) and not version.isx:
            flagbit(SetupFileFlags.IsReadmeFile)
        if True:
            flagbit(SetupFileFlags.CompareTimeStamp)
            flagbit(SetupFileFlags.FontIsNotTrueType)
        if version >= (1, 2, 5):
            flagbit(SetupFileFlags.SkipIfSourceDoesntExist)
        if version >= (1, 2, 6):
            flagbit(SetupFileFlags.OverwriteReadOnly)
        if version >= (1, 3, 21):
            flagbit(SetupFileFlags.OverwriteSameVersion)
            flagbit(SetupFileFlags.CustomDestName)
        if version >= (1, 3, 25):
            flagbit(SetupFileFlags.OnlyIfDestFileExists)
        if version >= (2, 0, 5):
            flagbit(SetupFileFlags.NoRegError)
        if version >= (3, 0, 1):
            flagbit(SetupFileFlags.UninsRestartDelete)
        if version >= (3, 0, 5):
            flagbit(SetupFileFlags.OnlyIfDoesntExist)
            flagbit(SetupFileFlags.IgnoreVersion)
            flagbit(SetupFileFlags.PromptIfOlder)
        if version >= (4, 0, 0) or version.isx and version >= (3, 0, 6, 1):
            flagbit(SetupFileFlags.DontCopy)
        if version >= (4, 0, 5):
            flagbit(SetupFileFlags.UninsRemoveReadOnly)
        if version >= (4, 1, 8):
            flagbit(SetupFileFlags.RecurseSubDirsExternal)
        if version >= (4, 2, 1):
            flagbit(SetupFileFlags.ReplaceSameVersionIfContentsDiffer)
        if version >= (4, 2, 5):
            flagbit(SetupFileFlags.DontVerifyChecksum)
        if version >= (5, 0, 3):
            flagbit(SetupFileFlags.UninsNoSharedFilePrompt)
        if version >= (5, 1, 0):
            flagbit(SetupFileFlags.CreateAllSubDirs)
        if version >= (5, 1, 2):
            flagbit(SetupFileFlags.Bits32)
            flagbit(SetupFileFlags.Bits64)
        if version >= (5, 2, 0):
            flagbit(SetupFileFlags.ExternalSizePreset)
            flagbit(SetupFileFlags.SetNtfsCompression)
            flagbit(SetupFileFlags.UnsetNtfsCompression)
        if version >= (5, 2, 5):
            flagbit(SetupFileFlags.GacInstall)

        reader.byte_align()

        if reader.tell() - flagstart == 3:
            reader.u8()

        self.Type = SetupFileType(reader.u8())

Ancestors

class TSetup (reader, version)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class TSetup(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion):
        super().__init__(reader, version)
        self.Header = h = SetupHeader(reader, version)

        def _array(count: int, parser: Type[_T]) -> List[_T]:
            return [parser(reader, version, self) for _ in range(count)]

        self.Languages = _array(h.LanguageCount, SetupLanguage)
        _default_codec = 'cp1252'

        if version.unicode:
            self.Codec = 'utf-16le'
        elif not self.Languages:
            self.Codec = _default_codec
        else:
            self.Codec = self.Languages[0].Codepage
            if any(language.Codepage == _default_codec for language in self.Languages):
                self.Codec = _default_codec

        if version.ascii:
            h.recode_strings(self.Codec)
        else:
            h.recode_strings('latin1')

        # if version < INNO_VERSION(4, 0, 0):
        #  load_wizard_and_decompressor

        self.Messages    = _array(h.MessageCount,    SetupMessage)     # noqa
        self.Permissions = _array(h.PermissionCount, SetupPermission)  # noqa
        self.Types       = _array(h.TypeCount,       SetupType)        # noqa
        self.Components  = _array(h.ComponentCount,  SetupComponent)   # noqa
        self.Tasks       = _array(h.TaskCount,       SetupTask)        # noqa
        self.Directories = _array(h.DirectoryCount,  SetupDirectory)   # noqa
        self.Files       = _array(h.FileCount,       SetupFile)        # noqa

        # self.Icons                  = _array(h.IconCount,                 SetupIcon)
        # self.IniEntries             = _array(h.IniEntryCount,             SetupIniEntry)
        # self.RegistryEntries        = _array(h.RegistryEntryCount,        SetupRegistryEntry)
        # self.DeleteEntries          = _array(h.DeleteEntryCount,          SetupDeleteEntry)
        # self.UninstallDeleteEntries = _array(h.UninstallDeleteEntryCount, SetupUninstallDeleteEntry)
        # self.RunEntries             = _array(h.RunEntryCount,             SetupRunEntry)
        # self.UninstallRunEntries    = _array(h.UninstallRunEntryCount,    SetupUninstallRunEntry)

        # if version >= INNO_VERSION(4, 0, 0):
        #  load_wizard_and_decompressor

Ancestors

class SetupDataEntryFlags (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class SetupDataEntryFlags(enum.IntFlag):
    Empty                    = 0           # noqa    
    VersionInfoValid         = enum.auto() # noqa
    VersionInfoNotValid      = enum.auto() # noqa
    BZipped                  = enum.auto() # noqa
    TimeStampInUTC           = enum.auto() # noqa
    IsUninstallerExe         = enum.auto() # noqa
    CallInstructionOptimized = enum.auto() # noqa
    Touch                    = enum.auto() # noqa
    ChunkEncrypted           = enum.auto() # noqa
    ChunkCompressed          = enum.auto() # noqa
    SolidBreak               = enum.auto() # noqa
    Sign                     = enum.auto() # noqa
    SignOnce                 = enum.auto() # noqa

Ancestors

  • enum.IntFlag
  • builtins.int
  • enum.Flag
  • enum.Enum

Class variables

var Empty
var VersionInfoValid
var VersionInfoNotValid
var BZipped
var TimeStampInUTC
var IsUninstallerExe
var CallInstructionOptimized
var Touch
var ChunkEncrypted
var ChunkCompressed
var SolidBreak
var Sign
var SignOnce
class SetupSignMode (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class SetupSignMode(enum.IntEnum):
    NoSetting  = 0  # noqa
    Yes        = 1  # noqa
    Once       = 2  # noqa
    Check      = 3  # noqa

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var NoSetting
var Yes
var Once
var Check
class SetupDataEntry (reader, version)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class SetupDataEntry(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion):
        super().__init__(reader, version)
        self.FirstSlice = reader.u32()
        self.LastSlice = reader.u32()
        self.ChunkOffset = reader.u32()
        if version >= (4, 0, 1):
            self.Offset = reader.u64()
        self.FileSize = reader.u64()
        self.ChunkSize = reader.u64()

        if version >= (6, 4, 0):
            self.ChecksumType = CheckSumType.SHA256
            self.Checksum = bytes(reader.read(32))
        elif version >= (5, 3, 9):
            self.ChecksumType = CheckSumType.SHA1
            self.Checksum = bytes(reader.read(20))
        elif version >= (4, 2, 0):
            self.ChecksumType = CheckSumType.MD5
            self.Checksum = bytes(reader.read(16))
        elif version >= (4, 0, 1):
            self.ChecksumType = CheckSumType.CRC32
            self.Checksum = reader.u32()
        else:
            self.ChecksumType = CheckSumType.Adler32
            self.Checksum = reader.u32()

        ft = reader.u64()
        ts = datetime.fromtimestamp(
            (ft - _FILE_TIME_1970_01_01) / 10000000, timezone.utc)
        self.FileTime = ts

        self.FileVersionMs = reader.u32()
        self.FileVersionLs = reader.u32()

        self.Flags = 0

        def flagbit(f):
            self.Flags |= f if reader.read_bit() else 0

        flagbit(SetupDataEntryFlags.VersionInfoNotValid)
        flagbit(SetupDataEntryFlags.VersionInfoValid)

        if version < (4, 0, 1):
            flagbit(SetupDataEntryFlags.BZipped)
        if version >= (4, 0, 10):
            flagbit(SetupDataEntryFlags.TimeStampInUTC)
        if version >= (4, 1, 0):
            flagbit(SetupDataEntryFlags.IsUninstallerExe)
        if version >= (4, 1, 8):
            flagbit(SetupDataEntryFlags.CallInstructionOptimized)
        if version >= (4, 2, 0):
            flagbit(SetupDataEntryFlags.Touch)
        if version >= (4, 2, 2):
            flagbit(SetupDataEntryFlags.ChunkEncrypted)
        if version >= (4, 2, 5):
            flagbit(SetupDataEntryFlags.ChunkCompressed)
        if version >= (5, 1, 13):
            flagbit(SetupDataEntryFlags.SolidBreak)
        if version >= (5, 5, 7) and version < (6, 3, 0):
            flagbit(SetupDataEntryFlags.Sign)
            flagbit(SetupDataEntryFlags.SignOnce)

        reader.byte_align()

        if version >= (6, 3, 0):
            self.SignMode = SetupSignMode(reader.u8())
        elif self.Flags & SetupDataEntryFlags.SignOnce:
            self.SignMode = SetupSignMode.Once
        elif self.Flags & SetupDataEntryFlags.Sign:
            self.SignMode = SetupSignMode.Yes
        else:
            self.SignMode = SetupSignMode.NoSetting

Ancestors

class TData (reader, version)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

Expand source code Browse git
class TData(InnoStruct):

    def __init__(self, reader: StructReader[memoryview], version: InnoVersion):
        super().__init__(reader, version)
        self.DataEntries: list[SetupDataEntry] = []
        while not reader.eof:
            self.DataEntries.append(SetupDataEntry(reader, version))

Ancestors

class InnoParseResult (version, streams, files, encrypted_file, warnings, failures, stream0, stream1)

InnoParseResult(version, streams, files, encrypted_file, warnings, failures, stream0, stream1)

Expand source code Browse git
class InnoParseResult(NamedTuple):
    version: InnoVersion
    streams: List[InnoStream]
    files: List[InnoFile]
    encrypted_file: Optional[InnoFile]
    warnings: int
    failures: List[str]
    stream0: TSetup
    stream1: TData

    def ok(self):
        return self.warnings == 0 and not self.failures

Ancestors

  • builtins.tuple

Instance variables

var version

Alias for field number 0

var streams

Alias for field number 1

var files

Alias for field number 2

var encrypted_file

Alias for field number 3

var warnings

Alias for field number 4

var failures

Alias for field number 5

var stream0

Alias for field number 6

var stream1

Alias for field number 7

Methods

def ok(self)
Expand source code Browse git
def ok(self):
    return self.warnings == 0 and not self.failures
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.

Expand source code Browse git
class xtinno(ArchiveUnit):
    """
    Extract files from InnoSetup archives.
    """

    _STREAM_NAMES = 'meta/TSetup', 'meta/TData', 'meta/Uninstaller'
    _ISCRIPT_NAME = 'meta/script'
    _LICENSE_NAME = 'meta/license.rtf'
    _OFFSETS_PATH = 'RCDATA/11111/0'
    _CHUNK_PREFIX = b'zlb\x1a'
    _MAX_ATTEMPTS = 200_000

    def unpack(self, data: bytearray):
        try:
            file_metadata = one(data | perc(self._OFFSETS_PATH))
        except Exception as E:
            raise ValueError(F'Could not find TSetupOffsets PE resource at {self._OFFSETS_PATH}') from E

        meta = TSetupOffsets(file_metadata)
        view = memoryview(data)
        base = meta.base
        inno = StructReader(view[base:base + meta.total_size])

        self._decompressed = {}
        password = self.args.pwd or None

        blobsize = meta.setup0 - meta.setup1
        inno.seek(meta.setup1)
        blobs = StructReader(inno.read(blobsize))

        header = bytes(inno.read(16))

        try:
            version = InnoVersion.ParseLegacy(header)
        except ValueError:
            header += bytes(inno.read(64 - 16))
            try:
                version = InnoVersion.Parse(header)
            except ValueError:
                name, _, _rest = header.partition(b'\0')
                method = 'broken'
                if any(_rest):
                    header = name.hex()
                else:
                    header = name.decode('latin1')
                if self.leniency < 1:
                    raise ValueError(F'unable to parse header identifier "{header}"')
                version = _DEFAULT_INNO_VERSION
            else:
                header, _, _ = header.partition(B'\0')
                header = header.decode('latin1')
                method = 'modern'
        else:
            header, _, _ = header.partition(B'\x1A')
            header = header.decode('latin1')
            method = 'legacy'

        self.log_info(F'inno {version!s} via {method} header: {header}')

        class _notok(object):
            def __init__(self, e: Exception):
                self.failures = [str(e)]

            def ok(self):
                return False

        def _parse(v: InnoVersion):
            try:
                inno.seekset(inno_start)
                if v.legacy:
                    inno.seekrel(-48)
                r = self._try_parse_as(inno, blobs, v, password)
            except Exception as e:
                self.log_info(F'exception while parsing as {v!s}: {exception_to_string(e)}')
                return _notok(e)
            else:
                results[v] = r
                return r

        inno_start = inno.tell()
        best_parse = None
        best_score = 0
        success = False
        results: dict[InnoVersion, InnoParseResult] = {}

        if not version.is_ambiguous():
            index = _VERSIONS.index(version)
        else:
            try:
                index = max(k for k, v in enumerate(_VERSIONS) if v <= version)
            except Exception:
                index = 0

        upper = index + 1
        lower = index

        while lower > 0 and _IS_AMBIGUOUS[_VERSIONS[lower - 1]]:
            lower -= 1

        versions = [version] + _VERSIONS[lower:index] + _VERSIONS[upper:] + _VERSIONS[:lower]

        for v in versions:
            result = _parse(v)
            if success := result.ok():
                break
            if not result.failures and (best_parse is None or result.warnings < best_score):
                best_score = best_score
                best_parse = result

        if not success:
            if best_parse is not None:
                result = best_parse
                self.log_warn(F'using parse result for {result.version!s} with {result.warnings} warnings')
            else:
                result = min(results.values(), key=lambda result: len(result.failures))
                self.log_warn(F'using parse result for {result.version!s} with {len(result.failures)} failures')
                for k, failure in enumerate(result.failures, 1):
                    self.log_info(F'failure {k}: {failure}')

        version = result.version
        codec = result.stream0.Codec
        stream0 = result.stream0
        stream1 = result.stream1
        files = result.files
        streams = result.streams
        encrypted_file = result.encrypted_file

        if version.unicode:
            codec = 'latin1'
        self.log_info(
            F'inno {version!s} '
            F'compression:{stream0.Header.CompressionMethod.name} '
            F'codepage:{codec} '
            F'password:{stream0.Header.PasswordType.name} '
        )

        if stream0.Header.CompiledCode:
            from refinery.units.formats.ifps import ifps
            script = stream0.Header.CompiledCode
            yield self._pack(F'{self._ISCRIPT_NAME}.ps', None, script | ifps(codec) | bytes)
            yield self._pack(F'{self._ISCRIPT_NAME}.bin', None, script)

        if encrypted_file is not None:
            assert password is None, (
                F'An encrypted test file was chosen even though a password was provided: {password!r}')
            self.log_info(F'guessing password using encrypted file: {encrypted_file.path}')
            try:
                def _pwd(s: str):
                    if not s:
                        return False
                    if re.search(r'\{\w+\}', s):
                        return False
                    if re.search(R'^\w{2,12}://', s):
                        return False
                    return True
                from refinery.units.formats.ifpsstr import ifpsstr
                from itertools import combinations
                strings = script | ifpsstr(codec) | {str}
                strings = [s for s in strings if _pwd(s)]
                total = 0
                for k in range(1, 10):
                    for parts in combinations(strings, k):
                        if total > self._MAX_ATTEMPTS:
                            break
                        total += 1
                        string = ''.join(parts)
                        try:
                            plaintext = self._read_file_and_check(encrypted_file, password=string)
                            if encrypted_file.check(plaintext) != encrypted_file.checksum:
                                continue
                            password = string
                            break
                        except Exception:
                            continue
                    if total > self._MAX_ATTEMPTS:
                        break
            except Exception as e:
                self.log_info(F'failed to extract strings from IFPS: {exception_to_string(e)}')

        yield self._pack(
            streams[2].name, None, lambda s=streams[2]: self._read_stream(s))

        yield self._pack(streams[0].name, None, streams[0].data)

        with BytesAsStringEncoder as encoder:
            yield self._pack(F'{streams[0].name}.json', None,
                encoder.dumps(stream0.json()).encode(self.codec))
            yield self._pack(self._LICENSE_NAME, None,
                stream0.Header.get_license().encode(self.codec))

        yield self._pack(streams[1].name, None, streams[1].data)

        with BytesAsStringEncoder as encoder:
            yield self._pack(F'{streams[1].name}.json', None,
                encoder.dumps(stream1.json()).encode(self.codec))

        for file in files:
            if file.dupe:
                continue
            yield self._pack(
                file.path,
                file.date,
                lambda f=file: self._read_file_and_check(f, password=password),
                tags=[t.name for t in SetupFileFlags if t & file.tags],
            )

    def _try_parse_as(
        self,
        inno: StructReader,
        blobs: StructReader,
        version: InnoVersion,
        password: Optional[str] = None,
        max_failures: int = 5
    ):
        streams: List[InnoStream] = []
        files: List[InnoFile] = []
        encrypted_file = None
        warnings = 0

        for name in self._STREAM_NAMES:
            stream = InnoStream(StreamHeader(inno, name, version))
            streams.append(stream)
            to_read = stream.header.StoredSize
            while to_read > 4:
                block = CrcCompressedBlock(inno, min(to_read - 4, 0x1000))
                stream.blocks.append(block)
                to_read -= len(block)

        self.log_debug(F'{version!s} parsing stream 1 (TData)')
        stream1 = TData(memoryview(self._read_stream(streams[1])), version)

        for meta in stream1.DataEntries:
            file = InnoFile(blobs, version, meta)
            files.append(file)
            if password or not file.encrypted or not file.size:
                continue
            if encrypted_file is None or file.size < encrypted_file.size:
                encrypted_file = file

        self.log_debug(F'{version!s} parsing stream 0 (TSetup)')
        stream0 = TSetup(memoryview(self._read_stream(streams[0])), version)
        path_dedup: dict[str, list[SetupFile]] = {}

        for file in files:
            file.compression_method = stream0.Header.CompressionMethod
            file.password_hash = stream0.Header.PasswordHash
            file.password_type = stream0.Header.PasswordType
            file.password_salt = stream0.Header.PasswordSalt

        for sf in stream0.Files:
            sf: SetupFile
            location = sf.Location
            if location == 0xFFFFFFFF or sf.Type != SetupFileType.UserFile or sf.Source:
                msg = F'skipping file: offset=0x{location:08X} type={sf.Type.name}'
                if sf.Source:
                    msg = F'{msg} src={sf.Source}'
                self.log_debug(msg)
                continue
            if location >= len(files):
                self.log_warn(F'parsed {len(file)} entries, ignoring invalid setup reference to entry {location + 1}')
                continue
            path = sf.Destination.replace('\\', '/')
            if condition := sf.Condition.Check:
                condition = condition.replace(' ', '-')
                path = F'{condition}/{path}'
            path = F'data/{path}'
            path_dedup.setdefault(path, []).append(sf)
            files[location].setup = sf
            files[location].path = path

        for path, infos in path_dedup.items():
            if len(infos) == 1:
                files[infos[0].Location].path = path
                continue
            bycheck = {}
            for info in infos:
                file = files[info.Location]
                if not file.checksum_type.strong():
                    bycheck.clear()
                    break
                dkey = (file.checksum, file.size)
                if dkey in bycheck:
                    self.log_debug(F'skipping exact duplicate: {path}')
                    file.dupe = True
                    continue
                bycheck[dkey] = info
            if bycheck:
                if len(bycheck) == 1:
                    file.path = path
                    continue
                infos = list(bycheck.values())
            for k, info in enumerate(infos):
                files[info.Location].path = F'{path}[{k}]'

        _width = len(str(len(files)))

        for k, file in enumerate(files):
            if file.dupe:
                continue
            if not file.path:
                self.log_debug(F'file {k} does not have a path')
                file.path = F'raw/FileData{k:0{_width}d}'

        warnings = sum(1 for file in files if file.setup is None)
        failures = []
        nonempty = [f for f in files if f.size > 0]

        self._decompressed.clear()

        for file in nonempty:
            if len(failures) >= max_failures:
                break
            if file.setup is None:
                failures.append(F'file {file.path} had no associated metadata')
                continue
            if file.chunk_length < 0x10000:
                try:
                    data = self._read_file(file)
                except InvalidPassword:
                    continue
                except Exception as e:
                    failures.append(F'extraction error for {file.path}: {e!s}')
                    continue
                if file.check(data) != file.checksum:
                    failures.append(F'invalid checksum for {file.path}')

        return InnoParseResult(
            version,
            streams,
            files,
            encrypted_file,
            warnings,
            failures,
            stream0,
            stream1,
        )

    def _read_stream(self, stream: InnoStream):
        if stream.data is not None:
            return stream.data
        result = bytearray()
        it = iter(stream.blocks)
        if stream.compression == StreamCompressionMethod.Store:
            class _dummy(self):
                def decompress(self, b):
                    return b
            dec = _dummy()
        elif stream.compression == StreamCompressionMethod.LZMA1:
            import lzma
            first = next(it).BlockData
            prop, first = first[:5], first[5:]
            filter = lzma._decode_filter_properties(lzma.FILTER_LZMA1, prop)
            dec = lzma.LZMADecompressor(lzma.FORMAT_RAW, filters=[filter])
            result.extend(dec.decompress(first))
        elif stream.compression == StreamCompressionMethod.ZLib:
            import zlib
            dec = zlib.decompressobj()
        for block in it:
            result.extend(dec.decompress(block.BlockData))
        stream.data = result
        return result

    def _read_chunk(self, file: InnoFile, password: Optional[str] = None):
        reader = file.reader
        offset = file.chunk_offset
        length = file.chunk_length
        method = file.compression

        if offset + length > len(reader):
            span = F'0x{offset:X}-0x{offset + length:X}'
            raise LookupError(
                F'File data spans 0x{len(file.reader):X} bytes, but the file {file.path} is located at {span}.')

        reader.seek(offset)
        prefix = reader.read(4)

        if prefix != self._CHUNK_PREFIX:
            raise ValueError(F'Error reading chunk at offset 0x{offset:X}; invalid magic {prefix.hex()}.')

        if file.encrypted:
            if file.password_type == PasswordType.Nothing:
                raise RuntimeError(F'File {file.path} is encrypted, but no password type was set.')
            if password is None:
                raise InvalidPassword
            if file.password_type == PasswordType.XChaCha20:
                salt, iterations, nonce = struct.unpack('=16sI24s', file.password_salt)
                key = password.encode('utf8') | pbkdf2(32, salt, iterations, 'SHA256') | bytes
                test_nonce = list(struct.unpack('6I', nonce))
                test_nonce[2] = ~test_nonce[2]
                test_nonce = struct.pack('6I', test_nonce)
                if B'\0\0\0\0' | xchacha(key, nonce=test_nonce) | bytes != file.password_hash:
                    raise InvalidPassword(password)
                decryptor = xchacha(key, nonce=nonce)
            else:
                password_bytes = password.encode(
                    'utf-16le' if file.unicode else 'utf8')
                algorithm = {
                    PasswordType.SHA1: sha1,
                    PasswordType.MD5 : md5,
                }[file.password_type]
                hash = algorithm(b'PasswordCheckHash' + file.password_salt)
                hash.update(password_bytes)
                if hash.digest() != file.password_hash:
                    raise InvalidPassword(password)
                hash = algorithm(reader.read(8))
                hash.update(password_bytes)
                decryptor = rc4(hash.digest(), discard=1000)

        data = reader.read_exactly(length)

        if file.encrypted:
            data = data | decryptor | bytearray

        if method is None:
            return chunk

        try:
            if method == CompressionMethod.Store:
                chunk = data
            elif method == CompressionMethod.Lzma1:
                props = lzma._decode_filter_properties(lzma.FILTER_LZMA1, data[0:5])
                dec = lzma.LZMADecompressor(lzma.FORMAT_RAW, filters=[props])
                chunk = dec.decompress(data[5:])
            elif method == CompressionMethod.Lzma2:
                props = lzma._decode_filter_properties(lzma.FILTER_LZMA2, data[0:1])
                dec = lzma.LZMADecompressor(lzma.FORMAT_RAW, filters=[props])
                chunk = dec.decompress(data[1:])
            elif method == CompressionMethod.Bzip2:
                chunk = bz2.decompress(data)
            elif method == CompressionMethod.ZLib:
                chunk = zlib.decompress(data)
        except Exception as E:
            if not file.encrypted:
                raise
            raise InvalidPassword(password) from E

        return chunk

    def _read_file(
        self,
        file: InnoFile,
        password: Optional[str] = None,
    ):
        offset = file.chunk_offset
        length = file.chunk_length

        try:
            chunk = self._decompressed[offset, length]
        except KeyError:
            chunk = self._decompressed[offset, length] = self._read_chunk(file, password)

        view = memoryview(chunk)
        data = view[file.offset:file.offset + file.size]

        if file.filtered:
            if file.version >= (5, 2, 0):
                flip = (file.version >= (5, 3, 9))
                data = self._filter_new(data, flip_high_byte=flip)
            else:
                data = self._filter_old(data)

        return data

    def _read_file_and_check(
        self,
        file: InnoFile,
        password: Optional[str] = None,
    ):
        data = self._read_file(file, password)

        if not self.leniency and (cs := file.check(data)) is not None and cs != file.checksum:
            if isinstance(cs, int):
                computed = F'{cs:08X}'
                expected = F'{file.checksum:08X}'
            else:
                computed = cs.hex().upper()
                expected = file.checksum.hex().upper()
            raise ValueError(F'checksum error; computed:{computed} expected:{expected} [ignore this check with -L]')

        return data

    @ArchiveUnit.Requires('numpy', 'speed', 'default', 'extended')
    def _numpy():
        import numpy
        return numpy

    def _filter_new(self, data: ByteStr, flip_high_byte=False):
        try:
            np = self._numpy
        except ImportError:
            return self._filter_new_fallback(data, flip_high_byte)
        u08 = np.uint8
        u32 = np.uint32
        ab0 = bytearray()
        ab1 = bytearray()
        ab2 = bytearray()
        ab3 = bytearray()
        positions = []
        if isinstance(data, bytearray):
            out = data
        else:
            out = bytearray(data)
        mem = memoryview(out)
        for k in range(0, len(mem), 0x10000):
            for match in re.finditer(B'(?s)[\xE8\xE9]....', mem[k:k + 0x10000], flags=re.DOTALL):
                a = match.start() + k
                top = mem[a + 4]
                if top != 0x00 and top != 0xFF:
                    continue
                ab0.append(mem[a + 1])
                ab1.append(mem[a + 2])
                ab2.append(mem[a + 3])
                ab3.append(top)
                positions.append(a + 5)
        ab0 = np.frombuffer(ab0, dtype=u08)
        ab1 = np.frombuffer(ab1, dtype=u08)
        low = np.frombuffer(ab2, dtype=u08).astype(u32)
        msb = np.frombuffer(ab3, dtype=u08)
        sub = np.fromiter(positions, dtype=u32)
        low <<= 8
        low += ab1
        low <<= 8
        low += ab0
        low -= sub
        low &= 0xFFFFFF
        if flip_high_byte:
            flips = low >> 23
            keeps = 1 - flips
            keeps *= msb
            msb ^= 0xFF
            msb *= flips
            msb += keeps
        low += (msb.astype(u32) << 24)
        addresses = low.tobytes()
        for k, offset in enumerate(positions):
            out[offset - 4:offset] = addresses[k * 4:(k + 1) * 4]
        return out

    def _filter_new_fallback(self, data: ByteStr, flip_high_byte=False):
        block_size = 0x10000
        out = bytearray(data)
        i = 0
        while len(data) - i >= 5:
            c = out[i]
            block_size_left = block_size - (i % block_size)
            i += 1
            if (c == 0xE8 or c == 0xE9) and block_size_left >= 5:
                address = out[i:i + 4]
                i += 4
                if address[3] == 0 or address[3] == 0xFF:
                    rel = address[0] | address[1] << 8 | address[2] << 16
                    rel -= i & 0xFFFFFF
                    out[i - 4] = rel & 0xFF
                    out[i - 3] = (rel >> 8) & 0xFF
                    out[i - 2] = (rel >> 16) & 0xFF
                    if flip_high_byte and (rel & 0x800000) != 0:
                        out[i - 1] = (~out[i - 1]) & 0xFF
        return out

    @staticmethod
    def _filter_old(data: ByteStr):
        if not isinstance(data, bytearray):
            data = bytearray(data)
        addr_bytes_left = 0
        addr_offset = 5
        addr = 0
        for i, c in enumerate(data):
            if addr_bytes_left == 0:
                if c == 0xE8 or c == 0xE9:
                    addr = (~addr_offset + 1) & 0xFFFFFFFF
                    addr_bytes_left = 4
            else:
                addr = (addr + c) & 0xFFFFFFFF
                c = addr & 0xFF
                addr = addr >> 8
                addr_bytes_left -= 1
            data[i] = c
        return data

    @classmethod
    def handles(self, data):
        if data[:2] != B'MZ':
            return False
        if re.search(re.escape(self._CHUNK_PREFIX), 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):
    try:
        file_metadata = one(data | perc(self._OFFSETS_PATH))
    except Exception as E:
        raise ValueError(F'Could not find TSetupOffsets PE resource at {self._OFFSETS_PATH}') from E

    meta = TSetupOffsets(file_metadata)
    view = memoryview(data)
    base = meta.base
    inno = StructReader(view[base:base + meta.total_size])

    self._decompressed = {}
    password = self.args.pwd or None

    blobsize = meta.setup0 - meta.setup1
    inno.seek(meta.setup1)
    blobs = StructReader(inno.read(blobsize))

    header = bytes(inno.read(16))

    try:
        version = InnoVersion.ParseLegacy(header)
    except ValueError:
        header += bytes(inno.read(64 - 16))
        try:
            version = InnoVersion.Parse(header)
        except ValueError:
            name, _, _rest = header.partition(b'\0')
            method = 'broken'
            if any(_rest):
                header = name.hex()
            else:
                header = name.decode('latin1')
            if self.leniency < 1:
                raise ValueError(F'unable to parse header identifier "{header}"')
            version = _DEFAULT_INNO_VERSION
        else:
            header, _, _ = header.partition(B'\0')
            header = header.decode('latin1')
            method = 'modern'
    else:
        header, _, _ = header.partition(B'\x1A')
        header = header.decode('latin1')
        method = 'legacy'

    self.log_info(F'inno {version!s} via {method} header: {header}')

    class _notok(object):
        def __init__(self, e: Exception):
            self.failures = [str(e)]

        def ok(self):
            return False

    def _parse(v: InnoVersion):
        try:
            inno.seekset(inno_start)
            if v.legacy:
                inno.seekrel(-48)
            r = self._try_parse_as(inno, blobs, v, password)
        except Exception as e:
            self.log_info(F'exception while parsing as {v!s}: {exception_to_string(e)}')
            return _notok(e)
        else:
            results[v] = r
            return r

    inno_start = inno.tell()
    best_parse = None
    best_score = 0
    success = False
    results: dict[InnoVersion, InnoParseResult] = {}

    if not version.is_ambiguous():
        index = _VERSIONS.index(version)
    else:
        try:
            index = max(k for k, v in enumerate(_VERSIONS) if v <= version)
        except Exception:
            index = 0

    upper = index + 1
    lower = index

    while lower > 0 and _IS_AMBIGUOUS[_VERSIONS[lower - 1]]:
        lower -= 1

    versions = [version] + _VERSIONS[lower:index] + _VERSIONS[upper:] + _VERSIONS[:lower]

    for v in versions:
        result = _parse(v)
        if success := result.ok():
            break
        if not result.failures and (best_parse is None or result.warnings < best_score):
            best_score = best_score
            best_parse = result

    if not success:
        if best_parse is not None:
            result = best_parse
            self.log_warn(F'using parse result for {result.version!s} with {result.warnings} warnings')
        else:
            result = min(results.values(), key=lambda result: len(result.failures))
            self.log_warn(F'using parse result for {result.version!s} with {len(result.failures)} failures')
            for k, failure in enumerate(result.failures, 1):
                self.log_info(F'failure {k}: {failure}')

    version = result.version
    codec = result.stream0.Codec
    stream0 = result.stream0
    stream1 = result.stream1
    files = result.files
    streams = result.streams
    encrypted_file = result.encrypted_file

    if version.unicode:
        codec = 'latin1'
    self.log_info(
        F'inno {version!s} '
        F'compression:{stream0.Header.CompressionMethod.name} '
        F'codepage:{codec} '
        F'password:{stream0.Header.PasswordType.name} '
    )

    if stream0.Header.CompiledCode:
        from refinery.units.formats.ifps import ifps
        script = stream0.Header.CompiledCode
        yield self._pack(F'{self._ISCRIPT_NAME}.ps', None, script | ifps(codec) | bytes)
        yield self._pack(F'{self._ISCRIPT_NAME}.bin', None, script)

    if encrypted_file is not None:
        assert password is None, (
            F'An encrypted test file was chosen even though a password was provided: {password!r}')
        self.log_info(F'guessing password using encrypted file: {encrypted_file.path}')
        try:
            def _pwd(s: str):
                if not s:
                    return False
                if re.search(r'\{\w+\}', s):
                    return False
                if re.search(R'^\w{2,12}://', s):
                    return False
                return True
            from refinery.units.formats.ifpsstr import ifpsstr
            from itertools import combinations
            strings = script | ifpsstr(codec) | {str}
            strings = [s for s in strings if _pwd(s)]
            total = 0
            for k in range(1, 10):
                for parts in combinations(strings, k):
                    if total > self._MAX_ATTEMPTS:
                        break
                    total += 1
                    string = ''.join(parts)
                    try:
                        plaintext = self._read_file_and_check(encrypted_file, password=string)
                        if encrypted_file.check(plaintext) != encrypted_file.checksum:
                            continue
                        password = string
                        break
                    except Exception:
                        continue
                if total > self._MAX_ATTEMPTS:
                    break
        except Exception as e:
            self.log_info(F'failed to extract strings from IFPS: {exception_to_string(e)}')

    yield self._pack(
        streams[2].name, None, lambda s=streams[2]: self._read_stream(s))

    yield self._pack(streams[0].name, None, streams[0].data)

    with BytesAsStringEncoder as encoder:
        yield self._pack(F'{streams[0].name}.json', None,
            encoder.dumps(stream0.json()).encode(self.codec))
        yield self._pack(self._LICENSE_NAME, None,
            stream0.Header.get_license().encode(self.codec))

    yield self._pack(streams[1].name, None, streams[1].data)

    with BytesAsStringEncoder as encoder:
        yield self._pack(F'{streams[1].name}.json', None,
            encoder.dumps(stream1.json()).encode(self.codec))

    for file in files:
        if file.dupe:
            continue
        yield self._pack(
            file.path,
            file.date,
            lambda f=file: self._read_file_and_check(f, password=password),
            tags=[t.name for t in SetupFileFlags if t & file.tags],
        )

Inherited members