dissect.cstruct 5.0.dev0__py3-none-any.whl → 5.0.dev7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. dissect/cstruct/__init__.py +2 -2
  2. dissect/cstruct/bitbuffer.py +3 -3
  3. dissect/cstruct/compiler.py +7 -7
  4. dissect/cstruct/cstruct.py +192 -65
  5. dissect/cstruct/expression.py +8 -9
  6. dissect/cstruct/lexer.py +17 -7
  7. dissect/cstruct/parser.py +31 -13
  8. dissect/cstruct/tools/stubgen.py +18 -22
  9. dissect/cstruct/types/base.py +78 -33
  10. dissect/cstruct/types/char.py +21 -11
  11. dissect/cstruct/types/enum.py +33 -13
  12. dissect/cstruct/types/int.py +9 -7
  13. dissect/cstruct/types/leb128.py +6 -4
  14. dissect/cstruct/types/packed.py +19 -10
  15. dissect/cstruct/types/pointer.py +74 -56
  16. dissect/cstruct/types/structure.py +130 -30
  17. dissect/cstruct/types/void.py +6 -4
  18. dissect/cstruct/types/wchar.py +26 -14
  19. dissect/cstruct/{utils.py → util.py} +36 -25
  20. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/METADATA +1 -1
  21. dissect_cstruct-5.0.dev7.dist-info/RECORD +32 -0
  22. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/scm_file_list.json +2 -2
  23. dissect_cstruct-5.0.dev7.dist-info/scm_version.json +8 -0
  24. dissect_cstruct-5.0.dev0.dist-info/RECORD +0 -32
  25. dissect_cstruct-5.0.dev0.dist-info/scm_version.json +0 -8
  26. /dissect/cstruct/{exceptions.py → exception.py} +0 -0
  27. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/WHEEL +0 -0
  28. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/entry_points.txt +0 -0
  29. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/licenses/COPYRIGHT +0 -0
  30. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/licenses/LICENSE +0 -0
  31. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,8 @@ from dissect.cstruct.types.base import BaseType
7
7
  if TYPE_CHECKING:
8
8
  from typing_extensions import Self
9
9
 
10
+ from dissect.cstruct.cstruct import Endianness
11
+
10
12
 
11
13
  class LEB128(int, BaseType):
12
14
  """Variable-length code compression to store an arbitrarily large integer in a small number of bytes.
@@ -17,7 +19,7 @@ class LEB128(int, BaseType):
17
19
  signed: bool
18
20
 
19
21
  @classmethod
20
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self:
22
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self:
21
23
  result = 0
22
24
  shift = 0
23
25
  while True:
@@ -37,11 +39,11 @@ class LEB128(int, BaseType):
37
39
  return cls.__new__(cls, result)
38
40
 
39
41
  @classmethod
40
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> list[Self]:
42
+ def _read_0(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> list[Self]:
41
43
  result = []
42
44
 
43
45
  while True:
44
- if (value := cls._read(stream, context)) == 0:
46
+ if (value := cls._read(stream, context=context, endian=endian)) == 0:
45
47
  break
46
48
 
47
49
  result.append(value)
@@ -49,7 +51,7 @@ class LEB128(int, BaseType):
49
51
  return result
50
52
 
51
53
  @classmethod
52
- def _write(cls, stream: BinaryIO, data: int) -> int:
54
+ def _write(cls, stream: BinaryIO, data: int, *, endian: Endianness) -> int:
53
55
  # only write negative numbers when in signed mode
54
56
  if data < 0 and not cls.signed:
55
57
  raise ValueError("Attempt to encode a negative integer using unsigned LEB128 encoding")
@@ -9,6 +9,8 @@ from dissect.cstruct.types.base import EOF, BaseType
9
9
  if TYPE_CHECKING:
10
10
  from typing_extensions import Self
11
11
 
12
+ from dissect.cstruct.cstruct import Endianness
13
+
12
14
 
13
15
  @lru_cache(1024)
14
16
  def _struct(endian: str, packchar: str) -> Struct:
@@ -24,11 +26,18 @@ class Packed(BaseType, Generic[T]):
24
26
  packchar: str
25
27
 
26
28
  @classmethod
27
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self:
28
- return cls._read_array(stream, 1, context)[0]
29
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self:
30
+ data = stream.read(cls.size)
31
+
32
+ if len(data) != cls.size:
33
+ raise EOFError(f"Read {len(data)} bytes, but expected {cls.size}")
34
+
35
+ return cls.__new__(cls, _struct(endian, cls.packchar).unpack(data)[0])
29
36
 
30
37
  @classmethod
31
- def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] | None = None) -> list[Self]:
38
+ def _read_array(
39
+ cls, stream: BinaryIO, count: int, *, context: dict[str, Any] | None = None, endian: Endianness
40
+ ) -> list[Self]:
32
41
  if count == EOF:
33
42
  data = stream.read()
34
43
  length = len(data)
@@ -37,7 +46,7 @@ class Packed(BaseType, Generic[T]):
37
46
  length = cls.size * count
38
47
  data = stream.read(length)
39
48
 
40
- fmt = _struct(cls.cs.endian, f"{count}{cls.packchar}")
49
+ fmt = _struct(endian, f"{count}{cls.packchar}")
41
50
 
42
51
  if len(data) != length:
43
52
  raise EOFError(f"Read {len(data)} bytes, but expected {length}")
@@ -45,10 +54,10 @@ class Packed(BaseType, Generic[T]):
45
54
  return [cls.__new__(cls, value) for value in fmt.unpack(data)]
46
55
 
47
56
  @classmethod
48
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self:
57
+ def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None, *, endian: Endianness) -> Self:
49
58
  result = []
50
59
 
51
- fmt = _struct(cls.cs.endian, cls.packchar)
60
+ fmt = _struct(endian, cls.packchar)
52
61
  while True:
53
62
  data = stream.read(cls.size)
54
63
 
@@ -63,9 +72,9 @@ class Packed(BaseType, Generic[T]):
63
72
  return result
64
73
 
65
74
  @classmethod
66
- def _write(cls, stream: BinaryIO, data: Packed[T]) -> int:
67
- return stream.write(_struct(cls.cs.endian, cls.packchar).pack(data))
75
+ def _write(cls, stream: BinaryIO, data: Packed[T], *, endian: Endianness) -> int:
76
+ return stream.write(_struct(endian, cls.packchar).pack(data))
68
77
 
69
78
  @classmethod
70
- def _write_array(cls, stream: BinaryIO, data: list[Packed[T]]) -> int:
71
- return stream.write(_struct(cls.cs.endian, f"{len(data)}{cls.packchar}").pack(*data))
79
+ def _write_array(cls, stream: BinaryIO, data: list[Packed[T]], *, endian: Endianness) -> int:
80
+ return stream.write(_struct(endian, f"{len(data)}{cls.packchar}").pack(*data))
@@ -1,15 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from functools import cache
3
4
  from typing import TYPE_CHECKING, Any, BinaryIO, Generic, TypeVar
4
5
 
5
- from dissect.cstruct.exceptions import NullPointerDereference
6
- from dissect.cstruct.types.base import BaseType
6
+ from dissect.cstruct.exception import NullPointerDereference
7
+ from dissect.cstruct.types.base import BaseType, normalize_endianness
7
8
  from dissect.cstruct.types.char import Char
8
9
  from dissect.cstruct.types.void import Void
9
10
 
10
11
  if TYPE_CHECKING:
12
+ from collections.abc import Callable
13
+
11
14
  from typing_extensions import Self
12
15
 
16
+ from dissect.cstruct.cstruct import AllowedEndianness, Endianness
17
+
13
18
  T = TypeVar("T", bound=BaseType)
14
19
 
15
20
 
@@ -19,13 +24,16 @@ class Pointer(int, BaseType, Generic[T]):
19
24
  type: type[T]
20
25
  _stream: BinaryIO | None
21
26
  _context: dict[str, Any] | None
22
- _value: T | None
27
+ _endian: Endianness
23
28
 
24
- def __new__(cls, value: int, stream: BinaryIO | None, context: dict[str, Any] | None = None) -> Self:
29
+ def __new__(
30
+ cls, value: int, stream: BinaryIO | None, *, context: dict[str, Any] | None = None, endian: Endianness
31
+ ) -> Self:
25
32
  obj = super().__new__(cls, value)
26
33
  obj._stream = stream
27
34
  obj._context = context
28
- obj._value = None
35
+ obj._endian = endian
36
+ obj.dereference = cache(obj.dereference)
29
37
  return obj
30
38
 
31
39
  def __repr__(self) -> str:
@@ -37,68 +45,78 @@ class Pointer(int, BaseType, Generic[T]):
37
45
  def __getattr__(self, attr: str) -> Any:
38
46
  return getattr(self.dereference(), attr)
39
47
 
40
- def __add__(self, other: int) -> Self:
41
- return type.__call__(self.__class__, int.__add__(self, other), self._stream, self._context)
42
-
43
- def __sub__(self, other: int) -> Self:
44
- return type.__call__(self.__class__, int.__sub__(self, other), self._stream, self._context)
45
-
46
- def __mul__(self, other: int) -> Self:
47
- return type.__call__(self.__class__, int.__mul__(self, other), self._stream, self._context)
48
-
49
- def __floordiv__(self, other: int) -> Self:
50
- return type.__call__(self.__class__, int.__floordiv__(self, other), self._stream, self._context)
51
-
52
- def __mod__(self, other: int) -> Self:
53
- return type.__call__(self.__class__, int.__mod__(self, other), self._stream, self._context)
54
-
55
- def __pow__(self, other: int) -> Self:
56
- return type.__call__(self.__class__, int.__pow__(self, other), self._stream, self._context)
57
-
58
- def __lshift__(self, other: int) -> Self:
59
- return type.__call__(self.__class__, int.__lshift__(self, other), self._stream, self._context)
60
-
61
- def __rshift__(self, other: int) -> Self:
62
- return type.__call__(self.__class__, int.__rshift__(self, other), self._stream, self._context)
63
-
64
- def __and__(self, other: int) -> Self:
65
- return type.__call__(self.__class__, int.__and__(self, other), self._stream, self._context)
66
-
67
- def __xor__(self, other: int) -> Self:
68
- return type.__call__(self.__class__, int.__xor__(self, other), self._stream, self._context)
69
-
70
- def __or__(self, other: int) -> Self:
71
- return type.__call__(self.__class__, int.__or__(self, other), self._stream, self._context)
48
+ @staticmethod
49
+ def __op(op: Callable[[int, int], int]) -> Callable[[Self, int], Self]:
50
+ def method(self: Self, other: int) -> Self:
51
+ return type.__call__(
52
+ self.__class__,
53
+ op(self, other),
54
+ self._stream,
55
+ context=self._context,
56
+ endian=self._endian,
57
+ )
58
+
59
+ return method
60
+
61
+ __add__ = __op(int.__add__)
62
+ __sub__ = __op(int.__sub__)
63
+ __mul__ = __op(int.__mul__)
64
+ __floordiv__ = __op(int.__floordiv__)
65
+ __mod__ = __op(int.__mod__)
66
+ __pow__ = __op(int.__pow__)
67
+ __lshift__ = __op(int.__lshift__)
68
+ __rshift__ = __op(int.__rshift__)
69
+ __and__ = __op(int.__and__)
70
+ __xor__ = __op(int.__xor__)
71
+ __or__ = __op(int.__or__)
72
72
 
73
73
  @classmethod
74
74
  def __default__(cls) -> Self:
75
- return cls.__new__(cls, cls.cs.pointer.__default__(), None, None)
75
+ return cls.__new__(
76
+ cls,
77
+ cls.cs.pointer.__default__(),
78
+ None,
79
+ context=None,
80
+ endian=cls.cs.endian,
81
+ )
76
82
 
77
83
  @classmethod
78
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self:
79
- return cls.__new__(cls, cls.cs.pointer._read(stream, context), stream, context)
84
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self:
85
+ return cls.__new__(
86
+ cls,
87
+ cls.cs.pointer._read(stream, context=context, endian=endian),
88
+ stream,
89
+ context=context,
90
+ endian=endian,
91
+ )
80
92
 
81
93
  @classmethod
82
- def _write(cls, stream: BinaryIO, data: int) -> int:
83
- return cls.cs.pointer._write(stream, data)
94
+ def _write(cls, stream: BinaryIO, data: int, *, endian: Endianness) -> int:
95
+ return cls.cs.pointer._write(stream, data, endian=endian)
96
+
97
+ def dereference(self, *, endian: AllowedEndianness | None = None) -> T | None:
98
+ """Dereference the pointer and read the value it points to.
84
99
 
85
- def dereference(self) -> T:
100
+ Args:
101
+ endian: Optional endianness to use when reading the value.
102
+ If not provided, the endianness used when reading the pointer itself will be used.
103
+ """
86
104
  if self == 0 or self._stream is None:
87
105
  raise NullPointerDereference
88
106
 
89
- if self._value is None and not issubclass(self.type, Void):
90
- # Read current position of file read/write pointer
91
- position = self._stream.tell()
92
- # Reposition the file read/write pointer
93
- self._stream.seek(self)
107
+ endian = normalize_endianness(endian) if endian is not None else self._endian
108
+ if issubclass(self.type, Void):
109
+ return None
94
110
 
95
- if issubclass(self.type, Char):
96
- # this makes the assumption that a char pointer is a null-terminated string
97
- value = self.type._read_0(self._stream, self._context)
98
- else:
99
- value = self.type._read(self._stream, self._context)
111
+ position = self._stream.tell()
112
+ self._stream.seek(self)
100
113
 
101
- self._stream.seek(position)
102
- self._value = value
114
+ if issubclass(self.type, Char):
115
+ # This makes the assumption that a char pointer is a null-terminated string
116
+ value = self.type._read_0(self._stream, context=self._context, endian=endian)
117
+ else:
118
+ value = self.type._read(self._stream, context=self._context, endian=endian)
103
119
 
104
- return self._value
120
+ # Restore the stream position after reading the value
121
+ self._stream.seek(position)
122
+ return value
@@ -13,7 +13,10 @@ from types import FunctionType
13
13
  from typing import TYPE_CHECKING, Any, BinaryIO
14
14
 
15
15
  from dissect.cstruct.bitbuffer import BitBuffer
16
+ from dissect.cstruct.expression import Expression
17
+ from dissect.cstruct.lexer import IDENTIFIER_TYPES
16
18
  from dissect.cstruct.types.base import (
19
+ BaseArray,
17
20
  BaseType,
18
21
  MetaType,
19
22
  _is_buffer_type,
@@ -28,6 +31,8 @@ if TYPE_CHECKING:
28
31
 
29
32
  from typing_extensions import Self
30
33
 
34
+ from dissect.cstruct.cstruct import Endianness
35
+
31
36
 
32
37
  class Field:
33
38
  """Structure field."""
@@ -71,15 +76,6 @@ class StructureMetaType(MetaType):
71
76
  return super().__new__(metacls, name, bases, classdict)
72
77
 
73
78
  def __call__(cls, *args, **kwargs) -> Self: # type: ignore
74
- if (
75
- cls.__fields__
76
- and len(args) == len(cls.__fields__) == 1
77
- and isinstance(args[0], bytes)
78
- and issubclass(cls.__fields__[0].type, bytes)
79
- and len(args[0]) == cls.__fields__[0].type.size
80
- ):
81
- # Shortcut for single char/bytes type
82
- return type.__call__(cls, *args, **kwargs)
83
79
  if not args and not kwargs:
84
80
  obj = type.__call__(cls)
85
81
  object.__setattr__(obj, "__dynamic_sizes__", {})
@@ -246,8 +242,8 @@ class StructureMetaType(MetaType):
246
242
  # The structure size is whatever the currently calculated offset is
247
243
  return offset, alignment
248
244
 
249
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self: # type: ignore
250
- bit_buffer = BitBuffer(stream, cls.cs.endian)
245
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self: # type: ignore
246
+ bit_buffer = BitBuffer(stream, endian=endian)
251
247
  struct_start = stream.tell()
252
248
 
253
249
  result = {}
@@ -256,7 +252,7 @@ class StructureMetaType(MetaType):
256
252
  offset = stream.tell()
257
253
 
258
254
  if field.offset is not None and offset != struct_start + field.offset:
259
- # Field is at a specific offset, either alligned or added that way
255
+ # Field is at a specific offset, either aligned or added that way
260
256
  offset = struct_start + field.offset
261
257
  stream.seek(offset)
262
258
 
@@ -276,7 +272,7 @@ class StructureMetaType(MetaType):
276
272
 
277
273
  bit_buffer.reset()
278
274
 
279
- value = field.type._read(stream, result)
275
+ value = field.type._read(stream, context=result, endian=endian)
280
276
 
281
277
  result[field._name] = value
282
278
  if field.type.dynamic:
@@ -292,21 +288,21 @@ class StructureMetaType(MetaType):
292
288
  obj.__dynamic_sizes__ = sizes
293
289
  return obj
294
290
 
295
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> list[Self]: # type: ignore
291
+ def _read_0(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> list[Self]: # type: ignore
296
292
  result = []
297
293
 
298
- while obj := cls._read(stream, context):
294
+ while obj := cls._read(stream, context=context, endian=endian):
299
295
  result.append(obj)
300
296
 
301
297
  return result
302
298
 
303
- def _write(cls, stream: BinaryIO, data: Structure) -> int:
304
- bit_buffer = BitBuffer(stream, cls.cs.endian)
299
+ def _write(cls, stream: BinaryIO, data: Structure, *, endian: Endianness) -> int:
300
+ bit_buffer = BitBuffer(stream, endian=endian)
305
301
  struct_start = stream.tell()
306
302
  num = 0
307
303
 
308
304
  for field in cls.__fields__:
309
- field_type = cls.cs.resolve(field.type)
305
+ field_type = field.type
310
306
 
311
307
  bit_field_type = (
312
308
  (field_type.type if isinstance(field_type, EnumMetaType) else field_type) if field.bits else None
@@ -322,7 +318,7 @@ class StructureMetaType(MetaType):
322
318
  offset = stream.tell()
323
319
 
324
320
  if field.offset is not None and offset < struct_start + field.offset:
325
- # Field is at a specific offset, either alligned or added that way
321
+ # Field is at a specific offset, either aligned or added that way
326
322
  stream.write(b"\x00" * (struct_start + field.offset - offset))
327
323
  offset = struct_start + field.offset
328
324
 
@@ -346,7 +342,7 @@ class StructureMetaType(MetaType):
346
342
  else:
347
343
  bit_buffer.write(field_type, value, field.bits)
348
344
  else:
349
- field_type._write(stream, value)
345
+ field_type._write(stream, value, endian=endian)
350
346
  num += stream.tell() - offset
351
347
 
352
348
  if bit_buffer._type is not None:
@@ -380,6 +376,102 @@ class StructureMetaType(MetaType):
380
376
  for key, value in classdict.items():
381
377
  setattr(cls, key, value)
382
378
 
379
+ def cdef(cls, *, recursive: bool = False) -> str:
380
+ """Render this structure (or union) back to its C-style definition.
381
+
382
+ When ``recursive=True``, any ``#define`` constants referenced in array sizes are emitted as well, so that the
383
+ result can be parsed on its own. Note that the result is semantically equivalent to the original definition,
384
+ but not necessarily identical.
385
+
386
+ Args:
387
+ recursive: Also emit the definitions of any types referenced by this structure.
388
+
389
+ Returns:
390
+ The C-style definition as a string.
391
+ """
392
+
393
+ def _decompose(type_: MetaType) -> tuple[MetaType, int, list[int | Expression | None]]:
394
+ dimensions = []
395
+ while issubclass(type_, BaseArray):
396
+ dimensions.append(type_.num_entries)
397
+ type_ = type_.type
398
+
399
+ stars = 0
400
+ while issubclass(type_, Pointer):
401
+ stars += 1
402
+ type_ = type_.type
403
+
404
+ return type_, stars, dimensions
405
+
406
+ def _deps(type_: StructureMetaType, seen: set[MetaType]) -> list[MetaType | str]:
407
+ deps = []
408
+ for field in type_.__fields__:
409
+ base, _, dimensions = _decompose(field.type)
410
+
411
+ for dim in dimensions:
412
+ if isinstance(dim, Expression):
413
+ for token in dim._tokens:
414
+ if (
415
+ token.type in IDENTIFIER_TYPES
416
+ and token.value in cls.cs.consts
417
+ and token.value not in seen
418
+ ):
419
+ deps.append(token.value)
420
+ seen.add(token.value)
421
+
422
+ if isinstance(base, StructureMetaType) and base not in seen:
423
+ if base.__anonymous__:
424
+ # Anonymous structures are not emitted, but their dependencies are
425
+ deps.extend(_deps(base, seen))
426
+ else:
427
+ seen.add(base)
428
+ deps.extend(_deps(base, seen))
429
+ deps.append(base)
430
+
431
+ elif isinstance(base, EnumMetaType) and base not in seen:
432
+ seen.add(base)
433
+ deps.append(base)
434
+
435
+ return deps
436
+
437
+ def _render_struct(type_: StructureMetaType, name: str) -> list[str]:
438
+ keyword = "union" if isinstance(type_, UnionMetaType) else "struct"
439
+ title = f"{keyword} {name}" if name else keyword
440
+
441
+ lines = [f"{title} {{"]
442
+ for field in type_.__fields__:
443
+ base, stars, dimensions = _decompose(field.type)
444
+
445
+ declarator = "*" * stars + (field.name or "") + "".join(f"[{dim or ''}]" for dim in dimensions)
446
+ if field.bits is not None:
447
+ declarator = f"{declarator} : {field.bits}" if declarator else f": {field.bits}"
448
+
449
+ if isinstance(base, StructureMetaType) and base.__anonymous__:
450
+ # Anonymous struct/union: inline the full definition
451
+ lines.extend(f" {line}" for line in _render_struct(base, ""))
452
+ lines[-1] = f"{lines[-1]} {declarator};" if declarator else f"{lines[-1]};"
453
+ else:
454
+ prefix = f"{base.__name__} " if declarator else base.__name__
455
+ lines.append(f" {prefix}{declarator};")
456
+
457
+ lines.append("}")
458
+ return lines
459
+
460
+ deps = _deps(cls, {cls}) if recursive else []
461
+ deps.append(cls)
462
+
463
+ consts = []
464
+ blocks = []
465
+ for type_ in deps:
466
+ if isinstance(type_, str):
467
+ consts.append(f"#define {type_} {cls.cs.consts[type_]!r}")
468
+ elif isinstance(type_, EnumMetaType):
469
+ blocks.append(type_.cdef())
470
+ else:
471
+ blocks.append("\n".join(_render_struct(type_, type_.__name__)) + ";")
472
+
473
+ return ("\n".join(consts) + "\n\n" if consts else "") + ("\n\n".join(blocks) if blocks else "")
474
+
383
475
 
384
476
  class Structure(BaseType, metaclass=StructureMetaType):
385
477
  """Base class for cstruct structure type classes.
@@ -459,6 +551,12 @@ class UnionMetaType(StructureMetaType):
459
551
  def __call__(cls, *args, **kwargs) -> Self: # type: ignore
460
552
  obj: Union = super().__call__(*args, **kwargs)
461
553
 
554
+ if not hasattr(obj, "_buf"):
555
+ # If we don't have a _buf attribute, we haven't read from a stream and are initializing with values
556
+ # Set default internal attributes
557
+ object.__setattr__(obj, "_buf", None)
558
+ object.__setattr__(obj, "_endian", cls.cs.endian)
559
+
462
560
  # Calling with non-stream args or kwargs means we are initializing with values
463
561
  if (args and not (len(args) == 1 and (_is_readable_type(args[0]) or _is_buffer_type(args[0])))) or kwargs:
464
562
  # We don't support user initialization of dynamic unions yet
@@ -502,7 +600,7 @@ class UnionMetaType(StructureMetaType):
502
600
  return size, alignment
503
601
 
504
602
  def _read_fields(
505
- cls, stream: BinaryIO, context: dict[str, Any] | None = None
603
+ cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness
506
604
  ) -> tuple[dict[str, Any], dict[str, int]]:
507
605
  result = {}
508
606
  sizes = {}
@@ -515,14 +613,14 @@ class UnionMetaType(StructureMetaType):
515
613
  buf = io.BytesIO(stream.read(cls.size))
516
614
 
517
615
  for field in cls.__fields__:
518
- field_type = cls.cs.resolve(field.type)
616
+ field_type = field.type
519
617
 
520
618
  start = 0
521
619
  if field.offset is not None:
522
620
  start = field.offset
523
621
 
524
622
  buf.seek(offset + start)
525
- value = field_type._read(buf, result)
623
+ value = field_type._read(buf, context=result, endian=endian)
526
624
 
527
625
  result[field._name] = value
528
626
  if field.type.dynamic:
@@ -530,10 +628,10 @@ class UnionMetaType(StructureMetaType):
530
628
 
531
629
  return result, sizes
532
630
 
533
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self: # type: ignore
631
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self: # type: ignore
534
632
  if cls.size is None:
535
633
  start = stream.tell()
536
- result, sizes = cls._read_fields(stream, context)
634
+ result, sizes = cls._read_fields(stream, context=context, endian=endian)
537
635
  size = stream.tell() - start
538
636
  stream.seek(start)
539
637
  buf = stream.read(size)
@@ -550,6 +648,7 @@ class UnionMetaType(StructureMetaType):
550
648
  obj: Union = type.__call__(cls, **result)
551
649
  object.__setattr__(obj, "__dynamic_sizes__", sizes)
552
650
  object.__setattr__(obj, "_buf", buf)
651
+ object.__setattr__(obj, "_endian", endian)
553
652
 
554
653
  if cls.size is not None:
555
654
  obj._update()
@@ -559,7 +658,7 @@ class UnionMetaType(StructureMetaType):
559
658
 
560
659
  return obj
561
660
 
562
- def _write(cls, stream: BinaryIO, data: Union) -> int:
661
+ def _write(cls, stream: BinaryIO, data: Union, *, endian: Endianness) -> int:
563
662
  if cls.dynamic:
564
663
  raise NotImplementedError("Writing dynamic unions is not yet supported")
565
664
 
@@ -578,12 +677,12 @@ class UnionMetaType(StructureMetaType):
578
677
  continue
579
678
 
580
679
  # Write the value
581
- field.type._write(stream, getattr(data, field._name))
680
+ field.type._write(stream, getattr(data, field._name), endian=endian)
582
681
  break
583
682
 
584
683
  # If we haven't written anything yet and we initially skipped an anonymous struct, write it now
585
684
  if stream.tell() == offset and anonymous_struct:
586
- anonymous_struct._write(stream, data)
685
+ anonymous_struct._write(stream, data, endian=endian)
587
686
 
588
687
  # If we haven't filled the union size yet, pad it
589
688
  if remaining := expected_offset - stream.tell():
@@ -596,6 +695,7 @@ class Union(Structure, metaclass=UnionMetaType):
596
695
  """Base class for cstruct union type classes."""
597
696
 
598
697
  _buf: bytes
698
+ _endian: Endianness
599
699
 
600
700
  def __eq__(self, other: object) -> bool:
601
701
  return self.__class__ is other.__class__ and bytes(self) == bytes(other)
@@ -619,7 +719,7 @@ class Union(Structure, metaclass=UnionMetaType):
619
719
  if (value := getattr(self, attr)) is None:
620
720
  value = field.type.__default__()
621
721
 
622
- field.type._write(buf, value)
722
+ field.type._write(buf, value, endian=self._endian)
623
723
 
624
724
  object.__setattr__(self, "_buf", buf.getvalue())
625
725
  self._update()
@@ -628,7 +728,7 @@ class Union(Structure, metaclass=UnionMetaType):
628
728
  self._proxify()
629
729
 
630
730
  def _update(self) -> None:
631
- result, sizes = self.__class__._read_fields(io.BytesIO(self._buf))
731
+ result, sizes = self.__class__._read_fields(io.BytesIO(self._buf), endian=self._endian)
632
732
  self.__dict__.update(result)
633
733
  object.__setattr__(self, "__dynamic_sizes__", sizes)
634
734
 
@@ -7,6 +7,8 @@ from dissect.cstruct.types.base import BaseArray, BaseType
7
7
  if TYPE_CHECKING:
8
8
  from typing_extensions import Self
9
9
 
10
+ from dissect.cstruct.cstruct import Endianness
11
+
10
12
 
11
13
  class VoidArray(list, BaseArray):
12
14
  """Array type representing void elements, primarily used for no-op reading and writing operations."""
@@ -16,11 +18,11 @@ class VoidArray(list, BaseArray):
16
18
  return cls()
17
19
 
18
20
  @classmethod
19
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self:
21
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self:
20
22
  return cls()
21
23
 
22
24
  @classmethod
23
- def _write(cls, stream: BinaryIO, data: bytes) -> int:
25
+ def _write(cls, stream: BinaryIO, data: bytes, *, endian: Endianness) -> int:
24
26
  return 0
25
27
 
26
28
 
@@ -36,9 +38,9 @@ class Void(BaseType):
36
38
  return isinstance(value, Void)
37
39
 
38
40
  @classmethod
39
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self:
41
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self:
40
42
  return cls.__new__(cls)
41
43
 
42
44
  @classmethod
43
- def _write(cls, stream: BinaryIO, data: Void) -> int:
45
+ def _write(cls, stream: BinaryIO, data: Void, *, endian: Endianness) -> int:
44
46
  return 0