diff --git a/garnet/bin/setui/BUILD.gn b/garnet/bin/setui/BUILD.gn index bada97b84cfadb8093dfa94f3478e4e8ed31327d..1b7c3782fe5c5d3a2086f987b524ebc0c2b2110b 100644 --- a/garnet/bin/setui/BUILD.gn +++ b/garnet/bin/setui/BUILD.gn @@ -33,6 +33,9 @@ rustc_binary("bin") { "//third_party/rust_crates:futures-preview", "//third_party/rust_crates:log", "//third_party/rust_crates:parking_lot", + "//third_party/rust_crates:serde", + "//third_party/rust_crates:serde_derive", + "//third_party/rust_crates:serde_json", ] } diff --git a/garnet/bin/setui/meta/setui_service.cmx b/garnet/bin/setui/meta/setui_service.cmx index 98a36e88ef4d7072b3fba7541429b4f05ce3f7fd..d8580f06a6ef5cf5d67ae61ff611975d77f076fa 100644 --- a/garnet/bin/setui/meta/setui_service.cmx +++ b/garnet/bin/setui/meta/setui_service.cmx @@ -3,6 +3,9 @@ "data": "data/setui_service" }, "sandbox": { + "features": [ + "isolated-persistent-storage" + ], "services": [ "fuchsia.logger.LogSink", "fuchsia.net.Connectivity", diff --git a/garnet/bin/setui/src/common.rs b/garnet/bin/setui/src/common.rs index e6c60f9967dbb71763e2fb4c6d19fdfa7df8aad1..8cb30715c7d73674c95e409d1f0cdf87f5ebd36f 100644 --- a/garnet/bin/setui/src/common.rs +++ b/garnet/bin/setui/src/common.rs @@ -7,6 +7,8 @@ use fidl_fuchsia_setui::*; use std::sync::mpsc::Sender; pub type ProcessMutation = dyn Fn(&Mutation) -> Result<Option<SettingData>, Error> + Send + Sync; +pub type BoxedSettingCodec<T> = Box<dyn SettingCodec<T> + Send + Sync>; +pub type BoxedStore = Box<dyn Store + Send + Sync>; /// Trait defining interface setting service uses to relay operations. Each /// adapter specifies the setting type it handles, which it will accept @@ -23,3 +25,18 @@ pub trait Adapter { /// Otherwise, the sender is stored for later invocation. fn listen(&self, sender: Sender<SettingData>, last_seen_data: Option<&SettingData>); } + +/// Trait for encoding and decoding Settings. +pub trait SettingCodec<T: ToString> { + fn encode(&self, data: SettingData) -> Result<T, Error>; + + fn decode(&self, encoded: T) -> Result<SettingData, Error>; +} + +pub trait Store { + /// Writes value to presistent storage. + fn write(&self, data: SettingData) -> Result<(), Error>; + + /// Reads value from persistent storage + fn read(&self) -> Result<Option<SettingData>, Error>; +} diff --git a/garnet/bin/setui/src/default_store.rs b/garnet/bin/setui/src/default_store.rs new file mode 100644 index 0000000000000000000000000000000000000000..25ae01b585e7356cd2a892cff66ef491ed3ed139 --- /dev/null +++ b/garnet/bin/setui/src/default_store.rs @@ -0,0 +1,46 @@ +// Copyright 2019 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 crate::common::*; +use failure::Error; +use fidl_fuchsia_setui::*; +use serde_json::Value; +use std::fs::File; +use std::io::BufReader; +use std::io::Write; +use std::path::Path; + +pub struct DefaultStore { + file_path: String, + codec: BoxedSettingCodec<Value>, +} + +impl DefaultStore { + pub fn new(file_path: String, codec: BoxedSettingCodec<Value>) -> DefaultStore { + return DefaultStore { file_path: file_path, codec: codec }; + } +} + +impl Store for DefaultStore { + /// Writes value to presistent storage. + fn write(&self, data: SettingData) -> Result<(), Error> { + let encoded_data = self.codec.encode(data)?; + let mut file = File::create(self.file_path.clone())?; + file.write_all(encoded_data.to_string().as_bytes())?; + + return Ok(()); + } + + /// Reads value from persistent storage + fn read(&self) -> Result<Option<SettingData>, Error> { + if !Path::new(&self.file_path).exists() { + return Ok(None); + } + + let reader = BufReader::new(File::open(self.file_path.clone())?); + let json = serde_json::from_reader(reader)?; + + return Ok(Some(self.codec.decode(json)?)); + } +} diff --git a/garnet/bin/setui/src/json_codec.rs b/garnet/bin/setui/src/json_codec.rs new file mode 100644 index 0000000000000000000000000000000000000000..78977ff2ee150985efcda5a7a55cc8fa0044a25f --- /dev/null +++ b/garnet/bin/setui/src/json_codec.rs @@ -0,0 +1,214 @@ +// Copyright 2019 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 crate::common::SettingCodec; +use failure::{format_err, Error}; +use fidl_fuchsia_setui::*; +use serde_json::json; +use serde_json::Value; + +pub struct JsonCodec {} + +impl JsonCodec { + pub fn new() -> JsonCodec { + return JsonCodec {}; + } +} + +const KEY_TYPE: &str = "type"; +const KEY_DATA: &str = "data"; + +impl SettingCodec<Value> for JsonCodec { + fn encode(&self, data: SettingData) -> Result<Value, Error> { + if let Some(setting_type) = get_type(&data) { + let mut data_value; + match setting_type { + TYPE_STRING_VALUE => { + data_value = Some(encode_string_value(&data)?); + } + TYPE_ACCOUNT_VALUE => { + data_value = Some(encode_account_settings(&data)?); + } + _ => { + return Err(format_err!("type encoding not available")); + } + } + + return Ok(json!({KEY_TYPE: setting_type, KEY_DATA: data_value.unwrap()})); + } else { + return Err(format_err!("unhandled data type")); + } + } + + fn decode(&self, encoded: Value) -> Result<SettingData, Error> { + if let Value::Object(mapping) = encoded { + // Get value mapped to the type key + if let (Some(type_value), Some(data_value)) = + (mapping.get(KEY_TYPE), mapping.get(KEY_DATA)) + { + // Retrieve setting type from value. + if let Some(setting_type) = type_value.as_u64() { + match setting_type { + TYPE_STRING_VALUE => return decode_string_value(data_value), + TYPE_ACCOUNT_VALUE => return decode_account(data_value), + _ => return Err(format_err!("unsupported encoded type")), + } + } else { + return Err(format_err!("type is not a number")); + } + } else { + return Err(format_err!("type or data not present")); + } + } else { + return Err(format_err!("root node should be an object")); + } + } +} + +const TYPE_STRING_VALUE: u64 = 1; +const TYPE_ACCOUNT_VALUE: u64 = 2; + +fn get_type(setting_data: &SettingData) -> Option<u64> { + match setting_data { + SettingData::StringValue(_val) => Some(TYPE_STRING_VALUE), + SettingData::Account(_val) => Some(TYPE_ACCOUNT_VALUE), + _ => None, + } +} + +fn encode_string_value(data: &SettingData) -> Result<Value, Error> { + if let SettingData::StringValue(string_val) = data { + return Ok(json!(string_val)); + } else { + return Err(format_err!("not a string value")); + } +} + +fn decode_string_value(encoded: &Value) -> Result<SettingData, Error> { + if let Value::String(string_val) = encoded { + return Ok(SettingData::StringValue(string_val.clone())); + } + + return Err(format_err!("not a string type")); +} + +const ACCOUNT_SETTINGS_MODE_KEY: &str = "mode"; + +fn encode_account_settings(data: &SettingData) -> Result<Value, Error> { + if let SettingData::Account(account_settings) = data { + if account_settings.mode == None { + return Ok(json!({})); + } + + let encoded_mode = encode_login_mode(account_settings.mode.unwrap())?; + return Ok(json!({ ACCOUNT_SETTINGS_MODE_KEY: encoded_mode })); + } else { + return Err(format_err!("not an account setting val")); + } +} + +fn decode_account(encoded: &Value) -> Result<SettingData, Error> { + if let Value::Object(mapping) = encoded { + match mapping.get(ACCOUNT_SETTINGS_MODE_KEY) { + Some(val) => { + Ok(SettingData::Account(AccountSettings { mode: Some(decode_login_mode(val)?) })) + } + _ => Ok(SettingData::Account(AccountSettings { mode: None })), + } + } else { + return Err(format_err!("malformed account json")); + } +} + +const LOGIN_MODE_NONE: u64 = 0; +const LOGIN_MODE_GUEST_OVERRIDE: u64 = 1; + +fn encode_login_mode(mode: LoginOverride) -> Result<Value, Error> { + match mode { + LoginOverride::None => { + return Ok(json!(LOGIN_MODE_NONE)); + } + LoginOverride::AutologinGuest => { + return Ok(json!(LOGIN_MODE_GUEST_OVERRIDE)); + } + } +} + +fn decode_login_mode(value: &Value) -> Result<LoginOverride, Error> { + if let Value::Number(number) = value { + if let Some(val) = number.as_u64() { + match val { + LOGIN_MODE_NONE => { + return Ok(LoginOverride::None); + } + LOGIN_MODE_GUEST_OVERRIDE => { + return Ok(LoginOverride::AutologinGuest); + } + _ => { + return Err(format_err!("not a decodable type")); + } + } + } else { + return Err(format_err!("incorrect number format")); + } + } else { + return Err(format_err!("not a number")); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A basic test to exercise that basic functionality works. In this case, we + /// mutate the unknown type, reserved for testing. We should always immediately + /// receive back an Ok response. + #[test] + fn test_encode_string() { + let test_val = "foo bar"; + let codec = JsonCodec::new(); + let encode_result = codec.encode(SettingData::StringValue(test_val.to_string())); + assert!(encode_result.is_ok()); + + let encoded_value = encode_result.unwrap(); + let decode_result = codec.decode(encoded_value); + assert!(decode_result.is_ok()); + + match decode_result.unwrap() { + SettingData::StringValue(val) => { + assert_eq!(val, test_val); + } + _ => { + panic!("wrong type!"); + } + } + } + + #[test] + fn test_encode_account() { + let codec = JsonCodec::new(); + let test_override = LoginOverride::AutologinGuest; + + let encode_result = + codec.encode(SettingData::Account(AccountSettings { mode: Some(test_override) })); + assert!(encode_result.is_ok()); + + let encoded_value = encode_result.unwrap(); + let decode_result = codec.decode(encoded_value); + assert!(decode_result.is_ok()); + + match decode_result.unwrap() { + SettingData::Account(account_settings) => { + if let Some(login_override) = account_settings.mode { + assert_eq!(test_override, login_override); + } else { + panic!("override should have been set"); + } + } + _ => { + panic!("wrong setting data type"); + } + } + } +} diff --git a/garnet/bin/setui/src/main.rs b/garnet/bin/setui/src/main.rs index ce0163714fe672a56c595c5b6f0ae6568001efae..da32da390ab49f7ace9e3ff44ae63dfe7de239da 100644 --- a/garnet/bin/setui/src/main.rs +++ b/garnet/bin/setui/src/main.rs @@ -4,6 +4,8 @@ #![feature(async_await, await_macro, futures_api)] use { + crate::default_store::DefaultStore, + crate::json_codec::JsonCodec, crate::mutation::*, crate::setting_adapter::SettingAdapter, failure::Error, @@ -18,7 +20,9 @@ use { }; mod common; +mod default_store; mod fidl_clone; +mod json_codec; mod mutation; mod setting_adapter; mod setui_handler; @@ -35,6 +39,7 @@ fn main() -> Result<(), Error> { // TODO(SU-210): Remove once other adapters are ready. handler.register_adapter(Box::new(SettingAdapter::new( SettingType::Unknown, + Box::new(DefaultStore::new("/data/unknown.dat".to_string(), Box::new(JsonCodec::new()))), Box::new(process_string_mutation), None, ))); diff --git a/garnet/bin/setui/src/setting_adapter.rs b/garnet/bin/setui/src/setting_adapter.rs index a039d522215144cc3ab361d39b89bff1d0691243..aac18fab8e7cb355a20466d266bd1a7def4ae837 100644 --- a/garnet/bin/setui/src/setting_adapter.rs +++ b/garnet/bin/setui/src/setting_adapter.rs @@ -5,6 +5,7 @@ use crate::common::*; use crate::fidl_clone::*; use fidl_fuchsia_setui::*; +use fuchsia_syslog::fx_log_err; use std::sync::mpsc::Sender; use std::sync::Mutex; @@ -17,20 +18,34 @@ pub struct SettingAdapter { latest_val: Option<SettingData>, senders: Mutex<Vec<Sender<SettingData>>>, mutation_process: Box<ProcessMutation>, + store: Mutex<BoxedStore>, } impl SettingAdapter { pub fn new( setting_type: SettingType, + store: BoxedStore, mutation_processor: Box<ProcessMutation>, default_value: Option<SettingData>, ) -> SettingAdapter { - return SettingAdapter { + let mut adapter = SettingAdapter { setting_type: setting_type, latest_val: default_value, senders: Mutex::new(vec![]), mutation_process: mutation_processor, + store: Mutex::new(store), }; + adapter.initialize(); + + return adapter; + } + + fn initialize(&mut self) { + if let Ok(store) = self.store.lock() { + if let Ok(Some(value)) = store.read() { + self.latest_val = Some(value); + } + } } } @@ -46,6 +61,12 @@ impl Adapter for SettingAdapter { Ok(Some(setting)) => { self.latest_val = Some(setting.clone()); + if let Ok(store) = self.store.lock() { + if store.write(setting.clone()).is_err() { + fx_log_err!("failed to write setting to file"); + } + } + if let Ok(mut senders) = self.senders.lock() { while !senders.is_empty() { let sender_option = senders.pop(); diff --git a/garnet/bin/setui/src/setui_handler.rs b/garnet/bin/setui/src/setui_handler.rs index 506f276eb5f506d82b88ee7d3b8db02392d9c83f..e8c3c595e3524a2a713b2140fd6eb486fce5987d 100644 --- a/garnet/bin/setui/src/setui_handler.rs +++ b/garnet/bin/setui/src/setui_handler.rs @@ -115,9 +115,28 @@ impl SetUIHandler { #[cfg(test)] mod tests { use super::*; + use crate::common::Store; use crate::mutation::*; use crate::setting_adapter::SettingAdapter; + struct TestStore {} + + impl TestStore { + fn new() -> TestStore { + TestStore {} + } + } + + impl Store for TestStore { + fn write(&self, _data: SettingData) -> Result<(), Error> { + Ok(()) + } + + fn read(&self) -> Result<Option<SettingData>, Error> { + Ok(None) + } + } + /// A basic test to exercise that basic functionality works. In this case, we /// mutate the unknown type, reserved for testing. We should always immediately /// receive back an Ok response. @@ -145,6 +164,7 @@ mod tests { let handler = SetUIHandler::new(); handler.register_adapter(Box::new(SettingAdapter::new( SettingType::Unknown, + Box::new(TestStore::new()), Box::new(process_string_mutation), None, )));