summaryrefslogtreecommitdiffstats
path: root/dom/localstorage
diff options
context:
space:
mode:
Diffstat (limited to 'dom/localstorage')
-rw-r--r--dom/localstorage/ActorsChild.cpp325
-rw-r--r--dom/localstorage/ActorsChild.h283
-rw-r--r--dom/localstorage/ActorsParent.cpp9144
-rw-r--r--dom/localstorage/ActorsParent.h89
-rw-r--r--dom/localstorage/LSDatabase.cpp448
-rw-r--r--dom/localstorage/LSDatabase.h105
-rw-r--r--dom/localstorage/LSInitializationTypes.h48
-rw-r--r--dom/localstorage/LSObject.cpp1167
-rw-r--r--dom/localstorage/LSObject.h220
-rw-r--r--dom/localstorage/LSObserver.cpp70
-rw-r--r--dom/localstorage/LSObserver.h68
-rw-r--r--dom/localstorage/LSSnapshot.cpp1078
-rw-r--r--dom/localstorage/LSSnapshot.h193
-rw-r--r--dom/localstorage/LSValue.cpp202
-rw-r--r--dom/localstorage/LSValue.h135
-rw-r--r--dom/localstorage/LSWriteOptimizer.cpp72
-rw-r--r--dom/localstorage/LSWriteOptimizer.h194
-rw-r--r--dom/localstorage/LSWriteOptimizerImpl.h62
-rw-r--r--dom/localstorage/LocalStorageCommon.cpp160
-rw-r--r--dom/localstorage/LocalStorageCommon.h263
-rw-r--r--dom/localstorage/LocalStorageManager2.cpp661
-rw-r--r--dom/localstorage/LocalStorageManager2.h68
-rw-r--r--dom/localstorage/PBackgroundLSDatabase.ipdl175
-rw-r--r--dom/localstorage/PBackgroundLSObserver.ipdl63
-rw-r--r--dom/localstorage/PBackgroundLSRequest.ipdl103
-rw-r--r--dom/localstorage/PBackgroundLSSharedTypes.ipdlh96
-rw-r--r--dom/localstorage/PBackgroundLSSimpleRequest.ipdl59
-rw-r--r--dom/localstorage/PBackgroundLSSnapshot.ipdl153
-rw-r--r--dom/localstorage/ReportInternalError.cpp31
-rw-r--r--dom/localstorage/ReportInternalError.h29
-rw-r--r--dom/localstorage/SerializationHelpers.h62
-rw-r--r--dom/localstorage/SnappyUtils.cpp80
-rw-r--r--dom/localstorage/SnappyUtils.h20
-rw-r--r--dom/localstorage/moz.build75
-rw-r--r--dom/localstorage/nsILocalStorageManager.idl38
-rw-r--r--dom/localstorage/test/browser.ini6
-rw-r--r--dom/localstorage/test/browser_private_ls.js44
-rw-r--r--dom/localstorage/test/gtest/TestLocalStorage.cpp140
-rw-r--r--dom/localstorage/test/gtest/moz.build17
-rw-r--r--dom/localstorage/test/helpers.js78
-rw-r--r--dom/localstorage/test/mochitest.ini10
-rw-r--r--dom/localstorage/test/page_private_ls.html9
-rw-r--r--dom/localstorage/test/test_largeItems.html19
-rw-r--r--dom/localstorage/test/unit/archive_profile.zipbin0 -> 1162 bytes
-rw-r--r--dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zipbin0 -> 3873 bytes
-rw-r--r--dom/localstorage/test/unit/corruptedDatabase_profile.zipbin0 -> 4096 bytes
-rw-r--r--dom/localstorage/test/unit/databaseShadowing-shared.js130
-rw-r--r--dom/localstorage/test/unit/groupMismatch_profile.zipbin0 -> 1706 bytes
-rw-r--r--dom/localstorage/test/unit/head.js332
-rw-r--r--dom/localstorage/test/unit/make_migration_emptyValue.js23
-rw-r--r--dom/localstorage/test/unit/migration_emptyValue_profile.zipbin0 -> 824 bytes
-rw-r--r--dom/localstorage/test/unit/migration_profile.zipbin0 -> 2026 bytes
-rw-r--r--dom/localstorage/test/unit/schema3upgrade_profile.zipbin0 -> 4092 bytes
-rw-r--r--dom/localstorage/test/unit/schema4upgrade_profile.zipbin0 -> 5385 bytes
-rw-r--r--dom/localstorage/test/unit/stringLength2_profile.zipbin0 -> 46237 bytes
-rw-r--r--dom/localstorage/test/unit/stringLength_profile.zipbin0 -> 13919 bytes
-rw-r--r--dom/localstorage/test/unit/test_archive.js78
-rw-r--r--dom/localstorage/test/unit/test_clientValidation.js32
-rw-r--r--dom/localstorage/test/unit/test_corruptedDatabase.js73
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing1.js23
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing2.js17
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js30
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js17
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js29
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js21
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js28
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js21
-rw-r--r--dom/localstorage/test/unit/test_eviction.js91
-rw-r--r--dom/localstorage/test/unit/test_flushing.js72
-rw-r--r--dom/localstorage/test/unit/test_groupLimit.js85
-rw-r--r--dom/localstorage/test/unit/test_groupMismatch.js45
-rw-r--r--dom/localstorage/test/unit/test_largeItems.js88
-rw-r--r--dom/localstorage/test/unit/test_lsng_enabled.js13
-rw-r--r--dom/localstorage/test/unit/test_migration.js127
-rw-r--r--dom/localstorage/test/unit/test_migration_emptyValue.js37
-rw-r--r--dom/localstorage/test/unit/test_old_lsng_pref.js17
-rw-r--r--dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js70
-rw-r--r--dom/localstorage/test/unit/test_originInit.js372
-rw-r--r--dom/localstorage/test/unit/test_preloading.js84
-rw-r--r--dom/localstorage/test/unit/test_schema3upgrade.js39
-rw-r--r--dom/localstorage/test/unit/test_schema4upgrade.js39
-rw-r--r--dom/localstorage/test/unit/test_snapshotting.js330
-rw-r--r--dom/localstorage/test/unit/test_stringLength.js74
-rw-r--r--dom/localstorage/test/unit/test_stringLength2.js79
-rw-r--r--dom/localstorage/test/unit/test_unicodeCharacters.js205
-rw-r--r--dom/localstorage/test/unit/test_uri_encoding_edge_cases.js69
-rw-r--r--dom/localstorage/test/unit/test_usage.js69
-rw-r--r--dom/localstorage/test/unit/test_usageAfterMigration.js164
-rw-r--r--dom/localstorage/test/unit/usageAfterMigration_profile.zipbin0 -> 1227 bytes
-rw-r--r--dom/localstorage/test/unit/xpcshell.ini73
90 files changed, 19458 insertions, 0 deletions
diff --git a/dom/localstorage/ActorsChild.cpp b/dom/localstorage/ActorsChild.cpp
new file mode 100644
index 0000000000..ea7c589a66
--- /dev/null
+++ b/dom/localstorage/ActorsChild.cpp
@@ -0,0 +1,325 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "ActorsChild.h"
+
+#include "ErrorList.h"
+#include "LSDatabase.h"
+#include "LSObserver.h"
+#include "LSSnapshot.h"
+#include "LocalStorageCommon.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/LSValue.h"
+#include "mozilla/dom/Storage.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla::dom {
+
+/*******************************************************************************
+ * LSDatabaseChild
+ ******************************************************************************/
+
+LSDatabaseChild::LSDatabaseChild(LSDatabase* aDatabase) : mDatabase(aDatabase) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aDatabase);
+
+ MOZ_COUNT_CTOR(LSDatabaseChild);
+}
+
+LSDatabaseChild::~LSDatabaseChild() {
+ AssertIsOnOwningThread();
+
+ MOZ_COUNT_DTOR(LSDatabaseChild);
+}
+
+void LSDatabaseChild::SendDeleteMeInternal() {
+ AssertIsOnOwningThread();
+
+ if (mDatabase) {
+ mDatabase->ClearActor();
+ mDatabase = nullptr;
+
+ MOZ_ALWAYS_TRUE(PBackgroundLSDatabaseChild::SendDeleteMe());
+ }
+}
+
+void LSDatabaseChild::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ if (mDatabase) {
+ mDatabase->ClearActor();
+#ifdef DEBUG
+ mDatabase = nullptr;
+#endif
+ }
+}
+
+mozilla::ipc::IPCResult LSDatabaseChild::RecvRequestAllowToClose() {
+ AssertIsOnOwningThread();
+
+ if (mDatabase) {
+ mDatabase->RequestAllowToClose();
+
+ // TODO: A new datastore will be prepared at first LocalStorage API
+ // synchronous call. It would be better to start preparing a new
+ // datastore right here, but asynchronously.
+ // However, we probably shouldn't do that if we are shutting down.
+ }
+
+ return IPC_OK();
+}
+
+PBackgroundLSSnapshotChild* LSDatabaseChild::AllocPBackgroundLSSnapshotChild(
+ const nsAString& aDocumentURI, const nsAString& aKey,
+ const bool& aIncreasePeakUsage, const int64_t& aMinSize,
+ LSSnapshotInitInfo* aInitInfo) {
+ MOZ_CRASH("PBackgroundLSSnapshotChild actor should be manually constructed!");
+}
+
+bool LSDatabaseChild::DeallocPBackgroundLSSnapshotChild(
+ PBackgroundLSSnapshotChild* aActor) {
+ MOZ_ASSERT(aActor);
+
+ delete aActor;
+ return true;
+}
+
+/*******************************************************************************
+ * LSObserverChild
+ ******************************************************************************/
+
+LSObserverChild::LSObserverChild(LSObserver* aObserver) : mObserver(aObserver) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aObserver);
+
+ MOZ_COUNT_CTOR(LSObserverChild);
+}
+
+LSObserverChild::~LSObserverChild() {
+ AssertIsOnOwningThread();
+
+ MOZ_COUNT_DTOR(LSObserverChild);
+}
+
+void LSObserverChild::SendDeleteMeInternal() {
+ AssertIsOnOwningThread();
+
+ if (mObserver) {
+ mObserver->ClearActor();
+ mObserver = nullptr;
+
+ MOZ_ALWAYS_TRUE(PBackgroundLSObserverChild::SendDeleteMe());
+ }
+}
+
+void LSObserverChild::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ if (mObserver) {
+ mObserver->ClearActor();
+#ifdef DEBUG
+ mObserver = nullptr;
+#endif
+ }
+}
+
+mozilla::ipc::IPCResult LSObserverChild::RecvObserve(
+ const PrincipalInfo& aPrincipalInfo, const uint32_t& aPrivateBrowsingId,
+ const nsAString& aDocumentURI, const nsAString& aKey,
+ const LSValue& aOldValue, const LSValue& aNewValue) {
+ AssertIsOnOwningThread();
+
+ if (!mObserver) {
+ return IPC_OK();
+ }
+
+ QM_TRY_INSPECT(const auto& principal,
+ PrincipalInfoToPrincipal(aPrincipalInfo),
+ IPC_FAIL(this, "PrincipalInfoToPrincipal failed!"));
+
+ Storage::NotifyChange(/* aStorage */ nullptr, principal, aKey,
+ aOldValue.AsString(), aNewValue.AsString(),
+ /* aStorageType */ kLocalStorageType, aDocumentURI,
+ /* aIsPrivate */ !!aPrivateBrowsingId,
+ /* aImmediateDispatch */ true);
+
+ return IPC_OK();
+}
+
+/*******************************************************************************
+ * LocalStorageRequestChild
+ ******************************************************************************/
+
+LSRequestChild::LSRequestChild() : mFinishing(false) {
+ AssertIsOnOwningThread();
+
+ MOZ_COUNT_CTOR(LSRequestChild);
+}
+
+LSRequestChild::~LSRequestChild() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mCallback);
+
+ MOZ_COUNT_DTOR(LSRequestChild);
+}
+
+void LSRequestChild::SetCallback(LSRequestChildCallback* aCallback) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(!mCallback);
+ MOZ_ASSERT(Manager());
+
+ mCallback = aCallback;
+}
+
+bool LSRequestChild::Finishing() const {
+ AssertIsOnOwningThread();
+
+ return mFinishing;
+}
+
+void LSRequestChild::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ if (mCallback) {
+ MOZ_ASSERT(aWhy != Deletion);
+
+ mCallback->OnResponse(NS_ERROR_FAILURE);
+
+ mCallback = nullptr;
+ }
+}
+
+mozilla::ipc::IPCResult LSRequestChild::Recv__delete__(
+ const LSRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mCallback);
+
+ mCallback->OnResponse(aResponse);
+
+ mCallback = nullptr;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult LSRequestChild::RecvReady() {
+ AssertIsOnOwningThread();
+
+ mFinishing = true;
+
+ // We only expect this to return false if the channel has been closed, but
+ // PBackground's channel never gets shutdown.
+ MOZ_ALWAYS_TRUE(SendFinish());
+
+ return IPC_OK();
+}
+
+/*******************************************************************************
+ * LSSimpleRequestChild
+ ******************************************************************************/
+
+LSSimpleRequestChild::LSSimpleRequestChild() {
+ AssertIsOnOwningThread();
+
+ MOZ_COUNT_CTOR(LSSimpleRequestChild);
+}
+
+LSSimpleRequestChild::~LSSimpleRequestChild() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mCallback);
+
+ MOZ_COUNT_DTOR(LSSimpleRequestChild);
+}
+
+void LSSimpleRequestChild::SetCallback(
+ LSSimpleRequestChildCallback* aCallback) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(!mCallback);
+ MOZ_ASSERT(Manager());
+
+ mCallback = aCallback;
+}
+
+void LSSimpleRequestChild::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ if (mCallback) {
+ MOZ_ASSERT(aWhy != Deletion);
+
+ mCallback->OnResponse(NS_ERROR_FAILURE);
+
+ mCallback = nullptr;
+ }
+}
+
+mozilla::ipc::IPCResult LSSimpleRequestChild::Recv__delete__(
+ const LSSimpleRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mCallback);
+
+ mCallback->OnResponse(aResponse);
+
+ mCallback = nullptr;
+
+ return IPC_OK();
+}
+
+/*******************************************************************************
+ * LSSnapshotChild
+ ******************************************************************************/
+
+LSSnapshotChild::LSSnapshotChild(LSSnapshot* aSnapshot) : mSnapshot(aSnapshot) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aSnapshot);
+
+ MOZ_COUNT_CTOR(LSSnapshotChild);
+}
+
+LSSnapshotChild::~LSSnapshotChild() {
+ AssertIsOnOwningThread();
+
+ MOZ_COUNT_DTOR(LSSnapshotChild);
+}
+
+void LSSnapshotChild::SendDeleteMeInternal() {
+ AssertIsOnOwningThread();
+
+ if (mSnapshot) {
+ mSnapshot->ClearActor();
+ mSnapshot = nullptr;
+
+ MOZ_ALWAYS_TRUE(PBackgroundLSSnapshotChild::SendDeleteMe());
+ }
+}
+
+void LSSnapshotChild::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ if (mSnapshot) {
+ mSnapshot->ClearActor();
+#ifdef DEBUG
+ mSnapshot = nullptr;
+#endif
+ }
+}
+
+mozilla::ipc::IPCResult LSSnapshotChild::RecvMarkDirty() {
+ AssertIsOnOwningThread();
+
+ if (!mSnapshot) {
+ return IPC_OK();
+ }
+
+ mSnapshot->MarkDirty();
+
+ return IPC_OK();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/localstorage/ActorsChild.h b/dom/localstorage/ActorsChild.h
new file mode 100644
index 0000000000..d9b614e401
--- /dev/null
+++ b/dom/localstorage/ActorsChild.h
@@ -0,0 +1,283 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_ActorsChild_h
+#define mozilla_dom_localstorage_ActorsChild_h
+
+#include <cstdint>
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/PBackgroundLSDatabaseChild.h"
+#include "mozilla/dom/PBackgroundLSObserverChild.h"
+#include "mozilla/dom/PBackgroundLSRequest.h"
+#include "mozilla/dom/PBackgroundLSRequestChild.h"
+#include "mozilla/dom/PBackgroundLSSimpleRequest.h"
+#include "mozilla/dom/PBackgroundLSSimpleRequestChild.h"
+#include "mozilla/dom/PBackgroundLSSnapshotChild.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nscore.h"
+
+namespace mozilla {
+
+namespace ipc {
+
+class BackgroundChildImpl;
+
+} // namespace ipc
+
+namespace dom {
+
+class LocalStorageManager2;
+class LSDatabase;
+class LSObject;
+class LSObserver;
+class LSRequestChildCallback;
+class LSSimpleRequestChildCallback;
+class LSSnapshot;
+
+/**
+ * Minimal glue actor with standard IPC-managed new/delete existence that exists
+ * primarily to track the continued existence of the LSDatabase in the child.
+ * Most of the interesting bits happen via PBackgroundLSSnapshot.
+ *
+ * Mutual raw pointers are maintained between LSDatabase and this class that are
+ * cleared at either (expected) when the child starts the deletion process
+ * (SendDeleteMeInternal) or unexpected actor death (ActorDestroy).
+ *
+ * See `PBackgroundLSDatabase.ipdl` for more information.
+ *
+ *
+ * ## Low-Level Lifecycle ##
+ * - Created by LSObject::EnsureDatabase if it had to create a database.
+ * - Deletion begun by LSDatabase's destructor invoking SendDeleteMeInternal
+ * which will result in the parent sending __delete__ which destroys the
+ * actor.
+ */
+class LSDatabaseChild final : public PBackgroundLSDatabaseChild {
+ friend class mozilla::ipc::BackgroundChildImpl;
+ friend class LSDatabase;
+ friend class LSObject;
+
+ LSDatabase* mDatabase;
+
+ NS_DECL_OWNINGTHREAD
+
+ public:
+ void AssertIsOnOwningThread() const {
+ NS_ASSERT_OWNINGTHREAD(LSDatabaseChild);
+ }
+
+ private:
+ // Only created by LSObject.
+ explicit LSDatabaseChild(LSDatabase* aDatabase);
+
+ // Only destroyed by mozilla::ipc::BackgroundChildImpl.
+ ~LSDatabaseChild();
+
+ void SendDeleteMeInternal();
+
+ // IPDL methods are only called by IPDL.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvRequestAllowToClose() override;
+
+ PBackgroundLSSnapshotChild* AllocPBackgroundLSSnapshotChild(
+ const nsAString& aDocumentURI, const nsAString& aKey,
+ const bool& aIncreasePeakUsage, const int64_t& aMinSize,
+ LSSnapshotInitInfo* aInitInfo) override;
+
+ bool DeallocPBackgroundLSSnapshotChild(
+ PBackgroundLSSnapshotChild* aActor) override;
+};
+
+/**
+ * Minimal IPC-managed (new/delete) actor that exists to receive and relay
+ * "storage" events from changes to LocalStorage that take place in other
+ * processes as their Snapshots are checkpointed to the canonical Datastore in
+ * the parent process.
+ *
+ * See `PBackgroundLSObserver.ipdl` for more info.
+ */
+class LSObserverChild final : public PBackgroundLSObserverChild {
+ friend class mozilla::ipc::BackgroundChildImpl;
+ friend class LSObserver;
+ friend class LSObject;
+
+ LSObserver* mObserver;
+
+ NS_DECL_OWNINGTHREAD
+
+ public:
+ void AssertIsOnOwningThread() const {
+ NS_ASSERT_OWNINGTHREAD(LSObserverChild);
+ }
+
+ private:
+ // Only created by LSObject.
+ explicit LSObserverChild(LSObserver* aObserver);
+
+ // Only destroyed by mozilla::ipc::BackgroundChildImpl.
+ ~LSObserverChild();
+
+ void SendDeleteMeInternal();
+
+ // IPDL methods are only called by IPDL.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvObserve(const PrincipalInfo& aPrinciplaInfo,
+ const uint32_t& aPrivateBrowsingId,
+ const nsAString& aDocumentURI,
+ const nsAString& aKey,
+ const LSValue& aOldValue,
+ const LSValue& aNewValue) override;
+};
+
+/**
+ * Minimal glue IPC-managed (new/delete) actor that is used by LSObject and its
+ * RequestHelper to perform synchronous requests on top of an asynchronous
+ * protocol.
+ *
+ * Takes an `LSReuestChildCallback` to be invoked when a response is received
+ * via __delete__.
+ *
+ * See `PBackgroundLSRequest.ipdl`, `LSObject`, and `RequestHelper` for more
+ * info.
+ */
+class LSRequestChild final : public PBackgroundLSRequestChild {
+ friend class LSObject;
+ friend class LocalStorageManager2;
+
+ RefPtr<LSRequestChildCallback> mCallback;
+
+ bool mFinishing;
+
+ NS_DECL_OWNINGTHREAD
+
+ public:
+ void AssertIsOnOwningThread() const {
+ NS_ASSERT_OWNINGTHREAD(LSReqeustChild);
+ }
+
+ bool Finishing() const;
+
+ private:
+ // Only created by LSObject.
+ LSRequestChild();
+
+ // Only destroyed by mozilla::ipc::BackgroundChildImpl.
+ ~LSRequestChild();
+
+ void SetCallback(LSRequestChildCallback* aCallback);
+
+ // IPDL methods are only called by IPDL.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult Recv__delete__(
+ const LSRequestResponse& aResponse) override;
+
+ mozilla::ipc::IPCResult RecvReady() override;
+};
+
+class NS_NO_VTABLE LSRequestChildCallback {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual void OnResponse(const LSRequestResponse& aResponse) = 0;
+
+ protected:
+ virtual ~LSRequestChildCallback() = default;
+};
+
+/**
+ * Minimal glue IPC-managed (new/delete) actor used by `LocalStorageManager2` to
+ * issue asynchronous requests in an asynchronous fashion.
+ *
+ * Takes an `LSSimpleRequestChildCallback` to be invoked when a response is
+ * received via __delete__.
+ *
+ * See `PBackgroundLSSimpleRequest.ipdl` for more info.
+ */
+class LSSimpleRequestChild final : public PBackgroundLSSimpleRequestChild {
+ friend class LocalStorageManager2;
+
+ RefPtr<LSSimpleRequestChildCallback> mCallback;
+
+ NS_DECL_OWNINGTHREAD
+
+ public:
+ void AssertIsOnOwningThread() const {
+ NS_ASSERT_OWNINGTHREAD(LSSimpleReqeustChild);
+ }
+
+ private:
+ // Only created by LocalStorageManager2.
+ LSSimpleRequestChild();
+
+ void SetCallback(LSSimpleRequestChildCallback* aCallback);
+
+ // Only destroyed by mozilla::ipc::BackgroundChildImpl.
+ ~LSSimpleRequestChild();
+
+ // IPDL methods are only called by IPDL.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult Recv__delete__(
+ const LSSimpleRequestResponse& aResponse) override;
+};
+
+class NS_NO_VTABLE LSSimpleRequestChildCallback {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual void OnResponse(const LSSimpleRequestResponse& aResponse) = 0;
+
+ protected:
+ virtual ~LSSimpleRequestChildCallback() = default;
+};
+
+/**
+ * Minimal IPC-managed (new/delete) actor that lasts as long as its owning
+ * LSSnapshot.
+ *
+ * Mutual raw pointers are maintained between LSSnapshot and this class that are
+ * cleared at either (expected) when the child starts the deletion process
+ * (SendDeleteMeInternal) or unexpected actor death (ActorDestroy).
+ *
+ * See `PBackgroundLSSnapshot.ipdl` and `LSSnapshot` for more info.
+ */
+class LSSnapshotChild final : public PBackgroundLSSnapshotChild {
+ friend class LSDatabase;
+ friend class LSSnapshot;
+
+ LSSnapshot* mSnapshot;
+
+ NS_DECL_OWNINGTHREAD
+
+ public:
+ void AssertIsOnOwningThread() const {
+ NS_ASSERT_OWNINGTHREAD(LSSnapshotChild);
+ }
+
+ private:
+ // Only created by LSDatabase.
+ explicit LSSnapshotChild(LSSnapshot* aSnapshot);
+
+ // Only destroyed by LSDatabaseChild.
+ ~LSSnapshotChild();
+
+ void SendDeleteMeInternal();
+
+ // IPDL methods are only called by IPDL.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvMarkDirty() override;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_ActorsChild_h
diff --git a/dom/localstorage/ActorsParent.cpp b/dom/localstorage/ActorsParent.cpp
new file mode 100644
index 0000000000..c4040fec1e
--- /dev/null
+++ b/dom/localstorage/ActorsParent.cpp
@@ -0,0 +1,9144 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "ActorsParent.h"
+
+// Local includes
+#include "LSInitializationTypes.h"
+#include "LSObject.h"
+#include "ReportInternalError.h"
+
+// Global includes
+#include <cinttypes>
+#include <cstdlib>
+#include <cstring>
+#include <new>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+#include "ErrorList.h"
+#include "MainThreadUtils.h"
+#include "mozIStorageAsyncConnection.h"
+#include "mozIStorageConnection.h"
+#include "mozIStorageFunction.h"
+#include "mozIStorageService.h"
+#include "mozIStorageStatement.h"
+#include "mozIStorageValueArray.h"
+#include "mozStorageCID.h"
+#include "mozStorageHelper.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MacroForEach.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/StoragePrincipalHelper.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Utf8.h"
+#include "mozilla/Variant.h"
+#include "mozilla/dom/ClientManagerService.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "mozilla/dom/LSSnapshot.h"
+#include "mozilla/dom/LSValue.h"
+#include "mozilla/dom/LSWriteOptimizer.h"
+#include "mozilla/dom/LSWriteOptimizerImpl.h"
+#include "mozilla/dom/LocalStorageCommon.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/PBackgroundLSDatabase.h"
+#include "mozilla/dom/PBackgroundLSDatabaseParent.h"
+#include "mozilla/dom/PBackgroundLSObserverParent.h"
+#include "mozilla/dom/PBackgroundLSRequestParent.h"
+#include "mozilla/dom/PBackgroundLSSharedTypes.h"
+#include "mozilla/dom/PBackgroundLSSimpleRequestParent.h"
+#include "mozilla/dom/PBackgroundLSSnapshotParent.h"
+#include "mozilla/dom/SnappyUtils.h"
+#include "mozilla/dom/StorageDBUpdater.h"
+#include "mozilla/dom/StorageUtils.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/dom/quota/CachingDatabaseConnection.h"
+#include "mozilla/dom/quota/CheckedUnsafePtr.h"
+#include "mozilla/dom/quota/Client.h"
+#include "mozilla/dom/quota/ClientImpl.h"
+#include "mozilla/dom/quota/DirectoryLock.h"
+#include "mozilla/dom/quota/FirstInitializationAttemptsImpl.h"
+#include "mozilla/dom/quota/OriginScope.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/StorageHelpers.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/QuotaObject.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/dom/quota/UsageInfo.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/PBackgroundParent.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/storage/Variant.h"
+#include "nsBaseHashtable.h"
+#include "nsCOMPtr.h"
+#include "nsClassHashtable.h"
+#include "nsTHashMap.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsHashKeys.h"
+#include "nsIBinaryInputStream.h"
+#include "nsIBinaryOutputStream.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsIEventTarget.h"
+#include "nsIFile.h"
+#include "nsIInputStream.h"
+#include "nsIObjectInputStream.h"
+#include "nsIObjectOutputStream.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIOutputStream.h"
+#include "nsIRunnable.h"
+#include "nsISerialEventTarget.h"
+#include "nsISupports.h"
+#include "nsIThread.h"
+#include "nsITimer.h"
+#include "nsIVariant.h"
+#include "nsInterfaceHashtable.h"
+#include "nsLiteralString.h"
+#include "nsNetUtil.h"
+#include "nsPointerHashKeys.h"
+#include "nsPrintfCString.h"
+#include "nsRefPtrHashtable.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsStringFlags.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTHashSet.h"
+#include "nsTLiteralString.h"
+#include "nsTStringRepr.h"
+#include "nsThreadUtils.h"
+#include "nsVariant.h"
+#include "nsXPCOM.h"
+#include "nsXULAppAPI.h"
+#include "nscore.h"
+#include "prenv.h"
+#include "prtime.h"
+
+#define LS_LOG_TEST() MOZ_LOG_TEST(GetLocalStorageLogger(), LogLevel::Info)
+#define LS_LOG(_args) MOZ_LOG(GetLocalStorageLogger(), LogLevel::Info, _args)
+
+#if defined(MOZ_WIDGET_ANDROID)
+# define LS_MOBILE
+#endif
+
+namespace mozilla::dom {
+
+using namespace mozilla::dom::quota;
+using namespace mozilla::dom::StorageUtils;
+using namespace mozilla::ipc;
+
+namespace {
+
+struct ArchivedOriginInfo;
+class ArchivedOriginScope;
+class Connection;
+class ConnectionThread;
+class Database;
+class Observer;
+class PrepareDatastoreOp;
+class PreparedDatastore;
+class QuotaClient;
+class Snapshot;
+
+using ArchivedOriginHashtable =
+ nsClassHashtable<nsCStringHashKey, ArchivedOriginInfo>;
+
+/*******************************************************************************
+ * Constants
+ ******************************************************************************/
+
+// Major schema version. Bump for almost everything.
+const uint32_t kMajorSchemaVersion = 5;
+
+// Minor schema version. Should almost always be 0 (maybe bump on release
+// branches if we have to).
+const uint32_t kMinorSchemaVersion = 0;
+
+// The schema version we store in the SQLite database is a (signed) 32-bit
+// integer. The major version is left-shifted 4 bits so the max value is
+// 0xFFFFFFF. The minor version occupies the lower 4 bits and its max is 0xF.
+static_assert(kMajorSchemaVersion <= 0xFFFFFFF,
+ "Major version needs to fit in 28 bits.");
+static_assert(kMinorSchemaVersion <= 0xF,
+ "Minor version needs to fit in 4 bits.");
+
+const int32_t kSQLiteSchemaVersion =
+ int32_t((kMajorSchemaVersion << 4) + kMinorSchemaVersion);
+
+// Changing the value here will override the page size of new databases only.
+// A journal mode change and VACUUM are needed to change existing databases, so
+// the best way to do that is to use the schema version upgrade mechanism.
+const uint32_t kSQLitePageSizeOverride =
+#ifdef LS_MOBILE
+ 512;
+#else
+ 1024;
+#endif
+
+static_assert(kSQLitePageSizeOverride == /* mozStorage default */ 0 ||
+ (kSQLitePageSizeOverride % 2 == 0 &&
+ kSQLitePageSizeOverride >= 512 &&
+ kSQLitePageSizeOverride <= 65536),
+ "Must be 0 (disabled) or a power of 2 between 512 and 65536!");
+
+// Set to some multiple of the page size to grow the database in larger chunks.
+const uint32_t kSQLiteGrowthIncrement = kSQLitePageSizeOverride * 2;
+
+static_assert(kSQLiteGrowthIncrement >= 0 &&
+ kSQLiteGrowthIncrement % kSQLitePageSizeOverride == 0 &&
+ kSQLiteGrowthIncrement < uint32_t(INT32_MAX),
+ "Must be 0 (disabled) or a positive multiple of the page size!");
+
+/**
+ * The database name for LocalStorage data in a per-origin directory.
+ */
+constexpr auto kDataFileName = u"data.sqlite"_ns;
+
+/**
+ * The journal corresponding to kDataFileName. (We don't use WAL mode.)
+ * Currently only needed in QuotaClient::InitOrigin and only in DEBUG builds.
+ * See the corresponding comment in QuotaClient::InitOrigin.
+ */
+#ifdef DEBUG
+constexpr auto kJournalFileName = u"data.sqlite-journal"_ns;
+#endif
+
+/**
+ * This file contains the current usage of the LocalStorage database as defined
+ * by the mozLength totals of all keys and values for the database, which
+ * differs from the actual size on disk. We store this value in a separate
+ * file as a cache so that we can initialize the QuotaClient faster.
+ * In the future, this file will be eliminated and the information will be
+ * stored in PROFILE/storage.sqlite or similar QuotaManager-wide storage.
+ *
+ * The file contains a binary verification cookie (32-bits) followed by the
+ * actual usage (64-bits).
+ */
+constexpr auto kUsageFileName = u"usage"_ns;
+
+/**
+ * Following a QuotaManager idiom, this journal file's existence is a marker
+ * that the usage file was in the process of being updated and is currently
+ * invalid. This file is created prior to updating the usage file and only
+ * deleted after the usage file has been written and closed and any pending
+ * database transactions have been committed. Note that this idiom is expected
+ * to work if Gecko crashes in the middle of a write, but is not expected to be
+ * foolproof in the face of a system crash, as we do not explicitly attempt to
+ * fsync the directory containing the journal file.
+ *
+ * If the journal file is found to exist at origin initialization time, the
+ * usage will be re-computed from the current state of DATA_FILE_NAME.
+ */
+constexpr auto kUsageJournalFileName = u"usage-journal"_ns;
+
+static const uint32_t kUsageFileSize = 12;
+static const uint32_t kUsageFileCookie = 0x420a420a;
+
+/**
+ * How long between the first moment we know we have data to be written on a
+ * `Connection` and when we should actually perform the write. This helps
+ * limit disk churn under silly usage patterns and is historically consistent
+ * with the previous, legacy implementation.
+ *
+ * Note that flushing happens downstream of Snapshot checkpointing and its
+ * batch mechanism which helps avoid wasteful IPC in the case of silly content
+ * code.
+ */
+const uint32_t kFlushTimeoutMs = 5000;
+
+const char kPrivateBrowsingObserverTopic[] = "last-pb-context-exited";
+
+const bool kDefaultShadowWrites = false;
+const uint32_t kDefaultSnapshotPrefill = 16384;
+const uint32_t kDefaultSnapshotGradualPrefill = 4096;
+const bool kDefaultClientValidation = true;
+/**
+ * Should all mutations also be reflected in the "shadow" database, which is
+ * the legacy webappsstore.sqlite database. When this is enabled, users can
+ * downgrade their version of Firefox and/or otherwise fall back to the legacy
+ * implementation without loss of data. (Older versions of Firefox will
+ * recognize the presence of ls-archive.sqlite and purge it and the other
+ * LocalStorage directories so privacy is maintained.)
+ */
+const char kShadowWritesPref[] = "dom.storage.shadow_writes";
+/**
+ * Byte budget for sending data down to the LSSnapshot instance when it is first
+ * created. If there is less data than this (measured by tallying the string
+ * length of the keys and values), all data is sent, otherwise partial data is
+ * sent. See `Snapshot`.
+ */
+const char kSnapshotPrefillPref[] = "dom.storage.snapshot_prefill";
+/**
+ * When a specific value is requested by an LSSnapshot that is not already fully
+ * populated, gradual prefill is used. This preference specifies the number of
+ * bytes to be used to send values beyond the specific value that is requested.
+ * (The size of the explicitly requested value does not impact this preference.)
+ * Setting the value to 0 disables gradual prefill. Tests may set this value to
+ * -1 which is converted to INT_MAX in order to cause gradual prefill to send
+ * all values not previously sent.
+ */
+const char kSnapshotGradualPrefillPref[] =
+ "dom.storage.snapshot_gradual_prefill";
+
+const char kClientValidationPref[] = "dom.storage.client_validation";
+
+/**
+ * The amount of time a PreparedDatastore instance should stick around after a
+ * preload is triggered in order to give time for the page to use LocalStorage
+ * without triggering worst-case synchronous jank.
+ */
+const uint32_t kPreparedDatastoreTimeoutMs = 20000;
+
+/**
+ * Cold storage for LocalStorage data extracted from webappsstore.sqlite at
+ * LSNG first-run that has not yet been migrated to its own per-origin directory
+ * by use.
+ *
+ * In other words, at first run, LSNG copies the contents of webappsstore.sqlite
+ * into this database. As requests are made for that LocalStorage data, the
+ * contents are removed from this database and placed into per-origin QM
+ * storage. So the contents of this database are always old, unused
+ * LocalStorage data that we can potentially get rid of at some point in the
+ * future.
+ */
+#define LS_ARCHIVE_FILE_NAME u"ls-archive.sqlite"
+/**
+ * The legacy LocalStorage database. Its contents are maintained as our
+ * "shadow" database so that LSNG can be disabled without loss of user data.
+ */
+#define WEB_APPS_STORE_FILE_NAME u"webappsstore.sqlite"
+
+// Shadow database Write Ahead Log's maximum size is 512KB
+const uint32_t kShadowMaxWALSize = 512 * 1024;
+
+const uint32_t kShadowJournalSizeLimit = kShadowMaxWALSize * 3;
+
+bool IsOnGlobalConnectionThread();
+
+void AssertIsOnGlobalConnectionThread();
+
+/*******************************************************************************
+ * SQLite functions
+ ******************************************************************************/
+
+int32_t MakeSchemaVersion(uint32_t aMajorSchemaVersion,
+ uint32_t aMinorSchemaVersion) {
+ return int32_t((aMajorSchemaVersion << 4) + aMinorSchemaVersion);
+}
+
+nsCString GetArchivedOriginHashKey(const nsACString& aOriginSuffix,
+ const nsACString& aOriginNoSuffix) {
+ return aOriginSuffix + ":"_ns + aOriginNoSuffix;
+}
+
+nsresult CreateDataTable(mozIStorageConnection* aConnection) {
+ return aConnection->ExecuteSimpleSQL(
+ "CREATE TABLE data"
+ "( key TEXT PRIMARY KEY"
+ ", utf16_length INTEGER NOT NULL"
+ ", conversion_type INTEGER NOT NULL"
+ ", compression_type INTEGER NOT NULL"
+ ", last_access_time INTEGER NOT NULL DEFAULT 0"
+ ", value BLOB NOT NULL"
+ ");"_ns);
+}
+
+nsresult CreateTables(mozIStorageConnection* aConnection) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(aConnection);
+
+ // Table `database`
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "CREATE TABLE database"
+ "( origin TEXT NOT NULL"
+ ", usage INTEGER NOT NULL DEFAULT 0"
+ ", last_vacuum_time INTEGER NOT NULL DEFAULT 0"
+ ", last_analyze_time INTEGER NOT NULL DEFAULT 0"
+ ", last_vacuum_size INTEGER NOT NULL DEFAULT 0"
+ ");"_ns)));
+
+ // Table `data`
+ QM_TRY(MOZ_TO_RESULT(CreateDataTable(aConnection)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(kSQLiteSchemaVersion)));
+
+ return NS_OK;
+}
+
+nsresult UpgradeSchemaFrom1_0To2_0(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "ALTER TABLE database ADD COLUMN usage INTEGER NOT NULL DEFAULT 0;"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "UPDATE database "
+ "SET usage = (SELECT total(utf16Length(key) + utf16Length(value)) "
+ "FROM data);"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(2, 0))));
+
+ return NS_OK;
+}
+
+nsresult UpgradeSchemaFrom2_0To3_0(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "ALTER TABLE data ADD COLUMN utf16Length INTEGER NOT NULL DEFAULT 0;"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "UPDATE data SET utf16Length = utf16Length(value);"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(3, 0))));
+
+ return NS_OK;
+}
+
+nsresult UpgradeSchemaFrom3_0To4_0(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(4, 0))));
+
+ return NS_OK;
+}
+
+nsresult UpgradeSchemaFrom4_0To5_0(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ // Recreate data table in new format following steps at
+ // https://www.sqlite.org/lang_altertable.html
+ // section "Making Other Kinds Of Table Schema Changes"
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "CREATE TABLE migrated_data"
+ "( key TEXT PRIMARY KEY"
+ ", utf16_length INTEGER NOT NULL"
+ ", conversion_type INTEGER NOT NULL"
+ ", compression_type INTEGER NOT NULL"
+ ", last_access_time INTEGER NOT NULL DEFAULT 0"
+ ", value BLOB NOT NULL"
+ ");"_ns)));
+
+ // Reinsert old data, all legacy data is UTF8
+ static_assert(1u ==
+ static_cast<uint8_t>(LSValue::ConversionType::UTF16_UTF8));
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "INSERT INTO migrated_data (key, utf16_length, conversion_type, "
+ "compression_type, last_access_time, value) "
+ "SELECT key, utf16Length, 1, compressed, lastAccessTime, value "
+ "FROM data;"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL("DROP TABLE data;"_ns)));
+
+ // Rename to data
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "ALTER TABLE migrated_data RENAME TO data;"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(5, 0))));
+
+ return NS_OK;
+}
+
+nsresult SetDefaultPragmas(mozIStorageConnection* aConnection) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->ExecuteSimpleSQL("PRAGMA synchronous = FULL;"_ns)));
+
+#ifndef LS_MOBILE
+ if (kSQLiteGrowthIncrement) {
+ // This is just an optimization so ignore the failure if the disk is
+ // currently too full.
+ QM_TRY(QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT(
+ aConnection->SetGrowthIncrement(kSQLiteGrowthIncrement, ""_ns)),
+ // Predicate.
+ IsSpecificError<NS_ERROR_FILE_TOO_BIG>,
+ // Fallback.
+ ErrToDefaultOk<>));
+ }
+#endif // LS_MOBILE
+
+ return NS_OK;
+}
+
+template <typename CorruptedFileHandler>
+Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateStorageConnection(
+ nsIFile& aDBFile, nsIFile& aUsageFile, const nsACString& aOrigin,
+ CorruptedFileHandler&& aCorruptedFileHandler) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+
+ // XXX Common logic should be refactored out of this method and
+ // cache::DBAction::OpenDBConnection, and maybe other similar functions.
+
+ QM_TRY_INSPECT(const auto& storageService,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ // XXX We can't use QM_OR_ELSE_WARN_IF because base-toolchains builds fail
+ // with: error: use of 'tryResult28' before deduction of 'auto'
+ QM_TRY_UNWRAP(
+ auto connection,
+ OrElseIf(
+ // Expression.
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, storageService, OpenDatabase,
+ &aDBFile, mozIStorageService::CONNECTION_DEFAULT),
+ // Predicate.
+ IsDatabaseCorruptionError,
+ // Fallback.
+ ([&aUsageFile, &aDBFile, &aCorruptedFileHandler,
+ &storageService](const nsresult rv)
+ -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
+ // Remove the usage file first (it might not exist at all due
+ // to corrupted state, which is ignored here).
+
+ // Usually we only use QM_OR_ELSE_LOG_VERBOSE(_IF) with Remove and
+ // NS_ERROR_FILE_NOT_FOUND check, but we're already in the rare case
+ // of corruption here, so the use of QM_OR_ELSE_WARN_IF is ok here.
+ QM_TRY(QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT(aUsageFile.Remove(false)),
+ // Predicate.
+ ([](const nsresult rv) {
+ return rv == NS_ERROR_FILE_NOT_FOUND;
+ }),
+ // Fallback.
+ ErrToDefaultOk<>));
+
+ // Call the corrupted file handler before trying to remove the
+ // database file, which might fail.
+ std::forward<CorruptedFileHandler>(aCorruptedFileHandler)();
+
+ // Nuke the database file.
+ QM_TRY(MOZ_TO_RESULT(aDBFile.Remove(false)));
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, storageService, OpenDatabase,
+ &aDBFile, mozIStorageService::CONNECTION_DEFAULT));
+ })));
+
+ QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(connection)));
+
+ // Check to make sure that the database schema is correct.
+ // XXX Try to make schemaVersion const.
+ QM_TRY_UNWRAP(int32_t schemaVersion,
+ MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion));
+
+ QM_TRY(OkIf(schemaVersion <= kSQLiteSchemaVersion), Err(NS_ERROR_FAILURE));
+
+ if (schemaVersion != kSQLiteSchemaVersion) {
+ const bool newDatabase = !schemaVersion;
+
+ if (newDatabase) {
+ // Set the page size first.
+ if (kSQLitePageSizeOverride) {
+ QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(nsPrintfCString(
+ "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride))));
+ }
+
+ // We have to set the auto_vacuum mode before opening a transaction.
+ QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(
+#ifdef LS_MOBILE
+ // Turn on full auto_vacuum mode to reclaim disk space on mobile
+ // devices (at the cost of some COMMIT speed).
+ "PRAGMA auto_vacuum = FULL;"_ns
+#else
+ // Turn on incremental auto_vacuum mode on desktop builds.
+ "PRAGMA auto_vacuum = INCREMENTAL;"_ns
+#endif
+ )));
+ }
+
+ bool vacuumNeeded = false;
+
+ mozStorageTransaction transaction(
+ connection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Start()));
+
+ if (newDatabase) {
+ QM_TRY(MOZ_TO_RESULT(CreateTables(connection)));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(
+ const int32_t& schemaVersion,
+ MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion),
+ QM_ASSERT_UNREACHABLE);
+
+ MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);
+ }
+#endif
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "INSERT INTO database (origin) VALUES (:origin)"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByName("origin"_ns, aOrigin)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+ } else {
+ // This logic needs to change next time we change the schema!
+ static_assert(kSQLiteSchemaVersion == int32_t((5 << 4) + 0),
+ "Upgrade function needed due to schema version increase.");
+
+ while (schemaVersion != kSQLiteSchemaVersion) {
+ if (schemaVersion == MakeSchemaVersion(1, 0)) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom1_0To2_0(connection)));
+ } else if (schemaVersion == MakeSchemaVersion(2, 0)) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom2_0To3_0(connection)));
+ } else if (schemaVersion == MakeSchemaVersion(3, 0)) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom3_0To4_0(connection)));
+ } else if (schemaVersion == MakeSchemaVersion(4, 0)) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom4_0To5_0(connection)));
+ vacuumNeeded = true;
+ } else {
+ LS_WARNING(
+ "Unable to open LocalStorage database, no upgrade path is "
+ "available!");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ QM_TRY_UNWRAP(schemaVersion, MOZ_TO_RESULT_INVOKE_MEMBER(
+ connection, GetSchemaVersion));
+ }
+
+ MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);
+ }
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
+
+ if (vacuumNeeded) {
+ QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL("VACUUM;"_ns)));
+ }
+
+ if (newDatabase) {
+ // Windows caches the file size, let's force it to stat the file again.
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, Exists));
+ Unused << exists;
+
+ QM_TRY_INSPECT(const int64_t& fileSize,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, GetFileSize));
+
+ MOZ_ASSERT(fileSize > 0);
+
+ const PRTime vacuumTime = PR_Now();
+ MOZ_ASSERT(vacuumTime);
+
+ QM_TRY_INSPECT(
+ const auto& vacuumTimeStmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr<mozIStorageStatement>,
+ connection, CreateStatement,
+ "UPDATE database "
+ "SET last_vacuum_time = :time"
+ ", last_vacuum_size = :size;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(
+ vacuumTimeStmt->BindInt64ByName("time"_ns, vacuumTime)));
+
+ QM_TRY(
+ MOZ_TO_RESULT(vacuumTimeStmt->BindInt64ByName("size"_ns, fileSize)));
+
+ QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->Execute()));
+ }
+ }
+
+ return connection;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult> GetStorageConnection(
+ const nsAString& aDatabaseFilePath) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(!aDatabaseFilePath.IsEmpty());
+ MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, u".sqlite"_ns));
+
+ QM_TRY_INSPECT(const auto& databaseFile, QM_NewLocalFile(aDatabaseFilePath));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(databaseFile, Exists));
+
+ QM_TRY(OkIf(exists), Err(NS_ERROR_FAILURE));
+
+ QM_TRY_INSPECT(const auto& ss,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ QM_TRY_UNWRAP(auto connection,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenDatabase,
+ databaseFile, mozIStorageService::CONNECTION_DEFAULT));
+
+ QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(connection)));
+
+ return connection;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetArchiveFile(
+ const nsAString& aStoragePath) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aStoragePath.IsEmpty());
+
+ QM_TRY_UNWRAP(auto archiveFile, QM_NewLocalFile(aStoragePath));
+
+ QM_TRY(MOZ_TO_RESULT(
+ archiveFile->Append(nsLiteralString(LS_ARCHIVE_FILE_NAME))));
+
+ return archiveFile;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult>
+CreateArchiveStorageConnection(const nsAString& aStoragePath) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aStoragePath.IsEmpty());
+
+ QM_TRY_INSPECT(const auto& archiveFile, GetArchiveFile(aStoragePath));
+
+ // QuotaManager ensures this file always exists.
+ DebugOnly<bool> exists;
+ MOZ_ASSERT(NS_SUCCEEDED(archiveFile->Exists(&exists)));
+ MOZ_ASSERT(exists);
+
+ QM_TRY_INSPECT(const bool& isDirectory,
+ MOZ_TO_RESULT_INVOKE_MEMBER(archiveFile, IsDirectory));
+
+ if (isDirectory) {
+ LS_WARNING("ls-archive is not a file!");
+ return nsCOMPtr<mozIStorageConnection>{};
+ }
+
+ QM_TRY_INSPECT(const auto& ss,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ QM_TRY_UNWRAP(
+ auto connection,
+ QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
+ archiveFile, mozIStorageService::CONNECTION_DEFAULT),
+ // Predicate.
+ IsDatabaseCorruptionError,
+ // Fallback. Don't throw an error, leave a corrupted ls-archive
+ // database as it is.
+ ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>));
+
+ if (connection) {
+ const nsresult rv = StorageDBUpdater::Update(connection);
+ if (NS_FAILED(rv)) {
+ // Don't throw an error, leave a non-updateable ls-archive database as
+ // it is.
+ return nsCOMPtr<mozIStorageConnection>{};
+ }
+ }
+
+ return connection;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetShadowFile(const nsAString& aBasePath) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(!aBasePath.IsEmpty());
+
+ QM_TRY_UNWRAP(auto archiveFile, QM_NewLocalFile(aBasePath));
+
+ QM_TRY(MOZ_TO_RESULT(
+ archiveFile->Append(nsLiteralString(WEB_APPS_STORE_FILE_NAME))));
+
+ return archiveFile;
+}
+
+nsresult SetShadowJournalMode(mozIStorageConnection* aConnection) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(aConnection);
+
+ // Try enabling WAL mode. This can fail in various circumstances so we have to
+ // check the results here.
+ constexpr auto journalModeQueryStart = "PRAGMA journal_mode = "_ns;
+ constexpr auto journalModeWAL = "wal"_ns;
+
+ QM_TRY_INSPECT(const auto& stmt,
+ CreateAndExecuteSingleStepStatement(
+ *aConnection, journalModeQueryStart + journalModeWAL));
+
+ QM_TRY_INSPECT(const auto& journalMode,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, *stmt,
+ GetUTF8String, 0));
+
+ if (journalMode.Equals(journalModeWAL)) {
+ // WAL mode successfully enabled. Set limits on its size here.
+
+ // Set the threshold for auto-checkpointing the WAL. We don't want giant
+ // logs slowing down us.
+ QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement(
+ *aConnection, "PRAGMA page_size;"_ns));
+
+ QM_TRY_INSPECT(const int32_t& pageSize,
+ MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0));
+
+ MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536);
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "PRAGMA wal_autocheckpoint = "_ns +
+ IntToCString(static_cast<int32_t>(kShadowMaxWALSize / pageSize)))));
+
+ // Set the maximum WAL log size to reduce footprint on mobile (large empty
+ // WAL files will be truncated)
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->ExecuteSimpleSQL("PRAGMA journal_size_limit = "_ns +
+ IntToCString(kShadowJournalSizeLimit))));
+ } else {
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->ExecuteSimpleSQL(journalModeQueryStart + "truncate"_ns)));
+ }
+
+ return NS_OK;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateShadowStorageConnection(
+ const nsAString& aBasePath) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(!aBasePath.IsEmpty());
+
+ QM_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath));
+
+ QM_TRY_INSPECT(const auto& ss,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ QM_TRY_UNWRAP(
+ auto connection,
+ QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
+ shadowFile, mozIStorageService::CONNECTION_DEFAULT),
+ // Predicate.
+ IsDatabaseCorruptionError,
+ // Fallback.
+ ([&shadowFile, &ss](const nsresult rv)
+ -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(shadowFile->Remove(false)));
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
+ shadowFile, mozIStorageService::CONNECTION_DEFAULT));
+ })));
+
+ QM_TRY(MOZ_TO_RESULT(SetShadowJournalMode(connection)));
+
+ // XXX Depending on whether the *first* call to OpenUnsharedDatabase above
+ // failed, we (a) might or (b) might not be dealing with a fresh database
+ // here. This is confusing, since in a failure of case (a) we would do the
+ // same thing again. Probably, the control flow should be changed here so that
+ // it's clear we only delete & create a fresh database once. If we still have
+ // a failure then, we better give up. Or, if we really want to handle that,
+ // the number of 2 retries seems arbitrary, and we should better do this in
+ // some loop until a maximum number of retries is reached.
+ //
+ // Compare this with QuotaManager::CreateLocalStorageArchiveConnection, which
+ // actually tracks if the file was removed before, but it's also more
+ // complicated than it should be. Maybe these two methods can be merged (which
+ // would mean that a parameter must be added that indicates whether it's
+ // handling the shadow file or not).
+ QM_TRY(QM_OR_ELSE_WARN(
+ // Expression.
+ MOZ_TO_RESULT(StorageDBUpdater::Update(connection)),
+ // Fallback.
+ ([&connection, &shadowFile, &ss](const nsresult) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(connection->Close()));
+ QM_TRY(MOZ_TO_RESULT(shadowFile->Remove(false)));
+
+ QM_TRY_UNWRAP(connection, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss,
+ OpenUnsharedDatabase, shadowFile,
+ mozIStorageService::CONNECTION_DEFAULT));
+
+ QM_TRY(MOZ_TO_RESULT(SetShadowJournalMode(connection)));
+
+ QM_TRY(
+ MOZ_TO_RESULT(StorageDBUpdater::CreateCurrentSchema(connection)));
+
+ return Ok{};
+ })));
+
+ return connection;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult> GetShadowStorageConnection(
+ const nsAString& aBasePath) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aBasePath.IsEmpty());
+
+ QM_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(shadowFile, Exists));
+
+ QM_TRY(OkIf(exists), Err(NS_ERROR_FAILURE));
+
+ QM_TRY_INSPECT(const auto& ss,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, shadowFile,
+ mozIStorageService::CONNECTION_DEFAULT));
+}
+
+nsresult AttachShadowDatabase(const nsAString& aBasePath,
+ mozIStorageConnection* aConnection) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(!aBasePath.IsEmpty());
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(shadowFile, Exists));
+
+ MOZ_ASSERT(exists);
+ }
+#endif
+
+ QM_TRY_INSPECT(const auto& path, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsString, shadowFile, GetPath));
+
+ QM_TRY_INSPECT(const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, aConnection,
+ CreateStatement, "ATTACH DATABASE :path AS shadow;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->BindStringByName("path"_ns, path)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+
+ return NS_OK;
+}
+
+nsresult DetachShadowDatabase(mozIStorageConnection* aConnection) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->ExecuteSimpleSQL("DETACH DATABASE shadow"_ns)));
+
+ return NS_OK;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetUsageFile(
+ const nsAString& aDirectoryPath) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(!aDirectoryPath.IsEmpty());
+
+ QM_TRY_UNWRAP(auto usageFile, QM_NewLocalFile(aDirectoryPath));
+
+ QM_TRY(MOZ_TO_RESULT(usageFile->Append(kUsageFileName)));
+
+ return usageFile;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetUsageJournalFile(
+ const nsAString& aDirectoryPath) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(!aDirectoryPath.IsEmpty());
+
+ QM_TRY_UNWRAP(auto usageJournalFile, QM_NewLocalFile(aDirectoryPath));
+
+ QM_TRY(MOZ_TO_RESULT(usageJournalFile->Append(kUsageJournalFileName)));
+
+ return usageJournalFile;
+}
+
+// Checks if aFile exists and is a file. Returns true if it exists and is a
+// file, false if it doesn't exist, and an error if it exists but isn't a file.
+Result<bool, nsresult> ExistsAsFile(nsIFile& aFile) {
+ enum class ExistsAsFileResult { DoesNotExist, IsDirectory, IsFile };
+
+ // This is an optimization to check both properties in one OS case, rather
+ // than calling Exists first, and then IsDirectory. IsDirectory also checks
+ // if the path exists. QM_OR_ELSE_WARN_IF is not used here since we just want
+ // to log NS_ERROR_FILE_NOT_FOUND result and not spam the reports.
+ QM_TRY_INSPECT(
+ const auto& res,
+ QM_OR_ELSE_LOG_VERBOSE_IF(
+ // Expression.
+ MOZ_TO_RESULT_INVOKE_MEMBER(aFile, IsDirectory)
+ .map([](const bool isDirectory) {
+ return isDirectory ? ExistsAsFileResult::IsDirectory
+ : ExistsAsFileResult::IsFile;
+ }),
+ // Predicate.
+ ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }),
+ // Fallback.
+ ErrToOk<ExistsAsFileResult::DoesNotExist>));
+
+ QM_TRY(OkIf(res != ExistsAsFileResult::IsDirectory), Err(NS_ERROR_FAILURE));
+
+ return res == ExistsAsFileResult::IsFile;
+}
+
+nsresult UpdateUsageFile(nsIFile* aUsageFile, nsIFile* aUsageJournalFile,
+ int64_t aUsage) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(aUsageFile);
+ MOZ_ASSERT(aUsageJournalFile);
+ MOZ_ASSERT(aUsage >= 0);
+
+ QM_TRY_INSPECT(const bool& usageJournalFileExists,
+ ExistsAsFile(*aUsageJournalFile));
+ if (!usageJournalFileExists) {
+ QM_TRY(MOZ_TO_RESULT(
+ aUsageJournalFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644)));
+ }
+
+ QM_TRY_INSPECT(const auto& stream, NS_NewLocalFileOutputStream(aUsageFile));
+
+ nsCOMPtr<nsIBinaryOutputStream> binaryStream =
+ NS_NewObjectOutputStream(stream);
+
+ QM_TRY(MOZ_TO_RESULT(binaryStream->Write32(kUsageFileCookie)));
+
+ QM_TRY(MOZ_TO_RESULT(binaryStream->Write64(aUsage)));
+
+#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG)
+ QM_TRY(MOZ_TO_RESULT(stream->Flush()));
+#endif
+
+ QM_TRY(MOZ_TO_RESULT(stream->Close()));
+
+ return NS_OK;
+}
+
+Result<UsageInfo, nsresult> LoadUsageFile(nsIFile& aUsageFile) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const int64_t& fileSize,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aUsageFile, GetFileSize));
+
+ QM_TRY(OkIf(fileSize == kUsageFileSize), Err(NS_ERROR_FILE_CORRUPTED));
+
+ QM_TRY_UNWRAP(auto stream, NS_NewLocalFileInputStream(&aUsageFile));
+
+ QM_TRY_INSPECT(const auto& bufferedStream,
+ NS_NewBufferedInputStream(stream.forget(), 16));
+
+ const nsCOMPtr<nsIBinaryInputStream> binaryStream =
+ NS_NewObjectInputStream(bufferedStream);
+
+ QM_TRY_INSPECT(const uint32_t& cookie,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
+
+ QM_TRY(OkIf(cookie == kUsageFileCookie), Err(NS_ERROR_FILE_CORRUPTED));
+
+ QM_TRY_INSPECT(const uint64_t& usage,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64));
+
+ return UsageInfo{DatabaseUsageType(Some(usage))};
+}
+
+/*******************************************************************************
+ * Non-actor class declarations
+ ******************************************************************************/
+
+/**
+ * Coalescing manipulation queue used by `Datastore`. Used by `Datastore` to
+ * update `Datastore::mOrderedItems` efficiently/for code simplification.
+ * (Datastore does not actually depend on the coalescing, as mutations are
+ * applied atomically when a Snapshot Checkpoints, and with `Datastore::mValues`
+ * being updated at the same time the mutations are applied to Datastore's
+ * mWriteOptimizer.)
+ */
+class DatastoreWriteOptimizer final : public LSWriteOptimizer<LSValue> {
+ public:
+ void ApplyAndReset(nsTArray<LSItemInfo>& aOrderedItems);
+};
+
+/**
+ * Coalescing manipulation queue used by `Connection`. Used by `Connection` to
+ * buffer and coalesce manipulations applied to the Datastore in batches by
+ * Snapshot Checkpointing until flushed to disk.
+ */
+class ConnectionWriteOptimizer final : public LSWriteOptimizer<LSValue> {
+ public:
+ // Returns the usage as the success value.
+ Result<int64_t, nsresult> Perform(Connection* aConnection,
+ bool aShadowWrites);
+
+ private:
+ /**
+ * Handlers for specific mutations. Each method knows how to `Perform` the
+ * manipulation against a `Connection` and the "shadow" database (legacy
+ * webappsstore.sqlite database that exists so LSNG can be disabled/safely
+ * downgraded from.)
+ */
+ nsresult PerformInsertOrUpdate(Connection* aConnection, bool aShadowWrites,
+ const nsAString& aKey, const LSValue& aValue);
+
+ nsresult PerformDelete(Connection* aConnection, bool aShadowWrites,
+ const nsAString& aKey);
+
+ nsresult PerformTruncate(Connection* aConnection, bool aShadowWrites);
+};
+
+class DatastoreOperationBase : public Runnable {
+ nsCOMPtr<nsIEventTarget> mOwningEventTarget;
+ nsresult mResultCode;
+ Atomic<bool> mMayProceedOnNonOwningThread;
+ bool mMayProceed;
+
+ public:
+ nsIEventTarget* OwningEventTarget() const {
+ MOZ_ASSERT(mOwningEventTarget);
+
+ return mOwningEventTarget;
+ }
+
+ bool IsOnOwningThread() const {
+ MOZ_ASSERT(mOwningEventTarget);
+
+ bool current;
+ return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(&current)) &&
+ current;
+ }
+
+ void AssertIsOnOwningThread() const {
+ MOZ_ASSERT(IsOnBackgroundThread());
+ MOZ_ASSERT(IsOnOwningThread());
+ }
+
+ nsresult ResultCode() const { return mResultCode; }
+
+ void SetFailureCode(nsresult aErrorCode) {
+ MOZ_ASSERT(NS_SUCCEEDED(mResultCode));
+ MOZ_ASSERT(NS_FAILED(aErrorCode));
+
+ mResultCode = aErrorCode;
+ }
+
+ void MaybeSetFailureCode(nsresult aErrorCode) {
+ MOZ_ASSERT(NS_FAILED(aErrorCode));
+
+ if (NS_SUCCEEDED(mResultCode)) {
+ mResultCode = aErrorCode;
+ }
+ }
+
+ void NoteComplete() {
+ AssertIsOnOwningThread();
+
+ mMayProceed = false;
+ mMayProceedOnNonOwningThread = false;
+ }
+
+ bool MayProceed() const {
+ AssertIsOnOwningThread();
+
+ return mMayProceed;
+ }
+
+ // May be called on any thread, but you should call MayProceed() if you know
+ // you're on the background thread because it is slightly faster.
+ bool MayProceedOnNonOwningThread() const {
+ return mMayProceedOnNonOwningThread;
+ }
+
+ protected:
+ DatastoreOperationBase()
+ : Runnable("dom::DatastoreOperationBase"),
+ mOwningEventTarget(GetCurrentEventTarget()),
+ mResultCode(NS_OK),
+ mMayProceedOnNonOwningThread(true),
+ mMayProceed(true) {}
+
+ ~DatastoreOperationBase() override { MOZ_ASSERT(!mMayProceed); }
+};
+
+class ConnectionDatastoreOperationBase : public DatastoreOperationBase {
+ protected:
+ RefPtr<Connection> mConnection;
+ /**
+ * This boolean flag is used by the CloseOp to avoid creating empty databases.
+ */
+ const bool mEnsureStorageConnection;
+
+ public:
+ // This callback will be called on the background thread before releasing the
+ // final reference to this request object. Subclasses may perform any
+ // additional cleanup here but must always call the base class implementation.
+ virtual void Cleanup();
+
+ protected:
+ ConnectionDatastoreOperationBase(Connection* aConnection,
+ bool aEnsureStorageConnection = true);
+
+ ~ConnectionDatastoreOperationBase();
+
+ // Must be overridden in subclasses. Called on the target thread to allow the
+ // subclass to perform necessary datastore operations. A successful return
+ // value will trigger an OnSuccess callback on the background thread while
+ // while a failure value will trigger an OnFailure callback.
+ virtual nsresult DoDatastoreWork() = 0;
+
+ // Methods that subclasses may implement.
+ virtual void OnSuccess();
+
+ virtual void OnFailure(nsresult aResultCode);
+
+ private:
+ void RunOnConnectionThread();
+
+ void RunOnOwningThread();
+
+ // Not to be overridden by subclasses.
+ NS_DECL_NSIRUNNABLE
+};
+
+class Connection final : public CachingDatabaseConnection {
+ friend class ConnectionThread;
+
+ class InitTemporaryOriginHelper;
+
+ class FlushOp;
+ class CloseOp;
+
+ RefPtr<ConnectionThread> mConnectionThread;
+ RefPtr<QuotaClient> mQuotaClient;
+ nsCOMPtr<nsITimer> mFlushTimer;
+ UniquePtr<ArchivedOriginScope> mArchivedOriginScope;
+ ConnectionWriteOptimizer mWriteOptimizer;
+ // XXX Consider changing this to ClientMetadata.
+ const OriginMetadata mOriginMetadata;
+ nsString mDirectoryPath;
+ /**
+ * Propagated from PrepareDatastoreOp. PrepareDatastoreOp may defer the
+ * creation of the localstorage client directory and database on the
+ * QuotaManager IO thread in its DatabaseWork method to
+ * Connection::EnsureStorageConnection, in which case the method needs to know
+ * it is responsible for taking those actions (without redundantly performing
+ * the existence checks).
+ */
+ const bool mDatabaseWasNotAvailable;
+ bool mHasCreatedDatabase;
+ bool mFlushScheduled;
+#ifdef DEBUG
+ bool mInUpdateBatch;
+ bool mFinished;
+#endif
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Connection)
+
+ void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(Connection); }
+
+ QuotaClient* GetQuotaClient() const {
+ MOZ_ASSERT(mQuotaClient);
+
+ return mQuotaClient;
+ }
+
+ ArchivedOriginScope* GetArchivedOriginScope() const {
+ return mArchivedOriginScope.get();
+ }
+
+ const nsCString& Origin() const { return mOriginMetadata.mOrigin; }
+
+ const nsString& DirectoryPath() const { return mDirectoryPath; }
+
+ void GetFinishInfo(bool& aDatabaseWasNotAvailable,
+ bool& aHasCreatedDatabase) const {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mFinished);
+
+ aDatabaseWasNotAvailable = mDatabaseWasNotAvailable;
+ aHasCreatedDatabase = mHasCreatedDatabase;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Methods which can only be called on the owning thread.
+
+ // This method is used to asynchronously execute a connection datastore
+ // operation on the connection thread.
+ void Dispatch(ConnectionDatastoreOperationBase* aOp);
+
+ // This method is used to asynchronously close the storage connection on the
+ // connection thread.
+ void Close(nsIRunnable* aCallback);
+
+ void SetItem(const nsString& aKey, const LSValue& aValue, int64_t aDelta,
+ bool aIsNewItem);
+
+ void RemoveItem(const nsString& aKey, int64_t aDelta);
+
+ void Clear(int64_t aDelta);
+
+ void BeginUpdateBatch();
+
+ void EndUpdateBatch();
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Methods which can only be called on the connection thread.
+
+ nsresult EnsureStorageConnection();
+
+ mozIStorageConnection* StorageConnection() const {
+ AssertIsOnGlobalConnectionThread();
+
+ return &MutableStorageConnection();
+ }
+
+ void CloseStorageConnection();
+
+ nsresult BeginWriteTransaction();
+
+ nsresult CommitWriteTransaction();
+
+ nsresult RollbackWriteTransaction();
+
+ private:
+ // Only created by ConnectionThread.
+ Connection(ConnectionThread* aConnectionThread,
+ const OriginMetadata& aOriginMetadata,
+ UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
+ bool aDatabaseWasNotAvailable);
+
+ ~Connection();
+
+ void ScheduleFlush();
+
+ void Flush();
+
+ static void FlushTimerCallback(nsITimer* aTimer, void* aClosure);
+};
+
+/**
+ * Helper to invoke EnsureTemporaryOriginIsInitialized on the QuotaManager IO
+ * thread from the LocalStorage connection thread when creating a database
+ * connection on demand. This is necessary because we attempt to defer the
+ * creation of the origin directory and the database until absolutely needed,
+ * but the directory creation and origin initialization must happen on the QM
+ * IO thread for invariant reasons. (We can't just use a mutex because there
+ * could be logic on the IO thread that also wants to deal with the same
+ * origin, so we need to queue a runnable and wait our turn.)
+ */
+class Connection::InitTemporaryOriginHelper final : public Runnable {
+ mozilla::Monitor mMonitor MOZ_UNANNOTATED;
+ const OriginMetadata mOriginMetadata;
+ nsString mOriginDirectoryPath;
+ nsresult mIOThreadResultCode;
+ bool mWaiting;
+
+ public:
+ explicit InitTemporaryOriginHelper(const OriginMetadata& aOriginMetadata)
+ : Runnable("dom::localstorage::Connection::InitTemporaryOriginHelper"),
+ mMonitor("InitTemporaryOriginHelper::mMonitor"),
+ mOriginMetadata(aOriginMetadata),
+ mIOThreadResultCode(NS_OK),
+ mWaiting(true) {
+ AssertIsOnGlobalConnectionThread();
+ }
+
+ Result<nsString, nsresult> BlockAndReturnOriginDirectoryPath();
+
+ private:
+ ~InitTemporaryOriginHelper() = default;
+
+ nsresult RunOnIOThread();
+
+ NS_DECL_NSIRUNNABLE
+};
+
+class Connection::FlushOp final : public ConnectionDatastoreOperationBase {
+ ConnectionWriteOptimizer mWriteOptimizer;
+ bool mShadowWrites;
+
+ public:
+ FlushOp(Connection* aConnection, ConnectionWriteOptimizer&& aWriteOptimizer);
+
+ private:
+ nsresult DoDatastoreWork() override;
+
+ void Cleanup() override;
+};
+
+class Connection::CloseOp final : public ConnectionDatastoreOperationBase {
+ nsCOMPtr<nsIRunnable> mCallback;
+
+ public:
+ CloseOp(Connection* aConnection, nsIRunnable* aCallback)
+ : ConnectionDatastoreOperationBase(aConnection,
+ /* aEnsureStorageConnection */ false),
+ mCallback(aCallback) {}
+
+ private:
+ nsresult DoDatastoreWork() override;
+
+ void Cleanup() override;
+};
+
+class ConnectionThread final {
+ friend class Connection;
+
+ nsCOMPtr<nsIThread> mThread;
+ nsRefPtrHashtable<nsCStringHashKey, Connection> mConnections;
+
+ public:
+ ConnectionThread();
+
+ void AssertIsOnOwningThread() const {
+ NS_ASSERT_OWNINGTHREAD(ConnectionThread);
+ }
+
+ bool IsOnConnectionThread();
+
+ void AssertIsOnConnectionThread();
+
+ already_AddRefed<Connection> CreateConnection(
+ const OriginMetadata& aOriginMetadata,
+ UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
+ bool aDatabaseWasNotAvailable);
+
+ void Shutdown();
+
+ NS_INLINE_DECL_REFCOUNTING(ConnectionThread)
+
+ private:
+ ~ConnectionThread();
+};
+
+/**
+ * Canonical state of Storage for an origin, containing all keys and their
+ * values in the parent process. Specifically, this is the state that will
+ * be handed out to freshly created Snapshots and that will be persisted to disk
+ * when the Connection's flush completes. State is mutated in batches as
+ * Snapshot instances Checkpoint their mutations locally accumulated in the
+ * child LSSnapshots.
+ */
+class Datastore final
+ : public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
+ RefPtr<DirectoryLock> mDirectoryLock;
+ RefPtr<Connection> mConnection;
+ RefPtr<QuotaObject> mQuotaObject;
+ nsCOMPtr<nsIRunnable> mCompleteCallback;
+ /**
+ * PrepareDatastoreOps register themselves with the Datastore at
+ * and unregister in PrepareDatastoreOp::Cleanup.
+ */
+ nsTHashSet<PrepareDatastoreOp*> mPrepareDatastoreOps;
+ /**
+ * PreparedDatastore instances register themselves with their associated
+ * Datastore at construction time and unregister at destruction time. They
+ * hang around for kPreparedDatastoreTimeoutMs in order to keep the Datastore
+ * from closing itself via MaybeClose(), thereby giving the document enough
+ * time to load and access LocalStorage.
+ */
+ nsTHashSet<PreparedDatastore*> mPreparedDatastores;
+ /**
+ * A database is live (and in this hashtable) if it has a live LSDatabase
+ * actor. There is at most one Database per origin per content process. Each
+ * Database corresponds to an LSDatabase in its associated content process.
+ */
+ nsTHashSet<Database*> mDatabases;
+ /**
+ * A database is active if it has a non-null `mSnapshot`. As long as there
+ * are any active databases final deltas can't be calculated and
+ * `UpdateUsage()` can't be invoked.
+ */
+ nsTHashSet<Database*> mActiveDatabases;
+ /**
+ * Non-authoritative hashtable representation of mOrderedItems for efficient
+ * lookup.
+ */
+ nsTHashMap<nsStringHashKey, LSValue> mValues;
+ /**
+ * The authoritative ordered state of the Datastore; mValue also exists as an
+ * unordered hashtable for efficient lookup.
+ */
+ nsTArray<LSItemInfo> mOrderedItems;
+ nsTArray<int64_t> mPendingUsageDeltas;
+ DatastoreWriteOptimizer mWriteOptimizer;
+ const OriginMetadata mOriginMetadata;
+ const uint32_t mPrivateBrowsingId;
+ int64_t mUsage;
+ int64_t mUpdateBatchUsage;
+ int64_t mSizeOfKeys;
+ int64_t mSizeOfItems;
+ bool mClosed;
+ bool mInUpdateBatch;
+ bool mHasLivePrivateDatastore;
+
+ public:
+ // Created by PrepareDatastoreOp.
+ Datastore(const OriginMetadata& aOriginMetadata, uint32_t aPrivateBrowsingId,
+ int64_t aUsage, int64_t aSizeOfKeys, int64_t aSizeOfItems,
+ RefPtr<DirectoryLock>&& aDirectoryLock,
+ RefPtr<Connection>&& aConnection,
+ RefPtr<QuotaObject>&& aQuotaObject,
+ nsTHashMap<nsStringHashKey, LSValue>& aValues,
+ nsTArray<LSItemInfo>&& aOrderedItems);
+
+ Maybe<DirectoryLock&> MaybeDirectoryLockRef() const {
+ AssertIsOnBackgroundThread();
+
+ return ToMaybeRef(mDirectoryLock.get());
+ }
+
+ const nsCString& Origin() const { return mOriginMetadata.mOrigin; }
+
+ uint32_t PrivateBrowsingId() const { return mPrivateBrowsingId; }
+
+ bool IsPersistent() const {
+ // Private-browsing is forbidden from touching disk, but
+ // StorageAccess::eSessionScoped is allowed to touch disk because
+ // QuotaManager's storage for such origins is wiped at shutdown.
+ return mPrivateBrowsingId == 0;
+ }
+
+ void Close();
+
+ bool IsClosed() const {
+ AssertIsOnBackgroundThread();
+
+ return mClosed;
+ }
+
+ void WaitForConnectionToComplete(nsIRunnable* aCallback);
+
+ void NoteLivePrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp);
+
+ void NoteFinishedPrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp);
+
+ void NoteLivePrivateDatastore();
+
+ void NoteFinishedPrivateDatastore();
+
+ void NoteLivePreparedDatastore(PreparedDatastore* aPreparedDatastore);
+
+ void NoteFinishedPreparedDatastore(PreparedDatastore* aPreparedDatastore);
+
+ bool HasOtherProcessDatabases(Database* aDatabase);
+
+ void NoteLiveDatabase(Database* aDatabase);
+
+ void NoteFinishedDatabase(Database* aDatabase);
+
+ void NoteActiveDatabase(Database* aDatabase);
+
+ void NoteInactiveDatabase(Database* aDatabase);
+
+ void GetSnapshotLoadInfo(const nsAString& aKey, bool& aAddKeyToUnknownItems,
+ nsTHashtable<nsStringHashKey>& aLoadedItems,
+ nsTArray<LSItemInfo>& aItemInfos,
+ uint32_t& aNextLoadIndex,
+ LSSnapshot::LoadState& aLoadState);
+
+ uint32_t GetLength() const { return mValues.Count(); }
+
+ const nsTArray<LSItemInfo>& GetOrderedItems() const { return mOrderedItems; }
+
+ void GetItem(const nsAString& aKey, LSValue& aValue) const;
+
+ void GetKeys(nsTArray<nsString>& aKeys) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Mutation Methods
+ //
+ // These are only called during Snapshot::Checkpoint
+
+ /**
+ * Used by Snapshot::Checkpoint to set a key/value pair as part of an
+ * explicit batch.
+ */
+ void SetItem(Database* aDatabase, const nsString& aKey,
+ const LSValue& aValue);
+
+ void RemoveItem(Database* aDatabase, const nsString& aKey);
+
+ void Clear(Database* aDatabase);
+
+ void BeginUpdateBatch(int64_t aSnapshotUsage);
+
+ int64_t EndUpdateBatch(int64_t aSnapshotPeakUsage);
+
+ int64_t GetUsage() const { return mUsage; }
+
+ int64_t AttemptToUpdateUsage(int64_t aMinSize, bool aInitial);
+
+ bool HasOtherProcessObservers(Database* aDatabase);
+
+ void NotifyOtherProcessObservers(Database* aDatabase,
+ const nsString& aDocumentURI,
+ const nsString& aKey,
+ const LSValue& aOldValue,
+ const LSValue& aNewValue);
+
+ void NoteChangedObserverArray(const nsTArray<NotNull<Observer*>>& aObservers);
+
+ void Stringify(nsACString& aResult) const;
+
+ NS_INLINE_DECL_REFCOUNTING(Datastore)
+
+ private:
+ // Reference counted.
+ ~Datastore();
+
+ bool UpdateUsage(int64_t aDelta);
+
+ void MaybeClose();
+
+ void ConnectionClosedCallback();
+
+ void CleanupMetadata();
+
+ void NotifySnapshots(Database* aDatabase, const nsAString& aKey,
+ const LSValue& aOldValue, bool aAffectsOrder);
+
+ void NoteChangedDatabaseMap();
+};
+
+class PrivateDatastore {
+ const NotNull<RefPtr<Datastore>> mDatastore;
+
+ public:
+ explicit PrivateDatastore(MovingNotNull<RefPtr<Datastore>> aDatastore)
+ : mDatastore(std::move(aDatastore)) {
+ AssertIsOnBackgroundThread();
+
+ mDatastore->NoteLivePrivateDatastore();
+ }
+
+ ~PrivateDatastore() { mDatastore->NoteFinishedPrivateDatastore(); }
+
+ const Datastore& DatastoreRef() const {
+ AssertIsOnBackgroundThread();
+
+ return *mDatastore;
+ }
+};
+
+class PreparedDatastore {
+ RefPtr<Datastore> mDatastore;
+ nsCOMPtr<nsITimer> mTimer;
+ const Maybe<ContentParentId> mContentParentId;
+ // Strings share buffers if possible, so it's not a problem to duplicate the
+ // origin here.
+ const nsCString mOrigin;
+ uint64_t mDatastoreId;
+ bool mForPreload;
+ bool mInvalidated;
+
+ public:
+ PreparedDatastore(Datastore* aDatastore,
+ const Maybe<ContentParentId>& aContentParentId,
+ const nsACString& aOrigin, uint64_t aDatastoreId,
+ bool aForPreload)
+ : mDatastore(aDatastore),
+ mTimer(NS_NewTimer()),
+ mContentParentId(aContentParentId),
+ mOrigin(aOrigin),
+ mDatastoreId(aDatastoreId),
+ mForPreload(aForPreload),
+ mInvalidated(false) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatastore);
+ MOZ_ASSERT(mTimer);
+
+ aDatastore->NoteLivePreparedDatastore(this);
+
+ MOZ_ALWAYS_SUCCEEDS(mTimer->InitWithNamedFuncCallback(
+ TimerCallback, this, kPreparedDatastoreTimeoutMs,
+ nsITimer::TYPE_ONE_SHOT, "PreparedDatastore::TimerCallback"));
+ }
+
+ ~PreparedDatastore() {
+ MOZ_ASSERT(mDatastore);
+ MOZ_ASSERT(mTimer);
+
+ mTimer->Cancel();
+
+ mDatastore->NoteFinishedPreparedDatastore(this);
+ }
+
+ const Datastore& DatastoreRef() const {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mDatastore);
+
+ return *mDatastore;
+ }
+
+ Datastore& MutableDatastoreRef() const {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mDatastore);
+
+ return *mDatastore;
+ }
+
+ const Maybe<ContentParentId>& GetContentParentId() const {
+ return mContentParentId;
+ }
+
+ const nsCString& Origin() const { return mOrigin; }
+
+ void Invalidate() {
+ AssertIsOnBackgroundThread();
+
+ mInvalidated = true;
+
+ if (mForPreload) {
+ mTimer->Cancel();
+
+ MOZ_ALWAYS_SUCCEEDS(mTimer->InitWithNamedFuncCallback(
+ TimerCallback, this, 0, nsITimer::TYPE_ONE_SHOT,
+ "PreparedDatastore::TimerCallback"));
+ }
+ }
+
+ bool IsInvalidated() const {
+ AssertIsOnBackgroundThread();
+
+ return mInvalidated;
+ }
+
+ private:
+ void Destroy();
+
+ static void TimerCallback(nsITimer* aTimer, void* aClosure);
+};
+
+/*******************************************************************************
+ * Actor class declarations
+ ******************************************************************************/
+
+class Database final
+ : public PBackgroundLSDatabaseParent,
+ public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
+ RefPtr<Datastore> mDatastore;
+ Snapshot* mSnapshot;
+ const PrincipalInfo mPrincipalInfo;
+ const Maybe<ContentParentId> mContentParentId;
+ // Strings share buffers if possible, so it's not a problem to duplicate the
+ // origin here.
+ nsCString mOrigin;
+ uint32_t mPrivateBrowsingId;
+ bool mAllowedToClose;
+ bool mActorDestroyed;
+ bool mRequestedAllowToClose;
+#ifdef DEBUG
+ bool mActorWasAlive;
+#endif
+
+ public:
+ // Created in AllocPBackgroundLSDatabaseParent.
+ Database(const PrincipalInfo& aPrincipalInfo,
+ const Maybe<ContentParentId>& aContentParentId,
+ const nsACString& aOrigin, uint32_t aPrivateBrowsingId);
+
+ Datastore* GetDatastore() const {
+ AssertIsOnBackgroundThread();
+ return mDatastore;
+ }
+
+ Maybe<Datastore&> MaybeDatastoreRef() const {
+ AssertIsOnBackgroundThread();
+
+ return ToMaybeRef(mDatastore.get());
+ }
+
+ const PrincipalInfo& GetPrincipalInfo() const { return mPrincipalInfo; }
+
+ bool IsOwnedByProcess(ContentParentId aContentParentId) const {
+ return mContentParentId && mContentParentId.value() == aContentParentId;
+ }
+
+ uint32_t PrivateBrowsingId() const { return mPrivateBrowsingId; }
+
+ const nsCString& Origin() const { return mOrigin; }
+
+ void SetActorAlive(Datastore* aDatastore);
+
+ void RegisterSnapshot(Snapshot* aSnapshot);
+
+ void UnregisterSnapshot(Snapshot* aSnapshot);
+
+ Snapshot* GetSnapshot() const {
+ AssertIsOnBackgroundThread();
+ return mSnapshot;
+ }
+
+ void RequestAllowToClose();
+
+ void ForceKill();
+
+ void Stringify(nsACString& aResult) const;
+
+ NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Database)
+
+ private:
+ // Reference counted.
+ ~Database();
+
+ void AllowToClose();
+
+ // IPDL methods are only called by IPDL.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvDeleteMe() override;
+
+ mozilla::ipc::IPCResult RecvAllowToClose() override;
+
+ PBackgroundLSSnapshotParent* AllocPBackgroundLSSnapshotParent(
+ const nsAString& aDocumentURI, const nsAString& aKey,
+ const bool& aIncreasePeakUsage, const int64_t& aMinSize,
+ LSSnapshotInitInfo* aInitInfo) override;
+
+ mozilla::ipc::IPCResult RecvPBackgroundLSSnapshotConstructor(
+ PBackgroundLSSnapshotParent* aActor, const nsAString& aDocumentURI,
+ const nsAString& aKey, const bool& aIncreasePeakUsage,
+ const int64_t& aMinSize, LSSnapshotInitInfo* aInitInfo) override;
+
+ bool DeallocPBackgroundLSSnapshotParent(
+ PBackgroundLSSnapshotParent* aActor) override;
+};
+
+/**
+ * Attempts to capture the state of the underlying Datastore at the time of its
+ * creation so run-to-completion semantics can be honored.
+ *
+ * Rather than simply duplicate the contents of `DataStore::mValues` and
+ * `Datastore::mOrderedItems` at the time of their creation, the Snapshot tracks
+ * mutations to the Datastore as they happen, saving off the state of values as
+ * they existed when the Snapshot was created. In other words, given an initial
+ * Datastore state of { foo: 'bar', bar: 'baz' }, the Snapshot won't store those
+ * values until it hears via `SaveItem` that "foo" is being over-written. At
+ * that time, it will save off foo='bar' in mValues.
+ *
+ * ## Quota Allocation ##
+ *
+ * ## States ##
+ *
+ */
+class Snapshot final : public PBackgroundLSSnapshotParent {
+ /**
+ * The Database that owns this snapshot. There is a 1:1 relationship between
+ * snapshots and databases.
+ */
+ RefPtr<Database> mDatabase;
+ RefPtr<Datastore> mDatastore;
+ /**
+ * The set of keys for which values have been sent to the child LSSnapshot.
+ * Cleared once all values have been sent as indicated by
+ * mLoadedItems.Count()==mTotalLength and therefore mLoadedAllItems should be
+ * true. No requests should be received for keys already in this set, and
+ * this is enforced by fatal IPC error (unless fuzzing).
+ */
+ nsTHashtable<nsStringHashKey> mLoadedItems;
+ /**
+ * The set of keys for which a RecvLoadValueAndMoreItems request was received
+ * but there was no such key, and so null was returned. The child LSSnapshot
+ * will also cache these values, so redundant requests are also handled with
+ * fatal process termination just like for mLoadedItems. Also cleared when
+ * mLoadedAllItems becomes true because then the child can infer that all
+ * other values must be null. (Note: this could also be done when
+ * mLoadKeysReceived is true as a further optimization, but is not.)
+ */
+ nsTHashSet<nsString> mUnknownItems;
+ /**
+ * Values that have changed in mDatastore as reported by SaveItem
+ * notifications that are not yet known to the child LSSnapshot.
+ *
+ * The naive way to snapshot the state of mDatastore would be to duplicate its
+ * internal mValues at the time of our creation, but that is wasteful if few
+ * changes are made to the Datastore's state. So we only track values that
+ * are changed/evicted from the Datastore as they happen, as reported to us by
+ * SaveItem notifications.
+ */
+ nsTHashMap<nsStringHashKey, LSValue> mValues;
+ /**
+ * Latched state of mDatastore's keys during a SaveItem notification with
+ * aAffectsOrder=true. The ordered keys needed to be saved off so that a
+ * consistent ordering could be presented to the child LSSnapshot when it asks
+ * for them via RecvLoadKeys.
+ */
+ nsTArray<nsString> mKeys;
+ nsString mDocumentURI;
+ /**
+ * The index used for restoring iteration over not yet sent key/value pairs to
+ * the child LSSnapshot.
+ */
+ uint32_t mNextLoadIndex;
+ /**
+ * The number of key/value pairs that were present in the Datastore at the
+ * time the snapshot was created. Once we have sent this many values to the
+ * child LSSnapshot, we can infer that it has received all of the keys/values
+ * and set mLoadedAllItems to true and clear mLoadedItems and mUnknownItems.
+ * Note that knowing the keys/values is not the same as knowing their ordering
+ * and so mKeys may be retained.
+ */
+ uint32_t mTotalLength;
+ int64_t mUsage;
+ int64_t mPeakUsage;
+ /**
+ * True if SaveItem has saved mDatastore's keys into mKeys because a SaveItem
+ * notification with aAffectsOrder=true was received.
+ */
+ bool mSavedKeys;
+ bool mActorDestroyed;
+ bool mFinishReceived;
+ bool mLoadedReceived;
+ /**
+ * True if LSSnapshot's mLoadState should be LoadState::AllOrderedItems or
+ * LoadState::AllUnorderedItems. It will be AllOrderedItems if the initial
+ * snapshot contained all the data or if the state was AllOrderedKeys and
+ * successive RecvLoadValueAndMoreItems requests have resulted in the
+ * LSSnapshot being told all of the key/value pairs. It will be
+ * AllUnorderedItems if the state was LoadState::Partial and successive
+ * RecvLoadValueAndMoreItem requests got all the keys/values but the key
+ * ordering was not retrieved.
+ */
+ bool mLoadedAllItems;
+ /**
+ * True if LSSnapshot's mLoadState should be LoadState::AllOrderedItems or
+ * AllOrderedKeys. This can occur because of the initial snapshot, or because
+ * a RecvLoadKeys request was received.
+ */
+ bool mLoadKeysReceived;
+ bool mSentMarkDirty;
+
+ /**
+ * True if there are Database objects in other content processes. The value
+ * never gets updated, we instead mark snapshots as dirty when Database
+ * objects are added or removed. Marking snapshots as dirty forces creation
+ * of new snapshots for new tasks.
+ */
+ bool mHasOtherProcessDatabases;
+ bool mHasOtherProcessObservers;
+
+ public:
+ // Created in AllocPBackgroundLSSnapshotParent.
+ Snapshot(Database* aDatabase, const nsAString& aDocumentURI);
+
+ void Init(nsTHashtable<nsStringHashKey>& aLoadedItems,
+ nsTHashSet<nsString>&& aUnknownItems, uint32_t aNextLoadIndex,
+ uint32_t aTotalLength, int64_t aUsage, int64_t aPeakUsage,
+ LSSnapshot::LoadState aLoadState, bool aHasOtherProcessDatabases,
+ bool aHasOtherProcessObservers) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aUsage >= 0);
+ MOZ_ASSERT(aPeakUsage >= aUsage);
+ MOZ_ASSERT_IF(aLoadState != LSSnapshot::LoadState::AllOrderedItems,
+ aNextLoadIndex < aTotalLength);
+ MOZ_ASSERT(mTotalLength == 0);
+ MOZ_ASSERT(mUsage == -1);
+ MOZ_ASSERT(mPeakUsage == -1);
+
+ mLoadedItems.SwapElements(aLoadedItems);
+ mUnknownItems = std::move(aUnknownItems);
+ mNextLoadIndex = aNextLoadIndex;
+ mTotalLength = aTotalLength;
+ mUsage = aUsage;
+ mPeakUsage = aPeakUsage;
+ if (aLoadState == LSSnapshot::LoadState::AllOrderedKeys) {
+ MOZ_ASSERT(mUnknownItems.Count() == 0);
+ mLoadKeysReceived = true;
+ } else if (aLoadState == LSSnapshot::LoadState::AllOrderedItems) {
+ MOZ_ASSERT(mLoadedItems.Count() == 0);
+ MOZ_ASSERT(mUnknownItems.Count() == 0);
+ MOZ_ASSERT(mNextLoadIndex == mTotalLength);
+ mLoadedReceived = true;
+ mLoadedAllItems = true;
+ mLoadKeysReceived = true;
+ }
+ mHasOtherProcessDatabases = aHasOtherProcessDatabases;
+ mHasOtherProcessObservers = aHasOtherProcessObservers;
+ }
+
+ /**
+ * Called via NotifySnapshots by Datastore whenever it is updating its
+ * internal state so that snapshots can save off the state of a value at the
+ * time of their creation.
+ */
+ void SaveItem(const nsAString& aKey, const LSValue& aOldValue,
+ bool aAffectsOrder);
+
+ void MarkDirty();
+
+ bool IsDirty() const {
+ AssertIsOnBackgroundThread();
+
+ return mSentMarkDirty;
+ }
+
+ bool HasOtherProcessDatabases() const {
+ AssertIsOnBackgroundThread();
+
+ return mHasOtherProcessDatabases;
+ }
+
+ bool HasOtherProcessObservers() const {
+ AssertIsOnBackgroundThread();
+
+ return mHasOtherProcessObservers;
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Snapshot)
+
+ private:
+ // Reference counted.
+ ~Snapshot();
+
+ mozilla::ipc::IPCResult Checkpoint(nsTArray<LSWriteInfo>&& aWriteInfos);
+
+ mozilla::ipc::IPCResult CheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos);
+
+ void Finish();
+
+ // IPDL methods are only called by IPDL.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvDeleteMe() override;
+
+ mozilla::ipc::IPCResult RecvAsyncCheckpoint(
+ nsTArray<LSWriteInfo>&& aWriteInfos) override;
+
+ mozilla::ipc::IPCResult RecvAsyncCheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) override;
+
+ mozilla::ipc::IPCResult RecvSyncCheckpoint(
+ nsTArray<LSWriteInfo>&& aWriteInfos) override;
+
+ mozilla::ipc::IPCResult RecvSyncCheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) override;
+
+ mozilla::ipc::IPCResult RecvAsyncFinish() override;
+
+ mozilla::ipc::IPCResult RecvSyncFinish() override;
+
+ mozilla::ipc::IPCResult RecvLoaded() override;
+
+ mozilla::ipc::IPCResult RecvLoadValueAndMoreItems(
+ const nsAString& aKey, LSValue* aValue,
+ nsTArray<LSItemInfo>* aItemInfos) override;
+
+ mozilla::ipc::IPCResult RecvLoadKeys(nsTArray<nsString>* aKeys) override;
+
+ mozilla::ipc::IPCResult RecvIncreasePeakUsage(const int64_t& aMinSize,
+ int64_t* aSize) override;
+};
+
+class Observer final : public PBackgroundLSObserverParent {
+ nsCString mOrigin;
+ bool mActorDestroyed;
+
+ public:
+ // Created in AllocPBackgroundLSObserverParent.
+ explicit Observer(const nsACString& aOrigin);
+
+ const nsCString& Origin() const { return mOrigin; }
+
+ void Observe(Database* aDatabase, const nsString& aDocumentURI,
+ const nsString& aKey, const LSValue& aOldValue,
+ const LSValue& aNewValue);
+
+ NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Observer)
+
+ private:
+ // Reference counted.
+ ~Observer();
+
+ // IPDL methods are only called by IPDL.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvDeleteMe() override;
+};
+
+class LSRequestBase : public DatastoreOperationBase,
+ public PBackgroundLSRequestParent {
+ protected:
+ enum class State {
+ // Just created on the PBackground thread. Next step is StartingRequest.
+ Initial,
+
+ // Waiting to start/starting request on the PBackground thread. Next step is
+ // either Nesting if a subclass needs to process more nested states or
+ // SendingReadyMessage if a subclass doesn't need any nested processing.
+ StartingRequest,
+
+ // Doing nested processing.
+ Nesting,
+
+ // Waiting to send/sending the ready message on the PBackground thread. Next
+ // step is WaitingForFinish.
+ SendingReadyMessage,
+
+ // Waiting for the finish message on the PBackground thread. Next step is
+ // SendingResults.
+ WaitingForFinish,
+
+ // Waiting to send/sending results on the PBackground thread. Next step is
+ // Completed.
+ SendingResults,
+
+ // All done.
+ Completed
+ };
+
+ const LSRequestParams mParams;
+ Maybe<ContentParentId> mContentParentId;
+ State mState;
+ bool mWaitingForFinish;
+
+ public:
+ LSRequestBase(const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ void Dispatch();
+
+ void StringifyState(nsACString& aResult) const;
+
+ virtual void Stringify(nsACString& aResult) const;
+
+ virtual void Log();
+
+ protected:
+ ~LSRequestBase() override;
+
+ virtual nsresult Start() = 0;
+
+ virtual nsresult NestedRun();
+
+ virtual void GetResponse(LSRequestResponse& aResponse) = 0;
+
+ virtual void Cleanup() {}
+
+ private:
+ bool VerifyRequestParams();
+
+ nsresult StartRequest();
+
+ void SendReadyMessage();
+
+ nsresult SendReadyMessageInternal();
+
+ void Finish();
+
+ void FinishInternal();
+
+ void SendResults();
+
+ protected:
+ // Common nsIRunnable implementation that subclasses may not override.
+ NS_IMETHOD
+ Run() final;
+
+ // IPDL methods.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ mozilla::ipc::IPCResult RecvCancel() final;
+
+ mozilla::ipc::IPCResult RecvFinish() final;
+};
+
+class PrepareDatastoreOp
+ : public LSRequestBase,
+ public OpenDirectoryListener,
+ public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
+ class LoadDataOp;
+
+ class CompressFunction;
+ class CompressionTypeFunction;
+
+ enum class NestedState {
+ // The nesting has not yet taken place. Next step is
+ // CheckExistingOperations.
+ BeforeNesting,
+
+ // Checking if a prepare datastore operation is already running for given
+ // origin on the PBackground thread. Next step is CheckClosingDatastore.
+ CheckExistingOperations,
+
+ // Checking if a datastore is closing the connection for given origin on
+ // the PBackground thread. Next step is PreparationPending.
+ CheckClosingDatastore,
+
+ // Ensuring quota manager is created and opening directory on the
+ // PBackground thread. Next step is either SendingResults if quota manager
+ // is not available or DirectoryOpenPending if quota manager is available.
+ // If a datastore already exists for given origin then the next state is
+ // SendingReadyMessage.
+ PreparationPending,
+
+ // Waiting for directory open allowed on the PBackground thread. The next
+ // step is either SendingReadyMessage if directory lock failed to acquire,
+ // or DatabaseWorkOpen if directory lock is acquired.
+ DirectoryOpenPending,
+
+ // Waiting to do/doing work on the QuotaManager IO thread. Its next step is
+ // BeginLoadData.
+ DatabaseWorkOpen,
+
+ // Starting a load data operation on the PBackground thread. Next step is
+ // DatabaseWorkLoadData.
+ BeginLoadData,
+
+ // Waiting to do/doing work on the connection thread. This involves waiting
+ // for the LoadDataOp to do its work. Eventually the state will transition
+ // to SendingReadyMessage.
+ DatabaseWorkLoadData,
+
+ // The nesting has completed.
+ AfterNesting
+ };
+
+ RefPtr<PrepareDatastoreOp> mDelayedOp;
+ RefPtr<DirectoryLock> mPendingDirectoryLock;
+ RefPtr<DirectoryLock> mDirectoryLock;
+ RefPtr<Connection> mConnection;
+ RefPtr<Datastore> mDatastore;
+ UniquePtr<ArchivedOriginScope> mArchivedOriginScope;
+ LoadDataOp* mLoadDataOp;
+ nsTHashMap<nsStringHashKey, LSValue> mValues;
+ nsTArray<LSItemInfo> mOrderedItems;
+ OriginMetadata mOriginMetadata;
+ nsCString mMainThreadOrigin;
+ nsString mDatabaseFilePath;
+ uint32_t mPrivateBrowsingId;
+ int64_t mUsage;
+ int64_t mSizeOfKeys;
+ int64_t mSizeOfItems;
+ uint64_t mDatastoreId;
+ NestedState mNestedState;
+ const bool mForPreload;
+ bool mDatabaseNotAvailable;
+ // Set when the Datastore has been registered with gPrivateDatastores so that
+ // it can be unregistered if an error is encountered in PrepareDatastoreOp.
+ FlippedOnce<false> mPrivateDatastoreRegistered;
+ // Set when the Datastore has been registered with gPreparedDatastores so
+ // that it can be unregistered if an error is encountered in
+ // PrepareDatastoreOp.
+ FlippedOnce<false> mPreparedDatastoreRegistered;
+ bool mInvalidated;
+
+#ifdef DEBUG
+ int64_t mDEBUGUsage;
+#endif
+
+ public:
+ PrepareDatastoreOp(const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ Maybe<DirectoryLock&> MaybeDirectoryLockRef() const {
+ AssertIsOnBackgroundThread();
+
+ return ToMaybeRef(mDirectoryLock.get());
+ }
+
+ bool OriginIsKnown() const {
+ MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread());
+
+ return !mOriginMetadata.mOrigin.IsEmpty();
+ }
+
+ const nsCString& Origin() const {
+ MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread());
+ MOZ_ASSERT(OriginIsKnown());
+
+ return mOriginMetadata.mOrigin;
+ }
+
+ void Invalidate() {
+ AssertIsOnOwningThread();
+
+ mInvalidated = true;
+ }
+
+ void StringifyNestedState(nsACString& aResult) const;
+
+ void Stringify(nsACString& aResult) const override;
+
+ void Log() override;
+
+ private:
+ ~PrepareDatastoreOp() override;
+
+ nsresult Start() override;
+
+ nsresult CheckExistingOperations();
+
+ nsresult CheckClosingDatastoreInternal();
+
+ nsresult CheckClosingDatastore();
+
+ nsresult BeginDatastorePreparationInternal();
+
+ nsresult BeginDatastorePreparation();
+
+ void SendToIOThread();
+
+ nsresult DatabaseWork();
+
+ nsresult DatabaseNotAvailable();
+
+ nsresult EnsureDirectoryEntry(nsIFile* aEntry, bool aCreateIfNotExists,
+ bool aDirectory,
+ bool* aAlreadyExisted = nullptr);
+
+ nsresult VerifyDatabaseInformation(mozIStorageConnection* aConnection);
+
+ already_AddRefed<QuotaObject> GetQuotaObject();
+
+ nsresult BeginLoadData();
+
+ void FinishNesting();
+
+ nsresult FinishNestingOnNonOwningThread();
+
+ nsresult NestedRun() override;
+
+ void GetResponse(LSRequestResponse& aResponse) override;
+
+ void Cleanup() override;
+
+ void ConnectionClosedCallback();
+
+ void CleanupMetadata();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // IPDL overrides.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ // OpenDirectoryListener overrides.
+ void DirectoryLockAcquired(DirectoryLock* aLock) override;
+
+ void DirectoryLockFailed() override;
+};
+
+class PrepareDatastoreOp::LoadDataOp final
+ : public ConnectionDatastoreOperationBase {
+ RefPtr<PrepareDatastoreOp> mPrepareDatastoreOp;
+
+ public:
+ explicit LoadDataOp(PrepareDatastoreOp* aPrepareDatastoreOp)
+ : ConnectionDatastoreOperationBase(aPrepareDatastoreOp->mConnection),
+ mPrepareDatastoreOp(aPrepareDatastoreOp) {}
+
+ private:
+ ~LoadDataOp() = default;
+
+ nsresult DoDatastoreWork() override;
+
+ void OnSuccess() override;
+
+ void OnFailure(nsresult aResultCode) override;
+
+ void Cleanup() override;
+};
+
+class PrepareDatastoreOp::CompressFunction final : public mozIStorageFunction {
+ private:
+ ~CompressFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+class PrepareDatastoreOp::CompressionTypeFunction final
+ : public mozIStorageFunction {
+ private:
+ ~CompressionTypeFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+class PrepareObserverOp : public LSRequestBase {
+ nsCString mOrigin;
+
+ public:
+ PrepareObserverOp(const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ private:
+ nsresult Start() override;
+
+ void GetResponse(LSRequestResponse& aResponse) override;
+};
+
+class LSSimpleRequestBase : public DatastoreOperationBase,
+ public PBackgroundLSSimpleRequestParent {
+ protected:
+ enum class State {
+ // Just created on the PBackground thread. Next step is StartingRequest.
+ Initial,
+
+ // Waiting to start/starting request on the PBackground thread. Next step is
+ // SendingResults.
+ StartingRequest,
+
+ // Waiting to send/sending results on the PBackground thread. Next step is
+ // Completed.
+ SendingResults,
+
+ // All done.
+ Completed
+ };
+
+ const LSSimpleRequestParams mParams;
+ Maybe<ContentParentId> mContentParentId;
+ State mState;
+
+ public:
+ LSSimpleRequestBase(const LSSimpleRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ void Dispatch();
+
+ protected:
+ ~LSSimpleRequestBase() override;
+
+ virtual nsresult Start() = 0;
+
+ virtual void GetResponse(LSSimpleRequestResponse& aResponse) = 0;
+
+ private:
+ bool VerifyRequestParams();
+
+ nsresult StartRequest();
+
+ void SendResults();
+
+ // Common nsIRunnable implementation that subclasses may not override.
+ NS_IMETHOD
+ Run() final;
+
+ // IPDL methods.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+};
+
+class PreloadedOp : public LSSimpleRequestBase {
+ nsCString mOrigin;
+
+ public:
+ PreloadedOp(const LSSimpleRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ private:
+ nsresult Start() override;
+
+ void GetResponse(LSSimpleRequestResponse& aResponse) override;
+};
+
+class GetStateOp : public LSSimpleRequestBase {
+ nsCString mOrigin;
+
+ public:
+ GetStateOp(const LSSimpleRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ private:
+ nsresult Start() override;
+
+ void GetResponse(LSSimpleRequestResponse& aResponse) override;
+};
+
+/*******************************************************************************
+ * Other class declarations
+ ******************************************************************************/
+
+struct ArchivedOriginInfo {
+ OriginAttributes mOriginAttributes;
+ nsCString mOriginNoSuffix;
+
+ ArchivedOriginInfo(const OriginAttributes& aOriginAttributes,
+ const nsACString& aOriginNoSuffix)
+ : mOriginAttributes(aOriginAttributes),
+ mOriginNoSuffix(aOriginNoSuffix) {}
+};
+
+class ArchivedOriginScope {
+ struct Origin {
+ nsCString mOriginSuffix;
+ nsCString mOriginNoSuffix;
+
+ Origin(const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix)
+ : mOriginSuffix(aOriginSuffix), mOriginNoSuffix(aOriginNoSuffix) {}
+
+ const nsACString& OriginSuffix() const { return mOriginSuffix; }
+
+ const nsACString& OriginNoSuffix() const { return mOriginNoSuffix; }
+ };
+
+ struct Prefix {
+ nsCString mOriginNoSuffix;
+
+ explicit Prefix(const nsACString& aOriginNoSuffix)
+ : mOriginNoSuffix(aOriginNoSuffix) {}
+
+ const nsACString& OriginNoSuffix() const { return mOriginNoSuffix; }
+ };
+
+ struct Pattern {
+ UniquePtr<OriginAttributesPattern> mPattern;
+
+ explicit Pattern(const OriginAttributesPattern& aPattern)
+ : mPattern(MakeUnique<OriginAttributesPattern>(aPattern)) {}
+
+ Pattern(const Pattern& aOther)
+ : mPattern(MakeUnique<OriginAttributesPattern>(*aOther.mPattern)) {}
+
+ Pattern(Pattern&& aOther) = default;
+
+ const OriginAttributesPattern& GetPattern() const {
+ MOZ_ASSERT(mPattern);
+ return *mPattern;
+ }
+ };
+
+ struct Null {};
+
+ using DataType = Variant<Origin, Pattern, Prefix, Null>;
+
+ DataType mData;
+
+ public:
+ static UniquePtr<ArchivedOriginScope> CreateFromOrigin(
+ const nsACString& aOriginAttrSuffix, const nsACString& aOriginKey);
+
+ static UniquePtr<ArchivedOriginScope> CreateFromPrefix(
+ const nsACString& aOriginKey);
+
+ static UniquePtr<ArchivedOriginScope> CreateFromPattern(
+ const OriginAttributesPattern& aPattern);
+
+ static UniquePtr<ArchivedOriginScope> CreateFromNull();
+
+ bool IsOrigin() const { return mData.is<Origin>(); }
+
+ bool IsPrefix() const { return mData.is<Prefix>(); }
+
+ bool IsPattern() const { return mData.is<Pattern>(); }
+
+ bool IsNull() const { return mData.is<Null>(); }
+
+ const nsACString& OriginSuffix() const {
+ MOZ_ASSERT(IsOrigin());
+
+ return mData.as<Origin>().OriginSuffix();
+ }
+
+ const nsACString& OriginNoSuffix() const {
+ MOZ_ASSERT(IsOrigin() || IsPrefix());
+
+ if (IsOrigin()) {
+ return mData.as<Origin>().OriginNoSuffix();
+ }
+ return mData.as<Prefix>().OriginNoSuffix();
+ }
+
+ const OriginAttributesPattern& GetPattern() const {
+ MOZ_ASSERT(IsPattern());
+
+ return mData.as<Pattern>().GetPattern();
+ }
+
+ nsLiteralCString GetBindingClause() const;
+
+ nsresult BindToStatement(mozIStorageStatement* aStatement) const;
+
+ bool HasMatches(ArchivedOriginHashtable* aHashtable) const;
+
+ void RemoveMatches(ArchivedOriginHashtable* aHashtable) const;
+
+ private:
+ // Move constructors
+ explicit ArchivedOriginScope(const Origin&& aOrigin) : mData(aOrigin) {}
+
+ explicit ArchivedOriginScope(const Pattern&& aPattern) : mData(aPattern) {}
+
+ explicit ArchivedOriginScope(const Prefix&& aPrefix) : mData(aPrefix) {}
+
+ explicit ArchivedOriginScope(const Null&& aNull) : mData(aNull) {}
+};
+
+class QuotaClient final : public mozilla::dom::quota::Client {
+ class Observer;
+ class MatchFunction;
+
+ static QuotaClient* sInstance;
+
+ Mutex mShadowDatabaseMutex MOZ_UNANNOTATED;
+
+ public:
+ QuotaClient();
+
+ static nsresult Initialize();
+
+ static QuotaClient* GetInstance() {
+ AssertIsOnBackgroundThread();
+
+ return sInstance;
+ }
+
+ mozilla::Mutex& ShadowDatabaseMutex() {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+
+ return mShadowDatabaseMutex;
+ }
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::QuotaClient, override)
+
+ Type GetType() override;
+
+ Result<UsageInfo, nsresult> InitOrigin(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) override;
+
+ nsresult InitOriginWithoutTracking(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) override;
+
+ Result<UsageInfo, nsresult> GetUsageForOrigin(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) override;
+
+ nsresult AboutToClearOrigins(
+ const Nullable<PersistenceType>& aPersistenceType,
+ const OriginScope& aOriginScope) override;
+
+ void OnOriginClearCompleted(PersistenceType aPersistenceType,
+ const nsACString& aOrigin) override;
+
+ void ReleaseIOThreadObjects() override;
+
+ void AbortOperationsForLocks(
+ const DirectoryLockIdTable& aDirectoryLockIds) override;
+
+ void AbortOperationsForProcess(ContentParentId aContentParentId) override;
+
+ void AbortAllOperations() override;
+
+ void StartIdleMaintenance() override;
+
+ void StopIdleMaintenance() override;
+
+ private:
+ ~QuotaClient() override;
+
+ void InitiateShutdown() override;
+ bool IsShutdownCompleted() const override;
+ nsCString GetShutdownStatus() const override;
+ void ForceKillActors() override;
+ void FinalizeShutdown() override;
+
+ Result<UniquePtr<ArchivedOriginScope>, nsresult> CreateArchivedOriginScope(
+ const OriginScope& aOriginScope);
+
+ nsresult PerformDelete(mozIStorageConnection* aConnection,
+ const nsACString& aSchemaName,
+ ArchivedOriginScope* aArchivedOriginScope) const;
+};
+
+class QuotaClient::Observer final : public nsIObserver {
+ public:
+ static nsresult Initialize();
+
+ private:
+ Observer() { MOZ_ASSERT(NS_IsMainThread()); }
+
+ ~Observer() { MOZ_ASSERT(NS_IsMainThread()); }
+
+ nsresult Init();
+
+ nsresult Shutdown();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+};
+
+class QuotaClient::MatchFunction final : public mozIStorageFunction {
+ OriginAttributesPattern mPattern;
+
+ public:
+ explicit MatchFunction(const OriginAttributesPattern& aPattern)
+ : mPattern(aPattern) {}
+
+ private:
+ ~MatchFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+/*******************************************************************************
+ * Helper classes
+ ******************************************************************************/
+
+class MOZ_STACK_CLASS AutoWriteTransaction final {
+ Connection* mConnection;
+ Maybe<MutexAutoLock> mShadowDatabaseLock;
+ bool mShadowWrites;
+
+ public:
+ explicit AutoWriteTransaction(bool aShadowWrites);
+
+ ~AutoWriteTransaction();
+
+ nsresult Start(Connection* aConnection);
+
+ nsresult Commit();
+
+ private:
+ nsresult LockAndAttachShadowDatabase(Connection* aConnection);
+
+ nsresult DetachShadowDatabaseAndUnlock();
+};
+
+/*******************************************************************************
+ * Globals
+ ******************************************************************************/
+
+#ifdef DEBUG
+bool gLocalStorageInitialized = false;
+#endif
+
+using PrepareDatastoreOpArray =
+ nsTArray<NotNull<CheckedUnsafePtr<PrepareDatastoreOp>>>;
+
+StaticAutoPtr<PrepareDatastoreOpArray> gPrepareDatastoreOps;
+
+// nsCStringHashKey with disabled memmove
+class nsCStringHashKeyDM : public nsCStringHashKey {
+ public:
+ explicit nsCStringHashKeyDM(const nsCStringHashKey::KeyTypePointer aKey)
+ : nsCStringHashKey(aKey) {}
+ enum { ALLOW_MEMMOVE = false };
+};
+
+// When CheckedUnsafePtr's checking is enabled, it's necessary to ensure that
+// the hashtable uses the copy constructor instead of memmove for moving entries
+// since memmove will break CheckedUnsafePtr in a memory-corrupting way.
+using DatastoreHashKey =
+ std::conditional<DiagnosticAssertEnabled::value, nsCStringHashKeyDM,
+ nsCStringHashKey>::type;
+
+using DatastoreHashtable =
+ nsBaseHashtable<DatastoreHashKey, NotNull<CheckedUnsafePtr<Datastore>>,
+ MovingNotNull<CheckedUnsafePtr<Datastore>>>;
+
+StaticAutoPtr<DatastoreHashtable> gDatastores;
+
+uint64_t gLastDatastoreId = 0;
+
+using PreparedDatastoreHashtable =
+ nsClassHashtable<nsUint64HashKey, PreparedDatastore>;
+
+StaticAutoPtr<PreparedDatastoreHashtable> gPreparedDatastores;
+
+using PrivateDatastoreHashtable =
+ nsClassHashtable<nsCStringHashKey, PrivateDatastore>;
+
+// Keeps Private Browsing Datastores alive until the private browsing session
+// is closed. This is necessary because LocalStorage Private Browsing data is
+// (currently) not written to disk and therefore needs to explicitly be kept
+// alive in memory so that if a user browses away from a site during a session
+// and then back to it that they will still have their data.
+//
+// The entries are wrapped by PrivateDatastore instances which call
+// NoteLivePrivateDatastore and NoteFinishedPrivateDatastore which set and
+// clear mHasLivePrivateDatastore which inhibits MaybeClose() from closing the
+// datastore (which would discard the data) when there are no active windows
+// using LocalStorage for the origin.
+//
+// The table is cleared when the Private Browsing session is closed, which will
+// cause NoteFinishedPrivateDatastore to be called on each Datastore which will
+// in turn call MaybeClose which should then discard the Datastore. Or in the
+// event of an (unlikely) race where the private browsing windows are still
+// being torn down, will cause the Datastore to be discarded when the last
+// window actually goes away.
+UniquePtr<PrivateDatastoreHashtable> gPrivateDatastores;
+
+using LiveDatabaseArray = nsTArray<NotNull<CheckedUnsafePtr<Database>>>;
+
+StaticAutoPtr<LiveDatabaseArray> gLiveDatabases;
+
+StaticRefPtr<ConnectionThread> gConnectionThread;
+
+uint64_t gLastObserverId = 0;
+
+using PreparedObserverHashtable = nsRefPtrHashtable<nsUint64HashKey, Observer>;
+
+StaticAutoPtr<PreparedObserverHashtable> gPreparedObsevers;
+
+using ObserverHashtable =
+ nsClassHashtable<nsCStringHashKey, nsTArray<NotNull<Observer*>>>;
+
+StaticAutoPtr<ObserverHashtable> gObservers;
+
+Atomic<bool> gShadowWrites(kDefaultShadowWrites);
+Atomic<int32_t, Relaxed> gSnapshotPrefill(kDefaultSnapshotPrefill);
+Atomic<int32_t, Relaxed> gSnapshotGradualPrefill(
+ kDefaultSnapshotGradualPrefill);
+Atomic<bool> gClientValidation(kDefaultClientValidation);
+
+using UsageHashtable = nsTHashMap<nsCStringHashKey, int64_t>;
+
+StaticAutoPtr<ArchivedOriginHashtable> gArchivedOrigins;
+
+// Can only be touched on the Quota Manager I/O thread.
+bool gInitializedShadowStorage = false;
+
+StaticAutoPtr<LSInitializationInfo> gInitializationInfo;
+
+bool IsOnGlobalConnectionThread() {
+ MOZ_ASSERT(gConnectionThread);
+ return gConnectionThread->IsOnConnectionThread();
+}
+
+void AssertIsOnGlobalConnectionThread() {
+ MOZ_ASSERT(gConnectionThread);
+ gConnectionThread->AssertIsOnConnectionThread();
+}
+
+already_AddRefed<Datastore> GetDatastore(const nsACString& aOrigin) {
+ AssertIsOnBackgroundThread();
+
+ if (gDatastores) {
+ auto maybeDatastore = gDatastores->MaybeGet(aOrigin);
+ if (maybeDatastore) {
+ RefPtr<Datastore> result(std::move(*maybeDatastore).unwrapBasePtr());
+ return result.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+nsresult LoadArchivedOrigins() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!gArchivedOrigins);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ // Ensure that the webappsstore.sqlite is moved to new place.
+ QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureStorageIsInitialized()));
+
+ QM_TRY_INSPECT(const auto& connection, CreateArchiveStorageConnection(
+ quotaManager->GetStoragePath()));
+
+ if (!connection) {
+ gArchivedOrigins = new ArchivedOriginHashtable();
+ return NS_OK;
+ }
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "SELECT DISTINCT originAttributes, originKey "
+ "FROM webappsstore2;"_ns));
+
+ auto archivedOrigins = MakeUnique<ArchivedOriginHashtable>();
+
+ // XXX Actually, this could use a hashtable variant of
+ // CollectElementsWhileHasResult
+ QM_TRY(quota::CollectWhileHasResult(
+ *stmt, [&archivedOrigins](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY_INSPECT(const auto& originSuffix,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
+ GetUTF8String, 0));
+ QM_TRY_INSPECT(const auto& originNoSuffix,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
+ GetUTF8String, 1));
+
+ const nsCString hashKey =
+ GetArchivedOriginHashKey(originSuffix, originNoSuffix);
+
+ OriginAttributes originAttributes;
+ QM_TRY(OkIf(originAttributes.PopulateFromSuffix(originSuffix)),
+ Err(NS_ERROR_FAILURE));
+
+ archivedOrigins->InsertOrUpdate(
+ hashKey,
+ MakeUnique<ArchivedOriginInfo>(originAttributes, originNoSuffix));
+
+ return Ok{};
+ }));
+
+ gArchivedOrigins = archivedOrigins.release();
+ return NS_OK;
+}
+
+Result<int64_t, nsresult> GetUsage(mozIStorageConnection& aConnection,
+ ArchivedOriginScope* aArchivedOriginScope) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ ([aArchivedOriginScope,
+ &aConnection]() -> Result<nsCOMPtr<mozIStorageStatement>, nsresult> {
+ if (aArchivedOriginScope) {
+ QM_TRY_RETURN(CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ aConnection,
+ "SELECT "
+ "total(utf16Length(key) + utf16Length(value)) "
+ "FROM webappsstore2 "
+ "WHERE originKey = :originKey "
+ "AND originAttributes = :originAttributes;"_ns,
+ [aArchivedOriginScope](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(
+ aArchivedOriginScope->BindToStatement(&stmt)));
+ return Ok{};
+ }));
+ }
+
+ QM_TRY_RETURN(CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ aConnection, "SELECT usage FROM database"_ns));
+ }()));
+
+ QM_TRY(OkIf(stmt), Err(NS_ERROR_FAILURE));
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 0));
+}
+
+void ShadowWritesPrefChangedCallback(const char* aPrefName, void* aClosure) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aPrefName, kShadowWritesPref));
+ MOZ_ASSERT(!aClosure);
+
+ gShadowWrites = Preferences::GetBool(aPrefName, kDefaultShadowWrites);
+}
+
+void SnapshotPrefillPrefChangedCallback(const char* aPrefName, void* aClosure) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aPrefName, kSnapshotPrefillPref));
+ MOZ_ASSERT(!aClosure);
+
+ int32_t snapshotPrefill =
+ Preferences::GetInt(aPrefName, kDefaultSnapshotPrefill);
+
+ // The magic -1 is for use only by tests.
+ if (snapshotPrefill == -1) {
+ snapshotPrefill = INT32_MAX;
+ }
+
+ gSnapshotPrefill = snapshotPrefill;
+}
+
+void SnapshotGradualPrefillPrefChangedCallback(const char* aPrefName,
+ void* aClosure) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aPrefName, kSnapshotGradualPrefillPref));
+ MOZ_ASSERT(!aClosure);
+
+ int32_t snapshotGradualPrefill =
+ Preferences::GetInt(aPrefName, kDefaultSnapshotGradualPrefill);
+
+ // The magic -1 is for use only by tests.
+ if (snapshotGradualPrefill == -1) {
+ snapshotGradualPrefill = INT32_MAX;
+ }
+
+ gSnapshotGradualPrefill = snapshotGradualPrefill;
+}
+
+int64_t GetSnapshotPeakUsagePreincrement(bool aInitial) {
+ return aInitial ? StaticPrefs::
+ dom_storage_snapshot_peak_usage_initial_preincrement()
+ : StaticPrefs::
+ dom_storage_snapshot_peak_usage_gradual_preincrement();
+}
+
+int64_t GetSnapshotPeakUsageReducedPreincrement(bool aInitial) {
+ return aInitial
+ ? StaticPrefs::
+ dom_storage_snapshot_peak_usage_reduced_initial_preincrement()
+ : StaticPrefs::
+ dom_storage_snapshot_peak_usage_reduced_gradual_preincrement();
+}
+
+void ClientValidationPrefChangedCallback(const char* aPrefName,
+ void* aClosure) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aPrefName, kClientValidationPref));
+ MOZ_ASSERT(!aClosure);
+
+ gClientValidation = Preferences::GetBool(aPrefName, kDefaultClientValidation);
+}
+
+template <typename Condition>
+void InvalidatePrepareDatastoreOpsMatching(const Condition& aCondition) {
+ if (!gPrepareDatastoreOps) {
+ return;
+ }
+
+ for (const auto& prepareDatastoreOp : *gPrepareDatastoreOps) {
+ if (aCondition(*prepareDatastoreOp)) {
+ prepareDatastoreOp->Invalidate();
+ }
+ }
+}
+
+template <typename Condition>
+void InvalidatePreparedDatastoresMatching(const Condition& aCondition) {
+ if (!gPreparedDatastores) {
+ return;
+ }
+
+ for (const auto& preparedDatastore : gPreparedDatastores->Values()) {
+ MOZ_ASSERT(preparedDatastore);
+
+ if (aCondition(*preparedDatastore)) {
+ preparedDatastore->Invalidate();
+ }
+ }
+}
+
+template <typename Condition>
+nsTArray<RefPtr<Database>> CollectDatabasesMatching(Condition aCondition) {
+ AssertIsOnBackgroundThread();
+
+ if (!gLiveDatabases) {
+ return nsTArray<RefPtr<Database>>{};
+ }
+
+ nsTArray<RefPtr<Database>> databases;
+
+ for (const auto& database : *gLiveDatabases) {
+ if (aCondition(*database)) {
+ databases.AppendElement(database.get());
+ }
+ }
+
+ return databases;
+}
+
+template <typename Condition>
+void RequestAllowToCloseDatabasesMatching(Condition aCondition) {
+ AssertIsOnBackgroundThread();
+
+ nsTArray<RefPtr<Database>> databases = CollectDatabasesMatching(aCondition);
+
+ for (const auto& database : databases) {
+ MOZ_ASSERT(database);
+
+ database->RequestAllowToClose();
+ }
+}
+
+void ForceKillAllDatabases() {
+ AssertIsOnBackgroundThread();
+
+ nsTArray<RefPtr<Database>> databases =
+ CollectDatabasesMatching([](const auto&) { return true; });
+
+ for (const auto& database : databases) {
+ MOZ_ASSERT(database);
+
+ database->ForceKill();
+ }
+}
+
+bool VerifyPrincipalInfo(const PrincipalInfo& aPrincipalInfo,
+ const PrincipalInfo& aStoragePrincipalInfo,
+ bool aCheckClientPrincipal) {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(aPrincipalInfo))) {
+ return false;
+ }
+
+ // Note that the client prinicpal could have a different spec than the node
+ // principal but they should have the same origin. It's because the client
+ // could be initialized when opening the initial about:blank document and pass
+ // to the newly opened window and reuse over there if the new window has the
+ // same origin as the initial about:blank document. But, the FilePath could be
+ // different. Therefore, we have to ignore comparing the Spec of the
+ // principals if we are verifying clinet principal here. Also, when
+ // document.domain is set, client principal won't get it. So, we don't compare
+ // domain for client princpal too.
+ bool result = aCheckClientPrincipal
+ ? StoragePrincipalHelper::
+ VerifyValidClientPrincipalInfoForPrincipalInfo(
+ aStoragePrincipalInfo, aPrincipalInfo)
+ : StoragePrincipalHelper::
+ VerifyValidStoragePrincipalInfoForPrincipalInfo(
+ aStoragePrincipalInfo, aPrincipalInfo);
+ if (NS_WARN_IF(!result)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool VerifyClientId(const Maybe<ContentParentId>& aContentParentId,
+ const Maybe<PrincipalInfo>& aPrincipalInfo,
+ const Maybe<nsID>& aClientId) {
+ AssertIsOnBackgroundThread();
+
+ if (gClientValidation) {
+ if (NS_WARN_IF(aClientId.isNothing())) {
+ return false;
+ }
+
+ if (NS_WARN_IF(aPrincipalInfo.isNothing())) {
+ return false;
+ }
+
+ RefPtr<ClientManagerService> svc = ClientManagerService::GetInstance();
+ if (svc && NS_WARN_IF(!svc->HasWindow(
+ aContentParentId, aPrincipalInfo.ref(), aClientId.ref()))) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool VerifyOriginKey(const nsACString& aOriginKey,
+ const PrincipalInfo& aPrincipalInfo) {
+ AssertIsOnBackgroundThread();
+
+ QM_TRY_INSPECT((const auto& [originAttrSuffix, originKey]),
+ GenerateOriginKey2(aPrincipalInfo), false);
+
+ Unused << originAttrSuffix;
+
+ QM_TRY(OkIf(originKey == aOriginKey), false,
+ ([&originKey = originKey, &aOriginKey](const auto) {
+ LS_WARNING("originKey (%s) doesn't match passed one (%s)!",
+ originKey.get(), nsCString(aOriginKey).get());
+ }));
+
+ return true;
+}
+
+LSInitializationInfo& MutableInitializationInfoRef(const CreateIfNonExistent&) {
+ if (!gInitializationInfo) {
+ gInitializationInfo = new LSInitializationInfo();
+ }
+ return *gInitializationInfo;
+}
+
+template <typename Func>
+auto ExecuteOriginInitialization(const nsACString& aOrigin,
+ const LSOriginInitialization aInitialization,
+ const nsACString& aContext, Func&& aFunc)
+ -> std::invoke_result_t<Func, const FirstInitializationAttempt<
+ LSOriginInitialization, Nothing>&> {
+ return ExecuteInitialization(
+ MutableInitializationInfoRef(CreateIfNonExistent{})
+ .MutableOriginInitializationInfoRef(aOrigin, CreateIfNonExistent{}),
+ aInitialization, aContext, std::forward<Func>(aFunc));
+}
+
+} // namespace
+
+/*******************************************************************************
+ * Exported functions
+ ******************************************************************************/
+
+void InitializeLocalStorage() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!gLocalStorageInitialized);
+
+ // XXX Isn't this redundant? It's already done in InitializeQuotaManager.
+ if (!QuotaManager::IsRunningGTests()) {
+ // This service has to be started on the main thread currently.
+ const nsCOMPtr<mozIStorageService> ss =
+ do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+
+ QM_WARNONLY_TRY(OkIf(ss));
+ }
+
+ QM_WARNONLY_TRY(QM_TO_RESULT(QuotaClient::Initialize()));
+
+ Preferences::RegisterCallbackAndCall(ShadowWritesPrefChangedCallback,
+ kShadowWritesPref);
+
+ Preferences::RegisterCallbackAndCall(SnapshotPrefillPrefChangedCallback,
+ kSnapshotPrefillPref);
+
+ Preferences::RegisterCallbackAndCall(
+ SnapshotGradualPrefillPrefChangedCallback, kSnapshotGradualPrefillPref);
+
+ Preferences::RegisterCallbackAndCall(ClientValidationPrefChangedCallback,
+ kClientValidationPref);
+
+#ifdef DEBUG
+ gLocalStorageInitialized = true;
+#endif
+}
+
+PBackgroundLSDatabaseParent* AllocPBackgroundLSDatabaseParent(
+ const PrincipalInfo& aPrincipalInfo, const uint32_t& aPrivateBrowsingId,
+ const uint64_t& aDatastoreId) {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(!gPreparedDatastores)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return nullptr;
+ }
+
+ PreparedDatastore* preparedDatastore = gPreparedDatastores->Get(aDatastoreId);
+ if (NS_WARN_IF(!preparedDatastore)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return nullptr;
+ }
+
+ // If we ever decide to return null from this point on, we need to make sure
+ // that the datastore is closed and the prepared datastore is removed from the
+ // gPreparedDatastores hashtable.
+ // We also assume that IPDL must call RecvPBackgroundLSDatabaseConstructor
+ // once we return a valid actor in this method.
+
+ RefPtr<Database> database =
+ new Database(aPrincipalInfo, preparedDatastore->GetContentParentId(),
+ preparedDatastore->Origin(), aPrivateBrowsingId);
+
+ // Transfer ownership to IPDL.
+ return database.forget().take();
+}
+
+bool RecvPBackgroundLSDatabaseConstructor(PBackgroundLSDatabaseParent* aActor,
+ const PrincipalInfo& aPrincipalInfo,
+ const uint32_t& aPrivateBrowsingId,
+ const uint64_t& aDatastoreId) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(gPreparedDatastores);
+ MOZ_ASSERT(gPreparedDatastores->Get(aDatastoreId));
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+
+ // The actor is now completely built (it has a manager, channel and it's
+ // registered as a subprotocol).
+ // ActorDestroy will be called if we fail here.
+
+ mozilla::UniquePtr<PreparedDatastore> preparedDatastore;
+ gPreparedDatastores->Remove(aDatastoreId, &preparedDatastore);
+ MOZ_ASSERT(preparedDatastore);
+
+ auto* database = static_cast<Database*>(aActor);
+
+ database->SetActorAlive(&preparedDatastore->MutableDatastoreRef());
+
+ // It's possible that AbortOperationsForLocks was called before the database
+ // actor was created and became live. Let the child know that the database is
+ // no longer valid.
+ if (preparedDatastore->IsInvalidated()) {
+ database->RequestAllowToClose();
+ }
+
+ return true;
+}
+
+bool DeallocPBackgroundLSDatabaseParent(PBackgroundLSDatabaseParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+
+ // Transfer ownership back from IPDL.
+ RefPtr<Database> actor = dont_AddRef(static_cast<Database*>(aActor));
+
+ return true;
+}
+
+PBackgroundLSObserverParent* AllocPBackgroundLSObserverParent(
+ const uint64_t& aObserverId) {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(!gPreparedObsevers)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return nullptr;
+ }
+
+ RefPtr<Observer> observer = gPreparedObsevers->Get(aObserverId);
+ if (NS_WARN_IF(!observer)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return nullptr;
+ }
+
+ // observer->SetObject(this);
+
+ // Transfer ownership to IPDL.
+ return observer.forget().take();
+}
+
+bool RecvPBackgroundLSObserverConstructor(PBackgroundLSObserverParent* aActor,
+ const uint64_t& aObserverId) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(gPreparedObsevers);
+ MOZ_ASSERT(gPreparedObsevers->GetWeak(aObserverId));
+
+ RefPtr<Observer> observer;
+ gPreparedObsevers->Remove(aObserverId, observer.StartAssignment());
+
+ if (!gPreparedObsevers->Count()) {
+ gPreparedObsevers = nullptr;
+ }
+
+ if (!gObservers) {
+ gObservers = new ObserverHashtable();
+ }
+
+ const auto notNullObserver = WrapNotNull(observer.get());
+
+ nsTArray<NotNull<Observer*>>* const array =
+ gObservers->GetOrInsertNew(notNullObserver->Origin());
+ array->AppendElement(notNullObserver);
+
+ if (RefPtr<Datastore> datastore = GetDatastore(observer->Origin())) {
+ datastore->NoteChangedObserverArray(*array);
+ }
+
+ return true;
+}
+
+bool DeallocPBackgroundLSObserverParent(PBackgroundLSObserverParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+
+ // Transfer ownership back from IPDL.
+ RefPtr<Observer> actor = dont_AddRef(static_cast<Observer*>(aActor));
+
+ return true;
+}
+
+PBackgroundLSRequestParent* AllocPBackgroundLSRequestParent(
+ PBackgroundParent* aBackgroundActor, const LSRequestParams& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aParams.type() != LSRequestParams::T__None);
+
+ if (NS_WARN_IF(!NextGenLocalStorageEnabled())) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+ return nullptr;
+ }
+
+ Maybe<ContentParentId> contentParentId;
+
+ uint64_t childID = BackgroundParent::GetChildID(aBackgroundActor);
+ if (childID) {
+ contentParentId = Some(ContentParentId(childID));
+ }
+
+ RefPtr<LSRequestBase> actor;
+
+ switch (aParams.type()) {
+ case LSRequestParams::TLSRequestPreloadDatastoreParams:
+ case LSRequestParams::TLSRequestPrepareDatastoreParams: {
+ RefPtr<PrepareDatastoreOp> prepareDatastoreOp =
+ new PrepareDatastoreOp(aParams, contentParentId);
+
+ if (!gPrepareDatastoreOps) {
+ gPrepareDatastoreOps = new PrepareDatastoreOpArray();
+ }
+ gPrepareDatastoreOps->AppendElement(
+ WrapNotNullUnchecked(prepareDatastoreOp.get()));
+
+ actor = std::move(prepareDatastoreOp);
+
+ break;
+ }
+
+ case LSRequestParams::TLSRequestPrepareObserverParams: {
+ RefPtr<PrepareObserverOp> prepareObserverOp =
+ new PrepareObserverOp(aParams, contentParentId);
+
+ actor = std::move(prepareObserverOp);
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+
+ // Transfer ownership to IPDL.
+ return actor.forget().take();
+}
+
+bool RecvPBackgroundLSRequestConstructor(PBackgroundLSRequestParent* aActor,
+ const LSRequestParams& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(aParams.type() != LSRequestParams::T__None);
+ MOZ_ASSERT(NextGenLocalStorageEnabled());
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+
+ // The actor is now completely built.
+
+ auto* op = static_cast<LSRequestBase*>(aActor);
+
+ op->Dispatch();
+
+ return true;
+}
+
+bool DeallocPBackgroundLSRequestParent(PBackgroundLSRequestParent* aActor) {
+ AssertIsOnBackgroundThread();
+
+ // Transfer ownership back from IPDL.
+ RefPtr<LSRequestBase> actor =
+ dont_AddRef(static_cast<LSRequestBase*>(aActor));
+
+ return true;
+}
+
+PBackgroundLSSimpleRequestParent* AllocPBackgroundLSSimpleRequestParent(
+ PBackgroundParent* aBackgroundActor, const LSSimpleRequestParams& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aParams.type() != LSSimpleRequestParams::T__None);
+
+ if (NS_WARN_IF(!NextGenLocalStorageEnabled())) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+ return nullptr;
+ }
+
+ Maybe<ContentParentId> contentParentId;
+
+ uint64_t childID = BackgroundParent::GetChildID(aBackgroundActor);
+ if (childID) {
+ contentParentId = Some(ContentParentId(childID));
+ }
+
+ RefPtr<LSSimpleRequestBase> actor;
+
+ switch (aParams.type()) {
+ case LSSimpleRequestParams::TLSSimpleRequestPreloadedParams: {
+ RefPtr<PreloadedOp> preloadedOp =
+ new PreloadedOp(aParams, contentParentId);
+
+ actor = std::move(preloadedOp);
+
+ break;
+ }
+
+ case LSSimpleRequestParams::TLSSimpleRequestGetStateParams: {
+ RefPtr<GetStateOp> getStateOp = new GetStateOp(aParams, contentParentId);
+
+ actor = std::move(getStateOp);
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+
+ // Transfer ownership to IPDL.
+ return actor.forget().take();
+}
+
+bool RecvPBackgroundLSSimpleRequestConstructor(
+ PBackgroundLSSimpleRequestParent* aActor,
+ const LSSimpleRequestParams& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(aParams.type() != LSSimpleRequestParams::T__None);
+ MOZ_ASSERT(NextGenLocalStorageEnabled());
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+
+ // The actor is now completely built.
+
+ auto* op = static_cast<LSSimpleRequestBase*>(aActor);
+
+ op->Dispatch();
+
+ return true;
+}
+
+bool DeallocPBackgroundLSSimpleRequestParent(
+ PBackgroundLSSimpleRequestParent* aActor) {
+ AssertIsOnBackgroundThread();
+
+ // Transfer ownership back from IPDL.
+ RefPtr<LSSimpleRequestBase> actor =
+ dont_AddRef(static_cast<LSSimpleRequestBase*>(aActor));
+
+ return true;
+}
+
+bool RecvLSClearPrivateBrowsing() {
+ AssertIsOnBackgroundThread();
+
+ gPrivateDatastores = nullptr;
+
+ if (gDatastores) {
+ for (const auto& datastore : gDatastores->Values()) {
+ if (datastore->PrivateBrowsingId()) {
+ datastore->Clear(nullptr);
+ }
+ }
+ }
+
+ return true;
+}
+
+namespace localstorage {
+
+already_AddRefed<mozilla::dom::quota::Client> CreateQuotaClient() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(CachedNextGenLocalStorageEnabled());
+
+ RefPtr<QuotaClient> client = new QuotaClient();
+ return client.forget();
+}
+
+} // namespace localstorage
+
+/*******************************************************************************
+ * DatastoreWriteOptimizer
+ ******************************************************************************/
+
+void DatastoreWriteOptimizer::ApplyAndReset(
+ nsTArray<LSItemInfo>& aOrderedItems) {
+ AssertIsOnOwningThread();
+
+ // The mWriteInfos hash table contains all write infos, but it keeps them in
+ // an arbitrary order, which means write infos need to be sorted before being
+ // processed. However, the order is not important for deletions and normal
+ // updates. Usually, filtering out deletions and updates would require extra
+ // work, but we have to check the hash table for each ordered item anyway, so
+ // we can remove the write info if it is a deletion or update without adding
+ // extra overhead. In the end, only insertions need to be sorted before being
+ // processed.
+
+ if (mTruncateInfo) {
+ aOrderedItems.Clear();
+ mTruncateInfo = nullptr;
+ }
+
+ for (int32_t index = aOrderedItems.Length() - 1; index >= 0; index--) {
+ LSItemInfo& item = aOrderedItems[index];
+
+ if (auto entry = mWriteInfos.Lookup(item.key())) {
+ WriteInfo* writeInfo = entry->get();
+
+ switch (writeInfo->GetType()) {
+ case WriteInfo::DeleteItem:
+ aOrderedItems.RemoveElementAt(index);
+ entry.Remove();
+ break;
+
+ case WriteInfo::UpdateItem: {
+ auto updateItemInfo = static_cast<UpdateItemInfo*>(writeInfo);
+ if (updateItemInfo->UpdateWithMove()) {
+ // See the comment in LSWriteOptimizer::InsertItem for more details
+ // about the UpdateWithMove flag.
+
+ aOrderedItems.RemoveElementAt(index);
+ entry.Data() = MakeUnique<InsertItemInfo>(
+ updateItemInfo->SerialNumber(), updateItemInfo->GetKey(),
+ updateItemInfo->GetValue());
+ } else {
+ item.value() = updateItemInfo->GetValue();
+ entry.Remove();
+ }
+ break;
+ }
+
+ case WriteInfo::InsertItem:
+ break;
+
+ default:
+ MOZ_CRASH("Bad type!");
+ }
+ }
+ }
+
+ nsTArray<NotNull<WriteInfo*>> writeInfos;
+ GetSortedWriteInfos(writeInfos);
+
+ for (WriteInfo* writeInfo : writeInfos) {
+ MOZ_ASSERT(writeInfo->GetType() == WriteInfo::InsertItem);
+
+ auto insertItemInfo = static_cast<InsertItemInfo*>(writeInfo);
+
+ LSItemInfo* itemInfo = aOrderedItems.AppendElement();
+ itemInfo->key() = insertItemInfo->GetKey();
+ itemInfo->value() = insertItemInfo->GetValue();
+ }
+
+ mWriteInfos.Clear();
+}
+
+/*******************************************************************************
+ * ConnectionWriteOptimizer
+ ******************************************************************************/
+
+Result<int64_t, nsresult> ConnectionWriteOptimizer::Perform(
+ Connection* aConnection, bool aShadowWrites) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ // The order of elements is not stored in the database, so write infos don't
+ // need to be sorted before being processed.
+
+ if (mTruncateInfo) {
+ QM_TRY(MOZ_TO_RESULT(PerformTruncate(aConnection, aShadowWrites)));
+ }
+
+ for (const auto& entry : mWriteInfos) {
+ const WriteInfo* const writeInfo = entry.GetWeak();
+
+ switch (writeInfo->GetType()) {
+ case WriteInfo::InsertItem:
+ case WriteInfo::UpdateItem: {
+ const auto* const insertItemInfo =
+ static_cast<const InsertItemInfo*>(writeInfo);
+
+ QM_TRY(MOZ_TO_RESULT(PerformInsertOrUpdate(
+ aConnection, aShadowWrites, insertItemInfo->GetKey(),
+ insertItemInfo->GetValue())));
+
+ break;
+ }
+
+ case WriteInfo::DeleteItem: {
+ const auto* const deleteItemInfo =
+ static_cast<const DeleteItemInfo*>(writeInfo);
+
+ QM_TRY(MOZ_TO_RESULT(PerformDelete(aConnection, aShadowWrites,
+ deleteItemInfo->GetKey())));
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Bad type!");
+ }
+ }
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement(
+ "UPDATE database "
+ "SET usage = usage + :delta"_ns,
+ [this](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName("delta"_ns, mTotalDelta)));
+
+ return Ok{};
+ })));
+
+ QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ aConnection->MutableStorageConnection(),
+ "SELECT usage FROM database"_ns));
+
+ QM_TRY(OkIf(stmt), Err(NS_ERROR_FAILURE));
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt64, 0));
+}
+
+nsresult ConnectionWriteOptimizer::PerformInsertOrUpdate(
+ Connection* aConnection, bool aShadowWrites, const nsAString& aKey,
+ const LSValue& aValue) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement(
+ "INSERT OR REPLACE INTO data (key, utf16_length, conversion_type, "
+ "compression_type, value) "
+ "VALUES(:key, :utf16_length, :conversion_type, :compression_type, :value)"_ns,
+ [&aKey, &aValue](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(stmt.BindStringByName("key"_ns, aKey)));
+ QM_TRY(MOZ_TO_RESULT(
+ stmt.BindInt32ByName("utf16_length"_ns, aValue.UTF16Length())));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindInt32ByName(
+ "conversion_type"_ns,
+ static_cast<int32_t>(aValue.GetConversionType()))));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindInt32ByName(
+ "compression_type"_ns,
+ static_cast<int32_t>(aValue.GetCompressionType()))));
+
+ if (0u == aValue.Length()) { // Otherwise empty string becomes null
+ QM_TRY(MOZ_TO_RESULT(
+ stmt.BindUTF8StringByName("value"_ns, aValue.AsCString())));
+ } else {
+ QM_TRY(MOZ_TO_RESULT(
+ stmt.BindUTF8StringAsBlobByName("value"_ns, aValue.AsCString())));
+ }
+
+ return Ok{};
+ })));
+
+ if (!aShadowWrites) {
+ return NS_OK;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement(
+ "INSERT OR REPLACE INTO shadow.webappsstore2 "
+ "(originAttributes, originKey, scope, key, value) "
+ "VALUES (:originAttributes, :originKey, :scope, :key, :value) "_ns,
+ [&aConnection, &aKey, &aValue](auto& stmt) -> Result<Ok, nsresult> {
+ using ConversionType = LSValue::ConversionType;
+ using CompressionType = LSValue::CompressionType;
+
+ const ArchivedOriginScope* const archivedOriginScope =
+ aConnection->GetArchivedOriginScope();
+
+ QM_TRY(MOZ_TO_RESULT(archivedOriginScope->BindToStatement(&stmt)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt.BindUTF8StringByName(
+ "scope"_ns, Scheme0Scope(archivedOriginScope->OriginSuffix(),
+ archivedOriginScope->OriginNoSuffix()))));
+
+ QM_TRY(MOZ_TO_RESULT(stmt.BindStringByName("key"_ns, aKey)));
+
+ bool isCompressed =
+ CompressionType::UNCOMPRESSED != aValue.GetCompressionType();
+ bool isAlreadyConverted =
+ ConversionType::NONE != aValue.GetConversionType();
+
+ nsCString buffer;
+ const nsCString& valueBlob = aValue.AsCString();
+ if (isCompressed) {
+ QM_TRY(OkIf(SnappyUncompress(valueBlob, buffer)),
+ Err(NS_ERROR_FAILURE));
+ }
+ const nsCString& value = isCompressed ? buffer : valueBlob;
+
+ // For shadow writes, we undo buffer swap and convert destructively
+ nsCString unconverted;
+ if (!isAlreadyConverted) {
+ nsString converted;
+ QM_TRY(OkIf(PutCStringBytesToString(value, converted)),
+ Err(NS_ERROR_OUT_OF_MEMORY));
+ QM_TRY(OkIf(CopyUTF16toUTF8(converted, unconverted, fallible)),
+ Err(NS_ERROR_OUT_OF_MEMORY)); // Corrupt invalid data
+ }
+ const nsCString& untransformed =
+ (!isAlreadyConverted) ? unconverted : value;
+
+ QM_TRY(MOZ_TO_RESULT(
+ stmt.BindUTF8StringByName("value"_ns, untransformed)));
+
+ return Ok{};
+ })));
+
+ return NS_OK;
+}
+
+nsresult ConnectionWriteOptimizer::PerformDelete(Connection* aConnection,
+ bool aShadowWrites,
+ const nsAString& aKey) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement(
+ "DELETE FROM data "
+ "WHERE key = :key;"_ns,
+ [&aKey](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(stmt.BindStringByName("key"_ns, aKey)));
+
+ return Ok{};
+ })));
+
+ if (!aShadowWrites) {
+ return NS_OK;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement(
+ "DELETE FROM shadow.webappsstore2 "
+ "WHERE originAttributes = :originAttributes "
+ "AND originKey = :originKey "
+ "AND key = :key;"_ns,
+ [&aConnection, &aKey](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->GetArchivedOriginScope()->BindToStatement(&stmt)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt.BindStringByName("key"_ns, aKey)));
+
+ return Ok{};
+ })));
+
+ return NS_OK;
+}
+
+nsresult ConnectionWriteOptimizer::PerformTruncate(Connection* aConnection,
+ bool aShadowWrites) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->ExecuteCachedStatement("DELETE FROM data;"_ns)));
+
+ if (!aShadowWrites) {
+ return NS_OK;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement(
+ "DELETE FROM shadow.webappsstore2 "
+ "WHERE originAttributes = :originAttributes "
+ "AND originKey = :originKey;"_ns,
+ [&aConnection](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->GetArchivedOriginScope()->BindToStatement(&stmt)));
+
+ return Ok{};
+ })));
+
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * DatastoreOperationBase
+ ******************************************************************************/
+
+/*******************************************************************************
+ * ConnectionDatastoreOperationBase
+ ******************************************************************************/
+
+ConnectionDatastoreOperationBase::ConnectionDatastoreOperationBase(
+ Connection* aConnection, bool aEnsureStorageConnection)
+ : mConnection(aConnection),
+ mEnsureStorageConnection(aEnsureStorageConnection) {
+ MOZ_ASSERT(aConnection);
+}
+
+ConnectionDatastoreOperationBase::~ConnectionDatastoreOperationBase() {
+ MOZ_ASSERT(!mConnection,
+ "ConnectionDatabaseOperationBase::Cleanup() was not called by a "
+ "subclass!");
+}
+
+void ConnectionDatastoreOperationBase::Cleanup() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mConnection);
+
+ mConnection = nullptr;
+
+ NoteComplete();
+}
+
+void ConnectionDatastoreOperationBase::OnSuccess() { AssertIsOnOwningThread(); }
+
+void ConnectionDatastoreOperationBase::OnFailure(nsresult aResultCode) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(NS_FAILED(aResultCode));
+}
+
+void ConnectionDatastoreOperationBase::RunOnConnectionThread() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+
+ if (!MayProceedOnNonOwningThread()) {
+ SetFailureCode(NS_ERROR_ABORT);
+ } else {
+ nsresult rv = NS_OK;
+
+ // The boolean flag is only used by the CloseOp to avoid creating empty
+ // databases.
+ if (mEnsureStorageConnection) {
+ rv = mConnection->EnsureStorageConnection();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ SetFailureCode(rv);
+ } else {
+ MOZ_ASSERT(mConnection->HasStorageConnection());
+ }
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = DoDatastoreWork();
+ if (NS_FAILED(rv)) {
+ SetFailureCode(rv);
+ }
+ }
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+}
+
+void ConnectionDatastoreOperationBase::RunOnOwningThread() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mConnection);
+
+ if (!MayProceed()) {
+ MaybeSetFailureCode(NS_ERROR_ABORT);
+ }
+
+ if (NS_SUCCEEDED(ResultCode())) {
+ OnSuccess();
+ } else {
+ OnFailure(ResultCode());
+ }
+
+ Cleanup();
+}
+
+NS_IMETHODIMP
+ConnectionDatastoreOperationBase::Run() {
+ if (IsOnGlobalConnectionThread()) {
+ RunOnConnectionThread();
+ } else {
+ RunOnOwningThread();
+ }
+
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * Connection implementation
+ ******************************************************************************/
+
+Connection::Connection(ConnectionThread* aConnectionThread,
+ const OriginMetadata& aOriginMetadata,
+ UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
+ bool aDatabaseWasNotAvailable)
+ : mConnectionThread(aConnectionThread),
+ mQuotaClient(QuotaClient::GetInstance()),
+ mArchivedOriginScope(std::move(aArchivedOriginScope)),
+ mOriginMetadata(aOriginMetadata),
+ mDatabaseWasNotAvailable(aDatabaseWasNotAvailable),
+ mHasCreatedDatabase(false),
+ mFlushScheduled(false)
+#ifdef DEBUG
+ ,
+ mInUpdateBatch(false),
+ mFinished(false)
+#endif
+{
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!aOriginMetadata.mGroup.IsEmpty());
+ MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty());
+}
+
+Connection::~Connection() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mFlushScheduled);
+ MOZ_ASSERT(!mInUpdateBatch);
+ MOZ_ASSERT(mFinished);
+}
+
+void Connection::Dispatch(ConnectionDatastoreOperationBase* aOp) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mConnectionThread);
+
+ MOZ_ALWAYS_SUCCEEDS(
+ mConnectionThread->mThread->Dispatch(aOp, NS_DISPATCH_NORMAL));
+}
+
+void Connection::Close(nsIRunnable* aCallback) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aCallback);
+
+ if (mFlushScheduled) {
+ MOZ_ASSERT(mFlushTimer);
+ MOZ_ALWAYS_SUCCEEDS(mFlushTimer->Cancel());
+
+ Flush();
+
+ mFlushTimer = nullptr;
+ }
+
+ RefPtr<CloseOp> op = new CloseOp(this, aCallback);
+
+ Dispatch(op);
+}
+
+void Connection::SetItem(const nsString& aKey, const LSValue& aValue,
+ int64_t aDelta, bool aIsNewItem) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mInUpdateBatch);
+
+ if (aIsNewItem) {
+ mWriteOptimizer.InsertItem(aKey, aValue, aDelta);
+ } else {
+ mWriteOptimizer.UpdateItem(aKey, aValue, aDelta);
+ }
+}
+
+void Connection::RemoveItem(const nsString& aKey, int64_t aDelta) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mInUpdateBatch);
+
+ mWriteOptimizer.DeleteItem(aKey, aDelta);
+}
+
+void Connection::Clear(int64_t aDelta) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mInUpdateBatch);
+
+ mWriteOptimizer.Truncate(aDelta);
+}
+
+void Connection::BeginUpdateBatch() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mInUpdateBatch);
+
+#ifdef DEBUG
+ mInUpdateBatch = true;
+#endif
+}
+
+void Connection::EndUpdateBatch() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mInUpdateBatch);
+
+ if (mWriteOptimizer.HasWrites() && !mFlushScheduled) {
+ ScheduleFlush();
+ }
+
+#ifdef DEBUG
+ mInUpdateBatch = false;
+#endif
+}
+
+nsresult Connection::EnsureStorageConnection() {
+ AssertIsOnGlobalConnectionThread();
+
+ if (HasStorageConnection()) {
+ return NS_OK;
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ if (!mDatabaseWasNotAvailable || mHasCreatedDatabase) {
+ QM_TRY_INSPECT(const auto& directoryEntry,
+ quotaManager->GetDirectoryForOrigin(PERSISTENCE_TYPE_DEFAULT,
+ Origin()));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(
+ NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME))));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->GetPath(mDirectoryPath)));
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(kDataFileName)));
+
+ QM_TRY_INSPECT(
+ const auto& databaseFilePath,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, directoryEntry, GetPath));
+
+ QM_TRY_UNWRAP(auto storageConnection,
+ GetStorageConnection(databaseFilePath));
+ LazyInit(WrapMovingNotNull(std::move(storageConnection)));
+
+ return NS_OK;
+ }
+
+ RefPtr<InitTemporaryOriginHelper> helper =
+ new InitTemporaryOriginHelper(mOriginMetadata);
+
+ QM_TRY_INSPECT(const auto& originDirectoryPath,
+ helper->BlockAndReturnOriginDirectoryPath());
+
+ QM_TRY_INSPECT(const auto& directoryEntry,
+ QM_NewLocalFile(originDirectoryPath));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(
+ NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME))));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->GetPath(mDirectoryPath)));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directoryEntry, Exists));
+
+ if (!exists) {
+ QM_TRY(
+ MOZ_TO_RESULT(directoryEntry->Create(nsIFile::DIRECTORY_TYPE, 0755)));
+ }
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(kDataFileName)));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directoryEntry, Exists));
+
+ MOZ_ASSERT(!exists);
+ }
+#endif
+
+ QM_TRY_INSPECT(const auto& usageFile, GetUsageFile(mDirectoryPath));
+
+ nsCOMPtr<mozIStorageConnection> storageConnection;
+
+ auto autoRemove = MakeScopeExit([&storageConnection, &directoryEntry] {
+ if (storageConnection) {
+ MOZ_ALWAYS_SUCCEEDS(storageConnection->Close());
+ }
+
+ nsresult rv = directoryEntry->Remove(false);
+ if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) {
+ NS_WARNING("Failed to remove database file!");
+ }
+ });
+
+ QM_TRY_UNWRAP(storageConnection,
+ CreateStorageConnection(*directoryEntry, *usageFile, Origin(),
+ [] { MOZ_ASSERT_UNREACHABLE(); }));
+
+ MOZ_ASSERT(mQuotaClient);
+
+ MutexAutoLock shadowDatabaseLock(mQuotaClient->ShadowDatabaseMutex());
+
+ nsCOMPtr<mozIStorageConnection> shadowConnection;
+ if (!gInitializedShadowStorage) {
+ QM_TRY_UNWRAP(shadowConnection,
+ CreateShadowStorageConnection(quotaManager->GetBasePath()));
+
+ gInitializedShadowStorage = true;
+ }
+
+ autoRemove.release();
+
+ if (!mHasCreatedDatabase) {
+ mHasCreatedDatabase = true;
+ }
+
+ LazyInit(WrapMovingNotNull(std::move(storageConnection)));
+
+ return NS_OK;
+}
+
+void Connection::CloseStorageConnection() {
+ AssertIsOnGlobalConnectionThread();
+
+ CachingDatabaseConnection::Close();
+}
+
+nsresult Connection::BeginWriteTransaction() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(HasStorageConnection());
+
+ QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("BEGIN IMMEDIATE;"_ns)));
+
+ return NS_OK;
+}
+
+nsresult Connection::CommitWriteTransaction() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(HasStorageConnection());
+
+ QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("COMMIT;"_ns)));
+
+ return NS_OK;
+}
+
+nsresult Connection::RollbackWriteTransaction() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(HasStorageConnection());
+
+ QM_TRY_INSPECT(const auto& stmt, BorrowCachedStatement("ROLLBACK;"_ns));
+
+ // This may fail if SQLite already rolled back the transaction so ignore any
+ // errors.
+ Unused << stmt->Execute();
+
+ return NS_OK;
+}
+
+void Connection::ScheduleFlush() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mWriteOptimizer.HasWrites());
+ MOZ_ASSERT(!mFlushScheduled);
+
+ if (!mFlushTimer) {
+ mFlushTimer = NS_NewTimer();
+ MOZ_ASSERT(mFlushTimer);
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(mFlushTimer->InitWithNamedFuncCallback(
+ FlushTimerCallback, this, kFlushTimeoutMs, nsITimer::TYPE_ONE_SHOT,
+ "Connection::FlushTimerCallback"));
+
+ mFlushScheduled = true;
+}
+
+void Connection::Flush() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mFlushScheduled);
+
+ if (mWriteOptimizer.HasWrites()) {
+ RefPtr<FlushOp> op = new FlushOp(this, std::move(mWriteOptimizer));
+
+ Dispatch(op);
+ }
+
+ mFlushScheduled = false;
+}
+
+// static
+void Connection::FlushTimerCallback(nsITimer* aTimer, void* aClosure) {
+ MOZ_ASSERT(aClosure);
+
+ auto* self = static_cast<Connection*>(aClosure);
+ MOZ_ASSERT(self);
+ MOZ_ASSERT(self->mFlushScheduled);
+
+ self->Flush();
+}
+
+Result<nsString, nsresult>
+Connection::InitTemporaryOriginHelper::BlockAndReturnOriginDirectoryPath() {
+ AssertIsOnGlobalConnectionThread();
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ MOZ_ALWAYS_SUCCEEDS(
+ quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL));
+
+ mozilla::MonitorAutoLock lock(mMonitor);
+ while (mWaiting) {
+ lock.Wait();
+ }
+
+ QM_TRY(MOZ_TO_RESULT(mIOThreadResultCode));
+
+ return mOriginDirectoryPath;
+}
+
+nsresult Connection::InitTemporaryOriginHelper::RunOnIOThread() {
+ AssertIsOnIOThread();
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY_INSPECT(const auto& directoryEntry,
+ quotaManager
+ ->EnsureTemporaryOriginIsInitialized(
+ PERSISTENCE_TYPE_DEFAULT, mOriginMetadata)
+ .map([](const auto& res) { return res.first; }));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->GetPath(mOriginDirectoryPath)));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Connection::InitTemporaryOriginHelper::Run() {
+ AssertIsOnIOThread();
+
+ nsresult rv = RunOnIOThread();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mIOThreadResultCode = rv;
+ }
+
+ mozilla::MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(mWaiting);
+
+ mWaiting = false;
+ lock.Notify();
+
+ return NS_OK;
+}
+
+Connection::FlushOp::FlushOp(Connection* aConnection,
+ ConnectionWriteOptimizer&& aWriteOptimizer)
+ : ConnectionDatastoreOperationBase(aConnection),
+ mWriteOptimizer(std::move(aWriteOptimizer)),
+ mShadowWrites(gShadowWrites) {}
+
+nsresult Connection::FlushOp::DoDatastoreWork() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(mConnection);
+
+ AutoWriteTransaction autoWriteTransaction(mShadowWrites);
+
+ QM_TRY(MOZ_TO_RESULT(autoWriteTransaction.Start(mConnection)));
+
+ QM_TRY_INSPECT(const int64_t& usage,
+ mWriteOptimizer.Perform(mConnection, mShadowWrites));
+
+ QM_TRY_INSPECT(const auto& usageFile,
+ GetUsageFile(mConnection->DirectoryPath()));
+
+ QM_TRY_INSPECT(const auto& usageJournalFile,
+ GetUsageJournalFile(mConnection->DirectoryPath()));
+
+ QM_TRY(MOZ_TO_RESULT(UpdateUsageFile(usageFile, usageJournalFile, usage)));
+
+ QM_TRY(MOZ_TO_RESULT(autoWriteTransaction.Commit()));
+
+ QM_TRY(MOZ_TO_RESULT(usageJournalFile->Remove(false)));
+
+ return NS_OK;
+}
+
+void Connection::FlushOp::Cleanup() {
+ AssertIsOnOwningThread();
+
+ mWriteOptimizer.Reset();
+
+ MOZ_ASSERT(!mWriteOptimizer.HasWrites());
+
+ ConnectionDatastoreOperationBase::Cleanup();
+}
+
+nsresult Connection::CloseOp::DoDatastoreWork() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(mConnection);
+
+ if (mConnection->HasStorageConnection()) {
+ mConnection->CloseStorageConnection();
+ }
+
+ return NS_OK;
+}
+
+void Connection::CloseOp::Cleanup() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mConnection);
+
+ mConnection->mConnectionThread->mConnections.Remove(mConnection->Origin());
+
+#ifdef DEBUG
+ MOZ_ASSERT(!mConnection->mFinished);
+ mConnection->mFinished = true;
+#endif
+
+ nsCOMPtr<nsIRunnable> callback;
+ mCallback.swap(callback);
+
+ callback->Run();
+
+ ConnectionDatastoreOperationBase::Cleanup();
+}
+
+/*******************************************************************************
+ * ConnectionThread implementation
+ ******************************************************************************/
+
+ConnectionThread::ConnectionThread() {
+ AssertIsOnOwningThread();
+ AssertIsOnBackgroundThread();
+
+ MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("LS Thread", getter_AddRefs(mThread)));
+}
+
+ConnectionThread::~ConnectionThread() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mConnections.Count());
+}
+
+bool ConnectionThread::IsOnConnectionThread() {
+ MOZ_ASSERT(mThread);
+
+ bool current;
+ return NS_SUCCEEDED(mThread->IsOnCurrentThread(&current)) && current;
+}
+
+void ConnectionThread::AssertIsOnConnectionThread() {
+ MOZ_ASSERT(IsOnConnectionThread());
+}
+
+already_AddRefed<Connection> ConnectionThread::CreateConnection(
+ const OriginMetadata& aOriginMetadata,
+ UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
+ bool aDatabaseWasNotAvailable) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty());
+ MOZ_ASSERT(!mConnections.Contains(aOriginMetadata.mOrigin));
+
+ RefPtr<Connection> connection =
+ new Connection(this, aOriginMetadata, std::move(aArchivedOriginScope),
+ aDatabaseWasNotAvailable);
+ mConnections.InsertOrUpdate(aOriginMetadata.mOrigin, RefPtr{connection});
+
+ return connection.forget();
+}
+
+void ConnectionThread::Shutdown() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mThread);
+
+ mThread->Shutdown();
+}
+
+/*******************************************************************************
+ * Datastore
+ ******************************************************************************/
+
+Datastore::Datastore(const OriginMetadata& aOriginMetadata,
+ uint32_t aPrivateBrowsingId, int64_t aUsage,
+ int64_t aSizeOfKeys, int64_t aSizeOfItems,
+ RefPtr<DirectoryLock>&& aDirectoryLock,
+ RefPtr<Connection>&& aConnection,
+ RefPtr<QuotaObject>&& aQuotaObject,
+ nsTHashMap<nsStringHashKey, LSValue>& aValues,
+ nsTArray<LSItemInfo>&& aOrderedItems)
+ : mDirectoryLock(std::move(aDirectoryLock)),
+ mConnection(std::move(aConnection)),
+ mQuotaObject(std::move(aQuotaObject)),
+ mOrderedItems(std::move(aOrderedItems)),
+ mOriginMetadata(aOriginMetadata),
+ mPrivateBrowsingId(aPrivateBrowsingId),
+ mUsage(aUsage),
+ mUpdateBatchUsage(-1),
+ mSizeOfKeys(aSizeOfKeys),
+ mSizeOfItems(aSizeOfItems),
+ mClosed(false),
+ mInUpdateBatch(false),
+ mHasLivePrivateDatastore(false) {
+ AssertIsOnBackgroundThread();
+
+ mValues.SwapElements(aValues);
+}
+
+Datastore::~Datastore() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mClosed);
+}
+
+void Datastore::Close() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(!mPrepareDatastoreOps.Count());
+ MOZ_ASSERT(!mPreparedDatastores.Count());
+ MOZ_ASSERT(!mDatabases.Count());
+ MOZ_ASSERT(mDirectoryLock);
+
+ mClosed = true;
+
+ if (IsPersistent()) {
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(mQuotaObject);
+
+ // We can't release the directory lock and unregister itself from the
+ // hashtable until the connection is fully closed.
+ nsCOMPtr<nsIRunnable> callback =
+ NewRunnableMethod("dom::Datastore::ConnectionClosedCallback", this,
+ &Datastore::ConnectionClosedCallback);
+ mConnection->Close(callback);
+ } else {
+ MOZ_ASSERT(!mConnection);
+ MOZ_ASSERT(!mQuotaObject);
+
+ // There's no connection, so it's safe to release the directory lock and
+ // unregister itself from the hashtable.
+
+ mDirectoryLock = nullptr;
+
+ CleanupMetadata();
+ }
+}
+
+void Datastore::WaitForConnectionToComplete(nsIRunnable* aCallback) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(!mCompleteCallback);
+ MOZ_ASSERT(mClosed);
+
+ mCompleteCallback = aCallback;
+}
+
+void Datastore::NoteLivePrepareDatastoreOp(
+ PrepareDatastoreOp* aPrepareDatastoreOp) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aPrepareDatastoreOp);
+ MOZ_ASSERT(!mPrepareDatastoreOps.Contains(aPrepareDatastoreOp));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mPrepareDatastoreOps.Insert(aPrepareDatastoreOp);
+}
+
+void Datastore::NoteFinishedPrepareDatastoreOp(
+ PrepareDatastoreOp* aPrepareDatastoreOp) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aPrepareDatastoreOp);
+ MOZ_ASSERT(mPrepareDatastoreOps.Contains(aPrepareDatastoreOp));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mPrepareDatastoreOps.Remove(aPrepareDatastoreOp);
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(
+ quota::Client::LS, "PrepareDatastoreOp finished"_ns);
+
+ MaybeClose();
+}
+
+void Datastore::NoteLivePrivateDatastore() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mHasLivePrivateDatastore);
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mHasLivePrivateDatastore = true;
+}
+
+void Datastore::NoteFinishedPrivateDatastore() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mHasLivePrivateDatastore);
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mHasLivePrivateDatastore = false;
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(
+ quota::Client::LS, "PrivateDatastore finished"_ns);
+
+ MaybeClose();
+}
+
+void Datastore::NoteLivePreparedDatastore(
+ PreparedDatastore* aPreparedDatastore) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aPreparedDatastore);
+ MOZ_ASSERT(!mPreparedDatastores.Contains(aPreparedDatastore));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mPreparedDatastores.Insert(aPreparedDatastore);
+}
+
+void Datastore::NoteFinishedPreparedDatastore(
+ PreparedDatastore* aPreparedDatastore) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aPreparedDatastore);
+ MOZ_ASSERT(mPreparedDatastores.Contains(aPreparedDatastore));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mPreparedDatastores.Remove(aPreparedDatastore);
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(
+ quota::Client::LS, "PreparedDatastore finished"_ns);
+
+ MaybeClose();
+}
+
+bool Datastore::HasOtherProcessDatabases(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+
+ PBackgroundParent* databaseBackgroundActor = aDatabase->Manager();
+
+ for (Database* database : mDatabases) {
+ if (database->Manager() != databaseBackgroundActor) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void Datastore::NoteLiveDatabase(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(!mDatabases.Contains(aDatabase));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mDatabases.Insert(aDatabase);
+
+ NoteChangedDatabaseMap();
+}
+
+void Datastore::NoteFinishedDatabase(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(mDatabases.Contains(aDatabase));
+ MOZ_ASSERT(!mActiveDatabases.Contains(aDatabase));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mDatabases.Remove(aDatabase);
+
+ NoteChangedDatabaseMap();
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(quota::Client::LS,
+ "Database finished"_ns);
+
+ MaybeClose();
+}
+
+void Datastore::NoteActiveDatabase(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(mDatabases.Contains(aDatabase));
+ MOZ_ASSERT(!mActiveDatabases.Contains(aDatabase));
+ MOZ_ASSERT(!mClosed);
+
+ mActiveDatabases.Insert(aDatabase);
+}
+
+void Datastore::NoteInactiveDatabase(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(mDatabases.Contains(aDatabase));
+ MOZ_ASSERT(mActiveDatabases.Contains(aDatabase));
+ MOZ_ASSERT(!mClosed);
+
+ mActiveDatabases.Remove(aDatabase);
+
+ if (!mActiveDatabases.Count() && mPendingUsageDeltas.Length()) {
+ int64_t finalDelta = 0;
+
+ for (auto delta : mPendingUsageDeltas) {
+ finalDelta += delta;
+ }
+
+ MOZ_ASSERT(finalDelta <= 0);
+
+ if (finalDelta != 0) {
+ DebugOnly<bool> ok = UpdateUsage(finalDelta);
+ MOZ_ASSERT(ok);
+ }
+
+ mPendingUsageDeltas.Clear();
+ }
+}
+
+void Datastore::GetSnapshotLoadInfo(const nsAString& aKey,
+ bool& aAddKeyToUnknownItems,
+ nsTHashtable<nsStringHashKey>& aLoadedItems,
+ nsTArray<LSItemInfo>& aItemInfos,
+ uint32_t& aNextLoadIndex,
+ LSSnapshot::LoadState& aLoadState) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(!mInUpdateBatch);
+
+#ifdef DEBUG
+ int64_t sizeOfKeys = 0;
+ int64_t sizeOfItems = 0;
+ for (auto item : mOrderedItems) {
+ int64_t sizeOfKey = static_cast<int64_t>(item.key().Length());
+ sizeOfKeys += sizeOfKey;
+ sizeOfItems += sizeOfKey + static_cast<int64_t>(item.value().Length());
+ }
+ MOZ_ASSERT(mSizeOfKeys == sizeOfKeys);
+ MOZ_ASSERT(mSizeOfItems == sizeOfItems);
+#endif
+
+ // Computes load state optimized for current size of keys and items.
+ // Zero key length and value can be passed to do a quick initial estimation.
+ // If computed load state is already AllOrderedItems then excluded key length
+ // and value length can't make it any better.
+ auto GetLoadState = [&](int64_t aKeyLength, int64_t aValueLength) {
+ if (mSizeOfKeys - aKeyLength <= gSnapshotPrefill) {
+ if (mSizeOfItems - aKeyLength - aValueLength <= gSnapshotPrefill) {
+ return LSSnapshot::LoadState::AllOrderedItems;
+ }
+
+ return LSSnapshot::LoadState::AllOrderedKeys;
+ }
+
+ return LSSnapshot::LoadState::Partial;
+ };
+
+ // Value for given aKey if aKey is not void (can be void too if value doesn't
+ // exist for given aKey).
+ LSValue value;
+ // If aKey and value are not void, checkKey will be set to true. Once we find
+ // an item for given aKey in one of the loops below, checkKey is set to false
+ // to prevent additional comparison of strings (string implementation compares
+ // string lengths first to avoid char by char comparison if possible).
+ bool checkKey = false;
+
+ // Avoid additional hash lookup if all ordered items fit into initial prefill
+ // already.
+ LSSnapshot::LoadState loadState = GetLoadState(/* aKeyLength */ 0,
+ /* aValueLength */ 0);
+ if (loadState != LSSnapshot::LoadState::AllOrderedItems && !aKey.IsVoid()) {
+ GetItem(aKey, value);
+ if (!value.IsVoid()) {
+ // Ok, we have a non void aKey and value.
+
+ // We have to watch for aKey during one of the loops below to exclude it
+ // from the size computation. The super fast mode (AllOrderedItems)
+ // doesn't have to do that though.
+ checkKey = true;
+
+ // We have to compute load state again because aKey length and value
+ // length is excluded from the size in this case.
+ loadState = GetLoadState(aKey.Length(), value.Length());
+ }
+ }
+
+ switch (loadState) {
+ case LSSnapshot::LoadState::AllOrderedItems: {
+ // We're sending all ordered items, we don't need to check keys because
+ // mOrderedItems must contain a value for aKey if checkKey is true.
+
+ aItemInfos.AppendElements(mOrderedItems);
+
+ MOZ_ASSERT(aItemInfos.Length() == mValues.Count());
+ aNextLoadIndex = mValues.Count();
+
+ aAddKeyToUnknownItems = false;
+
+ break;
+ }
+
+ case LSSnapshot::LoadState::AllOrderedKeys: {
+ // We don't have enough snapshot budget to send all items, but we do have
+ // enough to send all of the keys and to make a best effort to populate as
+ // many values as possible. We send void string values once we run out of
+ // budget. A complicating factor is that we want to make sure that we send
+ // the value for aKey which is a localStorage read that's triggering this
+ // request. Since that key can happen anywhere in the list of items, we
+ // need to handle it specially.
+ //
+ // The loop is effectively doing 2 things in parallel:
+ //
+ // 1. Looking for the `aKey` to send. This is tracked by `checkKey`
+ // which is true if there was an `aKey` specified and until we
+ // populate its value, and false thereafter.
+ // 2. Sending values until we run out of `size` budget and switch to
+ // sending void values. `doneSendingValues` tracks when we've run out
+ // of size budget, with `setVoidValue` tracking whether a value
+ // should be sent for each turn of the event loop but can be
+ // overridden when `aKey` is found.
+
+ int64_t size = mSizeOfKeys;
+ bool setVoidValue = false;
+ bool doneSendingValues = false;
+ for (uint32_t index = 0; index < mOrderedItems.Length(); index++) {
+ const LSItemInfo& item = mOrderedItems[index];
+
+ const nsString& key = item.key();
+ const LSValue& value = item.value();
+
+ if (checkKey && key == aKey) {
+ checkKey = false;
+ setVoidValue = false;
+ } else if (!setVoidValue) {
+ if (doneSendingValues) {
+ setVoidValue = true;
+ } else {
+ size += static_cast<int64_t>(value.Length());
+
+ if (size > gSnapshotPrefill) {
+ setVoidValue = true;
+ doneSendingValues = true;
+
+ // We set doneSendingValues to true and that will guard against
+ // entering this branch during next iterations. So aNextLoadIndex
+ // is set only once.
+ aNextLoadIndex = index;
+ }
+ }
+ }
+
+ LSItemInfo* itemInfo = aItemInfos.AppendElement();
+ itemInfo->key() = key;
+ if (setVoidValue) {
+ itemInfo->value().SetIsVoid(true);
+ } else {
+ aLoadedItems.PutEntry(key);
+ itemInfo->value() = value;
+ }
+ }
+
+ aAddKeyToUnknownItems = false;
+
+ break;
+ }
+
+ case LSSnapshot::LoadState::Partial: {
+ int64_t size = 0;
+ for (uint32_t index = 0; index < mOrderedItems.Length(); index++) {
+ const LSItemInfo& item = mOrderedItems[index];
+
+ const nsString& key = item.key();
+ const LSValue& value = item.value();
+
+ if (checkKey && key == aKey) {
+ checkKey = false;
+ } else {
+ size += static_cast<int64_t>(key.Length()) +
+ static_cast<int64_t>(value.Length());
+
+ if (size > gSnapshotPrefill) {
+ aNextLoadIndex = index;
+ break;
+ }
+ }
+
+ aLoadedItems.PutEntry(key);
+
+ LSItemInfo* itemInfo = aItemInfos.AppendElement();
+ itemInfo->key() = key;
+ itemInfo->value() = value;
+ }
+
+ aAddKeyToUnknownItems = false;
+
+ if (!aKey.IsVoid()) {
+ if (value.IsVoid()) {
+ aAddKeyToUnknownItems = true;
+ } else if (checkKey) {
+ // The item wasn't added in the loop above, add it here.
+
+ LSItemInfo* itemInfo = aItemInfos.AppendElement();
+ itemInfo->key() = aKey;
+ itemInfo->value() = value;
+ }
+ }
+
+ MOZ_ASSERT(aItemInfos.Length() < mOrderedItems.Length());
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Bad load state value!");
+ }
+
+ aLoadState = loadState;
+}
+
+void Datastore::GetItem(const nsAString& aKey, LSValue& aValue) const {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mClosed);
+
+ if (!mValues.Get(aKey, &aValue)) {
+ aValue.SetIsVoid(true);
+ }
+}
+
+void Datastore::GetKeys(nsTArray<nsString>& aKeys) const {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mClosed);
+
+ for (auto item : mOrderedItems) {
+ aKeys.AppendElement(item.key());
+ }
+}
+
+void Datastore::SetItem(Database* aDatabase, const nsString& aKey,
+ const LSValue& aValue) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(mInUpdateBatch);
+
+ LSValue oldValue;
+ GetItem(aKey, oldValue);
+
+ if (oldValue != aValue) {
+ bool isNewItem = oldValue.IsVoid();
+
+ NotifySnapshots(aDatabase, aKey, oldValue, /* affectsOrder */ isNewItem);
+
+ mValues.InsertOrUpdate(aKey, aValue);
+
+ int64_t delta;
+
+ if (isNewItem) {
+ mWriteOptimizer.InsertItem(aKey, aValue);
+
+ int64_t sizeOfKey = static_cast<int64_t>(aKey.Length());
+
+ delta = sizeOfKey + static_cast<int64_t>(aValue.UTF16Length());
+
+ mUpdateBatchUsage += delta;
+
+ mSizeOfKeys += sizeOfKey;
+ mSizeOfItems += sizeOfKey + static_cast<int64_t>(aValue.Length());
+ } else {
+ mWriteOptimizer.UpdateItem(aKey, aValue);
+
+ delta = static_cast<int64_t>(aValue.UTF16Length()) -
+ static_cast<int64_t>(oldValue.UTF16Length());
+
+ mUpdateBatchUsage += delta;
+
+ mSizeOfItems += static_cast<int64_t>(aValue.Length()) -
+ static_cast<int64_t>(oldValue.Length());
+ }
+
+ if (IsPersistent()) {
+ mConnection->SetItem(aKey, aValue, delta, isNewItem);
+ }
+ }
+}
+
+void Datastore::RemoveItem(Database* aDatabase, const nsString& aKey) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(mInUpdateBatch);
+
+ LSValue oldValue;
+ GetItem(aKey, oldValue);
+
+ if (!oldValue.IsVoid()) {
+ NotifySnapshots(aDatabase, aKey, oldValue, /* aAffectsOrder */ true);
+
+ mValues.Remove(aKey);
+
+ mWriteOptimizer.DeleteItem(aKey);
+
+ int64_t sizeOfKey = static_cast<int64_t>(aKey.Length());
+
+ int64_t delta = -sizeOfKey - static_cast<int64_t>(oldValue.UTF16Length());
+
+ mUpdateBatchUsage += delta;
+
+ mSizeOfKeys -= sizeOfKey;
+ mSizeOfItems -= sizeOfKey + static_cast<int64_t>(oldValue.Length());
+
+ if (IsPersistent()) {
+ mConnection->RemoveItem(aKey, delta);
+ }
+ }
+}
+
+void Datastore::Clear(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mClosed);
+
+ if (mValues.Count()) {
+ int64_t delta = 0;
+ for (const auto& entry : mValues) {
+ const nsAString& key = entry.GetKey();
+ const LSValue& value = entry.GetData();
+
+ delta += -static_cast<int64_t>(key.Length()) -
+ static_cast<int64_t>(value.UTF16Length());
+
+ NotifySnapshots(aDatabase, key, value, /* aAffectsOrder */ true);
+ }
+
+ mValues.Clear();
+
+ if (mInUpdateBatch) {
+ mWriteOptimizer.Truncate();
+
+ mUpdateBatchUsage += delta;
+ } else {
+ mOrderedItems.Clear();
+
+ DebugOnly<bool> ok = UpdateUsage(delta);
+ MOZ_ASSERT(ok);
+ }
+
+ mSizeOfKeys = 0;
+ mSizeOfItems = 0;
+
+ if (IsPersistent()) {
+ mConnection->Clear(delta);
+ }
+ }
+}
+
+void Datastore::BeginUpdateBatch(int64_t aSnapshotUsage) {
+ AssertIsOnBackgroundThread();
+ // Don't assert `aSnapshotUsage >= 0`, it can be negative when multiple
+ // snapshots are operating in parallel.
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(mUpdateBatchUsage == -1);
+ MOZ_ASSERT(!mInUpdateBatch);
+
+ mUpdateBatchUsage = aSnapshotUsage;
+
+ if (IsPersistent()) {
+ mConnection->BeginUpdateBatch();
+ }
+
+ mInUpdateBatch = true;
+}
+
+int64_t Datastore::EndUpdateBatch(int64_t aSnapshotPeakUsage) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(mInUpdateBatch);
+
+ mWriteOptimizer.ApplyAndReset(mOrderedItems);
+
+ MOZ_ASSERT(!mWriteOptimizer.HasWrites());
+
+ if (aSnapshotPeakUsage >= 0) {
+ int64_t delta = mUpdateBatchUsage - aSnapshotPeakUsage;
+
+ if (mActiveDatabases.Count()) {
+ // We can't apply deltas while other databases are still active.
+ // The final delta must be zero or negative, but individual deltas can
+ // be positive. A positive delta can't be applied asynchronously since
+ // there's no way to fire the quota exceeded error event.
+
+ mPendingUsageDeltas.AppendElement(delta);
+ } else {
+ MOZ_ASSERT(delta <= 0);
+ if (delta != 0) {
+ DebugOnly<bool> ok = UpdateUsage(delta);
+ MOZ_ASSERT(ok);
+ }
+ }
+ }
+
+ int64_t result = mUpdateBatchUsage;
+ mUpdateBatchUsage = -1;
+
+ if (IsPersistent()) {
+ mConnection->EndUpdateBatch();
+ }
+
+ mInUpdateBatch = false;
+
+ return result;
+}
+
+int64_t Datastore::AttemptToUpdateUsage(int64_t aMinSize, bool aInitial) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT_IF(aInitial, aMinSize >= 0);
+ MOZ_ASSERT_IF(!aInitial, aMinSize > 0);
+
+ const int64_t size = aMinSize + GetSnapshotPeakUsagePreincrement(aInitial);
+
+ if (size && UpdateUsage(size)) {
+ return size;
+ }
+
+ const int64_t reducedSize =
+ aMinSize + GetSnapshotPeakUsageReducedPreincrement(aInitial);
+
+ if (reducedSize && UpdateUsage(reducedSize)) {
+ return reducedSize;
+ }
+
+ if (aMinSize > 0 && UpdateUsage(aMinSize)) {
+ return aMinSize;
+ }
+
+ return 0;
+}
+
+bool Datastore::HasOtherProcessObservers(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+
+ if (!gObservers) {
+ return false;
+ }
+
+ nsTArray<NotNull<Observer*>>* array;
+ if (!gObservers->Get(mOriginMetadata.mOrigin, &array)) {
+ return false;
+ }
+
+ MOZ_ASSERT(array);
+
+ PBackgroundParent* databaseBackgroundActor = aDatabase->Manager();
+
+ for (Observer* observer : *array) {
+ if (observer->Manager() != databaseBackgroundActor) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void Datastore::NotifyOtherProcessObservers(Database* aDatabase,
+ const nsString& aDocumentURI,
+ const nsString& aKey,
+ const LSValue& aOldValue,
+ const LSValue& aNewValue) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+
+ if (!gObservers) {
+ return;
+ }
+
+ nsTArray<NotNull<Observer*>>* array;
+ if (!gObservers->Get(mOriginMetadata.mOrigin, &array)) {
+ return;
+ }
+
+ MOZ_ASSERT(array);
+
+ // We do not want to send information about events back to the content process
+ // that caused the change.
+ PBackgroundParent* databaseBackgroundActor = aDatabase->Manager();
+
+ for (Observer* observer : *array) {
+ if (observer->Manager() != databaseBackgroundActor) {
+ observer->Observe(aDatabase, aDocumentURI, aKey, aOldValue, aNewValue);
+ }
+ }
+}
+
+void Datastore::NoteChangedObserverArray(
+ const nsTArray<NotNull<Observer*>>& aObservers) {
+ AssertIsOnBackgroundThread();
+
+ for (Database* database : mActiveDatabases) {
+ Snapshot* snapshot = database->GetSnapshot();
+ MOZ_ASSERT(snapshot);
+
+ if (snapshot->IsDirty()) {
+ continue;
+ }
+
+ bool hasOtherProcessObservers = false;
+
+ PBackgroundParent* databaseBackgroundActor = database->Manager();
+
+ for (Observer* observer : aObservers) {
+ if (observer->Manager() != databaseBackgroundActor) {
+ hasOtherProcessObservers = true;
+ break;
+ }
+ }
+
+ if (snapshot->HasOtherProcessObservers() != hasOtherProcessObservers) {
+ snapshot->MarkDirty();
+ }
+ }
+}
+
+void Datastore::Stringify(nsACString& aResult) const {
+ AssertIsOnBackgroundThread();
+
+ aResult.AppendLiteral("DirectoryLock:");
+ aResult.AppendInt(!!mDirectoryLock);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("Connection:");
+ aResult.AppendInt(!!mConnection);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("QuotaObject:");
+ aResult.AppendInt(!!mQuotaObject);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("PrepareDatastoreOps:");
+ aResult.AppendInt(mPrepareDatastoreOps.Count());
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("PreparedDatastores:");
+ aResult.AppendInt(mPreparedDatastores.Count());
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("Databases:");
+ aResult.AppendInt(mDatabases.Count());
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("ActiveDatabases:");
+ aResult.AppendInt(mActiveDatabases.Count());
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("Origin:");
+ aResult.Append(AnonymizedOriginString(mOriginMetadata.mOrigin));
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("PrivateBrowsingId:");
+ aResult.AppendInt(mPrivateBrowsingId);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("Closed:");
+ aResult.AppendInt(mClosed);
+}
+
+bool Datastore::UpdateUsage(int64_t aDelta) {
+ AssertIsOnBackgroundThread();
+
+ // Check internal LocalStorage origin limit.
+ int64_t newUsage = mUsage + aDelta;
+
+ MOZ_ASSERT(newUsage >= 0);
+
+ if (newUsage > StaticPrefs::dom_storage_default_quota() * 1024) {
+ return false;
+ }
+
+ // Check QuotaManager limits (group and global limit).
+ if (IsPersistent()) {
+ MOZ_ASSERT(mQuotaObject);
+
+ if (!mQuotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) {
+ return false;
+ }
+ }
+
+ // Quota checks passed, set new usage.
+ mUsage = newUsage;
+
+ return true;
+}
+
+void Datastore::MaybeClose() {
+ AssertIsOnBackgroundThread();
+
+ if (!mPrepareDatastoreOps.Count() && !mHasLivePrivateDatastore &&
+ !mPreparedDatastores.Count() && !mDatabases.Count()) {
+ Close();
+ }
+}
+
+void Datastore::ConnectionClosedCallback() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(mQuotaObject);
+ MOZ_ASSERT(mClosed);
+
+ // Release the quota object first.
+ mQuotaObject = nullptr;
+
+ bool databaseWasNotAvailable;
+ bool hasCreatedDatabase;
+ mConnection->GetFinishInfo(databaseWasNotAvailable, hasCreatedDatabase);
+
+ if (databaseWasNotAvailable && !hasCreatedDatabase) {
+ MOZ_ASSERT(mUsage == 0);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ quotaManager->ResetUsageForClient(
+ ClientMetadata{mOriginMetadata, mozilla::dom::quota::Client::LS});
+ }
+
+ mConnection = nullptr;
+
+ // Now it's safe to release the directory lock and unregister itself from
+ // the hashtable.
+
+ mDirectoryLock = nullptr;
+
+ CleanupMetadata();
+
+ if (mCompleteCallback) {
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mCompleteCallback.forget()));
+ }
+}
+
+void Datastore::CleanupMetadata() {
+ AssertIsOnBackgroundThread();
+
+ MOZ_ASSERT(gDatastores);
+ const DebugOnly<bool> removed = gDatastores->Remove(mOriginMetadata.mOrigin);
+ MOZ_ASSERT(removed);
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(quota::Client::LS,
+ "Datastore removed"_ns);
+
+ if (!gDatastores->Count()) {
+ gDatastores = nullptr;
+ }
+}
+
+void Datastore::NotifySnapshots(Database* aDatabase, const nsAString& aKey,
+ const LSValue& aOldValue, bool aAffectsOrder) {
+ AssertIsOnBackgroundThread();
+
+ for (Database* database : mDatabases) {
+ MOZ_ASSERT(database);
+
+ if (database == aDatabase) {
+ continue;
+ }
+
+ Snapshot* snapshot = database->GetSnapshot();
+ if (snapshot) {
+ snapshot->SaveItem(aKey, aOldValue, aAffectsOrder);
+ }
+ }
+}
+
+void Datastore::NoteChangedDatabaseMap() {
+ AssertIsOnBackgroundThread();
+
+ for (Database* database : mActiveDatabases) {
+ Snapshot* snapshot = database->GetSnapshot();
+ MOZ_ASSERT(snapshot);
+
+ if (snapshot->IsDirty()) {
+ continue;
+ }
+
+ if (snapshot->HasOtherProcessDatabases() !=
+ HasOtherProcessDatabases(database)) {
+ snapshot->MarkDirty();
+ }
+ }
+}
+
+/*******************************************************************************
+ * PreparedDatastore
+ ******************************************************************************/
+
+void PreparedDatastore::Destroy() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(gPreparedDatastores);
+ DebugOnly<bool> removed = gPreparedDatastores->Remove(mDatastoreId);
+ MOZ_ASSERT(removed);
+}
+
+// static
+void PreparedDatastore::TimerCallback(nsITimer* aTimer, void* aClosure) {
+ AssertIsOnBackgroundThread();
+
+ auto* self = static_cast<PreparedDatastore*>(aClosure);
+ MOZ_ASSERT(self);
+
+ self->Destroy();
+}
+
+/*******************************************************************************
+ * Database
+ ******************************************************************************/
+
+Database::Database(const PrincipalInfo& aPrincipalInfo,
+ const Maybe<ContentParentId>& aContentParentId,
+ const nsACString& aOrigin, uint32_t aPrivateBrowsingId)
+ : mSnapshot(nullptr),
+ mPrincipalInfo(aPrincipalInfo),
+ mContentParentId(aContentParentId),
+ mOrigin(aOrigin),
+ mPrivateBrowsingId(aPrivateBrowsingId),
+ mAllowedToClose(false),
+ mActorDestroyed(false),
+ mRequestedAllowToClose(false)
+#ifdef DEBUG
+ ,
+ mActorWasAlive(false)
+#endif
+{
+ AssertIsOnBackgroundThread();
+}
+
+Database::~Database() {
+ MOZ_ASSERT_IF(mActorWasAlive, mAllowedToClose);
+ MOZ_ASSERT_IF(mActorWasAlive, mActorDestroyed);
+}
+
+void Database::SetActorAlive(Datastore* aDatastore) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorWasAlive);
+ MOZ_ASSERT(!mActorDestroyed);
+
+#ifdef DEBUG
+ mActorWasAlive = true;
+#endif
+
+ mDatastore = aDatastore;
+
+ mDatastore->NoteLiveDatabase(this);
+
+ if (!gLiveDatabases) {
+ gLiveDatabases = new LiveDatabaseArray();
+ }
+
+ gLiveDatabases->AppendElement(WrapNotNullUnchecked(this));
+}
+
+void Database::RegisterSnapshot(Snapshot* aSnapshot) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aSnapshot);
+ MOZ_ASSERT(!mSnapshot);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ // Only one snapshot at a time is currently supported.
+ mSnapshot = aSnapshot;
+
+ mDatastore->NoteActiveDatabase(this);
+}
+
+void Database::UnregisterSnapshot(Snapshot* aSnapshot) {
+ MOZ_ASSERT(aSnapshot);
+ MOZ_ASSERT(mSnapshot == aSnapshot);
+
+ mSnapshot = nullptr;
+
+ mDatastore->NoteInactiveDatabase(this);
+}
+
+void Database::RequestAllowToClose() {
+ AssertIsOnBackgroundThread();
+
+ if (mRequestedAllowToClose) {
+ return;
+ }
+
+ mRequestedAllowToClose = true;
+
+ // Send the RequestAllowToClose message to the child to avoid racing with the
+ // child actor. Except the case when the actor was already destroyed.
+ if (mActorDestroyed) {
+ MOZ_ASSERT(mAllowedToClose);
+ return;
+ }
+
+ if (NS_WARN_IF(!SendRequestAllowToClose()) && !mSnapshot) {
+ // This is not necessary, because there should be a runnable scheduled that
+ // will call ActorDestroy which calls AllowToClose. However we can speedup
+ // the shutdown a bit if we do it here directly, but only if there's no
+ // registered snapshot.
+ AllowToClose();
+ }
+}
+
+void Database::ForceKill() {
+ AssertIsOnBackgroundThread();
+
+ if (mActorDestroyed) {
+ MOZ_ASSERT(mAllowedToClose);
+ return;
+ }
+
+ Unused << PBackgroundLSDatabaseParent::Send__delete__(this);
+}
+
+void Database::Stringify(nsACString& aResult) const {
+ AssertIsOnBackgroundThread();
+
+ aResult.AppendLiteral("SnapshotRegistered:");
+ aResult.AppendInt(!!mSnapshot);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("OtherProcessActor:");
+ aResult.AppendInt(BackgroundParent::IsOtherProcessActor(Manager()));
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("Origin:");
+ aResult.Append(AnonymizedOriginString(mOrigin));
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("PrivateBrowsingId:");
+ aResult.AppendInt(mPrivateBrowsingId);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("AllowedToClose:");
+ aResult.AppendInt(mAllowedToClose);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("ActorDestroyed:");
+ aResult.AppendInt(mActorDestroyed);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("RequestedAllowToClose:");
+ aResult.AppendInt(mRequestedAllowToClose);
+}
+
+void Database::AllowToClose() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mAllowedToClose);
+ MOZ_ASSERT(mDatastore);
+ MOZ_ASSERT(!mSnapshot);
+
+ mAllowedToClose = true;
+
+ mDatastore->NoteFinishedDatabase(this);
+
+ mDatastore = nullptr;
+
+ MOZ_ASSERT(gLiveDatabases);
+ gLiveDatabases->RemoveElement(this);
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(quota::Client::LS,
+ "Live database removed"_ns);
+
+ if (gLiveDatabases->IsEmpty()) {
+ gLiveDatabases = nullptr;
+ }
+}
+
+void Database::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ mActorDestroyed = true;
+
+ if (!mAllowedToClose) {
+ AllowToClose();
+ }
+}
+
+mozilla::ipc::IPCResult Database::RecvDeleteMe() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ IProtocol* mgr = Manager();
+ if (!PBackgroundLSDatabaseParent::Send__delete__(this)) {
+ return IPC_FAIL(mgr, "Send__delete__ failed!");
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Database::RecvAllowToClose() {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(mAllowedToClose)) {
+ return IPC_FAIL(this, "mAllowedToClose already set!");
+ }
+
+ AllowToClose();
+
+ return IPC_OK();
+}
+
+PBackgroundLSSnapshotParent* Database::AllocPBackgroundLSSnapshotParent(
+ const nsAString& aDocumentURI, const nsAString& aKey,
+ const bool& aIncreasePeakUsage, const int64_t& aMinSize,
+ LSSnapshotInitInfo* aInitInfo) {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(aIncreasePeakUsage && aMinSize < 0)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(mAllowedToClose)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return nullptr;
+ }
+
+ RefPtr<Snapshot> snapshot = new Snapshot(this, aDocumentURI);
+
+ // Transfer ownership to IPDL.
+ return snapshot.forget().take();
+}
+
+mozilla::ipc::IPCResult Database::RecvPBackgroundLSSnapshotConstructor(
+ PBackgroundLSSnapshotParent* aActor, const nsAString& aDocumentURI,
+ const nsAString& aKey, const bool& aIncreasePeakUsage,
+ const int64_t& aMinSize, LSSnapshotInitInfo* aInitInfo) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT_IF(aIncreasePeakUsage, aMinSize >= 0);
+ MOZ_ASSERT(aInitInfo);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ auto* snapshot = static_cast<Snapshot*>(aActor);
+
+ bool addKeyToUnknownItems;
+ nsTHashtable<nsStringHashKey> loadedItems;
+ nsTArray<LSItemInfo> itemInfos;
+ uint32_t nextLoadIndex;
+ LSSnapshot::LoadState loadState;
+ mDatastore->GetSnapshotLoadInfo(aKey, addKeyToUnknownItems, loadedItems,
+ itemInfos, nextLoadIndex, loadState);
+
+ nsTHashSet<nsString> unknownItems;
+ if (addKeyToUnknownItems) {
+ unknownItems.Insert(aKey);
+ }
+
+ uint32_t totalLength = mDatastore->GetLength();
+
+ int64_t usage = mDatastore->GetUsage();
+
+ int64_t peakUsage = usage;
+
+ if (aIncreasePeakUsage) {
+ int64_t size =
+ mDatastore->AttemptToUpdateUsage(aMinSize, /* aInitial */ true);
+
+ peakUsage += size;
+ }
+
+ bool hasOtherProcessDatabases = mDatastore->HasOtherProcessDatabases(this);
+ bool hasOtherProcessObservers = mDatastore->HasOtherProcessObservers(this);
+
+ snapshot->Init(loadedItems, std::move(unknownItems), nextLoadIndex,
+ totalLength, usage, peakUsage, loadState,
+ hasOtherProcessDatabases, hasOtherProcessObservers);
+
+ RegisterSnapshot(snapshot);
+
+ aInitInfo->addKeyToUnknownItems() = addKeyToUnknownItems;
+ aInitInfo->itemInfos() = std::move(itemInfos);
+ aInitInfo->totalLength() = totalLength;
+ aInitInfo->usage() = usage;
+ aInitInfo->peakUsage() = peakUsage;
+ aInitInfo->loadState() = loadState;
+ aInitInfo->hasOtherProcessDatabases() = hasOtherProcessDatabases;
+ aInitInfo->hasOtherProcessObservers() = hasOtherProcessObservers;
+
+ return IPC_OK();
+}
+
+bool Database::DeallocPBackgroundLSSnapshotParent(
+ PBackgroundLSSnapshotParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+
+ // Transfer ownership back from IPDL.
+ RefPtr<Snapshot> actor = dont_AddRef(static_cast<Snapshot*>(aActor));
+
+ return true;
+}
+
+/*******************************************************************************
+ * Snapshot
+ ******************************************************************************/
+
+Snapshot::Snapshot(Database* aDatabase, const nsAString& aDocumentURI)
+ : mDatabase(aDatabase),
+ mDatastore(aDatabase->GetDatastore()),
+ mDocumentURI(aDocumentURI),
+ mTotalLength(0),
+ mUsage(-1),
+ mPeakUsage(-1),
+ mSavedKeys(false),
+ mActorDestroyed(false),
+ mFinishReceived(false),
+ mLoadedReceived(false),
+ mLoadedAllItems(false),
+ mLoadKeysReceived(false),
+ mSentMarkDirty(false) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+}
+
+Snapshot::~Snapshot() {
+ MOZ_ASSERT(mActorDestroyed);
+ MOZ_ASSERT(mFinishReceived);
+}
+
+void Snapshot::SaveItem(const nsAString& aKey, const LSValue& aOldValue,
+ bool aAffectsOrder) {
+ AssertIsOnBackgroundThread();
+
+ MarkDirty();
+
+ if (mLoadedAllItems) {
+ return;
+ }
+
+ if (!mLoadedItems.Contains(aKey) && !mUnknownItems.Contains(aKey)) {
+ mValues.LookupOrInsert(aKey, aOldValue);
+ }
+
+ if (aAffectsOrder && !mSavedKeys) {
+ mDatastore->GetKeys(mKeys);
+ mSavedKeys = true;
+ }
+}
+
+void Snapshot::MarkDirty() {
+ AssertIsOnBackgroundThread();
+
+ if (!mSentMarkDirty) {
+ Unused << SendMarkDirty();
+ mSentMarkDirty = true;
+ }
+}
+
+void Snapshot::Finish() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mDatabase);
+ MOZ_ASSERT(mDatastore);
+ MOZ_ASSERT(!mFinishReceived);
+
+ mDatastore->BeginUpdateBatch(mUsage);
+
+ mDatastore->EndUpdateBatch(mPeakUsage);
+
+ mDatabase->UnregisterSnapshot(this);
+
+ mFinishReceived = true;
+}
+
+void Snapshot::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ mActorDestroyed = true;
+
+ if (!mFinishReceived) {
+ Finish();
+ }
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvDeleteMe() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ IProtocol* mgr = Manager();
+ if (!PBackgroundLSSnapshotParent::Send__delete__(this)) {
+ return IPC_FAIL(mgr, "Send__delete__ failed!");
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::Checkpoint(
+ nsTArray<LSWriteInfo>&& aWriteInfos) {
+ AssertIsOnBackgroundThread();
+ // Don't assert `mUsage >= 0`, it can be negative when multiple snapshots are
+ // operating in parallel.
+ MOZ_ASSERT(mPeakUsage >= mUsage);
+
+ if (NS_WARN_IF(aWriteInfos.IsEmpty())) {
+ return IPC_FAIL(this, "aWriteInfos is empty!");
+ }
+
+ if (NS_WARN_IF(mHasOtherProcessObservers)) {
+ return IPC_FAIL(this, "mHasOtherProcessObservers already set!");
+ }
+
+ mDatastore->BeginUpdateBatch(mUsage);
+
+ for (uint32_t index = 0; index < aWriteInfos.Length(); index++) {
+ const LSWriteInfo& writeInfo = aWriteInfos[index];
+
+ switch (writeInfo.type()) {
+ case LSWriteInfo::TLSSetItemInfo: {
+ const LSSetItemInfo& info = writeInfo.get_LSSetItemInfo();
+
+ mDatastore->SetItem(mDatabase, info.key(), info.value());
+
+ break;
+ }
+
+ case LSWriteInfo::TLSRemoveItemInfo: {
+ const LSRemoveItemInfo& info = writeInfo.get_LSRemoveItemInfo();
+
+ mDatastore->RemoveItem(mDatabase, info.key());
+
+ break;
+ }
+
+ case LSWriteInfo::TLSClearInfo: {
+ mDatastore->Clear(mDatabase);
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+ }
+
+ mUsage = mDatastore->EndUpdateBatch(-1);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::CheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) {
+ AssertIsOnBackgroundThread();
+ // Don't assert `mUsage >= 0`, it can be negative when multiple snapshots are
+ // operating in parallel.
+ MOZ_ASSERT(mPeakUsage >= mUsage);
+
+ if (NS_WARN_IF(aWriteAndNotifyInfos.IsEmpty())) {
+ return IPC_FAIL(this, "aWriteAndNotifyInfos is empty!");
+ }
+
+ if (NS_WARN_IF(!mHasOtherProcessObservers)) {
+ return IPC_FAIL(this, "mHasOtherProcessObservers is not set!");
+ }
+
+ mDatastore->BeginUpdateBatch(mUsage);
+
+ for (uint32_t index = 0; index < aWriteAndNotifyInfos.Length(); index++) {
+ const LSWriteAndNotifyInfo& writeAndNotifyInfo =
+ aWriteAndNotifyInfos[index];
+
+ switch (writeAndNotifyInfo.type()) {
+ case LSWriteAndNotifyInfo::TLSSetItemAndNotifyInfo: {
+ const LSSetItemAndNotifyInfo& info =
+ writeAndNotifyInfo.get_LSSetItemAndNotifyInfo();
+
+ mDatastore->SetItem(mDatabase, info.key(), info.value());
+
+ mDatastore->NotifyOtherProcessObservers(
+ mDatabase, mDocumentURI, info.key(), info.oldValue(), info.value());
+
+ break;
+ }
+
+ case LSWriteAndNotifyInfo::TLSRemoveItemAndNotifyInfo: {
+ const LSRemoveItemAndNotifyInfo& info =
+ writeAndNotifyInfo.get_LSRemoveItemAndNotifyInfo();
+
+ mDatastore->RemoveItem(mDatabase, info.key());
+
+ mDatastore->NotifyOtherProcessObservers(mDatabase, mDocumentURI,
+ info.key(), info.oldValue(),
+ VoidLSValue());
+
+ break;
+ }
+
+ case LSWriteAndNotifyInfo::TLSClearInfo: {
+ mDatastore->Clear(mDatabase);
+
+ mDatastore->NotifyOtherProcessObservers(mDatabase, mDocumentURI,
+ VoidString(), VoidLSValue(),
+ VoidLSValue());
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+ }
+
+ mUsage = mDatastore->EndUpdateBatch(-1);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvAsyncCheckpoint(
+ nsTArray<LSWriteInfo>&& aWriteInfos) {
+ return Checkpoint(std::move(aWriteInfos));
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvAsyncCheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) {
+ return CheckpointAndNotify(std::move(aWriteAndNotifyInfos));
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvSyncCheckpoint(
+ nsTArray<LSWriteInfo>&& aWriteInfos) {
+ return Checkpoint(std::move(aWriteInfos));
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvSyncCheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) {
+ return CheckpointAndNotify(std::move(aWriteAndNotifyInfos));
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvAsyncFinish() {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return IPC_FAIL(this, "Already finished");
+ }
+
+ Finish();
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvSyncFinish() {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return IPC_FAIL(this, "Already finished");
+ }
+
+ Finish();
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvLoaded() {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ return IPC_FAIL(this, "mFinishReceived already set!");
+ }
+
+ if (NS_WARN_IF(mLoadedReceived)) {
+ return IPC_FAIL(this, "mLoadedReceived already set!");
+ }
+
+ if (NS_WARN_IF(mLoadedAllItems)) {
+ return IPC_FAIL(this, "mLoadedAllItems already set!");
+ }
+
+ if (NS_WARN_IF(mLoadKeysReceived)) {
+ return IPC_FAIL(this, "mLoadKeysReceived already set!");
+ }
+
+ mLoadedReceived = true;
+
+ mLoadedItems.Clear();
+ mUnknownItems.Clear();
+ mValues.Clear();
+ mKeys.Clear();
+ mLoadedAllItems = true;
+ mLoadKeysReceived = true;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvLoadValueAndMoreItems(
+ const nsAString& aKey, LSValue* aValue, nsTArray<LSItemInfo>* aItemInfos) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aValue);
+ MOZ_ASSERT(aItemInfos);
+ MOZ_ASSERT(mDatastore);
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ return IPC_FAIL(this, "mFinishReceived already set!");
+ }
+
+ if (NS_WARN_IF(mLoadedReceived)) {
+ return IPC_FAIL(this, "mLoadedReceived already set!");
+ }
+
+ if (NS_WARN_IF(mLoadedAllItems)) {
+ return IPC_FAIL(this, "mLoadedAllItems already set!");
+ }
+
+ if (mLoadedItems.Contains(aKey)) {
+ return IPC_FAIL(this, "mLoadedItems already contains aKey!");
+ }
+
+ if (mUnknownItems.Contains(aKey)) {
+ return IPC_FAIL(this, "mUnknownItems already contains aKey!");
+ }
+
+ if (auto entry = mValues.Lookup(aKey)) {
+ *aValue = entry.Data();
+ entry.Remove();
+ } else {
+ mDatastore->GetItem(aKey, *aValue);
+ }
+
+ if (aValue->IsVoid()) {
+ mUnknownItems.Insert(aKey);
+ } else {
+ mLoadedItems.PutEntry(aKey);
+
+ // mLoadedItems.Count()==mTotalLength is checked below.
+ }
+
+ // Load some more key/value pairs (as many as the snapshot gradual prefill
+ // byte budget allows).
+
+ if (gSnapshotGradualPrefill > 0) {
+ const nsTArray<LSItemInfo>& orderedItems = mDatastore->GetOrderedItems();
+
+ uint32_t length;
+ if (mSavedKeys) {
+ length = mKeys.Length();
+ } else {
+ length = orderedItems.Length();
+ }
+
+ int64_t size = 0;
+ while (mNextLoadIndex < length) {
+ // If the datastore's ordering has changed, mSavedKeys will be true and
+ // mKeys contains an ordered list of the keys. Otherwise we can use the
+ // datastore's key ordering which is still the same as when the snapshot
+ // was created.
+
+ nsString key;
+ if (mSavedKeys) {
+ key = mKeys[mNextLoadIndex];
+ } else {
+ key = orderedItems[mNextLoadIndex].key();
+ }
+
+ // Normally we would do this:
+ // if (!mLoadedItems.GetEntry(key)) {
+ // ...
+ // mLoadedItems.PutEntry(key);
+ // }
+ // but that requires two hash lookups. We can reduce that to just one
+ // hash lookup if we always call PutEntry and check the number of entries
+ // before and after the put (which is very cheap). However, if we reach
+ // the prefill limit, we need to call RemoveEntry, but that is also cheap
+ // because we pass the entry (not the key).
+
+ uint32_t countBeforePut = mLoadedItems.Count();
+ auto loadedItemEntry = mLoadedItems.PutEntry(key);
+ if (countBeforePut != mLoadedItems.Count()) {
+ // Check mValues first since that contains values as they existed when
+ // our snapshot was created, but have since been changed/removed in the
+ // datastore. If it's not there, then the datastore has the
+ // still-current value. However, if the datastore's key ordering has
+ // changed, we need to do a hash lookup rather than being able to do an
+ // optimized direct access to the index.
+
+ LSValue value;
+ auto valueEntry = mValues.Lookup(key);
+ if (valueEntry) {
+ value = valueEntry.Data();
+ } else if (mSavedKeys) {
+ mDatastore->GetItem(nsString(key), value);
+ } else {
+ value = orderedItems[mNextLoadIndex].value();
+ }
+
+ // All not loaded keys must have a value.
+ MOZ_ASSERT(!value.IsVoid());
+
+ size += static_cast<int64_t>(key.Length()) +
+ static_cast<int64_t>(value.Length());
+
+ if (size > gSnapshotGradualPrefill) {
+ mLoadedItems.RemoveEntry(loadedItemEntry);
+
+ // mNextLoadIndex is not incremented, so we will resume at the same
+ // position next time.
+ break;
+ }
+
+ if (valueEntry) {
+ valueEntry.Remove();
+ }
+
+ LSItemInfo* itemInfo = aItemInfos->AppendElement();
+ itemInfo->key() = key;
+ itemInfo->value() = value;
+ }
+
+ mNextLoadIndex++;
+ }
+ }
+
+ if (mLoadedItems.Count() == mTotalLength) {
+ mLoadedItems.Clear();
+ mUnknownItems.Clear();
+#ifdef DEBUG
+ const bool allValuesVoid =
+ std::all_of(mValues.Values().cbegin(), mValues.Values().cend(),
+ [](const auto& entry) { return entry.IsVoid(); });
+ MOZ_ASSERT(allValuesVoid);
+#endif
+ mValues.Clear();
+ mLoadedAllItems = true;
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvLoadKeys(nsTArray<nsString>* aKeys) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aKeys);
+ MOZ_ASSERT(mDatastore);
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ return IPC_FAIL(this, "mFinishReceived already set!");
+ }
+
+ if (NS_WARN_IF(mLoadedReceived)) {
+ return IPC_FAIL(this, "mLoadedReceived already set!");
+ }
+
+ if (NS_WARN_IF(mLoadKeysReceived)) {
+ return IPC_FAIL(this, "mLoadKeysReceived already set!");
+ }
+
+ mLoadKeysReceived = true;
+
+ if (mSavedKeys) {
+ aKeys->AppendElements(std::move(mKeys));
+ } else {
+ mDatastore->GetKeys(*aKeys);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvIncreasePeakUsage(const int64_t& aMinSize,
+ int64_t* aSize) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aSize);
+
+ if (NS_WARN_IF(aMinSize <= 0)) {
+ return IPC_FAIL(this, "aMinSize not valid!");
+ }
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ return IPC_FAIL(this, "mFinishReceived already set!");
+ }
+
+ int64_t size =
+ mDatastore->AttemptToUpdateUsage(aMinSize, /* aInitial */ false);
+
+ mPeakUsage += size;
+
+ *aSize = size;
+
+ return IPC_OK();
+}
+
+/*******************************************************************************
+ * Observer
+ ******************************************************************************/
+
+Observer::Observer(const nsACString& aOrigin)
+ : mOrigin(aOrigin), mActorDestroyed(false) {
+ AssertIsOnBackgroundThread();
+}
+
+Observer::~Observer() { MOZ_ASSERT(mActorDestroyed); }
+
+void Observer::Observe(Database* aDatabase, const nsString& aDocumentURI,
+ const nsString& aKey, const LSValue& aOldValue,
+ const LSValue& aNewValue) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+
+ Unused << SendObserve(aDatabase->GetPrincipalInfo(),
+ aDatabase->PrivateBrowsingId(), aDocumentURI, aKey,
+ aOldValue, aNewValue);
+}
+
+void Observer::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ mActorDestroyed = true;
+
+ MOZ_ASSERT(gObservers);
+
+ nsTArray<NotNull<Observer*>>* array;
+ gObservers->Get(mOrigin, &array);
+ MOZ_ASSERT(array);
+
+ array->RemoveElement(this);
+
+ if (RefPtr<Datastore> datastore = GetDatastore(mOrigin)) {
+ datastore->NoteChangedObserverArray(*array);
+ }
+
+ if (array->IsEmpty()) {
+ gObservers->Remove(mOrigin);
+ }
+
+ if (!gObservers->Count()) {
+ gObservers = nullptr;
+ }
+}
+
+mozilla::ipc::IPCResult Observer::RecvDeleteMe() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ IProtocol* mgr = Manager();
+ if (!PBackgroundLSObserverParent::Send__delete__(this)) {
+ return IPC_FAIL(mgr, "Send__delete__ failed!");
+ }
+ return IPC_OK();
+}
+
+/*******************************************************************************
+ * LSRequestBase
+ ******************************************************************************/
+
+LSRequestBase::LSRequestBase(const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : mParams(aParams),
+ mContentParentId(aContentParentId),
+ mState(State::Initial),
+ mWaitingForFinish(false) {}
+
+LSRequestBase::~LSRequestBase() {
+ MOZ_ASSERT_IF(MayProceedOnNonOwningThread(),
+ mState == State::Initial || mState == State::Completed);
+}
+
+void LSRequestBase::Dispatch() {
+ AssertIsOnOwningThread();
+
+ mState = State::StartingRequest;
+
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(this));
+}
+
+void LSRequestBase::StringifyState(nsACString& aResult) const {
+ AssertIsOnOwningThread();
+
+ switch (mState) {
+ case State::Initial:
+ aResult.AppendLiteral("Initial");
+ return;
+
+ case State::StartingRequest:
+ aResult.AppendLiteral("StartingRequest");
+ return;
+
+ case State::Nesting:
+ aResult.AppendLiteral("Nesting");
+ return;
+
+ case State::SendingReadyMessage:
+ aResult.AppendLiteral("SendingReadyMessage");
+ return;
+
+ case State::WaitingForFinish:
+ aResult.AppendLiteral("WaitingForFinish");
+ return;
+
+ case State::SendingResults:
+ aResult.AppendLiteral("SendingResults");
+ return;
+
+ case State::Completed:
+ aResult.AppendLiteral("Completed");
+ return;
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+}
+
+void LSRequestBase::Stringify(nsACString& aResult) const {
+ AssertIsOnOwningThread();
+
+ aResult.AppendLiteral("State:");
+ StringifyState(aResult);
+}
+
+void LSRequestBase::Log() {
+ AssertIsOnOwningThread();
+
+ if (!LS_LOG_TEST()) {
+ return;
+ }
+
+ LS_LOG(("LSRequestBase [%p]", this));
+
+ nsCString state;
+ StringifyState(state);
+
+ LS_LOG((" mState: %s", state.get()));
+}
+
+nsresult LSRequestBase::NestedRun() { return NS_OK; }
+
+bool LSRequestBase::VerifyRequestParams() {
+ AssertIsOnBackgroundThread();
+
+ MOZ_ASSERT(mParams.type() != LSRequestParams::T__None);
+
+ switch (mParams.type()) {
+ case LSRequestParams::TLSRequestPreloadDatastoreParams: {
+ const LSRequestCommonParams& params =
+ mParams.get_LSRequestPreloadDatastoreParams().commonParams();
+
+ if (NS_WARN_IF(!VerifyPrincipalInfo(
+ params.principalInfo(), params.storagePrincipalInfo(), false))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(
+ !VerifyOriginKey(params.originKey(), params.principalInfo()))) {
+ return false;
+ }
+
+ break;
+ }
+
+ case LSRequestParams::TLSRequestPrepareDatastoreParams: {
+ const LSRequestPrepareDatastoreParams& params =
+ mParams.get_LSRequestPrepareDatastoreParams();
+
+ const LSRequestCommonParams& commonParams = params.commonParams();
+
+ if (NS_WARN_IF(!VerifyPrincipalInfo(commonParams.principalInfo(),
+ commonParams.storagePrincipalInfo(),
+ false))) {
+ return false;
+ }
+
+ if (params.clientPrincipalInfo() &&
+ NS_WARN_IF(!VerifyPrincipalInfo(commonParams.principalInfo(),
+ params.clientPrincipalInfo().ref(),
+ true))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!VerifyClientId(mContentParentId,
+ params.clientPrincipalInfo(),
+ params.clientId()))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!VerifyOriginKey(commonParams.originKey(),
+ commonParams.principalInfo()))) {
+ return false;
+ }
+
+ break;
+ }
+
+ case LSRequestParams::TLSRequestPrepareObserverParams: {
+ const LSRequestPrepareObserverParams& params =
+ mParams.get_LSRequestPrepareObserverParams();
+
+ if (NS_WARN_IF(!VerifyPrincipalInfo(
+ params.principalInfo(), params.storagePrincipalInfo(), false))) {
+ return false;
+ }
+
+ if (params.clientPrincipalInfo() &&
+ NS_WARN_IF(!VerifyPrincipalInfo(params.principalInfo(),
+ params.clientPrincipalInfo().ref(),
+ true))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!VerifyClientId(mContentParentId,
+ params.clientPrincipalInfo(),
+ params.clientId()))) {
+ return false;
+ }
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+
+ return true;
+}
+
+nsresult LSRequestBase::StartRequest() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::StartingRequest);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+#ifdef DEBUG
+ // Always verify parameters in DEBUG builds!
+ bool trustParams = false;
+#else
+ bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager());
+#endif
+
+ if (!trustParams && NS_WARN_IF(!VerifyRequestParams())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(Start()));
+
+ return NS_OK;
+}
+
+void LSRequestBase::SendReadyMessage() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingReadyMessage);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ MaybeSetFailureCode(NS_ERROR_ABORT);
+ }
+
+ nsresult rv = SendReadyMessageInternal();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MaybeSetFailureCode(rv);
+
+ FinishInternal();
+ }
+}
+
+nsresult LSRequestBase::SendReadyMessageInternal() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingReadyMessage);
+
+ if (!MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+ if (NS_WARN_IF(!SendReady())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mState = State::WaitingForFinish;
+
+ mWaitingForFinish = true;
+
+ return NS_OK;
+}
+
+void LSRequestBase::Finish() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::WaitingForFinish);
+
+ mWaitingForFinish = false;
+
+ FinishInternal();
+}
+
+void LSRequestBase::FinishInternal() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingReadyMessage ||
+ mState == State::WaitingForFinish);
+
+ mState = State::SendingResults;
+
+ // This LSRequestBase can only be held alive by the IPDL. Run() can end up
+ // with clearing that last reference. So we need to add a self reference here.
+ RefPtr<LSRequestBase> kungFuDeathGrip = this;
+
+ MOZ_ALWAYS_SUCCEEDS(this->Run());
+}
+
+void LSRequestBase::SendResults() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ MaybeSetFailureCode(NS_ERROR_ABORT);
+ }
+
+ if (MayProceed()) {
+ LSRequestResponse response;
+
+ if (NS_SUCCEEDED(ResultCode())) {
+ GetResponse(response);
+
+ MOZ_ASSERT(response.type() != LSRequestResponse::T__None);
+
+ if (response.type() == LSRequestResponse::Tnsresult) {
+ MOZ_ASSERT(NS_FAILED(response.get_nsresult()));
+
+ SetFailureCode(response.get_nsresult());
+ }
+ } else {
+ response = ResultCode();
+ }
+
+ Unused << PBackgroundLSRequestParent::Send__delete__(this, response);
+ }
+
+ Cleanup();
+
+ mState = State::Completed;
+}
+
+NS_IMETHODIMP
+LSRequestBase::Run() {
+ nsresult rv;
+
+ switch (mState) {
+ case State::StartingRequest:
+ rv = StartRequest();
+ break;
+
+ case State::Nesting:
+ rv = NestedRun();
+ break;
+
+ case State::SendingReadyMessage:
+ SendReadyMessage();
+ return NS_OK;
+
+ case State::SendingResults:
+ SendResults();
+ return NS_OK;
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingReadyMessage) {
+ MaybeSetFailureCode(rv);
+
+ // Must set mState before dispatching otherwise we will race with the owning
+ // thread.
+ mState = State::SendingReadyMessage;
+
+ if (IsOnOwningThread()) {
+ SendReadyMessage();
+ } else {
+ MOZ_ALWAYS_SUCCEEDS(
+ OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+ }
+ }
+
+ return NS_OK;
+}
+
+void LSRequestBase::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ NoteComplete();
+
+ // Assume ActorDestroy can happen at any time, so we can't probe the current
+ // state since mState can be modified on any thread (only one thread at a time
+ // based on the state machine). However we can use mWaitingForFinish which is
+ // only touched on the owning thread. If mWaitingForFinisg is true, we can
+ // also modify mState since we are guaranteed that there are no pending
+ // runnables which would probe mState to decide what code needs to run (there
+ // shouldn't be any running runnables on other threads either).
+
+ if (mWaitingForFinish) {
+ Finish();
+ }
+
+ // We don't have to handle the case when mWaitingForFinish is not true since
+ // it means that either nothing has been initialized yet, so nothing to
+ // cleanup or there are pending runnables that will detect that the actor has
+ // been destroyed and cleanup accordingly.
+}
+
+mozilla::ipc::IPCResult LSRequestBase::RecvCancel() {
+ AssertIsOnOwningThread();
+
+ Log();
+
+ const char* crashOnCancel = PR_GetEnv("LSNG_CRASH_ON_CANCEL");
+ if (crashOnCancel) {
+ MOZ_CRASH("LSNG: Crash on cancel.");
+ }
+
+ IProtocol* mgr = Manager();
+ if (!PBackgroundLSRequestParent::Send__delete__(this, NS_ERROR_ABORT)) {
+ return IPC_FAIL(mgr, "Send__delete__ failed!");
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult LSRequestBase::RecvFinish() {
+ AssertIsOnOwningThread();
+
+ Finish();
+
+ return IPC_OK();
+}
+
+/*******************************************************************************
+ * PrepareDatastoreOp
+ ******************************************************************************/
+
+PrepareDatastoreOp::PrepareDatastoreOp(
+ const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : LSRequestBase(aParams, aContentParentId),
+ mLoadDataOp(nullptr),
+ mPrivateBrowsingId(0),
+ mUsage(0),
+ mSizeOfKeys(0),
+ mSizeOfItems(0),
+ mDatastoreId(0),
+ mNestedState(NestedState::BeforeNesting),
+ mForPreload(aParams.type() ==
+ LSRequestParams::TLSRequestPreloadDatastoreParams),
+ mDatabaseNotAvailable(false),
+ mInvalidated(false)
+#ifdef DEBUG
+ ,
+ mDEBUGUsage(0)
+#endif
+{
+ MOZ_ASSERT(
+ aParams.type() == LSRequestParams::TLSRequestPreloadDatastoreParams ||
+ aParams.type() == LSRequestParams::TLSRequestPrepareDatastoreParams);
+}
+
+PrepareDatastoreOp::~PrepareDatastoreOp() {
+ MOZ_ASSERT(!mDirectoryLock);
+ MOZ_ASSERT_IF(MayProceedOnNonOwningThread(),
+ mState == State::Initial || mState == State::Completed);
+ MOZ_ASSERT(!mLoadDataOp);
+}
+
+void PrepareDatastoreOp::StringifyNestedState(nsACString& aResult) const {
+ AssertIsOnOwningThread();
+
+ switch (mNestedState) {
+ case NestedState::BeforeNesting:
+ aResult.AppendLiteral("BeforeNesting");
+ return;
+
+ case NestedState::CheckExistingOperations:
+ aResult.AppendLiteral("CheckExistingOperations");
+ return;
+
+ case NestedState::CheckClosingDatastore:
+ aResult.AppendLiteral("CheckClosingDatastore");
+ return;
+
+ case NestedState::PreparationPending:
+ aResult.AppendLiteral("PreparationPending");
+ return;
+
+ case NestedState::DirectoryOpenPending:
+ aResult.AppendLiteral("DirectoryOpenPending");
+ return;
+
+ case NestedState::DatabaseWorkOpen:
+ aResult.AppendLiteral("DatabaseWorkOpen");
+ return;
+
+ case NestedState::BeginLoadData:
+ aResult.AppendLiteral("BeginLoadData");
+ return;
+
+ case NestedState::DatabaseWorkLoadData:
+ aResult.AppendLiteral("DatabaseWorkLoadData");
+ return;
+
+ case NestedState::AfterNesting:
+ aResult.AppendLiteral("AfterNesting");
+ return;
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+}
+
+void PrepareDatastoreOp::Stringify(nsACString& aResult) const {
+ AssertIsOnOwningThread();
+
+ LSRequestBase::Stringify(aResult);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("Origin:");
+ aResult.Append(AnonymizedOriginString(Origin()));
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("NestedState:");
+ StringifyNestedState(aResult);
+}
+
+void PrepareDatastoreOp::Log() {
+ AssertIsOnOwningThread();
+
+ LSRequestBase::Log();
+
+ if (!LS_LOG_TEST()) {
+ return;
+ }
+
+ nsCString nestedState;
+ StringifyNestedState(nestedState);
+
+ LS_LOG((" mNestedState: %s", nestedState.get()));
+
+ switch (mNestedState) {
+ case NestedState::CheckClosingDatastore: {
+ for (uint32_t index = gPrepareDatastoreOps->Length(); index > 0;
+ index--) {
+ const auto& existingOp = (*gPrepareDatastoreOps)[index - 1];
+
+ if (existingOp->mDelayedOp == this) {
+ LS_LOG((" mDelayedBy: [%p]",
+ static_cast<PrepareDatastoreOp*>(existingOp.get())));
+
+ existingOp->Log();
+
+ break;
+ }
+ }
+
+ break;
+ }
+
+ case NestedState::DirectoryOpenPending: {
+ MOZ_ASSERT(mPendingDirectoryLock);
+
+ LS_LOG((" mPendingDirectoryLock: [%p]", mPendingDirectoryLock.get()));
+
+ mPendingDirectoryLock->Log();
+
+ break;
+ }
+
+ default:;
+ }
+}
+
+nsresult PrepareDatastoreOp::Start() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::StartingRequest);
+ MOZ_ASSERT(mNestedState == NestedState::BeforeNesting);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ const LSRequestCommonParams& commonParams =
+ mForPreload
+ ? mParams.get_LSRequestPreloadDatastoreParams().commonParams()
+ : mParams.get_LSRequestPrepareDatastoreParams().commonParams();
+
+ const PrincipalInfo& storagePrincipalInfo =
+ commonParams.storagePrincipalInfo();
+
+ if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
+ mOriginMetadata = {QuotaManager::GetInfoForChrome(),
+ PERSISTENCE_TYPE_DEFAULT};
+ } else {
+ MOZ_ASSERT(storagePrincipalInfo.type() ==
+ PrincipalInfo::TContentPrincipalInfo);
+
+ PrincipalMetadata principalMetadata =
+ QuotaManager::GetInfoFromValidatedPrincipalInfo(storagePrincipalInfo);
+
+ mOriginMetadata.mSuffix = std::move(principalMetadata.mSuffix);
+ mOriginMetadata.mGroup = std::move(principalMetadata.mGroup);
+ // XXX We can probably get rid of mMainThreadOrigin if we change
+ // LSRequestBase::Dispatch to synchronously run LSRequestBase::StartRequest
+ // through LSRequestBase::Run.
+ mMainThreadOrigin = std::move(principalMetadata.mOrigin);
+ mOriginMetadata.mPersistenceType = PERSISTENCE_TYPE_DEFAULT;
+ }
+
+ mState = State::Nesting;
+ mNestedState = NestedState::CheckExistingOperations;
+
+ MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::CheckExistingOperations() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::CheckExistingOperations);
+ MOZ_ASSERT(gPrepareDatastoreOps);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+ const LSRequestCommonParams& commonParams =
+ mForPreload
+ ? mParams.get_LSRequestPreloadDatastoreParams().commonParams()
+ : mParams.get_LSRequestPrepareDatastoreParams().commonParams();
+
+ const PrincipalInfo& storagePrincipalInfo =
+ commonParams.storagePrincipalInfo();
+
+ nsCString originAttrSuffix;
+ uint32_t privateBrowsingId;
+
+ if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
+ privateBrowsingId = 0;
+ } else {
+ MOZ_ASSERT(storagePrincipalInfo.type() ==
+ PrincipalInfo::TContentPrincipalInfo);
+
+ const ContentPrincipalInfo& info =
+ storagePrincipalInfo.get_ContentPrincipalInfo();
+ const OriginAttributes& attrs = info.attrs();
+ attrs.CreateSuffix(originAttrSuffix);
+
+ privateBrowsingId = attrs.mPrivateBrowsingId;
+ }
+
+ mArchivedOriginScope = ArchivedOriginScope::CreateFromOrigin(
+ originAttrSuffix, commonParams.originKey());
+ MOZ_ASSERT(mArchivedOriginScope);
+
+ // Normally it's safe to access member variables without a mutex because even
+ // though we hop between threads, the variables are never accessed by multiple
+ // threads at the same time.
+ // However, the methods OriginIsKnown and Origin can be called at any time.
+ // So we have to make sure the member variable is set on the same thread as
+ // those methods are called.
+ mOriginMetadata.mOrigin = mMainThreadOrigin;
+
+ MOZ_ASSERT(OriginIsKnown());
+
+ mPrivateBrowsingId = privateBrowsingId;
+
+ mNestedState = NestedState::CheckClosingDatastore;
+
+ // See if this PrepareDatastoreOp needs to wait.
+ bool foundThis = false;
+ for (uint32_t index = gPrepareDatastoreOps->Length(); index > 0; index--) {
+ const auto& existingOp = (*gPrepareDatastoreOps)[index - 1];
+
+ if (existingOp == this) {
+ foundThis = true;
+ continue;
+ }
+
+ if (foundThis && existingOp->Origin() == Origin()) {
+ // Only one op can be delayed.
+ MOZ_ASSERT(!existingOp->mDelayedOp);
+ existingOp->mDelayedOp = this;
+
+ return NS_OK;
+ }
+ }
+
+ QM_TRY(MOZ_TO_RESULT(CheckClosingDatastoreInternal()));
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::CheckClosingDatastore() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::CheckClosingDatastore);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(CheckClosingDatastoreInternal()));
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::CheckClosingDatastoreInternal() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::CheckClosingDatastore);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ mNestedState = NestedState::PreparationPending;
+
+ RefPtr<Datastore> datastore;
+ if ((datastore = GetDatastore(Origin())) && datastore->IsClosed()) {
+ datastore->WaitForConnectionToComplete(this);
+
+ return NS_OK;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(BeginDatastorePreparationInternal()));
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::BeginDatastorePreparation() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::PreparationPending);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(BeginDatastorePreparationInternal()));
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::BeginDatastorePreparationInternal() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::PreparationPending);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+ MOZ_ASSERT(OriginIsKnown());
+ MOZ_ASSERT(!mDirectoryLock);
+
+ if ((mDatastore = GetDatastore(Origin()))) {
+ MOZ_ASSERT(!mDatastore->IsClosed());
+
+ mDatastore->NoteLivePrepareDatastoreOp(this);
+
+ FinishNesting();
+
+ return NS_OK;
+ }
+
+ QM_TRY(QuotaManager::EnsureCreated());
+
+ // Open directory
+ mPendingDirectoryLock = QuotaManager::Get()->CreateDirectoryLock(
+ PERSISTENCE_TYPE_DEFAULT, mOriginMetadata,
+ mozilla::dom::quota::Client::LS,
+ /* aExclusive */ false);
+
+ mNestedState = NestedState::DirectoryOpenPending;
+
+ {
+ // Pin the directory lock, because Acquire might clear mPendingDirectoryLock
+ // during the Acquire call.
+ RefPtr pinnedDirectoryLock = mPendingDirectoryLock;
+ pinnedDirectoryLock->Acquire(this);
+ }
+
+ return NS_OK;
+}
+
+void PrepareDatastoreOp::SendToIOThread() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ // Skip all disk related stuff and transition to SendingReadyMessage if we
+ // are preparing a datastore for private browsing.
+ // Note that we do use a directory lock for private browsing even though we
+ // don't do any stuff on disk. The thing is that without a directory lock,
+ // quota manager wouldn't call AbortOperationsForLocks for our private
+ // browsing origin when a clear origin operation is requested.
+ // AbortOperationsForLocks requests all databases to close and the datastore
+ // is destroyed in the end. Any following LocalStorage API call will trigger
+ // preparation of a new (empty) datastore.
+ if (mPrivateBrowsingId) {
+ FinishNesting();
+
+ return;
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ // Must set this before dispatching otherwise we will race with the IO thread.
+ mNestedState = NestedState::DatabaseWorkOpen;
+
+ MOZ_ALWAYS_SUCCEEDS(
+ quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL));
+}
+
+nsresult PrepareDatastoreOp::DatabaseWork() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(mArchivedOriginScope);
+ MOZ_ASSERT(mUsage == 0);
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::DatabaseWorkOpen);
+
+ const auto innerFunc = [&](const auto&) -> nsresult {
+ // XXX This function is too long, refactor it into helper functions for
+ // readability.
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) ||
+ !MayProceedOnNonOwningThread()) {
+ return NS_ERROR_ABORT;
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ // This must be called before EnsureTemporaryStorageIsInitialized.
+ QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureStorageIsInitialized()));
+
+ // This ensures that usages for existings origin directories are cached in
+ // memory.
+ QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureTemporaryStorageIsInitialized()));
+
+ const UsageInfo usageInfo = quotaManager->GetUsageForClient(
+ PERSISTENCE_TYPE_DEFAULT, mOriginMetadata,
+ mozilla::dom::quota::Client::LS);
+
+ const bool hasUsage = usageInfo.DatabaseUsage().isSome();
+ MOZ_ASSERT(usageInfo.FileUsage().isNothing());
+
+ if (!gArchivedOrigins) {
+ QM_TRY(MOZ_TO_RESULT(LoadArchivedOrigins()));
+ MOZ_ASSERT(gArchivedOrigins);
+ }
+
+ bool hasDataForMigration =
+ mArchivedOriginScope->HasMatches(gArchivedOrigins);
+
+ // If there's nothing to preload (except the case when we want to migrate
+ // data during preloading), then we can finish the operation without
+ // creating a datastore in GetResponse (GetResponse won't create a datastore
+ // if mDatatabaseNotAvailable and mForPreload are both true).
+ if (mForPreload && !hasUsage && !hasDataForMigration) {
+ return DatabaseNotAvailable();
+ }
+
+ // The origin directory doesn't need to be created when we don't have data
+ // for migration. It will be created on the connection thread in
+ // Connection::EnsureStorageConnection.
+ // However, origin quota must be initialized, GetQuotaObject in GetResponse
+ // would fail otherwise.
+ QM_TRY_INSPECT(
+ const auto& directoryEntry,
+ ([hasDataForMigration, &quotaManager,
+ this]() -> mozilla::Result<nsCOMPtr<nsIFile>, nsresult> {
+ if (hasDataForMigration) {
+ QM_TRY_RETURN(quotaManager
+ ->EnsureTemporaryOriginIsInitialized(
+ PERSISTENCE_TYPE_DEFAULT, mOriginMetadata)
+ .map([](const auto& res) { return res.first; }));
+ }
+
+ QM_TRY_UNWRAP(auto directoryEntry,
+ quotaManager->GetDirectoryForOrigin(
+ PERSISTENCE_TYPE_DEFAULT, Origin()));
+
+ quotaManager->EnsureQuotaForOrigin(mOriginMetadata);
+
+ return directoryEntry;
+ }()));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(
+ NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME))));
+
+ QM_TRY_INSPECT(
+ const auto& directoryPath,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, directoryEntry, GetPath));
+
+ // The ls directory doesn't need to be created when we don't have data for
+ // migration. It will be created on the connection thread in
+ // Connection::EnsureStorageConnection.
+ QM_TRY(MOZ_TO_RESULT(
+ EnsureDirectoryEntry(directoryEntry,
+ /* aCreateIfNotExists */ hasDataForMigration,
+ /* aIsDirectory */ true)));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(kDataFileName)));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->GetPath(mDatabaseFilePath)));
+
+ // The database doesn't need to be created when we don't have data for
+ // migration. It will be created on the connection thread in
+ // Connection::EnsureStorageConnection.
+ bool alreadyExisted;
+ QM_TRY(MOZ_TO_RESULT(
+ EnsureDirectoryEntry(directoryEntry,
+ /* aCreateIfNotExists */ hasDataForMigration,
+ /* aIsDirectory */ false, &alreadyExisted)));
+
+ if (alreadyExisted) {
+ // The database does exist.
+ MOZ_ASSERT(hasUsage);
+
+ // XXX Change type of mUsage to UsageInfo or DatabaseUsageType.
+ mUsage = usageInfo.DatabaseUsage().valueOr(0);
+ } else {
+ // The database doesn't exist.
+ MOZ_ASSERT(!hasUsage);
+
+ if (!hasDataForMigration) {
+ // The database doesn't exist and we don't have data for migration.
+ // Finish the operation, but create an empty datastore in GetResponse
+ // (GetResponse will create an empty datastore if mDatabaseNotAvailable
+ // is true and mForPreload is false).
+ return DatabaseNotAvailable();
+ }
+ }
+
+ // We initialized mDatabaseFilePath and mUsage, GetQuotaObject can now be
+ // called.
+ const RefPtr<QuotaObject> quotaObject = GetQuotaObject();
+
+ QM_TRY(OkIf(quotaObject), Err(NS_ERROR_FAILURE));
+
+ QM_TRY_INSPECT(const auto& usageFile, GetUsageFile(directoryPath));
+
+ QM_TRY_INSPECT(const auto& usageJournalFile,
+ GetUsageJournalFile(directoryPath));
+
+ QM_TRY_INSPECT(
+ const auto& connection,
+ (CreateStorageConnection(
+ *directoryEntry, *usageFile, Origin(), [&quotaObject, this] {
+ // This is called when the usage file was removed or we notice
+ // that the usage file doesn't exist anymore. Adjust the usage
+ // accordingly.
+
+ MOZ_ALWAYS_TRUE(
+ quotaObject->MaybeUpdateSize(0, /* aTruncate */ true));
+
+ mUsage = 0;
+ })));
+
+ QM_TRY(MOZ_TO_RESULT(VerifyDatabaseInformation(connection)));
+
+ if (hasDataForMigration) {
+ MOZ_ASSERT(mUsage == 0);
+
+ {
+ QM_TRY_INSPECT(const auto& archiveFile,
+ GetArchiveFile(quotaManager->GetStoragePath()));
+
+ auto autoArchiveDatabaseAttacher =
+ AutoDatabaseAttacher(connection, archiveFile, "archive"_ns);
+
+ QM_TRY(MOZ_TO_RESULT(autoArchiveDatabaseAttacher.Attach()));
+
+ QM_TRY_INSPECT(const int64_t& newUsage,
+ GetUsage(*connection, mArchivedOriginScope.get()));
+
+ if (!quotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) {
+ return NS_ERROR_FILE_NO_DEVICE_SPACE;
+ }
+
+ auto autoUpdateSize = MakeScopeExit([&quotaObject] {
+ MOZ_ALWAYS_TRUE(
+ quotaObject->MaybeUpdateSize(0, /* aTruncate */ true));
+ });
+
+ mozStorageTransaction transaction(
+ connection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Start()));
+
+ {
+ nsCOMPtr<mozIStorageFunction> function = new CompressFunction();
+
+ QM_TRY(MOZ_TO_RESULT(
+ connection->CreateFunction("compress"_ns, 1, function)));
+
+ function = new CompressionTypeFunction();
+
+ QM_TRY(MOZ_TO_RESULT(
+ connection->CreateFunction("compressionType"_ns, 1, function)));
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "INSERT INTO data (key, utf16_length, conversion_type, "
+ "compression_type, value) "
+ "SELECT key, utf16Length(value), :conversionType, "
+ "compressionType(value), compress(value)"
+ "FROM webappsstore2 "
+ "WHERE originKey = :originKey "
+ "AND originAttributes = :originAttributes;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName(
+ "conversionType"_ns,
+ static_cast<int32_t>(LSValue::ConversionType::UTF16_UTF8))));
+
+ QM_TRY(MOZ_TO_RESULT(mArchivedOriginScope->BindToStatement(stmt)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+
+ QM_TRY(MOZ_TO_RESULT(connection->RemoveFunction("compress"_ns)));
+
+ QM_TRY(
+ MOZ_TO_RESULT(connection->RemoveFunction("compressionType"_ns)));
+ }
+
+ {
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "UPDATE database SET usage = :usage;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByName("usage"_ns, newUsage)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+ }
+
+ {
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "DELETE FROM webappsstore2 "
+ "WHERE originKey = :originKey "
+ "AND originAttributes = :originAttributes;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(mArchivedOriginScope->BindToStatement(stmt)));
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+ }
+
+ QM_TRY(MOZ_TO_RESULT(
+ UpdateUsageFile(usageFile, usageJournalFile, newUsage)));
+ QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
+
+ autoUpdateSize.release();
+
+ QM_TRY(MOZ_TO_RESULT(usageJournalFile->Remove(false)));
+
+ mUsage = newUsage;
+
+ QM_TRY(MOZ_TO_RESULT(autoArchiveDatabaseAttacher.Detach()));
+ }
+
+ MOZ_ASSERT(gArchivedOrigins);
+ MOZ_ASSERT(mArchivedOriginScope->HasMatches(gArchivedOrigins));
+ mArchivedOriginScope->RemoveMatches(gArchivedOrigins);
+ }
+
+ nsCOMPtr<mozIStorageConnection> shadowConnection;
+ if (!gInitializedShadowStorage) {
+ QM_TRY_UNWRAP(shadowConnection,
+ CreateShadowStorageConnection(quotaManager->GetBasePath()));
+
+ gInitializedShadowStorage = true;
+ }
+
+ // Must close connections before dispatching otherwise we might race with
+ // the connection thread which needs to open the same databases.
+ MOZ_ALWAYS_SUCCEEDS(connection->Close());
+
+ if (shadowConnection) {
+ MOZ_ALWAYS_SUCCEEDS(shadowConnection->Close());
+ }
+
+ // Must set this before dispatching otherwise we will race with the owning
+ // thread.
+ mNestedState = NestedState::BeginLoadData;
+
+ QM_TRY(
+ MOZ_TO_RESULT(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)));
+
+ return NS_OK;
+ };
+
+ return ExecuteOriginInitialization(
+ mOriginMetadata.mOrigin, LSOriginInitialization::Datastore,
+ "dom::localstorage::FirstOriginInitializationAttempt::Datastore"_ns,
+ innerFunc);
+}
+
+nsresult PrepareDatastoreOp::DatabaseNotAvailable() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::DatabaseWorkOpen);
+
+ mDatabaseNotAvailable = true;
+
+ nsresult rv = FinishNestingOnNonOwningThread();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::EnsureDirectoryEntry(nsIFile* aEntry,
+ bool aCreateIfNotExists,
+ bool aIsDirectory,
+ bool* aAlreadyExisted) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aEntry);
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aEntry, Exists));
+
+ if (!exists) {
+ if (!aCreateIfNotExists) {
+ if (aAlreadyExisted) {
+ *aAlreadyExisted = false;
+ }
+ return NS_OK;
+ }
+
+ if (aIsDirectory) {
+ QM_TRY(MOZ_TO_RESULT(aEntry->Create(nsIFile::DIRECTORY_TYPE, 0755)));
+ }
+ }
+#ifdef DEBUG
+ else {
+ bool isDirectory;
+ MOZ_ASSERT(NS_SUCCEEDED(aEntry->IsDirectory(&isDirectory)));
+ MOZ_ASSERT(isDirectory == aIsDirectory);
+ }
+#endif
+
+ if (aAlreadyExisted) {
+ *aAlreadyExisted = exists;
+ }
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::VerifyDatabaseInformation(
+ mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY_INSPECT(const auto& stmt,
+ CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ *aConnection, "SELECT origin FROM database"_ns));
+
+ QM_TRY(OkIf(stmt), NS_ERROR_FILE_CORRUPTED);
+
+ QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCString, stmt, GetUTF8String, 0));
+
+ QM_TRY(OkIf(QuotaManager::AreOriginsEqualOnDisk(Origin(), origin)),
+ NS_ERROR_FILE_CORRUPTED);
+
+ return NS_OK;
+}
+
+already_AddRefed<QuotaObject> PrepareDatastoreOp::GetQuotaObject() {
+ MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread());
+ MOZ_ASSERT(!mOriginMetadata.mGroup.IsEmpty());
+ MOZ_ASSERT(OriginIsKnown());
+ MOZ_ASSERT(!mDatabaseFilePath.IsEmpty());
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ RefPtr<QuotaObject> quotaObject = quotaManager->GetQuotaObject(
+ PERSISTENCE_TYPE_DEFAULT, mOriginMetadata,
+ mozilla::dom::quota::Client::LS, mDatabaseFilePath, mUsage);
+
+ if (!quotaObject) {
+ LS_WARNING("Failed to get quota object for group (%s) and origin (%s)!",
+ mOriginMetadata.mGroup.get(), Origin().get());
+ }
+
+ return quotaObject.forget();
+}
+
+nsresult PrepareDatastoreOp::BeginLoadData() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::BeginLoadData);
+ MOZ_ASSERT(!mConnection);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+ if (!gConnectionThread) {
+ gConnectionThread = new ConnectionThread();
+ }
+
+ mConnection = gConnectionThread->CreateConnection(
+ mOriginMetadata, std::move(mArchivedOriginScope),
+ /* aDatabaseWasNotAvailable */ false);
+ MOZ_ASSERT(mConnection);
+
+ // Must set this before dispatching otherwise we will race with the
+ // connection thread.
+ mNestedState = NestedState::DatabaseWorkLoadData;
+
+ // Can't assign to mLoadDataOp directly since that's a weak reference and
+ // LoadDataOp is reference counted.
+ RefPtr<LoadDataOp> loadDataOp = new LoadDataOp(this);
+
+ // This add refs loadDataOp.
+ mConnection->Dispatch(loadDataOp);
+
+ // This is cleared in LoadDataOp::Cleanup() before the load data op is
+ // destroyed.
+ mLoadDataOp = loadDataOp;
+
+ return NS_OK;
+}
+
+void PrepareDatastoreOp::FinishNesting() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+
+ // The caller holds a strong reference to us, no need for a self reference
+ // before calling Run().
+
+ mState = State::SendingReadyMessage;
+ mNestedState = NestedState::AfterNesting;
+
+ MOZ_ALWAYS_SUCCEEDS(Run());
+}
+
+nsresult PrepareDatastoreOp::FinishNestingOnNonOwningThread() {
+ MOZ_ASSERT(!IsOnOwningThread());
+ MOZ_ASSERT(mState == State::Nesting);
+
+ // Must set mState before dispatching otherwise we will race with the owning
+ // thread.
+ mState = State::SendingReadyMessage;
+ mNestedState = NestedState::AfterNesting;
+
+ QM_TRY(
+ MOZ_TO_RESULT(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)));
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::NestedRun() {
+ nsresult rv;
+
+ switch (mNestedState) {
+ case NestedState::CheckExistingOperations:
+ rv = CheckExistingOperations();
+ break;
+
+ case NestedState::CheckClosingDatastore:
+ rv = CheckClosingDatastore();
+ break;
+
+ case NestedState::PreparationPending:
+ rv = BeginDatastorePreparation();
+ break;
+
+ case NestedState::DatabaseWorkOpen:
+ rv = DatabaseWork();
+ break;
+
+ case NestedState::BeginLoadData:
+ rv = BeginLoadData();
+ break;
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mNestedState = NestedState::AfterNesting;
+
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+void PrepareDatastoreOp::GetResponse(LSRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+ MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ // A datastore is not created when we are just trying to preload data and
+ // there's no database file.
+ if (mDatabaseNotAvailable && mForPreload) {
+ LSRequestPreloadDatastoreResponse preloadDatastoreResponse;
+
+ aResponse = preloadDatastoreResponse;
+
+ return;
+ }
+
+ if (!mDatastore) {
+ MOZ_ASSERT(mUsage == mDEBUGUsage);
+
+ RefPtr<QuotaObject> quotaObject;
+
+ if (mPrivateBrowsingId == 0) {
+ if (!mConnection) {
+ // This can happen when there's no database file.
+ MOZ_ASSERT(mDatabaseNotAvailable);
+
+ // Even though there's no database file, we need to create a connection
+ // and pass it to datastore.
+ if (!gConnectionThread) {
+ gConnectionThread = new ConnectionThread();
+ }
+
+ mConnection = gConnectionThread->CreateConnection(
+ mOriginMetadata, std::move(mArchivedOriginScope),
+ /* aDatabaseWasNotAvailable */ true);
+ MOZ_ASSERT(mConnection);
+ }
+
+ quotaObject = GetQuotaObject();
+ if (!quotaObject) {
+ aResponse = NS_ERROR_FAILURE;
+ return;
+ }
+ }
+
+ mDatastore = new Datastore(
+ mOriginMetadata, mPrivateBrowsingId, mUsage, mSizeOfKeys, mSizeOfItems,
+ std::move(mDirectoryLock), std::move(mConnection),
+ std::move(quotaObject), mValues, std::move(mOrderedItems));
+
+ mDatastore->NoteLivePrepareDatastoreOp(this);
+
+ if (!gDatastores) {
+ gDatastores = new DatastoreHashtable();
+ }
+
+ MOZ_ASSERT(!gDatastores->Contains(Origin()));
+ gDatastores->InsertOrUpdate(Origin(),
+ WrapMovingNotNullUnchecked(mDatastore));
+ }
+
+ if (mPrivateBrowsingId && !mInvalidated) {
+ if (!gPrivateDatastores) {
+ gPrivateDatastores = MakeUnique<PrivateDatastoreHashtable>();
+ }
+
+ gPrivateDatastores->LookupOrInsertWith(Origin(), [&] {
+ auto privateDatastore =
+ MakeUnique<PrivateDatastore>(WrapMovingNotNull(mDatastore));
+
+ mPrivateDatastoreRegistered.Flip();
+
+ return privateDatastore;
+ });
+ }
+
+ mDatastoreId = ++gLastDatastoreId;
+
+ if (!gPreparedDatastores) {
+ gPreparedDatastores = new PreparedDatastoreHashtable();
+ }
+ const auto& preparedDatastore = gPreparedDatastores->InsertOrUpdate(
+ mDatastoreId, MakeUnique<PreparedDatastore>(
+ mDatastore, mContentParentId, Origin(), mDatastoreId,
+ /* aForPreload */ mForPreload));
+
+ if (mInvalidated) {
+ preparedDatastore->Invalidate();
+ }
+
+ mPreparedDatastoreRegistered.Flip();
+
+ if (mForPreload) {
+ LSRequestPreloadDatastoreResponse preloadDatastoreResponse;
+
+ aResponse = preloadDatastoreResponse;
+ } else {
+ LSRequestPrepareDatastoreResponse prepareDatastoreResponse;
+ prepareDatastoreResponse.datastoreId() = mDatastoreId;
+
+ aResponse = prepareDatastoreResponse;
+ }
+}
+
+void PrepareDatastoreOp::Cleanup() {
+ AssertIsOnOwningThread();
+
+ if (mDatastore) {
+ MOZ_ASSERT(!mDirectoryLock);
+ MOZ_ASSERT(!mConnection);
+
+ if (NS_FAILED(ResultCode())) {
+ if (mPrivateDatastoreRegistered) {
+ MOZ_ASSERT(gPrivateDatastores);
+ DebugOnly<bool> removed = gPrivateDatastores->Remove(Origin());
+ MOZ_ASSERT(removed);
+
+ if (!gPrivateDatastores->Count()) {
+ gPrivateDatastores = nullptr;
+ }
+ }
+
+ if (mPreparedDatastoreRegistered) {
+ // Just in case we failed to send datastoreId to the child, we need to
+ // destroy prepared datastore, otherwise it won't be destroyed until
+ // the timer fires (after 20 seconds).
+ MOZ_ASSERT(gPreparedDatastores);
+ MOZ_ASSERT(mDatastoreId > 0);
+ DebugOnly<bool> removed = gPreparedDatastores->Remove(mDatastoreId);
+ MOZ_ASSERT(removed);
+
+ if (!gPreparedDatastores->Count()) {
+ gPreparedDatastores = nullptr;
+ }
+ }
+ }
+
+ // Make sure to release the datastore on this thread.
+
+ mDatastore->NoteFinishedPrepareDatastoreOp(this);
+
+ mDatastore = nullptr;
+
+ CleanupMetadata();
+ } else if (mConnection) {
+ // If we have a connection then the operation must have failed and there
+ // must be a directory lock too.
+ MOZ_ASSERT(NS_FAILED(ResultCode()));
+ MOZ_ASSERT(mDirectoryLock);
+
+ // We must close the connection on the connection thread before releasing
+ // it on this thread. The directory lock can't be released either.
+ nsCOMPtr<nsIRunnable> callback =
+ NewRunnableMethod("dom::OpenDatabaseOp::ConnectionClosedCallback", this,
+ &PrepareDatastoreOp::ConnectionClosedCallback);
+
+ mConnection->Close(callback);
+ } else {
+ // If we don't have a connection, but we do have a directory lock then the
+ // operation must have failed or we were preloading a datastore and there
+ // was no physical database on disk.
+ MOZ_ASSERT_IF(mDirectoryLock,
+ NS_FAILED(ResultCode()) || mDatabaseNotAvailable);
+
+ // There's no connection, so it's safe to release the directory lock and
+ // unregister itself from the array.
+
+ mDirectoryLock = nullptr;
+
+ CleanupMetadata();
+ }
+}
+
+void PrepareDatastoreOp::ConnectionClosedCallback() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(NS_FAILED(ResultCode()));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(mConnection);
+
+ mConnection = nullptr;
+ mDirectoryLock = nullptr;
+
+ CleanupMetadata();
+}
+
+void PrepareDatastoreOp::CleanupMetadata() {
+ AssertIsOnOwningThread();
+
+ if (mDelayedOp) {
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mDelayedOp.forget()));
+ }
+
+ MOZ_ASSERT(gPrepareDatastoreOps);
+ gPrepareDatastoreOps->RemoveElement(this);
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(
+ quota::Client::LS, "PrepareDatastoreOp completed"_ns);
+
+ if (gPrepareDatastoreOps->IsEmpty()) {
+ gPrepareDatastoreOps = nullptr;
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED0(PrepareDatastoreOp, LSRequestBase)
+
+void PrepareDatastoreOp::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ LSRequestBase::ActorDestroy(aWhy);
+
+ if (mLoadDataOp) {
+ mLoadDataOp->NoteComplete();
+ }
+}
+
+void PrepareDatastoreOp::DirectoryLockAcquired(DirectoryLock* aLock) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending);
+ MOZ_ASSERT(!mDirectoryLock);
+
+ mPendingDirectoryLock = nullptr;
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ MaybeSetFailureCode(NS_ERROR_ABORT);
+
+ FinishNesting();
+
+ return;
+ }
+
+ mDirectoryLock = aLock;
+
+ SendToIOThread();
+}
+
+void PrepareDatastoreOp::DirectoryLockFailed() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending);
+ MOZ_ASSERT(!mDirectoryLock);
+
+ mPendingDirectoryLock = nullptr;
+
+ MaybeSetFailureCode(NS_ERROR_FAILURE);
+
+ FinishNesting();
+}
+
+nsresult PrepareDatastoreOp::LoadDataOp::DoDatastoreWork() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(mPrepareDatastoreOp);
+ MOZ_ASSERT(mPrepareDatastoreOp->mState == State::Nesting);
+ MOZ_ASSERT(mPrepareDatastoreOp->mNestedState ==
+ NestedState::DatabaseWorkLoadData);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) ||
+ !MayProceedOnNonOwningThread()) {
+ return NS_ERROR_ABORT;
+ }
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ mConnection->BorrowCachedStatement(
+ "SELECT key, utf16_length, conversion_type, compression_type, value "
+ "FROM data;"_ns));
+
+ QM_TRY(quota::CollectWhileHasResult(
+ *stmt, [this](auto& stmt) -> mozilla::Result<Ok, nsresult> {
+ QM_TRY_UNWRAP(auto key, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsString, stmt, GetString, 0));
+
+ LSValue value;
+ QM_TRY(MOZ_TO_RESULT(value.InitFromStatement(&stmt, 1)));
+
+ mPrepareDatastoreOp->mValues.InsertOrUpdate(key, value);
+ mPrepareDatastoreOp->mSizeOfKeys += key.Length();
+ mPrepareDatastoreOp->mSizeOfItems += key.Length() + value.Length();
+#ifdef DEBUG
+ mPrepareDatastoreOp->mDEBUGUsage += key.Length() + value.UTF16Length();
+#endif
+
+ auto item = mPrepareDatastoreOp->mOrderedItems.AppendElement();
+ item->key() = std::move(key);
+ item->value() = std::move(value);
+
+ return Ok{};
+ }));
+
+ return NS_OK;
+}
+
+void PrepareDatastoreOp::LoadDataOp::OnSuccess() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mPrepareDatastoreOp);
+ MOZ_ASSERT(mPrepareDatastoreOp->mState == State::Nesting);
+ MOZ_ASSERT(mPrepareDatastoreOp->mNestedState ==
+ NestedState::DatabaseWorkLoadData);
+ MOZ_ASSERT(mPrepareDatastoreOp->mLoadDataOp == this);
+
+ mPrepareDatastoreOp->FinishNesting();
+}
+
+void PrepareDatastoreOp::LoadDataOp::OnFailure(nsresult aResultCode) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mPrepareDatastoreOp);
+ MOZ_ASSERT(mPrepareDatastoreOp->mState == State::Nesting);
+ MOZ_ASSERT(mPrepareDatastoreOp->mNestedState ==
+ NestedState::DatabaseWorkLoadData);
+ MOZ_ASSERT(mPrepareDatastoreOp->mLoadDataOp == this);
+
+ mPrepareDatastoreOp->SetFailureCode(aResultCode);
+
+ mPrepareDatastoreOp->FinishNesting();
+}
+
+void PrepareDatastoreOp::LoadDataOp::Cleanup() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mPrepareDatastoreOp);
+ MOZ_ASSERT(mPrepareDatastoreOp->mLoadDataOp == this);
+
+ mPrepareDatastoreOp->mLoadDataOp = nullptr;
+ mPrepareDatastoreOp = nullptr;
+
+ ConnectionDatastoreOperationBase::Cleanup();
+}
+
+NS_IMPL_ISUPPORTS(PrepareDatastoreOp::CompressFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+PrepareDatastoreOp::CompressFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aFunctionArguments);
+ MOZ_ASSERT(aResult);
+
+#ifdef DEBUG
+ {
+ uint32_t argCount;
+ MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetNumEntries(&argCount));
+ MOZ_ASSERT(argCount == 1);
+
+ int32_t type;
+ MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetTypeOfIndex(0, &type));
+ MOZ_ASSERT(type == mozIStorageValueArray::VALUE_TYPE_TEXT);
+ }
+#endif
+
+ QM_TRY_INSPECT(const auto& value,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCString, aFunctionArguments, GetUTF8String, 0));
+
+ nsCString compressed;
+ QM_TRY(OkIf(SnappyCompress(value, compressed)), NS_ERROR_OUT_OF_MEMORY);
+
+ const nsCString& buffer = compressed.IsVoid() ? value : compressed;
+
+ // mozStorage transforms empty blobs into null values, but our database
+ // schema doesn't allow null values. We can workaround this by storing
+ // empty buffers as UTF8 text (SQLite supports the type affinity, so the type
+ // of the column is not fixed).
+ nsCOMPtr<nsIVariant> result;
+ if (0u == buffer.Length()) { // Otherwise empty string becomes null
+ result = new storage::UTF8TextVariant(buffer);
+ } else {
+ result = new storage::BlobVariant(std::make_pair(
+ static_cast<const void*>(buffer.get()), int(buffer.Length())));
+ }
+
+ result.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(PrepareDatastoreOp::CompressionTypeFunction,
+ mozIStorageFunction)
+
+NS_IMETHODIMP
+PrepareDatastoreOp::CompressionTypeFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aFunctionArguments);
+ MOZ_ASSERT(aResult);
+
+#ifdef DEBUG
+ {
+ uint32_t argCount;
+ MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetNumEntries(&argCount));
+ MOZ_ASSERT(argCount == 1);
+
+ int32_t type;
+ MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetTypeOfIndex(0, &type));
+ MOZ_ASSERT(type == mozIStorageValueArray::VALUE_TYPE_TEXT);
+ }
+#endif
+
+ QM_TRY_INSPECT(const auto& value,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCString, aFunctionArguments, GetUTF8String, 0));
+
+ nsCString compressed;
+ QM_TRY(OkIf(SnappyCompress(value, compressed)), NS_ERROR_OUT_OF_MEMORY);
+
+ const int32_t compression = static_cast<int32_t>(
+ compressed.IsVoid() ? LSValue::CompressionType::UNCOMPRESSED
+ : LSValue::CompressionType::SNAPPY);
+
+ nsCOMPtr<nsIVariant> result = new storage::IntegerVariant(compression);
+
+ result.forget(aResult);
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * PrepareObserverOp
+ ******************************************************************************/
+
+PrepareObserverOp::PrepareObserverOp(
+ const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : LSRequestBase(aParams, aContentParentId) {
+ MOZ_ASSERT(aParams.type() ==
+ LSRequestParams::TLSRequestPrepareObserverParams);
+}
+
+nsresult PrepareObserverOp::Start() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::StartingRequest);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ const LSRequestPrepareObserverParams params =
+ mParams.get_LSRequestPrepareObserverParams();
+
+ const PrincipalInfo& storagePrincipalInfo = params.storagePrincipalInfo();
+
+ if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
+ mOrigin = QuotaManager::GetOriginForChrome();
+ } else {
+ MOZ_ASSERT(storagePrincipalInfo.type() ==
+ PrincipalInfo::TContentPrincipalInfo);
+
+ mOrigin =
+ QuotaManager::GetOriginFromValidatedPrincipalInfo(storagePrincipalInfo);
+ }
+
+ mState = State::SendingReadyMessage;
+ MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+
+ return NS_OK;
+}
+
+void PrepareObserverOp::GetResponse(LSRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+ MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ uint64_t observerId = ++gLastObserverId;
+
+ RefPtr<Observer> observer = new Observer(mOrigin);
+
+ if (!gPreparedObsevers) {
+ gPreparedObsevers = new PreparedObserverHashtable();
+ }
+ gPreparedObsevers->InsertOrUpdate(observerId, std::move(observer));
+
+ LSRequestPrepareObserverResponse prepareObserverResponse;
+ prepareObserverResponse.observerId() = observerId;
+
+ aResponse = prepareObserverResponse;
+}
+
+/*******************************************************************************
++ * LSSimpleRequestBase
++
+******************************************************************************/
+
+LSSimpleRequestBase::LSSimpleRequestBase(
+ const LSSimpleRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : mParams(aParams),
+ mContentParentId(aContentParentId),
+ mState(State::Initial) {}
+
+LSSimpleRequestBase::~LSSimpleRequestBase() {
+ MOZ_ASSERT_IF(MayProceedOnNonOwningThread(),
+ mState == State::Initial || mState == State::Completed);
+}
+
+void LSSimpleRequestBase::Dispatch() {
+ AssertIsOnOwningThread();
+
+ mState = State::StartingRequest;
+
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(this));
+}
+
+bool LSSimpleRequestBase::VerifyRequestParams() {
+ AssertIsOnBackgroundThread();
+
+ MOZ_ASSERT(mParams.type() != LSSimpleRequestParams::T__None);
+
+ switch (mParams.type()) {
+ case LSSimpleRequestParams::TLSSimpleRequestPreloadedParams: {
+ const LSSimpleRequestPreloadedParams& params =
+ mParams.get_LSSimpleRequestPreloadedParams();
+
+ if (NS_WARN_IF(!VerifyPrincipalInfo(
+ params.principalInfo(), params.storagePrincipalInfo(), false))) {
+ return false;
+ }
+
+ break;
+ }
+
+ case LSSimpleRequestParams::TLSSimpleRequestGetStateParams: {
+ const LSSimpleRequestGetStateParams& params =
+ mParams.get_LSSimpleRequestGetStateParams();
+
+ if (NS_WARN_IF(!VerifyPrincipalInfo(
+ params.principalInfo(), params.storagePrincipalInfo(), false))) {
+ return false;
+ }
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+
+ return true;
+}
+
+nsresult LSSimpleRequestBase::StartRequest() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::StartingRequest);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+#ifdef DEBUG
+ // Always verify parameters in DEBUG builds!
+ bool trustParams = false;
+#else
+ bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager());
+#endif
+
+ if (!trustParams && NS_WARN_IF(!VerifyRequestParams())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(Start()));
+
+ return NS_OK;
+}
+
+void LSSimpleRequestBase::SendResults() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ MaybeSetFailureCode(NS_ERROR_ABORT);
+ }
+
+ if (MayProceed()) {
+ LSSimpleRequestResponse response;
+
+ if (NS_SUCCEEDED(ResultCode())) {
+ GetResponse(response);
+ } else {
+ response = ResultCode();
+ }
+
+ Unused << PBackgroundLSSimpleRequestParent::Send__delete__(this, response);
+ }
+
+ mState = State::Completed;
+}
+
+NS_IMETHODIMP
+LSSimpleRequestBase::Run() {
+ nsresult rv;
+
+ switch (mState) {
+ case State::StartingRequest:
+ rv = StartRequest();
+ break;
+
+ case State::SendingResults:
+ SendResults();
+ return NS_OK;
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingResults) {
+ MaybeSetFailureCode(rv);
+
+ // Must set mState before dispatching otherwise we will race with the owning
+ // thread.
+ mState = State::SendingResults;
+
+ if (IsOnOwningThread()) {
+ SendResults();
+ } else {
+ MOZ_ALWAYS_SUCCEEDS(
+ OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+ }
+ }
+
+ return NS_OK;
+}
+
+void LSSimpleRequestBase::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ NoteComplete();
+}
+
+/*******************************************************************************
+ * PreloadedOp
+ ******************************************************************************/
+
+PreloadedOp::PreloadedOp(const LSSimpleRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : LSSimpleRequestBase(aParams, aContentParentId) {
+ MOZ_ASSERT(aParams.type() ==
+ LSSimpleRequestParams::TLSSimpleRequestPreloadedParams);
+}
+
+nsresult PreloadedOp::Start() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::StartingRequest);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ const LSSimpleRequestPreloadedParams& params =
+ mParams.get_LSSimpleRequestPreloadedParams();
+
+ const PrincipalInfo& storagePrincipalInfo = params.storagePrincipalInfo();
+
+ MOZ_ASSERT(
+ storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo ||
+ storagePrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo);
+ mOrigin = storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo
+ ? nsCString{QuotaManager::GetOriginForChrome()}
+ : QuotaManager::GetOriginFromValidatedPrincipalInfo(
+ storagePrincipalInfo);
+
+ mState = State::SendingResults;
+ MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+
+ return NS_OK;
+}
+
+void PreloadedOp::GetResponse(LSSimpleRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+ MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ bool preloaded;
+ RefPtr<Datastore> datastore;
+ if ((datastore = GetDatastore(mOrigin)) && !datastore->IsClosed()) {
+ preloaded = true;
+ } else {
+ preloaded = false;
+ }
+
+ LSSimpleRequestPreloadedResponse preloadedResponse;
+ preloadedResponse.preloaded() = preloaded;
+
+ aResponse = preloadedResponse;
+}
+
+/*******************************************************************************
+ * GetStateOp
+ ******************************************************************************/
+
+GetStateOp::GetStateOp(const LSSimpleRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : LSSimpleRequestBase(aParams, aContentParentId) {
+ MOZ_ASSERT(aParams.type() ==
+ LSSimpleRequestParams::TLSSimpleRequestGetStateParams);
+}
+
+nsresult GetStateOp::Start() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::StartingRequest);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ const LSSimpleRequestGetStateParams& params =
+ mParams.get_LSSimpleRequestGetStateParams();
+
+ const PrincipalInfo& storagePrincipalInfo = params.storagePrincipalInfo();
+
+ MOZ_ASSERT(
+ storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo ||
+ storagePrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo);
+ mOrigin = storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo
+ ? nsCString{QuotaManager::GetOriginForChrome()}
+ : QuotaManager::GetOriginFromValidatedPrincipalInfo(
+ storagePrincipalInfo);
+
+ mState = State::SendingResults;
+ MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+
+ return NS_OK;
+}
+
+void GetStateOp::GetResponse(LSSimpleRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+ MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ LSSimpleRequestGetStateResponse getStateResponse;
+
+ if (RefPtr<Datastore> datastore = GetDatastore(mOrigin)) {
+ if (!datastore->IsClosed()) {
+ getStateResponse.itemInfos() = datastore->GetOrderedItems().Clone();
+ }
+ }
+
+ aResponse = getStateResponse;
+}
+
+/*******************************************************************************
+ * ArchivedOriginScope
+ ******************************************************************************/
+
+// static
+UniquePtr<ArchivedOriginScope> ArchivedOriginScope::CreateFromOrigin(
+ const nsACString& aOriginAttrSuffix, const nsACString& aOriginKey) {
+ return WrapUnique(
+ new ArchivedOriginScope(Origin(aOriginAttrSuffix, aOriginKey)));
+}
+
+// static
+UniquePtr<ArchivedOriginScope> ArchivedOriginScope::CreateFromPrefix(
+ const nsACString& aOriginKey) {
+ return WrapUnique(new ArchivedOriginScope(Prefix(aOriginKey)));
+}
+
+// static
+UniquePtr<ArchivedOriginScope> ArchivedOriginScope::CreateFromPattern(
+ const OriginAttributesPattern& aPattern) {
+ return WrapUnique(new ArchivedOriginScope(Pattern(aPattern)));
+}
+
+// static
+UniquePtr<ArchivedOriginScope> ArchivedOriginScope::CreateFromNull() {
+ return WrapUnique(new ArchivedOriginScope(Null()));
+}
+
+nsLiteralCString ArchivedOriginScope::GetBindingClause() const {
+ return mData.match(
+ [](const Origin&) {
+ return " WHERE originKey = :originKey "
+ "AND originAttributes = :originAttributes"_ns;
+ },
+ [](const Pattern&) {
+ return " WHERE originAttributes MATCH :originAttributesPattern"_ns;
+ },
+ [](const Prefix&) { return " WHERE originKey = :originKey"_ns; },
+ [](const Null&) { return ""_ns; });
+}
+
+nsresult ArchivedOriginScope::BindToStatement(
+ mozIStorageStatement* aStmt) const {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(aStmt);
+
+ struct Matcher {
+ mozIStorageStatement* mStmt;
+
+ explicit Matcher(mozIStorageStatement* aStmt) : mStmt(aStmt) {}
+
+ nsresult operator()(const Origin& aOrigin) {
+ QM_TRY(MOZ_TO_RESULT(mStmt->BindUTF8StringByName(
+ "originKey"_ns, aOrigin.OriginNoSuffix())));
+
+ QM_TRY(MOZ_TO_RESULT(mStmt->BindUTF8StringByName(
+ "originAttributes"_ns, aOrigin.OriginSuffix())));
+
+ return NS_OK;
+ }
+
+ nsresult operator()(const Prefix& aPrefix) {
+ QM_TRY(MOZ_TO_RESULT(mStmt->BindUTF8StringByName(
+ "originKey"_ns, aPrefix.OriginNoSuffix())));
+
+ return NS_OK;
+ }
+
+ nsresult operator()(const Pattern& aPattern) {
+ QM_TRY(MOZ_TO_RESULT(mStmt->BindUTF8StringByName(
+ "originAttributesPattern"_ns, "pattern1"_ns)));
+
+ return NS_OK;
+ }
+
+ nsresult operator()(const Null& aNull) { return NS_OK; }
+ };
+
+ QM_TRY(MOZ_TO_RESULT(mData.match(Matcher(aStmt))));
+
+ return NS_OK;
+}
+
+bool ArchivedOriginScope::HasMatches(
+ ArchivedOriginHashtable* aHashtable) const {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aHashtable);
+
+ return mData.match(
+ [aHashtable](const Origin& aOrigin) {
+ const nsCString hashKey = GetArchivedOriginHashKey(
+ aOrigin.OriginSuffix(), aOrigin.OriginNoSuffix());
+
+ return aHashtable->Contains(hashKey);
+ },
+ [aHashtable](const Pattern& aPattern) {
+ return std::any_of(
+ aHashtable->Values().cbegin(), aHashtable->Values().cend(),
+ [&aPattern](const auto& entry) {
+ return aPattern.GetPattern().Matches(entry->mOriginAttributes);
+ });
+ },
+ [aHashtable](const Prefix& aPrefix) {
+ return std::any_of(
+ aHashtable->Values().cbegin(), aHashtable->Values().cend(),
+ [&aPrefix](const auto& entry) {
+ return entry->mOriginNoSuffix == aPrefix.OriginNoSuffix();
+ });
+ },
+ [aHashtable](const Null& aNull) { return !aHashtable->IsEmpty(); });
+}
+
+void ArchivedOriginScope::RemoveMatches(
+ ArchivedOriginHashtable* aHashtable) const {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aHashtable);
+
+ struct Matcher {
+ ArchivedOriginHashtable* mHashtable;
+
+ explicit Matcher(ArchivedOriginHashtable* aHashtable)
+ : mHashtable(aHashtable) {}
+
+ void operator()(const Origin& aOrigin) {
+ nsCString hashKey = GetArchivedOriginHashKey(aOrigin.OriginSuffix(),
+ aOrigin.OriginNoSuffix());
+
+ mHashtable->Remove(hashKey);
+ }
+
+ void operator()(const Prefix& aPrefix) {
+ for (auto iter = mHashtable->Iter(); !iter.Done(); iter.Next()) {
+ const auto& archivedOriginInfo = iter.Data();
+
+ if (archivedOriginInfo->mOriginNoSuffix == aPrefix.OriginNoSuffix()) {
+ iter.Remove();
+ }
+ }
+ }
+
+ void operator()(const Pattern& aPattern) {
+ for (auto iter = mHashtable->Iter(); !iter.Done(); iter.Next()) {
+ const auto& archivedOriginInfo = iter.Data();
+
+ if (aPattern.GetPattern().Matches(
+ archivedOriginInfo->mOriginAttributes)) {
+ iter.Remove();
+ }
+ }
+ }
+
+ void operator()(const Null& aNull) { mHashtable->Clear(); }
+ };
+
+ mData.match(Matcher(aHashtable));
+}
+
+/*******************************************************************************
+ * QuotaClient
+ ******************************************************************************/
+
+QuotaClient* QuotaClient::sInstance = nullptr;
+
+QuotaClient::QuotaClient()
+ : mShadowDatabaseMutex("LocalStorage mShadowDatabaseMutex") {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!sInstance, "We expect this to be a singleton!");
+
+ sInstance = this;
+}
+
+QuotaClient::~QuotaClient() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(sInstance == this, "We expect this to be a singleton!");
+
+ sInstance = nullptr;
+}
+
+// static
+nsresult QuotaClient::Initialize() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = Observer::Initialize();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+mozilla::dom::quota::Client::Type QuotaClient::GetType() {
+ return QuotaClient::LS;
+}
+
+Result<UsageInfo, nsresult> QuotaClient::InitOrigin(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY_INSPECT(const auto& directory,
+ quotaManager->GetDirectoryForOrigin(aPersistenceType,
+ aOriginMetadata.mOrigin));
+
+ MOZ_ASSERT(directory);
+
+ QM_TRY(MOZ_TO_RESULT(
+ directory->Append(NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME))));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists));
+ MOZ_ASSERT(exists);
+ }
+#endif
+
+ QM_TRY_INSPECT(const auto& directoryPath, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsString, directory, GetPath));
+
+ QM_TRY_INSPECT(const auto& usageFile, GetUsageFile(directoryPath));
+
+ // XXX Try to make usageFileExists const
+ QM_TRY_UNWRAP(bool usageFileExists, ExistsAsFile(*usageFile));
+
+ QM_TRY_INSPECT(const auto& usageJournalFile,
+ GetUsageJournalFile(directoryPath));
+
+ QM_TRY_INSPECT(const bool& usageJournalFileExists,
+ ExistsAsFile(*usageJournalFile));
+
+ if (usageJournalFileExists) {
+ if (usageFileExists) {
+ QM_TRY(MOZ_TO_RESULT(usageFile->Remove(false)));
+
+ usageFileExists = false;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(usageJournalFile->Remove(false)));
+ }
+
+ QM_TRY_INSPECT(const auto& file,
+ CloneFileAndAppend(*directory, kDataFileName));
+
+ QM_TRY_INSPECT(const bool& fileExists, ExistsAsFile(*file));
+
+ QM_TRY_INSPECT(
+ const UsageInfo& res,
+ ([fileExists, usageFileExists, &file, &usageFile, &usageJournalFile,
+ &aOriginMetadata]() -> Result<UsageInfo, nsresult> {
+ if (fileExists) {
+ QM_TRY_RETURN(QM_OR_ELSE_WARN(
+ // Expression. To simplify control flow, we call LoadUsageFile
+ // unconditionally here, even though it will necessarily fail if
+ // usageFileExists is false.
+ LoadUsageFile(*usageFile),
+ // Fallback.
+ ([&file, &usageFile, &usageJournalFile, &aOriginMetadata](
+ const nsresult) -> Result<UsageInfo, nsresult> {
+ QM_TRY_INSPECT(
+ const auto& connection,
+ CreateStorageConnection(*file, *usageFile,
+ aOriginMetadata.mOrigin, [] {}));
+
+ QM_TRY_INSPECT(const int64_t& usage,
+ GetUsage(*connection,
+ /* aArchivedOriginScope */ nullptr));
+
+ QM_TRY(MOZ_TO_RESULT(
+ UpdateUsageFile(usageFile, usageJournalFile, usage)));
+
+ QM_TRY(MOZ_TO_RESULT(usageJournalFile->Remove(false)));
+
+ MOZ_ASSERT(usage >= 0);
+ return UsageInfo{DatabaseUsageType(Some(uint64_t(usage)))};
+ })));
+ }
+
+ if (usageFileExists) {
+ QM_TRY(MOZ_TO_RESULT(usageFile->Remove(false)));
+ }
+
+ return UsageInfo{};
+ }()));
+
+ // Report unknown files in debug builds, but don't fail, just warn (we don't
+ // report unknown files in release builds because that requires extra
+ // scanning of the directory which would slow down entire initialization for
+ // little benefit).
+
+#ifdef DEBUG
+ QM_TRY(CollectEachFileAtomicCancelable(
+ *directory, aCanceled,
+ [](const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> {
+ QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file));
+
+ switch (dirEntryKind) {
+ case nsIFileKind::ExistsAsDirectory:
+ Unused << WARN_IF_FILE_IS_UNKNOWN(*file);
+ break;
+
+ case nsIFileKind::ExistsAsFile: {
+ QM_TRY_INSPECT(
+ const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, file, GetLeafName));
+
+ if (leafName.Equals(kDataFileName) ||
+ leafName.Equals(kJournalFileName) ||
+ leafName.Equals(kUsageFileName) ||
+ leafName.Equals(kUsageJournalFileName)) {
+ return Ok{};
+ }
+
+ Unused << WARN_IF_FILE_IS_UNKNOWN(*file);
+
+ break;
+ }
+
+ case nsIFileKind::DoesNotExist:
+ // Ignore files that got removed externally while iterating.
+ break;
+ }
+ return Ok{};
+ }));
+#endif
+
+ return res;
+}
+
+nsresult QuotaClient::InitOriginWithoutTracking(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) {
+ AssertIsOnIOThread();
+
+ // This is called when a storage/permanent/${origin}/ls directory exists. Even
+ // though this shouldn't happen with a "good" profile, we shouldn't return an
+ // error here, since that would cause origin initialization to fail. We just
+ // warn and otherwise ignore that.
+ UNKNOWN_FILE_WARNING(NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME));
+ return NS_OK;
+}
+
+Result<UsageInfo, nsresult> QuotaClient::GetUsageForOrigin(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT);
+
+ // We can't open the database at this point, since it can be already used
+ // by the connection thread. Use the cached value instead.
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ return quotaManager->GetUsageForClient(PERSISTENCE_TYPE_DEFAULT,
+ aOriginMetadata, Client::LS);
+}
+
+nsresult QuotaClient::AboutToClearOrigins(
+ const Nullable<PersistenceType>& aPersistenceType,
+ const OriginScope& aOriginScope) {
+ AssertIsOnIOThread();
+
+ // This method is not called when the clearing is triggered by the eviction
+ // process. It's on purpose to avoid a problem with the origin access time
+ // which can be described as follows:
+ // When there's a storage pressure condition and quota manager starts
+ // collecting origins for eviction, there can be an origin that hasn't been
+ // touched for long time. However, the old implementation of local storage
+ // could have touched the origin only recently and the new implementation
+ // hasn't had a chance to create a new per origin database for it yet (the
+ // data is still in the archive database), so the origin access time hasn't
+ // been updated either. In the end, the origin would be evicted despite the
+ // fact that there was recent local storage activity.
+ // So this method clears the archived data and shadow database entries for
+ // given origin scope, but only if it's a privacy-related origin clearing.
+
+ if (!aPersistenceType.IsNull() &&
+ aPersistenceType.Value() != PERSISTENCE_TYPE_DEFAULT) {
+ return NS_OK;
+ }
+
+ // There can be no data for the system principal in the archive or the shadow
+ // database. This early return silences potential warnings caused by failed
+ // `CreateAerchivedOriginScope` because it calls `GenerateOriginKey2` which
+ // doesn't support the system principal.
+ if (aOriginScope.IsOrigin() &&
+ aOriginScope.GetOrigin() == QuotaManager::GetOriginForChrome()) {
+ return NS_OK;
+ }
+
+ const bool shadowWrites = gShadowWrites;
+
+ QM_TRY_INSPECT(const auto& archivedOriginScope,
+ CreateArchivedOriginScope(aOriginScope));
+
+ if (!gArchivedOrigins) {
+ QM_TRY(MOZ_TO_RESULT(LoadArchivedOrigins()));
+ MOZ_ASSERT(gArchivedOrigins);
+ }
+
+ const bool hasDataForRemoval =
+ archivedOriginScope->HasMatches(gArchivedOrigins);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ const nsString& basePath = quotaManager->GetBasePath();
+
+ {
+ MutexAutoLock shadowDatabaseLock(mShadowDatabaseMutex);
+
+ QM_TRY_INSPECT(
+ const auto& connection,
+ ([&basePath]() -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
+ if (gInitializedShadowStorage) {
+ QM_TRY_RETURN(GetShadowStorageConnection(basePath));
+ }
+
+ QM_TRY_UNWRAP(auto connection,
+ CreateShadowStorageConnection(basePath));
+
+ gInitializedShadowStorage = true;
+
+ return connection;
+ }()));
+
+ {
+ Maybe<AutoDatabaseAttacher> maybeAutoArchiveDatabaseAttacher;
+
+ if (hasDataForRemoval) {
+ QM_TRY_INSPECT(const auto& archiveFile,
+ GetArchiveFile(quotaManager->GetStoragePath()));
+
+ maybeAutoArchiveDatabaseAttacher.emplace(
+ AutoDatabaseAttacher(connection, archiveFile, "archive"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(maybeAutoArchiveDatabaseAttacher->Attach()));
+ }
+
+ if (archivedOriginScope->IsPattern()) {
+ nsCOMPtr<mozIStorageFunction> function(
+ new MatchFunction(archivedOriginScope->GetPattern()));
+
+ QM_TRY(
+ MOZ_TO_RESULT(connection->CreateFunction("match"_ns, 2, function)));
+ }
+
+ {
+ QM_TRY_INSPECT(const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection,
+ CreateStatement, "BEGIN IMMEDIATE;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+ }
+
+ if (shadowWrites) {
+ QM_TRY(MOZ_TO_RESULT(
+ PerformDelete(connection, "main"_ns, archivedOriginScope.get())));
+ }
+
+ if (hasDataForRemoval) {
+ QM_TRY(MOZ_TO_RESULT(PerformDelete(connection, "archive"_ns,
+ archivedOriginScope.get())));
+ }
+
+ {
+ QM_TRY_INSPECT(const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection,
+ CreateStatement, "COMMIT;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+ }
+
+ if (archivedOriginScope->IsPattern()) {
+ QM_TRY(MOZ_TO_RESULT(connection->RemoveFunction("match"_ns)));
+ }
+
+ if (hasDataForRemoval) {
+ MOZ_ASSERT(maybeAutoArchiveDatabaseAttacher.isSome());
+ QM_TRY(MOZ_TO_RESULT(maybeAutoArchiveDatabaseAttacher->Detach()));
+
+ maybeAutoArchiveDatabaseAttacher.reset();
+
+ MOZ_ASSERT(gArchivedOrigins);
+ MOZ_ASSERT(archivedOriginScope->HasMatches(gArchivedOrigins));
+ archivedOriginScope->RemoveMatches(gArchivedOrigins);
+ }
+ }
+ QM_TRY(MOZ_TO_RESULT(connection->Close()));
+ }
+
+ if (aOriginScope.IsNull()) {
+ QM_TRY_INSPECT(const auto& shadowFile, GetShadowFile(basePath));
+
+ QM_TRY(MOZ_TO_RESULT(shadowFile->Remove(false)));
+
+ gInitializedShadowStorage = false;
+ }
+
+ return NS_OK;
+}
+
+void QuotaClient::OnOriginClearCompleted(PersistenceType aPersistenceType,
+ const nsACString& aOrigin) {
+ AssertIsOnIOThread();
+}
+
+void QuotaClient::ReleaseIOThreadObjects() {
+ AssertIsOnIOThread();
+
+ gInitializationInfo = nullptr;
+
+ // Delete archived origins hashtable since QuotaManager clears the whole
+ // storage directory including ls-archive.sqlite.
+
+ gArchivedOrigins = nullptr;
+}
+
+void QuotaClient::AbortOperationsForLocks(
+ const DirectoryLockIdTable& aDirectoryLockIds) {
+ AssertIsOnBackgroundThread();
+
+ // A PrepareDatastoreOp object could already acquire a directory lock for
+ // the given origin. Its last step is creation of a Datastore object (which
+ // will take ownership of the directory lock) and a PreparedDatastore object
+ // which keeps the Datastore alive until a database actor is created.
+ // We need to invalidate the PreparedDatastore object when it's created,
+ // otherwise the Datastore object can block the origin clear operation for
+ // long time. It's not a problem that we don't fail the PrepareDatastoreOp
+ // immediatelly (avoiding the creation of the Datastore and PreparedDatastore
+ // object). We will call RequestAllowToClose on the database actor once it's
+ // created and the child actor will respond by sending AllowToClose which
+ // will close the Datastore on the parent side (the closing releases the
+ // directory lock).
+
+ InvalidatePrepareDatastoreOpsMatching(
+ [&aDirectoryLockIds](const auto& prepareDatastoreOp) {
+ // Check if the PrepareDatastoreOp holds an acquired DirectoryLock.
+ // Origin clearing can't be blocked by this PrepareDatastoreOp if there
+ // is no acquired DirectoryLock. If there is an acquired DirectoryLock,
+ // check if the table contains the lock for the PrepareDatastoreOp.
+ return IsLockForObjectAcquiredAndContainedInLockTable(
+ prepareDatastoreOp, aDirectoryLockIds);
+ });
+
+ if (gPrivateDatastores) {
+ gPrivateDatastores->RemoveIf([&aDirectoryLockIds](const auto& iter) {
+ const auto& privateDatastore = iter.Data();
+
+ // The PrivateDatastore::mDatastore member is not cleared until the
+ // PrivateDatastore is destroyed.
+ const auto& datastore = privateDatastore->DatastoreRef();
+
+ // If the PrivateDatastore exists then it must be registered in
+ // Datastore::mHasLivePrivateDatastore as well. The Datastore must have
+ // a DirectoryLock if there is a registered PrivateDatastore.
+ return IsLockForObjectContainedInLockTable(datastore, aDirectoryLockIds);
+ });
+
+ if (!gPrivateDatastores->Count()) {
+ gPrivateDatastores = nullptr;
+ }
+ }
+
+ InvalidatePreparedDatastoresMatching([&aDirectoryLockIds](
+ const auto& preparedDatastore) {
+ // The PreparedDatastore::mDatastore member is not cleared until the
+ // PreparedDatastore is destroyed.
+ const auto& datastore = preparedDatastore.DatastoreRef();
+
+ // If the PreparedDatastore exists then it must be registered in
+ // Datastore::mPreparedDatastores as well. The Datastore must have a
+ // DirectoryLock if there are registered PreparedDatastore objects.
+ return IsLockForObjectContainedInLockTable(datastore, aDirectoryLockIds);
+ });
+
+ RequestAllowToCloseDatabasesMatching(
+ [&aDirectoryLockIds](const auto& database) {
+ const auto& maybeDatastore = database.MaybeDatastoreRef();
+
+ // If the Database is registered in gLiveDatabases then it must have a
+ // Datastore.
+ MOZ_ASSERT(maybeDatastore.isSome());
+
+ // If the Database is registered in gLiveDatabases then it must be
+ // registered in Datastore::mDatabases as well. The Datastore must have
+ // a DirectoryLock if there are registered Database objects.
+ return IsLockForObjectContainedInLockTable(*maybeDatastore,
+ aDirectoryLockIds);
+ });
+}
+
+void QuotaClient::AbortOperationsForProcess(ContentParentId aContentParentId) {
+ AssertIsOnBackgroundThread();
+
+ RequestAllowToCloseDatabasesMatching(
+ [&aContentParentId](const auto& database) {
+ return database.IsOwnedByProcess(aContentParentId);
+ });
+}
+
+void QuotaClient::AbortAllOperations() {
+ AssertIsOnBackgroundThread();
+
+ InvalidatePrepareDatastoreOpsMatching([](const auto& prepareDatastoreOp) {
+ return prepareDatastoreOp.MaybeDirectoryLockRef();
+ });
+
+ if (gPrivateDatastores) {
+ gPrivateDatastores = nullptr;
+ }
+
+ InvalidatePreparedDatastoresMatching([](const auto&) { return true; });
+
+ RequestAllowToCloseDatabasesMatching([](const auto&) { return true; });
+}
+
+void QuotaClient::StartIdleMaintenance() { AssertIsOnBackgroundThread(); }
+
+void QuotaClient::StopIdleMaintenance() { AssertIsOnBackgroundThread(); }
+
+void QuotaClient::InitiateShutdown() {
+ // gPrepareDatastoreOps are short lived objects running a state machine.
+ // The shutdown flag is checked between states, so we don't have to notify
+ // all the objects here.
+ // Allocation of a new PrepareDatastoreOp object is prevented once the
+ // shutdown flag is set.
+ // When the last PrepareDatastoreOp finishes, the gPrepareDatastoreOps array
+ // is destroyed.
+
+ if (gPreparedDatastores) {
+ gPreparedDatastores = nullptr;
+ }
+
+ if (gPrivateDatastores) {
+ gPrivateDatastores = nullptr;
+ }
+
+ RequestAllowToCloseDatabasesMatching([](const auto&) { return true; });
+
+ if (gPreparedObsevers) {
+ gPreparedObsevers = nullptr;
+ }
+}
+
+bool QuotaClient::IsShutdownCompleted() const {
+ // Don't have to check gPrivateDatastores and gPreparedDatastores since we
+ // nulled it out in InitiateShutdown.
+ return !gPrepareDatastoreOps && !gDatastores && !gLiveDatabases;
+}
+
+void QuotaClient::ForceKillActors() { ForceKillAllDatabases(); }
+
+nsCString QuotaClient::GetShutdownStatus() const {
+ AssertIsOnBackgroundThread();
+
+ nsCString data;
+
+ if (gPrepareDatastoreOps) {
+ data.Append("PrepareDatastoreOperations: ");
+ data.AppendInt(static_cast<uint32_t>(gPrepareDatastoreOps->Length()));
+ data.Append(" (");
+
+ // XXX What's the purpose of adding these to a hashtable before joining them
+ // to the string? (Maybe this used to be an ordered container before???)
+ nsTHashSet<nsCString> ids;
+ std::transform(gPrepareDatastoreOps->cbegin(), gPrepareDatastoreOps->cend(),
+ MakeInserter(ids), [](const auto& prepareDatastoreOp) {
+ nsCString id;
+ prepareDatastoreOp->Stringify(id);
+ return id;
+ });
+
+ StringJoinAppend(data, ", "_ns, ids);
+
+ data.Append(")\n");
+ }
+
+ if (gDatastores) {
+ data.Append("Datastores: ");
+ data.AppendInt(gDatastores->Count());
+ data.Append(" (");
+
+ // XXX It might be confusing to remove duplicates here, as the actual list
+ // won't match the count then.
+ nsTHashSet<nsCString> ids;
+ std::transform(gDatastores->Values().cbegin(), gDatastores->Values().cend(),
+ MakeInserter(ids), [](const auto& entry) {
+ nsCString id;
+ entry->Stringify(id);
+ return id;
+ });
+
+ StringJoinAppend(data, ", "_ns, ids);
+
+ data.Append(")\n");
+ }
+
+ if (gLiveDatabases) {
+ data.Append("LiveDatabases: ");
+ data.AppendInt(static_cast<uint32_t>(gLiveDatabases->Length()));
+ data.Append(" (");
+
+ // XXX It might be confusing to remove duplicates here, as the actual list
+ // won't match the count then.
+ nsTHashSet<nsCString> ids;
+ std::transform(gLiveDatabases->cbegin(), gLiveDatabases->cend(),
+ MakeInserter(ids), [](const auto& database) {
+ nsCString id;
+ database->Stringify(id);
+ return id;
+ });
+
+ StringJoinAppend(data, ", "_ns, ids);
+
+ data.Append(")\n");
+ }
+
+ return data;
+}
+
+void QuotaClient::FinalizeShutdown() {
+ // And finally, shutdown the connection thread.
+ if (gConnectionThread) {
+ gConnectionThread->Shutdown();
+
+ gConnectionThread = nullptr;
+ }
+}
+
+Result<UniquePtr<ArchivedOriginScope>, nsresult>
+QuotaClient::CreateArchivedOriginScope(const OriginScope& aOriginScope) {
+ AssertIsOnIOThread();
+
+ if (aOriginScope.IsOrigin()) {
+ QM_TRY_INSPECT(const auto& principalInfo,
+ QuotaManager::ParseOrigin(aOriginScope.GetOrigin()));
+
+ QM_TRY_INSPECT((const auto& [originAttrSuffix, originKey]),
+ GenerateOriginKey2(principalInfo));
+
+ return ArchivedOriginScope::CreateFromOrigin(originAttrSuffix, originKey);
+ }
+
+ if (aOriginScope.IsPrefix()) {
+ QM_TRY_INSPECT(const auto& principalInfo,
+ QuotaManager::ParseOrigin(aOriginScope.GetOriginNoSuffix()));
+
+ QM_TRY_INSPECT((const auto& [originAttrSuffix, originKey]),
+ GenerateOriginKey2(principalInfo));
+
+ Unused << originAttrSuffix;
+
+ return ArchivedOriginScope::CreateFromPrefix(originKey);
+ }
+
+ if (aOriginScope.IsPattern()) {
+ return ArchivedOriginScope::CreateFromPattern(aOriginScope.GetPattern());
+ }
+
+ MOZ_ASSERT(aOriginScope.IsNull());
+
+ return ArchivedOriginScope::CreateFromNull();
+}
+
+nsresult QuotaClient::PerformDelete(
+ mozIStorageConnection* aConnection, const nsACString& aSchemaName,
+ ArchivedOriginScope* aArchivedOriginScope) const {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+ MOZ_ASSERT(aArchivedOriginScope);
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement,
+ "DELETE FROM "_ns + aSchemaName + ".webappsstore2"_ns +
+ aArchivedOriginScope->GetBindingClause() + ";"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(aArchivedOriginScope->BindToStatement(stmt)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+
+ return NS_OK;
+}
+
+// static
+nsresult QuotaClient::Observer::Initialize() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<Observer> observer = new Observer();
+
+ QM_TRY(MOZ_TO_RESULT(observer->Init()));
+
+ return NS_OK;
+}
+
+nsresult QuotaClient::Observer::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (NS_WARN_IF(!obs)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = obs->AddObserver(this, kPrivateBrowsingObserverTopic, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult QuotaClient::Observer::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (NS_WARN_IF(!obs)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, kPrivateBrowsingObserverTopic));
+ MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID));
+
+ // In general, the instance will have died after the latter removal call, so
+ // it's not safe to do anything after that point.
+ // However, Shutdown is currently called from Observe which is called by the
+ // Observer Service which holds a strong reference to the observer while the
+ // Observe method is being called.
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(QuotaClient::Observer, nsIObserver)
+
+NS_IMETHODIMP
+QuotaClient::Observer::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!strcmp(aTopic, kPrivateBrowsingObserverTopic)) {
+ PBackgroundChild* const backgroundActor =
+ BackgroundChild::GetOrCreateForCurrentThread();
+ QM_TRY(OkIf(backgroundActor), NS_ERROR_FAILURE);
+
+ QM_TRY(OkIf(backgroundActor->SendLSClearPrivateBrowsing()),
+ NS_ERROR_FAILURE);
+
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ QM_TRY(MOZ_TO_RESULT(Shutdown()));
+
+ return NS_OK;
+ }
+
+ NS_WARNING("Unknown observer topic!");
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(QuotaClient::MatchFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+QuotaClient::MatchFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aFunctionArguments);
+ MOZ_ASSERT(aResult);
+
+ QM_TRY_INSPECT(const auto& suffix,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsAutoCString, aFunctionArguments, GetUTF8String, 1));
+
+ OriginAttributes oa;
+ QM_TRY(OkIf(oa.PopulateFromSuffix(suffix)), NS_ERROR_FAILURE);
+
+ const bool result = mPattern.Matches(oa);
+
+ RefPtr<nsVariant> outVar(new nsVariant());
+ QM_TRY(MOZ_TO_RESULT(outVar->SetAsBool(result)));
+
+ outVar.forget(aResult);
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * AutoWriteTransaction
+ ******************************************************************************/
+
+AutoWriteTransaction::AutoWriteTransaction(bool aShadowWrites)
+ : mConnection(nullptr), mShadowWrites(aShadowWrites) {
+ AssertIsOnGlobalConnectionThread();
+
+ MOZ_COUNT_CTOR(mozilla::dom::AutoWriteTransaction);
+}
+
+AutoWriteTransaction::~AutoWriteTransaction() {
+ AssertIsOnGlobalConnectionThread();
+
+ MOZ_COUNT_DTOR(mozilla::dom::AutoWriteTransaction);
+
+ if (mConnection) {
+ QM_WARNONLY_TRY(QM_TO_RESULT(mConnection->RollbackWriteTransaction()));
+
+ if (mShadowWrites) {
+ QM_WARNONLY_TRY(QM_TO_RESULT(DetachShadowDatabaseAndUnlock()));
+ }
+ }
+}
+
+nsresult AutoWriteTransaction::Start(Connection* aConnection) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+ MOZ_ASSERT(!mConnection);
+
+ if (mShadowWrites) {
+ QM_TRY(MOZ_TO_RESULT(LockAndAttachShadowDatabase(aConnection)));
+ }
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->BeginWriteTransaction()));
+
+ mConnection = aConnection;
+
+ return NS_OK;
+}
+
+nsresult AutoWriteTransaction::Commit() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(mConnection);
+
+ QM_TRY(MOZ_TO_RESULT(mConnection->CommitWriteTransaction()));
+
+ if (mShadowWrites) {
+ QM_TRY(MOZ_TO_RESULT(DetachShadowDatabaseAndUnlock()));
+ }
+
+ mConnection = nullptr;
+
+ return NS_OK;
+}
+
+nsresult AutoWriteTransaction::LockAndAttachShadowDatabase(
+ Connection* aConnection) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+ MOZ_ASSERT(!mConnection);
+ MOZ_ASSERT(mShadowDatabaseLock.isNothing());
+ MOZ_ASSERT(mShadowWrites);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ mShadowDatabaseLock.emplace(
+ aConnection->GetQuotaClient()->ShadowDatabaseMutex());
+
+ QM_TRY(MOZ_TO_RESULT(AttachShadowDatabase(
+ quotaManager->GetBasePath(), &aConnection->MutableStorageConnection())));
+
+ return NS_OK;
+}
+
+nsresult AutoWriteTransaction::DetachShadowDatabaseAndUnlock() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(mShadowDatabaseLock.isSome());
+ MOZ_ASSERT(mShadowWrites);
+
+ nsCOMPtr<mozIStorageConnection> storageConnection =
+ mConnection->StorageConnection();
+ MOZ_ASSERT(storageConnection);
+
+ QM_TRY(MOZ_TO_RESULT(DetachShadowDatabase(storageConnection)));
+
+ mShadowDatabaseLock.reset();
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/localstorage/ActorsParent.h b/dom/localstorage/ActorsParent.h
new file mode 100644
index 0000000000..eddba8ac98
--- /dev/null
+++ b/dom/localstorage/ActorsParent.h
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_ActorsParent_h
+#define mozilla_dom_localstorage_ActorsParent_h
+
+#include <cstdint>
+#include "mozilla/AlreadyAddRefed.h"
+
+namespace mozilla {
+
+namespace ipc {
+
+class PBackgroundParent;
+class PrincipalInfo;
+
+} // namespace ipc
+
+namespace dom {
+
+class LSRequestParams;
+class LSSimpleRequestParams;
+class PBackgroundLSDatabaseParent;
+class PBackgroundLSObserverParent;
+class PBackgroundLSRequestParent;
+class PBackgroundLSSimpleRequestParent;
+
+namespace quota {
+
+class Client;
+
+} // namespace quota
+
+void InitializeLocalStorage();
+
+PBackgroundLSDatabaseParent* AllocPBackgroundLSDatabaseParent(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
+ const uint32_t& aPrivateBrowsingId, const uint64_t& aDatastoreId);
+
+bool RecvPBackgroundLSDatabaseConstructor(
+ PBackgroundLSDatabaseParent* aActor,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
+ const uint32_t& aPrivateBrowsingId, const uint64_t& aDatastoreId);
+
+bool DeallocPBackgroundLSDatabaseParent(PBackgroundLSDatabaseParent* aActor);
+
+PBackgroundLSObserverParent* AllocPBackgroundLSObserverParent(
+ const uint64_t& aObserverId);
+
+bool RecvPBackgroundLSObserverConstructor(PBackgroundLSObserverParent* aActor,
+ const uint64_t& aObservereId);
+
+bool DeallocPBackgroundLSObserverParent(PBackgroundLSObserverParent* aActor);
+
+PBackgroundLSRequestParent* AllocPBackgroundLSRequestParent(
+ mozilla::ipc::PBackgroundParent* aBackgroundActor,
+ const LSRequestParams& aParams);
+
+bool RecvPBackgroundLSRequestConstructor(PBackgroundLSRequestParent* aActor,
+ const LSRequestParams& aParams);
+
+bool DeallocPBackgroundLSRequestParent(PBackgroundLSRequestParent* aActor);
+
+PBackgroundLSSimpleRequestParent* AllocPBackgroundLSSimpleRequestParent(
+ mozilla::ipc::PBackgroundParent* aBackgroundActor,
+ const LSSimpleRequestParams& aParams);
+
+bool RecvPBackgroundLSSimpleRequestConstructor(
+ PBackgroundLSSimpleRequestParent* aActor,
+ const LSSimpleRequestParams& aParams);
+
+bool DeallocPBackgroundLSSimpleRequestParent(
+ PBackgroundLSSimpleRequestParent* aActor);
+
+bool RecvLSClearPrivateBrowsing();
+
+namespace localstorage {
+
+already_AddRefed<mozilla::dom::quota::Client> CreateQuotaClient();
+
+} // namespace localstorage
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_ActorsParent_h
diff --git a/dom/localstorage/LSDatabase.cpp b/dom/localstorage/LSDatabase.cpp
new file mode 100644
index 0000000000..2dccdaad26
--- /dev/null
+++ b/dom/localstorage/LSDatabase.cpp
@@ -0,0 +1,448 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "LSDatabase.h"
+
+// Local includes
+#include "ActorsChild.h"
+#include "LSObject.h"
+#include "LSSnapshot.h"
+
+// Global includes
+#include <cstring>
+#include <new>
+#include <utility>
+#include "MainThreadUtils.h"
+#include "mozilla/MacroForEach.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/dom/PBackgroundLSDatabase.h"
+#include "nsBaseHashtable.h"
+#include "nsCOMPtr.h"
+#include "nsTHashMap.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsHashKeys.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nscore.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+#define XPCOM_SHUTDOWN_OBSERVER_TOPIC "xpcom-shutdown"
+
+using LSDatabaseHashtable = nsTHashMap<nsCStringHashKey, LSDatabase*>;
+
+StaticAutoPtr<LSDatabaseHashtable> gLSDatabases;
+
+} // namespace
+
+StaticRefPtr<LSDatabase::Observer> LSDatabase::sObserver;
+
+class LSDatabase::Observer final : public nsIObserver {
+ bool mInvalidated;
+
+ public:
+ Observer() : mInvalidated(false) { MOZ_ASSERT(NS_IsMainThread()); }
+
+ void Invalidate() { mInvalidated = true; }
+
+ private:
+ ~Observer() { MOZ_ASSERT(NS_IsMainThread()); }
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+};
+
+LSDatabase::LSDatabase(const nsACString& aOrigin)
+ : mActor(nullptr),
+ mSnapshot(nullptr),
+ mOrigin(aOrigin),
+ mAllowedToClose(false),
+ mRequestedAllowToClose(false) {
+ AssertIsOnOwningThread();
+
+ if (!gLSDatabases) {
+ gLSDatabases = new LSDatabaseHashtable();
+
+ MOZ_ASSERT(!sObserver);
+
+ sObserver = new Observer();
+
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsSvc);
+
+ MOZ_ALWAYS_SUCCEEDS(
+ obsSvc->AddObserver(sObserver, XPCOM_SHUTDOWN_OBSERVER_TOPIC, false));
+ }
+
+ MOZ_ASSERT(!gLSDatabases->Contains(mOrigin));
+ gLSDatabases->InsertOrUpdate(mOrigin, this);
+}
+
+LSDatabase::~LSDatabase() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mSnapshot);
+
+ if (!mAllowedToClose) {
+ AllowToClose();
+ }
+
+ if (mActor) {
+ mActor->SendDeleteMeInternal();
+ MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!");
+ }
+}
+
+// static
+LSDatabase* LSDatabase::Get(const nsACString& aOrigin) {
+ return gLSDatabases ? gLSDatabases->Get(aOrigin) : nullptr;
+}
+
+void LSDatabase::SetActor(LSDatabaseChild* aActor) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(!mActor);
+
+ mActor = aActor;
+}
+
+void LSDatabase::RequestAllowToClose() {
+ AssertIsOnOwningThread();
+
+ if (mRequestedAllowToClose) {
+ return;
+ }
+
+ mRequestedAllowToClose = true;
+
+ if (mSnapshot) {
+ mSnapshot->MarkDirty();
+ } else {
+ AllowToClose();
+ }
+}
+
+void LSDatabase::NoteFinishedSnapshot(LSSnapshot* aSnapshot) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aSnapshot == mSnapshot);
+
+ mSnapshot = nullptr;
+
+ if (mRequestedAllowToClose) {
+ AllowToClose();
+ }
+}
+
+// All these methods assert `!mAllowedToClose` because they shoudn't be called
+// if the database is being closed. Callers should first check the state by
+// calling `IsAlloweToClose` and eventually obtain a new database.
+
+nsresult LSDatabase::GetLength(LSObject* aObject, uint32_t* aResult) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aObject);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ nsresult rv = EnsureSnapshot(aObject, VoidString());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mSnapshot->GetLength(aResult);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSDatabase::GetKey(LSObject* aObject, uint32_t aIndex,
+ nsAString& aResult) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aObject);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ nsresult rv = EnsureSnapshot(aObject, VoidString());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mSnapshot->GetKey(aIndex, aResult);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSDatabase::GetItem(LSObject* aObject, const nsAString& aKey,
+ nsAString& aResult) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aObject);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ nsresult rv = EnsureSnapshot(aObject, aKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mSnapshot->GetItem(aKey, aResult);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSDatabase::GetKeys(LSObject* aObject, nsTArray<nsString>& aKeys) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aObject);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ nsresult rv = EnsureSnapshot(aObject, VoidString());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mSnapshot->GetKeys(aKeys);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSDatabase::SetItem(LSObject* aObject, const nsAString& aKey,
+ const nsAString& aValue,
+ LSNotifyInfo& aNotifyInfo) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aObject);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ nsresult rv = EnsureSnapshot(aObject, aKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mSnapshot->SetItem(aKey, aValue, aNotifyInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSDatabase::RemoveItem(LSObject* aObject, const nsAString& aKey,
+ LSNotifyInfo& aNotifyInfo) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aObject);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ nsresult rv = EnsureSnapshot(aObject, aKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mSnapshot->RemoveItem(aKey, aNotifyInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSDatabase::Clear(LSObject* aObject, LSNotifyInfo& aNotifyInfo) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aObject);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ nsresult rv = EnsureSnapshot(aObject, VoidString());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mSnapshot->Clear(aNotifyInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSDatabase::BeginExplicitSnapshot(LSObject* aObject) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aObject);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+ MOZ_ASSERT(!mSnapshot);
+
+ nsresult rv = EnsureSnapshot(aObject, VoidString(), /* aExplicit */ true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSDatabase::CheckpointExplicitSnapshot() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+ MOZ_ASSERT(mSnapshot);
+ MOZ_ASSERT(mSnapshot->Explicit());
+
+ nsresult rv = mSnapshot->ExplicitCheckpoint();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSDatabase::EndExplicitSnapshot() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+ MOZ_ASSERT(mSnapshot);
+ MOZ_ASSERT(mSnapshot->Explicit());
+
+ nsresult rv = mSnapshot->ExplicitEnd();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+bool LSDatabase::HasSnapshot() const {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ return !!mSnapshot;
+}
+
+int64_t LSDatabase::GetSnapshotUsage() const {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+ MOZ_ASSERT(mSnapshot);
+
+ return mSnapshot->GetUsage();
+}
+
+nsresult LSDatabase::EnsureSnapshot(LSObject* aObject, const nsAString& aKey,
+ bool aExplicit) {
+ MOZ_ASSERT(aObject);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT_IF(mSnapshot, !aExplicit);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ if (mSnapshot) {
+ return NS_OK;
+ }
+
+ RefPtr<LSSnapshot> snapshot = new LSSnapshot(this);
+
+ LSSnapshotChild* actor = new LSSnapshotChild(snapshot);
+
+ LSSnapshotInitInfo initInfo;
+ bool ok = mActor->SendPBackgroundLSSnapshotConstructor(
+ actor, aObject->DocumentURI(), nsString(aKey),
+ /* increasePeakUsage */ true,
+ /* minSize */ 0, &initInfo);
+ if (NS_WARN_IF(!ok)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ snapshot->SetActor(actor);
+
+ // This add refs snapshot.
+ nsresult rv = snapshot->Init(aKey, initInfo, aExplicit);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // This is cleared in LSSnapshot::Run() before the snapshot is destroyed.
+ mSnapshot = snapshot;
+
+ return NS_OK;
+}
+
+void LSDatabase::AllowToClose() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mAllowedToClose);
+ MOZ_ASSERT(!mSnapshot);
+
+ mAllowedToClose = true;
+
+ if (mActor) {
+ mActor->SendAllowToClose();
+ }
+
+ MOZ_ASSERT(gLSDatabases);
+ MOZ_ASSERT(gLSDatabases->Get(mOrigin));
+ gLSDatabases->Remove(mOrigin);
+
+ if (!gLSDatabases->Count()) {
+ gLSDatabases = nullptr;
+
+ MOZ_ASSERT(sObserver);
+
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsSvc);
+
+ MOZ_ALWAYS_SUCCEEDS(
+ obsSvc->RemoveObserver(sObserver, XPCOM_SHUTDOWN_OBSERVER_TOPIC));
+
+ // We also need to invalidate the observer because AllowToClose can be
+ // triggered by an indirectly related observer, so the observer service
+ // may still keep our observer alive and call Observe on it. This is
+ // possible because observer service snapshots the observer list for given
+ // subject before looping over the list.
+ sObserver->Invalidate();
+
+ sObserver = nullptr;
+ }
+}
+
+NS_IMPL_ISUPPORTS(LSDatabase::Observer, nsIObserver)
+
+NS_IMETHODIMP
+LSDatabase::Observer::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aTopic, XPCOM_SHUTDOWN_OBSERVER_TOPIC));
+
+ if (mInvalidated) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(gLSDatabases);
+
+ for (const RefPtr<LSDatabase>& database :
+ ToTArray<nsTArray<RefPtr<LSDatabase>>>(gLSDatabases->Values())) {
+ database->RequestAllowToClose();
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/localstorage/LSDatabase.h b/dom/localstorage/LSDatabase.h
new file mode 100644
index 0000000000..b85798eeac
--- /dev/null
+++ b/dom/localstorage/LSDatabase.h
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_LSDatabase_h
+#define mozilla_dom_localstorage_LSDatabase_h
+
+#include <cstdint>
+#include "ErrorList.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/StaticPtr.h"
+#include "nsISupports.h"
+#include "nsString.h"
+#include "nsTArrayForwardDeclare.h"
+
+namespace mozilla::dom {
+
+class LSDatabaseChild;
+class LSNotifyInfo;
+class LSObject;
+class LSSnapshot;
+
+class LSDatabase final {
+ class Observer;
+
+ LSDatabaseChild* mActor;
+
+ LSSnapshot* mSnapshot;
+
+ const nsCString mOrigin;
+
+ bool mAllowedToClose;
+ bool mRequestedAllowToClose;
+
+ static StaticRefPtr<Observer> sObserver;
+
+ public:
+ explicit LSDatabase(const nsACString& aOrigin);
+
+ static LSDatabase* Get(const nsACString& aOrigin);
+
+ NS_INLINE_DECL_REFCOUNTING(LSDatabase)
+
+ void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(LSDatabase); }
+
+ void SetActor(LSDatabaseChild* aActor);
+
+ void ClearActor() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+
+ mActor = nullptr;
+ }
+
+ bool IsAllowedToClose() const {
+ AssertIsOnOwningThread();
+
+ return mAllowedToClose;
+ }
+
+ void RequestAllowToClose();
+
+ void NoteFinishedSnapshot(LSSnapshot* aSnapshot);
+
+ nsresult GetLength(LSObject* aObject, uint32_t* aResult);
+
+ nsresult GetKey(LSObject* aObject, uint32_t aIndex, nsAString& aResult);
+
+ nsresult GetItem(LSObject* aObject, const nsAString& aKey,
+ nsAString& aResult);
+
+ nsresult GetKeys(LSObject* aObject, nsTArray<nsString>& aKeys);
+
+ nsresult SetItem(LSObject* aObject, const nsAString& aKey,
+ const nsAString& aValue, LSNotifyInfo& aNotifyInfo);
+
+ nsresult RemoveItem(LSObject* aObject, const nsAString& aKey,
+ LSNotifyInfo& aNotifyInfo);
+
+ nsresult Clear(LSObject* aObject, LSNotifyInfo& aNotifyInfo);
+
+ nsresult BeginExplicitSnapshot(LSObject* aObject);
+
+ nsresult CheckpointExplicitSnapshot();
+
+ nsresult EndExplicitSnapshot();
+
+ bool HasSnapshot() const;
+
+ int64_t GetSnapshotUsage() const;
+
+ private:
+ ~LSDatabase();
+
+ nsresult EnsureSnapshot(LSObject* aObject, const nsAString& aKey,
+ bool aExplicit = false);
+
+ void AllowToClose();
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_localstorage_LSDatabase_h
diff --git a/dom/localstorage/LSInitializationTypes.h b/dom/localstorage/LSInitializationTypes.h
new file mode 100644
index 0000000000..63090066da
--- /dev/null
+++ b/dom/localstorage/LSInitializationTypes.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_LOCALSTORAGE_INITIALIZATIONTYPES_H_
+#define DOM_LOCALSTORAGE_INITIALIZATIONTYPES_H_
+
+#include "mozilla/TypedEnumBits.h"
+#include <mozilla/dom/quota/FirstInitializationAttempts.h>
+#include "nsTHashMap.h"
+
+namespace mozilla {
+struct CreateIfNonExistent;
+}
+
+namespace mozilla::dom {
+
+enum class LSOriginInitialization {
+ None = 0,
+ Datastore = 1 << 0,
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(LSOriginInitialization)
+
+using LSOriginInitializationInfo =
+ quota::FirstInitializationAttempts<LSOriginInitialization, Nothing>;
+
+class LSInitializationInfo final {
+ nsTHashMap<nsCStringHashKey, LSOriginInitializationInfo>
+ mOriginInitializationInfos;
+
+ public:
+ LSOriginInitializationInfo& MutableOriginInitializationInfoRef(
+ const nsACString& aOrigin) {
+ return *mOriginInitializationInfos.Lookup(aOrigin);
+ }
+
+ LSOriginInitializationInfo& MutableOriginInitializationInfoRef(
+ const nsACString& aOrigin, const CreateIfNonExistent&) {
+ return mOriginInitializationInfos.LookupOrInsert(aOrigin);
+ }
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_LOCALSTORAGE_INITIALIZATIONTYPES_H_
diff --git a/dom/localstorage/LSObject.cpp b/dom/localstorage/LSObject.cpp
new file mode 100644
index 0000000000..23189d6f98
--- /dev/null
+++ b/dom/localstorage/LSObject.cpp
@@ -0,0 +1,1167 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "LSObject.h"
+
+// Local includes
+#include "ActorsChild.h"
+#include "LSDatabase.h"
+#include "LSObserver.h"
+
+// Global includes
+#include <utility>
+#include "MainThreadUtils.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/MacroForEach.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RemoteLazyInputStreamThread.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StorageAccess.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/ClientInfo.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/LocalStorageCommon.h"
+#include "mozilla/dom/PBackgroundLSRequest.h"
+#include "mozilla/dom/PBackgroundLSSharedTypes.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/ProcessChild.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIEventTarget.h"
+#include "nsIPrincipal.h"
+#include "nsIRunnable.h"
+#include "nsIScriptObjectPrincipal.h"
+#include "nsISerialEventTarget.h"
+#include "nsITimer.h"
+#include "nsPIDOMWindow.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsTStringRepr.h"
+#include "nsThread.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "nscore.h"
+
+/**
+ * Automatically cancel and abort synchronous LocalStorage requests (for example
+ * datastore preparation) if they take this long. We've chosen a value that is
+ * long enough that it is unlikely for the problem to be falsely triggered by
+ * slow system I/O. We've also chosen a value long enough so that automated
+ * tests should time out and fail if LocalStorage hangs. Also, this value is
+ * long enough so that testers can notice the (content process) hang; we want to
+ * know about the hangs, not hide them. On the other hand this value is less
+ * than 60 seconds which is used by nsTerminator to crash a hung main process.
+ */
+#define FAILSAFE_CANCEL_SYNC_OP_MS 50000
+
+/**
+ * Interval with which to wake up while waiting for the sync op to complete to
+ * check ExpectingShutdown().
+ */
+#define SYNC_OP_WAKE_INTERVAL_MS 500
+
+namespace mozilla::dom {
+
+namespace {
+
+class RequestHelper;
+
+/**
+ * Main-thread helper that implements the blocking logic required by
+ * LocalStorage's synchronous semantics. StartAndReturnResponse blocks on a
+ * monitor until a result is received. See StartAndReturnResponse() for info on
+ * this choice.
+ *
+ * The normal life-cycle of this method looks like:
+ * - Main Thread: LSObject::DoRequestSynchronously creates a RequestHelper and
+ * invokes StartAndReturnResponse(). It Dispatches the RequestHelper to the
+ * RemoteLazyInputStream thread, and waits on mMonitor.
+ * - RemoteLazyInputStream Thread: RequestHelper::Run is called, invoking
+ * Start() which invokes LSObject::StartRequest, which gets-or-creates the
+ * PBackground actor if necessary, sends LSRequest constructor which is
+ * provided with a callback reference to the RequestHelper. State advances to
+ * ResponsePending.
+ * - RemoteLazyInputStreamThread: LSRequestChild::Recv__delete__ is received,
+ * which invokes RequestHelepr::OnResponse, advancing the state to Complete
+ * and notifying mMonitor.
+ * - Main Thread: The main thread wakes up after waiting on the monitor,
+ * returning the received response.
+ *
+ * See LocalStorageCommon.h for high-level context and method comments for
+ * low-level details.
+ */
+class RequestHelper final : public Runnable, public LSRequestChildCallback {
+ enum class State {
+ /**
+ * The RequestHelper has been created and dispatched to the
+ * RemoteLazyInputStream Thread.
+ */
+ Initial,
+ /**
+ * Start() has been invoked on the RemoteLazyInputStream Thread and
+ * LSObject::StartRequest has been invoked from there, sending an IPC
+ * message to PBackground to service the request. We stay in this state
+ * until a response is received or a timeout occurs.
+ */
+ ResponsePending,
+ /**
+ * The request timed out, or failed in some fashion, and needs to be
+ * cancelled. A runnable has been dispatched to the DOM File thread to
+ * notify the parent actor, and the main thread will continue to block until
+ * we receive a reponse.
+ */
+ Canceling,
+ /**
+ * The request is complete, either successfully or due to being cancelled.
+ * The main thread can stop waiting and immediately return to the caller of
+ * StartAndReturnResponse.
+ */
+ Complete
+ };
+
+ // The object we are issuing a request on behalf of. Present because of the
+ // need to invoke LSObject::StartRequest off the main thread. Dropped on
+ // return to the main-thread in Finish().
+ RefPtr<LSObject> mObject;
+ // The thread the RequestHelper was created on. This should be the main
+ // thread.
+ nsCOMPtr<nsIEventTarget> mOwningEventTarget;
+ // The IPC actor handling the request with standard IPC allocation rules.
+ // Our reference is nulled in OnResponse which corresponds to the actor's
+ // __destroy__ method.
+ LSRequestChild* mActor;
+ const LSRequestParams mParams;
+ Monitor mMonitor;
+ LSRequestResponse mResponse MOZ_GUARDED_BY(mMonitor);
+ nsresult mResultCode MOZ_GUARDED_BY(mMonitor);
+ State mState MOZ_GUARDED_BY(mMonitor);
+
+ public:
+ RequestHelper(LSObject* aObject, const LSRequestParams& aParams)
+ : Runnable("dom::RequestHelper"),
+ mObject(aObject),
+ mOwningEventTarget(GetCurrentEventTarget()),
+ mActor(nullptr),
+ mParams(aParams),
+ mMonitor("dom::RequestHelper::mMonitor"),
+ mResultCode(NS_OK),
+ mState(State::Initial) {}
+
+ bool IsOnOwningThread() const {
+ MOZ_ASSERT(mOwningEventTarget);
+
+ bool current;
+ return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(&current)) &&
+ current;
+ }
+
+ void AssertIsOnOwningThread() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(IsOnOwningThread());
+ }
+
+ nsresult StartAndReturnResponse(LSRequestResponse& aResponse);
+
+ private:
+ ~RequestHelper() = default;
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_NSIRUNNABLE
+
+ // LSRequestChildCallback
+ void OnResponse(const LSRequestResponse& aResponse) override;
+};
+
+void AssertExplicitSnapshotInvariants(const LSObject& aObject) {
+ // Can be only called if the mInExplicitSnapshot flag is true.
+ // An explicit snapshot must have been created.
+ MOZ_ASSERT(aObject.InExplicitSnapshot());
+
+ // If an explicit snapshot has been created then mDatabase must be not null.
+ // DropDatabase could be called in the meatime, but that must be preceded by
+ // Disconnect which sets mInExplicitSnapshot to false. EnsureDatabase could
+ // be called in the meantime too, but that can't set mDatabase to null or to
+ // a new value. See the comment below.
+ MOZ_ASSERT(aObject.DatabaseStrongRef());
+
+ // Existence of a snapshot prevents the database from allowing to close. See
+ // LSDatabase::RequestAllowToClose and LSDatabase::NoteFinishedSnapshot.
+ // If the database is not allowed to close then mDatabase could not have been
+ // nulled out or set to a new value. See EnsureDatabase.
+ MOZ_ASSERT(!aObject.DatabaseStrongRef()->IsAllowedToClose());
+}
+
+} // namespace
+
+LSObject::LSObject(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal,
+ nsIPrincipal* aStoragePrincipal)
+ : Storage(aWindow, aPrincipal, aStoragePrincipal),
+ mPrivateBrowsingId(0),
+ mInExplicitSnapshot(false) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(NextGenLocalStorageEnabled());
+}
+
+LSObject::~LSObject() {
+ AssertIsOnOwningThread();
+
+ DropObserver();
+}
+
+// static
+nsresult LSObject::CreateForWindow(nsPIDOMWindowInner* aWindow,
+ Storage** aStorage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(aStorage);
+ MOZ_ASSERT(NextGenLocalStorageEnabled());
+ MOZ_ASSERT(StorageAllowedForWindow(aWindow) != StorageAccess::eDeny);
+
+ nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow);
+ MOZ_ASSERT(sop);
+
+ nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
+ if (NS_WARN_IF(!principal)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIPrincipal> storagePrincipal = sop->GetEffectiveStoragePrincipal();
+ if (NS_WARN_IF(!storagePrincipal)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (principal->IsSystemPrincipal()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // localStorage is not available on some pages on purpose, for example
+ // about:home. Match the old implementation by using GenerateOriginKey
+ // for the check.
+ nsCString originAttrSuffix;
+ nsCString originKey;
+ nsresult rv = storagePrincipal->GetStorageOriginKey(originKey);
+ storagePrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix);
+
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ auto principalInfo = MakeUnique<PrincipalInfo>();
+ rv = PrincipalToPrincipalInfo(principal, principalInfo.get());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo);
+
+ auto storagePrincipalInfo = MakeUnique<PrincipalInfo>();
+ rv = PrincipalToPrincipalInfo(storagePrincipal, storagePrincipalInfo.get());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(storagePrincipalInfo->type() ==
+ PrincipalInfo::TContentPrincipalInfo);
+
+ if (NS_WARN_IF(
+ !quota::QuotaManager::IsPrincipalInfoValid(*storagePrincipalInfo))) {
+ return NS_ERROR_FAILURE;
+ }
+
+#ifdef DEBUG
+ QM_TRY_INSPECT(
+ const auto& principalMetadata,
+ quota::QuotaManager::GetInfoFromPrincipal(storagePrincipal.get()));
+
+ MOZ_ASSERT(originAttrSuffix == principalMetadata.mSuffix);
+
+ const auto& origin = principalMetadata.mOrigin;
+#else
+ QM_TRY_INSPECT(
+ const auto& origin,
+ quota::QuotaManager::GetOriginFromPrincipal(storagePrincipal.get()));
+#endif
+
+ uint32_t privateBrowsingId;
+ rv = storagePrincipal->GetPrivateBrowsingId(&privateBrowsingId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ Maybe<ClientInfo> clientInfo = aWindow->GetClientInfo();
+ if (clientInfo.isNothing()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ Maybe<nsID> clientId = Some(clientInfo.ref().Id());
+
+ Maybe<PrincipalInfo> clientPrincipalInfo =
+ Some(clientInfo.ref().PrincipalInfo());
+
+ nsString documentURI;
+ if (nsCOMPtr<Document> doc = aWindow->GetExtantDoc()) {
+ rv = doc->GetDocumentURI(documentURI);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ RefPtr<LSObject> object = new LSObject(aWindow, principal, storagePrincipal);
+ object->mPrincipalInfo = std::move(principalInfo);
+ object->mStoragePrincipalInfo = std::move(storagePrincipalInfo);
+ object->mPrivateBrowsingId = privateBrowsingId;
+ object->mClientId = clientId;
+ object->mClientPrincipalInfo = clientPrincipalInfo;
+ object->mOrigin = origin;
+ object->mOriginKey = originKey;
+ object->mDocumentURI = documentURI;
+
+ object.forget(aStorage);
+ return NS_OK;
+}
+
+// static
+nsresult LSObject::CreateForPrincipal(nsPIDOMWindowInner* aWindow,
+ nsIPrincipal* aPrincipal,
+ nsIPrincipal* aStoragePrincipal,
+ const nsAString& aDocumentURI,
+ bool aPrivate, LSObject** aObject) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aStoragePrincipal);
+ MOZ_ASSERT(aObject);
+
+ nsCString originAttrSuffix;
+ nsCString originKey;
+ nsresult rv = aStoragePrincipal->GetStorageOriginKey(originKey);
+ aStoragePrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ auto principalInfo = MakeUnique<PrincipalInfo>();
+ rv = PrincipalToPrincipalInfo(aPrincipal, principalInfo.get());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo ||
+ principalInfo->type() == PrincipalInfo::TSystemPrincipalInfo);
+
+ auto storagePrincipalInfo = MakeUnique<PrincipalInfo>();
+ rv = PrincipalToPrincipalInfo(aStoragePrincipal, storagePrincipalInfo.get());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(
+ storagePrincipalInfo->type() == PrincipalInfo::TContentPrincipalInfo ||
+ storagePrincipalInfo->type() == PrincipalInfo::TSystemPrincipalInfo);
+
+ if (NS_WARN_IF(
+ !quota::QuotaManager::IsPrincipalInfoValid(*storagePrincipalInfo))) {
+ return NS_ERROR_FAILURE;
+ }
+
+#ifdef DEBUG
+ QM_TRY_INSPECT(
+ const auto& principalMetadata,
+ ([&storagePrincipalInfo,
+ &aPrincipal]() -> Result<quota::PrincipalMetadata, nsresult> {
+ if (storagePrincipalInfo->type() ==
+ PrincipalInfo::TSystemPrincipalInfo) {
+ return quota::QuotaManager::GetInfoForChrome();
+ }
+
+ QM_TRY_RETURN(quota::QuotaManager::GetInfoFromPrincipal(aPrincipal));
+ }()));
+
+ MOZ_ASSERT(originAttrSuffix == principalMetadata.mSuffix);
+
+ const auto& origin = principalMetadata.mOrigin;
+#else
+ QM_TRY_INSPECT(
+ const auto& origin, ([&storagePrincipalInfo,
+ &aPrincipal]() -> Result<nsAutoCString, nsresult> {
+ if (storagePrincipalInfo->type() ==
+ PrincipalInfo::TSystemPrincipalInfo) {
+ return nsAutoCString{quota::QuotaManager::GetOriginForChrome()};
+ }
+
+ QM_TRY_RETURN(quota::QuotaManager::GetOriginFromPrincipal(aPrincipal));
+ }()));
+#endif
+
+ Maybe<nsID> clientId;
+ if (aWindow) {
+ Maybe<ClientInfo> clientInfo = aWindow->GetClientInfo();
+ if (clientInfo.isNothing()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ clientId = Some(clientInfo.ref().Id());
+ } else if (Preferences::GetBool("dom.storage.client_validation")) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<LSObject> object =
+ new LSObject(aWindow, aPrincipal, aStoragePrincipal);
+ object->mPrincipalInfo = std::move(principalInfo);
+ object->mStoragePrincipalInfo = std::move(storagePrincipalInfo);
+ object->mPrivateBrowsingId = aPrivate ? 1 : 0;
+ object->mClientId = clientId;
+ object->mOrigin = origin;
+ object->mOriginKey = originKey;
+ object->mDocumentURI = aDocumentURI;
+
+ object.forget(aObject);
+ return NS_OK;
+} // namespace dom
+
+LSRequestChild* LSObject::StartRequest(const LSRequestParams& aParams,
+ LSRequestChildCallback* aCallback) {
+ AssertIsOnDOMFileThread();
+
+ mozilla::ipc::PBackgroundChild* backgroundActor =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!backgroundActor)) {
+ return nullptr;
+ }
+
+ LSRequestChild* actor = new LSRequestChild();
+
+ if (!backgroundActor->SendPBackgroundLSRequestConstructor(actor, aParams)) {
+ return nullptr;
+ }
+
+ // Must set callback after calling SendPBackgroundLSRequestConstructor since
+ // it can be called synchronously when SendPBackgroundLSRequestConstructor
+ // fails.
+ actor->SetCallback(aCallback);
+
+ return actor;
+}
+
+Storage::StorageType LSObject::Type() const {
+ AssertIsOnOwningThread();
+
+ return eLocalStorage;
+}
+
+bool LSObject::IsForkOf(const Storage* aStorage) const {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aStorage);
+
+ if (aStorage->Type() != eLocalStorage) {
+ return false;
+ }
+
+ return static_cast<const LSObject*>(aStorage)->mOrigin == mOrigin;
+}
+
+int64_t LSObject::GetOriginQuotaUsage() const {
+ AssertIsOnOwningThread();
+
+ // It's not necessary to return an actual value here. This method is
+ // implemented only because the SessionStore currently needs it to cap the
+ // amount of data it persists to disk (via nsIDOMWindowUtils.getStorageUsage).
+ // Any callers that want to know about storage usage should be asking
+ // QuotaManager directly.
+ //
+ // Note: This may change as LocalStorage is repurposed to be the new
+ // SessionStorage backend.
+ return 0;
+}
+
+void LSObject::Disconnect() {
+ // Explicit snapshots which were not ended in JS, must be ended here while
+ // IPC is still available. We can't do that in DropDatabase because actors
+ // may have been destroyed already at that point.
+ if (mInExplicitSnapshot) {
+ AssertExplicitSnapshotInvariants(*this);
+
+ nsresult rv = mDatabase->EndExplicitSnapshot();
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+
+ mInExplicitSnapshot = false;
+ }
+}
+
+uint32_t LSObject::GetLength(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return 0;
+ }
+
+ nsresult rv = EnsureDatabase();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return 0;
+ }
+
+ uint32_t result;
+ rv = mDatabase->GetLength(this, &result);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return 0;
+ }
+
+ return result;
+}
+
+void LSObject::Key(uint32_t aIndex, nsAString& aResult,
+ nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ nsresult rv = EnsureDatabase();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ nsString result;
+ rv = mDatabase->GetKey(this, aIndex, result);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ aResult = result;
+}
+
+void LSObject::GetItem(const nsAString& aKey, nsAString& aResult,
+ nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ nsresult rv = EnsureDatabase();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ nsString result;
+ rv = mDatabase->GetItem(this, aKey, result);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ aResult = result;
+}
+
+void LSObject::GetSupportedNames(nsTArray<nsString>& aNames) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(*nsContentUtils::SubjectPrincipal())) {
+ // Return just an empty array.
+ aNames.Clear();
+ return;
+ }
+
+ nsresult rv = EnsureDatabase();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ rv = mDatabase->GetKeys(this, aNames);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+}
+
+void LSObject::SetItem(const nsAString& aKey, const nsAString& aValue,
+ nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ nsresult rv = EnsureDatabase();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ LSNotifyInfo info;
+ rv = mDatabase->SetItem(this, aKey, aValue, info);
+ if (rv == NS_ERROR_FILE_NO_DEVICE_SPACE) {
+ rv = NS_ERROR_DOM_QUOTA_EXCEEDED_ERR;
+ }
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ if (info.changed()) {
+ OnChange(aKey, info.oldValue(), aValue);
+ }
+}
+
+void LSObject::RemoveItem(const nsAString& aKey,
+ nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ nsresult rv = EnsureDatabase();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ LSNotifyInfo info;
+ rv = mDatabase->RemoveItem(this, aKey, info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ if (info.changed()) {
+ OnChange(aKey, info.oldValue(), VoidString());
+ }
+}
+
+void LSObject::Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ nsresult rv = EnsureDatabase();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ LSNotifyInfo info;
+ rv = mDatabase->Clear(this, info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ if (info.changed()) {
+ OnChange(VoidString(), VoidString(), VoidString());
+ }
+}
+
+void LSObject::Open(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ nsresult rv = EnsureDatabase();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+}
+
+void LSObject::Close(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ DropDatabase();
+}
+
+void LSObject::BeginExplicitSnapshot(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ if (mInExplicitSnapshot) {
+ aError.Throw(NS_ERROR_ALREADY_INITIALIZED);
+ return;
+ }
+
+ nsresult rv = EnsureDatabase();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ rv = mDatabase->BeginExplicitSnapshot(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ mInExplicitSnapshot = true;
+}
+
+void LSObject::CheckpointExplicitSnapshot(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ if (!mInExplicitSnapshot) {
+ aError.Throw(NS_ERROR_NOT_INITIALIZED);
+ return;
+ }
+
+ AssertExplicitSnapshotInvariants(*this);
+
+ nsresult rv = mDatabase->CheckpointExplicitSnapshot();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+}
+
+void LSObject::EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ if (!mInExplicitSnapshot) {
+ aError.Throw(NS_ERROR_NOT_INITIALIZED);
+ return;
+ }
+
+ AssertExplicitSnapshotInvariants(*this);
+
+ nsresult rv = mDatabase->EndExplicitSnapshot();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ mInExplicitSnapshot = false;
+}
+
+bool LSObject::GetHasSnapshot(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return false;
+ }
+
+ // We can't call `HasSnapshot` on the database if it's being closed, but we
+ // know that a database which is being closed can't have a snapshot, so we
+ // return false in that case directly here.
+ if (!mDatabase || mDatabase->IsAllowedToClose()) {
+ return false;
+ }
+
+ return mDatabase->HasSnapshot();
+}
+
+int64_t LSObject::GetSnapshotUsage(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return 0;
+ }
+
+ if (!mDatabase || mDatabase->IsAllowedToClose()) {
+ aError.Throw(NS_ERROR_NOT_AVAILABLE);
+ return 0;
+ }
+
+ if (!mDatabase->HasSnapshot()) {
+ aError.Throw(NS_ERROR_NOT_AVAILABLE);
+ return 0;
+ }
+
+ return mDatabase->GetSnapshotUsage();
+}
+
+NS_IMPL_ADDREF_INHERITED(LSObject, Storage)
+NS_IMPL_RELEASE_INHERITED(LSObject, Storage)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LSObject)
+NS_INTERFACE_MAP_END_INHERITING(Storage)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(LSObject)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(LSObject, Storage)
+ tmp->AssertIsOnOwningThread();
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(LSObject, Storage)
+ tmp->AssertIsOnOwningThread();
+ tmp->DropDatabase();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+nsresult LSObject::DoRequestSynchronously(const LSRequestParams& aParams,
+ LSRequestResponse& aResponse) {
+ // We don't need this yet, but once the request successfully finishes, it's
+ // too late to initialize PBackground child on the owning thread, because
+ // it can fail and parent would keep an extra strong ref to the datastore or
+ // observer.
+ mozilla::ipc::PBackgroundChild* backgroundActor =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!backgroundActor)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<RequestHelper> helper = new RequestHelper(this, aParams);
+
+ // This will start and finish the request on the RemoteLazyInputStream thread.
+ // The owning thread is synchronously blocked while the request is
+ // asynchronously processed on the RemoteLazyInputStream thread.
+ nsresult rv = helper->StartAndReturnResponse(aResponse);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (aResponse.type() == LSRequestResponse::Tnsresult) {
+ nsresult errorCode = aResponse.get_nsresult();
+
+ if (errorCode == NS_ERROR_FILE_NO_DEVICE_SPACE) {
+ errorCode = NS_ERROR_DOM_QUOTA_EXCEEDED_ERR;
+ }
+
+ return errorCode;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSObject::EnsureDatabase() {
+ AssertIsOnOwningThread();
+
+ if (mDatabase && !mDatabase->IsAllowedToClose()) {
+ return NS_OK;
+ }
+
+ mDatabase = LSDatabase::Get(mOrigin);
+
+ if (mDatabase) {
+ MOZ_ASSERT(!mDatabase->IsAllowedToClose());
+ return NS_OK;
+ }
+
+ // We don't need this yet, but once the request successfully finishes, it's
+ // too late to initialize PBackground child on the owning thread, because
+ // it can fail and parent would keep an extra strong ref to the datastore.
+ mozilla::ipc::PBackgroundChild* backgroundActor =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!backgroundActor)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ LSRequestCommonParams commonParams;
+ commonParams.principalInfo() = *mPrincipalInfo;
+ commonParams.storagePrincipalInfo() = *mStoragePrincipalInfo;
+ commonParams.originKey() = mOriginKey;
+
+ LSRequestPrepareDatastoreParams params;
+ params.commonParams() = commonParams;
+ params.clientId() = mClientId;
+ params.clientPrincipalInfo() = mClientPrincipalInfo;
+
+ LSRequestResponse response;
+
+ nsresult rv = DoRequestSynchronously(params, response);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(response.type() ==
+ LSRequestResponse::TLSRequestPrepareDatastoreResponse);
+
+ const LSRequestPrepareDatastoreResponse& prepareDatastoreResponse =
+ response.get_LSRequestPrepareDatastoreResponse();
+
+ uint64_t datastoreId = prepareDatastoreResponse.datastoreId();
+
+ // The datastore is now ready on the parent side (prepared by the asynchronous
+ // request on the RemoteLazyInputStream thread).
+ // Let's create a direct connection to the datastore (through a database
+ // actor) from the owning thread.
+ // Note that we now can't error out, otherwise parent will keep an extra
+ // strong reference to the datastore.
+
+ RefPtr<LSDatabase> database = new LSDatabase(mOrigin);
+
+ LSDatabaseChild* actor = new LSDatabaseChild(database);
+
+ MOZ_ALWAYS_TRUE(backgroundActor->SendPBackgroundLSDatabaseConstructor(
+ actor, *mStoragePrincipalInfo, mPrivateBrowsingId, datastoreId));
+
+ database->SetActor(actor);
+
+ mDatabase = std::move(database);
+
+ return NS_OK;
+}
+
+void LSObject::DropDatabase() {
+ AssertIsOnOwningThread();
+
+ mDatabase = nullptr;
+}
+
+nsresult LSObject::EnsureObserver() {
+ AssertIsOnOwningThread();
+
+ if (mObserver) {
+ return NS_OK;
+ }
+
+ mObserver = LSObserver::Get(mOrigin);
+
+ if (mObserver) {
+ return NS_OK;
+ }
+
+ LSRequestPrepareObserverParams params;
+ params.principalInfo() = *mPrincipalInfo;
+ params.storagePrincipalInfo() = *mStoragePrincipalInfo;
+ params.clientId() = mClientId;
+ params.clientPrincipalInfo() = mClientPrincipalInfo;
+
+ LSRequestResponse response;
+
+ nsresult rv = DoRequestSynchronously(params, response);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(response.type() ==
+ LSRequestResponse::TLSRequestPrepareObserverResponse);
+
+ const LSRequestPrepareObserverResponse& prepareObserverResponse =
+ response.get_LSRequestPrepareObserverResponse();
+
+ uint64_t observerId = prepareObserverResponse.observerId();
+
+ // The obsserver is now ready on the parent side (prepared by the asynchronous
+ // request on the RemoteLazyInputStream thread).
+ // Let's create a direct connection to the observer (through an observer
+ // actor) from the owning thread.
+ // Note that we now can't error out, otherwise parent will keep an extra
+ // strong reference to the observer.
+
+ mozilla::ipc::PBackgroundChild* backgroundActor =
+ mozilla::ipc::BackgroundChild::GetForCurrentThread();
+ MOZ_ASSERT(backgroundActor);
+
+ RefPtr<LSObserver> observer = new LSObserver(mOrigin);
+
+ LSObserverChild* actor = new LSObserverChild(observer);
+
+ MOZ_ALWAYS_TRUE(
+ backgroundActor->SendPBackgroundLSObserverConstructor(actor, observerId));
+
+ observer->SetActor(actor);
+
+ mObserver = std::move(observer);
+
+ return NS_OK;
+}
+
+void LSObject::DropObserver() {
+ AssertIsOnOwningThread();
+
+ if (mObserver) {
+ mObserver = nullptr;
+ }
+}
+
+void LSObject::OnChange(const nsAString& aKey, const nsAString& aOldValue,
+ const nsAString& aNewValue) {
+ AssertIsOnOwningThread();
+
+ NotifyChange(/* aStorage */ this, StoragePrincipal(), aKey, aOldValue,
+ aNewValue, /* aStorageType */ kLocalStorageType, mDocumentURI,
+ /* aIsPrivate */ !!mPrivateBrowsingId,
+ /* aImmediateDispatch */ false);
+}
+
+void LSObject::LastRelease() {
+ AssertIsOnOwningThread();
+
+ DropDatabase();
+}
+
+nsresult RequestHelper::StartAndReturnResponse(LSRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ nsCOMPtr<nsIEventTarget> domFileThread =
+ RemoteLazyInputStreamThread::GetOrCreate();
+ if (NS_WARN_IF(!domFileThread)) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ nsresult rv = domFileThread->Dispatch(this, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ TimeStamp deadline = TimeStamp::Now() + TimeDuration::FromMilliseconds(
+ FAILSAFE_CANCEL_SYNC_OP_MS);
+
+ MonitorAutoLock lock(mMonitor);
+ while (mState != State::Complete) {
+ TimeStamp now = TimeStamp::Now();
+ // If we are expecting shutdown or have passed our deadline, immediately
+ // dispatch ourselves to the DOM File thread to cancel the operation. We
+ // don't abort until the cancellation has gone through, as otherwise we
+ // could race with the DOM File thread.
+ if (mozilla::ipc::ProcessChild::ExpectingShutdown() || now >= deadline) {
+ switch (mState) {
+ case State::Initial:
+ // The DOM File thread never even woke before ExpectingShutdown() or a
+ // timeout - skip even creating the actor and just report an error.
+ mResultCode = NS_ERROR_FAILURE;
+ mState = State::Complete;
+ continue;
+ case State::ResponsePending:
+ // The DOM File thread is currently waiting for a reply, switch to a
+ // canceling state, and notify it to cancel by dispatching a runnable.
+ mState = State::Canceling;
+ MOZ_ALWAYS_SUCCEEDS(
+ domFileThread->Dispatch(this, NS_DISPATCH_NORMAL));
+ [[fallthrough]];
+ case State::Canceling:
+ // We've cancelled the request, so just need to wait indefinitely for
+ // it to complete.
+ lock.Wait();
+ continue;
+ default:
+ MOZ_ASSERT_UNREACHABLE("unexpected state");
+ }
+ }
+
+ // Wait until either we reach out deadline or for SYNC_OP_WAIT_INTERVAL_MS.
+ lock.Wait(TimeDuration::Min(
+ TimeDuration::FromMilliseconds(SYNC_OP_WAKE_INTERVAL_MS),
+ deadline - now));
+ }
+
+ // The operation is complete, clear our reference to the LSObject.
+ mObject = nullptr;
+
+ if (NS_WARN_IF(NS_FAILED(mResultCode))) {
+ return mResultCode;
+ }
+
+ aResponse = std::move(mResponse);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED0(RequestHelper, Runnable)
+
+NS_IMETHODIMP
+RequestHelper::Run() {
+ AssertIsOnDOMFileThread();
+
+ MonitorAutoLock lock(mMonitor);
+
+ switch (mState) {
+ case State::Initial: {
+ mState = State::ResponsePending;
+ {
+ MonitorAutoUnlock unlock(mMonitor);
+ mActor = mObject->StartRequest(mParams, this);
+ }
+ if (NS_WARN_IF(!mActor) && mState != State::Complete) {
+ // If we fail to even create the actor, instantly fail and notify our
+ // caller of the error. Otherwise we'll notify from OnResponse as called
+ // by the actor.
+ mResultCode = NS_ERROR_FAILURE;
+ mState = State::Complete;
+ lock.Notify();
+ }
+ return NS_OK;
+ }
+
+ case State::Canceling: {
+ // StartRequest() could fail or OnResponse was already called, so we need
+ // to check if actor is not null. The actor can also be in the final
+ // (finishing) state, in that case we are not allowed to send the cancel
+ // message and it wouldn't make any sense because the request is about to
+ // be destroyed anyway.
+ if (mActor && !mActor->Finishing()) {
+ mActor->SendCancel();
+ }
+ return NS_OK;
+ }
+
+ case State::Complete: {
+ // The operation was cancelled before we ran, do nothing.
+ return NS_OK;
+ }
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+}
+
+void RequestHelper::OnResponse(const LSRequestResponse& aResponse) {
+ AssertIsOnDOMFileThread();
+
+ MonitorAutoLock lock(mMonitor);
+
+ MOZ_ASSERT(mState == State::ResponsePending || mState == State::Canceling);
+
+ mActor = nullptr;
+
+ mResponse = aResponse;
+
+ mState = State::Complete;
+
+ lock.Notify();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/localstorage/LSObject.h b/dom/localstorage/LSObject.h
new file mode 100644
index 0000000000..d394816fa4
--- /dev/null
+++ b/dom/localstorage/LSObject.h
@@ -0,0 +1,220 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_LSObject_h
+#define mozilla_dom_localstorage_LSObject_h
+
+#include <cstdint>
+#include "ErrorList.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/Storage.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsID.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsTArrayForwardDeclare.h"
+
+class nsGlobalWindowInner;
+class nsIEventTarget;
+class nsIPrincipal;
+class nsISerialEventTarget;
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+class LSDatabase;
+class LSObjectChild;
+class LSObserver;
+class LSRequestChild;
+class LSRequestChildCallback;
+class LSRequestParams;
+class LSRequestResponse;
+
+/**
+ * Backs the WebIDL `Storage` binding; all content LocalStorage calls are
+ * handled by this class.
+ *
+ * ## Semantics under e10s / multi-process ##
+ *
+ * A snapshot mechanism used in conjuction with stable points ensures that JS
+ * run-to-completion semantics are experienced even if the same origin is
+ * concurrently accessing LocalStorage across multiple content processes.
+ *
+ * ### Snapshot Consistency ###
+ *
+ * An LSSnapshot is created locally whenever the contents of LocalStorage are
+ * about to be read or written (including length). This synchronously
+ * establishes a corresponding Snapshot in PBackground in the parent process.
+ * An effort is made to send as much data from the parent process as possible,
+ * so sites using a small/reasonable amount of LocalStorage data will have it
+ * sent to the content process for immediate access. Sites with greater
+ * LocalStorage usage may only have some of the information relayed. In that
+ * case, the parent Snapshot will ensure that it retains the exact state of the
+ * parent Datastore at the moment the Snapshot was created.
+ */
+class LSObject final : public Storage {
+ using PrincipalInfo = mozilla::ipc::PrincipalInfo;
+
+ friend nsGlobalWindowInner;
+
+ UniquePtr<PrincipalInfo> mPrincipalInfo;
+ UniquePtr<PrincipalInfo> mStoragePrincipalInfo;
+
+ RefPtr<LSDatabase> mDatabase;
+ RefPtr<LSObserver> mObserver;
+
+ uint32_t mPrivateBrowsingId;
+ Maybe<nsID> mClientId;
+ Maybe<PrincipalInfo> mClientPrincipalInfo;
+ nsCString mOrigin;
+ nsCString mOriginKey;
+ nsString mDocumentURI;
+
+ bool mInExplicitSnapshot;
+
+ public:
+ /**
+ * The normal creation path invoked by nsGlobalWindowInner.
+ */
+ static nsresult CreateForWindow(nsPIDOMWindowInner* aWindow,
+ Storage** aStorage);
+
+ /**
+ * nsIDOMStorageManager creation path for use in testing logic. Supports the
+ * system principal where CreateForWindow does not. This is also why aPrivate
+ * exists separate from the principal; because the system principal can never
+ * be mutated to have a private browsing id even though it can be used in a
+ * window/document marked as private browsing. That's a legacy issue that is
+ * being dealt with, but it's why it exists here.
+ */
+ static nsresult CreateForPrincipal(nsPIDOMWindowInner* aWindow,
+ nsIPrincipal* aPrincipal,
+ nsIPrincipal* aStoragePrincipal,
+ const nsAString& aDocumentURI,
+ bool aPrivate, LSObject** aObject);
+
+ void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(LSObject); }
+
+ const RefPtr<LSDatabase>& DatabaseStrongRef() const { return mDatabase; }
+
+ const nsString& DocumentURI() const { return mDocumentURI; }
+
+ bool InExplicitSnapshot() const { return mInExplicitSnapshot; }
+
+ LSRequestChild* StartRequest(const LSRequestParams& aParams,
+ LSRequestChildCallback* aCallback);
+
+ // Storage overrides.
+ StorageType Type() const override;
+
+ bool IsForkOf(const Storage* aStorage) const override;
+
+ int64_t GetOriginQuotaUsage() const override;
+
+ void Disconnect() override;
+
+ uint32_t GetLength(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) override;
+
+ void Key(uint32_t aIndex, nsAString& aResult, nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) override;
+
+ void GetItem(const nsAString& aKey, nsAString& aResult,
+ nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) override;
+
+ void GetSupportedNames(nsTArray<nsString>& aNames) override;
+
+ void SetItem(const nsAString& aKey, const nsAString& aValue,
+ nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) override;
+
+ void RemoveItem(const nsAString& aKey, nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) override;
+
+ void Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Testing Methods: See Storage.h
+ void Open(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) override;
+
+ void Close(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) override;
+
+ void BeginExplicitSnapshot(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) override;
+
+ void CheckpointExplicitSnapshot(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) override;
+
+ void EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) override;
+
+ bool GetHasSnapshot(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) override;
+
+ int64_t GetSnapshotUsage(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) override;
+
+ //////////////////////////////////////////////////////////////////////////////
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(LSObject, Storage)
+
+ private:
+ LSObject(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal,
+ nsIPrincipal* aStoragePrincipal);
+
+ ~LSObject();
+
+ nsresult DoRequestSynchronously(const LSRequestParams& aParams,
+ LSRequestResponse& aResponse);
+
+ nsresult EnsureDatabase();
+
+ void DropDatabase();
+
+ /**
+ * Invoked by nsGlobalWindowInner whenever a new "storage" event listener is
+ * added to the window in order to ensure that "storage" events are received
+ * from other processes. (`LSObject::OnChange` directly invokes
+ * `Storage::NotifyChange` to notify in-process listeners.)
+ *
+ * If this is the first request in the process for an observer for this
+ * origin, this will trigger a RequestHelper-mediated synchronous LSRequest
+ * to prepare a new observer in the parent process and also construction of
+ * corresponding actors, which will result in the observer being fully
+ * registered in the parent process.
+ */
+ nsresult EnsureObserver();
+
+ /**
+ * Invoked by nsGlobalWindowInner whenever its last "storage" event listener
+ * is removed.
+ */
+ void DropObserver();
+
+ /**
+ * Internal helper method used by mutation methods that wraps the call to
+ * Storage::NotifyChange to generate same-process "storage" events.
+ */
+ void OnChange(const nsAString& aKey, const nsAString& aOldValue,
+ const nsAString& aNewValue);
+
+ // Storage overrides.
+ void LastRelease() override;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_LSObject_h
diff --git a/dom/localstorage/LSObserver.cpp b/dom/localstorage/LSObserver.cpp
new file mode 100644
index 0000000000..fda56bd1ed
--- /dev/null
+++ b/dom/localstorage/LSObserver.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "LSObserver.h"
+
+// Local includes
+#include "ActorsChild.h"
+
+// Global includes
+#include <utility>
+#include "mozilla/StaticPtr.h"
+#include "nsTHashMap.h"
+#include "nsHashKeys.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+using LSObserverHashtable = nsTHashMap<nsCStringHashKey, LSObserver*>;
+
+StaticAutoPtr<LSObserverHashtable> gLSObservers;
+
+} // namespace
+
+LSObserver::LSObserver(const nsACString& aOrigin)
+ : mActor(nullptr), mOrigin(aOrigin) {
+ AssertIsOnOwningThread();
+
+ if (!gLSObservers) {
+ gLSObservers = new LSObserverHashtable();
+ }
+
+ MOZ_ASSERT(!gLSObservers->Contains(mOrigin));
+ gLSObservers->InsertOrUpdate(mOrigin, this);
+}
+
+LSObserver::~LSObserver() {
+ AssertIsOnOwningThread();
+
+ if (mActor) {
+ mActor->SendDeleteMeInternal();
+ MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!");
+ }
+
+ MOZ_ASSERT(gLSObservers);
+ MOZ_ASSERT(gLSObservers->Get(mOrigin));
+ gLSObservers->Remove(mOrigin);
+
+ if (!gLSObservers->Count()) {
+ gLSObservers = nullptr;
+ }
+}
+
+// static
+LSObserver* LSObserver::Get(const nsACString& aOrigin) {
+ return gLSObservers ? gLSObservers->Get(aOrigin) : nullptr;
+}
+
+void LSObserver::SetActor(LSObserverChild* aActor) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(!mActor);
+
+ mActor = aActor;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/localstorage/LSObserver.h b/dom/localstorage/LSObserver.h
new file mode 100644
index 0000000000..55107bc6c4
--- /dev/null
+++ b/dom/localstorage/LSObserver.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_LSObserver_h
+#define mozilla_dom_localstorage_LSObserver_h
+
+#include "mozilla/Assertions.h"
+#include "nsISupports.h"
+#include "nsString.h"
+
+namespace mozilla::dom {
+
+class LSObserverChild;
+
+/**
+ * Effectively just a refcounted life-cycle management wrapper around
+ * LSObserverChild which exists to receive "storage" event information from
+ * other processes. (Same-process events are handled within the process, see
+ * `LSObject::OnChange`.)
+ *
+ * ## Lifecycle ##
+ * - Created by LSObject::EnsureObserver via synchronous LSRequest idiom
+ * whenever the first window's origin adds a "storage" event. Placed in the
+ * gLSObservers LSObserverHashtable for subsequent LSObject's via
+ * LSObserver::Get lookup.
+ * - The LSObserverChild directly handles "Observe" messages, shunting them
+ * directly to Storage::NotifyChange which does all the legwork of notifying
+ * windows about "storage" events.
+ * - Destroyed when refcount goes to zero due to all owning LSObjects being
+ * destroyed or having their `LSObject::DropObserver` methods invoked due to
+ * the last "storage" event listener being removed from the owning window.
+ */
+class LSObserver final {
+ friend class LSObject;
+
+ LSObserverChild* mActor;
+
+ const nsCString mOrigin;
+
+ public:
+ static LSObserver* Get(const nsACString& aOrigin);
+
+ NS_INLINE_DECL_REFCOUNTING(LSObserver)
+
+ void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(LSObserver); }
+
+ void SetActor(LSObserverChild* aActor);
+
+ void ClearActor() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+
+ mActor = nullptr;
+ }
+
+ private:
+ // Only created by LSObject.
+ explicit LSObserver(const nsACString& aOrigin);
+
+ ~LSObserver();
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_localstorage_LSObserver_h
diff --git a/dom/localstorage/LSSnapshot.cpp b/dom/localstorage/LSSnapshot.cpp
new file mode 100644
index 0000000000..e41a682ae4
--- /dev/null
+++ b/dom/localstorage/LSSnapshot.cpp
@@ -0,0 +1,1078 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "mozilla/dom/LSSnapshot.h"
+
+// Local includes
+#include "ActorsChild.h"
+#include "LSDatabase.h"
+#include "LSWriteOptimizer.h"
+#include "LSWriteOptimizerImpl.h"
+#include "LocalStorageCommon.h"
+
+// Global includes
+#include <cstdint>
+#include <cstdlib>
+#include <new>
+#include <type_traits>
+#include <utility>
+#include "ErrorList.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/MacroForEach.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/LSValue.h"
+#include "mozilla/dom/PBackgroundLSDatabase.h"
+#include "mozilla/dom/PBackgroundLSSharedTypes.h"
+#include "mozilla/dom/PBackgroundLSSnapshot.h"
+#include "nsBaseHashtable.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsITimer.h"
+#include "nsString.h"
+#include "nsStringFlags.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTStringRepr.h"
+#include "nscore.h"
+
+namespace mozilla::dom {
+
+/**
+ * Coalescing manipulation queue used by `LSSnapshot`. Used by `LSSnapshot` to
+ * buffer and coalesce manipulations before they are sent to the parent process,
+ * when a Snapshot Checkpoints. (This can only be done when there are no
+ * observers for other content processes.)
+ */
+class SnapshotWriteOptimizer final : public LSWriteOptimizer<LSValue> {
+ public:
+ void Enumerate(nsTArray<LSWriteInfo>& aWriteInfos);
+};
+
+void SnapshotWriteOptimizer::Enumerate(nsTArray<LSWriteInfo>& aWriteInfos) {
+ AssertIsOnOwningThread();
+
+ // The mWriteInfos hash table contains all write infos, but it keeps them in
+ // an arbitrary order, which means write infos need to be sorted before being
+ // processed.
+
+ nsTArray<NotNull<WriteInfo*>> writeInfos;
+ GetSortedWriteInfos(writeInfos);
+
+ for (WriteInfo* writeInfo : writeInfos) {
+ switch (writeInfo->GetType()) {
+ case WriteInfo::InsertItem: {
+ auto insertItemInfo = static_cast<InsertItemInfo*>(writeInfo);
+
+ LSSetItemInfo setItemInfo;
+ setItemInfo.key() = insertItemInfo->GetKey();
+ setItemInfo.value() = insertItemInfo->GetValue();
+
+ aWriteInfos.AppendElement(std::move(setItemInfo));
+
+ break;
+ }
+
+ case WriteInfo::UpdateItem: {
+ auto updateItemInfo = static_cast<UpdateItemInfo*>(writeInfo);
+
+ if (updateItemInfo->UpdateWithMove()) {
+ // See the comment in LSWriteOptimizer::InsertItem for more details
+ // about the UpdateWithMove flag.
+
+ LSRemoveItemInfo removeItemInfo;
+ removeItemInfo.key() = updateItemInfo->GetKey();
+
+ aWriteInfos.AppendElement(std::move(removeItemInfo));
+ }
+
+ LSSetItemInfo setItemInfo;
+ setItemInfo.key() = updateItemInfo->GetKey();
+ setItemInfo.value() = updateItemInfo->GetValue();
+
+ aWriteInfos.AppendElement(std::move(setItemInfo));
+
+ break;
+ }
+
+ case WriteInfo::DeleteItem: {
+ auto deleteItemInfo = static_cast<DeleteItemInfo*>(writeInfo);
+
+ LSRemoveItemInfo removeItemInfo;
+ removeItemInfo.key() = deleteItemInfo->GetKey();
+
+ aWriteInfos.AppendElement(std::move(removeItemInfo));
+
+ break;
+ }
+
+ case WriteInfo::Truncate: {
+ LSClearInfo clearInfo;
+
+ aWriteInfos.AppendElement(std::move(clearInfo));
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Bad type!");
+ }
+ }
+}
+
+LSSnapshot::LSSnapshot(LSDatabase* aDatabase)
+ : mDatabase(aDatabase),
+ mActor(nullptr),
+ mInitLength(0),
+ mLength(0),
+ mUsage(0),
+ mPeakUsage(0),
+ mLoadState(LoadState::Initial),
+ mHasOtherProcessDatabases(false),
+ mHasOtherProcessObservers(false),
+ mExplicit(false),
+ mHasPendingStableStateCallback(false),
+ mHasPendingIdleTimerCallback(false),
+ mDirty(false)
+#ifdef DEBUG
+ ,
+ mInitialized(false),
+ mSentFinish(false)
+#endif
+{
+ AssertIsOnOwningThread();
+}
+
+LSSnapshot::~LSSnapshot() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mDatabase);
+ MOZ_ASSERT(!mHasPendingStableStateCallback);
+ MOZ_ASSERT(!mHasPendingIdleTimerCallback);
+ MOZ_ASSERT_IF(mInitialized, mSentFinish);
+
+ if (mActor) {
+ mActor->SendDeleteMeInternal();
+ MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!");
+ }
+}
+
+void LSSnapshot::SetActor(LSSnapshotChild* aActor) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(!mActor);
+
+ mActor = aActor;
+}
+
+nsresult LSSnapshot::Init(const nsAString& aKey,
+ const LSSnapshotInitInfo& aInitInfo, bool aExplicit) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mSelfRef);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mLoadState == LoadState::Initial);
+ MOZ_ASSERT(!mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ mSelfRef = this;
+
+ LoadState loadState = aInitInfo.loadState();
+
+ const nsTArray<LSItemInfo>& itemInfos = aInitInfo.itemInfos();
+ for (uint32_t i = 0; i < itemInfos.Length(); i++) {
+ const LSItemInfo& itemInfo = itemInfos[i];
+
+ const LSValue& value = itemInfo.value();
+
+ if (loadState != LoadState::AllOrderedItems && !value.IsVoid()) {
+ mLoadedItems.Insert(itemInfo.key());
+ }
+
+ mValues.InsertOrUpdate(itemInfo.key(), value.AsString());
+ }
+
+ if (loadState == LoadState::Partial) {
+ if (aInitInfo.addKeyToUnknownItems()) {
+ mUnknownItems.Insert(aKey);
+ }
+ mInitLength = aInitInfo.totalLength();
+ mLength = mInitLength;
+ } else if (loadState == LoadState::AllOrderedKeys) {
+ mInitLength = aInitInfo.totalLength();
+ } else {
+ MOZ_ASSERT(loadState == LoadState::AllOrderedItems);
+ }
+
+ mUsage = aInitInfo.usage();
+ mPeakUsage = aInitInfo.peakUsage();
+
+ mLoadState = aInitInfo.loadState();
+
+ mHasOtherProcessDatabases = aInitInfo.hasOtherProcessDatabases();
+ mHasOtherProcessObservers = aInitInfo.hasOtherProcessObservers();
+
+ mExplicit = aExplicit;
+
+#ifdef DEBUG
+ mInitialized = true;
+#endif
+
+ if (mHasOtherProcessObservers) {
+ mWriteAndNotifyInfos = MakeUnique<nsTArray<LSWriteAndNotifyInfo>>();
+ } else {
+ mWriteOptimizer = MakeUnique<SnapshotWriteOptimizer>();
+ }
+
+ if (!mExplicit) {
+ mIdleTimer = NS_NewTimer();
+ MOZ_ASSERT(mIdleTimer);
+
+ ScheduleStableStateCallback();
+ }
+
+ return NS_OK;
+}
+
+nsresult LSSnapshot::GetLength(uint32_t* aResult) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ MaybeScheduleStableStateCallback();
+
+ if (mLoadState == LoadState::Partial) {
+ *aResult = mLength;
+ } else {
+ *aResult = mValues.Count();
+ }
+
+ return NS_OK;
+}
+
+nsresult LSSnapshot::GetKey(uint32_t aIndex, nsAString& aResult) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ MaybeScheduleStableStateCallback();
+
+ nsresult rv = EnsureAllKeys();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ aResult.SetIsVoid(true);
+ for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
+ if (aIndex == 0) {
+ aResult = iter.Key();
+ return NS_OK;
+ }
+ aIndex--;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSSnapshot::GetItem(const nsAString& aKey, nsAString& aResult) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ MaybeScheduleStableStateCallback();
+
+ nsString result;
+ nsresult rv = GetItemInternal(aKey, Optional<nsString>(), result);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ aResult = result;
+ return NS_OK;
+}
+
+nsresult LSSnapshot::GetKeys(nsTArray<nsString>& aKeys) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ MaybeScheduleStableStateCallback();
+
+ nsresult rv = EnsureAllKeys();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ AppendToArray(aKeys, mValues.Keys());
+
+ return NS_OK;
+}
+
+nsresult LSSnapshot::SetItem(const nsAString& aKey, const nsAString& aValue,
+ LSNotifyInfo& aNotifyInfo) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ MaybeScheduleStableStateCallback();
+
+ nsString oldValue;
+ nsresult rv =
+ GetItemInternal(aKey, Optional<nsString>(nsString(aValue)), oldValue);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ bool changed;
+ if (oldValue == aValue && oldValue.IsVoid() == aValue.IsVoid()) {
+ changed = false;
+ } else {
+ changed = true;
+
+ auto autoRevertValue = MakeScopeExit([&] {
+ if (oldValue.IsVoid()) {
+ mValues.Remove(aKey);
+ } else {
+ mValues.InsertOrUpdate(aKey, oldValue);
+ }
+ });
+
+ // Anything that can fail must be done early before we start modifying the
+ // state.
+
+ Maybe<LSValue> oldValueFromString;
+ if (mHasOtherProcessObservers) {
+ oldValueFromString.emplace();
+ if (NS_WARN_IF(!oldValueFromString->InitFromString(oldValue))) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ LSValue valueFromString;
+ if (NS_WARN_IF(!valueFromString.InitFromString(aValue))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int64_t delta = static_cast<int64_t>(aValue.Length()) -
+ static_cast<int64_t>(oldValue.Length());
+
+ if (oldValue.IsVoid()) {
+ delta += static_cast<int64_t>(aKey.Length());
+ }
+
+ rv = UpdateUsage(delta);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (oldValue.IsVoid() && mLoadState == LoadState::Partial) {
+ mLength++;
+ }
+
+ if (mHasOtherProcessObservers) {
+ MOZ_ASSERT(mWriteAndNotifyInfos);
+ MOZ_ASSERT(oldValueFromString.isSome());
+
+ LSSetItemAndNotifyInfo setItemAndNotifyInfo;
+ setItemAndNotifyInfo.key() = aKey;
+ setItemAndNotifyInfo.oldValue() = oldValueFromString.value();
+ setItemAndNotifyInfo.value() = valueFromString;
+
+ mWriteAndNotifyInfos->AppendElement(std::move(setItemAndNotifyInfo));
+ } else {
+ MOZ_ASSERT(mWriteOptimizer);
+
+ if (oldValue.IsVoid()) {
+ mWriteOptimizer->InsertItem(aKey, valueFromString);
+ } else {
+ mWriteOptimizer->UpdateItem(aKey, valueFromString);
+ }
+ }
+
+ autoRevertValue.release();
+ }
+
+ aNotifyInfo.changed() = changed;
+ aNotifyInfo.oldValue() = oldValue;
+
+ return NS_OK;
+}
+
+nsresult LSSnapshot::RemoveItem(const nsAString& aKey,
+ LSNotifyInfo& aNotifyInfo) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ MaybeScheduleStableStateCallback();
+
+ nsString oldValue;
+ nsresult rv =
+ GetItemInternal(aKey, Optional<nsString>(VoidString()), oldValue);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ bool changed;
+ if (oldValue.IsVoid()) {
+ changed = false;
+ } else {
+ changed = true;
+
+ auto autoRevertValue = MakeScopeExit([&] {
+ MOZ_ASSERT(!oldValue.IsVoid());
+ mValues.InsertOrUpdate(aKey, oldValue);
+ });
+
+ // Anything that can fail must be done early before we start modifying the
+ // state.
+
+ Maybe<LSValue> oldValueFromString;
+ if (mHasOtherProcessObservers) {
+ oldValueFromString.emplace();
+ if (NS_WARN_IF(!oldValueFromString->InitFromString(oldValue))) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ int64_t delta = -(static_cast<int64_t>(aKey.Length()) +
+ static_cast<int64_t>(oldValue.Length()));
+
+ DebugOnly<nsresult> rv = UpdateUsage(delta);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ if (mLoadState == LoadState::Partial) {
+ mLength--;
+ }
+
+ if (mHasOtherProcessObservers) {
+ MOZ_ASSERT(mWriteAndNotifyInfos);
+ MOZ_ASSERT(oldValueFromString.isSome());
+
+ LSRemoveItemAndNotifyInfo removeItemAndNotifyInfo;
+ removeItemAndNotifyInfo.key() = aKey;
+ removeItemAndNotifyInfo.oldValue() = oldValueFromString.value();
+
+ mWriteAndNotifyInfos->AppendElement(std::move(removeItemAndNotifyInfo));
+ } else {
+ MOZ_ASSERT(mWriteOptimizer);
+
+ mWriteOptimizer->DeleteItem(aKey);
+ }
+
+ autoRevertValue.release();
+ }
+
+ aNotifyInfo.changed() = changed;
+ aNotifyInfo.oldValue() = oldValue;
+
+ return NS_OK;
+}
+
+nsresult LSSnapshot::Clear(LSNotifyInfo& aNotifyInfo) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ MaybeScheduleStableStateCallback();
+
+ uint32_t length;
+ if (mLoadState == LoadState::Partial) {
+ length = mLength;
+ MOZ_ASSERT(length);
+
+ MOZ_ALWAYS_TRUE(mActor->SendLoaded());
+
+ mLoadedItems.Clear();
+ mUnknownItems.Clear();
+ mLength = 0;
+ mLoadState = LoadState::AllOrderedItems;
+ } else {
+ length = mValues.Count();
+ }
+
+ bool changed;
+ if (!length) {
+ changed = false;
+ } else {
+ changed = true;
+
+ int64_t delta = 0;
+ for (const auto& entry : mValues) {
+ const nsAString& key = entry.GetKey();
+ const nsString& value = entry.GetData();
+
+ delta += -static_cast<int64_t>(key.Length()) -
+ static_cast<int64_t>(value.Length());
+ }
+
+ DebugOnly<nsresult> rv = UpdateUsage(delta);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ mValues.Clear();
+
+ if (mHasOtherProcessObservers) {
+ MOZ_ASSERT(mWriteAndNotifyInfos);
+
+ LSClearInfo clearInfo;
+
+ mWriteAndNotifyInfos->AppendElement(std::move(clearInfo));
+ } else {
+ MOZ_ASSERT(mWriteOptimizer);
+
+ mWriteOptimizer->Truncate();
+ }
+ }
+
+ aNotifyInfo.changed() = changed;
+
+ return NS_OK;
+}
+
+void LSSnapshot::MarkDirty() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ if (mDirty) {
+ return;
+ }
+
+ mDirty = true;
+
+ if (!mExplicit && !mHasPendingStableStateCallback) {
+ CancelIdleTimer();
+
+ MOZ_ALWAYS_SUCCEEDS(Checkpoint());
+
+ MOZ_ALWAYS_SUCCEEDS(Finish());
+ } else {
+ MOZ_ASSERT(!mHasPendingIdleTimerCallback);
+ }
+}
+
+nsresult LSSnapshot::ExplicitCheckpoint() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mExplicit);
+ MOZ_ASSERT(!mHasPendingStableStateCallback);
+ MOZ_ASSERT(!mHasPendingIdleTimerCallback);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ nsresult rv = Checkpoint(/* aSync */ true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSSnapshot::ExplicitEnd() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mExplicit);
+ MOZ_ASSERT(!mHasPendingStableStateCallback);
+ MOZ_ASSERT(!mHasPendingIdleTimerCallback);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ nsresult rv = Checkpoint();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<LSSnapshot> kungFuDeathGrip = this;
+
+ rv = Finish(/* aSync */ true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+int64_t LSSnapshot::GetUsage() const {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ return mUsage;
+}
+
+void LSSnapshot::ScheduleStableStateCallback() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mIdleTimer);
+ MOZ_ASSERT(!mExplicit);
+ MOZ_ASSERT(!mHasPendingStableStateCallback);
+
+ CancelIdleTimer();
+
+ nsCOMPtr<nsIRunnable> runnable = this;
+ nsContentUtils::RunInStableState(runnable.forget());
+
+ mHasPendingStableStateCallback = true;
+}
+
+void LSSnapshot::MaybeScheduleStableStateCallback() {
+ AssertIsOnOwningThread();
+
+ if (!mExplicit && !mHasPendingStableStateCallback) {
+ ScheduleStableStateCallback();
+ } else {
+ MOZ_ASSERT(!mHasPendingIdleTimerCallback);
+ }
+}
+
+nsresult LSSnapshot::GetItemInternal(const nsAString& aKey,
+ const Optional<nsString>& aValue,
+ nsAString& aResult) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ nsString result;
+
+ switch (mLoadState) {
+ case LoadState::Partial: {
+ if (mValues.Get(aKey, &result)) {
+ MOZ_ASSERT(!result.IsVoid());
+ } else if (mLoadedItems.Contains(aKey) || mUnknownItems.Contains(aKey)) {
+ result.SetIsVoid(true);
+ } else {
+ LSValue value;
+ nsTArray<LSItemInfo> itemInfos;
+ if (NS_WARN_IF(!mActor->SendLoadValueAndMoreItems(
+ nsString(aKey), &value, &itemInfos))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ result = value.AsString();
+
+ if (result.IsVoid()) {
+ mUnknownItems.Insert(aKey);
+ } else {
+ mLoadedItems.Insert(aKey);
+ mValues.InsertOrUpdate(aKey, result);
+
+ // mLoadedItems.Count()==mInitLength is checked below.
+ }
+
+ for (uint32_t i = 0; i < itemInfos.Length(); i++) {
+ const LSItemInfo& itemInfo = itemInfos[i];
+
+ mLoadedItems.Insert(itemInfo.key());
+ mValues.InsertOrUpdate(itemInfo.key(), itemInfo.value().AsString());
+ }
+
+ if (mLoadedItems.Count() == mInitLength) {
+ mLoadedItems.Clear();
+ mUnknownItems.Clear();
+ mLength = 0;
+ mLoadState = LoadState::AllUnorderedItems;
+ }
+ }
+
+ if (aValue.WasPassed()) {
+ const nsString& value = aValue.Value();
+ if (!value.IsVoid()) {
+ mValues.InsertOrUpdate(aKey, value);
+ } else if (!result.IsVoid()) {
+ mValues.Remove(aKey);
+ }
+ }
+
+ break;
+ }
+
+ case LoadState::AllOrderedKeys: {
+ if (mValues.Get(aKey, &result)) {
+ if (result.IsVoid()) {
+ LSValue value;
+ nsTArray<LSItemInfo> itemInfos;
+ if (NS_WARN_IF(!mActor->SendLoadValueAndMoreItems(
+ nsString(aKey), &value, &itemInfos))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ result = value.AsString();
+
+ MOZ_ASSERT(!result.IsVoid());
+
+ mLoadedItems.Insert(aKey);
+ mValues.InsertOrUpdate(aKey, result);
+
+ // mLoadedItems.Count()==mInitLength is checked below.
+
+ for (uint32_t i = 0; i < itemInfos.Length(); i++) {
+ const LSItemInfo& itemInfo = itemInfos[i];
+
+ mLoadedItems.Insert(itemInfo.key());
+ mValues.InsertOrUpdate(itemInfo.key(), itemInfo.value().AsString());
+ }
+
+ if (mLoadedItems.Count() == mInitLength) {
+ mLoadedItems.Clear();
+ MOZ_ASSERT(mLength == 0);
+ mLoadState = LoadState::AllOrderedItems;
+ }
+ }
+ } else {
+ result.SetIsVoid(true);
+ }
+
+ if (aValue.WasPassed()) {
+ const nsString& value = aValue.Value();
+ if (!value.IsVoid()) {
+ mValues.InsertOrUpdate(aKey, value);
+ } else if (!result.IsVoid()) {
+ mValues.Remove(aKey);
+ }
+ }
+
+ break;
+ }
+
+ case LoadState::AllUnorderedItems:
+ case LoadState::AllOrderedItems: {
+ if (aValue.WasPassed()) {
+ const nsString& value = aValue.Value();
+ if (!value.IsVoid()) {
+ mValues.WithEntryHandle(aKey, [&](auto&& entry) {
+ if (entry) {
+ result = std::exchange(entry.Data(), value);
+ } else {
+ result.SetIsVoid(true);
+ entry.Insert(value);
+ }
+ });
+ } else {
+ if (auto entry = mValues.Lookup(aKey)) {
+ result = entry.Data();
+ MOZ_ASSERT(!result.IsVoid());
+ entry.Remove();
+ } else {
+ result.SetIsVoid(true);
+ }
+ }
+ } else {
+ if (mValues.Get(aKey, &result)) {
+ MOZ_ASSERT(!result.IsVoid());
+ } else {
+ result.SetIsVoid(true);
+ }
+ }
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+
+ aResult = result;
+ return NS_OK;
+}
+
+nsresult LSSnapshot::EnsureAllKeys() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+ MOZ_ASSERT(mLoadState != LoadState::Initial);
+
+ if (mLoadState == LoadState::AllOrderedKeys ||
+ mLoadState == LoadState::AllOrderedItems) {
+ return NS_OK;
+ }
+
+ nsTArray<nsString> keys;
+ if (NS_WARN_IF(!mActor->SendLoadKeys(&keys))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsTHashMap<nsStringHashKey, nsString> newValues;
+
+ for (auto key : keys) {
+ newValues.InsertOrUpdate(key, VoidString());
+ }
+
+ if (mHasOtherProcessObservers) {
+ MOZ_ASSERT(mWriteAndNotifyInfos);
+
+ if (!mWriteAndNotifyInfos->IsEmpty()) {
+ for (uint32_t index = 0; index < mWriteAndNotifyInfos->Length();
+ index++) {
+ const LSWriteAndNotifyInfo& writeAndNotifyInfo =
+ mWriteAndNotifyInfos->ElementAt(index);
+
+ switch (writeAndNotifyInfo.type()) {
+ case LSWriteAndNotifyInfo::TLSSetItemAndNotifyInfo: {
+ newValues.InsertOrUpdate(
+ writeAndNotifyInfo.get_LSSetItemAndNotifyInfo().key(),
+ VoidString());
+ break;
+ }
+ case LSWriteAndNotifyInfo::TLSRemoveItemAndNotifyInfo: {
+ newValues.Remove(
+ writeAndNotifyInfo.get_LSRemoveItemAndNotifyInfo().key());
+ break;
+ }
+ case LSWriteAndNotifyInfo::TLSClearInfo: {
+ newValues.Clear();
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+ }
+ }
+ } else {
+ MOZ_ASSERT(mWriteOptimizer);
+
+ if (mWriteOptimizer->HasWrites()) {
+ nsTArray<LSWriteInfo> writeInfos;
+ mWriteOptimizer->Enumerate(writeInfos);
+
+ MOZ_ASSERT(!writeInfos.IsEmpty());
+
+ for (uint32_t index = 0; index < writeInfos.Length(); index++) {
+ const LSWriteInfo& writeInfo = writeInfos[index];
+
+ switch (writeInfo.type()) {
+ case LSWriteInfo::TLSSetItemInfo: {
+ newValues.InsertOrUpdate(writeInfo.get_LSSetItemInfo().key(),
+ VoidString());
+ break;
+ }
+ case LSWriteInfo::TLSRemoveItemInfo: {
+ newValues.Remove(writeInfo.get_LSRemoveItemInfo().key());
+ break;
+ }
+ case LSWriteInfo::TLSClearInfo: {
+ newValues.Clear();
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+ }
+ }
+ }
+
+ MOZ_ASSERT_IF(mLoadState == LoadState::AllUnorderedItems,
+ newValues.Count() == mValues.Count());
+
+ for (auto iter = newValues.Iter(); !iter.Done(); iter.Next()) {
+ nsString value;
+ if (mValues.Get(iter.Key(), &value)) {
+ iter.Data() = value;
+ }
+ }
+
+ mValues.SwapElements(newValues);
+
+ if (mLoadState == LoadState::Partial) {
+ mUnknownItems.Clear();
+ mLength = 0;
+ mLoadState = LoadState::AllOrderedKeys;
+ } else {
+ MOZ_ASSERT(mLoadState == LoadState::AllUnorderedItems);
+
+ MOZ_ASSERT(mUnknownItems.Count() == 0);
+ MOZ_ASSERT(mLength == 0);
+ mLoadState = LoadState::AllOrderedItems;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSSnapshot::UpdateUsage(int64_t aDelta) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mDatabase);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mPeakUsage >= mUsage);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ int64_t newUsage = mUsage + aDelta;
+ if (newUsage > mPeakUsage) {
+ const int64_t minSize = newUsage - mPeakUsage;
+
+ int64_t size;
+ if (NS_WARN_IF(!mActor->SendIncreasePeakUsage(minSize, &size))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(size >= 0);
+
+ if (size == 0) {
+ return NS_ERROR_FILE_NO_DEVICE_SPACE;
+ }
+
+ mPeakUsage += size;
+ }
+
+ mUsage = newUsage;
+ return NS_OK;
+}
+
+nsresult LSSnapshot::Checkpoint(bool aSync) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ if (mHasOtherProcessObservers) {
+ MOZ_ASSERT(mWriteAndNotifyInfos);
+
+ if (!mWriteAndNotifyInfos->IsEmpty()) {
+ if (aSync) {
+ MOZ_ALWAYS_TRUE(
+ mActor->SendSyncCheckpointAndNotify(*mWriteAndNotifyInfos));
+ } else {
+ MOZ_ALWAYS_TRUE(
+ mActor->SendAsyncCheckpointAndNotify(*mWriteAndNotifyInfos));
+ }
+
+ mWriteAndNotifyInfos->Clear();
+ }
+ } else {
+ MOZ_ASSERT(mWriteOptimizer);
+
+ if (mWriteOptimizer->HasWrites()) {
+ nsTArray<LSWriteInfo> writeInfos;
+ mWriteOptimizer->Enumerate(writeInfos);
+
+ MOZ_ASSERT(!writeInfos.IsEmpty());
+
+ if (aSync) {
+ MOZ_ALWAYS_TRUE(mActor->SendSyncCheckpoint(writeInfos));
+ } else {
+ MOZ_ALWAYS_TRUE(mActor->SendAsyncCheckpoint(writeInfos));
+ }
+
+ mWriteOptimizer->Reset();
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult LSSnapshot::Finish(bool aSync) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mDatabase);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ if (aSync) {
+ MOZ_ALWAYS_TRUE(mActor->SendSyncFinish());
+ } else {
+ MOZ_ALWAYS_TRUE(mActor->SendAsyncFinish());
+ }
+
+ mDatabase->NoteFinishedSnapshot(this);
+
+#ifdef DEBUG
+ mSentFinish = true;
+#endif
+
+ // Clear the self reference added in Init method.
+ MOZ_ASSERT(mSelfRef);
+ mSelfRef = nullptr;
+
+ return NS_OK;
+}
+
+void LSSnapshot::CancelIdleTimer() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mIdleTimer);
+
+ if (mHasPendingIdleTimerCallback) {
+ MOZ_ALWAYS_SUCCEEDS(mIdleTimer->Cancel());
+ mHasPendingIdleTimerCallback = false;
+ }
+}
+
+// static
+void LSSnapshot::IdleTimerCallback(nsITimer* aTimer, void* aClosure) {
+ MOZ_ASSERT(aTimer);
+
+ auto* self = static_cast<LSSnapshot*>(aClosure);
+ MOZ_ASSERT(self);
+ MOZ_ASSERT(self->mIdleTimer);
+ MOZ_ASSERT(SameCOMIdentity(self->mIdleTimer, aTimer));
+ MOZ_ASSERT(!self->mHasPendingStableStateCallback);
+ MOZ_ASSERT(self->mHasPendingIdleTimerCallback);
+
+ self->mHasPendingIdleTimerCallback = false;
+
+ MOZ_ALWAYS_SUCCEEDS(self->Finish());
+}
+
+NS_IMPL_ISUPPORTS(LSSnapshot, nsIRunnable)
+
+NS_IMETHODIMP
+LSSnapshot::Run() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mExplicit);
+ MOZ_ASSERT(mHasPendingStableStateCallback);
+ MOZ_ASSERT(!mHasPendingIdleTimerCallback);
+
+ mHasPendingStableStateCallback = false;
+
+ MOZ_ALWAYS_SUCCEEDS(Checkpoint());
+
+ // 1. The unused pre-incremented snapshot peak usage can't be undone when
+ // there are other snapshots for the same database. We only add a pending
+ // usage delta when a snapshot finishes and usage deltas are then applied
+ // when the last database becomes inactive.
+ // 2. If there's a snapshot with pre-incremented peak usage, the next
+ // snapshot will use that as a base for its usage.
+ // 3. When a task for given snapshot finishes, we try to reuse the snapshot
+ // by only checkpointing the snapshot and delaying the finish by a timer.
+ // 4. If two or more tabs for the same origin use localStorage periodically
+ // at the same time the usage gradually grows until it hits the quota
+ // limit.
+ // 5. We prevent that from happening by finishing the snapshot immediatelly
+ // if there are databases in other processess.
+
+ if (mDirty || mHasOtherProcessDatabases ||
+ !Preferences::GetBool("dom.storage.snapshot_reusing")) {
+ MOZ_ALWAYS_SUCCEEDS(Finish());
+ } else {
+ MOZ_ASSERT(mIdleTimer);
+
+ MOZ_ALWAYS_SUCCEEDS(mIdleTimer->InitWithNamedFuncCallback(
+ IdleTimerCallback, this,
+ StaticPrefs::dom_storage_snapshot_idle_timeout_ms(),
+ nsITimer::TYPE_ONE_SHOT, "LSSnapshot::IdleTimerCallback"));
+
+ mHasPendingIdleTimerCallback = true;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/localstorage/LSSnapshot.h b/dom/localstorage/LSSnapshot.h
new file mode 100644
index 0000000000..c12a4efce2
--- /dev/null
+++ b/dom/localstorage/LSSnapshot.h
@@ -0,0 +1,193 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_LSSnapshot_h
+#define mozilla_dom_localstorage_LSSnapshot_h
+
+#include <cstdint>
+#include <cstdlib>
+#include "ErrorList.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+#include "nsTHashMap.h"
+#include "nsHashKeys.h"
+#include "nsIRunnable.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsTArrayForwardDeclare.h"
+#include "nsTHashSet.h"
+
+class nsITimer;
+
+namespace mozilla::dom {
+
+class LSDatabase;
+class LSNotifyInfo;
+class LSSnapshotChild;
+class LSSnapshotInitInfo;
+class LSWriteAndNotifyInfo;
+class SnapshotWriteOptimizer;
+
+template <typename>
+class Optional;
+
+class LSSnapshot final : public nsIRunnable {
+ public:
+ /**
+ * The LoadState expresses what subset of information a snapshot has from the
+ * authoritative Datastore in the parent process. The initial snapshot is
+ * populated heuristically based on the size of the keys and size of the items
+ * (inclusive of the key value; item is key+value, not just value) of the
+ * entire datastore relative to the configured prefill limit (via pref
+ * "dom.storage.snapshot_prefill" exposed as gSnapshotPrefill in bytes).
+ *
+ * If there's less data than the limit, we send both keys and values and end
+ * up as AllOrderedItems. If there's enough room for all the keys but not
+ * all the values, we end up as AllOrderedKeys with as many values present as
+ * would fit. If there's not enough room for all the keys, then we end up as
+ * Partial with as many key-value pairs as will fit.
+ *
+ * The state AllUnorderedItems can only be reached by code getting items one
+ * by one.
+ */
+ enum class LoadState {
+ /**
+ * Class constructed, Init(LSSnapshotInitInfo) has not been invoked yet.
+ */
+ Initial,
+ /**
+ * Some keys and their values are known.
+ */
+ Partial,
+ /**
+ * All the keys are known in order, but some values are unknown.
+ */
+ AllOrderedKeys,
+ /**
+ * All keys and their values are known, but in an arbitrary order.
+ */
+ AllUnorderedItems,
+ /**
+ * All keys and their values are known and are present in their canonical
+ * order. This is everything, and is the preferred case. The initial
+ * population will send this info when the size of all items is less than
+ * the prefill threshold.
+ *
+ * mValues will contain all keys and values, mLoadedItems and mUnknownItems
+ * are unused.
+ */
+ AllOrderedItems,
+ EndGuard
+ };
+
+ private:
+ RefPtr<LSSnapshot> mSelfRef;
+
+ RefPtr<LSDatabase> mDatabase;
+
+ nsCOMPtr<nsITimer> mIdleTimer;
+
+ LSSnapshotChild* mActor;
+
+ nsTHashSet<nsString> mLoadedItems;
+ nsTHashSet<nsString> mUnknownItems;
+ nsTHashMap<nsStringHashKey, nsString> mValues;
+ UniquePtr<SnapshotWriteOptimizer> mWriteOptimizer;
+ UniquePtr<nsTArray<LSWriteAndNotifyInfo>> mWriteAndNotifyInfos;
+
+ uint32_t mInitLength;
+ uint32_t mLength;
+ int64_t mUsage;
+ int64_t mPeakUsage;
+
+ LoadState mLoadState;
+
+ bool mHasOtherProcessDatabases;
+ bool mHasOtherProcessObservers;
+ bool mExplicit;
+ bool mHasPendingStableStateCallback;
+ bool mHasPendingIdleTimerCallback;
+ bool mDirty;
+
+#ifdef DEBUG
+ bool mInitialized;
+ bool mSentFinish;
+#endif
+
+ public:
+ explicit LSSnapshot(LSDatabase* aDatabase);
+
+ void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(LSSnapshot); }
+
+ void SetActor(LSSnapshotChild* aActor);
+
+ void ClearActor() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+
+ mActor = nullptr;
+ }
+
+ bool Explicit() const { return mExplicit; }
+
+ nsresult Init(const nsAString& aKey, const LSSnapshotInitInfo& aInitInfo,
+ bool aExplicit);
+
+ nsresult GetLength(uint32_t* aResult);
+
+ nsresult GetKey(uint32_t aIndex, nsAString& aResult);
+
+ nsresult GetItem(const nsAString& aKey, nsAString& aResult);
+
+ nsresult GetKeys(nsTArray<nsString>& aKeys);
+
+ nsresult SetItem(const nsAString& aKey, const nsAString& aValue,
+ LSNotifyInfo& aNotifyInfo);
+
+ nsresult RemoveItem(const nsAString& aKey, LSNotifyInfo& aNotifyInfo);
+
+ nsresult Clear(LSNotifyInfo& aNotifyInfo);
+
+ void MarkDirty();
+
+ nsresult ExplicitCheckpoint();
+
+ nsresult ExplicitEnd();
+
+ int64_t GetUsage() const;
+
+ private:
+ ~LSSnapshot();
+
+ void ScheduleStableStateCallback();
+
+ void MaybeScheduleStableStateCallback();
+
+ nsresult GetItemInternal(const nsAString& aKey,
+ const Optional<nsString>& aValue,
+ nsAString& aResult);
+
+ nsresult EnsureAllKeys();
+
+ nsresult UpdateUsage(int64_t aDelta);
+
+ nsresult Checkpoint(bool aSync = false);
+
+ nsresult Finish(bool aSync = false);
+
+ void CancelIdleTimer();
+
+ static void IdleTimerCallback(nsITimer* aTimer, void* aClosure);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIRUNNABLE
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_localstorage_LSSnapshot_h
diff --git a/dom/localstorage/LSValue.cpp b/dom/localstorage/LSValue.cpp
new file mode 100644
index 0000000000..03637fdeba
--- /dev/null
+++ b/dom/localstorage/LSValue.cpp
@@ -0,0 +1,202 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "mozilla/dom/LSValue.h"
+
+#include "mozIStorageStatement.h"
+#include "mozilla/dom/SnappyUtils.h"
+#include "mozilla/fallible.h"
+#include "mozilla/TextUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+bool PutStringBytesToCString(const nsAString& aSrc, nsCString& aDest) {
+ const char16_t* bufferData;
+ const size_t byteLength = sizeof(char16_t) * aSrc.GetData(&bufferData);
+
+ char* destDataPtr;
+ const auto newLength = aDest.GetMutableData(&destDataPtr, byteLength);
+ if (newLength != byteLength) {
+ return false;
+ }
+ std::memcpy(static_cast<void*>(destDataPtr),
+ static_cast<const void*>(bufferData), byteLength);
+
+ return true;
+}
+
+template <class T>
+using TypeBufferResult = Result<std::pair<T, nsCString>, nsresult>;
+
+} // namespace
+
+bool PutCStringBytesToString(const nsACString& aSrc, nsString& aDest) {
+ const char* bufferData;
+ const size_t byteLength = aSrc.GetData(&bufferData);
+ const size_t shortLength = byteLength / sizeof(char16_t);
+
+ char16_t* destDataPtr;
+ const auto newLength = aDest.GetMutableData(&destDataPtr, shortLength);
+ if (newLength != shortLength) {
+ return false;
+ }
+
+ std::memcpy(static_cast<void*>(destDataPtr),
+ static_cast<const void*>(bufferData), byteLength);
+ return true;
+}
+
+LSValue::Converter::Converter(const LSValue& aValue) {
+ using ConversionType = LSValue::ConversionType;
+ using CompressionType = LSValue::CompressionType;
+
+ if (aValue.mBuffer.IsVoid()) {
+ mBuffer.SetIsVoid(true);
+ return;
+ }
+
+ const CompressionType compressionType = aValue.GetCompressionType();
+ const ConversionType conversionType = aValue.GetConversionType();
+
+ const nsCString uncompressed = [compressionType, &aValue]() {
+ if (CompressionType::UNCOMPRESSED != compressionType) {
+ nsCString buffer;
+ MOZ_ASSERT(CompressionType::SNAPPY == compressionType);
+ if (NS_WARN_IF(!SnappyUncompress(aValue.mBuffer, buffer))) {
+ buffer.Truncate();
+ }
+ return buffer;
+ }
+
+ return aValue.mBuffer;
+ }();
+
+ if (ConversionType::NONE != conversionType) {
+ MOZ_ASSERT(ConversionType::UTF16_UTF8 == conversionType);
+ if (NS_WARN_IF(!CopyUTF8toUTF16(uncompressed, mBuffer, fallible))) {
+ mBuffer.SetIsVoid(true);
+ }
+ return;
+ }
+
+ if (NS_WARN_IF(!PutCStringBytesToString(uncompressed, mBuffer))) {
+ mBuffer.SetIsVoid(true);
+ }
+}
+
+bool LSValue::InitFromString(const nsAString& aBuffer) {
+ MOZ_ASSERT(mBuffer.IsVoid());
+ MOZ_ASSERT(!mUTF16Length);
+ MOZ_ASSERT(ConversionType::NONE == mConversionType);
+ MOZ_ASSERT(CompressionType::UNCOMPRESSED == mCompressionType);
+
+ if (aBuffer.IsVoid()) {
+ return true;
+ }
+
+ const uint32_t utf16Length = aBuffer.Length();
+
+ const auto conversionRes = [&aBuffer]() -> TypeBufferResult<ConversionType> {
+ nsCString converted;
+
+ if (Utf16ValidUpTo(aBuffer) == aBuffer.Length()) {
+ if (NS_WARN_IF(!CopyUTF16toUTF8(aBuffer, converted, fallible))) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ return std::pair{ConversionType::UTF16_UTF8, std::move(converted)};
+ }
+
+ if (NS_WARN_IF(!PutStringBytesToCString(aBuffer, converted))) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ return std::pair{ConversionType::NONE, std::move(converted)};
+ }();
+
+ if (conversionRes.isErr()) {
+ return false;
+ }
+
+ const auto& [conversionType, converted] = conversionRes.inspect();
+
+ const auto compressionRes =
+ [&converted = converted]() -> TypeBufferResult<CompressionType> {
+ nsCString compressed;
+ if (NS_WARN_IF(!SnappyCompress(converted, compressed))) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ if (!compressed.IsVoid()) {
+ return std::pair{CompressionType::SNAPPY, std::move(compressed)};
+ }
+
+ compressed = converted;
+ return std::pair{CompressionType::UNCOMPRESSED, std::move(compressed)};
+ }();
+
+ if (compressionRes.isErr()) {
+ return false;
+ }
+
+ const auto& [compressionType, compressed] = compressionRes.inspect();
+
+ mBuffer = compressed;
+ mUTF16Length = utf16Length;
+ mConversionType = conversionType;
+ mCompressionType = compressionType;
+
+ return true;
+}
+
+nsresult LSValue::InitFromStatement(mozIStorageStatement* aStatement,
+ uint32_t aIndex) {
+ MOZ_ASSERT(aStatement);
+ MOZ_ASSERT(mBuffer.IsVoid());
+ MOZ_ASSERT(!mUTF16Length);
+ MOZ_ASSERT(ConversionType::NONE == mConversionType);
+ MOZ_ASSERT(CompressionType::UNCOMPRESSED == mCompressionType);
+
+ int32_t utf16Length;
+ nsresult rv = aStatement->GetInt32(aIndex, &utf16Length);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ int32_t conversionType;
+ rv = aStatement->GetInt32(aIndex + 1, &conversionType);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ int32_t compressionType;
+ rv = aStatement->GetInt32(aIndex + 2, &compressionType);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCString buffer;
+ rv = aStatement->GetBlobAsUTF8String(aIndex + 3, buffer);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mBuffer = buffer;
+ mUTF16Length = static_cast<uint32_t>(utf16Length);
+ mConversionType = static_cast<decltype(mConversionType)>(conversionType);
+ mCompressionType = static_cast<decltype(mCompressionType)>(compressionType);
+
+ return NS_OK;
+}
+
+const LSValue& VoidLSValue() {
+ static const LSValue sVoidLSValue;
+
+ return sVoidLSValue;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/localstorage/LSValue.h b/dom/localstorage/LSValue.h
new file mode 100644
index 0000000000..c1b362a50f
--- /dev/null
+++ b/dom/localstorage/LSValue.h
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_LSValue_h
+#define mozilla_dom_localstorage_LSValue_h
+
+#include <cstdint>
+#include "ErrorList.h"
+#include "SnappyUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Span.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTStringRepr.h"
+
+class mozIStorageStatement;
+
+namespace IPC {
+template <typename>
+struct ParamTraits;
+}
+
+namespace mozilla::dom {
+
+/**
+ * Represents a LocalStorage value. From content's perspective, values (if
+ * present) are always DOMStrings. This is also true from a quota-tracking
+ * perspective. However, for memory and disk efficiency it's preferable to store
+ * the value in alternate compressed or utf-8 encoding representations. The
+ * LSValue type exists to support these alternate representations, dynamically
+ * decompressing/re-encoding to utf-16 while still tracking value size on a
+ * utf-16 basis for quota purposes.
+ */
+class LSValue final {
+ friend struct IPC::ParamTraits<LSValue>;
+
+ public:
+ enum class ConversionType : uint8_t {
+ NONE = 0u,
+ UTF16_UTF8 = 1u,
+ NUM_TYPES = 2u
+ };
+
+ enum class CompressionType : uint8_t {
+ UNCOMPRESSED = 0u,
+ SNAPPY = 1u,
+ NUM_TYPES = 2u
+ };
+
+ nsCString mBuffer;
+ uint32_t mUTF16Length;
+ ConversionType mConversionType;
+ CompressionType mCompressionType;
+
+ explicit LSValue()
+ : mUTF16Length(0u),
+ mConversionType(ConversionType::NONE),
+ mCompressionType(CompressionType::UNCOMPRESSED) {
+ SetIsVoid(true);
+ }
+
+ bool InitFromString(const nsAString& aBuffer);
+
+ nsresult InitFromStatement(mozIStorageStatement* aStatement, uint32_t aIndex);
+
+ bool IsVoid() const { return mBuffer.IsVoid(); }
+
+ void SetIsVoid(bool aVal) { mBuffer.SetIsVoid(aVal); }
+
+ /**
+ * This represents the "physical" length that the parent process uses for
+ * the size of value/item computation. This can also be used to see how much
+ * memory the value is using at rest or what the cost is for sending the value
+ * over IPC.
+ */
+ uint32_t Length() const { return mBuffer.Length(); }
+
+ /*
+ * This represents the "logical" length that content sees and that is also
+ * used for quota management purposes.
+ */
+ uint32_t UTF16Length() const { return mUTF16Length; }
+
+ ConversionType GetConversionType() const { return mConversionType; }
+
+ CompressionType GetCompressionType() const { return mCompressionType; }
+
+ bool Equals(const LSValue& aOther) const {
+ return mBuffer == aOther.mBuffer &&
+ mBuffer.IsVoid() == aOther.mBuffer.IsVoid() &&
+ mUTF16Length == aOther.mUTF16Length &&
+ mConversionType == aOther.mConversionType &&
+ mCompressionType == aOther.mCompressionType;
+ }
+
+ bool operator==(const LSValue& aOther) const { return Equals(aOther); }
+
+ bool operator!=(const LSValue& aOther) const { return !Equals(aOther); }
+
+ constexpr const nsCString& AsCString() const { return mBuffer; }
+
+ class Converter {
+ nsString mBuffer;
+
+ public:
+ explicit Converter(const LSValue& aValue);
+ Converter(Converter&& aOther) = default;
+ ~Converter() = default;
+
+ operator const nsString&() const { return mBuffer; }
+
+ private:
+ Converter() = delete;
+ Converter(const Converter&) = delete;
+ Converter& operator=(const Converter&) = delete;
+ Converter& operator=(const Converter&&) = delete;
+ };
+
+ Converter AsString() const { return Converter{*this}; }
+};
+
+const LSValue& VoidLSValue();
+
+/**
+ * XXX: This function doesn't have to be public
+ * once the support for shadow writes is removed.
+ */
+bool PutCStringBytesToString(const nsACString& aSrc, nsString& aDest);
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_localstorage_LSValue_h
diff --git a/dom/localstorage/LSWriteOptimizer.cpp b/dom/localstorage/LSWriteOptimizer.cpp
new file mode 100644
index 0000000000..0928dca35e
--- /dev/null
+++ b/dom/localstorage/LSWriteOptimizer.cpp
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "LSWriteOptimizer.h"
+
+#include <new>
+#include "nsBaseHashtable.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom {
+
+class LSWriteOptimizerBase::WriteInfoComparator {
+ public:
+ bool Equals(const WriteInfo* a, const WriteInfo* b) const {
+ MOZ_ASSERT(a && b);
+ return a->SerialNumber() == b->SerialNumber();
+ }
+
+ bool LessThan(const WriteInfo* a, const WriteInfo* b) const {
+ MOZ_ASSERT(a && b);
+ return a->SerialNumber() < b->SerialNumber();
+ }
+};
+
+void LSWriteOptimizerBase::DeleteItem(const nsAString& aKey, int64_t aDelta) {
+ AssertIsOnOwningThread();
+
+ mWriteInfos.WithEntryHandle(aKey, [&](auto&& entry) {
+ if (entry && entry.Data()->GetType() == WriteInfo::InsertItem) {
+ entry.Remove();
+ } else {
+ entry.InsertOrUpdate(
+ MakeUnique<DeleteItemInfo>(NextSerialNumber(), aKey));
+ }
+ });
+
+ mTotalDelta += aDelta;
+}
+
+void LSWriteOptimizerBase::Truncate(int64_t aDelta) {
+ AssertIsOnOwningThread();
+
+ mWriteInfos.Clear();
+
+ if (!mTruncateInfo) {
+ mTruncateInfo = MakeUnique<TruncateInfo>(NextSerialNumber());
+ }
+
+ mTotalDelta += aDelta;
+}
+
+void LSWriteOptimizerBase::GetSortedWriteInfos(
+ nsTArray<NotNull<WriteInfo*>>& aWriteInfos) {
+ AssertIsOnOwningThread();
+
+ if (mTruncateInfo) {
+ aWriteInfos.InsertElementSorted(WrapNotNullUnchecked(mTruncateInfo.get()),
+ WriteInfoComparator());
+ }
+
+ for (const auto& entry : mWriteInfos) {
+ WriteInfo* writeInfo = entry.GetWeak();
+
+ aWriteInfos.InsertElementSorted(WrapNotNull(writeInfo),
+ WriteInfoComparator());
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/localstorage/LSWriteOptimizer.h b/dom/localstorage/LSWriteOptimizer.h
new file mode 100644
index 0000000000..353f8c17b5
--- /dev/null
+++ b/dom/localstorage/LSWriteOptimizer.h
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_LSWriteOptimizer_h
+#define mozilla_dom_localstorage_LSWriteOptimizer_h
+
+#include <cstdint>
+#include <utility>
+#include "mozilla/Assertions.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/UniquePtr.h"
+#include "nsClassHashtable.h"
+#include "nsHashKeys.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsTArrayForwardDeclare.h"
+
+namespace mozilla::dom {
+
+/**
+ * Base class for coalescing manipulation queue.
+ */
+class LSWriteOptimizerBase {
+ class WriteInfoComparator;
+
+ protected:
+ class WriteInfo;
+ class DeleteItemInfo;
+ class TruncateInfo;
+
+ UniquePtr<WriteInfo> mTruncateInfo;
+ nsClassHashtable<nsStringHashKey, WriteInfo> mWriteInfos;
+ CheckedUint64 mLastSerialNumber;
+ int64_t mTotalDelta;
+
+ NS_DECL_OWNINGTHREAD
+
+ public:
+ LSWriteOptimizerBase() : mLastSerialNumber(0), mTotalDelta(0) {}
+
+ LSWriteOptimizerBase(LSWriteOptimizerBase&& aWriteOptimizer)
+ : mTruncateInfo(std::move(aWriteOptimizer.mTruncateInfo)) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(&aWriteOptimizer != this);
+
+ mWriteInfos.SwapElements(aWriteOptimizer.mWriteInfos);
+ mTotalDelta = aWriteOptimizer.mTotalDelta;
+ aWriteOptimizer.mTotalDelta = 0;
+ }
+
+ void AssertIsOnOwningThread() const {
+ NS_ASSERT_OWNINGTHREAD(LSWriteOptimizerBase);
+ }
+
+ void DeleteItem(const nsAString& aKey, int64_t aDelta = 0);
+
+ void Truncate(int64_t aDelta = 0);
+
+ bool HasWrites() const {
+ AssertIsOnOwningThread();
+
+ return mTruncateInfo || !mWriteInfos.IsEmpty();
+ }
+
+ void Reset() {
+ AssertIsOnOwningThread();
+
+ mTruncateInfo = nullptr;
+ mWriteInfos.Clear();
+ }
+
+ protected:
+ uint64_t NextSerialNumber() {
+ AssertIsOnOwningThread();
+
+ mLastSerialNumber++;
+
+ MOZ_ASSERT(mLastSerialNumber.isValid());
+
+ return mLastSerialNumber.value();
+ }
+
+ /**
+ * This method can be used by derived classes to get a sorted list of write
+ * infos. Write infos are sorted by the serial number.
+ */
+ void GetSortedWriteInfos(nsTArray<NotNull<WriteInfo*>>& aWriteInfos);
+};
+
+/**
+ * Base class for specific mutations.
+ */
+class LSWriteOptimizerBase::WriteInfo {
+ uint64_t mSerialNumber;
+
+ public:
+ WriteInfo(uint64_t aSerialNumber) : mSerialNumber(aSerialNumber) {}
+
+ virtual ~WriteInfo() = default;
+
+ uint64_t SerialNumber() const { return mSerialNumber; }
+
+ enum Type { InsertItem = 0, UpdateItem, DeleteItem, Truncate };
+
+ virtual Type GetType() const = 0;
+};
+
+class LSWriteOptimizerBase::DeleteItemInfo final : public WriteInfo {
+ nsString mKey;
+
+ public:
+ DeleteItemInfo(uint64_t aSerialNumber, const nsAString& aKey)
+ : WriteInfo(aSerialNumber), mKey(aKey) {}
+
+ const nsAString& GetKey() const { return mKey; }
+
+ private:
+ Type GetType() const override { return DeleteItem; }
+};
+
+/**
+ * Truncate mutation.
+ */
+class LSWriteOptimizerBase::TruncateInfo final : public WriteInfo {
+ public:
+ explicit TruncateInfo(uint64_t aSerialNumber) : WriteInfo(aSerialNumber) {}
+
+ private:
+ Type GetType() const override { return Truncate; }
+};
+
+/**
+ * Coalescing manipulation queue.
+ */
+template <typename T, typename U = T>
+class LSWriteOptimizer;
+
+template <typename T, typename U>
+class LSWriteOptimizer : public LSWriteOptimizerBase {
+ protected:
+ class InsertItemInfo;
+ class UpdateItemInfo;
+
+ public:
+ void InsertItem(const nsAString& aKey, const T& aValue, int64_t aDelta = 0);
+
+ void UpdateItem(const nsAString& aKey, const T& aValue, int64_t aDelta = 0);
+};
+
+/**
+ * Insert mutation (the key did not previously exist).
+ */
+template <typename T, typename U>
+class LSWriteOptimizer<T, U>::InsertItemInfo : public WriteInfo {
+ nsString mKey;
+ U mValue;
+
+ public:
+ InsertItemInfo(uint64_t aSerialNumber, const nsAString& aKey, const T& aValue)
+ : WriteInfo(aSerialNumber), mKey(aKey), mValue(aValue) {}
+
+ const nsAString& GetKey() const { return mKey; }
+
+ const T& GetValue() const { return mValue; }
+
+ private:
+ WriteInfo::Type GetType() const override { return InsertItem; }
+};
+
+/**
+ * Update mutation (the key already existed).
+ */
+template <typename T, typename U>
+class LSWriteOptimizer<T, U>::UpdateItemInfo final : public InsertItemInfo {
+ bool mUpdateWithMove;
+
+ public:
+ UpdateItemInfo(uint64_t aSerialNumber, const nsAString& aKey, const T& aValue,
+ bool aUpdateWithMove)
+ : InsertItemInfo(aSerialNumber, aKey, aValue),
+ mUpdateWithMove(aUpdateWithMove) {}
+
+ bool UpdateWithMove() const { return mUpdateWithMove; }
+
+ private:
+ WriteInfo::Type GetType() const override { return WriteInfo::UpdateItem; }
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_localstorage_LSWriteOptimizer_h
diff --git a/dom/localstorage/LSWriteOptimizerImpl.h b/dom/localstorage/LSWriteOptimizerImpl.h
new file mode 100644
index 0000000000..32ccff6c63
--- /dev/null
+++ b/dom/localstorage/LSWriteOptimizerImpl.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_LSWriteOptimizerImpl_h
+#define mozilla_dom_localstorage_LSWriteOptimizerImpl_h
+
+#include "LSWriteOptimizer.h"
+
+namespace mozilla::dom {
+
+template <typename T, typename U>
+void LSWriteOptimizer<T, U>::InsertItem(const nsAString& aKey, const T& aValue,
+ int64_t aDelta) {
+ AssertIsOnOwningThread();
+
+ mWriteInfos.WithEntryHandle(aKey, [&](auto&& entry) {
+ if (entry && entry.Data()->GetType() == WriteInfo::DeleteItem) {
+ // We could just simply replace the deletion with ordinary update, but
+ // that would preserve item's original position/index. Imagine a case when
+ // we have only one existing key k1. Now let's create a new optimizer and
+ // remove k1, add k2 and add k1 back. The final order should be k2, k1
+ // (ordinary update would produce k1, k2). So we need to differentiate
+ // between normal update and "optimized" update which resulted from a
+ // deletion followed by an insertion. We use the UpdateWithMove flag for
+ // this.
+
+ entry.Update(MakeUnique<UpdateItemInfo>(NextSerialNumber(), aKey, aValue,
+ /* aUpdateWithMove */ true));
+ } else {
+ entry.InsertOrUpdate(
+ MakeUnique<InsertItemInfo>(NextSerialNumber(), aKey, aValue));
+ }
+ });
+
+ mTotalDelta += aDelta;
+}
+
+template <typename T, typename U>
+void LSWriteOptimizer<T, U>::UpdateItem(const nsAString& aKey, const T& aValue,
+ int64_t aDelta) {
+ AssertIsOnOwningThread();
+
+ mWriteInfos.WithEntryHandle(aKey, [&](auto&& entry) {
+ if (entry && entry.Data()->GetType() == WriteInfo::InsertItem) {
+ entry.Update(
+ MakeUnique<InsertItemInfo>(NextSerialNumber(), aKey, aValue));
+ } else {
+ entry.InsertOrUpdate(
+ MakeUnique<UpdateItemInfo>(NextSerialNumber(), aKey, aValue,
+ /* aUpdateWithMove */ false));
+ }
+ });
+
+ mTotalDelta += aDelta;
+}
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_localstorage_LSWriteOptimizerImpl_h
diff --git a/dom/localstorage/LocalStorageCommon.cpp b/dom/localstorage/LocalStorageCommon.cpp
new file mode 100644
index 0000000000..a5b01e0146
--- /dev/null
+++ b/dom/localstorage/LocalStorageCommon.cpp
@@ -0,0 +1,160 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "LocalStorageCommon.h"
+
+#include <cstdint>
+#include "MainThreadUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Logging.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/StorageUtils.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "mozilla/net/MozURL.h"
+#include "mozilla/net/WebSocketFrame.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsPrintfCString.h"
+#include "nsString.h"
+#include "nsStringFlags.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla::dom {
+
+using namespace mozilla::net;
+
+namespace {
+
+StaticMutex gNextGenLocalStorageMutex;
+Atomic<int32_t> gNextGenLocalStorageEnabled(-1);
+LazyLogModule gLogger("LocalStorage");
+
+} // namespace
+
+const char16_t* kLocalStorageType = u"localStorage";
+
+bool NextGenLocalStorageEnabled() {
+ if (XRE_IsParentProcess()) {
+ StaticMutexAutoLock lock(gNextGenLocalStorageMutex);
+
+ if (gNextGenLocalStorageEnabled == -1) {
+ // Ideally all this Mutex stuff would be replaced with just using
+ // an AtStartup StaticPref, but there are concerns about this causing
+ // deadlocks if this access needs to init the AtStartup cache.
+ bool enabled =
+ !StaticPrefs::
+ dom_storage_enable_unsupported_legacy_implementation_DoNotUseDirectly();
+
+ gNextGenLocalStorageEnabled = enabled ? 1 : 0;
+ }
+
+ return !!gNextGenLocalStorageEnabled;
+ }
+
+ return CachedNextGenLocalStorageEnabled();
+}
+
+void RecvInitNextGenLocalStorageEnabled(const bool aEnabled) {
+ MOZ_ASSERT(!XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(gNextGenLocalStorageEnabled == -1);
+
+ gNextGenLocalStorageEnabled = aEnabled ? 1 : 0;
+}
+
+bool CachedNextGenLocalStorageEnabled() {
+ MOZ_ASSERT(gNextGenLocalStorageEnabled != -1);
+
+ return !!gNextGenLocalStorageEnabled;
+}
+
+Result<std::pair<nsCString, nsCString>, nsresult> GenerateOriginKey2(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo) {
+ OriginAttributes attrs;
+ nsCString spec;
+
+ switch (aPrincipalInfo.type()) {
+ case mozilla::ipc::PrincipalInfo::TNullPrincipalInfo: {
+ const auto& info = aPrincipalInfo.get_NullPrincipalInfo();
+
+ attrs = info.attrs();
+ spec = info.spec();
+
+ break;
+ }
+
+ case mozilla::ipc::PrincipalInfo::TContentPrincipalInfo: {
+ const auto& info = aPrincipalInfo.get_ContentPrincipalInfo();
+
+ attrs = info.attrs();
+ spec = info.spec();
+
+ break;
+ }
+
+ default: {
+ spec.SetIsVoid(true);
+
+ break;
+ }
+ }
+
+ if (spec.IsVoid()) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ nsCString originAttrSuffix;
+ attrs.CreateSuffix(originAttrSuffix);
+
+ RefPtr<MozURL> specURL;
+ QM_TRY(MOZ_TO_RESULT(MozURL::Init(getter_AddRefs(specURL), spec)));
+
+ nsCString host(specURL->Host());
+ uint32_t length = host.Length();
+ if (length > 0 && host.CharAt(0) == '[' && host.CharAt(length - 1) == ']') {
+ host = Substring(host, 1, length - 2);
+ }
+
+ nsCString domainOrigin(host);
+
+ if (domainOrigin.IsEmpty()) {
+ // For the file:/// protocol use the exact directory as domain.
+ if (specURL->Scheme().EqualsLiteral("file")) {
+ domainOrigin.Assign(specURL->Directory());
+ }
+ }
+
+ // Append reversed domain
+ nsAutoCString reverseDomain;
+ nsresult rv = StorageUtils::CreateReversedDomain(domainOrigin, reverseDomain);
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+
+ nsCString originKey = reverseDomain;
+
+ // Append scheme
+ originKey.Append(':');
+ originKey.Append(specURL->Scheme());
+
+ // Append port if any
+ int32_t port = specURL->RealPort();
+ if (port != -1) {
+ originKey.AppendPrintf(":%d", port);
+ }
+
+ return std::make_pair(std::move(originAttrSuffix), std::move(originKey));
+}
+
+LogModule* GetLocalStorageLogger() { return gLogger; }
+
+} // namespace mozilla::dom
diff --git a/dom/localstorage/LocalStorageCommon.h b/dom/localstorage/LocalStorageCommon.h
new file mode 100644
index 0000000000..408f7106a2
--- /dev/null
+++ b/dom/localstorage/LocalStorageCommon.h
@@ -0,0 +1,263 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_LocalStorageCommon_h
+#define mozilla_dom_localstorage_LocalStorageCommon_h
+
+#include <cstdint>
+#include "ErrorList.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "nsLiteralString.h"
+#include "nsStringFwd.h"
+
+/*
+ * Local storage
+ * ~~~~~~~~~~~~~
+ *
+ * Implementation overview
+ * ~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * The implementation is based on a per principal/origin cache (datastore)
+ * living in the main process and synchronous calls initiated from content
+ * processes.
+ * The IPC communication is managed by database actors which link to the
+ * datastore.
+ * The synchronous blocking of the main thread is done by using a special
+ * technique or by using standard synchronous IPC calls.
+ *
+ * General architecture
+ * ~~~~~~~~~~~~~~~~~~~~
+ * The current browser architecture consists of one main process and multiple
+ * content processes (there are other processes but for simplicity's sake, they
+ * are not mentioned here). The processes use the IPC communication to talk to
+ * each other. Local storage implementation uses the client-server model, so
+ * the main process manages all the data and content processes then request
+ * particular data from the main process. The main process is also called the
+ * parent or the parent side, the content process is then called the child or
+ * the child side.
+ *
+ * Datastores
+ * ~~~~~~~~~~
+ *
+ * A datastore provides a convenient way to access data for given origin. The
+ * data is always preloaded into memory and indexed using a hash table. This
+ * enables very fast access to particular stored items. There can be only one
+ * datastore per origin and exists solely on the parent side. It is represented
+ * by the "Datastore" class. A datastore instance is a ref counted object and
+ * lives on the PBackground thread, it is kept alive by database objects. When
+ * the last database object for given origin is destroyed, the associated
+ * datastore object is destroyed too.
+ *
+ * Databases
+ * ~~~~~~~~~
+ *
+ * A database allows direct access to a datastore from a content process. There
+ * can be multiple databases for the same origin, but they all share the same
+ * datastore.
+ * Databases use the PBackgroundLSDatabase IPDL protocol for IPC communication.
+ * Given the nature of local storage, most of PBackgroundLSDatabase messages
+ * are synchronous.
+ *
+ * On the parent side, the database is represented by the "Database" class that
+ * is a parent actor as well (implements the "PBackgroundLSDatabaseParent"
+ * interface). A database instance is a ref counted object and lives on the
+ * PBackground thread.
+ * All live database actors are tracked in an array.
+ *
+ * On the child side, the database is represented by the "LSDatabase" class
+ * that provides indirect access to a child actor. An LSDatabase instance is a
+ * ref counted object and lives on the main thread.
+ * The actual child actor is represented by the "LSDatabaseChild" class that
+ * implements the "PBackgroundLSDatabaseChild" interface. An "LSDatabaseChild"
+ * instance is not ref counted and lives on the main thread too.
+ *
+ * Synchronous blocking
+ * ~~~~~~~~~~~~~~~~~~~~
+ *
+ * Local storage is synchronous in nature which means the execution can't move
+ * forward until there's a reply for given method call.
+ * Since we have to use IPC anyway, we could just always use synchronous IPC
+ * messages for all local storage method calls. Well, there's a problem with
+ * that approach.
+ * If the main process needs to do some off PBackground thread stuff like
+ * getting info from principals on the main thread or some asynchronous stuff
+ * like directory locking before sending a reply to a synchronous message, then
+ * we would have to block the thread or spin the event loop which is usually a
+ * bad idea, especially in the main process.
+ * Instead, we can use a special thread in the content process called
+ * RemoteLazyInputStream thread for communication with the main process using
+ * asynchronous messages and synchronously block the main thread until the DOM
+ * File thread is done (the main thread blocking is a bit more complicated, see
+ * the comment in RequestHelper::StartAndReturnResponse for more details).
+ * Anyway, the extra hop to the RemoteLazyInputStream thread brings another
+ * overhead and latency. The final solution is to use a combination of the
+ * special thread for complex stuff like datastore preparation and synchronous
+ * IPC messages sent directly from the main thread for database access when data
+ * is already loaded from disk into memory.
+ *
+ * Requests
+ * ~~~~~~~~
+ *
+ * Requests are used to handle asynchronous high level datastore operations
+ * which are initiated in a content process and then processed in the parent
+ * process (for example, preparation of a datastore).
+ * Requests use the "PBackgroundLSRequest" IPDL protocol for IPC communication.
+ *
+ * On the parent side, the request is represented by the "LSRequestBase" class
+ * that is a parent actor as well (implements the "PBackgroundLSRequestParent"
+ * interface). It's an abstract class (contains pure virtual functions) so it
+ * can't be used to create instances.
+ * It also inherits from the "DatastoreOperationBase" class which is a generic
+ * base class for all datastore operations. The "DatastoreOperationsBase" class
+ * inherits from the "Runnable" class, so derived class instances are ref
+ * counted, can be dispatched to multiple threads and thus they are used on
+ * multiple threads. However, derived class instances can be created on the
+ * PBackground thread only.
+ *
+ * On the child side, the request is represented by the "RequestHelper" class
+ * that covers all the complexity needed to start a new request, handle
+ * responses and do safe main thread blocking at the same time.
+ * It inherits from the "Runnable" class, so instances are ref counted and
+ * they are internally used on multiple threads (specifically on the main
+ * thread and on the RemoteLazyInputStream thread). Anyway, users should create
+ * and use instances of this class only on the main thread.
+ * The actual child actor is represented by the "LSRequestChild" class that
+ * implements the "PBackgroundLSRequestChild" interface. An "LSRequestChild"
+ * instance is not ref counted and lives on the RemoteLazyInputStream thread.
+ * Request responses are passed using the "LSRequestChildCallback" interface.
+ *
+ * Preparation of a datastore
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * The datastore preparation is needed to make sure a datastore is fully loaded
+ * into memory. Every datastore preparation produces a unique id (even if the
+ * datastore for given origin already exists).
+ * On the parent side, the preparation is handled by the "PrepareDatastoreOp"
+ * class which inherits from the "LSRequestBase" class. The preparation process
+ * on the parent side is quite complicated, it happens sequentially on multiple
+ * threads and is managed by a state machine.
+ * On the child side, the preparation is done in the LSObject::EnsureDatabase
+ * method using the "RequestHelper" class. The method starts a new preparation
+ * request and obtains a unique id produced by the parent (or an error code if
+ * the requested failed to complete).
+ *
+ * Linking databases to a datastore
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * A datastore exists only on the parent side, but it can be accessed from the
+ * content via database actors. Database actors are initiated on the child side
+ * and they need to be linked to a datastore on the parent side via an id. The
+ * datastore preparation process gives us the required id.
+ * The linking is initiated on the child side in the LSObject::EnsureDatabase
+ * method by calling SendPBackgroundLSDatabaseConstructor and finished in
+ * RecvPBackgroundLSDatabaseConstructor on the parent side.
+ *
+ * Actor migration
+ * ~~~~~~~~~~~~~~~
+ *
+ * In theory, the datastore preparation request could return a database actor
+ * directly (instead of returning an id intended for database linking to a
+ * datastore). However, as it was explained above, the preparation must be done
+ * on the RemoteLazyInputStream thread and database objects are used on the main
+ * thread. The returned actor would have to be migrated from the
+ * RemoteLazyInputStream thread to the main thread and that's something which
+ * our IPDL doesn't support yet.
+ *
+ * Exposing local storage
+ * ~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * The implementation is exposed to the DOM via window.localStorage attribute.
+ * Local storage's sibling, session storage shares the same WebIDL interface
+ * for exposing it to web content, therefore there's an abstract class called
+ * "Storage" that handles some of the common DOM bindings stuff. Local storage
+ * specific functionality is defined in the "LSObject" derived class.
+ * The "LSObject" class is also a starting point for the datastore preparation
+ * and database linking.
+ *
+ * Local storage manager
+ * ~~~~~~~~~~~~~~~~~~~~~
+ *
+ * The local storage manager exposes some of the features that need to be
+ * available only in the chrome code or tests. The manager is represented by
+ * the "LocalStorageManager2" class that implements the "nsIDOMStorageManager"
+ * interface.
+ */
+
+namespace mozilla {
+
+class LogModule;
+
+namespace ipc {
+
+class PrincipalInfo;
+
+} // namespace ipc
+
+namespace dom {
+
+extern const char16_t* kLocalStorageType;
+
+/**
+ * Convenience data-structure to make it easier to track whether a value has
+ * changed and what its previous value was for notification purposes. Instances
+ * are created on the stack by LSObject and passed to LSDatabase which in turn
+ * passes them onto LSSnapshot for final updating/population. LSObject then
+ * generates an event, if appropriate.
+ */
+class MOZ_STACK_CLASS LSNotifyInfo {
+ bool mChanged;
+ nsString mOldValue;
+
+ public:
+ LSNotifyInfo() : mChanged(false) {}
+
+ bool changed() const { return mChanged; }
+
+ bool& changed() { return mChanged; }
+
+ const nsString& oldValue() const { return mOldValue; }
+
+ nsString& oldValue() { return mOldValue; }
+};
+
+/**
+ * A check of LSNG being enabled, the value is latched once initialized so
+ * changing the preference during runtime has no effect. May be called on any
+ * thread in the parent process, but you should call
+ * CachedNextGenLocalStorageEnabled if you know that NextGenLocalStorageEnabled
+ * was already called because it is faster. May be called on any thread in
+ * content processes, but you should call CachedNextGenLocalStorageEnabled
+ * directly if you know you are in a content process because it is slightly
+ * faster.
+ */
+bool NextGenLocalStorageEnabled();
+
+/**
+ * Called by ContentChild during content process initialization to initialize
+ * the global variable in the content process with the latched value in the
+ * parent process."
+ */
+void RecvInitNextGenLocalStorageEnabled(const bool aEnabled);
+
+/**
+ * Cached any-thread version of NextGenLocalStorageEnabled().
+ */
+bool CachedNextGenLocalStorageEnabled();
+
+/**
+ * Returns a success value containing a pair of origin attribute suffix and
+ * origin key.
+ */
+Result<std::pair<nsCString, nsCString>, nsresult> GenerateOriginKey2(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo);
+
+LogModule* GetLocalStorageLogger();
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_LocalStorageCommon_h
diff --git a/dom/localstorage/LocalStorageManager2.cpp b/dom/localstorage/LocalStorageManager2.cpp
new file mode 100644
index 0000000000..69e8c3b4dd
--- /dev/null
+++ b/dom/localstorage/LocalStorageManager2.cpp
@@ -0,0 +1,661 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "LocalStorageManager2.h"
+
+// Local includes
+#include "ActorsChild.h"
+#include "LSObject.h"
+
+// Global includes
+#include <utility>
+#include "MainThreadUtils.h"
+#include "jsapi.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/MacroForEach.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/RemoteLazyInputStreamThread.h"
+#include "mozilla/dom/LocalStorageCommon.h"
+#include "mozilla/dom/PBackgroundLSRequest.h"
+#include "mozilla/dom/PBackgroundLSSharedTypes.h"
+#include "mozilla/dom/PBackgroundLSSimpleRequest.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIEventTarget.h"
+#include "nsILocalStorageManager.h"
+#include "nsIPrincipal.h"
+#include "nsIRunnable.h"
+#include "nsPIDOMWindow.h"
+#include "nsStringFwd.h"
+#include "nsThreadUtils.h"
+#include "nscore.h"
+#include "xpcpublic.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+class AsyncRequestHelper final : public Runnable,
+ public LSRequestChildCallback {
+ enum class State {
+ /**
+ * The AsyncRequestHelper has been created and dispatched to the
+ * RemoteLazyInputStream Thread.
+ */
+ Initial,
+ /**
+ * Start() has been invoked on the RemoteLazyInputStream Thread and
+ * LocalStorageManager2::StartRequest has been invoked from there, sending
+ * an IPC message to PBackground to service the request. We stay in this
+ * state until a response is received.
+ */
+ ResponsePending,
+ /**
+ * A response has been received and AsyncRequestHelper has been dispatched
+ * back to the owning event target to call Finish().
+ */
+ Finishing,
+ /**
+ * Finish() has been called on the main thread. The promise will be resolved
+ * according to the received response.
+ */
+ Complete
+ };
+
+ // The object we are issuing a request on behalf of. Present because of the
+ // need to invoke LocalStorageManager2::StartRequest off the main thread.
+ // Dropped on return to the main-thread in Finish().
+ RefPtr<LocalStorageManager2> mManager;
+ // The thread the AsyncRequestHelper was created on. This should be the main
+ // thread.
+ nsCOMPtr<nsIEventTarget> mOwningEventTarget;
+ // The IPC actor handling the request with standard IPC allocation rules.
+ // Our reference is nulled in OnResponse which corresponds to the actor's
+ // __destroy__ method.
+ LSRequestChild* mActor;
+ RefPtr<Promise> mPromise;
+ const LSRequestParams mParams;
+ LSRequestResponse mResponse;
+ nsresult mResultCode;
+ State mState;
+
+ public:
+ AsyncRequestHelper(LocalStorageManager2* aManager, Promise* aPromise,
+ const LSRequestParams& aParams)
+ : Runnable("dom::LocalStorageManager2::AsyncRequestHelper"),
+ mManager(aManager),
+ mOwningEventTarget(GetCurrentEventTarget()),
+ mActor(nullptr),
+ mPromise(aPromise),
+ mParams(aParams),
+ mResultCode(NS_OK),
+ mState(State::Initial) {}
+
+ bool IsOnOwningThread() const {
+ MOZ_ASSERT(mOwningEventTarget);
+
+ bool current;
+ return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(&current)) &&
+ current;
+ }
+
+ void AssertIsOnOwningThread() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(IsOnOwningThread());
+ }
+
+ nsresult Dispatch();
+
+ private:
+ ~AsyncRequestHelper() = default;
+
+ nsresult Start();
+
+ void Finish();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_NSIRUNNABLE
+
+ // LSRequestChildCallback
+ void OnResponse(const LSRequestResponse& aResponse) override;
+};
+
+class SimpleRequestResolver final : public LSSimpleRequestChildCallback {
+ RefPtr<Promise> mPromise;
+
+ public:
+ explicit SimpleRequestResolver(Promise* aPromise) : mPromise(aPromise) {}
+
+ NS_INLINE_DECL_REFCOUNTING(SimpleRequestResolver, override);
+
+ private:
+ ~SimpleRequestResolver() = default;
+
+ void HandleResponse(nsresult aResponse);
+
+ void HandleResponse(bool aResponse);
+
+ void HandleResponse(const nsTArray<LSItemInfo>& aResponse);
+
+ // LSRequestChildCallback
+ void OnResponse(const LSSimpleRequestResponse& aResponse) override;
+};
+
+nsresult CreatePromise(JSContext* aContext, Promise** aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aContext);
+
+ nsIGlobalObject* global =
+ xpc::NativeGlobal(JS::CurrentGlobalOrNull(aContext));
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(global, result);
+ if (result.Failed()) {
+ return result.StealNSResult();
+ }
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+nsresult CheckedPrincipalToPrincipalInfo(
+ nsIPrincipal* aPrincipal, mozilla::ipc::PrincipalInfo& aPrincipalInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aPrincipalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(!quota::QuotaManager::IsPrincipalInfoValid(aPrincipalInfo))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aPrincipalInfo.type() !=
+ mozilla::ipc::PrincipalInfo::TContentPrincipalInfo &&
+ aPrincipalInfo.type() !=
+ mozilla::ipc::PrincipalInfo::TSystemPrincipalInfo) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return NS_OK;
+}
+
+} // namespace
+
+LocalStorageManager2::LocalStorageManager2() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(NextGenLocalStorageEnabled());
+}
+
+LocalStorageManager2::~LocalStorageManager2() { MOZ_ASSERT(NS_IsMainThread()); }
+
+NS_IMPL_ISUPPORTS(LocalStorageManager2, nsIDOMStorageManager,
+ nsILocalStorageManager)
+
+NS_IMETHODIMP
+LocalStorageManager2::PrecacheStorage(nsIPrincipal* aPrincipal,
+ nsIPrincipal* aStoragePrincipal,
+ Storage** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aStoragePrincipal);
+ MOZ_ASSERT(_retval);
+
+ // This method was created as part of the e10s-ification of the old LS
+ // implementation to perform a preload in the content/current process. That's
+ // not how things work in LSNG. Instead everything happens in the parent
+ // process, triggered by the official preloading spot,
+ // ContentParent::AboutToLoadHttpFtpDocumentForChild.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+LocalStorageManager2::CreateStorage(mozIDOMWindow* aWindow,
+ nsIPrincipal* aPrincipal,
+ nsIPrincipal* aStoragePrincipal,
+ const nsAString& aDocumentURI,
+ bool aPrivate, Storage** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aStoragePrincipal);
+ MOZ_ASSERT(_retval);
+
+ nsCOMPtr<nsPIDOMWindowInner> inner = nsPIDOMWindowInner::From(aWindow);
+
+ RefPtr<LSObject> object;
+ nsresult rv = LSObject::CreateForPrincipal(inner, aPrincipal,
+ aStoragePrincipal, aDocumentURI,
+ aPrivate, getter_AddRefs(object));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ object.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocalStorageManager2::GetStorage(mozIDOMWindow* aWindow,
+ nsIPrincipal* aPrincipal,
+ nsIPrincipal* aStoragePrincipal, bool aPrivate,
+ Storage** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aStoragePrincipal);
+ MOZ_ASSERT(_retval);
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+LocalStorageManager2::CloneStorage(Storage* aStorageToCloneFrom) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aStorageToCloneFrom);
+
+ // Cloning is specific to sessionStorage; state is forked when a new tab is
+ // opened from an existing tab.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+LocalStorageManager2::CheckStorage(nsIPrincipal* aPrincipal, Storage* aStorage,
+ bool* _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aStorage);
+ MOZ_ASSERT(_retval);
+
+ // Only used by sessionStorage.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+LocalStorageManager2::GetNextGenLocalStorageEnabled(bool* aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aResult);
+
+ *aResult = NextGenLocalStorageEnabled();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocalStorageManager2::Preload(nsIPrincipal* aPrincipal, JSContext* aContext,
+ Promise** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(_retval);
+
+ nsCString originAttrSuffix;
+ nsCString originKey;
+ nsresult rv = aPrincipal->GetStorageOriginKey(originKey);
+ aPrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mozilla::ipc::PrincipalInfo principalInfo;
+ rv = CheckedPrincipalToPrincipalInfo(aPrincipal, principalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<Promise> promise;
+
+ if (aContext) {
+ rv = CreatePromise(aContext, getter_AddRefs(promise));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ LSRequestCommonParams commonParams;
+ commonParams.principalInfo() = principalInfo;
+ commonParams.storagePrincipalInfo() = principalInfo;
+ commonParams.originKey() = originKey;
+
+ LSRequestPreloadDatastoreParams params(commonParams);
+
+ RefPtr<AsyncRequestHelper> helper =
+ new AsyncRequestHelper(this, promise, params);
+
+ // This will start and finish the async request on the RemoteLazyInputStream
+ // thread.
+ // This must be done on RemoteLazyInputStream Thread because it's very likely
+ // that a content process will issue a prepare datastore request for the same
+ // principal while blocking the content process on the main thread.
+ // There would be a potential for deadlock if the preloading was initialized
+ // from the main thread of the parent process and a11y issued a synchronous
+ // message from the parent process to the content process (approximately at
+ // the same time) because the preload request wouldn't be able to respond
+ // to the Ready message by sending the Finish message which is needed to
+ // finish the preload request and unblock the prepare datastore request.
+ rv = helper->Dispatch();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ promise.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocalStorageManager2::IsPreloaded(nsIPrincipal* aPrincipal, JSContext* aContext,
+ Promise** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(_retval);
+
+ RefPtr<Promise> promise;
+ nsresult rv = CreatePromise(aContext, getter_AddRefs(promise));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ LSSimpleRequestPreloadedParams params;
+
+ rv = CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ params.storagePrincipalInfo() = params.principalInfo();
+
+ rv = StartSimpleRequest(promise, params);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ promise.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocalStorageManager2::GetState(nsIPrincipal* aPrincipal, JSContext* aContext,
+ Promise** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(_retval);
+
+ RefPtr<Promise> promise;
+ nsresult rv = CreatePromise(aContext, getter_AddRefs(promise));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ LSSimpleRequestGetStateParams params;
+
+ rv = CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ params.storagePrincipalInfo() = params.principalInfo();
+
+ rv = StartSimpleRequest(promise, params);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ promise.forget(_retval);
+ return NS_OK;
+}
+
+LSRequestChild* LocalStorageManager2::StartRequest(
+ const LSRequestParams& aParams, LSRequestChildCallback* aCallback) {
+ AssertIsOnDOMFileThread();
+
+ mozilla::ipc::PBackgroundChild* backgroundActor =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!backgroundActor)) {
+ return nullptr;
+ }
+
+ auto actor = new LSRequestChild();
+
+ if (!backgroundActor->SendPBackgroundLSRequestConstructor(actor, aParams)) {
+ return nullptr;
+ }
+
+ // Must set callback after calling SendPBackgroundLSRequestConstructor since
+ // it can be called synchronously when SendPBackgroundLSRequestConstructor
+ // fails.
+ actor->SetCallback(aCallback);
+
+ return actor;
+}
+
+nsresult LocalStorageManager2::StartSimpleRequest(
+ Promise* aPromise, const LSSimpleRequestParams& aParams) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPromise);
+
+ mozilla::ipc::PBackgroundChild* backgroundActor =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!backgroundActor)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto actor = new LSSimpleRequestChild();
+
+ if (!backgroundActor->SendPBackgroundLSSimpleRequestConstructor(actor,
+ aParams)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<SimpleRequestResolver> resolver = new SimpleRequestResolver(aPromise);
+
+ // Must set callback after calling SendPBackgroundLSRequestConstructor since
+ // it can be called synchronously when SendPBackgroundLSRequestConstructor
+ // fails.
+ actor->SetCallback(resolver);
+
+ return NS_OK;
+}
+
+nsresult AsyncRequestHelper::Dispatch() {
+ AssertIsOnOwningThread();
+
+ nsCOMPtr<nsIEventTarget> domFileThread =
+ RemoteLazyInputStreamThread::GetOrCreate();
+ if (NS_WARN_IF(!domFileThread)) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ nsresult rv = domFileThread->Dispatch(this, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult AsyncRequestHelper::Start() {
+ AssertIsOnDOMFileThread();
+ MOZ_ASSERT(mState == State::Initial);
+
+ mState = State::ResponsePending;
+
+ LSRequestChild* actor = mManager->StartRequest(mParams, this);
+ if (NS_WARN_IF(!actor)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mActor = actor;
+
+ return NS_OK;
+}
+
+void AsyncRequestHelper::Finish() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Finishing);
+
+ if (NS_WARN_IF(NS_FAILED(mResultCode))) {
+ if (mPromise) {
+ mPromise->MaybeReject(mResultCode);
+ }
+ } else {
+ switch (mResponse.type()) {
+ case LSRequestResponse::Tnsresult:
+ if (mPromise) {
+ mPromise->MaybeReject(mResponse.get_nsresult());
+ }
+ break;
+
+ case LSRequestResponse::TLSRequestPreloadDatastoreResponse:
+ if (mPromise) {
+ mPromise->MaybeResolveWithUndefined();
+ }
+ break;
+ default:
+ MOZ_CRASH("Unknown response type!");
+ }
+ }
+
+ mManager = nullptr;
+
+ mState = State::Complete;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED0(AsyncRequestHelper, Runnable)
+
+NS_IMETHODIMP
+AsyncRequestHelper::Run() {
+ nsresult rv;
+
+ switch (mState) {
+ case State::Initial:
+ rv = Start();
+ break;
+
+ case State::Finishing:
+ Finish();
+ return NS_OK;
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::Finishing) {
+ if (NS_SUCCEEDED(mResultCode)) {
+ mResultCode = rv;
+ }
+
+ mState = State::Finishing;
+
+ if (IsOnOwningThread()) {
+ Finish();
+ } else {
+ MOZ_ALWAYS_SUCCEEDS(
+ mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL));
+ }
+ }
+
+ return NS_OK;
+}
+
+void AsyncRequestHelper::OnResponse(const LSRequestResponse& aResponse) {
+ AssertIsOnDOMFileThread();
+ MOZ_ASSERT(mState == State::ResponsePending);
+
+ mActor = nullptr;
+
+ mResponse = aResponse;
+
+ mState = State::Finishing;
+
+ MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL));
+}
+
+void SimpleRequestResolver::HandleResponse(nsresult aResponse) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPromise);
+
+ mPromise->MaybeReject(aResponse);
+}
+
+void SimpleRequestResolver::HandleResponse(bool aResponse) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPromise);
+
+ mPromise->MaybeResolve(aResponse);
+}
+
+[[nodiscard]] static bool ToJSValue(JSContext* aCx,
+ const nsTArray<LSItemInfo>& aArgument,
+ JS::MutableHandle<JS::Value> aValue) {
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+ if (!obj) {
+ return false;
+ }
+
+ for (size_t i = 0; i < aArgument.Length(); ++i) {
+ const LSItemInfo& itemInfo = aArgument[i];
+
+ const nsString& key = itemInfo.key();
+
+ JS::Rooted<JS::Value> value(aCx);
+ if (!ToJSValue(aCx, itemInfo.value().AsString(), &value)) {
+ return false;
+ }
+
+ if (!JS_DefineUCProperty(aCx, obj, key.BeginReading(), key.Length(), value,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+ }
+
+ aValue.setObject(*obj);
+ return true;
+}
+
+void SimpleRequestResolver::HandleResponse(
+ const nsTArray<LSItemInfo>& aResponse) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPromise);
+
+ mPromise->MaybeResolve(aResponse);
+}
+
+void SimpleRequestResolver::OnResponse(
+ const LSSimpleRequestResponse& aResponse) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ switch (aResponse.type()) {
+ case LSSimpleRequestResponse::Tnsresult:
+ HandleResponse(aResponse.get_nsresult());
+ break;
+
+ case LSSimpleRequestResponse::TLSSimpleRequestPreloadedResponse:
+ HandleResponse(
+ aResponse.get_LSSimpleRequestPreloadedResponse().preloaded());
+ break;
+
+ case LSSimpleRequestResponse::TLSSimpleRequestGetStateResponse:
+ HandleResponse(
+ aResponse.get_LSSimpleRequestGetStateResponse().itemInfos());
+ break;
+
+ default:
+ MOZ_CRASH("Unknown response type!");
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/localstorage/LocalStorageManager2.h b/dom/localstorage/LocalStorageManager2.h
new file mode 100644
index 0000000000..63cc9a9a15
--- /dev/null
+++ b/dom/localstorage/LocalStorageManager2.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_LocalStorageManager2_h
+#define mozilla_dom_localstorage_LocalStorageManager2_h
+
+#include <cstdint>
+#include "ErrorList.h"
+#include "nsIDOMStorageManager.h"
+#include "nsILocalStorageManager.h"
+#include "nsISupports.h"
+
+namespace mozilla::dom {
+
+class LSRequestChild;
+class LSRequestChildCallback;
+class LSRequestParams;
+class LSSimpleRequestParams;
+class Promise;
+
+/**
+ * Under LSNG this exposes nsILocalStorageManager::Preload to ContentParent to
+ * trigger preloading. Otherwise, this is basically just a place for test logic
+ * that doesn't make sense to put directly on the Storage WebIDL interface.
+ *
+ * Previously, the nsIDOMStorageManager XPCOM interface was also used by
+ * nsGlobalWindowInner to interact with LocalStorage, but in these de-XPCOM
+ * days, we've moved to just directly reference the relevant concrete classes
+ * (ex: LSObject) directly.
+ *
+ * Note that testing methods are now also directly exposed on the Storage WebIDL
+ * interface for simplicity/sanity.
+ */
+class LocalStorageManager2 final : public nsIDOMStorageManager,
+ public nsILocalStorageManager {
+ public:
+ LocalStorageManager2();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOMSTORAGEMANAGER
+ NS_DECL_NSILOCALSTORAGEMANAGER
+
+ /**
+ * Helper to trigger an LSRequest and resolve/reject the provided promise when
+ * the result comes in. This routine is notable because the LSRequest
+ * mechanism is normally used synchronously from content, but here it's
+ * exposed asynchronously.
+ */
+ LSRequestChild* StartRequest(const LSRequestParams& aParams,
+ LSRequestChildCallback* aCallback);
+
+ private:
+ ~LocalStorageManager2();
+
+ /**
+ * Helper to trigger an LSSimpleRequst and resolve/reject the provided promise
+ * when the result comes in.
+ */
+ nsresult StartSimpleRequest(Promise* aPromise,
+ const LSSimpleRequestParams& aParams);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_localstorage_LocalStorageManager2_h
diff --git a/dom/localstorage/PBackgroundLSDatabase.ipdl b/dom/localstorage/PBackgroundLSDatabase.ipdl
new file mode 100644
index 0000000000..9a4402c97e
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSDatabase.ipdl
@@ -0,0 +1,175 @@
+/* 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 protocol PBackground;
+include protocol PBackgroundLSSnapshot;
+
+include PBackgroundLSSharedTypes;
+
+include "mozilla/dom/localstorage/SerializationHelpers.h";
+
+using mozilla::dom::LSSnapshot::LoadState
+ from "mozilla/dom/LSSnapshot.h";
+
+namespace mozilla {
+namespace dom {
+
+/**
+ * Initial LSSnapshot state as produced by Datastore::GetSnapshotLoadInfo. See
+ * `LSSnapshot::LoadState` for more details about the possible states and a
+ * high level overview.
+ */
+struct LSSnapshotInitInfo
+{
+ /**
+ * Boolean indicating whether the `key` provided as an argument to the
+ * PBackgroundLSSnapshot constructor did not exist in the Datastore and should
+ * be treated as an unknown and therefore undefined value. Note that `key` may
+ * have been provided as a void string, in which case this value is forced to
+ * be false.
+ */
+ bool addKeyToUnknownItems;
+
+ /**
+ * As many key/value or key/void pairs as the snapshot prefill byte budget
+ * allowed.
+ */
+ LSItemInfo[] itemInfos;
+
+ /**
+ * The total number of key/value pairs in LocalStorage for this origin at the
+ * time the snapshot was created. (And the point of the snapshot is to
+ * conceptually freeze the state of the Datastore in time, so this value does
+ * not change despite what other LSDatabase objects get up to in other
+ * processes.)
+ */
+ uint32_t totalLength;
+
+ /**
+ * The current amount of LocalStorage usage as measured by the summing the
+ * nsString Length() of both the key and the value over all stored pairs.
+ */
+ int64_t usage;
+
+ /**
+ * The amount of storage allowed to be used by the Snapshot without requesting
+ * more storage space via IncreasePeakUsage. This is the `usage` plus 0 or
+ * more bytes of space. If space was available, the increase will be the
+ * `minSize` from the PBackgroundLSSnapshot constructor plus the configured
+ * pre-increment (via "dom.storage.snapshot_peak_usage.initial_preincrement").
+ * If the LocalStorage total usage was already close to the limit, then the
+ * fallback is either the `minSize` plus the configured reduced pre-increment
+ * (via "dom.storage.snapshot_peak_usage.reduced_initial_preincrement"), or
+ * `minSize`, or 0 depending on remaining available space.
+ */
+ int64_t peakUsage;
+
+ // See `LSSnapshot::LoadState` in `LSSnapshot.h`
+ LoadState loadState;
+
+ /**
+ * Boolean indicating whether there where cross-process databases registered
+ * for this origin at the time the snapshot was created.
+ */
+ bool hasOtherProcessDatabases;
+
+ /**
+ * Boolean indicating whether there where cross-process observers registered
+ * for this origin at the time the snapshot was created.
+ */
+ bool hasOtherProcessObservers;
+};
+
+/**
+ * This protocol is asynchronously created via constructor on PBackground but
+ * has synchronous semantics from the perspective of content on the main thread.
+ * The construction potentially involves waiting for disk I/O to load the
+ * LocalStorage data from disk as well as related QuotaManager checks, so async
+ * calls to PBackground are the only viable mechanism because blocking
+ * PBackground is not acceptable. (Note that an attempt is made to minimize any
+ * I/O latency by triggering preloading from
+ * ContentParent::AboutToLoadHttpFtpDocumentForChild, the central place
+ * for pre-loading.)
+ */
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+sync protocol PBackgroundLSDatabase
+{
+ manager PBackground;
+ manages PBackgroundLSSnapshot;
+
+parent:
+ // The DeleteMe message is used to avoid a race condition between the parent
+ // actor and the child actor. The PBackgroundLSDatabase protocol could be
+ // simply destroyed by sending the __delete__ message from the child side.
+ // However, that would destroy the child actor immediatelly and the parent
+ // could be sending a message to the child at the same time resulting in a
+ // routing error since the child actor wouldn't exist anymore. A routing
+ // error typically causes a crash. The race can be prevented by doing the
+ // teardown in two steps. First, we send the DeleteMe message to the parent
+ // and the parent then sends the __delete__ message to the child.
+ async DeleteMe();
+
+ /**
+ * Sent in response to a `RequestAllowToClose` message once the snapshot
+ * cleanup has happened OR from LSDatabase's destructor if AllowToClose has
+ * not already been reported.
+ */
+ async AllowToClose();
+
+ /**
+ * Invoked to create an LSSnapshot backed by a Snapshot in PBackground that
+ * presents an atomic and consistent view of the state of the authoritative
+ * Datastore state in the parent.
+ *
+ * This needs to be synchronous because LocalStorage's semantics are
+ * synchronous. Note that the Datastore in the PBackground parent already
+ * has the answers to this request immediately available without needing to
+ * consult any other threads or perform any I/O. Additionally, the response
+ * is explicitly bounded in size by the tunable snapshot prefill byte limit.
+ *
+ * @param key
+ * If key is non-void, then the snapshot is being triggered by a direct
+ * access to a localStorage key (get, set, or removal, with set/removal
+ * requiring the old value in order to properly populate the "storage"
+ * event), the key being requested. It's possible the key is not present in
+ * localStorage, in which case LSSnapshotInitInfo::addKeyToUnknownItems will
+ * be true indicating that there is no such key/value pair, otherwise it
+ * will be false.
+ * @param increasePeakUsage
+ * Whether the parent should increase initial peak uage of the Snapshot.
+ * See also the comment for LSSnapshotInitInfo::peakUsage above.
+ */
+ sync PBackgroundLSSnapshot(nsString documentURI,
+ nsString key,
+ bool increasePeakUsage,
+ int64_t minSize)
+ returns (LSSnapshotInitInfo initInfo);
+
+child:
+ /**
+ * Only sent by the parent in response to the child's DeleteMe request.
+ */
+ async __delete__();
+
+ /**
+ * Request to close the LSDatabase, checkpointing and finishing any
+ * outstanding snapshots so no state is lost. This request is issued when
+ * QuotaManager is shutting down or is aborting operations for an origin or
+ * process. Once the snapshot has cleaned up, AllowToClose will be sent to
+ * the parent.
+ *
+ * Note that the QuotaManager shutdown process is more likely to happen in
+ * unit tests where we explicitly reset the QuotaManager. At runtime, we
+ * expect windows to be closed and content processes terminated well before
+ * QuotaManager shutdown would actually occur.
+ *
+ * Also, Operations are usually aborted for an origin due to privacy API's
+ * clearing data for an origin. Operations are aborted for a process by
+ * ContentParent::ShutDownProcess.
+ */
+ async RequestAllowToClose();
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/localstorage/PBackgroundLSObserver.ipdl b/dom/localstorage/PBackgroundLSObserver.ipdl
new file mode 100644
index 0000000000..c800828da3
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSObserver.ipdl
@@ -0,0 +1,63 @@
+/* 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 protocol PBackground;
+
+include PBackgroundSharedTypes;
+
+include "mozilla/dom/localstorage/SerializationHelpers.h";
+
+using mozilla::dom::LSValue
+ from "mozilla/dom/LSValue.h";
+
+namespace mozilla {
+namespace dom {
+
+/**
+ * The observer protocol sends "storage" event notifications for changes to
+ * LocalStorage that take place in other processes as their Snapshots are
+ * Checkpointed to the canonical Datastore in the parent process. Same-process
+ * notifications are generated as mutations happen.
+ *
+ * Note that mutations are never generated for redundant mutations. Setting the
+ * key "foo" to have value "bar" when it already has value "bar" will never
+ * result in a "storage" event.
+ */
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+async protocol PBackgroundLSObserver
+{
+ manager PBackground;
+
+parent:
+ /**
+ * Sent by the LSObserver's destructor when it's going away. Any Observe
+ * messages received after this is sent will be ignored. Which is fine,
+ * because there should be nothing around left to hear. In the event a new
+ * page came into existence, its Observer creation will happen (effectively)
+ * synchronously.
+ */
+ async DeleteMe();
+
+child:
+ /**
+ * Only sent by the parent in response to a deletion request.
+ */
+ async __delete__();
+
+ /**
+ * Sent by the parent process as Snapshots from other processes are
+ * Checkpointed, applying their mutations. The child actor currently directly
+ * shunts these to Storage::NotifyChange to generate "storage" events for
+ * immediate dispatch.
+ */
+ async Observe(PrincipalInfo principalInfo,
+ uint32_t privateBrowsingId,
+ nsString documentURI,
+ nsString key,
+ LSValue oldValue,
+ LSValue newValue);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/localstorage/PBackgroundLSRequest.ipdl b/dom/localstorage/PBackgroundLSRequest.ipdl
new file mode 100644
index 0000000000..1ced0a9a17
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSRequest.ipdl
@@ -0,0 +1,103 @@
+/* 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 protocol PBackground;
+
+using struct mozilla::null_t from "mozilla/ipc/IPCCore.h";
+
+namespace mozilla {
+namespace dom {
+
+struct LSRequestPreloadDatastoreResponse
+{
+};
+
+struct LSRequestPrepareDatastoreResponse
+{
+ uint64_t datastoreId;
+};
+
+struct LSRequestPrepareObserverResponse
+{
+ uint64_t observerId;
+};
+
+/**
+ * Discriminated union which can contain an error code (`nsresult`) or
+ * particular request response.
+ */
+union LSRequestResponse
+{
+ nsresult;
+ LSRequestPreloadDatastoreResponse;
+ LSRequestPrepareDatastoreResponse;
+ LSRequestPrepareObserverResponse;
+};
+
+/**
+ * An asynchronous protocol for issuing requests that are used in a synchronous
+ * fashion by LocalStorage via LSObject's RequestHelper mechanism. This differs
+ * from LSSimpleRequest which is implemented and used asynchronously.
+ *
+ * See `PBackgroundLSSharedTypes.ipdlh` for more on the request types, the
+ * response types above for their corresponding responses, and `RequestHelper`
+ * for more on the usage and lifecycle of this mechanism.
+ */
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+protocol PBackgroundLSRequest
+{
+ manager PBackground;
+
+parent:
+ // The Cancel message is used to avoid a possible dead lock caused by a CPOW
+ // sending a synchronous message from the main thread in the chrome process
+ // to the main thread in the content process at the time we are blocking
+ // the main thread in the content process to handle a request.
+ // We use the PBackground thread on the parent side to handle requests, but
+ // sometimes we need to get information from principals and that's currently
+ // only possible on the main thread. So if the main thread in the chrome
+ // process is blocked by a CPOW operation, our request must wait for the CPOW
+ // operation to complete. However the CPOW operation can't complete either
+ // because we are blocking the main thread in the content process.
+ // The dead lock is prevented by canceling our nested event loop in the
+ // content process when we receive a synchronous IPC message from the parent.
+ //
+ // Note that cancellation isn't instantaneous. It's just an asynchronous flow
+ // that definitely doesn't involve the main thread in the parent process, so
+ // we're guaranteed to unblock the main-thread in the content process and
+ // allow the sync IPC to make progress. When Cancel() is received by the
+ // parent, it will Send__delete__. The child will either send Cancel or
+ // Finish, but not both.
+ async Cancel();
+
+ /**
+ * Sent by the child in response to Ready, requesting that __delete__ be sent
+ * with the result. The child will either send Finish or Cancel, but not
+ * both. No further message will be sent from the child after invoking one.
+ */
+ async Finish();
+
+child:
+ /**
+ * The deletion is sent with the result of the request directly in response to
+ * either Cancel or Finish.
+ */
+ async __delete__(LSRequestResponse response);
+
+ /**
+ * Sent by the parent when it has completed whatever async stuff it needs to
+ * do and is ready to send the results. It then awaits the Finish() call to
+ * send the results. This may seem redundant, but it's not. If the
+ * __delete__ was sent directly, it's possible there could be a race where
+ * Cancel() would be received by the parent after it had already sent
+ * __delete__. (Which may no longer be fatal thanks to improvements to the
+ * IPC layer, but it would still lead to warnings, etc. And we don't
+ * expect PBackground to be highly contended nor the RemoteLazyInputStream
+ * thread.)
+ */
+ async Ready();
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/localstorage/PBackgroundLSSharedTypes.ipdlh b/dom/localstorage/PBackgroundLSSharedTypes.ipdlh
new file mode 100644
index 0000000000..d71b0e6111
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSSharedTypes.ipdlh
@@ -0,0 +1,96 @@
+/* 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 PBackgroundSharedTypes;
+include ProtocolTypes;
+
+include "mozilla/dom/localstorage/SerializationHelpers.h";
+
+using mozilla::dom::LSValue
+ from "mozilla/dom/LSValue.h";
+
+namespace mozilla {
+namespace dom {
+
+struct LSRequestCommonParams
+{
+ PrincipalInfo principalInfo;
+ PrincipalInfo storagePrincipalInfo;
+ nsCString originKey;
+};
+
+struct LSRequestPreloadDatastoreParams
+{
+ LSRequestCommonParams commonParams;
+};
+
+struct LSRequestPrepareDatastoreParams
+{
+ LSRequestCommonParams commonParams;
+ nsID? clientId;
+ PrincipalInfo? clientPrincipalInfo;
+};
+
+/**
+ * In order to validate the principal with the client, we need to provide an
+ * additional principalInfo for the client. The client is using the foreign
+ * principal, see StoragePrincipalHelper.h for details, which is different from
+ * the principalInfo. So, we need to pass the principalInfo from the client So
+ * that we can verify it with the given client Id.
+ *
+ * Note that the storagePrincipalInfo is used to access the right cookie jar
+ * according to the Storage Access. This is passed in order to access the
+ * correct local storage. Essentially, the storage principal and the client
+ * principal are using the PartitionKey in their OriginAttributes. But, the
+ * existence of the PartitionKey between them is depending on different
+ * conditions. Namely, the storage principal depends on the Storage Access but
+ * the client principal depends on whether it's in a third party.
+ */
+struct LSRequestPrepareObserverParams
+{
+ PrincipalInfo principalInfo;
+ PrincipalInfo storagePrincipalInfo;
+ nsID? clientId;
+ PrincipalInfo? clientPrincipalInfo;
+};
+
+union LSRequestParams
+{
+ LSRequestPreloadDatastoreParams;
+ LSRequestPrepareDatastoreParams;
+ LSRequestPrepareObserverParams;
+};
+
+struct LSSimpleRequestPreloadedParams
+{
+ PrincipalInfo principalInfo;
+ PrincipalInfo storagePrincipalInfo;
+};
+
+struct LSSimpleRequestGetStateParams
+{
+ PrincipalInfo principalInfo;
+ PrincipalInfo storagePrincipalInfo;
+};
+
+union LSSimpleRequestParams
+{
+ LSSimpleRequestPreloadedParams;
+ LSSimpleRequestGetStateParams;
+};
+
+/**
+ * LocalStorage key/value pair wire representations. `value` may be void in
+ * cases where there is a value but it is not being sent for memory/bandwidth
+ * conservation purposes. (It's not possible to have a null/undefined `value`
+ * as Storage is defined explicitly as a String store.)
+ */
+struct LSItemInfo
+{
+ nsString key;
+ LSValue value;
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/localstorage/PBackgroundLSSimpleRequest.ipdl b/dom/localstorage/PBackgroundLSSimpleRequest.ipdl
new file mode 100644
index 0000000000..9a6a22d836
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSSimpleRequest.ipdl
@@ -0,0 +1,59 @@
+/* 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 protocol PBackground;
+
+include PBackgroundLSSharedTypes;
+
+namespace mozilla {
+namespace dom {
+
+/**
+ * Response to a `LSSimpleRequestPreloadedParams` request indicating whether the
+ * origin was preloaded.
+ */
+struct LSSimpleRequestPreloadedResponse
+{
+ bool preloaded;
+};
+
+struct LSSimpleRequestGetStateResponse
+{
+ LSItemInfo[] itemInfos;
+};
+
+/**
+ * Discriminated union which can contain an error code (`nsresult`) or
+ * particular simple request response.
+ */
+union LSSimpleRequestResponse
+{
+ nsresult;
+ LSSimpleRequestPreloadedResponse;
+ LSSimpleRequestGetStateResponse;
+};
+
+/**
+ * Simple requests are async-only from both a protocol perspective and the
+ * manner in which they're used. In comparison, PBackgroundLSRequests are
+ * async only from a protocol perspective; they are used synchronously from the
+ * main thread via LSObject's RequestHelper mechanism. (With the caveat that
+ * nsILocalStorageManager does expose LSRequests asynchronously.)
+ *
+ * These requests use the common idiom where the arguments to the request are
+ * sent in the constructor and the result is sent in the __delete__ response.
+ * Request types are indicated by the Params variant used and those live in
+ * `PBackgroundLSSharedTypes.ipdlh`.
+ */
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+protocol PBackgroundLSSimpleRequest
+{
+ manager PBackground;
+
+child:
+ async __delete__(LSSimpleRequestResponse response);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/localstorage/PBackgroundLSSnapshot.ipdl b/dom/localstorage/PBackgroundLSSnapshot.ipdl
new file mode 100644
index 0000000000..45c744c840
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSSnapshot.ipdl
@@ -0,0 +1,153 @@
+/* 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 protocol PBackground;
+include protocol PBackgroundLSDatabase;
+
+include PBackgroundLSSharedTypes;
+
+include "mozilla/dom/localstorage/SerializationHelpers.h";
+
+using mozilla::dom::LSValue
+ from "mozilla/dom/LSValue.h";
+
+namespace mozilla {
+namespace dom {
+
+struct LSSetItemInfo
+{
+ nsString key;
+ LSValue value;
+};
+
+struct LSRemoveItemInfo
+{
+ nsString key;
+};
+
+struct LSClearInfo
+{
+};
+
+/**
+ * Union of LocalStorage mutation types.
+ */
+union LSWriteInfo
+{
+ LSSetItemInfo;
+ LSRemoveItemInfo;
+ LSClearInfo;
+};
+
+struct LSSetItemAndNotifyInfo
+{
+ nsString key;
+ LSValue oldValue;
+ LSValue value;
+};
+
+struct LSRemoveItemAndNotifyInfo
+{
+ nsString key;
+ LSValue oldValue;
+};
+
+/**
+ * Union of LocalStorage mutation types.
+ */
+union LSWriteAndNotifyInfo
+{
+ LSSetItemAndNotifyInfo;
+ LSRemoveItemAndNotifyInfo;
+ LSClearInfo;
+};
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+sync protocol PBackgroundLSSnapshot
+{
+ manager PBackgroundLSDatabase;
+
+parent:
+ async DeleteMe();
+
+ async AsyncCheckpoint(LSWriteInfo[] writeInfos);
+
+ async AsyncCheckpointAndNotify(LSWriteAndNotifyInfo[] writeAndNotifyInfos);
+
+ // A synchronous checkpoint. This should only be used by the snapshotting code
+ // to checkpoint an explicit snapshot.
+ sync SyncCheckpoint(LSWriteInfo[] writeInfos);
+
+ // A synchronous checkpoint and notify. This should only be used by the
+ // snapshotting code to checkpoint and notify an explicit snapshot.
+ sync SyncCheckpointAndNotify(LSWriteAndNotifyInfo[] writeAndNotifyInfos);
+
+ async AsyncFinish();
+
+ // A synchronous finish. This should only be used by the snapshotting code to
+ // end an explicit snapshot.
+ sync SyncFinish();
+
+ async Loaded();
+
+ /**
+ * Invoked on demand to load an item that didn't fit into the initial
+ * snapshot prefill and also some additional key/value pairs to lower down
+ * the need to use this synchronous message again.
+ *
+ * This needs to be synchronous because LocalStorage's semantics are
+ * synchronous. Note that the Snapshot in the PBackground parent already
+ * has the answers to this request immediately available without needing to
+ * consult any other threads or perform any I/O.
+ */
+ sync LoadValueAndMoreItems(nsString key)
+ returns (LSValue value, LSItemInfo[] itemInfos);
+
+ /**
+ * Invoked on demand to load all keys in in their canonical order if they
+ * didn't fit into the initial snapshot prefill.
+ *
+ * This needs to be synchronous because LocalStorage's semantics are
+ * synchronous. Note that the Snapshot in the PBackground parent already
+ * has the answers to this request immediately available without needing to
+ * consult any other threads or perform any I/O.
+ */
+ sync LoadKeys()
+ returns (nsString[] keys);
+
+ /**
+ * This needs to be synchronous because LocalStorage's semantics are
+ * synchronous. Note that the Snapshot in the PBackground parent typically
+ * doesn't need to consult any other threads or perform any I/O to handle
+ * this request. However, it has to call a quota manager method that can
+ * potentially do I/O directly on the PBackground thread. It can only happen
+ * rarely in a storage pressure (low storage space) situation. Specifically,
+ * after we get a list of origin directories for eviction, we will delete
+ * them directly on the PBackground thread. This doesn't cause any
+ * performance problems, but avoiding I/O completely might need to be done as
+ * a futher optimization.
+ */
+ sync IncreasePeakUsage(int64_t minSize)
+ returns (int64_t size);
+
+child:
+ /**
+ * Compels the child LSSnapshot to Checkpoint() and Finish(), effectively
+ * compelling the snapshot to flush any issued mutations and close itself.
+ * The child LSSnapshot does that either immediately if it's just waiting
+ * to be reused or when it gets into a stable state.
+ *
+ * This message is expected to be sent in the following two cases only:
+ * 1. The state of the underlying Datastore starts to differ from the state
+ * captured at the time of snapshot creation.
+ * 2. The last private browsing context exits. And in that case we expect
+ * all private browsing globals to already have been destroyed.
+ */
+ async MarkDirty();
+
+ async __delete__();
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/localstorage/ReportInternalError.cpp b/dom/localstorage/ReportInternalError.cpp
new file mode 100644
index 0000000000..88fa4fd6e2
--- /dev/null
+++ b/dom/localstorage/ReportInternalError.cpp
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "ReportInternalError.h"
+
+#include <cinttypes>
+#include "nsContentUtils.h"
+#include "nsPrintfCString.h"
+#include "nsString.h"
+
+namespace mozilla::dom::localstorage {
+
+void ReportInternalError(const char* aFile, uint32_t aLine, const char* aStr) {
+ // Get leaf of file path
+ for (const char* p = aFile; *p; ++p) {
+ if (*p == '/' && *(p + 1)) {
+ aFile = p + 1;
+ }
+ }
+
+ nsContentUtils::LogSimpleConsoleError(
+ NS_ConvertUTF8toUTF16(
+ nsPrintfCString("LocalStorage %s: %s:%" PRIu32, aStr, aFile, aLine)),
+ "localstorage"_ns, false,
+ true /* Internal errors are chrome context only*/);
+}
+
+} // namespace mozilla::dom::localstorage
diff --git a/dom/localstorage/ReportInternalError.h b/dom/localstorage/ReportInternalError.h
new file mode 100644
index 0000000000..9cfd1cd72a
--- /dev/null
+++ b/dom/localstorage/ReportInternalError.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_ReportInternalError_h
+#define mozilla_dom_localstorage_ReportInternalError_h
+
+#include <cstdint>
+#include "mozilla/Attributes.h"
+#include "nsDebug.h"
+
+#define LS_WARNING(...) \
+ do { \
+ nsPrintfCString s(__VA_ARGS__); \
+ mozilla::dom::localstorage::ReportInternalError(__FILE__, __LINE__, \
+ s.get()); \
+ NS_WARNING(s.get()); \
+ } while (0)
+
+namespace mozilla::dom::localstorage {
+
+MOZ_COLD void ReportInternalError(const char* aFile, uint32_t aLine,
+ const char* aStr);
+
+} // namespace mozilla::dom::localstorage
+
+#endif // mozilla_dom_localstorage_ReportInternalError_h
diff --git a/dom/localstorage/SerializationHelpers.h b/dom/localstorage/SerializationHelpers.h
new file mode 100644
index 0000000000..f9b499671c
--- /dev/null
+++ b/dom/localstorage/SerializationHelpers.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_SerializationHelpers_h
+#define mozilla_dom_localstorage_SerializationHelpers_h
+
+#include <string>
+#include "chrome/common/ipc_message_utils.h"
+#include "ipc/EnumSerializer.h"
+#include "mozilla/dom/LSSnapshot.h"
+#include "mozilla/dom/LSValue.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::dom::LSSnapshot::LoadState>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::LSSnapshot::LoadState,
+ mozilla::dom::LSSnapshot::LoadState::Initial,
+ mozilla::dom::LSSnapshot::LoadState::EndGuard> {};
+
+template <>
+struct ParamTraits<mozilla::dom::LSValue::CompressionType>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::LSValue::CompressionType,
+ mozilla::dom::LSValue::CompressionType::UNCOMPRESSED,
+ mozilla::dom::LSValue::CompressionType::NUM_TYPES> {};
+
+static_assert(
+ 0u == static_cast<uint8_t>(mozilla::dom::LSValue::ConversionType::NONE));
+template <>
+struct ParamTraits<mozilla::dom::LSValue::ConversionType>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::LSValue::ConversionType,
+ mozilla::dom::LSValue::ConversionType::NONE,
+ mozilla::dom::LSValue::ConversionType::NUM_TYPES> {};
+
+template <>
+struct ParamTraits<mozilla::dom::LSValue> {
+ typedef mozilla::dom::LSValue paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mBuffer);
+ WriteParam(aWriter, aParam.mUTF16Length);
+ WriteParam(aWriter, aParam.mConversionType);
+ WriteParam(aWriter, aParam.mCompressionType);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mBuffer) &&
+ ReadParam(aReader, &aResult->mUTF16Length) &&
+ ReadParam(aReader, &aResult->mConversionType) &&
+ ReadParam(aReader, &aResult->mCompressionType);
+ }
+};
+
+} // namespace IPC
+
+#endif // mozilla_dom_localstorage_SerializationHelpers_h
diff --git a/dom/localstorage/SnappyUtils.cpp b/dom/localstorage/SnappyUtils.cpp
new file mode 100644
index 0000000000..cf47421bc5
--- /dev/null
+++ b/dom/localstorage/SnappyUtils.cpp
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "SnappyUtils.h"
+
+#include <stddef.h>
+#include "mozilla/Assertions.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/fallible.h"
+#include "nsDebug.h"
+#include "nsString.h"
+#include "snappy/snappy.h"
+
+namespace mozilla::dom {
+
+static_assert(SNAPPY_VERSION == 0x010109);
+
+bool SnappyCompress(const nsACString& aSource, nsACString& aDest) {
+ MOZ_ASSERT(!aSource.IsVoid());
+
+ size_t uncompressedLength = aSource.Length();
+
+ if (uncompressedLength <= 16) {
+ aDest.SetIsVoid(true);
+ return true;
+ }
+
+ size_t compressedLength = snappy::MaxCompressedLength(uncompressedLength);
+
+ if (NS_WARN_IF(!aDest.SetLength(compressedLength, fallible))) {
+ return false;
+ }
+
+ snappy::RawCompress(aSource.BeginReading(), uncompressedLength,
+ aDest.BeginWriting(), &compressedLength);
+
+ if (compressedLength >= uncompressedLength) {
+ aDest.SetIsVoid(true);
+ return true;
+ }
+
+ if (NS_WARN_IF(!aDest.SetLength(compressedLength, fallible))) {
+ return false;
+ }
+
+ return true;
+}
+
+bool SnappyUncompress(const nsACString& aSource, nsACString& aDest) {
+ MOZ_ASSERT(!aSource.IsVoid());
+
+ const char* compressed = aSource.BeginReading();
+
+ auto compressedLength = static_cast<size_t>(aSource.Length());
+
+ size_t uncompressedLength = 0u;
+ if (!snappy::GetUncompressedLength(compressed, compressedLength,
+ &uncompressedLength)) {
+ return false;
+ }
+
+ CheckedUint32 checkedLength(uncompressedLength);
+ if (!checkedLength.isValid()) {
+ return false;
+ }
+
+ aDest.SetLength(checkedLength.value());
+
+ if (!snappy::RawUncompress(compressed, compressedLength,
+ aDest.BeginWriting())) {
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/localstorage/SnappyUtils.h b/dom/localstorage/SnappyUtils.h
new file mode 100644
index 0000000000..45c09a9465
--- /dev/null
+++ b/dom/localstorage/SnappyUtils.h
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_localstorage_SnappyUtils_h
+#define mozilla_dom_localstorage_SnappyUtils_h
+
+#include "nsStringFwd.h"
+
+namespace mozilla::dom {
+
+bool SnappyCompress(const nsACString& aSource, nsACString& aDest);
+
+bool SnappyUncompress(const nsACString& aSource, nsACString& aDest);
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_localstorage_SnappyUtils_h
diff --git a/dom/localstorage/moz.build b/dom/localstorage/moz.build
new file mode 100644
index 0000000000..a7f5e0b359
--- /dev/null
+++ b/dom/localstorage/moz.build
@@ -0,0 +1,75 @@
+# -*- 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 = ("Core", "Storage: localStorage & sessionStorage")
+
+MOCHITEST_MANIFESTS += ["test/mochitest.ini"]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser.ini"]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
+
+TEST_HARNESS_FILES.xpcshell.dom.localstorage.test.unit += [
+ "test/unit/databaseShadowing-shared.js",
+]
+
+TEST_DIRS += ["test/gtest"]
+
+XPIDL_SOURCES += [
+ "nsILocalStorageManager.idl",
+]
+
+XPIDL_MODULE = "dom_localstorage"
+
+EXPORTS.mozilla.dom.localstorage += [
+ "ActorsParent.h",
+ "SerializationHelpers.h",
+]
+
+EXPORTS.mozilla.dom += [
+ "LocalStorageCommon.h",
+ "LocalStorageManager2.h",
+ "LSObject.h",
+ "LSObserver.h",
+ "LSSnapshot.h",
+ "LSValue.h",
+ "LSWriteOptimizer.h",
+ "LSWriteOptimizerImpl.h",
+ "SnappyUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "ActorsChild.cpp",
+ "ActorsParent.cpp",
+ "LocalStorageCommon.cpp",
+ "LocalStorageManager2.cpp",
+ "LSDatabase.cpp",
+ "LSObject.cpp",
+ "LSObserver.cpp",
+ "LSSnapshot.cpp",
+ "LSValue.cpp",
+ "LSWriteOptimizer.cpp",
+ "ReportInternalError.cpp",
+ "SnappyUtils.cpp",
+]
+
+IPDL_SOURCES += [
+ "PBackgroundLSDatabase.ipdl",
+ "PBackgroundLSObserver.ipdl",
+ "PBackgroundLSRequest.ipdl",
+ "PBackgroundLSSharedTypes.ipdlh",
+ "PBackgroundLSSimpleRequest.ipdl",
+ "PBackgroundLSSnapshot.ipdl",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "/dom/file/ipc",
+]
diff --git a/dom/localstorage/nsILocalStorageManager.idl b/dom/localstorage/nsILocalStorageManager.idl
new file mode 100644
index 0000000000..0e8db22482
--- /dev/null
+++ b/dom/localstorage/nsILocalStorageManager.idl
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 nsIPrincipal;
+
+/**
+ * Methods specific to LocalStorage, see nsIDOMStorageManager for methods shared
+ * with SessionStorage. Methods may migrate there as SessionStorage is
+ * overhauled.
+ */
+[scriptable, builtinclass, uuid(d4f534da-2744-4db3-8774-8b187c64ade9)]
+interface nsILocalStorageManager : nsISupports
+{
+ readonly attribute boolean nextGenLocalStorageEnabled;
+
+ /**
+ * Trigger preload of LocalStorage for the given principal. For use by
+ * ContentParent::AboutToLoadHttpFtpDocumentForChild to maximize the
+ * amount of time we have to load the data off disk before the page might
+ * attempt to touch LocalStorage.
+ *
+ * This method will not create a QuotaManager-managed directory on disk if
+ * one does not already exist for the principal.
+ */
+ [implicit_jscontext]
+ Promise preload(in nsIPrincipal aPrincipal);
+
+ [implicit_jscontext]
+ Promise isPreloaded(in nsIPrincipal aPrincipal);
+
+ [implicit_jscontext]
+ Promise getState(in nsIPrincipal aPrincipal);
+};
diff --git a/dom/localstorage/test/browser.ini b/dom/localstorage/test/browser.ini
new file mode 100644
index 0000000000..8119342ecb
--- /dev/null
+++ b/dom/localstorage/test/browser.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+skip-if = (buildapp != "browser")
+support-files =
+ page_private_ls.html
+
+[browser_private_ls.js]
diff --git a/dom/localstorage/test/browser_private_ls.js b/dom/localstorage/test/browser_private_ls.js
new file mode 100644
index 0000000000..d4708c8e23
--- /dev/null
+++ b/dom/localstorage/test/browser_private_ls.js
@@ -0,0 +1,44 @@
+/**
+ * This test is mainly to verify that datastores are cleared when the last
+ * private browsing context exited.
+ */
+
+async function lsCheckFunc() {
+ let storage = content.localStorage;
+
+ if (storage.length) {
+ return false;
+ }
+
+ // Store non-ASCII value to verify bug 1552428.
+ storage.setItem("foo", "úžasné");
+
+ return true;
+}
+
+function checkTabWindowLS(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], lsCheckFunc);
+}
+
+add_task(async function() {
+ const pageUrl =
+ "http://example.com/browser/dom/localstorage/test/page_private_ls.html";
+
+ for (let i = 0; i < 2; i++) {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let privateTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ pageUrl
+ );
+
+ ok(
+ await checkTabWindowLS(privateTab),
+ "LS works correctly in a private-browsing page."
+ );
+
+ await BrowserTestUtils.closeWindow(privateWin);
+ }
+});
diff --git a/dom/localstorage/test/gtest/TestLocalStorage.cpp b/dom/localstorage/test/gtest/TestLocalStorage.cpp
new file mode 100644
index 0000000000..6c5621f248
--- /dev/null
+++ b/dom/localstorage/test/gtest/TestLocalStorage.cpp
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "gtest/gtest.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/dom/LocalStorageCommon.h"
+#include "mozilla/dom/StorageUtils.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::dom::StorageUtils;
+using namespace mozilla::ipc;
+
+namespace {
+
+struct OriginKeyTest {
+ const char* mSpec;
+ const char* mOriginKey;
+ const char* mQuotaKey;
+};
+
+already_AddRefed<nsIPrincipal> GetContentPrincipal(const char* aSpec) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), nsDependentCString(aSpec));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ OriginAttributes attrs;
+
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(uri, attrs);
+
+ return principal.forget();
+}
+
+void CheckGeneratedOriginKey(nsIPrincipal* aPrincipal, const char* aOriginKey,
+ const char* aQuotaKey) {
+ nsCString originAttrSuffix;
+ nsCString originKey;
+ nsCString quotaKey;
+
+ aPrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix);
+
+ nsresult rv = aPrincipal->GetStorageOriginKey(originKey);
+ if (aOriginKey) {
+ ASSERT_EQ(rv, NS_OK) << "GetStorageOriginKey should not fail";
+ EXPECT_TRUE(originKey == nsDependentCString(aOriginKey));
+ } else {
+ ASSERT_NE(rv, NS_OK) << "GetStorageOriginKey should fail";
+ }
+
+ rv = aPrincipal->GetLocalStorageQuotaKey(quotaKey);
+ if (aQuotaKey) {
+ ASSERT_EQ(rv, NS_OK) << "GetLocalStorageQuotaKey should not fail";
+ EXPECT_TRUE(quotaKey == nsDependentCString(aQuotaKey));
+ } else {
+ ASSERT_NE(rv, NS_OK) << "GetLocalStorageQuotaKey should fail";
+ }
+
+ PrincipalInfo principalInfo;
+ rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo);
+ ASSERT_EQ(rv, NS_OK) << "PrincipalToPrincipalInfo should not fail";
+
+ const auto res = GenerateOriginKey2(principalInfo);
+ if (aOriginKey) {
+ ASSERT_TRUE(res.isOk())
+ << "GenerateOriginKey2 should not fail";
+ EXPECT_TRUE(res.inspect().second == nsDependentCString(aOriginKey));
+ } else {
+ ASSERT_TRUE(res.isErr())
+ << "GenerateOriginKey2 should fail";
+ }
+}
+
+} // namespace
+
+TEST(LocalStorage, OriginKey)
+{
+ // Check the system principal.
+ nsCOMPtr<nsIScriptSecurityManager> secMan =
+ nsContentUtils::GetSecurityManager();
+ ASSERT_TRUE(secMan)
+ << "GetSecurityManager() should not fail";
+
+ nsCOMPtr<nsIPrincipal> principal;
+ secMan->GetSystemPrincipal(getter_AddRefs(principal));
+ ASSERT_TRUE(principal)
+ << "GetSystemPrincipal() should not fail";
+
+ CheckGeneratedOriginKey(principal, nullptr, nullptr);
+
+ // Check the null principal.
+ principal = NullPrincipal::CreateWithoutOriginAttributes();
+ ASSERT_TRUE(principal)
+ << "CreateWithoutOriginAttributes() should not fail";
+
+ CheckGeneratedOriginKey(principal, nullptr, nullptr);
+
+ // Check content principals.
+ static const OriginKeyTest tests[] = {
+ {"http://localhost", "tsohlacol.:http:80", ":tsohlacol."},
+ {"http://www.mozilla.org", "gro.allizom.www.:http:80", ":gro.allizom."},
+ {"https://www.mozilla.org", "gro.allizom.www.:https:443",
+ ":gro.allizom."},
+ {"http://www.mozilla.org:32400", "gro.allizom.www.:http:32400",
+ ":gro.allizom."},
+ {"file:///Users/Joe/Sites/", "/setiS/eoJ/sresU/.:file",
+ ":/setiS/eoJ/sresU/."},
+ {"file:///Users/Joe/Sites/#foo", "/setiS/eoJ/sresU/.:file",
+ ":/setiS/eoJ/sresU/."},
+ {"file:///Users/Joe/Sites/?foo", "/setiS/eoJ/sresU/.:file",
+ ":/setiS/eoJ/sresU/."},
+ {"file:///Users/Joe/Sites", "/eoJ/sresU/.:file", ":/eoJ/sresU/."},
+ {"file:///Users/Joe/Sites#foo", "/eoJ/sresU/.:file", ":/eoJ/sresU/."},
+ {"file:///Users/Joe/Sites?foo", "/eoJ/sresU/.:file", ":/eoJ/sresU/."},
+ {"moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc/"
+ "_generated_background_page.html",
+ "cb0c762e20f1-1769-247e-de56-f8a11735.:moz-extension",
+ ":cb0c762e20f1-1769-247e-de56-f8a11735."},
+ {"http://[::1]:8/test.html", "1::.:http:8", ":1::."},
+ };
+
+ for (const auto& test : tests) {
+ principal = GetContentPrincipal(test.mSpec);
+ ASSERT_TRUE(principal)
+ << "GetContentPrincipal() should not fail";
+
+ CheckGeneratedOriginKey(principal, test.mOriginKey, test.mQuotaKey);
+ }
+}
diff --git a/dom/localstorage/test/gtest/moz.build b/dom/localstorage/test/gtest/moz.build
new file mode 100644
index 0000000000..f9d3c7bb0b
--- /dev/null
+++ b/dom/localstorage/test/gtest/moz.build
@@ -0,0 +1,17 @@
+# -*- 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 = [
+ "TestLocalStorage.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/localstorage",
+]
diff --git a/dom/localstorage/test/helpers.js b/dom/localstorage/test/helpers.js
new file mode 100644
index 0000000000..d668042343
--- /dev/null
+++ b/dom/localstorage/test/helpers.js
@@ -0,0 +1,78 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// testSteps is expected to be defined by the test using this file.
+/* global testSteps:false */
+
+function executeSoon(aFun) {
+ SpecialPowers.Services.tm.dispatchToMainThread({
+ run() {
+ aFun();
+ },
+ });
+}
+
+function clearAllDatabases() {
+ let qms = SpecialPowers.Services.qms;
+ let principal = SpecialPowers.wrap(document).nodePrincipal;
+ let request = qms.clearStoragesForPrincipal(principal);
+ return request;
+}
+
+if (!window.runTest) {
+ window.runTest = async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ info("Pushing preferences");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage.testing", true],
+ ["dom.quotaManager.testing", true],
+ ],
+ });
+
+ info("Clearing old databases");
+
+ await requestFinished(clearAllDatabases());
+
+ SimpleTest.registerCleanupFunction(async function() {
+ await requestFinished(clearAllDatabases());
+ });
+ };
+}
+
+function returnToEventLoop() {
+ return new Promise(function(resolve) {
+ executeSoon(resolve);
+ });
+}
+
+function getLocalStorage() {
+ return localStorage;
+}
+
+class RequestError extends Error {
+ constructor(resultCode, resultName) {
+ super(`Request failed (code: ${resultCode}, name: ${resultName})`);
+ this.name = "RequestError";
+ this.resultCode = resultCode;
+ this.resultName = resultName;
+ }
+}
+
+async function requestFinished(request) {
+ await new Promise(function(resolve) {
+ request.callback = SpecialPowers.wrapCallback(function() {
+ resolve();
+ });
+ });
+
+ if (request.resultCode !== SpecialPowers.Cr.NS_OK) {
+ throw new RequestError(request.resultCode, request.resultName);
+ }
+
+ return request.result;
+}
diff --git a/dom/localstorage/test/mochitest.ini b/dom/localstorage/test/mochitest.ini
new file mode 100644
index 0000000000..500bdd0a1c
--- /dev/null
+++ b/dom/localstorage/test/mochitest.ini
@@ -0,0 +1,10 @@
+# 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/.
+
+[DEFAULT]
+support-files =
+ helpers.js
+ unit/test_largeItems.js
+
+[test_largeItems.html]
diff --git a/dom/localstorage/test/page_private_ls.html b/dom/localstorage/test/page_private_ls.html
new file mode 100644
index 0000000000..795a814981
--- /dev/null
+++ b/dom/localstorage/test/page_private_ls.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ All the interesting stuff happens in ContentTask.spawn() calls.
+</body>
+</html>
diff --git a/dom/localstorage/test/test_largeItems.html b/dom/localstorage/test/test_largeItems.html
new file mode 100644
index 0000000000..92316085fc
--- /dev/null
+++ b/dom/localstorage/test/test_largeItems.html
@@ -0,0 +1,19 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>Large Items Test</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script type="text/javascript" src="unit/test_largeItems.js"></script>
+ <script type="text/javascript" src="helpers.js"></script>
+
+</head>
+
+<body onload="runTest();"></body>
+
+</html>
diff --git a/dom/localstorage/test/unit/archive_profile.zip b/dom/localstorage/test/unit/archive_profile.zip
new file mode 100644
index 0000000000..71b2d1e5f9
--- /dev/null
+++ b/dom/localstorage/test/unit/archive_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zip b/dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zip
new file mode 100644
index 0000000000..8cfa6e3d43
--- /dev/null
+++ b/dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/corruptedDatabase_profile.zip b/dom/localstorage/test/unit/corruptedDatabase_profile.zip
new file mode 100644
index 0000000000..2f60db2a45
--- /dev/null
+++ b/dom/localstorage/test/unit/corruptedDatabase_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/databaseShadowing-shared.js b/dom/localstorage/test/unit/databaseShadowing-shared.js
new file mode 100644
index 0000000000..ffee8579cb
--- /dev/null
+++ b/dom/localstorage/test/unit/databaseShadowing-shared.js
@@ -0,0 +1,130 @@
+/* import-globals-from head.js */
+
+const principalInfos = [
+ { url: "http://example.com", attrs: {} },
+
+ { url: "http://origin.test", attrs: {} },
+
+ { url: "http://prefix.test", attrs: {} },
+ { url: "http://prefix.test", attrs: { userContextId: 10 } },
+
+ { url: "http://pattern.test", attrs: { userContextId: 15 } },
+ { url: "http://pattern.test:8080", attrs: { userContextId: 15 } },
+ { url: "https://pattern.test", attrs: { userContextId: 15 } },
+];
+
+const surrogate = String.fromCharCode(0xdc00);
+const replacement = String.fromCharCode(0xfffd);
+const beginning = "beginning";
+const ending = "ending";
+const complexValue = beginning + surrogate + surrogate + ending;
+const corruptedValue = beginning + replacement + replacement + ending;
+
+function enableNextGenLocalStorage() {
+ info("Setting pref");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+}
+
+function disableNextGenLocalStorage() {
+ info("Setting pref");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ true
+ );
+}
+
+function storeData() {
+ for (let i = 0; i < principalInfos.length; i++) {
+ let principalInfo = principalInfos[i];
+ let principal = getPrincipal(principalInfo.url, principalInfo.attrs);
+
+ info("Getting storage");
+
+ let storage = getLocalStorage(principal);
+
+ info("Adding data");
+
+ storage.setItem("key0", "value0");
+ storage.clear();
+ storage.setItem("key1", "value1");
+ storage.removeItem("key1");
+ storage.setItem("key2", "value2");
+ storage.setItem("complexKey", complexValue);
+
+ info("Closing storage");
+
+ storage.close();
+ }
+}
+
+function exportShadowDatabase(name) {
+ info("Verifying shadow database");
+
+ let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let shadowDatabase = profileDir.clone();
+ shadowDatabase.append("webappsstore.sqlite");
+
+ let exists = shadowDatabase.exists();
+ ok(exists, "Shadow database does exist");
+
+ info("Copying shadow database");
+
+ let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ shadowDatabase.copyTo(currentDir, name);
+}
+
+function importShadowDatabase(name) {
+ info("Verifying shadow database");
+
+ let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ let shadowDatabase = currentDir.clone();
+ shadowDatabase.append(name);
+
+ let exists = shadowDatabase.exists();
+ if (!exists) {
+ return false;
+ }
+
+ info("Copying shadow database");
+
+ let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ shadowDatabase.copyTo(profileDir, "webappsstore.sqlite");
+
+ return true;
+}
+
+function verifyData(clearedOrigins, migrated = false) {
+ for (let i = 0; i < principalInfos.length; i++) {
+ let principalInfo = principalInfos[i];
+ let principal = getPrincipal(principalInfo.url, principalInfo.attrs);
+
+ info("Getting storage");
+
+ let storage = getLocalStorage(principal);
+
+ info("Verifying data");
+
+ if (clearedOrigins.includes(i)) {
+ ok(storage.getItem("key2") == null, "Correct value");
+ ok(storage.getItem("complexKey") == null, "Correct value");
+ } else {
+ ok(storage.getItem("key0") == null, "Correct value");
+ ok(storage.getItem("key1") == null, "Correct value");
+ is(storage.getItem("key2"), "value2", "Correct value");
+ is(
+ storage.getItem("complexKey"),
+ migrated ? corruptedValue : complexValue,
+ "Correct value"
+ );
+ }
+
+ info("Closing storage");
+
+ storage.close();
+ }
+}
diff --git a/dom/localstorage/test/unit/groupMismatch_profile.zip b/dom/localstorage/test/unit/groupMismatch_profile.zip
new file mode 100644
index 0000000000..182b013de0
--- /dev/null
+++ b/dom/localstorage/test/unit/groupMismatch_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/head.js b/dom/localstorage/test/unit/head.js
new file mode 100644
index 0000000000..eaca1ed173
--- /dev/null
+++ b/dom/localstorage/test/unit/head.js
@@ -0,0 +1,332 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests are expected to define testSteps.
+/* globals testSteps */
+
+const NS_ERROR_DOM_QUOTA_EXCEEDED_ERR = 22;
+
+function is(a, b, msg) {
+ Assert.equal(a, b, msg);
+}
+
+function ok(cond, msg) {
+ Assert.ok(!!cond, msg);
+}
+
+add_setup(function() {
+ do_get_profile();
+
+ enableTesting();
+
+ Cu.importGlobalProperties(["crypto"]);
+
+ registerCleanupFunction(resetTesting);
+});
+
+function returnToEventLoop() {
+ return new Promise(function(resolve) {
+ executeSoon(resolve);
+ });
+}
+
+function enableTesting() {
+ Services.prefs.setBoolPref("dom.simpleDB.enabled", true);
+ Services.prefs.setBoolPref("dom.storage.testing", true);
+
+ // xpcshell globals don't have associated clients in the Clients API sense, so
+ // we need to disable client validation so that the unit tests are allowed to
+ // use LocalStorage.
+ Services.prefs.setBoolPref("dom.storage.client_validation", false);
+
+ Services.prefs.setBoolPref("dom.quotaManager.testing", true);
+}
+
+function resetTesting() {
+ Services.prefs.clearUserPref("dom.quotaManager.testing");
+ Services.prefs.clearUserPref("dom.storage.client_validation");
+ Services.prefs.clearUserPref("dom.storage.testing");
+ Services.prefs.clearUserPref("dom.simpleDB.enabled");
+}
+
+function setGlobalLimit(globalLimit) {
+ Services.prefs.setIntPref(
+ "dom.quotaManager.temporaryStorage.fixedLimit",
+ globalLimit
+ );
+}
+
+function resetGlobalLimit() {
+ Services.prefs.clearUserPref("dom.quotaManager.temporaryStorage.fixedLimit");
+}
+
+function setOriginLimit(originLimit) {
+ Services.prefs.setIntPref("dom.storage.default_quota", originLimit);
+}
+
+function resetOriginLimit() {
+ Services.prefs.clearUserPref("dom.storage.default_quota");
+}
+
+function setTimeout(callback, timeout) {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+ timer.initWithCallback(
+ {
+ notify() {
+ callback();
+ },
+ },
+ timeout,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+
+ return timer;
+}
+
+function initStorage() {
+ return Services.qms.init();
+}
+
+function initTemporaryStorage() {
+ return Services.qms.initTemporaryStorage();
+}
+
+function initPersistentOrigin(principal) {
+ return Services.qms.initializePersistentOrigin(principal);
+}
+
+function initTemporaryOrigin(persistence, principal) {
+ return Services.qms.initializeTemporaryOrigin(persistence, principal);
+}
+
+function getOriginUsage(principal, fromMemory = false) {
+ let request = Services.qms.getUsageForPrincipal(
+ principal,
+ function() {},
+ fromMemory
+ );
+
+ return request;
+}
+
+function clear() {
+ let request = Services.qms.clear();
+
+ return request;
+}
+
+function clearOriginsByPattern(pattern) {
+ let request = Services.qms.clearStoragesForOriginAttributesPattern(pattern);
+
+ return request;
+}
+
+function clearOriginsByPrefix(principal, persistence) {
+ let request = Services.qms.clearStoragesForPrincipal(
+ principal,
+ persistence,
+ null,
+ true
+ );
+
+ return request;
+}
+
+function clearOrigin(principal, persistence) {
+ let request = Services.qms.clearStoragesForPrincipal(principal, persistence);
+
+ return request;
+}
+
+function reset() {
+ let request = Services.qms.reset();
+
+ return request;
+}
+
+function resetOrigin(principal) {
+ let request = Services.qms.resetStoragesForPrincipal(
+ principal,
+ "default",
+ "ls"
+ );
+
+ return request;
+}
+
+function installPackage(packageName) {
+ let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+
+ let packageFile = currentDir.clone();
+ packageFile.append(packageName + ".zip");
+
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(
+ Ci.nsIZipReader
+ );
+ zipReader.open(packageFile);
+
+ let entryNames = [];
+ let entries = zipReader.findEntries(null);
+ while (entries.hasMore()) {
+ let entry = entries.getNext();
+ entryNames.push(entry);
+ }
+ entryNames.sort();
+
+ for (let entryName of entryNames) {
+ let zipentry = zipReader.getEntry(entryName);
+
+ let file = getRelativeFile(entryName);
+
+ if (zipentry.isDirectory) {
+ file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+ } else {
+ let istream = zipReader.getInputStream(entryName);
+
+ var ostream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(file, -1, parseInt("0644", 8), 0);
+
+ let bostream = Cc[
+ "@mozilla.org/network/buffered-output-stream;1"
+ ].createInstance(Ci.nsIBufferedOutputStream);
+ bostream.init(ostream, 32768);
+
+ bostream.writeFrom(istream, istream.available());
+
+ istream.close();
+ bostream.close();
+ }
+ }
+
+ zipReader.close();
+}
+
+function getProfileDir() {
+ return Services.dirsvc.get("ProfD", Ci.nsIFile);
+}
+
+// Given a "/"-delimited path relative to the profile directory,
+// return an nsIFile representing the path. This does not test
+// for the existence of the file or parent directories.
+// It is safe even on Windows where the directory separator is not "/",
+// but make sure you're not passing in a "\"-delimited path.
+function getRelativeFile(relativePath) {
+ let profileDir = getProfileDir();
+
+ let file = profileDir.clone();
+ relativePath.split("/").forEach(function(component) {
+ file.append(component);
+ });
+
+ return file;
+}
+
+function repeatChar(count, ch) {
+ if (count == 0) {
+ return "";
+ }
+
+ let result = ch;
+ let count2 = count / 2;
+
+ // Double the input until it is long enough.
+ while (result.length <= count2) {
+ result += result;
+ }
+
+ // Use substring to hit the precise length target without using extra memory.
+ return result + result.substring(0, count - result.length);
+}
+
+function getPrincipal(url, attrs) {
+ let uri = Services.io.newURI(url);
+ if (!attrs) {
+ attrs = {};
+ }
+ return Services.scriptSecurityManager.createContentPrincipal(uri, attrs);
+}
+
+function getCurrentPrincipal() {
+ return Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
+}
+
+function getDefaultPrincipal() {
+ return getPrincipal("http://example.com");
+}
+
+function getSimpleDatabase(principal, persistence) {
+ let connection = Cc["@mozilla.org/dom/sdb-connection;1"].createInstance(
+ Ci.nsISDBConnection
+ );
+
+ if (!principal) {
+ principal = getDefaultPrincipal();
+ }
+
+ connection.init(principal, persistence);
+
+ return connection;
+}
+
+function getLocalStorage(principal) {
+ if (!principal) {
+ principal = getDefaultPrincipal();
+ }
+
+ return Services.domStorageManager.createStorage(
+ null,
+ principal,
+ principal,
+ ""
+ );
+}
+
+class RequestError extends Error {
+ constructor(resultCode, resultName) {
+ super(`Request failed (code: ${resultCode}, name: ${resultName})`);
+ this.name = "RequestError";
+ this.resultCode = resultCode;
+ this.resultName = resultName;
+ }
+}
+
+async function requestFinished(request) {
+ await new Promise(function(resolve) {
+ request.callback = function() {
+ resolve();
+ };
+ });
+
+ if (request.resultCode !== Cr.NS_OK) {
+ throw new RequestError(request.resultCode, request.resultName);
+ }
+
+ return request.result;
+}
+
+function loadSubscript(path) {
+ let file = do_get_file(path, false);
+ let uri = Services.io.newFileURI(file);
+ Services.scriptloader.loadSubScript(uri.spec);
+}
+
+async function readUsageFromUsageFile(usageFile) {
+ let file = await File.createFromNsIFile(usageFile);
+
+ let buffer = await new Promise(resolve => {
+ let reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result);
+ reader.readAsArrayBuffer(file);
+ });
+
+ // Manually getting the lower 32-bits because of the lack of support for
+ // 64-bit values currently from DataView/JS (other than the BigInt support
+ // that's currently behind a flag).
+ let view = new DataView(buffer, 8, 4);
+ return view.getUint32();
+}
diff --git a/dom/localstorage/test/unit/make_migration_emptyValue.js b/dom/localstorage/test/unit/make_migration_emptyValue.js
new file mode 100644
index 0000000000..12484ad7f3
--- /dev/null
+++ b/dom/localstorage/test/unit/make_migration_emptyValue.js
@@ -0,0 +1,23 @@
+/*
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+async function testSteps() {
+ const data = {
+ key: "foo",
+ value: "",
+ };
+
+ info("Setting pref");
+
+ Services.prefs.setBoolPref("dom.storage.next_gen", false);
+
+ info("Getting storage");
+
+ const storage = getLocalStorage();
+
+ info("Adding data");
+
+ storage.setItem(data.key, data.value);
+}
diff --git a/dom/localstorage/test/unit/migration_emptyValue_profile.zip b/dom/localstorage/test/unit/migration_emptyValue_profile.zip
new file mode 100644
index 0000000000..b829beae77
--- /dev/null
+++ b/dom/localstorage/test/unit/migration_emptyValue_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/migration_profile.zip b/dom/localstorage/test/unit/migration_profile.zip
new file mode 100644
index 0000000000..19dc3d4805
--- /dev/null
+++ b/dom/localstorage/test/unit/migration_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/schema3upgrade_profile.zip b/dom/localstorage/test/unit/schema3upgrade_profile.zip
new file mode 100644
index 0000000000..1ee9bfbf2e
--- /dev/null
+++ b/dom/localstorage/test/unit/schema3upgrade_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/schema4upgrade_profile.zip b/dom/localstorage/test/unit/schema4upgrade_profile.zip
new file mode 100644
index 0000000000..ae8ba09606
--- /dev/null
+++ b/dom/localstorage/test/unit/schema4upgrade_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/stringLength2_profile.zip b/dom/localstorage/test/unit/stringLength2_profile.zip
new file mode 100644
index 0000000000..de4d0fc3aa
--- /dev/null
+++ b/dom/localstorage/test/unit/stringLength2_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/stringLength_profile.zip b/dom/localstorage/test/unit/stringLength_profile.zip
new file mode 100644
index 0000000000..6cac890860
--- /dev/null
+++ b/dom/localstorage/test/unit/stringLength_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/test_archive.js b/dom/localstorage/test/unit/test_archive.js
new file mode 100644
index 0000000000..51af78db1b
--- /dev/null
+++ b/dom/localstorage/test/unit/test_archive.js
@@ -0,0 +1,78 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const lsArchiveFile = "storage/ls-archive.sqlite";
+
+ const principalInfo = {
+ url: "http://example.com",
+ attrs: {},
+ };
+
+ function checkStorage() {
+ let principal = getPrincipal(principalInfo.url, principalInfo.attrs);
+ let storage = getLocalStorage(principal);
+ try {
+ storage.open();
+ ok(true, "Did not throw");
+ } catch (ex) {
+ ok(false, "Should not have thrown");
+ }
+ }
+
+ info("Setting pref");
+
+ Services.prefs.setBoolPref("dom.storage.next_gen", true);
+
+ info("Sub test case 1 - Archive file is a directory.");
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ let archiveFile = getRelativeFile(lsArchiveFile);
+
+ archiveFile.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+
+ checkStorage();
+
+ info("Sub test case 2 - Corrupted archive file.");
+
+ info("Clearing");
+
+ request = clear();
+ await requestFinished(request);
+
+ let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ ostream.init(archiveFile, -1, parseInt("0644", 8), 0);
+ ostream.write("foobar", 6);
+ ostream.close();
+
+ checkStorage();
+
+ info("Sub test case 3 - Nonupdateable archive file.");
+
+ info("Clearing");
+
+ request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains storage.sqlite and storage/ls-archive.sqlite
+ // storage/ls-archive.sqlite was taken from FF 54 to force an upgrade.
+ // There's just one record in the webappsstore2 table. The record was
+ // modified by renaming the origin attribute userContextId to userContextKey.
+ // This triggers an error during the upgrade.
+ installPackage("archive_profile");
+
+ let fileSize = archiveFile.fileSize;
+ ok(fileSize > 0, "archive file size is greater than zero");
+
+ checkStorage();
+});
diff --git a/dom/localstorage/test/unit/test_clientValidation.js b/dom/localstorage/test/unit/test_clientValidation.js
new file mode 100644
index 0000000000..26dc2bfdf7
--- /dev/null
+++ b/dom/localstorage/test/unit/test_clientValidation.js
@@ -0,0 +1,32 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Because this is an xpcshell global, it does not have an associated client id.
+ * We turn on client validation for LocalStorage and ensure that we don't have
+ * access to LocalStorage.
+ */
+add_task(async function testSteps() {
+ const principal = getPrincipal("http://example.com");
+
+ info("Setting prefs");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+ Services.prefs.setBoolPref("dom.storage.client_validation", true);
+
+ info("Getting storage");
+
+ try {
+ getLocalStorage(principal);
+ ok(false, "Should have thrown");
+ } catch (ex) {
+ ok(true, "Did throw");
+ is(ex.name, "NS_ERROR_FAILURE", "Threw right Exception");
+ is(ex.result, Cr.NS_ERROR_FAILURE, "Threw with right result");
+ }
+});
diff --git a/dom/localstorage/test/unit/test_corruptedDatabase.js b/dom/localstorage/test/unit/test_corruptedDatabase.js
new file mode 100644
index 0000000000..da73bb92f6
--- /dev/null
+++ b/dom/localstorage/test/unit/test_corruptedDatabase.js
@@ -0,0 +1,73 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function doTest(profile) {
+ info("Testing profile " + profile);
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ installPackage(profile);
+
+ const principal = getPrincipal("http://example.org");
+
+ let storage = getLocalStorage(principal);
+
+ let length = storage.length;
+
+ ok(length === 0, "Correct length");
+
+ info("Resetting origin");
+
+ request = resetOrigin(principal);
+ await requestFinished(request);
+
+ info("Getting usage");
+
+ request = getOriginUsage(principal);
+ await requestFinished(request);
+
+ is(request.result.usage, 0, "Correct usage");
+}
+
+add_task(async function testSteps() {
+ info("Setting pref");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+
+ // XXX This should be refactored into separate sub test cases.
+
+ const profiles = [
+ // This profile contains one localStorage, all localStorage related files, a
+ // script for localStorage creation and the storage database:
+ // - storage/default/http+++example.org/ls
+ // - storage/ls-archive.sqlite
+ // - create_db.js
+ // - storage.sqlite
+ // - webappsstore.sqlite
+ // The file create_db.js in the package was run locally, specifically it was
+ // temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Manually change first 6 chars in data.sqlite to "foobar".
+ // 2. Remove the folder "storage/temporary".
+ "corruptedDatabase_profile",
+ // This profile is the same as corruptedDatabase_profile, except that the usage
+ // file (storage/default/http+++example.org/ls/usage) is missing.
+ "corruptedDatabase_missingUsageFile_profile",
+ ];
+
+ for (const profile of profiles) {
+ await doTest(profile);
+ }
+});
diff --git a/dom/localstorage/test/unit/test_databaseShadowing1.js b/dom/localstorage/test/unit/test_databaseShadowing1.js
new file mode 100644
index 0000000000..8582f434fc
--- /dev/null
+++ b/dom/localstorage/test/unit/test_databaseShadowing1.js
@@ -0,0 +1,23 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* import-globals-from databaseShadowing-shared.js */
+loadSubscript("databaseShadowing-shared.js");
+
+add_task(async function testSteps() {
+ enableNextGenLocalStorage();
+
+ storeData();
+
+ verifyData([]);
+
+ // Wait for all database connections to close.
+ let request = reset();
+ await requestFinished(request);
+
+ exportShadowDatabase("shadowdb.sqlite");
+
+ // The shadow database is now prepared for test_databaseShadowing2.js
+});
diff --git a/dom/localstorage/test/unit/test_databaseShadowing2.js b/dom/localstorage/test/unit/test_databaseShadowing2.js
new file mode 100644
index 0000000000..f7b0ddb1a2
--- /dev/null
+++ b/dom/localstorage/test/unit/test_databaseShadowing2.js
@@ -0,0 +1,17 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* import-globals-from databaseShadowing-shared.js */
+loadSubscript("databaseShadowing-shared.js");
+
+add_task(async function testSteps() {
+ // The shadow database was prepared in test_databaseShadowing1.js
+
+ disableNextGenLocalStorage();
+
+ ok(importShadowDatabase("shadowdb.sqlite"), "Import succeeded");
+
+ verifyData([], /* migrated */ true);
+});
diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js
new file mode 100644
index 0000000000..d88fde52e5
--- /dev/null
+++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js
@@ -0,0 +1,30 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* import-globals-from databaseShadowing-shared.js */
+loadSubscript("databaseShadowing-shared.js");
+
+add_task(async function testSteps() {
+ enableNextGenLocalStorage();
+
+ storeData();
+
+ verifyData([]);
+
+ let principal = getPrincipal("http://origin.test", {});
+ let request = clearOrigin(principal, "default");
+ await requestFinished(request);
+
+ verifyData([1]);
+
+ // Wait for all database connections to close.
+ request = reset();
+ await requestFinished(request);
+
+ exportShadowDatabase("shadowdb-clearedOrigin.sqlite");
+
+ // The shadow database is now prepared for
+ // test_databaseShadowing_clearOrigin2.js
+});
diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js
new file mode 100644
index 0000000000..83d792b496
--- /dev/null
+++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js
@@ -0,0 +1,17 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* import-globals-from databaseShadowing-shared.js */
+loadSubscript("databaseShadowing-shared.js");
+
+add_task(async function testSteps() {
+ // The shadow database was prepared in test_databaseShadowing_clearOrigin1.js
+
+ disableNextGenLocalStorage();
+
+ ok(importShadowDatabase("shadowdb-clearedOrigin.sqlite"), "Import succeeded");
+
+ verifyData([1], /* migrated */ true);
+});
diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js
new file mode 100644
index 0000000000..70367bbeff
--- /dev/null
+++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js
@@ -0,0 +1,29 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* import-globals-from databaseShadowing-shared.js */
+loadSubscript("databaseShadowing-shared.js");
+
+add_task(async function testSteps() {
+ enableNextGenLocalStorage();
+
+ storeData();
+
+ verifyData([]);
+
+ let request = clearOriginsByPattern(JSON.stringify({ userContextId: 15 }));
+ await requestFinished(request);
+
+ verifyData([4, 5, 6]);
+
+ // Wait for all database connections to close.
+ request = reset();
+ await requestFinished(request);
+
+ exportShadowDatabase("shadowdb-clearedOriginsByPattern.sqlite");
+
+ // The shadow database is now prepared for
+ // test_databaseShadowing_clearOriginsByPattern2.js
+});
diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js
new file mode 100644
index 0000000000..6c4d794d04
--- /dev/null
+++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js
@@ -0,0 +1,21 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* import-globals-from databaseShadowing-shared.js */
+loadSubscript("databaseShadowing-shared.js");
+
+add_task(async function testSteps() {
+ // The shadow database was prepared in
+ // test_databaseShadowing_clearOriginsByPattern1.js
+
+ disableNextGenLocalStorage();
+
+ ok(
+ importShadowDatabase("shadowdb-clearedOriginsByPattern.sqlite"),
+ "Import succeeded"
+ );
+
+ verifyData([4, 5, 6], /* migrated */ true);
+});
diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js
new file mode 100644
index 0000000000..2b605e953f
--- /dev/null
+++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js
@@ -0,0 +1,28 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* import-globals-from databaseShadowing-shared.js */
+loadSubscript("databaseShadowing-shared.js");
+
+add_task(async function testSteps() {
+ enableNextGenLocalStorage();
+
+ storeData();
+
+ verifyData([]);
+
+ let principal = getPrincipal("http://prefix.test", {});
+ let request = clearOriginsByPrefix(principal, "default");
+ await requestFinished(request);
+
+ // Wait for all database connections to close.
+ request = reset();
+ await requestFinished(request);
+
+ exportShadowDatabase("shadowdb-clearedOriginsByPrefix.sqlite");
+
+ // The shadow database is now prepared for
+ // test_databaseShadowing_clearOriginsByPrefix2.js
+});
diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js
new file mode 100644
index 0000000000..892a470723
--- /dev/null
+++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js
@@ -0,0 +1,21 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* import-globals-from databaseShadowing-shared.js */
+loadSubscript("databaseShadowing-shared.js");
+
+add_task(async function testSteps() {
+ // The shadow database was prepared in
+ // test_databaseShadowing_clearOriginsByPrefix1.js
+
+ disableNextGenLocalStorage();
+
+ ok(
+ importShadowDatabase("shadowdb-clearedOriginsByPrefix.sqlite"),
+ "Import succeeded"
+ );
+
+ verifyData([2, 3], /* migrated */ true);
+});
diff --git a/dom/localstorage/test/unit/test_eviction.js b/dom/localstorage/test/unit/test_eviction.js
new file mode 100644
index 0000000000..d8c10b771f
--- /dev/null
+++ b/dom/localstorage/test/unit/test_eviction.js
@@ -0,0 +1,91 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const globalLimitKB = 5 * 1024;
+
+ const data = {};
+ data.sizeKB = 1 * 1024;
+ data.key = "A";
+ data.value = repeatChar(data.sizeKB * 1024 - data.key.length, ".");
+ data.urlCount = globalLimitKB / data.sizeKB;
+
+ function getSpec(index) {
+ return "http://example" + index + ".com";
+ }
+
+ info("Setting prefs");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+ Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false);
+
+ info("Setting limits");
+
+ setGlobalLimit(globalLimitKB);
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Getting storages");
+
+ let storages = [];
+ for (let i = 0; i < data.urlCount; i++) {
+ let storage = getLocalStorage(getPrincipal(getSpec(i)));
+ storages.push(storage);
+ }
+
+ info("Filling up entire default storage");
+
+ for (let i = 0; i < data.urlCount; i++) {
+ storages[i].setItem(data.key, data.value);
+ await returnToEventLoop();
+ }
+
+ info("Verifying no more data can be written");
+
+ for (let i = 0; i < data.urlCount; i++) {
+ try {
+ storages[i].setItem("B", "");
+ ok(false, "Should have thrown");
+ } catch (ex) {
+ ok(true, "Did throw");
+ ok(DOMException.isInstance(ex), "Threw DOMException");
+ is(ex.name, "QuotaExceededError", "Threw right DOMException");
+ is(ex.code, NS_ERROR_DOM_QUOTA_EXCEEDED_ERR, "Threw with right code");
+ }
+ }
+
+ info("Closing first origin");
+
+ storages[0].close();
+
+ let principal = getPrincipal("http://example0.com");
+
+ request = resetOrigin(principal);
+ await requestFinished(request);
+
+ info("Getting usage for first origin");
+
+ request = getOriginUsage(principal);
+ await requestFinished(request);
+
+ is(request.result.usage, data.sizeKB * 1024, "Correct usage");
+
+ info("Verifying more data data can be written");
+
+ for (let i = 1; i < data.urlCount; i++) {
+ storages[i].setItem("B", "");
+ }
+
+ info("Getting usage for first origin");
+
+ request = getOriginUsage(principal);
+ await requestFinished(request);
+
+ is(request.result.usage, 0, "Zero usage");
+});
diff --git a/dom/localstorage/test/unit/test_flushing.js b/dom/localstorage/test/unit/test_flushing.js
new file mode 100644
index 0000000000..b33ef7c099
--- /dev/null
+++ b/dom/localstorage/test/unit/test_flushing.js
@@ -0,0 +1,72 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+/**
+ * This test is mainly to verify that the flush operation detaches the shadow
+ * database in the event of early return due to error. See bug 1559029.
+ */
+
+add_task(async function testSteps() {
+ const principal1 = getPrincipal("http://example1.com");
+
+ const usageFile1 = getRelativeFile(
+ "storage/default/http+++example1.com/ls/usage"
+ );
+
+ const principal2 = getPrincipal("http://example2.com");
+
+ const data = {
+ key: "foo",
+ value: "bar",
+ };
+
+ const flushSleepTimeSec = 6;
+
+ info("Setting prefs");
+
+ Services.prefs.setBoolPref("dom.storage.next_gen", true);
+
+ info("Getting storage 1");
+
+ let storage1 = getLocalStorage(principal1);
+
+ info("Adding item");
+
+ storage1.setItem(data.key, data.value);
+
+ info("Creating usage as a directory");
+
+ // This will cause a failure during the flush for first principal.
+ usageFile1.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+
+ info("Getting storage 2");
+
+ let storage2 = getLocalStorage(principal2);
+
+ info("Adding item");
+
+ storage2.setItem(data.key, data.value);
+
+ // The flush for second principal shouldn't be affected by failed flush for
+ // first principal.
+
+ info(
+ "Sleeping for " +
+ flushSleepTimeSec +
+ " seconds to let all flushes " +
+ "finish"
+ );
+
+ await new Promise(function(resolve) {
+ setTimeout(resolve, flushSleepTimeSec * 1000);
+ });
+
+ info("Resetting");
+
+ // Wait for all database connections to close.
+ let request = reset();
+ await requestFinished(request);
+});
diff --git a/dom/localstorage/test/unit/test_groupLimit.js b/dom/localstorage/test/unit/test_groupLimit.js
new file mode 100644
index 0000000000..92ff07b7ab
--- /dev/null
+++ b/dom/localstorage/test/unit/test_groupLimit.js
@@ -0,0 +1,85 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const groupLimitKB = 10 * 1024;
+
+ const globalLimitKB = groupLimitKB * 5;
+
+ const originLimit = 10 * 1024;
+
+ const urls = [
+ "http://example.com",
+ "http://test1.example.com",
+ "https://test2.example.com",
+ "http://test3.example.com:8080",
+ ];
+
+ const data = {};
+ data.sizeKB = 5 * 1024;
+ data.key = "A";
+ data.value = repeatChar(data.sizeKB * 1024 - data.key.length, ".");
+ data.urlCount = groupLimitKB / data.sizeKB;
+
+ info("Setting pref");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+ Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false);
+
+ info("Setting limits");
+
+ setGlobalLimit(globalLimitKB);
+
+ let request = clear();
+ await requestFinished(request);
+
+ setOriginLimit(originLimit);
+
+ info("Getting storages");
+
+ let storages = [];
+ for (let i = 0; i < urls.length; i++) {
+ let storage = getLocalStorage(getPrincipal(urls[i]));
+ storages.push(storage);
+ }
+
+ info("Filling up the whole group");
+
+ for (let i = 0; i < data.urlCount; i++) {
+ storages[i].setItem(data.key, data.value);
+ await returnToEventLoop();
+ }
+
+ info("Verifying no more data can be written");
+
+ for (let i = 0; i < urls.length; i++) {
+ try {
+ storages[i].setItem("B", "");
+ ok(false, "Should have thrown");
+ } catch (ex) {
+ ok(true, "Did throw");
+ ok(DOMException.isInstance(ex), "Threw DOMException");
+ is(ex.name, "QuotaExceededError", "Threw right DOMException");
+ is(ex.code, NS_ERROR_DOM_QUOTA_EXCEEDED_ERR, "Threw with right code");
+ }
+ }
+
+ info("Clearing first origin");
+
+ storages[0].clear();
+
+ // Let the internal snapshot finish (usage is not descreased until all
+ // snapshots finish)..
+ await returnToEventLoop();
+
+ info("Verifying more data can be written");
+
+ for (let i = 0; i < urls.length; i++) {
+ storages[i].setItem("B", "");
+ }
+});
diff --git a/dom/localstorage/test/unit/test_groupMismatch.js b/dom/localstorage/test/unit/test_groupMismatch.js
new file mode 100644
index 0000000000..46533aa2f7
--- /dev/null
+++ b/dom/localstorage/test/unit/test_groupMismatch.js
@@ -0,0 +1,45 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that metadata files with old group information
+ * get updated, so writing to local storage won't cause a crash because of null
+ * quota object. See bug 1516333.
+ */
+
+add_task(async function testSteps() {
+ const principal = getPrincipal("https://foo.bar.mozilla-iot.org");
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains one initialized origin directory, a script for origin
+ // initialization and the storage database:
+ // - storage/default/https+++foo.bar.mozilla-iot.org
+ // - create_db.js
+ // - storage.sqlite
+ // The file create_db.js in the package was run locally, specifically it was
+ // temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Manually change the group in .metadata and .metadata-v2 from
+ // "bar.mozilla-iot.org" to "mozilla-iot.org".
+ // 2. Remove the folder "storage/temporary".
+ // 3. Remove the file "storage/ls-archive.sqlite".
+ installPackage("groupMismatch_profile");
+
+ info("Getting storage");
+
+ let storage = getLocalStorage(principal);
+
+ info("Adding item");
+
+ storage.setItem("foo", "bar");
+});
diff --git a/dom/localstorage/test/unit/test_largeItems.js b/dom/localstorage/test/unit/test_largeItems.js
new file mode 100644
index 0000000000..3ea6bd21b4
--- /dev/null
+++ b/dom/localstorage/test/unit/test_largeItems.js
@@ -0,0 +1,88 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test repeatedly setting values that are just under the LocalStorage quota
+ * limit without yielding control flow in order to verify that the write
+ * optimizer is present / works. If there was no write optimizer present, the
+ * IPC message size limit would be exceeded, resulting in a crash.
+ */
+
+add_task(async function testSteps() {
+ const globalLimitKB = 5 * 1024;
+
+ // 18 and more iterations would produce an IPC message with size greater than
+ // 256 MB if write optimizer was not present. This number was determined
+ // experimentally by running the test with disabled write optimizer.
+ const numberOfIterations = 18;
+
+ const randomStringBlockSize = 65536;
+
+ // We need to use a random string because LS internally tries to compress
+ // values.
+ function getRandomString(size) {
+ let crypto = this.window ? this.window.crypto : this.crypto;
+ let decoder = new TextDecoder("ISO-8859-2");
+
+ function getRandomStringBlock(array) {
+ crypto.getRandomValues(array);
+ return decoder.decode(array);
+ }
+
+ let string = "";
+
+ let quotient = size / randomStringBlockSize;
+ if (quotient) {
+ let array = new Uint8Array(randomStringBlockSize);
+ for (let i = 1; i <= quotient; i++) {
+ string += getRandomStringBlock(array);
+ }
+ }
+
+ let remainder = size % randomStringBlockSize;
+ if (remainder) {
+ let array = new Uint8Array(remainder);
+ string += getRandomStringBlock(array);
+ }
+
+ return string;
+ }
+
+ const data = {};
+ data.key = "foo";
+ data.value = getRandomString(
+ globalLimitKB * 1024 -
+ data.key.length -
+ numberOfIterations.toString().length
+ );
+
+ info("Setting pref");
+
+ // By disabling snapshot reusing, we guarantee that the snapshot will be
+ // checkpointed once control returns to the event loop.
+ if (this.window) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.storage.snapshot_reusing", false]],
+ });
+ } else {
+ Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false);
+ }
+
+ info("Getting storage");
+
+ let storage = getLocalStorage();
+
+ info("Adding/updating item");
+
+ for (var i = 0; i < numberOfIterations; i++) {
+ storage.setItem(data.key, data.value + i);
+ }
+
+ info("Returning to event loop");
+
+ await returnToEventLoop();
+
+ ok(!storage.hasSnapshot, "Snapshot successfully finished");
+});
diff --git a/dom/localstorage/test/unit/test_lsng_enabled.js b/dom/localstorage/test/unit/test_lsng_enabled.js
new file mode 100644
index 0000000000..d978aaa901
--- /dev/null
+++ b/dom/localstorage/test/unit/test_lsng_enabled.js
@@ -0,0 +1,13 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that LSNG is not accidentally disabled which
+ * can lead to a data loss in a combination with disabled shadow writes.
+ */
+
+add_task(async function testSteps() {
+ ok(Services.domStorageManager.nextGenLocalStorageEnabled, "LSNG enabled");
+});
diff --git a/dom/localstorage/test/unit/test_migration.js b/dom/localstorage/test/unit/test_migration.js
new file mode 100644
index 0000000000..1249eb076f
--- /dev/null
+++ b/dom/localstorage/test/unit/test_migration.js
@@ -0,0 +1,127 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const principalInfos = [
+ { url: "http://localhost", attrs: {} },
+ { url: "http://www.mozilla.org", attrs: {} },
+ { url: "http://example.com", attrs: {} },
+ { url: "http://example.org", attrs: { userContextId: 5 } },
+
+ { url: "http://origin.test", attrs: {} },
+
+ { url: "http://prefix.test", attrs: {} },
+ { url: "http://prefix.test", attrs: { userContextId: 10 } },
+
+ { url: "http://pattern.test", attrs: { userContextId: 15 } },
+ { url: "http://pattern.test:8080", attrs: { userContextId: 15 } },
+ { url: "https://pattern.test", attrs: { userContextId: 15 } },
+ ];
+
+ const data = {
+ key: "foo",
+ value: "bar",
+ };
+
+ function verifyData(clearedOrigins) {
+ info("Getting storages");
+
+ let storages = [];
+ for (let i = 0; i < principalInfos.length; i++) {
+ let principalInfo = principalInfos[i];
+ let principal = getPrincipal(principalInfo.url, principalInfo.attrs);
+ let storage = getLocalStorage(principal);
+ storages.push(storage);
+ }
+
+ info("Verifying data");
+
+ for (let i = 0; i < storages.length; i++) {
+ let value = storages[i].getItem(data.key + i);
+ if (clearedOrigins.includes(i)) {
+ is(value, null, "Correct value");
+ } else {
+ is(value, data.value + i, "Correct value");
+ }
+ }
+ }
+
+ info("Setting pref");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+
+ info("Stage 1 - Testing archived data migration");
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains storage.sqlite and webappsstore.sqlite. The file
+ // create_db.js in the package was run locally, specifically it was
+ // temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js
+ installPackage("migration_profile");
+
+ verifyData([]);
+
+ info("Stage 2 - Testing archived data clearing");
+
+ for (let type of ["origin", "prefix", "pattern"]) {
+ info("Clearing");
+
+ request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // See the comment for the first installPackage() call.
+ installPackage("migration_profile");
+
+ let clearedOrigins = [];
+
+ switch (type) {
+ case "origin": {
+ let principal = getPrincipal("http://origin.test", {});
+ request = clearOrigin(principal, "default");
+ await requestFinished(request);
+
+ clearedOrigins.push(4);
+
+ break;
+ }
+
+ case "prefix": {
+ let principal = getPrincipal("http://prefix.test", {});
+ request = clearOriginsByPrefix(principal, "default");
+ await requestFinished(request);
+
+ clearedOrigins.push(5, 6);
+
+ break;
+ }
+
+ case "pattern": {
+ request = clearOriginsByPattern(JSON.stringify({ userContextId: 15 }));
+ await requestFinished(request);
+
+ clearedOrigins.push(7, 8, 9);
+
+ break;
+ }
+
+ default: {
+ throw new Error("Unknown type: " + type);
+ }
+ }
+
+ verifyData(clearedOrigins);
+ }
+});
diff --git a/dom/localstorage/test/unit/test_migration_emptyValue.js b/dom/localstorage/test/unit/test_migration_emptyValue.js
new file mode 100644
index 0000000000..dd09c82e88
--- /dev/null
+++ b/dom/localstorage/test/unit/test_migration_emptyValue.js
@@ -0,0 +1,37 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const data = {
+ key: "foo",
+ value: "",
+ };
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains storage.sqlite and webappsstore.sqlite.
+ // The archive migration_emptyValue_profile.zip was created by running
+ // make_migration_emptyValue.js locally, specifically the special test was
+ // temporarily activated in xpcshell.ini and then it was run as:
+ // mach test --interactive dom/localstorage/test/unit/make_migration_emptyValue.js
+ // Before packaging, additional manual steps are needed:
+ // 1. Folder "cache2" is removed.
+ // 2. Folder "crashes" is removed.
+ // 3. File "mozinfo.json" is removed.
+ installPackage("migration_emptyValue_profile");
+
+ info("Getting storage");
+
+ const storage = getLocalStorage();
+
+ info("Verifying data");
+
+ is(storage.getItem(data.key), data.value, "Correct value");
+});
diff --git a/dom/localstorage/test/unit/test_old_lsng_pref.js b/dom/localstorage/test/unit/test_old_lsng_pref.js
new file mode 100644
index 0000000000..d502ee8779
--- /dev/null
+++ b/dom/localstorage/test/unit/test_old_lsng_pref.js
@@ -0,0 +1,17 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that the old pref for switching LS
+ * implementations has no effect anymore.
+ */
+
+add_task(async function testSteps() {
+ info("Setting pref");
+
+ Services.prefs.setBoolPref("dom.storage.next_gen", false);
+
+ ok(Services.domStorageManager.nextGenLocalStorageEnabled, "LSNG enabled");
+});
diff --git a/dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js b/dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js
new file mode 100644
index 0000000000..88a2e45d2a
--- /dev/null
+++ b/dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js
@@ -0,0 +1,70 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const url = "http://example.com";
+
+ info("Setting pref");
+
+ Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false);
+
+ const items = [
+ { key: "key01", value: "value01" },
+ { key: "key02", value: "value02" },
+ { key: "key03", value: "value03" },
+ { key: "key04", value: "value04" },
+ { key: "key05", value: "value05" },
+ ];
+
+ info("Getting storage");
+
+ let storage = getLocalStorage(getPrincipal(url));
+
+ // 1st snapshot
+
+ info("Adding data");
+
+ for (let item of items) {
+ storage.setItem(item.key, item.value);
+ }
+
+ info("Returning to event loop");
+
+ await returnToEventLoop();
+
+ // 2nd snapshot
+
+ // Remove first two items, add some new items and add the two items back.
+
+ storage.removeItem("key01");
+ storage.removeItem("key02");
+
+ storage.setItem("key06", "value06");
+ storage.setItem("key07", "value07");
+ storage.setItem("key08", "value08");
+
+ storage.setItem("key01", "value01");
+ storage.setItem("key02", "value02");
+
+ info("Saving key order");
+
+ let savedKeys = Object.keys(storage);
+
+ info("Returning to event loop");
+
+ await returnToEventLoop();
+
+ // 3rd snapshot
+
+ info("Verifying key order");
+
+ let keys = Object.keys(storage);
+
+ is(keys.length, savedKeys.length);
+
+ for (let i = 0; i < keys.length; i++) {
+ is(keys[i], savedKeys[i], "Correct key");
+ }
+});
diff --git a/dom/localstorage/test/unit/test_originInit.js b/dom/localstorage/test/unit/test_originInit.js
new file mode 100644
index 0000000000..48afdf971b
--- /dev/null
+++ b/dom/localstorage/test/unit/test_originInit.js
@@ -0,0 +1,372 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const storageDirName = "storage";
+ const persistenceTypeDefaultDirName = "default";
+ const persistenceTypePersistentDirName = "permanent";
+
+ const principal = getPrincipal("http://example.com");
+
+ const originDirName = "http+++example.com";
+
+ const clientLSDirName = "ls";
+
+ const dataFile = getRelativeFile(
+ `${storageDirName}/${persistenceTypeDefaultDirName}/${originDirName}/` +
+ `${clientLSDirName}/data.sqlite`
+ );
+
+ const usageJournalFile = getRelativeFile(
+ `${storageDirName}/${persistenceTypeDefaultDirName}/${originDirName}/` +
+ `${clientLSDirName}/usage-journal`
+ );
+
+ const usageFile = getRelativeFile(
+ `${storageDirName}/${persistenceTypeDefaultDirName}/${originDirName}/` +
+ `${clientLSDirName}/usage`
+ );
+
+ const persistentLSDir = getRelativeFile(
+ `${storageDirName}/${persistenceTypePersistentDirName}/${originDirName}/` +
+ `${clientLSDirName}`
+ );
+
+ const data = {};
+ data.key = "key1";
+ data.value = "value1";
+ data.usage = data.key.length + data.value.length;
+
+ const usageFileCookie = 0x420a420a;
+
+ async function createTestOrigin() {
+ let storage = getLocalStorage(principal);
+
+ storage.setItem(data.key, data.value);
+
+ let request = reset();
+ await requestFinished(request);
+ }
+
+ async function createPersistentTestOrigin() {
+ let database = getSimpleDatabase(principal, "persistent");
+
+ let request = database.open("data");
+ await requestFinished(request);
+
+ request = reset();
+ await requestFinished(request);
+ }
+
+ function removeFile(file) {
+ file.remove(false);
+ }
+
+ function createEmptyFile(file) {
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o0644);
+ }
+
+ function createEmptyDirectory(dir) {
+ dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o0755);
+ }
+
+ function getBinaryOutputStream(file) {
+ var ostream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(file, -1, parseInt("0644", 8), 0);
+
+ let bstream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ bstream.setOutputStream(ostream);
+
+ return bstream;
+ }
+
+ async function initTestOrigin() {
+ let request = initStorage();
+ await requestFinished(request);
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ request = initTemporaryOrigin("default", principal);
+ await requestFinished(request);
+ }
+
+ async function initPersistentTestOrigin() {
+ let request = initStorage();
+ await requestFinished(request);
+
+ request = initPersistentOrigin(principal);
+ await requestFinished(request);
+ }
+
+ async function checkFiles(wantData, wantUsage) {
+ let exists = dataFile.exists();
+ if (wantData) {
+ ok(exists, "Data file does exist");
+ } else {
+ ok(!exists, "Data file doesn't exist");
+ }
+
+ exists = usageJournalFile.exists();
+ ok(!exists, "Usage journal file doesn't exist");
+
+ exists = usageFile.exists();
+ if (wantUsage) {
+ ok(exists, "Usage file does exist");
+ } else {
+ ok(!exists, "Usage file doesn't exist");
+ return;
+ }
+
+ let usage = await readUsageFromUsageFile(usageFile);
+ ok(usage == data.usage, "Correct usage");
+ }
+
+ async function clearTestOrigin() {
+ let request = clearOrigin(principal, "default");
+ await requestFinished(request);
+ }
+
+ async function clearPersistentTestOrigin() {
+ let request = clearOrigin(principal, "persistent");
+ await requestFinished(request);
+ }
+
+ info("Setting prefs");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+
+ info(
+ "Stage 1 - " +
+ "data.sqlite file doesn't exist, " +
+ "usage-journal file doesn't exist, " +
+ "any usage file exists"
+ );
+
+ await createTestOrigin();
+
+ removeFile(dataFile);
+
+ await initTestOrigin();
+
+ await checkFiles(/* wantData */ false, /* wantUsage */ false);
+
+ await clearTestOrigin();
+
+ info(
+ "Stage 2 - " +
+ "data.sqlite file doesn't exist, " +
+ "any usage-journal file exists, " +
+ "any usage file exists"
+ );
+
+ await createTestOrigin();
+
+ removeFile(dataFile);
+ createEmptyFile(usageJournalFile);
+
+ await initTestOrigin();
+
+ await checkFiles(/* wantData */ false, /* wantUsage */ false);
+
+ await clearTestOrigin();
+
+ info(
+ "Stage 3 - " +
+ "valid data.sqlite file exists, " +
+ "usage-journal file doesn't exist, " +
+ "usage file doesn't exist"
+ );
+
+ await createTestOrigin();
+
+ removeFile(usageFile);
+
+ await initTestOrigin();
+
+ await checkFiles(/* wantData */ true, /* wantUsage */ true);
+
+ await clearTestOrigin();
+
+ info(
+ "Stage 4 - " +
+ "valid data.sqlite file exists, " +
+ "usage-journal file doesn't exist, " +
+ "invalid (wrong file size) usage file exists"
+ );
+
+ await createTestOrigin();
+
+ removeFile(usageFile);
+ createEmptyFile(usageFile);
+
+ await initTestOrigin();
+
+ await checkFiles(/* wantData */ true, /* wantUsage */ true);
+
+ await clearTestOrigin();
+
+ info(
+ "Stage 5 - " +
+ "valid data.sqlite file exists, " +
+ "usage-journal file doesn't exist, " +
+ "invalid (wrong cookie) usage file exists"
+ );
+
+ await createTestOrigin();
+
+ let stream = getBinaryOutputStream(usageFile);
+ stream.write32(usageFileCookie - 1);
+ stream.write64(data.usage);
+ stream.close();
+
+ await initTestOrigin();
+
+ await checkFiles(/* wantData */ true, /* wantUsage */ true);
+
+ await clearTestOrigin();
+
+ info(
+ "Stage 6 - " +
+ "valid data.sqlite file exists, " +
+ "usage-journal file doesn't exist, " +
+ "valid usage file exists"
+ );
+
+ await createTestOrigin();
+
+ await initTestOrigin();
+
+ await checkFiles(/* wantData */ true, /* wantUsage */ true);
+
+ await clearTestOrigin();
+
+ info(
+ "Stage 7 - " +
+ "valid data.sqlite file exists, " +
+ "any usage-journal exists, " +
+ "usage file doesn't exist"
+ );
+
+ await createTestOrigin();
+
+ createEmptyFile(usageJournalFile);
+ removeFile(usageFile);
+
+ await initTestOrigin();
+
+ await checkFiles(/* wantData */ true, /* wantUsage */ true);
+
+ await clearTestOrigin();
+
+ info(
+ "Stage 8 - " +
+ "valid data.sqlite file exists, " +
+ "any usage-journal exists, " +
+ "invalid (wrong file size) usage file exists"
+ );
+
+ await createTestOrigin();
+
+ createEmptyFile(usageJournalFile);
+ removeFile(usageFile);
+ createEmptyFile(usageFile);
+
+ await initTestOrigin();
+
+ await checkFiles(/* wantData */ true, /* wantUsage */ true);
+
+ await clearTestOrigin();
+
+ info(
+ "Stage 9 - " +
+ "valid data.sqlite file exists, " +
+ "any usage-journal exists, " +
+ "invalid (wrong cookie) usage file exists"
+ );
+
+ await createTestOrigin();
+
+ createEmptyFile(usageJournalFile);
+ stream = getBinaryOutputStream(usageFile);
+ stream.write32(usageFileCookie - 1);
+ stream.write64(data.usage);
+ stream.close();
+
+ await initTestOrigin();
+
+ await checkFiles(/* wantData */ true, /* wantUsage */ true);
+
+ await clearTestOrigin();
+
+ info(
+ "Stage 10 - " +
+ "valid data.sqlite file exists, " +
+ "any usage-journal exists, " +
+ "invalid (wrong usage) usage file exists"
+ );
+
+ await createTestOrigin();
+
+ createEmptyFile(usageJournalFile);
+ stream = getBinaryOutputStream(usageFile);
+ stream.write32(usageFileCookie);
+ stream.write64(data.usage - 1);
+ stream.close();
+
+ await initTestOrigin();
+
+ await checkFiles(/* wantData */ true, /* wantUsage */ true);
+
+ await clearTestOrigin();
+
+ info(
+ "Stage 11 - " +
+ "valid data.sqlite file exists, " +
+ "any usage-journal exists, " +
+ "valid usage file exists"
+ );
+
+ await createTestOrigin();
+
+ createEmptyFile(usageJournalFile);
+
+ await initTestOrigin();
+
+ await checkFiles(/* wantData */ true, /* wantUsage */ true);
+
+ await clearTestOrigin();
+
+ // Verify that InitializeOrigin doesn't fail when a
+ // storage/permanent/${origin}/ls exists.
+ info(
+ "Stage 12 - Testing initialization of ls directory placed in permanent " +
+ "origin directory"
+ );
+
+ await createPersistentTestOrigin();
+
+ createEmptyDirectory(persistentLSDir);
+
+ try {
+ await initPersistentTestOrigin();
+
+ ok(true, "Should not have thrown");
+ } catch (ex) {
+ ok(false, "Should not have thrown");
+ }
+
+ let exists = persistentLSDir.exists();
+ ok(exists, "ls directory in permanent origin directory does exist");
+
+ await clearPersistentTestOrigin();
+});
diff --git a/dom/localstorage/test/unit/test_preloading.js b/dom/localstorage/test/unit/test_preloading.js
new file mode 100644
index 0000000000..977ab10d99
--- /dev/null
+++ b/dom/localstorage/test/unit/test_preloading.js
@@ -0,0 +1,84 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const principals = [
+ getPrincipal("http://example.com", {}),
+ getPrincipal("http://example.com", { privateBrowsingId: 1 }),
+ ];
+
+ async function isPreloaded(principal) {
+ return Services.domStorageManager.isPreloaded(principal);
+ }
+
+ info("Setting prefs");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+ Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false);
+
+ for (const principal of principals) {
+ info("Getting storage");
+
+ let storage = getLocalStorage(principal);
+
+ ok(
+ !(await isPreloaded(principal)),
+ "Data is not preloaded after getting storage"
+ );
+
+ info("Opening storage");
+
+ storage.open();
+
+ ok(await isPreloaded(principal), "Data is preloaded after opening storage");
+
+ info("Closing storage");
+
+ storage.close();
+
+ if (principal.privateBrowsingId > 0) {
+ ok(
+ await isPreloaded(principal),
+ "Data is still preloaded after closing storage"
+ );
+
+ info("Closing private session");
+
+ Services.obs.notifyObservers(null, "last-pb-context-exited");
+
+ ok(
+ !(await isPreloaded(principal)),
+ "Data is not preloaded anymore after closing private session"
+ );
+ } else {
+ ok(
+ !(await isPreloaded(principal)),
+ "Data is not preloaded anymore after closing storage"
+ );
+ }
+
+ info("Opening storage again");
+
+ storage.open();
+
+ ok(
+ await isPreloaded(principal),
+ "Data is preloaded after opening storage again"
+ );
+
+ info("Clearing origin");
+
+ let request = clearOrigin(principal, "default");
+ await requestFinished(request);
+
+ ok(
+ !(await isPreloaded(principal)),
+ "Data is not preloaded after clearing origin"
+ );
+ }
+});
diff --git a/dom/localstorage/test/unit/test_schema3upgrade.js b/dom/localstorage/test/unit/test_schema3upgrade.js
new file mode 100644
index 0000000000..4b851642ea
--- /dev/null
+++ b/dom/localstorage/test/unit/test_schema3upgrade.js
@@ -0,0 +1,39 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const url = "http://example.com";
+
+ info("Setting pref");
+
+ Services.prefs.setBoolPref("dom.storage.next_gen", true);
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains one initialized origin directory with local
+ // storage data, local storage archive, a script for origin initialization,
+ // the storage database and the web apps store database:
+ // - storage/default/http+++example.com
+ // - storage/ls-archive.sqlite
+ // - create_db.js
+ // - storage.sqlite
+ // - webappsstore.sqlite
+ // The file create_db.js in the package was run locally (with a build with
+ // local storage archive version 1 and database schema version 2),
+ // specifically it was temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Remove the folder "storage/temporary".
+ installPackage("schema3upgrade_profile");
+
+ let storage = getLocalStorage(getPrincipal(url));
+ storage.open();
+});
diff --git a/dom/localstorage/test/unit/test_schema4upgrade.js b/dom/localstorage/test/unit/test_schema4upgrade.js
new file mode 100644
index 0000000000..a6c308af35
--- /dev/null
+++ b/dom/localstorage/test/unit/test_schema4upgrade.js
@@ -0,0 +1,39 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const url = "http://example.com";
+
+ info("Setting pref");
+
+ Services.prefs.setBoolPref("dom.storage.next_gen", true);
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains one initialized origin directory with local
+ // storage data, local storage archive, a script for origin initialization,
+ // the storage database and the web apps store database:
+ // - storage/default/http+++example.com
+ // - storage/ls-archive.sqlite
+ // - storage.sqlite
+ // - test_create_db.js
+ // - webappsstore.sqlite
+ //
+ // The file test_create_db.js in the package was run locally by
+ // adding it temporarily to xpcshell.ini and then executed with
+ // mach xpcshell-test --headless dom/localstorage/test/unit/test_create_db.js
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Remove the folder "storage/temporary".
+ installPackage("schema4upgrade_profile");
+
+ let storage = getLocalStorage(getPrincipal(url));
+ storage.open();
+});
diff --git a/dom/localstorage/test/unit/test_snapshotting.js b/dom/localstorage/test/unit/test_snapshotting.js
new file mode 100644
index 0000000000..4b639395f7
--- /dev/null
+++ b/dom/localstorage/test/unit/test_snapshotting.js
@@ -0,0 +1,330 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const url = "http://example.com";
+
+ info("Setting pref");
+
+ Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false);
+
+ const items = [
+ { key: "key01", value: "value01" },
+ { key: "key02", value: "value02" },
+ { key: "key03", value: "value03" },
+ { key: "key04", value: "value04" },
+ { key: "key05", value: "value05" },
+ { key: "key06", value: "value06" },
+ { key: "key07", value: "value07" },
+ { key: "key08", value: "value08" },
+ { key: "key09", value: "value09" },
+ { key: "key10", value: "value10" },
+ ];
+
+ let sizeOfOneKey;
+ let sizeOfOneValue;
+ let sizeOfOneItem;
+ let sizeOfKeys = 0;
+ let sizeOfItems = 0;
+
+ for (let i = 0; i < items.length; i++) {
+ let item = items[i];
+ let sizeOfKey = item.key.length;
+ let sizeOfValue = item.value.length;
+ let sizeOfItem = sizeOfKey + sizeOfValue;
+ if (i == 0) {
+ sizeOfOneKey = sizeOfKey;
+ sizeOfOneValue = sizeOfValue;
+ sizeOfOneItem = sizeOfItem;
+ }
+ sizeOfKeys += sizeOfKey;
+ sizeOfItems += sizeOfItem;
+ }
+
+ info("Size of one key is " + sizeOfOneKey);
+ info("Size of one value is " + sizeOfOneValue);
+ info("Size of one item is " + sizeOfOneItem);
+ info("Size of keys is " + sizeOfKeys);
+ info("Size of items is " + sizeOfItems);
+
+ const prefillValues = [
+ // Zero prefill (prefill disabled)
+ 0,
+ // Less than one key length prefill
+ sizeOfOneKey - 1,
+ // Greater than one key length and less than one item length prefill
+ sizeOfOneKey + 1,
+ // Precisely one item length prefill
+ sizeOfOneItem,
+ // Precisely two times one item length prefill
+ 2 * sizeOfOneItem,
+ // Precisely three times one item length prefill
+ 3 * sizeOfOneItem,
+ // Precisely four times one item length prefill
+ 4 * sizeOfOneItem,
+ // Precisely size of keys prefill
+ sizeOfKeys,
+ // Less than size of keys plus one value length prefill
+ sizeOfKeys + sizeOfOneValue - 1,
+ // Precisely size of keys plus one value length prefill
+ sizeOfKeys + sizeOfOneValue,
+ // Greater than size of keys plus one value length and less than size of
+ // keys plus two times one value length prefill
+ sizeOfKeys + sizeOfOneValue + 1,
+ // Precisely size of keys plus two times one value length prefill
+ sizeOfKeys + 2 * sizeOfOneValue,
+ // Precisely size of keys plus three times one value length prefill
+ sizeOfKeys + 3 * sizeOfOneValue,
+ // Precisely size of keys plus four times one value length prefill
+ sizeOfKeys + 4 * sizeOfOneValue,
+ // Precisely size of keys plus five times one value length prefill
+ sizeOfKeys + 5 * sizeOfOneValue,
+ // Precisely size of keys plus six times one value length prefill
+ sizeOfKeys + 6 * sizeOfOneValue,
+ // Precisely size of keys plus seven times one value length prefill
+ sizeOfKeys + 7 * sizeOfOneValue,
+ // Precisely size of keys plus eight times one value length prefill
+ sizeOfKeys + 8 * sizeOfOneValue,
+ // Precisely size of keys plus nine times one value length prefill
+ sizeOfKeys + 9 * sizeOfOneValue,
+ // Precisely size of items prefill
+ sizeOfItems,
+ // Unlimited prefill
+ -1,
+ ];
+
+ for (let prefillValue of prefillValues) {
+ info("Setting prefill value to " + prefillValue);
+
+ Services.prefs.setIntPref("dom.storage.snapshot_prefill", prefillValue);
+
+ const gradualPrefillValues = [
+ // Zero gradual prefill
+ 0,
+ // Less than one key length gradual prefill
+ sizeOfOneKey - 1,
+ // Greater than one key length and less than one item length gradual
+ // prefill
+ sizeOfOneKey + 1,
+ // Precisely one item length gradual prefill
+ sizeOfOneItem,
+ // Precisely two times one item length gradual prefill
+ 2 * sizeOfOneItem,
+ // Precisely three times one item length gradual prefill
+ 3 * sizeOfOneItem,
+ // Precisely four times one item length gradual prefill
+ 4 * sizeOfOneItem,
+ // Precisely five times one item length gradual prefill
+ 5 * sizeOfOneItem,
+ // Precisely six times one item length gradual prefill
+ 6 * sizeOfOneItem,
+ // Precisely seven times one item length gradual prefill
+ 7 * sizeOfOneItem,
+ // Precisely eight times one item length gradual prefill
+ 8 * sizeOfOneItem,
+ // Precisely nine times one item length gradual prefill
+ 9 * sizeOfOneItem,
+ // Precisely size of items prefill
+ sizeOfItems,
+ // Unlimited gradual prefill
+ -1,
+ ];
+
+ for (let gradualPrefillValue of gradualPrefillValues) {
+ info("Setting gradual prefill value to " + gradualPrefillValue);
+
+ Services.prefs.setIntPref(
+ "dom.storage.snapshot_gradual_prefill",
+ gradualPrefillValue
+ );
+
+ info("Getting storage");
+
+ let storage = getLocalStorage(getPrincipal(url));
+
+ // 1st snapshot
+
+ info("Adding data");
+
+ for (let item of items) {
+ storage.setItem(item.key, item.value);
+ }
+
+ info("Saving key order");
+
+ // This forces GetKeys to be called internally.
+ let savedKeys = Object.keys(storage);
+
+ // GetKey should match GetKeys
+ for (let i = 0; i < savedKeys.length; i++) {
+ is(storage.key(i), savedKeys[i], "Correct key");
+ }
+
+ info("Returning to event loop");
+
+ // Returning to event loop forces the internal snapshot to finish.
+ await returnToEventLoop();
+
+ // 2nd snapshot
+
+ info("Verifying length");
+
+ is(storage.length, items.length, "Correct length");
+
+ info("Verifying key order");
+
+ let keys = Object.keys(storage);
+
+ is(keys.length, savedKeys.length);
+
+ for (let i = 0; i < keys.length; i++) {
+ is(keys[i], savedKeys[i], "Correct key");
+ }
+
+ info("Verifying values");
+
+ for (let item of items) {
+ is(storage.getItem(item.key), item.value, "Correct value");
+ }
+
+ info("Returning to event loop");
+
+ await returnToEventLoop();
+
+ // 3rd snapshot
+
+ // Force key2 to load.
+ storage.getItem("key02");
+
+ // Fill out write infos a bit.
+ storage.removeItem("key05");
+ storage.setItem("key05", "value05");
+ storage.removeItem("key05");
+ storage.setItem("key11", "value11");
+ storage.setItem("key05", "value05");
+
+ items.push({ key: "key11", value: "value11" });
+
+ info("Verifying length");
+
+ is(storage.length, items.length, "Correct length");
+
+ // This forces to get all keys from the parent and then apply write infos
+ // on already cached values.
+ savedKeys = Object.keys(storage);
+
+ info("Verifying values");
+
+ for (let item of items) {
+ is(storage.getItem(item.key), item.value, "Correct value");
+ }
+
+ storage.removeItem("key11");
+
+ items.pop();
+
+ info("Returning to event loop");
+
+ await returnToEventLoop();
+
+ // 4th snapshot
+
+ // Force loading of all items.
+ info("Verifying length");
+
+ is(storage.length, items.length, "Correct length");
+
+ info("Verifying values");
+
+ for (let item of items) {
+ is(storage.getItem(item.key), item.value, "Correct value");
+ }
+
+ is(storage.getItem("key11"), null, "Correct value");
+
+ info("Returning to event loop");
+
+ await returnToEventLoop();
+
+ // 5th snapshot
+
+ // Force loading of all keys.
+ info("Saving key order");
+
+ savedKeys = Object.keys(storage);
+
+ // Force loading of all items.
+ info("Verifying length");
+
+ is(storage.length, items.length, "Correct length");
+
+ info("Verifying values");
+
+ for (let item of items) {
+ is(storage.getItem(item.key), item.value, "Correct value");
+ }
+
+ is(storage.getItem("key11"), null, "Correct value");
+
+ info("Returning to event loop");
+
+ await returnToEventLoop();
+
+ // 6th snapshot
+ info("Verifying unknown item");
+
+ is(storage.getItem("key11"), null, "Correct value");
+
+ info("Verifying unknown item again");
+
+ is(storage.getItem("key11"), null, "Correct value");
+
+ info("Returning to event loop");
+
+ await returnToEventLoop();
+
+ // 7th snapshot
+
+ // Save actual key order.
+ info("Saving key order");
+
+ savedKeys = Object.keys(storage);
+
+ await returnToEventLoop();
+
+ // 8th snapshot
+
+ // Force loading of all items, but in reverse order.
+ info("Getting values");
+
+ for (let i = items.length - 1; i >= 0; i--) {
+ let item = items[i];
+ storage.getItem(item.key);
+ }
+
+ info("Verifying key order");
+
+ keys = Object.keys(storage);
+
+ is(keys.length, savedKeys.length);
+
+ for (let i = 0; i < keys.length; i++) {
+ is(keys[i], savedKeys[i], "Correct key");
+ }
+
+ await returnToEventLoop();
+
+ // 9th snapshot
+
+ info("Clearing");
+
+ storage.clear();
+
+ info("Returning to event loop");
+
+ await returnToEventLoop();
+ }
+ }
+});
diff --git a/dom/localstorage/test/unit/test_stringLength.js b/dom/localstorage/test/unit/test_stringLength.js
new file mode 100644
index 0000000000..52ee59f7d6
--- /dev/null
+++ b/dom/localstorage/test/unit/test_stringLength.js
@@ -0,0 +1,74 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const principal = getPrincipal("http://example.org");
+
+ const data = {};
+ data.key = "foobar";
+ data.secondKey = "foobaz";
+ data.value = {
+ length: 25637,
+ };
+ data.usage = data.key.length + data.value.length;
+
+ async function checkUsage(expectedUsage) {
+ info("Checking usage");
+
+ // This forces any pending changes to be flushed to disk. It also forces
+ // data to be reloaded from disk at next localStorage API call.
+ request = resetOrigin(principal);
+ await requestFinished(request);
+
+ request = getOriginUsage(principal);
+ await requestFinished(request);
+
+ is(request.result.usage, expectedUsage, "Correct usage");
+ }
+
+ info("Setting pref");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+
+ info("Stage 1 - Checking usage after profile installation");
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains storage.sqlite and webappsstore.sqlite.
+ installPackage("stringLength_profile");
+
+ await checkUsage(0);
+
+ info("Stage 2 - Checking usage after archived data migration");
+
+ info("Opening database");
+
+ let storage = getLocalStorage(principal);
+ storage.open();
+
+ await checkUsage(data.usage);
+
+ info("Stage 3 - Checking usage after copying the value");
+
+ info("Adding a second copy of the value");
+
+ let value = storage.getItem(data.key);
+ storage.setItem(data.secondKey, value);
+
+ await checkUsage(2 * data.usage);
+
+ info("Stage 4 - Checking length of the copied value");
+
+ value = storage.getItem(data.secondKey);
+ ok(value.length === data.value.length, "Correct string length");
+});
diff --git a/dom/localstorage/test/unit/test_stringLength2.js b/dom/localstorage/test/unit/test_stringLength2.js
new file mode 100644
index 0000000000..f1a1a902cf
--- /dev/null
+++ b/dom/localstorage/test/unit/test_stringLength2.js
@@ -0,0 +1,79 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that string length is correctly computed for
+ * database values containing NULs. See bug 1541681.
+ */
+
+add_task(async function testSteps() {
+ const principal = getPrincipal("http://example.org");
+
+ const data = {};
+ data.key = "foobar";
+ data.secondKey = "foobaz";
+ data.value = {
+ length: 19253,
+ };
+ data.usage = data.key.length + data.value.length;
+
+ async function checkUsage(expectedUsage) {
+ info("Checking usage");
+
+ // This forces any pending changes to be flushed to disk. It also forces
+ // data to be reloaded from disk at next localStorage API call.
+ request = resetOrigin(principal);
+ await requestFinished(request);
+
+ request = getOriginUsage(principal);
+ await requestFinished(request);
+
+ is(request.result.usage, expectedUsage, "Correct usage");
+ }
+
+ info("Setting pref");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+
+ info("Stage 1 - Checking usage after profile installation");
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains storage.sqlite and webappsstore.sqlite.
+ installPackage("stringLength2_profile");
+
+ await checkUsage(0);
+
+ info("Stage 2 - Checking usage after archived data migration");
+
+ info("Opening database");
+
+ let storage = getLocalStorage(principal);
+ storage.open();
+
+ await checkUsage(data.usage);
+
+ info("Stage 3 - Checking usage after copying the value");
+
+ info("Adding a second copy of the value");
+
+ let value = storage.getItem(data.key);
+ storage.setItem(data.secondKey, value);
+
+ await checkUsage(2 * data.usage);
+
+ info("Stage 4 - Checking length of the copied value");
+
+ value = storage.getItem(data.secondKey);
+ ok(value.length === data.value.length, "Correct string length");
+});
diff --git a/dom/localstorage/test/unit/test_unicodeCharacters.js b/dom/localstorage/test/unit/test_unicodeCharacters.js
new file mode 100644
index 0000000000..9e48274161
--- /dev/null
+++ b/dom/localstorage/test/unit/test_unicodeCharacters.js
@@ -0,0 +1,205 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const interpretChar = (chars, index) => {
+ return chars
+ .charCodeAt(index)
+ .toString(16)
+ .padStart(4, "0");
+};
+
+const hexEncode = str => {
+ let result = "";
+ const len = str.length;
+ for (let i = 0; i < len; ++i) {
+ result += interpretChar(str, i);
+ }
+ return result;
+};
+
+const collectCorrupted = (expected, actual) => {
+ const len = Math.min(expected.length, actual.length);
+ let notEquals = [];
+ for (let i = 0; i < len; ++i) {
+ if (expected[i] !== actual[i]) {
+ notEquals.push([hexEncode(expected[i]), hexEncode(actual[i])]);
+ }
+ }
+ return notEquals;
+};
+
+const sanitizeOutputWithSurrogates = (testValue, prefix = "") => {
+ let utf8What = prefix;
+ for (let i = 0; i < testValue.length; ++i) {
+ const valueChar = testValue.charCodeAt(i);
+ const isPlanar = 0xd800 <= valueChar && valueChar <= 0xdfff;
+ utf8What += isPlanar ? "\\u" + interpretChar(testValue, i) : testValue[i];
+ }
+ return utf8What;
+};
+
+const getEncodingSample = () => {
+ const expectedSample =
+ "3681207208613504e0a5028800b945551988c60050008027ebc2808c00d38e806e03d8210ac906722b85499be9d00000";
+
+ let result = "";
+ const len = expectedSample.length;
+ for (let i = 0; i < len; i += 4) {
+ result += String.fromCharCode(parseInt(expectedSample.slice(i, i + 4), 16));
+ }
+ return result;
+};
+
+const getSeparatedBasePlane = () => {
+ let result = "";
+ for (let i = 0xffff; i >= 0; --i) {
+ result += String.fromCharCode(i) + "\n";
+ }
+ return result;
+};
+
+const getJoinedBasePlane = () => {
+ let result = "";
+ for (let i = 0; i <= 0xffff; ++i) {
+ result += String.fromCharCode(i);
+ }
+ return result;
+};
+
+const getSurrogateCombinations = () => {
+ const upperLead = String.fromCharCode(0xdbff);
+ const lowerTrail = String.fromCharCode(0xdc00);
+
+ const regularSlot = ["w", "abcdefghijklmnopqrst", "aaaaaaaaaaaaaaaaaaaa", ""];
+ const surrogateSlot = [lowerTrail, upperLead];
+
+ let samples = [];
+ for (const leadSnippet of regularSlot) {
+ for (const firstSlot of surrogateSlot) {
+ for (const trailSnippet of regularSlot) {
+ for (const secondSlot of surrogateSlot) {
+ samples.push(leadSnippet + firstSlot + secondSlot + trailSnippet);
+ }
+ samples.push(leadSnippet + firstSlot + trailSnippet);
+ }
+ }
+ }
+
+ return samples;
+};
+
+const fetchFrom = async (itemKey, sample, meanwhile) => {
+ const principal = getPrincipal("http://example.com/", {});
+
+ let request = clearOrigin(principal);
+ await requestFinished(request);
+
+ const storage = getLocalStorage(principal);
+
+ await storage.setItem(itemKey, sample);
+
+ await meanwhile(principal);
+
+ return storage.getItem(itemKey);
+};
+
+/**
+ * Value fetched from existing snapshot based on
+ * existing in-memory datastore in the parent process
+ * without any communication between content/parent
+ */
+const fetchFromExistingSnapshotExistingDatastore = async (itemKey, sample) => {
+ return fetchFrom(itemKey, sample, async () => {});
+};
+
+/**
+ * Value fetched from newly created snapshot based on
+ * existing in-memory datastore in the parent process
+ */
+const fetchFromNewSnapshotExistingDatastore = async (itemKey, sample) => {
+ return fetchFrom(itemKey, sample, async () => {
+ await returnToEventLoop();
+ });
+};
+
+/**
+ * Value fetched from newly created snapshot based on newly created
+ * in-memory datastore based on database in the parent process
+ */
+const fetchFromNewSnapshotNewDatastore = async (itemKey, sample) => {
+ return fetchFrom(itemKey, sample, async principal => {
+ let request = resetOrigin(principal);
+ await requestFinished(request);
+ });
+};
+
+add_task(async function testSteps() {
+ /* This test is based on bug 1681300 */
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+ Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false);
+
+ const reportWhat = (testKey, testValue) => {
+ if (testKey.length + testValue.length > 82) {
+ return testKey;
+ }
+ return sanitizeOutputWithSurrogates(testValue, /* prefix */ testKey + ":");
+ };
+
+ const testFetchMode = async (testType, storeAndLookup) => {
+ const testPairs = [
+ { testEmptyValue: [""] },
+ { testSampleKey: [getEncodingSample()] },
+ { testSeparatedKey: [getSeparatedBasePlane()] },
+ { testJoinedKey: [getJoinedBasePlane()] },
+ { testCombinations: getSurrogateCombinations() },
+ ];
+
+ for (const testPair of testPairs) {
+ for (const [testKey, expectedValues] of Object.entries(testPair)) {
+ for (const expected of expectedValues) {
+ const actual = await storeAndLookup(testKey, expected);
+ const testInfo = reportWhat(testKey, expected);
+ is(
+ null != actual,
+ true,
+ testType + ": Value not null for " + testInfo
+ );
+ is(
+ expected.length,
+ actual.length,
+ testType + ": Returned size for " + testInfo
+ );
+
+ const notEquals = collectCorrupted(expected, actual);
+ for (let i = 0; i < notEquals.length; ++i) {
+ is(
+ notEquals[i][0],
+ notEquals[i][1],
+ testType + ": Unequal character at " + i + " for " + testInfo
+ );
+ }
+ }
+ }
+ }
+ };
+
+ await testFetchMode(
+ "ExistingSnapshotExistingDatastore",
+ fetchFromExistingSnapshotExistingDatastore
+ );
+
+ await testFetchMode(
+ "NewSnapshotExistingDatastore",
+ fetchFromNewSnapshotExistingDatastore
+ );
+
+ await testFetchMode(
+ "NewSnapshotNewDatastore",
+ fetchFromNewSnapshotNewDatastore
+ );
+});
diff --git a/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js b/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js
new file mode 100644
index 0000000000..0da4e8584d
--- /dev/null
+++ b/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js
@@ -0,0 +1,69 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test verifies that group and origin strings for URIs with special
+ * characters are consistent between calling
+ * EnsureQuotaForOringin/EnsureOriginIsInitailized and GetQuotaObject in
+ * PrepareDatastoreOp, so writing to local storage won't cause a crash because
+ * of a null quota object. See bug 1516333.
+ */
+
+add_task(async function testSteps() {
+ /**
+ * The edge cases are specified in this array of origins. Each edge case must
+ * contain two properties uri and path (origin directory path relative to the
+ * profile directory).
+ */
+ const origins = [
+ {
+ uri: "file:///test'.html",
+ path: "storage/default/file++++test'.html",
+ },
+ {
+ uri: "file:///test>.html",
+ path: "storage/default/file++++test%3E.html",
+ },
+ ];
+
+ info("Setting prefs");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+
+ for (let origin of origins) {
+ const principal = getPrincipal(origin.uri);
+
+ let originDir = getRelativeFile(origin.path);
+
+ info("Checking the origin directory existence");
+
+ ok(
+ !originDir.exists(),
+ `The origin directory ${origin.path} should not exists`
+ );
+
+ info("Getting storage");
+
+ let storage = getLocalStorage(principal);
+
+ info("Adding item");
+
+ storage.setItem("foo", "bar");
+
+ info("Resetting origin");
+
+ // This forces any pending changes to be flushed to disk (including origin
+ // directory creation).
+ let request = resetOrigin(principal);
+ await requestFinished(request);
+
+ info("Checking the origin directory existence");
+
+ ok(originDir.exists(), `The origin directory ${origin.path} should exist`);
+ }
+});
diff --git a/dom/localstorage/test/unit/test_usage.js b/dom/localstorage/test/unit/test_usage.js
new file mode 100644
index 0000000000..552a45e4a6
--- /dev/null
+++ b/dom/localstorage/test/unit/test_usage.js
@@ -0,0 +1,69 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const data = {};
+ data.key = "key1";
+ data.value = "value1";
+ data.usage = data.key.length + data.value.length;
+
+ const principal = getPrincipal("http://example.com");
+
+ info("Setting prefs");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+
+ info("Stage 1 - Testing usage after adding item");
+
+ info("Getting storage");
+
+ let storage = getLocalStorage(principal);
+
+ info("Adding item");
+
+ storage.setItem(data.key, data.value);
+
+ info("Resetting origin");
+
+ let request = resetOrigin(principal);
+ await requestFinished(request);
+
+ info("Getting usage");
+
+ request = getOriginUsage(principal);
+ await requestFinished(request);
+
+ is(request.result.usage, data.usage, "Correct usage");
+
+ info("Resetting");
+
+ request = reset();
+ await requestFinished(request);
+
+ info("Stage 2 - Testing usage after removing item");
+
+ info("Getting storage");
+
+ storage = getLocalStorage(principal);
+
+ info("Removing item");
+
+ storage.removeItem(data.key);
+
+ info("Resetting origin");
+
+ request = resetOrigin(principal);
+ await requestFinished(request);
+
+ info("Getting usage");
+
+ request = getOriginUsage(principal);
+ await requestFinished(request);
+
+ is(request.result.usage, 0, "Correct usage");
+});
diff --git a/dom/localstorage/test/unit/test_usageAfterMigration.js b/dom/localstorage/test/unit/test_usageAfterMigration.js
new file mode 100644
index 0000000000..a0bd5efd5b
--- /dev/null
+++ b/dom/localstorage/test/unit/test_usageAfterMigration.js
@@ -0,0 +1,164 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSteps() {
+ const principal = getPrincipal("http://example.com");
+
+ const dataFile = getRelativeFile(
+ "storage/default/http+++example.com/ls/data.sqlite"
+ );
+
+ const usageJournalFile = getRelativeFile(
+ "storage/default/http+++example.com/ls/usage-journal"
+ );
+
+ const usageFile = getRelativeFile(
+ "storage/default/http+++example.com/ls/usage"
+ );
+
+ const data = {};
+ data.key = "foo";
+ data.value = "bar";
+ data.usage = data.key.length + data.value.length;
+
+ async function createStorageForMigration(createUsageDir) {
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains storage.sqlite and webappsstore.sqlite. The file
+ // create_db.js in the package was run locally, specifically it was
+ // temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js
+ installPackage("usageAfterMigration_profile");
+
+ if (createUsageDir) {
+ // Origin must be initialized before the usage dir is created.
+
+ info("Initializing storage");
+
+ request = initStorage();
+ await requestFinished(request);
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Initializing origin");
+
+ request = initTemporaryOrigin("default", principal);
+ await requestFinished(request);
+
+ info("Creating usage as a directory");
+
+ // This will cause a failure during migration.
+ usageFile.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+ }
+ }
+
+ function verifyData() {
+ ok(dataFile.exists(), "Data file does exist");
+ }
+
+ async function verifyUsage(success) {
+ info("Verifying usage in memory");
+
+ let request = getOriginUsage(principal, /* fromMemory */ true);
+ await requestFinished(request);
+
+ if (success) {
+ is(request.result.usage, data.usage, "Correct usage");
+ } else {
+ is(request.result.usage, 0, "Zero usage");
+ }
+
+ info("Verifying usage on disk");
+
+ if (success) {
+ ok(!usageJournalFile.exists(), "Usage journal file doesn't exist");
+ ok(usageFile.exists(), "Usage file does exist");
+ let usage = await readUsageFromUsageFile(usageFile);
+ is(usage, data.usage, "Correct usage");
+ } else {
+ ok(usageJournalFile.exists(), "Usage journal file does exist");
+ ok(usageFile.exists(), "Usage file does exist");
+ }
+ }
+
+ info("Setting prefs");
+
+ Services.prefs.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+
+ info("Stage 1 - Testing usage after successful data migration");
+
+ await createStorageForMigration(/* createUsageDir */ false);
+
+ info("Getting storage");
+
+ let storage = getLocalStorage(principal);
+
+ info("Opening");
+
+ storage.open();
+
+ verifyData();
+
+ await verifyUsage(/* success */ true);
+
+ info("Stage 2 - Testing usage after unsuccessful data migration");
+
+ await createStorageForMigration(/* createUsageDir */ true);
+
+ info("Getting storage");
+
+ storage = getLocalStorage(principal);
+
+ info("Opening");
+
+ try {
+ storage.open();
+ ok(false, "Should have thrown");
+ } catch (ex) {
+ ok(true, "Did throw");
+ }
+
+ verifyData();
+
+ await verifyUsage(/* success */ false);
+
+ info("Stage 3 - Testing usage after unsuccessful/successful data migration");
+
+ await createStorageForMigration(/* createUsageDir */ true);
+
+ info("Getting storage");
+
+ storage = getLocalStorage(principal);
+
+ info("Opening");
+
+ try {
+ storage.open();
+ ok(false, "Should have thrown");
+ } catch (ex) {
+ ok(true, "Did throw");
+ }
+
+ usageFile.remove(true);
+
+ info("Opening");
+
+ storage.open();
+
+ verifyData();
+
+ await verifyUsage(/* success */ true);
+});
diff --git a/dom/localstorage/test/unit/usageAfterMigration_profile.zip b/dom/localstorage/test/unit/usageAfterMigration_profile.zip
new file mode 100644
index 0000000000..30a73292c3
--- /dev/null
+++ b/dom/localstorage/test/unit/usageAfterMigration_profile.zip
Binary files differ
diff --git a/dom/localstorage/test/unit/xpcshell.ini b/dom/localstorage/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..9b86f34349
--- /dev/null
+++ b/dom/localstorage/test/unit/xpcshell.ini
@@ -0,0 +1,73 @@
+# 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/.
+
+[DEFAULT]
+head = head.js
+tags = condprof
+support-files =
+ archive_profile.zip
+ corruptedDatabase_profile.zip
+ corruptedDatabase_missingUsageFile_profile.zip
+ groupMismatch_profile.zip
+ migration_profile.zip
+ schema3upgrade_profile.zip
+ schema4upgrade_profile.zip
+ stringLength2_profile.zip
+ stringLength_profile.zip
+ usageAfterMigration_profile.zip
+
+[make_migration_emptyValue.js]
+skip-if = true # Only used for recreating migration_emptyValue_profile.zip
+[test_archive.js]
+[test_clientValidation.js]
+[test_corruptedDatabase.js]
+[test_databaseShadowing1.js]
+prefs =
+ dom.storage.shadow_writes=true
+run-sequentially = test_databaseShadowing2.js depends on a file produced by this test
+[test_databaseShadowing2.js]
+run-sequentially = this test depends on a file produced by test_databaseShadowing1.js
+[test_databaseShadowing_clearOrigin1.js]
+prefs =
+ dom.storage.shadow_writes=true
+run-sequentially = test_databaseShadowing_clearOrigin2.js depends on a file produced by this test
+[test_databaseShadowing_clearOrigin2.js]
+run-sequentially = this test depends on a file produced by test_databaseShadowing_clearOrigin1.js
+[test_databaseShadowing_clearOriginsByPattern1.js]
+prefs =
+ dom.storage.shadow_writes=true
+run-sequentially = test_databaseShadowing_clearOriginsByPattern2.js depends on a file produced by this test
+[test_databaseShadowing_clearOriginsByPattern2.js]
+run-sequentially = this test depends on a file produced by test_databaseShadowing_clearOriginsByPattern1.js
+[test_databaseShadowing_clearOriginsByPrefix1.js]
+prefs =
+ dom.storage.shadow_writes=true
+run-sequentially = test_databaseShadowing_clearOriginsByPrefix2.js depends on a file produced by this test
+[test_databaseShadowing_clearOriginsByPrefix2.js]
+run-sequentially = this test depends on a file produced by test_databaseShadowing_clearOriginsByPrefix1.js
+[test_eviction.js]
+[test_flushing.js]
+[test_groupLimit.js]
+[test_groupMismatch.js]
+[test_largeItems.js]
+[test_lsng_enabled.js]
+[test_migration.js]
+[test_migration_emptyValue.js]
+support-files =
+ migration_emptyValue_profile.zip
+[test_old_lsng_pref.js]
+[test_orderingAfterRemoveAdd.js]
+[test_originInit.js]
+[test_preloading.js]
+[test_schema3upgrade.js]
+[test_schema4upgrade.js]
+[test_snapshotting.js]
+skip-if = tsan # Unreasonably slow, bug 1612707
+requesttimeoutfactor = 4
+[test_stringLength.js]
+[test_stringLength2.js]
+[test_unicodeCharacters.js]
+[test_uri_encoding_edge_cases.js]
+[test_usage.js]
+[test_usageAfterMigration.js]