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 let mut cur_sec: String = String::from("");
39
40 let lines = match read_lines_with_blank(&location) {
41 Ok(x) => x,
42 Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "Failed to read file"))
43 };
44
45 for line in lines {
46 if line.starts_with(CONFIG_COMMENT) {
47 continue;
48 }
49 if line.len() == 0 {
50 continue;
51 }
52
53 if line.starts_with(CONFIG_SECTION_START) && line.contains(CONFIG_SECTION_END) {
55 cur_sec = line.replace(CONFIG_SECTION_START, "").replace(CONFIG_SECTION_END, "").trim().to_string();
56 ret.config_map.insert(cur_sec.clone(), BTreeMap::new());
57 in_section = true;
58 continue;
59 }
60 else if line.contains(CONFIG_KVP_SPLIT) {
62 if !in_section {
63 return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry found before section."));
64 }
65
66 let kvp = match line.split_once(CONFIG_KVP_SPLIT) {
67 Some(x) => x,
68 None => return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry couldn't be split.")),
69 };
70
71 ret.config_map.get_mut(&cur_sec).unwrap().insert(kvp.0.to_string(), kvp.1.to_string());
72
73 continue;
74 }
75 else {
76 return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, line didn't hit any requirement"));
77 }
78 }
79 Ok(ret)
80 }
81
82 pub fn to_string(&self) -> Result<String, io::Error> {
84 let new_line = match OS {
85 "linux" => NEW_LINE_LINUX,
86 "windows" => NEW_LINE_WINDOWS,
87 _ => return Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported OS"))
88 };
89
90 let mut ret: String = String::new();
91
92 if self.config_map.is_empty() { return Ok(ret) }
93
94 for (section_k, section_v) in &self.config_map {
95 ret.push_str(CONFIG_SECTION_START);
96 ret.push_str(section_k);
97 ret.push_str(CONFIG_SECTION_END);
98 ret.push_str(new_line);
99
100 for (k,v) in section_v {
101 ret.push_str(k);
102 ret.push_str(CONFIG_KVP_SPLIT);
103 ret.push_str(v);
104 ret.push_str(new_line);
105 }
106 }
107
108 Ok(ret)
109 }
110
111 pub fn from_string(str: String) -> Result<Ini, io::Error> {
113 let mut in_section = false;
114 let mut ret = Ini{ config_map: BTreeMap::new(), config_file: "".to_string() };
115
116 let lines = match read_lines_with_blank_from_str(&str) {
117 Ok(x) => x,
118 Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "Failed to read file"))
119 };
120
121 for line in lines {
122 if line.starts_with(CONFIG_COMMENT) {
123 continue;
124 }
125 if line.len() == 0 {
126 continue;
127 }
128
129 if line.starts_with(CONFIG_SECTION_START) && line.contains(CONFIG_SECTION_END) {
131 let edit = line.replace(CONFIG_SECTION_START, "").replace(CONFIG_SECTION_END, "").trim().to_string();
132 ret.config_map.insert(edit.clone(), BTreeMap::new());
133 in_section = true;
134 continue;
135 }
136 else if line.contains(CONFIG_KVP_SPLIT) {
138 if !in_section {
139 return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry found before section."));
140 }
141
142 let kvp = match line.split_once(CONFIG_KVP_SPLIT) {
143 Some(x) => x,
144 None => return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry couldn't be split.")),
145 };
146
147 let mut last = match ret.config_map.last_entry() {
148 Some(x) => x,
149 None => return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry didn't have a section.")),
150 };
151
152 last.get_mut().insert(kvp.0.to_string(), kvp.1.to_string());
153
154 continue;
155 }
156 else {
157 return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, line didn't hit any requirement"));
158 }
159 }
160 Ok(ret)
161 }
162
163 pub fn save(&self) -> Result<usize, io::Error> {
168 if self.config_file.is_empty() {
169 return Err(io::Error::new(io::ErrorKind::Other, "config_file is not set. This is likely because this was created using from_string()"))
170 }
171
172 let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(&self.config_file)?;
173 let str = match self.to_string() {
174 Ok(x) => x,
175 Err(e) => return Err(e)
176 };
177
178 file.write_all(str.as_bytes())?;
179 file.flush()?;
180 file.sync_all()?;
181
182 Ok(file.metadata()?.len() as usize)
183 }
184
185
186 pub fn get(&self, section: &str, key: &str) -> Option<String> {
188 if let Some(section_map) = self.config_map.get(section) {
189 if let Some(value) = section_map.get(key) {
190 return Some(value.clone());
191 }
192 }
193 None
194 }
195
196 pub fn set(&mut self, section: &str, key: &str, value: &str) {
201 let section_map = self.config_map.entry(section.to_string()).or_insert(BTreeMap::new());
202 section_map.insert(key.to_string(), value.to_string());
203 }
204
205 pub fn remove(&mut self, section: &str, key: &str) {
210 if let Some(section_map) = self.config_map.get_mut(section) {
211 section_map.remove(key);
212 }
213 }
214
215 pub fn remove_section(&mut self, section: &str) {
218 self.config_map.remove(section);
219 }
220}
221
222impl fmt::Display for Ini {
224 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
225 let ret = self.to_string().unwrap();
226 write!(f, "{}", ret)
227 }
228}