dissect.cstruct 4.5.dev5__tar.gz → 4.6.dev1__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 (76) hide show
  1. dissect_cstruct-4.6.dev1/CHANGELOG.md +53 -0
  2. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/PKG-INFO +2 -1
  3. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/compiler.py +5 -7
  4. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/expression.py +5 -6
  5. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/parser.py +7 -7
  6. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/types/base.py +1 -1
  7. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/types/structure.py +64 -14
  8. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/utils.py +4 -5
  9. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect.cstruct.egg-info/PKG-INFO +2 -1
  10. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect.cstruct.egg-info/SOURCES.txt +1 -0
  11. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/pyproject.toml +1 -0
  12. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_align.py +1 -1
  13. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_compiler.py +3 -23
  14. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_expression.py +3 -3
  15. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_structure.py +35 -1
  16. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/.git-blame-ignore-revs +0 -0
  17. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/.gitattributes +0 -0
  18. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/COPYRIGHT +0 -0
  19. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/LICENSE +0 -0
  20. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/MANIFEST.in +0 -0
  21. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/README.md +0 -0
  22. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/__init__.py +0 -0
  23. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/bitbuffer.py +0 -0
  24. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/cstruct.py +0 -0
  25. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/exceptions.py +0 -0
  26. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/tools/__init__.py +0 -0
  27. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/tools/stubgen.py +0 -0
  28. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/types/__init__.py +0 -0
  29. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/types/char.py +0 -0
  30. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/types/enum.py +0 -0
  31. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/types/flag.py +0 -0
  32. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/types/int.py +0 -0
  33. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/types/leb128.py +0 -0
  34. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/types/packed.py +0 -0
  35. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/types/pointer.py +0 -0
  36. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/types/void.py +0 -0
  37. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect/cstruct/types/wchar.py +0 -0
  38. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect.cstruct.egg-info/dependency_links.txt +0 -0
  39. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect.cstruct.egg-info/entry_points.txt +0 -0
  40. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect.cstruct.egg-info/requires.txt +0 -0
  41. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/dissect.cstruct.egg-info/top_level.txt +0 -0
  42. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/examples/disk.py +0 -0
  43. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/examples/mirai.py +0 -0
  44. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/examples/pe.py +0 -0
  45. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/examples/protobuf.py +0 -0
  46. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/examples/secdesc.py +0 -0
  47. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/setup.cfg +0 -0
  48. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/__init__.py +0 -0
  49. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/_data/testdef.txt +0 -0
  50. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/_docs/Makefile +0 -0
  51. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/_docs/__init__.py +0 -0
  52. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/_docs/conf.py +0 -0
  53. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/_docs/index.rst +0 -0
  54. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/conftest.py +0 -0
  55. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_annotations.py +0 -0
  56. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_basic.py +0 -0
  57. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_bitbuffer.py +0 -0
  58. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_bitfield.py +0 -0
  59. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_ctypes.py +0 -0
  60. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_parser.py +0 -0
  61. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_tools_stubgen.py +0 -0
  62. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_base.py +0 -0
  63. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_char.py +0 -0
  64. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_custom.py +0 -0
  65. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_enum.py +0 -0
  66. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_flag.py +0 -0
  67. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_int.py +0 -0
  68. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_leb128.py +0 -0
  69. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_packed.py +0 -0
  70. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_pointer.py +0 -0
  71. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_union.py +0 -0
  72. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_void.py +0 -0
  73. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_types_wchar.py +0 -0
  74. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/test_utils.py +0 -0
  75. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tests/utils.py +0 -0
  76. {dissect_cstruct-4.5.dev5 → dissect_cstruct-4.6.dev1}/tox.ini +0 -0
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Changed
10
+
11
+ - Optimize storage of field sizes.
12
+ - Rename `_sizes` property of `Structure` to `__sizes__`.
13
+ - Rename `_values` property of `Structure` to `__values__`.
14
+
15
+ ## [4.5] - 20-05-2025
16
+
17
+ ### Added
18
+
19
+ - Introduce experimental tool `cstruct-stubgen` to generate type stubs for cstruct definitions.
20
+
21
+ ### Fixed
22
+
23
+ - Generated classes are now hashable.
24
+ - Suppress spurious `TypeError: Dynamic size` errors when using cstruct interactively.
25
+
26
+ ## [4.4] - 03-10-2025
27
+
28
+ ### Fixed
29
+
30
+ - Resolve documentation warnings.
31
+
32
+ ### Changed
33
+
34
+ - Use the Ruff linter.
35
+
36
+ ## [4.3] - 11-18-2024
37
+
38
+ ### Fixed
39
+
40
+ - All cstruct types are now correctly default-initialized using the `__default__` member.
41
+
42
+ ## [4.2] - 10-10-2024
43
+
44
+ ### Fixed
45
+
46
+ - The string representation of enums now outputs the name of the constants.
47
+
48
+ ## [4.1] - 10-09-2024
49
+
50
+ ### Fixed
51
+
52
+ - Declaring an array of a nested struct type now works as intended.
53
+
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.cstruct
3
- Version: 4.5.dev5
3
+ Version: 4.6.dev1
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
7
7
  Project-URL: homepage, https://dissect.tools
8
8
  Project-URL: documentation, https://docs.dissect.tools/en/latest/projects/dissect.cstruct
9
9
  Project-URL: repository, https://github.com/fox-it/dissect.cstruct
10
+ Project-URL: changelog, https://github.com/fox-it/dissect.cstruct/blob/main/CHANGELOG.md
10
11
  Classifier: Development Status :: 5 - Production/Stable
11
12
  Classifier: Environment :: Console
12
13
  Classifier: Intended Audience :: Developers
@@ -123,8 +123,7 @@ class _ReadSourceGenerator:
123
123
 
124
124
  outro = """
125
125
  obj = type.__call__(cls, **r)
126
- obj._sizes = s
127
- obj._values = r
126
+ obj.__dynamic_sizes__ = s
128
127
 
129
128
  return obj
130
129
  """
@@ -227,18 +226,18 @@ class _ReadSourceGenerator:
227
226
 
228
227
  def _generate_structure(self, field: Field) -> Iterator[str]:
229
228
  template = f"""
230
- _s = stream.tell()
229
+ {'_s = stream.tell()' if field.type.dynamic else ''}
231
230
  r["{field._name}"] = {self._map_field(field)}._read(stream, context=r)
232
- s["{field._name}"] = stream.tell() - _s
231
+ {f's["{field._name}"] = stream.tell() - _s' if field.type.dynamic else ''}
233
232
  """
234
233
 
235
234
  yield dedent(template)
236
235
 
237
236
  def _generate_array(self, field: Field) -> Iterator[str]:
238
237
  template = f"""
239
- _s = stream.tell()
238
+ {'_s = stream.tell()' if field.type.dynamic else ''}
240
239
  r["{field._name}"] = {self._map_field(field)}._read(stream, context=r)
241
- s["{field._name}"] = stream.tell() - _s
240
+ {f's["{field._name}"] = stream.tell() - _s' if field.type.dynamic else ''}
242
241
  """
243
242
 
244
243
  yield dedent(template)
@@ -326,7 +325,6 @@ class _ReadSourceGenerator:
326
325
  parser = parser_template.format(type=self._map_field(field), getter=getter)
327
326
 
328
327
  reads.append(f'r["{field._name}"] = {parser}')
329
- reads.append(f's["{field._name}"] = {field_type.size}')
330
328
  reads.append("") # Generates a newline in the resulting code
331
329
 
332
330
  size += field_type.size
@@ -187,8 +187,7 @@ class Expression:
187
187
  "sizeof": 6,
188
188
  }
189
189
 
190
- def __init__(self, cstruct: cstruct, expression: str):
191
- self.cstruct = cstruct
190
+ def __init__(self, expression: str):
192
191
  self.expression = expression
193
192
  self.tokens = ExpressionTokenizer(expression).tokenize()
194
193
  self.stack = []
@@ -222,7 +221,7 @@ class Expression:
222
221
  def is_number(self, token: str) -> bool:
223
222
  return token.isnumeric() or (len(token) > 2 and token[0] == "0" and token[1] in ("x", "X", "b", "B", "o", "O"))
224
223
 
225
- def evaluate(self, context: dict[str, int] | None = None) -> int:
224
+ def evaluate(self, cs: cstruct, context: dict[str, int] | None = None) -> int:
226
225
  """Evaluates an expression using a Shunting-Yard implementation."""
227
226
 
228
227
  self.stack = []
@@ -249,14 +248,14 @@ class Expression:
249
248
  self.queue.append(int(current_token, 0))
250
249
  elif current_token in context:
251
250
  self.queue.append(int(context[current_token]))
252
- elif current_token in self.cstruct.consts:
253
- self.queue.append(int(self.cstruct.consts[current_token]))
251
+ elif current_token in cs.consts:
252
+ self.queue.append(int(cs.consts[current_token]))
254
253
  elif current_token in self.unary_operators:
255
254
  self.stack.append(current_token)
256
255
  elif current_token == "sizeof":
257
256
  if len(tmp_expression) < i + 3 or (tmp_expression[i + 1] != "(" or tmp_expression[i + 3] != ")"):
258
257
  raise ExpressionParserError("Invalid sizeof operation")
259
- self.queue.append(len(self.cstruct.resolve(tmp_expression[i + 2])))
258
+ self.queue.append(len(cs.resolve(tmp_expression[i + 2])))
260
259
  i += 3
261
260
  elif current_token in operators:
262
261
  while (
@@ -92,7 +92,7 @@ class TokenParser(Parser):
92
92
 
93
93
  if isinstance(value, str):
94
94
  try:
95
- value = Expression(self.cstruct, value).evaluate()
95
+ value = Expression(value).evaluate(self.cstruct)
96
96
  except (ExpressionParserError, ExpressionTokenizerError):
97
97
  pass
98
98
 
@@ -120,7 +120,7 @@ class TokenParser(Parser):
120
120
  if not key:
121
121
  continue
122
122
 
123
- val = nextval if not val else Expression(self.cstruct, val).evaluate(values)
123
+ val = nextval if not val else Expression(val).evaluate(self.cstruct, values)
124
124
 
125
125
  if enumtype == "flag":
126
126
  high_bit = val.bit_length() - 1
@@ -293,9 +293,9 @@ class TokenParser(Parser):
293
293
  if count == "":
294
294
  count = None
295
295
  else:
296
- count = Expression(self.cstruct, count)
296
+ count = Expression(count)
297
297
  try:
298
- count = count.evaluate()
298
+ count = count.evaluate(self.cstruct)
299
299
  except Exception:
300
300
  pass
301
301
 
@@ -434,7 +434,7 @@ class CStyleParser(Parser):
434
434
  if not key:
435
435
  continue
436
436
 
437
- val = nextval if not val else Expression(self.cstruct, val).evaluate()
437
+ val = nextval if not val else Expression(val).evaluate(self.cstruct)
438
438
 
439
439
  if enumtype == "flag":
440
440
  high_bit = val.bit_length() - 1
@@ -515,9 +515,9 @@ class CStyleParser(Parser):
515
515
  if d["count"] == "":
516
516
  count = None
517
517
  else:
518
- count = Expression(self.cstruct, d["count"])
518
+ count = Expression(d["count"])
519
519
  try:
520
- count = count.evaluate()
520
+ count = count.evaluate(self.cstruct)
521
521
  except Exception:
522
522
  pass
523
523
 
@@ -262,7 +262,7 @@ class BaseArray(BaseType):
262
262
  num = EOF
263
263
  elif isinstance(cls.num_entries, Expression):
264
264
  try:
265
- num = max(0, cls.num_entries.evaluate(context))
265
+ num = max(0, cls.num_entries.evaluate(cls.cs, context))
266
266
  except Exception:
267
267
  if cls.num_entries.expression != "EOF":
268
268
  raise
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import io
4
+ from collections import ChainMap
5
+ from collections.abc import MutableMapping
4
6
  from contextlib import contextmanager
5
7
  from enum import Enum
6
8
  from functools import lru_cache
@@ -21,7 +23,7 @@ from dissect.cstruct.types.enum import EnumMetaType
21
23
  from dissect.cstruct.types.pointer import Pointer
22
24
 
23
25
  if TYPE_CHECKING:
24
- from collections.abc import Iterator
26
+ from collections.abc import Iterator, Mapping
25
27
  from types import FunctionType
26
28
 
27
29
  from typing_extensions import Self
@@ -60,6 +62,7 @@ class StructureMetaType(MetaType):
60
62
  __anonymous__: bool
61
63
  __updating__ = False
62
64
  __compiled__ = False
65
+ __static_sizes__: dict[str, int] # Cache of static sizes by field name
63
66
 
64
67
  def __new__(metacls, name: str, bases: tuple[type, ...], classdict: dict[str, Any]) -> Self: # type: ignore
65
68
  if (fields := classdict.pop("fields", None)) is not None:
@@ -79,8 +82,7 @@ class StructureMetaType(MetaType):
79
82
  return type.__call__(cls, *args, **kwargs)
80
83
  if not args and not kwargs:
81
84
  obj = type.__call__(cls)
82
- object.__setattr__(obj, "_values", {})
83
- object.__setattr__(obj, "_sizes", {})
85
+ object.__setattr__(obj, "__dynamic_sizes__", {})
84
86
  return obj
85
87
 
86
88
  return super().__call__(*args, **kwargs)
@@ -93,10 +95,14 @@ class StructureMetaType(MetaType):
93
95
  lookup = {}
94
96
  raw_lookup = {}
95
97
  field_names = []
98
+ static_sizes = {}
96
99
  for field in fields:
97
100
  if field._name in lookup and field._name != "_":
98
101
  raise ValueError(f"Duplicate field name: {field._name}")
99
102
 
103
+ if not field.type.dynamic:
104
+ static_sizes[field._name] = field.type.size
105
+
100
106
  if isinstance(field.type, StructureMetaType) and field.name is None:
101
107
  for anon_field in field.type.fields.values():
102
108
  attr = f"{field._name}.{anon_field.name}"
@@ -112,6 +118,7 @@ class StructureMetaType(MetaType):
112
118
  classdict["fields"] = lookup
113
119
  classdict["lookup"] = raw_lookup
114
120
  classdict["__fields__"] = fields
121
+ classdict["__static_sizes__"] = static_sizes
115
122
  classdict["__bool__"] = _generate__bool__(field_names)
116
123
 
117
124
  if issubclass(cls, UnionMetaType) or isinstance(cls, UnionMetaType):
@@ -271,8 +278,9 @@ class StructureMetaType(MetaType):
271
278
 
272
279
  value = field.type._read(stream, result)
273
280
 
274
- sizes[field._name] = stream.tell() - offset
275
281
  result[field._name] = value
282
+ if field.type.dynamic:
283
+ sizes[field._name] = stream.tell() - offset
276
284
 
277
285
  if cls.__align__:
278
286
  # Align the stream
@@ -281,8 +289,7 @@ class StructureMetaType(MetaType):
281
289
  # Using type.__call__ directly calls the __init__ method of the class
282
290
  # This is faster than calling cls() and bypasses the metaclass __call__ method
283
291
  obj = type.__call__(cls, **result)
284
- obj._sizes = sizes
285
- obj._values = result
292
+ obj.__dynamic_sizes__ = sizes
286
293
  return obj
287
294
 
288
295
  def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> list[Self]: # type: ignore
@@ -375,10 +382,13 @@ class StructureMetaType(MetaType):
375
382
 
376
383
 
377
384
  class Structure(BaseType, metaclass=StructureMetaType):
378
- """Base class for cstruct structure type classes."""
385
+ """Base class for cstruct structure type classes.
386
+
387
+ Note that setting attributes which do not correspond to a field in the structure results in undefined behavior.
388
+ For performance reasons, the structure does not check if the field exists when writing to an attribute.
389
+ """
379
390
 
380
- _values: dict[str, Any]
381
- _sizes: dict[str, int]
391
+ __dynamic_sizes__: dict[str, int]
382
392
 
383
393
  def __len__(self) -> int:
384
394
  return len(self.dumps())
@@ -401,6 +411,47 @@ class Structure(BaseType, metaclass=StructureMetaType):
401
411
 
402
412
  return f"<{self.__class__.__name__} {' '.join(values)}>"
403
413
 
414
+ @property
415
+ def __values__(self) -> MutableMapping[str, Any]:
416
+ return StructureValuesProxy(self)
417
+
418
+ @property
419
+ def __sizes__(self) -> Mapping[str, int | None]:
420
+ return ChainMap(self.__class__.__static_sizes__, self.__dynamic_sizes__)
421
+
422
+
423
+ class StructureValuesProxy(MutableMapping):
424
+ """A proxy for the values of fields of a Structure."""
425
+
426
+ def __init__(self, struct: Structure):
427
+ self._struct: Structure = struct
428
+
429
+ def __getitem__(self, key: str) -> Any:
430
+ if key in self:
431
+ return getattr(self._struct, key)
432
+ raise KeyError(key)
433
+
434
+ def __setitem__(self, key: str, value: Any) -> None:
435
+ if key in self:
436
+ return setattr(self._struct, key, value)
437
+ raise KeyError(key)
438
+
439
+ def __contains__(self, key: str) -> bool:
440
+ return key in self._struct.__class__.fields
441
+
442
+ def __iter__(self) -> Iterator[str]:
443
+ return iter(self._struct.__class__.fields)
444
+
445
+ def __len__(self) -> int:
446
+ return len(self._struct.__class__.fields)
447
+
448
+ def __repr__(self) -> str:
449
+ return repr(dict(self))
450
+
451
+ def __delitem__(self, _: str):
452
+ # Is abstract in base, but deleting is not supported.
453
+ raise NotImplementedError("Cannot delete fields from a Structure")
454
+
404
455
 
405
456
  class UnionMetaType(StructureMetaType):
406
457
  """Base metaclass for cstruct union type classes."""
@@ -473,8 +524,9 @@ class UnionMetaType(StructureMetaType):
473
524
  buf.seek(offset + start)
474
525
  value = field_type._read(buf, result)
475
526
 
476
- sizes[field._name] = buf.tell() - start
477
527
  result[field._name] = value
528
+ if field.type.dynamic:
529
+ sizes[field._name] = buf.tell() - start
478
530
 
479
531
  return result, sizes
480
532
 
@@ -496,8 +548,7 @@ class UnionMetaType(StructureMetaType):
496
548
  # It also makes it easier to differentiate between user-initialization of the class
497
549
  # and initialization from a stream read
498
550
  obj: Union = type.__call__(cls, **result)
499
- object.__setattr__(obj, "_values", result)
500
- object.__setattr__(obj, "_sizes", sizes)
551
+ object.__setattr__(obj, "__dynamic_sizes__", sizes)
501
552
  object.__setattr__(obj, "_buf", buf)
502
553
 
503
554
  if cls.size is not None:
@@ -579,8 +630,7 @@ class Union(Structure, metaclass=UnionMetaType):
579
630
  def _update(self) -> None:
580
631
  result, sizes = self.__class__._read_fields(io.BytesIO(self._buf))
581
632
  self.__dict__.update(result)
582
- object.__setattr__(self, "_values", result)
583
- object.__setattr__(self, "_sizes", sizes)
633
+ object.__setattr__(self, "__dynamic_sizes__", sizes)
584
634
 
585
635
  def _proxify(self) -> None:
586
636
  def _proxy_structure(value: Structure) -> None:
@@ -157,11 +157,6 @@ def _dumpstruct(
157
157
  if getattr(field.type, "anonymous", False):
158
158
  continue
159
159
 
160
- if color:
161
- foreground, background = colors[ci % len(colors)]
162
- palette.append((structure._sizes[field._name], background))
163
- ci += 1
164
-
165
160
  value = getattr(structure, field._name)
166
161
  if isinstance(value, (str, Pointer, Enum)):
167
162
  value = repr(value)
@@ -173,6 +168,10 @@ def _dumpstruct(
173
168
  value = value.replace("\n", f"\n{' ' * (len(field._name) + 4)}")
174
169
 
175
170
  if color:
171
+ foreground, background = colors[ci % len(colors)]
172
+ size = structure.__sizes__[field._name]
173
+ palette.append((size, background))
174
+ ci += 1
176
175
  out.append(f"- {foreground}{field._name}{COLOR_NORMAL}: {value}")
177
176
  else:
178
177
  out.append(f"- {field._name}: {value}")
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.cstruct
3
- Version: 4.5.dev5
3
+ Version: 4.6.dev1
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
7
7
  Project-URL: homepage, https://dissect.tools
8
8
  Project-URL: documentation, https://docs.dissect.tools/en/latest/projects/dissect.cstruct
9
9
  Project-URL: repository, https://github.com/fox-it/dissect.cstruct
10
+ Project-URL: changelog, https://github.com/fox-it/dissect.cstruct/blob/main/CHANGELOG.md
10
11
  Classifier: Development Status :: 5 - Production/Stable
11
12
  Classifier: Environment :: Console
12
13
  Classifier: Intended Audience :: Developers
@@ -1,5 +1,6 @@
1
1
  .git-blame-ignore-revs
2
2
  .gitattributes
3
+ CHANGELOG.md
3
4
  COPYRIGHT
4
5
  LICENSE
5
6
  MANIFEST.in
@@ -30,6 +30,7 @@ dynamic = ["version"]
30
30
  homepage = "https://dissect.tools"
31
31
  documentation = "https://docs.dissect.tools/en/latest/projects/dissect.cstruct"
32
32
  repository = "https://github.com/fox-it/dissect.cstruct"
33
+ changelog = "https://github.com/fox-it/dissect.cstruct/blob/main/CHANGELOG.md"
33
34
 
34
35
  [project.optional-dependencies]
35
36
  dev = [
@@ -45,7 +45,7 @@ def test_align_struct(cs: cstruct, compiled: bool) -> None:
45
45
  obj = cs.test(fh)
46
46
  assert fh.tell() == 32
47
47
 
48
- for name, value in obj._values.items():
48
+ for name, value in obj.__values__.items():
49
49
  assert cs.test.fields[name].offset == value
50
50
 
51
51
  assert obj.dumps() == buf
@@ -8,6 +8,7 @@ from unittest.mock import Mock
8
8
  import pytest
9
9
 
10
10
  from dissect.cstruct import compiler
11
+ from dissect.cstruct.expression import Expression
11
12
  from dissect.cstruct.types.structure import Field
12
13
 
13
14
  if TYPE_CHECKING:
@@ -111,16 +112,12 @@ def test_generate_packed_read(cs: cstruct) -> None:
111
112
  data = _struct(cls.cs.endian, "BhIq").unpack(buf)
112
113
 
113
114
  r["a"] = type.__call__(_0, data[0])
114
- s["a"] = 1
115
115
 
116
116
  r["b"] = type.__call__(_1, data[1])
117
- s["b"] = 2
118
117
 
119
118
  r["c"] = type.__call__(_2, data[2])
120
- s["c"] = 4
121
119
 
122
120
  r["d"] = type.__call__(_3, data[3])
123
- s["d"] = 8
124
121
  """
125
122
 
126
123
  assert code == dedent(expected)
@@ -143,22 +140,18 @@ def test_generate_packed_read_array(cs: cstruct) -> None:
143
140
  _t = _0
144
141
  _et = _t.type
145
142
  r["a"] = type.__call__(_t, [type.__call__(_et, e) for e in data[0:2]])
146
- s["a"] = 2
147
143
 
148
144
  _t = _1
149
145
  _et = _t.type
150
146
  r["b"] = type.__call__(_t, [type.__call__(_et, e) for e in data[2:5]])
151
- s["b"] = 6
152
147
 
153
148
  _t = _2
154
149
  _et = _t.type
155
150
  r["c"] = type.__call__(_t, [type.__call__(_et, e) for e in data[5:9]])
156
- s["c"] = 16
157
151
 
158
152
  _t = _3
159
153
  _et = _t.type
160
154
  r["d"] = type.__call__(_t, [type.__call__(_et, e) for e in data[9:14]])
161
- s["d"] = 40
162
155
  """
163
156
 
164
157
  assert code == dedent(expected)
@@ -181,25 +174,19 @@ def test_generate_packed_read_byte_types(cs: cstruct) -> None:
181
174
  data = _struct(cls.cs.endian, "18x").unpack(buf)
182
175
 
183
176
  r["a"] = type.__call__(_0, buf[0:1])
184
- s["a"] = 1
185
177
 
186
178
  r["b"] = type.__call__(_1, buf[1:3])
187
- s["b"] = 2
188
179
 
189
180
  r["c"] = _2(buf[3:5])
190
- s["c"] = 2
191
181
 
192
182
  r["d"] = _3(buf[5:9])
193
- s["d"] = 4
194
183
 
195
184
  r["e"] = _4(buf[9:12])
196
- s["e"] = 3
197
185
 
198
186
  _t = _5
199
187
  _et = _t.type
200
188
  _b = buf[12:18]
201
189
  r["f"] = type.__call__(_t, [_et(_b[i:i + 3]) for i in range(0, 6, 3)])
202
- s["f"] = 6
203
190
  """
204
191
 
205
192
  assert code == dedent(expected)
@@ -223,16 +210,13 @@ def test_generate_packed_read_composite_types(cs: cstruct, TestEnum: type[Enum])
223
210
  data = _struct(cls.cs.endian, "BQ2B").unpack(buf)
224
211
 
225
212
  r["a"] = type.__call__(_0, data[0])
226
- s["a"] = 1
227
213
 
228
214
  _pt = _1
229
215
  r["b"] = _pt.__new__(_pt, data[1], stream, r)
230
- s["b"] = 8
231
216
 
232
217
  _t = _2
233
218
  _et = _t.type
234
219
  r["c"] = type.__call__(_t, [type.__call__(_et, e) for e in data[2:4]])
235
- s["c"] = 2
236
220
  """
237
221
 
238
222
  assert code == dedent(expected)
@@ -251,10 +235,8 @@ def test_generate_packed_read_offsets(cs: cstruct) -> None:
251
235
  data = _struct(cls.cs.endian, "B7xB").unpack(buf)
252
236
 
253
237
  r["a"] = type.__call__(_0, data[0])
254
- s["a"] = 1
255
238
 
256
239
  r["b"] = type.__call__(_1, data[1])
257
- s["b"] = 1
258
240
  """
259
241
 
260
242
  assert code == dedent(expected)
@@ -335,7 +317,7 @@ def test_generate_fields_dynamic_after_bitfield(cs: cstruct, TestEnum: Enum, oth
335
317
  Field("size", cs.uint16, offset=0),
336
318
  Field("a", TestEnum, 4, offset=2),
337
319
  Field("b", _type, 4),
338
- Field("c", cs.char["size"], offset=3),
320
+ Field("c", cs.char[Expression("size")], offset=3),
339
321
  ]
340
322
 
341
323
  output = "\n".join(compiler._ReadSourceGenerator(cs, fields)._generate_fields())
@@ -346,7 +328,6 @@ def test_generate_fields_dynamic_after_bitfield(cs: cstruct, TestEnum: Enum, oth
346
328
  data = _struct(cls.cs.endian, "H").unpack(buf)
347
329
 
348
330
  r["size"] = type.__call__(_0, data[0])
349
- s["size"] = 2
350
331
 
351
332
 
352
333
  _t = _1
@@ -375,7 +356,7 @@ def test_generate_fields_dynamic_before_bitfield(cs: cstruct, TestEnum: Enum, ot
375
356
  Field("size", cs.uint16, offset=0),
376
357
  Field("a", _type, 4, offset=2),
377
358
  Field("b", TestEnum, 4),
378
- Field("c", cs.char["size"], offset=3),
359
+ Field("c", cs.char[Expression("size")], offset=3),
379
360
  ]
380
361
 
381
362
  output = "\n".join(compiler._ReadSourceGenerator(cs, fields)._generate_fields())
@@ -386,7 +367,6 @@ def test_generate_fields_dynamic_before_bitfield(cs: cstruct, TestEnum: Enum, ot
386
367
  data = _struct(cls.cs.endian, "H").unpack(buf)
387
368
 
388
369
  r["size"] = type.__call__(_0, data[0])
389
- s["size"] = 2
390
370
 
391
371
 
392
372
  _t = _1
@@ -88,8 +88,8 @@ def id_fn(val: Any) -> str | None:
88
88
 
89
89
  @pytest.mark.parametrize(("expression", "answer"), testdata, ids=id_fn)
90
90
  def test_expression(expression: str, answer: int) -> None:
91
- parser = Expression(Consts(), expression)
92
- assert parser.evaluate() == answer
91
+ parser = Expression(expression)
92
+ assert parser.evaluate(Consts()) == answer
93
93
 
94
94
 
95
95
  @pytest.mark.parametrize(
@@ -110,7 +110,7 @@ def test_expression(expression: str, answer: int) -> None:
110
110
  )
111
111
  def test_expression_failure(expression: str, exception: type, message: str) -> None:
112
112
  with pytest.raises(exception, match=message):
113
- Expression(Consts(), expression).evaluate()
113
+ Expression(expression).evaluate(Consts())
114
114
 
115
115
 
116
116
  def test_sizeof(cs: cstruct) -> None:
@@ -265,7 +265,8 @@ def test_structure_definition_simple(cs: cstruct, compiled: bool) -> None:
265
265
  with pytest.raises(AttributeError):
266
266
  obj.nope # noqa: B018
267
267
 
268
- assert obj._sizes["magic"] == 4
268
+ assert obj.__dynamic_sizes__ == { "string": 7, "wstring": 10 }
269
+ assert obj.__sizes__ == { "magic": 4, "wmagic": 8, "a": 1, "b": 2, "c": 4, "string": 7, "wstring": 10 }
269
270
  assert len(obj) == len(buf)
270
271
  assert obj.dumps() == buf
271
272
 
@@ -275,6 +276,39 @@ def test_structure_definition_simple(cs: cstruct, compiled: bool) -> None:
275
276
  obj.write(fh)
276
277
  assert fh.getvalue() == buf
277
278
 
279
+ def test_structure_values_dict(cs: cstruct, compiled: bool) -> None:
280
+ cdef = """
281
+ struct test {
282
+ char magic[4];
283
+ wchar wmagic[4];
284
+ uint8 a;
285
+ uint16 b;
286
+ uint32 c;
287
+ char string[];
288
+ wchar wstring[];
289
+ };
290
+ """
291
+ cs.load(cdef, compiled=compiled)
292
+ buf = b"testt\x00e\x00s\x00t\x00\x01\x02\x03\x04\x05\x06\x07lalala\x00t\x00e\x00s\x00t\x00\x00\x00"
293
+ obj = cs.test(buf)
294
+
295
+ # Test reading all values
296
+ values = obj.__values__
297
+ assert values["magic"] == b"test"
298
+ assert values["wmagic"] == "test"
299
+ assert values["a"] == 0x01
300
+ assert values["b"] == 0x0302
301
+ assert values["c"] == 0x07060504
302
+ assert values["string"] == b"lalala"
303
+ assert values["wstring"] == "test"
304
+
305
+ # Test writing a single field through the proxy
306
+ values["a"] = 0xFF
307
+ assert obj.a == 0xFF
308
+
309
+ # Test dictionary methods
310
+ assert values.keys() == {"magic", "wmagic", "a", "b", "c", "string", "wstring"}
311
+
278
312
 
279
313
  def test_structure_definition_simple_be(cs: cstruct, compiled: bool) -> None:
280
314
  cdef = """