| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! The store can be serialized and deserialized as a custom form of tlv: |
| //! https://en.wikipedia.org/wiki/Type-length-value |
| //! |
| //! It would be simpler here to simply pull in an external library to do something like json |
| //! serialization, but this implementation is much less likely to cause security concerns due to the |
| //! reduced functionality of the code. |
| |
| use anyhow::{format_err, Error}; |
| use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; |
| use fidl_fuchsia_mem; |
| use fidl_fuchsia_stash::{KeyValue, ListItem, Value, ValueType}; |
| use fuchsia_syslog::fx_log_info; |
| use fuchsia_zircon as zx; |
| use std::collections::HashMap; |
| use std::fs; |
| use std::io::{Cursor, ErrorKind, Read, Write}; |
| use std::path::PathBuf; |
| |
| pub type ClientName = String; |
| pub type Key = String; |
| |
| /// Clones a Value reference into a new Value. Can fail if the syscalls to clone a vmo fail. |
| /// We need to be able to clone Value to use it directly in the store, but both the Clone trait and |
| /// Value struct are external, so we can't impl Clone for Value. |
| pub fn clone_value(v: &Value) -> Result<Value, Error> { |
| Ok(match v { |
| Value::Intval(x) => Value::Intval(x.clone()), |
| Value::Floatval(x) => Value::Floatval(x.clone()), |
| Value::Boolval(x) => Value::Boolval(x.clone()), |
| Value::Stringval(x) => Value::Stringval(x.clone()), |
| Value::Bytesval(x) => Value::Bytesval(clone_buffer(x)?), |
| }) |
| } |
| |
| /// Creates a copy-on-write clone of the given fidl buffer |
| pub fn clone_buffer(b: &fidl_fuchsia_mem::Buffer) -> Result<fidl_fuchsia_mem::Buffer, Error> { |
| let new_vmo = b |
| .vmo |
| .create_child(zx::VmoChildOptions::COPY_ON_WRITE, 0, b.size) |
| .map_err(|s| format_err!(format!("error cloning buffer, zx status: {}", s)))?; |
| Ok(fidl_fuchsia_mem::Buffer { vmo: new_vmo, size: b.size }) |
| } |
| |
| /// Store is the struct representing the contents of the backing tlv file |
| #[derive(Debug, PartialEq, Default)] |
| pub struct Store { |
| data: HashMap<ClientName, HashMap<Key, Value>>, |
| } |
| |
| fn value_type_as_bytes(v: &Value) -> &[u8] { |
| match v { |
| Value::Intval(_) => &[0x00], |
| Value::Floatval(_) => &[0x01], |
| Value::Boolval(_) => &[0x02], |
| Value::Stringval(_) => &[0x03], |
| Value::Bytesval(_) => &[0x04], |
| } |
| } |
| |
| fn value_into_bytes(v: &Value) -> Result<Vec<u8>, Error> { |
| let mut wrt = vec![]; |
| match v { |
| Value::Intval(i) => wrt.write_i64::<LittleEndian>(*i)?, |
| Value::Floatval(f) => wrt.write_f64::<LittleEndian>(*f)?, |
| Value::Boolval(false) => wrt.write_u8(0x00)?, |
| Value::Boolval(true) => wrt.write_u8(0x01)?, |
| Value::Stringval(s) => wrt.append(&mut s.clone().into_bytes()), |
| Value::Bytesval(b) => { |
| let mut buf = vec![0; b.size as usize]; |
| b.vmo |
| .read(&mut buf, 0) |
| .map_err(|s| format_err!(format!("error reading buffer, zx status: {}", s)))?; |
| wrt.append(&mut buf); |
| } |
| } |
| Ok(wrt) |
| } |
| fn value_from_bytes(typ: u8, bytes: Vec<u8>) -> Result<Value, Error> { |
| match typ { |
| 0x00 => Ok(Value::Intval(Cursor::new(bytes).read_i64::<LittleEndian>()?)), |
| 0x01 => Ok(Value::Floatval(Cursor::new(bytes).read_f64::<LittleEndian>()?)), |
| 0x02 => match Cursor::new(bytes).read_u8()? { |
| 0x00 => Ok(Value::Boolval(false)), |
| 0x01 => Ok(Value::Boolval(true)), |
| b => Err(format_err!(format!("unknown bool value: {}", b))), |
| }, |
| 0x03 => Ok(Value::Stringval(String::from_utf8(bytes)?)), |
| 0x04 => { |
| let vmo = zx::Vmo::create(bytes.len() as u64) |
| .map_err(|s| format_err!(format!("error creating buffer, zx status: {}", s)))?; |
| vmo.write(&bytes, 0) |
| .map_err(|s| format_err!(format!("error writing buffer, zx status: {}", s)))?; |
| Ok(Value::Bytesval(fidl_fuchsia_mem::Buffer { vmo: vmo, size: bytes.len() as u64 })) |
| } |
| t => Err(format_err!(format!("unknown type: {}", t))), |
| } |
| } |
| |
| impl Store { |
| fn serialize(&self) -> Result<Vec<u8>, Error> { |
| let mut res = vec![]; |
| for (client_name, client_data) in self.data.iter() { |
| let mut client_name_length = vec![]; |
| client_name_length.write_u64::<LittleEndian>(client_name.len() as u64)?; |
| let mut client_name_bytes = client_name.clone().into_bytes(); |
| |
| let mut client_entries = vec![]; |
| client_entries.write_u64::<LittleEndian>(client_data.len() as u64)?; |
| |
| res.append(&mut client_name_length); |
| res.append(&mut client_name_bytes); |
| res.append(&mut client_entries); |
| |
| for (key, val) in client_data { |
| let mut key_length = vec![]; |
| key_length.write_u64::<LittleEndian>(key.len() as u64)?; |
| let mut key_bytes = key.clone().into_bytes(); |
| let val_type = value_type_as_bytes(val); |
| let mut val_bytes = value_into_bytes(val)?; |
| let mut val_length = vec![]; |
| val_length.write_u64::<LittleEndian>(val_bytes.len() as u64)?; |
| |
| res.append(&mut key_length); |
| res.append(&mut key_bytes); |
| res.extend_from_slice(val_type); |
| res.append(&mut val_length); |
| res.append(&mut val_bytes); |
| } |
| } |
| Ok(res) |
| } |
| |
| fn deserialize(bytes: Vec<u8>) -> Result<Store, Error> { |
| let mut res = Store::default(); |
| let bytes_len = bytes.len() as u64; |
| let mut cursor = Cursor::new(bytes); |
| while cursor.position() < bytes_len { |
| let client_name_length = cursor.read_u64::<LittleEndian>()?; |
| let mut client_name = Vec::new(); |
| client_name.resize(client_name_length as usize, 0); |
| cursor.read(&mut client_name[..])?; |
| |
| let client_entries = cursor.read_u64::<LittleEndian>()?; |
| |
| let mut client_data = HashMap::new(); |
| for _ in 0..client_entries { |
| let key_length = cursor.read_u64::<LittleEndian>()?; |
| let mut key_bytes = Vec::new(); |
| key_bytes.resize(key_length as usize, 0); |
| cursor.read(&mut key_bytes[..])?; |
| |
| let mut val_type = [0; 1]; |
| cursor.read(&mut val_type[..])?; |
| let val_type = val_type[0]; |
| |
| let val_length = cursor.read_u64::<LittleEndian>()?; |
| let mut val_bytes = Vec::new(); |
| val_bytes.resize(val_length as usize, 0); |
| cursor.read(&mut val_bytes[..])?; |
| |
| client_data |
| .insert(String::from_utf8(key_bytes)?, value_from_bytes(val_type, val_bytes)?); |
| } |
| |
| res.data.insert(String::from_utf8(client_name)?, client_data); |
| } |
| Ok(res) |
| } |
| } |
| |
| pub fn value_to_type(v: &Value) -> ValueType { |
| match v { |
| Value::Intval(_) => ValueType::IntVal, |
| Value::Floatval(_) => ValueType::FloatVal, |
| Value::Boolval(_) => ValueType::BoolVal, |
| Value::Stringval(_) => ValueType::StringVal, |
| Value::Bytesval(_) => ValueType::BytesVal, |
| } |
| } |
| |
| /// StoreManager is used to hold and manipulate a Store, writing changes to disk when appropriate. |
| pub struct StoreManager { |
| backing_file: PathBuf, |
| store: Store, |
| } |
| |
| impl StoreManager { |
| /// Create a new StoreManager, loading the store from backing_file if it exists, and writing |
| /// changes to the store into backing_file. |
| pub fn new<P: Into<PathBuf>>(backing_file: P) -> Result<StoreManager, Error> { |
| let backing_file = backing_file.into(); |
| let store = match fs::read(&backing_file) { |
| Ok(buf) => { |
| Store::deserialize(buf)? |
| } |
| Err(e) => { |
| if e.kind() != ErrorKind::NotFound { |
| return Err(format_err!(format!("{}", e))); |
| } |
| fx_log_info!("store file doesn't exist, using empty store"); |
| Store::default() |
| } |
| }; |
| |
| Ok(StoreManager { backing_file, store }) |
| } |
| |
| fn save_store(&self) -> Result<(), Error> { |
| let temp_file_path = self.backing_file.with_extension("tmp"); |
| let store_contents = self.store.serialize()?; |
| { |
| let mut f = fs::OpenOptions::new() |
| .write(true) |
| .truncate(true) |
| .create(true) |
| .open(temp_file_path.clone())?; |
| |
| f.write_all(&store_contents)?; |
| } |
| fs::rename(temp_file_path, &self.backing_file)?; |
| Ok(()) |
| } |
| |
| /// Gets a value from the store. Returns None if the value is unset. |
| pub fn get_value(&self, client_name: &str, key: &str) -> Option<&Value> { |
| match self.store.data.get(client_name) { |
| None => None, |
| Some(client_fields) => client_fields.get(key), |
| } |
| } |
| |
| /// Sets a value in the store. |
| pub fn set_value(&mut self, client_name: &str, key: String, field: Value) -> Result<(), Error> { |
| let client_fields = |
| self.store.data.entry(client_name.to_string()).or_insert(HashMap::new()); |
| client_fields.insert(key, field); |
| self.save_store() |
| } |
| |
| /// Delete a value from the store. Will return Ok(true) for a successful deletion, Ok(false) if |
| /// the field doesn't exist, and Err(e) on any unexpected errors. |
| pub fn delete_value(&mut self, client_name: &str, key: &str) -> Result<(), Error> { |
| let client_fields = |
| self.store.data.entry(client_name.to_string()).or_insert(HashMap::new()); |
| client_fields.remove(key); |
| self.save_store() |
| } |
| |
| /// Retrieves a list of key/type pairs for all keys containing the given prefix. |
| pub fn list_prefix(&self, client_name: &str, prefix: &str) -> Vec<ListItem> { |
| let o_client_fields = self.store.data.get(client_name); |
| if o_client_fields.is_none() { |
| return vec![]; |
| } |
| let client_fields = o_client_fields.unwrap(); |
| let mut result = vec![]; |
| for (k, v) in client_fields.iter() { |
| if k.starts_with(prefix) { |
| result.push(ListItem { key: k.clone(), type_: value_to_type(&v) }); |
| } |
| } |
| result |
| } |
| |
| /// Retrieves a list of key/value pairs for all keys containing the given prefix. |
| pub fn get_prefix(&self, client_name: &str, prefix: &str) -> Result<Vec<KeyValue>, Error> { |
| let o_client_fields = self.store.data.get(client_name); |
| if o_client_fields.is_none() { |
| return Ok(vec![]); |
| } |
| let client_fields = o_client_fields.unwrap(); |
| let mut res = Vec::new(); |
| for (k, v) in client_fields.iter() { |
| if k.starts_with(prefix) { |
| res.push(KeyValue { key: k.clone(), val: clone_value(v)? }); |
| } |
| } |
| Ok(res) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use crate::store::*; |
| use std::io; |
| use tempfile::TempDir; |
| |
| fn get_tmp_store_manager(tmp_dir: &TempDir) -> StoreManager { |
| StoreManager::new(tmp_dir.path().join("stash.store")).unwrap() |
| } |
| |
| #[test] |
| fn test_get_value() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let mut sm = get_tmp_store_manager(&tmp_dir); |
| |
| let test_client_name = "test_client".to_owned(); |
| let test_key = "test_key".to_owned(); |
| let test_field = Value::Boolval(true); |
| |
| let mut client_data = HashMap::new(); |
| client_data.insert(test_key.clone(), clone_value(&test_field).unwrap()); |
| |
| sm.store.data.insert(test_client_name.clone(), client_data); |
| sm.save_store().unwrap(); |
| |
| let fetched_field = sm.get_value(&test_client_name, &test_key); |
| assert_eq!(fetched_field, Some(&test_field)); |
| } |
| |
| #[test] |
| fn test_get_nonexistent_value() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let sm = get_tmp_store_manager(&tmp_dir); |
| |
| let test_client_name = "test_client".to_owned(); |
| let test_key = "test_key".to_owned(); |
| |
| let fetched_field = sm.get_value(&test_client_name, &test_key); |
| assert_eq!(fetched_field, None); |
| } |
| |
| #[test] |
| fn test_set_value() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let mut sm = get_tmp_store_manager(&tmp_dir); |
| |
| let test_client_name = "test_client".to_owned(); |
| let test_key = "test_key".to_owned(); |
| let test_field = Value::Boolval(true); |
| |
| sm.set_value(&test_client_name, test_key.clone(), clone_value(&test_field).unwrap()) |
| .unwrap(); |
| |
| let saved_field = sm.store.data.get(&test_client_name).unwrap().get(&test_key).unwrap(); |
| |
| assert_eq!(saved_field, &test_field); |
| } |
| |
| #[test] |
| fn test_delete_value() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let mut sm = get_tmp_store_manager(&tmp_dir); |
| |
| let test_client_name = "test_client".to_owned(); |
| let test_key = "test_key".to_owned(); |
| let test_field = Value::Boolval(true); |
| |
| let mut client_data = HashMap::new(); |
| client_data.insert(test_key.clone(), clone_value(&test_field).unwrap()); |
| |
| sm.store.data.insert(test_client_name.clone(), client_data); |
| sm.save_store().unwrap(); |
| |
| sm.delete_value(&test_client_name, &test_key).unwrap(); |
| assert_eq!(None, sm.store.data.get(&test_client_name).unwrap().get(&test_key)); |
| } |
| |
| #[test] |
| fn test_list_prefix() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let mut sm = get_tmp_store_manager(&tmp_dir); |
| |
| let test_client_name = "test_client".to_owned(); |
| let test_field = Value::Boolval(true); |
| |
| let mut client_data = HashMap::new(); |
| client_data.insert("a".to_string(), clone_value(&test_field).unwrap()); |
| client_data.insert("a/a".to_string(), clone_value(&test_field).unwrap()); |
| client_data.insert("b".to_string(), clone_value(&test_field).unwrap()); |
| |
| sm.store.data.insert(test_client_name.clone(), client_data); |
| sm.save_store().unwrap(); |
| |
| let res = sm.list_prefix(&test_client_name, &"a".to_string()); |
| assert_eq!(2, res.len()); |
| assert_eq!( |
| true, |
| res.contains(&ListItem { key: "a".to_string(), type_: ValueType::BoolVal }) |
| ); |
| assert_eq!( |
| true, |
| res.contains(&ListItem { key: "a/a".to_string(), type_: ValueType::BoolVal }) |
| ); |
| } |
| |
| #[test] |
| fn test_get_prefix() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let mut sm = get_tmp_store_manager(&tmp_dir); |
| |
| let test_client_name = "test_client".to_owned(); |
| let test_field = Value::Boolval(true); |
| |
| let mut client_data = HashMap::new(); |
| client_data.insert("a".to_string(), clone_value(&test_field).unwrap()); |
| client_data.insert("a/a".to_string(), clone_value(&test_field).unwrap()); |
| client_data.insert("b".to_string(), clone_value(&test_field).unwrap()); |
| |
| sm.store.data.insert(test_client_name.clone(), client_data); |
| sm.save_store().unwrap(); |
| |
| let res = sm.get_prefix(&test_client_name, &"a".to_string()).unwrap(); |
| assert_eq!(2, res.len()); |
| assert_eq!( |
| true, |
| res.contains(&KeyValue { key: "a".to_string(), val: Value::Boolval(true) }) |
| ); |
| assert_eq!( |
| true, |
| res.contains(&KeyValue { key: "a/a".to_string(), val: Value::Boolval(true) }) |
| ); |
| } |
| |
| #[test] |
| fn test_serialize_deserialize() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let backing_file = tmp_dir.path().join("file.store"); |
| let test_client = "test_client"; |
| |
| let mut store = Store::default(); |
| store.data.insert(test_client.to_string(), HashMap::new()); |
| store |
| .data |
| .get_mut(&test_client.to_string()) |
| .unwrap() |
| .insert("int".to_string(), Value::Intval(1)); |
| store |
| .data |
| .get_mut(&test_client.to_string()) |
| .unwrap() |
| .insert("float".to_string(), Value::Floatval(1.0)); |
| store |
| .data |
| .get_mut(&test_client.to_string()) |
| .unwrap() |
| .insert("bool true".to_string(), Value::Boolval(true)); |
| store |
| .data |
| .get_mut(&test_client.to_string()) |
| .unwrap() |
| .insert("bool false".to_string(), Value::Boolval(false)); |
| store |
| .data |
| .get_mut(&test_client.to_string()) |
| .unwrap() |
| .insert("string".to_string(), Value::Stringval("test string".to_string())); |
| // Value::Bytesval can't be tested here because the vmo handle number changes during the |
| // clone operation, making the assert_eq! below fail. |
| |
| let clone_store = |_data: &Store| { |
| let mut res = HashMap::new(); |
| for (client_name, client_fields) in store.data.iter() { |
| let mut client_data = HashMap::new(); |
| for (k, v) in client_fields.iter() { |
| client_data.insert(k.clone(), clone_value(v).unwrap()); |
| } |
| res.insert(client_name.clone(), client_data); |
| } |
| Store { data: res } |
| }; |
| |
| let mut sm_writer = StoreManager::new(backing_file.clone()) |
| .expect("couldn't make store manager for writing"); |
| sm_writer.store = clone_store(&store); |
| sm_writer.save_store().expect("couldn't save store"); |
| |
| let sm_reader = StoreManager::new(backing_file.clone()) |
| .expect("couldn't make store manager from pre-populated file"); |
| |
| assert_eq!(store, sm_writer.store); |
| assert_eq!(store, sm_reader.store); |
| } |
| |
| #[test] |
| fn test_comprehensive() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let mut sm = get_tmp_store_manager(&tmp_dir); |
| |
| let test_client_name = "test_client".to_owned(); |
| let test_key = "test_key".to_owned(); |
| let test_field = Value::Boolval(true); |
| |
| sm.set_value(&test_client_name, test_key.clone(), clone_value(&test_field).unwrap()) |
| .unwrap(); |
| |
| assert_eq!(Some(&test_field), sm.get_value(&test_client_name, &test_key)); |
| assert_eq!( |
| vec![ListItem { key: test_key.clone(), type_: ValueType::BoolVal }], |
| sm.list_prefix(&test_client_name, &"".to_string()) |
| ); |
| assert_eq!( |
| vec![KeyValue { key: test_key.clone(), val: Value::Boolval(true) }], |
| sm.get_prefix(&test_client_name, &"".to_string()).unwrap() |
| ); |
| |
| sm.delete_value(&test_client_name, &test_key).unwrap(); |
| |
| assert_eq!(None, sm.get_value(&test_client_name, &test_key)); |
| assert_eq!(vec![] as Vec<ListItem>, sm.list_prefix(&test_client_name, &"".to_string())); |
| assert_eq!( |
| vec![] as Vec<KeyValue>, |
| sm.get_prefix(&test_client_name, &"".to_string()).unwrap() |
| ); |
| } |
| |
| #[test] |
| fn test_file_is_updated() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let mut sm = get_tmp_store_manager(&tmp_dir); |
| |
| let test_client_name = "test_client".to_owned(); |
| let file_path = sm.backing_file.clone(); |
| |
| let mut curr_file_size = 0; |
| let mut new_file_size; |
| |
| // File shouldn't exist when we start |
| assert_eq!(fs::metadata(&file_path).unwrap_err().kind(), io::ErrorKind::NotFound); |
| |
| // Set a field, the file size should increase |
| sm.set_value(&test_client_name, "foo".to_string(), Value::Boolval(true)).unwrap(); |
| sm.save_store().unwrap(); |
| |
| new_file_size = fs::metadata(&file_path).unwrap().len(); |
| assert!(curr_file_size < new_file_size); |
| curr_file_size = new_file_size; |
| |
| // Set a different field, the file size should increase |
| sm.set_value(&test_client_name, "bar".to_string(), Value::Boolval(true)).unwrap(); |
| sm.save_store().unwrap(); |
| |
| new_file_size = fs::metadata(&file_path).unwrap().len(); |
| assert!(curr_file_size < new_file_size); |
| curr_file_size = new_file_size; |
| |
| // Delete a field, the file size should decrease |
| sm.delete_value(&test_client_name, "bar").unwrap(); |
| sm.save_store().unwrap(); |
| |
| new_file_size = fs::metadata(&file_path).unwrap().len(); |
| assert!(curr_file_size > new_file_size); |
| curr_file_size = new_file_size; |
| |
| // Delete a field, the file size should decrease |
| sm.delete_value(&test_client_name, "foo").unwrap(); |
| sm.save_store().unwrap(); |
| |
| new_file_size = fs::metadata(&file_path).unwrap().len(); |
| assert!(curr_file_size > new_file_size); |
| } |
| } |