Skip to content

Commit 506d1df

Browse files
authored
Set types as attributes on the cstruct object (#114)
1 parent c229e3f commit 506d1df

9 files changed

Lines changed: 263 additions & 120 deletions

File tree

dissect/cstruct/cstruct.py

Lines changed: 104 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
Void,
2727
Wchar,
2828
)
29-
from dissect.cstruct.types.base import MetaType
3029

3130
if TYPE_CHECKING:
3231
from collections.abc import Iterable
@@ -55,10 +54,12 @@ class cstruct:
5554
def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = None):
5655
self.endian = endian
5756

58-
self.consts = {}
59-
self.includes = []
57+
self.consts: dict[str, int | str | bytes] = {}
58+
self.types: dict[str, type[BaseType]] = {}
59+
self.includes: list[str] = []
60+
6061
# fmt: off
61-
self.typedefs = {
62+
initial_types = {
6263
# Internal types
6364
"int8": self._make_packed_type("int8", "b", int),
6465
"uint8": self._make_packed_type("uint8", "B", int),
@@ -102,6 +103,22 @@ def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = N
102103
"signed long long": "int64",
103104
"unsigned long long": "uint64",
104105

106+
# Other convenience types
107+
"u8": "uint8",
108+
"u16": "uint16",
109+
"u32": "uint32",
110+
"u64": "uint64",
111+
"u128": "uint128",
112+
"__u8": "uint8",
113+
"__u16": "uint16",
114+
"__u32": "uint32",
115+
"__u64": "uint64",
116+
"__u128": "uint128",
117+
"uchar": "uint8",
118+
"ushort": "uint16",
119+
"uint": "uint32",
120+
"ulong": "uint32",
121+
105122
# Windows types
106123
"BYTE": "uint8",
107124
"CHAR": "char",
@@ -169,25 +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-
"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",
188189
}
189190
# fmt: on
190191

192+
for name, type_ in initial_types.items():
193+
self.add_type(name, type_)
194+
191195
pointer = pointer or ("uint64" if sys.maxsize > 2**32 else "uint32")
192196
self.pointer: type[BaseType] = self.resolve(pointer)
193197
self._anonymous_count = 0
@@ -202,27 +206,25 @@ def __getattr__(self, attr: str) -> Any:
202206
pass
203207

204208
try:
205-
return self.resolve(self.typedefs[attr])
209+
return self.types[attr]
206210
except KeyError:
207211
pass
208212

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

211215
def __copy__(self) -> cstruct:
212216
cs = cstruct(endian=self.endian, pointer=self.pointer.__name__)
213217
cs._anonymous_count = self._anonymous_count
214-
cs.consts = self.consts.copy()
215218
cs.includes = self.includes.copy()
216219

217-
# Update typedefs to point to the new cstruct instance
218-
for name, type in self.typedefs.items():
219-
if name in cs.typedefs:
220-
continue
220+
# Update types to point to the new cstruct instance
221+
for name, type_ in self.types.items():
222+
new_type = copy.copy(type_)
223+
new_type.cs = cs
224+
cs.add_type(name, new_type, replace=True)
221225

222-
if isinstance(type, MetaType):
223-
new_type = copy.copy(type)
224-
new_type.cs = cs
225-
cs.typedefs[name] = new_type
226+
for name, value in self.consts.items():
227+
cs.add_const(name, value)
226228

227229
return cs
228230

@@ -231,22 +233,34 @@ def _next_anonymous(self) -> str:
231233
self._anonymous_count += 1
232234
return name
233235

236+
def _add_attr(self, name: str, value: Any) -> None:
237+
# Names that collide with the cstruct class (e.g. a struct named ``load``) are not set as attributes
238+
# to avoid breaking the instance. They remain accessible through ``resolve`` and the type dicts.
239+
if name in _RESERVED_NAMES:
240+
return
241+
setattr(self, name, value)
242+
234243
def add_type(self, name: str, type_: type[BaseType] | str, replace: bool = False) -> None:
235-
"""Add a type or type reference.
244+
"""Add a type or type alias.
236245
237246
Only use this method when creating type aliases or adding already bound types.
247+
All types will be resolved to their actual type objects prior to being added.
238248
239249
Args:
240250
name: Name of the type to be added.
241251
type_: The type to be added. Can be a str reference to another type or a compatible type class.
252+
If a str is given, it will be resolved to the actual type object.
253+
replace: Whether to replace the type if it already exists.
242254
243255
Raises:
244256
ValueError: If the type already exists.
245257
"""
246-
if not replace and (name in self.typedefs and self.resolve(self.typedefs[name]) != self.resolve(type_)):
258+
typeobj = self.resolve(type_)
259+
if not replace and (existing := self.types.get(name)) is not None and existing is not typeobj:
247260
raise ValueError(f"Duplicate type: {name}")
248261

249-
self.typedefs[name] = type_
262+
self.types[name] = typeobj
263+
self._add_attr(name, typeobj)
250264

251265
addtype = add_type
252266

@@ -267,6 +281,28 @@ def add_custom_type(
267281
"""
268282
self.add_type(name, self._make_type(name, (type_,), size, alignment=alignment, attrs=kwargs))
269283

284+
def add_const(self, name: str, value: Any) -> None:
285+
"""Add a constant value.
286+
287+
Args:
288+
name: Name of the constant to be added.
289+
value: The value of the constant.
290+
"""
291+
self.consts[name] = value
292+
self._add_attr(name, value)
293+
294+
def del_const(self, name: str) -> None:
295+
"""Delete a constant value.
296+
297+
Args:
298+
name: Name of the constant to be deleted.
299+
300+
Raises:
301+
KeyError: If the constant does not exist.
302+
"""
303+
del self.consts[name]
304+
self.__dict__.pop(name, None)
305+
270306
def load(self, definition: str, deftype: int | None = None, **kwargs) -> cstruct:
271307
"""Parse structures from the given definitions using the given definition type.
272308
@@ -324,8 +360,8 @@ def cdef(self) -> str:
324360
if defines:
325361
blocks.append("\n".join(defines))
326362

327-
for name, typedef in self.typedefs.items():
328-
if name in empty.typedefs or not isinstance(typedef, type):
363+
for name, typedef in self.types.items():
364+
if name in empty.types or not isinstance(typedef, type):
329365
continue
330366

331367
if not issubclass(typedef, (Structure, Enum, Flag)):
@@ -365,20 +401,13 @@ def resolve(self, name: type[BaseType] | str) -> type[BaseType]:
365401
Raises:
366402
ResolveError: If the type can't be resolved.
367403
"""
368-
type_name = name
369-
if not isinstance(type_name, str):
370-
return type_name
404+
if not isinstance(name, str):
405+
return name
371406

372-
for _ in range(10):
373-
if type_name not in self.typedefs:
374-
raise ResolveError(f"Unknown type {name}")
375-
376-
type_name = self.typedefs[type_name]
377-
378-
if not isinstance(type_name, str):
379-
return type_name
380-
381-
raise ResolveError(f"Recursion limit exceeded while resolving type {name}")
407+
try:
408+
return self.types[name]
409+
except KeyError:
410+
raise ResolveError(f"Unknown type {name}") from None
382411

383412
def copy(self) -> cstruct:
384413
"""Create a copy of this cstruct instance.
@@ -555,6 +584,21 @@ class void(Void): ...
555584
# signed long long: TypeAlias = int64
556585
# unsigned long long: TypeAlias = uint64
557586

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
601+
558602
BYTE: TypeAlias = uint8
559603
CHAR: TypeAlias = char
560604
SHORT: TypeAlias = int16
@@ -620,20 +664,17 @@ class void(Void): ...
620664
_QWORD: TypeAlias = uint64
621665
_OWORD: TypeAlias = uint128
622666

623-
u8: TypeAlias = uint8
624-
u16: TypeAlias = uint16
625-
u32: TypeAlias = uint32
626-
u64: TypeAlias = uint64
627-
u128: TypeAlias = uint128
628-
__u8: TypeAlias = uint8
629-
__u16: TypeAlias = uint16
630-
__u32: TypeAlias = uint32
631-
__u64: TypeAlias = uint64
632-
__u128: TypeAlias = uint128
633-
uchar: TypeAlias = uint8
634-
ushort: TypeAlias = uint16
635-
uint: TypeAlias = uint32
636-
ulong: TypeAlias = uint32
667+
668+
# Attribute names that types and constants may never shadow: the public API and methods of the cstruct
669+
# class itself, plus the instance attributes created in __init__.
670+
_RESERVED_NAMES = frozenset(dir(cstruct)) | {
671+
"endian",
672+
"consts",
673+
"types",
674+
"includes",
675+
"pointer",
676+
"_anonymous_count",
677+
}
637678

638679

639680
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
@@ -150,7 +150,8 @@ def _parse(self) -> None:
150150

151151
# If it's an anonymous enum/flag, add its members to the constants for convenience
152152
if not type_.__name__:
153-
self.cs.consts.update(type_.__members__)
153+
for k, v in type_.__members__.items():
154+
self.cs.add_const(k, v)
154155

155156
self._expect(TokenType.SEMICOLON)
156157
else:
@@ -201,17 +202,17 @@ def _parse_define(self) -> None:
201202
# If evaluation fails, just keep it as a string (e.g. for macro-like constants)
202203
pass
203204

204-
self.cs.consts[name_token.value] = value
205+
self.cs.add_const(name_token.value, value)
205206

206207
def _parse_undef(self) -> None:
207208
"""Parse an undef directive and remove the constant."""
208209
self._expect(TokenType.PP_UNDEF)
209210

210211
name_token = self._expect(TokenType.IDENTIFIER)
211-
if name_token.value in self.cs.consts:
212-
del self.cs.consts[name_token.value]
213-
else:
214-
raise self._error(f"constant {name_token.value!r} not defined", token=name_token)
212+
try:
213+
self.cs.del_const(name_token.value)
214+
except KeyError:
215+
raise self._error(f"constant {name_token.value!r} not defined", token=name_token) from None
215216

216217
def _parse_include(self) -> None:
217218
"""Parse an include directive and add the included file to the includes list."""
@@ -544,7 +545,7 @@ def _parse_type_spec(self) -> type[BaseType]:
544545
):
545546
# This identifier is followed by a field delimiter, it should be the field name,
546547
# UNLESS the current parts don't form a valid type yet.
547-
if " ".join(parts) in self.cs.typedefs:
548+
if " ".join(parts) in self.cs.types:
548549
break
549550

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

0 commit comments

Comments
 (0)