Source code for caterpillar.fields.pointer

# Copyright (C) MatrixEditor 2023-2026
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

from typing import Any, Final, Generic, TypeVar
from typing_extensions import override

from caterpillar.byteorder import Arch
from caterpillar.exception import DelegationError, StructException
from caterpillar.context import CTX_STREAM, CTX_FIELD, CTX_ARCH, CTX_SEQ
from caterpillar.options import Flag
from caterpillar._common import WithoutContextVar
from caterpillar.shared import getstruct
from caterpillar.abc import _ContextLambda, _ContextLike, _StructLike, _StreamType, _IT

from ._mixin import FieldStruct
from .common import uint16, uint24, uint32, uint64, uint8
from .common import int16, int32, int64, int24, int8
from .common import UInt, Int

_PtrValueT = TypeVar("_PtrValueT")


PTR_STRICT: Flag[None] = Flag("pointer.strict-mode")


[docs] class pointer(Generic[_PtrValueT], int): """ A custom integer subclass representing a pointer to another struct within the stream. :ivar Any obj: The associated object, if any. """ obj: _PtrValueT | None # pyright: ignore[reportUninitializedInstanceVariable] @override def __repr__(self) -> str: result = super().__repr__() if self.obj is not None: result = f"<{type(self.obj).__name__}* {hex(self)}>" return result def get(self): return self.obj
[docs] class Pointer(FieldStruct[int, pointer[_PtrValueT]]): """ A struct that represents a pointer to another struct within the stream. :ivar model: An optional configurable model to parse at the offset position. :ivar struct: The configured struct to use. """ __slots__: tuple[str, ...] = ("model", "struct") def __init__( self, struct: _StructLike[int, int] | _ContextLambda[_StructLike[int, int]], model: _StructLike[_PtrValueT, _PtrValueT] | None = None, ) -> None: self.struct: _StructLike[int, int] | _ContextLambda[_StructLike[int, int]] = ( struct ) self.model: _StructLike[_PtrValueT, _PtrValueT] | None = ( getstruct(model, model) if model is not None else None ) def __mul__(self, model: _StructLike[_IT, _IT]) -> "Pointer[_IT]": """ Create a new Pointer with a specified model. :param model: The model to use. :return: A new Pointer with the specified model. :rtype: Pointer """ return self.__class__(self.struct, model) # pyright: ignore[reportReturnType] def __type__(self): """ Get the type associated with the Pointer. :return: The type associated with the Pointer. :rtype: type """ return pointer def __size__(self, context: _ContextLike) -> int: """ Get the size of the Pointer struct. :param context: The context for size calculation. :return: The size of the Pointer struct. :rtype: int """ struct = self.struct if callable(self.struct): struct = self.struct(context) return struct.__size__(context)
[docs] @override def unpack_single(self, context: _ContextLike) -> pointer[_PtrValueT]: """ Unpack a single value using the Pointer struct. :param context: The context for unpacking. :return: The unpacked value. """ # fmt: off struct = self.struct if callable(struct): struct = self.struct(context) # pyright: ignore[reportCallIssue] stream: _StreamType = context[CTX_STREAM] # pyright: ignore[reportAny] start = stream.tell() with WithoutContextVar(context, CTX_SEQ, False): value: int = struct.__unpack__(context) # cleanup before further parsing value: int = self._clean(value, context) if self.model is None: return self._create(value, start, None, context) offset: int = self._to_offset(value, start, context) model_obj = None if offset != 0: # using an if-statement here reduces time overhead # of this branch fallback: int = stream.tell() try: stream.seek(offset) model_obj = self.model.__unpack__(context) except StructException as exc: if context[CTX_FIELD].has_flag(PTR_STRICT): raise DelegationError( "Could not parse model!", context ) from exc stream.seek(fallback) return self._create(value, start, model_obj, context)
[docs] @override def pack_single(self, obj: int, context: _ContextLike) -> None: """ Pack a single value using the Pointer struct. :param obj: The value to pack. :param context: The context for packing. """ # fmt: off struct = self.struct if callable(struct): struct = self.struct(context) # pyright: ignore[reportCallIssue] with WithoutContextVar(context, CTX_SEQ, False): struct.__pack__(int(obj), context)
def _to_offset(self, value: int, start: int, context: _ContextLike) -> int: """ Convert the pointer value to an offset. :param value: The pointer value. :param start: The starting position in the stream. :param context: The context for the conversion. :return: The offset value. :rtype: int """ return value def _clean(self, value: int, context: _ContextLike) -> int: """ Clean the pointer value. :param value: The pointer value. :param context: The context for cleaning. :return: The cleaned pointer value. """ return value def _create( self, value: int, start: int, model_obj: _PtrValueT | None, context: _ContextLike, ) -> pointer[_PtrValueT]: """ Create a new pointer object. :param value: The pointer value. :param start: The starting position in the stream. :param model_obj: The object parsed from the model. :param context: The context for creation. :return: The created pointer object. """ ptr: pointer[_PtrValueT] = pointer(value) setattr(ptr, "obj", model_obj) return ptr
UNSIGNED_POINTER_TYS: dict[int, _StructLike[int, int]] = { x.__bits__: x for x in [uint8, uint16, uint24, uint32, uint64] } SIGNED_POINTER_TYS: dict[int, _StructLike[int, int]] = { x.__bits__: x for x in [int8, int16, int24, int32, int64] }
[docs] def uintptr_fn(context: _ContextLike) -> _StructLike[int, int]: """ Generator function to decide which struct to use as the pointer type based on the current architecture. :param _ContextLike context: The input context. :return: The struct to use. :rtype: _StructLike """ arch: Arch = context._root.get(CTX_ARCH, context[CTX_FIELD].arch) return UNSIGNED_POINTER_TYS.get(arch.ptr_size, UInt(arch.ptr_size))
[docs] def intptr_fn(context: _ContextLike) -> _StructLike[int, int]: """ Generator function to decide which struct to use as the pointer type based on the current architecture. :param _ContextLike context: The input context. :return: The struct to use. :rtype: _StructLike """ arch: Arch = context._root.get(CTX_ARCH, context[CTX_FIELD].arch) return SIGNED_POINTER_TYS.get(arch.ptr_size, Int(arch.ptr_size))
uintptr: Final[Pointer[Any]] = Pointer(uintptr_fn) intptr: Final[Pointer[Any]] = Pointer(intptr_fn)
[docs] class relative_pointer(pointer[_PtrValueT]): """ A custom integer subclass representing a relative pointer to another struct within the stream. :ivar obj: The associated object, if any. :ivar base: The base offset. :ivar absolute: The absolute offset. """ base: int # pyright: ignore[reportUninitializedInstanceVariable] @property def absolute(self) -> int: """ Get the absolute offset. :return: The absolute offset. :rtype: int """ return self + self.base
[docs] class RelativePointer(Pointer[_PtrValueT]): """ A struct that represents a relative pointer to another struct within the stream. """ @override def __type__(self): """ Get the type associated with the RelativePointer. :return: The type associated with the RelativePointer. :rtype: type """ if self.model is not None: return f"relative_pointer[{self.model.__type__().__name__}]" return relative_pointer @override def _to_offset(self, value: int, start: int, context: _ContextLike) -> int: """ Convert the relative pointer value to an offset. :param value: The relative pointer value. :param start: The starting position in the stream. :param context: The context for the conversion. :return: The offset value. :rtype: int """ return start + value @override def _create( self, value: int, start: int, model_obj: _PtrValueT | None, context: _ContextLike, ) -> pointer[_PtrValueT]: """ Create a new relative pointer object. :param value: The relative pointer value. :param start: The starting position in the stream. :param model_obj: The object parsed from the model. :param context: The context for creation. :return: The created relative pointer object. """ ptr = super()._create(value, start, model_obj, context) setattr(ptr, "base", start) return ptr
offintptr: Final[RelativePointer[Any]] = RelativePointer(intptr_fn) offuintptr: Final[RelativePointer[Any]] = RelativePointer(uintptr_fn)