Skip to content

Commit 7c0a412

Browse files
committed
fix: solve the last remaining bugs I could find in the codebase
1 parent 6bf5c45 commit 7c0a412

11 files changed

Lines changed: 503 additions & 27 deletions

File tree

docs/advanced/alignment.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class s_t(struct):
109109
b: c_int = offset(3) # placed at offset 3, not rounded to 4
110110
c: c_int # aligned normally after b
111111

112-
size_of(s_t) # 7 (3 + 4)
112+
size_of(s_t) # 12 (b at offset 3 + 4 bytes = 7, c aligned to offset 8 + 4 bytes = 12)
113113
```
114114

115115
## alignment_of()

docs/advanced/forward_refs.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@ memory[4:12] = pystruct.pack("<q", 12) # next -> offset 12
5050

5151
# Node 1 at offset 12
5252
memory[12:16] = pystruct.pack("<i", 20)
53-
memory[16:24] = pystruct.pack("<q", 0) # next -> null
53+
memory[16:24] = pystruct.pack("<q", 0xDEAD) # next -> out of bounds
5454

5555
lib = inflater(memory)
5656
head = lib.inflate(Node, 0)
5757

5858
print(head.val.value) # 10
5959
print(head.next.unwrap().val.value) # 20
60-
print(head.next.unwrap().next.try_unwrap()) # None
60+
print(head.next.unwrap().next.try_unwrap()) # None (address out of bounds)
6161
```
6262

6363
## Tree Example

docs/basics/pointers.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,17 @@ class node_t(struct):
5454
val: c_int
5555
next: ptr[c_int]
5656

57-
memory = b"\x0a\x00\x00\x00" + b"\x00" * 8 # val=10, next=null
57+
# next points to address 0xFF..FF which is out of bounds
58+
memory = b"\x0a\x00\x00\x00" + b"\xff" * 8
5859
node = node_t.from_bytes(memory)
5960

6061
result = node.next.try_unwrap()
6162
print(result) # None
6263
```
6364

65+
!!! note
66+
A null pointer (address 0) is **not** automatically invalid — address 0 is a valid index in Python byte sequences. `try_unwrap()` only returns `None` when the address causes an `IndexError` or `ValueError` during resolution.
67+
6468
## Self-Referential Structs
6569

6670
Use `ptr["TypeName"]` for self-referential structs:
@@ -94,16 +98,16 @@ import struct as pystruct
9498
# Node 0 at offset 0: val=10, next -> offset 12
9599
memory[0:4] = pystruct.pack("<i", 10)
96100
memory[4:12] = pystruct.pack("<q", 12)
97-
# Node 1 at offset 12: val=20, next -> null
101+
# Node 1 at offset 12: val=20, next -> out of bounds
98102
memory[12:16] = pystruct.pack("<i", 20)
99-
memory[16:24] = pystruct.pack("<q", 0)
103+
memory[16:24] = pystruct.pack("<q", 0xDEAD)
100104

101105
lib = inflater(memory)
102106
head = lib.inflate(node_t, 0)
103107

104108
print(head.val.value) # 10
105109
print(head.next.unwrap().val.value) # 20
106-
print(head.next.unwrap().next.try_unwrap()) # None
110+
print(head.next.unwrap().next.try_unwrap()) # None (address out of bounds)
107111
```
108112

109113
## Pointer Arithmetic

docs/memory/resolvers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ All resolvers implement these methods:
4949

5050
| Method | Description |
5151
|---|---|
52-
| `resolve(size, offset)` | Read `size` bytes starting at the resolved address + offset |
52+
| `resolve(size, index)` | Read `size` bytes starting at the resolved address + index |
5353
| `resolve_address()` | Return the absolute address of this resolver |
5454
| `modify(size, index, value)` | Write `value` bytes at the resolved address + index |
55-
| `relative_from_own(offset, size)` | Create a child resolver at a relative offset |
55+
| `relative_from_own(address_offset, index_offset)` | Create a child resolver at a relative offset |
5656
| `absolute_from_own(address)` | Create a child resolver at an absolute address |
5757

5858
## Custom Memory Backends

libdestruct/c/struct_parser.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ def definition_to_type(definition: str) -> type[obj]:
9797

9898
result = struct_to_type(root)
9999

100-
PARSED_STRUCTS[root.name] = result
100+
if root.name:
101+
PARSED_STRUCTS[root.name] = result
101102

102103
return result
103104

@@ -170,6 +171,9 @@ def arr_to_type(arr: c_ast.ArrayDecl) -> type[obj]:
170171

171172
typ = ptr_to_type(arr.type) if isinstance(arr.type, c_ast.PtrDecl) else type_decl_to_type(arr.type)
172173

174+
if arr.dim is None:
175+
raise ValueError("Unsized arrays (flexible array members) are not supported.")
176+
173177
return array_of(typ, int(arr.dim.value))
174178

175179

libdestruct/common/array/array_impl.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ def count(self: array_impl) -> int:
4848

4949
def get(self: array, index: int = -1) -> object:
5050
"""Return the element at the given index, or all elements if index is -1."""
51+
if hasattr(self, "_frozen_elements"):
52+
if index == -1:
53+
return list(self._frozen_elements)
54+
return self._frozen_elements[index]
5155
if index == -1:
5256
return [self.backing_type(self.resolver.relative_from_own(i * self.item_size, 0)) for i in range(self._count)]
5357
return self.backing_type(self.resolver.relative_from_own(index * self.item_size, 0))
@@ -64,8 +68,21 @@ def to_dict(self: array_impl) -> list[object]:
6468
"""Return a JSON-serializable list of element values."""
6569
return [elem.to_dict() for elem in self]
6670

71+
def freeze(self: array_impl) -> None:
72+
"""Freeze the array, individually freezing each element."""
73+
self._frozen_elements = [
74+
self.backing_type(self.resolver.relative_from_own(i * self.item_size, 0))
75+
for i in range(self._count)
76+
]
77+
for elem in self._frozen_elements:
78+
elem.freeze()
79+
self._frozen_array_bytes = b"".join(bytes(x) for x in self._frozen_elements)
80+
super().freeze()
81+
6782
def to_bytes(self: array_impl) -> bytes:
6883
"""Return the serialized representation of the array."""
84+
if self._frozen:
85+
return self._frozen_array_bytes
6986
return b"".join(bytes(x) for x in self)
7087

7188
def to_str(self: array_impl, indent: int = 0) -> str:
@@ -87,5 +104,8 @@ def __setitem__(self: array_impl, index: int, value: obj) -> None:
87104

88105
def __iter__(self: array_impl) -> Generator[obj, None, None]:
89106
"""Iterate over the array."""
90-
for i in range(self._count):
91-
yield self[i]
107+
if self._frozen:
108+
yield from self._frozen_elements
109+
else:
110+
for i in range(self._count):
111+
yield self[i]

libdestruct/common/enum/enum.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def get(self: enum) -> Enum:
6464

6565
def _set(self: enum, value: Enum) -> None:
6666
"""Set the value of the enum."""
67-
self._backing_type.set(value.value)
67+
self._backing_type.set(int(value))
6868

6969
def to_bytes(self: enum) -> bytes:
7070
"""Return the serialized representation of the enum."""

libdestruct/common/obj.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ def set(self: obj, value: object) -> None:
7777

7878
def freeze(self: obj) -> None:
7979
"""Freeze the object."""
80-
self._frozen_value = self.get()
81-
self._frozen = True
80+
object.__setattr__(self, "_frozen_value", self.get())
81+
object.__setattr__(self, "_frozen", True)
8282

8383
def diff(self: obj) -> tuple[object, object]:
8484
"""Return the difference between the current value and the frozen value."""
@@ -97,7 +97,7 @@ def reset(self: obj) -> None:
9797
def update(self: obj) -> None:
9898
"""Update the object with the given value."""
9999
try:
100-
self._frozen_value = self.get()
100+
object.__setattr__(self, "_frozen_value", self.get())
101101
except ValueError as e:
102102
raise RuntimeError("Could not update the object.") from e
103103

libdestruct/common/ptr/ptr.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def unwrap(self: ptr, length: int | None = None) -> obj | bytes:
103103
result = self.wrapper(self.resolver.absolute_from_own(address))
104104
else:
105105
target_resolver = self.resolver.absolute_from_own(address)
106-
result = target_resolver.resolve(length or 1, 0)
106+
result = target_resolver.resolve(length if length is not None else 1, 0)
107107

108108
self._cached_unwrap = result
109109
self._cache_valid = True
@@ -123,7 +123,7 @@ def try_unwrap(self: ptr, length: int | None = None) -> obj | bytes | None:
123123

124124
try:
125125
# If the address is invalid, this will raise an IndexError or ValueError.
126-
self.resolver.absolute_from_own(address).resolve(length or 1, 0)
126+
self.resolver.absolute_from_own(address).resolve(length if length is not None else 1, 0)
127127
except (IndexError, ValueError):
128128
return None
129129

libdestruct/common/struct/struct_impl.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ def __init__(self: struct_impl, resolver: Resolver | None = None, **kwargs: ...)
4949
# struct overrides the __init__ method, so we need to call the parent class __init__ method
5050
obj.__init__(self, resolver)
5151

52-
self._struct_name = self.__class__.__name__
53-
self._members = {}
52+
object.__setattr__(self, "_struct_name", self.__class__.__name__)
53+
object.__setattr__(self, "_members", {})
5454

5555
reference_type = self._reference_struct
5656
self._inflate_struct_attributes(self._inflater, resolver, reference_type)
@@ -69,6 +69,17 @@ def __getattribute__(self: struct_impl, name: str) -> object:
6969
pass
7070
return super().__getattribute__(name)
7171

72+
def __setattr__(self: struct_impl, name: str, value: object) -> None:
73+
"""Set an attribute, delegating to member.value for struct fields."""
74+
try:
75+
members = object.__getattribute__(self, "_members")
76+
if name in members:
77+
members[name].value = value
78+
return
79+
except AttributeError:
80+
pass
81+
object.__setattr__(self, name, value)
82+
7283
def __new__(cls: struct_impl, *args: ..., **kwargs: ...) -> Self:
7384
"""Create a new struct."""
7485
# Skip the __new__ method of the parent class
@@ -82,9 +93,10 @@ def _inflate_struct_attributes(
8293
reference_type: type,
8394
) -> None:
8495
current_offset = 0
96+
max_alignment = 1
8597
bf_tracker = BitfieldTracker()
8698
aligned = getattr(reference_type, "_aligned_", False)
87-
self._member_offsets = {}
99+
object.__setattr__(self, "_member_offsets", {})
88100

89101
for name, annotation, reference in iterate_annotation_chain(reference_type, terminate_at=struct):
90102
if name == "_aligned_":
@@ -103,7 +115,9 @@ def _inflate_struct_attributes(
103115
if bitfield_field:
104116
if aligned and bf_tracker.needs_new_group(bitfield_field):
105117
current_offset += bf_tracker.flush()
106-
current_offset = _align_offset(current_offset, alignment_of(bitfield_field.backing_type))
118+
field_align = alignment_of(bitfield_field.backing_type)
119+
max_alignment = max(max_alignment, field_align)
120+
current_offset = _align_offset(current_offset, field_align)
107121
self._member_offsets[name] = current_offset
108122
result, offset_delta = bf_tracker.create_bitfield(
109123
bitfield_field, inflater, resolver, current_offset,
@@ -112,7 +126,18 @@ def _inflate_struct_attributes(
112126
else:
113127
current_offset += bf_tracker.flush()
114128
if aligned and explicit_offset is None:
115-
current_offset = _align_offset(current_offset, alignment_of(resolved_type))
129+
# Try alignment from the resolved type directly; for closures
130+
# (e.g. union inflaters) alignment_of can't inspect them, so
131+
# fall back to creating a probe instance.
132+
field_align = alignment_of(resolved_type)
133+
if field_align <= 1:
134+
try:
135+
probe = resolved_type(resolver.relative_from_own(current_offset, 0))
136+
field_align = alignment_of(probe)
137+
except (ValueError, TypeError):
138+
pass
139+
max_alignment = max(max_alignment, field_align)
140+
current_offset = _align_offset(current_offset, field_align)
116141
self._member_offsets[name] = current_offset
117142
result = resolved_type(resolver.relative_from_own(current_offset, 0))
118143
current_offset += size_of(result)
@@ -121,16 +146,22 @@ def _inflate_struct_attributes(
121146

122147
current_offset += bf_tracker.flush()
123148

149+
# Apply tail padding for aligned structs
150+
if aligned:
151+
if isinstance(aligned, int) and aligned is not True:
152+
max_alignment = max(max_alignment, aligned)
153+
current_offset = _align_offset(current_offset, max_alignment)
154+
124155
# For VLA structs, size must be computed dynamically since the count
125156
# can change at runtime. Detect VLA by duck-typing: vla_impl has a
126157
# _count_member attribute that plain array_impl does not.
127158
members = object.__getattribute__(self, "_members")
128159
last_member = list(members.values())[-1] if members else None
129160
if last_member is not None and hasattr(last_member, "_count_member"):
130-
last_name = list(self._members.keys())[-1]
131-
self._vla_fixed_offset = self._member_offsets[last_name]
161+
last_name = list(members.keys())[-1]
162+
object.__setattr__(self, "_vla_fixed_offset", self._member_offsets[last_name])
132163
else:
133-
self.size = current_offset
164+
object.__setattr__(self, "size", current_offset)
134165

135166
@staticmethod
136167
def _resolve_field(
@@ -316,7 +347,7 @@ def freeze(self: struct_impl) -> None:
316347

317348
def reset(self: struct_impl) -> None:
318349
"""Reset each member to its frozen value."""
319-
if not self._frozen:
350+
if not object.__getattribute__(self, "_frozen"):
320351
raise RuntimeError("Cannot reset a struct that has not been frozen.")
321352

322353
members = object.__getattribute__(self, "_members")

0 commit comments

Comments
 (0)