ifc_lite_core/parser/
header_scan.rs

1// SPDX-License-Identifier: MPL-2.0
2//! Quote-aware byte scanning shared by the source-header record readers.
3
4use crate::parser::is_step_space;
5
6/// A monotonically advancing scan of one byte buffer.
7///
8/// Once a `*/` search fails, no later comment opener can have a closer, so
9/// remembering that result keeps malformed-input scanning linear.
10pub struct Lex<'a> {
11    bytes: &'a [u8],
12    no_closer: bool,
13    /// `*/` searches performed. At most one can fail.
14    pub searches: u32,
15}
16
17impl<'a> Lex<'a> {
18    pub fn new(bytes: &'a [u8]) -> Self {
19        Lex {
20            bytes,
21            no_closer: false,
22            searches: 0,
23        }
24    }
25
26    /// If a `/* ... */` comment starts at `bytes[i]`, return the index just
27    /// past it. An unterminated opener is ordinary text, not a comment.
28    fn skip_comment_at(&mut self, i: usize) -> Option<usize> {
29        if self.bytes.get(i) != Some(&b'/') || self.bytes.get(i + 1) != Some(&b'*') {
30            return None;
31        }
32        if self.no_closer {
33            return None;
34        }
35        self.searches += 1;
36        match crate::parser::skip_step_comment(self.bytes, i) {
37            Some(end) => Some(end),
38            None => {
39                self.no_closer = true;
40                None
41            }
42        }
43    }
44
45    /// If a string literal or comment starts at `bytes[i]`, return the index
46    /// just past it. Doubled apostrophes remain inside the literal.
47    pub fn skip_lexical_at(&mut self, i: usize) -> Option<usize> {
48        if self.bytes.get(i)? == &b'\'' {
49            let mut p = i + 1;
50            while p < self.bytes.len() {
51                if self.bytes[p] != b'\'' || crate::parser::lexical::is_page_quote_operand(self.bytes, p) {
52                    p += 1;
53                } else if self.bytes.get(p + 1) == Some(&b'\'') {
54                    p += 2;
55                } else {
56                    return Some(p + 1);
57                }
58            }
59            return Some(self.bytes.len());
60        }
61        self.skip_comment_at(i)
62    }
63
64    /// Advance past whitespace and closed comments.
65    pub fn skip_trivia(&mut self, mut i: usize) -> usize {
66        loop {
67            while i < self.bytes.len() && is_step_space(self.bytes[i]) {
68                i += 1;
69            }
70            match self.skip_comment_at(i) {
71                Some(end) => i = end,
72                None => return i,
73            }
74        }
75    }
76}
77
78fn is_ident(b: u8) -> bool {
79    b.is_ascii_alphanumeric() || b == b'_'
80}
81
82/// Find the opening parenthesis of an exact record keyword outside
83/// literals/comments, ignoring ASCII case.
84pub fn find_record_open(haystack: &[u8], keyword: &[u8]) -> Option<usize> {
85    if keyword.is_empty() || keyword.len() > haystack.len() {
86        return None;
87    }
88    let mut lex = Lex::new(haystack);
89    let last_start = haystack.len() - keyword.len();
90    let mut i = 0;
91    while i <= last_start {
92        if let Some(end) = lex.skip_lexical_at(i) {
93            i = end;
94            continue;
95        }
96        let end = i + keyword.len();
97        let bounded = (i == 0 || !is_ident(haystack[i - 1]))
98            && (end == haystack.len() || !is_ident(haystack[end]));
99        if bounded
100            && haystack[i..end]
101                .iter()
102                .zip(keyword)
103                .all(|(a, b)| a.eq_ignore_ascii_case(b))
104        {
105            let open = lex.skip_trivia(end);
106            if haystack.get(open) == Some(&b'(') {
107                return Some(open);
108            }
109        }
110        i += 1;
111    }
112    None
113}
114
115/// Locate an exact section marker such as `ENDSEC;` or `DATA;`.
116///
117/// An identifier boundary plus trivia and a semicolon prevent an embedded
118/// substring such as the `DATA` in `METADATA;` from ending the header.
119pub fn find_section_marker(haystack: &[u8], keyword: &[u8]) -> Option<usize> {
120    if keyword.is_empty() || keyword.len() > haystack.len() {
121        return None;
122    }
123    let mut lex = Lex::new(haystack);
124    let last_start = haystack.len() - keyword.len();
125    let mut i = 0;
126    while i <= last_start {
127        if let Some(end) = lex.skip_lexical_at(i) {
128            i = end;
129            continue;
130        }
131        let before_is_ident =
132            i > 0 && (haystack[i - 1].is_ascii_alphanumeric() || haystack[i - 1] == b'_');
133        let matches = !before_is_ident
134            && haystack[i..i + keyword.len()]
135                .iter()
136                .zip(keyword)
137                .all(|(a, b)| a.eq_ignore_ascii_case(b));
138        if matches {
139            let after = lex.skip_trivia(i + keyword.len());
140            if haystack.get(after) == Some(&b';') {
141                return Some(i);
142            }
143        }
144        i += 1;
145    }
146    None
147}