Skip to content

Commit 304044a

Browse files
committed
feat: implement a way to specify offsets in a struct
1 parent fa987c9 commit 304044a

8 files changed

Lines changed: 123 additions & 22 deletions

File tree

libdestruct/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@
1212
pass
1313

1414
from libdestruct.c import c_int, c_long, c_str, c_uint, c_ulong
15-
from libdestruct.common import ptr
1615
from libdestruct.common.array import array, array_of
16+
from libdestruct.common.attributes import offset
1717
from libdestruct.common.enum import enum, enum_of
18+
from libdestruct.common.ptr import ptr
1819
from libdestruct.common.struct import ptr_to, ptr_to_self, struct
1920
from libdestruct.libdestruct import inflater
2021

2122
__all__ = [
2223
"array",
2324
"array_of",
25+
"offset",
2426
"c_int",
2527
"c_long",
2628
"c_str",

libdestruct/common/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,3 @@
33
# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
44
# Licensed under the MIT license. See LICENSE file in the project root for details.
55
#
6-
7-
from libdestruct.common.ptr import ptr
8-
9-
__all__ = ["ptr"]

libdestruct/common/attribute.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#
2+
# This file is part of libdestruct (https://github.com/mrindeciso/libdestruct).
3+
# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
4+
# Licensed under the MIT license. See LICENSE file in the project root for details.
5+
#
6+
7+
from abc import ABC
8+
9+
10+
class Attribute(ABC):
11+
"""Base class for all attributes, which are used to describe fields in a struct."""
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#
2+
# This file is part of libdestruct (https://github.com/mrindeciso/libdestruct).
3+
# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
4+
# Licensed under the MIT license. See LICENSE file in the project root for details.
5+
#
6+
7+
from libdestruct.common.attributes.offset import offset
8+
9+
__all__ = ["offset"]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#
2+
# This file is part of libdestruct (https://github.com/mrindeciso/libdestruct).
3+
# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
4+
# Licensed under the MIT license. See LICENSE file in the project root for details.
5+
#
6+
7+
from __future__ import annotations
8+
9+
from libdestruct.common.attributes.offset_attribute import OffsetAttribute
10+
11+
12+
def offset(offset: int) -> OffsetAttribute:
13+
"""Create an offset field."""
14+
return OffsetAttribute(offset)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#
2+
# This file is part of libdestruct (https://github.com/mrindeciso/libdestruct).
3+
# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
4+
# Licensed under the MIT license. See LICENSE file in the project root for details.
5+
#
6+
7+
from __future__ import annotations
8+
9+
from libdestruct.common.attribute import Attribute
10+
11+
12+
class OffsetAttribute(Attribute):
13+
"""A field that represents an offset in a struct."""
14+
15+
offset: int
16+
17+
def __init__(self: OffsetAttribute, offset: int) -> None:
18+
"""Initialize the offset field."""
19+
self.offset = offset

libdestruct/common/struct/struct_impl.py

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88

99
from typing import TYPE_CHECKING
1010

11+
from libdestruct.common.attributes.offset_attribute import OffsetAttribute
1112
from libdestruct.common.field import Field
1213
from libdestruct.common.obj import obj
1314
from libdestruct.common.struct import struct
1415
from libdestruct.common.type_registry import TypeRegistry
1516

16-
if TYPE_CHECKING: # pragma: no cover
17+
if TYPE_CHECKING: # pragma: no cover
1718
from libdestruct.backing.resolver import Resolver
1819

1920

@@ -54,8 +55,35 @@ def _inflate_struct_attributes(
5455
for name, annotation in reference_type.__annotations__.items():
5556
if name in reference_type.__dict__:
5657
# Field associated with the annotation
57-
field = getattr(reference_type, name)
58-
resolved_type = inflater.inflater_for((field, annotation), owner=(self, reference_type._type_impl))
58+
attrs = getattr(reference_type, name)
59+
60+
# If attrs is not a tuple, we need to convert it to a tuple
61+
if not isinstance(attrs, tuple):
62+
attrs = (attrs,)
63+
64+
# Assert that in all attributes, there is only one Field
65+
if sum(isinstance(attr, Field) for attr in attrs) > 1:
66+
raise ValueError("Only one Field is allowed per attribute.")
67+
68+
resolved_type = None
69+
70+
for attr in attrs:
71+
if isinstance(attr, Field):
72+
resolved_type = inflater.inflater_for(
73+
(attr, annotation),
74+
owner=(self, reference_type._type_impl),
75+
)
76+
elif isinstance(attr, OffsetAttribute):
77+
offset = attr.offset
78+
if offset < current_offset:
79+
raise ValueError("Offset must be greater than the current size.")
80+
current_offset = offset
81+
else:
82+
raise TypeError("Only Field and OffsetAttribute are allowed in attributes.")
83+
84+
# If we don't have a Field, we need to inflate the type as if we have no attributes
85+
if not resolved_type:
86+
resolved_type = inflater.inflater_for(annotation, owner=(self, reference_type._type_impl))
5987
else:
6088
resolved_type = inflater.inflater_for(annotation, owner=(self, reference_type._type_impl))
6189

@@ -72,8 +100,32 @@ def compute_own_size(cls: type[struct_impl], reference_type: type) -> None:
72100
for name, annotation in reference_type.__annotations__.items():
73101
if name in reference_type.__dict__:
74102
# Field associated with the annotation
75-
field = getattr(reference_type, name)
76-
attribute = cls._inflater.inflater_for((field, annotation))(None)
103+
attrs = getattr(reference_type, name)
104+
105+
# If attrs is not a tuple, we need to convert it to a tuple
106+
if not isinstance(attrs, tuple):
107+
attrs = (attrs,)
108+
109+
# Assert that in all attributes, there is only one Field
110+
if sum(isinstance(attr, Field) for attr in attrs) > 1:
111+
raise ValueError("Only one Field is allowed per attribute.")
112+
113+
attribute = None
114+
115+
for attr in attrs:
116+
if isinstance(attr, Field):
117+
attribute = cls._inflater.inflater_for((attr, annotation))(None)
118+
elif isinstance(attr, OffsetAttribute):
119+
offset = attr.offset
120+
if offset < size:
121+
raise ValueError("Offset must be greater than the current size.")
122+
size = offset
123+
else:
124+
raise TypeError("Only Field and OffsetAttribute are allowed in attributes.")
125+
126+
# If we don't have a Field, we need to inflate the attribute as if we have no attributes
127+
if not attribute:
128+
attribute = cls._inflater.inflater_for(annotation)
77129
elif isinstance(annotation, Field):
78130
attribute = cls._inflater.inflater_for((annotation, annotation.base_type))(None)
79131
else:
@@ -106,10 +158,7 @@ def freeze(self: struct_impl) -> None:
106158
def to_str(self: struct_impl, indent: int = 0) -> str:
107159
"""Return a string representation of the struct."""
108160
members = ",\n".join(
109-
[
110-
f"{' ' * (indent + 4)}{name}: {member.to_str(indent + 4)}"
111-
for name, member in self._members.items()
112-
],
161+
[f"{' ' * (indent + 4)}{name}: {member.to_str(indent + 4)}" for name, member in self._members.items()],
113162
)
114163
return f"""{self.name} {{
115164
{members}

libdestruct/common/utils.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,19 @@
1616
from libdestruct.common.obj import obj
1717

1818

19+
def is_field_bound_method(item: obj) -> bool:
20+
"""Check if the provided item is the bound method of a Field object."""
21+
return isinstance(item, MethodType) and isinstance(item.__self__, Field)
22+
23+
1924
def size_of(item_or_inflater: obj | callable[[Resolver], obj]) -> int:
2025
"""Return the size of an object, from an obj or it's inflater."""
2126
if hasattr(item_or_inflater, "size"):
2227
return item_or_inflater.size
2328

2429
# Check if item is the bound method of a Field
25-
if not isinstance(item_or_inflater, MethodType):
26-
raise TypeError("Provided inflater is not the bound method of a Field object")
27-
28-
field_object = item_or_inflater.__self__
29-
30-
if not isinstance(field_object, Field):
31-
raise TypeError("Provided inflater is not the bound method of a Field object")
30+
if is_field_bound_method(item_or_inflater):
31+
field_object = item_or_inflater.__self__
32+
return field_object.get_size()
3233

33-
return field_object.get_size()
34+
raise ValueError(f"Cannot determine the size of {item_or_inflater}")

0 commit comments

Comments
 (0)