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: &str = "#";
23
24const NEW_LINE_WINDOWS: &str = "\r\n";
25const NEW_LINE_LINUX: &str = "\n";
26
27impl Ini {
28 pub fn new(location: String) -> Result<Ini, io::Error> {
31 let mut ret = Ini{ config_map: BTreeMap::new(), config_file: location.clone() };
32
33 if !Path::new(&location).exists() {
34 return Ok(ret);
35 }
36
37 let mut in_section = false;
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 for line in lines {
45 if line.starts_with(CONFIG_COMMENT) {
46 continue;
47 }
48 if line.len() == 0 {
49 continue;
50 }
51
52 if line.starts_with(CONFIG_SECTION_START) && line.contains(CONFIG_SECTION_END) {
54 let edit = line.replace(CONFIG_SECTION_START, "").replace(CONFIG_SECTION_END, "").trim().to_string();
55 ret.config_map.insert(edit.clone(), BTreeMap::new());
56 in_section = true;
57 continue;
58 }
59 else if line.contains(CONFIG_KVP_SPLIT) {
61 if !in_section {
62 return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry found before section."));
63 }
64
65 let kvp = match line.split_once(CONFIG_KVP_SPLIT) {
66 Some(x) => x,
67 None => return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry couldn't be split.")),
68 };
69
70 let mut last = match ret.config_map.last_entry() {
71 Some(x) => x,
72 None => return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry didn't have a section.")),
73 };
74
75 last.get_mut().insert(kvp.0.to_string(), kvp.1.to_string());
76
77 continue;
78 }
79 else {
80 return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, line didn't hit any requirement"));
81 }
82 }
83 Ok(ret)
84 }
85
86 pub fn to_string(&self) -> Result<String, io::Error> {
88 let new_line = match OS {
89 "linux" => NEW_LINE_LINUX,
90 "windows" => NEW_LINE_WINDOWS,
91 _ => return Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported OS"))
92 };
93
94 let mut ret: String = String::new();
95
96 if self.config_map.is_empty() { return Ok(ret) }
97
98 for (section_k, section_v) in &self.config_map {
99 ret.push_str(CONFIG_SECTION_START);
100 ret.push_str(section_k);
101 ret.push_str(CONFIG_SECTION_END);
102 ret.push_str(new_line);
103
104 for (k,v) in section_v {
105 ret.push_str(k);
106 ret.push_str(CONFIG_KVP_SPLIT);
107 ret.push_str(v);
108 ret.push_str(new_line);
109 }
110 }
111
112 Ok(ret)
113 }
114
115 pub fn from_string(str: String) -> Result<Ini, io::Error> {
117 let mut in_section = false;
118 let mut ret = Ini{ config_map: BTreeMap::new(), config_file: "".to_string() };
119
120 let lines = match read_lines_with_blank_from_str(&str) {
121 Ok(x) => x,
122 Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "Failed to read file"))
123 };
124
125 for line in lines {
126 if line.starts_with(CONFIG_COMMENT) {
127 continue;
128 }
129 if line.len() == 0 {
130 continue;
131 }
132
133 if line.starts_with(CONFIG_SECTION_START) && line.contains(CONFIG_SECTION_END) {
135 let edit = line.replace(CONFIG_SECTION_START, "").replace(CONFIG_SECTION_END, "").trim().to_string();
136 ret.config_map.insert(edit.clone(), BTreeMap::new());
137 in_section = true;
138 continue;
139 }
140 else if line.contains(CONFIG_KVP_SPLIT) {
142 if !in_section {
143 return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry found before section."));
144 }
145
146 let kvp = match line.split_once(CONFIG_KVP_SPLIT) {
147 Some(x) => x,
148 None => return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry couldn't be split.")),
149 };
150
151 let mut last = match ret.config_map.last_entry() {
152 Some(x) => x,
153 None => return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry didn't have a section.")),
154 };
155
156 last.get_mut().insert(kvp.0.to_string(), kvp.1.to_string());
157
158 continue;
159 }
160 else {
161 return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, line didn't hit any requirement"));
162 }
163 }
164 Ok(ret)
165 }
166
167 pub fn save(&self) -> Result<usize, io::Error> {
172 if self.config_file.is_empty() {
173 return Err(io::Error::new(io::ErrorKind::Other, "config_file is not set. This is likely because this was created using from_string()"))
174 }
175
176 let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(&self.config_file)?;
177 let str = match self.to_string() {
178 Ok(x) => x,
179 Err(e) => return Err(e)
180 };
181
182 file.write_all(str.as_bytes())?;
183 file.flush()?;
184 file.sync_all()?;
185
186 Ok(file.metadata()?.len() as usize)
187 }
188
189
190 pub fn get(&self, section: &str, key: &str) -> Option<String> {
192 if let Some(section_map) = self.config_map.get(section) {
193 if let Some(value) = section_map.get(key) {
194 return Some(value.clone());
195 }
196 }
197 None
198 }
199
200 pub fn set(&mut self, section: &str, key: &str, value: &str) {
205 let section_map = self.config_map.entry(section.to_string()).or_insert(BTreeMap::new());
206 section_map.insert(key.to_string(), value.to_string());
207 }
208
209 pub fn remove(&mut self, section: &str, key: &str) {
214 if let Some(section_map) = self.config_map.get_mut(section) {
215 section_map.remove(key);
216 }
217 }
218
219 pub fn remove_section(&mut self, section: &str) {
222 self.config_map.remove(section);
223 }
224}
225
226impl fmt::Display for Ini {
228 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
229 let ret = self.to_string().unwrap();
230 write!(f, "{}", ret)
231 }
232}