use std::collections::HashMap;
use yaml_rust2::parser::{Event, MarkedEventReceiver, Parser, Tag};
use yaml_rust2::scanner::{Marker, ScanError, TScalarStyle};
use crate::WriteError;
use crate::document::{Doc, Value};
use crate::error::{DocumentError, OmnistError, ParseError};
use crate::formats::float_fmt;
use crate::formats::int_cap::{MAX_INT_DIGITS, over_cap_message};
use crate::formats::string_escape::{YAML_ESCAPES, write_quoted};
use crate::report::{Severity, WriteReport};
use indexmap::IndexMap;
use num_bigint::BigInt;
const MAX_MATERIALIZED_NODES: usize = 100_000;
fn count_nodes(node: &Raw) -> usize {
match node {
Raw::Scalar(..) => 1,
Raw::Sequence(items) => 1 + items.iter().map(count_nodes).sum::<usize>(),
Raw::Mapping(entries) => {
1 + entries
.iter()
.map(|(k, v)| count_nodes(k) + count_nodes(v))
.sum::<usize>()
}
}
}
#[derive(Debug, Clone)]
enum Raw {
Scalar(String, TScalarStyle, Option<Tag>),
Sequence(Vec<Raw>),
Mapping(Vec<(Raw, Raw)>),
}
struct Builder {
doc_stack: Vec<(Raw, usize)>,
key_stack: Vec<Option<Raw>>,
anchor_map: HashMap<usize, Raw>,
docs: Vec<Raw>,
node_count: usize,
error: Option<ParseError>,
}
impl Builder {
fn new() -> Self {
Builder {
doc_stack: Vec::new(),
key_stack: Vec::new(),
anchor_map: HashMap::new(),
docs: Vec::new(),
node_count: 0,
error: None,
}
}
fn charge(&mut self, n: usize, mark: Marker) -> bool {
if self.error.is_some() {
return false;
}
self.node_count = self.node_count.saturating_add(n);
if self.node_count > MAX_MATERIALIZED_NODES {
self.error = Some(ParseError::new(
mark.line(),
mark.col() + 1,
format!(
"invalid YAML: document materializes more than \
{MAX_MATERIALIZED_NODES} nodes (security: unbounded anchor/alias \
expansion can amplify a small document into an enormous tree, \
independent of nesting depth)"
),
));
return false;
}
true
}
fn insert(&mut self, node: Raw, aid: usize, _mark: Marker) {
if aid > 0 {
self.anchor_map.insert(aid, node.clone());
}
match self.doc_stack.last_mut() {
None => self.doc_stack.push((node, aid)),
Some((Raw::Sequence(items), _)) => items.push(node),
Some((Raw::Mapping(_), _)) => {
let cur_key = self
.key_stack
.last_mut()
.expect("a Mapping is only ever pushed alongside a matching key_stack entry");
match cur_key.take() {
None => *cur_key = Some(node),
Some(k) => {
if let Some((Raw::Mapping(entries), _)) = self.doc_stack.last_mut() {
entries.push((k, node));
}
}
}
}
Some((Raw::Scalar(..), _)) => {
unreachable!("a Scalar is never a container on doc_stack")
}
}
}
fn on_event_impl(&mut self, ev: Event, mark: Marker) {
if self.error.is_some() {
return;
}
match ev {
Event::Nothing | Event::StreamStart | Event::StreamEnd | Event::DocumentStart => {}
Event::DocumentEnd => match self.doc_stack.len() {
0 => self
.docs
.push(Raw::Scalar(String::new(), TScalarStyle::Plain, None)),
1 => self.docs.push(self.doc_stack.pop().unwrap().0),
_ => unreachable!("a single document's stack never nests more than one root"),
},
Event::SequenceStart(aid, _) => {
if !self.charge(1, mark) {
return;
}
self.doc_stack.push((Raw::Sequence(Vec::new()), aid));
}
Event::SequenceEnd => {
let (node, aid) = self.doc_stack.pop().expect("matched by SequenceStart");
self.insert(node, aid, mark);
}
Event::MappingStart(aid, _) => {
if !self.charge(1, mark) {
return;
}
self.doc_stack.push((Raw::Mapping(Vec::new()), aid));
self.key_stack.push(None);
}
Event::MappingEnd => {
let (node, aid) = self.doc_stack.pop().expect("matched by MappingStart");
self.key_stack.pop();
self.insert(node, aid, mark);
}
Event::Scalar(v, style, aid, tag) => {
if !self.charge(1, mark) {
return;
}
self.insert(Raw::Scalar(v, style, tag), aid, mark);
}
Event::Alias(id) => {
let n = {
let referenced = self.anchor_map.get(&id).expect(
"yaml_rust2's scanner rejects an alias to an undefined anchor before \
this receiver ever runs -- see on_event_impl's doc comment",
);
count_nodes(referenced)
};
if !self.charge(n, mark) {
return;
}
let node = self
.anchor_map
.get(&id)
.cloned()
.expect("checked above: the anchor_map entry exists for this id");
self.insert(node, 0, mark);
}
}
}
}
impl MarkedEventReceiver for Builder {
fn on_event(&mut self, ev: Event, mark: Marker) {
self.on_event_impl(ev, mark);
}
}
fn scan_error_to_parse_error(e: &ScanError) -> ParseError {
let mark = e.marker();
ParseError::new(mark.line(), mark.col() + 1, format!("invalid YAML: {e}"))
}
pub fn read_yaml(text: &str) -> Result<Doc, OmnistError> {
let mut parser = Parser::new(text.chars());
let mut builder = Builder::new();
parser
.load(&mut builder, true)
.map_err(|e| scan_error_to_parse_error(&e))?;
if let Some(e) = builder.error {
return Err(e.into());
}
if builder.docs.len() > 1 {
return Err(ParseError::new(
1,
1,
"invalid YAML: expected a single document in the stream, found more than one",
)
.into());
}
let raw = builder.docs.into_iter().next().unwrap_or(Raw::Scalar(
String::new(),
TScalarStyle::Plain,
None,
));
let resolved = resolve_merges(&raw, 0)?;
let value = raw_to_value(&resolved)?;
Ok(Doc::of(&value)?)
}
fn resolve_merges(node: &Raw, depth: usize) -> Result<Raw, OmnistError> {
crate::document::check_write_depth(depth, "$")?;
match node {
Raw::Scalar(..) => Ok(node.clone()),
Raw::Sequence(items) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
out.push(resolve_merges(item, depth + 1)?);
}
Ok(Raw::Sequence(out))
}
Raw::Mapping(entries) => {
let mut merged_from: Vec<(Raw, Raw)> = Vec::new();
let mut own: Vec<(Raw, Raw)> = Vec::new();
for (k, v) in entries {
if is_merge_key(k) {
for (mk, mv) in merge_source_entries(v, depth)? {
merged_from.push((mk, mv));
}
} else {
own.push((resolve_merges(k, depth + 1)?, resolve_merges(v, depth + 1)?));
}
}
let own_labels: std::collections::HashSet<&str> =
own.iter().filter_map(|(k, _)| scalar_key_text(k)).collect();
let mut merged_seen: std::collections::HashSet<&str> =
std::collections::HashSet::with_capacity(merged_from.len());
let mut result = own.clone();
for (k, v) in &merged_from {
let label = scalar_key_text(k);
if let Some(label) = label
&& (own_labels.contains(label) || !merged_seen.insert(label))
{
continue;
}
result.push((k.clone(), v.clone()));
}
Ok(Raw::Mapping(result))
}
}
}
fn scalar_key_text(k: &Raw) -> Option<&str> {
match k {
Raw::Scalar(s, _, _) => Some(s.as_str()),
_ => None,
}
}
fn is_merge_key(k: &Raw) -> bool {
matches!(k, Raw::Scalar(s, TScalarStyle::Plain, None) if s == "<<")
}
fn merge_source_entries(v: &Raw, depth: usize) -> Result<Vec<(Raw, Raw)>, OmnistError> {
match v {
Raw::Mapping(entries) => {
let mut out = Vec::with_capacity(entries.len());
for (k, val) in entries {
out.push((
resolve_merges(k, depth + 1)?,
resolve_merges(val, depth + 1)?,
));
}
Ok(out)
}
Raw::Sequence(items) => {
let mut out = Vec::new();
for item in items {
out.extend(merge_source_entries(item, depth + 1)?);
}
Ok(out)
}
Raw::Scalar(..) => Err(ParseError::new(
1,
1,
"invalid YAML: merge key '<<' requires a mapping or a sequence of mappings, \
found a scalar",
)
.into()),
}
}
fn raw_to_value(node: &Raw) -> Result<Value, OmnistError> {
match node {
Raw::Scalar(text, style, tag) => Ok(scalar_to_value(text, *style, tag.as_ref())?),
Raw::Sequence(items) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
out.push(raw_to_value(item)?);
}
Ok(Value::Array(out))
}
Raw::Mapping(entries) => {
let mut map: IndexMap<String, Value> = IndexMap::new();
for (k, v) in entries {
let key = match k {
Raw::Scalar(s, style, tag) => match scalar_to_value(s, *style, tag.as_ref())? {
Value::Str(s) => s,
other => {
return Err(DocumentError::new(
"$",
format!(
"object key {} is not a string",
describe_non_string_key(&other)
),
)
.into());
}
},
_ => {
return Err(ParseError::new(
1,
1,
"invalid YAML: a mapping key must be a scalar",
)
.into());
}
};
map.insert(key, raw_to_value(v)?);
}
Ok(Value::Object(map))
}
}
}
fn describe_non_string_key(v: &Value) -> String {
match v {
Value::Bool(true) => "True".to_string(),
Value::Bool(false) => "False".to_string(),
Value::Null => "None".to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => {
let s = f.to_string();
if f.is_finite() && !s.contains('.') && !s.contains('e') && !s.contains('E') {
format!("{s}.0")
} else {
s
}
}
other => format!("{other:?}"),
}
}
fn scalar_to_value(
text: &str,
style: TScalarStyle,
tag: Option<&Tag>,
) -> Result<Value, ParseError> {
if let Some(t) = tag
&& t.handle == "tag:yaml.org,2002:"
{
return explicit_tag_to_value(text, &t.suffix);
}
if style != TScalarStyle::Plain {
return Ok(Value::Str(text.to_string()));
}
resolve_plain_scalar(text)
}
fn explicit_tag_to_value(text: &str, suffix: &str) -> Result<Value, ParseError> {
match suffix {
"str" => Ok(Value::Str(text.to_string())),
"null" => Ok(Value::Null),
"bool" => match text.to_ascii_lowercase().as_str() {
"true" | "yes" | "on" => Ok(Value::Bool(true)),
"false" | "no" | "off" => Ok(Value::Bool(false)),
_ => Err(ParseError::new(
1,
1,
format!("invalid YAML: {text:?} is not a valid !!bool value"),
)),
},
"int" => parse_int_literal(text),
"float" => parse_float_literal(text),
other => Err(ParseError::new(
1,
1,
format!("invalid YAML: unsupported explicit tag '!!{other}'"),
)),
}
}
fn resolve_plain_scalar(text: &str) -> Result<Value, ParseError> {
match text {
"" | "~" | "null" | "Null" | "NULL" => return Ok(Value::Null),
"true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" => {
return Ok(Value::Bool(true));
}
"false" | "False" | "FALSE" | "no" | "No" | "NO" | "off" | "Off" | "OFF" => {
return Ok(Value::Bool(false));
}
_ => {}
}
if is_int_literal_shape(text) {
return parse_int_literal(text);
}
if is_sexagesimal_int_shape(text) {
return parse_sexagesimal_int(text);
}
if is_float_literal_shape(text) {
return parse_float_literal(text);
}
if let Some(iso) = normalize_timestamp(text)? {
return Ok(if iso.contains('T') {
Value::Datetime(iso)
} else {
Value::Date(iso)
});
}
Ok(Value::Str(text.to_string()))
}
static INT_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(
r"^(?:[-+]?0b[0-1_]+|[-+]?0[0-7_]+|[-+]?(?:0|[1-9][0-9_]*)|[-+]?0x[0-9a-fA-F_]+)$",
)
.unwrap()
});
fn is_int_literal_shape(text: &str) -> bool {
INT_RE.is_match(text)
}
static SEXAGESIMAL_INT_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(r"^[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+$").unwrap()
});
fn is_sexagesimal_int_shape(text: &str) -> bool {
SEXAGESIMAL_INT_RE.is_match(text)
}
fn parse_sexagesimal_int(text: &str) -> Result<Value, ParseError> {
let neg = text.starts_with('-');
let t = text.strip_prefix(['+', '-']).unwrap_or(text);
let mut acc = BigInt::from(0);
let sixty = BigInt::from(60);
for group in t.split(':') {
let cleaned: String = group.chars().filter(|&c| c != '_').collect();
let digit = BigInt::parse_bytes(cleaned.as_bytes(), 10)
.expect("SEXAGESIMAL_INT_RE guarantees decimal digit groups");
acc = acc * &sixty + digit;
}
let value = if neg { -acc } else { acc };
let digit_count = value.to_string().trim_start_matches('-').len();
if digit_count > MAX_INT_DIGITS {
return Err(ParseError::new(
1,
1,
over_cap_message("invalid YAML: ", digit_count),
));
}
Ok(Value::Int(value))
}
fn parse_int_literal(text: &str) -> Result<Value, ParseError> {
let neg = text.starts_with('-');
let t = text.strip_prefix(['+', '-']).unwrap_or(text);
let cleaned: String = t.chars().filter(|&c| c != '_').collect();
let (radix, digits) = if let Some(rest) = cleaned.strip_prefix("0x") {
(16, rest)
} else if let Some(rest) = cleaned.strip_prefix("0b") {
(2, rest)
} else if cleaned.starts_with('0') && cleaned.len() > 1 {
(8, &cleaned[1..])
} else {
(10, cleaned.as_str())
};
if radix == 10 && digits.len() > MAX_INT_DIGITS {
return Err(ParseError::new(
1,
1,
over_cap_message("invalid YAML: ", digits.len()),
));
}
let magnitude = BigInt::parse_bytes(digits.as_bytes(), radix)
.expect("is_int_literal_shape guarantees valid digits for the detected radix");
let value = if neg { -magnitude } else { magnitude };
Ok(Value::Int(value))
}
static FLOAT_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(
r"^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)?|\.[0-9][0-9_]*(?:[eE][-+][0-9]+)?|[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN))$",
)
.unwrap()
});
fn is_float_literal_shape(text: &str) -> bool {
FLOAT_RE.is_match(text)
}
fn parse_float_literal(text: &str) -> Result<Value, ParseError> {
match text {
".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => {
return Ok(Value::Float(f64::INFINITY));
}
"-.inf" | "-.Inf" | "-.INF" => return Ok(Value::Float(f64::NEG_INFINITY)),
".nan" | ".NaN" | ".NAN" => return Ok(Value::Float(f64::NAN)),
_ => {}
}
let cleaned: String = text.chars().filter(|&c| c != '_').collect();
cleaned.parse::<f64>().map(Value::Float).map_err(|_| {
ParseError::new(
1,
1,
format!("invalid YAML: invalid float literal {text:?}"),
)
})
}
fn normalize_timestamp(text: &str) -> Result<Option<String>, ParseError> {
static RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(
r"^(?P<year>[0-9]{4})-(?P<month>[0-9][0-9]?)-(?P<day>[0-9][0-9]?)(?:(?:[Tt]|[ \t]+)(?P<hour>[0-9][0-9]?):(?P<minute>[0-9][0-9]):(?P<second>[0-9][0-9])(?:\.(?P<fraction>[0-9]*))?(?:[ \t]*(?:Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?)(?::(?P<tz_minute>[0-9][0-9]))?))?)?$",
)
.unwrap()
});
let Some(caps) = RE.captures(text) else {
return Ok(None);
};
let bad = |what: &str| {
Err(ParseError::new(
1,
1,
format!("invalid YAML: {text:?} is timestamp-shaped but names an invalid {what}"),
))
};
let year: u32 = caps["year"].parse().unwrap_or(u32::MAX);
let month: u32 = caps["month"].parse().unwrap_or(u32::MAX);
let day: u32 = caps["day"].parse().unwrap_or(u32::MAX);
if !crate::schema::valid_ymd(year, month, day) {
return bad("calendar date");
}
let Some(hour_m) = caps.name("hour") else {
return Ok(Some(format!("{year:04}-{month:02}-{day:02}")));
};
let hour: u32 = hour_m.as_str().parse().unwrap_or(u32::MAX);
let minute: u32 = caps["minute"].parse().unwrap_or(u32::MAX);
let second: u32 = caps["second"].parse().unwrap_or(u32::MAX);
if !crate::schema::valid_hms(hour, minute, second) {
return bad("time of day");
}
let mut out = format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}");
if let Some(frac) = caps.name("fraction") {
let mut digits = frac.as_str().to_string();
while digits.len() < 6 {
digits.push('0');
}
digits.truncate(6);
out.push('.');
out.push_str(&digits);
}
match caps.name("tz_sign") {
Some(sign) => {
let tz_hour: u32 = caps["tz_hour"].parse().unwrap_or(u32::MAX);
let tz_minute: u32 = caps
.name("tz_minute")
.map(|m| m.as_str().parse().unwrap_or(u32::MAX))
.unwrap_or(0);
if tz_hour > 23 || tz_minute > 59 {
return bad("timezone offset");
}
out.push_str(sign.as_str());
out.push_str(&format!("{tz_hour:02}:{tz_minute:02}"));
}
None if text.trim_end().ends_with('Z') => out.push_str("+00:00"),
None => {}
}
Ok(Some(out))
}
pub fn write_yaml(
doc: &Doc,
strict: bool,
report: Option<&mut WriteReport>,
) -> Result<String, WriteError> {
let grouped = doc.to_grouped();
let rep = check_yaml_grouped(&grouped);
let mut out = String::new();
write_node(&grouped, 0, &mut out, true);
if out.ends_with('\n') {
out.pop();
}
crate::report::finish_write(out, rep, strict, report)
}
pub fn check_yaml(doc: &Doc) -> WriteReport {
let grouped = doc.to_grouped();
check_yaml_grouped(&grouped)
}
fn check_yaml_grouped(grouped: &Value) -> WriteReport {
let mut rep = WriteReport::new();
let mut path = String::from("$");
crate::formats::visit_grouped(grouped, &mut path, &mut |visited, path| match visited {
crate::formats::Visited::Edge { label } if label.contains('\u{0085}') => {
rep.add(
path,
"string.line-break-char",
"label contains U+0085 (NEL); written double-quoted to round-trip correctly",
Severity::Warning,
);
}
crate::formats::Visited::Node {
value: Value::Str(s),
} if s.contains('\u{0085}') => {
rep.add(
path,
"string.line-break-char",
"value contains U+0085 (NEL); written double-quoted to round-trip correctly",
Severity::Warning,
);
}
_ => {}
});
rep
}
pub(crate) struct Yaml;
impl crate::formats::Codec for Yaml {
const NAME: &'static str = "yaml";
fn read(text: &str) -> Result<Doc, OmnistError> {
read_yaml(text)
}
fn write(doc: &Doc) -> Result<String, OmnistError> {
write_yaml(doc, false, None).map_err(Into::into)
}
fn check(doc: &Doc) -> WriteReport {
check_yaml(doc)
}
}
fn indent(out: &mut String, level: usize) {
for _ in 0..level {
out.push_str(" ");
}
}
fn write_node(node: &Value, level: usize, out: &mut String, top: bool) {
match node {
Value::Object(map) if map.is_empty() => {
out.push_str("{}\n");
}
Value::Array(items) if items.is_empty() => {
out.push_str("[]\n");
}
Value::Object(map) => {
for (label, child) in map {
indent(out, level);
write_scalar(label, out);
out.push(':');
write_child(child, level, out);
}
let _ = top;
}
Value::Array(items) => {
for item in items {
indent(out, level);
out.push('-');
write_seq_child(item, level, out);
}
}
other => {
write_scalar_value(other, out);
out.push('\n');
}
}
}
fn write_child(child: &Value, level: usize, out: &mut String) {
match child {
Value::Object(m) if !m.is_empty() => {
out.push('\n');
write_node(child, level + 1, out, false);
}
Value::Array(a) if !a.is_empty() => {
out.push('\n');
write_node(child, level, out, false);
}
_ => {
out.push(' ');
write_node(child, level + 1, out, false);
}
}
}
fn write_seq_child(item: &Value, level: usize, out: &mut String) {
match item {
Value::Object(m) if !m.is_empty() => {
out.push(' ');
let mut first = true;
for (label, child) in m {
if !first {
indent(out, level + 1);
}
first = false;
write_scalar(label, out);
out.push(':');
write_child(child, level + 1, out);
}
}
Value::Array(a) if !a.is_empty() => {
out.push('\n');
write_node(item, level + 1, out, false);
}
_ => {
out.push(' ');
write_node(item, level + 1, out, false);
}
}
}
fn write_scalar(s: &str, out: &mut String) {
write_scalar_value(&Value::Str(s.to_string()), out);
}
fn write_scalar_value(v: &Value, out: &mut String) {
match v {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Int(i) => out.push_str(&i.to_string()),
Value::Float(x) => write_float(*x, out),
Value::Str(s) => write_yaml_string(s, out),
Value::Date(s) | Value::Datetime(s) => out.push_str(s),
Value::Time(s) => write_yaml_string(s, out),
Value::Object(_) | Value::Array(_) => {
unreachable!("write_scalar_value is only ever called on a leaf")
}
}
}
fn write_float(x: f64, out: &mut String) {
float_fmt::write_float(x, ".nan", ".inf", "-.inf", out);
}
fn write_yaml_string(s: &str, out: &mut String) {
if needs_quoting(s) {
write_quoted(s, &YAML_ESCAPES, out);
} else {
out.push_str(s);
}
}
fn needs_quoting(s: &str) -> bool {
if s.is_empty() || s.contains('\u{0085}') || s.contains('\n') {
return true;
}
if matches!(resolve_plain_scalar(s), Ok(Value::Str(ref t)) if t == s) {
} else {
return true; }
let first = s.chars().next().unwrap();
if matches!(
first,
'-' | '?'
| ':'
| ','
| '['
| ']'
| '{'
| '}'
| '#'
| '&'
| '*'
| '!'
| '|'
| '>'
| '\''
| '"'
| '%'
| '@'
| '`'
| ' '
) {
return true;
}
if s.ends_with(' ') || s.contains(": ") || s.ends_with(':') || s.contains(" #") {
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::{Doc, Scalar, Value};
fn obj(pairs: Vec<(&str, Value)>) -> Value {
Value::Object(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
}
fn doc_of(v: Value) -> Doc {
Doc::of(&v).unwrap()
}
#[test]
fn reads_every_scalar_kind() {
let doc = read_yaml("a: 1\nb: \"s\"\nc: true\nd: null\ne: 1.5\n").unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Str("s".to_string())
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Bool(true)
);
assert_eq!(*root.get_one("d").unwrap().value().unwrap(), Scalar::Null);
assert_eq!(
*root.get_one("e").unwrap().value().unwrap(),
Scalar::Float(1.5)
);
}
#[test]
fn reads_yaml_1_1_bool_spellings_but_not_bare_y_or_n() {
let doc = read_yaml("a: yes\nb: no\nc: on\nd: off\ne: Yes\nf: y\ng: n\n").unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Bool(true)
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Bool(false)
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Bool(true)
);
assert_eq!(
*root.get_one("d").unwrap().value().unwrap(),
Scalar::Bool(false)
);
assert_eq!(
*root.get_one("e").unwrap().value().unwrap(),
Scalar::Bool(true)
);
assert_eq!(
*root.get_one("f").unwrap().value().unwrap(),
Scalar::Str("y".to_string())
);
assert_eq!(
*root.get_one("g").unwrap().value().unwrap(),
Scalar::Str("n".to_string())
);
}
#[test]
fn quoted_yes_stays_a_string_not_a_bool() {
let doc = read_yaml("a: \"yes\"\n").unwrap();
assert_eq!(
*doc.root().get_one("a").unwrap().value().unwrap(),
Scalar::Str("yes".to_string())
);
}
#[test]
fn reads_null_spellings() {
let doc = read_yaml("a: ~\nb: null\nc: Null\nd: NULL\ne:\n").unwrap();
let root = doc.root();
for label in ["a", "b", "c", "d", "e"] {
assert_eq!(*root.get_one(label).unwrap().value().unwrap(), Scalar::Null);
}
}
#[test]
fn reads_negative_and_hex_and_octal_and_binary_ints() {
let doc = read_yaml("a: -5\nb: 0x1A\nc: 017\nd: 0b101\ne: 1_000\n").unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Int((-5).into())
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Int((26).into())
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Int((15).into())
);
assert_eq!(
*root.get_one("d").unwrap().value().unwrap(),
Scalar::Int((5).into())
);
assert_eq!(
*root.get_one("e").unwrap().value().unwrap(),
Scalar::Int((1000).into())
);
}
#[test]
fn a_yaml_1_2_style_0o_octal_prefix_is_not_recognized_and_stays_a_string() {
let doc = read_yaml("a: 0o17\n").unwrap();
assert_eq!(
*doc.root().get_one("a").unwrap().value().unwrap(),
Scalar::Str("0o17".to_string())
);
}
#[test]
fn reads_legacy_sexagesimal_int_forms() {
let doc =
read_yaml("a: 12:00:00\nb: 1:20\nc: 1:2:3\nd: -1:20\ne: +1:20\nf: 123:45\ng: 1_2:30\n")
.unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Int((43200).into())
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Int((80).into())
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Int((3723).into())
);
assert_eq!(
*root.get_one("d").unwrap().value().unwrap(),
Scalar::Int((-80).into())
);
assert_eq!(
*root.get_one("e").unwrap().value().unwrap(),
Scalar::Int((80).into())
);
assert_eq!(
*root.get_one("f").unwrap().value().unwrap(),
Scalar::Int((7425).into())
);
assert_eq!(
*root.get_one("g").unwrap().value().unwrap(),
Scalar::Int((750).into())
);
}
#[test]
fn sexagesimal_first_group_over_i64_range_parses() {
let text = format!("a: {}:0\n", "9".repeat(20));
let doc = read_yaml(&text).unwrap();
let value = doc.root().child("a").unwrap().value().unwrap();
assert_eq!(
value,
&Scalar::Int(num_bigint::BigInt::parse_bytes(b"5999999999999999999940", 10).unwrap())
);
}
#[test]
fn sexagesimal_fold_overflow_across_many_in_range_groups_parses() {
let text = format!("a: 1{}\n", ":59".repeat(15));
let doc = read_yaml(&text).unwrap();
let value = doc.root().child("a").unwrap().value().unwrap();
assert_eq!(
value,
&Scalar::Int(
num_bigint::BigInt::parse_bytes(b"940369969151999999999999999", 10).unwrap()
)
);
}
#[test]
fn sexagesimal_fold_still_rejects_past_the_digit_cap() {
let text = format!("a: 1{}\n", ":59".repeat(2500));
let err = read_yaml(&text).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("4300-digit")),
"got {err:?}"
);
}
#[test]
fn sexagesimal_shape_with_leading_zero_or_out_of_range_group_stays_a_string() {
let doc = read_yaml("a: 0:0:1\nb: 1:60\nc: 1:600\nd: 01:20\n").unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Str("0:0:1".to_string())
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Str("1:60".to_string())
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Str("1:600".to_string())
);
assert_eq!(
*root.get_one("d").unwrap().value().unwrap(),
Scalar::Str("01:20".to_string())
);
}
#[test]
fn reads_float_and_inf_and_nan_tokens() {
let doc = read_yaml("a: 1.5\nb: .inf\nc: -.inf\nd: .nan\ne: 1.0e+3\n").unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Float(1.5)
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Float(f64::INFINITY)
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Float(f64::NEG_INFINITY)
);
assert!(
matches!(root.get_one("d").unwrap().value().unwrap(), Scalar::Float(x) if x.is_nan())
);
assert_eq!(
*root.get_one("e").unwrap().value().unwrap(),
Scalar::Float(1000.0)
);
}
#[test]
fn a_bare_exponent_without_a_decimal_point_is_not_float_shaped_and_stays_a_string() {
let doc = read_yaml("a: 1e3\nb: 1.0e3\n").unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Str("1e3".to_string())
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Str("1.0e3".to_string())
);
}
#[test]
fn reads_bare_date_as_iso_string() {
let doc = read_yaml("a: 2024-01-15\n").unwrap();
assert_eq!(
*doc.root().get_one("a").unwrap().value().unwrap(),
Scalar::Date("2024-01-15".to_string())
);
}
#[test]
fn genuine_date_and_datetime_values_write_bare_and_a_time_value_writes_quoted() {
let v = obj(vec![
("d", Value::Date("2024-01-15".to_string())),
("dt", Value::Datetime("2024-01-15T12:00:00".to_string())),
("t", Value::Time("12:00:00".to_string())),
]);
let doc = doc_of(v);
let text = write_yaml(&doc, false, None).unwrap();
assert!(text.contains("d: 2024-01-15\n"));
assert!(text.contains("dt: 2024-01-15T12:00:00\n"));
assert!(text.contains("t: \"12:00:00\""));
}
#[test]
fn reads_loose_timestamp_and_normalizes_to_canonical_iso() {
let doc = read_yaml("a: 2001-2-3 4:05:06.7 Z\n").unwrap();
assert_eq!(
*doc.root().get_one("a").unwrap().value().unwrap(),
Scalar::Datetime("2001-02-03T04:05:06.700000+00:00".to_string())
);
}
#[test]
fn a_single_digit_minute_or_second_is_not_timestamp_shaped_and_stays_a_string() {
let doc = read_yaml("a: 2001-2-3 4:5:6\n").unwrap();
assert_eq!(
*doc.root().get_one("a").unwrap().value().unwrap(),
Scalar::Str("2001-2-3 4:5:6".to_string())
);
}
#[test]
fn reads_datetime_with_no_timezone_at_all() {
let doc = read_yaml("a: 2024-01-15T12:30:00\n").unwrap();
assert_eq!(
*doc.root().get_one("a").unwrap().value().unwrap(),
Scalar::Datetime("2024-01-15T12:30:00".to_string())
);
}
#[test]
fn reads_timestamp_with_explicit_offset() {
let doc = read_yaml("a: 2001-12-14T21:59:43.10-05:00\n").unwrap();
assert_eq!(
*doc.root().get_one("a").unwrap().value().unwrap(),
Scalar::Datetime("2001-12-14T21:59:43.100000-05:00".to_string())
);
}
#[test]
fn a_string_that_merely_looks_like_a_short_date_but_isnt_shaped_right_stays_a_string() {
let doc = read_yaml("a: 2024-1\n").unwrap();
assert_eq!(
*doc.root().get_one("a").unwrap().value().unwrap(),
Scalar::Str("2024-1".to_string())
);
}
#[test]
fn reads_nested_mapping_and_sequence() {
let doc = read_yaml("a:\n b:\n c: 1\nm:\n - 1\n - 2\n - 3\n").unwrap();
let root = doc.root();
let a = root.get_one("a").unwrap();
let b = a.get_one("b").unwrap();
assert_eq!(
*b.get_one("c").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
let ms = root.get("m");
assert_eq!(ms.len(), 3);
assert_eq!(*ms[2].value().unwrap(), Scalar::Int((3).into()));
}
#[test]
fn reads_flow_style_mapping_and_sequence() {
let doc = read_yaml("a: {b: 1, c: 2}\nm: [1, 2, 3]\n").unwrap();
let root = doc.root();
let a = root.get_one("a").unwrap();
assert_eq!(
*a.get_one("b").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
assert_eq!(root.get("m").len(), 3);
}
#[test]
fn bare_top_level_sequence_is_a_document_error_not_a_parse_error() {
let err = read_yaml("- 1\n- 2\n").unwrap_err();
assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
}
#[test]
fn sequence_of_sequences_is_a_document_error() {
let err = read_yaml("m:\n - [1, 2]\n").unwrap_err();
assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
}
#[test]
fn empty_input_reads_as_a_null_document() {
let doc = read_yaml("").unwrap();
assert_eq!(*doc.root().value().unwrap(), Scalar::Null);
}
#[test]
fn explicit_empty_document_marker_reads_as_a_null_document() {
let doc = read_yaml("---\n").unwrap();
assert_eq!(*doc.root().value().unwrap(), Scalar::Null);
}
#[test]
fn duplicate_mapping_keys_last_value_wins() {
let doc = read_yaml("a: 1\nb: 2\na: 3\n").unwrap();
let root = doc.root();
assert_eq!(root.labels(), vec!["a".to_string(), "b".to_string()]);
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Int((3).into())
);
}
#[test]
fn invalid_yaml_syntax_is_a_parse_error() {
let err = read_yaml("a: [1, 2\n").unwrap_err();
assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
}
#[test]
fn multiple_documents_is_a_parse_error() {
let err = read_yaml("a: 1\n---\nb: 2\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("single document")),
"got {err:?}"
);
}
#[test]
fn nesting_past_max_depth_is_a_document_error() {
let mut text = String::new();
for i in 0..=crate::document::MAX_DEPTH {
text.push_str(&" ".repeat(i));
text.push_str("a:\n");
}
let err = read_yaml(&text).unwrap_err();
assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
}
#[test]
fn integer_literal_over_digit_cap_is_rejected() {
let text = format!("a: {}\n", "9".repeat(MAX_INT_DIGITS + 1));
let err = read_yaml(&text).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("4300-digit")),
"got {err:?}"
);
}
#[test]
fn i64_min_round_trips_through_yaml() {
let doc = read_yaml("a: -9223372036854775808\n").unwrap();
assert_eq!(
*doc.root().get_one("a").unwrap().value().unwrap(),
Scalar::Int((i64::MIN).into())
);
}
#[test]
fn positive_integer_one_past_i64_max_parses() {
let doc = read_yaml("a: 9223372036854775808\n").unwrap();
let value = doc.root().child("a").unwrap().value().unwrap();
assert_eq!(value, &Scalar::Int(num_bigint::BigInt::from(i64::MAX) + 1));
}
#[test]
fn negative_integer_one_past_i64_min_parses() {
let doc = read_yaml("a: -9223372036854775809\n").unwrap();
let value = doc.root().child("a").unwrap().value().unwrap();
assert_eq!(value, &Scalar::Int(num_bigint::BigInt::from(i64::MIN) - 1));
}
#[test]
fn integer_literal_over_i64_range_parses() {
let text = format!("a: {}\n", "9".repeat(20));
let doc = read_yaml(&text).unwrap();
let value = doc.root().child("a").unwrap().value().unwrap();
assert_eq!(
value,
&Scalar::Int(num_bigint::BigInt::parse_bytes(b"99999999999999999999", 10).unwrap())
);
}
#[test]
fn reads_anchor_and_alias_as_a_deep_copy() {
let doc = read_yaml("a: &x [1, 2, 3]\nb: *x\n").unwrap();
let root = doc.root();
assert_eq!(root.get("a").len(), 3);
assert_eq!(root.get("b").len(), 3);
assert_eq!(*root.get("b")[1].value().unwrap(), Scalar::Int((2).into()));
}
#[test]
fn unknown_alias_is_a_parse_error() {
let err = read_yaml("a: *nope\n").unwrap_err();
assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
}
fn billion_laughs_yaml(generations: usize) -> String {
let mut out = String::new();
out.push_str("a0: &a0 [x, x]\n");
for i in 1..generations {
out.push_str(&format!("a{i}: &a{i} [*a{prev}, *a{prev}]\n", prev = i - 1));
}
out
}
#[test]
fn billion_laughs_alias_amplification_is_rejected_fast_issue_42() {
let text = billion_laughs_yaml(24);
assert!(text.len() < 1000, "source text should be tiny: {text:?}");
let start = std::time::Instant::now();
let err = read_yaml(&text).unwrap_err();
let elapsed = start.elapsed();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("materializes more than")
&& e.message.contains("100000")),
"expected a materialized-node-limit ParseError, got {err:?}"
);
assert!(
elapsed < std::time::Duration::from_secs(5),
"fix should reject the bomb almost immediately, took {elapsed:?}"
);
}
#[test]
fn moderate_legitimate_nested_alias_reuse_still_works() {
let doc = read_yaml(
"base: &base\n x: 1\n y: 2\na: *base\nb: *base\nc: *base\nlist: &list [1, 2, 3]\nd: *list\ne: *list\n",
)
.unwrap();
let root = doc.root();
for label in ["a", "b", "c"] {
let node = root.get_one(label).unwrap();
assert_eq!(
*node.get_one("x").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
assert_eq!(
*node.get_one("y").unwrap().value().unwrap(),
Scalar::Int((2).into())
);
}
for label in ["d", "e"] {
assert_eq!(root.get(label).len(), 3);
}
}
#[test]
fn merge_key_from_a_mapping_merges_with_local_keys_winning() {
let doc =
read_yaml("base: &b\n x: 1\n y: 2\nchild:\n <<: *b\n y: 20\n z: 3\n").unwrap();
let child = doc.root().get_one("child").unwrap();
assert_eq!(
*child.get_one("x").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
assert_eq!(
*child.get_one("y").unwrap().value().unwrap(),
Scalar::Int((20).into()),
"an explicit local key beats the merged-in value"
);
assert_eq!(
*child.get_one("z").unwrap().value().unwrap(),
Scalar::Int((3).into())
);
}
#[test]
fn merge_key_from_a_sequence_of_mappings_merges_each_in_order() {
let doc =
read_yaml("a: &a\n x: 1\nb: &b\n x: 2\n y: 3\nchild:\n <<: [*a, *b]\n").unwrap();
let child = doc.root().get_one("child").unwrap();
assert_eq!(
*child.get_one("x").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
assert_eq!(
*child.get_one("y").unwrap().value().unwrap(),
Scalar::Int((3).into())
);
}
#[test]
fn quoted_double_angle_bracket_key_is_a_literal_string_not_a_merge() {
let doc = read_yaml("a:\n \"<<\": 1\n").unwrap();
let a = doc.root().get_one("a").unwrap();
assert_eq!(
*a.get_one("<<").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
}
#[test]
fn merge_key_from_a_non_map_scalar_source_is_a_clean_parse_error_omnist_ts_46() {
let err = read_yaml("child:\n <<: 5\n y: 2\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("merge key")),
"got {err:?}"
);
}
#[test]
fn merge_key_from_a_sequence_containing_a_scalar_is_a_clean_parse_error() {
let err = read_yaml("child:\n <<: [1, 2]\n").unwrap_err();
assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
}
#[test]
fn merge_source_with_a_non_scalar_key_is_a_clean_parse_error() {
let err = read_yaml("base: &b\n ? [1, 2]\n : 3\nchild:\n <<: *b\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("mapping key must be a scalar")),
"got {err:?}"
);
}
#[test]
fn merge_key_from_an_alias_to_a_scalar_is_a_clean_parse_error() {
let err = read_yaml("base: &b 5\nchild:\n <<: *b\n").unwrap_err();
assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
}
#[test]
fn explicit_tags_construct_the_named_type_regardless_of_spelling() {
let doc = read_yaml("a: !!str yes\nb: !!int \"5\"\nc: !!bool \"true\"\n").unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Str("yes".to_string())
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Int((5).into())
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Bool(true)
);
}
#[test]
fn unsupported_explicit_tag_is_a_parse_error() {
let err = read_yaml("a: !!binary \"x\"\n").unwrap_err();
assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
}
#[test]
fn explicit_bool_tag_accepts_the_full_yaml_1_1_spelling_set_not_just_true_false() {
let doc = read_yaml("a: !!bool \"yes\"\nb: !!bool \"On\"\nc: !!bool \"OFF\"\n").unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Bool(true)
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Bool(true)
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Bool(false)
);
}
#[test]
fn invalid_explicit_bool_spelling_is_a_parse_error() {
let err = read_yaml("a: !!bool \"nonsense\"\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("!!bool")),
"got {err:?}"
);
}
#[test]
fn bare_y_or_n_is_not_a_valid_explicit_bool_spelling() {
let err = read_yaml("a: !!bool \"y\"\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("!!bool")),
"got {err:?}"
);
}
#[test]
fn bare_on_key_resolves_to_boolean_and_is_rejected_norway_problem() {
let err = read_yaml("on:\n push: true\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Document(e) if e.path == "$"),
"got {err:?}"
);
}
#[test]
fn other_implicit_bool_and_null_key_spellings_are_also_rejected() {
for key in ["off", "yes", "no", "Off", "YES", "~", "null", "true"] {
let text = format!("{key}:\n push: true\n");
let err = read_yaml(&text).unwrap_err();
assert!(
matches!(&err, OmnistError::Document(_)),
"key {key:?} got {err:?}"
);
}
}
#[test]
fn bare_y_or_n_key_is_not_a_bool_and_stays_a_string_label() {
let doc = read_yaml("y:\n push: true\n").unwrap();
assert!(doc.root().get_one("y").is_ok());
let doc = read_yaml("n:\n push: true\n").unwrap();
assert!(doc.root().get_one("n").is_ok());
}
#[test]
fn sexagesimal_looking_key_also_resolves_and_is_rejected() {
let err = read_yaml("12:00:00:\n push: true\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Document(e) if e.path == "$"),
"got {err:?}"
);
}
#[test]
fn describe_non_string_key_renders_pythonic_spellings() {
assert_eq!(describe_non_string_key(&Value::Bool(true)), "True");
assert_eq!(describe_non_string_key(&Value::Bool(false)), "False");
assert_eq!(describe_non_string_key(&Value::Null), "None");
assert_eq!(
describe_non_string_key(&Value::Int((43200).into())),
"43200"
);
assert_eq!(describe_non_string_key(&Value::Float(1.5)), "1.5");
assert_eq!(describe_non_string_key(&Value::Float(1.0)), "1.0");
assert_eq!(describe_non_string_key(&Value::Float(-2.0)), "-2.0");
assert_eq!(
describe_non_string_key(&Value::Str("x".to_string())),
"Str(\"x\")"
);
}
#[test]
fn int_and_float_shaped_mapping_keys_are_rejected_with_python_parity_messages() {
let err = read_yaml("123:\n a: 1\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Document(e) if e.path == "$" && e.message.contains("123")),
"got {err:?}"
);
let err = read_yaml("1.0:\n a: 1\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Document(e) if e.path == "$" && e.message.contains("1.0")),
"got {err:?}"
);
}
#[test]
fn a_non_scalar_mapping_key_is_a_clean_parse_error() {
let err = read_yaml("? [1, 2]\n: 3\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("mapping key must be a scalar")),
"got {err:?}"
);
}
#[test]
fn round_trips_every_scalar_kind() {
let v = obj(vec![
("null", Value::Null),
("bool", Value::Bool(true)),
("int", Value::Int((42).into())),
("float", Value::Float(1.5)),
("str", Value::Str("hi".to_string())),
]);
let doc = doc_of(v);
let text = write_yaml(&doc, false, None).unwrap();
let back = read_yaml(&text).unwrap();
assert!(doc.eq_doc(&back));
}
#[test]
fn round_trips_integral_float_at_and_above_1e17_boundary_issue_46() {
for x in [1.0e17, 1.0e18, -1.23e17, 9.9e16_f64] {
let doc = doc_of(obj(vec![("a", Value::Float(x))]));
let text = write_yaml(&doc, false, None).unwrap();
let back = read_yaml(&text).unwrap();
assert_eq!(
*back.root().get_one("a").unwrap().value().unwrap(),
Scalar::Float(x),
"x={x} text={text}"
);
}
}
#[test]
fn round_trips_nan_and_infinity_natively_no_adjustment_needed() {
let v = obj(vec![
("a", Value::Float(f64::NAN)),
("b", Value::Float(f64::INFINITY)),
("c", Value::Float(f64::NEG_INFINITY)),
]);
let doc = doc_of(v);
let mut rep = WriteReport::new();
let text = write_yaml(&doc, false, Some(&mut rep)).unwrap();
assert!(rep.is_empty());
let back = read_yaml(&text).unwrap();
assert!(
matches!(back.root().get_one("a").unwrap().value().unwrap(), Scalar::Float(x) if x.is_nan())
);
assert_eq!(
*back.root().get_one("b").unwrap().value().unwrap(),
Scalar::Float(f64::INFINITY)
);
assert_eq!(
*back.root().get_one("c").unwrap().value().unwrap(),
Scalar::Float(f64::NEG_INFINITY)
);
}
#[test]
fn round_trips_strings_that_look_like_other_scalar_kinds() {
let v = obj(vec![
("a", Value::Str("yes".to_string())),
("b", Value::Str("null".to_string())),
("c", Value::Str("123".to_string())),
("d", Value::Str("1.5".to_string())),
("e", Value::Str("".to_string())),
("f", Value::Str("2024-01-15".to_string())),
]);
let doc = doc_of(v);
let text = write_yaml(&doc, false, None).unwrap();
let back = read_yaml(&text).unwrap();
assert!(doc.eq_doc(&back), "text was:\n{text}");
}
#[test]
fn round_trips_repeated_labels_as_a_yaml_sequence() {
let doc = doc_of(obj(vec![(
"m",
Value::Array(vec![
Value::Int((1).into()),
Value::Int((2).into()),
Value::Int((3).into()),
]),
)]));
let text = write_yaml(&doc, false, None).unwrap();
let back = read_yaml(&text).unwrap();
assert!(doc.eq_doc(&back));
}
#[test]
fn round_trips_nested_mappings_and_sequences_of_mappings() {
let v = obj(vec![
(
"a",
obj(vec![
("b", Value::Int((1).into())),
("c", Value::Int((2).into())),
]),
),
(
"items",
Value::Array(vec![
obj(vec![
("x", Value::Int((1).into())),
("y", Value::Int((2).into())),
]),
obj(vec![
("x", Value::Int((3).into())),
("y", Value::Int((4).into())),
]),
]),
),
]);
let doc = doc_of(v);
let text = write_yaml(&doc, false, None).unwrap();
let back = read_yaml(&text).unwrap();
assert!(doc.eq_doc(&back), "text was:\n{text}");
}
#[test]
fn writes_empty_object_and_array_compactly() {
let doc = doc_of(obj(vec![("o", Value::Object(IndexMap::new()))]));
let text = write_yaml(&doc, false, None).unwrap();
assert!(text.contains("o: {}"));
}
#[test]
fn nel_string_triggers_a_warning_and_still_round_trips() {
let s = format!("a{}b", '\u{0085}');
let doc = doc_of(obj(vec![("s", Value::Str(s.clone()))]));
let mut rep = WriteReport::new();
let text = write_yaml(&doc, false, Some(&mut rep)).unwrap();
assert_eq!(rep.len(), 1);
assert_eq!(rep.adjustments()[0].code, "string.line-break-char");
let back = read_yaml(&text).unwrap();
assert_eq!(
*back.root().get_one("s").unwrap().value().unwrap(),
Scalar::Str(s)
);
}
#[test]
fn strict_write_with_nel_raises_and_carries_the_report() {
let s = format!("x{}y", '\u{0085}');
let doc = doc_of(obj(vec![("s", Value::Str(s))]));
let err = write_yaml(&doc, true, None).unwrap_err();
let rep = err.report().expect("strict WriteError carries a report");
assert_eq!(rep.len(), 1);
}
#[test]
fn strict_write_with_no_adjustments_succeeds() {
let doc = doc_of(obj(vec![("a", Value::Int((1).into()))]));
let text = write_yaml(&doc, true, None).unwrap();
assert!(text.contains("a: 1"));
}
#[test]
fn check_yaml_reports_without_producing_output() {
let s = format!("a{}b", '\u{0085}');
let doc = doc_of(obj(vec![("s", Value::Str(s))]));
let rep = check_yaml(&doc);
assert_eq!(rep.len(), 1);
assert_eq!(rep.adjustments()[0].path, "$.s");
}
#[test]
fn deeply_nested_document_write_reuses_doc_construction_depth_guard() {
let mut v = Value::Int((0).into());
for _ in 0..=crate::document::MAX_DEPTH {
v = obj(vec![("a", v)]);
}
assert!(Doc::of(&v).is_err());
}
#[test]
fn wide_document_smoke_test_reads_and_round_trips_every_field() {
let n = 5_000;
let mut text = String::new();
for i in 0..n {
text.push_str(&format!("field{i}: {i}\n"));
}
let doc = read_yaml(&text).unwrap();
let root = doc.root();
assert_eq!(root.labels().len(), n);
for i in [0, n / 2, n - 1] {
assert_eq!(
*root.get_one(&format!("field{i}")).unwrap().value().unwrap(),
Scalar::Int((i as i64).into())
);
}
let out = write_yaml(&doc, false, None).unwrap();
let back = read_yaml(&out).unwrap();
assert!(doc.eq_doc(&back));
}
#[test]
fn wide_flat_sequence_smoke_test() {
let n = 5_000;
let mut text = String::from("m:\n");
for i in 0..n {
text.push_str(&format!(" - {i}\n"));
}
let doc = read_yaml(&text).unwrap();
assert_eq!(doc.root().get("m").len(), n);
}
#[test]
fn invalid_explicit_float_literal_is_a_parse_error() {
let err = read_yaml("a: !!float \"not-a-float\"\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("invalid float literal")),
"got {err:?}"
);
}
#[test]
fn explicit_float_tag_accepts_inf_and_nan_and_negative() {
let doc = read_yaml("a: !!float \"-1.5\"\n").unwrap();
assert_eq!(
*doc.root().get_one("a").unwrap().value().unwrap(),
Scalar::Float(-1.5)
);
}
#[test]
fn timestamp_with_invalid_month_is_a_parse_error() {
let err = read_yaml("a: 2024-13-01\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("calendar date")),
"got {err:?}"
);
}
#[test]
fn timestamp_with_year_zero_is_a_parse_error() {
let err = read_yaml("a: 0000-01-01\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("calendar date")),
"got {err:?}"
);
}
#[test]
fn timestamp_with_a_day_that_doesnt_exist_in_the_month_is_a_parse_error() {
let err = read_yaml("a: 2024-02-30\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("calendar date")),
"got {err:?}"
);
}
#[test]
fn timestamp_february_29_on_a_non_leap_year_is_a_parse_error() {
let err = read_yaml("a: 2023-02-29\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("calendar date")),
"got {err:?}"
);
}
#[test]
fn timestamp_february_29_on_a_leap_year_normalizes_fine() {
let doc = read_yaml("a: 2024-02-29\n").unwrap();
assert_eq!(
*doc.root().get_one("a").unwrap().value().unwrap(),
Scalar::Date("2024-02-29".to_string())
);
}
#[test]
fn timestamp_with_out_of_range_hour_is_a_parse_error() {
let err = read_yaml("a: 2024-01-01T25:00:00\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("time of day")),
"got {err:?}"
);
}
#[test]
fn timestamp_with_out_of_range_minute_is_a_parse_error() {
let err = read_yaml("a: 2024-01-01T00:61:00\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("time of day")),
"got {err:?}"
);
}
#[test]
fn timestamp_with_out_of_range_timezone_offset_is_a_parse_error() {
let err = read_yaml("a: 2024-01-01T00:00:00+25:00\n").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("timezone offset")),
"got {err:?}"
);
}
#[test]
fn timestamp_with_hour_only_timezone_offset_normalizes_with_zero_minutes() {
let doc = read_yaml("a: 2024-01-01T00:00:00+05\n").unwrap();
assert_eq!(
*doc.root().get_one("a").unwrap().value().unwrap(),
Scalar::Datetime("2024-01-01T00:00:00+05:00".to_string())
);
}
#[test]
fn nel_in_a_label_triggers_a_warning_and_still_round_trips() {
let label = format!("a{}b", '\u{0085}');
let doc = doc_of(obj(vec![(label.as_str(), Value::Int((1).into()))]));
let mut rep = WriteReport::new();
let text = write_yaml(&doc, false, Some(&mut rep)).unwrap();
assert_eq!(rep.len(), 1);
assert_eq!(rep.adjustments()[0].code, "string.line-break-char");
let back = read_yaml(&text).unwrap();
assert_eq!(
*back.root().get_one(&label).unwrap().value().unwrap(),
Scalar::Int((1).into())
);
}
#[test]
fn write_node_on_a_bare_empty_array_writes_the_flow_empty_token() {
let mut out = String::new();
write_node(&Value::Array(vec![]), 0, &mut out, true);
assert_eq!(out, "[]\n");
}
#[test]
fn round_trips_strings_needing_every_quoting_trigger() {
let cases = [
"-leading-dash",
"?leading-question",
":leading-colon",
",leading-comma",
"[leading-bracket",
"]leading-bracket",
"{leading-brace",
"}leading-brace",
"#leading-hash",
"&leading-amp",
"*leading-star",
"!leading-bang",
"|leading-pipe",
">leading-gt",
"'leading-quote",
"\"leading-dquote",
"%leading-percent",
"@leading-at",
"`leading-backtick",
" leading-space",
"trailing-space ",
"embedded: colon-space",
"trailing-colon:",
"embedded #hash-space",
"line\nbreak",
"tab\ttab",
"quote\"quote",
"back\\slash",
"control\u{01}char",
];
for s in cases {
let doc = doc_of(obj(vec![("s", Value::Str(s.to_string()))]));
let text = write_yaml(&doc, false, None).unwrap();
let back = read_yaml(&text).unwrap();
assert!(
doc.eq_doc(&back),
"round trip failed for {s:?}, text was:\n{text}"
);
}
}
#[test]
fn write_scalar_value_panics_on_a_non_leaf_value() {
let result = std::panic::catch_unwind(|| {
let mut out = String::new();
write_scalar_value(&Value::Object(IndexMap::new()), &mut out);
});
assert!(result.is_err());
}
use yaml_rust2::parser::Event;
use yaml_rust2::scanner::Marker;
fn test_marker() -> Marker {
struct Capture(Option<Marker>);
impl MarkedEventReceiver for Capture {
fn on_event(&mut self, _ev: Event, mark: Marker) {
self.0.get_or_insert(mark);
}
}
let mut cap = Capture(None);
Parser::new("x".chars()).load(&mut cap, false).unwrap();
cap.0
.expect("a trivial scalar document always emits at least one event")
}
#[test]
fn builder_document_end_with_an_empty_stack_pushes_a_null_scalar() {
let mut b = Builder::new();
b.on_event_impl(Event::DocumentEnd, test_marker());
assert_eq!(b.docs.len(), 1);
assert!(matches!(&b.docs[0], Raw::Scalar(s, TScalarStyle::Plain, None) if s.is_empty()));
}
#[test]
#[should_panic(expected = "a single document's stack never nests more than one root")]
fn builder_document_end_with_more_than_one_stack_entry_panics() {
let mut b = Builder::new();
b.doc_stack
.push((Raw::Scalar(String::new(), TScalarStyle::Plain, None), 0));
b.doc_stack
.push((Raw::Scalar(String::new(), TScalarStyle::Plain, None), 0));
b.on_event_impl(Event::DocumentEnd, test_marker());
}
#[test]
#[should_panic(expected = "a Scalar is never a container on doc_stack")]
fn builder_insert_onto_a_scalar_container_panics() {
let mut b = Builder::new();
b.doc_stack
.push((Raw::Scalar("x".to_string(), TScalarStyle::Plain, None), 0));
b.insert(
Raw::Scalar("y".to_string(), TScalarStyle::Plain, None),
0,
test_marker(),
);
}
#[test]
#[should_panic(expected = "yaml_rust2's scanner rejects an alias to an undefined anchor")]
fn builder_alias_to_an_unknown_anchor_panics() {
let mut b = Builder::new();
b.on_event_impl(Event::Alias(999), test_marker());
}
#[test]
fn charge_after_already_tripped_is_a_pure_no_op() {
let mut b = Builder::new();
assert!(!b.charge(MAX_MATERIALIZED_NODES + 1, test_marker()));
let first_error = format!("{:?}", b.error);
let count_after_first_trip = b.node_count;
assert!(!b.charge(1, test_marker()));
assert_eq!(
format!("{:?}", b.error),
first_error,
"error must not change"
);
assert_eq!(
b.node_count, count_after_first_trip,
"node_count must not change once already tripped"
);
}
#[test]
fn sequence_start_can_itself_trip_the_node_count_guard() {
let mut b = Builder::new();
b.node_count = MAX_MATERIALIZED_NODES;
b.on_event_impl(Event::SequenceStart(0, None), test_marker());
assert!(
matches!(&b.error, Some(e) if e.message.contains("materializes more than")),
"got {:?}",
b.error
);
assert!(b.doc_stack.is_empty());
}
#[test]
fn mapping_start_can_itself_trip_the_node_count_guard() {
let mut b = Builder::new();
b.node_count = MAX_MATERIALIZED_NODES;
b.on_event_impl(Event::MappingStart(0, None), test_marker());
assert!(
matches!(&b.error, Some(e) if e.message.contains("materializes more than")),
"got {:?}",
b.error
);
assert!(b.doc_stack.is_empty());
assert!(b.key_stack.is_empty());
}
#[test]
fn scalar_can_itself_trip_the_node_count_guard() {
let mut b = Builder::new();
b.node_count = MAX_MATERIALIZED_NODES;
b.on_event_impl(
Event::Scalar("x".to_string(), TScalarStyle::Plain, 0, None),
test_marker(),
);
assert!(
matches!(&b.error, Some(e) if e.message.contains("materializes more than")),
"got {:?}",
b.error
);
assert!(b.doc_stack.is_empty());
}
#[test]
fn round_trips_an_integral_float_with_trailing_dot_zero() {
let doc = doc_of(obj(vec![("f", Value::Float(2.0))]));
let text = write_yaml(&doc, false, None).unwrap();
assert!(text.contains("f: 2.0"), "text was:\n{text}");
let back = read_yaml(&text).unwrap();
assert!(doc.eq_doc(&back));
}
#[test]
fn quoted_string_escapes_every_special_character_in_one_pass() {
let s = "-a\tb\\c\"d\u{01}e";
let doc = doc_of(obj(vec![("s", Value::Str(s.to_string()))]));
let text = write_yaml(&doc, false, None).unwrap();
let back = read_yaml(&text).unwrap();
assert!(doc.eq_doc(&back), "text was:\n{text}");
}
#[test]
fn write_seq_child_on_a_bare_non_empty_array_item_writes_it_on_the_next_line() {
let mut out = String::new();
write_seq_child(
&Value::Array(vec![Value::Int((1).into()), Value::Int((2).into())]),
0,
&mut out,
);
assert_eq!(out, "\n - 1\n - 2\n");
}
#[test]
fn test_yaml_merge_key_deduplication_order() {
let src = r#"
base1: &b1
a: 1
b: 2
dup: "from_b1"
base2: &b2
b: 20
c: 3
dup: "from_b2"
child:
<<: [*b1, *b2]
own: 0
a: 100
"#;
let doc = read_yaml(src).unwrap();
let root = doc.root();
let child = root.get_one("child").unwrap();
let labels = child.labels();
assert_eq!(labels, vec!["own", "a", "b", "dup", "c"]);
assert_eq!(
*child.get_one("a").unwrap().value().unwrap(),
Scalar::Int((100).into())
);
assert_eq!(
*child.get_one("b").unwrap().value().unwrap(),
Scalar::Int((2).into())
);
assert_eq!(
*child.get_one("dup").unwrap().value().unwrap(),
Scalar::Str("from_b1".into())
);
assert_eq!(
*child.get_one("c").unwrap().value().unwrap(),
Scalar::Int((3).into())
);
}
}