Skip to content

Commit 541b7b6

Browse files
committed
Set types as attributes on the cstruct object
1 parent 7678a8f commit 541b7b6

9 files changed

Lines changed: 217 additions & 97 deletions

File tree

dissect/cstruct/cstruct.py

Lines changed: 87 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = N
5656
self.endian = endian
5757

5858
self.consts = {}
59+
self.types = {}
5960
self.includes = []
61+
6062
# fmt: off
61-
self.typedefs = {
63+
initial_types = {
6264
# Internal types
6365
"int8": self._make_packed_type("int8", "b", int),
6466
"uint8": self._make_packed_type("uint8", "B", int),
@@ -102,6 +104,21 @@ def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = N
102104
"signed long long": "int64",
103105
"unsigned long long": "uint64",
104106

107+
# Other convenience types
108+
"u1": "uint8",
109+
"u2": "uint16",
110+
"u4": "uint32",
111+
"u8": "uint64",
112+
"u16": "uint128",
113+
"__u8": "uint8",
114+
"__u16": "uint16",
115+
"__u32": "uint32",
116+
"__u64": "uint64",
117+
"uchar": "uint8",
118+
"ushort": "uint16",
119+
"uint": "uint32",
120+
"ulong": "uint32",
121+
105122
# Windows types
106123
"BYTE": "uint8",
107124
"CHAR": "char",
@@ -169,24 +186,12 @@ def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = N
169186
"_DWORD": "uint32",
170187
"_QWORD": "uint64",
171188
"_OWORD": "uint128",
172-
173-
# Other convenience types
174-
"u1": "uint8",
175-
"u2": "uint16",
176-
"u4": "uint32",
177-
"u8": "uint64",
178-
"u16": "uint128",
179-
"__u8": "uint8",
180-
"__u16": "uint16",
181-
"__u32": "uint32",
182-
"__u64": "uint64",
183-
"uchar": "uint8",
184-
"ushort": "uint16",
185-
"uint": "uint32",
186-
"ulong": "uint32",
187189
}
188190
# fmt: on
189191

192+
for name, type_ in initial_types.items():
193+
self.add_type(name, type_)
194+
190195
pointer = pointer or ("uint64" if sys.maxsize > 2**32 else "uint32")
191196
self.pointer: type[BaseType] = self.resolve(pointer)
192197
self._anonymous_count = 0
@@ -201,27 +206,29 @@ def __getattr__(self, attr: str) -> Any:
201206
pass
202207

203208
try:
204-
return self.resolve(self.typedefs[attr])
209+
return self.types[attr]
205210
except KeyError:
206211
pass
207212

208-
raise AttributeError(f"Invalid attribute: {attr}")
213+
raise AttributeError(f"'{type(self).__name__}' object has no attribute {attr!r}")
209214

210215
def __copy__(self) -> cstruct:
211216
cs = cstruct(endian=self.endian, pointer=self.pointer.__name__)
212217
cs._anonymous_count = self._anonymous_count
213-
cs.consts = self.consts.copy()
214218
cs.includes = self.includes.copy()
215219

216-
# Update typedefs to point to the new cstruct instance
217-
for name, type in self.typedefs.items():
218-
if name in cs.typedefs:
220+
# Update types to point to the new cstruct instance
221+
for name, type_ in self.types.items():
222+
if name in cs.types:
219223
continue
220224

221-
if isinstance(type, MetaType):
222-
new_type = copy.copy(type)
225+
if isinstance(type_, MetaType):
226+
new_type = copy.copy(type_)
223227
new_type.cs = cs
224-
cs.typedefs[name] = new_type
228+
cs.add_type(name, new_type)
229+
230+
for name, value in self.consts.items():
231+
cs.add_const(name, value)
225232

226233
return cs
227234

@@ -230,22 +237,34 @@ def _next_anonymous(self) -> str:
230237
self._anonymous_count += 1
231238
return name
232239

240+
def _add_attr(self, name: str, value: Any) -> None:
241+
# Names that collide with the cstruct class (e.g. a struct named ``load``) are not set as attributes
242+
# to avoid breaking the instance. They remain accessible through ``resolve`` and the type dicts.
243+
if name in _RESERVED_NAMES:
244+
return
245+
setattr(self, name, value)
246+
233247
def add_type(self, name: str, type_: type[BaseType] | str, replace: bool = False) -> None:
234-
"""Add a type or type reference.
248+
"""Add a type or type alias.
235249
236250
Only use this method when creating type aliases or adding already bound types.
251+
All types will be resolved to their actual type objects prior to being added.
237252
238253
Args:
239254
name: Name of the type to be added.
240255
type_: The type to be added. Can be a str reference to another type or a compatible type class.
256+
If a str is given, it will be resolved to the actual type object.
257+
replace: Whether to replace the type if it already exists.
241258
242259
Raises:
243260
ValueError: If the type already exists.
244261
"""
245-
if not replace and (name in self.typedefs and self.resolve(self.typedefs[name]) != self.resolve(type_)):
262+
typeobj = self.resolve(type_)
263+
if not replace and (existing := self.types.get(name)) is not None and existing is not typeobj:
246264
raise ValueError(f"Duplicate type: {name}")
247265

248-
self.typedefs[name] = type_
266+
self.types[name] = typeobj
267+
self._add_attr(name, typeobj)
249268

250269
addtype = add_type
251270

@@ -266,6 +285,28 @@ def add_custom_type(
266285
"""
267286
self.add_type(name, self._make_type(name, (type_,), size, alignment=alignment, attrs=kwargs))
268287

288+
def add_const(self, name: str, value: Any) -> None:
289+
"""Add a constant value.
290+
291+
Args:
292+
name: Name of the constant to be added.
293+
value: The value of the constant.
294+
"""
295+
self.consts[name] = value
296+
self._add_attr(name, value)
297+
298+
def del_const(self, name: str) -> None:
299+
"""Delete a constant value.
300+
301+
Args:
302+
name: Name of the constant to be deleted.
303+
304+
Raises:
305+
KeyError: If the constant does not exist.
306+
"""
307+
del self.consts[name]
308+
self.__dict__.pop(name, None)
309+
269310
def load(self, definition: str, deftype: int | None = None, **kwargs) -> cstruct:
270311
"""Parse structures from the given definitions using the given definition type.
271312
@@ -328,20 +369,13 @@ def resolve(self, name: type[BaseType] | str) -> type[BaseType]:
328369
Raises:
329370
ResolveError: If the type can't be resolved.
330371
"""
331-
type_name = name
332-
if not isinstance(type_name, str):
333-
return type_name
334-
335-
for _ in range(10):
336-
if type_name not in self.typedefs:
337-
raise ResolveError(f"Unknown type {name}")
372+
if not isinstance(name, str):
373+
return name
338374

339-
type_name = self.typedefs[type_name]
340-
341-
if not isinstance(type_name, str):
342-
return type_name
343-
344-
raise ResolveError(f"Recursion limit exceeded while resolving type {name}")
375+
try:
376+
return self.types[name]
377+
except KeyError:
378+
raise ResolveError(f"Unknown type {name}") from None
345379

346380
def copy(self) -> cstruct:
347381
"""Create a copy of this cstruct instance.
@@ -598,6 +632,18 @@ class void(Void): ...
598632
ulong: TypeAlias = uint32
599633

600634

635+
# Attribute names that types and constants may never shadow: the public API and methods of the cstruct
636+
# class itself, plus the instance attributes created in __init__.
637+
_RESERVED_NAMES = frozenset(dir(cstruct)) | {
638+
"endian",
639+
"consts",
640+
"types",
641+
"includes",
642+
"pointer",
643+
"_anonymous_count",
644+
}
645+
646+
601647
def ctypes(structure: type[Structure]) -> type[_ctypes.Structure]:
602648
"""Create ctypes structures from cstruct structures."""
603649
fields = []

dissect/cstruct/expression.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from dissect.cstruct import cstruct
1313
from dissect.cstruct.lexer import Token
1414

15+
_MISSING = object()
16+
1517
BINARY_OPERATORS: dict[TokenType, Callable[[int, int], int]] = {
1618
TokenType.PIPE: lambda a, b: a | b,
1719
TokenType.CARET: lambda a, b: a ^ b,
@@ -164,16 +166,13 @@ def evaluate(self, cs: cstruct, context: dict[str, int] | None = None) -> int:
164166
obj = context[part]
165167
elif part in cs.consts:
166168
obj = cs.consts[part]
167-
elif part in cs.typedefs:
168-
obj = cs.resolve(part)
169+
elif part in cs.types:
170+
obj = cs.types[part]
169171
else:
170172
raise self._error(f"Unknown identifier: '{ident}'", token=token)
171173
else:
172-
if isinstance(obj, dict) and part in obj:
173-
obj = obj[part]
174-
elif hasattr(obj, part):
175-
obj = getattr(obj, part)
176-
else:
174+
obj = obj.get(part, _MISSING) if isinstance(obj, dict) else getattr(obj, part, _MISSING)
175+
if obj is _MISSING:
177176
raise self._error(f"Unknown identifier: '{ident}'", token=token)
178177

179178
try:

dissect/cstruct/parser.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ def _parse(self) -> None:
142142

143143
# If it's an anonymous enum/flag, add its members to the constants for convenience
144144
if not type_.__name__:
145-
self.cs.consts.update(type_.__members__)
145+
for k, v in type_.__members__.items():
146+
self.cs.add_const(k, v)
146147

147148
self._expect(TokenType.SEMICOLON)
148149
else:
@@ -192,17 +193,17 @@ def _parse_define(self) -> None:
192193
# If evaluation fails, just keep it as a string (e.g. for macro-like constants)
193194
pass
194195

195-
self.cs.consts[name_token.value] = value
196+
self.cs.add_const(name_token.value, value)
196197

197198
def _parse_undef(self) -> None:
198199
"""Parse an undef directive and remove the constant."""
199200
self._expect(TokenType.PP_UNDEF)
200201

201202
name_token = self._expect(TokenType.IDENTIFIER)
202-
if name_token.value in self.cs.consts:
203-
del self.cs.consts[name_token.value]
204-
else:
205-
raise self._error(f"constant {name_token.value!r} not defined", token=name_token)
203+
try:
204+
self.cs.del_const(name_token.value)
205+
except KeyError:
206+
raise self._error(f"constant {name_token.value!r} not defined", token=name_token) from None
206207

207208
def _parse_include(self) -> None:
208209
"""Parse an include directive and add the included file to the includes list."""
@@ -527,7 +528,7 @@ def _parse_type_spec(self) -> type[BaseType]:
527528
):
528529
# This identifier is followed by a field delimiter, it should be the field name,
529530
# UNLESS the current parts don't form a valid type yet.
530-
if " ".join(parts) in self.cs.typedefs:
531+
if " ".join(parts) in self.cs.types:
531532
break
532533

533534
# Current parts don't resolve, consume and hope this completes the type name

dissect/cstruct/tools/stubgen.py

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -79,31 +79,29 @@ def generate_cstruct_stub(cs: cstruct, module_prefix: str = "", cls_name: str =
7979

8080
defined_names = set()
8181

82-
# Then typedefs
83-
for name, typedef in cs.typedefs.items():
84-
if name in empty_cs.typedefs:
82+
# Then types
83+
for name, type_ in cs.types.items():
84+
if name in empty_cs.types:
8585
continue
8686

87-
if typedef.__name__ in empty_cs.typedefs:
88-
stub = f"{name}: TypeAlias = {cs_prefix}{typedef.__name__}"
89-
elif typedef.__name__ in defined_names:
87+
if type_.__name__ in empty_cs.types:
88+
stub = f"{name}: TypeAlias = {cs_prefix}{type_.__name__}"
89+
elif type_.__name__ in defined_names:
9090
# Create an alias to the type if we have already seen it before.
91-
stub = f"{name}: TypeAlias = {typedef.__name__}"
92-
elif issubclass(typedef, (types.Enum, types.Flag)):
93-
stub = generate_enum_stub(typedef, cs_prefix=cs_prefix, module_prefix=module_prefix)
94-
elif issubclass(typedef, types.Pointer):
95-
typehint = generate_typehint(typedef, prefix=cs_prefix, module_prefix=module_prefix)
91+
stub = f"{name}: TypeAlias = {type_.__name__}"
92+
elif issubclass(type_, (types.Enum, types.Flag)):
93+
stub = generate_enum_stub(type_, cs_prefix=cs_prefix, module_prefix=module_prefix)
94+
elif issubclass(type_, types.Pointer):
95+
typehint = generate_typehint(type_, prefix=cs_prefix, module_prefix=module_prefix)
9696
stub = f"{name}: TypeAlias = {typehint}"
97-
elif issubclass(typedef, types.Structure):
98-
stub = generate_structure_stub(typedef, cs_prefix=cs_prefix, module_prefix=module_prefix)
99-
elif issubclass(typedef, types.BaseType):
100-
stub = generate_generic_stub(typedef, cs_prefix=cs_prefix, module_prefix=module_prefix)
101-
elif isinstance(typedef, str):
102-
stub = f"{name}: TypeAlias = {typedef}"
97+
elif issubclass(type_, types.Structure):
98+
stub = generate_structure_stub(type_, cs_prefix=cs_prefix, module_prefix=module_prefix)
99+
elif issubclass(type_, types.BaseType):
100+
stub = generate_generic_stub(type_, cs_prefix=cs_prefix, module_prefix=module_prefix)
103101
else:
104-
raise TypeError(f"Unknown typedef: {typedef}")
102+
raise TypeError(f"Unknown type: {type_}")
105103

106-
defined_names.add(typedef.__name__)
104+
defined_names.add(type_.__name__)
107105

108106
body.append(textwrap.indent(stub, prefix=indent))
109107

@@ -158,7 +156,6 @@ def generate_structure_stub(
158156
module_prefix: str = "",
159157
) -> str:
160158
result = [f"class {name_prefix}{structure.__name__}({module_prefix}{structure.__base__.__name__}):"]
161-
162159
indent = " " * 4
163160

164161
args = ["self"]
@@ -171,7 +168,7 @@ def generate_structure_stub(
171168
while issubclass(nested_type, types.BaseArray):
172169
nested_type = nested_type.type
173170

174-
if issubclass(nested_type, types.Structure) and type_name not in structure.cs.typedefs:
171+
if issubclass(nested_type, types.Structure) and type_name not in structure.cs.types:
175172
inlined = True
176173
inline_stub = generate_structure_stub(nested_type, cs_prefix=cs_prefix, module_prefix=module_prefix)
177174

dissect/cstruct/types/structure.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ def _write(cls, stream: BinaryIO, data: Structure) -> int:
306306
num = 0
307307

308308
for field in cls.__fields__:
309-
field_type = cls.cs.resolve(field.type)
309+
field_type = field.type
310310

311311
bit_field_type = (
312312
(field_type.type if isinstance(field_type, EnumMetaType) else field_type) if field.bits else None
@@ -515,7 +515,7 @@ def _read_fields(
515515
buf = io.BytesIO(stream.read(cls.size))
516516

517517
for field in cls.__fields__:
518-
field_type = cls.cs.resolve(field.type)
518+
field_type = field.type
519519

520520
start = 0
521521
if field.offset is not None:

tests/test_annotations.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
from dissect.cstruct.cstruct import cstruct
99
from dissect.cstruct.types.base import BaseType
1010

11+
CS = cstruct()
12+
1113

1214
@pytest.mark.parametrize(
1315
"name",
14-
[name for name in cstruct().typedefs if " " not in name],
16+
[name for name in CS.types if " " not in name],
1517
)
1618
def test_cstruct_type_annotation(name: str, monkeypatch: pytest.MonkeyPatch) -> None:
19+
"""Verify that all default types defined in cstruct have type annotations."""
1720
with (
1821
patch("typing.TYPE_CHECKING", True),
1922
patch("dissect.cstruct.types.base.MetaType.__getitem__", lambda self, item: self),

0 commit comments

Comments
 (0)