Source code for icdutil.addrrange

#
# MIT License
#
# Copyright (c) 2023 nbiotcloud
#
# 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.
#

"""Helper Class For Handling Address Ranges."""


import typing

from attrs import field, frozen
from humannum import Bytes, Hex
from mementos import mementos


[docs]@frozen(init=False, repr=False, str=False) class AddrRange(mementos): """Address range starting at `baseaddr` with `size` in bytes.""" baseaddr: Hex = field() size: Bytes = field() addrwidth: typing.Optional[int] = field() item: typing.Optional[typing.Any] = field() is_sub: bool = field() # pylint: disable=too-many-arguments def __init__( self, baseaddr: int, size: int, addrwidth: typing.Optional[int] = None, item: typing.Optional[typing.Any] = None, is_sub=False, ) -> None: """ Address range starting at `baseaddr` with `size` in bytes. >>> a = AddrRange(0x1000, 0x100) >>> a AddrRange(0x1000, '256 bytes') >>> b = AddrRange(0x1000, 0x100, item='B') >>> b AddrRange('B', 0x1000, '256 bytes') >>> c = AddrRange(0x1000, 0x100, is_sub=True) >>> c AddrRange(0x1000, '256 bytes', is_sub=True) >>> d = AddrRange(0x1000, 0x100, item='D', is_sub=True) >>> d AddrRange('D', 0x1000, '256 bytes', is_sub=True) The addrwidth just formats the address representation: >>> a = AddrRange(0x1000, 0x100, addrwidth=32) >>> a AddrRange(0x00001000, '256 bytes', addrwidth=32) >>> str(a) '0x00001000-0x000010FF(256 bytes)' >>> str(a.nextaddr) '0x00001100' Address ranges can be compared: >>> AddrRange(0x1000, 0x100) == AddrRange(0x1000, 0x100) True >>> AddrRange(0x1000, 0x100) == AddrRange(0x1000, 0x200) False Comparing an AddrRange against another type just returns False: >>> AddrRange(0x1000, 0x100) == 42 False Addresses can be checked whether they lie within the range: >>> 0x1008 in a True >>> 0x1400 in a False Address ranges can be iterated over: >>> for i in AddrRange(0x200, 6): ... print(i) 512 513 514 515 516 517 """ baseaddr = Hex(baseaddr, width=addrwidth) size = Bytes(size) # pylint: disable=no-member self.__attrs_init__(baseaddr, size, addrwidth, item, is_sub) @property def endaddr(self) -> Hex: """Hexvalue of end address of range.""" return Hex(self.baseaddr + self.size - 1, width=self.addrwidth) @property def nextaddr(self) -> Hex: """Hexvalue of first address after range.""" return Hex(self.endaddr + 1, width=self.addrwidth) def __str__(self): """Return String representation.""" it_ = f"{self.item!r}: " if self.item else "" return f"{it_}{self.baseaddr}-{self.endaddr}({self.size})" def __repr__(self): """Return extended representation.""" aw_ = f", addrwidth={self.addrwidth}" if self.addrwidth else "" it_ = f"{self.item!r}, " if self.item else "" is_ = ", is_sub=True" if self.is_sub else "" return f"AddrRange({it_}{self.baseaddr}, '{self.size}'{aw_}{is_})" def __eq__(self, other): if other.__class__ is AddrRange: return (self.addrwidth, self.baseaddr, self.size) == (other.addrwidth, other.baseaddr, other.size) return NotImplemented def __contains__(self, value): """Check wether `value` lies wirhin address range.""" return self.baseaddr <= value <= self.endaddr def __iter__(self): """Iterate over all addresses inside range.""" return iter(range(self.baseaddr, self.endaddr + 1))
[docs] def is_overlapping(self, other: "AddrRange") -> bool: """ Return `True` if `other` overlaps. >>> AddrRange(0x1000, '4 KB').is_overlapping(AddrRange(0x3000, '4 KB')) False >>> AddrRange(0x1000, '4 KB').is_overlapping(AddrRange(0x2000, '4 KB')) False >>> AddrRange(0x1000, '4 KB').is_overlapping(AddrRange(0x2000, 0x1)) False >>> AddrRange(0x1000, '4 KB').is_overlapping(AddrRange(0x1FFF, 0x1)) True >>> AddrRange(0x1000, '4 KB').is_overlapping(AddrRange(0x1000, '4 KB')) True >>> AddrRange(0x3000, '4 KB').is_overlapping(AddrRange(0x2000, '4 KB')) False >>> AddrRange(0x3000, '4 KB').is_overlapping(AddrRange(0x2FFF, 0x1)) False >>> AddrRange(0x3000, '4 KB').is_overlapping(AddrRange(0x3000, 0x1)) True >>> AddrRange(0x3000, '4 KB').is_overlapping(AddrRange(0x0000, 0x8000)) True """ if self.baseaddr < other.baseaddr: # other is to the right of self return self.endaddr >= other.baseaddr # other is to the left of self return self.baseaddr <= other.endaddr
[docs] def get_intersect(self, other: "AddrRange", strict: bool = False) -> typing.Optional["AddrRange"]: """ Return intersection of self and `other`. Args: other(AddrRange): The other AddrRange for intersection with `self`. Keyword Args: strict(bool): Raise `IntersectError` when True and there is no intersection between self and `other`. Absolute AddrRanges just lead to the intersection: >>> AddrRange(0x1000, '4 KB').get_intersect(AddrRange(0x2000, '4 KB')) >>> AddrRange(0x1000, '4 KB').get_intersect(AddrRange(0x2000, 0x1)) >>> AddrRange(0x1000, '4 KB').get_intersect(AddrRange(0x1FFF, 0x1)) AddrRange(0x1FFF, '1 byte') Remark: humandfriendly package is rounding 4095 bytes to 4 KB. >>> a = AddrRange(0x1000, '4 KB').get_intersect(AddrRange(0x1000, 0xFFF)) >>> a AddrRange(0x1000, '4 KB') >>> int(a.size) 4095 >>> AddrRange(0x1000, '4 KB').get_intersect(AddrRange(0x1000, '4 KB')) AddrRange(0x1000, '4 KB') >>> AddrRange(0x3000, '4 KB').get_intersect(AddrRange(0x2000, '4 KB')) >>> AddrRange(0x3000, '4 KB').get_intersect(AddrRange(0x2FFF, 0x1)) >>> AddrRange(0x3000, '4 KB').get_intersect(AddrRange(0x3000, 0x1)) AddrRange(0x3000, '1 byte') >>> AddrRange(0x3000, '4 KB').get_intersect(AddrRange(0x0000, 0x8000)) AddrRange(0x3000, '4 KB') If `strict` is set and there is no insersection `IntersectError` is raised: >>> AddrRange(0x1000, '4 KB').get_intersect(AddrRange(0x1000, '4 KB'), strict=True) AddrRange(0x1000, '4 KB') >>> AddrRange(0x1000, '4 KB').get_intersect( ... AddrRange(0x3000, '4 KB'), strict=True) # doctest: +NORMALIZE_WHITESPACE Traceback (most recent call last): ... icdutil.addrrange.IntersectError: No intersection between AddrRange(0x1000, '4 KB') \ and AddrRange(0x3000, '4 KB'). If only one of the AddrRange is absolute, the sub range is taken relative to the absolute. If `self` is a subrange, then the intersection is again a subrange w/r/t to the absolute range. >>> AddrRange(0xF0000000, '1 MB').get_intersect(AddrRange(0x2000, '4 KB', is_sub=True)) AddrRange(0xF0002000, '4 KB') >>> AddrRange(0x2000, '4 KB', is_sub=True).get_intersect(AddrRange(0xF0000000, '1 MB')) AddrRange(0x2000, '4 KB', is_sub=True) If there is no overlap b/w absolute range and sub-range at the offset, the result depends on the `strict` parameter: >>> AddrRange(0xF0000000, '1 KB').get_intersect(AddrRange(0x2000, '4 KB', is_sub=True)) >>> AddrRange(0xF0000000, '1 KB').get_intersect( ... AddrRange(0x2000, '4 KB', is_sub=True), strict=True) # doctest: +NORMALIZE_WHITESPACE Traceback (most recent call last): ... icdutil.addrrange.IntersectError: No intersection between AddrRange(0xF0000000, '1 KB') \ and AddrRange(0x2000, '4 KB', is_sub=True). If both are sub-ranges it also just leads to an intersection, but the result is a sub-range: >>> AddrRange(0x3000, '4 KB', is_sub=True).get_intersect(AddrRange(0x0000, 0x8000, is_sub=True)) AddrRange(0x3000, '4 KB', is_sub=True) The item and aaddrwidth of the intersection is always inherited from `self`. >>> AddrRange(0x2000, '1 MB', item='A', addrwidth=20).get_intersect( ... AddrRange(0x5000, '4 KB', item='B', addrwidth=24)) AddrRange('A', 0x05000, '4 KB', addrwidth=20) """ baseaddr = self.baseaddr is_sub = self.is_sub if self.is_sub == other.is_sub: # either both or none is sub-range isect_base = max(self.baseaddr, other.baseaddr) isect_end = min(self.endaddr, other.endaddr) self_base = baseaddr else: # one of them is a sub-range isect_base = baseaddr + other.baseaddr self_base = baseaddr if not is_sub else isect_base self_end = self_base + self.size - 1 other_end = isect_base + other.size - 1 if other.is_sub else other.endaddr isect_end = min(self_end, other_end) size = isect_end - isect_base + 1 if size > 0: strt = baseaddr + isect_base - self_base return AddrRange(strt, size, addrwidth=self.addrwidth, item=self.item, is_sub=is_sub) if not strict: return None raise IntersectError(f"No intersection between {self!r} and {other!r}.")
[docs] def get_difference(self, other: "AddrRange") -> typing.List["AddrRange"]: """ Get difference of `self` and `other`. Args: other(AddrRange): The other AddrRange for intersection with `self`. Absolute AddrRanges just lead to the difference (i.e. the rest of `self` after the overlapping part has been removed): >>> AddrRange(0x1000, '4 KB').get_difference(AddrRange(0x2000, '4 KB')) [AddrRange(0x1000, '4 KB')] >>> AddrRange(0x1000, '4 KB').get_difference(AddrRange(0x800, '12 KB')) [] >>> AddrRange(0x1000, '4 KB').get_difference(AddrRange(0x1FFF, 0x1)) [AddrRange(0x1000, '4 KB')] >>> AddrRange(0x1000, '4 KB').get_difference(AddrRange(0x1001, '8 KB')) [AddrRange(0x1000, '1 byte')] >>> AddrRange(0x1000, '4 KB').get_difference(AddrRange(0x1000, 0xFFF)) [AddrRange(0x1FFF, '1 byte')] >>> AddrRange(0x1000, '4 KB').get_difference(AddrRange(0x1800, 0x1)) [AddrRange(0x1000, '2 KB'), AddrRange(0x1801, '2 KB')] If both are sub-ranges it also just leads to a difference, but the result is a sub-range: >>> AddrRange(0x1000, '4 KB', is_sub=True).get_difference(AddrRange(0x2000, '4 KB', is_sub=True)) [AddrRange(0x1000, '4 KB', is_sub=True)] >>> AddrRange(0x1000, '4 KB', is_sub=True).get_difference(AddrRange(0x800, '12 KB', is_sub=True)) [] >>> AddrRange(0x1000, '4 KB', is_sub=True).get_difference(AddrRange(0x1FFF, 0x1, is_sub=True)) [AddrRange(0x1000, '4 KB', is_sub=True)] If only one of the AddrRange is absolute, the sub range is taken relative to the absolute. If `self` is a subrange, then the difference is again a subrange w/r/t to the absolute range. >>> AddrRange(0x1000, '4 KB').get_difference(AddrRange(0x2000, '4 KB', is_sub=True)) [AddrRange(0x1000, '4 KB')] >>> AddrRange(0x1000, '4 KB', is_sub=True).get_difference(AddrRange(0x2000, '4 KB')) [AddrRange(0x1000, '4 KB', is_sub=True)] >>> AddrRange(0x1000, '4 KB').get_difference(AddrRange(0x800, '2 KB', is_sub=True)) [AddrRange(0x1000, '2 KB')] >>> AddrRange(0x1000, '4 KB').get_difference(AddrRange(0x800, '1 KB', is_sub=True)) [AddrRange(0x1000, '2 KB'), AddrRange(0x1C00, '1 KB')] >>> AddrRange(0x1000, '4 KB', is_sub=True).get_difference(AddrRange(0x800, '4 KB')) [AddrRange(0x1000, '4 KB', is_sub=True)] >>> AddrRange(0x400, '4 KB', is_sub=True).get_difference(AddrRange(0xF0000800, '4 KB')) [AddrRange(0x1000, '1 KB', is_sub=True)] Regardless of the `other` range, the difference always inherits addrwidth, the item (if there is any) and the is_sub attribute: >>> AddrRange(0x1000, '4 KB', item='A').get_difference(AddrRange(0x1800, '4 KB', item='B')) [AddrRange('A', 0x1000, '2 KB')] >>> AddrRange(0x1000, '4 KB', addrwidth=16).get_difference(AddrRange(0x1800, '4 KB', addrwidth=18)) [AddrRange(0x1000, '2 KB', addrwidth=16)] """ res = [] is_sub = self.is_sub if is_sub == other.is_sub: # either both or none is sub-range self_base = self.baseaddr self_end = self.endaddr isect_base = max(self_base, other.baseaddr) isect_end = min(self_end, other.endaddr) else: # one of them is a sub-range baseaddr = self.baseaddr isect_base = baseaddr + other.baseaddr self_base = baseaddr if not is_sub else isect_base self_end = self_base + self.size - 1 other_end = isect_base + other.size - 1 if other.is_sub else other.endaddr isect_end = min(self_end, other_end) if isect_end < isect_base: # no overlap at all return [self] if isect_base == self_base and isect_end == self_end: # other completely overlaps self return [] if self_base < isect_base: # either both are sub or self is absolute res.append( AddrRange( self.baseaddr, isect_base - self_base, addrwidth=self.addrwidth, item=self.item, is_sub=is_sub ) ) if isect_end < self_end: # either both are sub or self is absolute strt = self.baseaddr + isect_end - self_base + 1 res.append(AddrRange(strt, self_end - isect_end, addrwidth=self.addrwidth, item=self.item, is_sub=is_sub)) return res
[docs]class IntersectError(RuntimeError): """AddrRange Intersection Error."""