summaryrefslogtreecommitdiffstats
path: root/dom/localstorage
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/localstorage/ActorsChild.cpp325
-rw-r--r--dom/localstorage/ActorsChild.h283
-rw-r--r--dom/localstorage/ActorsParent.cpp9059
-rw-r--r--dom/localstorage/ActorsParent.h89
-rw-r--r--dom/localstorage/LSDatabase.cpp429
-rw-r--r--dom/localstorage/LSDatabase.h107
-rw-r--r--dom/localstorage/LSObject.cpp1384
-rw-r--r--dom/localstorage/LSObject.h247
-rw-r--r--dom/localstorage/LSObserver.cpp70
-rw-r--r--dom/localstorage/LSObserver.h70
-rw-r--r--dom/localstorage/LSSnapshot.cpp1022
-rw-r--r--dom/localstorage/LSSnapshot.h190
-rw-r--r--dom/localstorage/LSValue.cpp86
-rw-r--r--dom/localstorage/LSValue.h122
-rw-r--r--dom/localstorage/LSWriteOptimizer.cpp71
-rw-r--r--dom/localstorage/LSWriteOptimizer.h196
-rw-r--r--dom/localstorage/LSWriteOptimizerImpl.h66
-rw-r--r--dom/localstorage/LocalStorageCommon.cpp155
-rw-r--r--dom/localstorage/LocalStorageCommon.h286
-rw-r--r--dom/localstorage/LocalStorageManager2.cpp587
-rw-r--r--dom/localstorage/LocalStorageManager2.h70
-rw-r--r--dom/localstorage/PBackgroundLSDatabase.ipdl160
-rw-r--r--dom/localstorage/PBackgroundLSObserver.ipdl62
-rw-r--r--dom/localstorage/PBackgroundLSRequest.ipdl102
-rw-r--r--dom/localstorage/PBackgroundLSSharedTypes.ipdlh72
-rw-r--r--dom/localstorage/PBackgroundLSSimpleRequest.ipdl50
-rw-r--r--dom/localstorage/PBackgroundLSSnapshot.ipdl145
-rw-r--r--dom/localstorage/ReportInternalError.cpp30
-rw-r--r--dom/localstorage/ReportInternalError.h33
-rw-r--r--dom/localstorage/SerializationHelpers.h51
-rw-r--r--dom/localstorage/SnappyUtils.cpp72
-rw-r--r--dom/localstorage/SnappyUtils.h22
-rw-r--r--dom/localstorage/moz.build78
-rw-r--r--dom/localstorage/nsILocalStorageManager.idl35
-rw-r--r--dom/localstorage/test/.eslintrc.js5
-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.js86
-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.js110
-rw-r--r--dom/localstorage/test/unit/groupMismatch_profile.zipbin0 -> 1706 bytes
-rw-r--r--dom/localstorage/test/unit/head.js335
-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/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.js75
-rw-r--r--dom/localstorage/test/unit/test_clientValidation.js29
-rw-r--r--dom/localstorage/test/unit/test_corruptedDatabase.js70
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing1.js23
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing2.js19
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js30
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js19
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js29
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js20
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js28
-rw-r--r--dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js20
-rw-r--r--dom/localstorage/test/unit/test_eviction.js88
-rw-r--r--dom/localstorage/test/unit/test_flushing.js72
-rw-r--r--dom/localstorage/test/unit/test_groupLimit.js82
-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_migration.js124
-rw-r--r--dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js70
-rw-r--r--dom/localstorage/test/unit/test_originInit.js302
-rw-r--r--dom/localstorage/test/unit/test_preloading.js81
-rw-r--r--dom/localstorage/test/unit/test_schema3upgrade.js39
-rw-r--r--dom/localstorage/test/unit/test_snapshotting.js330
-rw-r--r--dom/localstorage/test/unit/test_stringLength.js71
-rw-r--r--dom/localstorage/test/unit/test_stringLength2.js76
-rw-r--r--dom/localstorage/test/unit/test_uri_encoding_edge_cases.js66
-rw-r--r--dom/localstorage/test/unit/test_usage.js66
-rw-r--r--dom/localstorage/test/unit/test_usageAfterMigration.js161
-rw-r--r--dom/localstorage/test/unit/usageAfterMigration_profile.zipbin0 -> 1227 bytes
-rw-r--r--dom/localstorage/test/unit/xpcshell.ini54
82 files changed, 18784 insertions, 0 deletions
diff --git a/dom/localstorage/ActorsChild.cpp b/dom/localstorage/ActorsChild.cpp
new file mode 100644
index 0000000000..3ba0a6d3f1
--- /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 nsString& aDocumentURI, const nsString& aKey,
+ const bool& aIncreasePeakUsage, const int64_t& aRequestedSize,
+ 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 nsString& aDocumentURI, const nsString& aKey,
+ const LSValue& aOldValue, const LSValue& aNewValue) {
+ AssertIsOnOwningThread();
+
+ if (!mObserver) {
+ return IPC_OK();
+ }
+
+ LS_TRY_INSPECT(const auto& principal,
+ PrincipalInfoToPrincipal(aPrincipalInfo),
+ IPC_FAIL_NO_REASON(this));
+
+ 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..701f8d1c33
--- /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 nsString& aDocumentURI, const nsString& aKey,
+ const bool& aIncreasePeakUsage, const int64_t& aRequestedSize,
+ 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 nsString& aDocumentURI,
+ const nsString& 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..b5c3f03336
--- /dev/null
+++ b/dom/localstorage/ActorsParent.cpp
@@ -0,0 +1,9059 @@
+/* -*- 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 "LSObject.h"
+#include "ReportInternalError.h"
+
+// Global includes
+#include <cinttypes>
+#include <cstdlib>
+#include <cstring>
+#include <new>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+#include "CrashAnnotations.h"
+#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/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/CheckedUnsafePtr.h"
+#include "mozilla/dom/quota/Client.h"
+#include "mozilla/dom/quota/OriginScope.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/QuotaObject.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 "nsDataHashtable.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsExceptionHandler.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 "nsTHashtable.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 DISABLE_ASSERTS_FOR_FUZZING 0
+
+#if DISABLE_ASSERTS_FOR_FUZZING
+# define ASSERT_UNLESS_FUZZING(...) \
+ do { \
+ } while (0)
+#else
+# define ASSERT_UNLESS_FUZZING(...) MOZ_ASSERT(false, __VA_ARGS__)
+#endif
+
+#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;
+
+typedef nsClassHashtable<nsCStringHashKey, ArchivedOriginInfo>
+ ArchivedOriginHashtable;
+
+/*******************************************************************************
+ * Constants
+ ******************************************************************************/
+
+// Major schema version. Bump for almost everything.
+const uint32_t kMajorSchemaVersion = 4;
+
+// 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.)
+ */
+constexpr auto kJournalFileName = u"data.sqlite-journal"_ns;
+
+/**
+ * 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 uint32_t kDefaultShadowWrites = true;
+const uint32_t kDefaultSnapshotPrefill = 16384;
+const uint32_t kDefaultSnapshotGradualPrefill = 4096;
+const uint32_t 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 IsOnConnectionThread();
+
+void AssertIsOnConnectionThread();
+
+/*******************************************************************************
+ * 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 CreateTables(mozIStorageConnection* aConnection) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread());
+ MOZ_ASSERT(aConnection);
+
+ // Table `database`
+ LS_TRY(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`
+ LS_TRY(aConnection->ExecuteSimpleSQL(
+ "CREATE TABLE data"
+ "( key TEXT PRIMARY KEY"
+ ", value TEXT NOT NULL"
+ ", utf16Length INTEGER NOT NULL DEFAULT 0"
+ ", compressed INTEGER NOT NULL DEFAULT 0"
+ ", lastAccessTime INTEGER NOT NULL DEFAULT 0"
+ ");"_ns));
+
+ LS_TRY(aConnection->SetSchemaVersion(kSQLiteSchemaVersion));
+
+ return NS_OK;
+}
+
+nsresult UpgradeSchemaFrom1_0To2_0(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ LS_TRY(aConnection->ExecuteSimpleSQL(
+ "ALTER TABLE database ADD COLUMN usage INTEGER NOT NULL DEFAULT 0;"_ns));
+
+ LS_TRY(aConnection->ExecuteSimpleSQL(
+ "UPDATE database "
+ "SET usage = (SELECT total(utf16Length(key) + utf16Length(value)) "
+ "FROM data);"_ns));
+
+ LS_TRY(aConnection->SetSchemaVersion(MakeSchemaVersion(2, 0)));
+
+ return NS_OK;
+}
+
+nsresult UpgradeSchemaFrom2_0To3_0(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ LS_TRY(aConnection->ExecuteSimpleSQL(
+ "ALTER TABLE data ADD COLUMN utf16Length INTEGER NOT NULL DEFAULT 0;"_ns));
+
+ LS_TRY(aConnection->ExecuteSimpleSQL(
+ "UPDATE data SET utf16Length = utf16Length(value);"_ns));
+
+ LS_TRY(aConnection->SetSchemaVersion(MakeSchemaVersion(3, 0)));
+
+ return NS_OK;
+}
+
+nsresult UpgradeSchemaFrom3_0To4_0(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ LS_TRY(aConnection->SetSchemaVersion(MakeSchemaVersion(4, 0)));
+
+ return NS_OK;
+}
+
+nsresult SetDefaultPragmas(mozIStorageConnection* aConnection) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aConnection);
+
+ LS_TRY(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.
+ LS_TRY(
+ ToResult(aConnection->SetGrowthIncrement(kSQLiteGrowthIncrement, ""_ns))
+ .orElse(ErrToDefaultOkOrErr<NS_ERROR_FILE_TOO_BIG, Ok>));
+ }
+#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() || IsOnConnectionThread());
+
+ // XXX Common logic should be refactored out of this method and
+ // cache::DBAction::OpenDBConnection, and maybe other similar functions.
+
+ LS_TRY_INSPECT(
+ const auto& storageService,
+ ToResultGet<nsCOMPtr<mozIStorageService>>(
+ MOZ_SELECT_OVERLOAD(do_GetService), MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ LS_TRY_UNWRAP(
+ auto connection,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>,
+ storageService, OpenDatabase, &aDBFile)
+ .orElse([&aUsageFile, &aDBFile, &aCorruptedFileHandler,
+ &storageService](const nsresult rv)
+ -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
+ if (rv == NS_ERROR_FILE_CORRUPTED) {
+ // Remove the usage file first (it might not exist at all due
+ // to corrupted state, which is ignored here).
+ LS_TRY(ToResult(aUsageFile.Remove(false))
+ .orElse([](const nsresult rv) -> Result<Ok, nsresult> {
+ if (rv == NS_ERROR_FILE_NOT_FOUND ||
+ rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
+ return Ok{};
+ }
+
+ return Err(rv);
+ }));
+
+ // Call the corrupted file handler before trying to remove the
+ // database file, which might fail.
+ std::forward<CorruptedFileHandler>(aCorruptedFileHandler)();
+
+ // Nuke the database file.
+ LS_TRY(aDBFile.Remove(false));
+
+ LS_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED(
+ nsCOMPtr<mozIStorageConnection>, storageService, OpenDatabase,
+ &aDBFile));
+ }
+ return Err(rv);
+ }));
+
+ LS_TRY(SetDefaultPragmas(connection));
+
+ // Check to make sure that the database schema is correct.
+ // XXX Try to make schemaVersion const.
+ LS_TRY_UNWRAP(int32_t schemaVersion,
+ MOZ_TO_RESULT_INVOKE(connection, GetSchemaVersion));
+
+ LS_TRY(OkIf(schemaVersion <= kSQLiteSchemaVersion), Err(NS_ERROR_FAILURE));
+
+ if (schemaVersion != kSQLiteSchemaVersion) {
+ const bool newDatabase = !schemaVersion;
+
+ if (newDatabase) {
+ // Set the page size first.
+ if (kSQLitePageSizeOverride) {
+ LS_TRY(connection->ExecuteSimpleSQL(nsPrintfCString(
+ "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride)));
+ }
+
+ // We have to set the auto_vacuum mode before opening a transaction.
+ LS_TRY(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
+ ));
+ }
+
+ mozStorageTransaction transaction(
+ connection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ if (newDatabase) {
+ LS_TRY(CreateTables(connection));
+
+#ifdef DEBUG
+ {
+ LS_TRY_INSPECT(const int32_t& schemaVersion,
+ MOZ_TO_RESULT_INVOKE(connection, GetSchemaVersion),
+ QM_ASSERT_UNREACHABLE);
+
+ MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);
+ }
+#endif
+
+ LS_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "INSERT INTO database (origin) VALUES (:origin)"_ns));
+
+ LS_TRY(stmt->BindUTF8StringByName("origin"_ns, aOrigin));
+
+ LS_TRY(stmt->Execute());
+ } else {
+ // This logic needs to change next time we change the schema!
+ static_assert(kSQLiteSchemaVersion == int32_t((4 << 4) + 0),
+ "Upgrade function needed due to schema version increase.");
+
+ while (schemaVersion != kSQLiteSchemaVersion) {
+ if (schemaVersion == MakeSchemaVersion(1, 0)) {
+ LS_TRY(UpgradeSchemaFrom1_0To2_0(connection));
+ } else if (schemaVersion == MakeSchemaVersion(2, 0)) {
+ LS_TRY(UpgradeSchemaFrom2_0To3_0(connection));
+ } else if (schemaVersion == MakeSchemaVersion(3, 0)) {
+ LS_TRY(UpgradeSchemaFrom3_0To4_0(connection));
+ } else {
+ LS_WARNING(
+ "Unable to open LocalStorage database, no upgrade path is "
+ "available!");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ LS_TRY_UNWRAP(schemaVersion,
+ MOZ_TO_RESULT_INVOKE(connection, GetSchemaVersion));
+ }
+
+ MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);
+ }
+
+ LS_TRY(transaction.Commit());
+
+ if (newDatabase) {
+ // Windows caches the file size, let's force it to stat the file again.
+ LS_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(aDBFile, Exists));
+ Unused << exists;
+
+ LS_TRY_INSPECT(const int64_t& fileSize,
+ MOZ_TO_RESULT_INVOKE(aDBFile, GetFileSize));
+
+ MOZ_ASSERT(fileSize > 0);
+
+ const PRTime vacuumTime = PR_Now();
+ MOZ_ASSERT(vacuumTime);
+
+ LS_TRY_INSPECT(
+ const auto& vacuumTimeStmt,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>, connection,
+ CreateStatement,
+ "UPDATE database "
+ "SET last_vacuum_time = :time"
+ ", last_vacuum_size = :size;"_ns));
+
+ LS_TRY(vacuumTimeStmt->BindInt64ByName("time"_ns, vacuumTime));
+
+ LS_TRY(vacuumTimeStmt->BindInt64ByName("size"_ns, fileSize));
+
+ LS_TRY(vacuumTimeStmt->Execute());
+ }
+ }
+
+ return connection;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult> GetStorageConnection(
+ const nsAString& aDatabaseFilePath) {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(!aDatabaseFilePath.IsEmpty());
+ MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, u".sqlite"_ns));
+
+ LS_TRY_INSPECT(const auto& databaseFile, QM_NewLocalFile(aDatabaseFilePath));
+
+ LS_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE(databaseFile, Exists));
+
+ LS_TRY(OkIf(exists), Err(NS_ERROR_FAILURE));
+
+ LS_TRY_INSPECT(const auto& ss, ToResultGet<nsCOMPtr<mozIStorageService>>(
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ LS_TRY_UNWRAP(auto connection,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, ss,
+ OpenDatabase, databaseFile));
+
+ LS_TRY(SetDefaultPragmas(connection));
+
+ return connection;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetArchiveFile(
+ const nsAString& aStoragePath) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aStoragePath.IsEmpty());
+
+ LS_TRY_UNWRAP(auto archiveFile, QM_NewLocalFile(aStoragePath));
+
+ LS_TRY(archiveFile->Append(nsLiteralString(LS_ARCHIVE_FILE_NAME)));
+
+ return archiveFile;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult>
+CreateArchiveStorageConnection(const nsAString& aStoragePath) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aStoragePath.IsEmpty());
+
+ LS_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);
+
+ LS_TRY_INSPECT(const bool& isDirectory,
+ MOZ_TO_RESULT_INVOKE(archiveFile, IsDirectory));
+
+ if (isDirectory) {
+ LS_WARNING("ls-archive is not a file!");
+ return nsCOMPtr<mozIStorageConnection>{};
+ }
+
+ LS_TRY_INSPECT(const auto& ss, ToResultGet<nsCOMPtr<mozIStorageService>>(
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ LS_TRY_UNWRAP(
+ auto connection,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, ss,
+ OpenUnsharedDatabase, archiveFile)
+ .orElse([](const nsresult rv)
+ -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
+ if (rv == NS_ERROR_FILE_CORRUPTED) {
+ // Don't throw an error, leave a corrupted ls-archive database as
+ // it is.
+ return nsCOMPtr<mozIStorageConnection>{};
+ }
+
+ return Err(rv);
+ }));
+
+ 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;
+}
+
+nsresult AttachArchiveDatabase(const nsAString& aStoragePath,
+ mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aStoragePath.IsEmpty());
+ MOZ_ASSERT(aConnection);
+
+ LS_TRY_INSPECT(const auto& archiveFile, GetArchiveFile(aStoragePath));
+
+#ifdef DEBUG
+ {
+ LS_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE(archiveFile, Exists));
+
+ MOZ_ASSERT(exists);
+ }
+#endif
+
+ LS_TRY_INSPECT(const auto& path,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsString, archiveFile, GetPath));
+
+ LS_TRY_INSPECT(const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_TYPED(
+ nsCOMPtr<mozIStorageStatement>, aConnection,
+ CreateStatement, "ATTACH DATABASE :path AS archive;"_ns));
+
+ LS_TRY(stmt->BindStringByName("path"_ns, path));
+ LS_TRY(stmt->Execute());
+
+ return NS_OK;
+}
+
+nsresult DetachArchiveDatabase(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ LS_TRY(aConnection->ExecuteSimpleSQL("DETACH DATABASE archive"_ns));
+
+ return NS_OK;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetShadowFile(const nsAString& aBasePath) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread());
+ MOZ_ASSERT(!aBasePath.IsEmpty());
+
+ LS_TRY_UNWRAP(auto archiveFile, QM_NewLocalFile(aBasePath));
+
+ LS_TRY(archiveFile->Append(nsLiteralString(WEB_APPS_STORE_FILE_NAME)));
+
+ return archiveFile;
+}
+
+nsresult SetShadowJournalMode(mozIStorageConnection* aConnection) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread());
+ 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;
+
+ LS_TRY_INSPECT(const auto& stmt,
+ CreateAndExecuteSingleStepStatement(
+ *aConnection, journalModeQueryStart + journalModeWAL));
+
+ LS_TRY_INSPECT(
+ const auto& journalMode,
+ MOZ_TO_RESULT_INVOKE_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.
+ LS_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement(
+ *aConnection, "PRAGMA page_size;"_ns));
+
+ LS_TRY_INSPECT(const int32_t& pageSize,
+ MOZ_TO_RESULT_INVOKE(*stmt, GetInt32, 0));
+
+ MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536);
+
+ LS_TRY(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)
+ LS_TRY(
+ aConnection->ExecuteSimpleSQL("PRAGMA journal_size_limit = "_ns +
+ IntToCString(kShadowJournalSizeLimit)));
+ } else {
+ LS_TRY(
+ aConnection->ExecuteSimpleSQL(journalModeQueryStart + "truncate"_ns));
+ }
+
+ return NS_OK;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateShadowStorageConnection(
+ const nsAString& aBasePath) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread());
+ MOZ_ASSERT(!aBasePath.IsEmpty());
+
+ LS_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath));
+
+ LS_TRY_INSPECT(const auto& ss, ToResultGet<nsCOMPtr<mozIStorageService>>(
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ LS_TRY_UNWRAP(
+ auto connection,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, ss,
+ OpenUnsharedDatabase, shadowFile)
+ .orElse([&shadowFile, &ss](const nsresult rv)
+ -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
+ if (rv == NS_ERROR_FILE_CORRUPTED) {
+ LS_TRY(shadowFile->Remove(false));
+
+ LS_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
+ shadowFile));
+ }
+
+ return Err(rv);
+ }));
+
+ LS_TRY(SetShadowJournalMode(connection));
+
+ LS_TRY(ToResult(StorageDBUpdater::Update(connection))
+ .orElse([&connection, &shadowFile,
+ &ss](const nsresult) -> Result<Ok, nsresult> {
+ LS_TRY(connection->Close());
+ LS_TRY(shadowFile->Remove(false));
+
+ LS_TRY_UNWRAP(connection,
+ MOZ_TO_RESULT_INVOKE_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss,
+ OpenUnsharedDatabase, shadowFile));
+
+ LS_TRY(SetShadowJournalMode(connection));
+
+ LS_TRY(StorageDBUpdater::Update(connection));
+
+ return Ok{};
+ }));
+
+ return connection;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult> GetShadowStorageConnection(
+ const nsAString& aBasePath) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aBasePath.IsEmpty());
+
+ LS_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath));
+
+ LS_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(shadowFile, Exists));
+
+ LS_TRY(OkIf(exists), Err(NS_ERROR_FAILURE));
+
+ LS_TRY_INSPECT(const auto& ss, ToResultGet<nsCOMPtr<mozIStorageService>>(
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ LS_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, ss,
+ OpenUnsharedDatabase, shadowFile));
+}
+
+nsresult AttachShadowDatabase(const nsAString& aBasePath,
+ mozIStorageConnection* aConnection) {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(!aBasePath.IsEmpty());
+ MOZ_ASSERT(aConnection);
+
+ LS_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath));
+
+#ifdef DEBUG
+ {
+ LS_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE(shadowFile, Exists));
+
+ MOZ_ASSERT(exists);
+ }
+#endif
+
+ LS_TRY_INSPECT(const auto& path,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsString, shadowFile, GetPath));
+
+ LS_TRY_INSPECT(const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_TYPED(
+ nsCOMPtr<mozIStorageStatement>, aConnection,
+ CreateStatement, "ATTACH DATABASE :path AS shadow;"_ns));
+
+ LS_TRY(stmt->BindStringByName("path"_ns, path));
+
+ LS_TRY(stmt->Execute());
+
+ return NS_OK;
+}
+
+nsresult DetachShadowDatabase(mozIStorageConnection* aConnection) {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ LS_TRY(aConnection->ExecuteSimpleSQL("DETACH DATABASE shadow"_ns));
+
+ return NS_OK;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetUsageFile(
+ const nsAString& aDirectoryPath) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread());
+ MOZ_ASSERT(!aDirectoryPath.IsEmpty());
+
+ LS_TRY_UNWRAP(auto usageFile, QM_NewLocalFile(aDirectoryPath));
+
+ LS_TRY(usageFile->Append(kUsageFileName));
+
+ return usageFile;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetUsageJournalFile(
+ const nsAString& aDirectoryPath) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread());
+ MOZ_ASSERT(!aDirectoryPath.IsEmpty());
+
+ LS_TRY_UNWRAP(auto usageJournalFile, QM_NewLocalFile(aDirectoryPath));
+
+ LS_TRY(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.
+ LS_TRY_INSPECT(
+ const auto& res,
+ MOZ_TO_RESULT_INVOKE(aFile, IsDirectory)
+ .map([](const bool isDirectory) {
+ return isDirectory ? ExistsAsFileResult::IsDirectory
+ : ExistsAsFileResult::IsFile;
+ })
+ .orElse(
+ [](const nsresult rv) -> Result<ExistsAsFileResult, nsresult> {
+ if (rv != NS_ERROR_FILE_NOT_FOUND &&
+ rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
+ return Err(rv);
+ }
+ return ExistsAsFileResult::DoesNotExist;
+ }));
+
+ LS_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() || IsOnConnectionThread());
+ MOZ_ASSERT(aUsageFile);
+ MOZ_ASSERT(aUsageJournalFile);
+ MOZ_ASSERT(aUsage >= 0);
+
+ LS_TRY_INSPECT(const bool& usageJournalFileExists,
+ ExistsAsFile(*aUsageJournalFile));
+ if (!usageJournalFileExists) {
+ LS_TRY(aUsageJournalFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644));
+ }
+
+ LS_TRY_INSPECT(const auto& stream, NS_NewLocalFileOutputStream(aUsageFile));
+
+ nsCOMPtr<nsIBinaryOutputStream> binaryStream =
+ NS_NewObjectOutputStream(stream);
+
+ LS_TRY(binaryStream->Write32(kUsageFileCookie));
+
+ LS_TRY(binaryStream->Write64(aUsage));
+
+ LS_TRY(stream->Close());
+
+ return NS_OK;
+}
+
+Result<UsageInfo, nsresult> LoadUsageFile(nsIFile& aUsageFile) {
+ AssertIsOnIOThread();
+
+ LS_TRY_INSPECT(const int64_t& fileSize,
+ MOZ_TO_RESULT_INVOKE(aUsageFile, GetFileSize));
+
+ LS_TRY(OkIf(fileSize == kUsageFileSize), Err(NS_ERROR_FILE_CORRUPTED));
+
+ LS_TRY_UNWRAP(auto stream, NS_NewLocalFileInputStream(&aUsageFile));
+
+ LS_TRY_INSPECT(const auto& bufferedStream,
+ NS_NewBufferedInputStream(stream.forget(), 16));
+
+ const nsCOMPtr<nsIBinaryInputStream> binaryStream =
+ NS_NewObjectInputStream(bufferedStream);
+
+ LS_TRY_INSPECT(const uint32_t& cookie,
+ MOZ_TO_RESULT_INVOKE(binaryStream, Read32));
+
+ LS_TRY(OkIf(cookie == kUsageFileCookie), Err(NS_ERROR_FILE_CORRUPTED));
+
+ LS_TRY_INSPECT(const uint64_t& usage,
+ MOZ_TO_RESULT_INVOKE(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 {
+ friend class ConnectionThread;
+
+ public:
+ class CachedStatement;
+
+ private:
+ class InitTemporaryOriginHelper;
+
+ class FlushOp;
+ class CloseOp;
+
+ RefPtr<ConnectionThread> mConnectionThread;
+ RefPtr<QuotaClient> mQuotaClient;
+ nsCOMPtr<nsITimer> mFlushTimer;
+ nsCOMPtr<mozIStorageConnection> mStorageConnection;
+ UniquePtr<ArchivedOriginScope> mArchivedOriginScope;
+ nsInterfaceHashtable<nsCStringHashKey, mozIStorageStatement>
+ mCachedStatements;
+ ConnectionWriteOptimizer mWriteOptimizer;
+ const QuotaInfo mQuotaInfo;
+ 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 mQuotaInfo.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 {
+ AssertIsOnConnectionThread();
+
+ return mStorageConnection;
+ }
+
+ void CloseStorageConnection();
+
+ nsresult GetCachedStatement(const nsACString& aQuery,
+ CachedStatement* aCachedStatement);
+
+ nsresult BeginWriteTransaction();
+
+ nsresult CommitWriteTransaction();
+
+ nsresult RollbackWriteTransaction();
+
+ private:
+ // Only created by ConnectionThread.
+ Connection(ConnectionThread* aConnectionThread, const QuotaInfo& aQuotaInfo,
+ UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
+ bool aDatabaseWasNotAvailable);
+
+ ~Connection();
+
+ void ScheduleFlush();
+
+ void Flush();
+
+ static void FlushTimerCallback(nsITimer* aTimer, void* aClosure);
+};
+
+class Connection::CachedStatement final {
+ friend class Connection;
+
+ nsCOMPtr<mozIStorageStatement> mStatement;
+ Maybe<mozStorageStatementScoper> mScoper;
+
+ public:
+ CachedStatement();
+ ~CachedStatement();
+
+ operator mozIStorageStatement*() const;
+
+ mozIStorageStatement* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN;
+
+ private:
+ // Only called by Connection.
+ void Assign(Connection* aConnection,
+ already_AddRefed<mozIStorageStatement> aStatement);
+
+ // No funny business allowed.
+ CachedStatement(const CachedStatement&) = delete;
+ CachedStatement& operator=(const CachedStatement&) = delete;
+};
+
+/**
+ * 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;
+ const QuotaInfo mQuotaInfo;
+ nsString mOriginDirectoryPath;
+ nsresult mIOThreadResultCode;
+ bool mWaiting;
+
+ public:
+ explicit InitTemporaryOriginHelper(const QuotaInfo& aQuotaInfo)
+ : Runnable("dom::localstorage::Connection::InitTemporaryOriginHelper"),
+ mMonitor("InitTemporaryOriginHelper::mMonitor"),
+ mQuotaInfo(aQuotaInfo),
+ mIOThreadResultCode(NS_OK),
+ mWaiting(true) {
+ AssertIsOnConnectionThread();
+ }
+
+ 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 QuotaInfo& aQuotaInfo,
+ 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.
+ */
+ nsTHashtable<nsPtrHashKey<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.
+ */
+ nsTHashtable<nsPtrHashKey<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.
+ */
+ nsTHashtable<nsPtrHashKey<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.
+ */
+ nsTHashtable<nsPtrHashKey<Database>> mActiveDatabases;
+ /**
+ * Non-authoritative hashtable representation of mOrderedItems for efficient
+ * lookup.
+ */
+ nsDataHashtable<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 GroupAndOrigin mGroupAndOrigin;
+ 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 GroupAndOrigin& aGroupAndOrigin, uint32_t aPrivateBrowsingId,
+ int64_t aUsage, int64_t aSizeOfKeys, int64_t aSizeOfItems,
+ RefPtr<DirectoryLock>&& aDirectoryLock,
+ RefPtr<Connection>&& aConnection,
+ RefPtr<QuotaObject>&& aQuotaObject,
+ nsDataHashtable<nsStringHashKey, LSValue>& aValues,
+ nsTArray<LSItemInfo>&& aOrderedItems);
+
+ Maybe<DirectoryLock&> MaybeDirectoryLockRef() const {
+ AssertIsOnBackgroundThread();
+
+ return ToMaybeRef(mDirectoryLock.get());
+ }
+
+ const nsCString& Origin() const { return mGroupAndOrigin.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);
+
+ void NoteLiveDatabase(Database* aDatabase);
+
+ void NoteFinishedDatabase(Database* aDatabase);
+
+ void NoteActiveDatabase(Database* aDatabase);
+
+ void NoteInactiveDatabase(Database* aDatabase);
+
+ void GetSnapshotLoadInfo(const nsString& 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 nsString& aKey, LSValue& aValue) const;
+
+ void GetKeys(nsTArray<nsString>& aKeys) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Mutation Methods
+ //
+ // These are only called during Snapshot::RecvCheckpoint
+
+ /**
+ * Used by Snapshot::RecvCheckpoint to set a key/value pair as part of a 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 aSnapshotInitialUsage);
+
+ int64_t EndUpdateBatch(int64_t aSnapshotPeakUsage);
+
+ int64_t GetUsage() const { return mUsage; }
+
+ int64_t RequestUpdateUsage(int64_t aRequestedSize, int64_t aMinSize);
+
+ 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);
+};
+
+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 nsString& aDocumentURI, const nsString& aKey,
+ const bool& aIncreasePeakUsage, const int64_t& aRequestedSize,
+ const int64_t& aMinSize, LSSnapshotInitInfo* aInitInfo) override;
+
+ mozilla::ipc::IPCResult RecvPBackgroundLSSnapshotConstructor(
+ PBackgroundLSSnapshotParent* aActor, const nsString& aDocumentURI,
+ const nsString& aKey, const bool& aIncreasePeakUsage,
+ const int64_t& aRequestedSize, 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.)
+ */
+ nsTHashtable<nsStringHashKey> 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.
+ */
+ nsDataHashtable<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;
+
+ bool mHasOtherProcessObservers;
+
+ public:
+ // Created in AllocPBackgroundLSSnapshotParent.
+ Snapshot(Database* aDatabase, const nsAString& aDocumentURI);
+
+ void Init(nsTHashtable<nsStringHashKey>& aLoadedItems,
+ nsTHashtable<nsStringHashKey>& aUnknownItems,
+ uint32_t aNextLoadIndex, uint32_t aTotalLength,
+ int64_t aInitialUsage, int64_t aPeakUsage,
+ LSSnapshot::LoadState aLoadState, bool aHasOtherProcessObservers) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aInitialUsage >= 0);
+ MOZ_ASSERT(aPeakUsage >= aInitialUsage);
+ 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.SwapElements(aUnknownItems);
+ mNextLoadIndex = aNextLoadIndex;
+ mTotalLength = aTotalLength;
+ mUsage = aInitialUsage;
+ 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;
+ }
+ 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 HasOtherProcessObservers() const {
+ AssertIsOnBackgroundThread();
+
+ return mHasOtherProcessObservers;
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Snapshot)
+
+ private:
+ // Reference counted.
+ ~Snapshot();
+
+ void Finish();
+
+ // IPDL methods are only called by IPDL.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvDeleteMe() override;
+
+ mozilla::ipc::IPCResult RecvCheckpoint(
+ nsTArray<LSWriteInfo>&& aWriteInfos) override;
+
+ mozilla::ipc::IPCResult RecvCheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) override;
+
+ mozilla::ipc::IPCResult RecvFinish() override;
+
+ mozilla::ipc::IPCResult RecvLoaded() override;
+
+ mozilla::ipc::IPCResult RecvLoadValueAndMoreItems(
+ const nsString& aKey, LSValue* aValue,
+ nsTArray<LSItemInfo>* aItemInfos) override;
+
+ mozilla::ipc::IPCResult RecvLoadKeys(nsTArray<nsString>* aKeys) override;
+
+ mozilla::ipc::IPCResult RecvIncreasePeakUsage(const int64_t& aRequestedSize,
+ const int64_t& aMinSize,
+ int64_t* aSize) override;
+
+ mozilla::ipc::IPCResult RecvPing() 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
+ };
+
+ nsCOMPtr<nsIEventTarget> mMainEventTarget;
+ const LSRequestParams mParams;
+ Maybe<ContentParentId> mContentParentId;
+ State mState;
+ bool mWaitingForFinish;
+
+ public:
+ LSRequestBase(nsIEventTarget* aMainEventTarget,
+ 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 CompressibleFunction;
+
+ 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,
+
+ // Opening directory or initializing quota manager on the PBackground
+ // thread. Next step is either DirectoryOpenPending if quota manager is
+ // already initialized or QuotaManagerPending if quota manager needs to be
+ // initialized.
+ // If a datastore already exists for given origin then the next state is
+ // SendingReadyMessage.
+ PreparationPending,
+
+ // Waiting for quota manager initialization to complete on the PBackground
+ // thread. Next step is either SendingReadyMessage if initialization failed
+ // or DirectoryOpenPending if initialization succeeded.
+ QuotaManagerPending,
+
+ // 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
+ };
+
+ nsCOMPtr<nsIEventTarget> mMainEventTarget;
+ RefPtr<PrepareDatastoreOp> mDelayedOp;
+ RefPtr<DirectoryLock> mPendingDirectoryLock;
+ RefPtr<DirectoryLock> mDirectoryLock;
+ RefPtr<Connection> mConnection;
+ RefPtr<Datastore> mDatastore;
+ UniquePtr<ArchivedOriginScope> mArchivedOriginScope;
+ LoadDataOp* mLoadDataOp;
+ nsDataHashtable<nsStringHashKey, LSValue> mValues;
+ nsTArray<LSItemInfo> mOrderedItems;
+ QuotaInfo mQuotaInfo;
+ 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(nsIEventTarget* aMainEventTarget,
+ const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ Maybe<DirectoryLock&> MaybeDirectoryLockRef() const {
+ AssertIsOnBackgroundThread();
+
+ return ToMaybeRef(mDirectoryLock.get());
+ }
+
+ bool OriginIsKnown() const {
+ MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread());
+
+ return !mQuotaInfo.mOrigin.IsEmpty();
+ }
+
+ const nsCString& Origin() const {
+ MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread());
+ MOZ_ASSERT(OriginIsKnown());
+
+ return mQuotaInfo.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();
+
+ nsresult QuotaManagerOpen();
+
+ nsresult OpenDirectory();
+
+ 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::CompressibleFunction final
+ : public mozIStorageFunction {
+ private:
+ ~CompressibleFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+class PrepareObserverOp : public LSRequestBase {
+ nsCString mOrigin;
+
+ public:
+ PrepareObserverOp(nsIEventTarget* aMainEventTarget,
+ 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;
+};
+
+/*******************************************************************************
+ * 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;
+ bool mShutdownRequested;
+
+ public:
+ QuotaClient();
+
+ static nsresult Initialize();
+
+ static QuotaClient* GetInstance() {
+ AssertIsOnBackgroundThread();
+
+ return sInstance;
+ }
+
+ static bool IsShuttingDownOnBackgroundThread() {
+ AssertIsOnBackgroundThread();
+
+ if (sInstance) {
+ return sInstance->IsShuttingDown();
+ }
+
+ return QuotaManager::IsShuttingDown();
+ }
+
+ static bool IsShuttingDownOnNonBackgroundThread() {
+ MOZ_ASSERT(!IsOnBackgroundThread());
+
+ return QuotaManager::IsShuttingDown();
+ }
+
+ mozilla::Mutex& ShadowDatabaseMutex() {
+ MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread());
+
+ return mShadowDatabaseMutex;
+ }
+
+ bool IsShuttingDown() const {
+ AssertIsOnBackgroundThread();
+
+ return mShutdownRequested;
+ }
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::QuotaClient, override)
+
+ Type GetType() override;
+
+ Result<UsageInfo, nsresult> InitOrigin(PersistenceType aPersistenceType,
+ const GroupAndOrigin& aGroupAndOrigin,
+ const AtomicBool& aCanceled) override;
+
+ nsresult InitOriginWithoutTracking(PersistenceType aPersistenceType,
+ const GroupAndOrigin& aGroupAndOrigin,
+ const AtomicBool& aCanceled) override;
+
+ Result<UsageInfo, nsresult> GetUsageForOrigin(
+ PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin,
+ 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.
+typedef std::conditional<DiagnosticAssertEnabled::value, nsCStringHashKeyDM,
+ nsCStringHashKey>::type DatastoreHashKey;
+
+typedef nsDataHashtable<DatastoreHashKey, CheckedUnsafePtr<Datastore>>
+ DatastoreHashtable;
+
+StaticAutoPtr<DatastoreHashtable> gDatastores;
+
+uint64_t gLastDatastoreId = 0;
+
+typedef nsClassHashtable<nsUint64HashKey, PreparedDatastore>
+ PreparedDatastoreHashtable;
+
+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;
+
+typedef nsRefPtrHashtable<nsUint64HashKey, Observer> PreparedObserverHashtable;
+
+StaticAutoPtr<PreparedObserverHashtable> gPreparedObsevers;
+
+typedef nsClassHashtable<nsCStringHashKey, nsTArray<NotNull<Observer*>>>
+ ObserverHashtable;
+
+StaticAutoPtr<ObserverHashtable> gObservers;
+
+Atomic<bool> gShadowWrites(kDefaultShadowWrites);
+Atomic<int32_t, Relaxed> gSnapshotPrefill(kDefaultSnapshotPrefill);
+Atomic<int32_t, Relaxed> gSnapshotGradualPrefill(
+ kDefaultSnapshotGradualPrefill);
+Atomic<bool> gClientValidation(kDefaultClientValidation);
+
+typedef nsDataHashtable<nsCStringHashKey, int64_t> UsageHashtable;
+
+StaticAutoPtr<ArchivedOriginHashtable> gArchivedOrigins;
+
+// Can only be touched on the Quota Manager I/O thread.
+bool gInitializedShadowStorage = false;
+
+bool IsOnConnectionThread() {
+ MOZ_ASSERT(gConnectionThread);
+ return gConnectionThread->IsOnConnectionThread();
+}
+
+void AssertIsOnConnectionThread() {
+ MOZ_ASSERT(gConnectionThread);
+ gConnectionThread->AssertIsOnConnectionThread();
+}
+
+already_AddRefed<Datastore> GetDatastore(const nsACString& aOrigin) {
+ AssertIsOnBackgroundThread();
+
+ if (gDatastores) {
+ CheckedUnsafePtr<Datastore> datastore;
+ if (gDatastores->Get(aOrigin, &datastore)) {
+ MOZ_ASSERT(datastore);
+
+ RefPtr<Datastore> result(datastore);
+ 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.
+ LS_TRY(quotaManager->EnsureStorageIsInitialized());
+
+ LS_TRY_INSPECT(const auto& connection, CreateArchiveStorageConnection(
+ quotaManager->GetStoragePath()));
+
+ if (!connection) {
+ gArchivedOrigins = new ArchivedOriginHashtable();
+ return NS_OK;
+ }
+
+ LS_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_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
+ LS_TRY(quota::CollectWhileHasResult(
+ *stmt, [&archivedOrigins](auto& stmt) -> Result<Ok, nsresult> {
+ LS_TRY_INSPECT(
+ const auto& originSuffix,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 0));
+ LS_TRY_INSPECT(
+ const auto& originNoSuffix,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 1));
+
+ const nsCString hashKey =
+ GetArchivedOriginHashKey(originSuffix, originNoSuffix);
+
+ OriginAttributes originAttributes;
+ LS_TRY(OkIf(originAttributes.PopulateFromSuffix(originSuffix)),
+ Err(NS_ERROR_FAILURE));
+
+ archivedOrigins->Put(hashKey, MakeUnique<ArchivedOriginInfo>(
+ originAttributes, originNoSuffix));
+
+ return Ok{};
+ }));
+
+ gArchivedOrigins = archivedOrigins.release();
+ return NS_OK;
+}
+
+Result<int64_t, nsresult> GetUsage(mozIStorageConnection& aConnection,
+ ArchivedOriginScope* aArchivedOriginScope) {
+ AssertIsOnIOThread();
+
+ LS_TRY_INSPECT(
+ const auto& stmt,
+ ([aArchivedOriginScope,
+ &aConnection]() -> Result<nsCOMPtr<mozIStorageStatement>, nsresult> {
+ if (aArchivedOriginScope) {
+ LS_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> {
+ LS_TRY(aArchivedOriginScope->BindToStatement(&stmt));
+ return Ok{};
+ }));
+ }
+
+ LS_TRY_RETURN(CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ aConnection, "SELECT usage FROM database"_ns));
+ }()));
+
+ LS_TRY(OkIf(stmt), Err(NS_ERROR_FAILURE));
+
+ LS_TRY_RETURN(MOZ_TO_RESULT_INVOKE(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;
+}
+
+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& preparedDatastoreEntry : *gPreparedDatastores) {
+ const auto& preparedDatastore = preparedDatastoreEntry.GetData();
+ 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) {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(aPrincipalInfo))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!StoragePrincipalHelper::
+ VerifyValidStoragePrincipalInfoForPrincipalInfo(
+ aStoragePrincipalInfo, aPrincipalInfo))) {
+ return false;
+ }
+
+ return true;
+}
+
+bool VerifyClientId(const Maybe<ContentParentId>& aContentParentId,
+ const PrincipalInfo& aPrincipalInfo,
+ const Maybe<nsID>& aClientId) {
+ AssertIsOnBackgroundThread();
+
+ if (gClientValidation) {
+ if (NS_WARN_IF(aClientId.isNothing())) {
+ return false;
+ }
+
+ RefPtr<ClientManagerService> svc = ClientManagerService::GetInstance();
+ if (svc && NS_WARN_IF(!svc->HasWindow(aContentParentId, aPrincipalInfo,
+ aClientId.ref()))) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool VerifyOriginKey(const nsACString& aOriginKey,
+ const PrincipalInfo& aPrincipalInfo) {
+ AssertIsOnBackgroundThread();
+
+ LS_TRY_INSPECT((const auto& [originAttrSuffix, originKey]),
+ GenerateOriginKey2(aPrincipalInfo), false);
+
+ Unused << originAttrSuffix;
+
+ LS_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;
+}
+
+} // namespace
+
+/*******************************************************************************
+ * Exported functions
+ ******************************************************************************/
+
+void InitializeLocalStorage() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!gLocalStorageInitialized);
+
+ if (!QuotaManager::IsRunningGTests()) {
+ // This service has to be started on the main thread currently.
+ nsCOMPtr<mozIStorageService> ss;
+ if (NS_WARN_IF(!(ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID)))) {
+ NS_WARNING("Failed to get storage service!");
+ }
+ }
+
+ if (NS_FAILED(QuotaClient::Initialize())) {
+ NS_WARNING("Failed to initialize quota client!");
+ }
+
+ 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)) {
+ ASSERT_UNLESS_FUZZING();
+ return nullptr;
+ }
+
+ PreparedDatastore* preparedDatastore = gPreparedDatastores->Get(aDatastoreId);
+ if (NS_WARN_IF(!preparedDatastore)) {
+ ASSERT_UNLESS_FUZZING();
+ 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)) {
+ ASSERT_UNLESS_FUZZING();
+ return nullptr;
+ }
+
+ RefPtr<Observer> observer = gPreparedObsevers->Get(aObserverId);
+ if (NS_WARN_IF(!observer)) {
+ ASSERT_UNLESS_FUZZING();
+ 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*>>* array;
+ if (!gObservers->Get(notNullObserver->Origin(), &array)) {
+ array = new nsTArray<NotNull<Observer*>>();
+ gObservers->Put(notNullObserver->Origin(), array);
+ }
+ 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(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+ return nullptr;
+ }
+
+ Maybe<ContentParentId> contentParentId;
+
+ uint64_t childID = BackgroundParent::GetChildID(aBackgroundActor);
+ if (childID) {
+ contentParentId = Some(ContentParentId(childID));
+ }
+
+ // If we're in the same process as the actor, we need to get the target event
+ // queue from the current RequestHelper.
+ nsCOMPtr<nsIEventTarget> mainEventTarget;
+ if (!BackgroundParent::IsOtherProcessActor(aBackgroundActor)) {
+ mainEventTarget = LSObject::GetSyncLoopEventTarget();
+ }
+
+ RefPtr<LSRequestBase> actor;
+
+ switch (aParams.type()) {
+ case LSRequestParams::TLSRequestPreloadDatastoreParams:
+ case LSRequestParams::TLSRequestPrepareDatastoreParams: {
+ RefPtr<PrepareDatastoreOp> prepareDatastoreOp =
+ new PrepareDatastoreOp(mainEventTarget, 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(mainEventTarget, 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(!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(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;
+ }
+
+ 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(!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& entry : *gDatastores) {
+ const auto& datastore = entry.GetData();
+ MOZ_ASSERT(datastore);
+
+ 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.Data().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) {
+ AssertIsOnConnectionThread();
+ 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) {
+ LS_TRY(PerformTruncate(aConnection, aShadowWrites));
+ }
+
+ for (auto iter = mWriteInfos.ConstIter(); !iter.Done(); iter.Next()) {
+ const WriteInfo* const writeInfo = iter.Data().get();
+
+ switch (writeInfo->GetType()) {
+ case WriteInfo::InsertItem:
+ case WriteInfo::UpdateItem: {
+ const auto* const insertItemInfo =
+ static_cast<const InsertItemInfo*>(writeInfo);
+
+ LS_TRY(PerformInsertOrUpdate(aConnection, aShadowWrites,
+ insertItemInfo->GetKey(),
+ insertItemInfo->GetValue()));
+
+ break;
+ }
+
+ case WriteInfo::DeleteItem: {
+ const auto* const deleteItemInfo =
+ static_cast<const DeleteItemInfo*>(writeInfo);
+
+ LS_TRY(PerformDelete(aConnection, aShadowWrites,
+ deleteItemInfo->GetKey()));
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Bad type!");
+ }
+ }
+
+ {
+ Connection::CachedStatement stmt;
+ nsresult rv = aConnection->GetCachedStatement(
+ nsLiteralCString("UPDATE database "
+ "SET usage = usage + :delta"),
+ &stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Err(rv);
+ }
+
+ rv = stmt->BindInt64ByName("delta"_ns, mTotalDelta);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Err(rv);
+ }
+
+ rv = stmt->Execute();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Err(rv);
+ }
+ }
+
+ LS_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ *aConnection->StorageConnection(),
+ "SELECT usage FROM database"_ns));
+
+ LS_TRY(OkIf(stmt), Err(NS_ERROR_FAILURE));
+
+ LS_TRY_RETURN(MOZ_TO_RESULT_INVOKE(*stmt, GetInt64, 0));
+}
+
+nsresult ConnectionWriteOptimizer::PerformInsertOrUpdate(
+ Connection* aConnection, bool aShadowWrites, const nsAString& aKey,
+ const LSValue& aValue) {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ Connection::CachedStatement stmt;
+ nsresult rv = aConnection->GetCachedStatement(
+ nsLiteralCString(
+ "INSERT OR REPLACE INTO data (key, value, utf16Length, compressed) "
+ "VALUES(:key, :value, :utf16Length, :compressed)"),
+ &stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->BindStringByName("key"_ns, aKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->BindUTF8StringByName("value"_ns, aValue);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->BindInt32ByName("utf16Length"_ns, aValue.UTF16Length());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->BindInt32ByName("compressed"_ns, aValue.IsCompressed());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->Execute();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!aShadowWrites) {
+ return NS_OK;
+ }
+
+ rv = aConnection->GetCachedStatement(
+ nsLiteralCString(
+ "INSERT OR REPLACE INTO shadow.webappsstore2 "
+ "(originAttributes, originKey, scope, key, value) "
+ "VALUES (:originAttributes, :originKey, :scope, :key, :value) "),
+ &stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ ArchivedOriginScope* archivedOriginScope =
+ aConnection->GetArchivedOriginScope();
+
+ rv = archivedOriginScope->BindToStatement(stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCString scope = Scheme0Scope(archivedOriginScope->OriginSuffix(),
+ archivedOriginScope->OriginNoSuffix());
+
+ rv = stmt->BindUTF8StringByName("scope"_ns, scope);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->BindStringByName("key"_ns, aKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (aValue.IsCompressed()) {
+ nsCString value;
+ if (NS_WARN_IF(!SnappyUncompress(aValue, value))) {
+ return NS_ERROR_FAILURE;
+ }
+ rv = stmt->BindUTF8StringByName("value"_ns, value);
+ } else {
+ rv = stmt->BindUTF8StringByName("value"_ns, aValue);
+ }
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->Execute();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult ConnectionWriteOptimizer::PerformDelete(Connection* aConnection,
+ bool aShadowWrites,
+ const nsAString& aKey) {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ Connection::CachedStatement stmt;
+ nsresult rv =
+ aConnection->GetCachedStatement(nsLiteralCString("DELETE FROM data "
+ "WHERE key = :key;"),
+ &stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->BindStringByName("key"_ns, aKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->Execute();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!aShadowWrites) {
+ return NS_OK;
+ }
+
+ rv = aConnection->GetCachedStatement(
+ nsLiteralCString("DELETE FROM shadow.webappsstore2 "
+ "WHERE originAttributes = :originAttributes "
+ "AND originKey = :originKey "
+ "AND key = :key;"),
+ &stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aConnection->GetArchivedOriginScope()->BindToStatement(stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->BindStringByName("key"_ns, aKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->Execute();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult ConnectionWriteOptimizer::PerformTruncate(Connection* aConnection,
+ bool aShadowWrites) {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ Connection::CachedStatement stmt;
+ nsresult rv = aConnection->GetCachedStatement("DELETE FROM data;"_ns, &stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->Execute();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!aShadowWrites) {
+ return NS_OK;
+ }
+
+ rv = aConnection->GetCachedStatement(
+ nsLiteralCString("DELETE FROM shadow.webappsstore2 "
+ "WHERE originAttributes = :originAttributes "
+ "AND originKey = :originKey;"),
+ &stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aConnection->GetArchivedOriginScope()->BindToStatement(stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->Execute();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ 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() {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+
+ if (!MayProceedOnNonOwningThread()) {
+ SetFailureCode(NS_ERROR_FAILURE);
+ } 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->StorageConnection());
+ }
+ }
+
+ 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_FAILURE);
+ }
+
+ if (NS_SUCCEEDED(ResultCode())) {
+ OnSuccess();
+ } else {
+ OnFailure(ResultCode());
+ }
+
+ Cleanup();
+}
+
+NS_IMETHODIMP
+ConnectionDatastoreOperationBase::Run() {
+ if (IsOnConnectionThread()) {
+ RunOnConnectionThread();
+ } else {
+ RunOnOwningThread();
+ }
+
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * Connection implementation
+ ******************************************************************************/
+
+Connection::Connection(ConnectionThread* aConnectionThread,
+ const QuotaInfo& aQuotaInfo,
+ UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
+ bool aDatabaseWasNotAvailable)
+ : mConnectionThread(aConnectionThread),
+ mQuotaClient(QuotaClient::GetInstance()),
+ mArchivedOriginScope(std::move(aArchivedOriginScope)),
+ mQuotaInfo(aQuotaInfo),
+ mDatabaseWasNotAvailable(aDatabaseWasNotAvailable),
+ mHasCreatedDatabase(false),
+ mFlushScheduled(false)
+#ifdef DEBUG
+ ,
+ mInUpdateBatch(false),
+ mFinished(false)
+#endif
+{
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!aQuotaInfo.mGroup.IsEmpty());
+ MOZ_ASSERT(!aQuotaInfo.mOrigin.IsEmpty());
+}
+
+Connection::~Connection() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mStorageConnection);
+ MOZ_ASSERT(!mCachedStatements.Count());
+ 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() {
+ AssertIsOnConnectionThread();
+
+ if (mStorageConnection) {
+ return NS_OK;
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ if (!mDatabaseWasNotAvailable || mHasCreatedDatabase) {
+ LS_TRY_INSPECT(const auto& directoryEntry,
+ quotaManager->GetDirectoryForOrigin(PERSISTENCE_TYPE_DEFAULT,
+ Origin()));
+
+ LS_TRY(directoryEntry->Append(
+ NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME)));
+
+ LS_TRY(directoryEntry->GetPath(mDirectoryPath));
+ LS_TRY(directoryEntry->Append(kDataFileName));
+
+ LS_TRY_INSPECT(
+ const auto& databaseFilePath,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsString, directoryEntry, GetPath));
+
+ LS_TRY_UNWRAP(mStorageConnection, GetStorageConnection(databaseFilePath));
+
+ return NS_OK;
+ }
+
+ RefPtr<InitTemporaryOriginHelper> helper =
+ new InitTemporaryOriginHelper(mQuotaInfo);
+
+ LS_TRY_INSPECT(const auto& originDirectoryPath,
+ helper->BlockAndReturnOriginDirectoryPath());
+
+ LS_TRY_INSPECT(const auto& directoryEntry,
+ QM_NewLocalFile(originDirectoryPath));
+
+ LS_TRY(directoryEntry->Append(
+ NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME)));
+
+ LS_TRY(directoryEntry->GetPath(mDirectoryPath));
+
+ LS_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE(directoryEntry, Exists));
+
+ if (!exists) {
+ LS_TRY(directoryEntry->Create(nsIFile::DIRECTORY_TYPE, 0755));
+ }
+
+ LS_TRY(directoryEntry->Append(kDataFileName));
+
+#ifdef DEBUG
+ {
+ LS_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE(directoryEntry, Exists));
+
+ MOZ_ASSERT(!exists);
+ }
+#endif
+
+ LS_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_TARGET_DOES_NOT_EXIST &&
+ rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) {
+ NS_WARNING("Failed to remove database file!");
+ }
+ });
+
+ LS_TRY_UNWRAP(storageConnection,
+ CreateStorageConnection(*directoryEntry, *usageFile, Origin(),
+ [] { MOZ_ASSERT_UNREACHABLE(); }));
+
+ MOZ_ASSERT(mQuotaClient);
+
+ MutexAutoLock shadowDatabaseLock(mQuotaClient->ShadowDatabaseMutex());
+
+ nsCOMPtr<mozIStorageConnection> shadowConnection;
+ if (!gInitializedShadowStorage) {
+ LS_TRY_UNWRAP(shadowConnection,
+ CreateShadowStorageConnection(quotaManager->GetBasePath()));
+
+ gInitializedShadowStorage = true;
+ }
+
+ autoRemove.release();
+
+ if (!mHasCreatedDatabase) {
+ mHasCreatedDatabase = true;
+ }
+
+ mStorageConnection = storageConnection;
+
+ return NS_OK;
+}
+
+void Connection::CloseStorageConnection() {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(mStorageConnection);
+
+ mCachedStatements.Clear();
+
+ MOZ_ALWAYS_SUCCEEDS(mStorageConnection->Close());
+ mStorageConnection = nullptr;
+}
+
+nsresult Connection::GetCachedStatement(const nsACString& aQuery,
+ CachedStatement* aCachedStatement) {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(!aQuery.IsEmpty());
+ MOZ_ASSERT(aCachedStatement);
+ MOZ_ASSERT(mStorageConnection);
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+
+ if (!mCachedStatements.Get(aQuery, getter_AddRefs(stmt))) {
+ nsresult rv =
+ mStorageConnection->CreateStatement(aQuery, getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) {
+#ifdef DEBUG
+ nsCString msg;
+ MOZ_ALWAYS_SUCCEEDS(mStorageConnection->GetLastErrorString(msg));
+
+ nsAutoCString error = "The statement '"_ns + aQuery +
+ "' failed to compile with the error message '"_ns +
+ msg + "'."_ns;
+
+ NS_WARNING(error.get());
+#endif
+ return rv;
+ }
+
+ mCachedStatements.Put(aQuery, stmt);
+ }
+
+ aCachedStatement->Assign(this, stmt.forget());
+ return NS_OK;
+}
+
+nsresult Connection::BeginWriteTransaction() {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(mStorageConnection);
+
+ CachedStatement stmt;
+ nsresult rv = GetCachedStatement("BEGIN IMMEDIATE;"_ns, &stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->Execute();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult Connection::CommitWriteTransaction() {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(mStorageConnection);
+
+ CachedStatement stmt;
+ nsresult rv = GetCachedStatement("COMMIT;"_ns, &stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->Execute();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult Connection::RollbackWriteTransaction() {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(mStorageConnection);
+
+ CachedStatement stmt;
+ nsresult rv = GetCachedStatement("ROLLBACK;"_ns, &stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // 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();
+}
+
+Connection::CachedStatement::CachedStatement() {
+ AssertIsOnConnectionThread();
+
+ MOZ_COUNT_CTOR(Connection::CachedStatement);
+}
+
+Connection::CachedStatement::~CachedStatement() {
+ AssertIsOnConnectionThread();
+
+ MOZ_COUNT_DTOR(Connection::CachedStatement);
+}
+
+Connection::CachedStatement::operator mozIStorageStatement*() const {
+ AssertIsOnConnectionThread();
+
+ return mStatement;
+}
+
+mozIStorageStatement* Connection::CachedStatement::operator->() const {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(mStatement);
+
+ return mStatement;
+}
+
+void Connection::CachedStatement::Assign(
+ Connection* aConnection,
+ already_AddRefed<mozIStorageStatement> aStatement) {
+ AssertIsOnConnectionThread();
+
+ mScoper.reset();
+
+ mStatement = aStatement;
+
+ if (mStatement) {
+ mScoper.emplace(mStatement);
+ }
+}
+
+Result<nsString, nsresult>
+Connection::InitTemporaryOriginHelper::BlockAndReturnOriginDirectoryPath() {
+ AssertIsOnConnectionThread();
+
+ 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();
+ }
+
+ LS_TRY(mIOThreadResultCode);
+
+ return mOriginDirectoryPath;
+}
+
+nsresult Connection::InitTemporaryOriginHelper::RunOnIOThread() {
+ AssertIsOnIOThread();
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ LS_TRY_INSPECT(const auto& directoryEntry,
+ quotaManager
+ ->EnsureTemporaryOriginIsInitialized(
+ PERSISTENCE_TYPE_DEFAULT, mQuotaInfo)
+ .map([](const auto& res) { return res.first; }));
+
+ LS_TRY(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() {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(mConnection);
+
+ AutoWriteTransaction autoWriteTransaction(mShadowWrites);
+
+ LS_TRY(autoWriteTransaction.Start(mConnection));
+
+ LS_TRY_INSPECT(const int64_t& usage,
+ mWriteOptimizer.Perform(mConnection, mShadowWrites));
+
+ LS_TRY_INSPECT(const auto& usageFile,
+ GetUsageFile(mConnection->DirectoryPath()));
+
+ LS_TRY_INSPECT(const auto& usageJournalFile,
+ GetUsageJournalFile(mConnection->DirectoryPath()));
+
+ LS_TRY(UpdateUsageFile(usageFile, usageJournalFile, usage));
+
+ LS_TRY(autoWriteTransaction.Commit());
+
+ LS_TRY(usageJournalFile->Remove(false));
+
+ return NS_OK;
+}
+
+void Connection::FlushOp::Cleanup() {
+ AssertIsOnOwningThread();
+
+ mWriteOptimizer.Reset();
+
+ MOZ_ASSERT(!mWriteOptimizer.HasWrites());
+
+ ConnectionDatastoreOperationBase::Cleanup();
+}
+
+nsresult Connection::CloseOp::DoDatastoreWork() {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(mConnection);
+
+ if (mConnection->StorageConnection()) {
+ 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 QuotaInfo& aQuotaInfo,
+ UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
+ bool aDatabaseWasNotAvailable) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!aQuotaInfo.mOrigin.IsEmpty());
+ MOZ_ASSERT(!mConnections.GetWeak(aQuotaInfo.mOrigin));
+
+ RefPtr<Connection> connection =
+ new Connection(this, aQuotaInfo, std::move(aArchivedOriginScope),
+ aDatabaseWasNotAvailable);
+ mConnections.Put(aQuotaInfo.mOrigin, RefPtr{connection});
+
+ return connection.forget();
+}
+
+void ConnectionThread::Shutdown() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mThread);
+
+ mThread->Shutdown();
+}
+
+/*******************************************************************************
+ * Datastore
+ ******************************************************************************/
+
+Datastore::Datastore(const GroupAndOrigin& aGroupAndOrigin,
+ uint32_t aPrivateBrowsingId, int64_t aUsage,
+ int64_t aSizeOfKeys, int64_t aSizeOfItems,
+ RefPtr<DirectoryLock>&& aDirectoryLock,
+ RefPtr<Connection>&& aConnection,
+ RefPtr<QuotaObject>&& aQuotaObject,
+ nsDataHashtable<nsStringHashKey, LSValue>& aValues,
+ nsTArray<LSItemInfo>&& aOrderedItems)
+ : mDirectoryLock(std::move(aDirectoryLock)),
+ mConnection(std::move(aConnection)),
+ mQuotaObject(std::move(aQuotaObject)),
+ mOrderedItems(std::move(aOrderedItems)),
+ mGroupAndOrigin(aGroupAndOrigin),
+ 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.GetEntry(aPrepareDatastoreOp));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mPrepareDatastoreOps.PutEntry(aPrepareDatastoreOp);
+}
+
+void Datastore::NoteFinishedPrepareDatastoreOp(
+ PrepareDatastoreOp* aPrepareDatastoreOp) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aPrepareDatastoreOp);
+ MOZ_ASSERT(mPrepareDatastoreOps.GetEntry(aPrepareDatastoreOp));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mPrepareDatastoreOps.RemoveEntry(aPrepareDatastoreOp);
+
+ QuotaManager::GetRef().MaybeRecordShutdownStep(
+ 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::GetRef().MaybeRecordShutdownStep(
+ quota::Client::LS, "PrivateDatastore finished"_ns);
+
+ MaybeClose();
+}
+
+void Datastore::NoteLivePreparedDatastore(
+ PreparedDatastore* aPreparedDatastore) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aPreparedDatastore);
+ MOZ_ASSERT(!mPreparedDatastores.GetEntry(aPreparedDatastore));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mPreparedDatastores.PutEntry(aPreparedDatastore);
+}
+
+void Datastore::NoteFinishedPreparedDatastore(
+ PreparedDatastore* aPreparedDatastore) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aPreparedDatastore);
+ MOZ_ASSERT(mPreparedDatastores.GetEntry(aPreparedDatastore));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mPreparedDatastores.RemoveEntry(aPreparedDatastore);
+
+ QuotaManager::GetRef().MaybeRecordShutdownStep(
+ quota::Client::LS, "PreparedDatastore finished"_ns);
+
+ MaybeClose();
+}
+
+void Datastore::NoteLiveDatabase(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(!mDatabases.GetEntry(aDatabase));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mDatabases.PutEntry(aDatabase);
+}
+
+void Datastore::NoteFinishedDatabase(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(mDatabases.GetEntry(aDatabase));
+ MOZ_ASSERT(!mActiveDatabases.GetEntry(aDatabase));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mDatabases.RemoveEntry(aDatabase);
+
+ QuotaManager::GetRef().MaybeRecordShutdownStep(quota::Client::LS,
+ "Database finished"_ns);
+
+ MaybeClose();
+}
+
+void Datastore::NoteActiveDatabase(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(mDatabases.GetEntry(aDatabase));
+ MOZ_ASSERT(!mActiveDatabases.GetEntry(aDatabase));
+ MOZ_ASSERT(!mClosed);
+
+ mActiveDatabases.PutEntry(aDatabase);
+}
+
+void Datastore::NoteInactiveDatabase(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(mDatabases.GetEntry(aDatabase));
+ MOZ_ASSERT(mActiveDatabases.GetEntry(aDatabase));
+ MOZ_ASSERT(!mClosed);
+
+ mActiveDatabases.RemoveEntry(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 nsString& 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 = [&](auto aKeyLength, auto 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 nsString& 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.Put(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 (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
+ const nsAString& key = iter.Key();
+ const LSValue& value = iter.Data();
+
+ 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 aSnapshotInitialUsage) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aSnapshotInitialUsage >= 0);
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(mUpdateBatchUsage == -1);
+ MOZ_ASSERT(!mInUpdateBatch);
+
+ mUpdateBatchUsage = aSnapshotInitialUsage;
+
+ 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::RequestUpdateUsage(int64_t aRequestedSize,
+ int64_t aMinSize) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aRequestedSize > 0);
+ MOZ_ASSERT(aMinSize > 0);
+
+ if (UpdateUsage(aRequestedSize)) {
+ return aRequestedSize;
+ }
+
+ if (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(mGroupAndOrigin.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(mGroupAndOrigin.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 (auto iter = mActiveDatabases.ConstIter(); !iter.Done(); iter.Next()) {
+ Database* database = iter.Get()->GetKey();
+
+ 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(mGroupAndOrigin.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(PERSISTENCE_TYPE_DEFAULT, mGroupAndOrigin,
+ 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);
+ MOZ_ASSERT(gDatastores->Get(mGroupAndOrigin.mOrigin));
+ gDatastores->Remove(mGroupAndOrigin.mOrigin);
+
+ QuotaManager::GetRef().MaybeRecordShutdownStep(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 (auto iter = mDatabases.ConstIter(); !iter.Done(); iter.Next()) {
+ Database* database = iter.Get()->GetKey();
+
+ MOZ_ASSERT(database);
+
+ if (database == aDatabase) {
+ continue;
+ }
+
+ Snapshot* snapshot = database->GetSnapshot();
+ if (snapshot) {
+ snapshot->SaveItem(aKey, aOldValue, aAffectsOrder);
+ }
+ }
+}
+
+/*******************************************************************************
+ * 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::GetRef().MaybeRecordShutdownStep(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_NO_REASON(mgr);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Database::RecvAllowToClose() {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(mAllowedToClose)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ AllowToClose();
+
+ return IPC_OK();
+}
+
+PBackgroundLSSnapshotParent* Database::AllocPBackgroundLSSnapshotParent(
+ const nsString& aDocumentURI, const nsString& aKey,
+ const bool& aIncreasePeakUsage, const int64_t& aRequestedSize,
+ const int64_t& aMinSize, LSSnapshotInitInfo* aInitInfo) {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(aIncreasePeakUsage && aRequestedSize <= 0)) {
+ ASSERT_UNLESS_FUZZING();
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(aIncreasePeakUsage && aMinSize <= 0)) {
+ ASSERT_UNLESS_FUZZING();
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(mAllowedToClose)) {
+ ASSERT_UNLESS_FUZZING();
+ 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 nsString& aDocumentURI,
+ const nsString& aKey, const bool& aIncreasePeakUsage,
+ const int64_t& aRequestedSize, const int64_t& aMinSize,
+ LSSnapshotInitInfo* aInitInfo) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT_IF(aIncreasePeakUsage, aRequestedSize > 0);
+ 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);
+
+ nsTHashtable<nsStringHashKey> unknownItems;
+ if (addKeyToUnknownItems) {
+ unknownItems.PutEntry(aKey);
+ }
+
+ uint32_t totalLength = mDatastore->GetLength();
+
+ int64_t initialUsage = mDatastore->GetUsage();
+
+ int64_t peakUsage = initialUsage;
+
+ if (aIncreasePeakUsage) {
+ int64_t size = mDatastore->RequestUpdateUsage(aRequestedSize, aMinSize);
+ peakUsage += size;
+ }
+
+ bool hasOtherProcessObservers = mDatastore->HasOtherProcessObservers(this);
+
+ snapshot->Init(loadedItems, unknownItems, nextLoadIndex, totalLength,
+ initialUsage, peakUsage, loadState, hasOtherProcessObservers);
+
+ RegisterSnapshot(snapshot);
+
+ aInitInfo->addKeyToUnknownItems() = addKeyToUnknownItems;
+ aInitInfo->itemInfos() = std::move(itemInfos);
+ aInitInfo->totalLength() = totalLength;
+ aInitInfo->initialUsage() = initialUsage;
+ aInitInfo->peakUsage() = peakUsage;
+ aInitInfo->loadState() = loadState;
+ 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.GetEntry(aKey) && !mUnknownItems.GetEntry(aKey)) {
+ LSValue oldValue(aOldValue);
+ mValues.LookupForAdd(aKey).OrInsert([oldValue]() { return oldValue; });
+ }
+
+ 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_NO_REASON(mgr);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvCheckpoint(
+ nsTArray<LSWriteInfo>&& aWriteInfos) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mUsage >= 0);
+ MOZ_ASSERT(mPeakUsage >= mUsage);
+
+ if (NS_WARN_IF(aWriteInfos.IsEmpty())) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (NS_WARN_IF(mHasOtherProcessObservers)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ 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::RecvCheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mUsage >= 0);
+ MOZ_ASSERT(mPeakUsage >= mUsage);
+
+ if (NS_WARN_IF(aWriteAndNotifyInfos.IsEmpty())) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (NS_WARN_IF(!mHasOtherProcessObservers)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ 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::RecvFinish() {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ Finish();
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvLoaded() {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (NS_WARN_IF(mLoadedReceived)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (NS_WARN_IF(mLoadedAllItems)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (NS_WARN_IF(mLoadKeysReceived)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ mLoadedReceived = true;
+
+ mLoadedItems.Clear();
+ mUnknownItems.Clear();
+ mValues.Clear();
+ mKeys.Clear();
+ mLoadedAllItems = true;
+ mLoadKeysReceived = true;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvLoadValueAndMoreItems(
+ const nsString& aKey, LSValue* aValue, nsTArray<LSItemInfo>* aItemInfos) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aValue);
+ MOZ_ASSERT(aItemInfos);
+ MOZ_ASSERT(mDatastore);
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (NS_WARN_IF(mLoadedReceived)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (NS_WARN_IF(mLoadedAllItems)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (mLoadedItems.GetEntry(aKey) || mUnknownItems.GetEntry(aKey)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (auto entry = mValues.Lookup(aKey)) {
+ *aValue = entry.Data();
+ entry.Remove();
+ } else {
+ mDatastore->GetItem(aKey, *aValue);
+ }
+
+ if (aValue->IsVoid()) {
+ mUnknownItems.PutEntry(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
+ for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
+ MOZ_ASSERT(iter.Data().IsVoid());
+ }
+#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)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (NS_WARN_IF(mLoadedReceived)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (NS_WARN_IF(mLoadKeysReceived)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ mLoadKeysReceived = true;
+
+ if (mSavedKeys) {
+ aKeys->AppendElements(std::move(mKeys));
+ } else {
+ mDatastore->GetKeys(*aKeys);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvIncreasePeakUsage(
+ const int64_t& aRequestedSize, const int64_t& aMinSize, int64_t* aSize) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aSize);
+
+ if (NS_WARN_IF(aRequestedSize <= 0)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (NS_WARN_IF(aMinSize <= 0)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ ASSERT_UNLESS_FUZZING();
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ int64_t size = mDatastore->RequestUpdateUsage(aRequestedSize, aMinSize);
+
+ mPeakUsage += size;
+
+ *aSize = size;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvPing() {
+ AssertIsOnBackgroundThread();
+
+ // Do nothing here. This is purely a sync message allowing the child to
+ // confirm that the actor has received previous async message.
+
+ 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_NO_REASON(mgr);
+ }
+ return IPC_OK();
+}
+
+/*******************************************************************************
+ * LSRequestBase
+ ******************************************************************************/
+
+LSRequestBase::LSRequestBase(nsIEventTarget* aMainEventTarget,
+ const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : mMainEventTarget(aMainEventTarget),
+ 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()))) {
+ 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()))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!VerifyClientId(mContentParentId,
+ commonParams.principalInfo(),
+ 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()))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!VerifyClientId(mContentParentId, params.principalInfo(),
+ 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_FAILURE;
+ }
+
+#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;
+ }
+
+ LS_TRY(Start());
+
+ return NS_OK;
+}
+
+void LSRequestBase::SendReadyMessage() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingReadyMessage);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ MaybeSetFailureCode(NS_ERROR_FAILURE);
+ }
+
+ 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_FAILURE;
+ }
+
+ 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_FAILURE);
+ }
+
+ 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_FAILURE)) {
+ return IPC_FAIL_NO_REASON(mgr);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult LSRequestBase::RecvFinish() {
+ AssertIsOnOwningThread();
+
+ Finish();
+
+ return IPC_OK();
+}
+
+/*******************************************************************************
+ * PrepareDatastoreOp
+ ******************************************************************************/
+
+PrepareDatastoreOp::PrepareDatastoreOp(
+ nsIEventTarget* aMainEventTarget, const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : LSRequestBase(aMainEventTarget, aParams, aContentParentId),
+ mMainEventTarget(aMainEventTarget),
+ 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::QuotaManagerPending:
+ aResult.AppendLiteral("QuotaManagerPending");
+ 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) {
+ mQuotaInfo = QuotaManager::GetInfoForChrome();
+ } else {
+ MOZ_ASSERT(storagePrincipalInfo.type() ==
+ PrincipalInfo::TContentPrincipalInfo);
+
+ QuotaInfo quotaInfo =
+ QuotaManager::GetInfoFromValidatedPrincipalInfo(storagePrincipalInfo);
+
+ mQuotaInfo.mSuffix = std::move(quotaInfo.mSuffix);
+ mQuotaInfo.mGroup = std::move(quotaInfo.mGroup);
+ mMainThreadOrigin = std::move(quotaInfo.mOrigin);
+ }
+
+ 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_FAILURE;
+ }
+
+ 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.
+ mQuotaInfo.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;
+ }
+ }
+
+ LS_TRY(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_FAILURE;
+ }
+
+ LS_TRY(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;
+ }
+
+ LS_TRY(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_FAILURE;
+ }
+
+ LS_TRY(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());
+
+ if ((mDatastore = GetDatastore(Origin()))) {
+ MOZ_ASSERT(!mDatastore->IsClosed());
+
+ mDatastore->NoteLivePrepareDatastoreOp(this);
+
+ FinishNesting();
+
+ return NS_OK;
+ }
+
+ if (QuotaManager::Get()) {
+ nsresult rv = OpenDirectory();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+ }
+
+ mNestedState = NestedState::QuotaManagerPending;
+ QuotaManager::GetOrCreate(this, mMainEventTarget);
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::QuotaManagerOpen() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::QuotaManagerPending);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ LS_TRY(OkIf(QuotaManager::Get()), NS_ERROR_FAILURE);
+
+ LS_TRY(OpenDirectory());
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::OpenDirectory() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::PreparationPending ||
+ mNestedState == NestedState::QuotaManagerPending);
+ MOZ_ASSERT(OriginIsKnown());
+ MOZ_ASSERT(!mDirectoryLock);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+ MOZ_ASSERT(QuotaManager::Get());
+
+ mNestedState = NestedState::DirectoryOpenPending;
+ mPendingDirectoryLock = QuotaManager::Get()->OpenDirectory(
+ PERSISTENCE_TYPE_DEFAULT, mQuotaInfo, mozilla::dom::quota::Client::LS,
+ /* aExclusive */ false, 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);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) ||
+ !MayProceedOnNonOwningThread()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ // This must be called before EnsureTemporaryStorageIsInitialized.
+ LS_TRY(quotaManager->EnsureStorageIsInitialized());
+
+ // This ensures that usages for existings origin directories are cached in
+ // memory.
+ LS_TRY(quotaManager->EnsureTemporaryStorageIsInitialized());
+
+ const UsageInfo usageInfo = quotaManager->GetUsageForClient(
+ PERSISTENCE_TYPE_DEFAULT, mQuotaInfo, mozilla::dom::quota::Client::LS);
+
+ const bool hasUsage = usageInfo.DatabaseUsage().isSome();
+ MOZ_ASSERT(usageInfo.FileUsage().isNothing());
+
+ if (!gArchivedOrigins) {
+ LS_TRY(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.
+ LS_TRY_INSPECT(
+ const auto& directoryEntry,
+ ([hasDataForMigration, &quotaManager,
+ this]() -> mozilla::Result<nsCOMPtr<nsIFile>, nsresult> {
+ if (hasDataForMigration) {
+ LS_TRY_RETURN(quotaManager
+ ->EnsureTemporaryOriginIsInitialized(
+ PERSISTENCE_TYPE_DEFAULT, mQuotaInfo)
+ .map([](const auto& res) { return res.first; }));
+ }
+
+ LS_TRY_UNWRAP(auto directoryEntry,
+ quotaManager->GetDirectoryForOrigin(
+ PERSISTENCE_TYPE_DEFAULT, Origin()));
+
+ quotaManager->EnsureQuotaForOrigin(PERSISTENCE_TYPE_DEFAULT,
+ mQuotaInfo);
+
+ return directoryEntry;
+ }()));
+
+ LS_TRY(directoryEntry->Append(
+ NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME)));
+
+ LS_TRY_INSPECT(const auto& directoryPath,
+ MOZ_TO_RESULT_INVOKE_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.
+ LS_TRY(EnsureDirectoryEntry(directoryEntry,
+ /* aCreateIfNotExists */ hasDataForMigration,
+ /* aIsDirectory */ true));
+
+ LS_TRY(directoryEntry->Append(kDataFileName));
+
+ LS_TRY(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;
+ LS_TRY(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();
+
+ LS_TRY(OkIf(quotaObject), Err(NS_ERROR_FAILURE));
+
+ LS_TRY_INSPECT(const auto& usageFile, GetUsageFile(directoryPath));
+
+ LS_TRY_INSPECT(const auto& usageJournalFile,
+ GetUsageJournalFile(directoryPath));
+
+ LS_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;
+ })));
+
+ LS_TRY(VerifyDatabaseInformation(connection));
+
+ if (hasDataForMigration) {
+ MOZ_ASSERT(mUsage == 0);
+
+ LS_TRY(AttachArchiveDatabase(quotaManager->GetStoragePath(), connection));
+
+ LS_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);
+
+ {
+ nsCOMPtr<mozIStorageFunction> function = new CompressFunction();
+
+ LS_TRY(connection->CreateFunction("compress"_ns, 1, function));
+
+ function = new CompressibleFunction();
+
+ LS_TRY(connection->CreateFunction("compressible"_ns, 1, function));
+
+ LS_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "INSERT INTO data (key, value, utf16Length, compressed) "
+ "SELECT key, compress(value), utf16Length(value), "
+ "compressible(value) "
+ "FROM webappsstore2 "
+ "WHERE originKey = :originKey "
+ "AND originAttributes = :originAttributes;"_ns));
+
+ LS_TRY(mArchivedOriginScope->BindToStatement(stmt));
+
+ LS_TRY(stmt->Execute());
+
+ LS_TRY(connection->RemoveFunction("compress"_ns));
+
+ LS_TRY(connection->RemoveFunction("compressible"_ns));
+ }
+
+ {
+ LS_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>, connection,
+ CreateStatement,
+ "UPDATE database SET usage = :usage;"_ns));
+
+ LS_TRY(stmt->BindInt64ByName("usage"_ns, newUsage));
+
+ LS_TRY(stmt->Execute());
+ }
+
+ {
+ LS_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "DELETE FROM webappsstore2 "
+ "WHERE originKey = :originKey "
+ "AND originAttributes = :originAttributes;"_ns));
+
+ LS_TRY(mArchivedOriginScope->BindToStatement(stmt));
+ LS_TRY(stmt->Execute());
+ }
+
+ LS_TRY(UpdateUsageFile(usageFile, usageJournalFile, newUsage));
+ LS_TRY(transaction.Commit());
+
+ autoUpdateSize.release();
+
+ LS_TRY(usageJournalFile->Remove(false));
+ LS_TRY(DetachArchiveDatabase(connection));
+
+ MOZ_ASSERT(gArchivedOrigins);
+ MOZ_ASSERT(mArchivedOriginScope->HasMatches(gArchivedOrigins));
+ mArchivedOriginScope->RemoveMatches(gArchivedOrigins);
+
+ mUsage = newUsage;
+ }
+
+ nsCOMPtr<mozIStorageConnection> shadowConnection;
+ if (!gInitializedShadowStorage) {
+ LS_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;
+
+ LS_TRY(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+
+ return NS_OK;
+}
+
+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);
+
+ LS_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(aEntry, Exists));
+
+ if (!exists) {
+ if (!aCreateIfNotExists) {
+ if (aAlreadyExisted) {
+ *aAlreadyExisted = false;
+ }
+ return NS_OK;
+ }
+
+ if (aIsDirectory) {
+ LS_TRY(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);
+
+ LS_TRY_INSPECT(const auto& stmt,
+ CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ *aConnection, "SELECT origin FROM database"_ns));
+
+ LS_TRY(OkIf(stmt), NS_ERROR_FILE_CORRUPTED);
+
+ LS_TRY_INSPECT(const auto& origin,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 0));
+
+ LS_TRY(OkIf(QuotaManager::AreOriginsEqualOnDisk(Origin(), origin)),
+ NS_ERROR_FILE_CORRUPTED);
+
+ return NS_OK;
+}
+
+already_AddRefed<QuotaObject> PrepareDatastoreOp::GetQuotaObject() {
+ MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread());
+ MOZ_ASSERT(!mQuotaInfo.mGroup.IsEmpty());
+ MOZ_ASSERT(OriginIsKnown());
+ MOZ_ASSERT(!mDatabaseFilePath.IsEmpty());
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ RefPtr<QuotaObject> quotaObject = quotaManager->GetQuotaObject(
+ PERSISTENCE_TYPE_DEFAULT, mQuotaInfo, mozilla::dom::quota::Client::LS,
+ mDatabaseFilePath, mUsage);
+
+ if (!quotaObject) {
+ LS_WARNING("Failed to get quota object for group (%s) and origin (%s)!",
+ mQuotaInfo.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_FAILURE;
+ }
+
+ if (!gConnectionThread) {
+ gConnectionThread = new ConnectionThread();
+ }
+
+ mConnection = gConnectionThread->CreateConnection(
+ mQuotaInfo, 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;
+
+ LS_TRY(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::QuotaManagerPending:
+ rv = QuotaManagerOpen();
+ 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(
+ mQuotaInfo, std::move(mArchivedOriginScope),
+ /* aDatabaseWasNotAvailable */ true);
+ MOZ_ASSERT(mConnection);
+ }
+
+ quotaObject = GetQuotaObject();
+ if (!quotaObject) {
+ aResponse = NS_ERROR_FAILURE;
+ return;
+ }
+ }
+
+ mDatastore = new Datastore(
+ mQuotaInfo, 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->Get(Origin()));
+ gDatastores->Put(Origin(), mDatastore);
+ }
+
+ if (mPrivateBrowsingId && !mInvalidated) {
+ if (!gPrivateDatastores) {
+ gPrivateDatastores = MakeUnique<PrivateDatastoreHashtable>();
+ }
+
+ if (!gPrivateDatastores->Get(Origin())) {
+ auto privateDatastore =
+ MakeUnique<PrivateDatastore>(WrapMovingNotNull(mDatastore));
+
+ gPrivateDatastores->Put(Origin(), std::move(privateDatastore));
+
+ mPrivateDatastoreRegistered.Flip();
+ }
+ }
+
+ mDatastoreId = ++gLastDatastoreId;
+
+ auto preparedDatastore = MakeUnique<PreparedDatastore>(
+ mDatastore, mContentParentId, Origin(), mDatastoreId,
+ /* aForPreload */ mForPreload);
+
+ if (!gPreparedDatastores) {
+ gPreparedDatastores = new PreparedDatastoreHashtable();
+ }
+ gPreparedDatastores->Put(mDatastoreId, preparedDatastore.get());
+
+ if (mInvalidated) {
+ preparedDatastore->Invalidate();
+ }
+
+ mPreparedDatastoreRegistered.Flip();
+
+ Unused << preparedDatastore.release();
+
+ 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::GetRef().MaybeRecordShutdownStep(
+ 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_FAILURE);
+
+ 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() {
+ AssertIsOnConnectionThread();
+ 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_FAILURE;
+ }
+
+ Connection::CachedStatement stmt;
+ nsresult rv = mConnection->GetCachedStatement(
+ nsLiteralCString("SELECT key, value, utf16Length, compressed "
+ "FROM data;"),
+ &stmt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ LS_TRY(quota::CollectWhileHasResult(
+ *stmt, [this](auto& stmt) -> mozilla::Result<Ok, nsresult> {
+ LS_TRY_UNWRAP(auto key,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsString, stmt, GetString, 0));
+
+ LSValue value;
+ LS_TRY(value.InitFromStatement(&stmt, 1));
+
+ mPrepareDatastoreOp->mValues.Put(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
+
+ LS_TRY_INSPECT(const auto& value,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCString, aFunctionArguments,
+ GetUTF8String, 0));
+
+ nsCString compressed;
+ LS_TRY(OkIf(SnappyCompress(value, compressed)), NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIVariant> result =
+ new storage::UTF8TextVariant(compressed.IsVoid() ? value : compressed);
+
+ result.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(PrepareDatastoreOp::CompressibleFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+PrepareDatastoreOp::CompressibleFunction::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
+
+ LS_TRY_INSPECT(const auto& value,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCString, aFunctionArguments,
+ GetUTF8String, 0));
+
+ nsCString compressed;
+ LS_TRY(OkIf(SnappyCompress(value, compressed)), NS_ERROR_FAILURE);
+
+ const bool compressible = !compressed.IsVoid();
+
+ nsCOMPtr<nsIVariant> result = new storage::IntegerVariant(compressible);
+
+ result.forget(aResult);
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * PrepareObserverOp
+ ******************************************************************************/
+
+PrepareObserverOp::PrepareObserverOp(
+ nsIEventTarget* aMainEventTarget, const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : LSRequestBase(aMainEventTarget, 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->Put(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()))) {
+ 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_FAILURE;
+ }
+
+#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;
+ }
+
+ LS_TRY(Start());
+
+ return NS_OK;
+}
+
+void LSSimpleRequestBase::SendResults() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ MaybeSetFailureCode(NS_ERROR_FAILURE);
+ }
+
+ 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;
+}
+
+/*******************************************************************************
+ * 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() || IsOnConnectionThread());
+ MOZ_ASSERT(aStmt);
+
+ struct Matcher {
+ mozIStorageStatement* mStmt;
+
+ explicit Matcher(mozIStorageStatement* aStmt) : mStmt(aStmt) {}
+
+ nsresult operator()(const Origin& aOrigin) {
+ LS_TRY(mStmt->BindUTF8StringByName("originKey"_ns,
+ aOrigin.OriginNoSuffix()));
+
+ LS_TRY(mStmt->BindUTF8StringByName("originAttributes"_ns,
+ aOrigin.OriginSuffix()));
+
+ return NS_OK;
+ }
+
+ nsresult operator()(const Prefix& aPrefix) {
+ LS_TRY(mStmt->BindUTF8StringByName("originKey"_ns,
+ aPrefix.OriginNoSuffix()));
+
+ return NS_OK;
+ }
+
+ nsresult operator()(const Pattern& aPattern) {
+ LS_TRY(mStmt->BindUTF8StringByName("originAttributesPattern"_ns,
+ "pattern1"_ns));
+
+ return NS_OK;
+ }
+
+ nsresult operator()(const Null& aNull) { return NS_OK; }
+ };
+
+ LS_TRY(mData.match(Matcher(aStmt)));
+
+ return NS_OK;
+}
+
+bool ArchivedOriginScope::HasMatches(
+ ArchivedOriginHashtable* aHashtable) const {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aHashtable);
+
+ struct Matcher {
+ ArchivedOriginHashtable* mHashtable;
+
+ explicit Matcher(ArchivedOriginHashtable* aHashtable)
+ : mHashtable(aHashtable) {}
+
+ bool operator()(const Origin& aOrigin) {
+ nsCString hashKey = GetArchivedOriginHashKey(aOrigin.OriginSuffix(),
+ aOrigin.OriginNoSuffix());
+
+ ArchivedOriginInfo* archivedOriginInfo;
+ return mHashtable->Get(hashKey, &archivedOriginInfo);
+ }
+
+ bool operator()(const Prefix& aPrefix) {
+ for (auto iter = mHashtable->ConstIter(); !iter.Done(); iter.Next()) {
+ const auto& archivedOriginInfo = iter.Data();
+
+ if (archivedOriginInfo->mOriginNoSuffix == aPrefix.OriginNoSuffix()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ bool operator()(const Pattern& aPattern) {
+ for (auto iter = mHashtable->ConstIter(); !iter.Done(); iter.Next()) {
+ const auto& archivedOriginInfo = iter.Data();
+
+ if (aPattern.GetPattern().Matches(
+ archivedOriginInfo->mOriginAttributes)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ bool operator()(const Null& aNull) { return mHashtable->Count(); }
+ };
+
+ return mData.match(Matcher(aHashtable));
+}
+
+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"),
+ mShutdownRequested(false) {
+ 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 GroupAndOrigin& aGroupAndOrigin,
+ const AtomicBool& aCanceled) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ LS_TRY_INSPECT(const auto& directory,
+ quotaManager->GetDirectoryForOrigin(aPersistenceType,
+ aGroupAndOrigin.mOrigin));
+
+ MOZ_ASSERT(directory);
+
+ LS_TRY(directory->Append(NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME)));
+
+#ifdef DEBUG
+ {
+ LS_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(directory, Exists));
+ MOZ_ASSERT(exists);
+ }
+#endif
+
+ LS_TRY_INSPECT(const auto& directoryPath,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsString, directory, GetPath));
+
+ LS_TRY_INSPECT(const auto& usageFile, GetUsageFile(directoryPath));
+
+ // XXX Try to make usageFileExists const
+ LS_TRY_UNWRAP(bool usageFileExists, ExistsAsFile(*usageFile));
+
+ LS_TRY_INSPECT(const auto& usageJournalFile,
+ GetUsageJournalFile(directoryPath));
+
+ LS_TRY_INSPECT(const bool& usageJournalFileExists,
+ ExistsAsFile(*usageJournalFile));
+
+ if (usageJournalFileExists) {
+ if (usageFileExists) {
+ LS_TRY(usageFile->Remove(false));
+
+ usageFileExists = false;
+ }
+
+ LS_TRY(usageJournalFile->Remove(false));
+ }
+
+ LS_TRY_INSPECT(const auto& file,
+ CloneFileAndAppend(*directory, kDataFileName));
+
+ LS_TRY_INSPECT(const bool& fileExists, ExistsAsFile(*file));
+
+ LS_TRY_INSPECT(
+ const UsageInfo& res,
+ ([fileExists, usageFileExists, &file, &usageFile, &usageJournalFile,
+ &aGroupAndOrigin]() -> Result<UsageInfo, nsresult> {
+ if (fileExists) {
+ LS_TRY_RETURN(
+ // To simplify control flow, we call LoadUsageFile unconditionally
+ // here, even though it will necessarily fail if usageFileExists
+ // is false.
+ LoadUsageFile(*usageFile)
+ .orElse([&file, &usageFile, &usageJournalFile,
+ &aGroupAndOrigin](
+ const nsresult) -> Result<UsageInfo, nsresult> {
+ LS_TRY_INSPECT(
+ const auto& connection,
+ CreateStorageConnection(
+ *file, *usageFile, aGroupAndOrigin.mOrigin, [] {}));
+
+ LS_TRY_INSPECT(
+ const int64_t& usage,
+ GetUsage(*connection,
+ /* aArchivedOriginScope */ nullptr));
+
+ LS_TRY(UpdateUsageFile(usageFile, usageJournalFile, usage));
+
+ LS_TRY(usageJournalFile->Remove(false));
+
+ MOZ_ASSERT(usage >= 0);
+ return UsageInfo{DatabaseUsageType(Some(uint64_t(usage)))};
+ }));
+ }
+
+ if (usageFileExists) {
+ LS_TRY(usageFile->Remove(false));
+ }
+
+ return UsageInfo{};
+ }()));
+
+ // Report unknown files in debug builds, but don't fail, just warn.
+
+#ifdef DEBUG
+ LS_TRY(CollectEachFileAtomicCancelable(
+ *directory, aCanceled,
+ [](const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> {
+ LS_TRY_INSPECT(const bool& isDirectory,
+ MOZ_TO_RESULT_INVOKE(file, IsDirectory));
+
+ if (isDirectory) {
+ Unused << WARN_IF_FILE_IS_UNKNOWN(*file);
+ return Ok{};
+ }
+
+ LS_TRY_INSPECT(const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_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);
+
+ return Ok{};
+ }));
+#endif
+
+ return res;
+}
+
+nsresult QuotaClient::InitOriginWithoutTracking(
+ PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin,
+ const AtomicBool& aCanceled) {
+ AssertIsOnIOThread();
+
+ // This is called when a storage/permanent/chrome/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 GroupAndOrigin& aGroupAndOrigin,
+ 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,
+ aGroupAndOrigin, 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;
+ }
+
+ const bool shadowWrites = gShadowWrites;
+
+ LS_TRY_INSPECT(const auto& archivedOriginScope,
+ CreateArchivedOriginScope(aOriginScope));
+
+ if (!gArchivedOrigins) {
+ LS_TRY(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);
+
+ LS_TRY_INSPECT(
+ const auto& connection,
+ ([&basePath]() -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
+ if (gInitializedShadowStorage) {
+ LS_TRY_RETURN(GetShadowStorageConnection(basePath));
+ }
+
+ LS_TRY_UNWRAP(auto connection,
+ CreateShadowStorageConnection(basePath));
+
+ gInitializedShadowStorage = true;
+
+ return connection;
+ }()));
+
+ if (hasDataForRemoval) {
+ LS_TRY(AttachArchiveDatabase(quotaManager->GetStoragePath(), connection));
+ }
+
+ if (archivedOriginScope->IsPattern()) {
+ nsCOMPtr<mozIStorageFunction> function(
+ new MatchFunction(archivedOriginScope->GetPattern()));
+
+ LS_TRY(connection->CreateFunction("match"_ns, 2, function));
+ }
+
+ {
+ LS_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>, connection,
+ CreateStatement, "BEGIN IMMEDIATE;"_ns));
+
+ LS_TRY(stmt->Execute());
+ }
+
+ if (shadowWrites) {
+ LS_TRY(PerformDelete(connection, "main"_ns, archivedOriginScope.get()));
+ }
+
+ if (hasDataForRemoval) {
+ LS_TRY(
+ PerformDelete(connection, "archive"_ns, archivedOriginScope.get()));
+ }
+
+ {
+ LS_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>, connection,
+ CreateStatement, "COMMIT;"_ns));
+
+ LS_TRY(stmt->Execute());
+ }
+
+ if (archivedOriginScope->IsPattern()) {
+ LS_TRY(connection->RemoveFunction("match"_ns));
+ }
+
+ if (hasDataForRemoval) {
+ LS_TRY(DetachArchiveDatabase(connection));
+
+ MOZ_ASSERT(gArchivedOrigins);
+ MOZ_ASSERT(archivedOriginScope->HasMatches(gArchivedOrigins));
+ archivedOriginScope->RemoveMatches(gArchivedOrigins);
+ }
+
+ LS_TRY(connection->Close());
+ }
+
+ if (aOriginScope.IsNull()) {
+ LS_TRY_INSPECT(const auto& shadowFile, GetShadowFile(basePath));
+
+ LS_TRY(shadowFile->Remove(false));
+
+ gInitializedShadowStorage = false;
+ }
+
+ return NS_OK;
+}
+
+void QuotaClient::OnOriginClearCompleted(PersistenceType aPersistenceType,
+ const nsACString& aOrigin) {
+ AssertIsOnIOThread();
+}
+
+void QuotaClient::ReleaseIOThreadObjects() {
+ AssertIsOnIOThread();
+
+ // 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() {
+ MOZ_ASSERT(!mShutdownRequested);
+ mShutdownRequested = true;
+
+ // 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 (gPrivateDatastores) {
+ gPrivateDatastores->Clear();
+ gPrivateDatastores = nullptr;
+ }
+
+ if (gPreparedDatastores) {
+ gPreparedDatastores->Clear();
+ gPreparedDatastores = nullptr;
+ }
+
+ RequestAllowToCloseDatabasesMatching([](const auto&) { return true; });
+
+ if (gPreparedObsevers) {
+ gPreparedObsevers->Clear();
+ 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(" (");
+
+ nsTHashtable<nsCStringHashKey> ids;
+
+ for (const auto& prepareDatastoreOp : *gPrepareDatastoreOps) {
+ nsCString id;
+ prepareDatastoreOp->Stringify(id);
+
+ ids.PutEntry(id);
+ }
+
+ StringifyTableKeys(ids, data);
+
+ data.Append(")\n");
+ }
+
+ if (gDatastores) {
+ data.Append("Datastores: ");
+ data.AppendInt(gDatastores->Count());
+ data.Append(" (");
+
+ nsTHashtable<nsCStringHashKey> ids;
+
+ for (const auto& entry : *gDatastores) {
+ MOZ_ASSERT(entry.GetData());
+
+ nsCString id;
+ entry.GetData()->Stringify(id);
+
+ ids.PutEntry(id);
+ }
+
+ StringifyTableKeys(ids, data);
+
+ data.Append(")\n");
+ }
+
+ if (gLiveDatabases) {
+ data.Append("LiveDatabases: ");
+ data.AppendInt(static_cast<uint32_t>(gLiveDatabases->Length()));
+ data.Append(" (");
+
+ nsTHashtable<nsCStringHashKey> ids;
+
+ for (const auto& database : *gLiveDatabases) {
+ nsCString id;
+ database->Stringify(id);
+
+ ids.PutEntry(id);
+ }
+
+ StringifyTableKeys(ids, data);
+
+ 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()) {
+ LS_TRY_INSPECT(const auto& principalInfo,
+ QuotaManager::ParseOrigin(aOriginScope.GetOrigin()));
+
+ LS_TRY_INSPECT((const auto& [originAttrSuffix, originKey]),
+ GenerateOriginKey2(principalInfo));
+
+ return ArchivedOriginScope::CreateFromOrigin(originAttrSuffix, originKey);
+ }
+
+ if (aOriginScope.IsPrefix()) {
+ LS_TRY_INSPECT(const auto& principalInfo,
+ QuotaManager::ParseOrigin(aOriginScope.GetOriginNoSuffix()));
+
+ LS_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);
+
+ LS_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_TYPED(
+ nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement,
+ "DELETE FROM "_ns + aSchemaName + ".webappsstore2"_ns +
+ aArchivedOriginScope->GetBindingClause() + ";"_ns));
+
+ LS_TRY(aArchivedOriginScope->BindToStatement(stmt));
+
+ LS_TRY(stmt->Execute());
+
+ return NS_OK;
+}
+
+// static
+nsresult QuotaClient::Observer::Initialize() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<Observer> observer = new Observer();
+
+ LS_TRY(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();
+ LS_TRY(OkIf(backgroundActor), NS_ERROR_FAILURE);
+
+ LS_TRY(OkIf(backgroundActor->SendLSClearPrivateBrowsing()),
+ NS_ERROR_FAILURE);
+
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ LS_TRY(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);
+
+ LS_TRY_INSPECT(const auto& suffix,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsAutoCString, aFunctionArguments,
+ GetUTF8String, 1));
+
+ OriginAttributes oa;
+ LS_TRY(OkIf(oa.PopulateFromSuffix(suffix)), NS_ERROR_FAILURE);
+
+ const bool result = mPattern.Matches(oa);
+
+ RefPtr<nsVariant> outVar(new nsVariant());
+ LS_TRY(outVar->SetAsBool(result));
+
+ outVar.forget(aResult);
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * AutoWriteTransaction
+ ******************************************************************************/
+
+AutoWriteTransaction::AutoWriteTransaction(bool aShadowWrites)
+ : mConnection(nullptr), mShadowWrites(aShadowWrites) {
+ AssertIsOnConnectionThread();
+
+ MOZ_COUNT_CTOR(mozilla::dom::AutoWriteTransaction);
+}
+
+AutoWriteTransaction::~AutoWriteTransaction() {
+ AssertIsOnConnectionThread();
+
+ MOZ_COUNT_DTOR(mozilla::dom::AutoWriteTransaction);
+
+ if (mConnection) {
+ if (NS_FAILED(mConnection->RollbackWriteTransaction())) {
+ NS_WARNING("Failed to rollback write transaction!");
+ }
+
+ if (mShadowWrites && NS_FAILED(DetachShadowDatabaseAndUnlock())) {
+ NS_WARNING("Failed to detach shadow database!");
+ }
+ }
+}
+
+nsresult AutoWriteTransaction::Start(Connection* aConnection) {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(aConnection);
+ MOZ_ASSERT(!mConnection);
+
+ if (mShadowWrites) {
+ LS_TRY(LockAndAttachShadowDatabase(aConnection));
+ }
+
+ LS_TRY(aConnection->BeginWriteTransaction());
+
+ mConnection = aConnection;
+
+ return NS_OK;
+}
+
+nsresult AutoWriteTransaction::Commit() {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(mConnection);
+
+ LS_TRY(mConnection->CommitWriteTransaction());
+
+ if (mShadowWrites) {
+ LS_TRY(DetachShadowDatabaseAndUnlock());
+ }
+
+ mConnection = nullptr;
+
+ return NS_OK;
+}
+
+nsresult AutoWriteTransaction::LockAndAttachShadowDatabase(
+ Connection* aConnection) {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(aConnection);
+ MOZ_ASSERT(!mConnection);
+ MOZ_ASSERT(mShadowDatabaseLock.isNothing());
+ MOZ_ASSERT(mShadowWrites);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ nsCOMPtr<mozIStorageConnection> storageConnection =
+ aConnection->StorageConnection();
+ MOZ_ASSERT(storageConnection);
+
+ mShadowDatabaseLock.emplace(
+ aConnection->GetQuotaClient()->ShadowDatabaseMutex());
+
+ LS_TRY(AttachShadowDatabase(quotaManager->GetBasePath(), storageConnection));
+
+ return NS_OK;
+}
+
+nsresult AutoWriteTransaction::DetachShadowDatabaseAndUnlock() {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(mShadowDatabaseLock.isSome());
+ MOZ_ASSERT(mShadowWrites);
+
+ nsCOMPtr<mozIStorageConnection> storageConnection =
+ mConnection->StorageConnection();
+ MOZ_ASSERT(storageConnection);
+
+ LS_TRY(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..ce579c72cd
--- /dev/null
+++ b/dom/localstorage/LSDatabase.cpp
@@ -0,0 +1,429 @@
+/* -*- 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 "nsDataHashtable.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"
+
+typedef nsDataHashtable<nsCStringHashKey, LSDatabase*> LSDatabaseHashtable;
+
+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->Get(mOrigin));
+ gLSDatabases->Put(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();
+ }
+}
+
+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);
+
+ if (mSnapshot) {
+ return NS_ERROR_ALREADY_INITIALIZED;
+ }
+
+ nsresult rv = EnsureSnapshot(aObject, VoidString(), /* aExplicit */ true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult LSDatabase::EndExplicitSnapshot(LSObject* aObject) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aObject);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ if (!mSnapshot) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ MOZ_ASSERT(mSnapshot->Explicit());
+
+ nsresult rv = mSnapshot->End();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+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,
+ /* requestedSize */ 131072,
+ /* minSize */ 4096, &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);
+
+ nsTArray<RefPtr<LSDatabase>> databases;
+
+ for (auto iter = gLSDatabases->ConstIter(); !iter.Done(); iter.Next()) {
+ LSDatabase* database = iter.Data();
+ MOZ_ASSERT(database);
+
+ databases.AppendElement(database);
+ }
+
+ for (RefPtr<LSDatabase>& database : databases) {
+ 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..dd05d24bb2
--- /dev/null
+++ b/dom/localstorage/LSDatabase.h
@@ -0,0 +1,107 @@
+/* -*- 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 {
+namespace 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 HasActiveSnapshot() const {
+ AssertIsOnOwningThread();
+
+ return !!mSnapshot;
+ }
+
+ 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 EndExplicitSnapshot(LSObject* aObject);
+
+ private:
+ ~LSDatabase();
+
+ nsresult EnsureSnapshot(LSObject* aObject, const nsAString& aKey,
+ bool aExplicit = false);
+
+ void AllowToClose();
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_LSDatabase_h
diff --git a/dom/localstorage/LSObject.cpp b/dom/localstorage/LSObject.cpp
new file mode 100644
index 0000000000..ca019e5bb2
--- /dev/null
+++ b/dom/localstorage/LSObject.cpp
@@ -0,0 +1,1384 @@
+/* -*- 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/OriginAttributes.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RemoteLazyInputStreamThread.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_dom.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/PBackgroundSharedTypes.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
+
+namespace mozilla::dom {
+
+namespace {
+
+class RequestHelper;
+
+StaticMutex gRequestHelperMutex;
+nsISerialEventTarget* gSyncLoopEventTarget = nullptr;
+/**
+ * Tracks whether a sync message has been received to the main thread but not
+ * yet processed. Used by RequestHelper logic to abort effectively synchronous
+ * calls if a sync IPC message is received which could result in deadlock.
+ * This is a boolean because, by definition, the parent can only send one sync
+ * message to the child at a time.
+ */
+bool gPendingSyncMessage = false;
+
+/*
+ * Wrapper for the pushed event queue. The wrapper automatically dispatches
+ * runnables to the main thread when pushed event queue is no longer active.
+ * This exists because the event loop spinning can be aborted.
+ */
+class NestedEventTargetWrapper final : public nsISerialEventTarget {
+ nsCOMPtr<nsISerialEventTarget> mNestedEventTarget;
+ bool mDisconnected;
+
+ public:
+ explicit NestedEventTargetWrapper(nsISerialEventTarget* aNestedEventTarget)
+ : mNestedEventTarget(aNestedEventTarget), mDisconnected(false) {}
+
+ private:
+ ~NestedEventTargetWrapper() = default;
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_DECL_NSIEVENTTARGET_FULL
+};
+
+/**
+ * Main-thread helper that implements the blocking logic required by
+ * LocalStorage's synchronous semantics. StartAndReturnResponse pushes an
+ * event queue which is a new event target and spins its nested event loop until
+ * a result is received or an abort is necessary due to a PContent-managed sync
+ * IPC message being received. Note that because the event queue is its own
+ * event target, there is no re-entrancy. Normal main-thread runnables will not
+ * get a chance to run. 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 pushes the event queue and Dispatches
+ * the RequestHelper to the RemoteLazyInputStream thread.
+ * - RemoteLazyInputStream Thread: RequestHelper::Run is called, invoking
+ * Start() which invokes LSObject::StartRequest, which gets-or-creates the
+ * PBackground actor if necessary (which may dispatch a runnable to the nested
+ * event queue on the main thread), 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 Finishing
+ * and dispatching RequestHelper to its own nested event target.
+ * - Main Thread: RequestHelper::Run is called, invoking Finish() which advances
+ * the state to Complete and sets mWaiting to false, allowing the nested event
+ * loop being spun by StartAndReturnResponse to cease spinning and return 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.
+ */
+ ResponsePending,
+ /**
+ * A response has been received and RequestHelper has been dispatched back
+ * to the nested event loop to call Finish().
+ */
+ Finishing,
+ /**
+ * Finish() has been called on the main thread. The nested event loop will
+ * terminate imminently and the received response returned 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 pushed event queue that we use to spin the event loop without
+ // processing any of the events dispatched at the mOwningEventTarget (which
+ // would result in re-entrancy and violate LocalStorage semantics).
+ nsCOMPtr<nsISerialEventTarget> mNestedEventTarget;
+ // The wrapper for the pushed event queue. The wrapper automatically
+ // dispatches runnables to the main thread when pushed event queue is no
+ // longer active. This exists because the event loop spinning can be aborted.
+ nsCOMPtr<nsISerialEventTarget> mNestedEventTargetWrapper;
+ // 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;
+ LSRequestResponse mResponse;
+ nsresult mResultCode;
+ State mState;
+ // Control flag for the nested event loop; once set to false, the loop ends.
+ bool mWaiting;
+ bool mCancelled;
+
+ public:
+ RequestHelper(LSObject* aObject, const LSRequestParams& aParams)
+ : Runnable("dom::RequestHelper"),
+ mObject(aObject),
+ mOwningEventTarget(GetCurrentEventTarget()),
+ mActor(nullptr),
+ mParams(aParams),
+ mResultCode(NS_OK),
+ mState(State::Initial),
+ mWaiting(true),
+ mCancelled(false) {}
+
+ 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;
+
+ nsresult Start();
+
+ void Finish();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_NSIRUNNABLE
+
+ // LSRequestChildCallback
+ void OnResponse(const LSRequestResponse& aResponse) override;
+};
+
+} // 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
+void LSObject::Initialize() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIEventTarget> domFileThread =
+ RemoteLazyInputStreamThread::GetOrCreate();
+ if (NS_WARN_IF(!domFileThread)) {
+ return;
+ }
+
+ RefPtr<Runnable> runnable =
+ NS_NewRunnableFunction("LSObject::Initialize", []() {
+ AssertIsOnDOMFileThread();
+
+ mozilla::ipc::PBackgroundChild* backgroundActor =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+
+ if (NS_WARN_IF(!backgroundActor)) {
+ return;
+ }
+ });
+
+ if (NS_WARN_IF(
+ NS_FAILED(domFileThread->Dispatch(runnable, NS_DISPATCH_NORMAL)))) {
+ return;
+ }
+}
+
+// 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
+ LS_TRY_INSPECT(
+ const auto& quotaInfo,
+ quota::QuotaManager::GetInfoFromPrincipal(storagePrincipal.get()));
+
+ MOZ_ASSERT(originAttrSuffix == quotaInfo.mSuffix);
+
+ const auto& origin = quotaInfo.mOrigin;
+#else
+ LS_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());
+
+ 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->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
+ LS_TRY_INSPECT(
+ const auto& quotaInfo,
+ ([&storagePrincipalInfo,
+ &aPrincipal]() -> Result<quota::QuotaInfo, nsresult> {
+ if (storagePrincipalInfo->type() ==
+ PrincipalInfo::TSystemPrincipalInfo) {
+ return quota::QuotaManager::GetInfoForChrome();
+ }
+
+ LS_TRY_RETURN(quota::QuotaManager::GetInfoFromPrincipal(aPrincipal));
+ }()));
+
+ MOZ_ASSERT(originAttrSuffix == quotaInfo.mSuffix);
+
+ const auto& origin = quotaInfo.mOrigin;
+#else
+ LS_TRY_INSPECT(
+ const auto& origin, ([&storagePrincipalInfo,
+ &aPrincipal]() -> Result<nsAutoCString, nsresult> {
+ if (storagePrincipalInfo->type() ==
+ PrincipalInfo::TSystemPrincipalInfo) {
+ return nsAutoCString{quota::QuotaManager::GetOriginForChrome()};
+ }
+
+ LS_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
+
+// static
+already_AddRefed<nsISerialEventTarget> LSObject::GetSyncLoopEventTarget() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsCOMPtr<nsISerialEventTarget> target;
+
+ {
+ StaticMutexAutoLock lock(gRequestHelperMutex);
+ target = gSyncLoopEventTarget;
+ }
+
+ return target.forget();
+}
+
+// static
+void LSObject::OnSyncMessageReceived() {
+ nsCOMPtr<nsISerialEventTarget> target;
+
+ {
+ StaticMutexAutoLock lock(gRequestHelperMutex);
+ target = gSyncLoopEventTarget;
+ gPendingSyncMessage = true;
+ }
+
+ if (target) {
+ RefPtr<Runnable> runnable =
+ NS_NewRunnableFunction("LSObject::CheckFlagRunnable", []() {});
+
+ MOZ_ALWAYS_SUCCEEDS(
+ target->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL));
+ }
+}
+
+// static
+void LSObject::OnSyncMessageHandled() {
+ StaticMutexAutoLock lock(gRequestHelperMutex);
+ gPendingSyncMessage = false;
+}
+
+LSRequestChild* LSObject::StartRequest(nsIEventTarget* aMainEventTarget,
+ const LSRequestParams& aParams,
+ LSRequestChildCallback* aCallback) {
+ AssertIsOnDOMFileThread();
+
+ mozilla::ipc::PBackgroundChild* backgroundActor =
+ XRE_IsParentProcess()
+ ? mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(
+ aMainEventTarget)
+ : mozilla::ipc::BackgroundChild::GetForCurrentThread();
+ 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;
+}
+
+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::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;
+ }
+
+ nsresult rv = EndExplicitSnapshotInternal();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+}
+
+bool LSObject::GetHasActiveSnapshot(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) {
+ AssertIsOnOwningThread();
+
+ if (!CanUseStorage(aSubjectPrincipal)) {
+ aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return 0;
+ }
+
+ if (mDatabase && mDatabase->HasActiveSnapshot()) {
+ MOZ_ASSERT(!mDatabase->IsAllowedToClose());
+
+ return true;
+ }
+
+ return false;
+}
+
+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;
+
+ 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();
+
+ if (mInExplicitSnapshot) {
+ nsresult rv = EndExplicitSnapshotInternal();
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ }
+
+ 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;
+
+ 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);
+}
+
+nsresult LSObject::EndExplicitSnapshotInternal() {
+ AssertIsOnOwningThread();
+
+ // Can be only called if the mInExplicitSnapshot flag is true.
+ // An explicit snapshot must have been created.
+ MOZ_ASSERT(mInExplicitSnapshot);
+
+ // If an explicit snapshot have been created then mDatabase must be not null.
+ // DropDatabase could be called in the meatime, but that would set
+ // 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(mDatabase);
+
+ // 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(!mDatabase->IsAllowedToClose());
+
+ nsresult rv = mDatabase->EndExplicitSnapshot(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mInExplicitSnapshot = false;
+
+ return NS_OK;
+}
+
+void LSObject::LastRelease() {
+ AssertIsOnOwningThread();
+
+ DropDatabase();
+}
+
+NS_IMPL_ISUPPORTS(NestedEventTargetWrapper, nsIEventTarget,
+ nsISerialEventTarget);
+
+NS_IMETHODIMP_(bool)
+NestedEventTargetWrapper::IsOnCurrentThreadInfallible() {
+ MOZ_CRASH(
+ "IsOnCurrentThreadInfallible should never be called on "
+ "NestedEventTargetWrapper");
+}
+
+NS_IMETHODIMP
+NestedEventTargetWrapper::IsOnCurrentThread(bool* aResult) {
+ MOZ_CRASH(
+ "IsOnCurrentThread should never be called on "
+ "NestedEventTargetWrapper");
+}
+
+NS_IMETHODIMP
+NestedEventTargetWrapper::Dispatch(already_AddRefed<nsIRunnable> aEvent,
+ uint32_t aFlags) {
+ MOZ_ASSERT(mNestedEventTarget);
+
+ if (mDisconnected) {
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(std::move(aEvent), aFlags));
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIRunnable> event(aEvent);
+
+ nsresult rv = mNestedEventTarget->Dispatch(event, aFlags);
+ if (rv == NS_ERROR_UNEXPECTED) {
+ mDisconnected = true;
+
+ // Dispatch leaked the event object on the NS_ERROR_UNEXPECTED failure, so
+ // we explicitly release this object once for that.
+ event.get()->Release();
+
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(event.forget(), aFlags));
+ } else if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+NestedEventTargetWrapper::DispatchFromScript(nsIRunnable* aEvent,
+ uint32_t aFlags) {
+ MOZ_CRASH(
+ "DispatchFromScript should never be called on "
+ "NestedEventTargetWrapper");
+}
+
+NS_IMETHODIMP
+NestedEventTargetWrapper::DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,
+ uint32_t aDelayMs) {
+ MOZ_CRASH(
+ "DelayedDispatch should never be called on "
+ "NestedEventTargetWrapper");
+}
+
+nsresult RequestHelper::StartAndReturnResponse(LSRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ // Normally, we would use the standard way of blocking the thread using
+ // a monitor.
+ // The problem is that BackgroundChild::GetOrCreateForCurrentThread()
+ // called on the RemoteLazyInputStream thread may dispatch a runnable to the
+ // main thread to finish initialization of PBackground. A monitor would block
+ // the main thread and the runnable would never get executed causing the
+ // helper to be stuck in a wait loop.
+ // However, BackgroundChild::GetOrCreateForCurrentThread() supports passing
+ // a custom main event target, so we can create a nested event target and
+ // spin the event loop. Nothing can dispatch to the nested event target
+ // except BackgroundChild::GetOrCreateForCurrentThread(), so spinning of the
+ // event loop can't fire any other events.
+ // This way the thread is synchronously blocked in a safe manner and the
+ // runnable gets executed.
+ {
+ auto thread = static_cast<nsThread*>(NS_GetCurrentThread());
+
+ const nsLocalExecutionGuard localExecution(thread->EnterLocalExecution());
+ mNestedEventTarget = localExecution.GetEventTarget();
+ MOZ_ASSERT(mNestedEventTarget);
+
+ mNestedEventTargetWrapper =
+ new NestedEventTargetWrapper(mNestedEventTarget);
+
+ nsCOMPtr<nsIEventTarget> domFileThread =
+ XRE_IsParentProcess() ? RemoteLazyInputStreamThread::GetOrCreate()
+ : RemoteLazyInputStreamThread::Get();
+ if (NS_WARN_IF(!domFileThread)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+
+ {
+ {
+ StaticMutexAutoLock lock(gRequestHelperMutex);
+
+ if (StaticPrefs::dom_storage_abort_on_sync_parent_to_child_messages() &&
+ NS_WARN_IF(gPendingSyncMessage)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ gSyncLoopEventTarget = mNestedEventTargetWrapper;
+ }
+
+ auto autoClearSyncLoopEventTarget = mozilla::MakeScopeExit([&] {
+ StaticMutexAutoLock lock(gRequestHelperMutex);
+ gSyncLoopEventTarget = nullptr;
+ });
+
+ rv = domFileThread->Dispatch(this, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsITimer> timer = NS_NewTimer();
+
+ MOZ_ALWAYS_SUCCEEDS(timer->SetTarget(mNestedEventTarget));
+
+ MOZ_ALWAYS_SUCCEEDS(timer->InitWithNamedFuncCallback(
+ [](nsITimer* aTimer, void* aClosure) {
+ auto helper = static_cast<RequestHelper*>(aClosure);
+
+ helper->mCancelled = true;
+ },
+ this, FAILSAFE_CANCEL_SYNC_OP_MS, nsITimer::TYPE_ONE_SHOT,
+ "RequestHelper::StartAndReturnResponse::SpinEventLoopTimer"));
+
+ MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
+ [&]() {
+ if (mCancelled) {
+ return true;
+ }
+
+ if (!mWaiting) {
+ return true;
+ }
+
+ {
+ StaticMutexAutoLock lock(gRequestHelperMutex);
+ if (StaticPrefs::
+ dom_storage_abort_on_sync_parent_to_child_messages() &&
+ NS_WARN_IF(gPendingSyncMessage)) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+ thread));
+
+ MOZ_ALWAYS_SUCCEEDS(timer->Cancel());
+ }
+
+ // If mWaiting is still set to true, it means that the event loop spinning
+ // was aborted and we need to cancel the request in the parent since we
+ // don't care about the result anymore.
+ // We can check mWaiting here because it's only ever touched on the main
+ // thread.
+ if (NS_WARN_IF(mWaiting)) {
+ // Don't touch mResponse, mResultCode or mState here! The
+ // RemoteLazyInputStream Thread may be accessing them at the same moment.
+
+ RefPtr<RequestHelper> self = this;
+
+ RefPtr<Runnable> runnable =
+ NS_NewRunnableFunction("RequestHelper::SendCancelRunnable", [self]() {
+ LSRequestChild* actor = self->mActor;
+
+ // Start() could fail or it hasn't had a chance to run yet, 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 sense because the request is about to be destroyed
+ // anyway.
+ if (actor && !actor->Finishing()) {
+ actor->SendCancel();
+ }
+ });
+
+ rv = domFileThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_ERROR_FAILURE;
+ }
+
+ // localExecution will be destructed when we leave this scope. If the event
+ // loop spinning was aborted and other threads dispatched new runnables to
+ // the nested event queue, they will be moved to the main event queue here
+ // and later asynchronusly processed. So nothing will be lost.
+ }
+
+ if (NS_WARN_IF(NS_FAILED(mResultCode))) {
+ return mResultCode;
+ }
+
+ aResponse = std::move(mResponse);
+ return NS_OK;
+}
+
+nsresult RequestHelper::Start() {
+ AssertIsOnDOMFileThread();
+ MOZ_ASSERT(mState == State::Initial);
+
+ mState = State::ResponsePending;
+
+ LSRequestChild* actor =
+ mObject->StartRequest(mNestedEventTargetWrapper, mParams, this);
+ if (NS_WARN_IF(!actor)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mActor = actor;
+
+ return NS_OK;
+}
+
+void RequestHelper::Finish() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Finishing);
+
+ mObject = nullptr;
+
+ mWaiting = false;
+
+ mState = State::Complete;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED0(RequestHelper, Runnable)
+
+NS_IMETHODIMP
+RequestHelper::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(
+ mNestedEventTargetWrapper->Dispatch(this, NS_DISPATCH_NORMAL));
+ }
+ }
+
+ return NS_OK;
+}
+
+void RequestHelper::OnResponse(const LSRequestResponse& aResponse) {
+ AssertIsOnDOMFileThread();
+ MOZ_ASSERT(mState == State::ResponsePending);
+
+ mActor = nullptr;
+
+ mResponse = aResponse;
+
+ mState = State::Finishing;
+
+ MOZ_ALWAYS_SUCCEEDS(
+ mNestedEventTargetWrapper->Dispatch(this, NS_DISPATCH_NORMAL));
+}
+
+} // namespace mozilla::dom
diff --git a/dom/localstorage/LSObject.h b/dom/localstorage/LSObject.h
new file mode 100644
index 0000000000..a658cb710f
--- /dev/null
+++ b/dom/localstorage/LSObject.h
@@ -0,0 +1,247 @@
+/* -*- 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 "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 ipc {
+
+class PrincipalInfo;
+
+} // namespace ipc
+
+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 {
+ typedef mozilla::ipc::PrincipalInfo PrincipalInfo;
+
+ friend nsGlobalWindowInner;
+
+ UniquePtr<PrincipalInfo> mPrincipalInfo;
+ UniquePtr<PrincipalInfo> mStoragePrincipalInfo;
+
+ RefPtr<LSDatabase> mDatabase;
+ RefPtr<LSObserver> mObserver;
+
+ uint32_t mPrivateBrowsingId;
+ Maybe<nsID> mClientId;
+ nsCString mOrigin;
+ nsCString mOriginKey;
+ nsString mDocumentURI;
+
+ bool mInExplicitSnapshot;
+
+ public:
+ static void Initialize();
+
+ /**
+ * 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);
+
+ /**
+ * Used for requests from the parent process to the parent process; in that
+ * case we want ActorsParent to know our event-target and this is better than
+ * trying to tunnel the pointer through IPC.
+ */
+ static already_AddRefed<nsISerialEventTarget> GetSyncLoopEventTarget();
+
+ /**
+ * Helper invoked by ContentChild::OnChannelReceivedMessage when a sync IPC
+ * message is received. This will be invoked on the IPC I/O thread and it
+ * will set the gPendingSyncMessage flag to true. It will also force the sync
+ * loop (if it's active) to check the gPendingSyncMessage flag which will
+ * result in premature finish of the loop.
+ *
+ * This is necessary to unblock the main thread when a sync IPC message is
+ * received to avoid the potential for browser deadlock. This should only
+ * occur in (ugly) testing scenarios where CPOWs are in use.
+ *
+ * Aborted sync loop will result in the underlying LSRequest being explicitly
+ * canceled, resulting in the parent sending an NS_ERROR_FAILURE result.
+ */
+ static void OnSyncMessageReceived();
+
+ /*
+ * Helper invoked by ContentChild::OnMessageReceived when a sync IPC message
+ * has been handled. This will be invoked on the main thread and it will
+ * set the gPendingSyncMessage flag to false.
+ */
+ static void OnSyncMessageHandled();
+
+ void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(LSObject); }
+
+ const nsString& DocumentURI() const { return mDocumentURI; }
+
+ LSRequestChild* StartRequest(nsIEventTarget* aMainEventTarget,
+ const LSRequestParams& aParams,
+ LSRequestChildCallback* aCallback);
+
+ // Storage overrides.
+ StorageType Type() const override;
+
+ bool IsForkOf(const Storage* aStorage) const override;
+
+ int64_t GetOriginQuotaUsage() const 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 EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aError) override;
+
+ bool GetHasActiveSnapshot(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);
+
+ nsresult EndExplicitSnapshotInternal();
+
+ // 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..d1545f1b11
--- /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 "nsDataHashtable.h"
+#include "nsHashKeys.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+typedef nsDataHashtable<nsCStringHashKey, LSObserver*> LSObserverHashtable;
+
+StaticAutoPtr<LSObserverHashtable> gLSObservers;
+
+} // namespace
+
+LSObserver::LSObserver(const nsACString& aOrigin)
+ : mActor(nullptr), mOrigin(aOrigin) {
+ AssertIsOnOwningThread();
+
+ if (!gLSObservers) {
+ gLSObservers = new LSObserverHashtable();
+ }
+
+ MOZ_ASSERT(!gLSObservers->Get(mOrigin));
+ gLSObservers->Put(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..d87a1b581c
--- /dev/null
+++ b/dom/localstorage/LSObserver.h
@@ -0,0 +1,70 @@
+/* -*- 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 {
+namespace 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 dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_LSObserver_h
diff --git a/dom/localstorage/LSSnapshot.cpp b/dom/localstorage/LSSnapshot.cpp
new file mode 100644
index 0000000000..6bb69c8d17
--- /dev/null
+++ b/dom/localstorage/LSSnapshot.cpp
@@ -0,0 +1,1022 @@
+/* -*- 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/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 "nsTHashtable.h"
+#include "nsTStringRepr.h"
+#include "nscore.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+const uint32_t kSnapshotTimeoutMs = 20000;
+
+} // namespace
+
+/**
+ * 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),
+ mExactUsage(0),
+ mPeakUsage(0),
+ mLoadState(LoadState::Initial),
+ mHasOtherProcessObservers(false),
+ mExplicit(false),
+ mHasPendingStableStateCallback(false),
+ mHasPendingTimerCallback(false),
+ mDirty(false)
+#ifdef DEBUG
+ ,
+ mInitialized(false),
+ mSentFinish(false)
+#endif
+{
+ AssertIsOnOwningThread();
+}
+
+LSSnapshot::~LSSnapshot() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mDatabase);
+ MOZ_ASSERT(!mHasPendingStableStateCallback);
+ MOZ_ASSERT(!mHasPendingTimerCallback);
+ 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.PutEntry(itemInfo.key());
+ }
+
+ mValues.Put(itemInfo.key(), value.AsString());
+ }
+
+ if (loadState == LoadState::Partial) {
+ if (aInitInfo.addKeyToUnknownItems()) {
+ mUnknownItems.PutEntry(aKey);
+ }
+ mInitLength = aInitInfo.totalLength();
+ mLength = mInitLength;
+ } else if (loadState == LoadState::AllOrderedKeys) {
+ mInitLength = aInitInfo.totalLength();
+ } else {
+ MOZ_ASSERT(loadState == LoadState::AllOrderedItems);
+ }
+
+ mExactUsage = aInitInfo.initialUsage();
+ mPeakUsage = aInitInfo.peakUsage();
+
+ mLoadState = aInitInfo.loadState();
+
+ mHasOtherProcessObservers = aInitInfo.hasOtherProcessObservers();
+
+ mExplicit = aExplicit;
+
+#ifdef DEBUG
+ mInitialized = true;
+#endif
+
+ if (mHasOtherProcessObservers) {
+ mWriteAndNotifyInfos = MakeUnique<nsTArray<LSWriteAndNotifyInfo>>();
+ } else {
+ mWriteOptimizer = MakeUnique<SnapshotWriteOptimizer>();
+ }
+
+ if (!mExplicit) {
+ mTimer = NS_NewTimer();
+ MOZ_ASSERT(mTimer);
+
+ 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;
+ }
+
+ for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
+ aKeys.AppendElement(iter.Key());
+ }
+
+ 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.Put(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.Put(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;
+
+ DebugOnly<nsresult> rv = UpdateUsage(-mExactUsage);
+ 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) {
+ CancelTimer();
+
+ MOZ_ALWAYS_SUCCEEDS(Checkpoint());
+
+ MOZ_ALWAYS_SUCCEEDS(Finish());
+ } else {
+ MOZ_ASSERT(!mHasPendingTimerCallback);
+ }
+}
+
+nsresult LSSnapshot::End() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mExplicit);
+ MOZ_ASSERT(!mHasPendingStableStateCallback);
+ MOZ_ASSERT(!mHasPendingTimerCallback);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ nsresult rv = Checkpoint();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<LSSnapshot> kungFuDeathGrip = this;
+
+ rv = Finish();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(!mActor->SendPing())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+void LSSnapshot::ScheduleStableStateCallback() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mTimer);
+ MOZ_ASSERT(!mExplicit);
+ MOZ_ASSERT(!mHasPendingStableStateCallback);
+
+ CancelTimer();
+
+ nsCOMPtr<nsIRunnable> runnable = this;
+ nsContentUtils::RunInStableState(runnable.forget());
+
+ mHasPendingStableStateCallback = true;
+}
+
+void LSSnapshot::MaybeScheduleStableStateCallback() {
+ AssertIsOnOwningThread();
+
+ if (!mExplicit && !mHasPendingStableStateCallback) {
+ ScheduleStableStateCallback();
+ } else {
+ MOZ_ASSERT(!mHasPendingTimerCallback);
+ }
+}
+
+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.GetEntry(aKey) || mUnknownItems.GetEntry(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.PutEntry(aKey);
+ } else {
+ mLoadedItems.PutEntry(aKey);
+ mValues.Put(aKey, result);
+
+ // mLoadedItems.Count()==mInitLength is checked below.
+ }
+
+ for (uint32_t i = 0; i < itemInfos.Length(); i++) {
+ const LSItemInfo& itemInfo = itemInfos[i];
+
+ mLoadedItems.PutEntry(itemInfo.key());
+ mValues.Put(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.Put(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.PutEntry(aKey);
+ mValues.Put(aKey, result);
+
+ // mLoadedItems.Count()==mInitLength is checked below.
+
+ for (uint32_t i = 0; i < itemInfos.Length(); i++) {
+ const LSItemInfo& itemInfo = itemInfos[i];
+
+ mLoadedItems.PutEntry(itemInfo.key());
+ mValues.Put(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.Put(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()) {
+ auto entry = mValues.LookupForAdd(aKey);
+ if (entry) {
+ result = entry.Data();
+ entry.Data() = value;
+ } else {
+ result.SetIsVoid(true);
+ entry.OrInsert([value]() { return 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;
+ }
+
+ nsDataHashtable<nsStringHashKey, nsString> newValues;
+
+ for (auto key : keys) {
+ newValues.Put(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.Put(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.Put(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 >= mExactUsage);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ int64_t newExactUsage = mExactUsage + aDelta;
+ if (newExactUsage > mPeakUsage) {
+ int64_t minSize = newExactUsage - mPeakUsage;
+ int64_t requestedSize = minSize + 4096;
+ int64_t size;
+ if (NS_WARN_IF(
+ !mActor->SendIncreasePeakUsage(requestedSize, minSize, &size))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(size >= 0);
+
+ if (size == 0) {
+ return NS_ERROR_FILE_NO_DEVICE_SPACE;
+ }
+
+ mPeakUsage += size;
+ }
+
+ mExactUsage = newExactUsage;
+ return NS_OK;
+}
+
+nsresult LSSnapshot::Checkpoint() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ if (mHasOtherProcessObservers) {
+ MOZ_ASSERT(mWriteAndNotifyInfos);
+
+ if (!mWriteAndNotifyInfos->IsEmpty()) {
+ MOZ_ALWAYS_TRUE(mActor->SendCheckpointAndNotify(*mWriteAndNotifyInfos));
+
+ mWriteAndNotifyInfos->Clear();
+ }
+ } else {
+ MOZ_ASSERT(mWriteOptimizer);
+
+ if (mWriteOptimizer->HasWrites()) {
+ nsTArray<LSWriteInfo> writeInfos;
+ mWriteOptimizer->Enumerate(writeInfos);
+
+ MOZ_ASSERT(!writeInfos.IsEmpty());
+
+ MOZ_ALWAYS_TRUE(mActor->SendCheckpoint(writeInfos));
+
+ mWriteOptimizer->Reset();
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult LSSnapshot::Finish() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mDatabase);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!mSentFinish);
+
+ MOZ_ALWAYS_TRUE(mActor->SendFinish());
+
+ 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::CancelTimer() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mTimer);
+
+ if (mHasPendingTimerCallback) {
+ MOZ_ALWAYS_SUCCEEDS(mTimer->Cancel());
+ mHasPendingTimerCallback = false;
+ }
+}
+
+// static
+void LSSnapshot::TimerCallback(nsITimer* aTimer, void* aClosure) {
+ MOZ_ASSERT(aTimer);
+
+ auto* self = static_cast<LSSnapshot*>(aClosure);
+ MOZ_ASSERT(self);
+ MOZ_ASSERT(self->mTimer);
+ MOZ_ASSERT(SameCOMIdentity(self->mTimer, aTimer));
+ MOZ_ASSERT(!self->mHasPendingStableStateCallback);
+ MOZ_ASSERT(self->mHasPendingTimerCallback);
+
+ self->mHasPendingTimerCallback = false;
+
+ MOZ_ALWAYS_SUCCEEDS(self->Finish());
+}
+
+NS_IMPL_ISUPPORTS(LSSnapshot, nsIRunnable)
+
+NS_IMETHODIMP
+LSSnapshot::Run() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mExplicit);
+ MOZ_ASSERT(mHasPendingStableStateCallback);
+ MOZ_ASSERT(!mHasPendingTimerCallback);
+
+ mHasPendingStableStateCallback = false;
+
+ MOZ_ALWAYS_SUCCEEDS(Checkpoint());
+
+ if (mDirty || !Preferences::GetBool("dom.storage.snapshot_reusing")) {
+ MOZ_ALWAYS_SUCCEEDS(Finish());
+ } else if (!mExplicit) {
+ MOZ_ASSERT(mTimer);
+
+ MOZ_ALWAYS_SUCCEEDS(mTimer->InitWithNamedFuncCallback(
+ TimerCallback, this, kSnapshotTimeoutMs, nsITimer::TYPE_ONE_SHOT,
+ "LSSnapshot::TimerCallback"));
+
+ mHasPendingTimerCallback = 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..ffc2929299
--- /dev/null
+++ b/dom/localstorage/LSSnapshot.h
@@ -0,0 +1,190 @@
+/* -*- 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 "nsDataHashtable.h"
+#include "nsHashKeys.h"
+#include "nsIRunnable.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsTArrayForwardDeclare.h"
+#include "nsTHashtable.h"
+
+class nsITimer;
+
+namespace mozilla {
+namespace 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> mTimer;
+
+ LSSnapshotChild* mActor;
+
+ nsTHashtable<nsStringHashKey> mLoadedItems;
+ nsTHashtable<nsStringHashKey> mUnknownItems;
+ nsDataHashtable<nsStringHashKey, nsString> mValues;
+ UniquePtr<SnapshotWriteOptimizer> mWriteOptimizer;
+ UniquePtr<nsTArray<LSWriteAndNotifyInfo>> mWriteAndNotifyInfos;
+
+ uint32_t mInitLength;
+ uint32_t mLength;
+ int64_t mExactUsage;
+ int64_t mPeakUsage;
+
+ LoadState mLoadState;
+
+ bool mHasOtherProcessObservers;
+ bool mExplicit;
+ bool mHasPendingStableStateCallback;
+ bool mHasPendingTimerCallback;
+ 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 End();
+
+ 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();
+
+ nsresult Finish();
+
+ void CancelTimer();
+
+ static void TimerCallback(nsITimer* aTimer, void* aClosure);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIRUNNABLE
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_LSSnapshot_h
diff --git a/dom/localstorage/LSValue.cpp b/dom/localstorage/LSValue.cpp
new file mode 100644
index 0000000000..edb43b0617
--- /dev/null
+++ b/dom/localstorage/LSValue.cpp
@@ -0,0 +1,86 @@
+/* -*- 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 "nsDebug.h"
+#include "nsError.h"
+
+namespace mozilla::dom {
+
+bool LSValue::InitFromString(const nsAString& aBuffer) {
+ MOZ_ASSERT(mBuffer.IsVoid());
+ MOZ_ASSERT(!mUTF16Length);
+ MOZ_ASSERT(!mCompressed);
+
+ if (aBuffer.IsVoid()) {
+ return true;
+ }
+
+ nsCString converted;
+ if (NS_WARN_IF(!CopyUTF16toUTF8(aBuffer, converted, fallible))) {
+ return false;
+ }
+
+ nsCString convertedAndCompressed;
+ if (NS_WARN_IF(!SnappyCompress(converted, convertedAndCompressed))) {
+ return false;
+ }
+
+ if (convertedAndCompressed.IsVoid()) {
+ mBuffer = converted;
+ mUTF16Length = aBuffer.Length();
+ } else {
+ mBuffer = convertedAndCompressed;
+ mUTF16Length = aBuffer.Length();
+ mCompressed = true;
+ }
+
+ return true;
+}
+
+nsresult LSValue::InitFromStatement(mozIStorageStatement* aStatement,
+ uint32_t aIndex) {
+ MOZ_ASSERT(aStatement);
+ MOZ_ASSERT(mBuffer.IsVoid());
+ MOZ_ASSERT(!mUTF16Length);
+ MOZ_ASSERT(!mCompressed);
+
+ nsCString buffer;
+ nsresult rv = aStatement->GetUTF8String(aIndex, buffer);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ int32_t utf16Length;
+ rv = aStatement->GetInt32(aIndex + 1, &utf16Length);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ int32_t compressed;
+ rv = aStatement->GetInt32(aIndex + 2, &compressed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mBuffer = buffer;
+ mUTF16Length = utf16Length;
+ mCompressed = compressed;
+
+ 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..1fbff5a71f
--- /dev/null
+++ b/dom/localstorage/LSValue.h
@@ -0,0 +1,122 @@
+/* -*- 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 {
+namespace 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>;
+
+ nsCString mBuffer;
+ uint32_t mUTF16Length;
+ bool mCompressed;
+
+ public:
+ LSValue() : mUTF16Length(0), mCompressed(false) { 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; }
+
+ bool IsCompressed() const { return mCompressed; }
+
+ bool Equals(const LSValue& aOther) const {
+ return mBuffer == aOther.mBuffer &&
+ mBuffer.IsVoid() == aOther.mBuffer.IsVoid() &&
+ mUTF16Length == aOther.mUTF16Length &&
+ mCompressed == aOther.mCompressed;
+ }
+
+ bool operator==(const LSValue& aOther) const { return Equals(aOther); }
+
+ bool operator!=(const LSValue& aOther) const { return !Equals(aOther); }
+
+ operator const nsCString&() const { return mBuffer; }
+
+ operator Span<const char>() const { return mBuffer; }
+
+ class Converter {
+ nsString mBuffer;
+
+ public:
+ explicit Converter(const LSValue& aValue) {
+ if (aValue.mBuffer.IsVoid()) {
+ mBuffer.SetIsVoid(true);
+ } else if (aValue.mCompressed) {
+ nsCString buffer;
+ MOZ_ALWAYS_TRUE(SnappyUncompress(aValue.mBuffer, buffer));
+ CopyUTF8toUTF16(buffer, mBuffer);
+ } else {
+ CopyUTF8toUTF16(aValue.mBuffer, mBuffer);
+ }
+ }
+ 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();
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_LSValue_h
diff --git a/dom/localstorage/LSWriteOptimizer.cpp b/dom/localstorage/LSWriteOptimizer.cpp
new file mode 100644
index 0000000000..23f88d0c78
--- /dev/null
+++ b/dom/localstorage/LSWriteOptimizer.cpp
@@ -0,0 +1,71 @@
+/* -*- 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();
+
+ WriteInfo* existingWriteInfo;
+ if (mWriteInfos.Get(aKey, &existingWriteInfo) &&
+ existingWriteInfo->GetType() == WriteInfo::InsertItem) {
+ mWriteInfos.Remove(aKey);
+ } else {
+ mWriteInfos.Put(aKey, 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 (auto iter = mWriteInfos.ConstIter(); !iter.Done(); iter.Next()) {
+ WriteInfo* writeInfo = iter.UserData();
+
+ 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..f7e0c677e1
--- /dev/null
+++ b/dom/localstorage/LSWriteOptimizer.h
@@ -0,0 +1,196 @@
+/* -*- 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 {
+namespace 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 dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_LSWriteOptimizer_h
diff --git a/dom/localstorage/LSWriteOptimizerImpl.h b/dom/localstorage/LSWriteOptimizerImpl.h
new file mode 100644
index 0000000000..99463f3657
--- /dev/null
+++ b/dom/localstorage/LSWriteOptimizerImpl.h
@@ -0,0 +1,66 @@
+/* -*- 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 {
+namespace dom {
+
+template <typename T, typename U>
+void LSWriteOptimizer<T, U>::InsertItem(const nsAString& aKey, const T& aValue,
+ int64_t aDelta) {
+ AssertIsOnOwningThread();
+
+ WriteInfo* existingWriteInfo;
+ UniquePtr<WriteInfo> newWriteInfo;
+ if (mWriteInfos.Get(aKey, &existingWriteInfo) &&
+ existingWriteInfo->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.
+
+ newWriteInfo = MakeUnique<UpdateItemInfo>(NextSerialNumber(), aKey, aValue,
+ /* aUpdateWithMove */ true);
+ } else {
+ newWriteInfo = MakeUnique<InsertItemInfo>(NextSerialNumber(), aKey, aValue);
+ }
+ mWriteInfos.Put(aKey, std::move(newWriteInfo));
+
+ mTotalDelta += aDelta;
+}
+
+template <typename T, typename U>
+void LSWriteOptimizer<T, U>::UpdateItem(const nsAString& aKey, const T& aValue,
+ int64_t aDelta) {
+ AssertIsOnOwningThread();
+
+ WriteInfo* existingWriteInfo;
+ UniquePtr<WriteInfo> newWriteInfo;
+ if (mWriteInfos.Get(aKey, &existingWriteInfo) &&
+ existingWriteInfo->GetType() == WriteInfo::InsertItem) {
+ newWriteInfo = MakeUnique<InsertItemInfo>(NextSerialNumber(), aKey, aValue);
+ } else {
+ newWriteInfo = MakeUnique<UpdateItemInfo>(NextSerialNumber(), aKey, aValue,
+ /* aUpdateWithMove */ false);
+ }
+ mWriteInfos.Put(aKey, std::move(newWriteInfo));
+
+ mTotalDelta += aDelta;
+}
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_LSWriteOptimizerImpl_h
diff --git a/dom/localstorage/LocalStorageCommon.cpp b/dom/localstorage/LocalStorageCommon.cpp
new file mode 100644
index 0000000000..038806d1db
--- /dev/null
+++ b/dom/localstorage/LocalStorageCommon.cpp
@@ -0,0 +1,155 @@
+/* -*- 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/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_next_gen_DoNotUseDirectly();
+ gNextGenLocalStorageEnabled = enabled ? 1 : 0;
+ }
+
+ return !!gNextGenLocalStorageEnabled;
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (gNextGenLocalStorageEnabled == -1) {
+ bool enabled = Preferences::GetBool("dom.storage.next_gen", false);
+ gNextGenLocalStorageEnabled = enabled ? 1 : 0;
+ }
+
+ return !!gNextGenLocalStorageEnabled;
+}
+
+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;
+ LS_TRY(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..b9c185bcaf
--- /dev/null
+++ b/dom/localstorage/LocalStorageCommon.h
@@ -0,0 +1,286 @@
+/* -*- 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 (apart from a special
+ * case when we need to cancel the request from an internal chromium IPC thread
+ * to prevent a dead lock involving CPOWs).
+ * 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.
+ */
+
+// LocalStorage equivalents of QM_TRY.
+#define LS_TRY_GLUE(...) \
+ QM_TRY_META(mozilla::dom::localstorage, MOZ_UNIQUE_VAR(tryResult), \
+ ##__VA_ARGS__)
+#define LS_TRY(...) LS_TRY_GLUE(__VA_ARGS__)
+
+// LocalStorage equivalents of QM_TRY_UNWRAP and QM_TRY_INSPECT.
+#define LS_TRY_ASSIGN_GLUE(accessFunction, ...) \
+ QM_TRY_ASSIGN_META(mozilla::dom::localstorage, MOZ_UNIQUE_VAR(tryResult), \
+ accessFunction, ##__VA_ARGS__)
+#define LS_TRY_UNWRAP(...) LS_TRY_ASSIGN_GLUE(unwrap, __VA_ARGS__)
+#define LS_TRY_INSPECT(...) LS_TRY_ASSIGN_GLUE(inspect, __VA_ARGS__)
+
+// LocalStorage equivalents of QM_TRY_RETURN.
+#define LS_TRY_RETURN_GLUE(...) \
+ QM_TRY_RETURN_META(mozilla::dom::localstorage, MOZ_UNIQUE_VAR(tryResult), \
+ ##__VA_ARGS__)
+#define LS_TRY_RETURN(...) LS_TRY_RETURN_GLUE(__VA_ARGS__)
+
+// LocalStorage equivalents of QM_FAIL.
+#define LS_FAIL_GLUE(...) \
+ QM_FAIL_META(mozilla::dom::localstorage, ##__VA_ARGS__)
+#define LS_FAIL(...) LS_FAIL_GLUE(__VA_ARGS__)
+
+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 the main thread only in a content process.
+ */
+bool NextGenLocalStorageEnabled();
+
+/**
+ * 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 localstorage {
+
+QM_META_HANDLE_ERROR("LocalStorage"_ns)
+
+} // namespace localstorage
+
+} // 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..348927d708
--- /dev/null
+++ b/dom/localstorage/LocalStorageManager2.cpp
@@ -0,0 +1,587 @@
+/* -*- 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);
+
+ // 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;
+}
+
+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_FAILURE;
+ }
+
+ 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);
+}
+
+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;
+
+ 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..ec9be788e4
--- /dev/null
+++ b/dom/localstorage/LocalStorageManager2.h
@@ -0,0 +1,70 @@
+/* -*- 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 {
+namespace 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 dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_LocalStorageManager2_h
diff --git a/dom/localstorage/PBackgroundLSDatabase.ipdl b/dom/localstorage/PBackgroundLSDatabase.ipdl
new file mode 100644
index 0000000000..3305b503b7
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSDatabase.ipdl
@@ -0,0 +1,160 @@
+/* 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 initialUsage;
+ /**
+ * The amount of storage allowed to be used by the Snapshot without requesting
+ * more storage space via IncreasePeakUsage. This is the `initialUsage` plus
+ * 0 or more bytes of space. If space was available, the increase will be the
+ * `requestedSize` from the PBackgroundLSSnapshot constructor. If the
+ * LocalStorage usage was already close to the limit, then the fallback is the
+ * `minSize` requested, or 0 if there wasn't space for that.
+ */
+ int64_t peakUsage;
+ // See `LSSnapshot::LoadState` in `LSSnapshot.h`
+ LoadState loadState;
+ /**
+ * 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.)
+ */
+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 attempt to pre-allocate some amount of quota
+ * usage to the Snapshot.
+ */
+ sync PBackgroundLSSnapshot(nsString documentURI,
+ nsString key,
+ bool increasePeakUsage,
+ int64_t requestedSize,
+ 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..ed6b8ca560
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSObserver.ipdl
@@ -0,0 +1,62 @@
+/* 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.
+ */
+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..4985c09d0f
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSRequest.ipdl
@@ -0,0 +1,102 @@
+/* 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.
+ */
+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..e460ebaae0
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSSharedTypes.ipdlh
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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;
+};
+
+struct LSRequestPrepareObserverParams
+{
+ PrincipalInfo principalInfo;
+ PrincipalInfo storagePrincipalInfo;
+ nsID? clientId;
+};
+
+union LSRequestParams
+{
+ LSRequestPreloadDatastoreParams;
+ LSRequestPrepareDatastoreParams;
+ LSRequestPrepareObserverParams;
+};
+
+struct LSSimpleRequestPreloadedParams
+{
+ PrincipalInfo principalInfo;
+ PrincipalInfo storagePrincipalInfo;
+};
+
+union LSSimpleRequestParams
+{
+ LSSimpleRequestPreloadedParams;
+};
+
+/**
+ * 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..c4bdd8c277
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSSimpleRequest.ipdl
@@ -0,0 +1,50 @@
+/* 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;
+
+namespace mozilla {
+namespace dom {
+
+/**
+ * Response to a `LSSimpleRequestPreloadedParams` request indicating whether the
+ * origin was preloaded.
+ */
+struct LSSimpleRequestPreloadedResponse
+{
+ bool preloaded;
+};
+
+/**
+ * Discriminated union which can contain an error code (`nsresult`) or
+ * particular simple request response.
+ */
+union LSSimpleRequestResponse
+{
+ nsresult;
+ LSSimpleRequestPreloadedResponse;
+};
+
+/**
+ * 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`.
+ */
+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..48e7136adc
--- /dev/null
+++ b/dom/localstorage/PBackgroundLSSnapshot.ipdl
@@ -0,0 +1,145 @@
+/* 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;
+};
+
+sync protocol PBackgroundLSSnapshot
+{
+ manager PBackgroundLSDatabase;
+
+parent:
+ async DeleteMe();
+
+ async Checkpoint(LSWriteInfo[] writeInfos);
+
+ async CheckpointAndNotify(LSWriteAndNotifyInfo[] writeAndNotifyInfos);
+
+ async Finish();
+
+ 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 requestedSize, int64_t minSize)
+ returns (int64_t size);
+
+ // A synchronous ping to the parent actor to confirm that the parent actor
+ // has received previous async message. This should only be used by the
+ // snapshotting code to end an explicit snapshot.
+ sync Ping();
+
+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..7d6b534437
--- /dev/null
+++ b/dom/localstorage/ReportInternalError.cpp
@@ -0,0 +1,30 @@
+/* -*- 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", 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..e72dbcd5e7
--- /dev/null
+++ b/dom/localstorage/ReportInternalError.h
@@ -0,0 +1,33 @@
+/* -*- 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 {
+namespace dom {
+namespace localstorage {
+
+MOZ_COLD void ReportInternalError(const char* aFile, uint32_t aLine,
+ const char* aStr);
+
+} // namespace localstorage
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_ReportInternalError_h
diff --git a/dom/localstorage/SerializationHelpers.h b/dom/localstorage/SerializationHelpers.h
new file mode 100644
index 0000000000..2d2bd93520
--- /dev/null
+++ b/dom/localstorage/SerializationHelpers.h
@@ -0,0 +1,51 @@
+/* -*- 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> {
+ typedef mozilla::dom::LSValue paramType;
+
+ static void Write(Message* aMsg, const paramType& aParam) {
+ WriteParam(aMsg, aParam.mBuffer);
+ WriteParam(aMsg, aParam.mUTF16Length);
+ WriteParam(aMsg, aParam.mCompressed);
+ }
+
+ static bool Read(const Message* aMsg, PickleIterator* aIter,
+ paramType* aResult) {
+ return ReadParam(aMsg, aIter, &aResult->mBuffer) &&
+ ReadParam(aMsg, aIter, &aResult->mUTF16Length) &&
+ ReadParam(aMsg, aIter, &aResult->mCompressed);
+ }
+
+ static void Log(const paramType& aParam, std::wstring* aLog) {
+ LogParam(aParam.mBuffer, aLog);
+ LogParam(aParam.mUTF16Length, aLog);
+ LogParam(aParam.mCompressed, aLog);
+ }
+};
+
+} // 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..61a2e3b9b8
--- /dev/null
+++ b/dom/localstorage/SnappyUtils.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 "SnappyUtils.h"
+
+#include <stddef.h>
+#include "mozilla/Assertions.h"
+#include "mozilla/fallible.h"
+#include "nsDebug.h"
+#include "nsString.h"
+#include "snappy/snappy.h"
+
+namespace mozilla::dom {
+
+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();
+
+ size_t compressedLength = aSource.Length();
+
+ size_t uncompressedLength;
+ if (!snappy::GetUncompressedLength(compressed, compressedLength,
+ &uncompressedLength)) {
+ return false;
+ }
+
+ aDest.SetLength(uncompressedLength);
+
+ 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..7b9a79e42a
--- /dev/null
+++ b/dom/localstorage/SnappyUtils.h
@@ -0,0 +1,22 @@
+/* -*- 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 {
+namespace dom {
+
+bool SnappyCompress(const nsACString& aSource, nsACString& aDest);
+
+bool SnappyUncompress(const nsACString& aSource, nsACString& aDest);
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_SnappyUtils_h
diff --git a/dom/localstorage/moz.build b/dom/localstorage/moz.build
new file mode 100644
index 0000000000..f86139ad11
--- /dev/null
+++ b/dom/localstorage/moz.build
@@ -0,0 +1,78 @@
+# -*- 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"
+
+if CONFIG["GNU_CXX"]:
+ CXXFLAGS += ["-Wno-error=shadow"]
+
+LOCAL_INCLUDES += [
+ "/dom/file/ipc",
+]
diff --git a/dom/localstorage/nsILocalStorageManager.idl b/dom/localstorage/nsILocalStorageManager.idl
new file mode 100644
index 0000000000..f31f5183dd
--- /dev/null
+++ b/dom/localstorage/nsILocalStorageManager.idl
@@ -0,0 +1,35 @@
+/* -*- 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);
+};
diff --git a/dom/localstorage/test/.eslintrc.js b/dom/localstorage/test/.eslintrc.js
new file mode 100644
index 0000000000..735f687ed1
--- /dev/null
+++ b/dom/localstorage/test/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/mochitest-test", "plugin:mozilla/browser-test"],
+};
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..69281b09df
--- /dev/null
+++ b/dom/localstorage/test/helpers.js
@@ -0,0 +1,86 @@
+/**
+ * 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());
+
+ ok(typeof testSteps === "function", "There should be a testSteps function");
+ ok(
+ testSteps.constructor.name === "AsyncFunction",
+ "testSteps should be an async function"
+ );
+
+ SimpleTest.registerCleanupFunction(async function() {
+ await requestFinished(clearAllDatabases());
+ });
+
+ add_task(testSteps);
+ };
+}
+
+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..03c6114ae9
--- /dev/null
+++ b/dom/localstorage/test/unit/databaseShadowing-shared.js
@@ -0,0 +1,110 @@
+/* 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 } },
+];
+
+function enableNextGenLocalStorage() {
+ info("Setting pref");
+
+ Services.prefs.setBoolPref("dom.storage.next_gen", true);
+}
+
+function disableNextGenLocalStorage() {
+ info("Setting pref");
+
+ Services.prefs.setBoolPref("dom.storage.next_gen", false);
+}
+
+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");
+
+ 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) {
+ 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");
+ } else {
+ ok(storage.getItem("key0") == null, "Correct value");
+ ok(storage.getItem("key1") == null, "Correct value");
+ ok(storage.getItem("key2") == "value2", "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..7c42b82e9a
--- /dev/null
+++ b/dom/localstorage/test/unit/head.js
@@ -0,0 +1,335 @@
+/**
+ * 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;
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function is(a, b, msg) {
+ Assert.equal(a, b, msg);
+}
+
+function ok(cond, msg) {
+ Assert.ok(!!cond, msg);
+}
+
+function run_test() {
+ runTest();
+}
+
+if (!this.runTest) {
+ this.runTest = function() {
+ do_get_profile();
+
+ enableTesting();
+
+ Cu.importGlobalProperties(["crypto"]);
+
+ Assert.ok(
+ typeof testSteps === "function",
+ "There should be a testSteps function"
+ );
+ Assert.ok(
+ testSteps.constructor.name === "AsyncFunction",
+ "testSteps should be an async function"
+ );
+
+ registerCleanupFunction(resetTesting);
+
+ add_task(testSteps);
+
+ // Since we defined run_test, we must invoke run_next_test() to start the
+ // async test.
+ run_next_test();
+ };
+}
+
+function returnToEventLoop() {
+ return new Promise(function(resolve) {
+ executeSoon(resolve);
+ });
+}
+
+function enableTesting() {
+ 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");
+}
+
+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 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 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/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/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..5b3f118cef
--- /dev/null
+++ b/dom/localstorage/test/unit/test_archive.js
@@ -0,0 +1,75 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+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);
+
+ // Profile 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();
+
+ // Profile 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();
+
+ // Profile 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..1725315f05
--- /dev/null
+++ b/dom/localstorage/test/unit/test_clientValidation.js
@@ -0,0 +1,29 @@
+/**
+ * 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.
+ */
+async function testSteps() {
+ const principal = getPrincipal("http://example.com");
+
+ info("Setting prefs");
+
+ Services.prefs.setBoolPref("dom.storage.next_gen", true);
+ 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..6c28957305
--- /dev/null
+++ b/dom/localstorage/test/unit/test_corruptedDatabase.js
@@ -0,0 +1,70 @@
+/**
+ * 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");
+}
+
+async function testSteps() {
+ info("Setting pref");
+
+ Services.prefs.setBoolPref("dom.storage.next_gen", true);
+
+ // 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..12c4a646e4
--- /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");
+
+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..476de87d26
--- /dev/null
+++ b/dom/localstorage/test/unit/test_databaseShadowing2.js
@@ -0,0 +1,19 @@
+/**
+ * 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");
+
+async function testSteps() {
+ // The shadow database was prepared in test_databaseShadowing1.js
+
+ disableNextGenLocalStorage();
+
+ if (!importShadowDatabase("shadowdb.sqlite")) {
+ return;
+ }
+
+ verifyData([]);
+}
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..e71e5d2b78
--- /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");
+
+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..8f6680fca9
--- /dev/null
+++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js
@@ -0,0 +1,19 @@
+/**
+ * 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");
+
+async function testSteps() {
+ // The shadow database was prepared in test_databaseShadowing_clearOrigin1.js
+
+ disableNextGenLocalStorage();
+
+ if (!importShadowDatabase("shadowdb-clearedOrigin.sqlite")) {
+ return;
+ }
+
+ verifyData([1]);
+}
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..961da5aaf7
--- /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");
+
+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..1726792f1e
--- /dev/null
+++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js
@@ -0,0 +1,20 @@
+/**
+ * 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");
+
+async function testSteps() {
+ // The shadow database was prepared in
+ // test_databaseShadowing_clearOriginsByPattern1.js
+
+ disableNextGenLocalStorage();
+
+ if (!importShadowDatabase("shadowdb-clearedOriginsByPattern.sqlite")) {
+ return;
+ }
+
+ verifyData([4, 5, 6]);
+}
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..6680fda937
--- /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");
+
+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..bea8e1bbb2
--- /dev/null
+++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js
@@ -0,0 +1,20 @@
+/**
+ * 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");
+
+async function testSteps() {
+ // The shadow database was prepared in
+ // test_databaseShadowing_clearOriginsByPrefix1.js
+
+ disableNextGenLocalStorage();
+
+ if (!importShadowDatabase("shadowdb-clearedOriginsByPrefix.sqlite")) {
+ return;
+ }
+
+ verifyData([2, 3]);
+}
diff --git a/dom/localstorage/test/unit/test_eviction.js b/dom/localstorage/test/unit/test_eviction.js
new file mode 100644
index 0000000000..0ed44f3927
--- /dev/null
+++ b/dom/localstorage/test/unit/test_eviction.js
@@ -0,0 +1,88 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+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.next_gen", true);
+ 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(ex instanceof DOMException, "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..2f7d631722
--- /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.
+ */
+
+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..6036c24717
--- /dev/null
+++ b/dom/localstorage/test/unit/test_groupLimit.js
@@ -0,0 +1,82 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+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.next_gen", true);
+ 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(ex instanceof DOMException, "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..ba0fe6f6ad
--- /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.
+ */
+
+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..f274724a88
--- /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.
+ */
+
+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.hasActiveSnapshot, "Snapshot successfully finished");
+}
diff --git a/dom/localstorage/test/unit/test_migration.js b/dom/localstorage/test/unit/test_migration.js
new file mode 100644
index 0000000000..fd21ffe282
--- /dev/null
+++ b/dom/localstorage/test/unit/test_migration.js
@@ -0,0 +1,124 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+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.next_gen", true);
+
+ 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_orderingAfterRemoveAdd.js b/dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js
new file mode 100644
index 0000000000..d9c1ec914a
--- /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/
+ */
+
+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..0980eafc2b
--- /dev/null
+++ b/dom/localstorage/test/unit/test_originInit.js
@@ -0,0 +1,302 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+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 = "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);
+ }
+
+ function removeFile(file) {
+ file.remove(false);
+ }
+
+ function createEmptyFile(file) {
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0644", 8));
+ }
+
+ 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 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);
+ }
+
+ info("Setting prefs");
+
+ Services.prefs.setBoolPref("dom.storage.next_gen", true);
+
+ 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();
+}
diff --git a/dom/localstorage/test/unit/test_preloading.js b/dom/localstorage/test/unit/test_preloading.js
new file mode 100644
index 0000000000..bf7bad6ae3
--- /dev/null
+++ b/dom/localstorage/test/unit/test_preloading.js
@@ -0,0 +1,81 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+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.next_gen", true);
+ 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..7a219ec9c4
--- /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/
+ */
+
+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_snapshotting.js b/dom/localstorage/test/unit/test_snapshotting.js
new file mode 100644
index 0000000000..d29cbf020c
--- /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/
+ */
+
+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..a43673682e
--- /dev/null
+++ b/dom/localstorage/test/unit/test_stringLength.js
@@ -0,0 +1,71 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+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.next_gen", true);
+
+ 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..830890fff7
--- /dev/null
+++ b/dom/localstorage/test/unit/test_stringLength2.js
@@ -0,0 +1,76 @@
+/**
+ * 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.
+ */
+
+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.next_gen", true);
+
+ 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_uri_encoding_edge_cases.js b/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js
new file mode 100644
index 0000000000..c711b7124b
--- /dev/null
+++ b/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js
@@ -0,0 +1,66 @@
+/**
+ * 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.
+ */
+
+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.next_gen", true);
+
+ 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..f910760ab4
--- /dev/null
+++ b/dom/localstorage/test/unit/test_usage.js
@@ -0,0 +1,66 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+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.next_gen", true);
+
+ 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..71be71abf4
--- /dev/null
+++ b/dom/localstorage/test/unit/test_usageAfterMigration.js
@@ -0,0 +1,161 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+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.next_gen", true);
+
+ 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..0a53f60156
--- /dev/null
+++ b/dom/localstorage/test/unit/xpcshell.ini
@@ -0,0 +1,54 @@
+# 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
+support-files =
+ archive_profile.zip
+ corruptedDatabase_profile.zip
+ corruptedDatabase_missingUsageFile_profile.zip
+ groupMismatch_profile.zip
+ migration_profile.zip
+ schema3upgrade_profile.zip
+ stringLength2_profile.zip
+ stringLength_profile.zip
+ usageAfterMigration_profile.zip
+
+[test_archive.js]
+[test_clientValidation.js]
+[test_corruptedDatabase.js]
+[test_databaseShadowing1.js]
+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]
+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]
+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]
+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_migration.js]
+[test_orderingAfterRemoveAdd.js]
+[test_originInit.js]
+[test_preloading.js]
+[test_schema3upgrade.js]
+[test_snapshotting.js]
+skip-if = tsan # Unreasonably slow, bug 1612707
+requesttimeoutfactor = 4
+[test_stringLength.js]
+[test_stringLength2.js]
+[test_uri_encoding_edge_cases.js]
+[test_usage.js]
+[test_usageAfterMigration.js]