1use std::fs::{OpenOptions};
2use std::io::{self, Write};
3use std::collections::BTreeMap;
4use std::env::consts::OS;
5use std::fmt;
6use std::path::Path;
7extern crate read_lines_with_blank;
8use read_lines_with_blank::{read_lines_with_blank, read_lines_with_blank_from_str};
9
10pub struct Ini {
15 pub config_map: BTreeMap<String, BTreeMap<String, String>>,
16 pub config_file: String,
17}
18
19const CONFIG_SECTION_START: &str = "[";
20const CONFIG_SECTION_END: &str = "]";
21const CONFIG_KVP_SPLIT: &str = "=";
22const CONFIG_COMMENT_HASH: &str = "#";
23const CONFIG_COMMENT_SEMI: &str = ";";
24
25const NEW_LINE_WINDOWS: &str = "\r\n";
26const NEW_LINE_LINUX: &str = "\n";
27
28impl Ini {
29 pub fn new(location: String) -> Result<Ini, io::Error> {
32 let mut ret = Ini{ config_map: BTreeMap::new(), config_file: location.clone() };
33
34 if !Path::new(&location).exists() {
35 return Ok(ret);
36 }
37
38
39 let lines = match read_lines_with_blank(&location) {
40 Ok(x) => x,
41 Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "Failed to read file"))
42 };
43
44 ret = match Self::build_struct(lines) {
45 Ok(x) => x,
46 Err(e) => return Err(e)
47 };
48
49 ret.config_file = location;
50 Ok(ret)
51 }
52
53 pub fn from_string(str: String) -> Result<Ini, io::Error> {
55 let lines = match read_lines_with_blank_from_str(&str) {
56 Ok(x) => x,
57 Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "Failed to read file"))
58 };
59
60 Self::build_struct(lines)
61 }
62
63 fn build_struct(lines: Vec<String>) -> Result<Ini, io::Error> {
66 let mut in_section = false;
67 let mut cur_sec: String = String::from("");
68 let mut ret = Ini{ config_map: BTreeMap::new(), config_file: "".to_string() };
69
70 for line in lines {
71 if line.starts_with(CONFIG_COMMENT_HASH) || line.starts_with(CONFIG_COMMENT_SEMI) {
72 continue;
73 }
74 if line.len() == 0 {
75 continue;
76 }
77
78 if line.starts_with(CONFIG_SECTION_START) && line.contains(CONFIG_SECTION_END) {
80 cur_sec = line.replace(CONFIG_SECTION_START, "").replace(CONFIG_SECTION_END, "").trim().to_string();
81 ret.config_map.insert(cur_sec.clone(), BTreeMap::new());
82 in_section = true;
83 continue;
84 }
85 else if line.contains(CONFIG_KVP_SPLIT) {
87 if !in_section {
88 return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry found before section."));
89 }
90
91 let kvp = match line.split_once(CONFIG_KVP_SPLIT) {
92 Some(x) => x,
93 None => return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry couldn't be split.")),
94 };
95
96 ret.config_map.get_mut(&cur_sec).unwrap().insert(kvp.0.to_string(), kvp.1.to_string());
97
98 continue;
99 }
100 else {
101 return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, line didn't hit any requirement"));
102 }
103 }
104 Ok(ret)
105 }
106
107 pub fn to_string(&self) -> Result<String, io::Error> {
109 let new_line = match OS {
110 "linux" => NEW_LINE_LINUX,
111 "windows" => NEW_LINE_WINDOWS,
112 _ => return Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported OS"))
113 };
114
115 let mut ret: String = String::new();
116
117 if self.config_map.is_empty() { return Ok(ret) }
118
119 for (section_k, section_v) in &self.config_map {
120 ret.push_str(CONFIG_SECTION_START);
121 ret.push_str(section_k);
122 ret.push_str(CONFIG_SECTION_END);
123 ret.push_str(new_line);
124
125 for (k,v) in section_v {
126 ret.push_str(k);
127 ret.push_str(CONFIG_KVP_SPLIT);
128 ret.push_str(v);
129 ret.push_str(new_line);
130 }
131 }
132
133 Ok(ret)
134 }
135
136 pub fn save(&self) -> Result<usize, io::Error> {
141 if self.config_file.is_empty() {
142 return Err(io::Error::new(io::ErrorKind::Other, "config_file is not set. This is likely because this was created using from_string()"))
143 }
144
145 let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(&self.config_file)?;
146 let str = match self.to_string() {
147 Ok(x) => x,
148 Err(e) => return Err(e)
149 };
150
151 file.write_all(str.as_bytes())?;
152 file.flush()?;
153 file.sync_all()?;
154
155 Ok(file.metadata()?.len() as usize)
156 }
157
158
159 pub fn get(&self, section: &str, key: &str) -> Option<String> {
161 if let Some(section_map) = self.config_map.get(section) {
162 if let Some(value) = section_map.get(key) {
163 return Some(value.clone().trim_start().to_string());
164 }
165 }
166 None
167 }
168
169 pub fn set(&mut self, section: &str, key: &str, value: &str) {
174 let section_map = self.config_map.entry(section.to_string()).or_insert(BTreeMap::new());
175 section_map.insert(key.to_string(), value.to_string());
176 }
177
178 pub fn remove(&mut self, section: &str, key: &str) {
183 if let Some(section_map) = self.config_map.get_mut(section) {
184 section_map.remove(key);
185 }
186 }
187
188 pub fn remove_section(&mut self, section: &str) {
191 self.config_map.remove(section);
192 }
193}
194
195impl fmt::Display for Ini {
197 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
198 let ret = self.to_string().unwrap();
199 write!(f, "{}", ret)
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use std::fs::{self, File};
206 use std::io::Read;
207 use crate::Ini;
208
209 const INI: &str = "test.ini";
210 const NEW_INI: &str = "test1.ini";
211
212 #[test]
213 fn test_create_struct() {
214 let text = get_text();
215 let str = Ini::from_string(text).unwrap();
216 let file = Ini::new(INI.to_string()).unwrap();
217 assert_eq!(file.config_map, str.config_map);
218 }
219
220 fn get_text() -> String {
221 let mut file = File::open(INI.to_string()).unwrap();
222 let mut contents = String::new();
223 file.read_to_string(&mut contents).unwrap();
224 if contents.len() == 0 {
225 panic!("No config file found");
226 }
227 contents
228 }
229
230 #[test]
232 fn test_to_string() {
233 let file = Ini::new(INI.to_string()).unwrap();
234 let text = get_text();
235 println!("COMPARE BELOW\n");
236 println!("Raw text\n{}\n", text);
237 println!("To string\n{}", file.to_string().unwrap());
238 assert_ne!(file.to_string().unwrap().len(), 0);
239 }
240
241 #[test]
242 fn test_save() {
243 let mut file = Ini::new(INI.to_string()).unwrap();
244 file.config_file = NEW_INI.to_string();
245 file.save().unwrap();
246 let exists = fs::exists(NEW_INI.to_string()).unwrap();
247 _ = fs::remove_file(NEW_INI.to_string());
248 assert_eq!(exists, true);
249 }
250
251 #[test]
252 fn test_remove_section_get() {
253 let mut ini = Ini::new(INI.to_string()).unwrap();
254 ini.remove_section("General");
255 assert_eq!(ini.get("General", "app_name"), None)
256 }
257
258 #[test]
259 fn test_set_get() {
260 let mut ini = Ini::new(INI.to_string()).unwrap();
261 ini.set("General", "app_name", "app");
262 assert_eq!(ini.get("General", "app_name").unwrap(), "app".to_string());
263 }
264
265 #[test]
266 fn test_remove_get() {
267 let mut ini = Ini::new(INI.to_string()).unwrap();
268 ini.remove("General", "app_name");
269 assert_eq!(ini.get("General", "app_name"), None);
270 }
271}