Skip to content

Commit 0496f2a

Browse files
committed
Add endian keyword argument to type reads
1 parent 7678a8f commit 0496f2a

22 files changed

Lines changed: 593 additions & 243 deletions

dissect/cstruct/bitbuffer.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
class BitBuffer:
1010
"""Implements a bit buffer that can read and write bit fields."""
1111

12-
def __init__(self, stream: BinaryIO, endian: str):
12+
def __init__(self, stream: BinaryIO, *, endian: str, **kwargs):
1313
self.stream = stream
1414
self.endian = endian
15+
self.kwargs = kwargs
1516

1617
self._type: type[BaseType] | None = None
1718
self._buffer = 0
@@ -24,7 +25,7 @@ def read(self, field_type: type[BaseType], bits: int) -> int:
2425

2526
self._type = field_type
2627
self._remaining = field_type.size * 8
27-
self._buffer = field_type._read(self.stream)
28+
self._buffer = field_type._read(self.stream, endian=self.endian, **self.kwargs)
2829

2930
if isinstance(self._buffer, bytes):
3031
if self.endian == "<":
@@ -71,7 +72,7 @@ def write(self, field_type: type[BaseType], data: int, bits: int) -> None:
7172

7273
def flush(self) -> None:
7374
if self._type is not None:
74-
self._type._write(self.stream, self._buffer)
75+
self._type._write(self.stream, self._buffer, endian=self.endian, **self.kwargs)
7576
self._type = None
7677
self._remaining = 0
7778
self._buffer = 0

dissect/cstruct/compiler.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def generate_source(self) -> str:
119119
"""
120120

121121
if any(field.bits for field in self.fields):
122-
preamble += "bit_reader = BitBuffer(stream, cls.cs.endian)\n"
122+
preamble += "bit_reader = BitBuffer(stream, endian=endian, **kwargs)\n"
123123

124124
read_code = "\n".join(self._generate_fields())
125125

@@ -132,7 +132,7 @@ def generate_source(self) -> str:
132132

133133
code = indent(dedent(preamble).lstrip() + read_code + dedent(outro), " ")
134134

135-
return f"def _read(cls, stream, context=None):\n{code}"
135+
return f"def _read(cls, stream, *, context=None, endian, **kwargs):\n{code}"
136136

137137
def _generate_fields(self) -> Iterator[str]:
138138
current_offset = 0
@@ -229,7 +229,7 @@ def align_to_field(field: Field) -> Iterator[str]:
229229
def _generate_structure(self, field: Field) -> Iterator[str]:
230230
template = f"""
231231
{"_s = stream.tell()" if field.type.dynamic else ""}
232-
r["{field._name}"] = {self._map_field(field)}._read(stream, context=r)
232+
r["{field._name}"] = {self._map_field(field)}._read(stream, context=r, endian=endian, **kwargs)
233233
{f's["{field._name}"] = stream.tell() - _s' if field.type.dynamic else ""}
234234
"""
235235

@@ -238,7 +238,7 @@ def _generate_structure(self, field: Field) -> Iterator[str]:
238238
def _generate_array(self, field: Field) -> Iterator[str]:
239239
template = f"""
240240
{"_s = stream.tell()" if field.type.dynamic else ""}
241-
r["{field._name}"] = {self._map_field(field)}._read(stream, context=r)
241+
r["{field._name}"] = {self._map_field(field)}._read(stream, context=r, endian=endian, **kwargs)
242242
{f's["{field._name}"] = stream.tell() - _s' if field.type.dynamic else ""}
243243
"""
244244

@@ -311,7 +311,7 @@ def _generate_packed(self, fields: list[Field]) -> Iterator[str]:
311311
item_parser = parser_template.format(type="_et", getter=f"_b[i:i + {field_type.type.size}]")
312312
list_comp = f"[{item_parser} for i in range(0, {count}, {field_type.type.size})]"
313313
elif issubclass(field_type.type, Pointer):
314-
item_parser = "_et.__new__(_et, e, stream, r)"
314+
item_parser = "_et.__new__(_et, e, stream, context=r, endian=endian, **kwargs)"
315315
list_comp = f"[{item_parser} for e in {getter}]"
316316
else:
317317
item_parser = parser_template.format(type="_et", getter="e")
@@ -322,7 +322,7 @@ def _generate_packed(self, fields: list[Field]) -> Iterator[str]:
322322
parser = f"type.__call__({self._map_field(field)}, {getter})"
323323
elif issubclass(field_type, Pointer):
324324
reads.append(f"_pt = {self._map_field(field)}")
325-
parser = f"_pt.__new__(_pt, {getter}, stream, r)"
325+
parser = f"_pt.__new__(_pt, {getter}, stream, context=r, endian=endian, **kwargs)"
326326
else:
327327
parser = parser_template.format(type=self._map_field(field), getter=getter)
328328

@@ -335,7 +335,7 @@ def _generate_packed(self, fields: list[Field]) -> Iterator[str]:
335335
if fmt == "x" or (len(fmt) == 2 and fmt[1] == "x"):
336336
unpack = ""
337337
else:
338-
unpack = f'data = _struct(cls.cs.endian, "{fmt}").unpack(buf)\n'
338+
unpack = f'data = _struct(endian, "{fmt}").unpack(buf)\n'
339339

340340
template = f"""
341341
buf = stream.read({size})

dissect/cstruct/cstruct.py

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
import copy
44
import ctypes as _ctypes
5+
import inspect
56
import struct
67
import sys
78
import types
9+
import warnings
10+
from contextlib import contextmanager
811
from pathlib import Path
9-
from typing import TYPE_CHECKING, Any, BinaryIO, TypeVar, cast
12+
from typing import TYPE_CHECKING, Any, BinaryIO, Literal, TypeVar, cast
1013

11-
from dissect.cstruct.exceptions import ResolveError
14+
from dissect.cstruct.exceptions import Error, ResolveError
1215
from dissect.cstruct.expression import Expression
1316
from dissect.cstruct.parser import CStyleParser
1417
from dissect.cstruct.types import (
@@ -26,10 +29,10 @@
2629
Void,
2730
Wchar,
2831
)
29-
from dissect.cstruct.types.base import MetaType
32+
from dissect.cstruct.types.base import MetaType, normalize_endianness
3033

3134
if TYPE_CHECKING:
32-
from collections.abc import Iterable
35+
from collections.abc import Iterable, Iterator
3336
from typing import TypeAlias
3437

3538
from dissect.cstruct.types import (
@@ -40,20 +43,23 @@
4043

4144
T = TypeVar("T", bound=BaseType)
4245

46+
AllowedEndianness: TypeAlias = Literal["little", "big", "network", "<", ">", "!", "@", "="]
47+
Endianness: TypeAlias = Literal["<", ">", "!", "@", "="]
48+
4349

4450
class cstruct:
4551
"""Main class of cstruct. All types are registered in here.
4652
4753
Args:
48-
endian: The endianness to use when parsing.
54+
endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =).
4955
pointer: The pointer type to use for pointers.
5056
"""
5157

5258
DEF_CSTYLE = 1
5359
DEF_LEGACY = 2
5460

55-
def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = None):
56-
self.endian = endian
61+
def __init__(self, load: str = "", *, endian: AllowedEndianness = "<", pointer: str | None = None):
62+
self.endian = normalize_endianness(endian)
5763

5864
self.consts = {}
5965
self.includes = []
@@ -215,9 +221,11 @@ def __copy__(self) -> cstruct:
215221

216222
# Update typedefs to point to the new cstruct instance
217223
for name, type in self.typedefs.items():
224+
# Skip all the default types
218225
if name in cs.typedefs:
219226
continue
220227

228+
# Update the cstruct instance for all other types
221229
if isinstance(type, MetaType):
222230
new_type = copy.copy(type)
223231
new_type.cs = cs
@@ -264,6 +272,33 @@ def add_custom_type(
264272
alignment: The alignment of the type.
265273
**kwargs: Additional attributes to add to the type.
266274
"""
275+
# In cstruct 5.0 we changed the function signature of _read and _write
276+
# Check if the function signature is compatible, and throw an error if not
277+
for type_to_check in (type_, type_.ArrayType):
278+
type_name = type_.__name__ + (f".{type_.ArrayType.__name__}" if type_to_check is type_.ArrayType else "")
279+
280+
for method in ("_read", "_read_array", "_read_0", "_write", "_write_array", "_write_0"):
281+
if not hasattr(type_to_check, method):
282+
continue
283+
284+
signature = inspect.signature(getattr(type_to_check, method))
285+
286+
# We added a few keyword-only parameters to the function signature, but any custom type will
287+
# continue to work fine as long as they accept **kwargs
288+
if not any(param.kind == inspect.Parameter.VAR_KEYWORD for param in signature.parameters.values()):
289+
raise Error(
290+
f"Custom type {type_name} has an incompatible {method} method signature. "
291+
"Please refer to the changelog of dissect.cstruct 5.0 for more information."
292+
)
293+
294+
# Only warn if the method doesn't accept an endian parameter
295+
if "endian" not in signature.parameters:
296+
warnings.warn(
297+
f"Custom type {type_name} is missing the 'endian' keyword-only parameter in its {method} method. " # noqa: E501
298+
"Please refer to the changelog of dissect.cstruct 5.0 for more information.",
299+
stacklevel=2,
300+
)
301+
267302
self.add_type(name, self._make_type(name, (type_,), size, alignment=alignment, attrs=kwargs))
268303

269304
def load(self, definition: str, deftype: int | None = None, **kwargs) -> cstruct:
@@ -351,6 +386,25 @@ def copy(self) -> cstruct:
351386
"""
352387
return copy.copy(self)
353388

389+
@contextmanager
390+
def endianness(self, endian: AllowedEndianness) -> Iterator[cstruct]:
391+
"""Context manager for temporarily changing the endianness of this cstruct instance."""
392+
new = self.copy()
393+
new.endian = normalize_endianness(endian)
394+
yield new
395+
396+
@contextmanager
397+
def big_endian(self) -> Iterator[cstruct]:
398+
"""Context manager for temporarily changing the endianness to big endian."""
399+
with self.endianness(">") as cs:
400+
yield cs
401+
402+
@contextmanager
403+
def little_endian(self) -> Iterator[cstruct]:
404+
"""Context manager for temporarily changing the endianness to little endian."""
405+
with self.endianness("<") as cs:
406+
yield cs
407+
354408
def _make_type(
355409
self,
356410
name: str,

0 commit comments

Comments
 (0)