Module refinery.lib.inno.ifps

The code is based on the logic implemented in IFPSTools: https://github.com/Wack0/IFPSTools

Expand source code Browse git
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
The code is based on the logic implemented in IFPSTools:
 https://github.com/Wack0/IFPSTools
"""
from __future__ import annotations

import abc
import enum
import io
import itertools

from typing import (
    Callable,
    Dict,
    Generator,
    List,
    NamedTuple,
    Optional,
    Tuple,
    Type,
    TypeVar,
    Union,
)

from uuid import UUID
from dataclasses import dataclass, field
from collections import OrderedDict
from functools import WRAPPER_ASSIGNMENTS, update_wrapper

from refinery.lib.structures import Struct, StructReader
from refinery.lib.inno.symbols import IFPSAPI, IFPSClasses, IFPSEvents
from refinery.lib.types import CaseInsensitiveDict

_E = TypeVar('_E', bound=Type[enum.Enum])
_C = TypeVar('_C', bound=Type)

_TAB = '\x20\x20'


def extended(_data: bytes):
    """
    A helper function to parse 10 bytes into an extended type float value within the IFPS runtime.
    """
    if len(_data) != 10:
        raise ValueError
    data = int.from_bytes(_data, 'little')
    sign = data >> 79
    data = data ^ (sign << 79)
    sign = -1.0 if sign else +1.0
    exponent = data >> 64
    data = data ^ (exponent << 64)
    if exponent == 0:
        if data == 0:
            return sign * 0
        exponent = -16382
    elif exponent == 0b111111111111111:
        if data == 0:
            return sign * float('Inf')
        else:
            return sign * float('NaN')
    else:
        exponent = exponent - 16383
    mantissa = data / (1 << 64)
    return sign * mantissa * (2 ** exponent)


def represent(cls: _E) -> _E:
    """
    A decorator for various IFPS integer enumeration classes to change the default string
    representation.
    """
    cls.__repr__ = lambda self: F'{self.__class__.__name__}.{self.name}'
    cls. __str__ = lambda self: self.name
    return cls


@represent
class Op(enum.IntEnum):
    """
    An enumeration of all known IFPS opcodes.
    """
    Assign       = 0x00  # noqa
    Calculate    = 0x01  # noqa
    Push         = 0x02  # noqa
    PushVar      = 0x03  # noqa
    Pop          = 0x04  # noqa
    Call         = 0x05  # noqa
    Jump         = 0x06  # noqa
    JumpTrue     = 0x07  # noqa
    JumpFalse    = 0x08  # noqa
    Ret          = 0x09  # noqa
    StackType    = 0x0A  # noqa
    PushType     = 0x0B  # noqa
    Compare      = 0x0C  # noqa
    CallVar      = 0x0D  # noqa
    SetPtr       = 0x0E  # noqa
    BooleanNot   = 0x0F  # noqa
    Neg          = 0x10  # noqa
    SetFlag      = 0x11  # noqa
    JumpFlag     = 0x12  # noqa
    PushEH       = 0x13  # noqa
    PopEH        = 0x14  # noqa
    IntegerNot   = 0x15  # noqa
    SetPtrToCopy = 0x16  # noqa
    Inc          = 0x17  # noqa
    Dec          = 0x18  # noqa
    JumpPop1     = 0x19  # noqa
    JumpPop2     = 0x1A  # noqa
    Nop          = 0xFF  # noqa
    _INVALID     = 0xDD  # noqa

    @classmethod
    def FromInt(cls, code: int):
        try:
            return cls(code)
        except ValueError:
            return cls._INVALID


class AOp(enum.IntEnum):
    """
    An enumeration of all known IFPS arithmetic opcodes.
    """
    Add = 0
    Sub = 1
    Mul = 2
    Div = 3
    Mod = 4
    Shl = 5
    Shr = 6
    And = 7
    BOr = 8
    Xor = 9

    def __str__(self):
        glyph = ('+', '-', '*', '/', '%', '<<', '>>', '&', '|', '^')[self]
        return F'{glyph}='


class COp(enum.IntEnum):
    """
    An enumeration of all known IFPS comparison opcodes.
    """
    GE = 0
    LE = 1
    GT = 2
    LT = 3
    NE = 4
    EQ = 5
    IN = 6
    IS = 7

    def __str__(self):
        return ('>=', '<=', '>', '<', '!=', '==', 'in', 'is')[self]


@represent
class TC(enum.IntEnum):
    """
    An enumeration of all known IFPS type codes.
    """
    ReturnAddress       = 0x00  # noqa
    U08                 = 0x01  # noqa
    S08                 = 0x02  # noqa
    U16                 = 0x03  # noqa
    S16                 = 0x04  # noqa
    U32                 = 0x05  # noqa
    S32                 = 0x06  # noqa
    Single              = 0x07  # noqa
    Double              = 0x08  # noqa
    Extended            = 0x09  # noqa
    AnsiString          = 0x0A  # noqa
    Record              = 0x0B  # noqa
    Array               = 0x0C  # noqa
    Pointer             = 0x0D  # noqa
    PChar               = 0x0E  # noqa
    ResourcePointer     = 0x0F  # noqa
    Variant             = 0x10  # noqa
    S64                 = 0x11  # noqa
    Char                = 0x12  # noqa
    WideString          = 0x13  # noqa
    WideChar            = 0x14  # noqa
    ProcPtr             = 0x15  # noqa
    StaticArray         = 0x16  # noqa
    Set                 = 0x17  # noqa
    Currency            = 0x18  # noqa
    Class               = 0x19  # noqa
    Interface           = 0x1A  # noqa
    NotificationVariant = 0x1B  # noqa
    UnicodeString       = 0x1C  # noqa
    Enum                = 0x81  # noqa
    Type                = 0x82  # noqa
    ExtClass            = 0x83  # noqa

    @property
    def primitive(self) -> bool:
        """
        Indicates whether the code represents a primitive type.
        """
        return self not in {
            TC.Class,
            TC.ProcPtr,
            TC.Interface,
            TC.Set,
            TC.StaticArray,
            TC.Array,
            TC.Record,
        }

    @property
    def container(self) -> bool:
        """
        Indicates whether the code represents a container type.
        """
        return self in {
            TC.StaticArray,
            TC.Array,
            TC.Record,
        }

    @property
    def width(self):
        """
        For primitive types, this gives the size of an immediate of this type in bytes.
        """
        return {
            TC.Variant       : 0x10,
            TC.Char          : 0x01,
            TC.S08           : 0x01,
            TC.U08           : 0x01,
            TC.WideChar      : 0x02,
            TC.S16           : 0x02,
            TC.U16           : 0x02,
            TC.WideString    : 0x04,
            TC.UnicodeString : 0x04,
            TC.Interface     : 0x04,
            TC.Class         : 0x04,
            TC.PChar         : 0x04,
            TC.AnsiString    : 0x04,
            TC.Single        : 0x04,
            TC.S32           : 0x04,
            TC.U32           : 0x04,
            TC.ProcPtr       : 0x0C,
            TC.Currency      : 0x08,
            TC.Pointer       : 0x0C,
            TC.Double        : 0x08,
            TC.S64           : 0x08,
            TC.Extended      : 0x0A,
            TC.ReturnAddress : 0x1C,
        }.get(self, 0)


@dataclass
class IFPSTypeMixin:
    """
    A helper class to mix additional properties into various IFPS type classes.
    """
    symbol: Optional[str] = None
    attributes: Optional[List[FunctionAttribute]] = None

    def __str__(self):
        if self.symbol is not None:
            return self.symbol
        return super().__str__()


@dataclass
class IFPSTypeBase(abc.ABC):
    """
    The base class for any IFPS type.
    """
    code: TC

    def simple(self, nested=False):
        """
        Indicate whether the type requires more than one line to pretty print.
        """
        return True

    def display(self, indent=0):
        """
        Compute a display string that can be used to represent the type in disassembly.
        """
        return indent * _TAB + self.code.name

    @abc.abstractmethod
    def py_type(self, key: Optional[int] = None) -> Optional[type]:
        """
        If possible, provide a Python type equivalent for this IFPS type. The optional key argument
        is required only for the `refinery.lib.inno.ifps.TRecord` class.
        """
        ...

    @abc.abstractmethod
    def default(self, key: Optional[int] = None):
        """
        Compute the default value for this type. The optional key argument is required only for the
        `refinery.lib.inno.ifps.TRecord` class.
        """
        ...

    @property
    def primitive(self) -> bool:
        """
        Indicates whether the type is primitive.
        """
        return self.code.primitive

    @property
    def container(self) -> bool:
        """
        Indicates whether the type is a container.
        """
        return self.code.container

    def __str__(self):
        return self.display(0)


def ifpstype(cls: _C) -> Union[_C, Type[IFPSTypeMixin]]:
    """
    A decorator for IFPS types to mix the `refinery.lib.inno.ifps.IFPSTypeMixin` into the dataclass
    definition.
    """
    cls = dataclass(cls)
    mix = type(cls.__qualname__, (IFPSTypeMixin, cls), {})
    assigned = set(WRAPPER_ASSIGNMENTS) - {'__annotations__'}
    update_wrapper(mix, cls, assigned=assigned, updated=())
    return dataclass(mix)


@ifpstype
class TPrimitive(IFPSTypeBase):
    """
    A primitive IFPS type.
    """
    def py_type(self, *_) -> Optional[type]:
        return {
            TC.ReturnAddress       : int,
            TC.U08                 : int,
            TC.S08                 : int,
            TC.U16                 : int,
            TC.S16                 : int,
            TC.U32                 : int,
            TC.S32                 : int,
            TC.Single              : float,
            TC.Double              : float,
            TC.Extended            : float,
            TC.AnsiString          : str,
            TC.Pointer             : VariableBase,
            TC.PChar               : str,
            TC.ResourcePointer     : VariableBase,
            TC.Variant             : object,
            TC.S64                 : int,
            TC.Char                : str,
            TC.WideString          : str,
            TC.WideChar            : str,
            TC.Currency            : float,
            TC.UnicodeString       : str,
            TC.Enum                : int,
            TC.Type                : IFPSType,
        }.get(self.code)

    def default(self, *_):
        if self.code in (TC.Char, TC.WideChar, TC.PChar):
            return '\0'
        tc = self.py_type()
        if issubclass(tc, (int, float, str)):
            return tc()


@ifpstype
class TProcPtr(IFPSTypeBase):
    """
    The procedure pointer IFPS type.
    """
    void: bool
    args: tuple[DeclSpecParam, ...]

    def py_type(self, *_):
        return None

    def default(self, *_):
        return None

    def display(self, indent=0):
        name = super().display(indent)
        args = []
        for k, spec in enumerate(self.args, 1):
            arg = F'Arg{k}'
            if not spec.const:
                arg = F'*{arg}'
            if spec.type is not None:
                arg = F'{spec.type!s} {arg}'
            args.append(arg)
        args = ', '.join(args)
        return F'{name}({args})'


@ifpstype
class TInterface(IFPSTypeBase):
    """
    An IFPS type representing a COM interface.
    """
    uuid: UUID

    def py_type(self, *_):
        return object

    def default(self, *_):
        return None

    def display(self, indent=0):
        display = super().display(indent)
        return F'{display}({self.uuid!s})'


@ifpstype
class TClass(IFPSTypeBase):
    """
    An IFPS type representing an IFPS class.
    """
    name: str

    def py_type(self, *_):
        return None

    def default(self, *_):
        return None


@ifpstype
class TSet(IFPSTypeBase):
    """
    An IFPS type representing a bit vector.
    """
    size: int

    def py_type(self, *_):
        return int

    def default(self, *_):
        return 0

    @property
    def size_in_bytes(self):
        q, r = divmod(self.size, 8)
        return q + (r and 1 or 0)

    def display(self, indent=0):
        display = super().display(indent)
        return F'{display}({self.size})'


@ifpstype
class TArray(IFPSTypeBase):
    """
    An IFPS type representing a dynamic array.
    """
    type: TPrimitive

    def py_type(self, key: Optional[int] = None):
        if key is None:
            return list
        return self.type.py_type()

    def default(self, key: Optional[int] = None):
        if key is None:
            return []
        return self.type.default()

    def display(self, indent=0):
        display = F'{_TAB * indent}{self.type!s}'
        return F'array of {display}'

    def simple(self, nested=False):
        return self.type.simple(nested)


@ifpstype
class TStaticArray(IFPSTypeBase):
    """
    An IFPS type representing a static array (i.e. a tuple).
    """
    type: TPrimitive
    size: int
    offset: Optional[int] = None

    def py_type(self, key: Optional[int] = None):
        if key is None:
            return list
        return self.type.py_type(key)

    def default(self, key: Optional[int] = None):
        if key is None:
            return [self.type.default() for _ in range(self.size)]
        return self.type.default()

    def display(self, indent=0):
        display = F'{_TAB * indent}{self.type!s}'
        return F'{display}[{self.size}]'

    def simple(self, nested=False):
        return self.type.simple(nested)


@ifpstype
class TRecord(IFPSTypeBase):
    """
    An IFPS type representing a structure.
    """
    members: Tuple[TPrimitive, ...]

    @property
    def size(self):
        return len(self.members)

    def py_type(self, key: Optional[int] = None):
        if key is None:
            return list
        return self.members[key].py_type()

    def default(self, key: Optional[int] = None):
        if key is None:
            return [member.default() for member in self.members]
        return self.members[key].default()

    def simple(self, nested=False):
        if nested:
            return False
        if len(self.members) > 10:
            return False
        return all(m.simple(True) for m in self.members)

    def display(self, indent=0):
        output = io.StringIO()
        output.write(indent * _TAB)
        output.write('struct {')
        if self.simple():
            output.write(', '.join(str(m) for m in self.members))
        else:
            for k, member in enumerate(self.members):
                if k > 0:
                    output.write(',')
                output.write('\n')
                output.write(member.display(indent + 1))
            if self.members:
                output.write(F'\n{_TAB * indent}')
        output.write('}')
        return output.getvalue()


IFPSType = Union[
    TRecord,
    TStaticArray,
    TArray,
    TSet,
    TProcPtr,
    TClass,
    TInterface,
    TPrimitive,
]
"""
Represents any of the possible IFPS data types:

- `refinery.lib.inno.ifps.TRecord`
- `refinery.lib.inno.ifps.TStaticArray`
- `refinery.lib.inno.ifps.TArray`
- `refinery.lib.inno.ifps.TSet`
- `refinery.lib.inno.ifps.TProcPtr`
- `refinery.lib.inno.ifps.TClass`
- `refinery.lib.inno.ifps.TInterface`
- `refinery.lib.inno.ifps.TPrimitive`
"""


class Value(NamedTuple):
    """
    A value of the given type within the IFPS runtime.
    """
    type: IFPSType
    value: Union[str, int, float, bytes, Function]

    def convert(self, *_):
        return self.type.py_type()

    def default(self, *_):
        return self.type.default()

    def __repr__(self):
        value = self.value
        if isinstance(value, bytes):
            value = value.hex()
        return F'{self.type.code.name}({value!r})'

    def __str__(self):
        v = self.value
        if isinstance(v, Function):
            return F'&{v!s}'
        return repr(v)


class FunctionAttribute(NamedTuple):
    """
    A function attribute.
    """
    name: str
    fields: Tuple[Value, ...]

    def __repr__(self):
        name = self.name
        if self.fields:
            name += '[{}]'.format(','.join(repr(f) for f in self.fields))
        return name


@dataclass
class DeclSpecParam:
    """
    A function parameter specification.
    """
    const: bool
    """
    True if this parameter is passed by value, not by reference.
    """
    type: Optional[TPrimitive] = None
    """
    The type of this parameter.
    """
    name: Optional[str] = None
    """
    The name of this parameter.
    """


class CallType(str, enum.Enum):
    """
    This enumeration classifies the different call types.
    """
    Symbol = 'symbol'
    Procedure = 'procedure'
    Function = 'function'
    Property = 'property'

    def __str__(self):
        return self.value


@dataclass
class DeclSpec:
    """
    This class captures the declaration info of a function symbol.
    """
    void: bool
    parameters: List[DeclSpecParam] = field(default_factory=list)
    name: str = ''
    calling_convention: Optional[str] = None
    return_type: Optional[IFPSType] = None
    module: Optional[str] = None
    classname: Optional[str] = None
    delay_load: bool = False
    vtable_index: Optional[int] = None
    load_with_altered_search_path: bool = False
    is_property: bool = False
    is_accessor: bool = False

    @property
    def argc(self):
        return len(self.parameters)

    def represent(self, name: str, ref: bool = False, rel: bool = False):
        def pparam(k: int, p: DeclSpecParam):
            name = p.name or F'{VariableType.Argument!s}{k}'
            if p.type is not None:
                name = F'{name}: {p.type!s}'
            if not p.const:
                name = F'*{name}'
            return name
        if self.name and name in self.name:
            name = self.name
        spec = name
        if self.vtable_index is not None:
            spec = F'{self.name}[{self.vtable_index}]'
        if not rel and self.classname:
            spec = F'{self.classname}.{spec}'
        if not rel and self.module:
            spec = F'{self.module}::{spec}'
        if not ref:
            if self.delay_load:
                spec = F'__delay_load {spec}'
            if self.calling_convention and not self.is_property:
                spec = F'__{self.calling_convention} {spec}'
            spec = F'{self.type} {spec}'
            args = self.parameters
            args = args and ', '.join(pparam(*t) for t in enumerate(args, 1)) or ''
            if self.is_property:
                if args:
                    spec = F'{spec}[{args}]'
            else:
                spec = F'{spec}({args})'
            if self.return_type:
                spec = F'{spec}: {self.return_type!s}'
        return spec

    @property
    def type(self):
        if self.is_property:
            return CallType.Property
        if self.void:
            return CallType.Procedure
        return CallType.Function

    def __repr__(self):
        return self.represent(self.name or '(*)')

    @classmethod
    def ParseF(cls, reader: StructReader[bytes], load_flags: bool):
        def ascii():
            return reader.read_c_string('latin1')

        def boolean():
            return bool(reader.u8())

        def cc():
            return {
                0: 'register',
                1: 'pascal',
                2: 'cdecl',
                3: 'stdcall',
            }.get(reader.u8(), cls.calling_convention)

        def read_parameters():
            nonlocal void
            void = not boolean()
            parameters.extend(DeclSpecParam(not b) for b in reader.read())

        void = True
        name = None
        properties = {}
        parameters = []

        if reader.readif(b'dll:'):
            reader.readif(B'files:')
            if (module := ascii()).lower().endswith('.dll'):
                module = module[:-4]
            properties.update(module=module)
            name = ascii()
            properties.update(calling_convention=cc())
            if load_flags:
                properties.update(delay_load=boolean(), load_with_altered_search_path=boolean())
            read_parameters()
        elif reader.readif(b'class:'):
            if reader.remaining_bytes == 1:
                spec = reader.peek(1)
                void = False
                parameters.append(DeclSpecParam(False))
                name = {
                    b'+': 'CastToType',
                    B'-': 'SetNil'
                }.get(spec)
                properties.update(classname='Class', calling_convention='pascal')
            else:
                properties.update(classname=reader.read_terminated_array(b'|').decode('latin1'))
                name = reader.read_terminated_array(b'|').decode('latin1')
                if name[-1] == '@':
                    properties.update(is_property=True)
                    name = name[:-1]
                properties.update(calling_convention=cc())
                read_parameters()
        elif reader.readif(b'intf:.'):
            name = 'CoInterface'
            properties.update(vtable_index=reader.u32())
            properties.update(calling_convention=cc())
            read_parameters()
        else:
            read_parameters()
        return cls(void, parameters, name=name, **properties)

    @classmethod
    def ParseE(cls, data: bytes, ipfs: IFPSFile):
        decl = data.split(B'\x20')
        try:
            return_type = int(decl.pop(0))
        except Exception:
            void = True
        else:
            void = return_type < 0
        if not void:
            return_type = ipfs.types[return_type]
        else:
            return_type = None
        parameters = []
        for param in decl:
            try:
                i = int(param[1:])
            except Exception:
                tv = None
            else:
                tv = ipfs.types[i]
            parameters.append(
                DeclSpecParam(param[:1] == B'@', tv))
        return cls(void, parameters, return_type=return_type)


@dataclass
class Function:
    """
    Represents a function in the IFPS runtime.
    """
    symbol: str = ''
    decl: Optional[DeclSpec] = None
    body: Optional[List[Instruction]] = None
    attributes: Optional[List[FunctionAttribute]] = None
    _bbs: Optional[Dict[int, BasicBlock]] = None
    _ins: Optional[Dict[int, Instruction]] = None
    getter: Optional[Function] = None
    setter: Optional[Function] = None

    @property
    def is_property(self):
        if decl := self.decl:
            return decl.is_property
        else:
            return False

    @property
    def name(self):
        symbol = self.symbol
        if (decl := self.decl) and (name := decl.name) and (symbol in name):
            symbol = name
        return symbol

    @property
    def code(self):
        if code := self._ins:
            return code
        self._ins = code = {i.offset: i for i in self.body}
        return code

    def reference(self, rel: bool = False) -> str:
        if self.decl is None:
            return self.symbol
        return self.decl.represent(self.symbol, ref=True, rel=rel)

    def __repr__(self):
        if self.decl is None:
            return F'symbol {self.symbol}'
        return self.decl.represent(self.symbol)

    def __str__(self):
        return self.reference()

    @property
    def type(self):
        if self.decl is None:
            return CallType.Symbol
        return self.decl.type

    def get_basic_blocks(self) -> Dict[int, BasicBlock]:
        if (bbs := self._bbs) is not None:
            return bbs
        if self.body is None:
            bbs = self._bbs = {}
            return bbs

        bbs: Dict[int, BasicBlock] = {0: (bb := BasicBlock(0))}
        self._bbs = bbs
        jump = False

        for insn in self.body:
            try:
                bb = bbs[insn.offset]
            except KeyError:
                if jump or insn.jumptarget:
                    nb = bbs[insn.offset] = BasicBlock(insn.offset)
                    if not jump:
                        nb.sources[bb.offset] = bb
                        bb.targets[nb.offset] = nb
                    bb = nb
            bb.body.append(insn)
            if not insn.branches:
                jump = False
                continue
            targets = [insn.operands[0]]
            sequence = insn.offset + insn.size
            jump = insn.jumps
            if not jump and insn.opcode != Op.Ret:
                targets.append(sequence)
            for t in targets:
                if not (bt := bbs.get(t)):
                    bt = bbs[t] = BasicBlock(t)
                bb.targets[t] = bt
                bt.sources[bb.offset] = bb

        for offset, bb in list(bbs.items()):
            if bb.body:
                continue
            del bbs[offset]
            for source in bb.sources.values():
                source.targets.pop(offset, None)

        visited: set[int] = set()
        errored: set[int] = set()

        def trace_stack(offset: int, stack: Optional[int]):
            if offset in errored:
                return
            bb = bbs[offset]
            if bb.stack is not None and stack != bb.stack:
                stack = None
            if stack is None:
                errored.add(offset)
            elif offset in visited:
                return
            else:
                visited.add(offset)
            bb.stack = stack
            body = [] if stack is None else bb.body
            for insn in body:
                insn.stack = stack
                stack += insn.stack_delta
            for t in bb.targets:
                trace_stack(t, stack)

        trace_stack(0, 0)

        for insn in self.body:
            if (stack := insn.stack) is None:
                continue
            for k, op in enumerate(insn.operands):
                if not isinstance(op, Operand):
                    continue
                if not (v := op.variable) or v.type != VariableType.Local:
                    continue
                if v.index <= stack:
                    continue
                raise IndexError(
                    F'Instruction {op!s} at offset 0x{insn.offset:X} in function {self.name} has '
                    F'variable operand {k} whose index {v.index} exceeds the stack depth {stack}.')

        return bbs


class VariableBase:
    """
    This class represents a variable within the IFPS runtime. This is primarily a base class for
    the more sophisticated `refinery.lib.inno.emulator.Variable`.
    """
    type: IFPSType
    """
    The type of the variable, see `refinery.lib.inno.ifps.IFPSType`.
    """
    spec: Optional[VariableSpec]
    """
    A `refinery.lib.inno.ifps.VariableSpec` that uniquely identifies the base variable. If this
    property is `None`, the variable is unbound: The `refinery.lib.inno.ifps.Op.SetPtrToCopy`
    opcode can create such variables.
    """

    __slots__ = 'type', 'spec'

    def __init__(self, type: IFPSType, spec: VariableSpec):
        self.type = type
        self.spec = spec

    def __str__(self):
        return F'{self.spec}: {self.type!s}'


@represent
class OperandType(enum.IntEnum):
    """
    Classifies the type of an `refinery.lib.inno.ifps.Operand`.
    """
    Variable = 0
    Value = 1
    IndexedByInt = 2
    IndexedByVar = 3


@represent
class EHType(enum.IntEnum):
    """
    This enumeration lists the possible types of code region covered by an exception handler.
    """
    Try = 0
    Finally = 1
    Catch = 2
    SecondFinally = 3


@represent
class NewEH(enum.IntEnum):
    """
    This enumeration gives names to the 4 arguments of the opcode responsible for registering a new
    exception handler. The first argument specifies the location of a finally, the second argument
    specifies the location of a catch handler, and so on.
    """
    Finally = 0
    CatchAt = 1
    SecondFinally = 2
    End = 3


class VariableType(str, enum.Enum):
    Global = 'GlobalVar'
    Local = 'LocalVar'
    Argument = 'Argument'

    def __repr__(self):
        return self.name

    def __str__(self):
        return self.value


class VariableSpec(NamedTuple):
    """
    Represents a reference to a variable within the IFPS runtime. There are three variable types:
    Locals, globals, and function arguments; see `refinery.lib.inno.ifps.VariableType`. A variable
    is then uniquely defined by its type and index within the (localized) list of such variables.
    The function argument of index zero is the return value of a function.
    """
    index: int
    type: VariableType

    def __repr__(self):
        if self.index == 0 and self.type == VariableType.Argument:
            return 'ReturnValue'
        return F'{self.type!s}{self.index}'


class Operand(NamedTuple):
    """
    Represends an operand to an IFPS opcode. An operand can either contain a value, which is an
    immediate that is encoded into the opcode, or a reference to a variable. A variable is given
    by its `refinery.lib.inno.ifps.VariableSpec`. Additionally, the operand can specify an index
    for this variable which can either be given by an immediate, or by another variable. In the
    latter case, the encoded index is also a `refinery.lib.inno.ifps.VariableSpec`. The type of
    operand is encoded as an `refinery.lib.inno.ifps.OperandType`.
    """
    type: OperandType
    variable: Optional[VariableSpec] = None
    value: Optional[Value] = None
    index: Optional[Union[VariableSpec, int]] = None

    def __repr__(self):
        return self.__tostring(repr)

    def __str__(self):
        return self.__tostring(str)

    @property
    def immediate(self):
        return self.type == OperandType.Value

    def __tostring(self, converter):
        if self.type is OperandType.Value:
            return converter(self.value)
        if self.type is OperandType.Variable:
            return converter(self.variable)
        if self.type is OperandType.IndexedByInt:
            return F'{converter(self.variable)}[0x{self.index:02X}]'
        if self.type is OperandType.IndexedByVar:
            return F'{converter(self.variable)}[{self.index!s}]'
        raise RuntimeError(F'Unexpected OperandType {self.type!r} in {self.__class__.__name__}')


_Op_Maxlen = max(len(op.name) for op in Op)
_Op_StackD = {
    Op.Push     : +1,
    Op.PushVar  : +1,
    Op.PushType : +1,
    Op.Pop      : -1,
    Op.JumpPop1 : -1,
    Op.JumpPop2 : -2,
}


@dataclass
class Instruction:
    offset: int
    opcode: Op
    size: int = 0
    stack: Optional[int] = None
    operands: List[Union[str, bool, int, float, Operand, IFPSType, Function, None]] = field(default_factory=list)
    operator: Optional[Union[AOp, COp]] = None
    jumptarget: bool = False

    def op(self, index: int):
        arg = self.operands[index]
        if not isinstance(arg, Operand):
            raise TypeError
        return arg

    @property
    def branches(self):
        return self.opcode in (
            Op.Jump,
            Op.JumpFalse,
            Op.JumpTrue,
            Op.JumpFlag,
            Op.JumpPop1,
            Op.JumpPop2,
        )

    @property
    def jumps(self):
        return self.opcode in (
            Op.Jump,
            Op.JumpPop1,
            Op.JumpPop2,
        )

    @property
    def stack_delta(self):
        return _Op_StackD.get(self.opcode, 0)

    def oprep(self, labels: Optional[dict[int, str]] = None):
        if self.branches:
            dst = self.operands[0]
            if not labels or not (label := labels.get(dst)):
                label = F'0x{dst:X}'
            var = [str(op) for op in self.operands[1:]]
            return ', '.join((label, *var))
        elif self.opcode is Op.PushEH:
            ops = []
            for op, name in reversed(list(zip(self.operands, NewEH))):
                if op is None:
                    continue
                ops.append(F'{name}:0x{op:X}')
            return '\x20'.join(ops)
        elif self.opcode is Op.PopEH:
            return F'End{EHType(self.operands[0])}'
        elif self.opcode is Op.SetFlag:
            rep, negated = self.operands
            return F'!{rep}' if negated else str(rep)
        elif self.opcode is Op.Compare:
            dst, a, b = self.operands
            return F'{dst!s} := {a!s} {self.operator!s} {b!s}'
        elif self.opcode is Op.Calculate:
            dst, src = self.operands
            return F'{dst!s} {self.operator!s} {src!s}'
        elif self.opcode in (Op.Assign, Op.SetPtr, Op.SetPtrToCopy):
            dst, src = self.operands
            return F'{dst!s} := {src!s}'
        else:
            return ', '.join(str(op) for op in self.operands)

    def pretty(self, labels: Optional[dict[int, str]] = None):
        return F'{self.opcode!s:<{_Op_Maxlen}}{_TAB}{self.oprep(labels)}'.strip()

    def __repr__(self):
        return F'{self.opcode.name}({self.oprep()})'

    def __str__(self):
        return self.pretty()


@dataclass
class BasicBlock:
    offset: int
    stack: Optional[int] = None
    body: List[Instruction] = field(default_factory=list)
    sources: Dict[int, BasicBlock] = field(default_factory=dict)
    targets: Dict[int, BasicBlock] = field(default_factory=dict)

    @property
    def stack_delta(self):
        return sum(insn.stack_delta for insn in self.body)

    @property
    def size(self):
        return sum(insn.size for insn in self.body)


class FTag(enum.IntFlag):
    External = 0b0001
    Exported = 0b0010
    HasAttrs = 0b0100

    def check(self, v):
        return bool(self & v)


class IFPSFile(Struct):
    MinVer = 12
    MaxVer = 23

    Magic = B'IFPS'

    def __init__(self, reader: StructReader[memoryview], codec: str = 'latin1', unicode: bool = True):
        self.codec = codec
        self.unicode = unicode
        self.types: List[IFPSType] = []
        self.functions: List[Function] = []
        self.globals: List[VariableBase] = []
        self.strings: List[str] = []
        self.reader = reader
        if reader.remaining_bytes < 28:
            raise ValueError('Less than 28 bytes in file, not enough data to parse.')
        magic = reader.read(4)
        if magic != self.Magic:
            raise ValueError(F'Invalid magic sequence: {magic.hex()}')
        self.version = reader.u32()
        self.count_types = reader.u32()
        self.count_functions = reader.u32()
        self.count_variables = reader.u32()
        self.entry = reader.u32()
        self.import_size = reader.u32()

        if self.version not in range(self.MinVer, self.MaxVer + 1):
            raise NotImplementedError(
                F'This IFPS file has version {self.version}, which is not in the supported range '
                F'[{self.MinVer},{self.MaxVer}].')

        self._known_type_names = {
            TC.U08   : {'Byte', 'Boolean'},
            TC.S08   : {'ShortInt'},
            TC.U16   : {'Word'},
            TC.S16   : {'SmallInt'},
            TC.S32   : {'Integer', 'LongInt'},
            TC.U32   : {'LongWord', 'Cardinal', 'HWND', 'TSetupProcessorArchitecture'},
            TC.Char  : {'AnsiChar'},
            TC.PChar : {'PAnsiChar'},
            TC.S64   : {'Int64'},
        }

        self._load_types()
        self._name_types()
        self._load_functions()
        self._load_variables()

        del self._known_type_names

    def _name_types(self, missing_types: Optional[set[str]] = None):
        tbn: dict[str, IFPSType] = CaseInsensitiveDict()
        self.types_by_name = tbn
        for t in self.types:
            name = str(t)
            code = t.code
            known = self._known_type_names.get(code)
            if known:
                known.discard(name)
            tbn[name] = t

        if missing_types:
            def add_type(name: str, type: IFPSType):
                tbn[name] = type
                self.types.append(type)
                return type

            def make_string(name: str = 'String'):
                try:
                    return tbn[name]
                except KeyError:
                    pass
                for type in tbn.values():
                    if type.code in (
                        TC.AnsiString,
                        TC.WideString,
                        TC.UnicodeString,
                    ):
                        break
                else:
                    code = TC.WideString if self.unicode else TC.AnsiString
                    type = TPrimitive(code, symbol=name)
                return add_type(name, type)

            for name in tbn:
                missing_types.discard(name)

            if 'TGUID' in missing_types:
                missing_types |= {'LongWord', 'Word', 'Byte'}

            for code in TC:
                name = code.name
                if name not in missing_types:
                    continue
                missing_types.discard(name)
                add_type(name, TPrimitive(code, symbol=name))

            for code, names in self._known_type_names.items():
                for name in names:
                    if name not in missing_types:
                        continue
                    missing_types.discard(name)
                    add_type(name, TPrimitive(code, symbol=name))

            for name in [
                'TVarType',
                'TInputQueryWizardPage',
                'TInputOptionWizardPage',
                'TInputDirWizardPage',
                'TInputFileWizardPage',
                'TOutputMsgWizardPage',
                'TOutputMsgMemoWizardPage',
                'TOutputProgressWizardPage',
                'TOutputMarqueeProgressWizardPage',
                'TDownloadWizardPage',
                'ExtractionWizardPage',
                'TWizardPage',
                'TSetupForm',
                'TComponent',
                'TNewNotebookPage',
            ]:
                if name not in missing_types:
                    continue
                add_type(name, TClass(TC.Class, name, symbol=name))

            if (name := 'String') in missing_types:
                make_string(name)

            if (name := 'AnyString') in missing_types:
                make_string(name)

            if (name := 'TArrayOfString') in missing_types:
                add_type(name, TArray(TC.Array, make_string()))

            if (name := 'IUnknown') in missing_types:
                add_type(name, TInterface(TC.Interface, UUID('{00000000-0000-0000-C000-000000000046}')))

            if (name := 'TGUID') in missing_types:
                add_type(name, TRecord(TC.Record, (
                    tbn['LongWord'],
                    tbn['Word'],
                    tbn['Word'],
                    TStaticArray(tbn['Byte'], 8)
                ), symbol=name))

    def _load_types(self):
        def _normalize(n: str):
            return IFPSClasses.Types.get(n.casefold(), n)
        reader = self.reader
        types = self.types
        for k in range(self.count_types):
            typecode = reader.u8()
            exported = bool(typecode & 0x80)
            typecode = typecode & 0x7F
            try:
                code = TC(typecode)
            except ValueError as V:
                raise ValueError(F'Unknown type code value 0x{typecode:02X}.') from V
            if code in (TC.Class, TC.ExtClass):
                t = TClass(code, _normalize(reader.read_length_prefixed_ascii()))
            elif code is TC.ProcPtr:
                spec = reader.read_length_prefixed()
                void = bool(spec[0])
                args = tuple(DeclSpecParam(not b) for b in spec[1:])
                t = TProcPtr(code, void, args)
            elif code is TC.Interface:
                guid = UUID(bytes=bytes(reader.read(0x10)))
                t = TInterface(code, guid)
            elif code is TC.Set:
                t = TSet(code, reader.u32())
            elif code is TC.StaticArray:
                type = types[reader.u32()]
                size = reader.u32()
                offset = None if self.version <= 22 else reader.u32()
                t = TStaticArray(code, type, size, offset)
            elif code is TC.Array:
                t = TArray(code, types[reader.u32()])
            elif code is TC.Record:
                length = reader.u32()
                members = tuple(types[reader.u32()] for _ in range(length))
                t = TRecord(code, members, symbol=F'RECORD{k}')
            else:
                t = TPrimitive(code, symbol=code.name)
            if exported:
                t.symbol = _normalize(reader.read_length_prefixed_ascii())
                if self.version <= 21:
                    t.name = _normalize(reader.read_length_prefixed_ascii())
            types.append(t)
            if self.version >= 21:
                t.attributes = list(self._read_attributes())

    def _read_value(self, reader: Optional[StructReader] = None) -> Value:
        if reader is None:
            reader = self.reader
        type = self.types[reader.u32()]
        size = type.code.width
        processor: Optional[Callable[[], Union[int, float, str, bytes]]] = {
            TC.U08           : reader.u8,
            TC.S08           : reader.i8,
            TC.U16           : reader.u16,
            TC.S16           : reader.i16,
            TC.U32           : reader.u32,
            TC.S32           : reader.i32,
            TC.S64           : reader.i64,
            TC.Single        : reader.f32,
            TC.Double        : reader.f64,
            TC.Extended      : lambda: extended(reader.read(10)),
            TC.AnsiString    : lambda: reader.read_length_prefixed(encoding=self.codec),
            TC.PChar         : lambda: reader.read_length_prefixed(encoding=self.codec),
            TC.WideString    : reader.read_length_prefixed_utf16,
            TC.UnicodeString : reader.read_length_prefixed_utf16,
            TC.Char          : lambda: chr(reader.u8()),
            TC.WideChar      : lambda: chr(reader.u16()),
            TC.ProcPtr       : lambda: self.functions[reader.u32()],
            TC.Set           : lambda: int.from_bytes(reader.read(type.size_in_bytes), 'little'),
            TC.Currency      : lambda: reader.u64() / 10_000,
        }.get(type.code, None)
        if processor is not None:
            data = processor()
        elif size > 0:
            data = bytes(reader.read(size))
        else:
            raise ValueError(F'Unable to read attribute of type {type!s}.')
        if isinstance(data, str) and data not in self.strings:
            self.strings.append(data)
        return Value(type, data)

    def _read_attributes(self) -> Generator[FunctionAttribute, None, None]:
        reader = self.reader
        count = reader.u32()
        for _ in range(count):
            name = reader.read_length_prefixed_ascii()
            fields = tuple(self._read_value() for _ in range(reader.u32()))
            yield FunctionAttribute(name, fields)

    def _load_functions(self):
        def _signature(name: str, decl: Optional[DeclSpec]):
            signature = IFPSAPI.get(name, IFPSEvents.get(name)) if name else None
            if decl and decl.classname and (ic := IFPSClasses.Classes.get(decl.classname)):
                if ic.name not in self.types_by_name:
                    missing_types.add(ic.name)
                signature = ic.members.get(decl.name, signature)
                decl.classname = ic.name
            return signature

        reader = self.reader
        rewind = reader.tell()
        width = len(F'{self.count_functions:X}')
        missing_types = set()
        load_flags = (self.version >= 23)

        reparsed = False
        all_void = True
        all_long = True
        has_dll_imports = False

        while True:
            for k in range(self.count_functions):
                decl = None
                body = None
                name = F'F{k:0{width}X}'
                tags = reader.u8()
                attributes = None
                exported = FTag.Exported.check(tags)
                if FTag.External.check(tags):
                    name = reader.read_length_prefixed_ascii(8)
                    if exported:
                        read = StructReader(bytes(reader.read_length_prefixed()))
                        decl = DeclSpec.ParseF(read, load_flags)
                        if not reparsed and decl.module is not None:
                            has_dll_imports = True
                            # inno: 0d13564460b4cca289ac60221e86ca5719d7217a8eb76671b4b2a8407c2af6b4
                            # ifps: 6c211c02652317903b23c827cbc311a258fcd6197eec6a3d2f91986bd8accb0e
                            # This script reports version 22 and therefore, load_flags starts as False.
                            # However, it should be true; the reasons are unclear. The code below is
                            # an attempt to identify incorrect load_flags values heuristically. When
                            # there are no __delay_load functions present, reading them with load_flags
                            # set to False will result in only procedures (void=True) with at least
                            # 2 arguments.
                            if not decl.void:
                                all_void = False
                            if len(decl.parameters) < 2:
                                all_long = False
                else:
                    offset = reader.u32()
                    length = reader.u32()
                    if exported:
                        name = reader.read_length_prefixed_ascii()
                        decl = DeclSpec.ParseE(bytes(reader.read_length_prefixed()), self)
                    with reader.detour(offset):
                        body = reader.read(length)
                if FTag.HasAttrs.check(tags):
                    attributes = list(self._read_attributes())
                fn = Function(name, decl, body, exported, attributes)
                self.functions.append(fn)
            if has_dll_imports and all_long and all_void and not reparsed:
                load_flags = True
                reparsed = True
                reader.seekset(rewind)
                self.functions.clear()
            else:
                break

        byfqn: Dict[str, List[Function]] = {}

        for function in self.functions:
            key = str(function)
            byfqn.setdefault(key, []).append(function)
            if body := function.body:
                void = decl.void if (decl := function.decl) else False
                function.body = list(self._parse_bytecode(body, void))

        for functions in byfqn.values():
            if len(functions) != 2:
                continue
            getter, setter = functions
            if not (s_decl := setter.decl):
                continue
            if not (g_decl := getter.decl):
                continue
            if setter.decl.is_property:
                setter, getter = getter, setter
                s_decl, g_decl = g_decl, s_decl
            if s_decl.is_property:
                continue
            if not g_decl.is_property:
                continue
            g_decl.is_property = False
            g_decl.name = F'Get{g_decl.name}'
            s_decl.name = F'Set{s_decl.name}'

        for function in self.functions:
            name = function.symbol
            decl = function.decl
            if (signature := _signature(name, decl)) and decl:
                if signature.argc == decl.argc:
                    for old, new in itertools.zip_longest(decl.parameters, signature.parameters):
                        if not new:
                            break
                        if old and (t := old.type):
                            t.symbol = new.type
                            continue
                        if t := self.types_by_name.get(new.type):
                            t.symbol = new.type
                        else:
                            missing_types.add(new.type)
                if sr := signature.return_type:
                    if (dr := decl.return_type) or (dr := self.types_by_name.get(sr)):
                        dr.symbol = sr
                    else:
                        missing_types.add(sr)

        self.type_name_conflicts = self._name_types(missing_types)

        for function in self.functions:
            decl = function.decl
            if signature := _signature(function.name, decl):
                decl = decl or DeclSpec(True)
                decl.void = signature.void
                parameters = decl.parameters
                if signature.argc != len(parameters):
                    decl.parameters = parameters = [DeclSpecParam(True) for _ in range(signature.argc)]
                for old, new in zip(parameters, signature.parameters):
                    if old.type is None:
                        old.type = self.types_by_name.get(new.type)
                    old.name = new.name or old.name
                    old.const = new.const
                function.symbol = decl.name = signature.name
                if (rt := signature.return_type) and (decl.return_type is None):
                    decl.return_type = self.types_by_name.get(rt, decl.return_type)
                function.decl = decl
            elif decl and decl.is_property and decl.argc >= (k := decl.void + 1):
                del decl.parameters[:k]

        for function in self.functions:
            if (decl := function.decl) and decl.is_property:
                classname = decl.classname
                this = DeclSpecParam(True, self.types_by_name.get(classname), 'This')
                nval = DeclSpecParam(True, decl.return_type, 'NewValue')
                info = IFPSClasses.Classes.get(classname)
                info = info and info.members.get(decl.name)
                if not info or info.writable:
                    function.setter = Function(decl=DeclSpec(
                        True,
                        [this, *decl.parameters, nval],
                        F'Set{decl.name}',
                        None,
                        None,
                        classname=classname,
                        is_accessor=True
                    ))
                if not info or info.readable:
                    function.getter = Function(decl=DeclSpec(
                        False,
                        [this, *decl.parameters],
                        F'Get{decl.name}',
                        None,
                        decl.return_type,
                        classname=classname,
                        is_accessor=True
                    ))

            if function.body is None:
                continue
            for instruction in function.body:
                if instruction.opcode is Op.Call:
                    t: Function = self.functions[instruction.operands[0]]
                    instruction.operands[0] = t

    def _load_variables(self):
        reader = self.reader
        for index in range(self.count_variables):
            code = reader.u32()
            spec = VariableSpec(index, VariableType.Global)
            if reader.u8() & 1:
                spec = reader.read_length_prefixed_ascii()
            self.globals.append(VariableBase(self.types[code], spec))

    def _read_variable_spec(self, index: int, void: bool) -> VariableSpec:
        if index < 0x40000000:
            return VariableSpec(index, VariableType.Global)
        index -= 0x60000000
        if index >= 0:
            return VariableSpec(index, VariableType.Local)
        index = -index if void else ~index
        return VariableSpec(index, VariableType.Argument)

    def _read_operand(self, reader: StructReader, void: bool) -> Operand:
        ot = OperandType(reader.u8())
        kw = {}
        if ot is OperandType.Variable:
            kw.update(variable=self._read_variable_spec(reader.u32(), void))
        if ot is OperandType.Value:
            kw.update(value=self._read_value(reader))
        if ot >= OperandType.IndexedByInt:
            kw.update(variable=self._read_variable_spec(reader.u32(), void))
            index = reader.u32()
            if ot is OperandType.IndexedByVar:
                index = self._read_variable_spec(index, void)
            kw.update(index=index)
        return Operand(ot, **kw)

    def _parse_bytecode(self, data: memoryview, void: bool) -> Generator[Instruction, None, None]:
        disassembly: Dict[int, Instruction] = OrderedDict()
        reader = StructReader(data)

        argcount = {
            Op.Assign: 2,
            Op.CallVar: 1,
            Op.Dec: 1,
            Op.Inc: 1,
            Op.BooleanNot: 1,
            Op.Neg: 1,
            Op.IntegerNot: 1,
            Op.SetPtrToCopy: 2,
            Op.SetPtr: 2,
        }

        while not reader.eof:
            def arg(k=1):
                for _ in range(k):
                    args.append(self._read_operand(reader, void))
            addr = reader.tell()
            cval = reader.u8()
            code = Op.FromInt(cval)
            insn = Instruction(addr, code)
            args = insn.operands
            disassembly[insn.offset] = insn
            aryness = argcount.get(code)
            if aryness is not None:
                arg(aryness)
            elif code in (Op.Ret, Op.Nop, Op.Pop):
                pass
            elif code is Op.Calculate:
                insn.operator = AOp(reader.u8())
                arg(2)
            elif code in (Op.Push, Op.PushVar):
                arg()
            elif code in (Op.Jump, Op.JumpFlag):
                target = reader.i32()
                args.append(reader.tell() + target)
            elif code is Op.Call:
                args.append(reader.u32())
            elif code in (Op.JumpTrue, Op.JumpFalse):
                target = reader.i32()
                val = self._read_operand(reader, void)
                args.append(reader.tell() + target)
                args.append(val)
            elif code is Op.JumpPop1:
                target = reader.i32()
                args.append(reader.tell() + target)
            elif code is Op.JumpPop2:
                target = reader.i32()
                args.append(reader.tell() + target)
            elif code is Op.StackType:
                args.append(self._read_variable_spec(reader.u32(), void))
                args.append(reader.u32())
            elif code is Op.PushType:
                args.append(self.types[reader.u32()])
            elif code is Op.Compare:
                insn.operator = COp(reader.u8())
                arg(3)
            elif code is Op.SetFlag:
                arg()
                args.append(bool(reader.u8()))
            elif code is Op.PushEH:
                args.extend(reader.i32() for _ in range(4))
                for k, a in enumerate(args):
                    args[k] = a + reader.tell() if a >= 0 else None
            elif code is Op.PopEH:
                args.append(reader.u8())
            elif code is Op._INVALID:
                raise ValueError(F'Unsupported opcode: 0x{cval:02X}')
            else:
                raise ValueError(F'Unhandled opcode: {code.name}')
            insn.size = reader.tell() - addr

        for k, instruction in enumerate(disassembly.values()):
            if not instruction.branches:
                continue
            target = instruction.operands[0]
            try:
                disassembly[target].jumptarget = True
            except KeyError as K:
                raise RuntimeError(
                    F'The jump target of instruction {k} at 0x{instruction.offset:X} is invalid; '
                    F'the invalid instruction is a {instruction.opcode.name} to 0x{target:X}.'
                ) from K

        yield from disassembly.values()

    def __str__(self):
        return self.disassembly()

    def disassembly(self) -> str:
        def sortkey(f: Function):
            d = (d.module or '', d.classname or '', d.void) if (d := f.decl) else ('', '', True)
            return (*d, f.name)

        for function in self.functions:
            function.get_basic_blocks()

        classes: dict[str, dict[str, Function]] = {}
        external: list[Function] = []
        internal: list[Function] = []

        for t in self.types:
            if isinstance(t, TClass):
                classes[t.name] = {}

        for function in self.functions:
            if (decl := function.decl) and (name := decl.classname):
                try:
                    members = classes[name]
                except KeyError:
                    members = classes[name] = {}
                members[decl.name] = function
                continue
            dl = internal if function.body else external
            dl.append(function)

        external.sort(key=sortkey)

        output = io.StringIO()
        _omax = max((
            max(insn.offset for insn in fn.body)
            for fn in self.functions if fn.body
        ), default=0)
        _smax = max((
            max(insn.stack for insn in fn.body if insn.stack is not None)
            for fn in self.functions if fn.body
        ), default=0)
        _omax = max(len(self.types), len(self.globals), _omax)
        _omax = len(F'{_omax:X}')
        _smax = len(F'{_smax:d}')

        if classes:
            for name, members in classes.items():
                if not members:
                    output.write(F'external class {name};\n')
            output.write('\n')
            for name, members in classes.items():
                if not members:
                    continue
                output.write(F'external class {name}')
                if members:
                    for spec in members.values():
                        if spec.decl.is_accessor:
                            continue
                        output.write(F'\n{_TAB}{spec.decl.represent(spec.symbol, rel=True)}')
                    output.write('\nend')
                output.write(';\n\n')

        if self.types:
            typedefs = []
            for type in self.types:
                if type.code != TC.Record and type.symbol in (type.code.name, None):
                    continue
                if isinstance(type, TClass):
                    continue
                typedefs.append((type.symbol, type.display()))
            typedefs.sort()
            for symbol, display in typedefs:
                output.write(F'typedef {symbol} = {display}\n')
            output.write('\n')

        if self.globals:
            for variable in self.globals:
                output.write(F'global {variable!s}\n')
            output.write('\n')

        if external:
            for function in external:
                output.write(F'external {function!r}\n')
            output.write('\n')

        if internal:
            for function in internal:
                output.write(F'{function!r}\nbegin\n')
                labels = [insn.offset for insn in function.body if insn.jumptarget]
                labelw = max(len(str(len(labels))), 2)
                labeld = {v: F'JumpDestination{k:0{labelw}d}' for k, v in enumerate(labels, 1)}
                labelc = 0
                for instruction in function.body:
                    stack = instruction.stack
                    stack = '?' * _smax if stack is None else F'{stack:>{_smax}d}'
                    if instruction.jumptarget:
                        output.write(F'{labeld[labels[labelc]]}:\n')
                        labelc += 1
                    output.write(F'{_TAB}0x{instruction.offset:0{_omax}X}{_TAB}{stack}{_TAB}{instruction.pretty(labeld)}\n')
                output.write('end;\n\n')

        return output.getvalue().strip()

Global variables

var IFPSType

Represents any of the possible IFPS data types:

Functions

def extended(_data)

A helper function to parse 10 bytes into an extended type float value within the IFPS runtime.

Expand source code Browse git
def extended(_data: bytes):
    """
    A helper function to parse 10 bytes into an extended type float value within the IFPS runtime.
    """
    if len(_data) != 10:
        raise ValueError
    data = int.from_bytes(_data, 'little')
    sign = data >> 79
    data = data ^ (sign << 79)
    sign = -1.0 if sign else +1.0
    exponent = data >> 64
    data = data ^ (exponent << 64)
    if exponent == 0:
        if data == 0:
            return sign * 0
        exponent = -16382
    elif exponent == 0b111111111111111:
        if data == 0:
            return sign * float('Inf')
        else:
            return sign * float('NaN')
    else:
        exponent = exponent - 16383
    mantissa = data / (1 << 64)
    return sign * mantissa * (2 ** exponent)
def represent(cls)

A decorator for various IFPS integer enumeration classes to change the default string representation.

Expand source code Browse git
def represent(cls: _E) -> _E:
    """
    A decorator for various IFPS integer enumeration classes to change the default string
    representation.
    """
    cls.__repr__ = lambda self: F'{self.__class__.__name__}.{self.name}'
    cls. __str__ = lambda self: self.name
    return cls
def ifpstype(cls)

A decorator for IFPS types to mix the IFPSTypeMixin into the dataclass definition.

Expand source code Browse git
def ifpstype(cls: _C) -> Union[_C, Type[IFPSTypeMixin]]:
    """
    A decorator for IFPS types to mix the `refinery.lib.inno.ifps.IFPSTypeMixin` into the dataclass
    definition.
    """
    cls = dataclass(cls)
    mix = type(cls.__qualname__, (IFPSTypeMixin, cls), {})
    assigned = set(WRAPPER_ASSIGNMENTS) - {'__annotations__'}
    update_wrapper(mix, cls, assigned=assigned, updated=())
    return dataclass(mix)

Classes

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

An enumeration of all known IFPS opcodes.

Expand source code Browse git
class Op(enum.IntEnum):
    """
    An enumeration of all known IFPS opcodes.
    """
    Assign       = 0x00  # noqa
    Calculate    = 0x01  # noqa
    Push         = 0x02  # noqa
    PushVar      = 0x03  # noqa
    Pop          = 0x04  # noqa
    Call         = 0x05  # noqa
    Jump         = 0x06  # noqa
    JumpTrue     = 0x07  # noqa
    JumpFalse    = 0x08  # noqa
    Ret          = 0x09  # noqa
    StackType    = 0x0A  # noqa
    PushType     = 0x0B  # noqa
    Compare      = 0x0C  # noqa
    CallVar      = 0x0D  # noqa
    SetPtr       = 0x0E  # noqa
    BooleanNot   = 0x0F  # noqa
    Neg          = 0x10  # noqa
    SetFlag      = 0x11  # noqa
    JumpFlag     = 0x12  # noqa
    PushEH       = 0x13  # noqa
    PopEH        = 0x14  # noqa
    IntegerNot   = 0x15  # noqa
    SetPtrToCopy = 0x16  # noqa
    Inc          = 0x17  # noqa
    Dec          = 0x18  # noqa
    JumpPop1     = 0x19  # noqa
    JumpPop2     = 0x1A  # noqa
    Nop          = 0xFF  # noqa
    _INVALID     = 0xDD  # noqa

    @classmethod
    def FromInt(cls, code: int):
        try:
            return cls(code)
        except ValueError:
            return cls._INVALID

Ancestors

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

Class variables

var Assign
var Calculate
var Push
var PushVar
var Pop
var Call
var Jump
var JumpTrue
var JumpFalse
var Ret
var StackType
var PushType
var Compare
var CallVar
var SetPtr
var BooleanNot
var Neg
var SetFlag
var JumpFlag
var PushEH
var PopEH
var IntegerNot
var SetPtrToCopy
var Inc
var Dec
var JumpPop1
var JumpPop2
var Nop

Static methods

def FromInt(code)
Expand source code Browse git
@classmethod
def FromInt(cls, code: int):
    try:
        return cls(code)
    except ValueError:
        return cls._INVALID
class AOp (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration of all known IFPS arithmetic opcodes.

Expand source code Browse git
class AOp(enum.IntEnum):
    """
    An enumeration of all known IFPS arithmetic opcodes.
    """
    Add = 0
    Sub = 1
    Mul = 2
    Div = 3
    Mod = 4
    Shl = 5
    Shr = 6
    And = 7
    BOr = 8
    Xor = 9

    def __str__(self):
        glyph = ('+', '-', '*', '/', '%', '<<', '>>', '&', '|', '^')[self]
        return F'{glyph}='

Ancestors

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

Class variables

var Add
var Sub
var Mul
var Div
var Mod
var Shl
var Shr
var And
var BOr
var Xor
class COp (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration of all known IFPS comparison opcodes.

Expand source code Browse git
class COp(enum.IntEnum):
    """
    An enumeration of all known IFPS comparison opcodes.
    """
    GE = 0
    LE = 1
    GT = 2
    LT = 3
    NE = 4
    EQ = 5
    IN = 6
    IS = 7

    def __str__(self):
        return ('>=', '<=', '>', '<', '!=', '==', 'in', 'is')[self]

Ancestors

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

Class variables

var GE
var LE
var GT
var LT
var NE
var EQ
var IN
var IS
class TC (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration of all known IFPS type codes.

Expand source code Browse git
class TC(enum.IntEnum):
    """
    An enumeration of all known IFPS type codes.
    """
    ReturnAddress       = 0x00  # noqa
    U08                 = 0x01  # noqa
    S08                 = 0x02  # noqa
    U16                 = 0x03  # noqa
    S16                 = 0x04  # noqa
    U32                 = 0x05  # noqa
    S32                 = 0x06  # noqa
    Single              = 0x07  # noqa
    Double              = 0x08  # noqa
    Extended            = 0x09  # noqa
    AnsiString          = 0x0A  # noqa
    Record              = 0x0B  # noqa
    Array               = 0x0C  # noqa
    Pointer             = 0x0D  # noqa
    PChar               = 0x0E  # noqa
    ResourcePointer     = 0x0F  # noqa
    Variant             = 0x10  # noqa
    S64                 = 0x11  # noqa
    Char                = 0x12  # noqa
    WideString          = 0x13  # noqa
    WideChar            = 0x14  # noqa
    ProcPtr             = 0x15  # noqa
    StaticArray         = 0x16  # noqa
    Set                 = 0x17  # noqa
    Currency            = 0x18  # noqa
    Class               = 0x19  # noqa
    Interface           = 0x1A  # noqa
    NotificationVariant = 0x1B  # noqa
    UnicodeString       = 0x1C  # noqa
    Enum                = 0x81  # noqa
    Type                = 0x82  # noqa
    ExtClass            = 0x83  # noqa

    @property
    def primitive(self) -> bool:
        """
        Indicates whether the code represents a primitive type.
        """
        return self not in {
            TC.Class,
            TC.ProcPtr,
            TC.Interface,
            TC.Set,
            TC.StaticArray,
            TC.Array,
            TC.Record,
        }

    @property
    def container(self) -> bool:
        """
        Indicates whether the code represents a container type.
        """
        return self in {
            TC.StaticArray,
            TC.Array,
            TC.Record,
        }

    @property
    def width(self):
        """
        For primitive types, this gives the size of an immediate of this type in bytes.
        """
        return {
            TC.Variant       : 0x10,
            TC.Char          : 0x01,
            TC.S08           : 0x01,
            TC.U08           : 0x01,
            TC.WideChar      : 0x02,
            TC.S16           : 0x02,
            TC.U16           : 0x02,
            TC.WideString    : 0x04,
            TC.UnicodeString : 0x04,
            TC.Interface     : 0x04,
            TC.Class         : 0x04,
            TC.PChar         : 0x04,
            TC.AnsiString    : 0x04,
            TC.Single        : 0x04,
            TC.S32           : 0x04,
            TC.U32           : 0x04,
            TC.ProcPtr       : 0x0C,
            TC.Currency      : 0x08,
            TC.Pointer       : 0x0C,
            TC.Double        : 0x08,
            TC.S64           : 0x08,
            TC.Extended      : 0x0A,
            TC.ReturnAddress : 0x1C,
        }.get(self, 0)

Ancestors

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

Class variables

var ReturnAddress
var U08
var S08
var U16
var S16
var U32
var S32
var Single
var Double
var Extended
var AnsiString
var Record
var Array
var Pointer
var PChar
var ResourcePointer
var Variant
var S64
var Char
var WideString
var WideChar
var ProcPtr
var StaticArray
var Set
var Currency
var Class
var Interface
var NotificationVariant
var UnicodeString
var Enum
var Type
var ExtClass

Instance variables

var primitive

Indicates whether the code represents a primitive type.

Expand source code Browse git
@property
def primitive(self) -> bool:
    """
    Indicates whether the code represents a primitive type.
    """
    return self not in {
        TC.Class,
        TC.ProcPtr,
        TC.Interface,
        TC.Set,
        TC.StaticArray,
        TC.Array,
        TC.Record,
    }
var container

Indicates whether the code represents a container type.

Expand source code Browse git
@property
def container(self) -> bool:
    """
    Indicates whether the code represents a container type.
    """
    return self in {
        TC.StaticArray,
        TC.Array,
        TC.Record,
    }
var width

For primitive types, this gives the size of an immediate of this type in bytes.

Expand source code Browse git
@property
def width(self):
    """
    For primitive types, this gives the size of an immediate of this type in bytes.
    """
    return {
        TC.Variant       : 0x10,
        TC.Char          : 0x01,
        TC.S08           : 0x01,
        TC.U08           : 0x01,
        TC.WideChar      : 0x02,
        TC.S16           : 0x02,
        TC.U16           : 0x02,
        TC.WideString    : 0x04,
        TC.UnicodeString : 0x04,
        TC.Interface     : 0x04,
        TC.Class         : 0x04,
        TC.PChar         : 0x04,
        TC.AnsiString    : 0x04,
        TC.Single        : 0x04,
        TC.S32           : 0x04,
        TC.U32           : 0x04,
        TC.ProcPtr       : 0x0C,
        TC.Currency      : 0x08,
        TC.Pointer       : 0x0C,
        TC.Double        : 0x08,
        TC.S64           : 0x08,
        TC.Extended      : 0x0A,
        TC.ReturnAddress : 0x1C,
    }.get(self, 0)
class IFPSTypeMixin (symbol=None, attributes=None)

A helper class to mix additional properties into various IFPS type classes.

Expand source code Browse git
class IFPSTypeMixin:
    """
    A helper class to mix additional properties into various IFPS type classes.
    """
    symbol: Optional[str] = None
    attributes: Optional[List[FunctionAttribute]] = None

    def __str__(self):
        if self.symbol is not None:
            return self.symbol
        return super().__str__()

Subclasses

Class variables

var symbol
var attributes
class IFPSTypeBase (code)

The base class for any IFPS type.

Expand source code Browse git
class IFPSTypeBase(abc.ABC):
    """
    The base class for any IFPS type.
    """
    code: TC

    def simple(self, nested=False):
        """
        Indicate whether the type requires more than one line to pretty print.
        """
        return True

    def display(self, indent=0):
        """
        Compute a display string that can be used to represent the type in disassembly.
        """
        return indent * _TAB + self.code.name

    @abc.abstractmethod
    def py_type(self, key: Optional[int] = None) -> Optional[type]:
        """
        If possible, provide a Python type equivalent for this IFPS type. The optional key argument
        is required only for the `refinery.lib.inno.ifps.TRecord` class.
        """
        ...

    @abc.abstractmethod
    def default(self, key: Optional[int] = None):
        """
        Compute the default value for this type. The optional key argument is required only for the
        `refinery.lib.inno.ifps.TRecord` class.
        """
        ...

    @property
    def primitive(self) -> bool:
        """
        Indicates whether the type is primitive.
        """
        return self.code.primitive

    @property
    def container(self) -> bool:
        """
        Indicates whether the type is a container.
        """
        return self.code.container

    def __str__(self):
        return self.display(0)

Ancestors

  • abc.ABC

Subclasses

Class variables

var code

Instance variables

var primitive

Indicates whether the type is primitive.

Expand source code Browse git
@property
def primitive(self) -> bool:
    """
    Indicates whether the type is primitive.
    """
    return self.code.primitive
var container

Indicates whether the type is a container.

Expand source code Browse git
@property
def container(self) -> bool:
    """
    Indicates whether the type is a container.
    """
    return self.code.container

Methods

def simple(self, nested=False)

Indicate whether the type requires more than one line to pretty print.

Expand source code Browse git
def simple(self, nested=False):
    """
    Indicate whether the type requires more than one line to pretty print.
    """
    return True
def display(self, indent=0)

Compute a display string that can be used to represent the type in disassembly.

Expand source code Browse git
def display(self, indent=0):
    """
    Compute a display string that can be used to represent the type in disassembly.
    """
    return indent * _TAB + self.code.name
def py_type(self, key=None)

If possible, provide a Python type equivalent for this IFPS type. The optional key argument is required only for the TRecord class.

Expand source code Browse git
@abc.abstractmethod
def py_type(self, key: Optional[int] = None) -> Optional[type]:
    """
    If possible, provide a Python type equivalent for this IFPS type. The optional key argument
    is required only for the `refinery.lib.inno.ifps.TRecord` class.
    """
    ...
def default(self, key=None)

Compute the default value for this type. The optional key argument is required only for the TRecord class.

Expand source code Browse git
@abc.abstractmethod
def default(self, key: Optional[int] = None):
    """
    Compute the default value for this type. The optional key argument is required only for the
    `refinery.lib.inno.ifps.TRecord` class.
    """
    ...
class TPrimitive (code)

A primitive IFPS type.

Expand source code Browse git
class TPrimitive(IFPSTypeBase):
    """
    A primitive IFPS type.
    """
    def py_type(self, *_) -> Optional[type]:
        return {
            TC.ReturnAddress       : int,
            TC.U08                 : int,
            TC.S08                 : int,
            TC.U16                 : int,
            TC.S16                 : int,
            TC.U32                 : int,
            TC.S32                 : int,
            TC.Single              : float,
            TC.Double              : float,
            TC.Extended            : float,
            TC.AnsiString          : str,
            TC.Pointer             : VariableBase,
            TC.PChar               : str,
            TC.ResourcePointer     : VariableBase,
            TC.Variant             : object,
            TC.S64                 : int,
            TC.Char                : str,
            TC.WideString          : str,
            TC.WideChar            : str,
            TC.Currency            : float,
            TC.UnicodeString       : str,
            TC.Enum                : int,
            TC.Type                : IFPSType,
        }.get(self.code)

    def default(self, *_):
        if self.code in (TC.Char, TC.WideChar, TC.PChar):
            return '\0'
        tc = self.py_type()
        if issubclass(tc, (int, float, str)):
            return tc()

Ancestors

Subclasses

Class variables

var code

Inherited members

class TProcPtr (code, void, args)

The procedure pointer IFPS type.

Expand source code Browse git
class TProcPtr(IFPSTypeBase):
    """
    The procedure pointer IFPS type.
    """
    void: bool
    args: tuple[DeclSpecParam, ...]

    def py_type(self, *_):
        return None

    def default(self, *_):
        return None

    def display(self, indent=0):
        name = super().display(indent)
        args = []
        for k, spec in enumerate(self.args, 1):
            arg = F'Arg{k}'
            if not spec.const:
                arg = F'*{arg}'
            if spec.type is not None:
                arg = F'{spec.type!s} {arg}'
            args.append(arg)
        args = ', '.join(args)
        return F'{name}({args})'

Ancestors

Subclasses

Class variables

var void
var args

Inherited members

class TInterface (code, uuid)

An IFPS type representing a COM interface.

Expand source code Browse git
class TInterface(IFPSTypeBase):
    """
    An IFPS type representing a COM interface.
    """
    uuid: UUID

    def py_type(self, *_):
        return object

    def default(self, *_):
        return None

    def display(self, indent=0):
        display = super().display(indent)
        return F'{display}({self.uuid!s})'

Ancestors

Subclasses

Class variables

var uuid

Inherited members

class TClass (code, name)

An IFPS type representing an IFPS class.

Expand source code Browse git
class TClass(IFPSTypeBase):
    """
    An IFPS type representing an IFPS class.
    """
    name: str

    def py_type(self, *_):
        return None

    def default(self, *_):
        return None

Ancestors

Subclasses

Class variables

var name

Inherited members

class TSet (code, size)

An IFPS type representing a bit vector.

Expand source code Browse git
class TSet(IFPSTypeBase):
    """
    An IFPS type representing a bit vector.
    """
    size: int

    def py_type(self, *_):
        return int

    def default(self, *_):
        return 0

    @property
    def size_in_bytes(self):
        q, r = divmod(self.size, 8)
        return q + (r and 1 or 0)

    def display(self, indent=0):
        display = super().display(indent)
        return F'{display}({self.size})'

Ancestors

Subclasses

Class variables

var size

Instance variables

var size_in_bytes
Expand source code Browse git
@property
def size_in_bytes(self):
    q, r = divmod(self.size, 8)
    return q + (r and 1 or 0)

Inherited members

class TArray (code, type)

An IFPS type representing a dynamic array.

Expand source code Browse git
class TArray(IFPSTypeBase):
    """
    An IFPS type representing a dynamic array.
    """
    type: TPrimitive

    def py_type(self, key: Optional[int] = None):
        if key is None:
            return list
        return self.type.py_type()

    def default(self, key: Optional[int] = None):
        if key is None:
            return []
        return self.type.default()

    def display(self, indent=0):
        display = F'{_TAB * indent}{self.type!s}'
        return F'array of {display}'

    def simple(self, nested=False):
        return self.type.simple(nested)

Ancestors

Subclasses

Class variables

var type

Inherited members

class TStaticArray (code, type, size, offset=None)

An IFPS type representing a static array (i.e. a tuple).

Expand source code Browse git
class TStaticArray(IFPSTypeBase):
    """
    An IFPS type representing a static array (i.e. a tuple).
    """
    type: TPrimitive
    size: int
    offset: Optional[int] = None

    def py_type(self, key: Optional[int] = None):
        if key is None:
            return list
        return self.type.py_type(key)

    def default(self, key: Optional[int] = None):
        if key is None:
            return [self.type.default() for _ in range(self.size)]
        return self.type.default()

    def display(self, indent=0):
        display = F'{_TAB * indent}{self.type!s}'
        return F'{display}[{self.size}]'

    def simple(self, nested=False):
        return self.type.simple(nested)

Ancestors

Subclasses

Class variables

var type
var size
var offset

Inherited members

class TRecord (code, members)

An IFPS type representing a structure.

Expand source code Browse git
class TRecord(IFPSTypeBase):
    """
    An IFPS type representing a structure.
    """
    members: Tuple[TPrimitive, ...]

    @property
    def size(self):
        return len(self.members)

    def py_type(self, key: Optional[int] = None):
        if key is None:
            return list
        return self.members[key].py_type()

    def default(self, key: Optional[int] = None):
        if key is None:
            return [member.default() for member in self.members]
        return self.members[key].default()

    def simple(self, nested=False):
        if nested:
            return False
        if len(self.members) > 10:
            return False
        return all(m.simple(True) for m in self.members)

    def display(self, indent=0):
        output = io.StringIO()
        output.write(indent * _TAB)
        output.write('struct {')
        if self.simple():
            output.write(', '.join(str(m) for m in self.members))
        else:
            for k, member in enumerate(self.members):
                if k > 0:
                    output.write(',')
                output.write('\n')
                output.write(member.display(indent + 1))
            if self.members:
                output.write(F'\n{_TAB * indent}')
        output.write('}')
        return output.getvalue()

Ancestors

Subclasses

Class variables

var members

Instance variables

var size
Expand source code Browse git
@property
def size(self):
    return len(self.members)

Inherited members

class Value (type, value)

A value of the given type within the IFPS runtime.

Expand source code Browse git
class Value(NamedTuple):
    """
    A value of the given type within the IFPS runtime.
    """
    type: IFPSType
    value: Union[str, int, float, bytes, Function]

    def convert(self, *_):
        return self.type.py_type()

    def default(self, *_):
        return self.type.default()

    def __repr__(self):
        value = self.value
        if isinstance(value, bytes):
            value = value.hex()
        return F'{self.type.code.name}({value!r})'

    def __str__(self):
        v = self.value
        if isinstance(v, Function):
            return F'&{v!s}'
        return repr(v)

Ancestors

  • builtins.tuple

Instance variables

var type

Alias for field number 0

var value

Alias for field number 1

Methods

def convert(self, *_)
Expand source code Browse git
def convert(self, *_):
    return self.type.py_type()
def default(self, *_)
Expand source code Browse git
def default(self, *_):
    return self.type.default()
class FunctionAttribute (name, fields)

A function attribute.

Expand source code Browse git
class FunctionAttribute(NamedTuple):
    """
    A function attribute.
    """
    name: str
    fields: Tuple[Value, ...]

    def __repr__(self):
        name = self.name
        if self.fields:
            name += '[{}]'.format(','.join(repr(f) for f in self.fields))
        return name

Ancestors

  • builtins.tuple

Instance variables

var name

Alias for field number 0

var fields

Alias for field number 1

class DeclSpecParam (const, type=None, name=None)

A function parameter specification.

Expand source code Browse git
class DeclSpecParam:
    """
    A function parameter specification.
    """
    const: bool
    """
    True if this parameter is passed by value, not by reference.
    """
    type: Optional[TPrimitive] = None
    """
    The type of this parameter.
    """
    name: Optional[str] = None
    """
    The name of this parameter.
    """

Class variables

var const

True if this parameter is passed by value, not by reference.

var type

The type of this parameter.

var name

The name of this parameter.

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

This enumeration classifies the different call types.

Expand source code Browse git
class CallType(str, enum.Enum):
    """
    This enumeration classifies the different call types.
    """
    Symbol = 'symbol'
    Procedure = 'procedure'
    Function = 'function'
    Property = 'property'

    def __str__(self):
        return self.value

Ancestors

  • builtins.str
  • enum.Enum

Class variables

var Symbol
var Procedure
var Function
var Property
class DeclSpec (void, parameters=<factory>, name='', calling_convention=None, return_type=None, module=None, classname=None, delay_load=False, vtable_index=None, load_with_altered_search_path=False, is_property=False, is_accessor=False)

This class captures the declaration info of a function symbol.

Expand source code Browse git
class DeclSpec:
    """
    This class captures the declaration info of a function symbol.
    """
    void: bool
    parameters: List[DeclSpecParam] = field(default_factory=list)
    name: str = ''
    calling_convention: Optional[str] = None
    return_type: Optional[IFPSType] = None
    module: Optional[str] = None
    classname: Optional[str] = None
    delay_load: bool = False
    vtable_index: Optional[int] = None
    load_with_altered_search_path: bool = False
    is_property: bool = False
    is_accessor: bool = False

    @property
    def argc(self):
        return len(self.parameters)

    def represent(self, name: str, ref: bool = False, rel: bool = False):
        def pparam(k: int, p: DeclSpecParam):
            name = p.name or F'{VariableType.Argument!s}{k}'
            if p.type is not None:
                name = F'{name}: {p.type!s}'
            if not p.const:
                name = F'*{name}'
            return name
        if self.name and name in self.name:
            name = self.name
        spec = name
        if self.vtable_index is not None:
            spec = F'{self.name}[{self.vtable_index}]'
        if not rel and self.classname:
            spec = F'{self.classname}.{spec}'
        if not rel and self.module:
            spec = F'{self.module}::{spec}'
        if not ref:
            if self.delay_load:
                spec = F'__delay_load {spec}'
            if self.calling_convention and not self.is_property:
                spec = F'__{self.calling_convention} {spec}'
            spec = F'{self.type} {spec}'
            args = self.parameters
            args = args and ', '.join(pparam(*t) for t in enumerate(args, 1)) or ''
            if self.is_property:
                if args:
                    spec = F'{spec}[{args}]'
            else:
                spec = F'{spec}({args})'
            if self.return_type:
                spec = F'{spec}: {self.return_type!s}'
        return spec

    @property
    def type(self):
        if self.is_property:
            return CallType.Property
        if self.void:
            return CallType.Procedure
        return CallType.Function

    def __repr__(self):
        return self.represent(self.name or '(*)')

    @classmethod
    def ParseF(cls, reader: StructReader[bytes], load_flags: bool):
        def ascii():
            return reader.read_c_string('latin1')

        def boolean():
            return bool(reader.u8())

        def cc():
            return {
                0: 'register',
                1: 'pascal',
                2: 'cdecl',
                3: 'stdcall',
            }.get(reader.u8(), cls.calling_convention)

        def read_parameters():
            nonlocal void
            void = not boolean()
            parameters.extend(DeclSpecParam(not b) for b in reader.read())

        void = True
        name = None
        properties = {}
        parameters = []

        if reader.readif(b'dll:'):
            reader.readif(B'files:')
            if (module := ascii()).lower().endswith('.dll'):
                module = module[:-4]
            properties.update(module=module)
            name = ascii()
            properties.update(calling_convention=cc())
            if load_flags:
                properties.update(delay_load=boolean(), load_with_altered_search_path=boolean())
            read_parameters()
        elif reader.readif(b'class:'):
            if reader.remaining_bytes == 1:
                spec = reader.peek(1)
                void = False
                parameters.append(DeclSpecParam(False))
                name = {
                    b'+': 'CastToType',
                    B'-': 'SetNil'
                }.get(spec)
                properties.update(classname='Class', calling_convention='pascal')
            else:
                properties.update(classname=reader.read_terminated_array(b'|').decode('latin1'))
                name = reader.read_terminated_array(b'|').decode('latin1')
                if name[-1] == '@':
                    properties.update(is_property=True)
                    name = name[:-1]
                properties.update(calling_convention=cc())
                read_parameters()
        elif reader.readif(b'intf:.'):
            name = 'CoInterface'
            properties.update(vtable_index=reader.u32())
            properties.update(calling_convention=cc())
            read_parameters()
        else:
            read_parameters()
        return cls(void, parameters, name=name, **properties)

    @classmethod
    def ParseE(cls, data: bytes, ipfs: IFPSFile):
        decl = data.split(B'\x20')
        try:
            return_type = int(decl.pop(0))
        except Exception:
            void = True
        else:
            void = return_type < 0
        if not void:
            return_type = ipfs.types[return_type]
        else:
            return_type = None
        parameters = []
        for param in decl:
            try:
                i = int(param[1:])
            except Exception:
                tv = None
            else:
                tv = ipfs.types[i]
            parameters.append(
                DeclSpecParam(param[:1] == B'@', tv))
        return cls(void, parameters, return_type=return_type)

Class variables

var void
var parameters
var name
var calling_convention
var return_type
var module
var classname
var delay_load
var vtable_index
var load_with_altered_search_path
var is_property
var is_accessor

Static methods

def ParseF(reader, load_flags)
Expand source code Browse git
@classmethod
def ParseF(cls, reader: StructReader[bytes], load_flags: bool):
    def ascii():
        return reader.read_c_string('latin1')

    def boolean():
        return bool(reader.u8())

    def cc():
        return {
            0: 'register',
            1: 'pascal',
            2: 'cdecl',
            3: 'stdcall',
        }.get(reader.u8(), cls.calling_convention)

    def read_parameters():
        nonlocal void
        void = not boolean()
        parameters.extend(DeclSpecParam(not b) for b in reader.read())

    void = True
    name = None
    properties = {}
    parameters = []

    if reader.readif(b'dll:'):
        reader.readif(B'files:')
        if (module := ascii()).lower().endswith('.dll'):
            module = module[:-4]
        properties.update(module=module)
        name = ascii()
        properties.update(calling_convention=cc())
        if load_flags:
            properties.update(delay_load=boolean(), load_with_altered_search_path=boolean())
        read_parameters()
    elif reader.readif(b'class:'):
        if reader.remaining_bytes == 1:
            spec = reader.peek(1)
            void = False
            parameters.append(DeclSpecParam(False))
            name = {
                b'+': 'CastToType',
                B'-': 'SetNil'
            }.get(spec)
            properties.update(classname='Class', calling_convention='pascal')
        else:
            properties.update(classname=reader.read_terminated_array(b'|').decode('latin1'))
            name = reader.read_terminated_array(b'|').decode('latin1')
            if name[-1] == '@':
                properties.update(is_property=True)
                name = name[:-1]
            properties.update(calling_convention=cc())
            read_parameters()
    elif reader.readif(b'intf:.'):
        name = 'CoInterface'
        properties.update(vtable_index=reader.u32())
        properties.update(calling_convention=cc())
        read_parameters()
    else:
        read_parameters()
    return cls(void, parameters, name=name, **properties)
def ParseE(data, ipfs)
Expand source code Browse git
@classmethod
def ParseE(cls, data: bytes, ipfs: IFPSFile):
    decl = data.split(B'\x20')
    try:
        return_type = int(decl.pop(0))
    except Exception:
        void = True
    else:
        void = return_type < 0
    if not void:
        return_type = ipfs.types[return_type]
    else:
        return_type = None
    parameters = []
    for param in decl:
        try:
            i = int(param[1:])
        except Exception:
            tv = None
        else:
            tv = ipfs.types[i]
        parameters.append(
            DeclSpecParam(param[:1] == B'@', tv))
    return cls(void, parameters, return_type=return_type)

Instance variables

var argc
Expand source code Browse git
@property
def argc(self):
    return len(self.parameters)
var type
Expand source code Browse git
@property
def type(self):
    if self.is_property:
        return CallType.Property
    if self.void:
        return CallType.Procedure
    return CallType.Function

Methods

def represent(self, name, ref=False, rel=False)
Expand source code Browse git
def represent(self, name: str, ref: bool = False, rel: bool = False):
    def pparam(k: int, p: DeclSpecParam):
        name = p.name or F'{VariableType.Argument!s}{k}'
        if p.type is not None:
            name = F'{name}: {p.type!s}'
        if not p.const:
            name = F'*{name}'
        return name
    if self.name and name in self.name:
        name = self.name
    spec = name
    if self.vtable_index is not None:
        spec = F'{self.name}[{self.vtable_index}]'
    if not rel and self.classname:
        spec = F'{self.classname}.{spec}'
    if not rel and self.module:
        spec = F'{self.module}::{spec}'
    if not ref:
        if self.delay_load:
            spec = F'__delay_load {spec}'
        if self.calling_convention and not self.is_property:
            spec = F'__{self.calling_convention} {spec}'
        spec = F'{self.type} {spec}'
        args = self.parameters
        args = args and ', '.join(pparam(*t) for t in enumerate(args, 1)) or ''
        if self.is_property:
            if args:
                spec = F'{spec}[{args}]'
        else:
            spec = F'{spec}({args})'
        if self.return_type:
            spec = F'{spec}: {self.return_type!s}'
    return spec
class Function (symbol='', decl=None, body=None, attributes=None, getter=None, setter=None)

Represents a function in the IFPS runtime.

Expand source code Browse git
class Function:
    """
    Represents a function in the IFPS runtime.
    """
    symbol: str = ''
    decl: Optional[DeclSpec] = None
    body: Optional[List[Instruction]] = None
    attributes: Optional[List[FunctionAttribute]] = None
    _bbs: Optional[Dict[int, BasicBlock]] = None
    _ins: Optional[Dict[int, Instruction]] = None
    getter: Optional[Function] = None
    setter: Optional[Function] = None

    @property
    def is_property(self):
        if decl := self.decl:
            return decl.is_property
        else:
            return False

    @property
    def name(self):
        symbol = self.symbol
        if (decl := self.decl) and (name := decl.name) and (symbol in name):
            symbol = name
        return symbol

    @property
    def code(self):
        if code := self._ins:
            return code
        self._ins = code = {i.offset: i for i in self.body}
        return code

    def reference(self, rel: bool = False) -> str:
        if self.decl is None:
            return self.symbol
        return self.decl.represent(self.symbol, ref=True, rel=rel)

    def __repr__(self):
        if self.decl is None:
            return F'symbol {self.symbol}'
        return self.decl.represent(self.symbol)

    def __str__(self):
        return self.reference()

    @property
    def type(self):
        if self.decl is None:
            return CallType.Symbol
        return self.decl.type

    def get_basic_blocks(self) -> Dict[int, BasicBlock]:
        if (bbs := self._bbs) is not None:
            return bbs
        if self.body is None:
            bbs = self._bbs = {}
            return bbs

        bbs: Dict[int, BasicBlock] = {0: (bb := BasicBlock(0))}
        self._bbs = bbs
        jump = False

        for insn in self.body:
            try:
                bb = bbs[insn.offset]
            except KeyError:
                if jump or insn.jumptarget:
                    nb = bbs[insn.offset] = BasicBlock(insn.offset)
                    if not jump:
                        nb.sources[bb.offset] = bb
                        bb.targets[nb.offset] = nb
                    bb = nb
            bb.body.append(insn)
            if not insn.branches:
                jump = False
                continue
            targets = [insn.operands[0]]
            sequence = insn.offset + insn.size
            jump = insn.jumps
            if not jump and insn.opcode != Op.Ret:
                targets.append(sequence)
            for t in targets:
                if not (bt := bbs.get(t)):
                    bt = bbs[t] = BasicBlock(t)
                bb.targets[t] = bt
                bt.sources[bb.offset] = bb

        for offset, bb in list(bbs.items()):
            if bb.body:
                continue
            del bbs[offset]
            for source in bb.sources.values():
                source.targets.pop(offset, None)

        visited: set[int] = set()
        errored: set[int] = set()

        def trace_stack(offset: int, stack: Optional[int]):
            if offset in errored:
                return
            bb = bbs[offset]
            if bb.stack is not None and stack != bb.stack:
                stack = None
            if stack is None:
                errored.add(offset)
            elif offset in visited:
                return
            else:
                visited.add(offset)
            bb.stack = stack
            body = [] if stack is None else bb.body
            for insn in body:
                insn.stack = stack
                stack += insn.stack_delta
            for t in bb.targets:
                trace_stack(t, stack)

        trace_stack(0, 0)

        for insn in self.body:
            if (stack := insn.stack) is None:
                continue
            for k, op in enumerate(insn.operands):
                if not isinstance(op, Operand):
                    continue
                if not (v := op.variable) or v.type != VariableType.Local:
                    continue
                if v.index <= stack:
                    continue
                raise IndexError(
                    F'Instruction {op!s} at offset 0x{insn.offset:X} in function {self.name} has '
                    F'variable operand {k} whose index {v.index} exceeds the stack depth {stack}.')

        return bbs

Class variables

var symbol
var decl
var body
var attributes
var getter
var setter

Instance variables

var is_property
Expand source code Browse git
@property
def is_property(self):
    if decl := self.decl:
        return decl.is_property
    else:
        return False
var name
Expand source code Browse git
@property
def name(self):
    symbol = self.symbol
    if (decl := self.decl) and (name := decl.name) and (symbol in name):
        symbol = name
    return symbol
var code
Expand source code Browse git
@property
def code(self):
    if code := self._ins:
        return code
    self._ins = code = {i.offset: i for i in self.body}
    return code
var type
Expand source code Browse git
@property
def type(self):
    if self.decl is None:
        return CallType.Symbol
    return self.decl.type

Methods

def reference(self, rel=False)
Expand source code Browse git
def reference(self, rel: bool = False) -> str:
    if self.decl is None:
        return self.symbol
    return self.decl.represent(self.symbol, ref=True, rel=rel)
def get_basic_blocks(self)
Expand source code Browse git
def get_basic_blocks(self) -> Dict[int, BasicBlock]:
    if (bbs := self._bbs) is not None:
        return bbs
    if self.body is None:
        bbs = self._bbs = {}
        return bbs

    bbs: Dict[int, BasicBlock] = {0: (bb := BasicBlock(0))}
    self._bbs = bbs
    jump = False

    for insn in self.body:
        try:
            bb = bbs[insn.offset]
        except KeyError:
            if jump or insn.jumptarget:
                nb = bbs[insn.offset] = BasicBlock(insn.offset)
                if not jump:
                    nb.sources[bb.offset] = bb
                    bb.targets[nb.offset] = nb
                bb = nb
        bb.body.append(insn)
        if not insn.branches:
            jump = False
            continue
        targets = [insn.operands[0]]
        sequence = insn.offset + insn.size
        jump = insn.jumps
        if not jump and insn.opcode != Op.Ret:
            targets.append(sequence)
        for t in targets:
            if not (bt := bbs.get(t)):
                bt = bbs[t] = BasicBlock(t)
            bb.targets[t] = bt
            bt.sources[bb.offset] = bb

    for offset, bb in list(bbs.items()):
        if bb.body:
            continue
        del bbs[offset]
        for source in bb.sources.values():
            source.targets.pop(offset, None)

    visited: set[int] = set()
    errored: set[int] = set()

    def trace_stack(offset: int, stack: Optional[int]):
        if offset in errored:
            return
        bb = bbs[offset]
        if bb.stack is not None and stack != bb.stack:
            stack = None
        if stack is None:
            errored.add(offset)
        elif offset in visited:
            return
        else:
            visited.add(offset)
        bb.stack = stack
        body = [] if stack is None else bb.body
        for insn in body:
            insn.stack = stack
            stack += insn.stack_delta
        for t in bb.targets:
            trace_stack(t, stack)

    trace_stack(0, 0)

    for insn in self.body:
        if (stack := insn.stack) is None:
            continue
        for k, op in enumerate(insn.operands):
            if not isinstance(op, Operand):
                continue
            if not (v := op.variable) or v.type != VariableType.Local:
                continue
            if v.index <= stack:
                continue
            raise IndexError(
                F'Instruction {op!s} at offset 0x{insn.offset:X} in function {self.name} has '
                F'variable operand {k} whose index {v.index} exceeds the stack depth {stack}.')

    return bbs
class VariableBase (type, spec)

This class represents a variable within the IFPS runtime. This is primarily a base class for the more sophisticated Variable.

Expand source code Browse git
class VariableBase:
    """
    This class represents a variable within the IFPS runtime. This is primarily a base class for
    the more sophisticated `refinery.lib.inno.emulator.Variable`.
    """
    type: IFPSType
    """
    The type of the variable, see `refinery.lib.inno.ifps.IFPSType`.
    """
    spec: Optional[VariableSpec]
    """
    A `refinery.lib.inno.ifps.VariableSpec` that uniquely identifies the base variable. If this
    property is `None`, the variable is unbound: The `refinery.lib.inno.ifps.Op.SetPtrToCopy`
    opcode can create such variables.
    """

    __slots__ = 'type', 'spec'

    def __init__(self, type: IFPSType, spec: VariableSpec):
        self.type = type
        self.spec = spec

    def __str__(self):
        return F'{self.spec}: {self.type!s}'

Subclasses

Instance variables

var spec

A VariableSpec that uniquely identifies the base variable. If this property is None, the variable is unbound: The Op.SetPtrToCopy opcode can create such variables.

var type

The type of the variable, see IFPSType.

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

Classifies the type of an Operand.

Expand source code Browse git
class OperandType(enum.IntEnum):
    """
    Classifies the type of an `refinery.lib.inno.ifps.Operand`.
    """
    Variable = 0
    Value = 1
    IndexedByInt = 2
    IndexedByVar = 3

Ancestors

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

Class variables

var Variable
var Value
var IndexedByInt
var IndexedByVar
class EHType (value, names=None, *, module=None, qualname=None, type=None, start=1)

This enumeration lists the possible types of code region covered by an exception handler.

Expand source code Browse git
class EHType(enum.IntEnum):
    """
    This enumeration lists the possible types of code region covered by an exception handler.
    """
    Try = 0
    Finally = 1
    Catch = 2
    SecondFinally = 3

Ancestors

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

Class variables

var Try
var Finally
var Catch
var SecondFinally
class NewEH (value, names=None, *, module=None, qualname=None, type=None, start=1)

This enumeration gives names to the 4 arguments of the opcode responsible for registering a new exception handler. The first argument specifies the location of a finally, the second argument specifies the location of a catch handler, and so on.

Expand source code Browse git
class NewEH(enum.IntEnum):
    """
    This enumeration gives names to the 4 arguments of the opcode responsible for registering a new
    exception handler. The first argument specifies the location of a finally, the second argument
    specifies the location of a catch handler, and so on.
    """
    Finally = 0
    CatchAt = 1
    SecondFinally = 2
    End = 3

Ancestors

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

Class variables

var Finally
var CatchAt
var SecondFinally
var End
class VariableType (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class VariableType(str, enum.Enum):
    Global = 'GlobalVar'
    Local = 'LocalVar'
    Argument = 'Argument'

    def __repr__(self):
        return self.name

    def __str__(self):
        return self.value

Ancestors

  • builtins.str
  • enum.Enum

Class variables

var Global
var Local
var Argument
class VariableSpec (index, type)

Represents a reference to a variable within the IFPS runtime. There are three variable types: Locals, globals, and function arguments; see VariableType. A variable is then uniquely defined by its type and index within the (localized) list of such variables. The function argument of index zero is the return value of a function.

Expand source code Browse git
class VariableSpec(NamedTuple):
    """
    Represents a reference to a variable within the IFPS runtime. There are three variable types:
    Locals, globals, and function arguments; see `refinery.lib.inno.ifps.VariableType`. A variable
    is then uniquely defined by its type and index within the (localized) list of such variables.
    The function argument of index zero is the return value of a function.
    """
    index: int
    type: VariableType

    def __repr__(self):
        if self.index == 0 and self.type == VariableType.Argument:
            return 'ReturnValue'
        return F'{self.type!s}{self.index}'

Ancestors

  • builtins.tuple

Instance variables

var index

Alias for field number 0

var type

Alias for field number 1

class Operand (type, variable=None, value=None, index=None)

Represends an operand to an IFPS opcode. An operand can either contain a value, which is an immediate that is encoded into the opcode, or a reference to a variable. A variable is given by its VariableSpec. Additionally, the operand can specify an index for this variable which can either be given by an immediate, or by another variable. In the latter case, the encoded index is also a VariableSpec. The type of operand is encoded as an OperandType.

Expand source code Browse git
class Operand(NamedTuple):
    """
    Represends an operand to an IFPS opcode. An operand can either contain a value, which is an
    immediate that is encoded into the opcode, or a reference to a variable. A variable is given
    by its `refinery.lib.inno.ifps.VariableSpec`. Additionally, the operand can specify an index
    for this variable which can either be given by an immediate, or by another variable. In the
    latter case, the encoded index is also a `refinery.lib.inno.ifps.VariableSpec`. The type of
    operand is encoded as an `refinery.lib.inno.ifps.OperandType`.
    """
    type: OperandType
    variable: Optional[VariableSpec] = None
    value: Optional[Value] = None
    index: Optional[Union[VariableSpec, int]] = None

    def __repr__(self):
        return self.__tostring(repr)

    def __str__(self):
        return self.__tostring(str)

    @property
    def immediate(self):
        return self.type == OperandType.Value

    def __tostring(self, converter):
        if self.type is OperandType.Value:
            return converter(self.value)
        if self.type is OperandType.Variable:
            return converter(self.variable)
        if self.type is OperandType.IndexedByInt:
            return F'{converter(self.variable)}[0x{self.index:02X}]'
        if self.type is OperandType.IndexedByVar:
            return F'{converter(self.variable)}[{self.index!s}]'
        raise RuntimeError(F'Unexpected OperandType {self.type!r} in {self.__class__.__name__}')

Ancestors

  • builtins.tuple

Instance variables

var type

Alias for field number 0

var variable

Alias for field number 1

var value

Alias for field number 2

var index

Alias for field number 3

var immediate
Expand source code Browse git
@property
def immediate(self):
    return self.type == OperandType.Value
class Instruction (offset, opcode, size=0, stack=None, operands=<factory>, operator=None, jumptarget=False)

Instruction(offset: 'int', opcode: 'Op', size: 'int' = 0, stack: 'Optional[int]' = None, operands: 'List[Union[str, bool, int, float, Operand, IFPSType, Function, None]]' = , operator: 'Optional[Union[AOp, COp]]' = None, jumptarget: 'bool' = False)

Expand source code Browse git
class Instruction:
    offset: int
    opcode: Op
    size: int = 0
    stack: Optional[int] = None
    operands: List[Union[str, bool, int, float, Operand, IFPSType, Function, None]] = field(default_factory=list)
    operator: Optional[Union[AOp, COp]] = None
    jumptarget: bool = False

    def op(self, index: int):
        arg = self.operands[index]
        if not isinstance(arg, Operand):
            raise TypeError
        return arg

    @property
    def branches(self):
        return self.opcode in (
            Op.Jump,
            Op.JumpFalse,
            Op.JumpTrue,
            Op.JumpFlag,
            Op.JumpPop1,
            Op.JumpPop2,
        )

    @property
    def jumps(self):
        return self.opcode in (
            Op.Jump,
            Op.JumpPop1,
            Op.JumpPop2,
        )

    @property
    def stack_delta(self):
        return _Op_StackD.get(self.opcode, 0)

    def oprep(self, labels: Optional[dict[int, str]] = None):
        if self.branches:
            dst = self.operands[0]
            if not labels or not (label := labels.get(dst)):
                label = F'0x{dst:X}'
            var = [str(op) for op in self.operands[1:]]
            return ', '.join((label, *var))
        elif self.opcode is Op.PushEH:
            ops = []
            for op, name in reversed(list(zip(self.operands, NewEH))):
                if op is None:
                    continue
                ops.append(F'{name}:0x{op:X}')
            return '\x20'.join(ops)
        elif self.opcode is Op.PopEH:
            return F'End{EHType(self.operands[0])}'
        elif self.opcode is Op.SetFlag:
            rep, negated = self.operands
            return F'!{rep}' if negated else str(rep)
        elif self.opcode is Op.Compare:
            dst, a, b = self.operands
            return F'{dst!s} := {a!s} {self.operator!s} {b!s}'
        elif self.opcode is Op.Calculate:
            dst, src = self.operands
            return F'{dst!s} {self.operator!s} {src!s}'
        elif self.opcode in (Op.Assign, Op.SetPtr, Op.SetPtrToCopy):
            dst, src = self.operands
            return F'{dst!s} := {src!s}'
        else:
            return ', '.join(str(op) for op in self.operands)

    def pretty(self, labels: Optional[dict[int, str]] = None):
        return F'{self.opcode!s:<{_Op_Maxlen}}{_TAB}{self.oprep(labels)}'.strip()

    def __repr__(self):
        return F'{self.opcode.name}({self.oprep()})'

    def __str__(self):
        return self.pretty()

Class variables

var offset
var opcode
var operands
var size
var stack
var operator
var jumptarget

Instance variables

var branches
Expand source code Browse git
@property
def branches(self):
    return self.opcode in (
        Op.Jump,
        Op.JumpFalse,
        Op.JumpTrue,
        Op.JumpFlag,
        Op.JumpPop1,
        Op.JumpPop2,
    )
var jumps
Expand source code Browse git
@property
def jumps(self):
    return self.opcode in (
        Op.Jump,
        Op.JumpPop1,
        Op.JumpPop2,
    )
var stack_delta
Expand source code Browse git
@property
def stack_delta(self):
    return _Op_StackD.get(self.opcode, 0)

Methods

def op(self, index)
Expand source code Browse git
def op(self, index: int):
    arg = self.operands[index]
    if not isinstance(arg, Operand):
        raise TypeError
    return arg
def oprep(self, labels=None)
Expand source code Browse git
def oprep(self, labels: Optional[dict[int, str]] = None):
    if self.branches:
        dst = self.operands[0]
        if not labels or not (label := labels.get(dst)):
            label = F'0x{dst:X}'
        var = [str(op) for op in self.operands[1:]]
        return ', '.join((label, *var))
    elif self.opcode is Op.PushEH:
        ops = []
        for op, name in reversed(list(zip(self.operands, NewEH))):
            if op is None:
                continue
            ops.append(F'{name}:0x{op:X}')
        return '\x20'.join(ops)
    elif self.opcode is Op.PopEH:
        return F'End{EHType(self.operands[0])}'
    elif self.opcode is Op.SetFlag:
        rep, negated = self.operands
        return F'!{rep}' if negated else str(rep)
    elif self.opcode is Op.Compare:
        dst, a, b = self.operands
        return F'{dst!s} := {a!s} {self.operator!s} {b!s}'
    elif self.opcode is Op.Calculate:
        dst, src = self.operands
        return F'{dst!s} {self.operator!s} {src!s}'
    elif self.opcode in (Op.Assign, Op.SetPtr, Op.SetPtrToCopy):
        dst, src = self.operands
        return F'{dst!s} := {src!s}'
    else:
        return ', '.join(str(op) for op in self.operands)
def pretty(self, labels=None)
Expand source code Browse git
def pretty(self, labels: Optional[dict[int, str]] = None):
    return F'{self.opcode!s:<{_Op_Maxlen}}{_TAB}{self.oprep(labels)}'.strip()
class BasicBlock (offset, stack=None, body=<factory>, sources=<factory>, targets=<factory>)

BasicBlock(offset: 'int', stack: 'Optional[int]' = None, body: 'List[Instruction]' = , sources: 'Dict[int, BasicBlock]' = , targets: 'Dict[int, BasicBlock]' = )

Expand source code Browse git
class BasicBlock:
    offset: int
    stack: Optional[int] = None
    body: List[Instruction] = field(default_factory=list)
    sources: Dict[int, BasicBlock] = field(default_factory=dict)
    targets: Dict[int, BasicBlock] = field(default_factory=dict)

    @property
    def stack_delta(self):
        return sum(insn.stack_delta for insn in self.body)

    @property
    def size(self):
        return sum(insn.size for insn in self.body)

Class variables

var offset
var body
var sources
var targets
var stack

Instance variables

var stack_delta
Expand source code Browse git
@property
def stack_delta(self):
    return sum(insn.stack_delta for insn in self.body)
var size
Expand source code Browse git
@property
def size(self):
    return sum(insn.size for insn in self.body)
class FTag (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code Browse git
class FTag(enum.IntFlag):
    External = 0b0001
    Exported = 0b0010
    HasAttrs = 0b0100

    def check(self, v):
        return bool(self & v)

Ancestors

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

Class variables

var External
var Exported
var HasAttrs

Methods

def check(self, v)
Expand source code Browse git
def check(self, v):
    return bool(self & v)
class IFPSFile (reader, codec='latin1', unicode=True)

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

foo = Struct(data, bar=29)

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

Expand source code Browse git
class IFPSFile(Struct):
    MinVer = 12
    MaxVer = 23

    Magic = B'IFPS'

    def __init__(self, reader: StructReader[memoryview], codec: str = 'latin1', unicode: bool = True):
        self.codec = codec
        self.unicode = unicode
        self.types: List[IFPSType] = []
        self.functions: List[Function] = []
        self.globals: List[VariableBase] = []
        self.strings: List[str] = []
        self.reader = reader
        if reader.remaining_bytes < 28:
            raise ValueError('Less than 28 bytes in file, not enough data to parse.')
        magic = reader.read(4)
        if magic != self.Magic:
            raise ValueError(F'Invalid magic sequence: {magic.hex()}')
        self.version = reader.u32()
        self.count_types = reader.u32()
        self.count_functions = reader.u32()
        self.count_variables = reader.u32()
        self.entry = reader.u32()
        self.import_size = reader.u32()

        if self.version not in range(self.MinVer, self.MaxVer + 1):
            raise NotImplementedError(
                F'This IFPS file has version {self.version}, which is not in the supported range '
                F'[{self.MinVer},{self.MaxVer}].')

        self._known_type_names = {
            TC.U08   : {'Byte', 'Boolean'},
            TC.S08   : {'ShortInt'},
            TC.U16   : {'Word'},
            TC.S16   : {'SmallInt'},
            TC.S32   : {'Integer', 'LongInt'},
            TC.U32   : {'LongWord', 'Cardinal', 'HWND', 'TSetupProcessorArchitecture'},
            TC.Char  : {'AnsiChar'},
            TC.PChar : {'PAnsiChar'},
            TC.S64   : {'Int64'},
        }

        self._load_types()
        self._name_types()
        self._load_functions()
        self._load_variables()

        del self._known_type_names

    def _name_types(self, missing_types: Optional[set[str]] = None):
        tbn: dict[str, IFPSType] = CaseInsensitiveDict()
        self.types_by_name = tbn
        for t in self.types:
            name = str(t)
            code = t.code
            known = self._known_type_names.get(code)
            if known:
                known.discard(name)
            tbn[name] = t

        if missing_types:
            def add_type(name: str, type: IFPSType):
                tbn[name] = type
                self.types.append(type)
                return type

            def make_string(name: str = 'String'):
                try:
                    return tbn[name]
                except KeyError:
                    pass
                for type in tbn.values():
                    if type.code in (
                        TC.AnsiString,
                        TC.WideString,
                        TC.UnicodeString,
                    ):
                        break
                else:
                    code = TC.WideString if self.unicode else TC.AnsiString
                    type = TPrimitive(code, symbol=name)
                return add_type(name, type)

            for name in tbn:
                missing_types.discard(name)

            if 'TGUID' in missing_types:
                missing_types |= {'LongWord', 'Word', 'Byte'}

            for code in TC:
                name = code.name
                if name not in missing_types:
                    continue
                missing_types.discard(name)
                add_type(name, TPrimitive(code, symbol=name))

            for code, names in self._known_type_names.items():
                for name in names:
                    if name not in missing_types:
                        continue
                    missing_types.discard(name)
                    add_type(name, TPrimitive(code, symbol=name))

            for name in [
                'TVarType',
                'TInputQueryWizardPage',
                'TInputOptionWizardPage',
                'TInputDirWizardPage',
                'TInputFileWizardPage',
                'TOutputMsgWizardPage',
                'TOutputMsgMemoWizardPage',
                'TOutputProgressWizardPage',
                'TOutputMarqueeProgressWizardPage',
                'TDownloadWizardPage',
                'ExtractionWizardPage',
                'TWizardPage',
                'TSetupForm',
                'TComponent',
                'TNewNotebookPage',
            ]:
                if name not in missing_types:
                    continue
                add_type(name, TClass(TC.Class, name, symbol=name))

            if (name := 'String') in missing_types:
                make_string(name)

            if (name := 'AnyString') in missing_types:
                make_string(name)

            if (name := 'TArrayOfString') in missing_types:
                add_type(name, TArray(TC.Array, make_string()))

            if (name := 'IUnknown') in missing_types:
                add_type(name, TInterface(TC.Interface, UUID('{00000000-0000-0000-C000-000000000046}')))

            if (name := 'TGUID') in missing_types:
                add_type(name, TRecord(TC.Record, (
                    tbn['LongWord'],
                    tbn['Word'],
                    tbn['Word'],
                    TStaticArray(tbn['Byte'], 8)
                ), symbol=name))

    def _load_types(self):
        def _normalize(n: str):
            return IFPSClasses.Types.get(n.casefold(), n)
        reader = self.reader
        types = self.types
        for k in range(self.count_types):
            typecode = reader.u8()
            exported = bool(typecode & 0x80)
            typecode = typecode & 0x7F
            try:
                code = TC(typecode)
            except ValueError as V:
                raise ValueError(F'Unknown type code value 0x{typecode:02X}.') from V
            if code in (TC.Class, TC.ExtClass):
                t = TClass(code, _normalize(reader.read_length_prefixed_ascii()))
            elif code is TC.ProcPtr:
                spec = reader.read_length_prefixed()
                void = bool(spec[0])
                args = tuple(DeclSpecParam(not b) for b in spec[1:])
                t = TProcPtr(code, void, args)
            elif code is TC.Interface:
                guid = UUID(bytes=bytes(reader.read(0x10)))
                t = TInterface(code, guid)
            elif code is TC.Set:
                t = TSet(code, reader.u32())
            elif code is TC.StaticArray:
                type = types[reader.u32()]
                size = reader.u32()
                offset = None if self.version <= 22 else reader.u32()
                t = TStaticArray(code, type, size, offset)
            elif code is TC.Array:
                t = TArray(code, types[reader.u32()])
            elif code is TC.Record:
                length = reader.u32()
                members = tuple(types[reader.u32()] for _ in range(length))
                t = TRecord(code, members, symbol=F'RECORD{k}')
            else:
                t = TPrimitive(code, symbol=code.name)
            if exported:
                t.symbol = _normalize(reader.read_length_prefixed_ascii())
                if self.version <= 21:
                    t.name = _normalize(reader.read_length_prefixed_ascii())
            types.append(t)
            if self.version >= 21:
                t.attributes = list(self._read_attributes())

    def _read_value(self, reader: Optional[StructReader] = None) -> Value:
        if reader is None:
            reader = self.reader
        type = self.types[reader.u32()]
        size = type.code.width
        processor: Optional[Callable[[], Union[int, float, str, bytes]]] = {
            TC.U08           : reader.u8,
            TC.S08           : reader.i8,
            TC.U16           : reader.u16,
            TC.S16           : reader.i16,
            TC.U32           : reader.u32,
            TC.S32           : reader.i32,
            TC.S64           : reader.i64,
            TC.Single        : reader.f32,
            TC.Double        : reader.f64,
            TC.Extended      : lambda: extended(reader.read(10)),
            TC.AnsiString    : lambda: reader.read_length_prefixed(encoding=self.codec),
            TC.PChar         : lambda: reader.read_length_prefixed(encoding=self.codec),
            TC.WideString    : reader.read_length_prefixed_utf16,
            TC.UnicodeString : reader.read_length_prefixed_utf16,
            TC.Char          : lambda: chr(reader.u8()),
            TC.WideChar      : lambda: chr(reader.u16()),
            TC.ProcPtr       : lambda: self.functions[reader.u32()],
            TC.Set           : lambda: int.from_bytes(reader.read(type.size_in_bytes), 'little'),
            TC.Currency      : lambda: reader.u64() / 10_000,
        }.get(type.code, None)
        if processor is not None:
            data = processor()
        elif size > 0:
            data = bytes(reader.read(size))
        else:
            raise ValueError(F'Unable to read attribute of type {type!s}.')
        if isinstance(data, str) and data not in self.strings:
            self.strings.append(data)
        return Value(type, data)

    def _read_attributes(self) -> Generator[FunctionAttribute, None, None]:
        reader = self.reader
        count = reader.u32()
        for _ in range(count):
            name = reader.read_length_prefixed_ascii()
            fields = tuple(self._read_value() for _ in range(reader.u32()))
            yield FunctionAttribute(name, fields)

    def _load_functions(self):
        def _signature(name: str, decl: Optional[DeclSpec]):
            signature = IFPSAPI.get(name, IFPSEvents.get(name)) if name else None
            if decl and decl.classname and (ic := IFPSClasses.Classes.get(decl.classname)):
                if ic.name not in self.types_by_name:
                    missing_types.add(ic.name)
                signature = ic.members.get(decl.name, signature)
                decl.classname = ic.name
            return signature

        reader = self.reader
        rewind = reader.tell()
        width = len(F'{self.count_functions:X}')
        missing_types = set()
        load_flags = (self.version >= 23)

        reparsed = False
        all_void = True
        all_long = True
        has_dll_imports = False

        while True:
            for k in range(self.count_functions):
                decl = None
                body = None
                name = F'F{k:0{width}X}'
                tags = reader.u8()
                attributes = None
                exported = FTag.Exported.check(tags)
                if FTag.External.check(tags):
                    name = reader.read_length_prefixed_ascii(8)
                    if exported:
                        read = StructReader(bytes(reader.read_length_prefixed()))
                        decl = DeclSpec.ParseF(read, load_flags)
                        if not reparsed and decl.module is not None:
                            has_dll_imports = True
                            # inno: 0d13564460b4cca289ac60221e86ca5719d7217a8eb76671b4b2a8407c2af6b4
                            # ifps: 6c211c02652317903b23c827cbc311a258fcd6197eec6a3d2f91986bd8accb0e
                            # This script reports version 22 and therefore, load_flags starts as False.
                            # However, it should be true; the reasons are unclear. The code below is
                            # an attempt to identify incorrect load_flags values heuristically. When
                            # there are no __delay_load functions present, reading them with load_flags
                            # set to False will result in only procedures (void=True) with at least
                            # 2 arguments.
                            if not decl.void:
                                all_void = False
                            if len(decl.parameters) < 2:
                                all_long = False
                else:
                    offset = reader.u32()
                    length = reader.u32()
                    if exported:
                        name = reader.read_length_prefixed_ascii()
                        decl = DeclSpec.ParseE(bytes(reader.read_length_prefixed()), self)
                    with reader.detour(offset):
                        body = reader.read(length)
                if FTag.HasAttrs.check(tags):
                    attributes = list(self._read_attributes())
                fn = Function(name, decl, body, exported, attributes)
                self.functions.append(fn)
            if has_dll_imports and all_long and all_void and not reparsed:
                load_flags = True
                reparsed = True
                reader.seekset(rewind)
                self.functions.clear()
            else:
                break

        byfqn: Dict[str, List[Function]] = {}

        for function in self.functions:
            key = str(function)
            byfqn.setdefault(key, []).append(function)
            if body := function.body:
                void = decl.void if (decl := function.decl) else False
                function.body = list(self._parse_bytecode(body, void))

        for functions in byfqn.values():
            if len(functions) != 2:
                continue
            getter, setter = functions
            if not (s_decl := setter.decl):
                continue
            if not (g_decl := getter.decl):
                continue
            if setter.decl.is_property:
                setter, getter = getter, setter
                s_decl, g_decl = g_decl, s_decl
            if s_decl.is_property:
                continue
            if not g_decl.is_property:
                continue
            g_decl.is_property = False
            g_decl.name = F'Get{g_decl.name}'
            s_decl.name = F'Set{s_decl.name}'

        for function in self.functions:
            name = function.symbol
            decl = function.decl
            if (signature := _signature(name, decl)) and decl:
                if signature.argc == decl.argc:
                    for old, new in itertools.zip_longest(decl.parameters, signature.parameters):
                        if not new:
                            break
                        if old and (t := old.type):
                            t.symbol = new.type
                            continue
                        if t := self.types_by_name.get(new.type):
                            t.symbol = new.type
                        else:
                            missing_types.add(new.type)
                if sr := signature.return_type:
                    if (dr := decl.return_type) or (dr := self.types_by_name.get(sr)):
                        dr.symbol = sr
                    else:
                        missing_types.add(sr)

        self.type_name_conflicts = self._name_types(missing_types)

        for function in self.functions:
            decl = function.decl
            if signature := _signature(function.name, decl):
                decl = decl or DeclSpec(True)
                decl.void = signature.void
                parameters = decl.parameters
                if signature.argc != len(parameters):
                    decl.parameters = parameters = [DeclSpecParam(True) for _ in range(signature.argc)]
                for old, new in zip(parameters, signature.parameters):
                    if old.type is None:
                        old.type = self.types_by_name.get(new.type)
                    old.name = new.name or old.name
                    old.const = new.const
                function.symbol = decl.name = signature.name
                if (rt := signature.return_type) and (decl.return_type is None):
                    decl.return_type = self.types_by_name.get(rt, decl.return_type)
                function.decl = decl
            elif decl and decl.is_property and decl.argc >= (k := decl.void + 1):
                del decl.parameters[:k]

        for function in self.functions:
            if (decl := function.decl) and decl.is_property:
                classname = decl.classname
                this = DeclSpecParam(True, self.types_by_name.get(classname), 'This')
                nval = DeclSpecParam(True, decl.return_type, 'NewValue')
                info = IFPSClasses.Classes.get(classname)
                info = info and info.members.get(decl.name)
                if not info or info.writable:
                    function.setter = Function(decl=DeclSpec(
                        True,
                        [this, *decl.parameters, nval],
                        F'Set{decl.name}',
                        None,
                        None,
                        classname=classname,
                        is_accessor=True
                    ))
                if not info or info.readable:
                    function.getter = Function(decl=DeclSpec(
                        False,
                        [this, *decl.parameters],
                        F'Get{decl.name}',
                        None,
                        decl.return_type,
                        classname=classname,
                        is_accessor=True
                    ))

            if function.body is None:
                continue
            for instruction in function.body:
                if instruction.opcode is Op.Call:
                    t: Function = self.functions[instruction.operands[0]]
                    instruction.operands[0] = t

    def _load_variables(self):
        reader = self.reader
        for index in range(self.count_variables):
            code = reader.u32()
            spec = VariableSpec(index, VariableType.Global)
            if reader.u8() & 1:
                spec = reader.read_length_prefixed_ascii()
            self.globals.append(VariableBase(self.types[code], spec))

    def _read_variable_spec(self, index: int, void: bool) -> VariableSpec:
        if index < 0x40000000:
            return VariableSpec(index, VariableType.Global)
        index -= 0x60000000
        if index >= 0:
            return VariableSpec(index, VariableType.Local)
        index = -index if void else ~index
        return VariableSpec(index, VariableType.Argument)

    def _read_operand(self, reader: StructReader, void: bool) -> Operand:
        ot = OperandType(reader.u8())
        kw = {}
        if ot is OperandType.Variable:
            kw.update(variable=self._read_variable_spec(reader.u32(), void))
        if ot is OperandType.Value:
            kw.update(value=self._read_value(reader))
        if ot >= OperandType.IndexedByInt:
            kw.update(variable=self._read_variable_spec(reader.u32(), void))
            index = reader.u32()
            if ot is OperandType.IndexedByVar:
                index = self._read_variable_spec(index, void)
            kw.update(index=index)
        return Operand(ot, **kw)

    def _parse_bytecode(self, data: memoryview, void: bool) -> Generator[Instruction, None, None]:
        disassembly: Dict[int, Instruction] = OrderedDict()
        reader = StructReader(data)

        argcount = {
            Op.Assign: 2,
            Op.CallVar: 1,
            Op.Dec: 1,
            Op.Inc: 1,
            Op.BooleanNot: 1,
            Op.Neg: 1,
            Op.IntegerNot: 1,
            Op.SetPtrToCopy: 2,
            Op.SetPtr: 2,
        }

        while not reader.eof:
            def arg(k=1):
                for _ in range(k):
                    args.append(self._read_operand(reader, void))
            addr = reader.tell()
            cval = reader.u8()
            code = Op.FromInt(cval)
            insn = Instruction(addr, code)
            args = insn.operands
            disassembly[insn.offset] = insn
            aryness = argcount.get(code)
            if aryness is not None:
                arg(aryness)
            elif code in (Op.Ret, Op.Nop, Op.Pop):
                pass
            elif code is Op.Calculate:
                insn.operator = AOp(reader.u8())
                arg(2)
            elif code in (Op.Push, Op.PushVar):
                arg()
            elif code in (Op.Jump, Op.JumpFlag):
                target = reader.i32()
                args.append(reader.tell() + target)
            elif code is Op.Call:
                args.append(reader.u32())
            elif code in (Op.JumpTrue, Op.JumpFalse):
                target = reader.i32()
                val = self._read_operand(reader, void)
                args.append(reader.tell() + target)
                args.append(val)
            elif code is Op.JumpPop1:
                target = reader.i32()
                args.append(reader.tell() + target)
            elif code is Op.JumpPop2:
                target = reader.i32()
                args.append(reader.tell() + target)
            elif code is Op.StackType:
                args.append(self._read_variable_spec(reader.u32(), void))
                args.append(reader.u32())
            elif code is Op.PushType:
                args.append(self.types[reader.u32()])
            elif code is Op.Compare:
                insn.operator = COp(reader.u8())
                arg(3)
            elif code is Op.SetFlag:
                arg()
                args.append(bool(reader.u8()))
            elif code is Op.PushEH:
                args.extend(reader.i32() for _ in range(4))
                for k, a in enumerate(args):
                    args[k] = a + reader.tell() if a >= 0 else None
            elif code is Op.PopEH:
                args.append(reader.u8())
            elif code is Op._INVALID:
                raise ValueError(F'Unsupported opcode: 0x{cval:02X}')
            else:
                raise ValueError(F'Unhandled opcode: {code.name}')
            insn.size = reader.tell() - addr

        for k, instruction in enumerate(disassembly.values()):
            if not instruction.branches:
                continue
            target = instruction.operands[0]
            try:
                disassembly[target].jumptarget = True
            except KeyError as K:
                raise RuntimeError(
                    F'The jump target of instruction {k} at 0x{instruction.offset:X} is invalid; '
                    F'the invalid instruction is a {instruction.opcode.name} to 0x{target:X}.'
                ) from K

        yield from disassembly.values()

    def __str__(self):
        return self.disassembly()

    def disassembly(self) -> str:
        def sortkey(f: Function):
            d = (d.module or '', d.classname or '', d.void) if (d := f.decl) else ('', '', True)
            return (*d, f.name)

        for function in self.functions:
            function.get_basic_blocks()

        classes: dict[str, dict[str, Function]] = {}
        external: list[Function] = []
        internal: list[Function] = []

        for t in self.types:
            if isinstance(t, TClass):
                classes[t.name] = {}

        for function in self.functions:
            if (decl := function.decl) and (name := decl.classname):
                try:
                    members = classes[name]
                except KeyError:
                    members = classes[name] = {}
                members[decl.name] = function
                continue
            dl = internal if function.body else external
            dl.append(function)

        external.sort(key=sortkey)

        output = io.StringIO()
        _omax = max((
            max(insn.offset for insn in fn.body)
            for fn in self.functions if fn.body
        ), default=0)
        _smax = max((
            max(insn.stack for insn in fn.body if insn.stack is not None)
            for fn in self.functions if fn.body
        ), default=0)
        _omax = max(len(self.types), len(self.globals), _omax)
        _omax = len(F'{_omax:X}')
        _smax = len(F'{_smax:d}')

        if classes:
            for name, members in classes.items():
                if not members:
                    output.write(F'external class {name};\n')
            output.write('\n')
            for name, members in classes.items():
                if not members:
                    continue
                output.write(F'external class {name}')
                if members:
                    for spec in members.values():
                        if spec.decl.is_accessor:
                            continue
                        output.write(F'\n{_TAB}{spec.decl.represent(spec.symbol, rel=True)}')
                    output.write('\nend')
                output.write(';\n\n')

        if self.types:
            typedefs = []
            for type in self.types:
                if type.code != TC.Record and type.symbol in (type.code.name, None):
                    continue
                if isinstance(type, TClass):
                    continue
                typedefs.append((type.symbol, type.display()))
            typedefs.sort()
            for symbol, display in typedefs:
                output.write(F'typedef {symbol} = {display}\n')
            output.write('\n')

        if self.globals:
            for variable in self.globals:
                output.write(F'global {variable!s}\n')
            output.write('\n')

        if external:
            for function in external:
                output.write(F'external {function!r}\n')
            output.write('\n')

        if internal:
            for function in internal:
                output.write(F'{function!r}\nbegin\n')
                labels = [insn.offset for insn in function.body if insn.jumptarget]
                labelw = max(len(str(len(labels))), 2)
                labeld = {v: F'JumpDestination{k:0{labelw}d}' for k, v in enumerate(labels, 1)}
                labelc = 0
                for instruction in function.body:
                    stack = instruction.stack
                    stack = '?' * _smax if stack is None else F'{stack:>{_smax}d}'
                    if instruction.jumptarget:
                        output.write(F'{labeld[labels[labelc]]}:\n')
                        labelc += 1
                    output.write(F'{_TAB}0x{instruction.offset:0{_omax}X}{_TAB}{stack}{_TAB}{instruction.pretty(labeld)}\n')
                output.write('end;\n\n')

        return output.getvalue().strip()

Ancestors

Class variables

var MinVer
var MaxVer
var Magic

Methods

def disassembly(self)
Expand source code Browse git
def disassembly(self) -> str:
    def sortkey(f: Function):
        d = (d.module or '', d.classname or '', d.void) if (d := f.decl) else ('', '', True)
        return (*d, f.name)

    for function in self.functions:
        function.get_basic_blocks()

    classes: dict[str, dict[str, Function]] = {}
    external: list[Function] = []
    internal: list[Function] = []

    for t in self.types:
        if isinstance(t, TClass):
            classes[t.name] = {}

    for function in self.functions:
        if (decl := function.decl) and (name := decl.classname):
            try:
                members = classes[name]
            except KeyError:
                members = classes[name] = {}
            members[decl.name] = function
            continue
        dl = internal if function.body else external
        dl.append(function)

    external.sort(key=sortkey)

    output = io.StringIO()
    _omax = max((
        max(insn.offset for insn in fn.body)
        for fn in self.functions if fn.body
    ), default=0)
    _smax = max((
        max(insn.stack for insn in fn.body if insn.stack is not None)
        for fn in self.functions if fn.body
    ), default=0)
    _omax = max(len(self.types), len(self.globals), _omax)
    _omax = len(F'{_omax:X}')
    _smax = len(F'{_smax:d}')

    if classes:
        for name, members in classes.items():
            if not members:
                output.write(F'external class {name};\n')
        output.write('\n')
        for name, members in classes.items():
            if not members:
                continue
            output.write(F'external class {name}')
            if members:
                for spec in members.values():
                    if spec.decl.is_accessor:
                        continue
                    output.write(F'\n{_TAB}{spec.decl.represent(spec.symbol, rel=True)}')
                output.write('\nend')
            output.write(';\n\n')

    if self.types:
        typedefs = []
        for type in self.types:
            if type.code != TC.Record and type.symbol in (type.code.name, None):
                continue
            if isinstance(type, TClass):
                continue
            typedefs.append((type.symbol, type.display()))
        typedefs.sort()
        for symbol, display in typedefs:
            output.write(F'typedef {symbol} = {display}\n')
        output.write('\n')

    if self.globals:
        for variable in self.globals:
            output.write(F'global {variable!s}\n')
        output.write('\n')

    if external:
        for function in external:
            output.write(F'external {function!r}\n')
        output.write('\n')

    if internal:
        for function in internal:
            output.write(F'{function!r}\nbegin\n')
            labels = [insn.offset for insn in function.body if insn.jumptarget]
            labelw = max(len(str(len(labels))), 2)
            labeld = {v: F'JumpDestination{k:0{labelw}d}' for k, v in enumerate(labels, 1)}
            labelc = 0
            for instruction in function.body:
                stack = instruction.stack
                stack = '?' * _smax if stack is None else F'{stack:>{_smax}d}'
                if instruction.jumptarget:
                    output.write(F'{labeld[labels[labelc]]}:\n')
                    labelc += 1
                output.write(F'{_TAB}0x{instruction.offset:0{_omax}X}{_TAB}{stack}{_TAB}{instruction.pretty(labeld)}\n')
            output.write('end;\n\n')

    return output.getvalue().strip()