diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /dom/localstorage | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/localstorage')
90 files changed, 19458 insertions, 0 deletions
diff --git a/dom/localstorage/ActorsChild.cpp b/dom/localstorage/ActorsChild.cpp new file mode 100644 index 0000000000..ea7c589a66 --- /dev/null +++ b/dom/localstorage/ActorsChild.cpp @@ -0,0 +1,325 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ActorsChild.h" + +#include "ErrorList.h" +#include "LSDatabase.h" +#include "LSObserver.h" +#include "LSSnapshot.h" +#include "LocalStorageCommon.h" +#include "mozilla/Assertions.h" +#include "mozilla/Result.h" +#include "mozilla/dom/LSValue.h" +#include "mozilla/dom/Storage.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "nsCOMPtr.h" + +namespace mozilla::dom { + +/******************************************************************************* + * LSDatabaseChild + ******************************************************************************/ + +LSDatabaseChild::LSDatabaseChild(LSDatabase* aDatabase) : mDatabase(aDatabase) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aDatabase); + + MOZ_COUNT_CTOR(LSDatabaseChild); +} + +LSDatabaseChild::~LSDatabaseChild() { + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(LSDatabaseChild); +} + +void LSDatabaseChild::SendDeleteMeInternal() { + AssertIsOnOwningThread(); + + if (mDatabase) { + mDatabase->ClearActor(); + mDatabase = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundLSDatabaseChild::SendDeleteMe()); + } +} + +void LSDatabaseChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + if (mDatabase) { + mDatabase->ClearActor(); +#ifdef DEBUG + mDatabase = nullptr; +#endif + } +} + +mozilla::ipc::IPCResult LSDatabaseChild::RecvRequestAllowToClose() { + AssertIsOnOwningThread(); + + if (mDatabase) { + mDatabase->RequestAllowToClose(); + + // TODO: A new datastore will be prepared at first LocalStorage API + // synchronous call. It would be better to start preparing a new + // datastore right here, but asynchronously. + // However, we probably shouldn't do that if we are shutting down. + } + + return IPC_OK(); +} + +PBackgroundLSSnapshotChild* LSDatabaseChild::AllocPBackgroundLSSnapshotChild( + const nsAString& aDocumentURI, const nsAString& aKey, + const bool& aIncreasePeakUsage, const int64_t& aMinSize, + LSSnapshotInitInfo* aInitInfo) { + MOZ_CRASH("PBackgroundLSSnapshotChild actor should be manually constructed!"); +} + +bool LSDatabaseChild::DeallocPBackgroundLSSnapshotChild( + PBackgroundLSSnapshotChild* aActor) { + MOZ_ASSERT(aActor); + + delete aActor; + return true; +} + +/******************************************************************************* + * LSObserverChild + ******************************************************************************/ + +LSObserverChild::LSObserverChild(LSObserver* aObserver) : mObserver(aObserver) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObserver); + + MOZ_COUNT_CTOR(LSObserverChild); +} + +LSObserverChild::~LSObserverChild() { + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(LSObserverChild); +} + +void LSObserverChild::SendDeleteMeInternal() { + AssertIsOnOwningThread(); + + if (mObserver) { + mObserver->ClearActor(); + mObserver = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundLSObserverChild::SendDeleteMe()); + } +} + +void LSObserverChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + if (mObserver) { + mObserver->ClearActor(); +#ifdef DEBUG + mObserver = nullptr; +#endif + } +} + +mozilla::ipc::IPCResult LSObserverChild::RecvObserve( + const PrincipalInfo& aPrincipalInfo, const uint32_t& aPrivateBrowsingId, + const nsAString& aDocumentURI, const nsAString& aKey, + const LSValue& aOldValue, const LSValue& aNewValue) { + AssertIsOnOwningThread(); + + if (!mObserver) { + return IPC_OK(); + } + + QM_TRY_INSPECT(const auto& principal, + PrincipalInfoToPrincipal(aPrincipalInfo), + IPC_FAIL(this, "PrincipalInfoToPrincipal failed!")); + + Storage::NotifyChange(/* aStorage */ nullptr, principal, aKey, + aOldValue.AsString(), aNewValue.AsString(), + /* aStorageType */ kLocalStorageType, aDocumentURI, + /* aIsPrivate */ !!aPrivateBrowsingId, + /* aImmediateDispatch */ true); + + return IPC_OK(); +} + +/******************************************************************************* + * LocalStorageRequestChild + ******************************************************************************/ + +LSRequestChild::LSRequestChild() : mFinishing(false) { + AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(LSRequestChild); +} + +LSRequestChild::~LSRequestChild() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mCallback); + + MOZ_COUNT_DTOR(LSRequestChild); +} + +void LSRequestChild::SetCallback(LSRequestChildCallback* aCallback) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aCallback); + MOZ_ASSERT(!mCallback); + MOZ_ASSERT(Manager()); + + mCallback = aCallback; +} + +bool LSRequestChild::Finishing() const { + AssertIsOnOwningThread(); + + return mFinishing; +} + +void LSRequestChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + if (mCallback) { + MOZ_ASSERT(aWhy != Deletion); + + mCallback->OnResponse(NS_ERROR_FAILURE); + + mCallback = nullptr; + } +} + +mozilla::ipc::IPCResult LSRequestChild::Recv__delete__( + const LSRequestResponse& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mCallback); + + mCallback->OnResponse(aResponse); + + mCallback = nullptr; + + return IPC_OK(); +} + +mozilla::ipc::IPCResult LSRequestChild::RecvReady() { + AssertIsOnOwningThread(); + + mFinishing = true; + + // We only expect this to return false if the channel has been closed, but + // PBackground's channel never gets shutdown. + MOZ_ALWAYS_TRUE(SendFinish()); + + return IPC_OK(); +} + +/******************************************************************************* + * LSSimpleRequestChild + ******************************************************************************/ + +LSSimpleRequestChild::LSSimpleRequestChild() { + AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(LSSimpleRequestChild); +} + +LSSimpleRequestChild::~LSSimpleRequestChild() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mCallback); + + MOZ_COUNT_DTOR(LSSimpleRequestChild); +} + +void LSSimpleRequestChild::SetCallback( + LSSimpleRequestChildCallback* aCallback) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aCallback); + MOZ_ASSERT(!mCallback); + MOZ_ASSERT(Manager()); + + mCallback = aCallback; +} + +void LSSimpleRequestChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + if (mCallback) { + MOZ_ASSERT(aWhy != Deletion); + + mCallback->OnResponse(NS_ERROR_FAILURE); + + mCallback = nullptr; + } +} + +mozilla::ipc::IPCResult LSSimpleRequestChild::Recv__delete__( + const LSSimpleRequestResponse& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mCallback); + + mCallback->OnResponse(aResponse); + + mCallback = nullptr; + + return IPC_OK(); +} + +/******************************************************************************* + * LSSnapshotChild + ******************************************************************************/ + +LSSnapshotChild::LSSnapshotChild(LSSnapshot* aSnapshot) : mSnapshot(aSnapshot) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aSnapshot); + + MOZ_COUNT_CTOR(LSSnapshotChild); +} + +LSSnapshotChild::~LSSnapshotChild() { + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(LSSnapshotChild); +} + +void LSSnapshotChild::SendDeleteMeInternal() { + AssertIsOnOwningThread(); + + if (mSnapshot) { + mSnapshot->ClearActor(); + mSnapshot = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundLSSnapshotChild::SendDeleteMe()); + } +} + +void LSSnapshotChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + if (mSnapshot) { + mSnapshot->ClearActor(); +#ifdef DEBUG + mSnapshot = nullptr; +#endif + } +} + +mozilla::ipc::IPCResult LSSnapshotChild::RecvMarkDirty() { + AssertIsOnOwningThread(); + + if (!mSnapshot) { + return IPC_OK(); + } + + mSnapshot->MarkDirty(); + + return IPC_OK(); +} + +} // namespace mozilla::dom diff --git a/dom/localstorage/ActorsChild.h b/dom/localstorage/ActorsChild.h new file mode 100644 index 0000000000..d9b614e401 --- /dev/null +++ b/dom/localstorage/ActorsChild.h @@ -0,0 +1,283 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_ActorsChild_h +#define mozilla_dom_localstorage_ActorsChild_h + +#include <cstdint> +#include "mozilla/RefPtr.h" +#include "mozilla/dom/PBackgroundLSDatabaseChild.h" +#include "mozilla/dom/PBackgroundLSObserverChild.h" +#include "mozilla/dom/PBackgroundLSRequest.h" +#include "mozilla/dom/PBackgroundLSRequestChild.h" +#include "mozilla/dom/PBackgroundLSSimpleRequest.h" +#include "mozilla/dom/PBackgroundLSSimpleRequestChild.h" +#include "mozilla/dom/PBackgroundLSSnapshotChild.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nscore.h" + +namespace mozilla { + +namespace ipc { + +class BackgroundChildImpl; + +} // namespace ipc + +namespace dom { + +class LocalStorageManager2; +class LSDatabase; +class LSObject; +class LSObserver; +class LSRequestChildCallback; +class LSSimpleRequestChildCallback; +class LSSnapshot; + +/** + * Minimal glue actor with standard IPC-managed new/delete existence that exists + * primarily to track the continued existence of the LSDatabase in the child. + * Most of the interesting bits happen via PBackgroundLSSnapshot. + * + * Mutual raw pointers are maintained between LSDatabase and this class that are + * cleared at either (expected) when the child starts the deletion process + * (SendDeleteMeInternal) or unexpected actor death (ActorDestroy). + * + * See `PBackgroundLSDatabase.ipdl` for more information. + * + * + * ## Low-Level Lifecycle ## + * - Created by LSObject::EnsureDatabase if it had to create a database. + * - Deletion begun by LSDatabase's destructor invoking SendDeleteMeInternal + * which will result in the parent sending __delete__ which destroys the + * actor. + */ +class LSDatabaseChild final : public PBackgroundLSDatabaseChild { + friend class mozilla::ipc::BackgroundChildImpl; + friend class LSDatabase; + friend class LSObject; + + LSDatabase* mDatabase; + + NS_DECL_OWNINGTHREAD + + public: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(LSDatabaseChild); + } + + private: + // Only created by LSObject. + explicit LSDatabaseChild(LSDatabase* aDatabase); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~LSDatabaseChild(); + + void SendDeleteMeInternal(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvRequestAllowToClose() override; + + PBackgroundLSSnapshotChild* AllocPBackgroundLSSnapshotChild( + const nsAString& aDocumentURI, const nsAString& aKey, + const bool& aIncreasePeakUsage, const int64_t& aMinSize, + LSSnapshotInitInfo* aInitInfo) override; + + bool DeallocPBackgroundLSSnapshotChild( + PBackgroundLSSnapshotChild* aActor) override; +}; + +/** + * Minimal IPC-managed (new/delete) actor that exists to receive and relay + * "storage" events from changes to LocalStorage that take place in other + * processes as their Snapshots are checkpointed to the canonical Datastore in + * the parent process. + * + * See `PBackgroundLSObserver.ipdl` for more info. + */ +class LSObserverChild final : public PBackgroundLSObserverChild { + friend class mozilla::ipc::BackgroundChildImpl; + friend class LSObserver; + friend class LSObject; + + LSObserver* mObserver; + + NS_DECL_OWNINGTHREAD + + public: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(LSObserverChild); + } + + private: + // Only created by LSObject. + explicit LSObserverChild(LSObserver* aObserver); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~LSObserverChild(); + + void SendDeleteMeInternal(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvObserve(const PrincipalInfo& aPrinciplaInfo, + const uint32_t& aPrivateBrowsingId, + const nsAString& aDocumentURI, + const nsAString& aKey, + const LSValue& aOldValue, + const LSValue& aNewValue) override; +}; + +/** + * Minimal glue IPC-managed (new/delete) actor that is used by LSObject and its + * RequestHelper to perform synchronous requests on top of an asynchronous + * protocol. + * + * Takes an `LSReuestChildCallback` to be invoked when a response is received + * via __delete__. + * + * See `PBackgroundLSRequest.ipdl`, `LSObject`, and `RequestHelper` for more + * info. + */ +class LSRequestChild final : public PBackgroundLSRequestChild { + friend class LSObject; + friend class LocalStorageManager2; + + RefPtr<LSRequestChildCallback> mCallback; + + bool mFinishing; + + NS_DECL_OWNINGTHREAD + + public: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(LSReqeustChild); + } + + bool Finishing() const; + + private: + // Only created by LSObject. + LSRequestChild(); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~LSRequestChild(); + + void SetCallback(LSRequestChildCallback* aCallback); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult Recv__delete__( + const LSRequestResponse& aResponse) override; + + mozilla::ipc::IPCResult RecvReady() override; +}; + +class NS_NO_VTABLE LSRequestChildCallback { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void OnResponse(const LSRequestResponse& aResponse) = 0; + + protected: + virtual ~LSRequestChildCallback() = default; +}; + +/** + * Minimal glue IPC-managed (new/delete) actor used by `LocalStorageManager2` to + * issue asynchronous requests in an asynchronous fashion. + * + * Takes an `LSSimpleRequestChildCallback` to be invoked when a response is + * received via __delete__. + * + * See `PBackgroundLSSimpleRequest.ipdl` for more info. + */ +class LSSimpleRequestChild final : public PBackgroundLSSimpleRequestChild { + friend class LocalStorageManager2; + + RefPtr<LSSimpleRequestChildCallback> mCallback; + + NS_DECL_OWNINGTHREAD + + public: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(LSSimpleReqeustChild); + } + + private: + // Only created by LocalStorageManager2. + LSSimpleRequestChild(); + + void SetCallback(LSSimpleRequestChildCallback* aCallback); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~LSSimpleRequestChild(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult Recv__delete__( + const LSSimpleRequestResponse& aResponse) override; +}; + +class NS_NO_VTABLE LSSimpleRequestChildCallback { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void OnResponse(const LSSimpleRequestResponse& aResponse) = 0; + + protected: + virtual ~LSSimpleRequestChildCallback() = default; +}; + +/** + * Minimal IPC-managed (new/delete) actor that lasts as long as its owning + * LSSnapshot. + * + * Mutual raw pointers are maintained between LSSnapshot and this class that are + * cleared at either (expected) when the child starts the deletion process + * (SendDeleteMeInternal) or unexpected actor death (ActorDestroy). + * + * See `PBackgroundLSSnapshot.ipdl` and `LSSnapshot` for more info. + */ +class LSSnapshotChild final : public PBackgroundLSSnapshotChild { + friend class LSDatabase; + friend class LSSnapshot; + + LSSnapshot* mSnapshot; + + NS_DECL_OWNINGTHREAD + + public: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(LSSnapshotChild); + } + + private: + // Only created by LSDatabase. + explicit LSSnapshotChild(LSSnapshot* aSnapshot); + + // Only destroyed by LSDatabaseChild. + ~LSSnapshotChild(); + + void SendDeleteMeInternal(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvMarkDirty() override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_ActorsChild_h diff --git a/dom/localstorage/ActorsParent.cpp b/dom/localstorage/ActorsParent.cpp new file mode 100644 index 0000000000..c4040fec1e --- /dev/null +++ b/dom/localstorage/ActorsParent.cpp @@ -0,0 +1,9144 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ActorsParent.h" + +// Local includes +#include "LSInitializationTypes.h" +#include "LSObject.h" +#include "ReportInternalError.h" + +// Global includes +#include <cinttypes> +#include <cstdlib> +#include <cstring> +#include <new> +#include <tuple> +#include <type_traits> +#include <utility> +#include "ErrorList.h" +#include "MainThreadUtils.h" +#include "mozIStorageAsyncConnection.h" +#include "mozIStorageConnection.h" +#include "mozIStorageFunction.h" +#include "mozIStorageService.h" +#include "mozIStorageStatement.h" +#include "mozIStorageValueArray.h" +#include "mozStorageCID.h" +#include "mozStorageHelper.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Logging.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Maybe.h" +#include "mozilla/Monitor.h" +#include "mozilla/Mutex.h" +#include "mozilla/NotNull.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/Preferences.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/Utf8.h" +#include "mozilla/Variant.h" +#include "mozilla/dom/ClientManagerService.h" +#include "mozilla/dom/FlippedOnce.h" +#include "mozilla/dom/LSSnapshot.h" +#include "mozilla/dom/LSValue.h" +#include "mozilla/dom/LSWriteOptimizer.h" +#include "mozilla/dom/LSWriteOptimizerImpl.h" +#include "mozilla/dom/LocalStorageCommon.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/PBackgroundLSDatabase.h" +#include "mozilla/dom/PBackgroundLSDatabaseParent.h" +#include "mozilla/dom/PBackgroundLSObserverParent.h" +#include "mozilla/dom/PBackgroundLSRequestParent.h" +#include "mozilla/dom/PBackgroundLSSharedTypes.h" +#include "mozilla/dom/PBackgroundLSSimpleRequestParent.h" +#include "mozilla/dom/PBackgroundLSSnapshotParent.h" +#include "mozilla/dom/SnappyUtils.h" +#include "mozilla/dom/StorageDBUpdater.h" +#include "mozilla/dom/StorageUtils.h" +#include "mozilla/dom/ipc/IdType.h" +#include "mozilla/dom/quota/CachingDatabaseConnection.h" +#include "mozilla/dom/quota/CheckedUnsafePtr.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/ClientImpl.h" +#include "mozilla/dom/quota/DirectoryLock.h" +#include "mozilla/dom/quota/FirstInitializationAttemptsImpl.h" +#include "mozilla/dom/quota/OriginScope.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/StorageHelpers.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/UsageInfo.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/storage/Variant.h" +#include "nsBaseHashtable.h" +#include "nsCOMPtr.h" +#include "nsClassHashtable.h" +#include "nsTHashMap.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsHashKeys.h" +#include "nsIBinaryInputStream.h" +#include "nsIBinaryOutputStream.h" +#include "nsIDirectoryEnumerator.h" +#include "nsIEventTarget.h" +#include "nsIFile.h" +#include "nsIInputStream.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIOutputStream.h" +#include "nsIRunnable.h" +#include "nsISerialEventTarget.h" +#include "nsISupports.h" +#include "nsIThread.h" +#include "nsITimer.h" +#include "nsIVariant.h" +#include "nsInterfaceHashtable.h" +#include "nsLiteralString.h" +#include "nsNetUtil.h" +#include "nsPointerHashKeys.h" +#include "nsPrintfCString.h" +#include "nsRefPtrHashtable.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsStringFlags.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsTHashSet.h" +#include "nsTLiteralString.h" +#include "nsTStringRepr.h" +#include "nsThreadUtils.h" +#include "nsVariant.h" +#include "nsXPCOM.h" +#include "nsXULAppAPI.h" +#include "nscore.h" +#include "prenv.h" +#include "prtime.h" + +#define LS_LOG_TEST() MOZ_LOG_TEST(GetLocalStorageLogger(), LogLevel::Info) +#define LS_LOG(_args) MOZ_LOG(GetLocalStorageLogger(), LogLevel::Info, _args) + +#if defined(MOZ_WIDGET_ANDROID) +# define LS_MOBILE +#endif + +namespace mozilla::dom { + +using namespace mozilla::dom::quota; +using namespace mozilla::dom::StorageUtils; +using namespace mozilla::ipc; + +namespace { + +struct ArchivedOriginInfo; +class ArchivedOriginScope; +class Connection; +class ConnectionThread; +class Database; +class Observer; +class PrepareDatastoreOp; +class PreparedDatastore; +class QuotaClient; +class Snapshot; + +using ArchivedOriginHashtable = + nsClassHashtable<nsCStringHashKey, ArchivedOriginInfo>; + +/******************************************************************************* + * Constants + ******************************************************************************/ + +// Major schema version. Bump for almost everything. +const uint32_t kMajorSchemaVersion = 5; + +// Minor schema version. Should almost always be 0 (maybe bump on release +// branches if we have to). +const uint32_t kMinorSchemaVersion = 0; + +// The schema version we store in the SQLite database is a (signed) 32-bit +// integer. The major version is left-shifted 4 bits so the max value is +// 0xFFFFFFF. The minor version occupies the lower 4 bits and its max is 0xF. +static_assert(kMajorSchemaVersion <= 0xFFFFFFF, + "Major version needs to fit in 28 bits."); +static_assert(kMinorSchemaVersion <= 0xF, + "Minor version needs to fit in 4 bits."); + +const int32_t kSQLiteSchemaVersion = + int32_t((kMajorSchemaVersion << 4) + kMinorSchemaVersion); + +// Changing the value here will override the page size of new databases only. +// A journal mode change and VACUUM are needed to change existing databases, so +// the best way to do that is to use the schema version upgrade mechanism. +const uint32_t kSQLitePageSizeOverride = +#ifdef LS_MOBILE + 512; +#else + 1024; +#endif + +static_assert(kSQLitePageSizeOverride == /* mozStorage default */ 0 || + (kSQLitePageSizeOverride % 2 == 0 && + kSQLitePageSizeOverride >= 512 && + kSQLitePageSizeOverride <= 65536), + "Must be 0 (disabled) or a power of 2 between 512 and 65536!"); + +// Set to some multiple of the page size to grow the database in larger chunks. +const uint32_t kSQLiteGrowthIncrement = kSQLitePageSizeOverride * 2; + +static_assert(kSQLiteGrowthIncrement >= 0 && + kSQLiteGrowthIncrement % kSQLitePageSizeOverride == 0 && + kSQLiteGrowthIncrement < uint32_t(INT32_MAX), + "Must be 0 (disabled) or a positive multiple of the page size!"); + +/** + * The database name for LocalStorage data in a per-origin directory. + */ +constexpr auto kDataFileName = u"data.sqlite"_ns; + +/** + * The journal corresponding to kDataFileName. (We don't use WAL mode.) + * Currently only needed in QuotaClient::InitOrigin and only in DEBUG builds. + * See the corresponding comment in QuotaClient::InitOrigin. + */ +#ifdef DEBUG +constexpr auto kJournalFileName = u"data.sqlite-journal"_ns; +#endif + +/** + * This file contains the current usage of the LocalStorage database as defined + * by the mozLength totals of all keys and values for the database, which + * differs from the actual size on disk. We store this value in a separate + * file as a cache so that we can initialize the QuotaClient faster. + * In the future, this file will be eliminated and the information will be + * stored in PROFILE/storage.sqlite or similar QuotaManager-wide storage. + * + * The file contains a binary verification cookie (32-bits) followed by the + * actual usage (64-bits). + */ +constexpr auto kUsageFileName = u"usage"_ns; + +/** + * Following a QuotaManager idiom, this journal file's existence is a marker + * that the usage file was in the process of being updated and is currently + * invalid. This file is created prior to updating the usage file and only + * deleted after the usage file has been written and closed and any pending + * database transactions have been committed. Note that this idiom is expected + * to work if Gecko crashes in the middle of a write, but is not expected to be + * foolproof in the face of a system crash, as we do not explicitly attempt to + * fsync the directory containing the journal file. + * + * If the journal file is found to exist at origin initialization time, the + * usage will be re-computed from the current state of DATA_FILE_NAME. + */ +constexpr auto kUsageJournalFileName = u"usage-journal"_ns; + +static const uint32_t kUsageFileSize = 12; +static const uint32_t kUsageFileCookie = 0x420a420a; + +/** + * How long between the first moment we know we have data to be written on a + * `Connection` and when we should actually perform the write. This helps + * limit disk churn under silly usage patterns and is historically consistent + * with the previous, legacy implementation. + * + * Note that flushing happens downstream of Snapshot checkpointing and its + * batch mechanism which helps avoid wasteful IPC in the case of silly content + * code. + */ +const uint32_t kFlushTimeoutMs = 5000; + +const char kPrivateBrowsingObserverTopic[] = "last-pb-context-exited"; + +const bool kDefaultShadowWrites = false; +const uint32_t kDefaultSnapshotPrefill = 16384; +const uint32_t kDefaultSnapshotGradualPrefill = 4096; +const bool kDefaultClientValidation = true; +/** + * Should all mutations also be reflected in the "shadow" database, which is + * the legacy webappsstore.sqlite database. When this is enabled, users can + * downgrade their version of Firefox and/or otherwise fall back to the legacy + * implementation without loss of data. (Older versions of Firefox will + * recognize the presence of ls-archive.sqlite and purge it and the other + * LocalStorage directories so privacy is maintained.) + */ +const char kShadowWritesPref[] = "dom.storage.shadow_writes"; +/** + * Byte budget for sending data down to the LSSnapshot instance when it is first + * created. If there is less data than this (measured by tallying the string + * length of the keys and values), all data is sent, otherwise partial data is + * sent. See `Snapshot`. + */ +const char kSnapshotPrefillPref[] = "dom.storage.snapshot_prefill"; +/** + * When a specific value is requested by an LSSnapshot that is not already fully + * populated, gradual prefill is used. This preference specifies the number of + * bytes to be used to send values beyond the specific value that is requested. + * (The size of the explicitly requested value does not impact this preference.) + * Setting the value to 0 disables gradual prefill. Tests may set this value to + * -1 which is converted to INT_MAX in order to cause gradual prefill to send + * all values not previously sent. + */ +const char kSnapshotGradualPrefillPref[] = + "dom.storage.snapshot_gradual_prefill"; + +const char kClientValidationPref[] = "dom.storage.client_validation"; + +/** + * The amount of time a PreparedDatastore instance should stick around after a + * preload is triggered in order to give time for the page to use LocalStorage + * without triggering worst-case synchronous jank. + */ +const uint32_t kPreparedDatastoreTimeoutMs = 20000; + +/** + * Cold storage for LocalStorage data extracted from webappsstore.sqlite at + * LSNG first-run that has not yet been migrated to its own per-origin directory + * by use. + * + * In other words, at first run, LSNG copies the contents of webappsstore.sqlite + * into this database. As requests are made for that LocalStorage data, the + * contents are removed from this database and placed into per-origin QM + * storage. So the contents of this database are always old, unused + * LocalStorage data that we can potentially get rid of at some point in the + * future. + */ +#define LS_ARCHIVE_FILE_NAME u"ls-archive.sqlite" +/** + * The legacy LocalStorage database. Its contents are maintained as our + * "shadow" database so that LSNG can be disabled without loss of user data. + */ +#define WEB_APPS_STORE_FILE_NAME u"webappsstore.sqlite" + +// Shadow database Write Ahead Log's maximum size is 512KB +const uint32_t kShadowMaxWALSize = 512 * 1024; + +const uint32_t kShadowJournalSizeLimit = kShadowMaxWALSize * 3; + +bool IsOnGlobalConnectionThread(); + +void AssertIsOnGlobalConnectionThread(); + +/******************************************************************************* + * SQLite functions + ******************************************************************************/ + +int32_t MakeSchemaVersion(uint32_t aMajorSchemaVersion, + uint32_t aMinorSchemaVersion) { + return int32_t((aMajorSchemaVersion << 4) + aMinorSchemaVersion); +} + +nsCString GetArchivedOriginHashKey(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix) { + return aOriginSuffix + ":"_ns + aOriginNoSuffix; +} + +nsresult CreateDataTable(mozIStorageConnection* aConnection) { + return aConnection->ExecuteSimpleSQL( + "CREATE TABLE data" + "( key TEXT PRIMARY KEY" + ", utf16_length INTEGER NOT NULL" + ", conversion_type INTEGER NOT NULL" + ", compression_type INTEGER NOT NULL" + ", last_access_time INTEGER NOT NULL DEFAULT 0" + ", value BLOB NOT NULL" + ");"_ns); +} + +nsresult CreateTables(mozIStorageConnection* aConnection) { + MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread()); + MOZ_ASSERT(aConnection); + + // Table `database` + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + "CREATE TABLE database" + "( origin TEXT NOT NULL" + ", usage INTEGER NOT NULL DEFAULT 0" + ", last_vacuum_time INTEGER NOT NULL DEFAULT 0" + ", last_analyze_time INTEGER NOT NULL DEFAULT 0" + ", last_vacuum_size INTEGER NOT NULL DEFAULT 0" + ");"_ns))); + + // Table `data` + QM_TRY(MOZ_TO_RESULT(CreateDataTable(aConnection))); + + QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(kSQLiteSchemaVersion))); + + return NS_OK; +} + +nsresult UpgradeSchemaFrom1_0To2_0(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + "ALTER TABLE database ADD COLUMN usage INTEGER NOT NULL DEFAULT 0;"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + "UPDATE database " + "SET usage = (SELECT total(utf16Length(key) + utf16Length(value)) " + "FROM data);"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(2, 0)))); + + return NS_OK; +} + +nsresult UpgradeSchemaFrom2_0To3_0(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + "ALTER TABLE data ADD COLUMN utf16Length INTEGER NOT NULL DEFAULT 0;"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + "UPDATE data SET utf16Length = utf16Length(value);"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(3, 0)))); + + return NS_OK; +} + +nsresult UpgradeSchemaFrom3_0To4_0(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(4, 0)))); + + return NS_OK; +} + +nsresult UpgradeSchemaFrom4_0To5_0(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // Recreate data table in new format following steps at + // https://www.sqlite.org/lang_altertable.html + // section "Making Other Kinds Of Table Schema Changes" + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + "CREATE TABLE migrated_data" + "( key TEXT PRIMARY KEY" + ", utf16_length INTEGER NOT NULL" + ", conversion_type INTEGER NOT NULL" + ", compression_type INTEGER NOT NULL" + ", last_access_time INTEGER NOT NULL DEFAULT 0" + ", value BLOB NOT NULL" + ");"_ns))); + + // Reinsert old data, all legacy data is UTF8 + static_assert(1u == + static_cast<uint8_t>(LSValue::ConversionType::UTF16_UTF8)); + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + "INSERT INTO migrated_data (key, utf16_length, conversion_type, " + "compression_type, last_access_time, value) " + "SELECT key, utf16Length, 1, compressed, lastAccessTime, value " + "FROM data;"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL("DROP TABLE data;"_ns))); + + // Rename to data + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + "ALTER TABLE migrated_data RENAME TO data;"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(5, 0)))); + + return NS_OK; +} + +nsresult SetDefaultPragmas(mozIStorageConnection* aConnection) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aConnection); + + QM_TRY(MOZ_TO_RESULT( + aConnection->ExecuteSimpleSQL("PRAGMA synchronous = FULL;"_ns))); + +#ifndef LS_MOBILE + if (kSQLiteGrowthIncrement) { + // This is just an optimization so ignore the failure if the disk is + // currently too full. + QM_TRY(QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT( + aConnection->SetGrowthIncrement(kSQLiteGrowthIncrement, ""_ns)), + // Predicate. + IsSpecificError<NS_ERROR_FILE_TOO_BIG>, + // Fallback. + ErrToDefaultOk<>)); + } +#endif // LS_MOBILE + + return NS_OK; +} + +template <typename CorruptedFileHandler> +Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateStorageConnection( + nsIFile& aDBFile, nsIFile& aUsageFile, const nsACString& aOrigin, + CorruptedFileHandler&& aCorruptedFileHandler) { + MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread()); + + // XXX Common logic should be refactored out of this method and + // cache::DBAction::OpenDBConnection, and maybe other similar functions. + + QM_TRY_INSPECT(const auto& storageService, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + // XXX We can't use QM_OR_ELSE_WARN_IF because base-toolchains builds fail + // with: error: use of 'tryResult28' before deduction of 'auto' + QM_TRY_UNWRAP( + auto connection, + OrElseIf( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, storageService, OpenDatabase, + &aDBFile, mozIStorageService::CONNECTION_DEFAULT), + // Predicate. + IsDatabaseCorruptionError, + // Fallback. + ([&aUsageFile, &aDBFile, &aCorruptedFileHandler, + &storageService](const nsresult rv) + -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> { + // Remove the usage file first (it might not exist at all due + // to corrupted state, which is ignored here). + + // Usually we only use QM_OR_ELSE_LOG_VERBOSE(_IF) with Remove and + // NS_ERROR_FILE_NOT_FOUND check, but we're already in the rare case + // of corruption here, so the use of QM_OR_ELSE_WARN_IF is ok here. + QM_TRY(QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT(aUsageFile.Remove(false)), + // Predicate. + ([](const nsresult rv) { + return rv == NS_ERROR_FILE_NOT_FOUND; + }), + // Fallback. + ErrToDefaultOk<>)); + + // Call the corrupted file handler before trying to remove the + // database file, which might fail. + std::forward<CorruptedFileHandler>(aCorruptedFileHandler)(); + + // Nuke the database file. + QM_TRY(MOZ_TO_RESULT(aDBFile.Remove(false))); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, storageService, OpenDatabase, + &aDBFile, mozIStorageService::CONNECTION_DEFAULT)); + }))); + + QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(connection))); + + // Check to make sure that the database schema is correct. + // XXX Try to make schemaVersion const. + QM_TRY_UNWRAP(int32_t schemaVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion)); + + QM_TRY(OkIf(schemaVersion <= kSQLiteSchemaVersion), Err(NS_ERROR_FAILURE)); + + if (schemaVersion != kSQLiteSchemaVersion) { + const bool newDatabase = !schemaVersion; + + if (newDatabase) { + // Set the page size first. + if (kSQLitePageSizeOverride) { + QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(nsPrintfCString( + "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride)))); + } + + // We have to set the auto_vacuum mode before opening a transaction. + QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL( +#ifdef LS_MOBILE + // Turn on full auto_vacuum mode to reclaim disk space on mobile + // devices (at the cost of some COMMIT speed). + "PRAGMA auto_vacuum = FULL;"_ns +#else + // Turn on incremental auto_vacuum mode on desktop builds. + "PRAGMA auto_vacuum = INCREMENTAL;"_ns +#endif + ))); + } + + bool vacuumNeeded = false; + + mozStorageTransaction transaction( + connection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(transaction.Start())); + + if (newDatabase) { + QM_TRY(MOZ_TO_RESULT(CreateTables(connection))); + +#ifdef DEBUG + { + QM_TRY_INSPECT( + const int32_t& schemaVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion), + QM_ASSERT_UNREACHABLE); + + MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); + } +#endif + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, connection, CreateStatement, + "INSERT INTO database (origin) VALUES (:origin)"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByName("origin"_ns, aOrigin))); + + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + } else { + // This logic needs to change next time we change the schema! + static_assert(kSQLiteSchemaVersion == int32_t((5 << 4) + 0), + "Upgrade function needed due to schema version increase."); + + while (schemaVersion != kSQLiteSchemaVersion) { + if (schemaVersion == MakeSchemaVersion(1, 0)) { + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom1_0To2_0(connection))); + } else if (schemaVersion == MakeSchemaVersion(2, 0)) { + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom2_0To3_0(connection))); + } else if (schemaVersion == MakeSchemaVersion(3, 0)) { + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom3_0To4_0(connection))); + } else if (schemaVersion == MakeSchemaVersion(4, 0)) { + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom4_0To5_0(connection))); + vacuumNeeded = true; + } else { + LS_WARNING( + "Unable to open LocalStorage database, no upgrade path is " + "available!"); + return Err(NS_ERROR_FAILURE); + } + + QM_TRY_UNWRAP(schemaVersion, MOZ_TO_RESULT_INVOKE_MEMBER( + connection, GetSchemaVersion)); + } + + MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); + } + + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + + if (vacuumNeeded) { + QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL("VACUUM;"_ns))); + } + + if (newDatabase) { + // Windows caches the file size, let's force it to stat the file again. + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, Exists)); + Unused << exists; + + QM_TRY_INSPECT(const int64_t& fileSize, + MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, GetFileSize)); + + MOZ_ASSERT(fileSize > 0); + + const PRTime vacuumTime = PR_Now(); + MOZ_ASSERT(vacuumTime); + + QM_TRY_INSPECT( + const auto& vacuumTimeStmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr<mozIStorageStatement>, + connection, CreateStatement, + "UPDATE database " + "SET last_vacuum_time = :time" + ", last_vacuum_size = :size;"_ns)); + + QM_TRY(MOZ_TO_RESULT( + vacuumTimeStmt->BindInt64ByName("time"_ns, vacuumTime))); + + QM_TRY( + MOZ_TO_RESULT(vacuumTimeStmt->BindInt64ByName("size"_ns, fileSize))); + + QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->Execute())); + } + } + + return connection; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> GetStorageConnection( + const nsAString& aDatabaseFilePath) { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(!aDatabaseFilePath.IsEmpty()); + MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, u".sqlite"_ns)); + + QM_TRY_INSPECT(const auto& databaseFile, QM_NewLocalFile(aDatabaseFilePath)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(databaseFile, Exists)); + + QM_TRY(OkIf(exists), Err(NS_ERROR_FAILURE)); + + QM_TRY_INSPECT(const auto& ss, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + QM_TRY_UNWRAP(auto connection, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenDatabase, + databaseFile, mozIStorageService::CONNECTION_DEFAULT)); + + QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(connection))); + + return connection; +} + +Result<nsCOMPtr<nsIFile>, nsresult> GetArchiveFile( + const nsAString& aStoragePath) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aStoragePath.IsEmpty()); + + QM_TRY_UNWRAP(auto archiveFile, QM_NewLocalFile(aStoragePath)); + + QM_TRY(MOZ_TO_RESULT( + archiveFile->Append(nsLiteralString(LS_ARCHIVE_FILE_NAME)))); + + return archiveFile; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> +CreateArchiveStorageConnection(const nsAString& aStoragePath) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aStoragePath.IsEmpty()); + + QM_TRY_INSPECT(const auto& archiveFile, GetArchiveFile(aStoragePath)); + + // QuotaManager ensures this file always exists. + DebugOnly<bool> exists; + MOZ_ASSERT(NS_SUCCEEDED(archiveFile->Exists(&exists))); + MOZ_ASSERT(exists); + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(archiveFile, IsDirectory)); + + if (isDirectory) { + LS_WARNING("ls-archive is not a file!"); + return nsCOMPtr<mozIStorageConnection>{}; + } + + QM_TRY_INSPECT(const auto& ss, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + QM_TRY_UNWRAP( + auto connection, + QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + archiveFile, mozIStorageService::CONNECTION_DEFAULT), + // Predicate. + IsDatabaseCorruptionError, + // Fallback. Don't throw an error, leave a corrupted ls-archive + // database as it is. + ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>)); + + if (connection) { + const nsresult rv = StorageDBUpdater::Update(connection); + if (NS_FAILED(rv)) { + // Don't throw an error, leave a non-updateable ls-archive database as + // it is. + return nsCOMPtr<mozIStorageConnection>{}; + } + } + + return connection; +} + +Result<nsCOMPtr<nsIFile>, nsresult> GetShadowFile(const nsAString& aBasePath) { + MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread()); + MOZ_ASSERT(!aBasePath.IsEmpty()); + + QM_TRY_UNWRAP(auto archiveFile, QM_NewLocalFile(aBasePath)); + + QM_TRY(MOZ_TO_RESULT( + archiveFile->Append(nsLiteralString(WEB_APPS_STORE_FILE_NAME)))); + + return archiveFile; +} + +nsresult SetShadowJournalMode(mozIStorageConnection* aConnection) { + MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread()); + MOZ_ASSERT(aConnection); + + // Try enabling WAL mode. This can fail in various circumstances so we have to + // check the results here. + constexpr auto journalModeQueryStart = "PRAGMA journal_mode = "_ns; + constexpr auto journalModeWAL = "wal"_ns; + + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement( + *aConnection, journalModeQueryStart + journalModeWAL)); + + QM_TRY_INSPECT(const auto& journalMode, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, *stmt, + GetUTF8String, 0)); + + if (journalMode.Equals(journalModeWAL)) { + // WAL mode successfully enabled. Set limits on its size here. + + // Set the threshold for auto-checkpointing the WAL. We don't want giant + // logs slowing down us. + QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement( + *aConnection, "PRAGMA page_size;"_ns)); + + QM_TRY_INSPECT(const int32_t& pageSize, + MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0)); + + MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + "PRAGMA wal_autocheckpoint = "_ns + + IntToCString(static_cast<int32_t>(kShadowMaxWALSize / pageSize))))); + + // Set the maximum WAL log size to reduce footprint on mobile (large empty + // WAL files will be truncated) + QM_TRY(MOZ_TO_RESULT( + aConnection->ExecuteSimpleSQL("PRAGMA journal_size_limit = "_ns + + IntToCString(kShadowJournalSizeLimit)))); + } else { + QM_TRY(MOZ_TO_RESULT( + aConnection->ExecuteSimpleSQL(journalModeQueryStart + "truncate"_ns))); + } + + return NS_OK; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateShadowStorageConnection( + const nsAString& aBasePath) { + MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread()); + MOZ_ASSERT(!aBasePath.IsEmpty()); + + QM_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath)); + + QM_TRY_INSPECT(const auto& ss, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + QM_TRY_UNWRAP( + auto connection, + QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + shadowFile, mozIStorageService::CONNECTION_DEFAULT), + // Predicate. + IsDatabaseCorruptionError, + // Fallback. + ([&shadowFile, &ss](const nsresult rv) + -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> { + QM_TRY(MOZ_TO_RESULT(shadowFile->Remove(false))); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + shadowFile, mozIStorageService::CONNECTION_DEFAULT)); + }))); + + QM_TRY(MOZ_TO_RESULT(SetShadowJournalMode(connection))); + + // XXX Depending on whether the *first* call to OpenUnsharedDatabase above + // failed, we (a) might or (b) might not be dealing with a fresh database + // here. This is confusing, since in a failure of case (a) we would do the + // same thing again. Probably, the control flow should be changed here so that + // it's clear we only delete & create a fresh database once. If we still have + // a failure then, we better give up. Or, if we really want to handle that, + // the number of 2 retries seems arbitrary, and we should better do this in + // some loop until a maximum number of retries is reached. + // + // Compare this with QuotaManager::CreateLocalStorageArchiveConnection, which + // actually tracks if the file was removed before, but it's also more + // complicated than it should be. Maybe these two methods can be merged (which + // would mean that a parameter must be added that indicates whether it's + // handling the shadow file or not). + QM_TRY(QM_OR_ELSE_WARN( + // Expression. + MOZ_TO_RESULT(StorageDBUpdater::Update(connection)), + // Fallback. + ([&connection, &shadowFile, &ss](const nsresult) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(connection->Close())); + QM_TRY(MOZ_TO_RESULT(shadowFile->Remove(false))); + + QM_TRY_UNWRAP(connection, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, shadowFile, + mozIStorageService::CONNECTION_DEFAULT)); + + QM_TRY(MOZ_TO_RESULT(SetShadowJournalMode(connection))); + + QM_TRY( + MOZ_TO_RESULT(StorageDBUpdater::CreateCurrentSchema(connection))); + + return Ok{}; + }))); + + return connection; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> GetShadowStorageConnection( + const nsAString& aBasePath) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aBasePath.IsEmpty()); + + QM_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(shadowFile, Exists)); + + QM_TRY(OkIf(exists), Err(NS_ERROR_FAILURE)); + + QM_TRY_INSPECT(const auto& ss, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, shadowFile, + mozIStorageService::CONNECTION_DEFAULT)); +} + +nsresult AttachShadowDatabase(const nsAString& aBasePath, + mozIStorageConnection* aConnection) { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(!aBasePath.IsEmpty()); + MOZ_ASSERT(aConnection); + + QM_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath)); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(shadowFile, Exists)); + + MOZ_ASSERT(exists); + } +#endif + + QM_TRY_INSPECT(const auto& path, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, shadowFile, GetPath)); + + QM_TRY_INSPECT(const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, + CreateStatement, "ATTACH DATABASE :path AS shadow;"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindStringByName("path"_ns, path))); + + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + + return NS_OK; +} + +nsresult DetachShadowDatabase(mozIStorageConnection* aConnection) { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(aConnection); + + QM_TRY(MOZ_TO_RESULT( + aConnection->ExecuteSimpleSQL("DETACH DATABASE shadow"_ns))); + + return NS_OK; +} + +Result<nsCOMPtr<nsIFile>, nsresult> GetUsageFile( + const nsAString& aDirectoryPath) { + MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread()); + MOZ_ASSERT(!aDirectoryPath.IsEmpty()); + + QM_TRY_UNWRAP(auto usageFile, QM_NewLocalFile(aDirectoryPath)); + + QM_TRY(MOZ_TO_RESULT(usageFile->Append(kUsageFileName))); + + return usageFile; +} + +Result<nsCOMPtr<nsIFile>, nsresult> GetUsageJournalFile( + const nsAString& aDirectoryPath) { + MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread()); + MOZ_ASSERT(!aDirectoryPath.IsEmpty()); + + QM_TRY_UNWRAP(auto usageJournalFile, QM_NewLocalFile(aDirectoryPath)); + + QM_TRY(MOZ_TO_RESULT(usageJournalFile->Append(kUsageJournalFileName))); + + return usageJournalFile; +} + +// Checks if aFile exists and is a file. Returns true if it exists and is a +// file, false if it doesn't exist, and an error if it exists but isn't a file. +Result<bool, nsresult> ExistsAsFile(nsIFile& aFile) { + enum class ExistsAsFileResult { DoesNotExist, IsDirectory, IsFile }; + + // This is an optimization to check both properties in one OS case, rather + // than calling Exists first, and then IsDirectory. IsDirectory also checks + // if the path exists. QM_OR_ELSE_WARN_IF is not used here since we just want + // to log NS_ERROR_FILE_NOT_FOUND result and not spam the reports. + QM_TRY_INSPECT( + const auto& res, + QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER(aFile, IsDirectory) + .map([](const bool isDirectory) { + return isDirectory ? ExistsAsFileResult::IsDirectory + : ExistsAsFileResult::IsFile; + }), + // Predicate. + ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }), + // Fallback. + ErrToOk<ExistsAsFileResult::DoesNotExist>)); + + QM_TRY(OkIf(res != ExistsAsFileResult::IsDirectory), Err(NS_ERROR_FAILURE)); + + return res == ExistsAsFileResult::IsFile; +} + +nsresult UpdateUsageFile(nsIFile* aUsageFile, nsIFile* aUsageJournalFile, + int64_t aUsage) { + MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread()); + MOZ_ASSERT(aUsageFile); + MOZ_ASSERT(aUsageJournalFile); + MOZ_ASSERT(aUsage >= 0); + + QM_TRY_INSPECT(const bool& usageJournalFileExists, + ExistsAsFile(*aUsageJournalFile)); + if (!usageJournalFileExists) { + QM_TRY(MOZ_TO_RESULT( + aUsageJournalFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644))); + } + + QM_TRY_INSPECT(const auto& stream, NS_NewLocalFileOutputStream(aUsageFile)); + + nsCOMPtr<nsIBinaryOutputStream> binaryStream = + NS_NewObjectOutputStream(stream); + + QM_TRY(MOZ_TO_RESULT(binaryStream->Write32(kUsageFileCookie))); + + QM_TRY(MOZ_TO_RESULT(binaryStream->Write64(aUsage))); + +#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG) + QM_TRY(MOZ_TO_RESULT(stream->Flush())); +#endif + + QM_TRY(MOZ_TO_RESULT(stream->Close())); + + return NS_OK; +} + +Result<UsageInfo, nsresult> LoadUsageFile(nsIFile& aUsageFile) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const int64_t& fileSize, + MOZ_TO_RESULT_INVOKE_MEMBER(aUsageFile, GetFileSize)); + + QM_TRY(OkIf(fileSize == kUsageFileSize), Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY_UNWRAP(auto stream, NS_NewLocalFileInputStream(&aUsageFile)); + + QM_TRY_INSPECT(const auto& bufferedStream, + NS_NewBufferedInputStream(stream.forget(), 16)); + + const nsCOMPtr<nsIBinaryInputStream> binaryStream = + NS_NewObjectInputStream(bufferedStream); + + QM_TRY_INSPECT(const uint32_t& cookie, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32)); + + QM_TRY(OkIf(cookie == kUsageFileCookie), Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY_INSPECT(const uint64_t& usage, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64)); + + return UsageInfo{DatabaseUsageType(Some(usage))}; +} + +/******************************************************************************* + * Non-actor class declarations + ******************************************************************************/ + +/** + * Coalescing manipulation queue used by `Datastore`. Used by `Datastore` to + * update `Datastore::mOrderedItems` efficiently/for code simplification. + * (Datastore does not actually depend on the coalescing, as mutations are + * applied atomically when a Snapshot Checkpoints, and with `Datastore::mValues` + * being updated at the same time the mutations are applied to Datastore's + * mWriteOptimizer.) + */ +class DatastoreWriteOptimizer final : public LSWriteOptimizer<LSValue> { + public: + void ApplyAndReset(nsTArray<LSItemInfo>& aOrderedItems); +}; + +/** + * Coalescing manipulation queue used by `Connection`. Used by `Connection` to + * buffer and coalesce manipulations applied to the Datastore in batches by + * Snapshot Checkpointing until flushed to disk. + */ +class ConnectionWriteOptimizer final : public LSWriteOptimizer<LSValue> { + public: + // Returns the usage as the success value. + Result<int64_t, nsresult> Perform(Connection* aConnection, + bool aShadowWrites); + + private: + /** + * Handlers for specific mutations. Each method knows how to `Perform` the + * manipulation against a `Connection` and the "shadow" database (legacy + * webappsstore.sqlite database that exists so LSNG can be disabled/safely + * downgraded from.) + */ + nsresult PerformInsertOrUpdate(Connection* aConnection, bool aShadowWrites, + const nsAString& aKey, const LSValue& aValue); + + nsresult PerformDelete(Connection* aConnection, bool aShadowWrites, + const nsAString& aKey); + + nsresult PerformTruncate(Connection* aConnection, bool aShadowWrites); +}; + +class DatastoreOperationBase : public Runnable { + nsCOMPtr<nsIEventTarget> mOwningEventTarget; + nsresult mResultCode; + Atomic<bool> mMayProceedOnNonOwningThread; + bool mMayProceed; + + public: + nsIEventTarget* OwningEventTarget() const { + MOZ_ASSERT(mOwningEventTarget); + + return mOwningEventTarget; + } + + bool IsOnOwningThread() const { + MOZ_ASSERT(mOwningEventTarget); + + bool current; + return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(¤t)) && + current; + } + + void AssertIsOnOwningThread() const { + MOZ_ASSERT(IsOnBackgroundThread()); + MOZ_ASSERT(IsOnOwningThread()); + } + + nsresult ResultCode() const { return mResultCode; } + + void SetFailureCode(nsresult aErrorCode) { + MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); + MOZ_ASSERT(NS_FAILED(aErrorCode)); + + mResultCode = aErrorCode; + } + + void MaybeSetFailureCode(nsresult aErrorCode) { + MOZ_ASSERT(NS_FAILED(aErrorCode)); + + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = aErrorCode; + } + } + + void NoteComplete() { + AssertIsOnOwningThread(); + + mMayProceed = false; + mMayProceedOnNonOwningThread = false; + } + + bool MayProceed() const { + AssertIsOnOwningThread(); + + return mMayProceed; + } + + // May be called on any thread, but you should call MayProceed() if you know + // you're on the background thread because it is slightly faster. + bool MayProceedOnNonOwningThread() const { + return mMayProceedOnNonOwningThread; + } + + protected: + DatastoreOperationBase() + : Runnable("dom::DatastoreOperationBase"), + mOwningEventTarget(GetCurrentEventTarget()), + mResultCode(NS_OK), + mMayProceedOnNonOwningThread(true), + mMayProceed(true) {} + + ~DatastoreOperationBase() override { MOZ_ASSERT(!mMayProceed); } +}; + +class ConnectionDatastoreOperationBase : public DatastoreOperationBase { + protected: + RefPtr<Connection> mConnection; + /** + * This boolean flag is used by the CloseOp to avoid creating empty databases. + */ + const bool mEnsureStorageConnection; + + public: + // This callback will be called on the background thread before releasing the + // final reference to this request object. Subclasses may perform any + // additional cleanup here but must always call the base class implementation. + virtual void Cleanup(); + + protected: + ConnectionDatastoreOperationBase(Connection* aConnection, + bool aEnsureStorageConnection = true); + + ~ConnectionDatastoreOperationBase(); + + // Must be overridden in subclasses. Called on the target thread to allow the + // subclass to perform necessary datastore operations. A successful return + // value will trigger an OnSuccess callback on the background thread while + // while a failure value will trigger an OnFailure callback. + virtual nsresult DoDatastoreWork() = 0; + + // Methods that subclasses may implement. + virtual void OnSuccess(); + + virtual void OnFailure(nsresult aResultCode); + + private: + void RunOnConnectionThread(); + + void RunOnOwningThread(); + + // Not to be overridden by subclasses. + NS_DECL_NSIRUNNABLE +}; + +class Connection final : public CachingDatabaseConnection { + friend class ConnectionThread; + + class InitTemporaryOriginHelper; + + class FlushOp; + class CloseOp; + + RefPtr<ConnectionThread> mConnectionThread; + RefPtr<QuotaClient> mQuotaClient; + nsCOMPtr<nsITimer> mFlushTimer; + UniquePtr<ArchivedOriginScope> mArchivedOriginScope; + ConnectionWriteOptimizer mWriteOptimizer; + // XXX Consider changing this to ClientMetadata. + const OriginMetadata mOriginMetadata; + nsString mDirectoryPath; + /** + * Propagated from PrepareDatastoreOp. PrepareDatastoreOp may defer the + * creation of the localstorage client directory and database on the + * QuotaManager IO thread in its DatabaseWork method to + * Connection::EnsureStorageConnection, in which case the method needs to know + * it is responsible for taking those actions (without redundantly performing + * the existence checks). + */ + const bool mDatabaseWasNotAvailable; + bool mHasCreatedDatabase; + bool mFlushScheduled; +#ifdef DEBUG + bool mInUpdateBatch; + bool mFinished; +#endif + + public: + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Connection) + + void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(Connection); } + + QuotaClient* GetQuotaClient() const { + MOZ_ASSERT(mQuotaClient); + + return mQuotaClient; + } + + ArchivedOriginScope* GetArchivedOriginScope() const { + return mArchivedOriginScope.get(); + } + + const nsCString& Origin() const { return mOriginMetadata.mOrigin; } + + const nsString& DirectoryPath() const { return mDirectoryPath; } + + void GetFinishInfo(bool& aDatabaseWasNotAvailable, + bool& aHasCreatedDatabase) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mFinished); + + aDatabaseWasNotAvailable = mDatabaseWasNotAvailable; + aHasCreatedDatabase = mHasCreatedDatabase; + } + + ////////////////////////////////////////////////////////////////////////////// + // Methods which can only be called on the owning thread. + + // This method is used to asynchronously execute a connection datastore + // operation on the connection thread. + void Dispatch(ConnectionDatastoreOperationBase* aOp); + + // This method is used to asynchronously close the storage connection on the + // connection thread. + void Close(nsIRunnable* aCallback); + + void SetItem(const nsString& aKey, const LSValue& aValue, int64_t aDelta, + bool aIsNewItem); + + void RemoveItem(const nsString& aKey, int64_t aDelta); + + void Clear(int64_t aDelta); + + void BeginUpdateBatch(); + + void EndUpdateBatch(); + + ////////////////////////////////////////////////////////////////////////////// + // Methods which can only be called on the connection thread. + + nsresult EnsureStorageConnection(); + + mozIStorageConnection* StorageConnection() const { + AssertIsOnGlobalConnectionThread(); + + return &MutableStorageConnection(); + } + + void CloseStorageConnection(); + + nsresult BeginWriteTransaction(); + + nsresult CommitWriteTransaction(); + + nsresult RollbackWriteTransaction(); + + private: + // Only created by ConnectionThread. + Connection(ConnectionThread* aConnectionThread, + const OriginMetadata& aOriginMetadata, + UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope, + bool aDatabaseWasNotAvailable); + + ~Connection(); + + void ScheduleFlush(); + + void Flush(); + + static void FlushTimerCallback(nsITimer* aTimer, void* aClosure); +}; + +/** + * Helper to invoke EnsureTemporaryOriginIsInitialized on the QuotaManager IO + * thread from the LocalStorage connection thread when creating a database + * connection on demand. This is necessary because we attempt to defer the + * creation of the origin directory and the database until absolutely needed, + * but the directory creation and origin initialization must happen on the QM + * IO thread for invariant reasons. (We can't just use a mutex because there + * could be logic on the IO thread that also wants to deal with the same + * origin, so we need to queue a runnable and wait our turn.) + */ +class Connection::InitTemporaryOriginHelper final : public Runnable { + mozilla::Monitor mMonitor MOZ_UNANNOTATED; + const OriginMetadata mOriginMetadata; + nsString mOriginDirectoryPath; + nsresult mIOThreadResultCode; + bool mWaiting; + + public: + explicit InitTemporaryOriginHelper(const OriginMetadata& aOriginMetadata) + : Runnable("dom::localstorage::Connection::InitTemporaryOriginHelper"), + mMonitor("InitTemporaryOriginHelper::mMonitor"), + mOriginMetadata(aOriginMetadata), + mIOThreadResultCode(NS_OK), + mWaiting(true) { + AssertIsOnGlobalConnectionThread(); + } + + Result<nsString, nsresult> BlockAndReturnOriginDirectoryPath(); + + private: + ~InitTemporaryOriginHelper() = default; + + nsresult RunOnIOThread(); + + NS_DECL_NSIRUNNABLE +}; + +class Connection::FlushOp final : public ConnectionDatastoreOperationBase { + ConnectionWriteOptimizer mWriteOptimizer; + bool mShadowWrites; + + public: + FlushOp(Connection* aConnection, ConnectionWriteOptimizer&& aWriteOptimizer); + + private: + nsresult DoDatastoreWork() override; + + void Cleanup() override; +}; + +class Connection::CloseOp final : public ConnectionDatastoreOperationBase { + nsCOMPtr<nsIRunnable> mCallback; + + public: + CloseOp(Connection* aConnection, nsIRunnable* aCallback) + : ConnectionDatastoreOperationBase(aConnection, + /* aEnsureStorageConnection */ false), + mCallback(aCallback) {} + + private: + nsresult DoDatastoreWork() override; + + void Cleanup() override; +}; + +class ConnectionThread final { + friend class Connection; + + nsCOMPtr<nsIThread> mThread; + nsRefPtrHashtable<nsCStringHashKey, Connection> mConnections; + + public: + ConnectionThread(); + + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(ConnectionThread); + } + + bool IsOnConnectionThread(); + + void AssertIsOnConnectionThread(); + + already_AddRefed<Connection> CreateConnection( + const OriginMetadata& aOriginMetadata, + UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope, + bool aDatabaseWasNotAvailable); + + void Shutdown(); + + NS_INLINE_DECL_REFCOUNTING(ConnectionThread) + + private: + ~ConnectionThread(); +}; + +/** + * Canonical state of Storage for an origin, containing all keys and their + * values in the parent process. Specifically, this is the state that will + * be handed out to freshly created Snapshots and that will be persisted to disk + * when the Connection's flush completes. State is mutated in batches as + * Snapshot instances Checkpoint their mutations locally accumulated in the + * child LSSnapshots. + */ +class Datastore final + : public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + RefPtr<DirectoryLock> mDirectoryLock; + RefPtr<Connection> mConnection; + RefPtr<QuotaObject> mQuotaObject; + nsCOMPtr<nsIRunnable> mCompleteCallback; + /** + * PrepareDatastoreOps register themselves with the Datastore at + * and unregister in PrepareDatastoreOp::Cleanup. + */ + nsTHashSet<PrepareDatastoreOp*> mPrepareDatastoreOps; + /** + * PreparedDatastore instances register themselves with their associated + * Datastore at construction time and unregister at destruction time. They + * hang around for kPreparedDatastoreTimeoutMs in order to keep the Datastore + * from closing itself via MaybeClose(), thereby giving the document enough + * time to load and access LocalStorage. + */ + nsTHashSet<PreparedDatastore*> mPreparedDatastores; + /** + * A database is live (and in this hashtable) if it has a live LSDatabase + * actor. There is at most one Database per origin per content process. Each + * Database corresponds to an LSDatabase in its associated content process. + */ + nsTHashSet<Database*> mDatabases; + /** + * A database is active if it has a non-null `mSnapshot`. As long as there + * are any active databases final deltas can't be calculated and + * `UpdateUsage()` can't be invoked. + */ + nsTHashSet<Database*> mActiveDatabases; + /** + * Non-authoritative hashtable representation of mOrderedItems for efficient + * lookup. + */ + nsTHashMap<nsStringHashKey, LSValue> mValues; + /** + * The authoritative ordered state of the Datastore; mValue also exists as an + * unordered hashtable for efficient lookup. + */ + nsTArray<LSItemInfo> mOrderedItems; + nsTArray<int64_t> mPendingUsageDeltas; + DatastoreWriteOptimizer mWriteOptimizer; + const OriginMetadata mOriginMetadata; + const uint32_t mPrivateBrowsingId; + int64_t mUsage; + int64_t mUpdateBatchUsage; + int64_t mSizeOfKeys; + int64_t mSizeOfItems; + bool mClosed; + bool mInUpdateBatch; + bool mHasLivePrivateDatastore; + + public: + // Created by PrepareDatastoreOp. + Datastore(const OriginMetadata& aOriginMetadata, uint32_t aPrivateBrowsingId, + int64_t aUsage, int64_t aSizeOfKeys, int64_t aSizeOfItems, + RefPtr<DirectoryLock>&& aDirectoryLock, + RefPtr<Connection>&& aConnection, + RefPtr<QuotaObject>&& aQuotaObject, + nsTHashMap<nsStringHashKey, LSValue>& aValues, + nsTArray<LSItemInfo>&& aOrderedItems); + + Maybe<DirectoryLock&> MaybeDirectoryLockRef() const { + AssertIsOnBackgroundThread(); + + return ToMaybeRef(mDirectoryLock.get()); + } + + const nsCString& Origin() const { return mOriginMetadata.mOrigin; } + + uint32_t PrivateBrowsingId() const { return mPrivateBrowsingId; } + + bool IsPersistent() const { + // Private-browsing is forbidden from touching disk, but + // StorageAccess::eSessionScoped is allowed to touch disk because + // QuotaManager's storage for such origins is wiped at shutdown. + return mPrivateBrowsingId == 0; + } + + void Close(); + + bool IsClosed() const { + AssertIsOnBackgroundThread(); + + return mClosed; + } + + void WaitForConnectionToComplete(nsIRunnable* aCallback); + + void NoteLivePrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp); + + void NoteFinishedPrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp); + + void NoteLivePrivateDatastore(); + + void NoteFinishedPrivateDatastore(); + + void NoteLivePreparedDatastore(PreparedDatastore* aPreparedDatastore); + + void NoteFinishedPreparedDatastore(PreparedDatastore* aPreparedDatastore); + + bool HasOtherProcessDatabases(Database* aDatabase); + + void NoteLiveDatabase(Database* aDatabase); + + void NoteFinishedDatabase(Database* aDatabase); + + void NoteActiveDatabase(Database* aDatabase); + + void NoteInactiveDatabase(Database* aDatabase); + + void GetSnapshotLoadInfo(const nsAString& aKey, bool& aAddKeyToUnknownItems, + nsTHashtable<nsStringHashKey>& aLoadedItems, + nsTArray<LSItemInfo>& aItemInfos, + uint32_t& aNextLoadIndex, + LSSnapshot::LoadState& aLoadState); + + uint32_t GetLength() const { return mValues.Count(); } + + const nsTArray<LSItemInfo>& GetOrderedItems() const { return mOrderedItems; } + + void GetItem(const nsAString& aKey, LSValue& aValue) const; + + void GetKeys(nsTArray<nsString>& aKeys) const; + + ////////////////////////////////////////////////////////////////////////////// + // Mutation Methods + // + // These are only called during Snapshot::Checkpoint + + /** + * Used by Snapshot::Checkpoint to set a key/value pair as part of an + * explicit batch. + */ + void SetItem(Database* aDatabase, const nsString& aKey, + const LSValue& aValue); + + void RemoveItem(Database* aDatabase, const nsString& aKey); + + void Clear(Database* aDatabase); + + void BeginUpdateBatch(int64_t aSnapshotUsage); + + int64_t EndUpdateBatch(int64_t aSnapshotPeakUsage); + + int64_t GetUsage() const { return mUsage; } + + int64_t AttemptToUpdateUsage(int64_t aMinSize, bool aInitial); + + bool HasOtherProcessObservers(Database* aDatabase); + + void NotifyOtherProcessObservers(Database* aDatabase, + const nsString& aDocumentURI, + const nsString& aKey, + const LSValue& aOldValue, + const LSValue& aNewValue); + + void NoteChangedObserverArray(const nsTArray<NotNull<Observer*>>& aObservers); + + void Stringify(nsACString& aResult) const; + + NS_INLINE_DECL_REFCOUNTING(Datastore) + + private: + // Reference counted. + ~Datastore(); + + bool UpdateUsage(int64_t aDelta); + + void MaybeClose(); + + void ConnectionClosedCallback(); + + void CleanupMetadata(); + + void NotifySnapshots(Database* aDatabase, const nsAString& aKey, + const LSValue& aOldValue, bool aAffectsOrder); + + void NoteChangedDatabaseMap(); +}; + +class PrivateDatastore { + const NotNull<RefPtr<Datastore>> mDatastore; + + public: + explicit PrivateDatastore(MovingNotNull<RefPtr<Datastore>> aDatastore) + : mDatastore(std::move(aDatastore)) { + AssertIsOnBackgroundThread(); + + mDatastore->NoteLivePrivateDatastore(); + } + + ~PrivateDatastore() { mDatastore->NoteFinishedPrivateDatastore(); } + + const Datastore& DatastoreRef() const { + AssertIsOnBackgroundThread(); + + return *mDatastore; + } +}; + +class PreparedDatastore { + RefPtr<Datastore> mDatastore; + nsCOMPtr<nsITimer> mTimer; + const Maybe<ContentParentId> mContentParentId; + // Strings share buffers if possible, so it's not a problem to duplicate the + // origin here. + const nsCString mOrigin; + uint64_t mDatastoreId; + bool mForPreload; + bool mInvalidated; + + public: + PreparedDatastore(Datastore* aDatastore, + const Maybe<ContentParentId>& aContentParentId, + const nsACString& aOrigin, uint64_t aDatastoreId, + bool aForPreload) + : mDatastore(aDatastore), + mTimer(NS_NewTimer()), + mContentParentId(aContentParentId), + mOrigin(aOrigin), + mDatastoreId(aDatastoreId), + mForPreload(aForPreload), + mInvalidated(false) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatastore); + MOZ_ASSERT(mTimer); + + aDatastore->NoteLivePreparedDatastore(this); + + MOZ_ALWAYS_SUCCEEDS(mTimer->InitWithNamedFuncCallback( + TimerCallback, this, kPreparedDatastoreTimeoutMs, + nsITimer::TYPE_ONE_SHOT, "PreparedDatastore::TimerCallback")); + } + + ~PreparedDatastore() { + MOZ_ASSERT(mDatastore); + MOZ_ASSERT(mTimer); + + mTimer->Cancel(); + + mDatastore->NoteFinishedPreparedDatastore(this); + } + + const Datastore& DatastoreRef() const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDatastore); + + return *mDatastore; + } + + Datastore& MutableDatastoreRef() const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDatastore); + + return *mDatastore; + } + + const Maybe<ContentParentId>& GetContentParentId() const { + return mContentParentId; + } + + const nsCString& Origin() const { return mOrigin; } + + void Invalidate() { + AssertIsOnBackgroundThread(); + + mInvalidated = true; + + if (mForPreload) { + mTimer->Cancel(); + + MOZ_ALWAYS_SUCCEEDS(mTimer->InitWithNamedFuncCallback( + TimerCallback, this, 0, nsITimer::TYPE_ONE_SHOT, + "PreparedDatastore::TimerCallback")); + } + } + + bool IsInvalidated() const { + AssertIsOnBackgroundThread(); + + return mInvalidated; + } + + private: + void Destroy(); + + static void TimerCallback(nsITimer* aTimer, void* aClosure); +}; + +/******************************************************************************* + * Actor class declarations + ******************************************************************************/ + +class Database final + : public PBackgroundLSDatabaseParent, + public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + RefPtr<Datastore> mDatastore; + Snapshot* mSnapshot; + const PrincipalInfo mPrincipalInfo; + const Maybe<ContentParentId> mContentParentId; + // Strings share buffers if possible, so it's not a problem to duplicate the + // origin here. + nsCString mOrigin; + uint32_t mPrivateBrowsingId; + bool mAllowedToClose; + bool mActorDestroyed; + bool mRequestedAllowToClose; +#ifdef DEBUG + bool mActorWasAlive; +#endif + + public: + // Created in AllocPBackgroundLSDatabaseParent. + Database(const PrincipalInfo& aPrincipalInfo, + const Maybe<ContentParentId>& aContentParentId, + const nsACString& aOrigin, uint32_t aPrivateBrowsingId); + + Datastore* GetDatastore() const { + AssertIsOnBackgroundThread(); + return mDatastore; + } + + Maybe<Datastore&> MaybeDatastoreRef() const { + AssertIsOnBackgroundThread(); + + return ToMaybeRef(mDatastore.get()); + } + + const PrincipalInfo& GetPrincipalInfo() const { return mPrincipalInfo; } + + bool IsOwnedByProcess(ContentParentId aContentParentId) const { + return mContentParentId && mContentParentId.value() == aContentParentId; + } + + uint32_t PrivateBrowsingId() const { return mPrivateBrowsingId; } + + const nsCString& Origin() const { return mOrigin; } + + void SetActorAlive(Datastore* aDatastore); + + void RegisterSnapshot(Snapshot* aSnapshot); + + void UnregisterSnapshot(Snapshot* aSnapshot); + + Snapshot* GetSnapshot() const { + AssertIsOnBackgroundThread(); + return mSnapshot; + } + + void RequestAllowToClose(); + + void ForceKill(); + + void Stringify(nsACString& aResult) const; + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Database) + + private: + // Reference counted. + ~Database(); + + void AllowToClose(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; + + mozilla::ipc::IPCResult RecvAllowToClose() override; + + PBackgroundLSSnapshotParent* AllocPBackgroundLSSnapshotParent( + const nsAString& aDocumentURI, const nsAString& aKey, + const bool& aIncreasePeakUsage, const int64_t& aMinSize, + LSSnapshotInitInfo* aInitInfo) override; + + mozilla::ipc::IPCResult RecvPBackgroundLSSnapshotConstructor( + PBackgroundLSSnapshotParent* aActor, const nsAString& aDocumentURI, + const nsAString& aKey, const bool& aIncreasePeakUsage, + const int64_t& aMinSize, LSSnapshotInitInfo* aInitInfo) override; + + bool DeallocPBackgroundLSSnapshotParent( + PBackgroundLSSnapshotParent* aActor) override; +}; + +/** + * Attempts to capture the state of the underlying Datastore at the time of its + * creation so run-to-completion semantics can be honored. + * + * Rather than simply duplicate the contents of `DataStore::mValues` and + * `Datastore::mOrderedItems` at the time of their creation, the Snapshot tracks + * mutations to the Datastore as they happen, saving off the state of values as + * they existed when the Snapshot was created. In other words, given an initial + * Datastore state of { foo: 'bar', bar: 'baz' }, the Snapshot won't store those + * values until it hears via `SaveItem` that "foo" is being over-written. At + * that time, it will save off foo='bar' in mValues. + * + * ## Quota Allocation ## + * + * ## States ## + * + */ +class Snapshot final : public PBackgroundLSSnapshotParent { + /** + * The Database that owns this snapshot. There is a 1:1 relationship between + * snapshots and databases. + */ + RefPtr<Database> mDatabase; + RefPtr<Datastore> mDatastore; + /** + * The set of keys for which values have been sent to the child LSSnapshot. + * Cleared once all values have been sent as indicated by + * mLoadedItems.Count()==mTotalLength and therefore mLoadedAllItems should be + * true. No requests should be received for keys already in this set, and + * this is enforced by fatal IPC error (unless fuzzing). + */ + nsTHashtable<nsStringHashKey> mLoadedItems; + /** + * The set of keys for which a RecvLoadValueAndMoreItems request was received + * but there was no such key, and so null was returned. The child LSSnapshot + * will also cache these values, so redundant requests are also handled with + * fatal process termination just like for mLoadedItems. Also cleared when + * mLoadedAllItems becomes true because then the child can infer that all + * other values must be null. (Note: this could also be done when + * mLoadKeysReceived is true as a further optimization, but is not.) + */ + nsTHashSet<nsString> mUnknownItems; + /** + * Values that have changed in mDatastore as reported by SaveItem + * notifications that are not yet known to the child LSSnapshot. + * + * The naive way to snapshot the state of mDatastore would be to duplicate its + * internal mValues at the time of our creation, but that is wasteful if few + * changes are made to the Datastore's state. So we only track values that + * are changed/evicted from the Datastore as they happen, as reported to us by + * SaveItem notifications. + */ + nsTHashMap<nsStringHashKey, LSValue> mValues; + /** + * Latched state of mDatastore's keys during a SaveItem notification with + * aAffectsOrder=true. The ordered keys needed to be saved off so that a + * consistent ordering could be presented to the child LSSnapshot when it asks + * for them via RecvLoadKeys. + */ + nsTArray<nsString> mKeys; + nsString mDocumentURI; + /** + * The index used for restoring iteration over not yet sent key/value pairs to + * the child LSSnapshot. + */ + uint32_t mNextLoadIndex; + /** + * The number of key/value pairs that were present in the Datastore at the + * time the snapshot was created. Once we have sent this many values to the + * child LSSnapshot, we can infer that it has received all of the keys/values + * and set mLoadedAllItems to true and clear mLoadedItems and mUnknownItems. + * Note that knowing the keys/values is not the same as knowing their ordering + * and so mKeys may be retained. + */ + uint32_t mTotalLength; + int64_t mUsage; + int64_t mPeakUsage; + /** + * True if SaveItem has saved mDatastore's keys into mKeys because a SaveItem + * notification with aAffectsOrder=true was received. + */ + bool mSavedKeys; + bool mActorDestroyed; + bool mFinishReceived; + bool mLoadedReceived; + /** + * True if LSSnapshot's mLoadState should be LoadState::AllOrderedItems or + * LoadState::AllUnorderedItems. It will be AllOrderedItems if the initial + * snapshot contained all the data or if the state was AllOrderedKeys and + * successive RecvLoadValueAndMoreItems requests have resulted in the + * LSSnapshot being told all of the key/value pairs. It will be + * AllUnorderedItems if the state was LoadState::Partial and successive + * RecvLoadValueAndMoreItem requests got all the keys/values but the key + * ordering was not retrieved. + */ + bool mLoadedAllItems; + /** + * True if LSSnapshot's mLoadState should be LoadState::AllOrderedItems or + * AllOrderedKeys. This can occur because of the initial snapshot, or because + * a RecvLoadKeys request was received. + */ + bool mLoadKeysReceived; + bool mSentMarkDirty; + + /** + * True if there are Database objects in other content processes. The value + * never gets updated, we instead mark snapshots as dirty when Database + * objects are added or removed. Marking snapshots as dirty forces creation + * of new snapshots for new tasks. + */ + bool mHasOtherProcessDatabases; + bool mHasOtherProcessObservers; + + public: + // Created in AllocPBackgroundLSSnapshotParent. + Snapshot(Database* aDatabase, const nsAString& aDocumentURI); + + void Init(nsTHashtable<nsStringHashKey>& aLoadedItems, + nsTHashSet<nsString>&& aUnknownItems, uint32_t aNextLoadIndex, + uint32_t aTotalLength, int64_t aUsage, int64_t aPeakUsage, + LSSnapshot::LoadState aLoadState, bool aHasOtherProcessDatabases, + bool aHasOtherProcessObservers) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aUsage >= 0); + MOZ_ASSERT(aPeakUsage >= aUsage); + MOZ_ASSERT_IF(aLoadState != LSSnapshot::LoadState::AllOrderedItems, + aNextLoadIndex < aTotalLength); + MOZ_ASSERT(mTotalLength == 0); + MOZ_ASSERT(mUsage == -1); + MOZ_ASSERT(mPeakUsage == -1); + + mLoadedItems.SwapElements(aLoadedItems); + mUnknownItems = std::move(aUnknownItems); + mNextLoadIndex = aNextLoadIndex; + mTotalLength = aTotalLength; + mUsage = aUsage; + mPeakUsage = aPeakUsage; + if (aLoadState == LSSnapshot::LoadState::AllOrderedKeys) { + MOZ_ASSERT(mUnknownItems.Count() == 0); + mLoadKeysReceived = true; + } else if (aLoadState == LSSnapshot::LoadState::AllOrderedItems) { + MOZ_ASSERT(mLoadedItems.Count() == 0); + MOZ_ASSERT(mUnknownItems.Count() == 0); + MOZ_ASSERT(mNextLoadIndex == mTotalLength); + mLoadedReceived = true; + mLoadedAllItems = true; + mLoadKeysReceived = true; + } + mHasOtherProcessDatabases = aHasOtherProcessDatabases; + mHasOtherProcessObservers = aHasOtherProcessObservers; + } + + /** + * Called via NotifySnapshots by Datastore whenever it is updating its + * internal state so that snapshots can save off the state of a value at the + * time of their creation. + */ + void SaveItem(const nsAString& aKey, const LSValue& aOldValue, + bool aAffectsOrder); + + void MarkDirty(); + + bool IsDirty() const { + AssertIsOnBackgroundThread(); + + return mSentMarkDirty; + } + + bool HasOtherProcessDatabases() const { + AssertIsOnBackgroundThread(); + + return mHasOtherProcessDatabases; + } + + bool HasOtherProcessObservers() const { + AssertIsOnBackgroundThread(); + + return mHasOtherProcessObservers; + } + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Snapshot) + + private: + // Reference counted. + ~Snapshot(); + + mozilla::ipc::IPCResult Checkpoint(nsTArray<LSWriteInfo>&& aWriteInfos); + + mozilla::ipc::IPCResult CheckpointAndNotify( + nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos); + + void Finish(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; + + mozilla::ipc::IPCResult RecvAsyncCheckpoint( + nsTArray<LSWriteInfo>&& aWriteInfos) override; + + mozilla::ipc::IPCResult RecvAsyncCheckpointAndNotify( + nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) override; + + mozilla::ipc::IPCResult RecvSyncCheckpoint( + nsTArray<LSWriteInfo>&& aWriteInfos) override; + + mozilla::ipc::IPCResult RecvSyncCheckpointAndNotify( + nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) override; + + mozilla::ipc::IPCResult RecvAsyncFinish() override; + + mozilla::ipc::IPCResult RecvSyncFinish() override; + + mozilla::ipc::IPCResult RecvLoaded() override; + + mozilla::ipc::IPCResult RecvLoadValueAndMoreItems( + const nsAString& aKey, LSValue* aValue, + nsTArray<LSItemInfo>* aItemInfos) override; + + mozilla::ipc::IPCResult RecvLoadKeys(nsTArray<nsString>* aKeys) override; + + mozilla::ipc::IPCResult RecvIncreasePeakUsage(const int64_t& aMinSize, + int64_t* aSize) override; +}; + +class Observer final : public PBackgroundLSObserverParent { + nsCString mOrigin; + bool mActorDestroyed; + + public: + // Created in AllocPBackgroundLSObserverParent. + explicit Observer(const nsACString& aOrigin); + + const nsCString& Origin() const { return mOrigin; } + + void Observe(Database* aDatabase, const nsString& aDocumentURI, + const nsString& aKey, const LSValue& aOldValue, + const LSValue& aNewValue); + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Observer) + + private: + // Reference counted. + ~Observer(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; +}; + +class LSRequestBase : public DatastoreOperationBase, + public PBackgroundLSRequestParent { + protected: + enum class State { + // Just created on the PBackground thread. Next step is StartingRequest. + Initial, + + // Waiting to start/starting request on the PBackground thread. Next step is + // either Nesting if a subclass needs to process more nested states or + // SendingReadyMessage if a subclass doesn't need any nested processing. + StartingRequest, + + // Doing nested processing. + Nesting, + + // Waiting to send/sending the ready message on the PBackground thread. Next + // step is WaitingForFinish. + SendingReadyMessage, + + // Waiting for the finish message on the PBackground thread. Next step is + // SendingResults. + WaitingForFinish, + + // Waiting to send/sending results on the PBackground thread. Next step is + // Completed. + SendingResults, + + // All done. + Completed + }; + + const LSRequestParams mParams; + Maybe<ContentParentId> mContentParentId; + State mState; + bool mWaitingForFinish; + + public: + LSRequestBase(const LSRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId); + + void Dispatch(); + + void StringifyState(nsACString& aResult) const; + + virtual void Stringify(nsACString& aResult) const; + + virtual void Log(); + + protected: + ~LSRequestBase() override; + + virtual nsresult Start() = 0; + + virtual nsresult NestedRun(); + + virtual void GetResponse(LSRequestResponse& aResponse) = 0; + + virtual void Cleanup() {} + + private: + bool VerifyRequestParams(); + + nsresult StartRequest(); + + void SendReadyMessage(); + + nsresult SendReadyMessageInternal(); + + void Finish(); + + void FinishInternal(); + + void SendResults(); + + protected: + // Common nsIRunnable implementation that subclasses may not override. + NS_IMETHOD + Run() final; + + // IPDL methods. + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + mozilla::ipc::IPCResult RecvCancel() final; + + mozilla::ipc::IPCResult RecvFinish() final; +}; + +class PrepareDatastoreOp + : public LSRequestBase, + public OpenDirectoryListener, + public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + class LoadDataOp; + + class CompressFunction; + class CompressionTypeFunction; + + enum class NestedState { + // The nesting has not yet taken place. Next step is + // CheckExistingOperations. + BeforeNesting, + + // Checking if a prepare datastore operation is already running for given + // origin on the PBackground thread. Next step is CheckClosingDatastore. + CheckExistingOperations, + + // Checking if a datastore is closing the connection for given origin on + // the PBackground thread. Next step is PreparationPending. + CheckClosingDatastore, + + // Ensuring quota manager is created and opening directory on the + // PBackground thread. Next step is either SendingResults if quota manager + // is not available or DirectoryOpenPending if quota manager is available. + // If a datastore already exists for given origin then the next state is + // SendingReadyMessage. + PreparationPending, + + // Waiting for directory open allowed on the PBackground thread. The next + // step is either SendingReadyMessage if directory lock failed to acquire, + // or DatabaseWorkOpen if directory lock is acquired. + DirectoryOpenPending, + + // Waiting to do/doing work on the QuotaManager IO thread. Its next step is + // BeginLoadData. + DatabaseWorkOpen, + + // Starting a load data operation on the PBackground thread. Next step is + // DatabaseWorkLoadData. + BeginLoadData, + + // Waiting to do/doing work on the connection thread. This involves waiting + // for the LoadDataOp to do its work. Eventually the state will transition + // to SendingReadyMessage. + DatabaseWorkLoadData, + + // The nesting has completed. + AfterNesting + }; + + RefPtr<PrepareDatastoreOp> mDelayedOp; + RefPtr<DirectoryLock> mPendingDirectoryLock; + RefPtr<DirectoryLock> mDirectoryLock; + RefPtr<Connection> mConnection; + RefPtr<Datastore> mDatastore; + UniquePtr<ArchivedOriginScope> mArchivedOriginScope; + LoadDataOp* mLoadDataOp; + nsTHashMap<nsStringHashKey, LSValue> mValues; + nsTArray<LSItemInfo> mOrderedItems; + OriginMetadata mOriginMetadata; + nsCString mMainThreadOrigin; + nsString mDatabaseFilePath; + uint32_t mPrivateBrowsingId; + int64_t mUsage; + int64_t mSizeOfKeys; + int64_t mSizeOfItems; + uint64_t mDatastoreId; + NestedState mNestedState; + const bool mForPreload; + bool mDatabaseNotAvailable; + // Set when the Datastore has been registered with gPrivateDatastores so that + // it can be unregistered if an error is encountered in PrepareDatastoreOp. + FlippedOnce<false> mPrivateDatastoreRegistered; + // Set when the Datastore has been registered with gPreparedDatastores so + // that it can be unregistered if an error is encountered in + // PrepareDatastoreOp. + FlippedOnce<false> mPreparedDatastoreRegistered; + bool mInvalidated; + +#ifdef DEBUG + int64_t mDEBUGUsage; +#endif + + public: + PrepareDatastoreOp(const LSRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId); + + Maybe<DirectoryLock&> MaybeDirectoryLockRef() const { + AssertIsOnBackgroundThread(); + + return ToMaybeRef(mDirectoryLock.get()); + } + + bool OriginIsKnown() const { + MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread()); + + return !mOriginMetadata.mOrigin.IsEmpty(); + } + + const nsCString& Origin() const { + MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread()); + MOZ_ASSERT(OriginIsKnown()); + + return mOriginMetadata.mOrigin; + } + + void Invalidate() { + AssertIsOnOwningThread(); + + mInvalidated = true; + } + + void StringifyNestedState(nsACString& aResult) const; + + void Stringify(nsACString& aResult) const override; + + void Log() override; + + private: + ~PrepareDatastoreOp() override; + + nsresult Start() override; + + nsresult CheckExistingOperations(); + + nsresult CheckClosingDatastoreInternal(); + + nsresult CheckClosingDatastore(); + + nsresult BeginDatastorePreparationInternal(); + + nsresult BeginDatastorePreparation(); + + void SendToIOThread(); + + nsresult DatabaseWork(); + + nsresult DatabaseNotAvailable(); + + nsresult EnsureDirectoryEntry(nsIFile* aEntry, bool aCreateIfNotExists, + bool aDirectory, + bool* aAlreadyExisted = nullptr); + + nsresult VerifyDatabaseInformation(mozIStorageConnection* aConnection); + + already_AddRefed<QuotaObject> GetQuotaObject(); + + nsresult BeginLoadData(); + + void FinishNesting(); + + nsresult FinishNestingOnNonOwningThread(); + + nsresult NestedRun() override; + + void GetResponse(LSRequestResponse& aResponse) override; + + void Cleanup() override; + + void ConnectionClosedCallback(); + + void CleanupMetadata(); + + NS_DECL_ISUPPORTS_INHERITED + + // IPDL overrides. + void ActorDestroy(ActorDestroyReason aWhy) override; + + // OpenDirectoryListener overrides. + void DirectoryLockAcquired(DirectoryLock* aLock) override; + + void DirectoryLockFailed() override; +}; + +class PrepareDatastoreOp::LoadDataOp final + : public ConnectionDatastoreOperationBase { + RefPtr<PrepareDatastoreOp> mPrepareDatastoreOp; + + public: + explicit LoadDataOp(PrepareDatastoreOp* aPrepareDatastoreOp) + : ConnectionDatastoreOperationBase(aPrepareDatastoreOp->mConnection), + mPrepareDatastoreOp(aPrepareDatastoreOp) {} + + private: + ~LoadDataOp() = default; + + nsresult DoDatastoreWork() override; + + void OnSuccess() override; + + void OnFailure(nsresult aResultCode) override; + + void Cleanup() override; +}; + +class PrepareDatastoreOp::CompressFunction final : public mozIStorageFunction { + private: + ~CompressFunction() = default; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION +}; + +class PrepareDatastoreOp::CompressionTypeFunction final + : public mozIStorageFunction { + private: + ~CompressionTypeFunction() = default; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION +}; + +class PrepareObserverOp : public LSRequestBase { + nsCString mOrigin; + + public: + PrepareObserverOp(const LSRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId); + + private: + nsresult Start() override; + + void GetResponse(LSRequestResponse& aResponse) override; +}; + +class LSSimpleRequestBase : public DatastoreOperationBase, + public PBackgroundLSSimpleRequestParent { + protected: + enum class State { + // Just created on the PBackground thread. Next step is StartingRequest. + Initial, + + // Waiting to start/starting request on the PBackground thread. Next step is + // SendingResults. + StartingRequest, + + // Waiting to send/sending results on the PBackground thread. Next step is + // Completed. + SendingResults, + + // All done. + Completed + }; + + const LSSimpleRequestParams mParams; + Maybe<ContentParentId> mContentParentId; + State mState; + + public: + LSSimpleRequestBase(const LSSimpleRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId); + + void Dispatch(); + + protected: + ~LSSimpleRequestBase() override; + + virtual nsresult Start() = 0; + + virtual void GetResponse(LSSimpleRequestResponse& aResponse) = 0; + + private: + bool VerifyRequestParams(); + + nsresult StartRequest(); + + void SendResults(); + + // Common nsIRunnable implementation that subclasses may not override. + NS_IMETHOD + Run() final; + + // IPDL methods. + void ActorDestroy(ActorDestroyReason aWhy) override; +}; + +class PreloadedOp : public LSSimpleRequestBase { + nsCString mOrigin; + + public: + PreloadedOp(const LSSimpleRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId); + + private: + nsresult Start() override; + + void GetResponse(LSSimpleRequestResponse& aResponse) override; +}; + +class GetStateOp : public LSSimpleRequestBase { + nsCString mOrigin; + + public: + GetStateOp(const LSSimpleRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId); + + private: + nsresult Start() override; + + void GetResponse(LSSimpleRequestResponse& aResponse) override; +}; + +/******************************************************************************* + * Other class declarations + ******************************************************************************/ + +struct ArchivedOriginInfo { + OriginAttributes mOriginAttributes; + nsCString mOriginNoSuffix; + + ArchivedOriginInfo(const OriginAttributes& aOriginAttributes, + const nsACString& aOriginNoSuffix) + : mOriginAttributes(aOriginAttributes), + mOriginNoSuffix(aOriginNoSuffix) {} +}; + +class ArchivedOriginScope { + struct Origin { + nsCString mOriginSuffix; + nsCString mOriginNoSuffix; + + Origin(const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix) + : mOriginSuffix(aOriginSuffix), mOriginNoSuffix(aOriginNoSuffix) {} + + const nsACString& OriginSuffix() const { return mOriginSuffix; } + + const nsACString& OriginNoSuffix() const { return mOriginNoSuffix; } + }; + + struct Prefix { + nsCString mOriginNoSuffix; + + explicit Prefix(const nsACString& aOriginNoSuffix) + : mOriginNoSuffix(aOriginNoSuffix) {} + + const nsACString& OriginNoSuffix() const { return mOriginNoSuffix; } + }; + + struct Pattern { + UniquePtr<OriginAttributesPattern> mPattern; + + explicit Pattern(const OriginAttributesPattern& aPattern) + : mPattern(MakeUnique<OriginAttributesPattern>(aPattern)) {} + + Pattern(const Pattern& aOther) + : mPattern(MakeUnique<OriginAttributesPattern>(*aOther.mPattern)) {} + + Pattern(Pattern&& aOther) = default; + + const OriginAttributesPattern& GetPattern() const { + MOZ_ASSERT(mPattern); + return *mPattern; + } + }; + + struct Null {}; + + using DataType = Variant<Origin, Pattern, Prefix, Null>; + + DataType mData; + + public: + static UniquePtr<ArchivedOriginScope> CreateFromOrigin( + const nsACString& aOriginAttrSuffix, const nsACString& aOriginKey); + + static UniquePtr<ArchivedOriginScope> CreateFromPrefix( + const nsACString& aOriginKey); + + static UniquePtr<ArchivedOriginScope> CreateFromPattern( + const OriginAttributesPattern& aPattern); + + static UniquePtr<ArchivedOriginScope> CreateFromNull(); + + bool IsOrigin() const { return mData.is<Origin>(); } + + bool IsPrefix() const { return mData.is<Prefix>(); } + + bool IsPattern() const { return mData.is<Pattern>(); } + + bool IsNull() const { return mData.is<Null>(); } + + const nsACString& OriginSuffix() const { + MOZ_ASSERT(IsOrigin()); + + return mData.as<Origin>().OriginSuffix(); + } + + const nsACString& OriginNoSuffix() const { + MOZ_ASSERT(IsOrigin() || IsPrefix()); + + if (IsOrigin()) { + return mData.as<Origin>().OriginNoSuffix(); + } + return mData.as<Prefix>().OriginNoSuffix(); + } + + const OriginAttributesPattern& GetPattern() const { + MOZ_ASSERT(IsPattern()); + + return mData.as<Pattern>().GetPattern(); + } + + nsLiteralCString GetBindingClause() const; + + nsresult BindToStatement(mozIStorageStatement* aStatement) const; + + bool HasMatches(ArchivedOriginHashtable* aHashtable) const; + + void RemoveMatches(ArchivedOriginHashtable* aHashtable) const; + + private: + // Move constructors + explicit ArchivedOriginScope(const Origin&& aOrigin) : mData(aOrigin) {} + + explicit ArchivedOriginScope(const Pattern&& aPattern) : mData(aPattern) {} + + explicit ArchivedOriginScope(const Prefix&& aPrefix) : mData(aPrefix) {} + + explicit ArchivedOriginScope(const Null&& aNull) : mData(aNull) {} +}; + +class QuotaClient final : public mozilla::dom::quota::Client { + class Observer; + class MatchFunction; + + static QuotaClient* sInstance; + + Mutex mShadowDatabaseMutex MOZ_UNANNOTATED; + + public: + QuotaClient(); + + static nsresult Initialize(); + + static QuotaClient* GetInstance() { + AssertIsOnBackgroundThread(); + + return sInstance; + } + + mozilla::Mutex& ShadowDatabaseMutex() { + MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread()); + + return mShadowDatabaseMutex; + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::QuotaClient, override) + + Type GetType() override; + + Result<UsageInfo, nsresult> InitOrigin(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) override; + + nsresult InitOriginWithoutTracking(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) override; + + Result<UsageInfo, nsresult> GetUsageForOrigin( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) override; + + nsresult AboutToClearOrigins( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope) override; + + void OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) override; + + void ReleaseIOThreadObjects() override; + + void AbortOperationsForLocks( + const DirectoryLockIdTable& aDirectoryLockIds) override; + + void AbortOperationsForProcess(ContentParentId aContentParentId) override; + + void AbortAllOperations() override; + + void StartIdleMaintenance() override; + + void StopIdleMaintenance() override; + + private: + ~QuotaClient() override; + + void InitiateShutdown() override; + bool IsShutdownCompleted() const override; + nsCString GetShutdownStatus() const override; + void ForceKillActors() override; + void FinalizeShutdown() override; + + Result<UniquePtr<ArchivedOriginScope>, nsresult> CreateArchivedOriginScope( + const OriginScope& aOriginScope); + + nsresult PerformDelete(mozIStorageConnection* aConnection, + const nsACString& aSchemaName, + ArchivedOriginScope* aArchivedOriginScope) const; +}; + +class QuotaClient::Observer final : public nsIObserver { + public: + static nsresult Initialize(); + + private: + Observer() { MOZ_ASSERT(NS_IsMainThread()); } + + ~Observer() { MOZ_ASSERT(NS_IsMainThread()); } + + nsresult Init(); + + nsresult Shutdown(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER +}; + +class QuotaClient::MatchFunction final : public mozIStorageFunction { + OriginAttributesPattern mPattern; + + public: + explicit MatchFunction(const OriginAttributesPattern& aPattern) + : mPattern(aPattern) {} + + private: + ~MatchFunction() = default; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION +}; + +/******************************************************************************* + * Helper classes + ******************************************************************************/ + +class MOZ_STACK_CLASS AutoWriteTransaction final { + Connection* mConnection; + Maybe<MutexAutoLock> mShadowDatabaseLock; + bool mShadowWrites; + + public: + explicit AutoWriteTransaction(bool aShadowWrites); + + ~AutoWriteTransaction(); + + nsresult Start(Connection* aConnection); + + nsresult Commit(); + + private: + nsresult LockAndAttachShadowDatabase(Connection* aConnection); + + nsresult DetachShadowDatabaseAndUnlock(); +}; + +/******************************************************************************* + * Globals + ******************************************************************************/ + +#ifdef DEBUG +bool gLocalStorageInitialized = false; +#endif + +using PrepareDatastoreOpArray = + nsTArray<NotNull<CheckedUnsafePtr<PrepareDatastoreOp>>>; + +StaticAutoPtr<PrepareDatastoreOpArray> gPrepareDatastoreOps; + +// nsCStringHashKey with disabled memmove +class nsCStringHashKeyDM : public nsCStringHashKey { + public: + explicit nsCStringHashKeyDM(const nsCStringHashKey::KeyTypePointer aKey) + : nsCStringHashKey(aKey) {} + enum { ALLOW_MEMMOVE = false }; +}; + +// When CheckedUnsafePtr's checking is enabled, it's necessary to ensure that +// the hashtable uses the copy constructor instead of memmove for moving entries +// since memmove will break CheckedUnsafePtr in a memory-corrupting way. +using DatastoreHashKey = + std::conditional<DiagnosticAssertEnabled::value, nsCStringHashKeyDM, + nsCStringHashKey>::type; + +using DatastoreHashtable = + nsBaseHashtable<DatastoreHashKey, NotNull<CheckedUnsafePtr<Datastore>>, + MovingNotNull<CheckedUnsafePtr<Datastore>>>; + +StaticAutoPtr<DatastoreHashtable> gDatastores; + +uint64_t gLastDatastoreId = 0; + +using PreparedDatastoreHashtable = + nsClassHashtable<nsUint64HashKey, PreparedDatastore>; + +StaticAutoPtr<PreparedDatastoreHashtable> gPreparedDatastores; + +using PrivateDatastoreHashtable = + nsClassHashtable<nsCStringHashKey, PrivateDatastore>; + +// Keeps Private Browsing Datastores alive until the private browsing session +// is closed. This is necessary because LocalStorage Private Browsing data is +// (currently) not written to disk and therefore needs to explicitly be kept +// alive in memory so that if a user browses away from a site during a session +// and then back to it that they will still have their data. +// +// The entries are wrapped by PrivateDatastore instances which call +// NoteLivePrivateDatastore and NoteFinishedPrivateDatastore which set and +// clear mHasLivePrivateDatastore which inhibits MaybeClose() from closing the +// datastore (which would discard the data) when there are no active windows +// using LocalStorage for the origin. +// +// The table is cleared when the Private Browsing session is closed, which will +// cause NoteFinishedPrivateDatastore to be called on each Datastore which will +// in turn call MaybeClose which should then discard the Datastore. Or in the +// event of an (unlikely) race where the private browsing windows are still +// being torn down, will cause the Datastore to be discarded when the last +// window actually goes away. +UniquePtr<PrivateDatastoreHashtable> gPrivateDatastores; + +using LiveDatabaseArray = nsTArray<NotNull<CheckedUnsafePtr<Database>>>; + +StaticAutoPtr<LiveDatabaseArray> gLiveDatabases; + +StaticRefPtr<ConnectionThread> gConnectionThread; + +uint64_t gLastObserverId = 0; + +using PreparedObserverHashtable = nsRefPtrHashtable<nsUint64HashKey, Observer>; + +StaticAutoPtr<PreparedObserverHashtable> gPreparedObsevers; + +using ObserverHashtable = + nsClassHashtable<nsCStringHashKey, nsTArray<NotNull<Observer*>>>; + +StaticAutoPtr<ObserverHashtable> gObservers; + +Atomic<bool> gShadowWrites(kDefaultShadowWrites); +Atomic<int32_t, Relaxed> gSnapshotPrefill(kDefaultSnapshotPrefill); +Atomic<int32_t, Relaxed> gSnapshotGradualPrefill( + kDefaultSnapshotGradualPrefill); +Atomic<bool> gClientValidation(kDefaultClientValidation); + +using UsageHashtable = nsTHashMap<nsCStringHashKey, int64_t>; + +StaticAutoPtr<ArchivedOriginHashtable> gArchivedOrigins; + +// Can only be touched on the Quota Manager I/O thread. +bool gInitializedShadowStorage = false; + +StaticAutoPtr<LSInitializationInfo> gInitializationInfo; + +bool IsOnGlobalConnectionThread() { + MOZ_ASSERT(gConnectionThread); + return gConnectionThread->IsOnConnectionThread(); +} + +void AssertIsOnGlobalConnectionThread() { + MOZ_ASSERT(gConnectionThread); + gConnectionThread->AssertIsOnConnectionThread(); +} + +already_AddRefed<Datastore> GetDatastore(const nsACString& aOrigin) { + AssertIsOnBackgroundThread(); + + if (gDatastores) { + auto maybeDatastore = gDatastores->MaybeGet(aOrigin); + if (maybeDatastore) { + RefPtr<Datastore> result(std::move(*maybeDatastore).unwrapBasePtr()); + return result.forget(); + } + } + + return nullptr; +} + +nsresult LoadArchivedOrigins() { + AssertIsOnIOThread(); + MOZ_ASSERT(!gArchivedOrigins); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // Ensure that the webappsstore.sqlite is moved to new place. + QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureStorageIsInitialized())); + + QM_TRY_INSPECT(const auto& connection, CreateArchiveStorageConnection( + quotaManager->GetStoragePath())); + + if (!connection) { + gArchivedOrigins = new ArchivedOriginHashtable(); + return NS_OK; + } + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, connection, CreateStatement, + "SELECT DISTINCT originAttributes, originKey " + "FROM webappsstore2;"_ns)); + + auto archivedOrigins = MakeUnique<ArchivedOriginHashtable>(); + + // XXX Actually, this could use a hashtable variant of + // CollectElementsWhileHasResult + QM_TRY(quota::CollectWhileHasResult( + *stmt, [&archivedOrigins](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& originSuffix, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt, + GetUTF8String, 0)); + QM_TRY_INSPECT(const auto& originNoSuffix, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt, + GetUTF8String, 1)); + + const nsCString hashKey = + GetArchivedOriginHashKey(originSuffix, originNoSuffix); + + OriginAttributes originAttributes; + QM_TRY(OkIf(originAttributes.PopulateFromSuffix(originSuffix)), + Err(NS_ERROR_FAILURE)); + + archivedOrigins->InsertOrUpdate( + hashKey, + MakeUnique<ArchivedOriginInfo>(originAttributes, originNoSuffix)); + + return Ok{}; + })); + + gArchivedOrigins = archivedOrigins.release(); + return NS_OK; +} + +Result<int64_t, nsresult> GetUsage(mozIStorageConnection& aConnection, + ArchivedOriginScope* aArchivedOriginScope) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT( + const auto& stmt, + ([aArchivedOriginScope, + &aConnection]() -> Result<nsCOMPtr<mozIStorageStatement>, nsresult> { + if (aArchivedOriginScope) { + QM_TRY_RETURN(CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + aConnection, + "SELECT " + "total(utf16Length(key) + utf16Length(value)) " + "FROM webappsstore2 " + "WHERE originKey = :originKey " + "AND originAttributes = :originAttributes;"_ns, + [aArchivedOriginScope](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT( + aArchivedOriginScope->BindToStatement(&stmt))); + return Ok{}; + })); + } + + QM_TRY_RETURN(CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + aConnection, "SELECT usage FROM database"_ns)); + }())); + + QM_TRY(OkIf(stmt), Err(NS_ERROR_FAILURE)); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 0)); +} + +void ShadowWritesPrefChangedCallback(const char* aPrefName, void* aClosure) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPrefName, kShadowWritesPref)); + MOZ_ASSERT(!aClosure); + + gShadowWrites = Preferences::GetBool(aPrefName, kDefaultShadowWrites); +} + +void SnapshotPrefillPrefChangedCallback(const char* aPrefName, void* aClosure) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPrefName, kSnapshotPrefillPref)); + MOZ_ASSERT(!aClosure); + + int32_t snapshotPrefill = + Preferences::GetInt(aPrefName, kDefaultSnapshotPrefill); + + // The magic -1 is for use only by tests. + if (snapshotPrefill == -1) { + snapshotPrefill = INT32_MAX; + } + + gSnapshotPrefill = snapshotPrefill; +} + +void SnapshotGradualPrefillPrefChangedCallback(const char* aPrefName, + void* aClosure) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPrefName, kSnapshotGradualPrefillPref)); + MOZ_ASSERT(!aClosure); + + int32_t snapshotGradualPrefill = + Preferences::GetInt(aPrefName, kDefaultSnapshotGradualPrefill); + + // The magic -1 is for use only by tests. + if (snapshotGradualPrefill == -1) { + snapshotGradualPrefill = INT32_MAX; + } + + gSnapshotGradualPrefill = snapshotGradualPrefill; +} + +int64_t GetSnapshotPeakUsagePreincrement(bool aInitial) { + return aInitial ? StaticPrefs:: + dom_storage_snapshot_peak_usage_initial_preincrement() + : StaticPrefs:: + dom_storage_snapshot_peak_usage_gradual_preincrement(); +} + +int64_t GetSnapshotPeakUsageReducedPreincrement(bool aInitial) { + return aInitial + ? StaticPrefs:: + dom_storage_snapshot_peak_usage_reduced_initial_preincrement() + : StaticPrefs:: + dom_storage_snapshot_peak_usage_reduced_gradual_preincrement(); +} + +void ClientValidationPrefChangedCallback(const char* aPrefName, + void* aClosure) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPrefName, kClientValidationPref)); + MOZ_ASSERT(!aClosure); + + gClientValidation = Preferences::GetBool(aPrefName, kDefaultClientValidation); +} + +template <typename Condition> +void InvalidatePrepareDatastoreOpsMatching(const Condition& aCondition) { + if (!gPrepareDatastoreOps) { + return; + } + + for (const auto& prepareDatastoreOp : *gPrepareDatastoreOps) { + if (aCondition(*prepareDatastoreOp)) { + prepareDatastoreOp->Invalidate(); + } + } +} + +template <typename Condition> +void InvalidatePreparedDatastoresMatching(const Condition& aCondition) { + if (!gPreparedDatastores) { + return; + } + + for (const auto& preparedDatastore : gPreparedDatastores->Values()) { + MOZ_ASSERT(preparedDatastore); + + if (aCondition(*preparedDatastore)) { + preparedDatastore->Invalidate(); + } + } +} + +template <typename Condition> +nsTArray<RefPtr<Database>> CollectDatabasesMatching(Condition aCondition) { + AssertIsOnBackgroundThread(); + + if (!gLiveDatabases) { + return nsTArray<RefPtr<Database>>{}; + } + + nsTArray<RefPtr<Database>> databases; + + for (const auto& database : *gLiveDatabases) { + if (aCondition(*database)) { + databases.AppendElement(database.get()); + } + } + + return databases; +} + +template <typename Condition> +void RequestAllowToCloseDatabasesMatching(Condition aCondition) { + AssertIsOnBackgroundThread(); + + nsTArray<RefPtr<Database>> databases = CollectDatabasesMatching(aCondition); + + for (const auto& database : databases) { + MOZ_ASSERT(database); + + database->RequestAllowToClose(); + } +} + +void ForceKillAllDatabases() { + AssertIsOnBackgroundThread(); + + nsTArray<RefPtr<Database>> databases = + CollectDatabasesMatching([](const auto&) { return true; }); + + for (const auto& database : databases) { + MOZ_ASSERT(database); + + database->ForceKill(); + } +} + +bool VerifyPrincipalInfo(const PrincipalInfo& aPrincipalInfo, + const PrincipalInfo& aStoragePrincipalInfo, + bool aCheckClientPrincipal) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(aPrincipalInfo))) { + return false; + } + + // Note that the client prinicpal could have a different spec than the node + // principal but they should have the same origin. It's because the client + // could be initialized when opening the initial about:blank document and pass + // to the newly opened window and reuse over there if the new window has the + // same origin as the initial about:blank document. But, the FilePath could be + // different. Therefore, we have to ignore comparing the Spec of the + // principals if we are verifying clinet principal here. Also, when + // document.domain is set, client principal won't get it. So, we don't compare + // domain for client princpal too. + bool result = aCheckClientPrincipal + ? StoragePrincipalHelper:: + VerifyValidClientPrincipalInfoForPrincipalInfo( + aStoragePrincipalInfo, aPrincipalInfo) + : StoragePrincipalHelper:: + VerifyValidStoragePrincipalInfoForPrincipalInfo( + aStoragePrincipalInfo, aPrincipalInfo); + if (NS_WARN_IF(!result)) { + return false; + } + + return true; +} + +bool VerifyClientId(const Maybe<ContentParentId>& aContentParentId, + const Maybe<PrincipalInfo>& aPrincipalInfo, + const Maybe<nsID>& aClientId) { + AssertIsOnBackgroundThread(); + + if (gClientValidation) { + if (NS_WARN_IF(aClientId.isNothing())) { + return false; + } + + if (NS_WARN_IF(aPrincipalInfo.isNothing())) { + return false; + } + + RefPtr<ClientManagerService> svc = ClientManagerService::GetInstance(); + if (svc && NS_WARN_IF(!svc->HasWindow( + aContentParentId, aPrincipalInfo.ref(), aClientId.ref()))) { + return false; + } + } + + return true; +} + +bool VerifyOriginKey(const nsACString& aOriginKey, + const PrincipalInfo& aPrincipalInfo) { + AssertIsOnBackgroundThread(); + + QM_TRY_INSPECT((const auto& [originAttrSuffix, originKey]), + GenerateOriginKey2(aPrincipalInfo), false); + + Unused << originAttrSuffix; + + QM_TRY(OkIf(originKey == aOriginKey), false, + ([&originKey = originKey, &aOriginKey](const auto) { + LS_WARNING("originKey (%s) doesn't match passed one (%s)!", + originKey.get(), nsCString(aOriginKey).get()); + })); + + return true; +} + +LSInitializationInfo& MutableInitializationInfoRef(const CreateIfNonExistent&) { + if (!gInitializationInfo) { + gInitializationInfo = new LSInitializationInfo(); + } + return *gInitializationInfo; +} + +template <typename Func> +auto ExecuteOriginInitialization(const nsACString& aOrigin, + const LSOriginInitialization aInitialization, + const nsACString& aContext, Func&& aFunc) + -> std::invoke_result_t<Func, const FirstInitializationAttempt< + LSOriginInitialization, Nothing>&> { + return ExecuteInitialization( + MutableInitializationInfoRef(CreateIfNonExistent{}) + .MutableOriginInitializationInfoRef(aOrigin, CreateIfNonExistent{}), + aInitialization, aContext, std::forward<Func>(aFunc)); +} + +} // namespace + +/******************************************************************************* + * Exported functions + ******************************************************************************/ + +void InitializeLocalStorage() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!gLocalStorageInitialized); + + // XXX Isn't this redundant? It's already done in InitializeQuotaManager. + if (!QuotaManager::IsRunningGTests()) { + // This service has to be started on the main thread currently. + const nsCOMPtr<mozIStorageService> ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); + + QM_WARNONLY_TRY(OkIf(ss)); + } + + QM_WARNONLY_TRY(QM_TO_RESULT(QuotaClient::Initialize())); + + Preferences::RegisterCallbackAndCall(ShadowWritesPrefChangedCallback, + kShadowWritesPref); + + Preferences::RegisterCallbackAndCall(SnapshotPrefillPrefChangedCallback, + kSnapshotPrefillPref); + + Preferences::RegisterCallbackAndCall( + SnapshotGradualPrefillPrefChangedCallback, kSnapshotGradualPrefillPref); + + Preferences::RegisterCallbackAndCall(ClientValidationPrefChangedCallback, + kClientValidationPref); + +#ifdef DEBUG + gLocalStorageInitialized = true; +#endif +} + +PBackgroundLSDatabaseParent* AllocPBackgroundLSDatabaseParent( + const PrincipalInfo& aPrincipalInfo, const uint32_t& aPrivateBrowsingId, + const uint64_t& aDatastoreId) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + if (NS_WARN_IF(!gPreparedDatastores)) { + MOZ_ASSERT_UNLESS_FUZZING(false); + return nullptr; + } + + PreparedDatastore* preparedDatastore = gPreparedDatastores->Get(aDatastoreId); + if (NS_WARN_IF(!preparedDatastore)) { + MOZ_ASSERT_UNLESS_FUZZING(false); + return nullptr; + } + + // If we ever decide to return null from this point on, we need to make sure + // that the datastore is closed and the prepared datastore is removed from the + // gPreparedDatastores hashtable. + // We also assume that IPDL must call RecvPBackgroundLSDatabaseConstructor + // once we return a valid actor in this method. + + RefPtr<Database> database = + new Database(aPrincipalInfo, preparedDatastore->GetContentParentId(), + preparedDatastore->Origin(), aPrivateBrowsingId); + + // Transfer ownership to IPDL. + return database.forget().take(); +} + +bool RecvPBackgroundLSDatabaseConstructor(PBackgroundLSDatabaseParent* aActor, + const PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const uint64_t& aDatastoreId) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(gPreparedDatastores); + MOZ_ASSERT(gPreparedDatastores->Get(aDatastoreId)); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + + // The actor is now completely built (it has a manager, channel and it's + // registered as a subprotocol). + // ActorDestroy will be called if we fail here. + + mozilla::UniquePtr<PreparedDatastore> preparedDatastore; + gPreparedDatastores->Remove(aDatastoreId, &preparedDatastore); + MOZ_ASSERT(preparedDatastore); + + auto* database = static_cast<Database*>(aActor); + + database->SetActorAlive(&preparedDatastore->MutableDatastoreRef()); + + // It's possible that AbortOperationsForLocks was called before the database + // actor was created and became live. Let the child know that the database is + // no longer valid. + if (preparedDatastore->IsInvalidated()) { + database->RequestAllowToClose(); + } + + return true; +} + +bool DeallocPBackgroundLSDatabaseParent(PBackgroundLSDatabaseParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<Database> actor = dont_AddRef(static_cast<Database*>(aActor)); + + return true; +} + +PBackgroundLSObserverParent* AllocPBackgroundLSObserverParent( + const uint64_t& aObserverId) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + if (NS_WARN_IF(!gPreparedObsevers)) { + MOZ_ASSERT_UNLESS_FUZZING(false); + return nullptr; + } + + RefPtr<Observer> observer = gPreparedObsevers->Get(aObserverId); + if (NS_WARN_IF(!observer)) { + MOZ_ASSERT_UNLESS_FUZZING(false); + return nullptr; + } + + // observer->SetObject(this); + + // Transfer ownership to IPDL. + return observer.forget().take(); +} + +bool RecvPBackgroundLSObserverConstructor(PBackgroundLSObserverParent* aActor, + const uint64_t& aObserverId) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(gPreparedObsevers); + MOZ_ASSERT(gPreparedObsevers->GetWeak(aObserverId)); + + RefPtr<Observer> observer; + gPreparedObsevers->Remove(aObserverId, observer.StartAssignment()); + + if (!gPreparedObsevers->Count()) { + gPreparedObsevers = nullptr; + } + + if (!gObservers) { + gObservers = new ObserverHashtable(); + } + + const auto notNullObserver = WrapNotNull(observer.get()); + + nsTArray<NotNull<Observer*>>* const array = + gObservers->GetOrInsertNew(notNullObserver->Origin()); + array->AppendElement(notNullObserver); + + if (RefPtr<Datastore> datastore = GetDatastore(observer->Origin())) { + datastore->NoteChangedObserverArray(*array); + } + + return true; +} + +bool DeallocPBackgroundLSObserverParent(PBackgroundLSObserverParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<Observer> actor = dont_AddRef(static_cast<Observer*>(aActor)); + + return true; +} + +PBackgroundLSRequestParent* AllocPBackgroundLSRequestParent( + PBackgroundParent* aBackgroundActor, const LSRequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != LSRequestParams::T__None); + + if (NS_WARN_IF(!NextGenLocalStorageEnabled())) { + return nullptr; + } + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + Maybe<ContentParentId> contentParentId; + + uint64_t childID = BackgroundParent::GetChildID(aBackgroundActor); + if (childID) { + contentParentId = Some(ContentParentId(childID)); + } + + RefPtr<LSRequestBase> actor; + + switch (aParams.type()) { + case LSRequestParams::TLSRequestPreloadDatastoreParams: + case LSRequestParams::TLSRequestPrepareDatastoreParams: { + RefPtr<PrepareDatastoreOp> prepareDatastoreOp = + new PrepareDatastoreOp(aParams, contentParentId); + + if (!gPrepareDatastoreOps) { + gPrepareDatastoreOps = new PrepareDatastoreOpArray(); + } + gPrepareDatastoreOps->AppendElement( + WrapNotNullUnchecked(prepareDatastoreOp.get())); + + actor = std::move(prepareDatastoreOp); + + break; + } + + case LSRequestParams::TLSRequestPrepareObserverParams: { + RefPtr<PrepareObserverOp> prepareObserverOp = + new PrepareObserverOp(aParams, contentParentId); + + actor = std::move(prepareObserverOp); + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +bool RecvPBackgroundLSRequestConstructor(PBackgroundLSRequestParent* aActor, + const LSRequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != LSRequestParams::T__None); + MOZ_ASSERT(NextGenLocalStorageEnabled()); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + + // The actor is now completely built. + + auto* op = static_cast<LSRequestBase*>(aActor); + + op->Dispatch(); + + return true; +} + +bool DeallocPBackgroundLSRequestParent(PBackgroundLSRequestParent* aActor) { + AssertIsOnBackgroundThread(); + + // Transfer ownership back from IPDL. + RefPtr<LSRequestBase> actor = + dont_AddRef(static_cast<LSRequestBase*>(aActor)); + + return true; +} + +PBackgroundLSSimpleRequestParent* AllocPBackgroundLSSimpleRequestParent( + PBackgroundParent* aBackgroundActor, const LSSimpleRequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != LSSimpleRequestParams::T__None); + + if (NS_WARN_IF(!NextGenLocalStorageEnabled())) { + return nullptr; + } + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + Maybe<ContentParentId> contentParentId; + + uint64_t childID = BackgroundParent::GetChildID(aBackgroundActor); + if (childID) { + contentParentId = Some(ContentParentId(childID)); + } + + RefPtr<LSSimpleRequestBase> actor; + + switch (aParams.type()) { + case LSSimpleRequestParams::TLSSimpleRequestPreloadedParams: { + RefPtr<PreloadedOp> preloadedOp = + new PreloadedOp(aParams, contentParentId); + + actor = std::move(preloadedOp); + + break; + } + + case LSSimpleRequestParams::TLSSimpleRequestGetStateParams: { + RefPtr<GetStateOp> getStateOp = new GetStateOp(aParams, contentParentId); + + actor = std::move(getStateOp); + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +bool RecvPBackgroundLSSimpleRequestConstructor( + PBackgroundLSSimpleRequestParent* aActor, + const LSSimpleRequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != LSSimpleRequestParams::T__None); + MOZ_ASSERT(NextGenLocalStorageEnabled()); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + + // The actor is now completely built. + + auto* op = static_cast<LSSimpleRequestBase*>(aActor); + + op->Dispatch(); + + return true; +} + +bool DeallocPBackgroundLSSimpleRequestParent( + PBackgroundLSSimpleRequestParent* aActor) { + AssertIsOnBackgroundThread(); + + // Transfer ownership back from IPDL. + RefPtr<LSSimpleRequestBase> actor = + dont_AddRef(static_cast<LSSimpleRequestBase*>(aActor)); + + return true; +} + +bool RecvLSClearPrivateBrowsing() { + AssertIsOnBackgroundThread(); + + gPrivateDatastores = nullptr; + + if (gDatastores) { + for (const auto& datastore : gDatastores->Values()) { + if (datastore->PrivateBrowsingId()) { + datastore->Clear(nullptr); + } + } + } + + return true; +} + +namespace localstorage { + +already_AddRefed<mozilla::dom::quota::Client> CreateQuotaClient() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + RefPtr<QuotaClient> client = new QuotaClient(); + return client.forget(); +} + +} // namespace localstorage + +/******************************************************************************* + * DatastoreWriteOptimizer + ******************************************************************************/ + +void DatastoreWriteOptimizer::ApplyAndReset( + nsTArray<LSItemInfo>& aOrderedItems) { + AssertIsOnOwningThread(); + + // The mWriteInfos hash table contains all write infos, but it keeps them in + // an arbitrary order, which means write infos need to be sorted before being + // processed. However, the order is not important for deletions and normal + // updates. Usually, filtering out deletions and updates would require extra + // work, but we have to check the hash table for each ordered item anyway, so + // we can remove the write info if it is a deletion or update without adding + // extra overhead. In the end, only insertions need to be sorted before being + // processed. + + if (mTruncateInfo) { + aOrderedItems.Clear(); + mTruncateInfo = nullptr; + } + + for (int32_t index = aOrderedItems.Length() - 1; index >= 0; index--) { + LSItemInfo& item = aOrderedItems[index]; + + if (auto entry = mWriteInfos.Lookup(item.key())) { + WriteInfo* writeInfo = entry->get(); + + switch (writeInfo->GetType()) { + case WriteInfo::DeleteItem: + aOrderedItems.RemoveElementAt(index); + entry.Remove(); + break; + + case WriteInfo::UpdateItem: { + auto updateItemInfo = static_cast<UpdateItemInfo*>(writeInfo); + if (updateItemInfo->UpdateWithMove()) { + // See the comment in LSWriteOptimizer::InsertItem for more details + // about the UpdateWithMove flag. + + aOrderedItems.RemoveElementAt(index); + entry.Data() = MakeUnique<InsertItemInfo>( + updateItemInfo->SerialNumber(), updateItemInfo->GetKey(), + updateItemInfo->GetValue()); + } else { + item.value() = updateItemInfo->GetValue(); + entry.Remove(); + } + break; + } + + case WriteInfo::InsertItem: + break; + + default: + MOZ_CRASH("Bad type!"); + } + } + } + + nsTArray<NotNull<WriteInfo*>> writeInfos; + GetSortedWriteInfos(writeInfos); + + for (WriteInfo* writeInfo : writeInfos) { + MOZ_ASSERT(writeInfo->GetType() == WriteInfo::InsertItem); + + auto insertItemInfo = static_cast<InsertItemInfo*>(writeInfo); + + LSItemInfo* itemInfo = aOrderedItems.AppendElement(); + itemInfo->key() = insertItemInfo->GetKey(); + itemInfo->value() = insertItemInfo->GetValue(); + } + + mWriteInfos.Clear(); +} + +/******************************************************************************* + * ConnectionWriteOptimizer + ******************************************************************************/ + +Result<int64_t, nsresult> ConnectionWriteOptimizer::Perform( + Connection* aConnection, bool aShadowWrites) { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(aConnection); + + // The order of elements is not stored in the database, so write infos don't + // need to be sorted before being processed. + + if (mTruncateInfo) { + QM_TRY(MOZ_TO_RESULT(PerformTruncate(aConnection, aShadowWrites))); + } + + for (const auto& entry : mWriteInfos) { + const WriteInfo* const writeInfo = entry.GetWeak(); + + switch (writeInfo->GetType()) { + case WriteInfo::InsertItem: + case WriteInfo::UpdateItem: { + const auto* const insertItemInfo = + static_cast<const InsertItemInfo*>(writeInfo); + + QM_TRY(MOZ_TO_RESULT(PerformInsertOrUpdate( + aConnection, aShadowWrites, insertItemInfo->GetKey(), + insertItemInfo->GetValue()))); + + break; + } + + case WriteInfo::DeleteItem: { + const auto* const deleteItemInfo = + static_cast<const DeleteItemInfo*>(writeInfo); + + QM_TRY(MOZ_TO_RESULT(PerformDelete(aConnection, aShadowWrites, + deleteItemInfo->GetKey()))); + + break; + } + + default: + MOZ_CRASH("Bad type!"); + } + } + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "UPDATE database " + "SET usage = usage + :delta"_ns, + [this](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName("delta"_ns, mTotalDelta))); + + return Ok{}; + }))); + + QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + aConnection->MutableStorageConnection(), + "SELECT usage FROM database"_ns)); + + QM_TRY(OkIf(stmt), Err(NS_ERROR_FAILURE)); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt64, 0)); +} + +nsresult ConnectionWriteOptimizer::PerformInsertOrUpdate( + Connection* aConnection, bool aShadowWrites, const nsAString& aKey, + const LSValue& aValue) { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(aConnection); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "INSERT OR REPLACE INTO data (key, utf16_length, conversion_type, " + "compression_type, value) " + "VALUES(:key, :utf16_length, :conversion_type, :compression_type, :value)"_ns, + [&aKey, &aValue](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindStringByName("key"_ns, aKey))); + QM_TRY(MOZ_TO_RESULT( + stmt.BindInt32ByName("utf16_length"_ns, aValue.UTF16Length()))); + QM_TRY(MOZ_TO_RESULT(stmt.BindInt32ByName( + "conversion_type"_ns, + static_cast<int32_t>(aValue.GetConversionType())))); + QM_TRY(MOZ_TO_RESULT(stmt.BindInt32ByName( + "compression_type"_ns, + static_cast<int32_t>(aValue.GetCompressionType())))); + + if (0u == aValue.Length()) { // Otherwise empty string becomes null + QM_TRY(MOZ_TO_RESULT( + stmt.BindUTF8StringByName("value"_ns, aValue.AsCString()))); + } else { + QM_TRY(MOZ_TO_RESULT( + stmt.BindUTF8StringAsBlobByName("value"_ns, aValue.AsCString()))); + } + + return Ok{}; + }))); + + if (!aShadowWrites) { + return NS_OK; + } + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "INSERT OR REPLACE INTO shadow.webappsstore2 " + "(originAttributes, originKey, scope, key, value) " + "VALUES (:originAttributes, :originKey, :scope, :key, :value) "_ns, + [&aConnection, &aKey, &aValue](auto& stmt) -> Result<Ok, nsresult> { + using ConversionType = LSValue::ConversionType; + using CompressionType = LSValue::CompressionType; + + const ArchivedOriginScope* const archivedOriginScope = + aConnection->GetArchivedOriginScope(); + + QM_TRY(MOZ_TO_RESULT(archivedOriginScope->BindToStatement(&stmt))); + + QM_TRY(MOZ_TO_RESULT(stmt.BindUTF8StringByName( + "scope"_ns, Scheme0Scope(archivedOriginScope->OriginSuffix(), + archivedOriginScope->OriginNoSuffix())))); + + QM_TRY(MOZ_TO_RESULT(stmt.BindStringByName("key"_ns, aKey))); + + bool isCompressed = + CompressionType::UNCOMPRESSED != aValue.GetCompressionType(); + bool isAlreadyConverted = + ConversionType::NONE != aValue.GetConversionType(); + + nsCString buffer; + const nsCString& valueBlob = aValue.AsCString(); + if (isCompressed) { + QM_TRY(OkIf(SnappyUncompress(valueBlob, buffer)), + Err(NS_ERROR_FAILURE)); + } + const nsCString& value = isCompressed ? buffer : valueBlob; + + // For shadow writes, we undo buffer swap and convert destructively + nsCString unconverted; + if (!isAlreadyConverted) { + nsString converted; + QM_TRY(OkIf(PutCStringBytesToString(value, converted)), + Err(NS_ERROR_OUT_OF_MEMORY)); + QM_TRY(OkIf(CopyUTF16toUTF8(converted, unconverted, fallible)), + Err(NS_ERROR_OUT_OF_MEMORY)); // Corrupt invalid data + } + const nsCString& untransformed = + (!isAlreadyConverted) ? unconverted : value; + + QM_TRY(MOZ_TO_RESULT( + stmt.BindUTF8StringByName("value"_ns, untransformed))); + + return Ok{}; + }))); + + return NS_OK; +} + +nsresult ConnectionWriteOptimizer::PerformDelete(Connection* aConnection, + bool aShadowWrites, + const nsAString& aKey) { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(aConnection); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "DELETE FROM data " + "WHERE key = :key;"_ns, + [&aKey](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindStringByName("key"_ns, aKey))); + + return Ok{}; + }))); + + if (!aShadowWrites) { + return NS_OK; + } + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "DELETE FROM shadow.webappsstore2 " + "WHERE originAttributes = :originAttributes " + "AND originKey = :originKey " + "AND key = :key;"_ns, + [&aConnection, &aKey](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT( + aConnection->GetArchivedOriginScope()->BindToStatement(&stmt))); + + QM_TRY(MOZ_TO_RESULT(stmt.BindStringByName("key"_ns, aKey))); + + return Ok{}; + }))); + + return NS_OK; +} + +nsresult ConnectionWriteOptimizer::PerformTruncate(Connection* aConnection, + bool aShadowWrites) { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(aConnection); + + QM_TRY(MOZ_TO_RESULT( + aConnection->ExecuteCachedStatement("DELETE FROM data;"_ns))); + + if (!aShadowWrites) { + return NS_OK; + } + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "DELETE FROM shadow.webappsstore2 " + "WHERE originAttributes = :originAttributes " + "AND originKey = :originKey;"_ns, + [&aConnection](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT( + aConnection->GetArchivedOriginScope()->BindToStatement(&stmt))); + + return Ok{}; + }))); + + return NS_OK; +} + +/******************************************************************************* + * DatastoreOperationBase + ******************************************************************************/ + +/******************************************************************************* + * ConnectionDatastoreOperationBase + ******************************************************************************/ + +ConnectionDatastoreOperationBase::ConnectionDatastoreOperationBase( + Connection* aConnection, bool aEnsureStorageConnection) + : mConnection(aConnection), + mEnsureStorageConnection(aEnsureStorageConnection) { + MOZ_ASSERT(aConnection); +} + +ConnectionDatastoreOperationBase::~ConnectionDatastoreOperationBase() { + MOZ_ASSERT(!mConnection, + "ConnectionDatabaseOperationBase::Cleanup() was not called by a " + "subclass!"); +} + +void ConnectionDatastoreOperationBase::Cleanup() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mConnection); + + mConnection = nullptr; + + NoteComplete(); +} + +void ConnectionDatastoreOperationBase::OnSuccess() { AssertIsOnOwningThread(); } + +void ConnectionDatastoreOperationBase::OnFailure(nsresult aResultCode) { + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aResultCode)); +} + +void ConnectionDatastoreOperationBase::RunOnConnectionThread() { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(mConnection); + MOZ_ASSERT(NS_SUCCEEDED(ResultCode())); + + if (!MayProceedOnNonOwningThread()) { + SetFailureCode(NS_ERROR_ABORT); + } else { + nsresult rv = NS_OK; + + // The boolean flag is only used by the CloseOp to avoid creating empty + // databases. + if (mEnsureStorageConnection) { + rv = mConnection->EnsureStorageConnection(); + if (NS_WARN_IF(NS_FAILED(rv))) { + SetFailureCode(rv); + } else { + MOZ_ASSERT(mConnection->HasStorageConnection()); + } + } + + if (NS_SUCCEEDED(rv)) { + rv = DoDatastoreWork(); + if (NS_FAILED(rv)) { + SetFailureCode(rv); + } + } + } + + MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +void ConnectionDatastoreOperationBase::RunOnOwningThread() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mConnection); + + if (!MayProceed()) { + MaybeSetFailureCode(NS_ERROR_ABORT); + } + + if (NS_SUCCEEDED(ResultCode())) { + OnSuccess(); + } else { + OnFailure(ResultCode()); + } + + Cleanup(); +} + +NS_IMETHODIMP +ConnectionDatastoreOperationBase::Run() { + if (IsOnGlobalConnectionThread()) { + RunOnConnectionThread(); + } else { + RunOnOwningThread(); + } + + return NS_OK; +} + +/******************************************************************************* + * Connection implementation + ******************************************************************************/ + +Connection::Connection(ConnectionThread* aConnectionThread, + const OriginMetadata& aOriginMetadata, + UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope, + bool aDatabaseWasNotAvailable) + : mConnectionThread(aConnectionThread), + mQuotaClient(QuotaClient::GetInstance()), + mArchivedOriginScope(std::move(aArchivedOriginScope)), + mOriginMetadata(aOriginMetadata), + mDatabaseWasNotAvailable(aDatabaseWasNotAvailable), + mHasCreatedDatabase(false), + mFlushScheduled(false) +#ifdef DEBUG + , + mInUpdateBatch(false), + mFinished(false) +#endif +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!aOriginMetadata.mGroup.IsEmpty()); + MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty()); +} + +Connection::~Connection() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mFlushScheduled); + MOZ_ASSERT(!mInUpdateBatch); + MOZ_ASSERT(mFinished); +} + +void Connection::Dispatch(ConnectionDatastoreOperationBase* aOp) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mConnectionThread); + + MOZ_ALWAYS_SUCCEEDS( + mConnectionThread->mThread->Dispatch(aOp, NS_DISPATCH_NORMAL)); +} + +void Connection::Close(nsIRunnable* aCallback) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aCallback); + + if (mFlushScheduled) { + MOZ_ASSERT(mFlushTimer); + MOZ_ALWAYS_SUCCEEDS(mFlushTimer->Cancel()); + + Flush(); + + mFlushTimer = nullptr; + } + + RefPtr<CloseOp> op = new CloseOp(this, aCallback); + + Dispatch(op); +} + +void Connection::SetItem(const nsString& aKey, const LSValue& aValue, + int64_t aDelta, bool aIsNewItem) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mInUpdateBatch); + + if (aIsNewItem) { + mWriteOptimizer.InsertItem(aKey, aValue, aDelta); + } else { + mWriteOptimizer.UpdateItem(aKey, aValue, aDelta); + } +} + +void Connection::RemoveItem(const nsString& aKey, int64_t aDelta) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mInUpdateBatch); + + mWriteOptimizer.DeleteItem(aKey, aDelta); +} + +void Connection::Clear(int64_t aDelta) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mInUpdateBatch); + + mWriteOptimizer.Truncate(aDelta); +} + +void Connection::BeginUpdateBatch() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mInUpdateBatch); + +#ifdef DEBUG + mInUpdateBatch = true; +#endif +} + +void Connection::EndUpdateBatch() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mInUpdateBatch); + + if (mWriteOptimizer.HasWrites() && !mFlushScheduled) { + ScheduleFlush(); + } + +#ifdef DEBUG + mInUpdateBatch = false; +#endif +} + +nsresult Connection::EnsureStorageConnection() { + AssertIsOnGlobalConnectionThread(); + + if (HasStorageConnection()) { + return NS_OK; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + if (!mDatabaseWasNotAvailable || mHasCreatedDatabase) { + QM_TRY_INSPECT(const auto& directoryEntry, + quotaManager->GetDirectoryForOrigin(PERSISTENCE_TYPE_DEFAULT, + Origin())); + + QM_TRY(MOZ_TO_RESULT(directoryEntry->Append( + NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME)))); + + QM_TRY(MOZ_TO_RESULT(directoryEntry->GetPath(mDirectoryPath))); + QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(kDataFileName))); + + QM_TRY_INSPECT( + const auto& databaseFilePath, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, directoryEntry, GetPath)); + + QM_TRY_UNWRAP(auto storageConnection, + GetStorageConnection(databaseFilePath)); + LazyInit(WrapMovingNotNull(std::move(storageConnection))); + + return NS_OK; + } + + RefPtr<InitTemporaryOriginHelper> helper = + new InitTemporaryOriginHelper(mOriginMetadata); + + QM_TRY_INSPECT(const auto& originDirectoryPath, + helper->BlockAndReturnOriginDirectoryPath()); + + QM_TRY_INSPECT(const auto& directoryEntry, + QM_NewLocalFile(originDirectoryPath)); + + QM_TRY(MOZ_TO_RESULT(directoryEntry->Append( + NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME)))); + + QM_TRY(MOZ_TO_RESULT(directoryEntry->GetPath(mDirectoryPath))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directoryEntry, Exists)); + + if (!exists) { + QM_TRY( + MOZ_TO_RESULT(directoryEntry->Create(nsIFile::DIRECTORY_TYPE, 0755))); + } + + QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(kDataFileName))); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directoryEntry, Exists)); + + MOZ_ASSERT(!exists); + } +#endif + + QM_TRY_INSPECT(const auto& usageFile, GetUsageFile(mDirectoryPath)); + + nsCOMPtr<mozIStorageConnection> storageConnection; + + auto autoRemove = MakeScopeExit([&storageConnection, &directoryEntry] { + if (storageConnection) { + MOZ_ALWAYS_SUCCEEDS(storageConnection->Close()); + } + + nsresult rv = directoryEntry->Remove(false); + if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + NS_WARNING("Failed to remove database file!"); + } + }); + + QM_TRY_UNWRAP(storageConnection, + CreateStorageConnection(*directoryEntry, *usageFile, Origin(), + [] { MOZ_ASSERT_UNREACHABLE(); })); + + MOZ_ASSERT(mQuotaClient); + + MutexAutoLock shadowDatabaseLock(mQuotaClient->ShadowDatabaseMutex()); + + nsCOMPtr<mozIStorageConnection> shadowConnection; + if (!gInitializedShadowStorage) { + QM_TRY_UNWRAP(shadowConnection, + CreateShadowStorageConnection(quotaManager->GetBasePath())); + + gInitializedShadowStorage = true; + } + + autoRemove.release(); + + if (!mHasCreatedDatabase) { + mHasCreatedDatabase = true; + } + + LazyInit(WrapMovingNotNull(std::move(storageConnection))); + + return NS_OK; +} + +void Connection::CloseStorageConnection() { + AssertIsOnGlobalConnectionThread(); + + CachingDatabaseConnection::Close(); +} + +nsresult Connection::BeginWriteTransaction() { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(HasStorageConnection()); + + QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("BEGIN IMMEDIATE;"_ns))); + + return NS_OK; +} + +nsresult Connection::CommitWriteTransaction() { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(HasStorageConnection()); + + QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("COMMIT;"_ns))); + + return NS_OK; +} + +nsresult Connection::RollbackWriteTransaction() { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(HasStorageConnection()); + + QM_TRY_INSPECT(const auto& stmt, BorrowCachedStatement("ROLLBACK;"_ns)); + + // This may fail if SQLite already rolled back the transaction so ignore any + // errors. + Unused << stmt->Execute(); + + return NS_OK; +} + +void Connection::ScheduleFlush() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mWriteOptimizer.HasWrites()); + MOZ_ASSERT(!mFlushScheduled); + + if (!mFlushTimer) { + mFlushTimer = NS_NewTimer(); + MOZ_ASSERT(mFlushTimer); + } + + MOZ_ALWAYS_SUCCEEDS(mFlushTimer->InitWithNamedFuncCallback( + FlushTimerCallback, this, kFlushTimeoutMs, nsITimer::TYPE_ONE_SHOT, + "Connection::FlushTimerCallback")); + + mFlushScheduled = true; +} + +void Connection::Flush() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mFlushScheduled); + + if (mWriteOptimizer.HasWrites()) { + RefPtr<FlushOp> op = new FlushOp(this, std::move(mWriteOptimizer)); + + Dispatch(op); + } + + mFlushScheduled = false; +} + +// static +void Connection::FlushTimerCallback(nsITimer* aTimer, void* aClosure) { + MOZ_ASSERT(aClosure); + + auto* self = static_cast<Connection*>(aClosure); + MOZ_ASSERT(self); + MOZ_ASSERT(self->mFlushScheduled); + + self->Flush(); +} + +Result<nsString, nsresult> +Connection::InitTemporaryOriginHelper::BlockAndReturnOriginDirectoryPath() { + AssertIsOnGlobalConnectionThread(); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + MOZ_ALWAYS_SUCCEEDS( + quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)); + + mozilla::MonitorAutoLock lock(mMonitor); + while (mWaiting) { + lock.Wait(); + } + + QM_TRY(MOZ_TO_RESULT(mIOThreadResultCode)); + + return mOriginDirectoryPath; +} + +nsresult Connection::InitTemporaryOriginHelper::RunOnIOThread() { + AssertIsOnIOThread(); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_INSPECT(const auto& directoryEntry, + quotaManager + ->EnsureTemporaryOriginIsInitialized( + PERSISTENCE_TYPE_DEFAULT, mOriginMetadata) + .map([](const auto& res) { return res.first; })); + + QM_TRY(MOZ_TO_RESULT(directoryEntry->GetPath(mOriginDirectoryPath))); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::InitTemporaryOriginHelper::Run() { + AssertIsOnIOThread(); + + nsresult rv = RunOnIOThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + mIOThreadResultCode = rv; + } + + mozilla::MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mWaiting); + + mWaiting = false; + lock.Notify(); + + return NS_OK; +} + +Connection::FlushOp::FlushOp(Connection* aConnection, + ConnectionWriteOptimizer&& aWriteOptimizer) + : ConnectionDatastoreOperationBase(aConnection), + mWriteOptimizer(std::move(aWriteOptimizer)), + mShadowWrites(gShadowWrites) {} + +nsresult Connection::FlushOp::DoDatastoreWork() { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(mConnection); + + AutoWriteTransaction autoWriteTransaction(mShadowWrites); + + QM_TRY(MOZ_TO_RESULT(autoWriteTransaction.Start(mConnection))); + + QM_TRY_INSPECT(const int64_t& usage, + mWriteOptimizer.Perform(mConnection, mShadowWrites)); + + QM_TRY_INSPECT(const auto& usageFile, + GetUsageFile(mConnection->DirectoryPath())); + + QM_TRY_INSPECT(const auto& usageJournalFile, + GetUsageJournalFile(mConnection->DirectoryPath())); + + QM_TRY(MOZ_TO_RESULT(UpdateUsageFile(usageFile, usageJournalFile, usage))); + + QM_TRY(MOZ_TO_RESULT(autoWriteTransaction.Commit())); + + QM_TRY(MOZ_TO_RESULT(usageJournalFile->Remove(false))); + + return NS_OK; +} + +void Connection::FlushOp::Cleanup() { + AssertIsOnOwningThread(); + + mWriteOptimizer.Reset(); + + MOZ_ASSERT(!mWriteOptimizer.HasWrites()); + + ConnectionDatastoreOperationBase::Cleanup(); +} + +nsresult Connection::CloseOp::DoDatastoreWork() { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(mConnection); + + if (mConnection->HasStorageConnection()) { + mConnection->CloseStorageConnection(); + } + + return NS_OK; +} + +void Connection::CloseOp::Cleanup() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mConnection); + + mConnection->mConnectionThread->mConnections.Remove(mConnection->Origin()); + +#ifdef DEBUG + MOZ_ASSERT(!mConnection->mFinished); + mConnection->mFinished = true; +#endif + + nsCOMPtr<nsIRunnable> callback; + mCallback.swap(callback); + + callback->Run(); + + ConnectionDatastoreOperationBase::Cleanup(); +} + +/******************************************************************************* + * ConnectionThread implementation + ******************************************************************************/ + +ConnectionThread::ConnectionThread() { + AssertIsOnOwningThread(); + AssertIsOnBackgroundThread(); + + MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("LS Thread", getter_AddRefs(mThread))); +} + +ConnectionThread::~ConnectionThread() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mConnections.Count()); +} + +bool ConnectionThread::IsOnConnectionThread() { + MOZ_ASSERT(mThread); + + bool current; + return NS_SUCCEEDED(mThread->IsOnCurrentThread(¤t)) && current; +} + +void ConnectionThread::AssertIsOnConnectionThread() { + MOZ_ASSERT(IsOnConnectionThread()); +} + +already_AddRefed<Connection> ConnectionThread::CreateConnection( + const OriginMetadata& aOriginMetadata, + UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope, + bool aDatabaseWasNotAvailable) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty()); + MOZ_ASSERT(!mConnections.Contains(aOriginMetadata.mOrigin)); + + RefPtr<Connection> connection = + new Connection(this, aOriginMetadata, std::move(aArchivedOriginScope), + aDatabaseWasNotAvailable); + mConnections.InsertOrUpdate(aOriginMetadata.mOrigin, RefPtr{connection}); + + return connection.forget(); +} + +void ConnectionThread::Shutdown() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mThread); + + mThread->Shutdown(); +} + +/******************************************************************************* + * Datastore + ******************************************************************************/ + +Datastore::Datastore(const OriginMetadata& aOriginMetadata, + uint32_t aPrivateBrowsingId, int64_t aUsage, + int64_t aSizeOfKeys, int64_t aSizeOfItems, + RefPtr<DirectoryLock>&& aDirectoryLock, + RefPtr<Connection>&& aConnection, + RefPtr<QuotaObject>&& aQuotaObject, + nsTHashMap<nsStringHashKey, LSValue>& aValues, + nsTArray<LSItemInfo>&& aOrderedItems) + : mDirectoryLock(std::move(aDirectoryLock)), + mConnection(std::move(aConnection)), + mQuotaObject(std::move(aQuotaObject)), + mOrderedItems(std::move(aOrderedItems)), + mOriginMetadata(aOriginMetadata), + mPrivateBrowsingId(aPrivateBrowsingId), + mUsage(aUsage), + mUpdateBatchUsage(-1), + mSizeOfKeys(aSizeOfKeys), + mSizeOfItems(aSizeOfItems), + mClosed(false), + mInUpdateBatch(false), + mHasLivePrivateDatastore(false) { + AssertIsOnBackgroundThread(); + + mValues.SwapElements(aValues); +} + +Datastore::~Datastore() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mClosed); +} + +void Datastore::Close() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(!mPrepareDatastoreOps.Count()); + MOZ_ASSERT(!mPreparedDatastores.Count()); + MOZ_ASSERT(!mDatabases.Count()); + MOZ_ASSERT(mDirectoryLock); + + mClosed = true; + + if (IsPersistent()) { + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mQuotaObject); + + // We can't release the directory lock and unregister itself from the + // hashtable until the connection is fully closed. + nsCOMPtr<nsIRunnable> callback = + NewRunnableMethod("dom::Datastore::ConnectionClosedCallback", this, + &Datastore::ConnectionClosedCallback); + mConnection->Close(callback); + } else { + MOZ_ASSERT(!mConnection); + MOZ_ASSERT(!mQuotaObject); + + // There's no connection, so it's safe to release the directory lock and + // unregister itself from the hashtable. + + mDirectoryLock = nullptr; + + CleanupMetadata(); + } +} + +void Datastore::WaitForConnectionToComplete(nsIRunnable* aCallback) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aCallback); + MOZ_ASSERT(!mCompleteCallback); + MOZ_ASSERT(mClosed); + + mCompleteCallback = aCallback; +} + +void Datastore::NoteLivePrepareDatastoreOp( + PrepareDatastoreOp* aPrepareDatastoreOp) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPrepareDatastoreOp); + MOZ_ASSERT(!mPrepareDatastoreOps.Contains(aPrepareDatastoreOp)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mPrepareDatastoreOps.Insert(aPrepareDatastoreOp); +} + +void Datastore::NoteFinishedPrepareDatastoreOp( + PrepareDatastoreOp* aPrepareDatastoreOp) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPrepareDatastoreOp); + MOZ_ASSERT(mPrepareDatastoreOps.Contains(aPrepareDatastoreOp)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mPrepareDatastoreOps.Remove(aPrepareDatastoreOp); + + QuotaManager::MaybeRecordQuotaClientShutdownStep( + quota::Client::LS, "PrepareDatastoreOp finished"_ns); + + MaybeClose(); +} + +void Datastore::NoteLivePrivateDatastore() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mHasLivePrivateDatastore); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mHasLivePrivateDatastore = true; +} + +void Datastore::NoteFinishedPrivateDatastore() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mHasLivePrivateDatastore); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mHasLivePrivateDatastore = false; + + QuotaManager::MaybeRecordQuotaClientShutdownStep( + quota::Client::LS, "PrivateDatastore finished"_ns); + + MaybeClose(); +} + +void Datastore::NoteLivePreparedDatastore( + PreparedDatastore* aPreparedDatastore) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPreparedDatastore); + MOZ_ASSERT(!mPreparedDatastores.Contains(aPreparedDatastore)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mPreparedDatastores.Insert(aPreparedDatastore); +} + +void Datastore::NoteFinishedPreparedDatastore( + PreparedDatastore* aPreparedDatastore) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPreparedDatastore); + MOZ_ASSERT(mPreparedDatastores.Contains(aPreparedDatastore)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mPreparedDatastores.Remove(aPreparedDatastore); + + QuotaManager::MaybeRecordQuotaClientShutdownStep( + quota::Client::LS, "PreparedDatastore finished"_ns); + + MaybeClose(); +} + +bool Datastore::HasOtherProcessDatabases(Database* aDatabase) { + AssertIsOnBackgroundThread(); + + PBackgroundParent* databaseBackgroundActor = aDatabase->Manager(); + + for (Database* database : mDatabases) { + if (database->Manager() != databaseBackgroundActor) { + return true; + } + } + + return false; +} + +void Datastore::NoteLiveDatabase(Database* aDatabase) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(!mDatabases.Contains(aDatabase)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mDatabases.Insert(aDatabase); + + NoteChangedDatabaseMap(); +} + +void Datastore::NoteFinishedDatabase(Database* aDatabase) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(mDatabases.Contains(aDatabase)); + MOZ_ASSERT(!mActiveDatabases.Contains(aDatabase)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mDatabases.Remove(aDatabase); + + NoteChangedDatabaseMap(); + + QuotaManager::MaybeRecordQuotaClientShutdownStep(quota::Client::LS, + "Database finished"_ns); + + MaybeClose(); +} + +void Datastore::NoteActiveDatabase(Database* aDatabase) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(mDatabases.Contains(aDatabase)); + MOZ_ASSERT(!mActiveDatabases.Contains(aDatabase)); + MOZ_ASSERT(!mClosed); + + mActiveDatabases.Insert(aDatabase); +} + +void Datastore::NoteInactiveDatabase(Database* aDatabase) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(mDatabases.Contains(aDatabase)); + MOZ_ASSERT(mActiveDatabases.Contains(aDatabase)); + MOZ_ASSERT(!mClosed); + + mActiveDatabases.Remove(aDatabase); + + if (!mActiveDatabases.Count() && mPendingUsageDeltas.Length()) { + int64_t finalDelta = 0; + + for (auto delta : mPendingUsageDeltas) { + finalDelta += delta; + } + + MOZ_ASSERT(finalDelta <= 0); + + if (finalDelta != 0) { + DebugOnly<bool> ok = UpdateUsage(finalDelta); + MOZ_ASSERT(ok); + } + + mPendingUsageDeltas.Clear(); + } +} + +void Datastore::GetSnapshotLoadInfo(const nsAString& aKey, + bool& aAddKeyToUnknownItems, + nsTHashtable<nsStringHashKey>& aLoadedItems, + nsTArray<LSItemInfo>& aItemInfos, + uint32_t& aNextLoadIndex, + LSSnapshot::LoadState& aLoadState) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(!mInUpdateBatch); + +#ifdef DEBUG + int64_t sizeOfKeys = 0; + int64_t sizeOfItems = 0; + for (auto item : mOrderedItems) { + int64_t sizeOfKey = static_cast<int64_t>(item.key().Length()); + sizeOfKeys += sizeOfKey; + sizeOfItems += sizeOfKey + static_cast<int64_t>(item.value().Length()); + } + MOZ_ASSERT(mSizeOfKeys == sizeOfKeys); + MOZ_ASSERT(mSizeOfItems == sizeOfItems); +#endif + + // Computes load state optimized for current size of keys and items. + // Zero key length and value can be passed to do a quick initial estimation. + // If computed load state is already AllOrderedItems then excluded key length + // and value length can't make it any better. + auto GetLoadState = [&](int64_t aKeyLength, int64_t aValueLength) { + if (mSizeOfKeys - aKeyLength <= gSnapshotPrefill) { + if (mSizeOfItems - aKeyLength - aValueLength <= gSnapshotPrefill) { + return LSSnapshot::LoadState::AllOrderedItems; + } + + return LSSnapshot::LoadState::AllOrderedKeys; + } + + return LSSnapshot::LoadState::Partial; + }; + + // Value for given aKey if aKey is not void (can be void too if value doesn't + // exist for given aKey). + LSValue value; + // If aKey and value are not void, checkKey will be set to true. Once we find + // an item for given aKey in one of the loops below, checkKey is set to false + // to prevent additional comparison of strings (string implementation compares + // string lengths first to avoid char by char comparison if possible). + bool checkKey = false; + + // Avoid additional hash lookup if all ordered items fit into initial prefill + // already. + LSSnapshot::LoadState loadState = GetLoadState(/* aKeyLength */ 0, + /* aValueLength */ 0); + if (loadState != LSSnapshot::LoadState::AllOrderedItems && !aKey.IsVoid()) { + GetItem(aKey, value); + if (!value.IsVoid()) { + // Ok, we have a non void aKey and value. + + // We have to watch for aKey during one of the loops below to exclude it + // from the size computation. The super fast mode (AllOrderedItems) + // doesn't have to do that though. + checkKey = true; + + // We have to compute load state again because aKey length and value + // length is excluded from the size in this case. + loadState = GetLoadState(aKey.Length(), value.Length()); + } + } + + switch (loadState) { + case LSSnapshot::LoadState::AllOrderedItems: { + // We're sending all ordered items, we don't need to check keys because + // mOrderedItems must contain a value for aKey if checkKey is true. + + aItemInfos.AppendElements(mOrderedItems); + + MOZ_ASSERT(aItemInfos.Length() == mValues.Count()); + aNextLoadIndex = mValues.Count(); + + aAddKeyToUnknownItems = false; + + break; + } + + case LSSnapshot::LoadState::AllOrderedKeys: { + // We don't have enough snapshot budget to send all items, but we do have + // enough to send all of the keys and to make a best effort to populate as + // many values as possible. We send void string values once we run out of + // budget. A complicating factor is that we want to make sure that we send + // the value for aKey which is a localStorage read that's triggering this + // request. Since that key can happen anywhere in the list of items, we + // need to handle it specially. + // + // The loop is effectively doing 2 things in parallel: + // + // 1. Looking for the `aKey` to send. This is tracked by `checkKey` + // which is true if there was an `aKey` specified and until we + // populate its value, and false thereafter. + // 2. Sending values until we run out of `size` budget and switch to + // sending void values. `doneSendingValues` tracks when we've run out + // of size budget, with `setVoidValue` tracking whether a value + // should be sent for each turn of the event loop but can be + // overridden when `aKey` is found. + + int64_t size = mSizeOfKeys; + bool setVoidValue = false; + bool doneSendingValues = false; + for (uint32_t index = 0; index < mOrderedItems.Length(); index++) { + const LSItemInfo& item = mOrderedItems[index]; + + const nsString& key = item.key(); + const LSValue& value = item.value(); + + if (checkKey && key == aKey) { + checkKey = false; + setVoidValue = false; + } else if (!setVoidValue) { + if (doneSendingValues) { + setVoidValue = true; + } else { + size += static_cast<int64_t>(value.Length()); + + if (size > gSnapshotPrefill) { + setVoidValue = true; + doneSendingValues = true; + + // We set doneSendingValues to true and that will guard against + // entering this branch during next iterations. So aNextLoadIndex + // is set only once. + aNextLoadIndex = index; + } + } + } + + LSItemInfo* itemInfo = aItemInfos.AppendElement(); + itemInfo->key() = key; + if (setVoidValue) { + itemInfo->value().SetIsVoid(true); + } else { + aLoadedItems.PutEntry(key); + itemInfo->value() = value; + } + } + + aAddKeyToUnknownItems = false; + + break; + } + + case LSSnapshot::LoadState::Partial: { + int64_t size = 0; + for (uint32_t index = 0; index < mOrderedItems.Length(); index++) { + const LSItemInfo& item = mOrderedItems[index]; + + const nsString& key = item.key(); + const LSValue& value = item.value(); + + if (checkKey && key == aKey) { + checkKey = false; + } else { + size += static_cast<int64_t>(key.Length()) + + static_cast<int64_t>(value.Length()); + + if (size > gSnapshotPrefill) { + aNextLoadIndex = index; + break; + } + } + + aLoadedItems.PutEntry(key); + + LSItemInfo* itemInfo = aItemInfos.AppendElement(); + itemInfo->key() = key; + itemInfo->value() = value; + } + + aAddKeyToUnknownItems = false; + + if (!aKey.IsVoid()) { + if (value.IsVoid()) { + aAddKeyToUnknownItems = true; + } else if (checkKey) { + // The item wasn't added in the loop above, add it here. + + LSItemInfo* itemInfo = aItemInfos.AppendElement(); + itemInfo->key() = aKey; + itemInfo->value() = value; + } + } + + MOZ_ASSERT(aItemInfos.Length() < mOrderedItems.Length()); + + break; + } + + default: + MOZ_CRASH("Bad load state value!"); + } + + aLoadState = loadState; +} + +void Datastore::GetItem(const nsAString& aKey, LSValue& aValue) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mClosed); + + if (!mValues.Get(aKey, &aValue)) { + aValue.SetIsVoid(true); + } +} + +void Datastore::GetKeys(nsTArray<nsString>& aKeys) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mClosed); + + for (auto item : mOrderedItems) { + aKeys.AppendElement(item.key()); + } +} + +void Datastore::SetItem(Database* aDatabase, const nsString& aKey, + const LSValue& aValue) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(mInUpdateBatch); + + LSValue oldValue; + GetItem(aKey, oldValue); + + if (oldValue != aValue) { + bool isNewItem = oldValue.IsVoid(); + + NotifySnapshots(aDatabase, aKey, oldValue, /* affectsOrder */ isNewItem); + + mValues.InsertOrUpdate(aKey, aValue); + + int64_t delta; + + if (isNewItem) { + mWriteOptimizer.InsertItem(aKey, aValue); + + int64_t sizeOfKey = static_cast<int64_t>(aKey.Length()); + + delta = sizeOfKey + static_cast<int64_t>(aValue.UTF16Length()); + + mUpdateBatchUsage += delta; + + mSizeOfKeys += sizeOfKey; + mSizeOfItems += sizeOfKey + static_cast<int64_t>(aValue.Length()); + } else { + mWriteOptimizer.UpdateItem(aKey, aValue); + + delta = static_cast<int64_t>(aValue.UTF16Length()) - + static_cast<int64_t>(oldValue.UTF16Length()); + + mUpdateBatchUsage += delta; + + mSizeOfItems += static_cast<int64_t>(aValue.Length()) - + static_cast<int64_t>(oldValue.Length()); + } + + if (IsPersistent()) { + mConnection->SetItem(aKey, aValue, delta, isNewItem); + } + } +} + +void Datastore::RemoveItem(Database* aDatabase, const nsString& aKey) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(mInUpdateBatch); + + LSValue oldValue; + GetItem(aKey, oldValue); + + if (!oldValue.IsVoid()) { + NotifySnapshots(aDatabase, aKey, oldValue, /* aAffectsOrder */ true); + + mValues.Remove(aKey); + + mWriteOptimizer.DeleteItem(aKey); + + int64_t sizeOfKey = static_cast<int64_t>(aKey.Length()); + + int64_t delta = -sizeOfKey - static_cast<int64_t>(oldValue.UTF16Length()); + + mUpdateBatchUsage += delta; + + mSizeOfKeys -= sizeOfKey; + mSizeOfItems -= sizeOfKey + static_cast<int64_t>(oldValue.Length()); + + if (IsPersistent()) { + mConnection->RemoveItem(aKey, delta); + } + } +} + +void Datastore::Clear(Database* aDatabase) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mClosed); + + if (mValues.Count()) { + int64_t delta = 0; + for (const auto& entry : mValues) { + const nsAString& key = entry.GetKey(); + const LSValue& value = entry.GetData(); + + delta += -static_cast<int64_t>(key.Length()) - + static_cast<int64_t>(value.UTF16Length()); + + NotifySnapshots(aDatabase, key, value, /* aAffectsOrder */ true); + } + + mValues.Clear(); + + if (mInUpdateBatch) { + mWriteOptimizer.Truncate(); + + mUpdateBatchUsage += delta; + } else { + mOrderedItems.Clear(); + + DebugOnly<bool> ok = UpdateUsage(delta); + MOZ_ASSERT(ok); + } + + mSizeOfKeys = 0; + mSizeOfItems = 0; + + if (IsPersistent()) { + mConnection->Clear(delta); + } + } +} + +void Datastore::BeginUpdateBatch(int64_t aSnapshotUsage) { + AssertIsOnBackgroundThread(); + // Don't assert `aSnapshotUsage >= 0`, it can be negative when multiple + // snapshots are operating in parallel. + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(mUpdateBatchUsage == -1); + MOZ_ASSERT(!mInUpdateBatch); + + mUpdateBatchUsage = aSnapshotUsage; + + if (IsPersistent()) { + mConnection->BeginUpdateBatch(); + } + + mInUpdateBatch = true; +} + +int64_t Datastore::EndUpdateBatch(int64_t aSnapshotPeakUsage) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(mInUpdateBatch); + + mWriteOptimizer.ApplyAndReset(mOrderedItems); + + MOZ_ASSERT(!mWriteOptimizer.HasWrites()); + + if (aSnapshotPeakUsage >= 0) { + int64_t delta = mUpdateBatchUsage - aSnapshotPeakUsage; + + if (mActiveDatabases.Count()) { + // We can't apply deltas while other databases are still active. + // The final delta must be zero or negative, but individual deltas can + // be positive. A positive delta can't be applied asynchronously since + // there's no way to fire the quota exceeded error event. + + mPendingUsageDeltas.AppendElement(delta); + } else { + MOZ_ASSERT(delta <= 0); + if (delta != 0) { + DebugOnly<bool> ok = UpdateUsage(delta); + MOZ_ASSERT(ok); + } + } + } + + int64_t result = mUpdateBatchUsage; + mUpdateBatchUsage = -1; + + if (IsPersistent()) { + mConnection->EndUpdateBatch(); + } + + mInUpdateBatch = false; + + return result; +} + +int64_t Datastore::AttemptToUpdateUsage(int64_t aMinSize, bool aInitial) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT_IF(aInitial, aMinSize >= 0); + MOZ_ASSERT_IF(!aInitial, aMinSize > 0); + + const int64_t size = aMinSize + GetSnapshotPeakUsagePreincrement(aInitial); + + if (size && UpdateUsage(size)) { + return size; + } + + const int64_t reducedSize = + aMinSize + GetSnapshotPeakUsageReducedPreincrement(aInitial); + + if (reducedSize && UpdateUsage(reducedSize)) { + return reducedSize; + } + + if (aMinSize > 0 && UpdateUsage(aMinSize)) { + return aMinSize; + } + + return 0; +} + +bool Datastore::HasOtherProcessObservers(Database* aDatabase) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + + if (!gObservers) { + return false; + } + + nsTArray<NotNull<Observer*>>* array; + if (!gObservers->Get(mOriginMetadata.mOrigin, &array)) { + return false; + } + + MOZ_ASSERT(array); + + PBackgroundParent* databaseBackgroundActor = aDatabase->Manager(); + + for (Observer* observer : *array) { + if (observer->Manager() != databaseBackgroundActor) { + return true; + } + } + + return false; +} + +void Datastore::NotifyOtherProcessObservers(Database* aDatabase, + const nsString& aDocumentURI, + const nsString& aKey, + const LSValue& aOldValue, + const LSValue& aNewValue) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + + if (!gObservers) { + return; + } + + nsTArray<NotNull<Observer*>>* array; + if (!gObservers->Get(mOriginMetadata.mOrigin, &array)) { + return; + } + + MOZ_ASSERT(array); + + // We do not want to send information about events back to the content process + // that caused the change. + PBackgroundParent* databaseBackgroundActor = aDatabase->Manager(); + + for (Observer* observer : *array) { + if (observer->Manager() != databaseBackgroundActor) { + observer->Observe(aDatabase, aDocumentURI, aKey, aOldValue, aNewValue); + } + } +} + +void Datastore::NoteChangedObserverArray( + const nsTArray<NotNull<Observer*>>& aObservers) { + AssertIsOnBackgroundThread(); + + for (Database* database : mActiveDatabases) { + Snapshot* snapshot = database->GetSnapshot(); + MOZ_ASSERT(snapshot); + + if (snapshot->IsDirty()) { + continue; + } + + bool hasOtherProcessObservers = false; + + PBackgroundParent* databaseBackgroundActor = database->Manager(); + + for (Observer* observer : aObservers) { + if (observer->Manager() != databaseBackgroundActor) { + hasOtherProcessObservers = true; + break; + } + } + + if (snapshot->HasOtherProcessObservers() != hasOtherProcessObservers) { + snapshot->MarkDirty(); + } + } +} + +void Datastore::Stringify(nsACString& aResult) const { + AssertIsOnBackgroundThread(); + + aResult.AppendLiteral("DirectoryLock:"); + aResult.AppendInt(!!mDirectoryLock); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("Connection:"); + aResult.AppendInt(!!mConnection); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("QuotaObject:"); + aResult.AppendInt(!!mQuotaObject); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("PrepareDatastoreOps:"); + aResult.AppendInt(mPrepareDatastoreOps.Count()); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("PreparedDatastores:"); + aResult.AppendInt(mPreparedDatastores.Count()); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("Databases:"); + aResult.AppendInt(mDatabases.Count()); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("ActiveDatabases:"); + aResult.AppendInt(mActiveDatabases.Count()); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("Origin:"); + aResult.Append(AnonymizedOriginString(mOriginMetadata.mOrigin)); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("PrivateBrowsingId:"); + aResult.AppendInt(mPrivateBrowsingId); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("Closed:"); + aResult.AppendInt(mClosed); +} + +bool Datastore::UpdateUsage(int64_t aDelta) { + AssertIsOnBackgroundThread(); + + // Check internal LocalStorage origin limit. + int64_t newUsage = mUsage + aDelta; + + MOZ_ASSERT(newUsage >= 0); + + if (newUsage > StaticPrefs::dom_storage_default_quota() * 1024) { + return false; + } + + // Check QuotaManager limits (group and global limit). + if (IsPersistent()) { + MOZ_ASSERT(mQuotaObject); + + if (!mQuotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) { + return false; + } + } + + // Quota checks passed, set new usage. + mUsage = newUsage; + + return true; +} + +void Datastore::MaybeClose() { + AssertIsOnBackgroundThread(); + + if (!mPrepareDatastoreOps.Count() && !mHasLivePrivateDatastore && + !mPreparedDatastores.Count() && !mDatabases.Count()) { + Close(); + } +} + +void Datastore::ConnectionClosedCallback() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mQuotaObject); + MOZ_ASSERT(mClosed); + + // Release the quota object first. + mQuotaObject = nullptr; + + bool databaseWasNotAvailable; + bool hasCreatedDatabase; + mConnection->GetFinishInfo(databaseWasNotAvailable, hasCreatedDatabase); + + if (databaseWasNotAvailable && !hasCreatedDatabase) { + MOZ_ASSERT(mUsage == 0); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + quotaManager->ResetUsageForClient( + ClientMetadata{mOriginMetadata, mozilla::dom::quota::Client::LS}); + } + + mConnection = nullptr; + + // Now it's safe to release the directory lock and unregister itself from + // the hashtable. + + mDirectoryLock = nullptr; + + CleanupMetadata(); + + if (mCompleteCallback) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mCompleteCallback.forget())); + } +} + +void Datastore::CleanupMetadata() { + AssertIsOnBackgroundThread(); + + MOZ_ASSERT(gDatastores); + const DebugOnly<bool> removed = gDatastores->Remove(mOriginMetadata.mOrigin); + MOZ_ASSERT(removed); + + QuotaManager::MaybeRecordQuotaClientShutdownStep(quota::Client::LS, + "Datastore removed"_ns); + + if (!gDatastores->Count()) { + gDatastores = nullptr; + } +} + +void Datastore::NotifySnapshots(Database* aDatabase, const nsAString& aKey, + const LSValue& aOldValue, bool aAffectsOrder) { + AssertIsOnBackgroundThread(); + + for (Database* database : mDatabases) { + MOZ_ASSERT(database); + + if (database == aDatabase) { + continue; + } + + Snapshot* snapshot = database->GetSnapshot(); + if (snapshot) { + snapshot->SaveItem(aKey, aOldValue, aAffectsOrder); + } + } +} + +void Datastore::NoteChangedDatabaseMap() { + AssertIsOnBackgroundThread(); + + for (Database* database : mActiveDatabases) { + Snapshot* snapshot = database->GetSnapshot(); + MOZ_ASSERT(snapshot); + + if (snapshot->IsDirty()) { + continue; + } + + if (snapshot->HasOtherProcessDatabases() != + HasOtherProcessDatabases(database)) { + snapshot->MarkDirty(); + } + } +} + +/******************************************************************************* + * PreparedDatastore + ******************************************************************************/ + +void PreparedDatastore::Destroy() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(gPreparedDatastores); + DebugOnly<bool> removed = gPreparedDatastores->Remove(mDatastoreId); + MOZ_ASSERT(removed); +} + +// static +void PreparedDatastore::TimerCallback(nsITimer* aTimer, void* aClosure) { + AssertIsOnBackgroundThread(); + + auto* self = static_cast<PreparedDatastore*>(aClosure); + MOZ_ASSERT(self); + + self->Destroy(); +} + +/******************************************************************************* + * Database + ******************************************************************************/ + +Database::Database(const PrincipalInfo& aPrincipalInfo, + const Maybe<ContentParentId>& aContentParentId, + const nsACString& aOrigin, uint32_t aPrivateBrowsingId) + : mSnapshot(nullptr), + mPrincipalInfo(aPrincipalInfo), + mContentParentId(aContentParentId), + mOrigin(aOrigin), + mPrivateBrowsingId(aPrivateBrowsingId), + mAllowedToClose(false), + mActorDestroyed(false), + mRequestedAllowToClose(false) +#ifdef DEBUG + , + mActorWasAlive(false) +#endif +{ + AssertIsOnBackgroundThread(); +} + +Database::~Database() { + MOZ_ASSERT_IF(mActorWasAlive, mAllowedToClose); + MOZ_ASSERT_IF(mActorWasAlive, mActorDestroyed); +} + +void Database::SetActorAlive(Datastore* aDatastore) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorWasAlive); + MOZ_ASSERT(!mActorDestroyed); + +#ifdef DEBUG + mActorWasAlive = true; +#endif + + mDatastore = aDatastore; + + mDatastore->NoteLiveDatabase(this); + + if (!gLiveDatabases) { + gLiveDatabases = new LiveDatabaseArray(); + } + + gLiveDatabases->AppendElement(WrapNotNullUnchecked(this)); +} + +void Database::RegisterSnapshot(Snapshot* aSnapshot) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aSnapshot); + MOZ_ASSERT(!mSnapshot); + MOZ_ASSERT(!mAllowedToClose); + + // Only one snapshot at a time is currently supported. + mSnapshot = aSnapshot; + + mDatastore->NoteActiveDatabase(this); +} + +void Database::UnregisterSnapshot(Snapshot* aSnapshot) { + MOZ_ASSERT(aSnapshot); + MOZ_ASSERT(mSnapshot == aSnapshot); + + mSnapshot = nullptr; + + mDatastore->NoteInactiveDatabase(this); +} + +void Database::RequestAllowToClose() { + AssertIsOnBackgroundThread(); + + if (mRequestedAllowToClose) { + return; + } + + mRequestedAllowToClose = true; + + // Send the RequestAllowToClose message to the child to avoid racing with the + // child actor. Except the case when the actor was already destroyed. + if (mActorDestroyed) { + MOZ_ASSERT(mAllowedToClose); + return; + } + + if (NS_WARN_IF(!SendRequestAllowToClose()) && !mSnapshot) { + // This is not necessary, because there should be a runnable scheduled that + // will call ActorDestroy which calls AllowToClose. However we can speedup + // the shutdown a bit if we do it here directly, but only if there's no + // registered snapshot. + AllowToClose(); + } +} + +void Database::ForceKill() { + AssertIsOnBackgroundThread(); + + if (mActorDestroyed) { + MOZ_ASSERT(mAllowedToClose); + return; + } + + Unused << PBackgroundLSDatabaseParent::Send__delete__(this); +} + +void Database::Stringify(nsACString& aResult) const { + AssertIsOnBackgroundThread(); + + aResult.AppendLiteral("SnapshotRegistered:"); + aResult.AppendInt(!!mSnapshot); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("OtherProcessActor:"); + aResult.AppendInt(BackgroundParent::IsOtherProcessActor(Manager())); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("Origin:"); + aResult.Append(AnonymizedOriginString(mOrigin)); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("PrivateBrowsingId:"); + aResult.AppendInt(mPrivateBrowsingId); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("AllowedToClose:"); + aResult.AppendInt(mAllowedToClose); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("ActorDestroyed:"); + aResult.AppendInt(mActorDestroyed); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("RequestedAllowToClose:"); + aResult.AppendInt(mRequestedAllowToClose); +} + +void Database::AllowToClose() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mAllowedToClose); + MOZ_ASSERT(mDatastore); + MOZ_ASSERT(!mSnapshot); + + mAllowedToClose = true; + + mDatastore->NoteFinishedDatabase(this); + + mDatastore = nullptr; + + MOZ_ASSERT(gLiveDatabases); + gLiveDatabases->RemoveElement(this); + + QuotaManager::MaybeRecordQuotaClientShutdownStep(quota::Client::LS, + "Live database removed"_ns); + + if (gLiveDatabases->IsEmpty()) { + gLiveDatabases = nullptr; + } +} + +void Database::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; + + if (!mAllowedToClose) { + AllowToClose(); + } +} + +mozilla::ipc::IPCResult Database::RecvDeleteMe() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + IProtocol* mgr = Manager(); + if (!PBackgroundLSDatabaseParent::Send__delete__(this)) { + return IPC_FAIL(mgr, "Send__delete__ failed!"); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult Database::RecvAllowToClose() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mAllowedToClose)) { + return IPC_FAIL(this, "mAllowedToClose already set!"); + } + + AllowToClose(); + + return IPC_OK(); +} + +PBackgroundLSSnapshotParent* Database::AllocPBackgroundLSSnapshotParent( + const nsAString& aDocumentURI, const nsAString& aKey, + const bool& aIncreasePeakUsage, const int64_t& aMinSize, + LSSnapshotInitInfo* aInitInfo) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(aIncreasePeakUsage && aMinSize < 0)) { + MOZ_ASSERT_UNLESS_FUZZING(false); + return nullptr; + } + + if (NS_WARN_IF(mAllowedToClose)) { + MOZ_ASSERT_UNLESS_FUZZING(false); + return nullptr; + } + + RefPtr<Snapshot> snapshot = new Snapshot(this, aDocumentURI); + + // Transfer ownership to IPDL. + return snapshot.forget().take(); +} + +mozilla::ipc::IPCResult Database::RecvPBackgroundLSSnapshotConstructor( + PBackgroundLSSnapshotParent* aActor, const nsAString& aDocumentURI, + const nsAString& aKey, const bool& aIncreasePeakUsage, + const int64_t& aMinSize, LSSnapshotInitInfo* aInitInfo) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT_IF(aIncreasePeakUsage, aMinSize >= 0); + MOZ_ASSERT(aInitInfo); + MOZ_ASSERT(!mAllowedToClose); + + auto* snapshot = static_cast<Snapshot*>(aActor); + + bool addKeyToUnknownItems; + nsTHashtable<nsStringHashKey> loadedItems; + nsTArray<LSItemInfo> itemInfos; + uint32_t nextLoadIndex; + LSSnapshot::LoadState loadState; + mDatastore->GetSnapshotLoadInfo(aKey, addKeyToUnknownItems, loadedItems, + itemInfos, nextLoadIndex, loadState); + + nsTHashSet<nsString> unknownItems; + if (addKeyToUnknownItems) { + unknownItems.Insert(aKey); + } + + uint32_t totalLength = mDatastore->GetLength(); + + int64_t usage = mDatastore->GetUsage(); + + int64_t peakUsage = usage; + + if (aIncreasePeakUsage) { + int64_t size = + mDatastore->AttemptToUpdateUsage(aMinSize, /* aInitial */ true); + + peakUsage += size; + } + + bool hasOtherProcessDatabases = mDatastore->HasOtherProcessDatabases(this); + bool hasOtherProcessObservers = mDatastore->HasOtherProcessObservers(this); + + snapshot->Init(loadedItems, std::move(unknownItems), nextLoadIndex, + totalLength, usage, peakUsage, loadState, + hasOtherProcessDatabases, hasOtherProcessObservers); + + RegisterSnapshot(snapshot); + + aInitInfo->addKeyToUnknownItems() = addKeyToUnknownItems; + aInitInfo->itemInfos() = std::move(itemInfos); + aInitInfo->totalLength() = totalLength; + aInitInfo->usage() = usage; + aInitInfo->peakUsage() = peakUsage; + aInitInfo->loadState() = loadState; + aInitInfo->hasOtherProcessDatabases() = hasOtherProcessDatabases; + aInitInfo->hasOtherProcessObservers() = hasOtherProcessObservers; + + return IPC_OK(); +} + +bool Database::DeallocPBackgroundLSSnapshotParent( + PBackgroundLSSnapshotParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<Snapshot> actor = dont_AddRef(static_cast<Snapshot*>(aActor)); + + return true; +} + +/******************************************************************************* + * Snapshot + ******************************************************************************/ + +Snapshot::Snapshot(Database* aDatabase, const nsAString& aDocumentURI) + : mDatabase(aDatabase), + mDatastore(aDatabase->GetDatastore()), + mDocumentURI(aDocumentURI), + mTotalLength(0), + mUsage(-1), + mPeakUsage(-1), + mSavedKeys(false), + mActorDestroyed(false), + mFinishReceived(false), + mLoadedReceived(false), + mLoadedAllItems(false), + mLoadKeysReceived(false), + mSentMarkDirty(false) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); +} + +Snapshot::~Snapshot() { + MOZ_ASSERT(mActorDestroyed); + MOZ_ASSERT(mFinishReceived); +} + +void Snapshot::SaveItem(const nsAString& aKey, const LSValue& aOldValue, + bool aAffectsOrder) { + AssertIsOnBackgroundThread(); + + MarkDirty(); + + if (mLoadedAllItems) { + return; + } + + if (!mLoadedItems.Contains(aKey) && !mUnknownItems.Contains(aKey)) { + mValues.LookupOrInsert(aKey, aOldValue); + } + + if (aAffectsOrder && !mSavedKeys) { + mDatastore->GetKeys(mKeys); + mSavedKeys = true; + } +} + +void Snapshot::MarkDirty() { + AssertIsOnBackgroundThread(); + + if (!mSentMarkDirty) { + Unused << SendMarkDirty(); + mSentMarkDirty = true; + } +} + +void Snapshot::Finish() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDatabase); + MOZ_ASSERT(mDatastore); + MOZ_ASSERT(!mFinishReceived); + + mDatastore->BeginUpdateBatch(mUsage); + + mDatastore->EndUpdateBatch(mPeakUsage); + + mDatabase->UnregisterSnapshot(this); + + mFinishReceived = true; +} + +void Snapshot::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; + + if (!mFinishReceived) { + Finish(); + } +} + +mozilla::ipc::IPCResult Snapshot::RecvDeleteMe() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + IProtocol* mgr = Manager(); + if (!PBackgroundLSSnapshotParent::Send__delete__(this)) { + return IPC_FAIL(mgr, "Send__delete__ failed!"); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::Checkpoint( + nsTArray<LSWriteInfo>&& aWriteInfos) { + AssertIsOnBackgroundThread(); + // Don't assert `mUsage >= 0`, it can be negative when multiple snapshots are + // operating in parallel. + MOZ_ASSERT(mPeakUsage >= mUsage); + + if (NS_WARN_IF(aWriteInfos.IsEmpty())) { + return IPC_FAIL(this, "aWriteInfos is empty!"); + } + + if (NS_WARN_IF(mHasOtherProcessObservers)) { + return IPC_FAIL(this, "mHasOtherProcessObservers already set!"); + } + + mDatastore->BeginUpdateBatch(mUsage); + + for (uint32_t index = 0; index < aWriteInfos.Length(); index++) { + const LSWriteInfo& writeInfo = aWriteInfos[index]; + + switch (writeInfo.type()) { + case LSWriteInfo::TLSSetItemInfo: { + const LSSetItemInfo& info = writeInfo.get_LSSetItemInfo(); + + mDatastore->SetItem(mDatabase, info.key(), info.value()); + + break; + } + + case LSWriteInfo::TLSRemoveItemInfo: { + const LSRemoveItemInfo& info = writeInfo.get_LSRemoveItemInfo(); + + mDatastore->RemoveItem(mDatabase, info.key()); + + break; + } + + case LSWriteInfo::TLSClearInfo: { + mDatastore->Clear(mDatabase); + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + } + + mUsage = mDatastore->EndUpdateBatch(-1); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::CheckpointAndNotify( + nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) { + AssertIsOnBackgroundThread(); + // Don't assert `mUsage >= 0`, it can be negative when multiple snapshots are + // operating in parallel. + MOZ_ASSERT(mPeakUsage >= mUsage); + + if (NS_WARN_IF(aWriteAndNotifyInfos.IsEmpty())) { + return IPC_FAIL(this, "aWriteAndNotifyInfos is empty!"); + } + + if (NS_WARN_IF(!mHasOtherProcessObservers)) { + return IPC_FAIL(this, "mHasOtherProcessObservers is not set!"); + } + + mDatastore->BeginUpdateBatch(mUsage); + + for (uint32_t index = 0; index < aWriteAndNotifyInfos.Length(); index++) { + const LSWriteAndNotifyInfo& writeAndNotifyInfo = + aWriteAndNotifyInfos[index]; + + switch (writeAndNotifyInfo.type()) { + case LSWriteAndNotifyInfo::TLSSetItemAndNotifyInfo: { + const LSSetItemAndNotifyInfo& info = + writeAndNotifyInfo.get_LSSetItemAndNotifyInfo(); + + mDatastore->SetItem(mDatabase, info.key(), info.value()); + + mDatastore->NotifyOtherProcessObservers( + mDatabase, mDocumentURI, info.key(), info.oldValue(), info.value()); + + break; + } + + case LSWriteAndNotifyInfo::TLSRemoveItemAndNotifyInfo: { + const LSRemoveItemAndNotifyInfo& info = + writeAndNotifyInfo.get_LSRemoveItemAndNotifyInfo(); + + mDatastore->RemoveItem(mDatabase, info.key()); + + mDatastore->NotifyOtherProcessObservers(mDatabase, mDocumentURI, + info.key(), info.oldValue(), + VoidLSValue()); + + break; + } + + case LSWriteAndNotifyInfo::TLSClearInfo: { + mDatastore->Clear(mDatabase); + + mDatastore->NotifyOtherProcessObservers(mDatabase, mDocumentURI, + VoidString(), VoidLSValue(), + VoidLSValue()); + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + } + + mUsage = mDatastore->EndUpdateBatch(-1); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::RecvAsyncCheckpoint( + nsTArray<LSWriteInfo>&& aWriteInfos) { + return Checkpoint(std::move(aWriteInfos)); +} + +mozilla::ipc::IPCResult Snapshot::RecvAsyncCheckpointAndNotify( + nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) { + return CheckpointAndNotify(std::move(aWriteAndNotifyInfos)); +} + +mozilla::ipc::IPCResult Snapshot::RecvSyncCheckpoint( + nsTArray<LSWriteInfo>&& aWriteInfos) { + return Checkpoint(std::move(aWriteInfos)); +} + +mozilla::ipc::IPCResult Snapshot::RecvSyncCheckpointAndNotify( + nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) { + return CheckpointAndNotify(std::move(aWriteAndNotifyInfos)); +} + +mozilla::ipc::IPCResult Snapshot::RecvAsyncFinish() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mFinishReceived)) { + MOZ_ASSERT_UNLESS_FUZZING(false); + return IPC_FAIL(this, "Already finished"); + } + + Finish(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::RecvSyncFinish() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mFinishReceived)) { + MOZ_ASSERT_UNLESS_FUZZING(false); + return IPC_FAIL(this, "Already finished"); + } + + Finish(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::RecvLoaded() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mFinishReceived)) { + return IPC_FAIL(this, "mFinishReceived already set!"); + } + + if (NS_WARN_IF(mLoadedReceived)) { + return IPC_FAIL(this, "mLoadedReceived already set!"); + } + + if (NS_WARN_IF(mLoadedAllItems)) { + return IPC_FAIL(this, "mLoadedAllItems already set!"); + } + + if (NS_WARN_IF(mLoadKeysReceived)) { + return IPC_FAIL(this, "mLoadKeysReceived already set!"); + } + + mLoadedReceived = true; + + mLoadedItems.Clear(); + mUnknownItems.Clear(); + mValues.Clear(); + mKeys.Clear(); + mLoadedAllItems = true; + mLoadKeysReceived = true; + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::RecvLoadValueAndMoreItems( + const nsAString& aKey, LSValue* aValue, nsTArray<LSItemInfo>* aItemInfos) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aValue); + MOZ_ASSERT(aItemInfos); + MOZ_ASSERT(mDatastore); + + if (NS_WARN_IF(mFinishReceived)) { + return IPC_FAIL(this, "mFinishReceived already set!"); + } + + if (NS_WARN_IF(mLoadedReceived)) { + return IPC_FAIL(this, "mLoadedReceived already set!"); + } + + if (NS_WARN_IF(mLoadedAllItems)) { + return IPC_FAIL(this, "mLoadedAllItems already set!"); + } + + if (mLoadedItems.Contains(aKey)) { + return IPC_FAIL(this, "mLoadedItems already contains aKey!"); + } + + if (mUnknownItems.Contains(aKey)) { + return IPC_FAIL(this, "mUnknownItems already contains aKey!"); + } + + if (auto entry = mValues.Lookup(aKey)) { + *aValue = entry.Data(); + entry.Remove(); + } else { + mDatastore->GetItem(aKey, *aValue); + } + + if (aValue->IsVoid()) { + mUnknownItems.Insert(aKey); + } else { + mLoadedItems.PutEntry(aKey); + + // mLoadedItems.Count()==mTotalLength is checked below. + } + + // Load some more key/value pairs (as many as the snapshot gradual prefill + // byte budget allows). + + if (gSnapshotGradualPrefill > 0) { + const nsTArray<LSItemInfo>& orderedItems = mDatastore->GetOrderedItems(); + + uint32_t length; + if (mSavedKeys) { + length = mKeys.Length(); + } else { + length = orderedItems.Length(); + } + + int64_t size = 0; + while (mNextLoadIndex < length) { + // If the datastore's ordering has changed, mSavedKeys will be true and + // mKeys contains an ordered list of the keys. Otherwise we can use the + // datastore's key ordering which is still the same as when the snapshot + // was created. + + nsString key; + if (mSavedKeys) { + key = mKeys[mNextLoadIndex]; + } else { + key = orderedItems[mNextLoadIndex].key(); + } + + // Normally we would do this: + // if (!mLoadedItems.GetEntry(key)) { + // ... + // mLoadedItems.PutEntry(key); + // } + // but that requires two hash lookups. We can reduce that to just one + // hash lookup if we always call PutEntry and check the number of entries + // before and after the put (which is very cheap). However, if we reach + // the prefill limit, we need to call RemoveEntry, but that is also cheap + // because we pass the entry (not the key). + + uint32_t countBeforePut = mLoadedItems.Count(); + auto loadedItemEntry = mLoadedItems.PutEntry(key); + if (countBeforePut != mLoadedItems.Count()) { + // Check mValues first since that contains values as they existed when + // our snapshot was created, but have since been changed/removed in the + // datastore. If it's not there, then the datastore has the + // still-current value. However, if the datastore's key ordering has + // changed, we need to do a hash lookup rather than being able to do an + // optimized direct access to the index. + + LSValue value; + auto valueEntry = mValues.Lookup(key); + if (valueEntry) { + value = valueEntry.Data(); + } else if (mSavedKeys) { + mDatastore->GetItem(nsString(key), value); + } else { + value = orderedItems[mNextLoadIndex].value(); + } + + // All not loaded keys must have a value. + MOZ_ASSERT(!value.IsVoid()); + + size += static_cast<int64_t>(key.Length()) + + static_cast<int64_t>(value.Length()); + + if (size > gSnapshotGradualPrefill) { + mLoadedItems.RemoveEntry(loadedItemEntry); + + // mNextLoadIndex is not incremented, so we will resume at the same + // position next time. + break; + } + + if (valueEntry) { + valueEntry.Remove(); + } + + LSItemInfo* itemInfo = aItemInfos->AppendElement(); + itemInfo->key() = key; + itemInfo->value() = value; + } + + mNextLoadIndex++; + } + } + + if (mLoadedItems.Count() == mTotalLength) { + mLoadedItems.Clear(); + mUnknownItems.Clear(); +#ifdef DEBUG + const bool allValuesVoid = + std::all_of(mValues.Values().cbegin(), mValues.Values().cend(), + [](const auto& entry) { return entry.IsVoid(); }); + MOZ_ASSERT(allValuesVoid); +#endif + mValues.Clear(); + mLoadedAllItems = true; + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::RecvLoadKeys(nsTArray<nsString>* aKeys) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aKeys); + MOZ_ASSERT(mDatastore); + + if (NS_WARN_IF(mFinishReceived)) { + return IPC_FAIL(this, "mFinishReceived already set!"); + } + + if (NS_WARN_IF(mLoadedReceived)) { + return IPC_FAIL(this, "mLoadedReceived already set!"); + } + + if (NS_WARN_IF(mLoadKeysReceived)) { + return IPC_FAIL(this, "mLoadKeysReceived already set!"); + } + + mLoadKeysReceived = true; + + if (mSavedKeys) { + aKeys->AppendElements(std::move(mKeys)); + } else { + mDatastore->GetKeys(*aKeys); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::RecvIncreasePeakUsage(const int64_t& aMinSize, + int64_t* aSize) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aSize); + + if (NS_WARN_IF(aMinSize <= 0)) { + return IPC_FAIL(this, "aMinSize not valid!"); + } + + if (NS_WARN_IF(mFinishReceived)) { + return IPC_FAIL(this, "mFinishReceived already set!"); + } + + int64_t size = + mDatastore->AttemptToUpdateUsage(aMinSize, /* aInitial */ false); + + mPeakUsage += size; + + *aSize = size; + + return IPC_OK(); +} + +/******************************************************************************* + * Observer + ******************************************************************************/ + +Observer::Observer(const nsACString& aOrigin) + : mOrigin(aOrigin), mActorDestroyed(false) { + AssertIsOnBackgroundThread(); +} + +Observer::~Observer() { MOZ_ASSERT(mActorDestroyed); } + +void Observer::Observe(Database* aDatabase, const nsString& aDocumentURI, + const nsString& aKey, const LSValue& aOldValue, + const LSValue& aNewValue) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + + Unused << SendObserve(aDatabase->GetPrincipalInfo(), + aDatabase->PrivateBrowsingId(), aDocumentURI, aKey, + aOldValue, aNewValue); +} + +void Observer::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; + + MOZ_ASSERT(gObservers); + + nsTArray<NotNull<Observer*>>* array; + gObservers->Get(mOrigin, &array); + MOZ_ASSERT(array); + + array->RemoveElement(this); + + if (RefPtr<Datastore> datastore = GetDatastore(mOrigin)) { + datastore->NoteChangedObserverArray(*array); + } + + if (array->IsEmpty()) { + gObservers->Remove(mOrigin); + } + + if (!gObservers->Count()) { + gObservers = nullptr; + } +} + +mozilla::ipc::IPCResult Observer::RecvDeleteMe() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + IProtocol* mgr = Manager(); + if (!PBackgroundLSObserverParent::Send__delete__(this)) { + return IPC_FAIL(mgr, "Send__delete__ failed!"); + } + return IPC_OK(); +} + +/******************************************************************************* + * LSRequestBase + ******************************************************************************/ + +LSRequestBase::LSRequestBase(const LSRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId) + : mParams(aParams), + mContentParentId(aContentParentId), + mState(State::Initial), + mWaitingForFinish(false) {} + +LSRequestBase::~LSRequestBase() { + MOZ_ASSERT_IF(MayProceedOnNonOwningThread(), + mState == State::Initial || mState == State::Completed); +} + +void LSRequestBase::Dispatch() { + AssertIsOnOwningThread(); + + mState = State::StartingRequest; + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(this)); +} + +void LSRequestBase::StringifyState(nsACString& aResult) const { + AssertIsOnOwningThread(); + + switch (mState) { + case State::Initial: + aResult.AppendLiteral("Initial"); + return; + + case State::StartingRequest: + aResult.AppendLiteral("StartingRequest"); + return; + + case State::Nesting: + aResult.AppendLiteral("Nesting"); + return; + + case State::SendingReadyMessage: + aResult.AppendLiteral("SendingReadyMessage"); + return; + + case State::WaitingForFinish: + aResult.AppendLiteral("WaitingForFinish"); + return; + + case State::SendingResults: + aResult.AppendLiteral("SendingResults"); + return; + + case State::Completed: + aResult.AppendLiteral("Completed"); + return; + + default: + MOZ_CRASH("Bad state!"); + } +} + +void LSRequestBase::Stringify(nsACString& aResult) const { + AssertIsOnOwningThread(); + + aResult.AppendLiteral("State:"); + StringifyState(aResult); +} + +void LSRequestBase::Log() { + AssertIsOnOwningThread(); + + if (!LS_LOG_TEST()) { + return; + } + + LS_LOG(("LSRequestBase [%p]", this)); + + nsCString state; + StringifyState(state); + + LS_LOG((" mState: %s", state.get())); +} + +nsresult LSRequestBase::NestedRun() { return NS_OK; } + +bool LSRequestBase::VerifyRequestParams() { + AssertIsOnBackgroundThread(); + + MOZ_ASSERT(mParams.type() != LSRequestParams::T__None); + + switch (mParams.type()) { + case LSRequestParams::TLSRequestPreloadDatastoreParams: { + const LSRequestCommonParams& params = + mParams.get_LSRequestPreloadDatastoreParams().commonParams(); + + if (NS_WARN_IF(!VerifyPrincipalInfo( + params.principalInfo(), params.storagePrincipalInfo(), false))) { + return false; + } + + if (NS_WARN_IF( + !VerifyOriginKey(params.originKey(), params.principalInfo()))) { + return false; + } + + break; + } + + case LSRequestParams::TLSRequestPrepareDatastoreParams: { + const LSRequestPrepareDatastoreParams& params = + mParams.get_LSRequestPrepareDatastoreParams(); + + const LSRequestCommonParams& commonParams = params.commonParams(); + + if (NS_WARN_IF(!VerifyPrincipalInfo(commonParams.principalInfo(), + commonParams.storagePrincipalInfo(), + false))) { + return false; + } + + if (params.clientPrincipalInfo() && + NS_WARN_IF(!VerifyPrincipalInfo(commonParams.principalInfo(), + params.clientPrincipalInfo().ref(), + true))) { + return false; + } + + if (NS_WARN_IF(!VerifyClientId(mContentParentId, + params.clientPrincipalInfo(), + params.clientId()))) { + return false; + } + + if (NS_WARN_IF(!VerifyOriginKey(commonParams.originKey(), + commonParams.principalInfo()))) { + return false; + } + + break; + } + + case LSRequestParams::TLSRequestPrepareObserverParams: { + const LSRequestPrepareObserverParams& params = + mParams.get_LSRequestPrepareObserverParams(); + + if (NS_WARN_IF(!VerifyPrincipalInfo( + params.principalInfo(), params.storagePrincipalInfo(), false))) { + return false; + } + + if (params.clientPrincipalInfo() && + NS_WARN_IF(!VerifyPrincipalInfo(params.principalInfo(), + params.clientPrincipalInfo().ref(), + true))) { + return false; + } + + if (NS_WARN_IF(!VerifyClientId(mContentParentId, + params.clientPrincipalInfo(), + params.clientId()))) { + return false; + } + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +nsresult LSRequestBase::StartRequest() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::StartingRequest); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + return NS_ERROR_ABORT; + } + +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + bool trustParams = false; +#else + bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager()); +#endif + + if (!trustParams && NS_WARN_IF(!VerifyRequestParams())) { + return NS_ERROR_FAILURE; + } + + QM_TRY(MOZ_TO_RESULT(Start())); + + return NS_OK; +} + +void LSRequestBase::SendReadyMessage() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingReadyMessage); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + MaybeSetFailureCode(NS_ERROR_ABORT); + } + + nsresult rv = SendReadyMessageInternal(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MaybeSetFailureCode(rv); + + FinishInternal(); + } +} + +nsresult LSRequestBase::SendReadyMessageInternal() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingReadyMessage); + + if (!MayProceed()) { + return NS_ERROR_ABORT; + } + + if (NS_WARN_IF(!SendReady())) { + return NS_ERROR_FAILURE; + } + + mState = State::WaitingForFinish; + + mWaitingForFinish = true; + + return NS_OK; +} + +void LSRequestBase::Finish() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::WaitingForFinish); + + mWaitingForFinish = false; + + FinishInternal(); +} + +void LSRequestBase::FinishInternal() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingReadyMessage || + mState == State::WaitingForFinish); + + mState = State::SendingResults; + + // This LSRequestBase can only be held alive by the IPDL. Run() can end up + // with clearing that last reference. So we need to add a self reference here. + RefPtr<LSRequestBase> kungFuDeathGrip = this; + + MOZ_ALWAYS_SUCCEEDS(this->Run()); +} + +void LSRequestBase::SendResults() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + MaybeSetFailureCode(NS_ERROR_ABORT); + } + + if (MayProceed()) { + LSRequestResponse response; + + if (NS_SUCCEEDED(ResultCode())) { + GetResponse(response); + + MOZ_ASSERT(response.type() != LSRequestResponse::T__None); + + if (response.type() == LSRequestResponse::Tnsresult) { + MOZ_ASSERT(NS_FAILED(response.get_nsresult())); + + SetFailureCode(response.get_nsresult()); + } + } else { + response = ResultCode(); + } + + Unused << PBackgroundLSRequestParent::Send__delete__(this, response); + } + + Cleanup(); + + mState = State::Completed; +} + +NS_IMETHODIMP +LSRequestBase::Run() { + nsresult rv; + + switch (mState) { + case State::StartingRequest: + rv = StartRequest(); + break; + + case State::Nesting: + rv = NestedRun(); + break; + + case State::SendingReadyMessage: + SendReadyMessage(); + return NS_OK; + + case State::SendingResults: + SendResults(); + return NS_OK; + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingReadyMessage) { + MaybeSetFailureCode(rv); + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + mState = State::SendingReadyMessage; + + if (IsOnOwningThread()) { + SendReadyMessage(); + } else { + MOZ_ALWAYS_SUCCEEDS( + OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); + } + } + + return NS_OK; +} + +void LSRequestBase::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + NoteComplete(); + + // Assume ActorDestroy can happen at any time, so we can't probe the current + // state since mState can be modified on any thread (only one thread at a time + // based on the state machine). However we can use mWaitingForFinish which is + // only touched on the owning thread. If mWaitingForFinisg is true, we can + // also modify mState since we are guaranteed that there are no pending + // runnables which would probe mState to decide what code needs to run (there + // shouldn't be any running runnables on other threads either). + + if (mWaitingForFinish) { + Finish(); + } + + // We don't have to handle the case when mWaitingForFinish is not true since + // it means that either nothing has been initialized yet, so nothing to + // cleanup or there are pending runnables that will detect that the actor has + // been destroyed and cleanup accordingly. +} + +mozilla::ipc::IPCResult LSRequestBase::RecvCancel() { + AssertIsOnOwningThread(); + + Log(); + + const char* crashOnCancel = PR_GetEnv("LSNG_CRASH_ON_CANCEL"); + if (crashOnCancel) { + MOZ_CRASH("LSNG: Crash on cancel."); + } + + IProtocol* mgr = Manager(); + if (!PBackgroundLSRequestParent::Send__delete__(this, NS_ERROR_ABORT)) { + return IPC_FAIL(mgr, "Send__delete__ failed!"); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult LSRequestBase::RecvFinish() { + AssertIsOnOwningThread(); + + Finish(); + + return IPC_OK(); +} + +/******************************************************************************* + * PrepareDatastoreOp + ******************************************************************************/ + +PrepareDatastoreOp::PrepareDatastoreOp( + const LSRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId) + : LSRequestBase(aParams, aContentParentId), + mLoadDataOp(nullptr), + mPrivateBrowsingId(0), + mUsage(0), + mSizeOfKeys(0), + mSizeOfItems(0), + mDatastoreId(0), + mNestedState(NestedState::BeforeNesting), + mForPreload(aParams.type() == + LSRequestParams::TLSRequestPreloadDatastoreParams), + mDatabaseNotAvailable(false), + mInvalidated(false) +#ifdef DEBUG + , + mDEBUGUsage(0) +#endif +{ + MOZ_ASSERT( + aParams.type() == LSRequestParams::TLSRequestPreloadDatastoreParams || + aParams.type() == LSRequestParams::TLSRequestPrepareDatastoreParams); +} + +PrepareDatastoreOp::~PrepareDatastoreOp() { + MOZ_ASSERT(!mDirectoryLock); + MOZ_ASSERT_IF(MayProceedOnNonOwningThread(), + mState == State::Initial || mState == State::Completed); + MOZ_ASSERT(!mLoadDataOp); +} + +void PrepareDatastoreOp::StringifyNestedState(nsACString& aResult) const { + AssertIsOnOwningThread(); + + switch (mNestedState) { + case NestedState::BeforeNesting: + aResult.AppendLiteral("BeforeNesting"); + return; + + case NestedState::CheckExistingOperations: + aResult.AppendLiteral("CheckExistingOperations"); + return; + + case NestedState::CheckClosingDatastore: + aResult.AppendLiteral("CheckClosingDatastore"); + return; + + case NestedState::PreparationPending: + aResult.AppendLiteral("PreparationPending"); + return; + + case NestedState::DirectoryOpenPending: + aResult.AppendLiteral("DirectoryOpenPending"); + return; + + case NestedState::DatabaseWorkOpen: + aResult.AppendLiteral("DatabaseWorkOpen"); + return; + + case NestedState::BeginLoadData: + aResult.AppendLiteral("BeginLoadData"); + return; + + case NestedState::DatabaseWorkLoadData: + aResult.AppendLiteral("DatabaseWorkLoadData"); + return; + + case NestedState::AfterNesting: + aResult.AppendLiteral("AfterNesting"); + return; + + default: + MOZ_CRASH("Bad state!"); + } +} + +void PrepareDatastoreOp::Stringify(nsACString& aResult) const { + AssertIsOnOwningThread(); + + LSRequestBase::Stringify(aResult); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("Origin:"); + aResult.Append(AnonymizedOriginString(Origin())); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("NestedState:"); + StringifyNestedState(aResult); +} + +void PrepareDatastoreOp::Log() { + AssertIsOnOwningThread(); + + LSRequestBase::Log(); + + if (!LS_LOG_TEST()) { + return; + } + + nsCString nestedState; + StringifyNestedState(nestedState); + + LS_LOG((" mNestedState: %s", nestedState.get())); + + switch (mNestedState) { + case NestedState::CheckClosingDatastore: { + for (uint32_t index = gPrepareDatastoreOps->Length(); index > 0; + index--) { + const auto& existingOp = (*gPrepareDatastoreOps)[index - 1]; + + if (existingOp->mDelayedOp == this) { + LS_LOG((" mDelayedBy: [%p]", + static_cast<PrepareDatastoreOp*>(existingOp.get()))); + + existingOp->Log(); + + break; + } + } + + break; + } + + case NestedState::DirectoryOpenPending: { + MOZ_ASSERT(mPendingDirectoryLock); + + LS_LOG((" mPendingDirectoryLock: [%p]", mPendingDirectoryLock.get())); + + mPendingDirectoryLock->Log(); + + break; + } + + default:; + } +} + +nsresult PrepareDatastoreOp::Start() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::StartingRequest); + MOZ_ASSERT(mNestedState == NestedState::BeforeNesting); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + const LSRequestCommonParams& commonParams = + mForPreload + ? mParams.get_LSRequestPreloadDatastoreParams().commonParams() + : mParams.get_LSRequestPrepareDatastoreParams().commonParams(); + + const PrincipalInfo& storagePrincipalInfo = + commonParams.storagePrincipalInfo(); + + if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + mOriginMetadata = {QuotaManager::GetInfoForChrome(), + PERSISTENCE_TYPE_DEFAULT}; + } else { + MOZ_ASSERT(storagePrincipalInfo.type() == + PrincipalInfo::TContentPrincipalInfo); + + PrincipalMetadata principalMetadata = + QuotaManager::GetInfoFromValidatedPrincipalInfo(storagePrincipalInfo); + + mOriginMetadata.mSuffix = std::move(principalMetadata.mSuffix); + mOriginMetadata.mGroup = std::move(principalMetadata.mGroup); + // XXX We can probably get rid of mMainThreadOrigin if we change + // LSRequestBase::Dispatch to synchronously run LSRequestBase::StartRequest + // through LSRequestBase::Run. + mMainThreadOrigin = std::move(principalMetadata.mOrigin); + mOriginMetadata.mPersistenceType = PERSISTENCE_TYPE_DEFAULT; + } + + mState = State::Nesting; + mNestedState = NestedState::CheckExistingOperations; + + MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +nsresult PrepareDatastoreOp::CheckExistingOperations() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::CheckExistingOperations); + MOZ_ASSERT(gPrepareDatastoreOps); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + return NS_ERROR_ABORT; + } + + const LSRequestCommonParams& commonParams = + mForPreload + ? mParams.get_LSRequestPreloadDatastoreParams().commonParams() + : mParams.get_LSRequestPrepareDatastoreParams().commonParams(); + + const PrincipalInfo& storagePrincipalInfo = + commonParams.storagePrincipalInfo(); + + nsCString originAttrSuffix; + uint32_t privateBrowsingId; + + if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + privateBrowsingId = 0; + } else { + MOZ_ASSERT(storagePrincipalInfo.type() == + PrincipalInfo::TContentPrincipalInfo); + + const ContentPrincipalInfo& info = + storagePrincipalInfo.get_ContentPrincipalInfo(); + const OriginAttributes& attrs = info.attrs(); + attrs.CreateSuffix(originAttrSuffix); + + privateBrowsingId = attrs.mPrivateBrowsingId; + } + + mArchivedOriginScope = ArchivedOriginScope::CreateFromOrigin( + originAttrSuffix, commonParams.originKey()); + MOZ_ASSERT(mArchivedOriginScope); + + // Normally it's safe to access member variables without a mutex because even + // though we hop between threads, the variables are never accessed by multiple + // threads at the same time. + // However, the methods OriginIsKnown and Origin can be called at any time. + // So we have to make sure the member variable is set on the same thread as + // those methods are called. + mOriginMetadata.mOrigin = mMainThreadOrigin; + + MOZ_ASSERT(OriginIsKnown()); + + mPrivateBrowsingId = privateBrowsingId; + + mNestedState = NestedState::CheckClosingDatastore; + + // See if this PrepareDatastoreOp needs to wait. + bool foundThis = false; + for (uint32_t index = gPrepareDatastoreOps->Length(); index > 0; index--) { + const auto& existingOp = (*gPrepareDatastoreOps)[index - 1]; + + if (existingOp == this) { + foundThis = true; + continue; + } + + if (foundThis && existingOp->Origin() == Origin()) { + // Only one op can be delayed. + MOZ_ASSERT(!existingOp->mDelayedOp); + existingOp->mDelayedOp = this; + + return NS_OK; + } + } + + QM_TRY(MOZ_TO_RESULT(CheckClosingDatastoreInternal())); + + return NS_OK; +} + +nsresult PrepareDatastoreOp::CheckClosingDatastore() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::CheckClosingDatastore); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + return NS_ERROR_ABORT; + } + + QM_TRY(MOZ_TO_RESULT(CheckClosingDatastoreInternal())); + + return NS_OK; +} + +nsresult PrepareDatastoreOp::CheckClosingDatastoreInternal() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::CheckClosingDatastore); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + mNestedState = NestedState::PreparationPending; + + RefPtr<Datastore> datastore; + if ((datastore = GetDatastore(Origin())) && datastore->IsClosed()) { + datastore->WaitForConnectionToComplete(this); + + return NS_OK; + } + + QM_TRY(MOZ_TO_RESULT(BeginDatastorePreparationInternal())); + + return NS_OK; +} + +nsresult PrepareDatastoreOp::BeginDatastorePreparation() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::PreparationPending); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + return NS_ERROR_ABORT; + } + + QM_TRY(MOZ_TO_RESULT(BeginDatastorePreparationInternal())); + + return NS_OK; +} + +nsresult PrepareDatastoreOp::BeginDatastorePreparationInternal() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::PreparationPending); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + MOZ_ASSERT(OriginIsKnown()); + MOZ_ASSERT(!mDirectoryLock); + + if ((mDatastore = GetDatastore(Origin()))) { + MOZ_ASSERT(!mDatastore->IsClosed()); + + mDatastore->NoteLivePrepareDatastoreOp(this); + + FinishNesting(); + + return NS_OK; + } + + QM_TRY(QuotaManager::EnsureCreated()); + + // Open directory + mPendingDirectoryLock = QuotaManager::Get()->CreateDirectoryLock( + PERSISTENCE_TYPE_DEFAULT, mOriginMetadata, + mozilla::dom::quota::Client::LS, + /* aExclusive */ false); + + mNestedState = NestedState::DirectoryOpenPending; + + { + // Pin the directory lock, because Acquire might clear mPendingDirectoryLock + // during the Acquire call. + RefPtr pinnedDirectoryLock = mPendingDirectoryLock; + pinnedDirectoryLock->Acquire(this); + } + + return NS_OK; +} + +void PrepareDatastoreOp::SendToIOThread() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + // Skip all disk related stuff and transition to SendingReadyMessage if we + // are preparing a datastore for private browsing. + // Note that we do use a directory lock for private browsing even though we + // don't do any stuff on disk. The thing is that without a directory lock, + // quota manager wouldn't call AbortOperationsForLocks for our private + // browsing origin when a clear origin operation is requested. + // AbortOperationsForLocks requests all databases to close and the datastore + // is destroyed in the end. Any following LocalStorage API call will trigger + // preparation of a new (empty) datastore. + if (mPrivateBrowsingId) { + FinishNesting(); + + return; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // Must set this before dispatching otherwise we will race with the IO thread. + mNestedState = NestedState::DatabaseWorkOpen; + + MOZ_ALWAYS_SUCCEEDS( + quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +nsresult PrepareDatastoreOp::DatabaseWork() { + AssertIsOnIOThread(); + MOZ_ASSERT(mArchivedOriginScope); + MOZ_ASSERT(mUsage == 0); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::DatabaseWorkOpen); + + const auto innerFunc = [&](const auto&) -> nsresult { + // XXX This function is too long, refactor it into helper functions for + // readability. + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !MayProceedOnNonOwningThread()) { + return NS_ERROR_ABORT; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // This must be called before EnsureTemporaryStorageIsInitialized. + QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureStorageIsInitialized())); + + // This ensures that usages for existings origin directories are cached in + // memory. + QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureTemporaryStorageIsInitialized())); + + const UsageInfo usageInfo = quotaManager->GetUsageForClient( + PERSISTENCE_TYPE_DEFAULT, mOriginMetadata, + mozilla::dom::quota::Client::LS); + + const bool hasUsage = usageInfo.DatabaseUsage().isSome(); + MOZ_ASSERT(usageInfo.FileUsage().isNothing()); + + if (!gArchivedOrigins) { + QM_TRY(MOZ_TO_RESULT(LoadArchivedOrigins())); + MOZ_ASSERT(gArchivedOrigins); + } + + bool hasDataForMigration = + mArchivedOriginScope->HasMatches(gArchivedOrigins); + + // If there's nothing to preload (except the case when we want to migrate + // data during preloading), then we can finish the operation without + // creating a datastore in GetResponse (GetResponse won't create a datastore + // if mDatatabaseNotAvailable and mForPreload are both true). + if (mForPreload && !hasUsage && !hasDataForMigration) { + return DatabaseNotAvailable(); + } + + // The origin directory doesn't need to be created when we don't have data + // for migration. It will be created on the connection thread in + // Connection::EnsureStorageConnection. + // However, origin quota must be initialized, GetQuotaObject in GetResponse + // would fail otherwise. + QM_TRY_INSPECT( + const auto& directoryEntry, + ([hasDataForMigration, "aManager, + this]() -> mozilla::Result<nsCOMPtr<nsIFile>, nsresult> { + if (hasDataForMigration) { + QM_TRY_RETURN(quotaManager + ->EnsureTemporaryOriginIsInitialized( + PERSISTENCE_TYPE_DEFAULT, mOriginMetadata) + .map([](const auto& res) { return res.first; })); + } + + QM_TRY_UNWRAP(auto directoryEntry, + quotaManager->GetDirectoryForOrigin( + PERSISTENCE_TYPE_DEFAULT, Origin())); + + quotaManager->EnsureQuotaForOrigin(mOriginMetadata); + + return directoryEntry; + }())); + + QM_TRY(MOZ_TO_RESULT(directoryEntry->Append( + NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME)))); + + QM_TRY_INSPECT( + const auto& directoryPath, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, directoryEntry, GetPath)); + + // The ls directory doesn't need to be created when we don't have data for + // migration. It will be created on the connection thread in + // Connection::EnsureStorageConnection. + QM_TRY(MOZ_TO_RESULT( + EnsureDirectoryEntry(directoryEntry, + /* aCreateIfNotExists */ hasDataForMigration, + /* aIsDirectory */ true))); + + QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(kDataFileName))); + + QM_TRY(MOZ_TO_RESULT(directoryEntry->GetPath(mDatabaseFilePath))); + + // The database doesn't need to be created when we don't have data for + // migration. It will be created on the connection thread in + // Connection::EnsureStorageConnection. + bool alreadyExisted; + QM_TRY(MOZ_TO_RESULT( + EnsureDirectoryEntry(directoryEntry, + /* aCreateIfNotExists */ hasDataForMigration, + /* aIsDirectory */ false, &alreadyExisted))); + + if (alreadyExisted) { + // The database does exist. + MOZ_ASSERT(hasUsage); + + // XXX Change type of mUsage to UsageInfo or DatabaseUsageType. + mUsage = usageInfo.DatabaseUsage().valueOr(0); + } else { + // The database doesn't exist. + MOZ_ASSERT(!hasUsage); + + if (!hasDataForMigration) { + // The database doesn't exist and we don't have data for migration. + // Finish the operation, but create an empty datastore in GetResponse + // (GetResponse will create an empty datastore if mDatabaseNotAvailable + // is true and mForPreload is false). + return DatabaseNotAvailable(); + } + } + + // We initialized mDatabaseFilePath and mUsage, GetQuotaObject can now be + // called. + const RefPtr<QuotaObject> quotaObject = GetQuotaObject(); + + QM_TRY(OkIf(quotaObject), Err(NS_ERROR_FAILURE)); + + QM_TRY_INSPECT(const auto& usageFile, GetUsageFile(directoryPath)); + + QM_TRY_INSPECT(const auto& usageJournalFile, + GetUsageJournalFile(directoryPath)); + + QM_TRY_INSPECT( + const auto& connection, + (CreateStorageConnection( + *directoryEntry, *usageFile, Origin(), ["aObject, this] { + // This is called when the usage file was removed or we notice + // that the usage file doesn't exist anymore. Adjust the usage + // accordingly. + + MOZ_ALWAYS_TRUE( + quotaObject->MaybeUpdateSize(0, /* aTruncate */ true)); + + mUsage = 0; + }))); + + QM_TRY(MOZ_TO_RESULT(VerifyDatabaseInformation(connection))); + + if (hasDataForMigration) { + MOZ_ASSERT(mUsage == 0); + + { + QM_TRY_INSPECT(const auto& archiveFile, + GetArchiveFile(quotaManager->GetStoragePath())); + + auto autoArchiveDatabaseAttacher = + AutoDatabaseAttacher(connection, archiveFile, "archive"_ns); + + QM_TRY(MOZ_TO_RESULT(autoArchiveDatabaseAttacher.Attach())); + + QM_TRY_INSPECT(const int64_t& newUsage, + GetUsage(*connection, mArchivedOriginScope.get())); + + if (!quotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) { + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + + auto autoUpdateSize = MakeScopeExit(["aObject] { + MOZ_ALWAYS_TRUE( + quotaObject->MaybeUpdateSize(0, /* aTruncate */ true)); + }); + + mozStorageTransaction transaction( + connection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(transaction.Start())); + + { + nsCOMPtr<mozIStorageFunction> function = new CompressFunction(); + + QM_TRY(MOZ_TO_RESULT( + connection->CreateFunction("compress"_ns, 1, function))); + + function = new CompressionTypeFunction(); + + QM_TRY(MOZ_TO_RESULT( + connection->CreateFunction("compressionType"_ns, 1, function))); + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, connection, CreateStatement, + "INSERT INTO data (key, utf16_length, conversion_type, " + "compression_type, value) " + "SELECT key, utf16Length(value), :conversionType, " + "compressionType(value), compress(value)" + "FROM webappsstore2 " + "WHERE originKey = :originKey " + "AND originAttributes = :originAttributes;"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName( + "conversionType"_ns, + static_cast<int32_t>(LSValue::ConversionType::UTF16_UTF8)))); + + QM_TRY(MOZ_TO_RESULT(mArchivedOriginScope->BindToStatement(stmt))); + + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + + QM_TRY(MOZ_TO_RESULT(connection->RemoveFunction("compress"_ns))); + + QM_TRY( + MOZ_TO_RESULT(connection->RemoveFunction("compressionType"_ns))); + } + + { + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, connection, CreateStatement, + "UPDATE database SET usage = :usage;"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByName("usage"_ns, newUsage))); + + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + } + + { + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, connection, CreateStatement, + "DELETE FROM webappsstore2 " + "WHERE originKey = :originKey " + "AND originAttributes = :originAttributes;"_ns)); + + QM_TRY(MOZ_TO_RESULT(mArchivedOriginScope->BindToStatement(stmt))); + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + } + + QM_TRY(MOZ_TO_RESULT( + UpdateUsageFile(usageFile, usageJournalFile, newUsage))); + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + + autoUpdateSize.release(); + + QM_TRY(MOZ_TO_RESULT(usageJournalFile->Remove(false))); + + mUsage = newUsage; + + QM_TRY(MOZ_TO_RESULT(autoArchiveDatabaseAttacher.Detach())); + } + + MOZ_ASSERT(gArchivedOrigins); + MOZ_ASSERT(mArchivedOriginScope->HasMatches(gArchivedOrigins)); + mArchivedOriginScope->RemoveMatches(gArchivedOrigins); + } + + nsCOMPtr<mozIStorageConnection> shadowConnection; + if (!gInitializedShadowStorage) { + QM_TRY_UNWRAP(shadowConnection, + CreateShadowStorageConnection(quotaManager->GetBasePath())); + + gInitializedShadowStorage = true; + } + + // Must close connections before dispatching otherwise we might race with + // the connection thread which needs to open the same databases. + MOZ_ALWAYS_SUCCEEDS(connection->Close()); + + if (shadowConnection) { + MOZ_ALWAYS_SUCCEEDS(shadowConnection->Close()); + } + + // Must set this before dispatching otherwise we will race with the owning + // thread. + mNestedState = NestedState::BeginLoadData; + + QM_TRY( + MOZ_TO_RESULT(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL))); + + return NS_OK; + }; + + return ExecuteOriginInitialization( + mOriginMetadata.mOrigin, LSOriginInitialization::Datastore, + "dom::localstorage::FirstOriginInitializationAttempt::Datastore"_ns, + innerFunc); +} + +nsresult PrepareDatastoreOp::DatabaseNotAvailable() { + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::DatabaseWorkOpen); + + mDatabaseNotAvailable = true; + + nsresult rv = FinishNestingOnNonOwningThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult PrepareDatastoreOp::EnsureDirectoryEntry(nsIFile* aEntry, + bool aCreateIfNotExists, + bool aIsDirectory, + bool* aAlreadyExisted) { + AssertIsOnIOThread(); + MOZ_ASSERT(aEntry); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aEntry, Exists)); + + if (!exists) { + if (!aCreateIfNotExists) { + if (aAlreadyExisted) { + *aAlreadyExisted = false; + } + return NS_OK; + } + + if (aIsDirectory) { + QM_TRY(MOZ_TO_RESULT(aEntry->Create(nsIFile::DIRECTORY_TYPE, 0755))); + } + } +#ifdef DEBUG + else { + bool isDirectory; + MOZ_ASSERT(NS_SUCCEEDED(aEntry->IsDirectory(&isDirectory))); + MOZ_ASSERT(isDirectory == aIsDirectory); + } +#endif + + if (aAlreadyExisted) { + *aAlreadyExisted = exists; + } + return NS_OK; +} + +nsresult PrepareDatastoreOp::VerifyDatabaseInformation( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + *aConnection, "SELECT origin FROM database"_ns)); + + QM_TRY(OkIf(stmt), NS_ERROR_FILE_CORRUPTED); + + QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, stmt, GetUTF8String, 0)); + + QM_TRY(OkIf(QuotaManager::AreOriginsEqualOnDisk(Origin(), origin)), + NS_ERROR_FILE_CORRUPTED); + + return NS_OK; +} + +already_AddRefed<QuotaObject> PrepareDatastoreOp::GetQuotaObject() { + MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread()); + MOZ_ASSERT(!mOriginMetadata.mGroup.IsEmpty()); + MOZ_ASSERT(OriginIsKnown()); + MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + RefPtr<QuotaObject> quotaObject = quotaManager->GetQuotaObject( + PERSISTENCE_TYPE_DEFAULT, mOriginMetadata, + mozilla::dom::quota::Client::LS, mDatabaseFilePath, mUsage); + + if (!quotaObject) { + LS_WARNING("Failed to get quota object for group (%s) and origin (%s)!", + mOriginMetadata.mGroup.get(), Origin().get()); + } + + return quotaObject.forget(); +} + +nsresult PrepareDatastoreOp::BeginLoadData() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::BeginLoadData); + MOZ_ASSERT(!mConnection); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + return NS_ERROR_ABORT; + } + + if (!gConnectionThread) { + gConnectionThread = new ConnectionThread(); + } + + mConnection = gConnectionThread->CreateConnection( + mOriginMetadata, std::move(mArchivedOriginScope), + /* aDatabaseWasNotAvailable */ false); + MOZ_ASSERT(mConnection); + + // Must set this before dispatching otherwise we will race with the + // connection thread. + mNestedState = NestedState::DatabaseWorkLoadData; + + // Can't assign to mLoadDataOp directly since that's a weak reference and + // LoadDataOp is reference counted. + RefPtr<LoadDataOp> loadDataOp = new LoadDataOp(this); + + // This add refs loadDataOp. + mConnection->Dispatch(loadDataOp); + + // This is cleared in LoadDataOp::Cleanup() before the load data op is + // destroyed. + mLoadDataOp = loadDataOp; + + return NS_OK; +} + +void PrepareDatastoreOp::FinishNesting() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + + // The caller holds a strong reference to us, no need for a self reference + // before calling Run(). + + mState = State::SendingReadyMessage; + mNestedState = NestedState::AfterNesting; + + MOZ_ALWAYS_SUCCEEDS(Run()); +} + +nsresult PrepareDatastoreOp::FinishNestingOnNonOwningThread() { + MOZ_ASSERT(!IsOnOwningThread()); + MOZ_ASSERT(mState == State::Nesting); + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + mState = State::SendingReadyMessage; + mNestedState = NestedState::AfterNesting; + + QM_TRY( + MOZ_TO_RESULT(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL))); + + return NS_OK; +} + +nsresult PrepareDatastoreOp::NestedRun() { + nsresult rv; + + switch (mNestedState) { + case NestedState::CheckExistingOperations: + rv = CheckExistingOperations(); + break; + + case NestedState::CheckClosingDatastore: + rv = CheckClosingDatastore(); + break; + + case NestedState::PreparationPending: + rv = BeginDatastorePreparation(); + break; + + case NestedState::DatabaseWorkOpen: + rv = DatabaseWork(); + break; + + case NestedState::BeginLoadData: + rv = BeginLoadData(); + break; + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + mNestedState = NestedState::AfterNesting; + + return rv; + } + + return NS_OK; +} + +void PrepareDatastoreOp::GetResponse(LSRequestResponse& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + MOZ_ASSERT(NS_SUCCEEDED(ResultCode())); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + // A datastore is not created when we are just trying to preload data and + // there's no database file. + if (mDatabaseNotAvailable && mForPreload) { + LSRequestPreloadDatastoreResponse preloadDatastoreResponse; + + aResponse = preloadDatastoreResponse; + + return; + } + + if (!mDatastore) { + MOZ_ASSERT(mUsage == mDEBUGUsage); + + RefPtr<QuotaObject> quotaObject; + + if (mPrivateBrowsingId == 0) { + if (!mConnection) { + // This can happen when there's no database file. + MOZ_ASSERT(mDatabaseNotAvailable); + + // Even though there's no database file, we need to create a connection + // and pass it to datastore. + if (!gConnectionThread) { + gConnectionThread = new ConnectionThread(); + } + + mConnection = gConnectionThread->CreateConnection( + mOriginMetadata, std::move(mArchivedOriginScope), + /* aDatabaseWasNotAvailable */ true); + MOZ_ASSERT(mConnection); + } + + quotaObject = GetQuotaObject(); + if (!quotaObject) { + aResponse = NS_ERROR_FAILURE; + return; + } + } + + mDatastore = new Datastore( + mOriginMetadata, mPrivateBrowsingId, mUsage, mSizeOfKeys, mSizeOfItems, + std::move(mDirectoryLock), std::move(mConnection), + std::move(quotaObject), mValues, std::move(mOrderedItems)); + + mDatastore->NoteLivePrepareDatastoreOp(this); + + if (!gDatastores) { + gDatastores = new DatastoreHashtable(); + } + + MOZ_ASSERT(!gDatastores->Contains(Origin())); + gDatastores->InsertOrUpdate(Origin(), + WrapMovingNotNullUnchecked(mDatastore)); + } + + if (mPrivateBrowsingId && !mInvalidated) { + if (!gPrivateDatastores) { + gPrivateDatastores = MakeUnique<PrivateDatastoreHashtable>(); + } + + gPrivateDatastores->LookupOrInsertWith(Origin(), [&] { + auto privateDatastore = + MakeUnique<PrivateDatastore>(WrapMovingNotNull(mDatastore)); + + mPrivateDatastoreRegistered.Flip(); + + return privateDatastore; + }); + } + + mDatastoreId = ++gLastDatastoreId; + + if (!gPreparedDatastores) { + gPreparedDatastores = new PreparedDatastoreHashtable(); + } + const auto& preparedDatastore = gPreparedDatastores->InsertOrUpdate( + mDatastoreId, MakeUnique<PreparedDatastore>( + mDatastore, mContentParentId, Origin(), mDatastoreId, + /* aForPreload */ mForPreload)); + + if (mInvalidated) { + preparedDatastore->Invalidate(); + } + + mPreparedDatastoreRegistered.Flip(); + + if (mForPreload) { + LSRequestPreloadDatastoreResponse preloadDatastoreResponse; + + aResponse = preloadDatastoreResponse; + } else { + LSRequestPrepareDatastoreResponse prepareDatastoreResponse; + prepareDatastoreResponse.datastoreId() = mDatastoreId; + + aResponse = prepareDatastoreResponse; + } +} + +void PrepareDatastoreOp::Cleanup() { + AssertIsOnOwningThread(); + + if (mDatastore) { + MOZ_ASSERT(!mDirectoryLock); + MOZ_ASSERT(!mConnection); + + if (NS_FAILED(ResultCode())) { + if (mPrivateDatastoreRegistered) { + MOZ_ASSERT(gPrivateDatastores); + DebugOnly<bool> removed = gPrivateDatastores->Remove(Origin()); + MOZ_ASSERT(removed); + + if (!gPrivateDatastores->Count()) { + gPrivateDatastores = nullptr; + } + } + + if (mPreparedDatastoreRegistered) { + // Just in case we failed to send datastoreId to the child, we need to + // destroy prepared datastore, otherwise it won't be destroyed until + // the timer fires (after 20 seconds). + MOZ_ASSERT(gPreparedDatastores); + MOZ_ASSERT(mDatastoreId > 0); + DebugOnly<bool> removed = gPreparedDatastores->Remove(mDatastoreId); + MOZ_ASSERT(removed); + + if (!gPreparedDatastores->Count()) { + gPreparedDatastores = nullptr; + } + } + } + + // Make sure to release the datastore on this thread. + + mDatastore->NoteFinishedPrepareDatastoreOp(this); + + mDatastore = nullptr; + + CleanupMetadata(); + } else if (mConnection) { + // If we have a connection then the operation must have failed and there + // must be a directory lock too. + MOZ_ASSERT(NS_FAILED(ResultCode())); + MOZ_ASSERT(mDirectoryLock); + + // We must close the connection on the connection thread before releasing + // it on this thread. The directory lock can't be released either. + nsCOMPtr<nsIRunnable> callback = + NewRunnableMethod("dom::OpenDatabaseOp::ConnectionClosedCallback", this, + &PrepareDatastoreOp::ConnectionClosedCallback); + + mConnection->Close(callback); + } else { + // If we don't have a connection, but we do have a directory lock then the + // operation must have failed or we were preloading a datastore and there + // was no physical database on disk. + MOZ_ASSERT_IF(mDirectoryLock, + NS_FAILED(ResultCode()) || mDatabaseNotAvailable); + + // There's no connection, so it's safe to release the directory lock and + // unregister itself from the array. + + mDirectoryLock = nullptr; + + CleanupMetadata(); + } +} + +void PrepareDatastoreOp::ConnectionClosedCallback() { + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(ResultCode())); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(mConnection); + + mConnection = nullptr; + mDirectoryLock = nullptr; + + CleanupMetadata(); +} + +void PrepareDatastoreOp::CleanupMetadata() { + AssertIsOnOwningThread(); + + if (mDelayedOp) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mDelayedOp.forget())); + } + + MOZ_ASSERT(gPrepareDatastoreOps); + gPrepareDatastoreOps->RemoveElement(this); + + QuotaManager::MaybeRecordQuotaClientShutdownStep( + quota::Client::LS, "PrepareDatastoreOp completed"_ns); + + if (gPrepareDatastoreOps->IsEmpty()) { + gPrepareDatastoreOps = nullptr; + } +} + +NS_IMPL_ISUPPORTS_INHERITED0(PrepareDatastoreOp, LSRequestBase) + +void PrepareDatastoreOp::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + LSRequestBase::ActorDestroy(aWhy); + + if (mLoadDataOp) { + mLoadDataOp->NoteComplete(); + } +} + +void PrepareDatastoreOp::DirectoryLockAcquired(DirectoryLock* aLock) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + mPendingDirectoryLock = nullptr; + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + MaybeSetFailureCode(NS_ERROR_ABORT); + + FinishNesting(); + + return; + } + + mDirectoryLock = aLock; + + SendToIOThread(); +} + +void PrepareDatastoreOp::DirectoryLockFailed() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + mPendingDirectoryLock = nullptr; + + MaybeSetFailureCode(NS_ERROR_FAILURE); + + FinishNesting(); +} + +nsresult PrepareDatastoreOp::LoadDataOp::DoDatastoreWork() { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mPrepareDatastoreOp); + MOZ_ASSERT(mPrepareDatastoreOp->mState == State::Nesting); + MOZ_ASSERT(mPrepareDatastoreOp->mNestedState == + NestedState::DatabaseWorkLoadData); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !MayProceedOnNonOwningThread()) { + return NS_ERROR_ABORT; + } + + QM_TRY_INSPECT( + const auto& stmt, + mConnection->BorrowCachedStatement( + "SELECT key, utf16_length, conversion_type, compression_type, value " + "FROM data;"_ns)); + + QM_TRY(quota::CollectWhileHasResult( + *stmt, [this](auto& stmt) -> mozilla::Result<Ok, nsresult> { + QM_TRY_UNWRAP(auto key, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, stmt, GetString, 0)); + + LSValue value; + QM_TRY(MOZ_TO_RESULT(value.InitFromStatement(&stmt, 1))); + + mPrepareDatastoreOp->mValues.InsertOrUpdate(key, value); + mPrepareDatastoreOp->mSizeOfKeys += key.Length(); + mPrepareDatastoreOp->mSizeOfItems += key.Length() + value.Length(); +#ifdef DEBUG + mPrepareDatastoreOp->mDEBUGUsage += key.Length() + value.UTF16Length(); +#endif + + auto item = mPrepareDatastoreOp->mOrderedItems.AppendElement(); + item->key() = std::move(key); + item->value() = std::move(value); + + return Ok{}; + })); + + return NS_OK; +} + +void PrepareDatastoreOp::LoadDataOp::OnSuccess() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mPrepareDatastoreOp); + MOZ_ASSERT(mPrepareDatastoreOp->mState == State::Nesting); + MOZ_ASSERT(mPrepareDatastoreOp->mNestedState == + NestedState::DatabaseWorkLoadData); + MOZ_ASSERT(mPrepareDatastoreOp->mLoadDataOp == this); + + mPrepareDatastoreOp->FinishNesting(); +} + +void PrepareDatastoreOp::LoadDataOp::OnFailure(nsresult aResultCode) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mPrepareDatastoreOp); + MOZ_ASSERT(mPrepareDatastoreOp->mState == State::Nesting); + MOZ_ASSERT(mPrepareDatastoreOp->mNestedState == + NestedState::DatabaseWorkLoadData); + MOZ_ASSERT(mPrepareDatastoreOp->mLoadDataOp == this); + + mPrepareDatastoreOp->SetFailureCode(aResultCode); + + mPrepareDatastoreOp->FinishNesting(); +} + +void PrepareDatastoreOp::LoadDataOp::Cleanup() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mPrepareDatastoreOp); + MOZ_ASSERT(mPrepareDatastoreOp->mLoadDataOp == this); + + mPrepareDatastoreOp->mLoadDataOp = nullptr; + mPrepareDatastoreOp = nullptr; + + ConnectionDatastoreOperationBase::Cleanup(); +} + +NS_IMPL_ISUPPORTS(PrepareDatastoreOp::CompressFunction, mozIStorageFunction) + +NS_IMETHODIMP +PrepareDatastoreOp::CompressFunction::OnFunctionCall( + mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { + AssertIsOnIOThread(); + MOZ_ASSERT(aFunctionArguments); + MOZ_ASSERT(aResult); + +#ifdef DEBUG + { + uint32_t argCount; + MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetNumEntries(&argCount)); + MOZ_ASSERT(argCount == 1); + + int32_t type; + MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetTypeOfIndex(0, &type)); + MOZ_ASSERT(type == mozIStorageValueArray::VALUE_TYPE_TEXT); + } +#endif + + QM_TRY_INSPECT(const auto& value, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, aFunctionArguments, GetUTF8String, 0)); + + nsCString compressed; + QM_TRY(OkIf(SnappyCompress(value, compressed)), NS_ERROR_OUT_OF_MEMORY); + + const nsCString& buffer = compressed.IsVoid() ? value : compressed; + + // mozStorage transforms empty blobs into null values, but our database + // schema doesn't allow null values. We can workaround this by storing + // empty buffers as UTF8 text (SQLite supports the type affinity, so the type + // of the column is not fixed). + nsCOMPtr<nsIVariant> result; + if (0u == buffer.Length()) { // Otherwise empty string becomes null + result = new storage::UTF8TextVariant(buffer); + } else { + result = new storage::BlobVariant(std::make_pair( + static_cast<const void*>(buffer.get()), int(buffer.Length()))); + } + + result.forget(aResult); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(PrepareDatastoreOp::CompressionTypeFunction, + mozIStorageFunction) + +NS_IMETHODIMP +PrepareDatastoreOp::CompressionTypeFunction::OnFunctionCall( + mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { + AssertIsOnIOThread(); + MOZ_ASSERT(aFunctionArguments); + MOZ_ASSERT(aResult); + +#ifdef DEBUG + { + uint32_t argCount; + MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetNumEntries(&argCount)); + MOZ_ASSERT(argCount == 1); + + int32_t type; + MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetTypeOfIndex(0, &type)); + MOZ_ASSERT(type == mozIStorageValueArray::VALUE_TYPE_TEXT); + } +#endif + + QM_TRY_INSPECT(const auto& value, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, aFunctionArguments, GetUTF8String, 0)); + + nsCString compressed; + QM_TRY(OkIf(SnappyCompress(value, compressed)), NS_ERROR_OUT_OF_MEMORY); + + const int32_t compression = static_cast<int32_t>( + compressed.IsVoid() ? LSValue::CompressionType::UNCOMPRESSED + : LSValue::CompressionType::SNAPPY); + + nsCOMPtr<nsIVariant> result = new storage::IntegerVariant(compression); + + result.forget(aResult); + return NS_OK; +} + +/******************************************************************************* + * PrepareObserverOp + ******************************************************************************/ + +PrepareObserverOp::PrepareObserverOp( + const LSRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId) + : LSRequestBase(aParams, aContentParentId) { + MOZ_ASSERT(aParams.type() == + LSRequestParams::TLSRequestPrepareObserverParams); +} + +nsresult PrepareObserverOp::Start() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::StartingRequest); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + const LSRequestPrepareObserverParams params = + mParams.get_LSRequestPrepareObserverParams(); + + const PrincipalInfo& storagePrincipalInfo = params.storagePrincipalInfo(); + + if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + mOrigin = QuotaManager::GetOriginForChrome(); + } else { + MOZ_ASSERT(storagePrincipalInfo.type() == + PrincipalInfo::TContentPrincipalInfo); + + mOrigin = + QuotaManager::GetOriginFromValidatedPrincipalInfo(storagePrincipalInfo); + } + + mState = State::SendingReadyMessage; + MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +void PrepareObserverOp::GetResponse(LSRequestResponse& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + MOZ_ASSERT(NS_SUCCEEDED(ResultCode())); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + uint64_t observerId = ++gLastObserverId; + + RefPtr<Observer> observer = new Observer(mOrigin); + + if (!gPreparedObsevers) { + gPreparedObsevers = new PreparedObserverHashtable(); + } + gPreparedObsevers->InsertOrUpdate(observerId, std::move(observer)); + + LSRequestPrepareObserverResponse prepareObserverResponse; + prepareObserverResponse.observerId() = observerId; + + aResponse = prepareObserverResponse; +} + +/******************************************************************************* ++ * LSSimpleRequestBase ++ +******************************************************************************/ + +LSSimpleRequestBase::LSSimpleRequestBase( + const LSSimpleRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId) + : mParams(aParams), + mContentParentId(aContentParentId), + mState(State::Initial) {} + +LSSimpleRequestBase::~LSSimpleRequestBase() { + MOZ_ASSERT_IF(MayProceedOnNonOwningThread(), + mState == State::Initial || mState == State::Completed); +} + +void LSSimpleRequestBase::Dispatch() { + AssertIsOnOwningThread(); + + mState = State::StartingRequest; + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(this)); +} + +bool LSSimpleRequestBase::VerifyRequestParams() { + AssertIsOnBackgroundThread(); + + MOZ_ASSERT(mParams.type() != LSSimpleRequestParams::T__None); + + switch (mParams.type()) { + case LSSimpleRequestParams::TLSSimpleRequestPreloadedParams: { + const LSSimpleRequestPreloadedParams& params = + mParams.get_LSSimpleRequestPreloadedParams(); + + if (NS_WARN_IF(!VerifyPrincipalInfo( + params.principalInfo(), params.storagePrincipalInfo(), false))) { + return false; + } + + break; + } + + case LSSimpleRequestParams::TLSSimpleRequestGetStateParams: { + const LSSimpleRequestGetStateParams& params = + mParams.get_LSSimpleRequestGetStateParams(); + + if (NS_WARN_IF(!VerifyPrincipalInfo( + params.principalInfo(), params.storagePrincipalInfo(), false))) { + return false; + } + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +nsresult LSSimpleRequestBase::StartRequest() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::StartingRequest); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + return NS_ERROR_ABORT; + } + +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + bool trustParams = false; +#else + bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager()); +#endif + + if (!trustParams && NS_WARN_IF(!VerifyRequestParams())) { + return NS_ERROR_FAILURE; + } + + QM_TRY(MOZ_TO_RESULT(Start())); + + return NS_OK; +} + +void LSSimpleRequestBase::SendResults() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + MaybeSetFailureCode(NS_ERROR_ABORT); + } + + if (MayProceed()) { + LSSimpleRequestResponse response; + + if (NS_SUCCEEDED(ResultCode())) { + GetResponse(response); + } else { + response = ResultCode(); + } + + Unused << PBackgroundLSSimpleRequestParent::Send__delete__(this, response); + } + + mState = State::Completed; +} + +NS_IMETHODIMP +LSSimpleRequestBase::Run() { + nsresult rv; + + switch (mState) { + case State::StartingRequest: + rv = StartRequest(); + break; + + case State::SendingResults: + SendResults(); + return NS_OK; + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingResults) { + MaybeSetFailureCode(rv); + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + mState = State::SendingResults; + + if (IsOnOwningThread()) { + SendResults(); + } else { + MOZ_ALWAYS_SUCCEEDS( + OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); + } + } + + return NS_OK; +} + +void LSSimpleRequestBase::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + NoteComplete(); +} + +/******************************************************************************* + * PreloadedOp + ******************************************************************************/ + +PreloadedOp::PreloadedOp(const LSSimpleRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId) + : LSSimpleRequestBase(aParams, aContentParentId) { + MOZ_ASSERT(aParams.type() == + LSSimpleRequestParams::TLSSimpleRequestPreloadedParams); +} + +nsresult PreloadedOp::Start() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::StartingRequest); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + const LSSimpleRequestPreloadedParams& params = + mParams.get_LSSimpleRequestPreloadedParams(); + + const PrincipalInfo& storagePrincipalInfo = params.storagePrincipalInfo(); + + MOZ_ASSERT( + storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo || + storagePrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo); + mOrigin = storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo + ? nsCString{QuotaManager::GetOriginForChrome()} + : QuotaManager::GetOriginFromValidatedPrincipalInfo( + storagePrincipalInfo); + + mState = State::SendingResults; + MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +void PreloadedOp::GetResponse(LSSimpleRequestResponse& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + MOZ_ASSERT(NS_SUCCEEDED(ResultCode())); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + bool preloaded; + RefPtr<Datastore> datastore; + if ((datastore = GetDatastore(mOrigin)) && !datastore->IsClosed()) { + preloaded = true; + } else { + preloaded = false; + } + + LSSimpleRequestPreloadedResponse preloadedResponse; + preloadedResponse.preloaded() = preloaded; + + aResponse = preloadedResponse; +} + +/******************************************************************************* + * GetStateOp + ******************************************************************************/ + +GetStateOp::GetStateOp(const LSSimpleRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId) + : LSSimpleRequestBase(aParams, aContentParentId) { + MOZ_ASSERT(aParams.type() == + LSSimpleRequestParams::TLSSimpleRequestGetStateParams); +} + +nsresult GetStateOp::Start() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::StartingRequest); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + const LSSimpleRequestGetStateParams& params = + mParams.get_LSSimpleRequestGetStateParams(); + + const PrincipalInfo& storagePrincipalInfo = params.storagePrincipalInfo(); + + MOZ_ASSERT( + storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo || + storagePrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo); + mOrigin = storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo + ? nsCString{QuotaManager::GetOriginForChrome()} + : QuotaManager::GetOriginFromValidatedPrincipalInfo( + storagePrincipalInfo); + + mState = State::SendingResults; + MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +void GetStateOp::GetResponse(LSSimpleRequestResponse& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + MOZ_ASSERT(NS_SUCCEEDED(ResultCode())); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + LSSimpleRequestGetStateResponse getStateResponse; + + if (RefPtr<Datastore> datastore = GetDatastore(mOrigin)) { + if (!datastore->IsClosed()) { + getStateResponse.itemInfos() = datastore->GetOrderedItems().Clone(); + } + } + + aResponse = getStateResponse; +} + +/******************************************************************************* + * ArchivedOriginScope + ******************************************************************************/ + +// static +UniquePtr<ArchivedOriginScope> ArchivedOriginScope::CreateFromOrigin( + const nsACString& aOriginAttrSuffix, const nsACString& aOriginKey) { + return WrapUnique( + new ArchivedOriginScope(Origin(aOriginAttrSuffix, aOriginKey))); +} + +// static +UniquePtr<ArchivedOriginScope> ArchivedOriginScope::CreateFromPrefix( + const nsACString& aOriginKey) { + return WrapUnique(new ArchivedOriginScope(Prefix(aOriginKey))); +} + +// static +UniquePtr<ArchivedOriginScope> ArchivedOriginScope::CreateFromPattern( + const OriginAttributesPattern& aPattern) { + return WrapUnique(new ArchivedOriginScope(Pattern(aPattern))); +} + +// static +UniquePtr<ArchivedOriginScope> ArchivedOriginScope::CreateFromNull() { + return WrapUnique(new ArchivedOriginScope(Null())); +} + +nsLiteralCString ArchivedOriginScope::GetBindingClause() const { + return mData.match( + [](const Origin&) { + return " WHERE originKey = :originKey " + "AND originAttributes = :originAttributes"_ns; + }, + [](const Pattern&) { + return " WHERE originAttributes MATCH :originAttributesPattern"_ns; + }, + [](const Prefix&) { return " WHERE originKey = :originKey"_ns; }, + [](const Null&) { return ""_ns; }); +} + +nsresult ArchivedOriginScope::BindToStatement( + mozIStorageStatement* aStmt) const { + MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread()); + MOZ_ASSERT(aStmt); + + struct Matcher { + mozIStorageStatement* mStmt; + + explicit Matcher(mozIStorageStatement* aStmt) : mStmt(aStmt) {} + + nsresult operator()(const Origin& aOrigin) { + QM_TRY(MOZ_TO_RESULT(mStmt->BindUTF8StringByName( + "originKey"_ns, aOrigin.OriginNoSuffix()))); + + QM_TRY(MOZ_TO_RESULT(mStmt->BindUTF8StringByName( + "originAttributes"_ns, aOrigin.OriginSuffix()))); + + return NS_OK; + } + + nsresult operator()(const Prefix& aPrefix) { + QM_TRY(MOZ_TO_RESULT(mStmt->BindUTF8StringByName( + "originKey"_ns, aPrefix.OriginNoSuffix()))); + + return NS_OK; + } + + nsresult operator()(const Pattern& aPattern) { + QM_TRY(MOZ_TO_RESULT(mStmt->BindUTF8StringByName( + "originAttributesPattern"_ns, "pattern1"_ns))); + + return NS_OK; + } + + nsresult operator()(const Null& aNull) { return NS_OK; } + }; + + QM_TRY(MOZ_TO_RESULT(mData.match(Matcher(aStmt)))); + + return NS_OK; +} + +bool ArchivedOriginScope::HasMatches( + ArchivedOriginHashtable* aHashtable) const { + AssertIsOnIOThread(); + MOZ_ASSERT(aHashtable); + + return mData.match( + [aHashtable](const Origin& aOrigin) { + const nsCString hashKey = GetArchivedOriginHashKey( + aOrigin.OriginSuffix(), aOrigin.OriginNoSuffix()); + + return aHashtable->Contains(hashKey); + }, + [aHashtable](const Pattern& aPattern) { + return std::any_of( + aHashtable->Values().cbegin(), aHashtable->Values().cend(), + [&aPattern](const auto& entry) { + return aPattern.GetPattern().Matches(entry->mOriginAttributes); + }); + }, + [aHashtable](const Prefix& aPrefix) { + return std::any_of( + aHashtable->Values().cbegin(), aHashtable->Values().cend(), + [&aPrefix](const auto& entry) { + return entry->mOriginNoSuffix == aPrefix.OriginNoSuffix(); + }); + }, + [aHashtable](const Null& aNull) { return !aHashtable->IsEmpty(); }); +} + +void ArchivedOriginScope::RemoveMatches( + ArchivedOriginHashtable* aHashtable) const { + AssertIsOnIOThread(); + MOZ_ASSERT(aHashtable); + + struct Matcher { + ArchivedOriginHashtable* mHashtable; + + explicit Matcher(ArchivedOriginHashtable* aHashtable) + : mHashtable(aHashtable) {} + + void operator()(const Origin& aOrigin) { + nsCString hashKey = GetArchivedOriginHashKey(aOrigin.OriginSuffix(), + aOrigin.OriginNoSuffix()); + + mHashtable->Remove(hashKey); + } + + void operator()(const Prefix& aPrefix) { + for (auto iter = mHashtable->Iter(); !iter.Done(); iter.Next()) { + const auto& archivedOriginInfo = iter.Data(); + + if (archivedOriginInfo->mOriginNoSuffix == aPrefix.OriginNoSuffix()) { + iter.Remove(); + } + } + } + + void operator()(const Pattern& aPattern) { + for (auto iter = mHashtable->Iter(); !iter.Done(); iter.Next()) { + const auto& archivedOriginInfo = iter.Data(); + + if (aPattern.GetPattern().Matches( + archivedOriginInfo->mOriginAttributes)) { + iter.Remove(); + } + } + } + + void operator()(const Null& aNull) { mHashtable->Clear(); } + }; + + mData.match(Matcher(aHashtable)); +} + +/******************************************************************************* + * QuotaClient + ******************************************************************************/ + +QuotaClient* QuotaClient::sInstance = nullptr; + +QuotaClient::QuotaClient() + : mShadowDatabaseMutex("LocalStorage mShadowDatabaseMutex") { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!sInstance, "We expect this to be a singleton!"); + + sInstance = this; +} + +QuotaClient::~QuotaClient() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(sInstance == this, "We expect this to be a singleton!"); + + sInstance = nullptr; +} + +// static +nsresult QuotaClient::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = Observer::Initialize(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +mozilla::dom::quota::Client::Type QuotaClient::GetType() { + return QuotaClient::LS; +} + +Result<UsageInfo, nsresult> QuotaClient::InitOrigin( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_INSPECT(const auto& directory, + quotaManager->GetDirectoryForOrigin(aPersistenceType, + aOriginMetadata.mOrigin)); + + MOZ_ASSERT(directory); + + QM_TRY(MOZ_TO_RESULT( + directory->Append(NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME)))); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); + MOZ_ASSERT(exists); + } +#endif + + QM_TRY_INSPECT(const auto& directoryPath, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, directory, GetPath)); + + QM_TRY_INSPECT(const auto& usageFile, GetUsageFile(directoryPath)); + + // XXX Try to make usageFileExists const + QM_TRY_UNWRAP(bool usageFileExists, ExistsAsFile(*usageFile)); + + QM_TRY_INSPECT(const auto& usageJournalFile, + GetUsageJournalFile(directoryPath)); + + QM_TRY_INSPECT(const bool& usageJournalFileExists, + ExistsAsFile(*usageJournalFile)); + + if (usageJournalFileExists) { + if (usageFileExists) { + QM_TRY(MOZ_TO_RESULT(usageFile->Remove(false))); + + usageFileExists = false; + } + + QM_TRY(MOZ_TO_RESULT(usageJournalFile->Remove(false))); + } + + QM_TRY_INSPECT(const auto& file, + CloneFileAndAppend(*directory, kDataFileName)); + + QM_TRY_INSPECT(const bool& fileExists, ExistsAsFile(*file)); + + QM_TRY_INSPECT( + const UsageInfo& res, + ([fileExists, usageFileExists, &file, &usageFile, &usageJournalFile, + &aOriginMetadata]() -> Result<UsageInfo, nsresult> { + if (fileExists) { + QM_TRY_RETURN(QM_OR_ELSE_WARN( + // Expression. To simplify control flow, we call LoadUsageFile + // unconditionally here, even though it will necessarily fail if + // usageFileExists is false. + LoadUsageFile(*usageFile), + // Fallback. + ([&file, &usageFile, &usageJournalFile, &aOriginMetadata]( + const nsresult) -> Result<UsageInfo, nsresult> { + QM_TRY_INSPECT( + const auto& connection, + CreateStorageConnection(*file, *usageFile, + aOriginMetadata.mOrigin, [] {})); + + QM_TRY_INSPECT(const int64_t& usage, + GetUsage(*connection, + /* aArchivedOriginScope */ nullptr)); + + QM_TRY(MOZ_TO_RESULT( + UpdateUsageFile(usageFile, usageJournalFile, usage))); + + QM_TRY(MOZ_TO_RESULT(usageJournalFile->Remove(false))); + + MOZ_ASSERT(usage >= 0); + return UsageInfo{DatabaseUsageType(Some(uint64_t(usage)))}; + }))); + } + + if (usageFileExists) { + QM_TRY(MOZ_TO_RESULT(usageFile->Remove(false))); + } + + return UsageInfo{}; + }())); + + // Report unknown files in debug builds, but don't fail, just warn (we don't + // report unknown files in release builds because that requires extra + // scanning of the directory which would slow down entire initialization for + // little benefit). + +#ifdef DEBUG + QM_TRY(CollectEachFileAtomicCancelable( + *directory, aCanceled, + [](const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: + Unused << WARN_IF_FILE_IS_UNKNOWN(*file); + break; + + case nsIFileKind::ExistsAsFile: { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, file, GetLeafName)); + + if (leafName.Equals(kDataFileName) || + leafName.Equals(kJournalFileName) || + leafName.Equals(kUsageFileName) || + leafName.Equals(kUsageJournalFileName)) { + return Ok{}; + } + + Unused << WARN_IF_FILE_IS_UNKNOWN(*file); + + break; + } + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + return Ok{}; + })); +#endif + + return res; +} + +nsresult QuotaClient::InitOriginWithoutTracking( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) { + AssertIsOnIOThread(); + + // This is called when a storage/permanent/${origin}/ls directory exists. Even + // though this shouldn't happen with a "good" profile, we shouldn't return an + // error here, since that would cause origin initialization to fail. We just + // warn and otherwise ignore that. + UNKNOWN_FILE_WARNING(NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME)); + return NS_OK; +} + +Result<UsageInfo, nsresult> QuotaClient::GetUsageForOrigin( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT); + + // We can't open the database at this point, since it can be already used + // by the connection thread. Use the cached value instead. + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + return quotaManager->GetUsageForClient(PERSISTENCE_TYPE_DEFAULT, + aOriginMetadata, Client::LS); +} + +nsresult QuotaClient::AboutToClearOrigins( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope) { + AssertIsOnIOThread(); + + // This method is not called when the clearing is triggered by the eviction + // process. It's on purpose to avoid a problem with the origin access time + // which can be described as follows: + // When there's a storage pressure condition and quota manager starts + // collecting origins for eviction, there can be an origin that hasn't been + // touched for long time. However, the old implementation of local storage + // could have touched the origin only recently and the new implementation + // hasn't had a chance to create a new per origin database for it yet (the + // data is still in the archive database), so the origin access time hasn't + // been updated either. In the end, the origin would be evicted despite the + // fact that there was recent local storage activity. + // So this method clears the archived data and shadow database entries for + // given origin scope, but only if it's a privacy-related origin clearing. + + if (!aPersistenceType.IsNull() && + aPersistenceType.Value() != PERSISTENCE_TYPE_DEFAULT) { + return NS_OK; + } + + // There can be no data for the system principal in the archive or the shadow + // database. This early return silences potential warnings caused by failed + // `CreateAerchivedOriginScope` because it calls `GenerateOriginKey2` which + // doesn't support the system principal. + if (aOriginScope.IsOrigin() && + aOriginScope.GetOrigin() == QuotaManager::GetOriginForChrome()) { + return NS_OK; + } + + const bool shadowWrites = gShadowWrites; + + QM_TRY_INSPECT(const auto& archivedOriginScope, + CreateArchivedOriginScope(aOriginScope)); + + if (!gArchivedOrigins) { + QM_TRY(MOZ_TO_RESULT(LoadArchivedOrigins())); + MOZ_ASSERT(gArchivedOrigins); + } + + const bool hasDataForRemoval = + archivedOriginScope->HasMatches(gArchivedOrigins); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + const nsString& basePath = quotaManager->GetBasePath(); + + { + MutexAutoLock shadowDatabaseLock(mShadowDatabaseMutex); + + QM_TRY_INSPECT( + const auto& connection, + ([&basePath]() -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> { + if (gInitializedShadowStorage) { + QM_TRY_RETURN(GetShadowStorageConnection(basePath)); + } + + QM_TRY_UNWRAP(auto connection, + CreateShadowStorageConnection(basePath)); + + gInitializedShadowStorage = true; + + return connection; + }())); + + { + Maybe<AutoDatabaseAttacher> maybeAutoArchiveDatabaseAttacher; + + if (hasDataForRemoval) { + QM_TRY_INSPECT(const auto& archiveFile, + GetArchiveFile(quotaManager->GetStoragePath())); + + maybeAutoArchiveDatabaseAttacher.emplace( + AutoDatabaseAttacher(connection, archiveFile, "archive"_ns)); + + QM_TRY(MOZ_TO_RESULT(maybeAutoArchiveDatabaseAttacher->Attach())); + } + + if (archivedOriginScope->IsPattern()) { + nsCOMPtr<mozIStorageFunction> function( + new MatchFunction(archivedOriginScope->GetPattern())); + + QM_TRY( + MOZ_TO_RESULT(connection->CreateFunction("match"_ns, 2, function))); + } + + { + QM_TRY_INSPECT(const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, connection, + CreateStatement, "BEGIN IMMEDIATE;"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + } + + if (shadowWrites) { + QM_TRY(MOZ_TO_RESULT( + PerformDelete(connection, "main"_ns, archivedOriginScope.get()))); + } + + if (hasDataForRemoval) { + QM_TRY(MOZ_TO_RESULT(PerformDelete(connection, "archive"_ns, + archivedOriginScope.get()))); + } + + { + QM_TRY_INSPECT(const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, connection, + CreateStatement, "COMMIT;"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + } + + if (archivedOriginScope->IsPattern()) { + QM_TRY(MOZ_TO_RESULT(connection->RemoveFunction("match"_ns))); + } + + if (hasDataForRemoval) { + MOZ_ASSERT(maybeAutoArchiveDatabaseAttacher.isSome()); + QM_TRY(MOZ_TO_RESULT(maybeAutoArchiveDatabaseAttacher->Detach())); + + maybeAutoArchiveDatabaseAttacher.reset(); + + MOZ_ASSERT(gArchivedOrigins); + MOZ_ASSERT(archivedOriginScope->HasMatches(gArchivedOrigins)); + archivedOriginScope->RemoveMatches(gArchivedOrigins); + } + } + QM_TRY(MOZ_TO_RESULT(connection->Close())); + } + + if (aOriginScope.IsNull()) { + QM_TRY_INSPECT(const auto& shadowFile, GetShadowFile(basePath)); + + QM_TRY(MOZ_TO_RESULT(shadowFile->Remove(false))); + + gInitializedShadowStorage = false; + } + + return NS_OK; +} + +void QuotaClient::OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) { + AssertIsOnIOThread(); +} + +void QuotaClient::ReleaseIOThreadObjects() { + AssertIsOnIOThread(); + + gInitializationInfo = nullptr; + + // Delete archived origins hashtable since QuotaManager clears the whole + // storage directory including ls-archive.sqlite. + + gArchivedOrigins = nullptr; +} + +void QuotaClient::AbortOperationsForLocks( + const DirectoryLockIdTable& aDirectoryLockIds) { + AssertIsOnBackgroundThread(); + + // A PrepareDatastoreOp object could already acquire a directory lock for + // the given origin. Its last step is creation of a Datastore object (which + // will take ownership of the directory lock) and a PreparedDatastore object + // which keeps the Datastore alive until a database actor is created. + // We need to invalidate the PreparedDatastore object when it's created, + // otherwise the Datastore object can block the origin clear operation for + // long time. It's not a problem that we don't fail the PrepareDatastoreOp + // immediatelly (avoiding the creation of the Datastore and PreparedDatastore + // object). We will call RequestAllowToClose on the database actor once it's + // created and the child actor will respond by sending AllowToClose which + // will close the Datastore on the parent side (the closing releases the + // directory lock). + + InvalidatePrepareDatastoreOpsMatching( + [&aDirectoryLockIds](const auto& prepareDatastoreOp) { + // Check if the PrepareDatastoreOp holds an acquired DirectoryLock. + // Origin clearing can't be blocked by this PrepareDatastoreOp if there + // is no acquired DirectoryLock. If there is an acquired DirectoryLock, + // check if the table contains the lock for the PrepareDatastoreOp. + return IsLockForObjectAcquiredAndContainedInLockTable( + prepareDatastoreOp, aDirectoryLockIds); + }); + + if (gPrivateDatastores) { + gPrivateDatastores->RemoveIf([&aDirectoryLockIds](const auto& iter) { + const auto& privateDatastore = iter.Data(); + + // The PrivateDatastore::mDatastore member is not cleared until the + // PrivateDatastore is destroyed. + const auto& datastore = privateDatastore->DatastoreRef(); + + // If the PrivateDatastore exists then it must be registered in + // Datastore::mHasLivePrivateDatastore as well. The Datastore must have + // a DirectoryLock if there is a registered PrivateDatastore. + return IsLockForObjectContainedInLockTable(datastore, aDirectoryLockIds); + }); + + if (!gPrivateDatastores->Count()) { + gPrivateDatastores = nullptr; + } + } + + InvalidatePreparedDatastoresMatching([&aDirectoryLockIds]( + const auto& preparedDatastore) { + // The PreparedDatastore::mDatastore member is not cleared until the + // PreparedDatastore is destroyed. + const auto& datastore = preparedDatastore.DatastoreRef(); + + // If the PreparedDatastore exists then it must be registered in + // Datastore::mPreparedDatastores as well. The Datastore must have a + // DirectoryLock if there are registered PreparedDatastore objects. + return IsLockForObjectContainedInLockTable(datastore, aDirectoryLockIds); + }); + + RequestAllowToCloseDatabasesMatching( + [&aDirectoryLockIds](const auto& database) { + const auto& maybeDatastore = database.MaybeDatastoreRef(); + + // If the Database is registered in gLiveDatabases then it must have a + // Datastore. + MOZ_ASSERT(maybeDatastore.isSome()); + + // If the Database is registered in gLiveDatabases then it must be + // registered in Datastore::mDatabases as well. The Datastore must have + // a DirectoryLock if there are registered Database objects. + return IsLockForObjectContainedInLockTable(*maybeDatastore, + aDirectoryLockIds); + }); +} + +void QuotaClient::AbortOperationsForProcess(ContentParentId aContentParentId) { + AssertIsOnBackgroundThread(); + + RequestAllowToCloseDatabasesMatching( + [&aContentParentId](const auto& database) { + return database.IsOwnedByProcess(aContentParentId); + }); +} + +void QuotaClient::AbortAllOperations() { + AssertIsOnBackgroundThread(); + + InvalidatePrepareDatastoreOpsMatching([](const auto& prepareDatastoreOp) { + return prepareDatastoreOp.MaybeDirectoryLockRef(); + }); + + if (gPrivateDatastores) { + gPrivateDatastores = nullptr; + } + + InvalidatePreparedDatastoresMatching([](const auto&) { return true; }); + + RequestAllowToCloseDatabasesMatching([](const auto&) { return true; }); +} + +void QuotaClient::StartIdleMaintenance() { AssertIsOnBackgroundThread(); } + +void QuotaClient::StopIdleMaintenance() { AssertIsOnBackgroundThread(); } + +void QuotaClient::InitiateShutdown() { + // gPrepareDatastoreOps are short lived objects running a state machine. + // The shutdown flag is checked between states, so we don't have to notify + // all the objects here. + // Allocation of a new PrepareDatastoreOp object is prevented once the + // shutdown flag is set. + // When the last PrepareDatastoreOp finishes, the gPrepareDatastoreOps array + // is destroyed. + + if (gPreparedDatastores) { + gPreparedDatastores = nullptr; + } + + if (gPrivateDatastores) { + gPrivateDatastores = nullptr; + } + + RequestAllowToCloseDatabasesMatching([](const auto&) { return true; }); + + if (gPreparedObsevers) { + gPreparedObsevers = nullptr; + } +} + +bool QuotaClient::IsShutdownCompleted() const { + // Don't have to check gPrivateDatastores and gPreparedDatastores since we + // nulled it out in InitiateShutdown. + return !gPrepareDatastoreOps && !gDatastores && !gLiveDatabases; +} + +void QuotaClient::ForceKillActors() { ForceKillAllDatabases(); } + +nsCString QuotaClient::GetShutdownStatus() const { + AssertIsOnBackgroundThread(); + + nsCString data; + + if (gPrepareDatastoreOps) { + data.Append("PrepareDatastoreOperations: "); + data.AppendInt(static_cast<uint32_t>(gPrepareDatastoreOps->Length())); + data.Append(" ("); + + // XXX What's the purpose of adding these to a hashtable before joining them + // to the string? (Maybe this used to be an ordered container before???) + nsTHashSet<nsCString> ids; + std::transform(gPrepareDatastoreOps->cbegin(), gPrepareDatastoreOps->cend(), + MakeInserter(ids), [](const auto& prepareDatastoreOp) { + nsCString id; + prepareDatastoreOp->Stringify(id); + return id; + }); + + StringJoinAppend(data, ", "_ns, ids); + + data.Append(")\n"); + } + + if (gDatastores) { + data.Append("Datastores: "); + data.AppendInt(gDatastores->Count()); + data.Append(" ("); + + // XXX It might be confusing to remove duplicates here, as the actual list + // won't match the count then. + nsTHashSet<nsCString> ids; + std::transform(gDatastores->Values().cbegin(), gDatastores->Values().cend(), + MakeInserter(ids), [](const auto& entry) { + nsCString id; + entry->Stringify(id); + return id; + }); + + StringJoinAppend(data, ", "_ns, ids); + + data.Append(")\n"); + } + + if (gLiveDatabases) { + data.Append("LiveDatabases: "); + data.AppendInt(static_cast<uint32_t>(gLiveDatabases->Length())); + data.Append(" ("); + + // XXX It might be confusing to remove duplicates here, as the actual list + // won't match the count then. + nsTHashSet<nsCString> ids; + std::transform(gLiveDatabases->cbegin(), gLiveDatabases->cend(), + MakeInserter(ids), [](const auto& database) { + nsCString id; + database->Stringify(id); + return id; + }); + + StringJoinAppend(data, ", "_ns, ids); + + data.Append(")\n"); + } + + return data; +} + +void QuotaClient::FinalizeShutdown() { + // And finally, shutdown the connection thread. + if (gConnectionThread) { + gConnectionThread->Shutdown(); + + gConnectionThread = nullptr; + } +} + +Result<UniquePtr<ArchivedOriginScope>, nsresult> +QuotaClient::CreateArchivedOriginScope(const OriginScope& aOriginScope) { + AssertIsOnIOThread(); + + if (aOriginScope.IsOrigin()) { + QM_TRY_INSPECT(const auto& principalInfo, + QuotaManager::ParseOrigin(aOriginScope.GetOrigin())); + + QM_TRY_INSPECT((const auto& [originAttrSuffix, originKey]), + GenerateOriginKey2(principalInfo)); + + return ArchivedOriginScope::CreateFromOrigin(originAttrSuffix, originKey); + } + + if (aOriginScope.IsPrefix()) { + QM_TRY_INSPECT(const auto& principalInfo, + QuotaManager::ParseOrigin(aOriginScope.GetOriginNoSuffix())); + + QM_TRY_INSPECT((const auto& [originAttrSuffix, originKey]), + GenerateOriginKey2(principalInfo)); + + Unused << originAttrSuffix; + + return ArchivedOriginScope::CreateFromPrefix(originKey); + } + + if (aOriginScope.IsPattern()) { + return ArchivedOriginScope::CreateFromPattern(aOriginScope.GetPattern()); + } + + MOZ_ASSERT(aOriginScope.IsNull()); + + return ArchivedOriginScope::CreateFromNull(); +} + +nsresult QuotaClient::PerformDelete( + mozIStorageConnection* aConnection, const nsACString& aSchemaName, + ArchivedOriginScope* aArchivedOriginScope) const { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(aArchivedOriginScope); + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement, + "DELETE FROM "_ns + aSchemaName + ".webappsstore2"_ns + + aArchivedOriginScope->GetBindingClause() + ";"_ns)); + + QM_TRY(MOZ_TO_RESULT(aArchivedOriginScope->BindToStatement(stmt))); + + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + + return NS_OK; +} + +// static +nsresult QuotaClient::Observer::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<Observer> observer = new Observer(); + + QM_TRY(MOZ_TO_RESULT(observer->Init())); + + return NS_OK; +} + +nsresult QuotaClient::Observer::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return NS_ERROR_FAILURE; + } + + nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = obs->AddObserver(this, kPrivateBrowsingObserverTopic, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + return rv; + } + + return NS_OK; +} + +nsresult QuotaClient::Observer::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return NS_ERROR_FAILURE; + } + + MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, kPrivateBrowsingObserverTopic)); + MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID)); + + // In general, the instance will have died after the latter removal call, so + // it's not safe to do anything after that point. + // However, Shutdown is currently called from Observe which is called by the + // Observer Service which holds a strong reference to the observer while the + // Observe method is being called. + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(QuotaClient::Observer, nsIObserver) + +NS_IMETHODIMP +QuotaClient::Observer::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!strcmp(aTopic, kPrivateBrowsingObserverTopic)) { + PBackgroundChild* const backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + QM_TRY(OkIf(backgroundActor), NS_ERROR_FAILURE); + + QM_TRY(OkIf(backgroundActor->SendLSClearPrivateBrowsing()), + NS_ERROR_FAILURE); + + return NS_OK; + } + + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + QM_TRY(MOZ_TO_RESULT(Shutdown())); + + return NS_OK; + } + + NS_WARNING("Unknown observer topic!"); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(QuotaClient::MatchFunction, mozIStorageFunction) + +NS_IMETHODIMP +QuotaClient::MatchFunction::OnFunctionCall( + mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { + AssertIsOnIOThread(); + MOZ_ASSERT(aFunctionArguments); + MOZ_ASSERT(aResult); + + QM_TRY_INSPECT(const auto& suffix, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoCString, aFunctionArguments, GetUTF8String, 1)); + + OriginAttributes oa; + QM_TRY(OkIf(oa.PopulateFromSuffix(suffix)), NS_ERROR_FAILURE); + + const bool result = mPattern.Matches(oa); + + RefPtr<nsVariant> outVar(new nsVariant()); + QM_TRY(MOZ_TO_RESULT(outVar->SetAsBool(result))); + + outVar.forget(aResult); + return NS_OK; +} + +/******************************************************************************* + * AutoWriteTransaction + ******************************************************************************/ + +AutoWriteTransaction::AutoWriteTransaction(bool aShadowWrites) + : mConnection(nullptr), mShadowWrites(aShadowWrites) { + AssertIsOnGlobalConnectionThread(); + + MOZ_COUNT_CTOR(mozilla::dom::AutoWriteTransaction); +} + +AutoWriteTransaction::~AutoWriteTransaction() { + AssertIsOnGlobalConnectionThread(); + + MOZ_COUNT_DTOR(mozilla::dom::AutoWriteTransaction); + + if (mConnection) { + QM_WARNONLY_TRY(QM_TO_RESULT(mConnection->RollbackWriteTransaction())); + + if (mShadowWrites) { + QM_WARNONLY_TRY(QM_TO_RESULT(DetachShadowDatabaseAndUnlock())); + } + } +} + +nsresult AutoWriteTransaction::Start(Connection* aConnection) { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(!mConnection); + + if (mShadowWrites) { + QM_TRY(MOZ_TO_RESULT(LockAndAttachShadowDatabase(aConnection))); + } + + QM_TRY(MOZ_TO_RESULT(aConnection->BeginWriteTransaction())); + + mConnection = aConnection; + + return NS_OK; +} + +nsresult AutoWriteTransaction::Commit() { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(mConnection); + + QM_TRY(MOZ_TO_RESULT(mConnection->CommitWriteTransaction())); + + if (mShadowWrites) { + QM_TRY(MOZ_TO_RESULT(DetachShadowDatabaseAndUnlock())); + } + + mConnection = nullptr; + + return NS_OK; +} + +nsresult AutoWriteTransaction::LockAndAttachShadowDatabase( + Connection* aConnection) { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(!mConnection); + MOZ_ASSERT(mShadowDatabaseLock.isNothing()); + MOZ_ASSERT(mShadowWrites); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + mShadowDatabaseLock.emplace( + aConnection->GetQuotaClient()->ShadowDatabaseMutex()); + + QM_TRY(MOZ_TO_RESULT(AttachShadowDatabase( + quotaManager->GetBasePath(), &aConnection->MutableStorageConnection()))); + + return NS_OK; +} + +nsresult AutoWriteTransaction::DetachShadowDatabaseAndUnlock() { + AssertIsOnGlobalConnectionThread(); + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mShadowDatabaseLock.isSome()); + MOZ_ASSERT(mShadowWrites); + + nsCOMPtr<mozIStorageConnection> storageConnection = + mConnection->StorageConnection(); + MOZ_ASSERT(storageConnection); + + QM_TRY(MOZ_TO_RESULT(DetachShadowDatabase(storageConnection))); + + mShadowDatabaseLock.reset(); + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/localstorage/ActorsParent.h b/dom/localstorage/ActorsParent.h new file mode 100644 index 0000000000..eddba8ac98 --- /dev/null +++ b/dom/localstorage/ActorsParent.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_ActorsParent_h +#define mozilla_dom_localstorage_ActorsParent_h + +#include <cstdint> +#include "mozilla/AlreadyAddRefed.h" + +namespace mozilla { + +namespace ipc { + +class PBackgroundParent; +class PrincipalInfo; + +} // namespace ipc + +namespace dom { + +class LSRequestParams; +class LSSimpleRequestParams; +class PBackgroundLSDatabaseParent; +class PBackgroundLSObserverParent; +class PBackgroundLSRequestParent; +class PBackgroundLSSimpleRequestParent; + +namespace quota { + +class Client; + +} // namespace quota + +void InitializeLocalStorage(); + +PBackgroundLSDatabaseParent* AllocPBackgroundLSDatabaseParent( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, const uint64_t& aDatastoreId); + +bool RecvPBackgroundLSDatabaseConstructor( + PBackgroundLSDatabaseParent* aActor, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, const uint64_t& aDatastoreId); + +bool DeallocPBackgroundLSDatabaseParent(PBackgroundLSDatabaseParent* aActor); + +PBackgroundLSObserverParent* AllocPBackgroundLSObserverParent( + const uint64_t& aObserverId); + +bool RecvPBackgroundLSObserverConstructor(PBackgroundLSObserverParent* aActor, + const uint64_t& aObservereId); + +bool DeallocPBackgroundLSObserverParent(PBackgroundLSObserverParent* aActor); + +PBackgroundLSRequestParent* AllocPBackgroundLSRequestParent( + mozilla::ipc::PBackgroundParent* aBackgroundActor, + const LSRequestParams& aParams); + +bool RecvPBackgroundLSRequestConstructor(PBackgroundLSRequestParent* aActor, + const LSRequestParams& aParams); + +bool DeallocPBackgroundLSRequestParent(PBackgroundLSRequestParent* aActor); + +PBackgroundLSSimpleRequestParent* AllocPBackgroundLSSimpleRequestParent( + mozilla::ipc::PBackgroundParent* aBackgroundActor, + const LSSimpleRequestParams& aParams); + +bool RecvPBackgroundLSSimpleRequestConstructor( + PBackgroundLSSimpleRequestParent* aActor, + const LSSimpleRequestParams& aParams); + +bool DeallocPBackgroundLSSimpleRequestParent( + PBackgroundLSSimpleRequestParent* aActor); + +bool RecvLSClearPrivateBrowsing(); + +namespace localstorage { + +already_AddRefed<mozilla::dom::quota::Client> CreateQuotaClient(); + +} // namespace localstorage + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_ActorsParent_h diff --git a/dom/localstorage/LSDatabase.cpp b/dom/localstorage/LSDatabase.cpp new file mode 100644 index 0000000000..2dccdaad26 --- /dev/null +++ b/dom/localstorage/LSDatabase.cpp @@ -0,0 +1,448 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "LSDatabase.h" + +// Local includes +#include "ActorsChild.h" +#include "LSObject.h" +#include "LSSnapshot.h" + +// Global includes +#include <cstring> +#include <new> +#include <utility> +#include "MainThreadUtils.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/dom/PBackgroundLSDatabase.h" +#include "nsBaseHashtable.h" +#include "nsCOMPtr.h" +#include "nsTHashMap.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsHashKeys.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nscore.h" + +namespace mozilla::dom { + +namespace { + +#define XPCOM_SHUTDOWN_OBSERVER_TOPIC "xpcom-shutdown" + +using LSDatabaseHashtable = nsTHashMap<nsCStringHashKey, LSDatabase*>; + +StaticAutoPtr<LSDatabaseHashtable> gLSDatabases; + +} // namespace + +StaticRefPtr<LSDatabase::Observer> LSDatabase::sObserver; + +class LSDatabase::Observer final : public nsIObserver { + bool mInvalidated; + + public: + Observer() : mInvalidated(false) { MOZ_ASSERT(NS_IsMainThread()); } + + void Invalidate() { mInvalidated = true; } + + private: + ~Observer() { MOZ_ASSERT(NS_IsMainThread()); } + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER +}; + +LSDatabase::LSDatabase(const nsACString& aOrigin) + : mActor(nullptr), + mSnapshot(nullptr), + mOrigin(aOrigin), + mAllowedToClose(false), + mRequestedAllowToClose(false) { + AssertIsOnOwningThread(); + + if (!gLSDatabases) { + gLSDatabases = new LSDatabaseHashtable(); + + MOZ_ASSERT(!sObserver); + + sObserver = new Observer(); + + nsCOMPtr<nsIObserverService> obsSvc = + mozilla::services::GetObserverService(); + MOZ_ASSERT(obsSvc); + + MOZ_ALWAYS_SUCCEEDS( + obsSvc->AddObserver(sObserver, XPCOM_SHUTDOWN_OBSERVER_TOPIC, false)); + } + + MOZ_ASSERT(!gLSDatabases->Contains(mOrigin)); + gLSDatabases->InsertOrUpdate(mOrigin, this); +} + +LSDatabase::~LSDatabase() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mSnapshot); + + if (!mAllowedToClose) { + AllowToClose(); + } + + if (mActor) { + mActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!"); + } +} + +// static +LSDatabase* LSDatabase::Get(const nsACString& aOrigin) { + return gLSDatabases ? gLSDatabases->Get(aOrigin) : nullptr; +} + +void LSDatabase::SetActor(LSDatabaseChild* aActor) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!mActor); + + mActor = aActor; +} + +void LSDatabase::RequestAllowToClose() { + AssertIsOnOwningThread(); + + if (mRequestedAllowToClose) { + return; + } + + mRequestedAllowToClose = true; + + if (mSnapshot) { + mSnapshot->MarkDirty(); + } else { + AllowToClose(); + } +} + +void LSDatabase::NoteFinishedSnapshot(LSSnapshot* aSnapshot) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aSnapshot == mSnapshot); + + mSnapshot = nullptr; + + if (mRequestedAllowToClose) { + AllowToClose(); + } +} + +// All these methods assert `!mAllowedToClose` because they shoudn't be called +// if the database is being closed. Callers should first check the state by +// calling `IsAlloweToClose` and eventually obtain a new database. + +nsresult LSDatabase::GetLength(LSObject* aObject, uint32_t* aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject, VoidString()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->GetLength(aResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult LSDatabase::GetKey(LSObject* aObject, uint32_t aIndex, + nsAString& aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject, VoidString()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->GetKey(aIndex, aResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult LSDatabase::GetItem(LSObject* aObject, const nsAString& aKey, + nsAString& aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject, aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->GetItem(aKey, aResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult LSDatabase::GetKeys(LSObject* aObject, nsTArray<nsString>& aKeys) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject, VoidString()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->GetKeys(aKeys); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult LSDatabase::SetItem(LSObject* aObject, const nsAString& aKey, + const nsAString& aValue, + LSNotifyInfo& aNotifyInfo) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject, aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->SetItem(aKey, aValue, aNotifyInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult LSDatabase::RemoveItem(LSObject* aObject, const nsAString& aKey, + LSNotifyInfo& aNotifyInfo) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject, aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->RemoveItem(aKey, aNotifyInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult LSDatabase::Clear(LSObject* aObject, LSNotifyInfo& aNotifyInfo) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject, VoidString()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->Clear(aNotifyInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult LSDatabase::BeginExplicitSnapshot(LSObject* aObject) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + MOZ_ASSERT(!mSnapshot); + + nsresult rv = EnsureSnapshot(aObject, VoidString(), /* aExplicit */ true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult LSDatabase::CheckpointExplicitSnapshot() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + MOZ_ASSERT(mSnapshot); + MOZ_ASSERT(mSnapshot->Explicit()); + + nsresult rv = mSnapshot->ExplicitCheckpoint(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult LSDatabase::EndExplicitSnapshot() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + MOZ_ASSERT(mSnapshot); + MOZ_ASSERT(mSnapshot->Explicit()); + + nsresult rv = mSnapshot->ExplicitEnd(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +bool LSDatabase::HasSnapshot() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + return !!mSnapshot; +} + +int64_t LSDatabase::GetSnapshotUsage() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + MOZ_ASSERT(mSnapshot); + + return mSnapshot->GetUsage(); +} + +nsresult LSDatabase::EnsureSnapshot(LSObject* aObject, const nsAString& aKey, + bool aExplicit) { + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT_IF(mSnapshot, !aExplicit); + MOZ_ASSERT(!mAllowedToClose); + + if (mSnapshot) { + return NS_OK; + } + + RefPtr<LSSnapshot> snapshot = new LSSnapshot(this); + + LSSnapshotChild* actor = new LSSnapshotChild(snapshot); + + LSSnapshotInitInfo initInfo; + bool ok = mActor->SendPBackgroundLSSnapshotConstructor( + actor, aObject->DocumentURI(), nsString(aKey), + /* increasePeakUsage */ true, + /* minSize */ 0, &initInfo); + if (NS_WARN_IF(!ok)) { + return NS_ERROR_FAILURE; + } + + snapshot->SetActor(actor); + + // This add refs snapshot. + nsresult rv = snapshot->Init(aKey, initInfo, aExplicit); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // This is cleared in LSSnapshot::Run() before the snapshot is destroyed. + mSnapshot = snapshot; + + return NS_OK; +} + +void LSDatabase::AllowToClose() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mAllowedToClose); + MOZ_ASSERT(!mSnapshot); + + mAllowedToClose = true; + + if (mActor) { + mActor->SendAllowToClose(); + } + + MOZ_ASSERT(gLSDatabases); + MOZ_ASSERT(gLSDatabases->Get(mOrigin)); + gLSDatabases->Remove(mOrigin); + + if (!gLSDatabases->Count()) { + gLSDatabases = nullptr; + + MOZ_ASSERT(sObserver); + + nsCOMPtr<nsIObserverService> obsSvc = + mozilla::services::GetObserverService(); + MOZ_ASSERT(obsSvc); + + MOZ_ALWAYS_SUCCEEDS( + obsSvc->RemoveObserver(sObserver, XPCOM_SHUTDOWN_OBSERVER_TOPIC)); + + // We also need to invalidate the observer because AllowToClose can be + // triggered by an indirectly related observer, so the observer service + // may still keep our observer alive and call Observe on it. This is + // possible because observer service snapshots the observer list for given + // subject before looping over the list. + sObserver->Invalidate(); + + sObserver = nullptr; + } +} + +NS_IMPL_ISUPPORTS(LSDatabase::Observer, nsIObserver) + +NS_IMETHODIMP +LSDatabase::Observer::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aTopic, XPCOM_SHUTDOWN_OBSERVER_TOPIC)); + + if (mInvalidated) { + return NS_OK; + } + + MOZ_ASSERT(gLSDatabases); + + for (const RefPtr<LSDatabase>& database : + ToTArray<nsTArray<RefPtr<LSDatabase>>>(gLSDatabases->Values())) { + database->RequestAllowToClose(); + } + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/localstorage/LSDatabase.h b/dom/localstorage/LSDatabase.h new file mode 100644 index 0000000000..b85798eeac --- /dev/null +++ b/dom/localstorage/LSDatabase.h @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_LSDatabase_h +#define mozilla_dom_localstorage_LSDatabase_h + +#include <cstdint> +#include "ErrorList.h" +#include "mozilla/Assertions.h" +#include "mozilla/StaticPtr.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsTArrayForwardDeclare.h" + +namespace mozilla::dom { + +class LSDatabaseChild; +class LSNotifyInfo; +class LSObject; +class LSSnapshot; + +class LSDatabase final { + class Observer; + + LSDatabaseChild* mActor; + + LSSnapshot* mSnapshot; + + const nsCString mOrigin; + + bool mAllowedToClose; + bool mRequestedAllowToClose; + + static StaticRefPtr<Observer> sObserver; + + public: + explicit LSDatabase(const nsACString& aOrigin); + + static LSDatabase* Get(const nsACString& aOrigin); + + NS_INLINE_DECL_REFCOUNTING(LSDatabase) + + void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(LSDatabase); } + + void SetActor(LSDatabaseChild* aActor); + + void ClearActor() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + + mActor = nullptr; + } + + bool IsAllowedToClose() const { + AssertIsOnOwningThread(); + + return mAllowedToClose; + } + + void RequestAllowToClose(); + + void NoteFinishedSnapshot(LSSnapshot* aSnapshot); + + nsresult GetLength(LSObject* aObject, uint32_t* aResult); + + nsresult GetKey(LSObject* aObject, uint32_t aIndex, nsAString& aResult); + + nsresult GetItem(LSObject* aObject, const nsAString& aKey, + nsAString& aResult); + + nsresult GetKeys(LSObject* aObject, nsTArray<nsString>& aKeys); + + nsresult SetItem(LSObject* aObject, const nsAString& aKey, + const nsAString& aValue, LSNotifyInfo& aNotifyInfo); + + nsresult RemoveItem(LSObject* aObject, const nsAString& aKey, + LSNotifyInfo& aNotifyInfo); + + nsresult Clear(LSObject* aObject, LSNotifyInfo& aNotifyInfo); + + nsresult BeginExplicitSnapshot(LSObject* aObject); + + nsresult CheckpointExplicitSnapshot(); + + nsresult EndExplicitSnapshot(); + + bool HasSnapshot() const; + + int64_t GetSnapshotUsage() const; + + private: + ~LSDatabase(); + + nsresult EnsureSnapshot(LSObject* aObject, const nsAString& aKey, + bool aExplicit = false); + + void AllowToClose(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_localstorage_LSDatabase_h diff --git a/dom/localstorage/LSInitializationTypes.h b/dom/localstorage/LSInitializationTypes.h new file mode 100644 index 0000000000..63090066da --- /dev/null +++ b/dom/localstorage/LSInitializationTypes.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_LOCALSTORAGE_INITIALIZATIONTYPES_H_ +#define DOM_LOCALSTORAGE_INITIALIZATIONTYPES_H_ + +#include "mozilla/TypedEnumBits.h" +#include <mozilla/dom/quota/FirstInitializationAttempts.h> +#include "nsTHashMap.h" + +namespace mozilla { +struct CreateIfNonExistent; +} + +namespace mozilla::dom { + +enum class LSOriginInitialization { + None = 0, + Datastore = 1 << 0, +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(LSOriginInitialization) + +using LSOriginInitializationInfo = + quota::FirstInitializationAttempts<LSOriginInitialization, Nothing>; + +class LSInitializationInfo final { + nsTHashMap<nsCStringHashKey, LSOriginInitializationInfo> + mOriginInitializationInfos; + + public: + LSOriginInitializationInfo& MutableOriginInitializationInfoRef( + const nsACString& aOrigin) { + return *mOriginInitializationInfos.Lookup(aOrigin); + } + + LSOriginInitializationInfo& MutableOriginInitializationInfoRef( + const nsACString& aOrigin, const CreateIfNonExistent&) { + return mOriginInitializationInfos.LookupOrInsert(aOrigin); + } +}; + +} // namespace mozilla::dom + +#endif // DOM_LOCALSTORAGE_INITIALIZATIONTYPES_H_ diff --git a/dom/localstorage/LSObject.cpp b/dom/localstorage/LSObject.cpp new file mode 100644 index 0000000000..23189d6f98 --- /dev/null +++ b/dom/localstorage/LSObject.cpp @@ -0,0 +1,1167 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "LSObject.h" + +// Local includes +#include "ActorsChild.h" +#include "LSDatabase.h" +#include "LSObserver.h" + +// Global includes +#include <utility> +#include "MainThreadUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Monitor.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/Preferences.h" +#include "mozilla/RemoteLazyInputStreamThread.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ClientInfo.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/LocalStorageCommon.h" +#include "mozilla/dom/PBackgroundLSRequest.h" +#include "mozilla/dom/PBackgroundLSSharedTypes.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/ProcessChild.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIEventTarget.h" +#include "nsIPrincipal.h" +#include "nsIRunnable.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsISerialEventTarget.h" +#include "nsITimer.h" +#include "nsPIDOMWindow.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsTStringRepr.h" +#include "nsThread.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "nscore.h" + +/** + * Automatically cancel and abort synchronous LocalStorage requests (for example + * datastore preparation) if they take this long. We've chosen a value that is + * long enough that it is unlikely for the problem to be falsely triggered by + * slow system I/O. We've also chosen a value long enough so that automated + * tests should time out and fail if LocalStorage hangs. Also, this value is + * long enough so that testers can notice the (content process) hang; we want to + * know about the hangs, not hide them. On the other hand this value is less + * than 60 seconds which is used by nsTerminator to crash a hung main process. + */ +#define FAILSAFE_CANCEL_SYNC_OP_MS 50000 + +/** + * Interval with which to wake up while waiting for the sync op to complete to + * check ExpectingShutdown(). + */ +#define SYNC_OP_WAKE_INTERVAL_MS 500 + +namespace mozilla::dom { + +namespace { + +class RequestHelper; + +/** + * Main-thread helper that implements the blocking logic required by + * LocalStorage's synchronous semantics. StartAndReturnResponse blocks on a + * monitor until a result is received. See StartAndReturnResponse() for info on + * this choice. + * + * The normal life-cycle of this method looks like: + * - Main Thread: LSObject::DoRequestSynchronously creates a RequestHelper and + * invokes StartAndReturnResponse(). It Dispatches the RequestHelper to the + * RemoteLazyInputStream thread, and waits on mMonitor. + * - RemoteLazyInputStream Thread: RequestHelper::Run is called, invoking + * Start() which invokes LSObject::StartRequest, which gets-or-creates the + * PBackground actor if necessary, sends LSRequest constructor which is + * provided with a callback reference to the RequestHelper. State advances to + * ResponsePending. + * - RemoteLazyInputStreamThread: LSRequestChild::Recv__delete__ is received, + * which invokes RequestHelepr::OnResponse, advancing the state to Complete + * and notifying mMonitor. + * - Main Thread: The main thread wakes up after waiting on the monitor, + * returning the received response. + * + * See LocalStorageCommon.h for high-level context and method comments for + * low-level details. + */ +class RequestHelper final : public Runnable, public LSRequestChildCallback { + enum class State { + /** + * The RequestHelper has been created and dispatched to the + * RemoteLazyInputStream Thread. + */ + Initial, + /** + * Start() has been invoked on the RemoteLazyInputStream Thread and + * LSObject::StartRequest has been invoked from there, sending an IPC + * message to PBackground to service the request. We stay in this state + * until a response is received or a timeout occurs. + */ + ResponsePending, + /** + * The request timed out, or failed in some fashion, and needs to be + * cancelled. A runnable has been dispatched to the DOM File thread to + * notify the parent actor, and the main thread will continue to block until + * we receive a reponse. + */ + Canceling, + /** + * The request is complete, either successfully or due to being cancelled. + * The main thread can stop waiting and immediately return to the caller of + * StartAndReturnResponse. + */ + Complete + }; + + // The object we are issuing a request on behalf of. Present because of the + // need to invoke LSObject::StartRequest off the main thread. Dropped on + // return to the main-thread in Finish(). + RefPtr<LSObject> mObject; + // The thread the RequestHelper was created on. This should be the main + // thread. + nsCOMPtr<nsIEventTarget> mOwningEventTarget; + // The IPC actor handling the request with standard IPC allocation rules. + // Our reference is nulled in OnResponse which corresponds to the actor's + // __destroy__ method. + LSRequestChild* mActor; + const LSRequestParams mParams; + Monitor mMonitor; + LSRequestResponse mResponse MOZ_GUARDED_BY(mMonitor); + nsresult mResultCode MOZ_GUARDED_BY(mMonitor); + State mState MOZ_GUARDED_BY(mMonitor); + + public: + RequestHelper(LSObject* aObject, const LSRequestParams& aParams) + : Runnable("dom::RequestHelper"), + mObject(aObject), + mOwningEventTarget(GetCurrentEventTarget()), + mActor(nullptr), + mParams(aParams), + mMonitor("dom::RequestHelper::mMonitor"), + mResultCode(NS_OK), + mState(State::Initial) {} + + bool IsOnOwningThread() const { + MOZ_ASSERT(mOwningEventTarget); + + bool current; + return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(¤t)) && + current; + } + + void AssertIsOnOwningThread() const { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsOnOwningThread()); + } + + nsresult StartAndReturnResponse(LSRequestResponse& aResponse); + + private: + ~RequestHelper() = default; + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_NSIRUNNABLE + + // LSRequestChildCallback + void OnResponse(const LSRequestResponse& aResponse) override; +}; + +void AssertExplicitSnapshotInvariants(const LSObject& aObject) { + // Can be only called if the mInExplicitSnapshot flag is true. + // An explicit snapshot must have been created. + MOZ_ASSERT(aObject.InExplicitSnapshot()); + + // If an explicit snapshot has been created then mDatabase must be not null. + // DropDatabase could be called in the meatime, but that must be preceded by + // Disconnect which sets mInExplicitSnapshot to false. EnsureDatabase could + // be called in the meantime too, but that can't set mDatabase to null or to + // a new value. See the comment below. + MOZ_ASSERT(aObject.DatabaseStrongRef()); + + // Existence of a snapshot prevents the database from allowing to close. See + // LSDatabase::RequestAllowToClose and LSDatabase::NoteFinishedSnapshot. + // If the database is not allowed to close then mDatabase could not have been + // nulled out or set to a new value. See EnsureDatabase. + MOZ_ASSERT(!aObject.DatabaseStrongRef()->IsAllowedToClose()); +} + +} // namespace + +LSObject::LSObject(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal) + : Storage(aWindow, aPrincipal, aStoragePrincipal), + mPrivateBrowsingId(0), + mInExplicitSnapshot(false) { + AssertIsOnOwningThread(); + MOZ_ASSERT(NextGenLocalStorageEnabled()); +} + +LSObject::~LSObject() { + AssertIsOnOwningThread(); + + DropObserver(); +} + +// static +nsresult LSObject::CreateForWindow(nsPIDOMWindowInner* aWindow, + Storage** aStorage) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aStorage); + MOZ_ASSERT(NextGenLocalStorageEnabled()); + MOZ_ASSERT(StorageAllowedForWindow(aWindow) != StorageAccess::eDeny); + + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow); + MOZ_ASSERT(sop); + + nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal(); + if (NS_WARN_IF(!principal)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIPrincipal> storagePrincipal = sop->GetEffectiveStoragePrincipal(); + if (NS_WARN_IF(!storagePrincipal)) { + return NS_ERROR_FAILURE; + } + + if (principal->IsSystemPrincipal()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // localStorage is not available on some pages on purpose, for example + // about:home. Match the old implementation by using GenerateOriginKey + // for the check. + nsCString originAttrSuffix; + nsCString originKey; + nsresult rv = storagePrincipal->GetStorageOriginKey(originKey); + storagePrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix); + + if (NS_FAILED(rv)) { + return NS_ERROR_NOT_AVAILABLE; + } + + auto principalInfo = MakeUnique<PrincipalInfo>(); + rv = PrincipalToPrincipalInfo(principal, principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo); + + auto storagePrincipalInfo = MakeUnique<PrincipalInfo>(); + rv = PrincipalToPrincipalInfo(storagePrincipal, storagePrincipalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(storagePrincipalInfo->type() == + PrincipalInfo::TContentPrincipalInfo); + + if (NS_WARN_IF( + !quota::QuotaManager::IsPrincipalInfoValid(*storagePrincipalInfo))) { + return NS_ERROR_FAILURE; + } + +#ifdef DEBUG + QM_TRY_INSPECT( + const auto& principalMetadata, + quota::QuotaManager::GetInfoFromPrincipal(storagePrincipal.get())); + + MOZ_ASSERT(originAttrSuffix == principalMetadata.mSuffix); + + const auto& origin = principalMetadata.mOrigin; +#else + QM_TRY_INSPECT( + const auto& origin, + quota::QuotaManager::GetOriginFromPrincipal(storagePrincipal.get())); +#endif + + uint32_t privateBrowsingId; + rv = storagePrincipal->GetPrivateBrowsingId(&privateBrowsingId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + Maybe<ClientInfo> clientInfo = aWindow->GetClientInfo(); + if (clientInfo.isNothing()) { + return NS_ERROR_FAILURE; + } + + Maybe<nsID> clientId = Some(clientInfo.ref().Id()); + + Maybe<PrincipalInfo> clientPrincipalInfo = + Some(clientInfo.ref().PrincipalInfo()); + + nsString documentURI; + if (nsCOMPtr<Document> doc = aWindow->GetExtantDoc()) { + rv = doc->GetDocumentURI(documentURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + RefPtr<LSObject> object = new LSObject(aWindow, principal, storagePrincipal); + object->mPrincipalInfo = std::move(principalInfo); + object->mStoragePrincipalInfo = std::move(storagePrincipalInfo); + object->mPrivateBrowsingId = privateBrowsingId; + object->mClientId = clientId; + object->mClientPrincipalInfo = clientPrincipalInfo; + object->mOrigin = origin; + object->mOriginKey = originKey; + object->mDocumentURI = documentURI; + + object.forget(aStorage); + return NS_OK; +} + +// static +nsresult LSObject::CreateForPrincipal(nsPIDOMWindowInner* aWindow, + nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, + const nsAString& aDocumentURI, + bool aPrivate, LSObject** aObject) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aStoragePrincipal); + MOZ_ASSERT(aObject); + + nsCString originAttrSuffix; + nsCString originKey; + nsresult rv = aStoragePrincipal->GetStorageOriginKey(originKey); + aStoragePrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix); + if (NS_FAILED(rv)) { + return NS_ERROR_NOT_AVAILABLE; + } + + auto principalInfo = MakeUnique<PrincipalInfo>(); + rv = PrincipalToPrincipalInfo(aPrincipal, principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo || + principalInfo->type() == PrincipalInfo::TSystemPrincipalInfo); + + auto storagePrincipalInfo = MakeUnique<PrincipalInfo>(); + rv = PrincipalToPrincipalInfo(aStoragePrincipal, storagePrincipalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT( + storagePrincipalInfo->type() == PrincipalInfo::TContentPrincipalInfo || + storagePrincipalInfo->type() == PrincipalInfo::TSystemPrincipalInfo); + + if (NS_WARN_IF( + !quota::QuotaManager::IsPrincipalInfoValid(*storagePrincipalInfo))) { + return NS_ERROR_FAILURE; + } + +#ifdef DEBUG + QM_TRY_INSPECT( + const auto& principalMetadata, + ([&storagePrincipalInfo, + &aPrincipal]() -> Result<quota::PrincipalMetadata, nsresult> { + if (storagePrincipalInfo->type() == + PrincipalInfo::TSystemPrincipalInfo) { + return quota::QuotaManager::GetInfoForChrome(); + } + + QM_TRY_RETURN(quota::QuotaManager::GetInfoFromPrincipal(aPrincipal)); + }())); + + MOZ_ASSERT(originAttrSuffix == principalMetadata.mSuffix); + + const auto& origin = principalMetadata.mOrigin; +#else + QM_TRY_INSPECT( + const auto& origin, ([&storagePrincipalInfo, + &aPrincipal]() -> Result<nsAutoCString, nsresult> { + if (storagePrincipalInfo->type() == + PrincipalInfo::TSystemPrincipalInfo) { + return nsAutoCString{quota::QuotaManager::GetOriginForChrome()}; + } + + QM_TRY_RETURN(quota::QuotaManager::GetOriginFromPrincipal(aPrincipal)); + }())); +#endif + + Maybe<nsID> clientId; + if (aWindow) { + Maybe<ClientInfo> clientInfo = aWindow->GetClientInfo(); + if (clientInfo.isNothing()) { + return NS_ERROR_FAILURE; + } + + clientId = Some(clientInfo.ref().Id()); + } else if (Preferences::GetBool("dom.storage.client_validation")) { + return NS_ERROR_FAILURE; + } + + RefPtr<LSObject> object = + new LSObject(aWindow, aPrincipal, aStoragePrincipal); + object->mPrincipalInfo = std::move(principalInfo); + object->mStoragePrincipalInfo = std::move(storagePrincipalInfo); + object->mPrivateBrowsingId = aPrivate ? 1 : 0; + object->mClientId = clientId; + object->mOrigin = origin; + object->mOriginKey = originKey; + object->mDocumentURI = aDocumentURI; + + object.forget(aObject); + return NS_OK; +} // namespace dom + +LSRequestChild* LSObject::StartRequest(const LSRequestParams& aParams, + LSRequestChildCallback* aCallback) { + AssertIsOnDOMFileThread(); + + mozilla::ipc::PBackgroundChild* backgroundActor = + mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return nullptr; + } + + LSRequestChild* actor = new LSRequestChild(); + + if (!backgroundActor->SendPBackgroundLSRequestConstructor(actor, aParams)) { + return nullptr; + } + + // Must set callback after calling SendPBackgroundLSRequestConstructor since + // it can be called synchronously when SendPBackgroundLSRequestConstructor + // fails. + actor->SetCallback(aCallback); + + return actor; +} + +Storage::StorageType LSObject::Type() const { + AssertIsOnOwningThread(); + + return eLocalStorage; +} + +bool LSObject::IsForkOf(const Storage* aStorage) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(aStorage); + + if (aStorage->Type() != eLocalStorage) { + return false; + } + + return static_cast<const LSObject*>(aStorage)->mOrigin == mOrigin; +} + +int64_t LSObject::GetOriginQuotaUsage() const { + AssertIsOnOwningThread(); + + // It's not necessary to return an actual value here. This method is + // implemented only because the SessionStore currently needs it to cap the + // amount of data it persists to disk (via nsIDOMWindowUtils.getStorageUsage). + // Any callers that want to know about storage usage should be asking + // QuotaManager directly. + // + // Note: This may change as LocalStorage is repurposed to be the new + // SessionStorage backend. + return 0; +} + +void LSObject::Disconnect() { + // Explicit snapshots which were not ended in JS, must be ended here while + // IPC is still available. We can't do that in DropDatabase because actors + // may have been destroyed already at that point. + if (mInExplicitSnapshot) { + AssertExplicitSnapshotInvariants(*this); + + nsresult rv = mDatabase->EndExplicitSnapshot(); + Unused << NS_WARN_IF(NS_FAILED(rv)); + + mInExplicitSnapshot = false; + } +} + +uint32_t LSObject::GetLength(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return 0; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return 0; + } + + uint32_t result; + rv = mDatabase->GetLength(this, &result); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return 0; + } + + return result; +} + +void LSObject::Key(uint32_t aIndex, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + nsString result; + rv = mDatabase->GetKey(this, aIndex, result); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + aResult = result; +} + +void LSObject::GetItem(const nsAString& aKey, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + nsString result; + rv = mDatabase->GetItem(this, aKey, result); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + aResult = result; +} + +void LSObject::GetSupportedNames(nsTArray<nsString>& aNames) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(*nsContentUtils::SubjectPrincipal())) { + // Return just an empty array. + aNames.Clear(); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = mDatabase->GetKeys(this, aNames); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } +} + +void LSObject::SetItem(const nsAString& aKey, const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + LSNotifyInfo info; + rv = mDatabase->SetItem(this, aKey, aValue, info); + if (rv == NS_ERROR_FILE_NO_DEVICE_SPACE) { + rv = NS_ERROR_DOM_QUOTA_EXCEEDED_ERR; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + if (info.changed()) { + OnChange(aKey, info.oldValue(), aValue); + } +} + +void LSObject::RemoveItem(const nsAString& aKey, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + LSNotifyInfo info; + rv = mDatabase->RemoveItem(this, aKey, info); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + if (info.changed()) { + OnChange(aKey, info.oldValue(), VoidString()); + } +} + +void LSObject::Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + LSNotifyInfo info; + rv = mDatabase->Clear(this, info); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + if (info.changed()) { + OnChange(VoidString(), VoidString(), VoidString()); + } +} + +void LSObject::Open(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } +} + +void LSObject::Close(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + DropDatabase(); +} + +void LSObject::BeginExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + if (mInExplicitSnapshot) { + aError.Throw(NS_ERROR_ALREADY_INITIALIZED); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + rv = mDatabase->BeginExplicitSnapshot(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + mInExplicitSnapshot = true; +} + +void LSObject::CheckpointExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + if (!mInExplicitSnapshot) { + aError.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + AssertExplicitSnapshotInvariants(*this); + + nsresult rv = mDatabase->CheckpointExplicitSnapshot(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } +} + +void LSObject::EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + if (!mInExplicitSnapshot) { + aError.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + AssertExplicitSnapshotInvariants(*this); + + nsresult rv = mDatabase->EndExplicitSnapshot(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + mInExplicitSnapshot = false; +} + +bool LSObject::GetHasSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return false; + } + + // We can't call `HasSnapshot` on the database if it's being closed, but we + // know that a database which is being closed can't have a snapshot, so we + // return false in that case directly here. + if (!mDatabase || mDatabase->IsAllowedToClose()) { + return false; + } + + return mDatabase->HasSnapshot(); +} + +int64_t LSObject::GetSnapshotUsage(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) { + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return 0; + } + + if (!mDatabase || mDatabase->IsAllowedToClose()) { + aError.Throw(NS_ERROR_NOT_AVAILABLE); + return 0; + } + + if (!mDatabase->HasSnapshot()) { + aError.Throw(NS_ERROR_NOT_AVAILABLE); + return 0; + } + + return mDatabase->GetSnapshotUsage(); +} + +NS_IMPL_ADDREF_INHERITED(LSObject, Storage) +NS_IMPL_RELEASE_INHERITED(LSObject, Storage) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LSObject) +NS_INTERFACE_MAP_END_INHERITING(Storage) + +NS_IMPL_CYCLE_COLLECTION_CLASS(LSObject) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(LSObject, Storage) + tmp->AssertIsOnOwningThread(); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(LSObject, Storage) + tmp->AssertIsOnOwningThread(); + tmp->DropDatabase(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +nsresult LSObject::DoRequestSynchronously(const LSRequestParams& aParams, + LSRequestResponse& aResponse) { + // We don't need this yet, but once the request successfully finishes, it's + // too late to initialize PBackground child on the owning thread, because + // it can fail and parent would keep an extra strong ref to the datastore or + // observer. + mozilla::ipc::PBackgroundChild* backgroundActor = + mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return NS_ERROR_FAILURE; + } + + RefPtr<RequestHelper> helper = new RequestHelper(this, aParams); + + // This will start and finish the request on the RemoteLazyInputStream thread. + // The owning thread is synchronously blocked while the request is + // asynchronously processed on the RemoteLazyInputStream thread. + nsresult rv = helper->StartAndReturnResponse(aResponse); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aResponse.type() == LSRequestResponse::Tnsresult) { + nsresult errorCode = aResponse.get_nsresult(); + + if (errorCode == NS_ERROR_FILE_NO_DEVICE_SPACE) { + errorCode = NS_ERROR_DOM_QUOTA_EXCEEDED_ERR; + } + + return errorCode; + } + + return NS_OK; +} + +nsresult LSObject::EnsureDatabase() { + AssertIsOnOwningThread(); + + if (mDatabase && !mDatabase->IsAllowedToClose()) { + return NS_OK; + } + + mDatabase = LSDatabase::Get(mOrigin); + + if (mDatabase) { + MOZ_ASSERT(!mDatabase->IsAllowedToClose()); + return NS_OK; + } + + // We don't need this yet, but once the request successfully finishes, it's + // too late to initialize PBackground child on the owning thread, because + // it can fail and parent would keep an extra strong ref to the datastore. + mozilla::ipc::PBackgroundChild* backgroundActor = + mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return NS_ERROR_FAILURE; + } + + LSRequestCommonParams commonParams; + commonParams.principalInfo() = *mPrincipalInfo; + commonParams.storagePrincipalInfo() = *mStoragePrincipalInfo; + commonParams.originKey() = mOriginKey; + + LSRequestPrepareDatastoreParams params; + params.commonParams() = commonParams; + params.clientId() = mClientId; + params.clientPrincipalInfo() = mClientPrincipalInfo; + + LSRequestResponse response; + + nsresult rv = DoRequestSynchronously(params, response); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(response.type() == + LSRequestResponse::TLSRequestPrepareDatastoreResponse); + + const LSRequestPrepareDatastoreResponse& prepareDatastoreResponse = + response.get_LSRequestPrepareDatastoreResponse(); + + uint64_t datastoreId = prepareDatastoreResponse.datastoreId(); + + // The datastore is now ready on the parent side (prepared by the asynchronous + // request on the RemoteLazyInputStream thread). + // Let's create a direct connection to the datastore (through a database + // actor) from the owning thread. + // Note that we now can't error out, otherwise parent will keep an extra + // strong reference to the datastore. + + RefPtr<LSDatabase> database = new LSDatabase(mOrigin); + + LSDatabaseChild* actor = new LSDatabaseChild(database); + + MOZ_ALWAYS_TRUE(backgroundActor->SendPBackgroundLSDatabaseConstructor( + actor, *mStoragePrincipalInfo, mPrivateBrowsingId, datastoreId)); + + database->SetActor(actor); + + mDatabase = std::move(database); + + return NS_OK; +} + +void LSObject::DropDatabase() { + AssertIsOnOwningThread(); + + mDatabase = nullptr; +} + +nsresult LSObject::EnsureObserver() { + AssertIsOnOwningThread(); + + if (mObserver) { + return NS_OK; + } + + mObserver = LSObserver::Get(mOrigin); + + if (mObserver) { + return NS_OK; + } + + LSRequestPrepareObserverParams params; + params.principalInfo() = *mPrincipalInfo; + params.storagePrincipalInfo() = *mStoragePrincipalInfo; + params.clientId() = mClientId; + params.clientPrincipalInfo() = mClientPrincipalInfo; + + LSRequestResponse response; + + nsresult rv = DoRequestSynchronously(params, response); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(response.type() == + LSRequestResponse::TLSRequestPrepareObserverResponse); + + const LSRequestPrepareObserverResponse& prepareObserverResponse = + response.get_LSRequestPrepareObserverResponse(); + + uint64_t observerId = prepareObserverResponse.observerId(); + + // The obsserver is now ready on the parent side (prepared by the asynchronous + // request on the RemoteLazyInputStream thread). + // Let's create a direct connection to the observer (through an observer + // actor) from the owning thread. + // Note that we now can't error out, otherwise parent will keep an extra + // strong reference to the observer. + + mozilla::ipc::PBackgroundChild* backgroundActor = + mozilla::ipc::BackgroundChild::GetForCurrentThread(); + MOZ_ASSERT(backgroundActor); + + RefPtr<LSObserver> observer = new LSObserver(mOrigin); + + LSObserverChild* actor = new LSObserverChild(observer); + + MOZ_ALWAYS_TRUE( + backgroundActor->SendPBackgroundLSObserverConstructor(actor, observerId)); + + observer->SetActor(actor); + + mObserver = std::move(observer); + + return NS_OK; +} + +void LSObject::DropObserver() { + AssertIsOnOwningThread(); + + if (mObserver) { + mObserver = nullptr; + } +} + +void LSObject::OnChange(const nsAString& aKey, const nsAString& aOldValue, + const nsAString& aNewValue) { + AssertIsOnOwningThread(); + + NotifyChange(/* aStorage */ this, StoragePrincipal(), aKey, aOldValue, + aNewValue, /* aStorageType */ kLocalStorageType, mDocumentURI, + /* aIsPrivate */ !!mPrivateBrowsingId, + /* aImmediateDispatch */ false); +} + +void LSObject::LastRelease() { + AssertIsOnOwningThread(); + + DropDatabase(); +} + +nsresult RequestHelper::StartAndReturnResponse(LSRequestResponse& aResponse) { + AssertIsOnOwningThread(); + + nsCOMPtr<nsIEventTarget> domFileThread = + RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!domFileThread)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + nsresult rv = domFileThread->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + TimeStamp deadline = TimeStamp::Now() + TimeDuration::FromMilliseconds( + FAILSAFE_CANCEL_SYNC_OP_MS); + + MonitorAutoLock lock(mMonitor); + while (mState != State::Complete) { + TimeStamp now = TimeStamp::Now(); + // If we are expecting shutdown or have passed our deadline, immediately + // dispatch ourselves to the DOM File thread to cancel the operation. We + // don't abort until the cancellation has gone through, as otherwise we + // could race with the DOM File thread. + if (mozilla::ipc::ProcessChild::ExpectingShutdown() || now >= deadline) { + switch (mState) { + case State::Initial: + // The DOM File thread never even woke before ExpectingShutdown() or a + // timeout - skip even creating the actor and just report an error. + mResultCode = NS_ERROR_FAILURE; + mState = State::Complete; + continue; + case State::ResponsePending: + // The DOM File thread is currently waiting for a reply, switch to a + // canceling state, and notify it to cancel by dispatching a runnable. + mState = State::Canceling; + MOZ_ALWAYS_SUCCEEDS( + domFileThread->Dispatch(this, NS_DISPATCH_NORMAL)); + [[fallthrough]]; + case State::Canceling: + // We've cancelled the request, so just need to wait indefinitely for + // it to complete. + lock.Wait(); + continue; + default: + MOZ_ASSERT_UNREACHABLE("unexpected state"); + } + } + + // Wait until either we reach out deadline or for SYNC_OP_WAIT_INTERVAL_MS. + lock.Wait(TimeDuration::Min( + TimeDuration::FromMilliseconds(SYNC_OP_WAKE_INTERVAL_MS), + deadline - now)); + } + + // The operation is complete, clear our reference to the LSObject. + mObject = nullptr; + + if (NS_WARN_IF(NS_FAILED(mResultCode))) { + return mResultCode; + } + + aResponse = std::move(mResponse); + return NS_OK; +} + +NS_IMPL_ISUPPORTS_INHERITED0(RequestHelper, Runnable) + +NS_IMETHODIMP +RequestHelper::Run() { + AssertIsOnDOMFileThread(); + + MonitorAutoLock lock(mMonitor); + + switch (mState) { + case State::Initial: { + mState = State::ResponsePending; + { + MonitorAutoUnlock unlock(mMonitor); + mActor = mObject->StartRequest(mParams, this); + } + if (NS_WARN_IF(!mActor) && mState != State::Complete) { + // If we fail to even create the actor, instantly fail and notify our + // caller of the error. Otherwise we'll notify from OnResponse as called + // by the actor. + mResultCode = NS_ERROR_FAILURE; + mState = State::Complete; + lock.Notify(); + } + return NS_OK; + } + + case State::Canceling: { + // StartRequest() could fail or OnResponse was already called, so we need + // to check if actor is not null. The actor can also be in the final + // (finishing) state, in that case we are not allowed to send the cancel + // message and it wouldn't make any sense because the request is about to + // be destroyed anyway. + if (mActor && !mActor->Finishing()) { + mActor->SendCancel(); + } + return NS_OK; + } + + case State::Complete: { + // The operation was cancelled before we ran, do nothing. + return NS_OK; + } + + default: + MOZ_CRASH("Bad state!"); + } +} + +void RequestHelper::OnResponse(const LSRequestResponse& aResponse) { + AssertIsOnDOMFileThread(); + + MonitorAutoLock lock(mMonitor); + + MOZ_ASSERT(mState == State::ResponsePending || mState == State::Canceling); + + mActor = nullptr; + + mResponse = aResponse; + + mState = State::Complete; + + lock.Notify(); +} + +} // namespace mozilla::dom diff --git a/dom/localstorage/LSObject.h b/dom/localstorage/LSObject.h new file mode 100644 index 0000000000..d394816fa4 --- /dev/null +++ b/dom/localstorage/LSObject.h @@ -0,0 +1,220 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_LSObject_h +#define mozilla_dom_localstorage_LSObject_h + +#include <cstdint> +#include "ErrorList.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/Storage.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsCycleCollectionParticipant.h" +#include "nsID.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArrayForwardDeclare.h" + +class nsGlobalWindowInner; +class nsIEventTarget; +class nsIPrincipal; +class nsISerialEventTarget; +class nsPIDOMWindowInner; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class LSDatabase; +class LSObjectChild; +class LSObserver; +class LSRequestChild; +class LSRequestChildCallback; +class LSRequestParams; +class LSRequestResponse; + +/** + * Backs the WebIDL `Storage` binding; all content LocalStorage calls are + * handled by this class. + * + * ## Semantics under e10s / multi-process ## + * + * A snapshot mechanism used in conjuction with stable points ensures that JS + * run-to-completion semantics are experienced even if the same origin is + * concurrently accessing LocalStorage across multiple content processes. + * + * ### Snapshot Consistency ### + * + * An LSSnapshot is created locally whenever the contents of LocalStorage are + * about to be read or written (including length). This synchronously + * establishes a corresponding Snapshot in PBackground in the parent process. + * An effort is made to send as much data from the parent process as possible, + * so sites using a small/reasonable amount of LocalStorage data will have it + * sent to the content process for immediate access. Sites with greater + * LocalStorage usage may only have some of the information relayed. In that + * case, the parent Snapshot will ensure that it retains the exact state of the + * parent Datastore at the moment the Snapshot was created. + */ +class LSObject final : public Storage { + using PrincipalInfo = mozilla::ipc::PrincipalInfo; + + friend nsGlobalWindowInner; + + UniquePtr<PrincipalInfo> mPrincipalInfo; + UniquePtr<PrincipalInfo> mStoragePrincipalInfo; + + RefPtr<LSDatabase> mDatabase; + RefPtr<LSObserver> mObserver; + + uint32_t mPrivateBrowsingId; + Maybe<nsID> mClientId; + Maybe<PrincipalInfo> mClientPrincipalInfo; + nsCString mOrigin; + nsCString mOriginKey; + nsString mDocumentURI; + + bool mInExplicitSnapshot; + + public: + /** + * The normal creation path invoked by nsGlobalWindowInner. + */ + static nsresult CreateForWindow(nsPIDOMWindowInner* aWindow, + Storage** aStorage); + + /** + * nsIDOMStorageManager creation path for use in testing logic. Supports the + * system principal where CreateForWindow does not. This is also why aPrivate + * exists separate from the principal; because the system principal can never + * be mutated to have a private browsing id even though it can be used in a + * window/document marked as private browsing. That's a legacy issue that is + * being dealt with, but it's why it exists here. + */ + static nsresult CreateForPrincipal(nsPIDOMWindowInner* aWindow, + nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, + const nsAString& aDocumentURI, + bool aPrivate, LSObject** aObject); + + void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(LSObject); } + + const RefPtr<LSDatabase>& DatabaseStrongRef() const { return mDatabase; } + + const nsString& DocumentURI() const { return mDocumentURI; } + + bool InExplicitSnapshot() const { return mInExplicitSnapshot; } + + LSRequestChild* StartRequest(const LSRequestParams& aParams, + LSRequestChildCallback* aCallback); + + // Storage overrides. + StorageType Type() const override; + + bool IsForkOf(const Storage* aStorage) const override; + + int64_t GetOriginQuotaUsage() const override; + + void Disconnect() override; + + uint32_t GetLength(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void Key(uint32_t aIndex, nsAString& aResult, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void GetItem(const nsAString& aKey, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) override; + + void GetSupportedNames(nsTArray<nsString>& aNames) override; + + void SetItem(const nsAString& aKey, const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) override; + + void RemoveItem(const nsAString& aKey, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) override; + + ////////////////////////////////////////////////////////////////////////////// + // Testing Methods: See Storage.h + void Open(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) override; + + void Close(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) override; + + void BeginExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void CheckpointExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + bool GetHasSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + int64_t GetSnapshotUsage(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + ////////////////////////////////////////////////////////////////////////////// + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(LSObject, Storage) + + private: + LSObject(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal); + + ~LSObject(); + + nsresult DoRequestSynchronously(const LSRequestParams& aParams, + LSRequestResponse& aResponse); + + nsresult EnsureDatabase(); + + void DropDatabase(); + + /** + * Invoked by nsGlobalWindowInner whenever a new "storage" event listener is + * added to the window in order to ensure that "storage" events are received + * from other processes. (`LSObject::OnChange` directly invokes + * `Storage::NotifyChange` to notify in-process listeners.) + * + * If this is the first request in the process for an observer for this + * origin, this will trigger a RequestHelper-mediated synchronous LSRequest + * to prepare a new observer in the parent process and also construction of + * corresponding actors, which will result in the observer being fully + * registered in the parent process. + */ + nsresult EnsureObserver(); + + /** + * Invoked by nsGlobalWindowInner whenever its last "storage" event listener + * is removed. + */ + void DropObserver(); + + /** + * Internal helper method used by mutation methods that wraps the call to + * Storage::NotifyChange to generate same-process "storage" events. + */ + void OnChange(const nsAString& aKey, const nsAString& aOldValue, + const nsAString& aNewValue); + + // Storage overrides. + void LastRelease() override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_LSObject_h diff --git a/dom/localstorage/LSObserver.cpp b/dom/localstorage/LSObserver.cpp new file mode 100644 index 0000000000..fda56bd1ed --- /dev/null +++ b/dom/localstorage/LSObserver.cpp @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "LSObserver.h" + +// Local includes +#include "ActorsChild.h" + +// Global includes +#include <utility> +#include "mozilla/StaticPtr.h" +#include "nsTHashMap.h" +#include "nsHashKeys.h" + +namespace mozilla::dom { + +namespace { + +using LSObserverHashtable = nsTHashMap<nsCStringHashKey, LSObserver*>; + +StaticAutoPtr<LSObserverHashtable> gLSObservers; + +} // namespace + +LSObserver::LSObserver(const nsACString& aOrigin) + : mActor(nullptr), mOrigin(aOrigin) { + AssertIsOnOwningThread(); + + if (!gLSObservers) { + gLSObservers = new LSObserverHashtable(); + } + + MOZ_ASSERT(!gLSObservers->Contains(mOrigin)); + gLSObservers->InsertOrUpdate(mOrigin, this); +} + +LSObserver::~LSObserver() { + AssertIsOnOwningThread(); + + if (mActor) { + mActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!"); + } + + MOZ_ASSERT(gLSObservers); + MOZ_ASSERT(gLSObservers->Get(mOrigin)); + gLSObservers->Remove(mOrigin); + + if (!gLSObservers->Count()) { + gLSObservers = nullptr; + } +} + +// static +LSObserver* LSObserver::Get(const nsACString& aOrigin) { + return gLSObservers ? gLSObservers->Get(aOrigin) : nullptr; +} + +void LSObserver::SetActor(LSObserverChild* aActor) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!mActor); + + mActor = aActor; +} + +} // namespace mozilla::dom diff --git a/dom/localstorage/LSObserver.h b/dom/localstorage/LSObserver.h new file mode 100644 index 0000000000..55107bc6c4 --- /dev/null +++ b/dom/localstorage/LSObserver.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_LSObserver_h +#define mozilla_dom_localstorage_LSObserver_h + +#include "mozilla/Assertions.h" +#include "nsISupports.h" +#include "nsString.h" + +namespace mozilla::dom { + +class LSObserverChild; + +/** + * Effectively just a refcounted life-cycle management wrapper around + * LSObserverChild which exists to receive "storage" event information from + * other processes. (Same-process events are handled within the process, see + * `LSObject::OnChange`.) + * + * ## Lifecycle ## + * - Created by LSObject::EnsureObserver via synchronous LSRequest idiom + * whenever the first window's origin adds a "storage" event. Placed in the + * gLSObservers LSObserverHashtable for subsequent LSObject's via + * LSObserver::Get lookup. + * - The LSObserverChild directly handles "Observe" messages, shunting them + * directly to Storage::NotifyChange which does all the legwork of notifying + * windows about "storage" events. + * - Destroyed when refcount goes to zero due to all owning LSObjects being + * destroyed or having their `LSObject::DropObserver` methods invoked due to + * the last "storage" event listener being removed from the owning window. + */ +class LSObserver final { + friend class LSObject; + + LSObserverChild* mActor; + + const nsCString mOrigin; + + public: + static LSObserver* Get(const nsACString& aOrigin); + + NS_INLINE_DECL_REFCOUNTING(LSObserver) + + void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(LSObserver); } + + void SetActor(LSObserverChild* aActor); + + void ClearActor() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + + mActor = nullptr; + } + + private: + // Only created by LSObject. + explicit LSObserver(const nsACString& aOrigin); + + ~LSObserver(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_localstorage_LSObserver_h diff --git a/dom/localstorage/LSSnapshot.cpp b/dom/localstorage/LSSnapshot.cpp new file mode 100644 index 0000000000..e41a682ae4 --- /dev/null +++ b/dom/localstorage/LSSnapshot.cpp @@ -0,0 +1,1078 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/LSSnapshot.h" + +// Local includes +#include "ActorsChild.h" +#include "LSDatabase.h" +#include "LSWriteOptimizer.h" +#include "LSWriteOptimizerImpl.h" +#include "LocalStorageCommon.h" + +// Global includes +#include <cstdint> +#include <cstdlib> +#include <new> +#include <type_traits> +#include <utility> +#include "ErrorList.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Maybe.h" +#include "mozilla/Preferences.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/LSValue.h" +#include "mozilla/dom/PBackgroundLSDatabase.h" +#include "mozilla/dom/PBackgroundLSSharedTypes.h" +#include "mozilla/dom/PBackgroundLSSnapshot.h" +#include "nsBaseHashtable.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsITimer.h" +#include "nsString.h" +#include "nsStringFlags.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsTStringRepr.h" +#include "nscore.h" + +namespace mozilla::dom { + +/** + * Coalescing manipulation queue used by `LSSnapshot`. Used by `LSSnapshot` to + * buffer and coalesce manipulations before they are sent to the parent process, + * when a Snapshot Checkpoints. (This can only be done when there are no + * observers for other content processes.) + */ +class SnapshotWriteOptimizer final : public LSWriteOptimizer<LSValue> { + public: + void Enumerate(nsTArray<LSWriteInfo>& aWriteInfos); +}; + +void SnapshotWriteOptimizer::Enumerate(nsTArray<LSWriteInfo>& aWriteInfos) { + AssertIsOnOwningThread(); + + // The mWriteInfos hash table contains all write infos, but it keeps them in + // an arbitrary order, which means write infos need to be sorted before being + // processed. + + nsTArray<NotNull<WriteInfo*>> writeInfos; + GetSortedWriteInfos(writeInfos); + + for (WriteInfo* writeInfo : writeInfos) { + switch (writeInfo->GetType()) { + case WriteInfo::InsertItem: { + auto insertItemInfo = static_cast<InsertItemInfo*>(writeInfo); + + LSSetItemInfo setItemInfo; + setItemInfo.key() = insertItemInfo->GetKey(); + setItemInfo.value() = insertItemInfo->GetValue(); + + aWriteInfos.AppendElement(std::move(setItemInfo)); + + break; + } + + case WriteInfo::UpdateItem: { + auto updateItemInfo = static_cast<UpdateItemInfo*>(writeInfo); + + if (updateItemInfo->UpdateWithMove()) { + // See the comment in LSWriteOptimizer::InsertItem for more details + // about the UpdateWithMove flag. + + LSRemoveItemInfo removeItemInfo; + removeItemInfo.key() = updateItemInfo->GetKey(); + + aWriteInfos.AppendElement(std::move(removeItemInfo)); + } + + LSSetItemInfo setItemInfo; + setItemInfo.key() = updateItemInfo->GetKey(); + setItemInfo.value() = updateItemInfo->GetValue(); + + aWriteInfos.AppendElement(std::move(setItemInfo)); + + break; + } + + case WriteInfo::DeleteItem: { + auto deleteItemInfo = static_cast<DeleteItemInfo*>(writeInfo); + + LSRemoveItemInfo removeItemInfo; + removeItemInfo.key() = deleteItemInfo->GetKey(); + + aWriteInfos.AppendElement(std::move(removeItemInfo)); + + break; + } + + case WriteInfo::Truncate: { + LSClearInfo clearInfo; + + aWriteInfos.AppendElement(std::move(clearInfo)); + + break; + } + + default: + MOZ_CRASH("Bad type!"); + } + } +} + +LSSnapshot::LSSnapshot(LSDatabase* aDatabase) + : mDatabase(aDatabase), + mActor(nullptr), + mInitLength(0), + mLength(0), + mUsage(0), + mPeakUsage(0), + mLoadState(LoadState::Initial), + mHasOtherProcessDatabases(false), + mHasOtherProcessObservers(false), + mExplicit(false), + mHasPendingStableStateCallback(false), + mHasPendingIdleTimerCallback(false), + mDirty(false) +#ifdef DEBUG + , + mInitialized(false), + mSentFinish(false) +#endif +{ + AssertIsOnOwningThread(); +} + +LSSnapshot::~LSSnapshot() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mDatabase); + MOZ_ASSERT(!mHasPendingStableStateCallback); + MOZ_ASSERT(!mHasPendingIdleTimerCallback); + MOZ_ASSERT_IF(mInitialized, mSentFinish); + + if (mActor) { + mActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!"); + } +} + +void LSSnapshot::SetActor(LSSnapshotChild* aActor) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!mActor); + + mActor = aActor; +} + +nsresult LSSnapshot::Init(const nsAString& aKey, + const LSSnapshotInitInfo& aInitInfo, bool aExplicit) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mSelfRef); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mLoadState == LoadState::Initial); + MOZ_ASSERT(!mInitialized); + MOZ_ASSERT(!mSentFinish); + + mSelfRef = this; + + LoadState loadState = aInitInfo.loadState(); + + const nsTArray<LSItemInfo>& itemInfos = aInitInfo.itemInfos(); + for (uint32_t i = 0; i < itemInfos.Length(); i++) { + const LSItemInfo& itemInfo = itemInfos[i]; + + const LSValue& value = itemInfo.value(); + + if (loadState != LoadState::AllOrderedItems && !value.IsVoid()) { + mLoadedItems.Insert(itemInfo.key()); + } + + mValues.InsertOrUpdate(itemInfo.key(), value.AsString()); + } + + if (loadState == LoadState::Partial) { + if (aInitInfo.addKeyToUnknownItems()) { + mUnknownItems.Insert(aKey); + } + mInitLength = aInitInfo.totalLength(); + mLength = mInitLength; + } else if (loadState == LoadState::AllOrderedKeys) { + mInitLength = aInitInfo.totalLength(); + } else { + MOZ_ASSERT(loadState == LoadState::AllOrderedItems); + } + + mUsage = aInitInfo.usage(); + mPeakUsage = aInitInfo.peakUsage(); + + mLoadState = aInitInfo.loadState(); + + mHasOtherProcessDatabases = aInitInfo.hasOtherProcessDatabases(); + mHasOtherProcessObservers = aInitInfo.hasOtherProcessObservers(); + + mExplicit = aExplicit; + +#ifdef DEBUG + mInitialized = true; +#endif + + if (mHasOtherProcessObservers) { + mWriteAndNotifyInfos = MakeUnique<nsTArray<LSWriteAndNotifyInfo>>(); + } else { + mWriteOptimizer = MakeUnique<SnapshotWriteOptimizer>(); + } + + if (!mExplicit) { + mIdleTimer = NS_NewTimer(); + MOZ_ASSERT(mIdleTimer); + + ScheduleStableStateCallback(); + } + + return NS_OK; +} + +nsresult LSSnapshot::GetLength(uint32_t* aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + if (mLoadState == LoadState::Partial) { + *aResult = mLength; + } else { + *aResult = mValues.Count(); + } + + return NS_OK; +} + +nsresult LSSnapshot::GetKey(uint32_t aIndex, nsAString& aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + nsresult rv = EnsureAllKeys(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aResult.SetIsVoid(true); + for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) { + if (aIndex == 0) { + aResult = iter.Key(); + return NS_OK; + } + aIndex--; + } + + return NS_OK; +} + +nsresult LSSnapshot::GetItem(const nsAString& aKey, nsAString& aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + nsString result; + nsresult rv = GetItemInternal(aKey, Optional<nsString>(), result); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aResult = result; + return NS_OK; +} + +nsresult LSSnapshot::GetKeys(nsTArray<nsString>& aKeys) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + nsresult rv = EnsureAllKeys(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + AppendToArray(aKeys, mValues.Keys()); + + return NS_OK; +} + +nsresult LSSnapshot::SetItem(const nsAString& aKey, const nsAString& aValue, + LSNotifyInfo& aNotifyInfo) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + nsString oldValue; + nsresult rv = + GetItemInternal(aKey, Optional<nsString>(nsString(aValue)), oldValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool changed; + if (oldValue == aValue && oldValue.IsVoid() == aValue.IsVoid()) { + changed = false; + } else { + changed = true; + + auto autoRevertValue = MakeScopeExit([&] { + if (oldValue.IsVoid()) { + mValues.Remove(aKey); + } else { + mValues.InsertOrUpdate(aKey, oldValue); + } + }); + + // Anything that can fail must be done early before we start modifying the + // state. + + Maybe<LSValue> oldValueFromString; + if (mHasOtherProcessObservers) { + oldValueFromString.emplace(); + if (NS_WARN_IF(!oldValueFromString->InitFromString(oldValue))) { + return NS_ERROR_FAILURE; + } + } + + LSValue valueFromString; + if (NS_WARN_IF(!valueFromString.InitFromString(aValue))) { + return NS_ERROR_FAILURE; + } + + int64_t delta = static_cast<int64_t>(aValue.Length()) - + static_cast<int64_t>(oldValue.Length()); + + if (oldValue.IsVoid()) { + delta += static_cast<int64_t>(aKey.Length()); + } + + rv = UpdateUsage(delta); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (oldValue.IsVoid() && mLoadState == LoadState::Partial) { + mLength++; + } + + if (mHasOtherProcessObservers) { + MOZ_ASSERT(mWriteAndNotifyInfos); + MOZ_ASSERT(oldValueFromString.isSome()); + + LSSetItemAndNotifyInfo setItemAndNotifyInfo; + setItemAndNotifyInfo.key() = aKey; + setItemAndNotifyInfo.oldValue() = oldValueFromString.value(); + setItemAndNotifyInfo.value() = valueFromString; + + mWriteAndNotifyInfos->AppendElement(std::move(setItemAndNotifyInfo)); + } else { + MOZ_ASSERT(mWriteOptimizer); + + if (oldValue.IsVoid()) { + mWriteOptimizer->InsertItem(aKey, valueFromString); + } else { + mWriteOptimizer->UpdateItem(aKey, valueFromString); + } + } + + autoRevertValue.release(); + } + + aNotifyInfo.changed() = changed; + aNotifyInfo.oldValue() = oldValue; + + return NS_OK; +} + +nsresult LSSnapshot::RemoveItem(const nsAString& aKey, + LSNotifyInfo& aNotifyInfo) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + nsString oldValue; + nsresult rv = + GetItemInternal(aKey, Optional<nsString>(VoidString()), oldValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool changed; + if (oldValue.IsVoid()) { + changed = false; + } else { + changed = true; + + auto autoRevertValue = MakeScopeExit([&] { + MOZ_ASSERT(!oldValue.IsVoid()); + mValues.InsertOrUpdate(aKey, oldValue); + }); + + // Anything that can fail must be done early before we start modifying the + // state. + + Maybe<LSValue> oldValueFromString; + if (mHasOtherProcessObservers) { + oldValueFromString.emplace(); + if (NS_WARN_IF(!oldValueFromString->InitFromString(oldValue))) { + return NS_ERROR_FAILURE; + } + } + + int64_t delta = -(static_cast<int64_t>(aKey.Length()) + + static_cast<int64_t>(oldValue.Length())); + + DebugOnly<nsresult> rv = UpdateUsage(delta); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (mLoadState == LoadState::Partial) { + mLength--; + } + + if (mHasOtherProcessObservers) { + MOZ_ASSERT(mWriteAndNotifyInfos); + MOZ_ASSERT(oldValueFromString.isSome()); + + LSRemoveItemAndNotifyInfo removeItemAndNotifyInfo; + removeItemAndNotifyInfo.key() = aKey; + removeItemAndNotifyInfo.oldValue() = oldValueFromString.value(); + + mWriteAndNotifyInfos->AppendElement(std::move(removeItemAndNotifyInfo)); + } else { + MOZ_ASSERT(mWriteOptimizer); + + mWriteOptimizer->DeleteItem(aKey); + } + + autoRevertValue.release(); + } + + aNotifyInfo.changed() = changed; + aNotifyInfo.oldValue() = oldValue; + + return NS_OK; +} + +nsresult LSSnapshot::Clear(LSNotifyInfo& aNotifyInfo) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + uint32_t length; + if (mLoadState == LoadState::Partial) { + length = mLength; + MOZ_ASSERT(length); + + MOZ_ALWAYS_TRUE(mActor->SendLoaded()); + + mLoadedItems.Clear(); + mUnknownItems.Clear(); + mLength = 0; + mLoadState = LoadState::AllOrderedItems; + } else { + length = mValues.Count(); + } + + bool changed; + if (!length) { + changed = false; + } else { + changed = true; + + int64_t delta = 0; + for (const auto& entry : mValues) { + const nsAString& key = entry.GetKey(); + const nsString& value = entry.GetData(); + + delta += -static_cast<int64_t>(key.Length()) - + static_cast<int64_t>(value.Length()); + } + + DebugOnly<nsresult> rv = UpdateUsage(delta); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + mValues.Clear(); + + if (mHasOtherProcessObservers) { + MOZ_ASSERT(mWriteAndNotifyInfos); + + LSClearInfo clearInfo; + + mWriteAndNotifyInfos->AppendElement(std::move(clearInfo)); + } else { + MOZ_ASSERT(mWriteOptimizer); + + mWriteOptimizer->Truncate(); + } + } + + aNotifyInfo.changed() = changed; + + return NS_OK; +} + +void LSSnapshot::MarkDirty() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + if (mDirty) { + return; + } + + mDirty = true; + + if (!mExplicit && !mHasPendingStableStateCallback) { + CancelIdleTimer(); + + MOZ_ALWAYS_SUCCEEDS(Checkpoint()); + + MOZ_ALWAYS_SUCCEEDS(Finish()); + } else { + MOZ_ASSERT(!mHasPendingIdleTimerCallback); + } +} + +nsresult LSSnapshot::ExplicitCheckpoint() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mExplicit); + MOZ_ASSERT(!mHasPendingStableStateCallback); + MOZ_ASSERT(!mHasPendingIdleTimerCallback); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + nsresult rv = Checkpoint(/* aSync */ true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult LSSnapshot::ExplicitEnd() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mExplicit); + MOZ_ASSERT(!mHasPendingStableStateCallback); + MOZ_ASSERT(!mHasPendingIdleTimerCallback); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + nsresult rv = Checkpoint(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<LSSnapshot> kungFuDeathGrip = this; + + rv = Finish(/* aSync */ true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +int64_t LSSnapshot::GetUsage() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + return mUsage; +} + +void LSSnapshot::ScheduleStableStateCallback() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mIdleTimer); + MOZ_ASSERT(!mExplicit); + MOZ_ASSERT(!mHasPendingStableStateCallback); + + CancelIdleTimer(); + + nsCOMPtr<nsIRunnable> runnable = this; + nsContentUtils::RunInStableState(runnable.forget()); + + mHasPendingStableStateCallback = true; +} + +void LSSnapshot::MaybeScheduleStableStateCallback() { + AssertIsOnOwningThread(); + + if (!mExplicit && !mHasPendingStableStateCallback) { + ScheduleStableStateCallback(); + } else { + MOZ_ASSERT(!mHasPendingIdleTimerCallback); + } +} + +nsresult LSSnapshot::GetItemInternal(const nsAString& aKey, + const Optional<nsString>& aValue, + nsAString& aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + nsString result; + + switch (mLoadState) { + case LoadState::Partial: { + if (mValues.Get(aKey, &result)) { + MOZ_ASSERT(!result.IsVoid()); + } else if (mLoadedItems.Contains(aKey) || mUnknownItems.Contains(aKey)) { + result.SetIsVoid(true); + } else { + LSValue value; + nsTArray<LSItemInfo> itemInfos; + if (NS_WARN_IF(!mActor->SendLoadValueAndMoreItems( + nsString(aKey), &value, &itemInfos))) { + return NS_ERROR_FAILURE; + } + + result = value.AsString(); + + if (result.IsVoid()) { + mUnknownItems.Insert(aKey); + } else { + mLoadedItems.Insert(aKey); + mValues.InsertOrUpdate(aKey, result); + + // mLoadedItems.Count()==mInitLength is checked below. + } + + for (uint32_t i = 0; i < itemInfos.Length(); i++) { + const LSItemInfo& itemInfo = itemInfos[i]; + + mLoadedItems.Insert(itemInfo.key()); + mValues.InsertOrUpdate(itemInfo.key(), itemInfo.value().AsString()); + } + + if (mLoadedItems.Count() == mInitLength) { + mLoadedItems.Clear(); + mUnknownItems.Clear(); + mLength = 0; + mLoadState = LoadState::AllUnorderedItems; + } + } + + if (aValue.WasPassed()) { + const nsString& value = aValue.Value(); + if (!value.IsVoid()) { + mValues.InsertOrUpdate(aKey, value); + } else if (!result.IsVoid()) { + mValues.Remove(aKey); + } + } + + break; + } + + case LoadState::AllOrderedKeys: { + if (mValues.Get(aKey, &result)) { + if (result.IsVoid()) { + LSValue value; + nsTArray<LSItemInfo> itemInfos; + if (NS_WARN_IF(!mActor->SendLoadValueAndMoreItems( + nsString(aKey), &value, &itemInfos))) { + return NS_ERROR_FAILURE; + } + + result = value.AsString(); + + MOZ_ASSERT(!result.IsVoid()); + + mLoadedItems.Insert(aKey); + mValues.InsertOrUpdate(aKey, result); + + // mLoadedItems.Count()==mInitLength is checked below. + + for (uint32_t i = 0; i < itemInfos.Length(); i++) { + const LSItemInfo& itemInfo = itemInfos[i]; + + mLoadedItems.Insert(itemInfo.key()); + mValues.InsertOrUpdate(itemInfo.key(), itemInfo.value().AsString()); + } + + if (mLoadedItems.Count() == mInitLength) { + mLoadedItems.Clear(); + MOZ_ASSERT(mLength == 0); + mLoadState = LoadState::AllOrderedItems; + } + } + } else { + result.SetIsVoid(true); + } + + if (aValue.WasPassed()) { + const nsString& value = aValue.Value(); + if (!value.IsVoid()) { + mValues.InsertOrUpdate(aKey, value); + } else if (!result.IsVoid()) { + mValues.Remove(aKey); + } + } + + break; + } + + case LoadState::AllUnorderedItems: + case LoadState::AllOrderedItems: { + if (aValue.WasPassed()) { + const nsString& value = aValue.Value(); + if (!value.IsVoid()) { + mValues.WithEntryHandle(aKey, [&](auto&& entry) { + if (entry) { + result = std::exchange(entry.Data(), value); + } else { + result.SetIsVoid(true); + entry.Insert(value); + } + }); + } else { + if (auto entry = mValues.Lookup(aKey)) { + result = entry.Data(); + MOZ_ASSERT(!result.IsVoid()); + entry.Remove(); + } else { + result.SetIsVoid(true); + } + } + } else { + if (mValues.Get(aKey, &result)) { + MOZ_ASSERT(!result.IsVoid()); + } else { + result.SetIsVoid(true); + } + } + + break; + } + + default: + MOZ_CRASH("Bad state!"); + } + + aResult = result; + return NS_OK; +} + +nsresult LSSnapshot::EnsureAllKeys() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + MOZ_ASSERT(mLoadState != LoadState::Initial); + + if (mLoadState == LoadState::AllOrderedKeys || + mLoadState == LoadState::AllOrderedItems) { + return NS_OK; + } + + nsTArray<nsString> keys; + if (NS_WARN_IF(!mActor->SendLoadKeys(&keys))) { + return NS_ERROR_FAILURE; + } + + nsTHashMap<nsStringHashKey, nsString> newValues; + + for (auto key : keys) { + newValues.InsertOrUpdate(key, VoidString()); + } + + if (mHasOtherProcessObservers) { + MOZ_ASSERT(mWriteAndNotifyInfos); + + if (!mWriteAndNotifyInfos->IsEmpty()) { + for (uint32_t index = 0; index < mWriteAndNotifyInfos->Length(); + index++) { + const LSWriteAndNotifyInfo& writeAndNotifyInfo = + mWriteAndNotifyInfos->ElementAt(index); + + switch (writeAndNotifyInfo.type()) { + case LSWriteAndNotifyInfo::TLSSetItemAndNotifyInfo: { + newValues.InsertOrUpdate( + writeAndNotifyInfo.get_LSSetItemAndNotifyInfo().key(), + VoidString()); + break; + } + case LSWriteAndNotifyInfo::TLSRemoveItemAndNotifyInfo: { + newValues.Remove( + writeAndNotifyInfo.get_LSRemoveItemAndNotifyInfo().key()); + break; + } + case LSWriteAndNotifyInfo::TLSClearInfo: { + newValues.Clear(); + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + } + } + } else { + MOZ_ASSERT(mWriteOptimizer); + + if (mWriteOptimizer->HasWrites()) { + nsTArray<LSWriteInfo> writeInfos; + mWriteOptimizer->Enumerate(writeInfos); + + MOZ_ASSERT(!writeInfos.IsEmpty()); + + for (uint32_t index = 0; index < writeInfos.Length(); index++) { + const LSWriteInfo& writeInfo = writeInfos[index]; + + switch (writeInfo.type()) { + case LSWriteInfo::TLSSetItemInfo: { + newValues.InsertOrUpdate(writeInfo.get_LSSetItemInfo().key(), + VoidString()); + break; + } + case LSWriteInfo::TLSRemoveItemInfo: { + newValues.Remove(writeInfo.get_LSRemoveItemInfo().key()); + break; + } + case LSWriteInfo::TLSClearInfo: { + newValues.Clear(); + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + } + } + } + + MOZ_ASSERT_IF(mLoadState == LoadState::AllUnorderedItems, + newValues.Count() == mValues.Count()); + + for (auto iter = newValues.Iter(); !iter.Done(); iter.Next()) { + nsString value; + if (mValues.Get(iter.Key(), &value)) { + iter.Data() = value; + } + } + + mValues.SwapElements(newValues); + + if (mLoadState == LoadState::Partial) { + mUnknownItems.Clear(); + mLength = 0; + mLoadState = LoadState::AllOrderedKeys; + } else { + MOZ_ASSERT(mLoadState == LoadState::AllUnorderedItems); + + MOZ_ASSERT(mUnknownItems.Count() == 0); + MOZ_ASSERT(mLength == 0); + mLoadState = LoadState::AllOrderedItems; + } + + return NS_OK; +} + +nsresult LSSnapshot::UpdateUsage(int64_t aDelta) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mDatabase); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mPeakUsage >= mUsage); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + int64_t newUsage = mUsage + aDelta; + if (newUsage > mPeakUsage) { + const int64_t minSize = newUsage - mPeakUsage; + + int64_t size; + if (NS_WARN_IF(!mActor->SendIncreasePeakUsage(minSize, &size))) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(size >= 0); + + if (size == 0) { + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + + mPeakUsage += size; + } + + mUsage = newUsage; + return NS_OK; +} + +nsresult LSSnapshot::Checkpoint(bool aSync) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + if (mHasOtherProcessObservers) { + MOZ_ASSERT(mWriteAndNotifyInfos); + + if (!mWriteAndNotifyInfos->IsEmpty()) { + if (aSync) { + MOZ_ALWAYS_TRUE( + mActor->SendSyncCheckpointAndNotify(*mWriteAndNotifyInfos)); + } else { + MOZ_ALWAYS_TRUE( + mActor->SendAsyncCheckpointAndNotify(*mWriteAndNotifyInfos)); + } + + mWriteAndNotifyInfos->Clear(); + } + } else { + MOZ_ASSERT(mWriteOptimizer); + + if (mWriteOptimizer->HasWrites()) { + nsTArray<LSWriteInfo> writeInfos; + mWriteOptimizer->Enumerate(writeInfos); + + MOZ_ASSERT(!writeInfos.IsEmpty()); + + if (aSync) { + MOZ_ALWAYS_TRUE(mActor->SendSyncCheckpoint(writeInfos)); + } else { + MOZ_ALWAYS_TRUE(mActor->SendAsyncCheckpoint(writeInfos)); + } + + mWriteOptimizer->Reset(); + } + } + + return NS_OK; +} + +nsresult LSSnapshot::Finish(bool aSync) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mDatabase); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + if (aSync) { + MOZ_ALWAYS_TRUE(mActor->SendSyncFinish()); + } else { + MOZ_ALWAYS_TRUE(mActor->SendAsyncFinish()); + } + + mDatabase->NoteFinishedSnapshot(this); + +#ifdef DEBUG + mSentFinish = true; +#endif + + // Clear the self reference added in Init method. + MOZ_ASSERT(mSelfRef); + mSelfRef = nullptr; + + return NS_OK; +} + +void LSSnapshot::CancelIdleTimer() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mIdleTimer); + + if (mHasPendingIdleTimerCallback) { + MOZ_ALWAYS_SUCCEEDS(mIdleTimer->Cancel()); + mHasPendingIdleTimerCallback = false; + } +} + +// static +void LSSnapshot::IdleTimerCallback(nsITimer* aTimer, void* aClosure) { + MOZ_ASSERT(aTimer); + + auto* self = static_cast<LSSnapshot*>(aClosure); + MOZ_ASSERT(self); + MOZ_ASSERT(self->mIdleTimer); + MOZ_ASSERT(SameCOMIdentity(self->mIdleTimer, aTimer)); + MOZ_ASSERT(!self->mHasPendingStableStateCallback); + MOZ_ASSERT(self->mHasPendingIdleTimerCallback); + + self->mHasPendingIdleTimerCallback = false; + + MOZ_ALWAYS_SUCCEEDS(self->Finish()); +} + +NS_IMPL_ISUPPORTS(LSSnapshot, nsIRunnable) + +NS_IMETHODIMP +LSSnapshot::Run() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mExplicit); + MOZ_ASSERT(mHasPendingStableStateCallback); + MOZ_ASSERT(!mHasPendingIdleTimerCallback); + + mHasPendingStableStateCallback = false; + + MOZ_ALWAYS_SUCCEEDS(Checkpoint()); + + // 1. The unused pre-incremented snapshot peak usage can't be undone when + // there are other snapshots for the same database. We only add a pending + // usage delta when a snapshot finishes and usage deltas are then applied + // when the last database becomes inactive. + // 2. If there's a snapshot with pre-incremented peak usage, the next + // snapshot will use that as a base for its usage. + // 3. When a task for given snapshot finishes, we try to reuse the snapshot + // by only checkpointing the snapshot and delaying the finish by a timer. + // 4. If two or more tabs for the same origin use localStorage periodically + // at the same time the usage gradually grows until it hits the quota + // limit. + // 5. We prevent that from happening by finishing the snapshot immediatelly + // if there are databases in other processess. + + if (mDirty || mHasOtherProcessDatabases || + !Preferences::GetBool("dom.storage.snapshot_reusing")) { + MOZ_ALWAYS_SUCCEEDS(Finish()); + } else { + MOZ_ASSERT(mIdleTimer); + + MOZ_ALWAYS_SUCCEEDS(mIdleTimer->InitWithNamedFuncCallback( + IdleTimerCallback, this, + StaticPrefs::dom_storage_snapshot_idle_timeout_ms(), + nsITimer::TYPE_ONE_SHOT, "LSSnapshot::IdleTimerCallback")); + + mHasPendingIdleTimerCallback = true; + } + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/localstorage/LSSnapshot.h b/dom/localstorage/LSSnapshot.h new file mode 100644 index 0000000000..c12a4efce2 --- /dev/null +++ b/dom/localstorage/LSSnapshot.h @@ -0,0 +1,193 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_LSSnapshot_h +#define mozilla_dom_localstorage_LSSnapshot_h + +#include <cstdint> +#include <cstdlib> +#include "ErrorList.h" +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "nsCOMPtr.h" +#include "nsTHashMap.h" +#include "nsHashKeys.h" +#include "nsIRunnable.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArrayForwardDeclare.h" +#include "nsTHashSet.h" + +class nsITimer; + +namespace mozilla::dom { + +class LSDatabase; +class LSNotifyInfo; +class LSSnapshotChild; +class LSSnapshotInitInfo; +class LSWriteAndNotifyInfo; +class SnapshotWriteOptimizer; + +template <typename> +class Optional; + +class LSSnapshot final : public nsIRunnable { + public: + /** + * The LoadState expresses what subset of information a snapshot has from the + * authoritative Datastore in the parent process. The initial snapshot is + * populated heuristically based on the size of the keys and size of the items + * (inclusive of the key value; item is key+value, not just value) of the + * entire datastore relative to the configured prefill limit (via pref + * "dom.storage.snapshot_prefill" exposed as gSnapshotPrefill in bytes). + * + * If there's less data than the limit, we send both keys and values and end + * up as AllOrderedItems. If there's enough room for all the keys but not + * all the values, we end up as AllOrderedKeys with as many values present as + * would fit. If there's not enough room for all the keys, then we end up as + * Partial with as many key-value pairs as will fit. + * + * The state AllUnorderedItems can only be reached by code getting items one + * by one. + */ + enum class LoadState { + /** + * Class constructed, Init(LSSnapshotInitInfo) has not been invoked yet. + */ + Initial, + /** + * Some keys and their values are known. + */ + Partial, + /** + * All the keys are known in order, but some values are unknown. + */ + AllOrderedKeys, + /** + * All keys and their values are known, but in an arbitrary order. + */ + AllUnorderedItems, + /** + * All keys and their values are known and are present in their canonical + * order. This is everything, and is the preferred case. The initial + * population will send this info when the size of all items is less than + * the prefill threshold. + * + * mValues will contain all keys and values, mLoadedItems and mUnknownItems + * are unused. + */ + AllOrderedItems, + EndGuard + }; + + private: + RefPtr<LSSnapshot> mSelfRef; + + RefPtr<LSDatabase> mDatabase; + + nsCOMPtr<nsITimer> mIdleTimer; + + LSSnapshotChild* mActor; + + nsTHashSet<nsString> mLoadedItems; + nsTHashSet<nsString> mUnknownItems; + nsTHashMap<nsStringHashKey, nsString> mValues; + UniquePtr<SnapshotWriteOptimizer> mWriteOptimizer; + UniquePtr<nsTArray<LSWriteAndNotifyInfo>> mWriteAndNotifyInfos; + + uint32_t mInitLength; + uint32_t mLength; + int64_t mUsage; + int64_t mPeakUsage; + + LoadState mLoadState; + + bool mHasOtherProcessDatabases; + bool mHasOtherProcessObservers; + bool mExplicit; + bool mHasPendingStableStateCallback; + bool mHasPendingIdleTimerCallback; + bool mDirty; + +#ifdef DEBUG + bool mInitialized; + bool mSentFinish; +#endif + + public: + explicit LSSnapshot(LSDatabase* aDatabase); + + void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(LSSnapshot); } + + void SetActor(LSSnapshotChild* aActor); + + void ClearActor() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + + mActor = nullptr; + } + + bool Explicit() const { return mExplicit; } + + nsresult Init(const nsAString& aKey, const LSSnapshotInitInfo& aInitInfo, + bool aExplicit); + + nsresult GetLength(uint32_t* aResult); + + nsresult GetKey(uint32_t aIndex, nsAString& aResult); + + nsresult GetItem(const nsAString& aKey, nsAString& aResult); + + nsresult GetKeys(nsTArray<nsString>& aKeys); + + nsresult SetItem(const nsAString& aKey, const nsAString& aValue, + LSNotifyInfo& aNotifyInfo); + + nsresult RemoveItem(const nsAString& aKey, LSNotifyInfo& aNotifyInfo); + + nsresult Clear(LSNotifyInfo& aNotifyInfo); + + void MarkDirty(); + + nsresult ExplicitCheckpoint(); + + nsresult ExplicitEnd(); + + int64_t GetUsage() const; + + private: + ~LSSnapshot(); + + void ScheduleStableStateCallback(); + + void MaybeScheduleStableStateCallback(); + + nsresult GetItemInternal(const nsAString& aKey, + const Optional<nsString>& aValue, + nsAString& aResult); + + nsresult EnsureAllKeys(); + + nsresult UpdateUsage(int64_t aDelta); + + nsresult Checkpoint(bool aSync = false); + + nsresult Finish(bool aSync = false); + + void CancelIdleTimer(); + + static void IdleTimerCallback(nsITimer* aTimer, void* aClosure); + + NS_DECL_ISUPPORTS + NS_DECL_NSIRUNNABLE +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_localstorage_LSSnapshot_h diff --git a/dom/localstorage/LSValue.cpp b/dom/localstorage/LSValue.cpp new file mode 100644 index 0000000000..03637fdeba --- /dev/null +++ b/dom/localstorage/LSValue.cpp @@ -0,0 +1,202 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/LSValue.h" + +#include "mozIStorageStatement.h" +#include "mozilla/dom/SnappyUtils.h" +#include "mozilla/fallible.h" +#include "mozilla/TextUtils.h" +#include "nsDebug.h" +#include "nsError.h" + +namespace mozilla::dom { + +namespace { + +bool PutStringBytesToCString(const nsAString& aSrc, nsCString& aDest) { + const char16_t* bufferData; + const size_t byteLength = sizeof(char16_t) * aSrc.GetData(&bufferData); + + char* destDataPtr; + const auto newLength = aDest.GetMutableData(&destDataPtr, byteLength); + if (newLength != byteLength) { + return false; + } + std::memcpy(static_cast<void*>(destDataPtr), + static_cast<const void*>(bufferData), byteLength); + + return true; +} + +template <class T> +using TypeBufferResult = Result<std::pair<T, nsCString>, nsresult>; + +} // namespace + +bool PutCStringBytesToString(const nsACString& aSrc, nsString& aDest) { + const char* bufferData; + const size_t byteLength = aSrc.GetData(&bufferData); + const size_t shortLength = byteLength / sizeof(char16_t); + + char16_t* destDataPtr; + const auto newLength = aDest.GetMutableData(&destDataPtr, shortLength); + if (newLength != shortLength) { + return false; + } + + std::memcpy(static_cast<void*>(destDataPtr), + static_cast<const void*>(bufferData), byteLength); + return true; +} + +LSValue::Converter::Converter(const LSValue& aValue) { + using ConversionType = LSValue::ConversionType; + using CompressionType = LSValue::CompressionType; + + if (aValue.mBuffer.IsVoid()) { + mBuffer.SetIsVoid(true); + return; + } + + const CompressionType compressionType = aValue.GetCompressionType(); + const ConversionType conversionType = aValue.GetConversionType(); + + const nsCString uncompressed = [compressionType, &aValue]() { + if (CompressionType::UNCOMPRESSED != compressionType) { + nsCString buffer; + MOZ_ASSERT(CompressionType::SNAPPY == compressionType); + if (NS_WARN_IF(!SnappyUncompress(aValue.mBuffer, buffer))) { + buffer.Truncate(); + } + return buffer; + } + + return aValue.mBuffer; + }(); + + if (ConversionType::NONE != conversionType) { + MOZ_ASSERT(ConversionType::UTF16_UTF8 == conversionType); + if (NS_WARN_IF(!CopyUTF8toUTF16(uncompressed, mBuffer, fallible))) { + mBuffer.SetIsVoid(true); + } + return; + } + + if (NS_WARN_IF(!PutCStringBytesToString(uncompressed, mBuffer))) { + mBuffer.SetIsVoid(true); + } +} + +bool LSValue::InitFromString(const nsAString& aBuffer) { + MOZ_ASSERT(mBuffer.IsVoid()); + MOZ_ASSERT(!mUTF16Length); + MOZ_ASSERT(ConversionType::NONE == mConversionType); + MOZ_ASSERT(CompressionType::UNCOMPRESSED == mCompressionType); + + if (aBuffer.IsVoid()) { + return true; + } + + const uint32_t utf16Length = aBuffer.Length(); + + const auto conversionRes = [&aBuffer]() -> TypeBufferResult<ConversionType> { + nsCString converted; + + if (Utf16ValidUpTo(aBuffer) == aBuffer.Length()) { + if (NS_WARN_IF(!CopyUTF16toUTF8(aBuffer, converted, fallible))) { + return Err(NS_ERROR_OUT_OF_MEMORY); + } + return std::pair{ConversionType::UTF16_UTF8, std::move(converted)}; + } + + if (NS_WARN_IF(!PutStringBytesToCString(aBuffer, converted))) { + return Err(NS_ERROR_OUT_OF_MEMORY); + } + return std::pair{ConversionType::NONE, std::move(converted)}; + }(); + + if (conversionRes.isErr()) { + return false; + } + + const auto& [conversionType, converted] = conversionRes.inspect(); + + const auto compressionRes = + [&converted = converted]() -> TypeBufferResult<CompressionType> { + nsCString compressed; + if (NS_WARN_IF(!SnappyCompress(converted, compressed))) { + return Err(NS_ERROR_OUT_OF_MEMORY); + } + if (!compressed.IsVoid()) { + return std::pair{CompressionType::SNAPPY, std::move(compressed)}; + } + + compressed = converted; + return std::pair{CompressionType::UNCOMPRESSED, std::move(compressed)}; + }(); + + if (compressionRes.isErr()) { + return false; + } + + const auto& [compressionType, compressed] = compressionRes.inspect(); + + mBuffer = compressed; + mUTF16Length = utf16Length; + mConversionType = conversionType; + mCompressionType = compressionType; + + return true; +} + +nsresult LSValue::InitFromStatement(mozIStorageStatement* aStatement, + uint32_t aIndex) { + MOZ_ASSERT(aStatement); + MOZ_ASSERT(mBuffer.IsVoid()); + MOZ_ASSERT(!mUTF16Length); + MOZ_ASSERT(ConversionType::NONE == mConversionType); + MOZ_ASSERT(CompressionType::UNCOMPRESSED == mCompressionType); + + int32_t utf16Length; + nsresult rv = aStatement->GetInt32(aIndex, &utf16Length); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int32_t conversionType; + rv = aStatement->GetInt32(aIndex + 1, &conversionType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int32_t compressionType; + rv = aStatement->GetInt32(aIndex + 2, &compressionType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString buffer; + rv = aStatement->GetBlobAsUTF8String(aIndex + 3, buffer); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mBuffer = buffer; + mUTF16Length = static_cast<uint32_t>(utf16Length); + mConversionType = static_cast<decltype(mConversionType)>(conversionType); + mCompressionType = static_cast<decltype(mCompressionType)>(compressionType); + + return NS_OK; +} + +const LSValue& VoidLSValue() { + static const LSValue sVoidLSValue; + + return sVoidLSValue; +} + +} // namespace mozilla::dom diff --git a/dom/localstorage/LSValue.h b/dom/localstorage/LSValue.h new file mode 100644 index 0000000000..c1b362a50f --- /dev/null +++ b/dom/localstorage/LSValue.h @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_LSValue_h +#define mozilla_dom_localstorage_LSValue_h + +#include <cstdint> +#include "ErrorList.h" +#include "SnappyUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Span.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsTStringRepr.h" + +class mozIStorageStatement; + +namespace IPC { +template <typename> +struct ParamTraits; +} + +namespace mozilla::dom { + +/** + * Represents a LocalStorage value. From content's perspective, values (if + * present) are always DOMStrings. This is also true from a quota-tracking + * perspective. However, for memory and disk efficiency it's preferable to store + * the value in alternate compressed or utf-8 encoding representations. The + * LSValue type exists to support these alternate representations, dynamically + * decompressing/re-encoding to utf-16 while still tracking value size on a + * utf-16 basis for quota purposes. + */ +class LSValue final { + friend struct IPC::ParamTraits<LSValue>; + + public: + enum class ConversionType : uint8_t { + NONE = 0u, + UTF16_UTF8 = 1u, + NUM_TYPES = 2u + }; + + enum class CompressionType : uint8_t { + UNCOMPRESSED = 0u, + SNAPPY = 1u, + NUM_TYPES = 2u + }; + + nsCString mBuffer; + uint32_t mUTF16Length; + ConversionType mConversionType; + CompressionType mCompressionType; + + explicit LSValue() + : mUTF16Length(0u), + mConversionType(ConversionType::NONE), + mCompressionType(CompressionType::UNCOMPRESSED) { + SetIsVoid(true); + } + + bool InitFromString(const nsAString& aBuffer); + + nsresult InitFromStatement(mozIStorageStatement* aStatement, uint32_t aIndex); + + bool IsVoid() const { return mBuffer.IsVoid(); } + + void SetIsVoid(bool aVal) { mBuffer.SetIsVoid(aVal); } + + /** + * This represents the "physical" length that the parent process uses for + * the size of value/item computation. This can also be used to see how much + * memory the value is using at rest or what the cost is for sending the value + * over IPC. + */ + uint32_t Length() const { return mBuffer.Length(); } + + /* + * This represents the "logical" length that content sees and that is also + * used for quota management purposes. + */ + uint32_t UTF16Length() const { return mUTF16Length; } + + ConversionType GetConversionType() const { return mConversionType; } + + CompressionType GetCompressionType() const { return mCompressionType; } + + bool Equals(const LSValue& aOther) const { + return mBuffer == aOther.mBuffer && + mBuffer.IsVoid() == aOther.mBuffer.IsVoid() && + mUTF16Length == aOther.mUTF16Length && + mConversionType == aOther.mConversionType && + mCompressionType == aOther.mCompressionType; + } + + bool operator==(const LSValue& aOther) const { return Equals(aOther); } + + bool operator!=(const LSValue& aOther) const { return !Equals(aOther); } + + constexpr const nsCString& AsCString() const { return mBuffer; } + + class Converter { + nsString mBuffer; + + public: + explicit Converter(const LSValue& aValue); + Converter(Converter&& aOther) = default; + ~Converter() = default; + + operator const nsString&() const { return mBuffer; } + + private: + Converter() = delete; + Converter(const Converter&) = delete; + Converter& operator=(const Converter&) = delete; + Converter& operator=(const Converter&&) = delete; + }; + + Converter AsString() const { return Converter{*this}; } +}; + +const LSValue& VoidLSValue(); + +/** + * XXX: This function doesn't have to be public + * once the support for shadow writes is removed. + */ +bool PutCStringBytesToString(const nsACString& aSrc, nsString& aDest); + +} // namespace mozilla::dom + +#endif // mozilla_dom_localstorage_LSValue_h diff --git a/dom/localstorage/LSWriteOptimizer.cpp b/dom/localstorage/LSWriteOptimizer.cpp new file mode 100644 index 0000000000..0928dca35e --- /dev/null +++ b/dom/localstorage/LSWriteOptimizer.cpp @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "LSWriteOptimizer.h" + +#include <new> +#include "nsBaseHashtable.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +class LSWriteOptimizerBase::WriteInfoComparator { + public: + bool Equals(const WriteInfo* a, const WriteInfo* b) const { + MOZ_ASSERT(a && b); + return a->SerialNumber() == b->SerialNumber(); + } + + bool LessThan(const WriteInfo* a, const WriteInfo* b) const { + MOZ_ASSERT(a && b); + return a->SerialNumber() < b->SerialNumber(); + } +}; + +void LSWriteOptimizerBase::DeleteItem(const nsAString& aKey, int64_t aDelta) { + AssertIsOnOwningThread(); + + mWriteInfos.WithEntryHandle(aKey, [&](auto&& entry) { + if (entry && entry.Data()->GetType() == WriteInfo::InsertItem) { + entry.Remove(); + } else { + entry.InsertOrUpdate( + MakeUnique<DeleteItemInfo>(NextSerialNumber(), aKey)); + } + }); + + mTotalDelta += aDelta; +} + +void LSWriteOptimizerBase::Truncate(int64_t aDelta) { + AssertIsOnOwningThread(); + + mWriteInfos.Clear(); + + if (!mTruncateInfo) { + mTruncateInfo = MakeUnique<TruncateInfo>(NextSerialNumber()); + } + + mTotalDelta += aDelta; +} + +void LSWriteOptimizerBase::GetSortedWriteInfos( + nsTArray<NotNull<WriteInfo*>>& aWriteInfos) { + AssertIsOnOwningThread(); + + if (mTruncateInfo) { + aWriteInfos.InsertElementSorted(WrapNotNullUnchecked(mTruncateInfo.get()), + WriteInfoComparator()); + } + + for (const auto& entry : mWriteInfos) { + WriteInfo* writeInfo = entry.GetWeak(); + + aWriteInfos.InsertElementSorted(WrapNotNull(writeInfo), + WriteInfoComparator()); + } +} + +} // namespace mozilla::dom diff --git a/dom/localstorage/LSWriteOptimizer.h b/dom/localstorage/LSWriteOptimizer.h new file mode 100644 index 0000000000..353f8c17b5 --- /dev/null +++ b/dom/localstorage/LSWriteOptimizer.h @@ -0,0 +1,194 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_LSWriteOptimizer_h +#define mozilla_dom_localstorage_LSWriteOptimizer_h + +#include <cstdint> +#include <utility> +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/UniquePtr.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArrayForwardDeclare.h" + +namespace mozilla::dom { + +/** + * Base class for coalescing manipulation queue. + */ +class LSWriteOptimizerBase { + class WriteInfoComparator; + + protected: + class WriteInfo; + class DeleteItemInfo; + class TruncateInfo; + + UniquePtr<WriteInfo> mTruncateInfo; + nsClassHashtable<nsStringHashKey, WriteInfo> mWriteInfos; + CheckedUint64 mLastSerialNumber; + int64_t mTotalDelta; + + NS_DECL_OWNINGTHREAD + + public: + LSWriteOptimizerBase() : mLastSerialNumber(0), mTotalDelta(0) {} + + LSWriteOptimizerBase(LSWriteOptimizerBase&& aWriteOptimizer) + : mTruncateInfo(std::move(aWriteOptimizer.mTruncateInfo)) { + AssertIsOnOwningThread(); + MOZ_ASSERT(&aWriteOptimizer != this); + + mWriteInfos.SwapElements(aWriteOptimizer.mWriteInfos); + mTotalDelta = aWriteOptimizer.mTotalDelta; + aWriteOptimizer.mTotalDelta = 0; + } + + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(LSWriteOptimizerBase); + } + + void DeleteItem(const nsAString& aKey, int64_t aDelta = 0); + + void Truncate(int64_t aDelta = 0); + + bool HasWrites() const { + AssertIsOnOwningThread(); + + return mTruncateInfo || !mWriteInfos.IsEmpty(); + } + + void Reset() { + AssertIsOnOwningThread(); + + mTruncateInfo = nullptr; + mWriteInfos.Clear(); + } + + protected: + uint64_t NextSerialNumber() { + AssertIsOnOwningThread(); + + mLastSerialNumber++; + + MOZ_ASSERT(mLastSerialNumber.isValid()); + + return mLastSerialNumber.value(); + } + + /** + * This method can be used by derived classes to get a sorted list of write + * infos. Write infos are sorted by the serial number. + */ + void GetSortedWriteInfos(nsTArray<NotNull<WriteInfo*>>& aWriteInfos); +}; + +/** + * Base class for specific mutations. + */ +class LSWriteOptimizerBase::WriteInfo { + uint64_t mSerialNumber; + + public: + WriteInfo(uint64_t aSerialNumber) : mSerialNumber(aSerialNumber) {} + + virtual ~WriteInfo() = default; + + uint64_t SerialNumber() const { return mSerialNumber; } + + enum Type { InsertItem = 0, UpdateItem, DeleteItem, Truncate }; + + virtual Type GetType() const = 0; +}; + +class LSWriteOptimizerBase::DeleteItemInfo final : public WriteInfo { + nsString mKey; + + public: + DeleteItemInfo(uint64_t aSerialNumber, const nsAString& aKey) + : WriteInfo(aSerialNumber), mKey(aKey) {} + + const nsAString& GetKey() const { return mKey; } + + private: + Type GetType() const override { return DeleteItem; } +}; + +/** + * Truncate mutation. + */ +class LSWriteOptimizerBase::TruncateInfo final : public WriteInfo { + public: + explicit TruncateInfo(uint64_t aSerialNumber) : WriteInfo(aSerialNumber) {} + + private: + Type GetType() const override { return Truncate; } +}; + +/** + * Coalescing manipulation queue. + */ +template <typename T, typename U = T> +class LSWriteOptimizer; + +template <typename T, typename U> +class LSWriteOptimizer : public LSWriteOptimizerBase { + protected: + class InsertItemInfo; + class UpdateItemInfo; + + public: + void InsertItem(const nsAString& aKey, const T& aValue, int64_t aDelta = 0); + + void UpdateItem(const nsAString& aKey, const T& aValue, int64_t aDelta = 0); +}; + +/** + * Insert mutation (the key did not previously exist). + */ +template <typename T, typename U> +class LSWriteOptimizer<T, U>::InsertItemInfo : public WriteInfo { + nsString mKey; + U mValue; + + public: + InsertItemInfo(uint64_t aSerialNumber, const nsAString& aKey, const T& aValue) + : WriteInfo(aSerialNumber), mKey(aKey), mValue(aValue) {} + + const nsAString& GetKey() const { return mKey; } + + const T& GetValue() const { return mValue; } + + private: + WriteInfo::Type GetType() const override { return InsertItem; } +}; + +/** + * Update mutation (the key already existed). + */ +template <typename T, typename U> +class LSWriteOptimizer<T, U>::UpdateItemInfo final : public InsertItemInfo { + bool mUpdateWithMove; + + public: + UpdateItemInfo(uint64_t aSerialNumber, const nsAString& aKey, const T& aValue, + bool aUpdateWithMove) + : InsertItemInfo(aSerialNumber, aKey, aValue), + mUpdateWithMove(aUpdateWithMove) {} + + bool UpdateWithMove() const { return mUpdateWithMove; } + + private: + WriteInfo::Type GetType() const override { return WriteInfo::UpdateItem; } +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_localstorage_LSWriteOptimizer_h diff --git a/dom/localstorage/LSWriteOptimizerImpl.h b/dom/localstorage/LSWriteOptimizerImpl.h new file mode 100644 index 0000000000..32ccff6c63 --- /dev/null +++ b/dom/localstorage/LSWriteOptimizerImpl.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_LSWriteOptimizerImpl_h +#define mozilla_dom_localstorage_LSWriteOptimizerImpl_h + +#include "LSWriteOptimizer.h" + +namespace mozilla::dom { + +template <typename T, typename U> +void LSWriteOptimizer<T, U>::InsertItem(const nsAString& aKey, const T& aValue, + int64_t aDelta) { + AssertIsOnOwningThread(); + + mWriteInfos.WithEntryHandle(aKey, [&](auto&& entry) { + if (entry && entry.Data()->GetType() == WriteInfo::DeleteItem) { + // We could just simply replace the deletion with ordinary update, but + // that would preserve item's original position/index. Imagine a case when + // we have only one existing key k1. Now let's create a new optimizer and + // remove k1, add k2 and add k1 back. The final order should be k2, k1 + // (ordinary update would produce k1, k2). So we need to differentiate + // between normal update and "optimized" update which resulted from a + // deletion followed by an insertion. We use the UpdateWithMove flag for + // this. + + entry.Update(MakeUnique<UpdateItemInfo>(NextSerialNumber(), aKey, aValue, + /* aUpdateWithMove */ true)); + } else { + entry.InsertOrUpdate( + MakeUnique<InsertItemInfo>(NextSerialNumber(), aKey, aValue)); + } + }); + + mTotalDelta += aDelta; +} + +template <typename T, typename U> +void LSWriteOptimizer<T, U>::UpdateItem(const nsAString& aKey, const T& aValue, + int64_t aDelta) { + AssertIsOnOwningThread(); + + mWriteInfos.WithEntryHandle(aKey, [&](auto&& entry) { + if (entry && entry.Data()->GetType() == WriteInfo::InsertItem) { + entry.Update( + MakeUnique<InsertItemInfo>(NextSerialNumber(), aKey, aValue)); + } else { + entry.InsertOrUpdate( + MakeUnique<UpdateItemInfo>(NextSerialNumber(), aKey, aValue, + /* aUpdateWithMove */ false)); + } + }); + + mTotalDelta += aDelta; +} + +} // namespace mozilla::dom + +#endif // mozilla_dom_localstorage_LSWriteOptimizerImpl_h diff --git a/dom/localstorage/LocalStorageCommon.cpp b/dom/localstorage/LocalStorageCommon.cpp new file mode 100644 index 0000000000..a5b01e0146 --- /dev/null +++ b/dom/localstorage/LocalStorageCommon.cpp @@ -0,0 +1,160 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "LocalStorageCommon.h" + +#include <cstdint> +#include "MainThreadUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Logging.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/Preferences.h" +#include "mozilla/RefPtr.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/StorageUtils.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/net/MozURL.h" +#include "mozilla/net/WebSocketFrame.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsPrintfCString.h" +#include "nsString.h" +#include "nsStringFlags.h" +#include "nsXULAppAPI.h" + +namespace mozilla::dom { + +using namespace mozilla::net; + +namespace { + +StaticMutex gNextGenLocalStorageMutex; +Atomic<int32_t> gNextGenLocalStorageEnabled(-1); +LazyLogModule gLogger("LocalStorage"); + +} // namespace + +const char16_t* kLocalStorageType = u"localStorage"; + +bool NextGenLocalStorageEnabled() { + if (XRE_IsParentProcess()) { + StaticMutexAutoLock lock(gNextGenLocalStorageMutex); + + if (gNextGenLocalStorageEnabled == -1) { + // Ideally all this Mutex stuff would be replaced with just using + // an AtStartup StaticPref, but there are concerns about this causing + // deadlocks if this access needs to init the AtStartup cache. + bool enabled = + !StaticPrefs:: + dom_storage_enable_unsupported_legacy_implementation_DoNotUseDirectly(); + + gNextGenLocalStorageEnabled = enabled ? 1 : 0; + } + + return !!gNextGenLocalStorageEnabled; + } + + return CachedNextGenLocalStorageEnabled(); +} + +void RecvInitNextGenLocalStorageEnabled(const bool aEnabled) { + MOZ_ASSERT(!XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(gNextGenLocalStorageEnabled == -1); + + gNextGenLocalStorageEnabled = aEnabled ? 1 : 0; +} + +bool CachedNextGenLocalStorageEnabled() { + MOZ_ASSERT(gNextGenLocalStorageEnabled != -1); + + return !!gNextGenLocalStorageEnabled; +} + +Result<std::pair<nsCString, nsCString>, nsresult> GenerateOriginKey2( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo) { + OriginAttributes attrs; + nsCString spec; + + switch (aPrincipalInfo.type()) { + case mozilla::ipc::PrincipalInfo::TNullPrincipalInfo: { + const auto& info = aPrincipalInfo.get_NullPrincipalInfo(); + + attrs = info.attrs(); + spec = info.spec(); + + break; + } + + case mozilla::ipc::PrincipalInfo::TContentPrincipalInfo: { + const auto& info = aPrincipalInfo.get_ContentPrincipalInfo(); + + attrs = info.attrs(); + spec = info.spec(); + + break; + } + + default: { + spec.SetIsVoid(true); + + break; + } + } + + if (spec.IsVoid()) { + return Err(NS_ERROR_UNEXPECTED); + } + + nsCString originAttrSuffix; + attrs.CreateSuffix(originAttrSuffix); + + RefPtr<MozURL> specURL; + QM_TRY(MOZ_TO_RESULT(MozURL::Init(getter_AddRefs(specURL), spec))); + + nsCString host(specURL->Host()); + uint32_t length = host.Length(); + if (length > 0 && host.CharAt(0) == '[' && host.CharAt(length - 1) == ']') { + host = Substring(host, 1, length - 2); + } + + nsCString domainOrigin(host); + + if (domainOrigin.IsEmpty()) { + // For the file:/// protocol use the exact directory as domain. + if (specURL->Scheme().EqualsLiteral("file")) { + domainOrigin.Assign(specURL->Directory()); + } + } + + // Append reversed domain + nsAutoCString reverseDomain; + nsresult rv = StorageUtils::CreateReversedDomain(domainOrigin, reverseDomain); + if (NS_FAILED(rv)) { + return Err(rv); + } + + nsCString originKey = reverseDomain; + + // Append scheme + originKey.Append(':'); + originKey.Append(specURL->Scheme()); + + // Append port if any + int32_t port = specURL->RealPort(); + if (port != -1) { + originKey.AppendPrintf(":%d", port); + } + + return std::make_pair(std::move(originAttrSuffix), std::move(originKey)); +} + +LogModule* GetLocalStorageLogger() { return gLogger; } + +} // namespace mozilla::dom diff --git a/dom/localstorage/LocalStorageCommon.h b/dom/localstorage/LocalStorageCommon.h new file mode 100644 index 0000000000..408f7106a2 --- /dev/null +++ b/dom/localstorage/LocalStorageCommon.h @@ -0,0 +1,263 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_LocalStorageCommon_h +#define mozilla_dom_localstorage_LocalStorageCommon_h + +#include <cstdint> +#include "ErrorList.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "nsLiteralString.h" +#include "nsStringFwd.h" + +/* + * Local storage + * ~~~~~~~~~~~~~ + * + * Implementation overview + * ~~~~~~~~~~~~~~~~~~~~~~~ + * + * The implementation is based on a per principal/origin cache (datastore) + * living in the main process and synchronous calls initiated from content + * processes. + * The IPC communication is managed by database actors which link to the + * datastore. + * The synchronous blocking of the main thread is done by using a special + * technique or by using standard synchronous IPC calls. + * + * General architecture + * ~~~~~~~~~~~~~~~~~~~~ + * The current browser architecture consists of one main process and multiple + * content processes (there are other processes but for simplicity's sake, they + * are not mentioned here). The processes use the IPC communication to talk to + * each other. Local storage implementation uses the client-server model, so + * the main process manages all the data and content processes then request + * particular data from the main process. The main process is also called the + * parent or the parent side, the content process is then called the child or + * the child side. + * + * Datastores + * ~~~~~~~~~~ + * + * A datastore provides a convenient way to access data for given origin. The + * data is always preloaded into memory and indexed using a hash table. This + * enables very fast access to particular stored items. There can be only one + * datastore per origin and exists solely on the parent side. It is represented + * by the "Datastore" class. A datastore instance is a ref counted object and + * lives on the PBackground thread, it is kept alive by database objects. When + * the last database object for given origin is destroyed, the associated + * datastore object is destroyed too. + * + * Databases + * ~~~~~~~~~ + * + * A database allows direct access to a datastore from a content process. There + * can be multiple databases for the same origin, but they all share the same + * datastore. + * Databases use the PBackgroundLSDatabase IPDL protocol for IPC communication. + * Given the nature of local storage, most of PBackgroundLSDatabase messages + * are synchronous. + * + * On the parent side, the database is represented by the "Database" class that + * is a parent actor as well (implements the "PBackgroundLSDatabaseParent" + * interface). A database instance is a ref counted object and lives on the + * PBackground thread. + * All live database actors are tracked in an array. + * + * On the child side, the database is represented by the "LSDatabase" class + * that provides indirect access to a child actor. An LSDatabase instance is a + * ref counted object and lives on the main thread. + * The actual child actor is represented by the "LSDatabaseChild" class that + * implements the "PBackgroundLSDatabaseChild" interface. An "LSDatabaseChild" + * instance is not ref counted and lives on the main thread too. + * + * Synchronous blocking + * ~~~~~~~~~~~~~~~~~~~~ + * + * Local storage is synchronous in nature which means the execution can't move + * forward until there's a reply for given method call. + * Since we have to use IPC anyway, we could just always use synchronous IPC + * messages for all local storage method calls. Well, there's a problem with + * that approach. + * If the main process needs to do some off PBackground thread stuff like + * getting info from principals on the main thread or some asynchronous stuff + * like directory locking before sending a reply to a synchronous message, then + * we would have to block the thread or spin the event loop which is usually a + * bad idea, especially in the main process. + * Instead, we can use a special thread in the content process called + * RemoteLazyInputStream thread for communication with the main process using + * asynchronous messages and synchronously block the main thread until the DOM + * File thread is done (the main thread blocking is a bit more complicated, see + * the comment in RequestHelper::StartAndReturnResponse for more details). + * Anyway, the extra hop to the RemoteLazyInputStream thread brings another + * overhead and latency. The final solution is to use a combination of the + * special thread for complex stuff like datastore preparation and synchronous + * IPC messages sent directly from the main thread for database access when data + * is already loaded from disk into memory. + * + * Requests + * ~~~~~~~~ + * + * Requests are used to handle asynchronous high level datastore operations + * which are initiated in a content process and then processed in the parent + * process (for example, preparation of a datastore). + * Requests use the "PBackgroundLSRequest" IPDL protocol for IPC communication. + * + * On the parent side, the request is represented by the "LSRequestBase" class + * that is a parent actor as well (implements the "PBackgroundLSRequestParent" + * interface). It's an abstract class (contains pure virtual functions) so it + * can't be used to create instances. + * It also inherits from the "DatastoreOperationBase" class which is a generic + * base class for all datastore operations. The "DatastoreOperationsBase" class + * inherits from the "Runnable" class, so derived class instances are ref + * counted, can be dispatched to multiple threads and thus they are used on + * multiple threads. However, derived class instances can be created on the + * PBackground thread only. + * + * On the child side, the request is represented by the "RequestHelper" class + * that covers all the complexity needed to start a new request, handle + * responses and do safe main thread blocking at the same time. + * It inherits from the "Runnable" class, so instances are ref counted and + * they are internally used on multiple threads (specifically on the main + * thread and on the RemoteLazyInputStream thread). Anyway, users should create + * and use instances of this class only on the main thread. + * The actual child actor is represented by the "LSRequestChild" class that + * implements the "PBackgroundLSRequestChild" interface. An "LSRequestChild" + * instance is not ref counted and lives on the RemoteLazyInputStream thread. + * Request responses are passed using the "LSRequestChildCallback" interface. + * + * Preparation of a datastore + * ~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * The datastore preparation is needed to make sure a datastore is fully loaded + * into memory. Every datastore preparation produces a unique id (even if the + * datastore for given origin already exists). + * On the parent side, the preparation is handled by the "PrepareDatastoreOp" + * class which inherits from the "LSRequestBase" class. The preparation process + * on the parent side is quite complicated, it happens sequentially on multiple + * threads and is managed by a state machine. + * On the child side, the preparation is done in the LSObject::EnsureDatabase + * method using the "RequestHelper" class. The method starts a new preparation + * request and obtains a unique id produced by the parent (or an error code if + * the requested failed to complete). + * + * Linking databases to a datastore + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * A datastore exists only on the parent side, but it can be accessed from the + * content via database actors. Database actors are initiated on the child side + * and they need to be linked to a datastore on the parent side via an id. The + * datastore preparation process gives us the required id. + * The linking is initiated on the child side in the LSObject::EnsureDatabase + * method by calling SendPBackgroundLSDatabaseConstructor and finished in + * RecvPBackgroundLSDatabaseConstructor on the parent side. + * + * Actor migration + * ~~~~~~~~~~~~~~~ + * + * In theory, the datastore preparation request could return a database actor + * directly (instead of returning an id intended for database linking to a + * datastore). However, as it was explained above, the preparation must be done + * on the RemoteLazyInputStream thread and database objects are used on the main + * thread. The returned actor would have to be migrated from the + * RemoteLazyInputStream thread to the main thread and that's something which + * our IPDL doesn't support yet. + * + * Exposing local storage + * ~~~~~~~~~~~~~~~~~~~~~~ + * + * The implementation is exposed to the DOM via window.localStorage attribute. + * Local storage's sibling, session storage shares the same WebIDL interface + * for exposing it to web content, therefore there's an abstract class called + * "Storage" that handles some of the common DOM bindings stuff. Local storage + * specific functionality is defined in the "LSObject" derived class. + * The "LSObject" class is also a starting point for the datastore preparation + * and database linking. + * + * Local storage manager + * ~~~~~~~~~~~~~~~~~~~~~ + * + * The local storage manager exposes some of the features that need to be + * available only in the chrome code or tests. The manager is represented by + * the "LocalStorageManager2" class that implements the "nsIDOMStorageManager" + * interface. + */ + +namespace mozilla { + +class LogModule; + +namespace ipc { + +class PrincipalInfo; + +} // namespace ipc + +namespace dom { + +extern const char16_t* kLocalStorageType; + +/** + * Convenience data-structure to make it easier to track whether a value has + * changed and what its previous value was for notification purposes. Instances + * are created on the stack by LSObject and passed to LSDatabase which in turn + * passes them onto LSSnapshot for final updating/population. LSObject then + * generates an event, if appropriate. + */ +class MOZ_STACK_CLASS LSNotifyInfo { + bool mChanged; + nsString mOldValue; + + public: + LSNotifyInfo() : mChanged(false) {} + + bool changed() const { return mChanged; } + + bool& changed() { return mChanged; } + + const nsString& oldValue() const { return mOldValue; } + + nsString& oldValue() { return mOldValue; } +}; + +/** + * A check of LSNG being enabled, the value is latched once initialized so + * changing the preference during runtime has no effect. May be called on any + * thread in the parent process, but you should call + * CachedNextGenLocalStorageEnabled if you know that NextGenLocalStorageEnabled + * was already called because it is faster. May be called on any thread in + * content processes, but you should call CachedNextGenLocalStorageEnabled + * directly if you know you are in a content process because it is slightly + * faster. + */ +bool NextGenLocalStorageEnabled(); + +/** + * Called by ContentChild during content process initialization to initialize + * the global variable in the content process with the latched value in the + * parent process." + */ +void RecvInitNextGenLocalStorageEnabled(const bool aEnabled); + +/** + * Cached any-thread version of NextGenLocalStorageEnabled(). + */ +bool CachedNextGenLocalStorageEnabled(); + +/** + * Returns a success value containing a pair of origin attribute suffix and + * origin key. + */ +Result<std::pair<nsCString, nsCString>, nsresult> GenerateOriginKey2( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo); + +LogModule* GetLocalStorageLogger(); + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_LocalStorageCommon_h diff --git a/dom/localstorage/LocalStorageManager2.cpp b/dom/localstorage/LocalStorageManager2.cpp new file mode 100644 index 0000000000..69e8c3b4dd --- /dev/null +++ b/dom/localstorage/LocalStorageManager2.cpp @@ -0,0 +1,661 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "LocalStorageManager2.h" + +// Local includes +#include "ActorsChild.h" +#include "LSObject.h" + +// Global includes +#include <utility> +#include "MainThreadUtils.h" +#include "jsapi.h" +#include "mozilla/Assertions.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/RefPtr.h" +#include "mozilla/RemoteLazyInputStreamThread.h" +#include "mozilla/dom/LocalStorageCommon.h" +#include "mozilla/dom/PBackgroundLSRequest.h" +#include "mozilla/dom/PBackgroundLSSharedTypes.h" +#include "mozilla/dom/PBackgroundLSSimpleRequest.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIEventTarget.h" +#include "nsILocalStorageManager.h" +#include "nsIPrincipal.h" +#include "nsIRunnable.h" +#include "nsPIDOMWindow.h" +#include "nsStringFwd.h" +#include "nsThreadUtils.h" +#include "nscore.h" +#include "xpcpublic.h" + +namespace mozilla::dom { + +namespace { + +class AsyncRequestHelper final : public Runnable, + public LSRequestChildCallback { + enum class State { + /** + * The AsyncRequestHelper has been created and dispatched to the + * RemoteLazyInputStream Thread. + */ + Initial, + /** + * Start() has been invoked on the RemoteLazyInputStream Thread and + * LocalStorageManager2::StartRequest has been invoked from there, sending + * an IPC message to PBackground to service the request. We stay in this + * state until a response is received. + */ + ResponsePending, + /** + * A response has been received and AsyncRequestHelper has been dispatched + * back to the owning event target to call Finish(). + */ + Finishing, + /** + * Finish() has been called on the main thread. The promise will be resolved + * according to the received response. + */ + Complete + }; + + // The object we are issuing a request on behalf of. Present because of the + // need to invoke LocalStorageManager2::StartRequest off the main thread. + // Dropped on return to the main-thread in Finish(). + RefPtr<LocalStorageManager2> mManager; + // The thread the AsyncRequestHelper was created on. This should be the main + // thread. + nsCOMPtr<nsIEventTarget> mOwningEventTarget; + // The IPC actor handling the request with standard IPC allocation rules. + // Our reference is nulled in OnResponse which corresponds to the actor's + // __destroy__ method. + LSRequestChild* mActor; + RefPtr<Promise> mPromise; + const LSRequestParams mParams; + LSRequestResponse mResponse; + nsresult mResultCode; + State mState; + + public: + AsyncRequestHelper(LocalStorageManager2* aManager, Promise* aPromise, + const LSRequestParams& aParams) + : Runnable("dom::LocalStorageManager2::AsyncRequestHelper"), + mManager(aManager), + mOwningEventTarget(GetCurrentEventTarget()), + mActor(nullptr), + mPromise(aPromise), + mParams(aParams), + mResultCode(NS_OK), + mState(State::Initial) {} + + bool IsOnOwningThread() const { + MOZ_ASSERT(mOwningEventTarget); + + bool current; + return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(¤t)) && + current; + } + + void AssertIsOnOwningThread() const { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsOnOwningThread()); + } + + nsresult Dispatch(); + + private: + ~AsyncRequestHelper() = default; + + nsresult Start(); + + void Finish(); + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_NSIRUNNABLE + + // LSRequestChildCallback + void OnResponse(const LSRequestResponse& aResponse) override; +}; + +class SimpleRequestResolver final : public LSSimpleRequestChildCallback { + RefPtr<Promise> mPromise; + + public: + explicit SimpleRequestResolver(Promise* aPromise) : mPromise(aPromise) {} + + NS_INLINE_DECL_REFCOUNTING(SimpleRequestResolver, override); + + private: + ~SimpleRequestResolver() = default; + + void HandleResponse(nsresult aResponse); + + void HandleResponse(bool aResponse); + + void HandleResponse(const nsTArray<LSItemInfo>& aResponse); + + // LSRequestChildCallback + void OnResponse(const LSSimpleRequestResponse& aResponse) override; +}; + +nsresult CreatePromise(JSContext* aContext, Promise** aPromise) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aContext); + + nsIGlobalObject* global = + xpc::NativeGlobal(JS::CurrentGlobalOrNull(aContext)); + if (NS_WARN_IF(!global)) { + return NS_ERROR_FAILURE; + } + + ErrorResult result; + RefPtr<Promise> promise = Promise::Create(global, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + promise.forget(aPromise); + return NS_OK; +} + +nsresult CheckedPrincipalToPrincipalInfo( + nsIPrincipal* aPrincipal, mozilla::ipc::PrincipalInfo& aPrincipalInfo) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aPrincipalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!quota::QuotaManager::IsPrincipalInfoValid(aPrincipalInfo))) { + return NS_ERROR_FAILURE; + } + + if (aPrincipalInfo.type() != + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo && + aPrincipalInfo.type() != + mozilla::ipc::PrincipalInfo::TSystemPrincipalInfo) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +} // namespace + +LocalStorageManager2::LocalStorageManager2() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(NextGenLocalStorageEnabled()); +} + +LocalStorageManager2::~LocalStorageManager2() { MOZ_ASSERT(NS_IsMainThread()); } + +NS_IMPL_ISUPPORTS(LocalStorageManager2, nsIDOMStorageManager, + nsILocalStorageManager) + +NS_IMETHODIMP +LocalStorageManager2::PrecacheStorage(nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, + Storage** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aStoragePrincipal); + MOZ_ASSERT(_retval); + + // This method was created as part of the e10s-ification of the old LS + // implementation to perform a preload in the content/current process. That's + // not how things work in LSNG. Instead everything happens in the parent + // process, triggered by the official preloading spot, + // ContentParent::AboutToLoadHttpFtpDocumentForChild. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LocalStorageManager2::CreateStorage(mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, + const nsAString& aDocumentURI, + bool aPrivate, Storage** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aStoragePrincipal); + MOZ_ASSERT(_retval); + + nsCOMPtr<nsPIDOMWindowInner> inner = nsPIDOMWindowInner::From(aWindow); + + RefPtr<LSObject> object; + nsresult rv = LSObject::CreateForPrincipal(inner, aPrincipal, + aStoragePrincipal, aDocumentURI, + aPrivate, getter_AddRefs(object)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + object.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +LocalStorageManager2::GetStorage(mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, bool aPrivate, + Storage** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aStoragePrincipal); + MOZ_ASSERT(_retval); + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LocalStorageManager2::CloneStorage(Storage* aStorageToCloneFrom) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aStorageToCloneFrom); + + // Cloning is specific to sessionStorage; state is forked when a new tab is + // opened from an existing tab. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LocalStorageManager2::CheckStorage(nsIPrincipal* aPrincipal, Storage* aStorage, + bool* _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aStorage); + MOZ_ASSERT(_retval); + + // Only used by sessionStorage. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LocalStorageManager2::GetNextGenLocalStorageEnabled(bool* aResult) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aResult); + + *aResult = NextGenLocalStorageEnabled(); + return NS_OK; +} + +NS_IMETHODIMP +LocalStorageManager2::Preload(nsIPrincipal* aPrincipal, JSContext* aContext, + Promise** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + nsCString originAttrSuffix; + nsCString originKey; + nsresult rv = aPrincipal->GetStorageOriginKey(originKey); + aPrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix); + if (NS_FAILED(rv)) { + return NS_ERROR_NOT_AVAILABLE; + } + + mozilla::ipc::PrincipalInfo principalInfo; + rv = CheckedPrincipalToPrincipalInfo(aPrincipal, principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<Promise> promise; + + if (aContext) { + rv = CreatePromise(aContext, getter_AddRefs(promise)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + LSRequestCommonParams commonParams; + commonParams.principalInfo() = principalInfo; + commonParams.storagePrincipalInfo() = principalInfo; + commonParams.originKey() = originKey; + + LSRequestPreloadDatastoreParams params(commonParams); + + RefPtr<AsyncRequestHelper> helper = + new AsyncRequestHelper(this, promise, params); + + // This will start and finish the async request on the RemoteLazyInputStream + // thread. + // This must be done on RemoteLazyInputStream Thread because it's very likely + // that a content process will issue a prepare datastore request for the same + // principal while blocking the content process on the main thread. + // There would be a potential for deadlock if the preloading was initialized + // from the main thread of the parent process and a11y issued a synchronous + // message from the parent process to the content process (approximately at + // the same time) because the preload request wouldn't be able to respond + // to the Ready message by sending the Finish message which is needed to + // finish the preload request and unblock the prepare datastore request. + rv = helper->Dispatch(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + promise.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +LocalStorageManager2::IsPreloaded(nsIPrincipal* aPrincipal, JSContext* aContext, + Promise** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + RefPtr<Promise> promise; + nsresult rv = CreatePromise(aContext, getter_AddRefs(promise)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + LSSimpleRequestPreloadedParams params; + + rv = CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + params.storagePrincipalInfo() = params.principalInfo(); + + rv = StartSimpleRequest(promise, params); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + promise.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +LocalStorageManager2::GetState(nsIPrincipal* aPrincipal, JSContext* aContext, + Promise** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + RefPtr<Promise> promise; + nsresult rv = CreatePromise(aContext, getter_AddRefs(promise)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + LSSimpleRequestGetStateParams params; + + rv = CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + params.storagePrincipalInfo() = params.principalInfo(); + + rv = StartSimpleRequest(promise, params); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + promise.forget(_retval); + return NS_OK; +} + +LSRequestChild* LocalStorageManager2::StartRequest( + const LSRequestParams& aParams, LSRequestChildCallback* aCallback) { + AssertIsOnDOMFileThread(); + + mozilla::ipc::PBackgroundChild* backgroundActor = + mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return nullptr; + } + + auto actor = new LSRequestChild(); + + if (!backgroundActor->SendPBackgroundLSRequestConstructor(actor, aParams)) { + return nullptr; + } + + // Must set callback after calling SendPBackgroundLSRequestConstructor since + // it can be called synchronously when SendPBackgroundLSRequestConstructor + // fails. + actor->SetCallback(aCallback); + + return actor; +} + +nsresult LocalStorageManager2::StartSimpleRequest( + Promise* aPromise, const LSSimpleRequestParams& aParams) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPromise); + + mozilla::ipc::PBackgroundChild* backgroundActor = + mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return NS_ERROR_FAILURE; + } + + auto actor = new LSSimpleRequestChild(); + + if (!backgroundActor->SendPBackgroundLSSimpleRequestConstructor(actor, + aParams)) { + return NS_ERROR_FAILURE; + } + + RefPtr<SimpleRequestResolver> resolver = new SimpleRequestResolver(aPromise); + + // Must set callback after calling SendPBackgroundLSRequestConstructor since + // it can be called synchronously when SendPBackgroundLSRequestConstructor + // fails. + actor->SetCallback(resolver); + + return NS_OK; +} + +nsresult AsyncRequestHelper::Dispatch() { + AssertIsOnOwningThread(); + + nsCOMPtr<nsIEventTarget> domFileThread = + RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!domFileThread)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + nsresult rv = domFileThread->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult AsyncRequestHelper::Start() { + AssertIsOnDOMFileThread(); + MOZ_ASSERT(mState == State::Initial); + + mState = State::ResponsePending; + + LSRequestChild* actor = mManager->StartRequest(mParams, this); + if (NS_WARN_IF(!actor)) { + return NS_ERROR_FAILURE; + } + + mActor = actor; + + return NS_OK; +} + +void AsyncRequestHelper::Finish() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Finishing); + + if (NS_WARN_IF(NS_FAILED(mResultCode))) { + if (mPromise) { + mPromise->MaybeReject(mResultCode); + } + } else { + switch (mResponse.type()) { + case LSRequestResponse::Tnsresult: + if (mPromise) { + mPromise->MaybeReject(mResponse.get_nsresult()); + } + break; + + case LSRequestResponse::TLSRequestPreloadDatastoreResponse: + if (mPromise) { + mPromise->MaybeResolveWithUndefined(); + } + break; + default: + MOZ_CRASH("Unknown response type!"); + } + } + + mManager = nullptr; + + mState = State::Complete; +} + +NS_IMPL_ISUPPORTS_INHERITED0(AsyncRequestHelper, Runnable) + +NS_IMETHODIMP +AsyncRequestHelper::Run() { + nsresult rv; + + switch (mState) { + case State::Initial: + rv = Start(); + break; + + case State::Finishing: + Finish(); + return NS_OK; + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::Finishing) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = rv; + } + + mState = State::Finishing; + + if (IsOnOwningThread()) { + Finish(); + } else { + MOZ_ALWAYS_SUCCEEDS( + mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); + } + } + + return NS_OK; +} + +void AsyncRequestHelper::OnResponse(const LSRequestResponse& aResponse) { + AssertIsOnDOMFileThread(); + MOZ_ASSERT(mState == State::ResponsePending); + + mActor = nullptr; + + mResponse = aResponse; + + mState = State::Finishing; + + MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +void SimpleRequestResolver::HandleResponse(nsresult aResponse) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPromise); + + mPromise->MaybeReject(aResponse); +} + +void SimpleRequestResolver::HandleResponse(bool aResponse) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPromise); + + mPromise->MaybeResolve(aResponse); +} + +[[nodiscard]] static bool ToJSValue(JSContext* aCx, + const nsTArray<LSItemInfo>& aArgument, + JS::MutableHandle<JS::Value> aValue) { + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + if (!obj) { + return false; + } + + for (size_t i = 0; i < aArgument.Length(); ++i) { + const LSItemInfo& itemInfo = aArgument[i]; + + const nsString& key = itemInfo.key(); + + JS::Rooted<JS::Value> value(aCx); + if (!ToJSValue(aCx, itemInfo.value().AsString(), &value)) { + return false; + } + + if (!JS_DefineUCProperty(aCx, obj, key.BeginReading(), key.Length(), value, + JSPROP_ENUMERATE)) { + return false; + } + } + + aValue.setObject(*obj); + return true; +} + +void SimpleRequestResolver::HandleResponse( + const nsTArray<LSItemInfo>& aResponse) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPromise); + + mPromise->MaybeResolve(aResponse); +} + +void SimpleRequestResolver::OnResponse( + const LSSimpleRequestResponse& aResponse) { + MOZ_ASSERT(NS_IsMainThread()); + + switch (aResponse.type()) { + case LSSimpleRequestResponse::Tnsresult: + HandleResponse(aResponse.get_nsresult()); + break; + + case LSSimpleRequestResponse::TLSSimpleRequestPreloadedResponse: + HandleResponse( + aResponse.get_LSSimpleRequestPreloadedResponse().preloaded()); + break; + + case LSSimpleRequestResponse::TLSSimpleRequestGetStateResponse: + HandleResponse( + aResponse.get_LSSimpleRequestGetStateResponse().itemInfos()); + break; + + default: + MOZ_CRASH("Unknown response type!"); + } +} + +} // namespace mozilla::dom diff --git a/dom/localstorage/LocalStorageManager2.h b/dom/localstorage/LocalStorageManager2.h new file mode 100644 index 0000000000..63cc9a9a15 --- /dev/null +++ b/dom/localstorage/LocalStorageManager2.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_LocalStorageManager2_h +#define mozilla_dom_localstorage_LocalStorageManager2_h + +#include <cstdint> +#include "ErrorList.h" +#include "nsIDOMStorageManager.h" +#include "nsILocalStorageManager.h" +#include "nsISupports.h" + +namespace mozilla::dom { + +class LSRequestChild; +class LSRequestChildCallback; +class LSRequestParams; +class LSSimpleRequestParams; +class Promise; + +/** + * Under LSNG this exposes nsILocalStorageManager::Preload to ContentParent to + * trigger preloading. Otherwise, this is basically just a place for test logic + * that doesn't make sense to put directly on the Storage WebIDL interface. + * + * Previously, the nsIDOMStorageManager XPCOM interface was also used by + * nsGlobalWindowInner to interact with LocalStorage, but in these de-XPCOM + * days, we've moved to just directly reference the relevant concrete classes + * (ex: LSObject) directly. + * + * Note that testing methods are now also directly exposed on the Storage WebIDL + * interface for simplicity/sanity. + */ +class LocalStorageManager2 final : public nsIDOMStorageManager, + public nsILocalStorageManager { + public: + LocalStorageManager2(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMSTORAGEMANAGER + NS_DECL_NSILOCALSTORAGEMANAGER + + /** + * Helper to trigger an LSRequest and resolve/reject the provided promise when + * the result comes in. This routine is notable because the LSRequest + * mechanism is normally used synchronously from content, but here it's + * exposed asynchronously. + */ + LSRequestChild* StartRequest(const LSRequestParams& aParams, + LSRequestChildCallback* aCallback); + + private: + ~LocalStorageManager2(); + + /** + * Helper to trigger an LSSimpleRequst and resolve/reject the provided promise + * when the result comes in. + */ + nsresult StartSimpleRequest(Promise* aPromise, + const LSSimpleRequestParams& aParams); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_localstorage_LocalStorageManager2_h diff --git a/dom/localstorage/PBackgroundLSDatabase.ipdl b/dom/localstorage/PBackgroundLSDatabase.ipdl new file mode 100644 index 0000000000..9a4402c97e --- /dev/null +++ b/dom/localstorage/PBackgroundLSDatabase.ipdl @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; +include protocol PBackgroundLSSnapshot; + +include PBackgroundLSSharedTypes; + +include "mozilla/dom/localstorage/SerializationHelpers.h"; + +using mozilla::dom::LSSnapshot::LoadState + from "mozilla/dom/LSSnapshot.h"; + +namespace mozilla { +namespace dom { + +/** + * Initial LSSnapshot state as produced by Datastore::GetSnapshotLoadInfo. See + * `LSSnapshot::LoadState` for more details about the possible states and a + * high level overview. + */ +struct LSSnapshotInitInfo +{ + /** + * Boolean indicating whether the `key` provided as an argument to the + * PBackgroundLSSnapshot constructor did not exist in the Datastore and should + * be treated as an unknown and therefore undefined value. Note that `key` may + * have been provided as a void string, in which case this value is forced to + * be false. + */ + bool addKeyToUnknownItems; + + /** + * As many key/value or key/void pairs as the snapshot prefill byte budget + * allowed. + */ + LSItemInfo[] itemInfos; + + /** + * The total number of key/value pairs in LocalStorage for this origin at the + * time the snapshot was created. (And the point of the snapshot is to + * conceptually freeze the state of the Datastore in time, so this value does + * not change despite what other LSDatabase objects get up to in other + * processes.) + */ + uint32_t totalLength; + + /** + * The current amount of LocalStorage usage as measured by the summing the + * nsString Length() of both the key and the value over all stored pairs. + */ + int64_t usage; + + /** + * The amount of storage allowed to be used by the Snapshot without requesting + * more storage space via IncreasePeakUsage. This is the `usage` plus 0 or + * more bytes of space. If space was available, the increase will be the + * `minSize` from the PBackgroundLSSnapshot constructor plus the configured + * pre-increment (via "dom.storage.snapshot_peak_usage.initial_preincrement"). + * If the LocalStorage total usage was already close to the limit, then the + * fallback is either the `minSize` plus the configured reduced pre-increment + * (via "dom.storage.snapshot_peak_usage.reduced_initial_preincrement"), or + * `minSize`, or 0 depending on remaining available space. + */ + int64_t peakUsage; + + // See `LSSnapshot::LoadState` in `LSSnapshot.h` + LoadState loadState; + + /** + * Boolean indicating whether there where cross-process databases registered + * for this origin at the time the snapshot was created. + */ + bool hasOtherProcessDatabases; + + /** + * Boolean indicating whether there where cross-process observers registered + * for this origin at the time the snapshot was created. + */ + bool hasOtherProcessObservers; +}; + +/** + * This protocol is asynchronously created via constructor on PBackground but + * has synchronous semantics from the perspective of content on the main thread. + * The construction potentially involves waiting for disk I/O to load the + * LocalStorage data from disk as well as related QuotaManager checks, so async + * calls to PBackground are the only viable mechanism because blocking + * PBackground is not acceptable. (Note that an attempt is made to minimize any + * I/O latency by triggering preloading from + * ContentParent::AboutToLoadHttpFtpDocumentForChild, the central place + * for pre-loading.) + */ +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +sync protocol PBackgroundLSDatabase +{ + manager PBackground; + manages PBackgroundLSSnapshot; + +parent: + // The DeleteMe message is used to avoid a race condition between the parent + // actor and the child actor. The PBackgroundLSDatabase protocol could be + // simply destroyed by sending the __delete__ message from the child side. + // However, that would destroy the child actor immediatelly and the parent + // could be sending a message to the child at the same time resulting in a + // routing error since the child actor wouldn't exist anymore. A routing + // error typically causes a crash. The race can be prevented by doing the + // teardown in two steps. First, we send the DeleteMe message to the parent + // and the parent then sends the __delete__ message to the child. + async DeleteMe(); + + /** + * Sent in response to a `RequestAllowToClose` message once the snapshot + * cleanup has happened OR from LSDatabase's destructor if AllowToClose has + * not already been reported. + */ + async AllowToClose(); + + /** + * Invoked to create an LSSnapshot backed by a Snapshot in PBackground that + * presents an atomic and consistent view of the state of the authoritative + * Datastore state in the parent. + * + * This needs to be synchronous because LocalStorage's semantics are + * synchronous. Note that the Datastore in the PBackground parent already + * has the answers to this request immediately available without needing to + * consult any other threads or perform any I/O. Additionally, the response + * is explicitly bounded in size by the tunable snapshot prefill byte limit. + * + * @param key + * If key is non-void, then the snapshot is being triggered by a direct + * access to a localStorage key (get, set, or removal, with set/removal + * requiring the old value in order to properly populate the "storage" + * event), the key being requested. It's possible the key is not present in + * localStorage, in which case LSSnapshotInitInfo::addKeyToUnknownItems will + * be true indicating that there is no such key/value pair, otherwise it + * will be false. + * @param increasePeakUsage + * Whether the parent should increase initial peak uage of the Snapshot. + * See also the comment for LSSnapshotInitInfo::peakUsage above. + */ + sync PBackgroundLSSnapshot(nsString documentURI, + nsString key, + bool increasePeakUsage, + int64_t minSize) + returns (LSSnapshotInitInfo initInfo); + +child: + /** + * Only sent by the parent in response to the child's DeleteMe request. + */ + async __delete__(); + + /** + * Request to close the LSDatabase, checkpointing and finishing any + * outstanding snapshots so no state is lost. This request is issued when + * QuotaManager is shutting down or is aborting operations for an origin or + * process. Once the snapshot has cleaned up, AllowToClose will be sent to + * the parent. + * + * Note that the QuotaManager shutdown process is more likely to happen in + * unit tests where we explicitly reset the QuotaManager. At runtime, we + * expect windows to be closed and content processes terminated well before + * QuotaManager shutdown would actually occur. + * + * Also, Operations are usually aborted for an origin due to privacy API's + * clearing data for an origin. Operations are aborted for a process by + * ContentParent::ShutDownProcess. + */ + async RequestAllowToClose(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/PBackgroundLSObserver.ipdl b/dom/localstorage/PBackgroundLSObserver.ipdl new file mode 100644 index 0000000000..c800828da3 --- /dev/null +++ b/dom/localstorage/PBackgroundLSObserver.ipdl @@ -0,0 +1,63 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; + +include PBackgroundSharedTypes; + +include "mozilla/dom/localstorage/SerializationHelpers.h"; + +using mozilla::dom::LSValue + from "mozilla/dom/LSValue.h"; + +namespace mozilla { +namespace dom { + +/** + * The observer protocol sends "storage" event notifications for changes to + * LocalStorage that take place in other processes as their Snapshots are + * Checkpointed to the canonical Datastore in the parent process. Same-process + * notifications are generated as mutations happen. + * + * Note that mutations are never generated for redundant mutations. Setting the + * key "foo" to have value "bar" when it already has value "bar" will never + * result in a "storage" event. + */ +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +async protocol PBackgroundLSObserver +{ + manager PBackground; + +parent: + /** + * Sent by the LSObserver's destructor when it's going away. Any Observe + * messages received after this is sent will be ignored. Which is fine, + * because there should be nothing around left to hear. In the event a new + * page came into existence, its Observer creation will happen (effectively) + * synchronously. + */ + async DeleteMe(); + +child: + /** + * Only sent by the parent in response to a deletion request. + */ + async __delete__(); + + /** + * Sent by the parent process as Snapshots from other processes are + * Checkpointed, applying their mutations. The child actor currently directly + * shunts these to Storage::NotifyChange to generate "storage" events for + * immediate dispatch. + */ + async Observe(PrincipalInfo principalInfo, + uint32_t privateBrowsingId, + nsString documentURI, + nsString key, + LSValue oldValue, + LSValue newValue); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/PBackgroundLSRequest.ipdl b/dom/localstorage/PBackgroundLSRequest.ipdl new file mode 100644 index 0000000000..1ced0a9a17 --- /dev/null +++ b/dom/localstorage/PBackgroundLSRequest.ipdl @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; + +using struct mozilla::null_t from "mozilla/ipc/IPCCore.h"; + +namespace mozilla { +namespace dom { + +struct LSRequestPreloadDatastoreResponse +{ +}; + +struct LSRequestPrepareDatastoreResponse +{ + uint64_t datastoreId; +}; + +struct LSRequestPrepareObserverResponse +{ + uint64_t observerId; +}; + +/** + * Discriminated union which can contain an error code (`nsresult`) or + * particular request response. + */ +union LSRequestResponse +{ + nsresult; + LSRequestPreloadDatastoreResponse; + LSRequestPrepareDatastoreResponse; + LSRequestPrepareObserverResponse; +}; + +/** + * An asynchronous protocol for issuing requests that are used in a synchronous + * fashion by LocalStorage via LSObject's RequestHelper mechanism. This differs + * from LSSimpleRequest which is implemented and used asynchronously. + * + * See `PBackgroundLSSharedTypes.ipdlh` for more on the request types, the + * response types above for their corresponding responses, and `RequestHelper` + * for more on the usage and lifecycle of this mechanism. + */ +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +protocol PBackgroundLSRequest +{ + manager PBackground; + +parent: + // The Cancel message is used to avoid a possible dead lock caused by a CPOW + // sending a synchronous message from the main thread in the chrome process + // to the main thread in the content process at the time we are blocking + // the main thread in the content process to handle a request. + // We use the PBackground thread on the parent side to handle requests, but + // sometimes we need to get information from principals and that's currently + // only possible on the main thread. So if the main thread in the chrome + // process is blocked by a CPOW operation, our request must wait for the CPOW + // operation to complete. However the CPOW operation can't complete either + // because we are blocking the main thread in the content process. + // The dead lock is prevented by canceling our nested event loop in the + // content process when we receive a synchronous IPC message from the parent. + // + // Note that cancellation isn't instantaneous. It's just an asynchronous flow + // that definitely doesn't involve the main thread in the parent process, so + // we're guaranteed to unblock the main-thread in the content process and + // allow the sync IPC to make progress. When Cancel() is received by the + // parent, it will Send__delete__. The child will either send Cancel or + // Finish, but not both. + async Cancel(); + + /** + * Sent by the child in response to Ready, requesting that __delete__ be sent + * with the result. The child will either send Finish or Cancel, but not + * both. No further message will be sent from the child after invoking one. + */ + async Finish(); + +child: + /** + * The deletion is sent with the result of the request directly in response to + * either Cancel or Finish. + */ + async __delete__(LSRequestResponse response); + + /** + * Sent by the parent when it has completed whatever async stuff it needs to + * do and is ready to send the results. It then awaits the Finish() call to + * send the results. This may seem redundant, but it's not. If the + * __delete__ was sent directly, it's possible there could be a race where + * Cancel() would be received by the parent after it had already sent + * __delete__. (Which may no longer be fatal thanks to improvements to the + * IPC layer, but it would still lead to warnings, etc. And we don't + * expect PBackground to be highly contended nor the RemoteLazyInputStream + * thread.) + */ + async Ready(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/PBackgroundLSSharedTypes.ipdlh b/dom/localstorage/PBackgroundLSSharedTypes.ipdlh new file mode 100644 index 0000000000..d71b0e6111 --- /dev/null +++ b/dom/localstorage/PBackgroundLSSharedTypes.ipdlh @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include PBackgroundSharedTypes; +include ProtocolTypes; + +include "mozilla/dom/localstorage/SerializationHelpers.h"; + +using mozilla::dom::LSValue + from "mozilla/dom/LSValue.h"; + +namespace mozilla { +namespace dom { + +struct LSRequestCommonParams +{ + PrincipalInfo principalInfo; + PrincipalInfo storagePrincipalInfo; + nsCString originKey; +}; + +struct LSRequestPreloadDatastoreParams +{ + LSRequestCommonParams commonParams; +}; + +struct LSRequestPrepareDatastoreParams +{ + LSRequestCommonParams commonParams; + nsID? clientId; + PrincipalInfo? clientPrincipalInfo; +}; + +/** + * In order to validate the principal with the client, we need to provide an + * additional principalInfo for the client. The client is using the foreign + * principal, see StoragePrincipalHelper.h for details, which is different from + * the principalInfo. So, we need to pass the principalInfo from the client So + * that we can verify it with the given client Id. + * + * Note that the storagePrincipalInfo is used to access the right cookie jar + * according to the Storage Access. This is passed in order to access the + * correct local storage. Essentially, the storage principal and the client + * principal are using the PartitionKey in their OriginAttributes. But, the + * existence of the PartitionKey between them is depending on different + * conditions. Namely, the storage principal depends on the Storage Access but + * the client principal depends on whether it's in a third party. + */ +struct LSRequestPrepareObserverParams +{ + PrincipalInfo principalInfo; + PrincipalInfo storagePrincipalInfo; + nsID? clientId; + PrincipalInfo? clientPrincipalInfo; +}; + +union LSRequestParams +{ + LSRequestPreloadDatastoreParams; + LSRequestPrepareDatastoreParams; + LSRequestPrepareObserverParams; +}; + +struct LSSimpleRequestPreloadedParams +{ + PrincipalInfo principalInfo; + PrincipalInfo storagePrincipalInfo; +}; + +struct LSSimpleRequestGetStateParams +{ + PrincipalInfo principalInfo; + PrincipalInfo storagePrincipalInfo; +}; + +union LSSimpleRequestParams +{ + LSSimpleRequestPreloadedParams; + LSSimpleRequestGetStateParams; +}; + +/** + * LocalStorage key/value pair wire representations. `value` may be void in + * cases where there is a value but it is not being sent for memory/bandwidth + * conservation purposes. (It's not possible to have a null/undefined `value` + * as Storage is defined explicitly as a String store.) + */ +struct LSItemInfo +{ + nsString key; + LSValue value; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/PBackgroundLSSimpleRequest.ipdl b/dom/localstorage/PBackgroundLSSimpleRequest.ipdl new file mode 100644 index 0000000000..9a6a22d836 --- /dev/null +++ b/dom/localstorage/PBackgroundLSSimpleRequest.ipdl @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; + +include PBackgroundLSSharedTypes; + +namespace mozilla { +namespace dom { + +/** + * Response to a `LSSimpleRequestPreloadedParams` request indicating whether the + * origin was preloaded. + */ +struct LSSimpleRequestPreloadedResponse +{ + bool preloaded; +}; + +struct LSSimpleRequestGetStateResponse +{ + LSItemInfo[] itemInfos; +}; + +/** + * Discriminated union which can contain an error code (`nsresult`) or + * particular simple request response. + */ +union LSSimpleRequestResponse +{ + nsresult; + LSSimpleRequestPreloadedResponse; + LSSimpleRequestGetStateResponse; +}; + +/** + * Simple requests are async-only from both a protocol perspective and the + * manner in which they're used. In comparison, PBackgroundLSRequests are + * async only from a protocol perspective; they are used synchronously from the + * main thread via LSObject's RequestHelper mechanism. (With the caveat that + * nsILocalStorageManager does expose LSRequests asynchronously.) + * + * These requests use the common idiom where the arguments to the request are + * sent in the constructor and the result is sent in the __delete__ response. + * Request types are indicated by the Params variant used and those live in + * `PBackgroundLSSharedTypes.ipdlh`. + */ +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +protocol PBackgroundLSSimpleRequest +{ + manager PBackground; + +child: + async __delete__(LSSimpleRequestResponse response); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/PBackgroundLSSnapshot.ipdl b/dom/localstorage/PBackgroundLSSnapshot.ipdl new file mode 100644 index 0000000000..45c744c840 --- /dev/null +++ b/dom/localstorage/PBackgroundLSSnapshot.ipdl @@ -0,0 +1,153 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; +include protocol PBackgroundLSDatabase; + +include PBackgroundLSSharedTypes; + +include "mozilla/dom/localstorage/SerializationHelpers.h"; + +using mozilla::dom::LSValue + from "mozilla/dom/LSValue.h"; + +namespace mozilla { +namespace dom { + +struct LSSetItemInfo +{ + nsString key; + LSValue value; +}; + +struct LSRemoveItemInfo +{ + nsString key; +}; + +struct LSClearInfo +{ +}; + +/** + * Union of LocalStorage mutation types. + */ +union LSWriteInfo +{ + LSSetItemInfo; + LSRemoveItemInfo; + LSClearInfo; +}; + +struct LSSetItemAndNotifyInfo +{ + nsString key; + LSValue oldValue; + LSValue value; +}; + +struct LSRemoveItemAndNotifyInfo +{ + nsString key; + LSValue oldValue; +}; + +/** + * Union of LocalStorage mutation types. + */ +union LSWriteAndNotifyInfo +{ + LSSetItemAndNotifyInfo; + LSRemoveItemAndNotifyInfo; + LSClearInfo; +}; + +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +sync protocol PBackgroundLSSnapshot +{ + manager PBackgroundLSDatabase; + +parent: + async DeleteMe(); + + async AsyncCheckpoint(LSWriteInfo[] writeInfos); + + async AsyncCheckpointAndNotify(LSWriteAndNotifyInfo[] writeAndNotifyInfos); + + // A synchronous checkpoint. This should only be used by the snapshotting code + // to checkpoint an explicit snapshot. + sync SyncCheckpoint(LSWriteInfo[] writeInfos); + + // A synchronous checkpoint and notify. This should only be used by the + // snapshotting code to checkpoint and notify an explicit snapshot. + sync SyncCheckpointAndNotify(LSWriteAndNotifyInfo[] writeAndNotifyInfos); + + async AsyncFinish(); + + // A synchronous finish. This should only be used by the snapshotting code to + // end an explicit snapshot. + sync SyncFinish(); + + async Loaded(); + + /** + * Invoked on demand to load an item that didn't fit into the initial + * snapshot prefill and also some additional key/value pairs to lower down + * the need to use this synchronous message again. + * + * This needs to be synchronous because LocalStorage's semantics are + * synchronous. Note that the Snapshot in the PBackground parent already + * has the answers to this request immediately available without needing to + * consult any other threads or perform any I/O. + */ + sync LoadValueAndMoreItems(nsString key) + returns (LSValue value, LSItemInfo[] itemInfos); + + /** + * Invoked on demand to load all keys in in their canonical order if they + * didn't fit into the initial snapshot prefill. + * + * This needs to be synchronous because LocalStorage's semantics are + * synchronous. Note that the Snapshot in the PBackground parent already + * has the answers to this request immediately available without needing to + * consult any other threads or perform any I/O. + */ + sync LoadKeys() + returns (nsString[] keys); + + /** + * This needs to be synchronous because LocalStorage's semantics are + * synchronous. Note that the Snapshot in the PBackground parent typically + * doesn't need to consult any other threads or perform any I/O to handle + * this request. However, it has to call a quota manager method that can + * potentially do I/O directly on the PBackground thread. It can only happen + * rarely in a storage pressure (low storage space) situation. Specifically, + * after we get a list of origin directories for eviction, we will delete + * them directly on the PBackground thread. This doesn't cause any + * performance problems, but avoiding I/O completely might need to be done as + * a futher optimization. + */ + sync IncreasePeakUsage(int64_t minSize) + returns (int64_t size); + +child: + /** + * Compels the child LSSnapshot to Checkpoint() and Finish(), effectively + * compelling the snapshot to flush any issued mutations and close itself. + * The child LSSnapshot does that either immediately if it's just waiting + * to be reused or when it gets into a stable state. + * + * This message is expected to be sent in the following two cases only: + * 1. The state of the underlying Datastore starts to differ from the state + * captured at the time of snapshot creation. + * 2. The last private browsing context exits. And in that case we expect + * all private browsing globals to already have been destroyed. + */ + async MarkDirty(); + + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/ReportInternalError.cpp b/dom/localstorage/ReportInternalError.cpp new file mode 100644 index 0000000000..88fa4fd6e2 --- /dev/null +++ b/dom/localstorage/ReportInternalError.cpp @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ReportInternalError.h" + +#include <cinttypes> +#include "nsContentUtils.h" +#include "nsPrintfCString.h" +#include "nsString.h" + +namespace mozilla::dom::localstorage { + +void ReportInternalError(const char* aFile, uint32_t aLine, const char* aStr) { + // Get leaf of file path + for (const char* p = aFile; *p; ++p) { + if (*p == '/' && *(p + 1)) { + aFile = p + 1; + } + } + + nsContentUtils::LogSimpleConsoleError( + NS_ConvertUTF8toUTF16( + nsPrintfCString("LocalStorage %s: %s:%" PRIu32, aStr, aFile, aLine)), + "localstorage"_ns, false, + true /* Internal errors are chrome context only*/); +} + +} // namespace mozilla::dom::localstorage diff --git a/dom/localstorage/ReportInternalError.h b/dom/localstorage/ReportInternalError.h new file mode 100644 index 0000000000..9cfd1cd72a --- /dev/null +++ b/dom/localstorage/ReportInternalError.h @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_ReportInternalError_h +#define mozilla_dom_localstorage_ReportInternalError_h + +#include <cstdint> +#include "mozilla/Attributes.h" +#include "nsDebug.h" + +#define LS_WARNING(...) \ + do { \ + nsPrintfCString s(__VA_ARGS__); \ + mozilla::dom::localstorage::ReportInternalError(__FILE__, __LINE__, \ + s.get()); \ + NS_WARNING(s.get()); \ + } while (0) + +namespace mozilla::dom::localstorage { + +MOZ_COLD void ReportInternalError(const char* aFile, uint32_t aLine, + const char* aStr); + +} // namespace mozilla::dom::localstorage + +#endif // mozilla_dom_localstorage_ReportInternalError_h diff --git a/dom/localstorage/SerializationHelpers.h b/dom/localstorage/SerializationHelpers.h new file mode 100644 index 0000000000..f9b499671c --- /dev/null +++ b/dom/localstorage/SerializationHelpers.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_SerializationHelpers_h +#define mozilla_dom_localstorage_SerializationHelpers_h + +#include <string> +#include "chrome/common/ipc_message_utils.h" +#include "ipc/EnumSerializer.h" +#include "mozilla/dom/LSSnapshot.h" +#include "mozilla/dom/LSValue.h" + +namespace IPC { + +template <> +struct ParamTraits<mozilla::dom::LSSnapshot::LoadState> + : public ContiguousEnumSerializer< + mozilla::dom::LSSnapshot::LoadState, + mozilla::dom::LSSnapshot::LoadState::Initial, + mozilla::dom::LSSnapshot::LoadState::EndGuard> {}; + +template <> +struct ParamTraits<mozilla::dom::LSValue::CompressionType> + : public ContiguousEnumSerializer< + mozilla::dom::LSValue::CompressionType, + mozilla::dom::LSValue::CompressionType::UNCOMPRESSED, + mozilla::dom::LSValue::CompressionType::NUM_TYPES> {}; + +static_assert( + 0u == static_cast<uint8_t>(mozilla::dom::LSValue::ConversionType::NONE)); +template <> +struct ParamTraits<mozilla::dom::LSValue::ConversionType> + : public ContiguousEnumSerializer< + mozilla::dom::LSValue::ConversionType, + mozilla::dom::LSValue::ConversionType::NONE, + mozilla::dom::LSValue::ConversionType::NUM_TYPES> {}; + +template <> +struct ParamTraits<mozilla::dom::LSValue> { + typedef mozilla::dom::LSValue paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mBuffer); + WriteParam(aWriter, aParam.mUTF16Length); + WriteParam(aWriter, aParam.mConversionType); + WriteParam(aWriter, aParam.mCompressionType); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &aResult->mBuffer) && + ReadParam(aReader, &aResult->mUTF16Length) && + ReadParam(aReader, &aResult->mConversionType) && + ReadParam(aReader, &aResult->mCompressionType); + } +}; + +} // namespace IPC + +#endif // mozilla_dom_localstorage_SerializationHelpers_h diff --git a/dom/localstorage/SnappyUtils.cpp b/dom/localstorage/SnappyUtils.cpp new file mode 100644 index 0000000000..cf47421bc5 --- /dev/null +++ b/dom/localstorage/SnappyUtils.cpp @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SnappyUtils.h" + +#include <stddef.h> +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/fallible.h" +#include "nsDebug.h" +#include "nsString.h" +#include "snappy/snappy.h" + +namespace mozilla::dom { + +static_assert(SNAPPY_VERSION == 0x010109); + +bool SnappyCompress(const nsACString& aSource, nsACString& aDest) { + MOZ_ASSERT(!aSource.IsVoid()); + + size_t uncompressedLength = aSource.Length(); + + if (uncompressedLength <= 16) { + aDest.SetIsVoid(true); + return true; + } + + size_t compressedLength = snappy::MaxCompressedLength(uncompressedLength); + + if (NS_WARN_IF(!aDest.SetLength(compressedLength, fallible))) { + return false; + } + + snappy::RawCompress(aSource.BeginReading(), uncompressedLength, + aDest.BeginWriting(), &compressedLength); + + if (compressedLength >= uncompressedLength) { + aDest.SetIsVoid(true); + return true; + } + + if (NS_WARN_IF(!aDest.SetLength(compressedLength, fallible))) { + return false; + } + + return true; +} + +bool SnappyUncompress(const nsACString& aSource, nsACString& aDest) { + MOZ_ASSERT(!aSource.IsVoid()); + + const char* compressed = aSource.BeginReading(); + + auto compressedLength = static_cast<size_t>(aSource.Length()); + + size_t uncompressedLength = 0u; + if (!snappy::GetUncompressedLength(compressed, compressedLength, + &uncompressedLength)) { + return false; + } + + CheckedUint32 checkedLength(uncompressedLength); + if (!checkedLength.isValid()) { + return false; + } + + aDest.SetLength(checkedLength.value()); + + if (!snappy::RawUncompress(compressed, compressedLength, + aDest.BeginWriting())) { + return false; + } + + return true; +} + +} // namespace mozilla::dom diff --git a/dom/localstorage/SnappyUtils.h b/dom/localstorage/SnappyUtils.h new file mode 100644 index 0000000000..45c09a9465 --- /dev/null +++ b/dom/localstorage/SnappyUtils.h @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_localstorage_SnappyUtils_h +#define mozilla_dom_localstorage_SnappyUtils_h + +#include "nsStringFwd.h" + +namespace mozilla::dom { + +bool SnappyCompress(const nsACString& aSource, nsACString& aDest); + +bool SnappyUncompress(const nsACString& aSource, nsACString& aDest); + +} // namespace mozilla::dom + +#endif // mozilla_dom_localstorage_SnappyUtils_h diff --git a/dom/localstorage/moz.build b/dom/localstorage/moz.build new file mode 100644 index 0000000000..a7f5e0b359 --- /dev/null +++ b/dom/localstorage/moz.build @@ -0,0 +1,75 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Storage: localStorage & sessionStorage") + +MOCHITEST_MANIFESTS += ["test/mochitest.ini"] + +BROWSER_CHROME_MANIFESTS += ["test/browser.ini"] + +XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] + +TEST_HARNESS_FILES.xpcshell.dom.localstorage.test.unit += [ + "test/unit/databaseShadowing-shared.js", +] + +TEST_DIRS += ["test/gtest"] + +XPIDL_SOURCES += [ + "nsILocalStorageManager.idl", +] + +XPIDL_MODULE = "dom_localstorage" + +EXPORTS.mozilla.dom.localstorage += [ + "ActorsParent.h", + "SerializationHelpers.h", +] + +EXPORTS.mozilla.dom += [ + "LocalStorageCommon.h", + "LocalStorageManager2.h", + "LSObject.h", + "LSObserver.h", + "LSSnapshot.h", + "LSValue.h", + "LSWriteOptimizer.h", + "LSWriteOptimizerImpl.h", + "SnappyUtils.h", +] + +UNIFIED_SOURCES += [ + "ActorsChild.cpp", + "ActorsParent.cpp", + "LocalStorageCommon.cpp", + "LocalStorageManager2.cpp", + "LSDatabase.cpp", + "LSObject.cpp", + "LSObserver.cpp", + "LSSnapshot.cpp", + "LSValue.cpp", + "LSWriteOptimizer.cpp", + "ReportInternalError.cpp", + "SnappyUtils.cpp", +] + +IPDL_SOURCES += [ + "PBackgroundLSDatabase.ipdl", + "PBackgroundLSObserver.ipdl", + "PBackgroundLSRequest.ipdl", + "PBackgroundLSSharedTypes.ipdlh", + "PBackgroundLSSimpleRequest.ipdl", + "PBackgroundLSSnapshot.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/dom/file/ipc", +] diff --git a/dom/localstorage/nsILocalStorageManager.idl b/dom/localstorage/nsILocalStorageManager.idl new file mode 100644 index 0000000000..0e8db22482 --- /dev/null +++ b/dom/localstorage/nsILocalStorageManager.idl @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIPrincipal; + +/** + * Methods specific to LocalStorage, see nsIDOMStorageManager for methods shared + * with SessionStorage. Methods may migrate there as SessionStorage is + * overhauled. + */ +[scriptable, builtinclass, uuid(d4f534da-2744-4db3-8774-8b187c64ade9)] +interface nsILocalStorageManager : nsISupports +{ + readonly attribute boolean nextGenLocalStorageEnabled; + + /** + * Trigger preload of LocalStorage for the given principal. For use by + * ContentParent::AboutToLoadHttpFtpDocumentForChild to maximize the + * amount of time we have to load the data off disk before the page might + * attempt to touch LocalStorage. + * + * This method will not create a QuotaManager-managed directory on disk if + * one does not already exist for the principal. + */ + [implicit_jscontext] + Promise preload(in nsIPrincipal aPrincipal); + + [implicit_jscontext] + Promise isPreloaded(in nsIPrincipal aPrincipal); + + [implicit_jscontext] + Promise getState(in nsIPrincipal aPrincipal); +}; diff --git a/dom/localstorage/test/browser.ini b/dom/localstorage/test/browser.ini new file mode 100644 index 0000000000..8119342ecb --- /dev/null +++ b/dom/localstorage/test/browser.ini @@ -0,0 +1,6 @@ +[DEFAULT] +skip-if = (buildapp != "browser") +support-files = + page_private_ls.html + +[browser_private_ls.js] diff --git a/dom/localstorage/test/browser_private_ls.js b/dom/localstorage/test/browser_private_ls.js new file mode 100644 index 0000000000..d4708c8e23 --- /dev/null +++ b/dom/localstorage/test/browser_private_ls.js @@ -0,0 +1,44 @@ +/** + * This test is mainly to verify that datastores are cleared when the last + * private browsing context exited. + */ + +async function lsCheckFunc() { + let storage = content.localStorage; + + if (storage.length) { + return false; + } + + // Store non-ASCII value to verify bug 1552428. + storage.setItem("foo", "úžasné"); + + return true; +} + +function checkTabWindowLS(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], lsCheckFunc); +} + +add_task(async function() { + const pageUrl = + "http://example.com/browser/dom/localstorage/test/page_private_ls.html"; + + for (let i = 0; i < 2; i++) { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let privateTab = await BrowserTestUtils.openNewForegroundTab( + privateWin.gBrowser, + pageUrl + ); + + ok( + await checkTabWindowLS(privateTab), + "LS works correctly in a private-browsing page." + ); + + await BrowserTestUtils.closeWindow(privateWin); + } +}); diff --git a/dom/localstorage/test/gtest/TestLocalStorage.cpp b/dom/localstorage/test/gtest/TestLocalStorage.cpp new file mode 100644 index 0000000000..6c5621f248 --- /dev/null +++ b/dom/localstorage/test/gtest/TestLocalStorage.cpp @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/dom/LocalStorageCommon.h" +#include "mozilla/dom/StorageUtils.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIURI.h" +#include "nsNetUtil.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::dom::StorageUtils; +using namespace mozilla::ipc; + +namespace { + +struct OriginKeyTest { + const char* mSpec; + const char* mOriginKey; + const char* mQuotaKey; +}; + +already_AddRefed<nsIPrincipal> GetContentPrincipal(const char* aSpec) { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), nsDependentCString(aSpec)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + OriginAttributes attrs; + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(uri, attrs); + + return principal.forget(); +} + +void CheckGeneratedOriginKey(nsIPrincipal* aPrincipal, const char* aOriginKey, + const char* aQuotaKey) { + nsCString originAttrSuffix; + nsCString originKey; + nsCString quotaKey; + + aPrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix); + + nsresult rv = aPrincipal->GetStorageOriginKey(originKey); + if (aOriginKey) { + ASSERT_EQ(rv, NS_OK) << "GetStorageOriginKey should not fail"; + EXPECT_TRUE(originKey == nsDependentCString(aOriginKey)); + } else { + ASSERT_NE(rv, NS_OK) << "GetStorageOriginKey should fail"; + } + + rv = aPrincipal->GetLocalStorageQuotaKey(quotaKey); + if (aQuotaKey) { + ASSERT_EQ(rv, NS_OK) << "GetLocalStorageQuotaKey should not fail"; + EXPECT_TRUE(quotaKey == nsDependentCString(aQuotaKey)); + } else { + ASSERT_NE(rv, NS_OK) << "GetLocalStorageQuotaKey should fail"; + } + + PrincipalInfo principalInfo; + rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo); + ASSERT_EQ(rv, NS_OK) << "PrincipalToPrincipalInfo should not fail"; + + const auto res = GenerateOriginKey2(principalInfo); + if (aOriginKey) { + ASSERT_TRUE(res.isOk()) + << "GenerateOriginKey2 should not fail"; + EXPECT_TRUE(res.inspect().second == nsDependentCString(aOriginKey)); + } else { + ASSERT_TRUE(res.isErr()) + << "GenerateOriginKey2 should fail"; + } +} + +} // namespace + +TEST(LocalStorage, OriginKey) +{ + // Check the system principal. + nsCOMPtr<nsIScriptSecurityManager> secMan = + nsContentUtils::GetSecurityManager(); + ASSERT_TRUE(secMan) + << "GetSecurityManager() should not fail"; + + nsCOMPtr<nsIPrincipal> principal; + secMan->GetSystemPrincipal(getter_AddRefs(principal)); + ASSERT_TRUE(principal) + << "GetSystemPrincipal() should not fail"; + + CheckGeneratedOriginKey(principal, nullptr, nullptr); + + // Check the null principal. + principal = NullPrincipal::CreateWithoutOriginAttributes(); + ASSERT_TRUE(principal) + << "CreateWithoutOriginAttributes() should not fail"; + + CheckGeneratedOriginKey(principal, nullptr, nullptr); + + // Check content principals. + static const OriginKeyTest tests[] = { + {"http://localhost", "tsohlacol.:http:80", ":tsohlacol."}, + {"http://www.mozilla.org", "gro.allizom.www.:http:80", ":gro.allizom."}, + {"https://www.mozilla.org", "gro.allizom.www.:https:443", + ":gro.allizom."}, + {"http://www.mozilla.org:32400", "gro.allizom.www.:http:32400", + ":gro.allizom."}, + {"file:///Users/Joe/Sites/", "/setiS/eoJ/sresU/.:file", + ":/setiS/eoJ/sresU/."}, + {"file:///Users/Joe/Sites/#foo", "/setiS/eoJ/sresU/.:file", + ":/setiS/eoJ/sresU/."}, + {"file:///Users/Joe/Sites/?foo", "/setiS/eoJ/sresU/.:file", + ":/setiS/eoJ/sresU/."}, + {"file:///Users/Joe/Sites", "/eoJ/sresU/.:file", ":/eoJ/sresU/."}, + {"file:///Users/Joe/Sites#foo", "/eoJ/sresU/.:file", ":/eoJ/sresU/."}, + {"file:///Users/Joe/Sites?foo", "/eoJ/sresU/.:file", ":/eoJ/sresU/."}, + {"moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc/" + "_generated_background_page.html", + "cb0c762e20f1-1769-247e-de56-f8a11735.:moz-extension", + ":cb0c762e20f1-1769-247e-de56-f8a11735."}, + {"http://[::1]:8/test.html", "1::.:http:8", ":1::."}, + }; + + for (const auto& test : tests) { + principal = GetContentPrincipal(test.mSpec); + ASSERT_TRUE(principal) + << "GetContentPrincipal() should not fail"; + + CheckGeneratedOriginKey(principal, test.mOriginKey, test.mQuotaKey); + } +} diff --git a/dom/localstorage/test/gtest/moz.build b/dom/localstorage/test/gtest/moz.build new file mode 100644 index 0000000000..f9d3c7bb0b --- /dev/null +++ b/dom/localstorage/test/gtest/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES = [ + "TestLocalStorage.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += [ + "/dom/localstorage", +] diff --git a/dom/localstorage/test/helpers.js b/dom/localstorage/test/helpers.js new file mode 100644 index 0000000000..d668042343 --- /dev/null +++ b/dom/localstorage/test/helpers.js @@ -0,0 +1,78 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// testSteps is expected to be defined by the test using this file. +/* global testSteps:false */ + +function executeSoon(aFun) { + SpecialPowers.Services.tm.dispatchToMainThread({ + run() { + aFun(); + }, + }); +} + +function clearAllDatabases() { + let qms = SpecialPowers.Services.qms; + let principal = SpecialPowers.wrap(document).nodePrincipal; + let request = qms.clearStoragesForPrincipal(principal); + return request; +} + +if (!window.runTest) { + window.runTest = async function() { + SimpleTest.waitForExplicitFinish(); + + info("Pushing preferences"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage.testing", true], + ["dom.quotaManager.testing", true], + ], + }); + + info("Clearing old databases"); + + await requestFinished(clearAllDatabases()); + + SimpleTest.registerCleanupFunction(async function() { + await requestFinished(clearAllDatabases()); + }); + }; +} + +function returnToEventLoop() { + return new Promise(function(resolve) { + executeSoon(resolve); + }); +} + +function getLocalStorage() { + return localStorage; +} + +class RequestError extends Error { + constructor(resultCode, resultName) { + super(`Request failed (code: ${resultCode}, name: ${resultName})`); + this.name = "RequestError"; + this.resultCode = resultCode; + this.resultName = resultName; + } +} + +async function requestFinished(request) { + await new Promise(function(resolve) { + request.callback = SpecialPowers.wrapCallback(function() { + resolve(); + }); + }); + + if (request.resultCode !== SpecialPowers.Cr.NS_OK) { + throw new RequestError(request.resultCode, request.resultName); + } + + return request.result; +} diff --git a/dom/localstorage/test/mochitest.ini b/dom/localstorage/test/mochitest.ini new file mode 100644 index 0000000000..500bdd0a1c --- /dev/null +++ b/dom/localstorage/test/mochitest.ini @@ -0,0 +1,10 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +[DEFAULT] +support-files = + helpers.js + unit/test_largeItems.js + +[test_largeItems.html] diff --git a/dom/localstorage/test/page_private_ls.html b/dom/localstorage/test/page_private_ls.html new file mode 100644 index 0000000000..795a814981 --- /dev/null +++ b/dom/localstorage/test/page_private_ls.html @@ -0,0 +1,9 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + All the interesting stuff happens in ContentTask.spawn() calls. +</body> +</html> diff --git a/dom/localstorage/test/test_largeItems.html b/dom/localstorage/test/test_largeItems.html new file mode 100644 index 0000000000..92316085fc --- /dev/null +++ b/dom/localstorage/test/test_largeItems.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Large Items Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_largeItems.js"></script> + <script type="text/javascript" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/localstorage/test/unit/archive_profile.zip b/dom/localstorage/test/unit/archive_profile.zip Binary files differnew file mode 100644 index 0000000000..71b2d1e5f9 --- /dev/null +++ b/dom/localstorage/test/unit/archive_profile.zip diff --git a/dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zip b/dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zip Binary files differnew file mode 100644 index 0000000000..8cfa6e3d43 --- /dev/null +++ b/dom/localstorage/test/unit/corruptedDatabase_missingUsageFile_profile.zip diff --git a/dom/localstorage/test/unit/corruptedDatabase_profile.zip b/dom/localstorage/test/unit/corruptedDatabase_profile.zip Binary files differnew file mode 100644 index 0000000000..2f60db2a45 --- /dev/null +++ b/dom/localstorage/test/unit/corruptedDatabase_profile.zip diff --git a/dom/localstorage/test/unit/databaseShadowing-shared.js b/dom/localstorage/test/unit/databaseShadowing-shared.js new file mode 100644 index 0000000000..ffee8579cb --- /dev/null +++ b/dom/localstorage/test/unit/databaseShadowing-shared.js @@ -0,0 +1,130 @@ +/* import-globals-from head.js */ + +const principalInfos = [ + { url: "http://example.com", attrs: {} }, + + { url: "http://origin.test", attrs: {} }, + + { url: "http://prefix.test", attrs: {} }, + { url: "http://prefix.test", attrs: { userContextId: 10 } }, + + { url: "http://pattern.test", attrs: { userContextId: 15 } }, + { url: "http://pattern.test:8080", attrs: { userContextId: 15 } }, + { url: "https://pattern.test", attrs: { userContextId: 15 } }, +]; + +const surrogate = String.fromCharCode(0xdc00); +const replacement = String.fromCharCode(0xfffd); +const beginning = "beginning"; +const ending = "ending"; +const complexValue = beginning + surrogate + surrogate + ending; +const corruptedValue = beginning + replacement + replacement + ending; + +function enableNextGenLocalStorage() { + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); +} + +function disableNextGenLocalStorage() { + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + true + ); +} + +function storeData() { + for (let i = 0; i < principalInfos.length; i++) { + let principalInfo = principalInfos[i]; + let principal = getPrincipal(principalInfo.url, principalInfo.attrs); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Adding data"); + + storage.setItem("key0", "value0"); + storage.clear(); + storage.setItem("key1", "value1"); + storage.removeItem("key1"); + storage.setItem("key2", "value2"); + storage.setItem("complexKey", complexValue); + + info("Closing storage"); + + storage.close(); + } +} + +function exportShadowDatabase(name) { + info("Verifying shadow database"); + + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + let shadowDatabase = profileDir.clone(); + shadowDatabase.append("webappsstore.sqlite"); + + let exists = shadowDatabase.exists(); + ok(exists, "Shadow database does exist"); + + info("Copying shadow database"); + + let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + shadowDatabase.copyTo(currentDir, name); +} + +function importShadowDatabase(name) { + info("Verifying shadow database"); + + let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + let shadowDatabase = currentDir.clone(); + shadowDatabase.append(name); + + let exists = shadowDatabase.exists(); + if (!exists) { + return false; + } + + info("Copying shadow database"); + + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + shadowDatabase.copyTo(profileDir, "webappsstore.sqlite"); + + return true; +} + +function verifyData(clearedOrigins, migrated = false) { + for (let i = 0; i < principalInfos.length; i++) { + let principalInfo = principalInfos[i]; + let principal = getPrincipal(principalInfo.url, principalInfo.attrs); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Verifying data"); + + if (clearedOrigins.includes(i)) { + ok(storage.getItem("key2") == null, "Correct value"); + ok(storage.getItem("complexKey") == null, "Correct value"); + } else { + ok(storage.getItem("key0") == null, "Correct value"); + ok(storage.getItem("key1") == null, "Correct value"); + is(storage.getItem("key2"), "value2", "Correct value"); + is( + storage.getItem("complexKey"), + migrated ? corruptedValue : complexValue, + "Correct value" + ); + } + + info("Closing storage"); + + storage.close(); + } +} diff --git a/dom/localstorage/test/unit/groupMismatch_profile.zip b/dom/localstorage/test/unit/groupMismatch_profile.zip Binary files differnew file mode 100644 index 0000000000..182b013de0 --- /dev/null +++ b/dom/localstorage/test/unit/groupMismatch_profile.zip diff --git a/dom/localstorage/test/unit/head.js b/dom/localstorage/test/unit/head.js new file mode 100644 index 0000000000..eaca1ed173 --- /dev/null +++ b/dom/localstorage/test/unit/head.js @@ -0,0 +1,332 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests are expected to define testSteps. +/* globals testSteps */ + +const NS_ERROR_DOM_QUOTA_EXCEEDED_ERR = 22; + +function is(a, b, msg) { + Assert.equal(a, b, msg); +} + +function ok(cond, msg) { + Assert.ok(!!cond, msg); +} + +add_setup(function() { + do_get_profile(); + + enableTesting(); + + Cu.importGlobalProperties(["crypto"]); + + registerCleanupFunction(resetTesting); +}); + +function returnToEventLoop() { + return new Promise(function(resolve) { + executeSoon(resolve); + }); +} + +function enableTesting() { + Services.prefs.setBoolPref("dom.simpleDB.enabled", true); + Services.prefs.setBoolPref("dom.storage.testing", true); + + // xpcshell globals don't have associated clients in the Clients API sense, so + // we need to disable client validation so that the unit tests are allowed to + // use LocalStorage. + Services.prefs.setBoolPref("dom.storage.client_validation", false); + + Services.prefs.setBoolPref("dom.quotaManager.testing", true); +} + +function resetTesting() { + Services.prefs.clearUserPref("dom.quotaManager.testing"); + Services.prefs.clearUserPref("dom.storage.client_validation"); + Services.prefs.clearUserPref("dom.storage.testing"); + Services.prefs.clearUserPref("dom.simpleDB.enabled"); +} + +function setGlobalLimit(globalLimit) { + Services.prefs.setIntPref( + "dom.quotaManager.temporaryStorage.fixedLimit", + globalLimit + ); +} + +function resetGlobalLimit() { + Services.prefs.clearUserPref("dom.quotaManager.temporaryStorage.fixedLimit"); +} + +function setOriginLimit(originLimit) { + Services.prefs.setIntPref("dom.storage.default_quota", originLimit); +} + +function resetOriginLimit() { + Services.prefs.clearUserPref("dom.storage.default_quota"); +} + +function setTimeout(callback, timeout) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + timer.initWithCallback( + { + notify() { + callback(); + }, + }, + timeout, + Ci.nsITimer.TYPE_ONE_SHOT + ); + + return timer; +} + +function initStorage() { + return Services.qms.init(); +} + +function initTemporaryStorage() { + return Services.qms.initTemporaryStorage(); +} + +function initPersistentOrigin(principal) { + return Services.qms.initializePersistentOrigin(principal); +} + +function initTemporaryOrigin(persistence, principal) { + return Services.qms.initializeTemporaryOrigin(persistence, principal); +} + +function getOriginUsage(principal, fromMemory = false) { + let request = Services.qms.getUsageForPrincipal( + principal, + function() {}, + fromMemory + ); + + return request; +} + +function clear() { + let request = Services.qms.clear(); + + return request; +} + +function clearOriginsByPattern(pattern) { + let request = Services.qms.clearStoragesForOriginAttributesPattern(pattern); + + return request; +} + +function clearOriginsByPrefix(principal, persistence) { + let request = Services.qms.clearStoragesForPrincipal( + principal, + persistence, + null, + true + ); + + return request; +} + +function clearOrigin(principal, persistence) { + let request = Services.qms.clearStoragesForPrincipal(principal, persistence); + + return request; +} + +function reset() { + let request = Services.qms.reset(); + + return request; +} + +function resetOrigin(principal) { + let request = Services.qms.resetStoragesForPrincipal( + principal, + "default", + "ls" + ); + + return request; +} + +function installPackage(packageName) { + let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + let packageFile = currentDir.clone(); + packageFile.append(packageName + ".zip"); + + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipReader.open(packageFile); + + let entryNames = []; + let entries = zipReader.findEntries(null); + while (entries.hasMore()) { + let entry = entries.getNext(); + entryNames.push(entry); + } + entryNames.sort(); + + for (let entryName of entryNames) { + let zipentry = zipReader.getEntry(entryName); + + let file = getRelativeFile(entryName); + + if (zipentry.isDirectory) { + file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } else { + let istream = zipReader.getInputStream(entryName); + + var ostream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + ostream.init(file, -1, parseInt("0644", 8), 0); + + let bostream = Cc[ + "@mozilla.org/network/buffered-output-stream;1" + ].createInstance(Ci.nsIBufferedOutputStream); + bostream.init(ostream, 32768); + + bostream.writeFrom(istream, istream.available()); + + istream.close(); + bostream.close(); + } + } + + zipReader.close(); +} + +function getProfileDir() { + return Services.dirsvc.get("ProfD", Ci.nsIFile); +} + +// Given a "/"-delimited path relative to the profile directory, +// return an nsIFile representing the path. This does not test +// for the existence of the file or parent directories. +// It is safe even on Windows where the directory separator is not "/", +// but make sure you're not passing in a "\"-delimited path. +function getRelativeFile(relativePath) { + let profileDir = getProfileDir(); + + let file = profileDir.clone(); + relativePath.split("/").forEach(function(component) { + file.append(component); + }); + + return file; +} + +function repeatChar(count, ch) { + if (count == 0) { + return ""; + } + + let result = ch; + let count2 = count / 2; + + // Double the input until it is long enough. + while (result.length <= count2) { + result += result; + } + + // Use substring to hit the precise length target without using extra memory. + return result + result.substring(0, count - result.length); +} + +function getPrincipal(url, attrs) { + let uri = Services.io.newURI(url); + if (!attrs) { + attrs = {}; + } + return Services.scriptSecurityManager.createContentPrincipal(uri, attrs); +} + +function getCurrentPrincipal() { + return Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal); +} + +function getDefaultPrincipal() { + return getPrincipal("http://example.com"); +} + +function getSimpleDatabase(principal, persistence) { + let connection = Cc["@mozilla.org/dom/sdb-connection;1"].createInstance( + Ci.nsISDBConnection + ); + + if (!principal) { + principal = getDefaultPrincipal(); + } + + connection.init(principal, persistence); + + return connection; +} + +function getLocalStorage(principal) { + if (!principal) { + principal = getDefaultPrincipal(); + } + + return Services.domStorageManager.createStorage( + null, + principal, + principal, + "" + ); +} + +class RequestError extends Error { + constructor(resultCode, resultName) { + super(`Request failed (code: ${resultCode}, name: ${resultName})`); + this.name = "RequestError"; + this.resultCode = resultCode; + this.resultName = resultName; + } +} + +async function requestFinished(request) { + await new Promise(function(resolve) { + request.callback = function() { + resolve(); + }; + }); + + if (request.resultCode !== Cr.NS_OK) { + throw new RequestError(request.resultCode, request.resultName); + } + + return request.result; +} + +function loadSubscript(path) { + let file = do_get_file(path, false); + let uri = Services.io.newFileURI(file); + Services.scriptloader.loadSubScript(uri.spec); +} + +async function readUsageFromUsageFile(usageFile) { + let file = await File.createFromNsIFile(usageFile); + + let buffer = await new Promise(resolve => { + let reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsArrayBuffer(file); + }); + + // Manually getting the lower 32-bits because of the lack of support for + // 64-bit values currently from DataView/JS (other than the BigInt support + // that's currently behind a flag). + let view = new DataView(buffer, 8, 4); + return view.getUint32(); +} diff --git a/dom/localstorage/test/unit/make_migration_emptyValue.js b/dom/localstorage/test/unit/make_migration_emptyValue.js new file mode 100644 index 0000000000..12484ad7f3 --- /dev/null +++ b/dom/localstorage/test/unit/make_migration_emptyValue.js @@ -0,0 +1,23 @@ +/* + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +async function testSteps() { + const data = { + key: "foo", + value: "", + }; + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", false); + + info("Getting storage"); + + const storage = getLocalStorage(); + + info("Adding data"); + + storage.setItem(data.key, data.value); +} diff --git a/dom/localstorage/test/unit/migration_emptyValue_profile.zip b/dom/localstorage/test/unit/migration_emptyValue_profile.zip Binary files differnew file mode 100644 index 0000000000..b829beae77 --- /dev/null +++ b/dom/localstorage/test/unit/migration_emptyValue_profile.zip diff --git a/dom/localstorage/test/unit/migration_profile.zip b/dom/localstorage/test/unit/migration_profile.zip Binary files differnew file mode 100644 index 0000000000..19dc3d4805 --- /dev/null +++ b/dom/localstorage/test/unit/migration_profile.zip diff --git a/dom/localstorage/test/unit/schema3upgrade_profile.zip b/dom/localstorage/test/unit/schema3upgrade_profile.zip Binary files differnew file mode 100644 index 0000000000..1ee9bfbf2e --- /dev/null +++ b/dom/localstorage/test/unit/schema3upgrade_profile.zip diff --git a/dom/localstorage/test/unit/schema4upgrade_profile.zip b/dom/localstorage/test/unit/schema4upgrade_profile.zip Binary files differnew file mode 100644 index 0000000000..ae8ba09606 --- /dev/null +++ b/dom/localstorage/test/unit/schema4upgrade_profile.zip diff --git a/dom/localstorage/test/unit/stringLength2_profile.zip b/dom/localstorage/test/unit/stringLength2_profile.zip Binary files differnew file mode 100644 index 0000000000..de4d0fc3aa --- /dev/null +++ b/dom/localstorage/test/unit/stringLength2_profile.zip diff --git a/dom/localstorage/test/unit/stringLength_profile.zip b/dom/localstorage/test/unit/stringLength_profile.zip Binary files differnew file mode 100644 index 0000000000..6cac890860 --- /dev/null +++ b/dom/localstorage/test/unit/stringLength_profile.zip diff --git a/dom/localstorage/test/unit/test_archive.js b/dom/localstorage/test/unit/test_archive.js new file mode 100644 index 0000000000..51af78db1b --- /dev/null +++ b/dom/localstorage/test/unit/test_archive.js @@ -0,0 +1,78 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const lsArchiveFile = "storage/ls-archive.sqlite"; + + const principalInfo = { + url: "http://example.com", + attrs: {}, + }; + + function checkStorage() { + let principal = getPrincipal(principalInfo.url, principalInfo.attrs); + let storage = getLocalStorage(principal); + try { + storage.open(); + ok(true, "Did not throw"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + } + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + + info("Sub test case 1 - Archive file is a directory."); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + let archiveFile = getRelativeFile(lsArchiveFile); + + archiveFile.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + + checkStorage(); + + info("Sub test case 2 - Corrupted archive file."); + + info("Clearing"); + + request = clear(); + await requestFinished(request); + + let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init(archiveFile, -1, parseInt("0644", 8), 0); + ostream.write("foobar", 6); + ostream.close(); + + checkStorage(); + + info("Sub test case 3 - Nonupdateable archive file."); + + info("Clearing"); + + request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and storage/ls-archive.sqlite + // storage/ls-archive.sqlite was taken from FF 54 to force an upgrade. + // There's just one record in the webappsstore2 table. The record was + // modified by renaming the origin attribute userContextId to userContextKey. + // This triggers an error during the upgrade. + installPackage("archive_profile"); + + let fileSize = archiveFile.fileSize; + ok(fileSize > 0, "archive file size is greater than zero"); + + checkStorage(); +}); diff --git a/dom/localstorage/test/unit/test_clientValidation.js b/dom/localstorage/test/unit/test_clientValidation.js new file mode 100644 index 0000000000..26dc2bfdf7 --- /dev/null +++ b/dom/localstorage/test/unit/test_clientValidation.js @@ -0,0 +1,32 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Because this is an xpcshell global, it does not have an associated client id. + * We turn on client validation for LocalStorage and ensure that we don't have + * access to LocalStorage. + */ +add_task(async function testSteps() { + const principal = getPrincipal("http://example.com"); + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + Services.prefs.setBoolPref("dom.storage.client_validation", true); + + info("Getting storage"); + + try { + getLocalStorage(principal); + ok(false, "Should have thrown"); + } catch (ex) { + ok(true, "Did throw"); + is(ex.name, "NS_ERROR_FAILURE", "Threw right Exception"); + is(ex.result, Cr.NS_ERROR_FAILURE, "Threw with right result"); + } +}); diff --git a/dom/localstorage/test/unit/test_corruptedDatabase.js b/dom/localstorage/test/unit/test_corruptedDatabase.js new file mode 100644 index 0000000000..da73bb92f6 --- /dev/null +++ b/dom/localstorage/test/unit/test_corruptedDatabase.js @@ -0,0 +1,73 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function doTest(profile) { + info("Testing profile " + profile); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + installPackage(profile); + + const principal = getPrincipal("http://example.org"); + + let storage = getLocalStorage(principal); + + let length = storage.length; + + ok(length === 0, "Correct length"); + + info("Resetting origin"); + + request = resetOrigin(principal); + await requestFinished(request); + + info("Getting usage"); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, 0, "Correct usage"); +} + +add_task(async function testSteps() { + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + // XXX This should be refactored into separate sub test cases. + + const profiles = [ + // This profile contains one localStorage, all localStorage related files, a + // script for localStorage creation and the storage database: + // - storage/default/http+++example.org/ls + // - storage/ls-archive.sqlite + // - create_db.js + // - storage.sqlite + // - webappsstore.sqlite + // The file create_db.js in the package was run locally, specifically it was + // temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Manually change first 6 chars in data.sqlite to "foobar". + // 2. Remove the folder "storage/temporary". + "corruptedDatabase_profile", + // This profile is the same as corruptedDatabase_profile, except that the usage + // file (storage/default/http+++example.org/ls/usage) is missing. + "corruptedDatabase_missingUsageFile_profile", + ]; + + for (const profile of profiles) { + await doTest(profile); + } +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing1.js b/dom/localstorage/test/unit/test_databaseShadowing1.js new file mode 100644 index 0000000000..8582f434fc --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing1.js @@ -0,0 +1,23 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + enableNextGenLocalStorage(); + + storeData(); + + verifyData([]); + + // Wait for all database connections to close. + let request = reset(); + await requestFinished(request); + + exportShadowDatabase("shadowdb.sqlite"); + + // The shadow database is now prepared for test_databaseShadowing2.js +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing2.js b/dom/localstorage/test/unit/test_databaseShadowing2.js new file mode 100644 index 0000000000..f7b0ddb1a2 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing2.js @@ -0,0 +1,17 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + // The shadow database was prepared in test_databaseShadowing1.js + + disableNextGenLocalStorage(); + + ok(importShadowDatabase("shadowdb.sqlite"), "Import succeeded"); + + verifyData([], /* migrated */ true); +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js new file mode 100644 index 0000000000..d88fde52e5 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js @@ -0,0 +1,30 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + enableNextGenLocalStorage(); + + storeData(); + + verifyData([]); + + let principal = getPrincipal("http://origin.test", {}); + let request = clearOrigin(principal, "default"); + await requestFinished(request); + + verifyData([1]); + + // Wait for all database connections to close. + request = reset(); + await requestFinished(request); + + exportShadowDatabase("shadowdb-clearedOrigin.sqlite"); + + // The shadow database is now prepared for + // test_databaseShadowing_clearOrigin2.js +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js new file mode 100644 index 0000000000..83d792b496 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js @@ -0,0 +1,17 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + // The shadow database was prepared in test_databaseShadowing_clearOrigin1.js + + disableNextGenLocalStorage(); + + ok(importShadowDatabase("shadowdb-clearedOrigin.sqlite"), "Import succeeded"); + + verifyData([1], /* migrated */ true); +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js new file mode 100644 index 0000000000..70367bbeff --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js @@ -0,0 +1,29 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + enableNextGenLocalStorage(); + + storeData(); + + verifyData([]); + + let request = clearOriginsByPattern(JSON.stringify({ userContextId: 15 })); + await requestFinished(request); + + verifyData([4, 5, 6]); + + // Wait for all database connections to close. + request = reset(); + await requestFinished(request); + + exportShadowDatabase("shadowdb-clearedOriginsByPattern.sqlite"); + + // The shadow database is now prepared for + // test_databaseShadowing_clearOriginsByPattern2.js +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js new file mode 100644 index 0000000000..6c4d794d04 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js @@ -0,0 +1,21 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + // The shadow database was prepared in + // test_databaseShadowing_clearOriginsByPattern1.js + + disableNextGenLocalStorage(); + + ok( + importShadowDatabase("shadowdb-clearedOriginsByPattern.sqlite"), + "Import succeeded" + ); + + verifyData([4, 5, 6], /* migrated */ true); +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js new file mode 100644 index 0000000000..2b605e953f --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js @@ -0,0 +1,28 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + enableNextGenLocalStorage(); + + storeData(); + + verifyData([]); + + let principal = getPrincipal("http://prefix.test", {}); + let request = clearOriginsByPrefix(principal, "default"); + await requestFinished(request); + + // Wait for all database connections to close. + request = reset(); + await requestFinished(request); + + exportShadowDatabase("shadowdb-clearedOriginsByPrefix.sqlite"); + + // The shadow database is now prepared for + // test_databaseShadowing_clearOriginsByPrefix2.js +}); diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js new file mode 100644 index 0000000000..892a470723 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js @@ -0,0 +1,21 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from databaseShadowing-shared.js */ +loadSubscript("databaseShadowing-shared.js"); + +add_task(async function testSteps() { + // The shadow database was prepared in + // test_databaseShadowing_clearOriginsByPrefix1.js + + disableNextGenLocalStorage(); + + ok( + importShadowDatabase("shadowdb-clearedOriginsByPrefix.sqlite"), + "Import succeeded" + ); + + verifyData([2, 3], /* migrated */ true); +}); diff --git a/dom/localstorage/test/unit/test_eviction.js b/dom/localstorage/test/unit/test_eviction.js new file mode 100644 index 0000000000..d8c10b771f --- /dev/null +++ b/dom/localstorage/test/unit/test_eviction.js @@ -0,0 +1,91 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const globalLimitKB = 5 * 1024; + + const data = {}; + data.sizeKB = 1 * 1024; + data.key = "A"; + data.value = repeatChar(data.sizeKB * 1024 - data.key.length, "."); + data.urlCount = globalLimitKB / data.sizeKB; + + function getSpec(index) { + return "http://example" + index + ".com"; + } + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + info("Setting limits"); + + setGlobalLimit(globalLimitKB); + + let request = clear(); + await requestFinished(request); + + info("Getting storages"); + + let storages = []; + for (let i = 0; i < data.urlCount; i++) { + let storage = getLocalStorage(getPrincipal(getSpec(i))); + storages.push(storage); + } + + info("Filling up entire default storage"); + + for (let i = 0; i < data.urlCount; i++) { + storages[i].setItem(data.key, data.value); + await returnToEventLoop(); + } + + info("Verifying no more data can be written"); + + for (let i = 0; i < data.urlCount; i++) { + try { + storages[i].setItem("B", ""); + ok(false, "Should have thrown"); + } catch (ex) { + ok(true, "Did throw"); + ok(DOMException.isInstance(ex), "Threw DOMException"); + is(ex.name, "QuotaExceededError", "Threw right DOMException"); + is(ex.code, NS_ERROR_DOM_QUOTA_EXCEEDED_ERR, "Threw with right code"); + } + } + + info("Closing first origin"); + + storages[0].close(); + + let principal = getPrincipal("http://example0.com"); + + request = resetOrigin(principal); + await requestFinished(request); + + info("Getting usage for first origin"); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, data.sizeKB * 1024, "Correct usage"); + + info("Verifying more data data can be written"); + + for (let i = 1; i < data.urlCount; i++) { + storages[i].setItem("B", ""); + } + + info("Getting usage for first origin"); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, 0, "Zero usage"); +}); diff --git a/dom/localstorage/test/unit/test_flushing.js b/dom/localstorage/test/unit/test_flushing.js new file mode 100644 index 0000000000..b33ef7c099 --- /dev/null +++ b/dom/localstorage/test/unit/test_flushing.js @@ -0,0 +1,72 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +/** + * This test is mainly to verify that the flush operation detaches the shadow + * database in the event of early return due to error. See bug 1559029. + */ + +add_task(async function testSteps() { + const principal1 = getPrincipal("http://example1.com"); + + const usageFile1 = getRelativeFile( + "storage/default/http+++example1.com/ls/usage" + ); + + const principal2 = getPrincipal("http://example2.com"); + + const data = { + key: "foo", + value: "bar", + }; + + const flushSleepTimeSec = 6; + + info("Setting prefs"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + + info("Getting storage 1"); + + let storage1 = getLocalStorage(principal1); + + info("Adding item"); + + storage1.setItem(data.key, data.value); + + info("Creating usage as a directory"); + + // This will cause a failure during the flush for first principal. + usageFile1.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + + info("Getting storage 2"); + + let storage2 = getLocalStorage(principal2); + + info("Adding item"); + + storage2.setItem(data.key, data.value); + + // The flush for second principal shouldn't be affected by failed flush for + // first principal. + + info( + "Sleeping for " + + flushSleepTimeSec + + " seconds to let all flushes " + + "finish" + ); + + await new Promise(function(resolve) { + setTimeout(resolve, flushSleepTimeSec * 1000); + }); + + info("Resetting"); + + // Wait for all database connections to close. + let request = reset(); + await requestFinished(request); +}); diff --git a/dom/localstorage/test/unit/test_groupLimit.js b/dom/localstorage/test/unit/test_groupLimit.js new file mode 100644 index 0000000000..92ff07b7ab --- /dev/null +++ b/dom/localstorage/test/unit/test_groupLimit.js @@ -0,0 +1,85 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const groupLimitKB = 10 * 1024; + + const globalLimitKB = groupLimitKB * 5; + + const originLimit = 10 * 1024; + + const urls = [ + "http://example.com", + "http://test1.example.com", + "https://test2.example.com", + "http://test3.example.com:8080", + ]; + + const data = {}; + data.sizeKB = 5 * 1024; + data.key = "A"; + data.value = repeatChar(data.sizeKB * 1024 - data.key.length, "."); + data.urlCount = groupLimitKB / data.sizeKB; + + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + info("Setting limits"); + + setGlobalLimit(globalLimitKB); + + let request = clear(); + await requestFinished(request); + + setOriginLimit(originLimit); + + info("Getting storages"); + + let storages = []; + for (let i = 0; i < urls.length; i++) { + let storage = getLocalStorage(getPrincipal(urls[i])); + storages.push(storage); + } + + info("Filling up the whole group"); + + for (let i = 0; i < data.urlCount; i++) { + storages[i].setItem(data.key, data.value); + await returnToEventLoop(); + } + + info("Verifying no more data can be written"); + + for (let i = 0; i < urls.length; i++) { + try { + storages[i].setItem("B", ""); + ok(false, "Should have thrown"); + } catch (ex) { + ok(true, "Did throw"); + ok(DOMException.isInstance(ex), "Threw DOMException"); + is(ex.name, "QuotaExceededError", "Threw right DOMException"); + is(ex.code, NS_ERROR_DOM_QUOTA_EXCEEDED_ERR, "Threw with right code"); + } + } + + info("Clearing first origin"); + + storages[0].clear(); + + // Let the internal snapshot finish (usage is not descreased until all + // snapshots finish).. + await returnToEventLoop(); + + info("Verifying more data can be written"); + + for (let i = 0; i < urls.length; i++) { + storages[i].setItem("B", ""); + } +}); diff --git a/dom/localstorage/test/unit/test_groupMismatch.js b/dom/localstorage/test/unit/test_groupMismatch.js new file mode 100644 index 0000000000..46533aa2f7 --- /dev/null +++ b/dom/localstorage/test/unit/test_groupMismatch.js @@ -0,0 +1,45 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that metadata files with old group information + * get updated, so writing to local storage won't cause a crash because of null + * quota object. See bug 1516333. + */ + +add_task(async function testSteps() { + const principal = getPrincipal("https://foo.bar.mozilla-iot.org"); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains one initialized origin directory, a script for origin + // initialization and the storage database: + // - storage/default/https+++foo.bar.mozilla-iot.org + // - create_db.js + // - storage.sqlite + // The file create_db.js in the package was run locally, specifically it was + // temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Manually change the group in .metadata and .metadata-v2 from + // "bar.mozilla-iot.org" to "mozilla-iot.org". + // 2. Remove the folder "storage/temporary". + // 3. Remove the file "storage/ls-archive.sqlite". + installPackage("groupMismatch_profile"); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Adding item"); + + storage.setItem("foo", "bar"); +}); diff --git a/dom/localstorage/test/unit/test_largeItems.js b/dom/localstorage/test/unit/test_largeItems.js new file mode 100644 index 0000000000..3ea6bd21b4 --- /dev/null +++ b/dom/localstorage/test/unit/test_largeItems.js @@ -0,0 +1,88 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test repeatedly setting values that are just under the LocalStorage quota + * limit without yielding control flow in order to verify that the write + * optimizer is present / works. If there was no write optimizer present, the + * IPC message size limit would be exceeded, resulting in a crash. + */ + +add_task(async function testSteps() { + const globalLimitKB = 5 * 1024; + + // 18 and more iterations would produce an IPC message with size greater than + // 256 MB if write optimizer was not present. This number was determined + // experimentally by running the test with disabled write optimizer. + const numberOfIterations = 18; + + const randomStringBlockSize = 65536; + + // We need to use a random string because LS internally tries to compress + // values. + function getRandomString(size) { + let crypto = this.window ? this.window.crypto : this.crypto; + let decoder = new TextDecoder("ISO-8859-2"); + + function getRandomStringBlock(array) { + crypto.getRandomValues(array); + return decoder.decode(array); + } + + let string = ""; + + let quotient = size / randomStringBlockSize; + if (quotient) { + let array = new Uint8Array(randomStringBlockSize); + for (let i = 1; i <= quotient; i++) { + string += getRandomStringBlock(array); + } + } + + let remainder = size % randomStringBlockSize; + if (remainder) { + let array = new Uint8Array(remainder); + string += getRandomStringBlock(array); + } + + return string; + } + + const data = {}; + data.key = "foo"; + data.value = getRandomString( + globalLimitKB * 1024 - + data.key.length - + numberOfIterations.toString().length + ); + + info("Setting pref"); + + // By disabling snapshot reusing, we guarantee that the snapshot will be + // checkpointed once control returns to the event loop. + if (this.window) { + await SpecialPowers.pushPrefEnv({ + set: [["dom.storage.snapshot_reusing", false]], + }); + } else { + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + } + + info("Getting storage"); + + let storage = getLocalStorage(); + + info("Adding/updating item"); + + for (var i = 0; i < numberOfIterations; i++) { + storage.setItem(data.key, data.value + i); + } + + info("Returning to event loop"); + + await returnToEventLoop(); + + ok(!storage.hasSnapshot, "Snapshot successfully finished"); +}); diff --git a/dom/localstorage/test/unit/test_lsng_enabled.js b/dom/localstorage/test/unit/test_lsng_enabled.js new file mode 100644 index 0000000000..d978aaa901 --- /dev/null +++ b/dom/localstorage/test/unit/test_lsng_enabled.js @@ -0,0 +1,13 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that LSNG is not accidentally disabled which + * can lead to a data loss in a combination with disabled shadow writes. + */ + +add_task(async function testSteps() { + ok(Services.domStorageManager.nextGenLocalStorageEnabled, "LSNG enabled"); +}); diff --git a/dom/localstorage/test/unit/test_migration.js b/dom/localstorage/test/unit/test_migration.js new file mode 100644 index 0000000000..1249eb076f --- /dev/null +++ b/dom/localstorage/test/unit/test_migration.js @@ -0,0 +1,127 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const principalInfos = [ + { url: "http://localhost", attrs: {} }, + { url: "http://www.mozilla.org", attrs: {} }, + { url: "http://example.com", attrs: {} }, + { url: "http://example.org", attrs: { userContextId: 5 } }, + + { url: "http://origin.test", attrs: {} }, + + { url: "http://prefix.test", attrs: {} }, + { url: "http://prefix.test", attrs: { userContextId: 10 } }, + + { url: "http://pattern.test", attrs: { userContextId: 15 } }, + { url: "http://pattern.test:8080", attrs: { userContextId: 15 } }, + { url: "https://pattern.test", attrs: { userContextId: 15 } }, + ]; + + const data = { + key: "foo", + value: "bar", + }; + + function verifyData(clearedOrigins) { + info("Getting storages"); + + let storages = []; + for (let i = 0; i < principalInfos.length; i++) { + let principalInfo = principalInfos[i]; + let principal = getPrincipal(principalInfo.url, principalInfo.attrs); + let storage = getLocalStorage(principal); + storages.push(storage); + } + + info("Verifying data"); + + for (let i = 0; i < storages.length; i++) { + let value = storages[i].getItem(data.key + i); + if (clearedOrigins.includes(i)) { + is(value, null, "Correct value"); + } else { + is(value, data.value + i, "Correct value"); + } + } + } + + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + info("Stage 1 - Testing archived data migration"); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite. The file + // create_db.js in the package was run locally, specifically it was + // temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js + installPackage("migration_profile"); + + verifyData([]); + + info("Stage 2 - Testing archived data clearing"); + + for (let type of ["origin", "prefix", "pattern"]) { + info("Clearing"); + + request = clear(); + await requestFinished(request); + + info("Installing package"); + + // See the comment for the first installPackage() call. + installPackage("migration_profile"); + + let clearedOrigins = []; + + switch (type) { + case "origin": { + let principal = getPrincipal("http://origin.test", {}); + request = clearOrigin(principal, "default"); + await requestFinished(request); + + clearedOrigins.push(4); + + break; + } + + case "prefix": { + let principal = getPrincipal("http://prefix.test", {}); + request = clearOriginsByPrefix(principal, "default"); + await requestFinished(request); + + clearedOrigins.push(5, 6); + + break; + } + + case "pattern": { + request = clearOriginsByPattern(JSON.stringify({ userContextId: 15 })); + await requestFinished(request); + + clearedOrigins.push(7, 8, 9); + + break; + } + + default: { + throw new Error("Unknown type: " + type); + } + } + + verifyData(clearedOrigins); + } +}); diff --git a/dom/localstorage/test/unit/test_migration_emptyValue.js b/dom/localstorage/test/unit/test_migration_emptyValue.js new file mode 100644 index 0000000000..dd09c82e88 --- /dev/null +++ b/dom/localstorage/test/unit/test_migration_emptyValue.js @@ -0,0 +1,37 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const data = { + key: "foo", + value: "", + }; + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite. + // The archive migration_emptyValue_profile.zip was created by running + // make_migration_emptyValue.js locally, specifically the special test was + // temporarily activated in xpcshell.ini and then it was run as: + // mach test --interactive dom/localstorage/test/unit/make_migration_emptyValue.js + // Before packaging, additional manual steps are needed: + // 1. Folder "cache2" is removed. + // 2. Folder "crashes" is removed. + // 3. File "mozinfo.json" is removed. + installPackage("migration_emptyValue_profile"); + + info("Getting storage"); + + const storage = getLocalStorage(); + + info("Verifying data"); + + is(storage.getItem(data.key), data.value, "Correct value"); +}); diff --git a/dom/localstorage/test/unit/test_old_lsng_pref.js b/dom/localstorage/test/unit/test_old_lsng_pref.js new file mode 100644 index 0000000000..d502ee8779 --- /dev/null +++ b/dom/localstorage/test/unit/test_old_lsng_pref.js @@ -0,0 +1,17 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that the old pref for switching LS + * implementations has no effect anymore. + */ + +add_task(async function testSteps() { + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", false); + + ok(Services.domStorageManager.nextGenLocalStorageEnabled, "LSNG enabled"); +}); diff --git a/dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js b/dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js new file mode 100644 index 0000000000..88a2e45d2a --- /dev/null +++ b/dom/localstorage/test/unit/test_orderingAfterRemoveAdd.js @@ -0,0 +1,70 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const url = "http://example.com"; + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + const items = [ + { key: "key01", value: "value01" }, + { key: "key02", value: "value02" }, + { key: "key03", value: "value03" }, + { key: "key04", value: "value04" }, + { key: "key05", value: "value05" }, + ]; + + info("Getting storage"); + + let storage = getLocalStorage(getPrincipal(url)); + + // 1st snapshot + + info("Adding data"); + + for (let item of items) { + storage.setItem(item.key, item.value); + } + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 2nd snapshot + + // Remove first two items, add some new items and add the two items back. + + storage.removeItem("key01"); + storage.removeItem("key02"); + + storage.setItem("key06", "value06"); + storage.setItem("key07", "value07"); + storage.setItem("key08", "value08"); + + storage.setItem("key01", "value01"); + storage.setItem("key02", "value02"); + + info("Saving key order"); + + let savedKeys = Object.keys(storage); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 3rd snapshot + + info("Verifying key order"); + + let keys = Object.keys(storage); + + is(keys.length, savedKeys.length); + + for (let i = 0; i < keys.length; i++) { + is(keys[i], savedKeys[i], "Correct key"); + } +}); diff --git a/dom/localstorage/test/unit/test_originInit.js b/dom/localstorage/test/unit/test_originInit.js new file mode 100644 index 0000000000..48afdf971b --- /dev/null +++ b/dom/localstorage/test/unit/test_originInit.js @@ -0,0 +1,372 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const storageDirName = "storage"; + const persistenceTypeDefaultDirName = "default"; + const persistenceTypePersistentDirName = "permanent"; + + const principal = getPrincipal("http://example.com"); + + const originDirName = "http+++example.com"; + + const clientLSDirName = "ls"; + + const dataFile = getRelativeFile( + `${storageDirName}/${persistenceTypeDefaultDirName}/${originDirName}/` + + `${clientLSDirName}/data.sqlite` + ); + + const usageJournalFile = getRelativeFile( + `${storageDirName}/${persistenceTypeDefaultDirName}/${originDirName}/` + + `${clientLSDirName}/usage-journal` + ); + + const usageFile = getRelativeFile( + `${storageDirName}/${persistenceTypeDefaultDirName}/${originDirName}/` + + `${clientLSDirName}/usage` + ); + + const persistentLSDir = getRelativeFile( + `${storageDirName}/${persistenceTypePersistentDirName}/${originDirName}/` + + `${clientLSDirName}` + ); + + const data = {}; + data.key = "key1"; + data.value = "value1"; + data.usage = data.key.length + data.value.length; + + const usageFileCookie = 0x420a420a; + + async function createTestOrigin() { + let storage = getLocalStorage(principal); + + storage.setItem(data.key, data.value); + + let request = reset(); + await requestFinished(request); + } + + async function createPersistentTestOrigin() { + let database = getSimpleDatabase(principal, "persistent"); + + let request = database.open("data"); + await requestFinished(request); + + request = reset(); + await requestFinished(request); + } + + function removeFile(file) { + file.remove(false); + } + + function createEmptyFile(file) { + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o0644); + } + + function createEmptyDirectory(dir) { + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o0755); + } + + function getBinaryOutputStream(file) { + var ostream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + ostream.init(file, -1, parseInt("0644", 8), 0); + + let bstream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + bstream.setOutputStream(ostream); + + return bstream; + } + + async function initTestOrigin() { + let request = initStorage(); + await requestFinished(request); + + request = initTemporaryStorage(); + await requestFinished(request); + + request = initTemporaryOrigin("default", principal); + await requestFinished(request); + } + + async function initPersistentTestOrigin() { + let request = initStorage(); + await requestFinished(request); + + request = initPersistentOrigin(principal); + await requestFinished(request); + } + + async function checkFiles(wantData, wantUsage) { + let exists = dataFile.exists(); + if (wantData) { + ok(exists, "Data file does exist"); + } else { + ok(!exists, "Data file doesn't exist"); + } + + exists = usageJournalFile.exists(); + ok(!exists, "Usage journal file doesn't exist"); + + exists = usageFile.exists(); + if (wantUsage) { + ok(exists, "Usage file does exist"); + } else { + ok(!exists, "Usage file doesn't exist"); + return; + } + + let usage = await readUsageFromUsageFile(usageFile); + ok(usage == data.usage, "Correct usage"); + } + + async function clearTestOrigin() { + let request = clearOrigin(principal, "default"); + await requestFinished(request); + } + + async function clearPersistentTestOrigin() { + let request = clearOrigin(principal, "persistent"); + await requestFinished(request); + } + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + info( + "Stage 1 - " + + "data.sqlite file doesn't exist, " + + "usage-journal file doesn't exist, " + + "any usage file exists" + ); + + await createTestOrigin(); + + removeFile(dataFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ false, /* wantUsage */ false); + + await clearTestOrigin(); + + info( + "Stage 2 - " + + "data.sqlite file doesn't exist, " + + "any usage-journal file exists, " + + "any usage file exists" + ); + + await createTestOrigin(); + + removeFile(dataFile); + createEmptyFile(usageJournalFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ false, /* wantUsage */ false); + + await clearTestOrigin(); + + info( + "Stage 3 - " + + "valid data.sqlite file exists, " + + "usage-journal file doesn't exist, " + + "usage file doesn't exist" + ); + + await createTestOrigin(); + + removeFile(usageFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 4 - " + + "valid data.sqlite file exists, " + + "usage-journal file doesn't exist, " + + "invalid (wrong file size) usage file exists" + ); + + await createTestOrigin(); + + removeFile(usageFile); + createEmptyFile(usageFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 5 - " + + "valid data.sqlite file exists, " + + "usage-journal file doesn't exist, " + + "invalid (wrong cookie) usage file exists" + ); + + await createTestOrigin(); + + let stream = getBinaryOutputStream(usageFile); + stream.write32(usageFileCookie - 1); + stream.write64(data.usage); + stream.close(); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 6 - " + + "valid data.sqlite file exists, " + + "usage-journal file doesn't exist, " + + "valid usage file exists" + ); + + await createTestOrigin(); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 7 - " + + "valid data.sqlite file exists, " + + "any usage-journal exists, " + + "usage file doesn't exist" + ); + + await createTestOrigin(); + + createEmptyFile(usageJournalFile); + removeFile(usageFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 8 - " + + "valid data.sqlite file exists, " + + "any usage-journal exists, " + + "invalid (wrong file size) usage file exists" + ); + + await createTestOrigin(); + + createEmptyFile(usageJournalFile); + removeFile(usageFile); + createEmptyFile(usageFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 9 - " + + "valid data.sqlite file exists, " + + "any usage-journal exists, " + + "invalid (wrong cookie) usage file exists" + ); + + await createTestOrigin(); + + createEmptyFile(usageJournalFile); + stream = getBinaryOutputStream(usageFile); + stream.write32(usageFileCookie - 1); + stream.write64(data.usage); + stream.close(); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 10 - " + + "valid data.sqlite file exists, " + + "any usage-journal exists, " + + "invalid (wrong usage) usage file exists" + ); + + await createTestOrigin(); + + createEmptyFile(usageJournalFile); + stream = getBinaryOutputStream(usageFile); + stream.write32(usageFileCookie); + stream.write64(data.usage - 1); + stream.close(); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + info( + "Stage 11 - " + + "valid data.sqlite file exists, " + + "any usage-journal exists, " + + "valid usage file exists" + ); + + await createTestOrigin(); + + createEmptyFile(usageJournalFile); + + await initTestOrigin(); + + await checkFiles(/* wantData */ true, /* wantUsage */ true); + + await clearTestOrigin(); + + // Verify that InitializeOrigin doesn't fail when a + // storage/permanent/${origin}/ls exists. + info( + "Stage 12 - Testing initialization of ls directory placed in permanent " + + "origin directory" + ); + + await createPersistentTestOrigin(); + + createEmptyDirectory(persistentLSDir); + + try { + await initPersistentTestOrigin(); + + ok(true, "Should not have thrown"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + + let exists = persistentLSDir.exists(); + ok(exists, "ls directory in permanent origin directory does exist"); + + await clearPersistentTestOrigin(); +}); diff --git a/dom/localstorage/test/unit/test_preloading.js b/dom/localstorage/test/unit/test_preloading.js new file mode 100644 index 0000000000..977ab10d99 --- /dev/null +++ b/dom/localstorage/test/unit/test_preloading.js @@ -0,0 +1,84 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const principals = [ + getPrincipal("http://example.com", {}), + getPrincipal("http://example.com", { privateBrowsingId: 1 }), + ]; + + async function isPreloaded(principal) { + return Services.domStorageManager.isPreloaded(principal); + } + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + for (const principal of principals) { + info("Getting storage"); + + let storage = getLocalStorage(principal); + + ok( + !(await isPreloaded(principal)), + "Data is not preloaded after getting storage" + ); + + info("Opening storage"); + + storage.open(); + + ok(await isPreloaded(principal), "Data is preloaded after opening storage"); + + info("Closing storage"); + + storage.close(); + + if (principal.privateBrowsingId > 0) { + ok( + await isPreloaded(principal), + "Data is still preloaded after closing storage" + ); + + info("Closing private session"); + + Services.obs.notifyObservers(null, "last-pb-context-exited"); + + ok( + !(await isPreloaded(principal)), + "Data is not preloaded anymore after closing private session" + ); + } else { + ok( + !(await isPreloaded(principal)), + "Data is not preloaded anymore after closing storage" + ); + } + + info("Opening storage again"); + + storage.open(); + + ok( + await isPreloaded(principal), + "Data is preloaded after opening storage again" + ); + + info("Clearing origin"); + + let request = clearOrigin(principal, "default"); + await requestFinished(request); + + ok( + !(await isPreloaded(principal)), + "Data is not preloaded after clearing origin" + ); + } +}); diff --git a/dom/localstorage/test/unit/test_schema3upgrade.js b/dom/localstorage/test/unit/test_schema3upgrade.js new file mode 100644 index 0000000000..4b851642ea --- /dev/null +++ b/dom/localstorage/test/unit/test_schema3upgrade.js @@ -0,0 +1,39 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const url = "http://example.com"; + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains one initialized origin directory with local + // storage data, local storage archive, a script for origin initialization, + // the storage database and the web apps store database: + // - storage/default/http+++example.com + // - storage/ls-archive.sqlite + // - create_db.js + // - storage.sqlite + // - webappsstore.sqlite + // The file create_db.js in the package was run locally (with a build with + // local storage archive version 1 and database schema version 2), + // specifically it was temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Remove the folder "storage/temporary". + installPackage("schema3upgrade_profile"); + + let storage = getLocalStorage(getPrincipal(url)); + storage.open(); +}); diff --git a/dom/localstorage/test/unit/test_schema4upgrade.js b/dom/localstorage/test/unit/test_schema4upgrade.js new file mode 100644 index 0000000000..a6c308af35 --- /dev/null +++ b/dom/localstorage/test/unit/test_schema4upgrade.js @@ -0,0 +1,39 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const url = "http://example.com"; + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains one initialized origin directory with local + // storage data, local storage archive, a script for origin initialization, + // the storage database and the web apps store database: + // - storage/default/http+++example.com + // - storage/ls-archive.sqlite + // - storage.sqlite + // - test_create_db.js + // - webappsstore.sqlite + // + // The file test_create_db.js in the package was run locally by + // adding it temporarily to xpcshell.ini and then executed with + // mach xpcshell-test --headless dom/localstorage/test/unit/test_create_db.js + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Remove the folder "storage/temporary". + installPackage("schema4upgrade_profile"); + + let storage = getLocalStorage(getPrincipal(url)); + storage.open(); +}); diff --git a/dom/localstorage/test/unit/test_snapshotting.js b/dom/localstorage/test/unit/test_snapshotting.js new file mode 100644 index 0000000000..4b639395f7 --- /dev/null +++ b/dom/localstorage/test/unit/test_snapshotting.js @@ -0,0 +1,330 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const url = "http://example.com"; + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + const items = [ + { key: "key01", value: "value01" }, + { key: "key02", value: "value02" }, + { key: "key03", value: "value03" }, + { key: "key04", value: "value04" }, + { key: "key05", value: "value05" }, + { key: "key06", value: "value06" }, + { key: "key07", value: "value07" }, + { key: "key08", value: "value08" }, + { key: "key09", value: "value09" }, + { key: "key10", value: "value10" }, + ]; + + let sizeOfOneKey; + let sizeOfOneValue; + let sizeOfOneItem; + let sizeOfKeys = 0; + let sizeOfItems = 0; + + for (let i = 0; i < items.length; i++) { + let item = items[i]; + let sizeOfKey = item.key.length; + let sizeOfValue = item.value.length; + let sizeOfItem = sizeOfKey + sizeOfValue; + if (i == 0) { + sizeOfOneKey = sizeOfKey; + sizeOfOneValue = sizeOfValue; + sizeOfOneItem = sizeOfItem; + } + sizeOfKeys += sizeOfKey; + sizeOfItems += sizeOfItem; + } + + info("Size of one key is " + sizeOfOneKey); + info("Size of one value is " + sizeOfOneValue); + info("Size of one item is " + sizeOfOneItem); + info("Size of keys is " + sizeOfKeys); + info("Size of items is " + sizeOfItems); + + const prefillValues = [ + // Zero prefill (prefill disabled) + 0, + // Less than one key length prefill + sizeOfOneKey - 1, + // Greater than one key length and less than one item length prefill + sizeOfOneKey + 1, + // Precisely one item length prefill + sizeOfOneItem, + // Precisely two times one item length prefill + 2 * sizeOfOneItem, + // Precisely three times one item length prefill + 3 * sizeOfOneItem, + // Precisely four times one item length prefill + 4 * sizeOfOneItem, + // Precisely size of keys prefill + sizeOfKeys, + // Less than size of keys plus one value length prefill + sizeOfKeys + sizeOfOneValue - 1, + // Precisely size of keys plus one value length prefill + sizeOfKeys + sizeOfOneValue, + // Greater than size of keys plus one value length and less than size of + // keys plus two times one value length prefill + sizeOfKeys + sizeOfOneValue + 1, + // Precisely size of keys plus two times one value length prefill + sizeOfKeys + 2 * sizeOfOneValue, + // Precisely size of keys plus three times one value length prefill + sizeOfKeys + 3 * sizeOfOneValue, + // Precisely size of keys plus four times one value length prefill + sizeOfKeys + 4 * sizeOfOneValue, + // Precisely size of keys plus five times one value length prefill + sizeOfKeys + 5 * sizeOfOneValue, + // Precisely size of keys plus six times one value length prefill + sizeOfKeys + 6 * sizeOfOneValue, + // Precisely size of keys plus seven times one value length prefill + sizeOfKeys + 7 * sizeOfOneValue, + // Precisely size of keys plus eight times one value length prefill + sizeOfKeys + 8 * sizeOfOneValue, + // Precisely size of keys plus nine times one value length prefill + sizeOfKeys + 9 * sizeOfOneValue, + // Precisely size of items prefill + sizeOfItems, + // Unlimited prefill + -1, + ]; + + for (let prefillValue of prefillValues) { + info("Setting prefill value to " + prefillValue); + + Services.prefs.setIntPref("dom.storage.snapshot_prefill", prefillValue); + + const gradualPrefillValues = [ + // Zero gradual prefill + 0, + // Less than one key length gradual prefill + sizeOfOneKey - 1, + // Greater than one key length and less than one item length gradual + // prefill + sizeOfOneKey + 1, + // Precisely one item length gradual prefill + sizeOfOneItem, + // Precisely two times one item length gradual prefill + 2 * sizeOfOneItem, + // Precisely three times one item length gradual prefill + 3 * sizeOfOneItem, + // Precisely four times one item length gradual prefill + 4 * sizeOfOneItem, + // Precisely five times one item length gradual prefill + 5 * sizeOfOneItem, + // Precisely six times one item length gradual prefill + 6 * sizeOfOneItem, + // Precisely seven times one item length gradual prefill + 7 * sizeOfOneItem, + // Precisely eight times one item length gradual prefill + 8 * sizeOfOneItem, + // Precisely nine times one item length gradual prefill + 9 * sizeOfOneItem, + // Precisely size of items prefill + sizeOfItems, + // Unlimited gradual prefill + -1, + ]; + + for (let gradualPrefillValue of gradualPrefillValues) { + info("Setting gradual prefill value to " + gradualPrefillValue); + + Services.prefs.setIntPref( + "dom.storage.snapshot_gradual_prefill", + gradualPrefillValue + ); + + info("Getting storage"); + + let storage = getLocalStorage(getPrincipal(url)); + + // 1st snapshot + + info("Adding data"); + + for (let item of items) { + storage.setItem(item.key, item.value); + } + + info("Saving key order"); + + // This forces GetKeys to be called internally. + let savedKeys = Object.keys(storage); + + // GetKey should match GetKeys + for (let i = 0; i < savedKeys.length; i++) { + is(storage.key(i), savedKeys[i], "Correct key"); + } + + info("Returning to event loop"); + + // Returning to event loop forces the internal snapshot to finish. + await returnToEventLoop(); + + // 2nd snapshot + + info("Verifying length"); + + is(storage.length, items.length, "Correct length"); + + info("Verifying key order"); + + let keys = Object.keys(storage); + + is(keys.length, savedKeys.length); + + for (let i = 0; i < keys.length; i++) { + is(keys[i], savedKeys[i], "Correct key"); + } + + info("Verifying values"); + + for (let item of items) { + is(storage.getItem(item.key), item.value, "Correct value"); + } + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 3rd snapshot + + // Force key2 to load. + storage.getItem("key02"); + + // Fill out write infos a bit. + storage.removeItem("key05"); + storage.setItem("key05", "value05"); + storage.removeItem("key05"); + storage.setItem("key11", "value11"); + storage.setItem("key05", "value05"); + + items.push({ key: "key11", value: "value11" }); + + info("Verifying length"); + + is(storage.length, items.length, "Correct length"); + + // This forces to get all keys from the parent and then apply write infos + // on already cached values. + savedKeys = Object.keys(storage); + + info("Verifying values"); + + for (let item of items) { + is(storage.getItem(item.key), item.value, "Correct value"); + } + + storage.removeItem("key11"); + + items.pop(); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 4th snapshot + + // Force loading of all items. + info("Verifying length"); + + is(storage.length, items.length, "Correct length"); + + info("Verifying values"); + + for (let item of items) { + is(storage.getItem(item.key), item.value, "Correct value"); + } + + is(storage.getItem("key11"), null, "Correct value"); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 5th snapshot + + // Force loading of all keys. + info("Saving key order"); + + savedKeys = Object.keys(storage); + + // Force loading of all items. + info("Verifying length"); + + is(storage.length, items.length, "Correct length"); + + info("Verifying values"); + + for (let item of items) { + is(storage.getItem(item.key), item.value, "Correct value"); + } + + is(storage.getItem("key11"), null, "Correct value"); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 6th snapshot + info("Verifying unknown item"); + + is(storage.getItem("key11"), null, "Correct value"); + + info("Verifying unknown item again"); + + is(storage.getItem("key11"), null, "Correct value"); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 7th snapshot + + // Save actual key order. + info("Saving key order"); + + savedKeys = Object.keys(storage); + + await returnToEventLoop(); + + // 8th snapshot + + // Force loading of all items, but in reverse order. + info("Getting values"); + + for (let i = items.length - 1; i >= 0; i--) { + let item = items[i]; + storage.getItem(item.key); + } + + info("Verifying key order"); + + keys = Object.keys(storage); + + is(keys.length, savedKeys.length); + + for (let i = 0; i < keys.length; i++) { + is(keys[i], savedKeys[i], "Correct key"); + } + + await returnToEventLoop(); + + // 9th snapshot + + info("Clearing"); + + storage.clear(); + + info("Returning to event loop"); + + await returnToEventLoop(); + } + } +}); diff --git a/dom/localstorage/test/unit/test_stringLength.js b/dom/localstorage/test/unit/test_stringLength.js new file mode 100644 index 0000000000..52ee59f7d6 --- /dev/null +++ b/dom/localstorage/test/unit/test_stringLength.js @@ -0,0 +1,74 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const principal = getPrincipal("http://example.org"); + + const data = {}; + data.key = "foobar"; + data.secondKey = "foobaz"; + data.value = { + length: 25637, + }; + data.usage = data.key.length + data.value.length; + + async function checkUsage(expectedUsage) { + info("Checking usage"); + + // This forces any pending changes to be flushed to disk. It also forces + // data to be reloaded from disk at next localStorage API call. + request = resetOrigin(principal); + await requestFinished(request); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, expectedUsage, "Correct usage"); + } + + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + info("Stage 1 - Checking usage after profile installation"); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite. + installPackage("stringLength_profile"); + + await checkUsage(0); + + info("Stage 2 - Checking usage after archived data migration"); + + info("Opening database"); + + let storage = getLocalStorage(principal); + storage.open(); + + await checkUsage(data.usage); + + info("Stage 3 - Checking usage after copying the value"); + + info("Adding a second copy of the value"); + + let value = storage.getItem(data.key); + storage.setItem(data.secondKey, value); + + await checkUsage(2 * data.usage); + + info("Stage 4 - Checking length of the copied value"); + + value = storage.getItem(data.secondKey); + ok(value.length === data.value.length, "Correct string length"); +}); diff --git a/dom/localstorage/test/unit/test_stringLength2.js b/dom/localstorage/test/unit/test_stringLength2.js new file mode 100644 index 0000000000..f1a1a902cf --- /dev/null +++ b/dom/localstorage/test/unit/test_stringLength2.js @@ -0,0 +1,79 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that string length is correctly computed for + * database values containing NULs. See bug 1541681. + */ + +add_task(async function testSteps() { + const principal = getPrincipal("http://example.org"); + + const data = {}; + data.key = "foobar"; + data.secondKey = "foobaz"; + data.value = { + length: 19253, + }; + data.usage = data.key.length + data.value.length; + + async function checkUsage(expectedUsage) { + info("Checking usage"); + + // This forces any pending changes to be flushed to disk. It also forces + // data to be reloaded from disk at next localStorage API call. + request = resetOrigin(principal); + await requestFinished(request); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, expectedUsage, "Correct usage"); + } + + info("Setting pref"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + info("Stage 1 - Checking usage after profile installation"); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite. + installPackage("stringLength2_profile"); + + await checkUsage(0); + + info("Stage 2 - Checking usage after archived data migration"); + + info("Opening database"); + + let storage = getLocalStorage(principal); + storage.open(); + + await checkUsage(data.usage); + + info("Stage 3 - Checking usage after copying the value"); + + info("Adding a second copy of the value"); + + let value = storage.getItem(data.key); + storage.setItem(data.secondKey, value); + + await checkUsage(2 * data.usage); + + info("Stage 4 - Checking length of the copied value"); + + value = storage.getItem(data.secondKey); + ok(value.length === data.value.length, "Correct string length"); +}); diff --git a/dom/localstorage/test/unit/test_unicodeCharacters.js b/dom/localstorage/test/unit/test_unicodeCharacters.js new file mode 100644 index 0000000000..9e48274161 --- /dev/null +++ b/dom/localstorage/test/unit/test_unicodeCharacters.js @@ -0,0 +1,205 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const interpretChar = (chars, index) => { + return chars + .charCodeAt(index) + .toString(16) + .padStart(4, "0"); +}; + +const hexEncode = str => { + let result = ""; + const len = str.length; + for (let i = 0; i < len; ++i) { + result += interpretChar(str, i); + } + return result; +}; + +const collectCorrupted = (expected, actual) => { + const len = Math.min(expected.length, actual.length); + let notEquals = []; + for (let i = 0; i < len; ++i) { + if (expected[i] !== actual[i]) { + notEquals.push([hexEncode(expected[i]), hexEncode(actual[i])]); + } + } + return notEquals; +}; + +const sanitizeOutputWithSurrogates = (testValue, prefix = "") => { + let utf8What = prefix; + for (let i = 0; i < testValue.length; ++i) { + const valueChar = testValue.charCodeAt(i); + const isPlanar = 0xd800 <= valueChar && valueChar <= 0xdfff; + utf8What += isPlanar ? "\\u" + interpretChar(testValue, i) : testValue[i]; + } + return utf8What; +}; + +const getEncodingSample = () => { + const expectedSample = + "3681207208613504e0a5028800b945551988c60050008027ebc2808c00d38e806e03d8210ac906722b85499be9d00000"; + + let result = ""; + const len = expectedSample.length; + for (let i = 0; i < len; i += 4) { + result += String.fromCharCode(parseInt(expectedSample.slice(i, i + 4), 16)); + } + return result; +}; + +const getSeparatedBasePlane = () => { + let result = ""; + for (let i = 0xffff; i >= 0; --i) { + result += String.fromCharCode(i) + "\n"; + } + return result; +}; + +const getJoinedBasePlane = () => { + let result = ""; + for (let i = 0; i <= 0xffff; ++i) { + result += String.fromCharCode(i); + } + return result; +}; + +const getSurrogateCombinations = () => { + const upperLead = String.fromCharCode(0xdbff); + const lowerTrail = String.fromCharCode(0xdc00); + + const regularSlot = ["w", "abcdefghijklmnopqrst", "aaaaaaaaaaaaaaaaaaaa", ""]; + const surrogateSlot = [lowerTrail, upperLead]; + + let samples = []; + for (const leadSnippet of regularSlot) { + for (const firstSlot of surrogateSlot) { + for (const trailSnippet of regularSlot) { + for (const secondSlot of surrogateSlot) { + samples.push(leadSnippet + firstSlot + secondSlot + trailSnippet); + } + samples.push(leadSnippet + firstSlot + trailSnippet); + } + } + } + + return samples; +}; + +const fetchFrom = async (itemKey, sample, meanwhile) => { + const principal = getPrincipal("http://example.com/", {}); + + let request = clearOrigin(principal); + await requestFinished(request); + + const storage = getLocalStorage(principal); + + await storage.setItem(itemKey, sample); + + await meanwhile(principal); + + return storage.getItem(itemKey); +}; + +/** + * Value fetched from existing snapshot based on + * existing in-memory datastore in the parent process + * without any communication between content/parent + */ +const fetchFromExistingSnapshotExistingDatastore = async (itemKey, sample) => { + return fetchFrom(itemKey, sample, async () => {}); +}; + +/** + * Value fetched from newly created snapshot based on + * existing in-memory datastore in the parent process + */ +const fetchFromNewSnapshotExistingDatastore = async (itemKey, sample) => { + return fetchFrom(itemKey, sample, async () => { + await returnToEventLoop(); + }); +}; + +/** + * Value fetched from newly created snapshot based on newly created + * in-memory datastore based on database in the parent process + */ +const fetchFromNewSnapshotNewDatastore = async (itemKey, sample) => { + return fetchFrom(itemKey, sample, async principal => { + let request = resetOrigin(principal); + await requestFinished(request); + }); +}; + +add_task(async function testSteps() { + /* This test is based on bug 1681300 */ + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + const reportWhat = (testKey, testValue) => { + if (testKey.length + testValue.length > 82) { + return testKey; + } + return sanitizeOutputWithSurrogates(testValue, /* prefix */ testKey + ":"); + }; + + const testFetchMode = async (testType, storeAndLookup) => { + const testPairs = [ + { testEmptyValue: [""] }, + { testSampleKey: [getEncodingSample()] }, + { testSeparatedKey: [getSeparatedBasePlane()] }, + { testJoinedKey: [getJoinedBasePlane()] }, + { testCombinations: getSurrogateCombinations() }, + ]; + + for (const testPair of testPairs) { + for (const [testKey, expectedValues] of Object.entries(testPair)) { + for (const expected of expectedValues) { + const actual = await storeAndLookup(testKey, expected); + const testInfo = reportWhat(testKey, expected); + is( + null != actual, + true, + testType + ": Value not null for " + testInfo + ); + is( + expected.length, + actual.length, + testType + ": Returned size for " + testInfo + ); + + const notEquals = collectCorrupted(expected, actual); + for (let i = 0; i < notEquals.length; ++i) { + is( + notEquals[i][0], + notEquals[i][1], + testType + ": Unequal character at " + i + " for " + testInfo + ); + } + } + } + } + }; + + await testFetchMode( + "ExistingSnapshotExistingDatastore", + fetchFromExistingSnapshotExistingDatastore + ); + + await testFetchMode( + "NewSnapshotExistingDatastore", + fetchFromNewSnapshotExistingDatastore + ); + + await testFetchMode( + "NewSnapshotNewDatastore", + fetchFromNewSnapshotNewDatastore + ); +}); diff --git a/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js b/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js new file mode 100644 index 0000000000..0da4e8584d --- /dev/null +++ b/dom/localstorage/test/unit/test_uri_encoding_edge_cases.js @@ -0,0 +1,69 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test verifies that group and origin strings for URIs with special + * characters are consistent between calling + * EnsureQuotaForOringin/EnsureOriginIsInitailized and GetQuotaObject in + * PrepareDatastoreOp, so writing to local storage won't cause a crash because + * of a null quota object. See bug 1516333. + */ + +add_task(async function testSteps() { + /** + * The edge cases are specified in this array of origins. Each edge case must + * contain two properties uri and path (origin directory path relative to the + * profile directory). + */ + const origins = [ + { + uri: "file:///test'.html", + path: "storage/default/file++++test'.html", + }, + { + uri: "file:///test>.html", + path: "storage/default/file++++test%3E.html", + }, + ]; + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + for (let origin of origins) { + const principal = getPrincipal(origin.uri); + + let originDir = getRelativeFile(origin.path); + + info("Checking the origin directory existence"); + + ok( + !originDir.exists(), + `The origin directory ${origin.path} should not exists` + ); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Adding item"); + + storage.setItem("foo", "bar"); + + info("Resetting origin"); + + // This forces any pending changes to be flushed to disk (including origin + // directory creation). + let request = resetOrigin(principal); + await requestFinished(request); + + info("Checking the origin directory existence"); + + ok(originDir.exists(), `The origin directory ${origin.path} should exist`); + } +}); diff --git a/dom/localstorage/test/unit/test_usage.js b/dom/localstorage/test/unit/test_usage.js new file mode 100644 index 0000000000..552a45e4a6 --- /dev/null +++ b/dom/localstorage/test/unit/test_usage.js @@ -0,0 +1,69 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const data = {}; + data.key = "key1"; + data.value = "value1"; + data.usage = data.key.length + data.value.length; + + const principal = getPrincipal("http://example.com"); + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + info("Stage 1 - Testing usage after adding item"); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Adding item"); + + storage.setItem(data.key, data.value); + + info("Resetting origin"); + + let request = resetOrigin(principal); + await requestFinished(request); + + info("Getting usage"); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, data.usage, "Correct usage"); + + info("Resetting"); + + request = reset(); + await requestFinished(request); + + info("Stage 2 - Testing usage after removing item"); + + info("Getting storage"); + + storage = getLocalStorage(principal); + + info("Removing item"); + + storage.removeItem(data.key); + + info("Resetting origin"); + + request = resetOrigin(principal); + await requestFinished(request); + + info("Getting usage"); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, 0, "Correct usage"); +}); diff --git a/dom/localstorage/test/unit/test_usageAfterMigration.js b/dom/localstorage/test/unit/test_usageAfterMigration.js new file mode 100644 index 0000000000..a0bd5efd5b --- /dev/null +++ b/dom/localstorage/test/unit/test_usageAfterMigration.js @@ -0,0 +1,164 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSteps() { + const principal = getPrincipal("http://example.com"); + + const dataFile = getRelativeFile( + "storage/default/http+++example.com/ls/data.sqlite" + ); + + const usageJournalFile = getRelativeFile( + "storage/default/http+++example.com/ls/usage-journal" + ); + + const usageFile = getRelativeFile( + "storage/default/http+++example.com/ls/usage" + ); + + const data = {}; + data.key = "foo"; + data.value = "bar"; + data.usage = data.key.length + data.value.length; + + async function createStorageForMigration(createUsageDir) { + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite. The file + // create_db.js in the package was run locally, specifically it was + // temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js + installPackage("usageAfterMigration_profile"); + + if (createUsageDir) { + // Origin must be initialized before the usage dir is created. + + info("Initializing storage"); + + request = initStorage(); + await requestFinished(request); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Initializing origin"); + + request = initTemporaryOrigin("default", principal); + await requestFinished(request); + + info("Creating usage as a directory"); + + // This will cause a failure during migration. + usageFile.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } + } + + function verifyData() { + ok(dataFile.exists(), "Data file does exist"); + } + + async function verifyUsage(success) { + info("Verifying usage in memory"); + + let request = getOriginUsage(principal, /* fromMemory */ true); + await requestFinished(request); + + if (success) { + is(request.result.usage, data.usage, "Correct usage"); + } else { + is(request.result.usage, 0, "Zero usage"); + } + + info("Verifying usage on disk"); + + if (success) { + ok(!usageJournalFile.exists(), "Usage journal file doesn't exist"); + ok(usageFile.exists(), "Usage file does exist"); + let usage = await readUsageFromUsageFile(usageFile); + is(usage, data.usage, "Correct usage"); + } else { + ok(usageJournalFile.exists(), "Usage journal file does exist"); + ok(usageFile.exists(), "Usage file does exist"); + } + } + + info("Setting prefs"); + + Services.prefs.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); + + info("Stage 1 - Testing usage after successful data migration"); + + await createStorageForMigration(/* createUsageDir */ false); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Opening"); + + storage.open(); + + verifyData(); + + await verifyUsage(/* success */ true); + + info("Stage 2 - Testing usage after unsuccessful data migration"); + + await createStorageForMigration(/* createUsageDir */ true); + + info("Getting storage"); + + storage = getLocalStorage(principal); + + info("Opening"); + + try { + storage.open(); + ok(false, "Should have thrown"); + } catch (ex) { + ok(true, "Did throw"); + } + + verifyData(); + + await verifyUsage(/* success */ false); + + info("Stage 3 - Testing usage after unsuccessful/successful data migration"); + + await createStorageForMigration(/* createUsageDir */ true); + + info("Getting storage"); + + storage = getLocalStorage(principal); + + info("Opening"); + + try { + storage.open(); + ok(false, "Should have thrown"); + } catch (ex) { + ok(true, "Did throw"); + } + + usageFile.remove(true); + + info("Opening"); + + storage.open(); + + verifyData(); + + await verifyUsage(/* success */ true); +}); diff --git a/dom/localstorage/test/unit/usageAfterMigration_profile.zip b/dom/localstorage/test/unit/usageAfterMigration_profile.zip Binary files differnew file mode 100644 index 0000000000..30a73292c3 --- /dev/null +++ b/dom/localstorage/test/unit/usageAfterMigration_profile.zip diff --git a/dom/localstorage/test/unit/xpcshell.ini b/dom/localstorage/test/unit/xpcshell.ini new file mode 100644 index 0000000000..9b86f34349 --- /dev/null +++ b/dom/localstorage/test/unit/xpcshell.ini @@ -0,0 +1,73 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +[DEFAULT] +head = head.js +tags = condprof +support-files = + archive_profile.zip + corruptedDatabase_profile.zip + corruptedDatabase_missingUsageFile_profile.zip + groupMismatch_profile.zip + migration_profile.zip + schema3upgrade_profile.zip + schema4upgrade_profile.zip + stringLength2_profile.zip + stringLength_profile.zip + usageAfterMigration_profile.zip + +[make_migration_emptyValue.js] +skip-if = true # Only used for recreating migration_emptyValue_profile.zip +[test_archive.js] +[test_clientValidation.js] +[test_corruptedDatabase.js] +[test_databaseShadowing1.js] +prefs = + dom.storage.shadow_writes=true +run-sequentially = test_databaseShadowing2.js depends on a file produced by this test +[test_databaseShadowing2.js] +run-sequentially = this test depends on a file produced by test_databaseShadowing1.js +[test_databaseShadowing_clearOrigin1.js] +prefs = + dom.storage.shadow_writes=true +run-sequentially = test_databaseShadowing_clearOrigin2.js depends on a file produced by this test +[test_databaseShadowing_clearOrigin2.js] +run-sequentially = this test depends on a file produced by test_databaseShadowing_clearOrigin1.js +[test_databaseShadowing_clearOriginsByPattern1.js] +prefs = + dom.storage.shadow_writes=true +run-sequentially = test_databaseShadowing_clearOriginsByPattern2.js depends on a file produced by this test +[test_databaseShadowing_clearOriginsByPattern2.js] +run-sequentially = this test depends on a file produced by test_databaseShadowing_clearOriginsByPattern1.js +[test_databaseShadowing_clearOriginsByPrefix1.js] +prefs = + dom.storage.shadow_writes=true +run-sequentially = test_databaseShadowing_clearOriginsByPrefix2.js depends on a file produced by this test +[test_databaseShadowing_clearOriginsByPrefix2.js] +run-sequentially = this test depends on a file produced by test_databaseShadowing_clearOriginsByPrefix1.js +[test_eviction.js] +[test_flushing.js] +[test_groupLimit.js] +[test_groupMismatch.js] +[test_largeItems.js] +[test_lsng_enabled.js] +[test_migration.js] +[test_migration_emptyValue.js] +support-files = + migration_emptyValue_profile.zip +[test_old_lsng_pref.js] +[test_orderingAfterRemoveAdd.js] +[test_originInit.js] +[test_preloading.js] +[test_schema3upgrade.js] +[test_schema4upgrade.js] +[test_snapshotting.js] +skip-if = tsan # Unreasonably slow, bug 1612707 +requesttimeoutfactor = 4 +[test_stringLength.js] +[test_stringLength2.js] +[test_unicodeCharacters.js] +[test_uri_encoding_edge_cases.js] +[test_usage.js] +[test_usageAfterMigration.js] |