Skip to content

Commit bbbb86e

Browse files
authored
Add #ifdef/#ifndef parsing (#135)
1 parent 01afa28 commit bbbb86e

2 files changed

Lines changed: 188 additions & 7 deletions

File tree

dissect/cstruct/parser.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,18 @@ def __init__(self, cs: cstruct, compiled: bool = True, align: bool = False):
4949
self.compiled = compiled
5050
self.align = align
5151
self.TOK = self._tokencollection()
52+
self._conditionals = []
53+
self._conditionals_depth = 0
5254

5355
@staticmethod
5456
def _tokencollection() -> TokenCollection:
5557
TOK = TokenCollection()
5658
TOK.add(r"#\[(?P<values>[^\]]+)\](?=\s*)", "CONFIG_FLAG")
57-
TOK.add(r"#define\s+(?P<name>[^\s]+)\s+(?P<value>[^\r\n]+)\s*", "DEFINE")
59+
TOK.add(r"#define\s+(?P<name>[^\s]+)(?P<value>[^\r\n]*)", "DEFINE")
60+
TOK.add(r"#ifdef\s+(?P<name>[^\s]+)\s*", "IFDEF")
61+
TOK.add(r"#ifndef\s+(?P<name>[^\s]+)\s*", "IFNDEF")
62+
TOK.add(r"#else\s*", "ELSE")
63+
TOK.add(r"#endif\s*", "ENDIF")
5864
TOK.add(r"typedef(?=\s)", "TYPEDEF")
5965
TOK.add(r"(?:struct|union)(?=\s|{)", "STRUCT")
6066
TOK.add(
@@ -80,12 +86,61 @@ def _identifier(self, tokens: TokenConsumer) -> str:
8086
idents.append(tokens.consume())
8187
return " ".join([i.value for i in idents])
8288

89+
def _conditional(self, tokens: TokenConsumer) -> None:
90+
token = tokens.consume()
91+
pattern = self.TOK.patterns[token.token]
92+
match = pattern.match(token.value).groupdict()
93+
94+
value = match["name"]
95+
96+
if token.token == self.TOK.IFDEF:
97+
self._conditionals.append(value in self.cstruct.consts)
98+
elif token.token == self.TOK.IFNDEF:
99+
self._conditionals.append(value not in self.cstruct.consts)
100+
101+
def _check_conditional(self, tokens: TokenConsumer) -> bool:
102+
"""Check and handle conditionals. Return a boolean indicating if we need to continue to the next token."""
103+
if self._conditionals and self._conditionals_depth == len(self._conditionals):
104+
# If we have a conditional and the depth matches, handle it accordingly
105+
if tokens.next == self.TOK.ELSE:
106+
# Flip the last conditional
107+
tokens.consume()
108+
self._conditionals[-1] = not self._conditionals[-1]
109+
return True
110+
111+
if tokens.next == self.TOK.ENDIF:
112+
# Pop the last conditional
113+
tokens.consume()
114+
self._conditionals.pop()
115+
self._conditionals_depth -= 1
116+
return True
117+
118+
if tokens.next in (self.TOK.IFDEF, self.TOK.IFNDEF):
119+
# If we encounter a new conditional, increase the depth
120+
self._conditionals_depth += 1
121+
122+
if tokens.next == self.TOK.ENDIF:
123+
# Similarly, decrease the depth if needed
124+
self._conditionals_depth -= 1
125+
126+
if self._conditionals and not self._conditionals[-1]:
127+
# If the last conditional evaluated to False, skip the next token
128+
tokens.consume()
129+
return True
130+
131+
if tokens.next in (self.TOK.IFDEF, self.TOK.IFNDEF):
132+
# If the next token is a conditional, process it
133+
self._conditional(tokens)
134+
return True
135+
136+
return False
137+
83138
def _constant(self, tokens: TokenConsumer) -> None:
84139
const = tokens.consume()
85140
pattern = self.TOK.patterns[self.TOK.DEFINE]
86141
match = pattern.match(const.value).groupdict()
87142

88-
value = match["value"]
143+
value = match["value"].strip()
89144
try:
90145
value = ast.literal_eval(value)
91146
except (ValueError, SyntaxError):
@@ -208,6 +263,9 @@ def _struct(self, tokens: TokenConsumer, register: bool = False) -> type[Structu
208263
tokens.consume()
209264
break
210265

266+
if self._check_conditional(tokens):
267+
continue
268+
211269
field = self._parse_field(tokens)
212270
fields.append(field)
213271

@@ -266,7 +324,7 @@ def _parse_field(self, tokens: TokenConsumer) -> Field:
266324
return Field(None, type_, None)
267325

268326
if tokens.next != self.TOK.NAME:
269-
raise ParserError(f"line {self._lineno(tokens.next)}: expected name")
327+
raise ParserError(f"line {self._lineno(tokens.next)}: expected name, got {tokens.next!r}")
270328
nametok = tokens.consume()
271329

272330
type_, name, bits = self._parse_field_type(type_, nametok.value)
@@ -378,6 +436,9 @@ def parse(self, data: str) -> None:
378436
if token is None:
379437
break
380438

439+
if self._check_conditional(tokens):
440+
continue
441+
381442
if token == self.TOK.CONFIG_FLAG:
382443
self._config_flag(tokens)
383444
elif token == self.TOK.DEFINE:
@@ -395,6 +456,9 @@ def parse(self, data: str) -> None:
395456
else:
396457
raise ParserError(f"line {self._lineno(token)}: unexpected token {token!r}")
397458

459+
if self._conditionals:
460+
raise ParserError(f"line {self._lineno(tokens.previous)}: unclosed conditional statement")
461+
398462

399463
class CStyleParser(Parser):
400464
"""Definition parser for C-like structure syntax.

tests/test_parser.py

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING
43
from unittest.mock import Mock
54

65
import pytest
76

7+
from dissect.cstruct import cstruct
88
from dissect.cstruct.exceptions import ParserError
99
from dissect.cstruct.parser import TokenParser
1010
from dissect.cstruct.types import BaseArray, Pointer, Structure
1111
from tests.utils import verify_compiled
1212

13-
if TYPE_CHECKING:
14-
from dissect.cstruct import cstruct
15-
1613

1714
def test_nested_structs(cs: cstruct, compiled: bool) -> None:
1815
cdef = """
@@ -164,3 +161,123 @@ def test_typedef_pointer(cs: cstruct) -> None:
164161
assert cs.IMAGE_DATA_DIRECTORY is cs._IMAGE_DATA_DIRECTORY
165162
assert issubclass(cs.PIMAGE_DATA_DIRECTORY, Pointer)
166163
assert cs.PIMAGE_DATA_DIRECTORY.type == cs._IMAGE_DATA_DIRECTORY
164+
165+
166+
def test_conditional_ifdef(cs: cstruct) -> None:
167+
cdef = """
168+
#define MY_CONST 42
169+
170+
#ifdef MY_CONST
171+
struct test {
172+
uint32 a;
173+
};
174+
#endif
175+
"""
176+
cs.load(cdef)
177+
178+
assert "test" in cs.typedefs
179+
180+
181+
def test_conditional_ifndef(cs: cstruct) -> None:
182+
cdef = """
183+
#ifndef MYVAR
184+
#define MYVAR (1)
185+
#endif
186+
"""
187+
cs.load(cdef)
188+
189+
assert "MYVAR" in cs.consts
190+
assert cs.consts["MYVAR"] == 1
191+
192+
193+
def test_conditional_ifndef_guard(cs: cstruct) -> None:
194+
cdef = """
195+
/* Define Guard */
196+
#ifndef __MYGUARD
197+
#define __MYGUARD
198+
199+
typedef struct myStruct
200+
{
201+
char charVal[16];
202+
}
203+
#endif // __MYGUARD
204+
"""
205+
cs.load(cdef)
206+
207+
assert "__MYGUARD" in cs.consts
208+
assert "myStruct" in cs.typedefs
209+
210+
211+
def test_conditional_nested() -> None:
212+
cdef = """
213+
#ifndef MYSWITCH1
214+
#define MYVAR1 (1)
215+
#else
216+
#ifdef MYSWITCH2
217+
#define MYVAR1 (2)
218+
#else
219+
#define MYVAR1 (3)
220+
#endif
221+
#endif
222+
"""
223+
cs = cstruct().load(cdef)
224+
225+
assert "MYVAR1" in cs.consts
226+
assert cs.consts["MYVAR1"] == 1
227+
228+
cs = cstruct().load("#define MYSWITCH1")
229+
230+
assert "MYSWITCH1" in cs.consts
231+
232+
cs.load(cdef)
233+
234+
assert "MYVAR1" in cs.consts
235+
assert cs.consts["MYVAR1"] == 3
236+
237+
238+
def test_conditional_in_struct(cs: cstruct) -> None:
239+
cdef = """
240+
struct t_bitfield {
241+
union {
242+
struct {
243+
uint32_t bit0:1;
244+
uint32_t bit1:1;
245+
#ifdef MYSWT
246+
uint32_t bit2:1;
247+
#endif
248+
} fval;
249+
uint32_t bits;
250+
};
251+
};
252+
"""
253+
cs.load(cdef)
254+
255+
assert "t_bitfield" in cs.typedefs
256+
assert "fval" in cs.t_bitfield.fields
257+
assert "bit0" in cs.t_bitfield.fields["fval"].type.fields
258+
assert "bit1" in cs.t_bitfield.fields["fval"].type.fields
259+
assert "bit2" not in cs.t_bitfield.fields["fval"].type.fields
260+
261+
262+
def test_conditional_parsing_error(cs: cstruct) -> None:
263+
cdef = """
264+
#ifndef __HELP
265+
#define __HELP
266+
#endif
267+
struct test {
268+
uint32 a;
269+
};
270+
#endif
271+
"""
272+
with pytest.raises(ParserError, match="line 8: unexpected token .+ENDIF"):
273+
cs.load(cdef)
274+
275+
cdef = """
276+
#ifndef __HELP
277+
#define __HELP
278+
struct test {
279+
uint32 a;
280+
};
281+
"""
282+
with pytest.raises(ParserError, match="line 6: unclosed conditional statement"):
283+
cs.load(cdef)

0 commit comments

Comments
 (0)