Module refinery.lib.scripts.au3.deobfuscation.fold

Constant folding: replacing an expression whose value every ingredient pins with the literal that spells it.

Evaluation is the constant evaluator refinery.lib.scripts.au3.analysis.constants, asked unbounded — what the source already spells is already in the tree — so a fold over string literals of any size stands. A call folds only when no script function shares the callee's name, because a script function shadows the builtin; both refusals leave the expression standing. The render gates own the spelling: a value whose literal runs deeper than the renderer's bound leaves the expression standing too, because no synthesizer prints such a chain.

And/Or fold to their boolean value once both operands' truth is known, and to the decided side alone when the left operand already decides — AutoIt never runs the right side there (probe4 60, 61), so a call on the skipped side is dropped soundly. A ternary over a literal condition folds to its chosen branch, the only side AutoIt evaluates (probe4 66, 67).

A fold that lands on an array value installs only where a declaration initializes, because an array literal is legal AutoIt nowhere else — a StringSplit over constants in any other position stands.

A replacement is installed only when it spells differently from the original: a fold that rewrote -5 into -5, or Binary("0x41") into itself, would report a change the next pass reports again, and the pipeline would never settle.

Expand source code Browse git
"""
Constant folding: replacing an expression whose value every ingredient pins with the literal that
spells it.

Evaluation is the constant evaluator `refinery.lib.scripts.au3.analysis.constants`, asked
unbounded — what the source already spells is already in the tree — so a fold over string
literals of any size stands. A call folds only when no script function shares the callee's name,
because a script function shadows the builtin; both refusals leave the expression standing. The
render gates own the spelling: a value whose literal runs deeper than the renderer's bound
leaves the expression standing too, because no synthesizer prints such a chain.

`And`/`Or` fold to their boolean value once both operands' truth is known, and to the decided
side alone when the left operand already decides — AutoIt never runs the right side there (probe4
60, 61), so a call on the skipped side is dropped soundly. A ternary over a literal condition
folds to its chosen branch, the only side AutoIt evaluates (probe4 66, 67).

A fold that lands on an array value installs only where a declaration initializes, because an
array literal is legal AutoIt nowhere else — a `StringSplit` over constants in any other
position stands.

A replacement is installed only when it spells differently from the original: a fold that
rewrote `-5` into `-5`, or `Binary("0x41")` into itself, would report a change the next pass
reports again, and the pipeline would never settle.
"""
from __future__ import annotations

from refinery.lib.scripts import Node, Transformer
from refinery.lib.scripts.au3 import ast
from refinery.lib.scripts.au3.analysis.cache import model_cache
from refinery.lib.scripts.au3.analysis.constants import Policy, constant_value
from refinery.lib.scripts.au3.analysis.values import (
    literal_value,
    render_at,
    truthy,
)
from refinery.lib.scripts.au3.model import (
    Au3BinaryExpression,
    Au3Call,
    Au3Macro,
    Au3Script,
    Au3Ternary,
    Au3UnaryExpression,
)
from refinery.lib.scripts.au3.synth import Au3Synthesizer


class Au3ConstantFolding(Transformer):
    """
    Replaces a macro, unary, binary, or call expression whose operands are known constants with
    the literal of its value, bottom-up in one pass.
    """

    def __init__(self):
        super().__init__()
        self._policy = Policy(frozenset())
        self._synthesizer = Au3Synthesizer()

    def visit(self, node: Node) -> Node | None:
        if isinstance(node, Au3Script):
            self._policy = Policy(model_cache(self, node).scopes.function_names)
        self.generic_visit(node)
        replacement = self._folded(node)
        if replacement is None:
            return None
        if self._synthesizer.convert(replacement) == self._synthesizer.convert(node):
            return None
        return replacement

    def _folded(self, node: Node) -> Node | None:
        if isinstance(node, Au3Ternary):
            return self._ternary_branch(node)
        if not isinstance(node, (Au3Macro, Au3UnaryExpression, Au3BinaryExpression, Au3Call)):
            return None
        value = constant_value(node, self._policy)
        if value is None:
            return None
        return render_at(node, value, self._policy.functions)

    def _ternary_branch(self, node: Au3Ternary) -> Node | None:
        """
        The branch a ternary over a literal condition takes, the only side AutoIt evaluates
        (probe4 66, 67). A branch holding an array literal spells only where a declaration
        initializes, so the fold refuses anywhere else.
        """
        condition = literal_value(node.condition, self._policy.functions)
        if condition is None:
            return None
        verdict = truthy(condition)
        if verdict is None:
            return None
        branch = node.consequent if verdict else node.alternate
        if branch is None:
            return None
        if ast.contains_an_array_literal(branch) and not ast.in_declaration_initializer(node):
            return None
        return branch

Classes

class Au3ConstantFolding

Replaces a macro, unary, binary, or call expression whose operands are known constants with the literal of its value, bottom-up in one pass.

Expand source code Browse git
class Au3ConstantFolding(Transformer):
    """
    Replaces a macro, unary, binary, or call expression whose operands are known constants with
    the literal of its value, bottom-up in one pass.
    """

    def __init__(self):
        super().__init__()
        self._policy = Policy(frozenset())
        self._synthesizer = Au3Synthesizer()

    def visit(self, node: Node) -> Node | None:
        if isinstance(node, Au3Script):
            self._policy = Policy(model_cache(self, node).scopes.function_names)
        self.generic_visit(node)
        replacement = self._folded(node)
        if replacement is None:
            return None
        if self._synthesizer.convert(replacement) == self._synthesizer.convert(node):
            return None
        return replacement

    def _folded(self, node: Node) -> Node | None:
        if isinstance(node, Au3Ternary):
            return self._ternary_branch(node)
        if not isinstance(node, (Au3Macro, Au3UnaryExpression, Au3BinaryExpression, Au3Call)):
            return None
        value = constant_value(node, self._policy)
        if value is None:
            return None
        return render_at(node, value, self._policy.functions)

    def _ternary_branch(self, node: Au3Ternary) -> Node | None:
        """
        The branch a ternary over a literal condition takes, the only side AutoIt evaluates
        (probe4 66, 67). A branch holding an array literal spells only where a declaration
        initializes, so the fold refuses anywhere else.
        """
        condition = literal_value(node.condition, self._policy.functions)
        if condition is None:
            return None
        verdict = truthy(condition)
        if verdict is None:
            return None
        branch = node.consequent if verdict else node.alternate
        if branch is None:
            return None
        if ast.contains_an_array_literal(branch) and not ast.in_declaration_initializer(node):
            return None
        return branch

Ancestors

Methods

def visit(self, node)
Expand source code Browse git
def visit(self, node: Node) -> Node | None:
    if isinstance(node, Au3Script):
        self._policy = Policy(model_cache(self, node).scopes.function_names)
    self.generic_visit(node)
    replacement = self._folded(node)
    if replacement is None:
        return None
    if self._synthesizer.convert(replacement) == self._synthesizer.convert(node):
        return None
    return replacement

Inherited members