#![allow(clippy::unneeded_field_pattern)]
#![allow(clippy::toplevel_ref_arg)]
#[macro_use]
mod macros;
mod array;
pub(crate) mod datetime;
mod document;
mod errors;
mod inline_table;
mod key;
mod numbers;
pub(crate) mod strings;
mod table;
mod trivia;
mod value;
pub use self::errors::TomlError;
pub(crate) use self::key::is_unquoted_char;
pub(crate) use self::key::simple_key as key_parser;
pub(crate) use self::value::value as value_parser;
use self::table::duplicate_key;
use crate::key::Key;
use crate::parser::errors::CustomError;
use crate::repr::Decor;
use crate::{ArrayOfTables, Document, Entry, Item, Table};
pub(crate) struct TomlParser {
document: Document,
trailing: String,
current_table_position: usize,
current_table: Table,
current_is_array: bool,
current_table_path: Vec<Key>,
}
impl TomlParser {
pub(crate) fn start_aray_table(
&mut self,
path: Vec<Key>,
decor: Decor,
) -> Result<(), CustomError> {
debug_assert!(!path.is_empty());
debug_assert!(self.current_table.is_empty());
debug_assert!(self.current_table_path.is_empty());
let root = self.document.as_table_mut();
let parent_table = Self::descend_path(root, &path[..path.len() - 1], false)?;
let key = &path[path.len() - 1];
let entry = parent_table
.entry_format(key)
.or_insert(Item::ArrayOfTables(ArrayOfTables::new()));
entry
.as_array_of_tables()
.ok_or_else(|| duplicate_key(&path, path.len() - 1))?;
self.current_table_position += 1;
self.current_table.decor = decor;
self.current_table.set_position(self.current_table_position);
self.current_is_array = true;
self.current_table_path = path;
Ok(())
}
pub(crate) fn start_table(&mut self, path: Vec<Key>, decor: Decor) -> Result<(), CustomError> {
debug_assert!(!path.is_empty());
debug_assert!(self.current_table.is_empty());
debug_assert!(self.current_table_path.is_empty());
let root = self.document.as_table_mut();
let parent_table = Self::descend_path(root, &path[..path.len() - 1], false)?;
let key = &path[path.len() - 1];
if let Some(entry) = parent_table.remove(key.get()) {
match entry {
Item::Table(t) if t.implicit => {
self.current_table = t;
}
_ => return Err(duplicate_key(&path, path.len() - 1)),
}
}
self.current_table_position += 1;
self.current_table.decor = decor;
self.current_table.set_position(self.current_table_position);
self.current_is_array = false;
self.current_table_path = path;
Ok(())
}
pub(crate) fn finalize_table(&mut self) -> Result<(), CustomError> {
let mut table = std::mem::take(&mut self.current_table);
let path = std::mem::take(&mut self.current_table_path);
let root = self.document.as_table_mut();
if path.is_empty() {
assert!(root.is_empty());
std::mem::swap(&mut table, root);
} else if self.current_is_array {
let parent_table = Self::descend_path(root, &path[..path.len() - 1], false)?;
let key = &path[path.len() - 1];
let entry = parent_table
.entry_format(key)
.or_insert(Item::ArrayOfTables(ArrayOfTables::new()));
let array = entry
.as_array_of_tables_mut()
.ok_or_else(|| duplicate_key(&path, path.len() - 1))?;
array.push(table);
} else {
let parent_table = Self::descend_path(root, &path[..path.len() - 1], false)?;
let key = &path[path.len() - 1];
let entry = parent_table.entry_format(key);
match entry {
Entry::Occupied(entry) => {
match entry.into_mut() {
Item::Table(ref mut t) if t.implicit => {
std::mem::swap(t, &mut table);
}
_ => return Err(duplicate_key(&path, path.len() - 1)),
}
}
Entry::Vacant(entry) => {
let item = Item::Table(table);
entry.insert(item);
}
}
}
Ok(())
}
}
impl Default for TomlParser {
fn default() -> Self {
Self {
document: Document::new(),
trailing: String::new(),
current_table_position: 0,
current_table: Table::new(),
current_is_array: false,
current_table_path: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use crate::parser::*;
use combine::stream::position::Stream;
use combine::*;
use pretty_assertions::assert_eq;
use std::fmt;
#[derive(PartialEq, Eq)]
struct PrettyString<'a>(pub(crate) &'a str);
impl<'a> fmt::Debug for PrettyString<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
macro_rules! parsed_eq {
($parsed:ident, $expected:expr) => {{
assert!($parsed.is_ok(), "{:?}", $parsed.err().unwrap());
let (v, rest) = $parsed.unwrap();
assert_eq!(v, $expected);
assert!(rest.input.is_empty());
}};
}
macro_rules! parsed_float_eq {
($input:ident, $expected:expr) => {{
let parsed = numbers::float().easy_parse(Stream::new($input.as_bytes()));
let (v, rest) = match parsed {
Ok(parsed) => parsed,
Err(err) => {
panic!("Unexpected error for {:?}: {:?}", $input, err);
}
};
if $expected.is_nan() {
assert!(v.is_nan());
} else if $expected.is_infinite() {
assert!(v.is_infinite());
assert_eq!($expected.is_sign_positive(), v.is_sign_positive());
} else {
dbg!($expected);
dbg!(v);
assert!(($expected - v).abs() < std::f64::EPSILON);
}
assert!(rest.input.is_empty());
}};
}
macro_rules! parsed_value_eq {
($input:expr) => {
let parsed = value::value().easy_parse(Stream::new($input.as_bytes()));
let (v, rest) = match parsed {
Ok(parsed) => parsed,
Err(err) => {
panic!("Unexpected error for {:?}: {:?}", $input, err);
}
};
assert_eq!(v.to_string(), *$input);
assert!(rest.input.is_empty());
};
}
macro_rules! parsed_date_time_eq {
($input:expr, $is:ident) => {{
let parsed = value::value().easy_parse(Stream::new($input.as_bytes()));
assert!(parsed.is_ok());
let (v, rest) = parsed.unwrap();
assert_eq!(v.to_string(), *$input);
assert!(rest.input.is_empty());
assert!(v.$is());
}};
}
#[test]
fn integers() {
let cases = [
("+99", 99),
("42", 42),
("0", 0),
("-17", -17),
("1_000", 1_000),
("5_349_221", 5_349_221),
("1_2_3_4_5", 1_2_3_4_5),
("0xF", 15),
("0o0_755", 493),
("0b1_0_1", 5),
(&std::i64::MIN.to_string()[..], std::i64::MIN),
(&std::i64::MAX.to_string()[..], std::i64::MAX),
];
for &(input, expected) in &cases {
let parsed = numbers::integer().easy_parse(Stream::new(input.as_bytes()));
parsed_eq!(parsed, expected);
}
let overflow = "1000000000000000000000000000000000";
let parsed = numbers::integer().easy_parse(Stream::new(overflow.as_bytes()));
assert!(parsed.is_err());
}
#[test]
fn floats() {
let cases = [
("+1.0", 1.0),
("3.1419", 3.1419),
("-0.01", -0.01),
("5e+22", 5e+22),
("1e6", 1e6),
("-2E-2", -2E-2),
("6.626e-34", 6.626e-34),
("9_224_617.445_991_228_313", 9_224_617.445_991_227),
("-1.7976931348623157e+308", std::f64::MIN),
("1.7976931348623157e+308", std::f64::MAX),
("nan", f64::NAN),
("+nan", f64::NAN),
("-nan", f64::NAN),
("inf", f64::INFINITY),
("+inf", f64::INFINITY),
("-inf", f64::NEG_INFINITY),
];
for &(input, expected) in &cases {
parsed_float_eq!(input, expected);
}
}
#[test]
fn basic_string() {
let input =
r#""I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF. \U0002070E""#;
let parsed = strings::string().easy_parse(Stream::new(input.as_bytes()));
parsed_eq!(
parsed,
"I\'m a string. \"You can quote me\". Name\tJosé\nLocation\tSF. \u{2070E}"
)
}
#[test]
fn ml_basic_string() {
let cases = [
(
r#""""
Roses are red
Violets are blue""""#,
r#"Roses are red
Violets are blue"#,
),
(r#"""" \""" """"#, " \"\"\" "),
(r#"""" \\""""#, " \\"),
];
for &(input, expected) in &cases {
dbg!(input);
let parsed = strings::string().easy_parse(Stream::new(input.as_bytes()));
parsed_eq!(parsed, expected);
}
let invalid_cases = [r#"""" """#, r#"""" \""""#];
for input in &invalid_cases {
let parsed = strings::ml_basic_string().easy_parse(Stream::new(input.as_bytes()));
assert!(parsed.is_err());
}
}
#[test]
fn ml_basic_string_escape_ws() {
let inputs = [
r#""""
The quick brown \
fox jumps over \
the lazy dog.""""#,
r#""""\
The quick brown \
fox jumps over \
the lazy dog.\
""""#,
];
for input in &inputs {
dbg!(input);
let parsed = strings::string().easy_parse(Stream::new(input.as_bytes()));
parsed_eq!(parsed, "The quick brown fox jumps over the lazy dog.");
}
let empties = [
r#""""\
""""#,
r#""""
\
\
""""#,
];
for empty in &empties {
let parsed = strings::string().easy_parse(Stream::new(empty.as_bytes()));
parsed_eq!(parsed, "");
}
}
#[test]
fn literal_string() {
let inputs = [
r#"'C:\Users\nodejs\templates'"#,
r#"'\\ServerX\admin$\system32\'"#,
r#"'Tom "Dubs" Preston-Werner'"#,
r#"'<\i\c*\s*>'"#,
];
for input in &inputs {
let parsed = strings::string().easy_parse(Stream::new(input.as_bytes()));
parsed_eq!(parsed, &input[1..input.len() - 1]);
}
}
#[test]
fn ml_literal_string() {
let input = r#"'''I [dw]on't need \d{2} apples'''"#;
let parsed = strings::string().easy_parse(Stream::new(input.as_bytes()));
parsed_eq!(parsed, &input[3..input.len() - 3]);
let input = r#"'''
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
'''"#;
let parsed = strings::string().easy_parse(Stream::new(input.as_bytes()));
parsed_eq!(parsed, &input[4..input.len() - 3]);
}
#[test]
fn offset_date_time() {
let inputs = [
"1979-05-27T07:32:00Z",
"1979-05-27T00:32:00-07:00",
"1979-05-27T00:32:00.999999-07:00",
];
for input in &inputs {
parsed_date_time_eq!(input, is_datetime);
}
}
#[test]
fn local_date_time() {
let inputs = ["1979-05-27T07:32:00", "1979-05-27T00:32:00.999999"];
for input in &inputs {
parsed_date_time_eq!(input, is_datetime);
}
}
#[test]
fn local_date() {
let inputs = ["1979-05-27", "2017-07-20"];
for input in &inputs {
parsed_date_time_eq!(input, is_datetime);
}
}
#[test]
fn local_time() {
let inputs = ["07:32:00", "00:32:00.999999"];
for input in &inputs {
parsed_date_time_eq!(input, is_datetime);
}
}
#[test]
fn trivia() {
let inputs = [
"",
r#" "#,
r#"
"#,
r#"
# comment
# comment2
"#,
r#"
"#,
r#"# comment
# comment2
"#,
];
for input in &inputs {
let parsed = trivia::ws_comment_newline().easy_parse(Stream::new(input.as_bytes()));
assert!(parsed.is_ok());
let (t, rest) = parsed.unwrap();
assert!(rest.input.is_empty());
assert_eq!(t, input.as_bytes());
}
}
#[test]
fn arrays() {
let inputs = [
r#"[]"#,
r#"[ ]"#,
r#"[
1, 2, 3
]"#,
r#"[
1,
2, # this is ok
]"#,
r#"[# comment
# comment2
]"#,
r#"[# comment
# comment2
1
#sd
,
# comment3
]"#,
r#"[1]"#,
r#"[1,]"#,
r#"[ "all", 'strings', """are the same""", '''type''']"#,
r#"[ 100, -2,]"#,
r#"[1, 2, 3]"#,
r#"[1.1, 2.1, 3.1]"#,
r#"["a", "b", "c"]"#,
r#"[ [ 1, 2 ], [3, 4, 5] ]"#,
r#"[ [ 1, 2 ], ["a", "b", "c"] ]"#,
r#"[ { x = 1, a = "2" }, {a = "a",b = "b", c = "c"} ]"#,
];
for input in &inputs {
parsed_value_eq!(input);
}
let invalid_inputs = [r#"["#, r#"[,]"#, r#"[,2]"#, r#"[1e165,,]"#];
for input in &invalid_inputs {
let parsed = array::array().easy_parse(Stream::new(input.as_bytes()));
assert!(parsed.is_err());
}
}
#[test]
fn inline_tables() {
let inputs = [
r#"{}"#,
r#"{ }"#,
r#"{a = 1e165}"#,
r#"{ hello = "world", a = 1}"#,
r#"{ hello.world = "a" }"#,
];
for input in &inputs {
parsed_value_eq!(input);
}
let invalid_inputs = [r#"{a = 1e165"#, r#"{ hello = "world", a = 2, hello = 1}"#];
for input in &invalid_inputs {
let parsed = inline_table::inline_table().easy_parse(Stream::new(input.as_bytes()));
assert!(parsed.is_err());
}
}
#[test]
fn keys() {
let cases = [
("a", "a"),
(r#""hello\n ""#, "hello\n "),
(r#"'hello\n '"#, "hello\\n "),
];
for &(input, expected) in &cases {
let parsed = key::simple_key().easy_parse(Stream::new(input.as_bytes()));
assert!(parsed.is_ok());
let ((.., k), rest) = parsed.unwrap();
assert_eq!(k.as_str(), expected);
assert_eq!(rest.input.len(), 0);
}
}
#[test]
fn values() {
let inputs = [
"1979-05-27T00:32:00.999999",
"-239",
"1e200",
"9_224_617.445_991_228_313",
r#"'''I [dw]on't need \d{2} apples'''"#,
r#"'''
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
'''"#,
r#""Jos\u00E9\n""#,
r#""\\\"\b/\f\n\r\t\u00E9\U000A0000""#,
r#"{ hello = "world", a = 1}"#,
r#"[ { x = 1, a = "2" }, {a = "a",b = "b", c = "c"} ]"#,
];
for input in &inputs {
parsed_value_eq!(input);
}
}
#[test]
fn documents() {
let documents = [
r#"
# This is a TOML document.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00 # First class dates
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# Indentation (tabs and/or spaces) is allowed but not required
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ]
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]
'some.wierd .stuff' = """
like
that
# """ # this broke my sintax highlighting
" also. like " = '''
that
'''
double = 2e39 # this number looks familiar
# trailing comment"#,
r#""#,
r#" "#,
r#" hello = 'darkness' # my old friend
"#,
r#"[parent . child]
key = "value"
"#,
r#"hello.world = "a"
"#,
];
for document in &documents {
let doc = TomlParser::parse(document.as_bytes());
let doc = match doc {
Ok(doc) => doc,
Err(err) => {
panic!(
"Parse error: {}\nFailed to parse:\n```\n{}\n```",
err, document
)
}
};
dbg!(doc.to_string());
dbg!(document);
assert_eq!(PrettyString(document), PrettyString(&doc.to_string()));
}
let invalid_inputs = [r#" hello = 'darkness' # my old friend
$"#];
for document in &invalid_inputs {
let doc = TomlParser::parse(document.as_bytes());
assert!(doc.is_err());
}
}
}