Module refinery.lib.excel.formula.xlsb

The MS-XLSB operand layouts for the shared RPN stack machine: one reader per token whose byte layout the XLSB formula grammar defines. The token identifiers are the same classless spellings the BIFF decoder masks its token bytes down to, but the operand widths differ: the XLSB grammar counts string characters with two bytes, grows the row of a reference to four bytes, resolves 3-D references through the externals section of the workbook part, and places array constants inside the token stream instead of the trailing pool BIFF keeps them in.

Expand source code Browse git
"""
The MS-XLSB operand layouts for the shared RPN stack machine: one reader per token whose byte
layout the XLSB formula grammar defines. The token identifiers are the same classless
spellings the BIFF decoder masks its token bytes down to, but the operand widths differ: the
XLSB grammar counts string characters with two bytes, grows the row of a reference to four
bytes, resolves 3-D references through the externals section of the workbook part, and places
array constants inside the token stream instead of the trailing pool BIFF keeps them in.
"""
from __future__ import annotations

from refinery.lib.excel.formula.model import (
    Expression,
    XlA1Reference,
    XlArrayConstant,
    XlBinaryExpression,
    XlBinaryOperator,
    XlNumber,
    XlString,
)
from refinery.lib.excel.formula.ptg import Ptg, RpnContext, RpnDecoder, RpnError


class XlsbRpnDecoder(RpnDecoder):
    """
    The stack machine over the formula stream of an MS-XLSB cell or name record. A reference
    token stores absolute coordinates whatever its relative flags say, because the flags only
    tell which axes Excel copies the reference along; the context resolves defined names and
    the extern-sheet indexes of 3-D references.
    """

    _context: RpnContext | None

    def _read_string(self) -> XlString:
        """
        The `ptgStr` token: a two-byte character count followed by UTF-16 characters.
        """
        count = self._read_u16()
        return XlString(value=self._read_text(2 * count, 'utf_16_le'))

    def _read_function_id(self) -> int:
        return self._read_u16()

    def _read_function_variable(self) -> tuple[int, int, bool]:
        """
        The payload of a `ptgFuncVar` token: an argument count whose top bit only marks a
        pending prompt, then a two-byte identifier. An identifier of 255 — with or without the
        macro bit, which selects the macro-command half of the id space — names a user-defined
        call whose name operand sits below the arguments on the stack.
        """
        argc = self._read_u8() & 0x7F
        index = self._read_u16()
        return index, argc, (index & 0x7FFF) == 0xFF

    def _read_defined_name(self) -> Expression:
        """
        The `ptgName` token: a one-based index into the name table, padded with two unused
        bytes.
        """
        index = self._read_u16()
        self._read_bytes(2)
        if self._context is None:
            raise RpnError('a name token has no workbook to resolve it')
        return self._context.defined_name(index)

    def _read_reference(self, relative: bool) -> Expression:
        """
        The `ptgRef` and `ptgRefN` tokens: a four-byte row and a two-byte column whose top
        bits carry the relative flags. The grammar stores the absolute position in both
        spellings; the containing cell of a copied formula is what the flags select for.
        """
        return self._compose(self._read_u32(), self._read_u16())

    def _read_area(self, relative: bool) -> Expression:
        """
        The `ptgArea` and `ptgAreaN` tokens: two corners of a rectangle, each carrying the
        reference layout of the grammar.
        """
        first_row = self._read_u32()
        second_row = self._read_u32()
        first_col_word = self._read_u16()
        second_col_word = self._read_u16()
        return XlBinaryExpression(
            left=self._compose(first_row, first_col_word),
            operator=XlBinaryOperator.RANGE,
            right=self._compose(second_row, second_col_word),
        )

    def _read_3d_reference(self) -> Expression:
        """
        The `ptgRef3d` token: an extern-sheet index followed by a reference.
        """
        sheets = self._read_3d_sheets()
        return self._qualify(self._read_reference(False), sheets)

    def _read_3d_area(self) -> Expression:
        """
        The `ptgArea3d` token: an extern-sheet index followed by an area.
        """
        sheets = self._read_3d_sheets()
        return self._qualify(self._read_area(False), sheets)

    def _read_3d_sheets(self) -> tuple[str, ...]:
        if self._context is None:
            raise RpnError('a 3-D reference has no workbook to resolve its sheets')
        return self._context.extern_sheets(self._read_u16())

    @staticmethod
    def _compose(row: int, col_word: int) -> XlA1Reference:
        return XlA1Reference(
            row=row + 1,
            col=(col_word & 0x3FFF) + 1,
            relative_row=bool(col_word & 0x8000),
            relative_col=bool(col_word & 0x4000),
        )

    def _read_array(self) -> Expression:
        """
        The `ptgArray` token: a column count in which zero spells two hundred fifty-six, a row
        count, and then the constants in row-major order, each announced by a type byte.
        """
        cols = self._read_u8()
        if cols == 0:
            cols = 256
        rows = self._read_u16()
        constants = []
        for _ in range(cols * rows):
            kind = self._read_u8()
            if kind == 0x01:
                number = self._read_double()
                # a double that holds an integer is an integer literal: the synthesizer prints
                # it without the decimal point, as Excel writes it
                if number.is_integer():
                    number = int(number)
                constants.append(XlNumber(value=number))
            elif kind == 0x02:
                count = self._read_u16()
                constants.append(XlString(value=self._read_text(2 * count, 'utf_16_le')))
            else:
                raise RpnError(F'an array constant of type {kind:#x} holds no literal')
        return XlArrayConstant(
            rows=[tuple(constants[index:index + cols]) for index in range(0, len(constants), cols)]
        )

    def _skip_mem_token(self, token: Ptg) -> None:
        """
        The memory tokens of the reference subexpressions: a length word every one of them
        carries, with the memory-area tokens additionally announcing the rectangles of the
        subexpression that follows, which this reader only has to step over.
        """
        if token == Ptg.MEMAREA:
            self._read_bytes(4)
            size = self._read_u16()
            if size:
                count = self._read_u16()
                self._read_bytes(12 * count)
        elif token == Ptg.MEMERR or token == Ptg.MEMNOMEM:
            self._read_bytes(4)
            self._read_bytes(self._read_u16())
        else:
            self._read_bytes(self._read_u16())

    def _skip_mem_func(self) -> None:
        self._read_bytes(self._read_u16())

    def _skip_reference_error(self, token: Ptg) -> None:
        """
        The broken-reference tokens: reserved words as wide as the reference token of the same
        kind, which is one reference layout wider than the BIFF8 pair.
        """
        if token == Ptg.REFERR:
            self._read_bytes(6)
        elif token == Ptg.AREAERR:
            self._read_bytes(12)
        elif token == Ptg.REFERR3D:
            self._read_bytes(8)
        else:
            self._read_bytes(14)

Classes

class XlsbRpnDecoder (view, context=None)

The stack machine over the formula stream of an MS-XLSB cell or name record. A reference token stores absolute coordinates whatever its relative flags say, because the flags only tell which axes Excel copies the reference along; the context resolves defined names and the extern-sheet indexes of 3-D references.

Expand source code Browse git
class XlsbRpnDecoder(RpnDecoder):
    """
    The stack machine over the formula stream of an MS-XLSB cell or name record. A reference
    token stores absolute coordinates whatever its relative flags say, because the flags only
    tell which axes Excel copies the reference along; the context resolves defined names and
    the extern-sheet indexes of 3-D references.
    """

    _context: RpnContext | None

    def _read_string(self) -> XlString:
        """
        The `ptgStr` token: a two-byte character count followed by UTF-16 characters.
        """
        count = self._read_u16()
        return XlString(value=self._read_text(2 * count, 'utf_16_le'))

    def _read_function_id(self) -> int:
        return self._read_u16()

    def _read_function_variable(self) -> tuple[int, int, bool]:
        """
        The payload of a `ptgFuncVar` token: an argument count whose top bit only marks a
        pending prompt, then a two-byte identifier. An identifier of 255 — with or without the
        macro bit, which selects the macro-command half of the id space — names a user-defined
        call whose name operand sits below the arguments on the stack.
        """
        argc = self._read_u8() & 0x7F
        index = self._read_u16()
        return index, argc, (index & 0x7FFF) == 0xFF

    def _read_defined_name(self) -> Expression:
        """
        The `ptgName` token: a one-based index into the name table, padded with two unused
        bytes.
        """
        index = self._read_u16()
        self._read_bytes(2)
        if self._context is None:
            raise RpnError('a name token has no workbook to resolve it')
        return self._context.defined_name(index)

    def _read_reference(self, relative: bool) -> Expression:
        """
        The `ptgRef` and `ptgRefN` tokens: a four-byte row and a two-byte column whose top
        bits carry the relative flags. The grammar stores the absolute position in both
        spellings; the containing cell of a copied formula is what the flags select for.
        """
        return self._compose(self._read_u32(), self._read_u16())

    def _read_area(self, relative: bool) -> Expression:
        """
        The `ptgArea` and `ptgAreaN` tokens: two corners of a rectangle, each carrying the
        reference layout of the grammar.
        """
        first_row = self._read_u32()
        second_row = self._read_u32()
        first_col_word = self._read_u16()
        second_col_word = self._read_u16()
        return XlBinaryExpression(
            left=self._compose(first_row, first_col_word),
            operator=XlBinaryOperator.RANGE,
            right=self._compose(second_row, second_col_word),
        )

    def _read_3d_reference(self) -> Expression:
        """
        The `ptgRef3d` token: an extern-sheet index followed by a reference.
        """
        sheets = self._read_3d_sheets()
        return self._qualify(self._read_reference(False), sheets)

    def _read_3d_area(self) -> Expression:
        """
        The `ptgArea3d` token: an extern-sheet index followed by an area.
        """
        sheets = self._read_3d_sheets()
        return self._qualify(self._read_area(False), sheets)

    def _read_3d_sheets(self) -> tuple[str, ...]:
        if self._context is None:
            raise RpnError('a 3-D reference has no workbook to resolve its sheets')
        return self._context.extern_sheets(self._read_u16())

    @staticmethod
    def _compose(row: int, col_word: int) -> XlA1Reference:
        return XlA1Reference(
            row=row + 1,
            col=(col_word & 0x3FFF) + 1,
            relative_row=bool(col_word & 0x8000),
            relative_col=bool(col_word & 0x4000),
        )

    def _read_array(self) -> Expression:
        """
        The `ptgArray` token: a column count in which zero spells two hundred fifty-six, a row
        count, and then the constants in row-major order, each announced by a type byte.
        """
        cols = self._read_u8()
        if cols == 0:
            cols = 256
        rows = self._read_u16()
        constants = []
        for _ in range(cols * rows):
            kind = self._read_u8()
            if kind == 0x01:
                number = self._read_double()
                # a double that holds an integer is an integer literal: the synthesizer prints
                # it without the decimal point, as Excel writes it
                if number.is_integer():
                    number = int(number)
                constants.append(XlNumber(value=number))
            elif kind == 0x02:
                count = self._read_u16()
                constants.append(XlString(value=self._read_text(2 * count, 'utf_16_le')))
            else:
                raise RpnError(F'an array constant of type {kind:#x} holds no literal')
        return XlArrayConstant(
            rows=[tuple(constants[index:index + cols]) for index in range(0, len(constants), cols)]
        )

    def _skip_mem_token(self, token: Ptg) -> None:
        """
        The memory tokens of the reference subexpressions: a length word every one of them
        carries, with the memory-area tokens additionally announcing the rectangles of the
        subexpression that follows, which this reader only has to step over.
        """
        if token == Ptg.MEMAREA:
            self._read_bytes(4)
            size = self._read_u16()
            if size:
                count = self._read_u16()
                self._read_bytes(12 * count)
        elif token == Ptg.MEMERR or token == Ptg.MEMNOMEM:
            self._read_bytes(4)
            self._read_bytes(self._read_u16())
        else:
            self._read_bytes(self._read_u16())

    def _skip_mem_func(self) -> None:
        self._read_bytes(self._read_u16())

    def _skip_reference_error(self, token: Ptg) -> None:
        """
        The broken-reference tokens: reserved words as wide as the reference token of the same
        kind, which is one reference layout wider than the BIFF8 pair.
        """
        if token == Ptg.REFERR:
            self._read_bytes(6)
        elif token == Ptg.AREAERR:
            self._read_bytes(12)
        elif token == Ptg.REFERR3D:
            self._read_bytes(8)
        else:
            self._read_bytes(14)

Ancestors