dissect.cstruct 5.0.dev11__tar.gz → 5.0.dev13__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/CHANGELOG.md +11 -0
  2. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/PKG-INFO +1 -1
  3. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/compiler.py +119 -81
  4. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/cstruct.py +1 -1
  5. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/base.py +23 -9
  6. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/char.py +7 -3
  7. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/structure.py +31 -32
  8. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/wchar.py +18 -7
  9. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/PKG-INFO +1 -1
  10. dissect_cstruct-5.0.dev13/dissect.cstruct.egg-info/scm_version.json +8 -0
  11. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_align.py +24 -0
  12. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_benchmark.py +38 -38
  13. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_bitfield.py +44 -0
  14. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_compiler.py +18 -17
  15. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_char.py +44 -0
  16. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_enum.py +23 -0
  17. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_pointer.py +20 -0
  18. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_structure.py +91 -5
  19. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_union.py +95 -0
  20. dissect_cstruct-5.0.dev13/tests/test_types_wchar.py +309 -0
  21. dissect_cstruct-5.0.dev11/dissect.cstruct.egg-info/scm_version.json +0 -8
  22. dissect_cstruct-5.0.dev11/tests/test_types_wchar.py +0 -150
  23. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/.git-blame-ignore-revs +0 -0
  24. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/.gitattributes +0 -0
  25. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/COPYRIGHT +0 -0
  26. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/LICENSE +0 -0
  27. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/MANIFEST.in +0 -0
  28. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/README.md +0 -0
  29. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/__init__.py +0 -0
  30. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/bitbuffer.py +0 -0
  31. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/exception.py +0 -0
  32. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/expression.py +0 -0
  33. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/lexer.py +0 -0
  34. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/parser.py +0 -0
  35. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/tools/__init__.py +0 -0
  36. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/tools/stubgen.py +0 -0
  37. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/__init__.py +0 -0
  38. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/enum.py +0 -0
  39. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/flag.py +0 -0
  40. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/int.py +0 -0
  41. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/leb128.py +0 -0
  42. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/packed.py +0 -0
  43. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/pointer.py +0 -0
  44. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/void.py +0 -0
  45. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/util.py +0 -0
  46. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/SOURCES.txt +0 -0
  47. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/dependency_links.txt +0 -0
  48. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/entry_points.txt +0 -0
  49. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/requires.txt +0 -0
  50. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/scm_file_list.json +0 -0
  51. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/top_level.txt +0 -0
  52. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/examples/disk.py +0 -0
  53. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/examples/mirai.py +0 -0
  54. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/examples/pe.py +0 -0
  55. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/examples/protobuf.py +0 -0
  56. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/examples/secdesc.py +0 -0
  57. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/pyproject.toml +0 -0
  58. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/setup.cfg +0 -0
  59. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/__init__.py +0 -0
  60. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/_data/testdef.txt +0 -0
  61. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/_docs/Makefile +0 -0
  62. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/_docs/__init__.py +0 -0
  63. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/_docs/conf.py +0 -0
  64. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/_docs/index.rst +0 -0
  65. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/conftest.py +0 -0
  66. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_annotations.py +0 -0
  67. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_basic.py +0 -0
  68. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_bitbuffer.py +0 -0
  69. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_ctypes.py +0 -0
  70. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_expression.py +0 -0
  71. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_lexer.py +0 -0
  72. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_parser.py +0 -0
  73. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_tools_stubgen.py +0 -0
  74. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_base.py +0 -0
  75. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_custom.py +0 -0
  76. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_flag.py +0 -0
  77. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_int.py +0 -0
  78. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_leb128.py +0 -0
  79. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_packed.py +0 -0
  80. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_void.py +0 -0
  81. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_utils.py +0 -0
  82. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/utils.py +0 -0
  83. {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tox.ini +0 -0
@@ -22,6 +22,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
22
22
  - Faster paths for parsing primitive types from bytes-like objects, enum and flag lookups and flag operators.
23
23
  - Null-terminated strings are read in chunks from seekable streams.
24
24
 
25
+ ### Fixed
26
+
27
+ - Structures and unions no longer share nested structures and arrays between their default values.
28
+ - Unions can be created with default values and read if they have doubly nested structures or nested unions.
29
+ - Writing a `bytearray` to a `char` array no longer modifies the `bytearray`.
30
+ - Writing a structure correctly returns the amount of bytes written, including padding and bit fields.
31
+ - Writing a structure raises an `AttributeError` for missing attributes and for values of a nested structure field that aren't a structure, instead of writing default values.
32
+ - A surrogate pair takes up two entries of a `wchar` array, so writing it no longer writes more bytes than the size of the array.
33
+ - A `wchar` string can hold a lone surrogate, instead of raising an error for the whole structure when reading or writing it. Bytes are written back as they were stored.
34
+ - Writing more than a single character to a `char` or `wchar` raises a `ValueError`, instead of writing all of it and moving the fields after it. An empty value is padded.
35
+
25
36
  ## [4.5] - 20-05-2025
26
37
 
27
38
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.cstruct
3
- Version: 5.0.dev11
3
+ Version: 5.0.dev13
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
@@ -182,8 +182,10 @@ _GLOBALS = {
182
182
  }
183
183
 
184
184
 
185
- class _ReadSourceGenerator:
186
- """Generates the source code of an optimized ``_read`` method for a structure or union."""
185
+ class _SourceGenerator:
186
+ """Base class for generating the source code of an optimized method of a structure or union."""
187
+
188
+ name: str
187
189
 
188
190
  def __init__(self, cls: type[Structure], fields: list[Field], inline: bool = True):
189
191
  self.cls = cls
@@ -191,24 +193,17 @@ class _ReadSourceGenerator:
191
193
  self.align = cls.__align__
192
194
  self.inline = inline and not self.align
193
195
 
196
+ # The values available to the generated code
197
+ self.symbols: dict[str, Any] = {"cs": cls.__cs__}
198
+ self._symbol_names: dict[int, str] = {}
199
+
194
200
  # Witnesses of inlined structure types (symbol of the type -> symbol of its __init__ at compile time)
195
201
  self._witnesses: dict[str, str] = {}
196
- self._fallback_symbol: str | None = None
197
202
 
198
203
  # The state of the bit buffer: the type of its current value and the remaining bits
199
204
  self._bits: tuple[type[BaseType], int] | None = None
200
205
  self._var_ids: dict[str, int] = {}
201
206
 
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
207
  def _type_symbol(self, type_: type[BaseType]) -> str:
213
208
  """Add a type as symbol, returns its name."""
214
209
  return self._add_symbol(type_, "type")
@@ -220,39 +215,12 @@ class _ReadSourceGenerator:
220
215
  self.symbols[symbol] = value
221
216
  return symbol
222
217
 
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
218
  def _var(self, name: str) -> str:
233
219
  """Return a unique variable name for the generated code."""
234
220
  index = self._var_ids.get(name, 0)
235
221
  self._var_ids[name] = index + 1
236
222
  return f"_{name}{index}"
237
223
 
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
224
  def _witness(self, type_: type[Structure]) -> str:
257
225
  """Add an inlined type as symbol, with a witness to detect modifications to it after compilation."""
258
226
  symbol = self._type_symbol(type_)
@@ -263,10 +231,22 @@ class _ReadSourceGenerator:
263
231
  """Return the conditions that are true if any of the inlined types were modified after compilation."""
264
232
  return [f"{type_}.__init__ is not {init}" for type_, init in self._witnesses.items()]
265
233
 
234
+ def _starts_bit_value(self, field: Field) -> bool:
235
+ """Return whether a bit field is the first of a new value of the bit buffer."""
236
+ return self._bits is None or self._bits[1] == 0 or self._bits[0] is not _get_bit_buffer_type(field)
237
+
238
+ def _advance_bits(self, field: Field) -> int:
239
+ """Update the state of the bit buffer for a bit field, returns the bits that were remaining before it."""
240
+ if self._starts_bit_value(field):
241
+ self._bits = (_get_bit_buffer_type(field), _get_bits_type(field).__size__ * 8)
242
+ remaining = self._bits[1]
243
+ self._bits = (self._bits[0], remaining - field.bits)
244
+ return remaining
245
+
266
246
  def generate(self) -> MethodType:
267
247
  source = self.generate_source()
268
248
 
269
- code = python_compile(source, f"<compiled {self.cls.__name__}._read>", "exec")
249
+ code = python_compile(source, f"<compiled {self.cls.__name__}.{self.name}>", "exec")
270
250
  exec(code, {**_GLOBALS, **self.symbols}, d := {})
271
251
  obj = d.popitem()[1]
272
252
  obj.__source__ = source
@@ -278,6 +258,56 @@ class _ReadSourceGenerator:
278
258
  return self._generate_union_source()
279
259
  return self._generate_struct_source()
280
260
 
261
+ def _generate_struct_source(self) -> str:
262
+ raise NotImplementedError
263
+
264
+ def _generate_union_source(self) -> str:
265
+ raise NotImplementedError
266
+
267
+
268
+ class _ReadSourceGenerator(_SourceGenerator):
269
+ """Generates the source code of an optimized ``_read`` method for a structure or union."""
270
+
271
+ name = "_read"
272
+
273
+ def __init__(self, cls: type[Structure], fields: list[Field], inline: bool = True):
274
+ super().__init__(cls, fields, inline)
275
+
276
+ self._fallback_symbol: str | None = None
277
+
278
+ # (field name, local variable name) of all fields assigned so far
279
+ self._assigned: list[tuple[str, str]] = []
280
+ self._has_context = False
281
+ self._uses_start = False
282
+ self._uses_sizes = False
283
+
284
+ def _context(self, lines: list[str]) -> str:
285
+ """Return the name of the context dictionary, creating it from the fields read so far if needed."""
286
+ if not self._has_context:
287
+ self._has_context = True
288
+ items = ", ".join(f"{name!r}: {var}" for name, var in self._assigned)
289
+ lines.append(f"values = {{{items}}}")
290
+
291
+ return "values"
292
+
293
+ def _assign(self, field: Field, value: str) -> str:
294
+ """Assign a value to a new local variable for the given field, and the context dictionary if it exists."""
295
+ var = f"_value{len(self._assigned)}"
296
+ self._assigned.append((field._name, var))
297
+ if self._has_context:
298
+ return f"{var} = values[{field._name!r}] = {value}"
299
+ return f"{var} = {value}"
300
+
301
+ def _assign_local(self, field: Field, value: str) -> list[str]:
302
+ return [self._assign(field, value)]
303
+
304
+ def _fallback(self) -> str:
305
+ """Return the symbol of the non-inlined fallback read function."""
306
+ if self._fallback_symbol is None:
307
+ fallback = _Fallback(self.cls, self.fields)
308
+ self._fallback_symbol = self._add_symbol(fallback, "fallback")
309
+ return self._fallback_symbol
310
+
281
311
  def _generate_struct_source(self) -> str:
282
312
  """Generate the code for reading a structure."""
283
313
  read_code = "\n".join(self._generate_fields_source())
@@ -405,6 +435,16 @@ class _ReadSourceGenerator:
405
435
 
406
436
  # Everything else - basic and composite types (and arrays of them), and inlined structures
407
437
  else:
438
+ if field.offset is None:
439
+ # The alignment depends on the position in the stream, so this needs a new block
440
+ if self.align:
441
+ yield from flush()
442
+ # A block is read in order, so a field before the current position needs a new block
443
+ elif current_offset is None or field.offset < current_offset:
444
+ yield from flush()
445
+ elif current_block:
446
+ current_offset = field.offset
447
+
408
448
  if not current_block:
409
449
  yield from align_to_field(field)
410
450
  current_block.append(field)
@@ -456,18 +496,11 @@ class _ReadSourceGenerator:
456
496
  element = self._type_symbol(field.type.__type__)
457
497
  return f"_type_call({type_}, {element}._read_array(stream, _count, context={context}, endian=endian))"
458
498
 
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
499
  def _generate_bits_source(self, field: Field) -> Iterator[str]:
464
500
  """Generate the code for reading a bit field."""
465
501
  read_type = _get_bits_type(field)
466
502
  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)
503
+ remaining = self._advance_bits(field)
471
504
 
472
505
  lines = []
473
506
  if new_value:
@@ -498,7 +531,7 @@ class _ReadSourceGenerator:
498
531
  """Generate the code for reading consecutive packed fields."""
499
532
  unpack = _Unpack("data")
500
533
  reads = []
501
- size = self._pack_fields(fields, 0, unpack, reads, self._assign_local, self.align)
534
+ size = self._pack_fields(fields, 0, unpack, reads, self._assign_local)
502
535
 
503
536
  lines = [f"buf = stream.read({size})", _check_eof(size)]
504
537
  if unpack.has_values():
@@ -513,7 +546,6 @@ class _ReadSourceGenerator:
513
546
  unpack: _Unpack,
514
547
  lines: list[str],
515
548
  target: Callable[[Field, str], list[str]],
516
- align: bool = False,
517
549
  ) -> int:
518
550
  """Generate the code for reading the given fields from ``buf``, starting at the given offset.
519
551
 
@@ -523,7 +555,7 @@ class _ReadSourceGenerator:
523
555
  Returns the offset after the last field.
524
556
  """
525
557
  inline = _inline_kind if self.inline else None
526
- for field, count, char in _generate_struct_info(fields, align, inline=inline):
558
+ for field, count, char in _generate_struct_info(fields, inline=inline):
527
559
  if field is None:
528
560
  # Padding
529
561
  unpack.add(count, "x")
@@ -595,16 +627,15 @@ class _ReadSourceGenerator:
595
627
  lines.append(f"_object_setattr({var}, '_endian', endian)")
596
628
  self._proxify(var, var, fields, lines)
597
629
 
598
- def _proxify(self, union: str, var: str, fields: list[Field], lines: list[str]) -> None:
630
+ def _proxify(self, union: str, var: str, fields: list[Field], lines: list[str], attr: str | None = None) -> None:
599
631
  """Generate the code equivalent to ``Union._proxify``."""
600
632
  for field in fields:
601
633
  if issubclass(field.type, Structure):
602
634
  nested = self._var("nested")
635
+ member = attr or field._name
603
636
  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)
637
+ lines.append(f"_object_setattr({var}, {field._name!r}, _UnionProxy({union}, {member!r}, {nested}))")
638
+ self._proxify(union, nested, field.type.__fields__, lines, member)
608
639
 
609
640
  def _generate_packed_field_source(
610
641
  self,
@@ -622,19 +653,18 @@ class _ReadSourceGenerator:
622
653
  array = self._type_symbol(field.type)
623
654
  element = self._type_symbol(field_type.__type__)
624
655
 
625
- if issubclass(field_type.__type__, Int):
626
- elem_size = field_type.__type__.__size__
627
- signed = bool(field_type.__type__.signed)
656
+ if issubclass(read_type, Int):
657
+ # Everything that's read as an integer type is sliced directly out of the buffer
658
+ elem_size = read_type.__size__
659
+ signed = bool(read_type.signed)
628
660
  lines.append(f"_array_buf = {getter}")
629
661
  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):
662
+ getter = f"[{raw} for i in range(0, {count}, {elem_size})]"
663
+
664
+ if issubclass(field_type.__type__, Pointer):
632
665
  context = self._context(lines)
633
666
  item = f"{element}.__new__({element}, value, stream, context={context}, endian=endian)"
634
667
  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
668
  else:
639
669
  value = f"list(map(_type_call, _repeat({element}), {getter}))"
640
670
 
@@ -647,7 +677,7 @@ class _ReadSourceGenerator:
647
677
  encoding = self._add_symbol(read_type.__encoding_map__, "encoding")
648
678
  # Using str() instead of .decode() also supports other bytes-like objects returned by the stream
649
679
  type_ = self._type_symbol(field.type)
650
- lines.extend(target(field, f"_type_call({type_}, str({getter}, {encoding}[endian]))"))
680
+ lines.extend(target(field, f"_type_call({type_}, str({getter}, {encoding}[endian], 'surrogatepass'))"))
651
681
 
652
682
  elif issubclass(field_type, Pointer):
653
683
  context = self._context(lines)
@@ -694,8 +724,7 @@ class _Unpack:
694
724
 
695
725
  def functions(self, name: str) -> dict[str, Callable]:
696
726
  """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}
727
+ return _struct_functions(_optimize_struct_fmt(self.items), name)
699
728
 
700
729
 
701
730
  def _check_union(cls: type[Union], fields: list[Field], inline: bool = True) -> int:
@@ -809,25 +838,18 @@ def _block(lines: list[str]) -> str:
809
838
 
810
839
  def _generate_struct_info(
811
840
  fields: list[Field],
812
- align: bool = False,
813
841
  inline: Callable[[type[BaseType]], str | None] | None = None,
814
842
  ) -> Iterator[tuple[Field, int, str]]:
815
843
  if not fields:
816
844
  return
817
845
 
818
846
  current_offset = fields[0].offset
819
- imaginary_offset = 0
820
847
  for field in fields:
821
848
  # We moved -- probably due to alignment
822
849
  if field.offset is not None and (drift := field.offset - current_offset) > 0:
823
850
  yield None, drift, "x"
824
851
  current_offset += drift
825
852
 
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
853
  count = 1
832
854
  read_type = _get_read_type(field.type)
833
855
 
@@ -853,25 +875,41 @@ def _generate_struct_info(
853
875
  elif issubclass(read_type, (Char, Wchar, Int)):
854
876
  yield field, count * read_type.__size__, "x"
855
877
 
856
- size = count * read_type.__size__
857
- imaginary_offset += size
858
878
  if current_offset is not None:
859
- current_offset += size
879
+ current_offset += count * read_type.__size__
860
880
 
861
881
 
862
882
  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."""
883
+ """Create a struct format string from ``(count, char)`` items, merging consecutive items of the same char.
884
+
885
+ Byte strings aren't merged, since those are a single value regardless of their count.
886
+ """
864
887
  chars = []
865
888
  for char, group in groupby(items, key=itemgetter(1)):
866
- if count := sum(count for count, _ in group):
889
+ if char == "s":
890
+ chars.extend(f"{count}{char}" for count, _ in group)
891
+ elif count := sum(count for count, _ in group):
867
892
  chars.append(f"{count if count > 1 else ''}{char}")
868
893
 
869
894
  return "".join(chars)
870
895
 
871
896
 
897
+ def _struct_functions(fmt: str, name: str) -> dict[str, Callable]:
898
+ """Return the given function of a struct with the given format for every endianness.
899
+
900
+ The ``@`` endianness uses the ``=`` format, since ``@`` would add padding between items to align them.
901
+ """
902
+ return {endian: getattr(_struct("=" if endian == "@" else endian, fmt), name) for endian in _ENDIANNESS}
903
+
904
+
905
+ def _get_bit_buffer_type(field: Field) -> type[BaseType]:
906
+ """Return the type that ``BitBuffer`` uses for a bit field, only bit fields of the same type share a value."""
907
+ return field.type.__type__ if issubclass(field.type, (Enum, Flag)) else field.type
908
+
909
+
872
910
  def _get_bits_type(field: Field) -> type[BaseType]:
873
911
  """Return the type that ``BitBuffer`` reads a bit field as."""
874
- type_ = field.type.__type__ if issubclass(field.type, (Enum, Flag)) else field.type
912
+ type_ = _get_bit_buffer_type(field)
875
913
  return type_.__cs__.uint8 if issubclass(type_, Char) else type_
876
914
 
877
915
 
@@ -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]:
@@ -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")