• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //
2 //  Copyright 2023 Google, Inc.
3 //
4 //  Licensed under the Apache License, Version 2.0 (the "License");
5 //  you may not use this file except in compliance with the License.
6 //  You may obtain a copy of the License at:
7 //
8 //  http://www.apache.org/licenses/LICENSE-2.0
9 //
10 //  Unless required by applicable law or agreed to in writing, software
11 //  distributed under the License is distributed on an "AS IS" BASIS,
12 //  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 //  See the License for the specific language governing permissions and
14 //  limitations under the License.
15 
16 //! # IniFile class
17 
18 use std::collections::HashMap;
19 use std::error::Error;
20 use std::fs::File;
21 use std::io::prelude::*;
22 use std::io::BufReader;
23 use std::path::PathBuf;
24 
25 /// A simple class to process init file. Based on
26 /// external/qemu/android/android-emu-base/android/base/files/IniFile.h
27 pub struct IniFile {
28     /// The data stored in the ini file.
29     data: HashMap<String, String>,
30     /// The path to the ini file.
31     filepath: PathBuf,
32 }
33 
34 impl IniFile {
35     /// Creates a new IniFile with the given filepath.
36     ///
37     /// # Arguments
38     ///
39     /// * `filepath` - The path to the ini file.
new(filepath: PathBuf) -> IniFile40     pub fn new(filepath: PathBuf) -> IniFile {
41         IniFile { data: HashMap::new(), filepath }
42     }
43 
44     /// Reads data into IniFile from the backing file, overwriting any
45     /// existing data.
46     ///
47     /// # Returns
48     ///
49     /// `Ok` if the write was successful, `Error` otherwise.
read(&mut self) -> Result<(), Box<dyn Error>>50     pub fn read(&mut self) -> Result<(), Box<dyn Error>> {
51         self.data.clear();
52 
53         let mut f = File::open(self.filepath.clone())?;
54         let reader = BufReader::new(&mut f);
55 
56         for line in reader.lines() {
57             let line = line?;
58             let parts = line.split_once('=');
59             if parts.is_none() {
60                 continue;
61             }
62             let key = parts.unwrap().0.trim();
63             let value = parts.unwrap().1.trim();
64             self.data.insert(key.to_owned(), value.to_owned());
65         }
66 
67         Ok(())
68     }
69 
70     /// Writes the current IniFile to the backing file.
71     ///
72     /// # Returns
73     ///
74     /// `Ok` if the write was successful, `Error` otherwise.
write(&self) -> Result<(), Box<dyn Error>>75     pub fn write(&self) -> Result<(), Box<dyn Error>> {
76         let mut f = File::create(self.filepath.clone())?;
77         for (key, value) in &self.data {
78             writeln!(&mut f, "{}={}", key, value)?;
79         }
80         f.flush()?;
81         Ok(())
82     }
83 
84     /// Checks if a certain key exists in the file.
85     ///
86     /// # Arguments
87     ///
88     /// * `key` - The key to check.
89     ///
90     /// # Returns
91     ///
92     /// `true` if the key exists, `false` otherwise.
contains_key(&self, key: &str) -> bool93     pub fn contains_key(&self, key: &str) -> bool {
94         self.data.contains_key(key)
95     }
96 
97     /// Gets value.
98     ///
99     /// # Arguments
100     ///
101     /// * `key` - The key to get the value for.
102     ///
103     /// # Returns
104     ///
105     /// An `Option` containing the value if it exists, `None` otherwise.
get(&self, key: &str) -> Option<&str>106     pub fn get(&self, key: &str) -> Option<&str> {
107         self.data.get(key).map(|v| v.as_str())
108     }
109 
110     /// Inserts a key-value pair.
111     ///
112     /// # Arguments
113     ///
114     /// * `key` - The key to set the value for.
115     /// * `value` - The value to set.
insert(&mut self, key: &str, value: &str)116     pub fn insert(&mut self, key: &str, value: &str) {
117         self.data.insert(key.to_owned(), value.to_owned());
118     }
119 }
120 
121 #[cfg(test)]
122 mod tests {
123     use rand::{distributions::Alphanumeric, Rng};
124     use std::env;
125     use std::fs::File;
126     use std::io::{Read, Write};
127     use std::path::PathBuf;
128 
129     use super::IniFile;
130 
get_temp_ini_filepath(prefix: &str) -> PathBuf131     fn get_temp_ini_filepath(prefix: &str) -> PathBuf {
132         env::temp_dir().join(format!(
133             "{prefix}_{}.ini",
134             rand::thread_rng()
135                 .sample_iter(&Alphanumeric)
136                 .take(8)
137                 .map(char::from)
138                 .collect::<String>()
139         ))
140     }
141 
142     // NOTE: ctest run a test at least twice tests in parallel, so we need to use unique temp file
143     // to prevent tests from accessing the same file simultaneously.
144     #[test]
test_read()145     fn test_read() {
146         for test_case in ["port=123", "port= 123", "port =123", " port = 123 "] {
147             let filepath = get_temp_ini_filepath("test_read");
148 
149             {
150                 let mut tmpfile = match File::create(&filepath) {
151                     Ok(f) => f,
152                     Err(_) => return,
153                 };
154                 writeln!(tmpfile, "{test_case}").unwrap();
155             }
156 
157             let mut inifile = IniFile::new(filepath.clone());
158             inifile.read().unwrap();
159 
160             assert!(!inifile.contains_key("unknown-key"));
161             assert!(inifile.contains_key("port"), "Fail in test case: {test_case}");
162             assert_eq!(inifile.get("port").unwrap(), "123");
163             assert_eq!(inifile.get("unknown-key"), None);
164 
165             // Note that there is no guarantee that the file is immediately deleted (e.g.,
166             // depending on platform, other open file descriptors may prevent immediate removal).
167             // https://doc.rust-lang.org/std/fs/fn.remove_file.html.
168             std::fs::remove_file(filepath).unwrap();
169         }
170     }
171 
172     #[test]
test_read_no_newline()173     fn test_read_no_newline() {
174         let filepath = get_temp_ini_filepath("test_read_no_newline");
175 
176         {
177             let mut tmpfile = match File::create(&filepath) {
178                 Ok(f) => f,
179                 Err(_) => return,
180             };
181             write!(tmpfile, "port=123").unwrap();
182         }
183 
184         let mut inifile = IniFile::new(filepath.clone());
185         inifile.read().unwrap();
186 
187         assert!(!inifile.contains_key("unknown-key"));
188         assert!(inifile.contains_key("port"));
189         assert_eq!(inifile.get("port").unwrap(), "123");
190         assert_eq!(inifile.get("unknown-key"), None);
191 
192         std::fs::remove_file(filepath).unwrap();
193     }
194 
195     #[test]
test_read_no_file()196     fn test_read_no_file() {
197         let filepath = get_temp_ini_filepath("test_read_no_file");
198         let mut inifile = IniFile::new(filepath.clone());
199         assert!(inifile.read().is_err());
200     }
201 
202     #[test]
test_read_multiple_lines()203     fn test_read_multiple_lines() {
204         let filepath = get_temp_ini_filepath("test_read_multiple_lines");
205 
206         {
207             let mut tmpfile = match File::create(&filepath) {
208                 Ok(f) => f,
209                 Err(_) => return,
210             };
211             write!(tmpfile, "port=123\nport2=456\n").unwrap();
212         }
213 
214         let mut inifile = IniFile::new(filepath.clone());
215         inifile.read().unwrap();
216 
217         assert!(!inifile.contains_key("unknown-key"));
218         assert!(inifile.contains_key("port"));
219         assert!(inifile.contains_key("port2"));
220         assert_eq!(inifile.get("port").unwrap(), "123");
221         assert_eq!(inifile.get("port2").unwrap(), "456");
222         assert_eq!(inifile.get("unknown-key"), None);
223 
224         std::fs::remove_file(filepath).unwrap();
225     }
226 
227     #[test]
test_insert_and_contains_key()228     fn test_insert_and_contains_key() {
229         let filepath = get_temp_ini_filepath("test_insert_and_contains_key");
230 
231         let mut inifile = IniFile::new(filepath);
232 
233         assert!(!inifile.contains_key("port"));
234         assert!(!inifile.contains_key("unknown-key"));
235 
236         inifile.insert("port", "123");
237 
238         assert!(inifile.contains_key("port"));
239         assert!(!inifile.contains_key("unknown-key"));
240         assert_eq!(inifile.get("port").unwrap(), "123");
241         assert_eq!(inifile.get("unknown-key"), None);
242 
243         // Update the value of an existing key.
244         inifile.insert("port", "234");
245 
246         assert!(inifile.contains_key("port"));
247         assert!(!inifile.contains_key("unknown-key"));
248         assert_eq!(inifile.get("port").unwrap(), "234");
249         assert_eq!(inifile.get("unknown-key"), None);
250     }
251 
252     #[test]
test_write()253     fn test_write() {
254         let filepath = get_temp_ini_filepath("test_write");
255 
256         let mut inifile = IniFile::new(filepath.clone());
257 
258         assert!(!inifile.contains_key("port"));
259         assert!(!inifile.contains_key("unknown-key"));
260 
261         inifile.insert("port", "123");
262 
263         assert!(inifile.contains_key("port"));
264         assert!(!inifile.contains_key("unknown-key"));
265         assert_eq!(inifile.get("port").unwrap(), "123");
266         assert_eq!(inifile.get("unknown-key"), None);
267 
268         if inifile.write().is_err() {
269             return;
270         }
271         let mut file = File::open(&filepath).unwrap();
272         let mut contents = String::new();
273         file.read_to_string(&mut contents).unwrap();
274 
275         assert_eq!(contents, "port=123\n");
276 
277         std::fs::remove_file(filepath).unwrap();
278     }
279 
280     #[test]
test_write_and_read()281     fn test_write_and_read() {
282         let filepath = get_temp_ini_filepath("test_write_and_read");
283 
284         {
285             let mut inifile = IniFile::new(filepath.clone());
286 
287             assert!(!inifile.contains_key("port"));
288             assert!(!inifile.contains_key("port2"));
289             assert!(!inifile.contains_key("unknown-key"));
290 
291             inifile.insert("port", "123");
292             inifile.insert("port2", "456");
293 
294             assert!(inifile.contains_key("port"));
295             assert!(!inifile.contains_key("unknown-key"));
296             assert_eq!(inifile.get("port").unwrap(), "123");
297             assert_eq!(inifile.get("unknown-key"), None);
298 
299             if inifile.write().is_err() {
300                 return;
301             }
302         }
303 
304         let mut inifile = IniFile::new(filepath.clone());
305         inifile.read().unwrap();
306 
307         assert!(!inifile.contains_key("unknown-key"));
308         assert!(inifile.contains_key("port"));
309         assert!(inifile.contains_key("port2"));
310         assert_eq!(inifile.get("port").unwrap(), "123");
311         assert_eq!(inifile.get("port2").unwrap(), "456");
312         assert_eq!(inifile.get("unknown-key"), None);
313 
314         std::fs::remove_file(filepath).unwrap();
315     }
316 
317     #[test]
test_overwrite()318     fn test_overwrite() {
319         let filepath = get_temp_ini_filepath("test_overwrite");
320         {
321             let mut tmpfile = match File::create(&filepath) {
322                 Ok(f) => f,
323                 Err(_) => return,
324             };
325             write!(tmpfile, "port=123\nport2=456\n").unwrap();
326         }
327 
328         let mut inifile = IniFile::new(filepath.clone());
329         inifile.insert("port3", "789");
330 
331         inifile.write().unwrap();
332         let mut file = File::open(&filepath).unwrap();
333         let mut contents = String::new();
334         file.read_to_string(&mut contents).unwrap();
335 
336         assert_eq!(contents, "port3=789\n");
337     }
338 }
339