Skip to content

Commit 91b0299

Browse files
SchamperCopilot
andcommitted
Process feedback
Co-authored-by: Copilot <copilot@github.com>
1 parent c5b1eb8 commit 91b0299

6 files changed

Lines changed: 282 additions & 54 deletions

File tree

dissect/cstruct/expression.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import TYPE_CHECKING
44

55
from dissect.cstruct.exceptions import ExpressionParserError
6-
from dissect.cstruct.lexer import _IDENTIFIER_TYPES, Lexer, TokenCursor, TokenType
6+
from dissect.cstruct.lexer import IDENTIFIER_TYPES, Lexer, TokenCursor, TokenType
77
from dissect.cstruct.utils import offsetof, sizeof
88

99
if TYPE_CHECKING:
@@ -12,7 +12,6 @@
1212
from dissect.cstruct import cstruct
1313
from dissect.cstruct.lexer import Token
1414

15-
1615
BINARY_OPERATORS: dict[TokenType, Callable[[int, int], int]] = {
1716
TokenType.PIPE: lambda a, b: a | b,
1817
TokenType.CARET: lambda a, b: a ^ b,
@@ -155,15 +154,32 @@ def evaluate(self, cs: cstruct, context: dict[str, int] | None = None) -> int:
155154
# Evaluate immediately
156155
self._evaluate_expression(cs)
157156

158-
elif token.type in _IDENTIFIER_TYPES:
159-
if token.value in context:
160-
self._queue.append(int(context[self._take().value]))
161-
162-
elif token.value in cs.consts:
163-
self._queue.append(int(cs.consts[self._take().value]))
164-
165-
else:
166-
raise self._error(f"Unknown identifier: '{token.value}'", token=token)
157+
elif token.type in IDENTIFIER_TYPES:
158+
ident = self._take().value
159+
160+
obj = None
161+
for i, part in enumerate(ident.split(".")):
162+
if i == 0:
163+
if part in context:
164+
obj = context[part]
165+
elif part in cs.consts:
166+
obj = cs.consts[part]
167+
elif part in cs.typedefs:
168+
obj = cs.resolve(part)
169+
else:
170+
raise self._error(f"Unknown identifier: '{ident}'", token=token)
171+
else:
172+
if isinstance(obj, dict) and part in obj:
173+
obj = obj[part]
174+
elif hasattr(obj, part):
175+
obj = getattr(obj, part)
176+
else:
177+
raise self._error(f"Unknown identifier: '{ident}'", token=token)
178+
179+
try:
180+
self._queue.append(int(obj))
181+
except (ValueError, TypeError):
182+
raise self._error(f"Identifier '{ident}' does not resolve to an integer", token=token)
167183

168184
elif token.type == TokenType.LPAREN:
169185
if self._previous().type == TokenType.NUMBER:

dissect/cstruct/lexer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,6 @@ def __repr__(self) -> str:
100100
"typedef": TokenType.TYPEDEF,
101101
}
102102

103-
_IDENTIFIER_TYPES = set(_C_KEYWORDS.values()) | {TokenType.IDENTIFIER}
104-
105103
_SINGLE_CHARS = {
106104
"{": TokenType.LBRACE,
107105
"}": TokenType.RBRACE,
@@ -125,9 +123,11 @@ def __repr__(self) -> str:
125123
"~": TokenType.TILDE,
126124
}
127125

128-
_RE_IDENTIFIER = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*")
126+
_RE_IDENTIFIER = re.compile(r"[a-zA-Z_.][a-zA-Z0-9_.]*")
129127
_RE_WHITESPACE = re.compile(r"[ \t\r\n]+")
130128

129+
IDENTIFIER_TYPES = set(_C_KEYWORDS.values()) | {TokenType.IDENTIFIER}
130+
131131

132132
def tokenize(data: str) -> list[Token]:
133133
"""Convenience function to tokenize input data."""

dissect/cstruct/parser.py

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
ParserError,
1212
)
1313
from dissect.cstruct.expression import Expression
14-
from dissect.cstruct.lexer import _IDENTIFIER_TYPES, TokenCursor, TokenType, tokenize
14+
from dissect.cstruct.lexer import IDENTIFIER_TYPES, TokenCursor, TokenType, tokenize
1515
from dissect.cstruct.types import BaseArray, BaseType, Field, Structure
1616

1717
if TYPE_CHECKING:
@@ -74,12 +74,7 @@ def parse(self, data: str) -> None:
7474

7575
data = _join_line_continuations(data)
7676

77-
# Tokenize and preprocess the input, then parse top-level definitions
7877
self._reset_tokens(tokenize(data))
79-
preprocessed_tokens = self._preprocess()
80-
self.reset()
81-
82-
self._reset_tokens(preprocessed_tokens)
8378
self._parse()
8479

8580
def _match(self, *types: TokenType) -> Token | None:
@@ -95,46 +90,41 @@ def _at(self, *types: TokenType) -> bool:
9590
def _error(self, msg: str, *, token: Token | None = None) -> ParserError:
9691
return ParserError(f"line {(token if token is not None else self._tokens[self._pos]).line}: {msg}")
9792

98-
def _preprocess(self) -> list[Token]:
99-
"""Handle preprocessor directives and return a new list of tokens with directives processed."""
100-
result = []
93+
def _in_false_branch(self) -> bool:
94+
"""Return whether we're currently in a false conditional branch."""
95+
return bool(self._conditional_stack) and not self._conditional_stack[-1][1]
10196

102-
while self._tokens[self._pos].type != TokenType.EOF:
103-
token = self._tokens[self._pos]
97+
def _handle_preprocessor(self) -> bool:
98+
"""Handle preprocessor directives.
10499
105-
# Always handle conditional directives first (even in false branches)
106-
if token.type in (TokenType.PP_IFDEF, TokenType.PP_IFNDEF, TokenType.PP_ELSE, TokenType.PP_ENDIF):
107-
self._handle_conditional()
108-
continue
100+
Returns ``True`` if a directive was processed, indicating the caller should continue its loop.
101+
"""
102+
token = self._current()
109103

110-
# If we're in a false conditional branch, skip this token
111-
if self._conditional_stack and not self._conditional_stack[-1][1]:
112-
self._pos += 1
113-
continue
104+
if token.type in (TokenType.PP_IFDEF, TokenType.PP_IFNDEF, TokenType.PP_ELSE, TokenType.PP_ENDIF):
105+
self._handle_conditional()
106+
return True
114107

115-
if token.type == TokenType.PP_DEFINE:
116-
self._parse_define()
117-
elif token.type == TokenType.PP_UNDEF:
118-
self._parse_undef()
119-
elif token.type == TokenType.PP_INCLUDE:
120-
self._parse_include()
121-
else:
122-
# Not a preprocessor directive, just add it to the result
123-
result.append(token)
124-
self._pos += 1
108+
if token.type == TokenType.PP_DEFINE:
109+
self._parse_define()
110+
return True
125111

126-
# Append EOF token
127-
result.append(self._tokens[self._pos])
128-
self._pos += 1
112+
if token.type == TokenType.PP_UNDEF:
113+
self._parse_undef()
114+
return True
129115

130-
if self._conditional_stack:
131-
raise self._error("unclosed conditional statement", token=self._conditional_stack[-1][0])
116+
if token.type == TokenType.PP_INCLUDE:
117+
self._parse_include()
118+
return True
132119

133-
return result
120+
return False
134121

135122
def _parse(self) -> None:
136123
"""Parse top-level definitions from the token stream."""
137124
while (token := self._current()).type != TokenType.EOF:
125+
if self._handle_preprocessor():
126+
continue
127+
138128
if token.type == TokenType.PP_FLAGS:
139129
self._parse_config_flags()
140130
elif token.type == TokenType.TYPEDEF:
@@ -158,6 +148,9 @@ def _parse(self) -> None:
158148
else:
159149
raise self._error(f"unexpected token {token.value!r}")
160150

151+
if self._conditional_stack:
152+
raise self._error("unclosed conditional statement", token=self._conditional_stack[-1][0])
153+
161154
# Preprocessor directives
162155

163156
def _parse_define(self) -> None:
@@ -246,6 +239,34 @@ def _handle_conditional(self) -> None:
246239
raise self._error("#endif without matching #ifdef/#ifndef", token=token)
247240
self._conditional_stack.pop()
248241

242+
# If we ended up in a false branch, skip ahead to the matching #else/#endif
243+
if self._in_false_branch():
244+
self._skip_false_branch()
245+
246+
def _skip_false_branch(self) -> None:
247+
"""Skip all tokens in a false conditional branch until the matching ``#else`` or ``#endif``."""
248+
depth = 0
249+
while self._current().type != TokenType.EOF:
250+
token_type = self._current().type
251+
252+
if token_type in (TokenType.PP_IFDEF, TokenType.PP_IFNDEF):
253+
depth += 1
254+
self._pos += 1
255+
# Skip the identifier argument
256+
if self._current().type != TokenType.EOF:
257+
self._pos += 1
258+
elif token_type == TokenType.PP_ENDIF:
259+
if depth == 0:
260+
return
261+
depth -= 1
262+
self._pos += 1
263+
elif token_type == TokenType.PP_ELSE:
264+
if depth == 0:
265+
return
266+
self._pos += 1
267+
else:
268+
self._pos += 1
269+
249270
# Type definitions
250271

251272
def _parse_typedef(self) -> None:
@@ -347,6 +368,8 @@ def _parse_enum_or_flag(self) -> type[Enum | Flag]:
347368
values: dict[str, int] = {}
348369

349370
while not self._at(TokenType.RBRACE):
371+
if self._handle_preprocessor():
372+
continue
350373
self._assert_not_eof()
351374

352375
member_name = self._expect(TokenType.IDENTIFIER).value
@@ -385,6 +408,8 @@ def _parse_field_list(self) -> list[Field]:
385408
fields: list[Field] = []
386409

387410
while not self._at(TokenType.RBRACE):
411+
if self._handle_preprocessor():
412+
continue
388413
self._assert_not_eof()
389414

390415
fields.append(self._parse_field())
@@ -419,7 +444,7 @@ def _parse_field_name(self, base_type: type[BaseType]) -> tuple[type[BaseType],
419444
type_ = self.cs._make_pointer(type_)
420445

421446
# Field name
422-
name = self._expect(*_IDENTIFIER_TYPES).value
447+
name = self._expect(*IDENTIFIER_TYPES).value
423448

424449
# Array dimensions
425450
type_ = self._parse_array_dimensions(type_)

tests/test_benchmark.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,6 @@ def test_benchmark_parser(cs: cstruct, benchmark: BenchmarkFixture) -> None:
187187
tokens = tokenize(cdef)
188188

189189
def parse(parser: CStyleParser, tokens: list[Token]) -> None:
190-
parser.reset()
191-
parser._tokens = tokens
192-
tokens = parser._preprocess()
193190
parser.reset()
194191
parser._tokens = tokens
195192
parser._parse()

tests/test_expression.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,33 @@ def test_expression(cs_with_consts: cstruct, expression: str, answer: int) -> No
167167
pytest.param(
168168
"sizeof(0 +)",
169169
ResolveError,
170-
"Unknown type 0 +",
170+
"Unknown type 0 \\+",
171171
id="invalid-sizeof-expression",
172172
),
173+
pytest.param(
174+
"UNKNOWN",
175+
ExpressionParserError,
176+
"Unknown identifier: 'UNKNOWN'",
177+
id="unknown-identifier",
178+
),
179+
pytest.param(
180+
"UNKNOWN.UNKNOWN",
181+
ExpressionParserError,
182+
"Unknown identifier: 'UNKNOWN.UNKNOWN'",
183+
id="unknown-identifier",
184+
),
185+
pytest.param(
186+
"1 2",
187+
ExpressionParserError,
188+
"Invalid expression: too many operands",
189+
id="too-many-operands",
190+
),
191+
pytest.param(
192+
"1 + + 2",
193+
ExpressionParserError,
194+
"Invalid expression: not enough operands",
195+
id="double-binary-operator",
196+
),
173197
],
174198
)
175199
def test_expression_failure(cs_with_consts: cstruct, expression: str, exception: type, message: str) -> None:
@@ -210,3 +234,38 @@ def test_offsetof(cs: cstruct) -> None:
210234
assert Expression("offsetof(test, b)").evaluate(cs) == 4
211235
assert Expression("offsetof(test, c)").evaluate(cs) == 12
212236
assert Expression("offsetof(test, d)").evaluate(cs) == 14
237+
238+
239+
def test_dotted_identifier(cs: cstruct) -> None:
240+
"""Test dotted member access in expressions."""
241+
cs.load("""
242+
enum color : uint8 {
243+
RED = 1,
244+
GREEN = 2,
245+
BLUE = 4
246+
};
247+
""")
248+
249+
assert Expression("color.RED").evaluate(cs) == 1
250+
assert Expression("color.RED | color.BLUE").evaluate(cs) == 5
251+
assert Expression("color.GREEN + 10").evaluate(cs) == 12
252+
253+
254+
def test_dotted_identifier_unknown_member(cs: cstruct) -> None:
255+
"""Test error when a member in a dotted expression doesn't exist."""
256+
cs.load("""
257+
enum color : uint8 {
258+
RED = 1
259+
};
260+
""")
261+
262+
with pytest.raises(ExpressionParserError, match="Unknown identifier: 'color\\.NOTAMEMBER'"):
263+
Expression("color.NOTAMEMBER").evaluate(cs)
264+
265+
266+
def test_dotted_identifier_not_integer(cs: cstruct) -> None:
267+
"""Test error when a dotted expression resolves to a non-integer."""
268+
cs.consts["mydict"] = {"nested": "hello"}
269+
270+
with pytest.raises(ExpressionParserError, match="does not resolve to an integer"):
271+
Expression("mydict.nested").evaluate(cs)

0 commit comments

Comments
 (0)