Skip to content

Commit 837c574

Browse files
committed
fix: struct freezing didn't work correctly, alignment was broken for arrays
1 parent 7af03fe commit 837c574

5 files changed

Lines changed: 83 additions & 6 deletions

File tree

libdestruct/common/struct/struct.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@ def from_bytes(cls: type[struct], data: bytes, endianness: str = "little") -> st
3434
"""Create a struct from a serialized representation."""
3535
type_inflater = inflater(data, endianness=endianness)
3636

37-
return type_inflater.inflate(cls, 0)
37+
result = type_inflater.inflate(cls, 0)
38+
result.freeze()
39+
return result

libdestruct/common/struct/struct_impl.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,8 @@ def to_bytes(self: struct_impl) -> bytes:
234234
"""Return the serialized representation of the struct, including padding."""
235235
if self._frozen:
236236
return self._frozen_struct_bytes
237-
return self.resolver.resolve(size_of(self), 0)
237+
resolver = object.__getattribute__(self, "resolver")
238+
return resolver.resolve(size_of(self), 0)
238239

239240
def to_dict(self: struct_impl) -> dict[str, object]:
240241
"""Return a JSON-serializable dict of field names to values."""
@@ -253,12 +254,13 @@ def _set(self: struct_impl, _: str) -> None:
253254

254255
def freeze(self: struct_impl) -> None:
255256
"""Freeze the struct, capturing the full byte representation including padding."""
256-
self._frozen_struct_bytes = self.resolver.resolve(size_of(self), 0)
257+
resolver = object.__getattribute__(self, "resolver")
258+
self._frozen_struct_bytes = resolver.resolve(size_of(self), 0)
257259

258260
for member in self._members.values():
259261
member.freeze()
260262

261-
self._frozen = True
263+
super().freeze()
262264

263265
def to_str(self: struct_impl, indent: int = 0) -> str:
264266
"""Return a string representation of the struct."""

libdestruct/common/utils.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,16 @@ def alignment_of(item: obj | type[obj]) -> int:
8181
if isinstance(item, type) and "alignment" in item.__dict__ and isinstance(item.__dict__["alignment"], int):
8282
return item.__dict__["alignment"]
8383

84-
# Field descriptors
84+
# Field descriptors — for array fields, alignment comes from the element type
8585
if isinstance(item, Field):
86+
if hasattr(item, "item"):
87+
return alignment_of(item.item)
8688
return _alignment_from_size(item.get_size())
8789
if is_field_bound_method(item):
88-
return _alignment_from_size(item.__self__.get_size())
90+
field = item.__self__
91+
if hasattr(field, "item"):
92+
return alignment_of(field.item)
93+
return _alignment_from_size(field.get_size())
8994

9095
# Derive from size for power-of-2 sized types
9196
try:

test/scripts/alignment_test.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,46 @@ class aligned_t(struct):
241241
self.assertEqual(s2.b.value, 42)
242242

243243

244+
class ArrayAlignmentTest(unittest.TestCase):
245+
def test_alignment_of_array_field(self):
246+
"""alignment_of(array_of(c_int, N)) should be 4 (element alignment), not 1."""
247+
from libdestruct import array_of
248+
249+
arr = array_of(c_int, 3)
250+
self.assertEqual(alignment_of(arr), 4)
251+
252+
def test_aligned_struct_with_array(self):
253+
"""Array field in aligned struct should be aligned to element alignment."""
254+
from libdestruct import array_of
255+
256+
class s_t(struct):
257+
_aligned_ = True
258+
a: c_char
259+
arr: list[c_int] = array_of(c_int, 3)
260+
261+
# a at 0 (1 byte), padding 3, arr at 4 (12 bytes) = 16, tail padded to 4 = 16
262+
self.assertEqual(size_of(s_t), 16)
263+
264+
def test_aligned_struct_with_array_read(self):
265+
"""Values read correctly from aligned array in struct."""
266+
from libdestruct import array_of
267+
268+
class s_t(struct):
269+
_aligned_ = True
270+
a: c_char
271+
arr: list[c_int] = array_of(c_int, 3)
272+
273+
memory = pystruct.pack("<b", 0x41) + b"\x00" * 3
274+
for v in [10, 20, 30]:
275+
memory += pystruct.pack("<i", v)
276+
277+
s = s_t.from_bytes(memory)
278+
self.assertEqual(s.a.value, 0x41)
279+
self.assertEqual(s.arr[0].value, 10)
280+
self.assertEqual(s.arr[1].value, 20)
281+
self.assertEqual(s.arr[2].value, 30)
282+
283+
244284
class BitfieldAlignmentTest(unittest.TestCase):
245285
def test_bitfield_alignment_padding(self):
246286
"""Bitfield backing type is aligned in aligned structs."""

test/scripts/struct_unit_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,34 @@ class test_t(struct):
198198
with self.assertRaises(ValueError):
199199
test.a.value = 999
200200

201+
def test_frozen_struct_value_not_none(self):
202+
"""Frozen struct .value should not be None."""
203+
class test_t(struct):
204+
a: c_int
205+
206+
memory = bytearray(b"\x2a\x00\x00\x00")
207+
lib = inflater(memory)
208+
test = lib.inflate(test_t, 0)
209+
test.freeze()
210+
self.assertIsNotNone(test.value)
211+
212+
def test_from_bytes_struct_is_frozen(self):
213+
"""struct.from_bytes should return a frozen struct, like obj.from_bytes."""
214+
class test_t(struct):
215+
a: c_int
216+
217+
test = test_t.from_bytes(b"\x2a\x00\x00\x00")
218+
self.assertTrue(test._frozen)
219+
220+
def test_from_bytes_struct_rejects_writes(self):
221+
"""struct.from_bytes result should reject writes with ValueError, not TypeError."""
222+
class test_t(struct):
223+
a: c_int
224+
225+
test = test_t.from_bytes(b"\x2a\x00\x00\x00")
226+
with self.assertRaises(ValueError):
227+
test.a.value = 99
228+
201229

202230
class ForwardRefPtrTest(unittest.TestCase):
203231
"""Forward reference ptr["Type"] syntax."""

0 commit comments

Comments
 (0)