Module refinery.lib.inno.emulator

An emulator for Inno Setup executables. The implementation is unlikely to be 100% correct as it was engineered by making various malicious scripts execute reasonably well, not by implementing an exact copy of the (only) reference implementation. This grew and grew as I wrote it, and seems mildly insane in hindsight.

Expand source code Browse git
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
An emulator for Inno Setup executables. The implementation is unlikely to be 100% correct as it
was engineered by making various malicious scripts execute reasonably well, not by implementing
an exact copy of [the (only) reference implementation][PS]. This grew and grew as I wrote it,
and seems mildly insane in hindsight.

[PS]: https://github.com/remobjects/pascalscript
"""
from __future__ import annotations

from typing import (
    get_origin,
    Any,
    TYPE_CHECKING,
    Callable,
    ClassVar,
    Generator,
    Dict,
    Set,
    Generic,
    List,
    Tuple,
    NamedTuple,
    Optional,
    Sequence,
    TypeVar,
    Union,
)

from dataclasses import dataclass, field
from enum import auto, Enum, IntFlag
from functools import partial, wraps
from pathlib import Path
from string import Formatter
from time import process_time
from urllib.parse import unquote
from datetime import datetime, timedelta

from refinery.lib.tools import cached_property
from refinery.lib.types import CaseInsensitiveDict
from refinery.lib.inno.archive import InnoArchive, Flags
from refinery.lib.types import AST, INF, NoMask
from refinery.lib.patterns import formats

from refinery.lib.inno.ifps import (
    AOp,
    COp,
    EHType,
    Function,
    IFPSFile,
    IFPSType,
    Op,
    Instruction,
    Operand,
    OperandType,
    TArray,
    TC,
    TRecord,
    TStaticArray,
    Value,
    VariableBase,
    VariableSpec,
    VariableType,
)

import fnmatch
import hashlib
import inspect
import io
import math
import operator
import random
import re
import struct
import time


if TYPE_CHECKING:
    from typing import ParamSpec
    _P = ParamSpec('_P')

_T = TypeVar('_T')
_Y = TypeVar('_Y')


class OleObject:
    """
    A dummy object representing an OLE interface created by an IFPS script. All it does so far is
    to remember the name of the object that was requested.
    """
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return F'OleObject({self.name!r})'

    def __str__(self):
        return self.name


class Variable(VariableBase, Generic[_T]):
    """
    This class represents a global or stack variable in the IFPS runtime.
    """
    data: Optional[Union[List[Variable], _T]]
    """
    The variable's value. This is a list of `refinery.lib.inno.emulator.Variable`s for container
    types, a `refinery.lib.inno.emulator.Variable` for pointer types, and a basic type otherwise.
    """
    path: Tuple[int, ...]
    """
    A tuple of integers that specify the seuqnce of indices required to access it, relative to the
    base variable given via `spec`.
    """

    __slots__ = (
        'data',
        'path',
        '_int_size',
        '_int_mask',
        '_int_bits',
        '_int_good',
    )

    @property
    def container(self):
        """
        A boolean indicating whether the given variable is a container.
        """
        return self.type.container

    @property
    def pointer(self):
        """
        A boolean indicating whether the given variable is a pointer.
        """
        return self.type.code == TC.Pointer

    def __len__(self):
        return len(self.data)

    def __bool__(self):
        return True

    def __getitem__(self, key: int):
        var = self.deref()
        if var.container:
            return var.at(key).get()
        else:
            return var.data[key]

    def __setitem__(self, key: int, v: _T):
        var = self.deref()
        if var.container:
            var.at(key).set(v)
        else:
            var.data[key] = var._wrap(v)

    def at(self, k: int):
        """
        Provides index access for the variable. If the variable is a pointer, it is dereferenced
        before accessing the data.
        """
        return self.deref().data[k]

    def deref(var):
        """
        Dereferences the variable until it is no longer a pointer and returns the result. If the
        variable is not a pointer, this function returns the variable itself.
        """
        while True:
            val = var.data
            if not isinstance(val, Variable):
                return var
            var = val

    def __init__(
        self,
        type: IFPSType,
        spec: Optional[VariableSpec] = None,
        path: Tuple[int, ...] = (),
        data: Optional[Union[_T, List]] = None
    ):
        super().__init__(type, spec)
        self.path = path

        self._int_size = _size = {
            TC.U08: +1,
            TC.U16: +1,
            TC.U32: +1,
            TC.S08: -1,
            TC.S16: -1,
            TC.S32: -1,
            TC.S64: -1,
        }.get((code := type.code), 0) * code.width
        if _size:
            bits = abs(_size) * 8
            umax = (1 << bits)
            self._int_bits = bits
            self._int_mask = umax - 1
            if _size < 0:
                self._int_good = range(-(umax >> 1), (umax >> 1))
            else:
                self._int_good = range(umax)
        else:
            self._int_mask = NoMask
            self._int_bits = INF
            self._int_good = AST

        if data is None:
            self.setdefault()
        else:
            self.set(data)

    def setdefault(self):
        """
        Set this variable's data to the default value for its type. This also initializes the
        values of any contained variables recursively.
        """
        spec = self.spec
        path = self.path

        def default(type: IFPSType, *sub_path):
            if isinstance(type, TRecord):
                return [Variable(t, spec, (*path, *sub_path, k)) for k, t in enumerate(type.members)]
            if isinstance(type, TStaticArray):
                t = type.type
                return [Variable(t, spec, (*path, *sub_path, k)) for k in range(type.size)]
            if isinstance(type, TArray):
                return []
            if sub_path:
                return Variable(type, spec, (*path, *sub_path))
            else:
                return type.default()

        self.data = default(self.type)

    def _wrap(self, value: Union[Value, _T], key: Optional[int] = None) -> _T:
        if (t := self.type.py_type(key)) and not isinstance(value, t):
            if issubclass(t, int):
                if isinstance(value, str) and len(value) == 1:
                    return ord(value[0])
                if isinstance(value, float):
                    return int(value)
            elif isinstance(value, int):
                if issubclass(t, str):
                    return chr(value)
                if issubclass(t, float):
                    return float(value)
            raise TypeError(F'Assigning value {value!r} to variable of type {self.type}.')
        if s := self._int_size and value not in self._int_good:
            mask = self._int_mask
            value &= mask
            if s < 0 and (value >> (self._int_bits - 1)):
                value = -(-value & mask)
        return value

    def resize(self, n: int):
        """
        This function is only valid for container type variables. It re-sizes the data list to
        ensure that the container stores exactly `n` sub-variables.
        """
        t = self.type
        m = n - len(self.data)
        if t.code != TC.Array:
            if t.code not in (TC.StaticArray, TC.Record):
                raise TypeError
            if n == t.size:
                return
            raise ValueError(F'Attempt to resize {t} of size {t.size} to {n}.')
        if m <= 0:
            del self.data[n:]
            return
        for k in range(m):
            self.data.append(Variable(t.type, self.spec, (*self.path, k)))

    def setptr(self, var: Variable, copy: bool = False):
        """
        This method is used to point a pointer variable to a target. This is different from calling
        the `refinery.lib.inno.emulator.Variable.set` method as the latter would try to dereference
        the pointer and assign to its target; this method sets the value of the pointer itself.
        """
        if not self.pointer:
            raise TypeError
        if not isinstance(var, Variable):
            raise TypeError
        if copy:
            var = Variable(var.type, data=var.get())
        self.data = var

    def set(self, value: Union[_T, Sequence, Variable]):
        """
        Assign a new value to the variable. This can either be an immediate value or a variable.
        For container types, it can also be a sequence of those.
        """
        if isinstance(value, Variable):
            value = value.get()
        elif isinstance(value, (Enum, Value)):
            value = value.value
        if self.pointer:
            return self.deref().set(value)
        elif self.container:
            if not isinstance(value, (list, tuple)):
                raise TypeError
            self.resize(len(value))
            for k, v in enumerate(value):
                self.data[k].set(v)
        else:
            self.data = self._wrap(value)

    def get(self) -> _T:
        """
        Return a representation of this variable that consists only of base types. For example, the
        result for a container type will not be a list of `refinery.lib.inno.emulator.Variable`s,
        but a list of their contents.
        """
        if self.pointer:
            return self.deref().get()
        if self.container:
            data: List[Variable] = self.data
            return [v.get() for v in data]
        return self.data

    @property
    def name(self):
        """
        Return the name of the variable as given by its spec.
        """
        if self.spec is None:
            return 'Unbound'
        name = F'{self.spec!s}'
        for k in self.path:
            name = F'{name}[{k}]'
        return name

    def __repr__(self):
        rep = self.name
        if (val := self.data) is None:
            return rep
        if self.type.code is TC.Set:
            val = F'{val:b}'
        elif self.pointer:
            val: Variable
            return F'{rep} -> {val.name}'
        elif isinstance(val, (str, int, float, list)):
            val = repr(self.get())
        else:
            return rep
        return F'{rep} = {val}'


class NeedSymbol(NotImplementedError):
    """
    An exception raised by `refinery.lib.inno.emulator.IFPSEmulator` if the runtime calls out to
    an external symbol that is not implemented.
    """
    pass


class OpCodeNotImplemented(NotImplementedError):
    """
    An exception raised by `refinery.lib.inno.emulator.IFPSEmulator` if an unsupported opcode is
    encountered during emulation.
    """
    pass


class EmulatorException(RuntimeError):
    """
    A generic exception representing any error that occurs during emulation.
    """
    pass


class AbortEmulation(Exception):
    """
    This exception can be raised by an external function handler to signal the emulator that script
    execution should be aborted.
    """
    pass


class IFPSException(RuntimeError):
    """
    This class represents an exception within the IFPS runtime, i.e. an exception that is subject
    to IFPS exception handling.
    """
    def __init__(self, msg: str, parent: Optional[BaseException] = None):
        super().__init__(msg)
        self.parent = parent


class EmulatorTimeout(TimeoutError):
    """
    The emulation timed out based on the given time limit in the configuration.
    """
    pass


class EmulatorExecutionLimit(TimeoutError):
    """
    The emulation timed out based on the given execution limit in the configuration.
    """
    pass


class EmulatorMaxStack(MemoryError):
    """
    The emulation was aborted because the stack limit given in the configuration was exceeded.
    """
    pass


class EmulatorMaxCalls(MemoryError):
    """
    The emulation was aborted because the call stack limit given in the configuration was exceeded.
    """
    pass


@dataclass
class ExceptionHandler:
    """
    This class represents an exception handler within the IFPS runtime.
    """
    finally_one: Optional[int]
    """
    Code offset of the first finally handler.
    """
    catch_error: Optional[int]
    """
    Code offset of the catch handler.
    """
    finally_two: Optional[int]
    """
    Code offset of the second finally handler.
    """
    handler_end: int
    """
    Code offset of the first instruction that is no longer covered.
    """
    current: EHType = EHType.Try
    """
    Represents the current state of this exception handler.
    """


class IFPSEmulatedFunction(NamedTuple):
    """
    Represents an emulated external symbol.
    """
    call: Callable
    """
    The actual callable function that implements the symbol.
    """
    spec: List[bool]
    """
    A list of boolean values, one for each parameter of the function. Each boolean indicates
    whether the parameter at that index is passed by reference.
    """
    static: bool
    """
    Indicates whether the handler is static. If this value is `False`, the callable expects an
    additional `self` parameter of type `refinery.lib.inno.emulator.IFPSEmulator`.
    """
    void: bool = False
    """
    Indicates whether the handler implements a procedure rather than a function in the IFPS
    runtime.
    """

    @property
    def argc(self):
        """
        The argument count for this handler.
        """
        return len(self.spec)


@dataclass
class IFPSEmulatorConfig:
    """
    The configuration for `refinery.lib.inno.emulator.IFPSEmulator`s.
    """
    x64: bool = True
    admin: bool = True
    windows_os_version: Tuple[int, int, int] = (10, 0, 10240)
    windows_sp_version: Tuple[int, int] = (2, 0)
    throw_abort: bool = False
    log_calls: bool = False
    log_passwords: bool = True
    log_mutexes: bool = True
    log_opcodes: bool = False
    wizard_silent: bool = True
    max_opcodes: int = 0
    max_seconds: int = 10
    start_time: datetime = field(default_factory=datetime.now)
    milliseconds_per_instruction: float = 0.001
    sleep_scale: float = 0.0
    max_data_stack: int = 1_000_000
    max_call_stack: int = 4096
    environment: Dict[str, str] = field(default_factory=dict)
    user_name: str = 'Frank'
    temp_path: str = ''
    host_name: str = 'Frank-PC'
    inno_name: str = 'ThisInstall'
    language: str = 'en'
    executable: str = 'C:\\Install.exe'
    install_to: str = 'I:\\'
    lcid: int = 0x0409

    @property
    def cwd(self):
        return Path(self.executable).parent


class TSetupStep(int, Enum):
    """
    An IFPS enumeration that classifies different setup steps.
    """
    ssPreInstall = 0
    ssInstall = auto()
    ssPostInstall = auto()
    ssDone = auto()


class TSplitType(int, Enum):
    """
    An IFPS enumeration that classifies different strategies for splitting strings.
    """
    stAll = 0
    stExcludeEmpty = auto()
    stExcludeLastEmpty = auto()


class TUninstallStep(int, Enum):
    """
    An IFPS enumeration that classifies uninstaller steps.
    """
    usAppMutexCheck = 0
    usUninstall = auto()
    usPostUninstall = auto()
    usDone = auto()


class TSetupProcessorArchitecture(int, Enum):
    """
    An IFPS enumeration that classifies different processor architectures.
    """
    paUnknown = 0
    paX86 = auto()
    paX64 = auto()
    paArm32 = auto()
    paArm64 = auto()


class PageID(int, Enum):
    """
    An IFPS enumeration that classifies the different installer pages.
    """
    wpWelcome = 1
    wpLicense = auto()
    wpPassword = auto()
    wpInfoBefore = auto()
    wpUserInfo = auto()
    wpSelectDir = auto()
    wpSelectComponents = auto()
    wpSelectProgramGroup = auto()
    wpSelectTasks = auto()
    wpReady = auto()
    wpPreparing = auto()
    wpInstalling = auto()
    wpInfoAfter = auto()
    wpFinished = auto()


class NewFunctionCall(NamedTuple):
    """
    An event generated by `refinery.lib.inno.emulator.IFPSEmulator.emulate_function` which
    represents a call to the function with the given name and arguments.
    """
    name: str
    args: tuple


class NewPassword(str):
    """
    An event generated by `refinery.lib.inno.emulator.IFPSEmulator.emulate_function` for each
    password that is entered by the emulated setup script to a password edit control.
    """
    pass


class NewMutex(str):
    """
    An event generated by `refinery.lib.inno.emulator.IFPSEmulator.emulate_function` for each
    mutex registered by the script.
    """
    pass


class NewInstruction(NamedTuple):
    """
    An event generated by `refinery.lib.inno.emulator.IFPSEmulator.emulate_function` for each
    executed instruction.
    """
    function: Function
    instruction: Instruction


class EventCall(Generic[_Y, _T]):
    """
    This class is a wrapper for generator functions that can also capture their return value.
    It is used for `refinery.lib.inno.emulator.IFPSEmulator.emulate_function`.
    """

    value: _T
    """
    The return value of the wrapped function.
    """

    def __init__(self, call: Generator[_Y, Any, _T]):
        self._call = call
        self._done = False
        self._buffer: List[_Y] = []
        self._value = None

    @classmethod
    def Wrap(cls, method: Callable[_P, Generator[_Y, Any, _T]]) -> Callable[_P, EventCall[_Y, _T]]:
        """
        Used for decorating generator functions.
        """
        @wraps(method)
        def wrapped(*args, **kwargs):
            return cls(method(*args, **kwargs))
        return wrapped

    @property
    def value(self):
        if not self._done:
            self._buffer = list(self)
        return self._value

    def __iter__(self):
        if self._done:
            yield from self._buffer
            assert self._value is not None
            self._buffer.clear()
        else:
            self._value = yield from self._call
            self._done = True
        return self._value


class FPUControl(IntFlag):
    """
    An integer flag representing FPU control words.
    """
    InvalidOperation    = 0b0_00_0_00_00_00_000001 # noqa
    DenormalizedOperand = 0b0_00_0_00_00_00_000010 # noqa
    ZeroDivide          = 0b0_00_0_00_00_00_000100 # noqa
    Overflow            = 0b0_00_0_00_00_00_001000 # noqa
    Underflow           = 0b0_00_0_00_00_00_010000 # noqa
    PrecisionError      = 0b0_00_0_00_00_00_100000 # noqa
    Reserved1           = 0b0_00_0_00_00_01_000000 # noqa
    Reserved2           = 0b0_00_0_00_00_10_000000 # noqa
    ExtendPrecision     = 0b0_00_0_00_01_00_000000 # noqa
    DoublePrecision     = 0b0_00_0_00_10_00_000000 # noqa
    MaxPrecision        = 0b0_00_0_00_11_00_000000 # noqa
    RoundDown           = 0b0_00_0_01_00_00_000000 # noqa
    RoundUp             = 0b0_00_0_10_00_00_000000 # noqa
    RoundTowardZero     = 0b0_00_0_11_00_00_000000 # noqa
    AffineInfinity      = 0b0_00_1_00_00_00_000000 # noqa
    Reserved3           = 0b0_01_0_00_00_00_000000 # noqa
    Reserved4           = 0b0_10_0_00_00_00_000000 # noqa
    ReservedBits        = 0b0_11_0_00_00_11_000000 # noqa


class IFPSEmulator:
    """
    The core IFPS emulator.
    """

    def __init__(
        self,
        archive: Union[InnoArchive, IFPSFile],
        options: Optional[IFPSEmulatorConfig] = None,
        **more
    ):
        if isinstance(archive, InnoArchive):
            self.inno = archive
            self.ifps = ifps = archive.ifps
            if ifps is None:
                raise ValueError('The input archive does not contain a script.')
        else:
            self.inno = None
            self.ifps = ifps = archive
        self.config = options or IFPSEmulatorConfig(**more)
        self.globals = [Variable(v.type, v.spec) for v in ifps.globals]
        self.stack: List[Variable] = []
        self.mutexes: Set[str] = set()
        self.symbols: Dict[str, Function] = CaseInsensitiveDict()
        self.reset()
        for pfn in ifps.functions:
            self.symbols[pfn.name] = pfn

    def __repr__(self):
        return self.__class__.__name__

    def reset(self):
        """
        Reset the emulator timing, FPU word, mutexes, trace, and stack. All global variables are
        set to their default values.
        """
        self.seconds_slept = 0.0
        self.clock = 0
        self.fpucw = FPUControl.MaxPrecision | FPUControl.RoundTowardZero
        self.jumpflag = False
        self.mutexes.clear()
        self.stack.clear()
        for v in self.globals:
            v.setdefault()
        return self

    def unimplemented(self, function: Function):
        """
        The base IFPS emulator raises `refinery.lib.inno.emulator.NeedSymbol` when an external
        symbol is unimplemented. Child classes can override this function to handle the missing
        symbol differently.
        """
        raise NeedSymbol(function.name)

    @EventCall.Wrap
    def emulate_function(self, function: Function, *args):
        """
        Emulate a function call to the given function, passing the given arguments. The method
        returns the return value of the emulated function call if it is not a procedure.
        """
        self.stack.clear()
        decl = function.decl
        if decl is None:
            raise NotImplementedError(F'Do not know how to call {function!s}.')
        if (n := len(decl.parameters)) != (m := len(args)):
            raise ValueError(
                F'Function {function!s} expects {n} arguments, only {m} were given.')
        for index, (argument, parameter) in enumerate(zip(args, decl.parameters), 1):
            variable = Variable(parameter.type, VariableSpec(index, VariableType.Local))
            variable.set(argument)
            self.stack.append(variable)
        self.stack.reverse()
        if not decl.void:
            result = Variable(decl.return_type, VariableSpec(0, VariableType.Argument))
            self.stack.append(result)
        yield from self.call(function)
        self.stack.clear()
        if not decl.void:
            return result.get()

    def call(self, function: Function):
        """
        Begin emulating at the start of the given function.
        """

        def operator_div(a, b):
            return a // b if isinstance(a, int) and isinstance(b, int) else a / b

        def operator_in(a, b):
            return a in b

        def getvar(op: Union[VariableSpec, Operand]) -> Variable:
            if not isinstance(op, Operand):
                v = op
                k = None
            elif op.type is OperandType.Value:
                raise TypeError('Attempting to retrieve variable for an immediate operand.')
            else:
                v = op.variable
                k = op.index
                if op.type is OperandType.IndexedByVar:
                    k = getvar(k).get()
            t, i = v.type, v.index
            if t is VariableType.Argument:
                if function.decl.void:
                    i -= 1
                var = self.stack[sp - i]
            elif t is VariableType.Global:
                var = self.globals[i]
            elif t is VariableType.Local:
                var = self.stack[sp + i]
            else:
                raise TypeError
            if k is not None:
                var = var.at(k)
            return var

        def getval(op: Operand):
            if op.immediate:
                return op.value.value
            return getvar(op).get()

        def setval(op: Operand, new):
            if op.immediate:
                raise RuntimeError('attempt to assign to an immediate')
            getvar(op).set(new)

        class CallState(NamedTuple):
            fn: Function
            ip: int
            sp: int
            eh: List[ExceptionHandler]

        callstack: List[CallState] = []
        exec_start = process_time()
        stack = self.stack
        _cfg_max_call_stack = self.config.max_call_stack
        _cfg_max_data_stack = self.config.max_data_stack
        _cfg_max_seconds = self.config.max_seconds
        _cfg_max_opcodes = self.config.max_opcodes
        _cfg_log_opcodes = self.config.log_opcodes

        ip: int = 0
        sp: int = len(stack) - 1
        pending_exception = None
        exceptions = []

        while True:
            if 0 < _cfg_max_call_stack < len(callstack):
                raise EmulatorMaxCalls

            if function.body is None:
                namespace = ''

                if decl := function.decl:
                    if decl.is_property:
                        if stack[-1].type.code == TC.Class:
                            function = function.setter
                        else:
                            function = function.getter
                        decl = function.decl
                    namespace = (
                        decl.classname or decl.module or '')

                name = function.name
                registry: Dict[str, IFPSEmulatedFunction] = self.external_symbols.get(namespace, {})
                handler = registry.get(name)

                if handler:
                    void = handler.void
                    argc = handler.argc
                elif decl:
                    void = decl.void
                    argc = decl.argc
                else:
                    void = True
                    argc = 0

                try:
                    rpos = 0 if void else 1
                    args = [stack[~k] for k in range(rpos, argc + rpos)]
                except IndexError:
                    raise EmulatorException(
                        F'Cannot call {function!s}; {argc} arguments + {rpos} return values expected,'
                        F' but stack size is only {len(stack)}.')

                if self.config.log_calls:
                    yield NewFunctionCall(str(function), tuple(a.get() for a in args))

                if handler is None:
                    self.unimplemented(function)
                else:
                    if decl and (decl.void != handler.void or decl.argc != handler.argc):
                        ok = False
                        if 1 + decl.argc - decl.void == 1 + handler.argc - handler.void:
                            if decl.void and not decl.parameters[0].const:
                                ok = True
                            elif handler.void and handler.spec[0]:
                                ok = True
                        if not ok:
                            raise RuntimeError(F'Handler for {function!s} is incompatible with declaration.')
                    for k, (var, byref) in enumerate(zip(args, handler.spec)):
                        if not byref:
                            args[k] = var.get()
                    if not handler.static:
                        args.insert(0, self)
                    try:
                        return_value = handler.call(*args)
                        if inspect.isgenerator(return_value):
                            return_value = yield from return_value
                    except GeneratorExit:
                        pass
                    except BaseException as b:
                        pending_exception = IFPSException(F'Error calling {function.name}: {b!s}', b)
                    else:
                        if not handler.void:
                            stack[-1].set(return_value)
                if not callstack:
                    if pending_exception is None:
                        return
                    raise pending_exception
                function, ip, sp, exceptions = callstack.pop()
                continue

            while insn := function.code.get(ip, None):
                if 0 < _cfg_max_seconds < process_time() - exec_start:
                    raise EmulatorTimeout
                if 0 < _cfg_max_opcodes < self.clock:
                    raise EmulatorExecutionLimit
                if 0 < _cfg_max_data_stack < len(stack):
                    raise EmulatorMaxStack
                if _cfg_log_opcodes:
                    yield NewInstruction(function, insn)
                try:
                    if pe := pending_exception:
                        pending_exception = None
                        raise pe

                    opc = insn.opcode
                    ip += insn.size
                    self.clock += 1

                    if opc == Op.Nop:
                        continue
                    elif opc == Op.Assign:
                        dst = getvar(insn.op(0))
                        src = insn.op(1)
                        if src.immediate:
                            dst.set(src.value)
                        else:
                            dst.set(getvar(src))
                    elif opc == Op.Calculate:
                        calculate = {
                            AOp.Add: operator.add,
                            AOp.Sub: operator.sub,
                            AOp.Mul: operator.mul,
                            AOp.Div: operator_div,
                            AOp.Mod: operator.mod,
                            AOp.Shl: operator.lshift,
                            AOp.Shr: operator.rshift,
                            AOp.And: operator.and_,
                            AOp.BOr: operator.or_,
                            AOp.Xor: operator.xor,
                        }[insn.operator]
                        src = insn.op(1)
                        dst = insn.op(0)
                        sv = getval(src)
                        dv = getval(dst)
                        fpu = isinstance(sv, float) or isinstance(dv, float)
                        try:
                            result = calculate(dv, sv)
                            if fpu and not isinstance(result, float):
                                raise FloatingPointError
                        except FloatingPointError as FPE:
                            if not self.fpucw & FPUControl.InvalidOperation:
                                result = float('nan')
                            else:
                                raise IFPSException('invalid operation', FPE) from FPE
                        except OverflowError as OFE:
                            if fpu and self.fpucw & FPUControl.Overflow:
                                result = float('nan')
                            else:
                                raise IFPSException('arithmetic overflow', OFE) from OFE
                        except ZeroDivisionError as ZDE:
                            if fpu and self.fpucw & FPUControl.ZeroDivide:
                                result = float('nan')
                            else:
                                raise IFPSException('division by zero', ZDE) from ZDE
                        setval(dst, result)
                    elif opc == Op.Push:
                        # TODO: I do not actually know how this works
                        stack.append(getval(insn.op(0)))
                    elif opc == Op.PushVar:
                        stack.append(getvar(insn.op(0)))
                    elif opc == Op.Pop:
                        self.temp = stack.pop()
                    elif opc == Op.Call:
                        callstack.append(CallState(function, ip, sp, exceptions))
                        function = insn.operands[0]
                        ip = 0
                        sp = len(stack) - 1
                        exceptions = []
                        break
                    elif opc == Op.Jump:
                        ip = insn.operands[0]
                    elif opc == Op.JumpTrue:
                        if getval(insn.op(1)):
                            ip = insn.operands[0]
                    elif opc == Op.JumpFalse:
                        if not getval(insn.op(1)):
                            ip = insn.operands[0]
                    elif opc == Op.Ret:
                        del stack[sp + 1:]
                        if not callstack:
                            return
                        function, ip, sp, exceptions = callstack.pop()
                        break
                    elif opc == Op.StackType:
                        raise OpCodeNotImplemented(str(opc))
                    elif opc == Op.PushType:
                        stack.append(Variable(
                            insn.operands[0],
                            VariableSpec(len(stack) - sp, VariableType.Local)
                        ))
                    elif opc == Op.Compare:
                        compare = {
                            COp.GE: operator.ge,
                            COp.LE: operator.le,
                            COp.GT: operator.gt,
                            COp.LT: operator.lt,
                            COp.NE: operator.ne,
                            COp.EQ: operator.eq,
                            COp.IN: operator_in,
                            COp.IS: operator.is_,
                        }[insn.operator]
                        d = getvar(insn.op(0))
                        a = getval(insn.op(1))
                        b = getval(insn.op(2))
                        d.set(compare(a, b))
                    elif opc == Op.CallVar:
                        call = getval(insn.op(0))
                        if isinstance(call, int):
                            call = self.ifps.functions[call]
                        if isinstance(call, Function):
                            callstack.append(CallState(function, ip, sp, exceptions))
                            function = call
                            ip = 0
                            sp = len(stack) - 1
                            exceptions = []
                            break
                    elif opc in (Op.SetPtr, Op.SetPtrToCopy):
                        copy = False
                        if opc == Op.SetPtrToCopy:
                            copy = True
                        dst = getvar(insn.op(0))
                        src = getvar(insn.op(1))
                        dst.setptr(src, copy=copy)
                    elif opc == Op.BooleanNot:
                        setval(a := insn.op(0), not getval(a))
                    elif opc == Op.IntegerNot:
                        setval(a := insn.op(0), ~getval(a))
                    elif opc == Op.Neg:
                        setval(a := insn.op(0), -getval(a))
                    elif opc == Op.SetFlag:
                        condition, negated = insn.operands
                        self.jumpflag = getval(condition) ^ negated
                    elif opc == Op.JumpFlag:
                        if self.jumpflag:
                            ip = insn.operands[0]
                    elif opc == Op.PushEH:
                        exceptions.append(ExceptionHandler(*insn.operands))
                    elif opc == Op.PopEH:
                        tp = None
                        et = EHType(insn.operands[0])
                        eh = exceptions[-1]
                        if eh.current != et:
                            raise RuntimeError(F'Expected {eh.current} block to end, but {et} was ended instead.')
                        while tp is None:
                            if et is None:
                                raise RuntimeError
                            tp, et = {
                                EHType.Catch         : (eh.finally_one, EHType.Finally),
                                EHType.Try           : (eh.finally_one, EHType.Finally),
                                EHType.Finally       : (eh.finally_two, EHType.SecondFinally),
                                EHType.SecondFinally : (eh.handler_end, None),
                            }[et]
                        eh.current = et
                        ip = tp
                        if et is None:
                            exceptions.pop()
                    elif opc == Op.Inc:
                        setval(a := insn.op(0), getval(a) + 1)
                    elif opc == Op.Dec:
                        setval(a := insn.op(0), getval(a) - 1)
                    elif opc == Op.JumpPop1:
                        stack.pop()
                        ip = insn.operands[0]
                    elif opc == Op.JumpPop2:
                        stack.pop()
                        stack.pop()
                        ip = insn.operands[0]
                    else:
                        raise RuntimeError(F'Function contains invalid opcode at 0x{ip:X}.')
                except IFPSException as EE:
                    try:
                        eh = exceptions[-1]
                    except IndexError:
                        raise EE
                    et = EHType.Try
                    tp = None
                    while tp is None:
                        if et is None:
                            raise RuntimeError
                        tp, et = {
                            EHType.Try           : (eh.catch_error, EHType.Catch),
                            EHType.Catch         : (eh.finally_one, EHType.Finally),
                            EHType.Finally       : (eh.finally_two, EHType.SecondFinally),
                            EHType.SecondFinally : (eh.handler_end, None),
                        }[et]
                    if et is None:
                        raise EE
                    eh.current = et
                    ip = tp
                except AbortEmulation:
                    raise
                except EmulatorException:
                    raise
                except Exception as RE:
                    raise EmulatorException(
                        F'In {function.symbol} at 0x{insn.offset:X} (cycle {self.clock}), '
                        F'emulation of {insn!r} failed: {RE!s}')
            if ip is None:
                raise RuntimeError(F'Instruction pointer moved out of bounds to 0x{ip:X}.')

    external_symbols: ClassVar[
        Dict[str,                        # class name for methods or empty string for functions
        Dict[str, IFPSEmulatedFunction]] # method or function name to emulation info
    ] = CaseInsensitiveDict()

    def external(*args, static=True, __reg: dict = external_symbols, **kwargs):
        def decorator(pfn):
            signature = inspect.signature(pfn)
            name: str = kwargs.get('name', pfn.__name__)
            csep: str = '.'
            if csep not in name:
                csep = '__'
            classname, _, name = name.rpartition(csep)
            if (registry := __reg.get(classname)) is None:
                registry = __reg[classname] = CaseInsensitiveDict()

            docs = F'{classname}::{name}' if classname else name
            docs = F'An emulated handler for the external symbol {docs}.'
            pfn.__doc__ = docs

            void = kwargs.get('void', signature.return_annotation == signature.empty)
            parameters: List[bool] = []
            specs = iter(signature.parameters.values())
            if not static:
                next(specs)
            for spec in specs:
                try:
                    hint = eval(spec.annotation)
                except Exception as E:
                    raise RuntimeError(F'Invalid signature: {signature}') from E
                if not isinstance(hint, type):
                    hint = get_origin(hint)
                var = isinstance(hint, type) and issubclass(hint, Variable)
                parameters.append(var)
            registry[name] = e = IFPSEmulatedFunction(pfn, parameters, static, void)
            aliases = kwargs.get('alias', [])
            if isinstance(aliases, str):
                aliases = [aliases]
            for name in aliases:
                registry[name] = e
            if static:
                pfn = staticmethod(pfn)
            return pfn
        return decorator(args[0]) if args else decorator

    @external
    def TInputDirWizardPage__GetValues(this: object, k: int) -> str:
        return F'$InputDir{k}'

    @external
    def TInputFileWizardPage__GetValues(this: object, k: int) -> str:
        return F'$InputFile{k}'

    @external(static=False)
    def TPasswordEdit__SetText(self, this: object, value: str):
        if value:
            yield NewPassword(value)
        return value

    @external
    def kernel32__GetTickCount() -> int:
        return time.monotonic_ns() // 1_000_000

    @external
    def user32__GetSystemMetrics(index: int) -> int:
        if index == 80:
            return 1
        if index == 43:
            return 2
        return 0

    @external
    def IsX86Compatible() -> bool:
        return True

    @external(alias=[
        'sArm64',
        'IsArm32Compatible',
        'Debugging',
        'IsUninstaller',
    ])
    def Terminated() -> bool:
        return False

    @external(static=False)
    def IsAdmin(self) -> bool:
        return self.config.admin

    @external(static=False, alias='Sleep')
    def kernel32__Sleep(self, ms: int):
        seconds = ms / 1000.0
        self.seconds_slept += seconds
        time.sleep(seconds * self.config.sleep_scale)

    @external
    def Random(top: int) -> int:
        return random.randrange(0, top)

    @external(alias='StrGet')
    def WStrGet(string: Variable[str], index: int) -> str:
        if index <= 0:
            raise ValueError
        return string[index - 1:index]

    @external(alias='StrSet')
    def WStrSet(char: str, index: int, dst: Variable[str]):
        old = dst.get()
        index -= 1
        dst.set(old[:index] + char + old[index:])

    @external(static=False)
    def GetEnv(self, name: str) -> str:
        return self.config.environment.get(name, F'%{name}%')

    @external
    def Beep():
        pass

    @external(static=False)
    def Abort(self):
        if self.config.throw_abort:
            raise AbortEmulation

    @external
    def DirExists(path: str) -> bool:
        return True

    @external
    def ForceDirectories(path: str) -> bool:
        return True

    @external(alias='LoadStringFromLockedFile')
    def LoadStringFromFile(path: str, out: Variable[str]) -> bool:
        return True

    @external(alias='LoadStringsFromLockedFile')
    def LoadStringsFromFile(path: str, out: Variable[str]) -> bool:
        return True

    @cached_property
    def constant_map(self) -> dict[str, str]:
        cfg = self.config
        tmp = cfg.temp_path
        if not tmp:
            tmp = ''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ', k=5))
            tmp = RF'C:\Windows\Temp\IS-{tmp}'
        map = {
            'app'               : cfg.install_to,
            'win'               : R'C:\Windows',
            'sys'               : R'C:\Windows\System',
            'sysnative'         : R'C:\Windows\System32',
            'src'               : str(Path(cfg.executable).parent),
            'sd'                : R'C:',
            'commonpf'          : R'C:\Program Files',
            'commoncf'          : R'C:\Program Files\Common Files',
            'tmp'               : tmp,
            'commonfonts'       : R'C:\Windows\Fonts',
            'dao'               : R'C:\Program Files\Common Files\Microsoft Shared\DAO',
            'dotnet11'          : R'C:\Windows\Microsoft.NET\Framework\v1.1.4322',
            'dotnet20'          : R'C:\Windows\Microsoft.NET\Framework\v3.0',
            'dotnet2032'        : R'C:\Windows\Microsoft.NET\Framework\v3.0',
            'dotnet40'          : R'C:\Windows\Microsoft.NET\Framework\v4.0.30319',
            'dotnet4032'        : R'C:\Windows\Microsoft.NET\Framework\v4.0.30319',
            'group'             : RF'C:\Users\{cfg.user_name}\Start Menu\Programs\{cfg.inno_name}',
            'localappdata'      : RF'C:\Users\{cfg.user_name}\AppData\Local',
            'userappdata'       : RF'C:\Users\{cfg.user_name}\AppData\Roaming',
            'userdesktop'       : RF'C:\Users\{cfg.user_name}\Desktop',
            'userdocs'          : RF'C:\Users\{cfg.user_name}\Documents',
            'userfavourites'    : RF'C:\Users\{cfg.user_name}\Favourites',
            'usersavedgames'    : RF'C:\Users\{cfg.user_name}\Saved Games',
            'usersendto'        : RF'C:\Users\{cfg.user_name}\SendTo',
            'userstartmenu'     : RF'C:\Users\{cfg.user_name}\Start Menu',
            'userprograms'      : RF'C:\Users\{cfg.user_name}\Start Menu\Programs',
            'userstartup'       : RF'C:\Users\{cfg.user_name}\Start Menu\Programs\Startup',
            'usertemplates'     : RF'C:\Users\{cfg.user_name}\Templates',
            'commonappdata'     : R'C:\ProgramData',
            'commondesktop'     : R'C:\ProgramData\Microsoft\Windows\Desktop',
            'commondocs'        : R'C:\ProgramData\Microsoft\Windows\Documents',
            'commonstartmenu'   : R'C:\ProgramData\Microsoft\Windows\Start Menu',
            'commonprograms'    : R'C:\ProgramData\Microsoft\Windows\Start Menu\Programs',
            'commonstartup'     : R'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup',
            'commontemplates'   : R'C:\ProgramData\Microsoft\Windows\Templates',
            'cmd'               : R'C:\Windows\System32\cmd.exe',
            'computername'      : cfg.host_name,
            'groupname'         : cfg.inno_name,
            'hwnd'              : '0',
            'wizardhwnd'        : '0',
            'language'          : cfg.language,
            'srcexe'            : cfg.executable,
            'sysuserinfoname'   : '{sysuserinfoname}',
            'sysuserinfoorg'    : '{sysuserinfoorg}',
            'userinfoname'      : '{userinfoname}',
            'userinfoorg'       : '{userinfoorg}',
            'userinfoserial'    : '{userinfoserial}',
            'username'          : cfg.user_name,
            'log'               : '',
        }

        if (inno := self.inno) is None or (inno.setup_info.Header.Flags & Flags.Uninstallable):
            map['uninstallexe'] = RF'{cfg.install_to}\unins000.exe'

        if cfg.x64:
            map['syswow64'] = R'C:\Windows\SysWOW64'
            map['commonpf32'] = R'C:\Program Files (x86)'
            map['commoncf32'] = R'C:\Program Files (x86)\Common Files'
            map['commonpf64'] = R'C:\Program Files'
            map['commoncf64'] = R'C:\Program Files\Common Files'
            map['dotnet2064'] = R'C:\Windows\Microsoft.NET\Framework64\v3.0'
            map['dotnet4064'] = R'C:\Windows\Microsoft.NET\Framework64\v4.0.30319'
        else:
            map['syswow64'] = R'C:\Windows\System32'
            map['commonpf32'] = R'C:\Program Files'
            map['commoncf32'] = R'C:\Program Files\Common Files'

        if cfg.windows_os_version[0] >= 10:
            map['userfonts'] = RF'{map["localappdata"]}\Microsoft\Windows\Fonts'

        if cfg.windows_os_version[0] >= 7:
            map['usercf'] = RF'{map["localappdata"]}\Programs\Common'
            map['userpf'] = RF'{map["localappdata"]}\Programs'

        for auto_var, admin_var, user_var in [
            ('autoappdata',       'commonappdata',       'userappdata',   ), # noqa
            ('autocf',            'commoncf',            'usercf',        ), # noqa
            ('autocf32',          'commoncf32',          'usercf',        ), # noqa
            ('autocf64',          'commoncf64',          'usercf',        ), # noqa
            ('autodesktop',       'commondesktop',       'userdesktop',   ), # noqa
            ('autodocs',          'commondocs',          'userdocs',      ), # noqa
            ('autofonts',         'commonfonts',         'userfonts',     ), # noqa
            ('autopf',            'commonpf',            'userpf',        ), # noqa
            ('autopf32',          'commonpf32',          'userpf',        ), # noqa
            ('autopf64',          'commonpf64',          'userpf',        ), # noqa
            ('autoprograms',      'commonprograms',      'userprograms',  ), # noqa
            ('autostartmenu',     'commonstartmenu',     'userstartmenu', ), # noqa
            ('autostartup',       'commonstartup',       'userstartup',   ), # noqa
            ('autotemplates',     'commontemplates',     'usertemplates', ), # noqa
        ]:
            try:
                map[auto_var] = map[admin_var] if cfg.admin else map[user_var]
            except KeyError:
                continue

        for legacy, new in [
            ('cf',     'commoncf',    ), # noqa
            ('cf32',   'commoncf32',  ), # noqa
            ('cf64',   'commoncf64',  ), # noqa
            ('fonts',  'commonfonts', ), # noqa
            ('pf',     'commonpf',    ), # noqa
            ('pf32',   'commonpf32',  ), # noqa
            ('pf64',   'commonpf64',  ), # noqa
            ('sendto', 'usersendto',  ), # noqa
        ]:
            try:
                map[legacy] = map[new]
            except KeyError:
                continue

        return map

    @external(static=False)
    def ExpandConstant(self, string: str) -> str:
        return self.expand_constant(string)

    @external(static=False)
    def ExpandConstantEx(self, string: str, custom_var: str, custom_val: str) -> str:
        return self.expand_constant(string, custom_var, custom_val)

    def expand_constant(
        self,
        string: str,
        custom_var: Optional[str] = None,
        custom_val: Optional[str] = None,
        unescape: bool = False
    ):
        config = self.config
        expand = partial(self.expand_constant, unescape=True)
        string = re.sub(r'(\{\{.*?\}(?!\}))', '\\1}', string)

        with io.StringIO() as result:
            constants = self.constant_map
            formatter = Formatter()
            backslash = False
            try:
                parsed = list(formatter.parse(string))
            except ValueError as VE:
                raise IFPSException(F'invalid format string: {string!r}', VE) from VE
            for prefix, spec, modifier, conversion in parsed:
                if backslash and prefix[:1] == '\\':
                    prefix = prefix[1:]
                if unescape:
                    prefix = unquote(prefix)
                result.write(prefix)
                if spec is None:
                    continue
                elif spec == '\\':
                    if modifier or conversion:
                        raise IFPSException('Invalid format string.', ValueError(string))
                    value = spec
                elif spec == custom_var:
                    value = custom_val
                elif spec.startswith('%'):
                    name, p, default = spec[1:].partition('|')
                    name = expand(name)
                    default = expand(default)
                    try:
                        value = config.environment[name]
                    except KeyError:
                        value = default if p else F'%{name}%'
                elif spec == 'drive':
                    value = self.ExtractFileDrive(expand(modifier))
                elif spec == 'ini':
                    # {ini:Filename,Section,Key|DefaultValue}
                    _, _, default = modifier.partition('|')
                    value = expand(default)
                elif spec == 'code':
                    # {code:FunctionName|Param}
                    symbol, _, param = modifier.partition('|')
                    param = expand(param)
                    try:
                        function = self.symbols[symbol]
                    except KeyError as KE:
                        raise IFPSException(F'String formatter references missing function {symbol}.', KE) from KE
                    emulation = self.emulate_function(function, param)
                    value = str(emulation.value)
                elif spec == 'cm':
                    # {cm:LaunchProgram,Inno Setup}
                    # The example above translates to "Launch Inno Setup" if English is the active language.
                    name, _, placeholders = modifier.partition(',')
                    value = self.CustomMessage(expand(name))
                    if placeholders:
                        def _placeholder(match: re.Match[str]):
                            try:
                                return placeholders[int(match[1]) - 1]
                            except Exception:
                                return match[0]
                        placeholders = [ph.strip() for ph in placeholders.split(',')]
                        value = re.sub('(?<!%)%([1-9]\\d*)', _placeholder, value)
                elif spec == 'reg':
                    # {reg:HKXX\SubkeyName,ValueName|DefaultValue}
                    _, _, default = modifier.partition('|')
                    value = expand(default)
                elif spec == 'param':
                    # {param:ParamName|DefaultValue}
                    _, _, default = modifier.partition('|')
                    value = expand(default)
                else:
                    try:
                        value = constants[spec]
                    except KeyError as KE:
                        raise IFPSException(F'invalid format field {spec}', KE) from KE
                backslash = value.endswith('\\')
                result.write(value)
            return result.getvalue()

    @external
    def DeleteFile(path: str) -> bool:
        return True

    @external
    def FileExists(file_name: str) -> bool:
        return False

    @external
    def Log(log: str):
        ...

    @external
    def Inc(p: Variable[Variable[int]]):
        p.set(p.get() + 1)

    @external
    def Dec(p: Variable[Variable[int]]):
        p.set(p.get() - 1)

    @external
    def FindFirst(file_name: str, frec: Variable) -> bool:
        return False

    @external
    def Trunc(x: float) -> float:
        return math.trunc(x)

    @external
    def GetSpaceOnDisk(
        path: str,
        in_megabytes: bool,
        avail: Variable[int],
        space: Variable[int],
    ) -> bool:
        _a = 3_000_000
        _t = 5_000_000
        if not in_megabytes:
            _a *= 1000
            _t *= 1000
        avail.set(_a)
        space.set(_t)
        return True

    @external
    def GetSpaceOnDisk64(
        path: str,
        avail: Variable[int],
        space: Variable[int],
    ) -> bool:
        avail.set(3_000_000_000)
        space.set(5_000_000_000)
        return True

    @external
    def Exec(
        exe: str,
        cmd: str,
        cwd: str,
        show: int,
        wait: int,
        out: Variable[int],
    ) -> bool:
        out.set(0)
        return True

    @external
    def GetCmdTail() -> str:
        return ''

    @external
    def ParamCount() -> int:
        return 0

    @external
    def ParamStr(index: int) -> str:
        return ''

    @external
    def ActiveLanguage() -> str:
        return 'en'

    @external(static=False)
    def CustomMessage(self, msg_name: str) -> str:
        by_language = {}
        for msg in self.inno.setup_info.Messages:
            if msg.EncodedName == msg_name:
                lng = msg.get_language_value().Name
                if lng == self.config.language:
                    return msg.Value
                by_language[lng] = msg.Value
        try:
            return by_language[0]
        except KeyError:
            pass
        try:
            return next(iter(by_language.values()))
        except StopIteration:
            raise IFPSException(F'Custom message with name {msg_name} not found.')

    @external
    def FmtMessage(fmt: str, args: List[str]) -> str:
        fmt = fmt.replace('{', '{{')
        fmt = fmt.replace('}', '}}')
        fmt = '%'.join(re.sub('%(\\d+)', '{\\1}', p) for p in fmt.split('%%'))
        return fmt.format(*args)

    @external
    def Format(fmt: str, args: List[Union[str, int, float]]) -> str:
        try:
            formatted = fmt % tuple(args)
        except Exception:
            raise IFPSException('invalid format')
        else:
            return formatted

    @external(static=False)
    def SetupMessage(self, id: int) -> str:
        try:
            return self.inno.setup_info.Messages[id].Value
        except (AttributeError, IndexError):
            return ''

    @external(static=False, alias=['Is64BitInstallMode', 'IsX64Compatible', 'IsX64OS'])
    def IsWin64(self) -> bool:
        return self.config.x64

    @external(static=False)
    def IsX86OS(self) -> bool:
        return not self.config.x64

    @external
    def RaiseException(msg: str):
        raise IFPSException(msg)

    @external(static=False)
    def ProcessorArchitecture(self) -> int:
        if self.config.x64:
            return TSetupProcessorArchitecture.paX64.value
        else:
            return TSetupProcessorArchitecture.paX86.value

    @external(static=False)
    def GetUserNameString(self) -> str:
        return self.config.user_name

    @external(static=False)
    def GetComputerNameString(self) -> str:
        return self.config.host_name

    @external(static=False)
    def GetUILanguage(self) -> str:
        return self.config.lcid

    @external
    def GetArrayLength(array: Variable) -> int:
        array = array.deref()
        return len(array)

    @external
    def SetArrayLength(array: Variable, n: int):
        a = array.deref()
        a.resize(n)

    @external(static=False)
    def WizardForm(self) -> object:
        return self

    @external
    def Unassigned() -> None:
        return None

    @external
    def Null() -> None:
        return None

    @external(static=False)
    def Set8087CW(self, cw: int):
        self.fpucw = FPUControl(cw)

    @external(static=False)
    def Get8087CW(self) -> int:
        return self.fpucw.value

    @external(static=False)
    def GetDateTimeString(
        self,
        fmt: str,
        date_separator: str,
        time_separator: str,
    ) -> str:

        now = self.config.start_time
        now = now + timedelta(
            milliseconds=(self.config.milliseconds_per_instruction * self.clock))
        now = now + timedelta(seconds=self.seconds_slept)

        date_separator = date_separator.lstrip('\0')
        time_separator = time_separator.lstrip('\0')

        def dt(m: re.Match[str]):
            spec = m[1]
            ampm = m[2]
            if ampm:
                am, _, pm = ampm.partition('/')
                spec = spec.upper()
                suffix = now.strftime('%p').lower()
                suffix = {'am': am, 'pm': pm}[suffix]
            else:
                suffix = ''
            if spec == 'dddddd' or spec == 'ddddd':
                return now.date.isoformat()
            if spec == 't':
                return now.time().isoformat('minutes')
            if spec == 'tt':
                return now.time().isoformat('seconds')
            if spec == 'd':
                return str(now.day)
            if spec == 'm':
                return str(now.month)
            if spec == 'h':
                return str(now.hour)
            if spec == 'n':
                return str(now.minute)
            if spec == 's':
                return str(now.second)
            if spec == 'H':
                return now.strftime('%I').lstrip('0') + suffix
            if spec == '/':
                return date_separator or spec
            if spec == ':':
                return time_separator or spec
            return now.strftime({
                'dddd'  : '%A',
                'ddd'   : '%a',
                'dd'    : '%d',
                'mmmm'  : '%B',
                'mmm'   : '%b',
                'mm'    : '%m',
                'yyyy'  : '%Y',
                'yy'    : '%y',
                'hh'    : '%H',
                'HH'    : '%I' + suffix,
                'nn'    : '%M',
                'ss'    : '%S',
            }.get(spec, m[0]))

        split = re.split(F'({formats.string!s})', fmt)
        for k in range(0, len(split), 2):
            split[k] = re.sub('([dmyhnst]+)((?:[aA][mM]?/[pP][mM]?)?)', dt, split[k])
        for k in range(1, len(split), 2):
            split[k] = split[k][1:-1]
        return ''.join(split)

    @external
    def Chr(b: int) -> str:
        return chr(b)

    @external
    def Ord(c: str) -> int:
        return ord(c)

    @external
    def Copy(string: str, index: int, count: int) -> str:
        index -= 1
        return string[index:index + count]

    @external
    def Length(string: str) -> int:
        return len(string)

    @external(alias='AnsiLowercase')
    def Lowercase(string: str) -> str:
        return string.lower()

    @external(alias='AnsiUppercase')
    def Uppercase(string: str) -> str:
        return string.upper()

    @external
    def StringOfChar(c: str, count: int) -> str:
        return c * count

    @external
    def Delete(string: Variable[str], index: int, count: int):
        index -= 1
        old = string.get()
        string.set(old[:index] + old[index + count:])

    @external
    def Insert(string: str, dest: Variable[str], index: int):
        index -= 1
        old = dest.get()
        dest.set(old[:index] + string + old[index:])

    @external(static=False)
    def StringChange(self, string: Variable[str], old: str, new: str) -> int:
        return self.StringChangeEx(string, old, new, False)

    @external
    def StringChangeEx(string: Variable[str], old: str, new: str, _: bool) -> int:
        haystack = string.get()
        count = haystack.count(old)
        string.set(haystack.replace(old, new))
        return count

    @external
    def Pos(string: str, sub: str) -> int:
        return string.find(sub) + 1

    @external
    def AddQuotes(string: str) -> str:
        if string and (string[0] != '"' or string[~0] != '"') and ' ' in string:
            string = F'"{string}"'
        return string

    @external
    def RemoveQuotes(string: str) -> str:
        if string and string[0] == '"' and string[~0] == '"':
            string = string[1:-1]
        return string

    @external(static=False)
    def CompareText(self, a: str, b: str) -> int:
        return self.CompareStr(a.casefold(), b.casefold())

    @external
    def CompareStr(a: str, b: str) -> int:
        if a > b:
            return +1
        if a < b:
            return -1
        return 0

    @external
    def SameText(a: str, b: str) -> bool:
        return a.casefold() == b.casefold()

    @external
    def SameStr(a: str, b: str) -> bool:
        return a == b

    @external
    def IsWildcard(pattern: str) -> bool:
        return '*' in pattern or '?' in pattern

    @external
    def WildcardMatch(text: str, pattern: str) -> bool:
        return fnmatch.fnmatch(text, pattern)

    @external
    def Trim(string: str) -> str:
        return string.strip()

    @external
    def TrimLeft(string: str) -> str:
        return string.lstrip()

    @external
    def TrimRight(string: str) -> str:
        return string.rstrip()

    @external
    def StringJoin(sep: str, values: List[str]) -> str:
        return sep.join(values)

    @external
    def StringSplitEx(string: str, separators: List[str], quote: str, how: TSplitType) -> List[str]:
        if not quote:
            parts = [string]
        else:
            quote = re.escape(quote)
            parts = re.split(F'({quote}.*?{quote})', string)
        sep = '|'.join(re.escape(s) for s in separators)
        out = []
        if how == TSplitType.stExcludeEmpty:
            sep = F'(?:{sep})+'
        for k in range(0, len(parts)):
            if k & 1 == 1:
                out.append(parts[k])
                continue
            out.extend(re.split(sep, string))
        if how == TSplitType.stExcludeLastEmpty:
            for k in reversed(range(len(out))):
                if not out[k]:
                    out.pop(k)
                    break
        return out

    @external(static=False)
    def StringSplit(self, string: str, separators: List[str], how: TSplitType) -> List[str]:
        return self.StringSplitEx(string, separators, None, how)

    @external(alias='StrToInt64')
    def StrToInt(s: str) -> int:
        return int(s)

    @external(alias='StrToInt64Def')
    def StrToIntDef(s: str, d: int) -> int:
        try:
            return int(s)
        except Exception:
            return d

    @external
    def StrToFloat(s: str) -> float:
        return float(s)

    @external(alias='FloatToStr')
    def IntToStr(i: int) -> str:
        return str(i)

    @external
    def StrToVersion(s: str, v: Variable[int]) -> bool:
        try:
            packed = bytes(map(int, s.split('.')))
        except Exception:
            return False
        if len(packed) != 4:
            return False
        v.set(int.from_bytes(packed, 'little'))
        return True

    @external
    def CharLength(string: str, index: int) -> int:
        return 1

    @external
    def AddBackslash(string: str) -> str:
        if string and string[~0] != '\\':
            string = F'{string}\\'
        return string

    @external
    def AddPeriod(string: str) -> str:
        if string and string[~0] != '.':
            string = F'{string}.'
        return string

    @external(static=False)
    def RemoveBackslashUnlessRoot(self, string: str) -> str:
        path = Path(string)
        if len(path.parts) == 1:
            return str(path)
        return self.RemoveBackslash(string)

    @external
    def RemoveBackslash(string: str) -> str:
        return string.rstrip('\\/')

    @external
    def ChangeFileExt(name: str, ext: str) -> str:
        if not ext.startswith('.'):
            ext = F'.{ext}'
        return str(Path(name).with_suffix(ext))

    @external
    def ExtractFileExt(name: str) -> str:
        return Path(name).suffix

    @external(alias='ExtractFilePath')
    def ExtractFileDir(name: str) -> str:
        dirname = str(Path(name).parent)
        return '' if dirname == '.' else dirname

    @external
    def ExtractFileName(name: str) -> str:
        if name:
            name = Path(name).parts[-1]
        return name

    @external
    def ExtractFileDrive(name: str) -> str:
        if name:
            parts = Path(name).parts
            if len(parts) >= 2 and parts[0] == '\\' and parts[1] == '?':
                parts = parts[2:]
            if parts[0] == '\\':
                if len(parts) >= 3:
                    return '\\'.join(parts[:3])
            else:
                root = parts[0]
                if len(root) == 2 and root[1] == ':':
                    return root
        return ''

    @external
    def ExtractRelativePath(base: str, dst: str) -> str:
        return str(Path(dst).relative_to(base))

    @external(static=False, alias='ExpandUNCFileName')
    def ExpandFileName(self, name: str) -> str:
        if self.ExtractFileDrive(name):
            return name
        return str(self.config.cwd / name)

    @external
    def SetLength(string: Variable[str], size: int):
        old = string.get()
        old = old.ljust(size, '\0')
        string.set(old[:size])

    @external(alias='OemToCharBuff')
    def CharToOemBuff(string: str) -> str:
        # TODO
        return string

    @external
    def Utf8Encode(string: str) -> str:
        return string.encode('utf8').decode('latin1')

    @external
    def Utf8Decode(string: str) -> str:
        return string.encode('latin1').decode('utf8')

    @external
    def GetMD5OfString(string: str) -> str:
        return hashlib.md5(string.encode('latin1')).hexdigest()

    @external
    def GetMD5OfUnicodeString(string: str) -> str:
        return hashlib.md5(string.encode('utf8')).hexdigest()

    @external
    def GetSHA1OfString(string: str) -> str:
        return hashlib.sha1(string.encode('latin1')).hexdigest()

    @external
    def GetSHA1OfUnicodeString(string: str) -> str:
        return hashlib.sha1(string.encode('utf8')).hexdigest()

    @external
    def GetSHA256OfString(string: str) -> str:
        return hashlib.sha256(string.encode('latin1')).hexdigest()

    @external
    def GetSHA256OfUnicodeString(string: str) -> str:
        return hashlib.sha256(string.encode('utf8')).hexdigest()

    @external
    def SysErrorMessage(code: int) -> str:
        return F'[description for error {code:08X}]'

    @external
    def MinimizePathName(path: str, font: object, max_len: int) -> str:
        return path

    @external(static=False)
    def CheckForMutexes(self, mutexes: str) -> bool:
        return any(m in self.mutexes for m in mutexes.split(','))

    @external(static=False)
    def CreateMutex(self, name: str):
        if self.config.log_mutexes:
            yield NewMutex(name)
        self.mutexes.add(name)

    @external(static=False)
    def GetWinDir(self) -> str:
        return self.expand_constant('{win}')

    @external(static=False)
    def GetSystemDir(self) -> str:
        return self.expand_constant('{sys}')

    @external(static=False)
    def GetWindowsVersion(self) -> int:
        version = int.from_bytes(
            struct.pack('>BBH', *self.config.windows_os_version), 'big')
        return version

    @external(static=False)
    def GetWindowsVersionEx(self, tv: Variable[Union[int, bool]]):
        tv[0], tv[1], tv[2] = self.config.windows_os_version # noqa
        tv[3], tv[4]        = self.config.windows_sp_version # noqa
        tv[5], tv[6], tv[7] = True, 0, 0

    @external(static=False)
    def GetWindowsVersionString(self) -> str:
        return '{0}.{1:02d}.{2:04d}'.format(*self.config.windows_os_version)

    @external
    def CreateOleObject(name: str) -> OleObject:
        return OleObject(name)

    @external
    def GetActiveOleObject(name: str) -> OleObject:
        return OleObject(name)

    @external
    def IDispatchInvoke(ole: OleObject, prop_set: bool, name: str, value: Any) -> int:
        return 0

    @external
    def FindWindowByClassName(name: str) -> int:
        return 0

    @external(static=False)
    def WizardSilent(self) -> bool:
        return self.config.wizard_silent

    @external(static=False)
    def SizeOf(self, var: Variable) -> int:
        if var.pointer:
            return (self.config.x64 + 1) * 4
        if var.container:
            return sum(self.SizeOf(x) for x in var.data)
        return var.type.code.width

    del external


class InnoSetupEmulator(IFPSEmulator):
    """
    A specialized `refinery.lib.emulator.IFPSEmulator` that can emulate the InnoSetup installation
    with a focus on continuing execution as much as possible.
    """

    def emulate_installation(self, password=''):
        """
        To the best of the author's knowledge, this function emulates the sequence of calls into
        the script that the IFPS runtime would make during a setup install.
        """

        class SetupDispatcher:

            InitializeSetup: Callable
            InitializeWizard: Callable
            CurStepChanged: Callable
            ShouldSkipPage: Callable
            CurPageChanged: Callable
            PrepareToInstall: Callable
            CheckPassword: Callable
            NextButtonClick: Callable
            DeinitializeSetup: Callable

            def __getattr__(_, name):
                if pfn := self.symbols.get(name):
                    def emulated(*a):
                        return (yield from self.emulate_function(pfn, *a))
                else:
                    def emulated(*a):
                        yield from ()
                        return False
                return emulated

        Setup = SetupDispatcher()

        yield from Setup.InitializeSetup()
        yield from Setup.InitializeWizard()
        yield from Setup.CurStepChanged(TSetupStep.ssPreInstall)

        for page in PageID:

            skip = yield from Setup.ShouldSkipPage(page)

            if not skip:
                yield from Setup.CurPageChanged(page)
                if page == PageID.wpPreparing:
                    yield from Setup.PrepareToInstall(False)
                if page == PageID.wpPassword:
                    yield from Setup.CheckPassword(password)

            yield from Setup.NextButtonClick(page)

            if page == PageID.wpPreparing:
                yield from Setup.CurStepChanged(TSetupStep.ssInstall)
            if page == PageID.wpInfoAfter:
                yield from Setup.CurStepChanged(TSetupStep.ssPostInstall)

        yield from Setup.CurStepChanged(TSetupStep.ssDone)
        yield from Setup.DeinitializeSetup()

    def unimplemented(self, function: Function):
        """
        Any unimplemented function is essentially skipped. Any arguments passed by reference and
        all return values that are of type integer are set to `1` in an attempt to indicate success
        wherever possible.
        """
        decl = function.decl
        if decl is None:
            return
        if not decl.void:
            rc = 1
            rv = self.stack[-1]
            if not rv.container:
                rt = rv.type.py_type()
                if isinstance(rt, type) and issubclass(rt, int):
                    rv.set(1)
        else:
            rc = 0
        for k in range(rc, rc + len(decl.parameters)):
            ptr: Variable[Variable] = self.stack[-k]
            if not ptr.pointer:
                continue
            var = ptr.deref()
            if var.container:
                continue
            vt = var.type.py_type()
            if isinstance(vt, type) and issubclass(vt, int):
                var.set(1)

Classes

class OleObject (name)

A dummy object representing an OLE interface created by an IFPS script. All it does so far is to remember the name of the object that was requested.

Expand source code Browse git
class OleObject:
    """
    A dummy object representing an OLE interface created by an IFPS script. All it does so far is
    to remember the name of the object that was requested.
    """
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return F'OleObject({self.name!r})'

    def __str__(self):
        return self.name
class Variable (type, spec=None, path=(), data=None)

This class represents a global or stack variable in the IFPS runtime.

Expand source code Browse git
class Variable(VariableBase, Generic[_T]):
    """
    This class represents a global or stack variable in the IFPS runtime.
    """
    data: Optional[Union[List[Variable], _T]]
    """
    The variable's value. This is a list of `refinery.lib.inno.emulator.Variable`s for container
    types, a `refinery.lib.inno.emulator.Variable` for pointer types, and a basic type otherwise.
    """
    path: Tuple[int, ...]
    """
    A tuple of integers that specify the seuqnce of indices required to access it, relative to the
    base variable given via `spec`.
    """

    __slots__ = (
        'data',
        'path',
        '_int_size',
        '_int_mask',
        '_int_bits',
        '_int_good',
    )

    @property
    def container(self):
        """
        A boolean indicating whether the given variable is a container.
        """
        return self.type.container

    @property
    def pointer(self):
        """
        A boolean indicating whether the given variable is a pointer.
        """
        return self.type.code == TC.Pointer

    def __len__(self):
        return len(self.data)

    def __bool__(self):
        return True

    def __getitem__(self, key: int):
        var = self.deref()
        if var.container:
            return var.at(key).get()
        else:
            return var.data[key]

    def __setitem__(self, key: int, v: _T):
        var = self.deref()
        if var.container:
            var.at(key).set(v)
        else:
            var.data[key] = var._wrap(v)

    def at(self, k: int):
        """
        Provides index access for the variable. If the variable is a pointer, it is dereferenced
        before accessing the data.
        """
        return self.deref().data[k]

    def deref(var):
        """
        Dereferences the variable until it is no longer a pointer and returns the result. If the
        variable is not a pointer, this function returns the variable itself.
        """
        while True:
            val = var.data
            if not isinstance(val, Variable):
                return var
            var = val

    def __init__(
        self,
        type: IFPSType,
        spec: Optional[VariableSpec] = None,
        path: Tuple[int, ...] = (),
        data: Optional[Union[_T, List]] = None
    ):
        super().__init__(type, spec)
        self.path = path

        self._int_size = _size = {
            TC.U08: +1,
            TC.U16: +1,
            TC.U32: +1,
            TC.S08: -1,
            TC.S16: -1,
            TC.S32: -1,
            TC.S64: -1,
        }.get((code := type.code), 0) * code.width
        if _size:
            bits = abs(_size) * 8
            umax = (1 << bits)
            self._int_bits = bits
            self._int_mask = umax - 1
            if _size < 0:
                self._int_good = range(-(umax >> 1), (umax >> 1))
            else:
                self._int_good = range(umax)
        else:
            self._int_mask = NoMask
            self._int_bits = INF
            self._int_good = AST

        if data is None:
            self.setdefault()
        else:
            self.set(data)

    def setdefault(self):
        """
        Set this variable's data to the default value for its type. This also initializes the
        values of any contained variables recursively.
        """
        spec = self.spec
        path = self.path

        def default(type: IFPSType, *sub_path):
            if isinstance(type, TRecord):
                return [Variable(t, spec, (*path, *sub_path, k)) for k, t in enumerate(type.members)]
            if isinstance(type, TStaticArray):
                t = type.type
                return [Variable(t, spec, (*path, *sub_path, k)) for k in range(type.size)]
            if isinstance(type, TArray):
                return []
            if sub_path:
                return Variable(type, spec, (*path, *sub_path))
            else:
                return type.default()

        self.data = default(self.type)

    def _wrap(self, value: Union[Value, _T], key: Optional[int] = None) -> _T:
        if (t := self.type.py_type(key)) and not isinstance(value, t):
            if issubclass(t, int):
                if isinstance(value, str) and len(value) == 1:
                    return ord(value[0])
                if isinstance(value, float):
                    return int(value)
            elif isinstance(value, int):
                if issubclass(t, str):
                    return chr(value)
                if issubclass(t, float):
                    return float(value)
            raise TypeError(F'Assigning value {value!r} to variable of type {self.type}.')
        if s := self._int_size and value not in self._int_good:
            mask = self._int_mask
            value &= mask
            if s < 0 and (value >> (self._int_bits - 1)):
                value = -(-value & mask)
        return value

    def resize(self, n: int):
        """
        This function is only valid for container type variables. It re-sizes the data list to
        ensure that the container stores exactly `n` sub-variables.
        """
        t = self.type
        m = n - len(self.data)
        if t.code != TC.Array:
            if t.code not in (TC.StaticArray, TC.Record):
                raise TypeError
            if n == t.size:
                return
            raise ValueError(F'Attempt to resize {t} of size {t.size} to {n}.')
        if m <= 0:
            del self.data[n:]
            return
        for k in range(m):
            self.data.append(Variable(t.type, self.spec, (*self.path, k)))

    def setptr(self, var: Variable, copy: bool = False):
        """
        This method is used to point a pointer variable to a target. This is different from calling
        the `refinery.lib.inno.emulator.Variable.set` method as the latter would try to dereference
        the pointer and assign to its target; this method sets the value of the pointer itself.
        """
        if not self.pointer:
            raise TypeError
        if not isinstance(var, Variable):
            raise TypeError
        if copy:
            var = Variable(var.type, data=var.get())
        self.data = var

    def set(self, value: Union[_T, Sequence, Variable]):
        """
        Assign a new value to the variable. This can either be an immediate value or a variable.
        For container types, it can also be a sequence of those.
        """
        if isinstance(value, Variable):
            value = value.get()
        elif isinstance(value, (Enum, Value)):
            value = value.value
        if self.pointer:
            return self.deref().set(value)
        elif self.container:
            if not isinstance(value, (list, tuple)):
                raise TypeError
            self.resize(len(value))
            for k, v in enumerate(value):
                self.data[k].set(v)
        else:
            self.data = self._wrap(value)

    def get(self) -> _T:
        """
        Return a representation of this variable that consists only of base types. For example, the
        result for a container type will not be a list of `refinery.lib.inno.emulator.Variable`s,
        but a list of their contents.
        """
        if self.pointer:
            return self.deref().get()
        if self.container:
            data: List[Variable] = self.data
            return [v.get() for v in data]
        return self.data

    @property
    def name(self):
        """
        Return the name of the variable as given by its spec.
        """
        if self.spec is None:
            return 'Unbound'
        name = F'{self.spec!s}'
        for k in self.path:
            name = F'{name}[{k}]'
        return name

    def __repr__(self):
        rep = self.name
        if (val := self.data) is None:
            return rep
        if self.type.code is TC.Set:
            val = F'{val:b}'
        elif self.pointer:
            val: Variable
            return F'{rep} -> {val.name}'
        elif isinstance(val, (str, int, float, list)):
            val = repr(self.get())
        else:
            return rep
        return F'{rep} = {val}'

Ancestors

Instance variables

var container

A boolean indicating whether the given variable is a container.

Expand source code Browse git
@property
def container(self):
    """
    A boolean indicating whether the given variable is a container.
    """
    return self.type.container
var pointer

A boolean indicating whether the given variable is a pointer.

Expand source code Browse git
@property
def pointer(self):
    """
    A boolean indicating whether the given variable is a pointer.
    """
    return self.type.code == TC.Pointer
var name

Return the name of the variable as given by its spec.

Expand source code Browse git
@property
def name(self):
    """
    Return the name of the variable as given by its spec.
    """
    if self.spec is None:
        return 'Unbound'
    name = F'{self.spec!s}'
    for k in self.path:
        name = F'{name}[{k}]'
    return name
var data

The variable's value. This is a list of Variables for container types, a Variable for pointer types, and a basic type otherwise.

var path

A tuple of integers that specify the seuqnce of indices required to access it, relative to the base variable given via spec.

Methods

def at(self, k)

Provides index access for the variable. If the variable is a pointer, it is dereferenced before accessing the data.

Expand source code Browse git
def at(self, k: int):
    """
    Provides index access for the variable. If the variable is a pointer, it is dereferenced
    before accessing the data.
    """
    return self.deref().data[k]
def deref(var)

Dereferences the variable until it is no longer a pointer and returns the result. If the variable is not a pointer, this function returns the variable itself.

Expand source code Browse git
def deref(var):
    """
    Dereferences the variable until it is no longer a pointer and returns the result. If the
    variable is not a pointer, this function returns the variable itself.
    """
    while True:
        val = var.data
        if not isinstance(val, Variable):
            return var
        var = val
def setdefault(self)

Set this variable's data to the default value for its type. This also initializes the values of any contained variables recursively.

Expand source code Browse git
def setdefault(self):
    """
    Set this variable's data to the default value for its type. This also initializes the
    values of any contained variables recursively.
    """
    spec = self.spec
    path = self.path

    def default(type: IFPSType, *sub_path):
        if isinstance(type, TRecord):
            return [Variable(t, spec, (*path, *sub_path, k)) for k, t in enumerate(type.members)]
        if isinstance(type, TStaticArray):
            t = type.type
            return [Variable(t, spec, (*path, *sub_path, k)) for k in range(type.size)]
        if isinstance(type, TArray):
            return []
        if sub_path:
            return Variable(type, spec, (*path, *sub_path))
        else:
            return type.default()

    self.data = default(self.type)
def resize(self, n)

This function is only valid for container type variables. It re-sizes the data list to ensure that the container stores exactly n sub-variables.

Expand source code Browse git
def resize(self, n: int):
    """
    This function is only valid for container type variables. It re-sizes the data list to
    ensure that the container stores exactly `n` sub-variables.
    """
    t = self.type
    m = n - len(self.data)
    if t.code != TC.Array:
        if t.code not in (TC.StaticArray, TC.Record):
            raise TypeError
        if n == t.size:
            return
        raise ValueError(F'Attempt to resize {t} of size {t.size} to {n}.')
    if m <= 0:
        del self.data[n:]
        return
    for k in range(m):
        self.data.append(Variable(t.type, self.spec, (*self.path, k)))
def setptr(self, var, copy=False)

This method is used to point a pointer variable to a target. This is different from calling the Variable.set() method as the latter would try to dereference the pointer and assign to its target; this method sets the value of the pointer itself.

Expand source code Browse git
def setptr(self, var: Variable, copy: bool = False):
    """
    This method is used to point a pointer variable to a target. This is different from calling
    the `refinery.lib.inno.emulator.Variable.set` method as the latter would try to dereference
    the pointer and assign to its target; this method sets the value of the pointer itself.
    """
    if not self.pointer:
        raise TypeError
    if not isinstance(var, Variable):
        raise TypeError
    if copy:
        var = Variable(var.type, data=var.get())
    self.data = var
def set(self, value)

Assign a new value to the variable. This can either be an immediate value or a variable. For container types, it can also be a sequence of those.

Expand source code Browse git
def set(self, value: Union[_T, Sequence, Variable]):
    """
    Assign a new value to the variable. This can either be an immediate value or a variable.
    For container types, it can also be a sequence of those.
    """
    if isinstance(value, Variable):
        value = value.get()
    elif isinstance(value, (Enum, Value)):
        value = value.value
    if self.pointer:
        return self.deref().set(value)
    elif self.container:
        if not isinstance(value, (list, tuple)):
            raise TypeError
        self.resize(len(value))
        for k, v in enumerate(value):
            self.data[k].set(v)
    else:
        self.data = self._wrap(value)
def get(self)

Return a representation of this variable that consists only of base types. For example, the result for a container type will not be a list of Variables, but a list of their contents.

Expand source code Browse git
def get(self) -> _T:
    """
    Return a representation of this variable that consists only of base types. For example, the
    result for a container type will not be a list of `refinery.lib.inno.emulator.Variable`s,
    but a list of their contents.
    """
    if self.pointer:
        return self.deref().get()
    if self.container:
        data: List[Variable] = self.data
        return [v.get() for v in data]
    return self.data

Inherited members

class NeedSymbol (*args, **kwargs)

An exception raised by IFPSEmulator if the runtime calls out to an external symbol that is not implemented.

Expand source code Browse git
class NeedSymbol(NotImplementedError):
    """
    An exception raised by `refinery.lib.inno.emulator.IFPSEmulator` if the runtime calls out to
    an external symbol that is not implemented.
    """
    pass

Ancestors

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

An exception raised by IFPSEmulator if an unsupported opcode is encountered during emulation.

Expand source code Browse git
class OpCodeNotImplemented(NotImplementedError):
    """
    An exception raised by `refinery.lib.inno.emulator.IFPSEmulator` if an unsupported opcode is
    encountered during emulation.
    """
    pass

Ancestors

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

A generic exception representing any error that occurs during emulation.

Expand source code Browse git
class EmulatorException(RuntimeError):
    """
    A generic exception representing any error that occurs during emulation.
    """
    pass

Ancestors

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

This exception can be raised by an external function handler to signal the emulator that script execution should be aborted.

Expand source code Browse git
class AbortEmulation(Exception):
    """
    This exception can be raised by an external function handler to signal the emulator that script
    execution should be aborted.
    """
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException
class IFPSException (msg, parent=None)

This class represents an exception within the IFPS runtime, i.e. an exception that is subject to IFPS exception handling.

Expand source code Browse git
class IFPSException(RuntimeError):
    """
    This class represents an exception within the IFPS runtime, i.e. an exception that is subject
    to IFPS exception handling.
    """
    def __init__(self, msg: str, parent: Optional[BaseException] = None):
        super().__init__(msg)
        self.parent = parent

Ancestors

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

The emulation timed out based on the given time limit in the configuration.

Expand source code Browse git
class EmulatorTimeout(TimeoutError):
    """
    The emulation timed out based on the given time limit in the configuration.
    """
    pass

Ancestors

  • builtins.TimeoutError
  • builtins.OSError
  • builtins.Exception
  • builtins.BaseException
class EmulatorExecutionLimit (*args, **kwargs)

The emulation timed out based on the given execution limit in the configuration.

Expand source code Browse git
class EmulatorExecutionLimit(TimeoutError):
    """
    The emulation timed out based on the given execution limit in the configuration.
    """
    pass

Ancestors

  • builtins.TimeoutError
  • builtins.OSError
  • builtins.Exception
  • builtins.BaseException
class EmulatorMaxStack (*args, **kwargs)

The emulation was aborted because the stack limit given in the configuration was exceeded.

Expand source code Browse git
class EmulatorMaxStack(MemoryError):
    """
    The emulation was aborted because the stack limit given in the configuration was exceeded.
    """
    pass

Ancestors

  • builtins.MemoryError
  • builtins.Exception
  • builtins.BaseException
class EmulatorMaxCalls (*args, **kwargs)

The emulation was aborted because the call stack limit given in the configuration was exceeded.

Expand source code Browse git
class EmulatorMaxCalls(MemoryError):
    """
    The emulation was aborted because the call stack limit given in the configuration was exceeded.
    """
    pass

Ancestors

  • builtins.MemoryError
  • builtins.Exception
  • builtins.BaseException
class ExceptionHandler (finally_one, catch_error, finally_two, handler_end, current=Try)

This class represents an exception handler within the IFPS runtime.

Expand source code Browse git
class ExceptionHandler:
    """
    This class represents an exception handler within the IFPS runtime.
    """
    finally_one: Optional[int]
    """
    Code offset of the first finally handler.
    """
    catch_error: Optional[int]
    """
    Code offset of the catch handler.
    """
    finally_two: Optional[int]
    """
    Code offset of the second finally handler.
    """
    handler_end: int
    """
    Code offset of the first instruction that is no longer covered.
    """
    current: EHType = EHType.Try
    """
    Represents the current state of this exception handler.
    """

Class variables

var finally_one

Code offset of the first finally handler.

var catch_error

Code offset of the catch handler.

var finally_two

Code offset of the second finally handler.

var handler_end

Code offset of the first instruction that is no longer covered.

var current

Represents the current state of this exception handler.

class IFPSEmulatedFunction (call, spec, static, void=False)

Represents an emulated external symbol.

Expand source code Browse git
class IFPSEmulatedFunction(NamedTuple):
    """
    Represents an emulated external symbol.
    """
    call: Callable
    """
    The actual callable function that implements the symbol.
    """
    spec: List[bool]
    """
    A list of boolean values, one for each parameter of the function. Each boolean indicates
    whether the parameter at that index is passed by reference.
    """
    static: bool
    """
    Indicates whether the handler is static. If this value is `False`, the callable expects an
    additional `self` parameter of type `refinery.lib.inno.emulator.IFPSEmulator`.
    """
    void: bool = False
    """
    Indicates whether the handler implements a procedure rather than a function in the IFPS
    runtime.
    """

    @property
    def argc(self):
        """
        The argument count for this handler.
        """
        return len(self.spec)

Ancestors

  • builtins.tuple

Instance variables

var call

The actual callable function that implements the symbol.

var spec

A list of boolean values, one for each parameter of the function. Each boolean indicates whether the parameter at that index is passed by reference.

var static

Indicates whether the handler is static. If this value is False, the callable expects an additional self parameter of type IFPSEmulator.

var void

Indicates whether the handler implements a procedure rather than a function in the IFPS runtime.

var argc

The argument count for this handler.

Expand source code Browse git
@property
def argc(self):
    """
    The argument count for this handler.
    """
    return len(self.spec)
class IFPSEmulatorConfig (x64=True, admin=True, windows_os_version=(10, 0, 10240), windows_sp_version=(2, 0), throw_abort=False, log_calls=False, log_passwords=True, log_mutexes=True, log_opcodes=False, wizard_silent=True, max_opcodes=0, max_seconds=10, start_time=<factory>, milliseconds_per_instruction=0.001, sleep_scale=0.0, max_data_stack=1000000, max_call_stack=4096, environment=<factory>, user_name='Frank', temp_path='', host_name='Frank-PC', inno_name='ThisInstall', language='en', executable='C:\\Install.exe', install_to='I:\\', lcid=1033)

The configuration for IFPSEmulators.

Expand source code Browse git
class IFPSEmulatorConfig:
    """
    The configuration for `refinery.lib.inno.emulator.IFPSEmulator`s.
    """
    x64: bool = True
    admin: bool = True
    windows_os_version: Tuple[int, int, int] = (10, 0, 10240)
    windows_sp_version: Tuple[int, int] = (2, 0)
    throw_abort: bool = False
    log_calls: bool = False
    log_passwords: bool = True
    log_mutexes: bool = True
    log_opcodes: bool = False
    wizard_silent: bool = True
    max_opcodes: int = 0
    max_seconds: int = 10
    start_time: datetime = field(default_factory=datetime.now)
    milliseconds_per_instruction: float = 0.001
    sleep_scale: float = 0.0
    max_data_stack: int = 1_000_000
    max_call_stack: int = 4096
    environment: Dict[str, str] = field(default_factory=dict)
    user_name: str = 'Frank'
    temp_path: str = ''
    host_name: str = 'Frank-PC'
    inno_name: str = 'ThisInstall'
    language: str = 'en'
    executable: str = 'C:\\Install.exe'
    install_to: str = 'I:\\'
    lcid: int = 0x0409

    @property
    def cwd(self):
        return Path(self.executable).parent

Class variables

var start_time
var environment
var x64
var admin
var windows_os_version
var windows_sp_version
var throw_abort
var log_calls
var log_passwords
var log_mutexes
var log_opcodes
var wizard_silent
var max_opcodes
var max_seconds
var milliseconds_per_instruction
var sleep_scale
var max_data_stack
var max_call_stack
var user_name
var temp_path
var host_name
var inno_name
var language
var executable
var install_to
var lcid

Instance variables

var cwd
Expand source code Browse git
@property
def cwd(self):
    return Path(self.executable).parent
class TSetupStep (value, names=None, *, module=None, qualname=None, type=None, start=1)

An IFPS enumeration that classifies different setup steps.

Expand source code Browse git
class TSetupStep(int, Enum):
    """
    An IFPS enumeration that classifies different setup steps.
    """
    ssPreInstall = 0
    ssInstall = auto()
    ssPostInstall = auto()
    ssDone = auto()

Ancestors

  • builtins.int
  • enum.Enum

Class variables

var ssPreInstall
var ssInstall
var ssPostInstall
var ssDone
class TSplitType (value, names=None, *, module=None, qualname=None, type=None, start=1)

An IFPS enumeration that classifies different strategies for splitting strings.

Expand source code Browse git
class TSplitType(int, Enum):
    """
    An IFPS enumeration that classifies different strategies for splitting strings.
    """
    stAll = 0
    stExcludeEmpty = auto()
    stExcludeLastEmpty = auto()

Ancestors

  • builtins.int
  • enum.Enum

Class variables

var stAll
var stExcludeEmpty
var stExcludeLastEmpty
class TUninstallStep (value, names=None, *, module=None, qualname=None, type=None, start=1)

An IFPS enumeration that classifies uninstaller steps.

Expand source code Browse git
class TUninstallStep(int, Enum):
    """
    An IFPS enumeration that classifies uninstaller steps.
    """
    usAppMutexCheck = 0
    usUninstall = auto()
    usPostUninstall = auto()
    usDone = auto()

Ancestors

  • builtins.int
  • enum.Enum

Class variables

var usAppMutexCheck
var usUninstall
var usPostUninstall
var usDone
class TSetupProcessorArchitecture (value, names=None, *, module=None, qualname=None, type=None, start=1)

An IFPS enumeration that classifies different processor architectures.

Expand source code Browse git
class TSetupProcessorArchitecture(int, Enum):
    """
    An IFPS enumeration that classifies different processor architectures.
    """
    paUnknown = 0
    paX86 = auto()
    paX64 = auto()
    paArm32 = auto()
    paArm64 = auto()

Ancestors

  • builtins.int
  • enum.Enum

Class variables

var paUnknown
var paX86
var paX64
var paArm32
var paArm64
class PageID (value, names=None, *, module=None, qualname=None, type=None, start=1)

An IFPS enumeration that classifies the different installer pages.

Expand source code Browse git
class PageID(int, Enum):
    """
    An IFPS enumeration that classifies the different installer pages.
    """
    wpWelcome = 1
    wpLicense = auto()
    wpPassword = auto()
    wpInfoBefore = auto()
    wpUserInfo = auto()
    wpSelectDir = auto()
    wpSelectComponents = auto()
    wpSelectProgramGroup = auto()
    wpSelectTasks = auto()
    wpReady = auto()
    wpPreparing = auto()
    wpInstalling = auto()
    wpInfoAfter = auto()
    wpFinished = auto()

Ancestors

  • builtins.int
  • enum.Enum

Class variables

var wpWelcome
var wpLicense
var wpPassword
var wpInfoBefore
var wpUserInfo
var wpSelectDir
var wpSelectComponents
var wpSelectProgramGroup
var wpSelectTasks
var wpReady
var wpPreparing
var wpInstalling
var wpInfoAfter
var wpFinished
class NewFunctionCall (name, args)

An event generated by IFPSEmulator.emulate_function() which represents a call to the function with the given name and arguments.

Expand source code Browse git
class NewFunctionCall(NamedTuple):
    """
    An event generated by `refinery.lib.inno.emulator.IFPSEmulator.emulate_function` which
    represents a call to the function with the given name and arguments.
    """
    name: str
    args: tuple

Ancestors

  • builtins.tuple

Instance variables

var name

Alias for field number 0

var args

Alias for field number 1

class NewPassword (...)

An event generated by IFPSEmulator.emulate_function() for each password that is entered by the emulated setup script to a password edit control.

Expand source code Browse git
class NewPassword(str):
    """
    An event generated by `refinery.lib.inno.emulator.IFPSEmulator.emulate_function` for each
    password that is entered by the emulated setup script to a password edit control.
    """
    pass

Ancestors

  • builtins.str
class NewMutex (...)

An event generated by IFPSEmulator.emulate_function() for each mutex registered by the script.

Expand source code Browse git
class NewMutex(str):
    """
    An event generated by `refinery.lib.inno.emulator.IFPSEmulator.emulate_function` for each
    mutex registered by the script.
    """
    pass

Ancestors

  • builtins.str
class NewInstruction (function, instruction)

An event generated by IFPSEmulator.emulate_function() for each executed instruction.

Expand source code Browse git
class NewInstruction(NamedTuple):
    """
    An event generated by `refinery.lib.inno.emulator.IFPSEmulator.emulate_function` for each
    executed instruction.
    """
    function: Function
    instruction: Instruction

Ancestors

  • builtins.tuple

Instance variables

var function

Alias for field number 0

var instruction

Alias for field number 1

class EventCall (call)

This class is a wrapper for generator functions that can also capture their return value. It is used for IFPSEmulator.emulate_function().

Expand source code Browse git
class EventCall(Generic[_Y, _T]):
    """
    This class is a wrapper for generator functions that can also capture their return value.
    It is used for `refinery.lib.inno.emulator.IFPSEmulator.emulate_function`.
    """

    value: _T
    """
    The return value of the wrapped function.
    """

    def __init__(self, call: Generator[_Y, Any, _T]):
        self._call = call
        self._done = False
        self._buffer: List[_Y] = []
        self._value = None

    @classmethod
    def Wrap(cls, method: Callable[_P, Generator[_Y, Any, _T]]) -> Callable[_P, EventCall[_Y, _T]]:
        """
        Used for decorating generator functions.
        """
        @wraps(method)
        def wrapped(*args, **kwargs):
            return cls(method(*args, **kwargs))
        return wrapped

    @property
    def value(self):
        if not self._done:
            self._buffer = list(self)
        return self._value

    def __iter__(self):
        if self._done:
            yield from self._buffer
            assert self._value is not None
            self._buffer.clear()
        else:
            self._value = yield from self._call
            self._done = True
        return self._value

Ancestors

  • typing.Generic

Static methods

def Wrap(method)

Used for decorating generator functions.

Expand source code Browse git
@classmethod
def Wrap(cls, method: Callable[_P, Generator[_Y, Any, _T]]) -> Callable[_P, EventCall[_Y, _T]]:
    """
    Used for decorating generator functions.
    """
    @wraps(method)
    def wrapped(*args, **kwargs):
        return cls(method(*args, **kwargs))
    return wrapped

Instance variables

var value

The return value of the wrapped function.

Expand source code Browse git
@property
def value(self):
    if not self._done:
        self._buffer = list(self)
    return self._value
class FPUControl (value, names=None, *, module=None, qualname=None, type=None, start=1)

An integer flag representing FPU control words.

Expand source code Browse git
class FPUControl(IntFlag):
    """
    An integer flag representing FPU control words.
    """
    InvalidOperation    = 0b0_00_0_00_00_00_000001 # noqa
    DenormalizedOperand = 0b0_00_0_00_00_00_000010 # noqa
    ZeroDivide          = 0b0_00_0_00_00_00_000100 # noqa
    Overflow            = 0b0_00_0_00_00_00_001000 # noqa
    Underflow           = 0b0_00_0_00_00_00_010000 # noqa
    PrecisionError      = 0b0_00_0_00_00_00_100000 # noqa
    Reserved1           = 0b0_00_0_00_00_01_000000 # noqa
    Reserved2           = 0b0_00_0_00_00_10_000000 # noqa
    ExtendPrecision     = 0b0_00_0_00_01_00_000000 # noqa
    DoublePrecision     = 0b0_00_0_00_10_00_000000 # noqa
    MaxPrecision        = 0b0_00_0_00_11_00_000000 # noqa
    RoundDown           = 0b0_00_0_01_00_00_000000 # noqa
    RoundUp             = 0b0_00_0_10_00_00_000000 # noqa
    RoundTowardZero     = 0b0_00_0_11_00_00_000000 # noqa
    AffineInfinity      = 0b0_00_1_00_00_00_000000 # noqa
    Reserved3           = 0b0_01_0_00_00_00_000000 # noqa
    Reserved4           = 0b0_10_0_00_00_00_000000 # noqa
    ReservedBits        = 0b0_11_0_00_00_11_000000 # noqa

Ancestors

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

Class variables

var InvalidOperation
var DenormalizedOperand
var ZeroDivide
var Overflow
var Underflow
var PrecisionError
var Reserved1
var Reserved2
var ExtendPrecision
var DoublePrecision
var MaxPrecision
var RoundDown
var RoundUp
var RoundTowardZero
var AffineInfinity
var Reserved3
var Reserved4
var ReservedBits
class IFPSEmulator (archive, options=None, **more)

The core IFPS emulator.

Expand source code Browse git
class IFPSEmulator:
    """
    The core IFPS emulator.
    """

    def __init__(
        self,
        archive: Union[InnoArchive, IFPSFile],
        options: Optional[IFPSEmulatorConfig] = None,
        **more
    ):
        if isinstance(archive, InnoArchive):
            self.inno = archive
            self.ifps = ifps = archive.ifps
            if ifps is None:
                raise ValueError('The input archive does not contain a script.')
        else:
            self.inno = None
            self.ifps = ifps = archive
        self.config = options or IFPSEmulatorConfig(**more)
        self.globals = [Variable(v.type, v.spec) for v in ifps.globals]
        self.stack: List[Variable] = []
        self.mutexes: Set[str] = set()
        self.symbols: Dict[str, Function] = CaseInsensitiveDict()
        self.reset()
        for pfn in ifps.functions:
            self.symbols[pfn.name] = pfn

    def __repr__(self):
        return self.__class__.__name__

    def reset(self):
        """
        Reset the emulator timing, FPU word, mutexes, trace, and stack. All global variables are
        set to their default values.
        """
        self.seconds_slept = 0.0
        self.clock = 0
        self.fpucw = FPUControl.MaxPrecision | FPUControl.RoundTowardZero
        self.jumpflag = False
        self.mutexes.clear()
        self.stack.clear()
        for v in self.globals:
            v.setdefault()
        return self

    def unimplemented(self, function: Function):
        """
        The base IFPS emulator raises `refinery.lib.inno.emulator.NeedSymbol` when an external
        symbol is unimplemented. Child classes can override this function to handle the missing
        symbol differently.
        """
        raise NeedSymbol(function.name)

    @EventCall.Wrap
    def emulate_function(self, function: Function, *args):
        """
        Emulate a function call to the given function, passing the given arguments. The method
        returns the return value of the emulated function call if it is not a procedure.
        """
        self.stack.clear()
        decl = function.decl
        if decl is None:
            raise NotImplementedError(F'Do not know how to call {function!s}.')
        if (n := len(decl.parameters)) != (m := len(args)):
            raise ValueError(
                F'Function {function!s} expects {n} arguments, only {m} were given.')
        for index, (argument, parameter) in enumerate(zip(args, decl.parameters), 1):
            variable = Variable(parameter.type, VariableSpec(index, VariableType.Local))
            variable.set(argument)
            self.stack.append(variable)
        self.stack.reverse()
        if not decl.void:
            result = Variable(decl.return_type, VariableSpec(0, VariableType.Argument))
            self.stack.append(result)
        yield from self.call(function)
        self.stack.clear()
        if not decl.void:
            return result.get()

    def call(self, function: Function):
        """
        Begin emulating at the start of the given function.
        """

        def operator_div(a, b):
            return a // b if isinstance(a, int) and isinstance(b, int) else a / b

        def operator_in(a, b):
            return a in b

        def getvar(op: Union[VariableSpec, Operand]) -> Variable:
            if not isinstance(op, Operand):
                v = op
                k = None
            elif op.type is OperandType.Value:
                raise TypeError('Attempting to retrieve variable for an immediate operand.')
            else:
                v = op.variable
                k = op.index
                if op.type is OperandType.IndexedByVar:
                    k = getvar(k).get()
            t, i = v.type, v.index
            if t is VariableType.Argument:
                if function.decl.void:
                    i -= 1
                var = self.stack[sp - i]
            elif t is VariableType.Global:
                var = self.globals[i]
            elif t is VariableType.Local:
                var = self.stack[sp + i]
            else:
                raise TypeError
            if k is not None:
                var = var.at(k)
            return var

        def getval(op: Operand):
            if op.immediate:
                return op.value.value
            return getvar(op).get()

        def setval(op: Operand, new):
            if op.immediate:
                raise RuntimeError('attempt to assign to an immediate')
            getvar(op).set(new)

        class CallState(NamedTuple):
            fn: Function
            ip: int
            sp: int
            eh: List[ExceptionHandler]

        callstack: List[CallState] = []
        exec_start = process_time()
        stack = self.stack
        _cfg_max_call_stack = self.config.max_call_stack
        _cfg_max_data_stack = self.config.max_data_stack
        _cfg_max_seconds = self.config.max_seconds
        _cfg_max_opcodes = self.config.max_opcodes
        _cfg_log_opcodes = self.config.log_opcodes

        ip: int = 0
        sp: int = len(stack) - 1
        pending_exception = None
        exceptions = []

        while True:
            if 0 < _cfg_max_call_stack < len(callstack):
                raise EmulatorMaxCalls

            if function.body is None:
                namespace = ''

                if decl := function.decl:
                    if decl.is_property:
                        if stack[-1].type.code == TC.Class:
                            function = function.setter
                        else:
                            function = function.getter
                        decl = function.decl
                    namespace = (
                        decl.classname or decl.module or '')

                name = function.name
                registry: Dict[str, IFPSEmulatedFunction] = self.external_symbols.get(namespace, {})
                handler = registry.get(name)

                if handler:
                    void = handler.void
                    argc = handler.argc
                elif decl:
                    void = decl.void
                    argc = decl.argc
                else:
                    void = True
                    argc = 0

                try:
                    rpos = 0 if void else 1
                    args = [stack[~k] for k in range(rpos, argc + rpos)]
                except IndexError:
                    raise EmulatorException(
                        F'Cannot call {function!s}; {argc} arguments + {rpos} return values expected,'
                        F' but stack size is only {len(stack)}.')

                if self.config.log_calls:
                    yield NewFunctionCall(str(function), tuple(a.get() for a in args))

                if handler is None:
                    self.unimplemented(function)
                else:
                    if decl and (decl.void != handler.void or decl.argc != handler.argc):
                        ok = False
                        if 1 + decl.argc - decl.void == 1 + handler.argc - handler.void:
                            if decl.void and not decl.parameters[0].const:
                                ok = True
                            elif handler.void and handler.spec[0]:
                                ok = True
                        if not ok:
                            raise RuntimeError(F'Handler for {function!s} is incompatible with declaration.')
                    for k, (var, byref) in enumerate(zip(args, handler.spec)):
                        if not byref:
                            args[k] = var.get()
                    if not handler.static:
                        args.insert(0, self)
                    try:
                        return_value = handler.call(*args)
                        if inspect.isgenerator(return_value):
                            return_value = yield from return_value
                    except GeneratorExit:
                        pass
                    except BaseException as b:
                        pending_exception = IFPSException(F'Error calling {function.name}: {b!s}', b)
                    else:
                        if not handler.void:
                            stack[-1].set(return_value)
                if not callstack:
                    if pending_exception is None:
                        return
                    raise pending_exception
                function, ip, sp, exceptions = callstack.pop()
                continue

            while insn := function.code.get(ip, None):
                if 0 < _cfg_max_seconds < process_time() - exec_start:
                    raise EmulatorTimeout
                if 0 < _cfg_max_opcodes < self.clock:
                    raise EmulatorExecutionLimit
                if 0 < _cfg_max_data_stack < len(stack):
                    raise EmulatorMaxStack
                if _cfg_log_opcodes:
                    yield NewInstruction(function, insn)
                try:
                    if pe := pending_exception:
                        pending_exception = None
                        raise pe

                    opc = insn.opcode
                    ip += insn.size
                    self.clock += 1

                    if opc == Op.Nop:
                        continue
                    elif opc == Op.Assign:
                        dst = getvar(insn.op(0))
                        src = insn.op(1)
                        if src.immediate:
                            dst.set(src.value)
                        else:
                            dst.set(getvar(src))
                    elif opc == Op.Calculate:
                        calculate = {
                            AOp.Add: operator.add,
                            AOp.Sub: operator.sub,
                            AOp.Mul: operator.mul,
                            AOp.Div: operator_div,
                            AOp.Mod: operator.mod,
                            AOp.Shl: operator.lshift,
                            AOp.Shr: operator.rshift,
                            AOp.And: operator.and_,
                            AOp.BOr: operator.or_,
                            AOp.Xor: operator.xor,
                        }[insn.operator]
                        src = insn.op(1)
                        dst = insn.op(0)
                        sv = getval(src)
                        dv = getval(dst)
                        fpu = isinstance(sv, float) or isinstance(dv, float)
                        try:
                            result = calculate(dv, sv)
                            if fpu and not isinstance(result, float):
                                raise FloatingPointError
                        except FloatingPointError as FPE:
                            if not self.fpucw & FPUControl.InvalidOperation:
                                result = float('nan')
                            else:
                                raise IFPSException('invalid operation', FPE) from FPE
                        except OverflowError as OFE:
                            if fpu and self.fpucw & FPUControl.Overflow:
                                result = float('nan')
                            else:
                                raise IFPSException('arithmetic overflow', OFE) from OFE
                        except ZeroDivisionError as ZDE:
                            if fpu and self.fpucw & FPUControl.ZeroDivide:
                                result = float('nan')
                            else:
                                raise IFPSException('division by zero', ZDE) from ZDE
                        setval(dst, result)
                    elif opc == Op.Push:
                        # TODO: I do not actually know how this works
                        stack.append(getval(insn.op(0)))
                    elif opc == Op.PushVar:
                        stack.append(getvar(insn.op(0)))
                    elif opc == Op.Pop:
                        self.temp = stack.pop()
                    elif opc == Op.Call:
                        callstack.append(CallState(function, ip, sp, exceptions))
                        function = insn.operands[0]
                        ip = 0
                        sp = len(stack) - 1
                        exceptions = []
                        break
                    elif opc == Op.Jump:
                        ip = insn.operands[0]
                    elif opc == Op.JumpTrue:
                        if getval(insn.op(1)):
                            ip = insn.operands[0]
                    elif opc == Op.JumpFalse:
                        if not getval(insn.op(1)):
                            ip = insn.operands[0]
                    elif opc == Op.Ret:
                        del stack[sp + 1:]
                        if not callstack:
                            return
                        function, ip, sp, exceptions = callstack.pop()
                        break
                    elif opc == Op.StackType:
                        raise OpCodeNotImplemented(str(opc))
                    elif opc == Op.PushType:
                        stack.append(Variable(
                            insn.operands[0],
                            VariableSpec(len(stack) - sp, VariableType.Local)
                        ))
                    elif opc == Op.Compare:
                        compare = {
                            COp.GE: operator.ge,
                            COp.LE: operator.le,
                            COp.GT: operator.gt,
                            COp.LT: operator.lt,
                            COp.NE: operator.ne,
                            COp.EQ: operator.eq,
                            COp.IN: operator_in,
                            COp.IS: operator.is_,
                        }[insn.operator]
                        d = getvar(insn.op(0))
                        a = getval(insn.op(1))
                        b = getval(insn.op(2))
                        d.set(compare(a, b))
                    elif opc == Op.CallVar:
                        call = getval(insn.op(0))
                        if isinstance(call, int):
                            call = self.ifps.functions[call]
                        if isinstance(call, Function):
                            callstack.append(CallState(function, ip, sp, exceptions))
                            function = call
                            ip = 0
                            sp = len(stack) - 1
                            exceptions = []
                            break
                    elif opc in (Op.SetPtr, Op.SetPtrToCopy):
                        copy = False
                        if opc == Op.SetPtrToCopy:
                            copy = True
                        dst = getvar(insn.op(0))
                        src = getvar(insn.op(1))
                        dst.setptr(src, copy=copy)
                    elif opc == Op.BooleanNot:
                        setval(a := insn.op(0), not getval(a))
                    elif opc == Op.IntegerNot:
                        setval(a := insn.op(0), ~getval(a))
                    elif opc == Op.Neg:
                        setval(a := insn.op(0), -getval(a))
                    elif opc == Op.SetFlag:
                        condition, negated = insn.operands
                        self.jumpflag = getval(condition) ^ negated
                    elif opc == Op.JumpFlag:
                        if self.jumpflag:
                            ip = insn.operands[0]
                    elif opc == Op.PushEH:
                        exceptions.append(ExceptionHandler(*insn.operands))
                    elif opc == Op.PopEH:
                        tp = None
                        et = EHType(insn.operands[0])
                        eh = exceptions[-1]
                        if eh.current != et:
                            raise RuntimeError(F'Expected {eh.current} block to end, but {et} was ended instead.')
                        while tp is None:
                            if et is None:
                                raise RuntimeError
                            tp, et = {
                                EHType.Catch         : (eh.finally_one, EHType.Finally),
                                EHType.Try           : (eh.finally_one, EHType.Finally),
                                EHType.Finally       : (eh.finally_two, EHType.SecondFinally),
                                EHType.SecondFinally : (eh.handler_end, None),
                            }[et]
                        eh.current = et
                        ip = tp
                        if et is None:
                            exceptions.pop()
                    elif opc == Op.Inc:
                        setval(a := insn.op(0), getval(a) + 1)
                    elif opc == Op.Dec:
                        setval(a := insn.op(0), getval(a) - 1)
                    elif opc == Op.JumpPop1:
                        stack.pop()
                        ip = insn.operands[0]
                    elif opc == Op.JumpPop2:
                        stack.pop()
                        stack.pop()
                        ip = insn.operands[0]
                    else:
                        raise RuntimeError(F'Function contains invalid opcode at 0x{ip:X}.')
                except IFPSException as EE:
                    try:
                        eh = exceptions[-1]
                    except IndexError:
                        raise EE
                    et = EHType.Try
                    tp = None
                    while tp is None:
                        if et is None:
                            raise RuntimeError
                        tp, et = {
                            EHType.Try           : (eh.catch_error, EHType.Catch),
                            EHType.Catch         : (eh.finally_one, EHType.Finally),
                            EHType.Finally       : (eh.finally_two, EHType.SecondFinally),
                            EHType.SecondFinally : (eh.handler_end, None),
                        }[et]
                    if et is None:
                        raise EE
                    eh.current = et
                    ip = tp
                except AbortEmulation:
                    raise
                except EmulatorException:
                    raise
                except Exception as RE:
                    raise EmulatorException(
                        F'In {function.symbol} at 0x{insn.offset:X} (cycle {self.clock}), '
                        F'emulation of {insn!r} failed: {RE!s}')
            if ip is None:
                raise RuntimeError(F'Instruction pointer moved out of bounds to 0x{ip:X}.')

    external_symbols: ClassVar[
        Dict[str,                        # class name for methods or empty string for functions
        Dict[str, IFPSEmulatedFunction]] # method or function name to emulation info
    ] = CaseInsensitiveDict()

    def external(*args, static=True, __reg: dict = external_symbols, **kwargs):
        def decorator(pfn):
            signature = inspect.signature(pfn)
            name: str = kwargs.get('name', pfn.__name__)
            csep: str = '.'
            if csep not in name:
                csep = '__'
            classname, _, name = name.rpartition(csep)
            if (registry := __reg.get(classname)) is None:
                registry = __reg[classname] = CaseInsensitiveDict()

            docs = F'{classname}::{name}' if classname else name
            docs = F'An emulated handler for the external symbol {docs}.'
            pfn.__doc__ = docs

            void = kwargs.get('void', signature.return_annotation == signature.empty)
            parameters: List[bool] = []
            specs = iter(signature.parameters.values())
            if not static:
                next(specs)
            for spec in specs:
                try:
                    hint = eval(spec.annotation)
                except Exception as E:
                    raise RuntimeError(F'Invalid signature: {signature}') from E
                if not isinstance(hint, type):
                    hint = get_origin(hint)
                var = isinstance(hint, type) and issubclass(hint, Variable)
                parameters.append(var)
            registry[name] = e = IFPSEmulatedFunction(pfn, parameters, static, void)
            aliases = kwargs.get('alias', [])
            if isinstance(aliases, str):
                aliases = [aliases]
            for name in aliases:
                registry[name] = e
            if static:
                pfn = staticmethod(pfn)
            return pfn
        return decorator(args[0]) if args else decorator

    @external
    def TInputDirWizardPage__GetValues(this: object, k: int) -> str:
        return F'$InputDir{k}'

    @external
    def TInputFileWizardPage__GetValues(this: object, k: int) -> str:
        return F'$InputFile{k}'

    @external(static=False)
    def TPasswordEdit__SetText(self, this: object, value: str):
        if value:
            yield NewPassword(value)
        return value

    @external
    def kernel32__GetTickCount() -> int:
        return time.monotonic_ns() // 1_000_000

    @external
    def user32__GetSystemMetrics(index: int) -> int:
        if index == 80:
            return 1
        if index == 43:
            return 2
        return 0

    @external
    def IsX86Compatible() -> bool:
        return True

    @external(alias=[
        'sArm64',
        'IsArm32Compatible',
        'Debugging',
        'IsUninstaller',
    ])
    def Terminated() -> bool:
        return False

    @external(static=False)
    def IsAdmin(self) -> bool:
        return self.config.admin

    @external(static=False, alias='Sleep')
    def kernel32__Sleep(self, ms: int):
        seconds = ms / 1000.0
        self.seconds_slept += seconds
        time.sleep(seconds * self.config.sleep_scale)

    @external
    def Random(top: int) -> int:
        return random.randrange(0, top)

    @external(alias='StrGet')
    def WStrGet(string: Variable[str], index: int) -> str:
        if index <= 0:
            raise ValueError
        return string[index - 1:index]

    @external(alias='StrSet')
    def WStrSet(char: str, index: int, dst: Variable[str]):
        old = dst.get()
        index -= 1
        dst.set(old[:index] + char + old[index:])

    @external(static=False)
    def GetEnv(self, name: str) -> str:
        return self.config.environment.get(name, F'%{name}%')

    @external
    def Beep():
        pass

    @external(static=False)
    def Abort(self):
        if self.config.throw_abort:
            raise AbortEmulation

    @external
    def DirExists(path: str) -> bool:
        return True

    @external
    def ForceDirectories(path: str) -> bool:
        return True

    @external(alias='LoadStringFromLockedFile')
    def LoadStringFromFile(path: str, out: Variable[str]) -> bool:
        return True

    @external(alias='LoadStringsFromLockedFile')
    def LoadStringsFromFile(path: str, out: Variable[str]) -> bool:
        return True

    @cached_property
    def constant_map(self) -> dict[str, str]:
        cfg = self.config
        tmp = cfg.temp_path
        if not tmp:
            tmp = ''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ', k=5))
            tmp = RF'C:\Windows\Temp\IS-{tmp}'
        map = {
            'app'               : cfg.install_to,
            'win'               : R'C:\Windows',
            'sys'               : R'C:\Windows\System',
            'sysnative'         : R'C:\Windows\System32',
            'src'               : str(Path(cfg.executable).parent),
            'sd'                : R'C:',
            'commonpf'          : R'C:\Program Files',
            'commoncf'          : R'C:\Program Files\Common Files',
            'tmp'               : tmp,
            'commonfonts'       : R'C:\Windows\Fonts',
            'dao'               : R'C:\Program Files\Common Files\Microsoft Shared\DAO',
            'dotnet11'          : R'C:\Windows\Microsoft.NET\Framework\v1.1.4322',
            'dotnet20'          : R'C:\Windows\Microsoft.NET\Framework\v3.0',
            'dotnet2032'        : R'C:\Windows\Microsoft.NET\Framework\v3.0',
            'dotnet40'          : R'C:\Windows\Microsoft.NET\Framework\v4.0.30319',
            'dotnet4032'        : R'C:\Windows\Microsoft.NET\Framework\v4.0.30319',
            'group'             : RF'C:\Users\{cfg.user_name}\Start Menu\Programs\{cfg.inno_name}',
            'localappdata'      : RF'C:\Users\{cfg.user_name}\AppData\Local',
            'userappdata'       : RF'C:\Users\{cfg.user_name}\AppData\Roaming',
            'userdesktop'       : RF'C:\Users\{cfg.user_name}\Desktop',
            'userdocs'          : RF'C:\Users\{cfg.user_name}\Documents',
            'userfavourites'    : RF'C:\Users\{cfg.user_name}\Favourites',
            'usersavedgames'    : RF'C:\Users\{cfg.user_name}\Saved Games',
            'usersendto'        : RF'C:\Users\{cfg.user_name}\SendTo',
            'userstartmenu'     : RF'C:\Users\{cfg.user_name}\Start Menu',
            'userprograms'      : RF'C:\Users\{cfg.user_name}\Start Menu\Programs',
            'userstartup'       : RF'C:\Users\{cfg.user_name}\Start Menu\Programs\Startup',
            'usertemplates'     : RF'C:\Users\{cfg.user_name}\Templates',
            'commonappdata'     : R'C:\ProgramData',
            'commondesktop'     : R'C:\ProgramData\Microsoft\Windows\Desktop',
            'commondocs'        : R'C:\ProgramData\Microsoft\Windows\Documents',
            'commonstartmenu'   : R'C:\ProgramData\Microsoft\Windows\Start Menu',
            'commonprograms'    : R'C:\ProgramData\Microsoft\Windows\Start Menu\Programs',
            'commonstartup'     : R'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup',
            'commontemplates'   : R'C:\ProgramData\Microsoft\Windows\Templates',
            'cmd'               : R'C:\Windows\System32\cmd.exe',
            'computername'      : cfg.host_name,
            'groupname'         : cfg.inno_name,
            'hwnd'              : '0',
            'wizardhwnd'        : '0',
            'language'          : cfg.language,
            'srcexe'            : cfg.executable,
            'sysuserinfoname'   : '{sysuserinfoname}',
            'sysuserinfoorg'    : '{sysuserinfoorg}',
            'userinfoname'      : '{userinfoname}',
            'userinfoorg'       : '{userinfoorg}',
            'userinfoserial'    : '{userinfoserial}',
            'username'          : cfg.user_name,
            'log'               : '',
        }

        if (inno := self.inno) is None or (inno.setup_info.Header.Flags & Flags.Uninstallable):
            map['uninstallexe'] = RF'{cfg.install_to}\unins000.exe'

        if cfg.x64:
            map['syswow64'] = R'C:\Windows\SysWOW64'
            map['commonpf32'] = R'C:\Program Files (x86)'
            map['commoncf32'] = R'C:\Program Files (x86)\Common Files'
            map['commonpf64'] = R'C:\Program Files'
            map['commoncf64'] = R'C:\Program Files\Common Files'
            map['dotnet2064'] = R'C:\Windows\Microsoft.NET\Framework64\v3.0'
            map['dotnet4064'] = R'C:\Windows\Microsoft.NET\Framework64\v4.0.30319'
        else:
            map['syswow64'] = R'C:\Windows\System32'
            map['commonpf32'] = R'C:\Program Files'
            map['commoncf32'] = R'C:\Program Files\Common Files'

        if cfg.windows_os_version[0] >= 10:
            map['userfonts'] = RF'{map["localappdata"]}\Microsoft\Windows\Fonts'

        if cfg.windows_os_version[0] >= 7:
            map['usercf'] = RF'{map["localappdata"]}\Programs\Common'
            map['userpf'] = RF'{map["localappdata"]}\Programs'

        for auto_var, admin_var, user_var in [
            ('autoappdata',       'commonappdata',       'userappdata',   ), # noqa
            ('autocf',            'commoncf',            'usercf',        ), # noqa
            ('autocf32',          'commoncf32',          'usercf',        ), # noqa
            ('autocf64',          'commoncf64',          'usercf',        ), # noqa
            ('autodesktop',       'commondesktop',       'userdesktop',   ), # noqa
            ('autodocs',          'commondocs',          'userdocs',      ), # noqa
            ('autofonts',         'commonfonts',         'userfonts',     ), # noqa
            ('autopf',            'commonpf',            'userpf',        ), # noqa
            ('autopf32',          'commonpf32',          'userpf',        ), # noqa
            ('autopf64',          'commonpf64',          'userpf',        ), # noqa
            ('autoprograms',      'commonprograms',      'userprograms',  ), # noqa
            ('autostartmenu',     'commonstartmenu',     'userstartmenu', ), # noqa
            ('autostartup',       'commonstartup',       'userstartup',   ), # noqa
            ('autotemplates',     'commontemplates',     'usertemplates', ), # noqa
        ]:
            try:
                map[auto_var] = map[admin_var] if cfg.admin else map[user_var]
            except KeyError:
                continue

        for legacy, new in [
            ('cf',     'commoncf',    ), # noqa
            ('cf32',   'commoncf32',  ), # noqa
            ('cf64',   'commoncf64',  ), # noqa
            ('fonts',  'commonfonts', ), # noqa
            ('pf',     'commonpf',    ), # noqa
            ('pf32',   'commonpf32',  ), # noqa
            ('pf64',   'commonpf64',  ), # noqa
            ('sendto', 'usersendto',  ), # noqa
        ]:
            try:
                map[legacy] = map[new]
            except KeyError:
                continue

        return map

    @external(static=False)
    def ExpandConstant(self, string: str) -> str:
        return self.expand_constant(string)

    @external(static=False)
    def ExpandConstantEx(self, string: str, custom_var: str, custom_val: str) -> str:
        return self.expand_constant(string, custom_var, custom_val)

    def expand_constant(
        self,
        string: str,
        custom_var: Optional[str] = None,
        custom_val: Optional[str] = None,
        unescape: bool = False
    ):
        config = self.config
        expand = partial(self.expand_constant, unescape=True)
        string = re.sub(r'(\{\{.*?\}(?!\}))', '\\1}', string)

        with io.StringIO() as result:
            constants = self.constant_map
            formatter = Formatter()
            backslash = False
            try:
                parsed = list(formatter.parse(string))
            except ValueError as VE:
                raise IFPSException(F'invalid format string: {string!r}', VE) from VE
            for prefix, spec, modifier, conversion in parsed:
                if backslash and prefix[:1] == '\\':
                    prefix = prefix[1:]
                if unescape:
                    prefix = unquote(prefix)
                result.write(prefix)
                if spec is None:
                    continue
                elif spec == '\\':
                    if modifier or conversion:
                        raise IFPSException('Invalid format string.', ValueError(string))
                    value = spec
                elif spec == custom_var:
                    value = custom_val
                elif spec.startswith('%'):
                    name, p, default = spec[1:].partition('|')
                    name = expand(name)
                    default = expand(default)
                    try:
                        value = config.environment[name]
                    except KeyError:
                        value = default if p else F'%{name}%'
                elif spec == 'drive':
                    value = self.ExtractFileDrive(expand(modifier))
                elif spec == 'ini':
                    # {ini:Filename,Section,Key|DefaultValue}
                    _, _, default = modifier.partition('|')
                    value = expand(default)
                elif spec == 'code':
                    # {code:FunctionName|Param}
                    symbol, _, param = modifier.partition('|')
                    param = expand(param)
                    try:
                        function = self.symbols[symbol]
                    except KeyError as KE:
                        raise IFPSException(F'String formatter references missing function {symbol}.', KE) from KE
                    emulation = self.emulate_function(function, param)
                    value = str(emulation.value)
                elif spec == 'cm':
                    # {cm:LaunchProgram,Inno Setup}
                    # The example above translates to "Launch Inno Setup" if English is the active language.
                    name, _, placeholders = modifier.partition(',')
                    value = self.CustomMessage(expand(name))
                    if placeholders:
                        def _placeholder(match: re.Match[str]):
                            try:
                                return placeholders[int(match[1]) - 1]
                            except Exception:
                                return match[0]
                        placeholders = [ph.strip() for ph in placeholders.split(',')]
                        value = re.sub('(?<!%)%([1-9]\\d*)', _placeholder, value)
                elif spec == 'reg':
                    # {reg:HKXX\SubkeyName,ValueName|DefaultValue}
                    _, _, default = modifier.partition('|')
                    value = expand(default)
                elif spec == 'param':
                    # {param:ParamName|DefaultValue}
                    _, _, default = modifier.partition('|')
                    value = expand(default)
                else:
                    try:
                        value = constants[spec]
                    except KeyError as KE:
                        raise IFPSException(F'invalid format field {spec}', KE) from KE
                backslash = value.endswith('\\')
                result.write(value)
            return result.getvalue()

    @external
    def DeleteFile(path: str) -> bool:
        return True

    @external
    def FileExists(file_name: str) -> bool:
        return False

    @external
    def Log(log: str):
        ...

    @external
    def Inc(p: Variable[Variable[int]]):
        p.set(p.get() + 1)

    @external
    def Dec(p: Variable[Variable[int]]):
        p.set(p.get() - 1)

    @external
    def FindFirst(file_name: str, frec: Variable) -> bool:
        return False

    @external
    def Trunc(x: float) -> float:
        return math.trunc(x)

    @external
    def GetSpaceOnDisk(
        path: str,
        in_megabytes: bool,
        avail: Variable[int],
        space: Variable[int],
    ) -> bool:
        _a = 3_000_000
        _t = 5_000_000
        if not in_megabytes:
            _a *= 1000
            _t *= 1000
        avail.set(_a)
        space.set(_t)
        return True

    @external
    def GetSpaceOnDisk64(
        path: str,
        avail: Variable[int],
        space: Variable[int],
    ) -> bool:
        avail.set(3_000_000_000)
        space.set(5_000_000_000)
        return True

    @external
    def Exec(
        exe: str,
        cmd: str,
        cwd: str,
        show: int,
        wait: int,
        out: Variable[int],
    ) -> bool:
        out.set(0)
        return True

    @external
    def GetCmdTail() -> str:
        return ''

    @external
    def ParamCount() -> int:
        return 0

    @external
    def ParamStr(index: int) -> str:
        return ''

    @external
    def ActiveLanguage() -> str:
        return 'en'

    @external(static=False)
    def CustomMessage(self, msg_name: str) -> str:
        by_language = {}
        for msg in self.inno.setup_info.Messages:
            if msg.EncodedName == msg_name:
                lng = msg.get_language_value().Name
                if lng == self.config.language:
                    return msg.Value
                by_language[lng] = msg.Value
        try:
            return by_language[0]
        except KeyError:
            pass
        try:
            return next(iter(by_language.values()))
        except StopIteration:
            raise IFPSException(F'Custom message with name {msg_name} not found.')

    @external
    def FmtMessage(fmt: str, args: List[str]) -> str:
        fmt = fmt.replace('{', '{{')
        fmt = fmt.replace('}', '}}')
        fmt = '%'.join(re.sub('%(\\d+)', '{\\1}', p) for p in fmt.split('%%'))
        return fmt.format(*args)

    @external
    def Format(fmt: str, args: List[Union[str, int, float]]) -> str:
        try:
            formatted = fmt % tuple(args)
        except Exception:
            raise IFPSException('invalid format')
        else:
            return formatted

    @external(static=False)
    def SetupMessage(self, id: int) -> str:
        try:
            return self.inno.setup_info.Messages[id].Value
        except (AttributeError, IndexError):
            return ''

    @external(static=False, alias=['Is64BitInstallMode', 'IsX64Compatible', 'IsX64OS'])
    def IsWin64(self) -> bool:
        return self.config.x64

    @external(static=False)
    def IsX86OS(self) -> bool:
        return not self.config.x64

    @external
    def RaiseException(msg: str):
        raise IFPSException(msg)

    @external(static=False)
    def ProcessorArchitecture(self) -> int:
        if self.config.x64:
            return TSetupProcessorArchitecture.paX64.value
        else:
            return TSetupProcessorArchitecture.paX86.value

    @external(static=False)
    def GetUserNameString(self) -> str:
        return self.config.user_name

    @external(static=False)
    def GetComputerNameString(self) -> str:
        return self.config.host_name

    @external(static=False)
    def GetUILanguage(self) -> str:
        return self.config.lcid

    @external
    def GetArrayLength(array: Variable) -> int:
        array = array.deref()
        return len(array)

    @external
    def SetArrayLength(array: Variable, n: int):
        a = array.deref()
        a.resize(n)

    @external(static=False)
    def WizardForm(self) -> object:
        return self

    @external
    def Unassigned() -> None:
        return None

    @external
    def Null() -> None:
        return None

    @external(static=False)
    def Set8087CW(self, cw: int):
        self.fpucw = FPUControl(cw)

    @external(static=False)
    def Get8087CW(self) -> int:
        return self.fpucw.value

    @external(static=False)
    def GetDateTimeString(
        self,
        fmt: str,
        date_separator: str,
        time_separator: str,
    ) -> str:

        now = self.config.start_time
        now = now + timedelta(
            milliseconds=(self.config.milliseconds_per_instruction * self.clock))
        now = now + timedelta(seconds=self.seconds_slept)

        date_separator = date_separator.lstrip('\0')
        time_separator = time_separator.lstrip('\0')

        def dt(m: re.Match[str]):
            spec = m[1]
            ampm = m[2]
            if ampm:
                am, _, pm = ampm.partition('/')
                spec = spec.upper()
                suffix = now.strftime('%p').lower()
                suffix = {'am': am, 'pm': pm}[suffix]
            else:
                suffix = ''
            if spec == 'dddddd' or spec == 'ddddd':
                return now.date.isoformat()
            if spec == 't':
                return now.time().isoformat('minutes')
            if spec == 'tt':
                return now.time().isoformat('seconds')
            if spec == 'd':
                return str(now.day)
            if spec == 'm':
                return str(now.month)
            if spec == 'h':
                return str(now.hour)
            if spec == 'n':
                return str(now.minute)
            if spec == 's':
                return str(now.second)
            if spec == 'H':
                return now.strftime('%I').lstrip('0') + suffix
            if spec == '/':
                return date_separator or spec
            if spec == ':':
                return time_separator or spec
            return now.strftime({
                'dddd'  : '%A',
                'ddd'   : '%a',
                'dd'    : '%d',
                'mmmm'  : '%B',
                'mmm'   : '%b',
                'mm'    : '%m',
                'yyyy'  : '%Y',
                'yy'    : '%y',
                'hh'    : '%H',
                'HH'    : '%I' + suffix,
                'nn'    : '%M',
                'ss'    : '%S',
            }.get(spec, m[0]))

        split = re.split(F'({formats.string!s})', fmt)
        for k in range(0, len(split), 2):
            split[k] = re.sub('([dmyhnst]+)((?:[aA][mM]?/[pP][mM]?)?)', dt, split[k])
        for k in range(1, len(split), 2):
            split[k] = split[k][1:-1]
        return ''.join(split)

    @external
    def Chr(b: int) -> str:
        return chr(b)

    @external
    def Ord(c: str) -> int:
        return ord(c)

    @external
    def Copy(string: str, index: int, count: int) -> str:
        index -= 1
        return string[index:index + count]

    @external
    def Length(string: str) -> int:
        return len(string)

    @external(alias='AnsiLowercase')
    def Lowercase(string: str) -> str:
        return string.lower()

    @external(alias='AnsiUppercase')
    def Uppercase(string: str) -> str:
        return string.upper()

    @external
    def StringOfChar(c: str, count: int) -> str:
        return c * count

    @external
    def Delete(string: Variable[str], index: int, count: int):
        index -= 1
        old = string.get()
        string.set(old[:index] + old[index + count:])

    @external
    def Insert(string: str, dest: Variable[str], index: int):
        index -= 1
        old = dest.get()
        dest.set(old[:index] + string + old[index:])

    @external(static=False)
    def StringChange(self, string: Variable[str], old: str, new: str) -> int:
        return self.StringChangeEx(string, old, new, False)

    @external
    def StringChangeEx(string: Variable[str], old: str, new: str, _: bool) -> int:
        haystack = string.get()
        count = haystack.count(old)
        string.set(haystack.replace(old, new))
        return count

    @external
    def Pos(string: str, sub: str) -> int:
        return string.find(sub) + 1

    @external
    def AddQuotes(string: str) -> str:
        if string and (string[0] != '"' or string[~0] != '"') and ' ' in string:
            string = F'"{string}"'
        return string

    @external
    def RemoveQuotes(string: str) -> str:
        if string and string[0] == '"' and string[~0] == '"':
            string = string[1:-1]
        return string

    @external(static=False)
    def CompareText(self, a: str, b: str) -> int:
        return self.CompareStr(a.casefold(), b.casefold())

    @external
    def CompareStr(a: str, b: str) -> int:
        if a > b:
            return +1
        if a < b:
            return -1
        return 0

    @external
    def SameText(a: str, b: str) -> bool:
        return a.casefold() == b.casefold()

    @external
    def SameStr(a: str, b: str) -> bool:
        return a == b

    @external
    def IsWildcard(pattern: str) -> bool:
        return '*' in pattern or '?' in pattern

    @external
    def WildcardMatch(text: str, pattern: str) -> bool:
        return fnmatch.fnmatch(text, pattern)

    @external
    def Trim(string: str) -> str:
        return string.strip()

    @external
    def TrimLeft(string: str) -> str:
        return string.lstrip()

    @external
    def TrimRight(string: str) -> str:
        return string.rstrip()

    @external
    def StringJoin(sep: str, values: List[str]) -> str:
        return sep.join(values)

    @external
    def StringSplitEx(string: str, separators: List[str], quote: str, how: TSplitType) -> List[str]:
        if not quote:
            parts = [string]
        else:
            quote = re.escape(quote)
            parts = re.split(F'({quote}.*?{quote})', string)
        sep = '|'.join(re.escape(s) for s in separators)
        out = []
        if how == TSplitType.stExcludeEmpty:
            sep = F'(?:{sep})+'
        for k in range(0, len(parts)):
            if k & 1 == 1:
                out.append(parts[k])
                continue
            out.extend(re.split(sep, string))
        if how == TSplitType.stExcludeLastEmpty:
            for k in reversed(range(len(out))):
                if not out[k]:
                    out.pop(k)
                    break
        return out

    @external(static=False)
    def StringSplit(self, string: str, separators: List[str], how: TSplitType) -> List[str]:
        return self.StringSplitEx(string, separators, None, how)

    @external(alias='StrToInt64')
    def StrToInt(s: str) -> int:
        return int(s)

    @external(alias='StrToInt64Def')
    def StrToIntDef(s: str, d: int) -> int:
        try:
            return int(s)
        except Exception:
            return d

    @external
    def StrToFloat(s: str) -> float:
        return float(s)

    @external(alias='FloatToStr')
    def IntToStr(i: int) -> str:
        return str(i)

    @external
    def StrToVersion(s: str, v: Variable[int]) -> bool:
        try:
            packed = bytes(map(int, s.split('.')))
        except Exception:
            return False
        if len(packed) != 4:
            return False
        v.set(int.from_bytes(packed, 'little'))
        return True

    @external
    def CharLength(string: str, index: int) -> int:
        return 1

    @external
    def AddBackslash(string: str) -> str:
        if string and string[~0] != '\\':
            string = F'{string}\\'
        return string

    @external
    def AddPeriod(string: str) -> str:
        if string and string[~0] != '.':
            string = F'{string}.'
        return string

    @external(static=False)
    def RemoveBackslashUnlessRoot(self, string: str) -> str:
        path = Path(string)
        if len(path.parts) == 1:
            return str(path)
        return self.RemoveBackslash(string)

    @external
    def RemoveBackslash(string: str) -> str:
        return string.rstrip('\\/')

    @external
    def ChangeFileExt(name: str, ext: str) -> str:
        if not ext.startswith('.'):
            ext = F'.{ext}'
        return str(Path(name).with_suffix(ext))

    @external
    def ExtractFileExt(name: str) -> str:
        return Path(name).suffix

    @external(alias='ExtractFilePath')
    def ExtractFileDir(name: str) -> str:
        dirname = str(Path(name).parent)
        return '' if dirname == '.' else dirname

    @external
    def ExtractFileName(name: str) -> str:
        if name:
            name = Path(name).parts[-1]
        return name

    @external
    def ExtractFileDrive(name: str) -> str:
        if name:
            parts = Path(name).parts
            if len(parts) >= 2 and parts[0] == '\\' and parts[1] == '?':
                parts = parts[2:]
            if parts[0] == '\\':
                if len(parts) >= 3:
                    return '\\'.join(parts[:3])
            else:
                root = parts[0]
                if len(root) == 2 and root[1] == ':':
                    return root
        return ''

    @external
    def ExtractRelativePath(base: str, dst: str) -> str:
        return str(Path(dst).relative_to(base))

    @external(static=False, alias='ExpandUNCFileName')
    def ExpandFileName(self, name: str) -> str:
        if self.ExtractFileDrive(name):
            return name
        return str(self.config.cwd / name)

    @external
    def SetLength(string: Variable[str], size: int):
        old = string.get()
        old = old.ljust(size, '\0')
        string.set(old[:size])

    @external(alias='OemToCharBuff')
    def CharToOemBuff(string: str) -> str:
        # TODO
        return string

    @external
    def Utf8Encode(string: str) -> str:
        return string.encode('utf8').decode('latin1')

    @external
    def Utf8Decode(string: str) -> str:
        return string.encode('latin1').decode('utf8')

    @external
    def GetMD5OfString(string: str) -> str:
        return hashlib.md5(string.encode('latin1')).hexdigest()

    @external
    def GetMD5OfUnicodeString(string: str) -> str:
        return hashlib.md5(string.encode('utf8')).hexdigest()

    @external
    def GetSHA1OfString(string: str) -> str:
        return hashlib.sha1(string.encode('latin1')).hexdigest()

    @external
    def GetSHA1OfUnicodeString(string: str) -> str:
        return hashlib.sha1(string.encode('utf8')).hexdigest()

    @external
    def GetSHA256OfString(string: str) -> str:
        return hashlib.sha256(string.encode('latin1')).hexdigest()

    @external
    def GetSHA256OfUnicodeString(string: str) -> str:
        return hashlib.sha256(string.encode('utf8')).hexdigest()

    @external
    def SysErrorMessage(code: int) -> str:
        return F'[description for error {code:08X}]'

    @external
    def MinimizePathName(path: str, font: object, max_len: int) -> str:
        return path

    @external(static=False)
    def CheckForMutexes(self, mutexes: str) -> bool:
        return any(m in self.mutexes for m in mutexes.split(','))

    @external(static=False)
    def CreateMutex(self, name: str):
        if self.config.log_mutexes:
            yield NewMutex(name)
        self.mutexes.add(name)

    @external(static=False)
    def GetWinDir(self) -> str:
        return self.expand_constant('{win}')

    @external(static=False)
    def GetSystemDir(self) -> str:
        return self.expand_constant('{sys}')

    @external(static=False)
    def GetWindowsVersion(self) -> int:
        version = int.from_bytes(
            struct.pack('>BBH', *self.config.windows_os_version), 'big')
        return version

    @external(static=False)
    def GetWindowsVersionEx(self, tv: Variable[Union[int, bool]]):
        tv[0], tv[1], tv[2] = self.config.windows_os_version # noqa
        tv[3], tv[4]        = self.config.windows_sp_version # noqa
        tv[5], tv[6], tv[7] = True, 0, 0

    @external(static=False)
    def GetWindowsVersionString(self) -> str:
        return '{0}.{1:02d}.{2:04d}'.format(*self.config.windows_os_version)

    @external
    def CreateOleObject(name: str) -> OleObject:
        return OleObject(name)

    @external
    def GetActiveOleObject(name: str) -> OleObject:
        return OleObject(name)

    @external
    def IDispatchInvoke(ole: OleObject, prop_set: bool, name: str, value: Any) -> int:
        return 0

    @external
    def FindWindowByClassName(name: str) -> int:
        return 0

    @external(static=False)
    def WizardSilent(self) -> bool:
        return self.config.wizard_silent

    @external(static=False)
    def SizeOf(self, var: Variable) -> int:
        if var.pointer:
            return (self.config.x64 + 1) * 4
        if var.container:
            return sum(self.SizeOf(x) for x in var.data)
        return var.type.code.width

    del external

Subclasses

Class variables

var external_symbols

Static methods

def TInputDirWizardPage__GetValues(this, k)

An emulated handler for the external symbol TInputDirWizardPage::GetValues.

Expand source code Browse git
@external
def TInputDirWizardPage__GetValues(this: object, k: int) -> str:
    return F'$InputDir{k}'
def TInputFileWizardPage__GetValues(this, k)

An emulated handler for the external symbol TInputFileWizardPage::GetValues.

Expand source code Browse git
@external
def TInputFileWizardPage__GetValues(this: object, k: int) -> str:
    return F'$InputFile{k}'
def kernel32__GetTickCount()

An emulated handler for the external symbol kernel32::GetTickCount.

Expand source code Browse git
@external
def kernel32__GetTickCount() -> int:
    return time.monotonic_ns() // 1_000_000
def user32__GetSystemMetrics(index)

An emulated handler for the external symbol user32::GetSystemMetrics.

Expand source code Browse git
@external
def user32__GetSystemMetrics(index: int) -> int:
    if index == 80:
        return 1
    if index == 43:
        return 2
    return 0
def IsX86Compatible()

An emulated handler for the external symbol IsX86Compatible.

Expand source code Browse git
@external
def IsX86Compatible() -> bool:
    return True
def Terminated()

An emulated handler for the external symbol Terminated.

Expand source code Browse git
@external(alias=[
    'sArm64',
    'IsArm32Compatible',
    'Debugging',
    'IsUninstaller',
])
def Terminated() -> bool:
    return False
def Random(top)

An emulated handler for the external symbol Random.

Expand source code Browse git
@external
def Random(top: int) -> int:
    return random.randrange(0, top)
def WStrGet(string, index)

An emulated handler for the external symbol WStrGet.

Expand source code Browse git
@external(alias='StrGet')
def WStrGet(string: Variable[str], index: int) -> str:
    if index <= 0:
        raise ValueError
    return string[index - 1:index]
def WStrSet(char, index, dst)

An emulated handler for the external symbol WStrSet.

Expand source code Browse git
@external(alias='StrSet')
def WStrSet(char: str, index: int, dst: Variable[str]):
    old = dst.get()
    index -= 1
    dst.set(old[:index] + char + old[index:])
def Beep()

An emulated handler for the external symbol Beep.

Expand source code Browse git
@external
def Beep():
    pass
def DirExists(path)

An emulated handler for the external symbol DirExists.

Expand source code Browse git
@external
def DirExists(path: str) -> bool:
    return True
def ForceDirectories(path)

An emulated handler for the external symbol ForceDirectories.

Expand source code Browse git
@external
def ForceDirectories(path: str) -> bool:
    return True
def LoadStringFromFile(path, out)

An emulated handler for the external symbol LoadStringFromFile.

Expand source code Browse git
@external(alias='LoadStringFromLockedFile')
def LoadStringFromFile(path: str, out: Variable[str]) -> bool:
    return True
def LoadStringsFromFile(path, out)

An emulated handler for the external symbol LoadStringsFromFile.

Expand source code Browse git
@external(alias='LoadStringsFromLockedFile')
def LoadStringsFromFile(path: str, out: Variable[str]) -> bool:
    return True
def DeleteFile(path)

An emulated handler for the external symbol DeleteFile.

Expand source code Browse git
@external
def DeleteFile(path: str) -> bool:
    return True
def FileExists(file_name)

An emulated handler for the external symbol FileExists.

Expand source code Browse git
@external
def FileExists(file_name: str) -> bool:
    return False
def Log(log)

An emulated handler for the external symbol Log.

Expand source code Browse git
@external
def Log(log: str):
    ...
def Inc(p)

An emulated handler for the external symbol Inc.

Expand source code Browse git
@external
def Inc(p: Variable[Variable[int]]):
    p.set(p.get() + 1)
def Dec(p)

An emulated handler for the external symbol Dec.

Expand source code Browse git
@external
def Dec(p: Variable[Variable[int]]):
    p.set(p.get() - 1)
def FindFirst(file_name, frec)

An emulated handler for the external symbol FindFirst.

Expand source code Browse git
@external
def FindFirst(file_name: str, frec: Variable) -> bool:
    return False
def Trunc(x)

An emulated handler for the external symbol Trunc.

Expand source code Browse git
@external
def Trunc(x: float) -> float:
    return math.trunc(x)
def GetSpaceOnDisk(path, in_megabytes, avail, space)

An emulated handler for the external symbol GetSpaceOnDisk.

Expand source code Browse git
@external
def GetSpaceOnDisk(
    path: str,
    in_megabytes: bool,
    avail: Variable[int],
    space: Variable[int],
) -> bool:
    _a = 3_000_000
    _t = 5_000_000
    if not in_megabytes:
        _a *= 1000
        _t *= 1000
    avail.set(_a)
    space.set(_t)
    return True
def GetSpaceOnDisk64(path, avail, space)

An emulated handler for the external symbol GetSpaceOnDisk64.

Expand source code Browse git
@external
def GetSpaceOnDisk64(
    path: str,
    avail: Variable[int],
    space: Variable[int],
) -> bool:
    avail.set(3_000_000_000)
    space.set(5_000_000_000)
    return True
def Exec(exe, cmd, cwd, show, wait, out)

An emulated handler for the external symbol Exec.

Expand source code Browse git
@external
def Exec(
    exe: str,
    cmd: str,
    cwd: str,
    show: int,
    wait: int,
    out: Variable[int],
) -> bool:
    out.set(0)
    return True
def GetCmdTail()

An emulated handler for the external symbol GetCmdTail.

Expand source code Browse git
@external
def GetCmdTail() -> str:
    return ''
def ParamCount()

An emulated handler for the external symbol ParamCount.

Expand source code Browse git
@external
def ParamCount() -> int:
    return 0
def ParamStr(index)

An emulated handler for the external symbol ParamStr.

Expand source code Browse git
@external
def ParamStr(index: int) -> str:
    return ''
def ActiveLanguage()

An emulated handler for the external symbol ActiveLanguage.

Expand source code Browse git
@external
def ActiveLanguage() -> str:
    return 'en'
def FmtMessage(fmt, args)

An emulated handler for the external symbol FmtMessage.

Expand source code Browse git
@external
def FmtMessage(fmt: str, args: List[str]) -> str:
    fmt = fmt.replace('{', '{{')
    fmt = fmt.replace('}', '}}')
    fmt = '%'.join(re.sub('%(\\d+)', '{\\1}', p) for p in fmt.split('%%'))
    return fmt.format(*args)
def Format(fmt, args)

An emulated handler for the external symbol Format.

Expand source code Browse git
@external
def Format(fmt: str, args: List[Union[str, int, float]]) -> str:
    try:
        formatted = fmt % tuple(args)
    except Exception:
        raise IFPSException('invalid format')
    else:
        return formatted
def RaiseException(msg)

An emulated handler for the external symbol RaiseException.

Expand source code Browse git
@external
def RaiseException(msg: str):
    raise IFPSException(msg)
def GetArrayLength(array)

An emulated handler for the external symbol GetArrayLength.

Expand source code Browse git
@external
def GetArrayLength(array: Variable) -> int:
    array = array.deref()
    return len(array)
def SetArrayLength(array, n)

An emulated handler for the external symbol SetArrayLength.

Expand source code Browse git
@external
def SetArrayLength(array: Variable, n: int):
    a = array.deref()
    a.resize(n)
def Unassigned()

An emulated handler for the external symbol Unassigned.

Expand source code Browse git
@external
def Unassigned() -> None:
    return None
def Null()

An emulated handler for the external symbol Null.

Expand source code Browse git
@external
def Null() -> None:
    return None
def Chr(b)

An emulated handler for the external symbol Chr.

Expand source code Browse git
@external
def Chr(b: int) -> str:
    return chr(b)
def Ord(c)

An emulated handler for the external symbol Ord.

Expand source code Browse git
@external
def Ord(c: str) -> int:
    return ord(c)
def Copy(string, index, count)

An emulated handler for the external symbol Copy.

Expand source code Browse git
@external
def Copy(string: str, index: int, count: int) -> str:
    index -= 1
    return string[index:index + count]
def Length(string)

An emulated handler for the external symbol Length.

Expand source code Browse git
@external
def Length(string: str) -> int:
    return len(string)
def Lowercase(string)

An emulated handler for the external symbol Lowercase.

Expand source code Browse git
@external(alias='AnsiLowercase')
def Lowercase(string: str) -> str:
    return string.lower()
def Uppercase(string)

An emulated handler for the external symbol Uppercase.

Expand source code Browse git
@external(alias='AnsiUppercase')
def Uppercase(string: str) -> str:
    return string.upper()
def StringOfChar(c, count)

An emulated handler for the external symbol StringOfChar.

Expand source code Browse git
@external
def StringOfChar(c: str, count: int) -> str:
    return c * count
def Delete(string, index, count)

An emulated handler for the external symbol Delete.

Expand source code Browse git
@external
def Delete(string: Variable[str], index: int, count: int):
    index -= 1
    old = string.get()
    string.set(old[:index] + old[index + count:])
def Insert(string, dest, index)

An emulated handler for the external symbol Insert.

Expand source code Browse git
@external
def Insert(string: str, dest: Variable[str], index: int):
    index -= 1
    old = dest.get()
    dest.set(old[:index] + string + old[index:])
def StringChangeEx(string, old, new, _)

An emulated handler for the external symbol StringChangeEx.

Expand source code Browse git
@external
def StringChangeEx(string: Variable[str], old: str, new: str, _: bool) -> int:
    haystack = string.get()
    count = haystack.count(old)
    string.set(haystack.replace(old, new))
    return count
def Pos(string, sub)

An emulated handler for the external symbol Pos.

Expand source code Browse git
@external
def Pos(string: str, sub: str) -> int:
    return string.find(sub) + 1
def AddQuotes(string)

An emulated handler for the external symbol AddQuotes.

Expand source code Browse git
@external
def AddQuotes(string: str) -> str:
    if string and (string[0] != '"' or string[~0] != '"') and ' ' in string:
        string = F'"{string}"'
    return string
def RemoveQuotes(string)

An emulated handler for the external symbol RemoveQuotes.

Expand source code Browse git
@external
def RemoveQuotes(string: str) -> str:
    if string and string[0] == '"' and string[~0] == '"':
        string = string[1:-1]
    return string
def CompareStr(a, b)

An emulated handler for the external symbol CompareStr.

Expand source code Browse git
@external
def CompareStr(a: str, b: str) -> int:
    if a > b:
        return +1
    if a < b:
        return -1
    return 0
def SameText(a, b)

An emulated handler for the external symbol SameText.

Expand source code Browse git
@external
def SameText(a: str, b: str) -> bool:
    return a.casefold() == b.casefold()
def SameStr(a, b)

An emulated handler for the external symbol SameStr.

Expand source code Browse git
@external
def SameStr(a: str, b: str) -> bool:
    return a == b
def IsWildcard(pattern)

An emulated handler for the external symbol IsWildcard.

Expand source code Browse git
@external
def IsWildcard(pattern: str) -> bool:
    return '*' in pattern or '?' in pattern
def WildcardMatch(text, pattern)

An emulated handler for the external symbol WildcardMatch.

Expand source code Browse git
@external
def WildcardMatch(text: str, pattern: str) -> bool:
    return fnmatch.fnmatch(text, pattern)
def Trim(string)

An emulated handler for the external symbol Trim.

Expand source code Browse git
@external
def Trim(string: str) -> str:
    return string.strip()
def TrimLeft(string)

An emulated handler for the external symbol TrimLeft.

Expand source code Browse git
@external
def TrimLeft(string: str) -> str:
    return string.lstrip()
def TrimRight(string)

An emulated handler for the external symbol TrimRight.

Expand source code Browse git
@external
def TrimRight(string: str) -> str:
    return string.rstrip()
def StringJoin(sep, values)

An emulated handler for the external symbol StringJoin.

Expand source code Browse git
@external
def StringJoin(sep: str, values: List[str]) -> str:
    return sep.join(values)
def StringSplitEx(string, separators, quote, how)

An emulated handler for the external symbol StringSplitEx.

Expand source code Browse git
@external
def StringSplitEx(string: str, separators: List[str], quote: str, how: TSplitType) -> List[str]:
    if not quote:
        parts = [string]
    else:
        quote = re.escape(quote)
        parts = re.split(F'({quote}.*?{quote})', string)
    sep = '|'.join(re.escape(s) for s in separators)
    out = []
    if how == TSplitType.stExcludeEmpty:
        sep = F'(?:{sep})+'
    for k in range(0, len(parts)):
        if k & 1 == 1:
            out.append(parts[k])
            continue
        out.extend(re.split(sep, string))
    if how == TSplitType.stExcludeLastEmpty:
        for k in reversed(range(len(out))):
            if not out[k]:
                out.pop(k)
                break
    return out
def StrToInt(s)

An emulated handler for the external symbol StrToInt.

Expand source code Browse git
@external(alias='StrToInt64')
def StrToInt(s: str) -> int:
    return int(s)
def StrToIntDef(s, d)

An emulated handler for the external symbol StrToIntDef.

Expand source code Browse git
@external(alias='StrToInt64Def')
def StrToIntDef(s: str, d: int) -> int:
    try:
        return int(s)
    except Exception:
        return d
def StrToFloat(s)

An emulated handler for the external symbol StrToFloat.

Expand source code Browse git
@external
def StrToFloat(s: str) -> float:
    return float(s)
def IntToStr(i)

An emulated handler for the external symbol IntToStr.

Expand source code Browse git
@external(alias='FloatToStr')
def IntToStr(i: int) -> str:
    return str(i)
def StrToVersion(s, v)

An emulated handler for the external symbol StrToVersion.

Expand source code Browse git
@external
def StrToVersion(s: str, v: Variable[int]) -> bool:
    try:
        packed = bytes(map(int, s.split('.')))
    except Exception:
        return False
    if len(packed) != 4:
        return False
    v.set(int.from_bytes(packed, 'little'))
    return True
def CharLength(string, index)

An emulated handler for the external symbol CharLength.

Expand source code Browse git
@external
def CharLength(string: str, index: int) -> int:
    return 1
def AddBackslash(string)

An emulated handler for the external symbol AddBackslash.

Expand source code Browse git
@external
def AddBackslash(string: str) -> str:
    if string and string[~0] != '\\':
        string = F'{string}\\'
    return string
def AddPeriod(string)

An emulated handler for the external symbol AddPeriod.

Expand source code Browse git
@external
def AddPeriod(string: str) -> str:
    if string and string[~0] != '.':
        string = F'{string}.'
    return string
def RemoveBackslash(string)

An emulated handler for the external symbol RemoveBackslash.

Expand source code Browse git
@external
def RemoveBackslash(string: str) -> str:
    return string.rstrip('\\/')
def ChangeFileExt(name, ext)

An emulated handler for the external symbol ChangeFileExt.

Expand source code Browse git
@external
def ChangeFileExt(name: str, ext: str) -> str:
    if not ext.startswith('.'):
        ext = F'.{ext}'
    return str(Path(name).with_suffix(ext))
def ExtractFileExt(name)

An emulated handler for the external symbol ExtractFileExt.

Expand source code Browse git
@external
def ExtractFileExt(name: str) -> str:
    return Path(name).suffix
def ExtractFileDir(name)

An emulated handler for the external symbol ExtractFileDir.

Expand source code Browse git
@external(alias='ExtractFilePath')
def ExtractFileDir(name: str) -> str:
    dirname = str(Path(name).parent)
    return '' if dirname == '.' else dirname
def ExtractFileName(name)

An emulated handler for the external symbol ExtractFileName.

Expand source code Browse git
@external
def ExtractFileName(name: str) -> str:
    if name:
        name = Path(name).parts[-1]
    return name
def ExtractFileDrive(name)

An emulated handler for the external symbol ExtractFileDrive.

Expand source code Browse git
@external
def ExtractFileDrive(name: str) -> str:
    if name:
        parts = Path(name).parts
        if len(parts) >= 2 and parts[0] == '\\' and parts[1] == '?':
            parts = parts[2:]
        if parts[0] == '\\':
            if len(parts) >= 3:
                return '\\'.join(parts[:3])
        else:
            root = parts[0]
            if len(root) == 2 and root[1] == ':':
                return root
    return ''
def ExtractRelativePath(base, dst)

An emulated handler for the external symbol ExtractRelativePath.

Expand source code Browse git
@external
def ExtractRelativePath(base: str, dst: str) -> str:
    return str(Path(dst).relative_to(base))
def SetLength(string, size)

An emulated handler for the external symbol SetLength.

Expand source code Browse git
@external
def SetLength(string: Variable[str], size: int):
    old = string.get()
    old = old.ljust(size, '\0')
    string.set(old[:size])
def CharToOemBuff(string)

An emulated handler for the external symbol CharToOemBuff.

Expand source code Browse git
@external(alias='OemToCharBuff')
def CharToOemBuff(string: str) -> str:
    # TODO
    return string
def Utf8Encode(string)

An emulated handler for the external symbol Utf8Encode.

Expand source code Browse git
@external
def Utf8Encode(string: str) -> str:
    return string.encode('utf8').decode('latin1')
def Utf8Decode(string)

An emulated handler for the external symbol Utf8Decode.

Expand source code Browse git
@external
def Utf8Decode(string: str) -> str:
    return string.encode('latin1').decode('utf8')
def GetMD5OfString(string)

An emulated handler for the external symbol GetMD5OfString.

Expand source code Browse git
@external
def GetMD5OfString(string: str) -> str:
    return hashlib.md5(string.encode('latin1')).hexdigest()
def GetMD5OfUnicodeString(string)

An emulated handler for the external symbol GetMD5OfUnicodeString.

Expand source code Browse git
@external
def GetMD5OfUnicodeString(string: str) -> str:
    return hashlib.md5(string.encode('utf8')).hexdigest()
def GetSHA1OfString(string)

An emulated handler for the external symbol GetSHA1OfString.

Expand source code Browse git
@external
def GetSHA1OfString(string: str) -> str:
    return hashlib.sha1(string.encode('latin1')).hexdigest()
def GetSHA1OfUnicodeString(string)

An emulated handler for the external symbol GetSHA1OfUnicodeString.

Expand source code Browse git
@external
def GetSHA1OfUnicodeString(string: str) -> str:
    return hashlib.sha1(string.encode('utf8')).hexdigest()
def GetSHA256OfString(string)

An emulated handler for the external symbol GetSHA256OfString.

Expand source code Browse git
@external
def GetSHA256OfString(string: str) -> str:
    return hashlib.sha256(string.encode('latin1')).hexdigest()
def GetSHA256OfUnicodeString(string)

An emulated handler for the external symbol GetSHA256OfUnicodeString.

Expand source code Browse git
@external
def GetSHA256OfUnicodeString(string: str) -> str:
    return hashlib.sha256(string.encode('utf8')).hexdigest()
def SysErrorMessage(code)

An emulated handler for the external symbol SysErrorMessage.

Expand source code Browse git
@external
def SysErrorMessage(code: int) -> str:
    return F'[description for error {code:08X}]'
def MinimizePathName(path, font, max_len)

An emulated handler for the external symbol MinimizePathName.

Expand source code Browse git
@external
def MinimizePathName(path: str, font: object, max_len: int) -> str:
    return path
def CreateOleObject(name)

An emulated handler for the external symbol CreateOleObject.

Expand source code Browse git
@external
def CreateOleObject(name: str) -> OleObject:
    return OleObject(name)
def GetActiveOleObject(name)

An emulated handler for the external symbol GetActiveOleObject.

Expand source code Browse git
@external
def GetActiveOleObject(name: str) -> OleObject:
    return OleObject(name)
def IDispatchInvoke(ole, prop_set, name, value)

An emulated handler for the external symbol IDispatchInvoke.

Expand source code Browse git
@external
def IDispatchInvoke(ole: OleObject, prop_set: bool, name: str, value: Any) -> int:
    return 0
def FindWindowByClassName(name)

An emulated handler for the external symbol FindWindowByClassName.

Expand source code Browse git
@external
def FindWindowByClassName(name: str) -> int:
    return 0

Instance variables

var constant_map
Expand source code
def __get__(self, instance, owner=None):
    if instance is None:
        return self
    if self.attrname is None:
        raise TypeError(
            "Cannot use cached_property instance without calling __set_name__ on it.")
    try:
        cache = instance.__dict__
    except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
        msg = (
            f"No '__dict__' attribute on {type(instance).__name__!r} "
            f"instance to cache {self.attrname!r} property."
        )
        raise TypeError(msg) from None
    val = cache.get(self.attrname, _NOT_FOUND)
    if val is _NOT_FOUND:
        with self.lock:
            # check if another thread filled cache while we awaited lock
            val = cache.get(self.attrname, _NOT_FOUND)
            if val is _NOT_FOUND:
                val = self.func(instance)
                try:
                    cache[self.attrname] = val
                except TypeError:
                    msg = (
                        f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                        f"does not support item assignment for caching {self.attrname!r} property."
                    )
                    raise TypeError(msg) from None
    return val

Methods

def reset(self)

Reset the emulator timing, FPU word, mutexes, trace, and stack. All global variables are set to their default values.

Expand source code Browse git
def reset(self):
    """
    Reset the emulator timing, FPU word, mutexes, trace, and stack. All global variables are
    set to their default values.
    """
    self.seconds_slept = 0.0
    self.clock = 0
    self.fpucw = FPUControl.MaxPrecision | FPUControl.RoundTowardZero
    self.jumpflag = False
    self.mutexes.clear()
    self.stack.clear()
    for v in self.globals:
        v.setdefault()
    return self
def unimplemented(self, function)

The base IFPS emulator raises NeedSymbol when an external symbol is unimplemented. Child classes can override this function to handle the missing symbol differently.

Expand source code Browse git
def unimplemented(self, function: Function):
    """
    The base IFPS emulator raises `refinery.lib.inno.emulator.NeedSymbol` when an external
    symbol is unimplemented. Child classes can override this function to handle the missing
    symbol differently.
    """
    raise NeedSymbol(function.name)
def emulate_function(self, function, *args)

Emulate a function call to the given function, passing the given arguments. The method returns the return value of the emulated function call if it is not a procedure.

Expand source code Browse git
@EventCall.Wrap
def emulate_function(self, function: Function, *args):
    """
    Emulate a function call to the given function, passing the given arguments. The method
    returns the return value of the emulated function call if it is not a procedure.
    """
    self.stack.clear()
    decl = function.decl
    if decl is None:
        raise NotImplementedError(F'Do not know how to call {function!s}.')
    if (n := len(decl.parameters)) != (m := len(args)):
        raise ValueError(
            F'Function {function!s} expects {n} arguments, only {m} were given.')
    for index, (argument, parameter) in enumerate(zip(args, decl.parameters), 1):
        variable = Variable(parameter.type, VariableSpec(index, VariableType.Local))
        variable.set(argument)
        self.stack.append(variable)
    self.stack.reverse()
    if not decl.void:
        result = Variable(decl.return_type, VariableSpec(0, VariableType.Argument))
        self.stack.append(result)
    yield from self.call(function)
    self.stack.clear()
    if not decl.void:
        return result.get()
def call(self, function)

Begin emulating at the start of the given function.

Expand source code Browse git
def call(self, function: Function):
    """
    Begin emulating at the start of the given function.
    """

    def operator_div(a, b):
        return a // b if isinstance(a, int) and isinstance(b, int) else a / b

    def operator_in(a, b):
        return a in b

    def getvar(op: Union[VariableSpec, Operand]) -> Variable:
        if not isinstance(op, Operand):
            v = op
            k = None
        elif op.type is OperandType.Value:
            raise TypeError('Attempting to retrieve variable for an immediate operand.')
        else:
            v = op.variable
            k = op.index
            if op.type is OperandType.IndexedByVar:
                k = getvar(k).get()
        t, i = v.type, v.index
        if t is VariableType.Argument:
            if function.decl.void:
                i -= 1
            var = self.stack[sp - i]
        elif t is VariableType.Global:
            var = self.globals[i]
        elif t is VariableType.Local:
            var = self.stack[sp + i]
        else:
            raise TypeError
        if k is not None:
            var = var.at(k)
        return var

    def getval(op: Operand):
        if op.immediate:
            return op.value.value
        return getvar(op).get()

    def setval(op: Operand, new):
        if op.immediate:
            raise RuntimeError('attempt to assign to an immediate')
        getvar(op).set(new)

    class CallState(NamedTuple):
        fn: Function
        ip: int
        sp: int
        eh: List[ExceptionHandler]

    callstack: List[CallState] = []
    exec_start = process_time()
    stack = self.stack
    _cfg_max_call_stack = self.config.max_call_stack
    _cfg_max_data_stack = self.config.max_data_stack
    _cfg_max_seconds = self.config.max_seconds
    _cfg_max_opcodes = self.config.max_opcodes
    _cfg_log_opcodes = self.config.log_opcodes

    ip: int = 0
    sp: int = len(stack) - 1
    pending_exception = None
    exceptions = []

    while True:
        if 0 < _cfg_max_call_stack < len(callstack):
            raise EmulatorMaxCalls

        if function.body is None:
            namespace = ''

            if decl := function.decl:
                if decl.is_property:
                    if stack[-1].type.code == TC.Class:
                        function = function.setter
                    else:
                        function = function.getter
                    decl = function.decl
                namespace = (
                    decl.classname or decl.module or '')

            name = function.name
            registry: Dict[str, IFPSEmulatedFunction] = self.external_symbols.get(namespace, {})
            handler = registry.get(name)

            if handler:
                void = handler.void
                argc = handler.argc
            elif decl:
                void = decl.void
                argc = decl.argc
            else:
                void = True
                argc = 0

            try:
                rpos = 0 if void else 1
                args = [stack[~k] for k in range(rpos, argc + rpos)]
            except IndexError:
                raise EmulatorException(
                    F'Cannot call {function!s}; {argc} arguments + {rpos} return values expected,'
                    F' but stack size is only {len(stack)}.')

            if self.config.log_calls:
                yield NewFunctionCall(str(function), tuple(a.get() for a in args))

            if handler is None:
                self.unimplemented(function)
            else:
                if decl and (decl.void != handler.void or decl.argc != handler.argc):
                    ok = False
                    if 1 + decl.argc - decl.void == 1 + handler.argc - handler.void:
                        if decl.void and not decl.parameters[0].const:
                            ok = True
                        elif handler.void and handler.spec[0]:
                            ok = True
                    if not ok:
                        raise RuntimeError(F'Handler for {function!s} is incompatible with declaration.')
                for k, (var, byref) in enumerate(zip(args, handler.spec)):
                    if not byref:
                        args[k] = var.get()
                if not handler.static:
                    args.insert(0, self)
                try:
                    return_value = handler.call(*args)
                    if inspect.isgenerator(return_value):
                        return_value = yield from return_value
                except GeneratorExit:
                    pass
                except BaseException as b:
                    pending_exception = IFPSException(F'Error calling {function.name}: {b!s}', b)
                else:
                    if not handler.void:
                        stack[-1].set(return_value)
            if not callstack:
                if pending_exception is None:
                    return
                raise pending_exception
            function, ip, sp, exceptions = callstack.pop()
            continue

        while insn := function.code.get(ip, None):
            if 0 < _cfg_max_seconds < process_time() - exec_start:
                raise EmulatorTimeout
            if 0 < _cfg_max_opcodes < self.clock:
                raise EmulatorExecutionLimit
            if 0 < _cfg_max_data_stack < len(stack):
                raise EmulatorMaxStack
            if _cfg_log_opcodes:
                yield NewInstruction(function, insn)
            try:
                if pe := pending_exception:
                    pending_exception = None
                    raise pe

                opc = insn.opcode
                ip += insn.size
                self.clock += 1

                if opc == Op.Nop:
                    continue
                elif opc == Op.Assign:
                    dst = getvar(insn.op(0))
                    src = insn.op(1)
                    if src.immediate:
                        dst.set(src.value)
                    else:
                        dst.set(getvar(src))
                elif opc == Op.Calculate:
                    calculate = {
                        AOp.Add: operator.add,
                        AOp.Sub: operator.sub,
                        AOp.Mul: operator.mul,
                        AOp.Div: operator_div,
                        AOp.Mod: operator.mod,
                        AOp.Shl: operator.lshift,
                        AOp.Shr: operator.rshift,
                        AOp.And: operator.and_,
                        AOp.BOr: operator.or_,
                        AOp.Xor: operator.xor,
                    }[insn.operator]
                    src = insn.op(1)
                    dst = insn.op(0)
                    sv = getval(src)
                    dv = getval(dst)
                    fpu = isinstance(sv, float) or isinstance(dv, float)
                    try:
                        result = calculate(dv, sv)
                        if fpu and not isinstance(result, float):
                            raise FloatingPointError
                    except FloatingPointError as FPE:
                        if not self.fpucw & FPUControl.InvalidOperation:
                            result = float('nan')
                        else:
                            raise IFPSException('invalid operation', FPE) from FPE
                    except OverflowError as OFE:
                        if fpu and self.fpucw & FPUControl.Overflow:
                            result = float('nan')
                        else:
                            raise IFPSException('arithmetic overflow', OFE) from OFE
                    except ZeroDivisionError as ZDE:
                        if fpu and self.fpucw & FPUControl.ZeroDivide:
                            result = float('nan')
                        else:
                            raise IFPSException('division by zero', ZDE) from ZDE
                    setval(dst, result)
                elif opc == Op.Push:
                    # TODO: I do not actually know how this works
                    stack.append(getval(insn.op(0)))
                elif opc == Op.PushVar:
                    stack.append(getvar(insn.op(0)))
                elif opc == Op.Pop:
                    self.temp = stack.pop()
                elif opc == Op.Call:
                    callstack.append(CallState(function, ip, sp, exceptions))
                    function = insn.operands[0]
                    ip = 0
                    sp = len(stack) - 1
                    exceptions = []
                    break
                elif opc == Op.Jump:
                    ip = insn.operands[0]
                elif opc == Op.JumpTrue:
                    if getval(insn.op(1)):
                        ip = insn.operands[0]
                elif opc == Op.JumpFalse:
                    if not getval(insn.op(1)):
                        ip = insn.operands[0]
                elif opc == Op.Ret:
                    del stack[sp + 1:]
                    if not callstack:
                        return
                    function, ip, sp, exceptions = callstack.pop()
                    break
                elif opc == Op.StackType:
                    raise OpCodeNotImplemented(str(opc))
                elif opc == Op.PushType:
                    stack.append(Variable(
                        insn.operands[0],
                        VariableSpec(len(stack) - sp, VariableType.Local)
                    ))
                elif opc == Op.Compare:
                    compare = {
                        COp.GE: operator.ge,
                        COp.LE: operator.le,
                        COp.GT: operator.gt,
                        COp.LT: operator.lt,
                        COp.NE: operator.ne,
                        COp.EQ: operator.eq,
                        COp.IN: operator_in,
                        COp.IS: operator.is_,
                    }[insn.operator]
                    d = getvar(insn.op(0))
                    a = getval(insn.op(1))
                    b = getval(insn.op(2))
                    d.set(compare(a, b))
                elif opc == Op.CallVar:
                    call = getval(insn.op(0))
                    if isinstance(call, int):
                        call = self.ifps.functions[call]
                    if isinstance(call, Function):
                        callstack.append(CallState(function, ip, sp, exceptions))
                        function = call
                        ip = 0
                        sp = len(stack) - 1
                        exceptions = []
                        break
                elif opc in (Op.SetPtr, Op.SetPtrToCopy):
                    copy = False
                    if opc == Op.SetPtrToCopy:
                        copy = True
                    dst = getvar(insn.op(0))
                    src = getvar(insn.op(1))
                    dst.setptr(src, copy=copy)
                elif opc == Op.BooleanNot:
                    setval(a := insn.op(0), not getval(a))
                elif opc == Op.IntegerNot:
                    setval(a := insn.op(0), ~getval(a))
                elif opc == Op.Neg:
                    setval(a := insn.op(0), -getval(a))
                elif opc == Op.SetFlag:
                    condition, negated = insn.operands
                    self.jumpflag = getval(condition) ^ negated
                elif opc == Op.JumpFlag:
                    if self.jumpflag:
                        ip = insn.operands[0]
                elif opc == Op.PushEH:
                    exceptions.append(ExceptionHandler(*insn.operands))
                elif opc == Op.PopEH:
                    tp = None
                    et = EHType(insn.operands[0])
                    eh = exceptions[-1]
                    if eh.current != et:
                        raise RuntimeError(F'Expected {eh.current} block to end, but {et} was ended instead.')
                    while tp is None:
                        if et is None:
                            raise RuntimeError
                        tp, et = {
                            EHType.Catch         : (eh.finally_one, EHType.Finally),
                            EHType.Try           : (eh.finally_one, EHType.Finally),
                            EHType.Finally       : (eh.finally_two, EHType.SecondFinally),
                            EHType.SecondFinally : (eh.handler_end, None),
                        }[et]
                    eh.current = et
                    ip = tp
                    if et is None:
                        exceptions.pop()
                elif opc == Op.Inc:
                    setval(a := insn.op(0), getval(a) + 1)
                elif opc == Op.Dec:
                    setval(a := insn.op(0), getval(a) - 1)
                elif opc == Op.JumpPop1:
                    stack.pop()
                    ip = insn.operands[0]
                elif opc == Op.JumpPop2:
                    stack.pop()
                    stack.pop()
                    ip = insn.operands[0]
                else:
                    raise RuntimeError(F'Function contains invalid opcode at 0x{ip:X}.')
            except IFPSException as EE:
                try:
                    eh = exceptions[-1]
                except IndexError:
                    raise EE
                et = EHType.Try
                tp = None
                while tp is None:
                    if et is None:
                        raise RuntimeError
                    tp, et = {
                        EHType.Try           : (eh.catch_error, EHType.Catch),
                        EHType.Catch         : (eh.finally_one, EHType.Finally),
                        EHType.Finally       : (eh.finally_two, EHType.SecondFinally),
                        EHType.SecondFinally : (eh.handler_end, None),
                    }[et]
                if et is None:
                    raise EE
                eh.current = et
                ip = tp
            except AbortEmulation:
                raise
            except EmulatorException:
                raise
            except Exception as RE:
                raise EmulatorException(
                    F'In {function.symbol} at 0x{insn.offset:X} (cycle {self.clock}), '
                    F'emulation of {insn!r} failed: {RE!s}')
        if ip is None:
            raise RuntimeError(F'Instruction pointer moved out of bounds to 0x{ip:X}.')
def TPasswordEdit__SetText(self, this, value)

An emulated handler for the external symbol TPasswordEdit::SetText.

Expand source code Browse git
@external(static=False)
def TPasswordEdit__SetText(self, this: object, value: str):
    if value:
        yield NewPassword(value)
    return value
def IsAdmin(self)

An emulated handler for the external symbol IsAdmin.

Expand source code Browse git
@external(static=False)
def IsAdmin(self) -> bool:
    return self.config.admin
def kernel32__Sleep(self, ms)

An emulated handler for the external symbol kernel32::Sleep.

Expand source code Browse git
@external(static=False, alias='Sleep')
def kernel32__Sleep(self, ms: int):
    seconds = ms / 1000.0
    self.seconds_slept += seconds
    time.sleep(seconds * self.config.sleep_scale)
def GetEnv(self, name)

An emulated handler for the external symbol GetEnv.

Expand source code Browse git
@external(static=False)
def GetEnv(self, name: str) -> str:
    return self.config.environment.get(name, F'%{name}%')
def Abort(self)

An emulated handler for the external symbol Abort.

Expand source code Browse git
@external(static=False)
def Abort(self):
    if self.config.throw_abort:
        raise AbortEmulation
def ExpandConstant(self, string)

An emulated handler for the external symbol ExpandConstant.

Expand source code Browse git
@external(static=False)
def ExpandConstant(self, string: str) -> str:
    return self.expand_constant(string)
def ExpandConstantEx(self, string, custom_var, custom_val)

An emulated handler for the external symbol ExpandConstantEx.

Expand source code Browse git
@external(static=False)
def ExpandConstantEx(self, string: str, custom_var: str, custom_val: str) -> str:
    return self.expand_constant(string, custom_var, custom_val)
def expand_constant(self, string, custom_var=None, custom_val=None, unescape=False)
Expand source code Browse git
def expand_constant(
    self,
    string: str,
    custom_var: Optional[str] = None,
    custom_val: Optional[str] = None,
    unescape: bool = False
):
    config = self.config
    expand = partial(self.expand_constant, unescape=True)
    string = re.sub(r'(\{\{.*?\}(?!\}))', '\\1}', string)

    with io.StringIO() as result:
        constants = self.constant_map
        formatter = Formatter()
        backslash = False
        try:
            parsed = list(formatter.parse(string))
        except ValueError as VE:
            raise IFPSException(F'invalid format string: {string!r}', VE) from VE
        for prefix, spec, modifier, conversion in parsed:
            if backslash and prefix[:1] == '\\':
                prefix = prefix[1:]
            if unescape:
                prefix = unquote(prefix)
            result.write(prefix)
            if spec is None:
                continue
            elif spec == '\\':
                if modifier or conversion:
                    raise IFPSException('Invalid format string.', ValueError(string))
                value = spec
            elif spec == custom_var:
                value = custom_val
            elif spec.startswith('%'):
                name, p, default = spec[1:].partition('|')
                name = expand(name)
                default = expand(default)
                try:
                    value = config.environment[name]
                except KeyError:
                    value = default if p else F'%{name}%'
            elif spec == 'drive':
                value = self.ExtractFileDrive(expand(modifier))
            elif spec == 'ini':
                # {ini:Filename,Section,Key|DefaultValue}
                _, _, default = modifier.partition('|')
                value = expand(default)
            elif spec == 'code':
                # {code:FunctionName|Param}
                symbol, _, param = modifier.partition('|')
                param = expand(param)
                try:
                    function = self.symbols[symbol]
                except KeyError as KE:
                    raise IFPSException(F'String formatter references missing function {symbol}.', KE) from KE
                emulation = self.emulate_function(function, param)
                value = str(emulation.value)
            elif spec == 'cm':
                # {cm:LaunchProgram,Inno Setup}
                # The example above translates to "Launch Inno Setup" if English is the active language.
                name, _, placeholders = modifier.partition(',')
                value = self.CustomMessage(expand(name))
                if placeholders:
                    def _placeholder(match: re.Match[str]):
                        try:
                            return placeholders[int(match[1]) - 1]
                        except Exception:
                            return match[0]
                    placeholders = [ph.strip() for ph in placeholders.split(',')]
                    value = re.sub('(?<!%)%([1-9]\\d*)', _placeholder, value)
            elif spec == 'reg':
                # {reg:HKXX\SubkeyName,ValueName|DefaultValue}
                _, _, default = modifier.partition('|')
                value = expand(default)
            elif spec == 'param':
                # {param:ParamName|DefaultValue}
                _, _, default = modifier.partition('|')
                value = expand(default)
            else:
                try:
                    value = constants[spec]
                except KeyError as KE:
                    raise IFPSException(F'invalid format field {spec}', KE) from KE
            backslash = value.endswith('\\')
            result.write(value)
        return result.getvalue()
def CustomMessage(self, msg_name)

An emulated handler for the external symbol CustomMessage.

Expand source code Browse git
@external(static=False)
def CustomMessage(self, msg_name: str) -> str:
    by_language = {}
    for msg in self.inno.setup_info.Messages:
        if msg.EncodedName == msg_name:
            lng = msg.get_language_value().Name
            if lng == self.config.language:
                return msg.Value
            by_language[lng] = msg.Value
    try:
        return by_language[0]
    except KeyError:
        pass
    try:
        return next(iter(by_language.values()))
    except StopIteration:
        raise IFPSException(F'Custom message with name {msg_name} not found.')
def SetupMessage(self, id)

An emulated handler for the external symbol SetupMessage.

Expand source code Browse git
@external(static=False)
def SetupMessage(self, id: int) -> str:
    try:
        return self.inno.setup_info.Messages[id].Value
    except (AttributeError, IndexError):
        return ''
def IsWin64(self)

An emulated handler for the external symbol IsWin64.

Expand source code Browse git
@external(static=False, alias=['Is64BitInstallMode', 'IsX64Compatible', 'IsX64OS'])
def IsWin64(self) -> bool:
    return self.config.x64
def IsX86OS(self)

An emulated handler for the external symbol IsX86OS.

Expand source code Browse git
@external(static=False)
def IsX86OS(self) -> bool:
    return not self.config.x64
def ProcessorArchitecture(self)

An emulated handler for the external symbol ProcessorArchitecture.

Expand source code Browse git
@external(static=False)
def ProcessorArchitecture(self) -> int:
    if self.config.x64:
        return TSetupProcessorArchitecture.paX64.value
    else:
        return TSetupProcessorArchitecture.paX86.value
def GetUserNameString(self)

An emulated handler for the external symbol GetUserNameString.

Expand source code Browse git
@external(static=False)
def GetUserNameString(self) -> str:
    return self.config.user_name
def GetComputerNameString(self)

An emulated handler for the external symbol GetComputerNameString.

Expand source code Browse git
@external(static=False)
def GetComputerNameString(self) -> str:
    return self.config.host_name
def GetUILanguage(self)

An emulated handler for the external symbol GetUILanguage.

Expand source code Browse git
@external(static=False)
def GetUILanguage(self) -> str:
    return self.config.lcid
def WizardForm(self)

An emulated handler for the external symbol WizardForm.

Expand source code Browse git
@external(static=False)
def WizardForm(self) -> object:
    return self
def Set8087CW(self, cw)

An emulated handler for the external symbol Set8087CW.

Expand source code Browse git
@external(static=False)
def Set8087CW(self, cw: int):
    self.fpucw = FPUControl(cw)
def Get8087CW(self)

An emulated handler for the external symbol Get8087CW.

Expand source code Browse git
@external(static=False)
def Get8087CW(self) -> int:
    return self.fpucw.value
def GetDateTimeString(self, fmt, date_separator, time_separator)

An emulated handler for the external symbol GetDateTimeString.

Expand source code Browse git
@external(static=False)
def GetDateTimeString(
    self,
    fmt: str,
    date_separator: str,
    time_separator: str,
) -> str:

    now = self.config.start_time
    now = now + timedelta(
        milliseconds=(self.config.milliseconds_per_instruction * self.clock))
    now = now + timedelta(seconds=self.seconds_slept)

    date_separator = date_separator.lstrip('\0')
    time_separator = time_separator.lstrip('\0')

    def dt(m: re.Match[str]):
        spec = m[1]
        ampm = m[2]
        if ampm:
            am, _, pm = ampm.partition('/')
            spec = spec.upper()
            suffix = now.strftime('%p').lower()
            suffix = {'am': am, 'pm': pm}[suffix]
        else:
            suffix = ''
        if spec == 'dddddd' or spec == 'ddddd':
            return now.date.isoformat()
        if spec == 't':
            return now.time().isoformat('minutes')
        if spec == 'tt':
            return now.time().isoformat('seconds')
        if spec == 'd':
            return str(now.day)
        if spec == 'm':
            return str(now.month)
        if spec == 'h':
            return str(now.hour)
        if spec == 'n':
            return str(now.minute)
        if spec == 's':
            return str(now.second)
        if spec == 'H':
            return now.strftime('%I').lstrip('0') + suffix
        if spec == '/':
            return date_separator or spec
        if spec == ':':
            return time_separator or spec
        return now.strftime({
            'dddd'  : '%A',
            'ddd'   : '%a',
            'dd'    : '%d',
            'mmmm'  : '%B',
            'mmm'   : '%b',
            'mm'    : '%m',
            'yyyy'  : '%Y',
            'yy'    : '%y',
            'hh'    : '%H',
            'HH'    : '%I' + suffix,
            'nn'    : '%M',
            'ss'    : '%S',
        }.get(spec, m[0]))

    split = re.split(F'({formats.string!s})', fmt)
    for k in range(0, len(split), 2):
        split[k] = re.sub('([dmyhnst]+)((?:[aA][mM]?/[pP][mM]?)?)', dt, split[k])
    for k in range(1, len(split), 2):
        split[k] = split[k][1:-1]
    return ''.join(split)
def StringChange(self, string, old, new)

An emulated handler for the external symbol StringChange.

Expand source code Browse git
@external(static=False)
def StringChange(self, string: Variable[str], old: str, new: str) -> int:
    return self.StringChangeEx(string, old, new, False)
def CompareText(self, a, b)

An emulated handler for the external symbol CompareText.

Expand source code Browse git
@external(static=False)
def CompareText(self, a: str, b: str) -> int:
    return self.CompareStr(a.casefold(), b.casefold())
def StringSplit(self, string, separators, how)

An emulated handler for the external symbol StringSplit.

Expand source code Browse git
@external(static=False)
def StringSplit(self, string: str, separators: List[str], how: TSplitType) -> List[str]:
    return self.StringSplitEx(string, separators, None, how)
def RemoveBackslashUnlessRoot(self, string)

An emulated handler for the external symbol RemoveBackslashUnlessRoot.

Expand source code Browse git
@external(static=False)
def RemoveBackslashUnlessRoot(self, string: str) -> str:
    path = Path(string)
    if len(path.parts) == 1:
        return str(path)
    return self.RemoveBackslash(string)
def ExpandFileName(self, name)

An emulated handler for the external symbol ExpandFileName.

Expand source code Browse git
@external(static=False, alias='ExpandUNCFileName')
def ExpandFileName(self, name: str) -> str:
    if self.ExtractFileDrive(name):
        return name
    return str(self.config.cwd / name)
def CheckForMutexes(self, mutexes)

An emulated handler for the external symbol CheckForMutexes.

Expand source code Browse git
@external(static=False)
def CheckForMutexes(self, mutexes: str) -> bool:
    return any(m in self.mutexes for m in mutexes.split(','))
def CreateMutex(self, name)

An emulated handler for the external symbol CreateMutex.

Expand source code Browse git
@external(static=False)
def CreateMutex(self, name: str):
    if self.config.log_mutexes:
        yield NewMutex(name)
    self.mutexes.add(name)
def GetWinDir(self)

An emulated handler for the external symbol GetWinDir.

Expand source code Browse git
@external(static=False)
def GetWinDir(self) -> str:
    return self.expand_constant('{win}')
def GetSystemDir(self)

An emulated handler for the external symbol GetSystemDir.

Expand source code Browse git
@external(static=False)
def GetSystemDir(self) -> str:
    return self.expand_constant('{sys}')
def GetWindowsVersion(self)

An emulated handler for the external symbol GetWindowsVersion.

Expand source code Browse git
@external(static=False)
def GetWindowsVersion(self) -> int:
    version = int.from_bytes(
        struct.pack('>BBH', *self.config.windows_os_version), 'big')
    return version
def GetWindowsVersionEx(self, tv)

An emulated handler for the external symbol GetWindowsVersionEx.

Expand source code Browse git
@external(static=False)
def GetWindowsVersionEx(self, tv: Variable[Union[int, bool]]):
    tv[0], tv[1], tv[2] = self.config.windows_os_version # noqa
    tv[3], tv[4]        = self.config.windows_sp_version # noqa
    tv[5], tv[6], tv[7] = True, 0, 0
def GetWindowsVersionString(self)

An emulated handler for the external symbol GetWindowsVersionString.

Expand source code Browse git
@external(static=False)
def GetWindowsVersionString(self) -> str:
    return '{0}.{1:02d}.{2:04d}'.format(*self.config.windows_os_version)
def WizardSilent(self)

An emulated handler for the external symbol WizardSilent.

Expand source code Browse git
@external(static=False)
def WizardSilent(self) -> bool:
    return self.config.wizard_silent
def SizeOf(self, var)

An emulated handler for the external symbol SizeOf.

Expand source code Browse git
@external(static=False)
def SizeOf(self, var: Variable) -> int:
    if var.pointer:
        return (self.config.x64 + 1) * 4
    if var.container:
        return sum(self.SizeOf(x) for x in var.data)
    return var.type.code.width
class InnoSetupEmulator (archive, options=None, **more)

A specialized refinery.lib.emulator.IFPSEmulator that can emulate the InnoSetup installation with a focus on continuing execution as much as possible.

Expand source code Browse git
class InnoSetupEmulator(IFPSEmulator):
    """
    A specialized `refinery.lib.emulator.IFPSEmulator` that can emulate the InnoSetup installation
    with a focus on continuing execution as much as possible.
    """

    def emulate_installation(self, password=''):
        """
        To the best of the author's knowledge, this function emulates the sequence of calls into
        the script that the IFPS runtime would make during a setup install.
        """

        class SetupDispatcher:

            InitializeSetup: Callable
            InitializeWizard: Callable
            CurStepChanged: Callable
            ShouldSkipPage: Callable
            CurPageChanged: Callable
            PrepareToInstall: Callable
            CheckPassword: Callable
            NextButtonClick: Callable
            DeinitializeSetup: Callable

            def __getattr__(_, name):
                if pfn := self.symbols.get(name):
                    def emulated(*a):
                        return (yield from self.emulate_function(pfn, *a))
                else:
                    def emulated(*a):
                        yield from ()
                        return False
                return emulated

        Setup = SetupDispatcher()

        yield from Setup.InitializeSetup()
        yield from Setup.InitializeWizard()
        yield from Setup.CurStepChanged(TSetupStep.ssPreInstall)

        for page in PageID:

            skip = yield from Setup.ShouldSkipPage(page)

            if not skip:
                yield from Setup.CurPageChanged(page)
                if page == PageID.wpPreparing:
                    yield from Setup.PrepareToInstall(False)
                if page == PageID.wpPassword:
                    yield from Setup.CheckPassword(password)

            yield from Setup.NextButtonClick(page)

            if page == PageID.wpPreparing:
                yield from Setup.CurStepChanged(TSetupStep.ssInstall)
            if page == PageID.wpInfoAfter:
                yield from Setup.CurStepChanged(TSetupStep.ssPostInstall)

        yield from Setup.CurStepChanged(TSetupStep.ssDone)
        yield from Setup.DeinitializeSetup()

    def unimplemented(self, function: Function):
        """
        Any unimplemented function is essentially skipped. Any arguments passed by reference and
        all return values that are of type integer are set to `1` in an attempt to indicate success
        wherever possible.
        """
        decl = function.decl
        if decl is None:
            return
        if not decl.void:
            rc = 1
            rv = self.stack[-1]
            if not rv.container:
                rt = rv.type.py_type()
                if isinstance(rt, type) and issubclass(rt, int):
                    rv.set(1)
        else:
            rc = 0
        for k in range(rc, rc + len(decl.parameters)):
            ptr: Variable[Variable] = self.stack[-k]
            if not ptr.pointer:
                continue
            var = ptr.deref()
            if var.container:
                continue
            vt = var.type.py_type()
            if isinstance(vt, type) and issubclass(vt, int):
                var.set(1)

Ancestors

Class variables

var external_symbols

Methods

def emulate_installation(self, password='')

To the best of the author's knowledge, this function emulates the sequence of calls into the script that the IFPS runtime would make during a setup install.

Expand source code Browse git
def emulate_installation(self, password=''):
    """
    To the best of the author's knowledge, this function emulates the sequence of calls into
    the script that the IFPS runtime would make during a setup install.
    """

    class SetupDispatcher:

        InitializeSetup: Callable
        InitializeWizard: Callable
        CurStepChanged: Callable
        ShouldSkipPage: Callable
        CurPageChanged: Callable
        PrepareToInstall: Callable
        CheckPassword: Callable
        NextButtonClick: Callable
        DeinitializeSetup: Callable

        def __getattr__(_, name):
            if pfn := self.symbols.get(name):
                def emulated(*a):
                    return (yield from self.emulate_function(pfn, *a))
            else:
                def emulated(*a):
                    yield from ()
                    return False
            return emulated

    Setup = SetupDispatcher()

    yield from Setup.InitializeSetup()
    yield from Setup.InitializeWizard()
    yield from Setup.CurStepChanged(TSetupStep.ssPreInstall)

    for page in PageID:

        skip = yield from Setup.ShouldSkipPage(page)

        if not skip:
            yield from Setup.CurPageChanged(page)
            if page == PageID.wpPreparing:
                yield from Setup.PrepareToInstall(False)
            if page == PageID.wpPassword:
                yield from Setup.CheckPassword(password)

        yield from Setup.NextButtonClick(page)

        if page == PageID.wpPreparing:
            yield from Setup.CurStepChanged(TSetupStep.ssInstall)
        if page == PageID.wpInfoAfter:
            yield from Setup.CurStepChanged(TSetupStep.ssPostInstall)

    yield from Setup.CurStepChanged(TSetupStep.ssDone)
    yield from Setup.DeinitializeSetup()
def unimplemented(self, function)

Any unimplemented function is essentially skipped. Any arguments passed by reference and all return values that are of type integer are set to 1 in an attempt to indicate success wherever possible.

Expand source code Browse git
def unimplemented(self, function: Function):
    """
    Any unimplemented function is essentially skipped. Any arguments passed by reference and
    all return values that are of type integer are set to `1` in an attempt to indicate success
    wherever possible.
    """
    decl = function.decl
    if decl is None:
        return
    if not decl.void:
        rc = 1
        rv = self.stack[-1]
        if not rv.container:
            rt = rv.type.py_type()
            if isinstance(rt, type) and issubclass(rt, int):
                rv.set(1)
    else:
        rc = 0
    for k in range(rc, rc + len(decl.parameters)):
        ptr: Variable[Variable] = self.stack[-k]
        if not ptr.pointer:
            continue
        var = ptr.deref()
        if var.container:
            continue
        vt = var.type.py_type()
        if isinstance(vt, type) and issubclass(vt, int):
            var.set(1)

Inherited members