use std::fs::{File, OpenOptions};
use std::io::{self, Write, BufRead, BufReader, Cursor};
use std::collections::HashMap;
use std::env::consts::OS;
use std::fmt;
use std::path::Path;
#[derive(Clone)]
pub struct Ini {
pub config_map: HashMap<String, HashMap<String, String>>,
pub global_params: HashMap<String, String>,
pub config_file: String,
ignore_case: bool,
}
const CONFIG_SECTION_START: &str = "[";
const CONFIG_SECTION_END: &str = "]";
const CONFIG_KVP_SPLIT: &str = "=";
const CONFIG_COMMENT_HASH: &str = "#";
const CONFIG_COMMENT_SEMI: &str = ";";
const NEW_LINE_WINDOWS: &str = "\r\n";
const NEW_LINE_LINUX: &str = "\n";
impl Ini {
pub fn new(location: String) -> Result<Ini, io::Error> {
if !Path::new(&location).exists() {
return Ok(Ini { config_map: HashMap::new(), global_params: HashMap::new(), config_file: location, ignore_case: false });
}
let f = File::open(&location)?;
let mut ret = match Self::build_struct(BufReader::new(f), false) {
Ok(x) => x,
Err(e) => return Err(e)
};
ret.config_file = location;
Ok(ret)
}
pub fn new_ignore_case(location: String) -> Result<Ini, io::Error> {
if !Path::new(&location).exists() {
return Ok(Ini { config_map: HashMap::new(), global_params: HashMap::new(), config_file: location, ignore_case: true });
}
let f = File::open(&location)?;
let mut ret = match Self::build_struct(BufReader::new(f), true) {
Ok(x) => x,
Err(e) => return Err(e)
};
ret.config_file = location;
Ok(ret)
}
pub fn from_string(str: String) -> Result<Ini, io::Error> {
let ret = match Self::build_struct(Cursor::new(&str.as_bytes()), false) {
Ok(x) => x,
Err(e) => return Err(e)
};
Ok(ret)
}
pub fn from_string_ignore_case(str: String) -> Result<Ini, io::Error> {
let ret = match Self::build_struct(Cursor::new(&str.as_bytes()), true) {
Ok(x) => x,
Err(e) => return Err(e)
};
Ok(ret)
}
fn build_struct<R: BufRead>(mut input: R, ignore_case: bool) -> Result<Ini, io::Error> {
let mut in_section = false;
let mut cur_sec: String = String::from("");
let mut ret = Ini{ config_map: HashMap::new(), global_params: HashMap::new(), config_file: "".to_string(), ignore_case };
let mut line = String::new();
loop {
line.clear();
match input.read_line(&mut line) {
Ok(x) => {
if x == 0 {
break;
}
}
Err(e) => return Err(e)
}
if line.starts_with(CONFIG_COMMENT_HASH) || line.starts_with(CONFIG_COMMENT_SEMI) {
continue;
}
if line.len() == 0 {
continue;
}
if line.starts_with(CONFIG_SECTION_START) && line.contains(CONFIG_SECTION_END) {
cur_sec = line.replace(CONFIG_SECTION_START, "").replace(CONFIG_SECTION_END, "").trim().to_string();
if ignore_case {
cur_sec = cur_sec.to_lowercase();
}
ret.config_map.insert(cur_sec.clone(), HashMap::new());
in_section = true;
continue;
}
else if line.contains(CONFIG_KVP_SPLIT) {
let key;
let value;
match line.split_once(CONFIG_KVP_SPLIT) {
Some(x) => {
if ignore_case {
key = x.0.trim().to_lowercase();
}
else {
key = x.0.trim().to_string();
}
value = x.1.trim().to_string();
},
None => return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry couldn't be split.")),
};
if !in_section {
ret.global_params.insert(key, value);
}
else {
ret.config_map.get_mut(&cur_sec).unwrap().insert(key, value);
}
continue;
}
else {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, line didn't hit any requirement"));
}
}
Ok(ret)
}
pub fn to_string(&self) -> Result<String, io::Error> {
let new_line = match OS {
"linux" => NEW_LINE_LINUX,
"windows" => NEW_LINE_WINDOWS,
_ => return Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported OS"))
};
let mut ret: String = String::new();
if self.config_map.is_empty() { return Ok(ret) }
for (section_k, section_v) in &self.config_map {
ret.push_str(CONFIG_SECTION_START);
ret.push_str(section_k);
ret.push_str(CONFIG_SECTION_END);
ret.push_str(new_line);
for (k,v) in section_v {
ret.push_str(k);
ret.push_str(CONFIG_KVP_SPLIT);
ret.push_str(v);
ret.push_str(new_line);
}
}
Ok(ret)
}
pub fn save(&self) -> Result<usize, io::Error> {
if self.config_file.is_empty() {
return Err(io::Error::new(io::ErrorKind::Other, "config_file is not set. This is likely because this was created using from_string()"))
}
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(&self.config_file)?;
let str = match self.to_string() {
Ok(x) => x,
Err(e) => return Err(e)
};
file.write_all(str.as_bytes())?;
file.flush()?;
file.sync_all()?;
Ok(file.metadata()?.len() as usize)
}
pub fn get(&self, section: &str, key: &str) -> Option<String> {
let s ;
let k;
if self.ignore_case {
s = section.to_lowercase();
k = key.to_lowercase();
}
else {
s = section.to_string();
k = key.to_string();
}
if s == "" {
if let Some(v) = self.global_params.get(&s) {
return Some(v.clone().trim_start().to_string());
}
}
if let Some(section_map) = self.config_map.get(&s) {
if let Some(value) = section_map.get(&k) {
return Some(value.clone().trim_start().to_string());
}
}
None
}
pub fn set(&mut self, section: &str, key: &str, value: &str) {
if self.ignore_case {
let section_map = self.config_map.entry(section.to_lowercase()).or_insert(HashMap::new());
section_map.insert(key.to_lowercase(), value.to_string());
}
else {
let section_map = self.config_map.entry(section.to_string()).or_insert(HashMap::new());
section_map.insert(key.to_string(), value.to_string());
}
}
pub fn remove(&mut self, section: &str, key: &str) {
if self.ignore_case {
if let Some(section_map) = self.config_map.get_mut(§ion.to_lowercase()) {
section_map.remove(&key.to_lowercase());
}
}
else {
if let Some(section_map) = self.config_map.get_mut(section) {
section_map.remove(key);
}
}
}
pub fn remove_section(&mut self, section: &str) {
if self.ignore_case {
self.config_map.remove(§ion.to_lowercase());
}
else {
self.config_map.remove(section);
}
}
}
impl fmt::Display for Ini {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let ret = self.to_string().unwrap();
write!(f, "{}", ret)
}
}
#[cfg(test)]
mod tests {
use std::fs::{self, File};
use std::io::Read;
use crate::Ini;
const INI: &str = "test.ini";
const INI_CASELESS: &str = "test_caseless.ini";
const NEW_INI: &str = "test1.ini";
#[test]
fn test_create_struct() {
let text = get_text();
let str = Ini::from_string(text).unwrap();
let file = Ini::new(INI.to_string()).unwrap();
assert_eq!(file.config_map, str.config_map);
}
fn get_text() -> String {
let mut file = File::open(INI.to_string()).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
if contents.len() == 0 {
panic!("No config file found");
}
contents
}
fn get_text_caseless() -> String {
let mut file = File::open(INI_CASELESS.to_string()).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
if contents.len() == 0 {
panic!("No config file found");
}
contents
}
#[test]
fn test_to_string() {
let file = Ini::new(INI.to_string()).unwrap();
let text = get_text();
println!("COMPARE BELOW\n");
println!("Raw text\n{}\n", text);
println!("To string\n{}", file.to_string().unwrap());
assert_ne!(file.to_string().unwrap().len(), 0);
}
#[test]
fn test_caseless() {
let text = get_text_caseless();
let file = Ini::new(INI_CASELESS.to_string()).unwrap();
let string = Ini::from_string(text).unwrap();
assert_eq!(file.config_map, string.config_map);
}
#[test]
fn test_save() {
let mut file = Ini::new(INI.to_string()).unwrap();
file.config_file = NEW_INI.to_string();
file.save().unwrap();
let exists = fs::exists(NEW_INI.to_string()).unwrap();
_ = fs::remove_file(NEW_INI.to_string());
assert_eq!(exists, true);
}
#[test]
fn test_remove_section_get() {
let mut ini = Ini::new(INI.to_string()).unwrap();
ini.remove_section("General");
assert_eq!(ini.get("General", "app_name"), None)
}
#[test]
fn test_set_get() {
let mut ini = Ini::new(INI.to_string()).unwrap();
ini.set("General", "app_name", "app");
assert_eq!(ini.get("General", "app_name").unwrap(), "app".to_string());
}
#[test]
fn test_remove_get() {
let mut ini = Ini::new(INI.to_string()).unwrap();
ini.remove("General", "app_name");
assert_eq!(ini.get("General", "app_name"), None);
}
}