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