|
2 | 2 |
|
3 | 3 | import copy |
4 | 4 | import ctypes as _ctypes |
| 5 | +import inspect |
5 | 6 | import struct |
6 | 7 | import sys |
7 | 8 | import types |
| 9 | +import warnings |
| 10 | +from contextlib import contextmanager |
8 | 11 | 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 |
10 | 13 |
|
11 | | -from dissect.cstruct.exceptions import ResolveError |
| 14 | +from dissect.cstruct.exceptions import Error, ResolveError |
12 | 15 | from dissect.cstruct.expression import Expression |
13 | 16 | from dissect.cstruct.parser import CStyleParser, TokenParser |
14 | 17 | from dissect.cstruct.types import ( |
|
26 | 29 | Void, |
27 | 30 | Wchar, |
28 | 31 | ) |
29 | | -from dissect.cstruct.types.base import MetaType |
| 32 | +from dissect.cstruct.types.base import MetaType, normalize_endianness |
30 | 33 |
|
31 | 34 | if TYPE_CHECKING: |
32 | | - from collections.abc import Iterable |
| 35 | + from collections.abc import Iterable, Iterator |
33 | 36 | from typing import TypeAlias |
34 | 37 |
|
35 | 38 | from dissect.cstruct.types import ( |
|
40 | 43 |
|
41 | 44 | T = TypeVar("T", bound=BaseType) |
42 | 45 |
|
| 46 | +AllowedEndianness: TypeAlias = Literal["little", "big", "network", "<", ">", "!", "@", "="] |
| 47 | +Endianness: TypeAlias = Literal["<", ">", "!", "@", "="] |
| 48 | + |
43 | 49 |
|
44 | 50 | class cstruct: |
45 | 51 | """Main class of cstruct. All types are registered in here. |
46 | 52 |
|
47 | 53 | Args: |
48 | | - endian: The endianness to use when parsing. |
| 54 | + endian: The endianness to use when parsing (little, big, network, <, >, !, @ or =). |
49 | 55 | pointer: The pointer type to use for pointers. |
50 | 56 | """ |
51 | 57 |
|
52 | 58 | DEF_CSTYLE = 1 |
53 | 59 | DEF_LEGACY = 2 |
54 | 60 |
|
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) |
57 | 63 |
|
58 | 64 | self.consts = {} |
59 | 65 | self.lookups = {} |
@@ -217,9 +223,11 @@ def __copy__(self) -> cstruct: |
217 | 223 |
|
218 | 224 | # Update typedefs to point to the new cstruct instance |
219 | 225 | for name, type in self.typedefs.items(): |
| 226 | + # Skip all the default types |
220 | 227 | if name in cs.typedefs: |
221 | 228 | continue |
222 | 229 |
|
| 230 | + # Update the cstruct instance for all other types |
223 | 231 | if isinstance(type, MetaType): |
224 | 232 | new_type = copy.copy(type) |
225 | 233 | new_type.cs = cs |
@@ -266,6 +274,33 @@ def add_custom_type( |
266 | 274 | alignment: The alignment of the type. |
267 | 275 | **kwargs: Additional attributes to add to the type. |
268 | 276 | """ |
| 277 | + # In cstruct 5.0 we changed the function signature of _read and _write |
| 278 | + # Check if the function signature is compatible, and throw an error if not |
| 279 | + for type_to_check in (type_, type_.ArrayType): |
| 280 | + type_name = type_.__name__ + (f".{type_.ArrayType.__name__}" if type_to_check is type_.ArrayType else "") |
| 281 | + |
| 282 | + for method in ("_read", "_read_array", "_read_0", "_write", "_write_array", "_write_0"): |
| 283 | + if not hasattr(type_to_check, method): |
| 284 | + continue |
| 285 | + |
| 286 | + signature = inspect.signature(getattr(type_to_check, method)) |
| 287 | + |
| 288 | + # We added a few keyword-only parameters to the function signature, but any custom type will |
| 289 | + # continue to work fine as long as they accept **kwargs |
| 290 | + if not any(param.kind == inspect.Parameter.VAR_KEYWORD for param in signature.parameters.values()): |
| 291 | + raise Error( |
| 292 | + f"Custom type {type_name} has an incompatible {method} method signature. " |
| 293 | + "Please refer to the changelog of dissect.cstruct 5.0 for more information." |
| 294 | + ) |
| 295 | + |
| 296 | + # Only warn if the method doesn't accept an endian parameter |
| 297 | + if "endian" not in signature.parameters: |
| 298 | + warnings.warn( |
| 299 | + f"Custom type {type_name} is missing the 'endian' keyword-only parameter in its {method} method. " # noqa: E501 |
| 300 | + "Please refer to the changelog of dissect.cstruct 5.0 for more information.", |
| 301 | + stacklevel=2, |
| 302 | + ) |
| 303 | + |
269 | 304 | self.add_type(name, self._make_type(name, (type_,), size, alignment=alignment, attrs=kwargs)) |
270 | 305 |
|
271 | 306 | def load(self, definition: str, deftype: int | None = None, **kwargs) -> cstruct: |
@@ -357,6 +392,25 @@ def copy(self) -> cstruct: |
357 | 392 | """ |
358 | 393 | return copy.copy(self) |
359 | 394 |
|
| 395 | + @contextmanager |
| 396 | + def endianness(self, endian: AllowedEndianness) -> Iterator[cstruct]: |
| 397 | + """Context manager for temporarily changing the endianness of this cstruct instance.""" |
| 398 | + new = self.copy() |
| 399 | + new.endian = normalize_endianness(endian) |
| 400 | + yield new |
| 401 | + |
| 402 | + @contextmanager |
| 403 | + def big_endian(self) -> Iterator[cstruct]: |
| 404 | + """Context manager for temporarily changing the endianness to big endian.""" |
| 405 | + with self.endianness(">") as cs: |
| 406 | + yield cs |
| 407 | + |
| 408 | + @contextmanager |
| 409 | + def little_endian(self) -> Iterator[cstruct]: |
| 410 | + """Context manager for temporarily changing the endianness to little endian.""" |
| 411 | + with self.endianness("<") as cs: |
| 412 | + yield cs |
| 413 | + |
360 | 414 | def _make_type( |
361 | 415 | self, |
362 | 416 | name: str, |
|
0 commit comments