use crate::de::{Error, Location};
use crate::tags::SfTag;
use saphyr_parser::ScalarStyle;
use std::str::FromStr;
pub(crate) fn parse_yaml11_bool(s: &str) -> Result<bool, String> {
let t = s.trim();
if t.eq_ignore_ascii_case("true")
|| t.eq_ignore_ascii_case("yes")
|| t.eq_ignore_ascii_case("y")
|| t.eq_ignore_ascii_case("on")
{
Ok(true)
} else if t.eq_ignore_ascii_case("false")
|| t.eq_ignore_ascii_case("no")
|| t.eq_ignore_ascii_case("n")
|| t.eq_ignore_ascii_case("off")
{
Ok(false)
} else {
Err(format!("invalid YAML 1.1 bool: `{}`", s))
}
}
fn parse_digits_u128(digits: &str, radix: u32) -> Option<u128> {
let mut val: u128 = 0;
let mut saw = false;
for b in digits.as_bytes() {
match *b {
b'_' => continue,
b'0'..=b'9' => {
let d = (b - b'0') as u32;
if d >= radix {
return None;
}
val = val.checked_mul(radix as u128)?;
val = val.checked_add(d as u128)?;
saw = true;
}
b'a'..=b'f' if radix > 10 => {
let d = 10 + (b - b'a') as u32;
if d >= radix {
return None;
}
val = val.checked_mul(radix as u128)?;
val = val.checked_add(d as u128)?;
saw = true;
}
b'A'..=b'F' if radix > 10 => {
let d = 10 + (b - b'A') as u32;
if d >= radix {
return None;
}
val = val.checked_mul(radix as u128)?;
val = val.checked_add(d as u128)?;
saw = true;
}
_ => return None,
}
}
if saw { Some(val) } else { None }
}
fn parse_decimal_unsigned_u128(digits: &str) -> Option<u128> {
let mut val: u128 = 0;
let mut saw = false;
for b in digits.as_bytes() {
match *b {
b'_' => continue,
b'0'..=b'9' => {
let d = (b - b'0') as u128;
val = val.checked_mul(10)?;
val = val.checked_add(d)?;
saw = true;
}
_ => return None,
}
}
if saw { Some(val) } else { None }
}
fn parse_decimal_signed_i128(digits: &str, neg: bool) -> Option<i128> {
if neg {
let mut val: i128 = 0;
let mut saw = false;
for b in digits.as_bytes() {
match *b {
b'_' => continue,
b'0'..=b'9' => {
let d = (b - b'0') as i128;
val = val.checked_mul(10)?;
val = val.checked_sub(d)?;
saw = true;
}
_ => return None,
}
}
if saw { Some(val) } else { None }
} else {
let mut val: i128 = 0;
let mut saw = false;
for b in digits.as_bytes() {
match *b {
b'_' => continue,
b'0'..=b'9' => {
let d = (b - b'0') as i128;
val = val.checked_mul(10)?;
val = val.checked_add(d)?;
saw = true;
}
_ => return None,
}
}
if saw { Some(val) } else { None }
}
}
pub(crate) fn parse_int_signed<T>(
s: &str,
ty: &'static str,
location: Location,
legacy_octal: bool,
) -> Result<T, Error>
where
T: TryFrom<i128>,
{
let invalid = || Error::InvalidScalar { ty, location };
let t = s.trim();
let (neg, rest) = match t.strip_prefix('+') {
Some(r) => (false, r),
None => match t.strip_prefix('-') {
Some(r) => (true, r),
None => (false, t),
},
};
let (radix, digits) = radix_and_digits(legacy_octal, rest);
if radix == 10 {
let val_i128 = parse_decimal_signed_i128(digits, neg)
.ok_or_else(invalid)?;
return T::try_from(val_i128)
.map_err(|_| invalid());
}
let mag = parse_digits_u128(digits, radix)
.ok_or_else(invalid)?;
let val_i128: i128 = if neg {
let mag_i128: i128 = mag
.try_into()
.map_err(|_| invalid())?;
mag_i128
.checked_neg()
.ok_or_else(invalid)?
} else {
mag.try_into()
.map_err(|_| invalid())?
};
T::try_from(val_i128).map_err(|_| invalid())
}
pub(crate) fn parse_int_unsigned<T>(
s: &str,
ty: &'static str,
location: Location,
legacy_octal: bool,
) -> Result<T, Error>
where
T: TryFrom<u128>,
{
let invalid = || Error::InvalidScalar { ty, location };
let t = s.trim();
if t.starts_with('-') {
return Err(invalid());
}
let rest = t.strip_prefix('+').unwrap_or(t);
let (radix, digits) = radix_and_digits(legacy_octal, rest);
if radix == 10 {
let val_u128 = parse_decimal_unsigned_u128(digits)
.ok_or_else(invalid)?;
return T::try_from(val_u128)
.map_err(|_| invalid());
}
let mag = parse_digits_u128(digits, radix)
.ok_or_else(invalid)?;
T::try_from(mag).map_err(|_| invalid())
}
fn radix_and_digits(legacy_octal: bool, rest: &str) -> (u32, &str) {
let (radix, digits) =
if let Some(r) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
(16u32, r)
} else if let Some(r) = rest.strip_prefix("0o").or_else(|| rest.strip_prefix("0O")) {
(8u32, r)
} else if let Some(r) = rest.strip_prefix("0b").or_else(|| rest.strip_prefix("0B")) {
(2u32, r)
} else if legacy_octal && rest.starts_with("00") {
if rest == "00" {
(8u32, "0")
} else {
(8u32, &rest[2..])
}
} else {
(10u32, rest)
};
(radix, digits)
}
#[cfg(feature = "robotics")]
pub(crate) fn parse_yaml12_float<T>(
s: &str,
location: Location,
tag: SfTag,
angle_conversions: bool,
) -> Result<T, Error>
where
T: FromStr + crate::robotics::FromF64,
T: num_traits::Float,
{
if angle_conversions {
return crate::robotics::parse_yaml12_float_angle_converting(s, location, tag);
}
let t = s.trim();
let lower = t.to_ascii_lowercase();
match lower.as_str() {
".nan" | "+.nan" | "-.nan" => Ok(T::nan()),
".inf" | "+.inf" => Ok(T::infinity()),
"-.inf" => Ok(T::neg_infinity()),
_ => t
.parse::<T>()
.map_err(|_| Error::InvalidScalar { ty: "floating point", location }),
}
}
#[cfg(not(feature = "robotics"))]
pub(crate) fn parse_yaml12_float<T>(
s: &str,
location: Location,
_tag: SfTag,
_angle_conversions: bool,
) -> Result<T, Error>
where
T: FromStr,
T: num_traits::Float,
{
let t = s.trim();
let lower = t.to_ascii_lowercase();
match lower.as_str() {
".nan" | "+.nan" | "-.nan" => Ok(T::nan()),
".inf" | "+.inf" => Ok(T::infinity()),
"-.inf" => Ok(T::neg_infinity()),
_ => t
.parse::<T>()
.map_err(|_| Error::InvalidScalar { ty: "floating point", location }),
}
}
pub(crate) fn maybe_not_string(s: &str, style: &ScalarStyle) -> bool {
let location = Location::UNKNOWN;
style == &ScalarStyle::Plain
&& (parse_yaml12_float::<f64>(s, location, SfTag::None, false).is_ok()
|| parse_int_signed::<i128>(s, "i128", location, false).is_ok()
|| parse_yaml11_bool(s).is_ok()
|| scalar_is_nullish(s, &ScalarStyle::Plain))
}
#[inline]
pub(crate) fn scalar_is_nullish(value: &str, style: &ScalarStyle) -> bool {
if !matches!(style, ScalarStyle::Plain) {
return false;
}
value.is_empty() || value == "~" || value.eq_ignore_ascii_case("null")
}
#[inline]
pub(crate) fn scalar_is_nullish_for_option(value: &str, style: &ScalarStyle) -> bool {
(value.is_empty() && !matches!(style, ScalarStyle::SingleQuoted | ScalarStyle::DoubleQuoted)) || (matches!(style, ScalarStyle::Plain) && (value == "~" || value.eq_ignore_ascii_case("null"))) }
pub(crate) fn leading_zero_decimal(t: &str) -> bool {
let s = t.trim();
let digits = s.strip_prefix(['+', '-']).unwrap_or(s);
if let Some(rest) = digits.strip_prefix('0') {
if let Some(next) = rest.chars().next() {
!matches!(next, 'x' | 'X' | 'o' | 'O' | 'b' | 'B')
} else {
false }
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_location() -> Location {
Location {
line: 42,
column: 7,
span: crate::location::Span::UNKNOWN,
}
}
#[test]
fn yaml11_bool_accepts_all_literals_and_trims_whitespace() {
let truthy = ["true", "Yes", " y ", "ON\n"];
for value in truthy {
assert!(parse_yaml11_bool(value).unwrap());
}
let falsy = ["false", "No", " n ", "OFF\t"];
for value in falsy {
assert!(!parse_yaml11_bool(value).unwrap());
}
}
#[test]
fn yaml11_bool_reports_error_for_invalid_literal() {
let err = parse_yaml11_bool("maybe").unwrap_err();
assert!(err.contains("invalid YAML 1.1 bool"));
}
#[test]
fn parse_int_signed_supports_alternate_radices_and_underscores() {
let loc = sample_location();
let value: i64 = parse_int_signed("0x7_fF", "i64", loc, false).unwrap();
assert_eq!(value, 0x7ff);
let value: i32 = parse_int_signed("0b1010_1010", "i32", loc, false).unwrap();
assert_eq!(value, 0b1010_1010);
}
#[test]
fn parse_int_signed_honors_legacy_octal_prefixes() {
let loc = sample_location();
let value: i32 = parse_int_signed("00077", "i32", loc, true).unwrap();
assert_eq!(value, 0o77);
}
#[test]
fn parse_int_signed_preserves_error_location() {
let loc = sample_location();
let err = parse_int_signed::<i64>("0x8000000000000000", "i64", loc, false).unwrap_err();
match err {
Error::InvalidScalar { location, .. } => assert_eq!(location, loc),
other => panic!("unexpected error variant: {:?}", other),
}
}
#[test]
fn parse_int_unsigned_rejects_negative_inputs() {
let loc = sample_location();
let err = parse_int_unsigned::<u32>("-5", "u32", loc, false).unwrap_err();
match err {
Error::InvalidScalar { location, .. } => assert_eq!(location, loc),
other => panic!("unexpected error variant: {:?}", other),
}
}
#[test]
fn parse_yaml12_floats_handle_nan_and_infinity_forms() {
let loc = sample_location();
let nan: f64 = parse_yaml12_float(" .NaN ", loc, SfTag::None, false).unwrap();
assert!(nan.is_nan());
let inf: f64 = parse_yaml12_float("+.INF", loc, SfTag::None, false).unwrap();
assert!(inf.is_infinite() && inf.is_sign_positive());
let neg_inf: f64 = parse_yaml12_float("-.Inf", loc, SfTag::None, false).unwrap();
assert!(neg_inf.is_infinite() && neg_inf.is_sign_negative());
}
fn loc() -> Location {
Location {
line: 1,
column: 1,
span: crate::location::Span::UNKNOWN,
}
}
#[test]
fn test_normal_values() {
assert_eq!(
parse_yaml12_float::<f32>("1.5", loc(), SfTag::None, false).unwrap(),
1.5f32
);
assert_eq!(
parse_yaml12_float::<f32>("-123.456", loc(), SfTag::None, false).unwrap(),
-123.456f32
);
}
#[test]
fn test_zero_values() {
assert_eq!(
parse_yaml12_float::<f32>("0", loc(), SfTag::None, false).unwrap(),
0.0f32
);
assert_eq!(
parse_yaml12_float::<f32>("-0", loc(), SfTag::None, false).unwrap(),
-0.0f32
);
}
#[test]
fn test_nan_and_infinity() {
let nan: f32 = parse_yaml12_float(".nan", loc(), SfTag::None, false).unwrap();
assert!(nan.is_nan());
let inf: f64 = parse_yaml12_float(".inf", loc(), SfTag::None, false).unwrap();
assert!(inf.is_infinite() && inf.is_sign_positive());
let ninf: f32 = parse_yaml12_float("-.Inf", loc(), SfTag::None, false).unwrap();
assert!(ninf.is_infinite() && ninf.is_sign_negative());
}
#[test]
fn test_subnormal_preserved() {
let smallest = f32::from_bits(1) as f64;
let val: f32 =
parse_yaml12_float(&format!("{}", smallest), loc(), SfTag::None, false).unwrap();
assert_eq!(val, f32::from_bits(1));
}
#[test]
fn test_negative_zero_preserved() {
let val: f32 = parse_yaml12_float("-0.0", loc(), SfTag::None, false).unwrap();
assert_eq!(val.to_bits(), (-0.0f32).to_bits());
}
}