summaryrefslogtreecommitdiffstats
path: root/toolkit/components/xulstore/src
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/xulstore/src')
-rw-r--r--toolkit/components/xulstore/src/error.rs81
-rw-r--r--toolkit/components/xulstore/src/ffi.rs325
-rw-r--r--toolkit/components/xulstore/src/iter.rs24
-rw-r--r--toolkit/components/xulstore/src/lib.rs223
-rw-r--r--toolkit/components/xulstore/src/persist.rs179
-rw-r--r--toolkit/components/xulstore/src/statics.rs255
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),
+ }
+}