Skip to content

Commit 8b8ca90

Browse files
committed
fix: correct wrong enum_of implementation in code and docs
1 parent f134091 commit 8b8ca90

8 files changed

Lines changed: 114 additions & 13 deletions

File tree

SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ Legacy syntax with `enum_of()` is still supported:
234234

235235
```python
236236
class pixel_t(struct):
237-
color: c_int = enum_of(Color)
237+
color: enum = enum_of(Color)
238238
alpha: c_int
239239
```
240240

docs/advanced/forward_refs.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ At inflation time, the string `"Node"` is resolved to the actual `Node` class. T
2121
For the common case of a pointer to the enclosing struct, the legacy `ptr_to_self` syntax is also available:
2222

2323
```python
24-
from libdestruct import struct, c_int, ptr_to_self
24+
from libdestruct import struct, c_int, ptr, ptr_to_self
2525

2626
class Node(struct):
2727
val: c_int
28-
next: ptr_to_self
28+
next: ptr = ptr_to_self()
2929
```
3030

3131
This is equivalent to `ptr["Node"]` but doesn't require you to spell out the type name. The `ptr["TypeName"]` syntax is preferred as it is more explicit.

docs/basics/enums.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ The legacy `enum_of()` syntax is also supported:
3232
from libdestruct import struct, c_int, enum_of
3333

3434
class pixel_t(struct):
35-
color: enum_of(Color, c_int)
35+
color: enum = enum_of(Color)
3636
x: c_int
3737
y: c_int
3838
```

libdestruct/common/struct/struct_impl.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,13 @@ def hexdump(self: struct_impl) -> str:
259259
"""Return a hex dump of this struct's bytes with field annotations."""
260260
member_offsets = object.__getattribute__(self, "_member_offsets")
261261
members = object.__getattribute__(self, "_members")
262-
annotations = {member_offsets[name]: name for name in members}
262+
annotations: dict[int, str] = {}
263+
for name in members:
264+
off = member_offsets[name]
265+
if off in annotations:
266+
annotations[off] += ", " + name
267+
else:
268+
annotations[off] = name
263269
address = struct_impl.address.fget(self) if not object.__getattribute__(self, "_frozen") else 0
264270
return format_hexdump(self.to_bytes(), address, annotations)
265271

libdestruct/common/union/union.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,11 @@ def diff(self: union) -> tuple[object, object]:
101101
return {name: v.diff() for name, v in self._variants.items()}
102102

103103
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()
104+
"""Reset the union to its frozen value by restoring the full frozen byte region."""
105+
if self._frozen_bytes is None:
106+
raise RuntimeError("Cannot reset a union that has not been frozen.")
107+
if self.resolver is not None:
108+
self.resolver.modify(self.size, 0, self._frozen_bytes)
110109

111110
def to_str(self: union, indent: int = 0) -> str:
112111
"""Return a string representation of the union."""

libdestruct/common/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,21 @@ def is_field_bound_method(item: obj) -> bool:
2727

2828
def size_of(item_or_inflater: obj | callable[[Resolver], obj]) -> int:
2929
"""Return the size in bytes of a type, instance, or field descriptor."""
30+
from types import GenericAlias
31+
3032
# Field instances (e.g. array_of, ptr_to) — must come before .size check
3133
if isinstance(item_or_inflater, Field):
3234
return item_or_inflater.get_size()
3335
if is_field_bound_method(item_or_inflater):
3436
return item_or_inflater.__self__.get_size()
3537

38+
# Subscripted GenericAlias types (e.g. array[c_int, 10], enum[Color], ptr[T])
39+
if isinstance(item_or_inflater, GenericAlias):
40+
from libdestruct.common.type_registry import TypeRegistry
41+
42+
inflater = TypeRegistry().inflater_for(item_or_inflater)
43+
return size_of(inflater)
44+
3645
# Struct types: size is on the inflated _type_impl class
3746
if isinstance(item_or_inflater, type) and hasattr(item_or_inflater, "_type_impl"):
3847
return item_or_inflater._type_impl.size

test/scripts/struct_unit_test.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,5 +524,92 @@ class s_t(struct):
524524
self.assertEqual(size_of(s_t), 2)
525525

526526

527+
class SizeOfGenericAliasTest(unittest.TestCase):
528+
"""size_of() must handle subscripted GenericAlias types like array[c_int, 10]."""
529+
530+
def test_size_of_subscripted_array(self):
531+
self.assertEqual(size_of(array[c_int, 10]), 40)
532+
533+
def test_size_of_subscripted_enum(self):
534+
from enum import IntEnum
535+
536+
class Color(IntEnum):
537+
RED = 0
538+
539+
self.assertEqual(size_of(enum[Color]), 4)
540+
self.assertEqual(size_of(enum[Color, c_short]), 2)
541+
542+
def test_size_of_subscripted_ptr(self):
543+
self.assertEqual(size_of(ptr[c_int]), 8)
544+
545+
546+
class UnionResetTest(unittest.TestCase):
547+
"""union.reset() must restore the full frozen byte region."""
548+
549+
def test_tagged_union_reset_struct_variant(self):
550+
"""reset() on a tagged union with a struct variant must not crash."""
551+
import struct as pystruct
552+
553+
class point_t(struct):
554+
x: c_int
555+
y: c_int
556+
557+
class msg_t(struct):
558+
tag: c_int
559+
payload: union = tagged_union("tag", {0: c_int, 1: point_t})
560+
561+
memory = bytearray(12)
562+
memory[0:4] = pystruct.pack("<i", 1) # tag = 1 → point_t
563+
memory[4:8] = pystruct.pack("<i", 10) # x = 10
564+
memory[8:12] = pystruct.pack("<i", 20) # y = 20
565+
566+
lib = inflater(memory)
567+
msg = lib.inflate(msg_t, 0)
568+
msg.freeze()
569+
570+
# Corrupt memory
571+
memory[4:12] = b"\xff" * 8
572+
msg.payload.reset()
573+
574+
# Verify restored
575+
self.assertEqual(msg.payload.variant.x.value, 10)
576+
self.assertEqual(msg.payload.variant.y.value, 20)
577+
578+
def test_plain_union_reset(self):
579+
"""reset() on a plain union must restore the full frozen region."""
580+
import struct as pystruct
581+
582+
memory = bytearray(8)
583+
memory[0:8] = pystruct.pack("<q", 0x1234567890ABCDEF)
584+
585+
class s_t(struct):
586+
data: union = union_of({"i": c_int, "l": c_long})
587+
588+
lib = inflater(memory)
589+
s = lib.inflate(s_t, 0)
590+
s.freeze()
591+
592+
memory[0:8] = b"\x00" * 8
593+
s.data.reset()
594+
595+
self.assertEqual(s.data.l.value, 0x1234567890ABCDEF)
596+
597+
598+
class HexdumpBitfieldAnnotationsTest(unittest.TestCase):
599+
"""hexdump() must show all bitfield names at the same offset."""
600+
601+
def test_bitfield_hexdump_shows_all_names(self):
602+
class flags_t(struct):
603+
read: c_int = bitfield_of(c_int, 1)
604+
write: c_int = bitfield_of(c_int, 1)
605+
execute: c_int = bitfield_of(c_int, 1)
606+
607+
f = flags_t.from_bytes(b"\x07\x00\x00\x00")
608+
dump = f.hexdump()
609+
self.assertIn("read", dump)
610+
self.assertIn("write", dump)
611+
self.assertIn("execute", dump)
612+
613+
527614
if __name__ == "__main__":
528615
unittest.main()

test/scripts/to_dict_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ class entity_t(struct):
5656
result = entity.to_dict()
5757
self.assertEqual(result, {"id": 1, "pos": {"x": 10, "y": 20}})
5858

59-
def test_struct_with_ptr_to_dict(self):
60-
"""Pointer field returns its address as int."""
59+
def test_struct_with_long_to_dict(self):
60+
"""Integer field returns its value."""
6161
class data_t(struct):
6262
value: c_int
6363
ref: c_long

0 commit comments

Comments
 (0)