Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 104 additions & 63 deletions dissect/cstruct/cstruct.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
Void,
Wchar,
)
from dissect.cstruct.types.base import MetaType

if TYPE_CHECKING:
from collections.abc import Iterable
Expand Down Expand Up @@ -55,10 +54,12 @@ class cstruct:
def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = None):
self.endian = endian

self.consts = {}
self.includes = []
self.consts: dict[str, int | str | bytes] = {}
self.types: dict[str, type[BaseType]] = {}
self.includes: list[str] = []

# fmt: off
self.typedefs = {
initial_types = {
# Internal types
"int8": self._make_packed_type("int8", "b", int),
"uint8": self._make_packed_type("uint8", "B", int),
Expand Down Expand Up @@ -102,6 +103,22 @@ def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = N
"signed long long": "int64",
"unsigned long long": "uint64",

# Other convenience types
"u8": "uint8",
"u16": "uint16",
"u32": "uint32",
"u64": "uint64",
"u128": "uint128",
"__u8": "uint8",
"__u16": "uint16",
"__u32": "uint32",
"__u64": "uint64",
"__u128": "uint128",
"uchar": "uint8",
"ushort": "uint16",
"uint": "uint32",
"ulong": "uint32",

# Windows types
"BYTE": "uint8",
"CHAR": "char",
Expand Down Expand Up @@ -169,25 +186,12 @@ def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = N
"_DWORD": "uint32",
"_QWORD": "uint64",
"_OWORD": "uint128",

# Other convenience types
"u8": "uint8",
"u16": "uint16",
"u32": "uint32",
"u64": "uint64",
"u128": "uint128",
"__u8": "uint8",
"__u16": "uint16",
"__u32": "uint32",
"__u64": "uint64",
"__u128": "uint128",
"uchar": "uint8",
"ushort": "uint16",
"uint": "uint32",
"ulong": "uint32",
}
# fmt: on

for name, type_ in initial_types.items():
self.add_type(name, type_)

pointer = pointer or ("uint64" if sys.maxsize > 2**32 else "uint32")
self.pointer: type[BaseType] = self.resolve(pointer)
self._anonymous_count = 0
Expand All @@ -202,27 +206,25 @@ def __getattr__(self, attr: str) -> Any:
pass

try:
return self.resolve(self.typedefs[attr])
return self.types[attr]
except KeyError:
pass

raise AttributeError(f"Invalid attribute: {attr}")
raise AttributeError(f"'{type(self).__name__}' object has no attribute {attr!r}")

def __copy__(self) -> cstruct:
cs = cstruct(endian=self.endian, pointer=self.pointer.__name__)
cs._anonymous_count = self._anonymous_count
cs.consts = self.consts.copy()
cs.includes = self.includes.copy()

# Update typedefs to point to the new cstruct instance
for name, type in self.typedefs.items():
if name in cs.typedefs:
continue
# Update types to point to the new cstruct instance
for name, type_ in self.types.items():
new_type = copy.copy(type_)
new_type.cs = cs
cs.add_type(name, new_type, replace=True)

if isinstance(type, MetaType):
new_type = copy.copy(type)
new_type.cs = cs
cs.typedefs[name] = new_type
for name, value in self.consts.items():
cs.add_const(name, value)

return cs

Expand All @@ -231,22 +233,34 @@ def _next_anonymous(self) -> str:
self._anonymous_count += 1
return name

def _add_attr(self, name: str, value: Any) -> None:
# Names that collide with the cstruct class (e.g. a struct named ``load``) are not set as attributes
# to avoid breaking the instance. They remain accessible through ``resolve`` and the type dicts.
if name in _RESERVED_NAMES:
return
setattr(self, name, value)

Comment thread
Schamper marked this conversation as resolved.
def add_type(self, name: str, type_: type[BaseType] | str, replace: bool = False) -> None:
"""Add a type or type reference.
"""Add a type or type alias.

Only use this method when creating type aliases or adding already bound types.
All types will be resolved to their actual type objects prior to being added.

Args:
name: Name of the type to be added.
type_: The type to be added. Can be a str reference to another type or a compatible type class.
If a str is given, it will be resolved to the actual type object.
replace: Whether to replace the type if it already exists.

Raises:
ValueError: If the type already exists.
"""
if not replace and (name in self.typedefs and self.resolve(self.typedefs[name]) != self.resolve(type_)):
typeobj = self.resolve(type_)
if not replace and (existing := self.types.get(name)) is not None and existing is not typeobj:
raise ValueError(f"Duplicate type: {name}")

self.typedefs[name] = type_
self.types[name] = typeobj
self._add_attr(name, typeobj)

addtype = add_type

Expand All @@ -267,6 +281,28 @@ def add_custom_type(
"""
self.add_type(name, self._make_type(name, (type_,), size, alignment=alignment, attrs=kwargs))

def add_const(self, name: str, value: Any) -> None:
"""Add a constant value.

Args:
name: Name of the constant to be added.
value: The value of the constant.
"""
self.consts[name] = value
self._add_attr(name, value)

def del_const(self, name: str) -> None:
"""Delete a constant value.

Args:
name: Name of the constant to be deleted.

Raises:
KeyError: If the constant does not exist.
"""
del self.consts[name]
self.__dict__.pop(name, None)

def load(self, definition: str, deftype: int | None = None, **kwargs) -> cstruct:
"""Parse structures from the given definitions using the given definition type.

Expand Down Expand Up @@ -324,8 +360,8 @@ def cdef(self) -> str:
if defines:
blocks.append("\n".join(defines))

for name, typedef in self.typedefs.items():
if name in empty.typedefs or not isinstance(typedef, type):
for name, typedef in self.types.items():
if name in empty.types or not isinstance(typedef, type):
continue

if not issubclass(typedef, (Structure, Enum, Flag)):
Expand Down Expand Up @@ -365,20 +401,13 @@ def resolve(self, name: type[BaseType] | str) -> type[BaseType]:
Raises:
ResolveError: If the type can't be resolved.
"""
type_name = name
if not isinstance(type_name, str):
return type_name
if not isinstance(name, str):
return name

for _ in range(10):
if type_name not in self.typedefs:
raise ResolveError(f"Unknown type {name}")

type_name = self.typedefs[type_name]

if not isinstance(type_name, str):
return type_name

raise ResolveError(f"Recursion limit exceeded while resolving type {name}")
try:
return self.types[name]
except KeyError:
raise ResolveError(f"Unknown type {name}") from None

def copy(self) -> cstruct:
"""Create a copy of this cstruct instance.
Expand Down Expand Up @@ -555,6 +584,21 @@ class void(Void): ...
# signed long long: TypeAlias = int64
# unsigned long long: TypeAlias = uint64

u8: TypeAlias = uint8
u16: TypeAlias = uint16
u32: TypeAlias = uint32
u64: TypeAlias = uint64
u128: TypeAlias = uint128
__u8: TypeAlias = uint8
__u16: TypeAlias = uint16
__u32: TypeAlias = uint32
__u64: TypeAlias = uint64
__u128: TypeAlias = uint128
uchar: TypeAlias = uint8
ushort: TypeAlias = uint16
uint: TypeAlias = uint32
ulong: TypeAlias = uint32

BYTE: TypeAlias = uint8
CHAR: TypeAlias = char
SHORT: TypeAlias = int16
Expand Down Expand Up @@ -620,20 +664,17 @@ class void(Void): ...
_QWORD: TypeAlias = uint64
_OWORD: TypeAlias = uint128

u8: TypeAlias = uint8
u16: TypeAlias = uint16
u32: TypeAlias = uint32
u64: TypeAlias = uint64
u128: TypeAlias = uint128
__u8: TypeAlias = uint8
__u16: TypeAlias = uint16
__u32: TypeAlias = uint32
__u64: TypeAlias = uint64
__u128: TypeAlias = uint128
uchar: TypeAlias = uint8
ushort: TypeAlias = uint16
uint: TypeAlias = uint32
ulong: TypeAlias = uint32

# Attribute names that types and constants may never shadow: the public API and methods of the cstruct
# class itself, plus the instance attributes created in __init__.
_RESERVED_NAMES = frozenset(dir(cstruct)) | {
"endian",
"consts",
"types",
"includes",
"pointer",
"_anonymous_count",
}


def ctypes(structure: type[Structure]) -> type[_ctypes.Structure]:
Expand Down
13 changes: 6 additions & 7 deletions dissect/cstruct/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from dissect.cstruct import cstruct
from dissect.cstruct.lexer import Token

_MISSING = object()

BINARY_OPERATORS: dict[TokenType, Callable[[int, int], int]] = {
TokenType.PIPE: lambda a, b: a | b,
TokenType.CARET: lambda a, b: a ^ b,
Expand Down Expand Up @@ -164,16 +166,13 @@ def evaluate(self, cs: cstruct, context: dict[str, int] | None = None) -> int:
obj = context[part]
elif part in cs.consts:
obj = cs.consts[part]
elif part in cs.typedefs:
obj = cs.resolve(part)
elif part in cs.types:
obj = cs.types[part]
else:
raise self._error(f"Unknown identifier: '{ident}'", token=token)
else:
if isinstance(obj, dict) and part in obj:
obj = obj[part]
elif hasattr(obj, part):
obj = getattr(obj, part)
else:
obj = obj.get(part, _MISSING) if isinstance(obj, dict) else getattr(obj, part, _MISSING)
if obj is _MISSING:
raise self._error(f"Unknown identifier: '{ident}'", token=token)

try:
Expand Down
15 changes: 8 additions & 7 deletions dissect/cstruct/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ def _parse(self) -> None:

# If it's an anonymous enum/flag, add its members to the constants for convenience
if not type_.__name__:
self.cs.consts.update(type_.__members__)
for k, v in type_.__members__.items():
self.cs.add_const(k, v)

self._expect(TokenType.SEMICOLON)
else:
Expand Down Expand Up @@ -201,17 +202,17 @@ def _parse_define(self) -> None:
# If evaluation fails, just keep it as a string (e.g. for macro-like constants)
pass

self.cs.consts[name_token.value] = value
self.cs.add_const(name_token.value, value)

def _parse_undef(self) -> None:
"""Parse an undef directive and remove the constant."""
self._expect(TokenType.PP_UNDEF)

name_token = self._expect(TokenType.IDENTIFIER)
if name_token.value in self.cs.consts:
del self.cs.consts[name_token.value]
else:
raise self._error(f"constant {name_token.value!r} not defined", token=name_token)
try:
self.cs.del_const(name_token.value)
except KeyError:
raise self._error(f"constant {name_token.value!r} not defined", token=name_token) from None

def _parse_include(self) -> None:
"""Parse an include directive and add the included file to the includes list."""
Expand Down Expand Up @@ -544,7 +545,7 @@ def _parse_type_spec(self) -> type[BaseType]:
):
# This identifier is followed by a field delimiter, it should be the field name,
# UNLESS the current parts don't form a valid type yet.
if " ".join(parts) in self.cs.typedefs:
if " ".join(parts) in self.cs.types:
break

# Current parts don't resolve, consume and hope this completes the type name
Expand Down
Loading
Loading