1use crate::{
2 char_traits::is_breakz,
3 error::ErrorKind,
4 input::{BorrowedInput, Input, SkipTabs},
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 #[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 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 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 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 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 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 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 #[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 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
509const BUFFER_LEN: usize = 128;
532
533#[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}