• 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 
24 /// A simple class to process init file. Based on
25 /// external/qemu/android/android-emu-base/android/base/files/IniFile.h
26 pub struct IniFile {
27     /// The data stored in the ini file.
28     data: HashMap<String, String>,
29     /// The path to the ini file.
30     filepath: String,
31 }
32 
33 impl IniFile {
34     /// Creates a new IniFile with the given filepath.
35     ///
36     /// # Arguments
37     ///
38     /// * `filepath` - The path to the ini file.
new(filepath: String) -> IniFile39     pub fn new(filepath: String) -> IniFile {
40         IniFile { data: HashMap::new(), filepath }
41     }
42 
43     /// Reads data into IniFile from the backing file, overwriting any
44     /// existing data.
45     ///
46     /// # Returns
47     ///
48     /// `Ok` if the write was successful, `Error` otherwise.
read(&mut self) -> Result<(), Box<dyn Error>>49     pub fn read(&mut self) -> Result<(), Box<dyn Error>> {
50         self.data.clear();
51 
52         if self.filepath.is_empty() {
53             return Err("Read called without a backing file!".into());
54         }
55 
56         let mut f = File::open(self.filepath.clone())?;
57         let reader = BufReader::new(&mut f);
58 
59         for line in reader.lines() {
60             let line = line?;
61             let parts = line.split_once('=');
62             if parts.is_none() {
63                 continue;
64             }
65             let key = parts.unwrap().0.trim();
66             let value = parts.unwrap().1.trim();
67             self.data.insert(key.to_owned(), value.to_owned());
68         }
69 
70         Ok(())
71     }
72 
73     /// Writes the current IniFile to the backing file.
74     ///
75     /// # Returns
76     ///
77     /// `Ok` if the write was successful, `Error` otherwise.
write(&self) -> Result<(), Box<dyn Error>>78     pub fn write(&self) -> Result<(), Box<dyn Error>> {
79         if self.filepath.is_empty() {
80             return Err("Write called without a backing file!".into());
81         }
82 
83         let mut f = File::create(self.filepath.clone())?;
84         for (key, value) in &self.data {
85             writeln!(&mut f, "{}={}", key, value)?;
86         }
87 
88         Ok(())
89     }
90 
91     /// Checks if a certain key exists in the file.
92     ///
93     /// # Arguments
94     ///
95     /// * `key` - The key to check.
96     ///
97     /// # Returns
98     ///
99     /// `true` if the key exists, `false` otherwise.
contains_key(&self, key: &str) -> bool100     pub fn contains_key(&self, key: &str) -> bool {
101         self.data.contains_key(key)
102     }
103 
104     /// Gets value.
105     ///
106     /// # Arguments
107     ///
108     /// * `key` - The key to get the value for.
109     ///
110     /// # Returns
111     ///
112     /// An `Option` containing the value if it exists, `None` otherwise.
get(&self, key: &str) -> Option<&str>113     pub fn get(&self, key: &str) -> Option<&str> {
114         self.data.get(key).map(|v| v.as_str())
115     }
116 
117     /// Inserts a key-value pair.
118     ///
119     /// # Arguments
120     ///
121     /// * `key` - The key to set the value for.
122     /// * `value` - The value to set.
insert(&mut self, key: &str, value: &str)123     pub fn insert(&mut self, key: &str, value: &str) {
124         self.data.insert(key.to_owned(), value.to_owned());
125     }
126 }
127 
128 #[cfg(test)]
129 mod tests {
130     use rand::{distributions::Alphanumeric, Rng};
131     use std::env;
132     use std::fs::File;
133     use std::io::{Read, Write};
134 
135     use super::IniFile;
136 
get_temp_ini_filepath(prefix: &str) -> String137     fn get_temp_ini_filepath(prefix: &str) -> String {
138         env::temp_dir()
139             .join(format!(
140                 "{prefix}_{}.ini",
141                 rand::thread_rng()
142                     .sample_iter(&Alphanumeric)
143                     .take(8)
144                     .map(char::from)
145                     .collect::<String>()
146             ))
147             .into_os_string()
148             .into_string()
149             .unwrap()
150     }
151 
152     // NOTE: ctest run a test at least twice tests in parallel, so we need to use unique temp file
153     // to prevent tests from accessing the same file simultaneously.
154     #[test]
test_read()155     fn test_read() {
156         for test_case in ["port=123", "port= 123", "port =123", " port = 123 "] {
157             let filepath = get_temp_ini_filepath("test_read");
158 
159             {
160                 let mut tmpfile = File::create(&filepath).unwrap();
161                 writeln!(tmpfile, "{test_case}").unwrap();
162             }
163 
164             let mut inifile = IniFile::new(filepath.clone());
165             inifile.read().unwrap();
166 
167             assert!(!inifile.contains_key("unknown-key"));
168             assert!(inifile.contains_key("port"), "Fail in test case: {test_case}");
169             assert_eq!(inifile.get("port").unwrap(), "123");
170             assert_eq!(inifile.get("unknown-key"), None);
171 
172             // Note that there is no guarantee that the file is immediately deleted (e.g.,
173             // depending on platform, other open file descriptors may prevent immediate removal).
174             // https://doc.rust-lang.org/std/fs/fn.remove_file.html.
175             std::fs::remove_file(filepath).unwrap();
176         }
177     }
178 
179     #[test]
test_read_no_newline()180     fn test_read_no_newline() {
181         let filepath = get_temp_ini_filepath("test_read_no_newline");
182 
183         {
184             let mut tmpfile = File::create(&filepath).unwrap();
185             write!(tmpfile, "port=123").unwrap();
186         }
187 
188         let mut inifile = IniFile::new(filepath.clone());
189         inifile.read().unwrap();
190 
191         assert!(!inifile.contains_key("unknown-key"));
192         assert!(inifile.contains_key("port"));
193         assert_eq!(inifile.get("port").unwrap(), "123");
194         assert_eq!(inifile.get("unknown-key"), None);
195 
196         std::fs::remove_file(filepath).unwrap();
197     }
198 
199     #[test]
test_read_multiple_lines()200     fn test_read_multiple_lines() {
201         let filepath = get_temp_ini_filepath("test_read_multiple_lines");
202 
203         {
204             let mut tmpfile = File::create(&filepath).unwrap();
205             write!(tmpfile, "port=123\nport2=456\n").unwrap();
206         }
207 
208         let mut inifile = IniFile::new(filepath.clone());
209         inifile.read().unwrap();
210 
211         assert!(!inifile.contains_key("unknown-key"));
212         assert!(inifile.contains_key("port"));
213         assert!(inifile.contains_key("port2"));
214         assert_eq!(inifile.get("port").unwrap(), "123");
215         assert_eq!(inifile.get("port2").unwrap(), "456");
216         assert_eq!(inifile.get("unknown-key"), None);
217 
218         std::fs::remove_file(filepath).unwrap();
219     }
220 
221     #[test]
test_insert_and_contains_key()222     fn test_insert_and_contains_key() {
223         let filepath = get_temp_ini_filepath("test_insert_and_contains_key");
224 
225         let mut inifile = IniFile::new(filepath);
226 
227         assert!(!inifile.contains_key("port"));
228         assert!(!inifile.contains_key("unknown-key"));
229 
230         inifile.insert("port", "123");
231 
232         assert!(inifile.contains_key("port"));
233         assert!(!inifile.contains_key("unknown-key"));
234         assert_eq!(inifile.get("port").unwrap(), "123");
235         assert_eq!(inifile.get("unknown-key"), None);
236 
237         // Update the value of an existing key.
238         inifile.insert("port", "234");
239 
240         assert!(inifile.contains_key("port"));
241         assert!(!inifile.contains_key("unknown-key"));
242         assert_eq!(inifile.get("port").unwrap(), "234");
243         assert_eq!(inifile.get("unknown-key"), None);
244     }
245 
246     #[test]
test_write()247     fn test_write() {
248         let filepath = get_temp_ini_filepath("test_write");
249 
250         let mut inifile = IniFile::new(filepath.clone());
251 
252         assert!(!inifile.contains_key("port"));
253         assert!(!inifile.contains_key("unknown-key"));
254 
255         inifile.insert("port", "123");
256 
257         assert!(inifile.contains_key("port"));
258         assert!(!inifile.contains_key("unknown-key"));
259         assert_eq!(inifile.get("port").unwrap(), "123");
260         assert_eq!(inifile.get("unknown-key"), None);
261 
262         inifile.write().unwrap();
263         let mut file = File::open(&filepath).unwrap();
264         let mut contents = String::new();
265         file.read_to_string(&mut contents).unwrap();
266 
267         assert_eq!(contents, "port=123\n");
268 
269         std::fs::remove_file(filepath).unwrap();
270     }
271 
272     #[test]
test_write_and_read()273     fn test_write_and_read() {
274         let filepath = get_temp_ini_filepath("test_write_and_read");
275 
276         {
277             let mut inifile = IniFile::new(filepath.clone());
278 
279             assert!(!inifile.contains_key("port"));
280             assert!(!inifile.contains_key("port2"));
281             assert!(!inifile.contains_key("unknown-key"));
282 
283             inifile.insert("port", "123");
284             inifile.insert("port2", "456");
285 
286             assert!(inifile.contains_key("port"));
287             assert!(!inifile.contains_key("unknown-key"));
288             assert_eq!(inifile.get("port").unwrap(), "123");
289             assert_eq!(inifile.get("unknown-key"), None);
290 
291             inifile.write().unwrap();
292         }
293 
294         let mut inifile = IniFile::new(filepath.clone());
295         inifile.read().unwrap();
296 
297         assert!(!inifile.contains_key("unknown-key"));
298         assert!(inifile.contains_key("port"));
299         assert!(inifile.contains_key("port2"));
300         assert_eq!(inifile.get("port").unwrap(), "123");
301         assert_eq!(inifile.get("port2").unwrap(), "456");
302         assert_eq!(inifile.get("unknown-key"), None);
303 
304         std::fs::remove_file(filepath).unwrap();
305     }
306 }
307