summaryrefslogtreecommitdiffstats
path: root/toolkit/components/xulstore
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/xulstore')
-rw-r--r--toolkit/components/xulstore/Cargo.toml20
-rw-r--r--toolkit/components/xulstore/XULStore.cpp108
-rw-r--r--toolkit/components/xulstore/XULStore.h56
-rw-r--r--toolkit/components/xulstore/components.conf35
-rw-r--r--toolkit/components/xulstore/moz.build44
-rw-r--r--toolkit/components/xulstore/new/XULStore.jsm107
-rw-r--r--toolkit/components/xulstore/nsIXULStore.idl100
-rw-r--r--toolkit/components/xulstore/old/XULStore.jsm329
-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
-rw-r--r--toolkit/components/xulstore/tests/chrome/chrome.ini5
-rw-r--r--toolkit/components/xulstore/tests/chrome/test_persistence.xhtml30
-rw-r--r--toolkit/components/xulstore/tests/chrome/window_persistence.xhtml67
-rw-r--r--toolkit/components/xulstore/tests/gtest/Cargo.toml7
-rw-r--r--toolkit/components/xulstore/tests/gtest/TestXULStore.cpp141
-rw-r--r--toolkit/components/xulstore/tests/gtest/moz.build11
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/test_XULStore.js150
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration.js85
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_data.js57
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_json.js42
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_profile_change.js56
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/xpcshell.ini13
26 files changed, 2550 insertions, 0 deletions
diff --git a/toolkit/components/xulstore/Cargo.toml b/toolkit/components/xulstore/Cargo.toml
new file mode 100644
index 0000000000..307020f33c
--- /dev/null
+++ b/toolkit/components/xulstore/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "xulstore"
+version = "0.1.0"
+authors = ["The Mozilla Project Developers"]
+license = "MPL-2.0"
+
+[dependencies]
+crossbeam-utils = "0.8"
+cstr = "0.2"
+libc = "0.2"
+log = "0.4"
+moz_task = { path = "../../../xpcom/rust/moz_task" }
+nsstring = { path = "../../../xpcom/rust/nsstring" }
+nserror = { path = "../../../xpcom/rust/nserror" }
+once_cell = "1"
+rkv = { version = "0.18", default-features = false, features=["no-canonicalize-path"] }
+serde_json = "1"
+tempfile = "3"
+thiserror = "1"
+xpcom = { path = "../../../xpcom/rust/xpcom" }
diff --git a/toolkit/components/xulstore/XULStore.cpp b/toolkit/components/xulstore/XULStore.cpp
new file mode 100644
index 0000000000..a544746c6e
--- /dev/null
+++ b/toolkit/components/xulstore/XULStore.cpp
@@ -0,0 +1,108 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/XULStore.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla {
+
+// The XULStore API is implemented in Rust and exposed to C++ via a set of
+// C functions with the "xulstore_" prefix. We declare them in this anonymous
+// namespace to prevent C++ code outside this file from accessing them,
+// as they are an internal implementation detail, and C++ code should use
+// the mozilla::XULStore::* functions and mozilla::XULStoreIterator class
+// declared in XULStore.h.
+namespace {
+extern "C" {
+void xulstore_new_service(nsIXULStore** result);
+nsresult xulstore_set_value(const nsAString* doc, const nsAString* id,
+ const nsAString* attr, const nsAString* value);
+nsresult xulstore_has_value(const nsAString* doc, const nsAString* id,
+ const nsAString* attr, bool* has_value);
+nsresult xulstore_get_value(const nsAString* doc, const nsAString* id,
+ const nsAString* attr, nsAString* value);
+nsresult xulstore_remove_value(const nsAString* doc, const nsAString* id,
+ const nsAString* attr);
+XULStoreIterator* xulstore_get_ids(const nsAString* doc, nsresult* result);
+XULStoreIterator* xulstore_get_attrs(const nsAString* doc, const nsAString* id,
+ nsresult* result);
+bool xulstore_iter_has_more(const XULStoreIterator*);
+nsresult xulstore_iter_get_next(XULStoreIterator*, nsAString* value);
+void xulstore_iter_free(XULStoreIterator* iterator);
+nsresult xulstore_shutdown();
+}
+
+// A static reference to the nsIXULStore singleton that JS uses to access
+// the store. Retrieved via mozilla::XULStore::GetService().
+static StaticRefPtr<nsIXULStore> sXULStore;
+} // namespace
+
+bool XULStoreIterator::HasMore() const { return xulstore_iter_has_more(this); }
+
+nsresult XULStoreIterator::GetNext(nsAString* item) {
+ return xulstore_iter_get_next(this, item);
+}
+
+void DefaultDelete<XULStoreIterator>::operator()(XULStoreIterator* ptr) const {
+ xulstore_iter_free(ptr);
+}
+
+namespace XULStore {
+already_AddRefed<nsIXULStore> GetService() {
+ nsCOMPtr<nsIXULStore> xulStore;
+
+ if (sXULStore) {
+ xulStore = sXULStore;
+ } else {
+ xulstore_new_service(getter_AddRefs(xulStore));
+ sXULStore = xulStore;
+ mozilla::ClearOnShutdown(&sXULStore);
+ }
+
+ return xulStore.forget();
+}
+
+nsresult SetValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr, const nsAString& value) {
+ return xulstore_set_value(&doc, &id, &attr, &value);
+}
+nsresult HasValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr, bool& has_value) {
+ return xulstore_has_value(&doc, &id, &attr, &has_value);
+}
+nsresult GetValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr, nsAString& value) {
+ return xulstore_get_value(&doc, &id, &attr, &value);
+}
+nsresult RemoveValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr) {
+ return xulstore_remove_value(&doc, &id, &attr);
+}
+nsresult GetIDs(const nsAString& doc, UniquePtr<XULStoreIterator>& iter) {
+ // We assign the value of the iter here in C++ via a return value
+ // rather than in the Rust function via an out parameter in order
+ // to ensure that any old value is deleted, since the UniquePtr's
+ // assignment operator won't delete the old value if the assignment
+ // happens in Rust.
+ nsresult result;
+ iter.reset(xulstore_get_ids(&doc, &result));
+ return result;
+}
+nsresult GetAttrs(const nsAString& doc, const nsAString& id,
+ UniquePtr<XULStoreIterator>& iter) {
+ // We assign the value of the iter here in C++ via a return value
+ // rather than in the Rust function via an out parameter in order
+ // to ensure that any old value is deleted, since the UniquePtr's
+ // assignment operator won't delete the old value if the assignment
+ // happens in Rust.
+ nsresult result;
+ iter.reset(xulstore_get_attrs(&doc, &id, &result));
+ return result;
+}
+nsresult Shutdown() { return xulstore_shutdown(); }
+
+}; // namespace XULStore
+}; // namespace mozilla
diff --git a/toolkit/components/xulstore/XULStore.h b/toolkit/components/xulstore/XULStore.h
new file mode 100644
index 0000000000..25b69ed12e
--- /dev/null
+++ b/toolkit/components/xulstore/XULStore.h
@@ -0,0 +1,56 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+/*
+ * This file declares the XULStore API for C++ via the mozilla::XULStore
+ * namespace and the mozilla::XULStoreIterator class. It also declares
+ * the mozilla::XULStore::GetService() function that the component manager
+ * uses to instantiate and retrieve the nsIXULStore singleton.
+ */
+
+#ifndef mozilla_XULStore_h
+#define mozilla_XULStore_h
+
+#include "nsIXULStore.h"
+
+namespace mozilla {
+class XULStoreIterator final {
+ public:
+ bool HasMore() const;
+ nsresult GetNext(nsAString* item);
+
+ private:
+ XULStoreIterator() = delete;
+ XULStoreIterator(const XULStoreIterator&) = delete;
+ XULStoreIterator& operator=(const XULStoreIterator&) = delete;
+ ~XULStoreIterator() = delete;
+};
+
+template <>
+class DefaultDelete<XULStoreIterator> {
+ public:
+ void operator()(XULStoreIterator* ptr) const;
+};
+
+namespace XULStore {
+// Instantiates and retrieves the nsIXULStore singleton that JS uses to access
+// the store. C++ code should use the mozilla::XULStore::* functions instead.
+already_AddRefed<nsIXULStore> GetService();
+
+nsresult SetValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr, const nsAString& value);
+nsresult HasValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr, bool& has_value);
+nsresult GetValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr, nsAString& value);
+nsresult RemoveValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr);
+nsresult GetIDs(const nsAString& doc, UniquePtr<XULStoreIterator>& iter);
+nsresult GetAttrs(const nsAString& doc, const nsAString& id,
+ UniquePtr<XULStoreIterator>& iter);
+nsresult Shutdown();
+}; // namespace XULStore
+}; // namespace mozilla
+
+#endif // mozilla_XULStore_h
diff --git a/toolkit/components/xulstore/components.conf b/toolkit/components/xulstore/components.conf
new file mode 100644
index 0000000000..2916108e65
--- /dev/null
+++ b/toolkit/components/xulstore/components.conf
@@ -0,0 +1,35 @@
+# -*- 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/.
+
+if defined('MOZ_NEW_XULSTORE'):
+ Classes = [
+ {
+ 'cid': '{be70bf11-0c28-4a02-a38c-0148538d42cf}',
+ 'contract_ids': ['@mozilla.org/xul/xulstore;1'],
+ 'type': 'nsIXULStore',
+ 'headers': ['mozilla/XULStore.h'],
+ 'singleton': True,
+ 'constructor': 'mozilla::XULStore::GetService',
+ },
+ {
+ 'js_name': 'xulStore',
+ 'cid': '{e8e12dba-b942-4c0d-aa21-2843cfc64529}',
+ 'contract_ids': ['@mozilla.org/xul/js-xulstore;1'],
+ 'jsm': 'resource://gre/modules/XULStore.jsm',
+ 'constructor': 'getXULStore',
+ },
+ ]
+else:
+ Classes = [
+ {
+ 'js_name': 'xulStore',
+ 'cid': '{6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea}',
+ 'contract_ids': ['@mozilla.org/xul/xulstore;1'],
+ 'interfaces': ['nsIXULStore'],
+ 'jsm': 'resource://gre/modules/XULStore.jsm',
+ 'constructor': 'XULStore',
+ },
+ ]
diff --git a/toolkit/components/xulstore/moz.build b/toolkit/components/xulstore/moz.build
new file mode 100644
index 0000000000..89064ce4d3
--- /dev/null
+++ b/toolkit/components/xulstore/moz.build
@@ -0,0 +1,44 @@
+# -*- 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", "Startup and Profile System")
+
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.ini"]
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]
+
+XPIDL_MODULE = "toolkit_xulstore"
+
+XPIDL_SOURCES += [
+ "nsIXULStore.idl",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+if CONFIG["MOZ_NEW_XULSTORE"]:
+ EXTRA_JS_MODULES += [
+ "new/XULStore.jsm",
+ ]
+
+ TEST_DIRS += [
+ "tests/gtest",
+ ]
+
+ EXPORTS.mozilla += [
+ "XULStore.h",
+ ]
+
+ UNIFIED_SOURCES += [
+ "XULStore.cpp",
+ ]
+
+ FINAL_LIBRARY = "xul"
+else:
+ EXTRA_JS_MODULES += [
+ "old/XULStore.jsm",
+ ]
diff --git a/toolkit/components/xulstore/new/XULStore.jsm b/toolkit/components/xulstore/new/XULStore.jsm
new file mode 100644
index 0000000000..16108eeb8b
--- /dev/null
+++ b/toolkit/components/xulstore/new/XULStore.jsm
@@ -0,0 +1,107 @@
+/* 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 strict";
+
+// This JS module wraps the nsIXULStore XPCOM service with useful abstractions.
+// In particular, it wraps the enumerators returned by getIDsEnumerator()
+// and getAttributeEnumerator() in JS objects that implement the iterable
+// protocol. It also implements the persist() method. JS consumers should use
+// this module rather than accessing nsIXULStore directly.
+
+const EXPORTED_SYMBOLS = ["XULStore", "getXULStore"];
+
+// Services.xulStore loads this module and returns its `XULStore` symbol
+// when this implementation of XULStore is enabled, so using it here
+// would loop infinitely. But the mozilla/use-services rule is a good
+// requiremnt for every other consumer of XULStore.
+// eslint-disable-next-line mozilla/use-services
+const xulStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore);
+
+// Enables logging.
+const debugMode = false;
+
+// Internal function for logging debug messages to the Error Console window
+function log(message) {
+ if (!debugMode) {
+ return;
+ }
+ console.log("XULStore: " + message);
+}
+
+const XULStore = {
+ setValue: xulStore.setValue,
+ hasValue: xulStore.hasValue,
+ getValue: xulStore.getValue,
+ removeValue: xulStore.removeValue,
+ removeDocument: xulStore.removeDocument,
+
+ /**
+ * Sets a value for a specified node's attribute, except in
+ * the case below:
+ * If the value is empty and if calling `hasValue` with the node's
+ * document and ID and `attr` would return true, then the
+ * value instead gets removed from the store (see Bug 1476680).
+ *
+ * @param node - DOM node
+ * @param attr - attribute to store
+ */
+ persist(node, attr) {
+ if (!node.id) {
+ throw new Error("Node without ID passed into persist()");
+ }
+
+ const uri = node.ownerDocument.documentURI;
+ const value = node.getAttribute(attr);
+
+ if (node.localName == "window") {
+ log("Persisting attributes to windows is handled by AppWindow.");
+ return;
+ }
+
+ // See Bug 1476680 - we could drop the `hasValue` check so that
+ // any time there's an empty attribute it gets removed from the
+ // store. Since this is copying behavior from document.persist,
+ // callers would need to be updated with that change.
+ if (!value && xulStore.hasValue(uri, node.id, attr)) {
+ xulStore.removeValue(uri, node.id, attr);
+ } else {
+ xulStore.setValue(uri, node.id, attr, value);
+ }
+ },
+
+ getIDsEnumerator(docURI) {
+ return new XULStoreEnumerator(xulStore.getIDsEnumerator(docURI));
+ },
+
+ getAttributeEnumerator(docURI, id) {
+ return new XULStoreEnumerator(xulStore.getAttributeEnumerator(docURI, id));
+ },
+};
+
+class XULStoreEnumerator {
+ constructor(enumerator) {
+ this.enumerator = enumerator;
+ }
+
+ hasMore() {
+ return this.enumerator.hasMore();
+ }
+
+ getNext() {
+ return this.enumerator.getNext();
+ }
+
+ *[Symbol.iterator]() {
+ while (this.enumerator.hasMore()) {
+ yield this.enumerator.getNext();
+ }
+ }
+}
+
+// Only here for the sake of component registration, which requires a
+// callable function.
+function getXULStore() {
+ return XULStore;
+}
diff --git a/toolkit/components/xulstore/nsIXULStore.idl b/toolkit/components/xulstore/nsIXULStore.idl
new file mode 100644
index 0000000000..48e68e8862
--- /dev/null
+++ b/toolkit/components/xulstore/nsIXULStore.idl
@@ -0,0 +1,100 @@
+/* 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"
+
+interface nsIStringEnumerator;
+webidl Node;
+
+/**
+ * The XUL store is used to store information related to a XUL document/application.
+ * Typically it is used to store the persisted state for the document, such as
+ * window location, toolbars that are open and nodes that are open and closed in a tree.
+ *
+ * If MOZ_NEW_XULSTORE is enabled:
+ * XULStore.jsm wraps this API in useful abstractions for JS consumers.
+ * XULStore.h provides a more idiomatic API for C++ consumers.
+ * You should use those APIs unless you have good reasons to use this one.
+ *
+ * If MOZ_NEW_XULSTORE is disabled:
+ * The data is serialized to [profile directory]/xulstore.json
+ */
+[scriptable, uuid(987c4b35-c426-4dd7-ad49-3c9fa4c65d20)]
+interface nsIXULStore: nsISupports
+{
+ /**
+ * Sets a value for a specified node's attribute, except in
+ * the case below:
+ * If the value is empty and if calling `hasValue` with the node's
+ * document and ID and `attr` would return true, then the
+ * value instead gets removed from the store (see Bug 1476680).
+ *
+ * @param node - DOM node
+ * @param attr - attribute to store
+ */
+ void persist(in Node aNode, in AString attr);
+
+ /**
+ * Sets a value in the store.
+ *
+ * @param doc - document URI
+ * @param id - identifier of the node
+ * @param attr - attribute to store
+ * @param value - value of the attribute
+ */
+ void setValue(in AString doc, in AString id, in AString attr, in AString value);
+
+ /**
+ * Returns true if the store contains a value for attr.
+ *
+ * @param doc - URI of the document
+ * @param id - identifier of the node
+ * @param attr - attribute
+ */
+ bool hasValue(in AString doc, in AString id, in AString attr);
+
+ /**
+ * Retrieves a value in the store, or an empty string if it does not exist.
+ *
+ * @param doc - document URI
+ * @param id - identifier of the node
+ * @param attr - attribute to retrieve
+ *
+ * @returns the value of the attribute
+ */
+ AString getValue(in AString doc, in AString id, in AString attr);
+
+ /**
+ * Removes a value in the store.
+ *
+ * @param doc - document URI
+ * @param id - identifier of the node
+ * @param attr - attribute to remove
+ */
+ void removeValue(in AString doc, in AString id, in AString attr);
+
+ /**
+ * Removes all values related to the given document.
+ *
+ * @param doc - document URI
+ */
+ void removeDocument(in AString doc);
+
+ /**
+ * Iterates over all of the ids associated with a given document uri that
+ * have stored data.
+ *
+ * @param doc - document URI
+ */
+ nsIStringEnumerator getIDsEnumerator(in AString doc);
+
+ /**
+ * Iterates over all of the attributes associated with a given document uri
+ * and id that have stored data.
+ *
+ * @param doc - document URI
+ * @param id - identifier of the node
+ */
+ nsIStringEnumerator getAttributeEnumerator(in AString doc, in AString id);
+};
diff --git a/toolkit/components/xulstore/old/XULStore.jsm b/toolkit/components/xulstore/old/XULStore.jsm
new file mode 100644
index 0000000000..553dc15e4a
--- /dev/null
+++ b/toolkit/components/xulstore/old/XULStore.jsm
@@ -0,0 +1,329 @@
+/* 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/. */
+
+// Enables logging and shorter save intervals.
+const debugMode = false;
+
+// Delay when a change is made to when the file is saved.
+// 30 seconds normally, or 3 seconds for testing
+const WRITE_DELAY_MS = (debugMode ? 3 : 30) * 1000;
+
+const XULSTORE_CID = Components.ID("{6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea}");
+const STOREDB_FILENAME = "xulstore.json";
+
+function XULStore() {
+ if (!Services.appinfo.inSafeMode) {
+ this.load();
+ }
+}
+
+XULStore.prototype = {
+ classID: XULSTORE_CID,
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsIXULStore",
+ "nsISupportsWeakReference",
+ ]),
+
+ /* ---------- private members ---------- */
+
+ /*
+ * The format of _data is _data[docuri][elementid][attribute]. For example:
+ * {
+ * "chrome://blah/foo.xul" : {
+ * "main-window" : { aaa : 1, bbb : "c" },
+ * "barColumn" : { ddd : 9, eee : "f" },
+ * },
+ *
+ * "chrome://foopy/b.xul" : { ... },
+ * ...
+ * }
+ */
+ _data: {},
+ _storeFile: null,
+ _needsSaving: false,
+ _saveAllowed: true,
+ _writeTimer: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
+
+ load() {
+ Services.obs.addObserver(this, "profile-before-change", true);
+
+ try {
+ this._storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ } catch (ex) {
+ try {
+ this._storeFile = Services.dirsvc.get("ProfDS", Ci.nsIFile);
+ } catch (ex) {
+ throw new Error("Can't find profile directory.");
+ }
+ }
+ this._storeFile.append(STOREDB_FILENAME);
+
+ this.readFile();
+ },
+
+ observe(subject, topic, data) {
+ this.writeFile();
+ if (topic == "profile-before-change") {
+ this._saveAllowed = false;
+ }
+ },
+
+ /*
+ * Internal function for logging debug messages to the Error Console window
+ */
+ log(message) {
+ if (!debugMode) {
+ return;
+ }
+ console.log("XULStore: " + message);
+ },
+
+ readFile() {
+ try {
+ this._data = JSON.parse(Cu.readUTF8File(this._storeFile));
+ } catch (e) {
+ this.log("Error reading JSON: " + e);
+ // This exception could mean that the file didn't exist.
+ // We'll just ignore the error and start with a blank slate.
+ }
+ },
+
+ async writeFile() {
+ if (!this._needsSaving) {
+ return;
+ }
+
+ this._needsSaving = false;
+
+ this.log("Writing to xulstore.json");
+
+ try {
+ await IOUtils.writeJSON(this._storeFile.path, this._data, {
+ tmpPath: this._storeFile.path + ".tmp",
+ });
+ } catch (e) {
+ this.log("Failed to write xulstore.json: " + e);
+ throw e;
+ }
+ },
+
+ markAsChanged() {
+ if (this._needsSaving || !this._storeFile) {
+ return;
+ }
+
+ // Don't write the file more than once every 30 seconds.
+ this._needsSaving = true;
+ this._writeTimer.init(this, WRITE_DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ /* ---------- interface implementation ---------- */
+
+ persist(node, attr) {
+ if (!node.id) {
+ throw new Error("Node without ID passed into persist()");
+ }
+
+ const uri = node.ownerDocument.documentURI;
+ const value = node.getAttribute(attr);
+
+ if (node.localName == "window") {
+ this.log("Persisting attributes to windows is handled by AppWindow.");
+ return;
+ }
+
+ // See Bug 1476680 - we could drop the `hasValue` check so that
+ // any time there's an empty attribute it gets removed from the
+ // store. Since this is copying behavior from document.persist,
+ // callers would need to be updated with that change.
+ if (!value && this.hasValue(uri, node.id, attr)) {
+ this.removeValue(uri, node.id, attr);
+ } else {
+ this.setValue(uri, node.id, attr, value);
+ }
+ },
+
+ setValue(docURI, id, attr, value) {
+ this.log(
+ "Saving " + attr + "=" + value + " for id=" + id + ", doc=" + docURI
+ );
+
+ if (!this._saveAllowed) {
+ Services.console.logStringMessage(
+ "XULStore: Changes after profile-before-change are ignored!"
+ );
+ return;
+ }
+
+ // bug 319846 -- don't save really long attributes or values.
+ if (id.length > 512 || attr.length > 512) {
+ throw Components.Exception(
+ "id or attribute name too long",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+
+ if (value.length > 4096) {
+ Services.console.logStringMessage(
+ "XULStore: Warning, truncating long attribute value"
+ );
+ value = value.substr(0, 4096);
+ }
+
+ let obj = this._data;
+ if (!(docURI in obj)) {
+ obj[docURI] = {};
+ }
+ obj = obj[docURI];
+ if (!(id in obj)) {
+ obj[id] = {};
+ }
+ obj = obj[id];
+
+ // Don't set the value if it is already set to avoid saving the file.
+ if (attr in obj && obj[attr] == value) {
+ return;
+ }
+
+ obj[attr] = value; // IE, this._data[docURI][id][attr] = value;
+
+ this.markAsChanged();
+ },
+
+ hasValue(docURI, id, attr) {
+ this.log(
+ "has store value for id=" + id + ", attr=" + attr + ", doc=" + docURI
+ );
+
+ let ids = this._data[docURI];
+ if (ids) {
+ let attrs = ids[id];
+ if (attrs) {
+ return attr in attrs;
+ }
+ }
+
+ return false;
+ },
+
+ getValue(docURI, id, attr) {
+ this.log(
+ "get store value for id=" + id + ", attr=" + attr + ", doc=" + docURI
+ );
+
+ let ids = this._data[docURI];
+ if (ids) {
+ let attrs = ids[id];
+ if (attrs) {
+ return attrs[attr] || "";
+ }
+ }
+
+ return "";
+ },
+
+ removeValue(docURI, id, attr) {
+ this.log(
+ "remove store value for id=" + id + ", attr=" + attr + ", doc=" + docURI
+ );
+
+ if (!this._saveAllowed) {
+ Services.console.logStringMessage(
+ "XULStore: Changes after profile-before-change are ignored!"
+ );
+ return;
+ }
+
+ let ids = this._data[docURI];
+ if (ids) {
+ let attrs = ids[id];
+ if (attrs && attr in attrs) {
+ delete attrs[attr];
+
+ if (!Object.getOwnPropertyNames(attrs).length) {
+ delete ids[id];
+
+ if (!Object.getOwnPropertyNames(ids).length) {
+ delete this._data[docURI];
+ }
+ }
+
+ this.markAsChanged();
+ }
+ }
+ },
+
+ removeDocument(docURI) {
+ this.log("remove store values for doc=" + docURI);
+
+ if (!this._saveAllowed) {
+ Services.console.logStringMessage(
+ "XULStore: Changes after profile-before-change are ignored!"
+ );
+ return;
+ }
+
+ if (this._data[docURI]) {
+ delete this._data[docURI];
+ this.markAsChanged();
+ }
+ },
+
+ getIDsEnumerator(docURI) {
+ this.log("Getting ID enumerator for doc=" + docURI);
+
+ if (!(docURI in this._data)) {
+ return new nsStringEnumerator([]);
+ }
+
+ let result = [];
+ let ids = this._data[docURI];
+ if (ids) {
+ for (let id in this._data[docURI]) {
+ result.push(id);
+ }
+ }
+
+ return new nsStringEnumerator(result);
+ },
+
+ getAttributeEnumerator(docURI, id) {
+ this.log("Getting attribute enumerator for id=" + id + ", doc=" + docURI);
+
+ if (!(docURI in this._data) || !(id in this._data[docURI])) {
+ return new nsStringEnumerator([]);
+ }
+
+ let attrs = [];
+ for (let attr in this._data[docURI][id]) {
+ attrs.push(attr);
+ }
+
+ return new nsStringEnumerator(attrs);
+ },
+};
+
+function nsStringEnumerator(items) {
+ this._items = items;
+}
+
+nsStringEnumerator.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStringEnumerator"]),
+ _nextIndex: 0,
+ [Symbol.iterator]() {
+ return this._items.values();
+ },
+ hasMore() {
+ return this._nextIndex < this._items.length;
+ },
+ getNext() {
+ if (!this.hasMore()) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ return this._items[this._nextIndex++];
+ },
+};
+
+var EXPORTED_SYMBOLS = ["XULStore"];
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),
+ }
+}
diff --git a/toolkit/components/xulstore/tests/chrome/chrome.ini b/toolkit/components/xulstore/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..c8f4ddf073
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/chrome.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+support-files =
+ window_persistence.xhtml
+
+[test_persistence.xhtml]
diff --git a/toolkit/components/xulstore/tests/chrome/test_persistence.xhtml b/toolkit/components/xulstore/tests/chrome/test_persistence.xhtml
new file mode 100644
index 0000000000..b3e65fb050
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/test_persistence.xhtml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Persistence Tests"
+ onload="runTest()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ function runTest() {
+ window.openDialog("window_persistence.xhtml", "_blank", "chrome,noopener", true, window);
+ }
+
+ function windowOpened() {
+ window.openDialog("window_persistence.xhtml", "_blank", "chrome,noopener", false, window);
+ }
+
+ function testDone() {
+ SimpleTest.finish();
+ }
+ </script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"/>
+</body>
+
+</window>
diff --git a/toolkit/components/xulstore/tests/chrome/window_persistence.xhtml b/toolkit/components/xulstore/tests/chrome/window_persistence.xhtml
new file mode 100644
index 0000000000..d474891cfc
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/window_persistence.xhtml
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Persistence Tests"
+ onload="opened()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ persist="screenX screenY width height">
+
+<button id="button1" label="Button1" persist="value"/>
+<button id="button2" label="Button2" value="Normal" persist="value"/>
+<button id="button3" label="Button3" value="Normal" persist="hidden" hidden="true"/>
+
+<script>
+<![CDATA[
+
+const XULStore = Services.xulStore;
+let URI = "chrome://mochitests/content/chrome/toolkit/components/xulstore/tests/chrome/window_persistence.xhtml";
+
+function opened()
+{
+ runTest();
+}
+
+function runTest()
+{
+ var firstRun = window.arguments[0];
+ var button1 = document.getElementById("button1");
+ var button2 = document.getElementById("button2");
+ var button3 = document.getElementById("button3");
+ if (firstRun) {
+ button1.setAttribute("value", "Pressed");
+ button2.removeAttribute("value");
+
+ button2.setAttribute("foo", "bar");
+ XULStore.persist(button2, "foo");
+ is(XULStore.getValue(URI, "button2", "foo"), "bar", "attribute persisted");
+ button2.removeAttribute("foo");
+ XULStore.persist(button2, "foo");
+ is(XULStore.hasValue(URI, "button2", "foo"), false, "attribute removed");
+
+ button3.removeAttribute("hidden");
+
+ window.close();
+ window.arguments[1].windowOpened();
+ }
+ else {
+ is(button1.getAttribute("value"), "Pressed",
+ "Attribute set");
+ is(button2.hasAttribute("value"), false,
+ "Attribute cleared");
+ is(button2.hasAttribute("foo"), false,
+ "Attribute cleared");
+
+ is(button3.hasAttribute("hidden"), false,
+ "Attribute cleared");
+
+ window.close();
+ window.arguments[1].testDone();
+ }
+}
+
+function is(l, r, n) { window.arguments[1].SimpleTest.is(l,r,n); }
+
+]]></script>
+
+</window>
diff --git a/toolkit/components/xulstore/tests/gtest/Cargo.toml b/toolkit/components/xulstore/tests/gtest/Cargo.toml
new file mode 100644
index 0000000000..53c75a2100
--- /dev/null
+++ b/toolkit/components/xulstore/tests/gtest/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "xulstore-gtest"
+version = "0.1.0"
+authors = ["The Mozilla Project Developers"]
+
+[lib]
+path = "test.rs"
diff --git a/toolkit/components/xulstore/tests/gtest/TestXULStore.cpp b/toolkit/components/xulstore/tests/gtest/TestXULStore.cpp
new file mode 100644
index 0000000000..50d7f54b80
--- /dev/null
+++ b/toolkit/components/xulstore/tests/gtest/TestXULStore.cpp
@@ -0,0 +1,141 @@
+#include <stdint.h>
+#include "gtest/gtest.h"
+#include "mozilla/XULStore.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+using mozilla::XULStoreIterator;
+using mozilla::XULStore::GetAttrs;
+using mozilla::XULStore::GetIDs;
+using mozilla::XULStore::GetValue;
+using mozilla::XULStore::HasValue;
+using mozilla::XULStore::RemoveValue;
+using mozilla::XULStore::SetValue;
+
+TEST(XULStore, SetGetValue)
+{
+ nsAutoString doc(u"SetGetValue"_ns);
+ nsAutoString id(u"foo"_ns);
+ nsAutoString attr(u"bar"_ns);
+ nsAutoString value;
+
+ EXPECT_EQ(GetValue(doc, id, attr, value), NS_OK);
+ EXPECT_TRUE(value.EqualsASCII(""));
+
+ {
+ nsAutoString value(u"baz"_ns);
+ EXPECT_EQ(SetValue(doc, id, attr, value), NS_OK);
+ }
+
+ EXPECT_EQ(GetValue(doc, id, attr, value), NS_OK);
+ EXPECT_TRUE(value.EqualsASCII("baz"));
+}
+
+TEST(XULStore, HasValue)
+{
+ nsAutoString doc(u"HasValue"_ns);
+ nsAutoString id(u"foo"_ns);
+ nsAutoString attr(u"bar"_ns);
+ bool hasValue = true;
+ EXPECT_EQ(HasValue(doc, id, attr, hasValue), NS_OK);
+ EXPECT_FALSE(hasValue);
+ nsAutoString value(u"baz"_ns);
+ EXPECT_EQ(SetValue(doc, id, attr, value), NS_OK);
+ EXPECT_EQ(HasValue(doc, id, attr, hasValue), NS_OK);
+ EXPECT_TRUE(hasValue);
+}
+
+TEST(XULStore, RemoveValue)
+{
+ nsAutoString doc(u"RemoveValue"_ns);
+ nsAutoString id(u"foo"_ns);
+ nsAutoString attr(u"bar"_ns);
+ nsAutoString value(u"baz"_ns);
+ EXPECT_EQ(SetValue(doc, id, attr, value), NS_OK);
+ EXPECT_EQ(GetValue(doc, id, attr, value), NS_OK);
+ EXPECT_TRUE(value.EqualsASCII("baz"));
+ EXPECT_EQ(RemoveValue(doc, id, attr), NS_OK);
+ EXPECT_EQ(GetValue(doc, id, attr, value), NS_OK);
+ EXPECT_TRUE(value.EqualsASCII(""));
+}
+
+TEST(XULStore, GetIDsIterator)
+{
+ nsAutoString doc(u"idIterDoc"_ns);
+ nsAutoString id1(u"id1"_ns);
+ nsAutoString id2(u"id2"_ns);
+ nsAutoString id3(u"id3"_ns);
+ nsAutoString attr(u"attr"_ns);
+ nsAutoString value(u"value"_ns);
+ nsAutoString id;
+
+ // Confirm that the store doesn't have any IDs yet.
+ mozilla::UniquePtr<XULStoreIterator> iter;
+ EXPECT_EQ(GetIDs(doc, iter), NS_OK);
+ EXPECT_FALSE(iter->HasMore());
+ // EXPECT_EQ(iter->GetNext(&id), NS_ERROR_FAILURE);
+
+ // Insert with IDs in non-alphanumeric order to confirm
+ // that store will order them when iterating them.
+ EXPECT_EQ(SetValue(doc, id3, attr, value), NS_OK);
+ EXPECT_EQ(SetValue(doc, id1, attr, value), NS_OK);
+ EXPECT_EQ(SetValue(doc, id2, attr, value), NS_OK);
+
+ // Insert different ID for another doc to confirm that store
+ // won't return it when iterating IDs for our doc.
+ nsAutoString otherDoc(u"otherDoc"_ns);
+ nsAutoString otherID(u"otherID"_ns);
+ EXPECT_EQ(SetValue(otherDoc, otherID, attr, value), NS_OK);
+
+ EXPECT_EQ(GetIDs(doc, iter), NS_OK);
+ EXPECT_TRUE(iter->HasMore());
+ EXPECT_EQ(iter->GetNext(&id), NS_OK);
+ EXPECT_TRUE(id.EqualsASCII("id1"));
+ EXPECT_TRUE(iter->HasMore());
+ EXPECT_EQ(iter->GetNext(&id), NS_OK);
+ EXPECT_TRUE(id.EqualsASCII("id2"));
+ EXPECT_TRUE(iter->HasMore());
+ EXPECT_EQ(iter->GetNext(&id), NS_OK);
+ EXPECT_TRUE(id.EqualsASCII("id3"));
+ EXPECT_FALSE(iter->HasMore());
+}
+
+TEST(XULStore, GetAttributeIterator)
+{
+ nsAutoString doc(u"attrIterDoc"_ns);
+ nsAutoString id(u"id"_ns);
+ nsAutoString attr1(u"attr1"_ns);
+ nsAutoString attr2(u"attr2"_ns);
+ nsAutoString attr3(u"attr3"_ns);
+ nsAutoString value(u"value"_ns);
+ nsAutoString attr;
+
+ mozilla::UniquePtr<XULStoreIterator> iter;
+ EXPECT_EQ(GetAttrs(doc, id, iter), NS_OK);
+ EXPECT_FALSE(iter->HasMore());
+ // EXPECT_EQ(iter->GetNext(&attr), NS_ERROR_FAILURE);
+
+ // Insert with attributes in non-alphanumeric order to confirm
+ // that store will order them when iterating them.
+ EXPECT_EQ(SetValue(doc, id, attr3, value), NS_OK);
+ EXPECT_EQ(SetValue(doc, id, attr1, value), NS_OK);
+ EXPECT_EQ(SetValue(doc, id, attr2, value), NS_OK);
+
+ // Insert different attribute for another ID to confirm that store
+ // won't return it when iterating attributes for our ID.
+ nsAutoString otherID(u"otherID"_ns);
+ nsAutoString otherAttr(u"otherAttr"_ns);
+ EXPECT_EQ(SetValue(doc, otherID, otherAttr, value), NS_OK);
+
+ EXPECT_EQ(GetAttrs(doc, id, iter), NS_OK);
+ EXPECT_TRUE(iter->HasMore());
+ EXPECT_EQ(iter->GetNext(&attr), NS_OK);
+ EXPECT_TRUE(attr.EqualsASCII("attr1"));
+ EXPECT_TRUE(iter->HasMore());
+ EXPECT_EQ(iter->GetNext(&attr), NS_OK);
+ EXPECT_TRUE(attr.EqualsASCII("attr2"));
+ EXPECT_TRUE(iter->HasMore());
+ EXPECT_EQ(iter->GetNext(&attr), NS_OK);
+ EXPECT_TRUE(attr.EqualsASCII("attr3"));
+ EXPECT_FALSE(iter->HasMore());
+}
diff --git a/toolkit/components/xulstore/tests/gtest/moz.build b/toolkit/components/xulstore/tests/gtest/moz.build
new file mode 100644
index 0000000000..81fd27a7e7
--- /dev/null
+++ b/toolkit/components/xulstore/tests/gtest/moz.build
@@ -0,0 +1,11 @@
+# -*- 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/.
+
+UNIFIED_SOURCES += [
+ "TestXULStore.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/toolkit/components/xulstore/tests/xpcshell/test_XULStore.js b/toolkit/components/xulstore/tests/xpcshell/test_XULStore.js
new file mode 100644
index 0000000000..d100592e81
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/test_XULStore.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/◦
+*/
+
+"use strict";
+
+var XULStore = null;
+var browserURI = "chrome://browser/content/browser.xhtml";
+var aboutURI = "about:config";
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+function checkValue(uri, id, attr, reference) {
+ let value = XULStore.getValue(uri, id, attr);
+ Assert.equal(value, reference);
+}
+
+function checkValueExists(uri, id, attr, exists) {
+ Assert.equal(XULStore.hasValue(uri, id, attr), exists);
+}
+
+function getIDs(uri) {
+ return Array.from(XULStore.getIDsEnumerator(uri)).sort();
+}
+
+function getAttributes(uri, id) {
+ return Array.from(XULStore.getAttributeEnumerator(uri, id)).sort();
+}
+
+function checkArrays(a, b) {
+ a.sort();
+ b.sort();
+ Assert.equal(a.toString(), b.toString());
+}
+
+add_task(async function setup() {
+ // Set a value that a future test depends on manually
+ XULStore = Services.xulStore;
+ XULStore.setValue(browserURI, "main-window", "width", "994");
+});
+
+add_task(async function testTruncation() {
+ let dos = Array(8192).join("~");
+ // Long id names should trigger an exception
+ Assert.throws(
+ () => XULStore.setValue(browserURI, dos, "foo", "foo"),
+ /NS_ERROR_ILLEGAL_VALUE/
+ );
+
+ // Long attr names should trigger an exception
+ Assert.throws(
+ () => XULStore.setValue(browserURI, "foo", dos, "foo"),
+ /NS_ERROR_ILLEGAL_VALUE/
+ );
+
+ // Long values should be truncated
+ XULStore.setValue(browserURI, "dos", "dos", dos);
+ dos = XULStore.getValue(browserURI, "dos", "dos");
+ Assert.ok(dos.length == 4096);
+ XULStore.removeValue(browserURI, "dos", "dos");
+});
+
+add_task(async function testGetValue() {
+ // Get non-existing property
+ checkValue(browserURI, "side-window", "height", "");
+
+ // Get existing property
+ checkValue(browserURI, "main-window", "width", "994");
+});
+
+add_task(async function testHasValue() {
+ // Check non-existing property
+ checkValueExists(browserURI, "side-window", "height", false);
+
+ // Check existing property
+ checkValueExists(browserURI, "main-window", "width", true);
+});
+
+add_task(async function testSetValue() {
+ // Set new attribute
+ checkValue(browserURI, "side-bar", "width", "");
+ XULStore.setValue(browserURI, "side-bar", "width", "1000");
+ checkValue(browserURI, "side-bar", "width", "1000");
+ checkArrays(["main-window", "side-bar"], getIDs(browserURI));
+ checkArrays(["width"], getAttributes(browserURI, "side-bar"));
+
+ // Modify existing property
+ checkValue(browserURI, "side-bar", "width", "1000");
+ XULStore.setValue(browserURI, "side-bar", "width", "1024");
+ checkValue(browserURI, "side-bar", "width", "1024");
+ checkArrays(["main-window", "side-bar"], getIDs(browserURI));
+ checkArrays(["width"], getAttributes(browserURI, "side-bar"));
+
+ // Add another attribute
+ checkValue(browserURI, "side-bar", "height", "");
+ XULStore.setValue(browserURI, "side-bar", "height", "1000");
+ checkValue(browserURI, "side-bar", "height", "1000");
+ checkArrays(["main-window", "side-bar"], getIDs(browserURI));
+ checkArrays(["width", "height"], getAttributes(browserURI, "side-bar"));
+});
+
+add_task(async function testRemoveValue() {
+ // Remove first attribute
+ checkValue(browserURI, "side-bar", "width", "1024");
+ XULStore.removeValue(browserURI, "side-bar", "width");
+ checkValue(browserURI, "side-bar", "width", "");
+ checkValueExists(browserURI, "side-bar", "width", false);
+ checkArrays(["main-window", "side-bar"], getIDs(browserURI));
+ checkArrays(["height"], getAttributes(browserURI, "side-bar"));
+
+ // Remove second attribute
+ checkValue(browserURI, "side-bar", "height", "1000");
+ XULStore.removeValue(browserURI, "side-bar", "height");
+ checkValue(browserURI, "side-bar", "height", "");
+ checkArrays(["main-window"], getIDs(browserURI));
+
+ // Removing an attribute that doesn't exists shouldn't fail
+ XULStore.removeValue(browserURI, "main-window", "bar");
+
+ // Removing from an id that doesn't exists shouldn't fail
+ XULStore.removeValue(browserURI, "foo", "bar");
+
+ // Removing from a document that doesn't exists shouldn't fail
+ let nonDocURI = "chrome://example/content/other.xul";
+ XULStore.removeValue(nonDocURI, "foo", "bar");
+
+ // Remove all attributes in browserURI
+ XULStore.removeValue(browserURI, "addon-bar", "collapsed");
+ checkArrays([], getAttributes(browserURI, "addon-bar"));
+ XULStore.removeValue(browserURI, "main-window", "width");
+ XULStore.removeValue(browserURI, "main-window", "height");
+ XULStore.removeValue(browserURI, "main-window", "screenX");
+ XULStore.removeValue(browserURI, "main-window", "screenY");
+ XULStore.removeValue(browserURI, "main-window", "sizemode");
+ checkArrays([], getAttributes(browserURI, "main-window"));
+ XULStore.removeValue(browserURI, "sidebar-title", "value");
+ checkArrays([], getAttributes(browserURI, "sidebar-title"));
+ checkArrays([], getIDs(browserURI));
+
+ // Remove all attributes in aboutURI
+ XULStore.removeValue(aboutURI, "prefCol", "ordinal");
+ XULStore.removeValue(aboutURI, "prefCol", "sortDirection");
+ checkArrays([], getAttributes(aboutURI, "prefCol"));
+ XULStore.removeValue(aboutURI, "lockCol", "ordinal");
+ checkArrays([], getAttributes(aboutURI, "lockCol"));
+ checkArrays([], getIDs(aboutURI));
+});
diff --git a/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration.js b/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration.js
new file mode 100644
index 0000000000..f054a45288
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration.js
@@ -0,0 +1,85 @@
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+add_task(
+ {
+ skip_if: () => !AppConstants.MOZ_NEW_XULSTORE,
+ },
+ async function test_create_old_datastore() {
+ const path = PathUtils.join(PathUtils.profileDir, "xulstore.json");
+
+ const xulstoreJSON = {
+ doc1: {
+ id1: {
+ attr1: "value1",
+ },
+ },
+ doc2: {
+ id1: {
+ attr2: "value2",
+ },
+ id2: {
+ attr1: "value1",
+ attr2: "value2",
+ attr3: "value3",
+ },
+ id3: {},
+ },
+ doc3: {},
+ };
+
+ await IOUtils.writeJSON(path, xulstoreJSON);
+ }
+);
+
+add_task(
+ {
+ skip_if: () => !AppConstants.MOZ_NEW_XULSTORE,
+ },
+ async function test_get_values() {
+ // We wait until now to import XULStore to ensure we've created
+ // the old datastore, as importing that module will initiate the attempt
+ // to migrate the old datastore to the new one.
+ const { XULStore } = ChromeUtils.import(
+ "resource://gre/modules/XULStore.jsm"
+ );
+
+ Assert.equal(await XULStore.getValue("doc1", "id1", "attr1"), "value1");
+ Assert.equal(await XULStore.getValue("doc1", "id1", "attr2"), "");
+ Assert.equal(await XULStore.getValue("doc1", "id1", "attr3"), "");
+ Assert.equal(await XULStore.getValue("doc1", "id2", "attr1"), "");
+ Assert.equal(await XULStore.getValue("doc1", "id2", "attr2"), "");
+ Assert.equal(await XULStore.getValue("doc1", "id2", "attr3"), "");
+ Assert.equal(await XULStore.getValue("doc1", "id3", "attr1"), "");
+ Assert.equal(await XULStore.getValue("doc1", "id3", "attr2"), "");
+ Assert.equal(await XULStore.getValue("doc1", "id3", "attr3"), "");
+
+ Assert.equal(await XULStore.getValue("doc2", "id1", "attr1"), "");
+ Assert.equal(await XULStore.getValue("doc2", "id1", "attr2"), "value2");
+ Assert.equal(await XULStore.getValue("doc2", "id1", "attr3"), "");
+ Assert.equal(await XULStore.getValue("doc2", "id2", "attr1"), "value1");
+ Assert.equal(await XULStore.getValue("doc2", "id2", "attr2"), "value2");
+ Assert.equal(await XULStore.getValue("doc2", "id2", "attr3"), "value3");
+ Assert.equal(await XULStore.getValue("doc2", "id3", "attr1"), "");
+ Assert.equal(await XULStore.getValue("doc2", "id3", "attr2"), "");
+ Assert.equal(await XULStore.getValue("doc2", "id3", "attr3"), "");
+
+ Assert.equal(await XULStore.getValue("doc3", "id1", "attr1"), "");
+ Assert.equal(await XULStore.getValue("doc3", "id1", "attr2"), "");
+ Assert.equal(await XULStore.getValue("doc3", "id1", "attr3"), "");
+ Assert.equal(await XULStore.getValue("doc3", "id2", "attr1"), "");
+ Assert.equal(await XULStore.getValue("doc3", "id2", "attr2"), "");
+ Assert.equal(await XULStore.getValue("doc3", "id2", "attr3"), "");
+ Assert.equal(await XULStore.getValue("doc3", "id3", "attr1"), "");
+ Assert.equal(await XULStore.getValue("doc3", "id3", "attr2"), "");
+ Assert.equal(await XULStore.getValue("doc3", "id3", "attr3"), "");
+ }
+);
diff --git a/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_data.js b/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_data.js
new file mode 100644
index 0000000000..b7b06bac2f
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_data.js
@@ -0,0 +1,57 @@
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+add_task(
+ {
+ skip_if: () => !AppConstants.MOZ_NEW_XULSTORE,
+ },
+ async function test_create_old_datastore() {
+ const path = PathUtils.join(PathUtils.profileDir, "xulstore.json");
+
+ // Valid JSON, but invalid data: attr1's value is a number, not a string.
+ const xulstoreJSON = {
+ doc1: {
+ id1: {
+ attr1: 1,
+ },
+ },
+ doc2: {
+ id2: {
+ attr2: "value2",
+ },
+ },
+ };
+
+ await IOUtils.writeJSON(path, xulstoreJSON);
+ }
+);
+
+add_task(
+ {
+ skip_if: () => !AppConstants.MOZ_NEW_XULSTORE,
+ },
+ async function test_get_values() {
+ // We wait until now to import XULStore to ensure we've created
+ // the old store, as importing that module will initiate the attempt
+ // to migrate the old store to the new one.
+ const { XULStore } = ChromeUtils.import(
+ "resource://gre/modules/XULStore.jsm"
+ );
+
+ // XULStore should *not* have migrated the values from the old store,
+ // so it should return empty strings when we try to retrieve them.
+ // That's true for both values, even though one of them is valid,
+ // because the migrator uses a typed parser that requires the entire
+ // JSON file to conform to the XULStore format.
+ Assert.equal(await XULStore.getValue("doc1", "id1", "attr1"), "");
+ Assert.equal(await XULStore.getValue("doc2", "id2", "attr2"), "");
+ }
+);
diff --git a/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_json.js b/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_json.js
new file mode 100644
index 0000000000..057e00c680
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_json.js
@@ -0,0 +1,42 @@
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+add_task(
+ {
+ skip_if: () => !AppConstants.MOZ_NEW_XULSTORE,
+ },
+ async function test_create_old_datastore() {
+ const path = PathUtils.join(PathUtils.profileDir, "xulstore.json");
+
+ // Invalid JSON: it's missing the final closing brace.
+ const xulstoreJSON = '{ doc: { id: { attr: "value" } }';
+
+ await IOUtils.writeUTF8(path, xulstoreJSON);
+ }
+);
+
+add_task(
+ {
+ skip_if: () => !AppConstants.MOZ_NEW_XULSTORE,
+ },
+ async function test_get_value() {
+ // We wait until now to import XULStore to ensure we've created
+ // the old store, as importing that module will initiate the attempt
+ // to migrate the old store to the new one.
+ const { XULStore } = ChromeUtils.import(
+ "resource://gre/modules/XULStore.jsm"
+ );
+
+ // XULStore should *not* have migrated the value from the old store,
+ // so it should return an empty string when we try to retrieve it.
+ Assert.equal(await XULStore.getValue("doc", "id", "attr"), "");
+ }
+);
diff --git a/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_profile_change.js b/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_profile_change.js
new file mode 100644
index 0000000000..d3db2af4f3
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_profile_change.js
@@ -0,0 +1,56 @@
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+add_task(
+ {
+ skip_if: () => !AppConstants.MOZ_NEW_XULSTORE,
+ },
+ async function test_get_values() {
+ // Import XULStore before getting the profile to ensure that the new store
+ // is initialized, as the purpose of this test is to confirm that the old
+ // store data gets migrated if the profile change happens post-initialization.
+ const { XULStore } = ChromeUtils.import(
+ "resource://gre/modules/XULStore.jsm"
+ );
+
+ // We haven't migrated any data yet (nor even changed to a profile), so there
+ // shouldn't be a value in the store.
+ Assert.equal(XULStore.getValue("doc1", "id1", "attr1"), "");
+
+ // Register an observer before the XULStore service registers its observer,
+ // so we can observe the profile-after-change notification first and create
+ // an old store for it to migrate. We need to write synchronously to avoid
+ // racing XULStore, so we use FileUtils instead of IOUtils.
+ Services.obs.addObserver(
+ {
+ observe() {
+ const file = FileUtils.getFile("ProfD", ["xulstore.json"]);
+ const xulstoreJSON = JSON.stringify({
+ doc1: {
+ id1: {
+ attr1: "value1",
+ },
+ },
+ });
+ let stream = FileUtils.openAtomicFileOutputStream(file);
+ stream.write(xulstoreJSON, xulstoreJSON.length);
+ FileUtils.closeAtomicFileOutputStream(stream);
+ },
+ },
+ "profile-after-change"
+ );
+
+ // This creates a profile and changes to it, triggering first our
+ // profile-after-change observer above and then XULStore's equivalent.
+ do_get_profile(true);
+
+ // XULStore should now have migrated the value from the old store.
+ Assert.equal(XULStore.getValue("doc1", "id1", "attr1"), "value1");
+ }
+);
diff --git a/toolkit/components/xulstore/tests/xpcshell/xpcshell.ini b/toolkit/components/xulstore/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..04c579dab5
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+skip-if = toolkit == 'android'
+
+[test_XULStore.js]
+
+# These tests only run on the new implementation of XULStore, since they
+# test migration of data from the old implementation to the new one.
+# But there isn't a skip-if condition we can add here to disable them,
+# so we disable them within each test file using add_task() properties.
+[test_XULStore_migration.js]
+[test_XULStore_migration_fail_invalid_json.js]
+[test_XULStore_migration_fail_invalid_data.js]
+[test_XULStore_migration_profile_change.js]