diff options
Diffstat (limited to 'toolkit/components/xulstore')
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] |