1use std::cmp::Ordering;
2use std::collections::{BTreeMap, HashSet};
3use std::fmt;
4
5use crate::{CollectionStyle, NodeId, SemanticKind, YamlDoc, YamlScalarStyle};
6
7const NULL_TAG: &str = "tag:yaml.org,2002:null";
8const BOOL_TAG: &str = "tag:yaml.org,2002:bool";
9const INT_TAG: &str = "tag:yaml.org,2002:int";
10const FLOAT_TAG: &str = "tag:yaml.org,2002:float";
11const STR_TAG: &str = "tag:yaml.org,2002:str";
12const SEQ_TAG: &str = "tag:yaml.org,2002:seq";
13const MAP_TAG: &str = "tag:yaml.org,2002:map";
14
15#[derive(Debug, Clone)]
20pub struct YamlNumber {
21 negative: bool,
22 digits: String,
23 exponent: i64,
24 integer_syntax: bool,
25}
26
27impl YamlNumber {
28 #[must_use]
30 pub const fn has_integer_syntax(&self) -> bool {
31 self.integer_syntax
32 }
33
34 #[must_use]
36 pub fn as_i128(&self) -> Option<i128> {
37 if !self.integer_syntax || self.exponent < 0 {
38 return None;
39 }
40 let mut text = self.digits.clone();
41 text.extend(std::iter::repeat_n(
42 '0',
43 usize::try_from(self.exponent).ok()?,
44 ));
45 if self.negative {
46 text.insert(0, '-');
47 }
48 text.parse().ok()
49 }
50
51 #[must_use]
53 pub fn as_u128(&self) -> Option<u128> {
54 if self.negative || !self.integer_syntax || self.exponent < 0 {
55 return None;
56 }
57 let mut text = self.digits.clone();
58 text.extend(std::iter::repeat_n(
59 '0',
60 usize::try_from(self.exponent).ok()?,
61 ));
62 text.parse().ok()
63 }
64
65 #[must_use]
67 pub fn as_f64(&self) -> Option<f64> {
68 let sign = if self.negative { "-" } else { "" };
69 format!("{sign}{}e{}", self.digits, self.exponent)
70 .parse()
71 .ok()
72 }
73}
74
75impl PartialEq for YamlNumber {
76 fn eq(&self, other: &Self) -> bool {
77 self.negative == other.negative
78 && self.digits == other.digits
79 && self.exponent == other.exponent
80 }
81}
82
83impl Eq for YamlNumber {}
84
85impl PartialOrd for YamlNumber {
86 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
87 Some(self.cmp(other))
88 }
89}
90
91impl Ord for YamlNumber {
92 fn cmp(&self, other: &Self) -> Ordering {
93 if self.digits == "0" && other.digits == "0" {
94 return Ordering::Equal;
95 }
96 if self.negative != other.negative {
97 return if self.negative {
98 Ordering::Less
99 } else {
100 Ordering::Greater
101 };
102 }
103 let magnitude = compare_number_magnitude(self, other);
104 if self.negative {
105 magnitude.reverse()
106 } else {
107 magnitude
108 }
109 }
110}
111
112impl fmt::Display for YamlNumber {
113 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
114 if self.negative {
115 formatter.write_str("-")?;
116 }
117 formatter.write_str(&self.digits)?;
118 if self.integer_syntax {
119 for _ in 0..self.exponent {
120 formatter.write_str("0")?;
121 }
122 Ok(())
123 } else {
124 write!(formatter, "e{}", self.exponent)
125 }
126 }
127}
128
129fn compare_number_magnitude(left: &YamlNumber, right: &YamlNumber) -> Ordering {
130 let left_places = left.digits.len() as i128 + i128::from(left.exponent);
131 let right_places = right.digits.len() as i128 + i128::from(right.exponent);
132 match left_places.cmp(&right_places) {
133 Ordering::Equal => {}
134 ordering => return ordering,
135 }
136 let compared = left.digits.len().max(right.digits.len());
137 let left_bytes = left.digits.as_bytes();
138 let right_bytes = right.digits.as_bytes();
139 for index in 0..compared {
140 let left = left_bytes.get(index).copied().unwrap_or(b'0');
141 let right = right_bytes.get(index).copied().unwrap_or(b'0');
142 match left.cmp(&right) {
143 Ordering::Equal => {}
144 ordering => return ordering,
145 }
146 }
147 Ordering::Equal
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum NonFiniteFloat {
153 PositiveInfinity,
155 NegativeInfinity,
157 NaN,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum ResolvedScalar {
164 Null,
166 Bool(bool),
168 Number(YamlNumber),
170 NonFinite(NonFiniteFloat),
172 String,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct ScalarResolveError {
179 message: String,
180}
181
182impl ScalarResolveError {
183 fn new(message: impl Into<String>) -> Self {
184 Self {
185 message: message.into(),
186 }
187 }
188}
189
190impl fmt::Display for ScalarResolveError {
191 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
192 formatter.write_str(&self.message)
193 }
194}
195
196impl std::error::Error for ScalarResolveError {}
197
198pub fn resolve_scalar(
205 value: &str,
206 style: YamlScalarStyle,
207 tag: Option<&str>,
208) -> Result<ResolvedScalar, ScalarResolveError> {
209 if tag == Some(STR_TAG) {
210 return Ok(ResolvedScalar::String);
211 }
212 if let Some(tag) = tag
213 && !matches!(tag, NULL_TAG | BOOL_TAG | INT_TAG | FLOAT_TAG)
214 {
215 return Err(ScalarResolveError::new(format!(
216 "unsupported scalar tag `{tag}`"
217 )));
218 }
219 if style != YamlScalarStyle::Plain && tag.is_none() {
220 return Ok(ResolvedScalar::String);
221 }
222 if tag == Some(NULL_TAG)
223 || tag.is_none() && matches!(value, "" | "~" | "null" | "Null" | "NULL")
224 {
225 return if matches!(value, "" | "~" | "null" | "Null" | "NULL") {
226 Ok(ResolvedScalar::Null)
227 } else {
228 Err(ScalarResolveError::new("invalid null scalar"))
229 };
230 }
231 if tag == Some(BOOL_TAG) || tag.is_none() {
232 match value {
233 "true" | "True" | "TRUE" => return Ok(ResolvedScalar::Bool(true)),
234 "false" | "False" | "FALSE" => return Ok(ResolvedScalar::Bool(false)),
235 _ if tag == Some(BOOL_TAG) => {
236 return Err(ScalarResolveError::new("invalid boolean scalar"));
237 }
238 _ => {}
239 }
240 }
241 if tag == Some(INT_TAG) || tag.is_none() {
242 if let Some(number) = parse_integer(value) {
243 return Ok(ResolvedScalar::Number(number));
244 }
245 if tag == Some(INT_TAG) {
246 return Err(ScalarResolveError::new("invalid integer scalar"));
247 }
248 }
249 if tag == Some(FLOAT_TAG) || tag.is_none() && looks_like_float(value) {
250 if let Some(special) = parse_non_finite(value) {
251 return Ok(ResolvedScalar::NonFinite(special));
252 }
253 if let Some(number) = parse_decimal(value, false) {
254 return Ok(ResolvedScalar::Number(number));
255 }
256 if tag == Some(FLOAT_TAG) {
257 return Err(ScalarResolveError::new("invalid float scalar"));
258 }
259 }
260 Ok(ResolvedScalar::String)
261}
262
263fn parse_integer(value: &str) -> Option<YamlNumber> {
264 let normalized = value.replace('_', "");
265 let (negative, unsigned) = strip_sign(&normalized);
266 let (radix, digits) = if let Some(rest) = unsigned.strip_prefix("0x") {
267 (16, rest)
268 } else if let Some(rest) = unsigned.strip_prefix("0o") {
269 (8, rest)
270 } else if let Some(rest) = unsigned.strip_prefix("0b") {
271 (2, rest)
272 } else {
273 (10, unsigned)
274 };
275 if digits.is_empty()
276 || !digits.chars().all(|character| character.is_digit(radix))
277 || radix == 10 && digits.len() > 1 && digits.starts_with('0')
278 {
279 return None;
280 }
281 let decimal = if radix == 10 {
282 digits.to_owned()
283 } else {
284 radix_to_decimal(digits, radix)?
285 };
286 normalize_number(negative, &decimal, 0, true)
287}
288
289fn radix_to_decimal(digits: &str, radix: u32) -> Option<String> {
290 let mut decimal = vec![0_u8];
291 for character in digits.chars() {
292 let digit = character.to_digit(radix)?;
293 let mut carry = digit;
294 for value in decimal.iter_mut().rev() {
295 let next = u32::from(*value) * radix + carry;
296 *value = u8::try_from(next % 10).ok()?;
297 carry = next / 10;
298 }
299 while carry > 0 {
300 decimal.insert(0, u8::try_from(carry % 10).ok()?);
301 carry /= 10;
302 }
303 }
304 Some(
305 decimal
306 .into_iter()
307 .map(|digit| char::from(b'0' + digit))
308 .collect(),
309 )
310}
311
312fn parse_decimal(value: &str, integer_syntax: bool) -> Option<YamlNumber> {
313 let normalized = value.replace('_', "");
314 let (negative, unsigned) = strip_sign(&normalized);
315 let (mantissa, exponent) = match unsigned.find(['e', 'E']) {
316 Some(index) => {
317 let exponent = unsigned[index + 1..].parse::<i64>().ok()?;
318 (&unsigned[..index], exponent)
319 }
320 None => (unsigned, 0),
321 };
322 let (whole, fraction) = match mantissa.split_once('.') {
323 Some(parts) => parts,
324 None => (mantissa, ""),
325 };
326 if whole.is_empty() && fraction.is_empty()
327 || !whole.chars().all(|character| character.is_ascii_digit())
328 || !fraction.chars().all(|character| character.is_ascii_digit())
329 {
330 return None;
331 }
332 let digits = format!("{whole}{fraction}");
333 let exponent = exponent.checked_sub(i64::try_from(fraction.len()).ok()?)?;
334 normalize_number(negative, &digits, exponent, integer_syntax)
335}
336
337fn normalize_number(
338 mut negative: bool,
339 digits: &str,
340 mut exponent: i64,
341 integer_syntax: bool,
342) -> Option<YamlNumber> {
343 let mut digits = digits.trim_start_matches('0').to_owned();
344 if digits.is_empty() {
345 negative = false;
346 digits.push('0');
347 exponent = 0;
348 } else {
349 while digits.ends_with('0') {
350 digits.pop();
351 exponent = exponent.checked_add(1)?;
352 }
353 }
354 Some(YamlNumber {
355 negative,
356 digits,
357 exponent,
358 integer_syntax,
359 })
360}
361
362fn strip_sign(value: &str) -> (bool, &str) {
363 if let Some(rest) = value.strip_prefix('-') {
364 (true, rest)
365 } else {
366 (false, value.strip_prefix('+').unwrap_or(value))
367 }
368}
369
370fn looks_like_float(value: &str) -> bool {
371 value.contains(['.', 'e', 'E'])
372}
373
374fn parse_non_finite(value: &str) -> Option<NonFiniteFloat> {
375 match value {
376 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => {
377 Some(NonFiniteFloat::PositiveInfinity)
378 }
379 "-.inf" | "-.Inf" | "-.INF" => Some(NonFiniteFloat::NegativeInfinity),
380 ".nan" | ".NaN" | ".NAN" => Some(NonFiniteFloat::NaN),
381 _ => None,
382 }
383}
384
385#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct SemanticValueError {
388 message: String,
389}
390
391impl SemanticValueError {
392 pub(crate) fn new(message: impl Into<String>) -> Self {
393 Self {
394 message: message.into(),
395 }
396 }
397}
398
399impl fmt::Display for SemanticValueError {
400 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
401 formatter.write_str(&self.message)
402 }
403}
404
405impl std::error::Error for SemanticValueError {}
406
407pub fn semantically_equal(
414 left_doc: &YamlDoc,
415 left: NodeId,
416 right_doc: &YamlDoc,
417 right: NodeId,
418) -> Result<bool, SemanticValueError> {
419 enum CompareAction {
420 Compare(NodeId, NodeId, usize),
421 Exit(NodeId, NodeId),
422 }
423
424 let mut active = HashSet::new();
425 let mut pending = vec![CompareAction::Compare(left, right, 0)];
426 while let Some(action) = pending.pop() {
427 let CompareAction::Compare(left, right, depth) = action else {
428 let CompareAction::Exit(left, right) = action else {
429 unreachable!();
430 };
431 active.remove(&(left, right));
432 continue;
433 };
434 if depth > 1024 {
435 return Err(SemanticValueError::new(
436 "semantic comparison recursion limit exceeded",
437 ));
438 }
439 let left = resolve_alias_chain(left_doc, left)?;
440 let right = resolve_alias_chain(right_doc, right)?;
441 if !active.insert((left, right)) {
442 return Err(SemanticValueError::new(
443 "cyclic YAML values are not JSON-compatible",
444 ));
445 }
446 match (left_doc.semantic_kind(left), right_doc.semantic_kind(right)) {
447 (
448 Some(SemanticKind::Scalar { style: left_style }),
449 Some(SemanticKind::Scalar { style: right_style }),
450 ) => {
451 let left_scalar = resolved_scalar_at(left_doc, left, left_style)?;
452 let right_scalar = resolved_scalar_at(right_doc, right, right_style)?;
453 if matches!(left_scalar, ResolvedScalar::NonFinite(_))
454 || matches!(right_scalar, ResolvedScalar::NonFinite(_))
455 {
456 return Err(SemanticValueError::new(
457 "infinities and NaN are not JSON-compatible",
458 ));
459 }
460 active.remove(&(left, right));
461 if left_scalar != right_scalar {
462 return Ok(false);
463 }
464 }
465 (
466 Some(SemanticKind::Sequence { style: left_style }),
467 Some(SemanticKind::Sequence { style: right_style }),
468 ) => {
469 validate_collection_tag(left_doc, left, left_style, false)?;
470 validate_collection_tag(right_doc, right, right_style, false)?;
471 let left_items = left_doc.sequence_items(left).collect::<Vec<_>>();
472 let right_items = right_doc.sequence_items(right).collect::<Vec<_>>();
473 if left_items.len() != right_items.len() {
474 return Ok(false);
475 }
476 pending.push(CompareAction::Exit(left, right));
477 for (left_item, right_item) in left_items.into_iter().zip(right_items).rev() {
478 pending.push(CompareAction::Compare(left_item, right_item, depth + 1));
479 }
480 }
481 (
482 Some(SemanticKind::Mapping { style: left_style }),
483 Some(SemanticKind::Mapping { style: right_style }),
484 ) => {
485 validate_collection_tag(left_doc, left, left_style, true)?;
486 validate_collection_tag(right_doc, right, right_style, true)?;
487 let left_entries = json_mapping(left_doc, left)?;
488 let right_entries = json_mapping(right_doc, right)?;
489 if left_entries.len() != right_entries.len() {
490 return Ok(false);
491 }
492 let mut children = Vec::with_capacity(left_entries.len());
493 for (key, left_value) in left_entries {
494 let Some(right_value) = right_entries.get(&key).copied() else {
495 return Ok(false);
496 };
497 children.push((left_value, right_value));
498 }
499 pending.push(CompareAction::Exit(left, right));
500 for (left_value, right_value) in children.into_iter().rev() {
501 pending.push(CompareAction::Compare(left_value, right_value, depth + 1));
502 }
503 }
504 (Some(SemanticKind::Alias), _) | (_, Some(SemanticKind::Alias)) => {
505 unreachable!("aliases are resolved before comparison")
506 }
507 (Some(_), Some(_)) => return Ok(false),
508 _ => return Err(SemanticValueError::new("unknown semantic YAML node")),
509 }
510 }
511 Ok(true)
512}
513
514fn resolved_scalar_at(
515 doc: &YamlDoc,
516 node: NodeId,
517 style: YamlScalarStyle,
518) -> Result<ResolvedScalar, SemanticValueError> {
519 let value = doc
520 .scalar_value(node)
521 .map_err(|error| SemanticValueError::new(error.to_string()))?;
522 let tag = doc
523 .resolved_tag(node)
524 .map_err(|error| SemanticValueError::new(error.to_string()))?;
525 resolve_scalar(&value, style, tag.as_deref())
526 .map_err(|error| SemanticValueError::new(error.to_string()))
527}
528
529fn resolve_alias_chain(doc: &YamlDoc, mut node: NodeId) -> Result<NodeId, SemanticValueError> {
530 let mut seen = HashSet::new();
531 while matches!(doc.semantic_kind(node), Some(SemanticKind::Alias)) {
532 if !seen.insert(node) {
533 return Err(SemanticValueError::new("cyclic alias chain"));
534 }
535 node = doc.resolve_alias(node).ok_or_else(|| {
536 SemanticValueError::new(format!(
537 "unresolved alias `*{}`",
538 doc.alias_name(node).unwrap_or_default()
539 ))
540 })?;
541 }
542 Ok(node)
543}
544
545fn json_mapping(
546 doc: &YamlDoc,
547 mapping: NodeId,
548) -> Result<BTreeMap<String, NodeId>, SemanticValueError> {
549 let mut entries = BTreeMap::new();
550 for (key, value) in doc.mapping_entries(mapping) {
551 let key = resolve_alias_chain(doc, key)?;
552 let Some(SemanticKind::Scalar { style }) = doc.semantic_kind(key) else {
553 return Err(SemanticValueError::new("mapping contains a non-string key"));
554 };
555 if resolved_scalar_at(doc, key, style)? != ResolvedScalar::String {
556 return Err(SemanticValueError::new("mapping contains a non-string key"));
557 }
558 let key = doc
559 .scalar_value(key)
560 .map_err(|error| SemanticValueError::new(error.to_string()))?
561 .into_owned();
562 if entries.insert(key.clone(), value).is_some() {
563 return Err(SemanticValueError::new(format!(
564 "mapping contains duplicate key `{key}`"
565 )));
566 }
567 }
568 Ok(entries)
569}
570
571fn validate_collection_tag(
572 doc: &YamlDoc,
573 node: NodeId,
574 _style: CollectionStyle,
575 mapping: bool,
576) -> Result<(), SemanticValueError> {
577 let tag = doc
578 .resolved_tag(node)
579 .map_err(|error| SemanticValueError::new(error.to_string()))?;
580 let expected = if mapping { MAP_TAG } else { SEQ_TAG };
581 if tag.as_deref().is_some_and(|tag| tag != expected) {
582 return Err(SemanticValueError::new(format!(
583 "custom-tagged collections are not JSON-compatible: `{}`",
584 tag.as_deref().unwrap_or_default()
585 )));
586 }
587 Ok(())
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593
594 fn nested_block_mapping(depth: usize, value: &str) -> String {
595 let mut yaml = String::new();
596 for level in 0..depth {
597 yaml.push_str(&" ".repeat(level));
598 yaml.push_str("key:\n");
599 }
600 yaml.push_str(&" ".repeat(depth));
601 yaml.push_str(value);
602 yaml.push('\n');
603 yaml
604 }
605
606 fn roots_are_equal(left: &YamlDoc, right: &YamlDoc) -> Result<bool, SemanticValueError> {
607 semantically_equal(
608 left,
609 left.document_root(0).unwrap().unwrap(),
610 right,
611 right.document_root(0).unwrap().unwrap(),
612 )
613 }
614
615 #[test]
616 fn exact_numbers_compare_across_yaml_spellings() {
617 let values = ["1", "1.0", "1e0", "0x1"];
618 let numbers = values
619 .into_iter()
620 .map(|value| {
621 let ResolvedScalar::Number(number) =
622 resolve_scalar(value, YamlScalarStyle::Plain, None).unwrap()
623 else {
624 panic!("expected number");
625 };
626 number
627 })
628 .collect::<Vec<_>>();
629 assert!(numbers.windows(2).all(|pair| pair[0] == pair[1]));
630 }
631
632 #[test]
633 fn exact_numbers_order_and_format_without_float_conversion() {
634 let number = |value| {
635 let ResolvedScalar::Number(number) =
636 resolve_scalar(value, YamlScalarStyle::Plain, None).unwrap()
637 else {
638 panic!("expected number");
639 };
640 number
641 };
642
643 assert!(number("-1e1000") < number("-9e999"));
644 assert!(number("9e999") < number("1e1000"));
645 assert_eq!(number("0x10").to_string(), "16");
646 assert_eq!(number("1.50").to_string(), "15e-1");
647 assert_eq!(number("1e1000").to_string(), "1e1000");
648 }
649
650 #[test]
651 fn semantic_equality_ignores_presentation_and_mapping_order() {
652 let left = YamlDoc::parse("a: 1\nb: ['x', true]\n").unwrap();
653 let right = YamlDoc::parse("{b: [x, TRUE], a: 1.0}\n").unwrap();
654 assert!(
655 semantically_equal(
656 &left,
657 left.document_root(0).unwrap().unwrap(),
658 &right,
659 right.document_root(0).unwrap().unwrap()
660 )
661 .unwrap()
662 );
663 }
664
665 #[test]
666 fn semantic_equality_rejects_non_string_keys() {
667 let left = YamlDoc::parse("1: value\n").unwrap();
668 let right = YamlDoc::parse("'1': value\n").unwrap();
669 assert!(
670 semantically_equal(
671 &left,
672 left.document_root(0).unwrap().unwrap(),
673 &right,
674 right.document_root(0).unwrap().unwrap()
675 )
676 .unwrap_err()
677 .to_string()
678 .contains("non-string")
679 );
680 }
681
682 #[test]
683 fn semantic_equality_handles_deep_equal_and_unequal_values_iteratively() {
684 std::thread::Builder::new()
685 .stack_size(32 * 1024 * 1024)
686 .spawn(|| {
687 let left = YamlDoc::parse(&nested_block_mapping(1024, "1")).unwrap();
688 let equal = YamlDoc::parse(&nested_block_mapping(1024, "1.0")).unwrap();
689 let unequal = YamlDoc::parse(&nested_block_mapping(1024, "2")).unwrap();
690 assert!(roots_are_equal(&left, &equal).unwrap());
691 assert!(!roots_are_equal(&left, &unequal).unwrap());
692
693 let too_deep = YamlDoc::parse(&nested_block_mapping(1025, "1")).unwrap();
694 assert!(
695 roots_are_equal(&too_deep, &too_deep)
696 .unwrap_err()
697 .to_string()
698 .contains("recursion limit")
699 );
700 })
701 .unwrap()
702 .join()
703 .unwrap();
704 }
705
706 #[test]
707 fn semantic_equality_still_rejects_cyclic_alias_values() {
708 let left = YamlDoc::parse("&root [*root]\n").unwrap();
709 let right = YamlDoc::parse("&root [*root]\n").unwrap();
710 assert!(
711 roots_are_equal(&left, &right)
712 .unwrap_err()
713 .to_string()
714 .contains("cyclic YAML values")
715 );
716 }
717}