#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]
#![deny(missing_docs)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::redundant_closure_for_method_calls)]
extern crate alloc;
use alloc::borrow::ToOwned;
use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use alloc::{format, vec};
use core::fmt;
use core::iter::FusedIterator;
use core::mem::take;
use core::sync::atomic::{AtomicBool, Ordering};
#[cfg(test)]
mod tests;
#[derive(Clone, Debug)]
pub struct Location {
file: Arc<str>,
line: u64,
}
impl Location {
#[must_use]
pub fn file(&self) -> &str {
&self.file
}
#[must_use]
pub fn line(&self) -> u64 {
self.line
}
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.file, self.line)
}
}
#[derive(Debug)]
struct Value {
value: Box<str>,
defined_at: Location,
read: AtomicBool,
}
#[derive(Clone, Debug, Default)]
pub struct Configuration {
items: Vec<(Box<str>, Arc<Value>)>,
}
impl fmt::Display for Configuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (key, value) in &self.items {
writeln!(f, "{key} = {value:?}")?;
}
Ok(())
}
}
#[non_exhaustive]
#[derive(Debug)]
pub enum Error {
#[cfg(feature = "std")]
IO(Box<str>, std::io::Error),
IllegalCharacter(Location, char),
InvalidUtf8(Location),
BadInt(Location, Box<str>),
BadUInt(Location, Box<str>),
BadFloat(Location, Box<str>),
BadBool(Location, Box<str>),
UnmatchedLeftBrace(Location),
InvalidKey(Location, Box<str>),
InvalidValue(Location),
InvalidLine(Location),
StrayCharsAfterString(Location),
UnterminatedString(Location, char),
InvalidEscapeSequence(Location, Box<str>),
DuplicateKey(Box<str>, Location, Location),
Multiple(Box<[Error]>),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "std")]
Self::IO(message, io_err) => write!(f, "{message}: {io_err}"),
Self::IllegalCharacter(location, c) => {
write!(f, "{location}: illegal character {c:?}")
}
Self::InvalidUtf8(location) => write!(f, "{location}: invalid UTF-8"),
Self::BadInt(location, value) => {
write!(f, "{location}: invalid integer: {value:?}")
}
Self::BadUInt(location, value) => {
write!(f, "{location}: invalid (unsigned) integer: {value:?}",)
}
Self::BadFloat(location, value) => {
write!(f, "{location}: invalid number: {value:?}")
}
Self::BadBool(location, value) => write!(
f,
"{location}: value {value:?} should be off/false/no or on/true/yes",
),
Self::UnmatchedLeftBrace(location) => {
write!(f, "{location}: Line starting with [ must end with ]")
}
Self::InvalidKey(location, key) => {
write!(f, "{location}: invalid key {key:?}")
}
Self::InvalidValue(location) => write!(f, "{location}: value contains null characters"),
Self::InvalidLine(location) => write!(
f,
"{location}: line should either start with [ or contain ="
),
Self::StrayCharsAfterString(location) => {
write!(f, "{location}: stray characters after string value")
}
Self::UnterminatedString(location, delimiter) => {
write!(f, "{location}: missing {delimiter} to close string")
}
Self::InvalidEscapeSequence(location, sequence) => write!(
f,
"{location}: invalid escape sequence {sequence:?} (try using \\\\ instead of \\ maybe?)",
),
Self::DuplicateKey(key, loc1, loc2) => {
write!(f, "{loc2}: key {key} was already defined at {loc1}")
}
Self::Multiple(errs) => {
let mut first = true;
for err in errs {
if !first {
writeln!(f)?;
}
first = false;
write!(f, "{err}")?;
}
Ok(())
}
}
}
}
impl core::error::Error for Error {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
#[cfg(feature = "std")]
if let Error::IO(_, e) = self {
return Some(e);
}
None
}
}
pub type Result<T> = core::result::Result<T, Error>;
fn parse_int(location: &Location, string: &str) -> Result<i64> {
let bad_int = || Error::BadInt(location.clone(), string.into());
let mut sign = "+";
let mut signless = string;
if let Some(s) = string.strip_prefix(['-', '+']) {
sign = &string[..1];
signless = s;
}
if signless.starts_with('+') {
return Err(bad_int());
}
let uint = parse_uint(location, signless).map_err(|_| bad_int())? as i64;
if sign == "-" { Ok(-uint) } else { Ok(uint) }
}
fn parse_uint(location: &Location, string: &str) -> Result<u64> {
let bad_uint = || Error::BadUInt(location.clone(), string.into());
if !string
.bytes()
.all(|c| c.is_ascii_hexdigit() || c == b'x' || c == b'X' || c == b'+')
{
return Err(bad_uint());
}
let signless = string.strip_prefix('+').unwrap_or(string);
let mut base = 16;
let baseless = signless
.strip_prefix("0x")
.or_else(|| signless.strip_prefix("0X"))
.unwrap_or_else(|| {
base = 10;
signless
});
if baseless.len() > 1 && baseless.starts_with('0') && base == 10 {
return Err(bad_uint());
}
for digit in baseless.bytes() {
if base == 10 && !digit.is_ascii_digit() {
return Err(bad_uint());
}
}
let val = u64::from_str_radix(baseless, base).map_err(|_| bad_uint())?;
if val >= (1u64 << 53) {
return Err(bad_uint());
}
Ok(val)
}
fn parse_float(location: &Location, string: &str) -> Result<f64> {
let bad_float = || Error::BadFloat(location.clone(), string.into());
if !string.bytes().all(|c| {
c.is_ascii_digit() || c == b'.' || c == b'+' || c == b'-' || c == b'e' || c == b'E'
}) {
return Err(bad_float());
}
for (i, c) in string.bytes().enumerate() {
if c == b'.' {
let ok = |j| string.as_bytes().get(j).is_some_and(u8::is_ascii_digit);
if !(ok(i.wrapping_sub(1)) && ok(i + 1)) {
return Err(bad_float());
}
}
}
string.parse().map_err(|_| bad_float())
}
fn parse_bool(location: &Location, string: &str) -> Result<bool> {
match string {
"yes" | "on" | "true" => Ok(true),
"no" | "off" | "false" => Ok(false),
_ => Err(Error::BadBool(location.clone(), string.into())),
}
}
fn parse_list(string: &str) -> Vec<String> {
let mut list = vec![];
let mut item = String::new();
let mut push = |item: &mut String, push_empty: bool| {
let mut item = take(item);
while item.ends_with([' ', '\t', '\n']) {
item.pop();
}
let leading_space_stripped = item.trim_start_matches([' ', '\t', '\n']);
if push_empty || !leading_space_stripped.is_empty() {
list.push(if leading_space_stripped.len() == item.len() {
item
} else {
leading_space_stripped.to_owned()
});
}
};
let mut chars = string.chars();
while let Some(c) = chars.next() {
if c == ',' {
push(&mut item, true);
item = String::new();
} else if c == '\\' {
if let Some(next) = chars.next() {
if next == ',' || next == '\\' {
item.push(next);
} else {
item.push('\\');
item.push(next);
}
} else {
item.push('\\');
break;
}
} else {
item.push(c);
}
}
push(&mut item, false);
list
}
pub trait Read {
fn read_until_lf(&mut self, line: &mut Vec<u8>) -> Result<bool>;
}
#[cfg(feature = "std")]
impl<R: std::io::BufRead> Read for R {
fn read_until_lf(&mut self, line: &mut Vec<u8>) -> Result<bool> {
self.read_until(b'\n', line)
.map_err(|e| Error::IO("read error".into(), e))?;
if line.ends_with(b"\n") {
line.pop();
Ok(true)
} else {
Ok(!line.is_empty())
}
}
}
#[cfg(not(feature = "std"))]
impl Read for &str {
fn read_until_lf(&mut self, line: &mut Vec<u8>) -> Result<bool> {
match self.split_once('\n') {
Some((pre, post)) => {
*self = post;
line.extend_from_slice(pre.as_bytes());
Ok(true)
}
None => {
if self.is_empty() {
return Ok(false);
}
line.extend_from_slice(self.as_bytes());
*self = "";
Ok(true)
}
}
}
}
#[cfg(not(feature = "std"))]
impl Read for &[u8] {
fn read_until_lf(&mut self, line: &mut Vec<u8>) -> Result<bool> {
match self.iter().position(|&c| c == b'\n') {
Some(i) => {
line.extend_from_slice(&self[..i]);
*self = &self[i + 1..];
Ok(true)
}
None => {
if self.is_empty() {
return Ok(false);
}
line.extend_from_slice(self);
*self = b"";
Ok(true)
}
}
}
}
fn is_illegal_byte(c: u8) -> bool {
(0..0x1f).contains(&c) && c != b'\t'
}
fn process_line(line_buf: &mut Vec<u8>, location: impl Fn() -> Location) -> Result<&str> {
line_buf.pop_if(|c| *c == b'\r');
for c in line_buf.iter() {
if is_illegal_byte(*c) {
return Err(Error::IllegalCharacter(location(), char::from(*c)));
}
}
str::from_utf8(line_buf).map_err(|_| Error::InvalidUtf8(location()))
}
fn parse_hex_digit(c: char) -> Option<u32> {
Some(match c {
'0'..='9' => (c as u32) - ('0' as u32),
'a'..='f' => (c as u32) - ('a' as u32) + 10,
'A'..='F' => (c as u32) - ('A' as u32) + 10,
_ => return None,
})
}
fn check_error_vec(mut errors: Vec<Error>) -> Result<()> {
match errors.len() {
0 => Ok(()),
1 => Err(errors.pop().unwrap()),
_ => Err(Error::Multiple(errors.into())),
}
}
#[derive(Default)]
struct Parser {
nonfatal_errors: Vec<Error>,
}
impl Parser {
fn check_valid_key(&mut self, location: &Location, s: &str) {
if s.is_empty()
|| s.starts_with('.')
|| s.ends_with('.')
|| s.contains("..")
|| !s.bytes().all(|c| {
c >= 0x80
|| c.is_ascii_alphanumeric()
|| matches!(c, b'.' | b'_' | b'/' | b'*' | b'-')
}) {
self.nonfatal_errors
.push(Error::InvalidKey(location.clone(), s.into()));
}
}
fn parse_escape_sequence(
&mut self,
chars: &mut core::str::Chars,
value: &mut String,
location: impl Fn() -> Location,
) {
let invalid_escape = |s: String| Error::InvalidEscapeSequence(location(), s.into());
let Some(c) = chars.next() else {
self.nonfatal_errors
.push(invalid_escape("\\(newline)".into()));
return;
};
match c {
'n' => value.push('\n'),
'r' => value.push('\r'),
't' => value.push('\t'),
'\\' | '\'' | '"' | '`' => value.push(c),
',' => value.push_str("\\,"),
'x' => {
let Some(c1) = chars.next() else {
self.nonfatal_errors.push(invalid_escape("\\x".into()));
return;
};
let Some(c2) = chars.next() else {
self.nonfatal_errors
.push(invalid_escape(format!("\\x{c1}")));
return;
};
let (Some(nibble1), Some(nibble2)) = (parse_hex_digit(c1), parse_hex_digit(c2))
else {
self.nonfatal_errors
.push(invalid_escape(format!("\\x{c1}{c2}")));
return;
};
let char_code = nibble1 << 4 | nibble2;
if char_code == 0 {
self.nonfatal_errors.push(Error::InvalidValue(location()));
}
if char_code >= 0x80 {
self.nonfatal_errors
.push(invalid_escape(format!("\\x{c1}{c2}")));
}
value.push(char::try_from(char_code).unwrap());
}
'u' => {
let mut c = chars.next();
if c != Some('{') {
self.nonfatal_errors.push(invalid_escape("\\u".into()));
return;
}
let mut code = 0u32;
for i in 0..7 {
c = chars.next();
if i == 6 {
break;
}
let Some(c) = c else {
break;
};
if c == '}' {
break;
}
code <<= 4;
let Some(digit) = parse_hex_digit(c) else {
self.nonfatal_errors
.push(invalid_escape(format!("\\u{{{code:x}{c}")));
return;
};
code |= digit;
}
if c != Some('}') {
self.nonfatal_errors
.push(invalid_escape("\\u{ has no matching }".into()));
return;
}
if code == 0 {
self.nonfatal_errors.push(Error::InvalidValue(location()));
}
let Ok(c) = char::try_from(code) else {
self.nonfatal_errors
.push(invalid_escape(format!("\\u{{{code:x}}}")));
return;
};
value.push(c);
}
_ => {
self.nonfatal_errors.push(invalid_escape(format!("\\{c}")));
}
}
}
fn read_quoted_value(
&mut self,
quoted: &str,
reader: &mut dyn Read,
start_location: &Location,
) -> Result<(String, u64)> {
let delimiter: char = quoted.chars().next().unwrap();
let mut unquoted = String::new();
let mut line_number = start_location.line;
let location = |line_number: u64| Location {
file: start_location.file.clone(),
line: line_number,
};
let mut line_buf = vec![];
let mut first = true;
loop {
let line = if first {
first = false;
"ed[1..]
} else {
line_buf.truncate(0);
if !reader.read_until_lf(&mut line_buf)? {
break;
}
line_number += 1;
process_line(&mut line_buf, || location(line_number))?
};
let mut chars = line.chars();
while let Some(c) = chars.next() {
if c == delimiter {
if !chars.all(|c| c == ' ' || c == '\t') {
self.nonfatal_errors
.push(Error::StrayCharsAfterString(location(line_number)));
}
return Ok((unquoted, line_number));
} else if c == '\\' {
self.parse_escape_sequence(&mut chars, &mut unquoted, || location(line_number));
} else if c == '\0' {
self.nonfatal_errors
.push(Error::InvalidValue(location(line_number)));
} else {
unquoted.push(c);
}
}
unquoted.push('\n');
}
Err(Error::UnterminatedString(start_location.clone(), delimiter))
}
fn load(&mut self, filename: &str, reader: &mut dyn Read) -> Result<Configuration> {
let mut items: Vec<(Box<str>, Arc<Value>)> = vec![];
let mut line: Vec<u8> = vec![];
let mut line_number: u64 = 0;
let mut current_section = String::new();
let filename: Arc<str> = filename.into();
loop {
line.truncate(0);
if !reader.read_until_lf(&mut line)? {
break;
}
if line_number == 0 && line.starts_with(b"\xEF\xBB\xBF") {
line.drain(..3);
}
line_number += 1;
let location = Location {
file: filename.clone(),
line: line_number,
};
let mut line = process_line(&mut line, || location.clone())?;
line = line.trim_start_matches(['\t', ' ']);
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') {
line = line.trim_end_matches(['\t', ' ']);
if !line.ends_with(']') {
return Err(Error::UnmatchedLeftBrace(location));
}
current_section = line[1..line.len() - 1].into();
if !current_section.is_empty() {
self.check_valid_key(&location, ¤t_section);
}
} else {
let (mut relative_key, mut value) = line
.split_once('=')
.ok_or_else(|| Error::InvalidLine(location.clone()))?;
relative_key = relative_key.trim_end_matches(['\t', ' ']);
self.check_valid_key(&location, relative_key);
let key: String = if current_section.is_empty() {
relative_key.into()
} else {
format!("{current_section}.{relative_key}")
};
value = value.trim_start_matches(['\t', ' ']);
if value.starts_with(['`', '"']) {
let (value, new_line_number) =
self.read_quoted_value(value, reader, &location)?;
items.push((
key.into(),
Arc::new(Value {
value: value.into(),
defined_at: location,
read: AtomicBool::new(false),
}),
));
line_number = new_line_number;
} else {
value = value.trim_end_matches(['\t', ' ']);
if value.contains('\0') {
return Err(Error::InvalidValue(location));
}
items.push((
key.into(),
Arc::new(Value {
value: value.into(),
defined_at: location,
read: AtomicBool::new(false),
}),
));
}
}
}
items.sort_unstable_by(|(k1, _), (k2, _)| k1.cmp(k2));
for window in items.windows(2) {
if window[0].0 == window[1].0 {
self.nonfatal_errors.push(Error::DuplicateKey(
window[0].0.clone(),
window[0].1.defined_at.clone(),
window[1].1.defined_at.clone(),
));
}
}
check_error_vec(take(&mut self.nonfatal_errors))?;
Ok(Configuration { items })
}
}
impl Configuration {
pub fn load<R: Read>(filename: &str, mut reader: R) -> Result<Self> {
Parser::default().load(filename, &mut reader)
}
#[cfg(feature = "std")]
pub fn load_path<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
let p = path.as_ref();
let filename = p.to_string_lossy();
let file = std::fs::File::open(p).map_err(|e| Error::IO(filename.clone().into(), e))?;
Configuration::load(&filename, std::io::BufReader::new(file))
}
fn binary_search_for(&self, key: &str) -> core::result::Result<usize, usize> {
self.items.binary_search_by(|(k, _)| k.as_ref().cmp(key))
}
fn subkey_start_idx(&self, key: &str) -> usize {
let key_dot = format!("{key}.");
self.binary_search_for(&key_dot)
.expect_err("items should not contain a key ending in .")
}
fn subkey_end_idx(&self, key: &str) -> usize {
let key_slash = format!("{key}/");
self.binary_search_for(&key_slash).unwrap_or_else(|x| x)
}
#[must_use]
pub fn section(&self, key: &str) -> Configuration {
let start_idx = self.subkey_start_idx(key);
let end_idx = self.subkey_end_idx(key);
Configuration {
items: self.items[start_idx..end_idx]
.iter()
.map(|(k, v)| (k[key.len() + 1..].into(), v.clone()))
.collect(),
}
}
pub fn keys(&self) -> Keys<'_> {
Keys {
iter: self.items.iter(),
prev: None,
}
}
pub fn iter(&self) -> ConfigurationIter<'_> {
self.into_iter()
}
fn get_val(&self, key: &str, mark_read: bool) -> Option<&Value> {
let idx = self.binary_search_for(key).ok()?;
let v = &self.items[idx].1;
if mark_read {
v.read.store(true, Ordering::Relaxed);
}
Some(v)
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&str> {
Some(self.get_val(key, true)?.value.as_ref())
}
#[must_use]
pub fn location(&self, key: &str) -> Option<Location> {
if let Some(val) = self.get_val(key, false) {
Some(val.defined_at.clone())
} else {
let start_idx = self.subkey_start_idx(key);
let end_idx = self.subkey_end_idx(key);
self.items[start_idx..end_idx]
.iter()
.map(|(_, value)| &value.defined_at)
.min_by_key(|loc| loc.line)
.cloned()
}
}
#[must_use]
pub fn has(&self, key: &str) -> bool {
self.get_val(key, false).is_some()
}
#[must_use]
pub fn get_or_default<'a>(&'a self, key: &str, default: &'a str) -> &'a str {
self.get(key).unwrap_or(default)
}
#[must_use]
pub fn get_int(&self, key: &str) -> Option<Result<i64>> {
let Value {
value, defined_at, ..
} = self.get_val(key, true)?;
Some(parse_int(defined_at, value.as_ref()))
}
pub fn get_int_or_default(&self, key: &str, default: i64) -> Result<i64> {
self.get_int(key).unwrap_or(Ok(default))
}
#[must_use]
pub fn get_uint(&self, key: &str) -> Option<Result<u64>> {
let Value {
value, defined_at, ..
} = self.get_val(key, true)?;
Some(parse_uint(defined_at, value.as_ref()))
}
pub fn get_uint_or_default(&self, key: &str, default: u64) -> Result<u64> {
self.get_uint(key).unwrap_or(Ok(default))
}
#[must_use]
pub fn get_float(&self, key: &str) -> Option<Result<f64>> {
let Value {
value, defined_at, ..
} = self.get_val(key, true)?;
Some(parse_float(defined_at, value.as_ref()))
}
pub fn get_float_or_default(&self, key: &str, default: f64) -> Result<f64> {
self.get_float(key).unwrap_or(Ok(default))
}
#[must_use]
pub fn get_bool(&self, key: &str) -> Option<Result<bool>> {
let Value {
value, defined_at, ..
} = self.get_val(key, true)?;
Some(parse_bool(defined_at, value.as_ref()))
}
pub fn get_bool_or_default(&self, key: &str, default: bool) -> Result<bool> {
self.get_bool(key).unwrap_or(Ok(default))
}
#[must_use]
pub fn get_list(&self, key: &str) -> Option<Vec<String>> {
let value = &self.get_val(key, true)?.value;
Some(parse_list(value.as_ref()))
}
pub fn get_list_or_default<L>(&self, key: &str, default: L) -> Vec<String>
where
L: IntoIterator,
String: From<L::Item>,
{
self.get_list(key)
.unwrap_or_else(|| default.into_iter().map(String::from).collect())
}
pub fn get_list_or_empty(&self, key: &str) -> Vec<String> {
let empty: [&'static str; 0] = [];
self.get_list_or_default(key, empty)
}
pub fn merge(&mut self, conf: &Configuration) {
let mut must_sort = false;
for (key, value) in &conf.items {
if let Ok(i) = self.binary_search_for(key) {
self.items[i].1 = value.clone();
} else {
self.items.push((key.clone(), value.clone()));
must_sort = true;
}
}
if must_sort {
self.items.sort_unstable_by(|(k1, _), (k2, _)| k1.cmp(k2));
}
}
pub fn unread_keys(&self) -> UnreadKeys<'_> {
UnreadKeys(self.items.iter())
}
}
type ItemsIter<'a> = core::slice::Iter<'a, (Box<str>, Arc<Value>)>;
#[derive(Clone, Debug)]
pub struct ConfigurationIter<'a>(ItemsIter<'a>);
impl<'a> Iterator for ConfigurationIter<'a> {
type Item = (&'a str, &'a str);
fn next(&mut self) -> Option<Self::Item> {
let (key, val) = self.0.next()?;
Some((key, val.value.as_ref()))
}
}
impl FusedIterator for ConfigurationIter<'_> {}
#[derive(Clone, Debug)]
pub struct Keys<'a> {
prev: Option<&'a str>,
iter: ItemsIter<'a>,
}
impl<'a> Iterator for Keys<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
loop {
let (key, _) = self.iter.next()?;
let first_component: &str = key.split_once('.').map_or(key.as_ref(), |(c, _)| c);
if self.prev != Some(first_component) {
self.prev = Some(first_component);
return Some(first_component);
}
}
}
}
impl FusedIterator for Keys<'_> {}
#[derive(Clone, Debug)]
pub struct UnreadKeys<'a>(ItemsIter<'a>);
impl<'a> Iterator for UnreadKeys<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
loop {
let (k, v) = self.0.next()?;
if !v.read.load(Ordering::Relaxed) {
return Some(k.as_ref());
}
}
}
}
impl FusedIterator for UnreadKeys<'_> {}
impl<'a> IntoIterator for &'a Configuration {
type IntoIter = ConfigurationIter<'a>;
type Item = (&'a str, &'a str);
fn into_iter(self) -> Self::IntoIter {
ConfigurationIter(self.items.iter())
}
}