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