dissect.cstruct 5.0.dev0__py3-none-any.whl → 5.0.dev7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. dissect/cstruct/__init__.py +2 -2
  2. dissect/cstruct/bitbuffer.py +3 -3
  3. dissect/cstruct/compiler.py +7 -7
  4. dissect/cstruct/cstruct.py +192 -65
  5. dissect/cstruct/expression.py +8 -9
  6. dissect/cstruct/lexer.py +17 -7
  7. dissect/cstruct/parser.py +31 -13
  8. dissect/cstruct/tools/stubgen.py +18 -22
  9. dissect/cstruct/types/base.py +78 -33
  10. dissect/cstruct/types/char.py +21 -11
  11. dissect/cstruct/types/enum.py +33 -13
  12. dissect/cstruct/types/int.py +9 -7
  13. dissect/cstruct/types/leb128.py +6 -4
  14. dissect/cstruct/types/packed.py +19 -10
  15. dissect/cstruct/types/pointer.py +74 -56
  16. dissect/cstruct/types/structure.py +130 -30
  17. dissect/cstruct/types/void.py +6 -4
  18. dissect/cstruct/types/wchar.py +26 -14
  19. dissect/cstruct/{utils.py → util.py} +36 -25
  20. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/METADATA +1 -1
  21. dissect_cstruct-5.0.dev7.dist-info/RECORD +32 -0
  22. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/scm_file_list.json +2 -2
  23. dissect_cstruct-5.0.dev7.dist-info/scm_version.json +8 -0
  24. dissect_cstruct-5.0.dev0.dist-info/RECORD +0 -32
  25. dissect_cstruct-5.0.dev0.dist-info/scm_version.json +0 -8
  26. /dissect/cstruct/{exceptions.py → exception.py} +0 -0
  27. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/WHEEL +0 -0
  28. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/entry_points.txt +0 -0
  29. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/licenses/COPYRIGHT +0 -0
  30. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/licenses/LICENSE +0 -0
  31. {dissect_cstruct-5.0.dev0.dist-info → dissect_cstruct-5.0.dev7.dist-info}/top_level.txt +0 -0
dissect/cstruct/parser.py CHANGED
@@ -5,13 +5,13 @@ import re
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from dissect.cstruct import compiler
8
- from dissect.cstruct.exceptions import (
8
+ from dissect.cstruct.exception import (
9
9
  ExpressionParserError,
10
10
  LexerError,
11
11
  ParserError,
12
12
  )
13
13
  from dissect.cstruct.expression import Expression
14
- from dissect.cstruct.lexer import IDENTIFIER_TYPES, TokenCursor, TokenType, tokenize
14
+ from dissect.cstruct.lexer import IDENTIFIER_TYPES, TokenCursor, TokenType, format_error_context, tokenize
15
15
  from dissect.cstruct.types import BaseArray, BaseType, Field, Structure
16
16
 
17
17
  if TYPE_CHECKING:
@@ -58,6 +58,7 @@ class CStyleParser(Parser):
58
58
  super().__init__(cs)
59
59
  self.compiled = compiled
60
60
  self.align = align
61
+ self._data = None
61
62
 
62
63
  self._flags: list[str] = []
63
64
  self._conditional_stack: list[tuple[Token, bool]] = []
@@ -74,6 +75,8 @@ class CStyleParser(Parser):
74
75
 
75
76
  data = _join_line_continuations(data)
76
77
 
78
+ # Keep a reference for error messages
79
+ self._data = data
77
80
  self._reset_tokens(tokenize(data))
78
81
  self._parse()
79
82
 
@@ -88,7 +91,12 @@ class CStyleParser(Parser):
88
91
  return self._tokens[self._pos].type in types
89
92
 
90
93
  def _error(self, msg: str, *, token: Token | None = None) -> ParserError:
91
- return ParserError(f"line {(token if token is not None else self._tokens[self._pos]).line}: {msg}")
94
+ lineno = (token if token is not None else self._tokens[self._pos]).line
95
+ if self._data is None:
96
+ return ParserError(f"line {lineno}: {msg}")
97
+
98
+ content = format_error_context(self._data, lineno)
99
+ return ParserError(f"line {lineno}: {msg}\n{content}")
92
100
 
93
101
  def _in_false_branch(self) -> bool:
94
102
  """Return whether we're currently in a false conditional branch."""
@@ -142,7 +150,8 @@ class CStyleParser(Parser):
142
150
 
143
151
  # If it's an anonymous enum/flag, add its members to the constants for convenience
144
152
  if not type_.__name__:
145
- self.cs.consts.update(type_.__members__)
153
+ for k, v in type_.__members__.items():
154
+ self.cs.add_const(k, v)
146
155
 
147
156
  self._expect(TokenType.SEMICOLON)
148
157
  else:
@@ -177,8 +186,9 @@ class CStyleParser(Parser):
177
186
  if value[-1] != quote:
178
187
  raise self._error("unterminated bytes literal", token=token)
179
188
 
180
- # Remove the leading b and surrounding quotes
181
- value = ast.literal_eval(f"b{value[2:-1]!r}")
189
+ # Remove the leading b and surrounding quotes and flatten escape sequences
190
+ value = value[2:-1].encode().decode("unicode_escape")
191
+ value = ast.literal_eval(f"b{value!r}")
182
192
  else:
183
193
  try:
184
194
  # Lazy mode, try to evaluate as a Python literal first (for simple constants)
@@ -192,17 +202,17 @@ class CStyleParser(Parser):
192
202
  # If evaluation fails, just keep it as a string (e.g. for macro-like constants)
193
203
  pass
194
204
 
195
- self.cs.consts[name_token.value] = value
205
+ self.cs.add_const(name_token.value, value)
196
206
 
197
207
  def _parse_undef(self) -> None:
198
208
  """Parse an undef directive and remove the constant."""
199
209
  self._expect(TokenType.PP_UNDEF)
200
210
 
201
211
  name_token = self._expect(TokenType.IDENTIFIER)
202
- if name_token.value in self.cs.consts:
203
- del self.cs.consts[name_token.value]
204
- else:
205
- raise self._error(f"constant {name_token.value!r} not defined", token=name_token)
212
+ try:
213
+ self.cs.del_const(name_token.value)
214
+ except KeyError:
215
+ raise self._error(f"constant {name_token.value!r} not defined", token=name_token) from None
206
216
 
207
217
  def _parse_include(self) -> None:
208
218
  """Parse an include directive and add the included file to the includes list."""
@@ -385,7 +395,15 @@ class CStyleParser(Parser):
385
395
  continue
386
396
  self._assert_not_eof()
387
397
 
388
- member_name = self._expect(TokenType.IDENTIFIER).value
398
+ # For historical reasons, we allow enum/flag member names to start with a digit
399
+ # E.g. `32BIT`
400
+ member_name = ""
401
+ if token := self._match(TokenType.NUMBER):
402
+ member_name += token.value
403
+ if token := self._match(TokenType.IDENTIFIER):
404
+ member_name += token.value
405
+ else:
406
+ member_name = self._expect(TokenType.IDENTIFIER).value
389
407
 
390
408
  if self._match(TokenType.EQUALS):
391
409
  expression = self._collect_until(TokenType.COMMA, TokenType.RBRACE)
@@ -527,7 +545,7 @@ class CStyleParser(Parser):
527
545
  ):
528
546
  # This identifier is followed by a field delimiter, it should be the field name,
529
547
  # UNLESS the current parts don't form a valid type yet.
530
- if " ".join(parts) in self.cs.typedefs:
548
+ if " ".join(parts) in self.cs.types:
531
549
  break
532
550
 
533
551
  # Current parts don't resolve, consume and hope this completes the type name
@@ -79,31 +79,29 @@ def generate_cstruct_stub(cs: cstruct, module_prefix: str = "", cls_name: str =
79
79
 
80
80
  defined_names = set()
81
81
 
82
- # Then typedefs
83
- for name, typedef in cs.typedefs.items():
84
- if name in empty_cs.typedefs:
82
+ # Then types
83
+ for name, type_ in cs.types.items():
84
+ if name in empty_cs.types:
85
85
  continue
86
86
 
87
- if typedef.__name__ in empty_cs.typedefs:
88
- stub = f"{name}: TypeAlias = {cs_prefix}{typedef.__name__}"
89
- elif typedef.__name__ in defined_names:
87
+ if type_.__name__ in empty_cs.types:
88
+ stub = f"{name}: TypeAlias = {cs_prefix}{type_.__name__}"
89
+ elif type_.__name__ in defined_names:
90
90
  # Create an alias to the type if we have already seen it before.
91
- stub = f"{name}: TypeAlias = {typedef.__name__}"
92
- elif issubclass(typedef, (types.Enum, types.Flag)):
93
- stub = generate_enum_stub(typedef, cs_prefix=cs_prefix, module_prefix=module_prefix)
94
- elif issubclass(typedef, types.Pointer):
95
- typehint = generate_typehint(typedef, prefix=cs_prefix, module_prefix=module_prefix)
91
+ stub = f"{name}: TypeAlias = {type_.__name__}"
92
+ elif issubclass(type_, (types.Enum, types.Flag)):
93
+ stub = generate_enum_stub(type_, cs_prefix=cs_prefix, module_prefix=module_prefix)
94
+ elif issubclass(type_, types.Pointer):
95
+ typehint = generate_typehint(type_, prefix=cs_prefix, module_prefix=module_prefix)
96
96
  stub = f"{name}: TypeAlias = {typehint}"
97
- elif issubclass(typedef, types.Structure):
98
- stub = generate_structure_stub(typedef, cs_prefix=cs_prefix, module_prefix=module_prefix)
99
- elif issubclass(typedef, types.BaseType):
100
- stub = generate_generic_stub(typedef, cs_prefix=cs_prefix, module_prefix=module_prefix)
101
- elif isinstance(typedef, str):
102
- stub = f"{name}: TypeAlias = {typedef}"
97
+ elif issubclass(type_, types.Structure):
98
+ stub = generate_structure_stub(type_, cs_prefix=cs_prefix, module_prefix=module_prefix)
99
+ elif issubclass(type_, types.BaseType):
100
+ stub = generate_generic_stub(type_, cs_prefix=cs_prefix, module_prefix=module_prefix)
103
101
  else:
104
- raise TypeError(f"Unknown typedef: {typedef}")
102
+ raise TypeError(f"Unknown type: {type_}")
105
103
 
106
- defined_names.add(typedef.__name__)
104
+ defined_names.add(type_.__name__)
107
105
 
108
106
  body.append(textwrap.indent(stub, prefix=indent))
109
107
 
@@ -158,12 +156,10 @@ def generate_structure_stub(
158
156
  module_prefix: str = "",
159
157
  ) -> str:
160
158
  result = [f"class {name_prefix}{structure.__name__}({module_prefix}{structure.__base__.__name__}):"]
161
-
162
159
  indent = " " * 4
163
160
 
164
161
  args = ["self"]
165
162
  for field_name, field in structure.fields.items():
166
- type_name = field.type.__name__
167
163
  inlined = False
168
164
 
169
165
  # If it's a structure and not globally defined, add an inline stub for it
@@ -171,7 +167,7 @@ def generate_structure_stub(
171
167
  while issubclass(nested_type, types.BaseArray):
172
168
  nested_type = nested_type.type
173
169
 
174
- if issubclass(nested_type, types.Structure) and type_name not in structure.cs.typedefs:
170
+ if issubclass(nested_type, types.Structure) and nested_type.__name__ not in structure.cs.types:
175
171
  inlined = True
176
172
  inline_stub = generate_structure_stub(nested_type, cs_prefix=cs_prefix, module_prefix=module_prefix)
177
173
 
@@ -4,7 +4,7 @@ import functools
4
4
  from io import BytesIO
5
5
  from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, TypeVar
6
6
 
7
- from dissect.cstruct.exceptions import ArraySizeError
7
+ from dissect.cstruct.exception import ArraySizeError
8
8
  from dissect.cstruct.expression import Expression
9
9
 
10
10
  if TYPE_CHECKING:
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
12
12
 
13
13
  from typing_extensions import Self
14
14
 
15
- from dissect.cstruct.cstruct import cstruct
15
+ from dissect.cstruct.cstruct import AllowedEndianness, Endianness, cstruct
16
16
 
17
17
 
18
18
  EOF = -0xE0F # Negative counts are illegal anyway, so abuse that for our EOF sentinel
@@ -41,14 +41,15 @@ class MetaType(type):
41
41
  stream = args[0]
42
42
 
43
43
  if _is_readable_type(stream):
44
- return cls._read(stream)
44
+ endian = normalize_endianness(endian) if (endian := kwargs.get("endian")) is not None else cls.cs.endian
45
+ return cls._read(stream, endian=endian)
45
46
 
46
47
  if issubclass(cls, bytes) and isinstance(stream, bytes) and len(stream) == cls.size:
47
48
  # Shortcut for char/bytes type
48
49
  return type.__call__(cls, *args, **kwargs)
49
50
 
50
51
  if _is_buffer_type(stream):
51
- return cls.reads(stream)
52
+ return cls.reads(stream, endian=kwargs.get("endian"))
52
53
 
53
54
  return type.__call__(cls, *args, **kwargs)
54
55
 
@@ -71,60 +72,73 @@ class MetaType(type):
71
72
  """Return the default value of this type."""
72
73
  return cls()
73
74
 
74
- def reads(cls, data: bytes | memoryview | bytearray) -> Self: # type: ignore
75
+ def reads(cls, data: bytes | memoryview | bytearray, *, endian: AllowedEndianness | None = None) -> Self: # type: ignore
75
76
  """Parse the given data from a bytes-like object.
76
77
 
77
78
  Args:
78
79
  data: Bytes-like object to parse.
80
+ endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
81
+ When omitted, the cstruct instance's current endianness is used.
79
82
 
80
83
  Returns:
81
84
  The parsed value of this type.
82
85
  """
83
- return cls._read(BytesIO(data))
86
+ endian = normalize_endianness(endian) if endian is not None else cls.cs.endian
87
+ return cls._read(BytesIO(data), endian=endian)
84
88
 
85
- def read(cls, obj: BinaryIO | bytes | memoryview | bytearray) -> Self: # type: ignore
89
+ def read(cls, obj: BinaryIO | bytes | memoryview | bytearray, *, endian: AllowedEndianness | None = None) -> Self: # type: ignore
86
90
  """Parse the given data.
87
91
 
88
92
  Args:
89
93
  obj: Data to parse. Can be a bytes-like object or a file-like object.
94
+ endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
95
+ When omitted, the cstruct instance's current endianness is used.
90
96
 
91
97
  Returns:
92
98
  The parsed value of this type.
93
99
  """
94
100
  if _is_buffer_type(obj):
95
- return cls.reads(obj)
101
+ return cls.reads(obj, endian=endian)
96
102
 
97
103
  if not _is_readable_type(obj):
98
104
  raise TypeError("Invalid object type")
99
105
 
100
- return cls._read(obj)
106
+ endian = normalize_endianness(endian) if endian is not None else cls.cs.endian
107
+ return cls._read(obj, endian=endian)
101
108
 
102
- def write(cls, stream: BinaryIO, value: Any) -> int:
109
+ def write(cls, stream: BinaryIO, value: Any, *, endian: AllowedEndianness | None = None) -> int:
103
110
  """Write a value to a writable file-like object.
104
111
 
105
112
  Args:
106
113
  stream: File-like objects that supports writing.
107
114
  value: Value to write.
115
+ endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
116
+ When omitted, the cstruct instance's current endianness is used.
108
117
 
109
118
  Returns:
110
119
  The amount of bytes written.
111
120
  """
112
- return cls._write(stream, value)
121
+ endian = normalize_endianness(endian) if endian is not None else cls.cs.endian
122
+ return cls._write(stream, value, endian=endian)
113
123
 
114
- def dumps(cls, value: Any) -> bytes:
124
+ def dumps(cls, value: Any, *, endian: AllowedEndianness | None = None) -> bytes:
115
125
  """Dump a value to a byte string.
116
126
 
117
127
  Args:
118
128
  value: Value to dump.
129
+ endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
130
+ When omitted, the cstruct instance's current endianness is used.
119
131
 
120
132
  Returns:
121
133
  The raw bytes of this type.
122
134
  """
135
+ endian = normalize_endianness(endian) if endian is not None else cls.cs.endian
136
+
123
137
  out = BytesIO()
124
- cls._write(out, value)
138
+ cls._write(out, value, endian=endian)
125
139
  return out.getvalue()
126
140
 
127
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self: # type: ignore
141
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self: # type: ignore
128
142
  """Internal function for reading value.
129
143
 
130
144
  Must be implemented per type.
@@ -132,10 +146,13 @@ class MetaType(type):
132
146
  Args:
133
147
  stream: The stream to read from.
134
148
  context: Optional reading context.
149
+ endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
135
150
  """
136
151
  raise NotImplementedError
137
152
 
138
- def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] | None = None) -> list[Self]: # type: ignore
153
+ def _read_array(
154
+ cls, stream: BinaryIO, count: int, *, context: dict[str, Any] | None = None, endian: Endianness
155
+ ) -> list[Self]: # type: ignore
139
156
  """Internal function for reading array values.
140
157
 
141
158
  Allows type implementations to do optimized reading for their type.
@@ -144,16 +161,17 @@ class MetaType(type):
144
161
  stream: The stream to read from.
145
162
  count: The amount of values to read.
146
163
  context: Optional reading context.
164
+ endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
147
165
  """
148
166
  if count == EOF:
149
167
  result = []
150
168
  while not _is_eof(stream):
151
- result.append(cls._read(stream, context))
169
+ result.append(cls._read(stream, context=context, endian=endian))
152
170
  return result
153
171
 
154
- return [cls._read(stream, context) for _ in range(count)]
172
+ return [cls._read(stream, context=context, endian=endian) for _ in range(count)]
155
173
 
156
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> list[Self]:
174
+ def _read_0(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> list[Self]:
157
175
  """Internal function for reading null-terminated data.
158
176
 
159
177
  "Null" is type specific, so must be implemented per type.
@@ -161,13 +179,14 @@ class MetaType(type):
161
179
  Args:
162
180
  stream: The stream to read from.
163
181
  context: Optional reading context.
182
+ endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
164
183
  """
165
184
  raise NotImplementedError
166
185
 
167
- def _write(cls, stream: BinaryIO, data: Any) -> int:
186
+ def _write(cls, stream: BinaryIO, data: Any, *, endian: Endianness) -> int:
168
187
  raise NotImplementedError
169
188
 
170
- def _write_array(cls, stream: BinaryIO, array: list[Self]) -> int: # type: ignore
189
+ def _write_array(cls, stream: BinaryIO, array: list[Self], *, endian: Endianness) -> int: # type: ignore
171
190
  """Internal function for writing arrays.
172
191
 
173
192
  Allows type implementations to do optimized writing for their type.
@@ -175,10 +194,11 @@ class MetaType(type):
175
194
  Args:
176
195
  stream: The stream to read from.
177
196
  array: The array to write.
197
+ endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
178
198
  """
179
- return sum(cls._write(stream, entry) for entry in array)
199
+ return sum(cls._write(stream, entry, endian=endian) for entry in array)
180
200
 
181
- def _write_0(cls, stream: BinaryIO, array: list[Self]) -> int: # type: ignore
201
+ def _write_0(cls, stream: BinaryIO, array: list[Self], *, endian: Endianness) -> int: # type: ignore
182
202
  """Internal function for writing null-terminated arrays.
183
203
 
184
204
  Allows type implementations to do optimized writing for their type.
@@ -186,8 +206,9 @@ class MetaType(type):
186
206
  Args:
187
207
  stream: The stream to read from.
188
208
  array: The array to write.
209
+ endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
189
210
  """
190
- return cls._write_array(stream, [*array, cls.__default__()])
211
+ return cls._write_array(stream, [*array, cls.__default__()], endian=endian)
191
212
 
192
213
 
193
214
  class _overload:
@@ -250,9 +271,9 @@ class BaseArray(BaseType):
250
271
  )
251
272
 
252
273
  @classmethod
253
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> list[BaseType]:
274
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> list[BaseType]:
254
275
  if cls.null_terminated:
255
- return cls.type._read_0(stream, context)
276
+ return cls.type._read_0(stream, context=context, endian=endian)
256
277
 
257
278
  if isinstance(cls.num_entries, int):
258
279
  num = max(0, cls.num_entries)
@@ -266,23 +287,25 @@ class BaseArray(BaseType):
266
287
  raise
267
288
  num = EOF
268
289
 
269
- return cls.type._read_array(stream, num, context)
290
+ return cls.type._read_array(stream, num, context=context, endian=endian)
270
291
 
271
292
  @classmethod
272
- def _write(cls, stream: BinaryIO, data: list[Any]) -> int:
293
+ def _write(cls, stream: BinaryIO, data: list[Any], *, endian: Endianness) -> int:
273
294
  if cls.null_terminated:
274
- return cls.type._write_0(stream, data)
295
+ return cls.type._write_0(stream, data, endian=endian)
275
296
 
276
- if not cls.dynamic and cls.num_entries != (actual_size := len(data)):
277
- raise ArraySizeError(f"Expected static array size {cls.num_entries}, got {actual_size} instead.")
297
+ if not cls.dynamic and (remaining := cls.num_entries - (actual_size := len(data))):
298
+ if remaining < 0:
299
+ raise ArraySizeError(f"Expected static array size {cls.num_entries}, got {actual_size} instead.")
300
+ data = data + [cls.type.__default__()] * remaining
278
301
 
279
- return cls.type._write_array(stream, data)
302
+ return cls.type._write_array(stream, data, endian=endian)
280
303
 
281
304
 
282
305
  class Array(list[T], BaseArray):
283
306
  @classmethod
284
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> list[T]:
285
- return cls(super()._read(stream, context))
307
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> list[T]:
308
+ return cls(super()._read(stream, context=context, endian=endian))
286
309
 
287
310
 
288
311
  def _is_readable_type(value: object) -> bool:
@@ -305,5 +328,27 @@ def _is_eof(stream: BinaryIO) -> bool:
305
328
  return False
306
329
 
307
330
 
331
+ ENDIANNESS_MAP: dict[AllowedEndianness, Endianness] = {
332
+ "<": "<",
333
+ ">": ">",
334
+ "!": "!",
335
+ "@": "@",
336
+ "=": "=",
337
+ "network": "!",
338
+ "little": "<",
339
+ "big": ">",
340
+ }
341
+
342
+
343
+ def normalize_endianness(endian: AllowedEndianness) -> Endianness:
344
+ """Normalize an endianness string to one of the standard format characters."""
345
+ try:
346
+ return ENDIANNESS_MAP[endian]
347
+ except KeyError:
348
+ raise ValueError(
349
+ f"Invalid endianness: {endian!r}, expected one of {', '.join(ENDIANNESS_MAP.keys())}"
350
+ ) from None
351
+
352
+
308
353
  # As mentioned in the BaseType class, we correctly set the type here
309
354
  MetaType.ArrayType = Array
@@ -2,11 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any, BinaryIO
4
4
 
5
+ from dissect.cstruct.exception import ArraySizeError
5
6
  from dissect.cstruct.types.base import EOF, BaseArray, BaseType
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from typing_extensions import Self
9
10
 
11
+ from dissect.cstruct.cstruct import Endianness
12
+
10
13
 
11
14
  class CharArray(bytes, BaseArray):
12
15
  """Character array type for reading and writing byte strings."""
@@ -16,19 +19,24 @@ class CharArray(bytes, BaseArray):
16
19
  return type.__call__(cls, b"\x00" * (0 if cls.dynamic or cls.null_terminated else cls.num_entries))
17
20
 
18
21
  @classmethod
19
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self:
20
- return type.__call__(cls, super()._read(stream, context))
22
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self:
23
+ return type.__call__(cls, super()._read(stream, context=context, endian=endian))
21
24
 
22
25
  @classmethod
23
- def _write(cls, stream: BinaryIO, data: bytes) -> int:
24
- if isinstance(data, list) and data and isinstance(data[0], int):
26
+ def _write(cls, stream: BinaryIO, data: bytes | str | list[int], *, endian: Endianness) -> int:
27
+ if isinstance(data, list):
25
28
  data = bytes(data)
26
-
27
29
  elif isinstance(data, str):
28
30
  data = data.encode("latin-1")
29
31
 
30
32
  if cls.null_terminated:
31
- return stream.write(data + b"\x00")
33
+ data += b"\x00"
34
+
35
+ if not cls.dynamic and (remaining := cls.num_entries - (actual_size := len(data))):
36
+ if remaining < 0:
37
+ raise ArraySizeError(f"Expected static array size {cls.num_entries}, got {actual_size} instead.")
38
+ data += b"\x00" * remaining
39
+
32
40
  return stream.write(data)
33
41
 
34
42
 
@@ -42,11 +50,13 @@ class Char(bytes, BaseType):
42
50
  return type.__call__(cls, b"\x00")
43
51
 
44
52
  @classmethod
45
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self:
46
- return cls._read_array(stream, 1, context)
53
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self:
54
+ return cls._read_array(stream, 1, context=context, endian=endian)
47
55
 
48
56
  @classmethod
49
- def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] | None = None) -> Self:
57
+ def _read_array(
58
+ cls, stream: BinaryIO, count: int, *, context: dict[str, Any] | None = None, endian: Endianness
59
+ ) -> Self:
50
60
  if count == 0:
51
61
  return type.__call__(cls, b"")
52
62
 
@@ -57,7 +67,7 @@ class Char(bytes, BaseType):
57
67
  return type.__call__(cls, data)
58
68
 
59
69
  @classmethod
60
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self:
70
+ def _read_0(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self:
61
71
  buf = []
62
72
  while True:
63
73
  byte = stream.read(1)
@@ -72,7 +82,7 @@ class Char(bytes, BaseType):
72
82
  return type.__call__(cls, b"".join(buf))
73
83
 
74
84
  @classmethod
75
- def _write(cls, stream: BinaryIO, data: bytes | int | str) -> int:
85
+ def _write(cls, stream: BinaryIO, data: bytes | int | str, *, endian: Endianness) -> int:
76
86
  if isinstance(data, int):
77
87
  data = chr(data)
78
88
 
@@ -10,7 +10,7 @@ from dissect.cstruct.types.base import BaseType, MetaType
10
10
  if TYPE_CHECKING:
11
11
  from typing_extensions import Self
12
12
 
13
- from dissect.cstruct.cstruct import cstruct
13
+ from dissect.cstruct.cstruct import Endianness, cstruct
14
14
  from dissect.cstruct.types.base import Array
15
15
 
16
16
 
@@ -84,25 +84,45 @@ class EnumMetaType(EnumMeta, MetaType):
84
84
  return True
85
85
  return value in cls._value2member_map_
86
86
 
87
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self:
88
- return cls(cls.type._read(stream, context))
87
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self:
88
+ return cls(cls.type._read(stream, context=context, endian=endian))
89
89
 
90
- def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] | None = None) -> list[Self]:
91
- return list(map(cls, cls.type._read_array(stream, count, context)))
90
+ def _read_array(
91
+ cls, stream: BinaryIO, count: int, *, context: dict[str, Any] | None = None, endian: Endianness
92
+ ) -> list[Self]:
93
+ return list(map(cls, cls.type._read_array(stream, count, context=context, endian=endian)))
92
94
 
93
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> list[Self]:
94
- return list(map(cls, cls.type._read_0(stream, context)))
95
+ def _read_0(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> list[Self]:
96
+ return list(map(cls, cls.type._read_0(stream, context=context, endian=endian)))
95
97
 
96
- def _write(cls, stream: BinaryIO, data: Enum) -> int:
97
- return cls.type._write(stream, data.value)
98
+ def _write(cls, stream: BinaryIO, data: Enum, *, endian: Endianness) -> int:
99
+ return cls.type._write(stream, data.value, endian=endian)
98
100
 
99
- def _write_array(cls, stream: BinaryIO, array: list[BaseType | int]) -> int:
101
+ def _write_array(cls, stream: BinaryIO, array: list[BaseType | int], *, endian: Endianness) -> int:
100
102
  data = [entry.value if isinstance(entry, _Enum) else entry for entry in array]
101
- return cls.type._write_array(stream, data)
103
+ return cls.type._write_array(stream, data, endian=endian)
102
104
 
103
- def _write_0(cls, stream: BinaryIO, array: list[BaseType | int]) -> int:
105
+ def _write_0(cls, stream: BinaryIO, array: list[BaseType | int], *, endian: Endianness) -> int:
104
106
  data = [entry.value if isinstance(entry, _Enum) else entry for entry in array]
105
- return cls._write_array(stream, [*data, cls.type.__default__()])
107
+ return cls._write_array(stream, [*data, cls.type.__default__()], endian=endian)
108
+
109
+ def cdef(cls) -> str:
110
+ """Render this enum (or flag) back to its C-style definition.
111
+
112
+ Returns:
113
+ The C-style definition as a string.
114
+ """
115
+ keyword = "flag" if issubclass(cls, IntFlag) else "enum"
116
+ underlying = cls.type.__name__
117
+ title = f"{keyword} {cls.__name__} : {underlying}" if cls.__name__ else f"{keyword} : {underlying}"
118
+
119
+ lines = [f"{title} {{"]
120
+ for member_name, member in cls.__members__.items():
121
+ value = hex(member.value) if keyword == "flag" else member.value
122
+ lines.append(f" {member_name} = {value},")
123
+ lines.append("};")
124
+
125
+ return "\n".join(lines)
106
126
 
107
127
 
108
128
  def _fix_alias_members(cls: type[Enum]) -> None:
@@ -3,11 +3,13 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any, BinaryIO
4
4
 
5
5
  from dissect.cstruct.types.base import BaseType
6
- from dissect.cstruct.utils import ENDIANNESS_MAP
6
+ from dissect.cstruct.util import ENDIANNESS_TO_BYTEORDER_MAP
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from typing_extensions import Self
10
10
 
11
+ from dissect.cstruct.cstruct import Endianness
12
+
11
13
 
12
14
  class Int(int, BaseType):
13
15
  """Integer type that can span an arbitrary amount of bytes."""
@@ -15,20 +17,20 @@ class Int(int, BaseType):
15
17
  signed: bool
16
18
 
17
19
  @classmethod
18
- def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self:
20
+ def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self:
19
21
  data = stream.read(cls.size)
20
22
 
21
23
  if len(data) != cls.size:
22
24
  raise EOFError(f"Read {len(data)} bytes, but expected {cls.size}")
23
25
 
24
- return cls.from_bytes(data, ENDIANNESS_MAP[cls.cs.endian], signed=cls.signed)
26
+ return cls.from_bytes(data, ENDIANNESS_TO_BYTEORDER_MAP[endian], signed=cls.signed)
25
27
 
26
28
  @classmethod
27
- def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> Self:
29
+ def _read_0(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Self:
28
30
  result = []
29
31
 
30
32
  while True:
31
- if (value := cls._read(stream, context)) == 0:
33
+ if (value := cls._read(stream, context=context, endian=endian)) == 0:
32
34
  break
33
35
 
34
36
  result.append(value)
@@ -36,5 +38,5 @@ class Int(int, BaseType):
36
38
  return result
37
39
 
38
40
  @classmethod
39
- def _write(cls, stream: BinaryIO, data: int) -> int:
40
- return stream.write(data.to_bytes(cls.size, ENDIANNESS_MAP[cls.cs.endian], signed=cls.signed))
41
+ def _write(cls, stream: BinaryIO, data: int, *, endian: Endianness) -> int:
42
+ return stream.write(data.to_bytes(cls.size, ENDIANNESS_TO_BYTEORDER_MAP[endian], signed=cls.signed))