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::decoder::StringBuilder;
7use crate::ErrorSink;
8use crate::Expected;
9use crate::ParseError;
10use crate::Raw;
11use crate::Span;
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 Some(first) = value.as_bytes().first() else {
162        return decode_invalid(raw, output, error);
163    };
164    match first {
165        // number starts
166        b'+' | b'-' => {
167            let start = value.offset_from(&raw.as_str());
168            let end = start + 1;
169            error.report_error(
170                ParseError::new("redundant numeric sign")
171                    .with_context(Span::new_unchecked(0, raw.len()))
172                    .with_expected(&[])
173                    .with_unexpected(Span::new_unchecked(start, end)),
174            );
175
176            let value = &value[1..];
177            decode_sign_prefix(raw, value, output, error)
178        }
179        // Report as if they were numbers because its most likely a typo
180        b'_' => decode_datetime_or_float_or_integer(value, raw, output, error),
181        // Date/number starts
182        b'0' => decode_zero_prefix(value, true, raw, output, error),
183        b'1'..=b'9' => decode_datetime_or_float_or_integer(value, raw, output, error),
184        // Report as if they were numbers because its most likely a typo
185        b'.' => {
186            let kind = ScalarKind::Float;
187            let stream = raw.as_str();
188            if ensure_float(stream, raw, error) {
189                decode_float_or_integer(stream, raw, kind, output, error)
190            } else {
191                kind
192            }
193        }
194        b'i' | b'I' => {
195            const SYMBOL: &str = "inf";
196            let kind = ScalarKind::Float;
197            if value != SYMBOL {
198                let expected = &[Expected::Literal(SYMBOL)];
199                let start = value.offset_from(&raw.as_str());
200                let end = start + value.len();
201                error.report_error(
202                    ParseError::new(kind.invalid_description())
203                        .with_context(Span::new_unchecked(0, raw.len()))
204                        .with_expected(expected)
205                        .with_unexpected(Span::new_unchecked(start, end)),
206                );
207                decode_as(raw, SYMBOL, kind, output, error)
208            } else {
209                decode_as_is(raw, kind, output, error)
210            }
211        }
212        b'n' | b'N' => {
213            const SYMBOL: &str = "nan";
214            let kind = ScalarKind::Float;
215            if value != SYMBOL {
216                let expected = &[Expected::Literal(SYMBOL)];
217                let start = value.offset_from(&raw.as_str());
218                let end = start + value.len();
219                error.report_error(
220                    ParseError::new(kind.invalid_description())
221                        .with_context(Span::new_unchecked(0, raw.len()))
222                        .with_expected(expected)
223                        .with_unexpected(Span::new_unchecked(start, end)),
224                );
225                decode_as(raw, SYMBOL, kind, output, error)
226            } else {
227                decode_as_is(raw, kind, output, error)
228            }
229        }
230        _ => decode_invalid(raw, output, error),
231    }
232}
233
234fn decode_zero_prefix<'i>(
235    value: &'i str,
236    signed: bool,
237    raw: Raw<'i>,
238    output: &mut dyn StringBuilder<'i>,
239    error: &mut dyn ErrorSink,
240) -> ScalarKind {
241    debug_assert_eq!(value.as_bytes()[0], b'0');
242    if value.len() == 1 {
243        let kind = ScalarKind::Integer(IntegerRadix::Dec);
244        // No extra validation needed
245        decode_float_or_integer(raw.as_str(), raw, kind, output, error)
246    } else {
247        let radix = value.as_bytes()[1];
248        match radix {
249            b'x' | b'X' => {
250                if value.contains(" ") {
251                    // Only datetimes can have a space
252                    return decode_invalid(raw, output, error);
253                }
254                if signed {
255                    error.report_error(
256                        ParseError::new("integers with a radix cannot be signed")
257                            .with_context(Span::new_unchecked(0, raw.len()))
258                            .with_expected(&[])
259                            .with_unexpected(Span::new_unchecked(0, 1)),
260                    );
261                }
262                if radix == b'X' {
263                    let start = value.offset_from(&raw.as_str());
264                    let end = start + 2;
265                    error.report_error(
266                        ParseError::new("radix must be lowercase")
267                            .with_context(Span::new_unchecked(0, raw.len()))
268                            .with_expected(&[Expected::Literal("0x")])
269                            .with_unexpected(Span::new_unchecked(start, end)),
270                    );
271                }
272                let radix = IntegerRadix::Hex;
273                let kind = ScalarKind::Integer(radix);
274                let stream = &value[2..];
275                if ensure_radixed_value(stream, raw, radix, error) {
276                    decode_float_or_integer(stream, raw, kind, output, error)
277                } else {
278                    kind
279                }
280            }
281            b'o' | b'O' => {
282                if value.contains(" ") {
283                    // Only datetimes can have a space
284                    return decode_invalid(raw, output, error);
285                }
286                if signed {
287                    error.report_error(
288                        ParseError::new("integers with a radix cannot be signed")
289                            .with_context(Span::new_unchecked(0, raw.len()))
290                            .with_expected(&[])
291                            .with_unexpected(Span::new_unchecked(0, 1)),
292                    );
293                }
294                if radix == b'O' {
295                    let start = value.offset_from(&raw.as_str());
296                    let end = start + 2;
297                    error.report_error(
298                        ParseError::new("radix must be lowercase")
299                            .with_context(Span::new_unchecked(0, raw.len()))
300                            .with_expected(&[Expected::Literal("0o")])
301                            .with_unexpected(Span::new_unchecked(start, end)),
302                    );
303                }
304                let radix = IntegerRadix::Oct;
305                let kind = ScalarKind::Integer(radix);
306                let stream = &value[2..];
307                if ensure_radixed_value(stream, raw, radix, error) {
308                    decode_float_or_integer(stream, raw, kind, output, error)
309                } else {
310                    kind
311                }
312            }
313            b'b' | b'B' => {
314                if value.contains(" ") {
315                    // Only datetimes can have a space
316                    return decode_invalid(raw, output, error);
317                }
318                if signed {
319                    error.report_error(
320                        ParseError::new("integers with a radix cannot be signed")
321                            .with_context(Span::new_unchecked(0, raw.len()))
322                            .with_expected(&[])
323                            .with_unexpected(Span::new_unchecked(0, 1)),
324                    );
325                }
326                if radix == b'B' {
327                    let start = value.offset_from(&raw.as_str());
328                    let end = start + 2;
329                    error.report_error(
330                        ParseError::new("radix must be lowercase")
331                            .with_context(Span::new_unchecked(0, raw.len()))
332                            .with_expected(&[Expected::Literal("0b")])
333                            .with_unexpected(Span::new_unchecked(start, end)),
334                    );
335                }
336                let radix = IntegerRadix::Bin;
337                let kind = ScalarKind::Integer(radix);
338                let stream = &value[2..];
339                if ensure_radixed_value(stream, raw, radix, error) {
340                    decode_float_or_integer(stream, raw, kind, output, error)
341                } else {
342                    kind
343                }
344            }
345            b'd' | b'D' => {
346                if value.contains(" ") {
347                    // Only datetimes can have a space
348                    return decode_invalid(raw, output, error);
349                }
350                if signed {
351                    error.report_error(
352                        ParseError::new("integers with a radix cannot be signed")
353                            .with_context(Span::new_unchecked(0, raw.len()))
354                            .with_expected(&[])
355                            .with_unexpected(Span::new_unchecked(0, 1)),
356                    );
357                }
358                let radix = IntegerRadix::Dec;
359                let kind = ScalarKind::Integer(radix);
360                let stream = &value[2..];
361                error.report_error(
362                    ParseError::new("redundant integer number prefix")
363                        .with_context(Span::new_unchecked(0, raw.len()))
364                        .with_expected(&[])
365                        .with_unexpected(Span::new_unchecked(0, 2)),
366                );
367                if ensure_radixed_value(stream, raw, radix, error) {
368                    decode_float_or_integer(stream, raw, kind, output, error)
369                } else {
370                    kind
371                }
372            }
373            _ => decode_datetime_or_float_or_integer(value, raw, output, error),
374        }
375    }
376}
377
378fn decode_datetime_or_float_or_integer<'i>(
379    value: &'i str,
380    raw: Raw<'i>,
381    output: &mut dyn StringBuilder<'i>,
382    error: &mut dyn ErrorSink,
383) -> ScalarKind {
384    let Some(digit_end) = value
385        .as_bytes()
386        .offset_for(|b| !(b'0'..=b'9').contains_token(b))
387    else {
388        let kind = ScalarKind::Integer(IntegerRadix::Dec);
389        let stream = raw.as_str();
390        if ensure_no_leading_zero(value, raw, error) {
391            return decode_float_or_integer(stream, raw, kind, output, error);
392        } else {
393            return kind;
394        }
395    };
396
397    #[cfg(feature = "unsafe")] // SAFETY: ascii digits ensures UTF-8 boundary
398    let rest = unsafe { &value.get_unchecked(digit_end..) };
399    #[cfg(not(feature = "unsafe"))]
400    let rest = &value[digit_end..];
401
402    if rest.starts_with("-") || rest.starts_with(":") {
403        decode_as_is(raw, ScalarKind::DateTime, output, error)
404    } else if rest.contains(" ") {
405        decode_invalid(raw, output, error)
406    } else if is_float(rest) {
407        let kind = ScalarKind::Float;
408        let stream = raw.as_str();
409        if ensure_float(value, raw, error) {
410            decode_float_or_integer(stream, raw, kind, output, error)
411        } else {
412            kind
413        }
414    } else if rest.starts_with("_") {
415        let kind = ScalarKind::Integer(IntegerRadix::Dec);
416        let stream = raw.as_str();
417        if ensure_no_leading_zero(value, raw, error) {
418            decode_float_or_integer(stream, raw, kind, output, error)
419        } else {
420            kind
421        }
422    } else {
423        decode_invalid(raw, output, error)
424    }
425}
426
427/// ```abnf
428/// ;; Float
429///
430/// float = float-int-part ( exp / frac [ exp ] )
431///
432/// float-int-part = dec-int
433/// frac = decimal-point zero-prefixable-int
434/// decimal-point = %x2E               ; .
435/// zero-prefixable-int = DIGIT *( DIGIT / underscore DIGIT )
436///
437/// exp = "e" float-exp-part
438/// float-exp-part = [ minus / plus ] zero-prefixable-int
439/// ```
440#[must_use]
441fn ensure_float<'i>(mut value: &'i str, raw: Raw<'i>, error: &mut dyn ErrorSink) -> bool {
442    let mut is_valid = true;
443
444    is_valid &= ensure_dec_uint(&mut value, raw, false, "invalid mantissa", error);
445
446    if value.starts_with(".") {
447        let _ = value.next_token();
448        is_valid &= ensure_dec_uint(&mut value, raw, true, "invalid fraction", error);
449    }
450
451    if value.starts_with(['e', 'E']) {
452        let _ = value.next_token();
453        if value.starts_with(['+', '-']) {
454            let _ = value.next_token();
455        }
456        is_valid &= ensure_dec_uint(&mut value, raw, true, "invalid exponent", error);
457    }
458
459    if !value.is_empty() {
460        let start = value.offset_from(&raw.as_str());
461        let end = raw.len();
462        error.report_error(
463            ParseError::new(ScalarKind::Float.invalid_description())
464                .with_context(Span::new_unchecked(0, raw.len()))
465                .with_expected(&[])
466                .with_unexpected(Span::new_unchecked(start, end)),
467        );
468        is_valid = false;
469    }
470
471    is_valid
472}
473
474#[must_use]
475fn ensure_dec_uint<'i>(
476    value: &mut &'i str,
477    raw: Raw<'i>,
478    zero_prefix: bool,
479    invalid_description: &'static str,
480    error: &mut dyn ErrorSink,
481) -> bool {
482    let mut is_valid = true;
483
484    let start = *value;
485    let mut digit_count = 0;
486    while let Some(current) = value.chars().next() {
487        if current.is_ascii_digit() {
488            digit_count += 1;
489        } else if current == '_' {
490        } else {
491            break;
492        }
493        let _ = value.next_token();
494    }
495
496    match digit_count {
497        0 => {
498            let start = start.offset_from(&raw.as_str());
499            let end = start;
500            error.report_error(
501                ParseError::new(invalid_description)
502                    .with_context(Span::new_unchecked(0, raw.len()))
503                    .with_expected(&[Expected::Description("digits")])
504                    .with_unexpected(Span::new_unchecked(start, end)),
505            );
506            is_valid = false;
507        }
508        1 => {}
509        _ if start.starts_with("0") && !zero_prefix => {
510            let start = start.offset_from(&raw.as_str());
511            let end = start + 1;
512            error.report_error(
513                ParseError::new("unexpected leading zero")
514                    .with_context(Span::new_unchecked(0, raw.len()))
515                    .with_expected(&[])
516                    .with_unexpected(Span::new_unchecked(start, end)),
517            );
518            is_valid = false;
519        }
520        _ => {}
521    }
522
523    is_valid
524}
525
526#[must_use]
527fn ensure_no_leading_zero<'i>(value: &'i str, raw: Raw<'i>, error: &mut dyn ErrorSink) -> bool {
528    let mut is_valid = true;
529
530    if value.starts_with("0") {
531        let start = value.offset_from(&raw.as_str());
532        let end = start + 1;
533        error.report_error(
534            ParseError::new("unexpected leading zero")
535                .with_context(Span::new_unchecked(0, raw.len()))
536                .with_expected(&[])
537                .with_unexpected(Span::new_unchecked(start, end)),
538        );
539        is_valid = false;
540    }
541
542    is_valid
543}
544
545#[must_use]
546fn ensure_radixed_value(
547    value: &str,
548    raw: Raw<'_>,
549    radix: IntegerRadix,
550    error: &mut dyn ErrorSink,
551) -> bool {
552    let mut is_valid = true;
553
554    let invalid = ['+', '-'];
555    let value = if let Some(value) = value.strip_prefix(invalid) {
556        let pos = raw.as_str().find(invalid).unwrap();
557        error.report_error(
558            ParseError::new("unexpected sign")
559                .with_context(Span::new_unchecked(0, raw.len()))
560                .with_expected(&[])
561                .with_unexpected(Span::new_unchecked(pos, pos + 1)),
562        );
563        is_valid = false;
564        value
565    } else {
566        value
567    };
568
569    let valid = radix.validator();
570    for (index, c) in value.char_indices() {
571        if !valid(c) && c != '_' {
572            let pos = value.offset_from(&raw.as_str()) + index;
573            error.report_error(
574                ParseError::new(radix.invalid_description())
575                    .with_context(Span::new_unchecked(0, raw.len()))
576                    .with_unexpected(Span::new_unchecked(pos, pos)),
577            );
578            is_valid = false;
579        }
580    }
581
582    is_valid
583}
584
585fn decode_float_or_integer<'i>(
586    stream: &'i str,
587    raw: Raw<'i>,
588    kind: ScalarKind,
589    output: &mut dyn StringBuilder<'i>,
590    error: &mut dyn ErrorSink,
591) -> ScalarKind {
592    output.clear();
593
594    let underscore = "_";
595
596    if has_underscore(stream) {
597        if stream.starts_with(underscore) {
598            error.report_error(
599                ParseError::new("`_` may only go between digits")
600                    .with_context(Span::new_unchecked(0, raw.len()))
601                    .with_expected(&[])
602                    .with_unexpected(Span::new_unchecked(0, underscore.len())),
603            );
604        }
605        if 1 < stream.len() && stream.ends_with(underscore) {
606            let start = stream.offset_from(&raw.as_str());
607            let end = start + stream.len();
608            error.report_error(
609                ParseError::new("`_` may only go between digits")
610                    .with_context(Span::new_unchecked(0, raw.len()))
611                    .with_expected(&[])
612                    .with_unexpected(Span::new_unchecked(end - underscore.len(), end)),
613            );
614        }
615
616        for part in stream.split(underscore) {
617            let part_start = part.offset_from(&raw.as_str());
618            let part_end = part_start + part.len();
619
620            if 0 < part_start {
621                let first = part.as_bytes().first().copied().unwrap_or(b'0');
622                if !is_any_digit(first, kind) {
623                    let start = part_start - underscore.len();
624                    let end = part_start;
625                    debug_assert_eq!(&raw.as_str()[start..end], underscore);
626                    error.report_error(
627                        ParseError::new("`_` may only go between digits")
628                            .with_context(Span::new_unchecked(0, raw.len()))
629                            .with_unexpected(Span::new_unchecked(start, end)),
630                    );
631                }
632            }
633            if 1 < part.len() && part_end < raw.len() {
634                let last = part.as_bytes().last().copied().unwrap_or(b'0');
635                if !is_any_digit(last, kind) {
636                    let start = part_end;
637                    let end = start + underscore.len();
638                    debug_assert_eq!(&raw.as_str()[start..end], underscore);
639                    error.report_error(
640                        ParseError::new("`_` may only go between digits")
641                            .with_context(Span::new_unchecked(0, raw.len()))
642                            .with_unexpected(Span::new_unchecked(start, end)),
643                    );
644                }
645            }
646
647            if part.is_empty() && part_start != 0 && part_end != raw.len() {
648                let start = part_start;
649                let end = start + 1;
650                error.report_error(
651                    ParseError::new("`_` may only go between digits")
652                        .with_context(Span::new_unchecked(0, raw.len()))
653                        .with_unexpected(Span::new_unchecked(start, end)),
654                );
655            }
656
657            if !part.is_empty() && !output.push_str(part) {
658                error.report_error(
659                    ParseError::new(ALLOCATION_ERROR)
660                        .with_unexpected(Span::new_unchecked(part_start, part_end)),
661                );
662            }
663        }
664    } else {
665        if !output.push_str(stream) {
666            error.report_error(
667                ParseError::new(ALLOCATION_ERROR)
668                    .with_unexpected(Span::new_unchecked(0, raw.len())),
669            );
670        }
671    }
672
673    kind
674}
675
676fn is_any_digit(b: u8, kind: ScalarKind) -> bool {
677    if kind == ScalarKind::Float {
678        is_dec_integer_digit(b)
679    } else {
680        is_any_integer_digit(b)
681    }
682}
683
684fn is_any_integer_digit(b: u8) -> bool {
685    (b'0'..=b'9', b'a'..=b'f', b'A'..=b'F').contains_token(b)
686}
687
688fn is_dec_integer_digit(b: u8) -> bool {
689    (b'0'..=b'9').contains_token(b)
690}
691
692fn has_underscore(raw: &str) -> bool {
693    raw.as_bytes().find_slice(b'_').is_some()
694}
695
696fn is_float(raw: &str) -> bool {
697    raw.as_bytes().find_slice((b'.', b'e', b'E')).is_some()
698}
699
700fn decode_as_is<'i>(
701    raw: Raw<'i>,
702    kind: ScalarKind,
703    output: &mut dyn StringBuilder<'i>,
704    error: &mut dyn ErrorSink,
705) -> ScalarKind {
706    let kind = decode_as(raw, raw.as_str(), kind, output, error);
707    kind
708}
709
710fn decode_as<'i>(
711    raw: Raw<'i>,
712    symbol: &'i str,
713    kind: ScalarKind,
714    output: &mut dyn StringBuilder<'i>,
715    error: &mut dyn ErrorSink,
716) -> ScalarKind {
717    output.clear();
718    if !output.push_str(symbol) {
719        error.report_error(
720            ParseError::new(ALLOCATION_ERROR).with_unexpected(Span::new_unchecked(0, raw.len())),
721        );
722    }
723    kind
724}
725
726fn decode_symbol<'i>(
727    raw: Raw<'i>,
728    symbol: &'static str,
729    kind: ScalarKind,
730    expected: &'static [Expected],
731    output: &mut dyn StringBuilder<'i>,
732    error: &mut dyn ErrorSink,
733) -> ScalarKind {
734    if raw.as_str() != symbol {
735        if raw.as_str().contains(" ") {
736            return decode_invalid(raw, output, error);
737        } else {
738            error.report_error(
739                ParseError::new(kind.invalid_description())
740                    .with_context(Span::new_unchecked(0, raw.len()))
741                    .with_expected(expected)
742                    .with_unexpected(Span::new_unchecked(0, raw.len())),
743            );
744        }
745    }
746
747    decode_as(raw, symbol, kind, output, error)
748}
749
750fn decode_invalid<'i>(
751    raw: Raw<'i>,
752    output: &mut dyn StringBuilder<'i>,
753    error: &mut dyn ErrorSink,
754) -> ScalarKind {
755    if raw.as_str().ends_with("'''") {
756        error.report_error(
757            ParseError::new("missing opening quote")
758                .with_context(Span::new_unchecked(0, raw.len()))
759                .with_expected(&[Expected::Literal(r#"'''"#)])
760                .with_unexpected(Span::new_unchecked(0, 0)),
761        );
762    } else if raw.as_str().ends_with(r#"""""#) {
763        error.report_error(
764            ParseError::new("missing opening quote")
765                .with_context(Span::new_unchecked(0, raw.len()))
766                .with_expected(&[Expected::Description("multi-line basic string")])
767                .with_expected(&[Expected::Literal(r#"""""#)])
768                .with_unexpected(Span::new_unchecked(0, 0)),
769        );
770    } else if raw.as_str().ends_with("'") {
771        error.report_error(
772            ParseError::new("missing opening quote")
773                .with_context(Span::new_unchecked(0, raw.len()))
774                .with_expected(&[Expected::Literal(r#"'"#)])
775                .with_unexpected(Span::new_unchecked(0, 0)),
776        );
777    } else if raw.as_str().ends_with(r#"""#) {
778        error.report_error(
779            ParseError::new("missing opening quote")
780                .with_context(Span::new_unchecked(0, raw.len()))
781                .with_expected(&[Expected::Literal(r#"""#)])
782                .with_unexpected(Span::new_unchecked(0, 0)),
783        );
784    } else {
785        error.report_error(
786            ParseError::new("string values must be quoted")
787                .with_context(Span::new_unchecked(0, raw.len()))
788                .with_expected(&[Expected::Description("literal string")])
789                .with_unexpected(Span::new_unchecked(0, raw.len())),
790        );
791    }
792
793    output.clear();
794    if !output.push_str(raw.as_str()) {
795        error.report_error(
796            ParseError::new(ALLOCATION_ERROR).with_unexpected(Span::new_unchecked(0, raw.len())),
797        );
798    }
799    ScalarKind::String
800}