objutils 0.10.0__tar.gz → 0.10.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {objutils-0.10.0 → objutils-0.10.2}/PKG-INFO +1 -1
  2. {objutils-0.10.0 → objutils-0.10.2}/objutils/__init__.py +3 -3
  3. {objutils-0.10.0 → objutils-0.10.2}/objutils/fpc.py +2 -2
  4. {objutils-0.10.0 → objutils-0.10.2}/objutils/hexfile.py +50 -22
  5. {objutils-0.10.0 → objutils-0.10.2}/objutils/ihex.py +8 -30
  6. {objutils-0.10.0 → objutils-0.10.2}/objutils/image.py +47 -0
  7. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/oj_hex_info.py +1 -9
  8. {objutils-0.10.0 → objutils-0.10.2}/objutils/section.py +142 -24
  9. {objutils-0.10.0 → objutils-0.10.2}/objutils/srec.py +1 -1
  10. objutils-0.10.2/objutils/tests/test_hexfile.py +136 -0
  11. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_image.py +27 -0
  12. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_section.py +44 -0
  13. objutils-0.10.2/objutils/tests/test_section_join.py +80 -0
  14. {objutils-0.10.0 → objutils-0.10.2}/objutils/version.py +1 -1
  15. {objutils-0.10.0 → objutils-0.10.2}/pyproject.toml +2 -2
  16. objutils-0.10.0/objutils/tests/test_hexfile.py +0 -65
  17. {objutils-0.10.0 → objutils-0.10.2}/CMakeLists.txt +0 -0
  18. {objutils-0.10.0 → objutils-0.10.2}/LICENSE +0 -0
  19. {objutils-0.10.0 → objutils-0.10.2}/build_ext.py +0 -0
  20. {objutils-0.10.0 → objutils-0.10.2}/docs/README.rst +0 -0
  21. {objutils-0.10.0 → objutils-0.10.2}/objutils/.coveragerc +0 -0
  22. {objutils-0.10.0 → objutils-0.10.2}/objutils/.vscode/launch.json +0 -0
  23. {objutils-0.10.0 → objutils-0.10.2}/objutils/.vscode/settings.json +0 -0
  24. {objutils-0.10.0 → objutils-0.10.2}/objutils/a2l_test.shf +0 -0
  25. {objutils-0.10.0 → objutils-0.10.2}/objutils/ash.py +0 -0
  26. {objutils-0.10.0 → objutils-0.10.2}/objutils/binfile.py +0 -0
  27. {objutils-0.10.0 → objutils-0.10.2}/objutils/checksums.py +0 -0
  28. {objutils-0.10.0 → objutils-0.10.2}/objutils/cosmac.py +0 -0
  29. {objutils-0.10.0 → objutils-0.10.2}/objutils/dwarf/.traverser.py.un~ +0 -0
  30. {objutils-0.10.0 → objutils-0.10.2}/objutils/dwarf/__init__.py +0 -0
  31. {objutils-0.10.0 → objutils-0.10.2}/objutils/dwarf/attrparser.py +0 -0
  32. {objutils-0.10.0 → objutils-0.10.2}/objutils/dwarf/c_generator.py +0 -0
  33. {objutils-0.10.0 → objutils-0.10.2}/objutils/dwarf/constants.py +0 -0
  34. {objutils-0.10.0 → objutils-0.10.2}/objutils/dwarf/encoding.py +0 -0
  35. {objutils-0.10.0 → objutils-0.10.2}/objutils/dwarf/lineprog.py +0 -0
  36. {objutils-0.10.0 → objutils-0.10.2}/objutils/dwarf/readers.py +0 -0
  37. {objutils-0.10.0 → objutils-0.10.2}/objutils/dwarf/sm.py +0 -0
  38. {objutils-0.10.0 → objutils-0.10.2}/objutils/dwarf/traverser.py +0 -0
  39. {objutils-0.10.0 → objutils-0.10.2}/objutils/elf/__init__.py +0 -0
  40. {objutils-0.10.0 → objutils-0.10.2}/objutils/elf/arm/__init__.py +0 -0
  41. {objutils-0.10.0 → objutils-0.10.2}/objutils/elf/arm/attributes.py +0 -0
  42. {objutils-0.10.0 → objutils-0.10.2}/objutils/elf/defs.py +0 -0
  43. {objutils-0.10.0 → objutils-0.10.2}/objutils/elf/model.py +0 -0
  44. {objutils-0.10.0 → objutils-0.10.2}/objutils/emon52.py +0 -0
  45. {objutils-0.10.0 → objutils-0.10.2}/objutils/etek.py +0 -0
  46. {objutils-0.10.0 → objutils-0.10.2}/objutils/exceptions.py +0 -0
  47. {objutils-0.10.0 → objutils-0.10.2}/objutils/extensions/__init__.py +0 -0
  48. {objutils-0.10.0 → objutils-0.10.2}/objutils/extensions/ctre.hpp +0 -0
  49. {objutils-0.10.0 → objutils-0.10.2}/objutils/extensions/difflib.h +0 -0
  50. {objutils-0.10.0 → objutils-0.10.2}/objutils/extensions/exceptions.cpp +0 -0
  51. {objutils-0.10.0 → objutils-0.10.2}/objutils/extensions/exceptions.hpp +0 -0
  52. {objutils-0.10.0 → objutils-0.10.2}/objutils/extensions/hexfile.cpp +0 -0
  53. {objutils-0.10.0 → objutils-0.10.2}/objutils/extensions/wrapper.cpp +0 -0
  54. {objutils-0.10.0 → objutils-0.10.2}/objutils/hexdump.py +0 -0
  55. {objutils-0.10.0 → objutils-0.10.2}/objutils/ieee695.py +0 -0
  56. {objutils-0.10.0 → objutils-0.10.2}/objutils/logger.py +0 -0
  57. {objutils-0.10.0 → objutils-0.10.2}/objutils/mostec.py +0 -0
  58. {objutils-0.10.0 → objutils-0.10.2}/objutils/objutils.code-workspace +0 -0
  59. {objutils-0.10.0 → objutils-0.10.2}/objutils/pecoff/__init__.py +0 -0
  60. {objutils-0.10.0 → objutils-0.10.2}/objutils/pecoff/defs.py +0 -0
  61. {objutils-0.10.0 → objutils-0.10.2}/objutils/pecoff/model.py +0 -0
  62. {objutils-0.10.0 → objutils-0.10.2}/objutils/pecoff/pdb/__init__.py +0 -0
  63. {objutils-0.10.0 → objutils-0.10.2}/objutils/pickleif.py +0 -0
  64. {objutils-0.10.0 → objutils-0.10.2}/objutils/rca.py +0 -0
  65. {objutils-0.10.0 → objutils-0.10.2}/objutils/readers.py +0 -0
  66. {objutils-0.10.0 → objutils-0.10.2}/objutils/registry.py +0 -0
  67. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/arduino_build_artifacts.py +0 -0
  68. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/oj_cgen.py +0 -0
  69. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/oj_coff_extract.py +0 -0
  70. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/oj_coff_import.py +0 -0
  71. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/oj_coff_info.py +0 -0
  72. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/oj_coff_syms.py +0 -0
  73. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/oj_dwarf_import.py +0 -0
  74. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/oj_elf_arm_attrs.py +0 -0
  75. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/oj_elf_extract.py +0 -0
  76. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/oj_elf_import.py +0 -0
  77. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/oj_elf_info.py +0 -0
  78. {objutils-0.10.0 → objutils-0.10.2}/objutils/scripts/oj_elf_syms.py +0 -0
  79. {objutils-0.10.0 → objutils-0.10.2}/objutils/shf.py +0 -0
  80. {objutils-0.10.0 → objutils-0.10.2}/objutils/sig.py +0 -0
  81. {objutils-0.10.0 → objutils-0.10.2}/objutils/tek.py +0 -0
  82. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/__init__.py +0 -0
  83. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_arm_attributes.py +0 -0
  84. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_ash.py +0 -0
  85. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_c_generator.py +0 -0
  86. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_checksums.py +0 -0
  87. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_cygpath.py +0 -0
  88. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_diff_bin.py +0 -0
  89. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_elf.py +0 -0
  90. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_emon52.py +0 -0
  91. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_examples_cgen.py +0 -0
  92. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_fpc.py +0 -0
  93. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_hexdump.py +0 -0
  94. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_ihex.py +0 -0
  95. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_mostec.py +0 -0
  96. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_readers.py +0 -0
  97. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_registry.py +0 -0
  98. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_repr.py +0 -0
  99. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_shf.py +0 -0
  100. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_sm.py +0 -0
  101. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_srec.py +0 -0
  102. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_tek.py +0 -0
  103. {objutils-0.10.0 → objutils-0.10.2}/objutils/tests/test_titext.py +0 -0
  104. {objutils-0.10.0 → objutils-0.10.2}/objutils/titxt.py +0 -0
  105. {objutils-0.10.0 → objutils-0.10.2}/objutils/utils/__init__.py +0 -0
  106. {objutils-0.10.0 → objutils-0.10.2}/objutils/utils/arduino.py +0 -0
  107. {objutils-0.10.0 → objutils-0.10.2}/objutils/utils/diff.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: objutils
3
- Version: 0.10.0
3
+ Version: 0.10.2
4
4
  Summary: Objectfile library for Python
5
5
  License: GPLv2
6
6
  License-File: LICENSE
@@ -13,7 +13,7 @@ Registers CODECS and implements an interface to them.
13
13
  The first parameter is always the codec name.
14
14
  """
15
15
 
16
- __version__ = "0.10.0"
16
+ __version__ = "0.10.2"
17
17
 
18
18
  __all__ = [
19
19
  "Image",
@@ -125,7 +125,7 @@ registry.register("ash", objutils.ash.Reader, objutils.ash.Writer, "ASCII hex sp
125
125
  registry.register("shf", objutils.shf.Reader, objutils.shf.Writer, "S Hexdump Format (rfc4149).")
126
126
 
127
127
 
128
- def load(codec_name: str, fp: str | Path | BinaryIO, join: bool = False, **kws: Any) -> Image:
128
+ def load(codec_name: str, fp: str | Path | BinaryIO, join: bool = True, **kws: Any) -> Image:
129
129
  """Load hex data from file.
130
130
 
131
131
  Parameters
@@ -140,7 +140,7 @@ def load(codec_name: str, fp: str | Path | BinaryIO, join: bool = False, **kws:
140
140
  return registry.get(codec_name).Reader().load(fp, join=join, **kws)
141
141
 
142
142
 
143
- def loads(codec_name: str, data: str | bytes | bytearray, join: bool = False, **kws: Any) -> Image:
143
+ def loads(codec_name: str, data: str | bytes | bytearray, join: bool = True, **kws: Any) -> Image:
144
144
  """Load hex data from bytes.
145
145
 
146
146
  Parameters
@@ -108,7 +108,7 @@ class Reader(hexfile.Reader):
108
108
  out_lines.append("".join(values))
109
109
  return "\n".join(out_lines)
110
110
 
111
- def read(self, fp: BinaryIO) -> Image:
111
+ def read(self, fp: BinaryIO, join: bool = False) -> Image:
112
112
  """Read FPC file and convert to Image.
113
113
 
114
114
  Args:
@@ -117,7 +117,7 @@ class Reader(hexfile.Reader):
117
117
  Returns:
118
118
  Image object containing decoded sections
119
119
  """
120
- return super().read(create_string_buffer(bytearray(self.decode(fp), "ascii")))
120
+ return super().read(create_string_buffer(bytearray(self.decode(fp), "ascii")), join=join)
121
121
 
122
122
  def convert_quintuple(self, quintuple: str) -> int:
123
123
  """Convert 5-character base-85 quintuple to 32-bit integer.
@@ -499,16 +499,21 @@ class FormatParser:
499
499
  # ============================================================================
500
500
 
501
501
 
502
+ @dataclass
502
503
  class Container:
503
- """Legacy dynamic attribute container.
504
-
505
- Deprecated: Use ParsedRecord instead.
506
- """
504
+ """Modern attribute container for parsed hex records."""
507
505
 
508
- def __init__(self) -> None:
509
- self.processing_instructions: list[Any] = []
506
+ line_number: int = 0
507
+ address: Optional[int] = None
508
+ length: Optional[int] = None
509
+ type: Optional[int] = None
510
+ checksum: Optional[int] = None
511
+ chunk: Optional[bytearray] = None
512
+ junk: Optional[str] = None
513
+ processing_instructions: list[Any] = field(default_factory=list)
510
514
 
511
515
  def add_processing_instruction(self, pi: Any) -> None:
516
+ """Add a processing instruction to the record."""
512
517
  self.processing_instructions.append(pi)
513
518
 
514
519
 
@@ -785,6 +790,7 @@ class Reader(BaseType):
785
790
  self.stats = Statistics()
786
791
  self.valid = True
787
792
  self.formats: list[tuple[int, re.Pattern[str]]] = []
793
+ self.base_address = 0 # Base address for relative addressing (if applicable - mainly Intel HEX)
788
794
 
789
795
  # Parse FORMAT_SPEC into compiled patterns
790
796
  if isinstance(self.FORMAT_SPEC, str):
@@ -831,6 +837,21 @@ class Reader(BaseType):
831
837
  buffer = create_string_buffer(image)
832
838
  return self.load(buffer, join=join)
833
839
 
840
+ def _parse_optional_int(self, groups: dict[str, Optional[str]], key: str) -> Optional[int]:
841
+ """Parse an optional numeric capture group using ``atoi``.
842
+
843
+ Args:
844
+ groups: Regex named groups from a parsed line.
845
+ key: Group name to parse.
846
+
847
+ Returns:
848
+ Parsed integer or ``None`` if the group is missing/empty.
849
+ """
850
+ value = groups.get(key)
851
+ if value is None or value == "":
852
+ return None
853
+ return atoi(value)
854
+
834
855
  def read(self, fp: BinaryIO, join: bool = False) -> Image:
835
856
  """Read and parse hex file.
836
857
 
@@ -860,25 +881,23 @@ class Reader(BaseType):
860
881
  continue
861
882
 
862
883
  matched = True
863
- container = Container()
864
- container.line_number = line_number
865
- dict_ = match.groupdict()
884
+ dict_: dict[str, Optional[str]] = match.groupdict()
866
885
 
867
886
  if not dict_:
868
887
  continue
869
888
 
870
889
  self.stats.record_types[format_type] += 1
871
890
 
872
- # Parse scalar values (address, length, type, checksum)
873
- for key, value in dict_.items():
874
- if key not in ("chunk", "junk"):
875
- setattr(container, key, atoi(value))
876
- elif key == "junk":
877
- setattr(container, key, value)
891
+ address = self._parse_optional_int(dict_, "address")
892
+ length = self._parse_optional_int(dict_, "length")
893
+ record_type = self._parse_optional_int(dict_, "type")
894
+ checksum = self._parse_optional_int(dict_, "checksum")
895
+ junk = dict_.get("junk")
878
896
 
879
897
  # Parse data chunk
880
- if "chunk" in dict_:
881
- chunk_str = dict_["chunk"]
898
+ chunk: Optional[bytearray] = None
899
+ chunk_str = dict_.get("chunk")
900
+ if chunk_str is not None:
882
901
  if self.DATA_SEPARATOR:
883
902
  chunk_str = chunk_str.replace(self.DATA_SEPARATOR, "")
884
903
 
@@ -886,7 +905,16 @@ class Reader(BaseType):
886
905
  if chunk_str:
887
906
  for idx in range(0, len(chunk_str), 2):
888
907
  chunk.append(atoi(chunk_str[idx : idx + 2]))
889
- container.chunk = chunk
908
+
909
+ container = Container(
910
+ line_number=line_number,
911
+ address=address,
912
+ length=length,
913
+ type=record_type,
914
+ checksum=checksum,
915
+ chunk=chunk,
916
+ junk=junk,
917
+ )
890
918
 
891
919
  # Validate line
892
920
  self.check_line(container, format_type)
@@ -894,8 +922,8 @@ class Reader(BaseType):
894
922
  # Process data lines
895
923
  if self.is_data_line(container, format_type):
896
924
  if self.parseData(container, format_type):
897
- address = getattr(container, "address", 0)
898
- chunk = getattr(container, "chunk", bytearray())
925
+ address = (container.address if container.address is not None else 0) + self.base_address
926
+ chunk = container.chunk if container.chunk is not None else bytearray()
899
927
  self.stats.data_bytes[format_type] += len(chunk)
900
928
  section = Section(address, chunk)
901
929
  sections.append(section)
@@ -908,8 +936,8 @@ class Reader(BaseType):
908
936
  meta_data[format_type].append(
909
937
  MetaRecord(
910
938
  format_type=format_type,
911
- address=getattr(container, "address", None),
912
- chunk=getattr(container, "chunk", None),
939
+ address=container.address,
940
+ chunk=container.chunk,
913
941
  )
914
942
  )
915
943
  break # Pattern matched, stop trying formats
@@ -56,19 +56,6 @@ class Reader(hexfile.Reader):
56
56
  def __init__(self) -> None:
57
57
  """Initialize reader with address calculation state."""
58
58
  super().__init__()
59
- self.segmentAddress: int = 0
60
- self._address_calculator: Callable[[int], int] = self._default_address_calculator
61
-
62
- def _default_address_calculator(self, x: int) -> int:
63
- """Default address calculator (identity function).
64
-
65
- Args:
66
- x: Input address
67
-
68
- Returns:
69
- Same address unchanged
70
- """
71
- return x
72
59
 
73
60
  def check_line(self, line: Any, format_type: int) -> None:
74
61
  """Validate Intel HEX record checksum.
@@ -106,7 +93,7 @@ class Reader(hexfile.Reader):
106
93
  """
107
94
  return line.type == DATA
108
95
 
109
- def calculate_extended_address(self, line: Any, shift_by: int, name: str) -> None:
96
+ def calculate_base_address(self, line: Any, shift_by: int, name: str) -> None:
110
97
  """Calculate extended address from segment/linear address record.
111
98
 
112
99
  Args:
@@ -118,9 +105,7 @@ class Reader(hexfile.Reader):
118
105
  # Extract 16-bit segment value
119
106
  segment = (line.chunk[0] << 8) | line.chunk[1]
120
107
  line.add_processing_instruction(("segment", segment))
121
-
122
- # Update address calculator to add segment base
123
- self._address_calculator = partial(operator.add, segment << shift_by)
108
+ self.base_address = segment << shift_by
124
109
  self.debug(f"EXTENDED_{name.upper()}_ADDRESS: {segment:#X}")
125
110
  else:
126
111
  self.error(f"Bad Extended {name} Address at line #{line.line_number}.")
@@ -135,43 +120,36 @@ class Reader(hexfile.Reader):
135
120
  Note:
136
121
  Handles extended addressing, start addresses, and EOF records.
137
122
  """
138
- if line.type == DATA:
139
- # Apply extended address calculation
140
- line.address = self._address_calculator(line.address)
141
-
142
- elif line.type == EXTENDED_SEGMENT_ADDRESS:
123
+ if line.type == EXTENDED_SEGMENT_ADDRESS:
143
124
  # Extended segment address (20-bit: segment << 4)
144
- self.calculate_extended_address(line, 4, "Segment")
145
-
125
+ self.calculate_base_address(line, 4, "Segment")
146
126
  elif line.type == START_SEGMENT_ADDRESS:
147
127
  # Start segment address (CS:IP for x86)
148
128
  if len(line.chunk) == 4:
149
129
  cs = (line.chunk[0] << 8) | line.chunk[1]
150
130
  ip = (line.chunk[2] << 8) | line.chunk[3]
131
+ self.base_address = (cs << 4) + ip
151
132
  line.add_processing_instruction(("cs", cs))
152
133
  line.add_processing_instruction(("ip", ip))
153
134
  self.debug(f"START_SEGMENT_ADDRESS: {hex(cs)}:{hex(ip)}")
154
135
  else:
155
136
  self.error(f"Bad Segment Address at line #{line.line_number}.")
156
-
157
137
  elif line.type == EXTENDED_LINEAR_ADDRESS:
158
138
  # Extended linear address (32-bit: segment << 16)
159
- self.calculate_extended_address(line, 16, "Linear")
160
-
139
+ self.calculate_base_address(line, 16, "Linear")
161
140
  elif line.type == START_LINEAR_ADDRESS:
162
141
  # Start linear address (EIP for x86)
163
142
  if len(line.chunk) == 4:
164
143
  eip = (line.chunk[0] << 24) | (line.chunk[1] << 16) | (line.chunk[2] << 8) | line.chunk[3]
144
+ self.base_address = eip
165
145
  line.add_processing_instruction(("eip", eip))
166
146
  self.debug(f"START_LINEAR_ADDRESS: {hex(eip)}")
167
147
  else:
168
148
  self.error(f"Bad Linear Address at line #{line.line_number}.")
169
-
170
149
  elif line.type == EOF:
171
150
  # End of file - nothing to process
172
151
  pass
173
-
174
- else:
152
+ elif line.type != DATA:
175
153
  self.warn(f"Invalid record type [{line.type}] at line {line.line_number}")
176
154
 
177
155
 
@@ -640,6 +640,28 @@ class Image:
640
640
  """Write an ASAM string datatype (ASCII/UTF8/UTF16/UTF32)."""
641
641
  self._call_address_function("write_asam_string", addr, value, dtype, **kws)
642
642
 
643
+ def read_asam_numeric_array(
644
+ self,
645
+ addr: int,
646
+ length: int,
647
+ dtype: str,
648
+ byte_order: str = "MSB_LAST",
649
+ **kws: Any,
650
+ ) -> list[Union[int, float]]:
651
+ """Read an ASAM numeric array with ECU byte order semantics."""
652
+ return self._call_address_function("read_asam_numeric_array", addr, length, dtype, byte_order, **kws)
653
+
654
+ def write_asam_numeric_array(
655
+ self,
656
+ addr: int,
657
+ data: Iterable[Union[int, float]],
658
+ dtype: str,
659
+ byte_order: str = "MSB_LAST",
660
+ **kws: Any,
661
+ ) -> None:
662
+ """Write an ASAM numeric array with ECU byte order semantics."""
663
+ self._call_address_function("write_asam_numeric_array", addr, data, dtype, byte_order, **kws)
664
+
643
665
  def read_numeric_array(self, addr: int, length: int, dtype: str, **kws: Any) -> list[Union[int, float]]:
644
666
  """Read array of numeric values from image.
645
667
 
@@ -688,6 +710,18 @@ class Image:
688
710
  """
689
711
  self._call_address_function("write_ndarray", addr, array, order=order, **kws)
690
712
 
713
+ def write_asam_ndarray(
714
+ self,
715
+ addr: int,
716
+ array: Any,
717
+ dtype: str,
718
+ byte_order: str = "MSB_LAST",
719
+ order: Optional[str] = None,
720
+ **kws: Any,
721
+ ) -> None:
722
+ """Write a NumPy ndarray using ASAM datatype and ECU byte order semantics."""
723
+ self._call_address_function("write_asam_ndarray", addr, array, dtype, byte_order, order=order, **kws)
724
+
691
725
  def read_ndarray(
692
726
  self,
693
727
  addr: int,
@@ -715,6 +749,19 @@ class Image:
715
749
  """
716
750
  return self._call_address_function("read_ndarray", addr, length, dtype, shape, order, **kws)
717
751
 
752
+ def read_asam_ndarray(
753
+ self,
754
+ addr: int,
755
+ length: int,
756
+ dtype: str,
757
+ shape: Optional[tuple[int, ...]] = None,
758
+ order: Optional[str] = None,
759
+ byte_order: str = "MSB_LAST",
760
+ **kws: Any,
761
+ ) -> Any:
762
+ """Read a NumPy ndarray using ASAM datatype and ECU byte order semantics."""
763
+ return self._call_address_function("read_asam_ndarray", addr, length, dtype, shape, order, byte_order, **kws)
764
+
718
765
  def read_string(self, addr: int, encoding: str = "latin1", length: int = -1, **kws: Any) -> str:
719
766
  """Read string from image.
720
767
 
@@ -54,14 +54,6 @@ def main():
54
54
  default=False,
55
55
  help="Print filename including path",
56
56
  )
57
- parser.add_argument(
58
- "-j",
59
- "--join-sections",
60
- dest="join_section",
61
- action="store_true",
62
- default=False,
63
- help="Join adjacent sections",
64
- )
65
57
 
66
58
  args = parser.parse_args()
67
59
 
@@ -88,7 +80,7 @@ def main():
88
80
  print(f"Could not determine file type for '{hex_file}'.")
89
81
  sys.exit(1)
90
82
 
91
- img = load(file_type.lower(), hex_file, join=args.join_section)
83
+ img = load(file_type.lower(), hex_file)
92
84
  if args.print_filename:
93
85
  print(f"\nFile: {hex_file}")
94
86
  print("\nSections")
@@ -32,6 +32,9 @@ Architecture
32
32
  ├── read/write(addr, length) # Raw bytes
33
33
  ├── read_numeric/write_numeric(addr, dtype) # Single values
34
34
  ├── read_numeric_array/write_numeric_array() # Arrays
35
+ ├── read_asam_numeric/write_asam_numeric() # ASAM scalars
36
+ ├── read_asam_numeric_array/write_asam_numeric_array() # ASAM arrays
37
+ ├── read_asam_ndarray/write_asam_ndarray() # ASAM NumPy arrays
35
38
  ├── read_string/write_string() # Null-terminated strings
36
39
  └── read_ndarray/write_ndarray() # NumPy arrays
37
40
 
@@ -587,10 +590,24 @@ class Section:
587
590
  internal_dtype = ASAM_NUMERIC_DTYPES.get(normalized_dtype)
588
591
  if internal_dtype is None:
589
592
  raise TypeError(f"Unsupported ASAM datatype {asam_dtype!r}")
590
- if internal_dtype in ("uint8", "int8"):
591
- return internal_dtype
592
593
  return f"{internal_dtype}_{ASAM_ENDIAN_FOR_BYTEORDER[asam_byte_order]}"
593
594
 
595
+ def _numpy_dtype_from_internal(self, dtype: str) -> np.dtype:
596
+ type_, byte_order = self._verify_dtype(dtype)
597
+ return np.dtype(type_).newbyteorder(BYTEORDER[byte_order])
598
+
599
+ def _permute_asam_buffer(self, data: bytes, internal_dtype: str, byte_order: str) -> bytes:
600
+ type_name = internal_dtype.split("_")[0]
601
+ element_size = TYPE_SIZES.get(type_name, 0)
602
+ if element_size <= 1 or not data:
603
+ return data
604
+ if len(data) % element_size != 0:
605
+ raise ValueError("ASAM buffer length must be a multiple of the element size")
606
+ return b"".join(
607
+ self._permute_asam_bytes_for_read(data[idx : idx + element_size], byte_order)
608
+ for idx in range(0, len(data), element_size)
609
+ )
610
+
594
611
  def read(self, addr: int, length: int, **kws) -> bytes:
595
612
  """Read raw bytes from section at specified address.
596
613
 
@@ -763,7 +780,7 @@ class Section:
763
780
 
764
781
  fmt = self._getformat(internal_dtype)
765
782
  packed = struct.pack(fmt, value)
766
- permuted = self._permute_asam_bytes_for_read(packed, asam_byte_order)
783
+ permuted = self._permute_asam_buffer(packed, internal_dtype, asam_byte_order)
767
784
  return struct.unpack(fmt, permuted)[0]
768
785
 
769
786
  def write_asam_numeric(
@@ -782,9 +799,44 @@ class Section:
782
799
 
783
800
  fmt = self._getformat(internal_dtype)
784
801
  packed = struct.pack(fmt, value)
785
- permuted = self._permute_asam_bytes_for_write(packed, asam_byte_order)
802
+ permuted = self._permute_asam_buffer(packed, internal_dtype, asam_byte_order)
786
803
  self.write(addr, permuted)
787
804
 
805
+ def read_asam_numeric_array(
806
+ self,
807
+ addr: int,
808
+ length: int,
809
+ dtype: str,
810
+ byte_order: str = "MSB_LAST",
811
+ **kws,
812
+ ) -> Union[tuple[int, ...], tuple[float, ...]]:
813
+ asam_byte_order = self._resolve_asam_byteorder(byte_order)
814
+ internal_dtype = self._asam_numeric_dtype_to_internal(dtype, asam_byte_order)
815
+ fmt = self._getformat(internal_dtype, length)
816
+ data_size = struct.calcsize(fmt)
817
+ raw_data = self.read(addr, data_size, **kws)
818
+ permuted = self._permute_asam_buffer(raw_data, internal_dtype, asam_byte_order)
819
+ return struct.unpack(fmt, permuted)
820
+
821
+ def write_asam_numeric_array(
822
+ self,
823
+ addr: int,
824
+ data: Union[list[int], list[float]],
825
+ dtype: str,
826
+ byte_order: str = "MSB_LAST",
827
+ **kws,
828
+ ) -> None:
829
+ if not hasattr(data, "__iter__"):
830
+ raise TypeError("data must be iterable")
831
+
832
+ values = tuple(data)
833
+ asam_byte_order = self._resolve_asam_byteorder(byte_order)
834
+ internal_dtype = self._asam_numeric_dtype_to_internal(dtype, asam_byte_order)
835
+ fmt = self._getformat(internal_dtype, len(values))
836
+ packed = struct.pack(fmt, *values)
837
+ permuted = self._permute_asam_buffer(packed, internal_dtype, asam_byte_order)
838
+ self.write(addr, permuted, **kws)
839
+
788
840
  def read_asam_string(self, addr: int, dtype: str, length: int = -1, **kws) -> str:
789
841
  encoding = ASAM_STRING_ENCODINGS.get(dtype.strip().upper())
790
842
  if encoding is None:
@@ -920,6 +972,28 @@ class Section:
920
972
  else:
921
973
  self.data[offset : offset + data_size] = array.tobytes()
922
974
 
975
+ def write_asam_ndarray(
976
+ self,
977
+ addr: int,
978
+ array: np.ndarray,
979
+ dtype: str,
980
+ byte_order: str = "MSB_LAST",
981
+ order: str = None,
982
+ **kws,
983
+ ) -> None:
984
+ if not isinstance(array, np.ndarray):
985
+ raise TypeError("array must be of type numpy.ndarray.")
986
+
987
+ asam_byte_order = self._resolve_asam_byteorder(byte_order)
988
+ internal_dtype = self._asam_numeric_dtype_to_internal(dtype, asam_byte_order)
989
+ typed_array = np.asarray(array, dtype=self._numpy_dtype_from_internal(internal_dtype))
990
+ if order is not None and order == "F":
991
+ raw_data = fortran_array_to_buffer(array=typed_array)
992
+ else:
993
+ raw_data = typed_array.tobytes()
994
+ permuted = self._permute_asam_buffer(raw_data, internal_dtype, asam_byte_order)
995
+ self.write(addr, permuted, **kws)
996
+
923
997
  def read_ndarray(self, addr: int, length: int, dtype: str, shape: tuple = None, order: str = None, **kws) -> np.ndarray:
924
998
  """ """
925
999
  offset = addr - self.start_address
@@ -927,9 +1001,7 @@ class Section:
927
1001
  raise InvalidAddressError(f"read_ndarray(0x{addr:08x}) access out of bounds.")
928
1002
  if offset + length > self.length:
929
1003
  raise InvalidAddressError(f"read_ndarray(0x{addr:08x}) access out of bounds.")
930
- type_, byte_order = self._verify_dtype(dtype)
931
- dt = np.dtype(type_)
932
- dt = dt.newbyteorder(BYTEORDER.get(byte_order))
1004
+ dt = self._numpy_dtype_from_internal(dtype)
933
1005
  if order is not None and order == "F":
934
1006
  arr = fortran_array_from_buffer(arr=self.data[offset : offset + length], shape=shape, dtype=dt)
935
1007
  else:
@@ -937,6 +1009,27 @@ class Section:
937
1009
  arr = flat.reshape(shape) if shape else flat
938
1010
  return arr
939
1011
 
1012
+ def read_asam_ndarray(
1013
+ self,
1014
+ addr: int,
1015
+ length: int,
1016
+ dtype: str,
1017
+ shape: tuple = None,
1018
+ order: str = None,
1019
+ byte_order: str = "MSB_LAST",
1020
+ **kws,
1021
+ ) -> np.ndarray:
1022
+ asam_byte_order = self._resolve_asam_byteorder(byte_order)
1023
+ internal_dtype = self._asam_numeric_dtype_to_internal(dtype, asam_byte_order)
1024
+ dt = self._numpy_dtype_from_internal(internal_dtype)
1025
+ raw_data = self.read(addr, length, **kws)
1026
+ permuted = self._permute_asam_buffer(raw_data, internal_dtype, asam_byte_order)
1027
+ if order is not None and order == "F":
1028
+ return fortran_array_from_buffer(arr=permuted, shape=shape, dtype=dt)
1029
+
1030
+ flat = np.frombuffer(permuted, dtype=dt)
1031
+ return flat.reshape(shape) if shape else flat
1032
+
940
1033
  """
941
1034
  def write_timestamp():
942
1035
  pass
@@ -1029,11 +1122,10 @@ class LazySection(Section):
1029
1122
 
1030
1123
 
1031
1124
  def join_sections(sections: list[Section]) -> list[Section]:
1032
- """Join consecutive sections into contiguous blocks.
1125
+ """Join sections when their address ranges are compatible.
1033
1126
 
1034
- Merges sections that are adjacent in memory (where one section's end address
1035
- equals the next section's start address) into single sections. Non-consecutive
1036
- sections remain separate.
1127
+ Merges sections that are adjacent in memory or overlap with identical bytes in
1128
+ the shared range. Conflicting overlaps remain separate sections.
1037
1129
 
1038
1130
  The function sorts sections by start address before processing.
1039
1131
 
@@ -1041,7 +1133,7 @@ def join_sections(sections: list[Section]) -> list[Section]:
1041
1133
  sections: List of Section objects to join
1042
1134
 
1043
1135
  Returns:
1044
- New list of Section objects with consecutive sections merged
1136
+ New list of Section objects with compatible sections merged
1045
1137
 
1046
1138
  Raises:
1047
1139
  TypeError: If any element is not a Section instance
@@ -1068,20 +1160,46 @@ def join_sections(sections: list[Section]) -> list[Section]:
1068
1160
  This function is automatically called when creating an Image with
1069
1161
  ``join=True`` parameter.
1070
1162
  """
1071
- result_sections = []
1072
- sections.sort(key=attrgetter("start_address"))
1073
- prev_section = Section()
1163
+ result_sections: list[Section] = []
1074
1164
  for section in sections:
1075
1165
  if not isinstance(section, Section):
1076
1166
  raise TypeError("'{}' is not a 'Section' instance", section)
1077
- if section.start_address == prev_section.start_address + prev_section.length and result_sections:
1078
- last_segment = result_sections[-1]
1167
+
1168
+ sorted_sections = sorted(sections, key=attrgetter("start_address"))
1169
+
1170
+ for section in sorted_sections:
1171
+
1172
+ if not result_sections:
1173
+ result_sections.append(Section(section.start_address, section.data))
1174
+ continue
1175
+
1176
+ last_segment = result_sections[-1]
1177
+ last_end = last_segment.start_address + last_segment.length
1178
+ section_start = section.start_address
1179
+ section_end = section_start + section.length
1180
+
1181
+ if section_start > last_end:
1182
+ result_sections.append(Section(section.start_address, section.data))
1183
+ continue
1184
+
1185
+ if section_start == last_end:
1079
1186
  last_segment.data.extend(section.data)
1080
- else:
1081
- # Create a new section.
1187
+ continue
1188
+
1189
+ overlap_start = section_start
1190
+ overlap_end = min(last_end, section_end)
1191
+ overlap_length = overlap_end - overlap_start
1192
+ last_offset = overlap_start - last_segment.start_address
1193
+ section_offset = overlap_start - section_start
1194
+
1195
+ last_overlap = last_segment.data[last_offset : last_offset + overlap_length]
1196
+ section_overlap = section.data[section_offset : section_offset + overlap_length]
1197
+ if last_overlap != section_overlap:
1082
1198
  result_sections.append(Section(section.start_address, section.data))
1083
- prev_section = section
1084
- if result_sections:
1085
- return result_sections
1086
- else:
1087
- return []
1199
+ continue
1200
+
1201
+ if section_end > last_end:
1202
+ tail_offset = overlap_end - section_start
1203
+ last_segment.data.extend(section.data[tail_offset:])
1204
+
1205
+ return result_sections
@@ -95,7 +95,7 @@ class Reader(hexfile.Reader):
95
95
  (S9, "S9LLAAAACC"),
96
96
  )
97
97
 
98
- def load(self, fp: Any, join: bool = False, **kws: Any) -> Any:
98
+ def load(self, fp: Any, join: bool = True, **kws: Any) -> Any:
99
99
  """Load and parse S-Record file.
100
100
 
101
101
  Args: