Source code for hexformat.intelhex

""" Provide class to handle IntelHex content.

  License::

    MIT License

    Copyright (c) 2015-2023 by Martin Scharrer <martin.scharrer@web.de>

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software
    and associated documentation files (the "Software"), to deal in the Software without
    restriction, including without limitation the rights to use, copy, modify, merge, publish,
    distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all copies or
    substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
    BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
    DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

  Attributes:
    RT_DATA (int constant=0): Intel-Hex record type number for data record.
    RT_END_OF_FILE (int constant=1): Intel-Hex record type number for end of file record.
    RT_EXTENDED_SEGMENT_ADDRESS (int constant=2): Intel-Hex record type number for extend segment address record.
    RT_START_SEGMENT_ADDRESS (int constant=3): Intel-Hex record type number for start segment address record.
    RT_EXTENDED_LINEAR_ADDRESS (int constant=4): Intel-Hex record type number for extended linear address record.
    RT_START_LINEAR_ADDRESS (int constant=5): Intel-Hex record type number for start linear address record.

"""

from hexformat.base import DecodeError, EncodeError, HexFormat

# Intel-Hex Record Types
RT_DATA = 0
RT_END_OF_FILE = 1
RT_EXTENDED_SEGMENT_ADDRESS = 2
RT_START_SEGMENT_ADDRESS = 3
RT_EXTENDED_LINEAR_ADDRESS = 4
RT_START_LINEAR_ADDRESS = 5


[docs]class IntelHex(HexFormat): """`Intel-Hex`_ file representation class. The IntelHex class is able to parse and generate binary data in the Intel-Hex representation. Args: bytesperline (int): Number of bytes per line. variant: Variant of Intel-Hex format. Must be one out of ('I08HEX', 'I8HEX', 'I16HEX', 'I32HEX', 8, 16, 32). cs_ip (int, 32-bit): Value of CS:IP starting address used for I16HEX variant. eip (int, 32-bit): Value of EIP starting address used for I32HEX variant. Attributes: _DATALENGTH (tuple): Data length according to record type. _VARIANTS (dict): Mapping dict from variant name to number. _DEFAULT_BYTES_PER_LINE (int): Default number of bytes per line. _DEFAULT_VARIANT: Default variant. _bytesperline (None or int): Number of bytes per line of this instance. If None the default value will be used. _cs_ip (None or int): CS:IP address for I16HEX variant. If None no CS:IP address will be written. _eip (None or int): EIP address for I32HEX variant. If None no CS:IP address will be written. _variant (None or 8, 16 or 32): Numeric file format variant. If None the default variant is used. .. _`Intel-Hex`: http://en.wikipedia.org/wiki/Intel_HEX """ _DATALENGTH = (None, 0, 2, 4, 2, 4) _VARIANTS = {'I08HEX': 8, 'I8HEX': 8, 'I16HEX': 16, 'I32HEX': 32, 8: 8, 16: 16, 32: 32} _DEFAULT_BYTESPERLINE = 16 _DEFAULT_VARIANT = 32 _DEFAULT_EIP = None _DEFAULT_CS_IP = None _SETTINGS = ['bytesperline', 'cs_ip', 'eip', 'variant'] _STANDARD_FORMAT = 'ihex' def __init__(self, **settings): super(IntelHex, self).__init__() self._bytesperline = None self._cs_ip = None self._eip = None self._variant = None self.settings(**settings) @property def bytesperline(self): return self._bytesperline @bytesperline.setter def bytesperline(self, bytesperline): self._bytesperline = self._parse_bytesperline(bytesperline) @staticmethod def _parse_bytesperline(bytesperline): if bytesperline is not None: bytesperline = int(bytesperline) if bytesperline < 1 or bytesperline > 255: raise ValueError("bytesperline must be between 1 and 255") return bytesperline @property def variant(self): return self._variant @variant.setter def variant(self, variant): self._variant = self._parse_variant(variant) def _parse_variant(self, variant): if variant is not None: if variant in self._VARIANTS: variant = self._VARIANTS[variant] else: raise ValueError("variant must be one of " + ", ".join((str(k) for k in self._VARIANTS.keys()))) return variant @property def cs_ip(self): return self._cs_ip @cs_ip.setter def cs_ip(self, cs_ip): self._cs_ip = self._parse_cs_ip(cs_ip) @staticmethod def _parse_cs_ip(cs_ip): if cs_ip is not None: cs_ip = int(cs_ip) if cs_ip < 0 or cs_ip > 0xFFFFFFFF: raise ValueError("cs_ip value must not be larger than 32 bit.") return cs_ip @property def eip(self): return self._eip @eip.setter def eip(self, eip): self._eip = self._parse_eip(eip) @staticmethod def _parse_eip(eip): if eip is not None: eip = int(eip) if eip < 0 or eip > 0xFFFFFFFF: raise ValueError("eip value must not be larger than 32 bit.") return eip """Parses settings and returns tuple with all settings. Default values are substituted if needed. Args: isinit (bool): If False substitute None values with default values. bytesperline (int): Number of bytes per line. variant: Variant of Intel-Hex format. Must be one out of ('I08HEX', 'I8HEX', 'I16HEX', 'I32HEX', 8, 16, 32). cs_ip (int, 32-bit): Value of CS:IP starting address used for I16HEX variant. eip (int, 32-bit): Value of EIP starting address used for I32HEX variant. Returns: Tuple with settings: (bytesperline, variant, cs_ip, eip) Raises: ValueError: If cs_ip or eip value is larger than 32 bit. """ def _parseihexline(self, line): """Parse Intel-Hex line and return decoded parts as tuple. Args: line (str): Single input line, usually with line termination character(s). Returns: Tuple (recordtype, address, data, bytecount, crccorrect) with types (int, int, Buffer, int, bool). Raises: DecodeError: on lines which do not start with start code (":"). DecodeError: on data length - byte count mismatch. DecodeError: on unknown record type. DecodeError: on data length - record type mismatch. """ try: line = line.rstrip("\r\n") startcode = line[0] if startcode != ":": raise DecodeError("No valid IntelHex start code found.") databytes = bytearray.fromhex(line[1:]) except: raise ValueError bytecount = databytes[0] if bytecount != len(databytes) - 5: raise DecodeError("Data length does not match byte count.") checksumcorrect = ((sum(databytes) & 0xFF) == 0x00) address = (databytes[1] << 8) | databytes[2] recordtype = databytes[3] try: supposed_datalength = self._DATALENGTH[recordtype] except IndexError: raise DecodeError("Unsupported record type.") if supposed_datalength is not None and supposed_datalength != bytecount: raise DecodeError("Data length does not correspond to record type.") data = databytes[4:-1] return recordtype, address, data, bytecount, checksumcorrect def _encodeihexline(self, recordtype, address16bit, data): """Encode given data to Intel-Hex format. One or more Intel-Hex lines are encoded from the given address and buffer and written to the given file handle. Args: recordtype (int: 0..5): Intel-Hex record type. address16bit (int): Lower 16-bit part of address of first byte in buffer data. data (Buffer): Buffer with data to be encoded. Returns: line (str): Intel-Hex encoded line. Raises: DecodeError: on unknown record type. DecodeError: on datalength - record type mismatch. """ # linelen = 2 * len(data) + 11 linetempl = ":{:02X}{:04X}{:02X}{:s}{:02X}\n" bytecount = len(data) try: recordtype = int(recordtype) if recordtype < 0: raise IndexError supposed_datalength = self._DATALENGTH[recordtype] except (TypeError, IndexError): raise EncodeError("Unsupported record type.") if supposed_datalength is not None and supposed_datalength != bytecount: raise EncodeError("Data length does not correspond to record type.") address16bit &= 0xFFFF datastr = "".join("{:02X}".format(byte) for byte in data) checksum = bytecount + sum(data) + (address16bit >> 8) + (address16bit & 0xFF) + recordtype checksum = (~checksum + 1) & 0xFF return linetempl.format(bytecount, address16bit, recordtype, datastr, checksum)
[docs] @classmethod def fromihexfile(cls, filename, ignore_checksum_errors=False): """Generates IntelHex instance from Intel-Hex file. Opens filename for reading and calls :meth:`fromihexfh` with the file handle. Args: filename (str): input filename ignore_checksum_errors (bool): If True no error is raised on checksum failures Returns: New instance of class with loaded data. """ with open(filename, "r") as fh: return cls.fromihexfh(fh, ignore_checksum_errors)
[docs] @classmethod def fromihexfh(cls, fh, ignore_checksum_errors=False): """Generates IntelHex instance from file handle which must point to Intel-Hex lines. Creates new instance and calls :meth:`loadihexfh` on it. Args: fh (file handle or compatible): Source of Intel-Hex lines. ignore_checksum_errors (bool): If True no error is raised on checksum failures. Returns: New instance of class with loaded data. """ self = cls() self.loadihexfh(fh, ignore_checksum_errors) return self
[docs] def loadihexfile(self, filename, ignore_checksum_errors=False): """Loads Intel-Hex lines from named file. Creates new instance and calls :meth:`loadihexfh` on it. Args: filename (str): Name of Intel-Hex file. ignore_checksum_errors (bool): If True no error is raised on checksum failures. Returns: self """ with open(filename, "r") as fh: return self.loadihexfh(fh, ignore_checksum_errors)
[docs] def loadihexfh(self, fh, ignore_checksum_errors=False): """Loads Intel-Hex lines from file handle. Args: fh (file handle or compatible): Source of Intel-Hex lines. ignore_checksum_errors (bool): If True no error is raised on checksum failures. Returns: self Raises: DecodeError: on checksum mismatch if ignore_checksum_errors is False. DecodeError: on unsupported record type. """ highaddr = 0 segmaddr = None line = fh.readline() while line != '': (recordtype, lowaddress, data, datasize, checksumcorrect) = self._parseihexline(line) if not checksumcorrect and not ignore_checksum_errors: raise DecodeError("Checksum mismatch.") if recordtype == 0: if highaddr is not None: self.set((highaddr + lowaddress), data, datasize) else: if (lowaddress + datasize) <= 0x10000: self.set((segmaddr + lowaddress), data, datasize) else: # wrap on segment boundary: fit = 0x10000 - lowaddress self.set((segmaddr + lowaddress), data, fit) self.set(segmaddr, data[fit:]) if self._bytesperline is None: self._bytesperline = datasize elif recordtype == 1: # End of file return self elif recordtype == 2: segmaddr = (data[0] << 12) | (data[1] << 4) highaddr = None if self._variant is None: self._variant = 16 elif recordtype == 3: self._cs_ip = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3] if self._variant is None: self._variant = 16 elif recordtype == 4: highaddr = (data[0] << 24) | (data[1] << 16) segmaddr = None if self._variant is None: self._variant = 32 elif recordtype == 5: self._eip = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3] if self._variant is None: self._variant = 32 else: raise DecodeError("Unsupported record type.") line = fh.readline() return self
# noinspection PyIncorrectDocstring
[docs] def toihexfile(self, filename, **settings): """Writes content as Intel-Hex file to given file name. Opens filename for writing and calls :meth:`toihexfh` with the file handle and all arguments. See :meth:`toihexfh` for description of the arguments. Args: filename (str): Input file name. bytesperline (int): Number of bytes per line. variant ('I08HEX', 'I8HEX', 'I16HEX', 'I32HEX', 8, 16, 32): Variant of Intel-Hex format. cs_ip (int, 32-bit): Value of CS:IP starting address used for I16HEX variant. eip (int, 32-bit): Value of EIP starting address used for I32HEX variant. Returns: self """ with open(filename, "w") as fh: return self.toihexfh(fh, **settings)
# noinspection PyIncorrectDocstring
[docs] def toihexfh(self, fh, **settings): """Writes content as Intel-Hex file to given file handle. Args: fh (file handle or compatible): Destination of S-Record lines. bytesperline (int): Number of bytes per line. variant ('I08HEX', 'I8HEX', 'I16HEX', 'I32HEX', 8, 16, 32): Variant of Intel-Hex format. cs_ip (int, 32-bit): Value of CS:IP starting address used for I16HEX variant. eip (int, 32-bit): Value of EIP starting address used for I32HEX variant. Returns: self Raises: EncodeError: if selected address length is not wide enough to fit all addresses. """ (bytesperline, cs_ip, eip, variant) = self._parse_settings(**settings) highaddr = 0 addresshigh = 0 for address, buffer in self._parts: pos = 0 datalength = len(buffer) while pos < datalength: if variant == 32: if address > 0xFFFFFFFF: raise EncodeError("Address to large for format.") addresslow = address & 0x0000FFFF addresshigh = address & 0xFFFF0000 elif variant == 16: if address > 0xFFFFF: raise EncodeError("Address to large for format.") if address > (addresshigh + 0x0FFFF): addresshigh = address & 0xFFF00 addresslow = address - addresshigh else: if address > 0xFFFF: raise EncodeError("Address to large for format.") addresslow = address if addresshigh != highaddr: highaddr = addresshigh if variant == 32: fh.write(self._encodeihexline(4, 0, [addresshigh >> 24, (addresshigh >> 16) & 0xFF])) else: fh.write(self._encodeihexline(2, 0, [addresshigh >> 12, (addresshigh >> 4) & 0xFF])) endpos = min(pos + bytesperline, datalength) fh.write(self._encodeihexline(0, addresslow, buffer[pos:endpos])) address += bytesperline pos = endpos if variant == 32 and eip is not None: fh.write(self._encodeihexline(5, 0, [eip >> 24, (eip >> 16) & 0xFF, (eip >> 8) & 0xFF, eip & 0xFF])) elif variant == 16 and cs_ip is not None: fh.write(self._encodeihexline(3, 0, [cs_ip >> 24, (cs_ip >> 16) & 0xFF, (cs_ip >> 8) & 0xFF, cs_ip & 0xFF])) fh.write(self._encodeihexline(1, 0, bytearray())) return self
# noinspection PyProtectedMember def __eq__(self, other): """Compare with other instance for equality. Both instances are equal if both _parts lists, _eip and _cs_ip are identical. """ return super(IntelHex, self).__eq__(other) and self._eip == other._eip and self._cs_ip == other._cs_ip