| // 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. |
| |
| use { |
| failure::Error, |
| fidl::endpoints::create_proxy, |
| fidl_fuchsia_bluetooth_host::BondingData, |
| fidl_fuchsia_stash::{ |
| GetIteratorMarker, |
| StoreAccessorMarker, |
| StoreAccessorProxy, |
| StoreMarker, |
| Value, |
| }, |
| fuchsia_bluetooth::error::Error as BtError, |
| fuchsia_syslog::{fx_log_info, fx_log_err}, |
| serde_json, |
| std::collections::HashMap, |
| }; |
| |
| use crate::store::{ |
| keys::{BONDING_DATA_PREFIX, bonding_data_key}, |
| serde::{ BondingDataDeserializer, BondingDataSerializer}, |
| }; |
| |
| /// Stash manages persistent data that is stored in bt-gap's component-specific storage. |
| #[derive(Debug)] |
| pub struct Stash { |
| // The proxy to the Fuchsia stash service. This is assumed to have been initialized as a |
| // read/write capable accessor with the identity of the current component. |
| proxy: StoreAccessorProxy, |
| |
| // In-memory state of the bonding data stash. Each entry is hierarchically indexed by a |
| // local Bluetooth adapter identity and a peer device identifier. |
| bonding_data: HashMap<String, HashMap<String, BondingData>>, |
| } |
| |
| impl Stash { |
| /// Updates the bonding data for a given device. Creates a new entry if one matching this |
| /// device does not exist. |
| pub fn store_bond(&mut self, data: BondingData) -> Result<(), Error> { |
| fx_log_info!("store_bond (id: {})", data.identifier); |
| |
| // Persist the serialized blob. |
| let serialized = serde_json::to_string(&BondingDataSerializer(&data))?; |
| self.proxy.set_value( |
| &bonding_data_key(&data.identifier), |
| &mut Value::Stringval(serialized), |
| )?; |
| self.proxy.commit()?; |
| |
| // Update the in memory cache. |
| let local_map = self |
| .bonding_data |
| .entry(data.local_address.clone()) |
| .or_insert(HashMap::new()); |
| local_map.insert(data.identifier.clone(), data); |
| Ok(()) |
| } |
| |
| /// Returns an iterator over the bonding data entries for the local adapter with the given |
| /// `address`. Returns None if no such data exists. |
| pub fn list_bonds(&self, local_address: &str) -> Option<impl Iterator<Item = &BondingData>> { |
| Some(self.bonding_data.get(local_address)?.values().into_iter()) |
| } |
| |
| // Initializes the stash using the given `accessor`. This asynchronously loads existing |
| // stash data. Returns an error in case of failure. |
| async fn new(accessor: StoreAccessorProxy) -> Result<Stash, Error> { |
| // Obtain a list iterator for all cached bonding data. |
| let (iter, server_end) = create_proxy::<GetIteratorMarker>()?; |
| accessor.get_prefix(BONDING_DATA_PREFIX, server_end)?; |
| |
| let mut bonding_map = HashMap::new(); |
| loop { |
| let next = await!(iter.get_next())?; |
| if next.is_empty() { |
| break; |
| } |
| for key_value in next { |
| if let Value::Stringval(json) = key_value.val { |
| let bonding_data: BondingDataDeserializer = serde_json::from_str(&json)?; |
| let bonding_data = bonding_data.contents(); |
| let local_address_entries = bonding_map |
| .entry(bonding_data.local_address.clone()) |
| .or_insert(HashMap::new()); |
| local_address_entries.insert(bonding_data.identifier.clone(), bonding_data); |
| } else { |
| fx_log_err!("stash malformed: bonding data should be a string"); |
| return Err(BtError::new("failed to initialize stash").into()); |
| } |
| } |
| } |
| Ok(Stash { |
| proxy: accessor, |
| bonding_data: bonding_map, |
| }) |
| } |
| } |
| |
| /// Connects to the stash service and initializes a Stash object. This function obtains |
| /// read/write capability to the component-specific storage identified by `component_id`. |
| pub async fn init_stash(component_id: &str) -> Result<Stash, Error> { |
| let stash_svc = fuchsia_app::client::connect_to_service::<StoreMarker>()?; |
| stash_svc.identify(component_id)?; |
| |
| let (proxy, server_end) = create_proxy::<StoreAccessorMarker>()?; |
| stash_svc.create_accessor(false, server_end)?; |
| |
| await!(Stash::new(proxy)) |
| } |
| |
| // These tests access stash in a hermetic envionment and thus it's ok for state to leak between |
| // test runs, regardless of test failure. Each test clears out the state in stash before performing |
| // its test logic. |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use {fuchsia_app::client::connect_to_service, |
| fuchsia_async as fasync, |
| pin_utils::pin_mut}; |
| |
| // create_stash_accessor will create a new accessor to stash scoped under the given test name. |
| // All preexisting data in stash under this identity is deleted before the accessor is |
| // returned. |
| fn create_stash_accessor(test_name: &str) -> Result<StoreAccessorProxy, Error> { |
| let stashserver = connect_to_service::<StoreMarker>()?; |
| |
| // Identify |
| stashserver.identify(&(BONDING_DATA_PREFIX.to_owned() + test_name))?; |
| |
| // Create an accessor |
| let (acc, server_end) = create_proxy()?; |
| stashserver.create_accessor(false, server_end)?; |
| |
| // Clear all data in stash under our identity |
| acc.delete_prefix("")?; |
| acc.commit()?; |
| |
| Ok(acc) |
| } |
| |
| #[test] |
| fn new_stash_succeeds_with_empty_values() { |
| let mut exec = fasync::Executor::new().expect("failed to create an executor"); |
| |
| // Create a Stash service interface. |
| let accessor_proxy = create_stash_accessor("new_stash_succeeds_with_empty_values") |
| .expect("failed to create StashAccessor"); |
| let stash_new_future = Stash::new(accessor_proxy); |
| pin_mut!(stash_new_future); |
| |
| // The stash should be initialized with no data. |
| assert!(exec.run_singlethreaded(stash_new_future) |
| .expect("expected Stash to initialize") |
| .bonding_data.is_empty()); |
| } |
| |
| #[test] |
| fn new_stash_fails_with_malformed_key_value_entry() { |
| let mut exec = fasync::Executor::new().expect("failed to create an executor"); |
| |
| // Create a Stash service interface. |
| let accessor_proxy = |
| create_stash_accessor("new_stash_fails_with_malformed_key_value_entry") |
| .expect("failed to create StashAccessor"); |
| |
| // Set a key/value that contains a non-string value. |
| accessor_proxy.set_value("bonding-data:test1234", &mut Value::Intval(5)) |
| .expect("failed to set a bonding data value"); |
| accessor_proxy.commit() |
| .expect("failed to commit a bonding data value"); |
| |
| // The stash should fail to initialize. |
| let stash_new_future = Stash::new(accessor_proxy); |
| assert!(exec.run_singlethreaded(stash_new_future).is_err()); |
| } |
| |
| #[test] |
| fn new_stash_fails_with_malformed_json() { |
| let mut exec = fasync::Executor::new().expect("failed to create an executor"); |
| |
| // Create a mock Stash service interface. |
| let accessor_proxy = create_stash_accessor("new_stash_fails_with_malformed_json") |
| .expect("failed to create StashAccessor"); |
| |
| // Set a vector that contains a malformed JSON value |
| accessor_proxy.set_value("bonding-data:test1234", &mut Value::Stringval("{0}".to_string())) |
| .expect("failed to set a bonding data value"); |
| accessor_proxy.commit() |
| .expect("failed to commit a bonding data value"); |
| |
| // The stash should fail to initialize. |
| let stash_new_future = Stash::new(accessor_proxy); |
| assert!(exec.run_singlethreaded(stash_new_future).is_err()); |
| } |
| |
| #[test] |
| fn new_stash_succeeds_with_values() { |
| let mut exec = fasync::Executor::new().expect("failed to create an executor"); |
| |
| // Create a Stash service interface. |
| let accessor_proxy = create_stash_accessor("new_stash_succeeds_with_values") |
| .expect("failed to create StashAccessor"); |
| |
| |
| // Insert values into stash that contain bonding data for several devices. |
| accessor_proxy.set_value("bonding-data:id-1", &mut Value::Stringval( |
| r#" |
| { |
| "identifier": "id-1", |
| "localAddress": "00:00:00:00:00:01", |
| "name": "Test Device 1", |
| "le": null |
| }"# |
| .to_string(), |
| )).expect("failed to set value"); |
| accessor_proxy.set_value("bonding-data:id-2", &mut Value::Stringval( |
| r#" |
| { |
| "identifier": "id-2", |
| "localAddress": "00:00:00:00:00:01", |
| "name": "Test Device 2", |
| "le": null |
| }"# |
| .to_string(), |
| )).expect("failed to set value"); |
| accessor_proxy.set_value("bonding-data:id-3", &mut Value::Stringval( |
| r#" |
| { |
| "identifier": "id-3", |
| "localAddress": "00:00:00:00:00:02", |
| "name": null, |
| "le": null |
| }"# |
| .to_string(), |
| )).expect("failed to set value"); |
| accessor_proxy.commit() |
| .expect("failed to commit bonding data values"); |
| |
| // The stash should initialize with bonding data stored in stash |
| let stash_new_future = Stash::new(accessor_proxy); |
| let stash = exec.run_singlethreaded(stash_new_future).expect("stash failed to initialize"); |
| |
| // There should be devices registered for two local addresses. |
| assert_eq!(2, stash.bonding_data.len()); |
| |
| // The first local address should have two devices associated with it. |
| let local = stash |
| .bonding_data |
| .get("00:00:00:00:00:01") |
| .expect("could not find local address entries"); |
| assert_eq!(2, local.len()); |
| assert_eq!( |
| &BondingData { |
| identifier: "id-1".to_string(), |
| local_address: "00:00:00:00:00:01".to_string(), |
| name: Some("Test Device 1".to_string()), |
| le: None, |
| }, |
| local.get("id-1").expect("could not find device") |
| ); |
| assert_eq!( |
| &BondingData { |
| identifier: "id-2".to_string(), |
| local_address: "00:00:00:00:00:01".to_string(), |
| name: Some("Test Device 2".to_string()), |
| le: None, |
| }, |
| local.get("id-2").expect("could not find device") |
| ); |
| |
| // The second local address should have one device associated with it. |
| let local = stash |
| .bonding_data |
| .get("00:00:00:00:00:02") |
| .expect("could not find local address entries"); |
| assert_eq!(1, local.len()); |
| assert_eq!( |
| &BondingData { |
| identifier: "id-3".to_string(), |
| local_address: "00:00:00:00:00:02".to_string(), |
| name: None, |
| le: None, |
| }, |
| local.get("id-3").expect("could not find device") |
| ); |
| } |
| |
| #[test] |
| fn store_bond_commits_entry() { |
| let mut exec = fasync::Executor::new().expect("failed to create an executor"); |
| let accessor_proxy = create_stash_accessor("store_bond_commits_entry") |
| .expect("failed to create StashAccessor"); |
| let mut stash = exec.run_singlethreaded(Stash::new(accessor_proxy.clone())) |
| .expect("stash failed to initialize"); |
| |
| let bonding_data = BondingData { |
| identifier: "id-1".to_string(), |
| local_address: "00:00:00:00:00:01".to_string(), |
| name: None, |
| le: None, |
| }; |
| assert!(stash.store_bond(bonding_data).is_ok()); |
| |
| // Make sure that the in-memory cache has been updated. |
| assert_eq!(1, stash.bonding_data.len()); |
| assert_eq!( |
| &BondingData { |
| identifier: "id-1".to_string(), |
| local_address: "00:00:00:00:00:01".to_string(), |
| name: None, |
| le: None, |
| }, |
| stash |
| .bonding_data |
| .get("00:00:00:00:00:01") |
| .unwrap() |
| .get("id-1") |
| .unwrap() |
| ); |
| |
| // The new data should be accessible over FIDL. |
| assert_eq!(exec.run_singlethreaded(accessor_proxy.get_value("bonding-data:id-1")) |
| .expect("failed to get value") |
| .map(|x| *x), |
| Some(Value::Stringval( |
| "{\"identifier\":\"id-1\",\"localAddress\":\"00:00:00:00:00:01\",\"name\":null,\ |
| \"le\":null}" |
| .to_string() |
| ))); |
| } |
| |
| #[test] |
| fn list_bonds() { |
| let mut exec = fasync::Executor::new().expect("failed to create an executor"); |
| let accessor_proxy = create_stash_accessor("list_bonds") |
| .expect("failed to create StashAccessor"); |
| |
| // Insert values into stash that contain bonding data for several devices. |
| accessor_proxy.set_value("bonding-data:id-1", &mut Value::Stringval( |
| r#" |
| { |
| "identifier": "id-1", |
| "localAddress": "00:00:00:00:00:01", |
| "name": null, |
| "le": null |
| }"# |
| .to_string(), |
| )).expect("failed to set value"); |
| accessor_proxy.set_value("bonding-data:id-2", &mut Value::Stringval( |
| r#" |
| { |
| "identifier": "id-2", |
| "localAddress": "00:00:00:00:00:01", |
| "name": null, |
| "le": null |
| }"# |
| .to_string(), |
| )).expect("failed to set value"); |
| accessor_proxy.commit() |
| .expect("failed to commit bonding data values"); |
| |
| let stash = exec.run_singlethreaded(Stash::new(accessor_proxy)) |
| .expect("stash failed to initialize"); |
| |
| // Should return None for unknown address. |
| assert!(stash.list_bonds("00:00:00:00:00:00").is_none()); |
| |
| let mut iter = stash |
| .list_bonds("00:00:00:00:00:01") |
| .expect("expected to find address"); |
| let next_id = &iter.next().unwrap().identifier; |
| assert!("id-1" == next_id.as_str() || "id-2" == next_id.as_str()); |
| let next_id = &iter.next().unwrap().identifier; |
| assert!("id-1" == next_id.as_str() || "id-2" == next_id.as_str()); |
| assert_eq!(None, iter.next()); |
| } |
| } |