summaryrefslogtreecommitdiffstats
path: root/toolkit/components/kvstore
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/kvstore')
-rw-r--r--toolkit/components/kvstore/Cargo.toml22
-rw-r--r--toolkit/components/kvstore/components.conf14
-rw-r--r--toolkit/components/kvstore/kvstore.sys.mjs207
-rw-r--r--toolkit/components/kvstore/moz.build26
-rw-r--r--toolkit/components/kvstore/nsIKeyValue.idl225
-rw-r--r--toolkit/components/kvstore/nsKeyValueModule.h15
-rw-r--r--toolkit/components/kvstore/src/error.rs89
-rw-r--r--toolkit/components/kvstore/src/lib.rs367
-rw-r--r--toolkit/components/kvstore/src/owned_value.rs72
-rw-r--r--toolkit/components/kvstore/src/task.rs727
-rw-r--r--toolkit/components/kvstore/test/xpcshell/data/test-env-32/data.mdbbin0 -> 16384 bytes
-rw-r--r--toolkit/components/kvstore/test/xpcshell/data/test-env-32/lock.mdbbin0 -> 8192 bytes
-rw-r--r--toolkit/components/kvstore/test/xpcshell/data/test-env-64/data.mdbbin0 -> 45056 bytes
-rw-r--r--toolkit/components/kvstore/test/xpcshell/data/test-env-64/lock.mdbbin0 -> 8192 bytes
-rw-r--r--toolkit/components/kvstore/test/xpcshell/make-test-env.js60
-rw-r--r--toolkit/components/kvstore/test/xpcshell/test_kvstore.js586
-rw-r--r--toolkit/components/kvstore/test/xpcshell/xpcshell.toml5
17 files changed, 2415 insertions, 0 deletions
diff --git a/toolkit/components/kvstore/Cargo.toml b/toolkit/components/kvstore/Cargo.toml
new file mode 100644
index 0000000000..cc98c67fc1
--- /dev/null
+++ b/toolkit/components/kvstore/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "kvstore"
+version = "0.1.0"
+authors = ["Myk Melez <myk@mykzilla.org>"]
+license = "MPL-2.0"
+
+[dependencies]
+atomic_refcell = "0.1"
+crossbeam-utils = "0.8"
+cstr = "0.2"
+lazy_static = "1"
+libc = "0.2"
+log = "0.4"
+moz_task = { path = "../../../xpcom/rust/moz_task" }
+nserror = { path = "../../../xpcom/rust/nserror" }
+nsstring = { path = "../../../xpcom/rust/nsstring" }
+rkv = { version = "0.19", default-features = false, features=["no-canonicalize-path"] }
+storage_variant = { path = "../../../storage/variant" }
+xpcom = { path = "../../../xpcom/rust/xpcom" }
+tempfile = "3"
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+thiserror = "1"
diff --git a/toolkit/components/kvstore/components.conf b/toolkit/components/kvstore/components.conf
new file mode 100644
index 0000000000..76ed89a225
--- /dev/null
+++ b/toolkit/components/kvstore/components.conf
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{6cc1a0a8-af97-4d41-9b4a-58dcec46ebce}',
+ 'contract_ids': ['@mozilla.org/key-value-service;1'],
+ 'headers': ['/toolkit/components/kvstore/nsKeyValueModule.h'],
+ 'legacy_constructor': 'nsKeyValueServiceConstructor',
+ },
+]
diff --git a/toolkit/components/kvstore/kvstore.sys.mjs b/toolkit/components/kvstore/kvstore.sys.mjs
new file mode 100644
index 0000000000..838f68a5df
--- /dev/null
+++ b/toolkit/components/kvstore/kvstore.sys.mjs
@@ -0,0 +1,207 @@
+/* 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/. */
+
+const gKeyValueService = Cc["@mozilla.org/key-value-service;1"].getService(
+ Ci.nsIKeyValueService
+);
+
+function promisify(fn, ...args) {
+ return new Promise((resolve, reject) => {
+ fn({ resolve, reject }, ...args);
+ });
+}
+
+/**
+ * This module wraps the nsIKeyValue* interfaces in a Promise-based API.
+ * To use it, import it, then call the KeyValueService.getOrCreate() method
+ * with a database's path and (optionally) its name:
+ *
+ * ```
+ * ChromeUtils.import("resource://gre/modules/kvstore.jsm");
+ * let database = await KeyValueService.getOrCreate(path, name);
+ * ```
+ *
+ * See the documentation in nsIKeyValue.idl for more information about the API
+ * for key/value storage.
+ */
+
+export class KeyValueService {
+ static async getOrCreate(dir, name) {
+ return new KeyValueDatabase(
+ await promisify(gKeyValueService.getOrCreate, dir, name)
+ );
+ }
+}
+
+/**
+ * A class that wraps an nsIKeyValueDatabase in a Promise-based API.
+ *
+ * This class isn't exported, so you can't instantiate it directly, but you
+ * can retrieve an instance of this class via KeyValueService.getOrCreate():
+ *
+ * ```
+ * const database = await KeyValueService.getOrCreate(path, name);
+ * ```
+ *
+ * You can then call its put(), get(), has(), and delete() methods to access
+ * and manipulate key/value pairs:
+ *
+ * ```
+ * await database.put("foo", 1);
+ * await database.get("foo") === 1; // true
+ * await database.has("foo"); // true
+ * await database.delete("foo");
+ * await database.has("foo"); // false
+ * ```
+ *
+ * You can also call writeMany() to put/delete multiple key/value pairs:
+ *
+ * ```
+ * await database.writeMany({
+ * key1: "value1",
+ * key2: "value2",
+ * key3: "value3",
+ * key4: null, // delete
+ * });
+ * ```
+ *
+ * And you can call its enumerate() method to retrieve a KeyValueEnumerator,
+ * which is described below.
+ */
+class KeyValueDatabase {
+ constructor(database) {
+ this.database = database;
+ }
+
+ put(key, value) {
+ return promisify(this.database.put, key, value);
+ }
+
+ /**
+ * Writes multiple key/value pairs to the database.
+ *
+ * Note:
+ * * Each write could be either put or delete.
+ * * Given multiple values with the same key, only the last value will be stored.
+ * * If the same key gets put and deleted for multiple times, the final state
+ * of that key is subject to the ordering of the put(s) and delete(s).
+ *
+ * @param pairs Pairs could be any of following types:
+ * * An Object, all its properties and the corresponding values will
+ * be used as key value pairs. A property with null or undefined indicating
+ * a deletion.
+ * * An Array or an iterable whose elements are key-value pairs. such as
+ * [["key1", "value1"], ["key2", "value2"]]. Use a pair with value null
+ * to delete a key-value pair, e.g. ["delete-key", null].
+ * * A Map. A key with null or undefined value indicating a deletion.
+ * @return A promise that is fulfilled when all the key/value pairs are written
+ * to the database.
+ */
+ writeMany(pairs) {
+ if (!pairs) {
+ throw new Error("writeMany(): unexpected argument.");
+ }
+
+ let entries;
+
+ if (
+ pairs instanceof Map ||
+ pairs instanceof Array ||
+ typeof pairs[Symbol.iterator] === "function"
+ ) {
+ try {
+ // Let Map constructor validate the argument. Note that Map remembers
+ // the original insertion order of the keys, which satisfies the ordering
+ // premise of this function.
+ const map = pairs instanceof Map ? pairs : new Map(pairs);
+ entries = Array.from(map, ([key, value]) => ({ key, value }));
+ } catch (error) {
+ throw new Error("writeMany(): unexpected argument.");
+ }
+ } else if (typeof pairs === "object") {
+ entries = Array.from(Object.entries(pairs), ([key, value]) => ({
+ key,
+ value,
+ }));
+ } else {
+ throw new Error("writeMany(): unexpected argument.");
+ }
+
+ if (entries.length) {
+ return promisify(this.database.writeMany, entries);
+ }
+ return Promise.resolve();
+ }
+
+ has(key) {
+ return promisify(this.database.has, key);
+ }
+
+ get(key, defaultValue) {
+ return promisify(this.database.get, key, defaultValue);
+ }
+
+ delete(key) {
+ return promisify(this.database.delete, key);
+ }
+
+ clear() {
+ return promisify(this.database.clear);
+ }
+
+ async enumerate(from_key, to_key) {
+ return new KeyValueEnumerator(
+ await promisify(this.database.enumerate, from_key, to_key)
+ );
+ }
+}
+
+/**
+ * A class that wraps an nsIKeyValueEnumerator in a Promise-based API.
+ *
+ * This class isn't exported, so you can't instantiate it directly, but you
+ * can retrieve an instance of this class by calling enumerate() on an instance
+ * of KeyValueDatabase:
+ *
+ * ```
+ * const database = await KeyValueService.getOrCreate(path, name);
+ * const enumerator = await database.enumerate();
+ * ```
+ *
+ * And then iterate pairs via its hasMoreElements() and getNext() methods:
+ *
+ * ```
+ * while (enumerator.hasMoreElements()) {
+ * const { key, value } = enumerator.getNext();
+ * …
+ * }
+ * ```
+ *
+ * Or with a `for...of` statement:
+ *
+ * ```
+ * for (const { key, value } of enumerator) {
+ * …
+ * }
+ * ```
+ */
+class KeyValueEnumerator {
+ constructor(enumerator) {
+ this.enumerator = enumerator;
+ }
+
+ hasMoreElements() {
+ return this.enumerator.hasMoreElements();
+ }
+
+ getNext() {
+ return this.enumerator.getNext();
+ }
+
+ *[Symbol.iterator]() {
+ while (this.enumerator.hasMoreElements()) {
+ yield this.enumerator.getNext();
+ }
+ }
+}
diff --git a/toolkit/components/kvstore/moz.build b/toolkit/components/kvstore/moz.build
new file mode 100644
index 0000000000..02a8d2eee8
--- /dev/null
+++ b/toolkit/components/kvstore/moz.build
@@ -0,0 +1,26 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Storage")
+
+EXTRA_JS_MODULES += [
+ "kvstore.sys.mjs",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"]
+
+XPIDL_SOURCES += [
+ "nsIKeyValue.idl",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+XPIDL_MODULE = "kvstore"
+
+FINAL_LIBRARY = "xul"
diff --git a/toolkit/components/kvstore/nsIKeyValue.idl b/toolkit/components/kvstore/nsIKeyValue.idl
new file mode 100644
index 0000000000..b90d45fc5a
--- /dev/null
+++ b/toolkit/components/kvstore/nsIKeyValue.idl
@@ -0,0 +1,225 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIVariant.idl"
+
+interface nsIKeyValueDatabaseCallback;
+interface nsIKeyValueEnumeratorCallback;
+interface nsIKeyValuePairCallback;
+interface nsIKeyValueVariantCallback;
+interface nsIKeyValueVoidCallback;
+interface nsIKeyValuePair;
+
+/**
+ * The nsIKeyValue* interfaces provide a simple, asynchronous API to a key/value
+ * storage engine. Basic put/get/has/delete operations are supported, as is
+ * enumeration of key/value pairs and the use of multiple named databases within
+ * a single storage file. Operations have ACID semantics.
+ *
+ * This API does not (yet) support transactions, so it will not be appropriate
+ * for all use cases. Extension of this API to support transactions is tracked
+ * by bug 1499238.
+ *
+ * The kvstore.jsm module wraps this API in a more idiomatic, Promise-based
+ * JS API that supports async/await. In most cases, you're better off using
+ * that API from JS rather than using this one directly. Bug 1512319 tracks
+ * native support for Promise in Rust-implemented XPCOM methods.
+ */
+
+/**
+ * The key/value service. Enables retrieval of handles to key/value databases.
+ */
+[scriptable, builtinclass, rust_sync, uuid(46c893dd-4c14-4de0-b33d-a1be18c6d062)]
+interface nsIKeyValueService : nsISupports {
+ /**
+ * Get a handle to an existing database or a newly-created one
+ * at the specified path and with the given name.
+ *
+ * The service supports multiple named databases at the same path
+ * (i.e. within the same storage file), so you can call this method
+ * multiple times with the same path and different names to retrieve
+ * multiple databases stored in the same location on disk.
+ */
+ void getOrCreate(
+ in nsIKeyValueDatabaseCallback callback,
+ in AUTF8String path,
+ in AUTF8String name);
+};
+
+/**
+ * A key/value database.
+ *
+ * All methods are asynchronous and take a callback as their first argument.
+ * The types of the callbacks vary, but they can all be implemented in JS
+ * via an object literal with the relevant methods.
+ */
+[scriptable, builtinclass, rust_sync, uuid(c449398e-174c-425b-8195-da6aa0ccd9a5)]
+interface nsIKeyValueDatabase : nsISupports {
+ /**
+ * Write the specified key/value pair to the database.
+ */
+ void put(
+ in nsIKeyValueVoidCallback callback,
+ in AUTF8String key,
+ in nsIVariant value);
+
+ /**
+ * Write multiple key/value pairs to the database.
+ *
+ * It supports two types of write:
+ * * Put a key/value pair into the database. It takes a nsIKeyValuePair
+ * where its key and value follow the same types as the put() method.
+ * * Delete a key/value pair from database. It takes a nsIkeyValuePair
+ * where its value property must be null or undefined.
+ *
+ * This features the "all-or-nothing" semantics, i.e. if any error occurs
+ * during the call, it will rollback the previous writes and terminate the
+ * call. In addition, writeMany should be more efficient than calling "put"
+ * or "delete" for every single key/value pair since it does all the writes
+ * in a single transaction.
+ *
+ * Note:
+ * * If there are multiple values with the same key in the specified
+ * pairs, only the last value will be stored in the database.
+ * * Deleting a key that is not in the database will be silently ignored.
+ * * If the same key gets put and deleted for multiple times, the final
+ * state of that key is subject to the ordering of the put(s) and delete(s).
+ */
+ void writeMany(
+ in nsIKeyValueVoidCallback callback,
+ in Array<nsIKeyValuePair> pairs);
+
+ /**
+ * Retrieve the value of the specified key from the database.
+ *
+ * If the key/value pair doesn't exist in the database, and you specify
+ * a default value, then the default value will be returned. Otherwise,
+ * the callback's resolve() method will be called with a variant
+ * of type VTYPE_EMPTY, which translates to the JS `null` value.
+ */
+ void get(
+ in nsIKeyValueVariantCallback callback,
+ in AUTF8String key,
+ [optional] in nsIVariant defaultValue);
+
+ /**
+ * Determine whether or not the key exists in the database.
+ */
+ void has(
+ in nsIKeyValueVariantCallback callback,
+ in AUTF8String key);
+
+ /**
+ * Remove the key/value pair with the given key from the database.
+ *
+ * If the given key doesn't exist in the database, this operation doesn't
+ * fail; or rather, it fails silently, calling the resolve() method
+ * of its callback rather than reject(). If you want to know whether
+ * or not a key exists when deleting it, call the has() method first.
+ */
+ void delete(
+ in nsIKeyValueVoidCallback callback,
+ in AUTF8String key);
+
+ /**
+ * Clear all the key/value pairs from the database.
+ */
+ void clear(in nsIKeyValueVoidCallback callback);
+
+ /**
+ * Enumerate key/value pairs, starting with the first key equal to
+ * or greater than the "from" key (inclusive) and ending with the last key
+ * less than the "to" key (exclusive) sorted lexicographically.
+ *
+ * If either key is omitted, the range extends to the first and/or last key
+ * in the database.
+ */
+ void enumerate(
+ in nsIKeyValueEnumeratorCallback callback,
+ [optional] in AUTF8String fromKey,
+ [optional] in AUTF8String toKey);
+};
+
+/**
+ * A key/value pair. Returned by nsIKeyValueEnumerator.getNext().
+ */
+[scriptable, uuid(bc37b06a-23b5-4b32-8281-4b8479601c7e)]
+interface nsIKeyValuePair : nsISupports {
+ readonly attribute AUTF8String key;
+ readonly attribute nsIVariant value;
+};
+
+/**
+ * An enumerator of key/value pairs. Although its methods are similar
+ * to those of nsISimpleEnumerator, this interface's getNext() method returns
+ * an nsIKeyValuePair rather than an nsISupports, so consumers don't need
+ * to QI it to that interface; but this interface doesn't implement the JS
+ * iteration protocol (because the Rust-XPCOM bindings don't yet support it),
+ * which is another reason why you should use the kvstore.jsm module from JS
+ * instead of accessing this API directly.
+ */
+[scriptable, builtinclass, rust_sync, uuid(b9ba7116-b7ff-4717-9a28-a08e6879b199)]
+interface nsIKeyValueEnumerator : nsISupports {
+ bool hasMoreElements();
+ nsIKeyValuePair getNext();
+};
+
+/**
+ * A callback for the nsIKeyValueService.getOrCreate() method.
+ *
+ * The result is an nsIKeyValueDatabase.
+ */
+[scriptable, uuid(2becc1f8-2d80-4b63-92a8-24ee8f79ee45)]
+interface nsIKeyValueDatabaseCallback : nsISupports {
+ void resolve(in nsIKeyValueDatabase database);
+ void reject(in AUTF8String message);
+};
+
+/**
+ * A callback for the nsIKeyValueDatabase.enumerate() method.
+ *
+ * The result is an nsIKeyValueEnumerator.
+ */
+[scriptable, uuid(b7ea2183-880b-4424-ab24-5aa1555b775d)]
+interface nsIKeyValueEnumeratorCallback : nsISupports {
+ void resolve(in nsIKeyValueEnumerator enumerator);
+ void reject(in AUTF8String message);
+};
+
+/**
+ * A callback for the nsIKeyValueEnumerator.getNext() method.
+ *
+ * The result is the next key/value pair, expressed as separate key and value
+ * parameters.
+ */
+[scriptable, uuid(50f65485-ec1e-4307-812b-b8a15e1f382e)]
+interface nsIKeyValuePairCallback : nsISupports {
+ void resolve(in nsIKeyValuePair pair);
+ void reject(in AUTF8String message);
+};
+
+/**
+ * A callback for the nsIKeyValueDatabase.has() and .get() methods.
+ *
+ * The result is an nsIVariant, which is always a boolean for the has() method
+ * and can be any supported data type for the get() method.
+ */
+[scriptable, uuid(174ebfa1-74ea-42a7-aa90-85bbaf1da4bf)]
+interface nsIKeyValueVariantCallback : nsISupports {
+ void resolve(in nsIVariant result);
+ void reject(in AUTF8String message);
+};
+
+/**
+ * A callback for the nsIKeyValueDatabase.put() and .delete() methods.
+ *
+ * There is no result, but the resolve() method is still called when those
+ * async operations complete, to notify consumers of completion.
+ */
+[scriptable, uuid(0c17497a-ccf8-451a-838d-9dfa7f846379)]
+interface nsIKeyValueVoidCallback : nsISupports {
+ void resolve();
+ void reject(in AUTF8String message);
+};
diff --git a/toolkit/components/kvstore/nsKeyValueModule.h b/toolkit/components/kvstore/nsKeyValueModule.h
new file mode 100644
index 0000000000..00bb75269d
--- /dev/null
+++ b/toolkit/components/kvstore/nsKeyValueModule.h
@@ -0,0 +1,15 @@
+/* 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/. */
+
+#ifndef nsKeyValueModule_h
+#define nsKeyValueModule_h
+
+#include "nsID.h"
+
+extern "C" {
+// Implemented in Rust.
+nsresult nsKeyValueServiceConstructor(REFNSIID aIID, void** aResult);
+} // extern "C"
+
+#endif // defined nsKeyValueModule_h
diff --git a/toolkit/components/kvstore/src/error.rs b/toolkit/components/kvstore/src/error.rs
new file mode 100644
index 0000000000..82ed2ce0d8
--- /dev/null
+++ b/toolkit/components/kvstore/src/error.rs
@@ -0,0 +1,89 @@
+/* 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_NOT_IMPLEMENTED, NS_ERROR_NO_INTERFACE,
+ NS_ERROR_NULL_POINTER, NS_ERROR_UNEXPECTED,
+};
+use nsstring::nsCString;
+use rkv::{MigrateError, StoreError};
+use std::{io::Error as IoError, str::Utf8Error, string::FromUtf16Error, sync::PoisonError};
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum KeyValueError {
+ #[error("error converting string: {0:?}")]
+ ConvertBytes(#[from] Utf8Error),
+
+ #[error("error converting string: {0:?}")]
+ ConvertString(#[from] FromUtf16Error),
+
+ #[error("I/O error: {0:?}")]
+ IoError(#[from] IoError),
+
+ #[error("migrate error: {0:?}")]
+ MigrateError(#[from] MigrateError),
+
+ #[error("no interface '{0}'")]
+ NoInterface(&'static str),
+
+ // NB: We can avoid storing the nsCString error description
+ // once nsresult is a real type with a Display implementation
+ // per https://bugzilla.mozilla.org/show_bug.cgi?id=1513350.
+ #[error("error result {0}")]
+ Nsresult(nsCString, nsresult),
+
+ #[error("arg is null")]
+ NullPointer,
+
+ #[error("poison error getting read/write lock")]
+ PoisonError,
+
+ #[error("error reading key/value pair")]
+ Read,
+
+ #[error("store error: {0:?}")]
+ StoreError(#[from] StoreError),
+
+ #[error("unsupported owned value type")]
+ UnsupportedOwned,
+
+ #[error("unexpected value")]
+ UnexpectedValue,
+
+ #[error("unsupported variant type: {0}")]
+ UnsupportedVariant(u16),
+}
+
+impl From<nsresult> for KeyValueError {
+ fn from(result: nsresult) -> KeyValueError {
+ KeyValueError::Nsresult(result.error_name(), result)
+ }
+}
+
+impl From<KeyValueError> for nsresult {
+ fn from(err: KeyValueError) -> nsresult {
+ match err {
+ KeyValueError::ConvertBytes(_) => NS_ERROR_FAILURE,
+ KeyValueError::ConvertString(_) => NS_ERROR_FAILURE,
+ KeyValueError::IoError(_) => NS_ERROR_FAILURE,
+ KeyValueError::NoInterface(_) => NS_ERROR_NO_INTERFACE,
+ KeyValueError::Nsresult(_, result) => result,
+ KeyValueError::NullPointer => NS_ERROR_NULL_POINTER,
+ KeyValueError::PoisonError => NS_ERROR_UNEXPECTED,
+ KeyValueError::Read => NS_ERROR_FAILURE,
+ KeyValueError::StoreError(_) => NS_ERROR_FAILURE,
+ KeyValueError::MigrateError(_) => NS_ERROR_FAILURE,
+ KeyValueError::UnsupportedOwned => NS_ERROR_NOT_IMPLEMENTED,
+ KeyValueError::UnexpectedValue => NS_ERROR_UNEXPECTED,
+ KeyValueError::UnsupportedVariant(_) => NS_ERROR_NOT_IMPLEMENTED,
+ }
+ }
+}
+
+impl<T> From<PoisonError<T>> for KeyValueError {
+ fn from(_err: PoisonError<T>) -> KeyValueError {
+ KeyValueError::PoisonError
+ }
+}
diff --git a/toolkit/components/kvstore/src/lib.rs b/toolkit/components/kvstore/src/lib.rs
new file mode 100644
index 0000000000..5601ecb12a
--- /dev/null
+++ b/toolkit/components/kvstore/src/lib.rs
@@ -0,0 +1,367 @@
+/* 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 atomic_refcell;
+extern crate crossbeam_utils;
+#[macro_use]
+extern crate cstr;
+extern crate libc;
+extern crate log;
+extern crate moz_task;
+extern crate nserror;
+extern crate nsstring;
+extern crate rkv;
+extern crate storage_variant;
+extern crate tempfile;
+extern crate thin_vec;
+extern crate thiserror;
+extern crate xpcom;
+
+mod error;
+mod owned_value;
+mod task;
+
+use atomic_refcell::AtomicRefCell;
+use error::KeyValueError;
+use libc::c_void;
+use moz_task::{create_background_task_queue, DispatchOptions, TaskRunnable};
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK};
+use nsstring::{nsACString, nsCString};
+use owned_value::{owned_to_variant, variant_to_owned};
+use rkv::backend::{SafeModeDatabase, SafeModeEnvironment};
+use rkv::OwnedValue;
+use std::{
+ ptr,
+ sync::{Arc, RwLock},
+ vec::IntoIter,
+};
+use task::{
+ ClearTask, DeleteTask, EnumerateTask, GetOrCreateTask, GetTask, HasTask, PutTask, WriteManyTask,
+};
+use thin_vec::ThinVec;
+use xpcom::{
+ getter_addrefs,
+ interfaces::{
+ nsIKeyValueDatabaseCallback, nsIKeyValueEnumeratorCallback, nsIKeyValuePair,
+ nsIKeyValueVariantCallback, nsIKeyValueVoidCallback, nsISerialEventTarget, nsIVariant,
+ },
+ nsIID, xpcom, xpcom_method, RefPtr,
+};
+
+type Rkv = rkv::Rkv<SafeModeEnvironment>;
+type SingleStore = rkv::SingleStore<SafeModeDatabase>;
+type KeyValuePairResult = Result<(String, OwnedValue), KeyValueError>;
+
+#[no_mangle]
+pub unsafe extern "C" fn nsKeyValueServiceConstructor(
+ iid: &nsIID,
+ result: *mut *mut c_void,
+) -> nsresult {
+ *result = ptr::null_mut();
+
+ let service = KeyValueService::new();
+ service.QueryInterface(iid, result)
+}
+
+// For each public XPCOM method in the nsIKeyValue* interfaces, we implement
+// a pair of Rust methods:
+//
+// 1. a method named after the XPCOM (as modified by the XPIDL parser, i.e.
+// by capitalization of its initial letter) that returns an nsresult;
+//
+// 2. a method with a Rust-y name that returns a Result<(), KeyValueError>.
+//
+// XPCOM calls the first method, which is only responsible for calling
+// the second one and converting its Result to an nsresult (logging errors
+// in the process). The second method is responsible for doing the work.
+//
+// For example, given an XPCOM method FooBar, we implement a method FooBar
+// that calls a method foo_bar. foo_bar returns a Result<(), KeyValueError>,
+// and FooBar converts that to an nsresult.
+//
+// This design allows us to use Rust idioms like the question mark operator
+// to simplify the implementation in the second method while returning XPCOM-
+// compatible nsresult values to XPCOM callers.
+//
+// The XPCOM methods are implemented using the xpcom_method! declarative macro
+// from the xpcom crate.
+
+#[xpcom(implement(nsIKeyValueService), atomic)]
+pub struct KeyValueService {}
+
+impl KeyValueService {
+ fn new() -> RefPtr<KeyValueService> {
+ KeyValueService::allocate(InitKeyValueService {})
+ }
+
+ xpcom_method!(
+ get_or_create => GetOrCreate(
+ callback: *const nsIKeyValueDatabaseCallback,
+ path: *const nsACString,
+ name: *const nsACString
+ )
+ );
+
+ fn get_or_create(
+ &self,
+ callback: &nsIKeyValueDatabaseCallback,
+ path: &nsACString,
+ name: &nsACString,
+ ) -> Result<(), nsresult> {
+ let task = Box::new(GetOrCreateTask::new(
+ RefPtr::new(callback),
+ nsCString::from(path),
+ nsCString::from(name),
+ ));
+
+ TaskRunnable::new("KVService::GetOrCreate", task)?
+ .dispatch_background_task_with_options(DispatchOptions::default().may_block(true))
+ }
+}
+
+#[xpcom(implement(nsIKeyValueDatabase), atomic)]
+pub struct KeyValueDatabase {
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ queue: RefPtr<nsISerialEventTarget>,
+}
+
+impl KeyValueDatabase {
+ fn new(
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ ) -> Result<RefPtr<KeyValueDatabase>, KeyValueError> {
+ let queue = create_background_task_queue(cstr!("KeyValueDatabase"))?;
+ Ok(KeyValueDatabase::allocate(InitKeyValueDatabase {
+ rkv,
+ store,
+ queue,
+ }))
+ }
+
+ xpcom_method!(
+ put => Put(
+ callback: *const nsIKeyValueVoidCallback,
+ key: *const nsACString,
+ value: *const nsIVariant
+ )
+ );
+
+ fn put(
+ &self,
+ callback: &nsIKeyValueVoidCallback,
+ key: &nsACString,
+ value: &nsIVariant,
+ ) -> Result<(), nsresult> {
+ let value = variant_to_owned(value)?.ok_or(KeyValueError::UnexpectedValue)?;
+
+ let task = Box::new(PutTask::new(
+ RefPtr::new(callback),
+ Arc::clone(&self.rkv),
+ self.store,
+ nsCString::from(key),
+ value,
+ ));
+
+ TaskRunnable::dispatch(TaskRunnable::new("KVDatabase::Put", task)?, &self.queue)
+ }
+
+ xpcom_method!(
+ write_many => WriteMany(
+ callback: *const nsIKeyValueVoidCallback,
+ pairs: *const ThinVec<Option<RefPtr<nsIKeyValuePair>>>
+ )
+ );
+
+ fn write_many(
+ &self,
+ callback: &nsIKeyValueVoidCallback,
+ pairs: &ThinVec<Option<RefPtr<nsIKeyValuePair>>>,
+ ) -> Result<(), nsresult> {
+ let mut entries = Vec::with_capacity(pairs.len());
+
+ for pair in pairs {
+ let pair = pair
+ .as_ref()
+ .ok_or(nsresult::from(KeyValueError::UnexpectedValue))?;
+
+ let mut key = nsCString::new();
+ unsafe { pair.GetKey(&mut *key) }.to_result()?;
+ if key.is_empty() {
+ return Err(nsresult::from(KeyValueError::UnexpectedValue));
+ }
+
+ let val: RefPtr<nsIVariant> = getter_addrefs(|p| unsafe { pair.GetValue(p) })?;
+ let value = variant_to_owned(&val)?;
+ entries.push((key, value));
+ }
+
+ let task = Box::new(WriteManyTask::new(
+ RefPtr::new(callback),
+ Arc::clone(&self.rkv),
+ self.store,
+ entries,
+ ));
+
+ TaskRunnable::dispatch(
+ TaskRunnable::new("KVDatabase::WriteMany", task)?,
+ &self.queue,
+ )
+ }
+
+ xpcom_method!(
+ get => Get(
+ callback: *const nsIKeyValueVariantCallback,
+ key: *const nsACString,
+ default_value: *const nsIVariant
+ )
+ );
+
+ fn get(
+ &self,
+ callback: &nsIKeyValueVariantCallback,
+ key: &nsACString,
+ default_value: &nsIVariant,
+ ) -> Result<(), nsresult> {
+ let task = Box::new(GetTask::new(
+ RefPtr::new(callback),
+ Arc::clone(&self.rkv),
+ self.store,
+ nsCString::from(key),
+ variant_to_owned(default_value)?,
+ ));
+
+ TaskRunnable::dispatch(TaskRunnable::new("KVDatabase::Get", task)?, &self.queue)
+ }
+
+ xpcom_method!(
+ has => Has(callback: *const nsIKeyValueVariantCallback, key: *const nsACString)
+ );
+
+ fn has(&self, callback: &nsIKeyValueVariantCallback, key: &nsACString) -> Result<(), nsresult> {
+ let task = Box::new(HasTask::new(
+ RefPtr::new(callback),
+ Arc::clone(&self.rkv),
+ self.store,
+ nsCString::from(key),
+ ));
+
+ TaskRunnable::dispatch(TaskRunnable::new("KVDatabase::Has", task)?, &self.queue)
+ }
+
+ xpcom_method!(
+ delete => Delete(callback: *const nsIKeyValueVoidCallback, key: *const nsACString)
+ );
+
+ fn delete(&self, callback: &nsIKeyValueVoidCallback, key: &nsACString) -> Result<(), nsresult> {
+ let task = Box::new(DeleteTask::new(
+ RefPtr::new(callback),
+ Arc::clone(&self.rkv),
+ self.store,
+ nsCString::from(key),
+ ));
+
+ TaskRunnable::dispatch(TaskRunnable::new("KVDatabase::Delete", task)?, &self.queue)
+ }
+
+ xpcom_method!(
+ clear => Clear(callback: *const nsIKeyValueVoidCallback)
+ );
+
+ fn clear(&self, callback: &nsIKeyValueVoidCallback) -> Result<(), nsresult> {
+ let task = Box::new(ClearTask::new(
+ RefPtr::new(callback),
+ Arc::clone(&self.rkv),
+ self.store,
+ ));
+
+ TaskRunnable::dispatch(TaskRunnable::new("KVDatabase::Clear", task)?, &self.queue)
+ }
+
+ xpcom_method!(
+ enumerate => Enumerate(
+ callback: *const nsIKeyValueEnumeratorCallback,
+ from_key: *const nsACString,
+ to_key: *const nsACString
+ )
+ );
+
+ fn enumerate(
+ &self,
+ callback: &nsIKeyValueEnumeratorCallback,
+ from_key: &nsACString,
+ to_key: &nsACString,
+ ) -> Result<(), nsresult> {
+ let task = Box::new(EnumerateTask::new(
+ RefPtr::new(callback),
+ Arc::clone(&self.rkv),
+ self.store,
+ nsCString::from(from_key),
+ nsCString::from(to_key),
+ ));
+
+ TaskRunnable::dispatch(
+ TaskRunnable::new("KVDatabase::Enumerate", task)?,
+ &self.queue,
+ )
+ }
+}
+
+#[xpcom(implement(nsIKeyValueEnumerator), atomic)]
+pub struct KeyValueEnumerator {
+ iter: AtomicRefCell<IntoIter<KeyValuePairResult>>,
+}
+
+impl KeyValueEnumerator {
+ fn new(pairs: Vec<KeyValuePairResult>) -> RefPtr<KeyValueEnumerator> {
+ KeyValueEnumerator::allocate(InitKeyValueEnumerator {
+ iter: AtomicRefCell::new(pairs.into_iter()),
+ })
+ }
+
+ xpcom_method!(has_more_elements => HasMoreElements() -> bool);
+
+ fn has_more_elements(&self) -> Result<bool, KeyValueError> {
+ Ok(!self.iter.borrow().as_slice().is_empty())
+ }
+
+ xpcom_method!(get_next => GetNext() -> *const nsIKeyValuePair);
+
+ fn get_next(&self) -> Result<RefPtr<nsIKeyValuePair>, KeyValueError> {
+ let mut iter = self.iter.borrow_mut();
+ let (key, value) = iter
+ .next()
+ .ok_or_else(|| KeyValueError::from(NS_ERROR_FAILURE))??;
+
+ // We fail on retrieval of the key/value pair if the key isn't valid
+ // UTF-*, if the value is unexpected, or if we encountered a store error
+ // while retrieving the pair.
+ Ok(RefPtr::new(
+ KeyValuePair::new(key, value).coerce::<nsIKeyValuePair>(),
+ ))
+ }
+}
+
+#[xpcom(implement(nsIKeyValuePair), atomic)]
+pub struct KeyValuePair {
+ key: String,
+ value: OwnedValue,
+}
+
+impl KeyValuePair {
+ fn new(key: String, value: OwnedValue) -> RefPtr<KeyValuePair> {
+ KeyValuePair::allocate(InitKeyValuePair { key, value })
+ }
+
+ xpcom_method!(get_key => GetKey() -> nsACString);
+ xpcom_method!(get_value => GetValue() -> *const nsIVariant);
+
+ fn get_key(&self) -> Result<nsCString, KeyValueError> {
+ Ok(nsCString::from(&self.key))
+ }
+
+ fn get_value(&self) -> Result<RefPtr<nsIVariant>, KeyValueError> {
+ Ok(owned_to_variant(self.value.clone())?)
+ }
+}
diff --git a/toolkit/components/kvstore/src/owned_value.rs b/toolkit/components/kvstore/src/owned_value.rs
new file mode 100644
index 0000000000..a22abcb8da
--- /dev/null
+++ b/toolkit/components/kvstore/src/owned_value.rs
@@ -0,0 +1,72 @@
+/* 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 error::KeyValueError;
+use nsstring::{nsCString, nsString};
+use rkv::OwnedValue;
+use std::convert::TryInto;
+use storage_variant::{DataType, NsIVariantExt, VariantType};
+use xpcom::{interfaces::nsIVariant, RefPtr};
+
+pub fn owned_to_variant(owned: OwnedValue) -> Result<RefPtr<nsIVariant>, KeyValueError> {
+ match owned {
+ OwnedValue::Bool(val) => Ok(val.into_variant()),
+ OwnedValue::I64(val) => Ok(val.into_variant()),
+ OwnedValue::F64(val) => Ok(val.into_variant()),
+ OwnedValue::Str(ref val) => Ok(nsString::from(val).into_variant()),
+
+ // kvstore doesn't (yet?) support these types of OwnedValue,
+ // and we should never encounter them, but we need to exhaust
+ // all possible variants of the OwnedValue enum.
+ OwnedValue::Instant(_) => Err(KeyValueError::UnsupportedOwned),
+ OwnedValue::Json(_) => Err(KeyValueError::UnsupportedOwned),
+ OwnedValue::U64(_) => Err(KeyValueError::UnsupportedOwned),
+ OwnedValue::Uuid(_) => Err(KeyValueError::UnsupportedOwned),
+ OwnedValue::Blob(_) => Err(KeyValueError::UnsupportedOwned),
+ }
+}
+
+pub fn variant_to_owned(variant: &nsIVariant) -> Result<Option<OwnedValue>, KeyValueError> {
+ let data_type = variant.get_data_type();
+
+ match data_type.try_into() {
+ Ok(DataType::Int32) => {
+ let mut val: i32 = 0;
+ unsafe { variant.GetAsInt32(&mut val) }.to_result()?;
+ Ok(Some(OwnedValue::I64(val.into())))
+ }
+ Ok(DataType::Int64) => {
+ let mut val: i64 = 0;
+ unsafe { variant.GetAsInt64(&mut val) }.to_result()?;
+ Ok(Some(OwnedValue::I64(val)))
+ }
+ Ok(DataType::Double) => {
+ let mut val: f64 = 0.0;
+ unsafe { variant.GetAsDouble(&mut val) }.to_result()?;
+ Ok(Some(OwnedValue::F64(val)))
+ }
+ Ok(DataType::CString)
+ | Ok(DataType::CharStr)
+ | Ok(DataType::StringSizeIs)
+ | Ok(DataType::Utf8String) => {
+ let mut val: nsCString = nsCString::new();
+ unsafe { variant.GetAsAUTF8String(&mut *val) }.to_result()?;
+ let s = std::str::from_utf8(&*val)?;
+ Ok(Some(OwnedValue::Str(s.into())))
+ }
+ Ok(DataType::AString) | Ok(DataType::WCharStr) | Ok(DataType::WStringSizeIs) => {
+ let mut val: nsString = nsString::new();
+ unsafe { variant.GetAsAString(&mut *val) }.to_result()?;
+ let str = String::from_utf16(&val)?;
+ Ok(Some(OwnedValue::Str(str)))
+ }
+ Ok(DataType::Bool) => {
+ let mut val: bool = false;
+ unsafe { variant.GetAsBool(&mut val) }.to_result()?;
+ Ok(Some(OwnedValue::Bool(val)))
+ }
+ Ok(DataType::Void) | Ok(DataType::EmptyArray) | Ok(DataType::Empty) => Ok(None),
+ Err(_) => Err(KeyValueError::UnsupportedVariant(data_type)),
+ }
+}
diff --git a/toolkit/components/kvstore/src/task.rs b/toolkit/components/kvstore/src/task.rs
new file mode 100644
index 0000000000..3608dc9665
--- /dev/null
+++ b/toolkit/components/kvstore/src/task.rs
@@ -0,0 +1,727 @@
+/* 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 xpcom;
+
+use crossbeam_utils::atomic::AtomicCell;
+use error::KeyValueError;
+use moz_task::Task;
+use nserror::{nsresult, NS_ERROR_FAILURE};
+use nsstring::nsCString;
+use owned_value::owned_to_variant;
+use rkv::backend::{BackendInfo, SafeMode, SafeModeDatabase, SafeModeEnvironment};
+use rkv::{OwnedValue, StoreError, StoreOptions, Value};
+use std::{
+ path::Path,
+ str,
+ sync::{Arc, RwLock},
+};
+use storage_variant::VariantType;
+use xpcom::{
+ interfaces::{
+ nsIKeyValueDatabaseCallback, nsIKeyValueEnumeratorCallback, nsIKeyValueVariantCallback,
+ nsIKeyValueVoidCallback, nsIVariant,
+ },
+ RefPtr, ThreadBoundRefPtr,
+};
+use KeyValueDatabase;
+use KeyValueEnumerator;
+use KeyValuePairResult;
+
+type Manager = rkv::Manager<SafeModeEnvironment>;
+type Rkv = rkv::Rkv<SafeModeEnvironment>;
+type SingleStore = rkv::SingleStore<SafeModeDatabase>;
+
+/// A macro to generate a done() implementation for a Task.
+/// Takes one argument that specifies the type of the Task's callback function:
+/// value: a callback function that takes a value
+/// void: the callback function doesn't take a value
+///
+/// The "value" variant calls self.convert() to convert a successful result
+/// into the value to pass to the callback function. So if you generate done()
+/// for a callback that takes a value, ensure you also implement convert()!
+macro_rules! task_done {
+ (value) => {
+ fn done(&self) -> Result<(), nsresult> {
+ // If TaskRunnable calls Task.done() to return a result on the
+ // main thread before TaskRunnable returns on the database thread,
+ // then the Task will get dropped on the database thread.
+ //
+ // But the callback is an nsXPCWrappedJS that isn't safe to release
+ // on the database thread. So we move it out of the Task here to ensure
+ // it gets released on the main thread.
+ let threadbound = self.callback.swap(None).ok_or(NS_ERROR_FAILURE)?;
+ let callback = threadbound.get_ref().ok_or(NS_ERROR_FAILURE)?;
+
+ match self.result.swap(None) {
+ Some(Ok(value)) => unsafe { callback.Resolve(self.convert(value)?.coerce()) },
+ Some(Err(err)) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
+ None => unsafe { callback.Reject(&*nsCString::from("unexpected")) },
+ }
+ .to_result()
+ }
+ };
+
+ (void) => {
+ fn done(&self) -> Result<(), nsresult> {
+ // If TaskRunnable calls Task.done() to return a result on the
+ // main thread before TaskRunnable returns on the database thread,
+ // then the Task will get dropped on the database thread.
+ //
+ // But the callback is an nsXPCWrappedJS that isn't safe to release
+ // on the database thread. So we move it out of the Task here to ensure
+ // it gets released on the main thread.
+ let threadbound = self.callback.swap(None).ok_or(NS_ERROR_FAILURE)?;
+ let callback = threadbound.get_ref().ok_or(NS_ERROR_FAILURE)?;
+
+ match self.result.swap(None) {
+ Some(Ok(())) => unsafe { callback.Resolve() },
+ Some(Err(err)) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
+ None => unsafe { callback.Reject(&*nsCString::from("unexpected")) },
+ }
+ .to_result()
+ }
+ };
+}
+
+/// A tuple comprising an Arc<RwLock<Rkv>> and a SingleStore, which is
+/// the result of GetOrCreateTask. We declare this type because otherwise
+/// Clippy complains "error: very complex type used. Consider factoring
+/// parts into `type` definitions" (i.e. clippy::type-complexity) when we
+/// declare the type of `GetOrCreateTask::result`.
+type RkvStoreTuple = (Arc<RwLock<Rkv>>, SingleStore);
+
+// The threshold for active resizing.
+const RESIZE_RATIO: f32 = 0.85;
+
+/// The threshold (50 MB) to switch the resizing policy from the double size to
+/// the constant increment for active resizing.
+const INCREMENTAL_RESIZE_THRESHOLD: usize = 52_428_800;
+
+/// The incremental resize step (5 MB)
+const INCREMENTAL_RESIZE_STEP: usize = 5_242_880;
+
+/// The RKV disk page size and mask.
+const PAGE_SIZE: usize = 4096;
+const PAGE_SIZE_MASK: usize = 0b_1111_1111_1111;
+
+/// Round the non-zero size to the multiple of page size greater or equal.
+///
+/// It does not handle the special cases such as size zero and overflow,
+/// because even if that happens (extremely unlikely though), RKV will
+/// ignore the new size if it's smaller than the current size.
+///
+/// E.g:
+/// [ 1 - 4096] -> 4096,
+/// [4097 - 8192] -> 8192,
+/// [8193 - 12286] -> 12286,
+fn round_to_pagesize(size: usize) -> usize {
+ if size & PAGE_SIZE_MASK == 0 {
+ size
+ } else {
+ (size & !PAGE_SIZE_MASK) + PAGE_SIZE
+ }
+}
+
+/// Kvstore employes two auto resizing strategies: active and passive resizing.
+/// They work together to liberate consumers from having to guess the "proper"
+/// size of the store upfront. See more detail about this in Bug 1543861.
+///
+/// Active resizing that is performed at the store startup.
+///
+/// It either increases the size in double, or by a constant size if its size
+/// reaches INCREMENTAL_RESIZE_THRESHOLD.
+///
+/// Note that on Linux / MAC OSX, the increased size would only take effect if
+/// there is a write transaction committed afterwards.
+fn active_resize(env: &Rkv) -> Result<(), StoreError> {
+ let info = env.info()?;
+ let current_size = info.map_size();
+
+ let size = if current_size < INCREMENTAL_RESIZE_THRESHOLD {
+ current_size << 1
+ } else {
+ current_size + INCREMENTAL_RESIZE_STEP
+ };
+
+ env.set_map_size(size)?;
+ Ok(())
+}
+
+/// Passive resizing that is performed when the MAP_FULL error occurs. It
+/// increases the store with a `wanted` size.
+///
+/// Note that the `wanted` size must be rounded to a multiple of page size
+/// by using `round_to_pagesize`.
+fn passive_resize(env: &Rkv, wanted: usize) -> Result<(), StoreError> {
+ let info = env.info()?;
+ let current_size = info.map_size();
+ env.set_map_size(current_size + wanted)?;
+ Ok(())
+}
+
+pub struct GetOrCreateTask {
+ callback: AtomicCell<Option<ThreadBoundRefPtr<nsIKeyValueDatabaseCallback>>>,
+ path: nsCString,
+ name: nsCString,
+ result: AtomicCell<Option<Result<RkvStoreTuple, KeyValueError>>>,
+}
+
+impl GetOrCreateTask {
+ pub fn new(
+ callback: RefPtr<nsIKeyValueDatabaseCallback>,
+ path: nsCString,
+ name: nsCString,
+ ) -> GetOrCreateTask {
+ GetOrCreateTask {
+ callback: AtomicCell::new(Some(ThreadBoundRefPtr::new(callback))),
+ path,
+ name,
+ result: AtomicCell::default(),
+ }
+ }
+
+ fn convert(&self, result: RkvStoreTuple) -> Result<RefPtr<KeyValueDatabase>, KeyValueError> {
+ Ok(KeyValueDatabase::new(result.0, result.1)?)
+ }
+}
+
+impl Task for GetOrCreateTask {
+ fn run(&self) {
+ // We do the work within a closure that returns a Result so we can
+ // use the ? operator to simplify the implementation.
+ self.result
+ .store(Some(|| -> Result<RkvStoreTuple, KeyValueError> {
+ let store;
+ let mut manager = Manager::singleton().write()?;
+ // Note that path canonicalization is diabled to work around crashes on Fennec:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1531887
+ let path = Path::new(str::from_utf8(&self.path)?);
+ let rkv = manager.get_or_create(path, Rkv::new::<SafeMode>)?;
+ {
+ let env = rkv.read()?;
+ let load_ratio = env.load_ratio()?.unwrap_or(0.0);
+ if load_ratio > RESIZE_RATIO {
+ active_resize(&env)?;
+ }
+ store = env.open_single(str::from_utf8(&self.name)?, StoreOptions::create())?;
+ }
+ Ok((rkv, store))
+ }()));
+ }
+
+ task_done!(value);
+}
+
+pub struct PutTask {
+ callback: AtomicCell<Option<ThreadBoundRefPtr<nsIKeyValueVoidCallback>>>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ key: nsCString,
+ value: OwnedValue,
+ result: AtomicCell<Option<Result<(), KeyValueError>>>,
+}
+
+impl PutTask {
+ pub fn new(
+ callback: RefPtr<nsIKeyValueVoidCallback>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ key: nsCString,
+ value: OwnedValue,
+ ) -> PutTask {
+ PutTask {
+ callback: AtomicCell::new(Some(ThreadBoundRefPtr::new(callback))),
+ rkv,
+ store,
+ key,
+ value,
+ result: AtomicCell::default(),
+ }
+ }
+}
+
+impl Task for PutTask {
+ fn run(&self) {
+ // We do the work within a closure that returns a Result so we can
+ // use the ? operator to simplify the implementation.
+ self.result.store(Some(|| -> Result<(), KeyValueError> {
+ let env = self.rkv.read()?;
+ let key = str::from_utf8(&self.key)?;
+ let v = Value::from(&self.value);
+ let mut resized = false;
+
+ // Use a loop here in case we want to retry from a recoverable
+ // error such as `StoreError::MapFull`.
+ loop {
+ let mut writer = env.write()?;
+
+ match self.store.put(&mut writer, key, &v) {
+ Ok(_) => (),
+
+ // Only handle the first MapFull error via passive resizing.
+ // Propogate the subsequent MapFull error.
+ Err(StoreError::MapFull) if !resized => {
+ // abort the failed transaction for resizing.
+ writer.abort();
+
+ // calculate the size of pairs and resize the store accordingly.
+ let pair_size =
+ key.len() + v.serialized_size().map_err(StoreError::from)? as usize;
+ let wanted = round_to_pagesize(pair_size);
+ passive_resize(&env, wanted)?;
+ resized = true;
+ continue;
+ }
+
+ Err(err) => return Err(KeyValueError::StoreError(err)),
+ }
+
+ // Ignore errors caused by simultaneous access.
+ // We intend to investigate/revert this in bug 1810212.
+ match writer.commit() {
+ Err(StoreError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => {
+ // Explicitly ignore errors from simultaneous access.
+ }
+ Err(e) => return Err(From::from(e)),
+ _ => (),
+ };
+ break;
+ }
+
+ Ok(())
+ }()));
+ }
+
+ task_done!(void);
+}
+
+pub struct WriteManyTask {
+ callback: AtomicCell<Option<ThreadBoundRefPtr<nsIKeyValueVoidCallback>>>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ pairs: Vec<(nsCString, Option<OwnedValue>)>,
+ result: AtomicCell<Option<Result<(), KeyValueError>>>,
+}
+
+impl WriteManyTask {
+ pub fn new(
+ callback: RefPtr<nsIKeyValueVoidCallback>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ pairs: Vec<(nsCString, Option<OwnedValue>)>,
+ ) -> WriteManyTask {
+ WriteManyTask {
+ callback: AtomicCell::new(Some(ThreadBoundRefPtr::new(callback))),
+ rkv,
+ store,
+ pairs,
+ result: AtomicCell::default(),
+ }
+ }
+
+ fn calc_pair_size(&self) -> Result<usize, StoreError> {
+ let mut total = 0;
+
+ for (key, value) in self.pairs.iter() {
+ if let Some(val) = value {
+ total += key.len();
+ total += Value::from(val)
+ .serialized_size()
+ .map_err(StoreError::from)? as usize;
+ }
+ }
+
+ Ok(total)
+ }
+}
+
+impl Task for WriteManyTask {
+ fn run(&self) {
+ // We do the work within a closure that returns a Result so we can
+ // use the ? operator to simplify the implementation.
+ self.result.store(Some(|| -> Result<(), KeyValueError> {
+ let env = self.rkv.read()?;
+ let mut resized = false;
+
+ // Use a loop here in case we want to retry from a recoverable
+ // error such as `StoreError::MapFull`.
+ 'outer: loop {
+ let mut writer = env.write()?;
+
+ for (key, value) in self.pairs.iter() {
+ let key = str::from_utf8(key)?;
+ match value {
+ // To put.
+ Some(val) => {
+ match self.store.put(&mut writer, key, &Value::from(val)) {
+ Ok(_) => (),
+
+ // Only handle the first MapFull error via passive resizing.
+ // Propogate the subsequent MapFull error.
+ Err(StoreError::MapFull) if !resized => {
+ // Abort the failed transaction for resizing.
+ writer.abort();
+
+ // Calculate the size of pairs and resize accordingly.
+ let pair_size = self.calc_pair_size()?;
+ let wanted = round_to_pagesize(pair_size);
+ passive_resize(&env, wanted)?;
+ resized = true;
+ continue 'outer;
+ }
+
+ Err(err) => return Err(KeyValueError::StoreError(err)),
+ }
+ }
+ // To delete.
+ None => {
+ match self.store.delete(&mut writer, key) {
+ Ok(_) => (),
+
+ // RKV fails with an error if the key to delete wasn't found,
+ // and Rkv returns that error, but we ignore it, as we expect most
+ // of our consumers to want this behavior.
+ Err(StoreError::KeyValuePairNotFound) => (),
+
+ Err(err) => return Err(KeyValueError::StoreError(err)),
+ };
+ }
+ }
+ }
+
+ // Ignore errors caused by simultaneous access.
+ // We intend to investigate/revert this in bug 1810212.
+ match writer.commit() {
+ Err(StoreError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => {
+ // Explicitly ignore errors from simultaneous access.
+ }
+ Err(e) => return Err(From::from(e)),
+ _ => (),
+ };
+ break; // 'outer: loop
+ }
+
+ Ok(())
+ }()));
+ }
+
+ task_done!(void);
+}
+
+pub struct GetTask {
+ callback: AtomicCell<Option<ThreadBoundRefPtr<nsIKeyValueVariantCallback>>>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ key: nsCString,
+ default_value: Option<OwnedValue>,
+ result: AtomicCell<Option<Result<Option<OwnedValue>, KeyValueError>>>,
+}
+
+impl GetTask {
+ pub fn new(
+ callback: RefPtr<nsIKeyValueVariantCallback>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ key: nsCString,
+ default_value: Option<OwnedValue>,
+ ) -> GetTask {
+ GetTask {
+ callback: AtomicCell::new(Some(ThreadBoundRefPtr::new(callback))),
+ rkv,
+ store,
+ key,
+ default_value,
+ result: AtomicCell::default(),
+ }
+ }
+
+ fn convert(&self, result: Option<OwnedValue>) -> Result<RefPtr<nsIVariant>, KeyValueError> {
+ Ok(match result {
+ Some(val) => owned_to_variant(val)?,
+ None => ().into_variant(),
+ })
+ }
+}
+
+impl Task for GetTask {
+ fn run(&self) {
+ // We do the work within a closure that returns a Result so we can
+ // use the ? operator to simplify the implementation.
+ self.result
+ .store(Some(|| -> Result<Option<OwnedValue>, KeyValueError> {
+ let key = str::from_utf8(&self.key)?;
+ let env = self.rkv.read()?;
+ let reader = env.read()?;
+ let value = self.store.get(&reader, key)?;
+
+ Ok(match value {
+ Some(value) => Some(OwnedValue::from(&value)),
+ None => match self.default_value {
+ Some(ref val) => Some(val.clone()),
+ None => None,
+ },
+ })
+ }()));
+ }
+
+ task_done!(value);
+}
+
+pub struct HasTask {
+ callback: AtomicCell<Option<ThreadBoundRefPtr<nsIKeyValueVariantCallback>>>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ key: nsCString,
+ result: AtomicCell<Option<Result<bool, KeyValueError>>>,
+}
+
+impl HasTask {
+ pub fn new(
+ callback: RefPtr<nsIKeyValueVariantCallback>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ key: nsCString,
+ ) -> HasTask {
+ HasTask {
+ callback: AtomicCell::new(Some(ThreadBoundRefPtr::new(callback))),
+ rkv,
+ store,
+ key,
+ result: AtomicCell::default(),
+ }
+ }
+
+ fn convert(&self, result: bool) -> Result<RefPtr<nsIVariant>, KeyValueError> {
+ Ok(result.into_variant())
+ }
+}
+
+impl Task for HasTask {
+ fn run(&self) {
+ // We do the work within a closure that returns a Result so we can
+ // use the ? operator to simplify the implementation.
+ self.result.store(Some(|| -> Result<bool, KeyValueError> {
+ let key = str::from_utf8(&self.key)?;
+ let env = self.rkv.read()?;
+ let reader = env.read()?;
+ let value = self.store.get(&reader, key)?;
+ Ok(value.is_some())
+ }()));
+ }
+
+ task_done!(value);
+}
+
+pub struct DeleteTask {
+ callback: AtomicCell<Option<ThreadBoundRefPtr<nsIKeyValueVoidCallback>>>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ key: nsCString,
+ result: AtomicCell<Option<Result<(), KeyValueError>>>,
+}
+
+impl DeleteTask {
+ pub fn new(
+ callback: RefPtr<nsIKeyValueVoidCallback>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ key: nsCString,
+ ) -> DeleteTask {
+ DeleteTask {
+ callback: AtomicCell::new(Some(ThreadBoundRefPtr::new(callback))),
+ rkv,
+ store,
+ key,
+ result: AtomicCell::default(),
+ }
+ }
+}
+
+impl Task for DeleteTask {
+ fn run(&self) {
+ // We do the work within a closure that returns a Result so we can
+ // use the ? operator to simplify the implementation.
+ self.result.store(Some(|| -> Result<(), KeyValueError> {
+ let key = str::from_utf8(&self.key)?;
+ let env = self.rkv.read()?;
+ let mut writer = env.write()?;
+
+ match self.store.delete(&mut writer, key) {
+ Ok(_) => (),
+
+ // RKV fails with an error if the key to delete wasn't found,
+ // and Rkv returns that error, but we ignore it, as we expect most
+ // of our consumers to want this behavior.
+ Err(StoreError::KeyValuePairNotFound) => (),
+
+ Err(err) => return Err(KeyValueError::StoreError(err)),
+ };
+
+ // Ignore errors caused by simultaneous access.
+ // We intend to investigate/revert this in bug 1810212.
+ match writer.commit() {
+ Err(StoreError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => {
+ // Explicitly ignore errors from simultaneous access.
+ }
+ Err(e) => return Err(From::from(e)),
+ _ => (),
+ };
+
+ Ok(())
+ }()));
+ }
+
+ task_done!(void);
+}
+
+pub struct ClearTask {
+ callback: AtomicCell<Option<ThreadBoundRefPtr<nsIKeyValueVoidCallback>>>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ result: AtomicCell<Option<Result<(), KeyValueError>>>,
+}
+
+impl ClearTask {
+ pub fn new(
+ callback: RefPtr<nsIKeyValueVoidCallback>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ ) -> ClearTask {
+ ClearTask {
+ callback: AtomicCell::new(Some(ThreadBoundRefPtr::new(callback))),
+ rkv,
+ store,
+ result: AtomicCell::default(),
+ }
+ }
+}
+
+impl Task for ClearTask {
+ fn run(&self) {
+ // We do the work within a closure that returns a Result so we can
+ // use the ? operator to simplify the implementation.
+ self.result.store(Some(|| -> Result<(), KeyValueError> {
+ let env = self.rkv.read()?;
+ let mut writer = env.write()?;
+ self.store.clear(&mut writer)?;
+ // Ignore errors caused by simultaneous access.
+ // We intend to investigate/revert this in bug 1810212.
+ match writer.commit() {
+ Err(StoreError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => {
+ // Explicitly ignore errors from simultaneous access.
+ }
+ Err(e) => return Err(From::from(e)),
+ _ => (),
+ };
+
+ Ok(())
+ }()));
+ }
+
+ task_done!(void);
+}
+
+pub struct EnumerateTask {
+ callback: AtomicCell<Option<ThreadBoundRefPtr<nsIKeyValueEnumeratorCallback>>>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ from_key: nsCString,
+ to_key: nsCString,
+ result: AtomicCell<Option<Result<Vec<KeyValuePairResult>, KeyValueError>>>,
+}
+
+impl EnumerateTask {
+ pub fn new(
+ callback: RefPtr<nsIKeyValueEnumeratorCallback>,
+ rkv: Arc<RwLock<Rkv>>,
+ store: SingleStore,
+ from_key: nsCString,
+ to_key: nsCString,
+ ) -> EnumerateTask {
+ EnumerateTask {
+ callback: AtomicCell::new(Some(ThreadBoundRefPtr::new(callback))),
+ rkv,
+ store,
+ from_key,
+ to_key,
+ result: AtomicCell::default(),
+ }
+ }
+
+ fn convert(
+ &self,
+ result: Vec<KeyValuePairResult>,
+ ) -> Result<RefPtr<KeyValueEnumerator>, KeyValueError> {
+ Ok(KeyValueEnumerator::new(result))
+ }
+}
+
+impl Task for EnumerateTask {
+ fn run(&self) {
+ // We do the work within a closure that returns a Result so we can
+ // use the ? operator to simplify the implementation.
+ self.result.store(Some(
+ || -> Result<Vec<KeyValuePairResult>, KeyValueError> {
+ let env = self.rkv.read()?;
+ let reader = env.read()?;
+ let from_key = str::from_utf8(&self.from_key)?;
+ let to_key = str::from_utf8(&self.to_key)?;
+
+ let iterator = if from_key.is_empty() {
+ self.store.iter_start(&reader)?
+ } else {
+ self.store.iter_from(&reader, &from_key)?
+ };
+
+ // Ideally, we'd enumerate pairs lazily, as the consumer calls
+ // nsIKeyValueEnumerator.getNext(), which calls our
+ // KeyValueEnumerator.get_next() implementation. But KeyValueEnumerator
+ // can't reference the Iter because Rust "cannot #[derive(xpcom)]
+ // on a generic type," and the Iter requires a lifetime parameter,
+ // which would make KeyValueEnumerator generic.
+ //
+ // Our fallback approach is to eagerly collect the iterator
+ // into a collection that KeyValueEnumerator owns. Fixing this so we
+ // enumerate pairs lazily is bug 1499252.
+ let pairs: Vec<KeyValuePairResult> = iterator
+ // Convert the key to a string so we can compare it to the "to" key.
+ // For forward compatibility, we don't fail here if we can't convert
+ // a key to UTF-8. Instead, we store the Err in the collection
+ // and fail lazily in KeyValueEnumerator.get_next().
+ .map(|result| match result {
+ Ok((key, val)) => Ok((str::from_utf8(&key), val)),
+ Err(err) => Err(err),
+ })
+ // Stop iterating once we reach the to_key, if any.
+ .take_while(|result| match result {
+ Ok((key, _val)) => {
+ if to_key.is_empty() {
+ true
+ } else {
+ match *key {
+ Ok(key) => key < to_key,
+ Err(_err) => true,
+ }
+ }
+ }
+ Err(_) => true,
+ })
+ // Convert the key/value pair to owned.
+ .map(|result| match result {
+ Ok((key, val)) => match (key, val) {
+ (Ok(key), val) => Ok((key.to_owned(), OwnedValue::from(&val))),
+ (Err(err), _) => Err(err.into()),
+ },
+ Err(err) => Err(KeyValueError::StoreError(err)),
+ })
+ .collect();
+
+ Ok(pairs)
+ }(),
+ ));
+ }
+
+ task_done!(value);
+}
diff --git a/toolkit/components/kvstore/test/xpcshell/data/test-env-32/data.mdb b/toolkit/components/kvstore/test/xpcshell/data/test-env-32/data.mdb
new file mode 100644
index 0000000000..75e555baab
--- /dev/null
+++ b/toolkit/components/kvstore/test/xpcshell/data/test-env-32/data.mdb
Binary files differ
diff --git a/toolkit/components/kvstore/test/xpcshell/data/test-env-32/lock.mdb b/toolkit/components/kvstore/test/xpcshell/data/test-env-32/lock.mdb
new file mode 100644
index 0000000000..76bed60632
--- /dev/null
+++ b/toolkit/components/kvstore/test/xpcshell/data/test-env-32/lock.mdb
Binary files differ
diff --git a/toolkit/components/kvstore/test/xpcshell/data/test-env-64/data.mdb b/toolkit/components/kvstore/test/xpcshell/data/test-env-64/data.mdb
new file mode 100644
index 0000000000..d6400c69e7
--- /dev/null
+++ b/toolkit/components/kvstore/test/xpcshell/data/test-env-64/data.mdb
Binary files differ
diff --git a/toolkit/components/kvstore/test/xpcshell/data/test-env-64/lock.mdb b/toolkit/components/kvstore/test/xpcshell/data/test-env-64/lock.mdb
new file mode 100644
index 0000000000..e083a773e9
--- /dev/null
+++ b/toolkit/components/kvstore/test/xpcshell/data/test-env-64/lock.mdb
Binary files differ
diff --git a/toolkit/components/kvstore/test/xpcshell/make-test-env.js b/toolkit/components/kvstore/test/xpcshell/make-test-env.js
new file mode 100644
index 0000000000..b40632a359
--- /dev/null
+++ b/toolkit/components/kvstore/test/xpcshell/make-test-env.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// An xpcshell script to create a test database. Useful for creating
+// the test-env-32 and test-env-64 databases that we use to test migration
+// of databases across architecture changes.
+//
+// To create a test database, simply run this script using xpcshell:
+//
+// path/to/xpcshell path/to/make-test-env.js
+//
+// The script will create the test-env-32 or test-env-64 directory
+// (depending on the current architecture) in the current working directory,
+// create a database called "db" within it, and populate the database
+// with sample data.
+//
+// Note: you don't necessarily need to run this script on every architecture
+// for which you'd like to create a database. Once you have a database for one
+// architecture, you can use the mdb_dump and mdb_load utilities (if available
+// for your systems) to create them for others. To do so, first dump the data
+// on the original architecture:
+//
+// mdb_dump -s db path/to/original/test-env-dir > path/to/dump.txt
+//
+// Then load the data on the new architecture:
+//
+// mkdir path/to/new/test-env-dir
+// mdb_load -s db path/to/dump.txt
+
+"use strict";
+
+const { KeyValueService } = ChromeUtils.importESModule(
+ "resource://gre/modules/kvstore.sys.mjs"
+);
+
+(async function () {
+ const currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path;
+ const testEnvDir = Services.appinfo.is64Bit ? "test-env-64" : "test-env-32";
+ const testEnvPath = PathUtils.join(currentDir, testEnvDir);
+ await IOUtils.makeDirectory(testEnvPath);
+
+ const database = await KeyValueService.getOrCreate(testEnvPath, "db");
+ await database.put("int-key", 1234);
+ await database.put("double-key", 56.78);
+ await database.put("string-key", "Héllo, wőrld!");
+ await database.put("bool-key", true);
+
+ scriptDone = true;
+})();
+
+// Do async processing until the async function call completes.
+// From <https://developer.mozilla.org/en/XPConnect/xpcshell/HOWTO>
+let scriptDone = false;
+const mainThread = Services.tm.currentThread;
+while (!scriptDone) {
+ mainThread.processNextEvent(true);
+}
+while (mainThread.hasPendingEvents()) {
+ mainThread.processNextEvent(true);
+}
diff --git a/toolkit/components/kvstore/test/xpcshell/test_kvstore.js b/toolkit/components/kvstore/test/xpcshell/test_kvstore.js
new file mode 100644
index 0000000000..363feaa43a
--- /dev/null
+++ b/toolkit/components/kvstore/test/xpcshell/test_kvstore.js
@@ -0,0 +1,586 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { KeyValueService } = ChromeUtils.importESModule(
+ "resource://gre/modules/kvstore.sys.mjs"
+);
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+async function makeDatabaseDir(name) {
+ const databaseDir = PathUtils.join(PathUtils.profileDir, name);
+ await IOUtils.makeDirectory(databaseDir);
+ return databaseDir;
+}
+
+const gKeyValueService = Cc["@mozilla.org/key-value-service;1"].getService(
+ Ci.nsIKeyValueService
+);
+
+add_task(async function getService() {
+ Assert.ok(gKeyValueService);
+});
+
+add_task(async function getOrCreate() {
+ const databaseDir = await makeDatabaseDir("getOrCreate");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+ Assert.ok(database);
+
+ // Test creating a database with a nonexistent path.
+ const nonexistentDir = PathUtils.join(PathUtils.profileDir, "nonexistent");
+ await Assert.rejects(
+ KeyValueService.getOrCreate(nonexistentDir, "db"),
+ /UnsuitableEnvironmentPath/
+ );
+
+ // Test creating a database with a non-normalized but fully-qualified path.
+ let nonNormalizedDir = await makeDatabaseDir("non-normalized");
+ nonNormalizedDir = [nonNormalizedDir, "..", ".", "non-normalized"].join(
+ Services.appinfo.OS === "WINNT" ? "\\" : "/"
+ );
+ Assert.ok(await KeyValueService.getOrCreate(nonNormalizedDir, "db"));
+});
+
+add_task(async function putGetHasDelete() {
+ const databaseDir = await makeDatabaseDir("putGetHasDelete");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+
+ // Getting key/value pairs that don't exist (yet) returns default values
+ // or null, depending on whether you specify a default value.
+ Assert.strictEqual(await database.get("int-key", 1), 1);
+ Assert.strictEqual(await database.get("double-key", 1.1), 1.1);
+ Assert.strictEqual(await database.get("string-key", ""), "");
+ Assert.strictEqual(await database.get("bool-key", false), false);
+ Assert.strictEqual(await database.get("int-key"), null);
+ Assert.strictEqual(await database.get("double-key"), null);
+ Assert.strictEqual(await database.get("string-key"), null);
+ Assert.strictEqual(await database.get("bool-key"), null);
+
+ // The put method succeeds without returning a value.
+ Assert.strictEqual(await database.put("int-key", 1234), undefined);
+ Assert.strictEqual(await database.put("double-key", 56.78), undefined);
+ Assert.strictEqual(
+ await database.put("string-key", "Héllo, wőrld!"),
+ undefined
+ );
+ Assert.strictEqual(await database.put("bool-key", true), undefined);
+
+ // Getting key/value pairs that exist returns the expected values.
+ Assert.strictEqual(await database.get("int-key", 1), 1234);
+ Assert.strictEqual(await database.get("double-key", 1.1), 56.78);
+ Assert.strictEqual(await database.get("string-key", ""), "Héllo, wőrld!");
+ Assert.strictEqual(await database.get("bool-key", false), true);
+ Assert.strictEqual(await database.get("int-key"), 1234);
+ Assert.strictEqual(await database.get("double-key"), 56.78);
+ Assert.strictEqual(await database.get("string-key"), "Héllo, wőrld!");
+ Assert.strictEqual(await database.get("bool-key"), true);
+
+ // The has() method works as expected for both existing and non-existent keys.
+ Assert.strictEqual(await database.has("int-key"), true);
+ Assert.strictEqual(await database.has("double-key"), true);
+ Assert.strictEqual(await database.has("string-key"), true);
+ Assert.strictEqual(await database.has("bool-key"), true);
+ Assert.strictEqual(await database.has("nonexistent-key"), false);
+
+ // The delete() method succeeds without returning a value.
+ Assert.strictEqual(await database.delete("int-key"), undefined);
+ Assert.strictEqual(await database.delete("double-key"), undefined);
+ Assert.strictEqual(await database.delete("string-key"), undefined);
+ Assert.strictEqual(await database.delete("bool-key"), undefined);
+
+ // The has() method works as expected for a deleted key.
+ Assert.strictEqual(await database.has("int-key"), false);
+ Assert.strictEqual(await database.has("double-key"), false);
+ Assert.strictEqual(await database.has("string-key"), false);
+ Assert.strictEqual(await database.has("bool-key"), false);
+
+ // Getting key/value pairs that were deleted returns default values.
+ Assert.strictEqual(await database.get("int-key", 1), 1);
+ Assert.strictEqual(await database.get("double-key", 1.1), 1.1);
+ Assert.strictEqual(await database.get("string-key", ""), "");
+ Assert.strictEqual(await database.get("bool-key", false), false);
+ Assert.strictEqual(await database.get("int-key"), null);
+ Assert.strictEqual(await database.get("double-key"), null);
+ Assert.strictEqual(await database.get("string-key"), null);
+ Assert.strictEqual(await database.get("bool-key"), null);
+});
+
+add_task(async function putWithResizing() {
+ const databaseDir = await makeDatabaseDir("putWithResizing");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+
+ // The default store size is 1MB, putting key/value pairs bigger than that
+ // would trigger auto resizing.
+ const base = "A humongous string in 32 bytes!!";
+ const val1M = base.repeat(32768);
+ const val2M = val1M.repeat(2);
+ Assert.strictEqual(await database.put("A-1M-value", val1M), undefined);
+ Assert.strictEqual(await database.put("A-2M-value", val2M), undefined);
+ Assert.strictEqual(await database.put("A-32B-value", base), undefined);
+
+ Assert.strictEqual(await database.get("A-1M-value"), val1M);
+ Assert.strictEqual(await database.get("A-2M-value"), val2M);
+ Assert.strictEqual(await database.get("A-32B-value"), base);
+});
+
+add_task(async function largeNumbers() {
+ const databaseDir = await makeDatabaseDir("largeNumbers");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+
+ const MAX_INT_VARIANT = Math.pow(2, 31) - 1;
+ const MIN_DOUBLE_VARIANT = Math.pow(2, 31);
+
+ await database.put("max-int-variant", MAX_INT_VARIANT);
+ await database.put("min-double-variant", MIN_DOUBLE_VARIANT);
+ await database.put("max-safe-integer", Number.MAX_SAFE_INTEGER);
+ await database.put("min-safe-integer", Number.MIN_SAFE_INTEGER);
+ await database.put("max-value", Number.MAX_VALUE);
+ await database.put("min-value", Number.MIN_VALUE);
+
+ Assert.strictEqual(await database.get("max-int-variant"), MAX_INT_VARIANT);
+ Assert.strictEqual(
+ await database.get("min-double-variant"),
+ MIN_DOUBLE_VARIANT
+ );
+ Assert.strictEqual(
+ await database.get("max-safe-integer"),
+ Number.MAX_SAFE_INTEGER
+ );
+ Assert.strictEqual(
+ await database.get("min-safe-integer"),
+ Number.MIN_SAFE_INTEGER
+ );
+ Assert.strictEqual(await database.get("max-value"), Number.MAX_VALUE);
+ Assert.strictEqual(await database.get("min-value"), Number.MIN_VALUE);
+});
+
+add_task(async function extendedCharacterKey() {
+ const databaseDir = await makeDatabaseDir("extendedCharacterKey");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+
+ // Ensure that we can use extended character (i.e. non-ASCII) strings as keys.
+
+ await database.put("Héllo, wőrld!", 1);
+ Assert.strictEqual(await database.has("Héllo, wőrld!"), true);
+ Assert.strictEqual(await database.get("Héllo, wőrld!"), 1);
+
+ const enumerator = await database.enumerate();
+ const { key } = enumerator.getNext();
+ Assert.strictEqual(key, "Héllo, wőrld!");
+
+ await database.delete("Héllo, wőrld!");
+});
+
+add_task(async function clear() {
+ const databaseDir = await makeDatabaseDir("clear");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+
+ await database.put("int-key", 1234);
+ await database.put("double-key", 56.78);
+ await database.put("string-key", "Héllo, wőrld!");
+ await database.put("bool-key", true);
+
+ Assert.strictEqual(await database.clear(), undefined);
+ Assert.strictEqual(await database.has("int-key"), false);
+ Assert.strictEqual(await database.has("double-key"), false);
+ Assert.strictEqual(await database.has("string-key"), false);
+ Assert.strictEqual(await database.has("bool-key"), false);
+});
+
+add_task(async function writeManyFailureCases() {
+ const databaseDir = await makeDatabaseDir("writeManyFailureCases");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+
+ Assert.throws(() => database.writeMany(), /unexpected argument/);
+ Assert.throws(() => database.writeMany("foo"), /unexpected argument/);
+ Assert.throws(() => database.writeMany(["foo"]), /unexpected argument/);
+});
+
+add_task(async function writeManyPutOnly() {
+ const databaseDir = await makeDatabaseDir("writeMany");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+
+ async function test_helper(pairs) {
+ Assert.strictEqual(await database.writeMany(pairs), undefined);
+ Assert.strictEqual(await database.get("int-key"), 1234);
+ Assert.strictEqual(await database.get("double-key"), 56.78);
+ Assert.strictEqual(await database.get("string-key"), "Héllo, wőrld!");
+ Assert.strictEqual(await database.get("bool-key"), true);
+ await database.clear();
+ }
+
+ // writeMany with an empty object is OK
+ Assert.strictEqual(await database.writeMany({}), undefined);
+
+ // writeMany with an object
+ const pairs = {
+ "int-key": 1234,
+ "double-key": 56.78,
+ "string-key": "Héllo, wőrld!",
+ "bool-key": true,
+ };
+ await test_helper(pairs);
+
+ // writeMany with an array of pairs
+ const arrayPairs = [
+ ["int-key", 1234],
+ ["double-key", 56.78],
+ ["string-key", "Héllo, wőrld!"],
+ ["bool-key", true],
+ ];
+ await test_helper(arrayPairs);
+
+ // writeMany with a key/value generator
+ function* pairMaker() {
+ yield ["int-key", 1234];
+ yield ["double-key", 56.78];
+ yield ["string-key", "Héllo, wőrld!"];
+ yield ["bool-key", true];
+ }
+ await test_helper(pairMaker());
+
+ // writeMany with a map
+ const mapPairs = new Map(arrayPairs);
+ await test_helper(mapPairs);
+});
+
+add_task(async function writeManyLargePairsWithResizing() {
+ const databaseDir = await makeDatabaseDir("writeManyWithResizing");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+
+ // The default store size is 1MB, putting key/value pairs bigger than that
+ // would trigger auto resizing.
+ const base = "A humongous string in 32 bytes!!";
+ const val1M = base.repeat(32768);
+ const val2M = val1M.repeat(2);
+
+ // writeMany with an object
+ const pairs = {
+ "A-1M-value": val1M,
+ "A-32B-value": base,
+ "A-2M-value": val2M,
+ };
+
+ Assert.strictEqual(await database.writeMany(pairs), undefined);
+
+ Assert.strictEqual(await database.get("A-1M-value"), val1M);
+ Assert.strictEqual(await database.get("A-2M-value"), val2M);
+ Assert.strictEqual(await database.get("A-32B-value"), base);
+});
+
+add_task(async function writeManySmallPairsWithResizing() {
+ const databaseDir = await makeDatabaseDir("writeManyWithResizing");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+
+ // The default store size is 1MB, putting key/value pairs bigger than that
+ // would trigger auto resizing.
+ const base = "A humongous string in 32 bytes!!";
+ const val1K = base.repeat(32);
+ // writeMany with a key/value generator
+ function* pairMaker() {
+ for (let i = 0; i < 1024; i++) {
+ yield [`key-${i}`, val1K];
+ }
+ }
+
+ Assert.strictEqual(await database.writeMany(pairMaker()), undefined);
+ for (let i = 0; i < 1024; i++) {
+ Assert.ok(await database.has(`key-${i}`));
+ }
+});
+
+add_task(async function writeManyDeleteOnly() {
+ const databaseDir = await makeDatabaseDir("writeManyDeletesOnly");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+
+ // writeMany with an object
+ const pairs = {
+ "int-key": 1234,
+ "double-key": 56.78,
+ "string-key": "Héllo, wőrld!",
+ "bool-key": true,
+ };
+
+ async function test_helper(deletes) {
+ Assert.strictEqual(await database.writeMany(pairs), undefined);
+ Assert.strictEqual(await database.writeMany(deletes), undefined);
+ Assert.strictEqual(await database.get("int-key"), null);
+ Assert.strictEqual(await database.get("double-key"), null);
+ Assert.strictEqual(await database.get("string-key"), null);
+ Assert.strictEqual(await database.get("bool-key"), null);
+ }
+
+ // writeMany with an empty object is OK
+ Assert.strictEqual(await database.writeMany({}), undefined);
+
+ // writeMany with an object
+ await test_helper({
+ "int-key": null,
+ "double-key": null,
+ "string-key": null,
+ "bool-key": null,
+ });
+
+ // writeMany with an array of pairs
+ const arrayPairs = [
+ ["int-key", null],
+ ["double-key", null],
+ ["string-key", null],
+ ["bool-key", null],
+ ];
+ await test_helper(arrayPairs);
+
+ // writeMany with a key/value generator
+ function* pairMaker() {
+ yield ["int-key", null];
+ yield ["double-key", null];
+ yield ["string-key", null];
+ yield ["bool-key", null];
+ }
+ await test_helper(pairMaker());
+
+ // writeMany with a map
+ const mapPairs = new Map(arrayPairs);
+ await test_helper(mapPairs);
+});
+
+add_task(async function writeManyPutDelete() {
+ const databaseDir = await makeDatabaseDir("writeManyPutDelete");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+
+ await database.writeMany([
+ ["key1", "val1"],
+ ["key3", "val3"],
+ ["key4", "val4"],
+ ["key5", "val5"],
+ ]);
+
+ await database.writeMany([
+ ["key2", "val2"],
+ ["key4", null],
+ ["key5", null],
+ ]);
+
+ Assert.strictEqual(await database.get("key1"), "val1");
+ Assert.strictEqual(await database.get("key2"), "val2");
+ Assert.strictEqual(await database.get("key3"), "val3");
+ Assert.strictEqual(await database.get("key4"), null);
+ Assert.strictEqual(await database.get("key5"), null);
+
+ await database.clear();
+
+ await database.writeMany([
+ ["key1", "val1"],
+ ["key1", null],
+ ["key1", "val11"],
+ ["key1", null],
+ ["key2", null],
+ ["key2", "val2"],
+ ]);
+
+ Assert.strictEqual(await database.get("key1"), null);
+ Assert.strictEqual(await database.get("key2"), "val2");
+});
+
+add_task(async function getOrCreateNamedDatabases() {
+ const databaseDir = await makeDatabaseDir("getOrCreateNamedDatabases");
+
+ let fooDB = await KeyValueService.getOrCreate(databaseDir, "foo");
+ Assert.ok(fooDB, "retrieval of first named database works");
+
+ let barDB = await KeyValueService.getOrCreate(databaseDir, "bar");
+ Assert.ok(barDB, "retrieval of second named database works");
+
+ let bazDB = await KeyValueService.getOrCreate(databaseDir, "baz");
+ Assert.ok(bazDB, "retrieval of third named database works");
+
+ // Key/value pairs that are put into a database don't exist in others.
+ await bazDB.put("key", 1);
+ Assert.ok(!(await fooDB.has("key")), "the foo DB still doesn't have the key");
+ await fooDB.put("key", 2);
+ Assert.ok(!(await barDB.has("key")), "the bar DB still doesn't have the key");
+ await barDB.put("key", 3);
+ Assert.strictEqual(
+ await bazDB.get("key", 0),
+ 1,
+ "the baz DB has its KV pair"
+ );
+ Assert.strictEqual(
+ await fooDB.get("key", 0),
+ 2,
+ "the foo DB has its KV pair"
+ );
+ Assert.strictEqual(
+ await barDB.get("key", 0),
+ 3,
+ "the bar DB has its KV pair"
+ );
+
+ // Key/value pairs that are deleted from a database still exist in other DBs.
+ await bazDB.delete("key");
+ Assert.strictEqual(
+ await fooDB.get("key", 0),
+ 2,
+ "the foo DB still has its KV pair"
+ );
+ await fooDB.delete("key");
+ Assert.strictEqual(
+ await barDB.get("key", 0),
+ 3,
+ "the bar DB still has its KV pair"
+ );
+ await barDB.delete("key");
+});
+
+add_task(async function enumeration() {
+ const databaseDir = await makeDatabaseDir("enumeration");
+ const database = await KeyValueService.getOrCreate(databaseDir, "db");
+
+ await database.put("int-key", 1234);
+ await database.put("double-key", 56.78);
+ await database.put("string-key", "Héllo, wőrld!");
+ await database.put("bool-key", true);
+
+ async function test(fromKey, toKey, pairs) {
+ const enumerator = await database.enumerate(fromKey, toKey);
+
+ for (const pair of pairs) {
+ Assert.strictEqual(enumerator.hasMoreElements(), true);
+ const element = enumerator.getNext();
+ Assert.ok(element);
+ Assert.strictEqual(element.key, pair[0]);
+ Assert.strictEqual(element.value, pair[1]);
+ }
+
+ Assert.strictEqual(enumerator.hasMoreElements(), false);
+ Assert.throws(() => enumerator.getNext(), /NS_ERROR_FAILURE/);
+ }
+
+ // Test enumeration without specifying "from" and "to" keys, which should
+ // enumerate all of the pairs in the database. This test does so explicitly
+ // by passing "null", "undefined" or "" (empty string) arguments
+ // for those parameters. The iterator test below also tests this implicitly
+ // by not specifying arguments for those parameters.
+ await test(null, null, [
+ ["bool-key", true],
+ ["double-key", 56.78],
+ ["int-key", 1234],
+ ["string-key", "Héllo, wőrld!"],
+ ]);
+ await test(undefined, undefined, [
+ ["bool-key", true],
+ ["double-key", 56.78],
+ ["int-key", 1234],
+ ["string-key", "Héllo, wőrld!"],
+ ]);
+
+ // The implementation doesn't distinguish between a null/undefined value
+ // and an empty string, so enumerating pairs from "" to "" has the same effect
+ // as enumerating pairs without specifying from/to keys: it enumerates
+ // all of the pairs in the database.
+ await test("", "", [
+ ["bool-key", true],
+ ["double-key", 56.78],
+ ["int-key", 1234],
+ ["string-key", "Héllo, wőrld!"],
+ ]);
+
+ // Test enumeration from a key that doesn't exist and is lexicographically
+ // less than the least key in the database, which should enumerate
+ // all of the pairs in the database.
+ await test("aaaaa", null, [
+ ["bool-key", true],
+ ["double-key", 56.78],
+ ["int-key", 1234],
+ ["string-key", "Héllo, wőrld!"],
+ ]);
+
+ // Test enumeration from a key that doesn't exist and is lexicographically
+ // greater than the first key in the database, which should enumerate pairs
+ // whose key is greater than or equal to the specified key.
+ await test("ccccc", null, [
+ ["double-key", 56.78],
+ ["int-key", 1234],
+ ["string-key", "Héllo, wőrld!"],
+ ]);
+
+ // Test enumeration from a key that does exist, which should enumerate pairs
+ // whose key is greater than or equal to that key.
+ await test("int-key", null, [
+ ["int-key", 1234],
+ ["string-key", "Héllo, wőrld!"],
+ ]);
+
+ // Test enumeration from a key that doesn't exist and is lexicographically
+ // greater than the greatest test key in the database, which should enumerate
+ // none of the pairs in the database.
+ await test("zzzzz", null, []);
+
+ // Test enumeration to a key that doesn't exist and is lexicographically
+ // greater than the greatest test key in the database, which should enumerate
+ // all of the pairs in the database.
+ await test(null, "zzzzz", [
+ ["bool-key", true],
+ ["double-key", 56.78],
+ ["int-key", 1234],
+ ["string-key", "Héllo, wőrld!"],
+ ]);
+
+ // Test enumeration to a key that doesn't exist and is lexicographically
+ // less than the greatest test key in the database, which should enumerate
+ // pairs whose key is less than the specified key.
+ await test(null, "ppppp", [
+ ["bool-key", true],
+ ["double-key", 56.78],
+ ["int-key", 1234],
+ ]);
+
+ // Test enumeration to a key that does exist, which should enumerate pairs
+ // whose key is less than that key.
+ await test(null, "int-key", [
+ ["bool-key", true],
+ ["double-key", 56.78],
+ ]);
+
+ // Test enumeration to a key that doesn't exist and is lexicographically
+ // less than the least key in the database, which should enumerate
+ // none of the pairs in the database.
+ await test(null, "aaaaa", []);
+
+ // Test enumeration between intermediate keys that don't exist, which should
+ // enumerate the pairs whose keys lie in between them.
+ await test("ggggg", "ppppp", [["int-key", 1234]]);
+
+ // Test enumeration from a key that exists to the same key, which shouldn't
+ // enumerate any pairs, because the "to" key is exclusive.
+ await test("int-key", "int-key", []);
+
+ // Test enumeration from a greater key to a lesser one, which should
+ // enumerate none of the pairs in the database, even if the reverse ordering
+ // would enumerate some pairs. Consumers are responsible for ordering
+ // the "from" and "to" keys such that "from" is less than or equal to "to".
+ await test("ppppp", "ccccc", []);
+ await test("int-key", "ccccc", []);
+ await test("ppppp", "int-key", []);
+
+ const actual = {};
+ for (const { key, value } of await database.enumerate()) {
+ actual[key] = value;
+ }
+ Assert.deepEqual(actual, {
+ "bool-key": true,
+ "double-key": 56.78,
+ "int-key": 1234,
+ "string-key": "Héllo, wőrld!",
+ });
+
+ await database.delete("int-key");
+ await database.delete("double-key");
+ await database.delete("string-key");
+ await database.delete("bool-key");
+});
diff --git a/toolkit/components/kvstore/test/xpcshell/xpcshell.toml b/toolkit/components/kvstore/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..55489006a7
--- /dev/null
+++ b/toolkit/components/kvstore/test/xpcshell/xpcshell.toml
@@ -0,0 +1,5 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+support-files = ["data/**"]
+
+["test_kvstore.js"]