use anyhow::{format_err, Error};
use std::collections::HashMap;
use std::io::Write;
use std::os::fd::AsRawFd;
use tracing::{error, info, warn};
use wlan_stash_constants::{FileContent, PersistentStorageData};
type FileContents = HashMap<String, FileContent>;
const SAVED_NETWORKS_KEY: &str = "saved_networks";
const VERSION_KEY: &str = "version";
const CURRENT_VERSION: u8 = 1;
pub struct StorageStore {
path: std::path::PathBuf,
}
impl StorageStore {
pub fn new(path: impl AsRef<std::path::Path>) -> Self {
Self { path: path.as_ref().into() }
}
pub fn empty_store(&self) -> Result<(), Error> {
self.write(Vec::new())
}
pub fn write(&self, network_configs: Vec<PersistentStorageData>) -> Result<(), Error> {
let mut tmp_path = self.path.clone();
tmp_path.as_mut_os_string().push(".tmp");
{
let mut data = FileContents::new();
data.insert(VERSION_KEY.to_string(), FileContent::Version(CURRENT_VERSION));
data.insert(SAVED_NETWORKS_KEY.to_string(), FileContent::Networks(network_configs));
let serialized_data = serde_json::to_vec(&data)?;
let mut file = std::fs::File::create(tmp_path.clone())?;
file.write_all(&serialized_data)?;
fuchsia_nix::unistd::fsync(file.as_raw_fd())?;
}
std::fs::rename(&tmp_path, &self.path)?;
Ok(())
}
pub fn load(&self) -> Result<Vec<PersistentStorageData>, Error> {
let serialized_data = std::fs::read(&self.path)?;
let mut contents: FileContents = serde_json::from_slice(&serialized_data)?;
match contents.get(VERSION_KEY) {
Some(FileContent::Version(version)) => {
if version > &CURRENT_VERSION {
info!(
"The version of the networks data is newer than the current version.
Unrecognizable data may not be retained."
);
}
}
Some(FileContent::Networks(_)) => {
error!("The recorded version was interpreted as saved networks data. The newest version will be used to interpret data.");
}
None => {
warn!(
"The version is not present in the saved networks file. It will be read
as the current version."
);
}
}
let networks = match contents.remove(SAVED_NETWORKS_KEY) {
Some(FileContent::Networks(n)) => n,
Some(_) => {
return Err(format_err!("Networks deserialized as the wrong type"));
}
None => {
return Err(format_err!("Networks missing from storage file"));
}
};
Ok(networks)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::rand_string;
use wlan_stash_constants::{Credential, SecurityType};
#[fuchsia::test]
fn test_write_and_load() {
let path = format!("/data/config.{}", rand_string());
let store = StorageStore::new(&path);
let network_configs = vec![PersistentStorageData {
ssid: "foo".into(),
security_type: SecurityType::Wpa2,
credential: Credential::Password(b"password".to_vec()),
has_ever_connected: false,
}];
store.write(network_configs.clone()).expect("write failed");
let store = StorageStore::new(&path);
assert_eq!(store.load().expect("load failed"), network_configs);
}
#[fuchsia::test]
fn test_load_if_necessary_parts_present_missing_field() {
let future_version = 255;
let file_contents = format!(
"{{\
\"saved_networks\":[{{\
\"ssid\":[100, 100, 100, 100, 100, 100],\
\"security_type\":\"Wpa2\",\
\"credential\":{{\"Password\":[100, 100, 100, 100, 100, 100]}}\
}}],\
\"{VERSION_KEY}\":{future_version}\
}}"
)
.into_bytes();
let network_configs = vec![PersistentStorageData {
ssid: vec![100, 100, 100, 100, 100, 100],
security_type: SecurityType::Wpa2,
credential: Credential::Password(vec![100, 100, 100, 100, 100, 100]),
has_ever_connected: false,
}];
let path = format!("/data/config.{}", rand_string());
let mut file = std::fs::File::create(&path).expect("failed to open file for writing");
assert_eq!(
file.write(&file_contents.clone()).expect("Failed to write to file"),
file_contents.len()
);
file.flush().expect("failed to flush contents of file");
let store = StorageStore::new(&path);
assert_eq!(store.load().expect("load failed"), network_configs);
}
#[fuchsia::test]
fn test_load_if_necessary_parts_present_extra_field() {
let future_version = 255;
let file_contents = format!(
"{{\
\"saved_networks\":[{{\
\"ssid\":[100, 100, 100, 100, 100, 100],\
\"security_type\":\"Wpa2\",\
\"credential\":{{\"Password\":[100, 100, 100, 100, 100, 100]}},\
\"has_ever_connected\": false,
\"other_field\": true
}}],\
\"{VERSION_KEY}\":{future_version}\
}}"
)
.into_bytes();
let network_configs = vec![PersistentStorageData {
ssid: vec![100, 100, 100, 100, 100, 100],
security_type: SecurityType::Wpa2,
credential: Credential::Password(vec![100, 100, 100, 100, 100, 100]),
has_ever_connected: false,
}];
let path = format!("/data/config.{}", rand_string());
let mut file = std::fs::File::create(&path).expect("failed to open file for writing");
assert_eq!(
file.write(&file_contents.clone()).expect("Failed to write to file"),
file_contents.len()
);
file.flush().expect("failed to flush contents of file");
let store = StorageStore::new(&path);
assert_eq!(store.load().expect("load failed"), network_configs);
}
#[fuchsia::test]
fn test_delete_store() {
let path = format!("/data/config.{}", rand_string());
let store = StorageStore::new(&path);
let network_config = vec![PersistentStorageData {
ssid: "foo".into(),
security_type: SecurityType::Wpa2,
credential: Credential::Password(b"password".to_vec()),
has_ever_connected: false,
}];
store.write(network_config.clone()).expect("write failed");
assert_eq!(store.load().expect("load failed"), network_config);
store.empty_store().expect("empty_store failed");
let store = StorageStore::new(&path);
assert_eq!(store.load().expect("load failed"), Vec::new());
}
}