dissect.cstruct 5.0.dev14__tar.gz → 5.0.dev15__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/PKG-INFO +1 -1
  2. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/lexer.py +23 -10
  3. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/parser.py +35 -19
  4. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect.cstruct.egg-info/PKG-INFO +1 -1
  5. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect.cstruct.egg-info/scm_version.json +2 -2
  6. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_lexer.py +8 -8
  7. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_parser.py +124 -16
  8. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/.git-blame-ignore-revs +0 -0
  9. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/.gitattributes +0 -0
  10. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/CHANGELOG.md +0 -0
  11. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/COPYRIGHT +0 -0
  12. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/LICENSE +0 -0
  13. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/MANIFEST.in +0 -0
  14. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/README.md +0 -0
  15. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/__init__.py +0 -0
  16. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/bitbuffer.py +0 -0
  17. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/compiler.py +0 -0
  18. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/cstruct.py +0 -0
  19. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/exception.py +0 -0
  20. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/expression.py +0 -0
  21. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/tools/__init__.py +0 -0
  22. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/tools/stubgen.py +0 -0
  23. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/types/__init__.py +0 -0
  24. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/types/base.py +0 -0
  25. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/types/char.py +0 -0
  26. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/types/enum.py +0 -0
  27. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/types/flag.py +0 -0
  28. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/types/int.py +0 -0
  29. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/types/leb128.py +0 -0
  30. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/types/packed.py +0 -0
  31. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/types/pointer.py +0 -0
  32. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/types/structure.py +0 -0
  33. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/types/void.py +0 -0
  34. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/types/wchar.py +0 -0
  35. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect/cstruct/util.py +0 -0
  36. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect.cstruct.egg-info/SOURCES.txt +0 -0
  37. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect.cstruct.egg-info/dependency_links.txt +0 -0
  38. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect.cstruct.egg-info/entry_points.txt +0 -0
  39. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect.cstruct.egg-info/requires.txt +0 -0
  40. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect.cstruct.egg-info/scm_file_list.json +0 -0
  41. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/dissect.cstruct.egg-info/top_level.txt +0 -0
  42. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/examples/disk.py +0 -0
  43. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/examples/mirai.py +0 -0
  44. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/examples/pe.py +0 -0
  45. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/examples/protobuf.py +0 -0
  46. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/examples/secdesc.py +0 -0
  47. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/pyproject.toml +0 -0
  48. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/setup.cfg +0 -0
  49. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/__init__.py +0 -0
  50. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/_data/testdef.txt +0 -0
  51. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/_docs/Makefile +0 -0
  52. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/_docs/__init__.py +0 -0
  53. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/_docs/conf.py +0 -0
  54. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/_docs/index.rst +0 -0
  55. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/conftest.py +0 -0
  56. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_align.py +0 -0
  57. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_annotations.py +0 -0
  58. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_basic.py +0 -0
  59. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_benchmark.py +0 -0
  60. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_bitbuffer.py +0 -0
  61. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_bitfield.py +0 -0
  62. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_compiler.py +0 -0
  63. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_ctypes.py +0 -0
  64. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_expression.py +0 -0
  65. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_tools_stubgen.py +0 -0
  66. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_base.py +0 -0
  67. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_char.py +0 -0
  68. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_custom.py +0 -0
  69. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_enum.py +0 -0
  70. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_flag.py +0 -0
  71. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_int.py +0 -0
  72. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_leb128.py +0 -0
  73. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_packed.py +0 -0
  74. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_pointer.py +0 -0
  75. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_structure.py +0 -0
  76. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_union.py +0 -0
  77. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_void.py +0 -0
  78. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_types_wchar.py +0 -0
  79. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/test_utils.py +0 -0
  80. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tests/utils.py +0 -0
  81. {dissect_cstruct-5.0.dev14 → dissect_cstruct-5.0.dev15}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.cstruct
3
- Version: 5.0.dev14
3
+ Version: 5.0.dev15
4
4
  Summary: A Dissect module implementing a parser for C-like structures: structure parsing in Python made easy
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License-Expression: Apache-2.0
@@ -125,6 +125,7 @@ _SINGLE_CHARS = {
125
125
 
126
126
  _RE_IDENTIFIER = re.compile(r"[a-zA-Z_.][a-zA-Z0-9_.]*")
127
127
  _RE_WHITESPACE = re.compile(r"[ \t\r\n]+")
128
+ _RE_DEFINE_LITERAL = re.compile(r"""(["'])(?:\\.|(?!\1)[^\\\n])*\1""")
128
129
 
129
130
  IDENTIFIER_TYPES = set(_C_KEYWORDS.values()) | {TokenType.IDENTIFIER}
130
131
 
@@ -292,10 +293,10 @@ class Lexer:
292
293
  if peek == "*":
293
294
  self._take(2) # Consume /*
294
295
  end = self.data.find("*/", self._pos)
295
- if end != -1:
296
- self._take(end - self._pos + 2)
297
- else:
298
- self._take(len(self.data) - self._pos)
296
+ if end == -1:
297
+ raise self._error("unterminated comment")
298
+
299
+ self._take(end - self._pos + 2)
299
300
 
300
301
  elif peek == "/":
301
302
  self._take(2) # Consume //
@@ -390,6 +391,23 @@ class Lexer:
390
391
 
391
392
  return result
392
393
 
394
+ def _read_define_value(self) -> str:
395
+ """Read the value of a ``#define`` up to the end of the line."""
396
+ value = []
397
+
398
+ while not self.eof and (ch := self._current()) != "\n":
399
+ if ch == "/" and self._peek() in ("/", "*"):
400
+ self._skip_comment()
401
+ value.append(" ")
402
+ elif ch in ('"', "'"):
403
+ if not (match := _RE_DEFINE_LITERAL.match(self.data, self._pos)):
404
+ raise self._error("unterminated string literal")
405
+ value.append(self._take(match.end() - self._pos))
406
+ else:
407
+ value.append(self._read_until("\n\"'/") or self._take())
408
+
409
+ return "".join(value).strip()
410
+
393
411
  def _read_preprocessor(self) -> None:
394
412
  """Read a preprocessor directive starting with ``#``."""
395
413
  line = self._line
@@ -423,12 +441,7 @@ class Lexer:
423
441
  raise self._error("expected identifier after '#define'", lineno=line)
424
442
  self._emit(TokenType.IDENTIFIER, name, line)
425
443
 
426
- self._skip_whitespace_and_comments()
427
- if self.eof or self._line != line:
428
- # No value, just a simple macro definition
429
- return
430
-
431
- if (value := self._read_until("\n")).strip():
444
+ if value := self._read_define_value():
432
445
  self._emit(TokenType.STRING, value, line)
433
446
 
434
447
  elif token_type == TokenType.PP_INCLUDE:
@@ -20,6 +20,10 @@ if TYPE_CHECKING:
20
20
  from dissect.cstruct.types import BaseType, Enum, Flag
21
21
 
22
22
 
23
+ # A string or the name of a constant in the value of a define
24
+ _RE_DEFINE_STRING = re.compile(r"""\s*(?:([bB]?)(["'])((?:\\.|(?!\2)[^\\\n])*)\2|([a-zA-Z_.][a-zA-Z0-9_.]*))""")
25
+
26
+
23
27
  class Parser(TokenCursor):
24
28
  """Base class for definition parsers.
25
29
 
@@ -42,7 +46,7 @@ class Parser(TokenCursor):
42
46
 
43
47
  def _join_line_continuations(string: str) -> str:
44
48
  # Join lines ending with backslash
45
- return re.sub(r"\\\n", "", string)
49
+ return re.sub(r"\\\r?\n", "", string)
46
50
 
47
51
 
48
52
  class CStyleParser(Parser):
@@ -138,11 +142,16 @@ class CStyleParser(Parser):
138
142
  elif token.type == TokenType.TYPEDEF:
139
143
  self._parse_typedef()
140
144
  elif token.type in (TokenType.STRUCT, TokenType.UNION):
141
- self._parse_struct_or_union()
145
+ type_ = self._parse_struct_or_union()
142
146
 
143
147
  # Skip variable declarations after struct/union definitions
144
- while not self._at(TokenType.SEMICOLON, TokenType.EOF):
145
- self._pos += 1
148
+ has_variable = self._at(TokenType.IDENTIFIER, TokenType.STAR)
149
+ while has_variable:
150
+ _, _, bits = self._parse_field_name(type_)
151
+ if bits is not None:
152
+ raise self._error("variables cannot have bitfields")
153
+
154
+ has_variable = self._match(TokenType.COMMA)
146
155
 
147
156
  self._expect(TokenType.SEMICOLON)
148
157
  elif token.type in (TokenType.ENUM, TokenType.FLAG):
@@ -174,21 +183,28 @@ class CStyleParser(Parser):
174
183
  value = self._take().value
175
184
 
176
185
  if value:
177
- if value[0] in ('"', "'"):
178
- quote = value[0]
179
- if value[-1] != quote:
180
- raise self._error("unterminated string literal", token=token)
181
-
182
- # Remove the surrounding quotes and flatten escape sequences
183
- value = value[1:-1].encode().decode("unicode_escape")
184
- elif value[:2].lower() in ("b'", 'b"'):
185
- quote = value[1]
186
- if value[-1] != quote:
187
- raise self._error("unterminated bytes literal", token=token)
188
-
189
- # Remove the leading b and surrounding quotes and flatten escape sequences
190
- value = value[2:-1].encode().decode("unicode_escape")
191
- value = ast.literal_eval(f"b{value!r}")
186
+ # Adjacent strings and constants that are strings are concatenated like in C
187
+ strings = []
188
+ pos = 0
189
+ if '"' in value or "'" in value:
190
+ while pos < len(value) and (match := _RE_DEFINE_STRING.match(value, pos)):
191
+ is_bytes, _, string, name = match.groups()
192
+ if name is None:
193
+ # Flatten escape sequences
194
+ string = string.encode().decode("unicode_escape")
195
+ strings.append(string.encode("latin-1") if is_bytes else string)
196
+ elif isinstance(constant := self.cs.consts.get(name), (str, bytes)):
197
+ strings.append(constant)
198
+ else:
199
+ break
200
+
201
+ pos = match.end()
202
+
203
+ if strings and pos == len(value):
204
+ try:
205
+ value = strings[0][:0].join(strings)
206
+ except TypeError:
207
+ raise self._error("cannot concatenate strings and bytes", token=token) from None
192
208
  else:
193
209
  try:
194
210
  # Lazy mode, try to evaluate as a Python literal first (for simple constants)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.cstruct
3
- Version: 5.0.dev14
3
+ Version: 5.0.dev15
4
4
  Summary: A Dissect module implementing a parser for C-like structures: structure parsing in Python made easy
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License-Expression: Apache-2.0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "tag": "5.0.dev0",
3
- "distance": 14,
4
- "node": "g2688580e0f466b79d4fc7fb94562f0defb978796",
3
+ "distance": 15,
4
+ "node": "ge066596a738ce1f58096078ed54b60e62f4032fc",
5
5
  "dirty": true,
6
6
  "branch": "main",
7
7
  "node_date": "2026-11-04"
@@ -90,14 +90,6 @@ from dissect.cstruct.lexer import TokenType, tokenize
90
90
  [TokenType.PP_DEFINE, TokenType.IDENTIFIER, TokenType.STRING],
91
91
  ["define", "FOO", "ADCRYPT\00"],
92
92
  ),
93
- (
94
- """
95
- #define FOO (1 \
96
- + 2)
97
- """,
98
- [TokenType.PP_DEFINE, TokenType.IDENTIFIER, TokenType.STRING],
99
- ["define", "FOO", "(1 + 2)"],
100
- ),
101
93
  ("#undef", [TokenType.PP_UNDEF], ["undef"]),
102
94
  ("#ifdef", [TokenType.PP_IFDEF], ["ifdef"]),
103
95
  ("#ifndef", [TokenType.PP_IFNDEF], ["ifndef"]),
@@ -174,6 +166,14 @@ def test_token(src: str, types: list[TokenType], values: list[str]) -> None:
174
166
  ("#include", "unexpected end of input"),
175
167
  ("#include <missing_end", "unexpected end of input"),
176
168
  ('#include "missing_end', "unexpected end of input"),
169
+ ('#define A "x\ny"\n#define B 2\n', "line 1: unterminated string literal"),
170
+ ('#define A "x\n#define B "y"\n', "line 1: unterminated string literal"),
171
+ ('#define A "x', "line 1: unterminated string literal"),
172
+ ("#define A 'x\n", "line 1: unterminated string literal"),
173
+ ("#define A it's\n", "line 1: unterminated string literal"),
174
+ ("/* unterminated", "line 1: unterminated comment"),
175
+ ("struct a {\n uint8 b; /* c\n};\n", "line 2: unterminated comment"),
176
+ ("#define A 1 /* c\n#define B 2\n", "line 1: unterminated comment"),
177
177
  ("@", "unexpected character"),
178
178
  ("`", "unexpected character"),
179
179
  ],
@@ -41,6 +41,65 @@ def test_struct(cs: cstruct, compiled: bool) -> None:
41
41
  cs.resolve(name)
42
42
 
43
43
 
44
+ def test_struct_variables(cs: cstruct) -> None:
45
+ """Test that all forms of variable declarations after a struct or union definition are silently ignored."""
46
+ cdef = """
47
+ #define SIZE 2
48
+
49
+ struct test {
50
+ uint8 a;
51
+ } b[4], *c[SIZE], d[2][3], e[];
52
+
53
+ union test1 {
54
+ uint8 a;
55
+ } f;
56
+
57
+ struct test g, *h;
58
+
59
+ typedef uint8 test2;
60
+ """
61
+ cs.load(cdef)
62
+
63
+ assert cs.resolve("test") is cs.test
64
+ assert cs.resolve("test1") is cs.test1
65
+ assert cs.resolve("test2") is cs.uint8
66
+
67
+ for name in ("b", "c", "d", "e", "f", "g", "h"):
68
+ with pytest.raises(ResolveError, match=f"Unknown type {name}"):
69
+ cs.resolve(name)
70
+
71
+
72
+ @pytest.mark.parametrize(
73
+ ("cdef", "match"),
74
+ [
75
+ # A missing semicolon before the next definition
76
+ ("struct a { uint8 x; }\nstruct b { uint8 x; };", "line 2: expected ;, got struct"),
77
+ ("struct a { uint8 x; }\nunion b { uint8 x; };", "line 2: expected ;, got union"),
78
+ ("struct a { uint8 x; }\nenum b : uint8 { C };", "line 2: expected ;, got enum"),
79
+ ("struct a { uint8 x; }\nflag b : uint8 { C };", "line 2: expected ;, got flag"),
80
+ ("struct a { uint8 x; }\ntypedef uint8 b;", "line 2: expected ;, got typedef"),
81
+ ("struct a { uint8 x; }\n#define B 1", "line 2: expected ;, got define"),
82
+ ("struct a { uint8 x; }\n#[nocompile]\nstruct b { uint8 x; };", "line 2: expected ;, got nocompile"),
83
+ ("union a { uint8 x; }\ntypedef uint8 b;", "line 2: expected ;, got typedef"),
84
+ ("struct a { uint8 x; } b\ntypedef uint8 c;", "line 2: expected ;, got typedef"),
85
+ ("struct a { uint8 x; }", "line 1: expected ;, got end of input"),
86
+ ("struct a { uint8 x; } b", "line 1: expected ;, got end of input"),
87
+ # Invalid variable declarations
88
+ ("struct a { uint8 x; } b c;", "line 1: expected ;, got c"),
89
+ ("struct a { uint8 x; } 1;", "line 1: expected ;, got 1"),
90
+ ("struct a { uint8 x; } , b;", "line 1: expected ;, got ,"),
91
+ ("struct a { uint8 x; } b,;", "line 1: expected one of .*, got ;"),
92
+ ("struct a { uint8 x; } *;", "line 1: expected one of .*, got ;"),
93
+ ("struct a { uint8 x; } b[2;", "line 1: expected ], got end of input"),
94
+ ("struct a { uint8 x; } b : 4;", "line 1: variables cannot have bitfields"),
95
+ ],
96
+ )
97
+ def test_struct_invalid_variables(cs: cstruct, cdef: str, match: str) -> None:
98
+ """Test that nothing other than variable declarations and a semicolon can follow a struct or union definition."""
99
+ with pytest.raises(ParserError, match=match):
100
+ cs.load(cdef)
101
+
102
+
44
103
  def test_nested_structs(cs: cstruct, compiled: bool) -> None:
45
104
  """Test parsing of nested structs, including anonymous ones."""
46
105
  cdef = """
@@ -248,14 +307,18 @@ def test_define(cs: cstruct) -> None:
248
307
  #define NULLSTR "ADCRYPT\00"
249
308
  #define NULLBYTES b"ADCRYPT\00"
250
309
  #define ARBITRARYBYTES b"\x00\x01\x02"
251
- #define MULTILINE (1 + \
252
- 2 + \
310
+ #define MULTILINE (1 + \\
311
+ 2 + \\
253
312
  3)
254
- #define QUOTES "\'\"a'b\""
313
+ #define QUOTES "'\\"a'b\\""
255
314
  #define ESCAPE "\\'\\"a'b\\"\\n"
256
315
  #define BYTES_ESCAPE b"`\\n"
257
316
  #define FUNC(x) ( x == 0 )
258
317
  #define TERNARY(x) ( x ? 1 : 0 )
318
+ #define STR_CONCAT "say " STR
319
+ #define BYTES_CONCAT BYTES b"!"
320
+ #define STR_AND_NUMBER "a" CONST
321
+ #define RAW_AND_RAW RAW RAW
259
322
  """
260
323
  cs.load(cdef)
261
324
 
@@ -275,6 +338,16 @@ def test_define(cs: cstruct) -> None:
275
338
  # We don't evaluate function-like macros yet, so they should be stored as their raw string representation
276
339
  assert cs.consts["FUNC"] == "(x) ( x == 0 )"
277
340
  assert cs.consts["TERNARY"] == "(x) ( x ? 1 : 0 )"
341
+ # Adjacent strings and constants that are strings are concatenated like in C
342
+ assert cs.consts["STR_CONCAT"] == "say hello"
343
+ assert cs.consts["BYTES_CONCAT"] == b"world!"
344
+ # A constant that isn't a string can't be concatenated, so the value is kept as it is
345
+ assert cs.consts["STR_AND_NUMBER"] == '"a" CONST'
346
+ # Without a string in the value we don't know whether these are strings, they could also just be raw values
347
+ assert cs.consts["RAW_AND_RAW"] == "RAW RAW"
348
+
349
+ with pytest.raises(ParserError, match="line 1: cannot concatenate strings and bytes"):
350
+ cs.load('#define MIXED "a" b"b"')
278
351
 
279
352
 
280
353
  def test_define_flag_value(cs: cstruct) -> None:
@@ -427,19 +500,6 @@ def test_conditional_parsing_error(cs: cstruct) -> None:
427
500
  cs.load(cdef)
428
501
 
429
502
 
430
- def test_multiline_define(cs: cstruct) -> None:
431
- """Test parsing of multi-line ``#define`` directives."""
432
- cdef = """
433
- #define MULTILINE_DEF (1 + \\
434
- 2 + \\
435
- 3)
436
- """
437
- cs.load(cdef)
438
-
439
- assert "MULTILINE_DEF" in cs.consts
440
- assert cs.consts["MULTILINE_DEF"] == 6
441
-
442
-
443
503
  def test_multiple_declarators(cs: cstruct) -> None:
444
504
  """Test parsing of multiple declarators in a single struct field declaration."""
445
505
  cdef = """
@@ -595,3 +655,51 @@ def test_error_context(cs: cstruct) -> None:
595
655
  3: 69
596
656
  """.rstrip()
597
657
  )
658
+
659
+
660
+ @pytest.mark.parametrize(
661
+ ("value", "expected"),
662
+ [
663
+ # A comment is replaced by a space, after which the value continues
664
+ ("data /* c */", "data"),
665
+ ("a/* c */b", "a b"),
666
+ ("data /* c\n#define C 3\n d */", "data"),
667
+ ("data // c", "data"),
668
+ ("1 // it's", 1),
669
+ ("http://x", "http:"),
670
+ ("1 /* c\n d */ + 2", 3),
671
+ ("/* c\n d */ 1", 1),
672
+ ("/* c */", ""),
673
+ # Comments after and in strings
674
+ ('"a" /* c\n d */', "a"),
675
+ ('"a" // it\'s "b"', "a"),
676
+ ("'b' /* c\n d */", "b"),
677
+ ('b"c" // c', b"c"),
678
+ ("b'd' /* c */", b"d"),
679
+ ('"\\"f\\"" // c', '"f"'),
680
+ ('"it\'s /* c */ // d"', "it's /* c */ // d"),
681
+ # Escapes and spaces in strings
682
+ ('"a\\\\"', "a\\"),
683
+ ('"a b"', "a b"),
684
+ ('b"\\xff"', b"\xff"),
685
+ # Line continuations are joined first
686
+ ('"a\\\nb"', "ab"),
687
+ ("1 /* c */ + \\\n 2", 3),
688
+ ('"a" // c \\\n "b"', "a"),
689
+ ("1 + \\\r\n 2\r", 3),
690
+ # Adjacent strings are concatenated
691
+ ('"bar" /* c */ \\\n "zomg"', "barzomg"),
692
+ # CRLF line endings
693
+ ('"a"\r', "a"),
694
+ ("data\r", "data"),
695
+ # Anything that isn't only strings is kept as it is, like other macro-like constants
696
+ ('"a" + 1', '"a" + 1'),
697
+ ],
698
+ )
699
+ def test_define_value(cs: cstruct, value: str, expected: int | str | bytes) -> None:
700
+ """Test that the value of a ``#define`` follows the rules of C for comments, continuations and strings."""
701
+ cs.load(f"#define A {value}\n#define B 2\n")
702
+
703
+ assert cs.consts["A"] == expected
704
+ assert cs.consts["B"] == 2
705
+ assert "C" not in cs.consts