#![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; /// File and line information #[derive(Clone, Debug)] pub struct Location { file: Arc, line: u64, } impl Location { /// File name #[must_use] pub fn file(&self) -> &str { &self.file } /// Line number #[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) } } /// A string value, together with location information about where it is defined. #[derive(Debug)] struct Value { value: Box, defined_at: Location, read: AtomicBool, } /// A parsed POM configuration. #[derive(Clone, Debug, Default)] pub struct Configuration { /// List of items in configuration, sorted by key. items: Vec<(Box, Arc)>, } 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(()) } } /// A parsing error. #[non_exhaustive] #[derive(Debug)] pub enum Error { /// I/O error /// /// The first field is a description of what led to the error. #[cfg(feature = "std")] IO(Box, std::io::Error), /// Illegal character in POM file /// /// Specifically, an ASCII control character other than LF, CR immediately followed by LF, and tab. IllegalCharacter(Location, char), /// Invalid UTF-8 in POM file InvalidUtf8(Location), /// Couldn't parse signed integer. BadInt(Location, Box), /// Couldn't parse unsigned integer. BadUInt(Location, Box), /// Couldn't parse floating-point number. BadFloat(Location, Box), /// Couldn't parse boolean. BadBool(Location, Box), /// Opening \[ without matching \]. UnmatchedLeftBrace(Location), /// Key contains invalid characters. /// /// The valid characters are anything outside of ASCII, /// as well as `a`–`z`, `A`–`Z`, `0`–`9`, and each of `/.-*_`. InvalidKey(Location, Box), /// Value contains a null character. /// /// These are not allowed for interoperability with languages /// with null-terminated strings (C). InvalidValue(Location), /// Line is not a `[section-header]` or `key = value`. InvalidLine(Location), /// Characters appear after a quoted value. /// /// e.g. `key = "value" foo` StrayCharsAfterString(Location), /// String opened but never closed with a matching character. UnterminatedString(Location, char), /// Invalid escape sequence appears in a quoted value. InvalidEscapeSequence(Location, Box), /// Key is defined twice in a file DuplicateKey(Box, Location, Location), /// 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 { #[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 } } /// Type alias for [`std::result::Result`] with [`Error`] as the error. pub type Result = core::result::Result; fn parse_int(location: &Location, string: &str) -> Result { 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 { 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 { // decimal leading zeroes are not allowed 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 { 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'.' { // decimal point must be preceded and followed by a digit 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 { 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 { 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 } /// Trait for reading configurations. /// /// Ordinarily you won't need to implement this trait, since it is /// already implemented by any `T` implementing [`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 is_illegal_byte(c: u8) -> bool { (0..0x1f).contains(&c) && c != b'\t' } fn process_line(line_buf: &mut Vec, 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 { 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, }) } /// Returns `Ok(())` if `errors` is empty, otherwise a compound error /// containing all the `errors`. fn check_error_vec(mut errors: Vec) -> Result<()> { match errors.len() { 0 => Ok(()), 1 => Err(errors.pop().unwrap()), _ => Err(Error::Multiple(errors.into())), } } #[derive(Default)] struct Parser { nonfatal_errors: Vec, } 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}"))); } } } /// 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; "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 { let mut items: Vec<(Box, Arc)> = vec![]; let mut line: Vec = vec![]; let mut line_number: u64 = 0; let mut current_section = String::new(); let filename: Arc = 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('#') { // comment/blank line continue; } if line.starts_with('[') { // [section.header] 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 { // key = value 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 { // duplicate key 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 { /// Load a configuration. /// /// `reader` can be `&[u8]` or anything that implements [`std::io::BufRead`] /// (if the `std` feature is enabled) such as `std::io::BufReader`. /// /// `filename` is used in error messages. 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) } /// Load a configuration from a file path. #[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)) } /// Binary search `self.items`. /// /// See [`std::slice::binary_search_by`]. fn binary_search_for(&self, key: &str) -> core::result::Result { self.items.binary_search_by(|(k, _)| k.as_ref().cmp(key)) } /// Returns the index of the first key which is greater than `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 .") } /// Returns the index of the first key which is greater than `key` + "." + `x` for all strings `x`. fn subkey_end_idx(&self, key: &str) -> usize { // NB: / is the next ASCII character after . let key_slash = format!("{key}/"); self.binary_search_for(&key_slash).unwrap_or_else(|x| x) } /// Extract a section out of a configuration. /// /// More specifically, this will give you the configuration consisting of all /// keys starting with `key.` in `self`, together with their values. #[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(), } } /// Get all “direct keys” in this configuration. /// /// More specifically, this returns an iterator of all unique /// first components of keys in `self`. /// /// (So if there were keys `sheep.age`, `sheep.colour`, and `farmer-name`, /// this would give an iterator yielding /// `"farmer-name"` and `"sheep"` in some order.) /// /// The order of items returned is arbitrary and may change /// in future versions without notice. pub fn keys(&self) -> Keys<'_> { Keys { iter: self.items.iter(), prev: None, } } /// Get all defined keys in this configuration, including nested ones, /// and their values. /// /// The order of items returned is arbitrary and may change /// in future versions without notice. 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) } /// Get value associated with `key`, if any. #[must_use] pub fn get(&self, key: &str) -> Option<&str> { Some(self.get_val(key, true)?.value.as_ref()) } /// Get location in the configuration file where `key` is defined, if any. #[must_use] pub fn location(&self, key: &str) -> Option { if let Some(val) = self.get_val(key, false) { Some(val.defined_at.clone()) } else { // Check if `key` has any defined subkeys 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() } } /// Returns `true` if `key` is defined in this configuration. #[must_use] pub fn has(&self, key: &str) -> bool { self.get_val(key, false).is_some() } /// Get value associated with `key`, or else use `default` if it isn't defined. #[must_use] pub fn get_or_default<'a>(&'a self, key: &str, default: &'a str) -> &'a str { self.get(key).unwrap_or(default) } /// Get value associated with `key`, and parse it as an integer. /// /// Returns `None` if `key` is not defined, /// and `Some(Err(…))` if `key` is defined but not an integer. #[must_use] pub fn get_int(&self, key: &str) -> Option> { let Value { value, defined_at, .. } = self.get_val(key, true)?; Some(parse_int(defined_at, value.as_ref())) } /// Get value associated with `key`, and parse it as an integer, or else use `default`. /// /// Returns `Err(…)` if `key` is defined but not an integer. pub fn get_int_or_default(&self, key: &str, default: i64) -> Result { self.get_int(key).unwrap_or(Ok(default)) } /// Get value associated with `key`, and parse it as an unsigned integer. /// /// Returns `None` if `key` is not defined, /// and `Some(Err(…))` if `key` is defined but not an unsigned integer. #[must_use] pub fn get_uint(&self, key: &str) -> Option> { let Value { value, defined_at, .. } = self.get_val(key, true)?; Some(parse_uint(defined_at, value.as_ref())) } /// Get value associated with `key`, and parse it as an unsinged integer, or else use `default`. /// /// Returns `Err(…)` if `key` is defined but not an unsigned integer. pub fn get_uint_or_default(&self, key: &str, default: u64) -> Result { self.get_uint(key).unwrap_or(Ok(default)) } /// Get value associated with `key`, and parse it as a float. /// /// Returns `None` if `key` is not defined, /// and `Some(Err(…))` if `key` is defined but not a float. #[must_use] pub fn get_float(&self, key: &str) -> Option> { let Value { value, defined_at, .. } = self.get_val(key, true)?; Some(parse_float(defined_at, value.as_ref())) } /// Get value associated with `key`, and parse it as a float, or else use `default`. /// /// Returns `Err(…)` if `key` is defined but not a float. pub fn get_float_or_default(&self, key: &str, default: f64) -> Result { self.get_float(key).unwrap_or(Ok(default)) } /// Get value associated with `key`, and parse it as a boolean. /// /// Returns `None` if `key` is not defined, /// and `Some(Err(…))` if `key` is defined but not equal to one of /// `off`, `no`, `false`, `on`, `yes`, `true`. #[must_use] pub fn get_bool(&self, key: &str) -> Option> { let Value { value, defined_at, .. } = self.get_val(key, true)?; Some(parse_bool(defined_at, value.as_ref())) } /// Get value associated with `key`, and parse it as a boolean, or else use `default`. /// /// Returns `Err(…)` if `key` is defined but not equal to one of /// `off`, `no`, `false`, `on`, `yes`, `true`. pub fn get_bool_or_default(&self, key: &str, default: bool) -> Result { self.get_bool(key).unwrap_or(Ok(default)) } /// Get value associated with `key`, and parse it as a comma-separated list. /// /// Commas in list entries can be escaped with `\,`. #[must_use] pub fn get_list(&self, key: &str) -> Option> { let value = &self.get_val(key, true)?.value; Some(parse_list(value.as_ref())) } /// Get value associated with `key`, and parse it as a comma-separated list, or else use `default`. /// /// If you want `default = []`, use [`Self::get_list_or_empty`] instead /// (this method will be cumbersome to use since Rust can't infer the type of `[]`). /// /// Commas in list entries can be escaped with `\,`. /// /// `default` can be any iterable-of-strings (`&[&str]`, `Vec`, etc.). pub fn get_list_or_default(&self, key: &str, default: L) -> Vec where L: IntoIterator, String: From, { self.get_list(key) .unwrap_or_else(|| default.into_iter().map(String::from).collect()) } /// Get value associated with `key`, and parse it as a comma-separated list, or else use `[]`. pub fn get_list_or_empty(&self, key: &str) -> Vec { let empty: [&'static str; 0] = []; self.get_list_or_default(key, empty) } /// Merge `conf` into `self`, preferring values in `conf`. 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)); } } /// Returns an iterator over all keys whose values have not been read. /// /// This includes getting them through [`Self::get`], [`Self::get_or_default`], [`Self::get_int`], etc. /// It also includes getting them through [`Self::get`] called on a section obtained via [`Self::section`]. /// /// The order of the items returned is arbitrary and may change in future versions without notice. /// /// Beware of race conditions when using this function in a multithreaded program /// (you should wait for all threads to finish reading the configuration before calling this). pub fn unread_keys(&self) -> UnreadKeys<'_> { UnreadKeys(self.items.iter()) } } // Type returned by `Configuration.items.iter()` type ItemsIter<'a> = core::slice::Iter<'a, (Box, Arc)>; /// Opaque type returned by [`Configuration::iter`]. #[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 { let (key, val) = self.0.next()?; Some((key, val.value.as_ref())) } } impl FusedIterator for ConfigurationIter<'_> {} /// Opaque type returned by [`Configuration::keys`]. #[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 { 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<'_> {} /// Opaque type returned by [`Configuration::unread_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 { 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); /// See [`Configuration::iter`]. fn into_iter(self) -> Self::IntoIter { ConfigurationIter(self.items.iter()) } }