136 lines
3.9 KiB
Rust
136 lines
3.9 KiB
Rust
//! 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<Item = &'a str> {
|
|
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<Value<'_>, 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)))
|
|
}
|