dissect.cstruct 5.0.dev12__py3-none-any.whl → 5.0.dev14__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.
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import io
6
6
  import logging
7
+ import struct
7
8
  from enum import Enum
8
9
  from itertools import groupby, repeat
9
10
  from keyword import iskeyword
@@ -11,7 +12,7 @@ from operator import itemgetter
11
12
  from textwrap import indent
12
13
  from typing import TYPE_CHECKING, Any
13
14
 
14
- from dissect.cstruct.exception import ExpressionParserError
15
+ from dissect.cstruct.exception import ArraySizeError, ExpressionParserError
15
16
  from dissect.cstruct.expression import SOURCE_GLOBALS, Expression
16
17
  from dissect.cstruct.types import (
17
18
  Array,
@@ -30,7 +31,7 @@ from dissect.cstruct.types import (
30
31
  WcharArray,
31
32
  )
32
33
  from dissect.cstruct.types.enum import EnumMetaType
33
- from dissect.cstruct.types.structure import UnionMetaType, UnionProxy
34
+ from dissect.cstruct.types.structure import StructureMetaType, UnionMetaType, UnionProxy
34
35
  from dissect.cstruct.util import ENDIANNESS_TO_BYTEORDER_MAP, _struct
35
36
 
36
37
  if TYPE_CHECKING:
@@ -63,25 +64,33 @@ python_compile = compile
63
64
 
64
65
 
65
66
  def compile_structure(cls: type[Structure]) -> type[Structure]:
66
- """Compile the ``_read`` method of a structure or union type, if the compiler supports it.
67
+ """Compile the ``_read`` and ``_write`` methods of a structure or union type, if the compiler supports it.
67
68
 
68
69
  The compilation happens on first use.
69
70
  """
70
71
  try:
71
72
  _check_supported(cls, cls.__fields__)
72
73
  cls._read = _LazyCompileRead(cls)
74
+ cls._write = _LazyCompileWrite(cls)
73
75
  cls.__compiled__ = True
74
76
  except Exception as e:
75
- # Revert to the interpreted reader
77
+ # Revert to the interpreted reader and writer
76
78
  log.debug("Failed to compile %s", cls, exc_info=e)
77
- cls._read = classmethod(type(cls)._read)
78
- cls.__compiled__ = False
79
+ _revert_to_interpreted(cls)
79
80
 
80
81
  return cls
81
82
 
82
83
 
83
- class _LazyCompileRead:
84
- """Descriptor that compiles the ``_read`` method of a structure on first access and replaces itself with it."""
84
+ def _revert_to_interpreted(cls: type[Structure]) -> None:
85
+ cls._read = classmethod(type(cls)._read)
86
+ cls._write = classmethod(type(cls)._write)
87
+ cls.__compiled__ = False
88
+
89
+
90
+ class _LazyCompile:
91
+ """Descriptor that compiles a method of a structure on first access and replaces itself with it."""
92
+
93
+ name: str
85
94
 
86
95
  def __init__(self, cls: type[Structure]):
87
96
  self.cls = cls
@@ -103,18 +112,47 @@ class _LazyCompileRead:
103
112
  inline = all(type_.__dict__.get("__init__") is init for type_, init in self.snapshot.items())
104
113
 
105
114
  try:
106
- self.method = _ReadSourceGenerator(cls, self.fields, inline).generate()
115
+ self.method = self._generator(inline).generate()
107
116
  except Exception as e:
108
- # Revert to the slower loop based method
117
+ # Revert to the slower interpreted method
109
118
  log.debug("Failed to compile %s", cls, exc_info=e)
110
- self.method = classmethod(type(cls)._read)
111
- cls.__compiled__ = False
119
+ self.method = self._revert()
112
120
 
113
- if cls.__dict__.get("_read") is self:
114
- cls._read = self.method
121
+ if cls.__dict__.get(self.name) is self:
122
+ setattr(cls, self.name, self.method)
115
123
 
116
124
  return self.method
117
125
 
126
+ def _generator(self, inline: bool) -> _SourceGenerator:
127
+ raise NotImplementedError
128
+
129
+ def _revert(self) -> classmethod:
130
+ """Return the interpreted version of the method."""
131
+ return classmethod(getattr(type(self.cls), self.name))
132
+
133
+
134
+ class _LazyCompileRead(_LazyCompile):
135
+ """Descriptor that compiles the ``_read`` method of a structure on first access and replaces itself with it."""
136
+
137
+ name = "_read"
138
+
139
+ def _generator(self, inline: bool) -> _SourceGenerator:
140
+ return _ReadSourceGenerator(self.cls, self.fields, inline)
141
+
142
+ def _revert(self) -> classmethod:
143
+ # The structure no longer counts as compiled, so adding a field won't recompile _write either
144
+ _revert_to_interpreted(self.cls)
145
+ return super()._revert()
146
+
147
+
148
+ class _LazyCompileWrite(_LazyCompile):
149
+ """Descriptor that compiles the ``_write`` method of a structure on first access and replaces itself with it."""
150
+
151
+ name = "_write"
152
+
153
+ def _generator(self, inline: bool) -> _SourceGenerator:
154
+ return _WriteSourceGenerator(self.cls, self.fields, inline)
155
+
118
156
 
119
157
  def _snapshot_structures(fields: list[Field], result: dict[type, Any]) -> dict[type, Any]:
120
158
  for field in fields:
@@ -166,7 +204,29 @@ _ENDIANNESS = ("<", ">", "!", "@", "=")
166
204
  # Types of which the value can be used as an integer directly
167
205
  _INTEGER_TYPES = (Int, Packed, Enum, Flag)
168
206
 
169
- # Globals that are available to all generated read functions
207
+ # Types of which the value can be packed, unless they have a custom way of writing
208
+ _PACKABLE_TYPES = (Array, Char, CharArray, Int, Packed, Pointer, Void, VoidArray, Wchar, WcharArray)
209
+
210
+
211
+ def _field_bytes(type_: type[BaseType], value: Any, endian: str) -> bytes:
212
+ """Return the bytes of a char or wchar field that isn't of the right type yet or doesn't fit."""
213
+ buf = type_.dumps(type_.__default__() if value is None else value, endian=endian)
214
+ if len(buf) > type_.__size__:
215
+ raise ArraySizeError(f"Expected {type_.__size__} bytes for {type_.__name__}, got {len(buf)} instead.")
216
+ return buf
217
+
218
+
219
+ def _array_values(type_: type[Array], value: list[Any] | None) -> list[Any]:
220
+ """Return the values of an array field that doesn't have the right size yet."""
221
+ return type_.__default__() if value is None else type_._pad(value)
222
+
223
+
224
+ def _or_default(value: Any, type_: type[BaseType]) -> Any:
225
+ """Return the value of a field, or the default of its type if it's ``None``."""
226
+ return type_.__default__() if value is None else value
227
+
228
+
229
+ # Globals that are available to all generated functions
170
230
  _GLOBALS = {
171
231
  "_type_call": type.__call__,
172
232
  "_object_new": object.__new__,
@@ -178,12 +238,20 @@ _GLOBALS = {
178
238
  "_getattr": getattr,
179
239
  "_UnionProxy": UnionProxy,
180
240
  "_union_read": UnionMetaType._read,
241
+ "_struct_write": StructureMetaType._write,
242
+ "_union_write": UnionMetaType._write,
243
+ "_struct_error": struct.error,
244
+ "_field_bytes": _field_bytes,
245
+ "_array_values": _array_values,
246
+ "_or_default": _or_default,
181
247
  **SOURCE_GLOBALS,
182
248
  }
183
249
 
184
250
 
185
- class _ReadSourceGenerator:
186
- """Generates the source code of an optimized ``_read`` method for a structure or union."""
251
+ class _SourceGenerator:
252
+ """Base class for generating the source code of an optimized method of a structure or union."""
253
+
254
+ name: str
187
255
 
188
256
  def __init__(self, cls: type[Structure], fields: list[Field], inline: bool = True):
189
257
  self.cls = cls
@@ -191,24 +259,17 @@ class _ReadSourceGenerator:
191
259
  self.align = cls.__align__
192
260
  self.inline = inline and not self.align
193
261
 
262
+ # The values available to the generated code
263
+ self.symbols: dict[str, Any] = {"cs": cls.__cs__}
264
+ self._symbol_names: dict[int, str] = {}
265
+
194
266
  # Witnesses of inlined structure types (symbol of the type -> symbol of its __init__ at compile time)
195
267
  self._witnesses: dict[str, str] = {}
196
- self._fallback_symbol: str | None = None
197
268
 
198
269
  # The state of the bit buffer: the type of its current value and the remaining bits
199
270
  self._bits: tuple[type[BaseType], int] | None = None
200
271
  self._var_ids: dict[str, int] = {}
201
272
 
202
- # The values available to the generated code
203
- self.symbols: dict[str, Any] = {"cs": cls.__cs__}
204
- self._symbol_names: dict[int, str] = {}
205
-
206
- # (field name, local variable name) of all fields assigned so far
207
- self._assigned: list[tuple[str, str]] = []
208
- self._has_context = False
209
- self._uses_start = False
210
- self._uses_sizes = False
211
-
212
273
  def _type_symbol(self, type_: type[BaseType]) -> str:
213
274
  """Add a type as symbol, returns its name."""
214
275
  return self._add_symbol(type_, "type")
@@ -220,39 +281,12 @@ class _ReadSourceGenerator:
220
281
  self.symbols[symbol] = value
221
282
  return symbol
222
283
 
223
- def _context(self, lines: list[str]) -> str:
224
- """Return the name of the context dictionary, creating it from the fields read so far if needed."""
225
- if not self._has_context:
226
- self._has_context = True
227
- items = ", ".join(f"{name!r}: {var}" for name, var in self._assigned)
228
- lines.append(f"values = {{{items}}}")
229
-
230
- return "values"
231
-
232
284
  def _var(self, name: str) -> str:
233
285
  """Return a unique variable name for the generated code."""
234
286
  index = self._var_ids.get(name, 0)
235
287
  self._var_ids[name] = index + 1
236
288
  return f"_{name}{index}"
237
289
 
238
- def _assign(self, field: Field, value: str) -> str:
239
- """Assign a value to a new local variable for the given field, and the context dictionary if it exists."""
240
- var = f"_value{len(self._assigned)}"
241
- self._assigned.append((field._name, var))
242
- if self._has_context:
243
- return f"{var} = values[{field._name!r}] = {value}"
244
- return f"{var} = {value}"
245
-
246
- def _assign_local(self, field: Field, value: str) -> list[str]:
247
- return [self._assign(field, value)]
248
-
249
- def _fallback(self) -> str:
250
- """Return the symbol of the non-inlined fallback read function."""
251
- if self._fallback_symbol is None:
252
- fallback = _Fallback(self.cls, self.fields)
253
- self._fallback_symbol = self._add_symbol(fallback, "fallback")
254
- return self._fallback_symbol
255
-
256
290
  def _witness(self, type_: type[Structure]) -> str:
257
291
  """Add an inlined type as symbol, with a witness to detect modifications to it after compilation."""
258
292
  symbol = self._type_symbol(type_)
@@ -263,10 +297,22 @@ class _ReadSourceGenerator:
263
297
  """Return the conditions that are true if any of the inlined types were modified after compilation."""
264
298
  return [f"{type_}.__init__ is not {init}" for type_, init in self._witnesses.items()]
265
299
 
300
+ def _starts_bit_value(self, field: Field) -> bool:
301
+ """Return whether a bit field is the first of a new value of the bit buffer."""
302
+ return self._bits is None or self._bits[1] == 0 or self._bits[0] is not _get_bit_buffer_type(field)
303
+
304
+ def _advance_bits(self, field: Field) -> int:
305
+ """Update the state of the bit buffer for a bit field, returns the bits that were remaining before it."""
306
+ if self._starts_bit_value(field):
307
+ self._bits = (_get_bit_buffer_type(field), _get_bits_type(field).__size__ * 8)
308
+ remaining = self._bits[1]
309
+ self._bits = (self._bits[0], remaining - field.bits)
310
+ return remaining
311
+
266
312
  def generate(self) -> MethodType:
267
313
  source = self.generate_source()
268
314
 
269
- code = python_compile(source, f"<compiled {self.cls.__name__}._read>", "exec")
315
+ code = python_compile(source, f"<compiled {self.cls.__name__}.{self.name}>", "exec")
270
316
  exec(code, {**_GLOBALS, **self.symbols}, d := {})
271
317
  obj = d.popitem()[1]
272
318
  obj.__source__ = source
@@ -278,6 +324,56 @@ class _ReadSourceGenerator:
278
324
  return self._generate_union_source()
279
325
  return self._generate_struct_source()
280
326
 
327
+ def _generate_struct_source(self) -> str:
328
+ raise NotImplementedError
329
+
330
+ def _generate_union_source(self) -> str:
331
+ raise NotImplementedError
332
+
333
+
334
+ class _ReadSourceGenerator(_SourceGenerator):
335
+ """Generates the source code of an optimized ``_read`` method for a structure or union."""
336
+
337
+ name = "_read"
338
+
339
+ def __init__(self, cls: type[Structure], fields: list[Field], inline: bool = True):
340
+ super().__init__(cls, fields, inline)
341
+
342
+ self._fallback_symbol: str | None = None
343
+
344
+ # (field name, local variable name) of all fields assigned so far
345
+ self._assigned: list[tuple[str, str]] = []
346
+ self._has_context = False
347
+ self._uses_start = False
348
+ self._uses_sizes = False
349
+
350
+ def _context(self, lines: list[str]) -> str:
351
+ """Return the name of the context dictionary, creating it from the fields read so far if needed."""
352
+ if not self._has_context:
353
+ self._has_context = True
354
+ items = ", ".join(f"{name!r}: {var}" for name, var in self._assigned)
355
+ lines.append(f"values = {{{items}}}")
356
+
357
+ return "values"
358
+
359
+ def _assign(self, field: Field, value: str) -> str:
360
+ """Assign a value to a new local variable for the given field, and the context dictionary if it exists."""
361
+ var = f"_value{len(self._assigned)}"
362
+ self._assigned.append((field._name, var))
363
+ if self._has_context:
364
+ return f"{var} = values[{field._name!r}] = {value}"
365
+ return f"{var} = {value}"
366
+
367
+ def _assign_local(self, field: Field, value: str) -> list[str]:
368
+ return [self._assign(field, value)]
369
+
370
+ def _fallback(self) -> str:
371
+ """Return the symbol of the non-inlined fallback read function."""
372
+ if self._fallback_symbol is None:
373
+ fallback = _Fallback(self.cls, self.fields)
374
+ self._fallback_symbol = self._add_symbol(fallback, "fallback")
375
+ return self._fallback_symbol
376
+
281
377
  def _generate_struct_source(self) -> str:
282
378
  """Generate the code for reading a structure."""
283
379
  read_code = "\n".join(self._generate_fields_source())
@@ -405,6 +501,16 @@ class _ReadSourceGenerator:
405
501
 
406
502
  # Everything else - basic and composite types (and arrays of them), and inlined structures
407
503
  else:
504
+ if field.offset is None:
505
+ # The alignment depends on the position in the stream, so this needs a new block
506
+ if self.align:
507
+ yield from flush()
508
+ # A block is read in order, so a field before the current position needs a new block
509
+ elif current_offset is None or field.offset < current_offset:
510
+ yield from flush()
511
+ elif current_block:
512
+ current_offset = field.offset
513
+
408
514
  if not current_block:
409
515
  yield from align_to_field(field)
410
516
  current_block.append(field)
@@ -456,18 +562,11 @@ class _ReadSourceGenerator:
456
562
  element = self._type_symbol(field.type.__type__)
457
563
  return f"_type_call({type_}, {element}._read_array(stream, _count, context={context}, endian=endian))"
458
564
 
459
- def _starts_bit_value(self, field: Field) -> bool:
460
- """Return whether a bit field is read from a new value of the bit buffer."""
461
- return self._bits is None or self._bits[1] == 0 or self._bits[0] is not _get_bits_type(field)
462
-
463
565
  def _generate_bits_source(self, field: Field) -> Iterator[str]:
464
566
  """Generate the code for reading a bit field."""
465
567
  read_type = _get_bits_type(field)
466
568
  new_value = self._starts_bit_value(field)
467
- if new_value:
468
- self._bits = (read_type, read_type.__size__ * 8)
469
- remaining = self._bits[1]
470
- self._bits = (read_type, remaining - field.bits)
569
+ remaining = self._advance_bits(field)
471
570
 
472
571
  lines = []
473
572
  if new_value:
@@ -498,7 +597,7 @@ class _ReadSourceGenerator:
498
597
  """Generate the code for reading consecutive packed fields."""
499
598
  unpack = _Unpack("data")
500
599
  reads = []
501
- size = self._pack_fields(fields, 0, unpack, reads, self._assign_local, self.align)
600
+ size = self._pack_fields(fields, 0, unpack, reads, self._assign_local)
502
601
 
503
602
  lines = [f"buf = stream.read({size})", _check_eof(size)]
504
603
  if unpack.has_values():
@@ -513,7 +612,6 @@ class _ReadSourceGenerator:
513
612
  unpack: _Unpack,
514
613
  lines: list[str],
515
614
  target: Callable[[Field, str], list[str]],
516
- align: bool = False,
517
615
  ) -> int:
518
616
  """Generate the code for reading the given fields from ``buf``, starting at the given offset.
519
617
 
@@ -523,7 +621,7 @@ class _ReadSourceGenerator:
523
621
  Returns the offset after the last field.
524
622
  """
525
623
  inline = _inline_kind if self.inline else None
526
- for field, count, char in _generate_struct_info(fields, align, inline=inline):
624
+ for field, count, char in _generate_struct_info(fields, inline=inline):
527
625
  if field is None:
528
626
  # Padding
529
627
  unpack.add(count, "x")
@@ -595,16 +693,15 @@ class _ReadSourceGenerator:
595
693
  lines.append(f"_object_setattr({var}, '_endian', endian)")
596
694
  self._proxify(var, var, fields, lines)
597
695
 
598
- def _proxify(self, union: str, var: str, fields: list[Field], lines: list[str]) -> None:
696
+ def _proxify(self, union: str, var: str, fields: list[Field], lines: list[str], attr: str | None = None) -> None:
599
697
  """Generate the code equivalent to ``Union._proxify``."""
600
698
  for field in fields:
601
699
  if issubclass(field.type, Structure):
602
700
  nested = self._var("nested")
701
+ member = attr or field._name
603
702
  lines.append(f"{nested} = _getattr({var}, {field._name!r})")
604
- lines.append(
605
- f"_object_setattr({var}, {field._name!r}, _UnionProxy({union}, {field._name!r}, {nested}))"
606
- )
607
- self._proxify(union, nested, field.type.__fields__, lines)
703
+ lines.append(f"_object_setattr({var}, {field._name!r}, _UnionProxy({union}, {member!r}, {nested}))")
704
+ self._proxify(union, nested, field.type.__fields__, lines, member)
608
705
 
609
706
  def _generate_packed_field_source(
610
707
  self,
@@ -622,19 +719,18 @@ class _ReadSourceGenerator:
622
719
  array = self._type_symbol(field.type)
623
720
  element = self._type_symbol(field_type.__type__)
624
721
 
625
- if issubclass(field_type.__type__, Int):
626
- elem_size = field_type.__type__.__size__
627
- signed = bool(field_type.__type__.signed)
722
+ if issubclass(read_type, Int):
723
+ # Everything that's read as an integer type is sliced directly out of the buffer
724
+ elem_size = read_type.__size__
725
+ signed = bool(read_type.signed)
628
726
  lines.append(f"_array_buf = {getter}")
629
727
  raw = f"_int_from_bytes(_array_buf[i:i + {elem_size}], _byteorder[endian], signed={signed})"
630
- value = f"[_type_call({element}, {raw}) for i in range(0, {count}, {elem_size})]"
631
- elif issubclass(field_type.__type__, Pointer):
728
+ getter = f"[{raw} for i in range(0, {count}, {elem_size})]"
729
+
730
+ if issubclass(field_type.__type__, Pointer):
632
731
  context = self._context(lines)
633
732
  item = f"{element}.__new__({element}, value, stream, context={context}, endian=endian)"
634
733
  value = f"[{item} for value in {getter}]"
635
- elif issubclass(read_type, (Wchar, Int)):
636
- # Preserve the behaviour of the generic parsing path for these types
637
- value = f"[{element}(value) for value in {getter}]"
638
734
  else:
639
735
  value = f"list(map(_type_call, _repeat({element}), {getter}))"
640
736
 
@@ -647,7 +743,7 @@ class _ReadSourceGenerator:
647
743
  encoding = self._add_symbol(read_type.__encoding_map__, "encoding")
648
744
  # Using str() instead of .decode() also supports other bytes-like objects returned by the stream
649
745
  type_ = self._type_symbol(field.type)
650
- lines.extend(target(field, f"_type_call({type_}, str({getter}, {encoding}[endian]))"))
746
+ lines.extend(target(field, f"_type_call({type_}, str({getter}, {encoding}[endian], 'surrogatepass'))"))
651
747
 
652
748
  elif issubclass(field_type, Pointer):
653
749
  context = self._context(lines)
@@ -694,8 +790,320 @@ class _Unpack:
694
790
 
695
791
  def functions(self, name: str) -> dict[str, Callable]:
696
792
  """Return the given function of the struct for every endianness."""
697
- fmt = _optimize_struct_fmt(self.items)
698
- return {endian: getattr(_struct(endian, fmt), name) for endian in _ENDIANNESS}
793
+ return _struct_functions(_optimize_struct_fmt(self.items), name)
794
+
795
+
796
+ class _WriteSourceGenerator(_SourceGenerator):
797
+ """Generates the source code of an optimized ``_write`` method for a structure or union."""
798
+
799
+ name = "_write"
800
+
801
+ def __init__(self, cls: type[Structure], fields: list[Field], inline: bool = True):
802
+ super().__init__(cls, fields, inline)
803
+
804
+ self._uses_start = False
805
+
806
+ def _generate_method_source(self, interpreted: str, lines: Iterable[str]) -> str:
807
+ """Generate the code of the method, ``interpreted`` is the name of the method to fall back to."""
808
+ write_code = "\n".join(lines)
809
+
810
+ # Fall back to the generic method if any of the inlined types was modified after compilation
811
+ checks = [f"cls is not {self._add_symbol(self.cls, 'cls')}", *self._modified_checks()]
812
+
813
+ preamble = [f"if {' or '.join(checks)}: return {interpreted}(cls, stream, data, endian=endian)"]
814
+ if self._uses_start:
815
+ preamble.append("start = stream.tell()")
816
+
817
+ code = indent("\n".join([*preamble, write_code]), " ")
818
+
819
+ return f"def _write(cls, stream, data, *, endian):\n{code}"
820
+
821
+ def _generate_struct_source(self) -> str:
822
+ """Generate the code for writing a structure."""
823
+ return self._generate_method_source("_struct_write", list(self._generate_fields_source()))
824
+
825
+ def _generate_union_source(self) -> str:
826
+ """Generate the code for writing a union."""
827
+ if not _can_pack(self.cls):
828
+ raise TypeError(f"Unsupported union for compiler: {self.cls}")
829
+
830
+ pack = _Pack()
831
+ self._pack_union("data", self.cls, pack)
832
+
833
+ lines = [self._generate_pack_source(pack), f"return {self.cls.__size__}"]
834
+ return self._generate_method_source("_union_write", lines)
835
+
836
+ def _generate_fields_source(self) -> Iterator[str]:
837
+ """Generate the code for writing all fields."""
838
+ # The offset of the current field in the structure, None if it's unknown due to a dynamic field
839
+ current_offset = 0
840
+ current_block: list[Field] = []
841
+ current_bits: list[Field] = []
842
+
843
+ def flush() -> Iterator[str]:
844
+ if current_block:
845
+ yield from self._generate_packed_source(current_block)
846
+ current_block[:] = []
847
+ if current_bits:
848
+ yield from self._generate_bits_source(current_bits)
849
+ current_bits[:] = []
850
+
851
+ def align_to_field(field: Field) -> Iterator[str]:
852
+ nonlocal current_offset
853
+
854
+ if field.offset is not None and field.offset != current_offset:
855
+ if current_offset is None:
856
+ # This only writes padding if the stream is before the offset, otherwise the amount is negative
857
+ yield f'stream.write(b"\\x00" * (start + {field.offset} - stream.tell()))'
858
+ else:
859
+ yield f"stream.write({bytes(field.offset - current_offset)!r})"
860
+ current_offset = field.offset
861
+
862
+ if self.align and field.offset is None:
863
+ yield f'stream.write(b"\\x00" * (-stream.tell() & ({field.alignment} - 1)))'
864
+
865
+ for field in self.fields:
866
+ if not field.bits:
867
+ self._bits = None
868
+ if current_bits:
869
+ yield from flush()
870
+
871
+ # Unlike reading, we can't move back in the stream
872
+ if None not in (field.offset, current_offset) and field.offset < current_offset:
873
+ raise TypeError(f"Unsupported offset for compiler: {field}")
874
+
875
+ size = field.type.__size__
876
+
877
+ # Bit fields
878
+ if field.bits:
879
+ if self._starts_bit_value(field):
880
+ if not _can_pack_bits(field):
881
+ raise TypeError(f"Unsupported type for bit field: {field.type}")
882
+
883
+ yield from flush()
884
+ yield from align_to_field(field)
885
+ elif field.offset is not None:
886
+ raise TypeError(f"Unsupported offset for bit field: {field}")
887
+ else:
888
+ # Only the first bit field of a value moves the stream
889
+ size = 0
890
+
891
+ self._advance_bits(field)
892
+ current_bits.append(field)
893
+
894
+ # Basic and composite types (and arrays of them), and inlined structures
895
+ elif _can_pack(field.type):
896
+ if field.offset is None:
897
+ # The alignment depends on the position in the stream, so this needs a new block
898
+ if self.align:
899
+ yield from flush()
900
+ elif current_offset is None:
901
+ yield from flush()
902
+ elif current_block:
903
+ current_offset = field.offset
904
+
905
+ if not current_block:
906
+ yield from align_to_field(field)
907
+ current_block.append(field)
908
+
909
+ # Everything else - sub structures, arrays of structures, multi-dimensional and dynamic arrays
910
+ else:
911
+ yield from flush()
912
+ yield from align_to_field(field)
913
+ yield from self._generate_field_source(field)
914
+
915
+ # An aligned structure writes more than its size if the stream isn't aligned
916
+ if self.align:
917
+ size = None
918
+
919
+ if current_offset is not None:
920
+ current_offset = None if size is None else current_offset + size
921
+
922
+ yield from flush()
923
+
924
+ if self.align:
925
+ self._uses_start = True
926
+ yield 'stream.write(b"\\x00" * (-stream.tell() & (cls.__alignment__ - 1)))'
927
+
928
+ yield "return stream.tell() - start" if self._uses_start else f"return {current_offset}"
929
+
930
+ def _generate_field_source(self, field: Field) -> Iterator[str]:
931
+ """Generate the code for writing a field using its type."""
932
+ self._uses_start = True
933
+ type_ = self._type_symbol(field.type)
934
+ lines = [
935
+ f"_value = {_get_attribute('data', field._name)}",
936
+ f"if _value is None: _value = {type_}.__default__()",
937
+ f"{type_}._write(stream, _value, endian=endian)",
938
+ ]
939
+
940
+ yield _block(lines)
941
+
942
+ def _generate_bits_source(self, fields: list[Field]) -> Iterator[str]:
943
+ """Generate the code for writing the bit fields that share a value of the bit buffer."""
944
+ type_ = _get_bits_type(fields[0])
945
+
946
+ pack = _Pack()
947
+ little = []
948
+ other = []
949
+ remaining = type_.__size__ * 8
950
+ for field in fields:
951
+ value = self._load_value(field, _get_attribute("data", field._name), pack)
952
+ if issubclass(field.type, (Enum, Flag)):
953
+ value += "._value_"
954
+
955
+ remaining -= field.bits
956
+ little.append(f"{value} << {type_.__size__ * 8 - remaining - field.bits}")
957
+ other.append(f"{value} << {remaining}")
958
+
959
+ # The bit buffer only writes the least significant bits first for explicit little endian
960
+ self._pack_number(type_, f"({' | '.join(little)} if endian == '<' else {' | '.join(other)})", pack)
961
+
962
+ yield self._generate_pack_source(pack)
963
+
964
+ def _generate_packed_source(self, fields: list[Field]) -> Iterator[str]:
965
+ """Generate the code for writing consecutive packed fields."""
966
+ pack = _Pack()
967
+ self._pack_fields(fields, "data", pack)
968
+
969
+ if pack.items:
970
+ yield self._generate_pack_source(pack)
971
+
972
+ def _generate_pack_source(self, pack: _Pack) -> str:
973
+ """Generate the code for checking, packing and writing values."""
974
+ function = f"{self._add_symbol(pack.functions('pack'), 'pack')}[endian]"
975
+ if pack.values == pack.retry:
976
+ return _block([*pack.lines, f"stream.write({function}({', '.join(pack.values)}))"])
977
+
978
+ # Numbers aren't checked for None up front, since struct.pack raises an error for those
979
+ return _block(
980
+ [
981
+ *pack.lines,
982
+ "try:",
983
+ f" buf = {function}({', '.join(pack.values)})",
984
+ "except _struct_error:",
985
+ f" buf = {function}({', '.join(pack.retry)})",
986
+ "stream.write(buf)",
987
+ ]
988
+ )
989
+
990
+ def _load_value(self, field: Field, value: str, pack: _Pack) -> str:
991
+ """Generate the code for loading a value, using the default of its type if it's ``None``."""
992
+ var = self._var("value")
993
+ pack.lines.append(f"if ({var} := {value}) is None: {var} = {self._type_symbol(field.type)}.__default__()")
994
+ return var
995
+
996
+ def _pack_fields(self, fields: list[Field], var: str, pack: _Pack, proxied: bool = False) -> None:
997
+ """Generate the code for packing the given fields of the object in ``var``.
998
+
999
+ The fields of objects in a union are proxied, in which case we use the object behind the proxy.
1000
+ """
1001
+ for field, count, char in _generate_struct_info(fields, inline=_inline_kind):
1002
+ if field is None:
1003
+ # Padding
1004
+ pack.add(count, "x")
1005
+ continue
1006
+
1007
+ value = _get_attribute(var, field._name)
1008
+
1009
+ if char in ("S", "U"):
1010
+ # Inlined structure or union
1011
+ self._witness(field.type)
1012
+ nested = self._load_value(field, value, pack)
1013
+ if proxied:
1014
+ pack.lines.append(f"if type({nested}) is _UnionProxy: {nested} = {nested}.__target__")
1015
+
1016
+ if char == "S":
1017
+ self._pack_fields(field.type.__fields__, nested, pack, proxied)
1018
+ else:
1019
+ self._pack_union(nested, field.type, pack)
1020
+ continue
1021
+
1022
+ self._pack_field(field, value, count, char, pack)
1023
+
1024
+ def _pack_union(self, var: str, type_: type[Union], pack: _Pack) -> None:
1025
+ """Generate the code for packing the union in ``var``, by the field the interpreted writer uses."""
1026
+ # Changing a nested structure can change which field is the largest
1027
+ for field in type_.__fields__:
1028
+ if issubclass(field.type, Structure):
1029
+ self._witness(field.type)
1030
+
1031
+ self._pack_fields([type_._write_field()], var, pack, proxied=True)
1032
+
1033
+ def _pack_field(self, field: Field, value: str, count: int, char: str, pack: _Pack) -> None:
1034
+ """Generate the code for checking and packing a field."""
1035
+ field_type = field.type
1036
+ read_type = _get_read_type(field_type)
1037
+ type_ = self._type_symbol(field_type)
1038
+
1039
+ if issubclass(field_type, Array):
1040
+ read_type = _get_read_type(field_type.__type__)
1041
+ var = self._var("value")
1042
+ num = field_type.__num_entries__
1043
+ pack.lines.append(
1044
+ f"if ({var} := {value}) is None or len({var}) != {num}: {var} = _array_values({type_}, {var})"
1045
+ )
1046
+
1047
+ # Entries can be enum members or plain integers, both can be packed as they are
1048
+ if issubclass(read_type, Int):
1049
+ pack.add(count, "s", f"b''.join([{_int_to_bytes(read_type, 'item')} for item in {var}])")
1050
+ else:
1051
+ pack.add(count, char, f"*{var}")
1052
+
1053
+ elif issubclass(field_type, (Char, CharArray, Wchar, WcharArray)):
1054
+ var = self._var("value")
1055
+
1056
+ # The format pads values that are too short with zeroes
1057
+ if issubclass(field_type, (Wchar, WcharArray)):
1058
+ encoding = self._add_symbol(Wchar.__encoding_map__, "encoding")
1059
+ pack.lines.append(
1060
+ f"if ({var} := {value}) is None or "
1061
+ f"len({var} := {var}.encode({encoding}[endian], 'surrogatepass')) > {count}: "
1062
+ f"{var} = _field_bytes({type_}, {value}, endian)"
1063
+ )
1064
+ else:
1065
+ pack.lines.append(
1066
+ f"if not isinstance({var} := {value}, bytes) or len({var}) > {count}: "
1067
+ f"{var} = _field_bytes({type_}, {var}, endian)"
1068
+ )
1069
+
1070
+ pack.add(count, "s", var)
1071
+
1072
+ elif issubclass(field_type, (Enum, Flag)):
1073
+ self._pack_number(read_type, f"{self._load_value(field, value, pack)}._value_", pack)
1074
+
1075
+ elif issubclass(read_type, Packed):
1076
+ pack.add(1, read_type.__fmt__, value, f"_or_default({value}, {type_})")
1077
+
1078
+ else:
1079
+ self._pack_number(read_type, self._load_value(field, value, pack), pack)
1080
+
1081
+ def _pack_number(self, type_: type[BaseType], value: str, pack: _Pack) -> None:
1082
+ """Generate the code for packing a value of an integer or packed type."""
1083
+ if issubclass(type_, Int):
1084
+ pack.add(type_.__size__, "s", _int_to_bytes(type_, value))
1085
+ else:
1086
+ pack.add(1, type_.__fmt__, value)
1087
+
1088
+
1089
+ class _Pack:
1090
+ """Tracks consecutive struct pack formats, and the code for the values to pack."""
1091
+
1092
+ def __init__(self):
1093
+ self.items: list[tuple[int, str]] = []
1094
+ self.lines: list[str] = []
1095
+ self.values: list[str] = []
1096
+ self.retry: list[str] = []
1097
+
1098
+ def add(self, count: int, char: str, value: str | None = None, retry: str | None = None) -> None:
1099
+ self.items.append((count, char))
1100
+ if value is not None:
1101
+ self.values.append(value)
1102
+ self.retry.append(retry or value)
1103
+
1104
+ def functions(self, name: str) -> dict[str, Callable]:
1105
+ """Return the given function of the struct for every endianness."""
1106
+ return _struct_functions(_optimize_struct_fmt(self.items), name)
699
1107
 
700
1108
 
701
1109
  def _check_union(cls: type[Union], fields: list[Field], inline: bool = True) -> int:
@@ -767,6 +1175,52 @@ def _uses_generated_init(type_: type[Structure]) -> bool:
767
1175
  return "__init__" in type_.__dict__ and type_.__new__ is object.__new__
768
1176
 
769
1177
 
1178
+ def _can_pack(type_: type[BaseType]) -> bool:
1179
+ """Determine if a value of a type can be written by packing it, instead of by using the ``_write`` method."""
1180
+ if type_.__size__ is None:
1181
+ return False
1182
+
1183
+ if issubclass(type_, (Enum, Flag)):
1184
+ return _can_pack(type_.__type__)
1185
+
1186
+ if issubclass(type_, Structure):
1187
+ kind = _inline_kind(type_)
1188
+ if kind == "U":
1189
+ field = type_._write_field()
1190
+ return field is not None and bool(field.type.__size__) and _can_pack(field.type)
1191
+ return kind == "S" and all(_can_pack(field.type) for field in type_.__fields__)
1192
+
1193
+ if issubclass(type_, Array) and (
1194
+ issubclass(type_.__type__, (Structure, BaseArray)) or not _can_pack(type_.__type__)
1195
+ ):
1196
+ return False
1197
+
1198
+ return any(issubclass(type_, base) and _has_default_write(type_, base) for base in _PACKABLE_TYPES)
1199
+
1200
+
1201
+ def _can_pack_bits(field: Field) -> bool:
1202
+ """Determine if the value of the bit buffer for a bit field can be written by packing it."""
1203
+ type_ = _get_bits_type(field)
1204
+ return issubclass(type_, (Int, Packed)) and _can_pack(type_)
1205
+
1206
+
1207
+ def _has_default_write(type_: type[BaseType], base: type[BaseType]) -> bool:
1208
+ """Return whether a type uses the write methods of its base type, and doesn't have custom ones."""
1209
+ return all(getattr(type_, name).__func__ is getattr(base, name).__func__ for name in ("_write", "_write_array"))
1210
+
1211
+
1212
+ def _int_to_bytes(type_: type[Int], value: str) -> str:
1213
+ """Return the code to convert a value of an integer type to bytes."""
1214
+ return f"{value}.to_bytes({type_.__size__}, _byteorder[endian], signed={bool(type_.signed)})"
1215
+
1216
+
1217
+ def _get_attribute(var: str, name: str) -> str:
1218
+ """Return the code to get an attribute."""
1219
+ if name.isidentifier() and not iskeyword(name):
1220
+ return f"{var}.{name}"
1221
+ return f"_getattr({var}, {name!r})"
1222
+
1223
+
770
1224
  def _set_attribute(var: str, name: str, value: str) -> str:
771
1225
  """Return the code to set an attribute."""
772
1226
  if name.isidentifier() and not iskeyword(name):
@@ -809,25 +1263,18 @@ def _block(lines: list[str]) -> str:
809
1263
 
810
1264
  def _generate_struct_info(
811
1265
  fields: list[Field],
812
- align: bool = False,
813
1266
  inline: Callable[[type[BaseType]], str | None] | None = None,
814
1267
  ) -> Iterator[tuple[Field, int, str]]:
815
1268
  if not fields:
816
1269
  return
817
1270
 
818
1271
  current_offset = fields[0].offset
819
- imaginary_offset = 0
820
1272
  for field in fields:
821
1273
  # We moved -- probably due to alignment
822
1274
  if field.offset is not None and (drift := field.offset - current_offset) > 0:
823
1275
  yield None, drift, "x"
824
1276
  current_offset += drift
825
1277
 
826
- if align and field.offset is None and (drift := -imaginary_offset & (field.alignment - 1)) > 0:
827
- # Assume we started at a correctly aligned boundary
828
- yield None, drift, "x"
829
- imaginary_offset += drift
830
-
831
1278
  count = 1
832
1279
  read_type = _get_read_type(field.type)
833
1280
 
@@ -853,25 +1300,41 @@ def _generate_struct_info(
853
1300
  elif issubclass(read_type, (Char, Wchar, Int)):
854
1301
  yield field, count * read_type.__size__, "x"
855
1302
 
856
- size = count * read_type.__size__
857
- imaginary_offset += size
858
1303
  if current_offset is not None:
859
- current_offset += size
1304
+ current_offset += count * read_type.__size__
860
1305
 
861
1306
 
862
1307
  def _optimize_struct_fmt(items: Iterable[tuple[int, str]]) -> str:
863
- """Create a struct format string from ``(count, char)`` items, merging consecutive items of the same char."""
1308
+ """Create a struct format string from ``(count, char)`` items, merging consecutive items of the same char.
1309
+
1310
+ Byte strings aren't merged, since those are a single value regardless of their count.
1311
+ """
864
1312
  chars = []
865
1313
  for char, group in groupby(items, key=itemgetter(1)):
866
- if count := sum(count for count, _ in group):
1314
+ if char == "s":
1315
+ chars.extend(f"{count}{char}" for count, _ in group)
1316
+ elif count := sum(count for count, _ in group):
867
1317
  chars.append(f"{count if count > 1 else ''}{char}")
868
1318
 
869
1319
  return "".join(chars)
870
1320
 
871
1321
 
1322
+ def _struct_functions(fmt: str, name: str) -> dict[str, Callable]:
1323
+ """Return the given function of a struct with the given format for every endianness.
1324
+
1325
+ The ``@`` endianness uses the ``=`` format, since ``@`` would add padding between items to align them.
1326
+ """
1327
+ return {endian: getattr(_struct("=" if endian == "@" else endian, fmt), name) for endian in _ENDIANNESS}
1328
+
1329
+
1330
+ def _get_bit_buffer_type(field: Field) -> type[BaseType]:
1331
+ """Return the type that ``BitBuffer`` uses for a bit field, only bit fields of the same type share a value."""
1332
+ return field.type.__type__ if issubclass(field.type, (Enum, Flag)) else field.type
1333
+
1334
+
872
1335
  def _get_bits_type(field: Field) -> type[BaseType]:
873
1336
  """Return the type that ``BitBuffer`` reads a bit field as."""
874
- type_ = field.type.__type__ if issubclass(field.type, (Enum, Flag)) else field.type
1337
+ type_ = _get_bit_buffer_type(field)
875
1338
  return type_.__cs__.uint8 if issubclass(type_, Char) else type_
876
1339
 
877
1340
 
@@ -86,7 +86,7 @@ class cstruct:
86
86
  "int24": self._make_int_type("int24", 3, True, alignment=4),
87
87
  "uint24": self._make_int_type("uint24", 3, False, alignment=4),
88
88
  "int48": self._make_int_type("int48", 6, True, alignment=8),
89
- "uint48": self._make_int_type("int48", 6, False, alignment=8),
89
+ "uint48": self._make_int_type("uint48", 6, False, alignment=8),
90
90
  "int128": self._make_int_type("int128", 16, True, alignment=16),
91
91
  "uint128": self._make_int_type("uint128", 16, False, alignment=16),
92
92
 
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import functools
4
4
  import warnings
5
5
  from io import SEEK_CUR, BytesIO
6
+ from types import MethodType
6
7
  from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, TypeVar
7
8
 
8
9
  from dissect.cstruct.exception import ArraySizeError
@@ -19,6 +20,8 @@ if TYPE_CHECKING:
19
20
 
20
21
  EOF = -0xE0F # Negative counts are illegal anyway, so abuse that for our EOF sentinel
21
22
 
23
+ _IMMUTABLE_TYPES = (int, float, bytes, str)
24
+
22
25
 
23
26
  class MetaType(type):
24
27
  """Base metaclass for cstruct type classes."""
@@ -218,7 +221,8 @@ class MetaType(type):
218
221
  array: The array to write.
219
222
  endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
220
223
  """
221
- return sum(cls._write(stream, entry, endian=endian) for entry in array)
224
+ write = cls._write
225
+ return sum(write(stream, entry, endian=endian) for entry in array)
222
226
 
223
227
  def _write_0(cls, stream: BinaryIO, array: list[Self], *, endian: Endianness) -> int: # type: ignore
224
228
  """Internal function for writing null-terminated arrays.
@@ -295,20 +299,29 @@ class _overload:
295
299
  b'\\x7b\\x00\\x00\\x00'
296
300
  """
297
301
 
298
- def __init__(self, func: Callable[..., Any]) -> None:
302
+ def __init__(self, func: Callable[..., Any], method: Callable[..., Any]) -> None:
299
303
  self.func = func
304
+ self.method = method
300
305
 
301
- def __get__(self, instance: BaseType | None, owner: type[BaseType]) -> Callable[[], bytes]:
306
+ def __get__(self, instance: BaseType | None, owner: type[BaseType]) -> Callable[..., Any]:
302
307
  if instance is None:
303
308
  return functools.partial(self.func, owner)
304
- return functools.partial(self.func, instance.__class__, value=instance)
309
+ return MethodType(self.method, instance)
310
+
311
+
312
+ def _instance_dumps(self: BaseType, *, endian: AllowedEndianness | None = None) -> bytes:
313
+ return MetaType.dumps(self.__class__, self, endian=endian)
314
+
315
+
316
+ def _instance_write(self: BaseType, stream: BinaryIO, *, endian: AllowedEndianness | None = None) -> int:
317
+ return MetaType.write(self.__class__, stream, self, endian=endian)
305
318
 
306
319
 
307
320
  class BaseType(metaclass=MetaType):
308
321
  """Base class for cstruct type classes."""
309
322
 
310
- dumps = _overload(MetaType.dumps)
311
- write = _overload(MetaType.write)
323
+ dumps = _overload(MetaType.dumps, _instance_dumps)
324
+ write = _overload(MetaType.write, _instance_write)
312
325
 
313
326
  def __len__(self) -> int:
314
327
  """Return the byte size of the type."""
@@ -338,9 +351,10 @@ class BaseArray(BaseType):
338
351
 
339
352
  @classmethod
340
353
  def __default__(cls) -> BaseType:
341
- return type.__call__(
342
- cls, [cls.__type__.__default__()] * (cls.__num_entries__ if isinstance(cls.__num_entries__, int) else 0)
343
- )
354
+ count = cls.__num_entries__ if isinstance(cls.__num_entries__, int) else 0
355
+ if isinstance(value := cls.__type__.__default__(), _IMMUTABLE_TYPES):
356
+ return type.__call__(cls, [value] * count)
357
+ return type.__call__(cls, [cls.__type__.__default__() for _ in range(count)])
344
358
 
345
359
  @classmethod
346
360
  def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> list[BaseType]:
@@ -370,12 +384,24 @@ class BaseArray(BaseType):
370
384
  if cls.__null_terminated__:
371
385
  return cls.__type__._write_0(stream, data, endian=endian)
372
386
 
373
- if not cls.__dynamic__ and (remaining := cls.__num_entries__ - (actual_size := len(data))):
387
+ if not cls.__dynamic__:
388
+ data = cls._pad(data)
389
+
390
+ return cls.__type__._write_array(stream, data, endian=endian)
391
+
392
+ @classmethod
393
+ def _pad(cls, data: list[Any]) -> list[Any]:
394
+ """Internal function for padding the entries of a static array to its size.
395
+
396
+ Args:
397
+ data: The entries of the array.
398
+ """
399
+ if remaining := cls.__num_entries__ - (actual_size := len(data)):
374
400
  if remaining < 0:
375
401
  raise ArraySizeError(f"Expected static array size {cls.__num_entries__}, got {actual_size} instead.")
376
402
  data = data + [cls.__type__.__default__()] * remaining
377
403
 
378
- return cls.__type__._write_array(stream, data, endian=endian)
404
+ return data
379
405
 
380
406
  @classproperty
381
407
  def type(cls) -> type[BaseType]:
@@ -23,8 +23,9 @@ class CharArray(bytes, BaseArray):
23
23
  return type.__call__(cls, super()._read(stream, context=context, endian=endian))
24
24
 
25
25
  @classmethod
26
- def _write(cls, stream: BinaryIO, data: bytes | str | list[int], *, endian: Endianness) -> int:
27
- if isinstance(data, list):
26
+ def _write(cls, stream: BinaryIO, data: bytes | bytearray | str | list[int], *, endian: Endianness) -> int:
27
+ # Copy mutable data types to bytes so we don't modify them in place
28
+ if isinstance(data, (list, bytearray)):
28
29
  data = bytes(data)
29
30
  elif isinstance(data, str):
30
31
  data = data.encode("latin-1")
@@ -92,4 +93,7 @@ class Char(bytes, BaseType):
92
93
  if isinstance(data, str):
93
94
  data = data.encode("latin-1")
94
95
 
95
- return stream.write(data)
96
+ if (actual_size := len(data)) > 1:
97
+ raise ValueError(f"Expected a single character, got {actual_size} instead.")
98
+
99
+ return stream.write(data or b"\x00")
@@ -7,7 +7,7 @@ from collections.abc import MutableMapping
7
7
  from contextlib import contextmanager
8
8
  from enum import Enum
9
9
  from functools import lru_cache
10
- from itertools import chain
10
+ from itertools import chain, repeat
11
11
  from operator import attrgetter
12
12
  from textwrap import dedent
13
13
  from types import FunctionType
@@ -18,6 +18,7 @@ from dissect.cstruct.expression import Expression
18
18
  from dissect.cstruct.lexer import IDENTIFIER_TYPES
19
19
  from dissect.cstruct.types.base import (
20
20
  _BUFFER_TYPES,
21
+ _IMMUTABLE_TYPES,
21
22
  BaseArray,
22
23
  BaseType,
23
24
  MetaType,
@@ -141,7 +142,6 @@ class StructureMetaType(MetaType):
141
142
  else:
142
143
  size, alignment = cls._calculate_size_and_offsets(fields, align)
143
144
 
144
- # TODO: compile _write
145
145
  # TODO: generate cached_property for lazy reading
146
146
 
147
147
  classdict["__size__"] = size
@@ -193,7 +193,7 @@ class StructureMetaType(MetaType):
193
193
  # Moved to a bit field of another type, e.g. uint16 f1 : 8, uint32 f2 : 8;
194
194
  or field_type != bits_type
195
195
  # Still processing a bit field, but it's at a different offset due to alignment or a manual offset
196
- or (bits_type is not None and offset > bits_field_offset + bits_type.__size__)
196
+ or (None not in (offset, bits_field_offset) and offset > bits_field_offset + bits_type.__size__)
197
197
  ):
198
198
  # ... if any of this is true, we have to move to the next field
199
199
  bits_type = field_type
@@ -299,7 +299,6 @@ class StructureMetaType(MetaType):
299
299
  def _write(cls, stream: BinaryIO, data: Structure, *, endian: Endianness) -> int:
300
300
  bit_buffer = None
301
301
  struct_start = stream.tell()
302
- num = 0
303
302
 
304
303
  for field in cls.__fields__:
305
304
  field_type = field.type
@@ -331,7 +330,7 @@ class StructureMetaType(MetaType):
331
330
  stream.write(b"\x00" * align_pad)
332
331
  offset += align_pad
333
332
 
334
- value = getattr(data, field._name, None)
333
+ value = getattr(data, field._name)
335
334
  if value is None:
336
335
  value = field_type.__default__()
337
336
 
@@ -345,7 +344,6 @@ class StructureMetaType(MetaType):
345
344
  bit_buffer.write(field_type, value, field.bits)
346
345
  else:
347
346
  field_type._write(stream, value, endian=endian)
348
- num += stream.tell() - offset
349
347
 
350
348
  if bit_buffer is not None and bit_buffer._type is not None:
351
349
  bit_buffer.flush()
@@ -354,7 +352,7 @@ class StructureMetaType(MetaType):
354
352
  # Align the stream
355
353
  stream.write(b"\x00" * (-stream.tell() & (cls.__alignment__ - 1)))
356
354
 
357
- return num
355
+ return stream.tell() - struct_start
358
356
 
359
357
  def add_field(cls, name: str, type_: type[BaseType], bits: int | None = None, offset: int | None = None) -> None:
360
358
  field = Field(name, type_, bits=bits, offset=offset)
@@ -698,6 +696,10 @@ class UnionMetaType(StructureMetaType):
698
696
 
699
697
  return obj
700
698
 
699
+ def _write_field(cls) -> Field | None:
700
+ """Return the field to write a union by, its largest field and preferably not an anonymous structure."""
701
+ return max(cls.__fields__, key=lambda e: (e.type.__size__ or 0, e.name is not None), default=None)
702
+
701
703
  def _write(cls, stream: BinaryIO, data: Union, *, endian: Endianness) -> int:
702
704
  if cls.__dynamic__:
703
705
  raise NotImplementedError("Writing dynamic unions is not yet supported")
@@ -705,24 +707,9 @@ class UnionMetaType(StructureMetaType):
705
707
  offset = stream.tell()
706
708
  expected_offset = offset + len(cls)
707
709
 
708
- # Sort by largest field
709
- fields = sorted(cls.__fields__, key=lambda e: e.type.__size__ or 0, reverse=True)
710
- anonymous_struct = False
711
-
712
- # Try to write by largest field
713
- for field in fields:
714
- if isinstance(field.type, StructureMetaType) and field.name is None:
715
- # Prefer to write regular fields initially
716
- anonymous_struct = field.type
717
- continue
718
-
710
+ if (field := cls._write_field()) is not None:
719
711
  # Write the value
720
712
  field.type._write(stream, getattr(data, field._name), endian=endian)
721
- break
722
-
723
- # If we haven't written anything yet and we initially skipped an anonymous struct, write it now
724
- if stream.tell() == offset and anonymous_struct:
725
- anonymous_struct._write(stream, data, endian=endian)
726
713
 
727
714
  # If we haven't filled the union size yet, pad it
728
715
  if remaining := expected_offset - stream.tell():
@@ -745,7 +732,9 @@ class Union(Structure, metaclass=UnionMetaType):
745
732
  raise NotImplementedError("Modifying a dynamic union is not yet supported")
746
733
 
747
734
  super().__setattr__(attr, value)
748
- self._rebuild(attr)
735
+ # Fields of an anonymous structure are rebuilt through the proxy of that structure
736
+ if attr in self.__class__.__lookup__:
737
+ self._rebuild(attr)
749
738
 
750
739
  def _rebuild(self, attr: str) -> None:
751
740
  if (cur_buf := getattr(self, "_buf", None)) is None:
@@ -773,13 +762,17 @@ class Union(Structure, metaclass=UnionMetaType):
773
762
  object.__setattr__(self, "__dynamic_sizes__", sizes)
774
763
 
775
764
  def _proxify(self) -> None:
776
- def _proxy_structure(value: Structure) -> None:
765
+ def _proxy_structure(value: Structure, attr: str | None = None) -> None:
777
766
  for field in value.__class__.__fields__:
778
767
  if issubclass(field.type, Structure):
779
768
  nested_value = getattr(value, field._name)
780
- proxy = UnionProxy(self, field._name, nested_value)
781
- object.__setattr__(value, field._name, proxy)
782
- _proxy_structure(nested_value)
769
+ # Changing a nested field rebuilds the union from the top level field it's in
770
+ member = attr or field._name
771
+ object.__setattr__(value, field._name, UnionProxy(self, member, nested_value))
772
+ # The value of a nested union is already a proxy, so continue with the value behind it
773
+ if isinstance(nested_value, UnionProxy):
774
+ nested_value = nested_value.__target__
775
+ _proxy_structure(nested_value, member)
783
776
 
784
777
  _proxy_structure(self)
785
778
 
@@ -864,7 +857,7 @@ def _make_structure__init__(fields: list[str]) -> str:
864
857
  """
865
858
  field_args = ", ".join(f"{field} = None" for field in fields)
866
859
  field_init = "\n".join(
867
- f" self.{name} = {name} if {name} is not None else _{i}_default" for i, name in enumerate(fields)
860
+ f" self.{name} = {name} if {name} is not None else _{i}_default()" for i, name in enumerate(fields)
868
861
  )
869
862
 
870
863
  code = f"def __init__(self{', ' + field_args or ''}):\n"
@@ -880,7 +873,7 @@ def _make_union__init__(fields: list[str]) -> str:
880
873
  """
881
874
  field_args = ", ".join(f"{field} = None" for field in fields)
882
875
  field_init = "\n".join(
883
- f" object.__setattr__(self, '{name}', {name} if {name} is not None else _{i}_default)"
876
+ f" object.__setattr__(self, '{name}', {name} if {name} is not None else _{i}_default())"
884
877
  for i, name in enumerate(fields)
885
878
  )
886
879
 
@@ -976,7 +969,7 @@ def _generate_structure__init__(fields: list[Field]) -> FunctionType:
976
969
  co_names=_remap_co_values(template.__code__.co_names, mapping),
977
970
  co_varnames=_remap_co_values(template.__code__.co_varnames, mapping),
978
971
  ),
979
- template.__globals__ | {f"__{field._name}_default__": field.type.__default__() for field in fields},
972
+ template.__globals__ | {f"__{field._name}_default__": _default_factory(field.type) for field in fields},
980
973
  argdefs=template.__defaults__,
981
974
  )
982
975
 
@@ -996,11 +989,17 @@ def _generate_union__init__(fields: list[Field]) -> FunctionType:
996
989
  co_names=_remap_co_values(template.__code__.co_names, mapping),
997
990
  co_varnames=_remap_co_values(template.__code__.co_varnames, mapping),
998
991
  ),
999
- template.__globals__ | {f"__{field._name}_default__": field.type.__default__() for field in fields},
992
+ template.__globals__ | {f"__{field._name}_default__": _default_factory(field.type) for field in fields},
1000
993
  argdefs=template.__defaults__,
1001
994
  )
1002
995
 
1003
996
 
997
+ def _default_factory(type_: type[BaseType]) -> Callable[[], Any]:
998
+ """Return a function that returns default values of a type, which are only shared if they're immutable."""
999
+ value = type_.__default__()
1000
+ return repeat(value).__next__ if isinstance(value, _IMMUTABLE_TYPES) else type_.__default__
1001
+
1002
+
1004
1003
  def _generate_co_mapping(fields: list[Field]) -> dict[str, str]:
1005
1004
  """Generates a mapping of generated code object names to field names.
1006
1005
 
@@ -28,12 +28,15 @@ class WcharArray(str, BaseArray):
28
28
  if cls.__null_terminated__:
29
29
  data += "\x00"
30
30
 
31
- if not cls.__dynamic__ and (remaining := cls.__num_entries__ - (actual_size := len(data))):
31
+ buf = data.encode(Wchar.__encoding_map__[endian], "surrogatepass")
32
+
33
+ # A surrogate pair is a single character that takes up two entries of the array
34
+ if not cls.__dynamic__ and (remaining := cls.__num_entries__ - (actual_size := len(buf) // 2)):
32
35
  if remaining < 0:
33
36
  raise ArraySizeError(f"Expected static array size {cls.__num_entries__}, got {actual_size} instead.")
34
- data += "\x00" * remaining
37
+ buf += b"\x00\x00" * remaining
35
38
 
36
- return stream.write(data.encode(Wchar.__encoding_map__[endian]))
39
+ return stream.write(buf)
37
40
 
38
41
 
39
42
  class Wchar(str, BaseType):
@@ -42,6 +45,7 @@ class Wchar(str, BaseType):
42
45
  __ArrayType__ = WcharArray
43
46
 
44
47
  __slots__ = ()
48
+ # Decode with "surrogatepass" so lone surrogates are handled correctly
45
49
  __encoding_map__: ClassVar[dict[Endianness, str]] = {
46
50
  "@": f"utf-16-{sys.byteorder[0]}e",
47
51
  "=": f"utf-16-{sys.byteorder[0]}e",
@@ -72,12 +76,13 @@ class Wchar(str, BaseType):
72
76
  if count != EOF and len(data) != count:
73
77
  raise EOFError(f"Read {len(data)} bytes, but expected {count}")
74
78
 
75
- return type.__call__(cls, str(data, cls.__encoding_map__[endian]))
79
+ return type.__call__(cls, str(data, cls.__encoding_map__[endian], "surrogatepass"))
76
80
 
77
81
  @classmethod
78
82
  def _read_0(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Wchar:
79
83
  if _is_seekable(stream):
80
- return type.__call__(cls, _read_null_terminated(stream, 2).decode(cls.__encoding_map__[endian]))
84
+ data = _read_null_terminated(stream, 2)
85
+ return type.__call__(cls, data.decode(cls.__encoding_map__[endian], "surrogatepass"))
81
86
 
82
87
  buf = []
83
88
  while True:
@@ -90,8 +95,14 @@ class Wchar(str, BaseType):
90
95
 
91
96
  buf.append(point)
92
97
 
93
- return type.__call__(cls, b"".join(buf).decode(cls.__encoding_map__[endian]))
98
+ return type.__call__(cls, b"".join(buf).decode(cls.__encoding_map__[endian], "surrogatepass"))
94
99
 
95
100
  @classmethod
96
101
  def _write(cls, stream: BinaryIO, data: str, *, endian: Endianness) -> int:
97
- return stream.write(data.encode(cls.__encoding_map__[endian]))
102
+ buf = data.encode(cls.__encoding_map__[endian], "surrogatepass")
103
+
104
+ # A surrogate pair is a single character that doesn't fit in a single wchar
105
+ if (actual_size := len(buf) // 2) > 1:
106
+ raise ValueError(f"Expected a single character, got {actual_size} instead.")
107
+
108
+ return stream.write(buf or b"\x00\x00")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.cstruct
3
- Version: 5.0.dev12
3
+ Version: 5.0.dev14
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-Expression: Apache-2.0
@@ -1,7 +1,7 @@
1
1
  dissect/cstruct/__init__.py,sha256=KOyNeUvu4c0JaAiyOfj7iftGwUIjTmD8MwHtFsLPLSU,1504
2
2
  dissect/cstruct/bitbuffer.py,sha256=aNzY4lkWe8ffAc8j7FODXzwZc8gZwbey77UosyoYCsw,2711
3
- dissect/cstruct/compiler.py,sha256=hhX1bxZFsPOcs6BUO82YHNtWqhKsiOb-H-d18Aa1Q6E,35148
4
- dissect/cstruct/cstruct.py,sha256=VQDgGF7OUaPKQ_Wsy_2fUHL9Jlims6R2JOh-1_bgiZ8,26550
3
+ dissect/cstruct/compiler.py,sha256=0u58J5pB4izosBj8G4wiBI1JSAAgHFMLM63IaSG4YO4,54509
4
+ dissect/cstruct/cstruct.py,sha256=mJerzPouWV29RdK5LqjGitDDMvNKcadoT2hj1zpExnM,26551
5
5
  dissect/cstruct/exception.py,sha256=RqD08LuBkbMqa3iQ1A05DNN8XgEmQR_SUnguwe9LGo8,316
6
6
  dissect/cstruct/expression.py,sha256=Y27-N48BomBoRFozbWhGDF_DS2lfklRwWVUbCzGnxHE,10873
7
7
  dissect/cstruct/lexer.py,sha256=NDUfAWgyxgRNRuVO8ExXQ51LqQXS2mv9bywVa_M7tFQ,19665
@@ -10,21 +10,21 @@ dissect/cstruct/util.py,sha256=vAf-kdOkk3zprW_gD6QmQeN3VIRgQqhPS4B_ha1jEpQ,18040
10
10
  dissect/cstruct/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  dissect/cstruct/tools/stubgen.py,sha256=MeZkLmSeNSLNcAEwlAdIGv1lKvlvK3UQ4WN85ovsxTM,7672
12
12
  dissect/cstruct/types/__init__.py,sha256=6P71Pu94Dv7RNjZpDZ19pqzx-G0QzmQw4q0-3zWALso,911
13
- dissect/cstruct/types/base.py,sha256=10HLF2SDeWUMVc6ISMv8xKdEggqrxLPHnJMwI03DEFg,17216
14
- dissect/cstruct/types/char.py,sha256=Rzjn2-pXbn-fV_UnNRGgQugFZ50mPuXQhcmgHvY5PR0,3172
13
+ dissect/cstruct/types/base.py,sha256=bbaxiHCVkoU6ugqn5RYishPQ5L6xGrPfQuejjro0K_g,18133
14
+ dissect/cstruct/types/char.py,sha256=N19R7Zzn2uMuG-ULW5X2UgmgkLvjaBj1Td7AQQI93Fg,3417
15
15
  dissect/cstruct/types/enum.py,sha256=nOdI6vGtqTfPMhOBkKuiuOriIB2YHSt8Q9ruri37s3U,9177
16
16
  dissect/cstruct/types/flag.py,sha256=YxQHix8ZhK1qQX3J_Og7GJp_4JAxCwzUbUibuJeDaWQ,3479
17
17
  dissect/cstruct/types/int.py,sha256=9RbdrkvAI28zATM97VVORQnras0XhLaVMtdPjJwpTH4,2089
18
18
  dissect/cstruct/types/leb128.py,sha256=V97XTJT_Xbw1LpTwTN1OkjPHuQQAQUxd8OGuA6yEDmE,2387
19
19
  dissect/cstruct/types/packed.py,sha256=NgCX0YWX8SH8z47atq0f39w9MBjU_eBFJtUisUWdO-M,3867
20
20
  dissect/cstruct/types/pointer.py,sha256=FNuB9bfENWRebbKX-R6Vs6xWPMHepjvOhhomeUhr7I8,3985
21
- dissect/cstruct/types/structure.py,sha256=WDMmfhP2XE5p1Bn-WgFFstfaWC709AU7_y-SGi3TyDw,39116
21
+ dissect/cstruct/types/structure.py,sha256=mxDvurQ0oAjI2flykhAPlT-03280cDpwGTz0-bqbr-0,39558
22
22
  dissect/cstruct/types/void.py,sha256=fV4hKk50uop_EKbR1nfy1cdtAzvUYrSymXsdakCyFk8,1222
23
- dissect/cstruct/types/wchar.py,sha256=qUbX6auj0qHWSOkgvaI7ZW_f7RGgwa-Hf_d_mDpntlM,3410
24
- dissect_cstruct-5.0.dev12.dist-info/licenses/COPYRIGHT,sha256=H-18RXfshdH9AdHwW2eO1Xa-5s6tY5eipHh5c0whDu4,316
25
- dissect_cstruct-5.0.dev12.dist-info/licenses/LICENSE,sha256=PhUqiw6jAh2KbBdVRPBq_hfAvfcTBin7nZ3CK7NQbTM,11341
26
- dissect_cstruct-5.0.dev12.dist-info/METADATA,sha256=TvTeDtkVFGsyMPWBgh3ANr5eKM1bLSi6SgI1hv4TUqw,9256
27
- dissect_cstruct-5.0.dev12.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
28
- dissect_cstruct-5.0.dev12.dist-info/entry_points.txt,sha256=z53zqZqwD2OLrAkRwrP4wTeiU9CQe7xrMly0T2c0_wQ,71
29
- dissect_cstruct-5.0.dev12.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
30
- dissect_cstruct-5.0.dev12.dist-info/RECORD,,
23
+ dissect/cstruct/types/wchar.py,sha256=VrnPXsP_lsyA-1xr4D2aSIVijumMhUuZWNlDHcn12-s,3964
24
+ dissect_cstruct-5.0.dev14.dist-info/licenses/COPYRIGHT,sha256=H-18RXfshdH9AdHwW2eO1Xa-5s6tY5eipHh5c0whDu4,316
25
+ dissect_cstruct-5.0.dev14.dist-info/licenses/LICENSE,sha256=PhUqiw6jAh2KbBdVRPBq_hfAvfcTBin7nZ3CK7NQbTM,11341
26
+ dissect_cstruct-5.0.dev14.dist-info/METADATA,sha256=0EhDz7fNITZQWzSYoy46-j0nObu4B1Am3-nEipozmxc,9256
27
+ dissect_cstruct-5.0.dev14.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
28
+ dissect_cstruct-5.0.dev14.dist-info/entry_points.txt,sha256=z53zqZqwD2OLrAkRwrP4wTeiU9CQe7xrMly0T2c0_wQ,71
29
+ dissect_cstruct-5.0.dev14.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
30
+ dissect_cstruct-5.0.dev14.dist-info/RECORD,,