blob: 1cd329ac9c3d4b3f906a52f6a756c89c186a2fed [file] [log] [blame]
//
// Copyright 2023 Google, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # IniFile class
use std::collections::HashMap;
use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
use std::path::PathBuf;
use log::error;
use super::os_utils::get_discovery_directory;
/// A simple class to process init file. Based on
/// external/qemu/android/android-emu-base/android/base/files/IniFile.h
struct IniFile {
/// The data stored in the ini file.
data: HashMap<String, String>,
/// The path to the ini file.
filepath: PathBuf,
}
impl IniFile {
/// Creates a new IniFile with the given filepath.
///
/// # Arguments
///
/// * `filepath` - The path to the ini file.
fn new(filepath: PathBuf) -> IniFile {
IniFile { data: HashMap::new(), filepath }
}
/// Reads data into IniFile from the backing file, overwriting any
/// existing data.
///
/// # Returns
///
/// `Ok` if the write was successful, `Error` otherwise.
fn read(&mut self) -> Result<(), Box<dyn Error>> {
self.data.clear();
let mut f = File::open(self.filepath.clone())?;
let reader = BufReader::new(&mut f);
for line in reader.lines() {
let line = line?;
let parts = line.split_once('=');
if parts.is_none() {
continue;
}
let key = parts.unwrap().0.trim();
let value = parts.unwrap().1.trim();
self.data.insert(key.to_owned(), value.to_owned());
}
Ok(())
}
/// Writes the current IniFile to the backing file.
///
/// # Returns
///
/// `Ok` if the write was successful, `Error` otherwise.
fn write(&self) -> std::io::Result<()> {
let mut f = create_new(self.filepath.clone())?;
for (key, value) in &self.data {
writeln!(&mut f, "{}={}", key, value)?;
}
f.flush()?;
Ok(())
}
/// Gets value.
///
/// # Arguments
///
/// * `key` - The key to get the value for.
///
/// # Returns
///
/// An `Option` containing the value if it exists, `None` otherwise.
fn get(&self, key: &str) -> Option<&str> {
self.data.get(key).map(|v| v.as_str())
}
/// Inserts a key-value pair.
///
/// # Arguments
///
/// * `key` - The key to set the value for.
/// * `value` - The value to set.
fn insert(&mut self, key: &str, value: &str) {
self.data.insert(key.to_owned(), value.to_owned());
}
}
// TODO: Replace with std::fs::File::create_new once Rust toolchain is upgraded to 1.77
/// Create new file, errors if it already exists.
fn create_new<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<File> {
std::fs::OpenOptions::new().read(true).write(true).create_new(true).open(path.as_ref())
}
/// Write ports to ini file
pub fn create_ini(
instance_num: u16,
grpc_port: u32,
web_port: Option<u16>,
websocket_port: Option<u16>,
) -> std::io::Result<()> {
// Instantiate IniFile
let filepath = get_ini_filepath(instance_num);
let mut ini_file = IniFile::new(filepath);
// Write ports to ini file
if let Some(num) = web_port {
ini_file.insert("web.port", &num.to_string());
}
if let Some(num) = websocket_port {
ini_file.insert("ws.port", &num.to_string())
}
ini_file.insert("grpc.port", &grpc_port.to_string());
ini_file.write()
}
/// Remove netsim ini file
pub fn remove_ini(instance_num: u16) -> std::io::Result<()> {
let filepath = get_ini_filepath(instance_num);
std::fs::remove_file(filepath)
}
/// Get the filepath of netsim.ini under discovery directory
fn get_ini_filepath(instance_num: u16) -> PathBuf {
let mut discovery_dir = get_discovery_directory();
let filename = if instance_num == 1 {
"netsim.ini".to_string()
} else {
format!("netsim_{instance_num}.ini")
};
discovery_dir.push(filename);
discovery_dir
}
/// Get the grpc server address for netsim
pub fn get_server_address(instance_num: u16) -> Option<String> {
let filepath = get_ini_filepath(instance_num);
if !filepath.exists() {
error!("Unable to find netsim ini file: {filepath:?}");
return None;
}
if !filepath.is_file() {
error!("Not a file: {filepath:?}");
return None;
}
let mut ini_file = IniFile::new(filepath);
if let Err(err) = ini_file.read() {
error!("Error reading ini file: {err:?}");
}
ini_file.get("grpc.port").map(|s: &str| {
if s.contains(':') {
s.to_string()
} else {
format!("localhost:{}", s)
}
})
}
#[cfg(test)]
mod tests {
use std::fs::File;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::{env, time::SystemTime};
use super::get_ini_filepath;
use super::IniFile;
use crate::tests::ENV_MUTEX;
impl IniFile {
/// Checks if a certain key exists in the file.
///
/// # Arguments
///
/// * `key` - The key to check.
///
/// # Returns
///
/// `true` if the key exists, `false` otherwise.
fn contains_key(&self, key: &str) -> bool {
self.data.contains_key(key)
}
}
fn get_temp_ini_filepath(prefix: &str) -> PathBuf {
env::temp_dir().join(format!(
"{prefix}_{}.ini",
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos()
.to_string()
+ "_"
+ &rand::random::<u64>().to_string()
))
}
// NOTE: ctest run a test at least twice tests in parallel, so we need to use unique temp file
// to prevent tests from accessing the same file simultaneously.
#[test]
fn test_read() {
for test_case in ["port=123", "port= 123", "port =123", " port = 123 "] {
let filepath = get_temp_ini_filepath("test_read");
{
let mut tmpfile = match File::create(&filepath) {
Ok(f) => f,
Err(_) => return,
};
writeln!(tmpfile, "{test_case}").unwrap();
}
let mut inifile = IniFile::new(filepath.clone());
inifile.read().unwrap();
assert!(!inifile.contains_key("unknown-key"));
assert!(inifile.contains_key("port"), "Fail in test case: {test_case}");
assert_eq!(inifile.get("port").unwrap(), "123");
assert_eq!(inifile.get("unknown-key"), None);
// Note that there is no guarantee that the file is immediately deleted (e.g.,
// depending on platform, other open file descriptors may prevent immediate removal).
// https://doc.rust-lang.org/std/fs/fn.remove_file.html.
std::fs::remove_file(filepath).unwrap();
}
}
#[test]
fn test_read_no_newline() {
let filepath = get_temp_ini_filepath("test_read_no_newline");
{
let mut tmpfile = match File::create(&filepath) {
Ok(f) => f,
Err(_) => return,
};
write!(tmpfile, "port=123").unwrap();
}
let mut inifile = IniFile::new(filepath.clone());
inifile.read().unwrap();
assert!(!inifile.contains_key("unknown-key"));
assert!(inifile.contains_key("port"));
assert_eq!(inifile.get("port").unwrap(), "123");
assert_eq!(inifile.get("unknown-key"), None);
std::fs::remove_file(filepath).unwrap();
}
#[test]
fn test_read_no_file() {
let filepath = get_temp_ini_filepath("test_read_no_file");
let mut inifile = IniFile::new(filepath.clone());
assert!(inifile.read().is_err());
}
#[test]
fn test_read_multiple_lines() {
let filepath = get_temp_ini_filepath("test_read_multiple_lines");
{
let mut tmpfile = match File::create(&filepath) {
Ok(f) => f,
Err(_) => return,
};
write!(tmpfile, "port=123\nport2=456\n").unwrap();
}
let mut inifile = IniFile::new(filepath.clone());
inifile.read().unwrap();
assert!(!inifile.contains_key("unknown-key"));
assert!(inifile.contains_key("port"));
assert!(inifile.contains_key("port2"));
assert_eq!(inifile.get("port").unwrap(), "123");
assert_eq!(inifile.get("port2").unwrap(), "456");
assert_eq!(inifile.get("unknown-key"), None);
std::fs::remove_file(filepath).unwrap();
}
#[test]
fn test_insert_and_contains_key() {
let filepath = get_temp_ini_filepath("test_insert_and_contains_key");
let mut inifile = IniFile::new(filepath);
assert!(!inifile.contains_key("port"));
assert!(!inifile.contains_key("unknown-key"));
inifile.insert("port", "123");
assert!(inifile.contains_key("port"));
assert!(!inifile.contains_key("unknown-key"));
assert_eq!(inifile.get("port").unwrap(), "123");
assert_eq!(inifile.get("unknown-key"), None);
// Update the value of an existing key.
inifile.insert("port", "234");
assert!(inifile.contains_key("port"));
assert!(!inifile.contains_key("unknown-key"));
assert_eq!(inifile.get("port").unwrap(), "234");
assert_eq!(inifile.get("unknown-key"), None);
}
#[test]
fn test_write() {
let filepath = get_temp_ini_filepath("test_write");
let mut inifile = IniFile::new(filepath.clone());
assert!(!inifile.contains_key("port"));
assert!(!inifile.contains_key("unknown-key"));
inifile.insert("port", "123");
assert!(inifile.contains_key("port"));
assert!(!inifile.contains_key("unknown-key"));
assert_eq!(inifile.get("port").unwrap(), "123");
assert_eq!(inifile.get("unknown-key"), None);
if inifile.write().is_err() {
return;
}
let mut file = File::open(&filepath).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
assert_eq!(contents, "port=123\n");
std::fs::remove_file(filepath).unwrap();
}
#[test]
fn test_write_and_read() {
let filepath = get_temp_ini_filepath("test_write_and_read");
{
let mut inifile = IniFile::new(filepath.clone());
assert!(!inifile.contains_key("port"));
assert!(!inifile.contains_key("port2"));
assert!(!inifile.contains_key("unknown-key"));
inifile.insert("port", "123");
inifile.insert("port2", "456");
assert!(inifile.contains_key("port"));
assert!(!inifile.contains_key("unknown-key"));
assert_eq!(inifile.get("port").unwrap(), "123");
assert_eq!(inifile.get("unknown-key"), None);
if inifile.write().is_err() {
return;
}
}
let mut inifile = IniFile::new(filepath.clone());
inifile.read().unwrap();
assert!(!inifile.contains_key("unknown-key"));
assert!(inifile.contains_key("port"));
assert!(inifile.contains_key("port2"));
assert_eq!(inifile.get("port").unwrap(), "123");
assert_eq!(inifile.get("port2").unwrap(), "456");
assert_eq!(inifile.get("unknown-key"), None);
std::fs::remove_file(filepath).unwrap();
}
#[test]
fn test_get_ini_filepath() {
let _locked = ENV_MUTEX.lock();
// Test with TMPDIR variable
std::env::set_var("TMPDIR", "/tmpdir");
// Test get_netsim_ini_filepath
assert_eq!(get_ini_filepath(1), PathBuf::from("/tmpdir/netsim.ini"));
assert_eq!(get_ini_filepath(2), PathBuf::from("/tmpdir/netsim_2.ini"));
}
}