1#[cfg(feature = "deserialize")]
2use crate::de::{Error, Location};
3#[cfg(feature = "deserialize")]
4use crate::tags::SfTag;
5#[cfg(feature = "deserialize")]
6use granit_parser::ScalarStyle;
7#[cfg(feature = "deserialize")]
8use std::str::FromStr;
9
10#[inline]
15pub(crate) fn yaml_trim(s: &str) -> &str {
16 s.trim_matches([' ', '\t', '\r', '\n'])
17}
18
19pub(crate) fn parse_yaml11_bool(s: &str) -> Result<bool, String> {
28 let t = yaml_trim(s);
29 if t.eq_ignore_ascii_case("true")
30 || t.eq_ignore_ascii_case("yes")
31 || t.eq_ignore_ascii_case("y")
32 || t.eq_ignore_ascii_case("on")
33 {
34 Ok(true)
35 } else if t.eq_ignore_ascii_case("false")
36 || t.eq_ignore_ascii_case("no")
37 || t.eq_ignore_ascii_case("n")
38 || t.eq_ignore_ascii_case("off")
39 {
40 Ok(false)
41 } else {
42 Err(format!("invalid YAML 1.1 bool: `{s}`"))
43 }
44}
45
46#[cfg(feature = "deserialize")]
47fn parse_digits_u128(digits: &str, radix: u32) -> Option<u128> {
48 let mut val: u128 = 0;
49 let mut saw = false;
50 let bytes = digits.as_bytes();
51 for (i, &b) in bytes.iter().enumerate() {
52 match b {
53 b'_' => {
54 let prev_ok = i > 0 && bytes[i - 1] != b'_';
55 let next_ok = i + 1 < bytes.len() && bytes[i + 1] != b'_';
56 if !prev_ok || !next_ok {
57 return None;
58 }
59 }
60 b'0'..=b'9' => {
61 let d = u32::from(b - b'0');
62 if d >= radix {
63 return None;
64 }
65 val = val.checked_mul(u128::from(radix))?;
66 val = val.checked_add(u128::from(d))?;
67 saw = true;
68 }
69 b'a'..=b'f' if radix > 10 => {
70 let d = 10 + u32::from(b - b'a');
71 if d >= radix {
72 return None;
73 }
74 val = val.checked_mul(u128::from(radix))?;
75 val = val.checked_add(u128::from(d))?;
76 saw = true;
77 }
78 b'A'..=b'F' if radix > 10 => {
79 let d = 10 + u32::from(b - b'A');
80 if d >= radix {
81 return None;
82 }
83 val = val.checked_mul(u128::from(radix))?;
84 val = val.checked_add(u128::from(d))?;
85 saw = true;
86 }
87 _ => return None,
88 }
89 }
90 if saw { Some(val) } else { None }
91}
92
93#[cfg(feature = "deserialize")]
94fn parse_decimal_unsigned_u128(digits: &str) -> Option<u128> {
95 let mut val: u128 = 0;
96 let mut saw = false;
97 let bytes = digits.as_bytes();
98 for (i, &b) in bytes.iter().enumerate() {
99 match b {
100 b'_' => {
101 let prev_ok = i > 0 && bytes[i - 1] != b'_';
102 let next_ok = i + 1 < bytes.len() && bytes[i + 1] != b'_';
103 if !prev_ok || !next_ok {
104 return None;
105 }
106 }
107 b'0'..=b'9' => {
108 let d = u128::from(b - b'0');
109 val = val.checked_mul(10)?;
110 val = val.checked_add(d)?;
111 saw = true;
112 }
113 _ => return None,
114 }
115 }
116 if saw { Some(val) } else { None }
117}
118
119#[cfg(feature = "deserialize")]
120fn parse_decimal_signed_i128(digits: &str, neg: bool) -> Option<i128> {
121 if neg {
122 let mut val: i128 = 0;
124 let mut saw = false;
125 let bytes = digits.as_bytes();
126 for (i, &b) in bytes.iter().enumerate() {
127 match b {
128 b'_' => {
129 let prev_ok = i > 0 && bytes[i - 1] != b'_';
130 let next_ok = i + 1 < bytes.len() && bytes[i + 1] != b'_';
131 if !prev_ok || !next_ok {
132 return None;
133 }
134 }
135 b'0'..=b'9' => {
136 let d = i128::from(b - b'0');
137 val = val.checked_mul(10)?;
138 val = val.checked_sub(d)?;
139 saw = true;
140 }
141 _ => return None,
142 }
143 }
144 if saw { Some(val) } else { None }
145 } else {
146 let mut val: i128 = 0;
147 let mut saw = false;
148 let bytes = digits.as_bytes();
149 for (i, &b) in bytes.iter().enumerate() {
150 match b {
151 b'_' => {
152 let prev_ok = i > 0 && bytes[i - 1] != b'_';
153 let next_ok = i + 1 < bytes.len() && bytes[i + 1] != b'_';
154 if !prev_ok || !next_ok {
155 return None;
156 }
157 }
158 b'0'..=b'9' => {
159 let d = i128::from(b - b'0');
160 val = val.checked_mul(10)?;
161 val = val.checked_add(d)?;
162 saw = true;
163 }
164 _ => return None,
165 }
166 }
167 if saw { Some(val) } else { None }
168 }
169}
170
171#[cfg(feature = "deserialize")]
172pub(crate) fn parse_int_signed<T>(
173 s: &str,
174 ty: &'static str,
175 location: Location,
176 legacy_octal: bool,
177) -> Result<T, Error>
178where
179 T: TryFrom<i128>,
180{
181 let invalid = || Error::InvalidScalar { ty, location };
182
183 let t = yaml_trim(s);
184 let (neg, rest) = match t.strip_prefix('+') {
185 Some(r) => (false, r),
186 None => match t.strip_prefix('-') {
187 Some(r) => (true, r),
188 None => (false, t),
189 },
190 };
191
192 let (radix, digits) = radix_and_digits(legacy_octal, rest);
193 if radix == 10 {
194 if digits.starts_with('0') && digits != "0" {
196 return Err(invalid());
197 }
198 let val_i128 = parse_decimal_signed_i128(digits, neg).ok_or_else(invalid)?;
199 return T::try_from(val_i128).map_err(|_| invalid());
200 }
201
202 let mag = parse_digits_u128(digits, radix).ok_or_else(invalid)?;
203 let val_i128: i128 = if neg {
204 if mag == (i128::MAX as u128) + 1 {
205 i128::MIN
206 } else {
207 let mag_i128: i128 = mag.try_into().map_err(|_| invalid())?;
208 mag_i128.checked_neg().ok_or_else(invalid)?
209 }
210 } else {
211 mag.try_into().map_err(|_| invalid())?
212 };
213 T::try_from(val_i128).map_err(|_| invalid())
214}
215
216#[cfg(feature = "deserialize")]
217pub(crate) fn parse_int_unsigned<T>(
218 s: &str,
219 ty: &'static str,
220 location: Location,
221 legacy_octal: bool,
222) -> Result<T, Error>
223where
224 T: TryFrom<u128>,
225{
226 let invalid = || Error::InvalidScalar { ty, location };
227
228 let t = yaml_trim(s);
229 if t.starts_with('-') {
230 return Err(invalid());
231 }
232 let rest = t.strip_prefix('+').unwrap_or(t);
233 let (radix, digits) = radix_and_digits(legacy_octal, rest);
234
235 if radix == 10 {
236 if digits.starts_with('0') && digits != "0" {
238 return Err(invalid());
239 }
240 let val_u128 = parse_decimal_unsigned_u128(digits).ok_or_else(invalid)?;
241 return T::try_from(val_u128).map_err(|_| invalid());
242 }
243
244 let mag = parse_digits_u128(digits, radix).ok_or_else(invalid)?;
245 T::try_from(mag).map_err(|_| invalid())
246}
247
248#[cfg(feature = "deserialize")]
249fn radix_and_digits(legacy_octal: bool, rest: &str) -> (u32, &str) {
250 let (radix, digits) =
251 if let Some(r) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
252 (16u32, normalize_prefixed_digits(legacy_octal, r))
253 } else if let Some(r) = rest.strip_prefix("0o").or_else(|| rest.strip_prefix("0O")) {
254 (8u32, normalize_prefixed_digits(legacy_octal, r))
255 } else if let Some(r) = rest.strip_prefix("0b").or_else(|| rest.strip_prefix("0B")) {
256 (2u32, normalize_prefixed_digits(legacy_octal, r))
257 } else if legacy_octal && rest.starts_with('0') {
258 if rest == "0" {
259 (8u32, "0")
261 } else {
262 (8u32, normalize_prefixed_digits(legacy_octal, &rest[1..]))
263 }
264 } else {
265 (10u32, rest)
266 };
267 (radix, digits)
268}
269
270#[cfg(feature = "deserialize")]
271fn normalize_prefixed_digits(legacy_octal: bool, digits: &str) -> &str {
272 if legacy_octal {
273 digits.strip_prefix('_').unwrap_or(digits)
274 } else {
275 digits
276 }
277}
278
279#[cfg(feature = "deserialize")]
280fn parse_yaml12_finite_float_fallback<T>(t: &str, location: Location) -> Result<T, Error>
281where
282 T: FromStr,
283 T: num_traits::Float,
284{
285 let value = t.parse::<T>().map_err(|_| Error::InvalidScalar {
286 ty: "floating point",
287 location,
288 })?;
289
290 if value.is_finite() {
291 Ok(value)
292 } else {
293 Err(Error::InvalidScalar {
294 ty: "floating point",
295 location,
296 })
297 }
298}
299
300#[cfg(all(feature = "deserialize", feature = "robotics"))]
301pub(crate) fn parse_yaml12_float<T>(
302 s: &str,
303 location: Location,
304 tag: SfTag,
305 angle_conversions: bool,
306) -> Result<T, Error>
307where
308 T: FromStr + crate::robotics::FromF64,
309 T: num_traits::Float,
310{
311 if angle_conversions {
312 return crate::robotics::parse_yaml12_float_angle_converting(s, location, tag);
313 }
314 let t = yaml_trim(s);
315 let lower = t.to_ascii_lowercase();
316 match lower.as_str() {
317 ".nan" | "+.nan" | "-.nan" => Ok(T::nan()),
318 ".inf" | "+.inf" => Ok(T::infinity()),
319 "-.inf" => Ok(T::neg_infinity()),
320 _ => parse_yaml12_finite_float_fallback(t, location),
321 }
322}
323
324#[cfg(all(feature = "deserialize", not(feature = "robotics")))]
325pub(crate) fn parse_yaml12_float<T>(
326 s: &str,
327 location: Location,
328 _tag: SfTag,
329 _angle_conversions: bool,
330) -> Result<T, Error>
331where
332 T: FromStr,
333 T: num_traits::Float,
334{
335 let t = yaml_trim(s);
336 let lower = t.to_ascii_lowercase();
337 match lower.as_str() {
338 ".nan" | "+.nan" | "-.nan" => Ok(T::nan()),
339 ".inf" | "+.inf" => Ok(T::infinity()),
340 "-.inf" => Ok(T::neg_infinity()),
341 _ => parse_yaml12_finite_float_fallback(t, location),
342 }
343}
344
345#[cfg(feature = "deserialize")]
346pub(crate) fn try_parse_float_incl_overflow(
361 s: &str,
362 location: Location,
363 tag: SfTag,
364 angle_conversions: bool,
365) -> Option<f64> {
366 if let Ok(v) = parse_yaml12_float::<f64>(s, location, tag, angle_conversions) {
367 return Some(v);
368 }
369
370 let t = yaml_trim(s);
371 let unsigned = t.strip_prefix(['+', '-']).unwrap_or(t);
372 if !unsigned.as_bytes().first().is_some_and(u8::is_ascii_digit) {
373 return None;
374 }
375
376 match t.parse::<f64>() {
377 Ok(v) if v.is_infinite() => Some(v),
378 _ => None,
379 }
380}
381
382#[cfg(feature = "deserialize")]
383pub(crate) fn maybe_not_string(s: &str, style: &ScalarStyle, strict_booleans: bool) -> bool {
385 let location = Location::UNKNOWN;
386 style == &ScalarStyle::Plain
387 && (parse_yaml12_float::<f64>(s, location, SfTag::None, false).is_ok()
388 || parse_int_signed::<i128>(s, "i128", location, false).is_ok()
389 || maybe_bool(s, strict_booleans)
390 || scalar_is_nullish(s, &ScalarStyle::Plain))
391}
392
393#[cfg(feature = "deserialize")]
395#[inline]
396fn maybe_bool(s: &str, strict: bool) -> bool {
397 if strict {
398 let s = yaml_trim(s);
399 s.eq_ignore_ascii_case("true") || s.eq_ignore_ascii_case("false")
400 } else {
401 parse_yaml11_bool(s).is_ok()
402 }
403}
404
405#[cfg(feature = "deserialize")]
418#[inline]
419pub(crate) fn scalar_is_nullish(value: &str, style: &ScalarStyle) -> bool {
420 if !matches!(style, ScalarStyle::Plain) {
421 return false;
422 }
423 value.is_empty() || value == "~" || value.eq_ignore_ascii_case("null")
424}
425
426#[cfg(feature = "deserialize")]
427#[inline]
428pub(crate) fn scalar_is_null(tag: &SfTag, value: &str, style: &ScalarStyle) -> bool {
431 *tag == SfTag::Null
432 || (!tag.is_core()
433 && !tag.forces_string()
434 && *tag != SfTag::Binary
435 && scalar_is_nullish(value, style))
436}
437
438#[cfg(feature = "deserialize")]
439pub(crate) fn leading_zero_decimal(t: &str) -> bool {
444 let s = yaml_trim(t);
445
446 let digits = s.strip_prefix(['+', '-']).unwrap_or(s);
448
449 if let Some(rest) = digits.strip_prefix('0') {
451 if let Some(next) = rest.chars().next() {
452 !matches!(next, 'x' | 'X' | 'o' | 'O' | 'b' | 'B')
454 } else {
455 false }
457 } else {
458 false
459 }
460}
461
462#[cfg(all(test, feature = "deserialize"))]
463mod tests {
464 use super::*;
465 use rstest::rstest;
466
467 fn sample_location() -> Location {
468 Location {
469 line: 42,
470 column: 7,
471 span: crate::location::Span::UNKNOWN,
472 source_id: 0,
473 }
474 }
475
476 #[test]
477 fn null_resolution_preserves_explicit_binary_scalars() {
478 for value in ["null", "Null", "NULL", "~", ""] {
479 assert!(scalar_is_null(&SfTag::None, value, &ScalarStyle::Plain));
480 assert!(scalar_is_null(&SfTag::Null, value, &ScalarStyle::Plain));
481 assert!(
482 !scalar_is_null(&SfTag::Binary, value, &ScalarStyle::Plain),
483 "explicit binary scalar must reach its deserializer: {value:?}"
484 );
485 }
486 }
487
488 #[test]
489 fn yaml11_bool_accepts_all_literals_and_trims_whitespace() {
490 let truthy = ["true", "Yes", " y ", "ON\n"];
491 for value in truthy {
492 assert!(parse_yaml11_bool(value).unwrap());
493 }
494
495 let falsy = ["false", "No", " n ", "OFF\t"];
496 for value in falsy {
497 assert!(!parse_yaml11_bool(value).unwrap());
498 }
499 }
500
501 #[test]
502 fn yaml11_bool_reports_error_for_invalid_literal() {
503 let err = parse_yaml11_bool("maybe").unwrap_err();
504 assert!(err.contains("invalid YAML 1.1 bool"));
505 }
506
507 #[test]
508 fn parse_int_signed_supports_alternate_radices_and_underscores() {
509 let loc = sample_location();
510 let value: i64 = parse_int_signed("0x7_fF", "i64", loc, false).unwrap();
511 assert_eq!(value, 0x7ff);
512
513 let value: i32 = parse_int_signed("0b1010_1010", "i32", loc, false).unwrap();
514 assert_eq!(value, 0b1010_1010);
515 }
516
517 #[test]
518 fn parse_int_signed_supports_i128_min_in_alternate_radices() {
519 let loc = sample_location();
520 let hex: i128 =
521 parse_int_signed("-0x80000000000000000000000000000000", "i128", loc, false).unwrap();
522 let binary_min = format!("-0b1{}", "0".repeat(127));
523 let binary: i128 = parse_int_signed(&binary_min, "i128", loc, false).unwrap();
524
525 assert_eq!(hex, i128::MIN);
526 assert_eq!(binary, i128::MIN);
527 assert!(
528 parse_int_signed::<i128>("0x80000000000000000000000000000000", "i128", loc, false)
529 .is_err()
530 );
531 }
532
533 #[test]
534 fn parse_int_signed_rejects_invalid_underscores() {
535 let loc = sample_location();
536 assert!(parse_int_signed::<i32>("_1", "i32", loc, false).is_err());
538 assert!(parse_int_signed::<i32>("1000_", "i32", loc, false).is_err());
540 assert!(parse_int_signed::<i32>("1__0", "i32", loc, false).is_err());
542 assert!(parse_int_signed::<i32>("1000_1000", "i32", loc, false).is_ok());
544 }
545
546 #[test]
547 fn parse_int_signed_honors_legacy_octal_prefixes() {
548 let loc = sample_location();
549 let value: i32 = parse_int_signed("00077", "i32", loc, true).unwrap();
550 assert_eq!(value, 0o77);
551 }
552
553 #[test]
554 fn parse_int_signed_honors_legacy_prefix_underscores() {
555 let loc = sample_location();
556
557 let octal: i32 = parse_int_signed("0_10", "i32", loc, true).unwrap();
558 let plus_octal: i32 = parse_int_signed("+0_10", "i32", loc, true).unwrap();
559 let negative_octal: i32 = parse_int_signed("-0_10", "i32", loc, true).unwrap();
560 let hex: i32 = parse_int_signed("0x_10", "i32", loc, true).unwrap();
561 let explicit_octal: i32 = parse_int_signed("0o_10", "i32", loc, true).unwrap();
562 let binary: i32 = parse_int_signed("0b_10", "i32", loc, true).unwrap();
563
564 assert_eq!(octal, 0o10);
565 assert_eq!(plus_octal, 0o10);
566 assert_eq!(negative_octal, -0o10);
567 assert_eq!(hex, 0x10);
568 assert_eq!(explicit_octal, 0o10);
569 assert_eq!(binary, 0b10);
570 }
571
572 #[test]
573 fn parse_int_signed_keeps_prefix_underscores_opt_in() {
574 let loc = sample_location();
575
576 assert!(parse_int_signed::<i32>("0_10", "i32", loc, false).is_err());
577 assert!(parse_int_signed::<i32>("0x_10", "i32", loc, false).is_err());
578 }
579
580 #[test]
581 fn parse_int_signed_preserves_error_location() {
582 let loc = sample_location();
583 let err = parse_int_signed::<i64>("0x8000000000000000", "i64", loc, false).unwrap_err();
584 match err {
585 Error::InvalidScalar { location, .. } => assert_eq!(location, loc),
586 other => panic!("unexpected error variant: {:?}", other),
587 }
588 }
589
590 #[test]
591 fn parse_int_unsigned_rejects_negative_inputs() {
592 let loc = sample_location();
593 let err = parse_int_unsigned::<u32>("-5", "u32", loc, false).unwrap_err();
594 match err {
595 Error::InvalidScalar { location, .. } => assert_eq!(location, loc),
596 other => panic!("unexpected error variant: {:?}", other),
597 }
598 }
599
600 #[test]
601 fn parse_int_unsigned_honors_legacy_prefix_underscores() {
602 let loc = sample_location();
603
604 let octal: u32 = parse_int_unsigned("0_10", "u32", loc, true).unwrap();
605 let plus_octal: u32 = parse_int_unsigned("+0_10", "u32", loc, true).unwrap();
606 let hex: u32 = parse_int_unsigned("0x_10", "u32", loc, true).unwrap();
607 let explicit_octal: u32 = parse_int_unsigned("0o_10", "u32", loc, true).unwrap();
608 let binary: u32 = parse_int_unsigned("0b_10", "u32", loc, true).unwrap();
609
610 assert_eq!(octal, 0o10);
611 assert_eq!(plus_octal, 0o10);
612 assert_eq!(hex, 0x10);
613 assert_eq!(explicit_octal, 0o10);
614 assert_eq!(binary, 0b10);
615 }
616
617 #[test]
618 fn parse_yaml12_floats_handle_nan_and_infinity_forms() {
619 let loc = sample_location();
620
621 let nan: f64 = parse_yaml12_float(" .NaN ", loc, SfTag::None, false).unwrap();
622 assert!(nan.is_nan());
623
624 let inf: f64 = parse_yaml12_float("+.INF", loc, SfTag::None, false).unwrap();
625 assert!(inf.is_infinite() && inf.is_sign_positive());
626
627 let neg_inf: f64 = parse_yaml12_float("-.Inf", loc, SfTag::None, false).unwrap();
628 assert!(neg_inf.is_infinite() && neg_inf.is_sign_negative());
629 }
630
631 #[rstest]
632 #[case::nan("nan")]
633 #[case::capital_nan("NaN")]
634 #[case::inf("inf")]
635 #[case::plus_inf("+inf")]
636 #[case::minus_inf("-inf")]
637 #[case::infinity("Infinity")]
638 #[case::plus_infinity("+Infinity")]
639 #[case::minus_infinity("-Infinity")]
640 fn parse_yaml12_float_rejects_rust_nonfinite_spellings(#[case] input: &str) {
641 assert!(parse_yaml12_float::<f64>(input, loc(), SfTag::None, false).is_err());
642 }
643
644 fn loc() -> Location {
645 Location {
647 line: 1,
648 column: 1,
649 span: crate::location::Span::UNKNOWN,
650 source_id: 0,
651 }
652 }
653
654 #[test]
655 fn test_normal_values() {
656 assert_eq!(
657 parse_yaml12_float::<f32>("1.5", loc(), SfTag::None, false).unwrap(),
658 1.5f32
659 );
660 assert_eq!(
661 parse_yaml12_float::<f32>("-123.456", loc(), SfTag::None, false).unwrap(),
662 -123.456f32
663 );
664 }
665
666 #[test]
667 fn test_zero_values() {
668 assert_eq!(
669 parse_yaml12_float::<f32>("0", loc(), SfTag::None, false).unwrap(),
670 0.0f32
671 );
672 assert_eq!(
673 parse_yaml12_float::<f32>("-0", loc(), SfTag::None, false).unwrap(),
674 -0.0f32
675 );
676 }
677
678 #[test]
679 fn test_nan_and_infinity() {
680 let nan: f32 = parse_yaml12_float(".nan", loc(), SfTag::None, false).unwrap();
681 assert!(nan.is_nan());
682
683 let inf: f64 = parse_yaml12_float(".inf", loc(), SfTag::None, false).unwrap();
684 assert!(inf.is_infinite() && inf.is_sign_positive());
685
686 let ninf: f32 = parse_yaml12_float("-.Inf", loc(), SfTag::None, false).unwrap();
687 assert!(ninf.is_infinite() && ninf.is_sign_negative());
688 }
689
690 #[test]
691 fn test_subnormal_preserved() {
692 let smallest = f32::from_bits(1) as f64;
694 let val: f32 =
695 parse_yaml12_float(&format!("{}", smallest), loc(), SfTag::None, false).unwrap();
696 assert_eq!(val, f32::from_bits(1));
697 }
698
699 #[test]
700 fn test_negative_zero_preserved() {
701 let val: f32 = parse_yaml12_float("-0.0", loc(), SfTag::None, false).unwrap();
702 assert_eq!(val.to_bits(), (-0.0f32).to_bits());
703 }
704}