Skip to main content

toml_parser/decoder/
scalar.rs

1use winnow::stream::ContainsToken as _;
2use winnow::stream::FindSlice as _;
3use winnow::stream::Offset as _;
4use winnow::stream::Stream as _;
5
6use crate::ErrorSink;
7use crate::Expected;
8use crate::ParseError;
9use crate::Raw;
10use crate::Span;
11use crate::decoder::StringBuilder;
12
13const ALLOCATION_ERROR: &str = "could not allocate for string";
14
15#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
16pub enum ScalarKind {
17    String,
18    Boolean(bool),
19    DateTime,
20    Float,
21    Integer(IntegerRadix),
22}
23
24impl ScalarKind {
25    pub fn description(&self) -> &'static str {
26        match self {
27            Self::String => "string",
28            Self::Boolean(_) => "boolean",
29            Self::DateTime => "date-time",
30            Self::Float => "float",
31            Self::Integer(radix) => radix.description(),
32        }
33    }
34
35    pub fn invalid_description(&self) -> &'static str {
36        match self {
37            Self::String => "invalid string",
38            Self::Boolean(_) => "invalid boolean",
39            Self::DateTime => "invalid date-time",
40            Self::Float => "invalid float",
41            Self::Integer(radix) => radix.invalid_description(),
42        }
43    }
44}
45
46#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
47pub enum IntegerRadix {
48    #[default]
49    Dec,
50    Hex,
51    Oct,
52    Bin,
53}
54
55impl IntegerRadix {
56    pub fn description(&self) -> &'static str {
57        match self {
58            Self::Dec => "integer",
59            Self::Hex => "hexadecimal",
60            Self::Oct => "octal",
61            Self::Bin => "binary",
62        }
63    }
64
65    pub fn value(&self) -> u32 {
66        match self {
67            Self::Dec => 10,
68            Self::Hex => 16,
69            Self::Oct => 8,
70            Self::Bin => 2,
71        }
72    }
73
74    pub fn invalid_description(&self) -> &'static str {
75        match self {
76            Self::Dec => "invalid integer number",
77            Self::Hex => "invalid hexadecimal number",
78            Self::Oct => "invalid octal number",
79            Self::Bin => "invalid binary number",
80        }
81    }
82
83    fn validator(&self) -> fn(char) -> bool {
84        match self {
85            Self::Dec => |c| c.is_ascii_digit(),
86            Self::Hex => |c| c.is_ascii_hexdigit(),
87            Self::Oct => |c| matches!(c, '0'..='7'),
88            Self::Bin => |c| matches!(c, '0'..='1'),
89        }
90    }
91}
92
93pub(crate) fn decode_unquoted_scalar<'i>(
94    raw: Raw<'i>,
95    output: &mut dyn StringBuilder<'i>,
96    error: &mut dyn ErrorSink,
97) -> ScalarKind {
98    let s = raw.as_str();
99    let Some(first) = s.as_bytes().first() else {
100        return decode_invalid(raw, output, error);
101    };
102    if !first.is_ascii_digit() && s.contains(" ") {
103        // Only datetimes can have a space
104        return decode_invalid(raw, output, error);
105    }
106    match first {
107        // number starts
108        b'+' | b'-' => {
109            let value = &raw.as_str()[1..];
110            decode_sign_prefix(raw, value, output, error)
111        }
112        // Report as if they were numbers because its most likely a typo
113        b'_' => decode_datetime_or_float_or_integer(raw.as_str(), raw, output, error),
114        // Date/number starts
115        b'0' => decode_zero_prefix(raw.as_str(), false, raw, output, error),
116        b'1'..=b'9' => decode_datetime_or_float_or_integer(raw.as_str(), raw, output, error),
117        // Report as if they were numbers because its most likely a typo
118        b'.' => {
119            let kind = ScalarKind::Float;
120            let stream = raw.as_str();
121            if ensure_float(stream, raw, error) {
122                decode_float_or_integer(stream, raw, kind, output, error)
123            } else {
124                kind
125            }
126        }
127        b't' | b'T' => {
128            const SYMBOL: &str = "true";
129            let kind = ScalarKind::Boolean(true);
130            let expected = &[Expected::Literal(SYMBOL)];
131            decode_symbol(raw, SYMBOL, kind, expected, output, error)
132        }
133        b'f' | b'F' => {
134            const SYMBOL: &str = "false";
135            let kind = ScalarKind::Boolean(false);
136            let expected = &[Expected::Literal(SYMBOL)];
137            decode_symbol(raw, SYMBOL, kind, expected, output, error)
138        }
139        b'i' | b'I' => {
140            const SYMBOL: &str = "inf";
141            let kind = ScalarKind::Float;
142            let expected = &[Expected::Literal(SYMBOL)];
143            decode_symbol(raw, SYMBOL, kind, expected, output, error)
144        }
145        b'n' | b'N' => {
146            const SYMBOL: &str = "nan";
147            let kind = ScalarKind::Float;
148            let expected = &[Expected::Literal(SYMBOL)];
149            decode_symbol(raw, SYMBOL, kind, expected, output, error)
150        }
151        _ => decode_invalid(raw, output, error),
152    }
153}
154
155fn decode_sign_prefix<'i>(
156    raw: Raw<'i>,
157    value: &'i str,
158    output: &mut dyn StringBuilder<'i>,
159    error: &mut dyn ErrorSink,
160) -> ScalarKind {
161    let mut value = value;
162    let first = loop {
163        let Some(first) = value.as_bytes().first() else {
164            return decode_invalid(raw, output, error);
165        };
166        if !matches!(first, b'+' | b'-') {
167            break first;
168        }
169        let start = value.offset_from(&raw.as_str());
170        let end = start + 1;
171        error.report_error(
172            ParseError::new("redundant numeric sign")
173                .with_context(Span::new_unchecked(0, raw.len()))
174                .with_expected(&[])
175                .with_unexpected(Span::new_unchecked(start, end)),
176        );
177        value = &value[1..];
178    };
179    match first {
180        // Report as if they were numbers because its most likely a typo
181        b'_' => decode_datetime_or_float_or_integer(value, raw, output, error),
182        // Date/number starts
183        b'0' => decode_zero_prefix(value, true, raw, output, error),
184        b'1'..=b'9' => decode_datetime_or_float_or_integer(value, raw, output, error),
185        // Report as if they were numbers because its most likely a typo
186        b'.' => {
187            let kind = ScalarKind::Float;
188            let stream = raw.as_str();
189            if ensure_float(stream, raw, error) {
190                decode_float_or_integer(stream, raw, kind, output, error)
191            } else {
192                kind
193            }
194        }
195        b'i' | b'I' => {
196            const SYMBOL: &str = "inf";
197            let kind = ScalarKind::Float;
198            if value != SYMBOL {
199                let expected = &[Expected::Literal(SYMBOL)];
200                let start = value.offset_from(&raw.as_str());
201                let end = start + value.len();
202                error.report_error(
203                    ParseError::new(kind.invalid_description())
204                        .with_context(Span::new_unchecked(0, raw.len()))
205                        .with_expected(expected)
206                        .with_unexpected(Span::new_unchecked(start, end)),
207                );
208                decode_as(raw, SYMBOL, kind, output, error)
209            } else {
210                decode_as_is(raw, kind, output, error)
211            }
212        }
213        b'n' | b'N' => {
214            const SYMBOL: &str = "nan";
215            let kind = ScalarKind::Float;
216            if value != SYMBOL {
217                let expected = &[Expected::Literal(SYMBOL)];
218                let start = value.offset_from(&raw.as_str());
219                let end = start + value.len();
220                error.report_error(
221                    ParseError::new(kind.invalid_description())
222                        .with_context(Span::new_unchecked(0, raw.len()))
223                        .with_expected(expected)
224                        .with_unexpected(Span::new_unchecked(start, end)),
225                );
226                decode_as(raw, SYMBOL, kind, output, error)
227            } else {
228                decode_as_is(raw, kind, output, error)
229            }
230        }
231        _ => decode_invalid(raw, output, error),
232    }
233}
234
235fn decode_zero_prefix<'i>(
236    value: &'i str,
237    signed: bool,
238    raw: Raw<'i>,
239    output: &mut dyn StringBuilder<'i>,
240    error: &mut dyn ErrorSink,
241) -> ScalarKind {
242    debug_assert_eq!(value.as_bytes()[0], b'0');
243    if value.len() == 1 {
244        let kind = ScalarKind::Integer(IntegerRadix::Dec);
245        // No extra validation needed
246        decode_float_or_integer(raw.as_str(), raw, kind, output, error)
247    } else {
248        let radix = value.as_bytes()[1];
249        match radix {
250            b'x' | b'X' => {
251                if value.contains(" ") {
252                    // Only datetimes can have a space
253                    return decode_invalid(raw, output, error);
254                }
255                if signed {
256                    error.report_error(
257                        ParseError::new("integers with a radix cannot be signed")
258                            .with_context(Span::new_unchecked(0, raw.len()))
259                            .with_expected(&[])
260                            .with_unexpected(Span::new_unchecked(0, 1)),
261                    );
262                }
263                if radix == b'X' {
264                    let start = value.offset_from(&raw.as_str());
265                    let end = start + 2;
266                    error.report_error(
267                        ParseError::new("radix must be lowercase")
268                            .with_context(Span::new_unchecked(0, raw.len()))
269                            .with_expected(&[Expected::Literal("0x")])
270                            .with_unexpected(Span::new_unchecked(start, end)),
271                    );
272                }
273                let radix = IntegerRadix::Hex;
274                let kind = ScalarKind::Integer(radix);
275                let stream = &value[2..];
276                if ensure_radixed_value(stream, raw, radix, error) {
277                    decode_float_or_integer(stream, raw, kind, output, error)
278                } else {
279                    kind
280                }
281            }
282            b'o' | b'O' => {
283                if value.contains(" ") {
284                    // Only datetimes can have a space
285                    return decode_invalid(raw, output, error);
286                }
287                if signed {
288                    error.report_error(
289                        ParseError::new("integers with a radix cannot be signed")
290                            .with_context(Span::new_unchecked(0, raw.len()))
291                            .with_expected(&[])
292                            .with_unexpected(Span::new_unchecked(0, 1)),
293                    );
294                }
295                if radix == b'O' {
296                    let start = value.offset_from(&raw.as_str());
297                    let end = start + 2;
298                    error.report_error(
299                        ParseError::new("radix must be lowercase")
300                            .with_context(Span::new_unchecked(0, raw.len()))
301                            .with_expected(&[Expected::Literal("0o")])
302                            .with_unexpected(Span::new_unchecked(start, end)),
303                    );
304                }
305                let radix = IntegerRadix::Oct;
306                let kind = ScalarKind::Integer(radix);
307                let stream = &value[2..];
308                if ensure_radixed_value(stream, raw, radix, error) {
309                    decode_float_or_integer(stream, raw, kind, output, error)
310                } else {
311                    kind
312                }
313            }
314            b'b' | b'B' => {
315                if value.contains(" ") {
316                    // Only datetimes can have a space
317                    return decode_invalid(raw, output, error);
318                }
319                if signed {
320                    error.report_error(
321                        ParseError::new("integers with a radix cannot be signed")
322                            .with_context(Span::new_unchecked(0, raw.len()))
323                            .with_expected(&[])
324                            .with_unexpected(Span::new_unchecked(0, 1)),
325                    );
326                }
327                if radix == b'B' {
328                    let start = value.offset_from(&raw.as_str());
329                    let end = start + 2;
330                    error.report_error(
331                        ParseError::new("radix must be lowercase")
332                            .with_context(Span::new_unchecked(0, raw.len()))
333                            .with_expected(&[Expected::Literal("0b")])
334                            .with_unexpected(Span::new_unchecked(start, end)),
335                    );
336                }
337                let radix = IntegerRadix::Bin;
338                let kind = ScalarKind::Integer(radix);
339                let stream = &value[2..];
340                if ensure_radixed_value(stream, raw, radix, error) {
341                    decode_float_or_integer(stream, raw, kind, output, error)
342                } else {
343                    kind
344                }
345            }
346            b'd' | b'D' => {
347                if value.contains(" ") {
348                    // Only datetimes can have a space
349                    return decode_invalid(raw, output, error);
350                }
351                if signed {
352                    error.report_error(
353                        ParseError::new("integers with a radix cannot be signed")
354                            .with_context(Span::new_unchecked(0, raw.len()))
355                            .with_expected(&[])
356                            .with_unexpected(Span::new_unchecked(0, 1)),
357                    );
358                }
359                let radix = IntegerRadix::Dec;
360                let kind = ScalarKind::Integer(radix);
361                let stream = &value[2..];
362                error.report_error(
363                    ParseError::new("redundant integer number prefix")
364                        .with_context(Span::new_unchecked(0, raw.len()))
365                        .with_expected(&[])
366                        .with_unexpected(Span::new_unchecked(0, 2)),
367                );
368                if ensure_radixed_value(stream, raw, radix, error) {
369                    decode_float_or_integer(stream, raw, kind, output, error)
370                } else {
371                    kind
372                }
373            }
374            _ => decode_datetime_or_float_or_integer(value, raw, output, error),
375        }
376    }
377}
378
379fn decode_datetime_or_float_or_integer<'i>(
380    value: &'i str,
381    raw: Raw<'i>,
382    output: &mut dyn StringBuilder<'i>,
383    error: &mut dyn ErrorSink,
384) -> ScalarKind {
385    let Some(digit_end) = value
386        .as_bytes()
387        .offset_for(|b| !(b'0'..=b'9').contains_token(b))
388    else {
389        let kind = ScalarKind::Integer(IntegerRadix::Dec);
390        let stream = raw.as_str();
391        if ensure_no_leading_zero(value, raw, error) {
392            return decode_float_or_integer(stream, raw, kind, output, error);
393        } else {
394            return kind;
395        }
396    };
397
398    #[cfg(feature = "unsafe")] // SAFETY: ascii digits ensures UTF-8 boundary
399    let rest = unsafe { &value.get_unchecked(digit_end..) };
400    #[cfg(not(feature = "unsafe"))]
401    let rest = &value[digit_end..];
402
403    if rest.starts_with("-") || rest.starts_with(":") {
404        decode_as_is(raw, ScalarKind::DateTime, output, error)
405    } else if rest.contains(" ") {
406        decode_invalid(raw, output, error)
407    } else if is_float(rest) {
408        let kind = ScalarKind::Float;
409        let stream = raw.as_str();
410        if ensure_float(value, raw, error) {
411            decode_float_or_integer(stream, raw, kind, output, error)
412        } else {
413            kind
414        }
415    } else if rest.starts_with("_") {
416        let kind = ScalarKind::Integer(IntegerRadix::Dec);
417        let stream = raw.as_str();
418        if ensure_no_leading_zero(value, raw, error) {
419            decode_float_or_integer(stream, raw, kind, output, error)
420        } else {
421            kind
422        }
423    } else {
424        decode_invalid(raw, output, error)
425    }
426}
427
428/// ```abnf
429/// ;; Float
430///
431/// float = float-int-part ( exp / frac [ exp ] )
432///
433/// float-int-part = dec-int
434/// frac = decimal-point zero-prefixable-int
435/// decimal-point = %x2E               ; .
436/// zero-prefixable-int = DIGIT *( DIGIT / underscore DIGIT )
437///
438/// exp = "e" float-exp-part
439/// float-exp-part = [ minus / plus ] zero-prefixable-int
440/// ```
441#[must_use]
442fn ensure_float<'i>(mut value: &'i str, raw: Raw<'i>, error: &mut dyn ErrorSink) -> bool {
443    let mut is_valid = true;
444
445    is_valid &= ensure_dec_uint(&mut value, raw, false, "invalid mantissa", error);
446
447    if value.starts_with(".") {
448        let _ = value.next_token();
449        is_valid &= ensure_dec_uint(&mut value, raw, true, "invalid fraction", error);
450    }
451
452    if value.starts_with(['e', 'E']) {
453        let _ = value.next_token();
454        if value.starts_with(['+', '-']) {
455            let _ = value.next_token();
456        }
457        is_valid &= ensure_dec_uint(&mut value, raw, true, "invalid exponent", error);
458    }
459
460    if !value.is_empty() {
461        let start = value.offset_from(&raw.as_str());
462        let end = raw.len();
463        error.report_error(
464            ParseError::new(ScalarKind::Float.invalid_description())
465                .with_context(Span::new_unchecked(0, raw.len()))
466                .with_expected(&[])
467                .with_unexpected(Span::new_unchecked(start, end)),
468        );
469        is_valid = false;
470    }
471
472    is_valid
473}
474
475#[must_use]
476fn ensure_dec_uint<'i>(
477    value: &mut &'i str,
478    raw: Raw<'i>,
479    zero_prefix: bool,
480    invalid_description: &'static str,
481    error: &mut dyn ErrorSink,
482) -> bool {
483    let mut is_valid = true;
484
485    let start = *value;
486    let mut digit_count = 0;
487    while let Some(current) = value.chars().next() {
488        if current.is_ascii_digit() {
489            digit_count += 1;
490        } else if current == '_' {
491        } else {
492            break;
493        }
494        let _ = value.next_token();
495    }
496
497    match digit_count {
498        0 => {
499            let start = start.offset_from(&raw.as_str());
500            let end = start;
501            error.report_error(
502                ParseError::new(invalid_description)
503                    .with_context(Span::new_unchecked(0, raw.len()))
504                    .with_expected(&[Expected::Description("digits")])
505                    .with_unexpected(Span::new_unchecked(start, end)),
506            );
507            is_valid = false;
508        }
509        1 => {}
510        _ if start.starts_with("0") && !zero_prefix => {
511            let start = start.offset_from(&raw.as_str());
512            let end = start + 1;
513            error.report_error(
514                ParseError::new("unexpected leading zero")
515                    .with_context(Span::new_unchecked(0, raw.len()))
516                    .with_expected(&[])
517                    .with_unexpected(Span::new_unchecked(start, end)),
518            );
519            is_valid = false;
520        }
521        _ => {}
522    }
523
524    is_valid
525}
526
527#[must_use]
528fn ensure_no_leading_zero<'i>(value: &'i str, raw: Raw<'i>, error: &mut dyn ErrorSink) -> bool {
529    let mut is_valid = true;
530
531    if value.starts_with("0") {
532        let start = value.offset_from(&raw.as_str());
533        let end = start + 1;
534        error.report_error(
535            ParseError::new("unexpected leading zero")
536                .with_context(Span::new_unchecked(0, raw.len()))
537                .with_expected(&[])
538                .with_unexpected(Span::new_unchecked(start, end)),
539        );
540        is_valid = false;
541    }
542
543    is_valid
544}
545
546#[must_use]
547fn ensure_radixed_value(
548    value: &str,
549    raw: Raw<'_>,
550    radix: IntegerRadix,
551    error: &mut dyn ErrorSink,
552) -> bool {
553    let mut is_valid = true;
554
555    let invalid = ['+', '-'];
556    let value = if let Some(value) = value.strip_prefix(invalid) {
557        let pos = raw.as_str().find(invalid).unwrap();
558        error.report_error(
559            ParseError::new("unexpected sign")
560                .with_context(Span::new_unchecked(0, raw.len()))
561                .with_expected(&[])
562                .with_unexpected(Span::new_unchecked(pos, pos + 1)),
563        );
564        is_valid = false;
565        value
566    } else {
567        value
568    };
569
570    let valid = radix.validator();
571    for (index, c) in value.char_indices() {
572        if !valid(c) && c != '_' {
573            let pos = value.offset_from(&raw.as_str()) + index;
574            error.report_error(
575                ParseError::new(radix.invalid_description())
576                    .with_context(Span::new_unchecked(0, raw.len()))
577                    .with_unexpected(Span::new_unchecked(pos, pos)),
578            );
579            is_valid = false;
580        }
581    }
582
583    is_valid
584}
585
586fn decode_float_or_integer<'i>(
587    mut stream: &'i str,
588    raw: Raw<'i>,
589    kind: ScalarKind,
590    output: &mut dyn StringBuilder<'i>,
591    error: &mut dyn ErrorSink,
592) -> ScalarKind {
593    output.clear();
594
595    let underscore = "_";
596
597    let stream_start = stream.offset_from(&raw.as_str());
598    while !stream.is_empty() {
599        let sep_pos = stream.find_slice(underscore);
600        let sep_start = sep_pos
601            .clone()
602            .map(|r| r.start)
603            .unwrap_or_else(|| stream.len());
604
605        let part_start = stream.offset_from(&raw.as_str());
606        let part_end = part_start + sep_start;
607        let part = stream.next_slice(sep_start);
608
609        if sep_pos.is_some() {
610            let _ = stream.next_slice(underscore.len());
611
612            let mut is_invalid_sep = false;
613            if let Some(last_pos) = sep_start.checked_sub(1) {
614                let last_byte = raw.as_bytes()[part_start + last_pos];
615                if !is_any_digit(last_byte, kind) {
616                    is_invalid_sep = true;
617                }
618            } else if part_start == stream_start {
619                is_invalid_sep = true;
620            }
621
622            if let Some(next_byte) = stream.as_bytes().first() {
623                if !is_any_digit(*next_byte, kind) {
624                    is_invalid_sep = true;
625                }
626            } else if stream.is_empty() {
627                is_invalid_sep = true;
628            }
629
630            if is_invalid_sep {
631                let start = part_end;
632                let end = start + underscore.len();
633                error.report_error(
634                    ParseError::new("`_` may only go between digits")
635                        .with_context(Span::new_unchecked(0, raw.len()))
636                        .with_expected(&[])
637                        .with_unexpected(Span::new_unchecked(end - underscore.len(), end)),
638                );
639            }
640        }
641
642        if !part.is_empty() && !output.push_str(part) {
643            error.report_error(
644                ParseError::new(ALLOCATION_ERROR)
645                    .with_unexpected(Span::new_unchecked(part_start, part_end)),
646            );
647        }
648    }
649
650    kind
651}
652
653fn is_any_digit(b: u8, kind: ScalarKind) -> bool {
654    if kind == ScalarKind::Float {
655        is_dec_integer_digit(b)
656    } else {
657        is_any_integer_digit(b)
658    }
659}
660
661fn is_any_integer_digit(b: u8) -> bool {
662    (b'0'..=b'9', b'a'..=b'f', b'A'..=b'F').contains_token(b)
663}
664
665fn is_dec_integer_digit(b: u8) -> bool {
666    (b'0'..=b'9').contains_token(b)
667}
668
669fn is_float(raw: &str) -> bool {
670    raw.as_bytes().find_slice((b'.', b'e', b'E')).is_some()
671}
672
673fn decode_as_is<'i>(
674    raw: Raw<'i>,
675    kind: ScalarKind,
676    output: &mut dyn StringBuilder<'i>,
677    error: &mut dyn ErrorSink,
678) -> ScalarKind {
679    let kind = decode_as(raw, raw.as_str(), kind, output, error);
680    kind
681}
682
683fn decode_as<'i>(
684    raw: Raw<'i>,
685    symbol: &'i str,
686    kind: ScalarKind,
687    output: &mut dyn StringBuilder<'i>,
688    error: &mut dyn ErrorSink,
689) -> ScalarKind {
690    output.clear();
691    if !output.push_str(symbol) {
692        error.report_error(
693            ParseError::new(ALLOCATION_ERROR).with_unexpected(Span::new_unchecked(0, raw.len())),
694        );
695    }
696    kind
697}
698
699fn decode_symbol<'i>(
700    raw: Raw<'i>,
701    symbol: &'static str,
702    kind: ScalarKind,
703    expected: &'static [Expected],
704    output: &mut dyn StringBuilder<'i>,
705    error: &mut dyn ErrorSink,
706) -> ScalarKind {
707    if raw.as_str() != symbol {
708        if raw.as_str().contains(" ") {
709            return decode_invalid(raw, output, error);
710        } else {
711            error.report_error(
712                ParseError::new(kind.invalid_description())
713                    .with_context(Span::new_unchecked(0, raw.len()))
714                    .with_expected(expected)
715                    .with_unexpected(Span::new_unchecked(0, raw.len())),
716            );
717        }
718    }
719
720    decode_as(raw, symbol, kind, output, error)
721}
722
723fn decode_invalid<'i>(
724    raw: Raw<'i>,
725    output: &mut dyn StringBuilder<'i>,
726    error: &mut dyn ErrorSink,
727) -> ScalarKind {
728    if raw.as_str().ends_with("'''") {
729        error.report_error(
730            ParseError::new("missing opening quote")
731                .with_context(Span::new_unchecked(0, raw.len()))
732                .with_expected(&[Expected::Literal(r#"'''"#)])
733                .with_unexpected(Span::new_unchecked(0, 0)),
734        );
735    } else if raw.as_str().ends_with(r#"""""#) {
736        error.report_error(
737            ParseError::new("missing opening quote")
738                .with_context(Span::new_unchecked(0, raw.len()))
739                .with_expected(&[Expected::Description("multi-line basic string")])
740                .with_expected(&[Expected::Literal(r#"""""#)])
741                .with_unexpected(Span::new_unchecked(0, 0)),
742        );
743    } else if raw.as_str().ends_with("'") {
744        error.report_error(
745            ParseError::new("missing opening quote")
746                .with_context(Span::new_unchecked(0, raw.len()))
747                .with_expected(&[Expected::Literal(r#"'"#)])
748                .with_unexpected(Span::new_unchecked(0, 0)),
749        );
750    } else if raw.as_str().ends_with(r#"""#) {
751        error.report_error(
752            ParseError::new("missing opening quote")
753                .with_context(Span::new_unchecked(0, raw.len()))
754                .with_expected(&[Expected::Literal(r#"""#)])
755                .with_unexpected(Span::new_unchecked(0, 0)),
756        );
757    } else {
758        error.report_error(
759            ParseError::new("string values must be quoted")
760                .with_context(Span::new_unchecked(0, raw.len()))
761                .with_expected(&[Expected::Description("literal string")])
762                .with_unexpected(Span::new_unchecked(0, raw.len())),
763        );
764    }
765
766    output.clear();
767    if !output.push_str(raw.as_str()) {
768        error.report_error(
769            ParseError::new(ALLOCATION_ERROR).with_unexpected(Span::new_unchecked(0, raw.len())),
770        );
771    }
772    ScalarKind::String
773}
774
775#[cfg(test)]
776#[cfg(feature = "std")]
777mod test {
778    use super::*;
779    use alloc::borrow::Cow;
780
781    #[test]
782    fn many_redundant_signs() {
783        // Regression test: decode_sign_prefix previously recursed once per sign
784        // character, causing a stack overflow on long runs.
785        let signs = "-".repeat(5_000);
786        let input = format!("{signs}1");
787
788        let mut error = Vec::new();
789        let mut output = Cow::Borrowed("");
790        decode_unquoted_scalar(
791            Raw::new_unchecked(&input, None, Default::default()),
792            &mut output,
793            &mut error,
794        );
795        assert!(!error.is_empty());
796    }
797}