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
- VariableBase
- typing.Generic
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
Variable
s for container types, aVariable
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
Variable
s, 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 additionalself
parameter of typeIFPSEmulator
. 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
IFPSEmulator
s.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
IFPSEmulator
:Abort
ActiveLanguage
AddBackslash
AddPeriod
AddQuotes
Beep
ChangeFileExt
CharLength
CharToOemBuff
CheckForMutexes
Chr
CompareStr
CompareText
Copy
CreateMutex
CreateOleObject
CustomMessage
Dec
Delete
DeleteFile
DirExists
Exec
ExpandConstant
ExpandConstantEx
ExpandFileName
ExtractFileDir
ExtractFileDrive
ExtractFileExt
ExtractFileName
ExtractRelativePath
FileExists
FindFirst
FindWindowByClassName
FmtMessage
ForceDirectories
Format
Get8087CW
GetActiveOleObject
GetArrayLength
GetCmdTail
GetComputerNameString
GetDateTimeString
GetEnv
GetMD5OfString
GetMD5OfUnicodeString
GetSHA1OfString
GetSHA1OfUnicodeString
GetSHA256OfString
GetSHA256OfUnicodeString
GetSpaceOnDisk
GetSpaceOnDisk64
GetSystemDir
GetUILanguage
GetUserNameString
GetWinDir
GetWindowsVersion
GetWindowsVersionEx
GetWindowsVersionString
IDispatchInvoke
Inc
Insert
IntToStr
IsAdmin
IsWildcard
IsWin64
IsX86Compatible
IsX86OS
Length
LoadStringFromFile
LoadStringsFromFile
Log
Lowercase
MinimizePathName
Null
Ord
ParamCount
ParamStr
Pos
ProcessorArchitecture
RaiseException
Random
RemoveBackslash
RemoveBackslashUnlessRoot
RemoveQuotes
SameStr
SameText
Set8087CW
SetArrayLength
SetLength
SetupMessage
SizeOf
StrToFloat
StrToInt
StrToIntDef
StrToVersion
StringChange
StringChangeEx
StringJoin
StringOfChar
StringSplit
StringSplitEx
SysErrorMessage
TInputDirWizardPage__GetValues
TInputFileWizardPage__GetValues
TPasswordEdit__SetText
Terminated
Trim
TrimLeft
TrimRight
Trunc
Unassigned
Uppercase
Utf8Decode
Utf8Encode
WStrGet
WStrSet
WildcardMatch
WizardForm
WizardSilent
call
emulate_function
kernel32__GetTickCount
kernel32__Sleep
reset
user32__GetSystemMetrics