Skip to content

Commit 4f5f8e1

Browse files
committed
Add endian keyword argument to type reads
1 parent 7678a8f commit 4f5f8e1

22 files changed

Lines changed: 570 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: 52 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.exceptions 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 = []
@@ -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,25 @@ 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 warn 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+
# Only warn if the method doesn't accept an endian parameter
287+
if "endian" not in signature.parameters:
288+
warnings.warn(
289+
f"Custom type {type_name} is missing the 'endian' keyword-only parameter in its {method} method. " # noqa: E501
290+
"Please refer to the changelog of dissect.cstruct 5.0 for more information.",
291+
stacklevel=2,
292+
)
293+
267294
self.add_type(name, self._make_type(name, (type_,), size, alignment=alignment, attrs=kwargs))
268295

269296
def load(self, definition: str, deftype: int | None = None, **kwargs) -> cstruct:
@@ -351,6 +378,25 @@ def copy(self) -> cstruct:
351378
"""
352379
return copy.copy(self)
353380

381+
@contextmanager
382+
def endianness(self, endian: AllowedEndianness) -> Iterator[cstruct]:
383+
"""Context manager for temporarily changing the endianness of this cstruct instance."""
384+
new = self.copy()
385+
new.endian = normalize_endianness(endian)
386+
yield new
387+
388+
@contextmanager
389+
def big_endian(self) -> Iterator[cstruct]:
390+
"""Context manager for temporarily changing the endianness to big endian."""
391+
with self.endianness(">") as cs:
392+
yield cs
393+
394+
@contextmanager
395+
def little_endian(self) -> Iterator[cstruct]:
396+
"""Context manager for temporarily changing the endianness to little endian."""
397+
with self.endianness("<") as cs:
398+
yield cs
399+
354400
def _make_type(
355401
self,
356402
name: str,

0 commit comments

Comments
 (0)