diff --git a/tests/tivars.py b/tests/tivars.py index 9894bb7..6c778de 100644 --- a/tests/tivars.py +++ b/tests/tivars.py @@ -601,7 +601,7 @@ def test_group(self): ungrouped = test_group.ungroup() - self.assertEqual(ungrouped[0], TIEquation("10sin(theta", name="R1")) + self.assertEqual(ungrouped[0], TIEquation("10sin(theta", name="r1")) self.assertEqual(type(ungrouped[1]), TIWindowSettings) self.assertEqual(TIGroup.group(ungrouped).ungroup(), ungrouped) diff --git a/tivars/data.py b/tivars/data.py index 5d3961c..f2be8d1 100644 --- a/tivars/data.py +++ b/tivars/data.py @@ -386,7 +386,7 @@ def __call__(self, func: Callable) -> 'Section': signature = inspect.signature(func) match len(signature.parameters): case 1: pass - case 2: new._set = lambda value, _set=new._set, *, instance=None, **kwargs:\ + case 2: new._set = lambda value, _set=self._set, *, instance=None, **kwargs:\ _set(func(instance, value), instance=instance, **kwargs) case _: raise TypeError("Section and View function definitions can only take 1 or 2 parameters.") diff --git a/tivars/tokenizer/__init__.py b/tivars/tokenizer/__init__.py index 743f52c..7986c88 100644 --- a/tivars/tokenizer/__init__.py +++ b/tivars/tokenizer/__init__.py @@ -3,6 +3,9 @@ """ +import re +from warnings import warn + from tivars.data import String from tivars.models import * from tivars.tokens.scripts import * @@ -24,8 +27,14 @@ def get(cls, data: bytes, **kwargs) -> _T: return decode(data.ljust(8, b'\x00'))[0] @classmethod - def set(cls, value: _T, **kwargs) -> bytes: - return encode(value, mode="string")[0].rstrip(b'\x00') + def set(cls, value: _T, *, instance=None, **kwargs) -> bytes: + data = encode(re.sub(r"[\u0398\u03F4\u1DBF]", "θ", value), mode="string")[0].rstrip(b'\x00') + + if instance is None or not data.startswith(instance.leading_name_byte): + warn(f"Entry has an invalid name: '{value}'.", + BytesWarning) + + return data __all__ = ["decode", "encode", "TokenizedString", diff --git a/tivars/types/gdb.py b/tivars/types/gdb.py index debed93..7f3fb62 100644 --- a/tivars/types/gdb.py +++ b/tivars/types/gdb.py @@ -13,6 +13,7 @@ from tivars.flags import * from tivars.data import * from tivars.models import * +from tivars.tokenizer import decode from tivars.var import TIEntry, SizedEntry from .real import * from .tokenized import TIEquation @@ -194,7 +195,7 @@ class IndexedEquationConverter(Converter): _T = TIGraphedEquation @classmethod - def get(cls, data: bytes, *, instance=None, **kwargs) -> _T: + def get(cls, data: bytes, *, instance=None) -> _T: """ Converts ``bytes`` -> `TIGraphedEquation` by finding the equation at ``index`` within a GDB @@ -276,6 +277,14 @@ def length(self) -> int: The length of this entry's user data section """ + @property + def json_name(self) -> str: + """ + :return: The name of this equation used in the GDB JSON format + """ + + return decode(self.raw.name, mode="accessible")[0].strip("{}|") + def load_data_section(self, data: BytesIO): flag_byte = data.read(1) data_length = int.from_bytes(length_bytes := data.read(2), 'little') @@ -369,12 +378,15 @@ class TIMonoGDB(SizedEntry, register=True): TI_83P: "8xd" } + min_data_length = 61 + + leading_name_byte = b'\x61' + mode_byte = 0x00 """ The byte which identifies the GDB type """ - min_data_length = 61 has_color = False """ Whether this GDB type carries color information @@ -920,7 +932,7 @@ def dict(self) -> dict: "Xres": int(self.Xres) }, "equations": { - equation.name: equation.dict() for equation in self.equations + equation.json_name: equation.dict() for equation in self.equations } } } @@ -1102,7 +1114,7 @@ def dict(self) -> dict: "Tstep": self.Tstep.json_number(), }, "equations": { - equation.name: equation.dict() for equation in self.equations + equation.json_name: equation.dict() for equation in self.equations } } } @@ -1238,7 +1250,7 @@ def dict(self) -> dict: "Thetastep": self.Thetastep.json_number(), }, "equations": { - equation.name: equation.dict() for equation in self.equations + equation.json_name: equation.dict() for equation in self.equations } } } @@ -1486,7 +1498,7 @@ def dict(self) -> dict: "wnMinp1": self.wnMinp1.json_number() }, "equations": { - equation.name: equation.dict() for equation in self.equations + equation.json_name: equation.dict() for equation in self.equations } } } diff --git a/tivars/types/list.py b/tivars/types/list.py index 593254d..a4ec1bb 100644 --- a/tivars/types/list.py +++ b/tivars/types/list.py @@ -56,23 +56,22 @@ def set(cls, value: _T, **kwargs) -> bytes: :return: The name encoding of ``value`` """ - varname = value[:7].upper() - varname = re.sub(r"(\u03b8|\u0398|\u03F4|\u1DBF)", "θ", varname) + varname = value.upper() + varname = re.sub(r"[\u0398\u03F4\u1DBF]", "θ", varname) varname = re.sub(r"]", "|L", varname) - varname = re.sub(r"[^θa-zA-Z0-9]", "", varname) - if varname != value: - warn(f"List name '{value}' was transformed to '{varname}'.", - UserWarning) + if not re.fullmatch(r"(L\d)|(\|L|.)?([A-Z]|\u03b8)([0-9A-Z]|\u03b8){,4}|IDList", varname): + warn(f"List has an invalid name: '{varname}'.", + BytesWarning) if "IDList" in varname: - return b']@' + return b'\x5D\x40' - elif varname.startswith("|L"): - return super().set(varname[-5:]) + elif re.fullmatch(r"L\d", varname): + return super().set(varname[:2]) else: - return super().set(varname[:2]) + return super().set(varname[-5:]) class ListEntry(TIEntry): diff --git a/tivars/types/matrix.py b/tivars/types/matrix.py index 893cce8..eedbc06 100644 --- a/tivars/types/matrix.py +++ b/tivars/types/matrix.py @@ -32,6 +32,8 @@ class TIMatrix(TIEntry, register=True): min_data_length = 2 + leading_name_byte = b'\x5C' + _type_id = 0x02 def __init__(self, init=None, *, diff --git a/tivars/types/picture.py b/tivars/types/picture.py index ce432be..5ec02d5 100644 --- a/tivars/types/picture.py +++ b/tivars/types/picture.py @@ -251,6 +251,8 @@ class TIMonoPicture(PictureEntry): has_color = False + leading_name_byte = b'\x60' + _type_id = 0x07 def __init__(self, init=None, *, @@ -302,6 +304,8 @@ class TIPicture(PictureEntry, register=True): pixel_type = RGB np_shape = (height, width, 3) + leading_name_byte = b'\x60' + _type_id = 0x07 def __init__(self, init=None, *, @@ -341,7 +345,8 @@ def get(cls, data: bytes, **kwargs) -> _T: @classmethod def set(cls, value: _T, **kwargs) -> bytes: if not re.fullmatch(r"(Image)?\d", value): - warn(f"'{value}' is not a valid image name; defaulting to 'Image1'.") + warn(f"'{value}' is not a valid image name; defaulting to 'Image1'.", + BytesWarning) value = "Image1" return b"\x3C" + bytes([int(value[-1], 16) - 1]) @@ -378,7 +383,8 @@ class TIImage(PictureEntry, register=True): pixel_type = RGB np_shape = (height, width, 3) - leading_bytes = b'\x81' + leading_name_byte = b'\x3C' + leading_data_bytes = b'\x81' _type_id = 0x1A diff --git a/tivars/types/tokenized.py b/tivars/types/tokenized.py index 557ae56..e37d836 100644 --- a/tivars/types/tokenized.py +++ b/tivars/types/tokenized.py @@ -153,58 +153,6 @@ def string(self) -> str: return format(self, "") -class EquationName(TokenizedString): - """ - Converter for the name section of equations - - Equation names can be any of the following: - - - ``Y1`` - ``Y0`` - - ``X1T`` - ``X6T`` - - ``Y1T`` - ``Y6T`` - - ``r1`` - ``r6`` - - ``u``, ``v``, or ``w``. - """ - - _T = str - - @classmethod - def get(cls, data: bytes, **kwargs) -> _T: - """ - Converts ``bytes`` -> ``str`` as done by the memory viewer - - :param data: The raw bytes to convert - :return: The equation name contained in ``data`` - """ - - varname = super().get(data) - - if varname.startswith("|"): - return varname[1:] - - else: - return varname.upper().strip("{}") - - @classmethod - def set(cls, value: _T, **kwargs) -> bytes: - """ - Converts ``str`` -> ``bytes`` to match appearance in the memory viewer - - :param value: The value to convert - :return: The name encoding of ``value`` - """ - - varname = value[:8].lower() - - if varname.startswith("|") or varname in ("u", "v", "w"): - varname = "|" + varname[-1] - - elif varname[0] != "{" and varname[-1] != "}": - varname = "{" + varname + "}" - - return super().set(varname) - - class TIEquation(TokenizedEntry, register=True): """ Parser for equations @@ -219,6 +167,8 @@ class TIEquation(TokenizedEntry, register=True): TI_83P: "8xy" } + leading_name_byte = b'\x5E' + _type_id = 0x03 def __init__(self, init=None, *, @@ -228,14 +178,24 @@ def __init__(self, init=None, *, super().__init__(init, for_flash=for_flash, name=name, version=version, archived=archived, data=data) - @Section(8, EquationName) - def name(self) -> str: + @Section(8, TokenizedString) + def name(self, value) -> str: """ The name of the entry - Must be one of the equation names + Must be an equation name used in function, parametric, polar, or sequence mode. + (See https://ti-toolkit.github.io/tokens-wiki/categories/Y%3D%20Functions.html) """ + varname = value + if varname in ("u", "v", "w"): + varname = "|" + varname + + elif match := re.fullmatch(r"\{?([XYr]\dT?)}?", varname): + varname = "{" + match[1] + "}" + + return varname + class TINewEquation(TIEquation, register=True): """ @@ -261,6 +221,8 @@ class TIString(TokenizedEntry, register=True): TI_83P: "8xs" } + leading_name_byte = b'\xAA' + _type_id = 0x04 def __init__(self, init=None, *, @@ -275,11 +237,11 @@ def name(self, value) -> str: """ The name of the entry - Must be one of the string names: ``Str1`` - ``Str0`` + Must be one of the string names: ``Str1`` - ``Str0``. """ - if not re.fullmatch(r"Str\d", varname := value[:4].capitalize()): - warn(f"String has an invalid name: {varname}.", + if not re.fullmatch(r"Str\d", varname := value.capitalize()): + warn(f"String has an invalid name: '{varname}'.", BytesWarning) return varname @@ -335,12 +297,9 @@ def name(self, value) -> str: The name cannot start with a digit. """ - varname = value[:8].upper() - varname = re.sub(r"(\u03b8|\u0398|\u03F4|\u1DBF)", "θ", varname) - varname = re.sub(r"[^θa-zA-Z0-9]", "", varname) - - if not varname or varname[0].isnumeric(): - warn(f"Program has an invalid name: {varname}.", + varname = re.sub(r"[\u0398\u03F4\u1DBF]", "θ", value.upper()) + if not re.fullmatch(r"([A-Z]|\u03b8)([0-9A-Z]|\u03b8){,7}", varname): + warn(f"Program has an invalid name: '{varname}'.", BytesWarning) return varname diff --git a/tivars/var.py b/tivars/var.py index 887aa8f..3acf4ae 100644 --- a/tivars/var.py +++ b/tivars/var.py @@ -275,9 +275,14 @@ class TIEntry(Dock, Converter): If an entry's data is fixed in size, this value is necessarily the length of the data """ - leading_bytes = b'' + leading_name_byte = b'' """ - Bytes that always occur at the start of this entry's data + Byte that always begins the name of this entry + """ + + leading_data_bytes = b'' + """ + Bytes that always begin this entry's data """ _type_id = None @@ -612,7 +617,7 @@ def clear(self): Clears this entry's data """ - self.raw.calc_data = bytearray(self.leading_bytes) + self.raw.calc_data = bytearray(self.leading_data_bytes) self.raw.calc_data.extend(bytearray(self.min_data_length - self.calc_data_length)) def get_min_os(self, data: bytes = None) -> OsVersion: @@ -1231,15 +1236,15 @@ def data(self) -> bytes: pass def clear(self): - self.raw.calc_data = bytearray([0, 0, *self.leading_bytes]) + self.raw.calc_data = bytearray([0, 0, *self.leading_data_bytes]) self.raw.calc_data.extend(bytearray(self.min_data_length - self.calc_data_length)) - self.length = len(self.leading_bytes) + len(self.data) + self.length = len(self.leading_data_bytes) + len(self.data) @Loader[bytes, bytearray, BytesIO] def load_bytes(self, data: bytes | BytesIO): super().load_bytes(data) - if self.length != (data_length := len(self.leading_bytes) + len(self.data)): + if self.length != (data_length := len(self.leading_data_bytes) + len(self.data)): warn(f"The entry has an unexpected data length (expected {self.length}, got {data_length}).", BytesWarning)