Skip to main content

granit_parser/input/
str.rs

1use crate::{
2    char_traits::is_breakz,
3    error::ErrorKind,
4    input::{BorrowedInput, Input, SkipTabs},
5};
6use alloc::string::String;
7
8/// A parser input backed by a `&str`.
9#[allow(clippy::module_name_repetitions)]
10pub struct StrInput<'a> {
11    /// The full, original input string.
12    ///
13    /// This is kept to support O(1) byte-offset capture and zero-copy slicing via the optional
14    /// [`Input::byte_offset`] / [`Input::slice_bytes`] APIs.
15    original: &'a str,
16    /// The remaining input slice.
17    ///
18    /// This is a moving window into [`Self::original`]. All consuming operations advance this
19    /// slice.
20    buffer: &'a str,
21    /// The number of characters we have looked ahead.
22    ///
23    /// This tracks how many characters the parser asked us to look ahead for so we can return the
24    /// correct value in [`Self::buflen`].
25    lookahead: usize,
26}
27
28impl<'a> StrInput<'a> {
29    /// Create a new [`StrInput`] over the given string slice.
30    #[must_use]
31    pub fn new(input: &'a str) -> Self {
32        Self {
33            original: input,
34            buffer: input,
35            lookahead: 0,
36        }
37    }
38
39    /// Return the number of bytes consumed from the original input.
40    ///
41    /// This is an O(1) operation derived from the invariant that [`Self::buffer`] is always a
42    /// suffix of [`Self::original`].
43    #[inline]
44    #[must_use]
45    fn consumed_bytes(&self) -> usize {
46        self.original.len() - self.buffer.len()
47    }
48
49    /// Find the end of the next plain-scalar chunk in the current input buffer.
50    ///
51    /// Returns its byte length and character count. This keeps the existing batched scanner in one
52    /// place so callers can either materialize the chunk or retain it as a borrowed source slice.
53    fn plain_scalar_chunk_len(&self, flow_level_gt_0: bool) -> (usize, usize) {
54        let bytes = self.buffer.as_bytes();
55        let mut byte_pos = 0;
56        let mut chars_consumed = 0;
57
58        while byte_pos < bytes.len() {
59            let byte = bytes[byte_pos];
60            if byte < 0x80 {
61                let character = byte as char;
62                if crate::char_traits::is_blank_or_breakz(character)
63                    || flow_level_gt_0 && crate::char_traits::is_flow(character)
64                {
65                    break;
66                }
67                if character == ':' {
68                    let next_byte = bytes.get(byte_pos + 1).copied().unwrap_or(0);
69                    // A non-ASCII character cannot be blank, breakz, or a flow indicator.
70                    let stops_scalar = next_byte < 0x80
71                        && (crate::char_traits::is_blank_or_breakz(next_byte as char)
72                            || flow_level_gt_0 && crate::char_traits::is_flow(next_byte as char));
73                    if stops_scalar {
74                        break;
75                    }
76                }
77                byte_pos += 1;
78            } else {
79                let character = self.buffer[byte_pos..].chars().next().unwrap();
80                byte_pos += character.len_utf8();
81            }
82            chars_consumed += 1;
83        }
84
85        (byte_pos, chars_consumed)
86    }
87}
88
89impl Input for StrInput<'_> {
90    #[inline]
91    fn lookahead(&mut self, x: usize) {
92        // We already have all characters that we need.
93        // We cannot add '\0's to the buffer when we reach EOF.
94        // Character-retrieving functions return '\0' when they read past EOF.
95        self.lookahead = self.lookahead.max(x);
96    }
97
98    #[inline]
99    fn buflen(&self) -> usize {
100        self.lookahead
101    }
102
103    #[inline]
104    fn bufmaxlen(&self) -> usize {
105        BUFFER_LEN
106    }
107
108    #[inline]
109    fn raw_read_ch(&mut self) -> char {
110        let mut chars = self.buffer.chars();
111        if let Some(c) = chars.next() {
112            self.buffer = chars.as_str();
113            c
114        } else {
115            '\0'
116        }
117    }
118
119    #[inline]
120    fn raw_read_non_breakz_ch(&mut self) -> Option<char> {
121        if let Some((c, sub_str)) = split_first_char(self.buffer) {
122            if is_breakz(c) {
123                None
124            } else {
125                self.buffer = sub_str;
126                Some(c)
127            }
128        } else {
129            None
130        }
131    }
132
133    #[inline]
134    fn skip(&mut self) {
135        if !self.buffer.is_empty() {
136            let b = self.buffer.as_bytes()[0];
137            if b < 0x80 {
138                self.buffer = &self.buffer[1..];
139            } else {
140                let mut chars = self.buffer.chars();
141                chars.next();
142                self.buffer = chars.as_str();
143            }
144        }
145    }
146
147    #[inline]
148    fn skip_n(&mut self, count: usize) {
149        let mut chars = self.buffer.chars();
150        for _ in 0..count {
151            if chars.next().is_none() {
152                break;
153            }
154        }
155        self.buffer = chars.as_str();
156    }
157
158    #[inline]
159    fn peek(&self) -> char {
160        if self.buffer.is_empty() {
161            return '\0';
162        }
163        let b = self.buffer.as_bytes()[0];
164        if b < 0x80 {
165            b as char
166        } else {
167            self.buffer.chars().next().unwrap()
168        }
169    }
170
171    #[inline]
172    fn peek_nth(&self, n: usize) -> char {
173        if n == 0 {
174            return self.peek();
175        }
176        let bytes = self.buffer.as_bytes();
177        if n == 1 && bytes.len() >= 2 && bytes[0] < 0x80 && bytes[1] < 0x80 {
178            return bytes[1] as char;
179        }
180        let mut chars = self.buffer.chars();
181        for _ in 0..n {
182            if chars.next().is_none() {
183                return '\0';
184            }
185        }
186        chars.next().unwrap_or('\0')
187    }
188
189    #[inline]
190    fn byte_offset(&self) -> Option<usize> {
191        Some(self.consumed_bytes())
192    }
193
194    #[inline]
195    fn slice_bytes(&self, start: usize, end: usize) -> Option<&str> {
196        debug_assert!(start <= end);
197        debug_assert!(end <= self.original.len());
198        self.original.get(start..end)
199    }
200
201    #[inline]
202    fn may_contain_comments(&self) -> bool {
203        self.original.as_bytes().contains(&b'#')
204    }
205
206    #[inline]
207    fn next_2_are(&self, c1: char, c2: char) -> bool {
208        let mut chars = self.buffer.chars();
209        chars.next() == Some(c1) && chars.next() == Some(c2)
210    }
211
212    #[inline]
213    fn next_3_are(&self, c1: char, c2: char, c3: char) -> bool {
214        let mut chars = self.buffer.chars();
215        chars.next() == Some(c1) && chars.next() == Some(c2) && chars.next() == Some(c3)
216    }
217
218    #[inline]
219    fn next_is_document_indicator(&self) -> bool {
220        if self.buffer.len() < 3 {
221            false
222        } else {
223            // Since all characters we look for are ASCII, we can directly use the byte API of str.
224            let bytes = self.buffer.as_bytes();
225            (bytes.len() == 3 || matches!(bytes[3], b' ' | b'\t' | 0 | b'\n' | b'\r'))
226                && (bytes[0] == b'.' || bytes[0] == b'-')
227                && bytes[0] == bytes[1]
228                && bytes[1] == bytes[2]
229        }
230    }
231
232    #[inline]
233    fn next_is_document_start(&self) -> bool {
234        if self.buffer.len() < 3 {
235            false
236        } else {
237            // Since all characters we look for are ASCII, we can directly use the byte API of str.
238            let bytes = self.buffer.as_bytes();
239            (bytes.len() == 3 || matches!(bytes[3], b' ' | b'\t' | 0 | b'\n' | b'\r'))
240                && bytes[0] == b'-'
241                && bytes[1] == b'-'
242                && bytes[2] == b'-'
243        }
244    }
245
246    #[inline]
247    fn next_is_document_end(&self) -> bool {
248        if self.buffer.len() < 3 {
249            false
250        } else {
251            // Since all characters we look for are ASCII, we can directly use the byte API of str.
252            let bytes = self.buffer.as_bytes();
253            (bytes.len() == 3 || matches!(bytes[3], b' ' | b'\t' | 0 | b'\n' | b'\r'))
254                && bytes[0] == b'.'
255                && bytes[1] == b'.'
256                && bytes[2] == b'.'
257        }
258    }
259
260    fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, ErrorKind>) {
261        assert!(!matches!(skip_tabs, SkipTabs::Result(..)));
262
263        let mut new_str = self.buffer;
264        let mut has_yaml_ws = false;
265        let mut encountered_tab = false;
266
267        // Separate loops keep the fast space-only path while still tracking whether tabs were seen.
268        if skip_tabs == SkipTabs::Yes {
269            loop {
270                if let Some(sub_str) = new_str.strip_prefix(' ') {
271                    has_yaml_ws = true;
272                    new_str = sub_str;
273                } else if let Some(sub_str) = new_str.strip_prefix('\t') {
274                    encountered_tab = true;
275                    new_str = sub_str;
276                } else {
277                    break;
278                }
279            }
280        } else {
281            while let Some(sub_str) = new_str.strip_prefix(' ') {
282                has_yaml_ws = true;
283                new_str = sub_str;
284            }
285        }
286
287        // All characters consumed were ASCII. We can use the byte length difference to count the
288        // number of whitespace ignored.
289        let mut chars_consumed = self.buffer.len() - new_str.len();
290
291        if !new_str.is_empty() && new_str.as_bytes()[0] == b'#' {
292            if !encountered_tab && !has_yaml_ws {
293                return (chars_consumed, Err(ErrorKind::CommentNotSeparated));
294            }
295
296            // Skip remaining characters until we hit a breakz.
297            while let Some((c, sub_str)) = split_first_char(new_str) {
298                if is_breakz(c) {
299                    break;
300                }
301                new_str = sub_str;
302                chars_consumed += 1;
303            }
304        }
305
306        self.buffer = new_str;
307
308        (
309            chars_consumed,
310            Ok(SkipTabs::Result(encountered_tab, has_yaml_ws)),
311        )
312    }
313
314    fn skip_ws_to_eol_blanks(&mut self, skip_tabs: SkipTabs) -> (usize, SkipTabs) {
315        assert!(!matches!(skip_tabs, SkipTabs::Result(..)));
316
317        let bytes = self.buffer.as_bytes();
318        let mut i = 0;
319        let mut encountered_tab = false;
320        let mut has_yaml_ws = false;
321
322        if skip_tabs == SkipTabs::Yes {
323            while i < bytes.len() {
324                match bytes[i] {
325                    b' ' => {
326                        has_yaml_ws = true;
327                        i += 1;
328                    }
329                    b'\t' => {
330                        encountered_tab = true;
331                        i += 1;
332                    }
333                    _ => break,
334                }
335            }
336        } else {
337            while i < bytes.len() && bytes[i] == b' ' {
338                has_yaml_ws = true;
339                i += 1;
340            }
341        }
342
343        self.buffer = &self.buffer[i..];
344
345        (i, SkipTabs::Result(encountered_tab, has_yaml_ws))
346    }
347
348    #[inline]
349    fn next_is_blank_or_break(&self) -> bool {
350        !self.buffer.is_empty() && matches!(self.buffer.as_bytes()[0], b' ' | b'\t' | b'\n' | b'\r')
351    }
352
353    #[inline]
354    fn next_is_blank_or_breakz(&self) -> bool {
355        self.buffer.is_empty()
356            || matches!(self.buffer.as_bytes()[0], b' ' | b'\t' | 0 | b'\n' | b'\r')
357    }
358
359    #[inline]
360    fn next_is_blank(&self) -> bool {
361        !self.buffer.is_empty() && matches!(self.buffer.as_bytes()[0], b' ' | b'\t')
362    }
363
364    #[inline]
365    fn next_is_break(&self) -> bool {
366        !self.buffer.is_empty() && matches!(self.buffer.as_bytes()[0], b'\n' | b'\r')
367    }
368
369    #[inline]
370    fn next_is_breakz(&self) -> bool {
371        self.buffer.is_empty() || matches!(self.buffer.as_bytes()[0], 0 | b'\n' | b'\r')
372    }
373
374    #[inline]
375    fn next_is_z(&self) -> bool {
376        self.buffer.is_empty()
377    }
378
379    #[inline]
380    fn next_is_flow(&self) -> bool {
381        !self.buffer.is_empty()
382            && matches!(self.buffer.as_bytes()[0], b',' | b'[' | b']' | b'{' | b'}')
383    }
384
385    #[inline]
386    fn next_is_digit(&self) -> bool {
387        !self.buffer.is_empty() && self.buffer.as_bytes()[0].is_ascii_digit()
388    }
389
390    /// Check if the next character is an ASCII alphanumeric, `_`, or `-`.
391    ///
392    /// This is used as a heuristic for error detection (e.g., when `:` is followed
393    /// by tab and then a potential value character). The ASCII-only check is intentional:
394    /// it catches common cases like `key:\tvalue` while avoiding false positives for
395    /// valid YAML constructs. Unicode value starters (e.g., `äöü`) are not detected,
396    /// but such cases will still fail to parse (with a less specific error message).
397    #[inline]
398    fn next_is_alpha(&self) -> bool {
399        !self.buffer.is_empty()
400            && matches!(self.buffer.as_bytes()[0], b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'-')
401    }
402
403    fn skip_while_non_breakz(&mut self) -> usize {
404        let mut byte_pos = 0;
405        let mut chars_consumed = 0;
406
407        for (i, c) in self.buffer.char_indices() {
408            if is_breakz(c) || !crate::char_traits::is_printable(c) {
409                break;
410            }
411            byte_pos = i + c.len_utf8();
412            chars_consumed += 1;
413        }
414
415        self.buffer = &self.buffer[byte_pos..];
416        chars_consumed
417    }
418
419    #[inline]
420    fn skip_while_blank(&mut self) -> usize {
421        let bytes = self.buffer.as_bytes();
422
423        let mut i = 0;
424        while i < bytes.len() {
425            match bytes[i] {
426                b' ' | b'\t' => i += 1,
427                _ => break,
428            }
429        }
430
431        self.buffer = &self.buffer[i..];
432        i
433    }
434
435    /// Fetch characters matching `is_alpha` (ASCII alphanumeric, `_`, `-`).
436    ///
437    /// This is used for scanning tag handles (e.g., `!foo!`). Per YAML 1.2 spec,
438    /// tag handles use `ns-word-char` which is `[0-9a-zA-Z-]`. Our implementation
439    /// is slightly more permissive by also accepting `_`, but this is harmless
440    /// and matches common practice. Unicode characters like `ä` or `π` are NOT
441    /// valid in tag handles per spec, so the ASCII-only byte-based scanning here
442    /// is both correct and efficient.
443    fn fetch_while_is_alpha(&mut self, out: &mut String) -> usize {
444        let bytes = self.buffer.as_bytes();
445        let mut i = 0;
446
447        // All target characters are ASCII, so we can scan bytes directly.
448        while i < bytes.len() {
449            match bytes[i] {
450                b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'-' => i += 1,
451                _ => break,
452            }
453        }
454
455        // All matched characters are ASCII, so we can safely slice and convert.
456        out.push_str(&self.buffer[..i]);
457        self.buffer = &self.buffer[i..];
458
459        i
460    }
461
462    fn fetch_while_is_yaml_non_space(&mut self, out: &mut String) -> usize {
463        let mut byte_pos = 0;
464        let mut chars_consumed = 0;
465
466        for (i, c) in self.buffer.char_indices() {
467            if !crate::char_traits::is_yaml_non_space(c) || crate::char_traits::is_z(c) {
468                break;
469            }
470
471            byte_pos = i + c.len_utf8();
472            chars_consumed += 1;
473        }
474
475        out.push_str(&self.buffer[..byte_pos]);
476        self.buffer = &self.buffer[byte_pos..];
477
478        chars_consumed
479    }
480
481    fn fetch_plain_scalar_chunk(
482        &mut self,
483        out: &mut String,
484        _count: usize,
485        flow_level_gt_0: bool,
486    ) -> (bool, usize) {
487        let (byte_pos, chars_consumed) = self.plain_scalar_chunk_len(flow_level_gt_0);
488        out.push_str(&self.buffer[..byte_pos]);
489        self.buffer = &self.buffer[byte_pos..];
490        (true, chars_consumed)
491    }
492
493    fn skip_plain_scalar_chunk(&mut self, _count: usize, flow_level_gt_0: bool) -> (bool, usize) {
494        let (byte_pos, chars_consumed) = self.plain_scalar_chunk_len(flow_level_gt_0);
495        self.buffer = &self.buffer[byte_pos..];
496        (true, chars_consumed)
497    }
498}
499
500impl<'a> BorrowedInput<'a> for StrInput<'a> {
501    #[inline]
502    fn slice_borrowed(&self, start: usize, end: usize) -> Option<&'a str> {
503        debug_assert!(start <= end);
504        debug_assert!(end <= self.original.len());
505        self.original.get(start..end)
506    }
507}
508
509/// The buffer size we return to the scanner.
510///
511/// This does not correspond to any allocated buffer size. In practice, the scanner may request any
512/// character in the virtual buffer: characters inside the input are returned as-is, and positions
513/// past EOF return `\0`.
514///
515/// The number of characters we are asked to retrieve in [`lookahead`] depends on the buffer size
516/// of the input. Our buffer here is virtually unlimited, but the scanner cannot work with that. It
517/// may allocate buffers of its own of the size we return in [`bufmaxlen`] (so we can't return
518/// [`usize::MAX`]). We can't always return the number of characters left either, as the scanner
519/// expects [`buflen`] to return the same value that was given to [`lookahead`] right after its
520/// call.
521///
522/// This creates a complex situation where [`bufmaxlen`] influences what value [`lookahead`] is
523/// called with, which in turn dictates what [`buflen`] returns. In order to avoid breaking any
524/// function, we return this constant in [`bufmaxlen`] which, since the input is processed one line
525/// at a time, should fit what we expect to be a good balance between memory consumption and what
526/// we expect the maximum line length to be.
527///
528/// [`lookahead`]: `StrInput::lookahead`
529/// [`bufmaxlen`]: `StrInput::bufmaxlen`
530/// [`buflen`]: `StrInput::buflen`
531const BUFFER_LEN: usize = 128;
532
533/// Splits the first character of the given string and returns it along with the rest of the
534/// string.
535#[inline]
536fn split_first_char(s: &str) -> Option<(char, &str)> {
537    let mut chars = s.chars();
538    let c = chars.next()?;
539    Some((c, chars.as_str()))
540}
541
542#[cfg(test)]
543mod test {
544    use alloc::string::String;
545
546    use crate::{
547        error::ErrorKind,
548        input::{BorrowedInput, Input, SkipTabs},
549    };
550
551    use super::StrInput;
552
553    #[test]
554    pub fn is_document_start() {
555        let input = StrInput::new("---\n");
556        assert!(input.next_is_document_start());
557        assert!(input.next_is_document_indicator());
558        let input = StrInput::new("---");
559        assert!(input.next_is_document_start());
560        assert!(input.next_is_document_indicator());
561        let input = StrInput::new("...\n");
562        assert!(!input.next_is_document_start());
563        assert!(input.next_is_document_indicator());
564        let input = StrInput::new("--- ");
565        assert!(input.next_is_document_start());
566        assert!(input.next_is_document_indicator());
567    }
568
569    #[test]
570    pub fn is_document_end() {
571        let input = StrInput::new("...\n");
572        assert!(input.next_is_document_end());
573        assert!(input.next_is_document_indicator());
574        let input = StrInput::new("...");
575        assert!(input.next_is_document_end());
576        assert!(input.next_is_document_indicator());
577        let input = StrInput::new("---\n");
578        assert!(!input.next_is_document_end());
579        assert!(input.next_is_document_indicator());
580        let input = StrInput::new("... ");
581        assert!(input.next_is_document_end());
582        assert!(input.next_is_document_indicator());
583    }
584
585    #[test]
586    fn raw_reads_track_byte_offsets_and_eof() {
587        let mut input = StrInput::new("aé");
588
589        assert_eq!(input.raw_read_ch(), 'a');
590        assert_eq!(input.byte_offset(), Some(1));
591        assert_eq!(input.raw_read_ch(), 'é');
592        assert_eq!(input.byte_offset(), Some(3));
593        assert_eq!(input.raw_read_ch(), '\0');
594        assert_eq!(input.byte_offset(), Some(3));
595    }
596
597    #[test]
598    fn raw_read_non_breakz_stops_before_breakz() {
599        let mut input = StrInput::new("a\n");
600
601        assert_eq!(input.raw_read_non_breakz_ch(), Some('a'));
602        assert_eq!(input.raw_read_non_breakz_ch(), None);
603        assert_eq!(input.peek(), '\n');
604
605        let mut empty = StrInput::new("");
606        assert_eq!(empty.raw_read_non_breakz_ch(), None);
607    }
608
609    #[test]
610    fn skip_handles_ascii_unicode_and_eof() {
611        let mut input = StrInput::new("éab");
612
613        input.skip();
614        assert_eq!(input.peek(), 'a');
615
616        input.skip_n(8);
617        assert_eq!(input.peek(), '\0');
618
619        input.skip();
620        assert_eq!(input.peek(), '\0');
621    }
622
623    #[test]
624    fn peeking_past_end_returns_nul() {
625        let ascii = StrInput::new("ab");
626        assert_eq!(ascii.peek_nth(1), 'b');
627        assert_eq!(ascii.peek_nth(3), '\0');
628
629        let unicode = StrInput::new("éab");
630        assert!(unicode.next_3_are('é', 'a', 'b'));
631        assert!(!unicode.next_3_are('é', 'a', 'c'));
632    }
633
634    #[test]
635    fn skip_ws_to_eol_without_tabs_stops_before_tab() {
636        let mut input = StrInput::new("  \t# comment\n");
637
638        let (consumed, result) = input.skip_ws_to_eol(SkipTabs::No);
639
640        assert_eq!(consumed, 2);
641        let result = result.unwrap();
642        assert!(!result.found_tabs());
643        assert!(result.has_valid_yaml_ws());
644        assert_eq!(input.peek(), '\t');
645    }
646
647    #[test]
648    fn skip_ws_to_eol_skips_comments_after_whitespace() {
649        let mut input = StrInput::new("  # comment\nnext");
650
651        let (consumed, result) = input.skip_ws_to_eol(SkipTabs::Yes);
652
653        assert_eq!(consumed, 11);
654        let result = result.unwrap();
655        assert!(!result.found_tabs());
656        assert!(result.has_valid_yaml_ws());
657        assert_eq!(input.peek(), '\n');
658    }
659
660    #[test]
661    fn skip_ws_to_eol_rejects_unseparated_comment() {
662        let mut input = StrInput::new("# comment\n");
663
664        let (consumed, result) = input.skip_ws_to_eol(SkipTabs::Yes);
665
666        assert_eq!(consumed, 0);
667        assert_eq!(result.err(), Some(ErrorKind::CommentNotSeparated));
668        assert_eq!(input.peek(), '#');
669    }
670
671    #[test]
672    fn fetch_while_is_alpha_is_ascii_only() {
673        let mut input = StrInput::new("abc_123-é");
674        let mut out = String::new();
675
676        assert_eq!(input.fetch_while_is_alpha(&mut out), 8);
677        assert_eq!(out, "abc_123-");
678        assert_eq!(input.peek(), 'é');
679    }
680
681    #[test]
682    fn fetch_plain_scalar_chunk_handles_non_ascii_after_colon() {
683        let mut input = StrInput::new("a:é ");
684        let mut out = String::new();
685
686        assert_eq!(
687            input.fetch_plain_scalar_chunk(&mut out, 16, false),
688            (true, 3)
689        );
690        assert_eq!(out, "a:é");
691        assert_eq!(input.peek(), ' ');
692    }
693
694    #[test]
695    fn fetch_plain_scalar_chunk_stops_at_flow_indicator() {
696        let mut input = StrInput::new("abc,def");
697        let mut out = String::new();
698
699        assert_eq!(
700            input.fetch_plain_scalar_chunk(&mut out, 16, true),
701            (true, 3)
702        );
703        assert_eq!(out, "abc");
704        assert_eq!(input.peek(), ',');
705    }
706
707    #[test]
708    fn borrowed_slices_use_original_input_lifetime() {
709        let input = StrInput::new("aéz");
710
711        assert_eq!(BorrowedInput::slice_borrowed(&input, 1, 3), Some("é"));
712        assert_eq!(input.slice_bytes(3, 4), Some("z"));
713    }
714}