diff options
Diffstat (limited to 'toolkit/components/xulstore/src/statics.rs')
-rw-r--r-- | toolkit/components/xulstore/src/statics.rs | 255 |
1 files changed, 255 insertions, 0 deletions
diff --git a/toolkit/components/xulstore/src/statics.rs b/toolkit/components/xulstore/src/statics.rs new file mode 100644 index 0000000000..4b988b6062 --- /dev/null +++ b/toolkit/components/xulstore/src/statics.rs @@ -0,0 +1,255 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::{ + error::{XULStoreError, XULStoreResult}, + ffi::ProfileChangeObserver, + make_key, SEPARATOR, +}; +use moz_task::is_main_thread; +use nsstring::nsString; +use once_cell::sync::Lazy; +use rkv::backend::{SafeMode, SafeModeDatabase, SafeModeEnvironment}; +use rkv::{StoreOptions, Value}; +use std::{ + collections::BTreeMap, + fs::{create_dir_all, remove_file, File}, + path::PathBuf, + str, + sync::{Arc, Mutex, RwLock}, +}; +use xpcom::{ + interfaces::{nsIFile, nsIObserverService, nsIProperties, nsIXULRuntime}, + RefPtr, XpCom, +}; + +type Manager = rkv::Manager<SafeModeEnvironment>; +type Rkv = rkv::Rkv<SafeModeEnvironment>; +type SingleStore = rkv::SingleStore<SafeModeDatabase>; +type XULStoreCache = BTreeMap<String, BTreeMap<String, BTreeMap<String, String>>>; + +pub struct Database { + pub rkv: Arc<RwLock<Rkv>>, + pub store: SingleStore, +} + +impl Database { + fn new(rkv: Arc<RwLock<Rkv>>, store: SingleStore) -> Database { + Database { rkv, store } + } +} + +static PROFILE_DIR: Lazy<Mutex<Option<PathBuf>>> = Lazy::new(|| { + observe_profile_change(); + Mutex::new(get_profile_dir().ok()) +}); + +pub(crate) static DATA_CACHE: Lazy<Mutex<Option<XULStoreCache>>> = + Lazy::new(|| Mutex::new(cache_data().ok())); + +pub(crate) fn get_database() -> XULStoreResult<Database> { + let mut manager = Manager::singleton().write()?; + let xulstore_dir = get_xulstore_dir()?; + let xulstore_path = xulstore_dir.as_path(); + let rkv = manager.get_or_create(xulstore_path, Rkv::new::<SafeMode>)?; + let store = rkv.read()?.open_single("db", StoreOptions::create())?; + Ok(Database::new(rkv, store)) +} + +pub(crate) fn update_profile_dir() { + // Failure to update the dir isn't fatal (although it means that we won't + // persist XULStore data for this session), so we don't return a result. + // But we use a closure returning a result to enable use of the ? operator. + (|| -> XULStoreResult<()> { + { + let mut profile_dir_guard = PROFILE_DIR.lock()?; + *profile_dir_guard = get_profile_dir().ok(); + } + + let mut cache_guard = DATA_CACHE.lock()?; + *cache_guard = cache_data().ok(); + + Ok(()) + })() + .unwrap_or_else(|err| error!("error updating profile dir: {}", err)); +} + +fn get_profile_dir() -> XULStoreResult<PathBuf> { + // We can't use getter_addrefs() here because get_DirectoryService() + // returns its nsIProperties interface, and its Get() method returns + // a directory via its nsQIResult out param, which gets translated to + // a `*mut *mut libc::c_void` in Rust, whereas getter_addrefs() expects + // a closure with a `*mut *const T` parameter. + + let dir_svc: RefPtr<nsIProperties> = + xpcom::components::Directory::service().map_err(|_| XULStoreError::Unavailable)?; + let mut profile_dir = xpcom::GetterAddrefs::<nsIFile>::new(); + unsafe { + dir_svc + .Get( + cstr!("ProfD").as_ptr(), + &nsIFile::IID, + profile_dir.void_ptr(), + ) + .to_result() + .or_else(|_| { + dir_svc + .Get( + cstr!("ProfDS").as_ptr(), + &nsIFile::IID, + profile_dir.void_ptr(), + ) + .to_result() + })?; + } + let profile_dir = profile_dir.refptr().ok_or(XULStoreError::Unavailable)?; + + let mut profile_path = nsString::new(); + unsafe { + profile_dir.GetPath(&mut *profile_path).to_result()?; + } + + let path = String::from_utf16(&profile_path[..])?; + Ok(PathBuf::from(&path)) +} + +fn get_xulstore_dir() -> XULStoreResult<PathBuf> { + let mut xulstore_dir = PROFILE_DIR + .lock()? + .clone() + .ok_or(XULStoreError::Unavailable)?; + xulstore_dir.push("xulstore"); + + create_dir_all(xulstore_dir.clone())?; + + Ok(xulstore_dir) +} + +fn observe_profile_change() { + assert!(is_main_thread()); + + // Failure to observe the change isn't fatal (although it means we won't + // persist XULStore data for this session), so we don't return a result. + // But we use a closure returning a result to enable use of the ? operator. + (|| -> XULStoreResult<()> { + // Observe profile changes so we can update this directory accordingly. + let obs_svc: RefPtr<nsIObserverService> = + xpcom::components::Observer::service().map_err(|_| XULStoreError::Unavailable)?; + let observer = ProfileChangeObserver::new(); + unsafe { + obs_svc + .AddObserver( + observer.coerce(), + cstr!("profile-after-change").as_ptr(), + false, + ) + .to_result()? + }; + Ok(()) + })() + .unwrap_or_else(|err| error!("error observing profile change: {}", err)); +} + +fn in_safe_mode() -> XULStoreResult<bool> { + let xul_runtime: RefPtr<nsIXULRuntime> = + xpcom::components::XULRuntime::service().map_err(|_| XULStoreError::Unavailable)?; + let mut in_safe_mode = false; + unsafe { + xul_runtime.GetInSafeMode(&mut in_safe_mode).to_result()?; + } + Ok(in_safe_mode) +} + +fn cache_data() -> XULStoreResult<XULStoreCache> { + let db = get_database()?; + maybe_migrate_data(&db, db.store); + + let mut all = XULStoreCache::default(); + if in_safe_mode()? { + return Ok(all); + } + + let env = db.rkv.read()?; + let reader = env.read()?; + let iterator = db.store.iter_start(&reader)?; + + for result in iterator { + let (key, value): (&str, String) = match result { + Ok((key, value)) => match (str::from_utf8(&key), unwrap_value(&value)) { + (Ok(key), Ok(value)) => (key, value), + (Err(err), _) => return Err(err.into()), + (_, Err(err)) => return Err(err), + }, + Err(err) => return Err(err.into()), + }; + + let parts = key.split(SEPARATOR).collect::<Vec<&str>>(); + if parts.len() != 3 { + return Err(XULStoreError::UnexpectedKey(key.to_string())); + } + let (doc, id, attr) = ( + parts[0].to_string(), + parts[1].to_string(), + parts[2].to_string(), + ); + + all.entry(doc) + .or_default() + .entry(id) + .or_default() + .entry(attr) + .or_insert(value); + } + + Ok(all) +} + +fn maybe_migrate_data(db: &Database, store: SingleStore) { + // Failure to migrate data isn't fatal, so we don't return a result. + // But we use a closure returning a result to enable use of the ? operator. + (|| -> XULStoreResult<()> { + let mut old_datastore = PROFILE_DIR + .lock()? + .clone() + .ok_or(XULStoreError::Unavailable)?; + old_datastore.push("xulstore.json"); + if !old_datastore.exists() { + debug!("old datastore doesn't exist: {:?}", old_datastore); + return Ok(()); + } + + let file = File::open(old_datastore.clone())?; + let json: XULStoreCache = serde_json::from_reader(file)?; + + let env = db.rkv.read()?; + let mut writer = env.write()?; + + for (doc, ids) in json { + for (id, attrs) in ids { + for (attr, value) in attrs { + let key = make_key(&doc, &id, &attr); + store.put(&mut writer, &key, &Value::Str(&value))?; + } + } + } + + writer.commit()?; + + remove_file(old_datastore)?; + + Ok(()) + })() + .unwrap_or_else(|err| error!("error migrating data: {}", err)); +} + +fn unwrap_value(value: &Value) -> XULStoreResult<String> { + match value { + Value::Str(val) => Ok(val.to_string()), + + // This should never happen, but it could happen in theory + // if someone writes a different kind of value into the store + // using a more general API (kvstore, rkv, LMDB). + _ => Err(XULStoreError::UnexpectedValue), + } +} |