From b0c7a662a20ee59b9215b27eec9741423e9d42bc Mon Sep 17 00:00:00 2001
From: Bryce Lee <brycelee@google.com>
Date: Wed, 24 Apr 2019 09:34:13 -0700
Subject: [PATCH] [SetUI] Add Persistent storage to SetUI service.

This changelist introduces persistent storage for retaining settings
accessed through the SetUI service. The functionality is exposed
through two main traits. The Store trait defines an API for writing
and retrieving values, while the SettingCodec trait defines an
encoding/decoding protocol. Concrete implementations for both
have been provided, using JSON for the latter.

Note that the codecs must be updated to support new SettingData
types. The JSON codec currently only supports StringValue and
AccountSettings.

Bug: SU-167
Test: fx run-test setui_service_tests -- --test
Change-Id: Iae55c256bdeaeec957a62836abe342793320d2c4
---
 garnet/bin/setui/BUILD.gn               |   3 +
 garnet/bin/setui/meta/setui_service.cmx |   3 +
 garnet/bin/setui/src/common.rs          |  17 ++
 garnet/bin/setui/src/default_store.rs   |  46 +++++
 garnet/bin/setui/src/json_codec.rs      | 214 ++++++++++++++++++++++++
 garnet/bin/setui/src/main.rs            |   5 +
 garnet/bin/setui/src/setting_adapter.rs |  23 ++-
 garnet/bin/setui/src/setui_handler.rs   |  20 +++
 8 files changed, 330 insertions(+), 1 deletion(-)
 create mode 100644 garnet/bin/setui/src/default_store.rs
 create mode 100644 garnet/bin/setui/src/json_codec.rs

diff --git a/garnet/bin/setui/BUILD.gn b/garnet/bin/setui/BUILD.gn
index bada97b84cf..1b7c3782fe5 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 98a36e88ef4..d8580f06a6e 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 e6c60f9967d..8cb30715c7d 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 00000000000..25ae01b585e
--- /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 00000000000..78977ff2ee1
--- /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 ce0163714fe..da32da390ab 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 a039d522215..aac18fab8e7 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 506f276eb5f..e8c3c595e35 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,
         )));
-- 
GitLab