Skip to content

Commit 9ba3cd4

Browse files
committed
Set types as attributes on the cstruct object
1 parent 0191708 commit 9ba3cd4

9 files changed

Lines changed: 232 additions & 112 deletions

File tree

dissect/cstruct/cstruct.py

Lines changed: 102 additions & 56 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,22 @@ 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+
"u8": "uint8",
109+
"u16": "uint16",
110+
"u32": "uint32",
111+
"u64": "uint64",
112+
"u128": "uint128",
113+
"__u8": "uint8",
114+
"__u16": "uint16",
115+
"__u32": "uint32",
116+
"__u64": "uint64",
117+
"__u128": "uint128",
118+
"uchar": "uint8",
119+
"ushort": "uint16",
120+
"uint": "uint32",
121+
"ulong": "uint32",
122+
105123
# Windows types
106124
"BYTE": "uint8",
107125
"CHAR": "char",
@@ -169,25 +187,12 @@ def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = N
169187
"_DWORD": "uint32",
170188
"_QWORD": "uint64",
171189
"_OWORD": "uint128",
172-
173-
# Other convenience types
174-
"u8": "uint8",
175-
"u16": "uint16",
176-
"u32": "uint32",
177-
"u64": "uint64",
178-
"u128": "uint128",
179-
"__u8": "uint8",
180-
"__u16": "uint16",
181-
"__u32": "uint32",
182-
"__u64": "uint64",
183-
"__u128": "uint128",
184-
"uchar": "uint8",
185-
"ushort": "uint16",
186-
"uint": "uint32",
187-
"ulong": "uint32",
188190
}
189191
# fmt: on
190192

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

204209
try:
205-
return self.resolve(self.typedefs[attr])
210+
return self.types[attr]
206211
except KeyError:
207212
pass
208213

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

211216
def __copy__(self) -> cstruct:
212217
cs = cstruct(endian=self.endian, pointer=self.pointer.__name__)
213218
cs._anonymous_count = self._anonymous_count
214-
cs.consts = self.consts.copy()
215219
cs.includes = self.includes.copy()
216220

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

222-
if isinstance(type, MetaType):
223-
new_type = copy.copy(type)
226+
if isinstance(type_, MetaType):
227+
new_type = copy.copy(type_)
224228
new_type.cs = cs
225-
cs.typedefs[name] = new_type
229+
cs.add_type(name, new_type)
230+
231+
for name, value in self.consts.items():
232+
cs.add_const(name, value)
226233

227234
return cs
228235

@@ -231,22 +238,34 @@ def _next_anonymous(self) -> str:
231238
self._anonymous_count += 1
232239
return name
233240

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

249-
self.typedefs[name] = type_
267+
self.types[name] = typeobj
268+
self._add_attr(name, typeobj)
250269

251270
addtype = add_type
252271

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

289+
def add_const(self, name: str, value: Any) -> None:
290+
"""Add a constant value.
291+
292+
Args:
293+
name: Name of the constant to be added.
294+
value: The value of the constant.
295+
"""
296+
self.consts[name] = value
297+
self._add_attr(name, value)
298+
299+
def del_const(self, name: str) -> None:
300+
"""Delete a constant value.
301+
302+
Args:
303+
name: Name of the constant to be deleted.
304+
305+
Raises:
306+
KeyError: If the constant does not exist.
307+
"""
308+
del self.consts[name]
309+
self.__dict__.pop(name, None)
310+
270311
def load(self, definition: str, deftype: int | None = None, **kwargs) -> cstruct:
271312
"""Parse structures from the given definitions using the given definition type.
272313
@@ -329,20 +370,13 @@ def resolve(self, name: type[BaseType] | str) -> type[BaseType]:
329370
Raises:
330371
ResolveError: If the type can't be resolved.
331372
"""
332-
type_name = name
333-
if not isinstance(type_name, str):
334-
return type_name
373+
if not isinstance(name, str):
374+
return name
335375

336-
for _ in range(10):
337-
if type_name not in self.typedefs:
338-
raise ResolveError(f"Unknown type {name}")
339-
340-
type_name = self.typedefs[type_name]
341-
342-
if not isinstance(type_name, str):
343-
return type_name
344-
345-
raise ResolveError(f"Recursion limit exceeded while resolving type {name}")
376+
try:
377+
return self.types[name]
378+
except KeyError:
379+
raise ResolveError(f"Unknown type {name}") from None
346380

347381
def copy(self) -> cstruct:
348382
"""Create a copy of this cstruct instance.
@@ -519,6 +553,21 @@ class void(Void): ...
519553
# signed long long: TypeAlias = int64
520554
# unsigned long long: TypeAlias = uint64
521555

556+
u8: TypeAlias = uint8
557+
u16: TypeAlias = uint16
558+
u32: TypeAlias = uint32
559+
u64: TypeAlias = uint64
560+
u128: TypeAlias = uint128
561+
__u8: TypeAlias = uint8
562+
__u16: TypeAlias = uint16
563+
__u32: TypeAlias = uint32
564+
__u64: TypeAlias = uint64
565+
__u128: TypeAlias = uint128
566+
uchar: TypeAlias = uint8
567+
ushort: TypeAlias = uint16
568+
uint: TypeAlias = uint32
569+
ulong: TypeAlias = uint32
570+
522571
BYTE: TypeAlias = uint8
523572
CHAR: TypeAlias = char
524573
SHORT: TypeAlias = int16
@@ -584,20 +633,17 @@ class void(Void): ...
584633
_QWORD: TypeAlias = uint64
585634
_OWORD: TypeAlias = uint128
586635

587-
u8: TypeAlias = uint8
588-
u16: TypeAlias = uint16
589-
u32: TypeAlias = uint32
590-
u64: TypeAlias = uint64
591-
u128: TypeAlias = uint128
592-
__u8: TypeAlias = uint8
593-
__u16: TypeAlias = uint16
594-
__u32: TypeAlias = uint32
595-
__u64: TypeAlias = uint64
596-
__u128: TypeAlias = uint128
597-
uchar: TypeAlias = uint8
598-
ushort: TypeAlias = uint16
599-
uint: TypeAlias = uint32
600-
ulong: TypeAlias = uint32
636+
637+
# Attribute names that types and constants may never shadow: the public API and methods of the cstruct
638+
# class itself, plus the instance attributes created in __init__.
639+
_RESERVED_NAMES = frozenset(dir(cstruct)) | {
640+
"endian",
641+
"consts",
642+
"types",
643+
"includes",
644+
"pointer",
645+
"_anonymous_count",
646+
}
601647

602648

603649
def ctypes(structure: type[Structure]) -> type[_ctypes.Structure]:

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

0 commit comments

Comments
 (0)