Module refinery.lib.scripts.xlm.deobfuscation.fold

The constant folding of the macrosheet model: every subtree of a formula that computes from stored state alone — literals, the operators over them, references to cells whose content no run changes, and the calls of the pure commands — is replaced by the value it computes, so the listing shows the strings the program assembles rather than the chains that assemble them.

A run changes the macrosheet cells its writes reach. The fold takes a census of the writes the program can make — in the formulas of the macrosheets and of the defined names, and in the formulas their string literals spell — and settles the cells they reach before it rewrites anything; a write whose cells the fold cannot compute, or whose entered formula it cannot tell, may reach every macrosheet cell. Every other cell keeps the content the workbook stores — no run writes a worksheet — and a formula cell among them reads as the value its formula computes. A defined name and a call of a command that is not pure compute nothing the fold can know. Every subtree the fold cannot finish computing stays as it is, rather than a guess at what the program would have run.

Expand source code Browse git
"""
The constant folding of the macrosheet model: every subtree of a formula that computes from
stored state alone — literals, the operators over them, references to cells whose content no
run changes, and the calls of the pure commands — is replaced by the value it computes, so the
listing shows the strings the program assembles rather than the chains that assemble them.

A run changes the macrosheet cells its writes reach. The fold takes a census of the writes the
program can make — in the formulas of the macrosheets and of the defined names, and in the
formulas their string literals spell — and settles the cells they reach before it rewrites
anything; a write whose cells the fold cannot compute, or whose entered formula it cannot tell,
may reach every macrosheet cell. Every other cell keeps the content the workbook stores — no run
writes a worksheet — and a formula cell among them reads as the value its formula computes. A
defined name and a call of a command that is not pure compute nothing the fold can know. Every
subtree the fold cannot finish computing stays as it is, rather than a guess at what the program
would have run.
"""
from __future__ import annotations

import dataclasses
import math

from typing import TYPE_CHECKING, NamedTuple, TypeGuard

from refinery.lib.excel import parse_formula
from refinery.lib.excel.formula.model import (
    Expression,
    XlA1Reference,
    XlArrayConstant,
    XlBinaryExpression,
    XlBinaryOperator,
    XlBoolean,
    XlDefinedName,
    XlError,
    XlFunctionCall,
    XlMissingArgument,
    XlNumber,
    XlParenExpression,
    XlR1C1Reference,
    XlString,
    XlUnaryExpression,
    XlUnparsedFormula,
)
from refinery.lib.scripts import TREE_RECURSION_DEPTH, set_child, set_child_list
from refinery.lib.scripts.xlm.commands import CELL_WRITERS, PURE_COMMANDS, XlmCellWrite
from refinery.lib.scripts.xlm.deobfuscation.program import program_nodes
from refinery.lib.scripts.xlm.engine import XlmEngine, callee_name
from refinery.lib.scripts.xlm.evaluate import evaluate_expression
from refinery.lib.scripts.xlm.model import XlmCell
from refinery.lib.scripts.xlm.references import XlmCursor, resolve_reference
from refinery.lib.scripts.xlm.trace import XlmStatus
from refinery.lib.scripts.xlm.values import XlmOutcome, XlmReference, XlmValue
from refinery.lib.tools import RecursionDepth

if TYPE_CHECKING:
    from refinery.lib.scripts.xlm.view import XlmView

_REFERENCES = (XlA1Reference, XlR1C1Reference)

_LITERALS = (XlNumber, XlString, XlBoolean, XlError, XlMissingArgument)

_LEAVES = (*_LITERALS, *_REFERENCES, XlArrayConstant, XlUnparsedFormula)

#: The cursor of a formula that runs in a cell the fold does not know: a formula a string
#: literal spells, or the formula of a defined name.
_NOWHERE = XlmCursor('', 0, 0)

_Key = tuple[str, int, int]


class _Unknown(Exception):
    """
    Raised when an evaluation of the fold depends on anything other than the state the workbook
    stores: a cell a run may change, a defined name, or a command that is not pure or fails.
    """


class _Area(NamedTuple):
    """
    A rectangle of cells on one macrosheet, by the lowercase name of the sheet.
    """

    sheet: str
    top: int
    left: int
    bottom: int
    right: int

    def covers(self, sheet: str, row: int, col: int) -> bool:
        return (
            sheet == self.sheet
            and self.top <= row <= self.bottom
            and self.left <= col <= self.right
        )


class _Reach:
    """
    The macrosheet cells the writes of a program may reach: single cells, rectangles of cells,
    and whether some write may reach any cell of any macrosheet.
    """

    def __init__(self):
        self.cells: set[_Key] = set()
        self.areas: set[_Area] = set()
        self.anywhere = False

    def add(self, sheet: str, first: XlmReference, last: XlmReference) -> None:
        """
        Add the rectangle two corners span on the sheet of the given name.
        """
        sheet = sheet.lower()
        top, bottom = sorted((first.row, last.row))
        left, right = sorted((first.col, last.col))
        if top == bottom and left == right:
            self.cells.add((sheet, top, left))
        else:
            self.areas.add(_Area(sheet, top, left, bottom, right))

    def covers(self, sheet: str, row: int, col: int) -> bool:
        """
        Whether a write may reach the cell at the given position of the sheet of the given
        lowercase name.
        """
        if self.anywhere or (sheet, row, col) in self.cells:
            return True
        return any(area.covers(sheet, row, col) for area in self.areas)

    def merge(self, other: _Reach) -> bool:
        """
        Add the cells another reach covers, and report whether this one grew.
        """
        grown = (
            other.anywhere > self.anywhere
            or not other.cells <= self.cells
            or not other.areas <= self.areas
        )
        self.anywhere |= other.anywhere
        self.cells |= other.cells
        self.areas |= other.areas
        return grown


class _Formula(NamedTuple):
    """
    The formula of a macrosheet cell, with the cell and the cursor it runs at.
    """

    cursor: XlmCursor
    cell: XlmCell
    formula: Expression


class _Writer(NamedTuple):
    """
    One write of the program: the call that makes it, the cursor of the cell whose formula
    holds it, and whether a string literal or a defined name spells it, which leaves the cell
    it runs in unknown.
    """

    call: XlFunctionCall
    cursor: XlmCursor
    spelled: bool


def _is_pure(call: XlFunctionCall) -> bool:
    """
    Whether a call computes from its arguments and stored cells alone: a pure command called by
    its own name, which neither a defined name nor a registered alias can stand for.
    """
    return isinstance(call.callee, str) and callee_name(call) in PURE_COMMANDS


class _StaticEngine(XlmEngine):
    """
    The engine the fold evaluates with, which computes what every run computes from the state
    the workbook stores. A read of a cell a write may reach, a defined name, and a call that is
    not pure or fails end the evaluation; any other formula cell reads as the value its formula
    computes. The engine keeps what every cell and every call computes for every later read
    until the reach of the writes grows.
    """

    def __init__(self, view: XlmView):
        super().__init__(view)
        self.reach = _Reach()
        self._cells: dict[_Key, XlmCell] = {}
        for macrosheet in view.macrosheets():
            sheet = macrosheet.name.lower()
            for cell in macrosheet.body:
                if isinstance(cell, XlmCell):
                    self._cells[sheet, cell.row, cell.col] = cell
        self._macrosheets = {macrosheet.name.lower() for macrosheet in view.macrosheets()}
        self._values: dict[_Key, XlmValue | None] = {}
        self._outcomes: dict[tuple[XlFunctionCall, XlmCursor, bool], XlmOutcome | None] = {}
        self._open: set[_Key] = set()

    def widen(self, reach: _Reach) -> bool:
        """
        Add the cells another reach covers to the cells the writes may reach, and report whether
        the reach grew; a grown reach forgets what the cells and calls computed, because the
        cells it gained may have fed them.
        """
        if not self.reach.merge(reach):
            return False
        self._values.clear()
        self._outcomes.clear()
        return True

    def keeps(self, reference: XlmReference, cursor: XlmCursor) -> bool:
        """
        Whether every run keeps the content the workbook stores in a cell: any cell but the
        macrosheet cells a write may reach.
        """
        sheet = (reference.sheet or cursor.sheet).lower()
        if sheet not in self._macrosheets:
            return True
        return not self.reach.covers(sheet, reference.row, reference.col)

    def read_reference(self, reference: XlmReference, cursor: XlmCursor) -> XlmValue:
        cell = XlmCursor(reference.sheet or cursor.sheet, reference.row, reference.col)
        key = (cell.sheet.lower(), cell.row, cell.col)
        try:
            value = self._values[key]
        except KeyError:
            value = self._values[key] = self._compute(cell, key)
        if value is None:
            raise _Unknown
        return dataclasses.replace(value, reference=XlmReference(*cell))

    def resolve_name(self, node: XlDefinedName, cursor: XlmCursor) -> XlmValue:
        raise _Unknown

    def node_reference(self, node, cursor: XlmCursor) -> XlmReference | None:
        if isinstance(node, XlDefinedName):
            raise _Unknown
        return super().node_reference(node, cursor)

    def call(self, call: XlFunctionCall, cursor: XlmCursor, nested: bool = False) -> XlmOutcome:
        if not _is_pure(call):
            raise _Unknown
        key = (call, cursor, nested)
        try:
            outcome = self._outcomes[key]
        except KeyError:
            try:
                outcome = self.dispatch(callee_name(call), call, cursor, nested)
            except _Unknown:
                outcome = None
            if outcome is not None and outcome.status is XlmStatus.Error:
                outcome = None
            self._outcomes[key] = outcome
        if outcome is None:
            raise _Unknown
        return outcome

    def _compute(self, cursor: XlmCursor, key: _Key) -> XlmValue | None:
        """
        The value the cell at a cursor holds through every run, or `None` when a run may change
        it. A cell whose formula reads itself again before it computes anything holds no such
        value.
        """
        if key[0] in self._macrosheets:
            if self.reach.covers(*key):
                return None
            cell = self._cells.get(key)
        else:
            worksheet = self.view.worksheet(cursor.sheet)
            cell = None if worksheet is None else worksheet.get((cursor.row, cursor.col))
        if cell is None or cell.formula is None:
            return super().read_reference(XlmReference(*cursor), cursor)
        if key in self._open:
            raise _Unknown
        self._open.add(key)
        try:
            return evaluate_expression(self, cell.formula, cursor)
        except _Unknown:
            return None
        finally:
            self._open.discard(key)


def fold(view: XlmView) -> None:
    """
    Rewrite every formula of every macrosheet of the view with its statically computable
    subtrees replaced by the values they compute.
    """
    with RecursionDepth(TREE_RECURSION_DEPTH):
        engine = _StaticEngine(view)
        parsed: dict[str, Expression] = {}
        formulas = [
            _Formula(XlmCursor(macrosheet.name, cell.row, cell.col), cell, cell.formula)
            for macrosheet in view.macrosheets()
            for cell in macrosheet.body
            if isinstance(cell, XlmCell) and cell.formula is not None
        ]
        writers = _census(view, formulas, parsed)
        while engine.widen(_reach(engine, writers, parsed)):
            continue
        for cursor, cell, formula in formulas:
            candidates = _candidates(engine, formula, cursor)
            try:
                folded = _fold(engine, formula, cursor, candidates)
            except RecursionError:
                continue
            if folded is not formula:
                set_child(cell, 'formula', folded)


def _writes(node) -> TypeGuard[XlFunctionCall]:
    """
    Whether a node is a call of a command that writes cells.
    """
    return isinstance(node, XlFunctionCall) and callee_name(node) in CELL_WRITERS


def _census(
    view: XlmView,
    formulas: list[_Formula],
    parsed: dict[str, Expression],
) -> list[_Writer]:
    """
    Every write the program can make: the calls of the commands that write cells in the
    formulas of the macrosheet cells and of the defined names, and in the formulas their string
    literals spell.
    """
    writers: list[_Writer] = []
    for cursor, _, formula in formulas:
        for node, spelled in program_nodes(formula, parsed):
            if _writes(node):
                writers.append(_Writer(node, cursor, spelled))
    for entry in view.names.entries():
        for node, _ in program_nodes(entry.formula, parsed):
            if _writes(node):
                writers.append(_Writer(node, _NOWHERE, True))
    return writers


def _parsed(text: str, parsed: dict[str, Expression]) -> Expression:
    tree = parsed.get(text)
    if tree is None:
        tree = parsed[text] = parse_formula(text)
    return tree


def _reach(engine: _StaticEngine, writers: list[_Writer], parsed: dict[str, Expression]) -> _Reach:
    """
    The cells the writes of the program reach as far as the engine computes them: the cells
    every write names, and the cells named by the writes of every formula a write enters. A
    write that a string literal or a defined name spells and that names no sheet may run on
    any macrosheet.
    """
    reach = _Reach()
    pending = list(writers)
    while pending:
        writer = pending.pop()
        write = CELL_WRITERS[callee_name(writer.call)]
        corners = _destination(engine, writer, write, parsed)
        installed = _installed(engine, writer, write, parsed) if write.installs else []
        if corners is None or installed is None:
            reach.anywhere = True
            return reach
        first, last = corners
        if first.sheet is not None:
            reach.add(first.sheet, first, last)
        else:
            for sheet in engine.view.macrosheets():
                reach.add(sheet.name, first, last)
        pending.extend(installed)
    return reach


def _destination(
    engine: _StaticEngine,
    writer: _Writer,
    write: XlmCellWrite,
    parsed: dict[str, Expression],
) -> tuple[XlmReference, XlmReference] | None:
    """
    The corners of the cells a write names, or `None` when the fold cannot tell which cells a
    run writes. A write in the formula of a macrosheet cell names its cells as the run resolves
    them; a spelled write can only name them by an address that does not depend on the cell it
    runs in.
    """
    arguments = writer.call.arguments
    if write.destination >= len(arguments):
        return None
    node = arguments[write.destination]
    if isinstance(node, XlMissingArgument):
        return None
    if writer.spelled:
        return _spelled_corners(node, parsed)
    try:
        corners = engine.range_corners(node, writer.cursor)
        if corners is not None:
            return corners
        reference = engine.node_reference(node, writer.cursor)
    except Exception:
        return None
    if reference is None:
        return None
    return reference, reference


def _spelled_corners(
    node,
    parsed: dict[str, Expression],
) -> tuple[XlmReference, XlmReference] | None:
    while isinstance(node, XlParenExpression):
        node = node.operand
    if isinstance(node, XlString):
        node = _parsed(node.value, parsed)
    if isinstance(node, _REFERENCES):
        reference = _absolute(node)
        return None if reference is None else (reference, reference)
    if (
        isinstance(node, XlBinaryExpression)
        and node.operator is XlBinaryOperator.RANGE
        and isinstance(node.left, _REFERENCES)
        and isinstance(node.right, _REFERENCES)
    ):
        first = _absolute(node.left)
        last = _absolute(node.right)
        if first is None or last is None:
            return None
        return first, last
    return None


def _absolute(node: XlA1Reference | XlR1C1Reference) -> XlmReference | None:
    """
    The address a reference names independently of the cell its formula sits in, or `None` for
    an R1C1 reference that counts from that cell.
    """
    if isinstance(node, XlR1C1Reference) and (node.relative_row or node.relative_col):
        return None
    return resolve_reference(node, _NOWHERE)


def _installed(
    engine: _StaticEngine,
    writer: _Writer,
    write: XlmCellWrite,
    parsed: dict[str, Expression],
) -> list[_Writer] | None:
    """
    The writes of the formula an installing write enters, or `None` when the fold cannot tell
    what it enters. A literal enters no formula the census has not seen already: a string
    literal is program text the census reads.
    """
    arguments = writer.call.arguments
    if write.source >= len(arguments):
        return []
    source = arguments[write.source]
    if isinstance(source, (*_LITERALS, XlArrayConstant)):
        return []
    if writer.spelled:
        return None
    value = _value_of(engine, source, writer.cursor)
    if value is None or value.partial:
        return None
    if not isinstance(value.value, str) or not value.value.startswith('='):
        return []
    return [
        _Writer(node, writer.cursor, True)
        for node, _ in program_nodes(_parsed(value.value, parsed), parsed)
        if _writes(node)
    ]


def _candidates(engine: _StaticEngine, formula: Expression, cursor: XlmCursor) -> set[int]:
    """
    The identities of the nodes of a formula whose subtrees compute from stored state alone as
    far as their parts tell: literals, references to cells every run keeps, the operators over
    such subtrees that read values those cells compute — a range reads no cell by naming its
    corners — and the calls of pure commands over such subtrees that compute a value. The nodes
    are visited in reverse preorder, which visits every node after its descendants, so that no
    operator over a part that computes nothing is evaluated, and the engine keeps what every
    call computes for the evaluation of the subtrees around it.
    """
    candidates: set[int] = set()

    def computes(operand) -> bool:
        if id(operand) not in candidates:
            return False
        if isinstance(operand, _REFERENCES):
            return _value_of(engine, operand, cursor) is not None
        return True

    for node in reversed(list(formula.walk())):
        if isinstance(node, (*_LITERALS, XlArrayConstant)):
            candidate = True
        elif isinstance(node, (XlParenExpression, XlUnaryExpression)):
            candidate = computes(node.operand)
        elif isinstance(node, XlBinaryExpression) and node.operator is XlBinaryOperator.RANGE:
            candidate = all(
                isinstance(side, _REFERENCES) or id(side) in candidates
                for side in (node.left, node.right)
            )
        elif isinstance(node, XlBinaryExpression):
            candidate = computes(node.left) and computes(node.right)
        elif isinstance(node, _REFERENCES):
            candidate = engine.keeps(resolve_reference(node, cursor), cursor)
        elif isinstance(node, XlFunctionCall):
            candidate = (
                _is_pure(node)
                and all(id(argument) in candidates for argument in node.arguments)
                and _value_of(engine, node, cursor) is not None
            )
        else:
            candidate = False
        if candidate:
            candidates.add(id(node))
    return candidates


def _fold(engine: _StaticEngine, node, cursor: XlmCursor, candidates: set[int]) -> Expression:
    """
    The node with every subtree below it that computes statically replaced by the literal it
    computes. A reference stays as it is, because a command may read it as an address.
    """
    if isinstance(node, _LEAVES):
        return node
    if id(node) in candidates:
        value = _value_of(engine, node, cursor)
        literal = None if value is None else _literal(value)
        if literal is not None:
            return literal
    if isinstance(node, (XlParenExpression, XlUnaryExpression)):
        _install(node, 'operand', _fold(engine, node.operand, cursor, candidates))
    elif isinstance(node, XlBinaryExpression):
        _install(node, 'left', _fold(engine, node.left, cursor, candidates))
        _install(node, 'right', _fold(engine, node.right, cursor, candidates))
    elif isinstance(node, XlFunctionCall):
        arguments = [_fold(engine, argument, cursor, candidates) for argument in node.arguments]
        if any(folded is not argument for folded, argument in zip(arguments, node.arguments)):
            set_child_list(node, 'arguments', arguments)
    return node


def _install(node, attribute: str, child: Expression) -> None:
    """
    Put the folded form of a child of a node in its place, unless the fold left it as it was.
    """
    if child is not getattr(node, attribute):
        set_child(node, attribute, child)


def _value_of(engine: _StaticEngine, node, cursor: XlmCursor) -> XlmValue | None:
    """
    The value a subtree computes from stored state alone, or `None` when it depends on anything
    else or fails to evaluate.
    """
    try:
        return evaluate_expression(engine, node, cursor)
    except Exception:
        return None


def _literal(value: XlmValue) -> Expression | None:
    """
    The literal that spells a value as the type it has: an error value, a truth value, a finite
    number, or a text — and a text that spells a number stays a text. A value the program never
    finished, an address, a range, and an array have no literal.
    """
    if value.partial or value.reference is not None or value.cells is not None:
        return None
    if value.error:
        return XlError(value=str(value.value))
    data = value.value
    if isinstance(data, bool):
        return XlBoolean(value=data)
    if isinstance(data, int):
        return XlNumber(value=data)
    if isinstance(data, float):
        if not math.isfinite(data):
            return None
        return XlNumber(value=int(data) if data.is_integer() else data)
    if isinstance(data, str):
        return XlString(value=data)
    return None

Functions

def fold(view)

Rewrite every formula of every macrosheet of the view with its statically computable subtrees replaced by the values they compute.

Expand source code Browse git
def fold(view: XlmView) -> None:
    """
    Rewrite every formula of every macrosheet of the view with its statically computable
    subtrees replaced by the values they compute.
    """
    with RecursionDepth(TREE_RECURSION_DEPTH):
        engine = _StaticEngine(view)
        parsed: dict[str, Expression] = {}
        formulas = [
            _Formula(XlmCursor(macrosheet.name, cell.row, cell.col), cell, cell.formula)
            for macrosheet in view.macrosheets()
            for cell in macrosheet.body
            if isinstance(cell, XlmCell) and cell.formula is not None
        ]
        writers = _census(view, formulas, parsed)
        while engine.widen(_reach(engine, writers, parsed)):
            continue
        for cursor, cell, formula in formulas:
            candidates = _candidates(engine, formula, cursor)
            try:
                folded = _fold(engine, formula, cursor, candidates)
            except RecursionError:
                continue
            if folded is not formula:
                set_child(cell, 'formula', folded)