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        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            // Section found
54            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            // KVP found
61            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    /// Dump out the INI file to a string, returns blank string if no data is present
83    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    /// Create ini structure from a string. Does not set the config_file so save doesn't work unless set manually.
112    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            // Section found
130            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            // KVP found
137            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    /// Save an INI file after being edited.
164    /// Only functions correctly on Windows and Linux.
165    /// Ok will contain the size in bytes of the file after writing.
166    /// All comments in the INI file will be lost by doing this.
167    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    /// Get a value from the INI file.
187    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    /// Set a value in the INI file.
197    /// If the section doesn't exist, it will be created.
198    /// If the key doesn't exist, it will be created.
199    /// This will not save the file.
200    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    /// Remove a key from the INI file.
206    /// If the section doesn't exist, it will be created.
207    /// If the key doesn't exist, it will be created.
208    /// This will not save the file.
209    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    /// Remove a section from the INI file.
216    /// This will not save the file.
217    pub fn remove_section(&mut self, section: &str) {
218        self.config_map.remove(section);
219    }   
220}
221
222/// Display trait. Returns the string dump of INI data
223impl 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}