dissect.cstruct 5.0.dev11__tar.gz → 5.0.dev13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/CHANGELOG.md +11 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/PKG-INFO +1 -1
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/compiler.py +119 -81
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/cstruct.py +1 -1
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/base.py +23 -9
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/char.py +7 -3
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/structure.py +31 -32
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/wchar.py +18 -7
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/PKG-INFO +1 -1
- dissect_cstruct-5.0.dev13/dissect.cstruct.egg-info/scm_version.json +8 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_align.py +24 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_benchmark.py +38 -38
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_bitfield.py +44 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_compiler.py +18 -17
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_char.py +44 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_enum.py +23 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_pointer.py +20 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_structure.py +91 -5
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_union.py +95 -0
- dissect_cstruct-5.0.dev13/tests/test_types_wchar.py +309 -0
- dissect_cstruct-5.0.dev11/dissect.cstruct.egg-info/scm_version.json +0 -8
- dissect_cstruct-5.0.dev11/tests/test_types_wchar.py +0 -150
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/.git-blame-ignore-revs +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/.gitattributes +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/COPYRIGHT +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/LICENSE +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/MANIFEST.in +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/README.md +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/__init__.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/bitbuffer.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/exception.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/expression.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/lexer.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/parser.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/tools/__init__.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/tools/stubgen.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/__init__.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/enum.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/flag.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/int.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/leb128.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/packed.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/pointer.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/types/void.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect/cstruct/util.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/SOURCES.txt +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/dependency_links.txt +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/entry_points.txt +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/requires.txt +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/scm_file_list.json +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/dissect.cstruct.egg-info/top_level.txt +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/examples/disk.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/examples/mirai.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/examples/pe.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/examples/protobuf.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/examples/secdesc.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/pyproject.toml +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/setup.cfg +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/__init__.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/_data/testdef.txt +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/_docs/Makefile +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/_docs/__init__.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/_docs/conf.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/_docs/index.rst +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/conftest.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_annotations.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_basic.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_bitbuffer.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_ctypes.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_expression.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_lexer.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_parser.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_tools_stubgen.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_base.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_custom.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_flag.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_int.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_leb128.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_packed.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_types_void.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/test_utils.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tests/utils.py +0 -0
- {dissect_cstruct-5.0.dev11 → dissect_cstruct-5.0.dev13}/tox.ini +0 -0
|
@@ -22,6 +22,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
22
22
|
- Faster paths for parsing primitive types from bytes-like objects, enum and flag lookups and flag operators.
|
|
23
23
|
- Null-terminated strings are read in chunks from seekable streams.
|
|
24
24
|
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Structures and unions no longer share nested structures and arrays between their default values.
|
|
28
|
+
- Unions can be created with default values and read if they have doubly nested structures or nested unions.
|
|
29
|
+
- Writing a `bytearray` to a `char` array no longer modifies the `bytearray`.
|
|
30
|
+
- Writing a structure correctly returns the amount of bytes written, including padding and bit fields.
|
|
31
|
+
- Writing a structure raises an `AttributeError` for missing attributes and for values of a nested structure field that aren't a structure, instead of writing default values.
|
|
32
|
+
- A surrogate pair takes up two entries of a `wchar` array, so writing it no longer writes more bytes than the size of the array.
|
|
33
|
+
- A `wchar` string can hold a lone surrogate, instead of raising an error for the whole structure when reading or writing it. Bytes are written back as they were stored.
|
|
34
|
+
- Writing more than a single character to a `char` or `wchar` raises a `ValueError`, instead of writing all of it and moving the fields after it. An empty value is padded.
|
|
35
|
+
|
|
25
36
|
## [4.5] - 20-05-2025
|
|
26
37
|
|
|
27
38
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dissect.cstruct
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.dev13
|
|
4
4
|
Summary: A Dissect module implementing a parser for C-like structures: structure parsing in Python made easy
|
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -182,8 +182,10 @@ _GLOBALS = {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
|
|
185
|
-
class
|
|
186
|
-
"""
|
|
185
|
+
class _SourceGenerator:
|
|
186
|
+
"""Base class for generating the source code of an optimized method of a structure or union."""
|
|
187
|
+
|
|
188
|
+
name: str
|
|
187
189
|
|
|
188
190
|
def __init__(self, cls: type[Structure], fields: list[Field], inline: bool = True):
|
|
189
191
|
self.cls = cls
|
|
@@ -191,24 +193,17 @@ class _ReadSourceGenerator:
|
|
|
191
193
|
self.align = cls.__align__
|
|
192
194
|
self.inline = inline and not self.align
|
|
193
195
|
|
|
196
|
+
# The values available to the generated code
|
|
197
|
+
self.symbols: dict[str, Any] = {"cs": cls.__cs__}
|
|
198
|
+
self._symbol_names: dict[int, str] = {}
|
|
199
|
+
|
|
194
200
|
# Witnesses of inlined structure types (symbol of the type -> symbol of its __init__ at compile time)
|
|
195
201
|
self._witnesses: dict[str, str] = {}
|
|
196
|
-
self._fallback_symbol: str | None = None
|
|
197
202
|
|
|
198
203
|
# The state of the bit buffer: the type of its current value and the remaining bits
|
|
199
204
|
self._bits: tuple[type[BaseType], int] | None = None
|
|
200
205
|
self._var_ids: dict[str, int] = {}
|
|
201
206
|
|
|
202
|
-
# The values available to the generated code
|
|
203
|
-
self.symbols: dict[str, Any] = {"cs": cls.__cs__}
|
|
204
|
-
self._symbol_names: dict[int, str] = {}
|
|
205
|
-
|
|
206
|
-
# (field name, local variable name) of all fields assigned so far
|
|
207
|
-
self._assigned: list[tuple[str, str]] = []
|
|
208
|
-
self._has_context = False
|
|
209
|
-
self._uses_start = False
|
|
210
|
-
self._uses_sizes = False
|
|
211
|
-
|
|
212
207
|
def _type_symbol(self, type_: type[BaseType]) -> str:
|
|
213
208
|
"""Add a type as symbol, returns its name."""
|
|
214
209
|
return self._add_symbol(type_, "type")
|
|
@@ -220,39 +215,12 @@ class _ReadSourceGenerator:
|
|
|
220
215
|
self.symbols[symbol] = value
|
|
221
216
|
return symbol
|
|
222
217
|
|
|
223
|
-
def _context(self, lines: list[str]) -> str:
|
|
224
|
-
"""Return the name of the context dictionary, creating it from the fields read so far if needed."""
|
|
225
|
-
if not self._has_context:
|
|
226
|
-
self._has_context = True
|
|
227
|
-
items = ", ".join(f"{name!r}: {var}" for name, var in self._assigned)
|
|
228
|
-
lines.append(f"values = {{{items}}}")
|
|
229
|
-
|
|
230
|
-
return "values"
|
|
231
|
-
|
|
232
218
|
def _var(self, name: str) -> str:
|
|
233
219
|
"""Return a unique variable name for the generated code."""
|
|
234
220
|
index = self._var_ids.get(name, 0)
|
|
235
221
|
self._var_ids[name] = index + 1
|
|
236
222
|
return f"_{name}{index}"
|
|
237
223
|
|
|
238
|
-
def _assign(self, field: Field, value: str) -> str:
|
|
239
|
-
"""Assign a value to a new local variable for the given field, and the context dictionary if it exists."""
|
|
240
|
-
var = f"_value{len(self._assigned)}"
|
|
241
|
-
self._assigned.append((field._name, var))
|
|
242
|
-
if self._has_context:
|
|
243
|
-
return f"{var} = values[{field._name!r}] = {value}"
|
|
244
|
-
return f"{var} = {value}"
|
|
245
|
-
|
|
246
|
-
def _assign_local(self, field: Field, value: str) -> list[str]:
|
|
247
|
-
return [self._assign(field, value)]
|
|
248
|
-
|
|
249
|
-
def _fallback(self) -> str:
|
|
250
|
-
"""Return the symbol of the non-inlined fallback read function."""
|
|
251
|
-
if self._fallback_symbol is None:
|
|
252
|
-
fallback = _Fallback(self.cls, self.fields)
|
|
253
|
-
self._fallback_symbol = self._add_symbol(fallback, "fallback")
|
|
254
|
-
return self._fallback_symbol
|
|
255
|
-
|
|
256
224
|
def _witness(self, type_: type[Structure]) -> str:
|
|
257
225
|
"""Add an inlined type as symbol, with a witness to detect modifications to it after compilation."""
|
|
258
226
|
symbol = self._type_symbol(type_)
|
|
@@ -263,10 +231,22 @@ class _ReadSourceGenerator:
|
|
|
263
231
|
"""Return the conditions that are true if any of the inlined types were modified after compilation."""
|
|
264
232
|
return [f"{type_}.__init__ is not {init}" for type_, init in self._witnesses.items()]
|
|
265
233
|
|
|
234
|
+
def _starts_bit_value(self, field: Field) -> bool:
|
|
235
|
+
"""Return whether a bit field is the first of a new value of the bit buffer."""
|
|
236
|
+
return self._bits is None or self._bits[1] == 0 or self._bits[0] is not _get_bit_buffer_type(field)
|
|
237
|
+
|
|
238
|
+
def _advance_bits(self, field: Field) -> int:
|
|
239
|
+
"""Update the state of the bit buffer for a bit field, returns the bits that were remaining before it."""
|
|
240
|
+
if self._starts_bit_value(field):
|
|
241
|
+
self._bits = (_get_bit_buffer_type(field), _get_bits_type(field).__size__ * 8)
|
|
242
|
+
remaining = self._bits[1]
|
|
243
|
+
self._bits = (self._bits[0], remaining - field.bits)
|
|
244
|
+
return remaining
|
|
245
|
+
|
|
266
246
|
def generate(self) -> MethodType:
|
|
267
247
|
source = self.generate_source()
|
|
268
248
|
|
|
269
|
-
code = python_compile(source, f"<compiled {self.cls.__name__}.
|
|
249
|
+
code = python_compile(source, f"<compiled {self.cls.__name__}.{self.name}>", "exec")
|
|
270
250
|
exec(code, {**_GLOBALS, **self.symbols}, d := {})
|
|
271
251
|
obj = d.popitem()[1]
|
|
272
252
|
obj.__source__ = source
|
|
@@ -278,6 +258,56 @@ class _ReadSourceGenerator:
|
|
|
278
258
|
return self._generate_union_source()
|
|
279
259
|
return self._generate_struct_source()
|
|
280
260
|
|
|
261
|
+
def _generate_struct_source(self) -> str:
|
|
262
|
+
raise NotImplementedError
|
|
263
|
+
|
|
264
|
+
def _generate_union_source(self) -> str:
|
|
265
|
+
raise NotImplementedError
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class _ReadSourceGenerator(_SourceGenerator):
|
|
269
|
+
"""Generates the source code of an optimized ``_read`` method for a structure or union."""
|
|
270
|
+
|
|
271
|
+
name = "_read"
|
|
272
|
+
|
|
273
|
+
def __init__(self, cls: type[Structure], fields: list[Field], inline: bool = True):
|
|
274
|
+
super().__init__(cls, fields, inline)
|
|
275
|
+
|
|
276
|
+
self._fallback_symbol: str | None = None
|
|
277
|
+
|
|
278
|
+
# (field name, local variable name) of all fields assigned so far
|
|
279
|
+
self._assigned: list[tuple[str, str]] = []
|
|
280
|
+
self._has_context = False
|
|
281
|
+
self._uses_start = False
|
|
282
|
+
self._uses_sizes = False
|
|
283
|
+
|
|
284
|
+
def _context(self, lines: list[str]) -> str:
|
|
285
|
+
"""Return the name of the context dictionary, creating it from the fields read so far if needed."""
|
|
286
|
+
if not self._has_context:
|
|
287
|
+
self._has_context = True
|
|
288
|
+
items = ", ".join(f"{name!r}: {var}" for name, var in self._assigned)
|
|
289
|
+
lines.append(f"values = {{{items}}}")
|
|
290
|
+
|
|
291
|
+
return "values"
|
|
292
|
+
|
|
293
|
+
def _assign(self, field: Field, value: str) -> str:
|
|
294
|
+
"""Assign a value to a new local variable for the given field, and the context dictionary if it exists."""
|
|
295
|
+
var = f"_value{len(self._assigned)}"
|
|
296
|
+
self._assigned.append((field._name, var))
|
|
297
|
+
if self._has_context:
|
|
298
|
+
return f"{var} = values[{field._name!r}] = {value}"
|
|
299
|
+
return f"{var} = {value}"
|
|
300
|
+
|
|
301
|
+
def _assign_local(self, field: Field, value: str) -> list[str]:
|
|
302
|
+
return [self._assign(field, value)]
|
|
303
|
+
|
|
304
|
+
def _fallback(self) -> str:
|
|
305
|
+
"""Return the symbol of the non-inlined fallback read function."""
|
|
306
|
+
if self._fallback_symbol is None:
|
|
307
|
+
fallback = _Fallback(self.cls, self.fields)
|
|
308
|
+
self._fallback_symbol = self._add_symbol(fallback, "fallback")
|
|
309
|
+
return self._fallback_symbol
|
|
310
|
+
|
|
281
311
|
def _generate_struct_source(self) -> str:
|
|
282
312
|
"""Generate the code for reading a structure."""
|
|
283
313
|
read_code = "\n".join(self._generate_fields_source())
|
|
@@ -405,6 +435,16 @@ class _ReadSourceGenerator:
|
|
|
405
435
|
|
|
406
436
|
# Everything else - basic and composite types (and arrays of them), and inlined structures
|
|
407
437
|
else:
|
|
438
|
+
if field.offset is None:
|
|
439
|
+
# The alignment depends on the position in the stream, so this needs a new block
|
|
440
|
+
if self.align:
|
|
441
|
+
yield from flush()
|
|
442
|
+
# A block is read in order, so a field before the current position needs a new block
|
|
443
|
+
elif current_offset is None or field.offset < current_offset:
|
|
444
|
+
yield from flush()
|
|
445
|
+
elif current_block:
|
|
446
|
+
current_offset = field.offset
|
|
447
|
+
|
|
408
448
|
if not current_block:
|
|
409
449
|
yield from align_to_field(field)
|
|
410
450
|
current_block.append(field)
|
|
@@ -456,18 +496,11 @@ class _ReadSourceGenerator:
|
|
|
456
496
|
element = self._type_symbol(field.type.__type__)
|
|
457
497
|
return f"_type_call({type_}, {element}._read_array(stream, _count, context={context}, endian=endian))"
|
|
458
498
|
|
|
459
|
-
def _starts_bit_value(self, field: Field) -> bool:
|
|
460
|
-
"""Return whether a bit field is read from a new value of the bit buffer."""
|
|
461
|
-
return self._bits is None or self._bits[1] == 0 or self._bits[0] is not _get_bits_type(field)
|
|
462
|
-
|
|
463
499
|
def _generate_bits_source(self, field: Field) -> Iterator[str]:
|
|
464
500
|
"""Generate the code for reading a bit field."""
|
|
465
501
|
read_type = _get_bits_type(field)
|
|
466
502
|
new_value = self._starts_bit_value(field)
|
|
467
|
-
|
|
468
|
-
self._bits = (read_type, read_type.__size__ * 8)
|
|
469
|
-
remaining = self._bits[1]
|
|
470
|
-
self._bits = (read_type, remaining - field.bits)
|
|
503
|
+
remaining = self._advance_bits(field)
|
|
471
504
|
|
|
472
505
|
lines = []
|
|
473
506
|
if new_value:
|
|
@@ -498,7 +531,7 @@ class _ReadSourceGenerator:
|
|
|
498
531
|
"""Generate the code for reading consecutive packed fields."""
|
|
499
532
|
unpack = _Unpack("data")
|
|
500
533
|
reads = []
|
|
501
|
-
size = self._pack_fields(fields, 0, unpack, reads, self._assign_local
|
|
534
|
+
size = self._pack_fields(fields, 0, unpack, reads, self._assign_local)
|
|
502
535
|
|
|
503
536
|
lines = [f"buf = stream.read({size})", _check_eof(size)]
|
|
504
537
|
if unpack.has_values():
|
|
@@ -513,7 +546,6 @@ class _ReadSourceGenerator:
|
|
|
513
546
|
unpack: _Unpack,
|
|
514
547
|
lines: list[str],
|
|
515
548
|
target: Callable[[Field, str], list[str]],
|
|
516
|
-
align: bool = False,
|
|
517
549
|
) -> int:
|
|
518
550
|
"""Generate the code for reading the given fields from ``buf``, starting at the given offset.
|
|
519
551
|
|
|
@@ -523,7 +555,7 @@ class _ReadSourceGenerator:
|
|
|
523
555
|
Returns the offset after the last field.
|
|
524
556
|
"""
|
|
525
557
|
inline = _inline_kind if self.inline else None
|
|
526
|
-
for field, count, char in _generate_struct_info(fields,
|
|
558
|
+
for field, count, char in _generate_struct_info(fields, inline=inline):
|
|
527
559
|
if field is None:
|
|
528
560
|
# Padding
|
|
529
561
|
unpack.add(count, "x")
|
|
@@ -595,16 +627,15 @@ class _ReadSourceGenerator:
|
|
|
595
627
|
lines.append(f"_object_setattr({var}, '_endian', endian)")
|
|
596
628
|
self._proxify(var, var, fields, lines)
|
|
597
629
|
|
|
598
|
-
def _proxify(self, union: str, var: str, fields: list[Field], lines: list[str]) -> None:
|
|
630
|
+
def _proxify(self, union: str, var: str, fields: list[Field], lines: list[str], attr: str | None = None) -> None:
|
|
599
631
|
"""Generate the code equivalent to ``Union._proxify``."""
|
|
600
632
|
for field in fields:
|
|
601
633
|
if issubclass(field.type, Structure):
|
|
602
634
|
nested = self._var("nested")
|
|
635
|
+
member = attr or field._name
|
|
603
636
|
lines.append(f"{nested} = _getattr({var}, {field._name!r})")
|
|
604
|
-
lines.append(
|
|
605
|
-
|
|
606
|
-
)
|
|
607
|
-
self._proxify(union, nested, field.type.__fields__, lines)
|
|
637
|
+
lines.append(f"_object_setattr({var}, {field._name!r}, _UnionProxy({union}, {member!r}, {nested}))")
|
|
638
|
+
self._proxify(union, nested, field.type.__fields__, lines, member)
|
|
608
639
|
|
|
609
640
|
def _generate_packed_field_source(
|
|
610
641
|
self,
|
|
@@ -622,19 +653,18 @@ class _ReadSourceGenerator:
|
|
|
622
653
|
array = self._type_symbol(field.type)
|
|
623
654
|
element = self._type_symbol(field_type.__type__)
|
|
624
655
|
|
|
625
|
-
if issubclass(
|
|
626
|
-
|
|
627
|
-
|
|
656
|
+
if issubclass(read_type, Int):
|
|
657
|
+
# Everything that's read as an integer type is sliced directly out of the buffer
|
|
658
|
+
elem_size = read_type.__size__
|
|
659
|
+
signed = bool(read_type.signed)
|
|
628
660
|
lines.append(f"_array_buf = {getter}")
|
|
629
661
|
raw = f"_int_from_bytes(_array_buf[i:i + {elem_size}], _byteorder[endian], signed={signed})"
|
|
630
|
-
|
|
631
|
-
|
|
662
|
+
getter = f"[{raw} for i in range(0, {count}, {elem_size})]"
|
|
663
|
+
|
|
664
|
+
if issubclass(field_type.__type__, Pointer):
|
|
632
665
|
context = self._context(lines)
|
|
633
666
|
item = f"{element}.__new__({element}, value, stream, context={context}, endian=endian)"
|
|
634
667
|
value = f"[{item} for value in {getter}]"
|
|
635
|
-
elif issubclass(read_type, (Wchar, Int)):
|
|
636
|
-
# Preserve the behaviour of the generic parsing path for these types
|
|
637
|
-
value = f"[{element}(value) for value in {getter}]"
|
|
638
668
|
else:
|
|
639
669
|
value = f"list(map(_type_call, _repeat({element}), {getter}))"
|
|
640
670
|
|
|
@@ -647,7 +677,7 @@ class _ReadSourceGenerator:
|
|
|
647
677
|
encoding = self._add_symbol(read_type.__encoding_map__, "encoding")
|
|
648
678
|
# Using str() instead of .decode() also supports other bytes-like objects returned by the stream
|
|
649
679
|
type_ = self._type_symbol(field.type)
|
|
650
|
-
lines.extend(target(field, f"_type_call({type_}, str({getter}, {encoding}[endian]))"))
|
|
680
|
+
lines.extend(target(field, f"_type_call({type_}, str({getter}, {encoding}[endian], 'surrogatepass'))"))
|
|
651
681
|
|
|
652
682
|
elif issubclass(field_type, Pointer):
|
|
653
683
|
context = self._context(lines)
|
|
@@ -694,8 +724,7 @@ class _Unpack:
|
|
|
694
724
|
|
|
695
725
|
def functions(self, name: str) -> dict[str, Callable]:
|
|
696
726
|
"""Return the given function of the struct for every endianness."""
|
|
697
|
-
|
|
698
|
-
return {endian: getattr(_struct(endian, fmt), name) for endian in _ENDIANNESS}
|
|
727
|
+
return _struct_functions(_optimize_struct_fmt(self.items), name)
|
|
699
728
|
|
|
700
729
|
|
|
701
730
|
def _check_union(cls: type[Union], fields: list[Field], inline: bool = True) -> int:
|
|
@@ -809,25 +838,18 @@ def _block(lines: list[str]) -> str:
|
|
|
809
838
|
|
|
810
839
|
def _generate_struct_info(
|
|
811
840
|
fields: list[Field],
|
|
812
|
-
align: bool = False,
|
|
813
841
|
inline: Callable[[type[BaseType]], str | None] | None = None,
|
|
814
842
|
) -> Iterator[tuple[Field, int, str]]:
|
|
815
843
|
if not fields:
|
|
816
844
|
return
|
|
817
845
|
|
|
818
846
|
current_offset = fields[0].offset
|
|
819
|
-
imaginary_offset = 0
|
|
820
847
|
for field in fields:
|
|
821
848
|
# We moved -- probably due to alignment
|
|
822
849
|
if field.offset is not None and (drift := field.offset - current_offset) > 0:
|
|
823
850
|
yield None, drift, "x"
|
|
824
851
|
current_offset += drift
|
|
825
852
|
|
|
826
|
-
if align and field.offset is None and (drift := -imaginary_offset & (field.alignment - 1)) > 0:
|
|
827
|
-
# Assume we started at a correctly aligned boundary
|
|
828
|
-
yield None, drift, "x"
|
|
829
|
-
imaginary_offset += drift
|
|
830
|
-
|
|
831
853
|
count = 1
|
|
832
854
|
read_type = _get_read_type(field.type)
|
|
833
855
|
|
|
@@ -853,25 +875,41 @@ def _generate_struct_info(
|
|
|
853
875
|
elif issubclass(read_type, (Char, Wchar, Int)):
|
|
854
876
|
yield field, count * read_type.__size__, "x"
|
|
855
877
|
|
|
856
|
-
size = count * read_type.__size__
|
|
857
|
-
imaginary_offset += size
|
|
858
878
|
if current_offset is not None:
|
|
859
|
-
current_offset +=
|
|
879
|
+
current_offset += count * read_type.__size__
|
|
860
880
|
|
|
861
881
|
|
|
862
882
|
def _optimize_struct_fmt(items: Iterable[tuple[int, str]]) -> str:
|
|
863
|
-
"""Create a struct format string from ``(count, char)`` items, merging consecutive items of the same char.
|
|
883
|
+
"""Create a struct format string from ``(count, char)`` items, merging consecutive items of the same char.
|
|
884
|
+
|
|
885
|
+
Byte strings aren't merged, since those are a single value regardless of their count.
|
|
886
|
+
"""
|
|
864
887
|
chars = []
|
|
865
888
|
for char, group in groupby(items, key=itemgetter(1)):
|
|
866
|
-
if
|
|
889
|
+
if char == "s":
|
|
890
|
+
chars.extend(f"{count}{char}" for count, _ in group)
|
|
891
|
+
elif count := sum(count for count, _ in group):
|
|
867
892
|
chars.append(f"{count if count > 1 else ''}{char}")
|
|
868
893
|
|
|
869
894
|
return "".join(chars)
|
|
870
895
|
|
|
871
896
|
|
|
897
|
+
def _struct_functions(fmt: str, name: str) -> dict[str, Callable]:
|
|
898
|
+
"""Return the given function of a struct with the given format for every endianness.
|
|
899
|
+
|
|
900
|
+
The ``@`` endianness uses the ``=`` format, since ``@`` would add padding between items to align them.
|
|
901
|
+
"""
|
|
902
|
+
return {endian: getattr(_struct("=" if endian == "@" else endian, fmt), name) for endian in _ENDIANNESS}
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _get_bit_buffer_type(field: Field) -> type[BaseType]:
|
|
906
|
+
"""Return the type that ``BitBuffer`` uses for a bit field, only bit fields of the same type share a value."""
|
|
907
|
+
return field.type.__type__ if issubclass(field.type, (Enum, Flag)) else field.type
|
|
908
|
+
|
|
909
|
+
|
|
872
910
|
def _get_bits_type(field: Field) -> type[BaseType]:
|
|
873
911
|
"""Return the type that ``BitBuffer`` reads a bit field as."""
|
|
874
|
-
type_ =
|
|
912
|
+
type_ = _get_bit_buffer_type(field)
|
|
875
913
|
return type_.__cs__.uint8 if issubclass(type_, Char) else type_
|
|
876
914
|
|
|
877
915
|
|
|
@@ -86,7 +86,7 @@ class cstruct:
|
|
|
86
86
|
"int24": self._make_int_type("int24", 3, True, alignment=4),
|
|
87
87
|
"uint24": self._make_int_type("uint24", 3, False, alignment=4),
|
|
88
88
|
"int48": self._make_int_type("int48", 6, True, alignment=8),
|
|
89
|
-
"uint48": self._make_int_type("
|
|
89
|
+
"uint48": self._make_int_type("uint48", 6, False, alignment=8),
|
|
90
90
|
"int128": self._make_int_type("int128", 16, True, alignment=16),
|
|
91
91
|
"uint128": self._make_int_type("uint128", 16, False, alignment=16),
|
|
92
92
|
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import functools
|
|
4
4
|
import warnings
|
|
5
5
|
from io import SEEK_CUR, BytesIO
|
|
6
|
+
from types import MethodType
|
|
6
7
|
from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, TypeVar
|
|
7
8
|
|
|
8
9
|
from dissect.cstruct.exception import ArraySizeError
|
|
@@ -19,6 +20,8 @@ if TYPE_CHECKING:
|
|
|
19
20
|
|
|
20
21
|
EOF = -0xE0F # Negative counts are illegal anyway, so abuse that for our EOF sentinel
|
|
21
22
|
|
|
23
|
+
_IMMUTABLE_TYPES = (int, float, bytes, str)
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
class MetaType(type):
|
|
24
27
|
"""Base metaclass for cstruct type classes."""
|
|
@@ -218,7 +221,8 @@ class MetaType(type):
|
|
|
218
221
|
array: The array to write.
|
|
219
222
|
endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
|
|
220
223
|
"""
|
|
221
|
-
|
|
224
|
+
write = cls._write
|
|
225
|
+
return sum(write(stream, entry, endian=endian) for entry in array)
|
|
222
226
|
|
|
223
227
|
def _write_0(cls, stream: BinaryIO, array: list[Self], *, endian: Endianness) -> int: # type: ignore
|
|
224
228
|
"""Internal function for writing null-terminated arrays.
|
|
@@ -295,20 +299,29 @@ class _overload:
|
|
|
295
299
|
b'\\x7b\\x00\\x00\\x00'
|
|
296
300
|
"""
|
|
297
301
|
|
|
298
|
-
def __init__(self, func: Callable[..., Any]) -> None:
|
|
302
|
+
def __init__(self, func: Callable[..., Any], method: Callable[..., Any]) -> None:
|
|
299
303
|
self.func = func
|
|
304
|
+
self.method = method
|
|
300
305
|
|
|
301
|
-
def __get__(self, instance: BaseType | None, owner: type[BaseType]) -> Callable[
|
|
306
|
+
def __get__(self, instance: BaseType | None, owner: type[BaseType]) -> Callable[..., Any]:
|
|
302
307
|
if instance is None:
|
|
303
308
|
return functools.partial(self.func, owner)
|
|
304
|
-
return
|
|
309
|
+
return MethodType(self.method, instance)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _instance_dumps(self: BaseType, *, endian: AllowedEndianness | None = None) -> bytes:
|
|
313
|
+
return MetaType.dumps(self.__class__, self, endian=endian)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _instance_write(self: BaseType, stream: BinaryIO, *, endian: AllowedEndianness | None = None) -> int:
|
|
317
|
+
return MetaType.write(self.__class__, stream, self, endian=endian)
|
|
305
318
|
|
|
306
319
|
|
|
307
320
|
class BaseType(metaclass=MetaType):
|
|
308
321
|
"""Base class for cstruct type classes."""
|
|
309
322
|
|
|
310
|
-
dumps = _overload(MetaType.dumps)
|
|
311
|
-
write = _overload(MetaType.write)
|
|
323
|
+
dumps = _overload(MetaType.dumps, _instance_dumps)
|
|
324
|
+
write = _overload(MetaType.write, _instance_write)
|
|
312
325
|
|
|
313
326
|
def __len__(self) -> int:
|
|
314
327
|
"""Return the byte size of the type."""
|
|
@@ -338,9 +351,10 @@ class BaseArray(BaseType):
|
|
|
338
351
|
|
|
339
352
|
@classmethod
|
|
340
353
|
def __default__(cls) -> BaseType:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
354
|
+
count = cls.__num_entries__ if isinstance(cls.__num_entries__, int) else 0
|
|
355
|
+
if isinstance(value := cls.__type__.__default__(), _IMMUTABLE_TYPES):
|
|
356
|
+
return type.__call__(cls, [value] * count)
|
|
357
|
+
return type.__call__(cls, [cls.__type__.__default__() for _ in range(count)])
|
|
344
358
|
|
|
345
359
|
@classmethod
|
|
346
360
|
def _read(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> list[BaseType]:
|
|
@@ -23,8 +23,9 @@ class CharArray(bytes, BaseArray):
|
|
|
23
23
|
return type.__call__(cls, super()._read(stream, context=context, endian=endian))
|
|
24
24
|
|
|
25
25
|
@classmethod
|
|
26
|
-
def _write(cls, stream: BinaryIO, data: bytes | str | list[int], *, endian: Endianness) -> int:
|
|
27
|
-
|
|
26
|
+
def _write(cls, stream: BinaryIO, data: bytes | bytearray | str | list[int], *, endian: Endianness) -> int:
|
|
27
|
+
# Copy mutable data types to bytes so we don't modify them in place
|
|
28
|
+
if isinstance(data, (list, bytearray)):
|
|
28
29
|
data = bytes(data)
|
|
29
30
|
elif isinstance(data, str):
|
|
30
31
|
data = data.encode("latin-1")
|
|
@@ -92,4 +93,7 @@ class Char(bytes, BaseType):
|
|
|
92
93
|
if isinstance(data, str):
|
|
93
94
|
data = data.encode("latin-1")
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
if (actual_size := len(data)) > 1:
|
|
97
|
+
raise ValueError(f"Expected a single character, got {actual_size} instead.")
|
|
98
|
+
|
|
99
|
+
return stream.write(data or b"\x00")
|