Skip to content

Commit 0edfc10

Browse files
authored
Add method to dump C definition (#160)
1 parent 0191708 commit 0edfc10

8 files changed

Lines changed: 497 additions & 4 deletions

File tree

dissect/cstruct/cstruct.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,42 @@ def loadfile(self, path: str, deftype: int | None = None, **kwargs) -> None:
302302
with Path(path).open() as fh:
303303
self.load(fh.read(), deftype, **kwargs)
304304

305+
def cdef(self) -> str:
306+
"""Render all constants, structure, union, enum and flag definitions back to their C-style definitions.
307+
308+
Note that the result is semantically equivalent to the original definitions, but not necessarily identical.
309+
310+
Returns:
311+
The C-style definitions as a string.
312+
"""
313+
empty = cstruct()
314+
315+
blocks = []
316+
317+
defines = []
318+
for name, value in self.consts.items():
319+
if name in empty.consts:
320+
continue
321+
322+
defines.append(f"#define {name} {value!r}")
323+
324+
if defines:
325+
blocks.append("\n".join(defines))
326+
327+
for name, typedef in self.typedefs.items():
328+
if name in empty.typedefs or not isinstance(typedef, type):
329+
continue
330+
331+
if not issubclass(typedef, (Structure, Enum, Flag)):
332+
continue
333+
334+
if typedef.__name__ == name:
335+
blocks.append(typedef.cdef())
336+
else:
337+
blocks.append(f"typedef {typedef.__name__} {name};")
338+
339+
return "\n\n".join(blocks)
340+
305341
def read(self, name: str, stream: BinaryIO) -> Any:
306342
"""Parse data using a given type.
307343

dissect/cstruct/types/enum.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,24 @@ def _write_0(cls, stream: BinaryIO, array: list[BaseType | int]) -> int:
104104
data = [entry.value if isinstance(entry, _Enum) else entry for entry in array]
105105
return cls._write_array(stream, [*data, cls.type.__default__()])
106106

107+
def cdef(cls) -> str:
108+
"""Render this enum (or flag) back to its C-style definition.
109+
110+
Returns:
111+
The C-style definition as a string.
112+
"""
113+
keyword = "flag" if issubclass(cls, IntFlag) else "enum"
114+
underlying = cls.type.__name__
115+
title = f"{keyword} {cls.__name__} : {underlying}" if cls.__name__ else f"{keyword} : {underlying}"
116+
117+
lines = [f"{title} {{"]
118+
for member_name, member in cls.__members__.items():
119+
value = hex(member.value) if keyword == "flag" else member.value
120+
lines.append(f" {member_name} = {value},")
121+
lines.append("};")
122+
123+
return "\n".join(lines)
124+
107125

108126
def _fix_alias_members(cls: type[Enum]) -> None:
109127
# Emulate aenum NoAlias behaviour

dissect/cstruct/types/structure.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
from typing import TYPE_CHECKING, Any, BinaryIO
1414

1515
from dissect.cstruct.bitbuffer import BitBuffer
16+
from dissect.cstruct.expression import Expression
17+
from dissect.cstruct.lexer import IDENTIFIER_TYPES
1618
from dissect.cstruct.types.base import (
19+
BaseArray,
1720
BaseType,
1821
MetaType,
1922
_is_buffer_type,
@@ -380,6 +383,102 @@ def commit(cls) -> None:
380383
for key, value in classdict.items():
381384
setattr(cls, key, value)
382385

386+
def cdef(cls, *, recursive: bool = False) -> str:
387+
"""Render this structure (or union) back to its C-style definition.
388+
389+
When ``recursive=True``, any ``#define`` constants referenced in array sizes are emitted as well, so that the
390+
result can be parsed on its own. Note that the result is semantically equivalent to the original definition,
391+
but not necessarily identical.
392+
393+
Args:
394+
recursive: Also emit the definitions of any types referenced by this structure.
395+
396+
Returns:
397+
The C-style definition as a string.
398+
"""
399+
400+
def _decompose(type_: MetaType) -> tuple[MetaType, int, list[int | Expression | None]]:
401+
dimensions = []
402+
while issubclass(type_, BaseArray):
403+
dimensions.append(type_.num_entries)
404+
type_ = type_.type
405+
406+
stars = 0
407+
while issubclass(type_, Pointer):
408+
stars += 1
409+
type_ = type_.type
410+
411+
return type_, stars, dimensions
412+
413+
def _deps(type_: StructureMetaType, seen: set[MetaType]) -> list[MetaType | str]:
414+
deps = []
415+
for field in type_.__fields__:
416+
base, _, dimensions = _decompose(field.type)
417+
418+
for dim in dimensions:
419+
if isinstance(dim, Expression):
420+
for token in dim._tokens:
421+
if (
422+
token.type in IDENTIFIER_TYPES
423+
and token.value in cls.cs.consts
424+
and token.value not in seen
425+
):
426+
deps.append(token.value)
427+
seen.add(token.value)
428+
429+
if isinstance(base, StructureMetaType) and base not in seen:
430+
if base.__anonymous__:
431+
# Anonymous structures are not emitted, but their dependencies are
432+
deps.extend(_deps(base, seen))
433+
else:
434+
seen.add(base)
435+
deps.extend(_deps(base, seen))
436+
deps.append(base)
437+
438+
elif isinstance(base, EnumMetaType) and base not in seen:
439+
seen.add(base)
440+
deps.append(base)
441+
442+
return deps
443+
444+
def _render_struct(type_: StructureMetaType, name: str) -> list[str]:
445+
keyword = "union" if isinstance(type_, UnionMetaType) else "struct"
446+
title = f"{keyword} {name}" if name else keyword
447+
448+
lines = [f"{title} {{"]
449+
for field in type_.__fields__:
450+
base, stars, dimensions = _decompose(field.type)
451+
452+
declarator = "*" * stars + (field.name or "") + "".join(f"[{dim or ''}]" for dim in dimensions)
453+
if field.bits is not None:
454+
declarator = f"{declarator} : {field.bits}" if declarator else f": {field.bits}"
455+
456+
if isinstance(base, StructureMetaType) and base.__anonymous__:
457+
# Anonymous struct/union: inline the full definition
458+
lines.extend(f" {line}" for line in _render_struct(base, ""))
459+
lines[-1] = f"{lines[-1]} {declarator};" if declarator else f"{lines[-1]};"
460+
else:
461+
prefix = f"{base.__name__} " if declarator else base.__name__
462+
lines.append(f" {prefix}{declarator};")
463+
464+
lines.append("}")
465+
return lines
466+
467+
deps = _deps(cls, {cls}) if recursive else []
468+
deps.append(cls)
469+
470+
consts = []
471+
blocks = []
472+
for type_ in deps:
473+
if isinstance(type_, str):
474+
consts.append(f"#define {type_} {cls.cs.consts[type_]!r}")
475+
elif isinstance(type_, EnumMetaType):
476+
blocks.append(type_.cdef())
477+
else:
478+
blocks.append("\n".join(_render_struct(type_, type_.__name__)) + ";")
479+
480+
return ("\n".join(consts) + "\n\n" if consts else "") + ("\n\n".join(blocks) if blocks else "")
481+
383482

384483
class Structure(BaseType, metaclass=StructureMetaType):
385484
"""Base class for cstruct structure type classes.

tests/test_basic.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,3 +551,86 @@ def test_copy(cs: cstruct) -> None:
551551
# Verify that types in the copied cstruct reference the copied cstruct
552552
assert cs_copy.resolve("test").cs is cs_copy
553553
assert cs_copy.resolve("test2").cs is cs_copy
554+
555+
556+
def test_cdef(cs: cstruct) -> None:
557+
"""Test that a cstruct instance can render all its definitions back to C-style definitions."""
558+
cs.load(
559+
"""
560+
#define MAGIC_LENGTH 4
561+
enum Color : uint16 { RED = 1, GREEN = 2, BLUE = 4 };
562+
struct Point { int32 x; int32 y; };
563+
union Val { uint32 as_int; float as_float; };
564+
typedef Point Coord;
565+
struct Header {
566+
char magic[MAGIC_LENGTH];
567+
Color color;
568+
Point points[2];
569+
Val v;
570+
};
571+
"""
572+
)
573+
574+
assert cs.cdef() == textwrap.dedent(
575+
"""\
576+
#define MAGIC_LENGTH 4
577+
578+
enum Color : uint16 {
579+
RED = 1,
580+
GREEN = 2,
581+
BLUE = 4,
582+
};
583+
584+
struct Point {
585+
int32 x;
586+
int32 y;
587+
};
588+
589+
union Val {
590+
uint32 as_int;
591+
float as_float;
592+
};
593+
594+
typedef Point Coord;
595+
596+
struct Header {
597+
char magic[4];
598+
Color color;
599+
Point points[2];
600+
Val v;
601+
};"""
602+
)
603+
604+
# The full dump should be parseable on its own into equivalent types
605+
reparsed = cstruct()
606+
reparsed.load(cs.cdef())
607+
assert reparsed.consts == cs.consts
608+
for name in ("Color", "Point", "Val", "Coord", "Header"):
609+
assert getattr(cs, name).size == getattr(reparsed, name).size
610+
611+
612+
def test_cdef_defines(cs: cstruct) -> None:
613+
"""Test that ``#define`` constants (including those referenced in expressions) are dumped and round-trip."""
614+
cs.load(
615+
"""
616+
#define INT_CONST 8
617+
#define STR_CONST "hello"
618+
#define BYTES_CONST b"world"
619+
620+
struct Test {
621+
uint32 count;
622+
uint8 data[count * INT_CONST];
623+
};
624+
"""
625+
)
626+
627+
src = cs.cdef()
628+
assert "#define INT_CONST 8" in src
629+
assert "#define STR_CONST 'hello'" in src
630+
assert "#define BYTES_CONST b'world'" in src
631+
# The constant referenced in the (dynamic) array expression must be emitted
632+
assert "uint8 data[count * INT_CONST];" in src
633+
634+
reparsed = cstruct()
635+
reparsed.load(src)
636+
assert reparsed.consts == cs.consts

tests/test_types_enum.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from enum import Enum as StdEnum
4+
from textwrap import dedent
45
from typing import TYPE_CHECKING
56

67
import pytest
@@ -448,3 +449,16 @@ def test_enum_default_default(cs: cstruct) -> None:
448449
cs.load(cdef)
449450

450451
assert cs.test.__default__() == cs.test.default == cs.test(0)
452+
453+
454+
def test_cdef_enum(cs: cstruct) -> None:
455+
cs.load("enum Color : uint16 { RED = 1, GREEN = 2, BLUE = 4 };")
456+
457+
assert cs.Color.cdef() == dedent(
458+
"""\
459+
enum Color : uint16 {
460+
RED = 1,
461+
GREEN = 2,
462+
BLUE = 4,
463+
};"""
464+
)

tests/test_types_flag.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from enum import Flag as StdFlag
4+
from textwrap import dedent
45
from typing import TYPE_CHECKING
56

67
import pytest
@@ -282,3 +283,16 @@ def test_flag_default(cs: cstruct) -> None:
282283
assert cs.test.__default__() == cs.test(0)
283284
assert cs.test[1].__default__() == [cs.test(0)]
284285
assert cs.test[None].__default__() == []
286+
287+
288+
def test_cdef_flag(cs: cstruct) -> None:
289+
cs.load("flag Perm : uint8 { R, W, X };")
290+
291+
assert cs.Perm.cdef() == dedent(
292+
"""\
293+
flag Perm : uint8 {
294+
R = 0x1,
295+
W = 0x2,
296+
X = 0x4,
297+
};"""
298+
)

0 commit comments

Comments
 (0)