use std::fmt;
use serde::de::DeserializeOwned;
#[derive(Clone, Copy, Debug, Default)]
pub struct DecodePolicy {
pub ignore_unknown_tags: bool,
pub preserve_merge_keys: bool,
}
#[derive(Debug)]
pub enum DecodeError {
InputTooLarge {
actual: usize,
maximum: usize,
},
Utf8(std::str::Utf8Error),
InvalidDocument {
message: String,
},
}
impl fmt::Display for DecodeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InputTooLarge { actual, maximum } => {
write!(
formatter,
"YAML input is {actual} bytes; maximum is {maximum}"
)
}
Self::Utf8(error) => write!(formatter, "YAML input is not UTF-8: {error}"),
Self::InvalidDocument { message } => write!(formatter, "invalid YAML: {message}"),
}
}
}
impl std::error::Error for DecodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Utf8(error) => Some(error),
_ => None,
}
}
}
pub fn from_slice<T: DeserializeOwned>(
bytes: &[u8],
maximum_input_bytes: usize,
policy: DecodePolicy,
) -> Result<T, DecodeError> {
if bytes.len() > maximum_input_bytes {
return Err(DecodeError::InputTooLarge {
actual: bytes.len(),
maximum: maximum_input_bytes,
});
}
let source = std::str::from_utf8(bytes).map_err(DecodeError::Utf8)?;
let document: serde_yaml::Value =
serde_yaml::from_str(source).map_err(|error| parse_error(&error))?;
admit_document(&document, policy)?;
serde_yaml::from_value(document).map_err(|error| parse_error(&error))
}
fn parse_error(error: &serde_yaml::Error) -> DecodeError {
DecodeError::InvalidDocument {
message: error.to_string(),
}
}
fn admit_document(document: &serde_yaml::Value, policy: DecodePolicy) -> Result<(), DecodeError> {
use serde_yaml::Value;
let mut pending = vec![document];
while let Some(value) = pending.pop() {
match value {
Value::Tagged(tagged) => {
if !policy.ignore_unknown_tags {
return Err(DecodeError::InvalidDocument {
message: "unsupported YAML tag".into(),
});
}
pending.push(&tagged.value);
}
Value::Mapping(mapping) => {
for (key, value) in mapping {
if !policy.preserve_merge_keys && key.as_str() == Some("<<") {
return Err(DecodeError::InvalidDocument {
message: "YAML merge keys are unsupported".into(),
});
}
pending.push(key);
pending.push(value);
}
}
Value::Sequence(sequence) => pending.extend(sequence),
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
}
Ok(())
}
#[cfg(test)]
mod tests;