Skip to content

Commit 86e3651

Browse files
committed
Change how #define values are handled
1 parent 5b2a382 commit 86e3651

4 files changed

Lines changed: 135 additions & 69 deletions

File tree

dissect/cstruct/lexer.py

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,38 @@ def _skip_whitespace(self) -> None:
274274
if match := _RE_WHITESPACE.match(self.data, self._pos):
275275
self._take(match.end() - self._pos)
276276

277+
def _skip_comment(self) -> None:
278+
"""Skip a comment starting at the current position, if present."""
279+
if self._current() == "/":
280+
peek = self._peek()
281+
282+
if peek == "*":
283+
self._take(2) # Consume /*
284+
end = self.data.find("*/", self._pos)
285+
if end != -1:
286+
self._take(end - self._pos + 2)
287+
else:
288+
self._take(len(self.data) - self._pos)
289+
290+
elif peek == "/":
291+
self._take(2) # Consume //
292+
end = self.data.find("\n", self._pos)
293+
if end != -1:
294+
self._take(end - self._pos)
295+
else:
296+
self._take(len(self.data) - self._pos)
297+
298+
def _skip_whitespace_and_comments(self) -> None:
299+
"""Skip whitespace and comments."""
300+
while True:
301+
start_pos = self._pos
302+
self._skip_whitespace()
303+
if self.eof:
304+
break
305+
self._skip_comment()
306+
if self.eof or self._pos == start_pos:
307+
break
308+
277309
def _read_identifier(self) -> str:
278310
"""Read an identifier starting with a letter or underscore, followed by letters, digits, or underscores."""
279311
if match := _RE_IDENTIFIER.match(self.data, self._pos):
@@ -348,13 +380,6 @@ def _read_string(self) -> str:
348380

349381
return result
350382

351-
def _read_angle_string(self) -> str:
352-
"""Read an angle-bracket string for ``#include <...>``."""
353-
self._expect("<") # Consume `<`
354-
value = self._read_until(">", or_eof=False)
355-
self._expect(">") # Consume closing `>`
356-
return f"<{value}>"
357-
358383
def _read_preprocessor(self) -> None:
359384
"""Read a preprocessor directive starting with ``#``."""
360385
line = self._line
@@ -373,23 +398,41 @@ def _read_preprocessor(self) -> None:
373398
return
374399

375400
# Read the keyword after #
376-
self._skip_whitespace()
401+
self._skip_whitespace_and_comments()
377402
keyword = self._read_identifier()
378403

379404
if (token_type := _PP_KEYWORDS.get(keyword)) is None:
380405
raise self._error(f"unknown preprocessor directive '#{keyword}'", line=line)
381406

382407
self._emit(token_type, keyword, line, col)
383408

384-
if token_type == TokenType.PP_INCLUDE:
409+
if token_type == TokenType.PP_DEFINE:
410+
self._skip_whitespace_and_comments()
411+
412+
if not (name := self._read_identifier()):
413+
raise self._error("expected identifier after '#define'", line=line)
414+
self._emit(TokenType.IDENTIFIER, name, line)
415+
416+
self._skip_whitespace_and_comments()
417+
if self.eof or self._line != line:
418+
# No value, just a simple macro definition
419+
return
420+
421+
if (value := self._read_until("\n")).strip():
422+
self._emit(TokenType.STRING, value, line)
423+
424+
elif token_type == TokenType.PP_INCLUDE:
385425
# Read include path — either "..." or <...>
386-
self._skip_whitespace()
426+
self._skip_whitespace_and_comments()
387427

388428
ch = self._current()
389429
if ch == '"' or ch == "'":
390430
value = self._read_string()
391431
elif ch == "<":
392-
value = self._read_angle_string()
432+
self._expect("<") # Consume `<`
433+
value = self._read_until(">", or_eof=False)
434+
self._expect(">") # Consume closing `>`
435+
value = f"<{value}>"
393436
else:
394437
raise self._error("expected include path after '#include'", line=line)
395438

@@ -398,34 +441,11 @@ def _read_preprocessor(self) -> None:
398441
def tokenize(self) -> list[Token]:
399442
"""Tokenize the input data and return a list of tokens."""
400443
while not self.eof:
401-
self._skip_whitespace()
444+
self._skip_whitespace_and_comments()
402445
if self.eof:
403446
break
404447

405448
ch = self._current()
406-
407-
# Skip comments
408-
if ch == "/":
409-
peek = self._peek()
410-
411-
if peek == "*":
412-
self._take(2) # Consume /*
413-
end = self.data.find("*/", self._pos)
414-
if end != -1:
415-
self._take(end - self._pos + 2)
416-
else:
417-
self._take(len(self.data) - self._pos)
418-
continue
419-
420-
if peek == "/":
421-
self._take(2) # Consume //
422-
end = self.data.find("\n", self._pos)
423-
if end != -1:
424-
self._take(end - self._pos)
425-
else:
426-
self._take(len(self.data) - self._pos)
427-
continue
428-
429449
line = self._line
430450
col = self._column
431451

@@ -439,7 +459,7 @@ def tokenize(self) -> list[Token]:
439459
elif ch in ("b", "B") and self._peek() in ("'", '"'):
440460
# Binary string literal like `b"..."` or `b'...'`
441461
self._take() # Consume `b`
442-
self._emit(TokenType.BYTES, f"b'{self._read_string()}'", line, col)
462+
self._emit(TokenType.BYTES, f"b{self._read_string()!r}", line, col)
443463

444464
elif ch.isdigit():
445465
self._emit(TokenType.NUMBER, self._read_number(), line, col)

dissect/cstruct/parser.py

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -159,25 +159,38 @@ def _parse_define(self) -> None:
159159

160160
name_token = self._expect(TokenType.IDENTIFIER)
161161

162-
# Collect all tokens on the same line as the #define
163-
parts = []
164-
while (token := self._current()).type != TokenType.EOF and token.line == name_token.line:
165-
parts.append(self._take().value)
166-
167-
value = "".join(parts).strip()
168-
try:
169-
# Lazy mode, try to evaluate as a Python literal first (for simple constants)
170-
value = ast.literal_eval(value)
171-
except (ValueError, SyntaxError):
172-
pass
173-
174-
# If it's still a string, try to evaluate it as an expression in the context of current constants
175-
if isinstance(value, str):
176-
try:
177-
value = Expression(value).evaluate(self.cs)
178-
except (LexerError, ExpressionParserError):
179-
# If evaluation fails, just keep it as a string (e.g. for macro-like constants)
180-
pass
162+
# If there's a value, it's emitted as a single raw STRING token
163+
value = ""
164+
if (token := self._current()).type == TokenType.STRING and token.line == name_token.line:
165+
value = self._take().value
166+
167+
if value:
168+
if value[0] in ('"', "'"):
169+
quote = value[0]
170+
if value[-1] != quote:
171+
raise self._error("unterminated string literal", token=token)
172+
173+
# Remove the surrounding and any duplicate quotes
174+
value = "".join(ch for ch in value if ch != quote)
175+
elif value[:2].lower() in ("b'", 'b"'):
176+
quote = value[1]
177+
if value[-1] != quote:
178+
raise self._error("unterminated bytes literal", token=token)
179+
180+
# Remove the leading b and surrounding quotes
181+
value = ast.literal_eval(f"b{value[2:-1]!r}")
182+
else:
183+
try:
184+
# Lazy mode, try to evaluate as a Python literal first (for simple constants)
185+
value = ast.literal_eval(value)
186+
except (ValueError, SyntaxError):
187+
# Try to evaluate it as an expression in the context of current constants
188+
if isinstance(value, str):
189+
try:
190+
value = Expression(value).evaluate(self.cs)
191+
except (LexerError, ExpressionParserError):
192+
# If evaluation fails, just keep it as a string (e.g. for macro-like constants)
193+
pass
181194

182195
self.cs.consts[name_token.value] = value
183196

tests/test_lexer.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,26 @@
7676
),
7777
("1 - 1", [TokenType.NUMBER, TokenType.MINUS, TokenType.NUMBER], ["1", "-", "1"]),
7878
# Preprocessor directives
79-
("#define", [TokenType.PP_DEFINE], ["define"]),
79+
("#define FOO", [TokenType.PP_DEFINE, TokenType.IDENTIFIER], ["define", "FOO"]),
80+
("#define FOO 42", [TokenType.PP_DEFINE, TokenType.IDENTIFIER, TokenType.STRING], ["define", "FOO", "42"]),
81+
(
82+
'#define FOO "hello"',
83+
[TokenType.PP_DEFINE, TokenType.IDENTIFIER, TokenType.STRING],
84+
["define", "FOO", '"hello"'],
85+
),
86+
(
87+
"#define FOO ADCRYPT\00",
88+
[TokenType.PP_DEFINE, TokenType.IDENTIFIER, TokenType.STRING],
89+
["define", "FOO", "ADCRYPT\00"],
90+
),
91+
(
92+
"""
93+
#define FOO (1 \
94+
+ 2)
95+
""",
96+
[TokenType.PP_DEFINE, TokenType.IDENTIFIER, TokenType.STRING],
97+
["define", "FOO", "(1 + 2)"],
98+
),
8099
("#undef", [TokenType.PP_UNDEF], ["undef"]),
81100
("#ifdef", [TokenType.PP_IFDEF], ["ifdef"]),
82101
("#ifndef", [TokenType.PP_IFNDEF], ["ifndef"]),

tests/test_parser.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -216,22 +216,36 @@ def test_typedef_enum(cs: cstruct) -> None:
216216

217217
def test_define(cs: cstruct) -> None:
218218
cdef = """
219-
#define MY_CONST 42
220-
#define MY_EXPR (1 + 2 * 3)
221-
#define MY_STR "hello"
222-
#define MY_BYTES b"world"
223-
#define MY_FUNC(x) ( x == 0 )
224-
#define MY_TERNARY(x) ( x ? 1 : 0 )
219+
#define CONST 42
220+
#define EXPR (1 + 2 * 3)
221+
#define RAW somevalue
222+
#define STR "hello"
223+
#define BYTES b"world"
224+
#define NULLRAW ADCRYPT\00
225+
#define NULLSTR "ADCRYPT\00"
226+
#define NULLBYTES b"ADCRYPT\00"
227+
#define ARBITRARYBYTES b"\x00\x01\x02"
228+
#define MULTILINE (1 + \
229+
2 + \
230+
3)
231+
#define FUNC(x) ( x == 0 )
232+
#define TERNARY(x) ( x ? 1 : 0 )
225233
"""
226234
cs.load(cdef)
227235

228-
assert cs.consts["MY_CONST"] == 42
229-
assert cs.consts["MY_EXPR"] == 7
230-
assert cs.consts["MY_STR"] == "hello"
231-
assert cs.consts["MY_BYTES"] == b"world"
236+
assert cs.consts["CONST"] == 42
237+
assert cs.consts["EXPR"] == 7
238+
assert cs.consts["RAW"] == "somevalue"
239+
assert cs.consts["STR"] == "hello"
240+
assert cs.consts["BYTES"] == b"world"
241+
assert cs.consts["NULLRAW"] == "ADCRYPT\00"
242+
assert cs.consts["NULLSTR"] == "ADCRYPT\00"
243+
assert cs.consts["NULLBYTES"] == b"ADCRYPT\00"
244+
assert cs.consts["ARBITRARYBYTES"] == b"\x00\x01\x02"
245+
assert cs.consts["MULTILINE"] == 6
232246
# We don't evaluate function-like macros yet, so they should be stored as their raw string representation
233-
assert cs.consts["MY_FUNC"] == "(x)(x==0)"
234-
assert cs.consts["MY_TERNARY"] == "(x)(x?1:0)"
247+
assert cs.consts["FUNC"] == "(x) ( x == 0 )"
248+
assert cs.consts["TERNARY"] == "(x) ( x ? 1 : 0 )"
235249

236250

237251
def test_define_flag_value(cs: cstruct) -> None:
@@ -517,7 +531,7 @@ def test_preprocessor_define_from_enum_in_struct() -> None:
517531

518532
assert cs.consts["PROTO"] == 6
519533
assert cs.consts["FLAG_SYNACK"] == 3
520-
assert cs.consts["FLAG_BIG"] == "flags.PSH|YOMOMMA"
534+
assert cs.consts["FLAG_BIG"] == "flags.PSH | YOMOMMA"
521535

522536
assert "type" in cs.packet.fields
523537
assert "options" in cs.packet.fields

0 commit comments

Comments
 (0)