Keystore 2.0: VPN profile store legacy support

Test: Manually tested by storing profiles through the UI then upgrading
      to keystore 2 and verifying that they are still visible.
      Also checked that they were correctly imported into the sqlite
      database.
Change-Id: I0cbfb026db032290163469ee8824b5f59edc06fb
diff --git a/keystore2/vpnprofilestore/lib.rs b/keystore2/vpnprofilestore/lib.rs
index b631286..f92eacd 100644
--- a/keystore2/vpnprofilestore/lib.rs
+++ b/keystore2/vpnprofilestore/lib.rs
@@ -21,10 +21,14 @@
 use android_security_vpnprofilestore::binder::{Result as BinderResult, Status as BinderStatus};
 use anyhow::{Context, Result};
 use binder::{ExceptionCode, Strong, ThreadState};
+use keystore2::{async_task::AsyncTask, legacy_blob::LegacyBlobLoader};
 use rusqlite::{
     params, Connection, OptionalExtension, Transaction, TransactionBehavior, NO_PARAMS,
 };
-use std::path::{Path, PathBuf};
+use std::{
+    collections::HashSet,
+    path::{Path, PathBuf},
+};
 
 struct DB {
     conn: Connection,
@@ -196,20 +200,27 @@
     )
 }
 
-// TODO make sure that ALIASES have a prefix of VPN_ PLATFORM_VPN_ or
-// is equal to LOCKDOWN_VPN.
-
 /// Implements IVpnProfileStore AIDL interface.
 pub struct VpnProfileStore {
     db_path: PathBuf,
+    async_task: AsyncTask,
+}
+
+struct AsyncState {
+    recently_imported: HashSet<(u32, String)>,
+    legacy_loader: LegacyBlobLoader,
+    db_path: PathBuf,
 }
 
 impl VpnProfileStore {
     /// Creates a new VpnProfileStore instance.
-    pub fn new_native_binder(db_path: &Path) -> Strong<dyn IVpnProfileStore> {
+    pub fn new_native_binder(path: &Path) -> Strong<dyn IVpnProfileStore> {
         let mut db_path = path.to_path_buf();
         db_path.push("vpnprofilestore.sqlite");
-        BnVpnProfileStore::new_binder(Self { db_path })
+
+        let result = Self { db_path, async_task: Default::default() };
+        result.init_shelf(path);
+        BnVpnProfileStore::new_binder(result)
     }
 
     fn open_db(&self) -> Result<DB> {
@@ -219,21 +230,38 @@
     fn get(&self, alias: &str) -> Result<Vec<u8>> {
         let mut db = self.open_db().context("In get.")?;
         let calling_uid = ThreadState::get_calling_uid();
-        db.get(calling_uid, alias)
-            .context("In get: Trying to load profile from DB.")?
-            .ok_or_else(Error::not_found)
-            .context("In get: No such profile.")
+
+        if let Some(profile) =
+            db.get(calling_uid, alias).context("In get: Trying to load profile from DB.")?
+        {
+            return Ok(profile);
+        }
+        if self.get_legacy(calling_uid, alias).context("In get: Trying to migrate legacy blob.")? {
+            // If we were able to migrate a legacy blob try again.
+            if let Some(profile) =
+                db.get(calling_uid, alias).context("In get: Trying to load profile from DB.")?
+            {
+                return Ok(profile);
+            }
+        }
+        Err(Error::not_found()).context("In get: No such profile.")
     }
 
     fn put(&self, alias: &str, profile: &[u8]) -> Result<()> {
-        let mut db = self.open_db().context("In put.")?;
         let calling_uid = ThreadState::get_calling_uid();
+        // In order to make sure that we don't have stale legacy profiles, make sure they are
+        // migrated before replacing them.
+        let _ = self.get_legacy(calling_uid, alias);
+        let mut db = self.open_db().context("In put.")?;
         db.put(calling_uid, alias, profile).context("In put: Trying to insert profile into DB.")
     }
 
     fn remove(&self, alias: &str) -> Result<()> {
-        let mut db = self.open_db().context("In remove.")?;
         let calling_uid = ThreadState::get_calling_uid();
+        let mut db = self.open_db().context("In remove.")?;
+        // In order to make sure that we don't have stale legacy profiles, make sure they are
+        // migrated before removing them.
+        let _ = self.get_legacy(calling_uid, alias);
         let removed = db
             .remove(calling_uid, alias)
             .context("In remove: Trying to remove profile from DB.")?;
@@ -247,12 +275,84 @@
     fn list(&self, prefix: &str) -> Result<Vec<String>> {
         let mut db = self.open_db().context("In list.")?;
         let calling_uid = ThreadState::get_calling_uid();
-        Ok(db
-            .list(calling_uid)
-            .context("In list: Trying to get list of profiles.")?
-            .into_iter()
-            .filter(|s| s.starts_with(prefix))
-            .collect())
+        let mut result = self.list_legacy(calling_uid).context("In list.")?;
+        result
+            .append(&mut db.list(calling_uid).context("In list: Trying to get list of profiles.")?);
+        result = result.into_iter().filter(|s| s.starts_with(prefix)).collect();
+        result.sort_unstable();
+        result.dedup();
+        Ok(result)
+    }
+
+    fn init_shelf(&self, path: &Path) {
+        let mut db_path = path.to_path_buf();
+        self.async_task.queue_hi(move |shelf| {
+            let legacy_loader = LegacyBlobLoader::new(&db_path);
+            db_path.push("vpnprofilestore.sqlite");
+
+            shelf.put(AsyncState { legacy_loader, db_path, recently_imported: Default::default() });
+        })
+    }
+
+    fn do_serialized<F, T: Send + 'static>(&self, f: F) -> Result<T>
+    where
+        F: FnOnce(&mut AsyncState) -> Result<T> + Send + 'static,
+    {
+        let (sender, receiver) = std::sync::mpsc::channel::<Result<T>>();
+        self.async_task.queue_hi(move |shelf| {
+            let state = shelf.get_downcast_mut::<AsyncState>().expect("Failed to get shelf.");
+            sender.send(f(state)).expect("Failed to send result.");
+        });
+        receiver.recv().context("In do_serialized: Failed to receive result.")?
+    }
+
+    fn list_legacy(&self, uid: u32) -> Result<Vec<String>> {
+        self.do_serialized(move |state| {
+            state
+                .legacy_loader
+                .list_vpn_profiles(uid)
+                .context("Trying to list legacy vnp profiles.")
+        })
+        .context("In list_legacy.")
+    }
+
+    fn get_legacy(&self, uid: u32, alias: &str) -> Result<bool> {
+        let alias = alias.to_string();
+        self.do_serialized(move |state| {
+            if state.recently_imported.contains(&(uid, alias.clone())) {
+                return Ok(true);
+            }
+            let mut db = DB::new(&state.db_path).context("In open_db: Failed to open db.")?;
+            let migrated =
+                Self::migrate_one_legacy_profile(uid, &alias, &state.legacy_loader, &mut db)
+                    .context("Trying to migrate legacy vpn profile.")?;
+            if migrated {
+                state.recently_imported.insert((uid, alias));
+            }
+            Ok(migrated)
+        })
+        .context("In get_legacy.")
+    }
+
+    fn migrate_one_legacy_profile(
+        uid: u32,
+        alias: &str,
+        legacy_loader: &LegacyBlobLoader,
+        db: &mut DB,
+    ) -> Result<bool> {
+        let blob = legacy_loader
+            .read_vpn_profile(uid, alias)
+            .context("In migrate_one_legacy_profile: Trying to read legacy vpn profile.")?;
+        if let Some(profile) = blob {
+            db.put(uid, alias, &profile)
+                .context("In migrate_one_legacy_profile: Trying to insert profile into DB.")?;
+            legacy_loader
+                .remove_vpn_profile(uid, alias)
+                .context("In migrate_one_legacy_profile: Trying to delete legacy profile.")?;
+            Ok(true)
+        } else {
+            Ok(false)
+        }
     }
 }