1mod alias;
93#[cfg(test)]
94mod alias_tests;
95
96use std::collections::HashMap;
97
98use yaml_rust2::parser::{Event, MarkedEventReceiver, Parser, Tag};
99use yaml_rust2::scanner::{Marker, ScanError, TScalarStyle};
100
101use crate::WriteError;
102use crate::document::{Doc, Value};
103use crate::error::{DocumentError, OmnistError, ParseError};
104use crate::formats::float_fmt;
105use crate::formats::string_escape::{YAML_ESCAPES, write_quoted};
106use crate::limits::{IntGuard, Limits, Resolved, int_exceeds};
107use crate::report::{Severity, WriteReport};
108use indexmap::IndexMap;
109use num_bigint::BigInt;
110
111#[cfg(test)]
130const MAX_MATERIALIZED_NODES: usize = crate::limits::DEFAULT_MAX_NODES as usize;
131
132fn count_nodes(node: &Raw) -> usize {
136 match node {
137 Raw::Scalar(..) => 0,
138 Raw::Sequence(items) => 1 + items.iter().map(count_nodes).sum::<usize>(),
139 Raw::Mapping(entries) => {
140 1 + entries
141 .iter()
142 .map(|(k, v)| count_nodes(k) + count_nodes(v))
143 .sum::<usize>()
144 }
145 }
146}
147
148#[derive(Debug, Clone)]
156enum Raw {
157 Scalar(String, TScalarStyle, Option<Tag>),
158 Sequence(Vec<Raw>),
159 Mapping(Vec<(Raw, Raw)>),
160}
161
162struct Builder {
168 doc_stack: Vec<(Raw, usize)>,
169 key_stack: Vec<Option<Raw>>,
170 anchor_map: HashMap<usize, Raw>,
171 docs: Vec<Raw>,
172 node_count: usize,
175 error: Option<DocumentError>,
180 limits: Resolved,
182 self_reference: Option<DocumentError>,
188 tag_forbidden: Option<DocumentError>,
192}
193
194impl Builder {
195 fn new(limits: &Resolved) -> Self {
196 Builder {
197 limits: *limits,
198 doc_stack: Vec::new(),
199 key_stack: Vec::new(),
200 anchor_map: HashMap::new(),
201 docs: Vec::new(),
202 node_count: 0,
203 error: None,
204 self_reference: None,
205 tag_forbidden: None,
206 }
207 }
208
209 fn too_deep(&self) -> bool {
214 self.doc_stack.len() > self.limits.max_depth + 8
215 }
216
217 fn charge(&mut self, n: usize, _mark: Marker) -> bool {
223 if self.error.is_some() {
224 return false;
225 }
226 self.node_count = self.node_count.saturating_add(n);
227 if self.node_count > self.limits.max_nodes {
228 self.error = Some(DocumentError::with_code(
229 "$",
230 "document.limit.nodes",
231 format!(
232 "invalid YAML: document materializes more than \
233 {} nodes (security: unbounded anchor/alias \
234 expansion can amplify a small document into an enormous tree, \
235 independent of nesting depth)",
236 self.limits.max_nodes
237 ),
238 ));
239 return false;
240 }
241 true
242 }
243
244 fn insert(&mut self, node: Raw, aid: usize, _mark: Marker) {
245 if aid > 0 {
246 self.anchor_map.insert(aid, node.clone());
247 }
248 match self.doc_stack.last_mut() {
249 None => self.doc_stack.push((node, aid)),
250 Some((Raw::Sequence(items), _)) => items.push(node),
251 Some((Raw::Mapping(_), _)) => {
252 let cur_key = self
253 .key_stack
254 .last_mut()
255 .expect("a Mapping is only ever pushed alongside a matching key_stack entry");
256 match cur_key.take() {
257 None => *cur_key = Some(node),
258 Some(k) => {
259 if let Some((Raw::Mapping(entries), _)) = self.doc_stack.last_mut() {
260 entries.push((k, node));
261 }
262 }
263 }
264 }
265 Some((Raw::Scalar(..), _)) => {
266 unreachable!("a Scalar is never a container on doc_stack")
270 }
271 }
272 }
273
274 fn on_event_impl(&mut self, ev: Event, mark: Marker) {
282 if self.error.is_some() || self.self_reference.is_some() || self.tag_forbidden.is_some() {
287 return;
288 }
289 match ev {
290 Event::Nothing | Event::StreamStart | Event::StreamEnd | Event::DocumentStart => {}
291 Event::DocumentEnd => match self.doc_stack.len() {
292 0 => self
293 .docs
294 .push(Raw::Scalar(String::new(), TScalarStyle::Plain, None)),
295 1 => self.docs.push(self.doc_stack.pop().unwrap().0),
296 _ => unreachable!("a single document's stack never nests more than one root"),
297 },
298 Event::SequenceStart(_, Some(ref tag))
299 | Event::MappingStart(_, Some(ref tag))
300 | Event::Scalar(_, _, _, Some(ref tag))
301 if is_forbidden_tag(Some(tag)) =>
302 {
303 self.tag_forbidden = Some(tag_forbidden_error(tag));
304 }
305 Event::SequenceStart(..) | Event::MappingStart(..) if self.too_deep() => {
306 self.self_reference = Some(self.limits.depth_error());
313 }
314 Event::SequenceStart(aid, _) => {
315 if !self.charge(1, mark) {
316 return;
317 }
318 self.doc_stack.push((Raw::Sequence(Vec::new()), aid));
319 }
320 Event::SequenceEnd => {
321 let (node, aid) = self.doc_stack.pop().expect("matched by SequenceStart");
322 self.insert(node, aid, mark);
323 }
324 Event::MappingStart(aid, _) => {
325 if !self.charge(1, mark) {
326 return;
327 }
328 self.doc_stack.push((Raw::Mapping(Vec::new()), aid));
329 self.key_stack.push(None);
330 }
331 Event::MappingEnd => {
332 let (node, aid) = self.doc_stack.pop().expect("matched by MappingStart");
333 self.key_stack.pop();
334 self.insert(node, aid, mark);
335 }
336 Event::Scalar(v, style, aid, tag) => {
337 self.insert(Raw::Scalar(v, style, tag), aid, mark);
339 }
340 Event::Alias(id) => {
341 let Some(referenced) = self.anchor_map.get(&id) else {
355 self.self_reference = Some(DocumentError::with_code(
356 "$",
357 "document.limit.alias-expansion",
358 "an alias refers to an anchor that is not yet complete: a \
359 self-referential definition has unbounded expansion (D-20)",
360 ));
361 return;
362 };
363 let n = count_nodes(referenced);
364 if !self.charge(n, mark) {
365 return;
366 }
367 let node = self
368 .anchor_map
369 .get(&id)
370 .cloned()
371 .expect("checked above: the anchor_map entry exists for this id");
372 self.insert(node, 0, mark);
373 }
374 }
375 }
376}
377
378impl MarkedEventReceiver for Builder {
379 fn on_event(&mut self, ev: Event, mark: Marker) {
380 self.on_event_impl(ev, mark);
381 }
382}
383
384pub const DEFAULT_MAX_ALIAS_EXPANSION: u32 = 50;
388
389pub const MAX_ALIAS_EXPANSION_CEILING: u32 = 10_000;
394
395pub const DEFAULT_MAX_EXPANDED_SLOTS: u32 = 1_000_000;
400
401pub const MAX_EXPANDED_SLOTS_CEILING: u32 = 10_000_000;
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
417#[non_exhaustive]
418pub struct YamlReadOptions {
419 pub max_alias_expansion: u32,
432 pub max_expanded_slots: u32,
447 pub limits: Limits,
450}
451
452impl Default for YamlReadOptions {
453 fn default() -> Self {
454 YamlReadOptions {
455 max_alias_expansion: DEFAULT_MAX_ALIAS_EXPANSION,
456 max_expanded_slots: DEFAULT_MAX_EXPANDED_SLOTS,
457 limits: Limits::default(),
458 }
459 }
460}
461
462impl YamlReadOptions {
463 #[must_use]
467 pub fn with_max_alias_expansion(mut self, n: u32) -> Self {
468 self.max_alias_expansion = n;
469 self
470 }
471
472 #[must_use]
474 pub fn with_max_expanded_slots(mut self, n: u32) -> Self {
475 self.max_expanded_slots = n;
476 self
477 }
478
479 #[must_use]
481 pub fn with_limits(mut self, limits: Limits) -> Self {
482 self.limits = limits;
483 self
484 }
485
486 pub fn effective_max_expanded_slots(&self) -> u32 {
489 if self.max_expanded_slots == 0 {
490 DEFAULT_MAX_EXPANDED_SLOTS
491 } else {
492 self.max_expanded_slots
493 }
494 }
495
496 pub fn effective_max_alias_expansion(&self) -> u32 {
499 if self.max_alias_expansion == 0 {
500 DEFAULT_MAX_ALIAS_EXPANSION
501 } else {
502 self.max_alias_expansion
503 }
504 }
505
506 pub fn validate(&self) -> Result<(), DocumentError> {
513 if self.max_alias_expansion > MAX_ALIAS_EXPANSION_CEILING {
514 return Err(DocumentError::new(
515 "$",
516 format!(
517 "max_alias_expansion {} exceeds the ceiling {MAX_ALIAS_EXPANSION_CEILING} \
518 (0 selects the default {DEFAULT_MAX_ALIAS_EXPANSION})",
519 self.max_alias_expansion
520 ),
521 ));
522 }
523 if self.max_expanded_slots > MAX_EXPANDED_SLOTS_CEILING {
524 return Err(DocumentError::new(
525 "$",
526 format!(
527 "max_expanded_slots {} exceeds the ceiling {MAX_EXPANDED_SLOTS_CEILING} \
528 (0 selects the default {DEFAULT_MAX_EXPANDED_SLOTS})",
529 self.max_expanded_slots
530 ),
531 ));
532 }
533 self.limits.validate()?;
534 Ok(())
535 }
536}
537
538fn feed_events(
568 parser: &mut Parser<std::str::Chars<'_>>,
569 check: &mut alias::AliasCheck,
570 max_depth: usize,
571) -> Result<Vec<(Event, Marker)>, ScanError> {
572 let mut events = Vec::new();
573 let mut halted = false;
574 loop {
575 let (ev, mark) = parser.next_token()?;
576 let done = ev == Event::StreamEnd;
577 check.event(&ev, (mark.line(), mark.col() + 1));
578 if !check.failed() && !halted {
579 halted = check.depth() > max_depth + 10;
580 events.push((ev, mark));
581 }
582 if done {
583 return Ok(events);
584 }
585 }
586}
587
588fn scan_error_to_parse_error(e: &ScanError) -> ParseError {
589 let mark = e.marker();
590 ParseError::codec_syntax(mark.line(), mark.col() + 1, format!("invalid YAML: {e}"))
591}
592
593pub fn read_yaml(text: &str) -> Result<Doc, OmnistError> {
610 read_yaml_with(text, &YamlReadOptions::default())
611}
612
613pub fn read_yaml_with(text: &str, options: &YamlReadOptions) -> Result<Doc, OmnistError> {
619 options.validate()?;
620 options
623 .limits
624 .resolve_validated()
625 .check_input_size(text.len())?;
626 let text = crate::bom::strip_leading_bom(text)
631 .map_err(|_| ParseError::codec_syntax(1, 1, crate::bom::DOUBLED_BOM_MESSAGE))?;
632 let mut parser = Parser::new(text.chars());
633 let mut check = alias::AliasCheck::new(
634 u64::from(options.effective_max_alias_expansion()),
635 u64::from(options.effective_max_expanded_slots()),
636 );
637 let limits = options.limits.resolve_validated();
638 let events = feed_events(&mut parser, &mut check, limits.max_depth)
639 .map_err(|e| scan_error_to_parse_error(&e))?;
640 if let Some(e) = check.into_error() {
641 return Err(e);
642 }
643 let mut builder = Builder::new(&limits);
644 for (ev, mark) in events {
645 builder.on_event(ev, mark);
646 }
647 if let Some(e) = builder.tag_forbidden {
648 return Err(e.into());
649 }
650 if let Some(e) = builder.self_reference {
651 return Err(e.into());
652 }
653 if let Some(e) = builder.error {
654 return Err(e.into());
655 }
656 if builder.docs.len() > 1 {
657 return Err(ParseError::codec_syntax(
658 1,
659 1,
660 "invalid YAML: expected a single document in the stream, found more than one",
661 )
662 .into());
663 }
664 let raw = builder.docs.into_iter().next().unwrap_or(Raw::Scalar(
665 String::new(),
666 TScalarStyle::Plain,
667 None,
668 ));
669 let mut dropped: Vec<Raw> = Vec::new();
670 let resolved = resolve_merges(&raw, 0, limits.max_depth, &mut dropped)?;
671 let mut guard = IntGuard::new(limits.max_int_digits);
672 let value = raw_to_value(&resolved, &Trail::Root, &mut guard)?;
673 for node in &dropped {
678 note_dropped_integers(node, &mut guard);
679 }
680 let doc = Doc::of_resolved(&value, limits).map_err(|e| limits.whole_document(e))?;
681 guard.finish(&limits)?;
684 Ok(doc)
685}
686
687fn resolve_merges(
691 node: &Raw,
692 depth: usize,
693 max_depth: usize,
694 dropped: &mut Vec<Raw>,
695) -> Result<Raw, OmnistError> {
696 crate::document::check_write_depth(depth, "$", max_depth)?;
697 match node {
698 Raw::Scalar(..) => Ok(node.clone()),
699 Raw::Sequence(items) => {
700 let mut out = Vec::with_capacity(items.len());
701 for item in items {
702 out.push(resolve_merges(item, depth + 1, max_depth, dropped)?);
703 }
704 Ok(Raw::Sequence(out))
705 }
706 Raw::Mapping(entries) => Ok(Raw::Mapping(resolve_mapping(
707 entries, depth, max_depth, dropped,
708 )?)),
709 }
710}
711
712fn resolve_mapping(
724 entries: &[(Raw, Raw)],
725 depth: usize,
726 max_depth: usize,
727 dropped: &mut Vec<Raw>,
728) -> Result<Vec<(Raw, Raw)>, OmnistError> {
729 let mut merged_from: Vec<(Raw, Raw)> = Vec::new();
730 let mut own: Vec<(Raw, Raw)> = Vec::new();
731 let mut written: std::collections::HashSet<&str> = std::collections::HashSet::new();
732 for (k, v) in entries {
733 if is_merge_key(k) {
734 merged_from.extend(merge_source_entries(v, depth, max_depth, dropped)?);
735 } else {
736 if let Some(label) = scalar_key_text(k)
740 && !written.insert(label)
741 {
742 return Err(ParseError::codec_syntax(
743 1,
744 1,
745 format!("invalid YAML: duplicate mapping key {label:?}"),
746 )
747 .into());
748 }
749 own.push((
750 resolve_merges(k, depth + 1, max_depth, dropped)?,
751 resolve_merges(v, depth + 1, max_depth, dropped)?,
752 ));
753 }
754 }
755 let mut result: Vec<(Raw, Raw)> = Vec::with_capacity(merged_from.len() + own.len());
758 let mut position: std::collections::HashMap<String, usize> =
759 std::collections::HashMap::with_capacity(merged_from.len());
760 for (k, v) in merged_from {
761 match scalar_key_text(&k) {
762 Some(label) if position.contains_key(label) => dropped.push(v),
763 Some(label) => {
764 position.insert(label.to_string(), result.len());
765 result.push((k, v));
766 }
767 None => result.push((k, v)),
768 }
769 }
770 for (k, v) in own {
774 match scalar_key_text(&k).and_then(|label| position.get(label).copied()) {
775 Some(at) => dropped.push(std::mem::replace(&mut result[at].1, v)),
776 None => result.push((k, v)),
777 }
778 }
779 Ok(result)
780}
781
782fn note_dropped_integers(node: &Raw, guard: &mut IntGuard) {
787 match node {
788 Raw::Scalar(text, style, tag) => {
789 let _ = scalar_to_value(text, *style, tag.as_ref(), guard);
790 }
791 Raw::Sequence(items) => {
792 for item in items {
793 note_dropped_integers(item, guard);
794 }
795 }
796 Raw::Mapping(entries) => {
797 for (_, v) in entries {
798 note_dropped_integers(v, guard);
799 }
800 }
801 }
802}
803
804fn scalar_key_text(k: &Raw) -> Option<&str> {
808 match k {
809 Raw::Scalar(s, _, _) => Some(s.as_str()),
810 _ => None,
811 }
812}
813
814fn is_merge_scalar(text: &str, style: TScalarStyle, tag: Option<&Tag>) -> bool {
819 text == "<<"
820 && match tag {
821 None => style == TScalarStyle::Plain,
822 Some(t) => is_yaml_tag(t, "merge"),
823 }
824}
825
826fn is_yaml_tag(tag: &Tag, suffix: &str) -> bool {
828 tag.handle == "tag:yaml.org,2002:" && tag.suffix == suffix
829}
830
831fn is_forbidden_tag(tag: Option<&Tag>) -> bool {
834 tag.is_some_and(|t| {
835 t.handle == "tag:yaml.org,2002:" && matches!(t.suffix.as_str(), "pairs" | "omap" | "set")
836 })
837}
838
839fn tag_forbidden_error(tag: &Tag) -> DocumentError {
842 DocumentError::with_code(
843 "$",
844 "format.tag-forbidden",
845 format!(
846 "the YAML tag '!!{}' is outside the supported profile (use a plain sequence or \
847 mapping instead)",
848 tag.suffix
849 ),
850 )
851}
852
853fn is_merge_key(k: &Raw) -> bool {
855 matches!(k, Raw::Scalar(s, style, tag) if is_merge_scalar(s, *style, tag.as_ref()))
856}
857
858fn merge_source_entries(
864 v: &Raw,
865 depth: usize,
866 max_depth: usize,
867 dropped: &mut Vec<Raw>,
868) -> Result<Vec<(Raw, Raw)>, OmnistError> {
869 match v {
870 Raw::Mapping(entries) => resolve_mapping(entries, depth + 1, max_depth, dropped),
873 Raw::Sequence(items) => {
874 let mut out = Vec::new();
875 for item in items {
876 let Raw::Mapping(entries) = item else {
877 unreachable!("the alias check refused a non-mapping merge member")
878 };
879 out.extend(resolve_mapping(entries, depth + 2, max_depth, dropped)?);
880 }
881 Ok(out)
882 }
883 Raw::Scalar(..) => unreachable!("the alias check refused a scalar merge value"),
884 }
885}
886
887enum Trail<'a> {
892 Root,
894 Key(&'a Trail<'a>, &'a str),
896 Index(&'a Trail<'a>, usize),
900}
901
902impl Trail<'_> {
903 fn render(&self) -> String {
905 match self {
906 Trail::Root => "$".to_string(),
907 Trail::Key(parent, key) => crate::document::join(&parent.render(), key),
908 Trail::Index(parent, i) => format!("{}[{i}]", parent.render()),
909 }
910 }
911}
912
913fn raw_to_value(node: &Raw, trail: &Trail<'_>, guard: &mut IntGuard) -> Result<Value, OmnistError> {
917 match node {
918 Raw::Scalar(text, style, tag) => Ok(scalar_to_value(text, *style, tag.as_ref(), guard)?),
919 Raw::Sequence(items) => {
920 let mut out = Vec::with_capacity(items.len());
921 for (i, item) in items.iter().enumerate() {
922 let value = if items.len() == 1 {
924 raw_to_value(item, trail, guard)?
925 } else {
926 raw_to_value(item, &Trail::Index(trail, i), guard)?
927 };
928 out.push(value);
929 }
930 Ok(Value::Array(out))
931 }
932 Raw::Mapping(entries) => {
933 let mut map: IndexMap<String, Value> = IndexMap::new();
934 for (k, v) in entries {
935 let key = match k {
936 Raw::Scalar(s, style, tag) => {
947 match scalar_to_value(s, *style, tag.as_ref(), guard)? {
948 Value::Str(s) => s,
949 Value::Int(i) if int_exceeds(&i, guard.max()) => {
952 return Err(DocumentError::with_code(
953 trail.render(),
954 "document.limit.int-digits",
955 crate::formats::int_cap::over_cap_message(
956 "invalid YAML: ",
957 guard.max(),
958 ),
959 )
960 .into());
961 }
962 other => {
963 return Err(DocumentError::with_code(
968 trail.render(),
969 "document.unlabeled-element",
970 format!(
971 "object key {} is not a string",
972 describe_non_string_key(&other)
973 ),
974 )
975 .into());
976 }
977 }
978 }
979 _ => {
980 return Err(ParseError::codec_syntax(
981 1,
982 1,
983 "invalid YAML: a mapping key must be a scalar",
984 )
985 .into());
986 }
987 };
988 let value = raw_to_value(v, &Trail::Key(trail, &key), guard)?;
991 map.insert(key, value);
992 }
993 Ok(Value::Object(map))
994 }
995 }
996}
997
998fn describe_non_string_key(v: &Value) -> String {
1009 match v {
1010 Value::Bool(true) => "True".to_string(),
1011 Value::Bool(false) => "False".to_string(),
1012 Value::Null => "None".to_string(),
1013 Value::Int(i) => i.to_string(),
1014 Value::Float(f) => {
1015 let s = f.to_string();
1019 if f.is_finite() && !s.contains('.') && !s.contains('e') && !s.contains('E') {
1020 format!("{s}.0")
1021 } else {
1022 s
1023 }
1024 }
1025 other => format!("{other:?}"),
1026 }
1027}
1028
1029fn scalar_to_value(
1030 text: &str,
1031 style: TScalarStyle,
1032 tag: Option<&Tag>,
1033 guard: &mut IntGuard,
1034) -> Result<Value, ParseError> {
1035 if let Some(t) = tag
1036 && t.handle == "tag:yaml.org,2002:"
1037 {
1038 return explicit_tag_to_value(text, &t.suffix, guard);
1039 }
1040 if style != TScalarStyle::Plain {
1041 return Ok(Value::Str(text.to_string()));
1042 }
1043 resolve_plain_scalar(text, guard)
1044}
1045
1046fn explicit_tag_to_value(
1055 text: &str,
1056 suffix: &str,
1057 guard: &mut IntGuard,
1058) -> Result<Value, ParseError> {
1059 match suffix {
1060 "str" => Ok(Value::Str(text.to_string())),
1061 "null" => Ok(Value::Null),
1062 "bool" => match text.to_ascii_lowercase().as_str() {
1070 "true" | "yes" | "on" => Ok(Value::Bool(true)),
1071 "false" | "no" | "off" => Ok(Value::Bool(false)),
1072 _ => Err(ParseError::codec_syntax(
1073 1,
1074 1,
1075 format!("invalid YAML: {text:?} is not a valid !!bool value"),
1076 )),
1077 },
1078 "int" => parse_int_literal(text, guard),
1079 "float" => parse_float_literal(text),
1080 other => Err(ParseError::codec_syntax(
1081 1,
1082 1,
1083 format!("invalid YAML: unsupported explicit tag '!!{other}'"),
1084 )),
1085 }
1086}
1087
1088fn resolve_plain_scalar(text: &str, guard: &mut IntGuard) -> Result<Value, ParseError> {
1092 match text {
1093 "" | "~" | "null" | "Null" | "NULL" => return Ok(Value::Null),
1094 "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" => {
1095 return Ok(Value::Bool(true));
1096 }
1097 "false" | "False" | "FALSE" | "no" | "No" | "NO" | "off" | "Off" | "OFF" => {
1098 return Ok(Value::Bool(false));
1099 }
1100 _ => {}
1101 }
1102 if is_int_literal_shape(text) {
1103 return parse_int_literal(text, guard);
1104 }
1105 if is_sexagesimal_int_shape(text) {
1106 return parse_sexagesimal_int(text, guard);
1107 }
1108 if is_float_literal_shape(text) {
1109 return parse_float_literal(text);
1110 }
1111 if SEXAGESIMAL_FLOAT_RE.is_match(text) {
1112 return Ok(parse_sexagesimal_float(text));
1113 }
1114 if let Some(iso) = normalize_timestamp(text)? {
1115 return Ok(if iso.contains('T') {
1123 Value::Datetime(iso)
1124 } else {
1125 Value::Date(iso)
1126 });
1127 }
1128 Ok(Value::Str(text.to_string()))
1129}
1130
1131static INT_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
1140 regex::Regex::new(
1141 r"^(?:[-+]?0b[0-1_]+|[-+]?0[0-7_]+|[-+]?(?:0|[1-9][0-9_]*)|[-+]?0x[0-9a-fA-F_]+)$",
1142 )
1143 .unwrap()
1144});
1145
1146fn is_int_literal_shape(text: &str) -> bool {
1147 INT_RE.is_match(text)
1148}
1149
1150static SEXAGESIMAL_INT_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
1160 regex::Regex::new(r"^[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+$").unwrap()
1161});
1162
1163fn is_sexagesimal_int_shape(text: &str) -> bool {
1164 SEXAGESIMAL_INT_RE.is_match(text)
1165}
1166
1167fn parse_sexagesimal_int(text: &str, guard: &mut IntGuard) -> Result<Value, ParseError> {
1182 let neg = text.starts_with('-');
1183 let t = text.strip_prefix(['+', '-']).unwrap_or(text);
1184 let mut acc = BigInt::from(0);
1185 let sixty = BigInt::from(60);
1186 for group in t.split(':') {
1187 let cleaned: String = group.chars().filter(|&c| c != '_').collect();
1188 let digit = BigInt::parse_bytes(cleaned.as_bytes(), 10)
1191 .expect("SEXAGESIMAL_INT_RE guarantees decimal digit groups");
1192 acc = acc * &sixty + digit;
1193 }
1194 let value = if neg { -acc } else { acc };
1195 if int_exceeds(&value, guard.max()) {
1196 return Ok(Value::Int(guard.placeholder()));
1197 }
1198 Ok(Value::Int(value))
1199}
1200
1201fn parse_int_literal(text: &str, guard: &mut IntGuard) -> Result<Value, ParseError> {
1207 let neg = text.starts_with('-');
1208 let t = text.strip_prefix(['+', '-']).unwrap_or(text);
1209 let cleaned: String = t.chars().filter(|&c| c != '_').collect();
1210 let (radix, digits) = if let Some(rest) = cleaned.strip_prefix("0x") {
1211 (16, rest)
1212 } else if let Some(rest) = cleaned.strip_prefix("0b") {
1213 (2, rest)
1214 } else if cleaned.starts_with('0') && cleaned.len() > 1 {
1215 (8, &cleaned[1..])
1216 } else {
1217 (10, cleaned.as_str())
1218 };
1219 if radix == 10 && digits.len() > guard.max() {
1220 return Ok(Value::Int(guard.placeholder()));
1221 }
1222 let Some(magnitude) = BigInt::parse_bytes(digits.as_bytes(), radix) else {
1226 return Err(ParseError::codec_syntax(
1227 1,
1228 1,
1229 format!("invalid YAML: {text:?} is not a valid !!int value"),
1230 ));
1231 };
1232 let value = if neg { -magnitude } else { magnitude };
1233 Ok(Value::Int(value))
1234}
1235
1236static FLOAT_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
1243 regex::Regex::new(
1244 r"^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)?|\.[0-9][0-9_]*(?:[eE][-+][0-9]+)?|[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN))$",
1245 )
1246 .unwrap()
1247});
1248
1249static SEXAGESIMAL_FLOAT_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
1254 regex::Regex::new(r"^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*$").unwrap()
1255});
1256
1257fn parse_sexagesimal_float(text: &str) -> Value {
1261 let neg = text.starts_with('-');
1262 let t = text.strip_prefix(['+', '-']).unwrap_or(text);
1263 let cleaned: String = t.chars().filter(|&c| c != '_').collect();
1264 let mut acc = 0.0_f64;
1265 for group in cleaned.split(':') {
1266 let group: f64 = group
1267 .parse()
1268 .expect("SEXAGESIMAL_FLOAT_RE guarantees numeric groups");
1269 acc = acc * 60.0 + group;
1270 }
1271 Value::Float(if neg { -acc } else { acc })
1272}
1273
1274fn is_float_literal_shape(text: &str) -> bool {
1275 FLOAT_RE.is_match(text)
1276}
1277
1278fn parse_float_literal(text: &str) -> Result<Value, ParseError> {
1279 match text {
1280 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => {
1281 return Ok(Value::Float(f64::INFINITY));
1282 }
1283 "-.inf" | "-.Inf" | "-.INF" => return Ok(Value::Float(f64::NEG_INFINITY)),
1284 ".nan" | ".NaN" | ".NAN" => return Ok(Value::Float(f64::NAN)),
1285 _ => {}
1286 }
1287 let cleaned: String = text.chars().filter(|&c| c != '_').collect();
1288 cleaned.parse::<f64>().map(Value::Float).map_err(|_| {
1289 ParseError::codec_syntax(
1290 1,
1291 1,
1292 format!("invalid YAML: invalid float literal {text:?}"),
1293 )
1294 })
1295}
1296
1297fn normalize_timestamp(text: &str) -> Result<Option<String>, ParseError> {
1318 static RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
1319 regex::Regex::new(
1320 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]))?))?)?$",
1321 )
1322 .unwrap()
1323 });
1324 let Some(caps) = RE.captures(text) else {
1325 return Ok(None);
1326 };
1327 let bad = |what: &str| {
1328 Err(ParseError::codec_syntax(
1329 1,
1330 1,
1331 format!("invalid YAML: {text:?} is timestamp-shaped but names an invalid {what}"),
1332 ))
1333 };
1334 let year: u32 = caps["year"].parse().unwrap_or(u32::MAX);
1335 let month: u32 = caps["month"].parse().unwrap_or(u32::MAX);
1336 let day: u32 = caps["day"].parse().unwrap_or(u32::MAX);
1337 if !crate::schema::valid_ymd(year, month, day) {
1338 return bad("calendar date");
1339 }
1340 let Some(hour_m) = caps.name("hour") else {
1341 return Ok(Some(format!("{year:04}-{month:02}-{day:02}")));
1342 };
1343 let hour: u32 = hour_m.as_str().parse().unwrap_or(u32::MAX);
1344 let minute: u32 = caps["minute"].parse().unwrap_or(u32::MAX);
1345 let second: u32 = caps["second"].parse().unwrap_or(u32::MAX);
1346 if !crate::schema::valid_hms(hour, minute, second) {
1347 return bad("time of day");
1348 }
1349 let mut out = format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}");
1350 if let Some(frac) = caps.name("fraction") {
1351 let mut digits = frac.as_str().to_string();
1352 if digits.len() > 6 && digits[6..].bytes().any(|b| b != b'0') {
1357 return bad("fractional second (more than six digits)");
1358 }
1359 while digits.len() < 6 {
1360 digits.push('0');
1361 }
1362 digits.truncate(6);
1363 out.push('.');
1364 out.push_str(&digits);
1365 }
1366 match caps.name("tz_sign") {
1367 Some(sign) => {
1368 let tz_hour: u32 = caps["tz_hour"].parse().unwrap_or(u32::MAX);
1369 let tz_minute: u32 = caps
1370 .name("tz_minute")
1371 .map(|m| m.as_str().parse().unwrap_or(u32::MAX))
1372 .unwrap_or(0);
1373 if tz_hour > 23 || tz_minute > 59 {
1374 return bad("timezone offset");
1375 }
1376 out.push_str(sign.as_str());
1377 out.push_str(&format!("{tz_hour:02}:{tz_minute:02}"));
1378 }
1379 None if text.trim_end().ends_with('Z') => out.push_str("+00:00"),
1380 None => {}
1381 }
1382 Ok(Some(out))
1383}
1384
1385pub fn write_yaml(
1391 doc: &Doc,
1392 strict: bool,
1393 report: Option<&mut WriteReport>,
1394) -> Result<String, WriteError> {
1395 let grouped = doc.to_grouped();
1396 let mut rep = check_yaml_grouped(&grouped);
1397 add_interleaving_diagnostic(doc, &mut rep);
1398 let mut out = String::new();
1399 write_node(&grouped, 0, &mut out, true);
1400 if out.ends_with('\n') {
1401 out.pop();
1402 }
1403 crate::report::finish_write(out, rep, strict, report)
1404}
1405
1406fn add_interleaving_diagnostic(doc: &Doc, rep: &mut WriteReport) {
1412 if doc.has_interleaving_loss() {
1413 rep.add(
1414 "$",
1415 "format.interleaving-lost",
1416 "cross-label interleaving could not be written; same-label edges were grouped",
1417 Severity::Warning,
1418 );
1419 }
1420}
1421
1422pub fn check_yaml(doc: &Doc) -> WriteReport {
1426 let grouped = doc.to_grouped();
1427 let mut rep = check_yaml_grouped(&grouped);
1428 add_interleaving_diagnostic(doc, &mut rep);
1429 rep
1430}
1431
1432fn check_yaml_grouped(grouped: &Value) -> WriteReport {
1433 let mut rep = WriteReport::new();
1434 let mut path = String::from("$");
1435 crate::formats::visit_grouped(grouped, &mut path, &mut |visited, path| match visited {
1436 crate::formats::Visited::Edge { label } if label.contains('\u{0085}') => {
1437 rep.add(
1438 path,
1439 "format.string-line-break-char",
1440 "label contains U+0085 (NEL); written double-quoted to round-trip correctly",
1441 Severity::Warning,
1442 );
1443 }
1444 crate::formats::Visited::Node {
1445 value: Value::Str(s),
1446 } if s.contains('\u{0085}') => {
1447 rep.add(
1448 path,
1449 "format.string-line-break-char",
1450 "value contains U+0085 (NEL); written double-quoted to round-trip correctly",
1451 Severity::Warning,
1452 );
1453 }
1454 crate::formats::Visited::Node {
1457 value: Value::Datetime(s),
1458 } if crate::formats::fraction_inexact(s) => {
1459 rep.add(
1460 path,
1461 "format.temporal-stringified",
1462 "fractional second has more than six digits; wrote as a quoted ISO-8601 string",
1463 Severity::Warning,
1464 );
1465 }
1466 crate::formats::Visited::Node {
1467 value: Value::Time(_),
1468 } => {
1469 rep.add(
1470 path,
1471 "format.temporal-stringified",
1472 "time has no native YAML literal; wrote as a quoted ISO-8601 string",
1473 Severity::Warning,
1474 );
1475 }
1476 _ => {}
1477 });
1478 rep
1479}
1480
1481pub(crate) struct Yaml;
1485
1486impl crate::formats::Codec for Yaml {
1487 const NAME: &'static str = "yaml";
1488
1489 fn read(text: &str) -> Result<Doc, OmnistError> {
1490 read_yaml(text)
1491 }
1492
1493 fn write(doc: &Doc) -> Result<String, OmnistError> {
1494 write_yaml(doc, false, None).map_err(Into::into)
1495 }
1496
1497 fn check(doc: &Doc) -> WriteReport {
1498 check_yaml(doc)
1499 }
1500}
1501
1502fn indent(out: &mut String, level: usize) {
1503 for _ in 0..level {
1504 out.push_str(" ");
1505 }
1506}
1507
1508fn write_node(node: &Value, level: usize, out: &mut String, top: bool) {
1514 match node {
1515 Value::Object(map) if map.is_empty() => {
1516 out.push_str("{}\n");
1517 }
1518 Value::Array(items) if items.is_empty() => {
1519 out.push_str("[]\n");
1520 }
1521 Value::Object(map) => {
1522 for (label, child) in map {
1523 indent(out, level);
1524 write_scalar(label, out);
1525 out.push(':');
1526 write_child(child, level, out);
1527 }
1528 let _ = top;
1529 }
1530 Value::Array(items) => {
1531 for item in items {
1532 indent(out, level);
1533 out.push('-');
1534 write_seq_child(item, level, out);
1535 }
1536 }
1537 other => {
1538 write_scalar_value(other, out);
1539 out.push('\n');
1540 }
1541 }
1542}
1543
1544fn write_child(child: &Value, level: usize, out: &mut String) {
1545 match child {
1546 Value::Object(m) if !m.is_empty() => {
1547 out.push('\n');
1548 write_node(child, level + 1, out, false);
1549 }
1550 Value::Array(a) if !a.is_empty() => {
1551 out.push('\n');
1552 write_node(child, level, out, false);
1553 }
1554 _ => {
1555 out.push(' ');
1556 write_node(child, level + 1, out, false);
1557 }
1558 }
1559}
1560
1561fn write_seq_child(item: &Value, level: usize, out: &mut String) {
1562 match item {
1563 Value::Object(m) if !m.is_empty() => {
1564 out.push(' ');
1565 let mut first = true;
1570 for (label, child) in m {
1571 if !first {
1572 indent(out, level + 1);
1573 }
1574 first = false;
1575 write_scalar(label, out);
1576 out.push(':');
1577 write_child(child, level + 1, out);
1578 }
1579 }
1580 Value::Array(a) if !a.is_empty() => {
1581 out.push('\n');
1582 write_node(item, level + 1, out, false);
1583 }
1584 _ => {
1585 out.push(' ');
1586 write_node(item, level + 1, out, false);
1587 }
1588 }
1589}
1590
1591fn write_scalar(s: &str, out: &mut String) {
1592 write_scalar_value(&Value::Str(s.to_string()), out);
1593}
1594
1595fn write_scalar_value(v: &Value, out: &mut String) {
1596 match v {
1597 Value::Null => out.push_str("null"),
1598 Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
1599 Value::Int(i) => out.push_str(&i.to_string()),
1600 Value::Float(x) => write_float(*x, out),
1601 Value::Str(s) => write_yaml_string(s, out),
1606 Value::Date(s) => out.push_str(s),
1612 Value::Datetime(s) => {
1613 let text = super::temporal_text(s, true);
1614 if super::fraction_inexact(&text) {
1615 write_quoted(&text, &YAML_ESCAPES, out);
1616 } else {
1617 out.push_str(&text);
1618 }
1619 }
1620 Value::Time(s) => write_quoted(&super::temporal_text(s, false), &YAML_ESCAPES, out),
1628 Value::Object(_) | Value::Array(_) => {
1629 unreachable!("write_scalar_value is only ever called on a leaf")
1630 }
1631 }
1632}
1633
1634fn write_float(x: f64, out: &mut String) {
1637 float_fmt::write_float(x, ".nan", ".inf", "-.inf", out);
1638}
1639
1640fn write_yaml_string(s: &str, out: &mut String) {
1646 if needs_quoting(s) {
1647 write_quoted(s, &YAML_ESCAPES, out);
1648 } else {
1649 out.push_str(s);
1650 }
1651}
1652
1653fn needs_quoting(s: &str) -> bool {
1654 if s.is_empty() || s.contains('\u{0085}') || s.contains('\n') {
1655 return true;
1656 }
1657 if s.chars()
1660 .any(|c| (c < ' ' && c != '\t' && c != '\r') || YAML_ESCAPES.also_escape.contains(&c))
1661 {
1662 return true;
1663 }
1664 if s.starts_with(crate::bom::BOM) {
1670 return true;
1671 }
1672 if matches!(resolve_plain_scalar(s, &mut IntGuard::new(crate::formats::int_cap::MAX_INT_DIGITS)), Ok(Value::Str(ref t)) if t == s)
1673 {
1674 } else {
1678 return true; }
1680 let first = s.chars().next().unwrap();
1684 if matches!(
1685 first,
1686 '-' | '?'
1687 | ':'
1688 | ','
1689 | '['
1690 | ']'
1691 | '{'
1692 | '}'
1693 | '#'
1694 | '&'
1695 | '*'
1696 | '!'
1697 | '|'
1698 | '>'
1699 | '\''
1700 | '"'
1701 | '%'
1702 | '@'
1703 | '`'
1704 | ' '
1705 ) {
1706 return true;
1707 }
1708 if s.ends_with(' ') || s.contains(": ") || s.ends_with(':') || s.contains(" #") {
1709 return true;
1710 }
1711 false
1712}
1713
1714#[cfg(test)]
1715mod tests {
1716 use super::*;
1717 use crate::document::{Doc, Scalar, Value};
1718 use crate::formats::int_cap::MAX_INT_DIGITS;
1719
1720 fn obj(pairs: Vec<(&str, Value)>) -> Value {
1721 Value::Object(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
1722 }
1723
1724 fn doc_of(v: Value) -> Doc {
1725 Doc::of(&v).unwrap()
1726 }
1727
1728 #[test]
1731 fn reads_every_scalar_kind() {
1732 let doc = read_yaml("a: 1\nb: \"s\"\nc: true\nd: null\ne: 1.5\n").unwrap();
1733 let root = doc.root();
1734 assert_eq!(
1735 *root.get_one("a").unwrap().value().unwrap(),
1736 Scalar::Int((1).into())
1737 );
1738 assert_eq!(
1739 *root.get_one("b").unwrap().value().unwrap(),
1740 Scalar::Str("s".to_string())
1741 );
1742 assert_eq!(
1743 *root.get_one("c").unwrap().value().unwrap(),
1744 Scalar::Bool(true)
1745 );
1746 assert_eq!(*root.get_one("d").unwrap().value().unwrap(), Scalar::Null);
1747 assert_eq!(
1748 *root.get_one("e").unwrap().value().unwrap(),
1749 Scalar::Float(1.5)
1750 );
1751 }
1752
1753 #[test]
1754 fn reads_yaml_1_1_bool_spellings_but_not_bare_y_or_n() {
1755 let doc = read_yaml("a: yes\nb: no\nc: on\nd: off\ne: Yes\nf: y\ng: n\n").unwrap();
1756 let root = doc.root();
1757 assert_eq!(
1758 *root.get_one("a").unwrap().value().unwrap(),
1759 Scalar::Bool(true)
1760 );
1761 assert_eq!(
1762 *root.get_one("b").unwrap().value().unwrap(),
1763 Scalar::Bool(false)
1764 );
1765 assert_eq!(
1766 *root.get_one("c").unwrap().value().unwrap(),
1767 Scalar::Bool(true)
1768 );
1769 assert_eq!(
1770 *root.get_one("d").unwrap().value().unwrap(),
1771 Scalar::Bool(false)
1772 );
1773 assert_eq!(
1774 *root.get_one("e").unwrap().value().unwrap(),
1775 Scalar::Bool(true)
1776 );
1777 assert_eq!(
1780 *root.get_one("f").unwrap().value().unwrap(),
1781 Scalar::Str("y".to_string())
1782 );
1783 assert_eq!(
1784 *root.get_one("g").unwrap().value().unwrap(),
1785 Scalar::Str("n".to_string())
1786 );
1787 }
1788
1789 #[test]
1790 fn a_string_starting_with_a_bom_is_quoted_so_the_output_never_leads_with_one() {
1791 let doc = doc_of(obj(vec![(
1792 "\u{FEFF}k",
1793 Value::Str("\u{FEFF}v".to_string()),
1794 )]));
1795 let text = write_yaml(&doc, false, None).unwrap();
1796 assert!(text.starts_with('"'), "{text:?}");
1797 assert!(!text.starts_with('\u{FEFF}'));
1798 assert_eq!(read_yaml(&text).unwrap().to_raw(), doc.to_raw());
1800 let doc = doc_of(obj(vec![("a\u{FEFF}b", Value::Str("x".to_string()))]));
1802 assert_eq!(write_yaml(&doc, false, None).unwrap(), "a\u{FEFF}b: x");
1803 }
1804
1805 #[test]
1806 fn quoted_yes_stays_a_string_not_a_bool() {
1807 let doc = read_yaml("a: \"yes\"\n").unwrap();
1808 assert_eq!(
1809 *doc.root().get_one("a").unwrap().value().unwrap(),
1810 Scalar::Str("yes".to_string())
1811 );
1812 }
1813
1814 #[test]
1815 fn reads_null_spellings() {
1816 let doc = read_yaml("a: ~\nb: null\nc: Null\nd: NULL\ne:\n").unwrap();
1817 let root = doc.root();
1818 for label in ["a", "b", "c", "d", "e"] {
1819 assert_eq!(*root.get_one(label).unwrap().value().unwrap(), Scalar::Null);
1820 }
1821 }
1822
1823 #[test]
1824 fn reads_negative_and_hex_and_octal_and_binary_ints() {
1825 let doc = read_yaml("a: -5\nb: 0x1A\nc: 017\nd: 0b101\ne: 1_000\n").unwrap();
1830 let root = doc.root();
1831 assert_eq!(
1832 *root.get_one("a").unwrap().value().unwrap(),
1833 Scalar::Int((-5).into())
1834 );
1835 assert_eq!(
1836 *root.get_one("b").unwrap().value().unwrap(),
1837 Scalar::Int((26).into())
1838 );
1839 assert_eq!(
1840 *root.get_one("c").unwrap().value().unwrap(),
1841 Scalar::Int((15).into())
1842 );
1843 assert_eq!(
1844 *root.get_one("d").unwrap().value().unwrap(),
1845 Scalar::Int((5).into())
1846 );
1847 assert_eq!(
1848 *root.get_one("e").unwrap().value().unwrap(),
1849 Scalar::Int((1000).into())
1850 );
1851 }
1852
1853 #[test]
1854 fn a_yaml_1_2_style_0o_octal_prefix_is_not_recognized_and_stays_a_string() {
1855 let doc = read_yaml("a: 0o17\n").unwrap();
1856 assert_eq!(
1857 *doc.root().get_one("a").unwrap().value().unwrap(),
1858 Scalar::Str("0o17".to_string())
1859 );
1860 }
1861
1862 #[test]
1863 fn reads_legacy_sexagesimal_int_forms() {
1864 let doc =
1868 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")
1869 .unwrap();
1870 let root = doc.root();
1871 assert_eq!(
1872 *root.get_one("a").unwrap().value().unwrap(),
1873 Scalar::Int((43200).into())
1874 );
1875 assert_eq!(
1876 *root.get_one("b").unwrap().value().unwrap(),
1877 Scalar::Int((80).into())
1878 );
1879 assert_eq!(
1880 *root.get_one("c").unwrap().value().unwrap(),
1881 Scalar::Int((3723).into())
1882 );
1883 assert_eq!(
1884 *root.get_one("d").unwrap().value().unwrap(),
1885 Scalar::Int((-80).into())
1886 );
1887 assert_eq!(
1888 *root.get_one("e").unwrap().value().unwrap(),
1889 Scalar::Int((80).into())
1890 );
1891 assert_eq!(
1892 *root.get_one("f").unwrap().value().unwrap(),
1893 Scalar::Int((7425).into())
1894 );
1895 assert_eq!(
1896 *root.get_one("g").unwrap().value().unwrap(),
1897 Scalar::Int((750).into())
1898 );
1899 }
1900
1901 #[test]
1902 fn sexagesimal_first_group_over_i64_range_parses() {
1903 let text = format!("a: {}:0\n", "9".repeat(20));
1908 let doc = read_yaml(&text).unwrap();
1909 let value = doc.root().child("a").unwrap().value().unwrap();
1910 assert_eq!(
1911 value,
1912 &Scalar::Int(num_bigint::BigInt::parse_bytes(b"5999999999999999999940", 10).unwrap())
1913 );
1914 }
1915
1916 #[test]
1917 fn sexagesimal_fold_overflow_across_many_in_range_groups_parses() {
1918 let text = format!("a: 1{}\n", ":59".repeat(15));
1923 let doc = read_yaml(&text).unwrap();
1924 let value = doc.root().child("a").unwrap().value().unwrap();
1925 assert_eq!(
1926 value,
1927 &Scalar::Int(
1928 num_bigint::BigInt::parse_bytes(b"940369969151999999999999999", 10).unwrap()
1929 )
1930 );
1931 }
1932
1933 #[test]
1934 fn sexagesimal_fold_still_rejects_past_the_digit_cap() {
1935 let text = format!("a: 1{}\n", ":59".repeat(2500));
1942 let err = read_yaml(&text).unwrap_err();
1943 assert!(
1944 matches!(&err, OmnistError::Document(e)
1945 if e.code.as_deref() == Some("document.limit.int-digits")
1946 && e.path == "$.a"
1947 && e.message.contains("4300-digit")),
1948 "got {err:?}"
1949 );
1950 }
1951
1952 #[test]
1953 fn sexagesimal_shape_with_leading_zero_or_out_of_range_group_stays_a_string() {
1954 let doc = read_yaml("a: 0:0:1\nb: 1:60\nc: 1:600\nd: 01:20\n").unwrap();
1959 let root = doc.root();
1960 assert_eq!(
1961 *root.get_one("a").unwrap().value().unwrap(),
1962 Scalar::Str("0:0:1".to_string())
1963 );
1964 assert_eq!(
1965 *root.get_one("b").unwrap().value().unwrap(),
1966 Scalar::Str("1:60".to_string())
1967 );
1968 assert_eq!(
1969 *root.get_one("c").unwrap().value().unwrap(),
1970 Scalar::Str("1:600".to_string())
1971 );
1972 assert_eq!(
1973 *root.get_one("d").unwrap().value().unwrap(),
1974 Scalar::Str("01:20".to_string())
1975 );
1976 }
1977
1978 #[test]
1979 fn reads_float_and_inf_and_nan_tokens() {
1980 let doc = read_yaml("a: 1.5\nb: .inf\nc: -.inf\nd: .nan\ne: 1.0e+3\n").unwrap();
1981 let root = doc.root();
1982 assert_eq!(
1983 *root.get_one("a").unwrap().value().unwrap(),
1984 Scalar::Float(1.5)
1985 );
1986 assert_eq!(
1987 *root.get_one("b").unwrap().value().unwrap(),
1988 Scalar::Float(f64::INFINITY)
1989 );
1990 assert_eq!(
1991 *root.get_one("c").unwrap().value().unwrap(),
1992 Scalar::Float(f64::NEG_INFINITY)
1993 );
1994 assert!(
1995 matches!(root.get_one("d").unwrap().value().unwrap(), Scalar::Float(x) if x.is_nan())
1996 );
1997 assert_eq!(
1998 *root.get_one("e").unwrap().value().unwrap(),
1999 Scalar::Float(1000.0)
2000 );
2001 }
2002
2003 #[test]
2004 fn a_bare_exponent_without_a_decimal_point_is_not_float_shaped_and_stays_a_string() {
2005 let doc = read_yaml("a: 1e3\nb: 1.0e3\n").unwrap();
2009 let root = doc.root();
2010 assert_eq!(
2011 *root.get_one("a").unwrap().value().unwrap(),
2012 Scalar::Str("1e3".to_string())
2013 );
2014 assert_eq!(
2015 *root.get_one("b").unwrap().value().unwrap(),
2016 Scalar::Str("1.0e3".to_string())
2017 );
2018 }
2019
2020 #[test]
2021 fn reads_bare_date_as_iso_string() {
2022 let doc = read_yaml("a: 2024-01-15\n").unwrap();
2023 assert_eq!(
2024 *doc.root().get_one("a").unwrap().value().unwrap(),
2025 Scalar::Date("2024-01-15".to_string())
2026 );
2027 }
2028
2029 #[test]
2030 fn genuine_date_and_datetime_values_write_bare_and_a_time_value_writes_quoted() {
2031 let v = obj(vec![
2036 ("d", Value::Date("2024-01-15".to_string())),
2037 ("dt", Value::Datetime("2024-01-15T12:00:00".to_string())),
2038 ("t", Value::Time("12:00:00".to_string())),
2039 ]);
2040 let doc = doc_of(v);
2041 let text = write_yaml(&doc, false, None).unwrap();
2042 assert!(text.contains("d: 2024-01-15\n"));
2043 assert!(text.contains("dt: 2024-01-15T12:00:00\n"));
2044 assert!(text.contains("t: \"12:00:00\""));
2045 }
2046
2047 #[test]
2048 fn reads_loose_timestamp_and_normalizes_to_canonical_iso() {
2049 let doc = read_yaml("a: 2001-2-3 4:05:06.7 Z\n").unwrap();
2055 assert_eq!(
2056 *doc.root().get_one("a").unwrap().value().unwrap(),
2057 Scalar::Datetime("2001-02-03T04:05:06.700000+00:00".to_string())
2058 );
2059 }
2060
2061 #[test]
2062 fn a_single_digit_minute_or_second_is_not_timestamp_shaped_and_stays_a_string() {
2063 let doc = read_yaml("a: 2001-2-3 4:5:6\n").unwrap();
2068 assert_eq!(
2069 *doc.root().get_one("a").unwrap().value().unwrap(),
2070 Scalar::Str("2001-2-3 4:5:6".to_string())
2071 );
2072 }
2073
2074 #[test]
2075 fn reads_datetime_with_no_timezone_at_all() {
2076 let doc = read_yaml("a: 2024-01-15T12:30:00\n").unwrap();
2080 assert_eq!(
2081 *doc.root().get_one("a").unwrap().value().unwrap(),
2082 Scalar::Datetime("2024-01-15T12:30:00".to_string())
2083 );
2084 }
2085
2086 #[test]
2087 fn reads_timestamp_with_explicit_offset() {
2088 let doc = read_yaml("a: 2001-12-14T21:59:43.10-05:00\n").unwrap();
2089 assert_eq!(
2090 *doc.root().get_one("a").unwrap().value().unwrap(),
2091 Scalar::Datetime("2001-12-14T21:59:43.100000-05:00".to_string())
2092 );
2093 }
2094
2095 #[test]
2096 fn a_string_that_merely_looks_like_a_short_date_but_isnt_shaped_right_stays_a_string() {
2097 let doc = read_yaml("a: 2024-1\n").unwrap();
2098 assert_eq!(
2099 *doc.root().get_one("a").unwrap().value().unwrap(),
2100 Scalar::Str("2024-1".to_string())
2101 );
2102 }
2103
2104 #[test]
2107 fn reads_nested_mapping_and_sequence() {
2108 let doc = read_yaml("a:\n b:\n c: 1\nm:\n - 1\n - 2\n - 3\n").unwrap();
2109 let root = doc.root();
2110 let a = root.get_one("a").unwrap();
2111 let b = a.get_one("b").unwrap();
2112 assert_eq!(
2113 *b.get_one("c").unwrap().value().unwrap(),
2114 Scalar::Int((1).into())
2115 );
2116 let ms = root.get("m");
2117 assert_eq!(ms.len(), 3);
2118 assert_eq!(*ms[2].value().unwrap(), Scalar::Int((3).into()));
2119 }
2120
2121 #[test]
2122 fn reads_flow_style_mapping_and_sequence() {
2123 let doc = read_yaml("a: {b: 1, c: 2}\nm: [1, 2, 3]\n").unwrap();
2124 let root = doc.root();
2125 let a = root.get_one("a").unwrap();
2126 assert_eq!(
2127 *a.get_one("b").unwrap().value().unwrap(),
2128 Scalar::Int((1).into())
2129 );
2130 assert_eq!(root.get("m").len(), 3);
2131 }
2132
2133 #[test]
2134 fn bare_top_level_sequence_is_a_document_error_not_a_parse_error() {
2135 let err = read_yaml("- 1\n- 2\n").unwrap_err();
2136 assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
2137 }
2138
2139 #[test]
2140 fn sequence_of_sequences_is_a_document_error() {
2141 let err = read_yaml("m:\n - [1, 2]\n").unwrap_err();
2142 assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
2143 }
2144
2145 #[test]
2146 fn empty_input_reads_as_a_null_document() {
2147 let doc = read_yaml("").unwrap();
2148 assert_eq!(*doc.root().value().unwrap(), Scalar::Null);
2149 }
2150
2151 #[test]
2152 fn explicit_empty_document_marker_reads_as_a_null_document() {
2153 let doc = read_yaml("---\n").unwrap();
2158 assert_eq!(*doc.root().value().unwrap(), Scalar::Null);
2159 }
2160
2161 #[test]
2162 fn a_repeated_mapping_key_is_a_syntax_error() {
2163 for text in [
2165 "a: 1\nb: 2\na: 3\n",
2166 "{a: 1, a: 2}\n",
2167 "o:\n k: 1\n k: 2\n",
2168 "m:\n - {a: 1, a: 2}\n",
2169 "a: 1\n\"a\": 2\n",
2170 "a: 1\na: [2, 3]\n",
2171 "p: &p {x: 1}\nq: {a: 1, <<: *p, a: 2}\n",
2172 ] {
2173 let e = read_yaml(text).unwrap_err();
2174 assert!(
2175 matches!(&e, OmnistError::Parse(p)
2176 if p.code == "parse.codec-syntax" && p.message.contains("duplicate mapping key")),
2177 "{text:?}: {e:?}"
2178 );
2179 }
2180 }
2181
2182 #[test]
2183 fn the_same_key_in_two_mappings_or_differing_in_case_is_not_a_repeat() {
2184 assert!(read_yaml("x: {a: 1}\ny: {a: 2}\n").is_ok());
2185 assert!(read_yaml("a: 1\nA: 2\n").is_ok());
2186 let doc = read_yaml("p: &p {a: 1, b: 2}\nq: {<<: *p, a: 9}\n").unwrap();
2188 let q = doc.root().get_one("q").unwrap();
2189 assert_eq!(
2190 *q.get_one("a").unwrap().value().unwrap(),
2191 Scalar::Int(9.into())
2192 );
2193 assert_eq!(
2194 *q.get_one("b").unwrap().value().unwrap(),
2195 Scalar::Int(2.into())
2196 );
2197 }
2198
2199 #[test]
2200 fn omap_pairs_and_set_are_refused_with_tag_forbidden_at_the_root() {
2201 for text in [
2203 "t: !!omap [{k: 1}, {j: 2}]\n",
2204 "t: !!pairs [{k: 1}, {k: 2}]\n",
2205 "t: !!set {x, y}\n",
2206 "t: !!set\n ? x\n ? y\n",
2207 "t: !!omap []\n",
2208 "!!omap [{k: 1}]\n",
2209 "a:\n b: !!set {x}\n",
2210 "t: !!set x\n",
2211 ] {
2212 let e = read_yaml(text).unwrap_err();
2213 assert!(
2214 matches!(&e, OmnistError::Document(d)
2215 if d.code.as_deref() == Some("format.tag-forbidden")
2216 && d.path == "$"
2217 && d.message.contains("!!")),
2218 "{text:?}: {e:?}"
2219 );
2220 }
2221 assert!(read_yaml("t:\n - {k: 1}\n - {j: 2}\n").is_ok());
2223 }
2224
2225 #[test]
2226 fn a_merge_tagged_merge_key_merges_and_a_str_tagged_one_does_not() {
2227 let doc = read_yaml("p: &p {x: 1}\nq: {!!merge <<: *p, y: 2}\n").unwrap();
2229 let q = doc.root().get_one("q").unwrap();
2230 assert_eq!(q.labels(), vec!["x".to_string(), "y".to_string()]);
2231 let doc = read_yaml("p: &p {x: 1}\nr: &r {y: 2}\nq: {!!merge <<: [*p, *r]}\n").unwrap();
2232 let q = doc.root().get_one("q").unwrap();
2233 assert_eq!(q.labels(), vec!["x".to_string(), "y".to_string()]);
2234 let doc = read_yaml("q: {!!str <<: 2}\n").unwrap();
2235 let q = doc.root().get_one("q").unwrap();
2236 assert_eq!(q.labels(), vec!["<<".to_string()]);
2237 for text in ["q: {!!merge <<: 1}\n", "q: !!merge x\n"] {
2240 let e = read_yaml(text).unwrap_err();
2241 assert!(
2242 matches!(&e, OmnistError::Parse(p) if p.code == "parse.codec-syntax"),
2243 "{text:?}: {e:?}"
2244 );
2245 }
2246 }
2247
2248 #[test]
2249 fn invalid_yaml_syntax_is_a_parse_error() {
2250 let err = read_yaml("a: [1, 2\n").unwrap_err();
2251 assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
2252 }
2253
2254 #[test]
2255 fn multiple_documents_is_a_parse_error() {
2256 let err = read_yaml("a: 1\n---\nb: 2\n").unwrap_err();
2257 assert!(
2258 matches!(&err, OmnistError::Parse(e) if e.message.contains("single document")),
2259 "got {err:?}"
2260 );
2261 }
2262
2263 #[test]
2264 fn nesting_far_past_max_depth_trips_the_event_stream_guard_not_the_stack() {
2265 let mut seq = String::new();
2271 for _ in 0..5000 {
2272 seq.push_str("- ");
2273 }
2274 let err = read_yaml(&seq).unwrap_err();
2275 assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
2276
2277 let mut map = String::new();
2278 for i in 0..5000 {
2279 map.push_str(&" ".repeat(i));
2280 map.push_str("a:\n");
2281 }
2282 let err = read_yaml(&map).unwrap_err();
2283 assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
2284 }
2285
2286 #[test]
2287 fn nesting_past_max_depth_is_a_document_error() {
2288 let mut text = String::new();
2291 for i in 0..=crate::document::MAX_DEPTH {
2292 text.push_str(&" ".repeat(i));
2293 text.push_str("a:\n");
2294 }
2295 let err = read_yaml(&text).unwrap_err();
2296 assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
2297 }
2298
2299 #[test]
2300 fn integer_literal_over_digit_cap_is_rejected() {
2301 let text = format!("a: {}\n", "9".repeat(MAX_INT_DIGITS + 1));
2302 let err = read_yaml(&text).unwrap_err();
2303 assert!(
2304 matches!(&err, OmnistError::Document(e)
2305 if e.code.as_deref() == Some("document.limit.int-digits")
2306 && e.path == "$.a"
2307 && e.message.contains("4300-digit")),
2308 "got {err:?}"
2309 );
2310 }
2311
2312 fn doc_diag(text: &str) -> (String, String) {
2314 use OmnistError as E;
2315 let err = read_yaml(text).unwrap_err();
2316 let E::Document(e) = err else { panic!() };
2317 (e.code.unwrap_or_default(), e.path)
2318 }
2319
2320 fn unlabeled(path: &str) -> (String, String) {
2321 ("document.unlabeled-element".to_string(), path.to_string())
2322 }
2323
2324 #[test]
2325 fn a_non_string_key_in_the_top_level_mapping_is_at_the_root() {
2326 assert_eq!(doc_diag("on: 1\n"), unlabeled("$"));
2328 assert_eq!(doc_diag("a: 1\n1: x\n"), unlabeled("$"));
2329 assert_eq!(doc_diag("~: x\n"), unlabeled("$"));
2330 }
2331
2332 #[test]
2333 fn a_non_string_key_in_a_nested_mapping_carries_that_mapping_s_path() {
2334 assert_eq!(doc_diag("a: {on: 1}\n"), unlabeled("$.a"));
2335 assert_eq!(doc_diag("a:\n on: 1\n"), unlabeled("$.a"));
2336 assert_eq!(
2337 doc_diag("a:\n b:\n c:\n 1: x\n"),
2338 unlabeled("$.a.b.c")
2339 );
2340 assert_eq!(doc_diag("x: 1\na:\n y: 2\n no: 3\n"), unlabeled("$.a"));
2341 assert_eq!(doc_diag("\"a.b\": {on: 1}\n"), unlabeled("$[\"a.b\"]"));
2344 assert_eq!(doc_diag("\"a b\": {on: 1}\n"), unlabeled("$.a b"));
2345 }
2346
2347 #[test]
2348 fn a_non_string_key_in_a_mapping_inside_a_sequence_follows_e10() {
2349 assert_eq!(doc_diag("a: [{on: 1}]\n"), unlabeled("$.a"));
2352 assert_eq!(doc_diag("a: [{x: 1}, {on: 1}]\n"), unlabeled("$.a[1]"));
2353 assert_eq!(doc_diag("a: [{on: 1}, {x: 1}]\n"), unlabeled("$.a[0]"));
2354 assert_eq!(
2355 doc_diag("a: [{x: 1}, {y: 2}, {z: 3}, {on: 1}]\n"),
2356 unlabeled("$.a[3]")
2357 );
2358 assert_eq!(
2360 doc_diag("a:\n b:\n - {x: 1}\n - {c: {on: 1}}\n"),
2361 unlabeled("$.a.b[1].c")
2362 );
2363 }
2364
2365 #[test]
2366 fn a_non_string_key_reached_again_through_a_merge_is_reported_where_first_written() {
2367 assert_eq!(
2370 doc_diag("base: &b {on: 1}\nm:\n <<: *b\n"),
2371 unlabeled("$.base")
2372 );
2373 }
2374
2375 #[test]
2376 fn an_over_long_integer_key_in_a_nested_mapping_carries_the_mapping_s_path() {
2377 let options =
2378 YamlReadOptions::default().with_limits(Limits::default().with_max_int_digits(3));
2379 let err = read_yaml_with("a: {b: {1000: x}}\n", &options).unwrap_err();
2380 assert!(
2381 matches!(&err, OmnistError::Document(e)
2382 if e.code.as_deref() == Some("document.limit.int-digits") && e.path == "$.a.b"),
2383 "got {err:?}"
2384 );
2385 }
2386
2387 #[test]
2388 fn an_over_limit_integer_in_a_dropped_merge_entry_is_still_refused() {
2389 let options =
2393 YamlReadOptions::default().with_limits(Limits::default().with_max_int_digits(3));
2394 for text in [
2395 "m: {<<: {a: 1000}, a: 1}
2396",
2397 "m: {a: 1, <<: {a: 1000}}
2398",
2399 "m: {<<: [{a: 1}, {a: 1000}]}
2400",
2401 "m: {<<: {a: [1, {z: 1000}]}, a: 1}
2402",
2403 "m: {<<: {<<: {a: 1000}, a: 1}}
2404",
2405 "m: {<<: {<<: {a: 1000}}, a: 1}
2406",
2407 ] {
2408 let err = read_yaml_with(text, &options).unwrap_err();
2409 assert!(
2410 matches!(&err, OmnistError::Document(e)
2411 if e.code.as_deref() == Some("document.limit.int-digits")),
2412 "{text:?} gave {err:?}"
2413 );
2414 }
2415 assert!(
2417 read_yaml_with(
2418 "m: {<<: {a: 999}, a: 1}
2419",
2420 &options
2421 )
2422 .is_ok()
2423 );
2424 }
2425
2426 #[test]
2427 fn an_over_long_integer_used_as_a_key_is_refused_by_the_digit_limit() {
2428 let options =
2430 YamlReadOptions::default().with_limits(Limits::default().with_max_int_digits(3));
2431 let err = read_yaml_with("1000: x\n", &options).unwrap_err();
2432 assert!(
2433 matches!(&err, OmnistError::Document(e)
2434 if e.code.as_deref() == Some("document.limit.int-digits") && e.path == "$"),
2435 "got {err:?}"
2436 );
2437 let err = read_yaml_with("999: x\n", &options).unwrap_err();
2439 assert!(
2440 matches!(&err, OmnistError::Document(e)
2441 if e.code.as_deref() == Some("document.unlabeled-element")),
2442 "got {err:?}"
2443 );
2444 }
2445
2446 #[test]
2447 fn i64_min_round_trips_through_yaml() {
2448 let doc = read_yaml("a: -9223372036854775808\n").unwrap();
2454 assert_eq!(
2455 *doc.root().get_one("a").unwrap().value().unwrap(),
2456 Scalar::Int((i64::MIN).into())
2457 );
2458 }
2459
2460 #[test]
2461 fn positive_integer_one_past_i64_max_parses() {
2462 let doc = read_yaml("a: 9223372036854775808\n").unwrap();
2465 let value = doc.root().child("a").unwrap().value().unwrap();
2466 assert_eq!(value, &Scalar::Int(num_bigint::BigInt::from(i64::MAX) + 1));
2467 }
2468
2469 #[test]
2470 fn negative_integer_one_past_i64_min_parses() {
2471 let doc = read_yaml("a: -9223372036854775809\n").unwrap();
2474 let value = doc.root().child("a").unwrap().value().unwrap();
2475 assert_eq!(value, &Scalar::Int(num_bigint::BigInt::from(i64::MIN) - 1));
2476 }
2477
2478 #[test]
2479 fn integer_literal_over_i64_range_parses() {
2480 let text = format!("a: {}\n", "9".repeat(20));
2481 let doc = read_yaml(&text).unwrap();
2482 let value = doc.root().child("a").unwrap().value().unwrap();
2483 assert_eq!(
2484 value,
2485 &Scalar::Int(num_bigint::BigInt::parse_bytes(b"99999999999999999999", 10).unwrap())
2486 );
2487 }
2488
2489 #[test]
2492 fn reads_anchor_and_alias_as_a_deep_copy() {
2493 let doc = read_yaml("a: &x [1, 2, 3]\nb: *x\n").unwrap();
2494 let root = doc.root();
2495 assert_eq!(root.get("a").len(), 3);
2496 assert_eq!(root.get("b").len(), 3);
2497 assert_eq!(*root.get("b")[1].value().unwrap(), Scalar::Int((2).into()));
2498 }
2499
2500 #[test]
2501 fn unknown_alias_is_a_parse_error() {
2502 let err = read_yaml("a: *nope\n").unwrap_err();
2503 assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
2504 }
2505
2506 fn billion_laughs_yaml(generations: usize) -> String {
2524 let mut out = String::new();
2525 out.push_str("a0: &a0 [x, x]\n");
2526 for i in 1..generations {
2527 out.push_str(&format!("a{i}: &a{i} [*a{prev}, *a{prev}]\n", prev = i - 1));
2528 }
2529 out
2530 }
2531
2532 #[test]
2533 fn billion_laughs_alias_amplification_is_rejected_fast_issue_42() {
2534 let text = billion_laughs_yaml(24);
2535 assert!(text.len() < 1000, "source text should be tiny: {text:?}");
2537
2538 let start = std::time::Instant::now();
2539 let err = read_yaml(&text).unwrap_err();
2540 let elapsed = start.elapsed();
2541
2542 assert!(
2546 matches!(&err, OmnistError::Document(e)
2547 if e.code.as_deref() == Some("document.limit.alias-expansion")
2548 && e.path == "$"),
2549 "expected the alias expansion rejection, got {err:?}"
2550 );
2551 assert!(
2552 elapsed < std::time::Duration::from_secs(5),
2553 "fix should reject the bomb almost immediately, took {elapsed:?}"
2554 );
2555 }
2556
2557 #[test]
2558 fn moderate_legitimate_nested_alias_reuse_still_works() {
2559 let doc = read_yaml(
2564 "base: &base\n x: 1\n y: 2\na: *base\nb: *base\nc: *base\nlist: &list [1, 2, 3]\nd: *list\ne: *list\n",
2565 )
2566 .unwrap();
2567 let root = doc.root();
2568 for label in ["a", "b", "c"] {
2569 let node = root.get_one(label).unwrap();
2570 assert_eq!(
2571 *node.get_one("x").unwrap().value().unwrap(),
2572 Scalar::Int((1).into())
2573 );
2574 assert_eq!(
2575 *node.get_one("y").unwrap().value().unwrap(),
2576 Scalar::Int((2).into())
2577 );
2578 }
2579 for label in ["d", "e"] {
2580 assert_eq!(root.get(label).len(), 3);
2581 }
2582 }
2583
2584 #[test]
2587 fn merge_key_from_a_mapping_merges_with_local_keys_winning() {
2588 let doc =
2589 read_yaml("base: &b\n x: 1\n y: 2\nchild:\n <<: *b\n y: 20\n z: 3\n").unwrap();
2590 let child = doc.root().get_one("child").unwrap();
2591 assert_eq!(
2592 *child.get_one("x").unwrap().value().unwrap(),
2593 Scalar::Int((1).into())
2594 );
2595 assert_eq!(
2596 *child.get_one("y").unwrap().value().unwrap(),
2597 Scalar::Int((20).into()),
2598 "an explicit local key beats the merged-in value"
2599 );
2600 assert_eq!(
2601 *child.get_one("z").unwrap().value().unwrap(),
2602 Scalar::Int((3).into())
2603 );
2604 }
2605
2606 #[test]
2607 fn merge_key_from_a_sequence_of_mappings_merges_each_in_order() {
2608 let doc =
2611 read_yaml("a: &a\n x: 1\nb: &b\n x: 2\n y: 3\nchild:\n <<: [*a, *b]\n").unwrap();
2612 let child = doc.root().get_one("child").unwrap();
2613 assert_eq!(
2614 *child.get_one("x").unwrap().value().unwrap(),
2615 Scalar::Int((1).into())
2616 );
2617 assert_eq!(
2618 *child.get_one("y").unwrap().value().unwrap(),
2619 Scalar::Int((3).into())
2620 );
2621 }
2622
2623 #[test]
2624 fn quoted_double_angle_bracket_key_is_a_literal_string_not_a_merge() {
2625 let doc = read_yaml("a:\n \"<<\": 1\n").unwrap();
2626 let a = doc.root().get_one("a").unwrap();
2627 assert_eq!(
2628 *a.get_one("<<").unwrap().value().unwrap(),
2629 Scalar::Int((1).into())
2630 );
2631 }
2632
2633 #[test]
2638 fn merge_key_from_a_non_map_scalar_source_is_a_clean_parse_error_omnist_ts_46() {
2639 let err = read_yaml("child:\n <<: 5\n y: 2\n").unwrap_err();
2640 assert!(
2641 matches!(&err, OmnistError::Parse(e) if e.message.contains("merge key")),
2642 "got {err:?}"
2643 );
2644 }
2645
2646 #[test]
2647 fn merge_key_from_a_sequence_containing_a_scalar_is_a_clean_parse_error() {
2648 let err = read_yaml("child:\n <<: [1, 2]\n").unwrap_err();
2649 assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
2650 }
2651
2652 #[test]
2653 fn merge_source_with_a_non_scalar_key_is_a_clean_parse_error() {
2654 let err = read_yaml("base: &b\n ? [1, 2]\n : 3\nchild:\n <<: *b\n").unwrap_err();
2659 assert!(
2660 matches!(&err, OmnistError::Parse(e) if e.message.contains("mapping key must be a scalar")),
2661 "got {err:?}"
2662 );
2663 }
2664
2665 #[test]
2666 fn merge_key_from_an_alias_to_a_scalar_is_a_clean_parse_error() {
2667 let err = read_yaml("base: &b 5\nchild:\n <<: *b\n").unwrap_err();
2668 assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
2669 }
2670
2671 #[test]
2674 fn explicit_tags_construct_the_named_type_regardless_of_spelling() {
2675 let doc = read_yaml("a: !!str yes\nb: !!int \"5\"\nc: !!bool \"true\"\n").unwrap();
2676 let root = doc.root();
2677 assert_eq!(
2678 *root.get_one("a").unwrap().value().unwrap(),
2679 Scalar::Str("yes".to_string())
2680 );
2681 assert_eq!(
2682 *root.get_one("b").unwrap().value().unwrap(),
2683 Scalar::Int((5).into())
2684 );
2685 assert_eq!(
2686 *root.get_one("c").unwrap().value().unwrap(),
2687 Scalar::Bool(true)
2688 );
2689 }
2690
2691 #[test]
2692 fn unsupported_explicit_tag_is_a_parse_error() {
2693 let err = read_yaml("a: !!binary \"x\"\n").unwrap_err();
2694 assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
2695 }
2696
2697 #[test]
2698 fn explicit_bool_tag_accepts_the_full_yaml_1_1_spelling_set_not_just_true_false() {
2699 let doc = read_yaml("a: !!bool \"yes\"\nb: !!bool \"On\"\nc: !!bool \"OFF\"\n").unwrap();
2704 let root = doc.root();
2705 assert_eq!(
2706 *root.get_one("a").unwrap().value().unwrap(),
2707 Scalar::Bool(true)
2708 );
2709 assert_eq!(
2710 *root.get_one("b").unwrap().value().unwrap(),
2711 Scalar::Bool(true)
2712 );
2713 assert_eq!(
2714 *root.get_one("c").unwrap().value().unwrap(),
2715 Scalar::Bool(false)
2716 );
2717 }
2718
2719 #[test]
2720 fn invalid_explicit_bool_spelling_is_a_parse_error() {
2721 let err = read_yaml("a: !!bool \"nonsense\"\n").unwrap_err();
2722 assert!(
2723 matches!(&err, OmnistError::Parse(e) if e.message.contains("!!bool")),
2724 "got {err:?}"
2725 );
2726 }
2727
2728 #[test]
2729 fn bare_y_or_n_is_not_a_valid_explicit_bool_spelling() {
2730 let err = read_yaml("a: !!bool \"y\"\n").unwrap_err();
2736 assert!(
2737 matches!(&err, OmnistError::Parse(e) if e.message.contains("!!bool")),
2738 "got {err:?}"
2739 );
2740 }
2741
2742 #[test]
2743 fn bare_on_key_resolves_to_boolean_and_is_rejected_norway_problem() {
2744 let err = read_yaml("on:\n push: true\n").unwrap_err();
2749 assert!(
2750 matches!(&err, OmnistError::Document(e) if e.path == "$"),
2751 "got {err:?}"
2752 );
2753 }
2754
2755 #[test]
2756 fn other_implicit_bool_and_null_key_spellings_are_also_rejected() {
2757 for key in ["off", "yes", "no", "Off", "YES", "~", "null", "true"] {
2758 let text = format!("{key}:\n push: true\n");
2759 let err = read_yaml(&text).unwrap_err();
2760 assert!(
2761 matches!(&err, OmnistError::Document(_)),
2762 "key {key:?} got {err:?}"
2763 );
2764 }
2765 }
2766
2767 #[test]
2768 fn bare_y_or_n_key_is_not_a_bool_and_stays_a_string_label() {
2769 let doc = read_yaml("y:\n push: true\n").unwrap();
2773 assert!(doc.root().get_one("y").is_ok());
2774 let doc = read_yaml("n:\n push: true\n").unwrap();
2775 assert!(doc.root().get_one("n").is_ok());
2776 }
2777
2778 #[test]
2779 fn sexagesimal_looking_key_also_resolves_and_is_rejected() {
2780 let err = read_yaml("12:00:00:\n push: true\n").unwrap_err();
2784 assert!(
2785 matches!(&err, OmnistError::Document(e) if e.path == "$"),
2786 "got {err:?}"
2787 );
2788 }
2789
2790 #[test]
2791 fn describe_non_string_key_renders_pythonic_spellings() {
2792 assert_eq!(describe_non_string_key(&Value::Bool(true)), "True");
2793 assert_eq!(describe_non_string_key(&Value::Bool(false)), "False");
2794 assert_eq!(describe_non_string_key(&Value::Null), "None");
2795 assert_eq!(
2796 describe_non_string_key(&Value::Int((43200).into())),
2797 "43200"
2798 );
2799 assert_eq!(describe_non_string_key(&Value::Float(1.5)), "1.5");
2800 assert_eq!(describe_non_string_key(&Value::Float(1.0)), "1.0");
2803 assert_eq!(describe_non_string_key(&Value::Float(-2.0)), "-2.0");
2804 assert_eq!(
2808 describe_non_string_key(&Value::Str("x".to_string())),
2809 "Str(\"x\")"
2810 );
2811 }
2812
2813 #[test]
2814 fn int_and_float_shaped_mapping_keys_are_rejected_with_python_parity_messages() {
2815 let err = read_yaml("123:\n a: 1\n").unwrap_err();
2819 assert!(
2820 matches!(&err, OmnistError::Document(e) if e.path == "$" && e.message.contains("123")),
2821 "got {err:?}"
2822 );
2823 let err = read_yaml("1.0:\n a: 1\n").unwrap_err();
2824 assert!(
2825 matches!(&err, OmnistError::Document(e) if e.path == "$" && e.message.contains("1.0")),
2826 "got {err:?}"
2827 );
2828 }
2829
2830 #[test]
2831 fn a_non_scalar_mapping_key_is_a_clean_parse_error() {
2832 let err = read_yaml("? [1, 2]\n: 3\n").unwrap_err();
2836 assert!(
2837 matches!(&err, OmnistError::Parse(e) if e.message.contains("mapping key must be a scalar")),
2838 "got {err:?}"
2839 );
2840 }
2841
2842 #[test]
2845 fn round_trips_every_scalar_kind() {
2846 let v = obj(vec![
2847 ("null", Value::Null),
2848 ("bool", Value::Bool(true)),
2849 ("int", Value::Int((42).into())),
2850 ("float", Value::Float(1.5)),
2851 ("str", Value::Str("hi".to_string())),
2852 ]);
2853 let doc = doc_of(v);
2854 let text = write_yaml(&doc, false, None).unwrap();
2855 let back = read_yaml(&text).unwrap();
2856 assert!(doc.eq_doc(&back));
2857 }
2858
2859 #[test]
2860 fn round_trips_integral_float_at_and_above_1e17_boundary_issue_46() {
2861 for x in [1.0e17, 1.0e18, -1.23e17, 9.9e16_f64] {
2865 let doc = doc_of(obj(vec![("a", Value::Float(x))]));
2866 let text = write_yaml(&doc, false, None).unwrap();
2867 let back = read_yaml(&text).unwrap();
2868 assert_eq!(
2869 *back.root().get_one("a").unwrap().value().unwrap(),
2870 Scalar::Float(x),
2871 "x={x} text={text}"
2872 );
2873 }
2874 }
2875
2876 #[test]
2877 fn round_trips_nan_and_infinity_natively_no_adjustment_needed() {
2878 let v = obj(vec![
2879 ("a", Value::Float(f64::NAN)),
2880 ("b", Value::Float(f64::INFINITY)),
2881 ("c", Value::Float(f64::NEG_INFINITY)),
2882 ]);
2883 let doc = doc_of(v);
2884 let mut rep = WriteReport::new();
2885 let text = write_yaml(&doc, false, Some(&mut rep)).unwrap();
2886 assert!(rep.is_empty());
2887 let back = read_yaml(&text).unwrap();
2888 assert!(
2889 matches!(back.root().get_one("a").unwrap().value().unwrap(), Scalar::Float(x) if x.is_nan())
2890 );
2891 assert_eq!(
2892 *back.root().get_one("b").unwrap().value().unwrap(),
2893 Scalar::Float(f64::INFINITY)
2894 );
2895 assert_eq!(
2896 *back.root().get_one("c").unwrap().value().unwrap(),
2897 Scalar::Float(f64::NEG_INFINITY)
2898 );
2899 }
2900
2901 #[test]
2902 fn round_trips_strings_that_look_like_other_scalar_kinds() {
2903 let v = obj(vec![
2904 ("a", Value::Str("yes".to_string())),
2905 ("b", Value::Str("null".to_string())),
2906 ("c", Value::Str("123".to_string())),
2907 ("d", Value::Str("1.5".to_string())),
2908 ("e", Value::Str("".to_string())),
2909 ("f", Value::Str("2024-01-15".to_string())),
2910 ]);
2911 let doc = doc_of(v);
2912 let text = write_yaml(&doc, false, None).unwrap();
2913 let back = read_yaml(&text).unwrap();
2914 assert!(doc.eq_doc(&back), "text was:\n{text}");
2915 }
2916
2917 #[test]
2918 fn round_trips_repeated_labels_as_a_yaml_sequence() {
2919 let doc = doc_of(obj(vec![(
2920 "m",
2921 Value::Array(vec![
2922 Value::Int((1).into()),
2923 Value::Int((2).into()),
2924 Value::Int((3).into()),
2925 ]),
2926 )]));
2927 let text = write_yaml(&doc, false, None).unwrap();
2928 let back = read_yaml(&text).unwrap();
2929 assert!(doc.eq_doc(&back));
2930 }
2931
2932 #[test]
2933 fn round_trips_nested_mappings_and_sequences_of_mappings() {
2934 let v = obj(vec![
2935 (
2936 "a",
2937 obj(vec![
2938 ("b", Value::Int((1).into())),
2939 ("c", Value::Int((2).into())),
2940 ]),
2941 ),
2942 (
2943 "items",
2944 Value::Array(vec![
2945 obj(vec![
2946 ("x", Value::Int((1).into())),
2947 ("y", Value::Int((2).into())),
2948 ]),
2949 obj(vec![
2950 ("x", Value::Int((3).into())),
2951 ("y", Value::Int((4).into())),
2952 ]),
2953 ]),
2954 ),
2955 ]);
2956 let doc = doc_of(v);
2957 let text = write_yaml(&doc, false, None).unwrap();
2958 let back = read_yaml(&text).unwrap();
2959 assert!(doc.eq_doc(&back), "text was:\n{text}");
2960 }
2961
2962 #[test]
2963 fn writes_empty_object_and_array_compactly() {
2964 let doc = doc_of(obj(vec![("o", Value::Object(IndexMap::new()))]));
2965 let text = write_yaml(&doc, false, None).unwrap();
2966 assert!(text.contains("o: {}"));
2967 }
2968
2969 #[test]
2970 fn nel_string_triggers_a_warning_and_still_round_trips() {
2971 let s = format!("a{}b", '\u{0085}');
2972 let doc = doc_of(obj(vec![("s", Value::Str(s.clone()))]));
2973 let mut rep = WriteReport::new();
2974 let text = write_yaml(&doc, false, Some(&mut rep)).unwrap();
2975 assert_eq!(rep.len(), 1);
2976 assert_eq!(rep.adjustments()[0].code, "format.string-line-break-char");
2977 let back = read_yaml(&text).unwrap();
2978 assert_eq!(
2979 *back.root().get_one("s").unwrap().value().unwrap(),
2980 Scalar::Str(s)
2981 );
2982 }
2983
2984 #[test]
2985 fn strict_write_with_nel_raises_and_carries_the_report() {
2986 let s = format!("x{}y", '\u{0085}');
2987 let doc = doc_of(obj(vec![("s", Value::Str(s))]));
2988 let err = write_yaml(&doc, true, None).unwrap_err();
2989 let rep = err.report().expect("strict WriteError carries a report");
2990 assert_eq!(rep.len(), 1);
2991 }
2992
2993 #[test]
2994 fn strict_write_with_no_adjustments_succeeds() {
2995 let doc = doc_of(obj(vec![("a", Value::Int((1).into()))]));
2996 let text = write_yaml(&doc, true, None).unwrap();
2997 assert!(text.contains("a: 1"));
2998 }
2999
3000 #[test]
3001 fn check_yaml_reports_without_producing_output() {
3002 let s = format!("a{}b", '\u{0085}');
3003 let doc = doc_of(obj(vec![("s", Value::Str(s))]));
3004 let rep = check_yaml(&doc);
3005 assert_eq!(rep.len(), 1);
3006 assert_eq!(rep.adjustments()[0].path, "$.s");
3007 }
3008
3009 #[test]
3010 fn deeply_nested_document_write_reuses_doc_construction_depth_guard() {
3011 let mut v = Value::Int((0).into());
3012 for _ in 0..=crate::document::MAX_DEPTH {
3013 v = obj(vec![("a", v)]);
3014 }
3015 assert!(Doc::of(&v).is_err());
3016 }
3017
3018 #[test]
3027 fn wide_document_smoke_test_reads_and_round_trips_every_field() {
3028 let n = 5_000;
3029 let mut text = String::new();
3030 for i in 0..n {
3031 text.push_str(&format!("field{i}: {i}\n"));
3032 }
3033 let doc = read_yaml(&text).unwrap();
3034 let root = doc.root();
3035 assert_eq!(root.labels().len(), n);
3036 for i in [0, n / 2, n - 1] {
3037 assert_eq!(
3038 *root.get_one(&format!("field{i}")).unwrap().value().unwrap(),
3039 Scalar::Int((i as i64).into())
3040 );
3041 }
3042 let out = write_yaml(&doc, false, None).unwrap();
3043 let back = read_yaml(&out).unwrap();
3044 assert!(doc.eq_doc(&back));
3045 }
3046
3047 #[test]
3048 fn wide_flat_sequence_smoke_test() {
3049 let n = 5_000;
3050 let mut text = String::from("m:\n");
3051 for i in 0..n {
3052 text.push_str(&format!(" - {i}\n"));
3053 }
3054 let doc = read_yaml(&text).unwrap();
3055 assert_eq!(doc.root().get("m").len(), n);
3056 }
3057
3058 #[test]
3061 fn invalid_explicit_float_literal_is_a_parse_error() {
3062 let err = read_yaml("a: !!float \"not-a-float\"\n").unwrap_err();
3063 assert!(
3064 matches!(&err, OmnistError::Parse(e) if e.message.contains("invalid float literal")),
3065 "got {err:?}"
3066 );
3067 }
3068
3069 #[test]
3070 fn explicit_float_tag_accepts_inf_and_nan_and_negative() {
3071 let doc = read_yaml("a: !!float \"-1.5\"\n").unwrap();
3072 assert_eq!(
3073 *doc.root().get_one("a").unwrap().value().unwrap(),
3074 Scalar::Float(-1.5)
3075 );
3076 }
3077
3078 #[test]
3089 fn timestamp_with_invalid_month_is_a_parse_error() {
3090 let err = read_yaml("a: 2024-13-01\n").unwrap_err();
3091 assert!(
3092 matches!(&err, OmnistError::Parse(e) if e.message.contains("calendar date")),
3093 "got {err:?}"
3094 );
3095 }
3096
3097 #[test]
3098 fn timestamp_with_year_zero_is_a_parse_error() {
3099 let err = read_yaml("a: 0000-01-01\n").unwrap_err();
3100 assert!(
3101 matches!(&err, OmnistError::Parse(e) if e.message.contains("calendar date")),
3102 "got {err:?}"
3103 );
3104 }
3105
3106 #[test]
3107 fn timestamp_with_a_day_that_doesnt_exist_in_the_month_is_a_parse_error() {
3108 let err = read_yaml("a: 2024-02-30\n").unwrap_err();
3112 assert!(
3113 matches!(&err, OmnistError::Parse(e) if e.message.contains("calendar date")),
3114 "got {err:?}"
3115 );
3116 }
3117
3118 #[test]
3119 fn timestamp_february_29_on_a_non_leap_year_is_a_parse_error() {
3120 let err = read_yaml("a: 2023-02-29\n").unwrap_err();
3121 assert!(
3122 matches!(&err, OmnistError::Parse(e) if e.message.contains("calendar date")),
3123 "got {err:?}"
3124 );
3125 }
3126
3127 #[test]
3128 fn timestamp_february_29_on_a_leap_year_normalizes_fine() {
3129 let doc = read_yaml("a: 2024-02-29\n").unwrap();
3130 assert_eq!(
3131 *doc.root().get_one("a").unwrap().value().unwrap(),
3132 Scalar::Date("2024-02-29".to_string())
3133 );
3134 }
3135
3136 #[test]
3137 fn timestamp_with_out_of_range_hour_is_a_parse_error() {
3138 let err = read_yaml("a: 2024-01-01T25:00:00\n").unwrap_err();
3139 assert!(
3140 matches!(&err, OmnistError::Parse(e) if e.message.contains("time of day")),
3141 "got {err:?}"
3142 );
3143 }
3144
3145 #[test]
3146 fn timestamp_with_out_of_range_minute_is_a_parse_error() {
3147 let err = read_yaml("a: 2024-01-01T00:61:00\n").unwrap_err();
3148 assert!(
3149 matches!(&err, OmnistError::Parse(e) if e.message.contains("time of day")),
3150 "got {err:?}"
3151 );
3152 }
3153
3154 #[test]
3155 fn timestamp_with_out_of_range_timezone_offset_is_a_parse_error() {
3156 let err = read_yaml("a: 2024-01-01T00:00:00+25:00\n").unwrap_err();
3157 assert!(
3158 matches!(&err, OmnistError::Parse(e) if e.message.contains("timezone offset")),
3159 "got {err:?}"
3160 );
3161 }
3162
3163 #[test]
3164 fn timestamp_with_hour_only_timezone_offset_normalizes_with_zero_minutes() {
3165 let doc = read_yaml("a: 2024-01-01T00:00:00+05\n").unwrap();
3166 assert_eq!(
3167 *doc.root().get_one("a").unwrap().value().unwrap(),
3168 Scalar::Datetime("2024-01-01T00:00:00+05:00".to_string())
3169 );
3170 }
3171
3172 #[test]
3175 fn nel_in_a_label_triggers_a_warning_and_still_round_trips() {
3176 let label = format!("a{}b", '\u{0085}');
3177 let doc = doc_of(obj(vec![(label.as_str(), Value::Int((1).into()))]));
3178 let mut rep = WriteReport::new();
3179 let text = write_yaml(&doc, false, Some(&mut rep)).unwrap();
3180 assert_eq!(rep.len(), 1);
3181 assert_eq!(rep.adjustments()[0].code, "format.string-line-break-char");
3182 let back = read_yaml(&text).unwrap();
3183 assert_eq!(
3184 *back.root().get_one(&label).unwrap().value().unwrap(),
3185 Scalar::Int((1).into())
3186 );
3187 }
3188
3189 #[test]
3190 fn write_node_on_a_bare_empty_array_writes_the_flow_empty_token() {
3191 let mut out = String::new();
3199 write_node(&Value::Array(vec![]), 0, &mut out, true);
3200 assert_eq!(out, "[]\n");
3201 }
3202
3203 #[test]
3204 fn round_trips_strings_needing_every_quoting_trigger() {
3205 let cases = [
3206 "-leading-dash",
3207 "?leading-question",
3208 ":leading-colon",
3209 ",leading-comma",
3210 "[leading-bracket",
3211 "]leading-bracket",
3212 "{leading-brace",
3213 "}leading-brace",
3214 "#leading-hash",
3215 "&leading-amp",
3216 "*leading-star",
3217 "!leading-bang",
3218 "|leading-pipe",
3219 ">leading-gt",
3220 "'leading-quote",
3221 "\"leading-dquote",
3222 "%leading-percent",
3223 "@leading-at",
3224 "`leading-backtick",
3225 " leading-space",
3226 "trailing-space ",
3227 "embedded: colon-space",
3228 "trailing-colon:",
3229 "embedded #hash-space",
3230 "line\nbreak",
3231 "tab\ttab",
3232 "quote\"quote",
3233 "back\\slash",
3234 "control\u{01}char",
3235 ];
3236 for s in cases {
3237 let doc = doc_of(obj(vec![("s", Value::Str(s.to_string()))]));
3238 let text = write_yaml(&doc, false, None).unwrap();
3239 let back = read_yaml(&text).unwrap();
3240 assert!(
3241 doc.eq_doc(&back),
3242 "round trip failed for {s:?}, text was:\n{text}"
3243 );
3244 }
3245 }
3246
3247 #[test]
3248 fn write_scalar_value_panics_on_a_non_leaf_value() {
3249 let result = std::panic::catch_unwind(|| {
3254 let mut out = String::new();
3255 write_scalar_value(&Value::Object(IndexMap::new()), &mut out);
3256 });
3257 assert!(result.is_err());
3258 }
3259
3260 use yaml_rust2::parser::Event;
3274 use yaml_rust2::scanner::Marker;
3275
3276 fn test_marker() -> Marker {
3281 struct Capture(Option<Marker>);
3282 impl MarkedEventReceiver for Capture {
3283 fn on_event(&mut self, _ev: Event, mark: Marker) {
3284 self.0.get_or_insert(mark);
3285 }
3286 }
3287 let mut cap = Capture(None);
3288 Parser::new("x".chars()).load(&mut cap, false).unwrap();
3289 cap.0
3290 .expect("a trivial scalar document always emits at least one event")
3291 }
3292
3293 #[test]
3297 #[should_panic(expected = "the alias check refused a scalar merge value")]
3298 fn merge_source_entries_panics_on_a_scalar_the_check_refused() {
3299 let scalar = Raw::Scalar("1".to_string(), TScalarStyle::Plain, None);
3300 let _ = merge_source_entries(&scalar, 0, 200, &mut Vec::new());
3301 }
3302
3303 #[test]
3304 #[should_panic(expected = "the alias check refused a non-mapping merge member")]
3305 fn merge_source_entries_panics_on_a_member_the_check_refused() {
3306 let member = Raw::Scalar("1".to_string(), TScalarStyle::Plain, None);
3307 let _ = merge_source_entries(&Raw::Sequence(vec![member]), 0, 200, &mut Vec::new());
3308 }
3309
3310 #[test]
3311 fn builder_document_end_with_an_empty_stack_pushes_a_null_scalar() {
3312 let mut b = Builder::new(&Resolved::DEFAULT);
3313 b.on_event_impl(Event::DocumentEnd, test_marker());
3314 assert_eq!(b.docs.len(), 1);
3315 assert!(matches!(&b.docs[0], Raw::Scalar(s, TScalarStyle::Plain, None) if s.is_empty()));
3316 }
3317
3318 #[test]
3319 #[should_panic(expected = "a single document's stack never nests more than one root")]
3320 fn builder_document_end_with_more_than_one_stack_entry_panics() {
3321 let mut b = Builder::new(&Resolved::DEFAULT);
3322 b.doc_stack
3323 .push((Raw::Scalar(String::new(), TScalarStyle::Plain, None), 0));
3324 b.doc_stack
3325 .push((Raw::Scalar(String::new(), TScalarStyle::Plain, None), 0));
3326 b.on_event_impl(Event::DocumentEnd, test_marker());
3327 }
3328
3329 #[test]
3330 #[should_panic(expected = "a Scalar is never a container on doc_stack")]
3331 fn builder_insert_onto_a_scalar_container_panics() {
3332 let mut b = Builder::new(&Resolved::DEFAULT);
3333 b.doc_stack
3334 .push((Raw::Scalar("x".to_string(), TScalarStyle::Plain, None), 0));
3335 b.insert(
3336 Raw::Scalar("y".to_string(), TScalarStyle::Plain, None),
3337 0,
3338 test_marker(),
3339 );
3340 }
3341
3342 #[test]
3343 fn builder_alias_to_an_anchor_with_no_entry_is_a_document_error_not_a_panic() {
3344 let mut b = Builder::new(&Resolved::DEFAULT);
3348 b.on_event_impl(Event::Alias(999), test_marker());
3349 let e = b.self_reference.as_ref().expect("recorded");
3350 assert_eq!(e.code.as_deref(), Some("document.limit.alias-expansion"));
3351 assert_eq!(e.path, "$");
3352 b.on_event_impl(Event::Alias(999), test_marker());
3354 }
3355
3356 #[test]
3357 fn an_explicit_int_tag_on_non_integer_text_is_a_syntax_error_not_a_panic() {
3358 for src in [
3359 "a: !!int x\n",
3360 "a: !!int -\n",
3361 "a: !!int 0xZZ\n",
3362 "a: !!int ''\n",
3363 ] {
3364 let e = read_yaml(src).unwrap_err();
3365 assert!(
3366 matches!(&e, OmnistError::Parse(p) if p.code == "parse.codec-syntax"),
3367 "{src:?}: {e:?}"
3368 );
3369 }
3370 assert_eq!(
3371 read_yaml("a: !!int 0x1f\n").unwrap().to_raw(),
3372 crate::document::RawNode::Edges(vec![(
3373 "a".to_string(),
3374 crate::document::RawNode::Leaf(Scalar::Int(31.into()))
3375 )])
3376 );
3377 }
3378
3379 #[test]
3380 fn self_referential_anchors_are_rejected_not_panicked() {
3381 for src in [
3383 "a: &A\n b: *A\n",
3384 "a: &A\n <<: *A\n x: 1\n",
3385 "a: &A [*A]\n",
3386 "a: &A\n b: &B\n c: *A\n",
3387 ] {
3388 let e = read_yaml(src).unwrap_err();
3389 assert!(
3390 matches!(&e, OmnistError::Document(d)
3391 if d.code.as_deref() == Some("document.limit.alias-expansion") && d.path == "$"),
3392 "{src:?}: {e:?}"
3393 );
3394 }
3395 assert!(read_yaml("a: &A\n b: 1\nc: *A\n").is_ok());
3397 }
3398
3399 #[test]
3408 fn charge_after_already_tripped_is_a_pure_no_op() {
3409 let mut b = Builder::new(&Resolved::DEFAULT);
3410 assert!(!b.charge(MAX_MATERIALIZED_NODES + 1, test_marker()));
3411 let first_error = format!("{:?}", b.error);
3412 let count_after_first_trip = b.node_count;
3413 assert!(!b.charge(1, test_marker()));
3414 assert_eq!(
3415 format!("{:?}", b.error),
3416 first_error,
3417 "error must not change"
3418 );
3419 assert_eq!(
3420 b.node_count, count_after_first_trip,
3421 "node_count must not change once already tripped"
3422 );
3423 }
3424
3425 #[test]
3431 fn sequence_start_can_itself_trip_the_node_count_guard() {
3432 let mut b = Builder::new(&Resolved::DEFAULT);
3433 b.node_count = MAX_MATERIALIZED_NODES;
3434 b.on_event_impl(Event::SequenceStart(0, None), test_marker());
3435 assert!(
3436 matches!(&b.error, Some(e) if e.message.contains("materializes more than")),
3437 "got {:?}",
3438 b.error
3439 );
3440 assert!(b.doc_stack.is_empty());
3442 }
3443
3444 #[test]
3446 fn mapping_start_can_itself_trip_the_node_count_guard() {
3447 let mut b = Builder::new(&Resolved::DEFAULT);
3448 b.node_count = MAX_MATERIALIZED_NODES;
3449 b.on_event_impl(Event::MappingStart(0, None), test_marker());
3450 assert!(
3451 matches!(&b.error, Some(e) if e.message.contains("materializes more than")),
3452 "got {:?}",
3453 b.error
3454 );
3455 assert!(b.doc_stack.is_empty());
3456 assert!(b.key_stack.is_empty());
3457 }
3458
3459 #[test]
3460 fn n189_the_cap_counts_containers_not_keys_or_scalar_values() {
3461 let mut b = Builder::new(&Resolved::DEFAULT);
3462 let mark = test_marker();
3463 b.on_event_impl(Event::MappingStart(0, None), mark);
3464 for i in 0..10 {
3465 b.on_event_impl(
3466 Event::Scalar(format!("k{i}"), TScalarStyle::Plain, 0, None),
3467 mark,
3468 );
3469 b.on_event_impl(
3470 Event::Scalar(format!("v{i}"), TScalarStyle::Plain, 0, None),
3471 mark,
3472 );
3473 }
3474 assert_eq!(b.node_count, 1, "one mapping, twenty scalars: one node");
3475 }
3476
3477 #[test]
3478 fn n189_the_node_cap_boundary_is_exact() {
3479 let mark = test_marker();
3480 let mut b = Builder::new(&Resolved::DEFAULT);
3481 b.node_count = MAX_MATERIALIZED_NODES - 1;
3482 b.on_event_impl(Event::MappingStart(0, None), mark);
3483 assert!(b.error.is_none(), "exactly the cap is accepted");
3484 assert_eq!(b.node_count, MAX_MATERIALIZED_NODES);
3485 b.on_event_impl(Event::SequenceStart(0, None), mark);
3486 assert!(
3487 matches!(&b.error, Some(e) if e.code.as_deref() == Some("document.limit.nodes")),
3488 "one past the cap is refused: {:?}",
3489 b.error
3490 );
3491 }
3492
3493 #[test]
3494 fn n189_the_default_node_cap_is_the_spec_reference_default() {
3495 assert_eq!(MAX_MATERIALIZED_NODES, 1_000_000);
3496 }
3497
3498 #[test]
3499 fn n189_an_alias_is_charged_for_the_containers_it_clones() {
3500 let mut b = Builder::new(&Resolved::DEFAULT);
3501 let mark = test_marker();
3502 b.on_event_impl(Event::SequenceStart(1, None), mark);
3504 b.on_event_impl(
3505 Event::Scalar("x".into(), TScalarStyle::Plain, 0, None),
3506 mark,
3507 );
3508 b.on_event_impl(Event::MappingStart(0, None), mark);
3509 b.on_event_impl(
3510 Event::Scalar("a".into(), TScalarStyle::Plain, 0, None),
3511 mark,
3512 );
3513 b.on_event_impl(
3514 Event::Scalar("b".into(), TScalarStyle::Plain, 0, None),
3515 mark,
3516 );
3517 b.on_event_impl(Event::MappingEnd, mark);
3518 b.on_event_impl(Event::SequenceEnd, mark);
3519 assert_eq!(b.node_count, 2);
3520 }
3521
3522 #[test]
3525 fn round_trips_an_integral_float_with_trailing_dot_zero() {
3526 let doc = doc_of(obj(vec![("f", Value::Float(2.0))]));
3527 let text = write_yaml(&doc, false, None).unwrap();
3528 assert!(text.contains("f: 2.0"), "text was:\n{text}");
3529 let back = read_yaml(&text).unwrap();
3530 assert!(doc.eq_doc(&back));
3531 }
3532
3533 #[test]
3534 fn quoted_string_escapes_every_special_character_in_one_pass() {
3535 let s = "-a\tb\\c\"d\u{01}e";
3540 let doc = doc_of(obj(vec![("s", Value::Str(s.to_string()))]));
3541 let text = write_yaml(&doc, false, None).unwrap();
3542 let back = read_yaml(&text).unwrap();
3543 assert!(doc.eq_doc(&back), "text was:\n{text}");
3544 }
3545
3546 #[test]
3547 fn write_seq_child_on_a_bare_non_empty_array_item_writes_it_on_the_next_line() {
3548 let mut out = String::new();
3555 write_seq_child(
3556 &Value::Array(vec![Value::Int((1).into()), Value::Int((2).into())]),
3557 0,
3558 &mut out,
3559 );
3560 assert_eq!(out, "\n - 1\n - 2\n");
3561 }
3562
3563 #[test]
3564 fn test_yaml_merge_key_deduplication_order() {
3565 let src = r#"
3566base1: &b1
3567 a: 1
3568 b: 2
3569 dup: "from_b1"
3570base2: &b2
3571 b: 20
3572 c: 3
3573 dup: "from_b2"
3574child:
3575 <<: [*b1, *b2]
3576 own: 0
3577 a: 100
3578"#;
3579 let doc = read_yaml(src).unwrap();
3580 let root = doc.root();
3581 let child = root.get_one("child").unwrap();
3582 let labels = child.labels();
3583 assert_eq!(labels, vec!["a", "b", "dup", "c", "own"]);
3588 assert_eq!(
3589 *child.get_one("a").unwrap().value().unwrap(),
3590 Scalar::Int((100).into())
3591 );
3592 assert_eq!(
3593 *child.get_one("b").unwrap().value().unwrap(),
3594 Scalar::Int((2).into())
3595 );
3596 assert_eq!(
3597 *child.get_one("dup").unwrap().value().unwrap(),
3598 Scalar::Str("from_b1".into())
3599 );
3600 assert_eq!(
3601 *child.get_one("c").unwrap().value().unwrap(),
3602 Scalar::Int((3).into())
3603 );
3604 }
3605
3606 fn interleaved_doc() -> Doc {
3613 Doc::from_raw(crate::document::RawNode::Edges(vec![
3614 (
3615 "m".to_string(),
3616 crate::document::RawNode::Leaf(Scalar::Str("A".to_string())),
3617 ),
3618 (
3619 "x".to_string(),
3620 crate::document::RawNode::Leaf(Scalar::Str("X".to_string())),
3621 ),
3622 (
3623 "m".to_string(),
3624 crate::document::RawNode::Leaf(Scalar::Str("B".to_string())),
3625 ),
3626 ]))
3627 .unwrap()
3628 }
3629
3630 fn contiguous_repeat_doc() -> Doc {
3631 Doc::from_raw(crate::document::RawNode::Edges(vec![
3632 (
3633 "m".to_string(),
3634 crate::document::RawNode::Leaf(Scalar::Str("A".to_string())),
3635 ),
3636 (
3637 "m".to_string(),
3638 crate::document::RawNode::Leaf(Scalar::Str("B".to_string())),
3639 ),
3640 (
3641 "x".to_string(),
3642 crate::document::RawNode::Leaf(Scalar::Str("X".to_string())),
3643 ),
3644 ]))
3645 .unwrap()
3646 }
3647
3648 #[test]
3649 fn reports_interleaving_lost_on_write() {
3650 let doc = interleaved_doc();
3651 let mut report = crate::report::WriteReport::new();
3652 write_yaml(&doc, false, Some(&mut report)).unwrap();
3653 let adjustments = report.adjustments();
3654 assert_eq!(adjustments.len(), 1);
3655 assert_eq!(adjustments[0].path, "$");
3656 assert_eq!(adjustments[0].code, "format.interleaving-lost");
3657 assert_eq!(adjustments[0].severity, crate::report::Severity::Warning);
3658 }
3659
3660 #[test]
3661 fn check_yaml_reports_interleaving_lost() {
3662 let rep = check_yaml(&interleaved_doc());
3663 assert_eq!(rep.adjustments().len(), 1);
3664 assert_eq!(rep.adjustments()[0].code, "format.interleaving-lost");
3665 }
3666
3667 #[test]
3668 fn contiguous_repeated_label_does_not_report_interleaving_lost() {
3669 let doc = contiguous_repeat_doc();
3670 let mut report = crate::report::WriteReport::new();
3671 write_yaml(&doc, false, Some(&mut report)).unwrap();
3672 assert!(report.is_empty());
3673 assert!(check_yaml(&doc).is_empty());
3674 }
3675}
3676
3677#[cfg(test)]
3678mod v035_tests {
3679 use super::*;
3680 use crate::document::{Doc, Scalar, Value};
3681
3682 fn one(label: &str, v: Value) -> Doc {
3683 let mut m = IndexMap::new();
3684 m.insert(label.to_string(), v);
3685 Doc::of(&Value::Object(m)).unwrap()
3686 }
3687
3688 fn written(v: Value) -> String {
3689 write_yaml(&one("a", v), false, None).unwrap()
3690 }
3691
3692 #[test]
3693 fn characters_yaml_does_not_print_are_escaped_in_lowercase() {
3694 for (c, want) in [
3695 ('\u{0}', "\"x\\x00y\""),
3696 ('\u{1}', "\"x\\x01y\""),
3697 ('\u{8}', "\"x\\x08y\""),
3698 ('\u{b}', "\"x\\x0by\""),
3699 ('\u{1f}', "\"x\\x1fy\""),
3700 ('\u{7f}', "\"x\\x7fy\""),
3701 ('\u{80}', "\"x\\x80y\""),
3702 ('\u{9f}', "\"x\\x9fy\""),
3703 ('\u{85}', "\"x\\Ny\""),
3704 ('\u{fffe}', "\"x\\ufffey\""),
3705 ('\u{ffff}', "\"x\\uffffy\""),
3706 ] {
3707 assert_eq!(
3708 written(Value::Str(format!("x{c}y"))),
3709 format!("a: {want}"),
3710 "{c:?}"
3711 );
3712 }
3713 for c in ['\u{a0}', '\u{fffd}', 'é'] {
3715 assert_eq!(written(Value::Str(format!("x{c}y"))), format!("a: x{c}y"));
3716 }
3717 }
3718
3719 #[test]
3720 fn a_time_is_written_double_quoted_with_its_seconds_and_reported() {
3721 for (t, want) in [
3722 ("12:00:00", "a: \"12:00:00\""),
3723 ("12:00", "a: \"12:00:00\""),
3724 ("12:00:00.5", "a: \"12:00:00.5\""),
3725 ("12:00:00.123456+01:00", "a: \"12:00:00.123456+01:00\""),
3726 ] {
3727 let doc = one("a", Value::Time(t.to_string()));
3728 let mut rep = WriteReport::new();
3729 assert_eq!(write_yaml(&doc, false, Some(&mut rep)).unwrap(), want);
3730 assert_eq!(rep.adjustments().len(), 1);
3731 assert_eq!(rep.adjustments()[0].path, "$.a");
3732 assert_eq!(rep.adjustments()[0].code, "format.temporal-stringified");
3733 assert_eq!(check_yaml(&doc).adjustments().len(), 1);
3734 }
3735 assert_eq!(
3736 written(Value::Datetime("2024-01-15 12:00".to_string())),
3737 "a: 2024-01-15T12:00:00"
3738 );
3739 }
3740
3741 #[test]
3742 fn a_sexagesimal_float_reads_as_a_number_and_the_look_alikes_stay_strings() {
3743 for (text, want) in [
3744 ("1:30.5", 90.5),
3745 ("-1:30.5", -90.5),
3746 ("+1:30.5", 90.5),
3747 ("1:30:15.25", 5415.25),
3748 ("1_0:30.5", 630.5),
3749 ("190:20:30.15", 685230.15),
3750 ("1:30.", 90.0),
3751 ] {
3752 let doc = read_yaml(&format!("a: {text}\n")).unwrap();
3753 assert_eq!(
3754 *doc.root().get_one("a").unwrap().value().unwrap(),
3755 Scalar::Float(want),
3756 "{text}"
3757 );
3758 }
3759 for text in [
3760 "0o17", "08", "09", "0:5", "1e3", "1.0e3", "1:60.5", "1:30.5x",
3761 ] {
3762 let doc = read_yaml(&format!("a: {text}\n")).unwrap();
3763 assert_eq!(
3764 *doc.root().get_one("a").unwrap().value().unwrap(),
3765 Scalar::Str(text.to_string()),
3766 );
3767 }
3768 }
3769
3770 #[test]
3771 fn a_string_that_reads_as_a_sexagesimal_float_is_written_quoted() {
3772 assert_eq!(written(Value::Str("1:30.5".to_string())), "a: \"1:30.5\"");
3773 }
3774
3775 #[test]
3776 fn a_fraction_past_six_digits_fails_the_read_and_is_quoted_on_write() {
3777 let err = read_yaml("t: 2024-01-15T12:00:00.1234567+01:00\n")
3778 .unwrap_err()
3779 .to_string();
3780 assert!(err.contains("fractional second"), "{err}");
3781 assert!(read_yaml("t: 2024-01-15T12:00:00.1234560\n").is_ok());
3783 let doc = one(
3784 "t",
3785 Value::Datetime("2024-01-15T12:00:00.1234567+01:00".to_string()),
3786 );
3787 let mut report = WriteReport::new();
3788 let text = write_yaml(&doc, false, Some(&mut report)).unwrap();
3789 assert_eq!(text, "t: \"2024-01-15T12:00:00.1234567+01:00\"");
3790 assert_eq!(report.adjustments()[0].code, "format.temporal-stringified");
3791 assert_eq!(check_yaml(&doc).adjustments().len(), 1);
3792 }
3793}