Module refinery.lib.scripts.xlm.handlers.system

The system commands of the macro language: the external calls the program makes, the files it opens and writes, the environment answers its questions read, and the Kernel32 pseudo-commands that allocate and fill the memory of the emulated process.

Expand source code Browse git
"""
The system commands of the macro language: the external calls the program makes, the files it
opens and writes, the environment answers its questions read, and the Kernel32 pseudo-commands
that allocate and fill the memory of the emulated process.
"""
from __future__ import annotations

from typing import TYPE_CHECKING

from refinery.lib.excel import synthesize_formula
from refinery.lib.scripts.xlm.evaluate import evaluate_expression
from refinery.lib.scripts.xlm.trace import XlmStatus
from refinery.lib.scripts.xlm.values import (
    XlmOutcome,
    XlmValue,
    ansi_bytes,
    is_number,
    wrap_literal,
)

if TYPE_CHECKING:
    from refinery.lib.excel.formula.model import XlFunctionCall
    from refinery.lib.scripts.xlm.engine import XlmEngine
    from refinery.lib.scripts.xlm.references import XlmCursor

_DIRECTORY = R'C:\Users\user\Documents'


def _partial(spelled: str) -> XlmOutcome:
    return XlmOutcome(value=XlmValue(value=spelled, partial=True))


def _arguments(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> list[XlmValue]:
    """
    The values of the arguments a call spells, each at the position the call wrote it and a
    missing argument the empty value.
    """
    return [evaluate_expression(engine, node, cursor) for node in call.arguments]


def _call(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    arguments = _arguments(engine, call, cursor)
    partial = any(argument.partial for argument in arguments)
    return XlmOutcome(value=XlmValue(
        value=0,
        text=F'CALL({",".join(argument.text or "" for argument in arguments)})',
        partial=partial,
    ))


def _register(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    if len(call.arguments) < 4:
        return XlmOutcome(
            value=XlmValue(value=0, text=synthesize_formula(call)),
            status=XlmStatus.Error,
        )
    arguments = _arguments(engine, call, cursor)
    engine.register_alias(
        arguments[3].unwrap(),
        F'{arguments[0].unwrap()}.{arguments[1].unwrap()}',
    )
    return XlmOutcome(value=XlmValue(
        value=0,
        text=F'REGISTER({",".join(argument.text or "" for argument in arguments)})',
    ))


def _register_id(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    if len(call.arguments) < 3:
        return XlmOutcome(
            value=XlmValue(value=0, text=synthesize_formula(call)),
            status=XlmStatus.Error,
        )
    arguments = _arguments(engine, call, cursor)
    return XlmOutcome(value=XlmValue(
        value=F'{arguments[0].unwrap()}.{arguments[1].unwrap()}',
        text=F'REGISTER.ID({",".join(argument.text or "" for argument in arguments)})',
    ))


def _fopen(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    name = evaluate_expression(engine, call.arguments[0], cursor)
    access = '1'
    if len(call.arguments) > 1:
        access = str(evaluate_expression(engine, call.arguments[1], cursor).value)
    file_name = name.unwrap() if not name.partial else 'default_name'
    engine.open_file(file_name, access)
    return XlmOutcome(value=XlmValue(
        value=file_name,
        text=F'FOPEN({name.text},{access})',
        partial=name.partial,
    ))


def _fsize(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    spelled = synthesize_formula(call)
    name = evaluate_expression(engine, call.arguments[0], cursor)
    if name.partial:
        return _partial(spelled)
    file_name = name.unwrap()
    size = engine.files.size(file_name)
    if size is None:
        return XlmOutcome(value=XlmValue(
            value=0,
            text=F'FSIZE({wrap_literal(file_name)})',
            partial=True,
        ))
    return XlmOutcome(value=XlmValue(
        value=size,
        text=F'FSIZE({wrap_literal(file_name)})',
    ))


def _fwrite(
    engine: XlmEngine,
    call: XlFunctionCall,
    cursor: XlmCursor,
    line: str = '',
) -> XlmOutcome:
    name = evaluate_expression(engine, call.arguments[0], cursor)
    content = evaluate_expression(engine, call.arguments[1], cursor)
    file_name = str(name.value)
    if not file_name.strip() or is_number(file_name):
        file_name = engine.files.first()
    written = content.unwrap()
    took = engine.write_file(file_name, F'{written}{line}')
    return XlmOutcome(value=XlmValue(
        value=0,
        text=F'FWRITE({wrap_literal(file_name)},{wrap_literal(written)})',
        partial=not took,
    ))


def _fwriteln(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    return _fwrite(engine, call, cursor, '\r\n')


def _files(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    name = evaluate_expression(engine, call.arguments[0], cursor)
    directory = name.unwrap()
    return XlmOutcome(value=XlmValue(
        value=directory,
        text=F'FILES({wrap_literal(directory)})',
    ))


def _directory(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    return XlmOutcome(value=XlmValue(value=_DIRECTORY, text=_DIRECTORY))


def _error(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    return XlmOutcome(value=XlmValue(value=0, text=synthesize_formula(call)))


def _app_maximize(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    return XlmOutcome(value=XlmValue(value=True))


def _get_workspace(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    spelled = synthesize_formula(call)
    if len(call.arguments) != 1:
        return _partial(spelled)
    number = evaluate_expression(engine, call.arguments[0], cursor)
    if number.partial or not is_number(number.text):
        return _partial(spelled)
    answer = engine.environment.workspace(int(float(number.unwrap())))
    return XlmOutcome(value=XlmValue(
        value=answer,
        text=F'GET.WORKSPACE({number.text})',
    ))


def _get_window(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    spelled = synthesize_formula(call)
    if len(call.arguments) != 1:
        return XlmOutcome(value=XlmValue(value='', text=spelled), status=XlmStatus.Error)
    number = evaluate_expression(engine, call.arguments[0], cursor)
    if number.partial or not is_number(number.text):
        return XlmOutcome(value=XlmValue(
            value='',
            text=F'GET.WINDOW({number.text})',
            partial=True,
        ))
    index = int(float(number.unwrap()))
    answer = engine.environment.window_value(
        index,
        engine.view.workbook_name,
        cursor.sheet,
    )
    window = engine.environment.window(index)
    return XlmOutcome(value=XlmValue(
        value=answer,
        text=str(window) if window is not None else None,
    ))


def _get_document(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    spelled = synthesize_formula(call)
    number = evaluate_expression(engine, call.arguments[0], cursor)
    if number.partial or not is_number(number.value):
        return XlmOutcome(value=XlmValue(value='', text=spelled), status=XlmStatus.Error)
    index = int(float(number.value))
    answer = engine.environment.document(
        index,
        engine.view.workbook_name,
        cursor.sheet,
    )
    if answer is None:
        return _partial(spelled)
    return XlmOutcome(value=XlmValue(value=answer, text=answer))


def _get_cell(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    return _partial(synthesize_formula(call))


def _virtual_alloc(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    spelled = synthesize_formula(call)
    base = evaluate_expression(engine, call.arguments[0], cursor)
    size = evaluate_expression(engine, call.arguments[1], cursor)
    if (
        base.partial
        or size.partial
        or not is_number(base.value)
        or not is_number(size.value)
    ):
        return _partial(spelled)
    address = engine.allocate_memory(int(float(base.value)), int(float(size.value)))
    return XlmOutcome(value=XlmValue(value=address, text=spelled))


def _write_process_memory(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    spelled = synthesize_formula(call)
    if len(call.arguments) <= 4:
        return _partial(spelled)
    arguments = _arguments(engine, call, cursor)
    if any(argument.partial for argument in arguments):
        return _partial(spelled)
    base = int(float(arguments[1].value))
    data = ansi_bytes(str(arguments[2].value))
    size = int(float(arguments[3].value))
    if not engine.write_memory(base, data, size):
        return XlmOutcome(
            value=XlmValue(value=0, text=spelled),
            status=XlmStatus.Error,
        )
    return XlmOutcome(value=XlmValue(value=0, text=(
        F'Kernel32.WriteProcessMemory({arguments[0].text},{base},'
        F'"{data.hex()}",{size},{arguments[4].text})'
    )))


def _rtl_copy_memory(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
    spelled = synthesize_formula(call)
    if len(call.arguments) != 3:
        return _partial(spelled)
    destination = evaluate_expression(engine, call.arguments[0], cursor)
    source = evaluate_expression(engine, call.arguments[1], cursor)
    size = evaluate_expression(engine, call.arguments[2], cursor)
    if destination.partial or source.partial:
        return _partial(spelled)
    data = ansi_bytes(str(source.value))
    if not engine.write_memory(int(float(destination.value)), data, len(data)):
        return XlmOutcome(
            value=XlmValue(value=0, text=spelled),
            status=XlmStatus.Error,
        )
    return XlmOutcome(value=XlmValue(value=0, text=(
        F'Kernel32.RtlCopyMemory({destination.text},"{data.hex()}",{size.text})'
    )))


SYSTEM_HANDLERS = {
    'APP.MAXIMIZE': _app_maximize,
    'CALL': _call,
    'DIRECTORY': _directory,
    'ERROR': _error,
    'FILES': _files,
    'FOPEN': _fopen,
    'FSIZE': _fsize,
    'FWRITE': _fwrite,
    'FWRITELN': _fwriteln,
    'GET.CELL': _get_cell,
    'GET.DOCUMENT': _get_document,
    'GET.WINDOW': _get_window,
    'GET.WORKSPACE': _get_workspace,
    'Kernel32.RtlCopyMemory': _rtl_copy_memory,
    'Kernel32.VirtualAlloc': _virtual_alloc,
    'Kernel32.WriteProcessMemory': _write_process_memory,
    'REGISTER': _register,
    'REGISTER.ID': _register_id,
}