use std::borrow::Cow;
use deser_core::adapters::{Base64, BytesEncoding};
use deser_core::ext::{Date, Datetime, ExtValue, Offset, Time};
use deser_core::{Atom, Bytes, Implicit, ImplicitValue};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Version {
V1_1,
#[default]
V1_2,
}
const TAG_PREFIX: &str = "tag:yaml.org,2002:";
pub(crate) enum ScalarTag<'t> {
Str,
Standard(&'t str),
Custom,
}
pub(crate) fn classify_tag(tag: &str) -> ScalarTag<'_> {
if tag == "!" {
return ScalarTag::Str;
}
match tag.strip_prefix(TAG_PREFIX) {
Some(
name @ ("str" | "int" | "float" | "bool" | "null" | "binary" | "timestamp" | "seq"
| "map"),
) => ScalarTag::Standard(name),
_ => ScalarTag::Custom,
}
}
pub(crate) fn is_collection_tag(tag: &str, is_map: bool) -> Result<bool, &'static str> {
match classify_tag(tag) {
ScalarTag::Str => Ok(true),
ScalarTag::Standard("seq") if !is_map => Ok(true),
ScalarTag::Standard("map") if is_map => Ok(true),
ScalarTag::Standard(_) => Err("tag does not apply to this kind of node"),
_ => Ok(false),
}
}
#[inline]
pub(crate) fn resolve_plain(value: Cow<'_, str>, version: Version) -> Atom<'_> {
match resolve_plain_str(&value, version) {
Some(atom) => atom,
None => Atom::Str(value.into()),
}
}
#[inline]
pub(crate) fn resolve_plain_implicit(value: Cow<'_, str>, version: Version) -> Atom<'_> {
match resolve_plain_str(&value, version) {
Some(atom) => match ImplicitValue::from_atom(&atom) {
Some(resolved) => Atom::Implicit(Implicit::new(value, resolved)),
None => atom,
},
None => Atom::Str(value.into()),
}
}
pub(crate) fn is_plain_str(s: &str, version: Version) -> bool {
resolve_plain_str(s, version).is_none()
}
pub(crate) fn is_yaml11_implicit(s: &str) -> bool {
s == "=" || is_yaml11_timestamp_syntax(s)
}
fn is_yaml11_timestamp_syntax(s: &str) -> bool {
let bytes = s.as_bytes();
let mut pos = 0;
let digits = |pos: &mut usize, min: usize, max: usize| -> bool {
let len = bytes[*pos..]
.iter()
.take(max)
.take_while(|x| x.is_ascii_digit())
.count();
*pos += len;
len >= min
};
let expect = |pos: &mut usize, c: u8| -> bool {
let rv = bytes.get(*pos) == Some(&c);
*pos += rv as usize;
rv
};
if !digits(&mut pos, 4, 4) || !expect(&mut pos, b'-') {
return false;
}
let date_start = pos;
if !digits(&mut pos, 1, 2) || !expect(&mut pos, b'-') || !digits(&mut pos, 1, 2) {
return false;
}
if pos == bytes.len() {
return pos - date_start == 5;
}
match bytes[pos] {
b'T' | b't' => pos += 1,
b' ' | b'\t' => {
while matches!(bytes.get(pos), Some(b' ' | b'\t')) {
pos += 1;
}
}
_ => return false,
}
if !digits(&mut pos, 1, 2)
|| !expect(&mut pos, b':')
|| !digits(&mut pos, 2, 2)
|| !expect(&mut pos, b':')
|| !digits(&mut pos, 2, 2)
{
return false;
}
if expect(&mut pos, b'.') {
digits(&mut pos, 0, usize::MAX);
}
while matches!(bytes.get(pos), Some(b' ' | b'\t')) {
pos += 1;
}
match bytes.get(pos) {
None => true,
Some(b'Z') => pos + 1 == bytes.len(),
Some(b'+' | b'-') => {
pos += 1;
if !digits(&mut pos, 1, 2) {
return false;
}
if expect(&mut pos, b':') && !digits(&mut pos, 2, 2) {
return false;
}
pos == bytes.len()
}
_ => false,
}
}
#[test]
fn test_yaml11_timestamp_syntax() {
for s in [
"2001-12-14",
"2001-12-14t21:59:43.10-05:00",
"2001-12-14 21:59:43.10 -5",
"2001-12-15 2:59:43.10",
"2002-12-14T21:59:43Z",
"2001-02-30",
] {
assert!(is_yaml11_timestamp_syntax(s), "{}", s);
}
for s in [
"2001-12",
"2001-1-1",
"20011-12-14",
"2001-12-14 foo",
"hello",
] {
assert!(!is_yaml11_timestamp_syntax(s), "{}", s);
}
}
pub(crate) fn writes_as_plain(value: &Implicit, compat: Version) -> bool {
let text = value.text().as_str();
let reads_as = |version| {
resolve_plain_str(text, version)
.and_then(|atom| ImplicitValue::from_atom(&atom))
.is_some_and(|resolved| resolved.is_same(value.value()))
};
!text.is_empty()
&& reads_as(Version::V1_2)
&& (compat == Version::V1_2 || reads_as(Version::V1_1))
}
fn resolve_plain_str(s: &str, version: Version) -> Option<Atom<'static>> {
let first = match s.as_bytes().first() {
Some(&first) => first,
None => return Some(Atom::Null),
};
match first {
b'0'..=b'9' | b'+' | b'-' | b'.' => match version {
Version::V1_2 => parse_core_number(s),
Version::V1_1 => parse_yaml11_int(s).or_else(|| parse_yaml11_float(s)),
},
b'~' if s.len() == 1 => Some(Atom::Null),
b'n' | b'N' | b't' | b'T' | b'f' | b'F' | b'y' | b'Y' | b'o' | b'O' => {
parse_null(s).or_else(|| parse_bool(s, version))
}
_ => None,
}
}
pub(crate) fn resolve_standard<'x>(
name: &str,
value: Cow<'x, str>,
version: Version,
) -> Result<Atom<'x>, &'static str> {
let s = &*value;
match name {
"str" => Ok(Atom::Str(value.into())),
"null" => parse_null(s).ok_or("invalid !!null value"),
"bool" => parse_bool(s, version).ok_or("invalid !!bool value"),
"int" => match version {
Version::V1_2 => parse_core_int(s),
Version::V1_1 => parse_yaml11_int(s),
}
.ok_or("invalid !!int value"),
"float" => match version {
Version::V1_2 => parse_core_float(s).or_else(|| parse_core_int(s).map(int_to_float)),
Version::V1_1 => {
parse_yaml11_float(s).or_else(|| parse_yaml11_int(s).map(int_to_float))
}
}
.ok_or("invalid !!float value"),
"binary" => decode_base64(s)
.map(|bytes| Atom::Bytes(Bytes::new(bytes)))
.ok_or("invalid !!binary value"),
"timestamp" => parse_timestamp(s)
.map(|value| Atom::Ext(ExtValue::owned(value)))
.ok_or("invalid !!timestamp value"),
_ => Err("tag does not apply to scalars"),
}
}
pub(crate) fn parse_timestamp(s: &str) -> Option<Datetime> {
let bytes = s.as_bytes();
let mut pos = 0;
let number = |pos: &mut usize, min: usize, max: usize| -> Option<u32> {
let len = bytes[*pos..]
.iter()
.take(max)
.take_while(|x| x.is_ascii_digit())
.count();
if len < min {
return None;
}
let rv = s[*pos..*pos + len].parse().ok()?;
*pos += len;
Some(rv)
};
let year = number(&mut pos, 4, 4)? as u16;
let date_only = bytes.len() == 10;
let expect =
|pos: &mut usize, c: u8| -> Option<()> { (bytes.get(*pos) == Some(&c)).then(|| *pos += 1) };
expect(&mut pos, b'-')?;
let (min, max) = if date_only { (2, 2) } else { (1, 2) };
let month = number(&mut pos, min, max)? as u8;
expect(&mut pos, b'-')?;
let day = number(&mut pos, min, max)? as u8;
let date = Date { year, month, day };
if !date.is_valid() {
return None;
}
if date_only {
return Some(Datetime::from(date));
}
match bytes.get(pos)? {
b'T' | b't' => pos += 1,
b' ' | b'\t' => {
while let Some(b' ' | b'\t') = bytes.get(pos) {
pos += 1;
}
}
_ => return None,
}
let hour = number(&mut pos, 1, 2)? as u8;
expect(&mut pos, b':')?;
let minute = number(&mut pos, 2, 2)? as u8;
expect(&mut pos, b':')?;
let second = number(&mut pos, 2, 2)? as u8;
let mut nanosecond = 0;
if bytes.get(pos) == Some(&b'.') {
pos += 1;
let start = pos;
while bytes.get(pos).is_some_and(u8::is_ascii_digit) {
pos += 1;
}
let digits = &s[start..pos.min(start + 9)];
if !digits.is_empty() {
nanosecond = digits.parse::<u32>().ok()? * 10u32.pow(9 - digits.len() as u32);
}
}
let time = Time {
hour,
minute,
second,
nanosecond,
};
if !time.is_valid() {
return None;
}
while let Some(b' ' | b'\t') = bytes.get(pos) {
pos += 1;
}
let offset = match bytes.get(pos) {
None => Offset::Z,
Some(b'Z') => {
pos += 1;
Offset::Z
}
Some(&sign @ (b'+' | b'-')) => {
pos += 1;
let hours = number(&mut pos, 1, 2)?;
let minutes = if bytes.get(pos) == Some(&b':') {
pos += 1;
number(&mut pos, 2, 2)?
} else {
0
};
if hours > 23 || minutes > 59 {
return None;
}
let minutes = (hours * 60 + minutes) as i16;
Offset::Custom {
minutes: if sign == b'-' { -minutes } else { minutes },
}
}
_ => return None,
};
if pos != bytes.len() {
return None;
}
Some(Datetime {
date: Some(date),
time: Some(time),
offset: Some(offset),
})
}
#[test]
fn test_parse_timestamp() {
let ts = |s: &str| parse_timestamp(s).map(|x| x.to_string());
assert_eq!(ts("2002-12-14").as_deref(), Some("2002-12-14"));
assert_eq!(
ts("2001-12-14t21:59:43.10-05:00").as_deref(),
Some("2001-12-14T21:59:43.1-05:00")
);
assert_eq!(
ts("2001-12-14 21:59:43.10 -5").as_deref(),
Some("2001-12-14T21:59:43.1-05:00")
);
assert_eq!(
ts("2001-12-15 2:59:43.10").as_deref(),
Some("2001-12-15T02:59:43.1Z")
);
assert_eq!(
ts("2001-12-15T02:59:43.1Z").as_deref(),
Some("2001-12-15T02:59:43.1Z")
);
assert_eq!(
ts("2001-1-5 02:59:43").as_deref(),
Some("2001-01-05T02:59:43Z")
);
for invalid in [
"",
"2002-12-1",
"2002-13-14",
"2002-12-14 ",
"2002-12-14T25:00:00",
"2002-12-14T02:59",
"2002-12-14T02:59:43X",
"2002-12-14T02:59:43+24",
"02002-12-14",
] {
assert!(parse_timestamp(invalid).is_none(), "{}", invalid);
}
}
fn int_to_float(atom: Atom) -> Atom<'static> {
Atom::F64(match atom {
Atom::U64(value) => value as f64,
Atom::I64(value) => value as f64,
Atom::F64(value) => value,
Atom::Ext(ref ext) => match (ext.downcast_ref::<u128>(), ext.downcast_ref::<i128>()) {
(Some(&value), _) => value as f64,
(_, Some(&value)) => value as f64,
_ => unreachable!(),
},
_ => unreachable!(),
})
}
fn parse_null(s: &str) -> Option<Atom<'static>> {
match s {
"" | "~" | "null" | "Null" | "NULL" => Some(Atom::Null),
_ => None,
}
}
fn parse_bool(s: &str, version: Version) -> Option<Atom<'static>> {
match s {
"true" | "True" | "TRUE" => Some(Atom::Bool(true)),
"false" | "False" | "FALSE" => Some(Atom::Bool(false)),
"y" | "Y" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" if version == Version::V1_1 => {
Some(Atom::Bool(true))
}
"n" | "N" | "no" | "No" | "NO" | "off" | "Off" | "OFF" if version == Version::V1_1 => {
Some(Atom::Bool(false))
}
_ => None,
}
}
fn split_sign(s: &str) -> (bool, &str) {
match s.as_bytes().first() {
Some(b'-') => (true, &s[1..]),
Some(b'+') => (false, &s[1..]),
_ => (false, s),
}
}
fn accumulate(digits: &str, radix: u32, underscores: bool) -> Option<Magnitude> {
let mut value = Some(0u128);
let mut approx = 0f64;
let mut seen = false;
for c in digits.chars() {
if c == '_' && underscores {
continue;
}
let digit = c.to_digit(radix)?;
seen = true;
value = value
.and_then(|v| v.checked_mul(radix as u128))
.and_then(|v| v.checked_add(digit as u128));
approx = approx * radix as f64 + digit as f64;
}
if value.is_none() && radix == 10 {
approx = digits.replace('_', "").parse().unwrap_or(approx);
}
if seen {
Some(Magnitude { value, approx })
} else {
None
}
}
struct Magnitude {
value: Option<u128>,
approx: f64,
}
fn make_int(negative: bool, magnitude: Magnitude) -> Atom<'static> {
match magnitude.value {
Some(value) if !negative => match u64::try_from(value) {
Ok(value) => Atom::U64(value),
Err(_) => Atom::Ext(ExtValue::owned(value)),
},
Some(value) if value <= 1u128 << 63 => Atom::I64((value as i128).wrapping_neg() as i64),
Some(value) if value <= 1u128 << 127 => {
Atom::Ext(ExtValue::owned((value as i128).wrapping_neg()))
}
_ => Atom::F64(if negative {
-magnitude.approx
} else {
magnitude.approx
}),
}
}
fn parse_core_int(s: &str) -> Option<Atom<'static>> {
if let Some(rest) = s.strip_prefix("0o") {
return accumulate(rest, 8, false).map(|m| make_int(false, m));
}
if let Some(rest) = s.strip_prefix("0x") {
return accumulate(rest, 16, false).map(|m| make_int(false, m));
}
let (negative, digits) = split_sign(s);
accumulate(digits, 10, false).map(|m| make_int(negative, m))
}
fn parse_core_number(s: &str) -> Option<Atom<'static>> {
let bytes = s.as_bytes();
let negative = bytes.first() == Some(&b'-');
let digits_start = usize::from(matches!(bytes.first(), Some(b'-' | b'+')));
let mut pos = digits_start;
let mut value = Some(0u64);
while let Some(&b @ b'0'..=b'9') = bytes.get(pos) {
value = value
.and_then(|v| v.checked_mul(10))
.and_then(|v| v.checked_add(u64::from(b - b'0')));
pos += 1;
}
match (bytes.get(pos), value) {
(None, Some(value)) if pos > digits_start => Some(make_int(
negative,
Magnitude {
value: Some(value.into()),
approx: value as f64,
},
)),
(Some(b'.' | b'e' | b'E'), _) => parse_core_float(s),
_ => parse_core_int(s).or_else(|| parse_core_float(s)),
}
}
fn parse_yaml11_int(s: &str) -> Option<Atom<'static>> {
let (negative, rest) = split_sign(s);
let magnitude = if let Some(digits) = rest.strip_prefix("0b") {
accumulate(digits, 2, true)?
} else if let Some(digits) = rest.strip_prefix("0x") {
accumulate(digits, 16, true)?
} else if rest == "0" {
accumulate(rest, 10, false)?
} else if let Some(digits) = rest.strip_prefix('0') {
accumulate(digits, 8, true)?
} else if rest
.as_bytes()
.first()
.is_some_and(|b| (b'1'..=b'9').contains(b))
{
if rest.contains(':') {
parse_base60(rest)?
} else {
accumulate(rest, 10, true)?
}
} else {
return None;
};
Some(make_int(negative, magnitude))
}
fn parse_base60(s: &str) -> Option<Magnitude> {
let mut parts = s.split(':');
let mut rv = accumulate(parts.next()?, 10, true)?;
for part in parts {
if !is_base60_digit(part) {
return None;
}
let digit = part.parse::<u8>().ok()?;
rv.value = rv
.value
.and_then(|v| v.checked_mul(60))
.and_then(|v| v.checked_add(digit as u128));
rv.approx = rv.approx * 60.0 + digit as f64;
}
Some(rv)
}
fn is_base60_digit(s: &str) -> bool {
matches!(s.as_bytes(), [b'0'..=b'9'] | [b'0'..=b'5', b'0'..=b'9'])
}
fn parse_special_float(s: &str) -> Option<Atom<'static>> {
match s {
".nan" | ".NaN" | ".NAN" => return Some(Atom::F64(f64::NAN)),
_ => {}
}
let (negative, rest) = split_sign(s);
match rest {
".inf" | ".Inf" | ".INF" => Some(Atom::F64(if negative {
f64::NEG_INFINITY
} else {
f64::INFINITY
})),
_ => None,
}
}
fn skip_digits(bytes: &[u8], pos: &mut usize, underscores: bool) -> usize {
let mut count = 0;
while let Some(&b) = bytes.get(*pos) {
if b.is_ascii_digit() {
count += 1;
} else if !(b == b'_' && underscores) {
break;
}
*pos += 1;
}
count
}
fn skip_exponent(bytes: &[u8], pos: &mut usize, sign_required: bool) -> bool {
if !matches!(bytes.get(*pos), Some(b'e' | b'E')) {
return true;
}
*pos += 1;
if matches!(bytes.get(*pos), Some(b'-' | b'+')) {
*pos += 1;
} else if sign_required {
return false;
}
skip_digits(bytes, pos, false) > 0
}
fn parse_float_text(s: &str, underscores: bool) -> Option<Atom<'static>> {
let value = if underscores && s.contains('_') {
s.replace('_', "").parse()
} else {
s.parse()
};
value.ok().map(Atom::F64)
}
fn parse_core_float(s: &str) -> Option<Atom<'static>> {
if let Some(atom) = parse_special_float(s) {
return Some(atom);
}
let bytes = s.as_bytes();
let mut pos = usize::from(matches!(bytes.first(), Some(b'-' | b'+')));
let int_digits = skip_digits(bytes, &mut pos, false);
let mut frac_digits = 0;
if bytes.get(pos) == Some(&b'.') {
pos += 1;
frac_digits = skip_digits(bytes, &mut pos, false);
} else if int_digits == 0 {
return None;
}
if int_digits == 0 && frac_digits == 0 {
return None;
}
if !skip_exponent(bytes, &mut pos, false) || pos != bytes.len() {
return None;
}
parse_float_text(s, false)
}
fn parse_yaml11_float(s: &str) -> Option<Atom<'static>> {
if let Some(atom) = parse_special_float(s) {
return Some(atom);
}
let (negative, rest) = split_sign(s);
let bytes = rest.as_bytes();
let mut pos = 0;
match bytes.first() {
Some(b'0'..=b'9') => {
skip_digits(bytes, &mut pos, true);
if bytes.get(pos) == Some(&b':') {
return parse_base60_float(negative, rest);
}
if bytes.get(pos) != Some(&b'.') {
return None;
}
pos += 1;
skip_digits(bytes, &mut pos, true);
}
Some(b'.') if matches!(bytes.get(1), Some(b'0'..=b'9')) => {
pos += 1;
skip_digits(bytes, &mut pos, true);
}
_ => return None,
}
if !skip_exponent(bytes, &mut pos, true) || pos != bytes.len() {
return None;
}
parse_float_text(s, true)
}
fn parse_base60_float(negative: bool, s: &str) -> Option<Atom<'static>> {
let (int_part, frac_part) = s.split_once('.')?;
let mut parts = int_part.split(':');
let mut value = accumulate(parts.next()?, 10, true)?.approx;
let mut count = 0;
for part in parts {
if !is_base60_digit(part) {
return None;
}
value = value * 60.0 + part.parse::<u8>().ok()? as f64;
count += 1;
}
if count == 0 || !frac_part.bytes().all(|b| b.is_ascii_digit() || b == b'_') {
return None;
}
let frac: f64 = format!("0.{}", frac_part.replace('_', "")).parse().ok()?;
value += frac;
Some(Atom::F64(if negative { -value } else { value }))
}
fn decode_base64(s: &str) -> Option<Vec<u8>> {
if s.bytes().any(|b| b.is_ascii_whitespace()) {
let s: String = s.chars().filter(|c| !c.is_ascii_whitespace()).collect();
Base64::decode(&s).ok()
} else {
Base64::decode(s).ok()
}
}
#[test]
fn test_base64() {
assert_eq!(decode_base64("").unwrap(), b"");
assert_eq!(decode_base64("Zg==").unwrap(), b"f");
assert_eq!(decode_base64("Zm8=").unwrap(), b"fo");
assert_eq!(decode_base64("Zm9v").unwrap(), b"foo");
assert_eq!(decode_base64("Zm9v\n YmFy").unwrap(), b"foobar");
assert_eq!(decode_base64("Zm9v\n YmE=\n").unwrap(), b"fooba");
assert_eq!(decode_base64("Zm8").unwrap(), b"fo");
assert_eq!(decode_base64("-_8=").unwrap(), b"\xfb\xff");
assert_eq!(decode_base64("Zm9"), None);
assert_eq!(decode_base64("Z=9v"), None);
assert_eq!(decode_base64("Zm9!"), None);
assert_eq!(decode_base64("Zm8=="), None);
assert_eq!(decode_base64("Zh=="), None);
}
#[test]
fn test_big_ints() {
assert_eq!(
parse_core_int("18446744073709551616"),
Some(Atom::Ext(ExtValue::owned(18446744073709551616u128)))
);
assert_eq!(
parse_core_int("-9223372036854775808"),
Some(Atom::I64(i64::MIN))
);
assert_eq!(
parse_core_int("-9223372036854775809"),
Some(Atom::Ext(ExtValue::owned(-9223372036854775809i128)))
);
assert_eq!(
parse_core_int("-170141183460469231731687303715884105728"),
Some(Atom::Ext(ExtValue::owned(i128::MIN)))
);
assert_eq!(
parse_core_int("1000000000000000000000000000000000000000000"),
Some(Atom::F64(1e42))
);
}
#[test]
fn test_core_number_fast_path() {
let tokens = [
"0",
"1",
"+1",
"-1",
"-0",
"007",
"123456789",
"9223372036854775807",
"9223372036854775808",
"-9223372036854775808",
"-9223372036854775809",
"18446744073709551615",
"18446744073709551616",
"-18446744073709551616",
"340282366920938463463374607431768211456",
"1.5",
"-1.5",
"+1.5",
"1.",
".5",
"-.5",
"1e5",
"1E+5",
"1e-5",
"1.5e-5",
"1e",
"1.e5",
"1e400",
"0x1f",
"0o17",
"-0x1f",
".inf",
"-.inf",
".nan",
"1_000",
"1-2",
"-",
"+",
"12a",
"1:30",
];
for token in tokens {
let expected = parse_core_int(token).or_else(|| parse_core_float(token));
assert_eq!(
format!("{:?}", parse_core_number(token)),
format!("{:?}", expected),
"{}",
token
);
}
}