Skip to content

Commit bf7bd5c

Browse files
committed
Add support for EOF length identifier
1 parent 5458791 commit bf7bd5c

File tree

5 files changed

+111
-13
lines changed

5 files changed

+111
-13
lines changed

dissect/cstruct/types/base.py

+24-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
from typing import TYPE_CHECKING, Any, BinaryIO, Optional, Union
55

66
from dissect.cstruct.exceptions import ArraySizeError
7-
from dissect.cstruct.expression import Expression
87

98
if TYPE_CHECKING:
109
from dissect.cstruct.cstruct import cstruct
10+
from dissect.cstruct.expression import Expression
11+
12+
13+
EOF = -0xE0F # Negative counts are illegal anyway, so abuse that for our EOF sentinel
1114

1215

1316
class MetaType(type):
@@ -93,6 +96,15 @@ def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] = Non
9396
count: The amount of values to read.
9497
context: Optional reading context.
9598
"""
99+
if count == EOF:
100+
result = []
101+
while True:
102+
try:
103+
result.append(cls._read(stream, context))
104+
except EOFError:
105+
break
106+
return result
107+
96108
return [cls._read(stream, context) for _ in range(count)]
97109

98110
def _read_0(cls, stream: BinaryIO, context: dict[str, Any] = None) -> list[BaseType]:
@@ -168,8 +180,17 @@ def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Array:
168180
if cls.null_terminated:
169181
return cls.type._read_0(stream, context)
170182

171-
num = cls.num_entries.evaluate(context) if cls.dynamic else cls.num_entries
172-
return cls.type._read_array(stream, max(0, num), context)
183+
if cls.dynamic:
184+
try:
185+
num = max(0, cls.num_entries.evaluate(context))
186+
except Exception:
187+
if cls.num_entries.expression != "EOF":
188+
raise
189+
num = EOF
190+
else:
191+
num = max(0, cls.num_entries)
192+
193+
return cls.type._read_array(stream, num, context)
173194

174195

175196
class Array(list, BaseType, metaclass=ArrayMetaType):

dissect/cstruct/types/char.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import Any, BinaryIO, Union
44

5-
from dissect.cstruct.types.base import ArrayMetaType, BaseType
5+
from dissect.cstruct.types.base import EOF, ArrayMetaType, BaseType
66

77

88
class Char(bytes, BaseType):
@@ -17,8 +17,8 @@ def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] = Non
1717
if count == 0:
1818
return b""
1919

20-
data = stream.read(count)
21-
if len(data) != count:
20+
data = stream.read(-1 if count == EOF else count)
21+
if count != EOF and len(data) != count:
2222
raise EOFError(f"Read {len(data)} bytes, but expected {count}")
2323

2424
return type.__call__(cls, data)

dissect/cstruct/types/packed.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from struct import Struct
55
from typing import Any, BinaryIO
66

7-
from dissect.cstruct.types.base import BaseType
7+
from dissect.cstruct.types.base import EOF, BaseType
88

99

1010
@lru_cache(1024)
@@ -23,8 +23,14 @@ def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Packed:
2323

2424
@classmethod
2525
def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] = None) -> list[Packed]:
26-
length = cls.size * count
27-
data = stream.read(length)
26+
if count == EOF:
27+
data = stream.read()
28+
length = len(data)
29+
count = length // cls.size
30+
else:
31+
length = cls.size * count
32+
data = stream.read(length)
33+
2834
fmt = _struct(cls.cs.endian, f"{count}{cls.packchar}")
2935

3036
if len(data) != length:

dissect/cstruct/types/wchar.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44
from typing import Any, BinaryIO
55

6-
from dissect.cstruct.types.base import ArrayMetaType, BaseType
6+
from dissect.cstruct.types.base import EOF, ArrayMetaType, BaseType
77

88

99
class Wchar(str, BaseType):
@@ -23,12 +23,14 @@ def _read(cls, stream: BinaryIO, context: dict[str, Any] = None) -> Wchar:
2323

2424
@classmethod
2525
def _read_array(cls, stream: BinaryIO, count: int, context: dict[str, Any] = None) -> Wchar:
26-
count *= 2
2726
if count == 0:
2827
return ""
2928

30-
data = stream.read(count)
31-
if len(data) != count:
29+
if count != EOF:
30+
count *= 2
31+
32+
data = stream.read(-1 if count == EOF else count)
33+
if count != EOF and len(data) != count:
3234
raise EOFError(f"Read {len(data)} bytes, but expected {count}")
3335

3436
return type.__call__(cls, data.decode(cls.__encoding_map__[cls.cs.endian]))

tests/test_types_base.py

+69
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,78 @@
33
from dissect.cstruct.cstruct import cstruct
44
from dissect.cstruct.exceptions import ArraySizeError
55

6+
from .utils import verify_compiled
7+
68

79
def test_array_size_mismatch(cs: cstruct):
810
with pytest.raises(ArraySizeError):
911
cs.uint8[2]([1, 2, 3]).dumps()
1012

1113
assert cs.uint8[2]([1, 2]).dumps()
14+
15+
16+
def test_eof(cs: cstruct, compiled: bool):
17+
cdef = """
18+
struct test_char {
19+
char data[EOF];
20+
};
21+
22+
struct test_wchar {
23+
wchar data[EOF];
24+
};
25+
26+
struct test_packed {
27+
uint16 data[EOF];
28+
};
29+
30+
struct test_int {
31+
uint24 data[EOF];
32+
};
33+
34+
enum Test : uint16 {
35+
A = 1
36+
};
37+
38+
struct test_enum {
39+
Test data[EOF];
40+
};
41+
42+
struct test_eof_field {
43+
uint8 EOF;
44+
char data[EOF];
45+
uint8 remainder;
46+
};
47+
"""
48+
cs.load(cdef, compiled=compiled)
49+
50+
assert verify_compiled(cs.test_char, compiled)
51+
assert verify_compiled(cs.test_wchar, compiled)
52+
assert verify_compiled(cs.test_packed, compiled)
53+
assert verify_compiled(cs.test_int, compiled)
54+
assert verify_compiled(cs.test_enum, compiled)
55+
assert verify_compiled(cs.test_eof_field, compiled)
56+
57+
test_char = cs.test_char(b"abc")
58+
assert test_char.data == b"abc"
59+
assert test_char.dumps() == b"abc"
60+
61+
test_wchar = cs.test_wchar("abc".encode("utf-16-le"))
62+
assert test_wchar.data == "abc"
63+
assert test_wchar.dumps() == "abc".encode("utf-16-le")
64+
65+
test_packed = cs.test_packed(b"\x01\x00\x02\x00")
66+
assert test_packed.data == [1, 2]
67+
assert test_packed.dumps() == b"\x01\x00\x02\x00"
68+
69+
test_int = cs.test_int(b"\x01\x00\x00\x02\x00\x00")
70+
assert test_int.data == [1, 2]
71+
assert test_int.dumps() == b"\x01\x00\x00\x02\x00\x00"
72+
73+
test_enum = cs.test_enum(b"\x01\x00")
74+
assert test_enum.data == [cs.Test.A]
75+
assert test_enum.dumps() == b"\x01\x00"
76+
77+
test_eof_field = cs.test_eof_field(b"\x01a\x02")
78+
assert test_eof_field.data == b"a"
79+
assert test_eof_field.remainder == 2
80+
assert test_eof_field.dumps() == b"\x01a\x02"

0 commit comments

Comments
 (0)