From 98b4971587e0159e5f6049efdec53add5f7167ed Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 2 Oct 2026 16:29:46 +0200 Subject: [PATCH] Upstream QEDK/configparser-rs state of the touched files before c14b35b3b5 --- src/ini.rs | 391 +++++++++++++++++--------------------------------- tests/test.rs | 64 ++++++++- 2 files changed, 195 insertions(+), 260 deletions(-) diff --git a/src/ini.rs b/src/ini.rs index 7e353be..c6d3115 100644 --- a/src/ini.rs +++ b/src/ini.rs @@ -11,6 +11,7 @@ use std::collections::HashMap as Map; #[cfg(feature = "tokio")] use tokio::fs as async_fs; +use std::borrow::Cow; use std::collections::HashMap; use std::convert::AsRef; use std::fmt::Write; @@ -24,7 +25,7 @@ use std::path::Path; /// ///let mut config = Ini::new(); ///``` -#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[derive(Debug, Clone, Eq, PartialEq)] #[non_exhaustive] pub struct Ini { map: Map>>, @@ -39,6 +40,12 @@ pub struct Ini { cascade_defaults: bool, } +impl Default for Ini { + fn default() -> Self { + Ini::new() + } +} + #[cfg(all(feature = "serde", not(feature = "indexmap")))] impl Serialize for Ini { fn serialize(&self, serializer: S) -> Result @@ -197,25 +204,20 @@ impl Default for IniDefault { inline_comment_symbols: None, delimiters: vec!['=', ':'], multiline: false, - boolean_values: [ + boolean_values: HashMap::from([ ( true, ["true", "yes", "t", "y", "on", "1"] - .iter() - .map(|&s| s.to_owned()) - .collect(), + .map(String::from) + .into(), ), ( false, ["false", "no", "f", "n", "off", "0"] - .iter() - .map(|&s| s.to_owned()) - .collect(), + .map(String::from) + .into(), ), - ] - .iter() - .cloned() - .collect(), + ]), case_sensitive: false, enable_inline_comments: true, // retain compatibility with previous versions cascade_defaults: false, // retain backwards compatibility @@ -318,6 +320,10 @@ const LINE_ENDING: &str = "\r\n"; #[cfg(not(windows))] const LINE_ENDING: &str = "\n"; +fn read_error(path: &Path, why: impl std::fmt::Display) -> String { + format!("couldn't read {}: {}", path.display(), why) +} + impl Ini { ///Creates a new `Map` of `Map>>` type for the struct. ///All values in the Map are stored in `String` type. @@ -428,6 +434,9 @@ impl Ini { self.delimiters = defaults.delimiters; self.boolean_values = defaults.boolean_values; self.case_sensitive = defaults.case_sensitive; + self.multiline = defaults.multiline; + self.enable_inline_comments = defaults.enable_inline_comments; + self.cascade_defaults = defaults.cascade_defaults; } ///Sets the default section header to the defined string (the default is `default`). @@ -476,7 +485,8 @@ impl Ini { self.inline_comment_symbols = symlist.map(|val| val.to_vec()); } - ///Sets multiline string support. + ///Sets multiline string support. A line continues the previous key's value when it is + ///indented deeper than that key's line; a section header ends the value. ///It must be set before `load()` or `read()` is called in order to take effect. ///## Example ///```rust @@ -541,28 +551,21 @@ impl Ini { &mut self, path: T, ) -> Result>>, String> { - self.map = match self.parse(match fs::read_to_string(&path) { - Err(why) => { - return Err(format!( - "couldn't read {}: {}", - &path.as_ref().display(), - why - )); - } - Ok(s) => s, - }) { - Err(why) => { - return Err(format!( - "couldn't read {}: {}", - &path.as_ref().display(), - why - )); - } - Ok(map) => map, - }; + self.map = self.parse_file(path.as_ref())?; Ok(self.map.clone()) } + fn parse_file(&self, path: &Path) -> Result>>, String> { + let input = fs::read_to_string(path).map_err(|why| read_error(path, why))?; + self.parse(input).map_err(|why| read_error(path, why)) + } + + fn append(&mut self, loaded: Map>>) { + for (section, section_map) in loaded { + self.map.entry(section).or_default().extend(section_map); + } + } + /// Loads configuration data from any stream implementing `std::io::Read`, parses it, and applies it to the internal map. /// /// This function reads the entire stream into a string, parses it as an INI file, and updates the internal map. @@ -623,30 +626,8 @@ impl Ini { &mut self, path: T, ) -> Result>>, String> { - let loaded = match self.parse(match fs::read_to_string(&path) { - Err(why) => { - return Err(format!( - "couldn't read {}: {}", - &path.as_ref().display(), - why - )); - } - Ok(s) => s, - }) { - Err(why) => { - return Err(format!( - "couldn't read {}: {}", - &path.as_ref().display(), - why - )); - } - Ok(map) => map, - }; - - for (section, section_map) in loaded { - self.map.entry(section).or_default().extend(section_map); - } - + let loaded = self.parse_file(path.as_ref())?; + self.append(loaded); Ok(self.map.clone()) } @@ -708,11 +689,7 @@ impl Ini { input: String, ) -> Result>>, String> { let loaded = self.parse(input)?; - - for (section, section_map) in loaded { - self.map.entry(section).or_default().extend(section_map); - } - + self.append(loaded); Ok(self.map.clone()) } @@ -895,42 +872,25 @@ impl Ini { .unwrap_or_else(|| self.comment_symbols.as_ref()); let mut map: Map>> = Map::new(); let mut section = self.default_section.clone(); - // Tracks whether `section` is already present in `map`. Section headers - // always insert their section, so the only section that can still be - // missing is the (lazily created) default section. + // only the default section is created lazily let mut section_exists = false; + // a line continues `current_key` only if indented deeper than the line that set it let mut current_key: Option = None; - - let caser = |val: &str| { - if self.case_sensitive { - val.to_owned() - } else { - val.to_lowercase() - } - }; - - // Track blank lines to preserve them in multiline values. + let mut current_indent = 0usize; let mut blank_lines = 0usize; - for (num, raw_line) in input.lines().enumerate() { + for (num, raw_line) in (1..).zip(input.lines()) { let line = raw_line.trim(); - // If the line is _just_ a comment, skip it entirely. Only the first - // character can make this a full-line comment, so there's no need to - // scan the whole line. if line.starts_with(|c: char| self.comment_symbols.contains(&c)) { continue; } - // Skip empty lines, but keep track of them for multiline values. if line.is_empty() { blank_lines += 1; continue; } - // Strip a trailing inline comment if enabled. `line` is already - // trimmed on both ends, so only the comment-stripped slice needs to - // be re-trimmed. let trimmed = if self.enable_inline_comments { match line.find(|c: char| inline_comment_symbols.contains(&c)) { Some(idx) => line[..idx].trim(), @@ -941,103 +901,59 @@ impl Ini { }; if trimmed.starts_with('[') { - match trimmed.rfind(']') { - Some(end) => { - section = caser(trimmed[1..end].trim()); - - map.entry(section.clone()).or_default(); - section_exists = true; - - continue; - } - None => { - return Err(format!( - "line {}: Found opening bracket for section name but no closing bracket", - num - )); - } - } + let end = trimmed.rfind(']').ok_or_else(|| { + format!( + "line {}: Found opening bracket for section name but no closing bracket", + num + ) + })?; + section = self.casefold(trimmed[1..end].trim()).into_owned(); + map.entry(section.clone()).or_default(); + section_exists = true; + current_key = None; + blank_lines = 0; + continue; } - // Ensure the current section exists, then take a mutable handle to - // it. Using `get_mut` on the common path avoids cloning the section - // name on every single line. if !section_exists { map.entry(section.clone()).or_default(); section_exists = true; } let valmap = map.get_mut(§ion).unwrap(); - if raw_line.starts_with(char::is_whitespace) && self.multiline { - let key = match current_key.as_ref() { - Some(x) => x, - None => { - return Err(format!( - "line {}: Started with indentation but there is no current entry", - num, - )); - } - }; + let indent = raw_line.len() - raw_line.trim_start().len(); + let continued = current_key + .as_deref() + .filter(|_| self.multiline && indent > current_indent); + if let Some(key) = continued { let val = valmap - .entry(key.clone()) - .or_insert_with(|| Some(String::new())); - - match val { - Some(s) => { - for _ in 0..blank_lines { - s.push_str(LINE_ENDING); - } - s.push_str(LINE_ENDING); - s.push_str(trimmed); - } - None => { - let mut s = String::with_capacity( - (blank_lines + 1) * LINE_ENDING.len() + trimmed.len(), - ); - for _ in 0..blank_lines { - s.push_str(LINE_ENDING); - } - s.push_str(LINE_ENDING); - s.push_str(trimmed); - *val = Some(s); - } + .get_mut(key) + .expect("multiline key missing from its section"); + let s = val.get_or_insert_with(String::new); + for _ in 0..=blank_lines { + s.push_str(LINE_ENDING); } + s.push_str(trimmed); } else { - match trimmed.find(&self.delimiters[..]) { - Some(delimiter) => { - let key = caser(trimmed[..delimiter].trim()); - + let (key, value) = match trimmed.find(&self.delimiters[..]) { + Some(idx) => { + let key = trimmed[..idx].trim(); if key.is_empty() { - return Err(format!("line {}:{}: Key cannot be empty", num, delimiter)); - } else { - // `current_key` is only read when stitching together - // multiline values, so only clone the key then. - if self.multiline { - current_key = Some(key.clone()); - } - - // `delimiter` is a byte offset, and the delimiter - // itself may be a multi-byte character, so advance - // by its UTF-8 length rather than assuming one byte. - let delimiter_len = trimmed[delimiter..] - .chars() - .next() - .map_or(1, char::len_utf8); - let value = trimmed[delimiter + delimiter_len..].trim().to_owned(); - - valmap.insert(key, Some(value)); + return Err(format!("line {}:{}: Key cannot be empty", num, idx)); } + // delimiter may be multi-byte + let delimiter_len = trimmed[idx..].chars().next().map_or(1, char::len_utf8); + (key, Some(trimmed[idx + delimiter_len..].trim().to_owned())) } - None => { - let key = caser(trimmed); - if self.multiline { - current_key = Some(key.clone()); - } - - valmap.insert(key, None); - } + None => (trimmed, None), + }; + let key = self.casefold(key).into_owned(); + if self.multiline { + current_key = Some(key.clone()); + current_indent = indent; } + valmap.insert(key, value); } blank_lines = 0; @@ -1046,15 +962,19 @@ impl Ini { Ok(map) } - ///Private function that cases things automatically depending on the set variable. - fn autocase(&self, section: &str, key: &str) -> (String, String) { - if self.case_sensitive { - (section.to_owned(), key.to_owned()) + ///Lowercases `val` unless case-sensitive. Borrows already-lowercase ASCII input. + fn casefold<'a>(&self, val: &'a str) -> Cow<'a, str> { + if self.case_sensitive || val.bytes().all(|b| b.is_ascii() && !b.is_ascii_uppercase()) { + Cow::Borrowed(val) } else { - (section.to_lowercase(), key.to_lowercase()) + Cow::Owned(val.to_lowercase()) } } + fn autocase<'a>(&self, section: &'a str, key: &'a str) -> (Cow<'a, str>, Cow<'a, str>) { + (self.casefold(section), self.casefold(key)) + } + ///Returns a clone of the stored value from the key stored in the defined section. ///Unlike accessing the map directly, `get()` can process your input to make case-insensitive access *if* the ///default constructor is used. @@ -1071,16 +991,14 @@ impl Ini { ///Returns `Some(value)` of type `String` if value is found or else returns `None`. pub fn get(&self, section: &str, key: &str) -> Option { let (section, key) = self.autocase(section, key); - let val = match self.map.get(§ion) { - Some(secmap) => match secmap.get(&key) { - Some(val) => val.clone(), - None => None, - }, - None => None, - }; + let val = self + .map + .get(&*section) + .and_then(|secmap| secmap.get(&*key)) + .and_then(Clone::clone); if val.is_none() && self.cascade_defaults { - return self.map.get(&self.default_section)?.get(&key)?.clone(); + return self.map.get(&self.default_section)?.get(&*key)?.clone(); } val @@ -1101,11 +1019,10 @@ impl Ini { ///If the parsing fails, it returns an `Err(string)`. pub fn getbool(&self, section: &str, key: &str) -> Result, String> { let (section, key) = self.autocase(section, key); - match self.map.get(§ion).and_then(|secmap| secmap.get(&key)) { - Some(Some(inner)) => match inner.to_lowercase().parse::() { - Err(why) => Err(why.to_string()), - Ok(boolean) => Ok(Some(boolean)), - }, + match self.map.get(&*section).and_then(|secmap| secmap.get(&*key)) { + Some(Some(inner)) if inner.eq_ignore_ascii_case("true") => Ok(Some(true)), + Some(Some(inner)) if inner.eq_ignore_ascii_case("false") => Ok(Some(false)), + Some(Some(_)) => Err("provided string was not `true` or `false`".to_owned()), _ => { if self.cascade_defaults && section != self.default_section { return self.getbool(&self.default_section, &key); @@ -1131,24 +1048,17 @@ impl Ini { ///If the parsing fails, it returns an `Err(string)`. pub fn getboolcoerce(&self, section: &str, key: &str) -> Result, String> { let (section, key) = self.autocase(section, key); - match self.map.get(§ion).and_then(|secmap| secmap.get(&key)) { + match self.map.get(&*section).and_then(|secmap| secmap.get(&*key)) { Some(Some(inner)) => { - let boolval = &inner.to_lowercase()[..]; - if self - .boolean_values - .get(&true) - .unwrap() - .iter() - .any(|elem| elem == boolval) - { + let boolval = inner.to_lowercase(); + let matches = |truth: bool| { + self.boolean_values + .get(&truth) + .is_some_and(|values| values.contains(&boolval)) + }; + if matches(true) { Ok(Some(true)) - } else if self - .boolean_values - .get(&false) - .unwrap() - .iter() - .any(|elem| elem == boolval) - { + } else if matches(false) { Ok(Some(false)) } else { Err(format!( @@ -1180,7 +1090,7 @@ impl Ini { ///If the parsing fails, it returns an `Err(string)`. pub fn getint(&self, section: &str, key: &str) -> Result, String> { let (section, key) = self.autocase(section, key); - match self.map.get(§ion).and_then(|secmap| secmap.get(&key)) { + match self.map.get(&*section).and_then(|secmap| secmap.get(&*key)) { Some(Some(inner)) => match inner.parse::() { Err(why) => Err(why.to_string()), Ok(int) => Ok(Some(int)), @@ -1208,7 +1118,7 @@ impl Ini { ///If the parsing fails, it returns an `Err(string)`. pub fn getuint(&self, section: &str, key: &str) -> Result, String> { let (section, key) = self.autocase(section, key); - match self.map.get(§ion).and_then(|secmap| secmap.get(&key)) { + match self.map.get(&*section).and_then(|secmap| secmap.get(&*key)) { Some(Some(inner)) => match inner.parse::() { Err(why) => Err(why.to_string()), Ok(uint) => Ok(Some(uint)), @@ -1236,7 +1146,7 @@ impl Ini { ///If the parsing fails, it returns an `Err(string)`. pub fn getfloat(&self, section: &str, key: &str) -> Result, String> { let (section, key) = self.autocase(section, key); - match self.map.get(§ion).and_then(|secmap| secmap.get(&key)) { + match self.map.get(&*section).and_then(|secmap| secmap.get(&*key)) { Some(Some(inner)) => match inner.parse::() { Err(why) => Err(why.to_string()), Ok(float) => Ok(Some(float)), @@ -1332,12 +1242,12 @@ impl Ini { value: Option, ) -> Option> { let (section, key) = self.autocase(section, key); - match self.map.get_mut(§ion) { - Some(secmap) => secmap.insert(key, value), + match self.map.get_mut(&*section) { + Some(secmap) => secmap.insert(key.into_owned(), value), None => { let mut valmap: Map> = Map::new(); - valmap.insert(key, value); - self.map.insert(section, valmap); + valmap.insert(key.into_owned(), value); + self.map.insert(section.into_owned(), valmap); None } } @@ -1365,8 +1275,7 @@ impl Ini { key: &str, value: Option<&str>, ) -> Option> { - let (section, key) = self.autocase(section, key); - self.set(§ion, &key, value.map(String::from)) + self.set(section, key, value.map(String::from)) } ///Clears the map, removing all sections and properties from the hashmap. It keeps the allocated memory for reuse. @@ -1400,18 +1309,14 @@ impl Ini { ///``` ///Returns `Some(section_map)` if the section exists or else, `None`. pub fn remove_section(&mut self, section: &str) -> Option>> { - let section = if self.case_sensitive { - section.to_owned() - } else { - section.to_lowercase() - }; + let section = self.casefold(section); #[cfg(not(feature = "indexmap"))] { - self.map.remove(§ion) + self.map.remove(&*section) } #[cfg(feature = "indexmap")] { - self.map.swap_remove(§ion) + self.map.swap_remove(&*section) } } @@ -1434,11 +1339,11 @@ impl Ini { let (section, key) = self.autocase(section, key); #[cfg(not(feature = "indexmap"))] { - self.map.get_mut(§ion)?.remove(&key) + self.map.get_mut(&*section)?.remove(&*key) } #[cfg(feature = "indexmap")] { - self.map.get_mut(§ion)?.swap_remove(&key) + self.map.get_mut(&*section)?.swap_remove(&*key) } } } @@ -1456,28 +1361,20 @@ impl Ini { &mut self, path: T, ) -> Result>>, String> { - self.map = match self.parse(match async_fs::read_to_string(&path).await { - Err(why) => { - return Err(format!( - "couldn't read {}: {}", - &path.as_ref().display(), - why - )); - } - Ok(s) => s, - }) { - Err(why) => { - return Err(format!( - "couldn't read {}: {}", - &path.as_ref().display(), - why - )); - } - Ok(map) => map, - }; + self.map = self.parse_file_async(path.as_ref()).await?; Ok(self.map.clone()) } + async fn parse_file_async( + &self, + path: &Path, + ) -> Result>>, String> { + let input = async_fs::read_to_string(path) + .await + .map_err(|why| read_error(path, why))?; + self.parse(input).map_err(|why| read_error(path, why)) + } + ///Loads a file from a defined path, parses it and applies it to the existing hashmap in our struct. ///While `load_async()` will clear the existing `Map`, `load_and_append_async()` applies the new values on top ///of the existing hashmap, preserving previous values. @@ -1490,30 +1387,8 @@ impl Ini { &mut self, path: T, ) -> Result>>, String> { - let loaded = match self.parse(match async_fs::read_to_string(&path).await { - Err(why) => { - return Err(format!( - "couldn't read {}: {}", - &path.as_ref().display(), - why - )); - } - Ok(s) => s, - }) { - Err(why) => { - return Err(format!( - "couldn't read {}: {}", - &path.as_ref().display(), - why - )); - } - Ok(map) => map, - }; - - for (section, section_map) in loaded { - self.map.entry(section).or_default().extend(section_map); - } - + let loaded = self.parse_file_async(path.as_ref()).await?; + self.append(loaded); Ok(self.map.clone()) } diff --git a/tests/test.rs b/tests/test.rs index 828bebb..221b62a 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -978,8 +978,7 @@ fn serde_case_sensitive_roundtrip() -> Result<(), Box> { fn multibyte_delimiter_does_not_panic() -> Result<(), Box> { use configparser::ini::IniDefault; - // A multi-byte delimiter must not cause a panic when splitting the - // key from the value on a non-char-boundary byte offset. + // A multi-byte delimiter must not split on a non-char boundary. let mut defaults = IniDefault::default(); defaults.delimiters = vec!['\u{00a7}']; // section sign, 2 bytes in UTF-8 @@ -989,3 +988,64 @@ fn multibyte_delimiter_does_not_panic() -> Result<(), Box> { assert_eq!(config.get("s", "key"), Some(String::from("value"))); Ok(()) } + +#[test] +fn default_ini_parses_like_new() -> Result<(), Box> { + let mut config = Ini::default(); + config.read(String::from("[S]\nKey = value ; note"))?; + assert_eq!(config.get("s", "key"), Some(String::from("value"))); + Ok(()) +} + +#[test] +fn load_defaults_applies_every_field() { + use configparser::ini::IniDefault; + + let mut defaults = IniDefault::default(); + defaults.multiline = true; + defaults.enable_inline_comments = false; + defaults.cascade_defaults = true; + + let mut config = Ini::new(); + config.load_defaults(defaults.clone()); + assert_eq!(config.defaults(), defaults); +} + +#[test] +fn multiline_continuation_rules() -> Result<(), Box> { + let mut config = Ini::new(); + config.set_multiline(true); + config.read(String::from( + "[a] + key = line one + line two + next = value + +[b] + other = value", + ))?; + + assert_eq!( + config.get("a", "key"), + Some(String::from("line one\nline two")) + ); + assert_eq!(config.get("a", "next"), Some(String::from("value"))); + assert_eq!(config.get("b", "other"), Some(String::from("value"))); + Ok(()) +} + +#[test] +fn bool_getters_reject_unknown_values() -> Result<(), Box> { + use configparser::ini::IniDefault; + use std::collections::HashMap; + + let mut defaults = IniDefault::default(); + defaults.boolean_values = HashMap::from([(true, vec![String::from("aye")])]); + + let mut config = Ini::new_from_defaults(defaults); + config.read(String::from("[s]\nyes = AYE\nno = nay"))?; + assert_eq!(config.getboolcoerce("s", "yes")?, Some(true)); + assert!(config.getboolcoerce("s", "no").is_err()); + assert!(config.getbool("s", "yes").is_err()); + Ok(()) +}