184184from array import array
185185from collections import namedtuple
186186from copy import copy
187- from dataclasses import dataclass , field
187+ from dataclasses import dataclass
188188from functools import reduce
189189from operator import attrgetter , mul
190190from 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
812886def join_sections (sections : list [Section ]) -> list [Section ]:
813887 """Join consecutive sections into contiguous blocks.
0 commit comments