dissect.cstruct 4.8.dev8__py3-none-any.whl → 5.0.dev0__py3-none-any.whl

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.
dissect/cstruct/parser.py CHANGED
@@ -2,23 +2,25 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  import re
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING
6
6
 
7
7
  from dissect.cstruct import compiler
8
8
  from dissect.cstruct.exceptions import (
9
9
  ExpressionParserError,
10
- ExpressionTokenizerError,
10
+ LexerError,
11
11
  ParserError,
12
12
  )
13
13
  from dissect.cstruct.expression import Expression
14
- from dissect.cstruct.types import BaseArray, Field, Structure
14
+ from dissect.cstruct.lexer import IDENTIFIER_TYPES, TokenCursor, TokenType, tokenize
15
+ from dissect.cstruct.types import BaseArray, BaseType, Field, Structure
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from dissect.cstruct import cstruct
18
- from dissect.cstruct.types import BaseType
19
+ from dissect.cstruct.lexer import Token
20
+ from dissect.cstruct.types import BaseType, Enum, Flag
19
21
 
20
22
 
21
- class Parser:
23
+ class Parser(TokenCursor):
22
24
  """Base class for definition parsers.
23
25
 
24
26
  Args:
@@ -26,688 +28,519 @@ class Parser:
26
28
  """
27
29
 
28
30
  def __init__(self, cs: cstruct):
29
- self.cstruct = cs
31
+ super().__init__()
32
+ self.cs = cs
30
33
 
31
34
  def parse(self, data: str) -> None:
32
35
  """This function should parse definitions to cstruct types.
33
36
 
34
37
  Args:
35
- data: Data to parse definitions from, usually a string.
38
+ data: Data to parse definitions from.
36
39
  """
37
40
  raise NotImplementedError
38
41
 
39
42
 
40
- class TokenParser(Parser):
41
- """Definition parser for C-like structure syntax.
43
+ def _join_line_continuations(string: str) -> str:
44
+ # Join lines ending with backslash
45
+ return re.sub(r"\\\n", "", string)
46
+
47
+
48
+ class CStyleParser(Parser):
49
+ """Recursive descent parser for C-like structure definitions.
42
50
 
43
51
  Args:
44
52
  cs: An instance of cstruct.
45
53
  compiled: Whether structs should be compiled or not.
54
+ align: Whether to use aligned struct reads.
46
55
  """
47
56
 
48
57
  def __init__(self, cs: cstruct, compiled: bool = True, align: bool = False):
49
58
  super().__init__(cs)
50
-
51
59
  self.compiled = compiled
52
60
  self.align = align
53
- self.TOK = self._tokencollection()
54
- self._conditionals = []
55
- self._conditionals_depth = 0
56
-
57
- @staticmethod
58
- def _tokencollection() -> TokenCollection:
59
- TOK = TokenCollection()
60
- TOK.add(r"#\[(?P<values>[^\]]+)\](?=\s*)", "CONFIG_FLAG")
61
- TOK.add(r"#define\s+(?P<name>[^\s]+)(?P<value>[^\r\n]*)", "DEFINE")
62
- TOK.add(r"#undef\s+(?P<name>[^\s]+)\s*", "UNDEF")
63
- TOK.add(r"#ifdef\s+(?P<name>[^\s]+)\s*", "IFDEF")
64
- TOK.add(r"#ifndef\s+(?P<name>[^\s]+)\s*", "IFNDEF")
65
- TOK.add(r"#else\s*", "ELSE")
66
- TOK.add(r"#endif\s*", "ENDIF")
67
- TOK.add(r"typedef(?=\s)", "TYPEDEF")
68
- TOK.add(r"(?:struct|union)(?=\s|{)", "STRUCT")
69
- TOK.add(
70
- r"(?P<enumtype>enum|flag)\s+(?P<name>[^\s:{]+)?\s*(:\s"
71
- r"*(?P<type>[^{]+?)\s*)?\{(?P<values>[^}]+)\}\s*(?=;)",
72
- "ENUM",
73
- )
74
- TOK.add(r"(?<=})\s*(?P<defs>(?:[a-zA-Z0-9_]+\s*,\s*)+[a-zA-Z0-9_]+)\s*(?=;)", "DEFS")
75
- TOK.add(r"(?P<name>\**?\s*[a-zA-Z0-9_]+)(?:\s*:\s*(?P<bits>\d+))?(?:\[(?P<count>[^;]*)\])?\s*(?=;)", "NAME")
76
- TOK.add(r"#include\s+(?P<name>[^\s]+)\s*", "INCLUDE")
77
- TOK.add(r"[a-zA-Z_][a-zA-Z0-9_]*", "IDENTIFIER")
78
- TOK.add(r"[{}]", "BLOCK")
79
- TOK.add(r"\$(?P<name>[^\s]+) = (?P<value>{[^}]+})\w*[\r\n]+", "LOOKUP")
80
- TOK.add(r";", "EOL")
81
- TOK.add(r"\s+", None)
82
- TOK.add(r".", None)
83
-
84
- return TOK
85
-
86
- def _identifier(self, tokens: TokenConsumer) -> str:
87
- idents = []
88
- while tokens.next == self.TOK.IDENTIFIER:
89
- idents.append(tokens.consume())
90
- return " ".join([i.value for i in idents])
91
-
92
- def _conditional(self, tokens: TokenConsumer) -> None:
93
- token = tokens.consume()
94
- pattern = self.TOK.patterns[token.token]
95
- match = pattern.match(token.value).groupdict()
96
-
97
- value = match["name"]
98
-
99
- if token.token == self.TOK.IFDEF:
100
- self._conditionals.append(value in self.cstruct.consts)
101
- elif token.token == self.TOK.IFNDEF:
102
- self._conditionals.append(value not in self.cstruct.consts)
103
-
104
- def _check_conditional(self, tokens: TokenConsumer) -> bool:
105
- """Check and handle conditionals. Return a boolean indicating if we need to continue to the next token."""
106
- if self._conditionals and self._conditionals_depth == len(self._conditionals):
107
- # If we have a conditional and the depth matches, handle it accordingly
108
- if tokens.next == self.TOK.ELSE:
109
- # Flip the last conditional
110
- tokens.consume()
111
- self._conditionals[-1] = not self._conditionals[-1]
112
- return True
113
-
114
- if tokens.next == self.TOK.ENDIF:
115
- # Pop the last conditional
116
- tokens.consume()
117
- self._conditionals.pop()
118
- self._conditionals_depth -= 1
119
- return True
120
-
121
- if tokens.next in (self.TOK.IFDEF, self.TOK.IFNDEF):
122
- # If we encounter a new conditional, increase the depth
123
- self._conditionals_depth += 1
124
-
125
- if tokens.next == self.TOK.ENDIF:
126
- # Similarly, decrease the depth if needed
127
- self._conditionals_depth -= 1
128
-
129
- if self._conditionals and not self._conditionals[-1]:
130
- # If the last conditional evaluated to False, skip the next token
131
- tokens.consume()
132
- return True
133
-
134
- if tokens.next in (self.TOK.IFDEF, self.TOK.IFNDEF):
135
- # If the next token is a conditional, process it
136
- self._conditional(tokens)
137
- return True
138
-
139
- return False
140
61
 
141
- def _constant(self, tokens: TokenConsumer) -> None:
142
- const = tokens.consume()
143
- pattern = self.TOK.patterns[self.TOK.DEFINE]
144
- match = pattern.match(const.value).groupdict()
62
+ self._flags: list[str] = []
63
+ self._conditional_stack: list[tuple[Token, bool]] = []
145
64
 
146
- value = match["value"].strip()
147
- try:
148
- value = ast.literal_eval(value)
149
- except (ValueError, SyntaxError):
150
- pass
65
+ def reset(self) -> None:
66
+ """Reset the parser state for a new input."""
67
+ self._reset_tokens([])
68
+ self._flags = []
69
+ self._conditional_stack = []
151
70
 
152
- if isinstance(value, str):
153
- try:
154
- value = Expression(value).evaluate(self.cstruct)
155
- except (ExpressionParserError, ExpressionTokenizerError):
156
- pass
71
+ def parse(self, data: str) -> None:
72
+ """Parse C-like definitions from the input data."""
73
+ self.reset()
157
74
 
158
- self.cstruct.consts[match["name"]] = value
75
+ data = _join_line_continuations(data)
159
76
 
160
- def _undef(self, tokens: TokenConsumer) -> None:
161
- const = tokens.consume()
162
- pattern = self.TOK.patterns[self.TOK.UNDEF]
163
- match = pattern.match(const.value).groupdict()
77
+ self._reset_tokens(tokenize(data))
78
+ self._parse()
164
79
 
165
- if match["name"] in self.cstruct.consts:
166
- del self.cstruct.consts[match["name"]]
167
- else:
168
- raise ParserError(f"line {self._lineno(const)}: constant {match['name']!r} not defined")
80
+ def _match(self, *types: TokenType) -> Token | None:
81
+ """Consume and return the current token if it matches any of the given types, otherwise return None."""
82
+ if self._current().type in types:
83
+ return self._take()
84
+ return None
169
85
 
170
- def _enum(self, tokens: TokenConsumer) -> None:
171
- # We cheat with enums because the entire enum is in the token
172
- etok = tokens.consume()
86
+ def _at(self, *types: TokenType) -> bool:
87
+ """Return whether the current token matches any of the given types."""
88
+ return self._tokens[self._pos].type in types
173
89
 
174
- pattern = self.TOK.patterns[self.TOK.ENUM]
175
- # Dirty trick because the regex expects a ; but we don't want it to be part of the value
176
- d = pattern.match(etok.value + ";").groupdict()
177
- enumtype = d["enumtype"]
90
+ def _error(self, msg: str, *, token: Token | None = None) -> ParserError:
91
+ return ParserError(f"line {(token if token is not None else self._tokens[self._pos]).line}: {msg}")
178
92
 
179
- nextval = 0
180
- if enumtype == "flag":
181
- nextval = 1
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]
182
96
 
183
- values = {}
184
- for line in d["values"].splitlines():
185
- for v in line.split(","):
186
- key, _, val = v.partition("=")
187
- key = key.strip()
188
- val = val.strip()
189
- if not key:
190
- continue
97
+ def _handle_preprocessor(self) -> bool:
98
+ """Handle preprocessor directives.
191
99
 
192
- val = nextval if not val else Expression(val).evaluate(self.cstruct, values)
100
+ Returns ``True`` if a directive was processed, indicating the caller should continue its loop.
101
+ """
102
+ token = self._current()
193
103
 
194
- if enumtype == "flag":
195
- high_bit = val.bit_length() - 1
196
- nextval = 2 ** (high_bit + 1)
197
- else:
198
- nextval = val + 1
104
+ if token.type in (TokenType.PP_IFDEF, TokenType.PP_IFNDEF, TokenType.PP_ELSE, TokenType.PP_ENDIF):
105
+ self._handle_conditional()
106
+ return True
199
107
 
200
- values[key] = val
108
+ if token.type == TokenType.PP_DEFINE:
109
+ self._parse_define()
110
+ return True
201
111
 
202
- if not d["type"]:
203
- d["type"] = "uint32"
112
+ if token.type == TokenType.PP_UNDEF:
113
+ self._parse_undef()
114
+ return True
204
115
 
205
- factory = self.cstruct._make_flag if enumtype == "flag" else self.cstruct._make_enum
116
+ if token.type == TokenType.PP_INCLUDE:
117
+ self._parse_include()
118
+ return True
206
119
 
207
- enum = factory(d["name"] or "", self.cstruct.resolve(d["type"]), values)
208
- if not enum.__name__:
209
- self.cstruct.consts.update(enum.__members__)
210
- else:
211
- self.cstruct.add_type(enum.__name__, enum)
120
+ return False
212
121
 
213
- tokens.eol()
122
+ def _parse(self) -> None:
123
+ """Parse top-level definitions from the token stream."""
124
+ while (token := self._current()).type != TokenType.EOF:
125
+ if self._handle_preprocessor():
126
+ continue
214
127
 
215
- def _typedef(self, tokens: TokenConsumer) -> None:
216
- tokens.consume()
217
- type_ = None
128
+ if token.type == TokenType.PP_FLAGS:
129
+ self._parse_config_flags()
130
+ elif token.type == TokenType.TYPEDEF:
131
+ self._parse_typedef()
132
+ elif token.type in (TokenType.STRUCT, TokenType.UNION):
133
+ self._parse_struct_or_union()
218
134
 
219
- names = []
135
+ # Skip variable declarations after struct/union definitions
136
+ while not self._at(TokenType.SEMICOLON, TokenType.EOF):
137
+ self._pos += 1
220
138
 
221
- if tokens.next == self.TOK.IDENTIFIER:
222
- type_ = self.cstruct.resolve(self._identifier(tokens))
223
- elif tokens.next == self.TOK.STRUCT:
224
- type_ = self._struct(tokens)
225
- if not type_.__anonymous__:
226
- names.append(type_.__name__)
139
+ self._expect(TokenType.SEMICOLON)
140
+ elif token.type in (TokenType.ENUM, TokenType.FLAG):
141
+ type_ = self._parse_enum_or_flag()
227
142
 
228
- names.extend(self._names(tokens))
229
- for name in names:
230
- if issubclass(type_, Structure) and type_.__anonymous__:
231
- type_.__anonymous__ = False
232
- type_.__name__ = name
233
- type_.__qualname__ = name
143
+ # If it's an anonymous enum/flag, add its members to the constants for convenience
144
+ if not type_.__name__:
145
+ self.cs.consts.update(type_.__members__)
234
146
 
235
- type_, name, bits = self._parse_field_type(type_, name)
236
- if bits is not None:
237
- raise ParserError(f"line {self._lineno(tokens.previous)}: typedefs cannot have bitfields")
238
- self.cstruct.add_type(name, type_)
239
-
240
- def _struct(self, tokens: TokenConsumer, register: bool = False) -> type[Structure]:
241
- stype = tokens.consume()
242
-
243
- factory = self.cstruct._make_union if stype.value.startswith("union") else self.cstruct._make_struct
244
-
245
- st = None
246
- names = []
247
- registered = False
248
-
249
- if tokens.next == self.TOK.IDENTIFIER:
250
- ident = tokens.consume()
251
- if register:
252
- # Pre-register an empty struct for self-referencing
253
- # We update this instance later with the fields
254
- st = factory(ident.value, [], align=self.align)
255
- if self.compiled and "nocompile" not in tokens.flags:
256
- st = compiler.compile(st)
257
- self.cstruct.add_type(ident.value, st)
258
- registered = True
147
+ self._expect(TokenType.SEMICOLON)
259
148
  else:
260
- names.append(ident.value)
261
-
262
- if tokens.next == self.TOK.NAME:
263
- # As part of a struct field
264
- # struct type_name field_name;
265
- if not names:
266
- raise ParserError(f"line {self._lineno(tokens.next)}: unexpected anonymous struct")
267
- return self.cstruct.resolve(names[0])
268
-
269
- if tokens.next != self.TOK.BLOCK:
270
- raise ParserError(f"line {self._lineno(tokens.next)}: expected start of block '{tokens.next}'")
271
-
272
- fields = []
273
- tokens.consume()
274
- while len(tokens):
275
- if tokens.next == self.TOK.BLOCK and tokens.next.value == "}":
276
- tokens.consume()
277
- break
149
+ raise self._error(f"unexpected token {token.value!r}")
278
150
 
279
- if self._check_conditional(tokens):
280
- continue
151
+ if self._conditional_stack:
152
+ raise self._error("unclosed conditional statement", token=self._conditional_stack[-1][0])
281
153
 
282
- field = self._parse_field(tokens)
283
- fields.append(field)
154
+ # Preprocessor directives
284
155
 
285
- if register:
286
- names.extend(self._names(tokens))
156
+ def _parse_define(self) -> None:
157
+ """Parse a define directive and add the constant."""
158
+ self._expect(TokenType.PP_DEFINE)
287
159
 
288
- # If the next token is EOL, consume it
289
- # Otherwise we're part of a typedef or field definition
290
- if tokens.next == self.TOK.EOL:
291
- tokens.eol()
160
+ name_token = self._expect(TokenType.IDENTIFIER)
292
161
 
293
- name = names[0] if names else None
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
294
166
 
295
- if st is None:
296
- is_anonymous = False
297
- if not name:
298
- is_anonymous = True
299
- name = self.cstruct._next_anonymous()
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)
300
172
 
301
- st = factory(name, fields, align=self.align, anonymous=is_anonymous)
302
- if self.compiled and "nocompile" not in tokens.flags:
303
- st = compiler.compile(st)
173
+ # Remove the surrounding quotes and flatten escape sequences
174
+ value = value[1:-1].encode().decode("unicode_escape")
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
194
+
195
+ self.cs.consts[name_token.value] = value
196
+
197
+ def _parse_undef(self) -> None:
198
+ """Parse an undef directive and remove the constant."""
199
+ self._expect(TokenType.PP_UNDEF)
200
+
201
+ name_token = self._expect(TokenType.IDENTIFIER)
202
+ if name_token.value in self.cs.consts:
203
+ del self.cs.consts[name_token.value]
304
204
  else:
305
- st.__fields__.extend(fields)
306
- st.commit()
307
-
308
- # This is pretty dirty
309
- if register:
310
- if not names and not registered:
311
- raise ParserError(f"line {self._lineno(stype)}: struct has no name")
312
-
313
- for name in names:
314
- self.cstruct.add_type(name, st)
315
-
316
- tokens.reset_flags()
317
- return st
318
-
319
- def _lookup(self, tokens: TokenConsumer) -> None:
320
- # Just like enums, we cheat and have the entire lookup in the token
321
- ltok = tokens.consume()
322
-
323
- pattern = self.TOK.patterns[self.TOK.LOOKUP]
324
- # Dirty trick because the regex expects a ; but we don't want it to be part of the value
325
- m = pattern.match(ltok.value + ";")
326
- d = ast.literal_eval(m.group(2))
327
- self.cstruct.lookups[m.group(1)] = {self.cstruct.consts[k]: v for k, v in d.items()}
328
-
329
- def _parse_field(self, tokens: TokenConsumer) -> Field:
330
- type_ = None
331
- if tokens.next == self.TOK.IDENTIFIER:
332
- type_ = self.cstruct.resolve(self._identifier(tokens))
333
- elif tokens.next == self.TOK.STRUCT:
334
- type_ = self._struct(tokens)
335
-
336
- if tokens.next != self.TOK.NAME:
337
- return Field(None, type_, None)
338
-
339
- if tokens.next != self.TOK.NAME:
340
- raise ParserError(f"line {self._lineno(tokens.next)}: expected name, got {tokens.next!r}")
341
- nametok = tokens.consume()
342
-
343
- type_, name, bits = self._parse_field_type(type_, nametok.value)
344
-
345
- tokens.eol()
346
- return Field(name.strip(), type_, bits)
347
-
348
- def _parse_field_type(self, type_: type[BaseType], name: str) -> tuple[type[BaseType], str, int | None]:
349
- pattern = self.TOK.patterns[self.TOK.NAME]
350
- # Dirty trick because the regex expects a ; but we don't want it to be part of the value
351
- d = pattern.match(name + ";").groupdict()
352
-
353
- name = d["name"]
354
- count_expression = d["count"]
355
-
356
- while name.startswith("*"):
357
- name = name[1:]
358
- type_ = self.cstruct._make_pointer(type_)
359
-
360
- if count_expression is not None:
361
- # Poor mans multi-dimensional array by abusing the eager regex match of count
362
- counts = count_expression.split("][") if "][" in count_expression else [count_expression]
363
-
364
- for count in reversed(counts):
365
- if count == "":
366
- count = None
367
- else:
368
- count = Expression(count)
369
- try:
370
- count = count.evaluate(self.cstruct)
371
- except Exception:
372
- pass
373
-
374
- if issubclass(type_, BaseArray) and count is None:
375
- raise ParserError("Depth required for multi-dimensional array")
376
-
377
- type_ = self.cstruct._make_array(type_, count)
378
-
379
- return type_, name.strip(), int(d["bits"]) if d["bits"] else None
380
-
381
- def _names(self, tokens: TokenConsumer) -> list[str]:
382
- names = []
383
- while True:
384
- if tokens.next == self.TOK.EOL:
385
- tokens.eol()
386
- break
205
+ raise self._error(f"constant {name_token.value!r} not defined", token=name_token)
206
+
207
+ def _parse_include(self) -> None:
208
+ """Parse an include directive and add the included file to the includes list."""
209
+ self._expect(TokenType.PP_INCLUDE)
210
+ self.cs.includes.append(self._expect(TokenType.STRING).value)
211
+
212
+ def _parse_config_flags(self) -> None:
213
+ """Parse configuration flags from a directive like ``#[flag1, flag2, ...]``."""
214
+ self._flags.extend(flag.strip() for flag in self._expect(TokenType.PP_FLAGS).value.split(","))
215
+
216
+ def _handle_conditional(self) -> None:
217
+ """Handle conditional directives: ``#ifdef``, ``#ifndef``, ``#else``, ``#endif``."""
218
+ if (token := self._take()).type not in (
219
+ TokenType.PP_IFDEF,
220
+ TokenType.PP_IFNDEF,
221
+ TokenType.PP_ELSE,
222
+ TokenType.PP_ENDIF,
223
+ ):
224
+ raise self._error("expected conditional directive")
225
+
226
+ if token.type == TokenType.PP_IFDEF:
227
+ name = self._expect(TokenType.IDENTIFIER).value
228
+ if self._conditional_stack and not self._conditional_stack[-1][1]:
229
+ # Parent is false, so this child is always false
230
+ self._conditional_stack.append((token, False))
231
+ else:
232
+ self._conditional_stack.append((token, name in self.cs.consts))
387
233
 
388
- if tokens.next not in (self.TOK.NAME, self.TOK.DEFS, self.TOK.IDENTIFIER):
389
- break
234
+ elif token.type == TokenType.PP_IFNDEF:
235
+ name = self._expect(TokenType.IDENTIFIER).value
236
+ if self._conditional_stack and not self._conditional_stack[-1][1]:
237
+ self._conditional_stack.append((token, False))
238
+ else:
239
+ self._conditional_stack.append((token, name not in self.cs.consts))
240
+
241
+ elif token.type == TokenType.PP_ELSE:
242
+ if not self._conditional_stack:
243
+ raise self._error("#else without matching #ifdef/#ifndef", token=token)
244
+
245
+ # Only flip if parent is true (or there's no parent)
246
+ parent_active = len(self._conditional_stack) < 2 or self._conditional_stack[-2][1]
247
+ if parent_active:
248
+ self._conditional_stack[-1] = (self._conditional_stack[-1][0], not self._conditional_stack[-1][1])
249
+
250
+ elif token.type == TokenType.PP_ENDIF:
251
+ if not self._conditional_stack:
252
+ raise self._error("#endif without matching #ifdef/#ifndef", token=token)
253
+ self._conditional_stack.pop()
254
+
255
+ # If we ended up in a false branch, skip ahead to the matching #else/#endif
256
+ if self._in_false_branch():
257
+ self._skip_false_branch()
258
+
259
+ def _skip_false_branch(self) -> None:
260
+ """Skip all tokens in a false conditional branch until the matching ``#else`` or ``#endif``."""
261
+ depth = 0
262
+ while self._current().type != TokenType.EOF:
263
+ token_type = self._current().type
264
+
265
+ if token_type in (TokenType.PP_IFDEF, TokenType.PP_IFNDEF):
266
+ depth += 1
267
+ self._pos += 1
268
+ # Skip the identifier argument
269
+ if self._current().type != TokenType.EOF:
270
+ self._pos += 1
271
+ elif token_type == TokenType.PP_ENDIF:
272
+ if depth == 0:
273
+ return
274
+ depth -= 1
275
+ self._pos += 1
276
+ elif token_type == TokenType.PP_ELSE:
277
+ if depth == 0:
278
+ return
279
+ self._pos += 1
280
+ else:
281
+ self._pos += 1
390
282
 
391
- ntoken = tokens.consume()
392
- if ntoken in (self.TOK.NAME, self.TOK.IDENTIFIER):
393
- names.append(ntoken.value.strip())
394
- elif ntoken == self.TOK.DEFS:
395
- names.extend([name.strip() for name in ntoken.value.strip().split(",")])
396
-
397
- return names
398
-
399
- def _include(self, tokens: TokenConsumer) -> None:
400
- include = tokens.consume()
401
- pattern = self.TOK.patterns[self.TOK.INCLUDE]
402
- match = pattern.match(include.value).groupdict()
403
-
404
- self.cstruct.includes.append(match["name"].strip().strip("'\""))
405
-
406
- @staticmethod
407
- def _remove_comments(string: str) -> str:
408
- # https://stackoverflow.com/a/18381470
409
- pattern = r"(\".*?\"|\'.*?\')|(/\*.*?\*/|//[^\r\n]*$)"
410
- # first group captures quoted strings (double or single)
411
- # second group captures comments (//single-line or /* multi-line */)
412
- regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
413
-
414
- def _replacer(match: re.Match) -> str:
415
- # if the 2nd group (capturing comments) is not None,
416
- # it means we have captured a non-quoted (real) comment string.
417
- if comment := match.group(2):
418
- return "\n" * comment.count("\n") # so we will return empty to remove the comment
419
- # otherwise, we will return the 1st group
420
- return match.group(1) # captured quoted-string
421
-
422
- return regex.sub(_replacer, string)
423
-
424
- @staticmethod
425
- def _lineno(tok: Token) -> int:
426
- """Quick and dirty line number calculator."""
427
- match = tok.match
428
- return match.string.count("\n", 0, match.start()) + 1
429
-
430
- def _config_flag(self, tokens: TokenConsumer) -> None:
431
- flag_token = tokens.consume()
432
- pattern = self.TOK.patterns[self.TOK.CONFIG_FLAG]
433
- tok_dict = pattern.match(flag_token.value).groupdict()
434
- tokens.flags.extend(tok_dict["values"].split(","))
283
+ # Type definitions
435
284
 
436
- def parse(self, data: str) -> None:
437
- scanner = re.Scanner(self.TOK.tokens)
438
- data = self._remove_comments(data)
439
- tokens, remaining = scanner.scan(data)
440
-
441
- if len(remaining):
442
- lineno = data.count("\n", 0, len(data) - len(remaining))
443
- raise ParserError(f"line {lineno}: invalid syntax in definition")
444
-
445
- tokens = TokenConsumer(tokens)
446
- while True:
447
- token = tokens.next
448
- if token is None:
285
+ def _parse_typedef(self) -> None:
286
+ """Parse a typedef definition."""
287
+ self._expect(TokenType.TYPEDEF)
288
+
289
+ base_type = self._parse_type_spec()
290
+
291
+ # Parse one or more typedef names with modifiers (pointers, arrays)
292
+ while self._at(TokenType.IDENTIFIER, TokenType.STAR):
293
+ type_, name, bits = self._parse_field_name(base_type)
294
+ if bits is not None:
295
+ raise self._error("typedefs cannot have bitfields")
296
+
297
+ # For convenience, we assign the typedef name to anonymous structs/unions
298
+ if issubclass(base_type, Structure) and base_type.__anonymous__:
299
+ base_type.__anonymous__ = False
300
+ base_type.__name__ = name
301
+ base_type.__qualname__ = name
302
+
303
+ self.cs.add_type(name, type_)
304
+
305
+ if not self._match(TokenType.COMMA):
449
306
  break
450
307
 
451
- if self._check_conditional(tokens):
452
- continue
308
+ self._match(TokenType.SEMICOLON)
453
309
 
454
- if token == self.TOK.CONFIG_FLAG:
455
- self._config_flag(tokens)
456
- elif token == self.TOK.DEFINE:
457
- self._constant(tokens)
458
- elif token == self.TOK.UNDEF:
459
- self._undef(tokens)
460
- elif token == self.TOK.TYPEDEF:
461
- self._typedef(tokens)
462
- elif token == self.TOK.STRUCT:
463
- self._struct(tokens, register=True)
464
- elif token == self.TOK.ENUM:
465
- self._enum(tokens)
466
- elif token == self.TOK.LOOKUP:
467
- self._lookup(tokens)
468
- elif token == self.TOK.INCLUDE:
469
- self._include(tokens)
470
- else:
471
- raise ParserError(f"line {self._lineno(token)}: unexpected token {token!r}")
310
+ def _parse_struct_or_union(self) -> type[Structure]:
311
+ """Parse a struct or union definition.
472
312
 
473
- if self._conditionals:
474
- raise ParserError(f"line {self._lineno(tokens.previous)}: unclosed conditional statement")
313
+ If ``register`` is ``True``, the struct will be registered with its name (which is required).
314
+ Otherwise, the struct will not be registered and can only be used as an inline type for fields.
315
+ """
316
+ start_token = self._expect(TokenType.STRUCT, TokenType.UNION)
475
317
 
318
+ is_union = start_token.type == TokenType.UNION
319
+ factory = self.cs._make_union if is_union else self.cs._make_struct
476
320
 
477
- class CStyleParser(Parser):
478
- """Definition parser for C-like structure syntax (legacy parser).
321
+ type = None
322
+ name = None
479
323
 
480
- Args:
481
- cs: An instance of cstruct
482
- compiled: Whether structs should be compiled or not.
483
- """
324
+ if not self._at(TokenType.LBRACE):
325
+ if not self._at(TokenType.IDENTIFIER):
326
+ raise self._error("expected struct name or '{'", token=start_token)
484
327
 
485
- def __init__(self, cs: cstruct, compiled: bool = True):
486
- self.compiled = compiled
487
- super().__init__(cs)
328
+ name = self._take().value
488
329
 
489
- def _constants(self, data: str) -> None:
490
- r = re.finditer(r"#define\s+(?P<name>[^\s]+)\s+(?P<value>[^\r\n]+)\s*\n", data)
491
- for t in r:
492
- d = t.groupdict()
493
- v = d["value"].rsplit("//")[0]
494
-
495
- try:
496
- v = ast.literal_eval(v)
497
- except (ValueError, SyntaxError):
498
- pass
499
-
500
- self.cstruct.consts[d["name"]] = v
501
-
502
- def _enums(self, data: str) -> None:
503
- r = re.finditer(
504
- r"(?P<enumtype>enum|flag)\s+(?P<name>[^\s:{]+)\s*(:\s*(?P<type>[^\s]+)\s*)?\{(?P<values>[^}]+)\}\s*;",
505
- data,
506
- )
507
- for t in r:
508
- d = t.groupdict()
509
- enumtype = d["enumtype"]
510
-
511
- nextval = 0
512
- if enumtype == "flag":
513
- nextval = 1
514
-
515
- values = {}
516
- for line in d["values"].split("\n"):
517
- line, _, _ = line.partition("//")
518
- for v in line.split(","):
519
- key, _, val = v.partition("=")
520
- key = key.strip()
521
- val = val.strip()
522
- if not key:
523
- continue
524
-
525
- val = nextval if not val else Expression(val).evaluate(self.cstruct)
526
-
527
- if enumtype == "flag":
528
- high_bit = val.bit_length() - 1
529
- nextval = 2 ** (high_bit + 1)
530
- else:
531
- nextval = val + 1
532
-
533
- values[key] = val
534
-
535
- if not d["type"]:
536
- d["type"] = "uint32"
537
-
538
- factory = self.cstruct._make_enum
539
- if enumtype == "flag":
540
- factory = self.cstruct._make_flag
541
-
542
- enum = factory(d["name"], self.cstruct.resolve(d["type"]), values)
543
- self.cstruct.add_type(enum.__name__, enum)
544
-
545
- def _structs(self, data: str) -> None:
546
- r = re.finditer(
547
- r"(#(?P<flags>(?:compile))\s+)?"
548
- r"((?P<typedef>typedef)\s+)?"
549
- r"(?P<type>[^\s]+)\s+"
550
- r"(?P<name>[^\s]+)?"
551
- r"(?P<fields>"
552
- r"\s*{[^}]+\}(?P<defs>\s+[^;\n]+)?"
553
- r")?\s*;",
554
- data,
555
- )
556
- for t in r:
557
- d = t.groupdict()
558
-
559
- if d["name"]:
560
- name = d["name"]
561
- elif d["defs"]:
562
- name = d["defs"].strip().split(",")[0].strip()
563
- else:
564
- raise ParserError("No name for struct")
565
-
566
- if d["type"] == "struct":
567
- data = self._parse_fields(d["fields"][1:-1].strip())
568
- st = self.cstruct._make_struct(name, data)
569
- if d["flags"] == "compile" or self.compiled:
570
- st = compiler.compile(st)
571
- elif d["typedef"] == "typedef":
572
- st = d["type"]
330
+ # struct name { ... }
331
+ if self._at(TokenType.LBRACE):
332
+ # Named struct/union, empty pre-register for self-referencing
333
+ type = factory(name, [], align=self.align)
334
+ if self.compiled and "nocompile" not in self._flags:
335
+ type = compiler.compile(type)
336
+ self.cs.add_type(name, type)
573
337
  else:
574
- continue
338
+ # struct typename ... (type reference)
339
+ return self.cs.resolve(name)
575
340
 
576
- if d["name"]:
577
- self.cstruct.add_type(d["name"], st)
341
+ # Parse body
342
+ self._expect(TokenType.LBRACE)
343
+ fields = self._parse_field_list()
344
+ self._expect(TokenType.RBRACE)
578
345
 
579
- if d["defs"]:
580
- for td in d["defs"].strip().split(","):
581
- td = td.strip()
582
- self.cstruct.add_type(td, st)
346
+ if type is None:
347
+ is_anonymous = name is None
348
+ name = name or self.cs._next_anonymous()
583
349
 
584
- def _parse_fields(self, data: str) -> None:
585
- fields = re.finditer(
586
- r"(?P<type>[^\s]+)\s+(?P<name>[^\s\[:]+)(:(?P<bits>\d+))?(\[(?P<count>[^;\n]*)\])?;",
587
- data,
588
- )
350
+ type = factory(name, fields, align=self.align, anonymous=is_anonymous)
351
+ if self.compiled and "nocompile" not in self._flags:
352
+ type = compiler.compile(type)
353
+ else:
354
+ type.__fields__.extend(fields)
355
+ type.commit()
589
356
 
590
- result = []
591
- for f in fields:
592
- d = f.groupdict()
593
- if d["type"].startswith("//"):
594
- continue
357
+ self._flags.clear()
358
+ return type
595
359
 
596
- type_ = self.cstruct.resolve(d["type"])
360
+ def _parse_enum_or_flag(self) -> type[Enum | Flag]:
361
+ """Parse an enum or flag definition."""
362
+ start_token = self._expect(TokenType.ENUM, TokenType.FLAG)
597
363
 
598
- d["name"] = d["name"].replace("(", "").replace(")", "")
364
+ is_flag = start_token.type == TokenType.FLAG
599
365
 
600
- # Maybe reimplement lazy type references later
601
- # _type = TypeReference(self, d['type'])
602
- if d["count"] is not None:
603
- if d["count"] == "":
604
- count = None
605
- else:
606
- count = Expression(d["count"])
607
- try:
608
- count = count.evaluate(self.cstruct)
609
- except Exception:
610
- pass
366
+ name = None
367
+ if self._at(TokenType.IDENTIFIER):
368
+ name = self._take().value
611
369
 
612
- type_ = self.cstruct._make_array(type_, count)
370
+ # Optional base type
371
+ base_type_str = "uint32"
372
+ if self._match(TokenType.COLON):
373
+ parts = []
374
+ while (token := self._match(TokenType.IDENTIFIER)) is not None:
375
+ parts.append(token.value)
376
+ base_type_str = " ".join(parts)
613
377
 
614
- if d["name"].startswith("*"):
615
- d["name"] = d["name"][1:]
616
- type_ = self.cstruct._make_pointer(type_)
378
+ self._expect(TokenType.LBRACE)
617
379
 
618
- field = Field(d["name"], type_, int(d["bits"]) if d["bits"] else None)
619
- result.append(field)
380
+ next_value = 1 if is_flag else 0
381
+ values: dict[str, int] = {}
620
382
 
621
- return result
383
+ while not self._at(TokenType.RBRACE):
384
+ if self._handle_preprocessor():
385
+ continue
386
+ self._assert_not_eof()
622
387
 
623
- def _lookups(self, data: str, consts: dict[str, int]) -> None:
624
- r = re.finditer(r"\$(?P<name>[^\s]+) = ({[^}]+})\w*\n", data)
388
+ member_name = self._expect(TokenType.IDENTIFIER).value
625
389
 
626
- for t in r:
627
- d = ast.literal_eval(t.group(2))
628
- self.cstruct.lookups[t.group(1)] = {self.cstruct.consts[k]: v for k, v in d.items()}
390
+ if self._match(TokenType.EQUALS):
391
+ expression = self._collect_until(TokenType.COMMA, TokenType.RBRACE)
392
+ value = Expression(expression).evaluate(self.cs, values)
393
+ else:
394
+ value = next_value
629
395
 
630
- def parse(self, data: str) -> None:
631
- self._constants(data)
632
- self._enums(data)
633
- self._structs(data)
634
- self._lookups(data, self.cstruct.consts)
396
+ if is_flag:
397
+ high_bit = value.bit_length() - 1
398
+ next_value = 2 ** (high_bit + 1)
399
+ else:
400
+ next_value = value + 1
401
+
402
+ values[member_name] = value
403
+ self._match(TokenType.COMMA) # optional trailing comma
635
404
 
405
+ self._expect(TokenType.RBRACE)
636
406
 
637
- class Token:
638
- __slots__ = ("match", "token", "value")
407
+ factory = self.cs._make_flag if is_flag else self.cs._make_enum
408
+ type_ = factory(name or "", self.cs.resolve(base_type_str), values)
639
409
 
640
- def __init__(self, token: str, value: str, match: re.Match):
641
- self.token = token
642
- self.value = value
643
- self.match = match
410
+ if name is not None:
411
+ # Register the enum/flag type if it has a name
412
+ # Anonymous enums/flags are handled in the top level parse loop
413
+ self.cs.add_type(type_.__name__, type_)
644
414
 
645
- def __eq__(self, other: object) -> bool:
646
- if isinstance(other, Token):
647
- other = other.token
415
+ return type_
648
416
 
649
- return self.token == other
417
+ # Field parsing
650
418
 
651
- def __ne__(self, other: object) -> bool:
652
- return not self == other
419
+ def _parse_field_list(self) -> list[Field]:
420
+ """Parse a list of fields inside a struct/union body until the closing brace."""
421
+ fields: list[Field] = []
653
422
 
654
- def __repr__(self) -> str:
655
- return f"<Token.{self.token} value={self.value!r}>"
423
+ while not self._at(TokenType.RBRACE):
424
+ if self._handle_preprocessor():
425
+ continue
426
+ self._assert_not_eof()
656
427
 
428
+ fields.append(self._parse_field())
657
429
 
658
- class TokenCollection:
659
- def __init__(self):
660
- self.tokens: list[Token] = []
661
- self.lookup: dict[str, str] = {}
662
- self.patterns: dict[str, re.Pattern] = {}
430
+ # Handle multiple fields declared in the same line, e.g., `int x, y, z;` or `struct { ... } a, b;`
431
+ while self._match(TokenType.COMMA):
432
+ type_, name, bits = self._parse_field_name(fields[-1].type)
433
+ fields.append(Field(name, type_, bits))
663
434
 
664
- def __getattr__(self, attr: str) -> str | Any:
665
- try:
666
- return self.lookup[attr]
667
- except AttributeError:
668
- pass
435
+ self._expect(TokenType.SEMICOLON)
669
436
 
670
- return object.__getattribute__(self, attr)
437
+ return fields
671
438
 
672
- def add(self, regex: str, name: str | None) -> None:
673
- if name is None:
674
- self.tokens.append((regex, None))
675
- else:
676
- self.lookup[name] = name
677
- self.patterns[name] = re.compile(regex)
678
- self.tokens.append((regex, lambda s, t: Token(name, t, s.match)))
439
+ def _parse_field(self) -> Field:
440
+ """Parse a single field declaration."""
441
+ # Regular field: `type name`
442
+ type_ = self._parse_type_spec()
679
443
 
444
+ # Handle the case where a semicolon follows immediately (e.g., anonymous struct/unions)
445
+ if self._at(TokenType.SEMICOLON):
446
+ return Field(None, type_, None)
680
447
 
681
- class TokenConsumer:
682
- def __init__(self, tokens: list[Token]):
683
- self.tokens = tokens
684
- self.flags = []
685
- self.previous = None
448
+ type_, name, bits = self._parse_field_name(type_)
449
+ return Field(name, type_, bits)
686
450
 
687
- def __contains__(self, token: Token) -> bool:
688
- return token in self.tokens
451
+ def _parse_field_name(self, base_type: type[BaseType]) -> tuple[type[BaseType], str, int | None]:
452
+ """Parses ``'*'* IDENTIFIER ('[' expr? ']')* (':' NUMBER)?``."""
453
+ type_ = base_type
689
454
 
690
- def __len__(self) -> int:
691
- return len(self.tokens)
455
+ # Pointer stars
456
+ while self._match(TokenType.STAR):
457
+ type_ = self.cs._make_pointer(type_)
692
458
 
693
- def __repr__(self) -> str:
694
- return f"<TokenConsumer next={self.next!r}>"
459
+ # Field name
460
+ name = self._expect(*IDENTIFIER_TYPES).value
695
461
 
696
- @property
697
- def next(self) -> Token:
698
- try:
699
- return self.tokens[0]
700
- except IndexError:
701
- return None
462
+ # Array dimensions
463
+ type_ = self._parse_array_dimensions(type_)
702
464
 
703
- def consume(self) -> Token:
704
- self.previous = self.tokens.pop(0)
705
- return self.previous
465
+ # Bitfield
466
+ bits = None
467
+ if self._match(TokenType.COLON):
468
+ bits = int(self._expect(TokenType.NUMBER).value, 0)
706
469
 
707
- def reset_flags(self) -> None:
708
- self.flags = []
470
+ return type_, name.strip(), bits
471
+
472
+ def _parse_array_dimensions(self, base_type: type[BaseType]) -> type[BaseType]:
473
+ """Parse array dimensions following a field name, e.g., ``field[10][20]``."""
474
+ dimensions: list[int | Expression] = []
475
+
476
+ while self._match(TokenType.LBRACKET):
477
+ if self._at(TokenType.RBRACKET):
478
+ dimensions.append(None)
479
+ else:
480
+ expression = self._collect_until(TokenType.RBRACKET)
481
+ count = Expression(expression)
482
+ try:
483
+ count = count.evaluate(self.cs)
484
+ except Exception:
485
+ pass
486
+ dimensions.append(count)
487
+ self._expect(TokenType.RBRACKET)
488
+
489
+ type_ = base_type
490
+ for count in reversed(dimensions):
491
+ if issubclass(type_, BaseArray) and count is None:
492
+ raise ParserError("Depth required for multi-dimensional array")
493
+ type_ = self.cs._make_array(type_, count)
494
+
495
+ return type_
496
+
497
+ # Type resolution
498
+
499
+ def _parse_type_spec(self) -> type[BaseType]:
500
+ """Parse a type specifier, handling multi-word types like ``unsigned long long``.
501
+
502
+ Uses lookahead to disambiguate type words from field names: if the next identifier is followed by a
503
+ field delimiter (any of ``;[:,}``) it is the field name, not part of the type — unless the current accumulated
504
+ parts don't form a valid type yet.
505
+ """
506
+ first = self._current()
507
+
508
+ # Handle struct/union/enum/flag inline definitions as type specifiers
509
+ if first.type in (TokenType.STRUCT, TokenType.UNION):
510
+ return self._parse_struct_or_union()
511
+
512
+ if first.type in (TokenType.ENUM, TokenType.FLAG):
513
+ return self._parse_enum_or_flag()
514
+
515
+ # Otherwise, accumulate identifiers for the type specifier until we hit a non-identifier or a field delimiter
516
+ parts = [self._expect(TokenType.IDENTIFIER).value]
517
+
518
+ while self._at(TokenType.IDENTIFIER):
519
+ next_after = self._peek(1)
520
+
521
+ if next_after.type in (
522
+ TokenType.SEMICOLON,
523
+ TokenType.LBRACKET,
524
+ TokenType.COLON,
525
+ TokenType.COMMA,
526
+ TokenType.RBRACE,
527
+ ):
528
+ # This identifier is followed by a field delimiter, it should be the field name,
529
+ # UNLESS the current parts don't form a valid type yet.
530
+ if " ".join(parts) in self.cs.typedefs:
531
+ break
532
+
533
+ # Current parts don't resolve, consume and hope this completes the type name
534
+ # (will error on resolve if not).
535
+ parts.append(self._take().value)
536
+ elif next_after.type == TokenType.STAR:
537
+ # Field name starts with * (pointer). This identifier is the last type word, consume it and then stop.
538
+ parts.append(self._take().value)
539
+ break
540
+ elif next_after.type == TokenType.IDENTIFIER:
541
+ # More identifiers follow, consume this one as part of the type.
542
+ parts.append(self._take().value)
543
+ else:
544
+ break
709
545
 
710
- def eol(self) -> None:
711
- token = self.consume()
712
- if token.token != "EOL":
713
- raise ParserError(f"line {self._lineno(token)}: expected EOL")
546
+ return self.cs.resolve(" ".join(parts))