1use crate::{
2 char_traits::is_breakz,
3 error::ErrorKind,
4 input::{BorrowedInput, Input, SkipTabs, WhitespaceResult},
5};
6use alloc::string::String;
7
8#[allow(clippy::module_name_repetitions)]
10pub struct StrInput<'a> {
11 original: &'a str,
16 buffer: &'a str,
21 lookahead: usize,
26}
27
28impl<'a> StrInput<'a> {
29 #[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 #[inline]
44 #[must_use]
45 fn consumed_bytes(&self) -> usize {
46 self.original.len() - self.buffer.len()
47 }
48
49 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 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 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 #[cfg(feature = "comments")]
202 #[inline]
203 fn may_contain_comments(&self) -> bool {
204 self.original.as_bytes().contains(&b'#')
205 }
206
207 #[inline]
208 fn next_2_are(&self, c1: char, c2: char) -> bool {
209 let mut chars = self.buffer.chars();
210 chars.next() == Some(c1) && chars.next() == Some(c2)
211 }
212
213 #[inline]
214 fn next_3_are(&self, c1: char, c2: char, c3: char) -> bool {
215 let mut chars = self.buffer.chars();
216 chars.next() == Some(c1) && chars.next() == Some(c2) && chars.next() == Some(c3)
217 }
218
219 #[inline]
220 fn next_is_document_indicator(&self) -> bool {
221 if self.buffer.len() < 3 {
222 false
223 } else {
224 let bytes = self.buffer.as_bytes();
226 (bytes.len() == 3 || matches!(bytes[3], b' ' | b'\t' | 0 | b'\n' | b'\r'))
227 && (bytes[0] == b'.' || bytes[0] == b'-')
228 && bytes[0] == bytes[1]
229 && bytes[1] == bytes[2]
230 }
231 }
232
233 #[inline]
234 fn next_is_document_start(&self) -> bool {
235 if self.buffer.len() < 3 {
236 false
237 } else {
238 let bytes = self.buffer.as_bytes();
240 (bytes.len() == 3 || matches!(bytes[3], b' ' | b'\t' | 0 | b'\n' | b'\r'))
241 && bytes[0] == b'-'
242 && bytes[1] == b'-'
243 && bytes[2] == b'-'
244 }
245 }
246
247 #[inline]
248 fn next_is_document_end(&self) -> bool {
249 if self.buffer.len() < 3 {
250 false
251 } else {
252 let bytes = self.buffer.as_bytes();
254 (bytes.len() == 3 || matches!(bytes[3], b' ' | b'\t' | 0 | b'\n' | b'\r'))
255 && bytes[0] == b'.'
256 && bytes[1] == b'.'
257 && bytes[2] == b'.'
258 }
259 }
260
261 fn skip_ws_to_eol(
262 &mut self,
263 skip_tabs: SkipTabs,
264 ) -> (usize, Result<WhitespaceResult, ErrorKind>) {
265 let mut new_str = self.buffer;
266 let mut has_yaml_ws = false;
267 let mut encountered_tab = false;
268
269 if skip_tabs == SkipTabs::Yes {
271 loop {
272 if let Some(sub_str) = new_str.strip_prefix(' ') {
273 has_yaml_ws = true;
274 new_str = sub_str;
275 } else if let Some(sub_str) = new_str.strip_prefix('\t') {
276 encountered_tab = true;
277 new_str = sub_str;
278 } else {
279 break;
280 }
281 }
282 } else {
283 while let Some(sub_str) = new_str.strip_prefix(' ') {
284 has_yaml_ws = true;
285 new_str = sub_str;
286 }
287 }
288
289 let mut chars_consumed = self.buffer.len() - new_str.len();
292
293 if !new_str.is_empty() && new_str.as_bytes()[0] == b'#' {
294 if !encountered_tab && !has_yaml_ws {
295 return (chars_consumed, Err(ErrorKind::CommentNotSeparated));
296 }
297
298 while let Some((c, sub_str)) = split_first_char(new_str) {
300 if is_breakz(c) {
301 break;
302 }
303 new_str = sub_str;
304 chars_consumed += 1;
305 }
306 }
307
308 self.buffer = new_str;
309
310 (
311 chars_consumed,
312 Ok(WhitespaceResult::new(encountered_tab, has_yaml_ws)),
313 )
314 }
315
316 fn skip_ws_to_eol_blanks(&mut self, skip_tabs: SkipTabs) -> (usize, WhitespaceResult) {
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, WhitespaceResult::new(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 #[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 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 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 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 #[inline]
482 fn fetch_block_scalar_line(&mut self, out: &mut String) -> usize {
483 let end = self
484 .buffer
485 .as_bytes()
486 .iter()
487 .position(|&byte| matches!(byte, b'\r' | b'\n' | 0))
488 .unwrap_or(self.buffer.len());
489
490 let (line, remaining) = self.buffer.split_at(end);
492 out.push_str(line);
493 self.buffer = remaining;
494 line.chars().count()
495 }
496
497 #[inline]
498 fn take_quoted_scalar_ascii_chunk(&mut self, single: bool) -> &str {
499 let quote = if single { b'\'' } else { b'"' };
500 let end = self
501 .buffer
502 .as_bytes()
503 .iter()
504 .position(|&byte| {
505 !(b'!'..=b'~').contains(&byte) || byte == quote || (!single && byte == b'\\')
506 })
507 .unwrap_or(self.buffer.len());
508
509 let (chunk, remaining) = self.buffer.split_at(end);
511 self.buffer = remaining;
512 chunk
513 }
514
515 #[inline]
516 fn take_quoted_scalar_chunk(&mut self, single: bool) -> (&str, usize) {
517 if self.buffer.as_bytes().first().map_or(true, u8::is_ascii) {
520 let chunk = self.take_quoted_scalar_ascii_chunk(single);
521 return (chunk, chunk.len());
522 }
523
524 let quote = if single { b'\'' } else { b'"' };
525 let bytes = self.buffer.as_bytes();
526 let mut end = 0;
527 let mut count = 0;
528 while end < bytes.len() {
529 let byte = bytes[end];
530 if byte.is_ascii() {
531 if !(b'!'..=b'~').contains(&byte) || byte == quote || (!single && byte == b'\\') {
533 break;
534 }
535 end += 1;
536 } else {
537 let character = self.buffer[end..].chars().next().unwrap();
540 if !crate::char_traits::is_printable(character) {
541 break;
542 }
543 end += character.len_utf8();
544 }
545 count += 1;
546 }
547 let (chunk, remaining) = self.buffer.split_at(end);
548 self.buffer = remaining;
549 (chunk, count)
550 }
551
552 fn fetch_plain_scalar_chunk(
553 &mut self,
554 out: &mut String,
555 _count: usize,
556 flow_level_gt_0: bool,
557 ) -> (bool, usize) {
558 let (byte_pos, chars_consumed) = self.plain_scalar_chunk_len(flow_level_gt_0);
559 out.push_str(&self.buffer[..byte_pos]);
560 self.buffer = &self.buffer[byte_pos..];
561 (true, chars_consumed)
562 }
563
564 fn skip_plain_scalar_chunk(&mut self, _count: usize, flow_level_gt_0: bool) -> (bool, usize) {
565 let (byte_pos, chars_consumed) = self.plain_scalar_chunk_len(flow_level_gt_0);
566 self.buffer = &self.buffer[byte_pos..];
567 (true, chars_consumed)
568 }
569}
570
571impl<'a> BorrowedInput<'a> for StrInput<'a> {
572 #[inline]
573 fn slice_borrowed(&self, start: usize, end: usize) -> Option<&'a str> {
574 debug_assert!(start <= end);
575 debug_assert!(end <= self.original.len());
576 self.original.get(start..end)
577 }
578}
579
580const BUFFER_LEN: usize = 128;
603
604#[inline]
607fn split_first_char(s: &str) -> Option<(char, &str)> {
608 let mut chars = s.chars();
609 let c = chars.next()?;
610 Some((c, chars.as_str()))
611}
612
613#[cfg(test)]
614mod test {
615 use alloc::string::String;
616
617 use crate::{
618 error::ErrorKind,
619 input::{BorrowedInput, Input, SkipTabs, WhitespaceResult},
620 };
621
622 use super::StrInput;
623
624 #[test]
625 pub fn is_document_start() {
626 let input = StrInput::new("---\n");
627 assert!(input.next_is_document_start());
628 assert!(input.next_is_document_indicator());
629 let input = StrInput::new("---");
630 assert!(input.next_is_document_start());
631 assert!(input.next_is_document_indicator());
632 let input = StrInput::new("...\n");
633 assert!(!input.next_is_document_start());
634 assert!(input.next_is_document_indicator());
635 let input = StrInput::new("--- ");
636 assert!(input.next_is_document_start());
637 assert!(input.next_is_document_indicator());
638 }
639
640 #[test]
641 pub fn is_document_end() {
642 let input = StrInput::new("...\n");
643 assert!(input.next_is_document_end());
644 assert!(input.next_is_document_indicator());
645 let input = StrInput::new("...");
646 assert!(input.next_is_document_end());
647 assert!(input.next_is_document_indicator());
648 let input = StrInput::new("---\n");
649 assert!(!input.next_is_document_end());
650 assert!(input.next_is_document_indicator());
651 let input = StrInput::new("... ");
652 assert!(input.next_is_document_end());
653 assert!(input.next_is_document_indicator());
654 }
655
656 #[test]
657 fn raw_reads_track_byte_offsets_and_eof() {
658 let mut input = StrInput::new("aé");
659
660 assert_eq!(input.raw_read_ch(), 'a');
661 assert_eq!(input.byte_offset(), Some(1));
662 assert_eq!(input.raw_read_ch(), 'é');
663 assert_eq!(input.byte_offset(), Some(3));
664 assert_eq!(input.raw_read_ch(), '\0');
665 assert_eq!(input.byte_offset(), Some(3));
666 }
667
668 #[test]
669 fn raw_read_non_breakz_stops_before_breakz() {
670 let mut input = StrInput::new("a\n");
671
672 assert_eq!(input.raw_read_non_breakz_ch(), Some('a'));
673 assert_eq!(input.raw_read_non_breakz_ch(), None);
674 assert_eq!(input.peek(), '\n');
675
676 let mut empty = StrInput::new("");
677 assert_eq!(empty.raw_read_non_breakz_ch(), None);
678 }
679
680 #[test]
681 fn skip_handles_ascii_unicode_and_eof() {
682 let mut input = StrInput::new("éab");
683
684 input.skip();
685 assert_eq!(input.peek(), 'a');
686
687 input.skip_n(8);
688 assert_eq!(input.peek(), '\0');
689
690 input.skip();
691 assert_eq!(input.peek(), '\0');
692 }
693
694 #[test]
695 fn peeking_past_end_returns_nul() {
696 let ascii = StrInput::new("ab");
697 assert_eq!(ascii.peek_nth(1), 'b');
698 assert_eq!(ascii.peek_nth(3), '\0');
699
700 let unicode = StrInput::new("éab");
701 assert!(unicode.next_3_are('é', 'a', 'b'));
702 assert!(!unicode.next_3_are('é', 'a', 'c'));
703 }
704
705 #[test]
706 fn skip_ws_to_eol_without_tabs_stops_before_tab() {
707 let mut input = StrInput::new(" \t# comment\n");
708
709 let (consumed, result) = input.skip_ws_to_eol(SkipTabs::No);
710
711 assert_eq!(consumed, 2);
712 let result = result.unwrap();
713 assert!(!result.found_tabs());
714 assert!(result.has_valid_yaml_ws());
715 assert_eq!(input.peek(), '\t');
716 }
717
718 #[test]
719 fn skip_ws_to_eol_skips_comments_after_whitespace() {
720 let mut input = StrInput::new(" # comment\nnext");
721
722 let (consumed, result) = input.skip_ws_to_eol(SkipTabs::Yes);
723
724 assert_eq!(consumed, 11);
725 let result = result.unwrap();
726 assert!(!result.found_tabs());
727 assert!(result.has_valid_yaml_ws());
728 assert_eq!(input.peek(), '\n');
729 }
730
731 #[test]
732 fn skip_ws_to_eol_rejects_unseparated_comment() {
733 for policy in [SkipTabs::Yes, SkipTabs::No] {
734 let mut input = StrInput::new("#é中\n");
735
736 let (consumed, result) = input.skip_ws_to_eol(policy);
737
738 assert_eq!(consumed, 0);
739 assert_eq!(result, Err(ErrorKind::CommentNotSeparated));
740 assert_eq!(input.byte_offset(), Some(0));
741 assert_eq!(input.peek(), '#');
742 }
743 }
744
745 #[test]
746 fn skip_ws_to_eol_counts_unicode_comments_and_tracks_whitespace_outside_them() {
747 for (source, consumed, expected, byte_offset, next) in [
748 ("", 0, WhitespaceResult::new(false, false), 0, '\0'),
749 (" \t#é中\r\n", 5, WhitespaceResult::new(true, true), 8, '\r'),
750 ("\t# é 中\n", 6, WhitespaceResult::new(true, false), 9, '\n'),
751 (" #é中", 4, WhitespaceResult::new(false, true), 7, '\0'),
752 ] {
753 let mut input = StrInput::new(source);
754 assert_eq!(
755 input.skip_ws_to_eol(SkipTabs::Yes),
756 (consumed, Ok(expected))
757 );
758 assert_eq!(input.byte_offset(), Some(byte_offset));
759 assert_eq!(input.peek(), next);
760 }
761 }
762
763 #[test]
764 fn skip_ws_to_eol_blanks_preserves_comments_and_unskipped_tabs() {
765 for (source, policy, consumed, found_tabs, found_spaces, next) in [
766 ("", SkipTabs::Yes, 0, false, false, '\0'),
767 (" #é中\n", SkipTabs::Yes, 2, false, true, '#'),
768 ("\t#é中\n", SkipTabs::Yes, 1, true, false, '#'),
769 (" \t#é中\n", SkipTabs::Yes, 2, true, true, '#'),
770 (" \t#é中\n", SkipTabs::No, 1, false, true, '\t'),
771 ("\t#é中\n", SkipTabs::No, 0, false, false, '\t'),
772 ] {
773 let mut input = StrInput::new(source);
774 assert_eq!(
775 input.skip_ws_to_eol_blanks(policy),
776 (consumed, WhitespaceResult::new(found_tabs, found_spaces)),
777 "source: {source:?}",
778 );
779 assert_eq!(input.byte_offset(), Some(consumed));
780 assert_eq!(input.peek(), next, "source: {source:?}");
781 }
782 }
783
784 #[test]
785 fn fetch_while_is_alpha_is_ascii_only() {
786 let mut input = StrInput::new("abc_123-é");
787 let mut out = String::new();
788
789 assert_eq!(input.fetch_while_is_alpha(&mut out), 8);
790 assert_eq!(out, "abc_123-");
791 assert_eq!(input.peek(), 'é');
792 }
793
794 #[test]
795 fn fetch_plain_scalar_chunk_handles_non_ascii_after_colon() {
796 let mut input = StrInput::new("a:é ");
797 let mut out = String::new();
798
799 assert_eq!(
800 input.fetch_plain_scalar_chunk(&mut out, 16, false),
801 (true, 3)
802 );
803 assert_eq!(out, "a:é");
804 assert_eq!(input.peek(), ' ');
805 }
806
807 #[test]
808 fn fetch_plain_scalar_chunk_stops_at_flow_indicator() {
809 let mut input = StrInput::new("abc,def");
810 let mut out = String::new();
811
812 assert_eq!(
813 input.fetch_plain_scalar_chunk(&mut out, 16, true),
814 (true, 3)
815 );
816 assert_eq!(out, "abc");
817 assert_eq!(input.peek(), ',');
818 }
819
820 #[test]
821 fn borrowed_slices_use_original_input_lifetime() {
822 let input = StrInput::new("aéz");
823
824 assert_eq!(BorrowedInput::slice_borrowed(&input, 1, 3), Some("é"));
825 assert_eq!(input.slice_bytes(3, 4), Some("z"));
826 }
827}