ini_rs/
lib.rs

1use std::fs::{File, OpenOptions};
2use std::io::{self, BufReader, Read, Write};
3use std::collections::BTreeMap;
4use std::env::consts::OS;
5use std::path::Path;
6
7/// Load INI files into a structured BTreeMap, then edit them.
8/// Can also create new INI files.
9/// You can access the data directly via config_map, or use the provided functions.
10/// This only works on Windows and Linux
11pub struct Ini {
12    pub config_map: BTreeMap<String, BTreeMap<String, String>>,
13    config_file: String,
14}
15
16const CONFIG_SECTION_START: &str = "[";
17const CONFIG_SECTION_END: &str = "]";
18const CONFIG_KVP_SPLIT: &str = "=";
19const CONFIG_COMMENT: &str = "#";
20
21const NEW_LINE_WINDOWS: &str = "\r\n";
22const NEW_LINE_LINUX: &str = "\n";
23
24fn read_lines_no_stop_on_blank(buf_read: &mut BufReader<File>) -> io::Result<Vec<String>> {
25    let mut ret: Vec<String> = Vec::new();
26    let new_line = match OS {
27        "linux" => NEW_LINE_LINUX,
28        "windows" => NEW_LINE_WINDOWS,
29        _ => return Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported OS"))
30    };
31
32    let mut data: Vec<u8> = Vec::new();
33    buf_read.read_to_end(&mut data)?;
34    let str: String = String::from_utf8_lossy(&data).to_string();
35
36    for v in str.split(new_line) {
37        ret.push(v.to_string());
38    }
39    Ok(ret)
40}
41
42impl Ini {
43    /// Load in an INI file and return its structure.
44    /// If the file doesn't exist, then returns empty structure.
45    pub fn new(location: String) -> Result<Ini, io::Error> {
46        let mut ret = Ini{ config_map: BTreeMap::new(), config_file: location.clone() };
47
48        if !Path::new(&location).exists() {
49            return Ok(ret);
50        }
51        let f = File::open(&location)?;
52        let mut reader = BufReader::new(f);
53
54        let mut in_section = false;
55
56        let lines = match read_lines_no_stop_on_blank(&mut reader) {
57            Ok(x) => x,
58            Err(e) => return Err(e),
59        };
60
61        println!("Number of lines: {}", lines.len());
62
63        for line in lines {
64            if line.starts_with(CONFIG_COMMENT) {
65                println!("Comment line");
66                continue;
67            }
68            if line.len() == 0 {
69                println!("Blank line");
70                continue;
71            }
72
73            println!("{}", line);
74
75            // Section found
76            if line.starts_with(CONFIG_SECTION_START) && line.contains(CONFIG_SECTION_END) {
77                let edit = line.replace(CONFIG_SECTION_START, "").replace(CONFIG_SECTION_END, "").trim().to_string();
78                ret.config_map.insert(edit.clone(), BTreeMap::new());
79                in_section = true;
80                println!("Found a section");
81                continue;
82            }
83            // KVP found
84            else if line.contains(CONFIG_KVP_SPLIT) {
85                if !in_section {
86                    return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry found before section."));
87                }
88                println!("Found a kvp split");
89                let kvp = match line.split_once(CONFIG_KVP_SPLIT) {
90                    Some(x) => x,
91                    None => return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry couldn't be split.")),
92                };
93
94                let mut last = match ret.config_map.last_entry() {
95                    Some(x) => x,
96                    None => return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, KVP entry didn't have a section.")),
97                };
98
99                last.get_mut().insert(kvp.0.to_string(), kvp.1.to_string());
100
101                continue;
102            }
103            else {
104                return Err(io::Error::new(io::ErrorKind::InvalidData, "Config file was invalid, line didn't hit any requirement"));
105            }
106        }
107        println!("Number of sections {}", ret.config_map.len());
108        Ok(ret)
109    }
110
111    /// Save an INI file after being edited.
112    /// Only functions correctly on Windows and Linux.
113    /// Ok will contain the size in bytes of the file after writing.
114    /// All comments in the INI file will be lost by doing this.
115    pub fn save(&self) -> Result<usize, io::Error> {
116        let new_line = match OS {
117            "linux" => NEW_LINE_LINUX,
118            "windows" => NEW_LINE_WINDOWS,
119            _ => return Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported OS"))
120        };
121
122        let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(&self.config_file)?;
123
124        for (section_k, section_v) in &self.config_map {
125            file.write_all(CONFIG_SECTION_START.as_bytes())?;
126            file.write_all(section_k.as_bytes())?;
127            file.write_all(CONFIG_SECTION_END.as_bytes())?;
128            file.write_all(new_line.as_bytes())?;
129
130            for (k,v) in section_v {
131                file.write_all(k.as_bytes())?;
132                file.write_all(CONFIG_KVP_SPLIT.as_bytes())?;
133                file.write_all(v.as_bytes())?;
134                file.write_all(new_line.as_bytes())?;
135            }
136
137            file.flush()?;
138        }
139
140        file.flush()?;
141        file.sync_all()?;
142        
143        Ok(file.metadata()?.len() as usize)
144    }
145    
146
147    /// Get a value from the INI file.
148    pub fn get(&self, section: &str, key: &str) -> Option<String> {
149        if let Some(section_map) = self.config_map.get(section) {
150            if let Some(value) = section_map.get(key) {
151                return Some(value.clone());
152            }
153        }
154        None
155    }
156
157    /// Set a value in the INI file.
158    /// If the section doesn't exist, it will be created.
159    /// If the key doesn't exist, it will be created.
160    /// This will not save the file.
161    pub fn set(&mut self, section: &str, key: &str, value: &str) {
162        let section_map = self.config_map.entry(section.to_string()).or_insert(BTreeMap::new());
163        section_map.insert(key.to_string(), value.to_string());
164    }
165
166    /// Remove a key from the INI file.
167    /// If the section doesn't exist, it will be created.
168    /// If the key doesn't exist, it will be created.
169    /// This will not save the file.
170    pub fn remove(&mut self, section: &str, key: &str) {
171        if let Some(section_map) = self.config_map.get_mut(section) {
172            section_map.remove(key);
173        }
174    }
175
176    /// Remove a section from the INI file.
177    /// This will not save the file.
178    pub fn remove_section(&mut self, section: &str) {
179        self.config_map.remove(section);
180    }   
181}