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