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