Skip to content

Commit 0c501ee

Browse files
committed
fix: solve various issues in the codebase that Opus created and then fixed itself
1 parent dd6df6b commit 0c501ee

12 files changed

Lines changed: 255 additions & 51 deletions

File tree

libdestruct/c/c_float_types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ def __float__(self: c_float) -> float:
4141
"""Return the value as a Python float."""
4242
return self.get()
4343

44+
def __int__(self: c_float) -> int:
45+
"""Return the value as a Python int."""
46+
return int(self.get())
47+
4448

4549
class c_double(obj):
4650
"""A C double (IEEE 754 double-precision, 64-bit)."""
@@ -71,3 +75,7 @@ def to_bytes(self: c_double) -> bytes:
7175
def __float__(self: c_double) -> float:
7276
"""Return the value as a Python float."""
7377
return self.get()
78+
79+
def __int__(self: c_double) -> int:
80+
"""Return the value as a Python int."""
81+
return int(self.get())

libdestruct/c/struct_parser.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@
5757
"""A cache for parsed type definitions, indexed by name."""
5858

5959

60+
def clear_parser_cache() -> None:
61+
"""Clear cached struct definitions and typedefs from previous parses."""
62+
PARSED_STRUCTS.clear()
63+
TYPEDEFS.clear()
64+
65+
6066
def definition_to_type(definition: str) -> type[obj]:
6167
"""Converts a C struct definition to a struct object."""
6268
parser = c_parser.CParser()

libdestruct/common/bitfield/bitfield.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ def to_bytes(self: bitfield) -> bytes:
9595
return self._backing_instance.to_bytes()
9696
return b""
9797

98+
def freeze(self: bitfield) -> None:
99+
"""Freeze the bitfield, also freezing the shared backing instance if this is the group owner."""
100+
if self._is_group_owner and not self._backing_instance._frozen:
101+
self._backing_instance.freeze()
102+
super().freeze()
103+
98104
def to_str(self: bitfield, _: int = 0) -> str:
99105
"""Return a string representation of the bitfield."""
100106
return f"{self.get()}"

libdestruct/common/bitfield/bitfield_tracker.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ def active(self: BitfieldTracker) -> bool:
3636
"""Return whether a bitfield group is currently active."""
3737
return self._backing_type is not None
3838

39+
def needs_new_group(self: BitfieldTracker, field: BitfieldField) -> bool:
40+
"""Return whether the given field would start a new bitfield group."""
41+
return (
42+
self._backing_type is not field.backing_type
43+
or self._bit_offset + field.bit_width > field.backing_type.size * 8
44+
)
45+
3946
def flush(self: BitfieldTracker) -> int:
4047
"""Close the current bitfield group and return the byte size to advance.
4148

libdestruct/common/obj.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def _compare_value(self: obj, other: object) -> tuple[object, object] | None:
135135
self_val = self.value
136136
if isinstance(other, obj):
137137
return self_val, other.value
138-
if isinstance(other, int | float):
138+
if isinstance(other, int | float | bytes):
139139
return self_val, other
140140
return None
141141

libdestruct/common/struct/struct_impl.py

Lines changed: 47 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,14 @@ def _inflate_struct_attributes(
8080
current_offset = 0
8181
bf_tracker = BitfieldTracker()
8282
aligned = getattr(reference_type, "_aligned_", False)
83+
self._member_offsets = {}
8384

8485
for name, annotation, reference in iterate_annotation_chain(reference_type, terminate_at=struct):
8586
if name == "_aligned_":
8687
continue
8788

88-
resolved_type, bitfield_field, explicit_offset = self._resolve_field(
89-
name, annotation, reference, inflater, reference_type,
89+
resolved_type, bitfield_field, explicit_offset = struct_impl._resolve_field(
90+
name, annotation, reference, inflater, owner=(self, reference_type._type_impl),
9091
)
9192

9293
if explicit_offset is not None:
@@ -95,6 +96,10 @@ def _inflate_struct_attributes(
9596
current_offset = explicit_offset
9697

9798
if bitfield_field:
99+
if aligned and bf_tracker.needs_new_group(bitfield_field):
100+
current_offset += bf_tracker.flush()
101+
current_offset = _align_offset(current_offset, alignment_of(bitfield_field.backing_type))
102+
self._member_offsets[name] = current_offset
98103
result, offset_delta = bf_tracker.create_bitfield(
99104
bitfield_field, inflater, resolver, current_offset,
100105
)
@@ -103,20 +108,21 @@ def _inflate_struct_attributes(
103108
current_offset += bf_tracker.flush()
104109
if aligned and explicit_offset is None:
105110
current_offset = _align_offset(current_offset, alignment_of(resolved_type))
111+
self._member_offsets[name] = current_offset
106112
result = resolved_type(resolver.relative_from_own(current_offset, 0))
107113
current_offset += size_of(result)
108114

109115
self._members[name] = result
110116

111117
current_offset += bf_tracker.flush()
112118

119+
@staticmethod
113120
def _resolve_field(
114-
self: struct_impl,
115121
name: str,
116122
annotation: type,
117123
reference: type,
118124
inflater: TypeRegistry,
119-
reference_type: type,
125+
owner: tuple[obj, type] | None,
120126
) -> tuple[object | None, BitfieldField | None, int | None]:
121127
"""Resolve a single struct field annotation to its inflater or BitfieldField.
122128
@@ -126,7 +132,7 @@ def _resolve_field(
126132
explicit_offset is set when an OffsetAttribute is present.
127133
"""
128134
if name not in reference.__dict__:
129-
return inflater.inflater_for(annotation, owner=(self, reference_type._type_impl)), None, None
135+
return inflater.inflater_for(annotation, owner=owner), None, None
130136

131137
attrs = getattr(reference, name)
132138
if not isinstance(attrs, tuple):
@@ -144,15 +150,15 @@ def _resolve_field(
144150
bitfield_field = attr
145151
elif isinstance(attr, Field):
146152
resolved_type = inflater.inflater_for(
147-
(attr, annotation), owner=(self, reference_type._type_impl),
153+
(attr, annotation), owner=owner,
148154
)
149155
elif isinstance(attr, OffsetAttribute):
150156
explicit_offset = attr.offset
151157
else:
152158
raise TypeError("Only Field, BitfieldField, and OffsetAttribute are allowed in attributes.")
153159

154160
if not resolved_type and not bitfield_field:
155-
resolved_type = inflater.inflater_for(annotation, owner=(self, reference_type._type_impl))
161+
resolved_type = inflater.inflater_for(annotation, owner=owner)
156162

157163
return resolved_type, bitfield_field, explicit_offset
158164

@@ -168,46 +174,39 @@ def compute_own_size(cls: type[struct_impl], reference_type: type) -> None:
168174
if name == "_aligned_":
169175
continue
170176

171-
bitfield_field = None
172-
attribute = None
173-
has_explicit_offset = False
174-
175-
if name in reference.__dict__:
176-
attrs = getattr(reference, name)
177-
if not isinstance(attrs, tuple):
178-
attrs = (attrs,)
179-
180-
if sum(isinstance(attr, Field) for attr in attrs) > 1:
181-
raise ValueError("Only one Field is allowed per attribute.")
182-
183-
for attr in attrs:
184-
if isinstance(attr, BitfieldField):
185-
bitfield_field = attr
186-
elif isinstance(attr, Field):
187-
attribute = cls._inflater.inflater_for((attr, annotation), (None, cls))(None)
188-
elif isinstance(attr, OffsetAttribute):
189-
has_explicit_offset = True
190-
offset = attr.offset
191-
if offset < size:
192-
raise ValueError("Offset must be greater than the current size.")
193-
size = offset
194-
195-
if not attribute and not bitfield_field:
196-
attribute = cls._inflater.inflater_for(annotation, (None, cls))
197-
elif isinstance(annotation, Field):
198-
attribute = cls._inflater.inflater_for((annotation, annotation.base_type), (None, cls))(None)
199-
else:
200-
attribute = cls._inflater.inflater_for(annotation, (None, cls))
177+
resolved_type, bitfield_field, explicit_offset = struct_impl._resolve_field(
178+
name, annotation, reference, cls._inflater, owner=(None, cls),
179+
)
180+
181+
has_explicit_offset = explicit_offset is not None
182+
if has_explicit_offset:
183+
if explicit_offset < size:
184+
raise ValueError("Offset must be greater than the current size.")
185+
size = explicit_offset
201186

202187
if bitfield_field:
188+
if aligned and bf_tracker.needs_new_group(bitfield_field):
189+
size += bf_tracker.flush()
190+
field_align = alignment_of(bitfield_field.backing_type)
191+
max_alignment = max(max_alignment, field_align)
192+
size = _align_offset(size, field_align)
203193
size += bf_tracker.compute_size(bitfield_field)
204194
else:
205195
size += bf_tracker.flush()
196+
# Get attribute for size computation — try size_of directly first,
197+
# falling back to calling the inflater with None for complex fields.
198+
# Direct size_of avoids recursion for forward-ref pointers.
199+
try:
200+
attribute_size = size_of(resolved_type)
201+
attribute = resolved_type
202+
except (ValueError, TypeError):
203+
attribute = resolved_type(None)
204+
attribute_size = size_of(attribute)
206205
if aligned and not has_explicit_offset:
207206
field_align = alignment_of(attribute)
208207
max_alignment = max(max_alignment, field_align)
209208
size = _align_offset(size, field_align)
210-
size += size_of(attribute)
209+
size += attribute_size
211210

212211
size += bf_tracker.flush()
213212

@@ -232,21 +231,19 @@ def get(self: struct_impl) -> str:
232231
return f"{name}(address={addr}, size={size_of(self)})"
233232

234233
def to_bytes(self: struct_impl) -> bytes:
235-
"""Return the serialized representation of the struct."""
236-
return b"".join(member.to_bytes() for member in self._members.values())
234+
"""Return the serialized representation of the struct, including padding."""
235+
if self._frozen:
236+
return self._frozen_struct_bytes
237+
return self.resolver.resolve(size_of(self), 0)
237238

238239
def to_dict(self: struct_impl) -> dict[str, object]:
239240
"""Return a JSON-serializable dict of field names to values."""
240241
return {name: member.to_dict() for name, member in self._members.items()}
241242

242243
def hexdump(self: struct_impl) -> str:
243244
"""Return a hex dump of this struct's bytes with field annotations."""
244-
annotations = {}
245-
offset = 0
246-
for name, member in self._members.items():
247-
annotations[offset] = name
248-
offset += len(member.to_bytes())
249-
245+
member_offsets = object.__getattribute__(self, "_member_offsets")
246+
annotations = {member_offsets[name]: name for name in self._members}
250247
address = struct_impl.address.fget(self) if not self._frozen else 0
251248
return format_hexdump(self.to_bytes(), address, annotations)
252249

@@ -255,8 +252,9 @@ def _set(self: struct_impl, _: str) -> None:
255252
raise RuntimeError("Cannot set the value of a struct.")
256253

257254
def freeze(self: struct_impl) -> None:
258-
"""Freeze the struct."""
259-
# The struct has no implicit value, but it must freeze its members
255+
"""Freeze the struct, capturing the full byte representation including padding."""
256+
self._frozen_struct_bytes = self.resolver.resolve(size_of(self), 0)
257+
260258
for member in self._members.values():
261259
member.freeze()
262260

@@ -288,7 +286,7 @@ def __repr__(self: struct_impl) -> str:
288286
def __eq__(self: struct_impl, value: object) -> bool:
289287
"""Return whether the struct is equal to the given value."""
290288
if not isinstance(value, struct_impl):
291-
return False
289+
return NotImplemented
292290

293291
if size_of(self) != size_of(value):
294292
return False

libdestruct/common/union/union.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,20 @@ def freeze(self: union) -> None:
9494
v.freeze()
9595
super().freeze()
9696

97+
def diff(self: union) -> tuple[object, object]:
98+
"""Return the difference between the frozen and current value."""
99+
if self._variant is not None:
100+
return self._variant.diff()
101+
return {name: v.diff() for name, v in self._variants.items()}
102+
103+
def reset(self: union) -> None:
104+
"""Reset the union to its frozen value."""
105+
if self._variant is not None:
106+
self._variant.reset()
107+
else:
108+
for v in self._variants.values():
109+
v.reset()
110+
97111
def to_str(self: union, indent: int = 0) -> str:
98112
"""Return a string representation of the union."""
99113
if self._variant is not None:

test/scripts/alignment_test.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,59 @@ class s_t(struct):
212212
c: c_int # should be at offset 8 (aligned to 4)
213213

214214
self.assertEqual(size_of(s_t), 12) # 4 + 1 + 3 padding + 4
215+
216+
217+
class AlignedStructSerializationTest(unittest.TestCase):
218+
def test_aligned_struct_to_bytes_includes_padding(self):
219+
"""to_bytes on aligned struct includes padding bytes."""
220+
class aligned_t(struct):
221+
_aligned_ = True
222+
a: c_char
223+
b: c_int
224+
225+
memory = pystruct.pack("<b", 0x41) + b"\x00" * 3 + pystruct.pack("<i", 42)
226+
s = aligned_t.from_bytes(memory)
227+
# to_bytes should be 8 bytes (including padding), not 5
228+
self.assertEqual(len(s.to_bytes()), 8)
229+
230+
def test_aligned_struct_to_bytes_round_trip(self):
231+
"""from_bytes(to_bytes()) preserves values for aligned structs."""
232+
class aligned_t(struct):
233+
_aligned_ = True
234+
a: c_char
235+
b: c_int
236+
237+
memory = pystruct.pack("<b", 0x41) + b"\x00" * 3 + pystruct.pack("<i", 42)
238+
s1 = aligned_t.from_bytes(memory)
239+
s2 = aligned_t.from_bytes(s1.to_bytes())
240+
self.assertEqual(s2.a.value, 0x41)
241+
self.assertEqual(s2.b.value, 42)
242+
243+
244+
class BitfieldAlignmentTest(unittest.TestCase):
245+
def test_bitfield_alignment_padding(self):
246+
"""Bitfield backing type is aligned in aligned structs."""
247+
from libdestruct import bitfield_of
248+
249+
class s_t(struct):
250+
_aligned_ = True
251+
a: c_char
252+
flags: c_int = bitfield_of(c_int, 3)
253+
254+
# c_char (1) + 3 padding + c_int backing (4) = 8
255+
self.assertEqual(size_of(s_t), 8)
256+
257+
def test_bitfield_alignment_read(self):
258+
"""Bitfield values read correctly in aligned structs."""
259+
from libdestruct import bitfield_of
260+
from libdestruct import c_uint
261+
262+
class s_t(struct):
263+
_aligned_ = True
264+
a: c_char
265+
flags: c_uint = bitfield_of(c_uint, 3)
266+
267+
memory = pystruct.pack("<b", 0x41) + b"\x00" * 3 + pystruct.pack("<I", 5)
268+
s = s_t.from_bytes(memory)
269+
self.assertEqual(s.a.value, 0x41)
270+
self.assertEqual(s.flags.value, 5)

test/scripts/bitfield_unit_test.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import unittest
88

9-
from libdestruct import c_int, c_uint, c_long, struct, bitfield_of
9+
from libdestruct import c_int, c_uint, c_long, inflater, struct, bitfield_of
1010
from libdestruct.c.struct_parser import definition_to_type
1111

1212

@@ -139,5 +139,23 @@ def test_bitfield_c_parser(self):
139139
self.assertEqual(test.reserved.value, 10)
140140

141141

142+
class BitfieldFreezeTest(unittest.TestCase):
143+
def test_bitfield_freeze_to_bytes(self):
144+
"""Frozen bitfield struct to_bytes returns original bytes."""
145+
memory = bytearray(4)
146+
memory[0] = 0b_00101_011 # a=3, b=5
147+
148+
class flags_t(struct):
149+
a: c_int = bitfield_of(c_int, 3)
150+
b: c_int = bitfield_of(c_int, 5)
151+
152+
lib = inflater(memory)
153+
s = lib.inflate(flags_t, 0)
154+
original_bytes = bytes(s.to_bytes())
155+
s.freeze()
156+
memory[0] = 0xFF
157+
self.assertEqual(s.to_bytes(), original_bytes)
158+
159+
142160
if __name__ == "__main__":
143161
unittest.main()

test/scripts/struct_unit_test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,5 +236,15 @@ class TreeNode(struct):
236236
self.assertEqual(node.data.value, 42)
237237

238238

239+
class StructEqualityTest(unittest.TestCase):
240+
def test_struct_eq_non_struct_returns_not_implemented(self):
241+
"""struct.__eq__ returns NotImplemented for non-struct values."""
242+
class s_t(struct):
243+
x: c_int
244+
245+
s = s_t.from_bytes(b"\x01\x00\x00\x00")
246+
self.assertIs(s.__eq__(42), NotImplemented)
247+
248+
239249
if __name__ == "__main__":
240250
unittest.main()

0 commit comments

Comments
 (0)