#![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; use alloc::borrow::Cow; #[cfg(not(feature = "std"))] use alloc::collections::BTreeMap as Map; use alloc::sync::Arc; use core::fmt; use core::mem::take; #[cfg(feature = "std")] use std::collections::HashMap as Map; /// File and line information #[derive(Clone, Debug)] pub struct Location { file: Arc, line: u64, } impl fmt::Display for Location { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}:{}", self.file, self.line) } } /// A string value, together with location information about where it is defined. #[derive(Clone, Debug)] struct Value { value: Box, defined_at: Location, } #[derive(Clone, Debug, Default)] pub struct Configuration { // wrap in an Arc for cheap cloning children: Arc, Configuration>>, values: Arc, Value>>, } impl fmt::Display for Configuration { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut lines = vec![]; fn format_value(val: &str) -> Cow { if val.chars().all(|c| !c.is_ascii_control()) && !val.starts_with(['\'', '"', '`']) && !val.starts_with(char::is_whitespace) { return Cow::Borrowed(val); } let mut quoted = String::from("\""); for c in val.chars() { if c == '"' { quoted.push_str("\\\""); } else if c == '\n' { quoted.push_str("\\n"); } else if c == '\r' { quoted.push_str("\\r"); } else if c == '\t' { quoted.push_str("\\t"); } else if c == '\\' { quoted.push('\\'); } else if c.is_ascii_control() { quoted.push_str(&format!("\\x{:02x}", c as u32)); } else { quoted.push(c); } } quoted.push('"'); Cow::Owned(quoted) } fn add_lines(lines: &mut Vec, prefix: &str, conf: &Configuration) { for (key, val) in conf.values.iter() { lines.push(format!("{prefix}{key}: {}", format_value(&val.value))); } for (key, child) in conf.children.iter() { add_lines(lines, &format!("{prefix}{key}."), child); } } add_lines(&mut lines, "", self); lines.sort(); for line in lines { writeln!(f, "{line}")?; } Ok(()) } } #[non_exhaustive] #[derive(Debug)] pub enum Error { #[cfg(feature = "std")] IO(Box, std::io::Error), IllegalCharacter(Location, char), InvalidUtf8(Location), BadInt(Location, Box), BadUInt(Location, Box), BadFloat(Location, Box), BadBool(Location, Box), UnmatchedLeftBrace(Location), InvalidKey(Location, Box), InvalidValue(Location), InvalidLine(Location), StrayCharsAfterQuotedString(Location), UnterminatedString(Location, char), InvalidEscapeSequence(Location, Box), /// Used when there is more than one error in a file. /// /// None of the errors in the array will be [`Error::Multiple`]'s, /// and the array will contain at least two elements. Multiple(Box<[Error]>), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { 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}: [ has no matching ]"), 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::StrayCharsAfterQuotedString(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::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 std::error::Error + 'static)> { if let Error::IO(_, e) = self { Some(e) } else { None } } } pub type Result = std::result::Result; fn parse_int(location: &Location, string: &str) -> Result { let bad_int = || Error::BadInt(location.clone(), string.into()); if !string .bytes() .all(|c| c.is_ascii_hexdigit() || c == b'x' || c == b'X' || c == b'-' || c == b'+') { return Err(bad_int()); } let signless = string.strip_prefix(['-', '+']).unwrap_or(string); let mut base = 10; let baseless = signless .strip_prefix("0x") .or_else(|| signless.strip_prefix("0X")) .unwrap_or_else(|| { base = 16; signless }); for digit in baseless.bytes() { if base == 10 && !digit.is_ascii_digit() { return Err(bad_int()); } } string.parse().map_err(|_| bad_int()) } fn parse_uint(location: &Location, string: &str) -> Result { 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 = 10; let baseless = signless .strip_prefix("0x") .or_else(|| signless.strip_prefix("0X")) .unwrap_or_else(|| { base = 16; signless }); for digit in baseless.bytes() { if base == 10 && !digit.is_ascii_digit() { return Err(bad_uint()); } } let val = signless.parse().map_err(|_| bad_uint())?; if val > i64::MAX as u64 { return Err(bad_uint()); } Ok(val) } fn parse_float(location: &Location, string: &str) -> Result { 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()); } string.parse().map_err(|_| bad_float()) } fn parse_bool(location: &Location, string: &str) -> Result { match string { "yes" | "on" | "true" => Ok(true), "no" | "off" | "false" => Ok(false), _ => Err(Error::BadBool(location.clone(), string.into())), } } fn parse_list(_location: &Location, _string: &str) -> Vec { todo!() } /// Trait for reading configurations. /// /// Ordinarily you won't need to implement this trait, since it is /// already implemented by any `T: `[`std::io::BufRead`] (or else `&str` and `&[u8]`, /// if the `std` feature is not enabled). pub trait Read { /// Read up to the next line feed (or EOF), not including the line feed itself. /// /// Puts the line in `line` and returns `Ok(true)`, if the end of file has been reached, /// `line` is unmodified and `Ok(false)` is returned. /// /// You don't need to check for valid UTF-8 here — that is already done in the code which uses /// this trait. fn read_until_lf(&mut self, line: &mut Vec) -> Result; } #[cfg(feature = "std")] impl Read for R { fn read_until_lf(&mut self, line: &mut Vec) -> Result { 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) -> Result { 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) -> Result { 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) } } } } #[derive(Default)] struct Parser { nonfatal_errors: Vec, } impl Parser { fn check_valid_key(&mut self, location: &Location, s: &str) { if !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())) } } /// Returns (unquoted value, new line number) 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; quoted } else { line_buf.truncate(0); if !reader.read_until_lf(&mut line_buf)? { break; } line_number += 1; line_buf.pop_if(|c| *c == b'\r'); str::from_utf8(&line_buf).map_err(|_| Error::InvalidUtf8(location(line_number)))? }; let mut chars = line.chars(); while let Some(c) = chars.next() { let invalid_escape = move |s: String| Error::InvalidEscapeSequence(location(line_number), s.into()); if c == delimiter { if !chars.all(|c| c == ' ' || c == '\t') { self.nonfatal_errors .push(Error::StrayCharsAfterQuotedString(location(line_number))); } return Ok((unquoted, line_number)); } else if c == '\\' { let Some(c) = chars.next() else { self.nonfatal_errors .push(invalid_escape("\\(newline)".into())); break; }; fn parse_hex_digit(c: char) -> Option { 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, }) } match c { 'n' => unquoted.push('\n'), 'r' => unquoted.push('\r'), 't' => unquoted.push('\t'), '\\' | '\'' | '"' | '`' => unquoted.push(c), ',' => unquoted.push_str("\\,"), 'x' => { let Some(c1) = chars.next() else { self.nonfatal_errors.push(invalid_escape("\\x".into())); break; }; let Some(c2) = chars.next() else { self.nonfatal_errors .push(invalid_escape(format!("\\x{c1}"))); break; }; let (Some(nibble1), Some(nibble2)) = (parse_hex_digit(c1), parse_hex_digit(c2)) else { self.nonfatal_errors .push(invalid_escape(format!("\\x{c1}{c2}"))); continue; }; if nibble1 == 0 && nibble2 == 0 { self.nonfatal_errors .push(Error::InvalidValue(location(line_number))); } unquoted.push(char::try_from(nibble1 << 8 | nibble2).unwrap()); } 'u' => { let mut c = chars.next(); if c != Some('{') { self.nonfatal_errors.push(invalid_escape("\\u".into())); continue; } 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; code |= parse_hex_digit(c) .ok_or_else(|| invalid_escape(format!("\\u{{{code:x}{c}")))?; } if c != Some('}') { self.nonfatal_errors .push(invalid_escape("\\u{ has no matching }".into())); continue; } let Ok(c) = char::try_from(code) else { self.nonfatal_errors .push(invalid_escape(format!("\\u{{{code:x}}}"))); continue; }; unquoted.push(c); } _ => { self.nonfatal_errors.push(invalid_escape(format!("\\{c}"))); } } } else if c == '\0' { self.nonfatal_errors .push(Error::InvalidValue(location(line_number))); } else { unquoted.push(c); } } } Err(Error::UnterminatedString(start_location.clone(), delimiter)) } fn load(&mut self, filename: &str, reader: &mut dyn Read) -> Result { let mut config = Configuration::default(); let mut line = vec![]; let mut line_number = 0; let mut current_section = &mut config; let filename: Arc = filename.into(); loop { line.truncate(0); if !reader.read_until_lf(&mut line)? { break; } line_number += 1; let location = Location { file: filename.clone(), line: line_number, }; line.pop_if(|c| *c == b'\r'); for c in &line { if (0..0x1f).contains(c) && *c != b'\t' { return Err(Error::IllegalCharacter(location, char::from(*c))); } } let mut line = str::from_utf8(&line).map_err(|_| Error::InvalidUtf8(location.clone()))?; line = line.trim_start_matches(['\t', ' ']); if line.is_empty() || line.starts_with('#') { // comment/blank line continue; } if line.starts_with('[') { line = line.trim_end_matches(['\t', ' ']); if !line.ends_with(']') { return Err(Error::UnmatchedLeftBrace(location)); } let new_section = line[1..line.len() - 1].into(); current_section = &mut config; self.check_valid_key(&location, new_section); if !new_section.is_empty() { for component in new_section.split('.') { current_section = Arc::get_mut(&mut current_section.children) .unwrap() .entry(component.into()) .or_default(); } } } 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); value = value.trim_start_matches(['\t', ' ']); fn insert( mut section: &mut Configuration, location: Location, mut key: &str, value: &str, ) { if let Some(last_dot) = key.rfind('.') { for component in key[..last_dot].split('.') { section = Arc::get_mut(&mut section.children) .unwrap() .entry(component.into()) .or_default() } key = &key[last_dot + 1..]; } Arc::get_mut(&mut section.values).unwrap().insert( key.into(), Value { value: value.into(), defined_at: location, }, ); } if value.starts_with(['`', '"', '\'']) { let (value, new_line_number) = self.read_quoted_value(value, reader, &location)?; insert(current_section, location, relative_key, &value); line_number = new_line_number; } else { value = value.trim_end_matches(['\t', ' ']); if value.contains('\0') { return Err(Error::InvalidValue(location)); } insert(current_section, location, relative_key, value); } } } match self.nonfatal_errors.len() { 0 => Ok(config), 1 => Err(self.nonfatal_errors.pop().unwrap()), 2.. => Err(Error::Multiple(take(&mut self.nonfatal_errors).into())), } } } impl Configuration { pub fn load(filename: &str, mut reader: R) -> Result { // avoid big code size by using dyn reference. // the impact on performance is not really important. Parser::default().load(filename, &mut reader) } #[cfg(feature = "std")] pub fn load_path>(path: P) -> Result { 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)) } pub fn section(&self, key: &str) -> Configuration { let mut node = self; for component in key.split('.') { node = match self.children.get(component) { Some(x) => x, None => return Configuration::default(), }; } node.clone() } pub fn keys(&self) -> impl '_ + Iterator { self.values .keys() .chain( self.children .keys() .filter(|&k| !self.values.contains_key(k)), ) .map(|x| x.as_ref()) } fn get_val(&self, key: &str) -> Option<&Value> { let Some(last_dot) = key.rfind('.') else { return self.values.get(key); }; let (path, last_component) = key.split_at(last_dot); let mut node = self; for component in path.split('.') { node = self.children.get(component)?; } node.values.get(last_component) } pub fn get(&self, key: &str) -> Option<&str> { Some(self.get_val(key)?.value.as_ref()) } pub fn location(&self, key: &str) -> Option { Some(self.get_val(key)?.defined_at.clone()) } pub fn has(&self, key: &str) -> bool { self.get(key).is_some() } pub fn get_or_default<'a>(&'a self, key: &str, default: &'a str) -> &'a str { self.get(key).unwrap_or(default) } pub fn get_int(&self, key: &str) -> Option> { let Value { value, defined_at } = self.get_val(key)?; Some(parse_int(defined_at, value.as_ref())) } pub fn get_int_or_default(&self, key: &str, default: i64) -> Result { self.get_int(key).unwrap_or(Ok(default)) } pub fn get_uint(&self, key: &str) -> Option> { let Value { value, defined_at } = self.get_val(key)?; Some(parse_uint(defined_at, value.as_ref())) } pub fn get_uint_or_default(&self, key: &str, default: u64) -> Result { self.get_uint(key).unwrap_or(Ok(default)) } pub fn get_float(&self, key: &str) -> Option> { let Value { value, defined_at } = self.get_val(key)?; Some(parse_float(defined_at, value.as_ref())) } pub fn get_float_or_default(&self, key: &str, default: f64) -> Result { self.get_float(key).unwrap_or(Ok(default)) } pub fn get_bool(&self, key: &str) -> Option> { let Value { value, defined_at } = self.get_val(key)?; Some(parse_bool(defined_at, value.as_ref())) } pub fn get_bool_or_default(&self, key: &str, default: bool) -> Result { self.get_bool(key).unwrap_or(Ok(default)) } pub fn get_list(&self, key: &str) -> Option> { let Value { value, defined_at } = self.get_val(key)?; Some(parse_list(defined_at, value.as_ref())) } pub fn get_list_or_default( &self, key: &str, default: impl FnOnce() -> Vec, ) -> Vec { self.get_list(key).unwrap_or_else(default) } /// Merge `conf` into `self`, preferring values in `conf`. pub fn merge(&mut self, conf: &Configuration) { let new_values = Arc::make_mut(&mut self.values); // merge conf.values into self.values for (key, val) in conf.values.iter() { new_values.insert(key.clone(), val.clone()); } // merge conf.children into self.children let new_children = Arc::make_mut(&mut self.children); for (key, child) in conf.children.iter() { new_children.entry(key.clone()).or_default().merge(child) } } }