dissect.cstruct 5.0.dev12__py3-none-any.whl → 5.0.dev14__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.
- dissect/cstruct/compiler.py +559 -96
- dissect/cstruct/cstruct.py +1 -1
- dissect/cstruct/types/base.py +37 -11
- dissect/cstruct/types/char.py +7 -3
- dissect/cstruct/types/structure.py +31 -32
- dissect/cstruct/types/wchar.py +18 -7
- {dissect_cstruct-5.0.dev12.dist-info → dissect_cstruct-5.0.dev14.dist-info}/METADATA +1 -1
- {dissect_cstruct-5.0.dev12.dist-info → dissect_cstruct-5.0.dev14.dist-info}/RECORD +13 -13
- {dissect_cstruct-5.0.dev12.dist-info → dissect_cstruct-5.0.dev14.dist-info}/WHEEL +0 -0
- {dissect_cstruct-5.0.dev12.dist-info → dissect_cstruct-5.0.dev14.dist-info}/entry_points.txt +0 -0
- {dissect_cstruct-5.0.dev12.dist-info → dissect_cstruct-5.0.dev14.dist-info}/licenses/COPYRIGHT +0 -0
- {dissect_cstruct-5.0.dev12.dist-info → dissect_cstruct-5.0.dev14.dist-info}/licenses/LICENSE +0 -0
- {dissect_cstruct-5.0.dev12.dist-info → dissect_cstruct-5.0.dev14.dist-info}/top_level.txt +0 -0
dissect/cstruct/compiler.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import io
|
|
6
6
|
import logging
|
|
7
|
+
import struct
|
|
7
8
|
from enum import Enum
|
|
8
9
|
from itertools import groupby, repeat
|
|
9
10
|
from keyword import iskeyword
|
|
@@ -11,7 +12,7 @@ from operator import itemgetter
|
|
|
11
12
|
from textwrap import indent
|
|
12
13
|
from typing import TYPE_CHECKING, Any
|
|
13
14
|
|
|
14
|
-
from dissect.cstruct.exception import ExpressionParserError
|
|
15
|
+
from dissect.cstruct.exception import ArraySizeError, ExpressionParserError
|
|
15
16
|
from dissect.cstruct.expression import SOURCE_GLOBALS, Expression
|
|
16
17
|
from dissect.cstruct.types import (
|
|
17
18
|
Array,
|
|
@@ -30,7 +31,7 @@ from dissect.cstruct.types import (
|
|
|
30
31
|
WcharArray,
|
|
31
32
|
)
|
|
32
33
|
from dissect.cstruct.types.enum import EnumMetaType
|
|
33
|
-
from dissect.cstruct.types.structure import UnionMetaType, UnionProxy
|
|
34
|
+
from dissect.cstruct.types.structure import StructureMetaType, UnionMetaType, UnionProxy
|
|
34
35
|
from dissect.cstruct.util import ENDIANNESS_TO_BYTEORDER_MAP, _struct
|
|
35
36
|
|
|
36
37
|
if TYPE_CHECKING:
|
|
@@ -63,25 +64,33 @@ python_compile = compile
|
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
def compile_structure(cls: type[Structure]) -> type[Structure]:
|
|
66
|
-
"""Compile the ``_read``
|
|
67
|
+
"""Compile the ``_read`` and ``_write`` methods of a structure or union type, if the compiler supports it.
|
|
67
68
|
|
|
68
69
|
The compilation happens on first use.
|
|
69
70
|
"""
|
|
70
71
|
try:
|
|
71
72
|
_check_supported(cls, cls.__fields__)
|
|
72
73
|
cls._read = _LazyCompileRead(cls)
|
|
74
|
+
cls._write = _LazyCompileWrite(cls)
|
|
73
75
|
cls.__compiled__ = True
|
|
74
76
|
except Exception as e:
|
|
75
|
-
# Revert to the interpreted reader
|
|
77
|
+
# Revert to the interpreted reader and writer
|
|
76
78
|
log.debug("Failed to compile %s", cls, exc_info=e)
|
|
77
|
-
|
|
78
|
-
cls.__compiled__ = False
|
|
79
|
+
_revert_to_interpreted(cls)
|
|
79
80
|
|
|
80
81
|
return cls
|
|
81
82
|
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
def _revert_to_interpreted(cls: type[Structure]) -> None:
|
|
85
|
+
cls._read = classmethod(type(cls)._read)
|
|
86
|
+
cls._write = classmethod(type(cls)._write)
|
|
87
|
+
cls.__compiled__ = False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _LazyCompile:
|
|
91
|
+
"""Descriptor that compiles a method of a structure on first access and replaces itself with it."""
|
|
92
|
+
|
|
93
|
+
name: str
|
|
85
94
|
|
|
86
95
|
def __init__(self, cls: type[Structure]):
|
|
87
96
|
self.cls = cls
|
|
@@ -103,18 +112,47 @@ class _LazyCompileRead:
|
|
|
103
112
|
inline = all(type_.__dict__.get("__init__") is init for type_, init in self.snapshot.items())
|
|
104
113
|
|
|
105
114
|
try:
|
|
106
|
-
self.method =
|
|
115
|
+
self.method = self._generator(inline).generate()
|
|
107
116
|
except Exception as e:
|
|
108
|
-
# Revert to the slower
|
|
117
|
+
# Revert to the slower interpreted method
|
|
109
118
|
log.debug("Failed to compile %s", cls, exc_info=e)
|
|
110
|
-
self.method =
|
|
111
|
-
cls.__compiled__ = False
|
|
119
|
+
self.method = self._revert()
|
|
112
120
|
|
|
113
|
-
if cls.__dict__.get(
|
|
114
|
-
cls.
|
|
121
|
+
if cls.__dict__.get(self.name) is self:
|
|
122
|
+
setattr(cls, self.name, self.method)
|
|
115
123
|
|
|
116
124
|
return self.method
|
|
117
125
|
|
|
126
|
+
def _generator(self, inline: bool) -> _SourceGenerator:
|
|
127
|
+
raise NotImplementedError
|
|
128
|
+
|
|
129
|
+
def _revert(self) -> classmethod:
|
|
130
|
+
"""Return the interpreted version of the method."""
|
|
131
|
+
return classmethod(getattr(type(self.cls), self.name))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class _LazyCompileRead(_LazyCompile):
|
|
135
|
+
"""Descriptor that compiles the ``_read`` method of a structure on first access and replaces itself with it."""
|
|
136
|
+
|
|
137
|
+
name = "_read"
|
|
138
|
+
|
|
139
|
+
def _generator(self, inline: bool) -> _SourceGenerator:
|
|
140
|
+
return _ReadSourceGenerator(self.cls, self.fields, inline)
|
|
141
|
+
|
|
142
|
+
def _revert(self) -> classmethod:
|
|
143
|
+
# The structure no longer counts as compiled, so adding a field won't recompile _write either
|
|
144
|
+
_revert_to_interpreted(self.cls)
|
|
145
|
+
return super()._revert()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class _LazyCompileWrite(_LazyCompile):
|
|
149
|
+
"""Descriptor that compiles the ``_write`` method of a structure on first access and replaces itself with it."""
|
|
150
|
+
|
|
151
|
+
name = "_write"
|
|
152
|
+
|
|
153
|
+
def _generator(self, inline: bool) -> _SourceGenerator:
|
|
154
|
+
return _WriteSourceGenerator(self.cls, self.fields, inline)
|
|
155
|
+
|
|
118
156
|
|
|
119
157
|
def _snapshot_structures(fields: list[Field], result: dict[type, Any]) -> dict[type, Any]:
|
|
120
158
|
for field in fields:
|
|
@@ -166,7 +204,29 @@ _ENDIANNESS = ("<", ">", "!", "@", "=")
|
|
|
166
204
|
# Types of which the value can be used as an integer directly
|
|
167
205
|
_INTEGER_TYPES = (Int, Packed, Enum, Flag)
|
|
168
206
|
|
|
169
|
-
#
|
|
207
|
+
# Types of which the value can be packed, unless they have a custom way of writing
|
|
208
|
+
_PACKABLE_TYPES = (Array, Char, CharArray, Int, Packed, Pointer, Void, VoidArray, Wchar, WcharArray)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _field_bytes(type_: type[BaseType], value: Any, endian: str) -> bytes:
|
|
212
|
+
"""Return the bytes of a char or wchar field that isn't of the right type yet or doesn't fit."""
|
|
213
|
+
buf = type_.dumps(type_.__default__() if value is None else value, endian=endian)
|
|
214
|
+
if len(buf) > type_.__size__:
|
|
215
|
+
raise ArraySizeError(f"Expected {type_.__size__} bytes for {type_.__name__}, got {len(buf)} instead.")
|
|
216
|
+
return buf
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _array_values(type_: type[Array], value: list[Any] | None) -> list[Any]:
|
|
220
|
+
"""Return the values of an array field that doesn't have the right size yet."""
|
|
221
|
+
return type_.__default__() if value is None else type_._pad(value)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _or_default(value: Any, type_: type[BaseType]) -> Any:
|
|
225
|
+
"""Return the value of a field, or the default of its type if it's ``None``."""
|
|
226
|
+
return type_.__default__() if value is None else value
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Globals that are available to all generated functions
|
|
170
230
|
_GLOBALS = {
|
|
171
231
|
"_type_call": type.__call__,
|
|
172
232
|
"_object_new": object.__new__,
|
|
@@ -178,12 +238,20 @@ _GLOBALS = {
|
|
|
178
238
|
"_getattr": getattr,
|
|
179
239
|
"_UnionProxy": UnionProxy,
|
|
180
240
|
"_union_read": UnionMetaType._read,
|
|
241
|
+
"_struct_write": StructureMetaType._write,
|
|
242
|
+
"_union_write": UnionMetaType._write,
|
|
243
|
+
"_struct_error": struct.error,
|
|
244
|
+
"_field_bytes": _field_bytes,
|
|
245
|
+
"_array_values": _array_values,
|
|
246
|
+
"_or_default": _or_default,
|
|
181
247
|
**SOURCE_GLOBALS,
|
|
182
248
|
}
|
|
183
249
|
|
|
184
250
|
|
|
185
|
-
class
|
|
186
|
-
"""
|
|
251
|
+
class _SourceGenerator:
|
|
252
|
+
"""Base class for generating the source code of an optimized method of a structure or union."""
|
|
253
|
+
|
|
254
|
+
name: str
|
|
187
255
|
|
|
188
256
|
def __init__(self, cls: type[Structure], fields: list[Field], inline: bool = True):
|
|
189
257
|
self.cls = cls
|
|
@@ -191,24 +259,17 @@ class _ReadSourceGenerator:
|
|
|
191
259
|
self.align = cls.__align__
|
|
192
260
|
self.inline = inline and not self.align
|
|
193
261
|
|
|
262
|
+
# The values available to the generated code
|
|
263
|
+
self.symbols: dict[str, Any] = {"cs": cls.__cs__}
|
|
264
|
+
self._symbol_names: dict[int, str] = {}
|
|
265
|
+
|
|
194
266
|
# Witnesses of inlined structure types (symbol of the type -> symbol of its __init__ at compile time)
|
|
195
267
|
self._witnesses: dict[str, str] = {}
|
|
196
|
-
self._fallback_symbol: str | None = None
|
|
197
268
|
|
|
198
269
|
# The state of the bit buffer: the type of its current value and the remaining bits
|
|
199
270
|
self._bits: tuple[type[BaseType], int] | None = None
|
|
200
271
|
self._var_ids: dict[str, int] = {}
|
|
201
272
|
|
|
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
273
|
def _type_symbol(self, type_: type[BaseType]) -> str:
|
|
213
274
|
"""Add a type as symbol, returns its name."""
|
|
214
275
|
return self._add_symbol(type_, "type")
|
|
@@ -220,39 +281,12 @@ class _ReadSourceGenerator:
|
|
|
220
281
|
self.symbols[symbol] = value
|
|
221
282
|
return symbol
|
|
222
283
|
|
|
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
284
|
def _var(self, name: str) -> str:
|
|
233
285
|
"""Return a unique variable name for the generated code."""
|
|
234
286
|
index = self._var_ids.get(name, 0)
|
|
235
287
|
self._var_ids[name] = index + 1
|
|
236
288
|
return f"_{name}{index}"
|
|
237
289
|
|
|
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
290
|
def _witness(self, type_: type[Structure]) -> str:
|
|
257
291
|
"""Add an inlined type as symbol, with a witness to detect modifications to it after compilation."""
|
|
258
292
|
symbol = self._type_symbol(type_)
|
|
@@ -263,10 +297,22 @@ class _ReadSourceGenerator:
|
|
|
263
297
|
"""Return the conditions that are true if any of the inlined types were modified after compilation."""
|
|
264
298
|
return [f"{type_}.__init__ is not {init}" for type_, init in self._witnesses.items()]
|
|
265
299
|
|
|
300
|
+
def _starts_bit_value(self, field: Field) -> bool:
|
|
301
|
+
"""Return whether a bit field is the first of a new value of the bit buffer."""
|
|
302
|
+
return self._bits is None or self._bits[1] == 0 or self._bits[0] is not _get_bit_buffer_type(field)
|
|
303
|
+
|
|
304
|
+
def _advance_bits(self, field: Field) -> int:
|
|
305
|
+
"""Update the state of the bit buffer for a bit field, returns the bits that were remaining before it."""
|
|
306
|
+
if self._starts_bit_value(field):
|
|
307
|
+
self._bits = (_get_bit_buffer_type(field), _get_bits_type(field).__size__ * 8)
|
|
308
|
+
remaining = self._bits[1]
|
|
309
|
+
self._bits = (self._bits[0], remaining - field.bits)
|
|
310
|
+
return remaining
|
|
311
|
+
|
|
266
312
|
def generate(self) -> MethodType:
|
|
267
313
|
source = self.generate_source()
|
|
268
314
|
|
|
269
|
-
code = python_compile(source, f"<compiled {self.cls.__name__}.
|
|
315
|
+
code = python_compile(source, f"<compiled {self.cls.__name__}.{self.name}>", "exec")
|
|
270
316
|
exec(code, {**_GLOBALS, **self.symbols}, d := {})
|
|
271
317
|
obj = d.popitem()[1]
|
|
272
318
|
obj.__source__ = source
|
|
@@ -278,6 +324,56 @@ class _ReadSourceGenerator:
|
|
|
278
324
|
return self._generate_union_source()
|
|
279
325
|
return self._generate_struct_source()
|
|
280
326
|
|
|
327
|
+
def _generate_struct_source(self) -> str:
|
|
328
|
+
raise NotImplementedError
|
|
329
|
+
|
|
330
|
+
def _generate_union_source(self) -> str:
|
|
331
|
+
raise NotImplementedError
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class _ReadSourceGenerator(_SourceGenerator):
|
|
335
|
+
"""Generates the source code of an optimized ``_read`` method for a structure or union."""
|
|
336
|
+
|
|
337
|
+
name = "_read"
|
|
338
|
+
|
|
339
|
+
def __init__(self, cls: type[Structure], fields: list[Field], inline: bool = True):
|
|
340
|
+
super().__init__(cls, fields, inline)
|
|
341
|
+
|
|
342
|
+
self._fallback_symbol: str | None = None
|
|
343
|
+
|
|
344
|
+
# (field name, local variable name) of all fields assigned so far
|
|
345
|
+
self._assigned: list[tuple[str, str]] = []
|
|
346
|
+
self._has_context = False
|
|
347
|
+
self._uses_start = False
|
|
348
|
+
self._uses_sizes = False
|
|
349
|
+
|
|
350
|
+
def _context(self, lines: list[str]) -> str:
|
|
351
|
+
"""Return the name of the context dictionary, creating it from the fields read so far if needed."""
|
|
352
|
+
if not self._has_context:
|
|
353
|
+
self._has_context = True
|
|
354
|
+
items = ", ".join(f"{name!r}: {var}" for name, var in self._assigned)
|
|
355
|
+
lines.append(f"values = {{{items}}}")
|
|
356
|
+
|
|
357
|
+
return "values"
|
|
358
|
+
|
|
359
|
+
def _assign(self, field: Field, value: str) -> str:
|
|
360
|
+
"""Assign a value to a new local variable for the given field, and the context dictionary if it exists."""
|
|
361
|
+
var = f"_value{len(self._assigned)}"
|
|
362
|
+
self._assigned.append((field._name, var))
|
|
363
|
+
if self._has_context:
|
|
364
|
+
return f"{var} = values[{field._name!r}] = {value}"
|
|
365
|
+
return f"{var} = {value}"
|
|
366
|
+
|
|
367
|
+
def _assign_local(self, field: Field, value: str) -> list[str]:
|
|
368
|
+
return [self._assign(field, value)]
|
|
369
|
+
|
|
370
|
+
def _fallback(self) -> str:
|
|
371
|
+
"""Return the symbol of the non-inlined fallback read function."""
|
|
372
|
+
if self._fallback_symbol is None:
|
|
373
|
+
fallback = _Fallback(self.cls, self.fields)
|
|
374
|
+
self._fallback_symbol = self._add_symbol(fallback, "fallback")
|
|
375
|
+
return self._fallback_symbol
|
|
376
|
+
|
|
281
377
|
def _generate_struct_source(self) -> str:
|
|
282
378
|
"""Generate the code for reading a structure."""
|
|
283
379
|
read_code = "\n".join(self._generate_fields_source())
|
|
@@ -405,6 +501,16 @@ class _ReadSourceGenerator:
|
|
|
405
501
|
|
|
406
502
|
# Everything else - basic and composite types (and arrays of them), and inlined structures
|
|
407
503
|
else:
|
|
504
|
+
if field.offset is None:
|
|
505
|
+
# The alignment depends on the position in the stream, so this needs a new block
|
|
506
|
+
if self.align:
|
|
507
|
+
yield from flush()
|
|
508
|
+
# A block is read in order, so a field before the current position needs a new block
|
|
509
|
+
elif current_offset is None or field.offset < current_offset:
|
|
510
|
+
yield from flush()
|
|
511
|
+
elif current_block:
|
|
512
|
+
current_offset = field.offset
|
|
513
|
+
|
|
408
514
|
if not current_block:
|
|
409
515
|
yield from align_to_field(field)
|
|
410
516
|
current_block.append(field)
|
|
@@ -456,18 +562,11 @@ class _ReadSourceGenerator:
|
|
|
456
562
|
element = self._type_symbol(field.type.__type__)
|
|
457
563
|
return f"_type_call({type_}, {element}._read_array(stream, _count, context={context}, endian=endian))"
|
|
458
564
|
|
|
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
565
|
def _generate_bits_source(self, field: Field) -> Iterator[str]:
|
|
464
566
|
"""Generate the code for reading a bit field."""
|
|
465
567
|
read_type = _get_bits_type(field)
|
|
466
568
|
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)
|
|
569
|
+
remaining = self._advance_bits(field)
|
|
471
570
|
|
|
472
571
|
lines = []
|
|
473
572
|
if new_value:
|
|
@@ -498,7 +597,7 @@ class _ReadSourceGenerator:
|
|
|
498
597
|
"""Generate the code for reading consecutive packed fields."""
|
|
499
598
|
unpack = _Unpack("data")
|
|
500
599
|
reads = []
|
|
501
|
-
size = self._pack_fields(fields, 0, unpack, reads, self._assign_local
|
|
600
|
+
size = self._pack_fields(fields, 0, unpack, reads, self._assign_local)
|
|
502
601
|
|
|
503
602
|
lines = [f"buf = stream.read({size})", _check_eof(size)]
|
|
504
603
|
if unpack.has_values():
|
|
@@ -513,7 +612,6 @@ class _ReadSourceGenerator:
|
|
|
513
612
|
unpack: _Unpack,
|
|
514
613
|
lines: list[str],
|
|
515
614
|
target: Callable[[Field, str], list[str]],
|
|
516
|
-
align: bool = False,
|
|
517
615
|
) -> int:
|
|
518
616
|
"""Generate the code for reading the given fields from ``buf``, starting at the given offset.
|
|
519
617
|
|
|
@@ -523,7 +621,7 @@ class _ReadSourceGenerator:
|
|
|
523
621
|
Returns the offset after the last field.
|
|
524
622
|
"""
|
|
525
623
|
inline = _inline_kind if self.inline else None
|
|
526
|
-
for field, count, char in _generate_struct_info(fields,
|
|
624
|
+
for field, count, char in _generate_struct_info(fields, inline=inline):
|
|
527
625
|
if field is None:
|
|
528
626
|
# Padding
|
|
529
627
|
unpack.add(count, "x")
|
|
@@ -595,16 +693,15 @@ class _ReadSourceGenerator:
|
|
|
595
693
|
lines.append(f"_object_setattr({var}, '_endian', endian)")
|
|
596
694
|
self._proxify(var, var, fields, lines)
|
|
597
695
|
|
|
598
|
-
def _proxify(self, union: str, var: str, fields: list[Field], lines: list[str]) -> None:
|
|
696
|
+
def _proxify(self, union: str, var: str, fields: list[Field], lines: list[str], attr: str | None = None) -> None:
|
|
599
697
|
"""Generate the code equivalent to ``Union._proxify``."""
|
|
600
698
|
for field in fields:
|
|
601
699
|
if issubclass(field.type, Structure):
|
|
602
700
|
nested = self._var("nested")
|
|
701
|
+
member = attr or field._name
|
|
603
702
|
lines.append(f"{nested} = _getattr({var}, {field._name!r})")
|
|
604
|
-
lines.append(
|
|
605
|
-
|
|
606
|
-
)
|
|
607
|
-
self._proxify(union, nested, field.type.__fields__, lines)
|
|
703
|
+
lines.append(f"_object_setattr({var}, {field._name!r}, _UnionProxy({union}, {member!r}, {nested}))")
|
|
704
|
+
self._proxify(union, nested, field.type.__fields__, lines, member)
|
|
608
705
|
|
|
609
706
|
def _generate_packed_field_source(
|
|
610
707
|
self,
|
|
@@ -622,19 +719,18 @@ class _ReadSourceGenerator:
|
|
|
622
719
|
array = self._type_symbol(field.type)
|
|
623
720
|
element = self._type_symbol(field_type.__type__)
|
|
624
721
|
|
|
625
|
-
if issubclass(
|
|
626
|
-
|
|
627
|
-
|
|
722
|
+
if issubclass(read_type, Int):
|
|
723
|
+
# Everything that's read as an integer type is sliced directly out of the buffer
|
|
724
|
+
elem_size = read_type.__size__
|
|
725
|
+
signed = bool(read_type.signed)
|
|
628
726
|
lines.append(f"_array_buf = {getter}")
|
|
629
727
|
raw = f"_int_from_bytes(_array_buf[i:i + {elem_size}], _byteorder[endian], signed={signed})"
|
|
630
|
-
|
|
631
|
-
|
|
728
|
+
getter = f"[{raw} for i in range(0, {count}, {elem_size})]"
|
|
729
|
+
|
|
730
|
+
if issubclass(field_type.__type__, Pointer):
|
|
632
731
|
context = self._context(lines)
|
|
633
732
|
item = f"{element}.__new__({element}, value, stream, context={context}, endian=endian)"
|
|
634
733
|
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
734
|
else:
|
|
639
735
|
value = f"list(map(_type_call, _repeat({element}), {getter}))"
|
|
640
736
|
|
|
@@ -647,7 +743,7 @@ class _ReadSourceGenerator:
|
|
|
647
743
|
encoding = self._add_symbol(read_type.__encoding_map__, "encoding")
|
|
648
744
|
# Using str() instead of .decode() also supports other bytes-like objects returned by the stream
|
|
649
745
|
type_ = self._type_symbol(field.type)
|
|
650
|
-
lines.extend(target(field, f"_type_call({type_}, str({getter}, {encoding}[endian]))"))
|
|
746
|
+
lines.extend(target(field, f"_type_call({type_}, str({getter}, {encoding}[endian], 'surrogatepass'))"))
|
|
651
747
|
|
|
652
748
|
elif issubclass(field_type, Pointer):
|
|
653
749
|
context = self._context(lines)
|
|
@@ -694,8 +790,320 @@ class _Unpack:
|
|
|
694
790
|
|
|
695
791
|
def functions(self, name: str) -> dict[str, Callable]:
|
|
696
792
|
"""Return the given function of the struct for every endianness."""
|
|
697
|
-
|
|
698
|
-
|
|
793
|
+
return _struct_functions(_optimize_struct_fmt(self.items), name)
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
class _WriteSourceGenerator(_SourceGenerator):
|
|
797
|
+
"""Generates the source code of an optimized ``_write`` method for a structure or union."""
|
|
798
|
+
|
|
799
|
+
name = "_write"
|
|
800
|
+
|
|
801
|
+
def __init__(self, cls: type[Structure], fields: list[Field], inline: bool = True):
|
|
802
|
+
super().__init__(cls, fields, inline)
|
|
803
|
+
|
|
804
|
+
self._uses_start = False
|
|
805
|
+
|
|
806
|
+
def _generate_method_source(self, interpreted: str, lines: Iterable[str]) -> str:
|
|
807
|
+
"""Generate the code of the method, ``interpreted`` is the name of the method to fall back to."""
|
|
808
|
+
write_code = "\n".join(lines)
|
|
809
|
+
|
|
810
|
+
# Fall back to the generic method if any of the inlined types was modified after compilation
|
|
811
|
+
checks = [f"cls is not {self._add_symbol(self.cls, 'cls')}", *self._modified_checks()]
|
|
812
|
+
|
|
813
|
+
preamble = [f"if {' or '.join(checks)}: return {interpreted}(cls, stream, data, endian=endian)"]
|
|
814
|
+
if self._uses_start:
|
|
815
|
+
preamble.append("start = stream.tell()")
|
|
816
|
+
|
|
817
|
+
code = indent("\n".join([*preamble, write_code]), " ")
|
|
818
|
+
|
|
819
|
+
return f"def _write(cls, stream, data, *, endian):\n{code}"
|
|
820
|
+
|
|
821
|
+
def _generate_struct_source(self) -> str:
|
|
822
|
+
"""Generate the code for writing a structure."""
|
|
823
|
+
return self._generate_method_source("_struct_write", list(self._generate_fields_source()))
|
|
824
|
+
|
|
825
|
+
def _generate_union_source(self) -> str:
|
|
826
|
+
"""Generate the code for writing a union."""
|
|
827
|
+
if not _can_pack(self.cls):
|
|
828
|
+
raise TypeError(f"Unsupported union for compiler: {self.cls}")
|
|
829
|
+
|
|
830
|
+
pack = _Pack()
|
|
831
|
+
self._pack_union("data", self.cls, pack)
|
|
832
|
+
|
|
833
|
+
lines = [self._generate_pack_source(pack), f"return {self.cls.__size__}"]
|
|
834
|
+
return self._generate_method_source("_union_write", lines)
|
|
835
|
+
|
|
836
|
+
def _generate_fields_source(self) -> Iterator[str]:
|
|
837
|
+
"""Generate the code for writing all fields."""
|
|
838
|
+
# The offset of the current field in the structure, None if it's unknown due to a dynamic field
|
|
839
|
+
current_offset = 0
|
|
840
|
+
current_block: list[Field] = []
|
|
841
|
+
current_bits: list[Field] = []
|
|
842
|
+
|
|
843
|
+
def flush() -> Iterator[str]:
|
|
844
|
+
if current_block:
|
|
845
|
+
yield from self._generate_packed_source(current_block)
|
|
846
|
+
current_block[:] = []
|
|
847
|
+
if current_bits:
|
|
848
|
+
yield from self._generate_bits_source(current_bits)
|
|
849
|
+
current_bits[:] = []
|
|
850
|
+
|
|
851
|
+
def align_to_field(field: Field) -> Iterator[str]:
|
|
852
|
+
nonlocal current_offset
|
|
853
|
+
|
|
854
|
+
if field.offset is not None and field.offset != current_offset:
|
|
855
|
+
if current_offset is None:
|
|
856
|
+
# This only writes padding if the stream is before the offset, otherwise the amount is negative
|
|
857
|
+
yield f'stream.write(b"\\x00" * (start + {field.offset} - stream.tell()))'
|
|
858
|
+
else:
|
|
859
|
+
yield f"stream.write({bytes(field.offset - current_offset)!r})"
|
|
860
|
+
current_offset = field.offset
|
|
861
|
+
|
|
862
|
+
if self.align and field.offset is None:
|
|
863
|
+
yield f'stream.write(b"\\x00" * (-stream.tell() & ({field.alignment} - 1)))'
|
|
864
|
+
|
|
865
|
+
for field in self.fields:
|
|
866
|
+
if not field.bits:
|
|
867
|
+
self._bits = None
|
|
868
|
+
if current_bits:
|
|
869
|
+
yield from flush()
|
|
870
|
+
|
|
871
|
+
# Unlike reading, we can't move back in the stream
|
|
872
|
+
if None not in (field.offset, current_offset) and field.offset < current_offset:
|
|
873
|
+
raise TypeError(f"Unsupported offset for compiler: {field}")
|
|
874
|
+
|
|
875
|
+
size = field.type.__size__
|
|
876
|
+
|
|
877
|
+
# Bit fields
|
|
878
|
+
if field.bits:
|
|
879
|
+
if self._starts_bit_value(field):
|
|
880
|
+
if not _can_pack_bits(field):
|
|
881
|
+
raise TypeError(f"Unsupported type for bit field: {field.type}")
|
|
882
|
+
|
|
883
|
+
yield from flush()
|
|
884
|
+
yield from align_to_field(field)
|
|
885
|
+
elif field.offset is not None:
|
|
886
|
+
raise TypeError(f"Unsupported offset for bit field: {field}")
|
|
887
|
+
else:
|
|
888
|
+
# Only the first bit field of a value moves the stream
|
|
889
|
+
size = 0
|
|
890
|
+
|
|
891
|
+
self._advance_bits(field)
|
|
892
|
+
current_bits.append(field)
|
|
893
|
+
|
|
894
|
+
# Basic and composite types (and arrays of them), and inlined structures
|
|
895
|
+
elif _can_pack(field.type):
|
|
896
|
+
if field.offset is None:
|
|
897
|
+
# The alignment depends on the position in the stream, so this needs a new block
|
|
898
|
+
if self.align:
|
|
899
|
+
yield from flush()
|
|
900
|
+
elif current_offset is None:
|
|
901
|
+
yield from flush()
|
|
902
|
+
elif current_block:
|
|
903
|
+
current_offset = field.offset
|
|
904
|
+
|
|
905
|
+
if not current_block:
|
|
906
|
+
yield from align_to_field(field)
|
|
907
|
+
current_block.append(field)
|
|
908
|
+
|
|
909
|
+
# Everything else - sub structures, arrays of structures, multi-dimensional and dynamic arrays
|
|
910
|
+
else:
|
|
911
|
+
yield from flush()
|
|
912
|
+
yield from align_to_field(field)
|
|
913
|
+
yield from self._generate_field_source(field)
|
|
914
|
+
|
|
915
|
+
# An aligned structure writes more than its size if the stream isn't aligned
|
|
916
|
+
if self.align:
|
|
917
|
+
size = None
|
|
918
|
+
|
|
919
|
+
if current_offset is not None:
|
|
920
|
+
current_offset = None if size is None else current_offset + size
|
|
921
|
+
|
|
922
|
+
yield from flush()
|
|
923
|
+
|
|
924
|
+
if self.align:
|
|
925
|
+
self._uses_start = True
|
|
926
|
+
yield 'stream.write(b"\\x00" * (-stream.tell() & (cls.__alignment__ - 1)))'
|
|
927
|
+
|
|
928
|
+
yield "return stream.tell() - start" if self._uses_start else f"return {current_offset}"
|
|
929
|
+
|
|
930
|
+
def _generate_field_source(self, field: Field) -> Iterator[str]:
|
|
931
|
+
"""Generate the code for writing a field using its type."""
|
|
932
|
+
self._uses_start = True
|
|
933
|
+
type_ = self._type_symbol(field.type)
|
|
934
|
+
lines = [
|
|
935
|
+
f"_value = {_get_attribute('data', field._name)}",
|
|
936
|
+
f"if _value is None: _value = {type_}.__default__()",
|
|
937
|
+
f"{type_}._write(stream, _value, endian=endian)",
|
|
938
|
+
]
|
|
939
|
+
|
|
940
|
+
yield _block(lines)
|
|
941
|
+
|
|
942
|
+
def _generate_bits_source(self, fields: list[Field]) -> Iterator[str]:
|
|
943
|
+
"""Generate the code for writing the bit fields that share a value of the bit buffer."""
|
|
944
|
+
type_ = _get_bits_type(fields[0])
|
|
945
|
+
|
|
946
|
+
pack = _Pack()
|
|
947
|
+
little = []
|
|
948
|
+
other = []
|
|
949
|
+
remaining = type_.__size__ * 8
|
|
950
|
+
for field in fields:
|
|
951
|
+
value = self._load_value(field, _get_attribute("data", field._name), pack)
|
|
952
|
+
if issubclass(field.type, (Enum, Flag)):
|
|
953
|
+
value += "._value_"
|
|
954
|
+
|
|
955
|
+
remaining -= field.bits
|
|
956
|
+
little.append(f"{value} << {type_.__size__ * 8 - remaining - field.bits}")
|
|
957
|
+
other.append(f"{value} << {remaining}")
|
|
958
|
+
|
|
959
|
+
# The bit buffer only writes the least significant bits first for explicit little endian
|
|
960
|
+
self._pack_number(type_, f"({' | '.join(little)} if endian == '<' else {' | '.join(other)})", pack)
|
|
961
|
+
|
|
962
|
+
yield self._generate_pack_source(pack)
|
|
963
|
+
|
|
964
|
+
def _generate_packed_source(self, fields: list[Field]) -> Iterator[str]:
|
|
965
|
+
"""Generate the code for writing consecutive packed fields."""
|
|
966
|
+
pack = _Pack()
|
|
967
|
+
self._pack_fields(fields, "data", pack)
|
|
968
|
+
|
|
969
|
+
if pack.items:
|
|
970
|
+
yield self._generate_pack_source(pack)
|
|
971
|
+
|
|
972
|
+
def _generate_pack_source(self, pack: _Pack) -> str:
|
|
973
|
+
"""Generate the code for checking, packing and writing values."""
|
|
974
|
+
function = f"{self._add_symbol(pack.functions('pack'), 'pack')}[endian]"
|
|
975
|
+
if pack.values == pack.retry:
|
|
976
|
+
return _block([*pack.lines, f"stream.write({function}({', '.join(pack.values)}))"])
|
|
977
|
+
|
|
978
|
+
# Numbers aren't checked for None up front, since struct.pack raises an error for those
|
|
979
|
+
return _block(
|
|
980
|
+
[
|
|
981
|
+
*pack.lines,
|
|
982
|
+
"try:",
|
|
983
|
+
f" buf = {function}({', '.join(pack.values)})",
|
|
984
|
+
"except _struct_error:",
|
|
985
|
+
f" buf = {function}({', '.join(pack.retry)})",
|
|
986
|
+
"stream.write(buf)",
|
|
987
|
+
]
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
def _load_value(self, field: Field, value: str, pack: _Pack) -> str:
|
|
991
|
+
"""Generate the code for loading a value, using the default of its type if it's ``None``."""
|
|
992
|
+
var = self._var("value")
|
|
993
|
+
pack.lines.append(f"if ({var} := {value}) is None: {var} = {self._type_symbol(field.type)}.__default__()")
|
|
994
|
+
return var
|
|
995
|
+
|
|
996
|
+
def _pack_fields(self, fields: list[Field], var: str, pack: _Pack, proxied: bool = False) -> None:
|
|
997
|
+
"""Generate the code for packing the given fields of the object in ``var``.
|
|
998
|
+
|
|
999
|
+
The fields of objects in a union are proxied, in which case we use the object behind the proxy.
|
|
1000
|
+
"""
|
|
1001
|
+
for field, count, char in _generate_struct_info(fields, inline=_inline_kind):
|
|
1002
|
+
if field is None:
|
|
1003
|
+
# Padding
|
|
1004
|
+
pack.add(count, "x")
|
|
1005
|
+
continue
|
|
1006
|
+
|
|
1007
|
+
value = _get_attribute(var, field._name)
|
|
1008
|
+
|
|
1009
|
+
if char in ("S", "U"):
|
|
1010
|
+
# Inlined structure or union
|
|
1011
|
+
self._witness(field.type)
|
|
1012
|
+
nested = self._load_value(field, value, pack)
|
|
1013
|
+
if proxied:
|
|
1014
|
+
pack.lines.append(f"if type({nested}) is _UnionProxy: {nested} = {nested}.__target__")
|
|
1015
|
+
|
|
1016
|
+
if char == "S":
|
|
1017
|
+
self._pack_fields(field.type.__fields__, nested, pack, proxied)
|
|
1018
|
+
else:
|
|
1019
|
+
self._pack_union(nested, field.type, pack)
|
|
1020
|
+
continue
|
|
1021
|
+
|
|
1022
|
+
self._pack_field(field, value, count, char, pack)
|
|
1023
|
+
|
|
1024
|
+
def _pack_union(self, var: str, type_: type[Union], pack: _Pack) -> None:
|
|
1025
|
+
"""Generate the code for packing the union in ``var``, by the field the interpreted writer uses."""
|
|
1026
|
+
# Changing a nested structure can change which field is the largest
|
|
1027
|
+
for field in type_.__fields__:
|
|
1028
|
+
if issubclass(field.type, Structure):
|
|
1029
|
+
self._witness(field.type)
|
|
1030
|
+
|
|
1031
|
+
self._pack_fields([type_._write_field()], var, pack, proxied=True)
|
|
1032
|
+
|
|
1033
|
+
def _pack_field(self, field: Field, value: str, count: int, char: str, pack: _Pack) -> None:
|
|
1034
|
+
"""Generate the code for checking and packing a field."""
|
|
1035
|
+
field_type = field.type
|
|
1036
|
+
read_type = _get_read_type(field_type)
|
|
1037
|
+
type_ = self._type_symbol(field_type)
|
|
1038
|
+
|
|
1039
|
+
if issubclass(field_type, Array):
|
|
1040
|
+
read_type = _get_read_type(field_type.__type__)
|
|
1041
|
+
var = self._var("value")
|
|
1042
|
+
num = field_type.__num_entries__
|
|
1043
|
+
pack.lines.append(
|
|
1044
|
+
f"if ({var} := {value}) is None or len({var}) != {num}: {var} = _array_values({type_}, {var})"
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
# Entries can be enum members or plain integers, both can be packed as they are
|
|
1048
|
+
if issubclass(read_type, Int):
|
|
1049
|
+
pack.add(count, "s", f"b''.join([{_int_to_bytes(read_type, 'item')} for item in {var}])")
|
|
1050
|
+
else:
|
|
1051
|
+
pack.add(count, char, f"*{var}")
|
|
1052
|
+
|
|
1053
|
+
elif issubclass(field_type, (Char, CharArray, Wchar, WcharArray)):
|
|
1054
|
+
var = self._var("value")
|
|
1055
|
+
|
|
1056
|
+
# The format pads values that are too short with zeroes
|
|
1057
|
+
if issubclass(field_type, (Wchar, WcharArray)):
|
|
1058
|
+
encoding = self._add_symbol(Wchar.__encoding_map__, "encoding")
|
|
1059
|
+
pack.lines.append(
|
|
1060
|
+
f"if ({var} := {value}) is None or "
|
|
1061
|
+
f"len({var} := {var}.encode({encoding}[endian], 'surrogatepass')) > {count}: "
|
|
1062
|
+
f"{var} = _field_bytes({type_}, {value}, endian)"
|
|
1063
|
+
)
|
|
1064
|
+
else:
|
|
1065
|
+
pack.lines.append(
|
|
1066
|
+
f"if not isinstance({var} := {value}, bytes) or len({var}) > {count}: "
|
|
1067
|
+
f"{var} = _field_bytes({type_}, {var}, endian)"
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
pack.add(count, "s", var)
|
|
1071
|
+
|
|
1072
|
+
elif issubclass(field_type, (Enum, Flag)):
|
|
1073
|
+
self._pack_number(read_type, f"{self._load_value(field, value, pack)}._value_", pack)
|
|
1074
|
+
|
|
1075
|
+
elif issubclass(read_type, Packed):
|
|
1076
|
+
pack.add(1, read_type.__fmt__, value, f"_or_default({value}, {type_})")
|
|
1077
|
+
|
|
1078
|
+
else:
|
|
1079
|
+
self._pack_number(read_type, self._load_value(field, value, pack), pack)
|
|
1080
|
+
|
|
1081
|
+
def _pack_number(self, type_: type[BaseType], value: str, pack: _Pack) -> None:
|
|
1082
|
+
"""Generate the code for packing a value of an integer or packed type."""
|
|
1083
|
+
if issubclass(type_, Int):
|
|
1084
|
+
pack.add(type_.__size__, "s", _int_to_bytes(type_, value))
|
|
1085
|
+
else:
|
|
1086
|
+
pack.add(1, type_.__fmt__, value)
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
class _Pack:
|
|
1090
|
+
"""Tracks consecutive struct pack formats, and the code for the values to pack."""
|
|
1091
|
+
|
|
1092
|
+
def __init__(self):
|
|
1093
|
+
self.items: list[tuple[int, str]] = []
|
|
1094
|
+
self.lines: list[str] = []
|
|
1095
|
+
self.values: list[str] = []
|
|
1096
|
+
self.retry: list[str] = []
|
|
1097
|
+
|
|
1098
|
+
def add(self, count: int, char: str, value: str | None = None, retry: str | None = None) -> None:
|
|
1099
|
+
self.items.append((count, char))
|
|
1100
|
+
if value is not None:
|
|
1101
|
+
self.values.append(value)
|
|
1102
|
+
self.retry.append(retry or value)
|
|
1103
|
+
|
|
1104
|
+
def functions(self, name: str) -> dict[str, Callable]:
|
|
1105
|
+
"""Return the given function of the struct for every endianness."""
|
|
1106
|
+
return _struct_functions(_optimize_struct_fmt(self.items), name)
|
|
699
1107
|
|
|
700
1108
|
|
|
701
1109
|
def _check_union(cls: type[Union], fields: list[Field], inline: bool = True) -> int:
|
|
@@ -767,6 +1175,52 @@ def _uses_generated_init(type_: type[Structure]) -> bool:
|
|
|
767
1175
|
return "__init__" in type_.__dict__ and type_.__new__ is object.__new__
|
|
768
1176
|
|
|
769
1177
|
|
|
1178
|
+
def _can_pack(type_: type[BaseType]) -> bool:
|
|
1179
|
+
"""Determine if a value of a type can be written by packing it, instead of by using the ``_write`` method."""
|
|
1180
|
+
if type_.__size__ is None:
|
|
1181
|
+
return False
|
|
1182
|
+
|
|
1183
|
+
if issubclass(type_, (Enum, Flag)):
|
|
1184
|
+
return _can_pack(type_.__type__)
|
|
1185
|
+
|
|
1186
|
+
if issubclass(type_, Structure):
|
|
1187
|
+
kind = _inline_kind(type_)
|
|
1188
|
+
if kind == "U":
|
|
1189
|
+
field = type_._write_field()
|
|
1190
|
+
return field is not None and bool(field.type.__size__) and _can_pack(field.type)
|
|
1191
|
+
return kind == "S" and all(_can_pack(field.type) for field in type_.__fields__)
|
|
1192
|
+
|
|
1193
|
+
if issubclass(type_, Array) and (
|
|
1194
|
+
issubclass(type_.__type__, (Structure, BaseArray)) or not _can_pack(type_.__type__)
|
|
1195
|
+
):
|
|
1196
|
+
return False
|
|
1197
|
+
|
|
1198
|
+
return any(issubclass(type_, base) and _has_default_write(type_, base) for base in _PACKABLE_TYPES)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def _can_pack_bits(field: Field) -> bool:
|
|
1202
|
+
"""Determine if the value of the bit buffer for a bit field can be written by packing it."""
|
|
1203
|
+
type_ = _get_bits_type(field)
|
|
1204
|
+
return issubclass(type_, (Int, Packed)) and _can_pack(type_)
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def _has_default_write(type_: type[BaseType], base: type[BaseType]) -> bool:
|
|
1208
|
+
"""Return whether a type uses the write methods of its base type, and doesn't have custom ones."""
|
|
1209
|
+
return all(getattr(type_, name).__func__ is getattr(base, name).__func__ for name in ("_write", "_write_array"))
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def _int_to_bytes(type_: type[Int], value: str) -> str:
|
|
1213
|
+
"""Return the code to convert a value of an integer type to bytes."""
|
|
1214
|
+
return f"{value}.to_bytes({type_.__size__}, _byteorder[endian], signed={bool(type_.signed)})"
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def _get_attribute(var: str, name: str) -> str:
|
|
1218
|
+
"""Return the code to get an attribute."""
|
|
1219
|
+
if name.isidentifier() and not iskeyword(name):
|
|
1220
|
+
return f"{var}.{name}"
|
|
1221
|
+
return f"_getattr({var}, {name!r})"
|
|
1222
|
+
|
|
1223
|
+
|
|
770
1224
|
def _set_attribute(var: str, name: str, value: str) -> str:
|
|
771
1225
|
"""Return the code to set an attribute."""
|
|
772
1226
|
if name.isidentifier() and not iskeyword(name):
|
|
@@ -809,25 +1263,18 @@ def _block(lines: list[str]) -> str:
|
|
|
809
1263
|
|
|
810
1264
|
def _generate_struct_info(
|
|
811
1265
|
fields: list[Field],
|
|
812
|
-
align: bool = False,
|
|
813
1266
|
inline: Callable[[type[BaseType]], str | None] | None = None,
|
|
814
1267
|
) -> Iterator[tuple[Field, int, str]]:
|
|
815
1268
|
if not fields:
|
|
816
1269
|
return
|
|
817
1270
|
|
|
818
1271
|
current_offset = fields[0].offset
|
|
819
|
-
imaginary_offset = 0
|
|
820
1272
|
for field in fields:
|
|
821
1273
|
# We moved -- probably due to alignment
|
|
822
1274
|
if field.offset is not None and (drift := field.offset - current_offset) > 0:
|
|
823
1275
|
yield None, drift, "x"
|
|
824
1276
|
current_offset += drift
|
|
825
1277
|
|
|
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
1278
|
count = 1
|
|
832
1279
|
read_type = _get_read_type(field.type)
|
|
833
1280
|
|
|
@@ -853,25 +1300,41 @@ def _generate_struct_info(
|
|
|
853
1300
|
elif issubclass(read_type, (Char, Wchar, Int)):
|
|
854
1301
|
yield field, count * read_type.__size__, "x"
|
|
855
1302
|
|
|
856
|
-
size = count * read_type.__size__
|
|
857
|
-
imaginary_offset += size
|
|
858
1303
|
if current_offset is not None:
|
|
859
|
-
current_offset +=
|
|
1304
|
+
current_offset += count * read_type.__size__
|
|
860
1305
|
|
|
861
1306
|
|
|
862
1307
|
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.
|
|
1308
|
+
"""Create a struct format string from ``(count, char)`` items, merging consecutive items of the same char.
|
|
1309
|
+
|
|
1310
|
+
Byte strings aren't merged, since those are a single value regardless of their count.
|
|
1311
|
+
"""
|
|
864
1312
|
chars = []
|
|
865
1313
|
for char, group in groupby(items, key=itemgetter(1)):
|
|
866
|
-
if
|
|
1314
|
+
if char == "s":
|
|
1315
|
+
chars.extend(f"{count}{char}" for count, _ in group)
|
|
1316
|
+
elif count := sum(count for count, _ in group):
|
|
867
1317
|
chars.append(f"{count if count > 1 else ''}{char}")
|
|
868
1318
|
|
|
869
1319
|
return "".join(chars)
|
|
870
1320
|
|
|
871
1321
|
|
|
1322
|
+
def _struct_functions(fmt: str, name: str) -> dict[str, Callable]:
|
|
1323
|
+
"""Return the given function of a struct with the given format for every endianness.
|
|
1324
|
+
|
|
1325
|
+
The ``@`` endianness uses the ``=`` format, since ``@`` would add padding between items to align them.
|
|
1326
|
+
"""
|
|
1327
|
+
return {endian: getattr(_struct("=" if endian == "@" else endian, fmt), name) for endian in _ENDIANNESS}
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def _get_bit_buffer_type(field: Field) -> type[BaseType]:
|
|
1331
|
+
"""Return the type that ``BitBuffer`` uses for a bit field, only bit fields of the same type share a value."""
|
|
1332
|
+
return field.type.__type__ if issubclass(field.type, (Enum, Flag)) else field.type
|
|
1333
|
+
|
|
1334
|
+
|
|
872
1335
|
def _get_bits_type(field: Field) -> type[BaseType]:
|
|
873
1336
|
"""Return the type that ``BitBuffer`` reads a bit field as."""
|
|
874
|
-
type_ =
|
|
1337
|
+
type_ = _get_bit_buffer_type(field)
|
|
875
1338
|
return type_.__cs__.uint8 if issubclass(type_, Char) else type_
|
|
876
1339
|
|
|
877
1340
|
|
dissect/cstruct/cstruct.py
CHANGED
|
@@ -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
|
|
dissect/cstruct/types/base.py
CHANGED
|
@@ -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]:
|
|
@@ -370,12 +384,24 @@ class BaseArray(BaseType):
|
|
|
370
384
|
if cls.__null_terminated__:
|
|
371
385
|
return cls.__type__._write_0(stream, data, endian=endian)
|
|
372
386
|
|
|
373
|
-
if not cls.__dynamic__
|
|
387
|
+
if not cls.__dynamic__:
|
|
388
|
+
data = cls._pad(data)
|
|
389
|
+
|
|
390
|
+
return cls.__type__._write_array(stream, data, endian=endian)
|
|
391
|
+
|
|
392
|
+
@classmethod
|
|
393
|
+
def _pad(cls, data: list[Any]) -> list[Any]:
|
|
394
|
+
"""Internal function for padding the entries of a static array to its size.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
data: The entries of the array.
|
|
398
|
+
"""
|
|
399
|
+
if remaining := cls.__num_entries__ - (actual_size := len(data)):
|
|
374
400
|
if remaining < 0:
|
|
375
401
|
raise ArraySizeError(f"Expected static array size {cls.__num_entries__}, got {actual_size} instead.")
|
|
376
402
|
data = data + [cls.__type__.__default__()] * remaining
|
|
377
403
|
|
|
378
|
-
return
|
|
404
|
+
return data
|
|
379
405
|
|
|
380
406
|
@classproperty
|
|
381
407
|
def type(cls) -> type[BaseType]:
|
dissect/cstruct/types/char.py
CHANGED
|
@@ -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")
|
|
@@ -7,7 +7,7 @@ from collections.abc import MutableMapping
|
|
|
7
7
|
from contextlib import contextmanager
|
|
8
8
|
from enum import Enum
|
|
9
9
|
from functools import lru_cache
|
|
10
|
-
from itertools import chain
|
|
10
|
+
from itertools import chain, repeat
|
|
11
11
|
from operator import attrgetter
|
|
12
12
|
from textwrap import dedent
|
|
13
13
|
from types import FunctionType
|
|
@@ -18,6 +18,7 @@ from dissect.cstruct.expression import Expression
|
|
|
18
18
|
from dissect.cstruct.lexer import IDENTIFIER_TYPES
|
|
19
19
|
from dissect.cstruct.types.base import (
|
|
20
20
|
_BUFFER_TYPES,
|
|
21
|
+
_IMMUTABLE_TYPES,
|
|
21
22
|
BaseArray,
|
|
22
23
|
BaseType,
|
|
23
24
|
MetaType,
|
|
@@ -141,7 +142,6 @@ class StructureMetaType(MetaType):
|
|
|
141
142
|
else:
|
|
142
143
|
size, alignment = cls._calculate_size_and_offsets(fields, align)
|
|
143
144
|
|
|
144
|
-
# TODO: compile _write
|
|
145
145
|
# TODO: generate cached_property for lazy reading
|
|
146
146
|
|
|
147
147
|
classdict["__size__"] = size
|
|
@@ -193,7 +193,7 @@ class StructureMetaType(MetaType):
|
|
|
193
193
|
# Moved to a bit field of another type, e.g. uint16 f1 : 8, uint32 f2 : 8;
|
|
194
194
|
or field_type != bits_type
|
|
195
195
|
# Still processing a bit field, but it's at a different offset due to alignment or a manual offset
|
|
196
|
-
or (
|
|
196
|
+
or (None not in (offset, bits_field_offset) and offset > bits_field_offset + bits_type.__size__)
|
|
197
197
|
):
|
|
198
198
|
# ... if any of this is true, we have to move to the next field
|
|
199
199
|
bits_type = field_type
|
|
@@ -299,7 +299,6 @@ class StructureMetaType(MetaType):
|
|
|
299
299
|
def _write(cls, stream: BinaryIO, data: Structure, *, endian: Endianness) -> int:
|
|
300
300
|
bit_buffer = None
|
|
301
301
|
struct_start = stream.tell()
|
|
302
|
-
num = 0
|
|
303
302
|
|
|
304
303
|
for field in cls.__fields__:
|
|
305
304
|
field_type = field.type
|
|
@@ -331,7 +330,7 @@ class StructureMetaType(MetaType):
|
|
|
331
330
|
stream.write(b"\x00" * align_pad)
|
|
332
331
|
offset += align_pad
|
|
333
332
|
|
|
334
|
-
value = getattr(data, field._name
|
|
333
|
+
value = getattr(data, field._name)
|
|
335
334
|
if value is None:
|
|
336
335
|
value = field_type.__default__()
|
|
337
336
|
|
|
@@ -345,7 +344,6 @@ class StructureMetaType(MetaType):
|
|
|
345
344
|
bit_buffer.write(field_type, value, field.bits)
|
|
346
345
|
else:
|
|
347
346
|
field_type._write(stream, value, endian=endian)
|
|
348
|
-
num += stream.tell() - offset
|
|
349
347
|
|
|
350
348
|
if bit_buffer is not None and bit_buffer._type is not None:
|
|
351
349
|
bit_buffer.flush()
|
|
@@ -354,7 +352,7 @@ class StructureMetaType(MetaType):
|
|
|
354
352
|
# Align the stream
|
|
355
353
|
stream.write(b"\x00" * (-stream.tell() & (cls.__alignment__ - 1)))
|
|
356
354
|
|
|
357
|
-
return
|
|
355
|
+
return stream.tell() - struct_start
|
|
358
356
|
|
|
359
357
|
def add_field(cls, name: str, type_: type[BaseType], bits: int | None = None, offset: int | None = None) -> None:
|
|
360
358
|
field = Field(name, type_, bits=bits, offset=offset)
|
|
@@ -698,6 +696,10 @@ class UnionMetaType(StructureMetaType):
|
|
|
698
696
|
|
|
699
697
|
return obj
|
|
700
698
|
|
|
699
|
+
def _write_field(cls) -> Field | None:
|
|
700
|
+
"""Return the field to write a union by, its largest field and preferably not an anonymous structure."""
|
|
701
|
+
return max(cls.__fields__, key=lambda e: (e.type.__size__ or 0, e.name is not None), default=None)
|
|
702
|
+
|
|
701
703
|
def _write(cls, stream: BinaryIO, data: Union, *, endian: Endianness) -> int:
|
|
702
704
|
if cls.__dynamic__:
|
|
703
705
|
raise NotImplementedError("Writing dynamic unions is not yet supported")
|
|
@@ -705,24 +707,9 @@ class UnionMetaType(StructureMetaType):
|
|
|
705
707
|
offset = stream.tell()
|
|
706
708
|
expected_offset = offset + len(cls)
|
|
707
709
|
|
|
708
|
-
|
|
709
|
-
fields = sorted(cls.__fields__, key=lambda e: e.type.__size__ or 0, reverse=True)
|
|
710
|
-
anonymous_struct = False
|
|
711
|
-
|
|
712
|
-
# Try to write by largest field
|
|
713
|
-
for field in fields:
|
|
714
|
-
if isinstance(field.type, StructureMetaType) and field.name is None:
|
|
715
|
-
# Prefer to write regular fields initially
|
|
716
|
-
anonymous_struct = field.type
|
|
717
|
-
continue
|
|
718
|
-
|
|
710
|
+
if (field := cls._write_field()) is not None:
|
|
719
711
|
# Write the value
|
|
720
712
|
field.type._write(stream, getattr(data, field._name), endian=endian)
|
|
721
|
-
break
|
|
722
|
-
|
|
723
|
-
# If we haven't written anything yet and we initially skipped an anonymous struct, write it now
|
|
724
|
-
if stream.tell() == offset and anonymous_struct:
|
|
725
|
-
anonymous_struct._write(stream, data, endian=endian)
|
|
726
713
|
|
|
727
714
|
# If we haven't filled the union size yet, pad it
|
|
728
715
|
if remaining := expected_offset - stream.tell():
|
|
@@ -745,7 +732,9 @@ class Union(Structure, metaclass=UnionMetaType):
|
|
|
745
732
|
raise NotImplementedError("Modifying a dynamic union is not yet supported")
|
|
746
733
|
|
|
747
734
|
super().__setattr__(attr, value)
|
|
748
|
-
|
|
735
|
+
# Fields of an anonymous structure are rebuilt through the proxy of that structure
|
|
736
|
+
if attr in self.__class__.__lookup__:
|
|
737
|
+
self._rebuild(attr)
|
|
749
738
|
|
|
750
739
|
def _rebuild(self, attr: str) -> None:
|
|
751
740
|
if (cur_buf := getattr(self, "_buf", None)) is None:
|
|
@@ -773,13 +762,17 @@ class Union(Structure, metaclass=UnionMetaType):
|
|
|
773
762
|
object.__setattr__(self, "__dynamic_sizes__", sizes)
|
|
774
763
|
|
|
775
764
|
def _proxify(self) -> None:
|
|
776
|
-
def _proxy_structure(value: Structure) -> None:
|
|
765
|
+
def _proxy_structure(value: Structure, attr: str | None = None) -> None:
|
|
777
766
|
for field in value.__class__.__fields__:
|
|
778
767
|
if issubclass(field.type, Structure):
|
|
779
768
|
nested_value = getattr(value, field._name)
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
769
|
+
# Changing a nested field rebuilds the union from the top level field it's in
|
|
770
|
+
member = attr or field._name
|
|
771
|
+
object.__setattr__(value, field._name, UnionProxy(self, member, nested_value))
|
|
772
|
+
# The value of a nested union is already a proxy, so continue with the value behind it
|
|
773
|
+
if isinstance(nested_value, UnionProxy):
|
|
774
|
+
nested_value = nested_value.__target__
|
|
775
|
+
_proxy_structure(nested_value, member)
|
|
783
776
|
|
|
784
777
|
_proxy_structure(self)
|
|
785
778
|
|
|
@@ -864,7 +857,7 @@ def _make_structure__init__(fields: list[str]) -> str:
|
|
|
864
857
|
"""
|
|
865
858
|
field_args = ", ".join(f"{field} = None" for field in fields)
|
|
866
859
|
field_init = "\n".join(
|
|
867
|
-
f" self.{name} = {name} if {name} is not None else _{i}_default" for i, name in enumerate(fields)
|
|
860
|
+
f" self.{name} = {name} if {name} is not None else _{i}_default()" for i, name in enumerate(fields)
|
|
868
861
|
)
|
|
869
862
|
|
|
870
863
|
code = f"def __init__(self{', ' + field_args or ''}):\n"
|
|
@@ -880,7 +873,7 @@ def _make_union__init__(fields: list[str]) -> str:
|
|
|
880
873
|
"""
|
|
881
874
|
field_args = ", ".join(f"{field} = None" for field in fields)
|
|
882
875
|
field_init = "\n".join(
|
|
883
|
-
f" object.__setattr__(self, '{name}', {name} if {name} is not None else _{i}_default)"
|
|
876
|
+
f" object.__setattr__(self, '{name}', {name} if {name} is not None else _{i}_default())"
|
|
884
877
|
for i, name in enumerate(fields)
|
|
885
878
|
)
|
|
886
879
|
|
|
@@ -976,7 +969,7 @@ def _generate_structure__init__(fields: list[Field]) -> FunctionType:
|
|
|
976
969
|
co_names=_remap_co_values(template.__code__.co_names, mapping),
|
|
977
970
|
co_varnames=_remap_co_values(template.__code__.co_varnames, mapping),
|
|
978
971
|
),
|
|
979
|
-
template.__globals__ | {f"__{field._name}_default__": field.type
|
|
972
|
+
template.__globals__ | {f"__{field._name}_default__": _default_factory(field.type) for field in fields},
|
|
980
973
|
argdefs=template.__defaults__,
|
|
981
974
|
)
|
|
982
975
|
|
|
@@ -996,11 +989,17 @@ def _generate_union__init__(fields: list[Field]) -> FunctionType:
|
|
|
996
989
|
co_names=_remap_co_values(template.__code__.co_names, mapping),
|
|
997
990
|
co_varnames=_remap_co_values(template.__code__.co_varnames, mapping),
|
|
998
991
|
),
|
|
999
|
-
template.__globals__ | {f"__{field._name}_default__": field.type
|
|
992
|
+
template.__globals__ | {f"__{field._name}_default__": _default_factory(field.type) for field in fields},
|
|
1000
993
|
argdefs=template.__defaults__,
|
|
1001
994
|
)
|
|
1002
995
|
|
|
1003
996
|
|
|
997
|
+
def _default_factory(type_: type[BaseType]) -> Callable[[], Any]:
|
|
998
|
+
"""Return a function that returns default values of a type, which are only shared if they're immutable."""
|
|
999
|
+
value = type_.__default__()
|
|
1000
|
+
return repeat(value).__next__ if isinstance(value, _IMMUTABLE_TYPES) else type_.__default__
|
|
1001
|
+
|
|
1002
|
+
|
|
1004
1003
|
def _generate_co_mapping(fields: list[Field]) -> dict[str, str]:
|
|
1005
1004
|
"""Generates a mapping of generated code object names to field names.
|
|
1006
1005
|
|
dissect/cstruct/types/wchar.py
CHANGED
|
@@ -28,12 +28,15 @@ class WcharArray(str, BaseArray):
|
|
|
28
28
|
if cls.__null_terminated__:
|
|
29
29
|
data += "\x00"
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
buf = data.encode(Wchar.__encoding_map__[endian], "surrogatepass")
|
|
32
|
+
|
|
33
|
+
# A surrogate pair is a single character that takes up two entries of the array
|
|
34
|
+
if not cls.__dynamic__ and (remaining := cls.__num_entries__ - (actual_size := len(buf) // 2)):
|
|
32
35
|
if remaining < 0:
|
|
33
36
|
raise ArraySizeError(f"Expected static array size {cls.__num_entries__}, got {actual_size} instead.")
|
|
34
|
-
|
|
37
|
+
buf += b"\x00\x00" * remaining
|
|
35
38
|
|
|
36
|
-
return stream.write(
|
|
39
|
+
return stream.write(buf)
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
class Wchar(str, BaseType):
|
|
@@ -42,6 +45,7 @@ class Wchar(str, BaseType):
|
|
|
42
45
|
__ArrayType__ = WcharArray
|
|
43
46
|
|
|
44
47
|
__slots__ = ()
|
|
48
|
+
# Decode with "surrogatepass" so lone surrogates are handled correctly
|
|
45
49
|
__encoding_map__: ClassVar[dict[Endianness, str]] = {
|
|
46
50
|
"@": f"utf-16-{sys.byteorder[0]}e",
|
|
47
51
|
"=": f"utf-16-{sys.byteorder[0]}e",
|
|
@@ -72,12 +76,13 @@ class Wchar(str, BaseType):
|
|
|
72
76
|
if count != EOF and len(data) != count:
|
|
73
77
|
raise EOFError(f"Read {len(data)} bytes, but expected {count}")
|
|
74
78
|
|
|
75
|
-
return type.__call__(cls, str(data, cls.__encoding_map__[endian]))
|
|
79
|
+
return type.__call__(cls, str(data, cls.__encoding_map__[endian], "surrogatepass"))
|
|
76
80
|
|
|
77
81
|
@classmethod
|
|
78
82
|
def _read_0(cls, stream: BinaryIO, *, context: dict[str, Any] | None = None, endian: Endianness) -> Wchar:
|
|
79
83
|
if _is_seekable(stream):
|
|
80
|
-
|
|
84
|
+
data = _read_null_terminated(stream, 2)
|
|
85
|
+
return type.__call__(cls, data.decode(cls.__encoding_map__[endian], "surrogatepass"))
|
|
81
86
|
|
|
82
87
|
buf = []
|
|
83
88
|
while True:
|
|
@@ -90,8 +95,14 @@ class Wchar(str, BaseType):
|
|
|
90
95
|
|
|
91
96
|
buf.append(point)
|
|
92
97
|
|
|
93
|
-
return type.__call__(cls, b"".join(buf).decode(cls.__encoding_map__[endian]))
|
|
98
|
+
return type.__call__(cls, b"".join(buf).decode(cls.__encoding_map__[endian], "surrogatepass"))
|
|
94
99
|
|
|
95
100
|
@classmethod
|
|
96
101
|
def _write(cls, stream: BinaryIO, data: str, *, endian: Endianness) -> int:
|
|
97
|
-
|
|
102
|
+
buf = data.encode(cls.__encoding_map__[endian], "surrogatepass")
|
|
103
|
+
|
|
104
|
+
# A surrogate pair is a single character that doesn't fit in a single wchar
|
|
105
|
+
if (actual_size := len(buf) // 2) > 1:
|
|
106
|
+
raise ValueError(f"Expected a single character, got {actual_size} instead.")
|
|
107
|
+
|
|
108
|
+
return stream.write(buf or b"\x00\x00")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dissect.cstruct
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.dev14
|
|
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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
dissect/cstruct/__init__.py,sha256=KOyNeUvu4c0JaAiyOfj7iftGwUIjTmD8MwHtFsLPLSU,1504
|
|
2
2
|
dissect/cstruct/bitbuffer.py,sha256=aNzY4lkWe8ffAc8j7FODXzwZc8gZwbey77UosyoYCsw,2711
|
|
3
|
-
dissect/cstruct/compiler.py,sha256=
|
|
4
|
-
dissect/cstruct/cstruct.py,sha256=
|
|
3
|
+
dissect/cstruct/compiler.py,sha256=0u58J5pB4izosBj8G4wiBI1JSAAgHFMLM63IaSG4YO4,54509
|
|
4
|
+
dissect/cstruct/cstruct.py,sha256=mJerzPouWV29RdK5LqjGitDDMvNKcadoT2hj1zpExnM,26551
|
|
5
5
|
dissect/cstruct/exception.py,sha256=RqD08LuBkbMqa3iQ1A05DNN8XgEmQR_SUnguwe9LGo8,316
|
|
6
6
|
dissect/cstruct/expression.py,sha256=Y27-N48BomBoRFozbWhGDF_DS2lfklRwWVUbCzGnxHE,10873
|
|
7
7
|
dissect/cstruct/lexer.py,sha256=NDUfAWgyxgRNRuVO8ExXQ51LqQXS2mv9bywVa_M7tFQ,19665
|
|
@@ -10,21 +10,21 @@ dissect/cstruct/util.py,sha256=vAf-kdOkk3zprW_gD6QmQeN3VIRgQqhPS4B_ha1jEpQ,18040
|
|
|
10
10
|
dissect/cstruct/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
dissect/cstruct/tools/stubgen.py,sha256=MeZkLmSeNSLNcAEwlAdIGv1lKvlvK3UQ4WN85ovsxTM,7672
|
|
12
12
|
dissect/cstruct/types/__init__.py,sha256=6P71Pu94Dv7RNjZpDZ19pqzx-G0QzmQw4q0-3zWALso,911
|
|
13
|
-
dissect/cstruct/types/base.py,sha256=
|
|
14
|
-
dissect/cstruct/types/char.py,sha256=
|
|
13
|
+
dissect/cstruct/types/base.py,sha256=bbaxiHCVkoU6ugqn5RYishPQ5L6xGrPfQuejjro0K_g,18133
|
|
14
|
+
dissect/cstruct/types/char.py,sha256=N19R7Zzn2uMuG-ULW5X2UgmgkLvjaBj1Td7AQQI93Fg,3417
|
|
15
15
|
dissect/cstruct/types/enum.py,sha256=nOdI6vGtqTfPMhOBkKuiuOriIB2YHSt8Q9ruri37s3U,9177
|
|
16
16
|
dissect/cstruct/types/flag.py,sha256=YxQHix8ZhK1qQX3J_Og7GJp_4JAxCwzUbUibuJeDaWQ,3479
|
|
17
17
|
dissect/cstruct/types/int.py,sha256=9RbdrkvAI28zATM97VVORQnras0XhLaVMtdPjJwpTH4,2089
|
|
18
18
|
dissect/cstruct/types/leb128.py,sha256=V97XTJT_Xbw1LpTwTN1OkjPHuQQAQUxd8OGuA6yEDmE,2387
|
|
19
19
|
dissect/cstruct/types/packed.py,sha256=NgCX0YWX8SH8z47atq0f39w9MBjU_eBFJtUisUWdO-M,3867
|
|
20
20
|
dissect/cstruct/types/pointer.py,sha256=FNuB9bfENWRebbKX-R6Vs6xWPMHepjvOhhomeUhr7I8,3985
|
|
21
|
-
dissect/cstruct/types/structure.py,sha256=
|
|
21
|
+
dissect/cstruct/types/structure.py,sha256=mxDvurQ0oAjI2flykhAPlT-03280cDpwGTz0-bqbr-0,39558
|
|
22
22
|
dissect/cstruct/types/void.py,sha256=fV4hKk50uop_EKbR1nfy1cdtAzvUYrSymXsdakCyFk8,1222
|
|
23
|
-
dissect/cstruct/types/wchar.py,sha256=
|
|
24
|
-
dissect_cstruct-5.0.
|
|
25
|
-
dissect_cstruct-5.0.
|
|
26
|
-
dissect_cstruct-5.0.
|
|
27
|
-
dissect_cstruct-5.0.
|
|
28
|
-
dissect_cstruct-5.0.
|
|
29
|
-
dissect_cstruct-5.0.
|
|
30
|
-
dissect_cstruct-5.0.
|
|
23
|
+
dissect/cstruct/types/wchar.py,sha256=VrnPXsP_lsyA-1xr4D2aSIVijumMhUuZWNlDHcn12-s,3964
|
|
24
|
+
dissect_cstruct-5.0.dev14.dist-info/licenses/COPYRIGHT,sha256=H-18RXfshdH9AdHwW2eO1Xa-5s6tY5eipHh5c0whDu4,316
|
|
25
|
+
dissect_cstruct-5.0.dev14.dist-info/licenses/LICENSE,sha256=PhUqiw6jAh2KbBdVRPBq_hfAvfcTBin7nZ3CK7NQbTM,11341
|
|
26
|
+
dissect_cstruct-5.0.dev14.dist-info/METADATA,sha256=0EhDz7fNITZQWzSYoy46-j0nObu4B1Am3-nEipozmxc,9256
|
|
27
|
+
dissect_cstruct-5.0.dev14.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
28
|
+
dissect_cstruct-5.0.dev14.dist-info/entry_points.txt,sha256=z53zqZqwD2OLrAkRwrP4wTeiU9CQe7xrMly0T2c0_wQ,71
|
|
29
|
+
dissect_cstruct-5.0.dev14.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
|
|
30
|
+
dissect_cstruct-5.0.dev14.dist-info/RECORD,,
|
|
File without changes
|
{dissect_cstruct-5.0.dev12.dist-info → dissect_cstruct-5.0.dev14.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{dissect_cstruct-5.0.dev12.dist-info → dissect_cstruct-5.0.dev14.dist-info}/licenses/COPYRIGHT
RENAMED
|
File without changes
|
{dissect_cstruct-5.0.dev12.dist-info → dissect_cstruct-5.0.dev14.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|