//! The strict TOML subset manifests and decisions are written in: //! `[table]` headers, `key = "string"`, `key = ["a", "b"]` on one line, //! `#` comments. Strings are printable ASCII without `"` or `\`. use crate::Error; /// One `key = value` line, with the table it is in. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Entry<'a> { pub line: usize, pub table: &'a str, pub key: &'a str, pub value: Value<'a>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Value<'a> { Str(&'a str), /// A one-line array of strings, already checked. List(List<'a>), } /// The inside of `[...]`: iterate it for its strings. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct List<'a>(&'a str); impl<'a> List<'a> { pub fn iter(&self) -> impl Iterator { self.0 .split(',') .map(str::trim) .filter(|s| !s.is_empty()) .map(|s| &s[1..s.len() - 1]) } } /// A table may be opened only once. const MAX_TABLES: usize = 8; /// Calls `entry` with every `key = value` in `text`, in order; stops at /// the first malformed line or the first error `entry` returns. pub fn parse<'a>( text: &'a str, mut entry: impl FnMut(Entry<'a>) -> Result<(), Error>, ) -> Result<(), Error> { let mut table = ""; let mut seen = [""; MAX_TABLES]; let mut tables = 0; for (i, raw) in text.lines().enumerate() { let line = i + 1; let content = strip_comment(raw, line)?.trim(); if content.is_empty() { continue; } if let Some(name) = content.strip_prefix('[') { let name = name .strip_suffix(']') .filter(|n| is_key(n)) .ok_or(Error::Syntax { line })?; if seen[..tables].contains(&name) { return Err(Error::Duplicate { line }); } *seen.get_mut(tables).ok_or(Error::Syntax { line })? = name; tables += 1; table = name; continue; } let (key, value) = content.split_once('=').ok_or(Error::Syntax { line })?; let (key, value) = (key.trim(), value.trim()); if table.is_empty() || !is_key(key) { return Err(Error::Syntax { line }); } entry(Entry { line, table, key, value: value_of(value, line)?, })?; } Ok(()) } /// The line up to a `#` outside a string. fn strip_comment(raw: &str, line: usize) -> Result<&str, Error> { let mut in_string = false; for (i, c) in raw.char_indices() { match c { '"' => in_string = !in_string, '#' if !in_string => return Ok(&raw[..i]), _ => {} } } if in_string { Err(Error::Syntax { line }) } else { Ok(raw) } } fn is_key(s: &str) -> bool { !s.is_empty() && s.bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') } fn string(s: &str, line: usize) -> Result<&str, Error> { let inner = s .strip_prefix('"') .and_then(|s| s.strip_suffix('"')) .ok_or(Error::Syntax { line })?; let printable = inner .bytes() .all(|b| (b' '..=b'~').contains(&b) && b != b'"' && b != b'\\'); if printable { Ok(inner) } else { Err(Error::Syntax { line }) } } fn value_of(s: &str, line: usize) -> Result, Error> { let Some(inner) = s.strip_prefix('[') else { return string(s, line).map(Value::Str); }; let inner = inner.strip_suffix(']').ok_or(Error::Syntax { line })?; let items: &str = inner.trim(); // A trailing comma is allowed; an empty item is not. let body = items.strip_suffix(',').unwrap_or(items); if !body.trim().is_empty() { for item in body.split(',') { string(item.trim(), line)?; } } Ok(Value::List(List(body))) }