diff options
Diffstat (limited to 'toolkit/components/xulstore/src')
-rw-r--r-- | toolkit/components/xulstore/src/error.rs | 81 | ||||
-rw-r--r-- | toolkit/components/xulstore/src/ffi.rs | 325 | ||||
-rw-r--r-- | toolkit/components/xulstore/src/iter.rs | 24 | ||||
-rw-r--r-- | toolkit/components/xulstore/src/lib.rs | 223 | ||||
-rw-r--r-- | toolkit/components/xulstore/src/persist.rs | 179 | ||||
-rw-r--r-- | toolkit/components/xulstore/src/statics.rs | 255 |
6 files changed, 1087 insertions, 0 deletions
diff --git a/toolkit/components/xulstore/src/error.rs b/toolkit/components/xulstore/src/error.rs new file mode 100644 index 0000000000..4bc8902389 --- /dev/null +++ b/toolkit/components/xulstore/src/error.rs @@ -0,0 +1,81 @@ +/* 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 nserror::{ + nsresult, NS_ERROR_FAILURE, NS_ERROR_ILLEGAL_VALUE, NS_ERROR_NOT_AVAILABLE, NS_ERROR_UNEXPECTED, +}; +use rkv::{MigrateError as RkvMigrateError, StoreError as RkvStoreError}; +use serde_json::Error as SerdeJsonError; +use std::{io::Error as IoError, str::Utf8Error, string::FromUtf16Error, sync::PoisonError}; +use thiserror::Error; + +pub(crate) type XULStoreResult<T> = Result<T, XULStoreError>; + +#[derive(Debug, Error)] +pub(crate) enum XULStoreError { + #[error("error converting bytes: {0:?}")] + ConvertBytes(#[from] Utf8Error), + + #[error("error converting string: {0:?}")] + ConvertString(#[from] FromUtf16Error), + + #[error("I/O error: {0:?}")] + IoError(#[from] IoError), + + #[error("iteration is finished")] + IterationFinished, + + #[error("JSON error: {0}")] + JsonError(#[from] SerdeJsonError), + + #[error("error result {0}")] + NsResult(#[from] nsresult), + + #[error("poison error getting read/write lock")] + PoisonError, + + #[error("migrate error: {0:?}")] + RkvMigrateError(#[from] RkvMigrateError), + + #[error("store error: {0:?}")] + RkvStoreError(#[from] RkvStoreError), + + #[error("id or attribute name too long")] + IdAttrNameTooLong, + + #[error("unavailable")] + Unavailable, + + #[error("unexpected key: {0:?}")] + UnexpectedKey(String), + + #[error("unexpected value")] + UnexpectedValue, +} + +impl From<XULStoreError> for nsresult { + fn from(err: XULStoreError) -> nsresult { + match err { + XULStoreError::ConvertBytes(_) => NS_ERROR_FAILURE, + XULStoreError::ConvertString(_) => NS_ERROR_FAILURE, + XULStoreError::IoError(_) => NS_ERROR_FAILURE, + XULStoreError::IterationFinished => NS_ERROR_FAILURE, + XULStoreError::JsonError(_) => NS_ERROR_FAILURE, + XULStoreError::NsResult(result) => result, + XULStoreError::PoisonError => NS_ERROR_UNEXPECTED, + XULStoreError::RkvMigrateError(_) => NS_ERROR_FAILURE, + XULStoreError::RkvStoreError(_) => NS_ERROR_FAILURE, + XULStoreError::IdAttrNameTooLong => NS_ERROR_ILLEGAL_VALUE, + XULStoreError::Unavailable => NS_ERROR_NOT_AVAILABLE, + XULStoreError::UnexpectedKey(_) => NS_ERROR_UNEXPECTED, + XULStoreError::UnexpectedValue => NS_ERROR_UNEXPECTED, + } + } +} + +impl<T> From<PoisonError<T>> for XULStoreError { + fn from(_: PoisonError<T>) -> XULStoreError { + XULStoreError::PoisonError + } +} diff --git a/toolkit/components/xulstore/src/ffi.rs b/toolkit/components/xulstore/src/ffi.rs new file mode 100644 index 0000000000..4d0027a175 --- /dev/null +++ b/toolkit/components/xulstore/src/ffi.rs @@ -0,0 +1,325 @@ +/* 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 as XULStore; +use crate::{iter::XULStoreIterator, statics::update_profile_dir}; +use libc::{c_char, c_void}; +use nserror::{nsresult, NS_ERROR_NOT_IMPLEMENTED, NS_OK}; +use nsstring::{nsAString, nsString}; +use std::cell::RefCell; +use std::ptr; +use xpcom::{ + interfaces::{nsIJSEnumerator, nsIStringEnumerator, nsISupports, nsIXULStore}, + RefPtr, +}; + +#[no_mangle] +pub unsafe extern "C" fn xulstore_new_service(result: *mut *const nsIXULStore) { + let xul_store_service = XULStoreService::new(); + RefPtr::new(xul_store_service.coerce::<nsIXULStore>()).forget(&mut *result); +} + +#[xpcom(implement(nsIXULStore), atomic)] +pub struct XULStoreService {} + +impl XULStoreService { + fn new() -> RefPtr<XULStoreService> { + XULStoreService::allocate(InitXULStoreService {}) + } + + #[allow(non_snake_case)] + fn Persist(&self, _node: *const c_void, _attr: *const nsAString) -> nsresult { + NS_ERROR_NOT_IMPLEMENTED + } + + xpcom_method!( + set_value => SetValue( + doc: *const nsAString, + id: *const nsAString, + attr: *const nsAString, + value: *const nsAString + ) + ); + + fn set_value( + &self, + doc: &nsAString, + id: &nsAString, + attr: &nsAString, + value: &nsAString, + ) -> Result<(), nsresult> { + XULStore::set_value(doc, id, attr, value).map_err(|err| err.into()) + } + + xpcom_method!( + has_value => HasValue( + doc: *const nsAString, + id: *const nsAString, + attr: *const nsAString + ) -> bool + ); + + fn has_value( + &self, + doc: &nsAString, + id: &nsAString, + attr: &nsAString, + ) -> Result<bool, nsresult> { + XULStore::has_value(doc, id, attr).map_err(|err| err.into()) + } + + xpcom_method!( + get_value => GetValue( + doc: *const nsAString, + id: *const nsAString, + attr: *const nsAString + ) -> nsAString + ); + + fn get_value( + &self, + doc: &nsAString, + id: &nsAString, + attr: &nsAString, + ) -> Result<nsString, nsresult> { + match XULStore::get_value(doc, id, attr) { + Ok(val) => Ok(nsString::from(&val)), + Err(err) => Err(err.into()), + } + } + + xpcom_method!( + remove_value => RemoveValue( + doc: *const nsAString, + id: *const nsAString, + attr: *const nsAString + ) + ); + + fn remove_value( + &self, + doc: &nsAString, + id: &nsAString, + attr: &nsAString, + ) -> Result<(), nsresult> { + XULStore::remove_value(doc, id, attr).map_err(|err| err.into()) + } + + xpcom_method!( + remove_document => RemoveDocument(doc: *const nsAString) + ); + + fn remove_document(&self, doc: &nsAString) -> Result<(), nsresult> { + XULStore::remove_document(doc).map_err(|err| err.into()) + } + + xpcom_method!( + get_ids_enumerator => GetIDsEnumerator( + doc: *const nsAString + ) -> * const nsIStringEnumerator + ); + + fn get_ids_enumerator(&self, doc: &nsAString) -> Result<RefPtr<nsIStringEnumerator>, nsresult> { + match XULStore::get_ids(doc) { + Ok(val) => { + let enumerator = StringEnumerator::new(val); + Ok(RefPtr::new(enumerator.coerce::<nsIStringEnumerator>())) + } + Err(err) => Err(err.into()), + } + } + + xpcom_method!( + get_attribute_enumerator => GetAttributeEnumerator( + doc: *const nsAString, + id: *const nsAString + ) -> * const nsIStringEnumerator + ); + + fn get_attribute_enumerator( + &self, + doc: &nsAString, + id: &nsAString, + ) -> Result<RefPtr<nsIStringEnumerator>, nsresult> { + match XULStore::get_attrs(doc, id) { + Ok(val) => { + let enumerator = StringEnumerator::new(val); + Ok(RefPtr::new(enumerator.coerce::<nsIStringEnumerator>())) + } + Err(err) => Err(err.into()), + } + } +} + +#[xpcom(implement(nsIStringEnumerator), nonatomic)] +pub(crate) struct StringEnumerator { + iter: RefCell<XULStoreIterator>, +} +impl StringEnumerator { + pub(crate) fn new(iter: XULStoreIterator) -> RefPtr<StringEnumerator> { + StringEnumerator::allocate(InitStringEnumerator { + iter: RefCell::new(iter), + }) + } + + xpcom_method!(string_iterator => StringIterator() -> *const nsIJSEnumerator); + + fn string_iterator(&self) -> Result<RefPtr<nsIJSEnumerator>, nsresult> { + Err(NS_ERROR_NOT_IMPLEMENTED) + } + + xpcom_method!(has_more => HasMore() -> bool); + + fn has_more(&self) -> Result<bool, nsresult> { + let iter = self.iter.borrow(); + Ok(iter.has_more()) + } + + xpcom_method!(get_next => GetNext() -> nsAString); + + fn get_next(&self) -> Result<nsString, nsresult> { + let mut iter = self.iter.borrow_mut(); + match iter.get_next() { + Ok(value) => Ok(nsString::from(&value)), + Err(err) => Err(err.into()), + } + } +} + +#[xpcom(implement(nsIObserver), nonatomic)] +pub(crate) struct ProfileChangeObserver {} +impl ProfileChangeObserver { + #[allow(non_snake_case)] + unsafe fn Observe( + &self, + _subject: *const nsISupports, + _topic: *const c_char, + _data: *const u16, + ) -> nsresult { + update_profile_dir(); + NS_OK + } + + pub(crate) fn new() -> RefPtr<ProfileChangeObserver> { + ProfileChangeObserver::allocate(InitProfileChangeObserver {}) + } +} + +#[no_mangle] +pub unsafe extern "C" fn xulstore_set_value( + doc: &nsAString, + id: &nsAString, + attr: &nsAString, + value: &nsAString, +) -> nsresult { + XULStore::set_value(doc, id, attr, value).into() +} + +#[no_mangle] +pub unsafe extern "C" fn xulstore_has_value( + doc: &nsAString, + id: &nsAString, + attr: &nsAString, + has_value: *mut bool, +) -> nsresult { + match XULStore::has_value(doc, id, attr) { + Ok(val) => { + *has_value = val; + NS_OK + } + Err(err) => err.into(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn xulstore_get_value( + doc: &nsAString, + id: &nsAString, + attr: &nsAString, + value: *mut nsAString, +) -> nsresult { + match XULStore::get_value(doc, id, attr) { + Ok(val) => { + (*value).assign(&nsString::from(&val)); + NS_OK + } + Err(err) => err.into(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn xulstore_remove_value( + doc: &nsAString, + id: &nsAString, + attr: &nsAString, +) -> nsresult { + XULStore::remove_value(doc, id, attr).into() +} + +#[no_mangle] +pub unsafe extern "C" fn xulstore_get_ids( + doc: &nsAString, + result: *mut nsresult, +) -> *mut XULStoreIterator { + match XULStore::get_ids(doc) { + Ok(iter) => { + *result = NS_OK; + Box::into_raw(Box::new(iter)) + } + Err(err) => { + *result = err.into(); + ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn xulstore_get_attrs( + doc: &nsAString, + id: &nsAString, + result: *mut nsresult, +) -> *mut XULStoreIterator { + match XULStore::get_attrs(doc, id) { + Ok(iter) => { + *result = NS_OK; + Box::into_raw(Box::new(iter)) + } + Err(err) => { + *result = err.into(); + ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn xulstore_iter_has_more(iter: &XULStoreIterator) -> bool { + iter.has_more() +} + +#[no_mangle] +pub unsafe extern "C" fn xulstore_iter_get_next( + iter: &mut XULStoreIterator, + value: *mut nsAString, +) -> nsresult { + match iter.get_next() { + Ok(val) => { + (*value).assign(&nsString::from(&val)); + NS_OK + } + Err(err) => err.into(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn xulstore_iter_free(iter: *mut XULStoreIterator) { + drop(Box::from_raw(iter)); +} + +#[no_mangle] +pub unsafe extern "C" fn xulstore_shutdown() -> nsresult { + match XULStore::shutdown() { + Ok(()) => NS_OK, + Err(err) => err.into(), + } +} diff --git a/toolkit/components/xulstore/src/iter.rs b/toolkit/components/xulstore/src/iter.rs new file mode 100644 index 0000000000..06e0ebf175 --- /dev/null +++ b/toolkit/components/xulstore/src/iter.rs @@ -0,0 +1,24 @@ +/* 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}; +use std::vec::IntoIter; + +pub struct XULStoreIterator { + values: IntoIter<String>, +} + +impl XULStoreIterator { + pub(crate) fn new(values: IntoIter<String>) -> Self { + Self { values } + } + + pub(crate) fn has_more(&self) -> bool { + !self.values.as_slice().is_empty() + } + + pub(crate) fn get_next(&mut self) -> XULStoreResult<String> { + Ok(self.values.next().ok_or(XULStoreError::IterationFinished)?) + } +} diff --git a/toolkit/components/xulstore/src/lib.rs b/toolkit/components/xulstore/src/lib.rs new file mode 100644 index 0000000000..fdeec0f6c8 --- /dev/null +++ b/toolkit/components/xulstore/src/lib.rs @@ -0,0 +1,223 @@ +/* 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/. */ + +extern crate crossbeam_utils; +#[macro_use] +extern crate cstr; +extern crate libc; +#[macro_use] +extern crate log; +extern crate moz_task; +extern crate nserror; +extern crate nsstring; +extern crate once_cell; +extern crate rkv; +extern crate serde_json; +extern crate tempfile; +extern crate thiserror; +#[macro_use] +extern crate xpcom; + +mod error; +mod ffi; +mod iter; +mod persist; +mod statics; + +use crate::{ + error::{XULStoreError, XULStoreResult}, + iter::XULStoreIterator, + persist::{flush_writes, persist}, + statics::DATA_CACHE, +}; +use nsstring::nsAString; +use std::collections::btree_map::Entry; +use std::fmt::Display; + +const SEPARATOR: char = '\u{0009}'; + +pub(crate) fn make_key(doc: &impl Display, id: &impl Display, attr: &impl Display) -> String { + format!("{}{}{}{}{}", doc, SEPARATOR, id, SEPARATOR, attr) +} + +pub(crate) fn set_value( + doc: &nsAString, + id: &nsAString, + attr: &nsAString, + value: &nsAString, +) -> XULStoreResult<()> { + debug!("XULStore set value: {} {} {} {}", doc, id, attr, value); + + // bug 319846 -- don't save really long attributes or values. + if id.len() > 512 || attr.len() > 512 { + return Err(XULStoreError::IdAttrNameTooLong); + } + + let value = if value.len() > 4096 { + warn!("XULStore: truncating long attribute value"); + String::from_utf16(&value[0..4096])? + } else { + String::from_utf16(value)? + }; + + let mut cache_guard = DATA_CACHE.lock()?; + let data = match cache_guard.as_mut() { + Some(data) => data, + None => return Ok(()), + }; + data.entry(doc.to_string()) + .or_default() + .entry(id.to_string()) + .or_default() + .insert(attr.to_string(), value.clone()); + + persist(make_key(doc, id, attr), Some(value))?; + + Ok(()) +} + +pub(crate) fn has_value(doc: &nsAString, id: &nsAString, attr: &nsAString) -> XULStoreResult<bool> { + debug!("XULStore has value: {} {} {}", doc, id, attr); + + let cache_guard = DATA_CACHE.lock()?; + let data = match cache_guard.as_ref() { + Some(data) => data, + None => return Ok(false), + }; + + match data.get(&doc.to_string()) { + Some(ids) => match ids.get(&id.to_string()) { + Some(attrs) => Ok(attrs.contains_key(&attr.to_string())), + None => Ok(false), + }, + None => Ok(false), + } +} + +pub(crate) fn get_value( + doc: &nsAString, + id: &nsAString, + attr: &nsAString, +) -> XULStoreResult<String> { + debug!("XULStore get value {} {} {}", doc, id, attr); + + let cache_guard = DATA_CACHE.lock()?; + let data = match cache_guard.as_ref() { + Some(data) => data, + None => return Ok(String::new()), + }; + + match data.get(&doc.to_string()) { + Some(ids) => match ids.get(&id.to_string()) { + Some(attrs) => match attrs.get(&attr.to_string()) { + Some(value) => Ok(value.clone()), + None => Ok(String::new()), + }, + None => Ok(String::new()), + }, + None => Ok(String::new()), + } +} + +pub(crate) fn remove_value( + doc: &nsAString, + id: &nsAString, + attr: &nsAString, +) -> XULStoreResult<()> { + debug!("XULStore remove value {} {} {}", doc, id, attr); + + let mut cache_guard = DATA_CACHE.lock()?; + let data = match cache_guard.as_mut() { + Some(data) => data, + None => return Ok(()), + }; + + let mut ids_empty = false; + if let Some(ids) = data.get_mut(&doc.to_string()) { + let mut attrs_empty = false; + if let Some(attrs) = ids.get_mut(&id.to_string()) { + attrs.remove(&attr.to_string()); + if attrs.is_empty() { + attrs_empty = true; + } + } + if attrs_empty { + ids.remove(&id.to_string()); + if ids.is_empty() { + ids_empty = true; + } + } + }; + if ids_empty { + data.remove(&doc.to_string()); + } + + persist(make_key(doc, id, attr), None)?; + + Ok(()) +} + +pub(crate) fn remove_document(doc: &nsAString) -> XULStoreResult<()> { + debug!("XULStore remove document {}", doc); + + let mut cache_guard = DATA_CACHE.lock()?; + let data = match cache_guard.as_mut() { + Some(data) => data, + None => return Ok(()), + }; + + if let Entry::Occupied(entry) = data.entry(doc.to_string()) { + for (id, attrs) in entry.get() { + for attr in attrs.keys() { + persist(make_key(entry.key(), id, attr), None)?; + } + } + entry.remove_entry(); + } + + Ok(()) +} + +pub(crate) fn get_ids(doc: &nsAString) -> XULStoreResult<XULStoreIterator> { + debug!("XULStore get IDs for {}", doc); + + let cache_guard = DATA_CACHE.lock()?; + let data = match cache_guard.as_ref() { + Some(data) => data, + None => return Ok(XULStoreIterator::new(vec![].into_iter())), + }; + + match data.get(&doc.to_string()) { + Some(ids) => { + let ids: Vec<String> = ids.keys().cloned().collect(); + Ok(XULStoreIterator::new(ids.into_iter())) + } + None => Ok(XULStoreIterator::new(vec![].into_iter())), + } +} + +pub(crate) fn get_attrs(doc: &nsAString, id: &nsAString) -> XULStoreResult<XULStoreIterator> { + debug!("XULStore get attrs for doc, ID: {} {}", doc, id); + + let cache_guard = DATA_CACHE.lock()?; + let data = match cache_guard.as_ref() { + Some(data) => data, + None => return Ok(XULStoreIterator::new(vec![].into_iter())), + }; + + match data.get(&doc.to_string()) { + Some(ids) => match ids.get(&id.to_string()) { + Some(attrs) => { + let attrs: Vec<String> = attrs.keys().cloned().collect(); + Ok(XULStoreIterator::new(attrs.into_iter())) + } + None => Ok(XULStoreIterator::new(vec![].into_iter())), + }, + None => Ok(XULStoreIterator::new(vec![].into_iter())), + } +} + +pub(crate) fn shutdown() -> XULStoreResult<()> { + flush_writes() +} diff --git a/toolkit/components/xulstore/src/persist.rs b/toolkit/components/xulstore/src/persist.rs new file mode 100644 index 0000000000..31ad83b920 --- /dev/null +++ b/toolkit/components/xulstore/src/persist.rs @@ -0,0 +1,179 @@ +/* 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/. */ + +//! The XULStore API is synchronous for both C++ and JS consumers and accessed +//! on the main thread, so we persist its data to disk on a background thread +//! to avoid janking the UI. +//! +//! We also re-open the database each time we write to it in order to conserve +//! heap memory, since holding a database connection open would consume at least +//! 3MB of heap memory in perpetuity. +//! +//! Since re-opening the database repeatedly to write individual changes can be +//! expensive when there are many of them in quick succession, we batch changes +//! and write them in batches. + +use crate::{ + error::{XULStoreError, XULStoreResult}, + statics::get_database, +}; +use crossbeam_utils::atomic::AtomicCell; +use moz_task::{DispatchOptions, Task, TaskRunnable}; +use nserror::nsresult; +use once_cell::sync::Lazy; +use rkv::{StoreError as RkvStoreError, Value}; +use std::{collections::HashMap, sync::Mutex, thread::sleep, time::Duration}; + +/// A map of key/value pairs to persist. Values are Options so we can +/// use the same structure for both puts and deletes, with a `None` value +/// identifying a key that should be deleted from the database. +/// +/// This is a map rather than a sequence in order to merge consecutive +/// changes to the same key, i.e. when a consumer sets *foo* to `bar` +/// and then sets it again to `baz` before we persist the first change. +/// +/// In that case, there's no point in setting *foo* to `bar` before we set +/// it to `baz`, and the map ensures we only ever persist the latest value +/// for any given key. +static CHANGES: Lazy<Mutex<Option<HashMap<String, Option<String>>>>> = + Lazy::new(|| Mutex::new(None)); + +/// A Mutex that prevents two PersistTasks from running at the same time, +/// since each task opens the database, and we need to ensure there is only +/// one open database handle for the database at any given time. +static PERSIST: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(())); + +/// Synchronously persists changes recorded in memory to disk. Typically +/// called from a background thread, however this can be called from the main +/// thread in Gecko during shutdown (via flush_writes). +fn sync_persist() -> XULStoreResult<()> { + // Get the map of key/value pairs from the mutex, replacing it + // with None. To avoid janking the main thread (if it decides + // to makes more changes while we're persisting to disk), we only + // lock the map long enough to move it out of the Mutex. + let writes = CHANGES.lock()?.take(); + + // Return an error early if there's nothing to actually write + let writes = writes.ok_or(XULStoreError::Unavailable)?; + + let db = get_database()?; + let env = db.rkv.read()?; + let mut writer = env.write()?; + + for (key, value) in writes.iter() { + match value { + Some(val) => db.store.put(&mut writer, &key, &Value::Str(val))?, + None => { + match db.store.delete(&mut writer, &key) { + Ok(_) => (), + + // The XULStore API doesn't care if a consumer tries + // to remove a value that doesn't exist in the store, + // so we ignore the error (although in this case the key + // should exist, since it was in the cache!). + Err(RkvStoreError::KeyValuePairNotFound) => { + warn!("tried to remove key that isn't in the store"); + } + + Err(err) => return Err(err.into()), + } + } + } + } + + writer.commit()?; + + Ok(()) +} + +pub(crate) fn flush_writes() -> XULStoreResult<()> { + // One of three things will happen here (barring unexpected errors): + // - There are no writes queued and the background thread is idle. In which + // case, we will get the lock, see that there's nothing to write, and + // return (with data in memory and on disk in sync). + // - There are no writes queued because the background thread is writing + // them. In this case, we will block waiting for the lock held by the + // writing thread (which will ensure that the changes are flushed), then + // discover there are no more to write, and return. + // - The background thread is busy writing changes, and another thread has + // in the mean time added some. In this case, we will block waiting for + // the lock held by the writing thread, discover that there are more + // changes left, flush them ourselves, and return. + // + // This is not airtight, if changes are being added on a different thread + // than the one calling this. However it should be a reasonably strong + // guarantee even so. + let _lock = PERSIST.lock()?; + match sync_persist() { + Ok(_) => (), + + // It's no problem (in fact it's generally expected) that there's just + // nothing to write. + Err(XULStoreError::Unavailable) => { + info!("Unable to persist xulstore"); + } + + Err(err) => return Err(err.into()), + } + Ok(()) +} + +pub(crate) fn persist(key: String, value: Option<String>) -> XULStoreResult<()> { + let mut changes = CHANGES.lock()?; + + if changes.is_none() { + *changes = Some(HashMap::new()); + + // If *changes* was `None`, then this is the first change since + // the last time we persisted, so dispatch a new PersistTask. + let task = Box::new(PersistTask::new()); + TaskRunnable::new("XULStore::Persist", task)? + .dispatch_background_task_with_options(DispatchOptions::default().may_block(true))?; + } + + // Now insert the key/value pair into the map. The unwrap() call here + // should never panic, since the code above sets `writes` to a Some(HashMap) + // if it's None. + changes.as_mut().unwrap().insert(key, value); + + Ok(()) +} + +pub struct PersistTask { + result: AtomicCell<Option<Result<(), XULStoreError>>>, +} + +impl PersistTask { + pub fn new() -> PersistTask { + PersistTask { + result: AtomicCell::default(), + } + } +} + +impl Task for PersistTask { + fn run(&self) { + self.result.store(Some(|| -> Result<(), XULStoreError> { + // Avoid persisting too often. We might want to adjust this value + // in the future to trade durability for performance. + sleep(Duration::from_millis(200)); + + // Prevent another PersistTask from running until this one finishes. + // We do this before getting the database to ensure that there is + // only ever one open database handle at a given time. + let _lock = PERSIST.lock()?; + sync_persist() + }())); + } + + fn done(&self) -> Result<(), nsresult> { + match self.result.swap(None) { + Some(Ok(())) => (), + Some(Err(err)) => error!("removeDocument error: {}", err), + None => error!("removeDocument error: unexpected result"), + }; + + Ok(()) + } +} 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), + } +} |