/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use std::{ collections::{BTreeMap, btree_map::Entry}, iter::Peekable, str::Chars, }; use super::{Config, Result}; use std::fmt::Write; const MAX_NEST_LEVEL: usize = 10; // Simple TOML parser for Stalwart Server configuration files. impl Config { pub fn new(toml: impl AsRef) -> Result { let mut config = Config::default(); config.parse(toml.as_ref())?; Ok(config) } pub fn parse(&mut self, toml: &str) -> Result<()> { let mut parser = TomlParser::new(&mut self.keys, toml); let mut table_name = String::new(); let mut last_array_name = String::new(); let mut last_array_pos = 0; while parser.seek_next_char() { match parser.peek_char()? { '[' => { parser.next_char(true, false)?; table_name.clear(); let mut is_array = match parser.next_char(true, false)? { '[' => true, ch => { table_name.push(ch); false } }; let mut in_quote = false; let mut last_ch = char::from(0); loop { let ch = parser.next_char(!in_quote, false)?; match ch { '\"' if !in_quote || last_ch != '\\' => { in_quote = !in_quote; } '\\' if in_quote => (), ']' if !in_quote => { if table_name.is_empty() { return Err(format!( "Empty table name at line {}.", parser.line )); } if is_array { if table_name == last_array_name { last_array_pos += 1; } else { last_array_pos = 0; last_array_name = table_name.to_string(); } is_array = false; write!(table_name, ".{last_array_pos:04}").ok(); } else { break; } } _ => { if !in_quote { if ch.is_alphanumeric() || ['.', '-', '_'].contains(&ch) { table_name.push(ch.to_ascii_lowercase()); } else { return Err(format!( "Unexpected character {:?} at line {}.", ch, parser.line )); } } else { table_name.push(ch); } } } last_ch = ch; } parser.skip_line(); } 'a'..='z' | 'A'..='Z' | '0'..='9' | '\"' => { let (key, _) = parser.key( if !table_name.is_empty() { format!("{table_name}.") } else { String::with_capacity(10) }, false, )?; parser.value(key, &['\n'], 0)?; } '#' => { parser.skip_line(); } ch => { let ch = *ch; return Err(format!( "Unexpected character {:?} at line {}.", ch, parser.line )); } } } Ok(()) } } struct TomlParser<'x, 'y> { keys: &'y mut BTreeMap, iter: Peekable>, line: usize, } impl<'x, 'y> TomlParser<'x, 'y> { fn new(keys: &'y mut BTreeMap, toml: &'x str) -> Self { Self { keys, iter: toml.chars().peekable(), line: 1, } } fn seek_next_char(&mut self) -> bool { while let Some(ch) = self.iter.peek() { match ch { '\n' => { self.iter.next(); self.line += 1; } '\r' | ' ' | '\t' => { self.iter.next(); } '#' => { self.skip_line(); } _ => { return true; } } } false } fn peek_char(&mut self) -> Result<&char> { self.iter.peek().ok_or_else(|| "".to_string()) } fn next_char(&mut self, skip_wsp: bool, allow_lf: bool) -> Result { for ch in &mut self.iter { match ch { '\r' => (), ' ' | '\t' if skip_wsp => (), '\n' => { return if allow_lf { self.line += 1; Ok(ch) } else { Err(format!("Unexpected end of line at line: {}", self.line)) }; } _ => { return Ok(ch); } } } Err(format!("Unexpected EOF at line: {}", self.line)) } fn skip_line(&mut self) { for ch in &mut self.iter { if ch == '\n' { self.line += 1; break; } } } #[allow(clippy::while_let_on_iterator)] fn key(&mut self, mut key: String, in_curly: bool) -> Result<(String, char)> { let start_key_len = key.len(); while let Some(ch) = self.iter.next() { match ch { '=' => { if start_key_len != key.len() { return Ok((key, ch)); } else { return Err(format!("Empty key at line: {}", self.line)); } } ',' | '}' if in_curly => { if start_key_len != key.len() { return Ok((key, ch)); } else { return Err(format!("Empty key at line: {}", self.line)); } } /*'a'..='z' | '.' | 'A'..='Z' | '0'..='9' | '_' | '-' => { key.push(ch); }*/ '\"' => { let mut last_ch = char::from(0); while let Some(ch) = self.iter.next() { match ch { '\\' => (), '\"' if last_ch != '\\' => { break; } '\n' => { return Err(format!( "Unexpected end of line while parsing quoted key at line: {}", self.line )); } _ => { key.push(ch); } } last_ch = ch; } } ' ' | '\t' | '\r' => (), '\n' => { if start_key_len == key.len() { self.line += 1; } else { return Err(format!( "Unexpected end of line while parsing key {:?} at line: {}", key, self.line )); } } _ => { key.push(ch); } } } Err(format!("Unexpected EOF at line: {}", self.line)) } fn value(&mut self, key: String, stop_chars: &[char], nest_level: usize) -> Result { if nest_level == MAX_NEST_LEVEL { return Err(format!("Too many nested structures at line {}.", self.line)); } match self.next_char(true, false)? { '[' => { let mut array_pos = 0; self.seek_next_char(); loop { match self.value( format!("{key}.{array_pos:04}"), &[',', ']'], nest_level + 1, )? { ',' => { self.seek_next_char(); array_pos += 1; } ']' => break, ch => { return Err(format!( "Unexpected character {:?} found in array for property {:?} at line {}.", ch, key, self.line )); } } } } '{' => { let base_key = format!("{key}."); let base_key_len = base_key.len(); loop { let (sub_key, stop_char) = self.key(base_key.clone(), true)?; match stop_char { '=' => { // Key value self.seek_next_char(); match self.value(sub_key, &[',', '}'], nest_level + 1)? { ',' => { self.seek_next_char(); } '}' => break, ch => { return Err(format!( "Unexpected character {:?} found in inline table for property {:?} at line {}.", ch, key, self.line )); } } } ',' => { // Set if sub_key.len() > base_key_len { self.insert_key(sub_key, String::new())?; } } '}' => { // Set if sub_key.len() > base_key_len { self.insert_key(sub_key, String::new())?; } break; } _ => unreachable!(), } } } qch @ ('\'' | '\"') => { let mut value = String::new(); if matches!(self.iter.peek(), Some(ch) if ch == &qch) { self.iter.next(); if matches!(self.iter.peek(), Some(ch) if ch == &qch) { self.iter.next(); if matches!(self.iter.peek(), Some(ch) if ch == &'\n') { self.iter.next(); self.line += 1; } let mut last_ch = char::from(0); let mut prev_last_ch = char::from(0); loop { let ch = self.next_char(false, true)?; if !(ch == qch && last_ch == qch && prev_last_ch == qch) { value.push(ch); prev_last_ch = last_ch; last_ch = ch; } else { value.truncate(value.len() - 2); break; } } } } else { let mut last_ch = char::from(0); loop { let ch = self.next_char(false, true)?; match ch { '\\' if last_ch != '\\' => (), 't' if last_ch == '\\' => { value.push('\t'); } 'r' if last_ch == '\\' => { value.push('\r'); } 'n' if last_ch == '\\' => { value.push('\n'); } ch => { if ch != qch || last_ch == '\\' { value.push(ch); } else { break; } } } last_ch = ch; } } self.insert_key(key, value)?; } ch if ch.is_alphanumeric() || ['.', '+', '-'].contains(&ch) => { let mut value = String::with_capacity(4); value.push(ch); while let Some(ch) = self.iter.peek() { if ch.is_alphanumeric() || ['.', '+', '-'].contains(ch) { value.push(self.next_char(true, false)?); } else { break; } } self.insert_key(key, value)?; } ch => { return if stop_chars.contains(&ch) { Ok(ch) } else { Err(format!( "Expected {:?} but found {:?} in value at line {}.", stop_chars, ch, self.line )) }; } } loop { match self.next_char(true, true)? { '#' => { self.skip_line(); if stop_chars.contains(&'\n') { return Ok('\n'); } } ch if stop_chars.contains(&ch) => { return Ok(ch); } '\n' if !stop_chars.contains(&'\n') => (), ch => { return Err(format!( "Expected {:?} but found {:?} in value at line {}.", stop_chars, ch, self.line )); } } } } fn insert_key(&mut self, key: String, mut value: String) -> Result<()> { match self.keys.entry(key) { Entry::Vacant(e) => { value.shrink_to_fit(); e.insert(value); Ok(()) } Entry::Occupied(e) => Err(format!( "Duplicate key {:?} at line {}.", e.key(), self.line )), } } } #[cfg(test)] mod tests { use std::{collections::BTreeMap, fs, path::PathBuf}; use crate::config::Config; #[test] fn toml_parse() { let file = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .parent() .unwrap() .to_path_buf() .join("tests") .join("resources") .join("smtp") .join("config") .join("toml-parser.toml"); let mut config = Config::default(); config.parse(&fs::read_to_string(file).unwrap()).unwrap(); let expected = BTreeMap::from_iter( [ ("arrays.colors.0000", "red"), ("arrays.colors.0001", "yellow"), ("arrays.colors.0002", "green"), ("arrays.contributors.0000", "Foo Bar "), ("arrays.contributors.0001.email", "bazqux@example.com"), ("arrays.contributors.0001.name", "Baz Qux"), ("arrays.contributors.0001.url", "https://example.com/bazqux"), ("arrays.integers.0000", "1"), ("arrays.integers.0001", "2"), ("arrays.integers.0002", "3"), ("arrays.integers2.0000", "1"), ("arrays.integers2.0001", "2"), ("arrays.integers2.0002", "3"), ("arrays.integers3.0000", "4"), ("arrays.integers3.0001", "5"), ("arrays.nested_arrays_of_ints.0000.0000", "1"), ("arrays.nested_arrays_of_ints.0000.0001", "2"), ("arrays.nested_arrays_of_ints.0001.0000", "3"), ("arrays.nested_arrays_of_ints.0001.0001", "4"), ("arrays.nested_arrays_of_ints.0001.0002", "5"), ("arrays.nested_mixed_array.0000.0000", "1"), ("arrays.nested_mixed_array.0000.0001", "2"), ("arrays.nested_mixed_array.0001.0000", "a"), ("arrays.nested_mixed_array.0001.0001", "b"), ("arrays.nested_mixed_array.0001.0002", "c"), ("arrays.numbers.0000", "0.1"), ("arrays.numbers.0001", "0.2"), ("arrays.numbers.0002", "0.5"), ("arrays.numbers.0003", "1"), ("arrays.numbers.0004", "2"), ("arrays.numbers.0005", "5"), ("arrays.string_array.0000", "all"), ("arrays.string_array.0001", "strings"), ("arrays.string_array.0002", "are the same"), ("arrays.string_array.0003", "type"), ("database.data.0000.0000", "delta"), ("database.data.0000.0001", "phi"), ("database.data.0001.0000", "3.14"), ("database.enabled", "true"), ("database.ports.0000", "8000"), ("database.ports.0001", "8001"), ("database.ports.0002", "8002"), ("database.temp_targets.case", "72.0"), ("database.temp_targets.cpu", "79.5"), ("products.0000.name", "Hammer"), ("products.0000.sku", "738594937"), ("products.0002.color", "gray"), ("products.0002.name", "Nail"), ("products.0002.sku", "284758393"), ("servers.127.0.0.1", "value"), ("servers.alpha.ip", "10.0.0.1"), ("servers.alpha.role", "frontend"), ("servers.beta.ip", "10.0.0.2"), ("servers.beta.role", "backend"), ("servers.character encoding", "value"), ( "strings.my \"string\" test.lines", concat!( "The first newline is\ntrimmed in raw strings.\n", "All other whitespace\nis preserved.\n" ), ), ("strings.my \"string\" test.str1", "I'm a string."), ("strings.my \"string\" test.str2", "You can \"quote\" me."), ("strings.my \"string\" test.str3", "Name\tTabs\nNew Line."), ("env.var1", "utils"), ("env.var2", "utils"), ("sets.integer.1", ""), ("sets.integers.1", ""), ("sets.integers.2", ""), ("sets.integers.3", ""), ("sets.string.red", ""), ("sets.strings.red", ""), ("sets.strings.yellow", ""), ("sets.strings.green", ""), ] .map(|(k, v)| (k.to_string(), v.to_string())), ); if config.keys != expected { for (key, value) in &config.keys { if let Some(expected_value) = expected.get(key) { if value != expected_value { panic!( "Expected value {:?} for key {:?} but found {:?}.", expected_value, key, value ); } } else { panic!( "Unexpected key {:?} found in config with value {:?}.", key, value ); } } for (key, value) in &expected { if let Some(config_value) = config.keys.get(key) { if value != config_value { panic!( "Expected value {:?} for key {:?} but found {:?}.", value, key, config_value ); } } else { panic!( "Expected key {:?} not found in config with value {:?}.", key, value ); } } } assert_eq!( config.set_values("sets.strings").collect::>(), vec!["green", "red", "yellow"] ); assert_eq!( config.sub_keys("sets.strings", ""), vec!["green", "red", "yellow"] ); assert_eq!(config.sub_keys("sets", ".red"), vec!["string", "strings"]); } }