Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion dissect/cstruct/lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,52 @@ def _read_string(self) -> str:

return result

def _read_define_value(self) -> str:
"""Read value of a define, ignoring comments and unecessary white space."""
text_offsets: list[tuple[int, int]] = []

while not self.eof:
char = self._current()
if char == "\n":
break

# Read byte string
next = self._peek()
if char == "b" and next is not None and next in ("\"'"):
sof = self._pos
self._pos += 1 # consume the b
self._read_string()
text_offsets.append((sof, self._pos))
continue

# read normal string
if char in ("\"'"):
sof = self._pos
self._read_string()
text_offsets.append((sof, self._pos))
continue

# read word
if char not in " \t\r":
sof = self._pos
self._read_until(" \t\r\n")
text_offsets.append((sof, self._pos))
continue

# Skip whitespace
self._read_while(" \t\r", or_eof=True)
if self.eof:
break
self._skip_comment()
if self.eof:
break

result: list[str] = []
for start, end in text_offsets:
self._pos = start
result.append(self._take(end - start))
return " ".join(result)

def _read_preprocessor(self) -> None:
"""Read a preprocessor directive starting with ``#``."""
line = self._line
Expand Down Expand Up @@ -428,7 +474,7 @@ def _read_preprocessor(self) -> None:
# No value, just a simple macro definition
return

if (value := self._read_until("\n")).strip():
if value := self._read_define_value():
self._emit(TokenType.STRING, value, line)

elif token_type == TokenType.PP_INCLUDE:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
+ 2)
""",
[TokenType.PP_DEFINE, TokenType.IDENTIFIER, TokenType.STRING],
["define", "FOO", "(1 + 2)"],
["define", "FOO", "(1 + 2)"],
),
("#undef", [TokenType.PP_UNDEF], ["undef"]),
("#ifdef", [TokenType.PP_IFDEF], ["ifdef"]),
Expand Down
35 changes: 34 additions & 1 deletion tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,14 @@ def test_define(cs: cstruct) -> None:
#define MULTILINE (1 + \
2 + \
3)
#define QUOTES "\'\"a'b\""
#define QUOTES "'\\"a'b\\""
#define ESCAPE "\\'\\"a'b\\"\\n"
#define BYTES_ESCAPE b"`\\n"
#define FUNC(x) ( x == 0 )
#define TERNARY(x) ( x ? 1 : 0 )
#define MULTI_LINE_QUOTE "\
#define DEFINITION "
#define SPACED_BYTES b" "
"""
cs.load(cdef)

Expand All @@ -275,6 +278,8 @@ def test_define(cs: cstruct) -> None:
# We don't evaluate function-like macros yet, so they should be stored as their raw string representation
assert cs.consts["FUNC"] == "(x) ( x == 0 )"
assert cs.consts["TERNARY"] == "(x) ( x ? 1 : 0 )"
assert cs.consts["MULTI_LINE_QUOTE"] == " #define DEFINITION "
assert cs.consts["SPACED_BYTES"] == b" "


def test_define_flag_value(cs: cstruct) -> None:
Expand Down Expand Up @@ -515,6 +520,34 @@ def test_preprocessor_in_struct_body(cs: cstruct) -> None:
assert cs.test.fields["bonus"].type == cs.uint64


def test_preprocessor_define_with_comments(cs: cstruct) -> None:
cdef = """
#define IGNORE_SCOPED_COMMENT data1 /* ... */
#define IGNORE_LINE_COMMENT data2 //
#define IGNORE_MULTILINE_COMMENT data3 /* Multiline
comment
*/
#define IGNORE_DEFINE_IN_COMMENT data4 /* define
#define TEST_NO data_no inside
comment
*/

@william-billaud william-billaud Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this does not compile as it is compiler explorer link

Maybe change with integer for something more correct in C?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only using L530 does indeed not compile, as the comment is not closed. using the full comment does work no? https://godbolt.org/z/zvfT8T4cT

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue was more related to the quoted /unquoted 'data4' vs '"data4"' (I was not really clear), but this is related to #170 (comment)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh that, yeah cstruct sees everything as a string after the define. I think we kept it that way for backwards compatibility with <5.0

#define TEXT_CONTAINING_COMMENT "text with comments /* data5 */"
#define IGNORE_STRING_AFTER_COMMENT "data6" // \
"string that cannot be reached"
#define NO_COMMENT_BETWEEN_CONCATINATION "data7" /* Comment */ \
"should get concatinated"
"""

cs.load(cdef)
assert cs.consts["IGNORE_SCOPED_COMMENT"] == "data1"
assert cs.consts["IGNORE_LINE_COMMENT"] == "data2"
assert cs.consts["IGNORE_MULTILINE_COMMENT"] == "data3"
assert cs.consts["IGNORE_DEFINE_IN_COMMENT"] == "data4"
assert cs.consts["TEXT_CONTAINING_COMMENT"] == "text with comments /* data5 */"

@william-billaud william-billaud Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cf comment below, I didn't find how to make a suggestion edit from Github gui for two lines....

Suggested change
assert cs.consts["TEXT_CONTAINING_COMMENT"] == "text with comments /* data5 */"
assert cs.consts["TEXT_CONTAINING_COMMENT"] == '"text with comments /* data5 */"'

assert cs.consts["IGNORE_STRING_AFTER_COMMENT"] == "data6"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We lost the difference between quoted and unquoted value (which is the difference between and int, a char and a string. I don't known dissect.cstruct enough but a first glance it's look like a potential issue

Suggested change
assert cs.consts["IGNORE_STRING_AFTER_COMMENT"] == "data6"
assert cs.consts["IGNORE_STRING_AFTER_COMMENT"] == '"data6"'
# to add in cdef
#define A_SIMPLE_INT 1 //
#define A_INT_AS_STRING "1"
#define A_INT_AS_CHAR '1'
assert cs.consts["A_SIMPLE_INT"] == "1"
assert cs.consts["A_INT_AS_STRING"] == '"1"'
assert cs.consts["A_INT_AS_CHAR"] == "'1'"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will take a look whether that will be an issue

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no difference between char and string in cstruct. As for an int, it is properly resolved:

Python 3.14.3 (main, Feb  3 2026, 22:52:18) [Clang 21.1.4 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from dissect.cstruct import cstruct
>>> cstruct()
<dissect.cstruct.cstruct.cstruct object at 0x7c966b671a90>
>>> x = cstruct()
>>> xxx = """
...     #define A_SIMPLE_INT         1      //
...     #define A_INT_AS_STRING        "1"
...     #define A_INT_AS_CHAR        '1'
... """
>>> x.load(xxx)
<dissect.cstruct.cstruct.cstruct object at 0x7c966b732710>
>>> x.A_SIMPLE_INT
1
>>> x.A_INT_AS_STRING
'1'
>>> x.A_INT_AS_CHAR
'1'

assert cs.consts["NO_COMMENT_BETWEEN_CONCATINATION"] == 'data7" "should get concatinated'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my understanting and test the value should be ``'data7should get concatinated'and not'data7" "should get concatinated'`

E.g link on compiler exporer/godbolt

❯ cat test.c

#include <stdio.h>

#define NO_COMMENT_BETWEEN_CONCATINATION "data7" /* Comment */ \
                                             "should get concatinated"
int main(void) {
    printf(NO_COMMENT_BETWEEN_CONCATINATION);
    printf("\n");
    return 0;
}
❯ gcc test.c
❯ ./a.out
data7should get concatinated

@william-billaud william-billaud Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert cs.consts["NO_COMMENT_BETWEEN_CONCATINATION"] == 'data7" "should get concatinated'
assert cs.consts["NO_COMMENT_BETWEEN_CONCATINATION"] == '"data7should get concatinated"'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true, I currently kept them separate in the define parsing because of a different issue:

#define DATA1 "world"
#define CONCAT "hello" \
            DATA1

where CONCAT should ideally resolve to helloworld. It currently doesn't of course and resolves to hello DATA1 where it cannot resolve DATA1 yet.
If we remove / join the strings, it would result in helloDATA1 and we would not be a clear separation of the string part and the part that needs to be resolved. So with that in mind, i decided to keep it separate for now.


@william-billaud william-billaud Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert "TEST_NO" not in cs.consts

Ensure TEST_NO is actually ignored


def test_preprocessor_define_from_enum_in_struct(cs: cstruct) -> None:
"""Test #define referencing enum values used for conditional fields and array sizes."""
cdef = """
Expand Down
Loading