From 8cb9cc4c65581548544ae28b1f2fed3fa8af874c Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 4 Sep 2026 09:28:35 +0200 Subject: [PATCH] Upstream QEDK/configparser-rs state of the touched files before 5ca2a5b537 --- src/ini.rs | 1536 +++++++++++++++++++++++++++++++++++++++++++++++++ tests/test.rs | 975 +++++++++++++++++++++++++++++++ 2 files changed, 2511 insertions(+) create mode 100644 src/ini.rs create mode 100644 tests/test.rs diff --git a/src/ini.rs b/src/ini.rs new file mode 100644 index 0000000..f1cf0da --- /dev/null +++ b/src/ini.rs @@ -0,0 +1,1536 @@ +//!The ini module provides all the things necessary to load and parse ini-syntax files. The most important of which is the `Ini` struct. +//!See the [implementation](https://docs.rs/configparser/*/configparser/ini/struct.Ini.html) documentation for more details. +#[cfg(feature = "indexmap")] +use indexmap::IndexMap as Map; +#[cfg(feature = "serde")] +use serde::de::{Deserialize, Deserializer}; +#[cfg(feature = "serde")] +use serde::ser::{Serialize, Serializer}; +#[cfg(not(feature = "indexmap"))] +use std::collections::HashMap as Map; +#[cfg(feature = "tokio")] +use tokio::fs as async_fs; + +use std::collections::HashMap; +use std::convert::AsRef; +use std::fmt::Write; +use std::fs; +use std::path::Path; + +///The `Ini` struct simply contains a nested hashmap of the loaded configuration, the default section header and comment symbols. +///## Example +///```rust +///use configparser::ini::Ini; +/// +///let mut config = Ini::new(); +///``` +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[non_exhaustive] +pub struct Ini { + map: Map>>, + default_section: std::string::String, + comment_symbols: Vec, + inline_comment_symbols: Option>, + delimiters: Vec, + boolean_values: HashMap>, + case_sensitive: bool, + multiline: bool, + enable_inline_comments: bool, + cascade_defaults: bool, +} + +#[cfg(all(feature = "serde", not(feature = "indexmap")))] +impl Serialize for Ini { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Delegate serialization to the internal map only + self.map.serialize(serializer) + } +} + +#[cfg(all(feature = "serde", not(feature = "indexmap")))] +impl<'de> Deserialize<'de> for Ini { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // First, deserialize the raw map + let map = Map::>>::deserialize(deserializer) + .map_err(serde::de::Error::custom)?; + // Build an Ini with defaults, then replace its map + let mut ini = Ini::new(); + ini.map = map; + Ok(ini) + } +} + +#[cfg(all(feature = "serde", feature = "indexmap"))] +impl Serialize for Ini { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Delegate to IndexMap’s Serialize impl + serde::Serialize::serialize(&self.map, serializer) + } +} + +#[cfg(all(feature = "serde", feature = "indexmap"))] +impl<'de> Deserialize<'de> for Ini { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let map = serde::Deserialize::deserialize(deserializer)?; + let mut ini = Ini::new(); + ini.map = map; + Ok(ini) + } +} + +///The `IniDefault` struct serves as a template to create other `Ini` objects from. It can be used to store and load +///default properties from different `Ini` objects. +///## Example +///```rust +///use configparser::ini::Ini; +/// +///let mut config = Ini::new(); +///let default = config.defaults(); +///let mut config2 = Ini::new_from_defaults(default); // default gets consumed +///``` +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub struct IniDefault { + ///Denotes the default section header name. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.default_section, "default"); + ///``` + pub default_section: std::string::String, + ///Denotes the set comment symbols for the object. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.comment_symbols, vec![';', '#']); + ///``` + pub comment_symbols: Vec, + ///Denotes the set of inline comment symbols for the object. The default of + ///`None` means to fall back to the normal comment symbols. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.inline_comment_symbols, None); + ///``` + pub inline_comment_symbols: Option>, + ///Denotes the set delimiters for the key-value pairs. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.delimiters, vec!['=', ':']); + ///``` + pub delimiters: Vec, + pub boolean_values: HashMap>, + ///Denotes if the `Ini` object is case-sensitive. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.case_sensitive, false); + ///``` + pub case_sensitive: bool, + ///Denotes if the `Ini` object parses multiline strings. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.multiline, false); + ///``` + pub multiline: bool, + ///Denotes if the `Ini` object recognizes inline comments. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.enable_inline_comments, true); + ///``` + pub enable_inline_comments: bool, + ///Cascade defaults automatically. If a key is not defined in a get*() + ///call, but exists in the [DEFAULT] section, the value in the [DEFAULT] + ///section will be returned. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.cascade_defaults, false); + ///``` + pub cascade_defaults: bool, +} + +impl Default for IniDefault { + fn default() -> Self { + Self { + default_section: "default".to_owned(), + comment_symbols: vec![';', '#'], + inline_comment_symbols: None, + delimiters: vec!['=', ':'], + multiline: false, + boolean_values: [ + ( + true, + ["true", "yes", "t", "y", "on", "1"] + .iter() + .map(|&s| s.to_owned()) + .collect(), + ), + ( + false, + ["false", "no", "f", "n", "off", "0"] + .iter() + .map(|&s| s.to_owned()) + .collect(), + ), + ] + .iter() + .cloned() + .collect(), + case_sensitive: false, + enable_inline_comments: true, // retain compatibility with previous versions + cascade_defaults: false, // retain backwards compatibility + } + } +} + +/// Use this struct to define formatting options for the `pretty_write` functions. +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub struct WriteOptions { + ///If true then the keys and values will be separated by " = ". In the special case where the value is empty, the + ///line ends with " =". + ///If false then keys and values will be separated by "=". + ///Default is `false`. + ///## Example + ///```rust + ///use configparser::ini::WriteOptions; + /// + ///let mut write_options = WriteOptions::default(); + ///assert_eq!(write_options.space_around_delimiters, false); + ///``` + pub space_around_delimiters: bool, + + ///Defines the number of spaces for indentation of for multiline values. + ///Default is 4 spaces. + ///## Example + ///```rust + ///use configparser::ini::WriteOptions; + /// + ///let mut write_options = WriteOptions::default(); + ///assert_eq!(write_options.multiline_line_indentation, 4); + ///``` + pub multiline_line_indentation: usize, + + ///Defines the number of blank lines between sections. + ///Default is 0. + ///## Example + ///```rust + ///use configparser::ini::WriteOptions; + /// + ///let mut write_options = WriteOptions::default(); + ///assert_eq!(write_options.blank_lines_between_sections, 0); + ///``` + pub blank_lines_between_sections: usize, +} + +impl Default for WriteOptions { + fn default() -> Self { + Self { + space_around_delimiters: false, + multiline_line_indentation: 4, + blank_lines_between_sections: 0, + } + } +} + +impl WriteOptions { + ///Creates a new `WriteOptions` object with the default values. + ///## Example + ///```rust + ///use configparser::ini::WriteOptions; + /// + ///let write_options = WriteOptions::new(); + ///assert_eq!(write_options.space_around_delimiters, false); + ///assert_eq!(write_options.multiline_line_indentation, 4); + ///assert_eq!(write_options.blank_lines_between_sections, 0); + ///``` + ///Returns the struct and stores it in the calling variable. + pub fn new() -> WriteOptions { + WriteOptions::default() + } + + ///Creates a new `WriteOptions` object with the given parameters. + ///## Example + ///```rust + ///use configparser::ini::WriteOptions; + /// + ///let write_options = WriteOptions::new_with_params(true, 2, 1); + ///assert_eq!(write_options.space_around_delimiters, true); + ///assert_eq!(write_options.multiline_line_indentation, 2); + ///assert_eq!(write_options.blank_lines_between_sections, 1); + ///``` + ///Returns the struct and stores it in the calling variable. + pub fn new_with_params( + space_around_delimiters: bool, + multiline_line_indentation: usize, + blank_lines_between_sections: usize, + ) -> WriteOptions { + Self { + space_around_delimiters, + multiline_line_indentation, + blank_lines_between_sections, + } + } +} + +#[cfg(windows)] +const LINE_ENDING: &str = "\r\n"; +#[cfg(not(windows))] +const LINE_ENDING: &str = "\n"; + +impl Ini { + ///Creates a new `Map` of `Map>>` type for the struct. + ///All values in the Map are stored in `String` type. + /// + ///By default, [`std::collections::HashMap`] is used for the Map object. + ///The `indexmap` feature can be used to use an [`indexmap::map::IndexMap`] instead, which + ///allows keeping the insertion order for sections and keys. + /// + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///``` + ///Returns the struct and stores it in the calling variable. + pub fn new() -> Ini { + Ini::new_from_defaults(IniDefault::default()) + } + + ///Creates a new **case-sensitive** `Map` of `Map>>` type for the struct. + ///All values in the Map are stored in `String` type. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new_cs(); + ///``` + ///Returns the struct and stores it in the calling variable. + pub fn new_cs() -> Ini { + Ini::new_from_defaults(IniDefault { + case_sensitive: true, + ..Default::default() + }) + } + + ///Creates a new `Ini` with the given defaults from an existing `IniDefault` object. + ///## Example + ///```rust + ///use configparser::ini::Ini; + ///use configparser::ini::IniDefault; + /// + ///let mut default = IniDefault::default(); + ///default.comment_symbols = vec![';']; + ///default.delimiters = vec!['=']; + ///let mut config = Ini::new_from_defaults(default.clone()); + ///// Now, load as usual with new defaults: + ///let map = config.load("tests/test.ini").unwrap(); + ///assert_eq!(config.defaults(), default); + /// + ///``` + pub fn new_from_defaults(defaults: IniDefault) -> Ini { + Ini { + map: Map::new(), + default_section: defaults.default_section, + comment_symbols: defaults.comment_symbols, + inline_comment_symbols: defaults.inline_comment_symbols, + delimiters: defaults.delimiters, + boolean_values: defaults.boolean_values, + case_sensitive: defaults.case_sensitive, + multiline: defaults.multiline, + enable_inline_comments: defaults.enable_inline_comments, + cascade_defaults: defaults.cascade_defaults, + } + } + + ///Fetches the defaults from the current `Ini` object and stores it as a `IniDefault` struct for usage elsewhere. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///``` + ///Returns an `IniDefault` object. Keep in mind that it will get borrowed since it has non-`Copy` types. + pub fn defaults(&self) -> IniDefault { + IniDefault { + default_section: self.default_section.to_owned(), + comment_symbols: self.comment_symbols.to_owned(), + inline_comment_symbols: self.inline_comment_symbols.to_owned(), + delimiters: self.delimiters.to_owned(), + boolean_values: self.boolean_values.to_owned(), + case_sensitive: self.case_sensitive, + multiline: self.multiline, + enable_inline_comments: self.enable_inline_comments, + cascade_defaults: self.cascade_defaults, + } + } + + ///Takes an `IniDefault` object and stores its properties in the calling `Ini` object. This happens in-place and + ///does not work retroactively, only future operations are affected. + ///## Example + ///```rust + ///use configparser::ini::Ini; + ///use configparser::ini::IniDefault; + /// + ///let mut config = Ini::new(); + ///let mut default = IniDefault::default(); + ///default.case_sensitive = true; + ///// This is equivalent to ini_cs() defaults + ///config.load_defaults(default.clone()); + ///assert_eq!(config.defaults(), default); + ///``` + ///Returns nothing. + pub fn load_defaults(&mut self, defaults: IniDefault) { + self.default_section = defaults.default_section; + self.comment_symbols = defaults.comment_symbols; + self.inline_comment_symbols = defaults.inline_comment_symbols; + self.delimiters = defaults.delimiters; + self.boolean_values = defaults.boolean_values; + self.case_sensitive = defaults.case_sensitive; + } + + ///Sets the default section header to the defined string (the default is `default`). + ///It must be set before `load()` or `read()` is called in order to take effect. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + /// + ///config.set_default_section("topsecret"); + ///let map = config.load("tests/test.ini").unwrap(); + ///``` + ///Returns nothing. + pub fn set_default_section(&mut self, section: &str) { + self.default_section = section.to_owned(); + } + + ///Sets the default comment symbols to the defined character slice (the defaults are `;` and `#`). + ///Keep in mind that this will remove the default symbols. It must be set before `load()` or `read()` is called in order to take effect. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.set_comment_symbols(&['!', '#']); + ///let map = config.load("tests/test.ini").unwrap(); + ///``` + ///Returns nothing. + pub fn set_comment_symbols(&mut self, symlist: &[char]) { + self.comment_symbols = symlist.to_vec(); + } + + ///Sets the default inline comment symbols to the defined character slice (the default is `None` which falls back to the normal comment symbols). + ///Keep in mind that this will remove the default symbols. It must be set before `load()` or `read()` is called in order to take effect. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.set_inline_comment_symbols(Some(&['!', '#'])); + ///let map = config.load("tests/test.ini").unwrap(); + ///``` + ///Returns nothing. + pub fn set_inline_comment_symbols(&mut self, symlist: Option<&[char]>) { + self.inline_comment_symbols = symlist.map(|val| val.to_vec()); + } + + ///Sets multiline string support. + ///It must be set before `load()` or `read()` is called in order to take effect. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.set_multiline(true); + ///let map = config.load("tests/test.ini").unwrap(); + ///``` + ///Returns nothing. + pub fn set_multiline(&mut self, multiline: bool) { + self.multiline = multiline; + } + + ///Sets the behavior around the cascading of defaults. If this is set + ///to `true`, a get*() call for a section without a matching key value will + ///attempt to get that key value from the [DEFAULT] section if it exists + ///there. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.set_cascade_defaults(true); + ///let map = config.load("tests/test_cascade_defaults.ini").unwrap(); + /// + ///let val = config.get("mysection", "fallback_key"); // Will fall back to the [DEFAULT] section for "key" + ///assert_eq!(val, Some(String::from("fallback value"))); + ///``` + pub fn set_cascade_defaults(&mut self, cascade_defaults: bool) { + self.cascade_defaults = cascade_defaults; + } + + ///Gets all the sections of the currently-stored `Map` in a vector. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let sections = config.sections(); + ///``` + ///Returns `Vec`. + pub fn sections(&self) -> Vec { + self.map.keys().cloned().collect() + } + + ///Loads a file from a defined path, parses it and puts the hashmap into our struct. + ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let map = config.load("tests/test.ini").unwrap(); // we can get a clone like this, or just store it + /////Then, we can use standard hashmap functions like: + ///let values = map.get("values").unwrap(); + ///``` + ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. + ///Use `get_mut_map()` if you want a mutable reference. + pub fn load>( + &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, + }; + Ok(self.map.clone()) + } + + /// 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. + /// On error, returns a descriptive message. The previous map is replaced with the new one. + /// + /// # Arguments + /// * `reader` - Any type that implements `std::io::Read`, such as a file, buffer, or network stream. + /// + /// # Returns + /// * `Result>>, String>` - The parsed map on success, or an error message on failure. + /// + /// # Example + /// ```rust + /// use configparser::ini::Ini; + /// + /// let mut config = Ini::new(); + /// let input = "[section]\nkey=value"; + /// config.load_from_stream(input.as_bytes()).unwrap(); + /// + /// assert_eq!(config.get("section", "key").unwrap(), "value"); + /// ``` + pub fn load_from_stream( + &mut self, + mut reader: R, + ) -> Result>>, String> { + let mut buf = String::new(); + if let Err(err) = reader.read_to_string(&mut buf) { + return Err(format!("couldn't read from stream: {}", err)); + } + + self.map = match self.parse(buf) { + Err(why) => { + return Err(format!("couldn't read from stream: {}", why)); + } + Ok(map) => map, + }; + Ok(self.map.clone()) + } + + ///Loads a file from a defined path, parses it and applies it to the existing hashmap in our struct. + ///While `load()` will clear the existing `Map`, `load_and_append()` applies the new values on top of + ///the existing hashmap, preserving previous values. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini").unwrap(); + ///config.load_and_append("tests/sys_cfg.ini").ok(); // we don't have to worry if this doesn't succeed + ///config.load_and_append("tests/user_cfg.ini").ok(); // we don't have to worry if this doesn't succeed + ///let map = config.get_map().unwrap(); + /////Then, we can use standard hashmap functions like: + ///let values = map.get("values").unwrap(); + ///``` + ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. + ///Use `get_mut_map()` if you want a mutable reference. + pub fn load_and_append>( + &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); + } + + Ok(self.map.clone()) + } + + ///Reads an input string, parses it and puts the hashmap into our struct. + ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let map = match config.read(String::from( + /// "[2000s] + /// 2020 = bad")) { + /// Err(why) => panic!("{}", why), + /// Ok(inner) => inner + ///}; + ///let this_year = map["2000s"]["2020"].clone().unwrap(); + ///assert_eq!(this_year, "bad"); // value accessible! + ///``` + ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. + ///Use `get_mut_map()` if you want a mutable reference. + pub fn read( + &mut self, + input: String, + ) -> Result>>, String> { + self.map = self.parse(input)?; + Ok(self.map.clone()) + } + + ///Reads an input string, parses it and applies it to the existing hashmap in our struct. + ///While `read()` and `load()` will clear the existing `Map`, `read_and_append()` applies the new + ///values on top of the existing hashmap, preserving previous values. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///if let Err(why) = config.read(String::from( + /// "[2000s] + /// 2020 = bad + /// 2023 = better")) { + /// panic!("{}", why); + ///}; + ///if let Err(why) = config.read_and_append(String::from( + /// "[2000s] + /// 2020 = terrible")) { + /// panic!("{}", why); + ///}; + ///let map = config.get_map().unwrap(); + ///let few_years_ago = map["2000s"]["2020"].clone().unwrap(); + ///let this_year = map["2000s"]["2023"].clone().unwrap(); + ///assert_eq!(few_years_ago, "terrible"); // value updated! + ///assert_eq!(this_year, "better"); // keeps old values! + ///``` + ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. + ///Use `get_mut_map()` if you want a mutable reference. + pub fn read_and_append( + &mut self, + input: String, + ) -> Result>>, String> { + let loaded = self.parse(input)?; + + for (section, section_map) in loaded { + self.map.entry(section).or_default().extend(section_map); + } + + Ok(self.map.clone()) + } + + ///Writes the current configuation to the specified path using default formatting. + ///If a file is not present then it is automatically created for you. If a file already exists then it is overwritten. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///fn main() -> std::io::Result<()> { + /// let mut config = Ini::new(); + /// config.read(String::from( + /// "[2000s] + /// 2020 = bad")); + /// config.write("output.ini") + ///} + ///``` + ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. + pub fn write>(&self, path: T) -> std::io::Result<()> { + fs::write(path.as_ref(), self.unparse(&WriteOptions::default())) + } + + ///Writes the current configuation to the specified path using the given formatting options. + ///If a file is not present then it is automatically created for you. If a file already exists then it is overwritten. + ///## Example + ///```rust + ///use configparser::ini::{Ini, WriteOptions}; + /// + ///fn main() -> std::io::Result<()> { + /// let mut write_options = WriteOptions::default(); + /// write_options.space_around_delimiters = true; + /// write_options.multiline_line_indentation = 2; + /// write_options.blank_lines_between_sections = 1; + /// + /// let mut config = Ini::new(); + /// config.read(String::from( + /// "[2000s] + /// 2020 = bad")); + /// config.pretty_write("output.ini", &write_options) + ///} + ///``` + ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. + pub fn pretty_write>( + &self, + path: T, + write_options: &WriteOptions, + ) -> std::io::Result<()> { + fs::write(path.as_ref(), self.unparse(write_options)) + } + + ///Returns a string with the current configuration formatted with valid ini-syntax using default formatting. + ///This is always safe since the configuration is validated during parsing. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[2000s] + /// 2020 = bad")); + ///let outstring = config.writes(); + ///``` + ///Returns a `String` type contatining the ini-syntax file. + pub fn writes(&self) -> String { + self.unparse(&WriteOptions::default()) + } + + ///Returns a string with the current configuration formatted with valid ini-syntax using the given formatting options. + ///This is always safe since the configuration is validated during parsing. + ///## Example + ///```rust + ///use configparser::ini::{Ini, WriteOptions}; + /// + ///let mut write_options = WriteOptions::default(); + ///write_options.space_around_delimiters = true; + ///write_options.multiline_line_indentation = 2; + ///write_options.blank_lines_between_sections = 1; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[2000s] + /// 2020 = bad")); + ///let outstring = config.pretty_writes(&write_options); + ///``` + ///Returns a `String` type contatining the ini-syntax file. + pub fn pretty_writes(&self, write_options: &WriteOptions) -> String { + self.unparse(write_options) + } + + ///Private function that converts the currently stored configuration into a valid ini-syntax string. + fn unparse(&self, write_options: &WriteOptions) -> String { + // push key/value pairs in outmap to out string. + fn unparse_key_values( + out: &mut String, + outmap: &Map>, + multiline: bool, + space_around_delimiters: bool, + indent: usize, + ) { + let delimiter = if space_around_delimiters { " = " } else { "=" }; + let empty_delimiter = delimiter.trim_end(); + // Precompute the multiline indentation once rather than rebuilding + // it for every continuation line. Stays empty (no allocation) when + // multiline output is disabled. + let indent_str = if multiline { + " ".repeat(indent) + } else { + String::new() + }; + for (key, val) in outmap.iter() { + out.push_str(key); + + if let Some(value) = val { + if value.is_empty() { + out.push_str(empty_delimiter); + } else { + out.push_str(delimiter); + } + + if multiline { + let mut lines = value.lines(); + + out.push_str(lines.next().unwrap_or_default()); + + for line in lines { + out.push_str(LINE_ENDING); + if !line.is_empty() { + out.push_str(&indent_str); + out.push_str(line); + } + } + } else { + out.push_str(value); + } + } + + out.push_str(LINE_ENDING); + } + } + + let line_endings = LINE_ENDING.repeat(write_options.blank_lines_between_sections); + let mut out = String::new(); + + if let Some(defaultmap) = self.map.get(&self.default_section) { + unparse_key_values( + &mut out, + defaultmap, + self.multiline, + write_options.space_around_delimiters, + write_options.multiline_line_indentation, + ); + } + + let mut is_first = true; + for (section, secmap) in self.map.iter() { + if !is_first { + out.push_str(line_endings.as_ref()); + } + if section != &self.default_section { + write!(out, "[{}]", section).unwrap(); + out.push_str(LINE_ENDING); + unparse_key_values( + &mut out, + secmap, + self.multiline, + write_options.space_around_delimiters, + write_options.multiline_line_indentation, + ); + } + is_first = false; + } + out + } + + ///Private function that parses ini-style syntax into a Map. + fn parse(&self, input: String) -> Result>>, String> { + let inline_comment_symbols: &[char] = self + .inline_comment_symbols + .as_deref() + .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. + let mut section_exists = false; + 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 blank_lines = 0usize; + + for (num, raw_line) in input.lines().enumerate() { + 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(), + None => line, + } + } else { + line + }; + + 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 + )); + } + } + } + + // 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 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); + } + } + } else { + match trimmed.find(&self.delimiters[..]) { + Some(delimiter) => { + let key = caser(trimmed[..delimiter].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()); + } + + let value = trimmed[delimiter + 1..].trim().to_owned(); + + valmap.insert(key, Some(value)); + } + } + None => { + let key = caser(trimmed); + if self.multiline { + current_key = Some(key.clone()); + } + + valmap.insert(key, None); + } + } + } + + blank_lines = 0; + } + + 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()) + } else { + (section.to_lowercase(), key.to_lowercase()) + } + } + + ///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. + ///All `get` functions will do this automatically under the hood. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let value = config.get("default", "defaultvalues").unwrap(); + ///assert_eq!(value, String::from("defaultvalues")); + ///``` + ///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, + }; + + if val.is_none() && self.cascade_defaults { + return self.map.get(&self.default_section)?.get(&key)?.clone(); + } + + val + } + + ///Parses the stored value from the key stored in the defined section to a `bool`. + ///For ease of use, the function converts the type case-insensitively (`true` == `True`). + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let value = config.getbool("values", "bool").unwrap().unwrap(); + ///assert!(value); // value accessible! + ///``` + ///Returns `Ok(Some(value))` of type `bool` if value is found or else returns `Ok(None)`. + ///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)), + }, + _ => { + if self.cascade_defaults && section != self.default_section { + return self.getbool(&self.default_section, &key); + } + Ok(None) + } + } + } + + ///Parses the stored value from the key stored in the defined section to a `bool`. For ease of use, the function converts the type coerces a match. + ///It attempts to case-insenstively find `true`, `yes`, `t`, `y`, `1` and `on` to parse it as `True`. + ///Similarly it attempts to case-insensitvely find `false`, `no`, `f`, `n`, `0` and `off` to parse it as `False`. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let value = config.getboolcoerce("values", "boolcoerce").unwrap().unwrap(); + ///assert!(!value); // value accessible! + ///``` + ///Returns `Ok(Some(value))` of type `bool` if value is found or else returns `Ok(None)`. + ///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)) { + Some(Some(inner)) => { + let boolval = &inner.to_lowercase()[..]; + if self + .boolean_values + .get(&true) + .unwrap() + .iter() + .any(|elem| elem == boolval) + { + Ok(Some(true)) + } else if self + .boolean_values + .get(&false) + .unwrap() + .iter() + .any(|elem| elem == boolval) + { + Ok(Some(false)) + } else { + Err(format!( + "Unable to parse value into bool at {}:{}", + section, key + )) + } + } + _ => { + if self.cascade_defaults && section != self.default_section { + return self.getboolcoerce(&self.default_section, &key); + } + Ok(None) + } + } + } + + ///Parses the stored value from the key stored in the defined section to an `i64`. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let value = config.getint("values", "int").unwrap().unwrap(); + ///assert_eq!(value, -31415); // value accessible! + ///``` + ///Returns `Ok(Some(value))` of type `i64` if value is found or else returns `Ok(None)`. + ///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)) { + Some(Some(inner)) => match inner.parse::() { + Err(why) => Err(why.to_string()), + Ok(int) => Ok(Some(int)), + }, + _ => { + if self.cascade_defaults && section != self.default_section { + return self.getint(&self.default_section, &key); + } + Ok(None) + } + } + } + + ///Parses the stored value from the key stored in the defined section to a `u64`. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let value = config.getint("values", "Uint").unwrap().unwrap(); + ///assert_eq!(value, 31415); // value accessible! + ///``` + ///Returns `Ok(Some(value))` of type `u64` if value is found or else returns `Ok(None)`. + ///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)) { + Some(Some(inner)) => match inner.parse::() { + Err(why) => Err(why.to_string()), + Ok(uint) => Ok(Some(uint)), + }, + _ => { + if self.cascade_defaults && section != self.default_section { + return self.getuint(&self.default_section, &key); + } + Ok(None) + } + } + } + + ///Parses the stored value from the key stored in the defined section to a `f64`. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let value = config.getfloat("values", "float").unwrap().unwrap(); + ///assert_eq!(value, 3.1415); // value accessible! + ///``` + ///Returns `Ok(Some(value))` of type `f64` if value is found or else returns `Ok(None)`. + ///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)) { + Some(Some(inner)) => match inner.parse::() { + Err(why) => Err(why.to_string()), + Ok(float) => Ok(Some(float)), + }, + _ => { + if self.cascade_defaults && section != self.default_section { + return self.getfloat(&self.default_section, &key); + } + Ok(None) + } + } + } + + ///Returns a clone of the `Map` stored in our struct. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[section] + /// key=values")); + ///let map = config.get_map().unwrap(); + ///assert_eq!(map, *config.get_map_ref()); // the cloned map is basically a snapshot that you own + ///``` + ///Returns `Some(map)` if map is non-empty or else returns `None`. + ///Similar to `load()` but returns an `Option` type with the currently stored `Map`. + pub fn get_map(&self) -> Option>>> { + if self.map.is_empty() { + None + } else { + Some(self.map.clone()) + } + } + + ///Returns an immutable reference to the `Map` stored in our struct. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let mapclone = config.read(String::from + /// ("[topsecrets] + /// Valueless key")).unwrap(); + /////Think of the clone as being a snapshot at a point of time while the reference always points to the current configuration. + ///assert_eq!(*config.get_map_ref(), mapclone); // same as expected. + ///``` + ///If you just need to definitely mutate the map, use `get_mut_map()` instead. Alternatively, you can generate a snapshot by getting a clone + ///with `get_map()` and work with that. + pub fn get_map_ref(&self) -> &Map>> { + &self.map + } + + ///Returns a mutable reference to the `Map` stored in our struct. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from + /// ("[topsecrets] + /// Valueless key")); + /////We can then get the mutable map and insert a value like: + ///config.get_mut_map().get_mut("topsecrets").unwrap().insert(String::from("nuclear launch codes"), None); + ///assert_eq!(config.get("topsecrets", "nuclear launch codes"), None); // inserted successfully! + ///``` + ///If you just need to access the map without mutating, use `get_map_ref()` or make a clone with `get_map()` instead. + pub fn get_mut_map(&mut self) -> &mut Map>> { + &mut self.map + } + + ///Sets an `Option` in the `Map` stored in our struct. If a particular section or key does not exist, it will be automatically created. + ///An existing value in the map will be overwritten. You can also set `None` safely. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[section] + /// key=value")); + ///let key_value = String::from("value"); + ///config.set("section", "key", Some(key_value)); + ///config.set("section", "key", None); // also valid! + ///assert_eq!(config.get("section", "key"), None); // correct! + ///``` + ///Returns `None` if there is no existing value, else returns `Some(Option)`, with the existing value being the wrapped `Option`. + ///If you want to insert using a string literal, use `setstr()` instead. + pub fn set( + &mut self, + section: &str, + key: &str, + value: Option, + ) -> Option> { + let (section, key) = self.autocase(section, key); + match self.map.get_mut(§ion) { + Some(secmap) => secmap.insert(key, value), + None => { + let mut valmap: Map> = Map::new(); + valmap.insert(key, value); + self.map.insert(section, valmap); + None + } + } + } + + ///Sets an `Option<&str>` in the `Map` stored in our struct. If a particular section or key does not exist, it will be automatically created. + ///An existing value in the map will be overwritten. You can also set `None` safely. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[section] + /// key=notvalue")); + ///config.setstr("section", "key", Some("value")); + ///config.setstr("section", "key", None); // also valid! + ///assert_eq!(config.get("section", "key"), None); // correct! + ///``` + ///Returns `None` if there is no existing value, else returns `Some(Option)`, with the existing value being the wrapped `Option`. + ///If you want to insert using a `String`, use `set()` instead. + pub fn setstr( + &mut self, + section: &str, + key: &str, + value: Option<&str>, + ) -> Option> { + let (section, key) = self.autocase(section, key); + self.set(§ion, &key, value.map(String::from)) + } + + ///Clears the map, removing all sections and properties from the hashmap. It keeps the allocated memory for reuse. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[section] + /// key=somevalue")); + ///config.clear(); + ///assert!(config.get_map_ref().is_empty()); // our map is empty! + ///``` + ///Returns nothing. + pub fn clear(&mut self) { + self.map.clear(); + } + + ///Removes a section from the hashmap, returning the properties stored in the section if the section was previously in the map. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[section] + /// updog=whatsupdog")); + ///config.remove_section("section"); // this will return a cloned hashmap of the stored property + ///assert!(config.get_map_ref().is_empty()); // with the last section removed, our map is now empty! + ///``` + ///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() + }; + #[cfg(not(feature = "indexmap"))] + { + self.map.remove(§ion) + } + #[cfg(feature = "indexmap")] + { + self.map.swap_remove(§ion) + } + } + + ///Removes a key from a section in the hashmap, returning the value attached to the key if it was previously in the map. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[section] + /// updog=whatsupdog + /// [anothersection] + /// updog=differentdog")); + ///let val = config.remove_key("anothersection", "updog").unwrap().unwrap(); + ///assert_eq!(val, String::from("differentdog")); // with the last section removed, our map is now empty! + ///``` + ///Returns `Some(Option)` if the value exists or else, `None`. + pub fn remove_key(&mut self, section: &str, key: &str) -> Option> { + let (section, key) = self.autocase(section, key); + #[cfg(not(feature = "indexmap"))] + { + self.map.get_mut(§ion)?.remove(&key) + } + #[cfg(feature = "indexmap")] + { + self.map.get_mut(§ion)?.swap_remove(&key) + } + } +} + +#[cfg(feature = "tokio")] +impl Ini { + ///Loads a file asynchronously from a defined path, parses it and puts the hashmap into our struct. + ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. + /// + ///Usage is similar to `load`, but `.await` must be called after along with the usual async rules. + /// + ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. + ///Use `get_mut_map()` if you want a mutable reference. + pub async fn load_async>( + &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, + }; + Ok(self.map.clone()) + } + + ///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. + /// + ///Usage is similar to `load_and_append`, but `.await` must be called after along with the usual async rules. + /// + ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. + ///Use `get_mut_map()` if you want a mutable reference. + pub async fn load_and_append_async>( + &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); + } + + Ok(self.map.clone()) + } + + ///Writes the current configuation to the specified path asynchronously using default formatting. If a file is not present, it is automatically created for you, if a file already + ///exists, it is truncated and the configuration is written to it. + /// + ///Usage is the same as `write`, but `.await` must be called after along with the usual async rules. + /// + ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. + pub async fn write_async>(&self, path: T) -> std::io::Result<()> { + async_fs::write(path.as_ref(), self.unparse(&WriteOptions::default())).await + } + + ///Writes the current configuation to the specified path asynchronously using the given formatting options. If a file is not present, it is automatically created for you, if a file already + ///exists, it is truncated and the configuration is written to it. + /// + ///Usage is the same as `pretty_pretty_write`, but `.await` must be called after along with the usual async rules. + /// + ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. + pub async fn pretty_write_async>( + &self, + path: T, + write_options: &WriteOptions, + ) -> std::io::Result<()> { + async_fs::write(path.as_ref(), self.unparse(write_options)).await + } +} diff --git a/tests/test.rs b/tests/test.rs new file mode 100644 index 0000000..6807c52 --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,975 @@ +use configparser::ini::Ini; +use std::collections::HashSet; +use std::error::Error; + +#[cfg(feature = "indexmap")] +use configparser::ini::WriteOptions; + +#[test] +#[allow(clippy::approx_constant)] +fn non_cs() -> Result<(), Box> { + let mut config = Ini::new(); + let map = config.load("tests/test.ini")?; + config.set_comment_symbols(&[';', '#', '!']); + let inpstring = config.read( + "defaultvalues=defaultvalues + [topsecret] + KFC = the secret herb is orega- + colon:value after colon + Empty string = + None string + Password=[in-brackets] + [ spacing ] + indented=indented + not indented = not indented ;testcomment + !modified comment + [values]#another comment + Bool = True + Boolcoerce = 0 + Int = -31415 + Uint = 31415 + Float = 3.1415" + .to_owned(), + )?; + assert_eq!(map, inpstring); + config.set("DEFAULT", "defaultvalues", Some("notdefault".to_owned())); + assert_eq!( + config.get("DEFAULT", "defaultvalues").unwrap(), + "notdefault" + ); + config.setstr("DEFAULT", "defaultvalues", Some("defaultvalues")); + assert_eq!( + config.get("DEFAULT", "defaultvalues").unwrap(), + "defaultvalues" + ); + config.setstr("DEFAULT", "defaultvalues", None); + config.write("output.ini")?; + let map2 = config.clone().load("output.ini")?; + assert_eq!(map2, *config.get_map_ref()); + let map3 = config.clone().read(config.writes())?; + assert_eq!(map2, map3); + assert_eq!(config.sections().len(), 4); + assert_eq!(config.get("DEFAULT", "defaultvalues"), None); + assert_eq!( + config.get("topsecret", "KFC").unwrap(), + "the secret herb is orega-" + ); + assert_eq!(config.get("topsecret", "Empty string").unwrap(), ""); + assert_eq!(config.get("topsecret", "None string"), None); + assert_eq!(config.get("spacing", "indented").unwrap(), "indented"); + assert_eq!( + config.get("spacing", "not indented").unwrap(), + "not indented" + ); + assert_eq!( + config.get("topsecret", "colon").unwrap(), + "value after colon" + ); + assert!(config.getbool("values", "Bool")?.unwrap()); + assert!(!config.getboolcoerce("values", "Boolcoerce")?.unwrap()); + assert_eq!(config.getint("values", "Int")?.unwrap(), -31415); + assert_eq!(config.getuint("values", "Uint")?.unwrap(), 31415); + assert_eq!(config.getfloat("values", "Float")?.unwrap(), 3.1415); + assert_eq!(config.getfloat("topsecret", "None string"), Ok(None)); + assert_eq!( + map["default"]["defaultvalues"].clone().unwrap(), + "defaultvalues" + ); + assert_eq!( + map["topsecret"]["kfc"].clone().unwrap(), + "the secret herb is orega-" + ); + assert_eq!(map["topsecret"]["empty string"].clone().unwrap(), ""); + assert_eq!(map["topsecret"]["none string"], None); + assert_eq!(map["spacing"]["indented"].clone().unwrap(), "indented"); + assert_eq!( + map["spacing"]["not indented"].clone().unwrap(), + "not indented" + ); + let mut config2 = config.clone(); + let val = config2.remove_key("default", "defaultvalues"); + assert_eq!(val, Some(None)); + assert_eq!(config2.get("default", "defaultvalues"), None); + config2.remove_section("default"); + assert_eq!(config2.get("default", "nope"), None); + let mut_map = config.get_mut_map(); + mut_map.get_mut("topsecret").unwrap().insert( + String::from("none string"), + Some(String::from("None string")), + ); + assert_eq!( + mut_map["topsecret"]["none string"].clone().unwrap(), + "None string" + ); + mut_map.clear(); + config2.clear(); + assert_eq!(config.get_map_ref(), config2.get_map_ref()); + + config.load("tests/test.ini")?; + config.read_and_append("defaultvalues=somenewvalue".to_owned())?; + assert_eq!( + config.get("default", "defaultvalues").unwrap(), + "somenewvalue" + ); + assert_eq!( + config.get("topsecret", "KFC").unwrap(), + "the secret herb is orega-" + ); + + let mut config3 = config.clone(); + let mut_map = config3.get_mut_map(); + mut_map.clear(); + config3.load("tests/test.ini")?; + config3.load_and_append("tests/test_more.ini")?; + assert_eq!( + config3.get("default", "defaultvalues").unwrap(), + "overwritten" + ); + assert_eq!(config3.get("topsecret", "KFC").unwrap(), "redacted"); + // spacing -> indented exists in tests/test.ini, but not tests/test_more.ini + assert_eq!(config3.get("spacing", "indented").unwrap(), "indented"); + assert!(!config3.getbool("values", "Bool")?.unwrap()); + + Ok(()) +} + +#[test] +#[allow(clippy::approx_constant)] +fn cs() -> Result<(), Box> { + let mut config = Ini::new_cs(); + let map = config.load("tests/test.ini")?; + config.set_comment_symbols(&[';', '#', '!']); + let inpstring = config.read( + "defaultvalues=defaultvalues + [topsecret] + KFC = the secret herb is orega- + colon:value after colon + Empty string = + None string + Password=[in-brackets] + [ spacing ] + indented=indented + not indented = not indented ;testcomment + !modified comment + [values]#another comment + Bool = True + Boolcoerce = 0 + Int = -31415 + Uint = 31415 + Float = 3.1415" + .to_owned(), + )?; + assert_eq!(map, inpstring); + config.set("default", "defaultvalues", Some("notdefault".to_owned())); + assert_eq!( + config.get("default", "defaultvalues").unwrap(), + "notdefault" + ); + config.setstr("default", "defaultvalues", Some("defaultvalues")); + assert_eq!( + config.get("default", "defaultvalues").unwrap(), + "defaultvalues" + ); + config.setstr("default", "defaultvalues", None); + config.write("output2.ini")?; + let map2 = config.clone().load("output2.ini")?; + assert_eq!(map2, *config.get_map_ref()); + let map3 = config.clone().read(config.writes())?; + assert_eq!(map2, map3); + assert_eq!(config.sections().len(), 4); + assert_eq!(config.get("default", "defaultvalues"), None); + assert_eq!( + config.get("topsecret", "KFC").unwrap(), + "the secret herb is orega-" + ); + assert_eq!(config.get("topsecret", "Empty string").unwrap(), ""); + assert_eq!(config.get("topsecret", "None string"), None); + assert_eq!(config.get("spacing", "indented").unwrap(), "indented"); + assert_eq!( + config.get("spacing", "not indented").unwrap(), + "not indented" + ); + assert_eq!( + config.get("topsecret", "colon").unwrap(), + "value after colon" + ); + assert!(config.getbool("values", "Bool")?.unwrap()); + assert!(!config.getboolcoerce("values", "Boolcoerce")?.unwrap()); + assert_eq!(config.getint("values", "Int")?.unwrap(), -31415); + assert_eq!(config.getuint("values", "Uint")?.unwrap(), 31415); + assert_eq!(config.getfloat("values", "Float")?.unwrap(), 3.1415); + assert_eq!(config.getfloat("topsecret", "None string"), Ok(None)); + assert_eq!( + map["default"]["defaultvalues"].clone().unwrap(), + "defaultvalues" + ); + assert_eq!( + map["topsecret"]["KFC"].clone().unwrap(), + "the secret herb is orega-" + ); + assert_eq!(map["topsecret"]["Empty string"].clone().unwrap(), ""); + assert_eq!(map["topsecret"]["None string"], None); + assert_eq!(map["spacing"]["indented"].clone().unwrap(), "indented"); + assert_eq!( + map["spacing"]["not indented"].clone().unwrap(), + "not indented" + ); + let mut config2 = config.clone(); + let val = config2.remove_key("default", "defaultvalues"); + assert_eq!(val, Some(None)); + assert_eq!(config2.get("default", "defaultvalues"), None); + config2.remove_section("default"); + assert_eq!(config2.get("default", "nope"), None); + let mut_map = config.get_mut_map(); + mut_map.get_mut("topsecret").unwrap().insert( + String::from("none string"), + Some(String::from("None string")), + ); + assert_eq!( + mut_map["topsecret"]["none string"].clone().unwrap(), + "None string" + ); + mut_map.clear(); + config2.clear(); + assert_eq!(config.get_map_ref(), config2.get_map_ref()); + Ok(()) +} + +#[test] +fn ensure_empty_sections_exist() -> Result<(), Box> { + const FILE_CONTENTS: &str = " +[basic_section] +basic_option=basic_value +[empty_section] +"; + + let mut config = Ini::new(); + config.read(FILE_CONTENTS.to_owned())?; + + assert_eq!( + HashSet::from_iter(config.sections().into_iter()), + HashSet::from([String::from("basic_section"), String::from("empty_section")]) + ); + + Ok(()) +} + +#[test] +fn inline_comment_symbols_enabled() -> Result<(), Box> { + const FILE_CONTENTS: &str = " +[basic_section] +; basic comment + ; comment with space +! extra_comment= +basic_option=value +basic_with_comment=value ; Simple comment +basic_with_extra_inline=value ! comment +empty_option= +"; + + let mut config = Ini::new(); + config.read(FILE_CONTENTS.to_owned())?; + + assert_eq!( + config.get("basic_section", "basic_option"), + Some(String::from("value")) + ); + assert_eq!( + config.get("basic_section", "basic_with_comment"), + Some(String::from("value")) + ); + assert_eq!( + config.get("basic_section", "basic_with_extra_inline"), + Some(String::from("value ! comment")) + ); + assert_eq!( + config.get("basic_section", "! extra_comment"), + Some(String::from("")) + ); + + assert_eq!( + config.get("basic_section", "empty_option"), + Some(String::from("")) + ); + + config.set_inline_comment_symbols(Some(&['!'])); + + config.read(FILE_CONTENTS.to_owned())?; + + assert_eq!( + config.get("basic_section", "basic_option"), + Some(String::from("value")) + ); + assert_eq!( + config.get("basic_section", "basic_with_comment"), + Some(String::from("value ; Simple comment")) + ); + assert_eq!( + config.get("basic_section", "basic_with_extra_inline"), + Some(String::from("value")) + ); + + config.set_inline_comment_symbols(Some(&[])); + + config.read(FILE_CONTENTS.to_owned())?; + + assert_eq!( + config.get("basic_section", "basic_option"), + Some(String::from("value")) + ); + assert_eq!( + config.get("basic_section", "basic_with_comment"), + Some(String::from("value ; Simple comment")) + ); + assert_eq!( + config.get("basic_section", "basic_with_extra_inline"), + Some(String::from("value ! comment")) + ); + + Ok(()) +} + +#[test] +fn inline_comment_symbols_disabled() -> Result<(), Box> { + use configparser::ini::IniDefault; + + const FILE_CONTENTS: &str = " +[basic_section] +; basic comment + ; comment with space +basic_option=value +basic_with_comment=value ; Simple comment +basic_with_comment_hash=value # Simple comment +basic_with_extra_inline=value ! comment +empty_option= +"; + + let mut parser_options = IniDefault::default(); + // true tested in inline_comment_symbols_enabled() + parser_options.enable_inline_comments = false; + + let mut config = Ini::new_from_defaults(parser_options); + config.read(FILE_CONTENTS.to_owned())?; + + assert_eq!( + config.get("basic_section", "basic_with_comment"), + Some(String::from("value ; Simple comment")) + ); + assert_eq!( + config.get("basic_section", "basic_with_comment_hash"), + Some(String::from("value # Simple comment")) + ); + assert_eq!( + config.get("basic_section", "basic_with_extra_inline"), + Some(String::from("value ! comment")) + ); + assert_eq!( + config.get("basic_section", "empty_option"), + Some(String::from("")) + ); + + config.set_inline_comment_symbols(Some(&['!'])); + config.read(FILE_CONTENTS.to_owned())?; + + assert_eq!( + config.get("basic_section", "basic_with_comment"), + Some(String::from("value ; Simple comment")) + ); + assert_eq!( + config.get("basic_section", "basic_with_extra_inline"), + Some(String::from("value ! comment")) + ); + + Ok(()) +} + +#[test] +#[allow(clippy::approx_constant)] +fn test_cascade_defaults() -> Result<(), Box> { + use configparser::ini::IniDefault; + + const FILE_CONTENTS: &str = " +[DEFAULT] +fallback_str = fallback value +fallback_bool = true +fallback_bool_c = 1 +fallback_int = -42 +fallback_uint = 42 +fallback_float = 3.1415 + +[fallback_section] +normal = section value +other_bool = false +other_int = -7 +other_uint = 7 +other_float = 1.1 + +[override_section] +other_key = some other value +fallback_str = non-fallback value +fallback_bool = false +fallback_bool_c = 0 +fallback_int = -1 +fallback_uint = 1 +fallback_float = -1.1 +"; + + let mut parser_options = IniDefault::default(); + // true tested in inline_comment_symbols_enabled() + parser_options.cascade_defaults = true; + + let mut config = Ini::new_from_defaults(parser_options); + config.read(FILE_CONTENTS.to_owned())?; + + assert!(config.defaults().cascade_defaults); + // test string values and basics + assert_eq!( + config.get("default", "fallback_str"), + Some(String::from("fallback value")) + ); + assert_eq!( + config.get("fallback_section", "normal"), + Some(String::from("section value")) + ); + assert_eq!( + config.get("fallback_section", "fallback_str"), + Some(String::from("fallback value")) + ); + assert_eq!( + config.get("override_section", "fallback_str"), + Some(String::from("non-fallback value")) + ); + assert_eq!( + config.get("override_section", "other_key"), + Some(String::from("some other value")) + ); + + // test bool parsing with fallback + assert_eq!(config.getbool("default", "fallback_bool"), Ok(Some(true))); + assert_eq!( + config.getbool("fallback_section", "fallback_bool"), + Ok(Some(true)) + ); + assert_eq!( + config.getbool("fallback_section", "other_bool"), + Ok(Some(false)) + ); + assert_eq!( + config.getbool("override_section", "fallback_bool"), + Ok(Some(false)) + ); + assert_eq!( + config.getboolcoerce("default", "fallback_bool_c"), + Ok(Some(true)) + ); + assert_eq!( + config.getboolcoerce("fallback_section", "fallback_bool_c"), + Ok(Some(true)) + ); + assert_eq!( + config.getboolcoerce("override_section", "fallback_bool_c"), + Ok(Some(false)) + ); + + // test the getint and getuint fallbacks + assert_eq!(config.getint("default", "fallback_int"), Ok(Some(-42))); + assert_eq!( + config.getint("fallback_section", "fallback_int"), + Ok(Some(-42)) + ); + assert_eq!(config.getint("fallback_section", "other_int"), Ok(Some(-7))); + assert_eq!( + config.getint("override_section", "fallback_int"), + Ok(Some(-1)) + ); + assert_eq!(config.getuint("default", "fallback_uint"), Ok(Some(42))); + assert_eq!( + config.getuint("fallback_section", "fallback_uint"), + Ok(Some(42)) + ); + assert_eq!( + config.getuint("fallback_section", "other_uint"), + Ok(Some(7)) + ); + assert_eq!( + config.getuint("override_section", "fallback_uint"), + Ok(Some(1)) + ); + + // test the float fallback + assert_eq!( + config.getfloat("default", "fallback_float"), + Ok(Some(3.1415)) + ); + assert_eq!( + config.getfloat("fallback_section", "fallback_float"), + Ok(Some(3.1415)) + ); + assert_eq!( + config.getfloat("fallback_section", "other_float"), + Ok(Some(1.1)) + ); + assert_eq!( + config.getfloat("override_section", "fallback_float"), + Ok(Some(-1.1)) + ); + + // and finally, one last set of tests with cascade_defaults disabled to + // ensure backwards compatibility + config.set_cascade_defaults(false); + assert_eq!(config.get("fallback_section", "fallback_str"), None); + assert_eq!( + config.getbool("fallback_section", "fallback_bool"), + Ok(None) + ); + assert_eq!( + config.getboolcoerce("fallback_section", "fallback_bool"), + Ok(None) + ); + assert_eq!(config.getint("fallback_section", "fallback_int"), Ok(None)); + assert_eq!( + config.getuint("fallback_section", "fallback_uint"), + Ok(None) + ); + assert_eq!( + config.getfloat("fallback_section", "fallback_float"), + Ok(None) + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "indexmap")] +fn sort_on_write() -> Result<(), Box> { + let mut config = Ini::new_cs(); + config.load("tests/test.ini")?; + + assert_eq!( + config.writes(), + "defaultvalues=defaultvalues +[topsecret] +KFC=the secret herb is orega- +colon=value after colon +Empty string= +None string +Password=[in-brackets] +[spacing] +indented=indented +not indented=not indented +[values] +Bool=True +Boolcoerce=0 +Int=-31415 +Uint=31415 +Float=3.1415 +" + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "indexmap")] +fn pretty_writes_result_is_formatted_correctly() -> Result<(), Box> { + use configparser::ini::IniDefault; + + const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues +[topsecret] +KFC=the secret herb is orega- +Empty string= +None string +Password=[in-brackets] +[Section] +Key1: Value1 +Key2: this is a haiku + spread across separate lines + a single value +Key3: another value +"; + + let mut ini_defaults = IniDefault::default(); + ini_defaults.case_sensitive = true; + ini_defaults.multiline = true; + let mut config = Ini::new_from_defaults(ini_defaults); + config.read(OUT_FILE_CONTENTS.to_owned())?; + + let mut write_options = WriteOptions::default(); + write_options.space_around_delimiters = true; + write_options.multiline_line_indentation = 2; + write_options.blank_lines_between_sections = 1; + assert_eq!( + config.pretty_writes(&write_options), + "defaultvalues = defaultvalues + +[topsecret] +KFC = the secret herb is orega- +Empty string = +None string +Password = [in-brackets] + +[Section] +Key1 = Value1 +Key2 = this is a haiku + spread across separate lines + a single value +Key3 = another value +" + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "indexmap")] +#[cfg(feature = "tokio")] +fn pretty_write_result_is_formatted_correctly() -> Result<(), Box> { + use configparser::ini::IniDefault; + + const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues +[topsecret] +KFC=the secret herb is orega- +Empty string= +None string +Password=[in-brackets] +[Section] +Key1: Value1 +Key2: this is a haiku + spread across separate lines + a single value +Key3: another value +"; + + let mut ini_defaults = IniDefault::default(); + ini_defaults.case_sensitive = true; + ini_defaults.multiline = true; + let mut config = Ini::new_from_defaults(ini_defaults); + config.read(OUT_FILE_CONTENTS.to_owned())?; + + let mut write_options = WriteOptions::default(); + write_options.space_around_delimiters = true; + write_options.multiline_line_indentation = 2; + write_options.blank_lines_between_sections = 1; + config.pretty_write("pretty_output.ini", &write_options)?; + + let file_contents = std::fs::read_to_string("pretty_output.ini")?; + assert_eq!( + file_contents, + "defaultvalues = defaultvalues + +[topsecret] +KFC = the secret herb is orega- +Empty string = +None string +Password = [in-brackets] + +[Section] +Key1 = Value1 +Key2 = this is a haiku + spread across separate lines + a single value +Key3 = another value +" + ); + + Ok(()) +} + +#[tokio::test] +#[cfg(feature = "indexmap")] +#[cfg(feature = "tokio")] +async fn async_pretty_print_result_is_formatted_correctly() -> Result<(), Box> { + use configparser::ini::IniDefault; + + const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues +[topsecret] +KFC=the secret herb is orega- +Empty string= +None string +Password=[in-brackets] +[Section] +Key1: Value1 +Key2: this is a haiku + spread across separate lines + a single value +Key3: another value +"; + + let mut ini_defaults = IniDefault::default(); + ini_defaults.case_sensitive = true; + ini_defaults.multiline = true; + let mut config = Ini::new_from_defaults(ini_defaults); + config.read(OUT_FILE_CONTENTS.to_owned())?; + + let mut write_options = WriteOptions::default(); + write_options.space_around_delimiters = true; + write_options.multiline_line_indentation = 2; + write_options.blank_lines_between_sections = 1; + config + .pretty_write_async("pretty_output_async.ini", &write_options) + .await + .map_err(|e| e.to_string())?; + + let file_contents = std::fs::read_to_string("pretty_output_async.ini")?; + assert_eq!( + file_contents, + "defaultvalues = defaultvalues + +[topsecret] +KFC = the secret herb is orega- +Empty string = +None string +Password = [in-brackets] + +[Section] +Key1 = Value1 +Key2 = this is a haiku + spread across separate lines + a single value +Key3 = another value +" + ); + + Ok(()) +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn async_load_write() -> Result<(), Box> { + const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues + [topsecret] + KFC = the secret herb is orega- + colon:value after colon + Empty string = + None string + Password=[in-brackets] + [ spacing ] + indented=indented + not indented = not indented ;testcomment + !modified comment + [values]#another comment + Bool = True + Boolcoerce = 0 + Int = -31415 + Uint = 31415 + Float = 3.1415"; + + let mut config = Ini::new(); + config.read(OUT_FILE_CONTENTS.to_owned())?; + config.write("output_sync.ini")?; + + let mut config_async = Ini::new(); + config_async.read(OUT_FILE_CONTENTS.to_owned())?; + config_async + .write_async("output_async.ini") + .await + .map_err(|e| e.to_string())?; + + let mut sync_content = Ini::new(); + sync_content.load("output_sync.ini")?; + + let mut async_content = Ini::new(); + async_content.load_async("output_async.ini").await?; + + assert_eq!(sync_content, async_content); + + Ok(()) +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn async_load_and_append() -> Result<(), Box> { + let mut sync_content = Ini::new(); + sync_content.load("tests/test.ini")?; + sync_content.load_and_append("tests/test_more.ini")?; + + let mut async_content = Ini::new(); + async_content.load_async("tests/test.ini").await?; + async_content + .load_and_append_async("tests/test_more.ini") + .await?; + + assert_eq!(sync_content, async_content); + + Ok(()) +} + +#[test] +#[cfg(feature = "indexmap")] +fn multiline_off() -> Result<(), Box> { + let mut config = Ini::new_cs(); + config.load("tests/test_multiline.ini")?; + + let map = config.get_map_ref(); + + let section = map.get("Section").unwrap(); + + assert_eq!(config.get("Section", "Key1").unwrap(), "Value1"); + assert_eq!(config.get("Section", "Key2").unwrap(), "Value Two"); + assert_eq!(config.get("Section", "Key3").unwrap(), "this is a haiku"); + assert!(section.contains_key("spread across separate lines")); + assert!(section.contains_key("a single value")); + + assert_eq!(config.get("Section", "Key4").unwrap(), "Four"); + + assert_eq!( + config.writes(), + "[Section] +Key1=Value1 +Key2=Value Two +Key3=this is a haiku +spread across separate lines +a single value +Key4=Four +" + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "indexmap")] +fn multiline_on() -> Result<(), Box> { + let mut config = Ini::new_cs(); + config.set_multiline(true); + config.load("tests/test_multiline.ini")?; + + assert_eq!(config.get("Section", "Key1").unwrap(), "Value1"); + assert_eq!(config.get("Section", "Key2").unwrap(), "Value Two"); + assert_eq!( + config.get("Section", "Key3").unwrap(), + "this is a haiku\nspread across separate lines\n\na single value" + ); + assert_eq!(config.get("Section", "Key4").unwrap(), "Four"); + + assert_eq!( + config.writes(), + "[Section] +Key1=Value1 +Key2=Value Two +Key3=this is a haiku + spread across separate lines + + a single value +Key4=Four +" + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "serde")] +fn serde_roundtrip() -> Result<(), Box> { + // 1. Load original from file + let mut original = Ini::new(); + let map1 = original.load("tests/test.ini")?; + + // 2. Serialize to JSON + let json = serde_json::to_string(&original)?; + + // 3. Deserialize back + let deserialized: Ini = serde_json::from_str(&json)?; + let map2 = deserialized + .get_map() + .expect("deserialized map should be non-empty"); + + // 4a. Quick equality check on the entire map + assert_eq!(map1, map2, "entire maps must match"); + + // 4b. Cross-check every section, key, and value + for (section, secmap) in &map1 { + // Ensure section exists + let sec2 = map2 + .get(section) + .unwrap_or_else(|| panic!("section `{}` missing", section)); + for (key, val1) in secmap { + let val2 = sec2 + .get(key) + .unwrap_or_else(|| panic!("key `{}` missing in section `{}`", key, section)); + assert_eq!( + val1, val2, + "mismatch at [{}] {}: {:?} != {:?}", + section, key, val1, val2 + ); + } + } + + Ok(()) +} + +#[test] +#[cfg(all(feature = "serde", feature = "indexmap"))] +fn serde_indexmap_roundtrip() -> Result<(), Box> { + // Same as above but with IndexMap ordering preserved + let mut original = Ini::new(); + let map1 = original.load("tests/test.ini")?; + + let json = serde_json::to_string(&original)?; + let deserialized: Ini = serde_json::from_str(&json)?; + let map2 = deserialized + .get_map() + .expect("deserialized map should be non-empty"); + + // Because IndexMap preserves insertion order, we still use equality + assert_eq!( + map1, map2, + "IndexMap-backed maps must match in content and order" + ); + Ok(()) +} + +#[test] +#[cfg(feature = "serde")] +fn serde_multiline_roundtrip() -> Result<(), Box> { + // 1. Build a case-sensitive Ini with multiline enabled + let mut orig = Ini::new(); + orig.set_multiline(true); + + // 2. Load the multiline fixture + let map1 = orig.load("tests/test_multiline.ini")?; + // 3. Capture the Key3 value before Serde + let before = orig.get("Section", "Key3").unwrap(); + + // 4. Serialize to JSON and back + let json = serde_json::to_string(&orig)?; + let mut deser: Ini = serde_json::from_str(&json)?; + + // 5. Re-enable multiline on the deserialized Ini + deser.set_multiline(true); + let after = deser.get("Section", "Key3").unwrap(); + + // 6. The entire map should match and Key3 must survive intact + let map2 = deser.get_map().unwrap(); + assert_eq!(map1, map2); + assert_eq!(after, before); + + Ok(()) +} + +#[test] +#[cfg(feature = "serde")] +fn serde_case_sensitive_roundtrip() -> Result<(), Box> { + // 1. Load in case-sensitive mode + let mut orig = Ini::new_cs(); + orig.load("tests/test.ini")?; + // 2. Check that mixed-case keys work, lowercase doesn't + let v1 = orig.get("default", "defaultvalues").unwrap(); + assert!(orig.get("default", "DefaultValues").is_none()); + + // 3. Serde round-trip + let json = serde_json::to_string(&orig)?; + let deser_plain: Ini = serde_json::from_str(&json)?; + let map2 = deser_plain.get_map().unwrap(); + + // 4. Reconstruct a case-sensitive Ini and inject the map + let mut deser_cs = Ini::new_cs(); + *deser_cs.get_mut_map() = map2; + + // 5. Assert the exact-case key still exists and lowercase still fails + let v2 = deser_cs.get("default", "defaultvalues").unwrap(); + assert_eq!(v2, v1); + assert!(deser_cs.get("default", "DefaultValues").is_none()); + + Ok(()) +}