#[cfg(feature = "std")]
use alloc::sync::Arc;
use alloc::{
string::{String, ToString},
vec::Vec,
};
use core::fmt;
use crate::scanner::Marker;
#[derive(Clone, Debug)]
pub struct InputIoError {
message: String,
#[cfg(feature = "std")]
source: Option<Arc<std::io::Error>>,
}
impl InputIoError {
#[must_use]
pub fn from_message(message: impl Into<String>) -> Self {
Self {
message: message.into(),
#[cfg(feature = "std")]
source: None,
}
}
#[cfg(feature = "std")]
#[must_use]
pub fn from_io(error: std::io::Error) -> Self {
Self {
message: error.to_string(),
source: Some(Arc::new(error)),
}
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[cfg(feature = "std")]
#[must_use]
pub fn io_error(&self) -> Option<&std::io::Error> {
self.source.as_deref()
}
#[cfg(feature = "std")]
pub fn try_into_io_error(self) -> Result<std::io::Error, Self> {
let Self { message, source } = self;
let Some(source) = source else {
return Err(Self {
message,
source: None,
});
};
match Arc::try_unwrap(source) {
Ok(error) => Ok(error),
Err(source) => Err(Self {
message,
source: Some(source),
}),
}
}
}
#[cfg(feature = "std")]
impl From<std::io::Error> for InputIoError {
fn from(error: std::io::Error) -> Self {
Self::from_io(error)
}
}
impl PartialEq for InputIoError {
fn eq(&self, other: &Self) -> bool {
self.message == other.message
}
}
impl Eq for InputIoError {}
impl core::hash::Hash for InputIoError {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(&self.message, state);
}
}
impl fmt::Display for InputIoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl core::error::Error for InputIoError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
#[cfg(feature = "std")]
{
self.source
.as_deref()
.map(|error| error as &(dyn core::error::Error + 'static))
}
#[cfg(not(feature = "std"))]
{
None
}
}
}
#[derive(Clone, PartialEq, Debug, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
TooManyComments,
InputIo {
error: InputIoError,
},
InputDecoding {
message: String,
},
InputByteLimitExceeded {
limit: usize,
},
UnexpectedEofFlowSequence,
UnexpectedEofFlowMapping,
UnexpectedEofImplicitFlowMapping,
UnexpectedEofBlockSequence,
UnexpectedEofBlockMapping,
UnexpectedEof,
ExpectedStreamStart,
DuplicateVersionDirective,
UnsupportedYamlMajorVersion,
DuplicateTagDirective,
ExpectedDocumentStart,
MissingDocumentEndBeforeDirective,
AnchorCountOverflow,
UnknownAnchor,
ExpectedNodeContent,
ExpectedBlockMappingKey,
ExpectedFlowMappingSeparator,
ExpectedFlowSequenceSeparator,
ExpectedBlockSequenceEntry,
UndeclaredTagHandle,
MissingIncludeResolver,
Custom(String),
MultipleDocumentsUnsupported,
InputOffsetsWithoutSlice,
InputSlicingUnavailable,
ExpectedTagBang,
ExpectedTagDirectiveBang,
InvalidGlobalTagCharacter,
SimpleKeyExpected,
InvalidSimpleKey,
InvalidDocumentEnd,
InvalidIndentation,
BomInsideDocument,
UnexpectedCharacter {
character: char,
},
TabNotAllowed,
TabInBlockIndentation,
CommentInterceptedScalar,
ExpectedWhitespace,
CommentNotSeparated,
InvalidDirectiveTerminator,
MissingYamlVersionSeparator,
MissingDirectiveName,
InvalidDirectiveName,
YamlVersionTooLong,
MissingYamlVersion,
InvalidTagDirectiveTerminator,
InvalidTagTerminator,
MissingTagUri,
UnclosedVerbatimTag,
InvalidTagEscape,
InvalidTagUtf8LeadingByte,
InvalidTagUtf8TrailingByte,
InvalidTagUtf8,
MissingAnchorOrAliasName,
MisplacedFlowCollectionEnd,
MismatchedFlowCollectionEnd {
open: char,
close: char,
},
UnclosedFlowCollection {
open: char,
},
RecursionLimitExceeded,
BlockEntryInFlowCollection,
BlockSequenceEntryNotAllowed,
InvalidBlockEntryWhitespace,
ZeroBlockScalarIndent,
InvalidBlockScalarHeader,
TabAtBlockScalarStart,
InvalidBlockScalarIndent,
DocumentIndicatorInQuotedScalar,
UnclosedQuotedScalar,
TabInIndentation,
InvalidQuotedScalarIndent,
InvalidTrailingSingleQuotedScalar,
InvalidTrailingDoubleQuotedScalar,
UnknownQuotedScalarEscape,
InvalidQuotedScalarHexEscape,
InvalidLowSurrogateHexEscape,
InvalidLowSurrogate,
MissingLowSurrogate,
UnpairedLowSurrogate,
InvalidUnicodeEscape,
InvalidFlowScalarIndent,
PlainScalarStartsWithDashFlowIndicator,
TabInPlainScalar,
UnexpectedEndOfPlainScalar,
MappingKeyNotAllowed,
FlowMappingValueAdjacentCollection,
InvalidMappingValueWhitespace,
InvalidColonPlacement,
MappingValueNotAllowed,
}
#[cfg(feature = "error_messages")]
impl fmt::Display for ErrorKind {
#[allow(clippy::too_many_lines)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooManyComments => {
f.write_str("too many consecutive comments before resolving collection entry")
}
Self::InputIo { error } => write!(f, "input I/O error: {error}"),
Self::InputDecoding { message } => {
write!(f, "input decoding error: {message}")
}
Self::InputByteLimitExceeded { limit } => {
write!(f, "input exceeds the configured limit of {limit} bytes")
}
Self::UnexpectedEofFlowSequence => {
f.write_str("unexpected EOF while parsing a flow sequence")
}
Self::UnexpectedEofFlowMapping => {
f.write_str("unexpected EOF while parsing a flow mapping")
}
Self::UnexpectedEofImplicitFlowMapping => {
f.write_str("unexpected EOF while parsing an implicit flow mapping")
}
Self::UnexpectedEofBlockSequence => {
f.write_str("unexpected EOF while parsing a block sequence")
}
Self::UnexpectedEofBlockMapping => {
f.write_str("unexpected EOF while parsing a block mapping")
}
Self::UnexpectedEof => f.write_str("unexpected eof"),
Self::ExpectedStreamStart => f.write_str("did not find expected <stream-start>"),
Self::DuplicateVersionDirective => f.write_str("duplicate version directive"),
Self::UnsupportedYamlMajorVersion => {
f.write_str("unsupported YAML major version")
}
Self::DuplicateTagDirective => f.write_str(
"the TAG directive must only be given at most once per handle in the same document",
),
Self::ExpectedDocumentStart => {
f.write_str("did not find expected <document start>")
}
Self::MissingDocumentEndBeforeDirective => {
f.write_str("missing explicit document end marker before directive")
}
Self::AnchorCountOverflow => {
f.write_str("while parsing anchor, anchor count exceeded supported limit")
}
Self::UnknownAnchor => f.write_str("while parsing node, found unknown anchor"),
Self::ExpectedNodeContent => {
f.write_str("while parsing a node, did not find expected node content")
}
Self::ExpectedBlockMappingKey => {
f.write_str("while parsing a block mapping, did not find expected key")
}
Self::ExpectedFlowMappingSeparator => {
f.write_str("while parsing a flow mapping, did not find expected ',' or '}'")
}
Self::ExpectedFlowSequenceSeparator => {
f.write_str("while parsing a flow sequence, expected ',' or ']'")
}
Self::ExpectedBlockSequenceEntry => f.write_str(
"while parsing a block collection, did not find expected '-' indicator",
),
Self::UndeclaredTagHandle => f.write_str("the handle wasn't declared"),
Self::MissingIncludeResolver => {
f.write_str("No include resolver set for parser stack.")
}
Self::Custom(message) => f.write_str(message),
Self::MultipleDocumentsUnsupported => {
f.write_str("multiple documents not supported here")
}
Self::InputOffsetsWithoutSlice => f.write_str(
"internal error: input advertised offsets but did not provide a slice",
),
Self::InputSlicingUnavailable => f.write_str(
"internal error: input advertised slicing but did not provide a slice",
),
Self::ExpectedTagBang => {
f.write_str("while scanning a tag, did not find expected '!'")
}
Self::ExpectedTagDirectiveBang => {
f.write_str("while parsing a tag directive, did not find expected '!'")
}
Self::InvalidGlobalTagCharacter => f.write_str("invalid global tag character"),
Self::SimpleKeyExpected => f.write_str("simple key expected ':'"),
Self::InvalidSimpleKey => f.write_str("simple key is no longer valid"),
Self::InvalidDocumentEnd => {
f.write_str("invalid content after document end marker")
}
Self::InvalidIndentation => f.write_str("invalid indentation"),
Self::BomInsideDocument => {
f.write_str("a BOM must not appear inside a document")
}
Self::UnexpectedCharacter { character } => {
write!(f, "unexpected character: `{}'", character.escape_default())
}
Self::TabNotAllowed => f.write_str("tabs disallowed in this context"),
Self::TabInBlockIndentation => {
f.write_str("tabs disallowed within this context (block indentation)")
}
Self::CommentInterceptedScalar => {
f.write_str("comment intercepting the multiline text")
}
Self::ExpectedWhitespace => f.write_str("expected whitespace"),
Self::CommentNotSeparated => {
f.write_str("comments must be separated from other tokens by whitespace")
}
Self::InvalidDirectiveTerminator => f.write_str(
"while scanning a directive, did not find expected comment or line break",
),
Self::MissingYamlVersionSeparator => f.write_str(
"while scanning a YAML directive, did not find expected digit or '.' character",
),
Self::MissingDirectiveName => f.write_str(
"while scanning a directive, could not find expected directive name",
),
Self::InvalidDirectiveName => f.write_str(
"while scanning a directive, found unexpected non-alphabetical character",
),
Self::YamlVersionTooLong => {
f.write_str("while scanning a YAML directive, found extremely long version number")
}
Self::MissingYamlVersion => f.write_str(
"while scanning a YAML directive, did not find expected version number",
),
Self::InvalidTagDirectiveTerminator => {
f.write_str("while scanning TAG, did not find expected whitespace or line break")
}
Self::InvalidTagTerminator => f.write_str(
"while scanning a tag, did not find expected whitespace or line break",
),
Self::MissingTagUri => {
f.write_str("while parsing a tag, did not find expected tag URI")
}
Self::UnclosedVerbatimTag => {
f.write_str("while scanning a verbatim tag, did not find the expected '>'")
}
Self::InvalidTagEscape => {
f.write_str("while parsing a tag, found an invalid escape sequence")
}
Self::InvalidTagUtf8LeadingByte => {
f.write_str("while parsing a tag, found an incorrect leading UTF-8 byte")
}
Self::InvalidTagUtf8TrailingByte => {
f.write_str("while parsing a tag, found an incorrect trailing UTF-8 byte")
}
Self::InvalidTagUtf8 => {
f.write_str("while parsing a tag, found an invalid UTF-8 codepoint")
}
Self::MissingAnchorOrAliasName => f.write_str(
"while scanning an anchor or alias, did not find expected alphabetic or numeric character",
),
Self::MisplacedFlowCollectionEnd => f.write_str("misplaced bracket"),
Self::MismatchedFlowCollectionEnd { open, close } => {
write!(f, "mismatched bracket '{open}' closed by '{close}'")
}
Self::UnclosedFlowCollection { open } => {
write!(f, "unclosed bracket '{open}'")
}
Self::RecursionLimitExceeded => f.write_str("recursion limit exceeded"),
Self::BlockEntryInFlowCollection => {
f.write_str(r#""-" is only valid inside a block"#)
}
Self::BlockSequenceEntryNotAllowed => {
f.write_str("block sequence entries are not allowed in this context")
}
Self::InvalidBlockEntryWhitespace => {
f.write_str("'-' must be followed by a valid YAML whitespace")
}
Self::ZeroBlockScalarIndent => f.write_str(
"while scanning a block scalar, found an indentation indicator equal to 0",
),
Self::InvalidBlockScalarHeader => f.write_str(
"while scanning a block scalar, did not find expected comment or line break",
),
Self::TabAtBlockScalarStart => {
f.write_str("a block scalar content cannot start with a tab")
}
Self::InvalidBlockScalarIndent => {
f.write_str("wrongly indented line in block scalar")
}
Self::DocumentIndicatorInQuotedScalar => f.write_str(
"while scanning a quoted scalar, found unexpected document indicator",
),
Self::UnclosedQuotedScalar => f.write_str("unclosed quote"),
Self::TabInIndentation => f.write_str("tab cannot be used as indentation"),
Self::InvalidQuotedScalarIndent => {
f.write_str("invalid indentation in multiline quoted scalar")
}
Self::InvalidTrailingSingleQuotedScalar => {
f.write_str("invalid trailing content after single-quoted scalar")
}
Self::InvalidTrailingDoubleQuotedScalar => {
f.write_str("invalid trailing content after double-quoted scalar")
}
Self::UnknownQuotedScalarEscape => {
f.write_str("while parsing a quoted scalar, found unknown escape character")
}
Self::InvalidQuotedScalarHexEscape => f.write_str(
"while parsing a quoted scalar, did not find expected hexadecimal number",
),
Self::InvalidLowSurrogateHexEscape => f.write_str(
"while parsing a quoted scalar, did not find expected hexadecimal number for low surrogate",
),
Self::InvalidLowSurrogate => {
f.write_str("while parsing a quoted scalar, found invalid low surrogate")
}
Self::MissingLowSurrogate => f.write_str(
"while parsing a quoted scalar, found high surrogate without following low surrogate",
),
Self::UnpairedLowSurrogate => {
f.write_str("while parsing a quoted scalar, found unpaired low surrogate")
}
Self::InvalidUnicodeEscape => f.write_str(
"while parsing a quoted scalar, found invalid Unicode character escape code",
),
Self::InvalidFlowScalarIndent => {
f.write_str("invalid indentation in flow construct")
}
Self::PlainScalarStartsWithDashFlowIndicator => {
f.write_str("plain scalar cannot start with '-' followed by ,[]{}")
}
Self::TabInPlainScalar => {
f.write_str("while scanning a plain scalar, found a tab")
}
Self::UnexpectedEndOfPlainScalar => f.write_str("unexpected end of plain scalar"),
Self::MappingKeyNotAllowed => {
f.write_str("mapping keys are not allowed in this context")
}
Self::FlowMappingValueAdjacentCollection => {
f.write_str("':' may not precede any of `[{` in flow mapping")
}
Self::InvalidMappingValueWhitespace => {
f.write_str("':' must be followed by a valid YAML whitespace")
}
Self::InvalidColonPlacement => f.write_str("illegal placement of ':' indicator"),
Self::MappingValueNotAllowed => {
f.write_str("mapping values are not allowed in this context")
}
}
}
}
#[cfg(not(feature = "error_messages"))]
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("")
}
}
#[derive(Clone, PartialEq, Debug, Eq)]
pub struct ScanError {
mark: Marker,
kind: ErrorKind,
source_stack: Vec<String>,
}
impl ScanError {
#[must_use]
#[cold]
pub fn new(loc: Marker, message: impl Into<String>) -> ScanError {
Self::from_kind(loc, ErrorKind::Custom(message.into()))
}
#[must_use]
#[cold]
pub(crate) fn from_kind(loc: Marker, kind: ErrorKind) -> ScanError {
ScanError {
mark: loc,
kind,
source_stack: Vec::new(),
}
}
#[must_use]
pub(crate) fn with_source_stack(mut self, source_stack: Vec<String>) -> Self {
self.source_stack = source_stack;
self
}
#[cold]
pub(crate) fn into_result<T>(self) -> Result<T, ScanError> {
Err(self)
}
#[must_use]
pub fn marker(&self) -> &Marker {
&self.mark
}
#[must_use]
pub fn kind(&self) -> &ErrorKind {
&self.kind
}
pub fn try_into_input_io_error(self) -> Result<InputIoError, Self> {
let Self {
mark,
kind,
source_stack,
} = self;
match kind {
ErrorKind::InputIo { error } => Ok(error),
kind => Err(Self {
mark,
kind,
source_stack,
}),
}
}
#[must_use]
pub fn source_stack(&self) -> &[String] {
&self.source_stack
}
#[must_use]
pub fn info(&self) -> String {
let mut info = self.kind.to_string();
if !info.is_empty() && self.source_stack().len() > 1 {
info.push_str("\nwhile parsing ");
info.push_str(&self.source_stack().join(" -> "));
}
info
}
}
impl fmt::Display for ScanError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} at char {} line {} column {}",
self.info(),
self.mark.index(),
self.mark.line(),
self.mark.col() + 1
)
}
}
impl core::error::Error for ScanError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match &self.kind {
ErrorKind::InputIo { error } => Some(error),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "error_messages")]
use alloc::format;
#[cfg(feature = "error_messages")]
use alloc::string::String;
use alloc::string::ToString;
use super::{ErrorKind, InputIoError, ScanError};
use crate::scanner::Marker;
#[cfg(feature = "error_messages")]
#[test]
fn constructor_retains_kind_and_derives_info() {
let marker = Marker::new(3, 2, 1);
let error = ScanError::from_kind(marker, ErrorKind::ExpectedWhitespace);
assert_eq!(error.kind(), &ErrorKind::ExpectedWhitespace);
assert_eq!(error.kind().to_string(), "expected whitespace");
assert_eq!(error.info(), "expected whitespace");
}
#[cfg(feature = "error_messages")]
#[test]
fn parameterized_kind_constructs_info() {
let marker = Marker::new(3, 2, 1);
let error = ScanError::from_kind(
marker,
ErrorKind::MismatchedFlowCollectionEnd {
open: '[',
close: '}',
},
);
assert_eq!(error.info(), "mismatched bracket '[' closed by '}'");
assert_eq!(
format!("{error}"),
"mismatched bracket '[' closed by '}' at char 3 line 2 column 2"
);
}
#[cfg(feature = "error_messages")]
#[test]
fn input_error_kinds_construct_info() {
assert_eq!(
ErrorKind::InputIo {
error: InputIoError::from_message("connection reset")
}
.to_string(),
"input I/O error: connection reset"
);
assert_eq!(
ErrorKind::InputDecoding {
message: String::from("invalid utf-8")
}
.to_string(),
"input decoding error: invalid utf-8"
);
assert_eq!(
ErrorKind::InputByteLimitExceeded { limit: 4096 }.to_string(),
"input exceeds the configured limit of 4096 bytes"
);
}
#[test]
fn message_only_input_io_error_has_no_source() {
use core::error::Error as _;
let error = InputIoError::from_message("portable failure");
assert_eq!(error.message(), "portable failure");
assert!(error.source().is_none());
}
#[cfg(feature = "std")]
#[test]
fn std_input_io_error_is_retained_in_scan_error_source_chain() {
use core::error::Error as _;
use std::io;
let details = InputIoError::from(io::Error::new(io::ErrorKind::BrokenPipe, "pipe closed"));
assert_eq!(details.message(), "pipe closed");
assert_eq!(
details
.io_error()
.expect("std construction should retain io::Error")
.kind(),
io::ErrorKind::BrokenPipe
);
let error = ScanError::from_kind(
Marker::new(3, 2, 1),
ErrorKind::InputIo {
error: details.clone(),
},
);
let input_error = error
.source()
.and_then(|source| source.downcast_ref::<InputIoError>())
.expect("ScanError should expose InputIoError as its source");
let io_error = input_error
.source()
.and_then(|source| source.downcast_ref::<io::Error>())
.expect("InputIoError should expose the retained io::Error");
assert_eq!(io_error.kind(), io::ErrorKind::BrokenPipe);
assert_eq!(details, *input_error);
}
#[cfg(feature = "std")]
#[test]
fn unique_std_input_io_error_can_be_recovered() {
use std::io;
let details = InputIoError::from(io::Error::from_raw_os_error(12_345));
let error = details
.try_into_io_error()
.expect("a uniquely owned io::Error should be recoverable");
assert_eq!(error.raw_os_error(), Some(12_345));
}
#[cfg(feature = "std")]
#[test]
fn shared_std_input_io_error_can_be_recovered_after_other_clone_is_dropped() {
use std::io;
let details = InputIoError::from(io::Error::from_raw_os_error(12_345));
let other = details.clone();
let details = details
.try_into_io_error()
.expect_err("a shared io::Error cannot be moved out");
drop(other);
let error = details
.try_into_io_error()
.expect("the last owner should recover the io::Error");
assert_eq!(error.raw_os_error(), Some(12_345));
}
#[cfg(feature = "std")]
#[test]
fn scan_error_moves_input_io_error_out_without_cloning() {
use std::io;
let error = ScanError::from_kind(
Marker::new(3, 2, 1),
ErrorKind::InputIo {
error: InputIoError::from(io::Error::from_raw_os_error(12_345)),
},
);
let details = error
.try_into_input_io_error()
.expect("input I/O details should be extractable");
let error = details
.try_into_io_error()
.expect("extracting the scan error should retain unique ownership");
assert_eq!(error.raw_os_error(), Some(12_345));
}
#[test]
fn extracting_input_io_error_preserves_other_scan_errors() {
let error = ScanError::from_kind(Marker::new(3, 2, 1), ErrorKind::ExpectedWhitespace);
let error = error
.try_into_input_io_error()
.expect_err("a non-I/O scan error should be returned unchanged");
assert_eq!(error.marker(), &Marker::new(3, 2, 1));
assert_eq!(error.kind(), &ErrorKind::ExpectedWhitespace);
}
#[cfg(feature = "error_messages")]
#[test]
fn public_constructor_copies_custom_message() {
let marker = Marker::new(3, 2, 1);
let mut message = String::from("adapter failed");
let error = ScanError::new(marker, &message);
message.clear();
assert_eq!(
error.kind(),
&ErrorKind::Custom(String::from("adapter failed"))
);
assert_eq!(error.info(), "adapter failed");
}
#[cfg(not(feature = "error_messages"))]
#[test]
fn disabled_error_messages_are_empty() {
let marker = Marker::new(3, 2, 1);
let error = ScanError::from_kind(
marker,
ErrorKind::MismatchedFlowCollectionEnd {
open: '[',
close: '}',
},
);
assert!(error.kind().to_string().is_empty());
assert!(error.info().is_empty());
}
}