1use std::collections::HashMap;
93
94use yaml_rust2::parser::{Event, MarkedEventReceiver, Parser, Tag};
95use yaml_rust2::scanner::{Marker, ScanError, TScalarStyle};
96
97use crate::WriteError;
98use crate::document::{Doc, Value};
99use crate::error::{DocumentError, OmnistError, ParseError};
100use crate::formats::float_fmt;
101use crate::formats::int_cap::{MAX_INT_DIGITS, over_cap_message};
102use crate::formats::string_escape::{YAML_ESCAPES, write_quoted};
103use crate::report::{Severity, WriteReport};
104use indexmap::IndexMap;
105use num_bigint::BigInt;
106
107const MAX_MATERIALIZED_NODES: usize = 100_000;
138
139fn count_nodes(node: &Raw) -> usize {
144 match node {
145 Raw::Scalar(..) => 1,
146 Raw::Sequence(items) => 1 + items.iter().map(count_nodes).sum::<usize>(),
147 Raw::Mapping(entries) => {
148 1 + entries
149 .iter()
150 .map(|(k, v)| count_nodes(k) + count_nodes(v))
151 .sum::<usize>()
152 }
153 }
154}
155
156#[derive(Debug, Clone)]
164enum Raw {
165 Scalar(String, TScalarStyle, Option<Tag>),
166 Sequence(Vec<Raw>),
167 Mapping(Vec<(Raw, Raw)>),
168}
169
170struct Builder {
176 doc_stack: Vec<(Raw, usize)>,
177 key_stack: Vec<Option<Raw>>,
178 anchor_map: HashMap<usize, Raw>,
179 docs: Vec<Raw>,
180 node_count: usize,
183 error: Option<ParseError>,
187}
188
189impl Builder {
190 fn new() -> Self {
191 Builder {
192 doc_stack: Vec::new(),
193 key_stack: Vec::new(),
194 anchor_map: HashMap::new(),
195 docs: Vec::new(),
196 node_count: 0,
197 error: None,
198 }
199 }
200
201 fn charge(&mut self, n: usize, mark: Marker) -> bool {
207 if self.error.is_some() {
208 return false;
209 }
210 self.node_count = self.node_count.saturating_add(n);
211 if self.node_count > MAX_MATERIALIZED_NODES {
212 self.error = Some(ParseError::new(
213 mark.line(),
214 mark.col() + 1,
215 format!(
216 "invalid YAML: document materializes more than \
217 {MAX_MATERIALIZED_NODES} nodes (security: unbounded anchor/alias \
218 expansion can amplify a small document into an enormous tree, \
219 independent of nesting depth)"
220 ),
221 ));
222 return false;
223 }
224 true
225 }
226
227 fn insert(&mut self, node: Raw, aid: usize, _mark: Marker) {
228 if aid > 0 {
229 self.anchor_map.insert(aid, node.clone());
230 }
231 match self.doc_stack.last_mut() {
232 None => self.doc_stack.push((node, aid)),
233 Some((Raw::Sequence(items), _)) => items.push(node),
234 Some((Raw::Mapping(_), _)) => {
235 let cur_key = self
236 .key_stack
237 .last_mut()
238 .expect("a Mapping is only ever pushed alongside a matching key_stack entry");
239 match cur_key.take() {
240 None => *cur_key = Some(node),
241 Some(k) => {
242 if let Some((Raw::Mapping(entries), _)) = self.doc_stack.last_mut() {
243 entries.push((k, node));
244 }
245 }
246 }
247 }
248 Some((Raw::Scalar(..), _)) => {
249 unreachable!("a Scalar is never a container on doc_stack")
253 }
254 }
255 }
256
257 fn on_event_impl(&mut self, ev: Event, mark: Marker) {
268 if self.error.is_some() {
273 return;
274 }
275 match ev {
276 Event::Nothing | Event::StreamStart | Event::StreamEnd | Event::DocumentStart => {}
277 Event::DocumentEnd => match self.doc_stack.len() {
278 0 => self
279 .docs
280 .push(Raw::Scalar(String::new(), TScalarStyle::Plain, None)),
281 1 => self.docs.push(self.doc_stack.pop().unwrap().0),
282 _ => unreachable!("a single document's stack never nests more than one root"),
283 },
284 Event::SequenceStart(aid, _) => {
285 if !self.charge(1, mark) {
286 return;
287 }
288 self.doc_stack.push((Raw::Sequence(Vec::new()), aid));
289 }
290 Event::SequenceEnd => {
291 let (node, aid) = self.doc_stack.pop().expect("matched by SequenceStart");
292 self.insert(node, aid, mark);
293 }
294 Event::MappingStart(aid, _) => {
295 if !self.charge(1, mark) {
296 return;
297 }
298 self.doc_stack.push((Raw::Mapping(Vec::new()), aid));
299 self.key_stack.push(None);
300 }
301 Event::MappingEnd => {
302 let (node, aid) = self.doc_stack.pop().expect("matched by MappingStart");
303 self.key_stack.pop();
304 self.insert(node, aid, mark);
305 }
306 Event::Scalar(v, style, aid, tag) => {
307 if !self.charge(1, mark) {
308 return;
309 }
310 self.insert(Raw::Scalar(v, style, tag), aid, mark);
311 }
312 Event::Alias(id) => {
313 let n = {
320 let referenced = self.anchor_map.get(&id).expect(
321 "yaml_rust2's scanner rejects an alias to an undefined anchor before \
322 this receiver ever runs -- see on_event_impl's doc comment",
323 );
324 count_nodes(referenced)
325 };
326 if !self.charge(n, mark) {
327 return;
328 }
329 let node = self
330 .anchor_map
331 .get(&id)
332 .cloned()
333 .expect("checked above: the anchor_map entry exists for this id");
334 self.insert(node, 0, mark);
335 }
336 }
337 }
338}
339
340impl MarkedEventReceiver for Builder {
341 fn on_event(&mut self, ev: Event, mark: Marker) {
342 self.on_event_impl(ev, mark);
343 }
344}
345
346fn scan_error_to_parse_error(e: &ScanError) -> ParseError {
347 let mark = e.marker();
348 ParseError::new(mark.line(), mark.col() + 1, format!("invalid YAML: {e}"))
349}
350
351pub fn read_yaml(text: &str) -> Result<Doc, OmnistError> {
362 let mut parser = Parser::new(text.chars());
363 let mut builder = Builder::new();
364 parser
365 .load(&mut builder, true)
366 .map_err(|e| scan_error_to_parse_error(&e))?;
367 if let Some(e) = builder.error {
368 return Err(e.into());
369 }
370 if builder.docs.len() > 1 {
371 return Err(ParseError::new(
372 1,
373 1,
374 "invalid YAML: expected a single document in the stream, found more than one",
375 )
376 .into());
377 }
378 let raw = builder.docs.into_iter().next().unwrap_or(Raw::Scalar(
379 String::new(),
380 TScalarStyle::Plain,
381 None,
382 ));
383 let resolved = resolve_merges(&raw, 0)?;
384 let value = raw_to_value(&resolved)?;
385 Ok(Doc::of(&value)?)
386}
387
388fn resolve_merges(node: &Raw, depth: usize) -> Result<Raw, OmnistError> {
392 crate::document::check_write_depth(depth, "$")?;
393 match node {
394 Raw::Scalar(..) => Ok(node.clone()),
395 Raw::Sequence(items) => {
396 let mut out = Vec::with_capacity(items.len());
397 for item in items {
398 out.push(resolve_merges(item, depth + 1)?);
399 }
400 Ok(Raw::Sequence(out))
401 }
402 Raw::Mapping(entries) => {
403 let mut merged_from: Vec<(Raw, Raw)> = Vec::new();
404 let mut own: Vec<(Raw, Raw)> = Vec::new();
405 for (k, v) in entries {
406 if is_merge_key(k) {
407 for (mk, mv) in merge_source_entries(v, depth)? {
408 merged_from.push((mk, mv));
409 }
410 } else {
411 own.push((resolve_merges(k, depth + 1)?, resolve_merges(v, depth + 1)?));
412 }
413 }
414 let own_labels: std::collections::HashSet<&str> =
420 own.iter().filter_map(|(k, _)| scalar_key_text(k)).collect();
421 let mut merged_seen: std::collections::HashSet<&str> =
422 std::collections::HashSet::with_capacity(merged_from.len());
423 let mut result = own.clone();
424 for (k, v) in &merged_from {
425 let label = scalar_key_text(k);
426 if let Some(label) = label
427 && (own_labels.contains(label) || !merged_seen.insert(label))
428 {
429 continue;
430 }
431 result.push((k.clone(), v.clone()));
432 }
433 Ok(Raw::Mapping(result))
434 }
435 }
436}
437
438fn scalar_key_text(k: &Raw) -> Option<&str> {
442 match k {
443 Raw::Scalar(s, _, _) => Some(s.as_str()),
444 _ => None,
445 }
446}
447
448fn is_merge_key(k: &Raw) -> bool {
453 matches!(k, Raw::Scalar(s, TScalarStyle::Plain, None) if s == "<<")
454}
455
456fn merge_source_entries(v: &Raw, depth: usize) -> Result<Vec<(Raw, Raw)>, OmnistError> {
461 match v {
462 Raw::Mapping(entries) => {
463 let mut out = Vec::with_capacity(entries.len());
464 for (k, val) in entries {
465 out.push((
466 resolve_merges(k, depth + 1)?,
467 resolve_merges(val, depth + 1)?,
468 ));
469 }
470 Ok(out)
471 }
472 Raw::Sequence(items) => {
473 let mut out = Vec::new();
474 for item in items {
475 out.extend(merge_source_entries(item, depth + 1)?);
476 }
477 Ok(out)
478 }
479 Raw::Scalar(..) => Err(ParseError::new(
480 1,
481 1,
482 "invalid YAML: merge key '<<' requires a mapping or a sequence of mappings, \
483 found a scalar",
484 )
485 .into()),
486 }
487}
488
489fn raw_to_value(node: &Raw) -> Result<Value, OmnistError> {
492 match node {
493 Raw::Scalar(text, style, tag) => Ok(scalar_to_value(text, *style, tag.as_ref())?),
494 Raw::Sequence(items) => {
495 let mut out = Vec::with_capacity(items.len());
496 for item in items {
497 out.push(raw_to_value(item)?);
498 }
499 Ok(Value::Array(out))
500 }
501 Raw::Mapping(entries) => {
502 let mut map: IndexMap<String, Value> = IndexMap::new();
503 for (k, v) in entries {
504 let key = match k {
505 Raw::Scalar(s, style, tag) => match scalar_to_value(s, *style, tag.as_ref())? {
516 Value::Str(s) => s,
517 other => {
518 return Err(DocumentError::new(
519 "$",
520 format!(
521 "object key {} is not a string",
522 describe_non_string_key(&other)
523 ),
524 )
525 .into());
526 }
527 },
528 _ => {
529 return Err(ParseError::new(
530 1,
531 1,
532 "invalid YAML: a mapping key must be a scalar",
533 )
534 .into());
535 }
536 };
537 map.insert(key, raw_to_value(v)?);
541 }
542 Ok(Value::Object(map))
543 }
544 }
545}
546
547fn describe_non_string_key(v: &Value) -> String {
558 match v {
559 Value::Bool(true) => "True".to_string(),
560 Value::Bool(false) => "False".to_string(),
561 Value::Null => "None".to_string(),
562 Value::Int(i) => i.to_string(),
563 Value::Float(f) => {
564 let s = f.to_string();
568 if f.is_finite() && !s.contains('.') && !s.contains('e') && !s.contains('E') {
569 format!("{s}.0")
570 } else {
571 s
572 }
573 }
574 other => format!("{other:?}"),
575 }
576}
577
578fn scalar_to_value(
579 text: &str,
580 style: TScalarStyle,
581 tag: Option<&Tag>,
582) -> Result<Value, ParseError> {
583 if let Some(t) = tag
584 && t.handle == "tag:yaml.org,2002:"
585 {
586 return explicit_tag_to_value(text, &t.suffix);
587 }
588 if style != TScalarStyle::Plain {
589 return Ok(Value::Str(text.to_string()));
590 }
591 resolve_plain_scalar(text)
592}
593
594fn explicit_tag_to_value(text: &str, suffix: &str) -> Result<Value, ParseError> {
603 match suffix {
604 "str" => Ok(Value::Str(text.to_string())),
605 "null" => Ok(Value::Null),
606 "bool" => match text.to_ascii_lowercase().as_str() {
614 "true" | "yes" | "on" => Ok(Value::Bool(true)),
615 "false" | "no" | "off" => Ok(Value::Bool(false)),
616 _ => Err(ParseError::new(
617 1,
618 1,
619 format!("invalid YAML: {text:?} is not a valid !!bool value"),
620 )),
621 },
622 "int" => parse_int_literal(text),
623 "float" => parse_float_literal(text),
624 other => Err(ParseError::new(
625 1,
626 1,
627 format!("invalid YAML: unsupported explicit tag '!!{other}'"),
628 )),
629 }
630}
631
632fn resolve_plain_scalar(text: &str) -> Result<Value, ParseError> {
636 match text {
637 "" | "~" | "null" | "Null" | "NULL" => return Ok(Value::Null),
638 "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" => {
639 return Ok(Value::Bool(true));
640 }
641 "false" | "False" | "FALSE" | "no" | "No" | "NO" | "off" | "Off" | "OFF" => {
642 return Ok(Value::Bool(false));
643 }
644 _ => {}
645 }
646 if is_int_literal_shape(text) {
647 return parse_int_literal(text);
648 }
649 if is_sexagesimal_int_shape(text) {
650 return parse_sexagesimal_int(text);
651 }
652 if is_float_literal_shape(text) {
653 return parse_float_literal(text);
654 }
655 if let Some(iso) = normalize_timestamp(text)? {
656 return Ok(if iso.contains('T') {
664 Value::Datetime(iso)
665 } else {
666 Value::Date(iso)
667 });
668 }
669 Ok(Value::Str(text.to_string()))
670}
671
672static INT_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
681 regex::Regex::new(
682 r"^(?:[-+]?0b[0-1_]+|[-+]?0[0-7_]+|[-+]?(?:0|[1-9][0-9_]*)|[-+]?0x[0-9a-fA-F_]+)$",
683 )
684 .unwrap()
685});
686
687fn is_int_literal_shape(text: &str) -> bool {
688 INT_RE.is_match(text)
689}
690
691static SEXAGESIMAL_INT_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
701 regex::Regex::new(r"^[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+$").unwrap()
702});
703
704fn is_sexagesimal_int_shape(text: &str) -> bool {
705 SEXAGESIMAL_INT_RE.is_match(text)
706}
707
708fn parse_sexagesimal_int(text: &str) -> Result<Value, ParseError> {
723 let neg = text.starts_with('-');
724 let t = text.strip_prefix(['+', '-']).unwrap_or(text);
725 let mut acc = BigInt::from(0);
726 let sixty = BigInt::from(60);
727 for group in t.split(':') {
728 let cleaned: String = group.chars().filter(|&c| c != '_').collect();
729 let digit = BigInt::parse_bytes(cleaned.as_bytes(), 10)
732 .expect("SEXAGESIMAL_INT_RE guarantees decimal digit groups");
733 acc = acc * &sixty + digit;
734 }
735 let value = if neg { -acc } else { acc };
736 let digit_count = value.to_string().trim_start_matches('-').len();
737 if digit_count > MAX_INT_DIGITS {
738 return Err(ParseError::new(
739 1,
740 1,
741 over_cap_message("invalid YAML: ", digit_count),
742 ));
743 }
744 Ok(Value::Int(value))
745}
746
747fn parse_int_literal(text: &str) -> Result<Value, ParseError> {
753 let neg = text.starts_with('-');
754 let t = text.strip_prefix(['+', '-']).unwrap_or(text);
755 let cleaned: String = t.chars().filter(|&c| c != '_').collect();
756 let (radix, digits) = if let Some(rest) = cleaned.strip_prefix("0x") {
757 (16, rest)
758 } else if let Some(rest) = cleaned.strip_prefix("0b") {
759 (2, rest)
760 } else if cleaned.starts_with('0') && cleaned.len() > 1 {
761 (8, &cleaned[1..])
762 } else {
763 (10, cleaned.as_str())
764 };
765 if radix == 10 && digits.len() > MAX_INT_DIGITS {
766 return Err(ParseError::new(
767 1,
768 1,
769 over_cap_message("invalid YAML: ", digits.len()),
770 ));
771 }
772 let magnitude = BigInt::parse_bytes(digits.as_bytes(), radix)
773 .expect("is_int_literal_shape guarantees valid digits for the detected radix");
774 let value = if neg { -magnitude } else { magnitude };
775 Ok(Value::Int(value))
776}
777
778static FLOAT_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
785 regex::Regex::new(
786 r"^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)?|\.[0-9][0-9_]*(?:[eE][-+][0-9]+)?|[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN))$",
787 )
788 .unwrap()
789});
790
791fn is_float_literal_shape(text: &str) -> bool {
792 FLOAT_RE.is_match(text)
793}
794
795fn parse_float_literal(text: &str) -> Result<Value, ParseError> {
796 match text {
797 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => {
798 return Ok(Value::Float(f64::INFINITY));
799 }
800 "-.inf" | "-.Inf" | "-.INF" => return Ok(Value::Float(f64::NEG_INFINITY)),
801 ".nan" | ".NaN" | ".NAN" => return Ok(Value::Float(f64::NAN)),
802 _ => {}
803 }
804 let cleaned: String = text.chars().filter(|&c| c != '_').collect();
805 cleaned.parse::<f64>().map(Value::Float).map_err(|_| {
806 ParseError::new(
807 1,
808 1,
809 format!("invalid YAML: invalid float literal {text:?}"),
810 )
811 })
812}
813
814fn normalize_timestamp(text: &str) -> Result<Option<String>, ParseError> {
835 static RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
836 regex::Regex::new(
837 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]))?))?)?$",
838 )
839 .unwrap()
840 });
841 let Some(caps) = RE.captures(text) else {
842 return Ok(None);
843 };
844 let bad = |what: &str| {
845 Err(ParseError::new(
846 1,
847 1,
848 format!("invalid YAML: {text:?} is timestamp-shaped but names an invalid {what}"),
849 ))
850 };
851 let year: u32 = caps["year"].parse().unwrap_or(u32::MAX);
852 let month: u32 = caps["month"].parse().unwrap_or(u32::MAX);
853 let day: u32 = caps["day"].parse().unwrap_or(u32::MAX);
854 if !crate::schema::valid_ymd(year, month, day) {
855 return bad("calendar date");
856 }
857 let Some(hour_m) = caps.name("hour") else {
858 return Ok(Some(format!("{year:04}-{month:02}-{day:02}")));
859 };
860 let hour: u32 = hour_m.as_str().parse().unwrap_or(u32::MAX);
861 let minute: u32 = caps["minute"].parse().unwrap_or(u32::MAX);
862 let second: u32 = caps["second"].parse().unwrap_or(u32::MAX);
863 if !crate::schema::valid_hms(hour, minute, second) {
864 return bad("time of day");
865 }
866 let mut out = format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}");
867 if let Some(frac) = caps.name("fraction") {
868 let mut digits = frac.as_str().to_string();
869 while digits.len() < 6 {
870 digits.push('0');
871 }
872 digits.truncate(6);
873 out.push('.');
874 out.push_str(&digits);
875 }
876 match caps.name("tz_sign") {
877 Some(sign) => {
878 let tz_hour: u32 = caps["tz_hour"].parse().unwrap_or(u32::MAX);
879 let tz_minute: u32 = caps
880 .name("tz_minute")
881 .map(|m| m.as_str().parse().unwrap_or(u32::MAX))
882 .unwrap_or(0);
883 if tz_hour > 23 || tz_minute > 59 {
884 return bad("timezone offset");
885 }
886 out.push_str(sign.as_str());
887 out.push_str(&format!("{tz_hour:02}:{tz_minute:02}"));
888 }
889 None if text.trim_end().ends_with('Z') => out.push_str("+00:00"),
890 None => {}
891 }
892 Ok(Some(out))
893}
894
895pub fn write_yaml(
901 doc: &Doc,
902 strict: bool,
903 report: Option<&mut WriteReport>,
904) -> Result<String, WriteError> {
905 let grouped = doc.to_grouped();
906 let mut rep = check_yaml_grouped(&grouped);
907 add_interleaving_diagnostic(doc, &mut rep);
908 let mut out = String::new();
909 write_node(&grouped, 0, &mut out, true);
910 if out.ends_with('\n') {
911 out.pop();
912 }
913 crate::report::finish_write(out, rep, strict, report)
914}
915
916fn add_interleaving_diagnostic(doc: &Doc, rep: &mut WriteReport) {
922 if doc.has_interleaving_loss() {
923 rep.add(
924 "$",
925 "format.interleaving-lost",
926 "cross-label interleaving could not be written; same-label edges were grouped",
927 Severity::Warning,
928 );
929 }
930}
931
932pub fn check_yaml(doc: &Doc) -> WriteReport {
936 let grouped = doc.to_grouped();
937 let mut rep = check_yaml_grouped(&grouped);
938 add_interleaving_diagnostic(doc, &mut rep);
939 rep
940}
941
942fn check_yaml_grouped(grouped: &Value) -> WriteReport {
943 let mut rep = WriteReport::new();
944 let mut path = String::from("$");
945 crate::formats::visit_grouped(grouped, &mut path, &mut |visited, path| match visited {
946 crate::formats::Visited::Edge { label } if label.contains('\u{0085}') => {
947 rep.add(
948 path,
949 "string.line-break-char",
950 "label contains U+0085 (NEL); written double-quoted to round-trip correctly",
951 Severity::Warning,
952 );
953 }
954 crate::formats::Visited::Node {
955 value: Value::Str(s),
956 } if s.contains('\u{0085}') => {
957 rep.add(
958 path,
959 "string.line-break-char",
960 "value contains U+0085 (NEL); written double-quoted to round-trip correctly",
961 Severity::Warning,
962 );
963 }
964 _ => {}
965 });
966 rep
967}
968
969pub(crate) struct Yaml;
973
974impl crate::formats::Codec for Yaml {
975 const NAME: &'static str = "yaml";
976
977 fn read(text: &str) -> Result<Doc, OmnistError> {
978 read_yaml(text)
979 }
980
981 fn write(doc: &Doc) -> Result<String, OmnistError> {
982 write_yaml(doc, false, None).map_err(Into::into)
983 }
984
985 fn check(doc: &Doc) -> WriteReport {
986 check_yaml(doc)
987 }
988}
989
990fn indent(out: &mut String, level: usize) {
991 for _ in 0..level {
992 out.push_str(" ");
993 }
994}
995
996fn write_node(node: &Value, level: usize, out: &mut String, top: bool) {
1002 match node {
1003 Value::Object(map) if map.is_empty() => {
1004 out.push_str("{}\n");
1005 }
1006 Value::Array(items) if items.is_empty() => {
1007 out.push_str("[]\n");
1008 }
1009 Value::Object(map) => {
1010 for (label, child) in map {
1011 indent(out, level);
1012 write_scalar(label, out);
1013 out.push(':');
1014 write_child(child, level, out);
1015 }
1016 let _ = top;
1017 }
1018 Value::Array(items) => {
1019 for item in items {
1020 indent(out, level);
1021 out.push('-');
1022 write_seq_child(item, level, out);
1023 }
1024 }
1025 other => {
1026 write_scalar_value(other, out);
1027 out.push('\n');
1028 }
1029 }
1030}
1031
1032fn write_child(child: &Value, level: usize, out: &mut String) {
1033 match child {
1034 Value::Object(m) if !m.is_empty() => {
1035 out.push('\n');
1036 write_node(child, level + 1, out, false);
1037 }
1038 Value::Array(a) if !a.is_empty() => {
1039 out.push('\n');
1040 write_node(child, level, out, false);
1041 }
1042 _ => {
1043 out.push(' ');
1044 write_node(child, level + 1, out, false);
1045 }
1046 }
1047}
1048
1049fn write_seq_child(item: &Value, level: usize, out: &mut String) {
1050 match item {
1051 Value::Object(m) if !m.is_empty() => {
1052 out.push(' ');
1053 let mut first = true;
1058 for (label, child) in m {
1059 if !first {
1060 indent(out, level + 1);
1061 }
1062 first = false;
1063 write_scalar(label, out);
1064 out.push(':');
1065 write_child(child, level + 1, out);
1066 }
1067 }
1068 Value::Array(a) if !a.is_empty() => {
1069 out.push('\n');
1070 write_node(item, level + 1, out, false);
1071 }
1072 _ => {
1073 out.push(' ');
1074 write_node(item, level + 1, out, false);
1075 }
1076 }
1077}
1078
1079fn write_scalar(s: &str, out: &mut String) {
1080 write_scalar_value(&Value::Str(s.to_string()), out);
1081}
1082
1083fn write_scalar_value(v: &Value, out: &mut String) {
1084 match v {
1085 Value::Null => out.push_str("null"),
1086 Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
1087 Value::Int(i) => out.push_str(&i.to_string()),
1088 Value::Float(x) => write_float(*x, out),
1089 Value::Str(s) => write_yaml_string(s, out),
1094 Value::Date(s) | Value::Datetime(s) => out.push_str(s),
1100 Value::Time(s) => write_yaml_string(s, out),
1108 Value::Object(_) | Value::Array(_) => {
1109 unreachable!("write_scalar_value is only ever called on a leaf")
1110 }
1111 }
1112}
1113
1114fn write_float(x: f64, out: &mut String) {
1117 float_fmt::write_float(x, ".nan", ".inf", "-.inf", out);
1118}
1119
1120fn write_yaml_string(s: &str, out: &mut String) {
1126 if needs_quoting(s) {
1127 write_quoted(s, &YAML_ESCAPES, out);
1128 } else {
1129 out.push_str(s);
1130 }
1131}
1132
1133fn needs_quoting(s: &str) -> bool {
1134 if s.is_empty() || s.contains('\u{0085}') || s.contains('\n') {
1135 return true;
1136 }
1137 if matches!(resolve_plain_scalar(s), Ok(Value::Str(ref t)) if t == s) {
1138 } else {
1142 return true; }
1144 let first = s.chars().next().unwrap();
1148 if matches!(
1149 first,
1150 '-' | '?'
1151 | ':'
1152 | ','
1153 | '['
1154 | ']'
1155 | '{'
1156 | '}'
1157 | '#'
1158 | '&'
1159 | '*'
1160 | '!'
1161 | '|'
1162 | '>'
1163 | '\''
1164 | '"'
1165 | '%'
1166 | '@'
1167 | '`'
1168 | ' '
1169 ) {
1170 return true;
1171 }
1172 if s.ends_with(' ') || s.contains(": ") || s.ends_with(':') || s.contains(" #") {
1173 return true;
1174 }
1175 false
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180 use super::*;
1181 use crate::document::{Doc, Scalar, Value};
1182
1183 fn obj(pairs: Vec<(&str, Value)>) -> Value {
1184 Value::Object(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
1185 }
1186
1187 fn doc_of(v: Value) -> Doc {
1188 Doc::of(&v).unwrap()
1189 }
1190
1191 #[test]
1194 fn reads_every_scalar_kind() {
1195 let doc = read_yaml("a: 1\nb: \"s\"\nc: true\nd: null\ne: 1.5\n").unwrap();
1196 let root = doc.root();
1197 assert_eq!(
1198 *root.get_one("a").unwrap().value().unwrap(),
1199 Scalar::Int((1).into())
1200 );
1201 assert_eq!(
1202 *root.get_one("b").unwrap().value().unwrap(),
1203 Scalar::Str("s".to_string())
1204 );
1205 assert_eq!(
1206 *root.get_one("c").unwrap().value().unwrap(),
1207 Scalar::Bool(true)
1208 );
1209 assert_eq!(*root.get_one("d").unwrap().value().unwrap(), Scalar::Null);
1210 assert_eq!(
1211 *root.get_one("e").unwrap().value().unwrap(),
1212 Scalar::Float(1.5)
1213 );
1214 }
1215
1216 #[test]
1217 fn reads_yaml_1_1_bool_spellings_but_not_bare_y_or_n() {
1218 let doc = read_yaml("a: yes\nb: no\nc: on\nd: off\ne: Yes\nf: y\ng: n\n").unwrap();
1219 let root = doc.root();
1220 assert_eq!(
1221 *root.get_one("a").unwrap().value().unwrap(),
1222 Scalar::Bool(true)
1223 );
1224 assert_eq!(
1225 *root.get_one("b").unwrap().value().unwrap(),
1226 Scalar::Bool(false)
1227 );
1228 assert_eq!(
1229 *root.get_one("c").unwrap().value().unwrap(),
1230 Scalar::Bool(true)
1231 );
1232 assert_eq!(
1233 *root.get_one("d").unwrap().value().unwrap(),
1234 Scalar::Bool(false)
1235 );
1236 assert_eq!(
1237 *root.get_one("e").unwrap().value().unwrap(),
1238 Scalar::Bool(true)
1239 );
1240 assert_eq!(
1243 *root.get_one("f").unwrap().value().unwrap(),
1244 Scalar::Str("y".to_string())
1245 );
1246 assert_eq!(
1247 *root.get_one("g").unwrap().value().unwrap(),
1248 Scalar::Str("n".to_string())
1249 );
1250 }
1251
1252 #[test]
1253 fn quoted_yes_stays_a_string_not_a_bool() {
1254 let doc = read_yaml("a: \"yes\"\n").unwrap();
1255 assert_eq!(
1256 *doc.root().get_one("a").unwrap().value().unwrap(),
1257 Scalar::Str("yes".to_string())
1258 );
1259 }
1260
1261 #[test]
1262 fn reads_null_spellings() {
1263 let doc = read_yaml("a: ~\nb: null\nc: Null\nd: NULL\ne:\n").unwrap();
1264 let root = doc.root();
1265 for label in ["a", "b", "c", "d", "e"] {
1266 assert_eq!(*root.get_one(label).unwrap().value().unwrap(), Scalar::Null);
1267 }
1268 }
1269
1270 #[test]
1271 fn reads_negative_and_hex_and_octal_and_binary_ints() {
1272 let doc = read_yaml("a: -5\nb: 0x1A\nc: 017\nd: 0b101\ne: 1_000\n").unwrap();
1277 let root = doc.root();
1278 assert_eq!(
1279 *root.get_one("a").unwrap().value().unwrap(),
1280 Scalar::Int((-5).into())
1281 );
1282 assert_eq!(
1283 *root.get_one("b").unwrap().value().unwrap(),
1284 Scalar::Int((26).into())
1285 );
1286 assert_eq!(
1287 *root.get_one("c").unwrap().value().unwrap(),
1288 Scalar::Int((15).into())
1289 );
1290 assert_eq!(
1291 *root.get_one("d").unwrap().value().unwrap(),
1292 Scalar::Int((5).into())
1293 );
1294 assert_eq!(
1295 *root.get_one("e").unwrap().value().unwrap(),
1296 Scalar::Int((1000).into())
1297 );
1298 }
1299
1300 #[test]
1301 fn a_yaml_1_2_style_0o_octal_prefix_is_not_recognized_and_stays_a_string() {
1302 let doc = read_yaml("a: 0o17\n").unwrap();
1303 assert_eq!(
1304 *doc.root().get_one("a").unwrap().value().unwrap(),
1305 Scalar::Str("0o17".to_string())
1306 );
1307 }
1308
1309 #[test]
1310 fn reads_legacy_sexagesimal_int_forms() {
1311 let doc =
1315 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")
1316 .unwrap();
1317 let root = doc.root();
1318 assert_eq!(
1319 *root.get_one("a").unwrap().value().unwrap(),
1320 Scalar::Int((43200).into())
1321 );
1322 assert_eq!(
1323 *root.get_one("b").unwrap().value().unwrap(),
1324 Scalar::Int((80).into())
1325 );
1326 assert_eq!(
1327 *root.get_one("c").unwrap().value().unwrap(),
1328 Scalar::Int((3723).into())
1329 );
1330 assert_eq!(
1331 *root.get_one("d").unwrap().value().unwrap(),
1332 Scalar::Int((-80).into())
1333 );
1334 assert_eq!(
1335 *root.get_one("e").unwrap().value().unwrap(),
1336 Scalar::Int((80).into())
1337 );
1338 assert_eq!(
1339 *root.get_one("f").unwrap().value().unwrap(),
1340 Scalar::Int((7425).into())
1341 );
1342 assert_eq!(
1343 *root.get_one("g").unwrap().value().unwrap(),
1344 Scalar::Int((750).into())
1345 );
1346 }
1347
1348 #[test]
1349 fn sexagesimal_first_group_over_i64_range_parses() {
1350 let text = format!("a: {}:0\n", "9".repeat(20));
1355 let doc = read_yaml(&text).unwrap();
1356 let value = doc.root().child("a").unwrap().value().unwrap();
1357 assert_eq!(
1358 value,
1359 &Scalar::Int(num_bigint::BigInt::parse_bytes(b"5999999999999999999940", 10).unwrap())
1360 );
1361 }
1362
1363 #[test]
1364 fn sexagesimal_fold_overflow_across_many_in_range_groups_parses() {
1365 let text = format!("a: 1{}\n", ":59".repeat(15));
1370 let doc = read_yaml(&text).unwrap();
1371 let value = doc.root().child("a").unwrap().value().unwrap();
1372 assert_eq!(
1373 value,
1374 &Scalar::Int(
1375 num_bigint::BigInt::parse_bytes(b"940369969151999999999999999", 10).unwrap()
1376 )
1377 );
1378 }
1379
1380 #[test]
1381 fn sexagesimal_fold_still_rejects_past_the_digit_cap() {
1382 let text = format!("a: 1{}\n", ":59".repeat(2500));
1389 let err = read_yaml(&text).unwrap_err();
1390 assert!(
1391 matches!(&err, OmnistError::Parse(e) if e.message.contains("4300-digit")),
1392 "got {err:?}"
1393 );
1394 }
1395
1396 #[test]
1397 fn sexagesimal_shape_with_leading_zero_or_out_of_range_group_stays_a_string() {
1398 let doc = read_yaml("a: 0:0:1\nb: 1:60\nc: 1:600\nd: 01:20\n").unwrap();
1403 let root = doc.root();
1404 assert_eq!(
1405 *root.get_one("a").unwrap().value().unwrap(),
1406 Scalar::Str("0:0:1".to_string())
1407 );
1408 assert_eq!(
1409 *root.get_one("b").unwrap().value().unwrap(),
1410 Scalar::Str("1:60".to_string())
1411 );
1412 assert_eq!(
1413 *root.get_one("c").unwrap().value().unwrap(),
1414 Scalar::Str("1:600".to_string())
1415 );
1416 assert_eq!(
1417 *root.get_one("d").unwrap().value().unwrap(),
1418 Scalar::Str("01:20".to_string())
1419 );
1420 }
1421
1422 #[test]
1423 fn reads_float_and_inf_and_nan_tokens() {
1424 let doc = read_yaml("a: 1.5\nb: .inf\nc: -.inf\nd: .nan\ne: 1.0e+3\n").unwrap();
1425 let root = doc.root();
1426 assert_eq!(
1427 *root.get_one("a").unwrap().value().unwrap(),
1428 Scalar::Float(1.5)
1429 );
1430 assert_eq!(
1431 *root.get_one("b").unwrap().value().unwrap(),
1432 Scalar::Float(f64::INFINITY)
1433 );
1434 assert_eq!(
1435 *root.get_one("c").unwrap().value().unwrap(),
1436 Scalar::Float(f64::NEG_INFINITY)
1437 );
1438 assert!(
1439 matches!(root.get_one("d").unwrap().value().unwrap(), Scalar::Float(x) if x.is_nan())
1440 );
1441 assert_eq!(
1442 *root.get_one("e").unwrap().value().unwrap(),
1443 Scalar::Float(1000.0)
1444 );
1445 }
1446
1447 #[test]
1448 fn a_bare_exponent_without_a_decimal_point_is_not_float_shaped_and_stays_a_string() {
1449 let doc = read_yaml("a: 1e3\nb: 1.0e3\n").unwrap();
1453 let root = doc.root();
1454 assert_eq!(
1455 *root.get_one("a").unwrap().value().unwrap(),
1456 Scalar::Str("1e3".to_string())
1457 );
1458 assert_eq!(
1459 *root.get_one("b").unwrap().value().unwrap(),
1460 Scalar::Str("1.0e3".to_string())
1461 );
1462 }
1463
1464 #[test]
1465 fn reads_bare_date_as_iso_string() {
1466 let doc = read_yaml("a: 2024-01-15\n").unwrap();
1467 assert_eq!(
1468 *doc.root().get_one("a").unwrap().value().unwrap(),
1469 Scalar::Date("2024-01-15".to_string())
1470 );
1471 }
1472
1473 #[test]
1474 fn genuine_date_and_datetime_values_write_bare_and_a_time_value_writes_quoted() {
1475 let v = obj(vec![
1480 ("d", Value::Date("2024-01-15".to_string())),
1481 ("dt", Value::Datetime("2024-01-15T12:00:00".to_string())),
1482 ("t", Value::Time("12:00:00".to_string())),
1483 ]);
1484 let doc = doc_of(v);
1485 let text = write_yaml(&doc, false, None).unwrap();
1486 assert!(text.contains("d: 2024-01-15\n"));
1487 assert!(text.contains("dt: 2024-01-15T12:00:00\n"));
1488 assert!(text.contains("t: \"12:00:00\""));
1489 }
1490
1491 #[test]
1492 fn reads_loose_timestamp_and_normalizes_to_canonical_iso() {
1493 let doc = read_yaml("a: 2001-2-3 4:05:06.7 Z\n").unwrap();
1499 assert_eq!(
1500 *doc.root().get_one("a").unwrap().value().unwrap(),
1501 Scalar::Datetime("2001-02-03T04:05:06.700000+00:00".to_string())
1502 );
1503 }
1504
1505 #[test]
1506 fn a_single_digit_minute_or_second_is_not_timestamp_shaped_and_stays_a_string() {
1507 let doc = read_yaml("a: 2001-2-3 4:5:6\n").unwrap();
1512 assert_eq!(
1513 *doc.root().get_one("a").unwrap().value().unwrap(),
1514 Scalar::Str("2001-2-3 4:5:6".to_string())
1515 );
1516 }
1517
1518 #[test]
1519 fn reads_datetime_with_no_timezone_at_all() {
1520 let doc = read_yaml("a: 2024-01-15T12:30:00\n").unwrap();
1524 assert_eq!(
1525 *doc.root().get_one("a").unwrap().value().unwrap(),
1526 Scalar::Datetime("2024-01-15T12:30:00".to_string())
1527 );
1528 }
1529
1530 #[test]
1531 fn reads_timestamp_with_explicit_offset() {
1532 let doc = read_yaml("a: 2001-12-14T21:59:43.10-05:00\n").unwrap();
1533 assert_eq!(
1534 *doc.root().get_one("a").unwrap().value().unwrap(),
1535 Scalar::Datetime("2001-12-14T21:59:43.100000-05:00".to_string())
1536 );
1537 }
1538
1539 #[test]
1540 fn a_string_that_merely_looks_like_a_short_date_but_isnt_shaped_right_stays_a_string() {
1541 let doc = read_yaml("a: 2024-1\n").unwrap();
1542 assert_eq!(
1543 *doc.root().get_one("a").unwrap().value().unwrap(),
1544 Scalar::Str("2024-1".to_string())
1545 );
1546 }
1547
1548 #[test]
1551 fn reads_nested_mapping_and_sequence() {
1552 let doc = read_yaml("a:\n b:\n c: 1\nm:\n - 1\n - 2\n - 3\n").unwrap();
1553 let root = doc.root();
1554 let a = root.get_one("a").unwrap();
1555 let b = a.get_one("b").unwrap();
1556 assert_eq!(
1557 *b.get_one("c").unwrap().value().unwrap(),
1558 Scalar::Int((1).into())
1559 );
1560 let ms = root.get("m");
1561 assert_eq!(ms.len(), 3);
1562 assert_eq!(*ms[2].value().unwrap(), Scalar::Int((3).into()));
1563 }
1564
1565 #[test]
1566 fn reads_flow_style_mapping_and_sequence() {
1567 let doc = read_yaml("a: {b: 1, c: 2}\nm: [1, 2, 3]\n").unwrap();
1568 let root = doc.root();
1569 let a = root.get_one("a").unwrap();
1570 assert_eq!(
1571 *a.get_one("b").unwrap().value().unwrap(),
1572 Scalar::Int((1).into())
1573 );
1574 assert_eq!(root.get("m").len(), 3);
1575 }
1576
1577 #[test]
1578 fn bare_top_level_sequence_is_a_document_error_not_a_parse_error() {
1579 let err = read_yaml("- 1\n- 2\n").unwrap_err();
1580 assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
1581 }
1582
1583 #[test]
1584 fn sequence_of_sequences_is_a_document_error() {
1585 let err = read_yaml("m:\n - [1, 2]\n").unwrap_err();
1586 assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
1587 }
1588
1589 #[test]
1590 fn empty_input_reads_as_a_null_document() {
1591 let doc = read_yaml("").unwrap();
1592 assert_eq!(*doc.root().value().unwrap(), Scalar::Null);
1593 }
1594
1595 #[test]
1596 fn explicit_empty_document_marker_reads_as_a_null_document() {
1597 let doc = read_yaml("---\n").unwrap();
1602 assert_eq!(*doc.root().value().unwrap(), Scalar::Null);
1603 }
1604
1605 #[test]
1606 fn duplicate_mapping_keys_last_value_wins() {
1607 let doc = read_yaml("a: 1\nb: 2\na: 3\n").unwrap();
1608 let root = doc.root();
1609 assert_eq!(root.labels(), vec!["a".to_string(), "b".to_string()]);
1610 assert_eq!(
1611 *root.get_one("a").unwrap().value().unwrap(),
1612 Scalar::Int((3).into())
1613 );
1614 }
1615
1616 #[test]
1617 fn invalid_yaml_syntax_is_a_parse_error() {
1618 let err = read_yaml("a: [1, 2\n").unwrap_err();
1619 assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
1620 }
1621
1622 #[test]
1623 fn multiple_documents_is_a_parse_error() {
1624 let err = read_yaml("a: 1\n---\nb: 2\n").unwrap_err();
1625 assert!(
1626 matches!(&err, OmnistError::Parse(e) if e.message.contains("single document")),
1627 "got {err:?}"
1628 );
1629 }
1630
1631 #[test]
1632 fn nesting_past_max_depth_is_a_document_error() {
1633 let mut text = String::new();
1636 for i in 0..=crate::document::MAX_DEPTH {
1637 text.push_str(&" ".repeat(i));
1638 text.push_str("a:\n");
1639 }
1640 let err = read_yaml(&text).unwrap_err();
1641 assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
1642 }
1643
1644 #[test]
1645 fn integer_literal_over_digit_cap_is_rejected() {
1646 let text = format!("a: {}\n", "9".repeat(MAX_INT_DIGITS + 1));
1647 let err = read_yaml(&text).unwrap_err();
1648 assert!(
1649 matches!(&err, OmnistError::Parse(e) if e.message.contains("4300-digit")),
1650 "got {err:?}"
1651 );
1652 }
1653
1654 #[test]
1655 fn i64_min_round_trips_through_yaml() {
1656 let doc = read_yaml("a: -9223372036854775808\n").unwrap();
1662 assert_eq!(
1663 *doc.root().get_one("a").unwrap().value().unwrap(),
1664 Scalar::Int((i64::MIN).into())
1665 );
1666 }
1667
1668 #[test]
1669 fn positive_integer_one_past_i64_max_parses() {
1670 let doc = read_yaml("a: 9223372036854775808\n").unwrap();
1673 let value = doc.root().child("a").unwrap().value().unwrap();
1674 assert_eq!(value, &Scalar::Int(num_bigint::BigInt::from(i64::MAX) + 1));
1675 }
1676
1677 #[test]
1678 fn negative_integer_one_past_i64_min_parses() {
1679 let doc = read_yaml("a: -9223372036854775809\n").unwrap();
1682 let value = doc.root().child("a").unwrap().value().unwrap();
1683 assert_eq!(value, &Scalar::Int(num_bigint::BigInt::from(i64::MIN) - 1));
1684 }
1685
1686 #[test]
1687 fn integer_literal_over_i64_range_parses() {
1688 let text = format!("a: {}\n", "9".repeat(20));
1689 let doc = read_yaml(&text).unwrap();
1690 let value = doc.root().child("a").unwrap().value().unwrap();
1691 assert_eq!(
1692 value,
1693 &Scalar::Int(num_bigint::BigInt::parse_bytes(b"99999999999999999999", 10).unwrap())
1694 );
1695 }
1696
1697 #[test]
1700 fn reads_anchor_and_alias_as_a_deep_copy() {
1701 let doc = read_yaml("a: &x [1, 2, 3]\nb: *x\n").unwrap();
1702 let root = doc.root();
1703 assert_eq!(root.get("a").len(), 3);
1704 assert_eq!(root.get("b").len(), 3);
1705 assert_eq!(*root.get("b")[1].value().unwrap(), Scalar::Int((2).into()));
1706 }
1707
1708 #[test]
1709 fn unknown_alias_is_a_parse_error() {
1710 let err = read_yaml("a: *nope\n").unwrap_err();
1711 assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
1712 }
1713
1714 fn billion_laughs_yaml(generations: usize) -> String {
1732 let mut out = String::new();
1733 out.push_str("a0: &a0 [x, x]\n");
1734 for i in 1..generations {
1735 out.push_str(&format!("a{i}: &a{i} [*a{prev}, *a{prev}]\n", prev = i - 1));
1736 }
1737 out
1738 }
1739
1740 #[test]
1741 fn billion_laughs_alias_amplification_is_rejected_fast_issue_42() {
1742 let text = billion_laughs_yaml(24);
1743 assert!(text.len() < 1000, "source text should be tiny: {text:?}");
1745
1746 let start = std::time::Instant::now();
1747 let err = read_yaml(&text).unwrap_err();
1748 let elapsed = start.elapsed();
1749
1750 assert!(
1751 matches!(&err, OmnistError::Parse(e) if e.message.contains("materializes more than")
1752 && e.message.contains("100000")),
1753 "expected a materialized-node-limit ParseError, got {err:?}"
1754 );
1755 assert!(
1756 elapsed < std::time::Duration::from_secs(5),
1757 "fix should reject the bomb almost immediately, took {elapsed:?}"
1758 );
1759 }
1760
1761 #[test]
1762 fn moderate_legitimate_nested_alias_reuse_still_works() {
1763 let doc = read_yaml(
1768 "base: &base\n x: 1\n y: 2\na: *base\nb: *base\nc: *base\nlist: &list [1, 2, 3]\nd: *list\ne: *list\n",
1769 )
1770 .unwrap();
1771 let root = doc.root();
1772 for label in ["a", "b", "c"] {
1773 let node = root.get_one(label).unwrap();
1774 assert_eq!(
1775 *node.get_one("x").unwrap().value().unwrap(),
1776 Scalar::Int((1).into())
1777 );
1778 assert_eq!(
1779 *node.get_one("y").unwrap().value().unwrap(),
1780 Scalar::Int((2).into())
1781 );
1782 }
1783 for label in ["d", "e"] {
1784 assert_eq!(root.get(label).len(), 3);
1785 }
1786 }
1787
1788 #[test]
1791 fn merge_key_from_a_mapping_merges_with_local_keys_winning() {
1792 let doc =
1793 read_yaml("base: &b\n x: 1\n y: 2\nchild:\n <<: *b\n y: 20\n z: 3\n").unwrap();
1794 let child = doc.root().get_one("child").unwrap();
1795 assert_eq!(
1796 *child.get_one("x").unwrap().value().unwrap(),
1797 Scalar::Int((1).into())
1798 );
1799 assert_eq!(
1800 *child.get_one("y").unwrap().value().unwrap(),
1801 Scalar::Int((20).into()),
1802 "an explicit local key beats the merged-in value"
1803 );
1804 assert_eq!(
1805 *child.get_one("z").unwrap().value().unwrap(),
1806 Scalar::Int((3).into())
1807 );
1808 }
1809
1810 #[test]
1811 fn merge_key_from_a_sequence_of_mappings_merges_each_in_order() {
1812 let doc =
1815 read_yaml("a: &a\n x: 1\nb: &b\n x: 2\n y: 3\nchild:\n <<: [*a, *b]\n").unwrap();
1816 let child = doc.root().get_one("child").unwrap();
1817 assert_eq!(
1818 *child.get_one("x").unwrap().value().unwrap(),
1819 Scalar::Int((1).into())
1820 );
1821 assert_eq!(
1822 *child.get_one("y").unwrap().value().unwrap(),
1823 Scalar::Int((3).into())
1824 );
1825 }
1826
1827 #[test]
1828 fn quoted_double_angle_bracket_key_is_a_literal_string_not_a_merge() {
1829 let doc = read_yaml("a:\n \"<<\": 1\n").unwrap();
1830 let a = doc.root().get_one("a").unwrap();
1831 assert_eq!(
1832 *a.get_one("<<").unwrap().value().unwrap(),
1833 Scalar::Int((1).into())
1834 );
1835 }
1836
1837 #[test]
1842 fn merge_key_from_a_non_map_scalar_source_is_a_clean_parse_error_omnist_ts_46() {
1843 let err = read_yaml("child:\n <<: 5\n y: 2\n").unwrap_err();
1844 assert!(
1845 matches!(&err, OmnistError::Parse(e) if e.message.contains("merge key")),
1846 "got {err:?}"
1847 );
1848 }
1849
1850 #[test]
1851 fn merge_key_from_a_sequence_containing_a_scalar_is_a_clean_parse_error() {
1852 let err = read_yaml("child:\n <<: [1, 2]\n").unwrap_err();
1853 assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
1854 }
1855
1856 #[test]
1857 fn merge_source_with_a_non_scalar_key_is_a_clean_parse_error() {
1858 let err = read_yaml("base: &b\n ? [1, 2]\n : 3\nchild:\n <<: *b\n").unwrap_err();
1863 assert!(
1864 matches!(&err, OmnistError::Parse(e) if e.message.contains("mapping key must be a scalar")),
1865 "got {err:?}"
1866 );
1867 }
1868
1869 #[test]
1870 fn merge_key_from_an_alias_to_a_scalar_is_a_clean_parse_error() {
1871 let err = read_yaml("base: &b 5\nchild:\n <<: *b\n").unwrap_err();
1872 assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
1873 }
1874
1875 #[test]
1878 fn explicit_tags_construct_the_named_type_regardless_of_spelling() {
1879 let doc = read_yaml("a: !!str yes\nb: !!int \"5\"\nc: !!bool \"true\"\n").unwrap();
1880 let root = doc.root();
1881 assert_eq!(
1882 *root.get_one("a").unwrap().value().unwrap(),
1883 Scalar::Str("yes".to_string())
1884 );
1885 assert_eq!(
1886 *root.get_one("b").unwrap().value().unwrap(),
1887 Scalar::Int((5).into())
1888 );
1889 assert_eq!(
1890 *root.get_one("c").unwrap().value().unwrap(),
1891 Scalar::Bool(true)
1892 );
1893 }
1894
1895 #[test]
1896 fn unsupported_explicit_tag_is_a_parse_error() {
1897 let err = read_yaml("a: !!binary \"x\"\n").unwrap_err();
1898 assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
1899 }
1900
1901 #[test]
1902 fn explicit_bool_tag_accepts_the_full_yaml_1_1_spelling_set_not_just_true_false() {
1903 let doc = read_yaml("a: !!bool \"yes\"\nb: !!bool \"On\"\nc: !!bool \"OFF\"\n").unwrap();
1908 let root = doc.root();
1909 assert_eq!(
1910 *root.get_one("a").unwrap().value().unwrap(),
1911 Scalar::Bool(true)
1912 );
1913 assert_eq!(
1914 *root.get_one("b").unwrap().value().unwrap(),
1915 Scalar::Bool(true)
1916 );
1917 assert_eq!(
1918 *root.get_one("c").unwrap().value().unwrap(),
1919 Scalar::Bool(false)
1920 );
1921 }
1922
1923 #[test]
1924 fn invalid_explicit_bool_spelling_is_a_parse_error() {
1925 let err = read_yaml("a: !!bool \"nonsense\"\n").unwrap_err();
1926 assert!(
1927 matches!(&err, OmnistError::Parse(e) if e.message.contains("!!bool")),
1928 "got {err:?}"
1929 );
1930 }
1931
1932 #[test]
1933 fn bare_y_or_n_is_not_a_valid_explicit_bool_spelling() {
1934 let err = read_yaml("a: !!bool \"y\"\n").unwrap_err();
1940 assert!(
1941 matches!(&err, OmnistError::Parse(e) if e.message.contains("!!bool")),
1942 "got {err:?}"
1943 );
1944 }
1945
1946 #[test]
1947 fn bare_on_key_resolves_to_boolean_and_is_rejected_norway_problem() {
1948 let err = read_yaml("on:\n push: true\n").unwrap_err();
1953 assert!(
1954 matches!(&err, OmnistError::Document(e) if e.path == "$"),
1955 "got {err:?}"
1956 );
1957 }
1958
1959 #[test]
1960 fn other_implicit_bool_and_null_key_spellings_are_also_rejected() {
1961 for key in ["off", "yes", "no", "Off", "YES", "~", "null", "true"] {
1962 let text = format!("{key}:\n push: true\n");
1963 let err = read_yaml(&text).unwrap_err();
1964 assert!(
1965 matches!(&err, OmnistError::Document(_)),
1966 "key {key:?} got {err:?}"
1967 );
1968 }
1969 }
1970
1971 #[test]
1972 fn bare_y_or_n_key_is_not_a_bool_and_stays_a_string_label() {
1973 let doc = read_yaml("y:\n push: true\n").unwrap();
1977 assert!(doc.root().get_one("y").is_ok());
1978 let doc = read_yaml("n:\n push: true\n").unwrap();
1979 assert!(doc.root().get_one("n").is_ok());
1980 }
1981
1982 #[test]
1983 fn sexagesimal_looking_key_also_resolves_and_is_rejected() {
1984 let err = read_yaml("12:00:00:\n push: true\n").unwrap_err();
1988 assert!(
1989 matches!(&err, OmnistError::Document(e) if e.path == "$"),
1990 "got {err:?}"
1991 );
1992 }
1993
1994 #[test]
1995 fn describe_non_string_key_renders_pythonic_spellings() {
1996 assert_eq!(describe_non_string_key(&Value::Bool(true)), "True");
1997 assert_eq!(describe_non_string_key(&Value::Bool(false)), "False");
1998 assert_eq!(describe_non_string_key(&Value::Null), "None");
1999 assert_eq!(
2000 describe_non_string_key(&Value::Int((43200).into())),
2001 "43200"
2002 );
2003 assert_eq!(describe_non_string_key(&Value::Float(1.5)), "1.5");
2004 assert_eq!(describe_non_string_key(&Value::Float(1.0)), "1.0");
2007 assert_eq!(describe_non_string_key(&Value::Float(-2.0)), "-2.0");
2008 assert_eq!(
2012 describe_non_string_key(&Value::Str("x".to_string())),
2013 "Str(\"x\")"
2014 );
2015 }
2016
2017 #[test]
2018 fn int_and_float_shaped_mapping_keys_are_rejected_with_python_parity_messages() {
2019 let err = read_yaml("123:\n a: 1\n").unwrap_err();
2023 assert!(
2024 matches!(&err, OmnistError::Document(e) if e.path == "$" && e.message.contains("123")),
2025 "got {err:?}"
2026 );
2027 let err = read_yaml("1.0:\n a: 1\n").unwrap_err();
2028 assert!(
2029 matches!(&err, OmnistError::Document(e) if e.path == "$" && e.message.contains("1.0")),
2030 "got {err:?}"
2031 );
2032 }
2033
2034 #[test]
2035 fn a_non_scalar_mapping_key_is_a_clean_parse_error() {
2036 let err = read_yaml("? [1, 2]\n: 3\n").unwrap_err();
2040 assert!(
2041 matches!(&err, OmnistError::Parse(e) if e.message.contains("mapping key must be a scalar")),
2042 "got {err:?}"
2043 );
2044 }
2045
2046 #[test]
2049 fn round_trips_every_scalar_kind() {
2050 let v = obj(vec![
2051 ("null", Value::Null),
2052 ("bool", Value::Bool(true)),
2053 ("int", Value::Int((42).into())),
2054 ("float", Value::Float(1.5)),
2055 ("str", Value::Str("hi".to_string())),
2056 ]);
2057 let doc = doc_of(v);
2058 let text = write_yaml(&doc, false, None).unwrap();
2059 let back = read_yaml(&text).unwrap();
2060 assert!(doc.eq_doc(&back));
2061 }
2062
2063 #[test]
2064 fn round_trips_integral_float_at_and_above_1e17_boundary_issue_46() {
2065 for x in [1.0e17, 1.0e18, -1.23e17, 9.9e16_f64] {
2069 let doc = doc_of(obj(vec![("a", Value::Float(x))]));
2070 let text = write_yaml(&doc, false, None).unwrap();
2071 let back = read_yaml(&text).unwrap();
2072 assert_eq!(
2073 *back.root().get_one("a").unwrap().value().unwrap(),
2074 Scalar::Float(x),
2075 "x={x} text={text}"
2076 );
2077 }
2078 }
2079
2080 #[test]
2081 fn round_trips_nan_and_infinity_natively_no_adjustment_needed() {
2082 let v = obj(vec![
2083 ("a", Value::Float(f64::NAN)),
2084 ("b", Value::Float(f64::INFINITY)),
2085 ("c", Value::Float(f64::NEG_INFINITY)),
2086 ]);
2087 let doc = doc_of(v);
2088 let mut rep = WriteReport::new();
2089 let text = write_yaml(&doc, false, Some(&mut rep)).unwrap();
2090 assert!(rep.is_empty());
2091 let back = read_yaml(&text).unwrap();
2092 assert!(
2093 matches!(back.root().get_one("a").unwrap().value().unwrap(), Scalar::Float(x) if x.is_nan())
2094 );
2095 assert_eq!(
2096 *back.root().get_one("b").unwrap().value().unwrap(),
2097 Scalar::Float(f64::INFINITY)
2098 );
2099 assert_eq!(
2100 *back.root().get_one("c").unwrap().value().unwrap(),
2101 Scalar::Float(f64::NEG_INFINITY)
2102 );
2103 }
2104
2105 #[test]
2106 fn round_trips_strings_that_look_like_other_scalar_kinds() {
2107 let v = obj(vec![
2108 ("a", Value::Str("yes".to_string())),
2109 ("b", Value::Str("null".to_string())),
2110 ("c", Value::Str("123".to_string())),
2111 ("d", Value::Str("1.5".to_string())),
2112 ("e", Value::Str("".to_string())),
2113 ("f", Value::Str("2024-01-15".to_string())),
2114 ]);
2115 let doc = doc_of(v);
2116 let text = write_yaml(&doc, false, None).unwrap();
2117 let back = read_yaml(&text).unwrap();
2118 assert!(doc.eq_doc(&back), "text was:\n{text}");
2119 }
2120
2121 #[test]
2122 fn round_trips_repeated_labels_as_a_yaml_sequence() {
2123 let doc = doc_of(obj(vec![(
2124 "m",
2125 Value::Array(vec![
2126 Value::Int((1).into()),
2127 Value::Int((2).into()),
2128 Value::Int((3).into()),
2129 ]),
2130 )]));
2131 let text = write_yaml(&doc, false, None).unwrap();
2132 let back = read_yaml(&text).unwrap();
2133 assert!(doc.eq_doc(&back));
2134 }
2135
2136 #[test]
2137 fn round_trips_nested_mappings_and_sequences_of_mappings() {
2138 let v = obj(vec![
2139 (
2140 "a",
2141 obj(vec![
2142 ("b", Value::Int((1).into())),
2143 ("c", Value::Int((2).into())),
2144 ]),
2145 ),
2146 (
2147 "items",
2148 Value::Array(vec![
2149 obj(vec![
2150 ("x", Value::Int((1).into())),
2151 ("y", Value::Int((2).into())),
2152 ]),
2153 obj(vec![
2154 ("x", Value::Int((3).into())),
2155 ("y", Value::Int((4).into())),
2156 ]),
2157 ]),
2158 ),
2159 ]);
2160 let doc = doc_of(v);
2161 let text = write_yaml(&doc, false, None).unwrap();
2162 let back = read_yaml(&text).unwrap();
2163 assert!(doc.eq_doc(&back), "text was:\n{text}");
2164 }
2165
2166 #[test]
2167 fn writes_empty_object_and_array_compactly() {
2168 let doc = doc_of(obj(vec![("o", Value::Object(IndexMap::new()))]));
2169 let text = write_yaml(&doc, false, None).unwrap();
2170 assert!(text.contains("o: {}"));
2171 }
2172
2173 #[test]
2174 fn nel_string_triggers_a_warning_and_still_round_trips() {
2175 let s = format!("a{}b", '\u{0085}');
2176 let doc = doc_of(obj(vec![("s", Value::Str(s.clone()))]));
2177 let mut rep = WriteReport::new();
2178 let text = write_yaml(&doc, false, Some(&mut rep)).unwrap();
2179 assert_eq!(rep.len(), 1);
2180 assert_eq!(rep.adjustments()[0].code, "string.line-break-char");
2181 let back = read_yaml(&text).unwrap();
2182 assert_eq!(
2183 *back.root().get_one("s").unwrap().value().unwrap(),
2184 Scalar::Str(s)
2185 );
2186 }
2187
2188 #[test]
2189 fn strict_write_with_nel_raises_and_carries_the_report() {
2190 let s = format!("x{}y", '\u{0085}');
2191 let doc = doc_of(obj(vec![("s", Value::Str(s))]));
2192 let err = write_yaml(&doc, true, None).unwrap_err();
2193 let rep = err.report().expect("strict WriteError carries a report");
2194 assert_eq!(rep.len(), 1);
2195 }
2196
2197 #[test]
2198 fn strict_write_with_no_adjustments_succeeds() {
2199 let doc = doc_of(obj(vec![("a", Value::Int((1).into()))]));
2200 let text = write_yaml(&doc, true, None).unwrap();
2201 assert!(text.contains("a: 1"));
2202 }
2203
2204 #[test]
2205 fn check_yaml_reports_without_producing_output() {
2206 let s = format!("a{}b", '\u{0085}');
2207 let doc = doc_of(obj(vec![("s", Value::Str(s))]));
2208 let rep = check_yaml(&doc);
2209 assert_eq!(rep.len(), 1);
2210 assert_eq!(rep.adjustments()[0].path, "$.s");
2211 }
2212
2213 #[test]
2214 fn deeply_nested_document_write_reuses_doc_construction_depth_guard() {
2215 let mut v = Value::Int((0).into());
2216 for _ in 0..=crate::document::MAX_DEPTH {
2217 v = obj(vec![("a", v)]);
2218 }
2219 assert!(Doc::of(&v).is_err());
2220 }
2221
2222 #[test]
2231 fn wide_document_smoke_test_reads_and_round_trips_every_field() {
2232 let n = 5_000;
2233 let mut text = String::new();
2234 for i in 0..n {
2235 text.push_str(&format!("field{i}: {i}\n"));
2236 }
2237 let doc = read_yaml(&text).unwrap();
2238 let root = doc.root();
2239 assert_eq!(root.labels().len(), n);
2240 for i in [0, n / 2, n - 1] {
2241 assert_eq!(
2242 *root.get_one(&format!("field{i}")).unwrap().value().unwrap(),
2243 Scalar::Int((i as i64).into())
2244 );
2245 }
2246 let out = write_yaml(&doc, false, None).unwrap();
2247 let back = read_yaml(&out).unwrap();
2248 assert!(doc.eq_doc(&back));
2249 }
2250
2251 #[test]
2252 fn wide_flat_sequence_smoke_test() {
2253 let n = 5_000;
2254 let mut text = String::from("m:\n");
2255 for i in 0..n {
2256 text.push_str(&format!(" - {i}\n"));
2257 }
2258 let doc = read_yaml(&text).unwrap();
2259 assert_eq!(doc.root().get("m").len(), n);
2260 }
2261
2262 #[test]
2265 fn invalid_explicit_float_literal_is_a_parse_error() {
2266 let err = read_yaml("a: !!float \"not-a-float\"\n").unwrap_err();
2267 assert!(
2268 matches!(&err, OmnistError::Parse(e) if e.message.contains("invalid float literal")),
2269 "got {err:?}"
2270 );
2271 }
2272
2273 #[test]
2274 fn explicit_float_tag_accepts_inf_and_nan_and_negative() {
2275 let doc = read_yaml("a: !!float \"-1.5\"\n").unwrap();
2276 assert_eq!(
2277 *doc.root().get_one("a").unwrap().value().unwrap(),
2278 Scalar::Float(-1.5)
2279 );
2280 }
2281
2282 #[test]
2293 fn timestamp_with_invalid_month_is_a_parse_error() {
2294 let err = read_yaml("a: 2024-13-01\n").unwrap_err();
2295 assert!(
2296 matches!(&err, OmnistError::Parse(e) if e.message.contains("calendar date")),
2297 "got {err:?}"
2298 );
2299 }
2300
2301 #[test]
2302 fn timestamp_with_year_zero_is_a_parse_error() {
2303 let err = read_yaml("a: 0000-01-01\n").unwrap_err();
2304 assert!(
2305 matches!(&err, OmnistError::Parse(e) if e.message.contains("calendar date")),
2306 "got {err:?}"
2307 );
2308 }
2309
2310 #[test]
2311 fn timestamp_with_a_day_that_doesnt_exist_in_the_month_is_a_parse_error() {
2312 let err = read_yaml("a: 2024-02-30\n").unwrap_err();
2316 assert!(
2317 matches!(&err, OmnistError::Parse(e) if e.message.contains("calendar date")),
2318 "got {err:?}"
2319 );
2320 }
2321
2322 #[test]
2323 fn timestamp_february_29_on_a_non_leap_year_is_a_parse_error() {
2324 let err = read_yaml("a: 2023-02-29\n").unwrap_err();
2325 assert!(
2326 matches!(&err, OmnistError::Parse(e) if e.message.contains("calendar date")),
2327 "got {err:?}"
2328 );
2329 }
2330
2331 #[test]
2332 fn timestamp_february_29_on_a_leap_year_normalizes_fine() {
2333 let doc = read_yaml("a: 2024-02-29\n").unwrap();
2334 assert_eq!(
2335 *doc.root().get_one("a").unwrap().value().unwrap(),
2336 Scalar::Date("2024-02-29".to_string())
2337 );
2338 }
2339
2340 #[test]
2341 fn timestamp_with_out_of_range_hour_is_a_parse_error() {
2342 let err = read_yaml("a: 2024-01-01T25:00:00\n").unwrap_err();
2343 assert!(
2344 matches!(&err, OmnistError::Parse(e) if e.message.contains("time of day")),
2345 "got {err:?}"
2346 );
2347 }
2348
2349 #[test]
2350 fn timestamp_with_out_of_range_minute_is_a_parse_error() {
2351 let err = read_yaml("a: 2024-01-01T00:61:00\n").unwrap_err();
2352 assert!(
2353 matches!(&err, OmnistError::Parse(e) if e.message.contains("time of day")),
2354 "got {err:?}"
2355 );
2356 }
2357
2358 #[test]
2359 fn timestamp_with_out_of_range_timezone_offset_is_a_parse_error() {
2360 let err = read_yaml("a: 2024-01-01T00:00:00+25:00\n").unwrap_err();
2361 assert!(
2362 matches!(&err, OmnistError::Parse(e) if e.message.contains("timezone offset")),
2363 "got {err:?}"
2364 );
2365 }
2366
2367 #[test]
2368 fn timestamp_with_hour_only_timezone_offset_normalizes_with_zero_minutes() {
2369 let doc = read_yaml("a: 2024-01-01T00:00:00+05\n").unwrap();
2370 assert_eq!(
2371 *doc.root().get_one("a").unwrap().value().unwrap(),
2372 Scalar::Datetime("2024-01-01T00:00:00+05:00".to_string())
2373 );
2374 }
2375
2376 #[test]
2379 fn nel_in_a_label_triggers_a_warning_and_still_round_trips() {
2380 let label = format!("a{}b", '\u{0085}');
2381 let doc = doc_of(obj(vec![(label.as_str(), Value::Int((1).into()))]));
2382 let mut rep = WriteReport::new();
2383 let text = write_yaml(&doc, false, Some(&mut rep)).unwrap();
2384 assert_eq!(rep.len(), 1);
2385 assert_eq!(rep.adjustments()[0].code, "string.line-break-char");
2386 let back = read_yaml(&text).unwrap();
2387 assert_eq!(
2388 *back.root().get_one(&label).unwrap().value().unwrap(),
2389 Scalar::Int((1).into())
2390 );
2391 }
2392
2393 #[test]
2394 fn write_node_on_a_bare_empty_array_writes_the_flow_empty_token() {
2395 let mut out = String::new();
2403 write_node(&Value::Array(vec![]), 0, &mut out, true);
2404 assert_eq!(out, "[]\n");
2405 }
2406
2407 #[test]
2408 fn round_trips_strings_needing_every_quoting_trigger() {
2409 let cases = [
2410 "-leading-dash",
2411 "?leading-question",
2412 ":leading-colon",
2413 ",leading-comma",
2414 "[leading-bracket",
2415 "]leading-bracket",
2416 "{leading-brace",
2417 "}leading-brace",
2418 "#leading-hash",
2419 "&leading-amp",
2420 "*leading-star",
2421 "!leading-bang",
2422 "|leading-pipe",
2423 ">leading-gt",
2424 "'leading-quote",
2425 "\"leading-dquote",
2426 "%leading-percent",
2427 "@leading-at",
2428 "`leading-backtick",
2429 " leading-space",
2430 "trailing-space ",
2431 "embedded: colon-space",
2432 "trailing-colon:",
2433 "embedded #hash-space",
2434 "line\nbreak",
2435 "tab\ttab",
2436 "quote\"quote",
2437 "back\\slash",
2438 "control\u{01}char",
2439 ];
2440 for s in cases {
2441 let doc = doc_of(obj(vec![("s", Value::Str(s.to_string()))]));
2442 let text = write_yaml(&doc, false, None).unwrap();
2443 let back = read_yaml(&text).unwrap();
2444 assert!(
2445 doc.eq_doc(&back),
2446 "round trip failed for {s:?}, text was:\n{text}"
2447 );
2448 }
2449 }
2450
2451 #[test]
2452 fn write_scalar_value_panics_on_a_non_leaf_value() {
2453 let result = std::panic::catch_unwind(|| {
2458 let mut out = String::new();
2459 write_scalar_value(&Value::Object(IndexMap::new()), &mut out);
2460 });
2461 assert!(result.is_err());
2462 }
2463
2464 use yaml_rust2::parser::Event;
2478 use yaml_rust2::scanner::Marker;
2479
2480 fn test_marker() -> Marker {
2485 struct Capture(Option<Marker>);
2486 impl MarkedEventReceiver for Capture {
2487 fn on_event(&mut self, _ev: Event, mark: Marker) {
2488 self.0.get_or_insert(mark);
2489 }
2490 }
2491 let mut cap = Capture(None);
2492 Parser::new("x".chars()).load(&mut cap, false).unwrap();
2493 cap.0
2494 .expect("a trivial scalar document always emits at least one event")
2495 }
2496
2497 #[test]
2498 fn builder_document_end_with_an_empty_stack_pushes_a_null_scalar() {
2499 let mut b = Builder::new();
2500 b.on_event_impl(Event::DocumentEnd, test_marker());
2501 assert_eq!(b.docs.len(), 1);
2502 assert!(matches!(&b.docs[0], Raw::Scalar(s, TScalarStyle::Plain, None) if s.is_empty()));
2503 }
2504
2505 #[test]
2506 #[should_panic(expected = "a single document's stack never nests more than one root")]
2507 fn builder_document_end_with_more_than_one_stack_entry_panics() {
2508 let mut b = Builder::new();
2509 b.doc_stack
2510 .push((Raw::Scalar(String::new(), TScalarStyle::Plain, None), 0));
2511 b.doc_stack
2512 .push((Raw::Scalar(String::new(), TScalarStyle::Plain, None), 0));
2513 b.on_event_impl(Event::DocumentEnd, test_marker());
2514 }
2515
2516 #[test]
2517 #[should_panic(expected = "a Scalar is never a container on doc_stack")]
2518 fn builder_insert_onto_a_scalar_container_panics() {
2519 let mut b = Builder::new();
2520 b.doc_stack
2521 .push((Raw::Scalar("x".to_string(), TScalarStyle::Plain, None), 0));
2522 b.insert(
2523 Raw::Scalar("y".to_string(), TScalarStyle::Plain, None),
2524 0,
2525 test_marker(),
2526 );
2527 }
2528
2529 #[test]
2530 #[should_panic(expected = "yaml_rust2's scanner rejects an alias to an undefined anchor")]
2531 fn builder_alias_to_an_unknown_anchor_panics() {
2532 let mut b = Builder::new();
2538 b.on_event_impl(Event::Alias(999), test_marker());
2539 }
2540
2541 #[test]
2550 fn charge_after_already_tripped_is_a_pure_no_op() {
2551 let mut b = Builder::new();
2552 assert!(!b.charge(MAX_MATERIALIZED_NODES + 1, test_marker()));
2553 let first_error = format!("{:?}", b.error);
2554 let count_after_first_trip = b.node_count;
2555 assert!(!b.charge(1, test_marker()));
2556 assert_eq!(
2557 format!("{:?}", b.error),
2558 first_error,
2559 "error must not change"
2560 );
2561 assert_eq!(
2562 b.node_count, count_after_first_trip,
2563 "node_count must not change once already tripped"
2564 );
2565 }
2566
2567 #[test]
2573 fn sequence_start_can_itself_trip_the_node_count_guard() {
2574 let mut b = Builder::new();
2575 b.node_count = MAX_MATERIALIZED_NODES;
2576 b.on_event_impl(Event::SequenceStart(0, None), test_marker());
2577 assert!(
2578 matches!(&b.error, Some(e) if e.message.contains("materializes more than")),
2579 "got {:?}",
2580 b.error
2581 );
2582 assert!(b.doc_stack.is_empty());
2584 }
2585
2586 #[test]
2588 fn mapping_start_can_itself_trip_the_node_count_guard() {
2589 let mut b = Builder::new();
2590 b.node_count = MAX_MATERIALIZED_NODES;
2591 b.on_event_impl(Event::MappingStart(0, None), test_marker());
2592 assert!(
2593 matches!(&b.error, Some(e) if e.message.contains("materializes more than")),
2594 "got {:?}",
2595 b.error
2596 );
2597 assert!(b.doc_stack.is_empty());
2598 assert!(b.key_stack.is_empty());
2599 }
2600
2601 #[test]
2603 fn scalar_can_itself_trip_the_node_count_guard() {
2604 let mut b = Builder::new();
2605 b.node_count = MAX_MATERIALIZED_NODES;
2606 b.on_event_impl(
2607 Event::Scalar("x".to_string(), TScalarStyle::Plain, 0, None),
2608 test_marker(),
2609 );
2610 assert!(
2611 matches!(&b.error, Some(e) if e.message.contains("materializes more than")),
2612 "got {:?}",
2613 b.error
2614 );
2615 assert!(b.doc_stack.is_empty());
2617 }
2618
2619 #[test]
2622 fn round_trips_an_integral_float_with_trailing_dot_zero() {
2623 let doc = doc_of(obj(vec![("f", Value::Float(2.0))]));
2624 let text = write_yaml(&doc, false, None).unwrap();
2625 assert!(text.contains("f: 2.0"), "text was:\n{text}");
2626 let back = read_yaml(&text).unwrap();
2627 assert!(doc.eq_doc(&back));
2628 }
2629
2630 #[test]
2631 fn quoted_string_escapes_every_special_character_in_one_pass() {
2632 let s = "-a\tb\\c\"d\u{01}e";
2637 let doc = doc_of(obj(vec![("s", Value::Str(s.to_string()))]));
2638 let text = write_yaml(&doc, false, None).unwrap();
2639 let back = read_yaml(&text).unwrap();
2640 assert!(doc.eq_doc(&back), "text was:\n{text}");
2641 }
2642
2643 #[test]
2644 fn write_seq_child_on_a_bare_non_empty_array_item_writes_it_on_the_next_line() {
2645 let mut out = String::new();
2652 write_seq_child(
2653 &Value::Array(vec![Value::Int((1).into()), Value::Int((2).into())]),
2654 0,
2655 &mut out,
2656 );
2657 assert_eq!(out, "\n - 1\n - 2\n");
2658 }
2659
2660 #[test]
2661 fn test_yaml_merge_key_deduplication_order() {
2662 let src = r#"
2663base1: &b1
2664 a: 1
2665 b: 2
2666 dup: "from_b1"
2667base2: &b2
2668 b: 20
2669 c: 3
2670 dup: "from_b2"
2671child:
2672 <<: [*b1, *b2]
2673 own: 0
2674 a: 100
2675"#;
2676 let doc = read_yaml(src).unwrap();
2677 let root = doc.root();
2678 let child = root.get_one("child").unwrap();
2679 let labels = child.labels();
2680 assert_eq!(labels, vec!["own", "a", "b", "dup", "c"]);
2682 assert_eq!(
2683 *child.get_one("a").unwrap().value().unwrap(),
2684 Scalar::Int((100).into())
2685 );
2686 assert_eq!(
2687 *child.get_one("b").unwrap().value().unwrap(),
2688 Scalar::Int((2).into())
2689 );
2690 assert_eq!(
2691 *child.get_one("dup").unwrap().value().unwrap(),
2692 Scalar::Str("from_b1".into())
2693 );
2694 assert_eq!(
2695 *child.get_one("c").unwrap().value().unwrap(),
2696 Scalar::Int((3).into())
2697 );
2698 }
2699
2700 fn interleaved_doc() -> Doc {
2707 Doc::from_raw(crate::document::RawNode::Edges(vec![
2708 (
2709 "m".to_string(),
2710 crate::document::RawNode::Leaf(Scalar::Str("A".to_string())),
2711 ),
2712 (
2713 "x".to_string(),
2714 crate::document::RawNode::Leaf(Scalar::Str("X".to_string())),
2715 ),
2716 (
2717 "m".to_string(),
2718 crate::document::RawNode::Leaf(Scalar::Str("B".to_string())),
2719 ),
2720 ]))
2721 .unwrap()
2722 }
2723
2724 fn contiguous_repeat_doc() -> Doc {
2725 Doc::from_raw(crate::document::RawNode::Edges(vec![
2726 (
2727 "m".to_string(),
2728 crate::document::RawNode::Leaf(Scalar::Str("A".to_string())),
2729 ),
2730 (
2731 "m".to_string(),
2732 crate::document::RawNode::Leaf(Scalar::Str("B".to_string())),
2733 ),
2734 (
2735 "x".to_string(),
2736 crate::document::RawNode::Leaf(Scalar::Str("X".to_string())),
2737 ),
2738 ]))
2739 .unwrap()
2740 }
2741
2742 #[test]
2743 fn reports_interleaving_lost_on_write() {
2744 let doc = interleaved_doc();
2745 let mut report = crate::report::WriteReport::new();
2746 write_yaml(&doc, false, Some(&mut report)).unwrap();
2747 let adjustments = report.adjustments();
2748 assert_eq!(adjustments.len(), 1);
2749 assert_eq!(adjustments[0].path, "$");
2750 assert_eq!(adjustments[0].code, "format.interleaving-lost");
2751 assert_eq!(adjustments[0].severity, crate::report::Severity::Warning);
2752 }
2753
2754 #[test]
2755 fn check_yaml_reports_interleaving_lost() {
2756 let rep = check_yaml(&interleaved_doc());
2757 assert_eq!(rep.adjustments().len(), 1);
2758 assert_eq!(rep.adjustments()[0].code, "format.interleaving-lost");
2759 }
2760
2761 #[test]
2762 fn contiguous_repeated_label_does_not_report_interleaving_lost() {
2763 let doc = contiguous_repeat_doc();
2764 let mut report = crate::report::WriteReport::new();
2765 write_yaml(&doc, false, Some(&mut report)).unwrap();
2766 assert!(report.is_empty());
2767 assert!(check_yaml(&doc).is_empty());
2768 }
2769}