Skip to content

Commit 43aaf31

Browse files
committed
Update section handling
1 parent f031234 commit 43aaf31

8 files changed

Lines changed: 160 additions & 42 deletions

File tree

objutils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
The first parameter is always the codec name.
1414
"""
1515

16-
__version__ = "0.9.0"
16+
__version__ = "0.9.1"
1717

1818
__all__ = [
1919
"Image",

objutils/ieee695.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -524,12 +524,12 @@ def onE2(self):
524524
elif discr == ASG:
525525
delim = self.readByte(self.fpos)
526526
if delim != 0xBE:
527-
pass # todo: FormatError!!!
527+
raise hexfile.FormatError(f"Expected 0xBE, got 0x{delim:02X} at 0x{self.fpos:X}")
528528
executionStartingAddr = self.readNumber(self.fpos)
529529
self.logger.debug(f"STARTING-ADDRESS: 0x{executionStartingAddr:04X}")
530530
delim = self.readByte(self.fpos)
531531
if delim != 0xBF:
532-
pass # todo: FormatError!!!
532+
raise hexfile.FormatError(f"Expected 0xBF, got 0x{delim:02X} at 0x{self.fpos:X}")
533533
self.info.executionStartingAddr = executionStartingAddr
534534
else:
535535
raise NotImplementedError(hex(discr))
@@ -587,7 +587,7 @@ def onF1(self):
587587
elif attrDef == 19:
588588
pass
589589
else:
590-
pass # todo: FormatError!!!
590+
raise hexfile.FormatError(f"Invalid symbol attribute at 0x{self.fpos:X}")
591591
## n4 =self.readNumber(self.fpos) # If n2 is non-zero, number of elements in the symbol type specified in n2
592592
if symbolTypeIndex != 0:
593593
numberOfElements = self.readNumber(self.fpos)
@@ -873,7 +873,7 @@ def onTY(self):
873873
""""""
874874
typeIndex = self.readNumber(self.fpos) # noqa: F841
875875
if self.readByte(self.fpos) != 0xCE:
876-
pass # todo: raise FormatError!!
876+
raise hexfile.FormatError(f"Expected 0xCE at 0x{self.fpos:X}")
877877
localNameIndex = self.readNumber(self.fpos) # noqa: F841
878878
values = []
879879
while True:

objutils/image.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,8 @@ def __init__(
421421
sections = list(sections)
422422
else:
423423
raise TypeError(f"Argument section is of wrong type {sections!r}")
424+
for section in sections:
425+
section._parent_image = self
424426
self._sections = sorted(sections, key=attrgetter("start_address"))
425427
self._join = join
426428
if join:
@@ -832,6 +834,17 @@ def join_sections(self) -> None:
832834
into single sections, reducing fragmentation and improving efficiency.
833835
"""
834836
self._sections = join_sections(self._sections)
837+
for section in self._sections:
838+
section._parent_image = self
839+
840+
def _validate_address_change(self, section: Section, new_address: int) -> None:
841+
"""Validate that changing a section's address doesn't cause overlaps."""
842+
# Temporary remove section to check against others
843+
others = [s for s in self._sections if s is not section]
844+
for s in others:
845+
# Check if new range [new_address, new_address + len(section)) overlaps with s
846+
if not (new_address + len(section) <= s.start_address or new_address >= s.start_address + len(s)):
847+
raise InvalidAddressError(f"New address 0x{new_address:08x} causes overlap with section at 0x{s.start_address:08x}")
835848

836849
def split(self, at: Optional[int] = None, equal_parts: Optional[int] = None, remap: Optional[bool] = None) -> None:
837850
"""Split image into multiple parts.

objutils/scripts/oj_hex_info.py

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,31 +27,17 @@
2727
import sys
2828
from os import path
2929

30-
from objutils import load
30+
from objutils import load, probe
3131

3232

3333
def main():
3434
parser = argparse.ArgumentParser(description="Displays informations about HEX files.")
3535
parser.add_argument(
36-
"file_type",
37-
help="file type",
38-
choices=[
39-
"ash",
40-
"cosmac",
41-
"emon52",
42-
"etek",
43-
"fpc",
44-
"ihex",
45-
"mostec",
46-
"rca",
47-
"shf",
48-
"sig",
49-
"srec",
50-
"tek",
51-
"titxt",
52-
],
36+
"file_or_type",
37+
nargs="?",
38+
help="file type or HEX file (if type is omitted)",
5339
)
54-
parser.add_argument("hex_file", help="HEX file")
40+
parser.add_argument("hex_file", nargs="?", help="HEX file")
5541
parser.add_argument(
5642
"-d",
5743
"--dump",
@@ -79,15 +65,32 @@ def main():
7965

8066
args = parser.parse_args()
8167

82-
if not path.exists(args.hex_file):
83-
print(f"File '{args.hex_file}' does not exist.")
68+
if args.hex_file is None:
69+
if args.file_or_type is None:
70+
parser.error("too few arguments")
71+
# Only one positional argument given: it's the hex_file, type is to be probed.
72+
hex_file = args.file_or_type
73+
file_type = None
74+
else:
75+
# Two positional arguments given: type and file.
76+
file_type = args.file_or_type
77+
hex_file = args.hex_file
78+
79+
if not path.exists(hex_file):
80+
print(f"File '{hex_file}' does not exist.")
81+
sys.exit(1)
82+
83+
if file_type is None:
84+
with open(hex_file, "rb") as f:
85+
file_type = probe(f)
86+
87+
if file_type is None:
88+
print(f"Could not determine file type for '{hex_file}'.")
8489
sys.exit(1)
8590

86-
img = load(args.file_type.lower(), args.hex_file)
91+
img = load(file_type.lower(), hex_file, join=args.join_section)
8792
if args.print_filename:
88-
print(f"\nFile: {args.hex_file}")
89-
if args.join_section:
90-
img.join_sections()
93+
print(f"\nFile: {hex_file}")
9194
print("\nSections")
9295
print("--------\n")
9396
print("Num Address Length")

objutils/section.py

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@
184184
from array import array
185185
from collections import namedtuple
186186
from copy import copy
187-
from dataclasses import dataclass, field
187+
from dataclasses import dataclass
188188
from functools import reduce
189189
from operator import attrgetter, mul
190190
from typing import Any, TextIO, Union
@@ -435,15 +435,32 @@ class Section:
435435
are equal if they have the same start address, data, and name.
436436
"""
437437

438-
start_address: int = field(hash=True, compare=True, default=0)
439-
data: bytearray = field(default_factory=bytearray, compare=True, hash=True)
440-
name: str = field(default="", compare=True, hash=True)
441-
442-
def __post_init__(self):
438+
def __init__(self, start_address: int = 0, data: Any = None, name: str = ""):
439+
self._start_address = start_address
440+
self.data = _data_converter(data if data is not None else bytearray())
441+
self.name = name
442+
self._parent_image = None
443443
self.repr = reprlib.Repr()
444444
self.repr.maxstring = 64
445445
self.repr.maxother = 64
446-
self.data = _data_converter(self.data)
446+
447+
@property
448+
def start_address(self) -> int:
449+
return self._start_address
450+
451+
@start_address.setter
452+
def start_address(self, value: int) -> None:
453+
if not isinstance(value, int):
454+
raise TypeError("start_address must be of type int")
455+
if value < 0:
456+
raise ValueError("start_address must be >= 0")
457+
if hasattr(self, "_parent_image") and self._parent_image:
458+
self._parent_image._validate_address_change(self, value)
459+
self._start_address = value
460+
461+
def __post_init__(self):
462+
# We handle initialization in __init__ now.
463+
pass
447464

448465
def __iter__(self):
449466
yield self
@@ -791,7 +808,7 @@ def __repr__(self) -> str:
791808
return "Section(address = 0X{:08X}, length = {:d}, data = {})".format(
792809
self.start_address,
793810
self.length,
794-
self.repr.repr(memoryview(self.data).tobytes()),
811+
self.repr.repr(bytes(self.data)),
795812
)
796813

797814
def __len__(self) -> int:
@@ -808,6 +825,63 @@ def __contains__(self, addr) -> bool:
808825
def address(self) -> int: # Alias
809826
return self.start_address
810827

828+
@address.setter
829+
def address(self, value: int) -> None:
830+
self.start_address = value
831+
832+
833+
class LazySection(Section):
834+
"""Memory-mapped Section for large binary files.
835+
836+
LazySection uses `mmap` to map a file into memory instead of loading
837+
everything into RAM. This is more efficient for very large files.
838+
"""
839+
840+
def __init__(self, start_address: int, filename: str, offset: int = 0, length: int = -1, name: str = ""):
841+
import mmap
842+
import os
843+
844+
self._start_address = start_address
845+
self.name = name
846+
self.filename = filename
847+
self._file = open(filename, "rb")
848+
if length == -1:
849+
length = os.path.getsize(filename) - offset
850+
851+
self.data = mmap.mmap(self._file.fileno(), length, offset=offset, access=mmap.ACCESS_READ)
852+
self.__post_init__()
853+
854+
def __post_init__(self):
855+
super().__post_init__()
856+
857+
def write(self, addr: int, data: bytes, **kws) -> None:
858+
raise NotImplementedError("LazySection is read-only")
859+
860+
def write_numeric(self, addr: int, value: Union[int, float], dtype: str, **kws) -> None:
861+
raise NotImplementedError("LazySection is read-only")
862+
863+
def write_numeric_array(self, addr: int, data: Union[list[int], list[float]], dtype: str, **kws) -> None:
864+
raise NotImplementedError("LazySection is read-only")
865+
866+
def write_string(self, addr: int, value: str, encoding: str = "latin1", **kws):
867+
raise NotImplementedError("LazySection is read-only")
868+
869+
def write_ndarray(self, addr: int, array: np.ndarray, order: str = None, **kws) -> None:
870+
raise NotImplementedError("LazySection is read-only")
871+
872+
def __del__(self):
873+
"""Close memory-mapped file when section is destroyed."""
874+
try:
875+
if hasattr(self, "data") and self.data:
876+
self.data.close()
877+
except (AttributeError, ValueError):
878+
pass
879+
try:
880+
if hasattr(self, "_file") and self._file:
881+
self._file.close()
882+
except (AttributeError, ValueError):
883+
pass
884+
811885

812886
def join_sections(sections: list[Section]) -> list[Section]:
813887
"""Join consecutive sections into contiguous blocks.

objutils/tests/test_hexfile.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python
22
import unittest
33

4-
from objutils import hexfile, loads
4+
from objutils import hexfile, loads, dumps, Section
55
from objutils.image import Image
66

77

@@ -28,6 +28,34 @@ def testRaisesError(self):
2828
# self.assertRaises(hexfile.InvalidRecordChecksumError, loads, "srec", b'S110000048656C6C6F20776F726C642166')
2929
pass
3030

31+
def test_reader_join_flag_preserves_sections(self):
32+
"""Ensure Reader join flag keeps original section boundaries."""
33+
sec_a = Section(0xFFE0, b"\x01\x02\x03\x04\x05\x06\x07\x08")
34+
sec_b = Section(0xFFE8, b"\x11\x12\x13\x14\x15\x16\x17\x18")
35+
img = Image([sec_a, sec_b], join=False)
36+
37+
ihex_bytes = dumps("ihex", img)
38+
roundtrip = loads("ihex", ihex_bytes, join=False)
39+
40+
self.assertEqual(len(roundtrip.sections), 2)
41+
self.assertEqual(roundtrip.sections[0].start_address, 0xFFE0)
42+
self.assertEqual(roundtrip.sections[0].data, sec_a.data)
43+
self.assertEqual(roundtrip.sections[1].start_address, 0xFFE8)
44+
self.assertEqual(roundtrip.sections[1].data, sec_b.data)
45+
46+
def test_reader_defaults_to_no_join(self):
47+
"""Default reader should preserve sections without merging."""
48+
sec_a = Section(0xFFE0, b"\x01" * 8)
49+
sec_b = Section(0xFFE8, b"\x02" * 8)
50+
img = Image([sec_a, sec_b], join=False)
51+
52+
ihex_bytes = dumps("ihex", img)
53+
roundtrip = loads("ihex", ihex_bytes)
54+
55+
self.assertEqual(len(roundtrip.sections), 2)
56+
self.assertEqual(roundtrip.sections[0].length, 8)
57+
self.assertEqual(roundtrip.sections[1].length, 8)
58+
3159

3260
def main():
3361
unittest.main()

objutils/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""objutils version module"""
22

3-
__version__ = "0.9.0"
3+
__version__ = "0.9.1"

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "objutils"
3-
version = "0.9.0"
3+
version = "0.9.1"
44
description = "Objectfile library for Python"
55
authors = ["Christoph Schueler <cpu12.gems@googlemail.com>"]
66
license = "GPLv2"
@@ -121,7 +121,7 @@ skip = ["*_i686", "*-musllinux*"]
121121
build-frontend = "build"
122122

123123
[tool.bumpver]
124-
current_version = "0.9.0"
124+
current_version = "0.9.1"
125125
version_pattern = "MAJOR.MINOR.PATCH"
126126
commit_message = "bump version {old_version} -> {new_version}"
127127
commit = true

0 commit comments

Comments
 (0)