ini_rs/
lib.rs

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
10/// Load INI files into a structured BTreeMap, then edit them.
11/// Can also create new INI files.
12/// You can access the data directly via config_map, or use the provided functions.
13/// This only works on Windows and Linux
14pub 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    /// Load in an INI file and return its structure.
29    /// If the file doesn't exist, then returns empty structure.
30    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            // Section found
53            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            // KVP found
60            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    /// Dump out the INI file to a string, returns blank string if no data is present
87    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    /// Create ini structure from a string. Does not set the config_file so save doesn't work unless set manually.
116    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            // Section found
134            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            // KVP found
141            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    /// Save an INI file after being edited.
168    /// Only functions correctly on Windows and Linux.
169    /// Ok will contain the size in bytes of the file after writing.
170    /// All comments in the INI file will be lost by doing this.
171    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    /// Get a value from the INI file.
191    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    /// Set a value in the INI file.
201    /// If the section doesn't exist, it will be created.
202    /// If the key doesn't exist, it will be created.
203    /// This will not save the file.
204    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    /// Remove a key from the INI file.
210    /// If the section doesn't exist, it will be created.
211    /// If the key doesn't exist, it will be created.
212    /// This will not save the file.
213    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    /// Remove a section from the INI file.
220    /// This will not save the file.
221    pub fn remove_section(&mut self, section: &str) {
222        self.config_map.remove(section);
223    }   
224}
225
226/// Display trait. Returns the string dump of INI data
227impl 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}