use std::collections::BTreeMap;
use crate::value::Value;
pub(crate) fn from_text(text: &str) -> Value {
let mut reader = Reader {
input: text,
at: 0,
depth: 0,
};
match reader.value() {
Some(value) if reader.rest().is_empty() => value,
_ => Value::String(text.to_owned()),
}
}
struct Reader<'a> {
input: &'a str,
at: usize,
depth: usize,
}
const NESTING_LIMIT: usize = 64;
impl<'a> Reader<'a> {
fn rest(&self) -> &'a str {
&self.input[self.at..]
}
fn peek(&self) -> Option<char> {
self.rest().chars().next()
}
fn eat(&mut self, expected: char) -> Option<()> {
let next = self.peek()?;
if next == expected {
self.at += next.len_utf8();
Some(())
} else {
None
}
}
fn eat_any(&mut self) -> Option<char> {
let next = self.peek()?;
self.at += next.len_utf8();
Some(next)
}
fn eat_word(&mut self, word: &str) -> bool {
if self.rest().starts_with(word) {
self.at += word.len();
true
} else {
false
}
}
fn skip_whitespace(&mut self) {
while let Some(next) = self.peek() {
if next.is_ascii_whitespace() {
self.at += next.len_utf8();
} else {
break;
}
}
}
fn take_while(&mut self, mut wanted: impl FnMut(char) -> bool) -> &'a str {
let start = self.at;
while let Some(next) = self.peek() {
if wanted(next) {
self.at += next.len_utf8();
} else {
break;
}
}
&self.input[start..self.at]
}
fn value(&mut self) -> Option<Value> {
self.skip_whitespace();
let value = if self.eat_word("true") {
Value::Bool(true)
} else if self.eat_word("false") {
Value::Bool(false)
} else {
match self.peek() {
Some('{') => Value::Table(self.dict()?),
Some('[') => Value::Array(self.array()?),
Some('"') => Value::String(self.string()?),
Some('\'') => {
self.eat('\'')?;
let character = self.eat_any()?;
self.eat('\'')?;
Value::String(character.to_string())
}
_ => bare(self.take_while(is_not_separator).trim()),
}
};
self.skip_whitespace();
Some(value)
}
fn array(&mut self) -> Option<Vec<Value>> {
self.delimited('[', ']', |reader| reader.value())
}
fn dict(&mut self) -> Option<BTreeMap<String, Value>> {
let entries = self.delimited('{', '}', |reader| {
reader.skip_whitespace();
let key = reader.key()?;
reader.skip_whitespace();
reader.eat('=')?;
let value = reader.value()?;
Some((key, value))
})?;
Some(entries.into_iter().collect())
}
fn delimited<T>(
&mut self,
open: char,
close: char,
mut item: impl FnMut(&mut Self) -> Option<T>,
) -> Option<Vec<T>> {
self.eat(open)?;
if self.depth == NESTING_LIMIT {
return None;
}
self.depth += 1;
let mut collected = Vec::new();
let read = loop {
if self.eat(close).is_some() {
break Some(collected);
}
let Some(next) = item(self) else {
break None;
};
collected.push(next);
if self.eat(',').is_none() {
break self.eat(close).map(|()| collected);
}
};
self.depth -= 1;
read
}
fn key(&mut self) -> Option<String> {
if self.peek() == Some('"') {
return self.string();
}
let key = self.take_while(is_key_char);
if key.is_empty() {
None
} else {
Some(key.to_owned())
}
}
fn string(&mut self) -> Option<String> {
self.eat('"')?;
let mut escaped = false;
let inner = self.take_while(|character| {
if escaped {
escaped = false;
return true;
}
if character == '\\' {
escaped = true;
return true;
}
character != '"'
});
self.eat('"')?;
unescape(inner)
}
}
fn is_not_separator(character: char) -> bool {
!matches!(character, ',' | '{' | '}' | '[' | ']')
}
fn is_key_char(character: char) -> bool {
character.is_ascii_alphanumeric() || character == '_' || character == '-'
}
fn bare(text: &str) -> Value {
if text.contains('.') {
if let Ok(float) = text.parse::<f64>() {
return Value::Float(float);
}
}
if let Ok(unsigned) = text.parse::<usize>() {
return Value::Integer(unsigned as i128);
}
if let Ok(signed) = text.parse::<isize>() {
return Value::Integer(signed as i128);
}
Value::String(text.to_owned())
}
fn unescape(string: &str) -> Option<String> {
let mut characters = string.chars();
let mut output = String::with_capacity(string.len());
while let Some(character) = characters.next() {
match character {
'\\' => match characters.next()? {
'"' => output.push('"'),
'\\' => output.push('\\'),
'b' => output.push('\u{8}'),
'f' => output.push('\u{c}'),
'n' => output.push('\n'),
'r' => output.push('\r'),
't' => output.push('\t'),
short @ ('u' | 'U') => {
let width = if short == 'u' { 4 } else { 8 };
output.push(hex(&mut characters, width)?);
}
_ => return None,
},
'\u{09}' => output.push('\u{09}'),
printable if printable >= '\u{20}' && printable != '\u{7f}' => output.push(printable),
_ => return None,
}
}
Some(output)
}
fn hex(characters: &mut std::str::Chars<'_>, width: usize) -> Option<char> {
let mut digits = String::with_capacity(width);
for _ in 0..width {
let digit = characters.next()?;
if digit.is_ascii_hexdigit() {
digits.push(digit);
} else {
return None;
}
}
char::from_u32(u32::from_str_radix(&digits, 16).ok()?)
}
#[cfg(test)]
mod tests {
use super::{from_text, NESTING_LIMIT};
use crate::value::Value;
#[test]
fn a_value_nested_past_the_limit_is_read_as_a_string() {
let deep = "[".repeat(200_000);
assert_eq!(from_text(&deep), Value::String(deep.clone()));
let inside = format!(
"{}1{}",
"[".repeat(NESTING_LIMIT),
"]".repeat(NESTING_LIMIT)
);
let outside = format!(
"{}1{}",
"[".repeat(NESTING_LIMIT + 1),
"]".repeat(NESTING_LIMIT + 1)
);
assert!(matches!(from_text(&inside), Value::Array(_)));
assert_eq!(from_text(&outside), Value::String(outside.clone()));
}
fn original(text: &str) -> Option<Value> {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let parsed = std::panic::catch_unwind(|| text.parse::<figment::value::Value>());
std::panic::set_hook(previous);
parsed.ok().map(|value| {
crate::backend::figment::from_figment(&value.expect("the original never errors"))
})
}
#[test]
fn no_input_takes_the_process_down() {
for (text, expected) in [
("\"é\\n\"", "é\n"),
("\"ü\\t\"", "ü\t"),
("\"aé\\nb\"", "aé\nb"),
("\"€\\\\\"", "€\\"),
] {
assert_eq!(
from_text(text),
Value::String(expected.to_owned()),
"{text:?} has a reading, and taking the process down is not it"
);
}
}
#[test]
fn the_grammar_reads_what_it_always_read() {
let cases = [
"true",
"false",
"\"false\"",
" false ",
"1",
" -0",
" -2",
" 123 ",
"a ",
"\" a \"",
"1.2",
"3.14159",
"\"\\t\"",
"\"abc\\td\\n\"",
"\"\\\"hi\\\"\"",
"\"hi\\u1234there\"",
"\"\\\\\"",
"\"\\\"",
"[1,2,3]",
"{a=b}",
"{\"a.b.c\"=b}",
"{a=1,b=hi}",
"[1,[2],3]",
"{a=[[-2]]}",
"[1,true,hi,\"a \"]",
"[1,{a=b},hi]",
"[[ -1], {a=[ b ]}, hi ]",
"",
" ",
"8080",
"18446744073709551615",
"99999999999999999999999999",
"1e5",
"1.2.3",
"'a'",
"'ab'",
"truex",
"[]",
"{}",
"[1,]",
"{a=b,}",
"[1 2]",
"postgres://user:pass@host:5432/db",
"a,b,c",
];
for case in cases {
assert_eq!(
Some(from_text(case)),
original(case),
"reading {case:?} moved away from the original"
);
}
}
proptest::proptest! {
#[test]
fn every_string_reads_the_same(text in ".*") {
if let Some(expected) = original(&text) {
proptest::prop_assert_eq!(from_text(&text), expected);
}
}
#[test]
fn configuration_shaped_text_reads_the_same(
text in r#"[ ]*(true|false|-?[0-9]{1,20}|-?[0-9]{1,5}\.[0-9]{1,5}|[a-z]{1,4}|"[a-z ]{0,4}"|'[a-z]'|\[[^]]{0,12}\]|\{[a-z]{1,3}=[a-z0-9]{1,3}\})[ ]*"#
) {
if let Some(expected) = original(&text) {
proptest::prop_assert_eq!(from_text(&text), expected);
}
}
}
}