Skip to content

Commit 88b489f

Browse files
committed
feat: add support for modifiers like offset specified through Annotated types
1 parent 726f12d commit 88b489f

2 files changed

Lines changed: 69 additions & 3 deletions

File tree

libdestruct/common/struct/struct_impl.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from __future__ import annotations
88

9+
from typing import Annotated, get_args, get_origin
10+
911
from typing_extensions import Self
1012

1113
from libdestruct.backing.fake_resolver import FakeResolver
@@ -131,8 +133,17 @@ def _resolve_field(
131133
Either resolved_inflater or bitfield_field will be non-None (not both).
132134
explicit_offset is set when an OffsetAttribute is present.
133135
"""
136+
# Unwrap Annotated[type, metadata...] — extract the real type and any metadata
137+
annotated_offset = None
138+
if get_origin(annotation) is Annotated:
139+
ann_args = get_args(annotation)
140+
annotation = ann_args[0]
141+
for meta in ann_args[1:]:
142+
if isinstance(meta, OffsetAttribute):
143+
annotated_offset = meta.offset
144+
134145
if name not in reference.__dict__:
135-
return inflater.inflater_for(annotation, owner=owner), None, None
146+
return inflater.inflater_for(annotation, owner=owner), None, annotated_offset
136147

137148
attrs = getattr(reference, name)
138149
if not isinstance(attrs, tuple):
@@ -143,7 +154,7 @@ def _resolve_field(
143154

144155
resolved_type = None
145156
bitfield_field = None
146-
explicit_offset = None
157+
explicit_offset = annotated_offset
147158

148159
for attr in attrs:
149160
if isinstance(attr, BitfieldField):

test/scripts/struct_unit_test.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import unittest
88
from enum import IntEnum
99

10-
from libdestruct import array, c_int, c_long, c_short, c_uint, inflater, struct, ptr, ptr_to_self, array_of, enum, enum_of
10+
from typing import Annotated
11+
12+
from libdestruct import array, c_int, c_long, c_short, c_uint, inflater, offset, struct, ptr, ptr_to_self, array_of, enum, enum_of
1113

1214

1315
class StructMemberCollisionTest(unittest.TestCase):
@@ -354,6 +356,59 @@ class s_t(struct):
354356
self.assertEqual(s.coords[1].value, 20)
355357

356358

359+
class AnnotatedOffsetTest(unittest.TestCase):
360+
"""Test Annotated[type, offset(N)] syntax for explicit field offsets."""
361+
362+
def test_annotated_offset_basic(self):
363+
"""Annotated[c_int, offset(N)] places a field at the given offset."""
364+
class s_t(struct):
365+
a: c_int
366+
b: Annotated[c_int, offset(8)]
367+
368+
from libdestruct import size_of
369+
self.assertEqual(size_of(s_t), 12) # 8 + 4
370+
371+
def test_annotated_offset_read(self):
372+
"""Values are read correctly from Annotated offset positions."""
373+
import struct as pystruct
374+
375+
class s_t(struct):
376+
a: c_int
377+
b: Annotated[c_int, offset(8)]
378+
379+
memory = pystruct.pack("<i", 10) + b"\x00" * 4 + pystruct.pack("<i", 20)
380+
s = s_t.from_bytes(memory)
381+
self.assertEqual(s.a.value, 10)
382+
self.assertEqual(s.b.value, 20)
383+
384+
def test_annotated_offset_with_subscript(self):
385+
"""Annotated works with subscript syntax types."""
386+
class s_t(struct):
387+
a: c_int
388+
data: Annotated[array[c_int, 2], offset(8)]
389+
390+
from libdestruct import size_of
391+
self.assertEqual(size_of(s_t), 16) # 8 + 2*4
392+
393+
def test_annotated_offset_with_ptr(self):
394+
"""Annotated works with ptr subscript syntax."""
395+
class s_t(struct):
396+
a: c_int
397+
ref: Annotated[ptr[c_int], offset(8)]
398+
399+
from libdestruct import size_of
400+
self.assertEqual(size_of(s_t), 16) # 8 + 8
401+
402+
def test_old_offset_syntax_still_works(self):
403+
"""The old offset() default value syntax continues to work."""
404+
class s_t(struct):
405+
a: c_int
406+
b: c_int = offset(8)
407+
408+
from libdestruct import size_of
409+
self.assertEqual(size_of(s_t), 12)
410+
411+
357412
class StructEqualityTest(unittest.TestCase):
358413
def test_struct_eq_non_struct_returns_not_implemented(self):
359414
"""struct.__eq__ returns NotImplemented for non-struct values."""

0 commit comments

Comments
 (0)