From afc457fdb3683f372e5327cf854b03528112b51a Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 22 Apr 2019 02:59:51 +0000 Subject: [PATCH] Bug 1460811 - migrate XULStore to rkv r=bgrins,lina Differential Revision: https://phabricator.services.mozilla.com/D25355 --- Cargo.lock | 19 + dom/xul/XULPersist.cpp | 61 ++-- dom/xul/XULPersist.h | 3 - .../talos/talos/xtalos/xperf_whitelist.json | 12 +- toolkit/components/moz.build | 2 +- toolkit/components/xulstore/Cargo.toml | 25 ++ toolkit/components/xulstore/XULStore.cpp | 107 ++++++ toolkit/components/xulstore/XULStore.h | 55 +++ toolkit/components/xulstore/XULStore.jsm | 321 +++-------------- toolkit/components/xulstore/components.conf | 8 +- toolkit/components/xulstore/moz.build | 16 +- toolkit/components/xulstore/nsIXULStore.idl | 17 +- toolkit/components/xulstore/src/error.rs | 112 ++++++ toolkit/components/xulstore/src/ffi.rs | 339 ++++++++++++++++++ toolkit/components/xulstore/src/iter.rs | 24 ++ toolkit/components/xulstore/src/lib.rs | 219 +++++++++++ toolkit/components/xulstore/src/persist.rs | 188 ++++++++++ toolkit/components/xulstore/src/statics.rs | 249 +++++++++++++ .../tests/chrome/window_persistence.xul | 2 +- .../xulstore/tests/gtest/Cargo.toml | 7 + .../xulstore/tests/gtest/TestXULStore.cpp | 141 ++++++++ .../components/xulstore/tests/gtest/moz.build | 14 + .../tests/xpcshell/test_XULStore_migration.js | 71 ++++ ...st_XULStore_migration_fail_invalid_data.js | 43 +++ ...st_XULStore_migration_fail_invalid_json.js | 28 ++ .../test_XULStore_migration_profile_change.js | 43 +++ .../xulstore/tests/xpcshell/xpcshell.ini | 4 + toolkit/library/rust/shared/Cargo.toml | 1 + toolkit/library/rust/shared/lib.rs | 1 + toolkit/modules/Services.jsm | 6 +- xpcom/build/Services.py | 4 + xpcom/rust/nserror/src/lib.rs | 12 + xpfe/appshell/nsXULWindow.cpp | 24 +- xpfe/appshell/nsXULWindow.h | 2 - 34 files changed, 1823 insertions(+), 357 deletions(-) create mode 100644 toolkit/components/xulstore/Cargo.toml create mode 100644 toolkit/components/xulstore/XULStore.cpp create mode 100644 toolkit/components/xulstore/XULStore.h create mode 100644 toolkit/components/xulstore/src/error.rs create mode 100644 toolkit/components/xulstore/src/ffi.rs create mode 100644 toolkit/components/xulstore/src/iter.rs create mode 100644 toolkit/components/xulstore/src/lib.rs create mode 100644 toolkit/components/xulstore/src/persist.rs create mode 100644 toolkit/components/xulstore/src/statics.rs create mode 100644 toolkit/components/xulstore/tests/gtest/Cargo.toml create mode 100644 toolkit/components/xulstore/tests/gtest/TestXULStore.cpp create mode 100644 toolkit/components/xulstore/tests/gtest/moz.build create mode 100644 toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration.js create mode 100644 toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_data.js create mode 100644 toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_json.js create mode 100644 toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_profile_change.js diff --git a/Cargo.lock b/Cargo.lock index a2bb7de0506d..eab78cd13bc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1248,6 +1248,7 @@ dependencies = [ "u2fhid 0.2.3", "webrender_bindings 0.1.0", "xpcom 0.1.0", + "xulstore 0.1.0", ] [[package]] @@ -3432,6 +3433,24 @@ dependencies = [ "syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "xulstore" +version = "0.1.0" +dependencies = [ + "crossbeam-utils 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "lmdb-rkv 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "moz_task 0.1.0", + "nserror 0.1.0", + "nsstring 0.1.0", + "rkv 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", + "xpcom 0.1.0", +] + [[package]] name = "yaml-rust" version = "0.4.2" diff --git a/dom/xul/XULPersist.cpp b/dom/xul/XULPersist.cpp index 4833490be696..90254753cc0e 100644 --- a/dom/xul/XULPersist.cpp +++ b/dom/xul/XULPersist.cpp @@ -5,8 +5,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include "XULPersist.h" - -#include "nsIXULStore.h" +#include "mozilla/XULStore.h" namespace mozilla { namespace dom { @@ -80,13 +79,6 @@ void XULPersist::Persist(Element* aElement, int32_t aNameSpaceID, return; } - if (!mLocalStore) { - mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1"); - if (NS_WARN_IF(!mLocalStore)) { - return; - } - } - nsAutoString id; aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id); @@ -103,13 +95,14 @@ void XULPersist::Persist(Element* aElement, int32_t aNameSpaceID, NS_ConvertUTF8toUTF16 uri(utf8uri); bool hasAttr; - rv = mLocalStore->HasValue(uri, id, attrstr, &hasAttr); + rv = XULStore::HasValue(uri, id, attrstr, hasAttr); if (NS_WARN_IF(NS_FAILED(rv))) { return; } if (hasAttr && valuestr.IsEmpty()) { - mLocalStore->RemoveValue(uri, id, attrstr); + rv = XULStore::RemoveValue(uri, id, attrstr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "value removed"); return; } @@ -121,7 +114,8 @@ void XULPersist::Persist(Element* aElement, int32_t aNameSpaceID, } } - mLocalStore->SetValue(uri, id, attrstr, valuestr); + rv = XULStore::SetValue(uri, id, attrstr, valuestr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "value set"); } nsresult XULPersist::ApplyPersistentAttributes() { @@ -135,13 +129,6 @@ nsresult XULPersist::ApplyPersistentAttributes() { // Add all of the 'persisted' attributes into the content // model. - if (!mLocalStore) { - mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1"); - if (NS_WARN_IF(!mLocalStore)) { - return NS_ERROR_NOT_INITIALIZED; - } - } - ApplyPersistentAttributesInternal(); return NS_OK; @@ -158,21 +145,18 @@ nsresult XULPersist::ApplyPersistentAttributesInternal() { NS_ConvertUTF8toUTF16 uri(utf8uri); // Get a list of element IDs for which persisted values are available - nsCOMPtr ids; - rv = mLocalStore->GetIDsEnumerator(uri, getter_AddRefs(ids)); + UniquePtr ids; + rv = XULStore::GetIDs(uri, ids); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - while (1) { - bool hasmore = false; - ids->HasMore(&hasmore); - if (!hasmore) { - break; - } - + while (ids->HasMore()) { nsAutoString id; - ids->GetNext(id); + rv = ids->GetNext(&id); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } // We want to hold strong refs to the elements while applying // persistent attributes, just in case. @@ -205,24 +189,21 @@ nsresult XULPersist::ApplyPersistentAttributesToElements( NS_ConvertUTF8toUTF16 uri(utf8uri); // Get a list of attributes for which persisted values are available - nsCOMPtr attrs; - rv = mLocalStore->GetAttributeEnumerator(uri, aID, getter_AddRefs(attrs)); + UniquePtr attrs; + rv = XULStore::GetAttrs(uri, aID, attrs); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - while (1) { - bool hasmore = PR_FALSE; - attrs->HasMore(&hasmore); - if (!hasmore) { - break; + while (attrs->HasMore()) { + nsAutoString attrstr; + rv = attrs->GetNext(&attrstr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; } - nsAutoString attrstr; - attrs->GetNext(attrstr); - nsAutoString value; - rv = mLocalStore->GetValue(uri, aID, attrstr, value); + rv = XULStore::GetValue(uri, aID, attrstr, value); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } diff --git a/dom/xul/XULPersist.h b/dom/xul/XULPersist.h index b63e6e21b09b..91c4c6819022 100644 --- a/dom/xul/XULPersist.h +++ b/dom/xul/XULPersist.h @@ -7,8 +7,6 @@ #ifndef mozilla_dom_XULPersist_h #define mozilla_dom_XULPersist_h -class nsIXULStore; - namespace mozilla { namespace dom { @@ -33,7 +31,6 @@ class XULPersist final : public nsStubDocumentObserver { nsresult ApplyPersistentAttributesToElements(const nsAString& aID, nsCOMArray& aElements); - nsCOMPtr mLocalStore; // A weak pointer to our document. Nulled out by DropDocumentReference. Document* MOZ_NON_OWNING_REF mDocument; }; diff --git a/testing/talos/talos/xtalos/xperf_whitelist.json b/testing/talos/talos/xtalos/xperf_whitelist.json index a5353c4e28cd..504aa1417492 100644 --- a/testing/talos/talos/xtalos/xperf_whitelist.json +++ b/testing/talos/talos/xtalos/xperf_whitelist.json @@ -566,16 +566,16 @@ "minbytes": 6000, "maxbytes": 6000 }, - "{profile}\\xulstore.json": { - "mincount": 0, - "maxcount": 0, - "minbytes": 0, - "maxbytes": 702 - }, "{talos}\\talos\\tests\\{tp5n_files}": { "mincount": 0, "maxcount": 2, "minbytes": 0, "maxbytes": 16384 + }, + "{profile}\\xulstore\\data.mdb": { + "mincount": 0, + "maxcount": 4, + "minbytes": 0, + "maxbytes": 608 } } diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build index 899cd3e0ef80..64424854875d 100644 --- a/toolkit/components/moz.build +++ b/toolkit/components/moz.build @@ -80,7 +80,7 @@ DIRS += [ 'windowcreator', 'windowwatcher', 'workerloader', - 'xulstore' + 'xulstore', ] if CONFIG['MOZ_BUILD_APP'] != 'mobile/android': diff --git a/toolkit/components/xulstore/Cargo.toml b/toolkit/components/xulstore/Cargo.toml new file mode 100644 index 000000000000..cab38a2e29dc --- /dev/null +++ b/toolkit/components/xulstore/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "xulstore" +version = "0.1.0" +authors = ["nobody@mozilla.org"] +license = "MPL-2.0" + +[dependencies] +crossbeam-utils = "0.6.3" +lazy_static = "1.0" +libc = "0.2" +lmdb-rkv = "0.11.2" +log = "0.4" +moz_task = { path = "../../../xpcom/rust/moz_task" } +nsstring = { path = "../../../xpcom/rust/nsstring" } +nserror = { path = "../../../xpcom/rust/nserror" } +rkv = "0.9.3" +serde_json = "1" +xpcom = { path = "../../../xpcom/rust/xpcom" } + +# Get rid of failure's dependency on backtrace. Eventually +# backtrace will move into Rust core, but we don't need it here. +[dependencies.failure] +version = "0.1" +default_features = false +features = ["derive"] diff --git a/toolkit/components/xulstore/XULStore.cpp b/toolkit/components/xulstore/XULStore.cpp new file mode 100644 index 000000000000..09e4d51d3061 --- /dev/null +++ b/toolkit/components/xulstore/XULStore.cpp @@ -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 https://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/XULStore.h" +#include "nsCOMPtr.h" +#include "nsIXULStore.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); +} + +// A static reference to the nsIXULStore singleton that JS uses to access +// the store. Retrieved via mozilla::XULStore::GetService(). +static StaticRefPtr 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::operator()(XULStoreIterator* ptr) const { + xulstore_iter_free(ptr); +} + +namespace XULStore { +already_AddRefed GetService() { + nsCOMPtr 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& 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& 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; +} + +}; // namespace XULStore +}; // namespace mozilla diff --git a/toolkit/components/xulstore/XULStore.h b/toolkit/components/xulstore/XULStore.h new file mode 100644 index 000000000000..4fc2cbab18c2 --- /dev/null +++ b/toolkit/components/xulstore/XULStore.h @@ -0,0 +1,55 @@ +/* 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 { + 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 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& iter); +nsresult GetAttrs(const nsAString& doc, const nsAString& id, + UniquePtr& iter); +}; // namespace XULStore +}; // namespace mozilla + +#endif // mozilla_XULStore_h diff --git a/toolkit/components/xulstore/XULStore.jsm b/toolkit/components/xulstore/XULStore.jsm index e04530bffe92..faa92de6734f 100644 --- a/toolkit/components/xulstore/XULStore.jsm +++ b/toolkit/components/xulstore/XULStore.jsm @@ -2,127 +2,45 @@ * 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. +"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"]; + +const xulStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore); + +// Enables logging. 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"; - -const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); -const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); - -ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); - -function XULStore() { - if (!Services.appinfo.inSafeMode) - this.load(); +// Internal function for logging debug messages to the Error Console window +function log(message) { + if (!debugMode) + return; + console.log("XULStore: " + message); } -XULStore.prototype = { - classID: XULSTORE_CID, - QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver, Ci.nsIXULStore, - Ci.nsISupportsWeakReference]), - _xpcom_factory: XPCOMUtils.generateSingletonFactory(XULStore), +const XULStore = { + setValue: xulStore.setValue, + hasValue: xulStore.hasValue, + getValue: xulStore.getValue, + removeValue: xulStore.removeValue, + removeDocument: xulStore.removeDocument, - /* ---------- 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" }, - * }, + /** + * Sets a value for a specified node's attribute, except in + * the case below (following the original XULDocument::persist): + * 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). * - * "chrome://foopy/b.xul" : { ... }, - * ... - * } + * @param node - DOM node + * @param attr - attribute to store */ - _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 { - let data = JSON.stringify(this._data); - let encoder = new TextEncoder(); - - data = encoder.encode(data); - await OS.File.writeAtomic(this._storeFile.path, 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()"); @@ -132,7 +50,7 @@ XULStore.prototype = { const value = node.getAttribute(attr); if (node.localName == "window") { - this.log("Persisting attributes to windows is handled by nsXULWindow."); + log("Persisting attributes to windows is handled by nsXULWindow."); return; } @@ -140,169 +58,38 @@ XULStore.prototype = { // 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); + if (!value && xulStore.hasValue(uri, node.id, attr)) { + xulStore.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 == 0) { - delete ids[id]; - - if (Object.getOwnPropertyNames(ids).length == 0) { - 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(); + xulStore.setValue(uri, node.id, attr, value); } }, 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); + return new XULStoreEnumerator(xulStore.getIDsEnumerator(docURI)); }, 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); + return new XULStoreEnumerator(xulStore.getAttributeEnumerator(docURI, id)); }, }; -function nsStringEnumerator(items) { - this._items = items; -} +class XULStoreEnumerator { + constructor(enumerator) { + this.enumerator = enumerator; + } -nsStringEnumerator.prototype = { - QueryInterface: ChromeUtils.generateQI([Ci.nsIStringEnumerator]), - _nextIndex: 0, - [Symbol.iterator]() { - return this._items.values(); - }, hasMore() { - return this._nextIndex < this._items.length; - }, - getNext() { - if (!this.hasMore()) - throw Cr.NS_ERROR_NOT_AVAILABLE; - return this._items[this._nextIndex++]; - }, -}; + return this.enumerator.hasMore(); + } -var EXPORTED_SYMBOLS = ["XULStore"]; + getNext() { + return this.enumerator.getNext(); + } + + * [Symbol.iterator]() { + while (this.enumerator.hasMore()) { + yield (this.enumerator.getNext()); + } + } +} diff --git a/toolkit/components/xulstore/components.conf b/toolkit/components/xulstore/components.conf index 192a909c950a..025497954b9a 100644 --- a/toolkit/components/xulstore/components.conf +++ b/toolkit/components/xulstore/components.conf @@ -6,9 +6,11 @@ Classes = [ { - 'cid': '{6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea}', + 'cid': '{be70bf11-0c28-4a02-a38c-0148538d42cf}', 'contract_ids': ['@mozilla.org/xul/xulstore;1'], - 'jsm': 'resource://gre/modules/XULStore.jsm', - 'constructor': 'XULStore', + 'type': 'nsIXULStore', + 'headers': ['mozilla/XULStore.h'], + 'singleton': True, + 'constructor': 'mozilla::XULStore::GetService', }, ] diff --git a/toolkit/components/xulstore/moz.build b/toolkit/components/xulstore/moz.build index 97aaa17d342e..50bea1bf82aa 100644 --- a/toolkit/components/xulstore/moz.build +++ b/toolkit/components/xulstore/moz.build @@ -14,12 +14,26 @@ XPIDL_SOURCES += [ 'nsIXULStore.idl', ] -XPIDL_MODULE = 'toolkit_xulstore' +TEST_DIRS += [ + 'tests/gtest', +] + +EXPORTS.mozilla += [ + 'XULStore.h', +] EXTRA_JS_MODULES += [ 'XULStore.jsm', ] +XPIDL_MODULE = 'xulstore' + XPCOM_MANIFESTS += [ 'components.conf', ] + +UNIFIED_SOURCES += [ + 'XULStore.cpp', +] + +FINAL_LIBRARY = 'xul' diff --git a/toolkit/components/xulstore/nsIXULStore.idl b/toolkit/components/xulstore/nsIXULStore.idl index 615918c66726..54ffb137962b 100644 --- a/toolkit/components/xulstore/nsIXULStore.idl +++ b/toolkit/components/xulstore/nsIXULStore.idl @@ -5,30 +5,19 @@ #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. * - * The data is serialized to [profile directory]/xulstore.json + * 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. */ [scriptable, uuid(987c4b35-c426-4dd7-ad49-3c9fa4c65d20)] interface nsIXULStore: nsISupports { - /** - * Sets a value for a specified node's attribute, except in - * the case below (following the original XULDocument::persist): - * 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. * diff --git a/toolkit/components/xulstore/src/error.rs b/toolkit/components/xulstore/src/error.rs new file mode 100644 index 000000000000..7a388bd8edef --- /dev/null +++ b/toolkit/components/xulstore/src/error.rs @@ -0,0 +1,112 @@ +/* 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::StoreError as RkvStoreError; +use serde_json::Error as SerdeJsonError; +use std::{io::Error as IoError, str::Utf8Error, string::FromUtf16Error, sync::PoisonError}; + +pub(crate) type XULStoreResult = Result; + +#[derive(Debug, Fail)] +pub(crate) enum XULStoreError { + #[fail(display = "error converting bytes: {:?}", _0)] + ConvertBytes(Utf8Error), + + #[fail(display = "error converting string: {:?}", _0)] + ConvertString(FromUtf16Error), + + #[fail(display = "I/O error: {:?}", _0)] + IoError(IoError), + + #[fail(display = "iteration is finished")] + IterationFinished, + + #[fail(display = "JSON error: {}", _0)] + JsonError(SerdeJsonError), + + #[fail(display = "error result {}", _0)] + NsResult(nsresult), + + #[fail(display = "poison error getting read/write lock")] + PoisonError, + + #[fail(display = "store error: {:?}", _0)] + RkvStoreError(RkvStoreError), + + #[fail(display = "id or attribute name too long")] + IdAttrNameTooLong, + + #[fail(display = "unavailable")] + Unavailable, + + #[fail(display = "unexpected key: {:?}", _0)] + UnexpectedKey(String), + + #[fail(display = "unexpected value")] + UnexpectedValue, +} + +impl From 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::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 From for XULStoreError { + fn from(err: FromUtf16Error) -> XULStoreError { + XULStoreError::ConvertString(err) + } +} + +impl From for XULStoreError { + fn from(result: nsresult) -> XULStoreError { + XULStoreError::NsResult(result) + } +} + +impl From> for XULStoreError { + fn from(_: PoisonError) -> XULStoreError { + XULStoreError::PoisonError + } +} + +impl From for XULStoreError { + fn from(err: RkvStoreError) -> XULStoreError { + XULStoreError::RkvStoreError(err) + } +} + +impl From for XULStoreError { + fn from(err: Utf8Error) -> XULStoreError { + XULStoreError::ConvertBytes(err) + } +} + +impl From for XULStoreError { + fn from(err: IoError) -> XULStoreError { + XULStoreError::IoError(err) + } +} + +impl From for XULStoreError { + fn from(err: SerdeJsonError) -> XULStoreError { + XULStoreError::JsonError(err) + } +} diff --git a/toolkit/components/xulstore/src/ffi.rs b/toolkit/components/xulstore/src/ffi.rs new file mode 100644 index 000000000000..04328f64d221 --- /dev/null +++ b/toolkit/components/xulstore/src/ffi.rs @@ -0,0 +1,339 @@ +/* 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, persist::clear_on_shutdown, statics::update_profile_dir}; +use libc::c_char; +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::()).forget(&mut *result); +} + +#[derive(xpcom)] +#[xpimplements(nsIXULStore)] +#[refcnt = "atomic"] +pub struct InitXULStoreService {} + +impl XULStoreService { + fn new() -> RefPtr { + XULStoreService::allocate(InitXULStoreService {}) + } + + 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 { + 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 { + 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, nsresult> { + match XULStore::get_ids(doc) { + Ok(val) => { + let enumerator = StringEnumerator::new(val); + Ok(RefPtr::new(enumerator.coerce::())) + } + 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, nsresult> { + match XULStore::get_attrs(doc, id) { + Ok(val) => { + let enumerator = StringEnumerator::new(val); + Ok(RefPtr::new(enumerator.coerce::())) + } + Err(err) => Err(err.into()), + } + } +} + +#[derive(xpcom)] +#[xpimplements(nsIStringEnumerator)] +#[refcnt = "nonatomic"] +pub(crate) struct InitStringEnumerator { + iter: RefCell, +} +impl StringEnumerator { + pub(crate) fn new(iter: XULStoreIterator) -> RefPtr { + StringEnumerator::allocate(InitStringEnumerator { + iter: RefCell::new(iter), + }) + } + + xpcom_method!(string_iterator => StringIterator() -> *const nsIJSEnumerator); + + fn string_iterator(&self) -> Result, nsresult> { + Err(NS_ERROR_NOT_IMPLEMENTED) + } + + xpcom_method!(has_more => HasMore() -> bool); + + fn has_more(&self) -> Result { + let iter = self.iter.borrow(); + Ok(iter.has_more()) + } + + xpcom_method!(get_next => GetNext() -> nsAString); + + fn get_next(&self) -> Result { + let mut iter = self.iter.borrow_mut(); + match iter.get_next() { + Ok(value) => Ok(nsString::from(&value)), + Err(err) => Err(err.into()), + } + } +} + +#[derive(xpcom)] +#[xpimplements(nsIObserver)] +#[refcnt = "nonatomic"] +pub(crate) struct InitProfileChangeObserver {} +impl ProfileChangeObserver { + #[allow(non_snake_case)] + unsafe fn Observe( + &self, + _subject: *const nsISupports, + _topic: *const c_char, + _data: *const i16, + ) -> nsresult { + update_profile_dir(); + NS_OK + } + + pub(crate) fn new() -> RefPtr { + ProfileChangeObserver::allocate(InitProfileChangeObserver {}) + } +} + +#[derive(xpcom)] +#[xpimplements(nsIObserver)] +#[refcnt = "nonatomic"] +pub(crate) struct InitXpcomShutdownObserver {} +impl XpcomShutdownObserver { + #[allow(non_snake_case)] + unsafe fn Observe( + &self, + _subject: *const nsISupports, + _topic: *const c_char, + _data: *const i16, + ) -> nsresult { + clear_on_shutdown(); + NS_OK + } + + pub(crate) fn new() -> RefPtr { + XpcomShutdownObserver::allocate(InitXpcomShutdownObserver {}) + } +} + +#[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)); +} diff --git a/toolkit/components/xulstore/src/iter.rs b/toolkit/components/xulstore/src/iter.rs new file mode 100644 index 000000000000..06e0ebf17577 --- /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, +} + +impl XULStoreIterator { + pub(crate) fn new(values: IntoIter) -> Self { + Self { values } + } + + pub(crate) fn has_more(&self) -> bool { + !self.values.as_slice().is_empty() + } + + pub(crate) fn get_next(&mut self) -> XULStoreResult { + 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 000000000000..95bb9ba6de4a --- /dev/null +++ b/toolkit/components/xulstore/src/lib.rs @@ -0,0 +1,219 @@ +/* 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 failure; +#[macro_use] +extern crate lazy_static; +extern crate libc; +extern crate lmdb; +#[macro_use] +extern crate log; +extern crate moz_task; +extern crate nserror; +extern crate nsstring; +extern crate rkv; +extern crate serde_json; +#[macro_use] +extern crate xpcom; + +mod error; +mod ffi; +mod iter; +mod persist; +mod statics; + +use crate::{ + error::{XULStoreError, XULStoreResult}, + iter::XULStoreIterator, + persist::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 { + 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 { + 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 { + 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 mut ids: Vec = 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 { + 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 mut attrs: Vec = attrs.keys().cloned().collect(); + Ok(XULStoreIterator::new(attrs.into_iter())) + } + None => Ok(XULStoreIterator::new(vec![].into_iter())), + }, + None => Ok(XULStoreIterator::new(vec![].into_iter())), + } +} diff --git a/toolkit/components/xulstore/src/persist.rs b/toolkit/components/xulstore/src/persist.rs new file mode 100644 index 000000000000..8868f7099536 --- /dev/null +++ b/toolkit/components/xulstore/src/persist.rs @@ -0,0 +1,188 @@ +/* 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::XpcomShutdownObserver, + statics::get_database, +}; +use crossbeam_utils::atomic::AtomicCell; +use lmdb::Error as LmdbError; +use moz_task::{create_thread, Task, TaskRunnable}; +use nserror::nsresult; +use rkv::{StoreError as RkvStoreError, Value}; +use std::{collections::HashMap, sync::Mutex, thread::sleep, time::Duration}; +use xpcom::{interfaces::nsIThread, RefPtr, ThreadBoundRefPtr}; + +/// 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. + +lazy_static! { + /// 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 ref CHANGES: Mutex>>> = { 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 ref PERSIST: Mutex<()> = { Mutex::new(()) }; + + static ref THREAD: Mutex>> = { + let thread: RefPtr = match create_thread("XULStore") { + Ok(thread) => thread, + Err(err) => { + error!("error creating XULStore thread: {}", err); + return Mutex::new(None); + } + }; + + // Observe XPCOM shutdown so we can clear the thread and thus not + // "leak" it (from the perspective of the leak checker). + observe_xpcom_shutdown(); + + Mutex::new(Some(ThreadBoundRefPtr::new(thread))) + }; +} + +fn observe_xpcom_shutdown() { + (|| -> XULStoreResult<()> { + let obs_svc = xpcom::services::get_ObserverService().ok_or(XULStoreError::Unavailable)?; + let observer = XpcomShutdownObserver::new(); + unsafe { + obs_svc + .AddObserver(observer.coerce(), c_str!("xpcom-shutdown").as_ptr(), false) + .to_result()? + }; + Ok(()) + })() + .unwrap_or_else(|err| error!("error observing XPCOM shutdown: {}", err)); +} + +pub(crate) fn clear_on_shutdown() { + (|| -> XULStoreResult<()> { + THREAD.lock()?.take(); + Ok(()) + })() + .unwrap_or_else(|err| error!("error clearing thread: {}", err)); +} + +pub(crate) fn persist(key: String, value: Option) -> 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()); + let thread_guard = THREAD.lock()?; + let thread = thread_guard + .as_ref() + .ok_or(XULStoreError::Unavailable)? + .get_ref() + .ok_or(XULStoreError::Unavailable)?; + TaskRunnable::new("XULStore::Persist", task)?.dispatch(thread)?; + } + + // 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>>, +} + +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()?; + + let db = get_database()?; + let mut writer = db.env.write()?; + + // 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(); + + // The Option should be a Some(HashMap) (otherwise the task + // shouldn't have been scheduled in the first place). If it's None, + // unexpectedly, then we return an error early. + let writes = writes.ok_or(XULStoreError::Unavailable)?; + + 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::LmdbError(LmdbError::NotFound)) => { + warn!("tried to remove key that isn't in the store"); + } + + Err(err) => return Err(err.into()), + } + } + } + } + + writer.commit()?; + + Ok(()) + }())); + } + + 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 000000000000..289f3b6b2967 --- /dev/null +++ b/toolkit/components/xulstore/src/statics.rs @@ -0,0 +1,249 @@ +/* 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 rkv::{Rkv, SingleStore, StoreOptions, Value}; +use std::{ + collections::BTreeMap, + fs::{create_dir_all, remove_file, File}, + path::PathBuf, + str, + sync::Mutex, +}; +use xpcom::{interfaces::nsIFile, XpCom}; + +type XULStoreCache = BTreeMap>>; + +pub struct Database { + pub env: Rkv, + pub store: SingleStore, +} + +impl Database { + fn new(env: Rkv, store: SingleStore) -> Database { + Database { env, store } + } +} + +lazy_static! { + static ref PROFILE_DIR: Mutex> = { + observe_profile_change(); + Mutex::new(get_profile_dir().ok()) + }; + pub(crate) static ref DATA_CACHE: Mutex> = + { Mutex::new(cache_data().ok()) }; +} + +pub(crate) fn get_database() -> XULStoreResult { + let xulstore_dir = get_xulstore_dir()?; + let env = Rkv::new(xulstore_dir.as_path())?; + let store = env.open_single("db", StoreOptions::create())?; + + Ok(Database::new(env, 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 { + // 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 = xpcom::services::get_DirectoryService().ok_or(XULStoreError::Unavailable)?; + let mut profile_dir = xpcom::GetterAddrefs::::new(); + unsafe { + dir_svc + .Get( + c_str!("ProfD").as_ptr(), + &nsIFile::IID, + profile_dir.void_ptr(), + ) + .to_result() + .or_else(|_| { + dir_svc + .Get( + c_str!("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 { + 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 = xpcom::services::get_ObserverService().ok_or(XULStoreError::Unavailable)?; + let observer = ProfileChangeObserver::new(); + unsafe { + obs_svc + .AddObserver( + observer.coerce(), + c_str!("profile-after-change").as_ptr(), + false, + ) + .to_result()? + }; + Ok(()) + })() + .unwrap_or_else(|err| error!("error observing profile change: {}", err)); +} + +fn in_safe_mode() -> XULStoreResult { + let app_info_svc = xpcom::services::get_AppInfoService().ok_or(XULStoreError::Unavailable)?; + let mut in_safe_mode = false; + unsafe { + app_info_svc.GetInSafeMode(&mut in_safe_mode).to_result()?; + } + Ok(in_safe_mode) +} + +fn cache_data() -> XULStoreResult { + let db = get_database()?; + maybe_migrate_data(&db.env, db.store); + + let mut all = XULStoreCache::default(); + if in_safe_mode()? { + return Ok(all); + } + + let reader = db.env.read()?; + let iterator = db.store.iter_start(&reader)?; + + for result in iterator { + let (key, value): (&str, String) = match result { + Ok((key, value)) => { + assert!(value.is_some(), "iterated key has 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::>(); + 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(env: &Rkv, 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 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: &Option) -> XULStoreResult { + match value { + Some(Value::Str(val)) => Ok(val.to_string()), + + // Per the XULStore API, return an empty string if the value + // isn't found. + None => Ok(String::new()), + + // 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). + Some(_) => Err(XULStoreError::UnexpectedValue), + } +} diff --git a/toolkit/components/xulstore/tests/chrome/window_persistence.xul b/toolkit/components/xulstore/tests/chrome/window_persistence.xul index 45da000f4fd6..830db00eb0eb 100644 --- a/toolkit/components/xulstore/tests/chrome/window_persistence.xul +++ b/toolkit/components/xulstore/tests/chrome/window_persistence.xul @@ -13,7 +13,7 @@