dissect.cstruct 5.0.dev0__tar.gz → 5.0.dev6__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 (86) hide show
  1. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/PKG-INFO +1 -1
  2. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/__init__.py +2 -2
  3. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/bitbuffer.py +3 -3
  4. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/compiler.py +7 -7
  5. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/cstruct.py +192 -65
  6. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/expression.py +8 -9
  7. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/lexer.py +17 -7
  8. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/parser.py +31 -13
  9. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/tools/stubgen.py +18 -22
  10. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/types/base.py +74 -31
  11. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/types/char.py +12 -8
  12. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/types/enum.py +33 -13
  13. dissect_cstruct-5.0.dev6/dissect/cstruct/types/int.py +42 -0
  14. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/types/leb128.py +6 -4
  15. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/types/packed.py +19 -10
  16. dissect_cstruct-5.0.dev6/dissect/cstruct/types/pointer.py +122 -0
  17. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/types/structure.py +128 -28
  18. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/types/void.py +6 -4
  19. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/types/wchar.py +19 -14
  20. dissect_cstruct-5.0.dev0/dissect/cstruct/utils.py → dissect_cstruct-5.0.dev6/dissect/cstruct/util.py +36 -25
  21. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect.cstruct.egg-info/PKG-INFO +1 -1
  22. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect.cstruct.egg-info/SOURCES.txt +2 -2
  23. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect.cstruct.egg-info/scm_file_list.json +53 -53
  24. dissect_cstruct-5.0.dev6/dissect.cstruct.egg-info/scm_version.json +8 -0
  25. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/pyproject.toml +1 -0
  26. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_annotations.py +4 -1
  27. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_basic.py +169 -18
  28. dissect_cstruct-5.0.dev6/tests/test_bitbuffer.py +55 -0
  29. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_compiler.py +13 -13
  30. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_expression.py +1 -1
  31. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_lexer.py +19 -1
  32. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_parser.py +59 -8
  33. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_tools_stubgen.py +58 -0
  34. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_types_base.py +7 -5
  35. dissect_cstruct-5.0.dev6/tests/test_types_custom.py +143 -0
  36. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_types_enum.py +14 -0
  37. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_types_flag.py +14 -0
  38. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_types_pointer.py +20 -2
  39. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_types_structure.py +265 -9
  40. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_types_union.py +34 -0
  41. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_utils.py +77 -77
  42. dissect_cstruct-5.0.dev0/dissect/cstruct/types/int.py +0 -40
  43. dissect_cstruct-5.0.dev0/dissect/cstruct/types/pointer.py +0 -104
  44. dissect_cstruct-5.0.dev0/dissect.cstruct.egg-info/scm_version.json +0 -8
  45. dissect_cstruct-5.0.dev0/tests/test_bitbuffer.py +0 -40
  46. dissect_cstruct-5.0.dev0/tests/test_types_custom.py +0 -93
  47. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/.git-blame-ignore-revs +0 -0
  48. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/.gitattributes +0 -0
  49. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/CHANGELOG.md +0 -0
  50. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/COPYRIGHT +0 -0
  51. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/LICENSE +0 -0
  52. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/MANIFEST.in +0 -0
  53. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/README.md +0 -0
  54. /dissect_cstruct-5.0.dev0/dissect/cstruct/exceptions.py → /dissect_cstruct-5.0.dev6/dissect/cstruct/exception.py +0 -0
  55. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/tools/__init__.py +0 -0
  56. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/types/__init__.py +0 -0
  57. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect/cstruct/types/flag.py +0 -0
  58. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect.cstruct.egg-info/dependency_links.txt +0 -0
  59. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect.cstruct.egg-info/entry_points.txt +0 -0
  60. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect.cstruct.egg-info/requires.txt +0 -0
  61. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/dissect.cstruct.egg-info/top_level.txt +0 -0
  62. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/examples/disk.py +0 -0
  63. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/examples/mirai.py +0 -0
  64. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/examples/pe.py +0 -0
  65. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/examples/protobuf.py +0 -0
  66. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/examples/secdesc.py +0 -0
  67. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/setup.cfg +0 -0
  68. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/__init__.py +0 -0
  69. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/_data/testdef.txt +0 -0
  70. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/_docs/Makefile +0 -0
  71. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/_docs/__init__.py +0 -0
  72. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/_docs/conf.py +0 -0
  73. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/_docs/index.rst +0 -0
  74. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/conftest.py +0 -0
  75. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_align.py +0 -0
  76. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_benchmark.py +0 -0
  77. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_bitfield.py +0 -0
  78. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_ctypes.py +0 -0
  79. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_types_char.py +0 -0
  80. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_types_int.py +0 -0
  81. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_types_leb128.py +0 -0
  82. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_types_packed.py +0 -0
  83. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_types_void.py +0 -0
  84. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/test_types_wchar.py +0 -0
  85. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tests/utils.py +0 -0
  86. {dissect_cstruct-5.0.dev0 → dissect_cstruct-5.0.dev6}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.cstruct
3
- Version: 5.0.dev0
3
+ Version: 5.0.dev6
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
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from dissect.cstruct.bitbuffer import BitBuffer
4
4
  from dissect.cstruct.cstruct import cstruct, ctypes, ctypes_type
5
- from dissect.cstruct.exceptions import (
5
+ from dissect.cstruct.exception import (
6
6
  Error,
7
7
  NullPointerDereference,
8
8
  ParserError,
@@ -28,7 +28,7 @@ from dissect.cstruct.types import (
28
28
  Wchar,
29
29
  WcharArray,
30
30
  )
31
- from dissect.cstruct.utils import (
31
+ from dissect.cstruct.util import (
32
32
  dumpstruct,
33
33
  hexdump,
34
34
  p8,
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
9
9
  class BitBuffer:
10
10
  """Implements a bit buffer that can read and write bit fields."""
11
11
 
12
- def __init__(self, stream: BinaryIO, endian: str):
12
+ def __init__(self, stream: BinaryIO, *, endian: str):
13
13
  self.stream = stream
14
14
  self.endian = endian
15
15
 
@@ -24,7 +24,7 @@ class BitBuffer:
24
24
 
25
25
  self._type = field_type
26
26
  self._remaining = field_type.size * 8
27
- self._buffer = field_type._read(self.stream)
27
+ self._buffer = field_type._read(self.stream, endian=self.endian)
28
28
 
29
29
  if isinstance(self._buffer, bytes):
30
30
  if self.endian == "<":
@@ -71,7 +71,7 @@ class BitBuffer:
71
71
 
72
72
  def flush(self) -> None:
73
73
  if self._type is not None:
74
- self._type._write(self.stream, self._buffer)
74
+ self._type._write(self.stream, self._buffer, endian=self.endian)
75
75
  self._type = None
76
76
  self._remaining = 0
77
77
  self._buffer = 0
@@ -119,7 +119,7 @@ class _ReadSourceGenerator:
119
119
  """
120
120
 
121
121
  if any(field.bits for field in self.fields):
122
- preamble += "bit_reader = BitBuffer(stream, cls.cs.endian)\n"
122
+ preamble += "bit_reader = BitBuffer(stream, endian=endian)\n"
123
123
 
124
124
  read_code = "\n".join(self._generate_fields())
125
125
 
@@ -132,7 +132,7 @@ class _ReadSourceGenerator:
132
132
 
133
133
  code = indent(dedent(preamble).lstrip() + read_code + dedent(outro), " ")
134
134
 
135
- return f"def _read(cls, stream, context=None):\n{code}"
135
+ return f"def _read(cls, stream, *, context=None, endian):\n{code}"
136
136
 
137
137
  def _generate_fields(self) -> Iterator[str]:
138
138
  current_offset = 0
@@ -229,7 +229,7 @@ class _ReadSourceGenerator:
229
229
  def _generate_structure(self, field: Field) -> Iterator[str]:
230
230
  template = f"""
231
231
  {"_s = stream.tell()" if field.type.dynamic else ""}
232
- r["{field._name}"] = {self._map_field(field)}._read(stream, context=r)
232
+ r["{field._name}"] = {self._map_field(field)}._read(stream, context=r, endian=endian)
233
233
  {f's["{field._name}"] = stream.tell() - _s' if field.type.dynamic else ""}
234
234
  """
235
235
 
@@ -238,7 +238,7 @@ class _ReadSourceGenerator:
238
238
  def _generate_array(self, field: Field) -> Iterator[str]:
239
239
  template = f"""
240
240
  {"_s = stream.tell()" if field.type.dynamic else ""}
241
- r["{field._name}"] = {self._map_field(field)}._read(stream, context=r)
241
+ r["{field._name}"] = {self._map_field(field)}._read(stream, context=r, endian=endian)
242
242
  {f's["{field._name}"] = stream.tell() - _s' if field.type.dynamic else ""}
243
243
  """
244
244
 
@@ -311,7 +311,7 @@ class _ReadSourceGenerator:
311
311
  item_parser = parser_template.format(type="_et", getter=f"_b[i:i + {field_type.type.size}]")
312
312
  list_comp = f"[{item_parser} for i in range(0, {count}, {field_type.type.size})]"
313
313
  elif issubclass(field_type.type, Pointer):
314
- item_parser = "_et.__new__(_et, e, stream, r)"
314
+ item_parser = "_et.__new__(_et, e, stream, context=r, endian=endian)"
315
315
  list_comp = f"[{item_parser} for e in {getter}]"
316
316
  else:
317
317
  item_parser = parser_template.format(type="_et", getter="e")
@@ -322,7 +322,7 @@ class _ReadSourceGenerator:
322
322
  parser = f"type.__call__({self._map_field(field)}, {getter})"
323
323
  elif issubclass(field_type, Pointer):
324
324
  reads.append(f"_pt = {self._map_field(field)}")
325
- parser = f"_pt.__new__(_pt, {getter}, stream, r)"
325
+ parser = f"_pt.__new__(_pt, {getter}, stream, context=r, endian=endian)"
326
326
  else:
327
327
  parser = parser_template.format(type=self._map_field(field), getter=getter)
328
328
 
@@ -335,7 +335,7 @@ class _ReadSourceGenerator:
335
335
  if fmt == "x" or (len(fmt) == 2 and fmt[1] == "x"):
336
336
  unpack = ""
337
337
  else:
338
- unpack = f'data = _struct(cls.cs.endian, "{fmt}").unpack(buf)\n'
338
+ unpack = f'data = _struct(endian, "{fmt}").unpack(buf)\n'
339
339
 
340
340
  template = f"""
341
341
  buf = stream.read({size})
@@ -2,13 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import copy
4
4
  import ctypes as _ctypes
5
+ import inspect
5
6
  import struct
6
7
  import sys
7
8
  import types
9
+ import warnings
10
+ from contextlib import contextmanager
8
11
  from pathlib import Path
9
- from typing import TYPE_CHECKING, Any, BinaryIO, TypeVar, cast
12
+ from typing import TYPE_CHECKING, Any, BinaryIO, Literal, TypeVar, cast
10
13
 
11
- from dissect.cstruct.exceptions import ResolveError
14
+ from dissect.cstruct.exception import ResolveError
12
15
  from dissect.cstruct.expression import Expression
13
16
  from dissect.cstruct.parser import CStyleParser
14
17
  from dissect.cstruct.types import (
@@ -26,10 +29,10 @@ from dissect.cstruct.types import (
26
29
  Void,
27
30
  Wchar,
28
31
  )
29
- from dissect.cstruct.types.base import MetaType
32
+ from dissect.cstruct.types.base import normalize_endianness
30
33
 
31
34
  if TYPE_CHECKING:
32
- from collections.abc import Iterable
35
+ from collections.abc import Iterable, Iterator
33
36
  from typing import TypeAlias
34
37
 
35
38
  from dissect.cstruct.types import (
@@ -40,25 +43,30 @@ if TYPE_CHECKING:
40
43
 
41
44
  T = TypeVar("T", bound=BaseType)
42
45
 
46
+ AllowedEndianness: TypeAlias = Literal["little", "big", "network", "<", ">", "!", "@", "="]
47
+ Endianness: TypeAlias = Literal["<", ">", "!", "@", "="]
48
+
43
49
 
44
50
  class cstruct:
45
51
  """Main class of cstruct. All types are registered in here.
46
52
 
47
53
  Args:
48
- endian: The endianness to use when parsing.
54
+ endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
49
55
  pointer: The pointer type to use for pointers.
50
56
  """
51
57
 
52
58
  DEF_CSTYLE = 1
53
59
  DEF_LEGACY = 2
54
60
 
55
- def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = None):
56
- self.endian = endian
61
+ def __init__(self, load: str = "", *, endian: AllowedEndianness = "<", pointer: str | None = None):
62
+ self.endian = normalize_endianness(endian)
63
+
64
+ self.consts: dict[str, int | str | bytes] = {}
65
+ self.types: dict[str, type[BaseType]] = {}
66
+ self.includes: list[str] = []
57
67
 
58
- self.consts = {}
59
- self.includes = []
60
68
  # fmt: off
61
- self.typedefs = {
69
+ initial_types = {
62
70
  # Internal types
63
71
  "int8": self._make_packed_type("int8", "b", int),
64
72
  "uint8": self._make_packed_type("uint8", "B", int),
@@ -102,6 +110,22 @@ class cstruct:
102
110
  "signed long long": "int64",
103
111
  "unsigned long long": "uint64",
104
112
 
113
+ # Other convenience types
114
+ "u8": "uint8",
115
+ "u16": "uint16",
116
+ "u32": "uint32",
117
+ "u64": "uint64",
118
+ "u128": "uint128",
119
+ "__u8": "uint8",
120
+ "__u16": "uint16",
121
+ "__u32": "uint32",
122
+ "__u64": "uint64",
123
+ "__u128": "uint128",
124
+ "uchar": "uint8",
125
+ "ushort": "uint16",
126
+ "uint": "uint32",
127
+ "ulong": "uint32",
128
+
105
129
  # Windows types
106
130
  "BYTE": "uint8",
107
131
  "CHAR": "char",
@@ -169,24 +193,12 @@ class cstruct:
169
193
  "_DWORD": "uint32",
170
194
  "_QWORD": "uint64",
171
195
  "_OWORD": "uint128",
172
-
173
- # Other convenience types
174
- "u1": "uint8",
175
- "u2": "uint16",
176
- "u4": "uint32",
177
- "u8": "uint64",
178
- "u16": "uint128",
179
- "__u8": "uint8",
180
- "__u16": "uint16",
181
- "__u32": "uint32",
182
- "__u64": "uint64",
183
- "uchar": "uint8",
184
- "ushort": "uint16",
185
- "uint": "uint32",
186
- "ulong": "uint32",
187
196
  }
188
197
  # fmt: on
189
198
 
199
+ for name, type_ in initial_types.items():
200
+ self.add_type(name, type_)
201
+
190
202
  pointer = pointer or ("uint64" if sys.maxsize > 2**32 else "uint32")
191
203
  self.pointer: type[BaseType] = self.resolve(pointer)
192
204
  self._anonymous_count = 0
@@ -201,27 +213,25 @@ class cstruct:
201
213
  pass
202
214
 
203
215
  try:
204
- return self.resolve(self.typedefs[attr])
216
+ return self.types[attr]
205
217
  except KeyError:
206
218
  pass
207
219
 
208
- raise AttributeError(f"Invalid attribute: {attr}")
220
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute {attr!r}")
209
221
 
210
222
  def __copy__(self) -> cstruct:
211
223
  cs = cstruct(endian=self.endian, pointer=self.pointer.__name__)
212
224
  cs._anonymous_count = self._anonymous_count
213
- cs.consts = self.consts.copy()
214
225
  cs.includes = self.includes.copy()
215
226
 
216
- # Update typedefs to point to the new cstruct instance
217
- for name, type in self.typedefs.items():
218
- if name in cs.typedefs:
219
- continue
227
+ # Update types to point to the new cstruct instance
228
+ for name, type_ in self.types.items():
229
+ new_type = copy.copy(type_)
230
+ new_type.cs = cs
231
+ cs.add_type(name, new_type, replace=True)
220
232
 
221
- if isinstance(type, MetaType):
222
- new_type = copy.copy(type)
223
- new_type.cs = cs
224
- cs.typedefs[name] = new_type
233
+ for name, value in self.consts.items():
234
+ cs.add_const(name, value)
225
235
 
226
236
  return cs
227
237
 
@@ -230,22 +240,34 @@ class cstruct:
230
240
  self._anonymous_count += 1
231
241
  return name
232
242
 
243
+ def _add_attr(self, name: str, value: Any) -> None:
244
+ # Names that collide with the cstruct class (e.g. a struct named ``load``) are not set as attributes
245
+ # to avoid breaking the instance. They remain accessible through ``resolve`` and the type dicts.
246
+ if name in _RESERVED_NAMES:
247
+ return
248
+ setattr(self, name, value)
249
+
233
250
  def add_type(self, name: str, type_: type[BaseType] | str, replace: bool = False) -> None:
234
- """Add a type or type reference.
251
+ """Add a type or type alias.
235
252
 
236
253
  Only use this method when creating type aliases or adding already bound types.
254
+ All types will be resolved to their actual type objects prior to being added.
237
255
 
238
256
  Args:
239
257
  name: Name of the type to be added.
240
258
  type_: The type to be added. Can be a str reference to another type or a compatible type class.
259
+ If a str is given, it will be resolved to the actual type object.
260
+ replace: Whether to replace the type if it already exists.
241
261
 
242
262
  Raises:
243
263
  ValueError: If the type already exists.
244
264
  """
245
- if not replace and (name in self.typedefs and self.resolve(self.typedefs[name]) != self.resolve(type_)):
265
+ typeobj = self.resolve(type_)
266
+ if not replace and (existing := self.types.get(name)) is not None and existing is not typeobj:
246
267
  raise ValueError(f"Duplicate type: {name}")
247
268
 
248
- self.typedefs[name] = type_
269
+ self.types[name] = typeobj
270
+ self._add_attr(name, typeobj)
249
271
 
250
272
  addtype = add_type
251
273
 
@@ -264,8 +286,49 @@ class cstruct:
264
286
  alignment: The alignment of the type.
265
287
  **kwargs: Additional attributes to add to the type.
266
288
  """
289
+ # In cstruct 5.0 we changed the function signature of _read and _write
290
+ # Check if the function signature is compatible, and warn if not
291
+ for type_to_check in (type_, type_.ArrayType):
292
+ type_name = type_.__name__ + (f".{type_.ArrayType.__name__}" if type_to_check is type_.ArrayType else "")
293
+
294
+ for method in ("_read", "_read_array", "_read_0", "_write", "_write_array", "_write_0"):
295
+ if not hasattr(type_to_check, method):
296
+ continue
297
+
298
+ signature = inspect.signature(getattr(type_to_check, method))
299
+
300
+ # Only warn if the method doesn't accept an endian parameter
301
+ if "endian" not in signature.parameters:
302
+ warnings.warn(
303
+ f"Custom type {type_name} is missing the 'endian' keyword-only parameter in its {method} method. " # noqa: E501
304
+ "Please refer to the changelog of dissect.cstruct 5.0 for more information.",
305
+ stacklevel=2,
306
+ )
307
+
267
308
  self.add_type(name, self._make_type(name, (type_,), size, alignment=alignment, attrs=kwargs))
268
309
 
310
+ def add_const(self, name: str, value: Any) -> None:
311
+ """Add a constant value.
312
+
313
+ Args:
314
+ name: Name of the constant to be added.
315
+ value: The value of the constant.
316
+ """
317
+ self.consts[name] = value
318
+ self._add_attr(name, value)
319
+
320
+ def del_const(self, name: str) -> None:
321
+ """Delete a constant value.
322
+
323
+ Args:
324
+ name: Name of the constant to be deleted.
325
+
326
+ Raises:
327
+ KeyError: If the constant does not exist.
328
+ """
329
+ del self.consts[name]
330
+ self.__dict__.pop(name, None)
331
+
269
332
  def load(self, definition: str, deftype: int | None = None, **kwargs) -> cstruct:
270
333
  """Parse structures from the given definitions using the given definition type.
271
334
 
@@ -301,6 +364,42 @@ class cstruct:
301
364
  with Path(path).open() as fh:
302
365
  self.load(fh.read(), deftype, **kwargs)
303
366
 
367
+ def cdef(self) -> str:
368
+ """Render all constants, structure, union, enum and flag definitions back to their C-style definitions.
369
+
370
+ Note that the result is semantically equivalent to the original definitions, but not necessarily identical.
371
+
372
+ Returns:
373
+ The C-style definitions as a string.
374
+ """
375
+ empty = cstruct()
376
+
377
+ blocks = []
378
+
379
+ defines = []
380
+ for name, value in self.consts.items():
381
+ if name in empty.consts:
382
+ continue
383
+
384
+ defines.append(f"#define {name} {value!r}")
385
+
386
+ if defines:
387
+ blocks.append("\n".join(defines))
388
+
389
+ for name, typedef in self.types.items():
390
+ if name in empty.types or not isinstance(typedef, type):
391
+ continue
392
+
393
+ if not issubclass(typedef, (Structure, Enum, Flag)):
394
+ continue
395
+
396
+ if typedef.__name__ == name:
397
+ blocks.append(typedef.cdef())
398
+ else:
399
+ blocks.append(f"typedef {typedef.__name__} {name};")
400
+
401
+ return "\n\n".join(blocks)
402
+
304
403
  def read(self, name: str, stream: BinaryIO) -> Any:
305
404
  """Parse data using a given type.
306
405
 
@@ -328,20 +427,13 @@ class cstruct:
328
427
  Raises:
329
428
  ResolveError: If the type can't be resolved.
330
429
  """
331
- type_name = name
332
- if not isinstance(type_name, str):
333
- return type_name
430
+ if not isinstance(name, str):
431
+ return name
334
432
 
335
- for _ in range(10):
336
- if type_name not in self.typedefs:
337
- raise ResolveError(f"Unknown type {name}")
338
-
339
- type_name = self.typedefs[type_name]
340
-
341
- if not isinstance(type_name, str):
342
- return type_name
343
-
344
- raise ResolveError(f"Recursion limit exceeded while resolving type {name}")
433
+ try:
434
+ return self.types[name]
435
+ except KeyError:
436
+ raise ResolveError(f"Unknown type {name}") from None
345
437
 
346
438
  def copy(self) -> cstruct:
347
439
  """Create a copy of this cstruct instance.
@@ -351,6 +443,28 @@ class cstruct:
351
443
  """
352
444
  return copy.copy(self)
353
445
 
446
+ @contextmanager
447
+ def endianness(self, endian: AllowedEndianness) -> Iterator[cstruct]:
448
+ """Context manager for temporarily changing the endianness of this cstruct instance."""
449
+ original = self.endian
450
+ self.endian = normalize_endianness(endian)
451
+ try:
452
+ yield self
453
+ finally:
454
+ self.endian = original
455
+
456
+ @contextmanager
457
+ def big_endian(self) -> Iterator[cstruct]:
458
+ """Context manager for temporarily changing the endianness to big endian."""
459
+ with self.endianness(">") as cs:
460
+ yield cs
461
+
462
+ @contextmanager
463
+ def little_endian(self) -> Iterator[cstruct]:
464
+ """Context manager for temporarily changing the endianness to little endian."""
465
+ with self.endianness("<") as cs:
466
+ yield cs
467
+
354
468
  def _make_type(
355
469
  self,
356
470
  name: str,
@@ -518,6 +632,21 @@ class cstruct:
518
632
  # signed long long: TypeAlias = int64
519
633
  # unsigned long long: TypeAlias = uint64
520
634
 
635
+ u8: TypeAlias = uint8
636
+ u16: TypeAlias = uint16
637
+ u32: TypeAlias = uint32
638
+ u64: TypeAlias = uint64
639
+ u128: TypeAlias = uint128
640
+ __u8: TypeAlias = uint8
641
+ __u16: TypeAlias = uint16
642
+ __u32: TypeAlias = uint32
643
+ __u64: TypeAlias = uint64
644
+ __u128: TypeAlias = uint128
645
+ uchar: TypeAlias = uint8
646
+ ushort: TypeAlias = uint16
647
+ uint: TypeAlias = uint32
648
+ ulong: TypeAlias = uint32
649
+
521
650
  BYTE: TypeAlias = uint8
522
651
  CHAR: TypeAlias = char
523
652
  SHORT: TypeAlias = int16
@@ -583,19 +712,17 @@ class cstruct:
583
712
  _QWORD: TypeAlias = uint64
584
713
  _OWORD: TypeAlias = uint128
585
714
 
586
- u1: TypeAlias = uint8
587
- u2: TypeAlias = uint16
588
- u4: TypeAlias = uint32
589
- u8: TypeAlias = uint64
590
- u16: TypeAlias = uint128
591
- __u8: TypeAlias = uint8
592
- __u16: TypeAlias = uint16
593
- __u32: TypeAlias = uint32
594
- __u64: TypeAlias = uint64
595
- uchar: TypeAlias = uint8
596
- ushort: TypeAlias = uint16
597
- uint: TypeAlias = uint32
598
- ulong: TypeAlias = uint32
715
+
716
+ # Attribute names that types and constants may never shadow: the public API and methods of the cstruct
717
+ # class itself, plus the instance attributes created in __init__.
718
+ _RESERVED_NAMES = frozenset(dir(cstruct)) | {
719
+ "endian",
720
+ "consts",
721
+ "types",
722
+ "includes",
723
+ "pointer",
724
+ "_anonymous_count",
725
+ }
599
726
 
600
727
 
601
728
  def ctypes(structure: type[Structure]) -> type[_ctypes.Structure]:
@@ -2,9 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- from dissect.cstruct.exceptions import ExpressionParserError
5
+ from dissect.cstruct.exception import ExpressionParserError
6
6
  from dissect.cstruct.lexer import IDENTIFIER_TYPES, Lexer, TokenCursor, TokenType
7
- from dissect.cstruct.utils import offsetof, sizeof
7
+ from dissect.cstruct.util import offsetof, sizeof
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from collections.abc import Callable
@@ -12,6 +12,8 @@ if TYPE_CHECKING:
12
12
  from dissect.cstruct import cstruct
13
13
  from dissect.cstruct.lexer import Token
14
14
 
15
+ _MISSING = object()
16
+
15
17
  BINARY_OPERATORS: dict[TokenType, Callable[[int, int], int]] = {
16
18
  TokenType.PIPE: lambda a, b: a | b,
17
19
  TokenType.CARET: lambda a, b: a ^ b,
@@ -164,16 +166,13 @@ class Expression(TokenCursor):
164
166
  obj = context[part]
165
167
  elif part in cs.consts:
166
168
  obj = cs.consts[part]
167
- elif part in cs.typedefs:
168
- obj = cs.resolve(part)
169
+ elif part in cs.types:
170
+ obj = cs.types[part]
169
171
  else:
170
172
  raise self._error(f"Unknown identifier: '{ident}'", token=token)
171
173
  else:
172
- if isinstance(obj, dict) and part in obj:
173
- obj = obj[part]
174
- elif hasattr(obj, part):
175
- obj = getattr(obj, part)
176
- else:
174
+ obj = obj.get(part, _MISSING) if isinstance(obj, dict) else getattr(obj, part, _MISSING)
175
+ if obj is _MISSING:
177
176
  raise self._error(f"Unknown identifier: '{ident}'", token=token)
178
177
 
179
178
  try:
@@ -4,7 +4,7 @@ import enum
4
4
  import re
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from dissect.cstruct.exceptions import LexerError
7
+ from dissect.cstruct.exception import LexerError
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from collections.abc import Callable
@@ -134,6 +134,14 @@ def tokenize(data: str) -> list[Token]:
134
134
  return Lexer(data).tokenize()
135
135
 
136
136
 
137
+ def format_error_context(data: str, lineno: int, window: int = 1) -> str:
138
+ """Format a snippet of the input data around a given line number for error messages."""
139
+ start = max(0, lineno - window - 2)
140
+ context = data.splitlines()[start:lineno]
141
+ no_len = len(str(lineno)) + 2
142
+ return "\n".join(f"{start + i + 1:>{no_len}}: {line}" for i, line in enumerate(context))
143
+
144
+
137
145
  class Lexer:
138
146
  """Lexer compatible with C-like syntax for struct definitions and preprocessor directives."""
139
147
 
@@ -213,8 +221,10 @@ class Lexer:
213
221
 
214
222
  return self._take()
215
223
 
216
- def _error(self, msg: str, *, line: int | None = None) -> LexerError:
217
- return LexerError(f"line {line if line is not None else self._line}: {msg}")
224
+ def _error(self, msg: str, *, lineno: int | None = None) -> LexerError:
225
+ lineno = lineno if lineno is not None else self._line
226
+ content = format_error_context(self.data, lineno)
227
+ return LexerError(f"line {lineno}: {msg}\n{content}")
218
228
 
219
229
  def _emit(self, type: TokenType, value: str, line: int, column: int = 0) -> None:
220
230
  """Emit a token with the given type and value at the specified line and column."""
@@ -402,7 +412,7 @@ class Lexer:
402
412
  keyword = self._read_identifier()
403
413
 
404
414
  if (token_type := _PP_KEYWORDS.get(keyword)) is None:
405
- raise self._error(f"unknown preprocessor directive '#{keyword}'", line=line)
415
+ raise self._error(f"unknown preprocessor directive '#{keyword}'", lineno=line)
406
416
 
407
417
  self._emit(token_type, keyword, line, col)
408
418
 
@@ -410,7 +420,7 @@ class Lexer:
410
420
  self._skip_whitespace_and_comments()
411
421
 
412
422
  if not (name := self._read_identifier()):
413
- raise self._error("expected identifier after '#define'", line=line)
423
+ raise self._error("expected identifier after '#define'", lineno=line)
414
424
  self._emit(TokenType.IDENTIFIER, name, line)
415
425
 
416
426
  self._skip_whitespace_and_comments()
@@ -434,7 +444,7 @@ class Lexer:
434
444
  self._expect(">") # Consume closing `>`
435
445
  value = f"<{value}>"
436
446
  else:
437
- raise self._error("expected include path after '#include'", line=line)
447
+ raise self._error("expected include path after '#include'", lineno=line)
438
448
 
439
449
  self._emit(TokenType.STRING, value, line)
440
450
 
@@ -486,7 +496,7 @@ class Lexer:
486
496
  self._emit(_SINGLE_CHARS[ch], self._take(), line, col)
487
497
 
488
498
  else:
489
- raise self._error(f"unexpected character {ch!r}", line=line)
499
+ raise self._error(f"unexpected character {ch!r}", lineno=line)
490
500
 
491
501
  self._emit(TokenType.EOF, "", self._line, self._column)
492
502
  return self._tokens