use crate::parse_scalars::parse_yaml11_bool;
#[inline]
fn is_numeric_looking(s: &str) -> bool {
let bytes = s.as_bytes();
let len = bytes.len();
if len == 0 {
return false;
}
#[inline]
fn consume_digit_run<F>(bytes: &[u8], mut p: usize, is_digit: F) -> Result<(usize, bool), ()>
where
F: Fn(u8) -> bool,
{
let start = p;
let mut saw_digit = false;
while p < bytes.len() {
if is_digit(bytes[p]) {
saw_digit = true;
p += 1;
continue;
}
if bytes[p] == b'_' {
let prev_is_digit = p > start && is_digit(bytes[p - 1]);
let next_is_digit = match bytes.get(p + 1) {
Some(&next) => is_digit(next),
None => false,
};
if !prev_is_digit || !next_is_digit {
return Err(());
}
p += 1;
continue;
}
break;
}
Ok((p, saw_digit))
}
#[inline]
fn consume_exponent(bytes: &[u8], mut p: usize) -> Option<usize> {
if p >= bytes.len() || (bytes[p] != b'e' && bytes[p] != b'E') {
return None;
}
p += 1;
if p < bytes.len() && (bytes[p] == b'+' || bytes[p] == b'-') {
p += 1;
}
let (p, saw_digit) = consume_digit_run(bytes, p, |b| b.is_ascii_digit()).ok()?;
saw_digit.then_some(p)
}
let mut p = 0;
if bytes[0] == b'+' || bytes[0] == b'-' {
p = 1;
if p == len {
return false;
}
}
if len.saturating_sub(p) >= 3 && bytes[p] == b'0' {
match bytes[p + 1] {
b'b' | b'B' => {
let (end, saw_digit) =
match consume_digit_run(bytes, p + 2, |b| matches!(b, b'0' | b'1')) {
Ok(run) => run,
Err(()) => return false,
};
return saw_digit && end == len;
}
b'o' | b'O' => {
let (end, saw_digit) =
match consume_digit_run(bytes, p + 2, |b| (b'0'..=b'7').contains(&b)) {
Ok(run) => run,
Err(()) => return false,
};
return saw_digit && end == len;
}
b'x' | b'X' => {
let (end, saw_digit) =
match consume_digit_run(bytes, p + 2, |b| b.is_ascii_hexdigit()) {
Ok(run) => run,
Err(()) => return false,
};
return saw_digit && end == len;
}
_ => {}
}
}
if bytes[p] == b'.' {
p += 1;
let (end, saw_digit) = match consume_digit_run(bytes, p, |b| b.is_ascii_digit()) {
Ok(run) => run,
Err(()) => return false,
};
p = end;
if !saw_digit {
return false;
}
return match consume_exponent(bytes, p) {
Some(end) => end == len,
None => p == len,
};
}
if !bytes[p].is_ascii_digit() {
return false;
}
let (end, saw_digit) = match consume_digit_run(bytes, p, |b| b.is_ascii_digit()) {
Ok(run) => run,
Err(()) => return false,
};
p = end;
debug_assert!(saw_digit, "decimal branch always starts on a digit");
if p == len {
return true;
}
if bytes[p] == b'.' {
p += 1;
let (end, _) = match consume_digit_run(bytes, p, |b| b.is_ascii_digit()) {
Ok(run) => run,
Err(()) => return false,
};
p = end;
return match consume_exponent(bytes, p) {
Some(end) => end == len,
None => p == len,
};
}
if bytes[p] == b'e' || bytes[p] == b'E' {
return matches!(consume_exponent(bytes, p), Some(end) if end == len);
}
false
}
fn is_ambiguous(s: &str) -> bool {
if s.is_empty() {
return true;
}
if s == "~"
|| s.eq_ignore_ascii_case("null")
|| s.eq_ignore_ascii_case("true")
|| s.eq_ignore_ascii_case("false")
{
return true;
}
#[inline]
fn is_ascii_lower(b: u8) -> u8 {
b | 0x20
}
#[inline]
fn is_special_inf_nan_ascii(s: &str) -> bool {
let bytes = s.as_bytes();
let mut i = 0usize;
if let Some(&c) = bytes.first()
&& (c == b'+' || c == b'-')
{
i = 1;
}
if let Some(&c) = bytes.get(i)
&& c == b'.'
{
i += 1;
} else {
return false;
}
if bytes.len() == i + 3 {
let a = is_ascii_lower(bytes[i]);
let b = is_ascii_lower(bytes[i + 1]);
let c = is_ascii_lower(bytes[i + 2]);
return (a == b'n' && b == b'a' && c == b'n') || (a == b'i' && b == b'n' && c == b'f');
}
false
}
if is_special_inf_nan_ascii(s) {
return true;
}
if is_numeric_looking(s) {
return true;
}
false
}
#[inline]
fn is_ambiguous_value(s: &str, yaml_12: bool) -> bool {
if is_ambiguous(s) {
return true;
}
if !yaml_12 && parse_yaml11_bool(s).is_ok() {
return true;
}
s.eq_ignore_ascii_case("nan")
|| s.eq_ignore_ascii_case("inf")
|| s.eq_ignore_ascii_case("+inf")
|| s.eq_ignore_ascii_case("-inf")
}
#[inline]
pub(crate) fn is_plain_safe(s: &str) -> bool {
if is_ambiguous(s) {
return false;
}
if s == "<<" {
return false;
}
let bytes = s.as_bytes();
if bytes.first().is_some_and(u8::is_ascii_whitespace)
|| bytes.last().is_some_and(u8::is_ascii_whitespace)
{
return false;
}
match bytes[0] {
b'-' | b'?' => {
if bytes.len() == 1 {
return false;
}
if bytes[1].is_ascii_whitespace() {
return false;
}
}
b',' => return false,
b':' | b'[' | b']' | b'{' | b'}' | b'#' | b'&' | b'*' | b'!' | b'|' | b'>' | b'\''
| b'"' | b'%' | b'@' | b'`' => return false,
_ => {}
}
!contains_any_or_is_control(s, &[':', '#'])
}
#[inline]
pub(crate) fn is_plain_value_safe(s: &str, yaml_12: bool, in_flow: bool) -> bool {
if is_ambiguous_value(s, yaml_12) {
return false;
}
let bytes = s.as_bytes();
if bytes.first().is_some_and(u8::is_ascii_whitespace) {
return false;
}
match bytes {
[b'-' | b'?'] => return false,
[b'-' | b'?', b1, ..] if b1.is_ascii_whitespace() => return false,
[
b',' | b':' | b'[' | b']' | b'{' | b'}' | b'#' | b'&' | b'*' | b'!' | b'|' | b'>'
| b'\'' | b'"' | b'%' | b'@' | b'`',
..,
] => return false,
_ => {}
}
if s.contains(": ") || s.trim().ends_with(':') {
return false;
}
if in_flow {
!contains_any_or_is_control(s, &[',', '[', ']', '{', '}', '#'])
} else {
!contains_any_or_is_control(s, &['#'])
}
}
#[inline]
pub(crate) fn is_block_scalar_content_safe(s: &str) -> bool {
s.chars().all(|ch| match ch {
'\n' | '\t' => true,
'\r' | '\u{0085}' | '\u{2028}' | '\u{2029}' | '\u{FEFF}' => false,
c => matches!(
c as u32,
0x20..=0x7E | 0xA0..=0xD7FF | 0xE000..=0xFFFD | 0x10000..=0x10FFFF
),
})
}
#[inline]
pub(crate) fn is_auto_block_scalar_readable(s: &str) -> bool {
!s.is_empty()
}
#[inline]
pub(crate) fn is_controll_which_needs_escaping(ch: char) -> bool {
ch.is_control() || matches!(ch, '\u{FEFF}' | '\u{2028}' | '\u{2029}')
}
fn contains_any_or_is_control(string: &str, values: &[char]) -> bool {
string
.chars()
.any(|x| is_controll_which_needs_escaping(x) || values.iter().any(|v| &x == v))
}
#[cfg(test)]
mod tests {
use super::{
is_controll_which_needs_escaping, is_numeric_looking, is_plain_safe, is_plain_value_safe,
};
use rstest::rstest;
#[rstest]
#[case::zero("0")]
#[case::neg_int("-19")]
#[case::pos_int("+12")]
#[case::leading_zero("01")]
#[case::underscore_sep("1_0")]
#[case::multi_underscore("1000_1000_1000")]
#[case::binary("0b10")]
#[case::pos_binary("+0b10")]
#[case::neg_binary_upper("-0B10")]
#[case::binary_underscore("0b1010_1010")]
#[case::octal("0o7")]
#[case::pos_octal_upper("+0O7")]
#[case::octal_underscore("0o7_1")]
#[case::hex("0x3A")]
#[case::pos_hex_upper("+0X3A")]
#[case::hex_underscore("0x3_A")]
#[case::leading_dot(".5")]
#[case::pos_leading_dot("+.5")]
#[case::neg_leading_dot("-.5")]
#[case::trailing_dot("0.")]
#[case::pos_zero_float("+0.0")]
#[case::neg_zero_float("-0.0")]
#[case::exponent("12e03")]
#[case::exponent_underscore("12e0_3")]
#[case::neg_exponent_upper("-2E+05")]
#[case::float_neg_exponent("12.34e-5")]
#[case::leading_dot_exponent(".5e+1")]
#[case::neg_leading_dot_exponent("-.5E-2")]
fn numeric_looking_matches(#[case] input: &str) {
assert!(is_numeric_looking(input), "{input:?} should match");
}
#[rstest]
#[case::empty("")]
#[case::lone_plus("+")]
#[case::lone_minus("-")]
#[case::lone_dot(".")]
#[case::leading_underscore("_1000")]
#[case::trailing_underscore("1000_")]
#[case::double_underscore("1__0")]
#[case::exponent_leading_underscore("1e_2")]
#[case::underscore_before_dot("_.5")]
#[case::underscore_after_dot("._5")]
#[case::empty_binary("0b")]
#[case::binary_trailing_underscore("0b10_")]
#[case::octal_leading_underscore("0o_7")]
#[case::hex_trailing_underscore("0x3A_")]
#[case::empty_octal("0o")]
#[case::empty_hex("0x")]
#[case::hex_with_sign("0x+1")]
#[case::hex_inner_sign("-0x-1")]
#[case::exponent_no_digits("12e")]
#[case::dot_exponent_no_mantissa(".e5")]
#[case::inf(".inf")]
#[case::nan(".nan")]
#[case::fractional_inner_underscore("1._0")]
fn numeric_looking_non_matches(#[case] input: &str) {
assert!(!is_numeric_looking(input), "{input:?} should not match");
}
#[rstest]
#[case::dash_no_space("-value")]
#[case::question_no_space("?query")]
#[case::interior_space("a b")]
fn plain_keys_allow_safe_inputs(#[case] input: &str) {
assert!(is_plain_safe(input), "{input:?}");
}
#[rstest]
#[case::dash_indicator_space("- value")]
#[case::question_indicator_tab("?\tvalue")]
#[case::trailing_space("foo ")]
#[case::leading_space(" foo")]
#[case::trailing_tab("foo\t")]
#[case::merge_key("<<")]
fn plain_keys_reject_unsafe_inputs(#[case] input: &str) {
assert!(!is_plain_safe(input), "{input:?}");
}
#[rstest]
#[case::nul('\0')]
#[case::tab('\t')]
#[case::newline('\n')]
#[case::carriage_return('\r')]
#[case::nel('\u{0085}')]
#[case::bom('\u{FEFF}')]
#[case::line_sep('\u{2028}')]
#[case::para_sep('\u{2029}')]
fn chars_needing_escaping(#[case] ch: char) {
assert!(is_controll_which_needs_escaping(ch), "{ch:?} should escape");
}
#[rstest]
#[case::ascii('a')]
#[case::space(' ')]
#[case::unicode('é')]
#[case::cjk('字')]
fn chars_not_needing_escaping(#[case] ch: char) {
assert!(
!is_controll_which_needs_escaping(ch),
"{ch:?} should stay plain"
);
}
#[rstest]
#[case::bom("\u{FEFF}")]
#[case::bom_prefixed("\u{FEFF}key")]
#[case::line_sep("a\u{2028}b")]
#[case::para_sep("a\u{2029}b")]
fn format_chars_are_not_plain_safe(#[case] input: &str) {
assert!(!is_plain_safe(input), "key {input:?}");
assert!(!is_plain_value_safe(input, false, false), "value {input:?}");
assert!(
!is_plain_value_safe(input, true, true),
"flow value {input:?}"
);
}
}