Files
k-os/crates/manifest/src/toml.rs

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)))
}