dissect.cstruct 4.2.dev1__py3-none-any.whl → 4.3.dev2__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.
@@ -321,7 +321,7 @@ class cstruct:
321
321
  size: int | None,
322
322
  *,
323
323
  alignment: int | None = None,
324
- attrs: dict[str, Any] = None,
324
+ attrs: dict[str, Any] | None = None,
325
325
  ) -> type[BaseType]:
326
326
  """Create a new type class bound to this cstruct instance.
327
327
 
@@ -36,14 +36,14 @@ class MetaType(type):
36
36
  if len(args) == 1 and not isinstance(args[0], cls):
37
37
  stream = args[0]
38
38
 
39
- if hasattr(stream, "read"):
39
+ if _is_readable_type(stream):
40
40
  return cls._read(stream)
41
41
 
42
42
  if issubclass(cls, bytes) and isinstance(stream, bytes) and len(stream) == cls.size:
43
43
  # Shortcut for char/bytes type
44
44
  return type.__call__(cls, *args, **kwargs)
45
45
 
46
- if isinstance(stream, (bytes, memoryview, bytearray)):
46
+ if _is_buffer_type(stream):
47
47
  return cls.reads(stream)
48
48
 
49
49
  return type.__call__(cls, *args, **kwargs)
@@ -59,7 +59,7 @@ class MetaType(type):
59
59
 
60
60
  return cls.size
61
61
 
62
- def default(cls) -> BaseType:
62
+ def __default__(cls) -> BaseType:
63
63
  """Return the default value of this type."""
64
64
  return cls()
65
65
 
@@ -83,7 +83,7 @@ class MetaType(type):
83
83
  Returns:
84
84
  The parsed value of this type.
85
85
  """
86
- if isinstance(obj, (bytes, memoryview, bytearray)):
86
+ if _is_buffer_type(obj):
87
87
  return cls.reads(obj)
88
88
 
89
89
  return cls._read(obj)
@@ -113,7 +113,7 @@ class MetaType(type):
113
113
  cls._write(out, value)
114
114
  return out.getvalue()
115
115
 
116
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> BaseType:
116
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> BaseType:
117
117
  """Internal function for reading value.
118
118
 
119
119
  Must be implemented per type.
@@ -124,7 +124,7 @@ class MetaType(type):
124
124
  """
125
125
  raise NotImplementedError()
126
126
 
127
- def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] = None) -> list[BaseType]:
127
+ def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] | None = None) -> list[BaseType]:
128
128
  """Internal function for reading array values.
129
129
 
130
130
  Allows type implementations to do optimized reading for their type.
@@ -145,7 +145,7 @@ class MetaType(type):
145
145
 
146
146
  return [cls._read(stream, context) for _ in range(count)]
147
147
 
148
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] = None) -> list[BaseType]:
148
+ def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> list[BaseType]:
149
149
  """Internal function for reading null-terminated data.
150
150
 
151
151
  "Null" is type specific, so must be implemented per type.
@@ -179,7 +179,7 @@ class MetaType(type):
179
179
  stream: The stream to read from.
180
180
  array: The array to write.
181
181
  """
182
- return cls._write_array(stream, array + [cls()])
182
+ return cls._write_array(stream, array + [cls.__default__()])
183
183
 
184
184
 
185
185
  class _overload:
@@ -225,7 +225,12 @@ class ArrayMetaType(MetaType):
225
225
  num_entries: int | Expression | None
226
226
  null_terminated: bool
227
227
 
228
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Array:
228
+ def __default__(cls) -> BaseType:
229
+ return type.__call__(
230
+ cls, [cls.type.__default__()] * (cls.num_entries if isinstance(cls.num_entries, int) else 0)
231
+ )
232
+
233
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Array:
229
234
  if cls.null_terminated:
230
235
  return cls.type._read_0(stream, context)
231
236
 
@@ -243,11 +248,6 @@ class ArrayMetaType(MetaType):
243
248
 
244
249
  return cls.type._read_array(stream, num, context)
245
250
 
246
- def default(cls) -> BaseType:
247
- return type.__call__(
248
- cls, [cls.type.default() for _ in range(0 if cls.dynamic or cls.null_terminated else cls.num_entries)]
249
- )
250
-
251
251
 
252
252
  class Array(list, BaseType, metaclass=ArrayMetaType):
253
253
  """Implements a fixed or dynamically sized array type.
@@ -261,7 +261,7 @@ class Array(list, BaseType, metaclass=ArrayMetaType):
261
261
  """
262
262
 
263
263
  @classmethod
264
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Array:
264
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Array:
265
265
  return cls(ArrayMetaType._read(cls, stream, context))
266
266
 
267
267
  @classmethod
@@ -275,5 +275,13 @@ class Array(list, BaseType, metaclass=ArrayMetaType):
275
275
  return cls.type._write_array(stream, data)
276
276
 
277
277
 
278
+ def _is_readable_type(value: Any) -> bool:
279
+ return hasattr(value, "read")
280
+
281
+
282
+ def _is_buffer_type(value: Any) -> bool:
283
+ return isinstance(value, (bytes, memoryview, bytearray))
284
+
285
+
278
286
  # As mentioned in the BaseType class, we correctly set the type here
279
287
  MetaType.ArrayType = Array
@@ -9,7 +9,7 @@ class CharArray(bytes, BaseType, metaclass=ArrayMetaType):
9
9
  """Character array type for reading and writing byte strings."""
10
10
 
11
11
  @classmethod
12
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> CharArray:
12
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> CharArray:
13
13
  return type.__call__(cls, ArrayMetaType._read(cls, stream, context))
14
14
 
15
15
  @classmethod
@@ -25,7 +25,7 @@ class CharArray(bytes, BaseType, metaclass=ArrayMetaType):
25
25
  return stream.write(data)
26
26
 
27
27
  @classmethod
28
- def default(cls) -> CharArray:
28
+ def __default__(cls) -> CharArray:
29
29
  return type.__call__(cls, b"\x00" * (0 if cls.dynamic or cls.null_terminated else cls.num_entries))
30
30
 
31
31
 
@@ -35,11 +35,11 @@ class Char(bytes, BaseType):
35
35
  ArrayType = CharArray
36
36
 
37
37
  @classmethod
38
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Char:
38
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Char:
39
39
  return cls._read_array(stream, 1, context)
40
40
 
41
41
  @classmethod
42
- def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] = None) -> Char:
42
+ def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] | None = None) -> Char:
43
43
  if count == 0:
44
44
  return type.__call__(cls, b"")
45
45
 
@@ -50,7 +50,7 @@ class Char(bytes, BaseType):
50
50
  return type.__call__(cls, data)
51
51
 
52
52
  @classmethod
53
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Char:
53
+ def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Char:
54
54
  buf = []
55
55
  while True:
56
56
  byte = stream.read(1)
@@ -75,5 +75,5 @@ class Char(bytes, BaseType):
75
75
  return stream.write(data)
76
76
 
77
77
  @classmethod
78
- def default(cls) -> Char:
78
+ def __default__(cls) -> Char:
79
79
  return type.__call__(cls, b"\x00")
@@ -27,7 +27,7 @@ class EnumMetaType(EnumMeta, MetaType):
27
27
  ) -> EnumMetaType:
28
28
  if name is None:
29
29
  if value is None:
30
- value = cls.type()
30
+ value = cls.type.__default__()
31
31
 
32
32
  if not isinstance(value, int):
33
33
  # value is a parsable value
@@ -64,13 +64,13 @@ class EnumMetaType(EnumMeta, MetaType):
64
64
  return True
65
65
  return value in cls._value2member_map_
66
66
 
67
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Enum:
67
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Enum:
68
68
  return cls(cls.type._read(stream, context))
69
69
 
70
- def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] = None) -> list[Enum]:
70
+ def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] | None = None) -> list[Enum]:
71
71
  return list(map(cls, cls.type._read_array(stream, count, context)))
72
72
 
73
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] = None) -> list[Enum]:
73
+ def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> list[Enum]:
74
74
  return list(map(cls, cls.type._read_0(stream, context)))
75
75
 
76
76
  def _write(cls, stream: BinaryIO, data: Enum) -> int:
@@ -82,7 +82,7 @@ class EnumMetaType(EnumMeta, MetaType):
82
82
 
83
83
  def _write_0(cls, stream: BinaryIO, array: list[BaseType]) -> int:
84
84
  data = [entry.value if isinstance(entry, Enum) else entry for entry in array]
85
- return cls._write_array(stream, data + [cls.type()])
85
+ return cls._write_array(stream, data + [cls.type.__default__()])
86
86
 
87
87
 
88
88
  def _fix_alias_members(cls: type[Enum]) -> None:
@@ -12,7 +12,7 @@ class Int(int, BaseType):
12
12
  signed: bool
13
13
 
14
14
  @classmethod
15
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Int:
15
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Int:
16
16
  data = stream.read(cls.size)
17
17
 
18
18
  if len(data) != cls.size:
@@ -21,7 +21,7 @@ class Int(int, BaseType):
21
21
  return cls.from_bytes(data, ENDIANNESS_MAP[cls.cs.endian], signed=cls.signed)
22
22
 
23
23
  @classmethod
24
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Int:
24
+ def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Int:
25
25
  result = []
26
26
 
27
27
  while True:
@@ -14,7 +14,7 @@ class LEB128(int, BaseType):
14
14
  signed: bool
15
15
 
16
16
  @classmethod
17
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> LEB128:
17
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> LEB128:
18
18
  result = 0
19
19
  shift = 0
20
20
  while True:
@@ -35,7 +35,7 @@ class LEB128(int, BaseType):
35
35
  return cls.__new__(cls, result)
36
36
 
37
37
  @classmethod
38
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] = None) -> LEB128:
38
+ def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> LEB128:
39
39
  result = []
40
40
 
41
41
  while True:
@@ -18,11 +18,11 @@ class Packed(BaseType):
18
18
  packchar: str
19
19
 
20
20
  @classmethod
21
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Packed:
21
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Packed:
22
22
  return cls._read_array(stream, 1, context)[0]
23
23
 
24
24
  @classmethod
25
- def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] = None) -> list[Packed]:
25
+ def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] | None = None) -> list[Packed]:
26
26
  if count == EOF:
27
27
  data = stream.read()
28
28
  length = len(data)
@@ -39,7 +39,7 @@ class Packed(BaseType):
39
39
  return [cls.__new__(cls, value) for value in fmt.unpack(data)]
40
40
 
41
41
  @classmethod
42
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Packed:
42
+ def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Packed:
43
43
  result = []
44
44
 
45
45
  fmt = _struct(cls.cs.endian, cls.packchar)
@@ -12,11 +12,11 @@ class Pointer(int, BaseType):
12
12
  """Pointer to some other type."""
13
13
 
14
14
  type: MetaType
15
- _stream: BinaryIO
16
- _context: dict[str, Any]
15
+ _stream: BinaryIO | None
16
+ _context: dict[str, Any] | None
17
17
  _value: BaseType
18
18
 
19
- def __new__(cls, value: int, stream: BinaryIO, context: dict[str, Any] = None) -> Pointer:
19
+ def __new__(cls, value: int, stream: BinaryIO | None, context: dict[str, Any] | None = None) -> Pointer:
20
20
  obj = super().__new__(cls, value)
21
21
  obj._stream = stream
22
22
  obj._context = context
@@ -66,7 +66,11 @@ class Pointer(int, BaseType):
66
66
  return type.__call__(self.__class__, int.__or__(self, other), self._stream, self._context)
67
67
 
68
68
  @classmethod
69
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Pointer:
69
+ def __default__(cls) -> Pointer:
70
+ return cls.__new__(cls, cls.cs.pointer.__default__(), None, None)
71
+
72
+ @classmethod
73
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Pointer:
70
74
  return cls.__new__(cls, cls.cs.pointer._read(stream, context), stream, context)
71
75
 
72
76
  @classmethod
@@ -74,7 +78,7 @@ class Pointer(int, BaseType):
74
78
  return cls.cs.pointer._write(stream, data)
75
79
 
76
80
  def dereference(self) -> Any:
77
- if self == 0:
81
+ if self == 0 or self._stream is None:
78
82
  raise NullPointerDereference()
79
83
 
80
84
  if self._value is None and not issubclass(self.type, Void):
@@ -4,13 +4,19 @@ import io
4
4
  from contextlib import contextmanager
5
5
  from enum import Enum
6
6
  from functools import lru_cache
7
+ from itertools import chain
7
8
  from operator import attrgetter
8
9
  from textwrap import dedent
9
10
  from types import FunctionType
10
- from typing import Any, BinaryIO, Callable, ContextManager
11
+ from typing import Any, BinaryIO, Callable, Iterator
11
12
 
12
13
  from dissect.cstruct.bitbuffer import BitBuffer
13
- from dissect.cstruct.types.base import BaseType, MetaType
14
+ from dissect.cstruct.types.base import (
15
+ BaseType,
16
+ MetaType,
17
+ _is_buffer_type,
18
+ _is_readable_type,
19
+ )
14
20
  from dissect.cstruct.types.enum import EnumMetaType
15
21
  from dissect.cstruct.types.pointer import Pointer
16
22
 
@@ -65,7 +71,7 @@ class StructureMetaType(MetaType):
65
71
  # Shortcut for single char/bytes type
66
72
  return type.__call__(cls, *args, **kwargs)
67
73
  elif not args and not kwargs:
68
- obj = cls(**{field.name: field.type.default() for field in cls.__fields__})
74
+ obj = type.__call__(cls)
69
75
  object.__setattr__(obj, "_values", {})
70
76
  object.__setattr__(obj, "_sizes", {})
71
77
  return obj
@@ -77,7 +83,6 @@ class StructureMetaType(MetaType):
77
83
 
78
84
  lookup = {}
79
85
  raw_lookup = {}
80
- init_names = []
81
86
  field_names = []
82
87
  for field in fields:
83
88
  if field.name in lookup and field.name != "_":
@@ -94,25 +99,21 @@ class StructureMetaType(MetaType):
94
99
 
95
100
  raw_lookup[field.name] = field
96
101
 
97
- num_fields = len(lookup)
98
102
  field_names = lookup.keys()
99
- init_names = raw_lookup.keys()
100
103
  classdict["fields"] = lookup
101
104
  classdict["lookup"] = raw_lookup
102
105
  classdict["__fields__"] = fields
103
- classdict["__bool__"] = _patch_attributes(_make__bool__(num_fields), field_names, 1)
106
+ classdict["__bool__"] = _generate__bool__(field_names)
104
107
 
105
108
  if issubclass(cls, UnionMetaType) or isinstance(cls, UnionMetaType):
106
- classdict["__init__"] = _patch_setattr_args_and_attributes(
107
- _make_setattr__init__(len(init_names)), init_names
108
- )
109
+ classdict["__init__"] = _generate_union__init__(raw_lookup.values())
109
110
  # Not a great way to do this but it works for now
110
111
  classdict["__eq__"] = Union.__eq__
111
112
  else:
112
- classdict["__init__"] = _patch_args_and_attributes(_make__init__(len(init_names)), init_names)
113
- classdict["__eq__"] = _patch_attributes(_make__eq__(num_fields), field_names, 1)
113
+ classdict["__init__"] = _generate_structure__init__(raw_lookup.values())
114
+ classdict["__eq__"] = _generate__eq__(field_names)
114
115
 
115
- classdict["__hash__"] = _patch_attributes(_make__hash__(num_fields), field_names, 1)
116
+ classdict["__hash__"] = _generate__hash__(field_names)
116
117
 
117
118
  # If we're calling this as a class method or a function on the metaclass
118
119
  if issubclass(cls, type):
@@ -229,7 +230,7 @@ class StructureMetaType(MetaType):
229
230
  # The structure size is whatever the currently calculated offset is
230
231
  return offset, alignment
231
232
 
232
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Structure:
233
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Structure:
233
234
  bit_buffer = BitBuffer(stream, cls.cs.endian)
234
235
  struct_start = stream.tell()
235
236
 
@@ -271,12 +272,14 @@ class StructureMetaType(MetaType):
271
272
  # Align the stream
272
273
  stream.seek(-stream.tell() & (cls.alignment - 1), io.SEEK_CUR)
273
274
 
274
- obj = cls(**result)
275
+ # Using type.__call__ directly calls the __init__ method of the class
276
+ # This is faster than calling cls() and bypasses the metaclass __call__ method
277
+ obj = type.__call__(cls, **result)
275
278
  obj._sizes = sizes
276
279
  obj._values = result
277
280
  return obj
278
281
 
279
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] = None) -> list[Structure]:
282
+ def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> list[Structure]:
280
283
  result = []
281
284
 
282
285
  while obj := cls._read(stream, context):
@@ -322,7 +325,7 @@ class StructureMetaType(MetaType):
322
325
 
323
326
  value = getattr(data, field.name, None)
324
327
  if value is None:
325
- value = field_type()
328
+ value = field_type.__default__()
326
329
 
327
330
  if field.bits:
328
331
  if isinstance(field_type, EnumMetaType):
@@ -350,7 +353,7 @@ class StructureMetaType(MetaType):
350
353
  cls.commit()
351
354
 
352
355
  @contextmanager
353
- def start_update(cls) -> ContextManager:
356
+ def start_update(cls) -> Iterator[None]:
354
357
  try:
355
358
  cls.__updating__ = True
356
359
  yield
@@ -397,11 +400,27 @@ class UnionMetaType(StructureMetaType):
397
400
  """Base metaclass for cstruct union type classes."""
398
401
 
399
402
  def __call__(cls, *args, **kwargs) -> Union:
400
- obj = super().__call__(*args, **kwargs)
401
- if kwargs:
402
- # Calling with kwargs means we are initializing with values
403
- # Proxify all values
403
+ obj: Union = super().__call__(*args, **kwargs)
404
+
405
+ # Calling with non-stream args or kwargs means we are initializing with values
406
+ if (args and not (len(args) == 1 and (_is_readable_type(args[0]) or _is_buffer_type(args[0])))) or kwargs:
407
+ # We don't support user initialization of dynamic unions yet
408
+ if cls.dynamic:
409
+ raise NotImplementedError("Initializing a dynamic union is not yet supported")
410
+
411
+ # User (partial) initialization, rebuild the union
412
+ # First user-provided field is the one used to rebuild the union
413
+ arg_fields = (field.name for _, field in zip(args, cls.__fields__))
414
+ kwarg_fields = (name for name in kwargs if name in cls.lookup)
415
+ if (first_field := next(chain(arg_fields, kwarg_fields), None)) is not None:
416
+ obj._rebuild(first_field)
417
+ elif not args and not kwargs:
418
+ # Initialized with default values
419
+ # Note that we proxify here in case we have a default initialization (cls())
420
+ # We don't proxify in case we read from a stream, as we do that later on in _read at a more appropriate time
421
+ # Same with (partial) user initialization, we do that after rebuilding the union
404
422
  obj._proxify()
423
+
405
424
  return obj
406
425
 
407
426
  def _calculate_size_and_offsets(cls, fields: list[Field], align: bool = False) -> tuple[int | None, int]:
@@ -425,7 +444,9 @@ class UnionMetaType(StructureMetaType):
425
444
 
426
445
  return size, alignment
427
446
 
428
- def _read_fields(cls, stream: BinaryIO, context: dict[str, Any] = None) -> tuple[dict[str, Any], dict[str, int]]:
447
+ def _read_fields(
448
+ cls, stream: BinaryIO, context: dict[str, Any] | None = None
449
+ ) -> tuple[dict[str, Any], dict[str, int]]:
429
450
  result = {}
430
451
  sizes = {}
431
452
 
@@ -451,7 +472,7 @@ class UnionMetaType(StructureMetaType):
451
472
 
452
473
  return result, sizes
453
474
 
454
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Union:
475
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Union:
455
476
  if cls.size is None:
456
477
  start = stream.tell()
457
478
  result, sizes = cls._read_fields(stream, context)
@@ -463,7 +484,12 @@ class UnionMetaType(StructureMetaType):
463
484
  sizes = {}
464
485
  buf = stream.read(cls.size)
465
486
 
466
- obj: Union = cls(**result)
487
+ # Create the object and set the values
488
+ # Using type.__call__ directly calls the __init__ method of the class
489
+ # This is faster than calling cls() and bypasses the metaclass __call__ method
490
+ # It also makes it easier to differentiate between user-initialization of the class
491
+ # and initialization from a stream read
492
+ obj: Union = type.__call__(cls, **result)
467
493
  object.__setattr__(obj, "_values", result)
468
494
  object.__setattr__(obj, "_sizes", sizes)
469
495
  object.__setattr__(obj, "_buf", buf)
@@ -471,14 +497,20 @@ class UnionMetaType(StructureMetaType):
471
497
  if cls.size is not None:
472
498
  obj._update()
473
499
 
500
+ # Proxify any nested structures
501
+ obj._proxify()
502
+
474
503
  return obj
475
504
 
476
505
  def _write(cls, stream: BinaryIO, data: Union) -> int:
506
+ if cls.dynamic:
507
+ raise NotImplementedError("Writing dynamic unions is not yet supported")
508
+
477
509
  offset = stream.tell()
478
510
  expected_offset = offset + len(cls)
479
511
 
480
512
  # Sort by largest field
481
- fields = sorted(cls.__fields__, key=lambda e: len(e.type), reverse=True)
513
+ fields = sorted(cls.__fields__, key=lambda e: e.type.size or 0, reverse=True)
482
514
  anonymous_struct = False
483
515
 
484
516
  # Try to write by largest field
@@ -488,12 +520,8 @@ class UnionMetaType(StructureMetaType):
488
520
  anonymous_struct = field.type
489
521
  continue
490
522
 
491
- # Skip empty values
492
- if (value := getattr(data, field.name)) is None:
493
- continue
494
-
495
- # We have a value, write it
496
- field.type._write(stream, value)
523
+ # Write the value
524
+ field.type._write(stream, getattr(data, field.name))
497
525
  break
498
526
 
499
527
  # If we haven't written anything yet and we initially skipped an anonymous struct, write it now
@@ -527,14 +555,21 @@ class Union(Structure, metaclass=UnionMetaType):
527
555
  cur_buf = b"\x00" * self.__class__.size
528
556
 
529
557
  buf = io.BytesIO(cur_buf)
530
- field = self.__class__.fields[attr]
558
+ field = self.__class__.lookup[attr]
531
559
  if field.offset:
532
560
  buf.seek(field.offset)
533
- field.type._write(buf, getattr(self, attr))
561
+
562
+ if (value := getattr(self, attr)) is None:
563
+ value = field.type.__default__()
564
+
565
+ field.type._write(buf, value)
534
566
 
535
567
  object.__setattr__(self, "_buf", buf.getvalue())
536
568
  self._update()
537
569
 
570
+ # (Re-)proxify all values
571
+ self._proxify()
572
+
538
573
  def _update(self) -> None:
539
574
  result, sizes = self.__class__._read_fields(io.BytesIO(self._buf))
540
575
  self.__dict__.update(result)
@@ -596,65 +631,71 @@ def attrsetter(path: str) -> Callable[[Any], Any]:
596
631
 
597
632
 
598
633
  def _codegen(func: FunctionType) -> FunctionType:
599
- # Inspired by https://github.com/dabeaz/dataklasses
600
- @lru_cache
601
- def make_func_code(num_fields: int) -> FunctionType:
602
- names = [f"_{n}" for n in range(num_fields)]
603
- exec(func(names), {}, d := {})
604
- return d.popitem()[1]
634
+ """Decorator that generates a template function with a specified number of fields.
605
635
 
606
- return make_func_code
636
+ This code is a little complex but allows use to cache generated functions for a specific number of fields.
637
+ For example, if we generate a structure with 10 fields, we can cache the generated code for that structure.
638
+ We can then reuse that code and patch it with the correct field names when we create a new structure with 10 fields.
607
639
 
640
+ The functions that are decorated with this decorator should take a list of field names and return a string of code.
641
+ The decorated function is needs to be called with the number of fields, instead of the field names.
642
+ The confusing part is that that the original function takes field names, but you then call it with
643
+ the number of fields instead.
608
644
 
609
- def _patch_args_and_attributes(func: FunctionType, fields: list[str], start: int = 0) -> FunctionType:
610
- return type(func)(
611
- func.__code__.replace(
612
- co_names=(*func.__code__.co_names[:start], *fields),
613
- co_varnames=("self", *fields),
614
- ),
615
- func.__globals__,
616
- argdefs=func.__defaults__,
617
- )
645
+ Inspired by https://github.com/dabeaz/dataklasses.
618
646
 
647
+ Args:
648
+ func: The decorated function that takes a list of field names and returns a string of code.
619
649
 
620
- def _patch_setattr_args_and_attributes(func: FunctionType, fields: list[str], start: int = 0) -> FunctionType:
621
- return type(func)(
622
- func.__code__.replace(
623
- co_consts=(None, *fields),
624
- co_varnames=("self", *fields),
625
- ),
626
- func.__globals__,
627
- argdefs=func.__defaults__,
628
- )
650
+ Returns:
651
+ A cached function that generates the desired function code, to be called with the number of fields.
652
+ """
629
653
 
654
+ def make_func_code(num_fields: int) -> FunctionType:
655
+ exec(func([f"_{n}" for n in range(num_fields)]), {}, d := {})
656
+ return d.popitem()[1]
630
657
 
631
- def _patch_attributes(func: FunctionType, fields: list[str], start: int = 0) -> FunctionType:
632
- return type(func)(
633
- func.__code__.replace(co_names=(*func.__code__.co_names[:start], *fields)),
634
- func.__globals__,
635
- )
658
+ make_func_code.__wrapped__ = func
659
+ return lru_cache(make_func_code)
636
660
 
637
661
 
638
662
  @_codegen
639
- def _make__init__(fields: list[str]) -> str:
663
+ def _make_structure__init__(fields: list[str]) -> str:
664
+ """Generates an ``__init__`` method for a structure with the specified fields.
665
+
666
+ Args:
667
+ fields: List of field names.
668
+ """
640
669
  field_args = ", ".join(f"{field} = None" for field in fields)
641
- field_init = "\n".join(f" self.{name} = {name}" for name in fields)
670
+ field_init = "\n".join(f" self.{name} = {name} if {name} is not None else {i}" for i, name in enumerate(fields))
642
671
 
643
- code = f"def __init__(self{', ' + field_args if field_args else ''}):\n"
672
+ code = f"def __init__(self{', ' + field_args or ''}):\n"
644
673
  return code + (field_init or " pass")
645
674
 
646
675
 
647
676
  @_codegen
648
- def _make_setattr__init__(fields: list[str]) -> str:
677
+ def _make_union__init__(fields: list[str]) -> str:
678
+ """Generates an ``__init__`` method for a class with the specified fields using setattr.
679
+
680
+ Args:
681
+ fields: List of field names.
682
+ """
649
683
  field_args = ", ".join(f"{field} = None" for field in fields)
650
- field_init = "\n".join(f" object.__setattr__(self, {name!r}, {name})" for name in fields)
684
+ field_init = "\n".join(
685
+ f" object.__setattr__(self, '{name}', {name} if {name} is not None else {i})" for i, name in enumerate(fields)
686
+ )
651
687
 
652
- code = f"def __init__(self{', ' + field_args if field_args else ''}):\n"
688
+ code = f"def __init__(self{', ' + field_args or ''}):\n"
653
689
  return code + (field_init or " pass")
654
690
 
655
691
 
656
692
  @_codegen
657
693
  def _make__eq__(fields: list[str]) -> str:
694
+ """Generates an ``__eq__`` method for a class with the specified fields.
695
+
696
+ Args:
697
+ fields: List of field names.
698
+ """
658
699
  self_vals = ",".join(f"self.{name}" for name in fields)
659
700
  other_vals = ",".join(f"other.{name}" for name in fields)
660
701
 
@@ -676,6 +717,11 @@ def _make__eq__(fields: list[str]) -> str:
676
717
 
677
718
  @_codegen
678
719
  def _make__bool__(fields: list[str]) -> str:
720
+ """Generates a ``__bool__`` method for a class with the specified fields.
721
+
722
+ Args:
723
+ fields: List of field names.
724
+ """
679
725
  vals = ", ".join(f"self.{name}" for name in fields)
680
726
 
681
727
  code = f"""
@@ -688,6 +734,11 @@ def _make__bool__(fields: list[str]) -> str:
688
734
 
689
735
  @_codegen
690
736
  def _make__hash__(fields: list[str]) -> str:
737
+ """Generates a ``__hash__`` method for a class with the specified fields.
738
+
739
+ Args:
740
+ fields: List of field names.
741
+ """
691
742
  vals = ", ".join(f"self.{name}" for name in fields)
692
743
 
693
744
  code = f"""
@@ -696,3 +747,83 @@ def _make__hash__(fields: list[str]) -> str:
696
747
  """
697
748
 
698
749
  return dedent(code)
750
+
751
+
752
+ def _patch_attributes(func: FunctionType, fields: list[str], start: int = 0) -> FunctionType:
753
+ """Patches a function's attributes.
754
+
755
+ Args:
756
+ func: The function to patch.
757
+ fields: List of field names to add.
758
+ start: The starting index for patching. Defaults to 0.
759
+ """
760
+ return type(func)(
761
+ func.__code__.replace(co_names=(*func.__code__.co_names[:start], *fields)),
762
+ func.__globals__,
763
+ )
764
+
765
+
766
+ def _generate_structure__init__(fields: list[Field]) -> FunctionType:
767
+ """Generates an ``__init__`` method for a structure with the specified fields.
768
+
769
+ Args:
770
+ fields: List of field names.
771
+ """
772
+ field_names = [field.name for field in fields]
773
+
774
+ template: FunctionType = _make_structure__init__(len(field_names))
775
+ return type(template)(
776
+ template.__code__.replace(
777
+ co_consts=(None, *[field.type.__default__() for field in fields]),
778
+ co_names=(*field_names,),
779
+ co_varnames=("self", *field_names),
780
+ ),
781
+ template.__globals__,
782
+ argdefs=template.__defaults__,
783
+ )
784
+
785
+
786
+ def _generate_union__init__(fields: list[Field]) -> FunctionType:
787
+ """Generates an ``__init__`` method for a union with the specified fields.
788
+
789
+ Args:
790
+ fields: List of field names.
791
+ """
792
+ field_names = [field.name for field in fields]
793
+
794
+ template: FunctionType = _make_union__init__(len(field_names))
795
+ return type(template)(
796
+ template.__code__.replace(
797
+ co_consts=(None, *sum([(field.name, field.type.__default__()) for field in fields], ())),
798
+ co_varnames=("self", *field_names),
799
+ ),
800
+ template.__globals__,
801
+ argdefs=template.__defaults__,
802
+ )
803
+
804
+
805
+ def _generate__eq__(fields: list[str]) -> FunctionType:
806
+ """Generates an ``__eq__`` method for a class with the specified fields.
807
+
808
+ Args:
809
+ fields: List of field names.
810
+ """
811
+ return _patch_attributes(_make__eq__(len(fields)), fields, 1)
812
+
813
+
814
+ def _generate__bool__(fields: list[str]) -> FunctionType:
815
+ """Generates a ``__bool__`` method for a class with the specified fields.
816
+
817
+ Args:
818
+ fields: List of field names.
819
+ """
820
+ return _patch_attributes(_make__bool__(len(fields)), fields, 1)
821
+
822
+
823
+ def _generate__hash__(fields: list[str]) -> FunctionType:
824
+ """Generates a ``__hash__`` method for a class with the specified fields.
825
+
826
+ Args:
827
+ fields: List of field names.
828
+ """
829
+ return _patch_attributes(_make__hash__(len(fields)), fields, 1)
@@ -11,10 +11,17 @@ class Void(BaseType):
11
11
  def __bool__(self) -> bool:
12
12
  return False
13
13
 
14
+ def __eq__(self, value: object) -> bool:
15
+ return isinstance(value, Void)
16
+
14
17
  @classmethod
15
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Void:
18
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Void:
16
19
  return cls.__new__(cls)
17
20
 
21
+ @classmethod
22
+ def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Void:
23
+ return [cls.__new__(cls)]
24
+
18
25
  @classmethod
19
26
  def _write(cls, stream: BinaryIO, data: Void) -> int:
20
27
  return 0
@@ -10,7 +10,7 @@ class WcharArray(str, BaseType, metaclass=ArrayMetaType):
10
10
  """Wide-character array type for reading and writing UTF-16 strings."""
11
11
 
12
12
  @classmethod
13
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> WcharArray:
13
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> WcharArray:
14
14
  return type.__call__(cls, ArrayMetaType._read(cls, stream, context))
15
15
 
16
16
  @classmethod
@@ -20,7 +20,7 @@ class WcharArray(str, BaseType, metaclass=ArrayMetaType):
20
20
  return stream.write(data.encode(Wchar.__encoding_map__[cls.cs.endian]))
21
21
 
22
22
  @classmethod
23
- def default(cls) -> WcharArray:
23
+ def __default__(cls) -> WcharArray:
24
24
  return type.__call__(cls, "\x00" * (0 if cls.dynamic or cls.null_terminated else cls.num_entries))
25
25
 
26
26
 
@@ -38,11 +38,11 @@ class Wchar(str, BaseType):
38
38
  }
39
39
 
40
40
  @classmethod
41
- def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Wchar:
41
+ def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Wchar:
42
42
  return cls._read_array(stream, 1, context)
43
43
 
44
44
  @classmethod
45
- def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] = None) -> Wchar:
45
+ def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] | None = None) -> Wchar:
46
46
  if count == 0:
47
47
  return type.__call__(cls, "")
48
48
 
@@ -56,7 +56,7 @@ class Wchar(str, BaseType):
56
56
  return type.__call__(cls, data.decode(cls.__encoding_map__[cls.cs.endian]))
57
57
 
58
58
  @classmethod
59
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Wchar:
59
+ def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Wchar:
60
60
  buf = []
61
61
  while True:
62
62
  point = stream.read(2)
@@ -75,5 +75,5 @@ class Wchar(str, BaseType):
75
75
  return stream.write(data.encode(cls.__encoding_map__[cls.cs.endian]))
76
76
 
77
77
  @classmethod
78
- def default(cls) -> Wchar:
78
+ def __default__(cls) -> Wchar:
79
79
  return type.__call__(cls, "\x00")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.cstruct
3
- Version: 4.2.dev1
3
+ Version: 4.3.dev2
4
4
  Summary: A Dissect module implementing a parser for C-like structures: structure parsing in Python made easy
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Apache License 2.0
@@ -0,0 +1,26 @@
1
+ dissect/cstruct/__init__.py,sha256=E8HT9kh67nPR6cnUmTmTJIdGANNfInG_htxb8mPjSik,1344
2
+ dissect/cstruct/bitbuffer.py,sha256=phqKSE-BS8crZ_sdzssF3E831bD5Gk8LOXIE7iQJXa8,2218
3
+ dissect/cstruct/compiler.py,sha256=dB7Wi1GC0pYoVe5x3olZDmAT3ypbyV-1h-BMefJk5dU,13991
4
+ dissect/cstruct/cstruct.py,sha256=2Hu0HSFucV1pY2QAnteqHnpob9jmqPr_glfnGZigfjo,14858
5
+ dissect/cstruct/exceptions.py,sha256=WqsUW4OiIpRLQLvixfLrfKl0rtvU1qx7pvfBrz9Sz-I,293
6
+ dissect/cstruct/expression.py,sha256=giBNkbFgsbsw2K2EvQi42GRlm9Z97UvhDhcHnNMG2dg,10763
7
+ dissect/cstruct/parser.py,sha256=kL9caswfpm6ltXoJfetf5UjEfOLIqj7WGMqdID8C2W4,20893
8
+ dissect/cstruct/utils.py,sha256=ze_i59PRxQ1q0xl-wsV_ZLKSnBirT71fvxfKEshgc18,10386
9
+ dissect/cstruct/types/__init__.py,sha256=basF3huZ-jOg4FqGQGW7-SmYDN2GuMuCQN21asLtelQ,855
10
+ dissect/cstruct/types/base.py,sha256=PB_H7PRt0pLehThnQQVNFBmKIfQRnlfXYfw7pNZdBaQ,9120
11
+ dissect/cstruct/types/char.py,sha256=XWBtODCWKYtZYml5MKlvxrnRyH2emiGa_To7RrwJc_Y,2433
12
+ dissect/cstruct/types/enum.py,sha256=OyW1HmdTg7B1_FNMckez1vOTlwAfPKj6cfM9ITAxEkw,6746
13
+ dissect/cstruct/types/flag.py,sha256=EOJVk-5G7aUtD6plg7_Y6yxoAcrAQCNMjxoUePEQukk,2345
14
+ dissect/cstruct/types/int.py,sha256=ihV8v6eR1k2Pnc7dAroxQOGhjQ0Um1coiJMAVHYqWn4,1081
15
+ dissect/cstruct/types/leb128.py,sha256=OYwwwGSTU2npHZVZxMV9PTseug7hBoy1A44Uj_dhtNs,2182
16
+ dissect/cstruct/types/packed.py,sha256=V0rEpxHJ3GOlUv9rzi94U8GyN6ArxW7CSPhBPk90qSc,1994
17
+ dissect/cstruct/types/pointer.py,sha256=2Ve0b6VxGpckK2CtHtTzQyU5tSSotnPZFu7ZoP9WqMI,3832
18
+ dissect/cstruct/types/structure.py,sha256=_9JCSjj_BsHr16HYwv6IAXHLt1B4yH0n3S8D2WclBmU,29481
19
+ dissect/cstruct/types/void.py,sha256=VL2qJ86rq-oBUnkBprNsPPgPGgHV6UENRJTQ2jw0rxc,669
20
+ dissect/cstruct/types/wchar.py,sha256=ZL54AuYndQLj46-QgxSFbmd18saRgYwyJaNTwxZCsqU,2572
21
+ dissect.cstruct-4.3.dev2.dist-info/COPYRIGHT,sha256=H-18RXfshdH9AdHwW2eO1Xa-5s6tY5eipHh5c0whDu4,316
22
+ dissect.cstruct-4.3.dev2.dist-info/LICENSE,sha256=PhUqiw6jAh2KbBdVRPBq_hfAvfcTBin7nZ3CK7NQbTM,11341
23
+ dissect.cstruct-4.3.dev2.dist-info/METADATA,sha256=jyWC9013BoKf9h7vS1EAeFY1pHdV3GlhH6ATVA9MaOw,8558
24
+ dissect.cstruct-4.3.dev2.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
25
+ dissect.cstruct-4.3.dev2.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
26
+ dissect.cstruct-4.3.dev2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,26 +0,0 @@
1
- dissect/cstruct/__init__.py,sha256=E8HT9kh67nPR6cnUmTmTJIdGANNfInG_htxb8mPjSik,1344
2
- dissect/cstruct/bitbuffer.py,sha256=phqKSE-BS8crZ_sdzssF3E831bD5Gk8LOXIE7iQJXa8,2218
3
- dissect/cstruct/compiler.py,sha256=dB7Wi1GC0pYoVe5x3olZDmAT3ypbyV-1h-BMefJk5dU,13991
4
- dissect/cstruct/cstruct.py,sha256=W-17FFc8OM2je7VTMMVQc-hivCAjaRj9qSdf4cEDrCY,14851
5
- dissect/cstruct/exceptions.py,sha256=WqsUW4OiIpRLQLvixfLrfKl0rtvU1qx7pvfBrz9Sz-I,293
6
- dissect/cstruct/expression.py,sha256=giBNkbFgsbsw2K2EvQi42GRlm9Z97UvhDhcHnNMG2dg,10763
7
- dissect/cstruct/parser.py,sha256=kL9caswfpm6ltXoJfetf5UjEfOLIqj7WGMqdID8C2W4,20893
8
- dissect/cstruct/utils.py,sha256=ze_i59PRxQ1q0xl-wsV_ZLKSnBirT71fvxfKEshgc18,10386
9
- dissect/cstruct/types/__init__.py,sha256=basF3huZ-jOg4FqGQGW7-SmYDN2GuMuCQN21asLtelQ,855
10
- dissect/cstruct/types/base.py,sha256=qZeXn1MAEb-vOlF4HdeqbMHx4-Mr5lTcHYt_s0lhu9g,8944
11
- dissect/cstruct/types/char.py,sha256=M_QmYL8c96bIO8r2nxHC26zXoPjO2dMtehoWtZJL2M8,2397
12
- dissect/cstruct/types/enum.py,sha256=1P-GUSRLWAKRcn55XhPYvK9KVMdX3x7GdmdDUy2HuEQ,6701
13
- dissect/cstruct/types/flag.py,sha256=EOJVk-5G7aUtD6plg7_Y6yxoAcrAQCNMjxoUePEQukk,2345
14
- dissect/cstruct/types/int.py,sha256=pBdr9InRY3EMHlULBmzBZCI2Qdh2XygsUelDWBrIRhc,1067
15
- dissect/cstruct/types/leb128.py,sha256=yHkWqcGEWkf-8ZzfLv_5p937fzrMRvlZXwxowEl36CU,2168
16
- dissect/cstruct/types/packed.py,sha256=gmNxtNRf9feY8k_7UYMGADe31dtXkw5fQYG1CtyvJBo,1973
17
- dissect/cstruct/types/pointer.py,sha256=kWfEBCgW7TDnJihZiDGUlldD3QSxVY19r8LGz7olswk,3644
18
- dissect/cstruct/types/structure.py,sha256=3pdEg_Jc4py7xzkcp06r4LtH-L_uhdlFhuhdP-zX3ww,24426
19
- dissect/cstruct/types/void.py,sha256=RGBlM1yBYCFUM9yLyOETMZBd_nEp9-6vH5xFXaphK8o,438
20
- dissect/cstruct/types/wchar.py,sha256=5kap4GDypRmt5IoLhMnREbWaWTEMhY_xVLCFginbwFo,2536
21
- dissect.cstruct-4.2.dev1.dist-info/COPYRIGHT,sha256=H-18RXfshdH9AdHwW2eO1Xa-5s6tY5eipHh5c0whDu4,316
22
- dissect.cstruct-4.2.dev1.dist-info/LICENSE,sha256=PhUqiw6jAh2KbBdVRPBq_hfAvfcTBin7nZ3CK7NQbTM,11341
23
- dissect.cstruct-4.2.dev1.dist-info/METADATA,sha256=93eUYeGsSm9i_1aKIm5qc_tEV5RRTkJJTduscjrFZic,8558
24
- dissect.cstruct-4.2.dev1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
25
- dissect.cstruct-4.2.dev1.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
26
- dissect.cstruct-4.2.dev1.dist-info/RECORD,,