Skip to main content

granit_parser/
error.rs

1//! Parser and scanner error types.
2
3#[cfg(feature = "std")]
4use alloc::sync::Arc;
5use alloc::{
6    string::{String, ToString},
7    vec::Vec,
8};
9use core::fmt;
10
11use crate::scanner::Marker;
12
13/// Details of an I/O failure reported by an input adapter.
14///
15/// This error is primarily intended for terminal failures such as a missing file, insufficient
16/// permissions, or a failed read, where an exact character position is usually not meaningful.
17/// Streaming inputs may be read ahead by a small lookahead window. Once an adapter reports an I/O
18/// failure, the parser reports it at its current marker; consequently, a few successfully read
19/// characters that were already buffered ahead of that marker may not be scanned or emitted.
20///
21/// The human-readable message is available in every build. With the `std` feature enabled, an
22/// instance constructed from `std::io::Error` also retains that original error and exposes it
23/// through `InputIoError::io_error` and the standard error source chain.
24///
25/// Equality and hashing use the portable message. The optional retained `std` error does not
26/// participate, so these operations have the same behavior with and without the `std` feature.
27#[derive(Clone, Debug)]
28pub struct InputIoError {
29    message: String,
30    #[cfg(feature = "std")]
31    source: Option<Arc<std::io::Error>>,
32}
33
34impl InputIoError {
35    /// Create I/O error details from a portable message.
36    ///
37    /// This constructor is available in `no_std` builds. It does not retain a typed source error.
38    #[must_use]
39    pub fn from_message(message: impl Into<String>) -> Self {
40        Self {
41            message: message.into(),
42            #[cfg(feature = "std")]
43            source: None,
44        }
45    }
46
47    /// Create I/O error details while retaining the original [`std::io::Error`].
48    #[cfg(feature = "std")]
49    #[must_use]
50    pub fn from_io(error: std::io::Error) -> Self {
51        Self {
52            message: error.to_string(),
53            source: Some(Arc::new(error)),
54        }
55    }
56
57    /// Return the portable human-readable error message.
58    #[must_use]
59    pub fn message(&self) -> &str {
60        &self.message
61    }
62
63    /// Return the retained [`std::io::Error`], when one is available.
64    #[cfg(feature = "std")]
65    #[must_use]
66    pub fn io_error(&self) -> Option<&std::io::Error> {
67        self.source.as_deref()
68    }
69
70    /// Recover the retained [`std::io::Error`] when this is its only owner.
71    ///
72    /// # Errors
73    /// Returns the original `InputIoError` when it was created from a portable message or when
74    /// another clone still shares the retained error.
75    #[cfg(feature = "std")]
76    pub fn try_into_io_error(self) -> Result<std::io::Error, Self> {
77        let Self { message, source } = self;
78        let Some(source) = source else {
79            return Err(Self {
80                message,
81                source: None,
82            });
83        };
84
85        match Arc::try_unwrap(source) {
86            Ok(error) => Ok(error),
87            Err(source) => Err(Self {
88                message,
89                source: Some(source),
90            }),
91        }
92    }
93}
94
95#[cfg(feature = "std")]
96impl From<std::io::Error> for InputIoError {
97    fn from(error: std::io::Error) -> Self {
98        Self::from_io(error)
99    }
100}
101
102impl PartialEq for InputIoError {
103    fn eq(&self, other: &Self) -> bool {
104        self.message == other.message
105    }
106}
107
108impl Eq for InputIoError {}
109
110impl core::hash::Hash for InputIoError {
111    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
112        core::hash::Hash::hash(&self.message, state);
113    }
114}
115
116impl fmt::Display for InputIoError {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.write_str(&self.message)
119    }
120}
121
122impl core::error::Error for InputIoError {
123    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
124        #[cfg(feature = "std")]
125        {
126            self.source
127                .as_deref()
128                .map(|error| error as &(dyn core::error::Error + 'static))
129        }
130
131        #[cfg(not(feature = "std"))]
132        {
133            None
134        }
135    }
136}
137
138/// Machine-readable category for a [`ScanError`].
139#[derive(Clone, PartialEq, Debug, Eq, Hash)]
140#[non_exhaustive]
141pub enum ErrorKind {
142    /// Too many comments were buffered while looking ahead to a collection entry or document.
143    #[cfg(feature = "comments")]
144    TooManyComments,
145    /// Reading from the input source failed.
146    InputIo {
147        /// Portable details and, with the `std` feature, an optional retained I/O error.
148        error: InputIoError,
149    },
150    /// The input source was not valid text in the adapter's expected encoding.
151    InputDecoding {
152        /// Human-readable details supplied by the input adapter.
153        message: String,
154    },
155    /// The raw input exceeded a configured byte limit.
156    InputByteLimitExceeded {
157        /// Maximum number of raw input bytes accepted by the adapter.
158        limit: usize,
159    },
160    /// Input ended while parsing a flow sequence.
161    UnexpectedEofFlowSequence,
162    /// Input ended while parsing a flow mapping.
163    UnexpectedEofFlowMapping,
164    /// Input ended while parsing an implicit flow mapping.
165    UnexpectedEofImplicitFlowMapping,
166    /// Input ended while parsing a block sequence.
167    UnexpectedEofBlockSequence,
168    /// Input ended while parsing a block mapping.
169    UnexpectedEofBlockMapping,
170    /// Input ended unexpectedly in another parser state.
171    UnexpectedEof,
172    /// A stream-start token was expected.
173    ExpectedStreamStart,
174    /// More than one YAML version directive was found for a document.
175    DuplicateVersionDirective,
176    /// The YAML major version is unsupported.
177    UnsupportedYamlMajorVersion,
178    /// A tag directive handle was declared more than once for a document.
179    DuplicateTagDirective,
180    /// A document-start token was expected.
181    ExpectedDocumentStart,
182    /// A directive followed an implicit document without an explicit document end.
183    MissingDocumentEndBeforeDirective,
184    /// The parser ran out of representable anchor identifiers.
185    AnchorCountOverflow,
186    /// An alias referred to an unknown anchor.
187    UnknownAnchor,
188    /// The parser did not find expected node content.
189    ExpectedNodeContent,
190    /// A block mapping key was expected.
191    ExpectedBlockMappingKey,
192    /// A flow mapping separator or closing brace was expected.
193    ExpectedFlowMappingSeparator,
194    /// A flow sequence separator or closing bracket was expected.
195    ExpectedFlowSequenceSeparator,
196    /// A block sequence entry indicator was expected.
197    ExpectedBlockSequenceEntry,
198    /// A tag used a handle that was not declared.
199    UndeclaredTagHandle,
200    /// No include resolver was configured for a parser stack.
201    MissingIncludeResolver,
202    /// An error supplied by an external parser adapter or resolver.
203    Custom(String),
204    /// A parser-stack entry contained multiple documents where only one is supported.
205    MultipleDocumentsUnsupported,
206    /// An input advertised byte offsets but did not provide the requested slice.
207    InputOffsetsWithoutSlice,
208    /// An input advertised slicing but did not provide the requested slice.
209    InputSlicingUnavailable,
210    /// A tag did not begin with the expected exclamation mark.
211    ExpectedTagBang,
212    /// A tag directive handle did not end with the expected exclamation mark.
213    ExpectedTagDirectiveBang,
214    /// A global tag started with an invalid character.
215    InvalidGlobalTagCharacter,
216    /// A required simple key was not followed by a value indicator.
217    SimpleKeyExpected,
218    /// A previously saved simple key was no longer valid.
219    InvalidSimpleKey,
220    /// Invalid content followed a document-end marker.
221    InvalidDocumentEnd,
222    /// Indentation was invalid for the current parser context.
223    InvalidIndentation,
224    /// A byte-order mark appeared inside a document.
225    BomInsideDocument,
226    /// An unexpected reserved character was encountered.
227    UnexpectedCharacter {
228        /// The character that was encountered.
229        character: char,
230    },
231    /// A tab was used in a context where it is not allowed.
232    TabNotAllowed,
233    /// A tab was used in block indentation.
234    TabInBlockIndentation,
235    /// A comment interrupted a multiline plain scalar.
236    CommentInterceptedScalar,
237    /// Required whitespace was not found.
238    ExpectedWhitespace,
239    /// A comment was not separated from the preceding token by whitespace.
240    CommentNotSeparated,
241    /// A directive did not end with a comment or line break.
242    InvalidDirectiveTerminator,
243    /// A directive was encountered while [`crate::Options::disallow_directives`] was enabled.
244    DirectiveDisallowed,
245    /// A YAML version directive did not contain the expected digit or dot.
246    MissingYamlVersionSeparator,
247    /// A directive name was missing.
248    MissingDirectiveName,
249    /// A directive name contained an invalid character.
250    InvalidDirectiveName,
251    /// A retained directive name and payload exceeded the configured byte limit.
252    DirectiveByteLimitExceeded {
253        /// Maximum number of directive bytes accepted by the scanner.
254        limit: usize,
255    },
256    /// A reserved directive carried more parameters than the configured limit.
257    TooManyReservedDirectiveParams {
258        /// Maximum number of reserved directive parameters accepted by the scanner.
259        limit: usize,
260    },
261    /// A YAML version component exceeded the supported length.
262    YamlVersionTooLong,
263    /// A YAML version component was missing.
264    MissingYamlVersion,
265    /// A tag directive did not end with whitespace or a line break.
266    InvalidTagDirectiveTerminator,
267    /// A tag token did not end with valid separation whitespace.
268    InvalidTagTerminator,
269    /// A tag URI was missing.
270    MissingTagUri,
271    /// A verbatim tag was missing its closing angle bracket.
272    UnclosedVerbatimTag,
273    /// A tag contained an invalid percent escape.
274    InvalidTagEscape,
275    /// A tag escape started with an invalid UTF-8 byte.
276    InvalidTagUtf8LeadingByte,
277    /// A tag escape contained an invalid trailing UTF-8 byte.
278    InvalidTagUtf8TrailingByte,
279    /// A tag escape did not decode to one valid Unicode scalar value.
280    InvalidTagUtf8,
281    /// An anchor or alias name was missing.
282    MissingAnchorOrAliasName,
283    /// A flow collection closing bracket was misplaced.
284    MisplacedFlowCollectionEnd,
285    /// A flow collection was closed with the wrong bracket type.
286    MismatchedFlowCollectionEnd {
287        /// The bracket that opened the flow collection.
288        open: char,
289        /// The bracket that closed the flow collection.
290        close: char,
291    },
292    /// A flow collection was not closed.
293    UnclosedFlowCollection {
294        /// The bracket that opened the flow collection.
295        open: char,
296    },
297    /// A configured flow or block collection nesting limit was exceeded.
298    RecursionLimitExceeded,
299    /// A block entry indicator appeared inside a flow collection.
300    BlockEntryInFlowCollection,
301    /// A block sequence entry appeared in a context that does not allow it.
302    BlockSequenceEntryNotAllowed,
303    /// A block entry indicator was followed by invalid whitespace.
304    InvalidBlockEntryWhitespace,
305    /// A block scalar used an indentation indicator of zero.
306    ZeroBlockScalarIndent,
307    /// A block scalar header did not end with a comment or line break.
308    InvalidBlockScalarHeader,
309    /// Block scalar content began with a tab.
310    TabAtBlockScalarStart,
311    /// A block scalar content line had invalid indentation.
312    InvalidBlockScalarIndent,
313    /// A document indicator appeared inside a quoted scalar.
314    DocumentIndicatorInQuotedScalar,
315    /// A quoted scalar was not closed.
316    UnclosedQuotedScalar,
317    /// A tab was used as indentation.
318    TabInIndentation,
319    /// A multiline quoted scalar had invalid indentation.
320    InvalidQuotedScalarIndent,
321    /// Invalid content followed a single-quoted scalar.
322    InvalidTrailingSingleQuotedScalar,
323    /// Invalid content followed a double-quoted scalar.
324    InvalidTrailingDoubleQuotedScalar,
325    /// A quoted scalar contained an unknown escape character.
326    UnknownQuotedScalarEscape,
327    /// A quoted scalar escape did not contain the expected hexadecimal digits.
328    InvalidQuotedScalarHexEscape,
329    /// A low-surrogate escape did not contain the expected hexadecimal digits.
330    InvalidLowSurrogateHexEscape,
331    /// A surrogate pair contained an invalid low surrogate.
332    InvalidLowSurrogate,
333    /// A high surrogate was not followed by a low surrogate.
334    MissingLowSurrogate,
335    /// A low surrogate appeared without a preceding high surrogate.
336    UnpairedLowSurrogate,
337    /// A quoted scalar escape did not represent a valid Unicode scalar value.
338    InvalidUnicodeEscape,
339    /// A flow scalar started at invalid indentation.
340    InvalidFlowScalarIndent,
341    /// A plain scalar began with a dash followed by a flow indicator.
342    PlainScalarStartsWithDashFlowIndicator,
343    /// A tab appeared where a plain scalar could not accept it.
344    TabInPlainScalar,
345    /// A plain scalar ended before consuming any content.
346    UnexpectedEndOfPlainScalar,
347    /// A mapping key appeared in a context that does not allow one.
348    MappingKeyNotAllowed,
349    /// A flow mapping value indicator was adjacent to a collection start.
350    FlowMappingValueAdjacentCollection,
351    /// A mapping value indicator was followed by invalid whitespace.
352    InvalidMappingValueWhitespace,
353    /// A value indicator was placed illegally in an implicit flow mapping.
354    InvalidColonPlacement,
355    /// A mapping value appeared in a context that does not allow one.
356    MappingValueNotAllowed,
357}
358
359#[cfg(feature = "error_messages")]
360impl fmt::Display for ErrorKind {
361    #[allow(clippy::too_many_lines)]
362    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363        match self {
364            #[cfg(feature = "comments")]
365            Self::TooManyComments => {
366                f.write_str("too many buffered comments while looking ahead")
367            }
368            Self::InputIo { error } => write!(f, "input I/O error: {error}"),
369            Self::InputDecoding { message } => {
370                write!(f, "input decoding error: {message}")
371            }
372            Self::InputByteLimitExceeded { limit } => {
373                write!(f, "input exceeds the configured limit of {limit} bytes")
374            }
375            Self::UnexpectedEofFlowSequence => {
376                f.write_str("unexpected EOF while parsing a flow sequence")
377            }
378            Self::UnexpectedEofFlowMapping => {
379                f.write_str("unexpected EOF while parsing a flow mapping")
380            }
381            Self::UnexpectedEofImplicitFlowMapping => {
382                f.write_str("unexpected EOF while parsing an implicit flow mapping")
383            }
384            Self::UnexpectedEofBlockSequence => {
385                f.write_str("unexpected EOF while parsing a block sequence")
386            }
387            Self::UnexpectedEofBlockMapping => {
388                f.write_str("unexpected EOF while parsing a block mapping")
389            }
390            Self::UnexpectedEof => f.write_str("unexpected eof"),
391            Self::ExpectedStreamStart => f.write_str("did not find expected <stream-start>"),
392            Self::DuplicateVersionDirective => f.write_str("duplicate version directive"),
393            Self::UnsupportedYamlMajorVersion => {
394                f.write_str("unsupported YAML major version")
395            }
396            Self::DuplicateTagDirective => f.write_str(
397                "the TAG directive must only be given at most once per handle in the same document",
398            ),
399            Self::ExpectedDocumentStart => {
400                f.write_str("did not find expected <document start>")
401            }
402            Self::MissingDocumentEndBeforeDirective => {
403                f.write_str("missing explicit document end marker before directive")
404            }
405            Self::AnchorCountOverflow => {
406                f.write_str("while parsing anchor, anchor count exceeded supported limit")
407            }
408            Self::UnknownAnchor => f.write_str("while parsing node, found unknown anchor"),
409            Self::ExpectedNodeContent => {
410                f.write_str("while parsing a node, did not find expected node content")
411            }
412            Self::ExpectedBlockMappingKey => {
413                f.write_str("while parsing a block mapping, did not find expected key")
414            }
415            Self::ExpectedFlowMappingSeparator => {
416                f.write_str("while parsing a flow mapping, did not find expected ',' or '}'")
417            }
418            Self::ExpectedFlowSequenceSeparator => {
419                f.write_str("while parsing a flow sequence, expected ',' or ']'")
420            }
421            Self::ExpectedBlockSequenceEntry => f.write_str(
422                "while parsing a block collection, did not find expected '-' indicator",
423            ),
424            Self::UndeclaredTagHandle => f.write_str("the handle wasn't declared"),
425            Self::MissingIncludeResolver => {
426                f.write_str("No include resolver set for parser stack.")
427            }
428            Self::Custom(message) => f.write_str(message),
429            Self::MultipleDocumentsUnsupported => {
430                f.write_str("multiple documents not supported here")
431            }
432            Self::InputOffsetsWithoutSlice => f.write_str(
433                "internal error: input advertised offsets but did not provide a slice",
434            ),
435            Self::InputSlicingUnavailable => f.write_str(
436                "internal error: input advertised slicing but did not provide a slice",
437            ),
438            Self::ExpectedTagBang => {
439                f.write_str("while scanning a tag, did not find expected '!'")
440            }
441            Self::ExpectedTagDirectiveBang => {
442                f.write_str("while parsing a tag directive, did not find expected '!'")
443            }
444            Self::InvalidGlobalTagCharacter => f.write_str("invalid global tag character"),
445            Self::SimpleKeyExpected => f.write_str("simple key expected ':'"),
446            Self::InvalidSimpleKey => f.write_str("simple key is no longer valid"),
447            Self::InvalidDocumentEnd => {
448                f.write_str("invalid content after document end marker")
449            }
450            Self::InvalidIndentation => f.write_str("invalid indentation"),
451            Self::BomInsideDocument => {
452                f.write_str("a BOM must not appear inside a document")
453            }
454            Self::UnexpectedCharacter { character } => {
455                write!(f, "unexpected character: `{}'", character.escape_default())
456            }
457            Self::TabNotAllowed => f.write_str("tabs disallowed in this context"),
458            Self::TabInBlockIndentation => {
459                f.write_str("tabs disallowed within this context (block indentation)")
460            }
461            Self::CommentInterceptedScalar => {
462                f.write_str("comment intercepting the multiline text")
463            }
464            Self::ExpectedWhitespace => f.write_str("expected whitespace"),
465            Self::CommentNotSeparated => {
466                f.write_str("comments must be separated from other tokens by whitespace")
467            }
468            Self::InvalidDirectiveTerminator => f.write_str(
469                "while scanning a directive, did not find expected comment or line break",
470            ),
471            Self::DirectiveDisallowed => f.write_str("YAML directives are disallowed"),
472            Self::MissingYamlVersionSeparator => f.write_str(
473                "while scanning a YAML directive, did not find expected digit or '.' character",
474            ),
475            Self::MissingDirectiveName => f.write_str(
476                "while scanning a directive, could not find expected directive name",
477            ),
478            Self::InvalidDirectiveName => f.write_str(
479                "while scanning a directive, found unexpected non-alphabetical character",
480            ),
481            Self::DirectiveByteLimitExceeded { limit } => write!(
482                f,
483                "directive exceeds the configured limit of {limit} bytes"
484            ),
485            Self::TooManyReservedDirectiveParams { limit } => write!(
486                f,
487                "reserved directive exceeds the configured limit of {limit} parameters"
488            ),
489            Self::YamlVersionTooLong => {
490                f.write_str("while scanning a YAML directive, found extremely long version number")
491            }
492            Self::MissingYamlVersion => f.write_str(
493                "while scanning a YAML directive, did not find expected version number",
494            ),
495            Self::InvalidTagDirectiveTerminator => {
496                f.write_str("while scanning TAG, did not find expected whitespace or line break")
497            }
498            Self::InvalidTagTerminator => f.write_str(
499                "while scanning a tag, did not find expected whitespace or line break",
500            ),
501            Self::MissingTagUri => {
502                f.write_str("while parsing a tag, did not find expected tag URI")
503            }
504            Self::UnclosedVerbatimTag => {
505                f.write_str("while scanning a verbatim tag, did not find the expected '>'")
506            }
507            Self::InvalidTagEscape => {
508                f.write_str("while parsing a tag, found an invalid escape sequence")
509            }
510            Self::InvalidTagUtf8LeadingByte => {
511                f.write_str("while parsing a tag, found an incorrect leading UTF-8 byte")
512            }
513            Self::InvalidTagUtf8TrailingByte => {
514                f.write_str("while parsing a tag, found an incorrect trailing UTF-8 byte")
515            }
516            Self::InvalidTagUtf8 => {
517                f.write_str("while parsing a tag, found an invalid UTF-8 codepoint")
518            }
519            Self::MissingAnchorOrAliasName => f.write_str(
520                "while scanning an anchor or alias, did not find expected alphabetic or numeric character",
521            ),
522            Self::MisplacedFlowCollectionEnd => f.write_str("misplaced bracket"),
523            Self::MismatchedFlowCollectionEnd { open, close } => {
524                write!(f, "mismatched bracket '{open}' closed by '{close}'")
525            }
526            Self::UnclosedFlowCollection { open } => {
527                write!(f, "unclosed bracket '{open}'")
528            }
529            Self::RecursionLimitExceeded => f.write_str("recursion limit exceeded"),
530            Self::BlockEntryInFlowCollection => {
531                f.write_str(r#""-" is only valid inside a block"#)
532            }
533            Self::BlockSequenceEntryNotAllowed => {
534                f.write_str("block sequence entries are not allowed in this context")
535            }
536            Self::InvalidBlockEntryWhitespace => {
537                f.write_str("'-' must be followed by a valid YAML whitespace")
538            }
539            Self::ZeroBlockScalarIndent => f.write_str(
540                "while scanning a block scalar, found an indentation indicator equal to 0",
541            ),
542            Self::InvalidBlockScalarHeader => f.write_str(
543                "while scanning a block scalar, did not find expected comment or line break",
544            ),
545            Self::TabAtBlockScalarStart => {
546                f.write_str("a block scalar content cannot start with a tab")
547            }
548            Self::InvalidBlockScalarIndent => {
549                f.write_str("wrongly indented line in block scalar")
550            }
551            Self::DocumentIndicatorInQuotedScalar => f.write_str(
552                "while scanning a quoted scalar, found unexpected document indicator",
553            ),
554            Self::UnclosedQuotedScalar => f.write_str("unclosed quote"),
555            Self::TabInIndentation => f.write_str("tab cannot be used as indentation"),
556            Self::InvalidQuotedScalarIndent => {
557                f.write_str("invalid indentation in multiline quoted scalar")
558            }
559            Self::InvalidTrailingSingleQuotedScalar => {
560                f.write_str("invalid trailing content after single-quoted scalar")
561            }
562            Self::InvalidTrailingDoubleQuotedScalar => {
563                f.write_str("invalid trailing content after double-quoted scalar")
564            }
565            Self::UnknownQuotedScalarEscape => {
566                f.write_str("while parsing a quoted scalar, found unknown escape character")
567            }
568            Self::InvalidQuotedScalarHexEscape => f.write_str(
569                "while parsing a quoted scalar, did not find expected hexadecimal number",
570            ),
571            Self::InvalidLowSurrogateHexEscape => f.write_str(
572                "while parsing a quoted scalar, did not find expected hexadecimal number for low surrogate",
573            ),
574            Self::InvalidLowSurrogate => {
575                f.write_str("while parsing a quoted scalar, found invalid low surrogate")
576            }
577            Self::MissingLowSurrogate => f.write_str(
578                "while parsing a quoted scalar, found high surrogate without following low surrogate",
579            ),
580            Self::UnpairedLowSurrogate => {
581                f.write_str("while parsing a quoted scalar, found unpaired low surrogate")
582            }
583            Self::InvalidUnicodeEscape => f.write_str(
584                "while parsing a quoted scalar, found invalid Unicode character escape code",
585            ),
586            Self::InvalidFlowScalarIndent => {
587                f.write_str("invalid indentation in flow construct")
588            }
589            Self::PlainScalarStartsWithDashFlowIndicator => {
590                f.write_str("plain scalar cannot start with '-' followed by ,[]{}")
591            }
592            Self::TabInPlainScalar => {
593                f.write_str("while scanning a plain scalar, found a tab")
594            }
595            Self::UnexpectedEndOfPlainScalar => f.write_str("unexpected end of plain scalar"),
596            Self::MappingKeyNotAllowed => {
597                f.write_str("mapping keys are not allowed in this context")
598            }
599            Self::FlowMappingValueAdjacentCollection => {
600                f.write_str("':' may not precede any of `[{` in flow mapping")
601            }
602            Self::InvalidMappingValueWhitespace => {
603                f.write_str("':' must be followed by a valid YAML whitespace")
604            }
605            Self::InvalidColonPlacement => f.write_str("illegal placement of ':' indicator"),
606            Self::MappingValueNotAllowed => {
607                f.write_str("mapping values are not allowed in this context")
608            }
609        }
610    }
611}
612
613#[cfg(not(feature = "error_messages"))]
614impl fmt::Display for ErrorKind {
615    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
616        f.write_str("")
617    }
618}
619
620/// An error that occurred while reading, scanning, or parsing YAML.
621#[derive(Clone, PartialEq, Debug, Eq)]
622pub struct ScanError {
623    /// The position at which the error happened in the source.
624    mark: Marker,
625    /// Machine-readable error category.
626    kind: ErrorKind,
627    /// Source names captured by a parser stack before its failing entry is removed.
628    source_stack: Vec<String>,
629}
630
631impl ScanError {
632    /// Create an externally supplied error from a location and message.
633    ///
634    /// The message is stored in [`ErrorKind::Custom`]. This is useful for adapters that
635    /// participate in parser APIs, such as a custom [`ParserTrait`](crate::ParserTrait)
636    /// implementation or a [`ParserStack`](crate::ParserStack) include resolver.
637    #[must_use]
638    #[cold]
639    pub fn new(loc: Marker, message: impl Into<String>) -> ScanError {
640        Self::from_kind(loc, ErrorKind::Custom(message.into()))
641    }
642
643    #[must_use]
644    #[cold]
645    pub(crate) fn from_kind(loc: Marker, kind: ErrorKind) -> ScanError {
646        ScanError {
647            mark: loc,
648            kind,
649            source_stack: Vec::new(),
650        }
651    }
652
653    #[must_use]
654    pub(crate) fn with_source_stack(mut self, source_stack: Vec<String>) -> Self {
655        self.source_stack = source_stack;
656        self
657    }
658
659    #[cold]
660    pub(crate) fn into_result<T>(self) -> Result<T, ScanError> {
661        Err(self)
662    }
663
664    /// Return the marker pointing to the error in the source.
665    #[must_use]
666    pub fn marker(&self) -> &Marker {
667        &self.mark
668    }
669
670    /// Return the machine-readable error category.
671    #[must_use]
672    pub fn kind(&self) -> &ErrorKind {
673        &self.kind
674    }
675
676    /// Extract the input I/O error details without cloning them.
677    ///
678    /// # Errors
679    /// Returns the original scan error unchanged when it has a different error category.
680    pub fn try_into_input_io_error(self) -> Result<InputIoError, Self> {
681        let Self {
682            mark,
683            kind,
684            source_stack,
685        } = self;
686
687        match kind {
688            ErrorKind::InputIo { error } => Ok(error),
689            kind => Err(Self {
690                mark,
691                kind,
692                source_stack,
693            }),
694        }
695    }
696
697    /// Return source names captured by a parser stack, from bottom to top.
698    #[must_use]
699    pub fn source_stack(&self) -> &[String] {
700        &self.source_stack
701    }
702
703    /// Render the error as a human-readable description.
704    ///
705    /// Parser-stack errors include their nested source names. The result remains
706    /// empty when the `error_messages` feature is disabled.
707    #[must_use]
708    pub fn info(&self) -> String {
709        let mut info = self.kind.to_string();
710        if !info.is_empty() && self.source_stack().len() > 1 {
711            info.push_str("\nwhile parsing ");
712            info.push_str(&self.source_stack().join(" -> "));
713        }
714        info
715    }
716}
717
718impl fmt::Display for ScanError {
719    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
720        write!(
721            f,
722            "{} at char {} line {} column {}",
723            self.info(),
724            self.mark.index(),
725            self.mark.line(),
726            self.mark.col() + 1
727        )
728    }
729}
730
731impl core::error::Error for ScanError {
732    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
733        match &self.kind {
734            ErrorKind::InputIo { error } => Some(error),
735            _ => None,
736        }
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    #[cfg(feature = "error_messages")]
743    use alloc::format;
744    #[cfg(feature = "error_messages")]
745    use alloc::string::String;
746    use alloc::string::ToString;
747
748    use super::{ErrorKind, InputIoError, ScanError};
749    use crate::scanner::Marker;
750
751    #[cfg(feature = "error_messages")]
752    #[test]
753    fn constructor_retains_kind_and_derives_info() {
754        let marker = Marker::new(3, 2, 1);
755        let error = ScanError::from_kind(marker, ErrorKind::ExpectedWhitespace);
756
757        assert_eq!(error.kind(), &ErrorKind::ExpectedWhitespace);
758        assert_eq!(error.kind().to_string(), "expected whitespace");
759        assert_eq!(error.info(), "expected whitespace");
760    }
761
762    #[cfg(feature = "error_messages")]
763    #[test]
764    fn parameterized_kind_constructs_info() {
765        let marker = Marker::new(3, 2, 1);
766        let error = ScanError::from_kind(
767            marker,
768            ErrorKind::MismatchedFlowCollectionEnd {
769                open: '[',
770                close: '}',
771            },
772        );
773
774        assert_eq!(error.info(), "mismatched bracket '[' closed by '}'");
775        assert_eq!(
776            format!("{error}"),
777            "mismatched bracket '[' closed by '}' at char 3 line 2 column 2"
778        );
779    }
780
781    #[cfg(feature = "error_messages")]
782    #[test]
783    fn input_error_kinds_construct_info() {
784        assert_eq!(
785            ErrorKind::InputIo {
786                error: InputIoError::from_message("connection reset")
787            }
788            .to_string(),
789            "input I/O error: connection reset"
790        );
791        assert_eq!(
792            ErrorKind::InputDecoding {
793                message: String::from("invalid utf-8")
794            }
795            .to_string(),
796            "input decoding error: invalid utf-8"
797        );
798        assert_eq!(
799            ErrorKind::InputByteLimitExceeded { limit: 4096 }.to_string(),
800            "input exceeds the configured limit of 4096 bytes"
801        );
802    }
803
804    #[test]
805    fn message_only_input_io_error_has_no_source() {
806        use core::error::Error as _;
807
808        let error = InputIoError::from_message("portable failure");
809
810        assert_eq!(error.message(), "portable failure");
811        assert!(error.source().is_none());
812    }
813
814    #[cfg(feature = "std")]
815    #[test]
816    fn std_input_io_error_is_retained_in_scan_error_source_chain() {
817        use core::error::Error as _;
818        use std::io;
819
820        let details = InputIoError::from(io::Error::new(io::ErrorKind::BrokenPipe, "pipe closed"));
821        assert_eq!(details.message(), "pipe closed");
822        assert_eq!(
823            details
824                .io_error()
825                .expect("std construction should retain io::Error")
826                .kind(),
827            io::ErrorKind::BrokenPipe
828        );
829
830        let error = ScanError::from_kind(
831            Marker::new(3, 2, 1),
832            ErrorKind::InputIo {
833                error: details.clone(),
834            },
835        );
836        let input_error = error
837            .source()
838            .and_then(|source| source.downcast_ref::<InputIoError>())
839            .expect("ScanError should expose InputIoError as its source");
840        let io_error = input_error
841            .source()
842            .and_then(|source| source.downcast_ref::<io::Error>())
843            .expect("InputIoError should expose the retained io::Error");
844
845        assert_eq!(io_error.kind(), io::ErrorKind::BrokenPipe);
846        assert_eq!(details, *input_error);
847    }
848
849    #[cfg(feature = "std")]
850    #[test]
851    fn unique_std_input_io_error_can_be_recovered() {
852        use std::io;
853
854        let details = InputIoError::from(io::Error::from_raw_os_error(12_345));
855        let error = details
856            .try_into_io_error()
857            .expect("a uniquely owned io::Error should be recoverable");
858
859        assert_eq!(error.raw_os_error(), Some(12_345));
860    }
861
862    #[cfg(feature = "std")]
863    #[test]
864    fn shared_std_input_io_error_can_be_recovered_after_other_clone_is_dropped() {
865        use std::io;
866
867        let details = InputIoError::from(io::Error::from_raw_os_error(12_345));
868        let other = details.clone();
869        let details = details
870            .try_into_io_error()
871            .expect_err("a shared io::Error cannot be moved out");
872
873        drop(other);
874
875        let error = details
876            .try_into_io_error()
877            .expect("the last owner should recover the io::Error");
878        assert_eq!(error.raw_os_error(), Some(12_345));
879    }
880
881    #[cfg(feature = "std")]
882    #[test]
883    fn scan_error_moves_input_io_error_out_without_cloning() {
884        use std::io;
885
886        let error = ScanError::from_kind(
887            Marker::new(3, 2, 1),
888            ErrorKind::InputIo {
889                error: InputIoError::from(io::Error::from_raw_os_error(12_345)),
890            },
891        );
892        let details = error
893            .try_into_input_io_error()
894            .expect("input I/O details should be extractable");
895        let error = details
896            .try_into_io_error()
897            .expect("extracting the scan error should retain unique ownership");
898
899        assert_eq!(error.raw_os_error(), Some(12_345));
900    }
901
902    #[test]
903    fn extracting_input_io_error_preserves_other_scan_errors() {
904        let error = ScanError::from_kind(Marker::new(3, 2, 1), ErrorKind::ExpectedWhitespace);
905        let error = error
906            .try_into_input_io_error()
907            .expect_err("a non-I/O scan error should be returned unchanged");
908
909        assert_eq!(error.marker(), &Marker::new(3, 2, 1));
910        assert_eq!(error.kind(), &ErrorKind::ExpectedWhitespace);
911    }
912
913    #[cfg(feature = "error_messages")]
914    #[test]
915    fn public_constructor_copies_custom_message() {
916        let marker = Marker::new(3, 2, 1);
917        let mut message = String::from("adapter failed");
918        let error = ScanError::new(marker, &message);
919        message.clear();
920
921        assert_eq!(
922            error.kind(),
923            &ErrorKind::Custom(String::from("adapter failed"))
924        );
925        assert_eq!(error.info(), "adapter failed");
926    }
927
928    #[cfg(not(feature = "error_messages"))]
929    #[test]
930    fn disabled_error_messages_are_empty() {
931        let marker = Marker::new(3, 2, 1);
932        let error = ScanError::from_kind(
933            marker,
934            ErrorKind::MismatchedFlowCollectionEnd {
935                open: '[',
936                close: '}',
937            },
938        );
939
940        assert_eq!(error.kind().to_string(), "");
941        assert_eq!(error.info(), "");
942    }
943}