use std::fmt;
use std::fmt::Formatter;
use std::fmt::Write;
use std::collections::HashMap;
use std::path::Path;
use std::fs;
use regex::Regex;
use lazy_static::lazy_static;
lazy_static! {
static ref SECTION_REGEX: Regex = Regex::new(r#"\[(.*)\]"#).unwrap();
static ref KEY_VALUE_REGEX: Regex = Regex::new(r#"^([^=]+)=(.*)(.*)"#).unwrap();
}
pub type Result<T> = std::result::Result<T, IniError>;
#[derive(Debug)]
pub struct IniError {
e: String
}
impl fmt::Display for IniError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.e.as_str())
}
}
impl std::error::Error for IniError {}
impl<T> From<T> for IniError where T: AsRef<str> {
fn from(i: T) -> Self {
let r = i.as_ref();
Self {
e: r.to_string()
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct Ini {
pub(crate) sections: Vec<IniSection>,
pub(crate) globals: HashMap<String, String>
}
impl Ini {
pub fn new() -> Self {
Self {
sections: Vec::new(),
globals: HashMap::new()
}
}
pub fn from_file<P>(path: P) -> Result<Self> where P: AsRef<Path> {
let path = path.as_ref();
let contents = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => return Err(e.to_string().into())
};
Self::deserialize(contents)
}
pub fn to_file<P>(&self, path: P) -> Result<()> where P: AsRef<Path> {
let serialized = self.serialize()?;
match fs::write(path, serialized) {
Ok(_) => Ok(()),
Err(e) => return Err(e.to_string().into())
}
}
pub fn deserialize<T>(input: T) -> Result<Self> where T: AsRef<str> {
let input = input.as_ref();
let mut globals = HashMap::new();
let mut sections = Vec::new();
let mut working_section = None;
for line in input.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if line.starts_with(";") {
continue;
}
if line.starts_with("[") {
if let Some(section) = working_section {
sections.push(section);
}
let captures = match SECTION_REGEX.captures(line) {
Some(c) => c,
None => return Err(format!("Invalid INI file. Line {} is invalid! (No matches)", line).into())
};
let section_name = match captures.get(1) {
Some(n) => n,
None => return Err(format!("Invalid INI File. Line {} is invalid! (Capture group 1)", line).into())
};
let section = IniSection::new(section_name.as_str());
working_section = Some(section)
} else if let Some(section) = &mut working_section {
let (k, v) = Self::get_kv(line)?;
section.keys.insert(k.to_string(), v.to_string());
} else {
let (k, v) = Self::get_kv(line)?;
globals.insert(k.to_string(), v.to_string());
}
}
if let Some(section) = working_section {
sections.push(section);
}
Ok(Self {
sections,
globals
})
}
pub fn serialize(&self) -> Result<String> {
let mut builder = String::new();
for (k, v) in &self.globals {
let _ = writeln!(&mut builder, "{}={}", k, v);
}
for section in &self.sections {
let _ = writeln!(&mut builder, "");
let _ = writeln!(&mut builder, "[{}]", section.name);
for (k, v) in §ion.keys {
let _ = writeln!(&mut builder, "{}={}", k, v);
}
}
Ok(builder)
}
pub fn add_section(&mut self, section: IniSection) {
self.sections.push(section);
}
pub fn add_section_with_values<T, K, V>(&mut self, name: T, values: HashMap<K, V>) where T: AsRef<str>, K: AsRef<str>, V: AsRef<str> {
let keys: HashMap<String, String> = values.iter().map(|(k, v)| (k.as_ref().to_string(), v.as_ref().to_string())).collect();
let section = IniSection {
name: name.as_ref().to_string(),
keys
};
self.sections.push(section);
}
pub fn get_section<K>(&self, name: K) -> Option<&IniSection> where K: AsRef<str> {
for section in &self.sections {
if section.name.eq(name.as_ref()) {
return Some(section)
}
}
None
}
pub fn get_section_mut<K>(&mut self, name: K) -> Option<&mut IniSection> where K: AsRef<str> {
for section in &mut self.sections {
if section.name.eq(name.as_ref()) {
return Some(section)
}
}
None
}
pub fn get<K>(&self, key: K) -> Option<&str> where K: AsRef<str> {
match self.globals.get(key.as_ref()) {
Some(g) => Some(g.as_str()),
None => None
}
}
pub fn set<K, V>(&mut self, key: K, value: V) where K: AsRef<str>, V: AsRef<str> {
self.globals.insert(key.as_ref().to_string(), value.as_ref().to_string());
}
fn get_kv(line: &str) -> Result<(&str, &str)> {
let captures = match KEY_VALUE_REGEX.captures(line) {
Some(c) => c,
None => return Err(format!("Invalid INI file. Line {} is invalid!", line).into())
};
let k = match captures.get(1) {
Some(k) => k,
None => return Err(format!("Invalid INI file. Line {} is invalid!", line).into())
};
let v = match captures.get(2) {
Some(v) => v,
None => return Err(format!("Invalid INI file. Line {} is invalid!", line).into())
};
Ok((k.as_str(), v.as_str()))
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct IniSection {
pub(crate) name: String,
pub(crate) keys: HashMap<String, String>
}
impl IniSection {
pub fn new<T>(name: T) -> Self where T: AsRef<str> {
let name = name.as_ref().to_string();
Self {
name,
keys: HashMap::new()
}
}
pub fn set<K, V>(&mut self, key: K, value: V) where K: AsRef<str>, V: AsRef<str> {
self.keys.insert(key.as_ref().to_string(), value.as_ref().to_string());
}
pub fn get<K>(&self, key: K) -> Option<&str> where K: AsRef<str> {
match self.keys.get(key.as_ref()) {
Some(v) => Some(v.as_str()),
None => None
}
}
}
#[cfg(test)]
mod test {
use crate::{Ini, IniSection};
use std::collections::HashMap;
use std::array::IntoIter;
use std::iter::FromIterator;
#[test]
fn parse_from_str() {
let ini = "some_global=VALUE\n\n[A]\nsome_section_variable=OTHER_VALUE\n";
let parsed = Ini::deserialize(ini).unwrap();
let correct = Ini { globals: HashMap::from_iter(IntoIter::new([("some_global".to_string(), "VALUE".to_string())])), sections: vec![
IniSection {
name: "A".to_string(),
keys: HashMap::from_iter(IntoIter::new([("some_section_variable".to_string(), "OTHER_VALUE".to_string())]))
}
]};
assert_eq!(correct, parsed);
}
#[test]
fn set_replace() {
let mut ini = Ini::new();
ini.set("foo", "bar");
ini.set("foo", "baz");
let serialized = ini.serialize().unwrap();
assert_eq!(&serialized, "foo=baz\n");
}
#[test]
fn serialize_to_str() {
let ini = "some_global=VALUE\n\n[A]\nsome_section_variable=OTHER_VALUE\n";
let parsed = Ini::deserialize(ini).unwrap();
let serialized = parsed.serialize().unwrap();
assert_eq!(serialized, ini);
}
#[test]
fn test_value_with_equals() {
let mut ini = Ini::new();
ini.set("foo", "'bar=baz'");
let serialized = ini.serialize().unwrap();
assert_eq!(&serialized, "foo='bar=baz'\n");
let deserialized = Ini::deserialize(&serialized).expect("Failed to deserialize");
assert_eq!(deserialized.get("foo").expect("Missing foo"), "'bar=baz'")
}
#[test]
fn build_manually() {
let ini = "some_global=VALUE\n\n[A]\nsome_section_variable=OTHER_VALUE\n[B]\nvar_b=value_b";
let parsed = Ini::deserialize(ini).unwrap();
let mut ini = Ini::new();
ini.set("some_global", "VALUE");
let mut section = IniSection::new("A");
section.set("some_section_variable", "OTHER_VALUE");
ini.add_section(section);
ini.add_section_with_values("B", HashMap::from_iter(IntoIter::new([("var_b", "value_b")])));
assert_eq!(parsed, ini);
}
}