Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions libdestruct/backing/fake_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,26 @@

from libdestruct.backing.resolver import Resolver

_PAGE_SIZE = 0x1000
_ZERO_PAGE = b"\x00" * _PAGE_SIZE


class FakeResolver(Resolver):
"""A class that can resolve elements in a simulated memory storage."""

def __init__(self: FakeResolver, memory: dict | None = None, address: int | None = 0, endianness: str = "little") -> None:

Check failure on line 18 in libdestruct/backing/fake_resolver.py

View workflow job for this annotation

GitHub Actions / lint (3.12)

ruff (E501)

libdestruct/backing/fake_resolver.py:18:121: E501 Line too long (126 > 120)

Check failure on line 18 in libdestruct/backing/fake_resolver.py

View workflow job for this annotation

GitHub Actions / lint (3.12)

ruff (E501)

libdestruct/backing/fake_resolver.py:18:121: E501 Line too long (126 > 120)
"""Initializes a basic fake resolver."""
self.memory = memory if memory is not None else {}
self._memory = memory if memory is not None else {}
self.address = address
self.parent = None
self.offset = None
self.endianness = endianness

@property
def memory(self: FakeResolver) -> dict:
"""The backing page dict. Read-only — mutate in place instead of reassigning."""
return self._memory

def resolve_address(self: FakeResolver) -> int:
"""Resolves self's address, mainly used by children to determine their own address."""
if self.address is not None:
Expand Down Expand Up @@ -48,11 +56,11 @@
result = b""

while size:
page = self.memory.get(page_address, b"\x00" * 0x1000)
page_size = min(size, 0x1000 - page_offset)
page = self.memory.get(page_address, _ZERO_PAGE)
page_size = min(size, _PAGE_SIZE - page_offset)
result += page[page_offset : page_offset + page_size]
size -= page_size
page_address += 0x1000
page_address += _PAGE_SIZE
page_offset = 0

return result
Expand All @@ -65,11 +73,11 @@
page_offset = address & 0xFFF

while size:
page = self.memory.get(page_address, b"\x00" * 0x1000)
page_size = min(size, 0x1000 - page_offset)
page = self.memory.get(page_address, _ZERO_PAGE)
page_size = min(size, _PAGE_SIZE - page_offset)
page = page[:page_offset] + value[:page_size] + page[page_offset + page_size :]
self.memory[page_address] = page
size -= page_size
value = value[page_size:]
page_address += 0x1000
page_address += _PAGE_SIZE
page_offset = 0
7 changes: 6 additions & 1 deletion libdestruct/backing/memory_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@
class MemoryResolver(Resolver):
"""A class that can resolve itself to a value in a referenced memory storage."""

def __init__(self: MemoryResolver, memory: MutableSequence, address: int | None, endianness: str = "little") -> None:

Check failure on line 20 in libdestruct/backing/memory_resolver.py

View workflow job for this annotation

GitHub Actions / lint (3.12)

ruff (E501)

libdestruct/backing/memory_resolver.py:20:121: E501 Line too long (121 > 120)

Check failure on line 20 in libdestruct/backing/memory_resolver.py

View workflow job for this annotation

GitHub Actions / lint (3.12)

ruff (E501)

libdestruct/backing/memory_resolver.py:20:121: E501 Line too long (121 > 120)
"""Initializes a basic memory resolver."""
self.memory = memory
self._memory = memory
self.address = address
self.parent = None
self.offset = None
self.endianness = endianness

@property
def memory(self: MemoryResolver) -> MutableSequence:
"""The backing memory buffer. Read-only — mutate in place instead of reassigning."""
return self._memory

def resolve_address(self: MemoryResolver) -> int:
"""Resolves self's address, mainly used by childs to determine their own address."""
if self.address is not None:
Expand Down
15 changes: 15 additions & 0 deletions libdestruct/c/c_integer_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,18 @@ class c_ulong(_c_integer):

signed: bool = False
"""Whether the long is signed."""


_SIGNED_INTEGER_BY_SIZE: dict[int, type[_c_integer]] = {
1: c_char,
2: c_short,
4: c_int,
8: c_long,
}


def signed_integer_for_size(size: int) -> type[_c_integer]:
"""Return the signed C integer type for the given byte size (1, 2, 4, or 8)."""
if size not in _SIGNED_INTEGER_BY_SIZE:
raise ValueError("The size of the field must be 1, 2, 4, or 8 bytes.")
return _SIGNED_INTEGER_BY_SIZE[size]
5 changes: 3 additions & 2 deletions libdestruct/c/c_str.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ def count(self: c_str) -> int:

def get(self: c_str, index: int = -1) -> bytes:
"""Return the character at the given index."""
if (index != -1 and index < 0) or index >= self.count():
length = self.count()
if (index != -1 and index < 0) or index >= length:
raise IndexError("String index out of range.")

if index == -1:
return self.resolver.resolve(self.count(), 0)
return self.resolver.resolve(length, 0)

return bytes([self.resolver.resolve(index + 1, 0)[-1]])

Expand Down
17 changes: 2 additions & 15 deletions libdestruct/common/enum/int_enum_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from typing import TYPE_CHECKING

from libdestruct.c.c_integer_types import c_char, c_int, c_long, c_short
from libdestruct.c.c_integer_types import signed_integer_for_size
from libdestruct.common.enum.enum import enum
from libdestruct.common.enum.enum_field import EnumField

Expand Down Expand Up @@ -43,20 +43,7 @@ def __init__(
self.backing_type = backing_type
return

if not 0 < size <= 8:
raise ValueError("The size of the field must be between 1 and 8 bytes.")

match size:
case 1:
self.backing_type = c_char
case 2:
self.backing_type = c_short
case 4:
self.backing_type = c_int
case 8:
self.backing_type = c_long
case _:
raise ValueError("The size of the field must be a power of 2.")
self.backing_type = signed_integer_for_size(size)

def inflate(self: IntEnumField, resolver: Resolver) -> int:
"""Inflate the field.
Expand Down
17 changes: 2 additions & 15 deletions libdestruct/common/flags/int_flag_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from typing import TYPE_CHECKING

from libdestruct.c.c_integer_types import c_char, c_int, c_long, c_short
from libdestruct.c.c_integer_types import signed_integer_for_size
from libdestruct.common.flags.flags import flags
from libdestruct.common.flags.flags_field import FlagsField

Expand Down Expand Up @@ -36,20 +36,7 @@ def __init__(
self.backing_type = backing_type
return

if not 0 < size <= 8:
raise ValueError("The size of the field must be between 1 and 8 bytes.")

match size:
case 1:
self.backing_type = c_char
case 2:
self.backing_type = c_short
case 4:
self.backing_type = c_int
case 8:
self.backing_type = c_long
case _:
raise ValueError("The size of the field must be a power of 2.")
self.backing_type = signed_integer_for_size(size)

def inflate(self: IntFlagField, resolver: Resolver) -> flags:
"""Inflate the field."""
Expand Down
4 changes: 4 additions & 0 deletions libdestruct/common/obj.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ def _compare_value(self: obj, other: object) -> tuple[object, object] | None:
return self_val, other
return None

# Restore identity hashing — Python blanks __hash__ when __eq__ is defined.
# Equality is value-based but hash is identity, so {a, b} won't dedupe equal values.
__hash__ = object.__hash__

def __eq__(self: obj, other: object) -> bool:
"""Return whether the object is equal to the given value."""
pair = self._compare_value(other)
Expand Down
35 changes: 16 additions & 19 deletions libdestruct/common/ptr/ptr.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ def __init__(self: ptr, resolver: Resolver, wrapper: type | None = None) -> None
"""
super().__init__(resolver)
self.wrapper = wrapper
self._cached_unwrap: obj | bytes | None = None
self._cache_valid: bool = False
self._cached_unwrap: obj | None = None
self._cached_address: int | None = None
self._cached_length: int | None = None

def get(self: ptr) -> int:
Expand All @@ -82,7 +82,7 @@ def _set(self: ptr, value: int) -> None:
def invalidate(self: ptr) -> None:
"""Clear the cached unwrap result."""
self._cached_unwrap = None
self._cache_valid = False
self._cached_address = None
self._cached_length = None

def unwrap(self: ptr, length: int | None = None) -> obj | bytes:
Expand All @@ -91,22 +91,22 @@ def unwrap(self: ptr, length: int | None = None) -> obj | bytes:
Args:
length: The length of the object in memory this points to.
"""
if self._cache_valid and self._cached_length == length:
return self._cached_unwrap

address = self.get()

if self.wrapper:
if length:
raise ValueError("Length is not supported when unwrapping a pointer to a wrapper object.")

result = self.wrapper(self.resolver.absolute_from_own(address))
else:
if not self.wrapper:
# Bytes are a snapshot; never cache — always read live.
target_resolver = self.resolver.absolute_from_own(address)
result = target_resolver.resolve(length if length is not None else 1, 0)
return target_resolver.resolve(length if length is not None else 1, 0)

if length:
raise ValueError("Length is not supported when unwrapping a pointer to a wrapper object.")

if self._cached_unwrap is not None and self._cached_address == address and self._cached_length == length:
return self._cached_unwrap

result = self.wrapper(self.resolver.absolute_from_own(address))
self._cached_unwrap = result
self._cache_valid = True
self._cached_address = address
self._cached_length = length
return result

Expand All @@ -116,9 +116,6 @@ def try_unwrap(self: ptr, length: int | None = None) -> obj | bytes | None:
Args:
length: The length of the object in memory this points to.
"""
if self._cache_valid and self._cached_length == length:
return self._cached_unwrap

address = self.get()

try:
Expand Down Expand Up @@ -152,12 +149,12 @@ def _element_size(self: ptr) -> int:
def __add__(self: ptr, n: int) -> ptr:
"""Return a new pointer advanced by n elements."""
new_addr = self.get() + n * self._element_size
return ptr(_ArithmeticResolver(self.resolver, new_addr), self.wrapper)
return type(self)(_ArithmeticResolver(self.resolver, new_addr), self.wrapper)

def __sub__(self: ptr, n: int) -> ptr:
"""Return a new pointer retreated by n elements."""
new_addr = self.get() - n * self._element_size
return ptr(_ArithmeticResolver(self.resolver, new_addr), self.wrapper)
return type(self)(_ArithmeticResolver(self.resolver, new_addr), self.wrapper)

def __getitem__(self: ptr, n: int) -> obj:
"""Return the object at index n relative to this pointer."""
Expand Down
Loading
Loading