#![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; #[cfg(not(feature = "std"))] use alloc::collections::BTreeMap as Map; use alloc::sync::Arc; use core::fmt; #[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>>, } #[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), } impl fmt::Display for Error { fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { todo!() } } 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) } } } } fn check_valid_key(location: &Location, s: &str) -> Result<()> { if s.bytes().all(|c| { c >= 0x80 || c.is_ascii_alphanumeric() || matches!(c, b'.' | b'_' | b'/' | b'*' | b'-') }) { Ok(()) } else { Err(Error::InvalidKey(location.clone(), s.into())) } } /// Returns (unquoted value, new line number) fn read_quoted_value( 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() { if c == delimiter { if !chars.all(|c| c == ' ' || c == '\t') { return Err(Error::StrayCharsAfterQuotedString(location(line_number))); } return Ok((unquoted, line_number)); } else if c == '\\' { todo!() // parse escape sequence } else if c == '\0' { return Err(Error::InvalidValue(location(line_number))); } else { unquoted.push(c); } } } Err(Error::UnterminatedString(start_location.clone(), delimiter)) } impl Configuration { fn load_dyn(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; 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()))?; check_valid_key(&location, relative_key)?; relative_key = relative_key.trim_end_matches(['\t', ' ']); 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) = 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); } } } Ok(config) } 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. Configuration::load_dyn(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() } 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) } } }