Skip to content

Commit 9bf45e6

Browse files
committed
Set types as attributes on the cstruct object
1 parent 194b1b5 commit 9bf45e6

7 files changed

Lines changed: 120 additions & 55 deletions

File tree

dissect/cstruct/cstruct.py

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,12 @@ def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = N
5252

5353
self.consts = {}
5454
self.lookups = {}
55+
self.types = {}
56+
self.typedefs = {}
5557
self.includes = []
58+
5659
# fmt: off
57-
self.typedefs = {
60+
initial_types = {
5861
# Internal types
5962
"int8": self._make_packed_type("int8", "b", int),
6063
"uint8": self._make_packed_type("uint8", "B", int),
@@ -98,6 +101,21 @@ def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = N
98101
"signed long long": "int64",
99102
"unsigned long long": "uint64",
100103

104+
# Other convenience types
105+
"u1": "uint8",
106+
"u2": "uint16",
107+
"u4": "uint32",
108+
"u8": "uint64",
109+
"u16": "uint128",
110+
"__u8": "uint8",
111+
"__u16": "uint16",
112+
"__u32": "uint32",
113+
"__u64": "uint64",
114+
"uchar": "uint8",
115+
"ushort": "uint16",
116+
"uint": "uint32",
117+
"ulong": "uint32",
118+
101119
# Windows types
102120
"BYTE": "uint8",
103121
"CHAR": "char",
@@ -165,24 +183,12 @@ def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = N
165183
"_DWORD": "uint32",
166184
"_QWORD": "uint64",
167185
"_OWORD": "uint128",
168-
169-
# Other convenience types
170-
"u1": "uint8",
171-
"u2": "uint16",
172-
"u4": "uint32",
173-
"u8": "uint64",
174-
"u16": "uint128",
175-
"__u8": "uint8",
176-
"__u16": "uint16",
177-
"__u32": "uint32",
178-
"__u64": "uint64",
179-
"uchar": "uint8",
180-
"ushort": "uint16",
181-
"uint": "uint32",
182-
"ulong": "uint32",
183186
}
184187
# fmt: on
185188

189+
for name, type_ in initial_types.items():
190+
self.add_type(name, type_)
191+
186192
pointer = pointer or ("uint64" if sys.maxsize > 2**32 else "uint32")
187193
self.pointer: type[BaseType] = self.resolve(pointer)
188194
self._anonymous_count = 0
@@ -196,37 +202,71 @@ def __getattr__(self, attr: str) -> Any:
196202
except KeyError:
197203
pass
198204

205+
try:
206+
return self.types[attr]
207+
except KeyError:
208+
pass
209+
199210
try:
200211
return self.resolve(self.typedefs[attr])
201212
except KeyError:
202213
pass
203214

204-
raise AttributeError(f"Invalid attribute: {attr}")
215+
return super().__getattribute__(attr)
205216

206217
def _next_anonymous(self) -> str:
207218
name = f"__anonymous_{self._anonymous_count}__"
208219
self._anonymous_count += 1
209220
return name
210221

222+
def _add_attr(self, name: str, value: Any, replace: bool = False) -> None:
223+
if not replace and (name in self.__dict__ and self.__dict__[name] != value):
224+
raise ValueError(f"Attribute already exists: {name}")
225+
setattr(self, name, value)
226+
211227
def add_type(self, name: str, type_: type[BaseType] | str, replace: bool = False) -> None:
212228
"""Add a type or type reference.
213229
214230
Only use this method when creating type aliases or adding already bound types.
231+
All types will be resolved to their actual type objects prior to being added.
232+
Use :func:`add_typedef` to add type references.
215233
216234
Args:
217235
name: Name of the type to be added.
218236
type_: The type to be added. Can be a str reference to another type or a compatible type class.
237+
If a str is given, it will be resolved to the actual type object.
219238
220239
Raises:
221240
ValueError: If the type already exists.
222241
"""
223-
if not replace and (name in self.typedefs and self.resolve(self.typedefs[name]) != self.resolve(type_)):
242+
typeobj = self.resolve(type_)
243+
if not replace and (name in self.types and self.types[name] != typeobj):
224244
raise ValueError(f"Duplicate type: {name}")
225245

226-
self.typedefs[name] = type_
246+
self.types[name] = typeobj
247+
self._add_attr(name, typeobj, replace=replace)
227248

228249
addtype = add_type
229250

251+
def add_typedef(self, name: str, type_: str, replace: bool = False) -> None:
252+
"""Add a type reference.
253+
254+
Use this method to add type references to this cstruct instance. These are type names that can be
255+
dynamically resolved at a later stage. Use :func:`add_type` to add actual type objects.
256+
257+
Args:
258+
name: Name of the type to be added.
259+
type_: The type reference to be added.
260+
replace: Whether to replace the type if it already exists.
261+
"""
262+
if not isinstance(type_, str):
263+
raise TypeError("Type reference must be a string")
264+
265+
if not replace and (name in self.typedefs and self.resolve(self.typedefs[name]) != self.resolve(type_)):
266+
raise ValueError(f"Duplicate type: {name}")
267+
268+
self.typedefs[name] = type_
269+
230270
def add_custom_type(
231271
self, name: str, type_: type[BaseType], size: int | None = None, alignment: int | None = None, **kwargs
232272
) -> None:
@@ -244,6 +284,16 @@ def add_custom_type(
244284
"""
245285
self.add_type(name, self._make_type(name, (type_,), size, alignment=alignment, attrs=kwargs))
246286

287+
def add_const(self, name: str, value: Any) -> None:
288+
"""Add a constant value.
289+
290+
Args:
291+
name: Name of the constant to be added.
292+
value: The value of the constant.
293+
"""
294+
self.consts[name] = value
295+
self._add_attr(name, value, replace=True)
296+
247297
def load(self, definition: str, deftype: int | None = None, **kwargs) -> cstruct:
248298
"""Parse structures from the given definitions using the given definition type.
249299
@@ -315,14 +365,14 @@ def resolve(self, name: type[BaseType] | str) -> type[BaseType]:
315365
return type_name
316366

317367
for _ in range(10):
368+
if type_name in self.types:
369+
return self.types[type_name]
370+
318371
if type_name not in self.typedefs:
319372
raise ResolveError(f"Unknown type {name}")
320373

321374
type_name = self.typedefs[type_name]
322375

323-
if not isinstance(type_name, str):
324-
return type_name
325-
326376
raise ResolveError(f"Recursion limit exceeded while resolving type {name}")
327377

328378
def _make_type(

dissect/cstruct/parser.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def _constant(self, tokens: TokenConsumer) -> None:
153153
except (ExpressionParserError, ExpressionTokenizerError):
154154
pass
155155

156-
self.cstruct.consts[match["name"]] = value
156+
self.cstruct.add_const(match["name"], value)
157157

158158
def _undef(self, tokens: TokenConsumer) -> None:
159159
const = tokens.consume()
@@ -204,20 +204,23 @@ def _enum(self, tokens: TokenConsumer) -> None:
204204

205205
enum = factory(d["name"] or "", self.cstruct.resolve(d["type"]), values)
206206
if not enum.__name__:
207-
self.cstruct.consts.update(enum.__members__)
207+
for k, v in enum.__members__.items():
208+
self.cstruct.add_const(k, v)
208209
else:
209210
self.cstruct.add_type(enum.__name__, enum)
210211

211212
tokens.eol()
212213

213214
def _typedef(self, tokens: TokenConsumer) -> None:
214215
tokens.consume()
216+
type_name = None
215217
type_ = None
216218

217219
names = []
218220

219221
if tokens.next == self.TOK.IDENTIFIER:
220-
type_ = self.cstruct.resolve(self._identifier(tokens))
222+
type_name = self._identifier(tokens)
223+
type_ = self.cstruct.resolve(type_name)
221224
elif tokens.next == self.TOK.STRUCT:
222225
type_ = self._struct(tokens)
223226
if not type_.__anonymous__:
@@ -230,10 +233,13 @@ def _typedef(self, tokens: TokenConsumer) -> None:
230233
type_.__name__ = name
231234
type_.__qualname__ = name
232235

233-
type_, name, bits = self._parse_field_type(type_, name)
236+
new_type, name, bits = self._parse_field_type(type_, name)
234237
if bits is not None:
235238
raise ParserError(f"line {self._lineno(tokens.previous)}: typedefs cannot have bitfields")
236-
self.cstruct.add_type(name, type_)
239+
if type_name is None or new_type is not type_:
240+
self.cstruct.add_type(name, new_type)
241+
else:
242+
self.cstruct.add_typedef(name, type_name)
237243

238244
def _struct(self, tokens: TokenConsumer, register: bool = False) -> type[Structure]:
239245
stype = tokens.consume()
@@ -496,7 +502,7 @@ def _constants(self, data: str) -> None:
496502
except (ValueError, SyntaxError):
497503
pass
498504

499-
self.cstruct.consts[d["name"]] = v
505+
self.cstruct.add_const(d["name"], v)
500506

501507
def _enums(self, data: str) -> None:
502508
r = re.finditer(
@@ -578,7 +584,7 @@ def _structs(self, data: str) -> None:
578584
if d["defs"]:
579585
for td in d["defs"].strip().split(","):
580586
td = td.strip()
581-
self.cstruct.add_type(td, st)
587+
self.cstruct.add_typedef(td, st)
582588

583589
def _parse_fields(self, data: str) -> None:
584590
fields = re.finditer(

dissect/cstruct/tools/stubgen.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,32 +79,41 @@ 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)
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)
9494
elif issubclass(typedef, types.Pointer):
9595
typehint = generate_typehint(typedef, 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_}")
103+
104+
defined_names.add(type_.__name__)
105+
106+
body.append(textwrap.indent(stub, prefix=indent))
107+
108+
# Then typedefs
109+
for name, typedef in cs.typedefs.items():
110+
if name in empty_cs.typedefs:
111+
continue
105112

106-
defined_names.add(typedef.__name__)
113+
if not isinstance(typedef, str):
114+
raise TypeError(f"Expected typedef to be a string, got {type(typedef)} for {name}")
107115

116+
stub = f"{name}: TypeAlias = {cs_prefix}{typedef}"
108117
body.append(textwrap.indent(stub, prefix=indent))
109118

110119
if not body:

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_basic.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_load_file(cs: cstruct, compiled: bool, tmp_path: Path) -> None:
3737
tmp_path.joinpath("testdef.txt").write_text(textwrap.dedent(cdef))
3838

3939
cs.loadfile(tmp_path.joinpath("testdef.txt"), compiled=compiled)
40-
assert "test" in cs.typedefs
40+
assert "test" in cs.types
4141

4242

4343
def test_load_init() -> None:
@@ -97,7 +97,7 @@ def test_type_resolve(cs: cstruct) -> None:
9797

9898
cs.add_type("ref0", "uint32")
9999
for i in range(1, 15): # Recursion limit is currently 10
100-
cs.add_type(f"ref{i}", f"ref{i - 1}")
100+
cs.add_typedef(f"ref{i}", f"ref{i - 1}")
101101

102102
with pytest.raises(ResolveError, match="Recursion limit exceeded"):
103103
cs.resolve("ref14")
@@ -455,7 +455,7 @@ def test_reserved_keyword(cs: cstruct, compiled: bool) -> None:
455455
cs.load(cdef, compiled=compiled)
456456

457457
for name in ["in", "class", "for"]:
458-
assert name in cs.typedefs
458+
assert name in cs.types
459459
assert verify_compiled(cs.resolve(name), compiled)
460460

461461
assert cs.resolve(name)(b"\x01").a == 1

tests/test_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def test_structure_names(cs: cstruct) -> None:
119119
"""
120120
cs.load(cdef)
121121

122-
assert all(c in cs.typedefs for c in ("a", "b", "c", "d", "e"))
122+
assert all(c in cs.typedefs | cs.types for c in ("a", "b", "c", "d", "e"))
123123

124124
assert cs.a.__name__ == "a"
125125
assert cs.b.__name__ == "b"

tests/test_tools_stubgen.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,6 @@ def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
288288
""",
289289
"""
290290
class cstruct(cstruct):
291-
__fs16: TypeAlias = cstruct.uint16
292-
__fs32: TypeAlias = cstruct.uint32
293-
__fs64: TypeAlias = cstruct.uint64
294291
class Test(Structure):
295292
a: cstruct.uint16
296293
b: cstruct.uint32
@@ -300,6 +297,9 @@ def __init__(self, a: cstruct.uint16 | None = ..., b: cstruct.uint32 | None = ..
300297
@overload
301298
def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
302299
300+
__fs16: TypeAlias = cstruct.__u16
301+
__fs32: TypeAlias = cstruct.__u32
302+
__fs64: TypeAlias = cstruct.__u64
303303
""", # noqa: E501
304304
id="typedef stub",
305305
),

0 commit comments

Comments
 (0)