Skip to content

Commit 0ab1683

Browse files
authored
Add examples and unit tests for custom types in cstruct v4 (#83)
1 parent d655171 commit 0ab1683

File tree

4 files changed

+175
-12
lines changed

4 files changed

+175
-12
lines changed

dissect/cstruct/types/base.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
from typing import TYPE_CHECKING, Any, BinaryIO, Callable
66

77
from dissect.cstruct.exceptions import ArraySizeError
8+
from dissect.cstruct.expression import Expression
89

910
if TYPE_CHECKING:
1011
from dissect.cstruct.cstruct import cstruct
11-
from dissect.cstruct.expression import Expression
1212

1313

1414
EOF = -0xE0F # Negative counts are illegal anyway, so abuse that for our EOF sentinel
@@ -210,6 +210,13 @@ class BaseType(metaclass=MetaType):
210210
dumps = _overload(MetaType.dumps)
211211
write = _overload(MetaType.write)
212212

213+
def __len__(self) -> int:
214+
"""Return the byte size of the type."""
215+
if self.__class__.size is None:
216+
raise TypeError("Dynamic size")
217+
218+
return self.__class__.size
219+
213220

214221
class ArrayMetaType(MetaType):
215222
"""Base metaclass for array-like types."""
@@ -222,15 +229,17 @@ def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Array:
222229
if cls.null_terminated:
223230
return cls.type._read_0(stream, context)
224231

225-
if cls.dynamic:
232+
if isinstance(cls.num_entries, int):
233+
num = max(0, cls.num_entries)
234+
elif cls.num_entries is None:
235+
num = EOF
236+
elif isinstance(cls.num_entries, Expression):
226237
try:
227238
num = max(0, cls.num_entries.evaluate(context))
228239
except Exception:
229240
if cls.num_entries.expression != "EOF":
230241
raise
231242
num = EOF
232-
else:
233-
num = max(0, cls.num_entries)
234243

235244
return cls.type._read_array(stream, num, context)
236245

examples/protobuf.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, BinaryIO
4+
5+
from dissect.cstruct import cstruct
6+
from dissect.cstruct.types import BaseType
7+
8+
9+
class ProtobufVarint(BaseType):
10+
"""Implements a protobuf integer type for dissect.cstruct that can span a variable amount of bytes.
11+
12+
Mainly follows the BaseType implementation with minor tweaks
13+
to support protobuf's msb varint implementation.
14+
15+
Resources:
16+
- https://protobuf.dev/programming-guides/encoding/
17+
- https://github.com/protocolbuffers/protobuf/blob/main/python/google/protobuf/internal/decoder.py
18+
"""
19+
20+
@classmethod
21+
def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> int:
22+
return decode_varint(stream)
23+
24+
@classmethod
25+
def _write(cls, stream: BinaryIO, data: int) -> int:
26+
return stream.write(encode_varint(data))
27+
28+
29+
def decode_varint(stream: BinaryIO) -> int:
30+
"""Reads a varint from the provided buffer stream.
31+
32+
If we have not reached the end of a varint, the msb will be 1.
33+
We read every byte from our current position until the msb is 0.
34+
"""
35+
result = 0
36+
i = 0
37+
while True:
38+
byte = stream.read(1)
39+
result |= (byte[0] & 0x7F) << (i * 7)
40+
i += 1
41+
if byte[0] & 0x80 == 0:
42+
break
43+
44+
return result
45+
46+
47+
def encode_varint(number: int) -> bytes:
48+
"""Encode a decoded protobuf varint to its original bytes."""
49+
buf = []
50+
while True:
51+
towrite = number & 0x7F
52+
number >>= 7
53+
if number:
54+
buf.append(towrite | 0x80)
55+
else:
56+
buf.append(towrite)
57+
break
58+
return bytes(buf)
59+
60+
61+
if __name__ == "__main__":
62+
cdef = """
63+
struct foo {
64+
uint32 foo;
65+
varint size;
66+
char bar[size];
67+
};
68+
"""
69+
70+
cs = cstruct(endian=">")
71+
cs.add_custom_type("varint", ProtobufVarint)
72+
cs.load(cdef, compiled=False)
73+
74+
aaa = b"a" * 123456
75+
buf = b"\x00\x00\x00\x01\xc0\xc4\x07" + aaa
76+
foo = cs.foo(buf + b"\x01\x02\x03")
77+
assert foo.foo == 1
78+
assert foo.size == 123456
79+
assert foo.bar == aaa
80+
assert foo.dumps() == buf
81+
82+
assert cs.varint[2](b"\x80\x01\x80\x02") == [128, 256]
83+
assert cs.varint[2].dumps([128, 256]) == b"\x80\x01\x80\x02"

examples/secdesc.py

+5-8
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@
5252
struct ACCESS_ALLOWED_OBJECT_ACE {
5353
uint32 Mask;
5454
uint32 Flags;
55-
char ObjectType[Flags & 1 * 16];
56-
char InheritedObjectType[Flags & 2 * 8];
55+
char ObjectType[(Flags & 1) * 16];
56+
char InheritedObjectType[(Flags & 2) * 8];
5757
LDAP_SID Sid;
5858
};
5959
"""
@@ -97,12 +97,9 @@ def __init__(self, fh=None, in_obj=None):
9797
self.ldap_sid = in_obj
9898

9999
def __repr__(self):
100-
return "S-{}-{}-{}".format(
101-
self.ldap_sid.Revision,
102-
bytearray(self.ldap_sid.IdentifierAuthority.Value)[5],
103-
"-".join(["{}".format(v) for v in self.ldap_sid.SubAuthority]),
104-
)
105-
100+
authority = bytearray(self.ldap_sid.IdentifierAuthority.Value)[5]
101+
sub_authority = "-".join(f"{v}" for v in self.ldap_sid.SubAuthority)
102+
return f"S-{self.ldap_sid.Revision}-{authority}-{sub_authority}"
106103

107104
class ACL:
108105
def __init__(self, fh):

tests/test_types_custom.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, BinaryIO
4+
5+
import pytest
6+
7+
from dissect.cstruct import cstruct
8+
from dissect.cstruct.types import BaseType, MetaType
9+
10+
11+
class EtwPointer(BaseType):
12+
type: MetaType
13+
size: int | None
14+
15+
@classmethod
16+
def _read(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> BaseType:
17+
return cls.type._read(stream, context)
18+
19+
@classmethod
20+
def _read_0(cls, stream: BinaryIO, context: dict[str, Any] | None = None) -> list[BaseType]:
21+
return cls.type._read_0(stream, context)
22+
23+
@classmethod
24+
def _write(cls, stream: BinaryIO, data: Any) -> int:
25+
return cls.type._write(stream, data)
26+
27+
@classmethod
28+
def as_32bit(cls) -> None:
29+
cls.type = cls.cs.uint32
30+
cls.size = 4
31+
32+
@classmethod
33+
def as_64bit(cls) -> None:
34+
cls.type = cls.cs.uint64
35+
cls.size = 8
36+
37+
38+
def test_adding_custom_type(cs: cstruct) -> None:
39+
cs.add_custom_type("EtwPointer", EtwPointer)
40+
41+
cs.EtwPointer.as_64bit()
42+
assert cs.EtwPointer.type is cs.uint64
43+
assert len(cs.EtwPointer) == 8
44+
assert cs.EtwPointer(b"\xDE\xAD\xBE\xEF" * 2).dumps() == b"\xDE\xAD\xBE\xEF" * 2
45+
46+
cs.EtwPointer.as_32bit()
47+
assert cs.EtwPointer.type is cs.uint32
48+
assert len(cs.EtwPointer) == 4
49+
assert cs.EtwPointer(b"\xDE\xAD\xBE\xEF" * 2).dumps() == b"\xDE\xAD\xBE\xEF"
50+
51+
52+
def test_using_type_in_struct(cs: cstruct) -> None:
53+
cs.add_custom_type("EtwPointer", EtwPointer)
54+
55+
struct_definition = """
56+
struct test {
57+
EtwPointer data;
58+
uint64 data2;
59+
};
60+
"""
61+
62+
cs.load(struct_definition)
63+
64+
cs.EtwPointer.as_64bit()
65+
assert len(cs.test().data) == 8
66+
67+
with pytest.raises(EOFError):
68+
# Input too small
69+
cs.test(b"\xDE\xAD\xBE\xEF" * 3)
70+
71+
cs.EtwPointer.as_32bit()
72+
assert len(cs.test().data) == 4
73+
74+
assert cs.test(b"\xDE\xAD\xBE\xEF" * 3).data.dumps() == b"\xDE\xAD\xBE\xEF"

0 commit comments

Comments
 (0)