diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8f21348..c3201a1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,8 @@ Changelog * Library rewritten from scratch (not backwards compatible). * Added new object oriented API, hopefully more user friendly. +* Added *Texas Instruments TI-TXT* file format. +* Improved docs and examples. 0.3.1 (2024-01-23) diff --git a/docs/_autosummary/hexrec.formats.titxt.TiTxtFile.rst b/docs/_autosummary/hexrec.formats.titxt.TiTxtFile.rst new file mode 100644 index 0000000..b40d1ad --- /dev/null +++ b/docs/_autosummary/hexrec.formats.titxt.TiTxtFile.rst @@ -0,0 +1,75 @@ +TiTxtFile +========= + +.. currentmodule:: hexrec.formats.titxt + +.. autoclass:: TiTxtFile + :members: + :inherited-members: + :private-members: + :special-members: + + + + + .. rubric:: Attributes + + .. autosummary:: + + ~TiTxtFile.DEFAULT_DATALEN + ~TiTxtFile.FILE_EXT + ~TiTxtFile.META_KEYS + ~TiTxtFile.maxdatalen + ~TiTxtFile.memory + ~TiTxtFile.records + + + + + + + .. rubric:: Methods + + .. autosummary:: + :nosignatures: + + ~TiTxtFile.__init__ + ~TiTxtFile.append + ~TiTxtFile.apply_records + ~TiTxtFile.clear + ~TiTxtFile.convert + ~TiTxtFile.copy + ~TiTxtFile.crop + ~TiTxtFile.cut + ~TiTxtFile.delete + ~TiTxtFile.discard_memory + ~TiTxtFile.discard_records + ~TiTxtFile.extend + ~TiTxtFile.fill + ~TiTxtFile.find + ~TiTxtFile.flood + ~TiTxtFile.from_blocks + ~TiTxtFile.from_bytes + ~TiTxtFile.from_memory + ~TiTxtFile.from_records + ~TiTxtFile.get_address_max + ~TiTxtFile.get_address_min + ~TiTxtFile.get_holes + ~TiTxtFile.get_meta + ~TiTxtFile.get_spans + ~TiTxtFile.index + ~TiTxtFile.load + ~TiTxtFile.merge + ~TiTxtFile.parse + ~TiTxtFile.print + ~TiTxtFile.read + ~TiTxtFile.save + ~TiTxtFile.serialize + ~TiTxtFile.set_meta + ~TiTxtFile.shift + ~TiTxtFile.split + ~TiTxtFile.update_records + ~TiTxtFile.validate_records + ~TiTxtFile.view + ~TiTxtFile.write + diff --git a/docs/_autosummary/hexrec.formats.titxt.TiTxtRecord.rst b/docs/_autosummary/hexrec.formats.titxt.TiTxtRecord.rst new file mode 100644 index 0000000..cdee7a0 --- /dev/null +++ b/docs/_autosummary/hexrec.formats.titxt.TiTxtRecord.rst @@ -0,0 +1,50 @@ +TiTxtRecord +=========== + +.. currentmodule:: hexrec.formats.titxt + +.. autoclass:: TiTxtRecord + :members: + :inherited-members: + :private-members: + :special-members: + + + + + .. rubric:: Attributes + + .. autosummary:: + + ~TiTxtRecord.EQUALITY_KEYS + ~TiTxtRecord.LINE_REGEX + ~TiTxtRecord.META_KEYS + + + + + + + .. rubric:: Methods + + .. autosummary:: + :nosignatures: + + ~TiTxtRecord.__init__ + ~TiTxtRecord.compute_checksum + ~TiTxtRecord.compute_count + ~TiTxtRecord.copy + ~TiTxtRecord.create_address + ~TiTxtRecord.create_data + ~TiTxtRecord.create_eof + ~TiTxtRecord.data_to_int + ~TiTxtRecord.get_meta + ~TiTxtRecord.parse + ~TiTxtRecord.print + ~TiTxtRecord.serialize + ~TiTxtRecord.to_bytestr + ~TiTxtRecord.to_tokens + ~TiTxtRecord.update_checksum + ~TiTxtRecord.update_count + ~TiTxtRecord.validate + diff --git a/docs/_autosummary/hexrec.formats.titxt.TiTxtTag.rst b/docs/_autosummary/hexrec.formats.titxt.TiTxtTag.rst new file mode 100644 index 0000000..24d1396 --- /dev/null +++ b/docs/_autosummary/hexrec.formats.titxt.TiTxtTag.rst @@ -0,0 +1,46 @@ +TiTxtTag +======== + +.. currentmodule:: hexrec.formats.titxt + +.. autoclass:: TiTxtTag + :members: + :inherited-members: + :private-members: + :special-members: + + + + + .. rubric:: Attributes + + .. autosummary:: + + ~TiTxtTag.DATA + ~TiTxtTag.ADDRESS + ~TiTxtTag.EOF + ~TiTxtTag.denominator + ~TiTxtTag.imag + ~TiTxtTag.numerator + ~TiTxtTag.real + + + + + + + .. rubric:: Methods + + .. autosummary:: + :nosignatures: + + ~TiTxtTag.is_address + ~TiTxtTag.is_eof + ~TiTxtTag.__init__ + ~TiTxtTag.as_integer_ratio + ~TiTxtTag.bit_count + ~TiTxtTag.bit_length + ~TiTxtTag.conjugate + ~TiTxtTag.from_bytes + ~TiTxtTag.to_bytes + diff --git a/docs/_autosummary/hexrec.formats.titxt.rst b/docs/_autosummary/hexrec.formats.titxt.rst new file mode 100644 index 0000000..8c7acc8 --- /dev/null +++ b/docs/_autosummary/hexrec.formats.titxt.rst @@ -0,0 +1,40 @@ +titxt +===== + +.. automodule:: hexrec.formats.titxt + + + + + + + + + + + + + + + .. rubric:: Classes + + .. autosummary:: + :toctree: + :template: custom-class-template.rst + :nosignatures: + + TiTxtFile + TiTxtRecord + TiTxtTag + + + + + + + + + + + + diff --git a/docs/index.rst b/docs/index.rst index d164bc5..af64099 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ Contents authors changelog + Indices and tables ================== diff --git a/docs/reference.rst b/docs/reference.rst index 96f2cde..2507c6f 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -12,6 +12,7 @@ Reference hexrec.formats.mos hexrec.formats.raw hexrec.formats.srec + hexrec.formats.titxt hexrec.formats.xtek hexrec.utils hexrec.xxd diff --git a/docs/requirements.txt b/docs/requirements.txt index ef73d5b..fc4f62b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,7 +2,7 @@ sphinx >= 7 sphinx-click sphinx-autodoc-typehints -bytesparse +bytesparse >= 0.0.8 colorama furo twine diff --git a/src/hexrec/__init__.py b/src/hexrec/__init__.py index b6b6ffd..0449ecf 100644 --- a/src/hexrec/__init__.py +++ b/src/hexrec/__init__.py @@ -36,12 +36,12 @@ from .formats.mos import MosFile from .formats.raw import RawFile from .formats.srec import SrecFile +from .formats.titxt import TiTxtFile from .formats.xtek import XtekFile from .xxd import xxd def _register_default_file_types(): - # TODO: __doc__ defaults = { # The most common formats come first @@ -50,6 +50,7 @@ def _register_default_file_types(): # Least common 'asciihex': AsciiHexFile, + 'titxt': TiTxtFile, 'xtek': XtekFile, 'mos': MosFile, diff --git a/src/hexrec/formats/asciihex.py b/src/hexrec/formats/asciihex.py index 98f4066..35afaf8 100644 --- a/src/hexrec/formats/asciihex.py +++ b/src/hexrec/formats/asciihex.py @@ -337,7 +337,8 @@ def to_bytestr( if self.tag == AsciiHexTag.ADDRESS: count = self.count or 1 - valstr = (b'$A%%0%dX%s' % (count, dollarend)) % (self.address & 0xFFFFFFFF) + mask = (1 << (4 * count)) - 1 + valstr = (b'$A%%0%dX%s' % (count, dollarend)) % (self.address & mask) elif self.tag == AsciiHexTag.CHECKSUM: valstr = b'$S%04X%s' % ((self.checksum & 0xFFFF), dollarend) @@ -397,7 +398,8 @@ def to_tokens( if tag == tag.ADDRESS: count = self.count or 1 - addrstr = (b'$A%%0%dX%s' % (count, dollarend)) % (self.address & 0xFFFFFFFF) + mask = (1 << (4 * count)) - 1 + addrstr = (b'$A%%0%dX%s' % (count, dollarend)) % (self.address & mask) elif tag == tag.CHECKSUM: chksstr = b'$S%04X%s' % ((self.checksum & 0xFFFF), dollarend) diff --git a/src/hexrec/formats/ihex.py b/src/hexrec/formats/ihex.py index a44cdc0..0264f24 100644 --- a/src/hexrec/formats/ihex.py +++ b/src/hexrec/formats/ihex.py @@ -128,7 +128,7 @@ def is_extension(self) -> bool: def is_file_termination(self) -> bool: - return super().is_file_termination() + return self.is_eof() def is_start(self) -> bool: r"""Tells whether this is a Start Address record tag. @@ -153,10 +153,6 @@ def is_start(self) -> bool: return ((self == self.START_SEGMENT_ADDRESS) or (self == self.START_LINEAR_ADDRESS)) - def is_file_termination(self) -> bool: - - return self.is_eof() - if not __TYPING_HAS_SELF: # pragma: no cover del Self @@ -774,7 +770,6 @@ def validate_records( extension = 0 for index, record in enumerate(records): - record = _cast(IhexRecord, record) record.validate() tag = _cast(IhexTag, record.tag) diff --git a/src/hexrec/formats/mos.py b/src/hexrec/formats/mos.py index fee8a81..402fcdb 100644 --- a/src/hexrec/formats/mos.py +++ b/src/hexrec/formats/mos.py @@ -582,7 +582,6 @@ def validate_records( last_data_endex = 0 for index, record in enumerate(records): - record = _cast(MosRecord, record) record.validate() tag = _cast(MosTag, record.tag) diff --git a/src/hexrec/formats/srec.py b/src/hexrec/formats/srec.py index 3ee0b10..fff860e 100644 --- a/src/hexrec/formats/srec.py +++ b/src/hexrec/formats/srec.py @@ -369,7 +369,7 @@ def is_data(self) -> bool: def is_file_termination(self) -> bool: - return super().is_file_termination() + return self.is_start() def is_header(self) -> bool: r"""Tells whether this is a header record tag. @@ -415,10 +415,6 @@ def is_start(self) -> bool: (self == self.START_24) or (self == self.START_32)) - def is_file_termination(self) -> bool: - - return self.is_start() - SIZE_TO_ADDRESS_FORMAT: Mapping[int, bytes] = { 2: b'%04X', @@ -1117,7 +1113,6 @@ def validate_records( data_count = 0 for index, record in enumerate(records): - record = _cast(SrecRecord, record) record.validate() tag = _cast(SrecTag, record.tag) diff --git a/src/hexrec/formats/titxt.py b/src/hexrec/formats/titxt.py new file mode 100644 index 0000000..2476d0a --- /dev/null +++ b/src/hexrec/formats/titxt.py @@ -0,0 +1,583 @@ +# Copyright (c) 2013-2024, Andrea Zoppi +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +r"""Texas Instruments TI-TXT format. + +See Also: + ``_ +""" + +import enum +import re +from typing import IO +from typing import Any +from typing import Mapping +from typing import Optional +from typing import Type +from typing import TypeVar +from typing import cast as _cast + +from ..base import AnyBytes +from ..base import BaseFile +from ..base import BaseRecord +from ..base import BaseTag +from ..base import TypeAlias +from ..utils import hexlify +from ..utils import unhexlify + +try: + from typing import Self +except ImportError: # pragma: no cover + Self: TypeAlias = Any # Python < 3.11 +__TYPING_HAS_SELF = Self is not Any + + +class TiTxtTag(BaseTag, enum.IntEnum): + r"""Texas Instruments TI-TXT tag.""" + + DATA = 0 + r"""Data.""" + + ADDRESS = 1 + r"""Address.""" + + EOF = 2 + r"""End Of File.""" + + _DATA = DATA + + def is_address(self) -> bool: + r"""Tells whether this is an address record. + + This method returns true if this record tag is used for *address* + records. + + Returns: + bool: This is an address record tag. + + Examples: + >>> from hexrec import TiTxtFile + >>> TiTxtTag = TiTxtFile.Record.Tag + >>> TiTxtTag.ADDRESS.is_address() + True + >>> TiTxtTag.DATA.is_address() + False + """ + + return self == self.ADDRESS + + def is_data(self) -> bool: + + return self == self.DATA + + def is_eof(self) -> bool: + r"""Tells whether this is an End Of File record. + + This method returns true if this record tag is used for *End Of File* + records. + + Returns: + bool: This is an End Of File record tag. + + Examples: + >>> from hexrec import TiTxtFile + >>> TiTxtTag = TiTxtFile.Record.Tag + >>> TiTxtTag.EOF.is_eof() + True + >>> TiTxtTag.DATA.is_eof() + False + """ + + return self == self.EOF + + def is_file_termination(self) -> bool: + + return self.is_eof() + + +if not __TYPING_HAS_SELF: # pragma: no cover + del Self + Self = TypeVar('Self', bound='TiTxtRecord') + + +class TiTxtRecord(BaseRecord): + r"""Texas Instruments TI-TXT record object.""" + + Tag: Type[TiTxtTag] = TiTxtTag + + LINE_REGEX = re.compile( + b'^\\s*(' + b"(?P([0-9A-Fa-f]{2}[ \\t]?)+)|" + b'(@(?P
[0-9A-Fa-f]+))|' + b'(?Pq)' + b')\\s*\\r?\\n?$' + ) + r"""Line parser regex.""" + + def compute_count(self) -> Optional[int]: + + Tag = self.Tag + tag = self.tag + + if tag == Tag.ADDRESS: + return self.count # loopback + else: + return None # not supported + + @classmethod + def create_address( + cls, + address: int, + addrlen: int = 4, + ) -> Self: + r"""Creates an address record. + + Args: + address (int): + Address value. + + addrlen (int): + Address length, in *nibbles* (4-bit units). + + Returns: + :class:`TiTxtRecord`: Address record object. + + Raises: + ValueError: invalid parameter. + + Examples: + >>> from hexrec import TiTxtFile + >>> record = TiTxtFile.Record.create_address(0x1234) + >>> str(record) + '@1234\r\n' + """ + + record = cls(cls.Tag.ADDRESS, address=address, count=addrlen) + return record + + @classmethod + def create_data( + cls, + address: int, + data: AnyBytes, + ) -> Self: + r"""Creates a data record. + + Args: + address (int): + Ignored; please provide zero. + + data (bytes): + Record byte data. + + Returns: + :class:`TiTxtRecord`: Data record object. + + Raises: + ValueError: invalid parameter. + + Examples: + >>> from hexrec import TiTxtFile + >>> record = TiTxtFile.Record.create_data(0, b'abc') + >>> str(record) + '61 62 63\r\n' + """ + + record = cls(cls.Tag.DATA, data=data, address=address) + return record + + @classmethod + def create_eof(cls) -> Self: + r"""Creates an End Of File record. + + Returns: + :class:`TiTxtRecord`: End Of File record object. + + Raises: + ValueError: invalid parameter. + + Examples: + >>> from hexrec import TiTxtFile + >>> record = TiTxtFile.Record.create_eof() + >>> str(record) + 'q\r\n' + """ + + record = cls(cls.Tag.EOF) + return record + + @classmethod + def parse( + cls, + line: AnyBytes, + address: int = 0, + validate: bool = True, + ) -> Self: + # TODO: __doc__ + + match = cls.LINE_REGEX.match(line) + if not match: + raise ValueError('syntax error') + + coords = match.span() + groups = match.groupdict() + groups_address = groups['address'] + groups_eof = groups['eof'] + groups_data = groups['data'] or b'' + + Tag = cls.Tag + count = None + data = b'' + + if groups_address: + tag = Tag.ADDRESS + address = int(groups_address, 16) + count = len(groups_address) + + elif groups_eof: + tag = Tag.EOF + + else: + tag = Tag.DATA + data = groups_data.translate(None, delete=b' \t') + data = unhexlify(data) + + record = cls(tag, + address=address, + data=data, + count=count, + coords=coords, + validate=validate) + return record + + def to_bytestr( + self, + end: AnyBytes = b'\r\n', + ) -> bytes: + r"""Converts into a byte string. + + Args: + end (bytes): + End of record termination bytes. + + Returns: + bytes: Byte string representation. + + Examples: + >>> from hexrec import TiTxtFile + >>> record = TiTxtFile.Record.create_data(0, b'abc') + >>> record.to_bytestr(end=b'\n') + b'61 62 63\n' + """ + + self.validate(checksum=False, count=False) + valstr = b'' + + if self.tag == TiTxtTag.ADDRESS: + count = self.count or 1 + mask = (1 << (4 * count)) - 1 + valstr = (b'@%%0%dX' % count) % (self.address & mask) + + elif self.tag == TiTxtTag.EOF: + valstr = b'q' + + elif self.data: + valstr = hexlify(self.data, b' ') + + bytestr = b'%s%s%s%s' % (self.before, valstr, self.after, end) + return bytestr + + def to_tokens( + self, + end: AnyBytes = b'\r\n', + ) -> Mapping[str, bytes]: + r"""Converts into byte string tokens. + + Args: + end (bytes): + End of record termination bytes. + + Returns: + bytes: Mapping of token keys to token byte strings. + + Examples: + >>> from hexrec import TiTxtFile + >>> record = TiTxtFile.Record.create_data(0, b'abc') + >>> record.to_tokens(end=b'\n') # doctest:+NORMALIZE_WHITESPACE + {'before': b'', 'begin': b'', 'address': b'', 'data': b'61 62 63', + 'after': b'', 'end': b'\n'} + """ + + self.validate(checksum=False, count=False) + tag = _cast(TiTxtTag, self.tag) + addrstr = b'' + eofstr = b'' + datastr = b'' + + if tag == tag.ADDRESS: + count = self.count or 1 + mask = (1 << (4 * count)) - 1 + addrstr = (b'@%%0%dX' % count) % (self.address & mask) + + elif tag == tag.EOF: + eofstr = b'q' + + elif self.data: + datastr = hexlify(self.data, b' ') + + return { + 'before': self.before, + 'begin': eofstr, + 'address': addrstr, + 'data': datastr, + 'after': self.after, + 'end': end, + } + + def validate( + self, + checksum: bool = True, + count: bool = True, + ) -> Self: + + super().validate(checksum=checksum, count=count) + Tag = self.Tag + tag = self.tag + + if self.after and not self.after.isspace(): + raise ValueError('junk after') + + if self.before and not self.before.isspace(): + raise ValueError('junk before') + + if count: + if self.count is None: + if tag == Tag.ADDRESS: + raise ValueError('count required') + else: + addrstr = b'%X' % self.address + if self.count < len(addrstr): + raise ValueError('count overflow') + + if self.data: + if tag != Tag.DATA: + raise ValueError('unexpected data') + + return self + + +if not __TYPING_HAS_SELF: # pragma: no cover + del Self + Self = TypeVar('Self', bound='TiTxtFile') + + +class TiTxtFile(BaseFile): + r"""Texas Instruments TI-TXT file object.""" + + Record: Type[TiTxtRecord] = TiTxtRecord + + @classmethod + def parse( + cls, + stream: IO, + ignore_errors: bool = False, + ignore_after_termination: bool = True, + ) -> Self: + + file = super().parse(stream, ignore_errors=ignore_errors, + ignore_after_termination=ignore_after_termination) + last_data_endex = 0 + + for record in file.records: + tag = _cast(TiTxtTag, record.tag) + + if tag.is_data(): + record.address = last_data_endex + last_data_endex += len(record.data) + + elif tag.is_address(): + last_data_endex = record.address + + return file + + def update_records( + self, + align: bool = False, + addrlen: int = 4, + ) -> Self: + r"""Applies memory and meta to records. + + This method processes the stored :attr:`memory` and *meta* information + to generate the sequence of :attr:`records`. + + This effectively converts the *memory role* into the *records role* + (keeping both). + + The :attr:`records` is assigned upon return. + Any exceptions being raised should not alter the file object. + + Args: + align (bool): + Aligns data record chunk address bounds to :attr:`maxdatalen`. + + addrlen (int): + Address length, in *nibbles* (4-bit units). + + Returns: + :class:`TiTxtFile`: *self*. + + Raises: + ValueError: :attr:`memory` attribute not populated. + + See Also: + :attr:`records` + :attr:`memory` + :meth:`get_meta` + :meth:`apply_records` + + Examples: + >>> from hexrec import TiTxtFile + >>> blocks = [[456, b'abc']] + >>> file = TiTxtFile.from_blocks(blocks, maxdatalen=8) + >>> file.memory.to_blocks() + [[456, b'abc']] + >>> file.get_meta() + {'maxdatalen': 8} + >>> _ = file.update_records() + >>> len(file.records) + 3 + >>> _ = file.print() + @01C8 + 61 62 63 + q + """ + + memory = self._memory + if memory is None: + raise ValueError('memory instance required') + + addrlen = addrlen.__index__() + if addrlen < 1: + raise ValueError('invalid address length') + + records = [] + Record = self.Record + last_data_endex = 0 + chunk_views = [] + try: + for chunk_start, chunk_view in memory.chop(self.maxdatalen, align=align): + chunk_views.append(chunk_view) + data = bytes(chunk_view) + + if chunk_start != last_data_endex: + record = Record.create_address(chunk_start, addrlen=addrlen) + records.append(record) + + record = Record.create_data(chunk_start, data) + records.append(record) + last_data_endex = chunk_start + len(chunk_view) + + finally: + for chunk_view in chunk_views: + chunk_view.release() + + record = Record.create_eof() + records.append(record) + + self.discard_records() + self._records = records + return self + + def validate_records( + self, + data_ordering: bool = False, + address_even: bool = True, + ) -> Self: + r"""Validates records. + + It performs consistency checks for the underlying :attr:`records`. + + Args: + data_ordering (bool): + Checks that the *data* record sequence has monotonically + increasing addresses, without any overlapping. + + address_even (bool): + Addresses must be even. + + Returns: + :class:`TiTxtFile`: *self*. + + Raises: + ValueError: Invalid record sequence. + + Examples: + >>> from hexrec import TiTxtFile + >>> records = [TiTxtFile.Record.create_data(456, b'abc')] + >>> file = TiTxtFile.from_records(records) + >>> file.validate_records() + Traceback (most recent call last): + ... + ValueError: missing end of file record + """ + + records = self._records + if records is None: + raise ValueError('records required') + + Tag = self.Record.Tag + last_data_endex = 0 + eof_record = None + + for index, record in enumerate(records): + record.validate() + tag = record.tag + + if tag == Tag.ADDRESS: + if address_even: + if record.address & 1: + raise ValueError('address not even') + + if data_ordering: + if record.address < last_data_endex: + raise ValueError('unordered data record') + last_data_endex = record.address + + elif tag == Tag.EOF: + if index != len(records) - 1: + raise ValueError('end of file record not last') + eof_record = record + + else: # elif tag == Tag.DATA: + last_data_endex += len(record.data) + + if eof_record is None: + raise ValueError('missing end of file record') + + return self + + +if not __TYPING_HAS_SELF: # pragma: no cover + del Self diff --git a/src/hexrec/formats/xtek.py b/src/hexrec/formats/xtek.py index a3cf4f9..f74bd97 100644 --- a/src/hexrec/formats/xtek.py +++ b/src/hexrec/formats/xtek.py @@ -690,7 +690,6 @@ def validate_records( last_data_endex = 0 for index, record in enumerate(records): - record = _cast(XtekRecord, record) record.validate() tag = _cast(XtekTag, record.tag) diff --git a/tests/test_formats_titxt.py b/tests/test_formats_titxt.py new file mode 100644 index 0000000..b2d9822 --- /dev/null +++ b/tests/test_formats_titxt.py @@ -0,0 +1,717 @@ +import io +import os +from pathlib import Path +from typing import cast as _cast + +import pytest +from bytesparse import Memory +from test_base import BaseTestFile +from test_base import BaseTestRecord +from test_base import BaseTestTag +from test_base import replace_stdin +from test_base import replace_stdout + +from hexrec.formats.titxt import TiTxtFile +from hexrec.formats.titxt import TiTxtRecord +from hexrec.formats.titxt import TiTxtTag + + +@pytest.fixture +def tmppath(tmpdir): # pragma: no cover + return Path(str(tmpdir)) + + +@pytest.fixture(scope='module') +def datadir(request): + dir_path, _ = os.path.splitext(request.module.__file__) + assert os.path.isdir(str(dir_path)) + return dir_path + + +@pytest.fixture +def datapath(datadir): + return Path(str(datadir)) + + +class TestTiTxtTag(BaseTestTag): + + Tag = TiTxtTag + + def test_enum(self): + assert TiTxtTag.DATA == 0 + assert TiTxtTag.ADDRESS == 1 + assert TiTxtTag.EOF == 2 + + def test_is_address(self): + assert TiTxtTag.DATA.is_address() is False + assert TiTxtTag.ADDRESS.is_address() is True + assert TiTxtTag.EOF.is_address() is False + + def test_is_data(self): + assert TiTxtTag.DATA.is_data() is True + assert TiTxtTag.ADDRESS.is_data() is False + assert TiTxtTag.EOF.is_data() is False + + def test_is_eof(self): + assert TiTxtTag.DATA.is_eof() is False + assert TiTxtTag.ADDRESS.is_eof() is False + assert TiTxtTag.EOF.is_eof() is True + + def test_is_file_termination(self): + assert TiTxtTag.DATA.is_file_termination() is False + assert TiTxtTag.ADDRESS.is_file_termination() is False + assert TiTxtTag.EOF.is_file_termination() is True + + +class TestTiTxtRecord(BaseTestRecord): + + Record = TiTxtRecord + + def test_compute_count(self): + assert TiTxtRecord.create_address(0x00000000).count == 4 + assert TiTxtRecord.create_address(0x00001234).count == 4 + assert TiTxtRecord.create_address(0x12345678, addrlen=8).count == 8 + assert TiTxtRecord.create_address(0, addrlen=1).count == 1 + assert TiTxtRecord.create_address(0, addrlen=2).count == 2 + assert TiTxtRecord.create_address(0, addrlen=3).count == 3 + assert TiTxtRecord.create_data(0, b'abc').count is None + assert TiTxtRecord.create_eof().count is None + + def test_create_address(self): + vector = [ + (1, 0x00000000), + (4, 0x00000000), + (4, 0x0000FFFF), + (8, 0x00000000), + (8, 0x0000FFFF), + (8, 0xFFFFFFFF), + ] + for addrlen, address in vector: + record = TiTxtRecord.create_address(address, addrlen=addrlen) + record.validate() + assert record.tag == TiTxtTag.ADDRESS + assert record.address == address + assert record.checksum is None + assert record.count == addrlen + assert record.data == b'' + + def test_create_address_raises(self): + vector = [ + (0, 0x00000000), + (3, 0x0000FFFF), + (4, 0x00010000), + (7, 0xFFFFFFFF), + (8, 0x100000000), + ] + for addrlen, address in vector: + with pytest.raises(ValueError, match='count overflow'): + TiTxtRecord.create_address(address, addrlen=addrlen) + + with pytest.raises(ValueError, match='address overflow'): + TiTxtRecord.create_address(-1) + + def test_create_data(self): + contents = [ + b'', + b'abc', + b'a' * 0xFF, + ] + addresses = [ + 0x0000, + 0xFFFF, + ] + for data in contents: + for address in addresses: + record = TiTxtRecord.create_data(address, data) + record.validate() + assert record.tag == TiTxtTag.DATA + assert record.address == address + assert record.checksum is None + assert record.count is None + assert record.data == data + + def test_create_eof(self): + record = TiTxtRecord.create_eof() + record.validate() + assert record.tag == TiTxtTag.EOF + assert record.address == 0 + assert record.checksum is None + assert record.count is None + assert record.data == b'' + + def test_parse(self): + lines = [ + b'@FFFF', + b'@FFFFFFFF', + b'FF', + b'FF ' * 0x1000, + b'q', + ] + records = [ + TiTxtRecord.create_address(0xFFFF), + TiTxtRecord.create_address(0xFFFFFFFF, addrlen=8), + TiTxtRecord.create_data(0, b'\xFF'), + TiTxtRecord.create_data(0, b'\xFF' * 0x1000), + TiTxtRecord.create_eof(), + ] + for line, expected in zip(lines, records): + actual = TiTxtRecord.parse(line) + assert actual == expected + assert actual.before == b'' + assert actual.after == b'' + + def test_parse_raises_syntax(self): + lines = [ + b'@', + b'@@FFFF', + b'@FFFF.', + b'.@FFFF', + + b'', + b'XX', + b'00.', + b'FF.', + + b'Q', + ] + for line in lines: + with pytest.raises(ValueError, match='syntax error'): + TiTxtRecord.parse(line) + + # SRecord test script "t0112a.sh" + def test_parse_srecord_t0112a(self): + lines = [ + b'@F000', + b'31 40 00 03 B2 40 80 5A 20 01 D2 D3 22 00 D2 E3', + b'21 00 3F 40 E8 FD 1F 83 FE 23 F9 3F', + b'@FFFE', + b'00 F0', + b'q', + ] + records = [ + TiTxtRecord.create_address(0xF000), + TiTxtRecord.create_data(0x0000, b'\x31\x40\x00\x03\xB2\x40\x80\x5A\x20\x01\xD2\xD3\x22\x00\xD2\xE3'), + TiTxtRecord.create_data(0x0000, b'\x21\x00\x3F\x40\xE8\xFD\x1F\x83\xFE\x23\xF9\x3F'), + TiTxtRecord.create_address(0xFFFE), + TiTxtRecord.create_data(0x0000, b'\x00\xF0'), + TiTxtRecord.create_eof(), + ] + for line, expected in zip(lines, records): + actual = TiTxtRecord.parse(line) + assert actual == expected + + def test_parse_syntax(self): + lines = [ + b'@0', + b'@0000', + b'@00000000', + b'@FFFF', + b'@FFFFFFFF', + b'@00000000', + b'@FFFFFFFF', + b' \t\v\f\r@FFFFFFFF', + b'@FFFFFFFF \t\v\f\r', + b'@FFFFFFFF\r\n', + b'@FFFFFFFF\n', + b'@ffffffff', + + b'00', + b'FF', + b'FF' * 0x1000, + b'FF ' * 0x1000, + b'FF\t' * 0x1000, + b' \t\v\f\rFF', + b'FF \t\v\f\r', + b'FF\r\n', + b'FF\n', + b'ff ' * 0x1000, + + b'q', + b' \t\v\f\rq', + b'q \t\v\f\r', + ] + records = [ + TiTxtRecord.create_address(0x00000000, addrlen=1), + TiTxtRecord.create_address(0x00000000), + TiTxtRecord.create_address(0x00000000, addrlen=8), + TiTxtRecord.create_address(0x0000FFFF), + TiTxtRecord.create_address(0xFFFFFFFF, addrlen=8), + TiTxtRecord.create_address(0x00000000, addrlen=8), + TiTxtRecord.create_address(0xFFFFFFFF, addrlen=8), + TiTxtRecord.create_address(0xFFFFFFFF, addrlen=8), + TiTxtRecord.create_address(0xFFFFFFFF, addrlen=8), + TiTxtRecord.create_address(0xFFFFFFFF, addrlen=8), + TiTxtRecord.create_address(0xFFFFFFFF, addrlen=8), + TiTxtRecord.create_address(0xFFFFFFFF, addrlen=8), + + TiTxtRecord.create_data(0, b'\x00'), + TiTxtRecord.create_data(0, b'\xFF'), + TiTxtRecord.create_data(0, b'\xFF' * 0x1000), + TiTxtRecord.create_data(0, b'\xFF' * 0x1000), + TiTxtRecord.create_data(0, b'\xFF' * 0x1000), + TiTxtRecord.create_data(0, b'\xFF'), + TiTxtRecord.create_data(0, b'\xFF'), + TiTxtRecord.create_data(0, b'\xFF'), + TiTxtRecord.create_data(0, b'\xFF'), + TiTxtRecord.create_data(0, b'\xFF' * 0x1000), + + TiTxtRecord.create_eof(), + TiTxtRecord.create_eof(), + TiTxtRecord.create_eof(), + ] + for line, expected in zip(lines, records): + actual = TiTxtRecord.parse(line) + assert actual == expected + + def test_to_bytestr(self): + lines = [ + b'@0000\r\n', + b'@FFFF\r\n', + b'@00000000\r\n', + b'@FFFFFFFF\r\n', + + b'00\r\n', + b'FF\r\n', + (b'FF ' * 0xFFF) + b'FF\r\n', + + b'q\r\n', + ] + records = [ + TiTxtRecord.create_address(0x00000000), + TiTxtRecord.create_address(0x0000FFFF), + TiTxtRecord.create_address(0x00000000, addrlen=8), + TiTxtRecord.create_address(0xFFFFFFFF, addrlen=8), + + TiTxtRecord.create_data(0, b'\x00'), + TiTxtRecord.create_data(0, b'\xFF'), + TiTxtRecord.create_data(0, b'\xFF' * 0x1000), + + TiTxtRecord.create_eof(), + ] + for expected, record in zip(lines, records): + record = _cast(TiTxtRecord, record) + actual = record.to_bytestr() + assert actual == expected + + def test_to_bytestr_end(self): + record = TiTxtRecord.create_address(0x1234) + assert record.to_bytestr() == b'@1234\r\n' + assert record.to_bytestr(end=b'\n') == b'@1234\n' + + record = TiTxtRecord.create_data(0, b'\x11\x22\x33\x44') + assert record.to_bytestr() == b'11 22 33 44\r\n' + assert record.to_bytestr(end=b'\n') == b'11 22 33 44\n' + + record = TiTxtRecord.create_eof() + assert record.to_bytestr() == b'q\r\n' + assert record.to_bytestr(end=b'\n') == b'q\n' + + def test_to_tokens(self): + lines = [ + b'||@0000|||\r\n', + b'||@FFFF|||\r\n', + b'||@00000000|||\r\n', + b'||@FFFFFFFF|||\r\n', + + b'|||||\r\n', + b'|||00||\r\n', + b'|||FF||\r\n', + b'|||' + (b'FF ' * 0xFFF) + b'FF||\r\n', + + b'|q||||\r\n', + ] + records = [ + TiTxtRecord.create_address(0x00000000), + TiTxtRecord.create_address(0x0000FFFF), + TiTxtRecord.create_address(0x00000000, addrlen=8), + TiTxtRecord.create_address(0xFFFFFFFF, addrlen=8), + + TiTxtRecord.create_data(0, b''), + TiTxtRecord.create_data(0, b'\x00'), + TiTxtRecord.create_data(0, b'\xFF'), + TiTxtRecord.create_data(0, b'\xFF' * 0x1000), + + TiTxtRecord.create_eof(), + ] + keys = [ + 'before', + 'begin', + 'address', + 'data', + 'after', + 'end', + ] + for expected, record in zip(lines, records): + tokens = record.to_tokens() + assert all((key in keys) for key in tokens.keys()) + actual = b'|'.join(tokens.get(key, b'?') for key in keys) + assert actual == expected + + def test_validate_raises(self): + matches = [ + 'junk after', + 'junk before', + + 'count required', + 'count overflow', + 'count overflow', + 'unexpected data', + + 'unexpected data', + ] + records = [ + TiTxtRecord(TiTxtTag.DATA, after=b'?', validate=False), + TiTxtRecord(TiTxtTag.DATA, before=b'?', validate=False), + + TiTxtRecord(TiTxtTag.ADDRESS, count=None, validate=False), + TiTxtRecord(TiTxtTag.ADDRESS, count=-1, validate=False), + TiTxtRecord(TiTxtTag.ADDRESS, count=4, address=0x10000, validate=False), + TiTxtRecord(TiTxtTag.ADDRESS, count=4, data=b'abc', validate=False), + + TiTxtRecord(TiTxtTag.EOF, data=b'abc', validate=False), + ] + for match, record in zip(matches, records): + record = _cast(TiTxtRecord, record) + with pytest.raises(ValueError, match=match): + record.validate() + + def test_validate_samples(self): + records = [ + TiTxtRecord.create_address(0x00000000), + TiTxtRecord.create_address(0x0000FFFF), + TiTxtRecord.create_address(0x00000000, addrlen=8), + TiTxtRecord.create_address(0xFFFFFFFF, addrlen=8), + + TiTxtRecord.create_data(0, b'\x00'), + TiTxtRecord.create_data(0, b'\xFF'), + TiTxtRecord.create_data(0, b'\xFF' * 0x1000), + + TiTxtRecord.create_eof(), + ] + for record in records: + returned = record.validate() + assert returned is record + + +class TestTiTxtFile(BaseTestFile): + + File = TiTxtFile + + def test_load_file(self, datapath): + path = str(datapath / 'simple.txt') + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_eof(), + ] + file = TiTxtFile.load(path) + assert file.records == records + + def test_load_stdin(self): + buffer = ( + b'61 62 63\r\n' + b'@1234\r\n' + b'78 79 7A\r\n' + b'q\r\n' + ) + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_eof(), + ] + stream = io.BytesIO(buffer) + with replace_stdin(stream): + file = TiTxtFile.load(None) + assert file._records == records + + def test_parse(self): + buffer = ( + b'61 62 63\r\n' + b'@1234\r\n' + b'78 79 7A\r\n' + b'q\r\n' + ) + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_eof(), + ] + with io.BytesIO(buffer) as stream: + file = TiTxtFile.parse(stream) + assert file._records == records + + # SRecord test script "t0112a.sh" + def test_parse_file_srecord_t0112a(self, datapath): + path = str(datapath / 'srecord_t0112a_sh.txt') + records = [ + TiTxtRecord.create_address(0xF000), + TiTxtRecord.create_data(0xF000, b'\x31\x40\x00\x03\xB2\x40\x80\x5A\x20\x01\xD2\xD3\x22\x00\xD2\xE3'), + TiTxtRecord.create_data(0xF010, b'\x21\x00\x3F\x40\xE8\xFD\x1F\x83\xFE\x23\xF9\x3F'), + TiTxtRecord.create_address(0xFFFE), + TiTxtRecord.create_data(0xFFFE, b'\x00\xF0'), + TiTxtRecord.create_eof(), + ] + with open(path, 'rb') as stream: + file = TiTxtFile.parse(stream) + assert file._records == records + + def test_parse_ignore_errors(self): + buffer = ( + b'61 62 63\r\n' + b'@@1234\r\n' + b'78 79 7A\r\n' + b'q\r\n' + ) + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_data(0x0003, b'xyz'), + TiTxtRecord.create_eof(), + ] + with io.BytesIO(buffer) as stream: + file = TiTxtFile.parse(stream, ignore_errors=True) + assert file._records == records + + def test_parse_plain(self): + buffer = ( + b'61 62 63\r\n' + b'@1234\r\n' + b'78 79 7A\r\n' + ) + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + ] + with io.BytesIO(buffer) as stream: + file = TiTxtFile.parse(stream) + assert file._records == records + + def test_parse_raises_syntax_error(self): + buffer = ( + b'61 62 63\r\n' + b'@@1234\r\n' + b'78 79 7A\r\n' + b'q\r\n' + ) + with pytest.raises(ValueError, match='syntax error'): + with io.BytesIO(buffer) as stream: + TiTxtFile.parse(stream, ignore_errors=False) + + def test_save_file(self, tmppath): + path = str(tmppath / 'test_save_file.txt') + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_eof(), + ] + expected = ( + b'61 62 63\r\n' + b'@1234\r\n' + b'78 79 7A\r\n' + b'q\r\n' + ) + file = TiTxtFile.from_records(records) + returned = file.save(path) + assert returned is file + with open(path, 'rb') as stream: + actual = stream.read() + assert actual == expected + + def test_save_stdout(self): + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_eof(), + ] + expected = ( + b'61 62 63\r\n' + b'@1234\r\n' + b'78 79 7A\r\n' + b'q\r\n' + ) + stream = io.BytesIO() + file = TiTxtFile.from_records(records) + with replace_stdout(stream): + returned = file.save(None) + assert returned is file + actual = stream.getvalue() + assert actual == expected + + def test_serialize(self): + expected = ( + b'61 62 63\r\n' + b'@1234\r\n' + b'78 79 7A\r\n' + b'q\r\n' + ) + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_eof(), + ] + file = TiTxtFile.from_records(records) + stream = io.BytesIO() + file.serialize(stream) + actual = stream.getvalue() + assert actual == expected + + def test_serialize_plain(self): + expected = ( + b'61 62 63\r\n' + b'@1234\r\n' + b'78 79 7A\r\n' + ) + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + ] + file = TiTxtFile.from_records(records) + stream = io.BytesIO() + file.serialize(stream) + actual = stream.getvalue() + assert actual == expected + + def test_update_records(self): + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_eof(), + ] + blocks = [ + [0x0000, b'abc'], + [0x1234, b'xyz'], + ] + file = TiTxtFile.from_blocks(blocks) + file._records = None + returned = file.update_records() + assert returned is file + assert file._records == records + + def test_update_records_addrlen(self): + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234, addrlen=8), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_eof(), + ] + blocks = [ + [0x0000, b'abc'], + [0x1234, b'xyz'], + ] + file = TiTxtFile.from_blocks(blocks) + file._records = None + returned = file.update_records(addrlen=8) + assert returned is file + assert file._records == records + + def test_update_records_empty(self): + file = TiTxtFile.from_memory() + file._records = None + file.update_records() + assert file._records is not None + assert file._records == [TiTxtRecord.create_eof()] + + def test_update_records_raises_addrlen(self): + file = TiTxtFile() + with pytest.raises(ValueError, match='invalid address length'): + file.update_records(addrlen=0) + + def test_update_records_raises_memory(self): + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_eof(), + ] + file = TiTxtFile.from_records(records) + with pytest.raises(ValueError, match='memory instance required'): + file.update_records() + + def test_validate_records(self): + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_eof(), + ] + file = TiTxtFile.from_records(records) + file.validate_records() + + def test_validate_records_address_even(self): + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_eof(), + ] + file = TiTxtFile.from_records(records) + file.validate_records(address_even=True) + file.validate_records(address_even=False) + + def test_validate_records_data_ordering(self): + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_eof(), + ] + file = TiTxtFile.from_records(records) + file.validate_records(data_ordering=True) + file.validate_records(data_ordering=False) + + def test_validate_records_raises_address_even(self): + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1235), + TiTxtRecord.create_data(0x1235, b'xyz'), + TiTxtRecord.create_eof(), + ] + file = TiTxtFile.from_records(records) + with pytest.raises(ValueError, match='address not even'): + file.validate_records(address_even=True) + + def test_validate_records_raises_data_ordering(self): + records = [ + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + TiTxtRecord.create_address(0x0000), + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_eof(), + ] + file = TiTxtFile.from_records(records) + with pytest.raises(ValueError, match='unordered data record'): + file.validate_records(data_ordering=True) + + def test_validate_records_raises_eof_missing(self): + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_data(0x1234, b'xyz'), + ] + file = TiTxtFile.from_records(records) + with pytest.raises(ValueError, match='missing end of file record'): + file.validate_records() + + def test_validate_records_raises_eof_not_last(self): + records = [ + TiTxtRecord.create_data(0x0000, b'abc'), + TiTxtRecord.create_address(0x1234), + TiTxtRecord.create_eof(), + TiTxtRecord.create_data(0x1234, b'xyz'), + ] + file = TiTxtFile.from_records(records) + with pytest.raises(ValueError, match='end of file record not last'): + file.validate_records() + + def test_validate_records_raises_records(self): + file = TiTxtFile.from_memory(Memory.from_bytes(b'abc')) + with pytest.raises(ValueError, match='records required'): + file.validate_records() diff --git a/tests/test_formats_titxt/simple.txt b/tests/test_formats_titxt/simple.txt new file mode 100644 index 0000000..8250e50 --- /dev/null +++ b/tests/test_formats_titxt/simple.txt @@ -0,0 +1,4 @@ +61 62 63 +@1234 +78 79 7A +q diff --git a/tests/test_formats_titxt/srecord_t0112a_sh.txt b/tests/test_formats_titxt/srecord_t0112a_sh.txt new file mode 100644 index 0000000..be4abfd --- /dev/null +++ b/tests/test_formats_titxt/srecord_t0112a_sh.txt @@ -0,0 +1,6 @@ +@F000 +31 40 00 03 B2 40 80 5A 20 01 D2 D3 22 00 D2 E3 +21 00 3F 40 E8 FD 1F 83 FE 23 F9 3F +@FFFE +00 F0 +q