Skip to content

Commit c3e00e8

Browse files
committed
Add endian keyword argument to type reads
1 parent 0edfc10 commit c3e00e8

22 files changed

Lines changed: 571 additions & 242 deletions

dissect/cstruct/bitbuffer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
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):
1313
self.stream = stream
1414
self.endian = endian
1515

@@ -24,7 +24,7 @@ def read(self, field_type: type[BaseType], bits: int) -> int:
2424

2525
self._type = field_type
2626
self._remaining = field_type.size * 8
27-
self._buffer = field_type._read(self.stream)
27+
self._buffer = field_type._read(self.stream, endian=self.endian)
2828

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

7272
def flush(self) -> None:
7373
if self._type is not None:
74-
self._type._write(self.stream, self._buffer)
74+
self._type._write(self.stream, self._buffer, endian=self.endian)
7575
self._type = None
7676
self._remaining = 0
7777
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)\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):\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)
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)
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)"
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)"
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: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
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

1114
from dissect.cstruct.exception import ResolveError
1215
from dissect.cstruct.expression import Expression
@@ -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 = []
@@ -216,9 +222,11 @@ def __copy__(self) -> cstruct:
216222

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

229+
# Update the cstruct instance for all other types
222230
if isinstance(type, MetaType):
223231
new_type = copy.copy(type)
224232
new_type.cs = cs
@@ -265,6 +273,25 @@ def add_custom_type(
265273
alignment: The alignment of the type.
266274
**kwargs: Additional attributes to add to the type.
267275
"""
276+
# In cstruct 5.0 we changed the function signature of _read and _write
277+
# Check if the function signature is compatible, and warn if not
278+
for type_to_check in (type_, type_.ArrayType):
279+
type_name = type_.__name__ + (f".{type_.ArrayType.__name__}" if type_to_check is type_.ArrayType else "")
280+
281+
for method in ("_read", "_read_array", "_read_0", "_write", "_write_array", "_write_0"):
282+
if not hasattr(type_to_check, method):
283+
continue
284+
285+
signature = inspect.signature(getattr(type_to_check, method))
286+
287+
# Only warn if the method doesn't accept an endian parameter
288+
if "endian" not in signature.parameters:
289+
warnings.warn(
290+
f"Custom type {type_name} is missing the 'endian' keyword-only parameter in its {method} method. " # noqa: E501
291+
"Please refer to the changelog of dissect.cstruct 5.0 for more information.",
292+
stacklevel=2,
293+
)
294+
268295
self.add_type(name, self._make_type(name, (type_,), size, alignment=alignment, attrs=kwargs))
269296

270297
def load(self, definition: str, deftype: int | None = None, **kwargs) -> cstruct:
@@ -388,6 +415,28 @@ def copy(self) -> cstruct:
388415
"""
389416
return copy.copy(self)
390417

418+
@contextmanager
419+
def endianness(self, endian: AllowedEndianness) -> Iterator[cstruct]:
420+
"""Context manager for temporarily changing the endianness of this cstruct instance."""
421+
original = self.endian
422+
self.endian = normalize_endianness(endian)
423+
try:
424+
yield self
425+
finally:
426+
self.endian = original
427+
428+
@contextmanager
429+
def big_endian(self) -> Iterator[cstruct]:
430+
"""Context manager for temporarily changing the endianness to big endian."""
431+
with self.endianness(">") as cs:
432+
yield cs
433+
434+
@contextmanager
435+
def little_endian(self) -> Iterator[cstruct]:
436+
"""Context manager for temporarily changing the endianness to little endian."""
437+
with self.endianness("<") as cs:
438+
yield cs
439+
391440
def _make_type(
392441
self,
393442
name: str,

0 commit comments

Comments
 (0)