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/quota | |
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/quota')
297 files changed, 41868 insertions, 0 deletions
diff --git a/dom/quota/ActorsChild.cpp b/dom/quota/ActorsChild.cpp new file mode 100644 index 0000000000..071d13214c --- /dev/null +++ b/dom/quota/ActorsChild.cpp @@ -0,0 +1,408 @@ +/* -*- 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" + +// Local includes +#include "QuotaManagerService.h" +#include "QuotaRequests.h" +#include "QuotaResults.h" + +// Global includes +#include <new> +#include <utility> +#include "mozilla/Assertions.h" +#include "mozilla/dom/quota/PQuotaRequest.h" +#include "mozilla/dom/quota/PQuotaUsageRequest.h" +#include "nsError.h" +#include "nsID.h" +#include "nsIEventTarget.h" +#include "nsIQuotaResults.h" +#include "nsISupports.h" +#include "nsIVariant.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "nsVariant.h" + +namespace mozilla::dom::quota { + +/******************************************************************************* + * QuotaChild + ******************************************************************************/ + +QuotaChild::QuotaChild(QuotaManagerService* aService) + : mService(aService) +#ifdef DEBUG + , + mOwningThread(GetCurrentEventTarget()) +#endif +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aService); + + MOZ_COUNT_CTOR(quota::QuotaChild); +} + +QuotaChild::~QuotaChild() { + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(quota::QuotaChild); +} + +#ifdef DEBUG + +void QuotaChild::AssertIsOnOwningThread() const { + MOZ_ASSERT(mOwningThread); + + bool current; + MOZ_ASSERT(NS_SUCCEEDED(mOwningThread->IsOnCurrentThread(¤t))); + MOZ_ASSERT(current); +} + +#endif // DEBUG + +void QuotaChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + if (mService) { + mService->ClearBackgroundActor(); +#ifdef DEBUG + mService = nullptr; +#endif + } +} + +PQuotaUsageRequestChild* QuotaChild::AllocPQuotaUsageRequestChild( + const UsageRequestParams& aParams) { + AssertIsOnOwningThread(); + + MOZ_CRASH("PQuotaUsageRequestChild actors should be manually constructed!"); +} + +bool QuotaChild::DeallocPQuotaUsageRequestChild( + PQuotaUsageRequestChild* aActor) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + + delete static_cast<QuotaUsageRequestChild*>(aActor); + return true; +} + +PQuotaRequestChild* QuotaChild::AllocPQuotaRequestChild( + const RequestParams& aParams) { + AssertIsOnOwningThread(); + + MOZ_CRASH("PQuotaRequestChild actors should be manually constructed!"); +} + +bool QuotaChild::DeallocPQuotaRequestChild(PQuotaRequestChild* aActor) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + + delete static_cast<QuotaRequestChild*>(aActor); + return true; +} + +/******************************************************************************* + * QuotaUsageRequestChild + ******************************************************************************/ + +QuotaUsageRequestChild::QuotaUsageRequestChild(UsageRequest* aRequest) + : mRequest(aRequest) { + AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(quota::QuotaUsageRequestChild); +} + +QuotaUsageRequestChild::~QuotaUsageRequestChild() { + // Can't assert owning thread here because the request is cleared. + + MOZ_COUNT_DTOR(quota::QuotaUsageRequestChild); +} + +#ifdef DEBUG + +void QuotaUsageRequestChild::AssertIsOnOwningThread() const { + MOZ_ASSERT(mRequest); + mRequest->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +void QuotaUsageRequestChild::HandleResponse(nsresult aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aResponse)); + MOZ_ASSERT(mRequest); + + mRequest->SetError(aResponse); +} + +void QuotaUsageRequestChild::HandleResponse( + const nsTArray<OriginUsage>& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + RefPtr<nsVariant> variant = new nsVariant(); + + if (aResponse.IsEmpty()) { + variant->SetAsEmptyArray(); + } else { + nsTArray<RefPtr<UsageResult>> usageResults(aResponse.Length()); + + for (const auto& originUsage : aResponse) { + usageResults.AppendElement(MakeRefPtr<UsageResult>( + originUsage.origin(), originUsage.persisted(), originUsage.usage(), + originUsage.lastAccessed())); + } + + variant->SetAsArray(nsIDataType::VTYPE_INTERFACE_IS, + &NS_GET_IID(nsIQuotaUsageResult), usageResults.Length(), + static_cast<void*>(usageResults.Elements())); + } + + mRequest->SetResult(variant); +} + +void QuotaUsageRequestChild::HandleResponse( + const OriginUsageResponse& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + RefPtr<OriginUsageResult> result = + new OriginUsageResult(aResponse.usage(), aResponse.fileUsage()); + + RefPtr<nsVariant> variant = new nsVariant(); + variant->SetAsInterface(NS_GET_IID(nsIQuotaOriginUsageResult), result); + + mRequest->SetResult(variant); +} + +void QuotaUsageRequestChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + if (mRequest) { + mRequest->ClearBackgroundActor(); +#ifdef DEBUG + mRequest = nullptr; +#endif + } +} + +mozilla::ipc::IPCResult QuotaUsageRequestChild::Recv__delete__( + const UsageRequestResponse& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + switch (aResponse.type()) { + case UsageRequestResponse::Tnsresult: + HandleResponse(aResponse.get_nsresult()); + break; + + case UsageRequestResponse::TAllUsageResponse: + HandleResponse(aResponse.get_AllUsageResponse().originUsages()); + break; + + case UsageRequestResponse::TOriginUsageResponse: + HandleResponse(aResponse.get_OriginUsageResponse()); + break; + + default: + MOZ_CRASH("Unknown response type!"); + } + + return IPC_OK(); +} + +/******************************************************************************* + * QuotaRequestChild + ******************************************************************************/ + +QuotaRequestChild::QuotaRequestChild(Request* aRequest) : mRequest(aRequest) { + AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(quota::QuotaRequestChild); +} + +QuotaRequestChild::~QuotaRequestChild() { + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(quota::QuotaRequestChild); +} + +#ifdef DEBUG + +void QuotaRequestChild::AssertIsOnOwningThread() const { + MOZ_ASSERT(mRequest); + mRequest->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +void QuotaRequestChild::HandleResponse(nsresult aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aResponse)); + MOZ_ASSERT(mRequest); + + mRequest->SetError(aResponse); +} + +void QuotaRequestChild::HandleResponse() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + RefPtr<nsVariant> variant = new nsVariant(); + variant->SetAsVoid(); + + mRequest->SetResult(variant); +} + +void QuotaRequestChild::HandleResponse(bool aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + RefPtr<nsVariant> variant = new nsVariant(); + variant->SetAsBool(aResponse); + + mRequest->SetResult(variant); +} + +void QuotaRequestChild::HandleResponse(const nsAString& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + RefPtr<nsVariant> variant = new nsVariant(); + variant->SetAsAString(aResponse); + + mRequest->SetResult(variant); +} + +void QuotaRequestChild::HandleResponse(const EstimateResponse& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + RefPtr<EstimateResult> result = + new EstimateResult(aResponse.usage(), aResponse.limit()); + + RefPtr<nsVariant> variant = new nsVariant(); + variant->SetAsInterface(NS_GET_IID(nsIQuotaEstimateResult), result); + + mRequest->SetResult(variant); +} + +void QuotaRequestChild::HandleResponse(const nsTArray<nsCString>& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + RefPtr<nsVariant> variant = new nsVariant(); + + if (aResponse.IsEmpty()) { + variant->SetAsEmptyArray(); + } else { + nsTArray<const char*> stringPointers(aResponse.Length()); + std::transform(aResponse.cbegin(), aResponse.cend(), + MakeBackInserter(stringPointers), + std::mem_fn(&nsCString::get)); + + variant->SetAsArray(nsIDataType::VTYPE_CHAR_STR, nullptr, + stringPointers.Length(), stringPointers.Elements()); + } + + mRequest->SetResult(variant); +} + +void QuotaRequestChild::HandleResponse( + const GetFullOriginMetadataResponse& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + RefPtr<nsVariant> variant = new nsVariant(); + + if (aResponse.maybeFullOriginMetadata()) { + RefPtr<FullOriginMetadataResult> result = + new FullOriginMetadataResult(*aResponse.maybeFullOriginMetadata()); + + variant->SetAsInterface(NS_GET_IID(nsIQuotaFullOriginMetadataResult), + result); + + } else { + variant->SetAsVoid(); + } + + mRequest->SetResult(variant); +} + +void QuotaRequestChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); +} + +mozilla::ipc::IPCResult QuotaRequestChild::Recv__delete__( + const RequestResponse& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + switch (aResponse.type()) { + case RequestResponse::Tnsresult: + HandleResponse(aResponse.get_nsresult()); + break; + + case RequestResponse::TStorageNameResponse: + HandleResponse(aResponse.get_StorageNameResponse().name()); + break; + + case RequestResponse::TStorageInitializedResponse: + HandleResponse(aResponse.get_StorageInitializedResponse().initialized()); + break; + + case RequestResponse::TTemporaryStorageInitializedResponse: + HandleResponse( + aResponse.get_TemporaryStorageInitializedResponse().initialized()); + break; + + case RequestResponse::TInitResponse: + case RequestResponse::TInitTemporaryStorageResponse: + case RequestResponse::TClearOriginResponse: + case RequestResponse::TResetOriginResponse: + case RequestResponse::TClearDataResponse: + case RequestResponse::TClearAllResponse: + case RequestResponse::TResetAllResponse: + case RequestResponse::TPersistResponse: + HandleResponse(); + break; + + case RequestResponse::TInitializePersistentOriginResponse: + HandleResponse( + aResponse.get_InitializePersistentOriginResponse().created()); + break; + + case RequestResponse::TInitializeTemporaryOriginResponse: + HandleResponse( + aResponse.get_InitializeTemporaryOriginResponse().created()); + break; + + case RequestResponse::TGetFullOriginMetadataResponse: + HandleResponse(aResponse.get_GetFullOriginMetadataResponse()); + break; + + case RequestResponse::TPersistedResponse: + HandleResponse(aResponse.get_PersistedResponse().persisted()); + break; + + case RequestResponse::TEstimateResponse: + HandleResponse(aResponse.get_EstimateResponse()); + break; + + case RequestResponse::TListOriginsResponse: + HandleResponse(aResponse.get_ListOriginsResponse().origins()); + break; + + default: + MOZ_CRASH("Unknown response type!"); + } + + return IPC_OK(); +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/ActorsChild.h b/dom/quota/ActorsChild.h new file mode 100644 index 0000000000..df1881b234 --- /dev/null +++ b/dom/quota/ActorsChild.h @@ -0,0 +1,158 @@ +/* -*- 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_quota_ActorsChild_h +#define mozilla_dom_quota_ActorsChild_h + +#include <cstdint> +#include "ErrorList.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/quota/PQuotaChild.h" +#include "mozilla/dom/quota/PQuotaRequestChild.h" +#include "mozilla/dom/quota/PQuotaUsageRequestChild.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "nsCOMPtr.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +class nsIEventTarget; + +namespace mozilla { +namespace ipc { + +class BackgroundChildImpl; + +} // namespace ipc + +namespace dom::quota { + +class QuotaManagerService; +class Request; +class UsageRequest; + +class QuotaChild final : public PQuotaChild { + friend class mozilla::ipc::BackgroundChildImpl; + friend class QuotaManagerService; + + QuotaManagerService* mService; + +#ifdef DEBUG + nsCOMPtr<nsIEventTarget> mOwningThread; +#endif + + public: + void AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + private: + // Only created by QuotaManagerService. + explicit QuotaChild(QuotaManagerService* aService); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~QuotaChild(); + + // IPDL methods are only called by IPDL. + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + virtual PQuotaUsageRequestChild* AllocPQuotaUsageRequestChild( + const UsageRequestParams& aParams) override; + + virtual bool DeallocPQuotaUsageRequestChild( + PQuotaUsageRequestChild* aActor) override; + + virtual PQuotaRequestChild* AllocPQuotaRequestChild( + const RequestParams& aParams) override; + + virtual bool DeallocPQuotaRequestChild(PQuotaRequestChild* aActor) override; +}; + +class QuotaUsageRequestChild final : public PQuotaUsageRequestChild { + friend class QuotaChild; + friend class QuotaManagerService; + + RefPtr<UsageRequest> mRequest; + + public: + void AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + private: + // Only created by QuotaManagerService. + explicit QuotaUsageRequestChild(UsageRequest* aRequest); + + // Only destroyed by QuotaChild. + ~QuotaUsageRequestChild(); + + void HandleResponse(nsresult aResponse); + + void HandleResponse(const nsTArray<OriginUsage>& aResponse); + + void HandleResponse(const OriginUsageResponse& aResponse); + + // IPDL methods are only called by IPDL. + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + virtual mozilla::ipc::IPCResult Recv__delete__( + const UsageRequestResponse& aResponse) override; +}; + +class QuotaRequestChild final : public PQuotaRequestChild { + friend class QuotaChild; + friend class QuotaManagerService; + + RefPtr<Request> mRequest; + + public: + void AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + private: + // Only created by QuotaManagerService. + explicit QuotaRequestChild(Request* aRequest); + + // Only destroyed by QuotaChild. + ~QuotaRequestChild(); + + void HandleResponse(nsresult aResponse); + + void HandleResponse(); + + void HandleResponse(bool aResponse); + + void HandleResponse(const nsAString& aResponse); + + void HandleResponse(const EstimateResponse& aResponse); + + void HandleResponse(const nsTArray<nsCString>& aResponse); + + void HandleResponse(const GetFullOriginMetadataResponse& aResponse); + + // IPDL methods are only called by IPDL. + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + virtual mozilla::ipc::IPCResult Recv__delete__( + const RequestResponse& aResponse) override; +}; + +} // namespace dom::quota +} // namespace mozilla + +#endif // mozilla_dom_quota_ActorsChild_h diff --git a/dom/quota/ActorsParent.cpp b/dom/quota/ActorsParent.cpp new file mode 100644 index 0000000000..ec1507b05d --- /dev/null +++ b/dom/quota/ActorsParent.cpp @@ -0,0 +1,10856 @@ +/* -*- 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 "CanonicalQuotaObject.h" +#include "ClientUsageArray.h" +#include "Flatten.h" +#include "FirstInitializationAttemptsImpl.h" +#include "GroupInfo.h" +#include "GroupInfoPair.h" +#include "OriginScope.h" +#include "OriginInfo.h" +#include "QuotaCommon.h" +#include "QuotaManager.h" +#include "ScopedLogExtraInfo.h" +#include "UsageInfo.h" + +// Global includes +#include <cinttypes> +#include <cstdlib> +#include <cstring> +#include <algorithm> +#include <cstdint> +#include <functional> +#include <new> +#include <numeric> +#include <tuple> +#include <type_traits> +#include <utility> +#include "DirectoryLockImpl.h" +#include "ErrorList.h" +#include "MainThreadUtils.h" +#include "mozIStorageAsyncConnection.h" +#include "mozIStorageConnection.h" +#include "mozIStorageService.h" +#include "mozIStorageStatement.h" +#include "mozStorageCID.h" +#include "mozStorageHelper.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/CondVar.h" +#include "mozilla/InitializedOnce.h" +#include "mozilla/Logging.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Maybe.h" +#include "mozilla/Mutex.h" +#include "mozilla/NotNull.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Services.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryHistogramEnums.h" +#include "mozilla/TextUtils.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/Variant.h" +#include "mozilla/dom/FileSystemQuotaClient.h" +#include "mozilla/dom/FlippedOnce.h" +#include "mozilla/dom/LocalStorageCommon.h" +#include "mozilla/dom/StorageDBUpdater.h" +#include "mozilla/dom/cache/QuotaClient.h" +#include "mozilla/dom/indexedDB/ActorsParent.h" +#include "mozilla/dom/ipc/IdType.h" +#include "mozilla/dom/localstorage/ActorsParent.h" +#include "mozilla/dom/quota/AssertionsImpl.h" +#include "mozilla/dom/quota/CheckedUnsafePtr.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/DirectoryLock.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/PQuota.h" +#include "mozilla/dom/quota/PQuotaParent.h" +#include "mozilla/dom/quota/PQuotaRequest.h" +#include "mozilla/dom/quota/PQuotaRequestParent.h" +#include "mozilla/dom/quota/PQuotaUsageRequest.h" +#include "mozilla/dom/quota/PQuotaUsageRequestParent.h" +#include "mozilla/dom/quota/QuotaManagerImpl.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/ScopedLogExtraInfo.h" +#include "mozilla/dom/simpledb/ActorsParent.h" +#include "mozilla/fallible.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/net/MozURL.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsBaseHashtable.h" +#include "nsCOMPtr.h" +#include "nsCRTGlue.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsClassHashtable.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsTHashMap.h" +#include "nsDebug.h" +#include "nsDirectoryServiceUtils.h" +#include "nsError.h" +#include "nsHashKeys.h" +#include "nsIBinaryInputStream.h" +#include "nsIBinaryOutputStream.h" +#include "nsIConsoleService.h" +#include "nsIDirectoryEnumerator.h" +#include "nsIEventTarget.h" +#include "nsIFile.h" +#include "nsIFileStreams.h" +#include "nsIInputStream.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIOutputStream.h" +#include "nsIPlatformInfo.h" +#include "nsIPrincipal.h" +#include "nsIRunnable.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsISupports.h" +#include "nsISupportsPrimitives.h" +#include "nsIThread.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsIWidget.h" +#include "nsLiteralString.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsPrintfCString.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsStringFlags.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsTHashtable.h" +#include "nsTLiteralString.h" +#include "nsTPromiseFlatString.h" +#include "nsTStringRepr.h" +#include "nsThreadUtils.h" +#include "nsURLHelper.h" +#include "nsXPCOM.h" +#include "nsXPCOMCID.h" +#include "nsXULAppAPI.h" +#include "prinrval.h" +#include "prio.h" +#include "prthread.h" +#include "prtime.h" + +// As part of bug 1536596 in order to identify the remaining sources of +// principal info inconsistencies, we have added anonymized crash logging and +// are temporarily making these checks occur on both debug and optimized +// nightly, dev-edition, and early beta builds through use of +// EARLY_BETA_OR_EARLIER during Firefox 82. The plan is to return this +// condition to MOZ_DIAGNOSTIC_ASSERT_ENABLED during Firefox 84 at the latest. +// The analysis and disabling is tracked by bug 1536596. + +#ifdef EARLY_BETA_OR_EARLIER +# define QM_PRINCIPALINFO_VERIFICATION_ENABLED +#endif + +// The amount of time, in milliseconds, that our IO thread will stay alive +// after the last event it processes. +#define DEFAULT_THREAD_TIMEOUT_MS 30000 + +/** + * If shutdown takes this long, kill actors of a quota client, to avoid reaching + * the crash timeout. + */ +#define SHUTDOWN_KILL_ACTORS_TIMEOUT_MS 5000 + +/** + * Automatically crash the browser if shutdown of a quota client takes 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 + * shutdown of a quota client takes too long. Also, this value is long enough + * so that testers can notice the timeout; we want to know about the timeouts, + * 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 SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS 45000 + +static_assert( + SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS > SHUTDOWN_KILL_ACTORS_TIMEOUT_MS, + "The kill actors timeout must be shorter than the crash browser one."); + +// profile-before-change, when we need to shut down quota manager +#define PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID "profile-before-change-qm" + +#define KB *1024ULL +#define MB *1024ULL KB +#define GB *1024ULL MB + +namespace mozilla::dom::quota { + +using namespace mozilla::ipc; +using mozilla::net::MozURL; + +// We want profiles to be platform-independent so we always need to replace +// the same characters on every platform. Windows has the most extensive set +// of illegal characters so we use its FILE_ILLEGAL_CHARACTERS and +// FILE_PATH_SEPARATOR. +const char QuotaManager::kReplaceChars[] = CONTROL_CHARACTERS "/:*?\"<>|\\"; +const char16_t QuotaManager::kReplaceChars16[] = + u"" CONTROL_CHARACTERS "/:*?\"<>|\\"; + +namespace { + +/******************************************************************************* + * Constants + ******************************************************************************/ + +const uint32_t kSQLitePageSizeOverride = 512; + +// Important version history: +// - Bug 1290481 bumped our schema from major.minor 2.0 to 3.0 in Firefox 57 +// which caused Firefox 57 release concerns because the major schema upgrade +// means anyone downgrading to Firefox 56 will experience a non-operational +// QuotaManager and all of its clients. +// - Bug 1404344 got very concerned about that and so we decided to effectively +// rename 3.0 to 2.1, effective in Firefox 57. This works because post +// storage.sqlite v1.0, QuotaManager doesn't care about minor storage version +// increases. It also works because all the upgrade did was give the DOM +// Cache API QuotaClient an opportunity to create its newly added .padding +// files during initialization/upgrade, which isn't functionally necessary as +// that can be done on demand. + +// Major storage version. Bump for backwards-incompatible changes. +// (The next major version should be 4 to distinguish from the Bug 1290481 +// downgrade snafu.) +const uint32_t kMajorStorageVersion = 2; + +// Minor storage version. Bump for backwards-compatible changes. +const uint32_t kMinorStorageVersion = 3; + +// The storage version we store in the SQLite database is a (signed) 32-bit +// integer. The major version is left-shifted 16 bits so the max value is +// 0xFFFF. The minor version occupies the lower 16 bits and its max is 0xFFFF. +static_assert(kMajorStorageVersion <= 0xFFFF, + "Major version needs to fit in 16 bits."); +static_assert(kMinorStorageVersion <= 0xFFFF, + "Minor version needs to fit in 16 bits."); + +const int32_t kStorageVersion = + int32_t((kMajorStorageVersion << 16) + kMinorStorageVersion); + +// See comments above about why these are a thing. +const int32_t kHackyPreDowngradeStorageVersion = int32_t((3 << 16) + 0); +const int32_t kHackyPostDowngradeStorageVersion = int32_t((2 << 16) + 1); + +const char kChromeOrigin[] = "chrome"; +const char kAboutHomeOriginPrefix[] = "moz-safe-about:home"; +const char kIndexedDBOriginPrefix[] = "indexeddb://"; +const char kResourceOriginPrefix[] = "resource://"; + +constexpr auto kStorageName = u"storage"_ns; +constexpr auto kSQLiteSuffix = u".sqlite"_ns; + +#define INDEXEDDB_DIRECTORY_NAME u"indexedDB" +#define ARCHIVES_DIRECTORY_NAME u"archives" +#define PERSISTENT_DIRECTORY_NAME u"persistent" +#define PERMANENT_DIRECTORY_NAME u"permanent" +#define TEMPORARY_DIRECTORY_NAME u"temporary" +#define DEFAULT_DIRECTORY_NAME u"default" + +// The name of the file that we use to load/save the last access time of an +// origin. +// XXX We should get rid of old metadata files at some point, bug 1343576. +#define METADATA_FILE_NAME u".metadata" +#define METADATA_TMP_FILE_NAME u".metadata-tmp" +#define METADATA_V2_FILE_NAME u".metadata-v2" +#define METADATA_V2_TMP_FILE_NAME u".metadata-v2-tmp" + +#define WEB_APPS_STORE_FILE_NAME u"webappsstore.sqlite" +#define LS_ARCHIVE_FILE_NAME u"ls-archive.sqlite" +#define LS_ARCHIVE_TMP_FILE_NAME u"ls-archive-tmp.sqlite" + +const int32_t kLocalStorageArchiveVersion = 4; + +const char kProfileDoChangeTopic[] = "profile-do-change"; + +const int32_t kCacheVersion = 2; + +/****************************************************************************** + * SQLite functions + ******************************************************************************/ + +int32_t MakeStorageVersion(uint32_t aMajorStorageVersion, + uint32_t aMinorStorageVersion) { + return int32_t((aMajorStorageVersion << 16) + aMinorStorageVersion); +} + +uint32_t GetMajorStorageVersion(int32_t aStorageVersion) { + return uint32_t(aStorageVersion >> 16); +} + +nsresult CreateTables(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // Table `database` + QM_TRY(MOZ_TO_RESULT( + aConnection->ExecuteSimpleSQL("CREATE TABLE database" + "( cache_version INTEGER NOT NULL DEFAULT 0" + ");"_ns))); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& storageVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion)); + + MOZ_ASSERT(storageVersion == 0); + } +#endif + + QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(kStorageVersion))); + + return NS_OK; +} + +Result<int32_t, nsresult> LoadCacheVersion(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + aConnection, "SELECT cache_version FROM database"_ns)); + + QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0)); +} + +nsresult SaveCacheVersion(mozIStorageConnection& aConnection, + int32_t aVersion) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement, + "UPDATE database SET cache_version = :version;"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("version"_ns, aVersion))); + + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + + return NS_OK; +} + +nsresult CreateCacheTables(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + // Table `cache` + QM_TRY(MOZ_TO_RESULT( + aConnection.ExecuteSimpleSQL("CREATE TABLE cache" + "( valid INTEGER NOT NULL DEFAULT 0" + ", build_id TEXT NOT NULL DEFAULT ''" + ");"_ns))); + + // Table `repository` + QM_TRY( + MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL("CREATE TABLE repository" + "( id INTEGER PRIMARY KEY" + ", name TEXT NOT NULL" + ");"_ns))); + + // Table `origin` + QM_TRY(MOZ_TO_RESULT( + aConnection.ExecuteSimpleSQL("CREATE TABLE origin" + "( repository_id INTEGER NOT NULL" + ", suffix TEXT" + ", group_ TEXT NOT NULL" + ", origin TEXT NOT NULL" + ", client_usages TEXT NOT NULL" + ", usage INTEGER NOT NULL" + ", last_access_time INTEGER NOT NULL" + ", accessed INTEGER NOT NULL" + ", persisted INTEGER NOT NULL" + ", PRIMARY KEY (repository_id, origin)" + ", FOREIGN KEY (repository_id) " + "REFERENCES repository(id) " + ");"_ns))); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& cacheVersion, LoadCacheVersion(aConnection)); + MOZ_ASSERT(cacheVersion == 0); + } +#endif + + QM_TRY(MOZ_TO_RESULT(SaveCacheVersion(aConnection, kCacheVersion))); + + return NS_OK; +} + +OkOrErr InvalidateCache(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + static constexpr auto kDeleteCacheQuery = "DELETE FROM origin;"_ns; + static constexpr auto kSetInvalidFlagQuery = "UPDATE cache SET valid = 0"_ns; + + QM_TRY(QM_OR_ELSE_WARN( + // Expression. + ([&]() -> OkOrErr { + mozStorageTransaction transaction(&aConnection, + /*aCommitOnComplete */ false); + + QM_TRY(QM_TO_RESULT(transaction.Start())); + QM_TRY(QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kDeleteCacheQuery))); + QM_TRY( + QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kSetInvalidFlagQuery))); + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return Ok{}; + }()), + // Fallback. + ([&](const QMResult& rv) -> OkOrErr { + QM_TRY( + QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kSetInvalidFlagQuery))); + + return Ok{}; + }))); + + return Ok{}; +} + +nsresult UpgradeCacheFrom1To2(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL( + "ALTER TABLE origin ADD COLUMN suffix TEXT"_ns))); + + QM_TRY(InvalidateCache(aConnection)); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& cacheVersion, LoadCacheVersion(aConnection)); + + MOZ_ASSERT(cacheVersion == 1); + } +#endif + + QM_TRY(MOZ_TO_RESULT(SaveCacheVersion(aConnection, 2))); + + return NS_OK; +} + +Result<bool, nsresult> MaybeCreateOrUpgradeCache( + mozIStorageConnection& aConnection) { + bool cacheUsable = true; + + QM_TRY_UNWRAP(int32_t cacheVersion, LoadCacheVersion(aConnection)); + + if (cacheVersion > kCacheVersion) { + cacheUsable = false; + } else if (cacheVersion != kCacheVersion) { + const bool newCache = !cacheVersion; + + mozStorageTransaction transaction( + &aConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(transaction.Start())); + + if (newCache) { + QM_TRY(MOZ_TO_RESULT(CreateCacheTables(aConnection))); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& cacheVersion, + LoadCacheVersion(aConnection)); + MOZ_ASSERT(cacheVersion == kCacheVersion); + } +#endif + + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL( + nsLiteralCString("INSERT INTO cache (valid, build_id) " + "VALUES (0, '')")))); + + nsCOMPtr<mozIStorageStatement> insertStmt; + + for (const PersistenceType persistenceType : kAllPersistenceTypes) { + if (insertStmt) { + MOZ_ALWAYS_SUCCEEDS(insertStmt->Reset()); + } else { + QM_TRY_UNWRAP(insertStmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, + aConnection, CreateStatement, + "INSERT INTO repository (id, name) " + "VALUES (:id, :name)"_ns)); + } + + QM_TRY(MOZ_TO_RESULT( + insertStmt->BindInt32ByName("id"_ns, persistenceType))); + + QM_TRY(MOZ_TO_RESULT(insertStmt->BindUTF8StringByName( + "name"_ns, PersistenceTypeToString(persistenceType)))); + + QM_TRY(MOZ_TO_RESULT(insertStmt->Execute())); + } + } else { + // This logic needs to change next time we change the cache! + static_assert(kCacheVersion == 2, + "Upgrade function needed due to cache version increase."); + + while (cacheVersion != kCacheVersion) { + if (cacheVersion == 1) { + QM_TRY(MOZ_TO_RESULT(UpgradeCacheFrom1To2(aConnection))); + } else { + QM_FAIL(Err(NS_ERROR_FAILURE), []() { + QM_WARNING( + "Unable to initialize cache, no upgrade path is " + "available!"); + }); + } + + QM_TRY_UNWRAP(cacheVersion, LoadCacheVersion(aConnection)); + } + + MOZ_ASSERT(cacheVersion == kCacheVersion); + } + + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + } + + return cacheUsable; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateWebAppsStoreConnection( + nsIFile& aWebAppsStoreFile, mozIStorageService& aStorageService) { + AssertIsOnIOThread(); + + // Check if the old database exists at all. + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aWebAppsStoreFile, Exists)); + + if (!exists) { + // webappsstore.sqlite doesn't exist, return a null connection. + return nsCOMPtr<mozIStorageConnection>{}; + } + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(aWebAppsStoreFile, IsDirectory)); + + if (isDirectory) { + QM_WARNING("webappsstore.sqlite is not a file!"); + return nsCOMPtr<mozIStorageConnection>{}; + } + + QM_TRY_INSPECT(const auto& connection, + QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, aStorageService, + OpenUnsharedDatabase, &aWebAppsStoreFile, + mozIStorageService::CONNECTION_DEFAULT), + // Predicate. + IsDatabaseCorruptionError, + // Fallback. Don't throw an error, leave a corrupted + // webappsstore database as it is. + ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>)); + + if (connection) { + // Don't propagate an error, leave a non-updateable webappsstore database as + // it is. + QM_TRY(MOZ_TO_RESULT(StorageDBUpdater::Update(connection)), + nsCOMPtr<mozIStorageConnection>{}); + } + + return connection; +} + +Result<nsCOMPtr<nsIFile>, QMResult> GetLocalStorageArchiveFile( + const nsAString& aDirectoryPath) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aDirectoryPath.IsEmpty()); + + QM_TRY_UNWRAP(auto lsArchiveFile, + QM_TO_RESULT_TRANSFORM(QM_NewLocalFile(aDirectoryPath))); + + QM_TRY(QM_TO_RESULT( + lsArchiveFile->Append(nsLiteralString(LS_ARCHIVE_FILE_NAME)))); + + return lsArchiveFile; +} + +Result<nsCOMPtr<nsIFile>, nsresult> GetLocalStorageArchiveTmpFile( + const nsAString& aDirectoryPath) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aDirectoryPath.IsEmpty()); + + QM_TRY_UNWRAP(auto lsArchiveTmpFile, QM_NewLocalFile(aDirectoryPath)); + + QM_TRY(MOZ_TO_RESULT( + lsArchiveTmpFile->Append(nsLiteralString(LS_ARCHIVE_TMP_FILE_NAME)))); + + return lsArchiveTmpFile; +} + +Result<bool, nsresult> IsLocalStorageArchiveInitialized( + mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + QM_TRY_RETURN( + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, TableExists, "database"_ns)); +} + +nsresult InitializeLocalStorageArchive(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const auto& initialized, + IsLocalStorageArchiveInitialized(*aConnection)); + MOZ_ASSERT(!initialized); + } +#endif + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + "CREATE TABLE database(version INTEGER NOT NULL DEFAULT 0);"_ns))); + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement, + "INSERT INTO database (version) VALUES (:version)"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("version"_ns, 0))); + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + + return NS_OK; +} + +Result<int32_t, nsresult> LoadLocalStorageArchiveVersion( + mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + aConnection, "SELECT version FROM database"_ns)); + + QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0)); +} + +nsresult SaveLocalStorageArchiveVersion(mozIStorageConnection* aConnection, + int32_t aVersion) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = aConnection->CreateStatement( + "UPDATE database SET version = :version;"_ns, getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt32ByName("version"_ns, aVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +template <typename FileFunc, typename DirectoryFunc> +Result<mozilla::Ok, nsresult> CollectEachFileEntry( + nsIFile& aDirectory, const FileFunc& aFileFunc, + const DirectoryFunc& aDirectoryFunc) { + AssertIsOnIOThread(); + + return CollectEachFile( + aDirectory, + [&aFileFunc, &aDirectoryFunc]( + const nsCOMPtr<nsIFile>& file) -> Result<mozilla::Ok, nsresult> { + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: + return aDirectoryFunc(file); + + case nsIFileKind::ExistsAsFile: + return aFileFunc(file); + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return Ok{}; + }); +} + +/****************************************************************************** + * Quota manager class declarations + ******************************************************************************/ + +} // namespace + +class QuotaManager::Observer final : public nsIObserver { + static Observer* sInstance; + + bool mPendingProfileChange; + bool mShutdownComplete; + + public: + static nsresult Initialize(); + + static nsIObserver* GetInstance(); + + static void ShutdownCompleted(); + + private: + Observer() : mPendingProfileChange(false), mShutdownComplete(false) { + MOZ_ASSERT(NS_IsMainThread()); + } + + ~Observer() { MOZ_ASSERT(NS_IsMainThread()); } + + nsresult Init(); + + nsresult Shutdown(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER +}; + +namespace { + +/******************************************************************************* + * Local class declarations + ******************************************************************************/ + +} // namespace + +namespace { + +class CollectOriginsHelper final : public Runnable { + uint64_t mMinSizeToBeFreed; + + Mutex& mMutex; + CondVar mCondVar; + + // The members below are protected by mMutex. + nsTArray<RefPtr<OriginDirectoryLock>> mLocks; + uint64_t mSizeToBeFreed; + bool mWaiting; + + public: + CollectOriginsHelper(mozilla::Mutex& aMutex, uint64_t aMinSizeToBeFreed); + + // Blocks the current thread until origins are collected on the main thread. + // The returned value contains an aggregate size of those origins. + int64_t BlockAndReturnOriginsForEviction( + nsTArray<RefPtr<OriginDirectoryLock>>& aLocks); + + private: + ~CollectOriginsHelper() = default; + + NS_IMETHOD + Run() override; +}; + +class OriginOperationBase : public BackgroundThreadObject, public Runnable { + protected: + nsresult mResultCode; + + enum State { + // Not yet run. + State_Initial, + + // Running on the owning thread in the listener for OpenDirectory. + State_DirectoryOpenPending, + + // Running on the IO thread. + State_DirectoryWorkOpen, + + // Running on the owning thread after all work is done. + State_UnblockingOpen, + + // All done. + State_Complete + }; + + private: + State mState; + bool mActorDestroyed; + + protected: + bool mNeedsQuotaManagerInit; + bool mNeedsStorageInit; + + public: + void NoteActorDestroyed() { + AssertIsOnOwningThread(); + + mActorDestroyed = true; + } + + bool IsActorDestroyed() const { + AssertIsOnOwningThread(); + + return mActorDestroyed; + } + + protected: + explicit OriginOperationBase(nsISerialEventTarget* aOwningThread, + const char* aRunnableName) + : BackgroundThreadObject(aOwningThread), + Runnable(aRunnableName), + mResultCode(NS_OK), + mState(State_Initial), + mActorDestroyed(false), + mNeedsQuotaManagerInit(false), + mNeedsStorageInit(false) {} + + // Reference counted. + virtual ~OriginOperationBase() { + MOZ_ASSERT(mState == State_Complete); + MOZ_ASSERT(mActorDestroyed); + } + +#ifdef DEBUG + State GetState() const { return mState; } +#endif + + void SetState(State aState) { + MOZ_ASSERT(mState == State_Initial); + mState = aState; + } + + void AdvanceState() { + switch (mState) { + case State_Initial: + mState = State_DirectoryOpenPending; + return; + case State_DirectoryOpenPending: + mState = State_DirectoryWorkOpen; + return; + case State_DirectoryWorkOpen: + mState = State_UnblockingOpen; + return; + case State_UnblockingOpen: + mState = State_Complete; + return; + default: + MOZ_CRASH("Bad state!"); + } + } + + NS_IMETHOD + Run() override; + + virtual void Open() = 0; + +#ifdef DEBUG + virtual nsresult DirectoryOpen(); +#else + nsresult DirectoryOpen(); +#endif + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) = 0; + + void Finish(nsresult aResult); + + virtual void UnblockOpen() = 0; + + private: + nsresult Init(); + + nsresult FinishInit(); + + nsresult DirectoryWork(); +}; + +class FinalizeOriginEvictionOp : public OriginOperationBase { + nsTArray<RefPtr<OriginDirectoryLock>> mLocks; + + public: + FinalizeOriginEvictionOp(nsISerialEventTarget* aBackgroundThread, + nsTArray<RefPtr<OriginDirectoryLock>>&& aLocks) + : OriginOperationBase(aBackgroundThread, + "dom::quota::FinalizeOriginEvictionOp"), + mLocks(std::move(aLocks)) { + MOZ_ASSERT(!NS_IsMainThread()); + } + + void Dispatch(); + + void RunOnIOThreadImmediately(); + + private: + ~FinalizeOriginEvictionOp() = default; + + virtual void Open() override; + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + virtual void UnblockOpen() override; +}; + +class NormalOriginOperationBase + : public OriginOperationBase, + public OpenDirectoryListener, + public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + protected: + OriginScope mOriginScope; + RefPtr<DirectoryLock> mDirectoryLock; + Nullable<PersistenceType> mPersistenceType; + Nullable<Client::Type> mClientType; + mozilla::Atomic<bool> mCanceled; + const bool mExclusive; + + public: + void RunImmediately() { + MOZ_ASSERT(GetState() == State_Initial); + + MOZ_ALWAYS_SUCCEEDS(this->Run()); + } + + protected: + NormalOriginOperationBase(const char* aRunnableName, + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, bool aExclusive) + : OriginOperationBase(GetCurrentSerialEventTarget(), aRunnableName), + mOriginScope(aOriginScope), + mPersistenceType(aPersistenceType), + mExclusive(aExclusive) { + AssertIsOnOwningThread(); + } + + ~NormalOriginOperationBase() = default; + + virtual RefPtr<DirectoryLock> CreateDirectoryLock(); + + private: + // Need to declare refcounting unconditionally, because + // OpenDirectoryListener has pure-virtual refcounting. + NS_DECL_ISUPPORTS_INHERITED + + virtual void Open() override; + + virtual void UnblockOpen() override; + + // OpenDirectoryListener overrides. + virtual void DirectoryLockAcquired(DirectoryLock* aLock) override; + + virtual void DirectoryLockFailed() override; + + // Used to send results before unblocking open. + virtual void SendResults() = 0; +}; + +class SaveOriginAccessTimeOp : public NormalOriginOperationBase { + int64_t mTimestamp; + + public: + SaveOriginAccessTimeOp(PersistenceType aPersistenceType, + const nsACString& aOrigin, int64_t aTimestamp) + : NormalOriginOperationBase("dom::quota::SaveOriginAccessTimeOp", + Nullable<PersistenceType>(aPersistenceType), + OriginScope::FromOrigin(aOrigin), + /* aExclusive */ false), + mTimestamp(aTimestamp) { + AssertIsOnOwningThread(); + } + + private: + ~SaveOriginAccessTimeOp() = default; + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + virtual void SendResults() override; +}; + +class ShutdownStorageOp : public NormalOriginOperationBase { + MozPromiseHolder<BoolPromise> mPromiseHolder; + + public: + ShutdownStorageOp() + : NormalOriginOperationBase("dom::quota::ShutdownStorageOp", + Nullable<PersistenceType>(), + OriginScope::FromNull(), + /* aExclusive */ true) { + AssertIsOnOwningThread(); + } + + RefPtr<BoolPromise> OnResults() { + AssertIsOnOwningThread(); + + return mPromiseHolder.Ensure(__func__); + } + + private: + ~ShutdownStorageOp() = default; + +#ifdef DEBUG + nsresult DirectoryOpen() override; +#endif + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void SendResults() override; +}; + +/******************************************************************************* + * Actor class declarations + ******************************************************************************/ + +class Quota final : public PQuotaParent { +#ifdef DEBUG + bool mActorDestroyed; +#endif + + public: + Quota(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::quota::Quota) + + private: + ~Quota(); + + bool VerifyRequestParams(const UsageRequestParams& aParams) const; + + bool VerifyRequestParams(const RequestParams& aParams) const; + + // IPDL methods. + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + virtual PQuotaUsageRequestParent* AllocPQuotaUsageRequestParent( + const UsageRequestParams& aParams) override; + + virtual mozilla::ipc::IPCResult RecvPQuotaUsageRequestConstructor( + PQuotaUsageRequestParent* aActor, + const UsageRequestParams& aParams) override; + + virtual bool DeallocPQuotaUsageRequestParent( + PQuotaUsageRequestParent* aActor) override; + + virtual PQuotaRequestParent* AllocPQuotaRequestParent( + const RequestParams& aParams) override; + + virtual mozilla::ipc::IPCResult RecvPQuotaRequestConstructor( + PQuotaRequestParent* aActor, const RequestParams& aParams) override; + + virtual bool DeallocPQuotaRequestParent(PQuotaRequestParent* aActor) override; + + virtual mozilla::ipc::IPCResult RecvStartIdleMaintenance() override; + + virtual mozilla::ipc::IPCResult RecvStopIdleMaintenance() override; + + virtual mozilla::ipc::IPCResult RecvAbortOperationsForProcess( + const ContentParentId& aContentParentId) override; +}; + +class QuotaUsageRequestBase : public NormalOriginOperationBase, + public PQuotaUsageRequestParent { + public: + // May be overridden by subclasses if they need to perform work on the + // background thread before being run. + virtual void Init(Quota& aQuota); + + protected: + QuotaUsageRequestBase(const char* aRunnableName) + : NormalOriginOperationBase(aRunnableName, Nullable<PersistenceType>(), + OriginScope::FromNull(), + /* aExclusive */ false) {} + + mozilla::Result<UsageInfo, nsresult> GetUsageForOrigin( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata); + + // Subclasses use this override to set the IPDL response value. + virtual void GetResponse(UsageRequestResponse& aResponse) = 0; + + private: + mozilla::Result<UsageInfo, nsresult> GetUsageForOriginEntries( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, nsIFile& aDirectory, + bool aInitialized); + + void SendResults() override; + + // IPDL methods. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvCancel() final; +}; + +// A mix-in class to simplify operations that need to process every origin in +// one or more repositories. Sub-classes should call TraverseRepository in their +// DoDirectoryWork and implement a ProcessOrigin method for their per-origin +// logic. +class TraverseRepositoryHelper { + public: + TraverseRepositoryHelper() = default; + + protected: + virtual ~TraverseRepositoryHelper() = default; + + // If ProcessOrigin returns an error, TraverseRepository will immediately + // terminate and return the received error code to its caller. + nsresult TraverseRepository(QuotaManager& aQuotaManager, + PersistenceType aPersistenceType); + + private: + virtual const Atomic<bool>& GetIsCanceledFlag() = 0; + + virtual nsresult ProcessOrigin(QuotaManager& aQuotaManager, + nsIFile& aOriginDir, const bool aPersistent, + const PersistenceType aPersistenceType) = 0; +}; + +class GetUsageOp final : public QuotaUsageRequestBase, + public TraverseRepositoryHelper { + nsTArray<OriginUsage> mOriginUsages; + nsTHashMap<nsCStringHashKey, uint32_t> mOriginUsagesIndex; + + bool mGetAll; + + public: + explicit GetUsageOp(const UsageRequestParams& aParams); + + private: + ~GetUsageOp() = default; + + void ProcessOriginInternal(QuotaManager* aQuotaManager, + const PersistenceType aPersistenceType, + const nsACString& aOrigin, + const int64_t aTimestamp, const bool aPersisted, + const uint64_t aUsage); + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + const Atomic<bool>& GetIsCanceledFlag() override; + + nsresult ProcessOrigin(QuotaManager& aQuotaManager, nsIFile& aOriginDir, + const bool aPersistent, + const PersistenceType aPersistenceType) override; + + void GetResponse(UsageRequestResponse& aResponse) override; +}; + +class GetOriginUsageOp final : public QuotaUsageRequestBase { + nsCString mSuffix; + nsCString mGroup; + uint64_t mUsage; + uint64_t mFileUsage; + bool mFromMemory; + + public: + explicit GetOriginUsageOp(const UsageRequestParams& aParams); + + private: + ~GetOriginUsageOp() = default; + + RefPtr<DirectoryLock> CreateDirectoryLock() override; + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(UsageRequestResponse& aResponse) override; +}; + +class QuotaRequestBase : public NormalOriginOperationBase, + public PQuotaRequestParent { + public: + // May be overridden by subclasses if they need to perform work on the + // background thread before being run. + virtual void Init(Quota& aQuota); + + protected: + explicit QuotaRequestBase(const char* aRunnableName, bool aExclusive) + : NormalOriginOperationBase(aRunnableName, Nullable<PersistenceType>(), + OriginScope::FromNull(), aExclusive) {} + + // Subclasses use this override to set the IPDL response value. + virtual void GetResponse(RequestResponse& aResponse) = 0; + + private: + virtual void SendResults() override; + + // IPDL methods. + virtual void ActorDestroy(ActorDestroyReason aWhy) override; +}; + +class StorageNameOp final : public QuotaRequestBase { + nsString mName; + + public: + StorageNameOp(); + + void Init(Quota& aQuota) override; + + private: + ~StorageNameOp() = default; + + RefPtr<DirectoryLock> CreateDirectoryLock() override; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitializedRequestBase : public QuotaRequestBase { + protected: + bool mInitialized; + + public: + void Init(Quota& aQuota) override; + + protected: + InitializedRequestBase(const char* aRunnableName); + + private: + RefPtr<DirectoryLock> CreateDirectoryLock() override; +}; + +class StorageInitializedOp final : public InitializedRequestBase { + public: + StorageInitializedOp() + : InitializedRequestBase("dom::quota::StorageInitializedOp") {} + + private: + ~StorageInitializedOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class TemporaryStorageInitializedOp final : public InitializedRequestBase { + public: + TemporaryStorageInitializedOp() + : InitializedRequestBase("dom::quota::StorageInitializedOp") {} + + private: + ~TemporaryStorageInitializedOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitOp final : public QuotaRequestBase { + public: + InitOp(); + + void Init(Quota& aQuota) override; + + private: + ~InitOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitTemporaryStorageOp final : public QuotaRequestBase { + public: + InitTemporaryStorageOp(); + + void Init(Quota& aQuota) override; + + private: + ~InitTemporaryStorageOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitializeOriginRequestBase : public QuotaRequestBase { + protected: + nsCString mSuffix; + nsCString mGroup; + bool mCreated; + + public: + void Init(Quota& aQuota) override; + + protected: + InitializeOriginRequestBase(const char* aRunnableName, + PersistenceType aPersistenceType, + const PrincipalInfo& aPrincipalInfo); +}; + +class InitializePersistentOriginOp final : public InitializeOriginRequestBase { + public: + explicit InitializePersistentOriginOp(const RequestParams& aParams); + + private: + ~InitializePersistentOriginOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitializeTemporaryOriginOp final : public InitializeOriginRequestBase { + public: + explicit InitializeTemporaryOriginOp(const RequestParams& aParams); + + private: + ~InitializeTemporaryOriginOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class GetFullOriginMetadataOp : public QuotaRequestBase { + const OriginMetadata mOriginMetadata; + Maybe<FullOriginMetadata> mMaybeFullOriginMetadata; + + public: + explicit GetFullOriginMetadataOp(const GetFullOriginMetadataParams& aParams); + + private: + RefPtr<DirectoryLock> CreateDirectoryLock() override; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ResetOrClearOp final : public QuotaRequestBase { + const bool mClear; + + public: + explicit ResetOrClearOp(bool aClear); + + void Init(Quota& aQuota) override; + + private: + ~ResetOrClearOp() = default; + + void DeleteFiles(QuotaManager& aQuotaManager); + + void DeleteStorageFile(QuotaManager& aQuotaManager); + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + virtual void GetResponse(RequestResponse& aResponse) override; +}; + +class ClearRequestBase : public QuotaRequestBase { + protected: + explicit ClearRequestBase(const char* aRunnableName, bool aExclusive) + : QuotaRequestBase(aRunnableName, aExclusive) { + AssertIsOnOwningThread(); + } + + void DeleteFiles(QuotaManager& aQuotaManager, + PersistenceType aPersistenceType); + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; +}; + +class ClearOriginOp final : public ClearRequestBase { + const ClearResetOriginParams mParams; + const bool mMatchAll; + + public: + explicit ClearOriginOp(const RequestParams& aParams); + + void Init(Quota& aQuota) override; + + private: + ~ClearOriginOp() = default; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ClearDataOp final : public ClearRequestBase { + const ClearDataParams mParams; + + public: + explicit ClearDataOp(const RequestParams& aParams); + + void Init(Quota& aQuota) override; + + private: + ~ClearDataOp() = default; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ResetOriginOp final : public QuotaRequestBase { + public: + explicit ResetOriginOp(const RequestParams& aParams); + + void Init(Quota& aQuota) override; + + private: + ~ResetOriginOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class PersistRequestBase : public QuotaRequestBase { + const PrincipalInfo mPrincipalInfo; + + protected: + nsCString mSuffix; + nsCString mGroup; + + public: + void Init(Quota& aQuota) override; + + protected: + explicit PersistRequestBase(const PrincipalInfo& aPrincipalInfo); +}; + +class PersistedOp final : public PersistRequestBase { + bool mPersisted; + + public: + explicit PersistedOp(const RequestParams& aParams); + + private: + ~PersistedOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class PersistOp final : public PersistRequestBase { + public: + explicit PersistOp(const RequestParams& aParams); + + private: + ~PersistOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class EstimateOp final : public QuotaRequestBase { + const OriginMetadata mOriginMetadata; + std::pair<uint64_t, uint64_t> mUsageAndLimit; + + public: + explicit EstimateOp(const EstimateParams& aParams); + + private: + ~EstimateOp() = default; + + RefPtr<DirectoryLock> CreateDirectoryLock() override; + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ListOriginsOp final : public QuotaRequestBase, + public TraverseRepositoryHelper { + // XXX Bug 1521541 will make each origin has it's own state. + nsTArray<nsCString> mOrigins; + + public: + ListOriginsOp(); + + void Init(Quota& aQuota) override; + + private: + ~ListOriginsOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + const Atomic<bool>& GetIsCanceledFlag() override; + + nsresult ProcessOrigin(QuotaManager& aQuotaManager, nsIFile& aOriginDir, + const bool aPersistent, + const PersistenceType aPersistenceType) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +/******************************************************************************* + * Other class declarations + ******************************************************************************/ + +class StoragePressureRunnable final : public Runnable { + const uint64_t mUsage; + + public: + explicit StoragePressureRunnable(uint64_t aUsage) + : Runnable("dom::quota::QuotaObject::StoragePressureRunnable"), + mUsage(aUsage) {} + + private: + ~StoragePressureRunnable() = default; + + NS_DECL_NSIRUNNABLE +}; + +class RecordQuotaInfoLoadTimeHelper final : public Runnable { + // TimeStamps that are set on the IO thread. + LazyInitializedOnceNotNull<const TimeStamp> mStartTime; + LazyInitializedOnceNotNull<const TimeStamp> mEndTime; + + // A TimeStamp that is set on the main thread. + LazyInitializedOnceNotNull<const TimeStamp> mInitializedTime; + + public: + RecordQuotaInfoLoadTimeHelper() + : Runnable("dom::quota::RecordQuotaInfoLoadTimeHelper") {} + + TimeStamp Start(); + + TimeStamp End(); + + private: + ~RecordQuotaInfoLoadTimeHelper() = default; + + NS_DECL_NSIRUNNABLE +}; + +/******************************************************************************* + * Helper classes + ******************************************************************************/ + +#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED + +class PrincipalVerifier final : public Runnable { + nsTArray<PrincipalInfo> mPrincipalInfos; + + public: + static already_AddRefed<PrincipalVerifier> CreateAndDispatch( + nsTArray<PrincipalInfo>&& aPrincipalInfos); + + private: + explicit PrincipalVerifier(nsTArray<PrincipalInfo>&& aPrincipalInfos) + : Runnable("dom::quota::PrincipalVerifier"), + mPrincipalInfos(std::move(aPrincipalInfos)) { + AssertIsOnIOThread(); + } + + virtual ~PrincipalVerifier() = default; + + Result<Ok, nsCString> CheckPrincipalInfoValidity( + const PrincipalInfo& aPrincipalInfo); + + NS_DECL_NSIRUNNABLE +}; + +#endif + +/******************************************************************************* + * Helper Functions + ******************************************************************************/ + +inline bool IsDotFile(const nsAString& aFileName) { + return QuotaManager::IsDotFile(aFileName); +} + +inline bool IsOSMetadata(const nsAString& aFileName) { + return QuotaManager::IsOSMetadata(aFileName); +} + +bool IsOriginMetadata(const nsAString& aFileName) { + return aFileName.EqualsLiteral(METADATA_FILE_NAME) || + aFileName.EqualsLiteral(METADATA_V2_FILE_NAME) || + IsOSMetadata(aFileName); +} + +bool IsTempMetadata(const nsAString& aFileName) { + return aFileName.EqualsLiteral(METADATA_TMP_FILE_NAME) || + aFileName.EqualsLiteral(METADATA_V2_TMP_FILE_NAME); +} + +// Return whether the group was actually updated. +Result<bool, nsresult> MaybeUpdateGroupForOrigin( + OriginMetadata& aOriginMetadata) { + MOZ_ASSERT(!NS_IsMainThread()); + + bool updated = false; + + if (aOriginMetadata.mOrigin.EqualsLiteral(kChromeOrigin)) { + if (!aOriginMetadata.mGroup.EqualsLiteral(kChromeOrigin)) { + aOriginMetadata.mGroup.AssignLiteral(kChromeOrigin); + updated = true; + } + } else { + OriginAttributes originAttributes; + nsCString originNoSuffix; + QM_TRY(OkIf(originAttributes.PopulateFromOrigin(aOriginMetadata.mOrigin, + originNoSuffix)), + Err(NS_ERROR_FAILURE)); + + RefPtr<MozURL> url; + QM_TRY(MOZ_TO_RESULT(MozURL::Init(getter_AddRefs(url), originNoSuffix)), + QM_PROPAGATE, [&originNoSuffix](const nsresult) { + QM_WARNING("A URL %s is not recognized by MozURL", + originNoSuffix.get()); + }); + + QM_TRY_INSPECT( + const auto& baseDomain, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, *url, BaseDomain)); + + const nsCString upToDateGroup = baseDomain + aOriginMetadata.mSuffix; + + if (aOriginMetadata.mGroup != upToDateGroup) { + aOriginMetadata.mGroup = upToDateGroup; + updated = true; + +#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED + ContentPrincipalInfo contentPrincipalInfo; + contentPrincipalInfo.attrs() = originAttributes; + contentPrincipalInfo.originNoSuffix() = originNoSuffix; + contentPrincipalInfo.spec() = originNoSuffix; + contentPrincipalInfo.baseDomain() = baseDomain; + + PrincipalInfo principalInfo(contentPrincipalInfo); + + nsTArray<PrincipalInfo> principalInfos; + principalInfos.AppendElement(principalInfo); + + RefPtr<PrincipalVerifier> principalVerifier = + PrincipalVerifier::CreateAndDispatch(std::move(principalInfos)); +#endif + } + } + + return updated; +} + +Result<bool, nsresult> MaybeUpdateLastAccessTimeForOrigin( + FullOriginMetadata& aFullOriginMetadata) { + MOZ_ASSERT(!NS_IsMainThread()); + + if (aFullOriginMetadata.mLastAccessTime == INT64_MIN) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_INSPECT( + const auto& metadataFile, + quotaManager->GetDirectoryForOrigin( + aFullOriginMetadata.mPersistenceType, aFullOriginMetadata.mOrigin)); + + QM_TRY(MOZ_TO_RESULT( + metadataFile->Append(nsLiteralString(METADATA_V2_FILE_NAME)))); + + QM_TRY_UNWRAP(int64_t timestamp, MOZ_TO_RESULT_INVOKE_MEMBER( + metadataFile, GetLastModifiedTime)); + + // Need to convert from milliseconds to microseconds. + MOZ_ASSERT((INT64_MAX / PR_USEC_PER_MSEC) > timestamp); + timestamp *= int64_t(PR_USEC_PER_MSEC); + + aFullOriginMetadata.mLastAccessTime = timestamp; + + return true; + } + + return false; +} + +} // namespace + +BackgroundThreadObject::BackgroundThreadObject() + : mOwningThread(GetCurrentSerialEventTarget()) { + AssertIsOnOwningThread(); +} + +BackgroundThreadObject::BackgroundThreadObject( + nsISerialEventTarget* aOwningThread) + : mOwningThread(aOwningThread) {} + +#ifdef DEBUG + +void BackgroundThreadObject::AssertIsOnOwningThread() const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mOwningThread); + bool current; + MOZ_ASSERT(NS_SUCCEEDED(mOwningThread->IsOnCurrentThread(¤t))); + MOZ_ASSERT(current); +} + +#endif // DEBUG + +nsISerialEventTarget* BackgroundThreadObject::OwningThread() const { + MOZ_ASSERT(mOwningThread); + return mOwningThread; +} + +bool IsOnIOThread() { + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Must have a manager here!"); + + bool currentThread; + return NS_SUCCEEDED( + quotaManager->IOThread()->IsOnCurrentThread(¤tThread)) && + currentThread; +} + +void AssertIsOnIOThread() { + NS_ASSERTION(IsOnIOThread(), "Running on the wrong thread!"); +} + +void DiagnosticAssertIsOnIOThread() { MOZ_DIAGNOSTIC_ASSERT(IsOnIOThread()); } + +void AssertCurrentThreadOwnsQuotaMutex() { +#ifdef DEBUG + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Must have a manager here!"); + + quotaManager->AssertCurrentThreadOwnsQuotaMutex(); +#endif +} + +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("Quota %s: %s:%" PRIu32, aStr, aFile, aLine)), + "quota"_ns, + false /* Quota Manager is not active in private browsing mode */, + true /* Quota Manager runs always in a chrome context */); +} + +namespace { + +bool gInvalidateQuotaCache = false; +StaticAutoPtr<nsString> gBasePath; +StaticAutoPtr<nsString> gStorageName; +StaticAutoPtr<nsCString> gBuildId; + +#ifdef DEBUG +bool gQuotaManagerInitialized = false; +#endif + +StaticRefPtr<QuotaManager> gInstance; +mozilla::Atomic<bool> gShutdown(false); + +// A time stamp that can only be accessed on the main thread. +TimeStamp gLastOSWake; + +using NormalOriginOpArray = + nsTArray<CheckedUnsafePtr<NormalOriginOperationBase>>; +StaticAutoPtr<NormalOriginOpArray> gNormalOriginOps; + +void RegisterNormalOriginOp(NormalOriginOperationBase& aNormalOriginOp) { + AssertIsOnBackgroundThread(); + + if (!gNormalOriginOps) { + gNormalOriginOps = new NormalOriginOpArray(); + } + + gNormalOriginOps->AppendElement(&aNormalOriginOp); +} + +void UnregisterNormalOriginOp(NormalOriginOperationBase& aNormalOriginOp) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(gNormalOriginOps); + + gNormalOriginOps->RemoveElement(&aNormalOriginOp); + + if (gNormalOriginOps->IsEmpty()) { + gNormalOriginOps = nullptr; + } +} + +class StorageOperationBase { + protected: + struct OriginProps { + enum Type { eChrome, eContent, eObsolete, eInvalid }; + + NotNull<nsCOMPtr<nsIFile>> mDirectory; + nsString mLeafName; + nsCString mSpec; + OriginAttributes mAttrs; + int64_t mTimestamp; + OriginMetadata mOriginMetadata; + nsCString mOriginalSuffix; + + LazyInitializedOnceEarlyDestructible<const PersistenceType> + mPersistenceType; + Type mType; + bool mNeedsRestore; + bool mNeedsRestore2; + bool mIgnore; + + public: + explicit OriginProps(MovingNotNull<nsCOMPtr<nsIFile>> aDirectory) + : mDirectory(std::move(aDirectory)), + mTimestamp(0), + mType(eContent), + mNeedsRestore(false), + mNeedsRestore2(false), + mIgnore(false) {} + + template <typename PersistenceTypeFunc> + nsresult Init(PersistenceTypeFunc&& aPersistenceTypeFunc); + }; + + nsTArray<OriginProps> mOriginProps; + + nsCOMPtr<nsIFile> mDirectory; + + public: + explicit StorageOperationBase(nsIFile* aDirectory) : mDirectory(aDirectory) { + AssertIsOnIOThread(); + } + + NS_INLINE_DECL_REFCOUNTING(StorageOperationBase) + + protected: + virtual ~StorageOperationBase() = default; + + nsresult GetDirectoryMetadata(nsIFile* aDirectory, int64_t& aTimestamp, + nsACString& aGroup, nsACString& aOrigin, + Nullable<bool>& aIsApp); + + // Upgrade helper to load the contents of ".metadata-v2" files from previous + // schema versions. Although QuotaManager has a similar GetDirectoryMetadata2 + // method, it is only intended to read current version ".metadata-v2" files. + // And unlike the old ".metadata" files, the ".metadata-v2" format can evolve + // because our "storage.sqlite" lets us track the overall version of the + // storage directory. + nsresult GetDirectoryMetadata2(nsIFile* aDirectory, int64_t& aTimestamp, + nsACString& aSuffix, nsACString& aGroup, + nsACString& aOrigin, bool& aIsApp); + + int64_t GetOriginLastModifiedTime(const OriginProps& aOriginProps); + + nsresult RemoveObsoleteOrigin(const OriginProps& aOriginProps); + + /** + * Rename the origin if the origin string generation from nsIPrincipal + * changed. This consists of renaming the origin in the metadata files and + * renaming the origin directory itself. For simplicity, the origin in + * metadata files is not actually updated, but the metadata files are + * recreated instead. + * + * @param aOriginProps the properties of the origin to check. + * + * @return whether origin was renamed. + */ + Result<bool, nsresult> MaybeRenameOrigin(const OriginProps& aOriginProps); + + nsresult ProcessOriginDirectories(); + + virtual nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) = 0; +}; + +class MOZ_STACK_CLASS OriginParser final { + public: + enum ResultType { InvalidOrigin, ObsoleteOrigin, ValidOrigin }; + + private: + using Tokenizer = + nsCCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>; + + enum SchemeType { eNone, eFile, eAbout, eChrome }; + + enum State { + eExpectingAppIdOrScheme, + eExpectingInMozBrowser, + eExpectingScheme, + eExpectingEmptyToken1, + eExpectingEmptyToken2, + eExpectingEmptyTokenOrUniversalFileOrigin, + eExpectingHost, + eExpectingPort, + eExpectingEmptyTokenOrDriveLetterOrPathnameComponent, + eExpectingEmptyTokenOrPathnameComponent, + eExpectingEmptyToken1OrHost, + + // We transit from eExpectingHost to this state when we encounter a host + // beginning with "[" which indicates an IPv6 literal. Because we mangle the + // IPv6 ":" delimiter to be a "+", we will receive separate tokens for each + // portion of the IPv6 address, including a final token that ends with "]". + // (Note that we do not mangle "[" or "]".) Note that the URL spec + // explicitly disclaims support for "<zone_id>" and so we don't have to deal + // with that. + eExpectingIPV6Token, + eComplete, + eHandledTrailingSeparator + }; + + const nsCString mOrigin; + Tokenizer mTokenizer; + + nsCString mScheme; + nsCString mHost; + Nullable<uint32_t> mPort; + nsTArray<nsCString> mPathnameComponents; + nsCString mHandledTokens; + + SchemeType mSchemeType; + State mState; + bool mInIsolatedMozBrowser; + bool mUniversalFileOrigin; + bool mMaybeDriveLetter; + bool mError; + bool mMaybeObsolete; + + // Number of group which a IPv6 address has. Should be less than 9. + uint8_t mIPGroup; + + public: + explicit OriginParser(const nsACString& aOrigin) + : mOrigin(aOrigin), + mTokenizer(aOrigin, '+'), + mPort(), + mSchemeType(eNone), + mState(eExpectingAppIdOrScheme), + mInIsolatedMozBrowser(false), + mUniversalFileOrigin(false), + mMaybeDriveLetter(false), + mError(false), + mMaybeObsolete(false), + mIPGroup(0) {} + + static ResultType ParseOrigin(const nsACString& aOrigin, nsCString& aSpec, + OriginAttributes* aAttrs, + nsCString& aOriginalSuffix); + + ResultType Parse(nsACString& aSpec); + + private: + void HandleScheme(const nsDependentCSubstring& aToken); + + void HandlePathnameComponent(const nsDependentCSubstring& aToken); + + void HandleToken(const nsDependentCSubstring& aToken); + + void HandleTrailingSeparator(); +}; + +class RepositoryOperationBase : public StorageOperationBase { + public: + explicit RepositoryOperationBase(nsIFile* aDirectory) + : StorageOperationBase(aDirectory) {} + + nsresult ProcessRepository(); + + protected: + virtual ~RepositoryOperationBase() = default; + + template <typename UpgradeMethod> + nsresult MaybeUpgradeClients(const OriginProps& aOriginsProps, + UpgradeMethod aMethod); + + private: + virtual PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) = 0; + + virtual nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) = 0; + + virtual nsresult PrepareClientDirectory(nsIFile* aFile, + const nsAString& aLeafName, + bool& aRemoved); +}; + +class CreateOrUpgradeDirectoryMetadataHelper final + : public RepositoryOperationBase { + nsCOMPtr<nsIFile> mPermanentStorageDir; + + // The legacy PersistenceType, before the default repository introduction. + enum class LegacyPersistenceType { + Persistent = 0, + Temporary + // The PersistenceType had also PERSISTENCE_TYPE_INVALID, but we don't need + // it here. + }; + + LazyInitializedOnce<const LegacyPersistenceType> mLegacyPersistenceType; + + public: + explicit CreateOrUpgradeDirectoryMetadataHelper(nsIFile* aDirectory) + : RepositoryOperationBase(aDirectory) {} + + nsresult Init(); + + private: + Maybe<LegacyPersistenceType> LegacyPersistenceTypeFromFile(nsIFile& aFile, + const fallible_t&); + + PersistenceType PersistenceTypeFromLegacyPersistentSpec( + const nsCString& aSpec); + + PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) override; + + nsresult MaybeUpgradeOriginDirectory(nsIFile* aDirectory); + + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +class UpgradeStorageHelperBase : public RepositoryOperationBase { + LazyInitializedOnce<const PersistenceType> mPersistenceType; + + public: + explicit UpgradeStorageHelperBase(nsIFile* aDirectory) + : RepositoryOperationBase(aDirectory) {} + + nsresult Init(); + + private: + PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) override; +}; + +class UpgradeStorageFrom0_0To1_0Helper final : public UpgradeStorageHelperBase { + public: + explicit UpgradeStorageFrom0_0To1_0Helper(nsIFile* aDirectory) + : UpgradeStorageHelperBase(aDirectory) {} + + private: + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +class UpgradeStorageFrom1_0To2_0Helper final : public UpgradeStorageHelperBase { + public: + explicit UpgradeStorageFrom1_0To2_0Helper(nsIFile* aDirectory) + : UpgradeStorageHelperBase(aDirectory) {} + + private: + nsresult MaybeRemoveMorgueDirectory(const OriginProps& aOriginProps); + + /** + * Remove the origin directory if appId is present in origin attributes. + * + * @param aOriginProps the properties of the origin to check. + * + * @return whether the origin directory was removed. + */ + Result<bool, nsresult> MaybeRemoveAppsData(const OriginProps& aOriginProps); + + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +class UpgradeStorageFrom2_0To2_1Helper final : public UpgradeStorageHelperBase { + public: + explicit UpgradeStorageFrom2_0To2_1Helper(nsIFile* aDirectory) + : UpgradeStorageHelperBase(aDirectory) {} + + private: + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +class UpgradeStorageFrom2_1To2_2Helper final : public UpgradeStorageHelperBase { + public: + explicit UpgradeStorageFrom2_1To2_2Helper(nsIFile* aDirectory) + : UpgradeStorageHelperBase(aDirectory) {} + + private: + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; + + nsresult PrepareClientDirectory(nsIFile* aFile, const nsAString& aLeafName, + bool& aRemoved) override; +}; + +class RestoreDirectoryMetadata2Helper final : public StorageOperationBase { + LazyInitializedOnce<const PersistenceType> mPersistenceType; + + public: + explicit RestoreDirectoryMetadata2Helper(nsIFile* aDirectory) + : StorageOperationBase(aDirectory) {} + + nsresult Init(); + + nsresult RestoreMetadata2File(); + + private: + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +auto MakeSanitizedOriginCString(const nsACString& aOrigin) { +#ifdef XP_WIN + NS_ASSERTION(!strcmp(QuotaManager::kReplaceChars, + FILE_ILLEGAL_CHARACTERS FILE_PATH_SEPARATOR), + "Illegal file characters have changed!"); +#endif + + nsAutoCString res{aOrigin}; + + res.ReplaceChar(QuotaManager::kReplaceChars, '+'); + + return res; +} + +auto MakeSanitizedOriginString(const nsACString& aOrigin) { + // An origin string is ASCII-only, since it is obtained via + // nsIPrincipal::GetOrigin, which returns an ACString. + return NS_ConvertASCIItoUTF16(MakeSanitizedOriginCString(aOrigin)); +} + +Result<nsAutoString, nsresult> GetPathForStorage( + nsIFile& aBaseDir, const nsAString& aStorageName) { + QM_TRY_INSPECT(const auto& storageDir, + CloneFileAndAppend(aBaseDir, aStorageName)); + + QM_TRY_RETURN( + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, storageDir, GetPath)); +} + +int64_t GetLastModifiedTime(PersistenceType aPersistenceType, nsIFile& aFile) { + AssertIsOnIOThread(); + + class MOZ_STACK_CLASS Helper final { + public: + static nsresult GetLastModifiedTime(nsIFile* aFile, int64_t* aTimestamp) { + AssertIsOnIOThread(); + MOZ_ASSERT(aFile); + MOZ_ASSERT(aTimestamp); + + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*aFile)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: + QM_TRY(CollectEachFile( + *aFile, + [&aTimestamp](const nsCOMPtr<nsIFile>& file) + -> Result<mozilla::Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(GetLastModifiedTime(file, aTimestamp))); + + return Ok{}; + })); + break; + + case nsIFileKind::ExistsAsFile: { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, aFile, + GetLeafName)); + + // Bug 1595445 will handle unknown files here. + + if (IsOriginMetadata(leafName) || IsTempMetadata(leafName) || + IsDotFile(leafName)) { + return NS_OK; + } + + QM_TRY_UNWRAP(int64_t timestamp, MOZ_TO_RESULT_INVOKE_MEMBER( + aFile, GetLastModifiedTime)); + + // Need to convert from milliseconds to microseconds. + MOZ_ASSERT((INT64_MAX / PR_USEC_PER_MSEC) > timestamp); + timestamp *= int64_t(PR_USEC_PER_MSEC); + + if (timestamp > *aTimestamp) { + *aTimestamp = timestamp; + } + break; + } + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return NS_OK; + } + }; + + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + return PR_Now(); + } + + int64_t timestamp = INT64_MIN; + nsresult rv = Helper::GetLastModifiedTime(&aFile, ×tamp); + if (NS_FAILED(rv)) { + timestamp = PR_Now(); + } + + // XXX if there were no suitable files for getting last modified time + // (timestamp is still set to INT64_MIN), we should return the current time + // instead of returning INT64_MIN. + + return timestamp; +} + +// Returns a bool indicating whether the directory was newly created. +Result<bool, nsresult> EnsureDirectory(nsIFile& aDirectory) { + AssertIsOnIOThread(); + + // Callers call this function without checking if the directory already + // exists (idempotent usage). QM_OR_ELSE_WARN_IF is not used here since we + // just want to log NS_ERROR_FILE_ALREADY_EXISTS result and not spam the + // reports. + QM_TRY_INSPECT(const auto& exists, + QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Create, + nsIFile::DIRECTORY_TYPE, 0755, + /* aSkipAncestors = */ false) + .map([](Ok) { return false; }), + // Predicate. + IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>, + // Fallback. + ErrToOk<true>)); + + if (exists) { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, IsDirectory)); + QM_TRY(OkIf(isDirectory), Err(NS_ERROR_UNEXPECTED)); + } + + return !exists; +} + +enum FileFlag { Truncate, Update, Append }; + +Result<nsCOMPtr<nsIOutputStream>, nsresult> GetOutputStream( + nsIFile& aFile, FileFlag aFileFlag) { + AssertIsOnIOThread(); + + switch (aFileFlag) { + case FileFlag::Truncate: + QM_TRY_RETURN(NS_NewLocalFileOutputStream(&aFile)); + + case FileFlag::Update: { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(&aFile, Exists)); + + if (!exists) { + return nsCOMPtr<nsIOutputStream>(); + } + + QM_TRY_INSPECT(const auto& stream, + NS_NewLocalFileRandomAccessStream(&aFile)); + + nsCOMPtr<nsIOutputStream> outputStream = do_QueryInterface(stream); + QM_TRY(OkIf(outputStream), Err(NS_ERROR_FAILURE)); + + return outputStream; + } + + case FileFlag::Append: + QM_TRY_RETURN(NS_NewLocalFileOutputStream( + &aFile, PR_WRONLY | PR_CREATE_FILE | PR_APPEND)); + + default: + MOZ_CRASH("Should never get here!"); + } +} + +Result<nsCOMPtr<nsIBinaryOutputStream>, nsresult> GetBinaryOutputStream( + nsIFile& aFile, FileFlag aFileFlag) { + QM_TRY_UNWRAP(auto outputStream, GetOutputStream(aFile, aFileFlag)); + + QM_TRY(OkIf(outputStream), Err(NS_ERROR_UNEXPECTED)); + + return nsCOMPtr<nsIBinaryOutputStream>( + NS_NewObjectOutputStream(outputStream)); +} + +void GetJarPrefix(bool aInIsolatedMozBrowser, nsACString& aJarPrefix) { + aJarPrefix.Truncate(); + + // Fallback. + if (!aInIsolatedMozBrowser) { + return; + } + + // AppId is an unused b2g identifier. Let's set it to 0 all the time (see bug + // 1320404). + // aJarPrefix = appId + "+" + { 't', 'f' } + "+"; + aJarPrefix.AppendInt(0); // TODO: this is the appId, to be removed. + aJarPrefix.Append('+'); + aJarPrefix.Append(aInIsolatedMozBrowser ? 't' : 'f'); + aJarPrefix.Append('+'); +} + +nsresult CreateDirectoryMetadata(nsIFile& aDirectory, int64_t aTimestamp, + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + + OriginAttributes groupAttributes; + + nsCString groupNoSuffix; + QM_TRY(OkIf(groupAttributes.PopulateFromOrigin(aOriginMetadata.mGroup, + groupNoSuffix)), + NS_ERROR_FAILURE); + + nsCString groupPrefix; + GetJarPrefix(groupAttributes.mInIsolatedMozBrowser, groupPrefix); + + nsCString group = groupPrefix + groupNoSuffix; + + OriginAttributes originAttributes; + + nsCString originNoSuffix; + QM_TRY(OkIf(originAttributes.PopulateFromOrigin(aOriginMetadata.mOrigin, + originNoSuffix)), + NS_ERROR_FAILURE); + + nsCString originPrefix; + GetJarPrefix(originAttributes.mInIsolatedMozBrowser, originPrefix); + + nsCString origin = originPrefix + originNoSuffix; + + MOZ_ASSERT(groupPrefix == originPrefix); + + QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIFile>, aDirectory, Clone)); + + QM_TRY(MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_TMP_FILE_NAME)))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, FileFlag::Truncate)); + MOZ_ASSERT(stream); + + QM_TRY(MOZ_TO_RESULT(stream->Write64(aTimestamp))); + + QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(group.get()))); + + QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(origin.get()))); + + // Currently unused (used to be isApp). + QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(false))); + + QM_TRY(MOZ_TO_RESULT(stream->Flush())); + + QM_TRY(MOZ_TO_RESULT(stream->Close())); + + QM_TRY(MOZ_TO_RESULT( + file->RenameTo(nullptr, nsLiteralString(METADATA_FILE_NAME)))); + + return NS_OK; +} + +nsresult CreateDirectoryMetadata2(nsIFile& aDirectory, int64_t aTimestamp, + bool aPersisted, + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIFile>, aDirectory, Clone)); + + QM_TRY( + MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_V2_TMP_FILE_NAME)))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, FileFlag::Truncate)); + MOZ_ASSERT(stream); + + QM_TRY(MOZ_TO_RESULT(stream->Write64(aTimestamp))); + + QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(aPersisted))); + + // Reserved data 1 + QM_TRY(MOZ_TO_RESULT(stream->Write32(0))); + + // Reserved data 2 + QM_TRY(MOZ_TO_RESULT(stream->Write32(0))); + + // The suffix isn't used right now, but we might need it in future. It's + // a bit of redundancy we can live with given how painful is to upgrade + // metadata files. + QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(aOriginMetadata.mSuffix.get()))); + + QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(aOriginMetadata.mGroup.get()))); + + QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(aOriginMetadata.mOrigin.get()))); + + // Currently unused (used to be isApp). + QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(false))); + + QM_TRY(MOZ_TO_RESULT(stream->Flush())); + + QM_TRY(MOZ_TO_RESULT(stream->Close())); + + QM_TRY(MOZ_TO_RESULT( + file->RenameTo(nullptr, nsLiteralString(METADATA_V2_FILE_NAME)))); + + return NS_OK; +} + +Result<nsCOMPtr<nsIBinaryInputStream>, nsresult> GetBinaryInputStream( + nsIFile& aDirectory, const nsAString& aFilename) { + MOZ_ASSERT(!NS_IsMainThread()); + + QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIFile>, aDirectory, Clone)); + + QM_TRY(MOZ_TO_RESULT(file->Append(aFilename))); + + QM_TRY_UNWRAP(auto stream, NS_NewLocalFileInputStream(file)); + + QM_TRY_INSPECT(const auto& bufferedStream, + NS_NewBufferedInputStream(stream.forget(), 512)); + + QM_TRY(OkIf(bufferedStream), Err(NS_ERROR_FAILURE)); + + return nsCOMPtr<nsIBinaryInputStream>( + NS_NewObjectInputStream(bufferedStream)); +} + +// This method computes and returns our best guess for the temporary storage +// limit (in bytes), based on disk capacity. +Result<uint64_t, nsresult> GetTemporaryStorageLimit(nsIFile& aStorageDir) { + // The fixed limit pref can be used to override temporary storage limit + // calculation. + if (StaticPrefs::dom_quotaManager_temporaryStorage_fixedLimit() >= 0) { + return static_cast<uint64_t>( + StaticPrefs::dom_quotaManager_temporaryStorage_fixedLimit()) * + 1024; + } + + constexpr int64_t teraByte = (1024LL * 1024LL * 1024LL * 1024LL); + constexpr int64_t maxAllowedCapacity = 8LL * teraByte; + + // Check for disk capacity of user's device on which storage directory lives. + int64_t diskCapacity = maxAllowedCapacity; + + // Log error when default disk capacity is returned due to the error + QM_WARNONLY_TRY(MOZ_TO_RESULT(aStorageDir.GetDiskCapacity(&diskCapacity))); + + MOZ_ASSERT(diskCapacity >= 0LL); + + // Allow temporary storage to consume up to 50% of disk capacity. + int64_t capacityLimit = diskCapacity / 2LL; + + // If the disk capacity reported by the operating system is very + // large and potentially incorrect due to hardware issues, + // a hardcoded limit is supplied instead. + QM_WARNONLY_TRY( + OkIf(capacityLimit < maxAllowedCapacity), + ([&capacityLimit](const auto&) { capacityLimit = maxAllowedCapacity; })); + + return capacityLimit; +} + +bool IsOriginUnaccessed(const FullOriginMetadata& aFullOriginMetadata, + const int64_t aRecentTime) { + if (aFullOriginMetadata.mLastAccessTime > aRecentTime) { + return false; + } + + return (aRecentTime - aFullOriginMetadata.mLastAccessTime) / PR_USEC_PER_SEC > + StaticPrefs::dom_quotaManager_unaccessedForLongTimeThresholdSec(); +} + +} // namespace + +/******************************************************************************* + * Exported functions + ******************************************************************************/ + +void InitializeQuotaManager() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!gQuotaManagerInitialized); + +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + ScopedLogExtraInfo::Initialize(); +#endif + + 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(QuotaManager::Initialize())); + +#ifdef DEBUG + gQuotaManagerInitialized = true; +#endif +} + +PQuotaParent* AllocPQuotaParent() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(QuotaManager::IsShuttingDown())) { + return nullptr; + } + + auto actor = MakeRefPtr<Quota>(); + + return actor.forget().take(); +} + +bool DeallocPQuotaParent(PQuotaParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<Quota> actor = dont_AddRef(static_cast<Quota*>(aActor)); + return true; +} + +bool RecvShutdownQuotaManager() { + AssertIsOnBackgroundThread(); + + // If we are already in shutdown, don't call ShutdownInstance() + // again and return true immediately. We shall see this incident + // in Telemetry. + // XXX todo: Make QM_TRY stacks thread-aware (Bug 1735124) + // XXX todo: Active QM_TRY context for shutdown (Bug 1735170) + QM_TRY(OkIf(!gShutdown), true); + + QuotaManager::ShutdownInstance(); + + return true; +} + +QuotaManager::Observer* QuotaManager::Observer::sInstance = nullptr; + +// static +nsresult QuotaManager::Observer::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<Observer> observer = new Observer(); + + nsresult rv = observer->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + sInstance = observer; + + return NS_OK; +} + +// static +nsIObserver* QuotaManager::Observer::GetInstance() { + MOZ_ASSERT(NS_IsMainThread()); + + return sInstance; +} + +// static +void QuotaManager::Observer::ShutdownCompleted() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(sInstance); + + sInstance->mShutdownComplete = true; +} + +nsresult QuotaManager::Observer::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return NS_ERROR_FAILURE; + } + + // XXX: Improve the way that we remove observer in failure cases. + nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = obs->AddObserver(this, kProfileDoChangeTopic, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + return rv; + } + + rv = obs->AddObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + obs->RemoveObserver(this, kProfileDoChangeTopic); + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + return rv; + } + + rv = obs->AddObserver(this, NS_WIDGET_WAKE_OBSERVER_TOPIC, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + obs->RemoveObserver(this, kProfileDoChangeTopic); + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + obs->RemoveObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID); + return rv; + } + + return NS_OK; +} + +nsresult QuotaManager::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, NS_WIDGET_WAKE_OBSERVER_TOPIC)); + MOZ_ALWAYS_SUCCEEDS( + obs->RemoveObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID)); + MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, kProfileDoChangeTopic)); + MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID)); + + sInstance = nullptr; + + // 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(QuotaManager::Observer, nsIObserver) + +NS_IMETHODIMP +QuotaManager::Observer::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + if (!strcmp(aTopic, kProfileDoChangeTopic)) { + if (NS_WARN_IF(gBasePath)) { + NS_WARNING( + "profile-before-change-qm must precede repeated " + "profile-do-change!"); + return NS_OK; + } + + Telemetry::SetEventRecordingEnabled("dom.quota.try"_ns, true); + + gBasePath = new nsString(); + + nsCOMPtr<nsIFile> baseDir; + rv = NS_GetSpecialDirectory(NS_APP_INDEXEDDB_PARENT_DIR, + getter_AddRefs(baseDir)); + if (NS_FAILED(rv)) { + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(baseDir)); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = baseDir->GetPath(*gBasePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + gStorageName = new nsString(); + + rv = Preferences::GetString("dom.quotaManager.storageName", *gStorageName); + if (NS_FAILED(rv)) { + *gStorageName = kStorageName; + } + + gBuildId = new nsCString(); + + nsCOMPtr<nsIPlatformInfo> platformInfo = + do_GetService("@mozilla.org/xre/app-info;1"); + if (NS_WARN_IF(!platformInfo)) { + return NS_ERROR_FAILURE; + } + + rv = platformInfo->GetPlatformBuildID(*gBuildId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + if (!strcmp(aTopic, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID)) { + if (NS_WARN_IF(!gBasePath)) { + NS_WARNING("profile-do-change must precede profile-before-change-qm!"); + return NS_OK; + } + + // mPendingProfileChange is our re-entrancy guard (the nested event loop + // below may cause re-entrancy). + if (mPendingProfileChange) { + return NS_OK; + } + + AutoRestore<bool> pending(mPendingProfileChange); + mPendingProfileChange = true; + + mShutdownComplete = false; + + PBackgroundChild* backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return NS_ERROR_FAILURE; + } + + if (NS_WARN_IF(!backgroundActor->SendShutdownQuotaManager())) { + return NS_ERROR_FAILURE; + } + + MOZ_ALWAYS_TRUE(SpinEventLoopUntil( + "QuotaManager::Observer::Observe profile-before-change-qm"_ns, + [&]() { return mShutdownComplete; })); + + gBasePath = nullptr; + + gStorageName = nullptr; + + gBuildId = nullptr; + + Telemetry::SetEventRecordingEnabled("dom.quota.try"_ns, false); + + return NS_OK; + } + + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + rv = Shutdown(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + if (!strcmp(aTopic, NS_WIDGET_WAKE_OBSERVER_TOPIC)) { + gLastOSWake = TimeStamp::Now(); + + return NS_OK; + } + + NS_WARNING("Unknown observer topic!"); + return NS_OK; +} + +/******************************************************************************* + * Quota manager + ******************************************************************************/ + +QuotaManager::QuotaManager(const nsAString& aBasePath, + const nsAString& aStorageName) + : mQuotaMutex("QuotaManager.mQuotaMutex"), + mBasePath(aBasePath), + mStorageName(aStorageName), + mTemporaryStorageUsage(0), + mNextDirectoryLockId(0), + mTemporaryStorageInitialized(false), + mCacheUsable(false), + mShuttingDownStorage(false) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!gInstance); +} + +QuotaManager::~QuotaManager() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!gInstance || gInstance == this); +} + +// static +nsresult QuotaManager::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = Observer::Initialize(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +// static +Result<MovingNotNull<RefPtr<QuotaManager>>, nsresult> +QuotaManager::GetOrCreate() { + AssertIsOnBackgroundThread(); + + if (gInstance) { + return WrapMovingNotNullUnchecked(RefPtr<QuotaManager>{gInstance}); + } + + QM_TRY(OkIf(gBasePath), Err(NS_ERROR_FAILURE), [](const auto&) { + NS_WARNING( + "Trying to create QuotaManager before profile-do-change! " + "Forgot to call do_get_profile()?"); + }); + + QM_TRY(OkIf(!IsShuttingDown()), Err(NS_ERROR_FAILURE), [](const auto&) { + MOZ_ASSERT(false, + "Trying to create QuotaManager after profile-before-change-qm!"); + }); + + auto instance = MakeRefPtr<QuotaManager>(*gBasePath, *gStorageName); + + QM_TRY(MOZ_TO_RESULT(instance->Init())); + + gInstance = instance; + + return WrapMovingNotNullUnchecked(std::move(instance)); +} + +Result<Ok, nsresult> QuotaManager::EnsureCreated() { + AssertIsOnBackgroundThread(); + + QM_TRY_RETURN(GetOrCreate().map([](const auto& res) { return Ok{}; })) +} + +// static +QuotaManager* QuotaManager::Get() { + // Does not return an owning reference. + return gInstance; +} + +// static +nsIObserver* QuotaManager::GetObserver() { + MOZ_ASSERT(NS_IsMainThread()); + + return Observer::GetInstance(); +} + +// static +bool QuotaManager::IsShuttingDown() { return gShutdown; } + +// static +void QuotaManager::ShutdownInstance() { + AssertIsOnBackgroundThread(); + + if (gInstance) { + gInstance->Shutdown(); + + gInstance = nullptr; + } + + RefPtr<Runnable> runnable = + NS_NewRunnableFunction("dom::quota::QuotaManager::ShutdownCompleted", + []() { Observer::ShutdownCompleted(); }); + MOZ_ASSERT(runnable); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget())); +} + +// static +void QuotaManager::Reset() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!gInstance); + MOZ_ASSERT(gShutdown); + + gShutdown = false; +} + +// static +bool QuotaManager::IsOSMetadata(const nsAString& aFileName) { + return aFileName.EqualsLiteral(DSSTORE_FILE_NAME) || + aFileName.EqualsLiteral(DESKTOP_FILE_NAME) || + aFileName.LowerCaseEqualsLiteral(DESKTOP_INI_FILE_NAME) || + aFileName.LowerCaseEqualsLiteral(THUMBS_DB_FILE_NAME); +} + +// static +bool QuotaManager::IsDotFile(const nsAString& aFileName) { + return aFileName.First() == char16_t('.'); +} + +void QuotaManager::RegisterDirectoryLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + mDirectoryLocks.AppendElement(WrapNotNullUnchecked(&aLock)); + + if (aLock.ShouldUpdateLockIdTable()) { + MutexAutoLock lock(mQuotaMutex); + + MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLockIdTable.Contains(aLock.Id())); + mDirectoryLockIdTable.InsertOrUpdate(aLock.Id(), + WrapNotNullUnchecked(&aLock)); + } + + if (aLock.ShouldUpdateLockTable()) { + DirectoryLockTable& directoryLockTable = + GetDirectoryLockTable(aLock.GetPersistenceType()); + + // XXX It seems that the contents of the array are never actually used, we + // just use that like an inefficient use counter. Can't we just change + // DirectoryLockTable to a nsTHashMap<nsCStringHashKey, uint32_t>? + directoryLockTable + .LookupOrInsertWith( + aLock.Origin(), + [this, &aLock] { + if (!IsShuttingDown()) { + UpdateOriginAccessTime(aLock.GetPersistenceType(), + aLock.OriginMetadata()); + } + return MakeUnique<nsTArray<NotNull<DirectoryLockImpl*>>>(); + }) + ->AppendElement(WrapNotNullUnchecked(&aLock)); + } + + aLock.SetRegistered(true); +} + +void QuotaManager::UnregisterDirectoryLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + MOZ_ALWAYS_TRUE(mDirectoryLocks.RemoveElement(&aLock)); + + if (aLock.ShouldUpdateLockIdTable()) { + MutexAutoLock lock(mQuotaMutex); + + MOZ_DIAGNOSTIC_ASSERT(mDirectoryLockIdTable.Contains(aLock.Id())); + mDirectoryLockIdTable.Remove(aLock.Id()); + } + + if (aLock.ShouldUpdateLockTable()) { + DirectoryLockTable& directoryLockTable = + GetDirectoryLockTable(aLock.GetPersistenceType()); + + nsTArray<NotNull<DirectoryLockImpl*>>* array; + MOZ_ALWAYS_TRUE(directoryLockTable.Get(aLock.Origin(), &array)); + + MOZ_ALWAYS_TRUE(array->RemoveElement(&aLock)); + if (array->IsEmpty()) { + directoryLockTable.Remove(aLock.Origin()); + + if (!IsShuttingDown()) { + UpdateOriginAccessTime(aLock.GetPersistenceType(), + aLock.OriginMetadata()); + } + } + } + + aLock.SetRegistered(false); +} + +void QuotaManager::AddPendingDirectoryLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + mPendingDirectoryLocks.AppendElement(&aLock); +} + +void QuotaManager::RemovePendingDirectoryLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + MOZ_ALWAYS_TRUE(mPendingDirectoryLocks.RemoveElement(&aLock)); +} + +uint64_t QuotaManager::CollectOriginsForEviction( + uint64_t aMinSizeToBeFreed, nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aLocks.IsEmpty()); + + // XXX This looks as if this could/should also use CollectLRUOriginInfosUntil, + // or maybe a generalization if that. + + struct MOZ_STACK_CLASS Helper final { + static void GetInactiveOriginInfos( + const nsTArray<NotNull<RefPtr<OriginInfo>>>& aOriginInfos, + const nsTArray<NotNull<const DirectoryLockImpl*>>& aLocks, + OriginInfosFlatTraversable& aInactiveOriginInfos) { + for (const auto& originInfo : aOriginInfos) { + MOZ_ASSERT(originInfo->mGroupInfo->mPersistenceType != + PERSISTENCE_TYPE_PERSISTENT); + + if (originInfo->LockedPersisted()) { + continue; + } + + // Never evict PERSISTENCE_TYPE_DEFAULT data associated to a + // moz-extension origin, unlike websites (which may more likely using + // the local data as a cache but still able to retrieve the same data + // from the server side) extensions do not have the same data stored + // anywhere else and evicting the data would result into potential data + // loss for the users. + // + // Also, unlike a website the extensions are explicitly installed and + // uninstalled by the user and all data associated to the extension + // principal will be completely removed once the addon is uninstalled. + if (originInfo->mGroupInfo->mPersistenceType != + PERSISTENCE_TYPE_TEMPORARY && + originInfo->IsExtensionOrigin()) { + continue; + } + + const auto originScope = OriginScope::FromOrigin(originInfo->mOrigin); + + const bool match = + std::any_of(aLocks.begin(), aLocks.end(), + [&originScope](const DirectoryLockImpl* const lock) { + return originScope.Matches(lock->GetOriginScope()); + }); + + if (!match) { + MOZ_ASSERT(!originInfo->mCanonicalQuotaObjects.Count(), + "Inactive origin shouldn't have open files!"); + aInactiveOriginInfos.InsertElementSorted( + originInfo, OriginInfoAccessTimeComparator()); + } + } + } + }; + + // Split locks into separate arrays and filter out locks for persistent + // storage, they can't block us. + const auto [temporaryStorageLocks, defaultStorageLocks] = [this] { + nsTArray<NotNull<const DirectoryLockImpl*>> temporaryStorageLocks; + nsTArray<NotNull<const DirectoryLockImpl*>> defaultStorageLocks; + for (NotNull<const DirectoryLockImpl*> const lock : mDirectoryLocks) { + const Nullable<PersistenceType>& persistenceType = + lock->NullablePersistenceType(); + + if (persistenceType.IsNull()) { + temporaryStorageLocks.AppendElement(lock); + defaultStorageLocks.AppendElement(lock); + } else if (persistenceType.Value() == PERSISTENCE_TYPE_TEMPORARY) { + temporaryStorageLocks.AppendElement(lock); + } else if (persistenceType.Value() == PERSISTENCE_TYPE_DEFAULT) { + defaultStorageLocks.AppendElement(lock); + } else { + MOZ_ASSERT(persistenceType.Value() == PERSISTENCE_TYPE_PERSISTENT); + + // Do nothing here, persistent origins don't need to be collected ever. + } + } + + return std::pair(std::move(temporaryStorageLocks), + std::move(defaultStorageLocks)); + }(); + + // Enumerate and process inactive origins. This must be protected by the + // mutex. + MutexAutoLock lock(mQuotaMutex); + + const auto [inactiveOrigins, sizeToBeFreed] = + [this, &temporaryStorageLocks = temporaryStorageLocks, + &defaultStorageLocks = defaultStorageLocks, aMinSizeToBeFreed] { + nsTArray<NotNull<RefPtr<const OriginInfo>>> inactiveOrigins; + for (const auto& entry : mGroupInfoPairs) { + const auto& pair = entry.GetData(); + + MOZ_ASSERT(!entry.GetKey().IsEmpty()); + MOZ_ASSERT(pair); + + RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY); + if (groupInfo) { + Helper::GetInactiveOriginInfos(groupInfo->mOriginInfos, + temporaryStorageLocks, + inactiveOrigins); + } + + groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT); + if (groupInfo) { + Helper::GetInactiveOriginInfos( + groupInfo->mOriginInfos, defaultStorageLocks, inactiveOrigins); + } + } + +#ifdef DEBUG + // Make sure the array is sorted correctly. + const bool inactiveOriginsSorted = + std::is_sorted(inactiveOrigins.cbegin(), inactiveOrigins.cend(), + [](const auto& lhs, const auto& rhs) { + return lhs->mAccessTime < rhs->mAccessTime; + }); + MOZ_ASSERT(inactiveOriginsSorted); +#endif + + // Create a list of inactive and the least recently used origins + // whose aggregate size is greater or equals the minimal size to be + // freed. + uint64_t sizeToBeFreed = 0; + for (uint32_t count = inactiveOrigins.Length(), index = 0; + index < count; index++) { + if (sizeToBeFreed >= aMinSizeToBeFreed) { + inactiveOrigins.TruncateLength(index); + break; + } + + sizeToBeFreed += inactiveOrigins[index]->LockedUsage(); + } + + return std::pair(std::move(inactiveOrigins), sizeToBeFreed); + }(); + + if (sizeToBeFreed >= aMinSizeToBeFreed) { + // Success, add directory locks for these origins, so any other + // operations for them will be delayed (until origin eviction is finalized). + + for (const auto& originInfo : inactiveOrigins) { + auto lock = DirectoryLockImpl::CreateForEviction( + WrapNotNullUnchecked(this), originInfo->mGroupInfo->mPersistenceType, + originInfo->FlattenToOriginMetadata()); + + lock->AcquireImmediately(); + + aLocks.AppendElement(lock.forget()); + } + + return sizeToBeFreed; + } + + return 0; +} + +template <typename P> +void QuotaManager::CollectPendingOriginsForListing(P aPredicate) { + MutexAutoLock lock(mQuotaMutex); + + for (const auto& entry : mGroupInfoPairs) { + const auto& pair = entry.GetData(); + + MOZ_ASSERT(!entry.GetKey().IsEmpty()); + MOZ_ASSERT(pair); + + RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT); + if (groupInfo) { + for (const auto& originInfo : groupInfo->mOriginInfos) { + if (!originInfo->mDirectoryExists) { + aPredicate(originInfo); + } + } + } + } +} + +nsresult QuotaManager::Init() { + AssertIsOnOwningThread(); + +#ifdef XP_WIN + CacheUseDOSDevicePathSyntaxPrefValue(); +#endif + + QM_TRY_INSPECT(const auto& baseDir, QM_NewLocalFile(mBasePath)); + + QM_TRY_UNWRAP( + do_Init(mIndexedDBPath), + GetPathForStorage(*baseDir, nsLiteralString(INDEXEDDB_DIRECTORY_NAME))); + + QM_TRY(MOZ_TO_RESULT(baseDir->Append(mStorageName))); + + QM_TRY_UNWRAP(do_Init(mStoragePath), + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, baseDir, GetPath)); + + QM_TRY_UNWRAP( + do_Init(mStorageArchivesPath), + GetPathForStorage(*baseDir, nsLiteralString(ARCHIVES_DIRECTORY_NAME))); + + QM_TRY_UNWRAP( + do_Init(mPermanentStoragePath), + GetPathForStorage(*baseDir, nsLiteralString(PERMANENT_DIRECTORY_NAME))); + + QM_TRY_UNWRAP( + do_Init(mTemporaryStoragePath), + GetPathForStorage(*baseDir, nsLiteralString(TEMPORARY_DIRECTORY_NAME))); + + QM_TRY_UNWRAP( + do_Init(mDefaultStoragePath), + GetPathForStorage(*baseDir, nsLiteralString(DEFAULT_DIRECTORY_NAME))); + + QM_TRY_UNWRAP(do_Init(mIOThread), + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<nsIThread>, MOZ_SELECT_OVERLOAD(NS_NewNamedThread), + "QuotaManager IO")); + + static_assert(Client::IDB == 0 && Client::DOMCACHE == 1 && Client::SDB == 2 && + Client::FILESYSTEM == 3 && Client::LS == 4 && + Client::TYPE_MAX == 5, + "Fix the registration!"); + + // Register clients. + auto clients = decltype(mClients)::ValueType{}; + clients.AppendElement(indexedDB::CreateQuotaClient()); + clients.AppendElement(cache::CreateQuotaClient()); + clients.AppendElement(simpledb::CreateQuotaClient()); + clients.AppendElement(fs::CreateQuotaClient()); + if (NextGenLocalStorageEnabled()) { + clients.AppendElement(localstorage::CreateQuotaClient()); + } else { + clients.SetLength(Client::TypeMax()); + } + + mClients.init(std::move(clients)); + + MOZ_ASSERT(mClients->Capacity() == Client::TYPE_MAX, + "Should be using an auto array with correct capacity!"); + + mAllClientTypes.init(ClientTypesArray{ + Client::Type::IDB, Client::Type::DOMCACHE, Client::Type::SDB, + Client::Type::FILESYSTEM, Client::Type::LS}); + mAllClientTypesExceptLS.init( + ClientTypesArray{Client::Type::IDB, Client::Type::DOMCACHE, + Client::Type::SDB, Client::Type::FILESYSTEM}); + + return NS_OK; +} + +// static +void QuotaManager::MaybeRecordQuotaClientShutdownStep( + const Client::Type aClientType, const nsACString& aStepDescription) { + // Callable on any thread. + + auto* const quotaManager = QuotaManager::Get(); + MOZ_DIAGNOSTIC_ASSERT(quotaManager); + + if (quotaManager->IsShuttingDown()) { + quotaManager->RecordShutdownStep(Some(aClientType), aStepDescription); + } +} + +// static +void QuotaManager::SafeMaybeRecordQuotaClientShutdownStep( + const Client::Type aClientType, const nsACString& aStepDescription) { + // Callable on any thread. + + auto* const quotaManager = QuotaManager::Get(); + + if (quotaManager && quotaManager->IsShuttingDown()) { + quotaManager->RecordShutdownStep(Some(aClientType), aStepDescription); + } +} + +void QuotaManager::RecordQuotaManagerShutdownStep( + const nsACString& aStepDescription) { + // Callable on any thread. + MOZ_ASSERT(mShutdownStarted); + + RecordShutdownStep(Nothing{}, aStepDescription); +} + +void QuotaManager::MaybeRecordQuotaManagerShutdownStep( + const nsACString& aStepDescription) { + // Callable on any thread. + + if (IsShuttingDown()) { + RecordQuotaManagerShutdownStep(aStepDescription); + } +} + +void QuotaManager::RecordShutdownStep(const Maybe<Client::Type> aClientType, + const nsACString& aStepDescription) { + MOZ_ASSERT(IsShuttingDown()); + + const TimeDuration elapsedSinceShutdownStart = + TimeStamp::NowLoRes() - *mShutdownStartedAt; + + const auto stepString = + nsPrintfCString("%fs: %s", elapsedSinceShutdownStart.ToSeconds(), + nsPromiseFlatCString(aStepDescription).get()); + + if (aClientType) { + AssertIsOnBackgroundThread(); + + mShutdownSteps[*aClientType].Append(stepString + "\n"_ns); + } else { + // Callable on any thread. + MutexAutoLock lock(mQuotaMutex); + + mQuotaManagerShutdownSteps.Append(stepString + "\n"_ns); + } + +#ifdef DEBUG + // XXX Probably this isn't the mechanism that should be used here. + + NS_DebugBreak( + NS_DEBUG_WARNING, + nsAutoCString(aClientType ? Client::TypeToText(*aClientType) + : "quota manager"_ns + " shutdown step"_ns) + .get(), + stepString.get(), __FILE__, __LINE__); +#endif +} + +void QuotaManager::Shutdown() { + AssertIsOnOwningThread(); + MOZ_DIAGNOSTIC_ASSERT(!gShutdown); + + // Define some local helper functions + + auto flagShutdownStarted = [this]() { + // Setting this flag prevents the service from being recreated and prevents + // further storages from being created. + // XXX: Harmonize QM shutdown flags, see bug 1726714 + gShutdown = true; + + // StopIdleMaintenance used to happen before mShutdownStarted is set true + // but it is just an internal flag for the recording of shutdown steps + // and not evaluated elsewhere. + + mShutdownStartedAt.init(TimeStamp::NowLoRes()); + mShutdownStarted = true; + }; + + nsCOMPtr<nsITimer> crashBrowserTimer; + + auto crashBrowserTimerCallback = [](nsITimer* aTimer, void* aClosure) { + auto* const quotaManager = static_cast<QuotaManager*>(aClosure); + + nsCString annotation; + + for (Client::Type type : quotaManager->AllClientTypes()) { + auto& quotaClient = *(*quotaManager->mClients)[type]; + + if (!quotaClient.IsShutdownCompleted()) { + annotation.AppendPrintf("%s: %s\nIntermediate steps:\n%s\n\n", + Client::TypeToText(type).get(), + quotaClient.GetShutdownStatus().get(), + quotaManager->mShutdownSteps[type].get()); + } + } + + if (gNormalOriginOps) { + annotation.AppendPrintf("QM: %zu normal origin ops pending\n", + gNormalOriginOps->Length()); +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + for (const auto& op : *gNormalOriginOps) { + nsCString name; + op->GetName(name); + annotation.AppendPrintf("Op: %s pending\n", name.get()); + } +#endif + } + { + MutexAutoLock lock(quotaManager->mQuotaMutex); + + annotation.AppendPrintf("Intermediate steps:\n%s\n", + quotaManager->mQuotaManagerShutdownSteps.get()); + } + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::QuotaManagerShutdownTimeout, annotation); + + MOZ_CRASH("Quota manager shutdown timed out"); + }; + + auto startCrashBrowserTimer = [&]() { + crashBrowserTimer = NS_NewTimer(); + MOZ_ASSERT(crashBrowserTimer); + if (crashBrowserTimer) { + RecordQuotaManagerShutdownStep("startCrashBrowserTimer"_ns); + MOZ_ALWAYS_SUCCEEDS(crashBrowserTimer->InitWithNamedFuncCallback( + crashBrowserTimerCallback, this, SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS, + nsITimer::TYPE_ONE_SHOT, + "quota::QuotaManager::Shutdown::crashBrowserTimer")); + } + }; + + auto stopCrashBrowserTimer = [&]() { + if (crashBrowserTimer) { + RecordQuotaManagerShutdownStep("stopCrashBrowserTimer"_ns); + QM_WARNONLY_TRY(QM_TO_RESULT(crashBrowserTimer->Cancel())); + } + }; + + auto initiateShutdownWorkThreads = [this]() { + RecordQuotaManagerShutdownStep("initiateShutdownWorkThreads"_ns); + bool needsToWait = false; + for (Client::Type type : AllClientTypes()) { + // Clients are supposed to also AbortAllOperations from this point on + // to speed up shutdown, if possible. Thus pending operations + // might not be executed anymore. + needsToWait |= (*mClients)[type]->InitiateShutdownWorkThreads(); + } + + return needsToWait; + }; + + nsCOMPtr<nsITimer> killActorsTimer; + + auto killActorsTimerCallback = [](nsITimer* aTimer, void* aClosure) { + auto* const quotaManager = static_cast<QuotaManager*>(aClosure); + + quotaManager->RecordQuotaManagerShutdownStep("killActorsTimerCallback"_ns); + + // XXX: This abort is a workaround to unblock shutdown, which + // ought to be removed by bug 1682326. We probably need more + // checks to immediately abort new operations during + // shutdown. + quotaManager->GetClient(Client::IDB)->AbortAllOperations(); + + for (Client::Type type : quotaManager->AllClientTypes()) { + quotaManager->GetClient(type)->ForceKillActors(); + } + }; + + auto startKillActorsTimer = [&]() { + killActorsTimer = NS_NewTimer(); + MOZ_ASSERT(killActorsTimer); + if (killActorsTimer) { + RecordQuotaManagerShutdownStep("startKillActorsTimer"_ns); + MOZ_ALWAYS_SUCCEEDS(killActorsTimer->InitWithNamedFuncCallback( + killActorsTimerCallback, this, SHUTDOWN_KILL_ACTORS_TIMEOUT_MS, + nsITimer::TYPE_ONE_SHOT, + "quota::QuotaManager::Shutdown::killActorsTimer")); + } + }; + + auto stopKillActorsTimer = [&]() { + if (killActorsTimer) { + RecordQuotaManagerShutdownStep("stopKillActorsTimer"_ns); + QM_WARNONLY_TRY(QM_TO_RESULT(killActorsTimer->Cancel())); + } + }; + + auto isAllClientsShutdownComplete = [this] { + return std::all_of(AllClientTypes().cbegin(), AllClientTypes().cend(), + [&self = *this](const auto type) { + return (*self.mClients)[type]->IsShutdownCompleted(); + }); + }; + + auto shutdownAndJoinWorkThreads = [this]() { + RecordQuotaManagerShutdownStep("shutdownAndJoinWorkThreads"_ns); + for (Client::Type type : AllClientTypes()) { + (*mClients)[type]->FinalizeShutdownWorkThreads(); + } + }; + + auto shutdownAndJoinIOThread = [this]() { + RecordQuotaManagerShutdownStep("shutdownAndJoinIOThread"_ns); + + // Make sure to join with our IO thread. + QM_WARNONLY_TRY(QM_TO_RESULT((*mIOThread)->Shutdown())); + }; + + auto invalidatePendingDirectoryLocks = [this]() { + RecordQuotaManagerShutdownStep("invalidatePendingDirectoryLocks"_ns); + for (RefPtr<DirectoryLockImpl>& lock : mPendingDirectoryLocks) { + lock->Invalidate(); + } + }; + + // Body of the function + ScopedLogExtraInfo scope{ScopedLogExtraInfo::kTagContext, + "dom::quota::QuotaManager::Shutdown"_ns}; + + // This must be called before `flagShutdownStarted`, it would fail otherwise. + // `ShutdownStorageOp` needs to acquire an exclusive directory lock over + // entire <profile>/storage which will abort any existing operations and wait + // for all existing directory locks to be released. So the shutdown operation + // will effectively run after all existing operations. + // We don't need to use the returned promise here because `ShutdownStorage` + // registers `ShudownStorageOp` in `gNormalOriginOps`. + ShutdownStorage(); + + flagShutdownStarted(); + + startCrashBrowserTimer(); + + // XXX: StopIdleMaintenance now just notifies all clients to abort any + // maintenance work. + // This could be done as part of QuotaClient::AbortAllOperations. + StopIdleMaintenance(); + + // XXX In theory, we could simplify the code below (and also the `Client` + // interface) by removing the `initiateShutdownWorkThreads` and + // `isAllClientsShutdownComplete` calls because it should be sufficient + // to rely on `ShutdownStorage` to abort all existing operations and to + // wait for all existing directory locks to be released as well. + + const bool needsToWait = + initiateShutdownWorkThreads() || static_cast<bool>(gNormalOriginOps); + + // If any clients cannot shutdown immediately, spin the event loop while we + // wait on all the threads to close. + if (needsToWait) { + startKillActorsTimer(); + + MOZ_ALWAYS_TRUE(SpinEventLoopUntil( + "QuotaManager::Shutdown"_ns, [isAllClientsShutdownComplete]() { + return !gNormalOriginOps && isAllClientsShutdownComplete(); + })); + + stopKillActorsTimer(); + } + + shutdownAndJoinWorkThreads(); + + shutdownAndJoinIOThread(); + + invalidatePendingDirectoryLocks(); + + stopCrashBrowserTimer(); +} + +void QuotaManager::InitQuotaForOrigin( + const FullOriginMetadata& aFullOriginMetadata, + const ClientUsageArray& aClientUsages, uint64_t aUsageBytes) { + AssertIsOnIOThread(); + MOZ_ASSERT(IsBestEffortPersistenceType(aFullOriginMetadata.mPersistenceType)); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo( + aFullOriginMetadata.mPersistenceType, aFullOriginMetadata.mSuffix, + aFullOriginMetadata.mGroup); + + groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>( + groupInfo, aFullOriginMetadata.mOrigin, aClientUsages, aUsageBytes, + aFullOriginMetadata.mLastAccessTime, aFullOriginMetadata.mPersisted, + /* aDirectoryExists */ true)); +} + +void QuotaManager::EnsureQuotaForOrigin(const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + MOZ_ASSERT(IsBestEffortPersistenceType(aOriginMetadata.mPersistenceType)); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo( + aOriginMetadata.mPersistenceType, aOriginMetadata.mSuffix, + aOriginMetadata.mGroup); + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + if (!originInfo) { + groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>( + groupInfo, aOriginMetadata.mOrigin, ClientUsageArray(), + /* aUsageBytes */ 0, + /* aAccessTime */ PR_Now(), /* aPersisted */ false, + /* aDirectoryExists */ false)); + } +} + +int64_t QuotaManager::NoteOriginDirectoryCreated( + const OriginMetadata& aOriginMetadata, bool aPersisted) { + AssertIsOnIOThread(); + MOZ_ASSERT(IsBestEffortPersistenceType(aOriginMetadata.mPersistenceType)); + + int64_t timestamp; + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo( + aOriginMetadata.mPersistenceType, aOriginMetadata.mSuffix, + aOriginMetadata.mGroup); + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + if (originInfo) { + timestamp = originInfo->LockedAccessTime(); + originInfo->mPersisted = aPersisted; + originInfo->mDirectoryExists = true; + } else { + timestamp = PR_Now(); + groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>( + groupInfo, aOriginMetadata.mOrigin, ClientUsageArray(), + /* aUsageBytes */ 0, + /* aAccessTime */ timestamp, aPersisted, /* aDirectoryExists */ true)); + } + + return timestamp; +} + +void QuotaManager::DecreaseUsageForClient(const ClientMetadata& aClientMetadata, + int64_t aSize) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(IsBestEffortPersistenceType(aClientMetadata.mPersistenceType)); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aClientMetadata.mGroup, &pair)) { + return; + } + + RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(aClientMetadata.mPersistenceType); + if (!groupInfo) { + return; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aClientMetadata.mOrigin); + if (originInfo) { + originInfo->LockedDecreaseUsage(aClientMetadata.mClientType, aSize); + } +} + +void QuotaManager::ResetUsageForClient(const ClientMetadata& aClientMetadata) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(IsBestEffortPersistenceType(aClientMetadata.mPersistenceType)); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aClientMetadata.mGroup, &pair)) { + return; + } + + RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(aClientMetadata.mPersistenceType); + if (!groupInfo) { + return; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aClientMetadata.mOrigin); + if (originInfo) { + originInfo->LockedResetUsageForClient(aClientMetadata.mClientType); + } +} + +UsageInfo QuotaManager::GetUsageForClient(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + Client::Type aClientType) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) { + return UsageInfo{}; + } + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + return UsageInfo{}; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + if (!originInfo) { + return UsageInfo{}; + } + + return originInfo->LockedGetUsageForClient(aClientType); +} + +void QuotaManager::UpdateOriginAccessTime( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + MOZ_ASSERT(!IsShuttingDown()); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) { + return; + } + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + return; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + if (originInfo) { + int64_t timestamp = PR_Now(); + originInfo->LockedUpdateAccessTime(timestamp); + + MutexAutoUnlock autoUnlock(mQuotaMutex); + + auto op = MakeRefPtr<SaveOriginAccessTimeOp>( + aPersistenceType, aOriginMetadata.mOrigin, timestamp); + + RegisterNormalOriginOp(*op); + + op->RunImmediately(); + } +} + +void QuotaManager::RemoveQuota() { + AssertIsOnIOThread(); + + MutexAutoLock lock(mQuotaMutex); + + for (const auto& entry : mGroupInfoPairs) { + const auto& pair = entry.GetData(); + + MOZ_ASSERT(!entry.GetKey().IsEmpty()); + MOZ_ASSERT(pair); + + RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY); + if (groupInfo) { + groupInfo->LockedRemoveOriginInfos(); + } + + groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT); + if (groupInfo) { + groupInfo->LockedRemoveOriginInfos(); + } + } + + mGroupInfoPairs.Clear(); + + MOZ_ASSERT(mTemporaryStorageUsage == 0, "Should be zero!"); +} + +nsresult QuotaManager::LoadQuota() { + AssertIsOnIOThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(!mTemporaryStorageInitialized); + + // A list of all unaccessed default or temporary origins. + nsTArray<FullOriginMetadata> unaccessedOrigins; + + auto MaybeCollectUnaccessedOrigin = + [loadQuotaInfoStartTime = PR_Now(), + &unaccessedOrigins](auto& fullOriginMetadata) { + if (IsOriginUnaccessed(fullOriginMetadata, loadQuotaInfoStartTime)) { + unaccessedOrigins.AppendElement(std::move(fullOriginMetadata)); + } + }; + + auto recordQuotaInfoLoadTimeHelper = + MakeRefPtr<RecordQuotaInfoLoadTimeHelper>(); + + const auto startTime = recordQuotaInfoLoadTimeHelper->Start(); + + auto LoadQuotaFromCache = [&]() -> nsresult { + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement, + "SELECT repository_id, suffix, group_, " + "origin, client_usages, usage, " + "last_access_time, accessed, persisted " + "FROM origin"_ns)); + + auto autoRemoveQuota = MakeScopeExit([&] { + RemoveQuota(); + unaccessedOrigins.Clear(); + }); + + QM_TRY(quota::CollectWhileHasResult( + *stmt, + [this, + &MaybeCollectUnaccessedOrigin](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const int32_t& repositoryId, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0)); + + const auto maybePersistenceType = + PersistenceTypeFromInt32(repositoryId, fallible); + QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE)); + + FullOriginMetadata fullOriginMetadata; + + fullOriginMetadata.mPersistenceType = maybePersistenceType.value(); + + QM_TRY_UNWRAP(fullOriginMetadata.mSuffix, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt, + GetUTF8String, 1)); + + QM_TRY_UNWRAP(fullOriginMetadata.mGroup, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt, + GetUTF8String, 2)); + + QM_TRY_UNWRAP(fullOriginMetadata.mOrigin, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt, + GetUTF8String, 3)); + + QM_TRY_INSPECT(const auto& clientUsagesText, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt, + GetUTF8String, 4)); + + ClientUsageArray clientUsages; + QM_TRY(MOZ_TO_RESULT(clientUsages.Deserialize(clientUsagesText))); + + QM_TRY_INSPECT(const int64_t& usage, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 5)); + QM_TRY_UNWRAP(fullOriginMetadata.mLastAccessTime, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 6)); + QM_TRY_INSPECT(const int64_t& accessed, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 7)); + QM_TRY_UNWRAP(fullOriginMetadata.mPersisted, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 8)); + + QM_TRY_INSPECT(const bool& groupUpdated, + MaybeUpdateGroupForOrigin(fullOriginMetadata)); + + Unused << groupUpdated; + + QM_TRY_INSPECT( + const bool& lastAccessTimeUpdated, + MaybeUpdateLastAccessTimeForOrigin(fullOriginMetadata)); + + Unused << lastAccessTimeUpdated; + + // We don't need to update the .metadata-v2 file on disk here, + // EnsureTemporaryOriginIsInitialized is responsible for doing that. + // We just need to use correct group and last access time before + // initializing quota for the given origin. (Note that calling + // LoadFullOriginMetadataWithRestore below might update the group in + // the metadata file, but only as a side-effect. The actual place we + // ensure consistency is in EnsureTemporaryOriginIsInitialized.) + + if (accessed) { + QM_TRY_INSPECT( + const auto& directory, + GetDirectoryForOrigin(fullOriginMetadata.mPersistenceType, + fullOriginMetadata.mOrigin)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); + + QM_TRY(OkIf(exists), Err(NS_ERROR_FAILURE)); + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, IsDirectory)); + + QM_TRY(OkIf(isDirectory), Err(NS_ERROR_FAILURE)); + + // Calling LoadFullOriginMetadataWithRestore might update the group + // in the metadata file, but only as a side-effect. The actual place + // we ensure consistency is in EnsureTemporaryOriginIsInitialized. + + QM_TRY_INSPECT(const auto& metadata, + LoadFullOriginMetadataWithRestore(directory)); + + QM_TRY(OkIf(fullOriginMetadata.mLastAccessTime == + metadata.mLastAccessTime), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(fullOriginMetadata.mPersisted == metadata.mPersisted), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(fullOriginMetadata.mPersistenceType == + metadata.mPersistenceType), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(fullOriginMetadata.mSuffix == metadata.mSuffix), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(fullOriginMetadata.mGroup == metadata.mGroup), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(fullOriginMetadata.mOrigin == metadata.mOrigin), + Err(NS_ERROR_FAILURE)); + + QM_TRY(MOZ_TO_RESULT(InitializeOrigin( + fullOriginMetadata.mPersistenceType, fullOriginMetadata, + fullOriginMetadata.mLastAccessTime, + fullOriginMetadata.mPersisted, directory))); + } else { + InitQuotaForOrigin(fullOriginMetadata, clientUsages, usage); + } + + MaybeCollectUnaccessedOrigin(fullOriginMetadata); + + return Ok{}; + })); + + autoRemoveQuota.release(); + + return NS_OK; + }; + + QM_TRY_INSPECT( + const bool& loadQuotaFromCache, ([this]() -> Result<bool, nsresult> { + if (mCacheUsable) { + QM_TRY_INSPECT( + const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + *mStorageConnection, "SELECT valid, build_id FROM cache"_ns)); + + QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY_INSPECT(const int32_t& valid, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0)); + + if (valid) { + if (!StaticPrefs::dom_quotaManager_caching_checkBuildId()) { + return true; + } + + QM_TRY_INSPECT(const auto& buildId, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoCString, stmt, GetUTF8String, 1)); + + return buildId == *gBuildId; + } + } + + return false; + }())); + + auto autoRemoveQuota = MakeScopeExit([&] { RemoveQuota(); }); + + if (!loadQuotaFromCache || + !StaticPrefs::dom_quotaManager_loadQuotaFromCache() || + ![&LoadQuotaFromCache] { + QM_WARNONLY_TRY_UNWRAP(auto res, MOZ_TO_RESULT(LoadQuotaFromCache())); + return static_cast<bool>(res); + }()) { + // A keeper to defer the return only in Nightly, so that the telemetry data + // for whole profile can be collected. +#ifdef NIGHTLY_BUILD + nsresult statusKeeper = NS_OK; +#endif + + const auto statusKeeperFunc = [&](const nsresult rv) { + RECORD_IN_NIGHTLY(statusKeeper, rv); + }; + + for (const PersistenceType type : kBestEffortPersistenceTypes) { + if (NS_WARN_IF(IsShuttingDown())) { + RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT); + } + + QM_TRY(([&]() -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(([this, type, &MaybeCollectUnaccessedOrigin] { + const auto innerFunc = [&](const auto&) -> nsresult { + return InitializeRepository(type, + MaybeCollectUnaccessedOrigin); + }; + + return ExecuteInitialization( + type == PERSISTENCE_TYPE_DEFAULT + ? Initialization::DefaultRepository + : Initialization::TemporaryRepository, + innerFunc); + }())), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }())); + } + +#ifdef NIGHTLY_BUILD + if (NS_FAILED(statusKeeper)) { + return statusKeeper; + } +#endif + } + + autoRemoveQuota.release(); + + const auto endTime = recordQuotaInfoLoadTimeHelper->End(); + + if (StaticPrefs::dom_quotaManager_checkQuotaInfoLoadTime() && + static_cast<uint32_t>((endTime - startTime).ToMilliseconds()) >= + StaticPrefs::dom_quotaManager_longQuotaInfoLoadTimeThresholdMs() && + !unaccessedOrigins.IsEmpty()) { + QM_WARNONLY_TRY(ArchiveOrigins(unaccessedOrigins)); + } + + return NS_OK; +} + +void QuotaManager::UnloadQuota() { + AssertIsOnIOThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(mTemporaryStorageInitialized); + MOZ_ASSERT(mCacheUsable); + + auto autoRemoveQuota = MakeScopeExit([&] { RemoveQuota(); }); + + mozStorageTransaction transaction( + mStorageConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(transaction.Start()), QM_VOID); + + QM_TRY(MOZ_TO_RESULT( + mStorageConnection->ExecuteSimpleSQL("DELETE FROM origin;"_ns)), + QM_VOID); + + nsCOMPtr<mozIStorageStatement> insertStmt; + + { + MutexAutoLock lock(mQuotaMutex); + + for (auto iter = mGroupInfoPairs.Iter(); !iter.Done(); iter.Next()) { + MOZ_ASSERT(!iter.Key().IsEmpty()); + + GroupInfoPair* const pair = iter.UserData(); + MOZ_ASSERT(pair); + + for (const PersistenceType type : kBestEffortPersistenceTypes) { + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type); + if (!groupInfo) { + continue; + } + + for (const auto& originInfo : groupInfo->mOriginInfos) { + MOZ_ASSERT(!originInfo->mCanonicalQuotaObjects.Count()); + + if (!originInfo->mDirectoryExists) { + continue; + } + + if (insertStmt) { + MOZ_ALWAYS_SUCCEEDS(insertStmt->Reset()); + } else { + QM_TRY_UNWRAP( + insertStmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, mStorageConnection, + CreateStatement, + "INSERT INTO origin (repository_id, suffix, group_, " + "origin, client_usages, usage, last_access_time, " + "accessed, persisted) " + "VALUES (:repository_id, :suffix, :group_, :origin, " + ":client_usages, :usage, :last_access_time, :accessed, " + ":persisted)"_ns), + QM_VOID); + } + + QM_TRY(MOZ_TO_RESULT(originInfo->LockedBindToStatement(insertStmt)), + QM_VOID); + + QM_TRY(MOZ_TO_RESULT(insertStmt->Execute()), QM_VOID); + } + + groupInfo->LockedRemoveOriginInfos(); + } + + iter.Remove(); + } + } + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement, + "UPDATE cache SET valid = :valid, build_id = :buildId;"_ns), + QM_VOID); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("valid"_ns, 1)), QM_VOID); + QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByName("buildId"_ns, *gBuildId)), + QM_VOID); + QM_TRY(MOZ_TO_RESULT(stmt->Execute()), QM_VOID); + QM_TRY(MOZ_TO_RESULT(transaction.Commit()), QM_VOID); +} + +already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + Client::Type aClientType, nsIFile* aFile, int64_t aFileSize, + int64_t* aFileSizeOut /* = nullptr */) { + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + + if (aFileSizeOut) { + *aFileSizeOut = 0; + } + + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + return nullptr; + } + + QM_TRY_INSPECT(const auto& path, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, aFile, GetPath), + nullptr); + +#ifdef DEBUG + { + QM_TRY_INSPECT( + const auto& directory, + GetDirectoryForOrigin(aPersistenceType, aOriginMetadata.mOrigin), + nullptr); + + nsAutoString clientType; + QM_TRY(OkIf(Client::TypeToText(aClientType, clientType, fallible)), + nullptr); + + QM_TRY(MOZ_TO_RESULT(directory->Append(clientType)), nullptr); + + QM_TRY_INSPECT( + const auto& directoryPath, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, directory, GetPath), + nullptr); + + MOZ_ASSERT(StringBeginsWith(path, directoryPath)); + } +#endif + + QM_TRY_INSPECT( + const int64_t fileSize, + ([&aFile, aFileSize]() -> Result<int64_t, nsresult> { + if (aFileSize == -1) { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aFile, Exists)); + + if (exists) { + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(aFile, GetFileSize)); + } + + return 0; + } + + return aFileSize; + }()), + nullptr); + + RefPtr<QuotaObject> result; + { + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) { + return nullptr; + } + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + + if (!groupInfo) { + return nullptr; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + + if (!originInfo) { + return nullptr; + } + + // We need this extra raw pointer because we can't assign to the smart + // pointer directly since QuotaObject::AddRef would try to acquire the same + // mutex. + const NotNull<CanonicalQuotaObject*> canonicalQuotaObject = + originInfo->mCanonicalQuotaObjects.LookupOrInsertWith(path, [&] { + // Create a new QuotaObject. The hashtable is not responsible to + // delete the QuotaObject. + return WrapNotNullUnchecked(new CanonicalQuotaObject( + originInfo, aClientType, path, fileSize)); + }); + + // Addref the QuotaObject and move the ownership to the result. This must + // happen before we unlock! + result = canonicalQuotaObject->LockedAddRef(); + } + + if (aFileSizeOut) { + *aFileSizeOut = fileSize; + } + + // The caller becomes the owner of the QuotaObject, that is, the caller is + // is responsible to delete it when the last reference is removed. + return result.forget(); +} + +already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + Client::Type aClientType, const nsAString& aPath, int64_t aFileSize, + int64_t* aFileSizeOut /* = nullptr */) { + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + + if (aFileSizeOut) { + *aFileSizeOut = 0; + } + + QM_TRY_INSPECT(const auto& file, QM_NewLocalFile(aPath), nullptr); + + return GetQuotaObject(aPersistenceType, aOriginMetadata, aClientType, file, + aFileSize, aFileSizeOut); +} + +already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject( + const int64_t aDirectoryLockId, const nsAString& aPath) { + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + + Maybe<MutexAutoLock> lock; + + // See the comment for mDirectoryLockIdTable in QuotaManager.h + if (!IsOnBackgroundThread()) { + lock.emplace(mQuotaMutex); + } + + if (auto maybeDirectoryLock = + mDirectoryLockIdTable.MaybeGet(aDirectoryLockId)) { + const auto& directoryLock = *maybeDirectoryLock; + MOZ_DIAGNOSTIC_ASSERT(directoryLock->ShouldUpdateLockIdTable()); + + const PersistenceType persistenceType = directoryLock->GetPersistenceType(); + const OriginMetadata& originMetadata = directoryLock->OriginMetadata(); + const Client::Type clientType = directoryLock->ClientType(); + + lock.reset(); + + return GetQuotaObject(persistenceType, originMetadata, clientType, aPath); + } + + MOZ_CRASH("Getting quota object for an unregistered directory lock?"); +} + +Nullable<bool> QuotaManager::OriginPersisted( + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<OriginInfo> originInfo = + LockedGetOriginInfo(PERSISTENCE_TYPE_DEFAULT, aOriginMetadata); + if (originInfo) { + return Nullable<bool>(originInfo->LockedPersisted()); + } + + return Nullable<bool>(); +} + +void QuotaManager::PersistOrigin(const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<OriginInfo> originInfo = + LockedGetOriginInfo(PERSISTENCE_TYPE_DEFAULT, aOriginMetadata); + if (originInfo && !originInfo->LockedPersisted()) { + originInfo->LockedPersist(); + } +} + +void QuotaManager::AbortOperationsForLocks( + const DirectoryLockIdTableArray& aLockIds) { + for (Client::Type type : AllClientTypes()) { + if (aLockIds[type].Filled()) { + (*mClients)[type]->AbortOperationsForLocks(aLockIds[type]); + } + } +} + +void QuotaManager::AbortOperationsForProcess(ContentParentId aContentParentId) { + AssertIsOnOwningThread(); + + for (const RefPtr<Client>& client : *mClients) { + client->AbortOperationsForProcess(aContentParentId); + } +} + +Result<nsCOMPtr<nsIFile>, nsresult> QuotaManager::GetDirectoryForOrigin( + PersistenceType aPersistenceType, const nsACString& aASCIIOrigin) const { + QM_TRY_UNWRAP(auto directory, + QM_NewLocalFile(GetStoragePath(aPersistenceType))); + + QM_TRY(MOZ_TO_RESULT( + directory->Append(MakeSanitizedOriginString(aASCIIOrigin)))); + + return directory; +} + +nsresult QuotaManager::RestoreDirectoryMetadata2(nsIFile* aDirectory) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(mStorageConnection); + + RefPtr<RestoreDirectoryMetadata2Helper> helper = + new RestoreDirectoryMetadata2Helper(aDirectory); + + QM_TRY(MOZ_TO_RESULT(helper->Init())); + + QM_TRY(MOZ_TO_RESULT(helper->RestoreMetadata2File())); + + return NS_OK; +} + +Result<FullOriginMetadata, nsresult> QuotaManager::LoadFullOriginMetadata( + nsIFile* aDirectory, PersistenceType aPersistenceType) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(mStorageConnection); + + QM_TRY_INSPECT(const auto& binaryStream, + GetBinaryInputStream(*aDirectory, + nsLiteralString(METADATA_V2_FILE_NAME))); + + FullOriginMetadata fullOriginMetadata; + + QM_TRY_UNWRAP(fullOriginMetadata.mLastAccessTime, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64)); + + QM_TRY_UNWRAP(fullOriginMetadata.mPersisted, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean)); + + QM_TRY_INSPECT(const bool& reservedData1, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32)); + Unused << reservedData1; + + // XXX Use for the persistence type. + QM_TRY_INSPECT(const bool& reservedData2, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32)); + Unused << reservedData2; + + fullOriginMetadata.mPersistenceType = aPersistenceType; + + QM_TRY_UNWRAP( + fullOriginMetadata.mSuffix, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, binaryStream, ReadCString)); + + QM_TRY_UNWRAP( + fullOriginMetadata.mGroup, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, binaryStream, ReadCString)); + + QM_TRY_UNWRAP( + fullOriginMetadata.mOrigin, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, binaryStream, ReadCString)); + + // Currently unused (used to be isApp). + QM_TRY_INSPECT(const bool& dummy, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean)); + Unused << dummy; + + QM_TRY(MOZ_TO_RESULT(binaryStream->Close())); + + QM_TRY_INSPECT(const bool& groupUpdated, + MaybeUpdateGroupForOrigin(fullOriginMetadata)); + + // A workaround for a bug in GetLastModifiedTime implementation which should + // have returned the current time instead of INT64_MIN when there were no + // suitable files for getting last modified time. + QM_TRY_INSPECT(const bool& lastAccessTimeUpdated, + MaybeUpdateLastAccessTimeForOrigin(fullOriginMetadata)); + + if (groupUpdated || lastAccessTimeUpdated) { + // Only overwriting .metadata-v2 (used to overwrite .metadata too) to reduce + // I/O. + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aDirectory, fullOriginMetadata.mLastAccessTime, + fullOriginMetadata.mPersisted, fullOriginMetadata))); + } + + return fullOriginMetadata; +} + +Result<FullOriginMetadata, nsresult> +QuotaManager::LoadFullOriginMetadataWithRestore(nsIFile* aDirectory) { + // XXX Once the persistence type is stored in the metadata file, this block + // for getting the persistence type from the parent directory name can be + // removed. + nsCOMPtr<nsIFile> parentDir; + QM_TRY(MOZ_TO_RESULT(aDirectory->GetParent(getter_AddRefs(parentDir)))); + + const auto maybePersistenceType = + PersistenceTypeFromFile(*parentDir, fallible); + QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE)); + + const auto& persistenceType = maybePersistenceType.value(); + + QM_TRY_RETURN(QM_OR_ELSE_WARN( + // Expression. + LoadFullOriginMetadata(aDirectory, persistenceType), + // Fallback. + ([&aDirectory, &persistenceType, + this](const nsresult rv) -> Result<FullOriginMetadata, nsresult> { + QM_TRY(MOZ_TO_RESULT(RestoreDirectoryMetadata2(aDirectory))); + + QM_TRY_RETURN(LoadFullOriginMetadata(aDirectory, persistenceType)); + }))); +} + +template <typename OriginFunc> +nsresult QuotaManager::InitializeRepository(PersistenceType aPersistenceType, + OriginFunc&& aOriginFunc) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_TEMPORARY || + aPersistenceType == PERSISTENCE_TYPE_DEFAULT); + + QM_TRY_INSPECT(const auto& directory, + QM_NewLocalFile(GetStoragePath(aPersistenceType))); + + QM_TRY_INSPECT(const bool& created, EnsureDirectory(*directory)); + + Unused << created; + + // A keeper to defer the return only in Nightly, so that the telemetry data + // for whole profile can be collected +#ifdef NIGHTLY_BUILD + nsresult statusKeeper = NS_OK; +#endif + + const auto statusKeeperFunc = [&](const nsresult rv) { + RECORD_IN_NIGHTLY(statusKeeper, rv); + }; + + struct RenameAndInitInfo { + nsCOMPtr<nsIFile> mOriginDirectory; + FullOriginMetadata mFullOriginMetadata; + int64_t mTimestamp; + bool mPersisted; + }; + nsTArray<RenameAndInitInfo> renameAndInitInfos; + + QM_TRY(([&]() -> Result<Ok, nsresult> { + QM_TRY( + CollectEachFile( + *directory, + [&](nsCOMPtr<nsIFile>&& childDirectory) -> Result<Ok, nsresult> { + if (NS_WARN_IF(IsShuttingDown())) { + RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT); + } + + QM_TRY( + ([this, &childDirectory, &renameAndInitInfos, + aPersistenceType, &aOriginFunc]() -> Result<Ok, nsresult> { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoString, childDirectory, GetLeafName)); + + QM_TRY_INSPECT(const auto& dirEntryKind, + GetDirEntryKind(*childDirectory)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: { + QM_TRY_UNWRAP( + auto metadata, + LoadFullOriginMetadataWithRestore(childDirectory)); + + MOZ_ASSERT(metadata.mPersistenceType == + aPersistenceType); + + // FIXME(tt): The check for origin name consistency can + // be removed once we have an upgrade to traverse origin + // directories and check through the directory metadata + // files. + const auto originSanitized = + MakeSanitizedOriginCString(metadata.mOrigin); + + NS_ConvertUTF16toUTF8 utf8LeafName(leafName); + if (!originSanitized.Equals(utf8LeafName)) { + QM_WARNING( + "The name of the origin directory (%s) doesn't " + "match the sanitized origin string (%s) in the " + "metadata file!", + utf8LeafName.get(), originSanitized.get()); + + // If it's the known case, we try to restore the + // origin directory name if it's possible. + if (originSanitized.Equals(utf8LeafName + "."_ns)) { + const int64_t lastAccessTime = + metadata.mLastAccessTime; + const bool persisted = metadata.mPersisted; + renameAndInitInfos.AppendElement(RenameAndInitInfo{ + std::move(childDirectory), std::move(metadata), + lastAccessTime, persisted}); + break; + } + + // XXXtt: Try to restore the unknown cases base on the + // content for their metadata files. Note that if the + // restore fails, QM should maintain a list and ensure + // they won't be accessed after initialization. + } + + QM_TRY(QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT(InitializeOrigin( + aPersistenceType, metadata, + metadata.mLastAccessTime, metadata.mPersisted, + childDirectory)), + // Predicate. + IsDatabaseCorruptionError, + // Fallback. + ([&childDirectory]( + const nsresult rv) -> Result<Ok, nsresult> { + // If the origin can't be initialized due to + // corruption, this is a permanent + // condition, and we need to remove all data + // for the origin on disk. + + QM_TRY( + MOZ_TO_RESULT(childDirectory->Remove(true))); + + return Ok{}; + }))); + + std::forward<OriginFunc>(aOriginFunc)(metadata); + + break; + } + + case nsIFileKind::ExistsAsFile: + if (IsOSMetadata(leafName) || IsDotFile(leafName)) { + break; + } + + // Unknown files during initialization are now allowed. + // Just warn if we find them. + UNKNOWN_FILE_WARNING(leafName); + break; + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while + // iterating. + break; + } + + return Ok{}; + }()), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }())); + + for (auto& info : renameAndInitInfos) { + QM_TRY(([&]() -> Result<Ok, nsresult> { + QM_TRY(([&directory, &info, this, aPersistenceType, + &aOriginFunc]() -> Result<Ok, nsresult> { + const auto originDirName = + MakeSanitizedOriginString(info.mFullOriginMetadata.mOrigin); + + // Check if targetDirectory exist. + QM_TRY_INSPECT(const auto& targetDirectory, + CloneFileAndAppend(*directory, originDirName)); + + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER( + targetDirectory, Exists)); + + if (exists) { + QM_TRY(MOZ_TO_RESULT(info.mOriginDirectory->Remove(true))); + + return Ok{}; + } + + QM_TRY(MOZ_TO_RESULT( + info.mOriginDirectory->RenameTo(nullptr, originDirName))); + + // XXX We don't check corruption here ? + QM_TRY(MOZ_TO_RESULT(InitializeOrigin( + aPersistenceType, info.mFullOriginMetadata, info.mTimestamp, + info.mPersisted, targetDirectory))); + + std::forward<OriginFunc>(aOriginFunc)(info.mFullOriginMetadata); + + return Ok{}; + }()), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }())); + } + +#ifdef NIGHTLY_BUILD + if (NS_FAILED(statusKeeper)) { + return statusKeeper; + } +#endif + + return NS_OK; +} + +nsresult QuotaManager::InitializeOrigin(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + int64_t aAccessTime, bool aPersisted, + nsIFile* aDirectory) { + AssertIsOnIOThread(); + + const bool trackQuota = aPersistenceType != PERSISTENCE_TYPE_PERSISTENT; + + // We need to initialize directories of all clients if they exists and also + // get the total usage to initialize the quota. + + ClientUsageArray clientUsages; + + // A keeper to defer the return only in Nightly, so that the telemetry data + // for whole profile can be collected +#ifdef NIGHTLY_BUILD + nsresult statusKeeper = NS_OK; +#endif + + QM_TRY(([&, statusKeeperFunc = [&](const nsresult rv) { + RECORD_IN_NIGHTLY(statusKeeper, rv); + }]() -> Result<Ok, nsresult> { + QM_TRY( + CollectEachFile( + *aDirectory, + [&](const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> { + if (NS_WARN_IF(IsShuttingDown())) { + RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT); + } + + QM_TRY( + ([this, &file, trackQuota, aPersistenceType, &aOriginMetadata, + &clientUsages]() -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoString, file, GetLeafName)); + + QM_TRY_INSPECT(const auto& dirEntryKind, + GetDirEntryKind(*file)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: { + Client::Type clientType; + const bool ok = Client::TypeFromText( + leafName, clientType, fallible); + if (!ok) { + // Unknown directories during initialization are now + // allowed. Just warn if we find them. + UNKNOWN_FILE_WARNING(leafName); + break; + } + + if (trackQuota) { + QM_TRY_INSPECT( + const auto& usageInfo, + (*mClients)[clientType]->InitOrigin( + aPersistenceType, aOriginMetadata, + /* aCanceled */ Atomic<bool>(false))); + + MOZ_ASSERT(!clientUsages[clientType]); + + if (usageInfo.TotalUsage()) { + // XXX(Bug 1683863) Until we identify the root cause + // of seemingly converted-from-negative usage + // values, we will just treat them as unset here, + // but log a warning to the browser console. + if (static_cast<int64_t>(*usageInfo.TotalUsage()) >= + 0) { + clientUsages[clientType] = usageInfo.TotalUsage(); + } else { +#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG) + const nsCOMPtr<nsIConsoleService> console = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (console) { + console->LogStringMessage( + nsString( + u"QuotaManager warning: client "_ns + + leafName + + u" reported negative usage for group "_ns + + NS_ConvertUTF8toUTF16( + aOriginMetadata.mGroup) + + u", origin "_ns + + NS_ConvertUTF8toUTF16( + aOriginMetadata.mOrigin)) + .get()); + } +#endif + } + } + } else { + QM_TRY(MOZ_TO_RESULT( + (*mClients)[clientType] + ->InitOriginWithoutTracking( + aPersistenceType, aOriginMetadata, + /* aCanceled */ Atomic<bool>(false)))); + } + + break; + } + + case nsIFileKind::ExistsAsFile: + if (IsOriginMetadata(leafName)) { + break; + } + + if (IsTempMetadata(leafName)) { + QM_TRY(MOZ_TO_RESULT( + file->Remove(/* recursive */ false))); + + break; + } + + if (IsOSMetadata(leafName) || IsDotFile(leafName)) { + break; + } + + // Unknown files during initialization are now allowed. + // Just warn if we find them. + UNKNOWN_FILE_WARNING(leafName); + // Bug 1595448 will handle the case for unknown files + // like idb, cache, or ls. + break; + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while + // iterating. + break; + } + + return Ok{}; + }()), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }())); + +#ifdef NIGHTLY_BUILD + if (NS_FAILED(statusKeeper)) { + return statusKeeper; + } +#endif + + if (trackQuota) { + const auto usage = std::accumulate( + clientUsages.cbegin(), clientUsages.cend(), CheckedUint64(0), + [](CheckedUint64 value, const Maybe<uint64_t>& clientUsage) { + return value + clientUsage.valueOr(0); + }); + + // XXX Should we log more information, i.e. the whole clientUsages array, in + // case usage is not valid? + + QM_TRY(OkIf(usage.isValid()), NS_ERROR_FAILURE); + + InitQuotaForOrigin( + FullOriginMetadata{aOriginMetadata, aPersisted, aAccessTime}, + clientUsages, usage.value()); + } + + return NS_OK; +} + +nsresult +QuotaManager::UpgradeFromIndexedDBDirectoryToPersistentStorageDirectory( + nsIFile* aIndexedDBDir) { + AssertIsOnIOThread(); + MOZ_ASSERT(aIndexedDBDir); + + const auto innerFunc = [this, &aIndexedDBDir](const auto&) -> nsresult { + bool isDirectory; + QM_TRY(MOZ_TO_RESULT(aIndexedDBDir->IsDirectory(&isDirectory))); + + if (!isDirectory) { + NS_WARNING("indexedDB entry is not a directory!"); + return NS_OK; + } + + auto persistentStorageDirOrErr = QM_NewLocalFile(*mStoragePath); + if (NS_WARN_IF(persistentStorageDirOrErr.isErr())) { + return persistentStorageDirOrErr.unwrapErr(); + } + + nsCOMPtr<nsIFile> persistentStorageDir = persistentStorageDirOrErr.unwrap(); + + QM_TRY(MOZ_TO_RESULT(persistentStorageDir->Append( + nsLiteralString(PERSISTENT_DIRECTORY_NAME)))); + + bool exists; + QM_TRY(MOZ_TO_RESULT(persistentStorageDir->Exists(&exists))); + + if (exists) { + QM_WARNING("Deleting old <profile>/indexedDB directory!"); + + QM_TRY(MOZ_TO_RESULT(aIndexedDBDir->Remove(/* aRecursive */ true))); + + return NS_OK; + } + + nsCOMPtr<nsIFile> storageDir; + QM_TRY(MOZ_TO_RESULT( + persistentStorageDir->GetParent(getter_AddRefs(storageDir)))); + + // MoveTo() is atomic if the move happens on the same volume which should + // be our case, so even if we crash in the middle of the operation nothing + // breaks next time we try to initialize. + // However there's a theoretical possibility that the indexedDB directory + // is on different volume, but it should be rare enough that we don't have + // to worry about it. + QM_TRY(MOZ_TO_RESULT(aIndexedDBDir->MoveTo( + storageDir, nsLiteralString(PERSISTENT_DIRECTORY_NAME)))); + + return NS_OK; + }; + + return ExecuteInitialization(Initialization::UpgradeFromIndexedDBDirectory, + innerFunc); +} + +nsresult +QuotaManager::UpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory( + nsIFile* aPersistentStorageDir) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistentStorageDir); + + const auto innerFunc = [this, + &aPersistentStorageDir](const auto&) -> nsresult { + QM_TRY_INSPECT( + const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(aPersistentStorageDir, IsDirectory)); + + if (!isDirectory) { + NS_WARNING("persistent entry is not a directory!"); + return NS_OK; + } + + { + QM_TRY_INSPECT(const auto& defaultStorageDir, + QM_NewLocalFile(*mDefaultStoragePath)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(defaultStorageDir, Exists)); + + if (exists) { + QM_WARNING("Deleting old <profile>/storage/persistent directory!"); + + QM_TRY(MOZ_TO_RESULT( + aPersistentStorageDir->Remove(/* aRecursive */ true))); + + return NS_OK; + } + } + + { + // Create real metadata files for origin directories in persistent + // storage. + auto helper = MakeRefPtr<CreateOrUpgradeDirectoryMetadataHelper>( + aPersistentStorageDir); + + QM_TRY(MOZ_TO_RESULT(helper->Init())); + + QM_TRY(MOZ_TO_RESULT(helper->ProcessRepository())); + + // Upgrade metadata files for origin directories in temporary storage. + QM_TRY_INSPECT(const auto& temporaryStorageDir, + QM_NewLocalFile(*mTemporaryStoragePath)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(temporaryStorageDir, Exists)); + + if (exists) { + QM_TRY_INSPECT( + const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(temporaryStorageDir, IsDirectory)); + + if (!isDirectory) { + NS_WARNING("temporary entry is not a directory!"); + return NS_OK; + } + + helper = MakeRefPtr<CreateOrUpgradeDirectoryMetadataHelper>( + temporaryStorageDir); + + QM_TRY(MOZ_TO_RESULT(helper->Init())); + + QM_TRY(MOZ_TO_RESULT(helper->ProcessRepository())); + } + } + + // And finally rename persistent to default. + QM_TRY(MOZ_TO_RESULT(aPersistentStorageDir->RenameTo( + nullptr, nsLiteralString(DEFAULT_DIRECTORY_NAME)))); + + return NS_OK; + }; + + return ExecuteInitialization( + Initialization::UpgradeFromPersistentStorageDirectory, innerFunc); +} + +template <typename Helper> +nsresult QuotaManager::UpgradeStorage(const int32_t aOldVersion, + const int32_t aNewVersion, + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aNewVersion > aOldVersion); + MOZ_ASSERT(aNewVersion <= kStorageVersion); + MOZ_ASSERT(aConnection); + + for (const PersistenceType persistenceType : kAllPersistenceTypes) { + QM_TRY_UNWRAP(auto directory, + QM_NewLocalFile(GetStoragePath(persistenceType))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); + + if (!exists) { + continue; + } + + RefPtr<UpgradeStorageHelperBase> helper = new Helper(directory); + + QM_TRY(MOZ_TO_RESULT(helper->Init())); + + QM_TRY(MOZ_TO_RESULT(helper->ProcessRepository())); + } + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& storageVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion)); + + MOZ_ASSERT(storageVersion == aOldVersion); + } +#endif + + QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(aNewVersion))); + + return NS_OK; +} + +nsresult QuotaManager::UpgradeStorageFrom0_0To1_0( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + const auto innerFunc = [this, &aConnection](const auto&) -> nsresult { + QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom0_0To1_0Helper>( + 0, MakeStorageVersion(1, 0), aConnection))); + + return NS_OK; + }; + + return ExecuteInitialization(Initialization::UpgradeStorageFrom0_0To1_0, + innerFunc); +} + +nsresult QuotaManager::UpgradeStorageFrom1_0To2_0( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // The upgrade consists of a number of logically distinct bugs that + // intentionally got fixed at the same time to trigger just one major + // version bump. + // + // + // Morgue directory cleanup + // [Feature/Bug]: + // The original bug that added "on demand" morgue cleanup is 1165119. + // + // [Mutations]: + // Morgue directories are removed from all origin directories during the + // upgrade process. Origin initialization and usage calculation doesn't try + // to remove morgue directories anymore. + // + // [Downgrade-incompatible changes]: + // Morgue directories can reappear if user runs an already upgraded profile + // in an older version of Firefox. Morgue directories then prevent current + // Firefox from initializing and using the storage. + // + // + // App data removal + // [Feature/Bug]: + // The bug that removes isApp flags is 1311057. + // + // [Mutations]: + // Origin directories with appIds are removed during the upgrade process. + // + // [Downgrade-incompatible changes]: + // Origin directories with appIds can reappear if user runs an already + // upgraded profile in an older version of Firefox. Origin directories with + // appIds don't prevent current Firefox from initializing and using the + // storage, but they wouldn't ever be removed again, potentially causing + // problems once appId is removed from origin attributes. + // + // + // Strip obsolete origin attributes + // [Feature/Bug]: + // The bug that strips obsolete origin attributes is 1314361. + // + // [Mutations]: + // Origin directories with obsolete origin attributes are renamed and their + // metadata files are updated during the upgrade process. + // + // [Downgrade-incompatible changes]: + // Origin directories with obsolete origin attributes can reappear if user + // runs an already upgraded profile in an older version of Firefox. Origin + // directories with obsolete origin attributes don't prevent current Firefox + // from initializing and using the storage, but they wouldn't ever be upgraded + // again, potentially causing problems in future. + // + // + // File manager directory renaming (client specific) + // [Feature/Bug]: + // The original bug that added "on demand" file manager directory renaming is + // 1056939. + // + // [Mutations]: + // All file manager directories are renamed to contain the ".files" suffix. + // + // [Downgrade-incompatible changes]: + // File manager directories with the ".files" suffix prevent older versions of + // Firefox from initializing and using the storage. + // File manager directories without the ".files" suffix can appear if user + // runs an already upgraded profile in an older version of Firefox. File + // manager directories without the ".files" suffix then prevent current + // Firefox from initializing and using the storage. + + const auto innerFunc = [this, &aConnection](const auto&) -> nsresult { + QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom1_0To2_0Helper>( + MakeStorageVersion(1, 0), MakeStorageVersion(2, 0), aConnection))); + + return NS_OK; + }; + + return ExecuteInitialization(Initialization::UpgradeStorageFrom1_0To2_0, + innerFunc); +} + +nsresult QuotaManager::UpgradeStorageFrom2_0To2_1( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // The upgrade is mainly to create a directory padding file in DOM Cache + // directory to record the overall padding size of an origin. + + const auto innerFunc = [this, &aConnection](const auto&) -> nsresult { + QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom2_0To2_1Helper>( + MakeStorageVersion(2, 0), MakeStorageVersion(2, 1), aConnection))); + + return NS_OK; + }; + + return ExecuteInitialization(Initialization::UpgradeStorageFrom2_0To2_1, + innerFunc); +} + +nsresult QuotaManager::UpgradeStorageFrom2_1To2_2( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // The upgrade is mainly to clean obsolete origins in the repositoies, remove + // asmjs client, and ".tmp" file in the idb folers. + + const auto innerFunc = [this, &aConnection](const auto&) -> nsresult { + QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom2_1To2_2Helper>( + MakeStorageVersion(2, 1), MakeStorageVersion(2, 2), aConnection))); + + return NS_OK; + }; + + return ExecuteInitialization(Initialization::UpgradeStorageFrom2_1To2_2, + innerFunc); +} + +nsresult QuotaManager::UpgradeStorageFrom2_2To2_3( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + const auto innerFunc = [&aConnection](const auto&) -> nsresult { + // Table `database` + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + nsLiteralCString("CREATE TABLE database" + "( cache_version INTEGER NOT NULL DEFAULT 0" + ");")))); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + nsLiteralCString("INSERT INTO database (cache_version) " + "VALUES (0)")))); + +#ifdef DEBUG + { + QM_TRY_INSPECT( + const int32_t& storageVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion)); + + MOZ_ASSERT(storageVersion == MakeStorageVersion(2, 2)); + } +#endif + + QM_TRY( + MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeStorageVersion(2, 3)))); + + return NS_OK; + }; + + return ExecuteInitialization(Initialization::UpgradeStorageFrom2_2To2_3, + innerFunc); +} + +nsresult QuotaManager::MaybeRemoveLocalStorageDataAndArchive( + nsIFile& aLsArchiveFile) { + AssertIsOnIOThread(); + MOZ_ASSERT(!CachedNextGenLocalStorageEnabled()); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists)); + + if (!exists) { + // If the ls archive doesn't exist then ls directories can't exist either. + return NS_OK; + } + + QM_TRY(MOZ_TO_RESULT(MaybeRemoveLocalStorageDirectories())); + + InvalidateQuotaCache(); + + // Finally remove the ls archive, so we don't have to check all origin + // directories next time this method is called. + QM_TRY(MOZ_TO_RESULT(aLsArchiveFile.Remove(false))); + + return NS_OK; +} + +nsresult QuotaManager::MaybeRemoveLocalStorageDirectories() { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& defaultStorageDir, + QM_NewLocalFile(*mDefaultStoragePath)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(defaultStorageDir, Exists)); + + if (!exists) { + return NS_OK; + } + + QM_TRY(CollectEachFile( + *defaultStorageDir, + [](const nsCOMPtr<nsIFile>& originDir) -> Result<Ok, nsresult> { +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(originDir, Exists)); + MOZ_ASSERT(exists); + } +#endif + + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*originDir)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: { + QM_TRY_INSPECT( + const auto& lsDir, + CloneFileAndAppend(*originDir, NS_LITERAL_STRING_FROM_CSTRING( + LS_DIRECTORY_NAME))); + + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(lsDir, Exists)); + + if (!exists) { + return Ok{}; + } + } + + { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(lsDir, IsDirectory)); + + if (!isDirectory) { + QM_WARNING("ls entry is not a directory!"); + + return Ok{}; + } + } + + nsString path; + QM_TRY(MOZ_TO_RESULT(lsDir->GetPath(path))); + + QM_WARNING("Deleting %s directory!", + NS_ConvertUTF16toUTF8(path).get()); + + QM_TRY(MOZ_TO_RESULT(lsDir->Remove(/* aRecursive */ true))); + + break; + } + + case nsIFileKind::ExistsAsFile: { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoString, originDir, GetLeafName)); + + // Unknown files during upgrade are allowed. Just warn if we find + // them. + if (!IsOSMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + + break; + } + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + return Ok{}; + })); + + return NS_OK; +} + +Result<Ok, nsresult> QuotaManager::CopyLocalStorageArchiveFromWebAppsStore( + nsIFile& aLsArchiveFile) const { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists)); + MOZ_ASSERT(!exists); + } +#endif + + // Get the storage service first, we will need it at multiple places. + QM_TRY_INSPECT(const auto& ss, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + // Get the web apps store file. + QM_TRY_INSPECT(const auto& webAppsStoreFile, QM_NewLocalFile(mBasePath)); + + QM_TRY(MOZ_TO_RESULT( + webAppsStoreFile->Append(nsLiteralString(WEB_APPS_STORE_FILE_NAME)))); + + // Now check if the web apps store is useable. + QM_TRY_INSPECT(const auto& connection, + CreateWebAppsStoreConnection(*webAppsStoreFile, *ss)); + + if (connection) { + // Find out the journal mode. + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement( + *connection, "PRAGMA journal_mode;"_ns)); + + QM_TRY_INSPECT(const auto& journalMode, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, *stmt, + GetUTF8String, 0)); + + QM_TRY(MOZ_TO_RESULT(stmt->Finalize())); + + if (journalMode.EqualsLiteral("wal")) { + // We don't copy the WAL file, so make sure the old database is fully + // checkpointed. + QM_TRY(MOZ_TO_RESULT( + connection->ExecuteSimpleSQL("PRAGMA wal_checkpoint(TRUNCATE);"_ns))); + } + + // Explicitely close the connection before the old database is copied. + QM_TRY(MOZ_TO_RESULT(connection->Close())); + + // Copy the old database. The database is copied from + // <profile>/webappsstore.sqlite to + // <profile>/storage/ls-archive-tmp.sqlite + // We use a "-tmp" postfix since we are not done yet. + QM_TRY_INSPECT(const auto& storageDir, QM_NewLocalFile(*mStoragePath)); + + QM_TRY(MOZ_TO_RESULT(webAppsStoreFile->CopyTo( + storageDir, nsLiteralString(LS_ARCHIVE_TMP_FILE_NAME)))); + + QM_TRY_INSPECT(const auto& lsArchiveTmpFile, + GetLocalStorageArchiveTmpFile(*mStoragePath)); + + if (journalMode.EqualsLiteral("wal")) { + QM_TRY_INSPECT( + const auto& lsArchiveTmpConnection, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + lsArchiveTmpFile, mozIStorageService::CONNECTION_DEFAULT)); + + // The archive will only be used for lazy data migration. There won't be + // any concurrent readers and writers that could benefit from Write-Ahead + // Logging. So switch to a standard rollback journal. The standard + // rollback journal also provides atomicity across multiple attached + // databases which is import for the lazy data migration to work safely. + QM_TRY(MOZ_TO_RESULT(lsArchiveTmpConnection->ExecuteSimpleSQL( + "PRAGMA journal_mode = DELETE;"_ns))); + + // Close the connection explicitly. We are going to rename the file below. + QM_TRY(MOZ_TO_RESULT(lsArchiveTmpConnection->Close())); + } + + // Finally, rename ls-archive-tmp.sqlite to ls-archive.sqlite + QM_TRY(MOZ_TO_RESULT(lsArchiveTmpFile->MoveTo( + nullptr, nsLiteralString(LS_ARCHIVE_FILE_NAME)))); + + return Ok{}; + } + + // If webappsstore database is not useable, just create an empty archive. + // XXX The code below should be removed and the caller should call us only + // when webappstore.sqlite exists. CreateWebAppsStoreConnection should be + // reworked to propagate database corruption instead of returning null + // connection. + // So, if there's no webappsstore.sqlite + // MaybeCreateOrUpgradeLocalStorageArchive will call + // CreateEmptyLocalStorageArchive instead of + // CopyLocalStorageArchiveFromWebAppsStore. + // If there's any corruption detected during + // MaybeCreateOrUpgradeLocalStorageArchive (including nested calls like + // CopyLocalStorageArchiveFromWebAppsStore and CreateWebAppsStoreConnection) + // EnsureStorageIsInitialized will fallback to + // CreateEmptyLocalStorageArchive. + + // Ensure the storage directory actually exists. + QM_TRY_INSPECT(const auto& storageDirectory, QM_NewLocalFile(*mStoragePath)); + + QM_TRY_INSPECT(const bool& created, EnsureDirectory(*storageDirectory)); + + Unused << created; + + QM_TRY_UNWRAP(auto lsArchiveConnection, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + &aLsArchiveFile, mozIStorageService::CONNECTION_DEFAULT)); + + QM_TRY(MOZ_TO_RESULT( + StorageDBUpdater::CreateCurrentSchema(lsArchiveConnection))); + + return Ok{}; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> +QuotaManager::CreateLocalStorageArchiveConnection( + nsIFile& aLsArchiveFile) const { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists)); + MOZ_ASSERT(exists); + } +#endif + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, IsDirectory)); + + // A directory with the name of the archive file is treated as corruption + // (similarly as wrong content of the file). + QM_TRY(OkIf(!isDirectory), Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY_INSPECT(const auto& ss, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + // This may return NS_ERROR_FILE_CORRUPTED too. + QM_TRY_UNWRAP(auto connection, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + &aLsArchiveFile, mozIStorageService::CONNECTION_DEFAULT)); + + // The legacy LS implementation removes the database and creates an empty one + // when the schema can't be updated. The same effect can be achieved here by + // mapping all errors to NS_ERROR_FILE_CORRUPTED. One such case is tested by + // sub test case 3 of dom/localstorage/test/unit/test_archive.js + QM_TRY( + MOZ_TO_RESULT(StorageDBUpdater::Update(connection)) + .mapErr([](const nsresult rv) { return NS_ERROR_FILE_CORRUPTED; })); + + return connection; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> +QuotaManager::RecopyLocalStorageArchiveFromWebAppsStore( + nsIFile& aLsArchiveFile) { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + QM_TRY(MOZ_TO_RESULT(MaybeRemoveLocalStorageDirectories())); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists)); + + MOZ_ASSERT(exists); + } +#endif + + QM_TRY(MOZ_TO_RESULT(aLsArchiveFile.Remove(false))); + + QM_TRY(CopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile)); + + QM_TRY_UNWRAP(auto connection, + CreateLocalStorageArchiveConnection(aLsArchiveFile)); + + QM_TRY(MOZ_TO_RESULT(InitializeLocalStorageArchive(connection))); + + return connection; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> +QuotaManager::DowngradeLocalStorageArchive(nsIFile& aLsArchiveFile) { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + QM_TRY_UNWRAP(auto connection, + RecopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile)); + + QM_TRY(MOZ_TO_RESULT( + SaveLocalStorageArchiveVersion(connection, kLocalStorageArchiveVersion))); + + return connection; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> +QuotaManager::UpgradeLocalStorageArchiveFromLessThan4To4( + nsIFile& aLsArchiveFile) { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + QM_TRY_UNWRAP(auto connection, + RecopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile)); + + QM_TRY(MOZ_TO_RESULT(SaveLocalStorageArchiveVersion(connection, 4))); + + return connection; +} + +/* +nsresult QuotaManager::UpgradeLocalStorageArchiveFrom4To5( + nsCOMPtr<mozIStorageConnection>& aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + nsresult rv = SaveLocalStorageArchiveVersion(aConnection, 5); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} +*/ + +#ifdef DEBUG + +void QuotaManager::AssertStorageIsInitialized() const { + AssertIsOnIOThread(); + MOZ_ASSERT(IsStorageInitialized()); +} + +#endif // DEBUG + +nsresult QuotaManager::MaybeUpgradeToDefaultStorageDirectory( + nsIFile& aStorageFile) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& storageFileExists, + MOZ_TO_RESULT_INVOKE_MEMBER(aStorageFile, Exists)); + + if (!storageFileExists) { + QM_TRY_INSPECT(const auto& indexedDBDir, QM_NewLocalFile(*mIndexedDBPath)); + + QM_TRY_INSPECT(const auto& indexedDBDirExists, + MOZ_TO_RESULT_INVOKE_MEMBER(indexedDBDir, Exists)); + + if (indexedDBDirExists) { + QM_TRY(MOZ_TO_RESULT( + UpgradeFromIndexedDBDirectoryToPersistentStorageDirectory( + indexedDBDir))); + } + + QM_TRY_INSPECT(const auto& persistentStorageDir, + QM_NewLocalFile(*mStoragePath)); + + QM_TRY(MOZ_TO_RESULT(persistentStorageDir->Append( + nsLiteralString(PERSISTENT_DIRECTORY_NAME)))); + + QM_TRY_INSPECT(const auto& persistentStorageDirExists, + MOZ_TO_RESULT_INVOKE_MEMBER(persistentStorageDir, Exists)); + + if (persistentStorageDirExists) { + QM_TRY(MOZ_TO_RESULT( + UpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory( + persistentStorageDir))); + } + } + + return NS_OK; +} + +nsresult QuotaManager::MaybeCreateOrUpgradeStorage( + mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + QM_TRY_UNWRAP(auto storageVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion)); + + // Hacky downgrade logic! + // If we see major.minor of 3.0, downgrade it to be 2.1. + if (storageVersion == kHackyPreDowngradeStorageVersion) { + storageVersion = kHackyPostDowngradeStorageVersion; + QM_TRY(MOZ_TO_RESULT(aConnection.SetSchemaVersion(storageVersion)), + QM_PROPAGATE, + [](const auto&) { MOZ_ASSERT(false, "Downgrade didn't take."); }); + } + + QM_TRY(OkIf(GetMajorStorageVersion(storageVersion) <= kMajorStorageVersion), + NS_ERROR_FAILURE, [](const auto&) { + NS_WARNING("Unable to initialize storage, version is too high!"); + }); + + if (storageVersion < kStorageVersion) { + const bool newDatabase = !storageVersion; + + QM_TRY_INSPECT(const auto& storageDir, QM_NewLocalFile(*mStoragePath)); + + QM_TRY_INSPECT(const auto& storageDirExists, + MOZ_TO_RESULT_INVOKE_MEMBER(storageDir, Exists)); + + const bool newDirectory = !storageDirExists; + + if (newDatabase) { + // Set the page size first. + if (kSQLitePageSizeOverride) { + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(nsPrintfCString( + "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride)))); + } + } + + mozStorageTransaction transaction( + &aConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(transaction.Start())); + + // An upgrade method can upgrade the database, the storage or both. + // The upgrade loop below can only be avoided when there's no database and + // no storage yet (e.g. new profile). + if (newDatabase && newDirectory) { + QM_TRY(MOZ_TO_RESULT(CreateTables(&aConnection))); + +#ifdef DEBUG + { + QM_TRY_INSPECT( + const int32_t& storageVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion), + QM_ASSERT_UNREACHABLE); + MOZ_ASSERT(storageVersion == kStorageVersion); + } +#endif + + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL( + nsLiteralCString("INSERT INTO database (cache_version) " + "VALUES (0)")))); + } else { + // This logic needs to change next time we change the storage! + static_assert(kStorageVersion == int32_t((2 << 16) + 3), + "Upgrade function needed due to storage version increase."); + + while (storageVersion != kStorageVersion) { + if (storageVersion == 0) { + QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom0_0To1_0(&aConnection))); + } else if (storageVersion == MakeStorageVersion(1, 0)) { + QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom1_0To2_0(&aConnection))); + } else if (storageVersion == MakeStorageVersion(2, 0)) { + QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom2_0To2_1(&aConnection))); + } else if (storageVersion == MakeStorageVersion(2, 1)) { + QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom2_1To2_2(&aConnection))); + } else if (storageVersion == MakeStorageVersion(2, 2)) { + QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom2_2To2_3(&aConnection))); + } else { + QM_FAIL(NS_ERROR_FAILURE, []() { + NS_WARNING( + "Unable to initialize storage, no upgrade path is " + "available!"); + }); + } + + QM_TRY_UNWRAP(storageVersion, MOZ_TO_RESULT_INVOKE_MEMBER( + aConnection, GetSchemaVersion)); + } + + MOZ_ASSERT(storageVersion == kStorageVersion); + } + + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + } + + return NS_OK; +} + +OkOrErr QuotaManager::MaybeRemoveLocalStorageArchiveTmpFile() { + AssertIsOnIOThread(); + + QM_TRY_INSPECT( + const auto& lsArchiveTmpFile, + QM_TO_RESULT_TRANSFORM(GetLocalStorageArchiveTmpFile(*mStoragePath))); + + QM_TRY_INSPECT(const bool& exists, + QM_TO_RESULT_INVOKE_MEMBER(lsArchiveTmpFile, Exists)); + + if (exists) { + QM_TRY(QM_TO_RESULT(lsArchiveTmpFile->Remove(false))); + } + + return Ok{}; +} + +Result<Ok, nsresult> QuotaManager::MaybeCreateOrUpgradeLocalStorageArchive( + nsIFile& aLsArchiveFile) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT( + const bool& lsArchiveFileExisted, + ([this, &aLsArchiveFile]() -> Result<bool, nsresult> { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists)); + + if (!exists) { + QM_TRY(CopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile)); + } + + return exists; + }())); + + QM_TRY_UNWRAP(auto connection, + CreateLocalStorageArchiveConnection(aLsArchiveFile)); + + QM_TRY_INSPECT(const auto& initialized, + IsLocalStorageArchiveInitialized(*connection)); + + if (!initialized) { + QM_TRY(MOZ_TO_RESULT(InitializeLocalStorageArchive(connection))); + } + + QM_TRY_UNWRAP(int32_t version, LoadLocalStorageArchiveVersion(*connection)); + + if (version > kLocalStorageArchiveVersion) { + // Close local storage archive connection. We are going to remove underlying + // file. + QM_TRY(MOZ_TO_RESULT(connection->Close())); + + // This will wipe the archive and any migrated data and recopy the archive + // from webappsstore.sqlite. + QM_TRY_UNWRAP(connection, DowngradeLocalStorageArchive(aLsArchiveFile)); + + QM_TRY_UNWRAP(version, LoadLocalStorageArchiveVersion(*connection)); + + MOZ_ASSERT(version == kLocalStorageArchiveVersion); + } else if (version != kLocalStorageArchiveVersion) { + // The version can be zero either when the archive didn't exist or it did + // exist, but the archive was created without any version information. + // We don't need to do any upgrades only if it didn't exist because existing + // archives without version information must be recopied to really fix bug + // 1542104. See also bug 1546305 which introduced archive versions. + if (!lsArchiveFileExisted) { + MOZ_ASSERT(version == 0); + + QM_TRY(MOZ_TO_RESULT(SaveLocalStorageArchiveVersion( + connection, kLocalStorageArchiveVersion))); + } else { + static_assert(kLocalStorageArchiveVersion == 4, + "Upgrade function needed due to LocalStorage archive " + "version increase."); + + while (version != kLocalStorageArchiveVersion) { + if (version < 4) { + // Close local storage archive connection. We are going to remove + // underlying file. + QM_TRY(MOZ_TO_RESULT(connection->Close())); + + // This won't do an "upgrade" in a normal sense. It will wipe the + // archive and any migrated data and recopy the archive from + // webappsstore.sqlite + QM_TRY_UNWRAP(connection, UpgradeLocalStorageArchiveFromLessThan4To4( + aLsArchiveFile)); + } /* else if (version == 4) { + QM_TRY(MOZ_TO_RESULT(UpgradeLocalStorageArchiveFrom4To5(connection))); + } */ + else { + QM_FAIL(Err(NS_ERROR_FAILURE), []() { + QM_WARNING( + "Unable to initialize LocalStorage archive, no upgrade path " + "is available!"); + }); + } + + QM_TRY_UNWRAP(version, LoadLocalStorageArchiveVersion(*connection)); + } + + MOZ_ASSERT(version == kLocalStorageArchiveVersion); + } + } + + // At this point, we have finished initializing the local storage archive, and + // can continue storage initialization. We don't know though if the actual + // data in the archive file is readable. We can't do a PRAGMA integrity_check + // here though, because that would be too heavyweight. + + return Ok{}; +} + +Result<Ok, nsresult> QuotaManager::CreateEmptyLocalStorageArchive( + nsIFile& aLsArchiveFile) const { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists)); + + // If it exists, remove it. It might be a directory, so remove it recursively. + if (exists) { + QM_TRY(MOZ_TO_RESULT(aLsArchiveFile.Remove(true))); + + // XXX If we crash right here, the next session will copy the archive from + // webappsstore.sqlite again! + // XXX Create a marker file before removing the archive which can be + // used in MaybeCreateOrUpgradeLocalStorageArchive to create an empty + // archive instead of recopying it from webapppstore.sqlite (in other + // words, finishing what was started here). + } + + 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(const auto connection, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + &aLsArchiveFile, mozIStorageService::CONNECTION_DEFAULT)); + + QM_TRY(MOZ_TO_RESULT(StorageDBUpdater::CreateCurrentSchema(connection))); + + QM_TRY(MOZ_TO_RESULT(InitializeLocalStorageArchive(connection))); + + QM_TRY(MOZ_TO_RESULT( + SaveLocalStorageArchiveVersion(connection, kLocalStorageArchiveVersion))); + + return Ok{}; +} + +nsresult QuotaManager::EnsureStorageIsInitialized() { + DiagnosticAssertIsOnIOThread(); + + const auto innerFunc = + [&](const auto& firstInitializationAttempt) -> nsresult { + if (mStorageConnection) { + MOZ_ASSERT(firstInitializationAttempt.Recorded()); + return NS_OK; + } + + QM_TRY_INSPECT(const auto& storageFile, QM_NewLocalFile(mBasePath)); + QM_TRY(MOZ_TO_RESULT(storageFile->Append(mStorageName + kSQLiteSuffix))); + + QM_TRY(MOZ_TO_RESULT(MaybeUpgradeToDefaultStorageDirectory(*storageFile))); + + 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, + storageFile, mozIStorageService::CONNECTION_DEFAULT), + // Predicate. + IsDatabaseCorruptionError, + // Fallback. + ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>)); + + if (!connection) { + // Nuke the database file. + QM_TRY(MOZ_TO_RESULT(storageFile->Remove(false))); + + QM_TRY_UNWRAP(connection, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, storageFile, + mozIStorageService::CONNECTION_DEFAULT)); + } + + // We want extra durability for this important file. + QM_TRY(MOZ_TO_RESULT( + connection->ExecuteSimpleSQL("PRAGMA synchronous = EXTRA;"_ns))); + + // Check to make sure that the storage version is correct. + QM_TRY(MOZ_TO_RESULT(MaybeCreateOrUpgradeStorage(*connection))); + + QM_TRY(MaybeRemoveLocalStorageArchiveTmpFile()); + + QM_TRY_INSPECT(const auto& lsArchiveFile, + GetLocalStorageArchiveFile(*mStoragePath)); + + if (CachedNextGenLocalStorageEnabled()) { + QM_TRY(QM_OR_ELSE_WARN_IF( + // Expression. + MaybeCreateOrUpgradeLocalStorageArchive(*lsArchiveFile), + // Predicate. + IsDatabaseCorruptionError, + // Fallback. + ([&](const nsresult rv) -> Result<Ok, nsresult> { + QM_TRY_RETURN(CreateEmptyLocalStorageArchive(*lsArchiveFile)); + }))); + } else { + QM_TRY( + MOZ_TO_RESULT(MaybeRemoveLocalStorageDataAndArchive(*lsArchiveFile))); + } + + QM_TRY_UNWRAP(mCacheUsable, MaybeCreateOrUpgradeCache(*connection)); + + if (mCacheUsable && gInvalidateQuotaCache) { + QM_TRY(InvalidateCache(*connection)); + + gInvalidateQuotaCache = false; + } + + mStorageConnection = std::move(connection); + + return NS_OK; + }; + + return ExecuteInitialization( + Initialization::Storage, + "dom::quota::FirstInitializationAttempt::Storage"_ns, innerFunc); +} + +RefPtr<ClientDirectoryLock> QuotaManager::CreateDirectoryLock( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + Client::Type aClientType, bool aExclusive) { + AssertIsOnOwningThread(); + + return DirectoryLockImpl::Create(WrapNotNullUnchecked(this), aPersistenceType, + aOriginMetadata, aClientType, aExclusive); +} + +RefPtr<UniversalDirectoryLock> QuotaManager::CreateDirectoryLockInternal( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, const Nullable<Client::Type>& aClientType, + bool aExclusive) { + AssertIsOnOwningThread(); + + return DirectoryLockImpl::CreateInternal(WrapNotNullUnchecked(this), + aPersistenceType, aOriginScope, + aClientType, aExclusive); +} + +Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> +QuotaManager::EnsurePersistentOriginIsInitialized( + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginMetadata.mPersistenceType == PERSISTENCE_TYPE_PERSISTENT); + MOZ_DIAGNOSTIC_ASSERT(mStorageConnection); + + const auto innerFunc = [&aOriginMetadata, + this](const auto& firstInitializationAttempt) + -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> { + QM_TRY_UNWRAP(auto directory, + GetDirectoryForOrigin(PERSISTENCE_TYPE_PERSISTENT, + aOriginMetadata.mOrigin)); + + if (mInitializedOrigins.Contains(aOriginMetadata.mOrigin)) { + MOZ_ASSERT(firstInitializationAttempt.Recorded()); + return std::pair(std::move(directory), false); + } + + QM_TRY_INSPECT(const bool& created, EnsureOriginDirectory(*directory)); + + QM_TRY_INSPECT( + const int64_t& timestamp, + ([this, created, &directory, + &aOriginMetadata]() -> Result<int64_t, nsresult> { + if (created) { + const int64_t timestamp = PR_Now(); + + // Only creating .metadata-v2 to reduce IO. + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(*directory, timestamp, + /* aPersisted */ true, + aOriginMetadata))); + + return timestamp; + } + + // Get the metadata. We only use the timestamp. + QM_TRY_INSPECT(const auto& metadata, + LoadFullOriginMetadataWithRestore(directory)); + + MOZ_ASSERT(metadata.mLastAccessTime <= PR_Now()); + + return metadata.mLastAccessTime; + }())); + + QM_TRY(MOZ_TO_RESULT(InitializeOrigin(PERSISTENCE_TYPE_PERSISTENT, + aOriginMetadata, timestamp, + /* aPersisted */ true, directory))); + + mInitializedOrigins.AppendElement(aOriginMetadata.mOrigin); + + return std::pair(std::move(directory), created); + }; + + return ExecuteOriginInitialization( + aOriginMetadata.mOrigin, OriginInitialization::PersistentOrigin, + "dom::quota::FirstOriginInitializationAttempt::PersistentOrigin"_ns, + innerFunc); +} + +Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> +QuotaManager::EnsureTemporaryOriginIsInitialized( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + MOZ_DIAGNOSTIC_ASSERT(mStorageConnection); + MOZ_DIAGNOSTIC_ASSERT(mTemporaryStorageInitialized); + + const auto innerFunc = [&aPersistenceType, &aOriginMetadata, + this](const auto&) + -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> { + // Get directory for this origin and persistence type. + QM_TRY_UNWRAP( + auto directory, + GetDirectoryForOrigin(aPersistenceType, aOriginMetadata.mOrigin)); + + QM_TRY_INSPECT(const bool& created, EnsureOriginDirectory(*directory)); + + if (created) { + const int64_t timestamp = + NoteOriginDirectoryCreated(aOriginMetadata, /* aPersisted */ false); + + // Only creating .metadata-v2 to reduce IO. + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(*directory, timestamp, + /* aPersisted */ false, + aOriginMetadata))); + } + + // TODO: If the metadata file exists and we didn't call + // LoadFullOriginMetadataWithRestore for it (because the quota info + // was loaded from the cache), then the group in the metadata file + // may be wrong, so it should be checked and eventually updated. + // It's not a big deal that we are not doing it here, because the + // origin will be marked as "accessed", so + // LoadFullOriginMetadataWithRestore will be called for the metadata + // file in next session in LoadQuotaFromCache. + + return std::pair(std::move(directory), created); + }; + + return ExecuteOriginInitialization( + aOriginMetadata.mOrigin, OriginInitialization::TemporaryOrigin, + "dom::quota::FirstOriginInitializationAttempt::TemporaryOrigin"_ns, + innerFunc); +} + +nsresult QuotaManager::EnsureTemporaryStorageIsInitialized() { + AssertIsOnIOThread(); + MOZ_DIAGNOSTIC_ASSERT(mStorageConnection); + + const auto innerFunc = + [&](const auto& firstInitializationAttempt) -> nsresult { + if (mTemporaryStorageInitialized) { + MOZ_ASSERT(firstInitializationAttempt.Recorded()); + return NS_OK; + } + + QM_TRY_INSPECT( + const auto& storageDir, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIFile>, + MOZ_SELECT_OVERLOAD(do_CreateInstance), + NS_LOCAL_FILE_CONTRACTID)); + + QM_TRY(MOZ_TO_RESULT(storageDir->InitWithPath(GetStoragePath()))); + + // The storage directory must exist before calling GetTemporaryStorageLimit. + QM_TRY_INSPECT(const bool& created, EnsureDirectory(*storageDir)); + + Unused << created; + + QM_TRY_UNWRAP(mTemporaryStorageLimit, + GetTemporaryStorageLimit(*storageDir)); + + QM_TRY(MOZ_TO_RESULT(LoadQuota())); + + mTemporaryStorageInitialized = true; + + CleanupTemporaryStorage(); + + if (mCacheUsable) { + QM_TRY(InvalidateCache(*mStorageConnection)); + } + + return NS_OK; + }; + + return ExecuteInitialization( + Initialization::TemporaryStorage, + "dom::quota::FirstInitializationAttempt::TemporaryStorage"_ns, innerFunc); +} + +RefPtr<BoolPromise> QuotaManager::ShutdownStorage() { + if (!mShuttingDownStorage) { + mShuttingDownStorage = true; + + auto shutdownStorageOp = MakeRefPtr<ShutdownStorageOp>(); + + RegisterNormalOriginOp(*shutdownStorageOp); + + shutdownStorageOp->RunImmediately(); + + shutdownStorageOp->OnResults()->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr<QuotaManager>(this)](bool aResolveValue) { + self->mShuttingDownStorage = false; + + self->mShutdownStoragePromiseHolder.ResolveIfExists(aResolveValue, + __func__); + }, + [self = RefPtr<QuotaManager>(this)](nsresult aRejectValue) { + self->mShuttingDownStorage = false; + + self->mShutdownStoragePromiseHolder.RejectIfExists(aRejectValue, + __func__); + }); + } + + return mShutdownStoragePromiseHolder.Ensure(__func__); +} + +void QuotaManager::ShutdownStorageInternal() { + AssertIsOnIOThread(); + + if (mStorageConnection) { + mInitializationInfo.ResetOriginInitializationInfos(); + mInitializedOrigins.Clear(); + + if (mTemporaryStorageInitialized) { + if (mCacheUsable) { + UnloadQuota(); + } else { + RemoveQuota(); + } + + mTemporaryStorageInitialized = false; + } + + ReleaseIOThreadObjects(); + + mStorageConnection = nullptr; + mCacheUsable = false; + } + + mInitializationInfo.ResetFirstInitializationAttempts(); +} + +Result<bool, nsresult> QuotaManager::EnsureOriginDirectory( + nsIFile& aDirectory) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Exists)); + + if (!exists) { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, aDirectory, GetLeafName) + .map([](const auto& leafName) { + return NS_ConvertUTF16toUTF8(leafName); + })); + + QM_TRY(OkIf(IsSanitizedOriginValid(leafName)), Err(NS_ERROR_FAILURE), + [](const auto&) { + QM_WARNING( + "Preventing creation of a new origin directory which is not " + "supported by our origin parser or is obsolete!"); + }); + } + + QM_TRY_RETURN(EnsureDirectory(aDirectory)); +} + +nsresult QuotaManager::AboutToClearOrigins( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, + const Nullable<Client::Type>& aClientType) { + AssertIsOnIOThread(); + + if (aClientType.IsNull()) { + for (Client::Type type : AllClientTypes()) { + QM_TRY(MOZ_TO_RESULT((*mClients)[type]->AboutToClearOrigins( + aPersistenceType, aOriginScope))); + } + } else { + QM_TRY(MOZ_TO_RESULT((*mClients)[aClientType.Value()]->AboutToClearOrigins( + aPersistenceType, aOriginScope))); + } + + return NS_OK; +} + +void QuotaManager::OriginClearCompleted( + PersistenceType aPersistenceType, const nsACString& aOrigin, + const Nullable<Client::Type>& aClientType) { + AssertIsOnIOThread(); + + if (aClientType.IsNull()) { + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + mInitializedOrigins.RemoveElement(aOrigin); + } + + for (Client::Type type : AllClientTypes()) { + (*mClients)[type]->OnOriginClearCompleted(aPersistenceType, aOrigin); + } + } else { + (*mClients)[aClientType.Value()]->OnOriginClearCompleted(aPersistenceType, + aOrigin); + } +} + +Client* QuotaManager::GetClient(Client::Type aClientType) { + MOZ_ASSERT(aClientType >= Client::IDB); + MOZ_ASSERT(aClientType < Client::TypeMax()); + + return (*mClients)[aClientType]; +} + +const AutoTArray<Client::Type, Client::TYPE_MAX>& +QuotaManager::AllClientTypes() { + if (CachedNextGenLocalStorageEnabled()) { + return *mAllClientTypes; + } + return *mAllClientTypesExceptLS; +} + +uint64_t QuotaManager::GetGroupLimit() const { + // To avoid one group evicting all the rest, limit the amount any one group + // can use to 20% resp. a fifth. To prevent individual sites from using + // exorbitant amounts of storage where there is a lot of free space, cap the + // group limit to 10GB. + const auto x = std::min<uint64_t>(mTemporaryStorageLimit / 5, 10 GB); + + // In low-storage situations, make an exception (while not exceeding the total + // storage limit). + return std::min<uint64_t>(mTemporaryStorageLimit, + std::max<uint64_t>(x, 10 MB)); +} + +std::pair<uint64_t, uint64_t> QuotaManager::GetUsageAndLimitForEstimate( + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + + uint64_t totalGroupUsage = 0; + + { + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) { + for (const PersistenceType type : kBestEffortPersistenceTypes) { + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type); + if (groupInfo) { + if (type == PERSISTENCE_TYPE_DEFAULT) { + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + + if (originInfo && originInfo->LockedPersisted()) { + return std::pair(mTemporaryStorageUsage, mTemporaryStorageLimit); + } + } + + AssertNoOverflow(totalGroupUsage, groupInfo->mUsage); + totalGroupUsage += groupInfo->mUsage; + } + } + } + } + + return std::pair(totalGroupUsage, GetGroupLimit()); +} + +uint64_t QuotaManager::GetOriginUsage( + const PrincipalMetadata& aPrincipalMetadata) { + AssertIsOnIOThread(); + + uint64_t usage = 0; + + { + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (mGroupInfoPairs.Get(aPrincipalMetadata.mGroup, &pair)) { + for (const PersistenceType type : kBestEffortPersistenceTypes) { + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type); + if (groupInfo) { + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aPrincipalMetadata.mOrigin); + if (originInfo) { + AssertNoOverflow(usage, originInfo->LockedUsage()); + usage += originInfo->LockedUsage(); + } + } + } + } + } + + return usage; +} + +Maybe<FullOriginMetadata> QuotaManager::GetFullOriginMetadata( + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + MOZ_DIAGNOSTIC_ASSERT(mStorageConnection); + MOZ_DIAGNOSTIC_ASSERT(mTemporaryStorageInitialized); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<OriginInfo> originInfo = + LockedGetOriginInfo(aOriginMetadata.mPersistenceType, aOriginMetadata); + if (originInfo) { + return Some(originInfo->LockedFlattenToFullOriginMetadata()); + } + + return Nothing(); +} + +void QuotaManager::NotifyStoragePressure(uint64_t aUsage) { + mQuotaMutex.AssertNotCurrentThreadOwns(); + + RefPtr<StoragePressureRunnable> storagePressureRunnable = + new StoragePressureRunnable(aUsage); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(storagePressureRunnable)); +} + +// static +void QuotaManager::GetStorageId(PersistenceType aPersistenceType, + const nsACString& aOrigin, + Client::Type aClientType, + nsACString& aDatabaseId) { + nsAutoCString str; + str.AppendInt(aPersistenceType); + str.Append('*'); + str.Append(aOrigin); + str.Append('*'); + str.AppendInt(aClientType); + + aDatabaseId = str; +} + +// static +bool QuotaManager::IsPrincipalInfoValid(const PrincipalInfo& aPrincipalInfo) { + switch (aPrincipalInfo.type()) { + // A system principal is acceptable. + case PrincipalInfo::TSystemPrincipalInfo: { + return true; + } + + // Validate content principals to ensure that the spec, originNoSuffix and + // baseDomain are sane. + case PrincipalInfo::TContentPrincipalInfo: { + const ContentPrincipalInfo& info = + aPrincipalInfo.get_ContentPrincipalInfo(); + + // Verify the principal spec parses. + RefPtr<MozURL> specURL; + nsresult rv = MozURL::Init(getter_AddRefs(specURL), info.spec()); + if (NS_WARN_IF(NS_FAILED(rv))) { + QM_WARNING("A URL %s is not recognized by MozURL", info.spec().get()); + return false; + } + + // Verify the principal originNoSuffix matches spec. + nsCString originNoSuffix; + specURL->Origin(originNoSuffix); + + if (NS_WARN_IF(originNoSuffix != info.originNoSuffix())) { + QM_WARNING("originNoSuffix (%s) doesn't match passed one (%s)!", + originNoSuffix.get(), info.originNoSuffix().get()); + return false; + } + + if (NS_WARN_IF(info.originNoSuffix().EqualsLiteral(kChromeOrigin))) { + return false; + } + + if (NS_WARN_IF(info.originNoSuffix().FindChar('^', 0) != -1)) { + QM_WARNING("originNoSuffix (%s) contains the '^' character!", + info.originNoSuffix().get()); + return false; + } + + // Verify the principal baseDomain exists. + if (NS_WARN_IF(info.baseDomain().IsVoid())) { + return false; + } + + // Verify the principal baseDomain matches spec. + nsCString baseDomain; + rv = specURL->BaseDomain(baseDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + if (NS_WARN_IF(baseDomain != info.baseDomain())) { + QM_WARNING("baseDomain (%s) doesn't match passed one (%s)!", + baseDomain.get(), info.baseDomain().get()); + return false; + } + + return true; + } + + default: { + break; + } + } + + // Null and expanded principals are not acceptable. + return false; +} + +// static +PrincipalMetadata QuotaManager::GetInfoFromValidatedPrincipalInfo( + const PrincipalInfo& aPrincipalInfo) { + MOZ_ASSERT(IsPrincipalInfoValid(aPrincipalInfo)); + + switch (aPrincipalInfo.type()) { + case PrincipalInfo::TSystemPrincipalInfo: { + return GetInfoForChrome(); + } + + case PrincipalInfo::TContentPrincipalInfo: { + const ContentPrincipalInfo& info = + aPrincipalInfo.get_ContentPrincipalInfo(); + + PrincipalMetadata principalMetadata; + + info.attrs().CreateSuffix(principalMetadata.mSuffix); + + principalMetadata.mGroup = info.baseDomain() + principalMetadata.mSuffix; + + principalMetadata.mOrigin = + info.originNoSuffix() + principalMetadata.mSuffix; + + return principalMetadata; + } + + default: { + MOZ_CRASH("Should never get here!"); + } + } +} + +// static +nsAutoCString QuotaManager::GetOriginFromValidatedPrincipalInfo( + const PrincipalInfo& aPrincipalInfo) { + MOZ_ASSERT(IsPrincipalInfoValid(aPrincipalInfo)); + + switch (aPrincipalInfo.type()) { + case PrincipalInfo::TSystemPrincipalInfo: { + return nsAutoCString{GetOriginForChrome()}; + } + + case PrincipalInfo::TContentPrincipalInfo: { + const ContentPrincipalInfo& info = + aPrincipalInfo.get_ContentPrincipalInfo(); + + nsAutoCString suffix; + + info.attrs().CreateSuffix(suffix); + + return info.originNoSuffix() + suffix; + } + + default: { + MOZ_CRASH("Should never get here!"); + } + } +} + +// static +Result<PrincipalMetadata, nsresult> QuotaManager::GetInfoFromPrincipal( + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + if (aPrincipal->IsSystemPrincipal()) { + return GetInfoForChrome(); + } + + if (aPrincipal->GetIsNullPrincipal()) { + NS_WARNING("IndexedDB not supported from this principal!"); + return Err(NS_ERROR_FAILURE); + } + + PrincipalMetadata principalMetadata; + + QM_TRY(MOZ_TO_RESULT(aPrincipal->GetOrigin(principalMetadata.mOrigin))); + + if (principalMetadata.mOrigin.EqualsLiteral(kChromeOrigin)) { + NS_WARNING("Non-chrome principal can't use chrome origin!"); + return Err(NS_ERROR_FAILURE); + } + + aPrincipal->OriginAttributesRef().CreateSuffix(principalMetadata.mSuffix); + + nsAutoCString baseDomain; + QM_TRY(MOZ_TO_RESULT(aPrincipal->GetBaseDomain(baseDomain))); + + MOZ_ASSERT(!baseDomain.IsEmpty()); + + principalMetadata.mGroup = baseDomain + principalMetadata.mSuffix; + + return principalMetadata; +} + +// static +Result<nsAutoCString, nsresult> QuotaManager::GetOriginFromPrincipal( + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + if (aPrincipal->IsSystemPrincipal()) { + return nsAutoCString{GetOriginForChrome()}; + } + + if (aPrincipal->GetIsNullPrincipal()) { + NS_WARNING("IndexedDB not supported from this principal!"); + return Err(NS_ERROR_FAILURE); + } + + QM_TRY_UNWRAP(const auto origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoCString, aPrincipal, GetOrigin)); + + if (origin.EqualsLiteral(kChromeOrigin)) { + NS_WARNING("Non-chrome principal can't use chrome origin!"); + return Err(NS_ERROR_FAILURE); + } + + return origin; +} + +// static +Result<nsAutoCString, nsresult> QuotaManager::GetOriginFromWindow( + nsPIDOMWindowOuter* aWindow) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow); + QM_TRY(OkIf(sop), Err(NS_ERROR_FAILURE)); + + nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal(); + QM_TRY(OkIf(principal), Err(NS_ERROR_FAILURE)); + + QM_TRY_RETURN(GetOriginFromPrincipal(principal)); +} + +// static +PrincipalMetadata QuotaManager::GetInfoForChrome() { + return {{}, GetOriginForChrome(), GetOriginForChrome()}; +} + +// static +nsLiteralCString QuotaManager::GetOriginForChrome() { + return nsLiteralCString{kChromeOrigin}; +} + +// static +bool QuotaManager::IsOriginInternal(const nsACString& aOrigin) { + MOZ_ASSERT(!aOrigin.IsEmpty()); + + // The first prompt is not required for these origins. + if (aOrigin.EqualsLiteral(kChromeOrigin) || + StringBeginsWith(aOrigin, nsDependentCString(kAboutHomeOriginPrefix)) || + StringBeginsWith(aOrigin, nsDependentCString(kIndexedDBOriginPrefix)) || + StringBeginsWith(aOrigin, nsDependentCString(kResourceOriginPrefix))) { + return true; + } + + return false; +} + +// static +bool QuotaManager::AreOriginsEqualOnDisk(const nsACString& aOrigin1, + const nsACString& aOrigin2) { + return MakeSanitizedOriginCString(aOrigin1) == + MakeSanitizedOriginCString(aOrigin2); +} + +// static +Result<PrincipalInfo, nsresult> QuotaManager::ParseOrigin( + const nsACString& aOrigin) { + // An origin string either corresponds to a SystemPrincipalInfo or a + // ContentPrincipalInfo, see + // QuotaManager::GetOriginFromValidatedPrincipalInfo. + + if (aOrigin.Equals(kChromeOrigin)) { + return PrincipalInfo{SystemPrincipalInfo{}}; + } + + ContentPrincipalInfo contentPrincipalInfo; + + nsCString originalSuffix; + const OriginParser::ResultType result = OriginParser::ParseOrigin( + MakeSanitizedOriginCString(aOrigin), contentPrincipalInfo.spec(), + &contentPrincipalInfo.attrs(), originalSuffix); + QM_TRY(OkIf(result == OriginParser::ValidOrigin), Err(NS_ERROR_FAILURE)); + + return PrincipalInfo{std::move(contentPrincipalInfo)}; +} + +// static +void QuotaManager::InvalidateQuotaCache() { gInvalidateQuotaCache = true; } + +uint64_t QuotaManager::LockedCollectOriginsForEviction( + uint64_t aMinSizeToBeFreed, nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) { + mQuotaMutex.AssertCurrentThreadOwns(); + + RefPtr<CollectOriginsHelper> helper = + new CollectOriginsHelper(mQuotaMutex, aMinSizeToBeFreed); + + // Unlock while calling out to XPCOM (code behind the dispatch method needs + // to acquire its own lock which can potentially lead to a deadlock and it + // also calls an observer that can do various stuff like IO, so it's better + // to not hold our mutex while that happens). + { + MutexAutoUnlock autoUnlock(mQuotaMutex); + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(helper, NS_DISPATCH_NORMAL)); + } + + return helper->BlockAndReturnOriginsForEviction(aLocks); +} + +void QuotaManager::LockedRemoveQuotaForOrigin( + const OriginMetadata& aOriginMetadata) { + mQuotaMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(aOriginMetadata.mPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) { + return; + } + + MOZ_ASSERT(pair); + + if (RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(aOriginMetadata.mPersistenceType)) { + groupInfo->LockedRemoveOriginInfo(aOriginMetadata.mOrigin); + + if (!groupInfo->LockedHasOriginInfos()) { + pair->LockedClearGroupInfo(aOriginMetadata.mPersistenceType); + + if (!pair->LockedHasGroupInfos()) { + mGroupInfoPairs.Remove(aOriginMetadata.mGroup); + } + } + } +} + +already_AddRefed<GroupInfo> QuotaManager::LockedGetOrCreateGroupInfo( + PersistenceType aPersistenceType, const nsACString& aSuffix, + const nsACString& aGroup) { + mQuotaMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + GroupInfoPair* const pair = + mGroupInfoPairs.GetOrInsertNew(aGroup, aSuffix, aGroup); + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + groupInfo = new GroupInfo(pair, aPersistenceType); + pair->LockedSetGroupInfo(aPersistenceType, groupInfo); + } + + return groupInfo.forget(); +} + +already_AddRefed<OriginInfo> QuotaManager::LockedGetOriginInfo( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) { + mQuotaMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + GroupInfoPair* pair; + if (mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) { + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (groupInfo) { + return groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + } + } + + return nullptr; +} + +template <typename Iterator> +void QuotaManager::MaybeInsertNonPersistedOriginInfos( + Iterator aDest, const RefPtr<GroupInfo>& aTemporaryGroupInfo, + const RefPtr<GroupInfo>& aDefaultGroupInfo) { + const auto copy = [&aDest](const GroupInfo& groupInfo) { + std::copy_if( + groupInfo.mOriginInfos.cbegin(), groupInfo.mOriginInfos.cend(), aDest, + [](const auto& originInfo) { return !originInfo->LockedPersisted(); }); + }; + + if (aTemporaryGroupInfo) { + MOZ_ASSERT(PERSISTENCE_TYPE_TEMPORARY == + aTemporaryGroupInfo->GetPersistenceType()); + + copy(*aTemporaryGroupInfo); + } + if (aDefaultGroupInfo) { + MOZ_ASSERT(PERSISTENCE_TYPE_DEFAULT == + aDefaultGroupInfo->GetPersistenceType()); + + copy(*aDefaultGroupInfo); + } +} + +template <typename Collect, typename Pred> +QuotaManager::OriginInfosFlatTraversable +QuotaManager::CollectLRUOriginInfosUntil(Collect&& aCollect, Pred&& aPred) { + OriginInfosFlatTraversable originInfos; + + std::forward<Collect>(aCollect)(MakeBackInserter(originInfos)); + + originInfos.Sort(OriginInfoAccessTimeComparator()); + + const auto foundIt = std::find_if(originInfos.cbegin(), originInfos.cend(), + std::forward<Pred>(aPred)); + + originInfos.TruncateLength(foundIt - originInfos.cbegin()); + + return originInfos; +} + +QuotaManager::OriginInfosNestedTraversable +QuotaManager::GetOriginInfosExceedingGroupLimit() const { + MutexAutoLock lock(mQuotaMutex); + + OriginInfosNestedTraversable originInfos; + + for (const auto& entry : mGroupInfoPairs) { + const auto& pair = entry.GetData(); + + MOZ_ASSERT(!entry.GetKey().IsEmpty()); + MOZ_ASSERT(pair); + + uint64_t groupUsage = 0; + + const RefPtr<GroupInfo> temporaryGroupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY); + if (temporaryGroupInfo) { + groupUsage += temporaryGroupInfo->mUsage; + } + + const RefPtr<GroupInfo> defaultGroupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT); + if (defaultGroupInfo) { + groupUsage += defaultGroupInfo->mUsage; + } + + if (groupUsage > 0) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager, "Shouldn't be null!"); + + if (groupUsage > quotaManager->GetGroupLimit()) { + originInfos.AppendElement(CollectLRUOriginInfosUntil( + [&temporaryGroupInfo, &defaultGroupInfo](auto inserter) { + MaybeInsertNonPersistedOriginInfos( + std::move(inserter), temporaryGroupInfo, defaultGroupInfo); + }, + [&groupUsage, quotaManager](const auto& originInfo) { + groupUsage -= originInfo->LockedUsage(); + + return groupUsage <= quotaManager->GetGroupLimit(); + })); + } + } + } + + return originInfos; +} + +QuotaManager::OriginInfosNestedTraversable +QuotaManager::GetOriginInfosExceedingGlobalLimit() const { + MutexAutoLock lock(mQuotaMutex); + + QuotaManager::OriginInfosNestedTraversable res; + res.AppendElement(CollectLRUOriginInfosUntil( + // XXX The lambda only needs to capture this, but due to Bug 1421435 it + // can't. + [&](auto inserter) { + for (const auto& entry : mGroupInfoPairs) { + const auto& pair = entry.GetData(); + + MOZ_ASSERT(!entry.GetKey().IsEmpty()); + MOZ_ASSERT(pair); + + MaybeInsertNonPersistedOriginInfos( + inserter, pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY), + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT)); + } + }, + [temporaryStorageUsage = mTemporaryStorageUsage, + temporaryStorageLimit = mTemporaryStorageLimit, + doomedUsage = uint64_t{0}](const auto& originInfo) mutable { + if (temporaryStorageUsage - doomedUsage <= temporaryStorageLimit) { + return true; + } + + doomedUsage += originInfo->LockedUsage(); + return false; + })); + + return res; +} + +void QuotaManager::ClearOrigins( + const OriginInfosNestedTraversable& aDoomedOriginInfos) { + AssertIsOnIOThread(); + + // If we are in shutdown, we could break off early from clearing origins. + // In such cases, we would like to track the ones that were already cleared + // up, such that other essential cleanup could be performed on clearedOrigins. + // clearedOrigins is used in calls to LockedRemoveQuotaForOrigin and + // OriginClearCompleted below. We could have used a collection of OriginInfos + // rather than flattening them to OriginMetadata but groupInfo in OriginInfo + // is just a raw ptr and LockedRemoveQuotaForOrigin might delete groupInfo and + // as a result, we would not be able to get origin persistence type required + // in OriginClearCompleted call after lockedRemoveQuotaForOrigin call. + nsTArray<OriginMetadata> clearedOrigins; + + // XXX Does this need to be done a) in order and/or b) sequentially? + for (const auto& doomedOriginInfo : + Flatten<OriginInfosFlatTraversable::value_type>(aDoomedOriginInfos)) { +#ifdef DEBUG + { + MutexAutoLock lock(mQuotaMutex); + MOZ_ASSERT(!doomedOriginInfo->LockedPersisted()); + } +#endif + + // TODO: We are currently only checking for this flag here which + // means that we cannot break off once we start cleaning an origin. It + // could be better if we could check for shutdown flag while cleaning an + // origin such that we could break off early from the cleaning process if + // we are stuck cleaning on one huge origin. Bug1797098 has been filed to + // track this. + if (QuotaManager::IsShuttingDown()) { + break; + } + + DeleteFilesForOrigin(doomedOriginInfo->mGroupInfo->mPersistenceType, + doomedOriginInfo->mOrigin); + + clearedOrigins.AppendElement(doomedOriginInfo->FlattenToOriginMetadata()); + } + + { + MutexAutoLock lock(mQuotaMutex); + + for (const auto& clearedOrigin : clearedOrigins) { + LockedRemoveQuotaForOrigin(clearedOrigin); + } + } + + for (const auto& clearedOrigin : clearedOrigins) { + OriginClearCompleted(clearedOrigin.mPersistenceType, clearedOrigin.mOrigin, + Nullable<Client::Type>()); + } +} + +void QuotaManager::CleanupTemporaryStorage() { + AssertIsOnIOThread(); + + // Evicting origins that exceed their group limit also affects the global + // temporary storage usage, so these steps have to be taken sequentially. + // Combining them doesn't seem worth the added complexity. + ClearOrigins(GetOriginInfosExceedingGroupLimit()); + ClearOrigins(GetOriginInfosExceedingGlobalLimit()); + + if (mTemporaryStorageUsage > mTemporaryStorageLimit) { + // If disk space is still low after origin clear, notify storage pressure. + NotifyStoragePressure(mTemporaryStorageUsage); + } +} + +void QuotaManager::DeleteFilesForOrigin(PersistenceType aPersistenceType, + const nsACString& aOrigin) { + QM_TRY_INSPECT(const auto& directory, + GetDirectoryForOrigin(aPersistenceType, aOrigin), QM_VOID); + + nsresult rv = directory->Remove(true); + if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + // This should never fail if we've closed all storage connections + // correctly... + NS_ERROR("Failed to remove directory!"); + } +} + +void QuotaManager::FinalizeOriginEviction( + nsTArray<RefPtr<OriginDirectoryLock>>&& aLocks) { + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + + RefPtr<FinalizeOriginEvictionOp> op = + new FinalizeOriginEvictionOp(mOwningThread, std::move(aLocks)); + + if (IsOnIOThread()) { + op->RunOnIOThreadImmediately(); + } else { + op->Dispatch(); + } +} + +Result<Ok, nsresult> QuotaManager::ArchiveOrigins( + const nsTArray<FullOriginMetadata>& aFullOriginMetadatas) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aFullOriginMetadatas.IsEmpty()); + + QM_TRY_INSPECT(const auto& storageArchivesDir, + QM_NewLocalFile(*mStorageArchivesPath)); + + // Create another subdir, so once we decide to remove all temporary archives, + // we can remove only the subdir and the parent directory can still be used + // for something else or similar in future. Otherwise, we would have to + // figure out a new name for it. + QM_TRY(MOZ_TO_RESULT(storageArchivesDir->Append(u"0"_ns))); + + PRExplodedTime now; + PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &now); + + const auto dateStr = + nsPrintfCString("%04hd-%02" PRId32 "-%02" PRId32, now.tm_year, + now.tm_month + 1, now.tm_mday); + + QM_TRY_INSPECT( + const auto& storageArchiveDir, + CloneFileAndAppend(*storageArchivesDir, NS_ConvertASCIItoUTF16(dateStr))); + + QM_TRY(MOZ_TO_RESULT( + storageArchiveDir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0700))); + + QM_TRY_INSPECT(const auto& defaultStorageArchiveDir, + CloneFileAndAppend(*storageArchiveDir, + nsLiteralString(DEFAULT_DIRECTORY_NAME))); + + QM_TRY_INSPECT(const auto& temporaryStorageArchiveDir, + CloneFileAndAppend(*storageArchiveDir, + nsLiteralString(TEMPORARY_DIRECTORY_NAME))); + + for (const auto& fullOriginMetadata : aFullOriginMetadatas) { + MOZ_ASSERT( + IsBestEffortPersistenceType(fullOriginMetadata.mPersistenceType)); + + QM_TRY_INSPECT(const auto& directory, + GetDirectoryForOrigin(fullOriginMetadata.mPersistenceType, + fullOriginMetadata.mOrigin)); + + // The origin could have been removed, for example due to corruption. + QM_TRY_INSPECT( + const auto& moved, + QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT( + directory->MoveTo(fullOriginMetadata.mPersistenceType == + PERSISTENCE_TYPE_DEFAULT + ? defaultStorageArchiveDir + : temporaryStorageArchiveDir, + u""_ns)) + .map([](Ok) { return true; }), + // Predicate. + ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }), + // Fallback. + ErrToOk<false>)); + + if (moved) { + RemoveQuotaForOrigin(fullOriginMetadata.mPersistenceType, + fullOriginMetadata); + } + } + + return Ok{}; +} + +auto QuotaManager::GetDirectoryLockTable(PersistenceType aPersistenceType) + -> DirectoryLockTable& { + switch (aPersistenceType) { + case PERSISTENCE_TYPE_TEMPORARY: + return mTemporaryDirectoryLockTable; + case PERSISTENCE_TYPE_DEFAULT: + return mDefaultDirectoryLockTable; + + case PERSISTENCE_TYPE_PERSISTENT: + case PERSISTENCE_TYPE_INVALID: + default: + MOZ_CRASH("Bad persistence type value!"); + } +} + +bool QuotaManager::IsSanitizedOriginValid(const nsACString& aSanitizedOrigin) { + AssertIsOnIOThread(); + + // Do not parse this sanitized origin string, if we already parsed it. + return mValidOrigins.LookupOrInsertWith( + aSanitizedOrigin, [&aSanitizedOrigin] { + nsCString spec; + OriginAttributes attrs; + nsCString originalSuffix; + const auto result = OriginParser::ParseOrigin(aSanitizedOrigin, spec, + &attrs, originalSuffix); + + return result == OriginParser::ValidOrigin; + }); +} + +int64_t QuotaManager::GenerateDirectoryLockId() { + const int64_t directorylockId = mNextDirectoryLockId; + + if (CheckedInt64 result = CheckedInt64(mNextDirectoryLockId) + 1; + result.isValid()) { + mNextDirectoryLockId = result.value(); + } else { + NS_WARNING("Quota manager has run out of ids for directory locks!"); + + // There's very little chance for this to happen given the max size of + // 64 bit integer but if it happens we can just reset mNextDirectoryLockId + // to zero since such old directory locks shouldn't exist anymore. + mNextDirectoryLockId = 0; + } + + // TODO: Maybe add an assertion here to check that there is no existing + // directory lock with given id. + + return directorylockId; +} + +template <typename Func> +auto QuotaManager::ExecuteInitialization(const Initialization aInitialization, + Func&& aFunc) + -> std::invoke_result_t<Func, const FirstInitializationAttempt< + Initialization, StringGenerator>&> { + return quota::ExecuteInitialization(mInitializationInfo, aInitialization, + std::forward<Func>(aFunc)); +} + +template <typename Func> +auto QuotaManager::ExecuteInitialization(const Initialization aInitialization, + const nsACString& aContext, + Func&& aFunc) + -> std::invoke_result_t<Func, const FirstInitializationAttempt< + Initialization, StringGenerator>&> { + return quota::ExecuteInitialization(mInitializationInfo, aInitialization, + aContext, std::forward<Func>(aFunc)); +} + +template <typename Func> +auto QuotaManager::ExecuteOriginInitialization( + const nsACString& aOrigin, const OriginInitialization aInitialization, + const nsACString& aContext, Func&& aFunc) + -> std::invoke_result_t<Func, const FirstInitializationAttempt< + Initialization, StringGenerator>&> { + return quota::ExecuteInitialization( + mInitializationInfo.MutableOriginInitializationInfoRef( + aOrigin, CreateIfNonExistent{}), + aInitialization, aContext, std::forward<Func>(aFunc)); +} + +/******************************************************************************* + * Local class implementations + ******************************************************************************/ + +CollectOriginsHelper::CollectOriginsHelper(mozilla::Mutex& aMutex, + uint64_t aMinSizeToBeFreed) + : Runnable("dom::quota::CollectOriginsHelper"), + mMinSizeToBeFreed(aMinSizeToBeFreed), + mMutex(aMutex), + mCondVar(aMutex, "CollectOriginsHelper::mCondVar"), + mSizeToBeFreed(0), + mWaiting(true) { + MOZ_ASSERT(!NS_IsMainThread(), "Wrong thread!"); + mMutex.AssertCurrentThreadOwns(); +} + +int64_t CollectOriginsHelper::BlockAndReturnOriginsForEviction( + nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) { + MOZ_ASSERT(!NS_IsMainThread(), "Wrong thread!"); + mMutex.AssertCurrentThreadOwns(); + + while (mWaiting) { + mCondVar.Wait(); + } + + mLocks.SwapElements(aLocks); + return mSizeToBeFreed; +} + +NS_IMETHODIMP +CollectOriginsHelper::Run() { + AssertIsOnBackgroundThread(); + + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Shouldn't be null!"); + + // We use extra stack vars here to avoid race detector warnings (the same + // memory accessed with and without the lock held). + nsTArray<RefPtr<OriginDirectoryLock>> locks; + uint64_t sizeToBeFreed = + quotaManager->CollectOriginsForEviction(mMinSizeToBeFreed, locks); + + MutexAutoLock lock(mMutex); + + NS_ASSERTION(mWaiting, "Huh?!"); + + mLocks.SwapElements(locks); + mSizeToBeFreed = sizeToBeFreed; + mWaiting = false; + mCondVar.Notify(); + + return NS_OK; +} + +/******************************************************************************* + * OriginOperationBase + ******************************************************************************/ + +NS_IMETHODIMP +OriginOperationBase::Run() { + nsresult rv; + + switch (mState) { + case State_Initial: { + rv = Init(); + break; + } + + case State_DirectoryOpenPending: { + rv = DirectoryOpen(); + break; + } + + case State_DirectoryWorkOpen: { + rv = DirectoryWork(); + break; + } + + case State_UnblockingOpen: { + UnblockOpen(); + return NS_OK; + } + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State_UnblockingOpen) { + Finish(rv); + } + + return NS_OK; +} + +nsresult OriginOperationBase::DirectoryOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State_DirectoryOpenPending); + + QuotaManager* const quotaManager = QuotaManager::Get(); + QM_TRY(OkIf(quotaManager), NS_ERROR_FAILURE); + + // Must set this before dispatching otherwise we will race with the IO thread. + AdvanceState(); + + QM_TRY(MOZ_TO_RESULT( + quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)), + NS_ERROR_FAILURE); + + return NS_OK; +} + +void OriginOperationBase::Finish(nsresult aResult) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = aResult; + } + + // Must set mState before dispatching otherwise we will race with the main + // thread. + mState = State_UnblockingOpen; + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +nsresult OriginOperationBase::Init() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State_Initial); + + if (QuotaManager::IsShuttingDown()) { + return NS_ERROR_ABORT; + } + + if (mNeedsQuotaManagerInit) { + QM_TRY(QuotaManager::EnsureCreated()); + } + + Open(); + + return NS_OK; +} + +nsresult OriginOperationBase::DirectoryWork() { + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State_DirectoryWorkOpen); + + QuotaManager* const quotaManager = QuotaManager::Get(); + QM_TRY(OkIf(quotaManager), NS_ERROR_FAILURE); + + if (mNeedsStorageInit) { + QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureStorageIsInitialized())); + } + + QM_TRY(MOZ_TO_RESULT(DoDirectoryWork(*quotaManager))); + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + AdvanceState(); + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +void FinalizeOriginEvictionOp::Dispatch() { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(GetState() == State_Initial); + + SetState(State_DirectoryOpenPending); + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +void FinalizeOriginEvictionOp::RunOnIOThreadImmediately() { + AssertIsOnIOThread(); + MOZ_ASSERT(GetState() == State_Initial); + + SetState(State_DirectoryWorkOpen); + + MOZ_ALWAYS_SUCCEEDS(this->Run()); +} + +void FinalizeOriginEvictionOp::Open() { MOZ_CRASH("Shouldn't get here!"); } + +nsresult FinalizeOriginEvictionOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("FinalizeOriginEvictionOp::DoDirectoryWork", OTHER); + + for (const auto& lock : mLocks) { + aQuotaManager.OriginClearCompleted( + lock->GetPersistenceType(), lock->Origin(), Nullable<Client::Type>()); + } + + return NS_OK; +} + +void FinalizeOriginEvictionOp::UnblockOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_UnblockingOpen); + +#ifdef DEBUG + NoteActorDestroyed(); +#endif + + mLocks.Clear(); + + AdvanceState(); +} + +NS_IMPL_ISUPPORTS_INHERITED0(NormalOriginOperationBase, Runnable) + +RefPtr<DirectoryLock> NormalOriginOperationBase::CreateDirectoryLock() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_DirectoryOpenPending); + MOZ_ASSERT(QuotaManager::Get()); + + return QuotaManager::Get()->CreateDirectoryLockInternal( + mPersistenceType, mOriginScope, mClientType, mExclusive); +} + +void NormalOriginOperationBase::Open() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_Initial); + MOZ_ASSERT(QuotaManager::Get()); + + AdvanceState(); + + RefPtr<DirectoryLock> directoryLock = CreateDirectoryLock(); + if (directoryLock) { + directoryLock->Acquire(this); + } else { + QM_TRY(MOZ_TO_RESULT(DirectoryOpen()), QM_VOID, + [this](const nsresult rv) { Finish(rv); }); + } +} + +void NormalOriginOperationBase::UnblockOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_UnblockingOpen); + + SendResults(); + + if (mDirectoryLock) { + mDirectoryLock = nullptr; + } + + UnregisterNormalOriginOp(*this); + + AdvanceState(); +} + +void NormalOriginOperationBase::DirectoryLockAcquired(DirectoryLock* aLock) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aLock); + MOZ_ASSERT(GetState() == State_DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + mDirectoryLock = aLock; + + QM_TRY(MOZ_TO_RESULT(DirectoryOpen()), QM_VOID, + [this](const nsresult rv) { Finish(rv); }); +} + +void NormalOriginOperationBase::DirectoryLockFailed() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + Finish(NS_ERROR_FAILURE); +} + +nsresult SaveOriginAccessTimeOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + MOZ_ASSERT(mOriginScope.IsOrigin()); + + AUTO_PROFILER_LABEL("SaveOriginAccessTimeOp::DoDirectoryWork", OTHER); + + QM_TRY_INSPECT(const auto& file, + aQuotaManager.GetDirectoryForOrigin(mPersistenceType.Value(), + mOriginScope.GetOrigin())); + + // The origin directory might not exist + // anymore, because it was deleted by a clear operation. + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(file, Exists)); + + if (exists) { + QM_TRY(MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_V2_FILE_NAME)))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, FileFlag::Update)); + MOZ_ASSERT(stream); + + QM_TRY(MOZ_TO_RESULT(stream->Write64(mTimestamp))); + } + + return NS_OK; +} + +void SaveOriginAccessTimeOp::SendResults() { +#ifdef DEBUG + NoteActorDestroyed(); +#endif +} + +#ifdef DEBUG +nsresult ShutdownStorageOp::DirectoryOpen() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDirectoryLock); + mDirectoryLock->AssertIsAcquiredExclusively(); + + return NormalOriginOperationBase::DirectoryOpen(); +} +#endif + +nsresult ShutdownStorageOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("ShutdownStorageOp::DoDirectoryWork", OTHER); + + aQuotaManager.ShutdownStorageInternal(); + + return NS_OK; +} + +void ShutdownStorageOp::SendResults() { +#ifdef DEBUG + NoteActorDestroyed(); +#endif + + if (NS_SUCCEEDED(mResultCode)) { + mPromiseHolder.ResolveIfExists(true, __func__); + } else { + mPromiseHolder.RejectIfExists(mResultCode, __func__); + } +} + +NS_IMETHODIMP +StoragePressureRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obsSvc)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsISupportsPRUint64> wrapper = + do_CreateInstance(NS_SUPPORTS_PRUINT64_CONTRACTID); + if (NS_WARN_IF(!wrapper)) { + return NS_ERROR_FAILURE; + } + + wrapper->SetData(mUsage); + + obsSvc->NotifyObservers(wrapper, "QuotaManager::StoragePressure", u""); + + return NS_OK; +} + +TimeStamp RecordQuotaInfoLoadTimeHelper::Start() { + AssertIsOnIOThread(); + + // XXX: If a OS sleep/wake occur after mStartTime is initialized but before + // gLastOSWake is set, then this time duration would still be recorded with + // key "Normal". We are assumming this is rather rare to happen. + mStartTime.init(TimeStamp::Now()); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + + return *mStartTime; +} + +TimeStamp RecordQuotaInfoLoadTimeHelper::End() { + AssertIsOnIOThread(); + + mEndTime.init(TimeStamp::Now()); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + + return *mEndTime; +} + +NS_IMETHODIMP +RecordQuotaInfoLoadTimeHelper::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mInitializedTime.isSome()) { + // Keys for QM_QUOTA_INFO_LOAD_TIME_V0: + // Normal: Normal conditions. + // WasSuspended: There was a OS sleep so that it was suspended. + // TimeStampErr1: The recorded start time is unexpectedly greater than the + // end time. + // TimeStampErr2: The initialized time for the recording class is unexpectly + // greater than the last OS wake time. + const auto key = [this, wasSuspended = gLastOSWake > *mInitializedTime]() { + if (wasSuspended) { + return "WasSuspended"_ns; + } + + // XXX File a bug if we have data for this key. + // We found negative values in our query in STMO for + // ScalarID::QM_REPOSITORIES_INITIALIZATION_TIME. This shouldn't happen + // because the documentation for TimeStamp::Now() says it returns a + // monotonically increasing number. + if (*mStartTime > *mEndTime) { + return "TimeStampErr1"_ns; + } + + if (*mInitializedTime > gLastOSWake) { + return "TimeStampErr2"_ns; + } + + return "Normal"_ns; + }(); + + Telemetry::AccumulateTimeDelta(Telemetry::QM_QUOTA_INFO_LOAD_TIME_V0, key, + *mStartTime, *mEndTime); + + return NS_OK; + } + + gLastOSWake = TimeStamp::Now(); + mInitializedTime.init(gLastOSWake); + + return NS_OK; +} + +/******************************************************************************* + * Quota + ******************************************************************************/ + +Quota::Quota() +#ifdef DEBUG + : mActorDestroyed(false) +#endif +{ +} + +Quota::~Quota() { MOZ_ASSERT(mActorDestroyed); } + +bool Quota::VerifyRequestParams(const UsageRequestParams& aParams) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None); + + switch (aParams.type()) { + case UsageRequestParams::TAllUsageParams: + break; + + case UsageRequestParams::TOriginUsageParams: { + const OriginUsageParams& params = aParams.get_OriginUsageParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +bool Quota::VerifyRequestParams(const RequestParams& aParams) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + switch (aParams.type()) { + case RequestParams::TStorageNameParams: + case RequestParams::TStorageInitializedParams: + case RequestParams::TTemporaryStorageInitializedParams: + case RequestParams::TInitParams: + case RequestParams::TInitTemporaryStorageParams: + break; + + case RequestParams::TInitializePersistentOriginParams: { + const InitializePersistentOriginParams& params = + aParams.get_InitializePersistentOriginParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TInitializeTemporaryOriginParams: { + const InitializeTemporaryOriginParams& params = + aParams.get_InitializeTemporaryOriginParams(); + + if (NS_WARN_IF(!IsBestEffortPersistenceType(params.persistenceType()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TGetFullOriginMetadataParams: { + const GetFullOriginMetadataParams& params = + aParams.get_GetFullOriginMetadataParams(); + if (NS_WARN_IF(!IsBestEffortPersistenceType(params.persistenceType()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TClearOriginParams: { + const ClearResetOriginParams& params = + aParams.get_ClearOriginParams().commonParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + if (params.persistenceTypeIsExplicit()) { + if (NS_WARN_IF(!IsValidPersistenceType(params.persistenceType()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + } + + if (params.clientTypeIsExplicit()) { + if (NS_WARN_IF(!Client::IsValidType(params.clientType()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + } + + break; + } + + case RequestParams::TResetOriginParams: { + const ClearResetOriginParams& params = + aParams.get_ResetOriginParams().commonParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + if (params.persistenceTypeIsExplicit()) { + if (NS_WARN_IF(!IsValidPersistenceType(params.persistenceType()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + } + + if (params.clientTypeIsExplicit()) { + if (NS_WARN_IF(!Client::IsValidType(params.clientType()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + } + + break; + } + + case RequestParams::TClearDataParams: { + if (BackgroundParent::IsOtherProcessActor(Manager())) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TClearAllParams: + case RequestParams::TResetAllParams: + case RequestParams::TListOriginsParams: + break; + + case RequestParams::TPersistedParams: { + const PersistedParams& params = aParams.get_PersistedParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TPersistParams: { + const PersistParams& params = aParams.get_PersistParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TEstimateParams: { + const EstimateParams& params = aParams.get_EstimateParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +void Quota::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnBackgroundThread(); +#ifdef DEBUG + MOZ_ASSERT(!mActorDestroyed); + mActorDestroyed = true; +#endif +} + +PQuotaUsageRequestParent* Quota::AllocPQuotaUsageRequestParent( + const UsageRequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None); + + if (NS_WARN_IF(QuotaManager::IsShuttingDown())) { + return nullptr; + } + +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + bool trustParams = false; +#else + bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager()); +#endif + + if (!trustParams && NS_WARN_IF(!VerifyRequestParams(aParams))) { + MOZ_CRASH_UNLESS_FUZZING(); + return nullptr; + } + + auto actor = [&]() -> RefPtr<QuotaUsageRequestBase> { + switch (aParams.type()) { + case UsageRequestParams::TAllUsageParams: + return MakeRefPtr<GetUsageOp>(aParams); + + case UsageRequestParams::TOriginUsageParams: + return MakeRefPtr<GetOriginUsageOp>(aParams); + + default: + MOZ_CRASH("Should never get here!"); + } + }(); + + MOZ_ASSERT(actor); + + RegisterNormalOriginOp(*actor); + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +mozilla::ipc::IPCResult Quota::RecvPQuotaUsageRequestConstructor( + PQuotaUsageRequestParent* aActor, const UsageRequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None); + MOZ_ASSERT(!QuotaManager::IsShuttingDown()); + + auto* op = static_cast<QuotaUsageRequestBase*>(aActor); + + op->Init(*this); + + op->RunImmediately(); + return IPC_OK(); +} + +bool Quota::DeallocPQuotaUsageRequestParent(PQuotaUsageRequestParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<QuotaUsageRequestBase> actor = + dont_AddRef(static_cast<QuotaUsageRequestBase*>(aActor)); + return true; +} + +PQuotaRequestParent* Quota::AllocPQuotaRequestParent( + const RequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + if (NS_WARN_IF(QuotaManager::IsShuttingDown())) { + return nullptr; + } + +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + bool trustParams = false; +#else + bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager()); +#endif + + if (!trustParams && NS_WARN_IF(!VerifyRequestParams(aParams))) { + MOZ_CRASH_UNLESS_FUZZING(); + return nullptr; + } + + auto actor = [&]() -> RefPtr<QuotaRequestBase> { + switch (aParams.type()) { + case RequestParams::TStorageNameParams: + return MakeRefPtr<StorageNameOp>(); + + case RequestParams::TStorageInitializedParams: + return MakeRefPtr<StorageInitializedOp>(); + + case RequestParams::TTemporaryStorageInitializedParams: + return MakeRefPtr<TemporaryStorageInitializedOp>(); + + case RequestParams::TInitParams: + return MakeRefPtr<InitOp>(); + + case RequestParams::TInitTemporaryStorageParams: + return MakeRefPtr<InitTemporaryStorageOp>(); + + case RequestParams::TInitializePersistentOriginParams: + return MakeRefPtr<InitializePersistentOriginOp>(aParams); + + case RequestParams::TInitializeTemporaryOriginParams: + return MakeRefPtr<InitializeTemporaryOriginOp>(aParams); + + case RequestParams::TGetFullOriginMetadataParams: + return MakeRefPtr<GetFullOriginMetadataOp>( + aParams.get_GetFullOriginMetadataParams()); + + case RequestParams::TClearOriginParams: + return MakeRefPtr<ClearOriginOp>(aParams); + + case RequestParams::TResetOriginParams: + return MakeRefPtr<ResetOriginOp>(aParams); + + case RequestParams::TClearDataParams: + return MakeRefPtr<ClearDataOp>(aParams); + + case RequestParams::TClearAllParams: + return MakeRefPtr<ResetOrClearOp>(/* aClear */ true); + + case RequestParams::TResetAllParams: + return MakeRefPtr<ResetOrClearOp>(/* aClear */ false); + + case RequestParams::TPersistedParams: + return MakeRefPtr<PersistedOp>(aParams); + + case RequestParams::TPersistParams: + return MakeRefPtr<PersistOp>(aParams); + + case RequestParams::TEstimateParams: + return MakeRefPtr<EstimateOp>(aParams.get_EstimateParams()); + + case RequestParams::TListOriginsParams: + return MakeRefPtr<ListOriginsOp>(); + + default: + MOZ_CRASH("Should never get here!"); + } + }(); + + MOZ_ASSERT(actor); + + RegisterNormalOriginOp(*actor); + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +mozilla::ipc::IPCResult Quota::RecvPQuotaRequestConstructor( + PQuotaRequestParent* aActor, const RequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + MOZ_ASSERT(!QuotaManager::IsShuttingDown()); + + auto* op = static_cast<QuotaRequestBase*>(aActor); + + op->Init(*this); + + op->RunImmediately(); + return IPC_OK(); +} + +bool Quota::DeallocPQuotaRequestParent(PQuotaRequestParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<QuotaRequestBase> actor = + dont_AddRef(static_cast<QuotaRequestBase*>(aActor)); + return true; +} + +mozilla::ipc::IPCResult Quota::RecvStartIdleMaintenance() { + AssertIsOnBackgroundThread(); + + PBackgroundParent* actor = Manager(); + MOZ_ASSERT(actor); + + if (BackgroundParent::IsOtherProcessActor(actor)) { + MOZ_CRASH_UNLESS_FUZZING(); + return IPC_FAIL(this, "Wrong actor"); + } + + if (QuotaManager::IsShuttingDown()) { + return IPC_OK(); + } + + QM_TRY(QuotaManager::EnsureCreated(), IPC_OK()); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + quotaManager->StartIdleMaintenance(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Quota::RecvStopIdleMaintenance() { + AssertIsOnBackgroundThread(); + + PBackgroundParent* actor = Manager(); + MOZ_ASSERT(actor); + + if (BackgroundParent::IsOtherProcessActor(actor)) { + MOZ_CRASH_UNLESS_FUZZING(); + return IPC_FAIL(this, "Wrong actor"); + } + + if (QuotaManager::IsShuttingDown()) { + return IPC_OK(); + } + + QuotaManager* quotaManager = QuotaManager::Get(); + if (!quotaManager) { + return IPC_OK(); + } + + quotaManager->StopIdleMaintenance(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Quota::RecvAbortOperationsForProcess( + const ContentParentId& aContentParentId) { + AssertIsOnBackgroundThread(); + + PBackgroundParent* actor = Manager(); + MOZ_ASSERT(actor); + + if (BackgroundParent::IsOtherProcessActor(actor)) { + MOZ_CRASH_UNLESS_FUZZING(); + return IPC_FAIL(this, "Wrong actor"); + } + + if (QuotaManager::IsShuttingDown()) { + return IPC_OK(); + } + + QuotaManager* quotaManager = QuotaManager::Get(); + if (!quotaManager) { + return IPC_OK(); + } + + quotaManager->AbortOperationsForProcess(aContentParentId); + + return IPC_OK(); +} + +void QuotaUsageRequestBase::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = true; +} + +Result<UsageInfo, nsresult> QuotaUsageRequestBase::GetUsageForOrigin( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& directory, + aQuotaManager.GetDirectoryForOrigin(aPersistenceType, + aOriginMetadata.mOrigin)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); + + if (!exists || mCanceled) { + return UsageInfo(); + } + + // If the directory exists then enumerate all the files inside, adding up + // the sizes to get the final usage statistic. + bool initialized; + + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + initialized = aQuotaManager.IsOriginInitialized(aOriginMetadata.mOrigin); + } else { + initialized = aQuotaManager.IsTemporaryStorageInitialized(); + } + + return GetUsageForOriginEntries(aQuotaManager, aPersistenceType, + aOriginMetadata, *directory, initialized); +} + +Result<UsageInfo, nsresult> QuotaUsageRequestBase::GetUsageForOriginEntries( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, nsIFile& aDirectory, + const bool aInitialized) { + AssertIsOnIOThread(); + + QM_TRY_RETURN((ReduceEachFileAtomicCancelable( + aDirectory, mCanceled, UsageInfo{}, + [&](UsageInfo oldUsageInfo, const nsCOMPtr<nsIFile>& file) + -> mozilla::Result<UsageInfo, nsresult> { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, GetLeafName)); + + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: { + Client::Type clientType; + const bool ok = + Client::TypeFromText(leafName, clientType, fallible); + if (!ok) { + // Unknown directories during getting usage for an origin (even + // for an uninitialized origin) are now allowed. Just warn if we + // find them. + UNKNOWN_FILE_WARNING(leafName); + break; + } + + Client* const client = aQuotaManager.GetClient(clientType); + MOZ_ASSERT(client); + + QM_TRY_INSPECT( + const auto& usageInfo, + aInitialized ? client->GetUsageForOrigin( + aPersistenceType, aOriginMetadata, mCanceled) + : client->InitOrigin(aPersistenceType, + aOriginMetadata, mCanceled)); + return oldUsageInfo + usageInfo; + } + + case nsIFileKind::ExistsAsFile: + // We are maintaining existing behavior for unknown files here (just + // continuing). + // This can possibly be used by developers to add temporary backups + // into origin directories without losing get usage functionality. + if (IsTempMetadata(leafName)) { + if (!aInitialized) { + QM_TRY(MOZ_TO_RESULT(file->Remove(/* recursive */ false))); + } + + break; + } + + if (IsOriginMetadata(leafName) || IsOSMetadata(leafName) || + IsDotFile(leafName)) { + break; + } + + // Unknown files during getting usage for an origin (even for an + // uninitialized origin) are now allowed. Just warn if we find them. + UNKNOWN_FILE_WARNING(leafName); + break; + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return oldUsageInfo; + }))); +} + +void QuotaUsageRequestBase::SendResults() { + AssertIsOnOwningThread(); + + if (IsActorDestroyed()) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = NS_ERROR_FAILURE; + } + } else { + if (mCanceled) { + mResultCode = NS_ERROR_FAILURE; + } + + UsageRequestResponse response; + + if (NS_SUCCEEDED(mResultCode)) { + GetResponse(response); + } else { + response = mResultCode; + } + + Unused << PQuotaUsageRequestParent::Send__delete__(this, response); + } +} + +void QuotaUsageRequestBase::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + NoteActorDestroyed(); +} + +mozilla::ipc::IPCResult QuotaUsageRequestBase::RecvCancel() { + AssertIsOnOwningThread(); + + if (mCanceled.exchange(true)) { + NS_WARNING("Canceled more than once?!"); + return IPC_FAIL(this, "Request canceled more than once"); + } + + return IPC_OK(); +} + +nsresult TraverseRepositoryHelper::TraverseRepository( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT( + const auto& directory, + QM_NewLocalFile(aQuotaManager.GetStoragePath(aPersistenceType))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); + + if (!exists) { + return NS_OK; + } + + QM_TRY(CollectEachFileAtomicCancelable( + *directory, GetIsCanceledFlag(), + [this, aPersistenceType, &aQuotaManager, + persistent = aPersistenceType == PERSISTENCE_TYPE_PERSISTENT]( + const nsCOMPtr<nsIFile>& originDir) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*originDir)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: + QM_TRY(MOZ_TO_RESULT(ProcessOrigin(aQuotaManager, *originDir, + persistent, aPersistenceType))); + break; + + case nsIFileKind::ExistsAsFile: { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoString, originDir, GetLeafName)); + + // Unknown files during getting usages are allowed. Just warn if we + // find them. + if (!IsOSMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + + break; + } + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return Ok{}; + })); + + return NS_OK; +} + +GetUsageOp::GetUsageOp(const UsageRequestParams& aParams) + : QuotaUsageRequestBase("dom::quota::GetUsageOp"), + mGetAll(aParams.get_AllUsageParams().getAll()) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == UsageRequestParams::TAllUsageParams); +} + +void GetUsageOp::ProcessOriginInternal(QuotaManager* aQuotaManager, + const PersistenceType aPersistenceType, + const nsACString& aOrigin, + const int64_t aTimestamp, + const bool aPersisted, + const uint64_t aUsage) { + if (!mGetAll && aQuotaManager->IsOriginInternal(aOrigin)) { + return; + } + + // We can't store pointers to OriginUsage objects in the hashtable + // since AppendElement() reallocates its internal array buffer as number + // of elements grows. + const auto& originUsage = + mOriginUsagesIndex.WithEntryHandle(aOrigin, [&](auto&& entry) { + if (entry) { + return WrapNotNullUnchecked(&mOriginUsages[entry.Data()]); + } + + entry.Insert(mOriginUsages.Length()); + + return mOriginUsages.EmplaceBack(nsCString{aOrigin}, false, 0, 0); + }); + + if (aPersistenceType == PERSISTENCE_TYPE_DEFAULT) { + originUsage->persisted() = aPersisted; + } + + originUsage->usage() = originUsage->usage() + aUsage; + + originUsage->lastAccessed() = + std::max<int64_t>(originUsage->lastAccessed(), aTimestamp); +} + +const Atomic<bool>& GetUsageOp::GetIsCanceledFlag() { + AssertIsOnIOThread(); + + return mCanceled; +} + +// XXX Remove aPersistent +// XXX Remove aPersistenceType once GetUsageForOrigin uses the persistence +// type from OriginMetadata +nsresult GetUsageOp::ProcessOrigin(QuotaManager& aQuotaManager, + nsIFile& aOriginDir, const bool aPersistent, + const PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& metadata, + aQuotaManager.LoadFullOriginMetadataWithRestore(&aOriginDir)); + + QM_TRY_INSPECT(const auto& usageInfo, + GetUsageForOrigin(aQuotaManager, aPersistenceType, metadata)); + + ProcessOriginInternal(&aQuotaManager, aPersistenceType, metadata.mOrigin, + metadata.mLastAccessTime, metadata.mPersisted, + usageInfo.TotalUsage().valueOr(0)); + + return NS_OK; +} + +nsresult GetUsageOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + + AUTO_PROFILER_LABEL("GetUsageOp::DoDirectoryWork", OTHER); + + nsresult rv; + + for (const PersistenceType type : kAllPersistenceTypes) { + rv = TraverseRepository(aQuotaManager, type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // TraverseRepository above only consulted the filesystem. We also need to + // consider origins which may have pending quota usage, such as buffered + // LocalStorage writes for an origin which didn't previously have any + // LocalStorage data. + + aQuotaManager.CollectPendingOriginsForListing( + [this, &aQuotaManager](const auto& originInfo) { + ProcessOriginInternal( + &aQuotaManager, originInfo->GetGroupInfo()->GetPersistenceType(), + originInfo->Origin(), originInfo->LockedAccessTime(), + originInfo->LockedPersisted(), originInfo->LockedUsage()); + }); + + return NS_OK; +} + +void GetUsageOp::GetResponse(UsageRequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = AllUsageResponse(); + + aResponse.get_AllUsageResponse().originUsages() = std::move(mOriginUsages); +} + +GetOriginUsageOp::GetOriginUsageOp(const UsageRequestParams& aParams) + : QuotaUsageRequestBase("dom::quota::GetOriginUsageOp"), + mUsage(0), + mFileUsage(0) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == UsageRequestParams::TOriginUsageParams); + + const OriginUsageParams& params = aParams.get_OriginUsageParams(); + + PrincipalMetadata principalMetadata = + QuotaManager::GetInfoFromValidatedPrincipalInfo(params.principalInfo()); + + mSuffix = std::move(principalMetadata.mSuffix); + mGroup = std::move(principalMetadata.mGroup); + mOriginScope.SetFromOrigin(principalMetadata.mOrigin); + + mFromMemory = params.fromMemory(); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = true; +} + +RefPtr<DirectoryLock> GetOriginUsageOp::CreateDirectoryLock() { + if (mFromMemory) { + return nullptr; + } + + return QuotaUsageRequestBase::CreateDirectoryLock(); +} + +nsresult GetOriginUsageOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + MOZ_ASSERT(mUsage == 0); + MOZ_ASSERT(mFileUsage == 0); + + AUTO_PROFILER_LABEL("GetOriginUsageOp::DoDirectoryWork", OTHER); + + if (mFromMemory) { + const PrincipalMetadata principalMetadata = { + mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}}; + + // Ensure temporary storage is initialized. If temporary storage hasn't been + // initialized yet, the method will initialize it by traversing the + // repositories for temporary and default storage (including our origin). + QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureTemporaryStorageIsInitialized())); + + // Get cached usage (the method doesn't have to stat any files). File usage + // is not tracked in memory separately, so just add to the total usage. + mUsage = aQuotaManager.GetOriginUsage(principalMetadata); + + return NS_OK; + } + + UsageInfo usageInfo; + + // Add all the persistent/temporary/default storage files we care about. + for (const PersistenceType type : kAllPersistenceTypes) { + const OriginMetadata originMetadata = { + mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}, type}; + + auto usageInfoOrErr = + GetUsageForOrigin(aQuotaManager, type, originMetadata); + if (NS_WARN_IF(usageInfoOrErr.isErr())) { + return usageInfoOrErr.unwrapErr(); + } + + usageInfo += usageInfoOrErr.unwrap(); + } + + mUsage = usageInfo.TotalUsage().valueOr(0); + mFileUsage = usageInfo.FileUsage().valueOr(0); + + return NS_OK; +} + +void GetOriginUsageOp::GetResponse(UsageRequestResponse& aResponse) { + AssertIsOnOwningThread(); + + OriginUsageResponse usageResponse; + + usageResponse.usage() = mUsage; + usageResponse.fileUsage() = mFileUsage; + + aResponse = usageResponse; +} + +void QuotaRequestBase::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = true; +} + +void QuotaRequestBase::SendResults() { + AssertIsOnOwningThread(); + + if (IsActorDestroyed()) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = NS_ERROR_FAILURE; + } + } else { + RequestResponse response; + + if (NS_SUCCEEDED(mResultCode)) { + GetResponse(response); + } else { + response = mResultCode; + } + + Unused << PQuotaRequestParent::Send__delete__(this, response); + } +} + +void QuotaRequestBase::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + NoteActorDestroyed(); +} + +StorageNameOp::StorageNameOp() + : QuotaRequestBase("dom::quota::StorageNameOp", /* aExclusive */ false) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; +} + +void StorageNameOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +RefPtr<DirectoryLock> StorageNameOp::CreateDirectoryLock() { return nullptr; } + +nsresult StorageNameOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("StorageNameOp::DoDirectoryWork", OTHER); + + mName = aQuotaManager.GetStorageName(); + + return NS_OK; +} + +void StorageNameOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + StorageNameResponse storageNameResponse; + + storageNameResponse.name() = mName; + + aResponse = storageNameResponse; +} + +InitializedRequestBase::InitializedRequestBase(const char* aRunnableName) + : QuotaRequestBase(aRunnableName, /* aExclusive */ false), + mInitialized(false) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; +} + +void InitializedRequestBase::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +RefPtr<DirectoryLock> InitializedRequestBase::CreateDirectoryLock() { + return nullptr; +} + +nsresult StorageInitializedOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("StorageInitializedOp::DoDirectoryWork", OTHER); + + mInitialized = aQuotaManager.IsStorageInitialized(); + + return NS_OK; +} + +void StorageInitializedOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + StorageInitializedResponse storageInitializedResponse; + + storageInitializedResponse.initialized() = mInitialized; + + aResponse = storageInitializedResponse; +} + +nsresult TemporaryStorageInitializedOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("TemporaryStorageInitializedOp::DoDirectoryWork", OTHER); + + mInitialized = aQuotaManager.IsTemporaryStorageInitialized(); + + return NS_OK; +} + +void TemporaryStorageInitializedOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + TemporaryStorageInitializedResponse temporaryStorageInitializedResponse; + + temporaryStorageInitializedResponse.initialized() = mInitialized; + + aResponse = temporaryStorageInitializedResponse; +} + +InitOp::InitOp() + : QuotaRequestBase("dom::quota::InitOp", /* aExclusive */ false) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; +} + +void InitOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +nsresult InitOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("InitOp::DoDirectoryWork", OTHER); + + QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureStorageIsInitialized())); + + return NS_OK; +} + +void InitOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = InitResponse(); +} + +InitTemporaryStorageOp::InitTemporaryStorageOp() + : QuotaRequestBase("dom::quota::InitTemporaryStorageOp", + /* aExclusive */ false) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; +} + +void InitTemporaryStorageOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +nsresult InitTemporaryStorageOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("InitTemporaryStorageOp::DoDirectoryWork", OTHER); + + QM_TRY(OkIf(aQuotaManager.IsStorageInitialized()), NS_ERROR_FAILURE); + + QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureTemporaryStorageIsInitialized())); + + return NS_OK; +} + +void InitTemporaryStorageOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = InitTemporaryStorageResponse(); +} + +InitializeOriginRequestBase::InitializeOriginRequestBase( + const char* aRunnableName, const PersistenceType aPersistenceType, + const PrincipalInfo& aPrincipalInfo) + : QuotaRequestBase(aRunnableName, + /* aExclusive */ false), + mCreated(false) { + AssertIsOnOwningThread(); + + auto principalMetadata = + QuotaManager::GetInfoFromValidatedPrincipalInfo(aPrincipalInfo); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; + + // Overwrite NormalOriginOperationBase default values. + mPersistenceType.SetValue(aPersistenceType); + mOriginScope.SetFromOrigin(principalMetadata.mOrigin); + + // Overwrite InitializeOriginRequestBase default values. + mSuffix = std::move(principalMetadata.mSuffix); + mGroup = std::move(principalMetadata.mGroup); +} + +void InitializeOriginRequestBase::Init(Quota& aQuota) { + AssertIsOnOwningThread(); +} + +InitializePersistentOriginOp::InitializePersistentOriginOp( + const RequestParams& aParams) + : InitializeOriginRequestBase( + "dom::quota::InitializePersistentOriginOp", + PERSISTENCE_TYPE_PERSISTENT, + aParams.get_InitializePersistentOriginParams().principalInfo()) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == + RequestParams::TInitializePersistentOriginParams); +} + +nsresult InitializePersistentOriginOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + + AUTO_PROFILER_LABEL("InitializePersistentOriginOp::DoDirectoryWork", OTHER); + + QM_TRY(OkIf(aQuotaManager.IsStorageInitialized()), NS_ERROR_FAILURE); + + QM_TRY_UNWRAP(mCreated, + (aQuotaManager + .EnsurePersistentOriginIsInitialized(OriginMetadata{ + mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}, + PERSISTENCE_TYPE_PERSISTENT}) + .map([](const auto& res) { return res.second; }))); + + return NS_OK; +} + +void InitializePersistentOriginOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = InitializePersistentOriginResponse(mCreated); +} + +InitializeTemporaryOriginOp::InitializeTemporaryOriginOp( + const RequestParams& aParams) + : InitializeOriginRequestBase( + "dom::quota::InitializeTemporaryOriginOp", + aParams.get_InitializeTemporaryOriginParams().persistenceType(), + aParams.get_InitializeTemporaryOriginParams().principalInfo()) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == RequestParams::TInitializeTemporaryOriginParams); +} + +nsresult InitializeTemporaryOriginOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + + AUTO_PROFILER_LABEL("InitializeTemporaryOriginOp::DoDirectoryWork", OTHER); + + QM_TRY(OkIf(aQuotaManager.IsStorageInitialized()), NS_ERROR_FAILURE); + + QM_TRY(OkIf(aQuotaManager.IsTemporaryStorageInitialized()), NS_ERROR_FAILURE); + + QM_TRY_UNWRAP(mCreated, + (aQuotaManager + .EnsureTemporaryOriginIsInitialized( + mPersistenceType.Value(), + OriginMetadata{mSuffix, mGroup, + nsCString{mOriginScope.GetOrigin()}, + mPersistenceType.Value()}) + .map([](const auto& res) { return res.second; }))); + + return NS_OK; +} + +void InitializeTemporaryOriginOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = InitializeTemporaryOriginResponse(mCreated); +} + +GetFullOriginMetadataOp::GetFullOriginMetadataOp( + const GetFullOriginMetadataParams& aParams) + : QuotaRequestBase("dom::quota::GetFullOriginMetadataOp", + /* aExclusive */ false), + mOriginMetadata(QuotaManager::GetInfoFromValidatedPrincipalInfo( + aParams.principalInfo()), + aParams.persistenceType()) { + AssertIsOnOwningThread(); +} + +RefPtr<DirectoryLock> GetFullOriginMetadataOp::CreateDirectoryLock() { + return nullptr; +} + +nsresult GetFullOriginMetadataOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("GetFullOriginMetadataOp::DoDirectoryWork", OTHER); + + // Ensure temporary storage is initialized. If temporary storage hasn't + // been initialized yet, the method will initialize it by traversing the + // repositories for temporary and default storage (including our origin). + QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureTemporaryStorageIsInitialized())); + + // Get metadata cached in memory (the method doesn't have to stat any + // files). + mMaybeFullOriginMetadata = + aQuotaManager.GetFullOriginMetadata(mOriginMetadata); + + return NS_OK; +} + +void GetFullOriginMetadataOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = GetFullOriginMetadataResponse(); + aResponse.get_GetFullOriginMetadataResponse().maybeFullOriginMetadata() = + std::move(mMaybeFullOriginMetadata); +} + +ResetOrClearOp::ResetOrClearOp(bool aClear) + : QuotaRequestBase("dom::quota::ResetOrClearOp", /* aExclusive */ true), + mClear(aClear) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; +} + +void ResetOrClearOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +void ResetOrClearOp::DeleteFiles(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + nsresult rv = aQuotaManager.AboutToClearOrigins(Nullable<PersistenceType>(), + OriginScope::FromNull(), + Nullable<Client::Type>()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + auto directoryOrErr = QM_NewLocalFile(aQuotaManager.GetStoragePath()); + if (NS_WARN_IF(directoryOrErr.isErr())) { + return; + } + + nsCOMPtr<nsIFile> directory = directoryOrErr.unwrap(); + + rv = directory->Remove(true); + if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + // This should never fail if we've closed all storage connections + // correctly... + MOZ_ASSERT(false, "Failed to remove storage directory!"); + } +} + +void ResetOrClearOp::DeleteStorageFile(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& storageFile, + QM_NewLocalFile(aQuotaManager.GetBasePath()), QM_VOID); + + QM_TRY(MOZ_TO_RESULT(storageFile->Append(aQuotaManager.GetStorageName() + + kSQLiteSuffix)), + QM_VOID); + + const nsresult rv = storageFile->Remove(true); + if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + // This should never fail if we've closed the storage connection + // correctly... + MOZ_ASSERT(false, "Failed to remove storage file!"); + } +} + +nsresult ResetOrClearOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("ResetOrClearOp::DoDirectoryWork", OTHER); + + if (mClear) { + DeleteFiles(aQuotaManager); + + aQuotaManager.RemoveQuota(); + } + + aQuotaManager.ShutdownStorageInternal(); + + if (mClear) { + DeleteStorageFile(aQuotaManager); + } + + return NS_OK; +} + +void ResetOrClearOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + if (mClear) { + aResponse = ClearAllResponse(); + } else { + aResponse = ResetAllResponse(); + } +} + +static Result<nsCOMPtr<nsIFile>, QMResult> OpenToBeRemovedDirectory( + const nsAString& aStoragePath) { + QM_TRY_INSPECT(const auto& dir, + QM_TO_RESULT_TRANSFORM(QM_NewLocalFile(aStoragePath))); + QM_TRY(QM_TO_RESULT(dir->Append(u"to-be-removed"_ns))); + + nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0700); + if (NS_SUCCEEDED(rv) || rv == NS_ERROR_FILE_ALREADY_EXISTS) { + return dir; + } + return Err(QMResult(rv)); +} + +static Result<Ok, QMResult> RemoveOrMoveToDir(nsIFile& aFile, + nsIFile* aMoveTargetDir) { + if (!aMoveTargetDir) { + QM_TRY(QM_TO_RESULT(aFile.Remove(true))); + return Ok(); + } + + nsIDToCString uuid(nsID::GenerateUUID()); + NS_ConvertUTF8toUTF16 subDirName(uuid.get(), NSID_LENGTH - 1); + QM_TRY(QM_TO_RESULT(aFile.MoveTo(aMoveTargetDir, subDirName))); + return Ok(); +} + +void ClearRequestBase::DeleteFiles(QuotaManager& aQuotaManager, + PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + QM_TRY(MOZ_TO_RESULT(aQuotaManager.AboutToClearOrigins( + Nullable<PersistenceType>(aPersistenceType), mOriginScope, + mClientType)), + QM_VOID); + + QM_TRY_INSPECT( + const auto& directory, + QM_NewLocalFile(aQuotaManager.GetStoragePath(aPersistenceType)), QM_VOID); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists), QM_VOID); + + if (!exists) { + return; + } + + nsTArray<nsCOMPtr<nsIFile>> directoriesForRemovalRetry; + + aQuotaManager.MaybeRecordQuotaManagerShutdownStep( + "ClearRequestBase: Starting deleting files"_ns); + nsCOMPtr<nsIFile> toBeRemovedDir; + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownTeardown)) { + QM_WARNONLY_TRY_UNWRAP( + auto result, OpenToBeRemovedDirectory(aQuotaManager.GetStoragePath())); + toBeRemovedDir = result.valueOr(nullptr); + } + QM_TRY( + CollectEachFile( + *directory, + [originScope = + [this] { + OriginScope originScope = mOriginScope.Clone(); + if (originScope.IsOrigin()) { + originScope.SetOrigin( + MakeSanitizedOriginCString(originScope.GetOrigin())); + } else if (originScope.IsPrefix()) { + originScope.SetOriginNoSuffix(MakeSanitizedOriginCString( + originScope.GetOriginNoSuffix())); + } + return originScope; + }(), + aPersistenceType, &aQuotaManager, &directoriesForRemovalRetry, + &toBeRemovedDir, + this](nsCOMPtr<nsIFile>&& file) -> mozilla::Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, + GetLeafName)); + + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: { + // Skip the origin directory if it doesn't match the pattern. + if (!originScope.Matches(OriginScope::FromOrigin( + NS_ConvertUTF16toUTF8(leafName)))) { + break; + } + + QM_TRY_INSPECT( + const auto& metadata, + aQuotaManager.LoadFullOriginMetadataWithRestore(file)); + + MOZ_ASSERT(metadata.mPersistenceType == aPersistenceType); + + if (!mClientType.IsNull()) { + nsAutoString clientDirectoryName; + QM_TRY( + OkIf(Client::TypeToText(mClientType.Value(), + clientDirectoryName, fallible)), + Err(NS_ERROR_FAILURE)); + + QM_TRY(MOZ_TO_RESULT(file->Append(clientDirectoryName))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(file, Exists)); + + if (!exists) { + break; + } + } + + // We can't guarantee that this will always succeed on + // Windows... + QM_WARNONLY_TRY( + RemoveOrMoveToDir(*file, toBeRemovedDir), [&](const auto&) { + directoriesForRemovalRetry.AppendElement(std::move(file)); + }); + + const bool initialized = + aPersistenceType == PERSISTENCE_TYPE_PERSISTENT + ? aQuotaManager.IsOriginInitialized(metadata.mOrigin) + : aQuotaManager.IsTemporaryStorageInitialized(); + + // If it hasn't been initialized, we don't need to update the + // quota and notify the removing client. + if (!initialized) { + break; + } + + if (aPersistenceType != PERSISTENCE_TYPE_PERSISTENT) { + if (mClientType.IsNull()) { + aQuotaManager.RemoveQuotaForOrigin(aPersistenceType, + metadata); + } else { + aQuotaManager.ResetUsageForClient( + ClientMetadata{metadata, mClientType.Value()}); + } + } + + aQuotaManager.OriginClearCompleted( + aPersistenceType, metadata.mOrigin, mClientType); + + break; + } + + case nsIFileKind::ExistsAsFile: + // Unknown files during clearing are allowed. Just warn if we + // find them. + if (!IsOSMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + + break; + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return Ok{}; + }), + QM_VOID); + + // Retry removing any directories that failed to be removed earlier now. + // + // XXX This will still block this operation. We might instead dispatch a + // runnable to our own thread for each retry round with a timer. We must + // ensure that the directory lock is upheld until we complete or give up + // though. + for (uint32_t index = 0; index < 10; index++) { + aQuotaManager.MaybeRecordQuotaManagerShutdownStepWith([index]() { + return nsPrintfCString( + "ClearRequestBase: Starting repeated directory removal #%d", index); + }); + + for (auto&& file : std::exchange(directoriesForRemovalRetry, + nsTArray<nsCOMPtr<nsIFile>>{})) { + QM_WARNONLY_TRY( + QM_TO_RESULT(file->Remove(true)), + ([&directoriesForRemovalRetry, &file](const auto&) { + directoriesForRemovalRetry.AppendElement(std::move(file)); + })); + } + + aQuotaManager.MaybeRecordQuotaManagerShutdownStepWith([index]() { + return nsPrintfCString( + "ClearRequestBase: Completed repeated directory removal #%d", index); + }); + + if (directoriesForRemovalRetry.IsEmpty()) { + break; + } + + aQuotaManager.MaybeRecordQuotaManagerShutdownStepWith([index]() { + return nsPrintfCString("ClearRequestBase: Before sleep #%d", index); + }); + + PR_Sleep(PR_MillisecondsToInterval(200)); + + aQuotaManager.MaybeRecordQuotaManagerShutdownStepWith([index]() { + return nsPrintfCString("ClearRequestBase: After sleep #%d", index); + }); + } + + QM_WARNONLY_TRY(OkIf(directoriesForRemovalRetry.IsEmpty())); + + aQuotaManager.MaybeRecordQuotaManagerShutdownStep( + "ClearRequestBase: Completed deleting files"_ns); +} + +nsresult ClearRequestBase::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + + AUTO_PROFILER_LABEL("ClearRequestBase::DoDirectoryWork", OTHER); + + if (mPersistenceType.IsNull()) { + for (const PersistenceType type : kAllPersistenceTypes) { + DeleteFiles(aQuotaManager, type); + } + } else { + DeleteFiles(aQuotaManager, mPersistenceType.Value()); + } + + return NS_OK; +} + +ClearOriginOp::ClearOriginOp(const RequestParams& aParams) + : ClearRequestBase("dom::quota::ClearOriginOp", /* aExclusive */ true), + mParams(aParams.get_ClearOriginParams().commonParams()), + mMatchAll(aParams.get_ClearOriginParams().matchAll()) { + MOZ_ASSERT(aParams.type() == RequestParams::TClearOriginParams); +} + +void ClearOriginOp::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + QuotaRequestBase::Init(aQuota); + + if (mParams.persistenceTypeIsExplicit()) { + mPersistenceType.SetValue(mParams.persistenceType()); + } + + // Figure out which origin we're dealing with. + const auto origin = QuotaManager::GetOriginFromValidatedPrincipalInfo( + mParams.principalInfo()); + + if (mMatchAll) { + mOriginScope.SetFromPrefix(origin); + } else { + mOriginScope.SetFromOrigin(origin); + } + + if (mParams.clientTypeIsExplicit()) { + mClientType.SetValue(mParams.clientType()); + } +} + +void ClearOriginOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ClearOriginResponse(); +} + +ClearDataOp::ClearDataOp(const RequestParams& aParams) + : ClearRequestBase("dom::quota::ClearDataOp", /* aExclusive */ true), + mParams(aParams) { + MOZ_ASSERT(aParams.type() == RequestParams::TClearDataParams); +} + +void ClearDataOp::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + QuotaRequestBase::Init(aQuota); + + mOriginScope.SetFromPattern(mParams.pattern()); +} + +void ClearDataOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ClearDataResponse(); +} + +ResetOriginOp::ResetOriginOp(const RequestParams& aParams) + : QuotaRequestBase("dom::quota::ResetOriginOp", /* aExclusive */ true) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == RequestParams::TResetOriginParams); + + const ClearResetOriginParams& params = + aParams.get_ResetOriginParams().commonParams(); + + const auto origin = + QuotaManager::GetOriginFromValidatedPrincipalInfo(params.principalInfo()); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; + + // Overwrite NormalOriginOperationBase default values. + if (params.persistenceTypeIsExplicit()) { + mPersistenceType.SetValue(params.persistenceType()); + } + + mOriginScope.SetFromOrigin(origin); + + if (params.clientTypeIsExplicit()) { + mClientType.SetValue(params.clientType()); + } +} + +void ResetOriginOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +nsresult ResetOriginOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("ResetOriginOp::DoDirectoryWork", OTHER); + + // All the work is handled by NormalOriginOperationBase parent class. In this + // particular case, we just needed to acquire an exclusive directory lock and + // that's it. + + return NS_OK; +} + +void ResetOriginOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ResetOriginResponse(); +} + +PersistRequestBase::PersistRequestBase(const PrincipalInfo& aPrincipalInfo) + : QuotaRequestBase("dom::quota::PersistRequestBase", + /* aExclusive */ false), + mPrincipalInfo(aPrincipalInfo) { + AssertIsOnOwningThread(); +} + +void PersistRequestBase::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + QuotaRequestBase::Init(aQuota); + + mPersistenceType.SetValue(PERSISTENCE_TYPE_DEFAULT); + + // Figure out which origin we're dealing with. + PrincipalMetadata principalMetadata = + QuotaManager::GetInfoFromValidatedPrincipalInfo(mPrincipalInfo); + + mSuffix = std::move(principalMetadata.mSuffix); + mGroup = std::move(principalMetadata.mGroup); + mOriginScope.SetFromOrigin(principalMetadata.mOrigin); +} + +PersistedOp::PersistedOp(const RequestParams& aParams) + : PersistRequestBase(aParams.get_PersistedParams().principalInfo()), + mPersisted(false) { + MOZ_ASSERT(aParams.type() == RequestParams::TPersistedParams); +} + +nsresult PersistedOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + MOZ_ASSERT(mPersistenceType.Value() == PERSISTENCE_TYPE_DEFAULT); + MOZ_ASSERT(mOriginScope.IsOrigin()); + + AUTO_PROFILER_LABEL("PersistedOp::DoDirectoryWork", OTHER); + + Nullable<bool> persisted = aQuotaManager.OriginPersisted( + OriginMetadata{mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}, + mPersistenceType.Value()}); + + if (!persisted.IsNull()) { + mPersisted = persisted.Value(); + return NS_OK; + } + + // If we get here, it means the origin hasn't been initialized yet. + // Try to get the persisted flag from directory metadata on disk. + + QM_TRY_INSPECT(const auto& directory, + aQuotaManager.GetDirectoryForOrigin(mPersistenceType.Value(), + mOriginScope.GetOrigin())); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); + + if (exists) { + // Get the metadata. We only use the persisted flag. + QM_TRY_INSPECT(const auto& metadata, + aQuotaManager.LoadFullOriginMetadataWithRestore(directory)); + + mPersisted = metadata.mPersisted; + } else { + // The directory has not been created yet. + mPersisted = false; + } + + return NS_OK; +} + +void PersistedOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + PersistedResponse persistedResponse; + persistedResponse.persisted() = mPersisted; + + aResponse = persistedResponse; +} + +PersistOp::PersistOp(const RequestParams& aParams) + : PersistRequestBase(aParams.get_PersistParams().principalInfo()) { + MOZ_ASSERT(aParams.type() == RequestParams::TPersistParams); +} + +nsresult PersistOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + MOZ_ASSERT(mPersistenceType.Value() == PERSISTENCE_TYPE_DEFAULT); + MOZ_ASSERT(mOriginScope.IsOrigin()); + + const OriginMetadata originMetadata = {mSuffix, mGroup, + nsCString{mOriginScope.GetOrigin()}, + mPersistenceType.Value()}; + + AUTO_PROFILER_LABEL("PersistOp::DoDirectoryWork", OTHER); + + // Update directory metadata on disk first. Then, create/update the originInfo + // if needed. + QM_TRY_INSPECT(const auto& directory, + aQuotaManager.GetDirectoryForOrigin(mPersistenceType.Value(), + originMetadata.mOrigin)); + + QM_TRY_INSPECT(const bool& created, + aQuotaManager.EnsureOriginDirectory(*directory)); + + if (created) { + int64_t timestamp; + + // Origin directory has been successfully created. + // Create OriginInfo too if temporary storage was already initialized. + if (aQuotaManager.IsTemporaryStorageInitialized()) { + timestamp = aQuotaManager.NoteOriginDirectoryCreated( + originMetadata, /* aPersisted */ true); + } else { + timestamp = PR_Now(); + } + + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(*directory, timestamp, + /* aPersisted */ true, + originMetadata))); + } else { + // Get the metadata (restore the metadata file if necessary). We only use + // the persisted flag. + QM_TRY_INSPECT(const auto& metadata, + aQuotaManager.LoadFullOriginMetadataWithRestore(directory)); + + if (!metadata.mPersisted) { + QM_TRY_INSPECT(const auto& file, + CloneFileAndAppend( + *directory, nsLiteralString(METADATA_V2_FILE_NAME))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, FileFlag::Update)); + + MOZ_ASSERT(stream); + + // Update origin access time while we are here. + QM_TRY(MOZ_TO_RESULT(stream->Write64(PR_Now()))); + + // Set the persisted flag to true. + QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(true))); + } + + // Directory metadata has been successfully updated. + // Update OriginInfo too if temporary storage was already initialized. + if (aQuotaManager.IsTemporaryStorageInitialized()) { + aQuotaManager.PersistOrigin(originMetadata); + } + } + + return NS_OK; +} + +void PersistOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = PersistResponse(); +} + +EstimateOp::EstimateOp(const EstimateParams& aParams) + : QuotaRequestBase("dom::quota::EstimateOp", /* aExclusive */ false), + mOriginMetadata(QuotaManager::GetInfoFromValidatedPrincipalInfo( + aParams.principalInfo()), + PERSISTENCE_TYPE_DEFAULT) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = true; +} + +RefPtr<DirectoryLock> EstimateOp::CreateDirectoryLock() { return nullptr; } + +nsresult EstimateOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + + AUTO_PROFILER_LABEL("EstimateOp::DoDirectoryWork", OTHER); + + // Ensure temporary storage is initialized. If temporary storage hasn't been + // initialized yet, the method will initialize it by traversing the + // repositories for temporary and default storage (including origins belonging + // to our group). + QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureTemporaryStorageIsInitialized())); + + // Get cached usage (the method doesn't have to stat any files). + mUsageAndLimit = aQuotaManager.GetUsageAndLimitForEstimate(mOriginMetadata); + + return NS_OK; +} + +void EstimateOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + EstimateResponse estimateResponse; + + estimateResponse.usage() = mUsageAndLimit.first; + estimateResponse.limit() = mUsageAndLimit.second; + + aResponse = estimateResponse; +} + +ListOriginsOp::ListOriginsOp() + : QuotaRequestBase("dom::quota::ListOriginsOp", /* aExclusive */ false), + TraverseRepositoryHelper() { + AssertIsOnOwningThread(); +} + +void ListOriginsOp::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = true; +} + +nsresult ListOriginsOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + + AUTO_PROFILER_LABEL("ListOriginsOp::DoDirectoryWork", OTHER); + + for (const PersistenceType type : kAllPersistenceTypes) { + QM_TRY(MOZ_TO_RESULT(TraverseRepository(aQuotaManager, type))); + } + + // TraverseRepository above only consulted the file-system to get a list of + // known origins, but we also need to include origins that have pending quota + // usage. + + aQuotaManager.CollectPendingOriginsForListing([this](const auto& originInfo) { + mOrigins.AppendElement(originInfo->Origin()); + }); + + return NS_OK; +} + +const Atomic<bool>& ListOriginsOp::GetIsCanceledFlag() { + AssertIsOnIOThread(); + + return mCanceled; +} + +nsresult ListOriginsOp::ProcessOrigin(QuotaManager& aQuotaManager, + nsIFile& aOriginDir, + const bool aPersistent, + const PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + // XXX We only use metadata.mOriginMetadata.mOrigin... + QM_TRY_UNWRAP(auto metadata, + aQuotaManager.LoadFullOriginMetadataWithRestore(&aOriginDir)); + + if (aQuotaManager.IsOriginInternal(metadata.mOrigin)) { + return NS_OK; + } + + mOrigins.AppendElement(std::move(metadata.mOrigin)); + + return NS_OK; +} + +void ListOriginsOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ListOriginsResponse(); + if (mOrigins.IsEmpty()) { + return; + } + + nsTArray<nsCString>& origins = aResponse.get_ListOriginsResponse().origins(); + mOrigins.SwapElements(origins); +} + +#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED + +// static +already_AddRefed<PrincipalVerifier> PrincipalVerifier::CreateAndDispatch( + nsTArray<PrincipalInfo>&& aPrincipalInfos) { + AssertIsOnIOThread(); + + RefPtr<PrincipalVerifier> verifier = + new PrincipalVerifier(std::move(aPrincipalInfos)); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(verifier)); + + return verifier.forget(); +} + +Result<Ok, nsCString> PrincipalVerifier::CheckPrincipalInfoValidity( + const PrincipalInfo& aPrincipalInfo) { + MOZ_ASSERT(NS_IsMainThread()); + + switch (aPrincipalInfo.type()) { + // A system principal is acceptable. + case PrincipalInfo::TSystemPrincipalInfo: { + return Ok{}; + } + + case PrincipalInfo::TContentPrincipalInfo: { + const ContentPrincipalInfo& info = + aPrincipalInfo.get_ContentPrincipalInfo(); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), info.spec()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err("NS_NewURI failed"_ns); + } + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(uri, info.attrs()); + if (NS_WARN_IF(!principal)) { + return Err("CreateContentPrincipal failed"_ns); + } + + nsCString originNoSuffix; + rv = principal->GetOriginNoSuffix(originNoSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err("GetOriginNoSuffix failed"_ns); + } + + if (NS_WARN_IF(originNoSuffix != info.originNoSuffix())) { + static const char messageTemplate[] = + "originNoSuffix (%s) doesn't match passed one (%s)!"; + + QM_WARNING(messageTemplate, originNoSuffix.get(), + info.originNoSuffix().get()); + + return Err(nsPrintfCString( + messageTemplate, AnonymizedOriginString(originNoSuffix).get(), + AnonymizedOriginString(info.originNoSuffix()).get())); + } + + nsCString baseDomain; + rv = principal->GetBaseDomain(baseDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err("GetBaseDomain failed"_ns); + } + + if (NS_WARN_IF(baseDomain != info.baseDomain())) { + static const char messageTemplate[] = + "baseDomain (%s) doesn't match passed one (%s)!"; + + QM_WARNING(messageTemplate, baseDomain.get(), info.baseDomain().get()); + + return Err(nsPrintfCString(messageTemplate, + AnonymizedCString(baseDomain).get(), + AnonymizedCString(info.baseDomain()).get())); + } + + return Ok{}; + } + + default: { + break; + } + } + + return Err("Null and expanded principals are not acceptable"_ns); +} + +NS_IMETHODIMP +PrincipalVerifier::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoCString allDetails; + for (auto& principalInfo : mPrincipalInfos) { + const auto res = CheckPrincipalInfoValidity(principalInfo); + if (res.isErr()) { + if (!allDetails.IsEmpty()) { + allDetails.AppendLiteral(", "); + } + + allDetails.Append(res.inspectErr()); + } + } + + if (!allDetails.IsEmpty()) { + allDetails.Insert("Invalid principal infos found: ", 0); + + // In case of invalid principal infos, this will produce a crash reason such + // as: + // Invalid principal infos found: originNoSuffix (https://aaa.aaaaaaa.aaa) + // doesn't match passed one (about:aaaa)! + // + // In case of errors while validating a principal, it will contain a + // different message describing that error, which does not contain any + // details of the actual principal info at the moment. + // + // This string will be leaked. + MOZ_CRASH_UNSAFE(strdup(allDetails.BeginReading())); + } + + return NS_OK; +} + +#endif + +nsresult StorageOperationBase::GetDirectoryMetadata(nsIFile* aDirectory, + int64_t& aTimestamp, + nsACString& aGroup, + nsACString& aOrigin, + Nullable<bool>& aIsApp) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + + QM_TRY_INSPECT( + const auto& binaryStream, + GetBinaryInputStream(*aDirectory, nsLiteralString(METADATA_FILE_NAME))); + + QM_TRY_INSPECT(const uint64_t& timestamp, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64)); + + QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, binaryStream, ReadCString)); + + Nullable<bool> isApp; + bool value; + if (NS_SUCCEEDED(binaryStream->ReadBoolean(&value))) { + isApp.SetValue(value); + } + + aTimestamp = timestamp; + aGroup = group; + aOrigin = origin; + aIsApp = std::move(isApp); + return NS_OK; +} + +nsresult StorageOperationBase::GetDirectoryMetadata2( + nsIFile* aDirectory, int64_t& aTimestamp, nsACString& aSuffix, + nsACString& aGroup, nsACString& aOrigin, bool& aIsApp) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + + QM_TRY_INSPECT(const auto& binaryStream, + GetBinaryInputStream(*aDirectory, + nsLiteralString(METADATA_V2_FILE_NAME))); + + QM_TRY_INSPECT(const uint64_t& timestamp, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64)); + + QM_TRY_INSPECT(const bool& persisted, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean)); + Unused << persisted; + + QM_TRY_INSPECT(const bool& reservedData1, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32)); + Unused << reservedData1; + + QM_TRY_INSPECT(const bool& reservedData2, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32)); + Unused << reservedData2; + + QM_TRY_INSPECT(const auto& suffix, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_INSPECT(const bool& isApp, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean)); + + aTimestamp = timestamp; + aSuffix = suffix; + aGroup = group; + aOrigin = origin; + aIsApp = isApp; + return NS_OK; +} + +int64_t StorageOperationBase::GetOriginLastModifiedTime( + const OriginProps& aOriginProps) { + return GetLastModifiedTime(*aOriginProps.mPersistenceType, + *aOriginProps.mDirectory); +} + +nsresult StorageOperationBase::RemoveObsoleteOrigin( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + QM_WARNING( + "Deleting obsolete %s directory that is no longer a legal " + "origin!", + NS_ConvertUTF16toUTF8(aOriginProps.mLeafName).get()); + + QM_TRY(MOZ_TO_RESULT(aOriginProps.mDirectory->Remove(/* recursive */ true))); + + return NS_OK; +} + +Result<bool, nsresult> StorageOperationBase::MaybeRenameOrigin( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + const nsAString& oldLeafName = aOriginProps.mLeafName; + + const auto newLeafName = + MakeSanitizedOriginString(aOriginProps.mOriginMetadata.mOrigin); + + if (oldLeafName == newLeafName) { + return false; + } + + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata(*aOriginProps.mDirectory, + aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + + QM_TRY_INSPECT(const auto& newFile, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIFile>, *aOriginProps.mDirectory, GetParent)); + + QM_TRY(MOZ_TO_RESULT(newFile->Append(newLeafName))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(newFile, Exists)); + + if (exists) { + QM_WARNING( + "Can't rename %s directory to %s, the target already exists, removing " + "instead of renaming!", + NS_ConvertUTF16toUTF8(oldLeafName).get(), + NS_ConvertUTF16toUTF8(newLeafName).get()); + } + + QM_TRY(CallWithDelayedRetriesIfAccessDenied( + [&exists, &aOriginProps, &newLeafName] { + if (exists) { + QM_TRY_RETURN(MOZ_TO_RESULT( + aOriginProps.mDirectory->Remove(/* recursive */ true))); + } + QM_TRY_RETURN(MOZ_TO_RESULT( + aOriginProps.mDirectory->RenameTo(nullptr, newLeafName))); + }, + StaticPrefs::dom_quotaManager_directoryRemovalOrRenaming_maxRetries(), + StaticPrefs::dom_quotaManager_directoryRemovalOrRenaming_delayMs())); + + return true; +} + +nsresult StorageOperationBase::ProcessOriginDirectories() { + AssertIsOnIOThread(); + MOZ_ASSERT(!mOriginProps.IsEmpty()); + +#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED + nsTArray<PrincipalInfo> principalInfos; +#endif + + for (auto& originProps : mOriginProps) { + switch (originProps.mType) { + case OriginProps::eChrome: { + originProps.mOriginMetadata = {QuotaManager::GetInfoForChrome(), + *originProps.mPersistenceType}; + break; + } + + case OriginProps::eContent: { + RefPtr<MozURL> specURL; + nsresult rv = MozURL::Init(getter_AddRefs(specURL), originProps.mSpec); + if (NS_WARN_IF(NS_FAILED(rv))) { + // If a URL cannot be understood by MozURL during restoring or + // upgrading, either marking the directory as broken or removing that + // corresponding directory should be considered. While the cost of + // marking the directory as broken during a upgrade is too high, + // removing the directory is a better choice rather than blocking the + // initialization or the upgrade. + QM_WARNING( + "A URL (%s) for the origin directory is not recognized by " + "MozURL. The directory will be deleted for now to pass the " + "initialization or the upgrade.", + originProps.mSpec.get()); + + originProps.mType = OriginProps::eObsolete; + break; + } + + nsCString originNoSuffix; + specURL->Origin(originNoSuffix); + + QM_TRY_INSPECT( + const auto& baseDomain, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, specURL, BaseDomain)); + + ContentPrincipalInfo contentPrincipalInfo; + contentPrincipalInfo.attrs() = originProps.mAttrs; + contentPrincipalInfo.originNoSuffix() = originNoSuffix; + contentPrincipalInfo.spec() = originProps.mSpec; + contentPrincipalInfo.baseDomain() = baseDomain; + + PrincipalInfo principalInfo(contentPrincipalInfo); + + originProps.mOriginMetadata = { + QuotaManager::GetInfoFromValidatedPrincipalInfo(principalInfo), + *originProps.mPersistenceType}; + +#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED + principalInfos.AppendElement(principalInfo); +#endif + + break; + } + + case OriginProps::eObsolete: { + // There's no way to get info for obsolete origins. + break; + } + + default: + MOZ_CRASH("Bad type!"); + } + } + +#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED + if (!principalInfos.IsEmpty()) { + RefPtr<PrincipalVerifier> principalVerifier = + PrincipalVerifier::CreateAndDispatch(std::move(principalInfos)); + } +#endif + + // Don't try to upgrade obsolete origins, remove them right after we detect + // them. + for (const auto& originProps : mOriginProps) { + if (originProps.mType == OriginProps::eObsolete) { + MOZ_ASSERT(originProps.mOriginMetadata.mSuffix.IsEmpty()); + MOZ_ASSERT(originProps.mOriginMetadata.mGroup.IsEmpty()); + MOZ_ASSERT(originProps.mOriginMetadata.mOrigin.IsEmpty()); + + QM_TRY(MOZ_TO_RESULT(RemoveObsoleteOrigin(originProps))); + } else { + MOZ_ASSERT(!originProps.mOriginMetadata.mGroup.IsEmpty()); + MOZ_ASSERT(!originProps.mOriginMetadata.mOrigin.IsEmpty()); + + QM_TRY(MOZ_TO_RESULT(ProcessOriginDirectory(originProps))); + } + } + + return NS_OK; +} + +// XXX Do the fallible initialization in a separate non-static member function +// of StorageOperationBase and eventually get rid of this method and use a +// normal constructor instead. +template <typename PersistenceTypeFunc> +nsresult StorageOperationBase::OriginProps::Init( + PersistenceTypeFunc&& aPersistenceTypeFunc) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, *mDirectory, + GetLeafName)); + + nsCString spec; + OriginAttributes attrs; + nsCString originalSuffix; + OriginParser::ResultType result = OriginParser::ParseOrigin( + NS_ConvertUTF16toUTF8(leafName), spec, &attrs, originalSuffix); + if (NS_WARN_IF(result == OriginParser::InvalidOrigin)) { + mType = OriginProps::eInvalid; + return NS_OK; + } + + const auto persistenceType = [&]() -> PersistenceType { + // XXX We shouldn't continue with initialization if OriginParser returned + // anything else but ValidOrigin. Otherwise, we have to deal with empty + // spec when the origin is obsolete, like here. The caller should handle + // the errors. Until it's fixed, we have to treat obsolete origins as + // origins with unknown/invalid persistence type. + if (result != OriginParser::ValidOrigin) { + return PERSISTENCE_TYPE_INVALID; + } + return std::forward<PersistenceTypeFunc>(aPersistenceTypeFunc)(spec); + }(); + + mLeafName = leafName; + mSpec = spec; + mAttrs = attrs; + mOriginalSuffix = originalSuffix; + mPersistenceType.init(persistenceType); + if (result == OriginParser::ObsoleteOrigin) { + mType = eObsolete; + } else if (mSpec.EqualsLiteral(kChromeOrigin)) { + mType = eChrome; + } else { + mType = eContent; + } + + return NS_OK; +} + +// static +auto OriginParser::ParseOrigin(const nsACString& aOrigin, nsCString& aSpec, + OriginAttributes* aAttrs, + nsCString& aOriginalSuffix) -> ResultType { + MOZ_ASSERT(!aOrigin.IsEmpty()); + MOZ_ASSERT(aAttrs); + + nsCString origin(aOrigin); + int32_t pos = origin.RFindChar('^'); + + if (pos == kNotFound) { + aOriginalSuffix.Truncate(); + } else { + aOriginalSuffix = Substring(origin, pos); + } + + OriginAttributes originAttributes; + + nsCString originNoSuffix; + bool ok = originAttributes.PopulateFromOrigin(aOrigin, originNoSuffix); + if (!ok) { + return InvalidOrigin; + } + + OriginParser parser(originNoSuffix); + + *aAttrs = originAttributes; + return parser.Parse(aSpec); +} + +auto OriginParser::Parse(nsACString& aSpec) -> ResultType { + while (mTokenizer.hasMoreTokens()) { + const nsDependentCSubstring& token = mTokenizer.nextToken(); + + HandleToken(token); + + if (mError) { + break; + } + + if (!mHandledTokens.IsEmpty()) { + mHandledTokens.AppendLiteral(", "); + } + mHandledTokens.Append('\''); + mHandledTokens.Append(token); + mHandledTokens.Append('\''); + } + + if (!mError && mTokenizer.separatorAfterCurrentToken()) { + HandleTrailingSeparator(); + } + + if (mError) { + QM_WARNING("Origin '%s' failed to parse, handled tokens: %s", mOrigin.get(), + mHandledTokens.get()); + + return (mSchemeType == eChrome || mSchemeType == eAbout) ? ObsoleteOrigin + : InvalidOrigin; + } + + MOZ_ASSERT(mState == eComplete || mState == eHandledTrailingSeparator); + + // For IPv6 URL, it should at least have three groups. + MOZ_ASSERT_IF(mIPGroup > 0, mIPGroup >= 3); + + nsAutoCString spec(mScheme); + + if (mSchemeType == eFile) { + spec.AppendLiteral("://"); + + if (mUniversalFileOrigin) { + MOZ_ASSERT(mPathnameComponents.Length() == 1); + + spec.Append(mPathnameComponents[0]); + } else { + for (uint32_t count = mPathnameComponents.Length(), index = 0; + index < count; index++) { + spec.Append('/'); + spec.Append(mPathnameComponents[index]); + } + } + + aSpec = spec; + + return ValidOrigin; + } + + if (mSchemeType == eAbout) { + if (mMaybeObsolete) { + // The "moz-safe-about+++home" was acciedntally created by a buggy nightly + // and can be safely removed. + return mHost.EqualsLiteral("home") ? ObsoleteOrigin : InvalidOrigin; + } + spec.Append(':'); + } else if (mSchemeType != eChrome) { + spec.AppendLiteral("://"); + } + + spec.Append(mHost); + + if (!mPort.IsNull()) { + spec.Append(':'); + spec.AppendInt(mPort.Value()); + } + + aSpec = spec; + + return mScheme.EqualsLiteral("app") ? ObsoleteOrigin : ValidOrigin; +} + +void OriginParser::HandleScheme(const nsDependentCSubstring& aToken) { + MOZ_ASSERT(!aToken.IsEmpty()); + MOZ_ASSERT(mState == eExpectingAppIdOrScheme || mState == eExpectingScheme); + + bool isAbout = false; + bool isMozSafeAbout = false; + bool isFile = false; + bool isChrome = false; + if (aToken.EqualsLiteral("http") || aToken.EqualsLiteral("https") || + (isAbout = aToken.EqualsLiteral("about") || + (isMozSafeAbout = aToken.EqualsLiteral("moz-safe-about"))) || + aToken.EqualsLiteral("indexeddb") || + (isFile = aToken.EqualsLiteral("file")) || aToken.EqualsLiteral("app") || + aToken.EqualsLiteral("resource") || + aToken.EqualsLiteral("moz-extension") || + (isChrome = aToken.EqualsLiteral(kChromeOrigin))) { + mScheme = aToken; + + if (isAbout) { + mSchemeType = eAbout; + mState = isMozSafeAbout ? eExpectingEmptyToken1OrHost : eExpectingHost; + } else if (isChrome) { + mSchemeType = eChrome; + if (mTokenizer.hasMoreTokens()) { + mError = true; + } + mState = eComplete; + } else { + if (isFile) { + mSchemeType = eFile; + } + mState = eExpectingEmptyToken1; + } + + return; + } + + QM_WARNING("'%s' is not a valid scheme!", nsCString(aToken).get()); + + mError = true; +} + +void OriginParser::HandlePathnameComponent( + const nsDependentCSubstring& aToken) { + MOZ_ASSERT(!aToken.IsEmpty()); + MOZ_ASSERT(mState == eExpectingEmptyTokenOrDriveLetterOrPathnameComponent || + mState == eExpectingEmptyTokenOrPathnameComponent); + MOZ_ASSERT(mSchemeType == eFile); + + mPathnameComponents.AppendElement(aToken); + + mState = mTokenizer.hasMoreTokens() ? eExpectingEmptyTokenOrPathnameComponent + : eComplete; +} + +void OriginParser::HandleToken(const nsDependentCSubstring& aToken) { + switch (mState) { + case eExpectingAppIdOrScheme: { + if (aToken.IsEmpty()) { + QM_WARNING("Expected an app id or scheme (not an empty string)!"); + + mError = true; + return; + } + + if (IsAsciiDigit(aToken.First())) { + // nsDependentCSubstring doesn't provice ToInteger() + nsCString token(aToken); + + nsresult rv; + Unused << token.ToInteger(&rv); + if (NS_SUCCEEDED(rv)) { + mState = eExpectingInMozBrowser; + return; + } + } + + HandleScheme(aToken); + + return; + } + + case eExpectingInMozBrowser: { + if (aToken.Length() != 1) { + QM_WARNING("'%zu' is not a valid length for the inMozBrowser flag!", + aToken.Length()); + + mError = true; + return; + } + + if (aToken.First() == 't') { + mInIsolatedMozBrowser = true; + } else if (aToken.First() == 'f') { + mInIsolatedMozBrowser = false; + } else { + QM_WARNING("'%s' is not a valid value for the inMozBrowser flag!", + nsCString(aToken).get()); + + mError = true; + return; + } + + mState = eExpectingScheme; + + return; + } + + case eExpectingScheme: { + if (aToken.IsEmpty()) { + QM_WARNING("Expected a scheme (not an empty string)!"); + + mError = true; + return; + } + + HandleScheme(aToken); + + return; + } + + case eExpectingEmptyToken1: { + if (!aToken.IsEmpty()) { + QM_WARNING("Expected the first empty token!"); + + mError = true; + return; + } + + mState = eExpectingEmptyToken2; + + return; + } + + case eExpectingEmptyToken2: { + if (!aToken.IsEmpty()) { + QM_WARNING("Expected the second empty token!"); + + mError = true; + return; + } + + if (mSchemeType == eFile) { + mState = eExpectingEmptyTokenOrUniversalFileOrigin; + } else { + if (mSchemeType == eAbout) { + mMaybeObsolete = true; + } + mState = eExpectingHost; + } + + return; + } + + case eExpectingEmptyTokenOrUniversalFileOrigin: { + MOZ_ASSERT(mSchemeType == eFile); + + if (aToken.IsEmpty()) { + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrDriveLetterOrPathnameComponent + : eComplete; + + return; + } + + if (aToken.EqualsLiteral("UNIVERSAL_FILE_URI_ORIGIN")) { + mUniversalFileOrigin = true; + + mPathnameComponents.AppendElement(aToken); + + mState = eComplete; + + return; + } + + QM_WARNING( + "Expected the third empty token or " + "UNIVERSAL_FILE_URI_ORIGIN!"); + + mError = true; + return; + } + + case eExpectingHost: { + if (aToken.IsEmpty()) { + QM_WARNING("Expected a host (not an empty string)!"); + + mError = true; + return; + } + + mHost = aToken; + + if (aToken.First() == '[') { + MOZ_ASSERT(mIPGroup == 0); + + ++mIPGroup; + mState = eExpectingIPV6Token; + + MOZ_ASSERT(mTokenizer.hasMoreTokens()); + return; + } + + if (mTokenizer.hasMoreTokens()) { + if (mSchemeType == eAbout) { + QM_WARNING("Expected an empty string after host!"); + + mError = true; + return; + } + + mState = eExpectingPort; + + return; + } + + mState = eComplete; + + return; + } + + case eExpectingPort: { + MOZ_ASSERT(mSchemeType == eNone); + + if (aToken.IsEmpty()) { + QM_WARNING("Expected a port (not an empty string)!"); + + mError = true; + return; + } + + // nsDependentCSubstring doesn't provice ToInteger() + nsCString token(aToken); + + nsresult rv; + uint32_t port = token.ToInteger(&rv); + if (NS_SUCCEEDED(rv)) { + mPort.SetValue() = port; + } else { + QM_WARNING("'%s' is not a valid port number!", token.get()); + + mError = true; + return; + } + + mState = eComplete; + + return; + } + + case eExpectingEmptyTokenOrDriveLetterOrPathnameComponent: { + MOZ_ASSERT(mSchemeType == eFile); + + if (aToken.IsEmpty()) { + mPathnameComponents.AppendElement(""_ns); + + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrPathnameComponent + : eComplete; + + return; + } + + if (aToken.Length() == 1 && IsAsciiAlpha(aToken.First())) { + mMaybeDriveLetter = true; + + mPathnameComponents.AppendElement(aToken); + + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrPathnameComponent + : eComplete; + + return; + } + + HandlePathnameComponent(aToken); + + return; + } + + case eExpectingEmptyTokenOrPathnameComponent: { + MOZ_ASSERT(mSchemeType == eFile); + + if (aToken.IsEmpty()) { + if (mMaybeDriveLetter) { + MOZ_ASSERT(mPathnameComponents.Length() == 1); + + nsCString& pathnameComponent = mPathnameComponents[0]; + pathnameComponent.Append(':'); + + mMaybeDriveLetter = false; + } else { + mPathnameComponents.AppendElement(""_ns); + } + + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrPathnameComponent + : eComplete; + + return; + } + + HandlePathnameComponent(aToken); + + return; + } + + case eExpectingEmptyToken1OrHost: { + MOZ_ASSERT(mSchemeType == eAbout && + mScheme.EqualsLiteral("moz-safe-about")); + + if (aToken.IsEmpty()) { + mState = eExpectingEmptyToken2; + } else { + mHost = aToken; + mState = mTokenizer.hasMoreTokens() ? eExpectingPort : eComplete; + } + + return; + } + + case eExpectingIPV6Token: { + // A safe check for preventing infinity recursion. + if (++mIPGroup > 8) { + mError = true; + return; + } + + mHost.AppendLiteral(":"); + mHost.Append(aToken); + if (!aToken.IsEmpty() && aToken.Last() == ']') { + mState = mTokenizer.hasMoreTokens() ? eExpectingPort : eComplete; + } + + return; + } + + default: + MOZ_CRASH("Should never get here!"); + } +} + +void OriginParser::HandleTrailingSeparator() { + MOZ_ASSERT(mState == eComplete); + MOZ_ASSERT(mSchemeType == eFile); + + mPathnameComponents.AppendElement(""_ns); + + mState = eHandledTrailingSeparator; +} + +nsresult RepositoryOperationBase::ProcessRepository() { + AssertIsOnIOThread(); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(mDirectory, Exists), + QM_ASSERT_UNREACHABLE); + MOZ_ASSERT(exists); + } +#endif + + QM_TRY(CollectEachFileEntry( + *mDirectory, + [](const auto& originFile) -> Result<mozilla::Ok, nsresult> { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoString, originFile, GetLeafName)); + + // Unknown files during upgrade are allowed. Just warn if we find + // them. + if (!IsOSMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + + return mozilla::Ok{}; + }, + [&self = *this](const auto& originDir) -> Result<mozilla::Ok, nsresult> { + OriginProps originProps(WrapMovingNotNullUnchecked(originDir)); + QM_TRY(MOZ_TO_RESULT(originProps.Init([&self](const auto& aSpec) { + return self.PersistenceTypeFromSpec(aSpec); + }))); + // Bypass invalid origins while upgrading + QM_TRY(OkIf(originProps.mType != OriginProps::eInvalid), mozilla::Ok{}); + + if (originProps.mType != OriginProps::eObsolete) { + QM_TRY_INSPECT(const bool& removed, + MOZ_TO_RESULT_INVOKE_MEMBER( + self, PrepareOriginDirectory, originProps)); + if (removed) { + return mozilla::Ok{}; + } + } + + self.mOriginProps.AppendElement(std::move(originProps)); + + return mozilla::Ok{}; + })); + + if (mOriginProps.IsEmpty()) { + return NS_OK; + } + + QM_TRY(MOZ_TO_RESULT(ProcessOriginDirectories())); + + return NS_OK; +} + +template <typename UpgradeMethod> +nsresult RepositoryOperationBase::MaybeUpgradeClients( + const OriginProps& aOriginProps, UpgradeMethod aMethod) { + AssertIsOnIOThread(); + MOZ_ASSERT(aMethod); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY(CollectEachFileEntry( + *aOriginProps.mDirectory, + [](const auto& file) -> Result<mozilla::Ok, nsresult> { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, GetLeafName)); + + if (!IsOriginMetadata(leafName) && !IsTempMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + + return mozilla::Ok{}; + }, + [quotaManager, &aMethod, + &self = *this](const auto& dir) -> Result<mozilla::Ok, nsresult> { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, dir, GetLeafName)); + + QM_TRY_INSPECT(const bool& removed, + MOZ_TO_RESULT_INVOKE_MEMBER(self, PrepareClientDirectory, + dir, leafName)); + if (removed) { + return mozilla::Ok{}; + } + + Client::Type clientType; + bool ok = Client::TypeFromText(leafName, clientType, fallible); + if (!ok) { + UNKNOWN_FILE_WARNING(leafName); + return mozilla::Ok{}; + } + + Client* client = quotaManager->GetClient(clientType); + MOZ_ASSERT(client); + + QM_TRY(MOZ_TO_RESULT((client->*aMethod)(dir))); + + return mozilla::Ok{}; + })); + + return NS_OK; +} + +nsresult RepositoryOperationBase::PrepareClientDirectory( + nsIFile* aFile, const nsAString& aLeafName, bool& aRemoved) { + AssertIsOnIOThread(); + + aRemoved = false; + return NS_OK; +} + +nsresult CreateOrUpgradeDirectoryMetadataHelper::Init() { + AssertIsOnIOThread(); + MOZ_ASSERT(mDirectory); + + const auto maybeLegacyPersistenceType = + LegacyPersistenceTypeFromFile(*mDirectory, fallible); + QM_TRY(OkIf(maybeLegacyPersistenceType.isSome()), Err(NS_ERROR_FAILURE)); + + mLegacyPersistenceType.init(maybeLegacyPersistenceType.value()); + + return NS_OK; +} + +Maybe<CreateOrUpgradeDirectoryMetadataHelper::LegacyPersistenceType> +CreateOrUpgradeDirectoryMetadataHelper::LegacyPersistenceTypeFromFile( + nsIFile& aFile, const fallible_t&) { + nsAutoString leafName; + MOZ_ALWAYS_SUCCEEDS(aFile.GetLeafName(leafName)); + + if (leafName.Equals(u"persistent"_ns)) { + return Some(LegacyPersistenceType::Persistent); + } + + if (leafName.Equals(u"temporary"_ns)) { + return Some(LegacyPersistenceType::Temporary); + } + + return Nothing(); +} + +PersistenceType +CreateOrUpgradeDirectoryMetadataHelper::PersistenceTypeFromLegacyPersistentSpec( + const nsCString& aSpec) { + if (QuotaManager::IsOriginInternal(aSpec)) { + return PERSISTENCE_TYPE_PERSISTENT; + } + + return PERSISTENCE_TYPE_DEFAULT; +} + +PersistenceType CreateOrUpgradeDirectoryMetadataHelper::PersistenceTypeFromSpec( + const nsCString& aSpec) { + switch (*mLegacyPersistenceType) { + case LegacyPersistenceType::Persistent: + return PersistenceTypeFromLegacyPersistentSpec(aSpec); + case LegacyPersistenceType::Temporary: + return PERSISTENCE_TYPE_TEMPORARY; + } + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Bad legacy persistence type value!"); +} + +nsresult CreateOrUpgradeDirectoryMetadataHelper::MaybeUpgradeOriginDirectory( + nsIFile* aDirectory) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + + QM_TRY_INSPECT( + const auto& metadataFile, + CloneFileAndAppend(*aDirectory, nsLiteralString(METADATA_FILE_NAME))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(metadataFile, Exists)); + + if (!exists) { + // Directory structure upgrade needed. + // Move all files to IDB specific directory. + + nsString idbDirectoryName; + QM_TRY(OkIf(Client::TypeToText(Client::IDB, idbDirectoryName, fallible)), + NS_ERROR_FAILURE); + + QM_TRY_INSPECT(const auto& idbDirectory, + CloneFileAndAppend(*aDirectory, idbDirectoryName)); + + // Usually we only use QM_OR_ELSE_LOG_VERBOSE/QM_OR_ELSE_LOG_VERBOSE_IF + // with Create and NS_ERROR_FILE_ALREADY_EXISTS check, but typically the + // idb directory shouldn't exist during the upgrade and the upgrade runs + // only once in most of the cases, so the use of QM_OR_ELSE_WARN_IF is ok + // here. + QM_TRY(QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT(idbDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755)), + // Predicate. + IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>, + // Fallback. + ([&idbDirectory](const nsresult rv) -> Result<Ok, nsresult> { + QM_TRY_INSPECT( + const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(idbDirectory, IsDirectory)); + + QM_TRY(OkIf(isDirectory), Err(NS_ERROR_UNEXPECTED)); + + return Ok{}; + }))); + + QM_TRY(CollectEachFile( + *aDirectory, + [&idbDirectory, &idbDirectoryName]( + const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, + GetLeafName)); + + if (!leafName.Equals(idbDirectoryName)) { + QM_TRY(MOZ_TO_RESULT(file->MoveTo(idbDirectory, u""_ns))); + } + + return Ok{}; + })); + + QM_TRY( + MOZ_TO_RESULT(metadataFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644))); + } + + return NS_OK; +} + +nsresult CreateOrUpgradeDirectoryMetadataHelper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aRemoved); + + if (*mLegacyPersistenceType == LegacyPersistenceType::Persistent) { + QM_TRY(MOZ_TO_RESULT( + MaybeUpgradeOriginDirectory(aOriginProps.mDirectory.get()))); + + aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps); + } else { + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + + QM_WARNONLY_TRY_UNWRAP( + const auto maybeDirectoryMetadata, + MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(), + timestamp, group, origin, isApp))); + if (!maybeDirectoryMetadata) { + aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps); + aOriginProps.mNeedsRestore = true; + } else if (!isApp.IsNull()) { + aOriginProps.mIgnore = true; + } + } + + *aRemoved = false; + return NS_OK; +} + +nsresult CreateOrUpgradeDirectoryMetadataHelper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + if (*mLegacyPersistenceType == LegacyPersistenceType::Persistent) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + + // Move internal origins to new persistent storage. + if (PersistenceTypeFromLegacyPersistentSpec(aOriginProps.mSpec) == + PERSISTENCE_TYPE_PERSISTENT) { + if (!mPermanentStorageDir) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + const nsString& permanentStoragePath = + quotaManager->GetStoragePath(PERSISTENCE_TYPE_PERSISTENT); + + QM_TRY_UNWRAP(mPermanentStorageDir, + QM_NewLocalFile(permanentStoragePath)); + } + + const nsAString& leafName = aOriginProps.mLeafName; + + QM_TRY_INSPECT(const auto& newDirectory, + CloneFileAndAppend(*mPermanentStorageDir, leafName)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(newDirectory, Exists)); + + if (exists) { + QM_WARNING("Found %s in storage/persistent and storage/permanent !", + NS_ConvertUTF16toUTF8(leafName).get()); + + QM_TRY(MOZ_TO_RESULT( + aOriginProps.mDirectory->Remove(/* recursive */ true))); + } else { + QM_TRY(MOZ_TO_RESULT( + aOriginProps.mDirectory->MoveTo(mPermanentStorageDir, u""_ns))); + } + } + } else if (aOriginProps.mNeedsRestore) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } else if (!aOriginProps.mIgnore) { + QM_TRY_INSPECT(const auto& file, + CloneFileAndAppend(*aOriginProps.mDirectory, + nsLiteralString(METADATA_FILE_NAME))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, FileFlag::Append)); + + MOZ_ASSERT(stream); + + // Currently unused (used to be isApp). + QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(false))); + } + + return NS_OK; +} + +nsresult UpgradeStorageHelperBase::Init() { + AssertIsOnIOThread(); + MOZ_ASSERT(mDirectory); + + const auto maybePersistenceType = + PersistenceTypeFromFile(*mDirectory, fallible); + QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE)); + + mPersistenceType.init(maybePersistenceType.value()); + + return NS_OK; +} + +PersistenceType UpgradeStorageHelperBase::PersistenceTypeFromSpec( + const nsCString& aSpec) { + // There's no moving of origin directories between repositories like in the + // CreateOrUpgradeDirectoryMetadataHelper + return *mPersistenceType; +} + +nsresult UpgradeStorageFrom0_0To1_0Helper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aRemoved); + + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + + QM_WARNONLY_TRY_UNWRAP( + const auto maybeDirectoryMetadata, + MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(), + timestamp, group, origin, isApp))); + if (!maybeDirectoryMetadata || isApp.IsNull()) { + aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps); + aOriginProps.mNeedsRestore = true; + } else { + aOriginProps.mTimestamp = timestamp; + } + + *aRemoved = false; + return NS_OK; +} + +nsresult UpgradeStorageFrom0_0To1_0Helper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + // This handles changes in origin string generation from nsIPrincipal, + // especially the change from: appId+inMozBrowser+originNoSuffix + // to: origin (with origin suffix). + QM_TRY_INSPECT(const bool& renamed, MaybeRenameOrigin(aOriginProps)); + if (renamed) { + return NS_OK; + } + + if (aOriginProps.mNeedsRestore) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } + + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + + return NS_OK; +} + +nsresult UpgradeStorageFrom1_0To2_0Helper::MaybeRemoveMorgueDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + // The Cache API was creating top level morgue directories by accident for + // a short time in nightly. This unfortunately prevents all storage from + // working. So recover these profiles permanently by removing these corrupt + // directories as part of this upgrade. + + QM_TRY_INSPECT(const auto& morgueDir, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIFile>, *aOriginProps.mDirectory, Clone)); + + QM_TRY(MOZ_TO_RESULT(morgueDir->Append(u"morgue"_ns))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(morgueDir, Exists)); + + if (exists) { + QM_WARNING("Deleting accidental morgue directory!"); + + QM_TRY(MOZ_TO_RESULT(morgueDir->Remove(/* recursive */ true))); + } + + return NS_OK; +} + +Result<bool, nsresult> UpgradeStorageFrom1_0To2_0Helper::MaybeRemoveAppsData( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + // TODO: This method was empty for some time due to accidental changes done + // in bug 1320404. This led to renaming of origin directories like: + // https+++developer.cdn.mozilla.net^appId=1007&inBrowser=1 + // to: + // https+++developer.cdn.mozilla.net^inBrowser=1 + // instead of just removing them. + + const nsCString& originalSuffix = aOriginProps.mOriginalSuffix; + if (!originalSuffix.IsEmpty()) { + MOZ_ASSERT(originalSuffix[0] == '^'); + + if (!URLParams::Parse( + Substring(originalSuffix, 1, originalSuffix.Length() - 1), + [](const nsAString& aName, const nsAString& aValue) { + if (aName.EqualsLiteral("appId")) { + return false; + } + + return true; + })) { + QM_TRY(MOZ_TO_RESULT(RemoveObsoleteOrigin(aOriginProps))); + + return true; + } + } + + return false; +} + +nsresult UpgradeStorageFrom1_0To2_0Helper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aRemoved); + + QM_TRY(MOZ_TO_RESULT(MaybeRemoveMorgueDirectory(aOriginProps))); + + QM_TRY(MOZ_TO_RESULT( + MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom1_0To2_0))); + + QM_TRY_INSPECT(const bool& removed, MaybeRemoveAppsData(aOriginProps)); + if (removed) { + *aRemoved = true; + return NS_OK; + } + + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + QM_WARNONLY_TRY_UNWRAP( + const auto maybeDirectoryMetadata, + MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(), + timestamp, group, origin, isApp))); + if (!maybeDirectoryMetadata || isApp.IsNull()) { + aOriginProps.mNeedsRestore = true; + } + + nsCString suffix; + QM_WARNONLY_TRY_UNWRAP(const auto maybeDirectoryMetadata2, + MOZ_TO_RESULT(GetDirectoryMetadata2( + aOriginProps.mDirectory.get(), timestamp, suffix, + group, origin, isApp.SetValue()))); + if (!maybeDirectoryMetadata2) { + aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps); + aOriginProps.mNeedsRestore2 = true; + } else { + aOriginProps.mTimestamp = timestamp; + } + + *aRemoved = false; + return NS_OK; +} + +nsresult UpgradeStorageFrom1_0To2_0Helper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + // This handles changes in origin string generation from nsIPrincipal, + // especially the stripping of obsolete origin attributes like addonId. + QM_TRY_INSPECT(const bool& renamed, MaybeRenameOrigin(aOriginProps)); + if (renamed) { + return NS_OK; + } + + if (aOriginProps.mNeedsRestore) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } + + if (aOriginProps.mNeedsRestore2) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + } + + return NS_OK; +} + +nsresult UpgradeStorageFrom2_0To2_1Helper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aRemoved); + + QM_TRY(MOZ_TO_RESULT( + MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom2_0To2_1))); + + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + QM_WARNONLY_TRY_UNWRAP( + const auto maybeDirectoryMetadata, + MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(), + timestamp, group, origin, isApp))); + if (!maybeDirectoryMetadata || isApp.IsNull()) { + aOriginProps.mNeedsRestore = true; + } + + nsCString suffix; + QM_WARNONLY_TRY_UNWRAP(const auto maybeDirectoryMetadata2, + MOZ_TO_RESULT(GetDirectoryMetadata2( + aOriginProps.mDirectory.get(), timestamp, suffix, + group, origin, isApp.SetValue()))); + if (!maybeDirectoryMetadata2) { + aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps); + aOriginProps.mNeedsRestore2 = true; + } else { + aOriginProps.mTimestamp = timestamp; + } + + *aRemoved = false; + return NS_OK; +} + +nsresult UpgradeStorageFrom2_0To2_1Helper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + if (aOriginProps.mNeedsRestore) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } + + if (aOriginProps.mNeedsRestore2) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + } + + return NS_OK; +} + +nsresult UpgradeStorageFrom2_1To2_2Helper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aRemoved); + + QM_TRY(MOZ_TO_RESULT( + MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom2_1To2_2))); + + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + QM_WARNONLY_TRY_UNWRAP( + const auto maybeDirectoryMetadata, + MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(), + timestamp, group, origin, isApp))); + if (!maybeDirectoryMetadata || isApp.IsNull()) { + aOriginProps.mNeedsRestore = true; + } + + nsCString suffix; + QM_WARNONLY_TRY_UNWRAP(const auto maybeDirectoryMetadata2, + MOZ_TO_RESULT(GetDirectoryMetadata2( + aOriginProps.mDirectory.get(), timestamp, suffix, + group, origin, isApp.SetValue()))); + if (!maybeDirectoryMetadata2) { + aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps); + aOriginProps.mNeedsRestore2 = true; + } else { + aOriginProps.mTimestamp = timestamp; + } + + *aRemoved = false; + return NS_OK; +} + +nsresult UpgradeStorageFrom2_1To2_2Helper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + if (aOriginProps.mNeedsRestore) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } + + if (aOriginProps.mNeedsRestore2) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + } + + return NS_OK; +} + +nsresult UpgradeStorageFrom2_1To2_2Helper::PrepareClientDirectory( + nsIFile* aFile, const nsAString& aLeafName, bool& aRemoved) { + AssertIsOnIOThread(); + + if (Client::IsDeprecatedClient(aLeafName)) { + QM_WARNING("Deleting deprecated %s client!", + NS_ConvertUTF16toUTF8(aLeafName).get()); + + QM_TRY(MOZ_TO_RESULT(aFile->Remove(true))); + + aRemoved = true; + } else { + aRemoved = false; + } + + return NS_OK; +} + +nsresult RestoreDirectoryMetadata2Helper::Init() { + AssertIsOnIOThread(); + MOZ_ASSERT(mDirectory); + + nsCOMPtr<nsIFile> parentDir; + QM_TRY(MOZ_TO_RESULT(mDirectory->GetParent(getter_AddRefs(parentDir)))); + + const auto maybePersistenceType = + PersistenceTypeFromFile(*parentDir, fallible); + QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE)); + + mPersistenceType.init(maybePersistenceType.value()); + + return NS_OK; +} + +nsresult RestoreDirectoryMetadata2Helper::RestoreMetadata2File() { + OriginProps originProps(WrapMovingNotNull(mDirectory)); + QM_TRY(MOZ_TO_RESULT(originProps.Init( + [&self = *this](const auto& aSpec) { return *self.mPersistenceType; }))); + + QM_TRY(OkIf(originProps.mType != OriginProps::eInvalid), NS_ERROR_FAILURE); + + originProps.mTimestamp = GetOriginLastModifiedTime(originProps); + + mOriginProps.AppendElement(std::move(originProps)); + + QM_TRY(MOZ_TO_RESULT(ProcessOriginDirectories())); + + return NS_OK; +} + +nsresult RestoreDirectoryMetadata2Helper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + // We don't have any approach to restore aPersisted, so reset it to false. + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + + return NS_OK; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/ActorsParent.h b/dom/quota/ActorsParent.h new file mode 100644 index 0000000000..4f8fcebd3e --- /dev/null +++ b/dom/quota/ActorsParent.h @@ -0,0 +1,26 @@ +/* -*- 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_quota_ActorsParent_h +#define mozilla_dom_quota_ActorsParent_h + +#include "mozilla/dom/quota/Config.h" + +namespace mozilla::dom::quota { + +class PQuotaParent; + +void InitializeQuotaManager(); + +PQuotaParent* AllocPQuotaParent(); + +bool DeallocPQuotaParent(PQuotaParent* aActor); + +bool RecvShutdownQuotaManager(); + +} // namespace mozilla::dom::quota + +#endif // mozilla_dom_quota_ActorsParent_h diff --git a/dom/quota/Assertions.cpp b/dom/quota/Assertions.cpp new file mode 100644 index 0000000000..a2dd155743 --- /dev/null +++ b/dom/quota/Assertions.cpp @@ -0,0 +1,36 @@ +/* -*- 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 "Assertions.h" + +namespace mozilla::dom::quota { + +bool IsOnIOThread() { + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Must have a manager here!"); + + bool currentThread; + return NS_SUCCEEDED( + quotaManager->IOThread()->IsOnCurrentThread(¤tThread)) && + currentThread; +} + +void AssertIsOnIOThread() { + NS_ASSERTION(IsOnIOThread(), "Running on the wrong thread!"); +} + +void DiagnosticAssertIsOnIOThread() { MOZ_DIAGNOSTIC_ASSERT(IsOnIOThread()); } + +void AssertCurrentThreadOwnsQuotaMutex() { +#ifdef DEBUG + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Must have a manager here!"); + + quotaManager->AssertCurrentThreadOwnsQuotaMutex(); +#endif +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/Assertions.h b/dom/quota/Assertions.h new file mode 100644 index 0000000000..d2eacb0c1d --- /dev/null +++ b/dom/quota/Assertions.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_QUOTA_ASSERTIONS_H_ +#define DOM_QUOTA_ASSERTIONS_H_ + +#include <cstdint> + +namespace mozilla::dom::quota { + +template <typename T> +void AssertNoOverflow(uint64_t aDest, T aArg); + +template <typename T, typename U> +void AssertNoUnderflow(T aDest, U aArg); + +bool IsOnIOThread(); + +void AssertIsOnIOThread(); + +void DiagnosticAssertIsOnIOThread(); + +void AssertCurrentThreadOwnsQuotaMutex(); + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_ASSERTIONS_H_ diff --git a/dom/quota/AssertionsImpl.h b/dom/quota/AssertionsImpl.h new file mode 100644 index 0000000000..77df103311 --- /dev/null +++ b/dom/quota/AssertionsImpl.h @@ -0,0 +1,52 @@ +/* -*- 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_QUOTA_ASSERTIONSIMPL_H_ +#define DOM_QUOTA_ASSERTIONSIMPL_H_ + +#include "mozilla/dom/quota/Assertions.h" + +#include <type_traits> +#include "mozilla/Assertions.h" + +namespace mozilla::dom::quota { + +namespace detail { + +template <typename T, bool = std::is_unsigned_v<T>> +struct IntChecker { + static void Assert(T aInt) { + static_assert(std::is_integral_v<T>, "Not an integer!"); + MOZ_ASSERT(aInt >= 0); + } +}; + +template <typename T> +struct IntChecker<T, true> { + static void Assert(T aInt) { + static_assert(std::is_integral_v<T>, "Not an integer!"); + } +}; + +} // namespace detail + +template <typename T> +void AssertNoOverflow(uint64_t aDest, T aArg) { + detail::IntChecker<T>::Assert(aDest); + detail::IntChecker<T>::Assert(aArg); + MOZ_ASSERT(UINT64_MAX - aDest >= uint64_t(aArg)); +} + +template <typename T, typename U> +void AssertNoUnderflow(T aDest, U aArg) { + detail::IntChecker<T>::Assert(aDest); + detail::IntChecker<T>::Assert(aArg); + MOZ_ASSERT(uint64_t(aDest) >= uint64_t(aArg)); +} + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_ASSERTIONSIMPL_H_ diff --git a/dom/quota/CachingDatabaseConnection.cpp b/dom/quota/CachingDatabaseConnection.cpp new file mode 100644 index 0000000000..cd07dba4e7 --- /dev/null +++ b/dom/quota/CachingDatabaseConnection.cpp @@ -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/. */ + +#include "mozilla/dom/quota/CachingDatabaseConnection.h" + +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ipc/BackgroundParent.h" + +namespace mozilla::dom::quota { + +CachingDatabaseConnection::CachingDatabaseConnection( + MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection) + : +#ifdef MOZ_THREAD_SAFETY_OWNERSHIP_CHECKS_SUPPORTED + mOwningThread{nsAutoOwningThread{}}, +#endif + mStorageConnection(std::move(aStorageConnection)) { +} + +void CachingDatabaseConnection::LazyInit( + MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection) { +#ifdef MOZ_THREAD_SAFETY_OWNERSHIP_CHECKS_SUPPORTED + mOwningThread.init(); +#endif + mStorageConnection.init(std::move(aStorageConnection)); +} + +Result<CachingDatabaseConnection::CachedStatement, nsresult> +CachingDatabaseConnection::GetCachedStatement(const nsACString& aQuery) { + AssertIsOnConnectionThread(); + MOZ_ASSERT(!aQuery.IsEmpty()); + MOZ_ASSERT(mStorageConnection); + + AUTO_PROFILER_LABEL("CachingDatabaseConnection::GetCachedStatement", DOM); + + QM_TRY_UNWRAP( + auto stmt, + mCachedStatements.TryLookupOrInsertWith( + aQuery, [&]() -> Result<nsCOMPtr<mozIStorageStatement>, nsresult> { + const auto extraInfo = + ScopedLogExtraInfo{ScopedLogExtraInfo::kTagQuery, aQuery}; + + QM_TRY_RETURN( + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, **mStorageConnection, + CreateStatement, aQuery), + QM_PROPAGATE, + ([&aQuery, + &storageConnection = **mStorageConnection](const auto&) { +#ifdef DEBUG + nsCString msg; + MOZ_ALWAYS_SUCCEEDS( + storageConnection.GetLastErrorString(msg)); + + nsAutoCString error = + "The statement '"_ns + aQuery + + "' failed to compile with the error message '"_ns + msg + + "'."_ns; + + NS_WARNING(error.get()); +#else + (void)aQuery; +#endif + })); + })); + + return CachedStatement{this, std::move(stmt), aQuery}; +} + +Result<CachingDatabaseConnection::BorrowedStatement, nsresult> +CachingDatabaseConnection::BorrowCachedStatement(const nsACString& aQuery) { + QM_TRY_UNWRAP(auto cachedStatement, GetCachedStatement(aQuery)); + + return cachedStatement.Borrow(); +} + +nsresult CachingDatabaseConnection::ExecuteCachedStatement( + const nsACString& aQuery) { + return ExecuteCachedStatement( + aQuery, [](auto&) -> Result<Ok, nsresult> { return Ok{}; }); +} + +void CachingDatabaseConnection::Close() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(HasStorageConnection()); + + AUTO_PROFILER_LABEL("CachingDatabaseConnection::Close", DOM); + + mCachedStatements.Clear(); + + MOZ_ALWAYS_SUCCEEDS((*mStorageConnection)->Close()); + mStorageConnection.destroy(); +} + +#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING) +CachingDatabaseConnection::CachedStatement::CachedStatement() +# ifdef DEBUG + : mDEBUGConnection(nullptr) +# endif +{ + AssertIsOnConnectionThread(); + + MOZ_COUNT_CTOR(CachingDatabaseConnection::CachedStatement); +} + +CachingDatabaseConnection::CachedStatement::~CachedStatement() { + AssertIsOnConnectionThread(); + + MOZ_COUNT_DTOR(CachingDatabaseConnection::CachedStatement); +} +#endif + +CachingDatabaseConnection::CachedStatement::operator bool() const { + AssertIsOnConnectionThread(); + + return mStatement; +} + +mozIStorageStatement& CachingDatabaseConnection::BorrowedStatement::operator*() + const { + return *operator->(); +} + +mozIStorageStatement* CachingDatabaseConnection::BorrowedStatement::operator->() + const { + MOZ_ASSERT(mStatement); + + return mStatement; +} + +CachingDatabaseConnection::BorrowedStatement +CachingDatabaseConnection::CachedStatement::Borrow() const { + AssertIsOnConnectionThread(); + +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + return BorrowedStatement{WrapNotNull(mStatement), mQuery}; +#else + return BorrowedStatement{WrapNotNull(mStatement)}; +#endif +} + +CachingDatabaseConnection::CachedStatement::CachedStatement( + CachingDatabaseConnection* aConnection, + nsCOMPtr<mozIStorageStatement> aStatement, const nsACString& aQuery) + : mStatement(std::move(aStatement)) +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + , + mQuery(aQuery) +#endif +#if defined(DEBUG) + , + mDEBUGConnection(aConnection) +#endif +{ +#ifdef DEBUG + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); +#endif + MOZ_ASSERT(mStatement); + AssertIsOnConnectionThread(); + + MOZ_COUNT_CTOR(CachingDatabaseConnection::CachedStatement); +} + +void CachingDatabaseConnection::CachedStatement::AssertIsOnConnectionThread() + const { +#ifdef DEBUG + if (mDEBUGConnection) { + mDEBUGConnection->AssertIsOnConnectionThread(); + } + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!mozilla::ipc::IsOnBackgroundThread()); +#endif +} + +Result<CachingDatabaseConnection::BorrowedStatement, nsresult> +CachingDatabaseConnection::LazyStatement::Borrow() { + if (!mCachedStatement) { + QM_TRY(Initialize()); + } + + return mCachedStatement.Borrow(); +} + +Result<Ok, nsresult> CachingDatabaseConnection::LazyStatement::Initialize() { + QM_TRY_UNWRAP(mCachedStatement, mConnection.GetCachedStatement(mQueryString)); + return Ok{}; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/CachingDatabaseConnection.h b/dom/quota/CachingDatabaseConnection.h new file mode 100644 index 0000000000..355baf5293 --- /dev/null +++ b/dom/quota/CachingDatabaseConnection.h @@ -0,0 +1,238 @@ +/* -*- 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_QUOTA_CACHINGDATABASECONNECTION_H_ +#define DOM_QUOTA_CACHINGDATABASECONNECTION_H_ + +#include "mozilla/dom/quota/Config.h" + +#include "mozStorageHelper.h" +#include "nsCOMPtr.h" +#include "nscore.h" +#include "nsHashKeys.h" +#include "nsInterfaceHashtable.h" +#include "nsString.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/InitializedOnce.h" +#include "mozilla/NotNull.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/ScopedLogExtraInfo.h" + +namespace mozilla::dom::quota { + +class CachingDatabaseConnection { + public: + class CachedStatement; + + // A stack-only RAII wrapper that resets its borrowed statement when the + // wrapper goes out of scope. Note it's intentionally not declared MOZ_RAII, + // because it actually is used as a temporary in simple cases like + // `stmt.Borrow()->Execute()`. It also automatically exposes the current query + // to ScopedLogExtraInfo as "query" in builds where this mechanism is active. + class MOZ_STACK_CLASS BorrowedStatement : mozStorageStatementScoper { + public: + mozIStorageStatement& operator*() const; + + MOZ_NONNULL_RETURN mozIStorageStatement* operator->() const + MOZ_NO_ADDREF_RELEASE_ON_RETURN; + + BorrowedStatement(BorrowedStatement&& aOther) = default; + + // No funny business allowed. + BorrowedStatement& operator=(BorrowedStatement&&) = delete; + BorrowedStatement(const BorrowedStatement&) = delete; + BorrowedStatement& operator=(const BorrowedStatement&) = delete; + + private: + friend class CachedStatement; + +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + BorrowedStatement(NotNull<mozIStorageStatement*> aStatement, + const nsACString& aQuery) + : mozStorageStatementScoper(aStatement), + mExtraInfo{ScopedLogExtraInfo::kTagQuery, aQuery} {} + + ScopedLogExtraInfo mExtraInfo; +#else + MOZ_IMPLICIT BorrowedStatement(NotNull<mozIStorageStatement*> aStatement) + : mozStorageStatementScoper(aStatement) {} +#endif + }; + + class LazyStatement; + + void AssertIsOnConnectionThread() const { +#ifdef MOZ_THREAD_SAFETY_OWNERSHIP_CHECKS_SUPPORTED + mOwningThread->AssertOwnership("CachingDatabaseConnection not thread-safe"); +#endif + } + + bool HasStorageConnection() const { + return static_cast<bool>(mStorageConnection); + } + + mozIStorageConnection& MutableStorageConnection() const { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + + return **mStorageConnection; + } + + Result<CachedStatement, nsresult> GetCachedStatement( + const nsACString& aQuery); + + Result<BorrowedStatement, nsresult> BorrowCachedStatement( + const nsACString& aQuery); + + template <typename BindFunctor> + nsresult ExecuteCachedStatement(const nsACString& aQuery, + BindFunctor&& aBindFunctor) { + QM_TRY_INSPECT(const auto& stmt, BorrowCachedStatement(aQuery)); + QM_TRY(std::forward<BindFunctor>(aBindFunctor)(*stmt)); + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + + return NS_OK; + } + + nsresult ExecuteCachedStatement(const nsACString& aQuery); + + template <typename BindFunctor> + Result<Maybe<BorrowedStatement>, nsresult> + BorrowAndExecuteSingleStepStatement(const nsACString& aQuery, + BindFunctor&& aBindFunctor); + +#ifdef DEBUG + ~CachingDatabaseConnection() { + MOZ_ASSERT(!mStorageConnection); + MOZ_ASSERT(!mCachedStatements.Count()); + } +#endif + + protected: + explicit CachingDatabaseConnection( + MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection); + + CachingDatabaseConnection() = default; + + void LazyInit( + MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection); + + void Close(); + + private: +#ifdef MOZ_THREAD_SAFETY_OWNERSHIP_CHECKS_SUPPORTED + LazyInitializedOnce<const nsAutoOwningThread> mOwningThread; +#endif + + LazyInitializedOnceEarlyDestructible< + const NotNull<nsCOMPtr<mozIStorageConnection>>> + mStorageConnection; + nsInterfaceHashtable<nsCStringHashKey, mozIStorageStatement> + mCachedStatements; +}; + +class CachingDatabaseConnection::CachedStatement final { + friend class CachingDatabaseConnection; + + nsCOMPtr<mozIStorageStatement> mStatement; + +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + nsCString mQuery; +#endif + +#ifdef DEBUG + CachingDatabaseConnection* mDEBUGConnection; +#endif + + public: +#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING) + CachedStatement(); + ~CachedStatement(); +#else + CachedStatement() = default; +#endif + + void AssertIsOnConnectionThread() const; + + explicit operator bool() const; + + BorrowedStatement Borrow() const; + + private: + // Only called by CachingDatabaseConnection. + CachedStatement(CachingDatabaseConnection* aConnection, + nsCOMPtr<mozIStorageStatement> aStatement, + const nsACString& aQuery); + + public: +#if defined(NS_BUILD_REFCNT_LOGGING) + CachedStatement(CachedStatement&& aOther) + : mStatement(std::move(aOther.mStatement)) +# ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + , + mQuery(std::move(aOther.mQuery)) +# endif +# ifdef DEBUG + , + mDEBUGConnection(aOther.mDEBUGConnection) +# endif + { + MOZ_COUNT_CTOR(CachingDatabaseConnection::CachedStatement); + } +#else + CachedStatement(CachedStatement&&) = default; +#endif + + CachedStatement& operator=(CachedStatement&&) = default; + + // No funny business allowed. + CachedStatement(const CachedStatement&) = delete; + CachedStatement& operator=(const CachedStatement&) = delete; +}; + +class CachingDatabaseConnection::LazyStatement final { + public: + LazyStatement(CachingDatabaseConnection& aConnection, + const nsACString& aQueryString) + : mConnection{aConnection}, mQueryString{aQueryString} {} + + Result<CachingDatabaseConnection::BorrowedStatement, nsresult> Borrow(); + + template <typename BindFunctor> + Result<Maybe<CachingDatabaseConnection::BorrowedStatement>, nsresult> + BorrowAndExecuteSingleStep(BindFunctor&& aBindFunctor) { + QM_TRY_UNWRAP(auto borrowedStatement, Borrow()); + + QM_TRY(std::forward<BindFunctor>(aBindFunctor)(*borrowedStatement)); + + QM_TRY_INSPECT( + const bool& hasResult, + MOZ_TO_RESULT_INVOKE_MEMBER(&*borrowedStatement, ExecuteStep)); + + return hasResult ? Some(std::move(borrowedStatement)) : Nothing{}; + } + + private: + Result<Ok, nsresult> Initialize(); + + CachingDatabaseConnection& mConnection; + const nsCString mQueryString; + CachingDatabaseConnection::CachedStatement mCachedStatement; +}; + +template <typename BindFunctor> +Result<Maybe<CachingDatabaseConnection::BorrowedStatement>, nsresult> +CachingDatabaseConnection::BorrowAndExecuteSingleStepStatement( + const nsACString& aQuery, BindFunctor&& aBindFunctor) { + return LazyStatement{*this, aQuery}.BorrowAndExecuteSingleStep( + std::forward<BindFunctor>(aBindFunctor)); +} + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_CACHINGDATABASECONNECTION_H_ diff --git a/dom/quota/CanonicalQuotaObject.cpp b/dom/quota/CanonicalQuotaObject.cpp new file mode 100644 index 0000000000..388f20e541 --- /dev/null +++ b/dom/quota/CanonicalQuotaObject.cpp @@ -0,0 +1,331 @@ +/* -*- 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 "CanonicalQuotaObject.h" + +#include "GroupInfo.h" +#include "GroupInfoPair.h" +#include "mozilla/dom/StorageActivityService.h" +#include "mozilla/dom/quota/AssertionsImpl.h" +#include "mozilla/dom/quota/DirectoryLock.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "OriginInfo.h" + +namespace mozilla::dom::quota { + +NS_IMETHODIMP_(MozExternalRefCountType) CanonicalQuotaObject::AddRef() { + QuotaManager* quotaManager = QuotaManager::Get(); + if (!quotaManager) { + NS_ERROR("Null quota manager, this shouldn't happen, possible leak!"); + + return ++mRefCnt; + } + + MutexAutoLock lock(quotaManager->mQuotaMutex); + + return ++mRefCnt; +} + +NS_IMETHODIMP_(MozExternalRefCountType) CanonicalQuotaObject::Release() { + QuotaManager* quotaManager = QuotaManager::Get(); + if (!quotaManager) { + NS_ERROR("Null quota manager, this shouldn't happen, possible leak!"); + + nsrefcnt count = --mRefCnt; + if (count == 0) { + mRefCnt = 1; + delete this; + return 0; + } + + return mRefCnt; + } + + { + MutexAutoLock lock(quotaManager->mQuotaMutex); + + --mRefCnt; + + if (mRefCnt > 0) { + return mRefCnt; + } + + if (mOriginInfo) { + mOriginInfo->mCanonicalQuotaObjects.Remove(mPath); + } + } + + delete this; + return 0; +} + +bool CanonicalQuotaObject::MaybeUpdateSize(int64_t aSize, bool aTruncate) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + MutexAutoLock lock(quotaManager->mQuotaMutex); + + return LockedMaybeUpdateSize(aSize, aTruncate); +} + +bool CanonicalQuotaObject::IncreaseSize(int64_t aDelta) { + MOZ_ASSERT(aDelta >= 0); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + MutexAutoLock lock(quotaManager->mQuotaMutex); + + AssertNoOverflow(mSize, aDelta); + int64_t size = mSize + aDelta; + + return LockedMaybeUpdateSize(size, /* aTruncate */ false); +} + +void CanonicalQuotaObject::DisableQuotaCheck() { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + MutexAutoLock lock(quotaManager->mQuotaMutex); + + mQuotaCheckDisabled = true; +} + +void CanonicalQuotaObject::EnableQuotaCheck() { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + MutexAutoLock lock(quotaManager->mQuotaMutex); + + mQuotaCheckDisabled = false; +} + +bool CanonicalQuotaObject::LockedMaybeUpdateSize(int64_t aSize, + bool aTruncate) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + quotaManager->mQuotaMutex.AssertCurrentThreadOwns(); + + if (mWritingDone == false && mOriginInfo) { + mWritingDone = true; + StorageActivityService::SendActivity(mOriginInfo->mOrigin); + } + + if (mQuotaCheckDisabled) { + return true; + } + + if (mSize == aSize) { + return true; + } + + if (!mOriginInfo) { + mSize = aSize; + return true; + } + + GroupInfo* groupInfo = mOriginInfo->mGroupInfo; + MOZ_ASSERT(groupInfo); + + if (mSize > aSize) { + if (aTruncate) { + const int64_t delta = mSize - aSize; + + AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, delta); + quotaManager->mTemporaryStorageUsage -= delta; + + if (!mOriginInfo->LockedPersisted()) { + AssertNoUnderflow(groupInfo->mUsage, delta); + groupInfo->mUsage -= delta; + } + + AssertNoUnderflow(mOriginInfo->mUsage, delta); + mOriginInfo->mUsage -= delta; + + MOZ_ASSERT(mOriginInfo->mClientUsages[mClientType].isSome()); + AssertNoUnderflow(mOriginInfo->mClientUsages[mClientType].value(), delta); + mOriginInfo->mClientUsages[mClientType] = + Some(mOriginInfo->mClientUsages[mClientType].value() - delta); + + mSize = aSize; + } + return true; + } + + MOZ_ASSERT(mSize < aSize); + + RefPtr<GroupInfo> complementaryGroupInfo = + groupInfo->mGroupInfoPair->LockedGetGroupInfo( + ComplementaryPersistenceType(groupInfo->mPersistenceType)); + + uint64_t delta = aSize - mSize; + + AssertNoOverflow(mOriginInfo->mClientUsages[mClientType].valueOr(0), delta); + uint64_t newClientUsage = + mOriginInfo->mClientUsages[mClientType].valueOr(0) + delta; + + AssertNoOverflow(mOriginInfo->mUsage, delta); + uint64_t newUsage = mOriginInfo->mUsage + delta; + + // Temporary storage has no limit for origin usage (there's a group and the + // global limit though). + + uint64_t newGroupUsage = groupInfo->mUsage; + if (!mOriginInfo->LockedPersisted()) { + AssertNoOverflow(groupInfo->mUsage, delta); + newGroupUsage += delta; + + uint64_t groupUsage = groupInfo->mUsage; + if (complementaryGroupInfo) { + AssertNoOverflow(groupUsage, complementaryGroupInfo->mUsage); + groupUsage += complementaryGroupInfo->mUsage; + } + + // Temporary storage has a hard limit for group usage (20 % of the global + // limit). + AssertNoOverflow(groupUsage, delta); + if (groupUsage + delta > quotaManager->GetGroupLimit()) { + return false; + } + } + + AssertNoOverflow(quotaManager->mTemporaryStorageUsage, delta); + uint64_t newTemporaryStorageUsage = + quotaManager->mTemporaryStorageUsage + delta; + + if (newTemporaryStorageUsage > quotaManager->mTemporaryStorageLimit) { + // This will block the thread without holding the lock while waitting. + + AutoTArray<RefPtr<OriginDirectoryLock>, 10> locks; + uint64_t sizeToBeFreed; + + if (::mozilla::ipc::IsOnBackgroundThread()) { + MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex); + + sizeToBeFreed = quotaManager->CollectOriginsForEviction(delta, locks); + } else { + sizeToBeFreed = + quotaManager->LockedCollectOriginsForEviction(delta, locks); + } + + if (!sizeToBeFreed) { + uint64_t usage = quotaManager->mTemporaryStorageUsage; + + MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex); + + quotaManager->NotifyStoragePressure(usage); + + return false; + } + + NS_ASSERTION(sizeToBeFreed >= delta, "Huh?"); + + { + MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex); + + for (const auto& lock : locks) { + quotaManager->DeleteFilesForOrigin(lock->GetPersistenceType(), + lock->Origin()); + } + } + + // Relocked. + + NS_ASSERTION(mOriginInfo, "How come?!"); + + for (const auto& lock : locks) { + MOZ_ASSERT(!(lock->GetPersistenceType() == groupInfo->mPersistenceType && + lock->Origin() == mOriginInfo->mOrigin), + "Deleted itself!"); + + quotaManager->LockedRemoveQuotaForOrigin(lock->OriginMetadata()); + } + + // We unlocked and relocked several times so we need to recompute all the + // essential variables and recheck the group limit. + + AssertNoUnderflow(aSize, mSize); + delta = aSize - mSize; + + AssertNoOverflow(mOriginInfo->mClientUsages[mClientType].valueOr(0), delta); + newClientUsage = mOriginInfo->mClientUsages[mClientType].valueOr(0) + delta; + + AssertNoOverflow(mOriginInfo->mUsage, delta); + newUsage = mOriginInfo->mUsage + delta; + + newGroupUsage = groupInfo->mUsage; + if (!mOriginInfo->LockedPersisted()) { + AssertNoOverflow(groupInfo->mUsage, delta); + newGroupUsage += delta; + + uint64_t groupUsage = groupInfo->mUsage; + if (complementaryGroupInfo) { + AssertNoOverflow(groupUsage, complementaryGroupInfo->mUsage); + groupUsage += complementaryGroupInfo->mUsage; + } + + AssertNoOverflow(groupUsage, delta); + if (groupUsage + delta > quotaManager->GetGroupLimit()) { + // Unfortunately some other thread increased the group usage in the + // meantime and we are not below the group limit anymore. + + // However, the origin eviction must be finalized in this case too. + MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex); + + quotaManager->FinalizeOriginEviction(std::move(locks)); + + return false; + } + } + + AssertNoOverflow(quotaManager->mTemporaryStorageUsage, delta); + newTemporaryStorageUsage = quotaManager->mTemporaryStorageUsage + delta; + + NS_ASSERTION( + newTemporaryStorageUsage <= quotaManager->mTemporaryStorageLimit, + "How come?!"); + + // Ok, we successfully freed enough space and the operation can continue + // without throwing the quota error. + mOriginInfo->mClientUsages[mClientType] = Some(newClientUsage); + + mOriginInfo->mUsage = newUsage; + if (!mOriginInfo->LockedPersisted()) { + groupInfo->mUsage = newGroupUsage; + } + quotaManager->mTemporaryStorageUsage = newTemporaryStorageUsage; + ; + + // Some other thread could increase the size in the meantime, but no more + // than this one. + MOZ_ASSERT(mSize < aSize); + mSize = aSize; + + // Finally, release IO thread only objects and allow next synchronized + // ops for the evicted origins. + MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex); + + quotaManager->FinalizeOriginEviction(std::move(locks)); + + return true; + } + + mOriginInfo->mClientUsages[mClientType] = Some(newClientUsage); + + mOriginInfo->mUsage = newUsage; + if (!mOriginInfo->LockedPersisted()) { + groupInfo->mUsage = newGroupUsage; + } + quotaManager->mTemporaryStorageUsage = newTemporaryStorageUsage; + + mSize = aSize; + + return true; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/CanonicalQuotaObject.h b/dom/quota/CanonicalQuotaObject.h new file mode 100644 index 0000000000..c47862f887 --- /dev/null +++ b/dom/quota/CanonicalQuotaObject.h @@ -0,0 +1,90 @@ +/* -*- 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_QUOTA_CANONICALQUOTAOBJECT_H_ +#define DOM_QUOTA_CANONICALQUOTAOBJECT_H_ + +// Local includes +#include "Client.h" + +// Global includes +#include <cstdint> +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/quota/Assertions.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "nsCOMPtr.h" +#include "nsISupports.h" +#include "nsStringFwd.h" + +// XXX Avoid including this here by moving function bodies to the cpp file. +#include "mozilla/dom/quota/QuotaCommon.h" + +namespace mozilla::dom::quota { + +class OriginInfo; +class QuotaManager; + +class CanonicalQuotaObject final : public QuotaObject { + friend class OriginInfo; + friend class QuotaManager; + + class StoragePressureRunnable; + + public: + NS_IMETHOD_(MozExternalRefCountType) AddRef() override; + + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + const nsAString& Path() const override { return mPath; } + + [[nodiscard]] bool MaybeUpdateSize(int64_t aSize, bool aTruncate) override; + + bool IncreaseSize(int64_t aDelta) override; + + void DisableQuotaCheck() override; + + void EnableQuotaCheck() override; + + private: + CanonicalQuotaObject(OriginInfo* aOriginInfo, Client::Type aClientType, + const nsAString& aPath, int64_t aSize) + : QuotaObject(/* aIsRemote */ false), + mOriginInfo(aOriginInfo), + mPath(aPath), + mSize(aSize), + mClientType(aClientType), + mQuotaCheckDisabled(false), + mWritingDone(false) { + MOZ_COUNT_CTOR(CanonicalQuotaObject); + } + + MOZ_COUNTED_DTOR(CanonicalQuotaObject) + + already_AddRefed<QuotaObject> LockedAddRef() { + AssertCurrentThreadOwnsQuotaMutex(); + + ++mRefCnt; + + RefPtr<QuotaObject> result = dont_AddRef(this); + return result.forget(); + } + + bool LockedMaybeUpdateSize(int64_t aSize, bool aTruncate); + + mozilla::ThreadSafeAutoRefCnt mRefCnt; + + OriginInfo* mOriginInfo; + nsString mPath; + int64_t mSize; + Client::Type mClientType; + bool mQuotaCheckDisabled; + bool mWritingDone; +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_CANONICALQUOTAOBJECT_H_ diff --git a/dom/quota/CheckedUnsafePtr.h b/dom/quota/CheckedUnsafePtr.h new file mode 100644 index 0000000000..70d18b4c7c --- /dev/null +++ b/dom/quota/CheckedUnsafePtr.h @@ -0,0 +1,405 @@ +/* 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/. */ + +// Diagnostic class template that helps finding dangling pointers. + +#ifndef mozilla_CheckedUnsafePtr_h +#define mozilla_CheckedUnsafePtr_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/DataMutex.h" +#include "nsTArray.h" + +#include <cstddef> +#include <type_traits> +#include <utility> + +namespace mozilla { +enum class CheckingSupport { + Disabled, + Enabled, +}; + +template <typename T> +class CheckedUnsafePtr; + +namespace detail { +class CheckedUnsafePtrBaseCheckingEnabled; + +struct CheckedUnsafePtrCheckData { + using Data = nsTArray<CheckedUnsafePtrBaseCheckingEnabled*>; + + DataMutex<Data> mPtrs{"mozilla::SupportsCheckedUnsafePtr"}; +}; + +class CheckedUnsafePtrBaseCheckingEnabled { + friend class CheckedUnsafePtrBaseAccess; + + protected: + constexpr CheckedUnsafePtrBaseCheckingEnabled() = default; + CheckedUnsafePtrBaseCheckingEnabled( + const CheckedUnsafePtrBaseCheckingEnabled& aOther) = default; + + // When copying an CheckedUnsafePtr, its mIsDangling member must be copied as + // well; otherwise the new copy might try to dereference a dangling pointer + // when destructed. + void CopyDanglingFlagIfAvailableFrom( + const CheckedUnsafePtrBaseCheckingEnabled& aOther) { + mIsDangling = aOther.mIsDangling; + } + + template <typename Ptr> + using DisableForCheckedUnsafePtr = std::enable_if_t< + !std::is_base_of<CheckedUnsafePtrBaseCheckingEnabled, Ptr>::value>; + + // When constructing an CheckedUnsafePtr from a different kind of pointer it's + // not possible to determine whether it's dangling; therefore it's undefined + // behavior to construct one from a dangling pointer, and we assume that any + // CheckedUnsafePtr thus constructed is not dangling. + template <typename Ptr> + DisableForCheckedUnsafePtr<Ptr> CopyDanglingFlagIfAvailableFrom(const Ptr&) {} + + template <typename F> + void WithCheckedUnsafePtrsImpl(CheckedUnsafePtrCheckData* const aRawPtr, + F&& aClosure) { + if (!mIsDangling && aRawPtr) { + const auto CheckedUnsafePtrs = aRawPtr->mPtrs.Lock(); + aClosure(this, *CheckedUnsafePtrs); + } + } + + private: + bool mIsDangling = false; +}; + +class CheckedUnsafePtrBaseAccess { + protected: + static void SetDanglingFlag(CheckedUnsafePtrBaseCheckingEnabled& aBase) { + aBase.mIsDangling = true; + } +}; + +template <typename T, CheckingSupport = T::SupportsChecking::value> +class CheckedUnsafePtrBase; + +template <typename T, typename U, typename S = std::nullptr_t> +using EnableIfCompatible = std::enable_if_t< + std::is_base_of< + T, std::remove_reference_t<decltype(*std::declval<U>())>>::value, + S>; + +template <typename T> +class CheckedUnsafePtrBase<T, CheckingSupport::Enabled> + : detail::CheckedUnsafePtrBaseCheckingEnabled { + public: + MOZ_IMPLICIT constexpr CheckedUnsafePtrBase(const std::nullptr_t = nullptr) + : mRawPtr(nullptr) {} + + template <typename U, typename = EnableIfCompatible<T, U>> + MOZ_IMPLICIT CheckedUnsafePtrBase(const U& aPtr) { + Set(aPtr); + } + + CheckedUnsafePtrBase(const CheckedUnsafePtrBase& aOther) { + Set(aOther.Downcast()); + } + + ~CheckedUnsafePtrBase() { Reset(); } + + CheckedUnsafePtr<T>& operator=(const std::nullptr_t) { + Reset(); + return Downcast(); + } + + template <typename U> + EnableIfCompatible<T, U, CheckedUnsafePtr<T>&> operator=(const U& aPtr) { + Replace(aPtr); + return Downcast(); + } + + CheckedUnsafePtrBase& operator=(const CheckedUnsafePtrBase& aOther) { + if (&aOther != this) { + Replace(aOther.Downcast()); + } + return Downcast(); + } + + constexpr T* get() const { return mRawPtr; } + + private: + template <typename U, CheckingSupport> + friend class CheckedUnsafePtrBase; + + CheckedUnsafePtr<T>& Downcast() { + return static_cast<CheckedUnsafePtr<T>&>(*this); + } + const CheckedUnsafePtr<T>& Downcast() const { + return static_cast<const CheckedUnsafePtr<T>&>(*this); + } + + using Base = detail::CheckedUnsafePtrBaseCheckingEnabled; + + template <typename U> + void Replace(const U& aPtr) { + Reset(); + Set(aPtr); + } + + void Reset() { + WithCheckedUnsafePtrs( + [](Base* const aSelf, + detail::CheckedUnsafePtrCheckData::Data& aCheckedUnsafePtrs) { + const auto index = aCheckedUnsafePtrs.IndexOf(aSelf); + aCheckedUnsafePtrs.UnorderedRemoveElementAt(index); + }); + mRawPtr = nullptr; + } + + template <typename U> + void Set(const U& aPtr) { + this->CopyDanglingFlagIfAvailableFrom(aPtr); + mRawPtr = &*aPtr; + WithCheckedUnsafePtrs( + [](Base* const aSelf, + detail::CheckedUnsafePtrCheckData::Data& aCheckedUnsafePtrs) { + aCheckedUnsafePtrs.AppendElement(aSelf); + }); + } + + template <typename F> + void WithCheckedUnsafePtrs(F&& aClosure) { + this->WithCheckedUnsafePtrsImpl(mRawPtr, std::forward<F>(aClosure)); + } + + T* mRawPtr; +}; + +template <typename T> +class CheckedUnsafePtrBase<T, CheckingSupport::Disabled> { + public: + MOZ_IMPLICIT constexpr CheckedUnsafePtrBase(const std::nullptr_t = nullptr) + : mRawPtr(nullptr) {} + + template <typename U, typename = EnableIfCompatible<T, U>> + MOZ_IMPLICIT constexpr CheckedUnsafePtrBase(const U& aPtr) : mRawPtr(aPtr) {} + + constexpr CheckedUnsafePtr<T>& operator=(const std::nullptr_t) { + mRawPtr = nullptr; + return Downcast(); + } + + template <typename U> + constexpr EnableIfCompatible<T, U, CheckedUnsafePtr<T>&> operator=( + const U& aPtr) { + mRawPtr = aPtr; + return Downcast(); + } + + constexpr T* get() const { return mRawPtr; } + + private: + constexpr CheckedUnsafePtr<T>& Downcast() { + return static_cast<CheckedUnsafePtr<T>&>(*this); + } + + T* mRawPtr; +}; +} // namespace detail + +class CheckingPolicyAccess { + protected: + template <typename CheckingPolicy> + static void NotifyCheckFailure(CheckingPolicy& aPolicy) { + aPolicy.NotifyCheckFailure(); + } +}; + +template <typename Derived> +class CheckCheckedUnsafePtrs : private CheckingPolicyAccess, + private detail::CheckedUnsafePtrBaseAccess { + public: + using SupportsChecking = + std::integral_constant<CheckingSupport, CheckingSupport::Enabled>; + + protected: + static constexpr bool ShouldCheck() { + static_assert( + std::is_base_of<CheckCheckedUnsafePtrs, Derived>::value, + "cannot instantiate with a type that's not a subclass of this class"); + return true; + } + + void Check(detail::CheckedUnsafePtrCheckData::Data& aCheckedUnsafePtrs) { + if (!aCheckedUnsafePtrs.IsEmpty()) { + for (auto* const aCheckedUnsafePtrBase : aCheckedUnsafePtrs) { + SetDanglingFlag(*aCheckedUnsafePtrBase); + } + NotifyCheckFailure(*static_cast<Derived*>(this)); + } + } +}; + +class CrashOnDanglingCheckedUnsafePtr + : public CheckCheckedUnsafePtrs<CrashOnDanglingCheckedUnsafePtr> { + friend class mozilla::CheckingPolicyAccess; + void NotifyCheckFailure() { MOZ_CRASH("Found dangling CheckedUnsafePtr"); } +}; + +struct DoNotCheckCheckedUnsafePtrs { + using SupportsChecking = + std::integral_constant<CheckingSupport, CheckingSupport::Disabled>; +}; + +namespace detail { +// Template parameter CheckingSupport controls the inclusion of +// CheckedUnsafePtrCheckData as a subobject of instantiations of +// SupportsCheckedUnsafePtr, ensuring that choosing a policy without checking +// support incurs no size overhead. +template <typename CheckingPolicy, + CheckingSupport = CheckingPolicy::SupportsChecking::value> +class SupportCheckedUnsafePtrImpl; + +template <typename CheckingPolicy> +class SupportCheckedUnsafePtrImpl<CheckingPolicy, CheckingSupport::Disabled> + : public CheckingPolicy { + protected: + template <typename... Args> + explicit SupportCheckedUnsafePtrImpl(Args&&... aArgs) + : CheckingPolicy(std::forward<Args>(aArgs)...) {} +}; + +template <typename CheckingPolicy> +class SupportCheckedUnsafePtrImpl<CheckingPolicy, CheckingSupport::Enabled> + : public CheckedUnsafePtrCheckData, public CheckingPolicy { + template <typename T> + friend class CheckedUnsafePtr; + + protected: + template <typename... Args> + explicit SupportCheckedUnsafePtrImpl(Args&&... aArgs) + : CheckingPolicy(std::forward<Args>(aArgs)...) {} + + ~SupportCheckedUnsafePtrImpl() { + if (this->ShouldCheck()) { + const auto ptrs = mPtrs.Lock(); + this->Check(*ptrs); + } + } +}; + +struct SupportsCheckedUnsafePtrTag {}; +} // namespace detail + +template <typename Condition, + typename CheckingPolicy = CrashOnDanglingCheckedUnsafePtr> +using CheckIf = std::conditional_t<Condition::value, CheckingPolicy, + DoNotCheckCheckedUnsafePtrs>; + +using DiagnosticAssertEnabled = std::integral_constant<bool, +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + true +#else + false +#endif + >; + +// A T class that publicly inherits from an instantiation of +// SupportsCheckedUnsafePtr and its subclasses can be pointed to by smart +// pointers of type CheckedUnsafePtr<T>. Whenever such a smart pointer is +// created, its existence is tracked by the pointee according to its +// CheckingPolicy. When the pointee goes out of scope it then uses the its +// CheckingPolicy to verify that no CheckedUnsafePtr pointers are left pointing +// to it. +// +// The CheckingPolicy type is used to control the kind of verification that +// happen at the end of the object's lifetime. By default, debug builds always +// check for dangling CheckedUnsafePtr pointers and assert that none are found, +// while release builds forgo all checks. (Release builds incur no size or +// runtime penalties compared to bare pointers.) +template <typename CheckingPolicy> +class SupportsCheckedUnsafePtr + : public detail::SupportCheckedUnsafePtrImpl<CheckingPolicy>, + public detail::SupportsCheckedUnsafePtrTag { + public: + template <typename... Args> + explicit SupportsCheckedUnsafePtr(Args&&... aArgs) + : detail::SupportCheckedUnsafePtrImpl<CheckingPolicy>( + std::forward<Args>(aArgs)...) {} +}; + +// CheckedUnsafePtr<T> is a smart pointer class that helps detect dangling +// pointers in cases where such pointers are not allowed. In order to use it, +// the pointee T must publicly inherit from an instantiation of +// SupportsCheckedUnsafePtr. An CheckedUnsafePtr<T> can be used anywhere a T* +// can be used, has the same size, and imposes no additional thread-safety +// restrictions. +template <typename T> +class CheckedUnsafePtr : public detail::CheckedUnsafePtrBase<T> { + static_assert( + std::is_base_of<detail::SupportsCheckedUnsafePtrTag, T>::value, + "type T must be derived from instantiation of SupportsCheckedUnsafePtr"); + + public: + using detail::CheckedUnsafePtrBase<T>::CheckedUnsafePtrBase; + using detail::CheckedUnsafePtrBase<T>::get; + + constexpr T* operator->() const { return get(); } + + constexpr T& operator*() const { return *get(); } + + MOZ_IMPLICIT constexpr operator T*() const { return get(); } + + template <typename U> + constexpr bool operator==( + detail::EnableIfCompatible<T, U, const U&> aRhs) const { + return get() == aRhs.get(); + } + + template <typename U> + friend constexpr bool operator==( + detail::EnableIfCompatible<T, U, const U&> aLhs, + const CheckedUnsafePtr& aRhs) { + return aRhs == aLhs; + } + + template <typename U> + constexpr bool operator!=( + detail::EnableIfCompatible<T, U, const U&> aRhs) const { + return !(*this == aRhs); + } + + template <typename U> + friend constexpr bool operator!=( + detail::EnableIfCompatible<T, U, const U&> aLhs, + const CheckedUnsafePtr& aRhs) { + return aRhs != aLhs; + } +}; + +} // namespace mozilla + +// nsTArray<T> requires by default that T can be safely moved with std::memmove. +// Since CheckedUnsafePtr<T> has a non-trivial copy constructor, it has to opt +// into nsTArray<T> using them. +template <typename T> +struct nsTArray_RelocationStrategy<mozilla::CheckedUnsafePtr<T>> { + using Type = std::conditional_t< + T::SupportsChecking::value == mozilla::CheckingSupport::Enabled, + nsTArray_RelocateUsingMoveConstructor<mozilla::CheckedUnsafePtr<T>>, + nsTArray_RelocateUsingMemutils>; +}; + +template <typename T> +struct nsTArray_RelocationStrategy< + mozilla::NotNull<mozilla::CheckedUnsafePtr<T>>> { + using Type = + std::conditional_t<T::SupportsChecking::value == + mozilla::CheckingSupport::Enabled, + nsTArray_RelocateUsingMoveConstructor< + mozilla::NotNull<mozilla::CheckedUnsafePtr<T>>>, + nsTArray_RelocateUsingMemutils>; +}; + +#endif // mozilla_CheckedUnsafePtr_h diff --git a/dom/quota/CipherStrategy.h b/dom/quota/CipherStrategy.h new file mode 100644 index 0000000000..495bdb7528 --- /dev/null +++ b/dom/quota/CipherStrategy.h @@ -0,0 +1,42 @@ +/* -*- 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_quota_CipherStrategy_h +#define mozilla_dom_quota_CipherStrategy_h + +namespace mozilla::dom::quota { + +enum class CipherMode { Encrypt, Decrypt }; + +// An implementation of the CipherStrategy concept must provide the following +// data members: +// +// static constexpr size_t BlockPrefixLength; +// static constexpr size_t BasicBlockSize; +// +// It must provide the following member types: +// +// KeyType +// BlockPrefixType, which must be an InputRange of type uint8_t and a +// SizedRange of size BlockPrefixLength +// +// It must provide the following member functions with compatible signatures: +// +// static Result<KeyType, nsresult> GenerateKey(); +// +// nsresult Cipher(const CipherMode aMode, const KeyType& aKey, +// Span<uint8_t> aIv, Span<const uint8_t> aIn, +// Span<uint8_t> aOut); +// +// BlockPrefixType MakeBlockPrefix(); +// +// Span<const uint8_t> SerializeKey(const KeyType& aKey); +// +// KeyType DeserializeKey(const Span<const uint8_t>& aSerializedKey); + +} // namespace mozilla::dom::quota + +#endif diff --git a/dom/quota/Client.cpp b/dom/quota/Client.cpp new file mode 100644 index 0000000000..87da8815cc --- /dev/null +++ b/dom/quota/Client.cpp @@ -0,0 +1,299 @@ +/* -*- 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 "Client.h" + +// Global includes +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/Assertions.h" +#include "mozilla/dom/quota/QuotaManager.h" + +namespace mozilla::dom::quota { + +using mozilla::ipc::AssertIsOnBackgroundThread; +using mozilla::ipc::IsOnBackgroundThread; + +namespace { + +const char kIDBPrefix = 'I'; +const char kDOMCachePrefix = 'C'; +const char kSDBPrefix = 'S'; +const char kFILESYSTEMPrefix = 'F'; +const char kLSPrefix = 'L'; + +template <Client::Type type> +struct ClientTypeTraits; + +template <> +struct ClientTypeTraits<Client::Type::IDB> { + template <typename T> + static void To(T& aData) { + aData.AssignLiteral(IDB_DIRECTORY_NAME); + } + + static void To(char& aData) { aData = kIDBPrefix; } + + template <typename T> + static bool From(const T& aData) { + return aData.EqualsLiteral(IDB_DIRECTORY_NAME); + } + + static bool From(char aData) { return aData == kIDBPrefix; } +}; + +template <> +struct ClientTypeTraits<Client::Type::DOMCACHE> { + template <typename T> + static void To(T& aData) { + aData.AssignLiteral(DOMCACHE_DIRECTORY_NAME); + } + + static void To(char& aData) { aData = kDOMCachePrefix; } + + template <typename T> + static bool From(const T& aData) { + return aData.EqualsLiteral(DOMCACHE_DIRECTORY_NAME); + } + + static bool From(char aData) { return aData == kDOMCachePrefix; } +}; + +template <> +struct ClientTypeTraits<Client::Type::SDB> { + template <typename T> + static void To(T& aData) { + aData.AssignLiteral(SDB_DIRECTORY_NAME); + } + + static void To(char& aData) { aData = kSDBPrefix; } + + template <typename T> + static bool From(const T& aData) { + return aData.EqualsLiteral(SDB_DIRECTORY_NAME); + } + + static bool From(char aData) { return aData == kSDBPrefix; } +}; + +template <> +struct ClientTypeTraits<Client::Type::FILESYSTEM> { + template <typename T> + static void To(T& aData) { + aData.AssignLiteral(FILESYSTEM_DIRECTORY_NAME); + } + + static void To(char& aData) { aData = kFILESYSTEMPrefix; } + + template <typename T> + static bool From(const T& aData) { + return aData.EqualsLiteral(FILESYSTEM_DIRECTORY_NAME); + } + + static bool From(char aData) { return aData == kFILESYSTEMPrefix; } +}; + +template <> +struct ClientTypeTraits<Client::Type::LS> { + template <typename T> + static void To(T& aData) { + aData.AssignLiteral(LS_DIRECTORY_NAME); + } + + static void To(char& aData) { aData = kLSPrefix; } + + template <typename T> + static bool From(const T& aData) { + return aData.EqualsLiteral(LS_DIRECTORY_NAME); + } + + static bool From(char aData) { return aData == kLSPrefix; } +}; + +template <typename T> +bool TypeTo_impl(Client::Type aType, T& aData) { + switch (aType) { + case Client::IDB: + ClientTypeTraits<Client::Type::IDB>::To(aData); + return true; + + case Client::DOMCACHE: + ClientTypeTraits<Client::Type::DOMCACHE>::To(aData); + return true; + + case Client::SDB: + ClientTypeTraits<Client::Type::SDB>::To(aData); + return true; + + case Client::FILESYSTEM: + ClientTypeTraits<Client::Type::FILESYSTEM>::To(aData); + return true; + + case Client::LS: + if (CachedNextGenLocalStorageEnabled()) { + ClientTypeTraits<Client::Type::LS>::To(aData); + return true; + } + [[fallthrough]]; + + case Client::TYPE_MAX: + default: + return false; + } + + MOZ_CRASH("Should never get here!"); +} + +template <typename T> +bool TypeFrom_impl(const T& aData, Client::Type& aType) { + if (ClientTypeTraits<Client::Type::IDB>::From(aData)) { + aType = Client::IDB; + return true; + } + + if (ClientTypeTraits<Client::Type::DOMCACHE>::From(aData)) { + aType = Client::DOMCACHE; + return true; + } + + if (ClientTypeTraits<Client::Type::SDB>::From(aData)) { + aType = Client::SDB; + return true; + } + + if (ClientTypeTraits<Client::Type::FILESYSTEM>::From(aData)) { + aType = Client::FILESYSTEM; + return true; + } + + if (CachedNextGenLocalStorageEnabled() && + ClientTypeTraits<Client::Type::LS>::From(aData)) { + aType = Client::LS; + return true; + } + + return false; +} + +void BadType() { MOZ_CRASH("Bad client type value!"); } + +} // namespace + +// static +bool Client::IsValidType(Type aType) { + switch (aType) { + case Client::IDB: + case Client::DOMCACHE: + case Client::SDB: + case Client::FILESYSTEM: + return true; + + case Client::LS: + if (CachedNextGenLocalStorageEnabled()) { + return true; + } + [[fallthrough]]; + + default: + return false; + } +} + +// static +bool Client::TypeToText(Type aType, nsAString& aText, const fallible_t&) { + nsString text; + if (!TypeTo_impl(aType, text)) { + return false; + } + aText = text; + return true; +} + +// static +nsAutoString Client::TypeToString(Type aType) { + nsAutoString res; + if (!TypeTo_impl(aType, res)) { + BadType(); + } + return res; +} + +// static +nsAutoCString Client::TypeToText(Type aType) { + nsAutoCString res; + if (!TypeTo_impl(aType, res)) { + BadType(); + } + return res; +} + +// static +bool Client::TypeFromText(const nsAString& aText, Type& aType, + const fallible_t&) { + Type type; + if (!TypeFrom_impl(aText, type)) { + return false; + } + aType = type; + return true; +} + +// static +Client::Type Client::TypeFromText(const nsACString& aText) { + Type type; + if (!TypeFrom_impl(aText, type)) { + BadType(); + } + return type; +} + +// static +char Client::TypeToPrefix(Type aType) { + char prefix; + if (!TypeTo_impl(aType, prefix)) { + BadType(); + } + return prefix; +} + +// static +bool Client::TypeFromPrefix(char aPrefix, Type& aType, const fallible_t&) { + Type type; + if (!TypeFrom_impl(aPrefix, type)) { + return false; + } + aType = type; + return true; +} + +bool Client::InitiateShutdownWorkThreads() { + AssertIsOnBackgroundThread(); + + QuotaManager::MaybeRecordQuotaClientShutdownStep(GetType(), "starting"_ns); + + InitiateShutdown(); + + return IsShutdownCompleted(); +} + +void Client::FinalizeShutdownWorkThreads() { + QuotaManager::MaybeRecordQuotaClientShutdownStep(GetType(), "completed"_ns); + + FinalizeShutdown(); +} + +// static +bool Client::IsShuttingDownOnBackgroundThread() { + MOZ_ASSERT(IsOnBackgroundThread()); + return QuotaManager::IsShuttingDown(); +} + +// static +bool Client::IsShuttingDownOnNonBackgroundThread() { + MOZ_ASSERT(!IsOnBackgroundThread()); + return QuotaManager::IsShuttingDown(); +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/Client.h b/dom/quota/Client.h new file mode 100644 index 0000000000..8a6f0a9548 --- /dev/null +++ b/dom/quota/Client.h @@ -0,0 +1,191 @@ +/* -*- 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_quota_client_h__ +#define mozilla_dom_quota_client_h__ + +#include "ErrorList.h" +#include "mozilla/Atomics.h" +#include "mozilla/Result.h" +#include "mozilla/dom/ipc/IdType.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "nsHashKeys.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTHashSet.h" + +// XXX Remove this dependency. +#include "mozilla/dom/LocalStorageCommon.h" + +class nsIFile; + +#define IDB_DIRECTORY_NAME "idb" +#define DOMCACHE_DIRECTORY_NAME "cache" +#define SDB_DIRECTORY_NAME "sdb" +#define FILESYSTEM_DIRECTORY_NAME "fs" +#define LS_DIRECTORY_NAME "ls" + +// Deprecated +#define ASMJSCACHE_DIRECTORY_NAME "asmjs" + +namespace mozilla::dom { +template <typename T> +struct Nullable; +} + +namespace mozilla::dom::quota { + +struct OriginMetadata; +class OriginScope; +class QuotaManager; +class UsageInfo; + +// An abstract interface for quota manager clients. +// Each storage API must provide an implementation of this interface in order +// to participate in centralized quota and storage handling. +class Client { + public: + using AtomicBool = Atomic<bool>; + + enum Type { + IDB = 0, + // APPCACHE, + DOMCACHE, + SDB, + FILESYSTEM, + LS, + TYPE_MAX + }; + + class DirectoryLockIdTable final { + nsTHashSet<uint64_t> mIds; + + public: + void Put(const int64_t aId) { mIds.Insert(aId); } + + bool Has(const int64_t aId) const { return mIds.Contains(aId); } + + bool Filled() const { return mIds.Count(); } + }; + + static Type TypeMax() { + if (NextGenLocalStorageEnabled()) { + return TYPE_MAX; + } + return LS; + } + + static bool IsValidType(Type aType); + + static bool TypeToText(Type aType, nsAString& aText, const fallible_t&); + + // TODO: Rename other similar methods to use String/CString instead of Text. + static nsAutoString TypeToString(Type aType); + + static nsAutoCString TypeToText(Type aType); + + static bool TypeFromText(const nsAString& aText, Type& aType, + const fallible_t&); + + static Type TypeFromText(const nsACString& aText); + + static char TypeToPrefix(Type aType); + + static bool TypeFromPrefix(char aPrefix, Type& aType, const fallible_t&); + + static bool IsDeprecatedClient(const nsAString& aText) { + return aText.EqualsLiteral(ASMJSCACHE_DIRECTORY_NAME); + } + + template <typename T> + static bool IsLockForObjectContainedInLockTable( + const T& aObject, const DirectoryLockIdTable& aIds); + + template <typename T> + static bool IsLockForObjectAcquiredAndContainedInLockTable( + const T& aObject, const DirectoryLockIdTable& aIds); + + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual Type GetType() = 0; + + // Methods which are called on the IO thread. + virtual nsresult UpgradeStorageFrom1_0To2_0(nsIFile* aDirectory) { + return NS_OK; + } + + virtual nsresult UpgradeStorageFrom2_0To2_1(nsIFile* aDirectory) { + return NS_OK; + } + + virtual nsresult UpgradeStorageFrom2_1To2_2(nsIFile* aDirectory) { + return NS_OK; + } + + virtual Result<UsageInfo, nsresult> InitOrigin( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) = 0; + + virtual nsresult InitOriginWithoutTracking( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) = 0; + + virtual Result<UsageInfo, nsresult> GetUsageForOrigin( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) = 0; + + // This method is called when origins are about to be cleared + // (except the case when clearing is triggered by the origin eviction). + virtual nsresult AboutToClearOrigins( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope) { + return NS_OK; + } + + virtual void OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) = 0; + + virtual void ReleaseIOThreadObjects() = 0; + + // Methods which are called on the background thread. + virtual void AbortOperationsForLocks( + const DirectoryLockIdTable& aDirectoryLockIds) = 0; + + virtual void AbortOperationsForProcess(ContentParentId aContentParentId) = 0; + + virtual void AbortAllOperations() = 0; + + virtual void StartIdleMaintenance() = 0; + + virtual void StopIdleMaintenance() = 0; + + // Both variants just check for QuotaManager::IsShuttingDown() + // but assert to be on the right thread. + // They must not be used for re-entrance checks. + // Deprecated: This distinction is not needed anymore. + // QuotaClients should call QuotaManager::IsShuttingDown instead. + static bool IsShuttingDownOnBackgroundThread(); + static bool IsShuttingDownOnNonBackgroundThread(); + + // Returns true if there is work that needs to be waited for. + bool InitiateShutdownWorkThreads(); + void FinalizeShutdownWorkThreads(); + + virtual nsCString GetShutdownStatus() const = 0; + virtual bool IsShutdownCompleted() const = 0; + virtual void ForceKillActors() = 0; + + private: + virtual void InitiateShutdown() = 0; + virtual void FinalizeShutdown() = 0; + + protected: + virtual ~Client() = default; +}; + +} // namespace mozilla::dom::quota + +#endif // mozilla_dom_quota_client_h__ diff --git a/dom/quota/ClientImpl.h b/dom/quota/ClientImpl.h new file mode 100644 index 0000000000..9602345e5d --- /dev/null +++ b/dom/quota/ClientImpl.h @@ -0,0 +1,36 @@ +/* -*- 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_QUOTA_CLIENTIMPL_H_ +#define DOM_QUOTA_CLIENTIMPL_H_ + +#include "mozilla/dom/quota/DirectoryLock.h" + +namespace mozilla::dom::quota { + +// static +template <typename T> +bool Client::IsLockForObjectContainedInLockTable( + const T& aObject, const DirectoryLockIdTable& aIds) { + const auto& maybeDirectoryLock = aObject.MaybeDirectoryLockRef(); + + MOZ_ASSERT(maybeDirectoryLock.isSome()); + + return aIds.Has(maybeDirectoryLock->Id()); +} + +// static +template <typename T> +bool Client::IsLockForObjectAcquiredAndContainedInLockTable( + const T& aObject, const DirectoryLockIdTable& aIds) { + const auto& maybeDirectoryLock = aObject.MaybeDirectoryLockRef(); + + return maybeDirectoryLock && aIds.Has(maybeDirectoryLock->Id()); +} + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_CLIENTIMPL_H_ diff --git a/dom/quota/ClientUsageArray.cpp b/dom/quota/ClientUsageArray.cpp new file mode 100644 index 0000000000..f0c8095b1c --- /dev/null +++ b/dom/quota/ClientUsageArray.cpp @@ -0,0 +1,55 @@ +/* -*- 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 "ClientUsageArray.h" + +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/ResultExtensions.h" + +namespace mozilla::dom::quota { + +void ClientUsageArray::Serialize(nsACString& aText) const { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + bool first = true; + + for (Client::Type type : quotaManager->AllClientTypes()) { + const Maybe<uint64_t>& clientUsage = ElementAt(type); + if (clientUsage.isSome()) { + if (first) { + first = false; + } else { + aText.Append(" "); + } + + aText.Append(Client::TypeToPrefix(type)); + aText.AppendInt(clientUsage.value()); + } + } +} + +nsresult ClientUsageArray::Deserialize(const nsACString& aText) { + for (const auto& token : + nsCCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>(aText, ' ') + .ToRange()) { + QM_TRY(OkIf(token.Length() >= 2), NS_ERROR_FAILURE); + + Client::Type clientType; + QM_TRY(OkIf(Client::TypeFromPrefix(token.First(), clientType, fallible)), + NS_ERROR_FAILURE); + + nsresult rv; + const uint64_t usage = Substring(token, 1).ToInteger64(&rv); + QM_TRY(MOZ_TO_RESULT(rv)); + + ElementAt(clientType) = Some(usage); + } + + return NS_OK; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/ClientUsageArray.h b/dom/quota/ClientUsageArray.h new file mode 100644 index 0000000000..3b57656143 --- /dev/null +++ b/dom/quota/ClientUsageArray.h @@ -0,0 +1,35 @@ +/* -*- 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_QUOTA_CLIENTUSAGEARAY_H_ +#define DOM_QUOTA_CLIENTUSAGEARAY_H_ + +#include <cstdint> +#include "mozilla/Maybe.h" +#include "mozilla/dom/quota/Client.h" + +namespace mozilla::dom::quota { + +// XXX Change this not to derive from AutoTArray. +class ClientUsageArray final + : public AutoTArray<Maybe<uint64_t>, Client::TYPE_MAX> { + public: + ClientUsageArray() { SetLength(Client::TypeMax()); } + + void Serialize(nsACString& aText) const; + + nsresult Deserialize(const nsACString& aText); + + ClientUsageArray Clone() const { + ClientUsageArray res; + res.Assign(*this); + return res; + } +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_CLIENTUSAGEARAY_H_ diff --git a/dom/quota/CommonMetadata.h b/dom/quota/CommonMetadata.h new file mode 100644 index 0000000000..094c965195 --- /dev/null +++ b/dom/quota/CommonMetadata.h @@ -0,0 +1,72 @@ +/* -*- 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_QUOTA_COMMONMETADATA_H_ +#define DOM_QUOTA_COMMONMETADATA_H_ + +#include <utility> +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "nsString.h" + +namespace mozilla::dom::quota { + +struct PrincipalMetadata { + nsCString mSuffix; + nsCString mGroup; + nsCString mOrigin; + + // These explicit constructors exist to prevent accidental aggregate + // initialization which could for example initialize mSuffix as group and + // mGroup as origin (if only two string arguments are used). + PrincipalMetadata() = default; + + PrincipalMetadata(nsCString aSuffix, nsCString aGroup, nsCString aOrigin) + : mSuffix{std::move(aSuffix)}, + mGroup{std::move(aGroup)}, + mOrigin{std::move(aOrigin)} {} +}; + +struct OriginMetadata : public PrincipalMetadata { + PersistenceType mPersistenceType; + + OriginMetadata() = default; + + OriginMetadata(nsCString aSuffix, nsCString aGroup, nsCString aOrigin, + PersistenceType aPersistenceType) + : PrincipalMetadata(std::move(aSuffix), std::move(aGroup), + std::move(aOrigin)), + mPersistenceType(aPersistenceType) {} + + OriginMetadata(PrincipalMetadata&& aPrincipalMetadata, + PersistenceType aPersistenceType) + : PrincipalMetadata(std::move(aPrincipalMetadata)), + mPersistenceType(aPersistenceType) {} +}; + +struct FullOriginMetadata : OriginMetadata { + bool mPersisted; + int64_t mLastAccessTime; + + FullOriginMetadata() = default; + + FullOriginMetadata(OriginMetadata aOriginMetadata, bool aPersisted, + int64_t aLastAccessTime) + : OriginMetadata(std::move(aOriginMetadata)), + mPersisted(aPersisted), + mLastAccessTime(aLastAccessTime) {} +}; + +struct ClientMetadata : OriginMetadata { + const Client::Type mClientType; + + ClientMetadata(OriginMetadata aOriginMetadata, Client::Type aClientType) + : OriginMetadata(std::move(aOriginMetadata)), mClientType(aClientType) {} +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_COMMONMETADATA_H_ diff --git a/dom/quota/Config.h b/dom/quota/Config.h new file mode 100644 index 0000000000..33056265a3 --- /dev/null +++ b/dom/quota/Config.h @@ -0,0 +1,32 @@ +/* -*- 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_QUOTA_CONFIG_H_ +#define DOM_QUOTA_CONFIG_H_ + +#ifdef DEBUG +# define QM_LOG_ERROR_TO_CONSOLE_ENABLED +#endif + +#define QM_LOG_ERROR_TO_BROWSER_CONSOLE_ENABLED + +#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG) +# define QM_LOG_ERROR_TO_TELEMETRY_ENABLED +#endif + +#if defined(QM_LOG_ERROR_TO_CONSOLE_ENABLED) || \ + defined(QM_LOG_ERROR_TO_BROWSER_CONSOLE_ENABLED) || \ + defined(QM_LOG_ERROR_TO_TELEMETRY_ENABLED) +# define QM_LOG_ERROR_ENABLED +#endif + +#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG) +# define QM_ERROR_STACKS_ENABLED +#endif + +#define QM_SCOPED_LOG_EXTRA_INFO_ENABLED + +#endif // DOM_QUOTA_CONFIG_H_ diff --git a/dom/quota/DecryptingInputStream.cpp b/dom/quota/DecryptingInputStream.cpp new file mode 100644 index 0000000000..939b1e3ad7 --- /dev/null +++ b/dom/quota/DecryptingInputStream.cpp @@ -0,0 +1,98 @@ +/* -*- 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 "DecryptingInputStream.h" +#include "DecryptingInputStream_impl.h" + +#include "nsStreamUtils.h" + +namespace mozilla::dom::quota { + +NS_IMPL_ADDREF(DecryptingInputStreamBase); +NS_IMPL_RELEASE(DecryptingInputStreamBase); + +NS_INTERFACE_MAP_BEGIN(DecryptingInputStreamBase) + NS_INTERFACE_MAP_ENTRY(nsIInputStream) + NS_INTERFACE_MAP_ENTRY(nsISeekableStream) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsICloneableInputStream, + mBaseCloneableInputStream || !mBaseStream) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL( + nsIIPCSerializableInputStream, + mBaseIPCSerializableInputStream || !mBaseStream) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream) +NS_INTERFACE_MAP_END + +DecryptingInputStreamBase::DecryptingInputStreamBase( + MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream, size_t aBlockSize) { + Init(std::move(aBaseStream), aBlockSize); +} + +void DecryptingInputStreamBase::Init( + MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream, size_t aBlockSize) { + mBlockSize.init(aBlockSize); + mBaseStream.init(std::move(aBaseStream)); + + const nsCOMPtr<nsISeekableStream> seekableStream = + do_QueryInterface(mBaseStream->get()); + MOZ_ASSERT(seekableStream && + SameCOMIdentity(mBaseStream->get(), seekableStream)); + mBaseSeekableStream.init(WrapNotNullUnchecked(seekableStream)); + + const nsCOMPtr<nsICloneableInputStream> cloneableInputStream = + do_QueryInterface(mBaseStream->get()); + if (cloneableInputStream && + SameCOMIdentity(mBaseStream->get(), cloneableInputStream)) { + mBaseCloneableInputStream.init(WrapNotNullUnchecked(cloneableInputStream)); + } + + const nsCOMPtr<nsIIPCSerializableInputStream> ipcSerializeInputStream = + do_QueryInterface(mBaseStream->get()); + if (ipcSerializeInputStream && + SameCOMIdentity(mBaseStream->get(), ipcSerializeInputStream)) { + mBaseIPCSerializableInputStream.init( + WrapNotNullUnchecked(ipcSerializeInputStream)); + } +} + +NS_IMETHODIMP DecryptingInputStreamBase::Read(char* aBuf, uint32_t aCount, + uint32_t* aBytesReadOut) { + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, aBytesReadOut); +} + +NS_IMETHODIMP DecryptingInputStreamBase::IsNonBlocking(bool* aNonBlockingOut) { + *aNonBlockingOut = false; + return NS_OK; +} + +NS_IMETHODIMP DecryptingInputStreamBase::SetEOF() { + // Cannot truncate a read-only stream. + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP DecryptingInputStreamBase::GetCloneable(bool* aCloneable) { + *aCloneable = true; + return NS_OK; +} + +void DecryptingInputStreamBase::SerializedComplexity(uint32_t aMaxSize, + uint32_t* aSizeUsed, + uint32_t* aPipes, + uint32_t* aTransferables) { + (*mBaseIPCSerializableInputStream) + ->SerializedComplexity(aMaxSize, aSizeUsed, aPipes, aTransferables); +} + +size_t DecryptingInputStreamBase::PlainLength() const { + MOZ_ASSERT(mNextByte <= mPlainBytes); + return mPlainBytes - mNextByte; +} + +size_t DecryptingInputStreamBase::EncryptedBufferLength() const { + return *mBlockSize; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/DecryptingInputStream.h b/dom/quota/DecryptingInputStream.h new file mode 100644 index 0000000000..7559ca860e --- /dev/null +++ b/dom/quota/DecryptingInputStream.h @@ -0,0 +1,163 @@ +/* -*- 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_quota_DecryptingInputStream_h +#define mozilla_dom_quota_DecryptingInputStream_h + +// Local includes +#include "EncryptedBlock.h" + +// Global includes +#include <cstddef> +#include <cstdint> +#include "ErrorList.h" +#include "mozilla/InitializedOnce.h" +#include "mozilla/Maybe.h" +#include "mozilla/NotNull.h" +#include "mozilla/ipc/InputStreamParams.h" +#include "nsCOMPtr.h" +#include "nsICloneableInputStream.h" +#include "nsIIPCSerializableInputStream.h" +#include "nsIInputStream.h" +#include "nsISeekableStream.h" +#include "nsISupports.h" +#include "nsITellableStream.h" +#include "nsTArray.h" +#include "nscore.h" + +template <class T> +class nsCOMPtr; + +namespace mozilla::dom::quota { + +class DecryptingInputStreamBase : public nsIInputStream, + public nsISeekableStream, + public nsICloneableInputStream, + public nsIIPCSerializableInputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + NS_IMETHOD Read(char* aBuf, uint32_t aCount, uint32_t* _retval) final; + NS_IMETHOD IsNonBlocking(bool* _retval) final; + + NS_IMETHOD SetEOF() final; + + using nsICloneableInputStream::GetCloneable; + NS_IMETHOD GetCloneable(bool* aCloneable) final; + + void SerializedComplexity(uint32_t aMaxSize, uint32_t* aSizeUsed, + uint32_t* aPipes, + uint32_t* aTransferables) override; + + protected: + DecryptingInputStreamBase(MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream, + size_t aBlockSize); + + // For deserialization only. + DecryptingInputStreamBase() = default; + + virtual ~DecryptingInputStreamBase() = default; + + void Init(MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream, + size_t aBlockSize); + + // Convenience routine to determine how many bytes of plain data + // we currently have in our buffer. + size_t PlainLength() const; + + size_t EncryptedBufferLength() const; + + LazyInitializedOnceEarlyDestructible<const NotNull<nsCOMPtr<nsIInputStream>>> + mBaseStream; + LazyInitializedOnce<const NotNull<nsISeekableStream*>> mBaseSeekableStream; + LazyInitializedOnce<const NotNull<nsICloneableInputStream*>> + mBaseCloneableInputStream; + LazyInitializedOnce<const NotNull<nsIIPCSerializableInputStream*>> + mBaseIPCSerializableInputStream; + + // Number of bytes of plain data in mBuffer. + size_t mPlainBytes = 0; + + // Next byte of mBuffer to return in ReadSegments(). + size_t mNextByte = 0; + + LazyInitializedOnceNotNull<const size_t> mBlockSize; + + size_t mLastBlockLength = 0; +}; + +// Wraps another nsIInputStream which contains data written using +// EncryptingInputStream with a compatible CipherStategy and key. See the +// remarks on EncryptingOutputStream. +template <typename CipherStrategy> +class DecryptingInputStream final : public DecryptingInputStreamBase { + public: + // Construct a new blocking stream to decrypt the given base stream. The + // base stream must also be blocking. The base stream does not have to be + // buffered. + DecryptingInputStream(MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream, + size_t aBlockSize, + typename CipherStrategy::KeyType aKey); + + // For deserialization only. + explicit DecryptingInputStream(); + + NS_IMETHOD Close() override; + NS_IMETHOD Available(uint64_t* _retval) override; + NS_IMETHOD ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) override; + + NS_DECL_NSITELLABLESTREAM + + NS_IMETHOD Seek(int32_t aWhence, int64_t aOffset) override; + + NS_IMETHOD Clone(nsIInputStream** _retval) override; + + void Serialize(mozilla::ipc::InputStreamParams& aParams, uint32_t aMaxSize, + uint32_t* aSizeUsed) override; + + bool Deserialize(const mozilla::ipc::InputStreamParams& aParams) override; + + private: + ~DecryptingInputStream(); + + // Parse the next chunk of data. This may populate mBuffer and set + // mBufferFillSize. This should not be called when mBuffer already + // contains data. + nsresult ParseNextChunk(uint32_t* aBytesReadOut); + + // Convenience routine to Read() from the base stream until we get + // the given number of bytes or reach EOF. + // + // aBuf - The buffer to write the bytes into. + // aCount - Max number of bytes to read. If the stream closes + // fewer bytes my be read. + // aMinValidCount - A minimum expected number of bytes. If we find + // fewer than this many bytes, then return + // NS_ERROR_CORRUPTED_CONTENT. If nothing was read due + // due to EOF (aBytesReadOut == 0), then NS_OK is returned. + // aBytesReadOut - An out parameter indicating how many bytes were read. + nsresult ReadAll(char* aBuf, uint32_t aCount, uint32_t aMinValidCount, + uint32_t* aBytesReadOut); + + bool EnsureBuffers(); + + CipherStrategy mCipherStrategy; + LazyInitializedOnce<const typename CipherStrategy::KeyType> mKey; + + // Buffer to hold encrypted data. Must copy here since we need a + // flat buffer to run the decryption process on. + using EncryptedBlockType = EncryptedBlock<CipherStrategy::BlockPrefixLength, + CipherStrategy::BasicBlockSize>; + Maybe<EncryptedBlockType> mEncryptedBlock; + + // Buffer storing the resulting plain data. + nsTArray<uint8_t> mPlainBuffer; +}; + +} // namespace mozilla::dom::quota + +#endif diff --git a/dom/quota/DecryptingInputStream_impl.h b/dom/quota/DecryptingInputStream_impl.h new file mode 100644 index 0000000000..ed5353d6ae --- /dev/null +++ b/dom/quota/DecryptingInputStream_impl.h @@ -0,0 +1,520 @@ +/* -*- 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_quota_DecryptingInputStream_impl_h +#define mozilla_dom_quota_DecryptingInputStream_impl_h + +#include "DecryptingInputStream.h" + +#include <algorithm> +#include <cstdio> +#include <type_traits> +#include <utility> +#include "CipherStrategy.h" +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Span.h" +#include "mozilla/fallible.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsFileStreams.h" +#include "nsID.h" +#include "nsIFileStreams.h" + +namespace mozilla::dom::quota { + +template <typename CipherStrategy> +DecryptingInputStream<CipherStrategy>::DecryptingInputStream( + MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream, size_t aBlockSize, + typename CipherStrategy::KeyType aKey) + : DecryptingInputStreamBase(std::move(aBaseStream), aBlockSize), + mKey(aKey) { + // XXX Move this to a fallible init function. + MOZ_ALWAYS_SUCCEEDS(mCipherStrategy.Init(CipherMode::Decrypt, + CipherStrategy::SerializeKey(aKey))); + + // This implementation only supports sync base streams. Verify this in debug + // builds. +#ifdef DEBUG + bool baseNonBlocking; + nsresult rv = (*mBaseStream)->IsNonBlocking(&baseNonBlocking); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(!baseNonBlocking); +#endif +} + +template <typename CipherStrategy> +DecryptingInputStream<CipherStrategy>::~DecryptingInputStream() { + Close(); +} + +template <typename CipherStrategy> +DecryptingInputStream<CipherStrategy>::DecryptingInputStream() + : DecryptingInputStreamBase{} {} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Close() { + if (!mBaseStream) { + return NS_OK; + } + + (*mBaseStream)->Close(); + mBaseStream.destroy(); + + mPlainBuffer.Clear(); + mEncryptedBlock.reset(); + + return NS_OK; +} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Available( + uint64_t* aLengthOut) { + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + int64_t oldPos, endPos; + nsresult rv = Tell(&oldPos); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = Seek(SEEK_END, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = Tell(&endPos); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = Seek(SEEK_SET, oldPos); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aLengthOut = endPos - oldPos; + return NS_OK; +} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::ReadSegments( + nsWriteSegmentFun aWriter, void* aClosure, uint32_t aCount, + uint32_t* aBytesReadOut) { + *aBytesReadOut = 0; + + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + nsresult rv; + + // Do not try to use the base stream's ReadSegments here. Its very + // unlikely we will get a single buffer that contains all of the encrypted + // data and therefore would have to copy into our own buffer anyways. + // Instead, focus on making efficient use of the Read() interface. + + while (aCount > 0) { + // We have some decrypted data in our buffer. Provide it to the callers + // writer function. + if (mPlainBytes > 0) { + MOZ_ASSERT(!mPlainBuffer.IsEmpty()); + uint32_t remaining = PlainLength(); + uint32_t numToWrite = std::min(aCount, remaining); + uint32_t numWritten; + rv = aWriter(this, aClosure, + reinterpret_cast<const char*>(&mPlainBuffer[mNextByte]), + *aBytesReadOut, numToWrite, &numWritten); + + // As defined in nsIInputputStream.idl, do not pass writer func errors. + if (NS_FAILED(rv)) { + return NS_OK; + } + + // End-of-file + if (numWritten == 0) { + return NS_OK; + } + + *aBytesReadOut += numWritten; + mNextByte += numWritten; + MOZ_ASSERT(mNextByte <= mPlainBytes); + + if (mNextByte == mPlainBytes) { + mNextByte = 0; + mLastBlockLength = mPlainBytes; + mPlainBytes = 0; + } + + aCount -= numWritten; + + continue; + } + + // Otherwise decrypt the next chunk and loop. Any resulting data + // will set mPlainBytes which we check at the top of the loop. + uint32_t bytesRead; + rv = ParseNextChunk(&bytesRead); + if (NS_FAILED(rv)) { + return rv; + } + + // If we couldn't read anything and there is no more data to provide + // to the caller, then this is eof. + if (bytesRead == 0 && mPlainBytes == 0) { + return NS_OK; + } + + mPlainBytes += bytesRead; + } + + return NS_OK; +} + +template <typename CipherStrategy> +nsresult DecryptingInputStream<CipherStrategy>::ParseNextChunk( + uint32_t* const aBytesReadOut) { + // There must not be any plain data already in mPlainBuffer. + MOZ_ASSERT(mPlainBytes == 0); + MOZ_ASSERT(mNextByte == 0); + + *aBytesReadOut = 0; + + if (!EnsureBuffers()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Read the data to our internal encrypted buffer. + auto wholeBlock = mEncryptedBlock->MutableWholeBlock(); + nsresult rv = + ReadAll(AsWritableChars(wholeBlock).Elements(), wholeBlock.Length(), + wholeBlock.Length(), aBytesReadOut); + if (NS_WARN_IF(NS_FAILED(rv)) || *aBytesReadOut == 0) { + return rv; + } + + // XXX Do we need to know the actual decrypted size? + rv = mCipherStrategy.Cipher(mEncryptedBlock->MutableCipherPrefix(), + mEncryptedBlock->Payload(), + AsWritableBytes(Span{mPlainBuffer})); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aBytesReadOut = mEncryptedBlock->ActualPayloadLength(); + + return NS_OK; +} + +template <typename CipherStrategy> +nsresult DecryptingInputStream<CipherStrategy>::ReadAll( + char* aBuf, uint32_t aCount, uint32_t aMinValidCount, + uint32_t* aBytesReadOut) { + MOZ_ASSERT(aCount >= aMinValidCount); + MOZ_ASSERT(mBaseStream); + + *aBytesReadOut = 0; + + uint32_t offset = 0; + while (aCount > 0) { + uint32_t bytesRead = 0; + nsresult rv = (*mBaseStream)->Read(aBuf + offset, aCount, &bytesRead); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // EOF, but don't immediately return. We need to validate min read bytes + // below. + if (bytesRead == 0) { + break; + } + + *aBytesReadOut += bytesRead; + offset += bytesRead; + aCount -= bytesRead; + } + + // Reading zero bytes is not an error. Its the expected EOF condition. + // Only compare to the minimum valid count if we read at least one byte. + if (*aBytesReadOut != 0 && *aBytesReadOut < aMinValidCount) { + return NS_ERROR_CORRUPTED_CONTENT; + } + + return NS_OK; +} + +template <typename CipherStrategy> +bool DecryptingInputStream<CipherStrategy>::EnsureBuffers() { + // Lazily create our two buffers so we can report OOM during stream + // operation. These allocations only happens once. The buffers are reused + // until the stream is closed. + if (!mEncryptedBlock) { + // XXX Do we need to do this fallible (as the comment above suggests)? + mEncryptedBlock.emplace(*mBlockSize); + + MOZ_ASSERT(mPlainBuffer.IsEmpty()); + if (NS_WARN_IF(!mPlainBuffer.SetLength(mEncryptedBlock->MaxPayloadLength(), + fallible))) { + return false; + } + } + + return true; +} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Tell( + int64_t* const aRetval) { + MOZ_ASSERT(aRetval); + + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + if (!EnsureBuffers()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + int64_t basePosition; + nsresult rv = (*mBaseSeekableStream)->Tell(&basePosition); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + const auto fullBlocks = basePosition / *mBlockSize; + MOZ_ASSERT(0 == basePosition % *mBlockSize); + + *aRetval = (fullBlocks - ((mPlainBytes || mLastBlockLength) ? 1 : 0)) * + mEncryptedBlock->MaxPayloadLength() + + mNextByte + (mNextByte ? 0 : mLastBlockLength); + return NS_OK; +} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Seek(const int32_t aWhence, + int64_t aOffset) { + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + if (!EnsureBuffers()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + int64_t baseBlocksOffset; + int64_t nextByteOffset; + switch (aWhence) { + case NS_SEEK_CUR: + // XXX Simplify this without using Tell. + { + int64_t current; + nsresult rv = Tell(¤t); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aOffset += current; + } + break; + case NS_SEEK_SET: + break; + + case NS_SEEK_END: + // XXX Simplify this without using Seek/Tell. + { + // XXX The size of the stream could also be queried and stored once + // only. + nsresult rv = (*mBaseSeekableStream)->Seek(NS_SEEK_SET, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint64_t baseStreamSize; + rv = (*mBaseStream)->Available(&baseStreamSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + auto decryptedStreamSizeOrErr = [baseStreamSize, + this]() -> Result<int64_t, nsresult> { + if (!baseStreamSize) { + return 0; + } + + nsresult rv = + (*mBaseSeekableStream) + ->Seek(NS_SEEK_END, -static_cast<int64_t>(*mBlockSize)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err(rv); + } + + mNextByte = 0; + mPlainBytes = 0; + + uint32_t bytesRead; + rv = ParseNextChunk(&bytesRead); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err(rv); + } + MOZ_ASSERT(bytesRead); + + // XXX Shouldn't ParseNextChunk better update mPlainBytes? + mPlainBytes = bytesRead; + + mNextByte = bytesRead; + + int64_t current; + rv = Tell(¤t); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err(rv); + } + + return current; + }(); + + if (decryptedStreamSizeOrErr.isErr()) { + return decryptedStreamSizeOrErr.unwrapErr(); + } + + aOffset += decryptedStreamSizeOrErr.unwrap(); + } + break; + + default: + return NS_ERROR_ILLEGAL_VALUE; + } + + baseBlocksOffset = aOffset / mEncryptedBlock->MaxPayloadLength(); + nextByteOffset = aOffset % mEncryptedBlock->MaxPayloadLength(); + + // XXX If we remain in the same block as before, we can skip this. + nsresult rv = + (*mBaseSeekableStream)->Seek(NS_SEEK_SET, baseBlocksOffset * *mBlockSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mNextByte = 0; + mPlainBytes = 0; + + uint32_t readBytes; + rv = ParseNextChunk(&readBytes); + if (NS_WARN_IF(NS_FAILED(rv))) { + // XXX Do we need to do more here? Restore any previous state? + return rv; + } + + // We positioned after the last block, we must read that to know its size. + // XXX We could know earlier if we positioned us after the last block. + if (!readBytes) { + if (baseBlocksOffset == 0) { + // The stream is empty. + return aOffset == 0 ? NS_OK : NS_ERROR_ILLEGAL_VALUE; + } + + nsresult rv = (*mBaseSeekableStream)->Seek(NS_SEEK_CUR, -*mBlockSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = ParseNextChunk(&readBytes); + if (NS_WARN_IF(NS_FAILED(rv))) { + // XXX Do we need to do more here? Restore any previous state? + return rv; + } + } + + mPlainBytes = readBytes; + mNextByte = nextByteOffset; + + return NS_OK; +} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Clone( + nsIInputStream** _retval) { + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + if (!(*mBaseCloneableInputStream)->GetCloneable()) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIInputStream> clonedStream; + nsresult rv = + (*mBaseCloneableInputStream)->Clone(getter_AddRefs(clonedStream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *_retval = MakeAndAddRef<DecryptingInputStream>( + WrapNotNull(std::move(clonedStream)), *mBlockSize, *mKey) + .take(); + + return NS_OK; +} + +template <typename CipherStrategy> +void DecryptingInputStream<CipherStrategy>::Serialize( + mozilla::ipc::InputStreamParams& aParams, uint32_t aMaxSize, + uint32_t* aSizeUsed) { + MOZ_ASSERT(mBaseStream); + MOZ_ASSERT(mBaseIPCSerializableInputStream); + + mozilla::ipc::InputStreamParams baseStreamParams; + (*mBaseIPCSerializableInputStream) + ->Serialize(baseStreamParams, aMaxSize, aSizeUsed); + + MOZ_ASSERT(baseStreamParams.type() == + mozilla::ipc::InputStreamParams::TFileInputStreamParams); + + mozilla::ipc::EncryptedFileInputStreamParams encryptedFileInputStreamParams; + encryptedFileInputStreamParams.fileInputStreamParams() = + std::move(baseStreamParams); + encryptedFileInputStreamParams.key().AppendElements( + mCipherStrategy.SerializeKey(*mKey)); + encryptedFileInputStreamParams.blockSize() = *mBlockSize; + + aParams = std::move(encryptedFileInputStreamParams); +} + +template <typename CipherStrategy> +bool DecryptingInputStream<CipherStrategy>::Deserialize( + const mozilla::ipc::InputStreamParams& aParams) { + MOZ_ASSERT(aParams.type() == + mozilla::ipc::InputStreamParams::TEncryptedFileInputStreamParams); + const auto& params = aParams.get_EncryptedFileInputStreamParams(); + + nsCOMPtr<nsIFileInputStream> stream; + nsFileInputStream::Create(NS_GET_IID(nsIFileInputStream), + getter_AddRefs(stream)); + nsCOMPtr<nsIIPCSerializableInputStream> baseSerializable = + do_QueryInterface(stream); + + if (NS_WARN_IF( + !baseSerializable->Deserialize(params.fileInputStreamParams()))) { + return false; + } + + Init(WrapNotNull<nsCOMPtr<nsIInputStream>>(std::move(stream)), + params.blockSize()); + mKey.init(mCipherStrategy.DeserializeKey(params.key())); + if (NS_WARN_IF( + NS_FAILED(mCipherStrategy.Init(CipherMode::Decrypt, params.key())))) { + return false; + } + + return true; +} + +} // namespace mozilla::dom::quota + +#endif diff --git a/dom/quota/DirectoryLock.h b/dom/quota/DirectoryLock.h new file mode 100644 index 0000000000..b35f74b47e --- /dev/null +++ b/dom/quota/DirectoryLock.h @@ -0,0 +1,100 @@ +/* -*- 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_QUOTA_DIRECTORYLOCK_H_ +#define DOM_QUOTA_DIRECTORYLOCK_H_ + +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/PersistenceType.h" + +namespace mozilla::dom::quota { + +class ClientDirectoryLock; +class OpenDirectoryListener; +struct OriginMetadata; + +// Basic directory lock interface shared by all other directory lock classes. +// The class must contain pure virtual functions only to avoid problems with +// multiple inheritance. +class NS_NO_VTABLE DirectoryLock { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual int64_t Id() const = 0; + + // XXX This method is now deprecated, use the one which returns the + // `BoolPromise` + virtual void Acquire(RefPtr<OpenDirectoryListener> aOpenListener) = 0; + + virtual RefPtr<BoolPromise> Acquire() = 0; + + virtual void AcquireImmediately() = 0; + + virtual void AssertIsAcquiredExclusively() = 0; + + virtual void Log() const = 0; +}; + +// A directory lock specialized for a given origin directory. +class NS_NO_VTABLE OriginDirectoryLock : public DirectoryLock { + public: + // 'Get' prefix is to avoid name collisions with the enum + virtual PersistenceType GetPersistenceType() const = 0; + + virtual quota::OriginMetadata OriginMetadata() const = 0; + + virtual const nsACString& Origin() const = 0; +}; + +// A directory lock specialized for a given client directory (inside an origin +// directory). +class NS_NO_VTABLE ClientDirectoryLock : public OriginDirectoryLock { + public: + virtual Client::Type ClientType() const = 0; +}; + +// A directory lock for universal use. A universal lock can handle any possible +// combination of nullable persistence type, origin scope and nullable client +// type. +// +// For example, if the persistence type is set to null, origin scope is null +// and the client type is set to Client::IDB, then the lock will cover +// <profile>/storage/*/*/idb +// +// If no property is set, then the lock will cover the entire storage directory +// and its subdirectories. +class UniversalDirectoryLock : public DirectoryLock { + public: + // XXX Rename to NullablePersistenceTypeRef. + virtual const Nullable<PersistenceType>& NullablePersistenceType() const = 0; + + // XXX Rename to OriginScopeRef. + virtual const OriginScope& GetOriginScope() const = 0; + + // XXX Rename to NullableClientTypeRef. + virtual const Nullable<Client::Type>& NullableClientType() const = 0; + + virtual RefPtr<ClientDirectoryLock> SpecializeForClient( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + Client::Type aClientType) const = 0; +}; + +class NS_NO_VTABLE OpenDirectoryListener { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void DirectoryLockAcquired(DirectoryLock* aLock) = 0; + + virtual void DirectoryLockFailed() = 0; + + protected: + virtual ~OpenDirectoryListener() = default; +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_DIRECTORYLOCK_H_ diff --git a/dom/quota/DirectoryLockImpl.cpp b/dom/quota/DirectoryLockImpl.cpp new file mode 100644 index 0000000000..05c210d195 --- /dev/null +++ b/dom/quota/DirectoryLockImpl.cpp @@ -0,0 +1,372 @@ +/* -*- 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 "DirectoryLockImpl.h" + +#include "mozilla/ReverseIterator.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/QuotaManager.h" + +namespace mozilla::dom::quota { + +DirectoryLockImpl::DirectoryLockImpl( + MovingNotNull<RefPtr<QuotaManager>> aQuotaManager, + const Nullable<PersistenceType>& aPersistenceType, + const nsACString& aSuffix, const nsACString& aGroup, + const OriginScope& aOriginScope, const Nullable<Client::Type>& aClientType, + const bool aExclusive, const bool aInternal, + const ShouldUpdateLockIdTableFlag aShouldUpdateLockIdTableFlag) + : mQuotaManager(std::move(aQuotaManager)), + mPersistenceType(aPersistenceType), + mSuffix(aSuffix), + mGroup(aGroup), + mOriginScope(aOriginScope), + mClientType(aClientType), + mId(mQuotaManager->GenerateDirectoryLockId()), + mExclusive(aExclusive), + mInternal(aInternal), + mShouldUpdateLockIdTable(aShouldUpdateLockIdTableFlag == + ShouldUpdateLockIdTableFlag::Yes), + mRegistered(false) { + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(aOriginScope.IsOrigin(), !aOriginScope.GetOrigin().IsEmpty()); + MOZ_ASSERT_IF(!aInternal, !aPersistenceType.IsNull()); + MOZ_ASSERT_IF(!aInternal, + aPersistenceType.Value() != PERSISTENCE_TYPE_INVALID); + MOZ_ASSERT_IF(!aInternal, !aGroup.IsEmpty()); + MOZ_ASSERT_IF(!aInternal, aOriginScope.IsOrigin()); + MOZ_ASSERT_IF(!aInternal, !aClientType.IsNull()); + MOZ_ASSERT_IF(!aInternal, aClientType.Value() < Client::TypeMax()); +} + +DirectoryLockImpl::~DirectoryLockImpl() { + AssertIsOnOwningThread(); + + // We must call UnregisterDirectoryLock before unblocking other locks because + // UnregisterDirectoryLock also updates the origin last access time and the + // access flag (if the last lock for given origin is unregistered). One of the + // blocked locks could be requested by the clear/reset operation which stores + // cached information about origins in storage.sqlite. So if the access flag + // is not updated before unblocking the lock for reset/clear, we might store + // invalid information which can lead to omitting origin initialization during + // next temporary storage initialization. + if (mRegistered) { + mQuotaManager->UnregisterDirectoryLock(*this); + } + + MOZ_ASSERT(!mRegistered); + + for (NotNull<RefPtr<DirectoryLockImpl>> blockingLock : mBlocking) { + blockingLock->MaybeUnblock(*this); + } + + mBlocking.Clear(); +} + +#ifdef DEBUG + +void DirectoryLockImpl::AssertIsOnOwningThread() const { + mQuotaManager->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +bool DirectoryLockImpl::Overlaps(const DirectoryLockImpl& aLock) const { + AssertIsOnOwningThread(); + + // If the persistence types don't overlap, the op can proceed. + if (!aLock.mPersistenceType.IsNull() && !mPersistenceType.IsNull() && + aLock.mPersistenceType.Value() != mPersistenceType.Value()) { + return false; + } + + // If the origin scopes don't overlap, the op can proceed. + bool match = aLock.mOriginScope.Matches(mOriginScope); + if (!match) { + return false; + } + + // If the client types don't overlap, the op can proceed. + if (!aLock.mClientType.IsNull() && !mClientType.IsNull() && + aLock.mClientType.Value() != mClientType.Value()) { + return false; + } + + // Otherwise, when all attributes overlap (persistence type, origin scope and + // client type) the op must wait. + return true; +} + +bool DirectoryLockImpl::MustWaitFor(const DirectoryLockImpl& aLock) const { + AssertIsOnOwningThread(); + + // Waiting is never required if the ops in comparison represent shared locks. + if (!aLock.mExclusive && !mExclusive) { + return false; + } + + // Wait if the ops overlap. + return Overlaps(aLock); +} + +void DirectoryLockImpl::NotifyOpenListener() { + AssertIsOnOwningThread(); + + if (mInvalidated) { + if (mOpenListener) { + (*mOpenListener)->DirectoryLockFailed(); + } else { + mAcquirePromiseHolder.Reject(NS_ERROR_FAILURE, __func__); + } + } else { +#ifdef DEBUG + mAcquired.Flip(); +#endif + + if (mOpenListener) { + (*mOpenListener) + ->DirectoryLockAcquired(static_cast<UniversalDirectoryLock*>(this)); + } else { + mAcquirePromiseHolder.Resolve(true, __func__); + } + } + + if (mOpenListener) { + mOpenListener.destroy(); + } else { + MOZ_ASSERT(mAcquirePromiseHolder.IsEmpty()); + } + + mQuotaManager->RemovePendingDirectoryLock(*this); + + mPending.Flip(); +} + +void DirectoryLockImpl::Acquire(RefPtr<OpenDirectoryListener> aOpenListener) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aOpenListener); + + mOpenListener.init(WrapNotNullUnchecked(std::move(aOpenListener))); + + AcquireInternal(); +} + +RefPtr<BoolPromise> DirectoryLockImpl::Acquire() { + AssertIsOnOwningThread(); + + RefPtr<BoolPromise> result = mAcquirePromiseHolder.Ensure(__func__); + + AcquireInternal(); + + return result; +} + +void DirectoryLockImpl::AcquireInternal() { + AssertIsOnOwningThread(); + + mQuotaManager->AddPendingDirectoryLock(*this); + + // See if this lock needs to wait. + bool blocked = false; + + // XXX It is probably unnecessary to iterate this in reverse order. + for (DirectoryLockImpl* const existingLock : + Reversed(mQuotaManager->mDirectoryLocks)) { + if (MustWaitFor(*existingLock)) { + existingLock->AddBlockingLock(*this); + AddBlockedOnLock(*existingLock); + blocked = true; + } + } + + mQuotaManager->RegisterDirectoryLock(*this); + + // Otherwise, notify the open listener immediately. + if (!blocked) { + NotifyOpenListener(); + return; + } + + if (!mExclusive || !mInternal) { + return; + } + + // All the locks that block this new exclusive internal lock need to be + // invalidated. We also need to notify clients to abort operations for them. + QuotaManager::DirectoryLockIdTableArray lockIds; + lockIds.SetLength(Client::TypeMax()); + + const auto& blockedOnLocks = GetBlockedOnLocks(); + MOZ_ASSERT(!blockedOnLocks.IsEmpty()); + + for (DirectoryLockImpl* blockedOnLock : blockedOnLocks) { + if (!blockedOnLock->IsInternal()) { + blockedOnLock->Invalidate(); + + // Clients don't have to handle pending locks. Invalidation is sufficient + // in that case (once a lock is ready and the listener needs to be + // notified, we will call DirectoryLockFailed instead of + // DirectoryLockAcquired which should release any remaining references to + // the lock). + if (!blockedOnLock->IsPending()) { + lockIds[blockedOnLock->ClientType()].Put(blockedOnLock->Id()); + } + } + } + + mQuotaManager->AbortOperationsForLocks(lockIds); +} + +void DirectoryLockImpl::AcquireImmediately() { + AssertIsOnOwningThread(); + +#ifdef DEBUG + for (const DirectoryLockImpl* const existingLock : + mQuotaManager->mDirectoryLocks) { + MOZ_ASSERT(!MustWaitFor(*existingLock)); + } +#endif + + mQuotaManager->RegisterDirectoryLock(*this); +} + +#ifdef DEBUG +void DirectoryLockImpl::AssertIsAcquiredExclusively() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mBlockedOn.IsEmpty()); + MOZ_ASSERT(mExclusive); + MOZ_ASSERT(mInternal); + MOZ_ASSERT(mRegistered); + MOZ_ASSERT(!mInvalidated); + MOZ_ASSERT(mAcquired); + + bool found = false; + + for (const DirectoryLockImpl* const existingLock : + mQuotaManager->mDirectoryLocks) { + if (existingLock == this) { + MOZ_ASSERT(!found); + found = true; + } else if (existingLock->mAcquired) { + MOZ_ASSERT(false); + } + } + + MOZ_ASSERT(found); +} +#endif + +RefPtr<ClientDirectoryLock> DirectoryLockImpl::SpecializeForClient( + PersistenceType aPersistenceType, + const quota::OriginMetadata& aOriginMetadata, + Client::Type aClientType) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_INVALID); + MOZ_ASSERT(!aOriginMetadata.mGroup.IsEmpty()); + MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty()); + MOZ_ASSERT(aClientType < Client::TypeMax()); + MOZ_ASSERT(!mOpenListener); + MOZ_ASSERT(mAcquirePromiseHolder.IsEmpty()); + MOZ_ASSERT(mBlockedOn.IsEmpty()); + + if (NS_WARN_IF(mExclusive)) { + return nullptr; + } + + RefPtr<DirectoryLockImpl> lock = Create( + mQuotaManager, Nullable<PersistenceType>(aPersistenceType), + aOriginMetadata.mSuffix, aOriginMetadata.mGroup, + OriginScope::FromOrigin(aOriginMetadata.mOrigin), + Nullable<Client::Type>(aClientType), + /* aExclusive */ false, mInternal, ShouldUpdateLockIdTableFlag::Yes); + if (NS_WARN_IF(!Overlaps(*lock))) { + return nullptr; + } + +#ifdef DEBUG + for (DirectoryLockImpl* const existingLock : + Reversed(mQuotaManager->mDirectoryLocks)) { + if (existingLock != this && !existingLock->MustWaitFor(*this)) { + MOZ_ASSERT(!existingLock->MustWaitFor(*lock)); + } + } +#endif + + for (const auto& blockedLock : mBlocking) { + if (blockedLock->MustWaitFor(*lock)) { + lock->AddBlockingLock(*blockedLock); + blockedLock->AddBlockedOnLock(*lock); + } + } + + mQuotaManager->RegisterDirectoryLock(*lock); + + if (mInvalidated) { + lock->Invalidate(); + } + + return lock; +} + +void DirectoryLockImpl::Log() const { + AssertIsOnOwningThread(); + + if (!QM_LOG_TEST()) { + return; + } + + QM_LOG(("DirectoryLockImpl [%p]", this)); + + nsCString persistenceType; + if (mPersistenceType.IsNull()) { + persistenceType.AssignLiteral("null"); + } else { + persistenceType.Assign(PersistenceTypeToString(mPersistenceType.Value())); + } + QM_LOG((" mPersistenceType: %s", persistenceType.get())); + + QM_LOG((" mGroup: %s", mGroup.get())); + + nsCString originScope; + if (mOriginScope.IsOrigin()) { + originScope.AssignLiteral("origin:"); + originScope.Append(mOriginScope.GetOrigin()); + } else if (mOriginScope.IsPrefix()) { + originScope.AssignLiteral("prefix:"); + originScope.Append(mOriginScope.GetOriginNoSuffix()); + } else if (mOriginScope.IsPattern()) { + originScope.AssignLiteral("pattern:"); + // Can't call GetJSONPattern since it only works on the main thread. + } else { + MOZ_ASSERT(mOriginScope.IsNull()); + originScope.AssignLiteral("null"); + } + QM_LOG((" mOriginScope: %s", originScope.get())); + + const auto clientType = mClientType.IsNull() + ? nsAutoCString{"null"_ns} + : Client::TypeToText(mClientType.Value()); + QM_LOG((" mClientType: %s", clientType.get())); + + nsCString blockedOnString; + for (auto blockedOn : mBlockedOn) { + blockedOnString.Append( + nsPrintfCString(" [%p]", static_cast<void*>(blockedOn))); + } + QM_LOG((" mBlockedOn:%s", blockedOnString.get())); + + QM_LOG((" mExclusive: %d", mExclusive)); + + QM_LOG((" mInternal: %d", mInternal)); + + QM_LOG((" mInvalidated: %d", static_cast<bool>(mInvalidated))); + + for (auto blockedOn : mBlockedOn) { + blockedOn->Log(); + } +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/DirectoryLockImpl.h b/dom/quota/DirectoryLockImpl.h new file mode 100644 index 0000000000..818d1c81bd --- /dev/null +++ b/dom/quota/DirectoryLockImpl.h @@ -0,0 +1,273 @@ +/* -*- 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_QUOTA_DIRECTORYLOCKIMPL_H_ +#define DOM_QUOTA_DIRECTORYLOCKIMPL_H_ + +#include "mozilla/InitializedOnce.h" +#include "mozilla/MozPromise.h" +#include "mozilla/dom/FlippedOnce.h" +#include "mozilla/dom/quota/CommonMetadata.h" +#include "mozilla/dom/quota/DirectoryLock.h" +#include "mozilla/dom/quota/OriginScope.h" + +namespace mozilla::dom::quota { + +enum class ShouldUpdateLockIdTableFlag { No, Yes }; + +class DirectoryLockImpl final : public ClientDirectoryLock, + public UniversalDirectoryLock { + const NotNull<RefPtr<QuotaManager>> mQuotaManager; + + const Nullable<PersistenceType> mPersistenceType; + const nsCString mSuffix; + const nsCString mGroup; + const OriginScope mOriginScope; + const Nullable<Client::Type> mClientType; + LazyInitializedOnceEarlyDestructible< + const NotNull<RefPtr<OpenDirectoryListener>>> + mOpenListener; + MozPromiseHolder<BoolPromise> mAcquirePromiseHolder; + + nsTArray<NotNull<DirectoryLockImpl*>> mBlocking; + nsTArray<NotNull<DirectoryLockImpl*>> mBlockedOn; + + const int64_t mId; + + const bool mExclusive; + + // Internal quota manager operations use this flag to prevent directory lock + // registraction/unregistration from updating origin access time, etc. + const bool mInternal; + + const bool mShouldUpdateLockIdTable; + + bool mRegistered; + FlippedOnce<true> mPending; + FlippedOnce<false> mInvalidated; + +#ifdef DEBUG + FlippedOnce<false> mAcquired; +#endif + + public: + DirectoryLockImpl(MovingNotNull<RefPtr<QuotaManager>> aQuotaManager, + const Nullable<PersistenceType>& aPersistenceType, + const nsACString& aSuffix, const nsACString& aGroup, + const OriginScope& aOriginScope, + const Nullable<Client::Type>& aClientType, bool aExclusive, + bool aInternal, + ShouldUpdateLockIdTableFlag aShouldUpdateLockIdTableFlag); + + static RefPtr<ClientDirectoryLock> Create( + MovingNotNull<RefPtr<QuotaManager>> aQuotaManager, + PersistenceType aPersistenceType, + const quota::OriginMetadata& aOriginMetadata, Client::Type aClientType, + bool aExclusive) { + return Create(std::move(aQuotaManager), + Nullable<PersistenceType>(aPersistenceType), + aOriginMetadata.mSuffix, aOriginMetadata.mGroup, + OriginScope::FromOrigin(aOriginMetadata.mOrigin), + Nullable<Client::Type>(aClientType), aExclusive, false, + ShouldUpdateLockIdTableFlag::Yes); + } + + static RefPtr<OriginDirectoryLock> CreateForEviction( + MovingNotNull<RefPtr<QuotaManager>> aQuotaManager, + PersistenceType aPersistenceType, + const quota::OriginMetadata& aOriginMetadata) { + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_INVALID); + MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty()); + + return Create(std::move(aQuotaManager), + Nullable<PersistenceType>(aPersistenceType), + aOriginMetadata.mSuffix, aOriginMetadata.mGroup, + OriginScope::FromOrigin(aOriginMetadata.mOrigin), + Nullable<Client::Type>(), + /* aExclusive */ true, /* aInternal */ true, + ShouldUpdateLockIdTableFlag::No); + } + + static RefPtr<UniversalDirectoryLock> CreateInternal( + MovingNotNull<RefPtr<QuotaManager>> aQuotaManager, + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, + const Nullable<Client::Type>& aClientType, bool aExclusive) { + return Create(std::move(aQuotaManager), aPersistenceType, ""_ns, ""_ns, + aOriginScope, aClientType, aExclusive, true, + ShouldUpdateLockIdTableFlag::Yes); + } + + void AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + bool IsInternal() const { return mInternal; } + + void SetRegistered(bool aRegistered) { mRegistered = aRegistered; } + + bool IsPending() const { return mPending; } + + // Ideally, we would have just one table (instead of these two: + // QuotaManager::mDirectoryLocks and QuotaManager::mDirectoryLockIdTable) for + // all registered locks. However, some directory locks need to be accessed off + // the PBackground thread, so the access must be protected by the quota mutex. + // The problem is that directory locks for eviction must be currently created + // while the mutex lock is already acquired. So we decided to have two tables + // for now and to not register directory locks for eviction in + // QuotaManager::mDirectoryLockIdTable. This can be improved in future after + // some refactoring of the mutex locking. + bool ShouldUpdateLockIdTable() const { return mShouldUpdateLockIdTable; } + + bool ShouldUpdateLockTable() { + return !mInternal && + mPersistenceType.Value() != PERSISTENCE_TYPE_PERSISTENT; + } + + bool Overlaps(const DirectoryLockImpl& aLock) const; + + // Test whether this DirectoryLock needs to wait for the given lock. + bool MustWaitFor(const DirectoryLockImpl& aLock) const; + + void AddBlockingLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + mBlocking.AppendElement(WrapNotNull(&aLock)); + } + + const nsTArray<NotNull<DirectoryLockImpl*>>& GetBlockedOnLocks() { + return mBlockedOn; + } + + void AddBlockedOnLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + mBlockedOn.AppendElement(WrapNotNull(&aLock)); + } + + void MaybeUnblock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + mBlockedOn.RemoveElement(&aLock); + if (mBlockedOn.IsEmpty()) { + NotifyOpenListener(); + } + } + + void NotifyOpenListener(); + + void Invalidate() { + AssertIsOnOwningThread(); + + mInvalidated.EnsureFlipped(); + } + + // DirectoryLock interface + + NS_INLINE_DECL_REFCOUNTING(DirectoryLockImpl, override) + + int64_t Id() const override { return mId; } + + void Acquire(RefPtr<OpenDirectoryListener> aOpenListener) override; + + RefPtr<BoolPromise> Acquire() override; + + void AcquireImmediately() override; + + void AssertIsAcquiredExclusively() override +#ifdef DEBUG + ; +#else + { + } +#endif + + void Log() const override; + + // OriginDirectoryLock interface + + PersistenceType GetPersistenceType() const override { + MOZ_DIAGNOSTIC_ASSERT(!mPersistenceType.IsNull()); + + return mPersistenceType.Value(); + } + + quota::OriginMetadata OriginMetadata() const override { + MOZ_DIAGNOSTIC_ASSERT(!mGroup.IsEmpty()); + + return quota::OriginMetadata{mSuffix, mGroup, nsCString(Origin()), + GetPersistenceType()}; + } + + const nsACString& Origin() const override { + MOZ_DIAGNOSTIC_ASSERT(mOriginScope.IsOrigin()); + MOZ_DIAGNOSTIC_ASSERT(!mOriginScope.GetOrigin().IsEmpty()); + + return mOriginScope.GetOrigin(); + } + + // ClientDirectoryLock interface + + Client::Type ClientType() const override { + MOZ_DIAGNOSTIC_ASSERT(!mClientType.IsNull()); + MOZ_DIAGNOSTIC_ASSERT(mClientType.Value() < Client::TypeMax()); + + return mClientType.Value(); + } + + // UniversalDirectoryLock interface + + const Nullable<PersistenceType>& NullablePersistenceType() const override { + return mPersistenceType; + } + + const OriginScope& GetOriginScope() const override { return mOriginScope; } + + const Nullable<Client::Type>& NullableClientType() const override { + return mClientType; + } + + RefPtr<ClientDirectoryLock> SpecializeForClient( + PersistenceType aPersistenceType, + const quota::OriginMetadata& aOriginMetadata, + Client::Type aClientType) const override; + + private: + ~DirectoryLockImpl(); + + static RefPtr<DirectoryLockImpl> Create( + MovingNotNull<RefPtr<QuotaManager>> aQuotaManager, + const Nullable<PersistenceType>& aPersistenceType, + const nsACString& aSuffix, const nsACString& aGroup, + const OriginScope& aOriginScope, + const Nullable<Client::Type>& aClientType, bool aExclusive, + bool aInternal, + ShouldUpdateLockIdTableFlag aShouldUpdateLockIdTableFlag) { + MOZ_ASSERT_IF(aOriginScope.IsOrigin(), !aOriginScope.GetOrigin().IsEmpty()); + MOZ_ASSERT_IF(!aInternal, !aPersistenceType.IsNull()); + MOZ_ASSERT_IF(!aInternal, + aPersistenceType.Value() != PERSISTENCE_TYPE_INVALID); + MOZ_ASSERT_IF(!aInternal, !aGroup.IsEmpty()); + MOZ_ASSERT_IF(!aInternal, aOriginScope.IsOrigin()); + MOZ_ASSERT_IF(!aInternal, !aClientType.IsNull()); + MOZ_ASSERT_IF(!aInternal, aClientType.Value() < Client::TypeMax()); + + return MakeRefPtr<DirectoryLockImpl>( + std::move(aQuotaManager), aPersistenceType, aSuffix, aGroup, + aOriginScope, aClientType, aExclusive, aInternal, + aShouldUpdateLockIdTableFlag); + } + + void AcquireInternal(); +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_DIRECTORYLOCKIMPL_H_ diff --git a/dom/quota/DummyCipherStrategy.h b/dom/quota/DummyCipherStrategy.h new file mode 100644 index 0000000000..6c6b7f7b0d --- /dev/null +++ b/dom/quota/DummyCipherStrategy.h @@ -0,0 +1,56 @@ +/* -*- 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_quota_DummyCipherStrategy_h +#define mozilla_dom_quota_DummyCipherStrategy_h + +#include <algorithm> +#include <array> +#include <cstddef> +#include <cstdint> +#include <utility> +#include "ErrorList.h" +#include "mozilla/Result.h" +#include "mozilla/Span.h" +#include "mozilla/dom/quota/CipherStrategy.h" + +namespace mozilla::dom::quota { + +struct DummyCipherStrategy { + struct KeyType {}; + + static constexpr size_t BlockPrefixLength = 8; + static constexpr size_t BasicBlockSize = 4; + + static void DummyTransform(Span<const uint8_t> aIn, Span<uint8_t> aOut) { + std::transform(aIn.cbegin(), aIn.cend(), aOut.begin(), + [](const uint8_t byte) { return byte ^ 42; }); + } + + static Result<KeyType, nsresult> GenerateKey() { return KeyType{}; } + + nsresult Init(CipherMode aCipherMode, Span<const uint8_t> aKey, + Span<const uint8_t> aInitialIv = Span<const uint8_t>{}) { + return NS_OK; + } + + nsresult Cipher(Span<uint8_t> aIv, Span<const uint8_t> aIn, + Span<uint8_t> aOut) { + DummyTransform(aIn, aOut); + return NS_OK; + } + + static std::array<uint8_t, BlockPrefixLength> MakeBlockPrefix() { + return {{42, 43, 44, 45}}; + } + + static Span<const uint8_t> SerializeKey(const KeyType&) { return {}; } + + static KeyType DeserializeKey(const Span<const uint8_t>&) { return {}; } +}; +} // namespace mozilla::dom::quota + +#endif diff --git a/dom/quota/EncryptedBlock.h b/dom/quota/EncryptedBlock.h new file mode 100644 index 0000000000..703fede8fd --- /dev/null +++ b/dom/quota/EncryptedBlock.h @@ -0,0 +1,93 @@ +/* -*- 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_quota_EncryptedBlock_h +#define mozilla_dom_quota_EncryptedBlock_h + +#include <cstdint> +#include <cstring> +#include <limits> +#include "mozilla/Assertions.h" +#include "mozilla/Span.h" +#include "nsTArray.h" + +namespace mozilla::dom::quota { + +// An encrypted block has the following format: +// - one basic block containing a uint16_t stating the actual payload length +// - one basic block containing the cipher prefix (tyipically a nonce) +// - encrypted payload up to the remainder of the specified overall size +// We currently assume the basic block size is the same as the cipher prefix +// length. +// +// XXX Actually, we don't need the actual payload length in every block. Only +// the last block may be incomplete. The tricky thing is just that it might be +// incomplete by just one or two bytes. +template <size_t CipherPrefixLength, size_t BasicBlockSize> +class EncryptedBlock { + public: + explicit EncryptedBlock(const size_t aOverallSize) { + MOZ_RELEASE_ASSERT(aOverallSize > + CipherPrefixOffset() + CipherPrefixLength); + MOZ_RELEASE_ASSERT(aOverallSize <= std::numeric_limits<uint16_t>::max()); + // XXX Do we need this to be fallible? Then we need a factory/init function. + // But maybe that's not necessary as the block size is not user-provided and + // small. + mData.SetLength(aOverallSize); + SetActualPayloadLength(MaxPayloadLength()); + } + + size_t MaxPayloadLength() const { + return mData.Length() - CipherPrefixLength - CipherPrefixOffset(); + } + + void SetActualPayloadLength(uint16_t aActualPayloadLength) { + memcpy(mData.Elements(), &aActualPayloadLength, sizeof(uint16_t)); + } + size_t ActualPayloadLength() const { + return *reinterpret_cast<const uint16_t*>(mData.Elements()); + } + + using ConstSpan = Span<const uint8_t>; + using MutableSpan = Span<uint8_t>; + + ConstSpan CipherPrefix() const { + return WholeBlock().Subspan(CipherPrefixOffset(), CipherPrefixLength); + } + MutableSpan MutableCipherPrefix() { + return MutableWholeBlock().Subspan(CipherPrefixOffset(), + CipherPrefixLength); + } + + ConstSpan Payload() const { + return WholeBlock() + .SplitAt(CipherPrefixOffset() + CipherPrefixLength) + .second.First(RoundedUpToBasicBlockSize(ActualPayloadLength())); + } + MutableSpan MutablePayload() { + return MutableWholeBlock() + .SplitAt(CipherPrefixOffset() + CipherPrefixLength) + .second.First(RoundedUpToBasicBlockSize(ActualPayloadLength())); + } + + ConstSpan WholeBlock() const { return mData; } + MutableSpan MutableWholeBlock() { return mData; } + + private: + static constexpr size_t CipherPrefixOffset() { + return RoundedUpToBasicBlockSize(sizeof(uint16_t)); + } + + static constexpr size_t RoundedUpToBasicBlockSize(const size_t aValue) { + return (aValue + BasicBlockSize - 1) / BasicBlockSize * BasicBlockSize; + } + + nsTArray<uint8_t> mData; ///< XXX use some "safe memory" here? +}; + +} // namespace mozilla::dom::quota + +#endif diff --git a/dom/quota/EncryptingOutputStream.cpp b/dom/quota/EncryptingOutputStream.cpp new file mode 100644 index 0000000000..80279eae27 --- /dev/null +++ b/dom/quota/EncryptingOutputStream.cpp @@ -0,0 +1,64 @@ +/* -*- 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 "EncryptingOutputStream.h" +#include "EncryptingOutputStream_impl.h" + +#include <type_traits> +#include "mozilla/MacroForEach.h" +#include "nsStreamUtils.h" + +namespace mozilla::dom::quota { + +NS_IMPL_ISUPPORTS(EncryptingOutputStreamBase, nsIOutputStream); + +EncryptingOutputStreamBase::EncryptingOutputStreamBase( + nsCOMPtr<nsIOutputStream> aBaseStream, size_t aBlockSize) + : mBaseStream(WrapNotNull(std::move(aBaseStream))), + mBlockSize(aBlockSize) {} + +NS_IMETHODIMP EncryptingOutputStreamBase::Write(const char* aBuf, + uint32_t aCount, + uint32_t* aResultOut) { + return WriteSegments(NS_CopyBufferToSegment, const_cast<char*>(aBuf), aCount, + aResultOut); +} + +NS_IMETHODIMP EncryptingOutputStreamBase::WriteFrom(nsIInputStream*, uint32_t, + uint32_t*) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP EncryptingOutputStreamBase::IsNonBlocking(bool* aNonBlockingOut) { + *aNonBlockingOut = false; + return NS_OK; +} + +nsresult EncryptingOutputStreamBase::WriteAll(const char* aBuf, uint32_t aCount, + uint32_t* aBytesWrittenOut) { + *aBytesWrittenOut = 0; + + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + uint32_t offset = 0; + while (aCount > 0) { + uint32_t numWritten = 0; + nsresult rv = (*mBaseStream)->Write(aBuf + offset, aCount, &numWritten); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + offset += numWritten; + aCount -= numWritten; + *aBytesWrittenOut += numWritten; + } + + return NS_OK; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/EncryptingOutputStream.h b/dom/quota/EncryptingOutputStream.h new file mode 100644 index 0000000000..adcfe73155 --- /dev/null +++ b/dom/quota/EncryptingOutputStream.h @@ -0,0 +1,103 @@ +/* -*- 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_quota_EncryptingOutputStream_h +#define mozilla_dom_quota_EncryptingOutputStream_h + +// Local includes +#include "EncryptedBlock.h" // for EncryptedBlock + +// Global includes +#include <cstddef> +#include <cstdint> +#include "ErrorList.h" +#include "mozilla/InitializedOnce.h" +#include "mozilla/Maybe.h" +#include "mozilla/NotNull.h" +#include "nsCOMPtr.h" +#include "nsIOutputStream.h" +#include "nsISupports.h" +#include "nsTArray.h" +#include "nscore.h" + +class nsIInputStream; + +namespace mozilla::dom::quota { +class EncryptingOutputStreamBase : public nsIOutputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + NS_IMETHOD Write(const char* aBuf, uint32_t aCount, uint32_t* _retval) final; + NS_IMETHOD WriteFrom(nsIInputStream* aFromStream, uint32_t aCount, + uint32_t* _retval) final; + NS_IMETHOD IsNonBlocking(bool* _retval) final; + + protected: + EncryptingOutputStreamBase(nsCOMPtr<nsIOutputStream> aBaseStream, + size_t aBlockSize); + + virtual ~EncryptingOutputStreamBase() = default; + + nsresult WriteAll(const char* aBuf, uint32_t aCount, + uint32_t* aBytesWrittenOut); + + InitializedOnce<const NotNull<nsCOMPtr<nsIOutputStream>>> mBaseStream; + const size_t mBlockSize; +}; + +// Wraps another nsIOutputStream using the CipherStrategy to encrypt it a +// page-based manner. Essentially, the CipherStrategy is not actually +// necessarily doing encryption, but any transformation to a page requiring some +// fixed-size reserved size per page. +// +// Paired with DecryptingInputStream which can be used to read the data written +// to the underlying stream, using the same (or more generally, a compatible) +// CipherStrategy, when created with the same key (assuming a symmetric cipher +// is being used; in principle, an asymmetric cipher would probably also work). +template <typename CipherStrategy> +class EncryptingOutputStream final : public EncryptingOutputStreamBase { + public: + // Construct a new blocking output stream to encrypt data to + // the given base stream. The base stream must also be blocking. + // The encryption block size may optionally be set to a value + // up to kMaxBlockSize. + explicit EncryptingOutputStream(nsCOMPtr<nsIOutputStream> aBaseStream, + size_t aBlockSize, + typename CipherStrategy::KeyType aKey); + + private: + ~EncryptingOutputStream(); + + nsresult FlushToBaseStream(); + + bool EnsureBuffers(); + + CipherStrategy mCipherStrategy; + + // Buffer holding copied plain data. This must be copied here + // so that the encryption can be performed on a single flat buffer. + // XXX This is only necessary if the data written doesn't contain a portion of + // effective block size at a block boundary. + nsTArray<uint8_t> mBuffer; + + // The next byte in the plain data to copy incoming data to. + size_t mNextByte = 0; + + // Buffer holding the resulting encrypted data. + using EncryptedBlockType = EncryptedBlock<CipherStrategy::BlockPrefixLength, + CipherStrategy::BasicBlockSize>; + Maybe<EncryptedBlockType> mEncryptedBlock; + + public: + NS_IMETHOD Close() override; + NS_IMETHOD Flush() override; + NS_IMETHOD WriteSegments(nsReadSegmentFun aReader, void* aClosure, + uint32_t aCount, uint32_t* _retval) override; +}; + +} // namespace mozilla::dom::quota + +#endif diff --git a/dom/quota/EncryptingOutputStream_impl.h b/dom/quota/EncryptingOutputStream_impl.h new file mode 100644 index 0000000000..25ba7a26a3 --- /dev/null +++ b/dom/quota/EncryptingOutputStream_impl.h @@ -0,0 +1,246 @@ +/* -*- 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_quota_EncryptingOutputStream_impl_h +#define mozilla_dom_quota_EncryptingOutputStream_impl_h + +#include "EncryptingOutputStream.h" + +#include <algorithm> +#include <utility> +#include "CipherStrategy.h" +#include "mozilla/Assertions.h" +#include "mozilla/Span.h" +#include "mozilla/fallible.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIAsyncOutputStream.h" + +namespace mozilla::dom::quota { +template <typename CipherStrategy> +EncryptingOutputStream<CipherStrategy>::EncryptingOutputStream( + nsCOMPtr<nsIOutputStream> aBaseStream, size_t aBlockSize, + typename CipherStrategy::KeyType aKey) + : EncryptingOutputStreamBase(std::move(aBaseStream), aBlockSize) { + // XXX Move this to a fallible init function. + MOZ_ALWAYS_SUCCEEDS(mCipherStrategy.Init(CipherMode::Encrypt, + CipherStrategy::SerializeKey(aKey), + CipherStrategy::MakeBlockPrefix())); + + MOZ_ASSERT(mBlockSize > 0); + MOZ_ASSERT(mBlockSize % CipherStrategy::BasicBlockSize == 0); + static_assert( + CipherStrategy::BlockPrefixLength % CipherStrategy::BasicBlockSize == 0); + + // This implementation only supports sync base streams. Verify this in debug + // builds. Note, this is a bit complicated because the streams we support + // advertise different capabilities: + // - nsFileOutputStream - blocking and sync + // - FixedBufferOutputStream - non-blocking and sync + // - nsPipeOutputStream - can be blocking, but provides async interface +#ifdef DEBUG + bool baseNonBlocking; + nsresult rv = (*mBaseStream)->IsNonBlocking(&baseNonBlocking); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (baseNonBlocking) { + nsCOMPtr<nsIAsyncOutputStream> async = + do_QueryInterface((*mBaseStream).get()); + MOZ_ASSERT(!async); + } +#endif +} + +template <typename CipherStrategy> +EncryptingOutputStream<CipherStrategy>::~EncryptingOutputStream() { + Close(); +} + +template <typename CipherStrategy> +NS_IMETHODIMP EncryptingOutputStream<CipherStrategy>::Close() { + if (!mBaseStream) { + return NS_OK; + } + + // When closing, flush to the base stream unconditionally, i.e. even if the + // buffer is not completely full. + nsresult rv = FlushToBaseStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // XXX Maybe this Flush call can be removed, since the base stream is closed + // afterwards anyway. + rv = (*mBaseStream)->Flush(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // XXX What if closing the base stream failed? Fail this method, or at least + // log a warning? + (*mBaseStream)->Close(); + mBaseStream.destroy(); + + mBuffer.Clear(); + mEncryptedBlock.reset(); + + return NS_OK; +} + +template <typename CipherStrategy> +NS_IMETHODIMP EncryptingOutputStream<CipherStrategy>::Flush() { + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + if (!EnsureBuffers()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // We cannot call FlushBaseStream() here if the buffer is not completely + // full, we would write an incomplete page, which might be read sequentially, + // but we want to support random accesses in DecryptingInputStream, which + // would no longer be feasible. + if (mNextByte && mNextByte == mEncryptedBlock->MaxPayloadLength()) { + nsresult rv = FlushToBaseStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return (*mBaseStream)->Flush(); +} + +template <typename CipherStrategy> +NS_IMETHODIMP EncryptingOutputStream<CipherStrategy>::WriteSegments( + nsReadSegmentFun aReader, void* aClosure, uint32_t aCount, + uint32_t* aBytesWrittenOut) { + *aBytesWrittenOut = 0; + + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + if (!EnsureBuffers()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + const size_t plainBufferSize = mEncryptedBlock->MaxPayloadLength(); + + while (aCount > 0) { + // Determine how much space is left in our flat, plain buffer. + MOZ_ASSERT(mNextByte <= plainBufferSize); + uint32_t remaining = plainBufferSize - mNextByte; + + // If it is full, then encrypt and flush the data to the base stream. + if (remaining == 0) { + nsresult rv = FlushToBaseStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Now the entire buffer should be available for copying. + MOZ_ASSERT(!mNextByte); + remaining = plainBufferSize; + } + + uint32_t numToRead = std::min(remaining, aCount); + uint32_t numRead = 0; + + nsresult rv = + aReader(this, aClosure, reinterpret_cast<char*>(&mBuffer[mNextByte]), + *aBytesWrittenOut, numToRead, &numRead); + + // As defined in nsIOutputStream.idl, do not pass reader func errors. + if (NS_FAILED(rv)) { + return NS_OK; + } + + // End-of-file + if (numRead == 0) { + return NS_OK; + } + + mNextByte += numRead; + *aBytesWrittenOut += numRead; + aCount -= numRead; + } + + return NS_OK; +} + +template <typename CipherStrategy> +bool EncryptingOutputStream<CipherStrategy>::EnsureBuffers() { + // Lazily create the encrypted buffer on our first flush. This + // allows us to report OOM during stream operation. This buffer + // will then get re-used until the stream is closed. + if (!mEncryptedBlock) { + // XXX Do we need to do this fallible (as the comment above suggests)? + mEncryptedBlock.emplace(mBlockSize); + MOZ_ASSERT(mBuffer.IsEmpty()); + + if (NS_WARN_IF(!mBuffer.SetLength(mEncryptedBlock->MaxPayloadLength(), + fallible))) { + return false; + } + } + + return true; +} + +template <typename CipherStrategy> +nsresult EncryptingOutputStream<CipherStrategy>::FlushToBaseStream() { + MOZ_ASSERT(mBaseStream); + + if (!mNextByte) { + // Nothing to do. + return NS_OK; + } + + // XXX The compressing stream implementation this was based on wrote a stream + // identifier, containing e.g. the block size. Should we do something like + // that as well? At the moment, we don't need it, but maybe this were + // convenient if we use this for persistent files in the future across version + // updates, which might change such parameters. + + const auto iv = mCipherStrategy.MakeBlockPrefix(); + static_assert(iv.size() * sizeof(decltype(*iv.begin())) == + CipherStrategy::BlockPrefixLength); + std::copy(iv.cbegin(), iv.cend(), + mEncryptedBlock->MutableCipherPrefix().begin()); + + // Encrypt the data to our internal encrypted buffer. + // XXX Do we need to know the actual encrypted size? + nsresult rv = mCipherStrategy.Cipher( + mEncryptedBlock->MutableCipherPrefix(), + mozilla::Span(reinterpret_cast<uint8_t*>(mBuffer.Elements()), + ((mNextByte + (CipherStrategy::BasicBlockSize - 1)) / + CipherStrategy::BasicBlockSize) * + CipherStrategy::BasicBlockSize), + mEncryptedBlock->MutablePayload()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mEncryptedBlock->SetActualPayloadLength(mNextByte); + + mNextByte = 0; + + // Write the encrypted buffer out to the base stream. + uint32_t numWritten = 0; + const auto& wholeBlock = mEncryptedBlock->WholeBlock(); + rv = WriteAll(AsChars(wholeBlock).Elements(), wholeBlock.Length(), + &numWritten); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + MOZ_ASSERT(wholeBlock.Length() == numWritten); + + return NS_OK; +} + +} // namespace mozilla::dom::quota + +#endif diff --git a/dom/quota/FileStreams.cpp b/dom/quota/FileStreams.cpp new file mode 100644 index 0000000000..22419fe70c --- /dev/null +++ b/dom/quota/FileStreams.cpp @@ -0,0 +1,200 @@ +/* -*- 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 "FileStreams.h" + +// Local includes +#include "QuotaCommon.h" +#include "QuotaManager.h" +#include "QuotaObject.h" +#include "RemoteQuotaObject.h" + +// Global includes +#include <utility> +#include "mozilla/Assertions.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Result.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/ipc/RandomAccessStreamParams.h" +#include "nsDebug.h" +#include "prio.h" + +namespace mozilla::dom::quota { + +template <class FileStreamBase> +NS_IMETHODIMP FileQuotaStream<FileStreamBase>::SetEOF() { + // If the stream is not quota tracked, or on an early or late stage in the + // lifecycle, mQuotaObject is null. Under these circumstances, + // we don't check the quota limit in order to avoid breakage. + if (mQuotaObject) { + int64_t offset = 0; + QM_TRY(MOZ_TO_RESULT(FileStreamBase::Tell(&offset))); + + QM_TRY(OkIf(mQuotaObject->MaybeUpdateSize(offset, /* aTruncate */ true)), + NS_ERROR_FILE_NO_DEVICE_SPACE); + } + + QM_TRY(MOZ_TO_RESULT(FileStreamBase::SetEOF())); + + return NS_OK; +} + +template <class FileStreamBase> +NS_IMETHODIMP FileQuotaStream<FileStreamBase>::Close() { + QM_TRY(MOZ_TO_RESULT(FileStreamBase::Close())); + + if (mQuotaObject) { + if (auto* remoteQuotaObject = mQuotaObject->AsRemoteQuotaObject()) { + remoteQuotaObject->Close(); + } + + mQuotaObject = nullptr; + } + + return NS_OK; +} + +template <class FileStreamBase> +nsresult FileQuotaStream<FileStreamBase>::DoOpen() { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + MOZ_RELEASE_ASSERT(!mDeserialized); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager, "Shouldn't be null!"); + + MOZ_ASSERT(!mQuotaObject, "Creating quota object more than once?"); + mQuotaObject = quotaManager->GetQuotaObject( + mPersistenceType, mOriginMetadata, mClientType, + FileStreamBase::mOpenParams.localFile); + + QM_TRY(MOZ_TO_RESULT(FileStreamBase::DoOpen())); + + if (mQuotaObject && (FileStreamBase::mOpenParams.ioFlags & PR_TRUNCATE)) { + DebugOnly<bool> res = + mQuotaObject->MaybeUpdateSize(0, /* aTruncate */ true); + MOZ_ASSERT(res); + } + + return NS_OK; +} + +template <class FileStreamBase> +NS_IMETHODIMP FileQuotaStreamWithWrite<FileStreamBase>::Write( + const char* aBuf, uint32_t aCount, uint32_t* _retval) { + if (FileQuotaStreamWithWrite::mQuotaObject) { + int64_t offset; + QM_TRY(MOZ_TO_RESULT(FileStreamBase::Tell(&offset))); + + MOZ_ASSERT(INT64_MAX - offset >= int64_t(aCount)); + + if (!FileQuotaStreamWithWrite::mQuotaObject->MaybeUpdateSize( + offset + int64_t(aCount), + /* aTruncate */ false)) { + *_retval = 0; + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + } + + QM_TRY(MOZ_TO_RESULT(FileStreamBase::Write(aBuf, aCount, _retval))); + + return NS_OK; +} + +mozilla::ipc::RandomAccessStreamParams FileRandomAccessStream::Serialize( + nsIInterfaceRequestor* aCallbacks) { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + MOZ_RELEASE_ASSERT(!mDeserialized); + MOZ_ASSERT(mOpenParams.localFile); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + RefPtr<QuotaObject> quotaObject = quotaManager->GetQuotaObject( + mPersistenceType, mOriginMetadata, mClientType, mOpenParams.localFile); + MOZ_ASSERT(quotaObject); + + IPCQuotaObject ipcQuotaObject = quotaObject->Serialize(aCallbacks); + + mozilla::ipc::RandomAccessStreamParams randomAccessStreamParams = + nsFileRandomAccessStream::Serialize(aCallbacks); + + MOZ_ASSERT( + randomAccessStreamParams.type() == + mozilla::ipc::RandomAccessStreamParams::TFileRandomAccessStreamParams); + + mozilla::ipc::LimitingFileRandomAccessStreamParams + limitingFileRandomAccessStreamParams; + limitingFileRandomAccessStreamParams.fileRandomAccessStreamParams() = + std::move(randomAccessStreamParams); + limitingFileRandomAccessStreamParams.quotaObject() = + std::move(ipcQuotaObject); + + return limitingFileRandomAccessStreamParams; +} + +bool FileRandomAccessStream::Deserialize( + mozilla::ipc::RandomAccessStreamParams& aParams) { + MOZ_ASSERT(aParams.type() == mozilla::ipc::RandomAccessStreamParams:: + TLimitingFileRandomAccessStreamParams); + + auto& params = aParams.get_LimitingFileRandomAccessStreamParams(); + + mozilla::ipc::RandomAccessStreamParams randomAccessStreamParams( + std::move(params.fileRandomAccessStreamParams())); + + QM_TRY(MOZ_TO_RESULT( + nsFileRandomAccessStream::Deserialize(randomAccessStreamParams)), + false); + + mQuotaObject = QuotaObject::Deserialize(params.quotaObject()); + + return true; +} + +Result<MovingNotNull<nsCOMPtr<nsIInputStream>>, nsresult> CreateFileInputStream( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + Client::Type aClientType, nsIFile* aFile, int32_t aIOFlags, int32_t aPerm, + int32_t aBehaviorFlags) { + auto stream = MakeRefPtr<FileInputStream>(aPersistenceType, aOriginMetadata, + aClientType); + + QM_TRY(MOZ_TO_RESULT(stream->Init(aFile, aIOFlags, aPerm, aBehaviorFlags))); + + return WrapMovingNotNullUnchecked( + nsCOMPtr<nsIInputStream>(std::move(stream))); +} + +Result<MovingNotNull<nsCOMPtr<nsIOutputStream>>, nsresult> +CreateFileOutputStream(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + Client::Type aClientType, nsIFile* aFile, + int32_t aIOFlags, int32_t aPerm, + int32_t aBehaviorFlags) { + auto stream = MakeRefPtr<FileOutputStream>(aPersistenceType, aOriginMetadata, + aClientType); + + QM_TRY(MOZ_TO_RESULT(stream->Init(aFile, aIOFlags, aPerm, aBehaviorFlags))); + + return WrapMovingNotNullUnchecked( + nsCOMPtr<nsIOutputStream>(std::move(stream))); +} + +Result<MovingNotNull<nsCOMPtr<nsIRandomAccessStream>>, nsresult> +CreateFileRandomAccessStream(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + Client::Type aClientType, nsIFile* aFile, + int32_t aIOFlags, int32_t aPerm, + int32_t aBehaviorFlags) { + auto stream = MakeRefPtr<FileRandomAccessStream>( + aPersistenceType, aOriginMetadata, aClientType); + + QM_TRY(MOZ_TO_RESULT(stream->Init(aFile, aIOFlags, aPerm, aBehaviorFlags))); + + return WrapMovingNotNullUnchecked( + nsCOMPtr<nsIRandomAccessStream>(std::move(stream))); +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/FileStreams.h b/dom/quota/FileStreams.h new file mode 100644 index 0000000000..9a91b474c5 --- /dev/null +++ b/dom/quota/FileStreams.h @@ -0,0 +1,176 @@ +/* -*- 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_quota_filestreams_h__ +#define mozilla_dom_quota_filestreams_h__ + +// Local includes +#include "Client.h" + +// Global includes +#include <cstdint> +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/quota/CommonMetadata.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "nsFileStreams.h" +#include "nsISupports.h" +#include "nscore.h" + +class nsIFile; + +namespace mozilla { +class Runnable; +} + +namespace mozilla::dom::quota { + +class QuotaObject; + +template <class FileStreamBase> +class FileQuotaStream : public FileStreamBase { + public: + // nsFileStreamBase override + NS_IMETHOD + SetEOF() override; + + NS_IMETHOD + Close() override; + + protected: + FileQuotaStream(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + Client::Type aClientType) + : mPersistenceType(aPersistenceType), + mOriginMetadata(aOriginMetadata), + mClientType(aClientType), + mDeserialized(false) {} + + FileQuotaStream() + : mPersistenceType(PERSISTENCE_TYPE_INVALID), + mClientType(Client::TYPE_MAX), + mDeserialized(true) {} + + ~FileQuotaStream() { Close(); } + + // nsFileStreamBase override + virtual nsresult DoOpen() override; + + PersistenceType mPersistenceType; + OriginMetadata mOriginMetadata; + Client::Type mClientType; + RefPtr<QuotaObject> mQuotaObject; + const bool mDeserialized; +}; + +template <class FileStreamBase> +class FileQuotaStreamWithWrite : public FileQuotaStream<FileStreamBase> { + public: + // nsFileStreamBase override + NS_IMETHOD + Write(const char* aBuf, uint32_t aCount, uint32_t* _retval) override; + + protected: + FileQuotaStreamWithWrite(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + Client::Type aClientType) + : FileQuotaStream<FileStreamBase>(aPersistenceType, aOriginMetadata, + aClientType) {} + + FileQuotaStreamWithWrite() = default; +}; + +class FileInputStream : public FileQuotaStream<nsFileInputStream> { + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(FileInputStream, + FileQuotaStream<nsFileInputStream>) + + FileInputStream(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + Client::Type aClientType) + : FileQuotaStream<nsFileInputStream>(aPersistenceType, aOriginMetadata, + aClientType) {} + + private: + virtual ~FileInputStream() { Close(); } +}; + +class FileOutputStream : public FileQuotaStreamWithWrite<nsFileOutputStream> { + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED( + FileOutputStream, FileQuotaStreamWithWrite<nsFileOutputStream>); + + FileOutputStream(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + Client::Type aClientType) + : FileQuotaStreamWithWrite<nsFileOutputStream>( + aPersistenceType, aOriginMetadata, aClientType) {} + + private: + virtual ~FileOutputStream() { Close(); } +}; + +// FileRandomAccessStream type is serializable, but only in a restricted +// manner. The type is only safe to serialize in the parent process and only +// when the type hasn't been previously deserialized. So the type can be +// serialized in the parent process and desrialized in a child process or it +// can be serialized in the parent process and deserialized in the parent +// process as well (non-e10s mode). The same type can never be +// serialized/deserialized more than once. +class FileRandomAccessStream + : public FileQuotaStreamWithWrite<nsFileRandomAccessStream> { + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED( + FileRandomAccessStream, + FileQuotaStreamWithWrite<nsFileRandomAccessStream>) + + FileRandomAccessStream(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + Client::Type aClientType) + : FileQuotaStreamWithWrite<nsFileRandomAccessStream>( + aPersistenceType, aOriginMetadata, aClientType) {} + + FileRandomAccessStream() = default; + + // nsFileRandomAccessStream override + + // Serialize this FileRandomAccessStream. This method works only in the + // parent process and only with streams which haven't been previously + // deserialized. + mozilla::ipc::RandomAccessStreamParams Serialize( + nsIInterfaceRequestor* aCallbacks) override; + + // Deserialize this FileRandomAccessStream. This method works in both the + // child and parent. + bool Deserialize(mozilla::ipc::RandomAccessStreamParams& aParams) override; + + private: + virtual ~FileRandomAccessStream() { Close(); } +}; + +Result<MovingNotNull<nsCOMPtr<nsIInputStream>>, nsresult> CreateFileInputStream( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + Client::Type aClientType, nsIFile* aFile, int32_t aIOFlags = -1, + int32_t aPerm = -1, int32_t aBehaviorFlags = 0); + +Result<MovingNotNull<nsCOMPtr<nsIOutputStream>>, nsresult> +CreateFileOutputStream(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + Client::Type aClientType, nsIFile* aFile, + int32_t aIOFlags = -1, int32_t aPerm = -1, + int32_t aBehaviorFlags = 0); + +Result<MovingNotNull<nsCOMPtr<nsIRandomAccessStream>>, nsresult> +CreateFileRandomAccessStream(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + Client::Type aClientType, nsIFile* aFile, + int32_t aIOFlags = -1, int32_t aPerm = -1, + int32_t aBehaviorFlags = 0); + +} // namespace mozilla::dom::quota + +#endif /* mozilla_dom_quota_filestreams_h__ */ diff --git a/dom/quota/FirstInitializationAttempts.h b/dom/quota/FirstInitializationAttempts.h new file mode 100644 index 0000000000..e997985371 --- /dev/null +++ b/dom/quota/FirstInitializationAttempts.h @@ -0,0 +1,69 @@ +/* -*- 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_QUOTA_FIRSTINITIALIZATIONATTEMPTS_H_ +#define DOM_QUOTA_FIRSTINITIALIZATIONATTEMPTS_H_ + +#include <cstdint> +#include <utility> +#include "ErrorList.h" + +namespace mozilla::dom::quota { + +template <typename Initialization, typename StringGenerator> +class FirstInitializationAttempts { + Initialization mFirstInitializationAttempts = Initialization::None; + + public: + class FirstInitializationAttemptImpl { + using FirstInitializationAttemptsType = + FirstInitializationAttempts<Initialization, StringGenerator>; + + FirstInitializationAttemptsType& mOwner; + const Initialization mInitialization; + + public: + FirstInitializationAttemptImpl(FirstInitializationAttemptsType& aOwner, + const Initialization aInitialization) + : mOwner(aOwner), mInitialization(aInitialization) {} + + bool Recorded() const { + return mOwner.FirstInitializationAttemptRecorded(mInitialization); + } + + void Record(const nsresult aRv) const { + mOwner.RecordFirstInitializationAttempt(mInitialization, aRv); + } + }; + + template <typename Func> + auto WithFirstInitializationAttempt(const Initialization aInitialization, + Func&& aFunc) + -> std::invoke_result_t<Func, FirstInitializationAttemptImpl&&> { + return std::forward<Func>(aFunc)( + FirstInitializationAttemptImpl(*this, aInitialization)); + } + + bool FirstInitializationAttemptRecorded( + const Initialization aInitialization) const { + return static_cast<bool>(mFirstInitializationAttempts & aInitialization); + } + + void RecordFirstInitializationAttempt(const Initialization aInitialization, + nsresult aRv); + + void ResetFirstInitializationAttempts() { + mFirstInitializationAttempts = Initialization::None; + } +}; + +template <typename Initialization, typename StringGenerator> +using FirstInitializationAttempt = typename FirstInitializationAttempts< + Initialization, StringGenerator>::FirstInitializationAttemptImpl; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_FIRSTINITIALIZATIONATTEMPTS_H_ diff --git a/dom/quota/FirstInitializationAttemptsImpl.h b/dom/quota/FirstInitializationAttemptsImpl.h new file mode 100644 index 0000000000..79ae3daae0 --- /dev/null +++ b/dom/quota/FirstInitializationAttemptsImpl.h @@ -0,0 +1,36 @@ +/* -*- 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_QUOTA_FIRSTINITIALIZATIONATTEMPTSIMPL_H_ +#define DOM_QUOTA_FIRSTINITIALIZATIONATTEMPTSIMPL_H_ + +#include "FirstInitializationAttempts.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryHistogramEnums.h" +#include "nsError.h" + +namespace mozilla::dom::quota { + +template <typename Initialization, typename StringGenerator> +void FirstInitializationAttempts<Initialization, StringGenerator>:: + RecordFirstInitializationAttempt(const Initialization aInitialization, + const nsresult aRv) { + MOZ_ASSERT(!FirstInitializationAttemptRecorded(aInitialization)); + + mFirstInitializationAttempts |= aInitialization; + + if constexpr (!std::is_same_v<StringGenerator, Nothing>) { + Telemetry::Accumulate(Telemetry::QM_FIRST_INITIALIZATION_ATTEMPT, + StringGenerator::GetString(aInitialization), + static_cast<uint32_t>(NS_SUCCEEDED(aRv))); + } +} + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_FIRSTINITIALIZATIONATTEMPTSIMPL_H_ diff --git a/dom/quota/Flatten.h b/dom/quota/Flatten.h new file mode 100644 index 0000000000..2eb29c6010 --- /dev/null +++ b/dom/quota/Flatten.h @@ -0,0 +1,118 @@ +/* -*- 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_QUOTA_FLATTEN_H_ +#define DOM_QUOTA_FLATTEN_H_ + +#include <iterator> +#include <type_traits> +#include <utility> + +// XXX This should be moved to MFBT. + +namespace mozilla::dom::quota { + +namespace detail { + +using std::begin; +using std::end; + +template <typename T, typename NestedRange> +auto Flatten(NestedRange&& aRange) -> std::enable_if_t< + std::is_same_v<T, std::decay_t<typename decltype(begin( + std::declval<const NestedRange&>()))::value_type>>, + std::conditional_t<std::is_rvalue_reference_v<NestedRange>, + std::decay_t<NestedRange>, NestedRange>> { + return std::forward<NestedRange>(aRange); +} + +template <typename T, typename NestedRange> +struct FlatIter { + using OuterIterator = + decltype(begin(std::declval<const std::decay_t<NestedRange>&>())); + using InnerIterator = + decltype(begin(*begin(std::declval<const std::decay_t<NestedRange>&>()))); + + explicit FlatIter(const NestedRange& aRange, OuterIterator aIter) + : mOuterIter{std::move(aIter)}, mOuterEnd{end(aRange)} { + InitInner(); + } + + const T& operator*() const { return *mInnerIter; } + + FlatIter& operator++() { + ++mInnerIter; + if (mInnerIter == mInnerEnd) { + ++mOuterIter; + InitInner(); + } + return *this; + } + + bool operator!=(const FlatIter& aOther) const { + return mOuterIter != aOther.mOuterIter || + (mOuterIter != mOuterEnd && mInnerIter != aOther.mInnerIter); + } + + private: + void InitInner() { + while (mOuterIter != mOuterEnd) { + const typename OuterIterator::value_type& innerRange = *mOuterIter; + + mInnerIter = begin(innerRange); + mInnerEnd = end(innerRange); + + if (mInnerIter != mInnerEnd) { + break; + } + + ++mOuterIter; + } + } + + OuterIterator mOuterIter; + const OuterIterator mOuterEnd; + + InnerIterator mInnerIter; + InnerIterator mInnerEnd; +}; + +template <typename T, typename NestedRange> +struct FlatRange { + explicit FlatRange(NestedRange aRange) : mRange{std::move(aRange)} {} + + auto begin() const { + using std::begin; + return FlatIter<T, NestedRange>{mRange, begin(mRange)}; + } + auto end() const { + using std::end; + return FlatIter<T, NestedRange>{mRange, end(mRange)}; + } + + private: + NestedRange mRange; +}; + +template <typename T, typename NestedRange> +auto Flatten(NestedRange&& aRange) -> std::enable_if_t< + !std::is_same_v< + T, std::decay_t<typename decltype(begin( + std::declval<const std::decay_t<NestedRange>&>()))::value_type>>, + FlatRange<T, NestedRange>> { + return FlatRange<T, NestedRange>{std::forward<NestedRange>(aRange)}; +} + +} // namespace detail + +template <typename T, typename NestedRange> +auto Flatten(NestedRange&& aRange) -> decltype(auto) { + return detail::Flatten<T>(std::forward<NestedRange>(aRange)); +} + +} // namespace mozilla::dom::quota + +#endif diff --git a/dom/quota/ForwardDecls.h b/dom/quota/ForwardDecls.h new file mode 100644 index 0000000000..6989e91d69 --- /dev/null +++ b/dom/quota/ForwardDecls.h @@ -0,0 +1,37 @@ +/* -*- 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_QUOTA_FORWARD_DECLS_H_ +#define DOM_QUOTA_FORWARD_DECLS_H_ + +#include <cstdint> +#include "mozilla/dom/quota/Config.h" + +enum class nsresult : uint32_t; + +namespace mozilla { + +#ifdef QM_ERROR_STACKS_ENABLED +class QMResult; +#else +using QMResult = nsresult; +#endif + +struct Ok; +template <typename V, typename E> +class Result; + +using OkOrErr = Result<Ok, QMResult>; + +template <typename ResolveValueT, typename RejectValueT, bool IsExclusive> +class MozPromise; + +using BoolPromise = MozPromise<bool, nsresult, false>; +using Int64Promise = MozPromise<int64_t, nsresult, false>; + +} // namespace mozilla + +#endif // DOM_QUOTA_FORWARD_DECLS_H_ diff --git a/dom/quota/GroupInfo.cpp b/dom/quota/GroupInfo.cpp new file mode 100644 index 0000000000..96f4f806f6 --- /dev/null +++ b/dom/quota/GroupInfo.cpp @@ -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/. */ + +#include "GroupInfo.h" + +#include "mozilla/dom/quota/AssertionsImpl.h" +#include "OriginInfo.h" + +namespace mozilla::dom::quota { + +already_AddRefed<OriginInfo> GroupInfo::LockedGetOriginInfo( + const nsACString& aOrigin) { + AssertCurrentThreadOwnsQuotaMutex(); + + for (const auto& originInfo : mOriginInfos) { + if (originInfo->mOrigin == aOrigin) { + RefPtr<OriginInfo> result = originInfo; + return result.forget(); + } + } + + return nullptr; +} + +void GroupInfo::LockedAddOriginInfo(NotNull<RefPtr<OriginInfo>>&& aOriginInfo) { + AssertCurrentThreadOwnsQuotaMutex(); + + NS_ASSERTION(!mOriginInfos.Contains(aOriginInfo), + "Replacing an existing entry!"); + mOriginInfos.AppendElement(std::move(aOriginInfo)); + + uint64_t usage = aOriginInfo->LockedUsage(); + + if (!aOriginInfo->LockedPersisted()) { + AssertNoOverflow(mUsage, usage); + mUsage += usage; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + AssertNoOverflow(quotaManager->mTemporaryStorageUsage, usage); + quotaManager->mTemporaryStorageUsage += usage; +} + +void GroupInfo::LockedAdjustUsageForRemovedOriginInfo( + const OriginInfo& aOriginInfo) { + const uint64_t usage = aOriginInfo.LockedUsage(); + + if (!aOriginInfo.LockedPersisted()) { + AssertNoUnderflow(mUsage, usage); + mUsage -= usage; + } + + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, usage); + quotaManager->mTemporaryStorageUsage -= usage; +} + +void GroupInfo::LockedRemoveOriginInfo(const nsACString& aOrigin) { + AssertCurrentThreadOwnsQuotaMutex(); + + const auto foundIt = std::find_if(mOriginInfos.cbegin(), mOriginInfos.cend(), + [&aOrigin](const auto& originInfo) { + return originInfo->mOrigin == aOrigin; + }); + + // XXX Or can we MOZ_ASSERT(foundIt != mOriginInfos.cend()) ? + if (foundIt != mOriginInfos.cend()) { + LockedAdjustUsageForRemovedOriginInfo(**foundIt); + + mOriginInfos.RemoveElementAt(foundIt); + } +} + +void GroupInfo::LockedRemoveOriginInfos() { + AssertCurrentThreadOwnsQuotaMutex(); + + for (const auto& originInfo : std::exchange(mOriginInfos, {})) { + LockedAdjustUsageForRemovedOriginInfo(*originInfo); + } +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/GroupInfo.h b/dom/quota/GroupInfo.h new file mode 100644 index 0000000000..a88914743b --- /dev/null +++ b/dom/quota/GroupInfo.h @@ -0,0 +1,71 @@ +/* -*- 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_QUOTA_GROUPINFO_H_ +#define DOM_QUOTA_GROUPINFO_H_ + +#include "OriginInfo.h" +#include "mozilla/NotNull.h" +#include "mozilla/dom/quota/Assertions.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" + +namespace mozilla::dom::quota { + +class GroupInfoPair; +class OriginInfo; + +class GroupInfo final { + friend class CanonicalQuotaObject; + friend class GroupInfoPair; + friend class OriginInfo; + friend class QuotaManager; + + public: + GroupInfo(GroupInfoPair* aGroupInfoPair, PersistenceType aPersistenceType) + : mGroupInfoPair(aGroupInfoPair), + mPersistenceType(aPersistenceType), + mUsage(0) { + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + MOZ_COUNT_CTOR(GroupInfo); + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GroupInfo) + + PersistenceType GetPersistenceType() const { return mPersistenceType; } + + private: + // Private destructor, to discourage deletion outside of Release(): + MOZ_COUNTED_DTOR(GroupInfo) + + already_AddRefed<OriginInfo> LockedGetOriginInfo(const nsACString& aOrigin); + + void LockedAddOriginInfo(NotNull<RefPtr<OriginInfo>>&& aOriginInfo); + + void LockedAdjustUsageForRemovedOriginInfo(const OriginInfo& aOriginInfo); + + void LockedRemoveOriginInfo(const nsACString& aOrigin); + + void LockedRemoveOriginInfos(); + + bool LockedHasOriginInfos() { + AssertCurrentThreadOwnsQuotaMutex(); + + return !mOriginInfos.IsEmpty(); + } + + nsTArray<NotNull<RefPtr<OriginInfo>>> mOriginInfos; + + GroupInfoPair* mGroupInfoPair; + PersistenceType mPersistenceType; + uint64_t mUsage; +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_GROUPINFO_H_ diff --git a/dom/quota/GroupInfoPair.cpp b/dom/quota/GroupInfoPair.cpp new file mode 100644 index 0000000000..bd912f004b --- /dev/null +++ b/dom/quota/GroupInfoPair.cpp @@ -0,0 +1,26 @@ +/* -*- 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 "GroupInfoPair.h" + +namespace mozilla::dom::quota { + +RefPtr<GroupInfo>& GroupInfoPair::GetGroupInfoForPersistenceType( + PersistenceType aPersistenceType) { + switch (aPersistenceType) { + case PERSISTENCE_TYPE_TEMPORARY: + return mTemporaryStorageGroupInfo; + case PERSISTENCE_TYPE_DEFAULT: + return mDefaultStorageGroupInfo; + + case PERSISTENCE_TYPE_PERSISTENT: + case PERSISTENCE_TYPE_INVALID: + default: + MOZ_CRASH("Bad persistence type value!"); + } +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/GroupInfoPair.h b/dom/quota/GroupInfoPair.h new file mode 100644 index 0000000000..20b8ab8ee9 --- /dev/null +++ b/dom/quota/GroupInfoPair.h @@ -0,0 +1,79 @@ +/* -*- 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_QUOTA_GROUPINFOPAIR_H_ +#define DOM_QUOTA_GROUPINFOPAIR_H_ + +#include "GroupInfo.h" +#include "mozilla/dom/quota/Assertions.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "nsISupportsImpl.h" +#include "nsString.h" + +namespace mozilla::dom::quota { + +class GroupInfo; + +// XXX Consider a new name for this class, it has other data members now +// (besides two GroupInfo objects). +class GroupInfoPair { + public: + GroupInfoPair(const nsACString& aSuffix, const nsACString& aGroup) + : mSuffix(aSuffix), mGroup(aGroup) { + MOZ_COUNT_CTOR(GroupInfoPair); + } + + MOZ_COUNTED_DTOR(GroupInfoPair) + + const nsCString& Suffix() const { return mSuffix; } + + const nsCString& Group() const { return mGroup; } + + RefPtr<GroupInfo> LockedGetGroupInfo(PersistenceType aPersistenceType) { + AssertCurrentThreadOwnsQuotaMutex(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + return GetGroupInfoForPersistenceType(aPersistenceType); + } + + void LockedSetGroupInfo(PersistenceType aPersistenceType, + GroupInfo* aGroupInfo) { + AssertCurrentThreadOwnsQuotaMutex(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + RefPtr<GroupInfo>& groupInfo = + GetGroupInfoForPersistenceType(aPersistenceType); + groupInfo = aGroupInfo; + } + + void LockedClearGroupInfo(PersistenceType aPersistenceType) { + AssertCurrentThreadOwnsQuotaMutex(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + RefPtr<GroupInfo>& groupInfo = + GetGroupInfoForPersistenceType(aPersistenceType); + groupInfo = nullptr; + } + + bool LockedHasGroupInfos() { + AssertCurrentThreadOwnsQuotaMutex(); + + return mTemporaryStorageGroupInfo || mDefaultStorageGroupInfo; + } + + private: + RefPtr<GroupInfo>& GetGroupInfoForPersistenceType( + PersistenceType aPersistenceType); + + const nsCString mSuffix; + const nsCString mGroup; + RefPtr<GroupInfo> mTemporaryStorageGroupInfo; + RefPtr<GroupInfo> mDefaultStorageGroupInfo; +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_GROUPINFOPAIR_H_ diff --git a/dom/quota/IPCQuotaObject.ipdlh b/dom/quota/IPCQuotaObject.ipdlh new file mode 100644 index 0000000000..04c86ff585 --- /dev/null +++ b/dom/quota/IPCQuotaObject.ipdlh @@ -0,0 +1,20 @@ +/* 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 PRemoteQuotaObject; + +using mozilla::ipc::Endpoint from "mozilla/ipc/Endpoint.h"; + +namespace mozilla { +namespace dom { +namespace quota { + +struct IPCQuotaObject +{ + Endpoint<PRemoteQuotaObjectChild> childEndpoint; +}; + +} // namespace quota +} // namespace dom +} // namespace mozilla diff --git a/dom/quota/IPCStreamCipherStrategy.h b/dom/quota/IPCStreamCipherStrategy.h new file mode 100644 index 0000000000..c66c644827 --- /dev/null +++ b/dom/quota/IPCStreamCipherStrategy.h @@ -0,0 +1,16 @@ +/* -*- 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_quota_IPCStreamCipherStrategy_h +#define mozilla_dom_quota_IPCStreamCipherStrategy_h + +#include "mozilla/dom/quota/NSSCipherStrategy.h" + +namespace mozilla::dom::quota { +using IPCStreamCipherStrategy = NSSCipherStrategy; +} + +#endif diff --git a/dom/quota/InitializationTypes.cpp b/dom/quota/InitializationTypes.cpp new file mode 100644 index 0000000000..fffb4693aa --- /dev/null +++ b/dom/quota/InitializationTypes.cpp @@ -0,0 +1,57 @@ +/* -*- 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 "InitializationTypes.h" + +namespace mozilla::dom::quota { + +// static +nsLiteralCString StringGenerator::GetString( + const Initialization aInitialization) { + switch (aInitialization) { + case Initialization::Storage: + return "Storage"_ns; + case Initialization::TemporaryStorage: + return "TemporaryStorage"_ns; + case Initialization::DefaultRepository: + return "DefaultRepository"_ns; + case Initialization::TemporaryRepository: + return "TemporaryRepository"_ns; + case Initialization::UpgradeStorageFrom0_0To1_0: + return "UpgradeStorageFrom0_0To1_0"_ns; + case Initialization::UpgradeStorageFrom1_0To2_0: + return "UpgradeStorageFrom1_0To2_0"_ns; + case Initialization::UpgradeStorageFrom2_0To2_1: + return "UpgradeStorageFrom2_0To2_1"_ns; + case Initialization::UpgradeStorageFrom2_1To2_2: + return "UpgradeStorageFrom2_1To2_2"_ns; + case Initialization::UpgradeStorageFrom2_2To2_3: + return "UpgradeStorageFrom2_2To2_3"_ns; + case Initialization::UpgradeFromIndexedDBDirectory: + return "UpgradeFromIndexedDBDirectory"_ns; + case Initialization::UpgradeFromPersistentStorageDirectory: + return "UpgradeFromPersistentStorageDirectory"_ns; + + default: + MOZ_CRASH("Bad initialization value!"); + } +} + +// static +nsLiteralCString StringGenerator::GetString( + const OriginInitialization aOriginInitialization) { + switch (aOriginInitialization) { + case OriginInitialization::PersistentOrigin: + return "PersistentOrigin"_ns; + case OriginInitialization::TemporaryOrigin: + return "TemporaryOrigin"_ns; + + default: + MOZ_CRASH("Bad origin initialization value!"); + } +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/InitializationTypes.h b/dom/quota/InitializationTypes.h new file mode 100644 index 0000000000..06d46abbd1 --- /dev/null +++ b/dom/quota/InitializationTypes.h @@ -0,0 +1,79 @@ +/* -*- 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_QUOTA_INITIALIZATIONTYPES_H_ +#define DOM_QUOTA_INITIALIZATIONTYPES_H_ + +#include "mozilla/TypedEnumBits.h" +#include "mozilla/dom/quota/FirstInitializationAttempts.h" +#include "nsLiteralString.h" +#include "nsStringFwd.h" +#include "nsTHashMap.h" + +namespace mozilla { +struct CreateIfNonExistent; +} + +namespace mozilla::dom::quota { + +enum class Initialization { + None = 0, + Storage = 1 << 0, + TemporaryStorage = 1 << 1, + DefaultRepository = 1 << 2, + TemporaryRepository = 1 << 3, + UpgradeStorageFrom0_0To1_0 = 1 << 4, + UpgradeStorageFrom1_0To2_0 = 1 << 5, + UpgradeStorageFrom2_0To2_1 = 1 << 6, + UpgradeStorageFrom2_1To2_2 = 1 << 7, + UpgradeStorageFrom2_2To2_3 = 1 << 8, + UpgradeFromIndexedDBDirectory = 1 << 9, + UpgradeFromPersistentStorageDirectory = 1 << 10, +}; + +enum class OriginInitialization { + None = 0, + PersistentOrigin = 1 << 0, + TemporaryOrigin = 1 << 1, +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(Initialization) +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(OriginInitialization) + +class StringGenerator final { + public: + // TODO: Use constexpr here once bug 1594094 is addressed. + static nsLiteralCString GetString(Initialization aInitialization); + + // TODO: Use constexpr here once bug 1594094 is addressed. + static nsLiteralCString GetString(OriginInitialization aOriginInitialization); +}; + +using OriginInitializationInfo = + FirstInitializationAttempts<OriginInitialization, StringGenerator>; + +class InitializationInfo + : public FirstInitializationAttempts<Initialization, StringGenerator> { + nsTHashMap<nsCStringHashKey, OriginInitializationInfo> + mOriginInitializationInfos; + + public: + OriginInitializationInfo& MutableOriginInitializationInfoRef( + const nsACString& aOrigin) { + return *mOriginInitializationInfos.Lookup(aOrigin); + } + + OriginInitializationInfo& MutableOriginInitializationInfoRef( + const nsACString& aOrigin, const CreateIfNonExistent&) { + return mOriginInitializationInfos.LookupOrInsert(aOrigin); + } + + void ResetOriginInitializationInfos() { mOriginInitializationInfos.Clear(); } +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_INITIALIZATIONTYPES_H_ diff --git a/dom/quota/NSSCipherStrategy.cpp b/dom/quota/NSSCipherStrategy.cpp new file mode 100644 index 0000000000..d3278b54b7 --- /dev/null +++ b/dom/quota/NSSCipherStrategy.cpp @@ -0,0 +1,152 @@ +/* -*- 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 "NSSCipherStrategy.h" + +#include <algorithm> +#include <cstdlib> +#include <cstring> +#include <memory> +#include <type_traits> +#include <utility> +#include "mozilla/Assertions.h" +#include "mozilla/ResultExtensions.h" + +// NSS includes +#include "blapit.h" +#include "pk11pub.h" +#include "pkcs11t.h" +#include "seccomon.h" +#include "secmodt.h" + +namespace mozilla::dom::quota { + +static_assert(sizeof(NSSCipherStrategy::KeyType) == 32); +static_assert(NSSCipherStrategy::BlockPrefixLength == 32); +static_assert(NSSCipherStrategy::BasicBlockSize == 16); + +Result<NSSCipherStrategy::KeyType, nsresult> NSSCipherStrategy::GenerateKey() { + const auto slot = UniquePK11SlotInfo{PK11_GetInternalSlot()}; + if (slot == nullptr) { + return Err(NS_ERROR_FAILURE); + } + const auto symKey = UniquePK11SymKey{PK11_KeyGen( + slot.get(), CKM_CHACHA20_KEY_GEN, nullptr, sizeof(KeyType), nullptr)}; + if (symKey == nullptr) { + return Err(NS_ERROR_FAILURE); + } + if (PK11_ExtractKeyValue(symKey.get()) != SECSuccess) { + return Err(NS_ERROR_FAILURE); + } + // No need to free keyData as it is a buffer managed by symKey. + SECItem* keyData = PK11_GetKeyData(symKey.get()); + if (keyData == nullptr) { + return Err(NS_ERROR_FAILURE); + } + KeyType key; + MOZ_RELEASE_ASSERT(keyData->len == key.size()); + std::copy(keyData->data, keyData->data + key.size(), key.data()); + return key; +} + +nsresult NSSCipherStrategy::Init(const CipherMode aMode, + const Span<const uint8_t> aKey, + const Span<const uint8_t> aInitialIv) { + MOZ_ASSERT_IF(CipherMode::Encrypt == aMode, aInitialIv.Length() == 32); + + mMode.init(aMode); + + mIv.AppendElements(aInitialIv); + + const auto slot = UniquePK11SlotInfo{PK11_GetInternalSlot()}; + if (slot == nullptr) { + return NS_ERROR_FAILURE; + } + + SECItem keyItem; + keyItem.data = const_cast<uint8_t*>(aKey.Elements()); + keyItem.len = aKey.Length(); + const auto symKey = UniquePK11SymKey{ + PK11_ImportSymKey(slot.get(), CKM_CHACHA20_POLY1305, PK11_OriginUnwrap, + CKA_ENCRYPT, &keyItem, nullptr)}; + if (symKey == nullptr) { + return NS_ERROR_FAILURE; + } + + SECItem empty = {siBuffer, nullptr, 0}; + auto pk11Context = UniquePK11Context{PK11_CreateContextBySymKey( + CKM_CHACHA20_POLY1305, + CKA_NSS_MESSAGE | + (CipherMode::Encrypt == aMode ? CKA_ENCRYPT : CKA_DECRYPT), + symKey.get(), &empty)}; + if (pk11Context == nullptr) { + return NS_ERROR_FAILURE; + } + + mPK11Context.init(std::move(pk11Context)); + return NS_OK; +} + +nsresult NSSCipherStrategy::Cipher(const Span<uint8_t> aIv, + const Span<const uint8_t> aIn, + const Span<uint8_t> aOut) { + if (CipherMode::Encrypt == *mMode) { + MOZ_RELEASE_ASSERT(aIv.Length() == mIv.Length()); + memcpy(aIv.Elements(), mIv.Elements(), aIv.Length()); + } + + // XXX make tag a separate parameter + constexpr size_t tagLen = 16; + const auto tag = Span{aIv}.Last(tagLen); + // tag is const on decrypt, but returned on encrypt + + const auto iv = Span{aIv}.First(12); + MOZ_ASSERT(tag.Length() + iv.Length() <= aIv.Length()); + + int outLen; + // aIn and aOut may not overlap resp. be the same, so we can't do this + // in-place. + const SECStatus rv = PK11_AEADOp( + mPK11Context->get(), CKG_GENERATE_COUNTER, 0, iv.Elements(), iv.Length(), + nullptr, 0, aOut.Elements(), &outLen, aOut.Length(), tag.Elements(), + tag.Length(), aIn.Elements(), aIn.Length()); + + if (CipherMode::Encrypt == *mMode) { + memcpy(mIv.Elements(), aIv.Elements(), aIv.Length()); + } + + return MapSECStatus(rv); +} + +template <size_t N> +static std::array<uint8_t, N> MakeRandomData() { + std::array<uint8_t, N> res; + + const auto rv = PK11_GenerateRandom(res.data(), res.size()); + /// XXX Allow return of error code to handle this gracefully. + MOZ_RELEASE_ASSERT(rv == SECSuccess); + + return res; +} + +std::array<uint8_t, NSSCipherStrategy::BlockPrefixLength> +NSSCipherStrategy::MakeBlockPrefix() { + return MakeRandomData<BlockPrefixLength>(); +} + +Span<const uint8_t> NSSCipherStrategy::SerializeKey(const KeyType& aKey) { + return Span(aKey); +} + +NSSCipherStrategy::KeyType NSSCipherStrategy::DeserializeKey( + const Span<const uint8_t>& aSerializedKey) { + KeyType res; + MOZ_ASSERT(res.size() == aSerializedKey.size()); + std::copy(aSerializedKey.cbegin(), aSerializedKey.cend(), res.begin()); + return res; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/NSSCipherStrategy.h b/dom/quota/NSSCipherStrategy.h new file mode 100644 index 0000000000..31b3eb03e2 --- /dev/null +++ b/dom/quota/NSSCipherStrategy.h @@ -0,0 +1,56 @@ +/* -*- 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_quota_NSSCipherStrategy_h +#define mozilla_dom_quota_NSSCipherStrategy_h + +#include "CipherStrategy.h" + +#include <cstddef> +#include <cstdint> +#include "mozilla/InitializedOnce.h" +#include "mozilla/Result.h" +#include "mozilla/Span.h" +#include "ErrorList.h" +#include "nsTArray.h" + +#include "ScopedNSSTypes.h" + +#include <array> + +namespace mozilla::dom::quota { + +struct NSSCipherStrategy { + // Use numeric literals here to avoid having to include NSS headers here. + // static_assert's in the cpp file check their consistency. + using KeyType = std::array<uint8_t, 32>; + static constexpr size_t BlockPrefixLength = 32; + static constexpr size_t BasicBlockSize = 16; + + static Result<KeyType, nsresult> GenerateKey(); + + nsresult Init(CipherMode aCipherMode, Span<const uint8_t> aKey, + Span<const uint8_t> aInitialIv = Span<const uint8_t>{}); + + nsresult Cipher(Span<uint8_t> aIv, Span<const uint8_t> aIn, + Span<uint8_t> aOut); + + static std::array<uint8_t, BlockPrefixLength> MakeBlockPrefix(); + + static Span<const uint8_t> SerializeKey(const KeyType& aKey); + + static KeyType DeserializeKey(const Span<const uint8_t>& aSerializedKey); + + private: + // XXX Remove EarlyDestructible, remove moving of the CipherStrategy. + LazyInitializedOnceEarlyDestructible<const CipherMode> mMode; + LazyInitializedOnceEarlyDestructible<const UniquePK11Context> mPK11Context; + nsTArray<uint8_t> mIv; +}; + +} // namespace mozilla::dom::quota + +#endif diff --git a/dom/quota/OriginInfo.cpp b/dom/quota/OriginInfo.cpp new file mode 100644 index 0000000000..4985bb627c --- /dev/null +++ b/dom/quota/OriginInfo.cpp @@ -0,0 +1,167 @@ +/* -*- 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 "OriginInfo.h" + +#include "GroupInfo.h" +#include "GroupInfoPair.h" +#include "mozilla/dom/quota/AssertionsImpl.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/UsageInfo.h" + +namespace mozilla::dom::quota { + +OriginInfo::OriginInfo(GroupInfo* aGroupInfo, const nsACString& aOrigin, + const ClientUsageArray& aClientUsages, uint64_t aUsage, + int64_t aAccessTime, bool aPersisted, + bool aDirectoryExists) + : mClientUsages(aClientUsages.Clone()), + mGroupInfo(aGroupInfo), + mOrigin(aOrigin), + mUsage(aUsage), + mAccessTime(aAccessTime), + mAccessed(false), + mPersisted(aPersisted), + mDirectoryExists(aDirectoryExists) { + MOZ_ASSERT(aGroupInfo); + MOZ_ASSERT(aClientUsages.Length() == Client::TypeMax()); + MOZ_ASSERT_IF(aPersisted, + aGroupInfo->mPersistenceType == PERSISTENCE_TYPE_DEFAULT); + + // This constructor is called from the "QuotaManager IO" thread and so + // we can't check if the principal has a WebExtensionPolicy instance + // associated to it, and even besides that if the extension is currently + // disabled (and so no WebExtensionPolicy instance would actually exist) + // its stored data shouldn't be cleared until the extension is uninstalled + // and so here we resort to check the origin scheme instead. + mIsExtension = StringBeginsWith(mOrigin, "moz-extension://"_ns); + +#ifdef DEBUG + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + uint64_t usage = 0; + for (Client::Type type : quotaManager->AllClientTypes()) { + AssertNoOverflow(usage, aClientUsages[type].valueOr(0)); + usage += aClientUsages[type].valueOr(0); + } + MOZ_ASSERT(aUsage == usage); +#endif + + MOZ_COUNT_CTOR(OriginInfo); +} + +OriginMetadata OriginInfo::FlattenToOriginMetadata() const { + return {mGroupInfo->mGroupInfoPair->Suffix(), + mGroupInfo->mGroupInfoPair->Group(), mOrigin, + mGroupInfo->mPersistenceType}; +} + +FullOriginMetadata OriginInfo::LockedFlattenToFullOriginMetadata() const { + AssertCurrentThreadOwnsQuotaMutex(); + + return {FlattenToOriginMetadata(), mPersisted, mAccessTime}; +} + +nsresult OriginInfo::LockedBindToStatement( + mozIStorageStatement* aStatement) const { + AssertCurrentThreadOwnsQuotaMutex(); + MOZ_ASSERT(mGroupInfo); + + QM_TRY(MOZ_TO_RESULT(aStatement->BindInt32ByName( + "repository_id"_ns, mGroupInfo->mPersistenceType))); + + QM_TRY(MOZ_TO_RESULT(aStatement->BindUTF8StringByName( + "suffix"_ns, mGroupInfo->mGroupInfoPair->Suffix()))); + QM_TRY(MOZ_TO_RESULT(aStatement->BindUTF8StringByName( + "group_"_ns, mGroupInfo->mGroupInfoPair->Group()))); + QM_TRY(MOZ_TO_RESULT(aStatement->BindUTF8StringByName("origin"_ns, mOrigin))); + + nsCString clientUsagesText; + mClientUsages.Serialize(clientUsagesText); + + QM_TRY(MOZ_TO_RESULT( + aStatement->BindUTF8StringByName("client_usages"_ns, clientUsagesText))); + QM_TRY(MOZ_TO_RESULT(aStatement->BindInt64ByName("usage"_ns, mUsage))); + QM_TRY(MOZ_TO_RESULT( + aStatement->BindInt64ByName("last_access_time"_ns, mAccessTime))); + QM_TRY(MOZ_TO_RESULT(aStatement->BindInt32ByName("accessed"_ns, mAccessed))); + QM_TRY( + MOZ_TO_RESULT(aStatement->BindInt32ByName("persisted"_ns, mPersisted))); + + return NS_OK; +} + +void OriginInfo::LockedDecreaseUsage(Client::Type aClientType, int64_t aSize) { + AssertCurrentThreadOwnsQuotaMutex(); + + MOZ_ASSERT(mClientUsages[aClientType].isSome()); + AssertNoUnderflow(mClientUsages[aClientType].value(), aSize); + mClientUsages[aClientType] = Some(mClientUsages[aClientType].value() - aSize); + + AssertNoUnderflow(mUsage, aSize); + mUsage -= aSize; + + if (!LockedPersisted()) { + AssertNoUnderflow(mGroupInfo->mUsage, aSize); + mGroupInfo->mUsage -= aSize; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, aSize); + quotaManager->mTemporaryStorageUsage -= aSize; +} + +void OriginInfo::LockedResetUsageForClient(Client::Type aClientType) { + AssertCurrentThreadOwnsQuotaMutex(); + + uint64_t size = mClientUsages[aClientType].valueOr(0); + + mClientUsages[aClientType].reset(); + + AssertNoUnderflow(mUsage, size); + mUsage -= size; + + if (!LockedPersisted()) { + AssertNoUnderflow(mGroupInfo->mUsage, size); + mGroupInfo->mUsage -= size; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, size); + quotaManager->mTemporaryStorageUsage -= size; +} + +UsageInfo OriginInfo::LockedGetUsageForClient(Client::Type aClientType) { + AssertCurrentThreadOwnsQuotaMutex(); + + // The current implementation of this method only supports DOMCACHE and LS, + // which only use DatabaseUsage. If this assertion is lifted, the logic below + // must be adapted. + MOZ_ASSERT(aClientType == Client::Type::DOMCACHE || + aClientType == Client::Type::LS || + aClientType == Client::Type::FILESYSTEM); + + return UsageInfo{DatabaseUsageType{mClientUsages[aClientType]}}; +} + +void OriginInfo::LockedPersist() { + AssertCurrentThreadOwnsQuotaMutex(); + MOZ_ASSERT(mGroupInfo->mPersistenceType == PERSISTENCE_TYPE_DEFAULT); + MOZ_ASSERT(!mPersisted); + + mPersisted = true; + + // Remove Usage from GroupInfo + AssertNoUnderflow(mGroupInfo->mUsage, mUsage); + mGroupInfo->mUsage -= mUsage; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/OriginInfo.h b/dom/quota/OriginInfo.h new file mode 100644 index 0000000000..33a3a30355 --- /dev/null +++ b/dom/quota/OriginInfo.h @@ -0,0 +1,138 @@ +/* -*- 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_QUOTA_ORIGININFO_H_ +#define DOM_QUOTA_ORIGININFO_H_ + +#include "Assertions.h" +#include "ClientUsageArray.h" +#include "mozilla/dom/quota/QuotaManager.h" + +namespace mozilla::dom::quota { + +class CanonicalQuotaObject; +class GroupInfo; + +class OriginInfo final { + friend class CanonicalQuotaObject; + friend class GroupInfo; + friend class QuotaManager; + + public: + OriginInfo(GroupInfo* aGroupInfo, const nsACString& aOrigin, + const ClientUsageArray& aClientUsages, uint64_t aUsage, + int64_t aAccessTime, bool aPersisted, bool aDirectoryExists); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OriginInfo) + + GroupInfo* GetGroupInfo() const { return mGroupInfo; } + + const nsCString& Origin() const { return mOrigin; } + + int64_t LockedUsage() const { + AssertCurrentThreadOwnsQuotaMutex(); + +#ifdef DEBUG + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + uint64_t usage = 0; + for (Client::Type type : quotaManager->AllClientTypes()) { + AssertNoOverflow(usage, mClientUsages[type].valueOr(0)); + usage += mClientUsages[type].valueOr(0); + } + MOZ_ASSERT(mUsage == usage); +#endif + + return mUsage; + } + + int64_t LockedAccessTime() const { + AssertCurrentThreadOwnsQuotaMutex(); + + return mAccessTime; + } + + bool LockedPersisted() const { + AssertCurrentThreadOwnsQuotaMutex(); + + return mPersisted; + } + + OriginMetadata FlattenToOriginMetadata() const; + + FullOriginMetadata LockedFlattenToFullOriginMetadata() const; + + nsresult LockedBindToStatement(mozIStorageStatement* aStatement) const; + + private: + // Private destructor, to discourage deletion outside of Release(): + ~OriginInfo() { + MOZ_COUNT_DTOR(OriginInfo); + + MOZ_ASSERT(!mCanonicalQuotaObjects.Count()); + } + + void LockedDecreaseUsage(Client::Type aClientType, int64_t aSize); + + void LockedResetUsageForClient(Client::Type aClientType); + + UsageInfo LockedGetUsageForClient(Client::Type aClientType); + + void LockedUpdateAccessTime(int64_t aAccessTime) { + AssertCurrentThreadOwnsQuotaMutex(); + + mAccessTime = aAccessTime; + if (!mAccessed) { + mAccessed = true; + } + } + + void LockedPersist(); + + bool IsExtensionOrigin() { return mIsExtension; } + + nsTHashMap<nsStringHashKey, NotNull<CanonicalQuotaObject*>> + mCanonicalQuotaObjects; + ClientUsageArray mClientUsages; + GroupInfo* mGroupInfo; + const nsCString mOrigin; + bool mIsExtension; + uint64_t mUsage; + int64_t mAccessTime; + bool mAccessed; + bool mPersisted; + /** + * In some special cases like the LocalStorage client where it's possible to + * create a Quota-using representation but not actually write any data, we + * want to be able to track quota for an origin without creating its origin + * directory or the per-client files until they are actually needed to store + * data. In those cases, the OriginInfo will be created by + * EnsureQuotaForOrigin and the resulting mDirectoryExists will be false until + * the origin actually needs to be created. It is possible for mUsage to be + * greater than zero while mDirectoryExists is false, representing a state + * where a client like LocalStorage has reserved quota for disk writes, but + * has not yet flushed the data to disk. + */ + bool mDirectoryExists; +}; + +class OriginInfoAccessTimeComparator { + public: + bool Equals(const NotNull<RefPtr<const OriginInfo>>& a, + const NotNull<RefPtr<const OriginInfo>>& b) const { + return a->LockedAccessTime() == b->LockedAccessTime(); + } + + bool LessThan(const NotNull<RefPtr<const OriginInfo>>& a, + const NotNull<RefPtr<const OriginInfo>>& b) const { + return a->LockedAccessTime() < b->LockedAccessTime(); + } +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_ORIGININFO_H_ diff --git a/dom/quota/OriginScope.h b/dom/quota/OriginScope.h new file mode 100644 index 0000000000..a7047e5c82 --- /dev/null +++ b/dom/quota/OriginScope.h @@ -0,0 +1,344 @@ +/* -*- 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_quota_originorpatternstring_h__ +#define mozilla_dom_quota_originorpatternstring_h__ + +#include <utility> +#include "mozilla/Assertions.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Variant.h" +#include "nsStringFlags.h" +#include "nsStringFwd.h" + +namespace mozilla::dom::quota { + +class OriginScope { + class Origin { + nsCString mOrigin; + nsCString mOriginNoSuffix; + UniquePtr<OriginAttributes> mAttributes; + + public: + explicit Origin(const nsACString& aOrigin) : mOrigin(aOrigin) { + InitMembers(); + } + + Origin(const Origin& aOther) + : mOrigin(aOther.mOrigin), + mOriginNoSuffix(aOther.mOriginNoSuffix), + mAttributes(MakeUnique<OriginAttributes>(*aOther.mAttributes)) {} + + Origin(Origin&& aOther) = default; + + const nsACString& GetOrigin() const { return mOrigin; } + + void SetOrigin(const nsACString& aOrigin) { + mOrigin = aOrigin; + + InitMembers(); + } + + const nsACString& GetOriginNoSuffix() const { return mOriginNoSuffix; } + + const OriginAttributes& GetAttributes() const { + MOZ_ASSERT(mAttributes); + + return *mAttributes; + } + + private: + void InitMembers() { + mAttributes = MakeUnique<OriginAttributes>(); + + MOZ_ALWAYS_TRUE( + mAttributes->PopulateFromOrigin(mOrigin, mOriginNoSuffix)); + } + }; + + class Prefix { + nsCString mOriginNoSuffix; + + public: + explicit Prefix(const nsACString& aOriginNoSuffix) + : mOriginNoSuffix(aOriginNoSuffix) {} + + const nsCString& GetOriginNoSuffix() const { return mOriginNoSuffix; } + + void SetOriginNoSuffix(const nsACString& aOriginNoSuffix) { + mOriginNoSuffix = aOriginNoSuffix; + } + }; + + class Pattern { + UniquePtr<OriginAttributesPattern> mPattern; + + public: + explicit Pattern(const OriginAttributesPattern& aPattern) + : mPattern(MakeUnique<OriginAttributesPattern>(aPattern)) {} + + explicit Pattern(const nsAString& aJSONPattern) + : mPattern(MakeUnique<OriginAttributesPattern>()) { + MOZ_ALWAYS_TRUE(mPattern->Init(aJSONPattern)); + } + + Pattern(const Pattern& aOther) + : mPattern(MakeUnique<OriginAttributesPattern>(*aOther.mPattern)) {} + + Pattern(Pattern&& aOther) = default; + + const OriginAttributesPattern& GetPattern() const { + MOZ_ASSERT(mPattern); + + return *mPattern; + } + + void SetPattern(const OriginAttributesPattern& aPattern) { + mPattern = MakeUnique<OriginAttributesPattern>(aPattern); + } + + nsString GetJSONPattern() const { + MOZ_ASSERT(mPattern); + + nsString result; + MOZ_ALWAYS_TRUE(mPattern->ToJSON(result)); + + return result; + } + }; + + struct Null {}; + + using DataType = Variant<Origin, Prefix, Pattern, Null>; + + DataType mData; + + public: + OriginScope() : mData(Null()) {} + + static OriginScope FromOrigin(const nsACString& aOrigin) { + return OriginScope(std::move(Origin(aOrigin))); + } + + static OriginScope FromPrefix(const nsACString& aPrefix) { + return OriginScope(std::move(Prefix(aPrefix))); + } + + static OriginScope FromPattern(const OriginAttributesPattern& aPattern) { + return OriginScope(std::move(Pattern(aPattern))); + } + + static OriginScope FromJSONPattern(const nsAString& aJSONPattern) { + return OriginScope(std::move(Pattern(aJSONPattern))); + } + + static OriginScope FromNull() { return OriginScope(std::move(Null())); } + + 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>(); } + + void SetFromOrigin(const nsACString& aOrigin) { + mData = AsVariant(Origin(aOrigin)); + } + + void SetFromPrefix(const nsACString& aPrefix) { + mData = AsVariant(Prefix(aPrefix)); + } + + void SetFromPattern(const OriginAttributesPattern& aPattern) { + mData = AsVariant(Pattern(aPattern)); + } + + void SetFromJSONPattern(const nsAString& aJSONPattern) { + mData = AsVariant(Pattern(aJSONPattern)); + } + + void SetFromNull() { mData = AsVariant(Null()); } + + const nsACString& GetOrigin() const { + MOZ_ASSERT(IsOrigin()); + + return mData.as<Origin>().GetOrigin(); + } + + void SetOrigin(const nsACString& aOrigin) { + MOZ_ASSERT(IsOrigin()); + + mData.as<Origin>().SetOrigin(aOrigin); + } + + const nsACString& GetOriginNoSuffix() const { + MOZ_ASSERT(IsOrigin() || IsPrefix()); + + if (IsOrigin()) { + return mData.as<Origin>().GetOriginNoSuffix(); + } + return mData.as<Prefix>().GetOriginNoSuffix(); + } + + void SetOriginNoSuffix(const nsACString& aOriginNoSuffix) { + MOZ_ASSERT(IsPrefix()); + + mData.as<Prefix>().SetOriginNoSuffix(aOriginNoSuffix); + } + + const OriginAttributesPattern& GetPattern() const { + MOZ_ASSERT(IsPattern()); + + return mData.as<Pattern>().GetPattern(); + } + + nsString GetJSONPattern() const { + MOZ_ASSERT(IsPattern()); + + return mData.as<Pattern>().GetJSONPattern(); + } + + void SetPattern(const OriginAttributesPattern& aPattern) { + MOZ_ASSERT(IsPattern()); + + mData.as<Pattern>().SetPattern(aPattern); + } + + bool Matches(const OriginScope& aOther) const { + struct Matcher { + const OriginScope& mThis; + + explicit Matcher(const OriginScope& aThis) : mThis(aThis) {} + + bool operator()(const Origin& aOther) { + return mThis.MatchesOrigin(aOther); + } + + bool operator()(const Prefix& aOther) { + return mThis.MatchesPrefix(aOther); + } + + bool operator()(const Pattern& aOther) { + return mThis.MatchesPattern(aOther); + } + + bool operator()(const Null& aOther) { return true; } + }; + + return aOther.mData.match(Matcher(*this)); + } + + OriginScope Clone() { return OriginScope(mData); } + + private: + // Move constructors + explicit OriginScope(const Origin&& aOrigin) : mData(aOrigin) {} + + explicit OriginScope(const Prefix&& aPrefix) : mData(aPrefix) {} + + explicit OriginScope(const Pattern&& aPattern) : mData(aPattern) {} + + explicit OriginScope(const Null&& aNull) : mData(aNull) {} + + // Copy constructor + explicit OriginScope(const DataType& aOther) : mData(aOther) {} + + bool MatchesOrigin(const Origin& aOther) const { + struct OriginMatcher { + const Origin& mOther; + + explicit OriginMatcher(const Origin& aOther) : mOther(aOther) {} + + bool operator()(const Origin& aThis) { + return aThis.GetOrigin().Equals(mOther.GetOrigin()); + } + + bool operator()(const Prefix& aThis) { + return aThis.GetOriginNoSuffix().Equals(mOther.GetOriginNoSuffix()); + } + + bool operator()(const Pattern& aThis) { + return aThis.GetPattern().Matches(mOther.GetAttributes()); + } + + bool operator()(const Null& aThis) { + // Null covers everything. + return true; + } + }; + + return mData.match(OriginMatcher(aOther)); + } + + bool MatchesPrefix(const Prefix& aOther) const { + struct PrefixMatcher { + const Prefix& mOther; + + explicit PrefixMatcher(const Prefix& aOther) : mOther(aOther) {} + + bool operator()(const Origin& aThis) { + return aThis.GetOriginNoSuffix().Equals(mOther.GetOriginNoSuffix()); + } + + bool operator()(const Prefix& aThis) { + return aThis.GetOriginNoSuffix().Equals(mOther.GetOriginNoSuffix()); + } + + bool operator()(const Pattern& aThis) { + // The match will be always true here because any origin attributes + // pattern overlaps any origin prefix (an origin prefix targets all + // origin attributes). + return true; + } + + bool operator()(const Null& aThis) { + // Null covers everything. + return true; + } + }; + + return mData.match(PrefixMatcher(aOther)); + } + + bool MatchesPattern(const Pattern& aOther) const { + struct PatternMatcher { + const Pattern& mOther; + + explicit PatternMatcher(const Pattern& aOther) : mOther(aOther) {} + + bool operator()(const Origin& aThis) { + return mOther.GetPattern().Matches(aThis.GetAttributes()); + } + + bool operator()(const Prefix& aThis) { + // The match will be always true here because any origin attributes + // pattern overlaps any origin prefix (an origin prefix targets all + // origin attributes). + return true; + } + + bool operator()(const Pattern& aThis) { + return aThis.GetPattern().Overlaps(mOther.GetPattern()); + } + + bool operator()(const Null& aThis) { + // Null covers everything. + return true; + } + }; + + PatternMatcher patternMatcher(aOther); + return mData.match(PatternMatcher(aOther)); + } + + bool operator==(const OriginScope& aOther) = delete; +}; + +} // namespace mozilla::dom::quota + +#endif // mozilla_dom_quota_originorpatternstring_h__ diff --git a/dom/quota/PQuota.ipdl b/dom/quota/PQuota.ipdl new file mode 100644 index 0000000000..f34109b239 --- /dev/null +++ b/dom/quota/PQuota.ipdl @@ -0,0 +1,180 @@ +/* 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 PQuotaRequest; +include protocol PQuotaUsageRequest; + +include PBackgroundSharedTypes; + +include "mozilla/dom/quota/SerializationHelpers.h"; + +using mozilla::OriginAttributesPattern + from "mozilla/OriginAttributes.h"; + +using mozilla::dom::quota::PersistenceType + from "mozilla/dom/quota/PersistenceType.h"; + +using mozilla::dom::quota::Client::Type + from "mozilla/dom/quota/Client.h"; + +using mozilla::dom::ContentParentId + from "mozilla/dom/ipc/IdType.h"; + +namespace mozilla { +namespace dom { +namespace quota { + +struct StorageNameParams +{ +}; + +struct StorageInitializedParams +{ +}; + +struct TemporaryStorageInitializedParams +{ +}; + +struct InitParams +{ +}; + +struct InitTemporaryStorageParams +{ +}; + +struct InitializePersistentOriginParams +{ + PrincipalInfo principalInfo; +}; + +struct InitializeTemporaryOriginParams +{ + PersistenceType persistenceType; + PrincipalInfo principalInfo; +}; + +struct GetFullOriginMetadataParams +{ + PersistenceType persistenceType; + PrincipalInfo principalInfo; +}; + +struct AllUsageParams +{ + bool getAll; +}; + +struct OriginUsageParams +{ + PrincipalInfo principalInfo; + bool fromMemory; +}; + +union UsageRequestParams +{ + AllUsageParams; + OriginUsageParams; +}; + +struct ClearResetOriginParams +{ + PrincipalInfo principalInfo; + PersistenceType persistenceType; + bool persistenceTypeIsExplicit; + Type clientType; + bool clientTypeIsExplicit; +}; + +struct ClearOriginParams +{ + ClearResetOriginParams commonParams; + bool matchAll; +}; + +struct ResetOriginParams +{ + ClearResetOriginParams commonParams; +}; + +struct ClearDataParams +{ + OriginAttributesPattern pattern; +}; + +struct ClearAllParams +{ +}; + +struct ResetAllParams +{ +}; + +struct PersistedParams +{ + PrincipalInfo principalInfo; +}; + +struct PersistParams +{ + PrincipalInfo principalInfo; +}; + +struct EstimateParams +{ + PrincipalInfo principalInfo; +}; + +struct ListOriginsParams +{ +}; + +union RequestParams +{ + StorageNameParams; + StorageInitializedParams; + TemporaryStorageInitializedParams; + InitParams; + InitTemporaryStorageParams; + InitializePersistentOriginParams; + InitializeTemporaryOriginParams; + GetFullOriginMetadataParams; + ClearOriginParams; + ResetOriginParams; + ClearDataParams; + ClearAllParams; + ResetAllParams; + PersistedParams; + PersistParams; + EstimateParams; + ListOriginsParams; +}; + +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +protocol PQuota +{ + manager PBackground; + + manages PQuotaRequest; + manages PQuotaUsageRequest; + +parent: + async __delete__(); + + async PQuotaUsageRequest(UsageRequestParams params); + + async PQuotaRequest(RequestParams params); + + async StartIdleMaintenance(); + + async StopIdleMaintenance(); + + async AbortOperationsForProcess(ContentParentId contentParentId); +}; + +} // namespace quota +} // namespace dom +} // namespace mozilla diff --git a/dom/quota/PQuotaRequest.ipdl b/dom/quota/PQuotaRequest.ipdl new file mode 100644 index 0000000000..b155bda557 --- /dev/null +++ b/dom/quota/PQuotaRequest.ipdl @@ -0,0 +1,127 @@ +/* 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 PQuota; + +include "mozilla/dom/quota/SerializationHelpers.h"; + +using mozilla::dom::quota::FullOriginMetadata + from "mozilla/dom/quota/CommonMetadata.h"; + +namespace mozilla { +namespace dom { +namespace quota { + +struct StorageNameResponse +{ + nsString name; +}; + +struct StorageInitializedResponse +{ + bool initialized; +}; + +struct TemporaryStorageInitializedResponse +{ + bool initialized; +}; + +struct InitResponse +{ +}; + +struct InitTemporaryStorageResponse +{ +}; + +struct InitializePersistentOriginResponse +{ + bool created; +}; + +struct InitializeTemporaryOriginResponse +{ + bool created; +}; + +struct GetFullOriginMetadataResponse +{ + FullOriginMetadata? maybeFullOriginMetadata; +}; + +struct ClearOriginResponse +{ +}; + +struct ResetOriginResponse +{ +}; + +struct ClearDataResponse +{ +}; + +struct ClearAllResponse +{ +}; + +struct ResetAllResponse +{ +}; + +struct PersistedResponse +{ + bool persisted; +}; + +struct PersistResponse +{ +}; + +struct EstimateResponse +{ + uint64_t usage; + uint64_t limit; +}; + +struct ListOriginsResponse +{ + nsCString[] origins; +}; + +union RequestResponse +{ + nsresult; + StorageNameResponse; + StorageInitializedResponse; + TemporaryStorageInitializedResponse; + InitResponse; + InitTemporaryStorageResponse; + InitializePersistentOriginResponse; + InitializeTemporaryOriginResponse; + GetFullOriginMetadataResponse; + ClearOriginResponse; + ResetOriginResponse; + ClearDataResponse; + ClearAllResponse; + ResetAllResponse; + PersistedResponse; + PersistResponse; + EstimateResponse; + ListOriginsResponse; +}; + +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +protocol PQuotaRequest +{ + manager PQuota; + +child: + async __delete__(RequestResponse response); +}; + +} // namespace quota +} // namespace dom +} // namespace mozilla diff --git a/dom/quota/PQuotaUsageRequest.ipdl b/dom/quota/PQuotaUsageRequest.ipdl new file mode 100644 index 0000000000..26401c84e6 --- /dev/null +++ b/dom/quota/PQuotaUsageRequest.ipdl @@ -0,0 +1,51 @@ +/* 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 PQuota; + +namespace mozilla { +namespace dom { +namespace quota { + +struct OriginUsage +{ + nsCString origin; + bool persisted; + uint64_t usage; + uint64_t lastAccessed; +}; + +struct AllUsageResponse +{ + OriginUsage[] originUsages; +}; + +struct OriginUsageResponse +{ + uint64_t usage; + uint64_t fileUsage; +}; + +union UsageRequestResponse +{ + nsresult; + AllUsageResponse; + OriginUsageResponse; +}; + +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +protocol PQuotaUsageRequest +{ + manager PQuota; + +parent: + async Cancel(); + +child: + async __delete__(UsageRequestResponse response); +}; + +} // namespace quota +} // namespace dom +} // namespace mozilla diff --git a/dom/quota/PRemoteQuotaObject.ipdl b/dom/quota/PRemoteQuotaObject.ipdl new file mode 100644 index 0000000000..f0221d7b9c --- /dev/null +++ b/dom/quota/PRemoteQuotaObject.ipdl @@ -0,0 +1,22 @@ +/* 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/. */ + +namespace mozilla { +namespace dom { +namespace quota { + +sync protocol PRemoteQuotaObject +{ + parent: + // This must be synchronous because we don't have quota file stream wrappers + // which would be fully asynchronous (non-blocking). Given that, this message + // should never be sent on the main thread or the PBackground thread or a DOM + // worker thread. + sync MaybeUpdateSize(int64_t size, bool truncate) + returns(bool result); +}; + +} // namespace quota +} // namespace dom +} // namespace mozilla diff --git a/dom/quota/PersistenceType.cpp b/dom/quota/PersistenceType.cpp new file mode 100644 index 0000000000..637175c886 --- /dev/null +++ b/dom/quota/PersistenceType.cpp @@ -0,0 +1,202 @@ +/* -*- 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 "PersistenceType.h" + +#include <utility> +#include "nsIFile.h" +#include "nsLiteralString.h" +#include "nsString.h" + +namespace mozilla::dom::quota { + +namespace { + +constexpr auto kPersistentCString = "persistent"_ns; +constexpr auto kTemporaryCString = "temporary"_ns; +constexpr auto kDefaultCString = "default"_ns; + +constexpr auto kPermanentString = u"permanent"_ns; +constexpr auto kTemporaryString = u"temporary"_ns; +constexpr auto kDefaultString = u"default"_ns; + +static_assert(PERSISTENCE_TYPE_PERSISTENT == 0 && + PERSISTENCE_TYPE_TEMPORARY == 1 && + PERSISTENCE_TYPE_DEFAULT == 2 && + PERSISTENCE_TYPE_INVALID == 3, + "Incorrect enum values!"); + +template <PersistenceType type> +struct PersistenceTypeTraits; + +template <> +struct PersistenceTypeTraits<PERSISTENCE_TYPE_PERSISTENT> { + template <typename T> + static T To(); + + static bool From(const nsACString& aString) { + return aString == kPersistentCString; + } + + static bool From(const int32_t aInt32) { return aInt32 == 0; } + + static bool From(nsIFile& aFile) { + nsAutoString leafName; + MOZ_ALWAYS_SUCCEEDS(aFile.GetLeafName(leafName)); + return leafName == kPermanentString; + } +}; + +template <> +nsLiteralCString +PersistenceTypeTraits<PERSISTENCE_TYPE_PERSISTENT>::To<nsLiteralCString>() { + return kPersistentCString; +} + +template <> +struct PersistenceTypeTraits<PERSISTENCE_TYPE_TEMPORARY> { + template <typename T> + static T To(); + + static bool From(const nsACString& aString) { + return aString == kTemporaryCString; + } + + static bool From(const int32_t aInt32) { return aInt32 == 1; } + + static bool From(nsIFile& aFile) { + nsAutoString leafName; + MOZ_ALWAYS_SUCCEEDS(aFile.GetLeafName(leafName)); + return leafName == kTemporaryString; + } +}; + +template <> +nsLiteralCString +PersistenceTypeTraits<PERSISTENCE_TYPE_TEMPORARY>::To<nsLiteralCString>() { + return kTemporaryCString; +} + +template <> +struct PersistenceTypeTraits<PERSISTENCE_TYPE_DEFAULT> { + template <typename T> + static T To(); + + static bool From(const nsACString& aString) { + return aString == kDefaultCString; + } + + static bool From(const int32_t aInt32) { return aInt32 == 2; } + + static bool From(nsIFile& aFile) { + nsAutoString leafName; + MOZ_ALWAYS_SUCCEEDS(aFile.GetLeafName(leafName)); + return leafName == kDefaultString; + } +}; + +template <> +nsLiteralCString +PersistenceTypeTraits<PERSISTENCE_TYPE_DEFAULT>::To<nsLiteralCString>() { + return kDefaultCString; +} + +template <typename T> +Maybe<T> TypeTo_impl(const PersistenceType aPersistenceType) { + switch (aPersistenceType) { + case PERSISTENCE_TYPE_PERSISTENT: + return Some(PersistenceTypeTraits<PERSISTENCE_TYPE_PERSISTENT>::To<T>()); + + case PERSISTENCE_TYPE_TEMPORARY: + return Some(PersistenceTypeTraits<PERSISTENCE_TYPE_TEMPORARY>::To<T>()); + + case PERSISTENCE_TYPE_DEFAULT: + return Some(PersistenceTypeTraits<PERSISTENCE_TYPE_DEFAULT>::To<T>()); + + default: + return Nothing(); + } +} + +template <typename T> +Maybe<PersistenceType> TypeFrom_impl(T& aData) { + if (PersistenceTypeTraits<PERSISTENCE_TYPE_PERSISTENT>::From(aData)) { + return Some(PERSISTENCE_TYPE_PERSISTENT); + } + + if (PersistenceTypeTraits<PERSISTENCE_TYPE_TEMPORARY>::From(aData)) { + return Some(PERSISTENCE_TYPE_TEMPORARY); + } + + if (PersistenceTypeTraits<PERSISTENCE_TYPE_DEFAULT>::From(aData)) { + return Some(PERSISTENCE_TYPE_DEFAULT); + } + + return Nothing(); +} + +void BadPersistenceType() { MOZ_CRASH("Bad persistence type value!"); } + +} // namespace + +bool IsValidPersistenceType(const PersistenceType aPersistenceType) { + switch (aPersistenceType) { + case PERSISTENCE_TYPE_PERSISTENT: + case PERSISTENCE_TYPE_TEMPORARY: + case PERSISTENCE_TYPE_DEFAULT: + return true; + + default: + return false; + } +} + +bool IsBestEffortPersistenceType(const PersistenceType aPersistenceType) { + switch (aPersistenceType) { + case PERSISTENCE_TYPE_TEMPORARY: + case PERSISTENCE_TYPE_DEFAULT: + return true; + + case PERSISTENCE_TYPE_PERSISTENT: + case PERSISTENCE_TYPE_INVALID: + default: + return false; + } +} + +nsLiteralCString PersistenceTypeToString( + const PersistenceType aPersistenceType) { + const auto maybeString = TypeTo_impl<nsLiteralCString>(aPersistenceType); + if (maybeString.isNothing()) { + BadPersistenceType(); + } + return maybeString.value(); +} + +Maybe<PersistenceType> PersistenceTypeFromString(const nsACString& aString, + const fallible_t&) { + return TypeFrom_impl(aString); +} + +PersistenceType PersistenceTypeFromString(const nsACString& aString) { + const auto maybePersistenceType = TypeFrom_impl(aString); + if (maybePersistenceType.isNothing()) { + BadPersistenceType(); + } + return maybePersistenceType.value(); +} + +Maybe<PersistenceType> PersistenceTypeFromInt32(const int32_t aInt32, + const fallible_t&) { + return TypeFrom_impl(aInt32); +} + +Maybe<PersistenceType> PersistenceTypeFromFile(nsIFile& aFile, + const fallible_t&) { + return TypeFrom_impl(aFile); +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/PersistenceType.h b/dom/quota/PersistenceType.h new file mode 100644 index 0000000000..0fcab4ff63 --- /dev/null +++ b/dom/quota/PersistenceType.h @@ -0,0 +1,69 @@ +/* -*- 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_quota_persistencetype_h__ +#define mozilla_dom_quota_persistencetype_h__ + +#include <cstdint> +#include "mozilla/Assertions.h" +#include "mozilla/Maybe.h" +#include "mozilla/fallible.h" +#include "nsStringFwd.h" + +class nsIFile; + +namespace mozilla::dom::quota { + +enum PersistenceType { + PERSISTENCE_TYPE_PERSISTENT = 0, + PERSISTENCE_TYPE_TEMPORARY, + PERSISTENCE_TYPE_DEFAULT, + + // Only needed for IPC serialization helper, should never be used in code. + PERSISTENCE_TYPE_INVALID +}; + +static const PersistenceType kAllPersistenceTypes[] = { + PERSISTENCE_TYPE_PERSISTENT, PERSISTENCE_TYPE_TEMPORARY, + PERSISTENCE_TYPE_DEFAULT}; + +static const PersistenceType kBestEffortPersistenceTypes[] = { + PERSISTENCE_TYPE_TEMPORARY, PERSISTENCE_TYPE_DEFAULT}; + +bool IsValidPersistenceType(PersistenceType aPersistenceType); + +bool IsBestEffortPersistenceType(const PersistenceType aPersistenceType); + +nsLiteralCString PersistenceTypeToString(PersistenceType aPersistenceType); + +Maybe<PersistenceType> PersistenceTypeFromString(const nsACString& aString, + const fallible_t&); + +PersistenceType PersistenceTypeFromString(const nsACString& aString); + +Maybe<PersistenceType> PersistenceTypeFromInt32(int32_t aInt32, + const fallible_t&); + +// aFile is expected to be a repository directory (not some file or directory +// within that). +Maybe<PersistenceType> PersistenceTypeFromFile(nsIFile& aFile, + const fallible_t&); + +inline PersistenceType ComplementaryPersistenceType( + const PersistenceType aPersistenceType) { + MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT || + aPersistenceType == PERSISTENCE_TYPE_TEMPORARY); + + if (aPersistenceType == PERSISTENCE_TYPE_DEFAULT) { + return PERSISTENCE_TYPE_TEMPORARY; + } + + return PERSISTENCE_TYPE_DEFAULT; +} + +} // namespace mozilla::dom::quota + +#endif // mozilla_dom_quota_persistencetype_h__ diff --git a/dom/quota/QMResult.cpp b/dom/quota/QMResult.cpp new file mode 100644 index 0000000000..1b743679ad --- /dev/null +++ b/dom/quota/QMResult.cpp @@ -0,0 +1,26 @@ +/* -*- 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 "QMResult.h" + +#ifdef QM_ERROR_STACKS_ENABLED +# include "mozilla/Atomics.h" +#endif + +namespace mozilla { + +#ifdef QM_ERROR_STACKS_ENABLED +namespace { + +static Atomic<uint64_t> gLastStackId{0}; + +} + +QMResult::QMResult(nsresult aNSResult) + : mStackId(++gLastStackId), mFrameId(1), mNSResult(aNSResult) {} +#endif + +} // namespace mozilla diff --git a/dom/quota/QMResult.h b/dom/quota/QMResult.h new file mode 100644 index 0000000000..f47c82584e --- /dev/null +++ b/dom/quota/QMResult.h @@ -0,0 +1,58 @@ +/* -*- 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_QUOTA_QMRESULT_H_ +#define DOM_QUOTA_QMRESULT_H_ + +#include "ErrorList.h" +#include "mozilla/dom/quota/Config.h" +#include "mozilla/dom/quota/ForwardDecls.h" + +namespace mozilla { + +#ifdef QM_ERROR_STACKS_ENABLED +// A wrapped nsresult, primarily intended for use along with mozilla::Result +// and QM_TRY macros. The wrapper contains stack id and frame id which are +// reported in LogError besides the error result itself. +// +// XXX Document the general situation more, bug 1709777. +class QMResult { + uint64_t mStackId; + uint32_t mFrameId; + nsresult mNSResult; + + public: + QMResult() : QMResult(NS_OK) {} + + explicit QMResult(nsresult aNSResult); + + uint64_t StackId() const { return mStackId; } + + uint32_t FrameId() const { return mFrameId; } + + nsresult NSResult() const { return mNSResult; } + + /** + * Propagate the result. + * + * This is used by GenericErrorResult<QMResult> to create a propagated + * result. + */ + QMResult Propagate() const { + return QMResult{mStackId, mFrameId + 1, mNSResult}; + } + + private: + QMResult(uint64_t aStackId, uint32_t aFrameId, nsresult aNSResult) + : mStackId(aStackId), mFrameId(aFrameId), mNSResult(aNSResult) {} +}; +#endif + +inline QMResult ToQMResult(nsresult aValue) { return QMResult(aValue); } + +} // namespace mozilla + +#endif diff --git a/dom/quota/QuotaCommon.cpp b/dom/quota/QuotaCommon.cpp new file mode 100644 index 0000000000..e2df8a1082 --- /dev/null +++ b/dom/quota/QuotaCommon.cpp @@ -0,0 +1,639 @@ +/* -*- 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 "mozilla/dom/quota/QuotaCommon.h" + +#ifdef QM_ERROR_STACKS_ENABLED +# include "base/process_util.h" +#endif +#include "mozIStorageConnection.h" +#include "mozIStorageStatement.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Logging.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryComms.h" +#include "mozilla/TelemetryEventEnums.h" +#include "mozilla/TextUtils.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/ScopedLogExtraInfo.h" +#include "nsIConsoleService.h" +#include "nsIFile.h" +#include "nsServiceManagerUtils.h" +#include "nsStringFlags.h" +#include "nsTStringRepr.h" +#include "nsUnicharUtils.h" +#include "nsXPCOM.h" +#include "nsXULAppAPI.h" + +#ifdef XP_WIN +# include "mozilla/Atomics.h" +# include "mozilla/ipc/BackgroundParent.h" +# include "mozilla/StaticPrefs_dom.h" +# include "nsILocalFileWin.h" +#endif + +namespace mozilla { + +RefPtr<BoolPromise> CreateAndRejectBoolPromise(const char* aFunc, + nsresult aRv) { + return CreateAndRejectMozPromise<BoolPromise>(aFunc, aRv); +} + +RefPtr<Int64Promise> CreateAndRejectInt64Promise(const char* aFunc, + nsresult aRv) { + return CreateAndRejectMozPromise<Int64Promise>(aFunc, aRv); +} + +RefPtr<BoolPromise> CreateAndRejectBoolPromiseFromQMResult( + const char* aFunc, const QMResult& aRv) { + return CreateAndRejectMozPromise<BoolPromise>(aFunc, aRv); +} + +namespace dom::quota { + +using namespace mozilla::Telemetry; + +namespace { + +#ifdef DEBUG +constexpr auto kDSStoreFileName = u".DS_Store"_ns; +constexpr auto kDesktopFileName = u".desktop"_ns; +constexpr auto kDesktopIniFileName = u"desktop.ini"_ns; +constexpr auto kThumbsDbFileName = u"thumbs.db"_ns; +#endif + +#ifdef XP_WIN +Atomic<int32_t> gUseDOSDevicePathSyntax(-1); +#endif + +LazyLogModule gLogger("QuotaManager"); + +void AnonymizeCString(nsACString& aCString, uint32_t aStart) { + MOZ_ASSERT(!aCString.IsEmpty()); + MOZ_ASSERT(aStart < aCString.Length()); + + char* iter = aCString.BeginWriting() + aStart; + char* end = aCString.EndWriting(); + + while (iter != end) { + char c = *iter; + + if (IsAsciiAlpha(c)) { + *iter = 'a'; + } else if (IsAsciiDigit(c)) { + *iter = 'D'; + } + + ++iter; + } +} + +} // namespace + +const char kQuotaGenericDelimiter = '|'; + +#ifdef NIGHTLY_BUILD +const nsLiteralCString kQuotaInternalError = "internal"_ns; +const nsLiteralCString kQuotaExternalError = "external"_ns; +#endif + +LogModule* GetQuotaManagerLogger() { return gLogger; } + +void AnonymizeCString(nsACString& aCString) { + if (aCString.IsEmpty()) { + return; + } + AnonymizeCString(aCString, /* aStart */ 0); +} + +void AnonymizeOriginString(nsACString& aOriginString) { + if (aOriginString.IsEmpty()) { + return; + } + + int32_t start = aOriginString.FindChar(':'); + if (start < 0) { + start = 0; + } + + AnonymizeCString(aOriginString, start); +} + +#ifdef XP_WIN +void CacheUseDOSDevicePathSyntaxPrefValue() { + MOZ_ASSERT(XRE_IsParentProcess()); + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (gUseDOSDevicePathSyntax == -1) { + bool useDOSDevicePathSyntax = + StaticPrefs::dom_quotaManager_useDOSDevicePathSyntax_DoNotUseDirectly(); + gUseDOSDevicePathSyntax = useDOSDevicePathSyntax ? 1 : 0; + } +} +#endif + +Result<nsCOMPtr<nsIFile>, nsresult> QM_NewLocalFile(const nsAString& aPath) { + QM_TRY_UNWRAP( + auto file, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<nsIFile>, NS_NewLocalFile, aPath, + /* aFollowLinks */ false), + QM_PROPAGATE, [&aPath](const nsresult rv) { + QM_WARNING("Failed to construct a file for path (%s)", + NS_ConvertUTF16toUTF8(aPath).get()); + }); + +#ifdef XP_WIN + MOZ_ASSERT(gUseDOSDevicePathSyntax != -1); + + if (gUseDOSDevicePathSyntax) { + QM_TRY_INSPECT( + const auto& winFile, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsILocalFileWin>, + MOZ_SELECT_OVERLOAD(do_QueryInterface), file)); + + MOZ_ASSERT(winFile); + winFile->SetUseDOSDevicePathSyntax(true); + } +#endif + + return file; +} + +nsDependentCSubstring GetLeafName(const nsACString& aPath) { + nsACString::const_iterator start, end; + aPath.BeginReading(start); + aPath.EndReading(end); + + bool found = RFindInReadable("/"_ns, start, end); + if (found) { + start = end; + } + + aPath.EndReading(end); + + return nsDependentCSubstring(start.get(), end.get()); +} + +Result<nsCOMPtr<nsIFile>, nsresult> CloneFileAndAppend( + nsIFile& aDirectory, const nsAString& aPathElement) { + QM_TRY_UNWRAP(auto resultFile, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIFile>, aDirectory, Clone)); + + QM_TRY(MOZ_TO_RESULT(resultFile->Append(aPathElement))); + + return resultFile; +} + +Result<nsIFileKind, nsresult> GetDirEntryKind(nsIFile& aFile) { + // Callers call this function without checking if the directory already + // exists (idempotent usage). QM_OR_ELSE_WARN_IF is not used here since we + // just want to log NS_ERROR_FILE_NOT_FOUND and NS_ERROR_FILE_FS_CORRUPTED + // results and not spam the reports. + QM_TRY_RETURN(QM_OR_ELSE_LOG_VERBOSE_IF( + MOZ_TO_RESULT_INVOKE_MEMBER(aFile, IsDirectory) + .map([](const bool isDirectory) { + return isDirectory ? nsIFileKind::ExistsAsDirectory + : nsIFileKind::ExistsAsFile; + }), + ([](const nsresult rv) { + return rv == NS_ERROR_FILE_NOT_FOUND || + // We treat NS_ERROR_FILE_FS_CORRUPTED as if the file did not + // exist at all. + rv == NS_ERROR_FILE_FS_CORRUPTED; + }), + ErrToOk<nsIFileKind::DoesNotExist>)); +} + +Result<nsCOMPtr<mozIStorageStatement>, nsresult> CreateStatement( + mozIStorageConnection& aConnection, const nsACString& aStatementString) { + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement, + aStatementString)); +} + +template <SingleStepResult ResultHandling> +Result<SingleStepSuccessType<ResultHandling>, nsresult> ExecuteSingleStep( + nsCOMPtr<mozIStorageStatement>&& aStatement) { + QM_TRY_INSPECT(const bool& hasResult, + MOZ_TO_RESULT_INVOKE_MEMBER(aStatement, ExecuteStep)); + + if constexpr (ResultHandling == SingleStepResult::AssertHasResult) { + MOZ_ASSERT(hasResult); + (void)hasResult; + + return WrapNotNullUnchecked(std::move(aStatement)); + } else { + return hasResult ? std::move(aStatement) : nullptr; + } +} + +template Result<SingleStepSuccessType<SingleStepResult::AssertHasResult>, + nsresult> +ExecuteSingleStep<SingleStepResult::AssertHasResult>( + nsCOMPtr<mozIStorageStatement>&&); + +template Result<SingleStepSuccessType<SingleStepResult::ReturnNullIfNoResult>, + nsresult> +ExecuteSingleStep<SingleStepResult::ReturnNullIfNoResult>( + nsCOMPtr<mozIStorageStatement>&&); + +template <SingleStepResult ResultHandling> +Result<SingleStepSuccessType<ResultHandling>, nsresult> +CreateAndExecuteSingleStepStatement(mozIStorageConnection& aConnection, + const nsACString& aStatementString) { + QM_TRY_UNWRAP(auto stmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, + CreateStatement, aStatementString)); + + return ExecuteSingleStep<ResultHandling>(std::move(stmt)); +} + +template Result<SingleStepSuccessType<SingleStepResult::AssertHasResult>, + nsresult> +CreateAndExecuteSingleStepStatement<SingleStepResult::AssertHasResult>( + mozIStorageConnection& aConnection, const nsACString& aStatementString); + +template Result<SingleStepSuccessType<SingleStepResult::ReturnNullIfNoResult>, + nsresult> +CreateAndExecuteSingleStepStatement<SingleStepResult::ReturnNullIfNoResult>( + mozIStorageConnection& aConnection, const nsACString& aStatementString); + +namespace detail { + +// Given aPath of /foo/bar/baz and aRelativePath of /bar/baz, returns the +// absolute portion of aPath /foo by removing the common suffix from aPath. +nsDependentCSubstring GetTreeBase(const nsLiteralCString& aPath, + const nsLiteralCString& aRelativePath) { + MOZ_ASSERT(StringEndsWith(aPath, aRelativePath)); + return Substring(aPath, 0, aPath.Length() - aRelativePath.Length()); +} + +nsDependentCSubstring GetSourceTreeBase() { + static constexpr auto thisSourceFileRelativePath = + "/dom/quota/QuotaCommon.cpp"_ns; + + return GetTreeBase(nsLiteralCString(__FILE__), thisSourceFileRelativePath); +} + +nsDependentCSubstring GetObjdirDistIncludeTreeBase( + const nsLiteralCString& aQuotaCommonHPath) { + static constexpr auto quotaCommonHSourceFileRelativePath = + "/mozilla/dom/quota/QuotaCommon.h"_ns; + + return GetTreeBase(aQuotaCommonHPath, quotaCommonHSourceFileRelativePath); +} + +static constexpr auto kSourceFileRelativePathMap = + std::array<std::pair<nsLiteralCString, nsLiteralCString>, 1>{ + {{"mozilla/dom/LocalStorageCommon.h"_ns, + "dom/localstorage/LocalStorageCommon.h"_ns}}}; + +nsDependentCSubstring MakeSourceFileRelativePath( + const nsACString& aSourceFilePath) { + static constexpr auto error = "ERROR"_ns; + static constexpr auto mozillaRelativeBase = "mozilla/"_ns; + + static const auto sourceTreeBase = GetSourceTreeBase(); + + if (MOZ_LIKELY(StringBeginsWith(aSourceFilePath, sourceTreeBase))) { + return Substring(aSourceFilePath, sourceTreeBase.Length() + 1); + } + + // The source file could have been exported to the OBJDIR/dist/include + // directory, so we need to check that case as well. + static const auto objdirDistIncludeTreeBase = GetObjdirDistIncludeTreeBase(); + + if (MOZ_LIKELY( + StringBeginsWith(aSourceFilePath, objdirDistIncludeTreeBase))) { + const auto sourceFileRelativePath = + Substring(aSourceFilePath, objdirDistIncludeTreeBase.Length() + 1); + + // Exported source files don't have to use the same directory structure as + // original source files. Check if we have a mapping for the exported + // source file. + const auto foundIt = std::find_if( + kSourceFileRelativePathMap.cbegin(), kSourceFileRelativePathMap.cend(), + [&sourceFileRelativePath](const auto& entry) { + return entry.first == sourceFileRelativePath; + }); + + if (MOZ_UNLIKELY(foundIt != kSourceFileRelativePathMap.cend())) { + return Substring(foundIt->second, 0); + } + + // If we don't have a mapping for it, just remove the mozilla/ prefix + // (if there's any). + if (MOZ_LIKELY( + StringBeginsWith(sourceFileRelativePath, mozillaRelativeBase))) { + return Substring(sourceFileRelativePath, mozillaRelativeBase.Length()); + } + + // At this point, we don't know how to transform the relative path of the + // exported source file back to the relative path of the original source + // file. This can happen when QM_TRY is used in an exported nsIFoo.h file. + // If you really need to use QM_TRY there, consider adding a new mapping + // for the exported source file. + return sourceFileRelativePath; + } + + nsCString::const_iterator begin, end; + if (RFindInReadable("/"_ns, aSourceFilePath.BeginReading(begin), + aSourceFilePath.EndReading(end))) { + // Use the basename as a fallback, to avoid exposing any user parts of the + // path. + ++begin; + return Substring(begin, aSourceFilePath.EndReading(end)); + } + + return nsDependentCSubstring{static_cast<mozilla::Span<const char>>( + static_cast<const nsCString&>(error))}; +} + +} // namespace detail + +#ifdef QM_LOG_ERROR_ENABLED +# ifdef QM_ERROR_STACKS_ENABLED +void LogError(const nsACString& aExpr, const ResultType& aResult, + const nsACString& aSourceFilePath, const int32_t aSourceFileLine, + const Severity aSeverity) +# else +void LogError(const nsACString& aExpr, const Maybe<nsresult> aMaybeRv, + const nsACString& aSourceFilePath, const int32_t aSourceFileLine, + const Severity aSeverity) +# endif +{ + // TODO: Add MOZ_LOG support, bug 1711661. + + // We have to ignore failures with the Verbose severity until we have support + // for MOZ_LOG. + if (aSeverity == Severity::Verbose) { + return; + } + + nsAutoCString context; + +# ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + const auto& extraInfoMap = ScopedLogExtraInfo::GetExtraInfoMap(); + + if (const auto contextIt = extraInfoMap.find(ScopedLogExtraInfo::kTagContext); + contextIt != extraInfoMap.cend()) { + context = *contextIt->second; + } +# endif + + const auto severityString = [&aSeverity]() -> nsLiteralCString { + switch (aSeverity) { + case Severity::Error: + return "ERROR"_ns; + case Severity::Warning: + return "WARNING"_ns; + case Severity::Info: + return "INFO"_ns; + case Severity::Verbose: + return "VERBOSE"_ns; + } + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Bad severity value!"); + }(); + + Maybe<nsresult> maybeRv; + +# ifdef QM_ERROR_STACKS_ENABLED + if (aResult.is<QMResult>()) { + maybeRv = Some(aResult.as<QMResult>().NSResult()); + } else if (aResult.is<nsresult>()) { + maybeRv = Some(aResult.as<nsresult>()); + } +# else + maybeRv = aMaybeRv; +# endif + + nsAutoCString rvCode; + nsAutoCString rvName; + + if (maybeRv) { + nsresult rv = *maybeRv; + + rvCode = nsPrintfCString("0x%" PRIX32, static_cast<uint32_t>(rv)); + + // XXX NS_ERROR_MODULE_WIN32 should be handled in GetErrorName directly. + if (NS_ERROR_GET_MODULE(rv) == NS_ERROR_MODULE_WIN32) { + // XXX We could also try to get the Win32 error name here. + rvName = nsPrintfCString( + "NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_WIN32, 0x%" PRIX16 ")", + NS_ERROR_GET_CODE(rv)); + } else { + mozilla::GetErrorName(rv, rvName); + } + } + +# ifdef QM_ERROR_STACKS_ENABLED + nsAutoCString frameIdString; + nsAutoCString stackIdString; + nsAutoCString processIdString; + + if (aResult.is<QMResult>()) { + const QMResult& result = aResult.as<QMResult>(); + frameIdString = IntToCString(result.FrameId()); + stackIdString = IntToCString(result.StackId()); + processIdString = + IntToCString(static_cast<uint32_t>(base::GetCurrentProcId())); + } +# endif + + nsAutoCString extraInfosString; + + if (!rvCode.IsEmpty()) { + extraInfosString.Append(" failed with resultCode "_ns + rvCode); + } + + if (!rvName.IsEmpty()) { + extraInfosString.Append(", resultName "_ns + rvName); + } + +# ifdef QM_ERROR_STACKS_ENABLED + if (!frameIdString.IsEmpty()) { + extraInfosString.Append(", frameId "_ns + frameIdString); + } + + if (!stackIdString.IsEmpty()) { + extraInfosString.Append(", stackId "_ns + stackIdString); + } + + if (!processIdString.IsEmpty()) { + extraInfosString.Append(", processId "_ns + processIdString); + } +# endif + +# ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + for (const auto& item : extraInfoMap) { + extraInfosString.Append(", "_ns + nsDependentCString(item.first) + " "_ns + + *item.second); + } +# endif + + const auto sourceFileRelativePath = + detail::MakeSourceFileRelativePath(aSourceFilePath); + +# ifdef QM_LOG_ERROR_TO_CONSOLE_ENABLED + NS_DebugBreak( + NS_DEBUG_WARNING, + nsAutoCString("QM_TRY failure ("_ns + severityString + ")"_ns).get(), + (extraInfosString.IsEmpty() ? nsPromiseFlatCString(aExpr) + : static_cast<const nsCString&>(nsAutoCString( + aExpr + extraInfosString))) + .get(), + nsPromiseFlatCString(sourceFileRelativePath).get(), aSourceFileLine); +# endif + +# ifdef QM_LOG_ERROR_TO_BROWSER_CONSOLE_ENABLED + // XXX We might want to allow reporting to the browsing console even when + // there's no context in future once we are sure that it can't spam the + // browser console or when we have special about:quotamanager for the + // reporting (instead of the browsing console). + // Another option is to keep the current check and rely on MOZ_LOG reporting + // in future once that's available. + if (!context.IsEmpty()) { + nsCOMPtr<nsIConsoleService> console = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (console) { + NS_ConvertUTF8toUTF16 message( + "QM_TRY failure ("_ns + severityString + ")"_ns + ": '"_ns + aExpr + + extraInfosString + "', file "_ns + sourceFileRelativePath + ":"_ns + + IntToCString(aSourceFileLine)); + + // The concatenation above results in a message like: + // QM_TRY failure (ERROR): 'MaybeRemoveLocalStorageArchiveTmpFile() failed + // with resultCode 0x80004005, resultName NS_ERROR_FAILURE, frameId 1, + // stackId 1, processId 53978, context Initialization::Storage', file + // dom/quota/ActorsParent.cpp:6029 + + console->LogStringMessage(message.get()); + } + } +# endif + +# ifdef QM_LOG_ERROR_TO_TELEMETRY_ENABLED + if (!context.IsEmpty()) { + // For now, we don't include aExpr in the telemetry event. It might help to + // match locations across versions, but they might be large. + auto extra = Some([&] { + auto res = CopyableTArray<EventExtraEntry>{}; + res.SetCapacity(9); + + res.AppendElement(EventExtraEntry{"context"_ns, nsCString{context}}); + +# ifdef QM_ERROR_STACKS_ENABLED + if (!frameIdString.IsEmpty()) { + res.AppendElement( + EventExtraEntry{"frame_id"_ns, nsCString{frameIdString}}); + } + + if (!processIdString.IsEmpty()) { + res.AppendElement( + EventExtraEntry{"process_id"_ns, nsCString{processIdString}}); + } +# endif + + if (!rvName.IsEmpty()) { + res.AppendElement(EventExtraEntry{"result"_ns, nsCString{rvName}}); + } + + // Here, we are generating thread local sequence number and thread Id + // information which could be useful for summarizing and categorizing + // log statistics in QM_TRY stack propagation scripts. Since, this is + // a thread local object, we do not need to worry about data races. + static MOZ_THREAD_LOCAL(uint32_t) sSequenceNumber; + + // This would be initialized once, all subsequent calls would be a no-op. + MOZ_ALWAYS_TRUE(sSequenceNumber.init()); + + // sequence number should always starts at number 1. + // `sSequenceNumber` gets initialized to 0; so we have to increment here. + const auto newSeqNum = sSequenceNumber.get() + 1; + const auto threadId = + mozilla::baseprofiler::profiler_current_thread_id().ToNumber(); + + const auto threadIdAndSequence = + (static_cast<uint64_t>(threadId) << 32) | (newSeqNum & 0xFFFFFFFF); + + res.AppendElement( + EventExtraEntry{"seq"_ns, IntToCString(threadIdAndSequence)}); + + sSequenceNumber.set(newSeqNum); + + res.AppendElement(EventExtraEntry{"severity"_ns, severityString}); + + res.AppendElement( + EventExtraEntry{"source_file"_ns, nsCString(sourceFileRelativePath)}); + + res.AppendElement( + EventExtraEntry{"source_line"_ns, IntToCString(aSourceFileLine)}); + +# ifdef QM_ERROR_STACKS_ENABLED + if (!stackIdString.IsEmpty()) { + res.AppendElement( + EventExtraEntry{"stack_id"_ns, nsCString{stackIdString}}); + } +# endif + + return res; + }()); + + Telemetry::RecordEvent(Telemetry::EventID::DomQuotaTry_Error_Step, + Nothing(), extra); + } +# endif +} +#endif + +#ifdef DEBUG +Result<bool, nsresult> WarnIfFileIsUnknown(nsIFile& aFile, + const char* aSourceFilePath, + const int32_t aSourceFileLine) { + nsString leafName; + nsresult rv = aFile.GetLeafName(leafName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err(rv); + } + + bool isDirectory; + rv = aFile.IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err(rv); + } + + if (!isDirectory) { + // Don't warn about OS metadata files. These files are only used in + // different platforms, but the profile can be shared across different + // operating systems, so we check it on all platforms. + if (leafName.Equals(kDSStoreFileName) || + leafName.Equals(kDesktopFileName) || + leafName.Equals(kDesktopIniFileName, + nsCaseInsensitiveStringComparator) || + leafName.Equals(kThumbsDbFileName, nsCaseInsensitiveStringComparator)) { + return false; + } + + // Don't warn about files starting with ".". + if (leafName.First() == char16_t('.')) { + return false; + } + } + + NS_DebugBreak( + NS_DEBUG_WARNING, + nsPrintfCString("Something (%s) in the directory that doesn't belong!", + NS_ConvertUTF16toUTF8(leafName).get()) + .get(), + nullptr, aSourceFilePath, aSourceFileLine); + + return true; +} +#endif + +} // namespace dom::quota +} // namespace mozilla diff --git a/dom/quota/QuotaCommon.h b/dom/quota/QuotaCommon.h new file mode 100644 index 0000000000..848118797e --- /dev/null +++ b/dom/quota/QuotaCommon.h @@ -0,0 +1,1675 @@ +/* -*- 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_quota_quotacommon_h__ +#define mozilla_dom_quota_quotacommon_h__ + +#include "mozilla/dom/quota/Config.h" + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <type_traits> +#include <utility> +#include "mozIStorageStatement.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/Likely.h" +#include "mozilla/MacroArgs.h" +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#if defined(QM_LOG_ERROR_ENABLED) && defined(QM_ERROR_STACKS_ENABLED) +# include "mozilla/Variant.h" +#endif +#include "mozilla/dom/QMResult.h" +#include "mozilla/dom/quota/FirstInitializationAttemptsImpl.h" +#include "mozilla/dom/quota/RemoveParen.h" +#include "mozilla/dom/quota/ScopedLogExtraInfo.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIDirectoryEnumerator.h" +#include "nsIEventTarget.h" +#include "nsIFile.h" +#include "nsLiteralString.h" +#include "nsPrintfCString.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsTLiteralString.h" + +namespace mozilla { +template <typename T> +class NotNull; +} + +#define MOZ_ARGS_AFTER_3(a1, a2, a3, ...) __VA_ARGS__ + +#define MOZ_ADD_ARGS2(...) , ##__VA_ARGS__ +#define MOZ_ADD_ARGS(...) MOZ_ADD_ARGS2(__VA_ARGS__) + +// Proper use of unique variable names can be tricky (especially if nesting of +// the final macro is required). +// See https://lifecs.likai.org/2016/07/c-preprocessor-hygienic-macros.html +#define MOZ_UNIQUE_VAR(base) MOZ_CONCAT(base, __COUNTER__) + +// See https://florianjw.de/en/passing_overloaded_functions.html +// TODO: Add a test for this macro. +#define MOZ_SELECT_OVERLOAD(func) \ + [](auto&&... aArgs) -> decltype(auto) { \ + return func(std::forward<decltype(aArgs)>(aArgs)...); \ + } + +#define DSSTORE_FILE_NAME ".DS_Store" +#define DESKTOP_FILE_NAME ".desktop" +#define DESKTOP_INI_FILE_NAME "desktop.ini" +#define THUMBS_DB_FILE_NAME "thumbs.db" + +#define QM_WARNING(...) \ + do { \ + nsPrintfCString str(__VA_ARGS__); \ + mozilla::dom::quota::ReportInternalError(__FILE__, __LINE__, str.get()); \ + NS_WARNING(str.get()); \ + } while (0) + +#define QM_LOG_TEST() MOZ_LOG_TEST(GetQuotaManagerLogger(), LogLevel::Info) +#define QM_LOG(_args) MOZ_LOG(GetQuotaManagerLogger(), LogLevel::Info, _args) + +#define UNKNOWN_FILE_WARNING(_leafName) \ + NS_WARNING( \ + nsPrintfCString("Something (%s) in the directory that doesn't belong!", \ + NS_ConvertUTF16toUTF8(_leafName).get()) \ + .get()) + +// This macro should be used in directory traversals for files or directories +// that are unknown for given directory traversal. It should only be called +// after all known (directory traversal specific) files or directories have +// been checked and handled. +// XXX Consider renaming the macro to QM_LOG_UNKNOWN_DIR_ENTRY. +#ifdef DEBUG +# define WARN_IF_FILE_IS_UNKNOWN(_file) \ + mozilla::dom::quota::WarnIfFileIsUnknown(_file, __FILE__, __LINE__) +#else +# define WARN_IF_FILE_IS_UNKNOWN(_file) Result<bool, nsresult>(false) +#endif + +/** + * There are multiple ways to handle unrecoverable conditions (note that the + * patterns are put in reverse chronological order and only the first pattern + * QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT/QM_TRY_RETURN/QM_FAIL should be used in + * new code): + * + * 1. Using QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT/QM_TRY_RETURN/QM_FAIL macros + * (Quota manager specific, defined below) + * + * Typical use cases: + * + * nsresult MyFunc1(nsIFile& aFile) { + * bool exists; + * QM_TRY(aFile.Exists(&exists)); + * QM_TRY(OkIf(exists), NS_ERROR_FAILURE); + * + * // The file exists, and data could be read from it here. + * + * return NS_OK; + * } + * + * nsresult MyFunc2(nsIFile& aFile) { + * bool exists; + * QM_TRY(aFile.Exists(&exists), NS_ERROR_UNEXPECTED); + * QM_TRY(OkIf(exists), NS_ERROR_UNEXPECTED); + * + * // The file exists, and data could be read from it here. + * + * return NS_OK; + * } + * + * void MyFunc3(nsIFile& aFile) { + * bool exists; + * QM_TRY(aFile.Exists(&exists), QM_VOID); + * QM_TRY(OkIf(exists), QM_VOID); + * + * // The file exists, and data could be read from it here. + * } + * + * nsresult MyFunc4(nsIFile& aFile) { + * bool exists; + * QM_TRY(storageFile->Exists(&exists), QM_PROPAGATE, + * []() { NS_WARNING("The Exists call failed!"); }); + * QM_TRY(OkIf(exists), NS_ERROR_FAILURE, + * []() { NS_WARNING("The file doesn't exist!"); }); + * + * // The file exists, and data could be read from it here. + * + * return NS_OK; + * } + * + * nsresult MyFunc5(nsIFile& aFile) { + * bool exists; + * QM_TRY(aFile.Exists(&exists)); + * if (exists) { + * // The file exists, and data could be read from it here. + * } else { + * QM_FAIL(NS_ERROR_FAILURE); + * } + * + * return NS_OK; + * } + * + * nsresult MyFunc6(nsIFile& aFile) { + * bool exists; + * QM_TRY(aFile.Exists(&exists)); + * if (exists) { + * // The file exists, and data could be read from it here. + * } else { + * QM_FAIL(NS_ERROR_FAILURE, + * []() { NS_WARNING("The file doesn't exist!"); }); + * } + * + * return NS_OK; + * } + * + * 2. Using MOZ_TRY/MOZ_TRY_VAR macros + * + * Typical use cases: + * + * nsresult MyFunc1(nsIFile& aFile) { + * // MOZ_TRY can't return a custom return value + * + * return NS_OK; + * } + * + * nsresult MyFunc2(nsIFile& aFile) { + * // MOZ_TRY can't return a custom return value + * + * return NS_OK; + * } + * + * void MyFunc3(nsIFile& aFile) { + * // MOZ_TRY can't return a custom return value, "void" in this case + * } + * + * nsresult MyFunc4(nsIFile& aFile) { + * // MOZ_TRY can't return a custom return value and run an additional + * // cleanup function + * + * return NS_OK; + * } + * + * nsresult MyFunc5(nsIFile& aFile) { + * // There's no MOZ_FAIL, MOZ_TRY can't return a custom return value + * + * return NS_OK; + * } + * + * nsresult MyFunc6(nsIFile& aFile) { + * // There's no MOZ_FAIL, MOZ_TRY can't return a custom return value and run + * // an additional cleanup function + * + * return NS_OK; + * } + * + * 3. Using NS_WARN_IF and NS_WARNING macro with own control flow handling + * + * Typical use cases: + * + * nsresult MyFunc1(nsIFile& aFile) { + * bool exists; + * nsresult rv = aFile.Exists(&exists); + * if (NS_WARN_IF(NS_FAILED(rv)) { + * return rv; + * } + * if (NS_WARN_IF(!exists) { + * return NS_ERROR_FAILURE; + * } + * + * // The file exists, and data could be read from it here. + * + * return NS_OK; + * } + * + * nsresult MyFunc2(nsIFile& aFile) { + * bool exists; + * nsresult rv = aFile.Exists(&exists); + * if (NS_WARN_IF(NS_FAILED(rv)) { + * return NS_ERROR_UNEXPECTED; + * } + * if (NS_WARN_IF(!exists) { + * return NS_ERROR_UNEXPECTED; + * } + * + * // The file exists, and data could be read from it here. + * + * return NS_OK; + * } + * + * void MyFunc3(nsIFile& aFile) { + * bool exists; + * nsresult rv = aFile.Exists(&exists); + * if (NS_WARN_IF(NS_FAILED(rv)) { + * return; + * } + * if (NS_WARN_IF(!exists) { + * return; + * } + * + * // The file exists, and data could be read from it here. + * } + * + * nsresult MyFunc4(nsIFile& aFile) { + * bool exists; + * nsresult rv = aFile.Exists(&exists); + * if (NS_WARN_IF(NS_FAILED(rv)) { + * NS_WARNING("The Exists call failed!"); + * return rv; + * } + * if (NS_WARN_IF(!exists) { + * NS_WARNING("The file doesn't exist!"); + * return NS_ERROR_FAILURE; + * } + * + * // The file exists, and data could be read from it here. + * + * return NS_OK; + * } + * + * nsresult MyFunc5(nsIFile& aFile) { + * bool exists; + * nsresult rv = aFile.Exists(&exists); + * if (NS_WARN_IF(NS_FAILED(rv)) { + * return rv; + * } + * if (exists) { + * // The file exists, and data could be read from it here. + * } else { + * return NS_ERROR_FAILURE; + * } + * + * return NS_OK; + * } + * + * nsresult MyFunc6(nsIFile& aFile) { + * bool exists; + * nsresult rv = aFile.Exists(&exists); + * if (NS_WARN_IF(NS_FAILED(rv)) { + * return rv; + * } + * if (exists) { + * // The file exists, and data could be read from it here. + * } else { + * NS_WARNING("The file doesn't exist!"); + * return NS_ERROR_FAILURE; + * } + * + * return NS_OK; + * } + * + * 4. Using NS_ENSURE_* macros + * + * Mainly: + * - NS_ENSURE_SUCCESS + * - NS_ENSURE_SUCCESS_VOID + * - NS_ENSURE_TRUE + * - NS_ENSURE_TRUE_VOID + * + * Typical use cases: + * + * nsresult MyFunc1(nsIFile& aFile) { + * bool exists; + * nsresult rv = aFile.Exists(&exists); + * NS_ENSURE_SUCCESS(rv, rv); + * NS_ENSURE_TRUE(exists, NS_ERROR_FAILURE); + * + * // The file exists, and data could be read from it here. + * + * return NS_OK; + * } + * + * nsresult MyFunc2(nsIFile& aFile) { + * bool exists; + * nsresult rv = aFile.Exists(&exists); + * NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED); + * NS_ENSURE_TRUE(exists, NS_ERROR_UNEXPECTED); + * + * // The file exists, and data could be read from it here. + * + * return NS_OK; + * } + * + * void MyFunc3(nsIFile& aFile) { + * bool exists; + * nsresult rv = aFile.Exists(&exists); + * NS_ENSURE_SUCCESS_VOID(rv); + * NS_ENSURE_TRUE_VOID(exists); + * + * // The file exists, and data could be read from it here. + * } + * + * nsresult MyFunc4(nsIFile& aFile) { + * // NS_ENSURE_SUCCESS/NS_ENSURE_TRUE can't run an additional cleanup + * // function + * + * return NS_OK; + * } + * + * nsresult MyFunc5(nsIFile& aFile) { + * bool exists; + * nsresult rv = aFile.Exists(&exists); + * NS_ENSURE_SUCCESS(rv, rv); + * if (exists) { + * // The file exists, and data could be read from it here. + * } else { + * NS_ENSURE_TRUE(false, NS_ERROR_FAILURE); + * } + * + * return NS_OK; + * } + * + * nsresult MyFunc6(nsIFile& aFile) { + * // NS_ENSURE_TRUE can't run an additional cleanup function + * + * return NS_OK; + * } + * + * QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT is like MOZ_TRY/MOZ_TRY_VAR but if an + * error occurs it additionally calls a generic function HandleError to handle + * the error and it can be used to return custom return values as well and even + * call an additional cleanup function. + * HandleError currently only warns in debug builds, it will report to the + * browser console and telemetry in the future. + * The other advantage of QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT is that a local + * nsresult is not needed at all in all cases, all calls can be wrapped + * directly. If an error occurs, the warning contains a concrete call instead + * of the rv local variable. For example: + * + * 1. WARNING: NS_ENSURE_SUCCESS(rv, rv) failed with result 0x80004005 + * (NS_ERROR_FAILURE): file XYZ, line N + * + * 2. WARNING: 'NS_FAILED(rv)', file XYZ, line N + * + * 3. Nothing (MOZ_TRY doesn't warn) + * + * 4. WARNING: Error: 'aFile.Exists(&exists)', file XYZ, line N + * + * QM_TRY_RETURN is a supplementary macro for cases when the result's success + * value can be directly returned (instead of assigning to a variable as in the + * QM_TRY_UNWRAP/QM_TRY_INSPECT case). + * + * QM_FAIL is a supplementary macro for cases when an error needs to be + * returned without evaluating an expression. It's possible to write + * QM_TRY(OkIf(false), NS_ERROR_FAILURE), but QM_FAIL(NS_ERROR_FAILURE) looks + * more straightforward. + * + * It's highly recommended to use + * QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT/QM_TRY_RETURN/QM_FAIL in new code for + * quota manager and quota clients. Existing code should be incrementally + * converted as needed. + * + * QM_TRY_VOID/QM_TRY_UNWRAP_VOID/QM_TRY_INSPECT_VOID/QM_FAIL_VOID is not + * defined on purpose since it's possible to use + * QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT/QM_FAIL even in void functions. + * However, QM_TRY(Task(), ) would look odd so it's recommended to use a dummy + * define QM_VOID that evaluates to nothing instead: QM_TRY(Task(), QM_VOID) + * + * Custom return values can be static or dynamically generated using functions + * with one of these signatures: + * auto(const char* aFunc, const char* aExpr); + * auto(const char* aFunc, const T& aRv); + * auto(const T& aRc); + */ + +#define QM_VOID + +#define QM_PROPAGATE Err(tryTempError) + +#define QM_IPC_FAIL(actor) \ + [&_actor = *actor](const char* aFunc, const char* aExpr) { \ + return Err( \ + mozilla::ipc::IPCResult::Fail(WrapNotNull(&_actor), aFunc, aExpr)); \ + } + +#ifdef DEBUG +# define QM_ASSERT_UNREACHABLE \ + [](const char*, const char*) -> ::mozilla::GenericErrorResult<nsresult> { \ + MOZ_CRASH("Should never be reached."); \ + } + +# define QM_ASSERT_UNREACHABLE_VOID \ + [](const char*, const char*) { MOZ_CRASH("Should never be reached."); } +#endif + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED +# define QM_DIAGNOSTIC_ASSERT_UNREACHABLE \ + [](const char*, const char*) -> ::mozilla::GenericErrorResult<nsresult> { \ + MOZ_CRASH("Should never be reached."); \ + } + +# define QM_DIAGNOSTIC_ASSERT_UNREACHABLE_VOID \ + [](const char*, const char*) { MOZ_CRASH("Should never be reached."); } +#endif + +// QM_MISSING_ARGS and QM_HANDLE_ERROR macros are implementation details of +// QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT/QM_FAIL and shouldn't be used directly. + +#define QM_MISSING_ARGS(...) \ + do { \ + static_assert(false, "Did you forget arguments?"); \ + } while (0) + +#ifdef DEBUG +# define QM_HANDLE_ERROR(expr, error, severity) \ + HandleError(#expr, error, __FILE__, __LINE__, severity) +#else +# define QM_HANDLE_ERROR(expr, error, severity) \ + HandleError("Unavailable", error, __FILE__, __LINE__, severity) +#endif + +#ifdef DEBUG +# define QM_HANDLE_ERROR_RETURN_NOTHING(expr, error, severity) \ + HandleErrorReturnNothing(#expr, error, __FILE__, __LINE__, severity) +#else +# define QM_HANDLE_ERROR_RETURN_NOTHING(expr, error, severity) \ + HandleErrorReturnNothing("Unavailable", error, __FILE__, __LINE__, severity) +#endif + +#ifdef DEBUG +# define QM_HANDLE_ERROR_WITH_CLEANUP_RETURN_NOTHING(expr, error, severity, \ + cleanup) \ + HandleErrorWithCleanupReturnNothing(#expr, error, __FILE__, __LINE__, \ + severity, cleanup) +#else +# define QM_HANDLE_ERROR_WITH_CLEANUP_RETURN_NOTHING(expr, error, severity, \ + cleanup) \ + HandleErrorWithCleanupReturnNothing("Unavailable", error, __FILE__, \ + __LINE__, severity, cleanup) +#endif + +// Handles the case when QM_VOID is passed as a custom return value. +#define QM_HANDLE_CUSTOM_RET_VAL_HELPER0(func, expr, error) + +#define QM_HANDLE_CUSTOM_RET_VAL_HELPER1(func, expr, error, customRetVal) \ + mozilla::dom::quota::HandleCustomRetVal(func, #expr, error, customRetVal) + +#define QM_HANDLE_CUSTOM_RET_VAL_GLUE(a, b) a b + +#define QM_HANDLE_CUSTOM_RET_VAL(...) \ + QM_HANDLE_CUSTOM_RET_VAL_GLUE( \ + MOZ_PASTE_PREFIX_AND_ARG_COUNT(QM_HANDLE_CUSTOM_RET_VAL_HELPER, \ + MOZ_ARGS_AFTER_3(__VA_ARGS__)), \ + (MOZ_ARG_1(__VA_ARGS__), MOZ_ARG_2(__VA_ARGS__), \ + MOZ_ARG_3(__VA_ARGS__) MOZ_ADD_ARGS(MOZ_ARGS_AFTER_3(__VA_ARGS__)))) + +// QM_TRY_PROPAGATE_ERR, QM_TRY_CUSTOM_RET_VAL, +// QM_TRY_CUSTOM_RET_VAL_WITH_CLEANUP and QM_TRY_GLUE macros are implementation +// details of QM_TRY and shouldn't be used directly. + +// Handles the two arguments case when the error is propagated. +#define QM_TRY_PROPAGATE_ERR(tryResult, expr) \ + auto tryResult = (expr); \ + static_assert(std::is_empty_v<typename decltype(tryResult)::ok_type>); \ + if (MOZ_UNLIKELY(tryResult.isErr())) { \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + expr, tryResult.inspectErr(), mozilla::dom::quota::Severity::Error); \ + return tryResult.propagateErr(); \ + } + +// Handles the three arguments case when a custom return value needs to be +// returned +#define QM_TRY_CUSTOM_RET_VAL(tryResult, expr, customRetVal) \ + auto tryResult = (expr); \ + static_assert(std::is_empty_v<typename decltype(tryResult)::ok_type>); \ + if (MOZ_UNLIKELY(tryResult.isErr())) { \ + auto tryTempError MOZ_MAYBE_UNUSED = tryResult.unwrapErr(); \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + expr, tryTempError, mozilla::dom::quota::Severity::Error); \ + constexpr const auto& func MOZ_MAYBE_UNUSED = __func__; \ + return QM_HANDLE_CUSTOM_RET_VAL(func, expr, tryTempError, customRetVal); \ + } + +// Handles the four arguments case when a cleanup function needs to be called +// before a custom return value is returned +#define QM_TRY_CUSTOM_RET_VAL_WITH_CLEANUP(tryResult, expr, customRetVal, \ + cleanup) \ + auto tryResult = (expr); \ + static_assert(std::is_empty_v<typename decltype(tryResult)::ok_type>); \ + if (MOZ_UNLIKELY(tryResult.isErr())) { \ + auto tryTempError = tryResult.unwrapErr(); \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + expr, tryTempError, mozilla::dom::quota::Severity::Error); \ + cleanup(tryTempError); \ + constexpr const auto& func MOZ_MAYBE_UNUSED = __func__; \ + return QM_HANDLE_CUSTOM_RET_VAL(func, expr, tryTempError, customRetVal); \ + } + +// Chooses the final implementation macro for given argument count. +// This could use MOZ_PASTE_PREFIX_AND_ARG_COUNT, but explicit named suffxes +// read slightly better than plain numbers. +// See also +// https://stackoverflow.com/questions/3046889/optional-parameters-with-c-macros +#define QM_TRY_META(...) \ + { \ + MOZ_ARG_6( \ + , ##__VA_ARGS__, QM_TRY_CUSTOM_RET_VAL_WITH_CLEANUP(__VA_ARGS__), \ + QM_TRY_CUSTOM_RET_VAL(__VA_ARGS__), QM_TRY_PROPAGATE_ERR(__VA_ARGS__), \ + QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__)) \ + } + +// Generates unique variable name. This extra internal macro (along with +// __COUNTER__) allows nesting of the final macro. +#define QM_TRY_GLUE(...) QM_TRY_META(MOZ_UNIQUE_VAR(tryResult), ##__VA_ARGS__) + +/** + * QM_TRY(expr[, customRetVal, cleanup]) is the C++ equivalent of Rust's + * `try!(expr);`. First, it evaluates expr, which must produce a Result value + * with empty ok_type. On Success, it does nothing else. On error, it calls + * HandleError and an additional cleanup function (if the third argument was + * passed) and finally returns an error Result from the enclosing function or a + * custom return value (if the second argument was passed). + */ +#define QM_TRY(...) QM_TRY_GLUE(__VA_ARGS__) + +// QM_TRY_ASSIGN_PROPAGATE_ERR, QM_TRY_ASSIGN_CUSTOM_RET_VAL, +// QM_TRY_ASSIGN_CUSTOM_RET_VAL_WITH_CLEANUP and QM_TRY_ASSIGN_GLUE macros are +// implementation details of QM_TRY_UNWRAP/QM_TRY_INSPECT and shouldn't be used +// directly. + +// Handles the four arguments case when the error is propagated. +#define QM_TRY_ASSIGN_PROPAGATE_ERR(tryResult, accessFunction, target, expr) \ + auto tryResult = (expr); \ + if (MOZ_UNLIKELY(tryResult.isErr())) { \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + expr, tryResult.inspectErr(), mozilla::dom::quota::Severity::Error); \ + return tryResult.propagateErr(); \ + } \ + MOZ_REMOVE_PAREN(target) = tryResult.accessFunction(); + +// Handles the five arguments case when a custom return value needs to be +// returned +#define QM_TRY_ASSIGN_CUSTOM_RET_VAL(tryResult, accessFunction, target, expr, \ + customRetVal) \ + auto tryResult = (expr); \ + if (MOZ_UNLIKELY(tryResult.isErr())) { \ + auto tryTempError MOZ_MAYBE_UNUSED = tryResult.unwrapErr(); \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + expr, tryTempError, mozilla::dom::quota::Severity::Error); \ + constexpr const auto& func MOZ_MAYBE_UNUSED = __func__; \ + return QM_HANDLE_CUSTOM_RET_VAL(func, expr, tryTempError, customRetVal); \ + } \ + MOZ_REMOVE_PAREN(target) = tryResult.accessFunction(); + +// Handles the six arguments case when a cleanup function needs to be called +// before a custom return value is returned +#define QM_TRY_ASSIGN_CUSTOM_RET_VAL_WITH_CLEANUP( \ + tryResult, accessFunction, target, expr, customRetVal, cleanup) \ + auto tryResult = (expr); \ + if (MOZ_UNLIKELY(tryResult.isErr())) { \ + auto tryTempError = tryResult.unwrapErr(); \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + expr, tryTempError, mozilla::dom::quota::Severity::Error); \ + cleanup(tryTempError); \ + constexpr const auto& func MOZ_MAYBE_UNUSED = __func__; \ + return QM_HANDLE_CUSTOM_RET_VAL(func, expr, tryTempError, customRetVal); \ + } \ + MOZ_REMOVE_PAREN(target) = tryResult.accessFunction(); + +// Chooses the final implementation macro for given argument count. +// See also the comment for QM_TRY_META. +#define QM_TRY_ASSIGN_META(...) \ + MOZ_ARG_8(, ##__VA_ARGS__, \ + QM_TRY_ASSIGN_CUSTOM_RET_VAL_WITH_CLEANUP(__VA_ARGS__), \ + QM_TRY_ASSIGN_CUSTOM_RET_VAL(__VA_ARGS__), \ + QM_TRY_ASSIGN_PROPAGATE_ERR(__VA_ARGS__), \ + QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__), \ + QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__)) + +// Generates unique variable name. This extra internal macro (along with +// __COUNTER__) allows nesting of the final macro. +#define QM_TRY_ASSIGN_GLUE(accessFunction, ...) \ + QM_TRY_ASSIGN_META(MOZ_UNIQUE_VAR(tryResult), accessFunction, ##__VA_ARGS__) + +/** + * QM_TRY_UNWRAP(target, expr[, customRetVal, cleanup]) is the C++ equivalent of + * Rust's `target = try!(expr);`. First, it evaluates expr, which must produce + * a Result value. On success, the result's success value is unwrapped and + * assigned to target. On error, it calls HandleError and an additional cleanup + * function (if the fourth argument was passed) and finally returns the error + * result or a custom return value (if the third argument was passed). |target| + * must be an lvalue. + */ +#define QM_TRY_UNWRAP(...) QM_TRY_ASSIGN_GLUE(unwrap, __VA_ARGS__) + +/** + * QM_TRY_INSPECT is similar to QM_TRY_UNWRAP, but it does not unwrap a success + * value, but inspects it and binds it to the target. It can therefore only be + * used when the target declares a const&. In general, + * + * QM_TRY_INSPECT(const auto &target, DoSomething()) + * + * should be preferred over + * + * QM_TRY_UNWRAP(const auto target, DoSomething()) + * + * as it avoids unnecessary moves/copies. + */ +#define QM_TRY_INSPECT(...) QM_TRY_ASSIGN_GLUE(inspect, __VA_ARGS__) + +// QM_TRY_RETURN_PROPAGATE_ERR, QM_TRY_RETURN_CUSTOM_RET_VAL, +// QM_TRY_RETURN_CUSTOM_RET_VAL_WITH_CLEANUP and QM_TRY_RETURN_GLUE macros are +// implementation details of QM_TRY_RETURN and shouldn't be used directly. + +// Handles the two arguments case when the error is (also) propagated. +// Note that this deliberately uses a single return statement without going +// through unwrap/unwrapErr/propagateErr, so that this does not prevent NRVO or +// tail call optimizations when possible. +#define QM_TRY_RETURN_PROPAGATE_ERR(tryResult, expr) \ + auto tryResult = (expr); \ + if (MOZ_UNLIKELY(tryResult.isErr())) { \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + expr, tryResult.inspectErr(), mozilla::dom::quota::Severity::Error); \ + } \ + return tryResult; + +// Handles the three arguments case when a custom return value needs to be +// returned +#define QM_TRY_RETURN_CUSTOM_RET_VAL(tryResult, expr, customRetVal) \ + auto tryResult = (expr); \ + if (MOZ_UNLIKELY(tryResult.isErr())) { \ + auto tryTempError MOZ_MAYBE_UNUSED = tryResult.unwrapErr(); \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + expr, tryResult.inspectErr(), mozilla::dom::quota::Severity::Error); \ + constexpr const auto& func MOZ_MAYBE_UNUSED = __func__; \ + return QM_HANDLE_CUSTOM_RET_VAL(func, expr, tryTempError, customRetVal); \ + } \ + return tryResult.unwrap(); + +// Handles the four arguments case when a cleanup function needs to be called +// before a custom return value is returned +#define QM_TRY_RETURN_CUSTOM_RET_VAL_WITH_CLEANUP(tryResult, expr, \ + customRetVal, cleanup) \ + auto tryResult = (expr); \ + if (MOZ_UNLIKELY(tryResult.isErr())) { \ + auto tryTempError = tryResult.unwrapErr(); \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + expr, tryTempError, mozilla::dom::quota::Severity::Error); \ + cleanup(tryTempError); \ + constexpr const auto& func MOZ_MAYBE_UNUSED = __func__; \ + return QM_HANDLE_CUSTOM_RET_VAL(func, expr, tryTempError, customRetVal); \ + } \ + return tryResult.unwrap(); + +// Chooses the final implementation macro for given argument count. +// See also the comment for QM_TRY_META. +#define QM_TRY_RETURN_META(...) \ + { \ + MOZ_ARG_6(, ##__VA_ARGS__, \ + QM_TRY_RETURN_CUSTOM_RET_VAL_WITH_CLEANUP(__VA_ARGS__), \ + QM_TRY_RETURN_CUSTOM_RET_VAL(__VA_ARGS__), \ + QM_TRY_RETURN_PROPAGATE_ERR(__VA_ARGS__), \ + QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__)) \ + } + +// Generates unique variable name. This extra internal macro (along with +// __COUNTER__) allows nesting of the final macro. +#define QM_TRY_RETURN_GLUE(...) \ + QM_TRY_RETURN_META(MOZ_UNIQUE_VAR(tryResult), ##__VA_ARGS__) + +/** + * QM_TRY_RETURN(expr[, customRetVal, cleanup]) evaluates expr, which must + * produce a Result value. On success, the result's success value is returned. + * On error, it calls HandleError and an additional cleanup function (if the + * third argument was passed) and finally returns the error result or a custom + * return value (if the second argument was passed). + */ +#define QM_TRY_RETURN(...) QM_TRY_RETURN_GLUE(__VA_ARGS__) + +// QM_FAIL_RET_VAL and QM_FAIL_RET_VAL_WITH_CLEANUP macros are implementation +// details of QM_FAIL and shouldn't be used directly. + +// Handles the one argument case when just an error is returned +#define QM_FAIL_RET_VAL(retVal) \ + mozilla::dom::quota::QM_HANDLE_ERROR(Failure, 0, \ + mozilla::dom::quota::Severity::Error); \ + return retVal; + +// Handles the two arguments case when a cleanup function needs to be called +// before a return value is returned +#define QM_FAIL_RET_VAL_WITH_CLEANUP(retVal, cleanup) \ + mozilla::dom::quota::QM_HANDLE_ERROR(Failure, 0, \ + mozilla::dom::quota::Severity::Error); \ + cleanup(); \ + return retVal; + +// Chooses the final implementation macro for given argument count. +// See also the comment for QM_TRY_META. +#define QM_FAIL_META(...) \ + MOZ_ARG_4(, ##__VA_ARGS__, QM_FAIL_RET_VAL_WITH_CLEANUP(__VA_ARGS__), \ + QM_FAIL_RET_VAL(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__)) + +// This extra internal macro allows nesting of the final macro. +#define QM_FAIL_GLUE(...) QM_FAIL_META(__VA_ARGS__) + +/** + * QM_FAIL(retVal[, cleanup]) calls HandleError and an additional cleanup + * function (if the second argument was passed) and returns a return value. + */ +#define QM_FAIL(...) QM_FAIL_GLUE(__VA_ARGS__) + +// QM_REPORTONLY_TRY, QM_REPORTONLY_TRY_WITH_CLEANUP, QM_REPORTONLY_TRY_GLUE +// macros are implementation details of QM_WARNONLY_TRY/QM_INFOONLY_TRY and +// shouldn't be used directly. + +// Handles the three arguments case when only a warning/info is reported. +#define QM_REPORTONLY_TRY(tryResult, severity, expr) \ + auto tryResult = (expr); \ + static_assert(std::is_empty_v<typename decltype(tryResult)::ok_type>); \ + if (MOZ_UNLIKELY(tryResult.isErr())) { \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + expr, tryResult.unwrapErr(), mozilla::dom::quota::Severity::severity); \ + } + +// Handles the four arguments case when a cleanup function needs to be called +#define QM_REPORTONLY_TRY_WITH_CLEANUP(tryResult, severity, expr, cleanup) \ + auto tryResult = (expr); \ + static_assert(std::is_empty_v<typename decltype(tryResult)::ok_type>); \ + if (MOZ_UNLIKELY(tryResult.isErr())) { \ + auto tryTempError = tryResult.unwrapErr(); \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + expr, tryTempError, mozilla::dom::quota::Severity::severity); \ + cleanup(tryTempError); \ + } + +// Chooses the final implementation macro for given argument count. +// See also the comment for QM_TRY_META. +#define QM_REPORTONLY_TRY_META(...) \ + { \ + MOZ_ARG_6(, ##__VA_ARGS__, QM_REPORTONLY_TRY_WITH_CLEANUP(__VA_ARGS__), \ + QM_REPORTONLY_TRY(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__), \ + QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__)) \ + } + +// Generates unique variable name. This extra internal macro (along with +// __COUNTER__) allows nesting of the final macro. +#define QM_REPORTONLY_TRY_GLUE(severity, ...) \ + QM_REPORTONLY_TRY_META(MOZ_UNIQUE_VAR(tryResult), severity, ##__VA_ARGS__) + +/** + * QM_WARNONLY_TRY(expr[, cleanup]) evaluates expr, which must produce a + * Result value with empty ok_type. On Success, it does nothing else. On error, + * it calls HandleError and an additional cleanup function (if the second + * argument was passed). This macro never returns and failures are always + * reported using a lower level of severity relative to failures reported by + * QM_TRY. The macro is intended for failures that should not be propagated. + */ +#define QM_WARNONLY_TRY(...) QM_REPORTONLY_TRY_GLUE(Warning, __VA_ARGS__) + +/** + * QM_INFOONLY_TRY is like QM_WARNONLY_TRY. The only difference is that + * failures are reported using a lower level of severity relative to failures + * reported by QM_WARNONLY_TRY. + */ +#define QM_INFOONLY_TRY(...) QM_REPORTONLY_TRY_GLUE(Info, __VA_ARGS__) + +// QM_REPORTONLY_TRY_ASSIGN, QM_REPORTONLY_TRY_ASSIGN_WITH_CLEANUP, +// QM_REPORTONLY_TRY_ASSIGN_GLUE macros are implementation details of +// QM_WARNONLY_TRY_UNWRAP/QM_INFOONLY_TRY_UNWRAP and shouldn't be used +// directly. + +// Handles the four arguments case when only a warning/info is reported. +#define QM_REPORTONLY_TRY_ASSIGN(tryResult, severity, target, expr) \ + auto tryResult = (expr); \ + MOZ_REMOVE_PAREN(target) = \ + MOZ_LIKELY(tryResult.isOk()) \ + ? Some(tryResult.unwrap()) \ + : mozilla::dom::quota::QM_HANDLE_ERROR_RETURN_NOTHING( \ + expr, tryResult.unwrapErr(), \ + mozilla::dom::quota::Severity::severity); + +// Handles the five arguments case when a cleanup function needs to be called +#define QM_REPORTONLY_TRY_ASSIGN_WITH_CLEANUP(tryResult, severity, target, \ + expr, cleanup) \ + auto tryResult = (expr); \ + MOZ_REMOVE_PAREN(target) = \ + MOZ_LIKELY(tryResult.isOk()) \ + ? Some(tryResult.unwrap()) \ + : mozilla::dom::quota::QM_HANDLE_ERROR_WITH_CLEANUP_RETURN_NOTHING( \ + expr, tryResult.unwrapErr(), \ + mozilla::dom::quota::Severity::severity, cleanup); + +// Chooses the final implementation macro for given argument count. +// See also the comment for QM_TRY_META. +#define QM_REPORTONLY_TRY_ASSIGN_META(...) \ + MOZ_ARG_7(, ##__VA_ARGS__, \ + QM_REPORTONLY_TRY_ASSIGN_WITH_CLEANUP(__VA_ARGS__), \ + QM_REPORTONLY_TRY_ASSIGN(__VA_ARGS__), \ + QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__), \ + QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__)) + +// Generates unique variable name. This extra internal macro (along with +// __COUNTER__) allows nesting of the final macro. +#define QM_REPORTONLY_TRY_ASSIGN_GLUE(severity, ...) \ + QM_REPORTONLY_TRY_ASSIGN_META(MOZ_UNIQUE_VAR(tryResult), severity, \ + ##__VA_ARGS__) + +/** + * QM_WARNONLY_TRY_UNWRAP(target, expr[, cleanup]) evaluates expr, which must + * produce a Result value. On success, the result's success value is first + * unwrapped from the Result, then wrapped in a Maybe and finally assigned to + * target. On error, it calls HandleError and an additional cleanup + * function (if the third argument was passed) and finally assigns Nothing to + * target. |target| must be an lvalue. This macro never returns and failures + * are always reported using a lower level of severity relative to failures + * reported by QM_TRY_UNWRAP/QM_TRY_INSPECT. The macro is intended for failures + * that should not be propagated. + */ +#define QM_WARNONLY_TRY_UNWRAP(...) \ + QM_REPORTONLY_TRY_ASSIGN_GLUE(Warning, __VA_ARGS__) + +// QM_WARNONLY_TRY_INSPECT doesn't make sense. + +/** + * QM_INFOONLY_TRY_UNWRAP is like QM_WARNONLY_TRY_UNWRAP. The only difference is + * that failures are reported using a lower level of severity relative to + * failures reported by QM_WARNONLY_TRY_UNWRAP. + */ +#define QM_INFOONLY_TRY_UNWRAP(...) \ + QM_REPORTONLY_TRY_ASSIGN_GLUE(Info, __VA_ARGS__) + +// QM_INFOONLY_TRY_INSPECT doesn't make sense. + +// QM_OR_ELSE_REPORT macro is an implementation detail of +// QM_OR_ELSE_WARN/QM_OR_ELSE_INFO/QM_OR_ELSE_LOG_VERBOSE and shouldn't be used +// directly. + +#define QM_OR_ELSE_REPORT(severity, expr, fallback) \ + (expr).orElse([&](const auto& firstRes) { \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + #expr, firstRes, mozilla::dom::quota::Severity::severity); \ + return fallback(firstRes); \ + }) + +/* + * QM_OR_ELSE_WARN(expr, fallback) evaluates expr, which must produce a Result + * value. On Success, it just moves the success over. On error, it calls + * HandleError (with the Warning severity) and a fallback function (passed as + * the second argument) which produces a new result. Failed expr is always + * reported as a warning (the macro essentially wraps the fallback function + * with a warning). QM_OR_ELSE_WARN is a sub macro and is intended to be used + * along with one of the main macros such as QM_TRY. + */ +#define QM_OR_ELSE_WARN(...) QM_OR_ELSE_REPORT(Warning, __VA_ARGS__) + +/** + * QM_OR_ELSE_INFO is like QM_OR_ELSE_WARN. The only difference is that + * failures are reported using a lower level of severity relative to failures + * reported by QM_OR_ELSE_WARN. + */ +#define QM_OR_ELSE_INFO(...) QM_OR_ELSE_REPORT(Info, __VA_ARGS__) + +/** + * QM_OR_ELSE_LOG_VERBOSE is like QM_OR_ELSE_WARN. The only difference is that + * failures are reported using the lowest severity which is currently ignored + * in LogError, so nothing goes to the console, browser console and telemetry. + * Since nothing goes to the telemetry, the macro can't signal the end of the + * underlying error stack or change the type of the error stack in the + * telemetry. For that reason, the expression shouldn't contain nested QM_TRY + * macro uses. + */ +#define QM_OR_ELSE_LOG_VERBOSE(...) QM_OR_ELSE_REPORT(Log, __VA_ARGS__) + +namespace mozilla::dom::quota { + +// XXX Support orElseIf directly in mozilla::Result +template <typename V, typename E, typename P, typename F> +auto OrElseIf(Result<V, E>&& aResult, P&& aPred, F&& aFunc) -> Result<V, E> { + return MOZ_UNLIKELY(aResult.isErr()) + ? (std::forward<P>(aPred)(aResult.inspectErr())) + ? std::forward<F>(aFunc)(aResult.unwrapErr()) + : aResult.propagateErr() + : aResult.unwrap(); +} + +} // namespace mozilla::dom::quota + +// QM_OR_ELSE_REPORT_IF macro is an implementation detail of +// QM_OR_ELSE_WARN_IF/QM_OR_ELSE_INFO_IF/QM_OR_ELSE_LOG_VERBOSE_IF and +// shouldn't be used directly. + +#define QM_OR_ELSE_REPORT_IF(severity, expr, predicate, fallback) \ + mozilla::dom::quota::OrElseIf( \ + (expr), \ + [&](const auto& firstRes) { \ + bool res = predicate(firstRes); \ + mozilla::dom::quota::QM_HANDLE_ERROR( \ + #expr, firstRes, \ + res ? mozilla::dom::quota::Severity::severity \ + : mozilla::dom::quota::Severity::Error); \ + return res; \ + }, \ + fallback) + +/* + * QM_OR_ELSE_WARN_IF(expr, predicate, fallback) evaluates expr first, which + * must produce a Result value. On Success, it just moves the success over. + * On error, it calls a predicate function (passed as the second argument) and + * then it either calls HandleError (with the Warning severity) and a fallback + * function (passed as the third argument) which produces a new result if the + * predicate returned true. Or it calls HandleError (with the Error severity) + * and propagates the error result if the predicate returned false. So failed + * expr can be reported as a warning or as an error depending on the predicate. + * QM_OR_ELSE_WARN_IF is a sub macro and is intended to be used along with one + * of the main macros such as QM_TRY. + */ +#define QM_OR_ELSE_WARN_IF(...) QM_OR_ELSE_REPORT_IF(Warning, __VA_ARGS__) + +/** + * QM_OR_ELSE_INFO_IF is like QM_OR_ELSE_WARN_IF. The only difference is that + * failures are reported using a lower level of severity relative to failures + * reported by QM_OR_ELSE_WARN_IF. + */ +#define QM_OR_ELSE_INFO_IF(...) QM_OR_ELSE_REPORT_IF(Info, __VA_ARGS__) + +/** + * QM_OR_ELSE_LOG_VERBOSE_IF is like QM_OR_ELSE_WARN_IF. The only difference is + * that failures are reported using the lowest severity which is currently + * ignored in LogError, so nothing goes to the console, browser console and + * telemetry. Since nothing goes to the telemetry, the macro can't signal the + * end of the underlying error stack or change the type of the error stack in + * the telemetry. For that reason, the expression shouldn't contain nested + * QM_TRY macro uses. + */ +#define QM_OR_ELSE_LOG_VERBOSE_IF(...) \ + QM_OR_ELSE_REPORT_IF(Verbose, __VA_ARGS__) + +// Telemetry probes to collect number of failure during the initialization. +#ifdef NIGHTLY_BUILD +# define RECORD_IN_NIGHTLY(_recorder, _status) \ + do { \ + if (NS_SUCCEEDED(_recorder)) { \ + _recorder = _status; \ + } \ + } while (0) + +# define OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS \ + Ok {} + +# define RETURN_STATUS_OR_RESULT(_status, _rv) \ + return Err(NS_FAILED(_status) ? (_status) : (_rv)) +#else +# define RECORD_IN_NIGHTLY(_dummy, _status) \ + {} + +# define OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS QM_PROPAGATE + +# define RETURN_STATUS_OR_RESULT(_status, _rv) return Err(_rv) +#endif + +class mozIStorageConnection; +class mozIStorageStatement; +class nsIFile; + +namespace mozilla { + +class LogModule; + +struct CreateIfNonExistent {}; + +struct NotOk {}; + +// Allow MOZ_TRY/QM_TRY to handle `bool` values by wrapping them with OkIf. +// TODO: Maybe move this to mfbt/ResultExtensions.h +inline Result<Ok, NotOk> OkIf(bool aValue) { + if (aValue) { + return Ok(); + } + return Err(NotOk()); +} + +// TODO: Maybe move this to mfbt/ResultExtensions.h +template <auto SuccessValue> +auto OkToOk(Ok) -> Result<decltype(SuccessValue), nsresult> { + return SuccessValue; +} + +template <nsresult ErrorValue, auto SuccessValue, + typename V = decltype(SuccessValue)> +auto ErrToOkOrErr(nsresult aValue) -> Result<V, nsresult> { + if (aValue == ErrorValue) { + return V{SuccessValue}; + } + return Err(aValue); +} + +template <nsresult ErrorValue, typename V = mozilla::Ok> +auto ErrToDefaultOkOrErr(nsresult aValue) -> Result<V, nsresult> { + if (aValue == ErrorValue) { + return V{}; + } + return Err(aValue); +} + +// Helper template function so that QM_TRY predicates checking for a specific +// error can be concisely written as IsSpecificError<NS_SOME_ERROR> instead of +// as a more verbose lambda. +template <nsresult ErrorValue> +bool IsSpecificError(const nsresult aValue) { + return aValue == ErrorValue; +} + +// Helper template function so that QM_TRY fallback functions that are +// converting errors into specific in-band success values can be concisely +// written as ErrToOk<SuccessValueToReturn> (with the return type inferred). +// For example, many file-related APIs that access information about a file may +// return an nsresult error code if the file does not exist. From an +// application perspective, the file not existing is not actually exceptional +// and can instead be handled by the success case. +template <auto SuccessValue, typename V = decltype(SuccessValue)> +auto ErrToOk(const nsresult aValue) -> Result<V, nsresult> { + return V{SuccessValue}; +} + +// Helper template function so that QM_TRY fallback functions that are +// suppressing errors by converting them into (generic) success can be +// concisely written as ErrToDefaultOk<>. +template <typename V = mozilla::Ok> +auto ErrToDefaultOk(const nsresult aValue) -> Result<V, nsresult> { + return V{}; +} + +template <typename MozPromiseType, typename RejectValueT = nsresult> +auto CreateAndRejectMozPromise(const char* aFunc, const RejectValueT& aRv) + -> decltype(auto) { + if constexpr (std::is_same_v<RejectValueT, nsresult>) { + return MozPromiseType::CreateAndReject(aRv, aFunc); + } else if constexpr (std::is_same_v<RejectValueT, QMResult>) { + return MozPromiseType::CreateAndReject(aRv.NSResult(), aFunc); + } +} + +RefPtr<BoolPromise> CreateAndRejectBoolPromise(const char* aFunc, nsresult aRv); + +RefPtr<Int64Promise> CreateAndRejectInt64Promise(const char* aFunc, + nsresult aRv); + +RefPtr<BoolPromise> CreateAndRejectBoolPromiseFromQMResult(const char* aFunc, + const QMResult& aRv); + +// Like Rust's collect with a step function, not a generic iterator/range. +// +// Cond must be a function type with a return type to Result<V, E>, where +// V is convertible to bool +// - success converts to true indicates that collection shall continue +// - success converts to false indicates that collection is completed +// - error indicates that collection shall stop, propagating the error +// +// Body must a function type accepting a V xvalue with a return type convertible +// to Result<empty, E>. +template <typename Step, typename Body> +auto CollectEach(Step aStep, const Body& aBody) + -> Result<mozilla::Ok, typename std::invoke_result_t<Step>::err_type> { + using StepResultType = typename std::invoke_result_t<Step>::ok_type; + + static_assert( + std::is_empty_v< + typename std::invoke_result_t<Body, StepResultType&&>::ok_type>); + + while (true) { + StepResultType element; + MOZ_TRY_VAR(element, aStep()); + + if (!static_cast<bool>(element)) { + break; + } + + MOZ_TRY(aBody(std::move(element))); + } + + return mozilla::Ok{}; +} + +// This is like std::reduce with a to-be-defined execution policy (we don't want +// to std::terminate on an error, but probably it's fine to just propagate any +// error that occurred), operating not on a pair of iterators but rather a +// generator function. +template <typename InputGenerator, typename T, typename BinaryOp> +auto ReduceEach(InputGenerator aInputGenerator, T aInit, + const BinaryOp& aBinaryOp) + -> Result<T, typename std::invoke_result_t<InputGenerator>::err_type> { + T res = std::move(aInit); + + // XXX This can be done in parallel! + MOZ_TRY(CollectEach( + std::move(aInputGenerator), + [&res, &aBinaryOp](const auto& element) + -> Result<Ok, + typename std::invoke_result_t<InputGenerator>::err_type> { + MOZ_TRY_VAR(res, aBinaryOp(std::move(res), element)); + + return Ok{}; + })); + + return std::move(res); +} + +// This is like std::reduce with a to-be-defined execution policy (we don't want +// to std::terminate on an error, but probably it's fine to just propagate any +// error that occurred). +template <typename Range, typename T, typename BinaryOp> +auto Reduce(Range&& aRange, T aInit, const BinaryOp& aBinaryOp) { + using std::begin; + using std::end; + return ReduceEach( + [it = begin(aRange), end = end(aRange)]() mutable { + auto res = ToMaybeRef(it != end ? &*it++ : nullptr); + return Result<decltype(res), typename std::invoke_result_t< + BinaryOp, T, decltype(res)>::err_type>( + res); + }, + aInit, aBinaryOp); +} + +template <typename Range, typename Body> +auto CollectEachInRange(Range&& aRange, const Body& aBody) + -> Result<mozilla::Ok, nsresult> { + for (auto&& element : aRange) { + MOZ_TRY(aBody(element)); + } + + return mozilla::Ok{}; +} + +// Like Rust's collect with a while loop, not a generic iterator/range. +// +// Cond must be a function type accepting no parameters with a return type +// convertible to Result<bool, E>, where +// - success true indicates that collection shall continue +// - success false indicates that collection is completed +// - error indicates that collection shall stop, propagating the error +// +// Body must a function type accepting no parameters with a return type +// convertible to Result<empty, E>. +template <typename Cond, typename Body> +auto CollectWhile(const Cond& aCond, const Body& aBody) + -> Result<mozilla::Ok, typename std::invoke_result_t<Cond>::err_type> { + return CollectEach(aCond, [&aBody](bool) { return aBody(); }); +} + +template <> +class [[nodiscard]] GenericErrorResult<mozilla::ipc::IPCResult> { + mozilla::ipc::IPCResult mErrorValue; + + template <typename V, typename E2> + friend class Result; + + public: + explicit GenericErrorResult(mozilla::ipc::IPCResult aErrorValue) + : mErrorValue(aErrorValue) { + MOZ_ASSERT(!aErrorValue); + } + + GenericErrorResult(mozilla::ipc::IPCResult aErrorValue, + const ErrorPropagationTag&) + : GenericErrorResult(aErrorValue) {} + + operator mozilla::ipc::IPCResult() const { return mErrorValue; } +}; + +namespace dom::quota { + +extern const char kQuotaGenericDelimiter; + +// Telemetry keys to indicate types of errors. +#ifdef NIGHTLY_BUILD +extern const nsLiteralCString kQuotaInternalError; +extern const nsLiteralCString kQuotaExternalError; +#else +// No need for these when we're not collecting telemetry. +# define kQuotaInternalError +# define kQuotaExternalError +#endif + +class BackgroundThreadObject { + protected: + nsCOMPtr<nsISerialEventTarget> mOwningThread; + + public: + void AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + nsISerialEventTarget* OwningThread() const; + + protected: + BackgroundThreadObject(); + + explicit BackgroundThreadObject(nsISerialEventTarget* aOwningThread); +}; + +MOZ_COLD void ReportInternalError(const char* aFile, uint32_t aLine, + const char* aStr); + +LogModule* GetQuotaManagerLogger(); + +void AnonymizeCString(nsACString& aCString); + +inline auto AnonymizedCString(const nsACString& aCString) { + nsAutoCString result{aCString}; + AnonymizeCString(result); + return result; +} + +void AnonymizeOriginString(nsACString& aOriginString); + +inline auto AnonymizedOriginString(const nsACString& aOriginString) { + nsAutoCString result{aOriginString}; + AnonymizeOriginString(result); + return result; +} + +#ifdef XP_WIN +void CacheUseDOSDevicePathSyntaxPrefValue(); +#endif + +Result<nsCOMPtr<nsIFile>, nsresult> QM_NewLocalFile(const nsAString& aPath); + +nsDependentCSubstring GetLeafName(const nsACString& aPath); + +Result<nsCOMPtr<nsIFile>, nsresult> CloneFileAndAppend( + nsIFile& aDirectory, const nsAString& aPathElement); + +enum class nsIFileKind { + ExistsAsDirectory, + ExistsAsFile, + DoesNotExist, +}; + +// XXX We can use this outside of QM and its clients as well, probably. Maybe it +// could be moved to xpcom/io? +Result<nsIFileKind, nsresult> GetDirEntryKind(nsIFile& aFile); + +Result<nsCOMPtr<mozIStorageStatement>, nsresult> CreateStatement( + mozIStorageConnection& aConnection, const nsACString& aStatementString); + +enum class SingleStepResult { AssertHasResult, ReturnNullIfNoResult }; + +template <SingleStepResult ResultHandling> +using SingleStepSuccessType = + std::conditional_t<ResultHandling == SingleStepResult::AssertHasResult, + NotNull<nsCOMPtr<mozIStorageStatement>>, + nsCOMPtr<mozIStorageStatement>>; + +template <SingleStepResult ResultHandling> +Result<SingleStepSuccessType<ResultHandling>, nsresult> ExecuteSingleStep( + nsCOMPtr<mozIStorageStatement>&& aStatement); + +// Creates a statement with the specified aStatementString, executes a single +// step, and returns the statement. +// Depending on the value ResultHandling, +// - it is asserted that there is a result (default resp. +// SingleStepResult::AssertHasResult), and the success type is +// MovingNotNull<nsCOMPtr<mozIStorageStatement>> +// - it is asserted that there is no result, and the success type is Ok +// - in case there is no result, nullptr is returned, and the success type is +// nsCOMPtr<mozIStorageStatement> +// Any other errors are always propagated. +template <SingleStepResult ResultHandling = SingleStepResult::AssertHasResult> +Result<SingleStepSuccessType<ResultHandling>, nsresult> +CreateAndExecuteSingleStepStatement(mozIStorageConnection& aConnection, + const nsACString& aStatementString); + +namespace detail { + +// Determine the absolute path of the root of our built source tree so we can +// derive source-relative paths for non-exported header files in +// MakeSourceFileRelativePath. Exported header files end up in the objdir and +// we have GetObjdirDistIncludeTreeBase for that. +nsDependentCSubstring GetSourceTreeBase(); + +// Determine the absolute path of the root of our built OBJDIR/dist/include +// directory. The aQuotaCommonHPath argument cleverly defaults to __FILE__ +// initialized in our exported header; no argument should ever be provided to +// this method. GetSourceTreeBase handles identifying the root of the source +// tree. +nsDependentCSubstring GetObjdirDistIncludeTreeBase( + const nsLiteralCString& aQuotaCommonHPath = nsLiteralCString(__FILE__)); + +nsDependentCSubstring MakeSourceFileRelativePath( + const nsACString& aSourceFilePath); + +} // namespace detail + +enum class Severity { + Error, + Warning, + Info, + Verbose, +}; + +#ifdef QM_LOG_ERROR_ENABLED +# ifdef QM_ERROR_STACKS_ENABLED +using ResultType = Variant<QMResult, nsresult, Nothing>; + +void LogError(const nsACString& aExpr, const ResultType& aResult, + const nsACString& aSourceFilePath, int32_t aSourceFileLine, + Severity aSeverity) +# else +void LogError(const nsACString& aExpr, Maybe<nsresult> aMaybeRv, + const nsACString& aSourceFilePath, int32_t aSourceFileLine, + Severity aSeverity) +# endif + ; +#endif + +#ifdef DEBUG +Result<bool, nsresult> WarnIfFileIsUnknown(nsIFile& aFile, + const char* aSourceFilePath, + int32_t aSourceFileLine); +#endif + +// As HandleError is a function that will only be called in error cases, it is +// marked with MOZ_COLD to avoid bloating the code of calling functions, if it's +// not empty. +// +// For the same reason, the string-ish parameters are of type const char* rather +// than any ns*String type, to minimize the code at each call site. This +// deliberately de-optimizes runtime performance, which is uncritical during +// error handling. +// +// This functions are not intended to be called +// directly, they should only be called from the QM_* macros. +#ifdef QM_LOG_ERROR_ENABLED +template <typename T> +MOZ_COLD MOZ_NEVER_INLINE void HandleError(const char* aExpr, const T& aRv, + const char* aSourceFilePath, + int32_t aSourceFileLine, + const Severity aSeverity) { +# ifdef QM_ERROR_STACKS_ENABLED + if constexpr (std::is_same_v<T, QMResult> || std::is_same_v<T, nsresult>) { + mozilla::dom::quota::LogError(nsDependentCString(aExpr), ResultType(aRv), + nsDependentCString(aSourceFilePath), + aSourceFileLine, aSeverity); + } else { + mozilla::dom::quota::LogError( + nsDependentCString(aExpr), ResultType(Nothing{}), + nsDependentCString(aSourceFilePath), aSourceFileLine, aSeverity); + } +# else + if constexpr (std::is_same_v<T, nsresult>) { + mozilla::dom::quota::LogError(nsDependentCString(aExpr), Some(aRv), + nsDependentCString(aSourceFilePath), + aSourceFileLine, aSeverity); + } else { + mozilla::dom::quota::LogError(nsDependentCString(aExpr), Nothing{}, + nsDependentCString(aSourceFilePath), + aSourceFileLine, aSeverity); + } +# endif +} +#else +template <typename T> +MOZ_ALWAYS_INLINE constexpr void HandleError(const char* aExpr, const T& aRv, + const char* aSourceFilePath, + int32_t aSourceFileLine, + const Severity aSeverity) {} +#endif + +template <typename T> +Nothing HandleErrorReturnNothing(const char* aExpr, const T& aRv, + const char* aSourceFilePath, + int32_t aSourceFileLine, + const Severity aSeverity) { + HandleError(aExpr, aRv, aSourceFilePath, aSourceFileLine, aSeverity); + return Nothing(); +} + +template <typename T, typename CleanupFunc> +Nothing HandleErrorWithCleanupReturnNothing(const char* aExpr, const T& aRv, + const char* aSourceFilePath, + int32_t aSourceFileLine, + const Severity aSeverity, + CleanupFunc&& aCleanupFunc) { + HandleError(aExpr, aRv, aSourceFilePath, aSourceFileLine, aSeverity); + std::forward<CleanupFunc>(aCleanupFunc)(aRv); + return Nothing(); +} + +template <typename T, typename CustomRetVal> +auto HandleCustomRetVal(const char* aFunc, const char* aExpr, const T& aRv, + CustomRetVal&& aCustomRetVal) { + if constexpr (std::is_invocable<CustomRetVal, const char*, + const char*>::value) { + return aCustomRetVal(aFunc, aExpr); + } else if constexpr (std::is_invocable<CustomRetVal, const char*, + const T&>::value) { + return aCustomRetVal(aFunc, aRv); + } else if constexpr (std::is_invocable<CustomRetVal, const T&>::value) { + return aCustomRetVal(aRv); + } else { + return std::forward<CustomRetVal>(aCustomRetVal); + } +} + +template <SingleStepResult ResultHandling = SingleStepResult::AssertHasResult, + typename BindFunctor> +Result<SingleStepSuccessType<ResultHandling>, nsresult> +CreateAndExecuteSingleStepStatement(mozIStorageConnection& aConnection, + const nsACString& aStatementString, + BindFunctor aBindFunctor) { + QM_TRY_UNWRAP(auto stmt, CreateStatement(aConnection, aStatementString)); + + QM_TRY(aBindFunctor(*stmt)); + + return ExecuteSingleStep<ResultHandling>(std::move(stmt)); +} + +template <typename StepFunc> +Result<Ok, nsresult> CollectWhileHasResult(mozIStorageStatement& aStmt, + StepFunc&& aStepFunc) { + return CollectWhile( + [&aStmt] { + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(aStmt, ExecuteStep)); + }, + [&aStmt, &aStepFunc] { return aStepFunc(aStmt); }); +} + +template <typename StepFunc, + typename ArrayType = nsTArray<typename std::invoke_result_t< + StepFunc, mozIStorageStatement&>::ok_type>> +auto CollectElementsWhileHasResult(mozIStorageStatement& aStmt, + StepFunc&& aStepFunc) + -> Result<ArrayType, nsresult> { + ArrayType res; + + QM_TRY(CollectWhileHasResult( + aStmt, [&aStepFunc, &res](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY_UNWRAP(auto element, aStepFunc(stmt)); + res.AppendElement(std::move(element)); + return Ok{}; + })); + + return std::move(res); +} + +template <typename ArrayType, typename StepFunc> +auto CollectElementsWhileHasResultTyped(mozIStorageStatement& aStmt, + StepFunc&& aStepFunc) { + return CollectElementsWhileHasResult<StepFunc, ArrayType>( + aStmt, std::forward<StepFunc>(aStepFunc)); +} + +namespace detail { +template <typename Cancel, typename Body> +Result<mozilla::Ok, nsresult> CollectEachFile(nsIFile& aDirectory, + const Cancel& aCancel, + const Body& aBody) { + QM_TRY_INSPECT(const auto& entries, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIDirectoryEnumerator>, + aDirectory, GetDirectoryEntries)); + + return CollectEach( + [&entries, &aCancel]() -> Result<nsCOMPtr<nsIFile>, nsresult> { + if (aCancel()) { + return nsCOMPtr<nsIFile>{}; + } + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr<nsIFile>, + entries, GetNextFile)); + }, + aBody); +} +} // namespace detail + +template <typename Body> +Result<mozilla::Ok, nsresult> CollectEachFile(nsIFile& aDirectory, + const Body& aBody) { + return detail::CollectEachFile( + aDirectory, [] { return false; }, aBody); +} + +template <typename Body> +Result<mozilla::Ok, nsresult> CollectEachFileAtomicCancelable( + nsIFile& aDirectory, const Atomic<bool>& aCanceled, const Body& aBody) { + return detail::CollectEachFile( + aDirectory, [&aCanceled] { return static_cast<bool>(aCanceled); }, aBody); +} + +template <typename T, typename Body> +auto ReduceEachFileAtomicCancelable(nsIFile& aDirectory, + const Atomic<bool>& aCanceled, T aInit, + const Body& aBody) -> Result<T, nsresult> { + QM_TRY_INSPECT(const auto& entries, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIDirectoryEnumerator>, + aDirectory, GetDirectoryEntries)); + + return ReduceEach( + [&entries, &aCanceled]() -> Result<nsCOMPtr<nsIFile>, nsresult> { + if (aCanceled) { + return nsCOMPtr<nsIFile>{}; + } + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr<nsIFile>, + entries, GetNextFile)); + }, + std::move(aInit), aBody); +} + +constexpr bool IsDatabaseCorruptionError(const nsresult aRv) { + return aRv == NS_ERROR_FILE_CORRUPTED || aRv == NS_ERROR_STORAGE_IOERR; +} + +template <typename Func> +auto CallWithDelayedRetriesIfAccessDenied(Func&& aFunc, uint32_t aMaxRetries, + uint32_t aDelayMs) + -> Result<typename std::invoke_result_t<Func>::ok_type, nsresult> { + uint32_t retries = 0; + + while (true) { + auto result = std::forward<Func>(aFunc)(); + + if (result.isOk()) { + return result; + } + + if (result.inspectErr() != NS_ERROR_FILE_IS_LOCKED && + result.inspectErr() != NS_ERROR_FILE_ACCESS_DENIED) { + return result; + } + + if (retries++ >= aMaxRetries) { + return result; + } + + PR_Sleep(PR_MillisecondsToInterval(aDelayMs)); + } +} + +namespace detail { + +template <bool flag = false> +void UnsupportedReturnType() { + static_assert(flag, "Unsupported return type!"); +} + +} // namespace detail + +template <typename Initialization, typename StringGenerator, typename Func> +auto ExecuteInitialization( + FirstInitializationAttempts<Initialization, StringGenerator>& + aFirstInitializationAttempts, + const Initialization aInitialization, Func&& aFunc) + -> std::invoke_result_t<Func, const FirstInitializationAttempt< + Initialization, StringGenerator>&> { + return aFirstInitializationAttempts.WithFirstInitializationAttempt( + aInitialization, [&aFunc](auto&& firstInitializationAttempt) { + auto res = std::forward<Func>(aFunc)(firstInitializationAttempt); + + const auto rv = [&res]() -> nsresult { + using RetType = + std::invoke_result_t<Func, const FirstInitializationAttempt< + Initialization, StringGenerator>&>; + + if constexpr (std::is_same_v<RetType, nsresult>) { + return res; + } else if constexpr (mozilla::detail::IsResult<RetType>::value && + std::is_same_v<typename RetType::err_type, + nsresult>) { + return res.isOk() ? NS_OK : res.inspectErr(); + } else { + detail::UnsupportedReturnType(); + } + }(); + + // NS_ERROR_ABORT signals a non-fatal, recoverable problem during + // initialization. We do not want these kind of failures to count + // against our overall first initialization attempt telemetry. Thus we + // just ignore this kind of failure and keep + // aFirstInitializationAttempts unflagged to stay ready to record a real + // success or failure on the next attempt. + if (rv == NS_ERROR_ABORT) { + return res; + } + + if (!firstInitializationAttempt.Recorded()) { + firstInitializationAttempt.Record(rv); + } + + return res; + }); +} + +template <typename Initialization, typename StringGenerator, typename Func> +auto ExecuteInitialization( + FirstInitializationAttempts<Initialization, StringGenerator>& + aFirstInitializationAttempts, + const Initialization aInitialization, const nsACString& aContext, + Func&& aFunc) + -> std::invoke_result_t<Func, const FirstInitializationAttempt< + Initialization, StringGenerator>&> { + return ExecuteInitialization( + aFirstInitializationAttempts, aInitialization, + [&](const auto& firstInitializationAttempt) -> decltype(auto) { +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + const auto maybeScopedLogExtraInfo = + firstInitializationAttempt.Recorded() + ? Nothing{} + : Some(ScopedLogExtraInfo{ScopedLogExtraInfo::kTagContext, + aContext}); +#endif + + return std::forward<Func>(aFunc)(firstInitializationAttempt); + }); +} + +} // namespace dom::quota +} // namespace mozilla + +#endif // mozilla_dom_quota_quotacommon_h__ diff --git a/dom/quota/QuotaManager.h b/dom/quota/QuotaManager.h new file mode 100644 index 0000000000..e18454909f --- /dev/null +++ b/dom/quota/QuotaManager.h @@ -0,0 +1,676 @@ +/* -*- 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_quota_quotamanager_h__ +#define mozilla_dom_quota_quotamanager_h__ + +#include <cstdint> +#include <utility> +#include "Client.h" +#include "ErrorList.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/InitializedOnce.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Result.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/ipc/IdType.h" +#include "mozilla/dom/quota/Assertions.h" +#include "mozilla/dom/quota/CommonMetadata.h" +#include "mozilla/dom/quota/ForwardDecls.h" +#include "mozilla/dom/quota/InitializationTypes.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "nsCOMPtr.h" +#include "nsClassHashtable.h" +#include "nsTHashMap.h" +#include "nsDebug.h" +#include "nsHashKeys.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsTStringRepr.h" +#include "nscore.h" +#include "prenv.h" + +#define QUOTA_MANAGER_CONTRACTID "@mozilla.org/dom/quota/manager;1" + +class mozIStorageConnection; +class nsIEventTarget; +class nsIFile; +class nsIPrincipal; +class nsIRunnable; +class nsIThread; +class nsITimer; +class nsPIDOMWindowOuter; + +namespace mozilla { + +class OriginAttributes; + +namespace ipc { + +class PrincipalInfo; + +} // namespace ipc + +} // namespace mozilla + +namespace mozilla::dom::quota { + +class CanonicalQuotaObject; +class ClientUsageArray; +class ClientDirectoryLock; +class DirectoryLockImpl; +class GroupInfo; +class GroupInfoPair; +class OriginDirectoryLock; +class OriginInfo; +class OriginScope; +class QuotaObject; +class UniversalDirectoryLock; + +class QuotaManager final : public BackgroundThreadObject { + friend class CanonicalQuotaObject; + friend class DirectoryLockImpl; + friend class GroupInfo; + friend class OriginInfo; + + using PrincipalInfo = mozilla::ipc::PrincipalInfo; + using DirectoryLockTable = + nsClassHashtable<nsCStringHashKey, nsTArray<NotNull<DirectoryLockImpl*>>>; + + class Observer; + + public: + QuotaManager(const nsAString& aBasePath, const nsAString& aStorageName); + + NS_INLINE_DECL_REFCOUNTING(QuotaManager) + + static nsresult Initialize(); + + static bool IsRunningXPCShellTests() { + static bool kRunningXPCShellTests = + !!PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR"); + return kRunningXPCShellTests; + } + + static bool IsRunningGTests() { + static bool kRunningGTests = !!PR_GetEnv("MOZ_RUN_GTEST"); + return kRunningGTests; + } + + static const char kReplaceChars[]; + static const char16_t kReplaceChars16[]; + + static Result<MovingNotNull<RefPtr<QuotaManager>>, nsresult> GetOrCreate(); + + static Result<Ok, nsresult> EnsureCreated(); + + // Returns a non-owning reference. + static QuotaManager* Get(); + + // Use only in gtests! + static nsIObserver* GetObserver(); + + // Returns true if we've begun the shutdown process. + static bool IsShuttingDown(); + + static void ShutdownInstance(); + + // Use only in gtests! + static void Reset(); + + static bool IsOSMetadata(const nsAString& aFileName); + + static bool IsDotFile(const nsAString& aFileName); + + bool IsOriginInitialized(const nsACString& aOrigin) const { + AssertIsOnIOThread(); + + return mInitializedOrigins.Contains(aOrigin); + } + + bool IsTemporaryStorageInitialized() const { + AssertIsOnIOThread(); + + return mTemporaryStorageInitialized; + } + + /** + * For initialization of an origin where the directory already exists. This is + * used by EnsureTemporaryStorageIsInitialized/InitializeRepository once it + * has tallied origin usage by calling each of the QuotaClient InitOrigin + * methods. + */ + void InitQuotaForOrigin(const FullOriginMetadata& aFullOriginMetadata, + const ClientUsageArray& aClientUsages, + uint64_t aUsageBytes); + + /** + * For use in special-cases like LSNG where we need to be able to know that + * there is no data stored for an origin. LSNG knows that there is 0 usage for + * its storage of an origin and wants to make sure there is a QuotaObject + * tracking this. This method will create a non-persisted, 0-usage, + * mDirectoryExists=false OriginInfo if there isn't already an OriginInfo. If + * an OriginInfo already exists, it will be left as-is, because that implies a + * different client has usages for the origin (and there's no need to add + * LSNG's 0 usage to the QuotaObject). + */ + void EnsureQuotaForOrigin(const OriginMetadata& aOriginMetadata); + + /** + * For use when creating an origin directory. It's possible that origin usage + * is already being tracked due to a call to EnsureQuotaForOrigin, and in that + * case we need to update the existing OriginInfo rather than create a new + * one. + * + * @return last access time of the origin. + */ + int64_t NoteOriginDirectoryCreated(const OriginMetadata& aOriginMetadata, + bool aPersisted); + + // XXX clients can use QuotaObject instead of calling this method directly. + void DecreaseUsageForClient(const ClientMetadata& aClientMetadata, + int64_t aSize); + + void ResetUsageForClient(const ClientMetadata& aClientMetadata); + + UsageInfo GetUsageForClient(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + Client::Type aClientType); + + void UpdateOriginAccessTime(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata); + + void RemoveQuota(); + + void RemoveQuotaForOrigin(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata) { + MutexAutoLock lock(mQuotaMutex); + LockedRemoveQuotaForOrigin(aOriginMetadata); + } + + nsresult LoadQuota(); + + void UnloadQuota(); + + already_AddRefed<QuotaObject> GetQuotaObject( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + Client::Type aClientType, nsIFile* aFile, int64_t aFileSize = -1, + int64_t* aFileSizeOut = nullptr); + + already_AddRefed<QuotaObject> GetQuotaObject( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + Client::Type aClientType, const nsAString& aPath, int64_t aFileSize = -1, + int64_t* aFileSizeOut = nullptr); + + already_AddRefed<QuotaObject> GetQuotaObject(const int64_t aDirectoryLockId, + const nsAString& aPath); + + Nullable<bool> OriginPersisted(const OriginMetadata& aOriginMetadata); + + void PersistOrigin(const OriginMetadata& aOriginMetadata); + + using DirectoryLockIdTableArray = + AutoTArray<Client::DirectoryLockIdTable, Client::TYPE_MAX>; + void AbortOperationsForLocks(const DirectoryLockIdTableArray& aLockIds); + + // Called when a process is being shot down. Aborts any running operations + // for the given process. + void AbortOperationsForProcess(ContentParentId aContentParentId); + + Result<nsCOMPtr<nsIFile>, nsresult> GetDirectoryForOrigin( + PersistenceType aPersistenceType, const nsACString& aASCIIOrigin) const; + + nsresult RestoreDirectoryMetadata2(nsIFile* aDirectory); + + // XXX Remove aPersistenceType argument once the persistence type is stored + // in the metadata file. + Result<FullOriginMetadata, nsresult> LoadFullOriginMetadata( + nsIFile* aDirectory, PersistenceType aPersistenceType); + + Result<FullOriginMetadata, nsresult> LoadFullOriginMetadataWithRestore( + nsIFile* aDirectory); + + // This is the main entry point into the QuotaManager API. + // Any storage API implementation (quota client) that participates in + // centralized quota and storage handling should call this method to get + // a directory lock which will protect client's files from being deleted + // while they are still in use. + // After a lock is acquired, client is notified via the open listener's + // method DirectoryLockAcquired. If the lock couldn't be acquired, client + // gets DirectoryLockFailed notification. + // A lock is a reference counted object and at the time DirectoryLockAcquired + // is called, quota manager holds just one strong reference to it which is + // then immediatelly cleared by quota manager. So it's up to client to add + // a new reference in order to keep the lock alive. + // Unlocking is simply done by dropping all references to the lock object. + // In other words, protection which the lock represents dies with the lock + // object itself. + RefPtr<ClientDirectoryLock> CreateDirectoryLock( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + Client::Type aClientType, bool aExclusive); + + // XXX RemoveMe once bug 1170279 gets fixed. + RefPtr<UniversalDirectoryLock> CreateDirectoryLockInternal( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, + const Nullable<Client::Type>& aClientType, bool aExclusive); + + // Collect inactive and the least recently used origins. + uint64_t CollectOriginsForEviction( + uint64_t aMinSizeToBeFreed, + nsTArray<RefPtr<OriginDirectoryLock>>& aLocks); + + /** + * Helper method to invoke the provided predicate on all "pending" OriginInfo + * instances. These are origins for which the origin directory has not yet + * been created but for which quota is already being tracked. This happens, + * for example, for the LocalStorage client where an origin that previously + * was not using LocalStorage can start issuing writes which it buffers until + * eventually flushing them. We defer creating the origin directory for as + * long as possible in that case, so the directory won't exist. Logic that + * would otherwise only consult the filesystem also needs to use this method. + */ + template <typename P> + void CollectPendingOriginsForListing(P aPredicate); + + bool IsStorageInitialized() const { + AssertIsOnIOThread(); + return static_cast<bool>(mStorageConnection); + } + + void AssertStorageIsInitialized() const +#ifdef DEBUG + ; +#else + { + } +#endif + + nsresult EnsureStorageIsInitialized(); + + // Returns a pair of an nsIFile object referring to the directory, and a bool + // indicating whether the directory was newly created. + Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> + EnsurePersistentOriginIsInitialized(const OriginMetadata& aOriginMetadata); + + // Returns a pair of an nsIFile object referring to the directory, and a bool + // indicating whether the directory was newly created. + Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> + EnsureTemporaryOriginIsInitialized(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata); + + nsresult EnsureTemporaryStorageIsInitialized(); + + RefPtr<BoolPromise> ShutdownStorage(); + + void ShutdownStorageInternal(); + + // Returns a bool indicating whether the directory was newly created. + Result<bool, nsresult> EnsureOriginDirectory(nsIFile& aDirectory); + + nsresult AboutToClearOrigins( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, + const Nullable<Client::Type>& aClientType); + + void OriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin, + const Nullable<Client::Type>& aClientType); + + void StartIdleMaintenance() { + AssertIsOnOwningThread(); + + for (const auto& client : *mClients) { + client->StartIdleMaintenance(); + } + } + + void StopIdleMaintenance() { + AssertIsOnOwningThread(); + + for (const auto& client : *mClients) { + client->StopIdleMaintenance(); + } + } + + void AssertCurrentThreadOwnsQuotaMutex() { + mQuotaMutex.AssertCurrentThreadOwns(); + } + + nsIThread* IOThread() { return mIOThread->get(); } + + Client* GetClient(Client::Type aClientType); + + const AutoTArray<Client::Type, Client::TYPE_MAX>& AllClientTypes(); + + const nsString& GetBasePath() const { return mBasePath; } + + const nsString& GetStorageName() const { return mStorageName; } + + const nsString& GetStoragePath() const { return *mStoragePath; } + + const nsString& GetStoragePath(PersistenceType aPersistenceType) const { + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + return *mPermanentStoragePath; + } + + if (aPersistenceType == PERSISTENCE_TYPE_TEMPORARY) { + return *mTemporaryStoragePath; + } + + MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT); + + return *mDefaultStoragePath; + } + + uint64_t GetGroupLimit() const; + + std::pair<uint64_t, uint64_t> GetUsageAndLimitForEstimate( + const OriginMetadata& aOriginMetadata); + + uint64_t GetOriginUsage(const PrincipalMetadata& aPrincipalMetadata); + + Maybe<FullOriginMetadata> GetFullOriginMetadata( + const OriginMetadata& aOriginMetadata); + + void NotifyStoragePressure(uint64_t aUsage); + + // Record a quota client shutdown step, if shutting down. + // Assumes that the QuotaManager singleton is alive. + static void MaybeRecordQuotaClientShutdownStep( + const Client::Type aClientType, const nsACString& aStepDescription); + + // Record a quota client shutdown step, if shutting down. + // Checks if the QuotaManager singleton is alive. + static void SafeMaybeRecordQuotaClientShutdownStep( + Client::Type aClientType, const nsACString& aStepDescription); + + // Record a quota manager shutdown step, use only if shutdown is active. + void RecordQuotaManagerShutdownStep(const nsACString& aStepDescription); + + // Record a quota manager shutdown step, if shutting down. + void MaybeRecordQuotaManagerShutdownStep(const nsACString& aStepDescription); + + template <typename F> + void MaybeRecordQuotaManagerShutdownStepWith(F&& aFunc); + + static void GetStorageId(PersistenceType aPersistenceType, + const nsACString& aOrigin, Client::Type aClientType, + nsACString& aDatabaseId); + + static bool IsPrincipalInfoValid(const PrincipalInfo& aPrincipalInfo); + + static PrincipalMetadata GetInfoFromValidatedPrincipalInfo( + const PrincipalInfo& aPrincipalInfo); + + static nsAutoCString GetOriginFromValidatedPrincipalInfo( + const PrincipalInfo& aPrincipalInfo); + + static Result<PrincipalMetadata, nsresult> GetInfoFromPrincipal( + nsIPrincipal* aPrincipal); + + static Result<nsAutoCString, nsresult> GetOriginFromPrincipal( + nsIPrincipal* aPrincipal); + + static Result<nsAutoCString, nsresult> GetOriginFromWindow( + nsPIDOMWindowOuter* aWindow); + + static nsLiteralCString GetOriginForChrome(); + + static PrincipalMetadata GetInfoForChrome(); + + static bool IsOriginInternal(const nsACString& aOrigin); + + static bool AreOriginsEqualOnDisk(const nsACString& aOrigin1, + const nsACString& aOrigin2); + + static Result<PrincipalInfo, nsresult> ParseOrigin(const nsACString& aOrigin); + + static void InvalidateQuotaCache(); + + private: + virtual ~QuotaManager(); + + nsresult Init(); + + void Shutdown(); + + void RegisterDirectoryLock(DirectoryLockImpl& aLock); + + void UnregisterDirectoryLock(DirectoryLockImpl& aLock); + + void AddPendingDirectoryLock(DirectoryLockImpl& aLock); + + void RemovePendingDirectoryLock(DirectoryLockImpl& aLock); + + uint64_t LockedCollectOriginsForEviction( + uint64_t aMinSizeToBeFreed, + nsTArray<RefPtr<OriginDirectoryLock>>& aLocks); + + void LockedRemoveQuotaForOrigin(const OriginMetadata& aOriginMetadata); + + already_AddRefed<GroupInfo> LockedGetOrCreateGroupInfo( + PersistenceType aPersistenceType, const nsACString& aSuffix, + const nsACString& aGroup); + + already_AddRefed<OriginInfo> LockedGetOriginInfo( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata); + + nsresult UpgradeFromIndexedDBDirectoryToPersistentStorageDirectory( + nsIFile* aIndexedDBDir); + + nsresult UpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory( + nsIFile* aPersistentStorageDir); + + nsresult MaybeUpgradeToDefaultStorageDirectory(nsIFile& aStorageFile); + + template <typename Helper> + nsresult UpgradeStorage(const int32_t aOldVersion, const int32_t aNewVersion, + mozIStorageConnection* aConnection); + + nsresult UpgradeStorageFrom0_0To1_0(mozIStorageConnection* aConnection); + + nsresult UpgradeStorageFrom1_0To2_0(mozIStorageConnection* aConnection); + + nsresult UpgradeStorageFrom2_0To2_1(mozIStorageConnection* aConnection); + + nsresult UpgradeStorageFrom2_1To2_2(mozIStorageConnection* aConnection); + + nsresult UpgradeStorageFrom2_2To2_3(mozIStorageConnection* aConnection); + + nsresult MaybeCreateOrUpgradeStorage(mozIStorageConnection& aConnection); + + OkOrErr MaybeRemoveLocalStorageArchiveTmpFile(); + + nsresult MaybeRemoveLocalStorageDataAndArchive(nsIFile& aLsArchiveFile); + + nsresult MaybeRemoveLocalStorageDirectories(); + + Result<Ok, nsresult> CopyLocalStorageArchiveFromWebAppsStore( + nsIFile& aLsArchiveFile) const; + + Result<nsCOMPtr<mozIStorageConnection>, nsresult> + CreateLocalStorageArchiveConnection(nsIFile& aLsArchiveFile) const; + + Result<nsCOMPtr<mozIStorageConnection>, nsresult> + RecopyLocalStorageArchiveFromWebAppsStore(nsIFile& aLsArchiveFile); + + Result<nsCOMPtr<mozIStorageConnection>, nsresult> + DowngradeLocalStorageArchive(nsIFile& aLsArchiveFile); + + Result<nsCOMPtr<mozIStorageConnection>, nsresult> + UpgradeLocalStorageArchiveFromLessThan4To4(nsIFile& aLsArchiveFile); + + /* + nsresult UpgradeLocalStorageArchiveFrom4To5(); + */ + + Result<Ok, nsresult> MaybeCreateOrUpgradeLocalStorageArchive( + nsIFile& aLsArchiveFile); + + Result<Ok, nsresult> CreateEmptyLocalStorageArchive( + nsIFile& aLsArchiveFile) const; + + template <typename OriginFunc> + nsresult InitializeRepository(PersistenceType aPersistenceType, + OriginFunc&& aOriginFunc); + + nsresult InitializeOrigin(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + int64_t aAccessTime, bool aPersisted, + nsIFile* aDirectory); + + using OriginInfosFlatTraversable = + nsTArray<NotNull<RefPtr<const OriginInfo>>>; + + using OriginInfosNestedTraversable = + nsTArray<nsTArray<NotNull<RefPtr<const OriginInfo>>>>; + + OriginInfosNestedTraversable GetOriginInfosExceedingGroupLimit() const; + + OriginInfosNestedTraversable GetOriginInfosExceedingGlobalLimit() const; + + void ClearOrigins(const OriginInfosNestedTraversable& aDoomedOriginInfos); + + void CleanupTemporaryStorage(); + + void DeleteFilesForOrigin(PersistenceType aPersistenceType, + const nsACString& aOrigin); + + void FinalizeOriginEviction(nsTArray<RefPtr<OriginDirectoryLock>>&& aLocks); + + Result<Ok, nsresult> ArchiveOrigins( + const nsTArray<FullOriginMetadata>& aFullOriginMetadatas); + + void ReleaseIOThreadObjects() { + AssertIsOnIOThread(); + + for (Client::Type type : AllClientTypes()) { + (*mClients)[type]->ReleaseIOThreadObjects(); + } + } + + DirectoryLockTable& GetDirectoryLockTable(PersistenceType aPersistenceType); + + bool IsSanitizedOriginValid(const nsACString& aSanitizedOrigin); + + int64_t GenerateDirectoryLockId(); + + bool ShutdownStarted() const; + + void RecordShutdownStep(Maybe<Client::Type> aClientType, + const nsACString& aStepDescription); + + template <typename Func> + auto ExecuteInitialization(Initialization aInitialization, Func&& aFunc) + -> std::invoke_result_t<Func, const FirstInitializationAttempt< + Initialization, StringGenerator>&>; + + template <typename Func> + auto ExecuteInitialization(Initialization aInitialization, + const nsACString& aContext, Func&& aFunc) + -> std::invoke_result_t<Func, const FirstInitializationAttempt< + Initialization, StringGenerator>&>; + + template <typename Func> + auto ExecuteOriginInitialization(const nsACString& aOrigin, + const OriginInitialization aInitialization, + const nsACString& aContext, Func&& aFunc) + -> std::invoke_result_t<Func, const FirstInitializationAttempt< + Initialization, StringGenerator>&>; + + template <typename Iterator> + static void MaybeInsertNonPersistedOriginInfos( + Iterator aDest, const RefPtr<GroupInfo>& aTemporaryGroupInfo, + const RefPtr<GroupInfo>& aDefaultGroupInfo); + + template <typename Collect, typename Pred> + static OriginInfosFlatTraversable CollectLRUOriginInfosUntil( + Collect&& aCollect, Pred&& aPred); + + // Thread on which IO is performed. + LazyInitializedOnceNotNull<const nsCOMPtr<nsIThread>> mIOThread; + + nsCOMPtr<mozIStorageConnection> mStorageConnection; + + EnumeratedArray<Client::Type, Client::TYPE_MAX, nsCString> mShutdownSteps; + LazyInitializedOnce<const TimeStamp> mShutdownStartedAt; + Atomic<bool> mShutdownStarted; + + // Accesses to mQuotaManagerShutdownSteps must be protected by mQuotaMutex. + nsCString mQuotaManagerShutdownSteps; + + mutable mozilla::Mutex mQuotaMutex MOZ_UNANNOTATED; + + nsClassHashtable<nsCStringHashKey, GroupInfoPair> mGroupInfoPairs; + + // Maintains a list of directory locks that are queued. + nsTArray<RefPtr<DirectoryLockImpl>> mPendingDirectoryLocks; + + // Maintains a list of directory locks that are acquired or queued. It can be + // accessed on the owning (PBackground) thread only. + nsTArray<NotNull<DirectoryLockImpl*>> mDirectoryLocks; + + // Only modifed on the owning thread, but read on multiple threads. Therefore + // all modifications (including those on the owning thread) and all reads off + // the owning thread must be protected by mQuotaMutex. In other words, only + // reads on the owning thread don't have to be protected by mQuotaMutex. + nsTHashMap<nsUint64HashKey, NotNull<DirectoryLockImpl*>> + mDirectoryLockIdTable; + + // Directory lock tables that are used to update origin access time. + DirectoryLockTable mTemporaryDirectoryLockTable; + DirectoryLockTable mDefaultDirectoryLockTable; + + // A list of all successfully initialized persistent origins. This list isn't + // protected by any mutex but it is only ever touched on the IO thread. + nsTArray<nsCString> mInitializedOrigins; + + // A hash table that is used to cache origin parser results for given + // sanitized origin strings. This hash table isn't protected by any mutex but + // it is only ever touched on the IO thread. + nsTHashMap<nsCStringHashKey, bool> mValidOrigins; + + // This array is populated at initialization time and then never modified, so + // it can be iterated on any thread. + LazyInitializedOnce<const AutoTArray<RefPtr<Client>, Client::TYPE_MAX>> + mClients; + + using ClientTypesArray = AutoTArray<Client::Type, Client::TYPE_MAX>; + LazyInitializedOnce<const ClientTypesArray> mAllClientTypes; + LazyInitializedOnce<const ClientTypesArray> mAllClientTypesExceptLS; + + // This object isn't protected by any mutex but it is only ever touched on + // the IO thread. + InitializationInfo mInitializationInfo; + + const nsString mBasePath; + const nsString mStorageName; + LazyInitializedOnce<const nsString> mIndexedDBPath; + LazyInitializedOnce<const nsString> mStoragePath; + LazyInitializedOnce<const nsString> mStorageArchivesPath; + LazyInitializedOnce<const nsString> mPermanentStoragePath; + LazyInitializedOnce<const nsString> mTemporaryStoragePath; + LazyInitializedOnce<const nsString> mDefaultStoragePath; + + MozPromiseHolder<BoolPromise> mShutdownStoragePromiseHolder; + + uint64_t mTemporaryStorageLimit; + uint64_t mTemporaryStorageUsage; + int64_t mNextDirectoryLockId; + bool mTemporaryStorageInitialized; + bool mCacheUsable; + bool mShuttingDownStorage; +}; + +} // namespace mozilla::dom::quota + +#endif /* mozilla_dom_quota_quotamanager_h__ */ diff --git a/dom/quota/QuotaManagerImpl.h b/dom/quota/QuotaManagerImpl.h new file mode 100644 index 0000000000..1cdd5ed99e --- /dev/null +++ b/dom/quota/QuotaManagerImpl.h @@ -0,0 +1,25 @@ +/* -*- 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_QUOTA_QUOTAMANAGERIMPL_H_ +#define DOM_QUOTA_QUOTAMANAGERIMPL_H_ + +#include "QuotaManager.h" + +namespace mozilla::dom::quota { + +template <typename F> +void QuotaManager::MaybeRecordQuotaManagerShutdownStepWith(F&& aFunc) { + // Callable on any thread. + + if (IsShuttingDown()) { + RecordShutdownStep(Nothing{}, std::forward<F>(aFunc)()); + } +} + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_QUOTAMANAGERIMPL_H_ diff --git a/dom/quota/QuotaManagerService.cpp b/dom/quota/QuotaManagerService.cpp new file mode 100644 index 0000000000..48dcbfa659 --- /dev/null +++ b/dom/quota/QuotaManagerService.cpp @@ -0,0 +1,1046 @@ +/* -*- 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 "QuotaManagerService.h" + +// Local includes +#include "ActorsChild.h" +#include "Client.h" +#include "QuotaManager.h" +#include "QuotaRequests.h" + +// Global includes +#include <cstdint> +#include <cstring> +#include <utility> +#include "MainThreadUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Hal.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Maybe.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Unused.h" +#include "mozilla/Variant.h" +#include "mozilla/dom/quota/PQuota.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/fallible.h" +#include "mozilla/hal_sandbox/PHal.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIObserverService.h" +#include "nsIPrincipal.h" +#include "nsIUserIdleService.h" +#include "nsServiceManagerUtils.h" +#include "nsStringFwd.h" +#include "nsXULAppAPI.h" +#include "nscore.h" + +#define PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID "profile-before-change-qm" + +namespace mozilla::dom::quota { + +using namespace mozilla::ipc; + +namespace { + +const char kIdleServiceContractId[] = "@mozilla.org/widget/useridleservice;1"; + +// The number of seconds we will wait after receiving the idle-daily +// notification before beginning maintenance. +const uint32_t kIdleObserverTimeSec = 1; + +mozilla::StaticRefPtr<QuotaManagerService> gQuotaManagerService; + +mozilla::Atomic<bool> gInitialized(false); +mozilla::Atomic<bool> gClosed(false); + +nsresult CheckedPrincipalToPrincipalInfo(nsIPrincipal* aPrincipal, + PrincipalInfo& aPrincipalInfo) { + MOZ_ASSERT(aPrincipal); + + nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aPrincipalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(aPrincipalInfo))) { + return NS_ERROR_FAILURE; + } + + if (aPrincipalInfo.type() != PrincipalInfo::TContentPrincipalInfo && + aPrincipalInfo.type() != PrincipalInfo::TSystemPrincipalInfo) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +nsresult GetClearResetOriginParams(nsIPrincipal* aPrincipal, + const nsACString& aPersistenceType, + const nsAString& aClientType, + ClearResetOriginParams& aParams) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + nsresult rv = + CheckedPrincipalToPrincipalInfo(aPrincipal, aParams.principalInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aPersistenceType.IsVoid()) { + aParams.persistenceTypeIsExplicit() = false; + } else { + const auto maybePersistenceType = + PersistenceTypeFromString(aPersistenceType, fallible); + if (NS_WARN_IF(maybePersistenceType.isNothing())) { + return NS_ERROR_INVALID_ARG; + } + + aParams.persistenceType() = maybePersistenceType.value(); + aParams.persistenceTypeIsExplicit() = true; + } + + if (aClientType.IsVoid()) { + aParams.clientTypeIsExplicit() = false; + } else { + Client::Type clientType; + bool ok = Client::TypeFromText(aClientType, clientType, fallible); + if (NS_WARN_IF(!ok)) { + return NS_ERROR_INVALID_ARG; + } + + aParams.clientType() = clientType; + aParams.clientTypeIsExplicit() = true; + } + + return NS_OK; +} + +} // namespace + +class QuotaManagerService::PendingRequestInfo { + protected: + RefPtr<RequestBase> mRequest; + + public: + explicit PendingRequestInfo(RequestBase* aRequest) : mRequest(aRequest) {} + + virtual ~PendingRequestInfo() = default; + + RequestBase* GetRequest() const { return mRequest; } + + virtual nsresult InitiateRequest(QuotaChild* aActor) = 0; +}; + +class QuotaManagerService::UsageRequestInfo : public PendingRequestInfo { + UsageRequestParams mParams; + + public: + UsageRequestInfo(UsageRequest* aRequest, const UsageRequestParams& aParams) + : PendingRequestInfo(aRequest), mParams(aParams) { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None); + } + + virtual nsresult InitiateRequest(QuotaChild* aActor) override; +}; + +class QuotaManagerService::RequestInfo : public PendingRequestInfo { + RequestParams mParams; + + public: + RequestInfo(Request* aRequest, const RequestParams& aParams) + : PendingRequestInfo(aRequest), mParams(aParams) { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + } + + virtual nsresult InitiateRequest(QuotaChild* aActor) override; +}; + +class QuotaManagerService::IdleMaintenanceInfo : public PendingRequestInfo { + const bool mStart; + + public: + explicit IdleMaintenanceInfo(bool aStart) + : PendingRequestInfo(nullptr), mStart(aStart) {} + + virtual nsresult InitiateRequest(QuotaChild* aActor) override; +}; + +QuotaManagerService::QuotaManagerService() + : mBackgroundActor(nullptr), + mBackgroundActorFailed(false), + mIdleObserverRegistered(false) { + MOZ_ASSERT(NS_IsMainThread()); +} + +QuotaManagerService::~QuotaManagerService() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mIdleObserverRegistered); +} + +// static +QuotaManagerService* QuotaManagerService::GetOrCreate() { + MOZ_ASSERT(NS_IsMainThread()); + + if (gClosed) { + MOZ_ASSERT(false, "Calling GetOrCreate() after shutdown!"); + return nullptr; + } + + if (!gQuotaManagerService) { + RefPtr<QuotaManagerService> instance(new QuotaManagerService()); + + nsresult rv = instance->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + if (gInitialized.exchange(true)) { + MOZ_ASSERT(false, "Initialized more than once?!"); + } + + gQuotaManagerService = instance; + + ClearOnShutdown(&gQuotaManagerService); + } + + return gQuotaManagerService; +} + +// static +QuotaManagerService* QuotaManagerService::Get() { + // Does not return an owning reference. + return gQuotaManagerService; +} + +// static +already_AddRefed<QuotaManagerService> QuotaManagerService::FactoryCreate() { + RefPtr<QuotaManagerService> quotaManagerService = GetOrCreate(); + return quotaManagerService.forget(); +} + +void QuotaManagerService::ClearBackgroundActor() { + MOZ_ASSERT(NS_IsMainThread()); + + mBackgroundActor = nullptr; +} + +void QuotaManagerService::AbortOperationsForProcess( + ContentParentId aContentParentId) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = EnsureBackgroundActor(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (NS_WARN_IF( + !mBackgroundActor->SendAbortOperationsForProcess(aContentParentId))) { + return; + } +} + +nsresult QuotaManagerService::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (NS_WARN_IF(!observerService)) { + return NS_ERROR_FAILURE; + } + + nsresult rv = observerService->AddObserver( + this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +void QuotaManagerService::Destroy() { + // Setting the closed flag prevents the service from being recreated. + // Don't set it though if there's no real instance created. + if (gInitialized && gClosed.exchange(true)) { + MOZ_ASSERT(false, "Shutdown more than once?!"); + } + + delete this; +} + +nsresult QuotaManagerService::EnsureBackgroundActor() { + MOZ_ASSERT(NS_IsMainThread()); + + // Nothing can be done here if we have previously failed to create a + // background actor. + if (mBackgroundActorFailed) { + return NS_ERROR_FAILURE; + } + + if (!mBackgroundActor) { + PBackgroundChild* backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + mBackgroundActorFailed = true; + return NS_ERROR_FAILURE; + } + + { + QuotaChild* actor = new QuotaChild(this); + + mBackgroundActor = static_cast<QuotaChild*>( + backgroundActor->SendPQuotaConstructor(actor)); + } + } + + if (!mBackgroundActor) { + mBackgroundActorFailed = true; + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult QuotaManagerService::InitiateRequest(PendingRequestInfo& aInfo) { + nsresult rv = EnsureBackgroundActor(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aInfo.InitiateRequest(mBackgroundActor); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void QuotaManagerService::PerformIdleMaintenance() { + using namespace mozilla::hal; + + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + // If we're running on battery power then skip all idle maintenance since we + // would otherwise be doing lots of disk I/O. + BatteryInformation batteryInfo; + +#ifdef MOZ_WIDGET_ANDROID + // Android XPCShell doesn't load the AndroidBridge that is needed to make + // GetCurrentBatteryInformation work... + if (!QuotaManager::IsRunningXPCShellTests()) +#endif + { + // In order to give the correct battery level, hal must have registered + // battery observers. + RegisterBatteryObserver(this); + GetCurrentBatteryInformation(&batteryInfo); + UnregisterBatteryObserver(this); + } + + // If we're running XPCShell because we always want to be able to test this + // code so pretend that we're always charging. + if (QuotaManager::IsRunningXPCShellTests()) { + batteryInfo.level() = 100; + batteryInfo.charging() = true; + } + + if (NS_WARN_IF(!batteryInfo.charging())) { + return; + } + + if (QuotaManager::IsRunningXPCShellTests()) { + // We don't want user activity to impact this code if we're running tests. + Unused << Observe(nullptr, OBSERVER_TOPIC_IDLE, nullptr); + } else if (!mIdleObserverRegistered) { + nsCOMPtr<nsIUserIdleService> idleService = + do_GetService(kIdleServiceContractId); + MOZ_ASSERT(idleService); + + MOZ_ALWAYS_SUCCEEDS( + idleService->AddIdleObserver(this, kIdleObserverTimeSec)); + + mIdleObserverRegistered = true; + } +} + +void QuotaManagerService::RemoveIdleObserver() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + if (mIdleObserverRegistered) { + nsCOMPtr<nsIUserIdleService> idleService = + do_GetService(kIdleServiceContractId); + MOZ_ASSERT(idleService); + + // Ignore the return value of RemoveIdleObserver, it may fail if the + // observer has already been unregistered during shutdown. + Unused << idleService->RemoveIdleObserver(this, kIdleObserverTimeSec); + + mIdleObserverRegistered = false; + } +} + +NS_IMPL_ADDREF(QuotaManagerService) +NS_IMPL_RELEASE_WITH_DESTROY(QuotaManagerService, Destroy()) +NS_IMPL_QUERY_INTERFACE(QuotaManagerService, nsIQuotaManagerService, + nsIObserver) + +NS_IMETHODIMP +QuotaManagerService::StorageName(nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<Request> request = new Request(); + + StorageNameParams params; + + RequestInfo info(request, params); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::StorageInitialized(nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<Request> request = new Request(); + + StorageInitializedParams params; + + RequestInfo info(request, params); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::TemporaryStorageInitialized(nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<Request> request = new Request(); + + TemporaryStorageInitializedParams params; + + RequestInfo info(request, params); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::Init(nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<Request> request = new Request(); + + InitParams params; + + RequestInfo info(request, params); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::InitTemporaryStorage(nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<Request> request = new Request(); + + InitTemporaryStorageParams params; + + RequestInfo info(request, params); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::InitializePersistentOrigin(nsIPrincipal* aPrincipal, + nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<Request> request = new Request(); + + InitializePersistentOriginParams params; + + nsresult rv = + CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RequestInfo info(request, params); + + rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::InitializeTemporaryOrigin( + const nsACString& aPersistenceType, nsIPrincipal* aPrincipal, + nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<Request> request = new Request(); + + InitializeTemporaryOriginParams params; + + const auto maybePersistenceType = + PersistenceTypeFromString(aPersistenceType, fallible); + if (NS_WARN_IF(maybePersistenceType.isNothing())) { + return NS_ERROR_INVALID_ARG; + } + + if (NS_WARN_IF(!IsBestEffortPersistenceType(maybePersistenceType.value()))) { + return NS_ERROR_FAILURE; + } + + params.persistenceType() = maybePersistenceType.value(); + + nsresult rv = + CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RequestInfo info(request, params); + + rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::GetFullOriginMetadata(const nsACString& aPersistenceType, + nsIPrincipal* aPrincipal, + nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + QM_TRY(OkIf(StaticPrefs::dom_quotaManager_testing()), NS_ERROR_UNEXPECTED); + + const auto maybePersistenceType = + PersistenceTypeFromString(aPersistenceType, fallible); + QM_TRY(OkIf(maybePersistenceType.isSome()), NS_ERROR_INVALID_ARG); + QM_TRY(OkIf(IsBestEffortPersistenceType(*maybePersistenceType)), + NS_ERROR_INVALID_ARG); + + PrincipalInfo principalInfo; + QM_TRY(MOZ_TO_RESULT(PrincipalToPrincipalInfo(aPrincipal, &principalInfo))); + QM_TRY(OkIf(QuotaManager::IsPrincipalInfoValid(principalInfo)), + NS_ERROR_INVALID_ARG); + + RefPtr<Request> request = new Request(); + + GetFullOriginMetadataParams params; + + params.persistenceType() = *maybePersistenceType; + params.principalInfo() = std::move(principalInfo); + + RequestInfo info(request, params); + + QM_TRY(MOZ_TO_RESULT(InitiateRequest(info))); + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::GetUsage(nsIQuotaUsageCallback* aCallback, bool aGetAll, + nsIQuotaUsageRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aCallback); + + RefPtr<UsageRequest> request = new UsageRequest(aCallback); + + AllUsageParams params; + + params.getAll() = aGetAll; + + UsageRequestInfo info(request, params); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::GetUsageForPrincipal(nsIPrincipal* aPrincipal, + nsIQuotaUsageCallback* aCallback, + bool aFromMemory, + nsIQuotaUsageRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aCallback); + + RefPtr<UsageRequest> request = new UsageRequest(aPrincipal, aCallback); + + OriginUsageParams params; + + nsresult rv = + CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + params.fromMemory() = aFromMemory; + + UsageRequestInfo info(request, params); + + rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::Clear(nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<Request> request = new Request(); + + ClearAllParams params; + + RequestInfo info(request, params); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::ClearStoragesForOriginAttributesPattern( + const nsAString& aPattern, nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + + OriginAttributesPattern pattern; + MOZ_ALWAYS_TRUE(pattern.Init(aPattern)); + + RefPtr<Request> request = new Request(); + + ClearDataParams params; + + params.pattern() = pattern; + + RequestInfo info(request, params); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::ClearStoragesForPrincipal( + nsIPrincipal* aPrincipal, const nsACString& aPersistenceType, + const nsAString& aClientType, bool aClearAll, nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + nsCString suffix; + aPrincipal->OriginAttributesRef().CreateSuffix(suffix); + + if (NS_WARN_IF(aClearAll && !suffix.IsEmpty())) { + // The originAttributes should be default originAttributes when the + // aClearAll flag is set. + return NS_ERROR_INVALID_ARG; + } + + RefPtr<Request> request = new Request(aPrincipal); + + ClearResetOriginParams commonParams; + + nsresult rv = GetClearResetOriginParams(aPrincipal, aPersistenceType, + aClientType, commonParams); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + ClearOriginParams params; + params.commonParams() = std::move(commonParams); + params.matchAll() = aClearAll; + + RequestInfo info(request, params); + + rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::Reset(nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<Request> request = new Request(); + + ResetAllParams params; + + RequestInfo info(request, params); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::ResetStoragesForPrincipal( + nsIPrincipal* aPrincipal, const nsACString& aPersistenceType, + const nsAString& aClientType, nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<Request> request = new Request(aPrincipal); + + ClearResetOriginParams commonParams; + + nsresult rv = GetClearResetOriginParams(aPrincipal, aPersistenceType, + aClientType, commonParams); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RequestParams params; + params = ResetOriginParams(commonParams); + + RequestInfo info(request, params); + + rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::Persisted(nsIPrincipal* aPrincipal, + nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + RefPtr<Request> request = new Request(aPrincipal); + + PersistedParams params; + + nsresult rv = + CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RequestInfo info(request, params); + + rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::Persist(nsIPrincipal* aPrincipal, + nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + RefPtr<Request> request = new Request(aPrincipal); + + PersistParams params; + + nsresult rv = + CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RequestInfo info(request, params); + + rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::Estimate(nsIPrincipal* aPrincipal, + nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + RefPtr<Request> request = new Request(aPrincipal); + + EstimateParams params; + + nsresult rv = + CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RequestInfo info(request, params); + + rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::ListOrigins(nsIQuotaRequest** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<Request> request = new Request(); + + ListOriginsParams params; + + RequestInfo info(request, params); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManagerService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + if (!strcmp(aTopic, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID)) { + RemoveIdleObserver(); + return NS_OK; + } + + if (!strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY)) { + PerformIdleMaintenance(); + return NS_OK; + } + + if (!strcmp(aTopic, OBSERVER_TOPIC_IDLE)) { + IdleMaintenanceInfo info(/* aStart */ true); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + if (!strcmp(aTopic, OBSERVER_TOPIC_ACTIVE)) { + RemoveIdleObserver(); + + IdleMaintenanceInfo info(/* aStart */ false); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + MOZ_ASSERT_UNREACHABLE("Should never get here!"); + return NS_OK; +} + +void QuotaManagerService::Notify(const hal::BatteryInformation& aBatteryInfo) { + // This notification is received when battery data changes. We don't need to + // deal with this notification. +} + +nsresult QuotaManagerService::UsageRequestInfo::InitiateRequest( + QuotaChild* aActor) { + MOZ_ASSERT(aActor); + + auto request = static_cast<UsageRequest*>(mRequest.get()); + + auto actor = new QuotaUsageRequestChild(request); + + if (!aActor->SendPQuotaUsageRequestConstructor(actor, mParams)) { + request->SetError(NS_ERROR_FAILURE); + return NS_ERROR_FAILURE; + } + + request->SetBackgroundActor(actor); + + return NS_OK; +} + +nsresult QuotaManagerService::RequestInfo::InitiateRequest(QuotaChild* aActor) { + MOZ_ASSERT(aActor); + + auto request = static_cast<Request*>(mRequest.get()); + + auto actor = new QuotaRequestChild(request); + + if (!aActor->SendPQuotaRequestConstructor(actor, mParams)) { + request->SetError(NS_ERROR_FAILURE); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult QuotaManagerService::IdleMaintenanceInfo::InitiateRequest( + QuotaChild* aActor) { + MOZ_ASSERT(aActor); + + bool result; + + if (mStart) { + result = aActor->SendStartIdleMaintenance(); + } else { + result = aActor->SendStopIdleMaintenance(); + } + + if (!result) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/QuotaManagerService.h b/dom/quota/QuotaManagerService.h new file mode 100644 index 0000000000..f8f8c01c31 --- /dev/null +++ b/dom/quota/QuotaManagerService.h @@ -0,0 +1,102 @@ +/* -*- 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_quota_QuotaManagerService_h +#define mozilla_dom_quota_QuotaManagerService_h + +#include <cstdint> +#include "ErrorList.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/HalBatteryInformation.h" +#include "mozilla/dom/ipc/IdType.h" +#include "nsIObserver.h" +#include "nsIQuotaManagerService.h" +#include "nsISupports.h" + +#define QUOTAMANAGER_SERVICE_CONTRACTID \ + "@mozilla.org/dom/quota-manager-service;1" + +class nsIPrincipal; +class nsIQuotaRequest; +class nsIQuotaUsageCallback; +class nsIQuotaUsageRequest; + +namespace mozilla { +namespace ipc { + +class PBackgroundChild; + +} // namespace ipc + +namespace hal { +class BatteryInformation; +} + +namespace dom::quota { + +class QuotaChild; +class QuotaManager; + +class QuotaManagerService final : public nsIQuotaManagerService, + public nsIObserver, + public hal::BatteryObserver { + using PBackgroundChild = mozilla::ipc::PBackgroundChild; + + class BackgroundCreateCallback; + class PendingRequestInfo; + class UsageRequestInfo; + class RequestInfo; + class IdleMaintenanceInfo; + + QuotaChild* mBackgroundActor; + + bool mBackgroundActorFailed; + bool mIdleObserverRegistered; + + public: + // Returns a non-owning reference. + static QuotaManagerService* GetOrCreate(); + + // Returns a non-owning reference. + static QuotaManagerService* Get(); + + // No one should call this but the factory. + static already_AddRefed<QuotaManagerService> FactoryCreate(); + + void ClearBackgroundActor(); + + // Called when a process is being shot down. Aborts any running operations + // for the given process. + void AbortOperationsForProcess(ContentParentId aContentParentId); + + private: + QuotaManagerService(); + ~QuotaManagerService(); + + nsresult Init(); + + void Destroy(); + + nsresult EnsureBackgroundActor(); + + nsresult InitiateRequest(PendingRequestInfo& aInfo); + + void PerformIdleMaintenance(); + + void RemoveIdleObserver(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIQUOTAMANAGERSERVICE + NS_DECL_NSIOBSERVER + + // BatteryObserver override + void Notify(const hal::BatteryInformation& aBatteryInfo) override; +}; + +} // namespace dom::quota +} // namespace mozilla + +#endif /* mozilla_dom_quota_QuotaManagerService_h */ diff --git a/dom/quota/QuotaObject.cpp b/dom/quota/QuotaObject.cpp new file mode 100644 index 0000000000..94256ed1eb --- /dev/null +++ b/dom/quota/QuotaObject.cpp @@ -0,0 +1,73 @@ +/* -*- 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 "QuotaObject.h" + +#include "CanonicalQuotaObject.h" +#include "mozilla/dom/quota/IPCQuotaObject.h" +#include "mozilla/dom/quota/PRemoteQuotaObject.h" +#include "mozilla/dom/quota/RemoteQuotaObjectChild.h" +#include "mozilla/dom/quota/RemoteQuotaObjectParent.h" +#include "mozilla/dom/quota/RemoteQuotaObjectParentTracker.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "RemoteQuotaObject.h" + +namespace mozilla::dom::quota { + +CanonicalQuotaObject* QuotaObject::AsCanonicalQuotaObject() { + return mIsRemote ? nullptr : static_cast<CanonicalQuotaObject*>(this); +} + +RemoteQuotaObject* QuotaObject::AsRemoteQuotaObject() { + return mIsRemote ? static_cast<RemoteQuotaObject*>(this) : nullptr; +} + +IPCQuotaObject QuotaObject::Serialize(nsIInterfaceRequestor* aCallbacks) { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + MOZ_RELEASE_ASSERT(!NS_IsMainThread()); + MOZ_RELEASE_ASSERT(!mozilla::ipc::IsOnBackgroundThread()); + MOZ_RELEASE_ASSERT(!GetCurrentThreadWorkerPrivate()); + MOZ_RELEASE_ASSERT(!mIsRemote); + + mozilla::ipc::Endpoint<PRemoteQuotaObjectParent> parentEndpoint; + mozilla::ipc::Endpoint<PRemoteQuotaObjectChild> childEndpoint; + MOZ_ALWAYS_SUCCEEDS( + PRemoteQuotaObject::CreateEndpoints(&parentEndpoint, &childEndpoint)); + + nsCOMPtr<RemoteQuotaObjectParentTracker> tracker = + do_GetInterface(aCallbacks); + + auto actor = + MakeRefPtr<RemoteQuotaObjectParent>(AsCanonicalQuotaObject(), tracker); + + if (tracker) { + tracker->RegisterRemoteQuotaObjectParent(WrapNotNull(actor)); + } + + parentEndpoint.Bind(actor); + + IPCQuotaObject ipcQuotaObject; + ipcQuotaObject.childEndpoint() = std::move(childEndpoint); + + return ipcQuotaObject; +} + +// static +RefPtr<QuotaObject> QuotaObject::Deserialize(IPCQuotaObject& aQuotaObject) { + MOZ_RELEASE_ASSERT(!NS_IsMainThread()); + MOZ_RELEASE_ASSERT(!mozilla::ipc::IsOnBackgroundThread()); + MOZ_RELEASE_ASSERT(!GetCurrentThreadWorkerPrivate()); + + auto actor = MakeRefPtr<RemoteQuotaObjectChild>(); + + aQuotaObject.childEndpoint().Bind(actor); + + return MakeRefPtr<RemoteQuotaObject>(actor); +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/QuotaObject.h b/dom/quota/QuotaObject.h new file mode 100644 index 0000000000..96397faf66 --- /dev/null +++ b/dom/quota/QuotaObject.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_QUOTA_QUOTAOBJECT_H_ +#define DOM_QUOTA_QUOTAOBJECT_H_ + +#include "nsISupportsImpl.h" + +class nsIInterfaceRequestor; + +namespace mozilla::dom::quota { + +class CanonicalQuotaObject; +class IPCQuotaObject; +class RemoteQuotaObject; + +// QuotaObject type is serializable, but only in a restricted manner. The type +// is only safe to serialize in the parent process and only when the type +// hasn't been previously deserialized. So the type can be serialized in the +// parent process and deserialized in a child process or it can be serialized +// in the parent process and deserialized in the parent process as well +// (non-e10s mode). The same type can never be serialized/deserialized more +// than once. +// The deserialized type (remote variant) can only be used on the thread it was +// deserialized on and it will stop working if the thread it was sent from is +// shutdown (consumers should make sure that the originating thread is kept +// alive for the necessary time). +class QuotaObject { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + CanonicalQuotaObject* AsCanonicalQuotaObject(); + + RemoteQuotaObject* AsRemoteQuotaObject(); + + // Serialize this QuotaObject. This method works only in the parent process + // and only with objects which haven't been previously deserialized. + // The serial event target where this method is called should be highly + // available, as it will be used to process requests from the remote variant. + IPCQuotaObject Serialize(nsIInterfaceRequestor* aCallbacks); + + // Deserialize a QuotaObject. This method works in both the child and parent. + // The deserialized QuotaObject can only be used on the calling serial event + // target. + static RefPtr<QuotaObject> Deserialize(IPCQuotaObject& aQuotaObject); + + virtual const nsAString& Path() const = 0; + + [[nodiscard]] virtual bool MaybeUpdateSize(int64_t aSize, bool aTruncate) = 0; + + virtual bool IncreaseSize(int64_t aDelta) = 0; + + virtual void DisableQuotaCheck() = 0; + + virtual void EnableQuotaCheck() = 0; + + protected: + QuotaObject(bool aIsRemote) : mIsRemote(aIsRemote) {} + + virtual ~QuotaObject() = default; + + const bool mIsRemote; +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_QUOTAOBJECT_H_ diff --git a/dom/quota/QuotaRequests.cpp b/dom/quota/QuotaRequests.cpp new file mode 100644 index 0000000000..ee781831c8 --- /dev/null +++ b/dom/quota/QuotaRequests.cpp @@ -0,0 +1,288 @@ +/* -*- 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 "QuotaRequests.h" + +// Local includes +#include "ActorsChild.h" + +// Global includes +#include "mozilla/ErrorNames.h" +#include "mozilla/MacroForEach.h" +#include "nsDebug.h" +#include "nsIQuotaCallbacks.h" +#include "nsISupportsUtils.h" +#include "nsIVariant.h" +#include "nsStringFwd.h" +#include "nscore.h" + +namespace mozilla { +class Runnable; +} + +namespace mozilla::dom::quota { + +RequestBase::RequestBase() : mResultCode(NS_OK), mHaveResultOrErrorCode(false) { + AssertIsOnOwningThread(); +} + +RequestBase::RequestBase(nsIPrincipal* aPrincipal) + : mPrincipal(aPrincipal), + mResultCode(NS_OK), + mHaveResultOrErrorCode(false) { + AssertIsOnOwningThread(); +} + +#ifdef DEBUG + +void RequestBase::AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(RequestBase); +} + +#endif // DEBUG + +void RequestBase::SetError(nsresult aRv) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mResultCode == NS_OK); + MOZ_ASSERT(!mHaveResultOrErrorCode); + + mResultCode = aRv; + mHaveResultOrErrorCode = true; + + FireCallback(); +} + +NS_IMPL_CYCLE_COLLECTION_0(RequestBase) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RequestBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(RequestBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(RequestBase) + +NS_IMETHODIMP +RequestBase::GetPrincipal(nsIPrincipal** aPrincipal) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aPrincipal); + + NS_IF_ADDREF(*aPrincipal = mPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +RequestBase::GetResultCode(nsresult* aResultCode) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aResultCode); + + if (!mHaveResultOrErrorCode) { + return NS_ERROR_FAILURE; + } + + *aResultCode = mResultCode; + return NS_OK; +} + +NS_IMETHODIMP +RequestBase::GetResultName(nsACString& aResultName) { + AssertIsOnOwningThread(); + + if (!mHaveResultOrErrorCode) { + return NS_ERROR_FAILURE; + } + + mozilla::GetErrorName(mResultCode, aResultName); + return NS_OK; +} + +UsageRequest::UsageRequest(nsIQuotaUsageCallback* aCallback) + : mCallback(aCallback), mBackgroundActor(nullptr), mCanceled(false) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aCallback); +} + +UsageRequest::UsageRequest(nsIPrincipal* aPrincipal, + nsIQuotaUsageCallback* aCallback) + : RequestBase(aPrincipal), + mCallback(aCallback), + mBackgroundActor(nullptr), + mCanceled(false) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aCallback); +} + +UsageRequest::~UsageRequest() { AssertIsOnOwningThread(); } + +void UsageRequest::SetBackgroundActor( + QuotaUsageRequestChild* aBackgroundActor) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aBackgroundActor); + MOZ_ASSERT(!mBackgroundActor); + + mBackgroundActor = aBackgroundActor; + + if (mCanceled) { + mBackgroundActor->SendCancel(); + } +} + +void UsageRequest::SetResult(nsIVariant* aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aResult); + MOZ_ASSERT(!mHaveResultOrErrorCode); + + mResult = aResult; + + mHaveResultOrErrorCode = true; + + FireCallback(); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(UsageRequest, RequestBase, mCallback) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UsageRequest) + NS_INTERFACE_MAP_ENTRY(nsIQuotaUsageRequest) +NS_INTERFACE_MAP_END_INHERITING(RequestBase) + +NS_IMPL_ADDREF_INHERITED(UsageRequest, RequestBase) +NS_IMPL_RELEASE_INHERITED(UsageRequest, RequestBase) + +NS_IMETHODIMP +UsageRequest::GetResult(nsIVariant** aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aResult); + + if (!mHaveResultOrErrorCode) { + return NS_ERROR_FAILURE; + } + + NS_IF_ADDREF(*aResult = mResult); + return NS_OK; +} + +NS_IMETHODIMP +UsageRequest::GetCallback(nsIQuotaUsageCallback** aCallback) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aCallback); + + NS_IF_ADDREF(*aCallback = mCallback); + return NS_OK; +} + +NS_IMETHODIMP +UsageRequest::SetCallback(nsIQuotaUsageCallback* aCallback) { + AssertIsOnOwningThread(); + + mCallback = aCallback; + return NS_OK; +} + +NS_IMETHODIMP +UsageRequest::Cancel() { + AssertIsOnOwningThread(); + + if (mCanceled) { + NS_WARNING("Canceled more than once?!"); + return NS_ERROR_UNEXPECTED; + } + + if (mBackgroundActor) { + mBackgroundActor->SendCancel(); + } + + mCanceled = true; + + return NS_OK; +} + +void UsageRequest::FireCallback() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mCallback); + + mCallback->OnUsageResult(this); + + // Clean up. + mCallback = nullptr; +} + +Request::Request() { AssertIsOnOwningThread(); } + +Request::Request(nsIPrincipal* aPrincipal) : RequestBase(aPrincipal) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aPrincipal); +} + +Request::Request(nsIQuotaCallback* aCallback) : mCallback(aCallback) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aCallback); +} + +Request::~Request() { AssertIsOnOwningThread(); } + +void Request::SetResult(nsIVariant* aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aResult); + MOZ_ASSERT(!mHaveResultOrErrorCode); + + mResult = aResult; + + mHaveResultOrErrorCode = true; + + FireCallback(); +} + +NS_IMETHODIMP +Request::GetResult(nsIVariant** aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aResult); + + if (!mHaveResultOrErrorCode) { + return NS_ERROR_FAILURE; + } + + NS_IF_ADDREF(*aResult = mResult); + return NS_OK; +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(Request, RequestBase, mCallback, mResult) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Request) + NS_INTERFACE_MAP_ENTRY(nsIQuotaRequest) +NS_INTERFACE_MAP_END_INHERITING(RequestBase) + +NS_IMPL_ADDREF_INHERITED(mozilla::dom::quota::Request, RequestBase) +NS_IMPL_RELEASE_INHERITED(mozilla::dom::quota::Request, RequestBase) + +NS_IMETHODIMP +Request::GetCallback(nsIQuotaCallback** aCallback) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aCallback); + + NS_IF_ADDREF(*aCallback = mCallback); + return NS_OK; +} + +NS_IMETHODIMP +Request::SetCallback(nsIQuotaCallback* aCallback) { + AssertIsOnOwningThread(); + + mCallback = aCallback; + return NS_OK; +} + +void Request::FireCallback() { + AssertIsOnOwningThread(); + + if (mCallback) { + mCallback->OnComplete(this); + + // Clean up. + mCallback = nullptr; + } +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/QuotaRequests.h b/dom/quota/QuotaRequests.h new file mode 100644 index 0000000000..7cfa69ae6d --- /dev/null +++ b/dom/quota/QuotaRequests.h @@ -0,0 +1,121 @@ +/* -*- 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_quota_UsageRequest_h +#define mozilla_dom_quota_UsageRequest_h + +#include <cstdint> +#include "ErrorList.h" +#include "mozilla/Assertions.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIPrincipal.h" +#include "nsIQuotaRequests.h" +#include "nsISupports.h" + +class nsIQuotaCallback; +class nsIQuotaUsageCallback; +class nsIVariant; + +namespace mozilla::dom::quota { + +class QuotaUsageRequestChild; + +class RequestBase : public nsIQuotaRequestBase { + protected: + nsCOMPtr<nsIPrincipal> mPrincipal; + + nsresult mResultCode; + bool mHaveResultOrErrorCode; + + public: + void AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + void SetError(nsresult aRv); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIQUOTAREQUESTBASE + NS_DECL_CYCLE_COLLECTION_CLASS(RequestBase) + + protected: + RequestBase(); + + RequestBase(nsIPrincipal* aPrincipal); + + virtual ~RequestBase() { AssertIsOnOwningThread(); } + + virtual void FireCallback() = 0; +}; + +class UsageRequest final : public RequestBase, public nsIQuotaUsageRequest { + nsCOMPtr<nsIQuotaUsageCallback> mCallback; + + nsCOMPtr<nsIVariant> mResult; + + QuotaUsageRequestChild* mBackgroundActor; + + bool mCanceled; + + public: + explicit UsageRequest(nsIQuotaUsageCallback* aCallback); + + UsageRequest(nsIPrincipal* aPrincipal, nsIQuotaUsageCallback* aCallback); + + void SetBackgroundActor(QuotaUsageRequestChild* aBackgroundActor); + + void ClearBackgroundActor() { + AssertIsOnOwningThread(); + + mBackgroundActor = nullptr; + } + + void SetResult(nsIVariant* aResult); + + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSIQUOTAREQUESTBASE(RequestBase::) + NS_DECL_NSIQUOTAUSAGEREQUEST + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(UsageRequest, RequestBase) + + private: + ~UsageRequest(); + + virtual void FireCallback() override; +}; + +class Request final : public RequestBase, public nsIQuotaRequest { + nsCOMPtr<nsIQuotaCallback> mCallback; + + nsCOMPtr<nsIVariant> mResult; + + public: + Request(); + + explicit Request(nsIPrincipal* aPrincipal); + + explicit Request(nsIQuotaCallback* aCallback); + + void SetResult(nsIVariant* aResult); + + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSIQUOTAREQUESTBASE(RequestBase::) + NS_DECL_NSIQUOTAREQUEST + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Request, RequestBase) + + private: + ~Request(); + + virtual void FireCallback() override; +}; + +} // namespace mozilla::dom::quota + +#endif // mozilla_dom_quota_UsageRequest_h diff --git a/dom/quota/QuotaResults.cpp b/dom/quota/QuotaResults.cpp new file mode 100644 index 0000000000..2b715efa22 --- /dev/null +++ b/dom/quota/QuotaResults.cpp @@ -0,0 +1,144 @@ +/* -*- 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 "QuotaResults.h" + +#include "ErrorList.h" +#include "mozilla/Assertions.h" +#include "mozilla/MacroForEach.h" +#include "nscore.h" + +namespace mozilla::dom::quota { + +FullOriginMetadataResult::FullOriginMetadataResult( + const FullOriginMetadata& aFullOriginMetadata) + : mFullOriginMetadata(aFullOriginMetadata) {} + +NS_IMPL_ISUPPORTS(FullOriginMetadataResult, nsIQuotaFullOriginMetadataResult) + +NS_IMETHODIMP +FullOriginMetadataResult::GetSuffix(nsACString& aSuffix) { + aSuffix = mFullOriginMetadata.mSuffix; + return NS_OK; +} + +NS_IMETHODIMP +FullOriginMetadataResult::GetGroup(nsACString& aGroup) { + aGroup = mFullOriginMetadata.mGroup; + return NS_OK; +} + +NS_IMETHODIMP +FullOriginMetadataResult::GetOrigin(nsACString& aOrigin) { + aOrigin = mFullOriginMetadata.mOrigin; + return NS_OK; +} + +NS_IMETHODIMP +FullOriginMetadataResult::GetPersistenceType(nsACString& aPersistenceType) { + aPersistenceType = + PersistenceTypeToString(mFullOriginMetadata.mPersistenceType); + return NS_OK; +} + +NS_IMETHODIMP +FullOriginMetadataResult::GetPersisted(bool* aPersisted) { + MOZ_ASSERT(aPersisted); + + *aPersisted = mFullOriginMetadata.mPersisted; + return NS_OK; +} + +NS_IMETHODIMP +FullOriginMetadataResult::GetLastAccessTime(int64_t* aLastAccessTime) { + MOZ_ASSERT(aLastAccessTime); + + *aLastAccessTime = mFullOriginMetadata.mLastAccessTime; + return NS_OK; +} + +UsageResult::UsageResult(const nsACString& aOrigin, bool aPersisted, + uint64_t aUsage, uint64_t aLastAccessed) + : mOrigin(aOrigin), + mUsage(aUsage), + mPersisted(aPersisted), + mLastAccessed(aLastAccessed) {} + +NS_IMPL_ISUPPORTS(UsageResult, nsIQuotaUsageResult) + +NS_IMETHODIMP +UsageResult::GetOrigin(nsACString& aOrigin) { + aOrigin = mOrigin; + return NS_OK; +} + +NS_IMETHODIMP +UsageResult::GetPersisted(bool* aPersisted) { + MOZ_ASSERT(aPersisted); + + *aPersisted = mPersisted; + return NS_OK; +} + +NS_IMETHODIMP +UsageResult::GetUsage(uint64_t* aUsage) { + MOZ_ASSERT(aUsage); + + *aUsage = mUsage; + return NS_OK; +} + +NS_IMETHODIMP +UsageResult::GetLastAccessed(uint64_t* aLastAccessed) { + MOZ_ASSERT(aLastAccessed); + + *aLastAccessed = mLastAccessed; + return NS_OK; +} + +OriginUsageResult::OriginUsageResult(uint64_t aUsage, uint64_t aFileUsage) + : mUsage(aUsage), mFileUsage(aFileUsage) {} + +NS_IMPL_ISUPPORTS(OriginUsageResult, nsIQuotaOriginUsageResult) + +NS_IMETHODIMP +OriginUsageResult::GetUsage(uint64_t* aUsage) { + MOZ_ASSERT(aUsage); + + *aUsage = mUsage; + return NS_OK; +} + +NS_IMETHODIMP +OriginUsageResult::GetFileUsage(uint64_t* aFileUsage) { + MOZ_ASSERT(aFileUsage); + + *aFileUsage = mFileUsage; + return NS_OK; +} + +EstimateResult::EstimateResult(uint64_t aUsage, uint64_t aLimit) + : mUsage(aUsage), mLimit(aLimit) {} + +NS_IMPL_ISUPPORTS(EstimateResult, nsIQuotaEstimateResult) + +NS_IMETHODIMP +EstimateResult::GetUsage(uint64_t* aUsage) { + MOZ_ASSERT(aUsage); + + *aUsage = mUsage; + return NS_OK; +} + +NS_IMETHODIMP +EstimateResult::GetLimit(uint64_t* aLimit) { + MOZ_ASSERT(aLimit); + + *aLimit = mLimit; + return NS_OK; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/QuotaResults.h b/dom/quota/QuotaResults.h new file mode 100644 index 0000000000..e60eff4043 --- /dev/null +++ b/dom/quota/QuotaResults.h @@ -0,0 +1,79 @@ +/* -*- 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_quota_QuotaResults_h +#define mozilla_dom_quota_QuotaResults_h + +#include <cstdint> +#include "mozilla/dom/quota/CommonMetadata.h" +#include "nsIQuotaResults.h" +#include "nsISupports.h" +#include "nsString.h" + +namespace mozilla::dom::quota { + +class FullOriginMetadataResult : public nsIQuotaFullOriginMetadataResult { + const FullOriginMetadata mFullOriginMetadata; + + public: + explicit FullOriginMetadataResult( + const FullOriginMetadata& aFullOriginMetadata); + + private: + virtual ~FullOriginMetadataResult() = default; + + NS_DECL_ISUPPORTS + NS_DECL_NSIQUOTAFULLORIGINMETADATARESULT +}; + +class UsageResult : public nsIQuotaUsageResult { + nsCString mOrigin; + uint64_t mUsage; + bool mPersisted; + uint64_t mLastAccessed; + + public: + UsageResult(const nsACString& aOrigin, bool aPersisted, uint64_t aUsage, + uint64_t aLastAccessed); + + private: + virtual ~UsageResult() = default; + + NS_DECL_ISUPPORTS + NS_DECL_NSIQUOTAUSAGERESULT +}; + +class OriginUsageResult : public nsIQuotaOriginUsageResult { + uint64_t mUsage; + uint64_t mFileUsage; + + public: + OriginUsageResult(uint64_t aUsage, uint64_t aFileUsage); + + private: + virtual ~OriginUsageResult() = default; + + NS_DECL_ISUPPORTS + NS_DECL_NSIQUOTAORIGINUSAGERESULT +}; + +class EstimateResult : public nsIQuotaEstimateResult { + uint64_t mUsage; + uint64_t mLimit; + + public: + EstimateResult(uint64_t aUsage, uint64_t aLimit); + + private: + virtual ~EstimateResult() = default; + + NS_DECL_ISUPPORTS + NS_DECL_NSIQUOTAESTIMATERESULT +}; + +} // namespace mozilla::dom::quota + +#endif // mozilla_dom_quota_QuotaResults_h diff --git a/dom/quota/RemoteQuotaObject.cpp b/dom/quota/RemoteQuotaObject.cpp new file mode 100644 index 0000000000..3fa3370fc1 --- /dev/null +++ b/dom/quota/RemoteQuotaObject.cpp @@ -0,0 +1,65 @@ +/* -*- 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 "RemoteQuotaObject.h" + +#include "mozilla/dom/quota/RemoteQuotaObjectChild.h" +#include "mozilla/ipc/BackgroundParent.h" + +namespace mozilla::dom::quota { + +RemoteQuotaObject::RemoteQuotaObject(RefPtr<RemoteQuotaObjectChild> aActor) + : QuotaObject(/* aIsRemote */ true), mActor(std::move(aActor)) { + MOZ_COUNT_CTOR(RemoteQuotaObject); + + mActor->SetRemoteQuotaObject(this); +} + +RemoteQuotaObject::~RemoteQuotaObject() { + MOZ_COUNT_DTOR(RemoteQuotaObject); + + Close(); +} + +void RemoteQuotaObject::ClearActor() { + MOZ_ASSERT(mActor); + + mActor = nullptr; +} + +void RemoteQuotaObject::Close() { + if (!mActor) { + return; + } + + MOZ_ASSERT(mActor->GetActorEventTarget()->IsOnCurrentThread()); + + mActor->Close(); + MOZ_ASSERT(!mActor); +} + +const nsAString& RemoteQuotaObject::Path() const { return EmptyString(); } + +bool RemoteQuotaObject::MaybeUpdateSize(int64_t aSize, bool aTruncate) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!mozilla::ipc::IsOnBackgroundThread()); + MOZ_ASSERT(!GetCurrentThreadWorkerPrivate()); + + if (!mActor) { + return false; + } + + MOZ_ASSERT(mActor->GetActorEventTarget()->IsOnCurrentThread()); + + bool result; + if (!mActor->SendMaybeUpdateSize(aSize, aTruncate, &result)) { + return false; + } + + return result; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/RemoteQuotaObject.h b/dom/quota/RemoteQuotaObject.h new file mode 100644 index 0000000000..48ae0c51f3 --- /dev/null +++ b/dom/quota/RemoteQuotaObject.h @@ -0,0 +1,47 @@ +/* -*- 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_QUOTA_REMOTEQUOTAOBJECT_H_ +#define DOM_QUOTA_REMOTEQUOTAOBJECT_H_ + +#include "mozilla/dom/quota/QuotaObject.h" + +namespace mozilla::dom::quota { + +class RemoteQuotaObjectChild; + +// This object can only be used on the thread which it was created on +class RemoteQuotaObject final : public QuotaObject { + public: + explicit RemoteQuotaObject(RefPtr<RemoteQuotaObjectChild> aActor); + + NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET(RemoteQuotaObject, override) + + void ClearActor(); + + void Close(); + + const nsAString& Path() const override; + + // This method should never be called on the main thread or the PBackground + // thread or a DOM worker thread (It does sync IPC). + [[nodiscard]] bool MaybeUpdateSize(int64_t aSize, bool aTruncate) override; + + bool IncreaseSize(int64_t aDelta) override { return false; } + + void DisableQuotaCheck() override {} + + void EnableQuotaCheck() override {} + + private: + ~RemoteQuotaObject(); + + RefPtr<RemoteQuotaObjectChild> mActor; +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_REMOTEQUOTAOBJECT_H_ diff --git a/dom/quota/RemoteQuotaObjectChild.cpp b/dom/quota/RemoteQuotaObjectChild.cpp new file mode 100644 index 0000000000..2ea0d64978 --- /dev/null +++ b/dom/quota/RemoteQuotaObjectChild.cpp @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteQuotaObjectChild.h" + +#include "RemoteQuotaObject.h" + +namespace mozilla::dom::quota { + +RemoteQuotaObjectChild::RemoteQuotaObjectChild() + : mRemoteQuotaObject(nullptr) {} + +RemoteQuotaObjectChild::~RemoteQuotaObjectChild() = default; + +void RemoteQuotaObjectChild::SetRemoteQuotaObject( + RemoteQuotaObject* aRemoteQuotaObject) { + MOZ_ASSERT(aRemoteQuotaObject); + MOZ_ASSERT(!mRemoteQuotaObject); + + mRemoteQuotaObject = aRemoteQuotaObject; +} + +void RemoteQuotaObjectChild::ActorDestroy(ActorDestroyReason aWhy) { + if (mRemoteQuotaObject) { + mRemoteQuotaObject->ClearActor(); + mRemoteQuotaObject = nullptr; + } +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/RemoteQuotaObjectChild.h b/dom/quota/RemoteQuotaObjectChild.h new file mode 100644 index 0000000000..a052a3748a --- /dev/null +++ b/dom/quota/RemoteQuotaObjectChild.h @@ -0,0 +1,35 @@ +/* -*- 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_QUOTA_REMOTEQUOTAOBJECTCHILD_H_ +#define DOM_QUOTA_REMOTEQUOTAOBJECTCHILD_H_ + +#include "mozilla/dom/quota/PRemoteQuotaObjectChild.h" + +namespace mozilla::dom::quota { + +class RemoteQuotaObject; + +class RemoteQuotaObjectChild : public PRemoteQuotaObjectChild { + public: + RemoteQuotaObjectChild(); + + NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET(RemoteQuotaObjectChild, override) + + void SetRemoteQuotaObject(RemoteQuotaObject* aRemoteQuotaObject); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + virtual ~RemoteQuotaObjectChild(); + + // The weak reference is cleared in ActorDestroy. + RemoteQuotaObject* MOZ_NON_OWNING_REF mRemoteQuotaObject; +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_REMOTEQUOTAOBJECTCHILD_H_ diff --git a/dom/quota/RemoteQuotaObjectParent.cpp b/dom/quota/RemoteQuotaObjectParent.cpp new file mode 100644 index 0000000000..2624aa8b58 --- /dev/null +++ b/dom/quota/RemoteQuotaObjectParent.cpp @@ -0,0 +1,45 @@ +/* -*- 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 "RemoteQuotaObjectParent.h" + +#include "CanonicalQuotaObject.h" +#include "mozilla/dom/quota/RemoteQuotaObjectParentTracker.h" +#include "mozilla/ipc/BackgroundParent.h" + +namespace mozilla::dom::quota { + +RemoteQuotaObjectParent::RemoteQuotaObjectParent( + RefPtr<CanonicalQuotaObject> aCanonicalQuotaObject, + nsCOMPtr<RemoteQuotaObjectParentTracker> aTracker) + : mCanonicalQuotaObject(std::move(aCanonicalQuotaObject)), + mTracker(std::move(aTracker)) {} + +RemoteQuotaObjectParent::~RemoteQuotaObjectParent() { MOZ_ASSERT(!CanSend()); } + +mozilla::ipc::IPCResult RemoteQuotaObjectParent::RecvMaybeUpdateSize( + int64_t aSize, bool aTruncate, bool* aResult) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!mozilla::ipc::IsOnBackgroundThread()); + MOZ_ASSERT(!GetCurrentThreadWorkerPrivate()); + + *aResult = mCanonicalQuotaObject->MaybeUpdateSize(aSize, aTruncate); + return IPC_OK(); +} + +void RemoteQuotaObjectParent::ActorDestroy(ActorDestroyReason aWhy) { + // XXX Check that the child properly used `MaybeUpdateSize` before each + // write, so the file size on disk matches mCanonicalQuotaObject::mSize. + // If the size doesn't match, do necessary adjustments. + + mCanonicalQuotaObject = nullptr; + + if (mTracker) { + mTracker->UnregisterRemoteQuotaObjectParent(WrapNotNullUnchecked(this)); + } +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/RemoteQuotaObjectParent.h b/dom/quota/RemoteQuotaObjectParent.h new file mode 100644 index 0000000000..4932e37e64 --- /dev/null +++ b/dom/quota/RemoteQuotaObjectParent.h @@ -0,0 +1,39 @@ +/* -*- 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_QUOTA_REMOTEQUOTAOBJECTPARENT_H_ +#define DOM_QUOTA_REMOTEQUOTAOBJECTPARENT_H_ + +#include "mozilla/dom/quota/PRemoteQuotaObjectParent.h" + +namespace mozilla::dom::quota { + +class CanonicalQuotaObject; +class RemoteQuotaObjectParentTracker; + +class RemoteQuotaObjectParent : public PRemoteQuotaObjectParent { + public: + RemoteQuotaObjectParent(RefPtr<CanonicalQuotaObject> aCanonicalQuotaObject, + nsCOMPtr<RemoteQuotaObjectParentTracker> aTracker); + + NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET(RemoteQuotaObjectParent, override) + + mozilla::ipc::IPCResult RecvMaybeUpdateSize(int64_t aSize, bool aTruncate, + bool* aResult); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + virtual ~RemoteQuotaObjectParent(); + + RefPtr<CanonicalQuotaObject> mCanonicalQuotaObject; + + nsCOMPtr<RemoteQuotaObjectParentTracker> mTracker; +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_REMOTEQUOTAOBJECTPARENT_H_ diff --git a/dom/quota/RemoteQuotaObjectParentTracker.h b/dom/quota/RemoteQuotaObjectParentTracker.h new file mode 100644 index 0000000000..d712e08c0d --- /dev/null +++ b/dom/quota/RemoteQuotaObjectParentTracker.h @@ -0,0 +1,45 @@ +/* -*- 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_QUOTA_REMOTEQUOTAOBJECTPARENTTRACKER_H_ +#define DOM_QUOTA_REMOTEQUOTAOBJECTPARENTTRACKER_H_ + +#include "mozilla/NotNull.h" +#include "nsISupports.h" + +#define MOZILLA_DOM_QUOTA_REMOTEQUOTAOBJECTPARENTTRACKER_IID \ + { \ + 0x42f96136, 0x5b2b, 0x4487, { \ + 0xa4, 0x4e, 0x45, 0x0a, 0x00, 0x8f, 0xc5, 0xd4 \ + } \ + } + +namespace mozilla::dom::quota { + +class RemoteQuotaObjectParent; + +class RemoteQuotaObjectParentTracker : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR( + NS_DOM_QUOTA_REMOTEQUOTAOBJECTPARENTTRACKER_IID) + + virtual void RegisterRemoteQuotaObjectParent( + NotNull<RemoteQuotaObjectParent*> aActor) = 0; + + virtual void UnregisterRemoteQuotaObjectParent( + NotNull<RemoteQuotaObjectParent*> aActor) = 0; + + protected: + virtual ~RemoteQuotaObjectParentTracker() = default; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR( + RemoteQuotaObjectParentTracker, + MOZILLA_DOM_QUOTA_REMOTEQUOTAOBJECTPARENTTRACKER_IID) + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_REMOTEQUOTAOBJECTPARENTTRACKER_H_ diff --git a/dom/quota/RemoveParen.h b/dom/quota/RemoveParen.h new file mode 100644 index 0000000000..4dbc9f129b --- /dev/null +++ b/dom/quota/RemoveParen.h @@ -0,0 +1,18 @@ +/* -*- 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_QUOTA_REMOVEPAREN_H_ +#define DOM_QUOTA_REMOVEPAREN_H_ + +// See +// https://stackoverflow.com/questions/24481810/how-to-remove-the-enclosing-parentheses-with-macro +#define MOZ_REMOVE_PAREN(X) MOZ_REMOVE_PAREN_HELPER2(MOZ_REMOVE_PAREN_HELPER X) +#define MOZ_REMOVE_PAREN_HELPER(...) MOZ_REMOVE_PAREN_HELPER __VA_ARGS__ +#define MOZ_REMOVE_PAREN_HELPER2(...) MOZ_REMOVE_PAREN_HELPER3(__VA_ARGS__) +#define MOZ_REMOVE_PAREN_HELPER3(...) MOZ_REMOVE_PAREN_HELPER4_##__VA_ARGS__ +#define MOZ_REMOVE_PAREN_HELPER4_MOZ_REMOVE_PAREN_HELPER + +#endif // DOM_QUOTA_REMOVEPAREN_H_ diff --git a/dom/quota/ResultExtensions.h b/dom/quota/ResultExtensions.h new file mode 100644 index 0000000000..c19bf92661 --- /dev/null +++ b/dom/quota/ResultExtensions.h @@ -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/. */ + +#ifndef DOM_QUOTA_QMRESULTINLINES_H_ +#define DOM_QUOTA_QMRESULTINLINES_H_ + +#include "nsError.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/dom/QMResult.h" +#include "mozilla/dom/quota/Config.h" +#include "mozilla/dom/quota/RemoveParen.h" + +#ifdef QM_ERROR_STACKS_ENABLED +# include "mozilla/ResultVariant.h" +#endif + +namespace mozilla { + +// Allow bool errors to automatically convert to bool values, so MOZ_TRY/QM_TRY +// can be used in bool returning methods with Result<T, bool> results. +template <> +class [[nodiscard]] GenericErrorResult<bool> { + bool mErrorValue; + + template <typename V, typename E2> + friend class Result; + + public: + explicit GenericErrorResult(bool aErrorValue) : mErrorValue(aErrorValue) { + MOZ_ASSERT(!aErrorValue); + } + + GenericErrorResult(bool aErrorValue, const ErrorPropagationTag&) + : GenericErrorResult(aErrorValue) {} + + MOZ_IMPLICIT operator bool() const { return mErrorValue; } +}; + +// Allow MOZ_TRY/QM_TRY to handle `bool` values. +template <typename E = nsresult> +inline Result<Ok, E> ToResult(bool aValue) { + if (aValue) { + return Ok(); + } + return Err(ResultTypeTraits<E>::From(NS_ERROR_FAILURE)); +} + +constexpr nsresult ToNSResult(nsresult aError) { return aError; } + +#ifdef QM_ERROR_STACKS_ENABLED + +inline nsresult ToNSResult(const QMResult& aError) { return aError.NSResult(); } + +// Allow QMResult errors to use existing stack id and to increase the frame id +// during error propagation. +template <> +class [[nodiscard]] GenericErrorResult<QMResult> { + QMResult mErrorValue; + + template <typename V, typename E2> + friend class Result; + + public: + explicit GenericErrorResult(const QMResult& aErrorValue) + : mErrorValue(aErrorValue) { + MOZ_ASSERT(NS_FAILED(aErrorValue.NSResult())); + } + + explicit GenericErrorResult(QMResult&& aErrorValue) + : mErrorValue(std::move(aErrorValue)) { + MOZ_ASSERT(NS_FAILED(aErrorValue.NSResult())); + } + + explicit GenericErrorResult(const QMResult& aErrorValue, + const ErrorPropagationTag&) + : GenericErrorResult(aErrorValue.Propagate()) {} + + explicit GenericErrorResult(QMResult&& aErrorValue, + const ErrorPropagationTag&) + : GenericErrorResult(aErrorValue.Propagate()) {} + + operator QMResult() const { return mErrorValue; } + + operator nsresult() const { return mErrorValue.NSResult(); } +}; + +template <> +struct ResultTypeTraits<QMResult> { + static QMResult From(nsresult aValue) { return ToQMResult(aValue); } + + static QMResult From(const QMResult& aValue) { return aValue; } + + static QMResult From(QMResult&& aValue) { return std::move(aValue); } +}; + +template <typename E> +inline Result<Ok, E> ToResult(const QMResult& aValue) { + if (NS_FAILED(aValue.NSResult())) { + return Err(ResultTypeTraits<E>::From(aValue)); + } + return Ok(); +} + +template <typename E> +inline Result<Ok, E> ToResult(QMResult&& aValue) { + if (NS_FAILED(aValue.NSResult())) { + return Err(ResultTypeTraits<E>::From(aValue)); + } + return Ok(); +} +#endif + +template <typename E = nsresult, typename V, typename E2> +inline Result<V, E> ToResultTransform(Result<V, E2>&& aValue) { + return std::forward<Result<V, E2>>(aValue).mapErr( + [](auto&& err) { return ResultTypeTraits<E>::From(err); }); +} + +// TODO: Maybe move this to mfbt/ResultExtensions.h +template <typename R, typename Func, typename... Args> +Result<R, nsresult> ToResultGet(const Func& aFunc, Args&&... aArgs) { + nsresult rv; + R res = aFunc(std::forward<Args>(aArgs)..., &rv); + if (NS_FAILED(rv)) { + return Err(rv); + } + return res; +} + +} // namespace mozilla + +// TODO: Maybe move this to mfbt/ResultExtensions.h +#define MOZ_TO_RESULT(expr) ToResult(expr) + +#define QM_TO_RESULT(expr) ToResult<QMResult>(expr) + +#define QM_TO_RESULT_TRANSFORM(value) ToResultTransform<QMResult>(value) + +#define MOZ_TO_RESULT_GET_TYPED(resultType, ...) \ + ::mozilla::ToResultGet<MOZ_REMOVE_PAREN(resultType)>(__VA_ARGS__) + +#define MOZ_TO_RESULT_INVOKE_TYPED(resultType, ...) \ + ::mozilla::ToResultInvoke<MOZ_REMOVE_PAREN(resultType)>(__VA_ARGS__) + +#define QM_TO_RESULT_INVOKE_MEMBER(obj, methodname, ...) \ + ::mozilla::ToResultInvokeMember<QMResult>( \ + (obj), &::mozilla::detail::DerefedType<decltype(obj)>::methodname, \ + ##__VA_ARGS__) + +#define QM_TO_RESULT_INVOKE_MEMBER_TYPED(resultType, obj, methodname, ...) \ + (::mozilla::ToResultInvoke<resultType, QMResult>( \ + ::std::mem_fn( \ + &::mozilla::detail::DerefedType<decltype(obj)>::methodname), \ + (obj), ##__VA_ARGS__)) + +#endif diff --git a/dom/quota/ScopedLogExtraInfo.cpp b/dom/quota/ScopedLogExtraInfo.cpp new file mode 100644 index 0000000000..c3ad5e04e1 --- /dev/null +++ b/dom/quota/ScopedLogExtraInfo.cpp @@ -0,0 +1,80 @@ +/* -*- 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 "ScopedLogExtraInfo.h" + +namespace mozilla::dom::quota { + +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED +MOZ_THREAD_LOCAL(const nsACString*) ScopedLogExtraInfo::sQueryValue; +MOZ_THREAD_LOCAL(const nsACString*) ScopedLogExtraInfo::sContextValue; + +/* static */ +auto ScopedLogExtraInfo::FindSlot(const char* aTag) { + // XXX For now, don't use a real map but just allow the known tag values. + + if (aTag == kTagQuery) { + return &sQueryValue; + } + + if (aTag == kTagContext) { + return &sContextValue; + } + + MOZ_CRASH("Unknown tag!"); +} + +ScopedLogExtraInfo::~ScopedLogExtraInfo() { + if (mTag) { + MOZ_ASSERT(&mCurrentValue == FindSlot(mTag)->get(), + "Bad scoping of ScopedLogExtraInfo, must not be interleaved!"); + + FindSlot(mTag)->set(mPreviousValue); + } +} + +ScopedLogExtraInfo::ScopedLogExtraInfo(ScopedLogExtraInfo&& aOther) + : mTag(aOther.mTag), + mPreviousValue(aOther.mPreviousValue), + mCurrentValue(std::move(aOther.mCurrentValue)) { + aOther.mTag = nullptr; + FindSlot(mTag)->set(&mCurrentValue); +} + +/* static */ ScopedLogExtraInfo::ScopedLogExtraInfoMap +ScopedLogExtraInfo::GetExtraInfoMap() { + // This could be done in a cheaper way, but this is never called on a hot + // path, so we anticipate using a real map inside here to make use simpler for + // the caller(s). + + ScopedLogExtraInfoMap map; + if (XRE_IsParentProcess()) { + if (sQueryValue.get()) { + map.emplace(kTagQuery, sQueryValue.get()); + } + + if (sContextValue.get()) { + map.emplace(kTagContext, sContextValue.get()); + } + } + return map; +} + +/* static */ void ScopedLogExtraInfo::Initialize() { + MOZ_ALWAYS_TRUE(sQueryValue.init()); + MOZ_ALWAYS_TRUE(sContextValue.init()); +} + +void ScopedLogExtraInfo::AddInfo() { + auto* slot = FindSlot(mTag); + MOZ_ASSERT(slot); + mPreviousValue = slot->get(); + + slot->set(&mCurrentValue); +} +#endif + +} // namespace mozilla::dom::quota diff --git a/dom/quota/ScopedLogExtraInfo.h b/dom/quota/ScopedLogExtraInfo.h new file mode 100644 index 0000000000..d8412ae5b1 --- /dev/null +++ b/dom/quota/ScopedLogExtraInfo.h @@ -0,0 +1,75 @@ +/* -*- 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_QUOTA_SCOPEDLOGEXTRAINFO_H_ +#define DOM_QUOTA_SCOPEDLOGEXTRAINFO_H_ + +#include "mozilla/dom/quota/Config.h" + +#include <map> +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/ThreadLocal.h" +#include "nsString.h" +#include "nsXULAppAPI.h" + +namespace mozilla::dom::quota { + +struct MOZ_STACK_CLASS ScopedLogExtraInfo { + static constexpr const char kTagQuery[] = "query"; + static constexpr const char kTagContext[] = "context"; + +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + private: + static auto FindSlot(const char* aTag); + + public: + template <size_t N> + ScopedLogExtraInfo(const char (&aTag)[N], const nsACString& aExtraInfo) + : mTag{aTag}, mCurrentValue{aExtraInfo} { + // Initialize is currently only called in the parent process, we could call + // it directly from nsLayoutStatics::Initialize in the content process to + // allow use of ScopedLogExtraInfo in that too. The check in GetExtraInfoMap + // must be removed then. + MOZ_ASSERT(XRE_IsParentProcess()); + + AddInfo(); + } + + ~ScopedLogExtraInfo(); + + ScopedLogExtraInfo(ScopedLogExtraInfo&& aOther); + ScopedLogExtraInfo& operator=(ScopedLogExtraInfo&& aOther) = delete; + + ScopedLogExtraInfo(const ScopedLogExtraInfo&) = delete; + ScopedLogExtraInfo& operator=(const ScopedLogExtraInfo&) = delete; + + using ScopedLogExtraInfoMap = std::map<const char*, const nsACString*>; + static ScopedLogExtraInfoMap GetExtraInfoMap(); + + static void Initialize(); + + private: + const char* mTag; + const nsACString* mPreviousValue; + nsCString mCurrentValue; + + static MOZ_THREAD_LOCAL(const nsACString*) sQueryValue; + static MOZ_THREAD_LOCAL(const nsACString*) sContextValue; + + void AddInfo(); +#else + template <size_t N> + ScopedLogExtraInfo(const char (&aTag)[N], const nsACString& aExtraInfo) {} + + // user-defined to silence unused variable warnings + ~ScopedLogExtraInfo() {} +#endif +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_SCOPEDLOGEXTRAINFO_H_ diff --git a/dom/quota/SerializationHelpers.h b/dom/quota/SerializationHelpers.h new file mode 100644 index 0000000000..e63e3ef67d --- /dev/null +++ b/dom/quota/SerializationHelpers.h @@ -0,0 +1,79 @@ +/* -*- 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_quota_SerializationHelpers_h +#define mozilla_dom_quota_SerializationHelpers_h + +#include "ipc/EnumSerializer.h" +#include "ipc/IPCMessageUtils.h" + +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/CommonMetadata.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/OriginAttributes.h" + +namespace IPC { + +template <> +struct ParamTraits<mozilla::dom::quota::PersistenceType> + : public ContiguousEnumSerializer< + mozilla::dom::quota::PersistenceType, + mozilla::dom::quota::PERSISTENCE_TYPE_PERSISTENT, + mozilla::dom::quota::PERSISTENCE_TYPE_INVALID> {}; + +template <> +struct ParamTraits<mozilla::dom::quota::Client::Type> + : public ContiguousEnumSerializer<mozilla::dom::quota::Client::Type, + mozilla::dom::quota::Client::IDB, + mozilla::dom::quota::Client::TYPE_MAX> {}; + +template <> +struct ParamTraits<mozilla::dom::quota::FullOriginMetadata> { + using ParamType = mozilla::dom::quota::FullOriginMetadata; + + static void Write(MessageWriter* aWriter, const ParamType& aParam) { + WriteParam(aWriter, aParam.mSuffix); + WriteParam(aWriter, aParam.mGroup); + WriteParam(aWriter, aParam.mOrigin); + WriteParam(aWriter, aParam.mPersistenceType); + WriteParam(aWriter, aParam.mPersisted); + WriteParam(aWriter, aParam.mLastAccessTime); + } + + static bool Read(MessageReader* aReader, ParamType* aResult) { + return ReadParam(aReader, &aResult->mSuffix) && + ReadParam(aReader, &aResult->mGroup) && + ReadParam(aReader, &aResult->mOrigin) && + ReadParam(aReader, &aResult->mPersistenceType) && + ReadParam(aReader, &aResult->mPersisted) && + ReadParam(aReader, &aResult->mLastAccessTime); + } +}; + +template <> +struct ParamTraits<mozilla::OriginAttributesPattern> { + typedef mozilla::OriginAttributesPattern paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mFirstPartyDomain); + WriteParam(aWriter, aParam.mInIsolatedMozBrowser); + WriteParam(aWriter, aParam.mPrivateBrowsingId); + WriteParam(aWriter, aParam.mUserContextId); + WriteParam(aWriter, aParam.mGeckoViewSessionContextId); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &aResult->mFirstPartyDomain) && + ReadParam(aReader, &aResult->mInIsolatedMozBrowser) && + ReadParam(aReader, &aResult->mPrivateBrowsingId) && + ReadParam(aReader, &aResult->mUserContextId) && + ReadParam(aReader, &aResult->mGeckoViewSessionContextId); + } +}; + +} // namespace IPC + +#endif // mozilla_dom_quota_SerializationHelpers_h diff --git a/dom/quota/StorageHelpers.cpp b/dom/quota/StorageHelpers.cpp new file mode 100644 index 0000000000..84cd806297 --- /dev/null +++ b/dom/quota/StorageHelpers.cpp @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/quota/StorageHelpers.h" + +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" + +namespace mozilla::dom::quota { + +AutoDatabaseAttacher::AutoDatabaseAttacher( + nsCOMPtr<mozIStorageConnection> aConnection, + nsCOMPtr<nsIFile> aDatabaseFile, const nsLiteralCString& aSchemaName) + : mConnection(std::move(aConnection)), + mDatabaseFile(std::move(aDatabaseFile)), + mSchemaName(aSchemaName), + mAttached(false) {} + +AutoDatabaseAttacher::~AutoDatabaseAttacher() { + if (mAttached) { + QM_WARNONLY_TRY(MOZ_TO_RESULT(Detach())); + } +} + +nsresult AutoDatabaseAttacher::Attach() { + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mDatabaseFile); + MOZ_ASSERT(!mAttached); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(mDatabaseFile, Exists)); + + MOZ_ASSERT(exists); + } +#endif + + QM_TRY_INSPECT(const auto& path, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, mDatabaseFile, GetPath)); + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, mConnection, CreateStatement, + "ATTACH DATABASE :path AS "_ns + mSchemaName + ";"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindStringByName("path"_ns, path))); + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + + mAttached = true; + + return NS_OK; +} + +nsresult AutoDatabaseAttacher::Detach() { + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mAttached); + + QM_TRY(MOZ_TO_RESULT( + mConnection->ExecuteSimpleSQL("DETACH DATABASE "_ns + mSchemaName))); + + mAttached = false; + return NS_OK; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/StorageHelpers.h b/dom/quota/StorageHelpers.h new file mode 100644 index 0000000000..f1830db360 --- /dev/null +++ b/dom/quota/StorageHelpers.h @@ -0,0 +1,54 @@ +/* -*- 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_QUOTA_QUOTADATABASEHELPER_H +#define DOM_QUOTA_QUOTADATABASEHELPER_H + +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozIStorageConnection.h" +#include "nsCOMPtr.h" +#include "nsString.h" + +namespace mozilla::dom::quota { + +/** + * This class provides a RAII wrap of attaching and detaching database + * in a given C++ scope. It is guaranteed that an attached database will + * be detached even if you have an exception or return early. + * + * @param aConnection + * The connection to attach a database to. + * @param aDatabaseFile + * The database file to attach. + * @param aSchemaName + * The schema-name. Can be any string literal which is supported by the + * underlying database. For more details about schema-name, see + * https://www.sqlite.org/lang_attach.html + */ +class MOZ_STACK_CLASS AutoDatabaseAttacher final { + public: + explicit AutoDatabaseAttacher(nsCOMPtr<mozIStorageConnection> aConnection, + nsCOMPtr<nsIFile> aDatabaseFile, + const nsLiteralCString& aSchemaName); + + ~AutoDatabaseAttacher(); + + AutoDatabaseAttacher() = delete; + + [[nodiscard]] nsresult Attach(); + + [[nodiscard]] nsresult Detach(); + + private: + nsCOMPtr<mozIStorageConnection> mConnection; + nsCOMPtr<nsIFile> mDatabaseFile; + const nsLiteralCString mSchemaName; + bool mAttached; +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_QUOTADATABASEHELPER_H diff --git a/dom/quota/StorageManager.cpp b/dom/quota/StorageManager.cpp new file mode 100644 index 0000000000..bf05265bc0 --- /dev/null +++ b/dom/quota/StorageManager.cpp @@ -0,0 +1,808 @@ +/* -*- 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 "StorageManager.h" +#include "fs/FileSystemRequestHandler.h" + +#include <cstdint> +#include <cstdlib> +#include <utility> +#include "ErrorList.h" +#include "fs/FileSystemRequestHandler.h" +#include "MainThreadUtils.h" +#include "js/CallArgs.h" +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Maybe.h" +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryScalarEnums.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/FileSystemManager.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseWorkerProxy.h" +#include "mozilla/dom/StorageManagerBinding.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerStatus.h" +#include "mozilla/dom/quota/QuotaManagerService.h" +#include "nsContentPermissionHelper.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIGlobalObject.h" +#include "nsIPrincipal.h" +#include "nsIQuotaCallbacks.h" +#include "nsIQuotaManagerService.h" +#include "nsIQuotaRequests.h" +#include "nsIQuotaResults.h" +#include "nsIVariant.h" +#include "nsLiteralString.h" +#include "nsPIDOMWindow.h" +#include "nsString.h" +#include "nsStringFlags.h" +#include "nsTLiteralString.h" +#include "nscore.h" + +class JSObject; +struct JSContext; +struct nsID; + +namespace mozilla { +class Runnable; +} + +using namespace mozilla::dom::quota; + +namespace mozilla::dom { + +namespace { + +// This class is used to get quota usage, request persist and check persisted +// status callbacks. +class RequestResolver final : public nsIQuotaCallback { + public: + enum Type { Estimate, Persist, Persisted }; + + private: + class FinishWorkerRunnable; + + // If this resolver was created for a window then mPromise must be non-null. + // Otherwise mProxy must be non-null. + RefPtr<Promise> mPromise; + RefPtr<PromiseWorkerProxy> mProxy; + + nsresult mResultCode; + StorageEstimate mStorageEstimate; + const Type mType; + bool mPersisted; + + public: + RequestResolver(Type aType, Promise* aPromise) + : mPromise(aPromise), + mResultCode(NS_OK), + mType(aType), + mPersisted(false) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPromise); + } + + RequestResolver(Type aType, PromiseWorkerProxy* aProxy) + : mProxy(aProxy), mResultCode(NS_OK), mType(aType), mPersisted(false) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aProxy); + } + + void ResolveOrReject(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIQUOTACALLBACK + + private: + ~RequestResolver() = default; + + nsresult GetStorageEstimate(nsIVariant* aResult); + + nsresult GetPersisted(nsIVariant* aResult); + + nsresult OnCompleteInternal(nsIQuotaRequest* aRequest); + + nsresult Finish(); +}; + +// This class is used to return promise on worker thread. +class RequestResolver::FinishWorkerRunnable final : public WorkerRunnable { + RefPtr<RequestResolver> mResolver; + + public: + explicit FinishWorkerRunnable(RequestResolver* aResolver) + : WorkerRunnable(aResolver->mProxy->GetWorkerPrivate()), + mResolver(aResolver) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aResolver); + } + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; +}; + +class EstimateWorkerMainThreadRunnable final : public WorkerMainThreadRunnable { + RefPtr<PromiseWorkerProxy> mProxy; + + public: + EstimateWorkerMainThreadRunnable(WorkerPrivate* aWorkerPrivate, + PromiseWorkerProxy* aProxy) + : WorkerMainThreadRunnable(aWorkerPrivate, + "StorageManager :: Estimate"_ns), + mProxy(aProxy) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aProxy); + } + + bool MainThreadRun() override; +}; + +class PersistedWorkerMainThreadRunnable final + : public WorkerMainThreadRunnable { + RefPtr<PromiseWorkerProxy> mProxy; + + public: + PersistedWorkerMainThreadRunnable(WorkerPrivate* aWorkerPrivate, + PromiseWorkerProxy* aProxy) + : WorkerMainThreadRunnable(aWorkerPrivate, + "StorageManager :: Persisted"_ns), + mProxy(aProxy) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aProxy); + } + + bool MainThreadRun() override; +}; + +/******************************************************************************* + * PersistentStoragePermissionRequest + ******************************************************************************/ + +class PersistentStoragePermissionRequest final + : public ContentPermissionRequestBase { + RefPtr<Promise> mPromise; + + public: + PersistentStoragePermissionRequest(nsIPrincipal* aPrincipal, + nsPIDOMWindowInner* aWindow, + Promise* aPromise) + : ContentPermissionRequestBase(aPrincipal, aWindow, + "dom.storageManager"_ns, + "persistent-storage"_ns), + mPromise(aPromise) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aPromise); + } + + nsresult Start(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PersistentStoragePermissionRequest, + ContentPermissionRequestBase) + + // nsIContentPermissionRequest + NS_IMETHOD Cancel(void) override; + NS_IMETHOD Allow(JS::Handle<JS::Value> choices) override; + + private: + ~PersistentStoragePermissionRequest() = default; +}; + +nsresult Estimate(nsIPrincipal* aPrincipal, nsIQuotaCallback* aCallback, + nsIQuotaRequest** aRequest) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aCallback); + MOZ_ASSERT(aRequest); + + // Firefox and Quota Manager have always used the schemeless origin group + // (https://storage.spec.whatwg.org/#schemeless-origin-group) for quota limit + // purposes. This has been to prevent a site/eTLD+1 from claiming more than + // its fair share of storage through the use of sub-domains. Because the limit + // is at the group level and the usage needs to make sense in the context of + // that limit, we also expose the group usage. Bug 1374970 reflects this + // reality and bug 1305665 tracks our plan to eliminate our use of groups for + // this. + + nsCOMPtr<nsIQuotaManagerService> qms = QuotaManagerService::GetOrCreate(); + if (NS_WARN_IF(!qms)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIQuotaRequest> request; + nsresult rv = qms->Estimate(aPrincipal, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ALWAYS_SUCCEEDS(request->SetCallback(aCallback)); + + request.forget(aRequest); + return NS_OK; +}; + +nsresult Persisted(nsIPrincipal* aPrincipal, nsIQuotaCallback* aCallback, + nsIQuotaRequest** aRequest) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aCallback); + MOZ_ASSERT(aRequest); + + nsCOMPtr<nsIQuotaManagerService> qms = QuotaManagerService::GetOrCreate(); + if (NS_WARN_IF(!qms)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIQuotaRequest> request; + nsresult rv = qms->Persisted(aPrincipal, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // All the methods in nsIQuotaManagerService shouldn't synchronously fire + // any callbacks when they are being executed. Even when a result is ready, + // a new runnable should be dispatched to current thread to fire the callback + // asynchronously. It's safe to set the callback after we call Persisted(). + MOZ_ALWAYS_SUCCEEDS(request->SetCallback(aCallback)); + + request.forget(aRequest); + + return NS_OK; +}; + +already_AddRefed<Promise> ExecuteOpOnMainOrWorkerThread( + nsIGlobalObject* aGlobal, RequestResolver::Type aType, ErrorResult& aRv) { + MOZ_ASSERT(aGlobal); + MOZ_ASSERT_IF(aType == RequestResolver::Type::Persist, NS_IsMainThread()); + + RefPtr<Promise> promise = Promise::Create(aGlobal, aRv); + if (NS_WARN_IF(!promise)) { + return nullptr; + } + + if (NS_IsMainThread()) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr<Document> doc = window->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + MOZ_ASSERT(principal); + + // Storage Standard 7. API + // If origin is an opaque origin, then reject promise with a TypeError. + if (principal->GetIsNullPrincipal()) { + switch (aType) { + case RequestResolver::Type::Persisted: + promise->MaybeRejectWithTypeError( + "persisted() called for opaque origin"); + break; + case RequestResolver::Type::Persist: + promise->MaybeRejectWithTypeError( + "persist() called for opaque origin"); + break; + case RequestResolver::Type::Estimate: + promise->MaybeRejectWithTypeError( + "estimate() called for opaque origin"); + break; + } + + return promise.forget(); + } + + switch (aType) { + case RequestResolver::Type::Persisted: { + RefPtr<RequestResolver> resolver = + new RequestResolver(RequestResolver::Type::Persisted, promise); + + RefPtr<nsIQuotaRequest> request; + aRv = Persisted(principal, resolver, getter_AddRefs(request)); + + break; + } + + case RequestResolver::Type::Persist: { + RefPtr<PersistentStoragePermissionRequest> request = + new PersistentStoragePermissionRequest(principal, window, promise); + + // In private browsing mode, no permission prompt. + if (nsContentUtils::IsInPrivateBrowsing(doc)) { + aRv = request->Cancel(); + } else if (!request->CheckPermissionDelegate()) { + aRv = request->Cancel(); + } else { + aRv = request->Start(); + } + + break; + } + + case RequestResolver::Type::Estimate: { + RefPtr<RequestResolver> resolver = + new RequestResolver(RequestResolver::Type::Estimate, promise); + + RefPtr<nsIQuotaRequest> request; + aRv = Estimate(principal, resolver, getter_AddRefs(request)); + + break; + } + + default: + MOZ_CRASH("Invalid aRequest type!"); + } + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return promise.forget(); + } + + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + RefPtr<PromiseWorkerProxy> promiseProxy = + PromiseWorkerProxy::Create(workerPrivate, promise); + if (NS_WARN_IF(!promiseProxy)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + switch (aType) { + case RequestResolver::Type::Estimate: { + RefPtr<EstimateWorkerMainThreadRunnable> runnnable = + new EstimateWorkerMainThreadRunnable(promiseProxy->GetWorkerPrivate(), + promiseProxy); + runnnable->Dispatch(Canceling, aRv); + + break; + } + + case RequestResolver::Type::Persisted: { + RefPtr<PersistedWorkerMainThreadRunnable> runnnable = + new PersistedWorkerMainThreadRunnable( + promiseProxy->GetWorkerPrivate(), promiseProxy); + runnnable->Dispatch(Canceling, aRv); + + break; + } + + default: + MOZ_CRASH("Invalid aRequest type"); + } + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return promise.forget(); +}; + +} // namespace + +/******************************************************************************* + * Local class implementations + ******************************************************************************/ + +void RequestResolver::ResolveOrReject() { + class MOZ_STACK_CLASS AutoCleanup final { + RefPtr<PromiseWorkerProxy> mProxy; + + public: + explicit AutoCleanup(PromiseWorkerProxy* aProxy) : mProxy(aProxy) { + MOZ_ASSERT(aProxy); + } + + ~AutoCleanup() { + MOZ_ASSERT(mProxy); + + mProxy->CleanUp(); + } + }; + + RefPtr<Promise> promise; + Maybe<AutoCleanup> autoCleanup; + + if (mPromise) { + promise = mPromise; + } else { + MOZ_ASSERT(mProxy); + + promise = mProxy->WorkerPromise(); + + // Only clean up for worker case. + autoCleanup.emplace(mProxy); + } + + MOZ_ASSERT(promise); + + if (mType == Type::Estimate) { + if (NS_SUCCEEDED(mResultCode)) { + promise->MaybeResolve(mStorageEstimate); + } else { + promise->MaybeRejectWithTypeError( + "Internal error while estimating storage usage"); + } + + return; + } + + MOZ_ASSERT(mType == Type::Persist || mType == Type::Persisted); + + if (NS_SUCCEEDED(mResultCode)) { + promise->MaybeResolve(mPersisted); + } else { + promise->MaybeResolve(false); + } +} + +NS_IMPL_ISUPPORTS(RequestResolver, nsIQuotaCallback) + +nsresult RequestResolver::GetStorageEstimate(nsIVariant* aResult) { + MOZ_ASSERT(aResult); + MOZ_ASSERT(mType == Type::Estimate); + + MOZ_ASSERT(aResult->GetDataType() == nsIDataType::VTYPE_INTERFACE_IS); + + nsID* iid; + nsCOMPtr<nsISupports> supports; + nsresult rv = aResult->GetAsInterface(&iid, getter_AddRefs(supports)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + free(iid); + + nsCOMPtr<nsIQuotaEstimateResult> estimateResult = do_QueryInterface(supports); + MOZ_ASSERT(estimateResult); + + MOZ_ALWAYS_SUCCEEDS( + estimateResult->GetUsage(&mStorageEstimate.mUsage.Construct())); + + MOZ_ALWAYS_SUCCEEDS( + estimateResult->GetLimit(&mStorageEstimate.mQuota.Construct())); + + return NS_OK; +} + +nsresult RequestResolver::GetPersisted(nsIVariant* aResult) { + MOZ_ASSERT(aResult); + MOZ_ASSERT(mType == Type::Persist || mType == Type::Persisted); + +#ifdef DEBUG + uint16_t dataType = aResult->GetDataType(); +#endif + + if (mType == Type::Persist) { + MOZ_ASSERT(dataType == nsIDataType::VTYPE_VOID); + + mPersisted = true; + return NS_OK; + } + + MOZ_ASSERT(dataType == nsIDataType::VTYPE_BOOL); + + bool persisted; + nsresult rv = aResult->GetAsBool(&persisted); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mPersisted = persisted; + return NS_OK; +} + +nsresult RequestResolver::OnCompleteInternal(nsIQuotaRequest* aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequest); + + nsresult resultCode; + nsresult rv = aRequest->GetResultCode(&resultCode); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_FAILED(resultCode)) { + return resultCode; + } + + nsCOMPtr<nsIVariant> result; + rv = aRequest->GetResult(getter_AddRefs(result)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mType == Type::Estimate) { + rv = GetStorageEstimate(result); + } else { + MOZ_ASSERT(mType == Type::Persist || mType == Type::Persisted); + + rv = GetPersisted(result); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult RequestResolver::Finish() { + // In a main thread request. + if (!mProxy) { + MOZ_ASSERT(mPromise); + + ResolveOrReject(); + return NS_OK; + } + + { + // In a worker thread request. + MutexAutoLock lock(mProxy->Lock()); + + if (NS_WARN_IF(mProxy->CleanedUp())) { + return NS_ERROR_FAILURE; + } + + RefPtr<FinishWorkerRunnable> runnable = new FinishWorkerRunnable(this); + if (NS_WARN_IF(!runnable->Dispatch())) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +RequestResolver::OnComplete(nsIQuotaRequest* aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequest); + + mResultCode = OnCompleteInternal(aRequest); + + nsresult rv = Finish(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +bool RequestResolver::FinishWorkerRunnable::WorkerRun( + JSContext* aCx, WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + MOZ_ASSERT(mResolver); + mResolver->ResolveOrReject(); + + return true; +} + +bool EstimateWorkerMainThreadRunnable::MainThreadRun() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIPrincipal> principal; + + { + MutexAutoLock lock(mProxy->Lock()); + if (mProxy->CleanedUp()) { + return true; + } + principal = mProxy->GetWorkerPrivate()->GetPrincipal(); + } + + MOZ_ASSERT(principal); + + RefPtr<RequestResolver> resolver = + new RequestResolver(RequestResolver::Type::Estimate, mProxy); + + RefPtr<nsIQuotaRequest> request; + nsresult rv = Estimate(principal, resolver, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return true; +} + +bool PersistedWorkerMainThreadRunnable::MainThreadRun() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIPrincipal> principal; + + { + MutexAutoLock lock(mProxy->Lock()); + if (mProxy->CleanedUp()) { + return true; + } + principal = mProxy->GetWorkerPrivate()->GetPrincipal(); + } + + MOZ_ASSERT(principal); + + RefPtr<RequestResolver> resolver = + new RequestResolver(RequestResolver::Type::Persisted, mProxy); + + RefPtr<nsIQuotaRequest> request; + nsresult rv = Persisted(principal, resolver, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return true; +} + +nsresult PersistentStoragePermissionRequest::Start() { + MOZ_ASSERT(NS_IsMainThread()); + + PromptResult pr; +#ifdef MOZ_WIDGET_ANDROID + // on Android calling `ShowPrompt` here calls + // `nsContentPermissionUtils::AskPermission` once, and a response of + // `PromptResult::Pending` calls it again. This results in multiple requests + // for storage access, so we check the prompt prefs only to ensure we only + // request it once. + pr = CheckPromptPrefs(); +#else + nsresult rv = ShowPrompt(pr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } +#endif + if (pr == PromptResult::Granted) { + return Allow(JS::UndefinedHandleValue); + } + if (pr == PromptResult::Denied) { + return Cancel(); + } + + return nsContentPermissionUtils::AskPermission(this, mWindow); +} + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0( + PersistentStoragePermissionRequest, ContentPermissionRequestBase) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(PersistentStoragePermissionRequest, + ContentPermissionRequestBase, mPromise) + +NS_IMETHODIMP +PersistentStoragePermissionRequest::Cancel() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPromise); + + RefPtr<RequestResolver> resolver = + new RequestResolver(RequestResolver::Type::Persisted, mPromise); + + RefPtr<nsIQuotaRequest> request; + + return Persisted(mPrincipal, resolver, getter_AddRefs(request)); +} + +NS_IMETHODIMP +PersistentStoragePermissionRequest::Allow(JS::Handle<JS::Value> aChoices) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<RequestResolver> resolver = + new RequestResolver(RequestResolver::Type::Persist, mPromise); + + nsCOMPtr<nsIQuotaManagerService> qms = QuotaManagerService::GetOrCreate(); + if (NS_WARN_IF(!qms)) { + return NS_ERROR_FAILURE; + } + + RefPtr<nsIQuotaRequest> request; + + nsresult rv = qms->Persist(mPrincipal, getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ALWAYS_SUCCEEDS(request->SetCallback(resolver)); + + return NS_OK; +} + +/******************************************************************************* + * StorageManager + ******************************************************************************/ + +StorageManager::StorageManager(nsIGlobalObject* aGlobal) : mOwner(aGlobal) { + MOZ_ASSERT(aGlobal); +} + +StorageManager::~StorageManager() { Shutdown(); } + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StorageManager) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(StorageManager) +NS_IMPL_CYCLE_COLLECTING_RELEASE(StorageManager) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(StorageManager) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(StorageManager) + tmp->Shutdown(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(StorageManager) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFileSystemManager) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +void StorageManager::Shutdown() { + if (mFileSystemManager) { + mFileSystemManager->Shutdown(); + mFileSystemManager = nullptr; + } +} + +already_AddRefed<FileSystemManager> StorageManager::GetFileSystemManager() { + if (!mFileSystemManager) { + MOZ_ASSERT(mOwner); + + mFileSystemManager = MakeRefPtr<FileSystemManager>(mOwner, this); + } + + return do_AddRef(mFileSystemManager); +} + +// WebIDL Boilerplate + +JSObject* StorageManager::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return StorageManager_Binding::Wrap(aCx, this, aGivenProto); +} + +// WebIDL Interface + +already_AddRefed<Promise> StorageManager::Persisted(ErrorResult& aRv) { + MOZ_ASSERT(mOwner); + + return ExecuteOpOnMainOrWorkerThread(mOwner, RequestResolver::Type::Persisted, + aRv); +} + +already_AddRefed<Promise> StorageManager::Persist(ErrorResult& aRv) { + MOZ_ASSERT(mOwner); + + Telemetry::ScalarAdd(Telemetry::ScalarID::NAVIGATOR_STORAGE_PERSIST_COUNT, 1); + return ExecuteOpOnMainOrWorkerThread(mOwner, RequestResolver::Type::Persist, + aRv); +} + +already_AddRefed<Promise> StorageManager::Estimate(ErrorResult& aRv) { + MOZ_ASSERT(mOwner); + + Telemetry::ScalarAdd(Telemetry::ScalarID::NAVIGATOR_STORAGE_ESTIMATE_COUNT, + 1); + return ExecuteOpOnMainOrWorkerThread(mOwner, RequestResolver::Type::Estimate, + aRv); +} + +already_AddRefed<Promise> StorageManager::GetDirectory(ErrorResult& aRv) { + return RefPtr(GetFileSystemManager())->GetDirectory(aRv); +} + +} // namespace mozilla::dom diff --git a/dom/quota/StorageManager.h b/dom/quota/StorageManager.h new file mode 100644 index 0000000000..8622c1eb5d --- /dev/null +++ b/dom/quota/StorageManager.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_StorageManager_h +#define mozilla_dom_StorageManager_h + +#include "js/TypeDecls.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" + +class JSObject; +class nsIGlobalObject; +struct JSContext; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class FileSystemManager; +class Promise; +struct StorageEstimate; + +class StorageManager final : public nsISupports, public nsWrapperCache { + nsCOMPtr<nsIGlobalObject> mOwner; + + public: + explicit StorageManager(nsIGlobalObject* aGlobal); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(StorageManager) + + void Shutdown(); + + already_AddRefed<FileSystemManager> GetFileSystemManager(); + + // WebIDL Boilerplate + nsIGlobalObject* GetParentObject() const { return mOwner; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL Interface + already_AddRefed<Promise> Persisted(ErrorResult& aRv); + + already_AddRefed<Promise> Persist(ErrorResult& aRv); + + already_AddRefed<Promise> Estimate(ErrorResult& aRv); + + already_AddRefed<Promise> GetDirectory(ErrorResult& aRv); + + private: + ~StorageManager(); + + RefPtr<FileSystemManager> mFileSystemManager; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_StorageManager_h diff --git a/dom/quota/UsageInfo.h b/dom/quota/UsageInfo.h new file mode 100644 index 0000000000..7f7bf5f85a --- /dev/null +++ b/dom/quota/UsageInfo.h @@ -0,0 +1,103 @@ +/* -*- 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_quota_usageinfo_h__ +#define mozilla_dom_quota_usageinfo_h__ + +#include <cstdint> +#include <utility> +#include "mozilla/CheckedInt.h" +#include "mozilla/Maybe.h" + +namespace mozilla::dom::quota { + +enum struct UsageKind { Database, File }; + +namespace detail { +inline void AddCapped(Maybe<uint64_t>& aValue, const Maybe<uint64_t> aDelta) { + if (aDelta.isSome()) { + CheckedUint64 value = aValue.valueOr(0); + + value += aDelta.value(); + + aValue = Some(value.isValid() ? value.value() : UINT64_MAX); + } +} + +template <UsageKind Kind> +struct Usage { + explicit Usage(Maybe<uint64_t> aValue = Nothing{}) : mValue(aValue) {} + + Maybe<uint64_t> GetValue() const { return mValue; } + + Usage& operator+=(const Usage aDelta) { + AddCapped(mValue, aDelta.mValue); + + return *this; + } + + Usage operator+(const Usage aDelta) const { + Usage res = *this; + res += aDelta; + return res; + } + + private: + Maybe<uint64_t> mValue; +}; +} // namespace detail + +using DatabaseUsageType = detail::Usage<UsageKind::Database>; +using FileUsageType = detail::Usage<UsageKind::File>; + +class UsageInfo final { + public: + UsageInfo() = default; + + explicit UsageInfo(const DatabaseUsageType aUsage) : mDatabaseUsage(aUsage) {} + + explicit UsageInfo(const FileUsageType aUsage) : mFileUsage(aUsage) {} + + UsageInfo operator+(const UsageInfo& aUsageInfo) { + UsageInfo res = *this; + res += aUsageInfo; + return res; + } + + UsageInfo& operator+=(const UsageInfo& aUsageInfo) { + mDatabaseUsage += aUsageInfo.mDatabaseUsage; + mFileUsage += aUsageInfo.mFileUsage; + return *this; + } + + UsageInfo& operator+=(const DatabaseUsageType aUsage) { + mDatabaseUsage += aUsage; + return *this; + } + + UsageInfo& operator+=(const FileUsageType aUsage) { + mFileUsage += aUsage; + return *this; + } + + Maybe<uint64_t> DatabaseUsage() const { return mDatabaseUsage.GetValue(); } + + Maybe<uint64_t> FileUsage() const { return mFileUsage.GetValue(); } + + Maybe<uint64_t> TotalUsage() const { + Maybe<uint64_t> res = mDatabaseUsage.GetValue(); + detail::AddCapped(res, FileUsage()); + return res; + } + + private: + DatabaseUsageType mDatabaseUsage; + FileUsageType mFileUsage; +}; + +} // namespace mozilla::dom::quota + +#endif // mozilla_dom_quota_usageinfo_h__ diff --git a/dom/quota/components.conf b/dom/quota/components.conf new file mode 100644 index 0000000000..b55e861e0f --- /dev/null +++ b/dom/quota/components.conf @@ -0,0 +1,23 @@ +# -*- 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/. + +Classes = [ + { + 'cid': '{b6f2f870-b0bc-4a1a-9c40-02cc171adb5b}', + 'contract_ids': ['@mozilla.org/network/protocol;1?name=indexeddb'], + 'type': 'nsIndexedDBProtocolHandler', + 'headers': ['/dom/quota/nsIndexedDBProtocolHandler.h'], + 'protocol_config': { + 'scheme': 'indexeddb', + 'flags': [ + 'URI_STD', + 'URI_DANGEROUS_TO_LOAD', + 'URI_DOES_NOT_RETURN_DATA', + 'URI_NON_PERSISTABLE', + ], + }, + }, +] diff --git a/dom/quota/moz.build b/dom/quota/moz.build new file mode 100644 index 0000000000..b4416a4bed --- /dev/null +++ b/dom/quota/moz.build @@ -0,0 +1,120 @@ +# -*- 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: Quota Manager") + +DIRS += ["test"] + +XPIDL_SOURCES += [ + "nsIQuotaCallbacks.idl", + "nsIQuotaManagerService.idl", + "nsIQuotaRequests.idl", + "nsIQuotaResults.idl", +] + +XPIDL_MODULE = "dom_quota" + +EXPORTS.mozilla.dom += [ + "QMResult.h", + "StorageManager.h", +] + +EXPORTS.mozilla.dom.quota += [ + "ActorsParent.h", + "Assertions.h", + "AssertionsImpl.h", + "CachingDatabaseConnection.h", + "CheckedUnsafePtr.h", + "CipherStrategy.h", + "Client.h", + "ClientImpl.h", + "CommonMetadata.h", + "Config.h", + "DecryptingInputStream.h", + "DecryptingInputStream_impl.h", + "DirectoryLock.h", + "DummyCipherStrategy.h", + "EncryptedBlock.h", + "EncryptingOutputStream.h", + "EncryptingOutputStream_impl.h", + "FileStreams.h", + "FirstInitializationAttempts.h", + "FirstInitializationAttemptsImpl.h", + "ForwardDecls.h", + "InitializationTypes.h", + "IPCStreamCipherStrategy.h", + "NSSCipherStrategy.h", + "OriginScope.h", + "PersistenceType.h", + "QuotaCommon.h", + "QuotaManager.h", + "QuotaManagerImpl.h", + "QuotaManagerService.h", + "QuotaObject.h", + "RemoteQuotaObjectChild.h", + "RemoteQuotaObjectParent.h", + "RemoteQuotaObjectParentTracker.h", + "RemoveParen.h", + "ResultExtensions.h", + "ScopedLogExtraInfo.h", + "SerializationHelpers.h", + "StorageHelpers.h", + "UsageInfo.h", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +UNIFIED_SOURCES += [ + "ActorsChild.cpp", + "ActorsParent.cpp", + "CachingDatabaseConnection.cpp", + "CanonicalQuotaObject.cpp", + "Client.cpp", + "ClientUsageArray.cpp", + "DecryptingInputStream.cpp", + "DirectoryLockImpl.cpp", + "EncryptingOutputStream.cpp", + "FileStreams.cpp", + "GroupInfo.cpp", + "GroupInfoPair.cpp", + "InitializationTypes.cpp", + "nsIndexedDBProtocolHandler.cpp", + "NSSCipherStrategy.cpp", + "OriginInfo.cpp", + "PersistenceType.cpp", + "QMResult.cpp", + "QuotaCommon.cpp", + "QuotaManagerService.cpp", + "QuotaObject.cpp", + "QuotaRequests.cpp", + "QuotaResults.cpp", + "RemoteQuotaObject.cpp", + "RemoteQuotaObjectChild.cpp", + "RemoteQuotaObjectParent.cpp", + "ScopedLogExtraInfo.cpp", + "StorageHelpers.cpp", + "StorageManager.cpp", +] + +IPDL_SOURCES += [ + "IPCQuotaObject.ipdlh", + "PQuota.ipdl", + "PQuotaRequest.ipdl", + "PQuotaUsageRequest.ipdl", + "PRemoteQuotaObject.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/caps", + "/dom/fs/include", +] diff --git a/dom/quota/nsIQuotaCallbacks.idl b/dom/quota/nsIQuotaCallbacks.idl new file mode 100644 index 0000000000..7c53db20c0 --- /dev/null +++ b/dom/quota/nsIQuotaCallbacks.idl @@ -0,0 +1,22 @@ +/* -*- 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 nsIQuotaRequest; +interface nsIQuotaUsageRequest; + +[scriptable, function, uuid(c8a21a2a-17b9-4b63-ad95-e0fbcff5de18)] +interface nsIQuotaUsageCallback : nsISupports +{ + void onUsageResult(in nsIQuotaUsageRequest aRequest); +}; + +[scriptable, function, uuid(a08a28e2-5a74-4c84-8070-ed45a07eb013)] +interface nsIQuotaCallback : nsISupports +{ + void onComplete(in nsIQuotaRequest aRequest); +}; diff --git a/dom/quota/nsIQuotaManagerService.idl b/dom/quota/nsIQuotaManagerService.idl new file mode 100644 index 0000000000..9855283a5f --- /dev/null +++ b/dom/quota/nsIQuotaManagerService.idl @@ -0,0 +1,296 @@ +/* -*- 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; +interface nsIQuotaRequest; +interface nsIQuotaCallback; +interface nsIQuotaUsageCallback; +interface nsIQuotaUsageRequest; + +[scriptable, builtinclass, uuid(1b3d0a38-8151-4cf9-89fa-4f92c2ef0e7e)] +interface nsIQuotaManagerService : nsISupports +{ + /** + * Asynchronously retrieves storage name and returns it as a plain string. + * + * If the dom.quotaManager.testing preference is not true the call will be + * a no-op. + */ + [must_use] nsIQuotaRequest + storageName(); + + /** + * Check if storage is initialized. + * + * If the dom.quotaManager.testing preference is not true the call will be + * a no-op. + */ + [must_use] nsIQuotaRequest + storageInitialized(); + + /** + * Check if temporary storage is initialized. + * + * If the dom.quotaManager.testing preference is not true the call will be + * a no-op. + */ + [must_use] nsIQuotaRequest + temporaryStorageInitialized(); + + /** + * Initializes storage directory. This can be used in tests to verify + * upgrade methods. + * + * If the dom.quotaManager.testing preference is not true the call will be + * a no-op. + */ + [must_use] nsIQuotaRequest + init(); + + /** + * Initializes temporary storage. This can be used in tests to verify + * temporary storage initialization. + * + * If the dom.quotaManager.testing preference is not true the call will be + * a no-op. + */ + [must_use] nsIQuotaRequest + initTemporaryStorage(); + + /** + * Initializes persistent origin directory for the given origin. This can be + * used in tests to verify origin initialization. + * + * If the dom.quotaManager.testing preference is not true the call will be + * a no-op. + * + * @param aPrincipal + * A principal for the origin whose directory is to be initialized. + */ + [must_use] nsIQuotaRequest + initializePersistentOrigin(in nsIPrincipal aPrincipal); + + /** + * Initializes temporary origin directory for the given origin. This can be + * used in tests to verify origin initialization. + * + * If the dom.quotaManager.testing preference is not true the call will be + * a no-op. + * + * @param aPersistenceType + * A string that tells what persistence type of origin will be + * initialized (temporary or default). + * + * @param aPrincipal + * A principal for the origin whose directory is to be initialized. + */ + [must_use] nsIQuotaRequest + initializeTemporaryOrigin(in ACString aPersistenceType, + in nsIPrincipal aPrincipal); + + /** + * Gets full origin metadata cached in memory for the given persistence type + * and origin. + * + * NOTE: This operation may still be delayed by other operations on the QM + * I/O thread that are peforming I/O. + * + * @param aPersistenceType + * A string that tells what persistence type will be used for getting + * the metadata (either "temporary" or "default"). + * @param aPrincipal + * A principal that tells which origin will be used for getting the + * metadata. + */ + [must_use] nsIQuotaRequest + getFullOriginMetadata(in ACString aPersistenceType, + in nsIPrincipal aPrincipal); + + /** + * Schedules an asynchronous callback that will inspect all origins and + * return the total amount of disk space being used by storages for each + * origin separately. + * + * @param aCallback + * The callback that will be called when the usage is available. + * @param aGetAll + * An optional boolean to indicate inspection of all origins, + * including internal ones. + */ + [must_use] nsIQuotaUsageRequest + getUsage(in nsIQuotaUsageCallback aCallback, + [optional] in boolean aGetAll); + + /** + * Schedules an asynchronous callback that will return the total amount of + * disk space being used by storages for the given origin. + * + * @param aPrincipal + * A principal for the origin whose usage is being queried. + * @param aCallback + * The callback that will be called when the usage is available. + * @param aFromMemory + * An optional flag to indicate whether the cached usage should be + * obtained. The default value is false. Note that this operation may + * still be delayed by other operations on the QM I/O thread that are + * peforming I/O. + * Note: Origin usage here represents total usage of an origin. However, + * cached usage here represents only non-persistent usage of an origin. + */ + [must_use] nsIQuotaUsageRequest + getUsageForPrincipal(in nsIPrincipal aPrincipal, + in nsIQuotaUsageCallback aCallback, + [optional] in boolean aFromMemory); + + /** + * Asynchronously lists all origins and returns them as plain strings. + */ + [must_use] nsIQuotaRequest + listOrigins(); + + /** + * Removes all storages. The files may not be deleted immediately depending + * on prohibitive concurrent operations. + * Be careful, this removes *all* the data that has ever been stored! + * + * If the dom.quotaManager.testing preference is not true the call will be + * a no-op. + */ + [must_use] nsIQuotaRequest + clear(); + + /** + * Removes all storages stored for the given pattern. The files may not be + * deleted immediately depending on prohibitive concurrent operations. In + * terms of locks, it will get an exclusive multi directory lock for given + * pattern. For example, given pattern {"userContextId":1007} and set of 3 + * origins ["http://www.mozilla.org^userContextId=1007", + * "http://www.example.org^userContextId=1007", + * "http://www.example.org^userContextId=1008"], the method will only lock 2 + * origins ["http://www.mozilla.org^userContextId=1007", + * "http://www.example.org^userContextId=1007"]. + * + * @param aPattern + * A pattern for the origins whose storages are to be cleared. + * Currently this is expected to be a JSON representation of the + * OriginAttributesPatternDictionary defined in ChromeUtils.webidl. + */ + [must_use] nsIQuotaRequest + clearStoragesForOriginAttributesPattern(in AString aPattern); + + /** + * Removes all storages stored for the given principal. The files may not be + * deleted immediately depending on prohibitive concurrent operations. + * + * @param aPrincipal + * A principal for the origin whose storages are to be cleared. + * @param aPersistenceType + * An optional string that tells what persistence type of storages + * will be cleared. If omitted (or void), all persistence types will + * be cleared for the principal. If a single persistence type + * ("persistent", "temporary", or "default") is provided, then only + * that persistence directory will be considered. Note that + * "persistent" is different than being "persisted" via persist() and + * is only for chrome principals. See bug 1354500 for more info. + * In general, null is the right thing to pass here. + * @param aClientType + * An optional string that tells what client type of storages + * will be cleared. If omitted (or void), all client types will be + * cleared for the principal. If a single client type is provided + * from Client.h, then only that client's storage will be cleared. + * If you want to clear multiple client types (but not all), then you + * must call this method multiple times. + * @param aClearAll + * An optional boolean to indicate clearing all storages under the + * given origin. + */ + [must_use] nsIQuotaRequest + clearStoragesForPrincipal(in nsIPrincipal aPrincipal, + [optional] in ACString aPersistenceType, + [optional] in AString aClientType, + [optional] in boolean aClearAll); + + /** + * Resets quota and storage management. This can be used to force + * reinitialization of the temp storage, for example when the pref for + * overriding the temp storage limit has changed. + * Be carefull, this invalidates all live storages! + * + * If the dom.quotaManager.testing preference is not true the call will be + * a no-op. + */ + [must_use] nsIQuotaRequest + reset(); + + /** + * Resets all storages stored for the given principal. + * + * If the dom.quotaManager.testing preference is not true the call will be + * a no-op. + * + * @param aPrincipal + * A principal for the origin whose storages are to be reset. + * @param aPersistenceType + * An optional string that tells what persistence type of storages + * will be reset. If omitted (or void), all persistence types will + * be cleared for the principal. If a single persistence type + * ("persistent", "temporary", or "default") is provided, then only + * that persistence directory will be considered. Note that + * "persistent" is different than being "persisted" via persist() and + * is only for chrome principals. See bug 1354500 for more info. + * In general, null is the right thing to pass here. + * @param aClientType + * An optional string that tells what client type of storages + * will be reset. If omitted (or void), all client types will be + * cleared for the principal. If a single client type is provided + * from Client.h, then only that client's storage will be cleared. + * If you want to clear multiple client types (but not all), then you + * must call this method multiple times. + */ + [must_use] nsIQuotaRequest + resetStoragesForPrincipal(in nsIPrincipal aPrincipal, + [optional] in ACString aPersistenceType, + [optional] in AString aClientType); + + /** + * Check if given origin is persisted. + * + * @param aPrincipal + * A principal for the origin which we want to check. + */ + [must_use] nsIQuotaRequest + persisted(in nsIPrincipal aPrincipal); + + /** + * Persist given origin. + * + * @param aPrincipal + * A principal for the origin which we want to persist. + */ + [must_use] nsIQuotaRequest + persist(in nsIPrincipal aPrincipal); + + /** + * Given an origin, asynchronously calculate its group quota usage and quota + * limit. An origin's group is the set of all origins that share the same + * eTLD+1. This method is intended to be used for our implementation of the + * StorageManager.estimate() method. When we fix bug 1305665 and stop tracking + * quota limits on a group basis, this method will switch to operating on + * origins. Callers should strongly consider whether they want to be using + * getUsageForPrincipal() instead. + * + * This mechanism uses cached quota values and does not perform any I/O on its + * own, but it may be delayed by QuotaManager operations that do need to + * perform I/O on the QuotaManager I/O thread. + * + * @param aPrincipal + * A principal for the origin (group) which we want to estimate. + */ + [must_use] nsIQuotaRequest + estimate(in nsIPrincipal aPrincipal); +}; diff --git a/dom/quota/nsIQuotaRequests.idl b/dom/quota/nsIQuotaRequests.idl new file mode 100644 index 0000000000..8b05717e83 --- /dev/null +++ b/dom/quota/nsIQuotaRequests.idl @@ -0,0 +1,49 @@ +/* -*- 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; +interface nsIQuotaCallback; +interface nsIQuotaUsageCallback; +interface nsIVariant; + +[scriptable, uuid(9af54222-0407-48fd-a4ab-9457c986fc49)] +interface nsIQuotaRequestBase : nsISupports +{ + readonly attribute nsIPrincipal principal; + + [must_use] readonly attribute nsresult resultCode; + + [must_use] readonly attribute ACString resultName; +}; + +[scriptable, uuid(166e28e6-cf6d-4927-a6d7-b51bca9d3469)] +interface nsIQuotaUsageRequest : nsIQuotaRequestBase +{ + // The result can contain one of these types: + // array of nsIQuotaUsageResult + // nsIQuotaOriginUsageResult + [must_use] readonly attribute nsIVariant result; + + attribute nsIQuotaUsageCallback callback; + + [must_use] void + cancel(); +}; + +[scriptable, uuid(22890e3e-ff25-4372-9684-d901060e2f6c)] +interface nsIQuotaRequest : nsIQuotaRequestBase +{ + // The result can contain one of these types: + // void + // bool + // nsIQuotaEstimateResult + // array of strings + [must_use] readonly attribute nsIVariant result; + + attribute nsIQuotaCallback callback; +}; diff --git a/dom/quota/nsIQuotaResults.idl b/dom/quota/nsIQuotaResults.idl new file mode 100644 index 0000000000..bf9ab322b8 --- /dev/null +++ b/dom/quota/nsIQuotaResults.idl @@ -0,0 +1,51 @@ +/* -*- 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" + +[scriptable, function, uuid(4d8def75-014e-404d-bf30-e2f0Bfcf4d89)] +interface nsIQuotaFullOriginMetadataResult : nsISupports +{ + readonly attribute ACString suffix; + + readonly attribute ACString group; + + readonly attribute ACString origin; + + readonly attribute ACString persistenceType; + + readonly attribute boolean persisted; + + readonly attribute long long lastAccessTime; +}; + +[scriptable, function, uuid(d8c9328b-9aa8-4f5d-90e6-482de4a6d5b8)] +interface nsIQuotaUsageResult : nsISupports +{ + readonly attribute ACString origin; + + readonly attribute boolean persisted; + + readonly attribute unsigned long long usage; + + readonly attribute unsigned long long lastAccessed; +}; + +[scriptable, function, uuid(96df03d2-116a-493f-bb0b-118c212a6b32)] +interface nsIQuotaOriginUsageResult : nsISupports +{ + readonly attribute unsigned long long usage; + + readonly attribute unsigned long long fileUsage; +}; + +[scriptable, function, uuid(9827fc69-7ea9-48ef-b30d-2e2ae0451ec0)] +interface nsIQuotaEstimateResult : nsISupports +{ + readonly attribute unsigned long long usage; + + readonly attribute unsigned long long limit; +}; diff --git a/dom/quota/nsIndexedDBProtocolHandler.cpp b/dom/quota/nsIndexedDBProtocolHandler.cpp new file mode 100644 index 0000000000..10ca278098 --- /dev/null +++ b/dom/quota/nsIndexedDBProtocolHandler.cpp @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sts=2 sw=2 et cin: + * + * 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 "nsIndexedDBProtocolHandler.h" + +#include <cstdint> +#include "ErrorList.h" +#include "mozilla/Assertions.h" +#include "mozilla/MacroForEach.h" +#include "nsIWeakReference.h" +#include "nsStandardURL.h" +#include "nsStringFwd.h" +#include "nscore.h" + +using namespace mozilla::net; + +nsIndexedDBProtocolHandler::nsIndexedDBProtocolHandler() = default; + +nsIndexedDBProtocolHandler::~nsIndexedDBProtocolHandler() = default; + +NS_IMPL_ISUPPORTS(nsIndexedDBProtocolHandler, nsIProtocolHandler, + nsISupportsWeakReference) + +NS_IMETHODIMP nsIndexedDBProtocolHandler::GetScheme(nsACString& aScheme) { + aScheme.AssignLiteral("indexeddb"); + return NS_OK; +} + +NS_IMETHODIMP +nsIndexedDBProtocolHandler::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIChannel** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsIndexedDBProtocolHandler::AllowPort(int32_t aPort, const char* aScheme, + bool* _retval) { + *_retval = false; + return NS_OK; +} diff --git a/dom/quota/nsIndexedDBProtocolHandler.h b/dom/quota/nsIndexedDBProtocolHandler.h new file mode 100644 index 0000000000..00cc554cb4 --- /dev/null +++ b/dom/quota/nsIndexedDBProtocolHandler.h @@ -0,0 +1,26 @@ +/* -*- 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 nsIndexedDBProtocolHandler_h +#define nsIndexedDBProtocolHandler_h + +#include "nsIProtocolHandler.h" +#include "nsISupports.h" +#include "nsWeakReference.h" + +class nsIndexedDBProtocolHandler final : public nsIProtocolHandler, + public nsSupportsWeakReference { + public: + nsIndexedDBProtocolHandler(); + + private: + ~nsIndexedDBProtocolHandler(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER +}; + +#endif // nsIndexedDBProtocolHandler_h diff --git a/dom/quota/scripts/analyze_qm_failures.py b/dom/quota/scripts/analyze_qm_failures.py new file mode 100755 index 0000000000..f4afd64ab4 --- /dev/null +++ b/dom/quota/scripts/analyze_qm_failures.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# 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/. + +import getopt +import sys + +import fn_anchors +import stackanalysis +import utils + +""" +The analysis is based on stack frames of the following form: + +[ + { + "event_timeabs": 1617121013137, + "session_startabs": 1617120840000, + "build_id": "20210329095128", + "client_id": "0013a68f-9893-461a-93d4-2d7a2f85583f", + "session_id": "8cd37159-bd5c-481c-99ad-9eace9ea726a", + "seq": 1, + "context": "Initialization::TemporaryStorage", + "source_file": "dom/localstorage/ActorsParent.cpp", + "source_line": "1018", + "severity": "ERROR", + "result": "NS_ERROR_FILE_NOT_FOUND" + }, +... +] + +The location of the input file is expected to be found in the +last item of the list inside qmexecutions.json. +""" + + +def usage(): + print("analyze_qm_faiures.py -w <workdir=.>") + print("") + print("Analyzes the results from fetch_qm_failures.py's JSON file.") + print( + "Writes out several JSON results as files and a bugzilla markup table on stdout." + ) + print("-w <workdir>: Working directory, default is '.'") + sys.exit(2) + + +days = 1 +workdir = "." + +try: + opts, args = getopt.getopt(sys.argv[1:], "w:", ["workdir="]) + for opt, arg in opts: + if opt == "-w": + workdir = arg +except getopt.GetoptError: + usage() + +run = utils.getLastRunFromExecutionFile(workdir) +if "numrows" not in run: + print("No previous execution from fetch_qm_failures.py found.") + usage() +if run["numrows"] == 0: + print("The last execution yielded no result.") + +infile = run["rawfile"] + + +def getFname(prefix): + return "{}/{}_until_{}.json".format(workdir, prefix, run["lasteventtime"]) + + +# read rows from JSON +rows = utils.readJSONFile(getFname("qmrows")) +print("Found {} rows of data.".format(len(rows))) +rows = stackanalysis.sanitize(rows) + +# enrich rows with hg locations +buildids = stackanalysis.extractBuildIDs(rows) +utils.fetchBuildRevisions(buildids) +stackanalysis.constructHGLinks(buildids, rows) + +# transform rows to unique stacks +raw_stacks = stackanalysis.collectRawStacks(rows) +all_stacks = stackanalysis.mergeEqualStacks(raw_stacks) + +# enrich with function anchors +for stack in all_stacks: + for frame in stack["frames"]: + frame["anchor"] = "{}:{}".format( + frame["source_file"], fn_anchors.getFunctionName(frame["location"]) + ) + +# separate stacks for relevance +error_stacks = [] +warn_stacks = [] +info_stacks = [] +abort_stacks = [] +stackanalysis.filterStacksForPropagation( + all_stacks, error_stacks, warn_stacks, info_stacks, abort_stacks +) +run["errorfile"] = getFname("qmerrors") +utils.writeJSONFile(run["errorfile"], error_stacks) +run["warnfile"] = getFname("qmwarnings") +utils.writeJSONFile(run["warnfile"], warn_stacks) +run["infofile"] = getFname("qminfo") +utils.writeJSONFile(run["infofile"], info_stacks) +run["abortfile"] = getFname("qmabort") +utils.writeJSONFile(run["abortfile"], abort_stacks) +utils.updateLastRunToExecutionFile(workdir, run) + + +# print results to stdout +print("Found {} error stacks.".format(len(error_stacks))) +print("Found {} warning stacks.".format(len(warn_stacks))) +print("Found {} info stacks.".format(len(info_stacks))) +print("Found {} aborted stacks.".format(len(abort_stacks))) +print("") +print("Error stacks:") +print(stackanalysis.printStacks(error_stacks)) +print("") +print("Error stacks grouped by anchors:") +anchors = stackanalysis.groupStacksForAnchors(error_stacks) +anchornames = list(anchors.keys()) +for a in anchornames: + print(stackanalysis.printStacks(anchors[a]["stacks"])) + print("") +print("") +print("Warning stacks:") +print(stackanalysis.printStacks(warn_stacks)) +print("") +print("Info stacks:") +print(stackanalysis.printStacks(info_stacks)) +print("") +print("Aborted stacks:") +print(stackanalysis.printStacks(abort_stacks)) diff --git a/dom/quota/scripts/fetch_fn_names.sh b/dom/quota/scripts/fetch_fn_names.sh new file mode 100755 index 0000000000..6d3a3c4d23 --- /dev/null +++ b/dom/quota/scripts/fetch_fn_names.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# 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/. + +# This script assumes to have rust-code-analysis-cli in the path. +HG_URL=$1 +TEMPDIR=/tmp/fetch_fn_names_$BASHPID +TEMPSRC=$TEMPDIR/src +mkdir $TEMPDIR +echo "" > $TEMPDIR/empty.json +HG_URL=`echo $HG_URL | sed 's/annotate/raw-file/g'` +wget -q -O "$TEMPSRC" $HG_URL +rust-code-analysis-cli -m -O json -o "$TEMPDIR" -p "$TEMPSRC" +CONTENT=`cat $TEMPDIR/*.json` +rm -rf $TEMPDIR +echo $CONTENT diff --git a/dom/quota/scripts/fetch_qm_failures.py b/dom/quota/scripts/fetch_qm_failures.py new file mode 100755 index 0000000000..546b213582 --- /dev/null +++ b/dom/quota/scripts/fetch_qm_failures.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# 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/. + +import getopt +import sys + +import telemetry +import utils + +""" +The analysis is based on the following query: +https://sql.telemetry.mozilla.org/queries/78691/source?p_day=28&p_month=03&p_year=2021 + +SELECT UNIX_MILLIS(timestamp) AS submit_timeabs, + session_start_time, + submission_date, + build_id, + client_id, + session_id, + event_timestamp, + CAST(mozfun.map.get_key(event_map_values, "seq") AS INT64) AS seq, + mozfun.map.get_key(event_map_values, "context") AS context, + mozfun.map.get_key(event_map_values, "source_file") AS source_file, + mozfun.map.get_key(event_map_values, "source_line") AS source_line, + mozfun.map.get_key(event_map_values, "severity") AS severity, + mozfun.map.get_key(event_map_values, "result") AS result, +FROM telemetry.events +WHERE submission_date >= CAST('{{ year }}-{{ month }}-{{ day }}' AS DATE) + AND event_category='dom.quota.try' + AND build_id >= '{{ build }}' + AND UNIX_MILLIS(timestamp) > {{ last }} +ORDER BY submit_timeabs +LIMIT 600000 + +We fetch events in chronological order, as we want to keep track of where we already +arrived with our analysis. To accomplish this we write our runs into qmexecutions.json. + +[ + { + "workdir": ".", + "daysback": 1, + "numrows": 17377, + "lasteventtime": 1617303855145, + "rawfile": "./qmrows_until_1617303855145.json" + } +] + +lasteventtime is the highest value of event_timeabs we found in our data. + +analyze_qm_failures instead needs the rows to be ordered by +client_id, session_id, thread_id, submit_timeabs, seq +Thus we sort the rows accordingly before writing them. +""" + + +def usage(): + print( + "fetch_qm_faiures.py -k <apikey> -b <minimum build=20210329000000>" + "-d <days back=1> -l <last event time> -w <workdir=.>" + ) + print("") + print("Invokes the query 78691 and stores the result in a JSON file.") + print("-k <apikey>: Your personal telemetry API key (not the query key!).") + print("-d <daysback>: Number of days to go back. Default is 1.") + print("-b <minimum build>: The lowest build id we will fetch data for.") + print("-l <last event time>: Fetch only events after this. Default is 0.") + print("-w <workdir>: Working directory, default is '.'") + sys.exit(2) + + +days = 1 +lasteventtime = 0 +key = "undefined" +workdir = "." +minbuild = "20210329000000" + +try: + opts, args = getopt.getopt( + sys.argv[1:], + "k:b:d:l:w:", + ["key=", "build=", "days=", "lasteventtime=", "workdir="], + ) + for opt, arg in opts: + if opt == "-k": + key = arg + elif opt == "-d": + days = int(arg) + elif opt == "-l": + lasteventtime = int(arg) + elif opt == "-b": + minbuild = arg + elif opt == "-w": + workdir = arg +except getopt.GetoptError: + usage() + +if key == "undefined": + usage() + +start = utils.dateback(days) +year = start.year +month = start.month +day = start.day + +run = {} +lastrun = utils.getLastRunFromExecutionFile(workdir) +if "lasteventtime" in lastrun: + lasteventtime = lastrun["lasteventtime"] +run["workdir"] = workdir +run["daysback"] = days +run["minbuild"] = minbuild + +p_params = "p_year={:04d}&p_month={:02d}&p_day={:02d}&p_build={}" "&p_last={}".format( + year, month, day, minbuild, lasteventtime +) +print(p_params) +result = telemetry.query(key, 78691, p_params) +rows = result["query_result"]["data"]["rows"] +run["numrows"] = len(rows) +if run["numrows"] > 0: + lasteventtime = telemetry.getLastEventTimeAbs(rows) + run["lasteventtime"] = lasteventtime + rows.sort( + key=lambda row: "{}.{}.{}.{}.{:06d}".format( + row["client_id"], + row["session_id"], + row["seq"] >> 32, # thread_id + row["submit_timeabs"], + row["seq"] & 0x00000000FFFFFFFF, # seq, + ), + reverse=False, + ) + outfile = "{}/qmrows_until_{}.json".format(workdir, lasteventtime) + utils.writeJSONFile(outfile, rows) + run["rawfile"] = outfile +else: + print("No results found, maybe next time.") + run["lasteventtime"] = lasteventtime + +utils.addNewRunToExecutionFile(workdir, run) diff --git a/dom/quota/scripts/fn_anchors.py b/dom/quota/scripts/fn_anchors.py new file mode 100644 index 0000000000..eeaf43764c --- /dev/null +++ b/dom/quota/scripts/fn_anchors.py @@ -0,0 +1,68 @@ +# 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/. + +import json +import subprocess + +cached_functions = {} + + +def getMetricsJson(src_url): + if src_url.startswith("http"): + print("Fetching source for function extraction: {}".format(src_url)) + metrics = subprocess.check_output(["./fetch_fn_names.sh", src_url]) + else: + print("Skip fetching source: {}".format(src_url)) + metrics = "" + + try: + return json.loads(metrics) + except ValueError: + return {"kind": "empty", "name": "anonymous", "spaces": []} + + +def getSpaceFunctionsRecursive(metrics_space): + functions = [] + if ( + metrics_space["kind"] == "function" + and metrics_space["name"] + and metrics_space["name"] != "<anonymous>" + ): + functions.append( + { + "name": metrics_space["name"], + "start_line": int(metrics_space["start_line"]), + "end_line": int(metrics_space["end_line"]), + } + ) + for space in metrics_space["spaces"]: + functions += getSpaceFunctionsRecursive(space) + return functions + + +def getSourceFunctions(src_url): + if src_url not in cached_functions: + metrics_space = getMetricsJson(src_url) + cached_functions[src_url] = getSpaceFunctionsRecursive(metrics_space) + + return cached_functions[src_url] + + +def getFunctionName(location): + location.replace("annotate", "raw-file") + pieces = location.split("#l") + src_url = pieces[0] + line = int(pieces[1]) + closest_name = "<Unknown {}>".format(line) + closest_start = 0 + functions = getSourceFunctions(src_url) + for fn in functions: + if ( + fn["start_line"] > closest_start + and line >= fn["start_line"] + and line <= fn["end_line"] + ): + closest_start = fn["start_line"] + closest_name = fn["name"] + return closest_name diff --git a/dom/quota/scripts/stackanalysis.py b/dom/quota/scripts/stackanalysis.py new file mode 100644 index 0000000000..f0363c5e1f --- /dev/null +++ b/dom/quota/scripts/stackanalysis.py @@ -0,0 +1,396 @@ +# 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/. + + +# There seem to be sometimes identical events recorded twice by telemetry +def sanitize(rows): + newrows = [] + pcid = "unset" + psid = "unset" + pseq = "unset" + for row in rows: + cid = row["client_id"] + sid = row["session_id"] + seq = row["seq"] + if cid != pcid or sid != psid or seq != pseq: + newrows.append(row) + pcid = cid + psid = sid + pseq = seq + + return newrows + + +# Given a set of rows, find all distinct build ids +def extractBuildIDs(rows): + buildids = {} + for row in rows: + id = row["build_id"] + if id in buildids: + buildids[id] = buildids[id] + 1 + else: + buildids[id] = 1 + return buildids + + +# Given a set of build ids and rows, enrich each row by an hg link. +# Relys on the result of utils.fetchBuildRevisions in buildids. +def constructHGLinks(buildids, rows): + for row in rows: + id = row["build_id"] + if id in buildids: + row["location"] = ( + buildids[id] + "/" + row["source_file"] + "#l" + row["source_line"] + ) + else: + row["location"] = id + "/" + row["source_file"] + "#l" + row["source_line"] + + +topmost_stackframes = set() +delta_frames = {} + + +def isTopmostFrame(frame): + f = (frame["location"], frame["result"]) + return f in topmost_stackframes + + +def addTopmostFrame(frame): + f = (frame["location"], frame["result"]) + if not isTopmostFrame(frame): + # print("Found new topmost frame {}.".format(frame)) + topmost_stackframes.add(f) + frame["topmost"] = True + + +def addFrameDelta(frame1, frame2): + if frame1["client_id"] != frame2["client_id"]: + return + if frame1["session_id"] != frame2["session_id"]: + return + + fkey = "{}:{}-{}:{}".format( + frame2["location"], frame2["result"], frame1["location"], frame1["result"] + ) + if fkey not in delta_frames: + fdelta = {"delta_sum": 0, "delta_cnt": 0} + fdelta["prev_row"] = frame1 + fdelta["candidate"] = frame2 + delta_frames[fkey] = fdelta + + fdelta = delta_frames[fkey] + etv1 = frame1["event_timestamp"] + etv2 = frame2["event_timestamp"] + if isinstance(etv1, int) and isinstance(etv2, int) and etv2 > etv1: + delta = etv2 - etv1 + fdelta["delta_sum"] = fdelta["delta_sum"] + delta + fdelta["delta_cnt"] = fdelta["delta_cnt"] + 1 + + +# There can be outliers in terms of time distance between two stack frames +# that belong to the same propagation stack. In order to not increase the +# risk that one outlier breaks thousands of stacks, we check for the average +# time distance. +def checkAverageFrameTimeDeltas(rows, max_delta): + # print("checkAverageFrameTimeDeltas") + prev_row = None + for row in rows: + if "topmost" in row or not row["session_complete"]: + prev_row = None + continue + + if prev_row: + addFrameDelta(prev_row, row) + prev_row = row + + for fd in delta_frames: + sum = delta_frames[fd]["delta_sum"] + cnt = delta_frames[fd]["delta_cnt"] + if cnt > 0 and (sum / cnt) > max_delta: + # print(delta_frames[fd]) + addTopmostFrame(delta_frames[fd]["candidate"]) + + +# A topmost frame is considered to initiate a new raw stack. We collect all +# candidates before we actually apply them. This implies, that we should run +# this function on a "large enough" sample of rows to be more accurate. +# As a side effect, we mark all rows that are part of a "complete" session +# (a session, that started within our data scope). +def collectTopmostFrames(rows): + prev_cid = "unset" + prev_sid = "unset" + prev_tid = "unset" + prev_ctx = "unset" + prev_sev = "ERROR" + session_complete = False + after_severity_downgrade = False + for row in rows: + cid = row["client_id"] + sid = row["session_id"] + tid = row["seq"] >> 32 # thread_id + ctx = row["context"] + seq = row["seq"] & 0x00000000FFFFFFFF # seq + sev = row["severity"] + + # If we have a new session, ensure it is complete from start, + # otherwise we will ignore it entirely. + if cid != prev_cid or sid != prev_sid or tid != prev_tid: + if seq == 1: + session_complete = True + else: + session_complete = False + row["session_complete"] = session_complete + if session_complete: + # If we change client, session, thread or context, we can be sure to have + # a new topmost frame. + if ( + seq == 1 + or cid != prev_cid + or sid != prev_sid + or tid != prev_tid + or ctx != prev_ctx + ): + addTopmostFrame(row) + after_severity_downgrade = False + # We do not expect a non-error to be ever upgraded to an error + elif sev == "ERROR" and prev_sev != "ERROR": + addTopmostFrame(row) + after_severity_downgrade = False + # If we just had a severity downgrade, we assume that we wanted + # to break the error propagation after this point and split, too + elif after_severity_downgrade: + addTopmostFrame(row) + after_severity_downgrade = False + elif prev_sev == "ERROR" and sev != "ERROR": + after_severity_downgrade = True + + prev_cid = cid + prev_sid = sid + prev_tid = tid + prev_ctx = ctx + prev_sev = sev + + # Should be ms. We've seen quite some runtime between stackframes in the + # wild. We might want to consider to make this configurable. In general + # we prefer local context over letting slip through some topmost frame + # unrecognized, assuming that fixing the issues one by one they will + # uncover them succesively. This is achieved by a rather high delta value. + max_avg_delta = 200 + checkAverageFrameTimeDeltas(rows, max_avg_delta) + + +def getFrameKey(frame): + return "{}.{}|".format(frame["location"], frame["result"]) + + +def getStackKey(stack): + stack_key = "" + for frame in stack["frames"]: + stack_key += getFrameKey(frame) + return hash(stack_key) + + +# A "raw stack" is a list of frames, that: +# - share the same build_id (implicitely through location) +# - share the same client_id +# - share the same session_id +# - has a growing sequence number +# - stops at the first downgrade of severity from ERROR to else +# - XXX: contains each location at most once (no recursion) +# - appears to be in a reasonable short timeframe +# Calculates also a hash key to identify identical stacks +def collectRawStacks(rows): + collectTopmostFrames(rows) + raw_stacks = [] + stack = { + "stack_id": "unset", + "client_id": "unset", + "session_id": "unset", + "submit_timeabs": "unset", + "frames": [{"location": "unset"}], + } + stack_id = 1 + first = True + for row in rows: + if isTopmostFrame(row): + if not first: + stack["stack_key"] = getStackKey(stack) + raw_stacks.append(stack) + stack_id += 1 + stack = { + "stack_id": stack_id, + "client_id": row["client_id"], + "session_id": row["session_id"], + "submit_timeabs": row["submit_timeabs"], + "context": row["context"], + "frames": [], + } + + stack["frames"].append( + { + "location": row["location"], + "source_file": row["source_file"], + "source_line": row["source_line"], + "seq": row["seq"], + "severity": row["severity"], + "result": row["result"], + } + ) + first = False + + return raw_stacks + + +# Merge all stacks that have the same hash key and count occurences. +# Relys on the ordering per client_id/session_id for correct counting. +def mergeEqualStacks(raw_stacks): + merged_stacks = {} + last_client_id = "none" + last_session_id = "none" + for stack in raw_stacks: + stack_key = stack["stack_key"] + merged_stack = stack + if stack_key in merged_stacks: + merged_stack = merged_stacks[stack_key] + if stack["client_id"] != last_client_id: + last_client_id = stack["client_id"] + merged_stack["client_count"] += 1 + if stack["session_id"] != last_session_id: + last_session_id = stack["session_id"] + merged_stack["session_count"] += 1 + merged_stack["hit_count"] += 1 + else: + merged_stack["client_count"] = 1 + last_client_id = merged_stack["client_id"] + merged_stack["session_count"] = 1 + last_session_id = merged_stack["session_id"] + merged_stack["hit_count"] = 1 + merged_stacks[stack_key] = merged_stack + + merged_list = list(merged_stacks.values()) + merged_list.sort(key=lambda x: x.get("hit_count"), reverse=True) + return merged_list + + +# Split the list of stacks into: +# - aborted (has at least one frame with NS_ERROR_ABORT) +# - info/warning (has at least one frame with that severity) +# - error (has only error frames) +def filterStacksForPropagation( + all_stacks, error_stacks, warn_stacks, info_stacks, abort_stacks +): + for stack in all_stacks: + warn = list(filter(lambda x: x["severity"] == "WARNING", stack["frames"])) + info = list(filter(lambda x: x["severity"] == "INFO", stack["frames"])) + abort = list(filter(lambda x: x["result"] == "NS_ERROR_ABORT", stack["frames"])) + if len(abort) > 0: + abort_stacks.append(stack) + elif len(info) > 0: + info_stacks.append(stack) + elif len(warn) > 0: + warn_stacks.append(stack) + else: + error_stacks.append(stack) + + +# Bugzilla comment markup +def printStacks(stacks): + row_format = "{} | {} | {} | {} | {}\n" + out = "" + out += row_format.format("Clients", "Sessions", "Hits", "Anchor (Context)", "Stack") + out += row_format.format("-------", "--------", "----", "----------------", "-----") + for stack in stacks: + framestr = "" + first = True + for frame in stack["frames"]: + if not first: + framestr += " <- " + framestr += "[{}#{}:{}]({})".format( + frame["source_file"], + frame["source_line"], + frame["result"], + frame["location"], + ) + first = False + out += row_format.format( + stack["client_count"], + stack["session_count"], + stack["hit_count"], + "{} ({})".format(stack["frames"][0]["anchor"], stack["context"]), + framestr, + ) + + return out + + +def groupStacksForAnchors(stacks): + anchors = {} + for stack in stacks: + anchor_name = stack["frames"][0]["anchor"] + if anchor_name in anchors: + anchors[anchor_name]["stacks"].append(stack) + else: + anchor = {"anchor": anchor_name, "stacks": [stack]} + anchors[anchor_name] = anchor + return anchors + + +""" +def getSummaryForAnchor(anchor): + return "[QM_TRY] Errors in function {}".format(anchor) + + +def searchBugForAnchor(bugzilla_key, anchor): + summary = getSummaryForAnchor(anchor) + bug_url = "https://bugzilla.mozilla.org/rest/bug?" \ + "summary={}&api_key={}".format(summary, bugzilla_key) + return requests.get(url=bug_url).json()["bugs"] + + +def createBugForAnchor(bugzilla_key, anchor): + summary = getSummaryForAnchor(anchor) + bug_url = "https://bugzilla.mozilla.org/rest/bug?" \ + "Bugzilla_api_key={}".format(bugzilla_key) + body = { + "product" : "Core", + "component" : "Storage: Quota Manager", + "version" : "unspecified", + "summary" : summary, + "description" : "This bug collects errors reported by QM_TRY" + "macros for function {}.".format(anchor), + } + resp = requests.post(url=bug_url, json=body) + if resp.status_code != 200: + print(resp) + return 0 + id = resp.json()["id"] + print("Added new bug {}:".format(id)) + return id + + +def ensureBugForAnchor(bugzilla_key, anchor): + buglist = searchBugForAnchor(bugzilla_key, anchor) + if (len(buglist) > 0): + id = buglist[0]["id"] + print("Found existing bug {}:".format(id)) + return id + return createBugForAnchor(bugzilla_key, anchor) + + +def addCommentForAnchor(bugzilla_key, anchor, stacks): + id = ensureBugForAnchor(bugzilla_key, anchor) + if (id <= 0): + print("Unable to create a bug for {}.".format(anchor)) + return + comment = printStacks(stacks) + print("") + print("Add comment to bug {}:".format(id)) + print(comment) + + +def addCommentsForStacks(bugzilla_key, stacks): + anchors = groupStacksForAnchors(stacks) + for anchor in anchors: + addCommentForAnchor(bugzilla_key, anchors[anchor]["anchor"], anchors[anchor]["stacks"]) +""" diff --git a/dom/quota/scripts/telemetry.py b/dom/quota/scripts/telemetry.py new file mode 100644 index 0000000000..a62abd62b1 --- /dev/null +++ b/dom/quota/scripts/telemetry.py @@ -0,0 +1,54 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import time + +import requests + + +def query(key, query, p_params): + headers = {"Authorization": "Key {}".format(key)} + start_url = "https://sql.telemetry.mozilla.org/api/" "queries/{}/refresh?{}".format( + query, p_params + ) + print(start_url) + resp = requests.post(url=start_url, headers=headers) + job = resp.json()["job"] + jid = job["id"] + print("Started job {}".format(jid)) + + poll_url = "https://sql.telemetry.mozilla.org/api/" "jobs/{}".format(jid) + print(poll_url) + poll = True + status = 0 + qresultid = 0 + while poll: + print(".", end="", flush=True) + resp = requests.get(url=poll_url, headers=headers) + status = resp.json()["job"]["status"] + if status > 2: + # print(resp.json()) + poll = False + qresultid = resp.json()["job"]["query_result_id"] + else: + time.sleep(0.2) + print(".") + print("Finished with status {}".format(status)) + + if status == 3: + fetch_url = ( + "https://sql.telemetry.mozilla.org/api/" + "queries/78691/results/{}.json".format(qresultid) + ) + print(fetch_url) + resp = requests.get(url=fetch_url, headers=headers) + return resp.json() + + return {"query_result": {"data": {"rows": {}}}} + + +def getLastEventTimeAbs(rows): + if len(rows) == 0: + return 0 + return rows[len(rows) - 1]["submit_timeabs"] diff --git a/dom/quota/scripts/utils.py b/dom/quota/scripts/utils.py new file mode 100644 index 0000000000..d5322728a5 --- /dev/null +++ b/dom/quota/scripts/utils.py @@ -0,0 +1,89 @@ +# 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/. + +import datetime +import json + +import requests + + +def readJSONFile(FileName): + f = open(FileName, "r") + p = json.load(f) + f.close() + return p + + +def writeJSONFile(FileName, Content): + with open(FileName, "w") as outfile: + json.dump(Content, outfile, indent=4) + + +def dateback(days): + today = datetime.date.today() + delta = datetime.timedelta(days) + return today - delta + + +def lastweek(): + today = datetime.date.today() + delta = datetime.timedelta(days=7) + return today - delta + + +# Given a set of build ids, fetch the repository base URL for each id. +def fetchBuildRevisions(buildids): + buildhub_url = "https://buildhub.moz.tools/api/search" + delids = {} + for bid in buildids: + print("Fetching revision for build {}.".format(bid)) + body = {"size": 1, "query": {"term": {"build.id": bid}}} + resp = requests.post(url=buildhub_url, json=body) + hits = resp.json()["hits"]["hits"] + if len(hits) > 0: + buildids[bid] = ( + hits[0]["_source"]["source"]["repository"] + + "/annotate/" + + hits[0]["_source"]["source"]["revision"] + ) + else: + print("No revision for build.id {}".format(bid)) + delids[bid] = "x" + for bid in delids: + buildids.pop(bid) + + +def readExecutionFile(workdir): + exefile = "{}/qmexecutions.json".format(workdir) + try: + return readJSONFile(exefile) + except OSError: + return [] + + +def writeExecutionFile(workdir, executions): + exefile = "{}/qmexecutions.json".format(workdir) + try: + writeJSONFile(exefile, executions) + except OSError: + print("Error writing execution record.") + + +def getLastRunFromExecutionFile(workdir): + executions = readExecutionFile(workdir) + if len(executions) > 0: + return executions[len(executions) - 1] + return {} + + +def updateLastRunToExecutionFile(workdir, run): + executions = readExecutionFile(workdir) + executions[len(executions) - 1] = run + writeExecutionFile(workdir, executions) + + +def addNewRunToExecutionFile(workdir, run): + executions = readExecutionFile(workdir) + executions.append(run) + writeExecutionFile(workdir, executions) diff --git a/dom/quota/test/browser/browser.ini b/dom/quota/test/browser/browser.ini new file mode 100644 index 0000000000..d0c79f90a9 --- /dev/null +++ b/dom/quota/test/browser/browser.ini @@ -0,0 +1,13 @@ +[DEFAULT] +skip-if = (buildapp != "browser") +support-files = + head.js + helpers.js + empty.html + permissionsPrompt.html + +[browser_permissionsCrossOrigin.js] +[browser_permissionsPromptAllow.js] +[browser_permissionsPromptDeny.js] +[browser_permissionsPromptUnknown.js] +[browser_simpledb.js] diff --git a/dom/quota/test/browser/browser_permissionsCrossOrigin.js b/dom/quota/test/browser/browser_permissionsCrossOrigin.js new file mode 100644 index 0000000000..6dcf32ff67 --- /dev/null +++ b/dom/quota/test/browser/browser_permissionsCrossOrigin.js @@ -0,0 +1,56 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const emptyURL = + "https://example.com/browser/dom/quota/test/browser/empty.html"; + +addTest(async function testNoPermissionPrompt() { + registerPopupEventHandler("popupshowing", function() { + ok(false, "Shouldn't show a popup this time"); + }); + registerPopupEventHandler("popupshown", function() { + ok(false, "Shouldn't show a popup this time"); + }); + registerPopupEventHandler("popuphidden", function() { + ok(false, "Shouldn't show a popup this time"); + }); + + info("Creating tab"); + + await BrowserTestUtils.withNewTab(emptyURL, async function(browser) { + await new Promise(r => { + SpecialPowers.pushPrefEnv( + { + set: [ + ["permissions.delegation.enabled", true], + ["dom.security.featurePolicy.header.enabled", true], + ["dom.security.featurePolicy.webidl.enabled", true], + ], + }, + r + ); + }); + + await SpecialPowers.spawn(browser, [], async function(host0) { + let frame = content.document.createElement("iframe"); + // Cross origin src + frame.src = "https://example.org/browser/dom/quota/test/empty.html"; + content.document.body.appendChild(frame); + await ContentTaskUtils.waitForEvent(frame, "load"); + + await content.SpecialPowers.spawn(frame, [], async function() { + // Request a permission. + const persistAllowed = await this.content.navigator.storage.persist(); + Assert.ok( + !persistAllowed, + "navigator.storage.persist() has been denied" + ); + }); + content.document.body.removeChild(frame); + }); + }); + + unregisterAllPopupEventHandlers(); +}); diff --git a/dom/quota/test/browser/browser_permissionsPromptAllow.js b/dom/quota/test/browser/browser_permissionsPromptAllow.js new file mode 100644 index 0000000000..4ddcfada96 --- /dev/null +++ b/dom/quota/test/browser/browser_permissionsPromptAllow.js @@ -0,0 +1,66 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const testPageURL = + "https://example.com/browser/dom/quota/test/browser/permissionsPrompt.html"; + +addTest(async function testPermissionAllow() { + removePermission(testPageURL, "persistent-storage"); + + registerPopupEventHandler("popupshowing", function() { + ok(true, "prompt showing"); + }); + registerPopupEventHandler("popupshown", function() { + ok(true, "prompt shown"); + triggerMainCommand(this); + }); + registerPopupEventHandler("popuphidden", function() { + ok(true, "prompt hidden"); + }); + + info("Creating tab"); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + info("Loading test page: " + testPageURL); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testPageURL); + await waitForMessage(true, gBrowser); + + is( + getPermission(testPageURL, "persistent-storage"), + Ci.nsIPermissionManager.ALLOW_ACTION, + "Correct permission set" + ); + gBrowser.removeCurrentTab(); + unregisterAllPopupEventHandlers(); + // Keep persistent-storage permission for the next test. +}); + +addTest(async function testNoPermissionPrompt() { + registerPopupEventHandler("popupshowing", function() { + ok(false, "Shouldn't show a popup this time"); + }); + registerPopupEventHandler("popupshown", function() { + ok(false, "Shouldn't show a popup this time"); + }); + registerPopupEventHandler("popuphidden", function() { + ok(false, "Shouldn't show a popup this time"); + }); + + info("Creating tab"); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + info("Loading test page: " + testPageURL); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testPageURL); + await waitForMessage(true, gBrowser); + + is( + getPermission(testPageURL, "persistent-storage"), + Ci.nsIPermissionManager.ALLOW_ACTION, + "Correct permission set" + ); + gBrowser.removeCurrentTab(); + unregisterAllPopupEventHandlers(); + removePermission(testPageURL, "persistent-storage"); +}); diff --git a/dom/quota/test/browser/browser_permissionsPromptDeny.js b/dom/quota/test/browser/browser_permissionsPromptDeny.js new file mode 100644 index 0000000000..19ee8ef499 --- /dev/null +++ b/dom/quota/test/browser/browser_permissionsPromptDeny.js @@ -0,0 +1,150 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const testPageURL = + "https://example.com/browser/dom/quota/test/browser/permissionsPrompt.html"; + +addTest(async function testPermissionTemporaryDenied() { + registerPopupEventHandler("popupshowing", function() { + ok(true, "prompt showing"); + }); + registerPopupEventHandler("popupshown", function() { + ok(true, "prompt shown"); + triggerSecondaryCommand(this); + }); + registerPopupEventHandler("popuphidden", function() { + ok(true, "prompt hidden"); + }); + + info("Creating tab"); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + info("Loading test page: " + testPageURL); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testPageURL); + await waitForMessage(false, gBrowser); + + is( + getPermission(testPageURL, "persistent-storage"), + Ci.nsIPermissionManager.UNKNOWN_ACTION, + "Correct permission set" + ); + + let tempBlock = SitePermissions.getAllForBrowser( + gBrowser.selectedBrowser + ).find( + p => + p.id == "persistent-storage" && + p.state == SitePermissions.BLOCK && + p.scope == SitePermissions.SCOPE_TEMPORARY + ); + ok(tempBlock, "Should have a temporary block permission on active browser"); + + unregisterAllPopupEventHandlers(); + gBrowser.removeCurrentTab(); + removePermission(testPageURL, "persistent-storage"); +}); + +addTest(async function testPermissionDenied() { + removePermission(testPageURL, "persistent-storage"); + + registerPopupEventHandler("popupshowing", function() { + ok(true, "prompt showing"); + }); + registerPopupEventHandler("popupshown", function() { + ok(true, "prompt shown"); + triggerSecondaryCommand(this, /* remember = */ true); + }); + registerPopupEventHandler("popuphidden", function() { + ok(true, "prompt hidden"); + }); + + info("Creating tab"); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + info("Loading test page: " + testPageURL); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testPageURL); + await waitForMessage(false, gBrowser); + + is( + getPermission(testPageURL, "persistent-storage"), + Ci.nsIPermissionManager.DENY_ACTION, + "Correct permission set" + ); + unregisterAllPopupEventHandlers(); + gBrowser.removeCurrentTab(); + // Keep persistent-storage permission for the next test. +}); + +addTest(async function testNoPermissionPrompt() { + registerPopupEventHandler("popupshowing", function() { + ok(false, "Shouldn't show a popup this time"); + }); + registerPopupEventHandler("popupshown", function() { + ok(false, "Shouldn't show a popup this time"); + }); + registerPopupEventHandler("popuphidden", function() { + ok(false, "Shouldn't show a popup this time"); + }); + + info("Creating tab"); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + info("Loading test page: " + testPageURL); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testPageURL); + await waitForMessage(false, gBrowser); + + is( + getPermission(testPageURL, "persistent-storage"), + Ci.nsIPermissionManager.DENY_ACTION, + "Correct permission set" + ); + unregisterAllPopupEventHandlers(); + gBrowser.removeCurrentTab(); + removePermission(testPageURL, "persistent-storage"); +}); + +addTest(async function testPermissionDeniedDismiss() { + registerPopupEventHandler("popupshowing", function() { + ok(true, "prompt showing"); + }); + registerPopupEventHandler("popupshown", function() { + ok(true, "prompt shown"); + // Dismiss permission prompt. + dismissNotification(this); + }); + registerPopupEventHandler("popuphidden", function() { + ok(true, "prompt hidden"); + }); + + info("Creating tab"); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + info("Loading test page: " + testPageURL); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testPageURL); + await waitForMessage(false, gBrowser); + + // Pressing ESC results in a temporary block permission on the browser object. + // So the global permission for the URL should still be unknown, but the browser + // should have a block permission with a temporary scope. + is( + getPermission(testPageURL, "persistent-storage"), + Ci.nsIPermissionManager.UNKNOWN_ACTION, + "Correct permission set" + ); + + let tempBlock = SitePermissions.getAllForBrowser( + gBrowser.selectedBrowser + ).find( + p => + p.id == "persistent-storage" && + p.state == SitePermissions.BLOCK && + p.scope == SitePermissions.SCOPE_TEMPORARY + ); + ok(tempBlock, "Should have a temporary block permission on active browser"); + + unregisterAllPopupEventHandlers(); + gBrowser.removeCurrentTab(); + removePermission(testPageURL, "persistent-storage"); +}); diff --git a/dom/quota/test/browser/browser_permissionsPromptUnknown.js b/dom/quota/test/browser/browser_permissionsPromptUnknown.js new file mode 100644 index 0000000000..31c7e588a5 --- /dev/null +++ b/dom/quota/test/browser/browser_permissionsPromptUnknown.js @@ -0,0 +1,53 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const testPageURL = + "https://example.com/browser/dom/quota/test/browser/permissionsPrompt.html"; + +addTest(async function testPermissionUnknownInPrivateWindow() { + removePermission(testPageURL, "persistent-storage"); + info("Creating private window"); + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + registerPopupEventHandler( + "popupshowing", + function() { + ok(false, "Shouldn't show a popup this time"); + }, + win + ); + registerPopupEventHandler( + "popupshown", + function() { + ok(false, "Shouldn't show a popup this time"); + }, + win + ); + registerPopupEventHandler( + "popuphidden", + function() { + ok(false, "Shouldn't show a popup this time"); + }, + win + ); + + info("Creating private tab"); + win.gBrowser.selectedTab = BrowserTestUtils.addTab(win.gBrowser); + + info("Loading test page: " + testPageURL); + BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, testPageURL); + await waitForMessage(false, win.gBrowser); + + is( + getPermission(testPageURL, "persistent-storage"), + Ci.nsIPermissionManager.UNKNOWN_ACTION, + "Correct permission set" + ); + unregisterAllPopupEventHandlers(win); + win.gBrowser.removeCurrentTab(); + await BrowserTestUtils.closeWindow(win); + win = null; + removePermission(testPageURL, "persistent-storage"); +}); diff --git a/dom/quota/test/browser/browser_simpledb.js b/dom/quota/test/browser/browser_simpledb.js new file mode 100644 index 0000000000..b3be78fca6 --- /dev/null +++ b/dom/quota/test/browser/browser_simpledb.js @@ -0,0 +1,51 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// getRandomBuffer, compareBuffers +loadScript("dom/quota/test/common/file.js"); + +addTest(async function testSimpleDB() { + const name = "data"; + const bufferSize = 100; + + let database = getSimpleDatabase(); + + let request = database.open("data"); + await requestFinished(request); + + let buffer1 = getRandomBuffer(bufferSize); + + request = database.write(buffer1); + await requestFinished(request); + + request = database.seek(0); + await requestFinished(request); + + request = database.read(bufferSize); + let result = await requestFinished(request); + + let buffer2 = result.getAsArrayBuffer(); + + ok(compareBuffers(buffer1, buffer2), "Buffers equal."); + + let database2 = getSimpleDatabase(); + + try { + request = database2.open(name); + await requestFinished(request); + ok(false, "Should have thrown!"); + } catch (ex) { + ok(request.resultCode == NS_ERROR_STORAGE_BUSY, "Good result code."); + } + + request = database.close(); + await requestFinished(request); + + request = database2.open(name); + await requestFinished(request); + + request = database2.close(); + await requestFinished(request); +}); diff --git a/dom/quota/test/browser/empty.html b/dom/quota/test/browser/empty.html new file mode 100644 index 0000000000..1ad28bb1f7 --- /dev/null +++ b/dom/quota/test/browser/empty.html @@ -0,0 +1,8 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Empty file</title> +</head> +<body> +</body> +</html> diff --git a/dom/quota/test/browser/head.js b/dom/quota/test/browser/head.js new file mode 100644 index 0000000000..fd81168b02 --- /dev/null +++ b/dom/quota/test/browser/head.js @@ -0,0 +1,149 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// The path to the top level directory. +const depth = "../../../../"; + +var gActiveListeners = {}; + +loadScript("dom/quota/test/common/browser.js"); + +function loadScript(path) { + const url = new URL(depth + path, gTestPath); + Services.scriptloader.loadSubScript(url.href, this); +} + +// These event (un)registration handlers only work for one window, DONOT use +// them with multiple windows. + +function registerPopupEventHandler(eventName, callback, win) { + if (!win) { + win = window; + } + gActiveListeners[eventName] = function(event) { + if (event.target != win.PopupNotifications.panel) { + return; + } + win.PopupNotifications.panel.removeEventListener( + eventName, + gActiveListeners[eventName] + ); + delete gActiveListeners[eventName]; + + callback.call(win.PopupNotifications.panel); + }; + win.PopupNotifications.panel.addEventListener( + eventName, + gActiveListeners[eventName] + ); +} + +function unregisterAllPopupEventHandlers(win) { + if (!win) { + win = window; + } + for (let eventName in gActiveListeners) { + win.PopupNotifications.panel.removeEventListener( + eventName, + gActiveListeners[eventName] + ); + } + gActiveListeners = {}; +} + +function triggerMainCommand(popup, win) { + if (!win) { + win = window; + } + info("triggering main command"); + let notifications = popup.childNodes; + ok(notifications.length, "at least one notification displayed"); + let notification = notifications[0]; + info("triggering command: " + notification.getAttribute("buttonlabel")); + + EventUtils.synthesizeMouseAtCenter(notification.button, {}, win); +} + +async function triggerSecondaryCommand(popup, remember = false, win = window) { + info("triggering secondary command"); + let notifications = popup.childNodes; + ok(notifications.length, "at least one notification displayed"); + let notification = notifications[0]; + + if (remember) { + notification.checkbox.checked = true; + } + + await EventUtils.synthesizeMouseAtCenter( + notification.secondaryButton, + {}, + win + ); +} + +function dismissNotification(popup, win = window) { + info("dismissing notification"); + executeSoon(function() { + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + }); +} + +function waitForMessage(aMessage, browser) { + // We cannot capture aMessage inside the checkFn, so we override the + // checkFn.toSource to tunnel aMessage instead. + let checkFn = function() {}; + checkFn.toSource = function() { + return `function checkFn(event) { + let message = ${aMessage.toSource()}; + if (event.data == message) { + return true; + } + throw new Error( + \`Unexpected result: \$\{event.data\}, expected \$\{message\}\` + ); + }`; + }; + + return BrowserTestUtils.waitForContentEvent( + browser.selectedBrowser, + "message", + /* capture */ true, + checkFn, + /* wantsUntrusted */ true + ).then(() => { + // An assertion in checkFn wouldn't be recorded as part of the test, so we + // use this assertion to confirm that we've successfully received the + // message (we'll only reach this point if that's the case). + ok(true, "Received message: " + aMessage); + }); +} + +function removePermission(url, permission) { + let uri = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(url); + let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + let principal = ssm.createContentPrincipal(uri, {}); + + Cc["@mozilla.org/permissionmanager;1"] + .getService(Ci.nsIPermissionManager) + .removeFromPrincipal(principal, permission); +} + +function getPermission(url, permission) { + let uri = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(url); + let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + let principal = ssm.createContentPrincipal(uri, {}); + + return Cc["@mozilla.org/permissionmanager;1"] + .getService(Ci.nsIPermissionManager) + .testPermissionFromPrincipal(principal, permission); +} diff --git a/dom/quota/test/browser/helpers.js b/dom/quota/test/browser/helpers.js new file mode 100644 index 0000000000..f3bbb36b71 --- /dev/null +++ b/dom/quota/test/browser/helpers.js @@ -0,0 +1,46 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// The path to the top level directory. +const depth = "../../../../"; + +var testGenerator; +var testResult; + +loadScript("dom/quota/test/common/nestedtest.js"); + +function loadScript(path) { + const url = new URL(depth + path, window.location.href); + SpecialPowers.Services.scriptloader.loadSubScript(url.href, this); +} + +function runTest() { + clearAllDatabases(() => { + testGenerator = testSteps(); + testGenerator.next(); + }); +} + +function finishTestNow() { + if (testGenerator) { + testGenerator.return(); + testGenerator = undefined; + } +} + +function finishTest() { + clearAllDatabases(() => { + setTimeout(finishTestNow, 0); + setTimeout(() => { + window.parent.postMessage(testResult, "*"); + }, 0); + }); +} + +function continueToNextStep() { + setTimeout(() => { + testGenerator.next(); + }, 0); +} diff --git a/dom/quota/test/browser/permissionsPrompt.html b/dom/quota/test/browser/permissionsPrompt.html new file mode 100644 index 0000000000..5f11bf6c95 --- /dev/null +++ b/dom/quota/test/browser/permissionsPrompt.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> + <head> + <meta charset=UTF-8> + <title>Persistent-Storage Permission Prompt Test</title> + + <script type="text/javascript" src="helpers.js"></script> + + <script type="text/javascript"> + function* testSteps() + { + SpecialPowers.pushPrefEnv({ + "set": [["dom.storageManager.prompt.testing", false], + ["dom.storageManager.prompt.testing.allow", false]] + }, continueToNextStep); + yield undefined; + + navigator.storage.persist().then(result => { + testGenerator.next(result); + }); + testResult = yield undefined; + + finishTest(); + } + </script> + + </head> + + <body onload="runTest();" onunload="finishTestNow();"></body> + +</html> diff --git a/dom/quota/test/common/browser.js b/dom/quota/test/common/browser.js new file mode 100644 index 0000000000..aae80adcd1 --- /dev/null +++ b/dom/quota/test/common/browser.js @@ -0,0 +1,34 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadScript("dom/quota/test/common/system.js"); + +function addTest(testFunction) { + const taskFunction = async function() { + await enableStorageTesting(); + + await testFunction(); + }; + + Object.defineProperty(taskFunction, "name", { + value: testFunction.name, + writable: false, + }); + + add_task(taskFunction); +} + +async function enableStorageTesting() { + const prefsToSet = [ + ["dom.quotaManager.testing", true], + ["dom.storageManager.enabled", true], + ["dom.simpleDB.enabled", true], + ]; + if (Services.appinfo.OS === "WINNT") { + prefsToSet.push(["dom.quotaManager.useDOSDevicePathSyntax", true]); + } + + await SpecialPowers.pushPrefEnv({ set: prefsToSet }); +} diff --git a/dom/quota/test/common/content.js b/dom/quota/test/common/content.js new file mode 100644 index 0000000000..d3927d649a --- /dev/null +++ b/dom/quota/test/common/content.js @@ -0,0 +1,62 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const NS_ERROR_STORAGE_BUSY = SpecialPowers.Cr.NS_ERROR_STORAGE_BUSY; + +loadScript("dom/quota/test/common/global.js"); + +function clearAllDatabases(callback) { + let qms = SpecialPowers.Services.qms; + let principal = SpecialPowers.wrap(document).nodePrincipal; + let request = qms.clearStoragesForPrincipal(principal); + let cb = SpecialPowers.wrapCallback(callback); + request.callback = cb; + return request; +} + +// SimpleDB connections and SpecialPowers wrapping: +// +// SpecialPowers provides a SpecialPowersHandler Proxy mechanism that lets our +// content-privileged code borrow its chrome-privileged principal to access +// things we shouldn't be able to access. The proxies wrap their returned +// values, so once we have something wrapped we can rely on returned objects +// being wrapped as well. The proxy will also automatically unwrap wrapped +// arguments we pass in. However, we need to invoke wrapCallback on callback +// functions so that the arguments they receive will be wrapped because the +// proxy does not automatically wrap content-privileged functions. +// +// Our use of (wrapped) SpecialPowers.Cc results in getSimpleDatabase() +// producing a wrapped nsISDBConnection instance. The nsISDBResult instances +// exposed on the (wrapped) nsISDBRequest are also wrapped. +// In particular, the wrapper takes responsibility for automatically cloning +// the ArrayBuffer returned by nsISDBResult.getAsArrayBuffer into the content +// compartment (rather than wrapping it) so that constructing a Uint8Array +// from it will succeed. + +function getSimpleDatabase() { + let connection = SpecialPowers.Cc[ + "@mozilla.org/dom/sdb-connection;1" + ].createInstance(SpecialPowers.Ci.nsISDBConnection); + + let principal = SpecialPowers.wrap(document).nodePrincipal; + + connection.init(principal); + + return connection; +} + +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/quota/test/common/file.js b/dom/quota/test/common/file.js new file mode 100644 index 0000000000..55e2e189fb --- /dev/null +++ b/dom/quota/test/common/file.js @@ -0,0 +1,45 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function getBuffer(size) { + let buffer = new ArrayBuffer(size); + is(buffer.byteLength, size, "Correct byte length"); + return buffer; +} + +// May be called for any size, but you should call getBuffer() if you know +// that size is big and that randomness is not necessary because it is +// noticeably faster. +function getRandomBuffer(size) { + let buffer = getBuffer(size); + let view = new Uint8Array(buffer); + for (let i = 0; i < size; i++) { + view[i] = parseInt(Math.random() * 255); + } + return buffer; +} + +function compareBuffers(buffer1, buffer2) { + if (buffer1.byteLength != buffer2.byteLength) { + return false; + } + + let view1 = buffer1 instanceof Uint8Array ? buffer1 : new Uint8Array(buffer1); + let view2 = buffer2 instanceof Uint8Array ? buffer2 : new Uint8Array(buffer2); + for (let i = 0; i < buffer1.byteLength; i++) { + if (view1[i] != view2[i]) { + return false; + } + } + return true; +} + +function getBlob(type, object) { + return new Blob([object], { type }); +} + +function getNullBlob(size) { + return getBlob("binary/null", getBuffer(size)); +} diff --git a/dom/quota/test/common/global.js b/dom/quota/test/common/global.js new file mode 100644 index 0000000000..bb0f3669be --- /dev/null +++ b/dom/quota/test/common/global.js @@ -0,0 +1,47 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const INT64_MIN = -0x8000000000000000n; + +class RequestError extends Error { + constructor(resultCode, resultName) { + super(`Request failed (code: ${resultCode}, name: ${resultName})`); + this.name = "RequestError"; + this.resultCode = resultCode; + this.resultName = resultName; + } +} + +function openDBRequestUpgradeNeeded(request) { + return new Promise(function(resolve, reject) { + request.onerror = function(event) { + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + reject(event); + }; + request.onupgradeneeded = function(event) { + resolve(event); + }; + request.onsuccess = function(event) { + ok(false, "Got success, but did not expect it!"); + reject(event); + }; + }); +} + +function openDBRequestSucceeded(request) { + return new Promise(function(resolve, reject) { + request.onerror = function(event) { + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + reject(event); + }; + request.onupgradeneeded = function(event) { + ok(false, "Got upgrade, but did not expect it!"); + reject(event); + }; + request.onsuccess = function(event) { + resolve(event); + }; + }); +} diff --git a/dom/quota/test/common/mochitest.js b/dom/quota/test/common/mochitest.js new file mode 100644 index 0000000000..1b867f6e92 --- /dev/null +++ b/dom/quota/test/common/mochitest.js @@ -0,0 +1,19 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadScript("dom/quota/test/common/content.js"); + +async function enableStorageTesting() { + let prefsToSet = [ + ["dom.quotaManager.testing", true], + ["dom.storageManager.enabled", true], + ["dom.simpleDB.enabled", true], + ]; + if (SpecialPowers.Services.appinfo.OS === "WINNT") { + prefsToSet.push(["dom.quotaManager.useDOSDevicePathSyntax", true]); + } + + await SpecialPowers.pushPrefEnv({ set: prefsToSet }); +} diff --git a/dom/quota/test/common/nestedtest.js b/dom/quota/test/common/nestedtest.js new file mode 100644 index 0000000000..5c2011bfe9 --- /dev/null +++ b/dom/quota/test/common/nestedtest.js @@ -0,0 +1,6 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadScript("dom/quota/test/common/content.js"); diff --git a/dom/quota/test/common/system.js b/dom/quota/test/common/system.js new file mode 100644 index 0000000000..25acbe9384 --- /dev/null +++ b/dom/quota/test/common/system.js @@ -0,0 +1,74 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const PR_USEC_PER_SEC = 1000000; + +const NS_ERROR_STORAGE_BUSY = Cr.NS_ERROR_STORAGE_BUSY; + +loadScript("dom/quota/test/common/global.js"); + +function getProfileDir() { + return Services.dirsvc.get("ProfD", Ci.nsIFile); +} + +// Given a "/"-delimited path relative to a base file (or the profile +// directory if a base file is not provided) 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, baseFile) { + if (!baseFile) { + baseFile = getProfileDir(); + } + + let file = baseFile.clone(); + + if (Services.appinfo.OS === "WINNT") { + let winFile = file.QueryInterface(Ci.nsILocalFileWin); + winFile.useDOSDevicePathSyntax = true; + } + + relativePath.split("/").forEach(function(component) { + if (component == "..") { + file = file.parent; + } else { + file.append(component); + } + }); + + return file; +} + +function getCurrentPrincipal() { + return Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal); +} + +function getSimpleDatabase(principal, persistence) { + let connection = Cc["@mozilla.org/dom/sdb-connection;1"].createInstance( + Ci.nsISDBConnection + ); + + if (!principal) { + principal = getCurrentPrincipal(); + } + + connection.init(principal, persistence); + + return connection; +} + +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; +} diff --git a/dom/quota/test/common/test_simpledb.js b/dom/quota/test/common/test_simpledb.js new file mode 100644 index 0000000000..dee7019097 --- /dev/null +++ b/dom/quota/test/common/test_simpledb.js @@ -0,0 +1,50 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadScript("dom/quota/test/common/file.js"); + +async function testSteps() { + const name = "data"; + const bufferSize = 100; + + let database = getSimpleDatabase(); + + let request = database.open(name); + await requestFinished(request); + + let buffer1 = getRandomBuffer(bufferSize); + + request = database.write(buffer1); + await requestFinished(request); + + request = database.seek(0); + await requestFinished(request); + + request = database.read(bufferSize); + let result = await requestFinished(request); + + let buffer2 = result.getAsArrayBuffer(); + + ok(compareBuffers(buffer1, buffer2), "Buffers equal."); + + let database2 = getSimpleDatabase(); + + try { + request = database2.open(name); + await requestFinished(request); + ok(false, "Should have thrown!"); + } catch (ex) { + ok(request.resultCode == NS_ERROR_STORAGE_BUSY, "Good result code."); + } + + request = database.close(); + await requestFinished(request); + + request = database2.open(name); + await requestFinished(request); + + request = database2.close(); + await requestFinished(request); +} diff --git a/dom/quota/test/common/test_storage_manager_persist_allow.js b/dom/quota/test/common/test_storage_manager_persist_allow.js new file mode 100644 index 0000000000..0a6e59843d --- /dev/null +++ b/dom/quota/test/common/test_storage_manager_persist_allow.js @@ -0,0 +1,30 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function* testSteps() { + SpecialPowers.pushPrefEnv( + { + set: [["dom.storageManager.prompt.testing.allow", true]], + }, + continueToNextStep + ); + yield undefined; + + navigator.storage.persist().then(grabArgAndContinueHandler); + let persistResult = yield undefined; + + is(persistResult, true, "Persist succeeded"); + + navigator.storage.persisted().then(grabArgAndContinueHandler); + let persistedResult = yield undefined; + + is( + persistResult, + persistedResult, + "Persist/persisted results are consistent" + ); + + finishTest(); +} diff --git a/dom/quota/test/common/test_storage_manager_persist_deny.js b/dom/quota/test/common/test_storage_manager_persist_deny.js new file mode 100644 index 0000000000..855d739ca3 --- /dev/null +++ b/dom/quota/test/common/test_storage_manager_persist_deny.js @@ -0,0 +1,34 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function* testSteps() { + SpecialPowers.pushPrefEnv( + { + set: [["dom.storageManager.prompt.testing.allow", false]], + }, + continueToNextStep + ); + yield undefined; + + navigator.storage.persist().then(grabArgAndContinueHandler); + let persistResult = yield undefined; + + is( + persistResult, + false, + "Cancel the persist prompt and resolve a promise with false" + ); + + navigator.storage.persisted().then(grabArgAndContinueHandler); + let persistedResult = yield undefined; + + is( + persistResult, + persistedResult, + "Persist/persisted results are consistent" + ); + + finishTest(); +} diff --git a/dom/quota/test/common/test_storage_manager_persisted.js b/dom/quota/test/common/test_storage_manager_persisted.js new file mode 100644 index 0000000000..ebda93649a --- /dev/null +++ b/dom/quota/test/common/test_storage_manager_persisted.js @@ -0,0 +1,13 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function* testSteps() { + navigator.storage.persisted().then(grabArgAndContinueHandler); + let persistedResult = yield undefined; + + is(persistedResult, false, "Persisted returns false"); + + finishTest(); +} diff --git a/dom/quota/test/common/xpcshell.js b/dom/quota/test/common/xpcshell.js new file mode 100644 index 0000000000..ed3afaa467 --- /dev/null +++ b/dom/quota/test/common/xpcshell.js @@ -0,0 +1,91 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadScript("dom/quota/test/common/system.js"); + +function enableStorageTesting() { + Services.prefs.setBoolPref("dom.quotaManager.testing", true); + Services.prefs.setBoolPref("dom.storageManager.enabled", true); + Services.prefs.setBoolPref("dom.simpleDB.enabled", true); + if (Services.appinfo.OS === "WINNT") { + Services.prefs.setBoolPref("dom.quotaManager.useDOSDevicePathSyntax", true); + } +} + +function resetStorageTesting() { + Services.prefs.clearUserPref("dom.quotaManager.testing"); + Services.prefs.clearUserPref("dom.storageManager.enabled"); + Services.prefs.clearUserPref("dom.simpleDB.enabled"); + if (Services.appinfo.OS === "WINNT") { + Services.prefs.clearUserPref("dom.quotaManager.useDOSDevicePathSyntax"); + } +} + +function clear(callback) { + let request = Services.qms.clear(); + request.callback = callback; + + return request; +} + +function reset(callback) { + let request = Services.qms.reset(); + request.callback = callback; + + return request; +} + +function installPackage(packageRelativePath, allowFileOverwrites) { + let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + + let packageFile = getRelativeFile(packageRelativePath + ".zip", currentDir); + + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipReader.open(packageFile); + + let entryNames = Array.from(zipReader.findEntries(null)); + entryNames.sort(); + + for (let entryName of entryNames) { + if (entryName.match(/^create_db\.(html|js)/)) { + continue; + } + + let zipentry = zipReader.getEntry(entryName); + + let file = getRelativeFile(entryName); + + if (zipentry.isDirectory) { + if (!file.exists()) { + file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } + } else { + if (!allowFileOverwrites && file.exists()) { + throw new Error("File already exists!"); + } + + 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(); +} diff --git a/dom/quota/test/gtest/Common.cpp b/dom/quota/test/gtest/Common.cpp new file mode 100644 index 0000000000..efbfd94775 --- /dev/null +++ b/dom/quota/test/gtest/Common.cpp @@ -0,0 +1,28 @@ +/* -*- 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 "Common.h" + +#include "mozilla/dom/QMResult.h" + +namespace mozilla::dom::quota { + +#ifdef QM_ERROR_STACKS_ENABLED +uint64_t DOM_Quota_Test::sExpectedStackId; + +// static +void DOM_Quota_Test::SetUpTestCase() { + sExpectedStackId = QMResult().StackId(); +} + +// static +void DOM_Quota_Test::IncreaseExpectedStackId() { sExpectedStackId++; } + +// static +uint64_t DOM_Quota_Test::ExpectedStackId() { return sExpectedStackId; } +#endif + +} // namespace mozilla::dom::quota diff --git a/dom/quota/test/gtest/Common.h b/dom/quota/test/gtest/Common.h new file mode 100644 index 0000000000..eac58bd7fb --- /dev/null +++ b/dom/quota/test/gtest/Common.h @@ -0,0 +1,32 @@ +/* -*- 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_QUOTA_TEST_GTEST_COMMON_H_ +#define DOM_QUOTA_TEST_GTEST_COMMON_H_ + +#include <cstdint> +#include "gtest/gtest.h" +#include "mozilla/dom/quota/Config.h" + +namespace mozilla::dom::quota { + +class DOM_Quota_Test : public testing::Test { +#ifdef QM_ERROR_STACKS_ENABLED + public: + static void SetUpTestCase(); + + static void IncreaseExpectedStackId(); + + static uint64_t ExpectedStackId(); + + private: + static uint64_t sExpectedStackId; +#endif +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_TEST_GTEST_COMMON_H_ diff --git a/dom/quota/test/gtest/PQuotaTest.ipdl b/dom/quota/test/gtest/PQuotaTest.ipdl new file mode 100644 index 0000000000..e4ca37325a --- /dev/null +++ b/dom/quota/test/gtest/PQuotaTest.ipdl @@ -0,0 +1,26 @@ +/* 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/. */ + +namespace mozilla { +namespace dom { +namespace quota { + +sync protocol PQuotaTest { + parent: + sync Try_Success_CustomErr_QmIpcFail() + returns (bool tryDidNotReturn); + + sync Try_Success_CustomErr_IpcFail() + returns (bool tryDidNotReturn); + + sync TryInspect_Success_CustomErr_QmIpcFail() + returns (bool tryDidNotReturn); + + sync TryInspect_Success_CustomErr_IpcFail() + returns (bool tryDidNotReturn); +}; + +} // namespace quota +} // namespace dom +} // namespace mozilla diff --git a/dom/quota/test/gtest/QuotaManagerDependencyFixture.cpp b/dom/quota/test/gtest/QuotaManagerDependencyFixture.cpp new file mode 100644 index 0000000000..2e52b4b69a --- /dev/null +++ b/dom/quota/test/gtest/QuotaManagerDependencyFixture.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 "QuotaManagerDependencyFixture.h" + +#include "mozIStorageService.h" +#include "mozStorageCID.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/quota/QuotaManagerService.h" +#include "mozilla/gtest/MozAssertions.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsIQuotaCallbacks.h" +#include "nsIQuotaRequests.h" +#include "nsIVariant.h" +#include "nsScriptSecurityManager.h" + +namespace mozilla::dom::quota::test { + +namespace { + +class RequestResolver final : public nsIQuotaCallback { + public: + RequestResolver() : mDone(false) {} + + bool Done() const { return mDone; } + + NS_DECL_ISUPPORTS + + NS_IMETHOD OnComplete(nsIQuotaRequest* aRequest) override { + mDone = true; + + return NS_OK; + } + + private: + ~RequestResolver() = default; + + bool mDone; +}; + +} // namespace + +NS_IMPL_ISUPPORTS(RequestResolver, nsIQuotaCallback) + +// static +void QuotaManagerDependencyFixture::InitializeFixture() { + // Some QuotaManagerService methods fail if the testing pref is not set. + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + + prefs->SetBoolPref("dom.quotaManager.testing", true); + + // The first initialization of storage service must be done on the main + // thread. + nsCOMPtr<mozIStorageService> storageService = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); + ASSERT_TRUE(storageService); + + nsIObserver* observer = QuotaManager::GetObserver(); + ASSERT_TRUE(observer); + + nsresult rv = observer->Observe(nullptr, "profile-do-change", nullptr); + ASSERT_NS_SUCCEEDED(rv); + + ASSERT_NO_FATAL_FAILURE(StorageInitialized()); + + QuotaManager* quotaManager = QuotaManager::Get(); + ASSERT_TRUE(quotaManager); + + ASSERT_TRUE(quotaManager->OwningThread()); + + sBackgroundTarget = quotaManager->OwningThread(); +} + +// static +void QuotaManagerDependencyFixture::ShutdownFixture() { + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + + prefs->SetBoolPref("dom.quotaManager.testing", false); + + nsIObserver* observer = QuotaManager::GetObserver(); + ASSERT_TRUE(observer); + + nsresult rv = observer->Observe(nullptr, "profile-before-change-qm", nullptr); + ASSERT_NS_SUCCEEDED(rv); + + PerformOnBackgroundThread([]() { QuotaManager::Reset(); }); + + sBackgroundTarget = nullptr; +} + +// static +void QuotaManagerDependencyFixture::StorageInitialized(bool* aResult) { + AutoJSAPI jsapi; + + bool ok = jsapi.Init(xpc::PrivilegedJunkScope()); + ASSERT_TRUE(ok); + + nsCOMPtr<nsIQuotaManagerService> qms = QuotaManagerService::GetOrCreate(); + ASSERT_TRUE(qms); + + nsCOMPtr<nsIQuotaRequest> request; + nsresult rv = qms->StorageInitialized(getter_AddRefs(request)); + ASSERT_NS_SUCCEEDED(rv); + + RefPtr<RequestResolver> resolver = new RequestResolver(); + + rv = request->SetCallback(resolver); + ASSERT_NS_SUCCEEDED(rv); + + SpinEventLoopUntil("Promise is fulfilled"_ns, + [&resolver]() { return resolver->Done(); }); + + if (aResult) { + nsCOMPtr<nsIVariant> result; + rv = request->GetResult(getter_AddRefs(result)); + ASSERT_NS_SUCCEEDED(rv); + + rv = result->GetAsBool(aResult); + ASSERT_NS_SUCCEEDED(rv); + } +} + +// static +void QuotaManagerDependencyFixture::ClearStoragesForOrigin( + const OriginMetadata& aOriginMetadata) { + nsCOMPtr<nsIQuotaManagerService> qms = QuotaManagerService::GetOrCreate(); + ASSERT_TRUE(qms); + + nsCOMPtr<nsIScriptSecurityManager> ssm = + nsScriptSecurityManager::GetScriptSecurityManager(); + ASSERT_TRUE(ssm); + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = ssm->CreateContentPrincipalFromOrigin( + aOriginMetadata.mOrigin, getter_AddRefs(principal)); + ASSERT_NS_SUCCEEDED(rv); + + nsCOMPtr<nsIQuotaRequest> request; + rv = qms->ClearStoragesForPrincipal(principal, VoidCString(), VoidString(), + /* aClearAll */ false, + getter_AddRefs(request)); + ASSERT_NS_SUCCEEDED(rv); + + RefPtr<RequestResolver> resolver = new RequestResolver(); + ASSERT_TRUE(resolver); + + rv = request->SetCallback(resolver); + ASSERT_NS_SUCCEEDED(rv); + + SpinEventLoopUntil("Promise is fulfilled"_ns, + [&resolver]() { return resolver->Done(); }); +} + +nsCOMPtr<nsISerialEventTarget> QuotaManagerDependencyFixture::sBackgroundTarget; + +} // namespace mozilla::dom::quota::test diff --git a/dom/quota/test/gtest/QuotaManagerDependencyFixture.h b/dom/quota/test/gtest/QuotaManagerDependencyFixture.h new file mode 100644 index 0000000000..4d57952242 --- /dev/null +++ b/dom/quota/test/gtest/QuotaManagerDependencyFixture.h @@ -0,0 +1,87 @@ +/* -*- 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_QUOTA_TEST_GTEST_QUOTAMANAGERDEPENDENCYFIXTURE_H_ +#define DOM_QUOTA_TEST_GTEST_QUOTAMANAGERDEPENDENCYFIXTURE_H_ + +#include "gtest/gtest.h" +#include "mozilla/MozPromise.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/dom/quota/ForwardDecls.h" +#include "mozilla/dom/quota/QuotaManager.h" + +namespace mozilla::dom::quota::test { + +class QuotaManagerDependencyFixture : public testing::Test { + public: + protected: + static void InitializeFixture(); + + static void ShutdownFixture(); + + static void StorageInitialized(bool* aResult = nullptr); + + static void ClearStoragesForOrigin(const OriginMetadata& aOriginMetadata); + + /* Convenience method for tasks which must be called on PBackground thread */ + template <class Invokable, class... Args> + static void PerformOnBackgroundThread(Invokable&& aInvokable, + Args&&... aArgs) { + bool done = false; + auto boundTask = + // For c++17, bind is cleaner than tuple for parameter pack forwarding + // NOLINTNEXTLINE(modernize-avoid-bind) + std::bind(std::forward<Invokable>(aInvokable), + std::forward<Args>(aArgs)...); + InvokeAsync(BackgroundTargetStrongRef(), __func__, + [boundTask = std::move(boundTask)] { + boundTask(); + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [&done](const BoolPromise::ResolveOrRejectValue& /* aValue */) { + done = true; + }); + + SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; }); + } + + /* Convenience method for tasks which must be executed on IO thread */ + template <class Invokable, class... Args> + static void PerformOnIOThread(Invokable&& aInvokable, Args&&... aArgs) { + QuotaManager* quotaManager = QuotaManager::Get(); + ASSERT_TRUE(quotaManager); + + bool done = false; + auto boundTask = + // For c++17, bind is cleaner than tuple for parameter pack forwarding + // NOLINTNEXTLINE(modernize-avoid-bind) + std::bind(std::forward<Invokable>(aInvokable), + std::forward<Args>(aArgs)...); + InvokeAsync(quotaManager->IOThread(), __func__, + [boundTask = std::move(boundTask)]() { + boundTask(); + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [&done](const BoolPromise::ResolveOrRejectValue& value) { + done = true; + }); + + SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; }); + } + + static const nsCOMPtr<nsISerialEventTarget>& BackgroundTargetStrongRef() { + return sBackgroundTarget; + } + + private: + static nsCOMPtr<nsISerialEventTarget> sBackgroundTarget; +}; + +} // namespace mozilla::dom::quota::test + +#endif // DOM_QUOTA_TEST_GTEST_QUOTAMANAGERDEPENDENCYFIXTURE_H_ diff --git a/dom/quota/test/gtest/QuotaTestChild.h b/dom/quota/test/gtest/QuotaTestChild.h new file mode 100644 index 0000000000..d0d87d8267 --- /dev/null +++ b/dom/quota/test/gtest/QuotaTestChild.h @@ -0,0 +1,23 @@ +/* -*- 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_QUOTA_TEST_GTEST_QUOTATESTCHILD_H_ +#define DOM_QUOTA_TEST_GTEST_QUOTATESTCHILD_H_ + +#include "mozilla/dom/quota/PQuotaTestChild.h" + +namespace mozilla::dom::quota { + +class QuotaTestChild : public PQuotaTestChild { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(QuotaTestChild, override) + + private: + ~QuotaTestChild() = default; +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_TEST_GTEST_QUOTATESTCHILD_H_ diff --git a/dom/quota/test/gtest/QuotaTestParent.h b/dom/quota/test/gtest/QuotaTestParent.h new file mode 100644 index 0000000000..5ec1e128b3 --- /dev/null +++ b/dom/quota/test/gtest/QuotaTestParent.h @@ -0,0 +1,36 @@ +/* -*- 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_QUOTA_TEST_GTEST_QUOTATESTPARENT_H_ +#define DOM_QUOTA_TEST_GTEST_QUOTATESTPARENT_H_ + +#include "mozilla/dom/quota/PQuotaTestParent.h" + +namespace mozilla::dom::quota { + +class QuotaTestParent : public PQuotaTestParent { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(QuotaTestParent, override) + + public: + mozilla::ipc::IPCResult RecvTry_Success_CustomErr_QmIpcFail( + bool* aTryDidNotReturn); + + mozilla::ipc::IPCResult RecvTry_Success_CustomErr_IpcFail( + bool* aTryDidNotReturn); + + mozilla::ipc::IPCResult RecvTryInspect_Success_CustomErr_QmIpcFail( + bool* aTryDidNotReturn); + + mozilla::ipc::IPCResult RecvTryInspect_Success_CustomErr_IpcFail( + bool* aTryDidNotReturn); + + private: + ~QuotaTestParent() = default; +}; + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_TEST_GTEST_QUOTATESTPARENT_H_ diff --git a/dom/quota/test/gtest/TestCheckedUnsafePtr.cpp b/dom/quota/test/gtest/TestCheckedUnsafePtr.cpp new file mode 100644 index 0000000000..14abb6631d --- /dev/null +++ b/dom/quota/test/gtest/TestCheckedUnsafePtr.cpp @@ -0,0 +1,145 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/quota/CheckedUnsafePtr.h" + +#include "gtest/gtest.h" + +#include <memory> +#include <type_traits> +#include <utility> +#include "mozilla/fallible.h" + +using namespace mozilla; + +class NoCheckTestType + : public SupportsCheckedUnsafePtr<DoNotCheckCheckedUnsafePtrs> {}; + +#if __cplusplus < 202002L +static_assert(std::is_literal_type_v<CheckedUnsafePtr<NoCheckTestType>>); +#endif + +static_assert( + std::is_trivially_copy_constructible_v<CheckedUnsafePtr<NoCheckTestType>>); +static_assert( + std::is_trivially_copy_assignable_v<CheckedUnsafePtr<NoCheckTestType>>); +static_assert( + std::is_trivially_move_constructible_v<CheckedUnsafePtr<NoCheckTestType>>); +static_assert( + std::is_trivially_move_assignable_v<CheckedUnsafePtr<NoCheckTestType>>); + +class TestCheckingPolicy : public CheckCheckedUnsafePtrs<TestCheckingPolicy> { + protected: + explicit TestCheckingPolicy(bool& aPassedCheck) + : mPassedCheck(aPassedCheck) {} + + private: + friend class mozilla::CheckingPolicyAccess; + void NotifyCheckFailure() { mPassedCheck = false; } + + bool& mPassedCheck; +}; + +struct BasePointee : public SupportsCheckedUnsafePtr<TestCheckingPolicy> { + explicit BasePointee(bool& aCheckPassed) + : SupportsCheckedUnsafePtr<TestCheckingPolicy>(aCheckPassed) {} +}; + +struct DerivedPointee : public BasePointee { + using BasePointee::BasePointee; +}; + +class CheckedUnsafePtrTest : public ::testing::Test { + protected: + bool mPassedCheck = true; +}; + +TEST_F(CheckedUnsafePtrTest, PointeeWithNoCheckedUnsafePtrs) { + { DerivedPointee pointee{mPassedCheck}; } + ASSERT_TRUE(mPassedCheck); +} + +template <typename PointerType> +class TypedCheckedUnsafePtrTest : public CheckedUnsafePtrTest {}; + +TYPED_TEST_SUITE_P(TypedCheckedUnsafePtrTest); + +TYPED_TEST_P(TypedCheckedUnsafePtrTest, PointeeWithOneCheckedUnsafePtr) { + { + DerivedPointee pointee{this->mPassedCheck}; + CheckedUnsafePtr<TypeParam> ptr = &pointee; + } + ASSERT_TRUE(this->mPassedCheck); +} + +TYPED_TEST_P(TypedCheckedUnsafePtrTest, CheckedUnsafePtrCopyConstructed) { + { + DerivedPointee pointee{this->mPassedCheck}; + CheckedUnsafePtr<TypeParam> ptr1 = &pointee; + CheckedUnsafePtr<TypeParam> ptr2 = ptr1; + } + ASSERT_TRUE(this->mPassedCheck); +} + +TYPED_TEST_P(TypedCheckedUnsafePtrTest, CheckedUnsafePtrCopyAssigned) { + { + DerivedPointee pointee{this->mPassedCheck}; + CheckedUnsafePtr<TypeParam> ptr1 = &pointee; + CheckedUnsafePtr<TypeParam> ptr2; + ptr2 = ptr1; + } + ASSERT_TRUE(this->mPassedCheck); +} + +TYPED_TEST_P(TypedCheckedUnsafePtrTest, + PointeeWithOneDanglingCheckedUnsafePtr) { + [this]() -> CheckedUnsafePtr<TypeParam> { + DerivedPointee pointee{this->mPassedCheck}; + return &pointee; + }(); + ASSERT_FALSE(this->mPassedCheck); +} + +TYPED_TEST_P(TypedCheckedUnsafePtrTest, + PointeeWithOneCopiedDanglingCheckedUnsafePtr) { + const auto dangling1 = [this]() -> CheckedUnsafePtr<DerivedPointee> { + DerivedPointee pointee{this->mPassedCheck}; + return &pointee; + }(); + EXPECT_FALSE(this->mPassedCheck); + + // With AddressSanitizer we would hopefully detect if the copy constructor + // tries to add dangling2 to the now-gone pointee's unsafe pointer array. No + // promises though, since it might be optimized away. + CheckedUnsafePtr<TypeParam> dangling2{dangling1}; + ASSERT_TRUE(dangling2); +} + +TYPED_TEST_P(TypedCheckedUnsafePtrTest, + PointeeWithOneCopyAssignedDanglingCheckedUnsafePtr) { + const auto dangling1 = [this]() -> CheckedUnsafePtr<DerivedPointee> { + DerivedPointee pointee{this->mPassedCheck}; + return &pointee; + }(); + EXPECT_FALSE(this->mPassedCheck); + + // With AddressSanitizer we would hopefully detect if the assignment tries to + // add dangling2 to the now-gone pointee's unsafe pointer array. No promises + // though, since it might be optimized away. + CheckedUnsafePtr<TypeParam> dangling2; + dangling2 = dangling1; + ASSERT_TRUE(dangling2); +} + +REGISTER_TYPED_TEST_SUITE_P(TypedCheckedUnsafePtrTest, + PointeeWithOneCheckedUnsafePtr, + CheckedUnsafePtrCopyConstructed, + CheckedUnsafePtrCopyAssigned, + PointeeWithOneDanglingCheckedUnsafePtr, + PointeeWithOneCopiedDanglingCheckedUnsafePtr, + PointeeWithOneCopyAssignedDanglingCheckedUnsafePtr); + +using BothTypes = ::testing::Types<BasePointee, DerivedPointee>; +INSTANTIATE_TYPED_TEST_SUITE_P(InstantiationOf, TypedCheckedUnsafePtrTest, + BothTypes); diff --git a/dom/quota/test/gtest/TestClientUsageArray.cpp b/dom/quota/test/gtest/TestClientUsageArray.cpp new file mode 100644 index 0000000000..f5db984ccd --- /dev/null +++ b/dom/quota/test/gtest/TestClientUsageArray.cpp @@ -0,0 +1,17 @@ +/* -*- 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 "ClientUsageArray.h" +#include "gtest/gtest.h" + +using namespace mozilla::dom::quota; + +TEST(DOM_Quota_ClientUsageArray, Deserialize) +{ + ClientUsageArray clientUsages; + nsresult rv = clientUsages.Deserialize("I872215 C8404073805 L161709"_ns); + ASSERT_EQ(rv, NS_OK); +} diff --git a/dom/quota/test/gtest/TestEncryptedStream.cpp b/dom/quota/test/gtest/TestEncryptedStream.cpp new file mode 100644 index 0000000000..b5e64f2cf1 --- /dev/null +++ b/dom/quota/test/gtest/TestEncryptedStream.cpp @@ -0,0 +1,786 @@ +/* -*- 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 <algorithm> +#include <cstdint> +#include <cstdlib> +#include <new> +#include <numeric> +#include <ostream> +#include <string> +#include <type_traits> +#include <utility> +#include <vector> +#include "ErrorList.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/FixedBufferOutputStream.h" +#include "mozilla/NotNull.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Scoped.h" +#include "mozilla/Span.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/SafeRefPtr.h" +#include "mozilla/dom/quota/DecryptingInputStream_impl.h" +#include "mozilla/dom/quota/DummyCipherStrategy.h" +#include "mozilla/dom/quota/EncryptedBlock.h" +#include "mozilla/dom/quota/EncryptingOutputStream_impl.h" +#include "mozilla/dom/quota/NSSCipherStrategy.h" +#include "mozilla/fallible.h" +#include "nsCOMPtr.h" +#include "nsError.h" +#include "nsICloneableInputStream.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsISeekableStream.h" +#include "nsISupports.h" +#include "nsITellableStream.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nscore.h" +#include "nss.h" + +namespace mozilla::dom::quota { + +// Similar to ArrayBufferInputStream from netwerk/base/ArrayBufferInputStream.h, +// but this is initialized from a Span on construction, rather than lazily from +// a JS ArrayBuffer. +class ArrayBufferInputStream : public nsIInputStream, + public nsISeekableStream, + public nsICloneableInputStream { + public: + explicit ArrayBufferInputStream(mozilla::Span<const uint8_t> aData); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSITELLABLESTREAM + NS_DECL_NSISEEKABLESTREAM + NS_DECL_NSICLONEABLEINPUTSTREAM + + private: + virtual ~ArrayBufferInputStream() = default; + + mozilla::UniquePtr<char[]> mArrayBuffer; + uint32_t mBufferLength; + uint32_t mPos; + bool mClosed; +}; + +NS_IMPL_ADDREF(ArrayBufferInputStream); +NS_IMPL_RELEASE(ArrayBufferInputStream); + +NS_INTERFACE_MAP_BEGIN(ArrayBufferInputStream) + NS_INTERFACE_MAP_ENTRY(nsIInputStream) + NS_INTERFACE_MAP_ENTRY(nsISeekableStream) + NS_INTERFACE_MAP_ENTRY(nsICloneableInputStream) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream) +NS_INTERFACE_MAP_END + +ArrayBufferInputStream::ArrayBufferInputStream( + mozilla::Span<const uint8_t> aData) + : mArrayBuffer(MakeUnique<char[]>(aData.Length())), + mBufferLength(aData.Length()), + mPos(0), + mClosed(false) { + std::copy(aData.cbegin(), aData.cend(), mArrayBuffer.get()); +} + +NS_IMETHODIMP +ArrayBufferInputStream::Close() { + mClosed = true; + return NS_OK; +} + +NS_IMETHODIMP +ArrayBufferInputStream::Available(uint64_t* aCount) { + if (mClosed) { + return NS_BASE_STREAM_CLOSED; + } + + if (mArrayBuffer) { + *aCount = mBufferLength ? mBufferLength - mPos : 0; + } else { + *aCount = 0; + } + + return NS_OK; +} + +NS_IMETHODIMP +ArrayBufferInputStream::Read(char* aBuf, uint32_t aCount, + uint32_t* aReadCount) { + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, aReadCount); +} + +NS_IMETHODIMP +ArrayBufferInputStream::ReadSegments(nsWriteSegmentFun writer, void* closure, + uint32_t aCount, uint32_t* result) { + MOZ_ASSERT(result, "null ptr"); + MOZ_ASSERT(mBufferLength >= mPos, "bad stream state"); + + if (mClosed) { + return NS_BASE_STREAM_CLOSED; + } + + MOZ_ASSERT(mArrayBuffer || (mPos == mBufferLength), + "stream inited incorrectly"); + + *result = 0; + while (mPos < mBufferLength) { + uint32_t remaining = mBufferLength - mPos; + MOZ_ASSERT(mArrayBuffer); + + uint32_t count = std::min(aCount, remaining); + if (count == 0) { + break; + } + + uint32_t written; + nsresult rv = writer(this, closure, &mArrayBuffer[0] + mPos, *result, count, + &written); + if (NS_FAILED(rv)) { + // InputStreams do not propagate errors to caller. + return NS_OK; + } + + MOZ_ASSERT(written <= count, + "writer should not write more than we asked it to write"); + mPos += written; + *result += written; + aCount -= written; + } + + return NS_OK; +} + +NS_IMETHODIMP +ArrayBufferInputStream::IsNonBlocking(bool* aNonBlocking) { + // Actually, the stream never blocks, but we lie about it because of the + // assumptions in DecryptingInputStream. + *aNonBlocking = false; + return NS_OK; +} + +NS_IMETHODIMP ArrayBufferInputStream::Tell(int64_t* const aRetval) { + MOZ_ASSERT(aRetval); + + *aRetval = mPos; + + return NS_OK; +} + +NS_IMETHODIMP ArrayBufferInputStream::Seek(const int32_t aWhence, + const int64_t aOffset) { + // XXX This is not safe. it's hard to use CheckedInt here, though. As long as + // the class is only used for testing purposes, that's probably fine. + + int32_t newPos = mPos; + switch (aWhence) { + case NS_SEEK_SET: + newPos = aOffset; + break; + case NS_SEEK_CUR: + newPos += aOffset; + break; + case NS_SEEK_END: + newPos = mBufferLength; + newPos += aOffset; + break; + default: + return NS_ERROR_ILLEGAL_VALUE; + } + if (newPos < 0 || static_cast<uint32_t>(newPos) > mBufferLength) { + return NS_ERROR_ILLEGAL_VALUE; + } + mPos = newPos; + + return NS_OK; +} + +NS_IMETHODIMP ArrayBufferInputStream::SetEOF() { + // Truncating is not supported on a read-only stream. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ArrayBufferInputStream::GetCloneable(bool* aCloneable) { + *aCloneable = true; + return NS_OK; +} + +NS_IMETHODIMP ArrayBufferInputStream::Clone(nsIInputStream** _retval) { + *_retval = MakeAndAddRef<ArrayBufferInputStream>( + AsBytes(Span{mArrayBuffer.get(), mBufferLength})) + .take(); + + return NS_OK; +} +} // namespace mozilla::dom::quota + +namespace mozilla { +MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedNSSContext, NSSInitContext, + NSS_ShutdownContext); + +} // namespace mozilla + +using namespace mozilla; +using namespace mozilla::dom::quota; + +class DOM_Quota_EncryptedStream : public ::testing::Test { + public: + static void SetUpTestCase() { + // Do this only once, do not tear it down per test case. + if (!sNssContext) { + sNssContext = + NSS_InitContext("", "", "", "", nullptr, + NSS_INIT_READONLY | NSS_INIT_NOCERTDB | + NSS_INIT_NOMODDB | NSS_INIT_FORCEOPEN | + NSS_INIT_OPTIMIZESPACE | NSS_INIT_NOROOTINIT); + } + } + + static void TearDownTestCase() { sNssContext = nullptr; } + + private: + inline static ScopedNSSContext sNssContext = ScopedNSSContext{}; +}; + +enum struct FlushMode { AfterEachChunk, Never }; +enum struct ChunkSize { SingleByte, Unaligned, DataSize }; + +using PackedTestParams = + std::tuple<size_t, ChunkSize, ChunkSize, size_t, FlushMode>; + +static size_t EffectiveChunkSize(const ChunkSize aChunkSize, + const size_t aDataSize) { + switch (aChunkSize) { + case ChunkSize::SingleByte: + return 1; + case ChunkSize::Unaligned: + return 17; + case ChunkSize::DataSize: + return aDataSize; + } + MOZ_CRASH("Unknown ChunkSize"); +} + +struct TestParams { + MOZ_IMPLICIT constexpr TestParams(const PackedTestParams& aPackedParams) + : mDataSize(std::get<0>(aPackedParams)), + mWriteChunkSize(std::get<1>(aPackedParams)), + mReadChunkSize(std::get<2>(aPackedParams)), + mBlockSize(std::get<3>(aPackedParams)), + mFlushMode(std::get<4>(aPackedParams)) {} + + constexpr size_t DataSize() const { return mDataSize; } + + size_t EffectiveWriteChunkSize() const { + return EffectiveChunkSize(mWriteChunkSize, mDataSize); + } + + size_t EffectiveReadChunkSize() const { + return EffectiveChunkSize(mReadChunkSize, mDataSize); + } + + constexpr size_t BlockSize() const { return mBlockSize; } + + constexpr enum FlushMode FlushMode() const { return mFlushMode; } + + private: + size_t mDataSize; + + ChunkSize mWriteChunkSize; + ChunkSize mReadChunkSize; + + size_t mBlockSize; + enum FlushMode mFlushMode; +}; + +std::string TestParamToString( + const testing::TestParamInfo<PackedTestParams>& aTestParams) { + const TestParams& testParams = aTestParams.param; + + static constexpr char kSeparator[] = "_"; + + std::stringstream ss; + ss << "data" << testParams.DataSize() << kSeparator << "writechunk" + << testParams.EffectiveWriteChunkSize() << kSeparator << "readchunk" + << testParams.EffectiveReadChunkSize() << kSeparator << "block" + << testParams.BlockSize() << kSeparator; + switch (testParams.FlushMode()) { + case FlushMode::Never: + ss << "FlushNever"; + break; + case FlushMode::AfterEachChunk: + ss << "FlushAfterEachChunk"; + break; + }; + return ss.str(); +} + +class ParametrizedCryptTest + : public DOM_Quota_EncryptedStream, + public testing::WithParamInterface<PackedTestParams> {}; + +static auto MakeTestData(const size_t aDataSize) { + auto data = nsTArray<uint8_t>(); + data.SetLength(aDataSize); + std::iota(data.begin(), data.end(), 0); + return data; +} + +template <typename CipherStrategy> +static void WriteTestData(nsCOMPtr<nsIOutputStream>&& aBaseOutputStream, + const Span<const uint8_t> aData, + const size_t aWriteChunkSize, const size_t aBlockSize, + const typename CipherStrategy::KeyType& aKey, + const FlushMode aFlushMode) { + auto outStream = MakeSafeRefPtr<EncryptingOutputStream<CipherStrategy>>( + std::move(aBaseOutputStream), aBlockSize, aKey); + + for (auto remaining = aData; !remaining.IsEmpty();) { + auto [currentChunk, newRemaining] = + remaining.SplitAt(std::min(aWriteChunkSize, remaining.Length())); + remaining = newRemaining; + + uint32_t written; + EXPECT_EQ(NS_OK, outStream->Write( + reinterpret_cast<const char*>(currentChunk.Elements()), + currentChunk.Length(), &written)); + EXPECT_EQ(currentChunk.Length(), written); + + if (aFlushMode == FlushMode::AfterEachChunk) { + outStream->Flush(); + } + } + + // Close explicitly so we can check the result. + EXPECT_EQ(NS_OK, outStream->Close()); +} + +template <typename CipherStrategy> +static void NoExtraChecks(DecryptingInputStream<CipherStrategy>& aInputStream, + Span<const uint8_t> aExpectedData, + Span<const uint8_t> aRemainder) {} + +template <typename CipherStrategy, + typename ExtraChecks = decltype(NoExtraChecks<CipherStrategy>)> +static void ReadTestData( + DecryptingInputStream<CipherStrategy>& aDecryptingInputStream, + const Span<const uint8_t> aExpectedData, const size_t aReadChunkSize, + const ExtraChecks& aExtraChecks = NoExtraChecks<CipherStrategy>) { + auto readData = nsTArray<uint8_t>(); + readData.SetLength(aReadChunkSize); + for (auto remainder = aExpectedData; !remainder.IsEmpty();) { + auto [currentExpected, newExpectedRemainder] = + remainder.SplitAt(std::min(aReadChunkSize, remainder.Length())); + remainder = newExpectedRemainder; + + uint32_t read; + EXPECT_EQ(NS_OK, aDecryptingInputStream.Read( + reinterpret_cast<char*>(readData.Elements()), + currentExpected.Length(), &read)); + EXPECT_EQ(currentExpected.Length(), read); + EXPECT_EQ(currentExpected, + Span{readData}.First(currentExpected.Length()).AsConst()); + + aExtraChecks(aDecryptingInputStream, aExpectedData, remainder); + } + + // Expect EOF. + uint32_t read; + EXPECT_EQ(NS_OK, aDecryptingInputStream.Read( + reinterpret_cast<char*>(readData.Elements()), + readData.Length(), &read)); + EXPECT_EQ(0u, read); +} + +template <typename CipherStrategy, + typename ExtraChecks = decltype(NoExtraChecks<CipherStrategy>)> +static auto ReadTestData( + MovingNotNull<nsCOMPtr<nsIInputStream>>&& aBaseInputStream, + const Span<const uint8_t> aExpectedData, const size_t aReadChunkSize, + const size_t aBlockSize, const typename CipherStrategy::KeyType& aKey, + const ExtraChecks& aExtraChecks = NoExtraChecks<CipherStrategy>) { + auto inStream = MakeSafeRefPtr<DecryptingInputStream<CipherStrategy>>( + std::move(aBaseInputStream), aBlockSize, aKey); + + ReadTestData(*inStream, aExpectedData, aReadChunkSize, aExtraChecks); + + return inStream; +} + +// XXX Change to return the buffer instead. +template <typename CipherStrategy, + typename ExtraChecks = decltype(NoExtraChecks<CipherStrategy>)> +static RefPtr<FixedBufferOutputStream> DoRoundtripTest( + const size_t aDataSize, const size_t aWriteChunkSize, + const size_t aReadChunkSize, const size_t aBlockSize, + const typename CipherStrategy::KeyType& aKey, const FlushMode aFlushMode, + const ExtraChecks& aExtraChecks = NoExtraChecks<CipherStrategy>) { + // XXX Add deduction guide for RefPtr from already_AddRefed + const auto baseOutputStream = WrapNotNull( + RefPtr<FixedBufferOutputStream>{FixedBufferOutputStream::Create(2048)}); + + const auto data = MakeTestData(aDataSize); + + WriteTestData<CipherStrategy>( + nsCOMPtr<nsIOutputStream>{baseOutputStream.get()}, Span{data}, + aWriteChunkSize, aBlockSize, aKey, aFlushMode); + + const auto baseInputStream = + MakeRefPtr<ArrayBufferInputStream>(baseOutputStream->WrittenData()); + + ReadTestData<CipherStrategy>( + WrapNotNull(nsCOMPtr<nsIInputStream>{baseInputStream}), Span{data}, + aReadChunkSize, aBlockSize, aKey, aExtraChecks); + + return baseOutputStream; +} + +TEST_P(ParametrizedCryptTest, NSSCipherStrategy) { + using CipherStrategy = NSSCipherStrategy; + const TestParams& testParams = GetParam(); + + auto keyOrErr = CipherStrategy::GenerateKey(); + ASSERT_FALSE(keyOrErr.isErr()); + + DoRoundtripTest<CipherStrategy>( + testParams.DataSize(), testParams.EffectiveWriteChunkSize(), + testParams.EffectiveReadChunkSize(), testParams.BlockSize(), + keyOrErr.unwrap(), testParams.FlushMode()); +} + +TEST_P(ParametrizedCryptTest, DummyCipherStrategy_CheckOutput) { + using CipherStrategy = DummyCipherStrategy; + const TestParams& testParams = GetParam(); + + const auto encryptedDataStream = DoRoundtripTest<CipherStrategy>( + testParams.DataSize(), testParams.EffectiveWriteChunkSize(), + testParams.EffectiveReadChunkSize(), testParams.BlockSize(), + CipherStrategy::KeyType{}, testParams.FlushMode()); + + if (HasFailure()) { + return; + } + + const auto encryptedData = encryptedDataStream->WrittenData(); + const auto encryptedDataSpan = AsBytes(Span(encryptedData)); + + const auto plainTestData = MakeTestData(testParams.DataSize()); + auto encryptedBlock = EncryptedBlock<DummyCipherStrategy::BlockPrefixLength, + DummyCipherStrategy::BasicBlockSize>{ + testParams.BlockSize(), + }; + for (auto [encryptedRemainder, plainRemainder] = + std::pair(encryptedDataSpan, Span(plainTestData)); + !encryptedRemainder.IsEmpty();) { + const auto [currentBlock, newEncryptedRemainder] = + encryptedRemainder.SplitAt(testParams.BlockSize()); + encryptedRemainder = newEncryptedRemainder; + + std::copy(currentBlock.cbegin(), currentBlock.cend(), + encryptedBlock.MutableWholeBlock().begin()); + + ASSERT_FALSE(plainRemainder.IsEmpty()); + const auto [currentPlain, newPlainRemainder] = + plainRemainder.SplitAt(encryptedBlock.ActualPayloadLength()); + plainRemainder = newPlainRemainder; + + const auto pseudoIV = encryptedBlock.CipherPrefix(); + const auto payload = encryptedBlock.Payload(); + + EXPECT_EQ(Span(DummyCipherStrategy::MakeBlockPrefix()), pseudoIV); + + auto untransformedPayload = nsTArray<uint8_t>(); + untransformedPayload.SetLength(testParams.BlockSize()); + DummyCipherStrategy::DummyTransform(payload, untransformedPayload); + + EXPECT_EQ( + currentPlain, + Span(untransformedPayload).AsConst().First(currentPlain.Length())); + } +} + +TEST_P(ParametrizedCryptTest, DummyCipherStrategy_Tell) { + using CipherStrategy = DummyCipherStrategy; + const TestParams& testParams = GetParam(); + + DoRoundtripTest<CipherStrategy>( + testParams.DataSize(), testParams.EffectiveWriteChunkSize(), + testParams.EffectiveReadChunkSize(), testParams.BlockSize(), + CipherStrategy::KeyType{}, testParams.FlushMode(), + [](auto& inStream, Span<const uint8_t> expectedData, + Span<const uint8_t> remainder) { + // Check that Tell tells the right position. + int64_t pos; + EXPECT_EQ(NS_OK, inStream.Tell(&pos)); + EXPECT_EQ(expectedData.Length() - remainder.Length(), + static_cast<uint64_t>(pos)); + }); +} + +TEST_P(ParametrizedCryptTest, DummyCipherStrategy_Available) { + using CipherStrategy = DummyCipherStrategy; + const TestParams& testParams = GetParam(); + + DoRoundtripTest<CipherStrategy>( + testParams.DataSize(), testParams.EffectiveWriteChunkSize(), + testParams.EffectiveReadChunkSize(), testParams.BlockSize(), + CipherStrategy::KeyType{}, testParams.FlushMode(), + [](auto& inStream, Span<const uint8_t> expectedData, + Span<const uint8_t> remainder) { + // Check that Available tells the right remainder. + uint64_t available; + EXPECT_EQ(NS_OK, inStream.Available(&available)); + EXPECT_EQ(remainder.Length(), available); + }); +} + +TEST_P(ParametrizedCryptTest, DummyCipherStrategy_Clone) { + using CipherStrategy = DummyCipherStrategy; + const TestParams& testParams = GetParam(); + + // XXX Add deduction guide for RefPtr from already_AddRefed + const auto baseOutputStream = WrapNotNull( + RefPtr<FixedBufferOutputStream>{FixedBufferOutputStream::Create(2048)}); + + const auto data = MakeTestData(testParams.DataSize()); + + WriteTestData<CipherStrategy>( + nsCOMPtr<nsIOutputStream>{baseOutputStream.get()}, Span{data}, + testParams.EffectiveWriteChunkSize(), testParams.BlockSize(), + CipherStrategy::KeyType{}, testParams.FlushMode()); + + const auto baseInputStream = + MakeRefPtr<ArrayBufferInputStream>(baseOutputStream->WrittenData()); + + const auto inStream = ReadTestData<CipherStrategy>( + WrapNotNull(nsCOMPtr<nsIInputStream>{baseInputStream}), Span{data}, + testParams.EffectiveReadChunkSize(), testParams.BlockSize(), + CipherStrategy::KeyType{}); + + nsCOMPtr<nsIInputStream> clonedInputStream; + EXPECT_EQ(NS_OK, inStream->Clone(getter_AddRefs(clonedInputStream))); + + ReadTestData( + static_cast<DecryptingInputStream<CipherStrategy>&>(*clonedInputStream), + Span{data}, testParams.EffectiveReadChunkSize()); +} + +// XXX This test is actually only parametrized on the block size. +TEST_P(ParametrizedCryptTest, DummyCipherStrategy_IncompleteBlock) { + using CipherStrategy = DummyCipherStrategy; + const TestParams& testParams = GetParam(); + + // Provide half a block, content doesn't matter. + nsTArray<uint8_t> data; + data.SetLength(testParams.BlockSize() / 2); + + const auto baseInputStream = MakeRefPtr<ArrayBufferInputStream>(data); + + const auto inStream = MakeSafeRefPtr<DecryptingInputStream<CipherStrategy>>( + WrapNotNull(nsCOMPtr<nsIInputStream>{baseInputStream}), + testParams.BlockSize(), CipherStrategy::KeyType{}); + + nsTArray<uint8_t> readData; + readData.SetLength(testParams.BlockSize()); + uint32_t read; + EXPECT_EQ(NS_ERROR_CORRUPTED_CONTENT, + inStream->Read(reinterpret_cast<char*>(readData.Elements()), + readData.Length(), &read)); +} + +enum struct SeekOffset { + Zero, + MinusHalfDataSize, + PlusHalfDataSize, + PlusDataSize, + MinusDataSize +}; +using SeekOp = std::pair<int32_t, SeekOffset>; + +using PackedSeekTestParams = std::tuple<size_t, size_t, std::vector<SeekOp>>; + +struct SeekTestParams { + size_t mDataSize; + size_t mBlockSize; + std::vector<SeekOp> mSeekOps; + + MOZ_IMPLICIT SeekTestParams(const PackedSeekTestParams& aPackedParams) + : mDataSize(std::get<0>(aPackedParams)), + mBlockSize(std::get<1>(aPackedParams)), + mSeekOps(std::get<2>(aPackedParams)) {} +}; + +std::string SeekTestParamToString( + const testing::TestParamInfo<PackedSeekTestParams>& aTestParams) { + const SeekTestParams& testParams = aTestParams.param; + + static constexpr char kSeparator[] = "_"; + + std::stringstream ss; + ss << "data" << testParams.mDataSize << kSeparator << "writechunk" + << testParams.mBlockSize << kSeparator; + for (const auto& seekOp : testParams.mSeekOps) { + switch (seekOp.first) { + case nsISeekableStream::NS_SEEK_SET: + ss << "Set"; + break; + case nsISeekableStream::NS_SEEK_CUR: + ss << "Cur"; + break; + case nsISeekableStream::NS_SEEK_END: + ss << "End"; + break; + }; + switch (seekOp.second) { + case SeekOffset::Zero: + ss << "Zero"; + break; + case SeekOffset::MinusHalfDataSize: + ss << "MinusHalfDataSize"; + break; + case SeekOffset::PlusHalfDataSize: + ss << "PlusHalfDataSize"; + break; + case SeekOffset::MinusDataSize: + ss << "MinusDataSize"; + break; + case SeekOffset::PlusDataSize: + ss << "PlusDataSize"; + break; + }; + } + return ss.str(); +} + +class ParametrizedSeekCryptTest + : public DOM_Quota_EncryptedStream, + public testing::WithParamInterface<PackedSeekTestParams> {}; + +TEST_P(ParametrizedSeekCryptTest, DummyCipherStrategy_Seek) { + using CipherStrategy = DummyCipherStrategy; + const SeekTestParams& testParams = GetParam(); + + const auto baseOutputStream = WrapNotNull( + RefPtr<FixedBufferOutputStream>{FixedBufferOutputStream::Create(2048)}); + + const auto data = MakeTestData(testParams.mDataSize); + + WriteTestData<CipherStrategy>( + nsCOMPtr<nsIOutputStream>{baseOutputStream.get()}, Span{data}, + testParams.mDataSize, testParams.mBlockSize, CipherStrategy::KeyType{}, + FlushMode::Never); + + const auto baseInputStream = + MakeRefPtr<ArrayBufferInputStream>(baseOutputStream->WrittenData()); + + const auto inStream = MakeSafeRefPtr<DecryptingInputStream<CipherStrategy>>( + WrapNotNull(nsCOMPtr<nsIInputStream>{baseInputStream}), + testParams.mBlockSize, CipherStrategy::KeyType{}); + + uint32_t accumulatedOffset = 0; + for (const auto& seekOp : testParams.mSeekOps) { + const auto offset = [offsetKind = seekOp.second, + dataSize = testParams.mDataSize]() -> int64_t { + switch (offsetKind) { + case SeekOffset::Zero: + return 0; + case SeekOffset::MinusHalfDataSize: + return -static_cast<int64_t>(dataSize) / 2; + case SeekOffset::PlusHalfDataSize: + return dataSize / 2; + case SeekOffset::MinusDataSize: + return -static_cast<int64_t>(dataSize); + case SeekOffset::PlusDataSize: + return dataSize; + } + MOZ_CRASH("Unknown SeekOffset"); + }(); + switch (seekOp.first) { + case nsISeekableStream::NS_SEEK_SET: + accumulatedOffset = offset; + break; + case nsISeekableStream::NS_SEEK_CUR: + accumulatedOffset += offset; + break; + case nsISeekableStream::NS_SEEK_END: + accumulatedOffset = testParams.mDataSize + offset; + break; + } + EXPECT_EQ(NS_OK, inStream->Seek(seekOp.first, offset)); + } + + { + int64_t actualOffset; + EXPECT_EQ(NS_OK, inStream->Tell(&actualOffset)); + + EXPECT_EQ(actualOffset, accumulatedOffset); + } + + auto readData = nsTArray<uint8_t>(); + readData.SetLength(data.Length()); + uint32_t read; + EXPECT_EQ(NS_OK, inStream->Read(reinterpret_cast<char*>(readData.Elements()), + readData.Length(), &read)); + // XXX Or should 'read' indicate the actual number of bytes read, + // including the encryption overhead? + EXPECT_EQ(testParams.mDataSize - accumulatedOffset, read); + EXPECT_EQ(Span{data}.SplitAt(accumulatedOffset).second, + Span{readData}.First(read).AsConst()); +} + +INSTANTIATE_TEST_SUITE_P( + DOM_Quota_EncryptedStream_Parametrized, ParametrizedCryptTest, + testing::Combine( + /* dataSize */ testing::Values(0u, 16u, 256u, 512u, 513u), + /* writeChunkSize */ + testing::Values(ChunkSize::SingleByte, ChunkSize::Unaligned, + ChunkSize::DataSize), + /* readChunkSize */ + testing::Values(ChunkSize::SingleByte, ChunkSize::Unaligned, + ChunkSize::DataSize), + /* blockSize */ testing::Values(256u, 1024u /*, 8192u*/), + /* flushMode */ + testing::Values(FlushMode::Never, FlushMode::AfterEachChunk)), + TestParamToString); + +INSTANTIATE_TEST_SUITE_P( + DOM_IndexedDB_EncryptedStream_ParametrizedSeek, ParametrizedSeekCryptTest, + testing::Combine( + /* dataSize */ testing::Values(0u, 16u, 256u, 512u, 513u), + /* blockSize */ testing::Values(256u, 1024u /*, 8192u*/), + /* seekOperations */ + testing::Values(/* NS_SEEK_SET only, single ops */ + std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_SET, + SeekOffset::PlusDataSize}}, + std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_SET, + SeekOffset::PlusHalfDataSize}}, + /* NS_SEEK_SET only, multiple ops */ + std::vector<SeekOp>{ + {nsISeekableStream::NS_SEEK_SET, + SeekOffset::PlusHalfDataSize}, + {nsISeekableStream::NS_SEEK_SET, SeekOffset::Zero}}, + /* NS_SEEK_CUR only, single ops */ + std::vector<SeekOp>{ + {nsISeekableStream::NS_SEEK_CUR, SeekOffset::Zero}}, + std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_CUR, + SeekOffset::PlusDataSize}}, + std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_CUR, + SeekOffset::PlusHalfDataSize}}, + /* NS_SEEK_END only, single ops */ + std::vector<SeekOp>{ + {nsISeekableStream::NS_SEEK_END, SeekOffset::Zero}}, + std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_END, + SeekOffset::MinusDataSize}}, + std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_END, + SeekOffset::MinusHalfDataSize}})), + SeekTestParamToString); diff --git a/dom/quota/test/gtest/TestFileOutputStream.cpp b/dom/quota/test/gtest/TestFileOutputStream.cpp new file mode 100644 index 0000000000..b62f15363e --- /dev/null +++ b/dom/quota/test/gtest/TestFileOutputStream.cpp @@ -0,0 +1,182 @@ +/* -*- 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 "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/CommonMetadata.h" +#include "mozilla/dom/quota/FileStreams.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/gtest/MozAssertions.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "QuotaManagerDependencyFixture.h" + +namespace mozilla::dom::quota::test { + +class TestFileOutputStream : public QuotaManagerDependencyFixture { + public: + static void SetUpTestCase() { + ASSERT_NO_FATAL_FAILURE(InitializeFixture()); + + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + + prefs->SetIntPref("dom.quotaManager.temporaryStorage.fixedLimit", + mQuotaLimit); + } + + static void TearDownTestCase() { + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + + prefs->ClearUserPref("dom.quotaManager.temporaryStorage.fixedLimit"); + + ASSERT_NO_FATAL_FAILURE(ShutdownFixture()); + } + + static const int32_t mQuotaLimit = 8192; +}; + +TEST_F(TestFileOutputStream, extendFileStreamWithSetEOF) { + auto ioTask = []() { + quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); + + auto originMetadata = + quota::OriginMetadata{""_ns, "example.com"_ns, "http://example.com"_ns, + quota::PERSISTENCE_TYPE_DEFAULT}; + + { + ASSERT_NS_SUCCEEDED(quotaManager->EnsureStorageIsInitialized()); + + ASSERT_NS_SUCCEEDED(quotaManager->EnsureTemporaryStorageIsInitialized()); + + auto res = quotaManager->EnsureTemporaryOriginIsInitialized( + quota::PERSISTENCE_TYPE_DEFAULT, originMetadata); + ASSERT_TRUE(res.isOk()); + } + + const int64_t groupLimit = + static_cast<int64_t>(quotaManager->GetGroupLimit()); + ASSERT_TRUE(mQuotaLimit * 1024LL == groupLimit); + + // We don't use the tested stream itself to check the file size as it + // may report values which have not been written to disk. + RefPtr<quota::FileOutputStream> check = MakeRefPtr<quota::FileOutputStream>( + quota::PERSISTENCE_TYPE_DEFAULT, originMetadata, + quota::Client::Type::SDB); + + RefPtr<quota::FileOutputStream> stream = + MakeRefPtr<quota::FileOutputStream>(quota::PERSISTENCE_TYPE_DEFAULT, + originMetadata, + quota::Client::Type::SDB); + + { + auto testPathRes = quotaManager->GetDirectoryForOrigin( + quota::PERSISTENCE_TYPE_DEFAULT, originMetadata.mOrigin); + + ASSERT_TRUE(testPathRes.isOk()); + + nsCOMPtr<nsIFile> testPath = testPathRes.unwrap(); + + ASSERT_NS_SUCCEEDED(testPath->AppendRelativePath(u"sdb"_ns)); + + ASSERT_NS_SUCCEEDED( + testPath->AppendRelativePath(u"tTestFileOutputStream.txt"_ns)); + + bool exists = true; + ASSERT_NS_SUCCEEDED(testPath->Exists(&exists)); + + if (exists) { + ASSERT_NS_SUCCEEDED(testPath->Remove(/* recursive */ false)); + } + + ASSERT_NS_SUCCEEDED(testPath->Exists(&exists)); + ASSERT_FALSE(exists); + + ASSERT_NS_SUCCEEDED(testPath->Create(nsIFile::NORMAL_FILE_TYPE, 0666)); + + ASSERT_NS_SUCCEEDED(testPath->Exists(&exists)); + ASSERT_TRUE(exists); + + nsCOMPtr<nsIFile> checkPath; + ASSERT_NS_SUCCEEDED(testPath->Clone(getter_AddRefs(checkPath))); + + const int32_t IOFlags = -1; + const int32_t perm = -1; + const int32_t behaviorFlags = 0; + ASSERT_NS_SUCCEEDED(stream->Init(testPath, IOFlags, perm, behaviorFlags)); + + ASSERT_NS_SUCCEEDED(check->Init(testPath, IOFlags, perm, behaviorFlags)); + } + + // Check that we start with an empty file + int64_t avail = 42; + ASSERT_NS_SUCCEEDED(check->GetSize(&avail)); + + ASSERT_TRUE(0 == avail); + + // Enlarge the file + const int64_t toSize = groupLimit; + ASSERT_NS_SUCCEEDED(stream->Seek(nsISeekableStream::NS_SEEK_SET, toSize)); + + ASSERT_NS_SUCCEEDED(check->GetSize(&avail)); + + ASSERT_TRUE(0 == avail); + + ASSERT_NS_SUCCEEDED(stream->SetEOF()); + + ASSERT_NS_SUCCEEDED(check->GetSize(&avail)); + + ASSERT_TRUE(toSize == avail); + + // Try to enlarge the file past the limit + const int64_t overGroupLimit = groupLimit + 1; + + // Seeking is allowed + ASSERT_NS_SUCCEEDED( + stream->Seek(nsISeekableStream::NS_SEEK_SET, overGroupLimit)); + + ASSERT_NS_SUCCEEDED(check->GetSize(&avail)); + + ASSERT_TRUE(toSize == avail); + + // Setting file size to exceed quota should yield no device space error + ASSERT_TRUE(NS_ERROR_FILE_NO_DEVICE_SPACE == stream->SetEOF()); + + ASSERT_NS_SUCCEEDED(check->GetSize(&avail)); + + ASSERT_TRUE(toSize == avail); + + // Shrink the file + const int64_t toHalfSize = toSize / 2; + ASSERT_NS_SUCCEEDED( + stream->Seek(nsISeekableStream::NS_SEEK_SET, toHalfSize)); + + ASSERT_NS_SUCCEEDED(check->GetSize(&avail)); + + ASSERT_TRUE(toSize == avail); + + ASSERT_NS_SUCCEEDED(stream->SetEOF()); + + ASSERT_NS_SUCCEEDED(check->GetSize(&avail)); + + ASSERT_TRUE(toHalfSize == avail); + + // Shrink the file back to nothing + ASSERT_NS_SUCCEEDED(stream->Seek(nsISeekableStream::NS_SEEK_SET, 0)); + + ASSERT_NS_SUCCEEDED(check->GetSize(&avail)); + + ASSERT_TRUE(toHalfSize == avail); + + ASSERT_NS_SUCCEEDED(stream->SetEOF()); + + ASSERT_NS_SUCCEEDED(check->GetSize(&avail)); + + ASSERT_TRUE(0 == avail); + }; + + PerformOnIOThread(std::move(ioTask)); +} + +} // namespace mozilla::dom::quota::test diff --git a/dom/quota/test/gtest/TestFlatten.cpp b/dom/quota/test/gtest/TestFlatten.cpp new file mode 100644 index 0000000000..5ca7675887 --- /dev/null +++ b/dom/quota/test/gtest/TestFlatten.cpp @@ -0,0 +1,81 @@ +/* -*- 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 "Flatten.h" + +#include "gtest/gtest.h" + +#include "mozilla/Unused.h" +#include "nsTArray.h" + +namespace mozilla::dom::quota { + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunreachable-code-loop-increment" +#endif +TEST(Flatten, FlatEmpty) +{ + for (const auto& item : Flatten<int>(nsTArray<int>{})) { + Unused << item; + FAIL(); + } +} + +TEST(Flatten, NestedOuterEmpty) +{ + for (const auto& item : Flatten<int>(nsTArray<CopyableTArray<int>>{})) { + Unused << item; + FAIL(); + } +} + +TEST(Flatten, NestedInnerEmpty) +{ + for (const auto& item : + Flatten<int>(nsTArray<CopyableTArray<int>>{CopyableTArray<int>{}})) { + Unused << item; + FAIL(); + } +} +#ifdef __clang__ +# pragma clang diagnostic pop +#endif + +TEST(Flatten, NestedInnerSingular) +{ + nsTArray<int> flattened; + for (const auto& item : + Flatten<int>(nsTArray<CopyableTArray<int>>{CopyableTArray<int>{1}})) { + flattened.AppendElement(item); + } + + EXPECT_EQ(nsTArray{1}, flattened); +} + +TEST(Flatten, NestedInnerSingulars) +{ + nsTArray<int> flattened; + for (const auto& item : Flatten<int>(nsTArray<CopyableTArray<int>>{ + CopyableTArray<int>{1}, CopyableTArray<int>{2}})) { + flattened.AppendElement(item); + } + + EXPECT_EQ((nsTArray<int>{{1, 2}}), flattened); +} + +TEST(Flatten, NestedInnerNonSingulars) +{ + nsTArray<int> flattened; + for (const auto& item : Flatten<int>(nsTArray<CopyableTArray<int>>{ + CopyableTArray<int>{1, 2}, CopyableTArray<int>{3, 4}})) { + flattened.AppendElement(item); + } + + EXPECT_EQ((nsTArray<int>{{1, 2, 3, 4}}), flattened); +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/test/gtest/TestForwardDecls.cpp b/dom/quota/test/gtest/TestForwardDecls.cpp new file mode 100644 index 0000000000..223f83af03 --- /dev/null +++ b/dom/quota/test/gtest/TestForwardDecls.cpp @@ -0,0 +1,12 @@ +/* -*- 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 <type_traits> +#include "mozilla/dom/quota/ForwardDecls.h" + +using namespace mozilla; + +static_assert(std::is_same_v<OkOrErr, Result<Ok, QMResult>>); diff --git a/dom/quota/test/gtest/TestPersistenceType.cpp b/dom/quota/test/gtest/TestPersistenceType.cpp new file mode 100644 index 0000000000..c4f1e5d581 --- /dev/null +++ b/dom/quota/test/gtest/TestPersistenceType.cpp @@ -0,0 +1,44 @@ +/* -*- 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 "mozilla/dom/quota/PersistenceType.h" + +#include "gtest/gtest.h" + +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIFile.h" + +namespace mozilla::dom::quota { + +TEST(PersistenceType, FromFile) +{ + nsCOMPtr<nsIFile> base; + nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(base)); + EXPECT_EQ(rv, NS_OK); + + const auto testPersistenceType = [&base](const nsLiteralString& aString, + const Maybe<PersistenceType> aType) { + nsCOMPtr<nsIFile> file; + + nsresult rv = base->Clone(getter_AddRefs(file)); + EXPECT_EQ(rv, NS_OK); + + rv = file->Append(aString); + EXPECT_EQ(rv, NS_OK); + + auto maybePersistenceType = PersistenceTypeFromFile(*file, fallible); + EXPECT_EQ(maybePersistenceType, aType); + }; + + testPersistenceType(u"permanent"_ns, Some(PERSISTENCE_TYPE_PERSISTENT)); + testPersistenceType(u"temporary"_ns, Some(PERSISTENCE_TYPE_TEMPORARY)); + testPersistenceType(u"default"_ns, Some(PERSISTENCE_TYPE_DEFAULT)); + testPersistenceType(u"persistent"_ns, Nothing()); + testPersistenceType(u"foobar"_ns, Nothing()); +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/test/gtest/TestQMResult.cpp b/dom/quota/test/gtest/TestQMResult.cpp new file mode 100644 index 0000000000..94cbec7364 --- /dev/null +++ b/dom/quota/test/gtest/TestQMResult.cpp @@ -0,0 +1,72 @@ +/* -*- 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 "Common.h" +#include "gtest/gtest.h" +#include "mozilla/dom/QMResult.h" + +using namespace mozilla; +using namespace mozilla::dom::quota; + +class DOM_Quota_QMResult : public DOM_Quota_Test {}; + +#ifdef QM_ERROR_STACKS_ENABLED +TEST_F(DOM_Quota_QMResult, Construct_Default) { + QMResult res; + + IncreaseExpectedStackId(); + + ASSERT_EQ(res.StackId(), ExpectedStackId()); + ASSERT_EQ(res.FrameId(), 1u); + ASSERT_EQ(res.NSResult(), NS_OK); +} +#endif + +TEST_F(DOM_Quota_QMResult, Construct_FromNSResult) { + QMResult res(NS_ERROR_FAILURE); + +#ifdef QM_ERROR_STACKS_ENABLED + IncreaseExpectedStackId(); + + ASSERT_EQ(res.StackId(), ExpectedStackId()); + ASSERT_EQ(res.FrameId(), 1u); + ASSERT_EQ(res.NSResult(), NS_ERROR_FAILURE); +#else + ASSERT_EQ(res, NS_ERROR_FAILURE); +#endif +} + +#ifdef QM_ERROR_STACKS_ENABLED +TEST_F(DOM_Quota_QMResult, Propagate) { + QMResult res1(NS_ERROR_FAILURE); + + IncreaseExpectedStackId(); + + ASSERT_EQ(res1.StackId(), ExpectedStackId()); + ASSERT_EQ(res1.FrameId(), 1u); + ASSERT_EQ(res1.NSResult(), NS_ERROR_FAILURE); + + QMResult res2 = res1.Propagate(); + + ASSERT_EQ(res2.StackId(), ExpectedStackId()); + ASSERT_EQ(res2.FrameId(), 2u); + ASSERT_EQ(res2.NSResult(), NS_ERROR_FAILURE); +} +#endif + +TEST_F(DOM_Quota_QMResult, ToQMResult) { + auto res = ToQMResult(NS_ERROR_FAILURE); + +#ifdef QM_ERROR_STACKS_ENABLED + IncreaseExpectedStackId(); + + ASSERT_EQ(res.StackId(), ExpectedStackId()); + ASSERT_EQ(res.FrameId(), 1u); + ASSERT_EQ(res.NSResult(), NS_ERROR_FAILURE); +#else + ASSERT_EQ(res, NS_ERROR_FAILURE); +#endif +} diff --git a/dom/quota/test/gtest/TestQuotaCommon.cpp b/dom/quota/test/gtest/TestQuotaCommon.cpp new file mode 100644 index 0000000000..c1a047a533 --- /dev/null +++ b/dom/quota/test/gtest/TestQuotaCommon.cpp @@ -0,0 +1,2171 @@ +/* -*- 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 "mozilla/dom/quota/QuotaCommon.h" + +#include "gtest/gtest.h" + +#include <algorithm> +#include <array> +#include <cstddef> +#include <cstdint> +#include <map> +#include <new> +#include <ostream> +#include <type_traits> +#include <utility> +#include <vector> +#include "ErrorList.h" +#include "mozilla/Assertions.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/Unused.h" +#include "mozilla/fallible.h" +#include "mozilla/dom/quota/QuotaTestParent.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsCOMPtr.h" +#include "nsLiteralString.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsTLiteralString.h" + +class nsISupports; + +using namespace mozilla; +using namespace mozilla::dom::quota; + +mozilla::ipc::IPCResult QuotaTestParent::RecvTry_Success_CustomErr_QmIpcFail( + bool* aTryDidNotReturn) { + QM_TRY(MOZ_TO_RESULT(NS_OK), QM_IPC_FAIL(this)); + + *aTryDidNotReturn = true; + + return IPC_OK(); +} + +mozilla::ipc::IPCResult QuotaTestParent::RecvTry_Success_CustomErr_IpcFail( + bool* aTryDidNotReturn) { + QM_TRY(MOZ_TO_RESULT(NS_OK), IPC_FAIL(this, "Custom why")); + + *aTryDidNotReturn = true; + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +QuotaTestParent::RecvTryInspect_Success_CustomErr_QmIpcFail( + bool* aTryDidNotReturn) { + QM_TRY_INSPECT(const auto& x, (mozilla::Result<int32_t, nsresult>{42}), + QM_IPC_FAIL(this)); + Unused << x; + + *aTryDidNotReturn = true; + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +QuotaTestParent::RecvTryInspect_Success_CustomErr_IpcFail( + bool* aTryDidNotReturn) { + QM_TRY_INSPECT(const auto& x, (mozilla::Result<int32_t, nsresult>{42}), + IPC_FAIL(this, "Custom why")); + Unused << x; + + *aTryDidNotReturn = true; + + return IPC_OK(); +} + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunreachable-code" +#endif + +TEST(QuotaCommon_Try, Success) +{ + bool tryDidNotReturn = false; + + nsresult rv = [&tryDidNotReturn]() -> nsresult { + QM_TRY(MOZ_TO_RESULT(NS_OK)); + + tryDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(tryDidNotReturn); + EXPECT_EQ(rv, NS_OK); +} + +TEST(QuotaCommon_Try, Success_CustomErr_QmIpcFail) +{ + auto foo = MakeRefPtr<QuotaTestParent>(); + + bool tryDidNotReturn = false; + + auto res = foo->RecvTry_Success_CustomErr_QmIpcFail(&tryDidNotReturn); + + EXPECT_TRUE(tryDidNotReturn); + EXPECT_TRUE(res); +} + +TEST(QuotaCommon_Try, Success_CustomErr_IpcFail) +{ + auto foo = MakeRefPtr<QuotaTestParent>(); + + bool tryDidNotReturn = false; + + auto res = foo->RecvTry_Success_CustomErr_IpcFail(&tryDidNotReturn); + + EXPECT_TRUE(tryDidNotReturn); + EXPECT_TRUE(res); +} + +#ifdef DEBUG +TEST(QuotaCommon_Try, Success_CustomErr_AssertUnreachable) +{ + bool tryDidNotReturn = false; + + nsresult rv = [&tryDidNotReturn]() -> nsresult { + QM_TRY(MOZ_TO_RESULT(NS_OK), QM_ASSERT_UNREACHABLE); + + tryDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(tryDidNotReturn); + EXPECT_EQ(rv, NS_OK); +} + +TEST(QuotaCommon_Try, Success_NoErr_AssertUnreachable) +{ + bool tryDidNotReturn = false; + + [&tryDidNotReturn]() -> void { + QM_TRY(MOZ_TO_RESULT(NS_OK), QM_ASSERT_UNREACHABLE_VOID); + + tryDidNotReturn = true; + }(); + + EXPECT_TRUE(tryDidNotReturn); +} +#else +# if defined(QM_ASSERT_UNREACHABLE) || defined(QM_ASSERT_UNREACHABLE_VOID) +#error QM_ASSERT_UNREACHABLE and QM_ASSERT_UNREACHABLE_VOID should not be defined. +# endif +#endif + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED +TEST(QuotaCommon_Try, Success_CustomErr_DiagnosticAssertUnreachable) +{ + bool tryDidNotReturn = false; + + nsresult rv = [&tryDidNotReturn]() -> nsresult { + QM_TRY(MOZ_TO_RESULT(NS_OK), QM_DIAGNOSTIC_ASSERT_UNREACHABLE); + + tryDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(tryDidNotReturn); + EXPECT_EQ(rv, NS_OK); +} + +TEST(QuotaCommon_Try, Success_NoErr_DiagnosticAssertUnreachable) +{ + bool tryDidNotReturn = false; + + [&tryDidNotReturn]() -> void { + QM_TRY(MOZ_TO_RESULT(NS_OK), QM_DIAGNOSTIC_ASSERT_UNREACHABLE_VOID); + + tryDidNotReturn = true; + }(); + + EXPECT_TRUE(tryDidNotReturn); +} +#else +# if defined(QM_DIAGNOSTIC_ASSERT_UNREACHABLE) || \ + defined(QM_DIAGNOSTIC_ASSERT_UNREACHABLE_VOID) +#error QM_DIAGNOSTIC_ASSERT_UNREACHABLE and QM_DIAGNOSTIC_ASSERT_UNREACHABLE_VOID should not be defined. +# endif +#endif + +TEST(QuotaCommon_Try, Success_CustomErr_CustomLambda) +{ +#define SUBTEST(...) \ + { \ + bool tryDidNotReturn = false; \ + \ + nsresult rv = [&tryDidNotReturn]() -> nsresult { \ + QM_TRY(MOZ_TO_RESULT(NS_OK), [](__VA_ARGS__) { return aRv; }); \ + \ + tryDidNotReturn = true; \ + \ + return NS_OK; \ + }(); \ + \ + EXPECT_TRUE(tryDidNotReturn); \ + EXPECT_EQ(rv, NS_OK); \ + } + + SUBTEST(const char*, nsresult aRv); + SUBTEST(nsresult aRv); + +#undef SUBTEST +} + +TEST(QuotaCommon_Try, Success_WithCleanup) +{ + bool tryCleanupRan = false; + bool tryDidNotReturn = false; + + nsresult rv = [&tryCleanupRan, &tryDidNotReturn]() -> nsresult { + QM_TRY(MOZ_TO_RESULT(NS_OK), QM_PROPAGATE, + [&tryCleanupRan](const auto&) { tryCleanupRan = true; }); + + tryDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_FALSE(tryCleanupRan); + EXPECT_TRUE(tryDidNotReturn); + EXPECT_EQ(rv, NS_OK); +} + +TEST(QuotaCommon_Try, Failure_PropagateErr) +{ + bool tryDidNotReturn = false; + + nsresult rv = [&tryDidNotReturn]() -> nsresult { + QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE)); + + tryDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_FALSE(tryDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_Try, Failure_CustomErr) +{ + bool tryDidNotReturn = false; + + nsresult rv = [&tryDidNotReturn]() -> nsresult { + QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE), NS_ERROR_UNEXPECTED); + + tryDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_FALSE(tryDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_UNEXPECTED); +} + +TEST(QuotaCommon_Try, Failure_CustomErr_CustomLambda) +{ +#define SUBTEST(...) \ + { \ + bool tryDidNotReturn = false; \ + \ + nsresult rv = [&tryDidNotReturn]() -> nsresult { \ + QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE), \ + [](__VA_ARGS__) { return NS_ERROR_UNEXPECTED; }); \ + \ + tryDidNotReturn = true; \ + \ + return NS_OK; \ + }(); \ + \ + EXPECT_FALSE(tryDidNotReturn); \ + EXPECT_EQ(rv, NS_ERROR_UNEXPECTED); \ + } + + SUBTEST(const char* aFunc, nsresult); + SUBTEST(nsresult rv); + +#undef SUBTEST +} + +TEST(QuotaCommon_Try, Failure_NoErr) +{ + bool tryDidNotReturn = false; + + [&tryDidNotReturn]() -> void { + QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE), QM_VOID); + + tryDidNotReturn = true; + }(); + + EXPECT_FALSE(tryDidNotReturn); +} + +TEST(QuotaCommon_Try, Failure_WithCleanup) +{ + bool tryCleanupRan = false; + bool tryDidNotReturn = false; + + nsresult rv = [&tryCleanupRan, &tryDidNotReturn]() -> nsresult { + QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE), QM_PROPAGATE, + [&tryCleanupRan](const auto& result) { + EXPECT_EQ(result, NS_ERROR_FAILURE); + + tryCleanupRan = true; + }); + + tryDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(tryCleanupRan); + EXPECT_FALSE(tryDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_Try, Failure_WithCleanup_UnwrapErr) +{ + bool tryCleanupRan = false; + bool tryDidNotReturn = false; + + nsresult rv; + + [&tryCleanupRan, &tryDidNotReturn](nsresult& aRv) -> void { + QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE), QM_VOID, + ([&tryCleanupRan, &aRv](auto& result) { + EXPECT_EQ(result, NS_ERROR_FAILURE); + + aRv = result; + + tryCleanupRan = true; + })); + + tryDidNotReturn = true; + + aRv = NS_OK; + }(rv); + + EXPECT_TRUE(tryCleanupRan); + EXPECT_FALSE(tryDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_Try, SameLine) +{ + // clang-format off + QM_TRY(MOZ_TO_RESULT(NS_OK), QM_VOID); QM_TRY(MOZ_TO_RESULT(NS_OK), QM_VOID); + // clang-format on +} + +TEST(QuotaCommon_Try, NestingMadness_Success) +{ + bool nestedTryDidNotReturn = false; + bool tryDidNotReturn = false; + + nsresult rv = [&nestedTryDidNotReturn, &tryDidNotReturn]() -> nsresult { + QM_TRY(([&nestedTryDidNotReturn]() -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(NS_OK)); + + nestedTryDidNotReturn = true; + + return Ok(); + }())); + + tryDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(nestedTryDidNotReturn); + EXPECT_TRUE(tryDidNotReturn); + EXPECT_EQ(rv, NS_OK); +} + +TEST(QuotaCommon_Try, NestingMadness_Failure) +{ + bool nestedTryDidNotReturn = false; + bool tryDidNotReturn = false; + + nsresult rv = [&nestedTryDidNotReturn, &tryDidNotReturn]() -> nsresult { + QM_TRY(([&nestedTryDidNotReturn]() -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE)); + + nestedTryDidNotReturn = true; + + return Ok(); + }())); + + tryDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_FALSE(nestedTryDidNotReturn); + EXPECT_FALSE(tryDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_Try, NestingMadness_Multiple_Success) +{ + bool nestedTry1DidNotReturn = false; + bool nestedTry2DidNotReturn = false; + bool tryDidNotReturn = false; + + nsresult rv = [&nestedTry1DidNotReturn, &nestedTry2DidNotReturn, + &tryDidNotReturn]() -> nsresult { + QM_TRY(([&nestedTry1DidNotReturn, + &nestedTry2DidNotReturn]() -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(NS_OK)); + + nestedTry1DidNotReturn = true; + + QM_TRY(MOZ_TO_RESULT(NS_OK)); + + nestedTry2DidNotReturn = true; + + return Ok(); + }())); + + tryDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(nestedTry1DidNotReturn); + EXPECT_TRUE(nestedTry2DidNotReturn); + EXPECT_TRUE(tryDidNotReturn); + EXPECT_EQ(rv, NS_OK); +} + +TEST(QuotaCommon_Try, NestingMadness_Multiple_Failure1) +{ + bool nestedTry1DidNotReturn = false; + bool nestedTry2DidNotReturn = false; + bool tryDidNotReturn = false; + + nsresult rv = [&nestedTry1DidNotReturn, &nestedTry2DidNotReturn, + &tryDidNotReturn]() -> nsresult { + QM_TRY(([&nestedTry1DidNotReturn, + &nestedTry2DidNotReturn]() -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE)); + + nestedTry1DidNotReturn = true; + + QM_TRY(MOZ_TO_RESULT(NS_OK)); + + nestedTry2DidNotReturn = true; + + return Ok(); + }())); + + tryDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_FALSE(nestedTry1DidNotReturn); + EXPECT_FALSE(nestedTry2DidNotReturn); + EXPECT_FALSE(tryDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_Try, NestingMadness_Multiple_Failure2) +{ + bool nestedTry1DidNotReturn = false; + bool nestedTry2DidNotReturn = false; + bool tryDidNotReturn = false; + + nsresult rv = [&nestedTry1DidNotReturn, &nestedTry2DidNotReturn, + &tryDidNotReturn]() -> nsresult { + QM_TRY(([&nestedTry1DidNotReturn, + &nestedTry2DidNotReturn]() -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(NS_OK)); + + nestedTry1DidNotReturn = true; + + QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE)); + + nestedTry2DidNotReturn = true; + + return Ok(); + }())); + + tryDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(nestedTry1DidNotReturn); + EXPECT_FALSE(nestedTry2DidNotReturn); + EXPECT_FALSE(tryDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_TryInspect, Success) +{ + bool tryInspectDidNotReturn = false; + + nsresult rv = [&tryInspectDidNotReturn]() -> nsresult { + QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42})); + EXPECT_EQ(x, 42); + + tryInspectDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(tryInspectDidNotReturn); + EXPECT_EQ(rv, NS_OK); +} + +TEST(QuotaCommon_TryInspect, Success_CustomErr_QmIpcFail) +{ + auto foo = MakeRefPtr<QuotaTestParent>(); + + bool tryDidNotReturn = false; + + auto res = foo->RecvTryInspect_Success_CustomErr_QmIpcFail(&tryDidNotReturn); + + EXPECT_TRUE(tryDidNotReturn); + EXPECT_TRUE(res); +} + +TEST(QuotaCommon_TryInspect, Success_CustomErr_IpcFail) +{ + auto foo = MakeRefPtr<QuotaTestParent>(); + + bool tryDidNotReturn = false; + + auto res = foo->RecvTryInspect_Success_CustomErr_IpcFail(&tryDidNotReturn); + + EXPECT_TRUE(tryDidNotReturn); + EXPECT_TRUE(res); +} + +#ifdef DEBUG +TEST(QuotaCommon_TryInspect, Success_CustomErr_AssertUnreachable) +{ + bool tryInspectDidNotReturn = false; + + nsresult rv = [&tryInspectDidNotReturn]() -> nsresult { + QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42}), + QM_ASSERT_UNREACHABLE); + EXPECT_EQ(x, 42); + + tryInspectDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(tryInspectDidNotReturn); + EXPECT_EQ(rv, NS_OK); +} + +TEST(QuotaCommon_TryInspect, Success_NoErr_AssertUnreachable) +{ + bool tryInspectDidNotReturn = false; + + [&tryInspectDidNotReturn]() -> void { + QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42}), + QM_ASSERT_UNREACHABLE_VOID); + EXPECT_EQ(x, 42); + + tryInspectDidNotReturn = true; + }(); + + EXPECT_TRUE(tryInspectDidNotReturn); +} +#endif + +TEST(QuotaCommon_TryInspect, Success_CustomErr_CustomLambda) +{ +#define SUBTEST(...) \ + { \ + bool tryInspectDidNotReturn = false; \ + \ + nsresult rv = [&tryInspectDidNotReturn]() -> nsresult { \ + QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42}), \ + [](__VA_ARGS__) { return aRv; }); \ + EXPECT_EQ(x, 42); \ + \ + tryInspectDidNotReturn = true; \ + \ + return NS_OK; \ + }(); \ + \ + EXPECT_TRUE(tryInspectDidNotReturn); \ + EXPECT_EQ(rv, NS_OK); \ + } + + SUBTEST(const char*, nsresult aRv); + SUBTEST(nsresult aRv); + +#undef SUBTEST +} + +TEST(QuotaCommon_TryInspect, Success_WithCleanup) +{ + bool tryInspectCleanupRan = false; + bool tryInspectDidNotReturn = false; + + nsresult rv = [&tryInspectCleanupRan, &tryInspectDidNotReturn]() -> nsresult { + QM_TRY_INSPECT( + const auto& x, (Result<int32_t, nsresult>{42}), QM_PROPAGATE, + [&tryInspectCleanupRan](const auto&) { tryInspectCleanupRan = true; }); + EXPECT_EQ(x, 42); + + tryInspectDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_FALSE(tryInspectCleanupRan); + EXPECT_TRUE(tryInspectDidNotReturn); + EXPECT_EQ(rv, NS_OK); +} + +TEST(QuotaCommon_TryInspect, Failure_PropagateErr) +{ + bool tryInspectDidNotReturn = false; + + nsresult rv = [&tryInspectDidNotReturn]() -> nsresult { + QM_TRY_INSPECT(const auto& x, + (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)})); + Unused << x; + + tryInspectDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_FALSE(tryInspectDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_TryInspect, Failure_CustomErr) +{ + bool tryInspectDidNotReturn = false; + + nsresult rv = [&tryInspectDidNotReturn]() -> nsresult { + QM_TRY_INSPECT(const auto& x, + (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}), + NS_ERROR_UNEXPECTED); + Unused << x; + + tryInspectDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_FALSE(tryInspectDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_UNEXPECTED); +} + +TEST(QuotaCommon_TryInspect, Failure_CustomErr_CustomLambda) +{ +#define SUBTEST(...) \ + { \ + bool tryInspectDidNotReturn = false; \ + \ + nsresult rv = [&tryInspectDidNotReturn]() -> nsresult { \ + QM_TRY_INSPECT(const auto& x, \ + (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}), \ + [](__VA_ARGS__) { return NS_ERROR_UNEXPECTED; }); \ + Unused << x; \ + \ + tryInspectDidNotReturn = true; \ + \ + return NS_OK; \ + }(); \ + \ + EXPECT_FALSE(tryInspectDidNotReturn); \ + EXPECT_EQ(rv, NS_ERROR_UNEXPECTED); \ + } + + SUBTEST(const char*, nsresult); + SUBTEST(nsresult); + +#undef SUBTEST +} + +TEST(QuotaCommon_TryInspect, Failure_NoErr) +{ + bool tryInspectDidNotReturn = false; + + [&tryInspectDidNotReturn]() -> void { + QM_TRY_INSPECT(const auto& x, + (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}), QM_VOID); + Unused << x; + + tryInspectDidNotReturn = true; + }(); + + EXPECT_FALSE(tryInspectDidNotReturn); +} + +TEST(QuotaCommon_TryInspect, Failure_WithCleanup) +{ + bool tryInspectCleanupRan = false; + bool tryInspectDidNotReturn = false; + + nsresult rv = [&tryInspectCleanupRan, &tryInspectDidNotReturn]() -> nsresult { + QM_TRY_INSPECT(const auto& x, + (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}), + QM_PROPAGATE, [&tryInspectCleanupRan](const auto& result) { + EXPECT_EQ(result, NS_ERROR_FAILURE); + + tryInspectCleanupRan = true; + }); + Unused << x; + + tryInspectDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(tryInspectCleanupRan); + EXPECT_FALSE(tryInspectDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_TryInspect, Failure_WithCleanup_UnwrapErr) +{ + bool tryInspectCleanupRan = false; + bool tryInspectDidNotReturn = false; + + nsresult rv; + + [&tryInspectCleanupRan, &tryInspectDidNotReturn](nsresult& aRv) -> void { + QM_TRY_INSPECT(const auto& x, + (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}), QM_VOID, + ([&tryInspectCleanupRan, &aRv](auto& result) { + EXPECT_EQ(result, NS_ERROR_FAILURE); + + aRv = result; + + tryInspectCleanupRan = true; + })); + Unused << x; + + tryInspectDidNotReturn = true; + + aRv = NS_OK; + }(rv); + + EXPECT_TRUE(tryInspectCleanupRan); + EXPECT_FALSE(tryInspectDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_TryInspect, ConstDecl) +{ + QM_TRY_INSPECT(const int32_t& x, (Result<int32_t, nsresult>{42}), QM_VOID); + + static_assert(std::is_same_v<decltype(x), const int32_t&>); + + EXPECT_EQ(x, 42); +} + +TEST(QuotaCommon_TryInspect, SameScopeDecl) +{ + QM_TRY_INSPECT(const int32_t& x, (Result<int32_t, nsresult>{42}), QM_VOID); + EXPECT_EQ(x, 42); + + QM_TRY_INSPECT(const int32_t& y, (Result<int32_t, nsresult>{42}), QM_VOID); + EXPECT_EQ(y, 42); +} + +TEST(QuotaCommon_TryInspect, SameLine) +{ + // clang-format off + QM_TRY_INSPECT(const auto &x, (Result<int32_t, nsresult>{42}), QM_VOID); QM_TRY_INSPECT(const auto &y, (Result<int32_t, nsresult>{42}), QM_VOID); + // clang-format on + + EXPECT_EQ(x, 42); + EXPECT_EQ(y, 42); +} + +TEST(QuotaCommon_TryInspect, NestingMadness_Success) +{ + bool nestedTryInspectDidNotReturn = false; + bool tryInspectDidNotReturn = false; + + nsresult rv = [&nestedTryInspectDidNotReturn, + &tryInspectDidNotReturn]() -> nsresult { + QM_TRY_INSPECT( + const auto& x, + ([&nestedTryInspectDidNotReturn]() -> Result<int32_t, nsresult> { + QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42})); + + nestedTryInspectDidNotReturn = true; + + return x; + }())); + EXPECT_EQ(x, 42); + + tryInspectDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(nestedTryInspectDidNotReturn); + EXPECT_TRUE(tryInspectDidNotReturn); + EXPECT_EQ(rv, NS_OK); +} + +TEST(QuotaCommon_TryInspect, NestingMadness_Failure) +{ + bool nestedTryInspectDidNotReturn = false; + bool tryInspectDidNotReturn = false; + + nsresult rv = [&nestedTryInspectDidNotReturn, + &tryInspectDidNotReturn]() -> nsresult { + QM_TRY_INSPECT( + const auto& x, + ([&nestedTryInspectDidNotReturn]() -> Result<int32_t, nsresult> { + QM_TRY_INSPECT(const auto& x, + (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)})); + + nestedTryInspectDidNotReturn = true; + + return x; + }())); + Unused << x; + + tryInspectDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_FALSE(nestedTryInspectDidNotReturn); + EXPECT_FALSE(tryInspectDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_TryInspect, NestingMadness_Multiple_Success) +{ + bool nestedTryInspect1DidNotReturn = false; + bool nestedTryInspect2DidNotReturn = false; + bool tryInspectDidNotReturn = false; + + nsresult rv = [&nestedTryInspect1DidNotReturn, &nestedTryInspect2DidNotReturn, + &tryInspectDidNotReturn]() -> nsresult { + QM_TRY_INSPECT( + const auto& z, + ([&nestedTryInspect1DidNotReturn, + &nestedTryInspect2DidNotReturn]() -> Result<int32_t, nsresult> { + QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42})); + + nestedTryInspect1DidNotReturn = true; + + QM_TRY_INSPECT(const auto& y, (Result<int32_t, nsresult>{42})); + + nestedTryInspect2DidNotReturn = true; + + return x + y; + }())); + EXPECT_EQ(z, 84); + + tryInspectDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(nestedTryInspect1DidNotReturn); + EXPECT_TRUE(nestedTryInspect2DidNotReturn); + EXPECT_TRUE(tryInspectDidNotReturn); + EXPECT_EQ(rv, NS_OK); +} + +TEST(QuotaCommon_TryInspect, NestingMadness_Multiple_Failure1) +{ + bool nestedTryInspect1DidNotReturn = false; + bool nestedTryInspect2DidNotReturn = false; + bool tryInspectDidNotReturn = false; + + nsresult rv = [&nestedTryInspect1DidNotReturn, &nestedTryInspect2DidNotReturn, + &tryInspectDidNotReturn]() -> nsresult { + QM_TRY_INSPECT( + const auto& z, + ([&nestedTryInspect1DidNotReturn, + &nestedTryInspect2DidNotReturn]() -> Result<int32_t, nsresult> { + QM_TRY_INSPECT(const auto& x, + (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)})); + + nestedTryInspect1DidNotReturn = true; + + QM_TRY_INSPECT(const auto& y, (Result<int32_t, nsresult>{42})); + + nestedTryInspect2DidNotReturn = true; + + return x + y; + }())); + Unused << z; + + tryInspectDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_FALSE(nestedTryInspect1DidNotReturn); + EXPECT_FALSE(nestedTryInspect2DidNotReturn); + EXPECT_FALSE(tryInspectDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_TryInspect, NestingMadness_Multiple_Failure2) +{ + bool nestedTryInspect1DidNotReturn = false; + bool nestedTryInspect2DidNotReturn = false; + bool tryInspectDidNotReturn = false; + + nsresult rv = [&nestedTryInspect1DidNotReturn, &nestedTryInspect2DidNotReturn, + &tryInspectDidNotReturn]() -> nsresult { + QM_TRY_INSPECT( + const auto& z, + ([&nestedTryInspect1DidNotReturn, + &nestedTryInspect2DidNotReturn]() -> Result<int32_t, nsresult> { + QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42})); + + nestedTryInspect1DidNotReturn = true; + + QM_TRY_INSPECT(const auto& y, + (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)})); + + nestedTryInspect2DidNotReturn = true; + + return x + y; + }())); + Unused << z; + + tryInspectDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(nestedTryInspect1DidNotReturn); + EXPECT_FALSE(nestedTryInspect2DidNotReturn); + EXPECT_FALSE(tryInspectDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +// We are not repeating all QM_TRY_INSPECT test cases for QM_TRY_UNWRAP, since +// they are largely based on the same implementation. We just add some where +// inspecting and unwrapping differ. + +TEST(QuotaCommon_TryUnwrap, NonConstDecl) +{ + QM_TRY_UNWRAP(int32_t x, (Result<int32_t, nsresult>{42}), QM_VOID); + + static_assert(std::is_same_v<decltype(x), int32_t>); + + EXPECT_EQ(x, 42); +} + +TEST(QuotaCommon_TryUnwrap, RvalueDecl) +{ + QM_TRY_UNWRAP(int32_t && x, (Result<int32_t, nsresult>{42}), QM_VOID); + + static_assert(std::is_same_v<decltype(x), int32_t&&>); + + EXPECT_EQ(x, 42); +} + +TEST(QuotaCommon_TryUnwrap, ParenDecl) +{ + QM_TRY_UNWRAP( + (auto&& [x, y]), + (Result<std::pair<int32_t, bool>, nsresult>{std::pair{42, true}}), + QM_VOID); + + static_assert(std::is_same_v<decltype(x), int32_t>); + static_assert(std::is_same_v<decltype(y), bool>); + + EXPECT_EQ(x, 42); + EXPECT_EQ(y, true); +} + +TEST(QuotaCommon_TryReturn, Success) +{ + bool tryReturnDidNotReturn = false; + + auto res = [&tryReturnDidNotReturn] { + QM_TRY_RETURN((Result<int32_t, nsresult>{42})); + + tryReturnDidNotReturn = true; + }(); + + EXPECT_FALSE(tryReturnDidNotReturn); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), 42); +} + +TEST(QuotaCommon_TryReturn, Success_nsresult) +{ + bool tryReturnDidNotReturn = false; + + auto res = [&tryReturnDidNotReturn] { + QM_TRY_RETURN(MOZ_TO_RESULT(NS_OK)); + + tryReturnDidNotReturn = true; + }(); + + EXPECT_FALSE(tryReturnDidNotReturn); + EXPECT_TRUE(res.isOk()); +} + +#ifdef DEBUG +TEST(QuotaCommon_TryReturn, Success_CustomErr_AssertUnreachable) +{ + bool tryReturnDidNotReturn = false; + + auto res = [&tryReturnDidNotReturn]() -> Result<int32_t, nsresult> { + QM_TRY_RETURN((Result<int32_t, nsresult>{42}), QM_ASSERT_UNREACHABLE); + + tryReturnDidNotReturn = true; + }(); + + EXPECT_FALSE(tryReturnDidNotReturn); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), 42); +} +#endif + +TEST(QuotaCommon_TryReturn, Success_CustomErr_CustomLambda) +{ +#define SUBTEST(...) \ + { \ + bool tryReturnDidNotReturn = false; \ + \ + auto res = [&tryReturnDidNotReturn]() -> Result<Ok, nsresult> { \ + QM_TRY_RETURN(MOZ_TO_RESULT(NS_OK), \ + [](__VA_ARGS__) { return Err(aRv); }); \ + \ + tryReturnDidNotReturn = true; \ + }(); \ + \ + EXPECT_FALSE(tryReturnDidNotReturn); \ + EXPECT_TRUE(res.isOk()); \ + } + + SUBTEST(const char*, nsresult aRv); + SUBTEST(nsresult aRv); + +#undef SUBTEST +} + +TEST(QuotaCommon_TryReturn, Success_WithCleanup) +{ + bool tryReturnCleanupRan = false; + bool tryReturnDidNotReturn = false; + + auto res = [&tryReturnCleanupRan, + &tryReturnDidNotReturn]() -> Result<int32_t, nsresult> { + QM_TRY_RETURN( + (Result<int32_t, nsresult>{42}), QM_PROPAGATE, + [&tryReturnCleanupRan](const auto&) { tryReturnCleanupRan = true; }); + + tryReturnDidNotReturn = true; + }(); + + EXPECT_FALSE(tryReturnCleanupRan); + EXPECT_FALSE(tryReturnDidNotReturn); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), 42); +} + +TEST(QuotaCommon_TryReturn, Failure_PropagateErr) +{ + bool tryReturnDidNotReturn = false; + + auto res = [&tryReturnDidNotReturn] { + QM_TRY_RETURN((Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)})); + + tryReturnDidNotReturn = true; + }(); + + EXPECT_FALSE(tryReturnDidNotReturn); + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_TryReturn, Failure_PropagateErr_nsresult) +{ + bool tryReturnDidNotReturn = false; + + auto res = [&tryReturnDidNotReturn] { + QM_TRY_RETURN(MOZ_TO_RESULT(NS_ERROR_FAILURE)); + + tryReturnDidNotReturn = true; + }(); + + EXPECT_FALSE(tryReturnDidNotReturn); + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_TryReturn, Failure_CustomErr) +{ + bool tryReturnDidNotReturn = false; + + auto res = [&tryReturnDidNotReturn]() -> Result<int32_t, nsresult> { + QM_TRY_RETURN((Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}), + Err(NS_ERROR_UNEXPECTED)); + + tryReturnDidNotReturn = true; + }(); + + EXPECT_FALSE(tryReturnDidNotReturn); + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED); +} + +TEST(QuotaCommon_TryReturn, Failure_CustomErr_CustomLambda) +{ +#define SUBTEST(...) \ + { \ + bool tryReturnDidNotReturn = false; \ + \ + auto res = [&tryReturnDidNotReturn]() -> Result<int32_t, nsresult> { \ + QM_TRY_RETURN((Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}), \ + [](__VA_ARGS__) { return Err(NS_ERROR_UNEXPECTED); }); \ + \ + tryReturnDidNotReturn = true; \ + }(); \ + \ + EXPECT_FALSE(tryReturnDidNotReturn); \ + EXPECT_TRUE(res.isErr()); \ + EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED); \ + } + + SUBTEST(const char*, nsresult); + SUBTEST(nsresult); + +#undef SUBTEST +} + +TEST(QuotaCommon_TryReturn, Failure_WithCleanup) +{ + bool tryReturnCleanupRan = false; + bool tryReturnDidNotReturn = false; + + auto res = [&tryReturnCleanupRan, + &tryReturnDidNotReturn]() -> Result<int32_t, nsresult> { + QM_TRY_RETURN((Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}), + QM_PROPAGATE, [&tryReturnCleanupRan](const auto& result) { + EXPECT_EQ(result, NS_ERROR_FAILURE); + + tryReturnCleanupRan = true; + }); + + tryReturnDidNotReturn = true; + }(); + + EXPECT_TRUE(tryReturnCleanupRan); + EXPECT_FALSE(tryReturnDidNotReturn); + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_TryReturn, SameLine) +{ + // clang-format off + auto res1 = [] { QM_TRY_RETURN((Result<int32_t, nsresult>{42})); }(); auto res2 = []() -> Result<int32_t, nsresult> { QM_TRY_RETURN((Result<int32_t, nsresult>{42})); }(); + // clang-format on + + EXPECT_TRUE(res1.isOk()); + EXPECT_EQ(res1.unwrap(), 42); + EXPECT_TRUE(res2.isOk()); + EXPECT_EQ(res2.unwrap(), 42); +} + +TEST(QuotaCommon_TryReturn, NestingMadness_Success) +{ + bool nestedTryReturnDidNotReturn = false; + bool tryReturnDidNotReturn = false; + + auto res = [&nestedTryReturnDidNotReturn, &tryReturnDidNotReturn] { + QM_TRY_RETURN(([&nestedTryReturnDidNotReturn] { + QM_TRY_RETURN((Result<int32_t, nsresult>{42})); + + nestedTryReturnDidNotReturn = true; + }())); + + tryReturnDidNotReturn = true; + }(); + + EXPECT_FALSE(nestedTryReturnDidNotReturn); + EXPECT_FALSE(tryReturnDidNotReturn); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), 42); +} + +TEST(QuotaCommon_TryReturn, NestingMadness_Failure) +{ + bool nestedTryReturnDidNotReturn = false; + bool tryReturnDidNotReturn = false; + + auto res = [&nestedTryReturnDidNotReturn, &tryReturnDidNotReturn] { + QM_TRY_RETURN(([&nestedTryReturnDidNotReturn] { + QM_TRY_RETURN((Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)})); + + nestedTryReturnDidNotReturn = true; + }())); + + tryReturnDidNotReturn = true; + }(); + + EXPECT_FALSE(nestedTryReturnDidNotReturn); + EXPECT_FALSE(tryReturnDidNotReturn); + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_Fail, ReturnValue) +{ + bool failDidNotReturn = false; + + nsresult rv = [&failDidNotReturn]() -> nsresult { + QM_FAIL(NS_ERROR_FAILURE); + + failDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_FALSE(failDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_Fail, ReturnValue_WithCleanup) +{ + bool failCleanupRan = false; + bool failDidNotReturn = false; + + nsresult rv = [&failCleanupRan, &failDidNotReturn]() -> nsresult { + QM_FAIL(NS_ERROR_FAILURE, [&failCleanupRan]() { failCleanupRan = true; }); + + failDidNotReturn = true; + + return NS_OK; + }(); + + EXPECT_TRUE(failCleanupRan); + EXPECT_FALSE(failDidNotReturn); + EXPECT_EQ(rv, NS_ERROR_FAILURE); +} + +TEST(QuotaCommon_WarnOnlyTry, Success) +{ + bool warnOnlyTryDidNotReturn = false; + + const auto res = + [&warnOnlyTryDidNotReturn]() -> mozilla::Result<mozilla::Ok, NotOk> { + QM_WARNONLY_TRY(OkIf(true)); + + warnOnlyTryDidNotReturn = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isOk()); + EXPECT_TRUE(warnOnlyTryDidNotReturn); +} + +TEST(QuotaCommon_WarnOnlyTry, Success_WithCleanup) +{ + bool warnOnlyTryCleanupRan = false; + bool warnOnlyTryDidNotReturn = false; + + const auto res = + [&warnOnlyTryCleanupRan, + &warnOnlyTryDidNotReturn]() -> mozilla::Result<mozilla::Ok, NotOk> { + QM_WARNONLY_TRY(OkIf(true), [&warnOnlyTryCleanupRan](const auto&) { + warnOnlyTryCleanupRan = true; + }); + + warnOnlyTryDidNotReturn = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isOk()); + EXPECT_FALSE(warnOnlyTryCleanupRan); + EXPECT_TRUE(warnOnlyTryDidNotReturn); +} + +TEST(QuotaCommon_WarnOnlyTry, Failure) +{ + bool warnOnlyTryDidNotReturn = false; + + const auto res = + [&warnOnlyTryDidNotReturn]() -> mozilla::Result<mozilla::Ok, NotOk> { + QM_WARNONLY_TRY(OkIf(false)); + + warnOnlyTryDidNotReturn = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isOk()); + EXPECT_TRUE(warnOnlyTryDidNotReturn); +} + +TEST(QuotaCommon_WarnOnlyTry, Failure_WithCleanup) +{ + bool warnOnlyTryCleanupRan = false; + bool warnOnlyTryDidNotReturn = false; + + const auto res = + [&warnOnlyTryCleanupRan, + &warnOnlyTryDidNotReturn]() -> mozilla::Result<mozilla::Ok, NotOk> { + QM_WARNONLY_TRY(OkIf(false), ([&warnOnlyTryCleanupRan](const auto&) { + warnOnlyTryCleanupRan = true; + })); + + warnOnlyTryDidNotReturn = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isOk()); + EXPECT_TRUE(warnOnlyTryCleanupRan); + EXPECT_TRUE(warnOnlyTryDidNotReturn); +} + +TEST(QuotaCommon_WarnOnlyTryUnwrap, Success) +{ + bool warnOnlyTryUnwrapDidNotReturn = false; + + const auto res = [&warnOnlyTryUnwrapDidNotReturn]() + -> mozilla::Result<mozilla::Ok, NotOk> { + QM_WARNONLY_TRY_UNWRAP(const auto x, (Result<int32_t, NotOk>{42})); + EXPECT_TRUE(x); + EXPECT_EQ(*x, 42); + + warnOnlyTryUnwrapDidNotReturn = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isOk()); + EXPECT_TRUE(warnOnlyTryUnwrapDidNotReturn); +} + +TEST(QuotaCommon_WarnOnlyTryUnwrap, Success_WithCleanup) +{ + bool warnOnlyTryUnwrapCleanupRan = false; + bool warnOnlyTryUnwrapDidNotReturn = false; + + const auto res = [&warnOnlyTryUnwrapCleanupRan, + &warnOnlyTryUnwrapDidNotReturn]() + -> mozilla::Result<mozilla::Ok, NotOk> { + QM_WARNONLY_TRY_UNWRAP(const auto x, (Result<int32_t, NotOk>{42}), + [&warnOnlyTryUnwrapCleanupRan](const auto&) { + warnOnlyTryUnwrapCleanupRan = true; + }); + EXPECT_TRUE(x); + EXPECT_EQ(*x, 42); + + warnOnlyTryUnwrapDidNotReturn = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isOk()); + EXPECT_FALSE(warnOnlyTryUnwrapCleanupRan); + EXPECT_TRUE(warnOnlyTryUnwrapDidNotReturn); +} + +TEST(QuotaCommon_WarnOnlyTryUnwrap, Failure) +{ + bool warnOnlyTryUnwrapDidNotReturn = false; + + const auto res = [&warnOnlyTryUnwrapDidNotReturn]() + -> mozilla::Result<mozilla::Ok, NotOk> { + QM_WARNONLY_TRY_UNWRAP(const auto x, + (Result<int32_t, NotOk>{Err(NotOk{})})); + EXPECT_FALSE(x); + + warnOnlyTryUnwrapDidNotReturn = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isOk()); + EXPECT_TRUE(warnOnlyTryUnwrapDidNotReturn); +} + +TEST(QuotaCommon_WarnOnlyTryUnwrap, Failure_WithCleanup) +{ + bool warnOnlyTryUnwrapCleanupRan = false; + bool warnOnlyTryUnwrapDidNotReturn = false; + + const auto res = [&warnOnlyTryUnwrapCleanupRan, + &warnOnlyTryUnwrapDidNotReturn]() + -> mozilla::Result<mozilla::Ok, NotOk> { + QM_WARNONLY_TRY_UNWRAP(const auto x, (Result<int32_t, NotOk>{Err(NotOk{})}), + [&warnOnlyTryUnwrapCleanupRan](const auto&) { + warnOnlyTryUnwrapCleanupRan = true; + }); + EXPECT_FALSE(x); + + warnOnlyTryUnwrapDidNotReturn = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isOk()); + EXPECT_TRUE(warnOnlyTryUnwrapCleanupRan); + EXPECT_TRUE(warnOnlyTryUnwrapDidNotReturn); +} + +TEST(QuotaCommon_OrElseWarn, Success) +{ + bool fallbackRun = false; + bool tryContinued = false; + + const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> { + QM_TRY(QM_OR_ELSE_WARN(OkIf(true), ([&fallbackRun](const NotOk) { + fallbackRun = true; + return mozilla::Result<mozilla::Ok, NotOk>{ + mozilla::Ok{}}; + }))); + + tryContinued = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isOk()); + EXPECT_FALSE(fallbackRun); + EXPECT_TRUE(tryContinued); +} + +TEST(QuotaCommon_OrElseWarn, Failure_MappedToSuccess) +{ + bool fallbackRun = false; + bool tryContinued = false; + + // XXX Consider allowing to set a custom error handler, so that we can + // actually assert that a warning was emitted. + const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> { + QM_TRY(QM_OR_ELSE_WARN(OkIf(false), ([&fallbackRun](const NotOk) { + fallbackRun = true; + return mozilla::Result<mozilla::Ok, NotOk>{ + mozilla::Ok{}}; + }))); + tryContinued = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isOk()); + EXPECT_TRUE(fallbackRun); + EXPECT_TRUE(tryContinued); +} + +TEST(QuotaCommon_OrElseWarn, Failure_MappedToError) +{ + bool fallbackRun = false; + bool tryContinued = false; + + // XXX Consider allowing to set a custom error handler, so that we can + // actually assert that a warning was emitted. + const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> { + QM_TRY(QM_OR_ELSE_WARN(OkIf(false), ([&fallbackRun](const NotOk) { + fallbackRun = true; + return mozilla::Result<mozilla::Ok, NotOk>{ + NotOk{}}; + }))); + tryContinued = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isErr()); + EXPECT_TRUE(fallbackRun); + EXPECT_FALSE(tryContinued); +} + +TEST(QuotaCommon_OrElseWarnIf, Success) +{ + bool predicateRun = false; + bool fallbackRun = false; + bool tryContinued = false; + + const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> { + QM_TRY(QM_OR_ELSE_WARN_IF( + OkIf(true), + [&predicateRun](const NotOk) { + predicateRun = true; + return false; + }, + ([&fallbackRun](const NotOk) { + fallbackRun = true; + return mozilla::Result<mozilla::Ok, NotOk>{mozilla::Ok{}}; + }))); + + tryContinued = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isOk()); + EXPECT_FALSE(predicateRun); + EXPECT_FALSE(fallbackRun); + EXPECT_TRUE(tryContinued); +} + +TEST(QuotaCommon_OrElseWarnIf, Failure_PredicateReturnsFalse) +{ + bool predicateRun = false; + bool fallbackRun = false; + bool tryContinued = false; + + const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> { + QM_TRY(QM_OR_ELSE_WARN_IF( + OkIf(false), + [&predicateRun](const NotOk) { + predicateRun = true; + return false; + }, + ([&fallbackRun](const NotOk) { + fallbackRun = true; + return mozilla::Result<mozilla::Ok, NotOk>{mozilla::Ok{}}; + }))); + + tryContinued = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isErr()); + EXPECT_TRUE(predicateRun); + EXPECT_FALSE(fallbackRun); + EXPECT_FALSE(tryContinued); +} + +TEST(QuotaCommon_OrElseWarnIf, Failure_PredicateReturnsTrue_MappedToSuccess) +{ + bool predicateRun = false; + bool fallbackRun = false; + bool tryContinued = false; + + const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> { + QM_TRY(QM_OR_ELSE_WARN_IF( + OkIf(false), + [&predicateRun](const NotOk) { + predicateRun = true; + return true; + }, + ([&fallbackRun](const NotOk) { + fallbackRun = true; + return mozilla::Result<mozilla::Ok, NotOk>{mozilla::Ok{}}; + }))); + + tryContinued = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isOk()); + EXPECT_TRUE(predicateRun); + EXPECT_TRUE(fallbackRun); + EXPECT_TRUE(tryContinued); +} + +TEST(QuotaCommon_OrElseWarnIf, Failure_PredicateReturnsTrue_MappedToError) +{ + bool predicateRun = false; + bool fallbackRun = false; + bool tryContinued = false; + + const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> { + QM_TRY(QM_OR_ELSE_WARN_IF( + OkIf(false), + [&predicateRun](const NotOk) { + predicateRun = true; + return true; + }, + ([&fallbackRun](const NotOk) { + fallbackRun = true; + return mozilla::Result<mozilla::Ok, NotOk>{mozilla::NotOk{}}; + }))); + + tryContinued = true; + return mozilla::Ok{}; + }(); + + EXPECT_TRUE(res.isErr()); + EXPECT_TRUE(predicateRun); + EXPECT_TRUE(fallbackRun); + EXPECT_FALSE(tryContinued); +} + +TEST(QuotaCommon_OkIf, True) +{ + auto res = OkIf(true); + + EXPECT_TRUE(res.isOk()); +} + +TEST(QuotaCommon_OkIf, False) +{ + auto res = OkIf(false); + + EXPECT_TRUE(res.isErr()); +} + +TEST(QuotaCommon_OkToOk, Bool_True) +{ + auto res = OkToOk<true>(Ok()); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), true); +} + +TEST(QuotaCommon_OkToOk, Bool_False) +{ + auto res = OkToOk<false>(Ok()); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), false); +} + +TEST(QuotaCommon_OkToOk, Int_42) +{ + auto res = OkToOk<42>(Ok()); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), 42); +} + +TEST(QuotaCommon_ErrToOkOrErr, Bool_True) +{ + auto res = ErrToOkOrErr<NS_ERROR_FAILURE, true>(NS_ERROR_FAILURE); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), true); +} + +TEST(QuotaCommon_ErrToOkOrErr, Bool_True_Err) +{ + auto res = ErrToOkOrErr<NS_ERROR_FAILURE, true>(NS_ERROR_UNEXPECTED); + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED); +} + +TEST(QuotaCommon_ErrToOkOrErr, Bool_False) +{ + auto res = ErrToOkOrErr<NS_ERROR_FAILURE, false>(NS_ERROR_FAILURE); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), false); +} + +TEST(QuotaCommon_ErrToOkOrErr, Bool_False_Err) +{ + auto res = ErrToOkOrErr<NS_ERROR_FAILURE, false>(NS_ERROR_UNEXPECTED); + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED); +} + +TEST(QuotaCommon_ErrToOkOrErr, Int_42) +{ + auto res = ErrToOkOrErr<NS_ERROR_FAILURE, 42>(NS_ERROR_FAILURE); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), 42); +} + +TEST(QuotaCommon_ErrToOkOrErr, Int_42_Err) +{ + auto res = ErrToOkOrErr<NS_ERROR_FAILURE, 42>(NS_ERROR_UNEXPECTED); + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED); +} + +TEST(QuotaCommon_ErrToOkOrErr, NsCOMPtr_nullptr) +{ + auto res = ErrToOkOrErr<NS_ERROR_FAILURE, nullptr, nsCOMPtr<nsISupports>>( + NS_ERROR_FAILURE); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), nullptr); +} + +TEST(QuotaCommon_ErrToOkOrErr, NsCOMPtr_nullptr_Err) +{ + auto res = ErrToOkOrErr<NS_ERROR_FAILURE, nullptr, nsCOMPtr<nsISupports>>( + NS_ERROR_UNEXPECTED); + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED); +} + +TEST(QuotaCommon_ErrToDefaultOkOrErr, Ok) +{ + auto res = ErrToDefaultOkOrErr<NS_ERROR_FAILURE, Ok>(NS_ERROR_FAILURE); + EXPECT_TRUE(res.isOk()); +} + +TEST(QuotaCommon_ErrToDefaultOkOrErr, Ok_Err) +{ + auto res = ErrToDefaultOkOrErr<NS_ERROR_FAILURE, Ok>(NS_ERROR_UNEXPECTED); + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED); +} + +TEST(QuotaCommon_ErrToDefaultOkOrErr, NsCOMPtr) +{ + auto res = ErrToDefaultOkOrErr<NS_ERROR_FAILURE, nsCOMPtr<nsISupports>>( + NS_ERROR_FAILURE); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), nullptr); +} + +TEST(QuotaCommon_ErrToDefaultOkOrErr, NsCOMPtr_Err) +{ + auto res = ErrToDefaultOkOrErr<NS_ERROR_FAILURE, nsCOMPtr<nsISupports>>( + NS_ERROR_UNEXPECTED); + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED); +} + +TEST(QuotaCommon_IsSpecificError, Match) +{ EXPECT_TRUE(IsSpecificError<NS_ERROR_FAILURE>(NS_ERROR_FAILURE)); } + +TEST(QuotaCommon_IsSpecificError, Mismatch) +{ EXPECT_FALSE(IsSpecificError<NS_ERROR_FAILURE>(NS_ERROR_UNEXPECTED)); } + +TEST(QuotaCommon_ErrToOk, Bool_True) +{ + auto res = ErrToOk<true>(NS_ERROR_FAILURE); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), true); +} + +TEST(QuotaCommon_ErrToOk, Bool_False) +{ + auto res = ErrToOk<false>(NS_ERROR_FAILURE); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), false); +} + +TEST(QuotaCommon_ErrToOk, Int_42) +{ + auto res = ErrToOk<42>(NS_ERROR_FAILURE); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), 42); +} + +TEST(QuotaCommon_ErrToOk, NsCOMPtr_nullptr) +{ + auto res = ErrToOk<nullptr, nsCOMPtr<nsISupports>>(NS_ERROR_FAILURE); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), nullptr); +} + +TEST(QuotaCommon_ErrToDefaultOk, Ok) +{ + auto res = ErrToDefaultOk<Ok>(NS_ERROR_FAILURE); + EXPECT_TRUE(res.isOk()); +} + +TEST(QuotaCommon_ErrToDefaultOk, NsCOMPtr) +{ + auto res = ErrToDefaultOk<nsCOMPtr<nsISupports>>(NS_ERROR_FAILURE); + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), nullptr); +} + +class StringPairParameterized + : public ::testing::TestWithParam<std::pair<const char*, const char*>> {}; + +TEST_P(StringPairParameterized, AnonymizedOriginString) { + const auto [in, expectedAnonymized] = GetParam(); + const auto anonymized = AnonymizedOriginString(nsDependentCString(in)); + EXPECT_STREQ(anonymized.get(), expectedAnonymized); +} + +INSTANTIATE_TEST_SUITE_P( + QuotaCommon, StringPairParameterized, + ::testing::Values( + // XXX Do we really want to anonymize about: origins? + std::pair("about:home", "about:aaaa"), + std::pair("https://foo.bar.com", "https://aaa.aaa.aaa"), + std::pair("https://foo.bar.com:8000", "https://aaa.aaa.aaa:DDDD"), + std::pair("file://UNIVERSAL_FILE_ORIGIN", + "file://aaaaaaaaa_aaaa_aaaaaa"))); + +// BEGIN COPY FROM mfbt/tests/TestResult.cpp +struct Failed {}; + +static GenericErrorResult<Failed> Fail() { return Err(Failed()); } + +static Result<Ok, Failed> Task1(bool pass) { + if (!pass) { + return Fail(); // implicit conversion from GenericErrorResult to Result + } + return Ok(); +} +// END COPY FROM mfbt/tests/TestResult.cpp + +static Result<bool, Failed> Condition(bool aNoError, bool aResult) { + return Task1(aNoError).map([aResult](auto) { return aResult; }); +} + +TEST(QuotaCommon_CollectWhileTest, NoFailures) +{ + const size_t loopCount = 5; + size_t conditionExecutions = 0; + size_t bodyExecutions = 0; + auto result = CollectWhile( + [&conditionExecutions] { + ++conditionExecutions; + return Condition(true, conditionExecutions <= loopCount); + }, + [&bodyExecutions] { + ++bodyExecutions; + return Task1(true); + }); + static_assert(std::is_same_v<decltype(result), Result<Ok, Failed>>); + MOZ_RELEASE_ASSERT(result.isOk()); + MOZ_RELEASE_ASSERT(loopCount == bodyExecutions); + MOZ_RELEASE_ASSERT(1 + loopCount == conditionExecutions); +} + +TEST(QuotaCommon_CollectWhileTest, BodyFailsImmediately) +{ + size_t conditionExecutions = 0; + size_t bodyExecutions = 0; + auto result = CollectWhile( + [&conditionExecutions] { + ++conditionExecutions; + return Condition(true, true); + }, + [&bodyExecutions] { + ++bodyExecutions; + return Task1(false); + }); + static_assert(std::is_same_v<decltype(result), Result<Ok, Failed>>); + MOZ_RELEASE_ASSERT(result.isErr()); + MOZ_RELEASE_ASSERT(1 == bodyExecutions); + MOZ_RELEASE_ASSERT(1 == conditionExecutions); +} + +TEST(QuotaCommon_CollectWhileTest, BodyFailsOnSecondExecution) +{ + size_t conditionExecutions = 0; + size_t bodyExecutions = 0; + auto result = CollectWhile( + [&conditionExecutions] { + ++conditionExecutions; + return Condition(true, true); + }, + [&bodyExecutions] { + ++bodyExecutions; + return Task1(bodyExecutions < 2); + }); + static_assert(std::is_same_v<decltype(result), Result<Ok, Failed>>); + MOZ_RELEASE_ASSERT(result.isErr()); + MOZ_RELEASE_ASSERT(2 == bodyExecutions); + MOZ_RELEASE_ASSERT(2 == conditionExecutions); +} + +TEST(QuotaCommon_CollectWhileTest, ConditionFailsImmediately) +{ + size_t conditionExecutions = 0; + size_t bodyExecutions = 0; + auto result = CollectWhile( + [&conditionExecutions] { + ++conditionExecutions; + return Condition(false, true); + }, + [&bodyExecutions] { + ++bodyExecutions; + return Task1(true); + }); + static_assert(std::is_same_v<decltype(result), Result<Ok, Failed>>); + MOZ_RELEASE_ASSERT(result.isErr()); + MOZ_RELEASE_ASSERT(0 == bodyExecutions); + MOZ_RELEASE_ASSERT(1 == conditionExecutions); +} + +TEST(QuotaCommon_CollectWhileTest, ConditionFailsOnSecondExecution) +{ + size_t conditionExecutions = 0; + size_t bodyExecutions = 0; + auto result = CollectWhile( + [&conditionExecutions] { + ++conditionExecutions; + return Condition(conditionExecutions < 2, true); + }, + [&bodyExecutions] { + ++bodyExecutions; + return Task1(true); + }); + static_assert(std::is_same_v<decltype(result), Result<Ok, Failed>>); + MOZ_RELEASE_ASSERT(result.isErr()); + MOZ_RELEASE_ASSERT(1 == bodyExecutions); + MOZ_RELEASE_ASSERT(2 == conditionExecutions); +} + +TEST(QuotaCommon_CollectEachInRange, Success) +{ + size_t bodyExecutions = 0; + const auto result = CollectEachInRange( + std::array<int, 5>{{1, 2, 3, 4, 5}}, + [&bodyExecutions](const int val) -> Result<Ok, nsresult> { + ++bodyExecutions; + return Ok{}; + }); + + MOZ_RELEASE_ASSERT(result.isOk()); + MOZ_RELEASE_ASSERT(5 == bodyExecutions); +} + +TEST(QuotaCommon_CollectEachInRange, FailureShortCircuit) +{ + size_t bodyExecutions = 0; + const auto result = CollectEachInRange( + std::array<int, 5>{{1, 2, 3, 4, 5}}, + [&bodyExecutions](const int val) -> Result<Ok, nsresult> { + ++bodyExecutions; + return val == 3 ? Err(NS_ERROR_FAILURE) : Result<Ok, nsresult>{Ok{}}; + }); + + MOZ_RELEASE_ASSERT(result.isErr()); + MOZ_RELEASE_ASSERT(NS_ERROR_FAILURE == result.inspectErr()); + MOZ_RELEASE_ASSERT(3 == bodyExecutions); +} + +TEST(QuotaCommon_ReduceEach, Success) +{ + const auto result = ReduceEach( + [i = int{0}]() mutable -> Result<int, Failed> { + if (i < 5) { + return ++i; + } + return 0; + }, + 0, [](int val, int add) -> Result<int, Failed> { return val + add; }); + static_assert(std::is_same_v<decltype(result), const Result<int, Failed>>); + + MOZ_RELEASE_ASSERT(result.isOk()); + MOZ_RELEASE_ASSERT(15 == result.inspect()); +} + +TEST(QuotaCommon_ReduceEach, StepError) +{ + const auto result = ReduceEach( + [i = int{0}]() mutable -> Result<int, Failed> { + if (i < 5) { + return ++i; + } + return 0; + }, + 0, + [](int val, int add) -> Result<int, Failed> { + if (val > 2) { + return Err(Failed{}); + } + return val + add; + }); + static_assert(std::is_same_v<decltype(result), const Result<int, Failed>>); + + MOZ_RELEASE_ASSERT(result.isErr()); +} + +TEST(QuotaCommon_ReduceEach, GeneratorError) +{ + size_t generatorExecutions = 0; + const auto result = ReduceEach( + [i = int{0}, &generatorExecutions]() mutable -> Result<int, Failed> { + ++generatorExecutions; + if (i < 1) { + return ++i; + } + return Err(Failed{}); + }, + 0, + [](int val, int add) -> Result<int, Failed> { + if (val > 2) { + return Err(Failed{}); + } + return val + add; + }); + static_assert(std::is_same_v<decltype(result), const Result<int, Failed>>); + + MOZ_RELEASE_ASSERT(result.isErr()); + MOZ_RELEASE_ASSERT(2 == generatorExecutions); +} + +TEST(QuotaCommon_Reduce, Success) +{ + const auto range = std::vector{0, 1, 2, 3, 4, 5}; + const auto result = Reduce( + range, 0, [](int val, Maybe<const int&> add) -> Result<int, Failed> { + return val + add.ref(); + }); + static_assert(std::is_same_v<decltype(result), const Result<int, Failed>>); + + MOZ_RELEASE_ASSERT(result.isOk()); + MOZ_RELEASE_ASSERT(15 == result.inspect()); +} + +TEST(QuotaCommon_CallWithDelayedRetriesIfAccessDenied, NoFailures) +{ + uint32_t tries = 0; + + auto res = CallWithDelayedRetriesIfAccessDenied( + [&tries]() -> Result<Ok, nsresult> { + ++tries; + return Ok{}; + }, + 10, 2); + + EXPECT_EQ(tries, 1u); + EXPECT_TRUE(res.isOk()); +} + +TEST(QuotaCommon_CallWithDelayedRetriesIfAccessDenied, PermanentFailures) +{ + uint32_t tries = 0; + + auto res = CallWithDelayedRetriesIfAccessDenied( + [&tries]() -> Result<Ok, nsresult> { + ++tries; + return Err(NS_ERROR_FILE_IS_LOCKED); + }, + 10, 2); + + EXPECT_EQ(tries, 11u); + EXPECT_TRUE(res.isErr()); +} + +TEST(QuotaCommon_CallWithDelayedRetriesIfAccessDenied, FailuresAndSuccess) +{ + uint32_t tries = 0; + + auto res = CallWithDelayedRetriesIfAccessDenied( + [&tries]() -> Result<Ok, nsresult> { + if (++tries == 5) { + return Ok{}; + } + return Err(NS_ERROR_FILE_ACCESS_DENIED); + }, + 10, 2); + + EXPECT_EQ(tries, 5u); + EXPECT_TRUE(res.isOk()); +} + +TEST(QuotaCommon_MakeSourceFileRelativePath, ThisSourceFile) +{ + static constexpr auto thisSourceFileRelativePath = + "dom/quota/test/gtest/TestQuotaCommon.cpp"_ns; + + const nsCString sourceFileRelativePath{ + mozilla::dom::quota::detail::MakeSourceFileRelativePath( + nsLiteralCString(__FILE__))}; + + EXPECT_STREQ(sourceFileRelativePath.get(), thisSourceFileRelativePath.get()); +} + +static nsCString MakeTreePath(const nsACString& aBasePath, + const nsACString& aRelativePath) { + nsCString path{aBasePath}; + + path.Append("/"); + path.Append(aRelativePath); + + return path; +} + +static nsCString MakeSourceTreePath(const nsACString& aRelativePath) { + return MakeTreePath(mozilla::dom::quota::detail::GetSourceTreeBase(), + aRelativePath); +} + +static nsCString MakeObjdirDistIncludeTreePath( + const nsACString& aRelativePath) { + return MakeTreePath( + mozilla::dom::quota::detail::GetObjdirDistIncludeTreeBase(), + aRelativePath); +} + +TEST(QuotaCommon_MakeSourceFileRelativePath, DomQuotaSourceFile) +{ + static constexpr auto domQuotaSourceFileRelativePath = + "dom/quota/ActorsParent.cpp"_ns; + + const nsCString sourceFileRelativePath{ + mozilla::dom::quota::detail::MakeSourceFileRelativePath( + MakeSourceTreePath(domQuotaSourceFileRelativePath))}; + + EXPECT_STREQ(sourceFileRelativePath.get(), + domQuotaSourceFileRelativePath.get()); +} + +TEST(QuotaCommon_MakeSourceFileRelativePath, DomQuotaSourceFile_Exported) +{ + static constexpr auto mozillaDomQuotaSourceFileRelativePath = + "mozilla/dom/quota/QuotaCommon.h"_ns; + + static constexpr auto domQuotaSourceFileRelativePath = + "dom/quota/QuotaCommon.h"_ns; + + const nsCString sourceFileRelativePath{ + mozilla::dom::quota::detail::MakeSourceFileRelativePath( + MakeObjdirDistIncludeTreePath( + mozillaDomQuotaSourceFileRelativePath))}; + + EXPECT_STREQ(sourceFileRelativePath.get(), + domQuotaSourceFileRelativePath.get()); +} + +TEST(QuotaCommon_MakeSourceFileRelativePath, DomIndexedDBSourceFile) +{ + static constexpr auto domIndexedDBSourceFileRelativePath = + "dom/indexedDB/ActorsParent.cpp"_ns; + + const nsCString sourceFileRelativePath{ + mozilla::dom::quota::detail::MakeSourceFileRelativePath( + MakeSourceTreePath(domIndexedDBSourceFileRelativePath))}; + + EXPECT_STREQ(sourceFileRelativePath.get(), + domIndexedDBSourceFileRelativePath.get()); +} + +TEST(QuotaCommon_MakeSourceFileRelativePath, + DomLocalstorageSourceFile_Exported_Mapped) +{ + static constexpr auto mozillaDomSourceFileRelativePath = + "mozilla/dom/LocalStorageCommon.h"_ns; + + static constexpr auto domLocalstorageSourceFileRelativePath = + "dom/localstorage/LocalStorageCommon.h"_ns; + + const nsCString sourceFileRelativePath{ + mozilla::dom::quota::detail::MakeSourceFileRelativePath( + MakeObjdirDistIncludeTreePath(mozillaDomSourceFileRelativePath))}; + + EXPECT_STREQ(sourceFileRelativePath.get(), + domLocalstorageSourceFileRelativePath.get()); +} + +TEST(QuotaCommon_MakeSourceFileRelativePath, NonDomSourceFile) +{ + static constexpr auto nonDomSourceFileRelativePath = + "storage/mozStorageService.cpp"_ns; + + const nsCString sourceFileRelativePath{ + mozilla::dom::quota::detail::MakeSourceFileRelativePath( + MakeSourceTreePath(nonDomSourceFileRelativePath))}; + + EXPECT_STREQ(sourceFileRelativePath.get(), + nonDomSourceFileRelativePath.get()); +} + +TEST(QuotaCommon_MakeSourceFileRelativePath, OtherSourceFile) +{ + constexpr auto otherSourceFilePath = "/foo/bar/Test.cpp"_ns; + const nsCString sourceFileRelativePath{ + mozilla::dom::quota::detail::MakeSourceFileRelativePath( + otherSourceFilePath)}; + + EXPECT_STREQ(sourceFileRelativePath.get(), "Test.cpp"); +} + +#ifdef __clang__ +# pragma clang diagnostic pop +#endif diff --git a/dom/quota/test/gtest/TestQuotaManager.cpp b/dom/quota/test/gtest/TestQuotaManager.cpp new file mode 100644 index 0000000000..7d185df481 --- /dev/null +++ b/dom/quota/test/gtest/TestQuotaManager.cpp @@ -0,0 +1,181 @@ +/* -*- 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 "mozilla/dom/quota/OriginScope.h" + +#include "gtest/gtest.h" + +#include <cstdint> +#include <memory> +#include "ErrorList.h" +#include "mozilla/Result.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/fallible.h" +#include "nsCOMPtr.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIFile.h" +#include "nsLiteralString.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsTLiteralString.h" + +using namespace mozilla; +using namespace mozilla::dom::quota; + +namespace { + +struct OriginTest { + const char* mOrigin; + bool mMatch; +}; + +void CheckOriginScopeMatchesOrigin(const OriginScope& aOriginScope, + const char* aOrigin, bool aMatch) { + bool result = aOriginScope.Matches( + OriginScope::FromOrigin(nsDependentCString(aOrigin))); + + EXPECT_TRUE(result == aMatch); +} + +void CheckUnknownFileEntry(nsIFile& aBase, const nsAString& aName, + const bool aWarnIfFile, const bool aWarnIfDir) { + nsCOMPtr<nsIFile> file; + nsresult rv = aBase.Clone(getter_AddRefs(file)); + ASSERT_EQ(rv, NS_OK); + + rv = file->Append(aName); + ASSERT_EQ(rv, NS_OK); + + rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 0600); + ASSERT_EQ(rv, NS_OK); + + auto okOrErr = WARN_IF_FILE_IS_UNKNOWN(*file); + ASSERT_TRUE(okOrErr.isOk()); + +#ifdef DEBUG + EXPECT_TRUE(okOrErr.inspect() == aWarnIfFile); +#else + EXPECT_TRUE(okOrErr.inspect() == false); +#endif + + rv = file->Remove(false); + ASSERT_EQ(rv, NS_OK); + + rv = file->Create(nsIFile::DIRECTORY_TYPE, 0700); + ASSERT_EQ(rv, NS_OK); + + okOrErr = WARN_IF_FILE_IS_UNKNOWN(*file); + ASSERT_TRUE(okOrErr.isOk()); + +#ifdef DEBUG + EXPECT_TRUE(okOrErr.inspect() == aWarnIfDir); +#else + EXPECT_TRUE(okOrErr.inspect() == false); +#endif + + rv = file->Remove(false); + ASSERT_EQ(rv, NS_OK); +} + +} // namespace + +TEST(QuotaManager, OriginScope) +{ + OriginScope originScope; + + // Sanity checks. + + { + constexpr auto origin = "http://www.mozilla.org"_ns; + originScope.SetFromOrigin(origin); + EXPECT_TRUE(originScope.IsOrigin()); + EXPECT_TRUE(originScope.GetOrigin().Equals(origin)); + EXPECT_TRUE(originScope.GetOriginNoSuffix().Equals(origin)); + } + + { + constexpr auto prefix = "http://www.mozilla.org"_ns; + originScope.SetFromPrefix(prefix); + EXPECT_TRUE(originScope.IsPrefix()); + EXPECT_TRUE(originScope.GetOriginNoSuffix().Equals(prefix)); + } + + { + originScope.SetFromNull(); + EXPECT_TRUE(originScope.IsNull()); + } + + // Test each origin scope type against particular origins. + + { + originScope.SetFromOrigin("http://www.mozilla.org"_ns); + + static const OriginTest tests[] = { + {"http://www.mozilla.org", true}, + {"http://www.example.org", false}, + }; + + for (const auto& test : tests) { + CheckOriginScopeMatchesOrigin(originScope, test.mOrigin, test.mMatch); + } + } + + { + originScope.SetFromPrefix("http://www.mozilla.org"_ns); + + static const OriginTest tests[] = { + {"http://www.mozilla.org", true}, + {"http://www.mozilla.org^userContextId=1", true}, + {"http://www.example.org^userContextId=1", false}, + }; + + for (const auto& test : tests) { + CheckOriginScopeMatchesOrigin(originScope, test.mOrigin, test.mMatch); + } + } + + { + originScope.SetFromNull(); + + static const OriginTest tests[] = { + {"http://www.mozilla.org", true}, + {"http://www.mozilla.org^userContextId=1", true}, + {"http://www.example.org^userContextId=1", true}, + }; + + for (const auto& test : tests) { + CheckOriginScopeMatchesOrigin(originScope, test.mOrigin, test.mMatch); + } + } +} + +TEST(QuotaManager, WarnIfUnknownFile) +{ + nsCOMPtr<nsIFile> base; + nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(base)); + ASSERT_EQ(rv, NS_OK); + + rv = base->Append(u"mozquotatests"_ns); + ASSERT_EQ(rv, NS_OK); + + base->Remove(true); + + rv = base->Create(nsIFile::DIRECTORY_TYPE, 0700); + ASSERT_EQ(rv, NS_OK); + + CheckUnknownFileEntry(*base, u"foo.bar"_ns, true, true); + CheckUnknownFileEntry(*base, u".DS_Store"_ns, false, true); + CheckUnknownFileEntry(*base, u".desktop"_ns, false, true); + CheckUnknownFileEntry(*base, u"desktop.ini"_ns, false, true); + CheckUnknownFileEntry(*base, u"DESKTOP.INI"_ns, false, true); + CheckUnknownFileEntry(*base, u"thumbs.db"_ns, false, true); + CheckUnknownFileEntry(*base, u"THUMBS.DB"_ns, false, true); + CheckUnknownFileEntry(*base, u".xyz"_ns, false, true); + + rv = base->Remove(true); + ASSERT_EQ(rv, NS_OK); +} diff --git a/dom/quota/test/gtest/TestResultExtensions.cpp b/dom/quota/test/gtest/TestResultExtensions.cpp new file mode 100644 index 0000000000..137acc1423 --- /dev/null +++ b/dom/quota/test/gtest/TestResultExtensions.cpp @@ -0,0 +1,333 @@ +/* -*- 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 "Common.h" +#include "gtest/gtest.h" +#include "mozilla/dom/QMResult.h" +#include "mozilla/dom/quota/ResultExtensions.h" + +using namespace mozilla; +using namespace mozilla::dom::quota; + +namespace { +class TestClass { + public: + static constexpr int kTestValue = 42; + + nsresult NonOverloadedNoInputComplex(std::pair<int, int>* aOut) { + *aOut = std::pair{kTestValue, kTestValue}; + return NS_OK; + } + nsresult NonOverloadedNoInputFailsComplex(std::pair<int, int>* aOut) { + return NS_ERROR_FAILURE; + } +}; +} // namespace + +class DOM_Quota_ResultExtensions_ToResult : public DOM_Quota_Test {}; +class DOM_Quota_ResultExtensions_GenericErrorResult : public DOM_Quota_Test {}; + +TEST_F(DOM_Quota_ResultExtensions_ToResult, FromBool) { + // success + { + auto res = ToResult(true); + static_assert(std::is_same_v<decltype(res), Result<Ok, nsresult>>); + EXPECT_TRUE(res.isOk()); + } + + // failure + { + auto res = ToResult(false); + static_assert(std::is_same_v<decltype(res), Result<Ok, nsresult>>); + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE); + } +} + +TEST_F(DOM_Quota_ResultExtensions_ToResult, FromQMResult_Failure) { + // copy + { + const auto res = ToQMResult(NS_ERROR_FAILURE); + auto okOrErr = ToResult<QMResult>(res); + static_assert(std::is_same_v<decltype(okOrErr), OkOrErr>); + ASSERT_TRUE(okOrErr.isErr()); + auto err = okOrErr.unwrapErr(); + +#ifdef QM_ERROR_STACKS_ENABLED + IncreaseExpectedStackId(); + + ASSERT_EQ(err.StackId(), ExpectedStackId()); + ASSERT_EQ(err.FrameId(), 1u); + ASSERT_EQ(err.NSResult(), NS_ERROR_FAILURE); +#else + ASSERT_EQ(err, NS_ERROR_FAILURE); +#endif + } + + // move + { + auto res = ToQMResult(NS_ERROR_FAILURE); + auto okOrErr = ToResult<QMResult>(std::move(res)); + static_assert(std::is_same_v<decltype(okOrErr), OkOrErr>); + ASSERT_TRUE(okOrErr.isErr()); + auto err = okOrErr.unwrapErr(); + +#ifdef QM_ERROR_STACKS_ENABLED + IncreaseExpectedStackId(); + + ASSERT_EQ(err.StackId(), ExpectedStackId()); + ASSERT_EQ(err.FrameId(), 1u); + ASSERT_EQ(err.NSResult(), NS_ERROR_FAILURE); +#else + ASSERT_EQ(err, NS_ERROR_FAILURE); +#endif + } +} + +TEST_F(DOM_Quota_ResultExtensions_ToResult, FromNSResult_Failure_Macro) { + auto okOrErr = QM_TO_RESULT(NS_ERROR_FAILURE); + static_assert(std::is_same_v<decltype(okOrErr), OkOrErr>); + ASSERT_TRUE(okOrErr.isErr()); + auto err = okOrErr.unwrapErr(); + +#ifdef QM_ERROR_STACKS_ENABLED + IncreaseExpectedStackId(); + + ASSERT_EQ(err.StackId(), ExpectedStackId()); + ASSERT_EQ(err.FrameId(), 1u); + ASSERT_EQ(err.NSResult(), NS_ERROR_FAILURE); +#else + ASSERT_EQ(err, NS_ERROR_FAILURE); +#endif +} + +TEST_F(DOM_Quota_ResultExtensions_GenericErrorResult, ErrorPropagation) { + OkOrErr okOrErr1 = ToResult<QMResult>(ToQMResult(NS_ERROR_FAILURE)); + const auto& err1 = okOrErr1.inspectErr(); + +#ifdef QM_ERROR_STACKS_ENABLED + IncreaseExpectedStackId(); + + ASSERT_EQ(err1.StackId(), ExpectedStackId()); + ASSERT_EQ(err1.FrameId(), 1u); + ASSERT_EQ(err1.NSResult(), NS_ERROR_FAILURE); +#else + ASSERT_EQ(err1, NS_ERROR_FAILURE); +#endif + + OkOrErr okOrErr2 = okOrErr1.propagateErr(); + const auto& err2 = okOrErr2.inspectErr(); + +#ifdef QM_ERROR_STACKS_ENABLED + ASSERT_EQ(err2.StackId(), ExpectedStackId()); + ASSERT_EQ(err2.FrameId(), 2u); + ASSERT_EQ(err2.NSResult(), NS_ERROR_FAILURE); +#else + ASSERT_EQ(err2, NS_ERROR_FAILURE); +#endif + + OkOrErr okOrErr3 = okOrErr2.propagateErr(); + const auto& err3 = okOrErr3.inspectErr(); + +#ifdef QM_ERROR_STACKS_ENABLED + ASSERT_EQ(err3.StackId(), ExpectedStackId()); + ASSERT_EQ(err3.FrameId(), 3u); + ASSERT_EQ(err3.NSResult(), NS_ERROR_FAILURE); +#else + ASSERT_EQ(err3, NS_ERROR_FAILURE); +#endif +} + +TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_NoInput) +{ + auto res = ToResultGet<int32_t>([](nsresult* aRv) -> int32_t { + *aRv = NS_OK; + return 42; + }); + + static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>); + + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), 42); +} + +TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_NoInput_Err) +{ + auto res = ToResultGet<int32_t>([](nsresult* aRv) -> int32_t { + *aRv = NS_ERROR_FAILURE; + return -1; + }); + + static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>); + + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE); +} + +TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_WithInput) +{ + auto res = ToResultGet<int32_t>( + [](int32_t aValue, nsresult* aRv) -> int32_t { + *aRv = NS_OK; + return aValue * 2; + }, + 42); + + static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>); + + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), 84); +} + +TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_WithInput_Err) +{ + auto res = ToResultGet<int32_t>( + [](int32_t aValue, nsresult* aRv) -> int32_t { + *aRv = NS_ERROR_FAILURE; + return -1; + }, + 42); + + static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>); + + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE); +} + +TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_NoInput_Macro_Typed) +{ + auto res = MOZ_TO_RESULT_GET_TYPED(int32_t, [](nsresult* aRv) -> int32_t { + *aRv = NS_OK; + return 42; + }); + + static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>); + + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), 42); +} + +TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_NoInput_Macro_Typed_Parens) +{ + auto res = + MOZ_TO_RESULT_GET_TYPED((std::pair<int32_t, int32_t>), + [](nsresult* aRv) -> std::pair<int32_t, int32_t> { + *aRv = NS_OK; + return std::pair{42, 42}; + }); + + static_assert(std::is_same_v<decltype(res), + Result<std::pair<int32_t, int32_t>, nsresult>>); + + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), (std::pair{42, 42})); +} + +TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_NoInput_Err_Macro_Typed) +{ + auto res = MOZ_TO_RESULT_GET_TYPED(int32_t, [](nsresult* aRv) -> int32_t { + *aRv = NS_ERROR_FAILURE; + return -1; + }); + + static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>); + + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE); +} + +TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_WithInput_Macro_Typed) +{ + auto res = MOZ_TO_RESULT_GET_TYPED( + int32_t, + [](int32_t aValue, nsresult* aRv) -> int32_t { + *aRv = NS_OK; + return aValue * 2; + }, + 42); + + static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>); + + EXPECT_TRUE(res.isOk()); + EXPECT_EQ(res.unwrap(), 84); +} + +TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_WithInput_Err_Macro_Typed) +{ + auto res = MOZ_TO_RESULT_GET_TYPED( + int32_t, + [](int32_t aValue, nsresult* aRv) -> int32_t { + *aRv = NS_ERROR_FAILURE; + return -1; + }, + 42); + + static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>); + + EXPECT_TRUE(res.isErr()); + EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE); +} + +TEST(DOM_Quota_ResultExtensions_ToResultInvoke, Lambda_NoInput_Complex) +{ + TestClass foo; + + // success + { + auto valOrErr = + ToResultInvoke<std::pair<int, int>>([&foo](std::pair<int, int>* out) { + return foo.NonOverloadedNoInputComplex(out); + }); + static_assert(std::is_same_v<decltype(valOrErr), + Result<std::pair<int, int>, nsresult>>); + ASSERT_TRUE(valOrErr.isOk()); + ASSERT_EQ((std::pair{TestClass::kTestValue, TestClass::kTestValue}), + valOrErr.unwrap()); + } + + // failure + { + auto valOrErr = + ToResultInvoke<std::pair<int, int>>([&foo](std::pair<int, int>* out) { + return foo.NonOverloadedNoInputFailsComplex(out); + }); + static_assert(std::is_same_v<decltype(valOrErr), + Result<std::pair<int, int>, nsresult>>); + ASSERT_TRUE(valOrErr.isErr()); + ASSERT_EQ(NS_ERROR_FAILURE, valOrErr.unwrapErr()); + } +} + +TEST(DOM_Quota_ResultExtensions_ToResultInvoke, + Lambda_NoInput_Complex_Macro_Typed) +{ + TestClass foo; + + // success + { + auto valOrErr = MOZ_TO_RESULT_INVOKE_TYPED( + (std::pair<int, int>), [&foo](std::pair<int, int>* out) { + return foo.NonOverloadedNoInputComplex(out); + }); + static_assert(std::is_same_v<decltype(valOrErr), + Result<std::pair<int, int>, nsresult>>); + ASSERT_TRUE(valOrErr.isOk()); + ASSERT_EQ((std::pair{TestClass::kTestValue, TestClass::kTestValue}), + valOrErr.unwrap()); + } + + // failure + { + auto valOrErr = MOZ_TO_RESULT_INVOKE_TYPED( + (std::pair<int, int>), [&foo](std::pair<int, int>* out) { + return foo.NonOverloadedNoInputFailsComplex(out); + }); + static_assert(std::is_same_v<decltype(valOrErr), + Result<std::pair<int, int>, nsresult>>); + ASSERT_TRUE(valOrErr.isErr()); + ASSERT_EQ(NS_ERROR_FAILURE, valOrErr.unwrapErr()); + } +} diff --git a/dom/quota/test/gtest/TestScopedLogExtraInfo.cpp b/dom/quota/test/gtest/TestScopedLogExtraInfo.cpp new file mode 100644 index 0000000000..00a3393844 --- /dev/null +++ b/dom/quota/test/gtest/TestScopedLogExtraInfo.cpp @@ -0,0 +1,65 @@ +/* -*- 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 "mozilla/dom/quota/ScopedLogExtraInfo.h" + +#include "gtest/gtest.h" + +using namespace mozilla::dom::quota; + +TEST(DOM_Quota_ScopedLogExtraInfo, AddAndRemove) +{ + static constexpr auto text = "foo"_ns; + + { + const auto extraInfo = + ScopedLogExtraInfo{ScopedLogExtraInfo::kTagQuery, text}; + +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + const auto& extraInfoMap = ScopedLogExtraInfo::GetExtraInfoMap(); + + EXPECT_EQ(text, *extraInfoMap.at(ScopedLogExtraInfo::kTagQuery)); +#endif + } + +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + const auto& extraInfoMap = ScopedLogExtraInfo::GetExtraInfoMap(); + + EXPECT_EQ(0u, extraInfoMap.count(ScopedLogExtraInfo::kTagQuery)); +#endif +} + +TEST(DOM_Quota_ScopedLogExtraInfo, Nested) +{ + static constexpr auto text = "foo"_ns; + static constexpr auto nestedText = "bar"_ns; + + { + const auto extraInfo = + ScopedLogExtraInfo{ScopedLogExtraInfo::kTagQuery, text}; + + { + const auto extraInfo = + ScopedLogExtraInfo{ScopedLogExtraInfo::kTagQuery, nestedText}; + +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + const auto& extraInfoMap = ScopedLogExtraInfo::GetExtraInfoMap(); + EXPECT_EQ(nestedText, *extraInfoMap.at(ScopedLogExtraInfo::kTagQuery)); +#endif + } + +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + const auto& extraInfoMap = ScopedLogExtraInfo::GetExtraInfoMap(); + EXPECT_EQ(text, *extraInfoMap.at(ScopedLogExtraInfo::kTagQuery)); +#endif + } + +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + const auto& extraInfoMap = ScopedLogExtraInfo::GetExtraInfoMap(); + + EXPECT_EQ(0u, extraInfoMap.count(ScopedLogExtraInfo::kTagQuery)); +#endif +} diff --git a/dom/quota/test/gtest/TestUsageInfo.cpp b/dom/quota/test/gtest/TestUsageInfo.cpp new file mode 100644 index 0000000000..124783b715 --- /dev/null +++ b/dom/quota/test/gtest/TestUsageInfo.cpp @@ -0,0 +1,136 @@ +/* -*- 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 "mozilla/dom/quota/UsageInfo.h" + +#include "gtest/gtest.h" + +#include <cstdint> +#include <memory> +#include <ostream> +#include <utility> +#include "mozilla/Maybe.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/fallible.h" + +using namespace mozilla; +using namespace mozilla::dom::quota; + +namespace { +constexpr uint64_t kTestValue = 42; +constexpr uint64_t kTestValueZero = 0; +} // namespace + +TEST(DOM_Quota_UsageInfo, DefaultConstructed) +{ + const UsageInfo usageInfo; + EXPECT_EQ(Nothing(), usageInfo.FileUsage()); + EXPECT_EQ(Nothing(), usageInfo.TotalUsage()); +} + +TEST(DOM_Quota_UsageInfo, FileOnly) +{ + const UsageInfo usageInfo = [] { + UsageInfo usageInfo; + usageInfo += FileUsageType(Some(kTestValue)); + return usageInfo; + }(); + EXPECT_EQ(Some(kTestValue), usageInfo.FileUsage()); + EXPECT_EQ(Some(kTestValue), usageInfo.TotalUsage()); +} + +TEST(DOM_Quota_UsageInfo, DatabaseOnly) +{ + const UsageInfo usageInfo = [] { + UsageInfo usageInfo; + usageInfo += DatabaseUsageType(Some(kTestValue)); + return usageInfo; + }(); + EXPECT_EQ(Nothing(), usageInfo.FileUsage()); + EXPECT_EQ(Some(kTestValue), usageInfo.TotalUsage()); +} + +TEST(DOM_Quota_UsageInfo, FileOnly_Zero) +{ + const UsageInfo usageInfo = [] { + UsageInfo usageInfo; + usageInfo += FileUsageType(Some(kTestValueZero)); + return usageInfo; + }(); + EXPECT_EQ(Some(kTestValueZero), usageInfo.FileUsage()); + EXPECT_EQ(Some(kTestValueZero), usageInfo.TotalUsage()); +} + +TEST(DOM_Quota_UsageInfo, DatabaseOnly_Zero) +{ + const UsageInfo usageInfo = [] { + UsageInfo usageInfo; + usageInfo += DatabaseUsageType(Some(kTestValueZero)); + return usageInfo; + }(); + EXPECT_EQ(Nothing(), usageInfo.FileUsage()); + EXPECT_EQ(Some(kTestValueZero), usageInfo.TotalUsage()); +} + +TEST(DOM_Quota_UsageInfo, Both) +{ + const UsageInfo usageInfo = [] { + UsageInfo usageInfo; + usageInfo += FileUsageType(Some(kTestValue)); + usageInfo += DatabaseUsageType(Some(kTestValue)); + return usageInfo; + }(); + EXPECT_EQ(Some(kTestValue), usageInfo.FileUsage()); + EXPECT_EQ(Some(2 * kTestValue), usageInfo.TotalUsage()); +} + +TEST(DOM_Quota_UsageInfo, Both_Zero) +{ + const UsageInfo usageInfo = [] { + UsageInfo usageInfo; + usageInfo += FileUsageType(Some(kTestValueZero)); + usageInfo += DatabaseUsageType(Some(kTestValueZero)); + return usageInfo; + }(); + EXPECT_EQ(Some(kTestValueZero), usageInfo.FileUsage()); + EXPECT_EQ(Some(kTestValueZero), usageInfo.TotalUsage()); +} + +TEST(DOM_Quota_UsageInfo, CapCombined) +{ + const UsageInfo usageInfo = [] { + UsageInfo usageInfo; + usageInfo += FileUsageType(Some(UINT64_MAX)); + usageInfo += DatabaseUsageType(Some(kTestValue)); + return usageInfo; + }(); + EXPECT_EQ(Some(UINT64_MAX), usageInfo.FileUsage()); + EXPECT_EQ(Some(UINT64_MAX), usageInfo.TotalUsage()); +} + +TEST(DOM_Quota_UsageInfo, CapFileUsage) +{ + const UsageInfo usageInfo = [] { + UsageInfo usageInfo; + usageInfo += FileUsageType(Some(UINT64_MAX)); + usageInfo += FileUsageType(Some(kTestValue)); + return usageInfo; + }(); + EXPECT_EQ(Some(UINT64_MAX), usageInfo.FileUsage()); + EXPECT_EQ(Some(UINT64_MAX), usageInfo.TotalUsage()); +} + +TEST(DOM_Quota_UsageInfo, CapDatabaseUsage) +{ + const UsageInfo usageInfo = [] { + UsageInfo usageInfo; + usageInfo += DatabaseUsageType(Some(UINT64_MAX)); + usageInfo += DatabaseUsageType(Some(kTestValue)); + return usageInfo; + }(); + EXPECT_EQ(Nothing(), usageInfo.FileUsage()); + EXPECT_EQ(Some(UINT64_MAX), usageInfo.TotalUsage()); +} diff --git a/dom/quota/test/gtest/moz.build b/dom/quota/test/gtest/moz.build new file mode 100644 index 0000000000..30ca99b254 --- /dev/null +++ b/dom/quota/test/gtest/moz.build @@ -0,0 +1,44 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.dom.quota += [ + "QuotaTestChild.h", + "QuotaTestParent.h", +] + +EXPORTS.mozilla.dom.quota.test += [ + "QuotaManagerDependencyFixture.h", +] + +UNIFIED_SOURCES = [ + "Common.cpp", + "QuotaManagerDependencyFixture.cpp", + "TestCheckedUnsafePtr.cpp", + "TestClientUsageArray.cpp", + "TestEncryptedStream.cpp", + "TestFileOutputStream.cpp", + "TestFlatten.cpp", + "TestForwardDecls.cpp", + "TestPersistenceType.cpp", + "TestQMResult.cpp", + "TestQuotaCommon.cpp", + "TestQuotaManager.cpp", + "TestResultExtensions.cpp", + "TestScopedLogExtraInfo.cpp", + "TestUsageInfo.cpp", +] + +IPDL_SOURCES += [ + "PQuotaTest.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += [ + "/dom/quota", +] diff --git a/dom/quota/test/mochitest/helpers.js b/dom/quota/test/mochitest/helpers.js new file mode 100644 index 0000000000..4be2e131c2 --- /dev/null +++ b/dom/quota/test/mochitest/helpers.js @@ -0,0 +1,312 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// The path to the top level directory. +const depth = "../../../../"; + +var testGenerator; +var testHarnessGenerator; +var workerScriptPaths = []; + +loadScript("dom/quota/test/common/mochitest.js"); + +function loadScript(path) { + const url = new URL(depth + path, window.location.href); + SpecialPowers.Services.scriptloader.loadSubScript(url.href, this); +} + +function loadWorkerScript(path) { + const url = new URL(depth + path, window.location.href); + workerScriptPaths.push(url.href); +} + +function* testHarnessSteps() { + function nextTestHarnessStep(val) { + testHarnessGenerator.next(val); + } + + info("Enabling storage testing"); + + enableStorageTesting().then(nextTestHarnessStep); + yield undefined; + + info("Pushing preferences"); + + SpecialPowers.pushPrefEnv( + { + set: [["dom.storageManager.prompt.testing", true]], + }, + nextTestHarnessStep + ); + yield undefined; + + info("Clearing old databases"); + + clearAllDatabases(nextTestHarnessStep); + yield undefined; + + info("Running test in given scopes"); + + if (workerScriptPaths.length) { + info("Running test in a worker"); + + let workerScriptBlob = new Blob(["(" + workerScript.toString() + ")();"], { + type: "text/javascript", + }); + let workerScriptURL = URL.createObjectURL(workerScriptBlob); + + let worker = new Worker(workerScriptURL); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + worker.terminate(); + nextTestHarnessStep(); + }; + + worker.onmessage = function(event) { + let message = event.data; + switch (message.op) { + case "ok": + ok(message.condition, message.name + " - " + message.diag); + break; + + case "todo": + todo(message.condition, message.name, message.diag); + break; + + case "info": + info(message.msg); + break; + + case "ready": + worker.postMessage({ op: "load", files: workerScriptPaths }); + break; + + case "loaded": + worker.postMessage({ op: "start" }); + break; + + case "done": + ok(true, "Worker finished"); + nextTestHarnessStep(); + break; + + case "clearAllDatabases": + clearAllDatabases(function() { + worker.postMessage({ op: "clearAllDatabasesDone" }); + }); + break; + + default: + ok( + false, + "Received a bad message from worker: " + JSON.stringify(message) + ); + nextTestHarnessStep(); + } + }; + + URL.revokeObjectURL(workerScriptURL); + + yield undefined; + + worker.terminate(); + worker = null; + + clearAllDatabases(nextTestHarnessStep); + yield undefined; + } + + info("Running test in main thread"); + + // Now run the test script in the main thread. + if (testSteps.constructor.name === "AsyncFunction") { + SimpleTest.registerCleanupFunction(async function() { + await requestFinished(clearAllDatabases()); + }); + + add_task(testSteps); + } else { + testGenerator = testSteps(); + testGenerator.next(); + + yield undefined; + } +} + +if (!window.runTest) { + window.runTest = function() { + SimpleTest.waitForExplicitFinish(); + testHarnessGenerator = testHarnessSteps(); + testHarnessGenerator.next(); + }; +} + +function finishTest() { + SimpleTest.executeSoon(function() { + clearAllDatabases(function() { + SimpleTest.finish(); + }); + }); +} + +function grabArgAndContinueHandler(arg) { + testGenerator.next(arg); +} + +function continueToNextStep() { + SimpleTest.executeSoon(function() { + testGenerator.next(); + }); +} + +function continueToNextStepSync() { + testGenerator.next(); +} + +function workerScript() { + "use strict"; + + self.testGenerator = null; + + self.repr = function(_thing_) { + if (typeof _thing_ == "undefined") { + return "undefined"; + } + + let str; + + try { + str = _thing_ + ""; + } catch (e) { + return "[" + typeof _thing_ + "]"; + } + + if (typeof _thing_ == "function") { + str = str.replace(/^\s+/, ""); + let idx = str.indexOf("{"); + if (idx != -1) { + str = str.substr(0, idx) + "{...}"; + } + } + + return str; + }; + + self.ok = function(_condition_, _name_, _diag_) { + self.postMessage({ + op: "ok", + condition: !!_condition_, + name: _name_, + diag: _diag_, + }); + }; + + self.is = function(_a_, _b_, _name_) { + let pass = _a_ == _b_; + let diag = pass ? "" : "got " + repr(_a_) + ", expected " + repr(_b_); + ok(pass, _name_, diag); + }; + + self.isnot = function(_a_, _b_, _name_) { + let pass = _a_ != _b_; + let diag = pass ? "" : "didn't expect " + repr(_a_) + ", but got it"; + ok(pass, _name_, diag); + }; + + self.todo = function(_condition_, _name_, _diag_) { + self.postMessage({ + op: "todo", + condition: !!_condition_, + name: _name_, + diag: _diag_, + }); + }; + + self.info = function(_msg_) { + self.postMessage({ op: "info", msg: _msg_ }); + }; + + self.executeSoon = function(_fun_) { + var channel = new MessageChannel(); + channel.port1.postMessage(""); + channel.port2.onmessage = function(event) { + _fun_(); + }; + }; + + self.finishTest = function() { + self.postMessage({ op: "done" }); + }; + + self.grabArgAndContinueHandler = function(_arg_) { + testGenerator.next(_arg_); + }; + + self.continueToNextStep = function() { + executeSoon(function() { + testGenerator.next(); + }); + }; + + self.continueToNextStepSync = function() { + testGenerator.next(); + }; + + self._clearAllDatabasesCallback = undefined; + self.clearAllDatabases = function(_callback_) { + self._clearAllDatabasesCallback = _callback_; + self.postMessage({ op: "clearAllDatabases" }); + }; + + self.onerror = function(_message_, _file_, _line_) { + ok( + false, + "Worker: uncaught exception [" + + _file_ + + ":" + + _line_ + + "]: '" + + _message_ + + "'" + ); + self.finishTest(); + self.close(); + return true; + }; + + self.onmessage = function(_event_) { + let message = _event_.data; + switch (message.op) { + case "load": + info("Worker: loading " + JSON.stringify(message.files)); + self.importScripts(message.files); + self.postMessage({ op: "loaded" }); + break; + + case "start": + executeSoon(function() { + info("Worker: starting tests"); + testGenerator = testSteps(); + testGenerator.next(); + }); + break; + + case "clearAllDatabasesDone": + info("Worker: all databases are cleared"); + if (self._clearAllDatabasesCallback) { + self._clearAllDatabasesCallback(); + } + break; + + default: + throw new Error( + "Received a bad message from parent: " + JSON.stringify(message) + ); + } + }; + + self.postMessage({ op: "ready" }); +} diff --git a/dom/quota/test/mochitest/mochitest.ini b/dom/quota/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..09cd6e66c8 --- /dev/null +++ b/dom/quota/test/mochitest/mochitest.ini @@ -0,0 +1,16 @@ +# 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 + +[test_simpledb.html] +[test_storage_manager_persist_allow.html] +fail-if = xorigin +scheme=https +[test_storage_manager_persist_deny.html] +scheme=https +[test_storage_manager_persisted.html] +scheme=https diff --git a/dom/quota/test/mochitest/test_simpledb.html b/dom/quota/test/mochitest/test_simpledb.html new file mode 100644 index 0000000000..29ca8be65e --- /dev/null +++ b/dom/quota/test/mochitest/test_simpledb.html @@ -0,0 +1,21 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>SimpleDB 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="helpers.js"></script> + <script type="text/javascript"> + loadScript("dom/quota/test/common/test_simpledb.js"); + </script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/quota/test/mochitest/test_storage_manager_persist_allow.html b/dom/quota/test/mochitest/test_storage_manager_persist_allow.html new file mode 100644 index 0000000000..8477630f21 --- /dev/null +++ b/dom/quota/test/mochitest/test_storage_manager_persist_allow.html @@ -0,0 +1,21 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Allow Persist Prompt for StorageManager</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="helpers.js"></script> + <script type="text/javascript"> + loadScript("dom/quota/test/common/test_storage_manager_persist_allow.js"); + </script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/quota/test/mochitest/test_storage_manager_persist_deny.html b/dom/quota/test/mochitest/test_storage_manager_persist_deny.html new file mode 100644 index 0000000000..2b0fab4423 --- /dev/null +++ b/dom/quota/test/mochitest/test_storage_manager_persist_deny.html @@ -0,0 +1,21 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Deny Persist Prompt for StorageManager</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="helpers.js"></script> + <script type="text/javascript"> + loadScript("dom/quota/test/common/test_storage_manager_persist_deny.js"); + </script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/quota/test/mochitest/test_storage_manager_persisted.html b/dom/quota/test/mochitest/test_storage_manager_persisted.html new file mode 100644 index 0000000000..6e03f33e2d --- /dev/null +++ b/dom/quota/test/mochitest/test_storage_manager_persisted.html @@ -0,0 +1,24 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Storage Manager Persisted 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="helpers.js"></script> + <script type="text/javascript"> + const path = "dom/quota/test/common/test_storage_manager_persisted.js"; + + loadScript(path); + loadWorkerScript(path); + </script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/quota/test/modules/content/.eslintrc.js b/dom/quota/test/modules/content/.eslintrc.js new file mode 100644 index 0000000000..03499166b5 --- /dev/null +++ b/dom/quota/test/modules/content/.eslintrc.js @@ -0,0 +1,22 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +module.exports = { + overrides: [ + { + files: [ + "Assert.js", + "ModuleLoader.js", + "StorageUtils.js", + "WorkerDriver.js", + ], + parserOptions: { + sourceType: "module", + }, + }, + ], +}; diff --git a/dom/quota/test/modules/content/Assert.js b/dom/quota/test/modules/content/Assert.js new file mode 100644 index 0000000000..e2c8df19c8 --- /dev/null +++ b/dom/quota/test/modules/content/Assert.js @@ -0,0 +1,10 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Just a wrapper around SimpleTest related functions for now. +export const Assert = { + ok, + equal: is, +}; diff --git a/dom/quota/test/modules/content/ModuleLoader.js b/dom/quota/test/modules/content/ModuleLoader.js new file mode 100644 index 0000000000..a47061f751 --- /dev/null +++ b/dom/quota/test/modules/content/ModuleLoader.js @@ -0,0 +1,61 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +export function ModuleLoader(base, depth, proto) { + const modules = {}; + + const principal = SpecialPowers.wrap(document).nodePrincipal; + + const sharedGlobalSandbox = SpecialPowers.Cu.Sandbox(principal, { + invisibleToDebugger: true, + sandboxName: "FS Module Loader", + sandboxPrototype: proto, + wantComponents: false, + wantGlobalProperties: [], + wantXrays: false, + }); + + const require = async function(id) { + if (modules[id]) { + return modules[id].exported_symbols; + } + + const url = new URL(depth + id, base); + + const module = Object.create(null, { + exported_symbols: { + configurable: false, + enumerable: true, + value: Object.create(null), + writable: true, + }, + }); + + modules[id] = module; + + const properties = { + require_module: require, + exported_symbols: module.exported_symbols, + }; + + // Create a new object in this sandbox, that will be used as the scope + // object for this particular module. + const sandbox = sharedGlobalSandbox.Object(); + Object.assign(sandbox, properties); + + SpecialPowers.Services.scriptloader.loadSubScript(url.href, sandbox); + + return module.exported_symbols; + }; + + const returnObj = { + require: { + enumerable: true, + value: require, + }, + }; + + return Object.create(null, returnObj); +} diff --git a/dom/quota/test/modules/content/StorageUtils.js b/dom/quota/test/modules/content/StorageUtils.js new file mode 100644 index 0000000000..b29f7d9c26 --- /dev/null +++ b/dom/quota/test/modules/content/StorageUtils.js @@ -0,0 +1,48 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +class RequestError extends Error { + constructor(resultCode, resultName) { + super(`Request failed (code: ${resultCode}, name: ${resultName})`); + this.name = "RequestError"; + this.resultCode = resultCode; + this.resultName = resultName; + } +} + +export async function setStoragePrefs(optionalPrefsToSet) { + const prefsToSet = [ + // Not needed right now, but might be needed in future. + // ["dom.quotaManager.testing", true], + ]; + + if (SpecialPowers.Services.appinfo.OS === "WINNT") { + prefsToSet.push(["dom.quotaManager.useDOSDevicePathSyntax", true]); + } + + if (optionalPrefsToSet) { + prefsToSet.push(...optionalPrefsToSet); + } + + await SpecialPowers.pushPrefEnv({ set: prefsToSet }); +} + +export async function clearStoragesForOrigin(principal) { + const request = SpecialPowers.Services.qms.clearStoragesForPrincipal( + principal + ); + + 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/quota/test/modules/content/WorkerDriver.js b/dom/quota/test/modules/content/WorkerDriver.js new file mode 100644 index 0000000000..365d00f363 --- /dev/null +++ b/dom/quota/test/modules/content/WorkerDriver.js @@ -0,0 +1,53 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +export async function runTestInWorker(script, base, listener) { + return new Promise(function(resolve) { + const globalHeadUrl = new URL( + "/tests/dom/quota/test/modules/worker/head.js", + base + ); + + const worker = new Worker(globalHeadUrl.href); + + worker.onmessage = function(event) { + const data = event.data; + + switch (data.op) { + case "ok": + listener.onOk(data.value, data.message); + break; + + case "is": + listener.onIs(data.a, data.b, data.message); + break; + + case "info": + listener.onInfo(data.message); + break; + + case "finish": + resolve(); + break; + + case "failure": + listener.onOk(false, "Worker had a failure: " + data.message); + resolve(); + break; + } + }; + + worker.onerror = function(event) { + listener.onOk(false, "Worker had an error: " + event.data); + resolve(); + }; + + const scriptUrl = new URL(script, base); + + const localHeadUrl = new URL("head.js", scriptUrl); + + worker.postMessage([localHeadUrl.href, scriptUrl.href]); + }); +} diff --git a/dom/quota/test/modules/content/worker/.eslintrc.js b/dom/quota/test/modules/content/worker/.eslintrc.js new file mode 100644 index 0000000000..470c23b2fa --- /dev/null +++ b/dom/quota/test/modules/content/worker/.eslintrc.js @@ -0,0 +1,21 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +module.exports = { + env: { + worker: true, + }, + + overrides: [ + { + files: ["Assert.js", "ModuleLoader.js"], + parserOptions: { + sourceType: "script", + }, + }, + ], +}; diff --git a/dom/quota/test/modules/content/worker/Assert.js b/dom/quota/test/modules/content/worker/Assert.js new file mode 100644 index 0000000000..7c7e2683ea --- /dev/null +++ b/dom/quota/test/modules/content/worker/Assert.js @@ -0,0 +1,22 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const Assert = { + ok(value, message) { + postMessage({ + op: "ok", + value: !!value, + message, + }); + }, + equal(a, b, message) { + postMessage({ + op: "is", + a, + b, + message, + }); + }, +}; diff --git a/dom/quota/test/modules/content/worker/ModuleLoader.js b/dom/quota/test/modules/content/worker/ModuleLoader.js new file mode 100644 index 0000000000..23ee7c06df --- /dev/null +++ b/dom/quota/test/modules/content/worker/ModuleLoader.js @@ -0,0 +1,52 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function ModuleLoader(base, depth) { + const modules = {}; + + const require = async function(id) { + if (modules[id]) { + return modules[id].exported_symbols; + } + + const url = new URL(depth + id, base); + + const module = Object.create(null, { + exported_symbols: { + configurable: false, + enumerable: true, + value: Object.create(null), + writable: true, + }, + }); + + modules[id] = module; + + const xhr = new XMLHttpRequest(); + xhr.open("GET", url.href, false); + xhr.responseType = "text"; + xhr.send(); + + let source = xhr.responseText; + + let code = new Function( + "require_module", + "exported_symbols", + `eval(arguments[2] + "\\n//# sourceURL=" + arguments[3] + "\\n")` + ); + code(require, module.exported_symbols, source, url.href); + + return module.exported_symbols; + }; + + const returnObj = { + require: { + enumerable: true, + value: require, + }, + }; + + return Object.create(null, returnObj); +} diff --git a/dom/quota/test/modules/content/worker/head.js b/dom/quota/test/modules/content/worker/head.js new file mode 100644 index 0000000000..58d4591e47 --- /dev/null +++ b/dom/quota/test/modules/content/worker/head.js @@ -0,0 +1,55 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const Cr = { + NS_ERROR_NOT_IMPLEMENTED: 2147500033, +}; + +function add_task(func) { + if (!add_task.tasks) { + add_task.tasks = []; + add_task.index = 0; + } + + add_task.tasks.push(func); +} + +addEventListener("message", async function onMessage(event) { + function info(message) { + postMessage({ op: "info", message }); + } + + function executeSoon(callback) { + const channel = new MessageChannel(); + channel.port1.postMessage(""); + channel.port2.onmessage = function() { + callback(); + }; + } + + function runNextTest() { + if (add_task.index < add_task.tasks.length) { + const task = add_task.tasks[add_task.index++]; + info("add_task | Entering test " + task.name); + task() + .then(function() { + executeSoon(runNextTest); + info("add_task | Leaving test " + task.name); + }) + .catch(function(ex) { + postMessage({ op: "failure", message: "" + ex }); + }); + } else { + postMessage({ op: "finish" }); + } + } + + removeEventListener("message", onMessage); + + const data = event.data; + importScripts(...data); + + executeSoon(runNextTest); +}); diff --git a/dom/quota/test/modules/system/ModuleLoader.sys.mjs b/dom/quota/test/modules/system/ModuleLoader.sys.mjs new file mode 100644 index 0000000000..7bddfd2760 --- /dev/null +++ b/dom/quota/test/modules/system/ModuleLoader.sys.mjs @@ -0,0 +1,63 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +export function ModuleLoader(base, depth, proto) { + const modules = {}; + + const principal = Cc["@mozilla.org/systemprincipal;1"].createInstance( + Ci.nsIPrincipal + ); + + const sharedGlobalsandbox = Cu.Sandbox(principal, { + invisibleToDebugger: true, + sandboxName: "FS Module Loader", + sandboxPrototype: proto, + wantComponents: false, + wantGlobalProperties: [], + wantXrays: false, + }); + + const require = async function(id) { + if (modules[id]) { + return modules[id].exported_symbols; + } + + const url = new URL(depth + id, base); + + const module = Object.create(null, { + exported_symbols: { + configurable: false, + enumerable: true, + value: Object.create(null), + writable: true, + }, + }); + + modules[id] = module; + + const properties = { + require_module: require, + exported_symbols: module.exported_symbols, + }; + + // Create a new object in this sandbox, that will be used as the scope + // object for this particular module. + const sandbox = sharedGlobalsandbox.Object(); + Object.assign(sandbox, properties); + + Services.scriptloader.loadSubScript(url.href, sandbox); + + return module.exported_symbols; + }; + + const returnObj = { + require: { + enumerable: true, + value: require, + }, + }; + + return Object.create(null, returnObj); +} diff --git a/dom/quota/test/modules/system/StorageUtils.sys.mjs b/dom/quota/test/modules/system/StorageUtils.sys.mjs new file mode 100644 index 0000000000..bda40a4747 --- /dev/null +++ b/dom/quota/test/modules/system/StorageUtils.sys.mjs @@ -0,0 +1,65 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +class RequestError extends Error { + constructor(resultCode, resultName) { + super(`Request failed (code: ${resultCode}, name: ${resultName})`); + this.name = "RequestError"; + this.resultCode = resultCode; + this.resultName = resultName; + } +} + +export function setStoragePrefs(optionalPrefsToSet) { + const prefsToSet = [["dom.quotaManager.testing", true]]; + + if (Services.appinfo.OS === "WINNT") { + prefsToSet.push(["dom.quotaManager.useDOSDevicePathSyntax", true]); + } + + if (optionalPrefsToSet) { + prefsToSet.push(...optionalPrefsToSet); + } + + for (const pref of prefsToSet) { + Services.prefs.setBoolPref(pref[0], pref[1]); + } +} + +export function clearStoragePrefs(optionalPrefsToClear) { + const prefsToClear = [ + "dom.quotaManager.testing", + "dom.simpleDB.enabled", + "dom.storageManager.enabled", + ]; + + if (Services.appinfo.OS === "WINNT") { + prefsToClear.push("dom.quotaManager.useDOSDevicePathSyntax"); + } + + if (optionalPrefsToClear) { + prefsToClear.push(...optionalPrefsToClear); + } + + for (const pref of prefsToClear) { + Services.prefs.clearUserPref(pref); + } +} + +export async function clearStoragesForOrigin(principal) { + const request = Services.qms.clearStoragesForPrincipal(principal); + + 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; +} diff --git a/dom/quota/test/modules/system/WorkerDriver.sys.mjs b/dom/quota/test/modules/system/WorkerDriver.sys.mjs new file mode 100644 index 0000000000..c03a94cf56 --- /dev/null +++ b/dom/quota/test/modules/system/WorkerDriver.sys.mjs @@ -0,0 +1,52 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +export async function runTestInWorker(script, base, listener) { + return new Promise(function(resolve) { + const globalHeadUrl = new URL( + "resource://testing-common/dom/quota/test/modules/worker/head.js" + ); + + const worker = new Worker(globalHeadUrl.href); + + worker.onmessage = function(event) { + const data = event.data; + + switch (data.op) { + case "ok": + listener.onOk(data.value, data.message); + break; + + case "is": + listener.onIs(data.a, data.b, data.message); + break; + + case "info": + listener.onInfo(data.message); + break; + + case "finish": + resolve(); + break; + + case "failure": + listener.onOk(false, "Worker had a failure: " + data.message); + resolve(); + break; + } + }; + + worker.onerror = function(event) { + listener.onOk(false, "Worker had an error: " + event.data); + resolve(); + }; + + const scriptUrl = new URL(script, base); + + const localHeadUrl = new URL("head.js", scriptUrl); + + worker.postMessage([localHeadUrl.href, scriptUrl.href]); + }); +} diff --git a/dom/quota/test/modules/system/worker/.eslintrc.js b/dom/quota/test/modules/system/worker/.eslintrc.js new file mode 100644 index 0000000000..505e079c57 --- /dev/null +++ b/dom/quota/test/modules/system/worker/.eslintrc.js @@ -0,0 +1,21 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +module.exports = { + env: { + worker: true, + }, + + overrides: [ + { + files: ["head.js"], + env: { + worker: true, + }, + }, + ], +}; diff --git a/dom/quota/test/modules/system/worker/Assert.js b/dom/quota/test/modules/system/worker/Assert.js new file mode 100644 index 0000000000..7c7e2683ea --- /dev/null +++ b/dom/quota/test/modules/system/worker/Assert.js @@ -0,0 +1,22 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const Assert = { + ok(value, message) { + postMessage({ + op: "ok", + value: !!value, + message, + }); + }, + equal(a, b, message) { + postMessage({ + op: "is", + a, + b, + message, + }); + }, +}; diff --git a/dom/quota/test/modules/system/worker/ModuleLoader.js b/dom/quota/test/modules/system/worker/ModuleLoader.js new file mode 100644 index 0000000000..6de1fbc299 --- /dev/null +++ b/dom/quota/test/modules/system/worker/ModuleLoader.js @@ -0,0 +1,52 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function ModuleLoader(base, depth, proto) { + const modules = {}; + + const require = async function(id) { + if (modules[id]) { + return modules[id].exported_symbols; + } + + const url = new URL(depth + id, base); + + const module = Object.create(null, { + exported_symbols: { + configurable: false, + enumerable: true, + value: Object.create(null), + writable: true, + }, + }); + + modules[id] = module; + + const xhr = new XMLHttpRequest(); + xhr.open("GET", url.href, false); + xhr.responseType = "text"; + xhr.send(); + + let source = xhr.responseText; + + let code = new Function( + "require_module", + "exported_symbols", + `eval(arguments[2] + "\\n//# sourceURL=" + arguments[3] + "\\n")` + ); + code(require, module.exported_symbols, source, url.href); + + return module.exported_symbols; + }; + + const returnObj = { + require: { + enumerable: true, + value: require, + }, + }; + + return Object.create(null, returnObj); +} diff --git a/dom/quota/test/modules/system/worker/head.js b/dom/quota/test/modules/system/worker/head.js new file mode 100644 index 0000000000..1308508918 --- /dev/null +++ b/dom/quota/test/modules/system/worker/head.js @@ -0,0 +1,56 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// eslint-disable-next-line mozilla/no-define-cc-etc +const Cr = { + NS_ERROR_NOT_IMPLEMENTED: 2147500033, +}; + +function add_task(func) { + if (!add_task.tasks) { + add_task.tasks = []; + add_task.index = 0; + } + + add_task.tasks.push(func); +} + +addEventListener("message", async function onMessage(event) { + function info(message) { + postMessage({ op: "info", message }); + } + + function executeSoon(callback) { + const channel = new MessageChannel(); + channel.port1.postMessage(""); + channel.port2.onmessage = function() { + callback(); + }; + } + + function runNextTest() { + if (add_task.index < add_task.tasks.length) { + const task = add_task.tasks[add_task.index++]; + info("add_task | Entering test " + task.name); + task() + .then(function() { + executeSoon(runNextTest); + info("add_task | Leaving test " + task.name); + }) + .catch(function(ex) { + postMessage({ op: "failure", message: "" + ex }); + }); + } else { + postMessage({ op: "finish" }); + } + } + + removeEventListener("message", onMessage); + + const data = event.data; + importScripts(...data); + + executeSoon(runNextTest); +}); diff --git a/dom/quota/test/moz.build b/dom/quota/test/moz.build new file mode 100644 index 0000000000..bab65f643e --- /dev/null +++ b/dom/quota/test/moz.build @@ -0,0 +1,76 @@ +# -*- 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/. + +TEST_DIRS += ["gtest"] + +BROWSER_CHROME_MANIFESTS += ["browser/browser.ini"] + +MOCHITEST_MANIFESTS += ["mochitest/mochitest.ini"] + +XPCSHELL_TESTS_MANIFESTS += [ + "xpcshell/caching/xpcshell.ini", + "xpcshell/telemetry/xpcshell.ini", + "xpcshell/upgrades/xpcshell.ini", + "xpcshell/xpcshell.ini", +] + +TEST_HARNESS_FILES.testing.mochitest.browser.dom.quota.test.common += [ + "common/browser.js", + "common/content.js", + "common/file.js", + "common/global.js", + "common/nestedtest.js", + "common/system.js", +] + +TEST_HARNESS_FILES.testing.mochitest.tests.dom.quota.test.common += [ + "common/content.js", + "common/file.js", + "common/global.js", + "common/mochitest.js", + "common/test_simpledb.js", + "common/test_storage_manager_persist_allow.js", + "common/test_storage_manager_persist_deny.js", + "common/test_storage_manager_persisted.js", +] + +TEST_HARNESS_FILES.testing.mochitest.tests.dom.quota.test.modules += [ + "modules/content/Assert.js", + "modules/content/ModuleLoader.js", + "modules/content/StorageUtils.js", + "modules/content/WorkerDriver.js", +] + +TEST_HARNESS_FILES.testing.mochitest.tests.dom.quota.test.modules.worker += [ + "modules/content/worker/Assert.js", + "modules/content/worker/head.js", + "modules/content/worker/ModuleLoader.js", +] + +TEST_HARNESS_FILES.xpcshell.dom.quota.test.common += [ + "common/file.js", + "common/global.js", + "common/system.js", + "common/test_simpledb.js", + "common/xpcshell.js", +] + +TEST_HARNESS_FILES.xpcshell.dom.quota.test.xpcshell.common += [ + "xpcshell/common/head.js", + "xpcshell/common/utils.js", +] + +TESTING_JS_MODULES.dom.quota.test.modules += [ + "modules/system/ModuleLoader.sys.mjs", + "modules/system/StorageUtils.sys.mjs", + "modules/system/WorkerDriver.sys.mjs", +] + +TESTING_JS_MODULES.dom.quota.test.modules.worker += [ + "modules/system/worker/Assert.js", + "modules/system/worker/head.js", + "modules/system/worker/ModuleLoader.js", +] diff --git a/dom/quota/test/xpcshell/basics_profile.zip b/dom/quota/test/xpcshell/basics_profile.zip Binary files differnew file mode 100644 index 0000000000..bbdb0f50cf --- /dev/null +++ b/dom/quota/test/xpcshell/basics_profile.zip diff --git a/dom/quota/test/xpcshell/caching/groupMismatch_profile.zip b/dom/quota/test/xpcshell/caching/groupMismatch_profile.zip Binary files differnew file mode 100644 index 0000000000..8124e589de --- /dev/null +++ b/dom/quota/test/xpcshell/caching/groupMismatch_profile.zip diff --git a/dom/quota/test/xpcshell/caching/head.js b/dom/quota/test/xpcshell/caching/head.js new file mode 100644 index 0000000000..5c36d82ca6 --- /dev/null +++ b/dom/quota/test/xpcshell/caching/head.js @@ -0,0 +1,14 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// The path to the top level directory. +const depth = "../../../../../"; + +loadScript("dom/quota/test/xpcshell/common/head.js"); + +function loadScript(path) { + let uri = Services.io.newFileURI(do_get_file(depth + path)); + Services.scriptloader.loadSubScript(uri.spec); +} diff --git a/dom/quota/test/xpcshell/caching/make_unsetLastAccessTime.js b/dom/quota/test/xpcshell/caching/make_unsetLastAccessTime.js new file mode 100644 index 0000000000..9be377e4f3 --- /dev/null +++ b/dom/quota/test/xpcshell/caching/make_unsetLastAccessTime.js @@ -0,0 +1,25 @@ +/* + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +async function testSteps() { + const originDirPath = "storage/default/https+++foo.example.com"; + + info("Initializing"); + + let request = init(); + await requestFinished(request); + + info("Creating an empty origin directory"); + + let originDir = getRelativeFile(originDirPath); + originDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + // The metadata file should be now restored. +} diff --git a/dom/quota/test/xpcshell/caching/removedOrigin_profile.zip b/dom/quota/test/xpcshell/caching/removedOrigin_profile.zip Binary files differnew file mode 100644 index 0000000000..a5ccc05aa9 --- /dev/null +++ b/dom/quota/test/xpcshell/caching/removedOrigin_profile.zip diff --git a/dom/quota/test/xpcshell/caching/test_groupMismatch.js b/dom/quota/test/xpcshell/caching/test_groupMismatch.js new file mode 100644 index 0000000000..3f8e843798 --- /dev/null +++ b/dom/quota/test/xpcshell/caching/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 the group loaded from the origin table + * gets updated (if necessary) before quota initialization for the given origin. + */ + +async function testSteps() { + const principal = getPrincipal("https://foo.bar.mozilla-iot.org"); + const originUsage = 100; + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains one initialized origin directory with simple database + // data, 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/quota/test/xpcshell/create_db.js + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Manually change the group and accessed values in the origin table in + // storage.sqlite by running this SQL statement: + // UPDATE origin SET group_ = 'mozilla-iot.org', accessed = 0 + // 2. Manually change the group in .metadata-v2 from "bar.mozilla-iot.org" to + // "mozilla-iot.org". + // 3. Remove the folder "storage/temporary". + // 4. Remove the file "storage/ls-archive.sqlite". + installPackage("groupMismatch_profile"); + + request = getOriginUsage(principal, /* fromMemory */ true); + await requestFinished(request); + + is(request.result.usage, originUsage, "Correct origin usage"); +} diff --git a/dom/quota/test/xpcshell/caching/test_removedOrigin.js b/dom/quota/test/xpcshell/caching/test_removedOrigin.js new file mode 100644 index 0000000000..8b5702ad9c --- /dev/null +++ b/dom/quota/test/xpcshell/caching/test_removedOrigin.js @@ -0,0 +1,61 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Verify that temporary storage initialization will notice a removed origin + * that the cache has data for and which indicates the origin was accessed + * during the last run. Currently, we expect LoadQuotaFromCache to fail because + * of this inconsistency and to fall back to full initialization. + */ + +async function testSteps() { + const principal = getPrincipal("http://example.com"); + const originUsage = 0; + + info("Setting pref"); + + // The packaged profile will have a different build ID and we would treat the + // cache as invalid if we didn't bypass this check. + Services.prefs.setBoolPref("dom.quotaManager.caching.checkBuildId", false); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains empty default storage, a script for origin + // initialization and the storage database: + // - storage/default + // - 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/quota/test/xpcshell/create_db.js + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Remove the folder "storage/default/http+++example.com". + // 2. Remove the folder "storage/temporary". + // 3. Remove the file "storage/ls-archive.sqlite". + installPackage("removedOrigin_profile"); + + info("Initializing"); + + request = init(); + await requestFinished(request); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Getting origin usage"); + + request = getOriginUsage(principal, /* fromMemory */ true); + await requestFinished(request); + + is(request.result.usage, originUsage, "Correct origin usage"); +} diff --git a/dom/quota/test/xpcshell/caching/test_unsetLastAccessTime.js b/dom/quota/test/xpcshell/caching/test_unsetLastAccessTime.js new file mode 100644 index 0000000000..5abe76eade --- /dev/null +++ b/dom/quota/test/xpcshell/caching/test_unsetLastAccessTime.js @@ -0,0 +1,46 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() { + const principal = getPrincipal("https://foo.example.com/"); + + info("Setting pref"); + + // The packaged profile will have a different build ID and we would treat the + // cache as invalid if we didn't bypass this check. + Services.prefs.setBoolPref("dom.quotaManager.caching.checkBuildId", false); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains one initialized origin directory and the storage + // database: + // - storage/default/https+++foo.example.com + // - storage.sqlite + // The file make_unsetLastAccessTime.js was run locally, specifically it was + // temporarily enabled in xpcshell.ini and then executed: + // mach test --interactive dom/quota/test/xpcshell/caching/make_unsetLastAccessTime.js + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Remove the folder "storage/temporary". + // 2. Remove the file "storage/ls-archive.sqlite". + installPackage("unsetLastAccessTime_profile"); + + info("Getting full origin metadata"); + + request = getFullOriginMetadata("default", principal); + await requestFinished(request); + + info("Verifying last access time"); + + ok( + BigInt(request.result.lastAccessTime) != INT64_MIN, + "Correct last access time" + ); +} diff --git a/dom/quota/test/xpcshell/caching/unsetLastAccessTime_profile.zip b/dom/quota/test/xpcshell/caching/unsetLastAccessTime_profile.zip Binary files differnew file mode 100644 index 0000000000..2b14ca7276 --- /dev/null +++ b/dom/quota/test/xpcshell/caching/unsetLastAccessTime_profile.zip diff --git a/dom/quota/test/xpcshell/caching/xpcshell.ini b/dom/quota/test/xpcshell/caching/xpcshell.ini new file mode 100644 index 0000000000..d08f15c0a8 --- /dev/null +++ b/dom/quota/test/xpcshell/caching/xpcshell.ini @@ -0,0 +1,17 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +[DEFAULT] +head = head.js +support-files = + groupMismatch_profile.zip + removedOrigin_profile.zip + +[make_unsetLastAccessTime.js] +skip-if = true # Only used for recreating unsetLastAccessTime_profile.zip +[test_groupMismatch.js] +[test_removedOrigin.js] +[test_unsetLastAccessTime.js] +support-files = + unsetLastAccessTime_profile.zip diff --git a/dom/quota/test/xpcshell/clearStoragesForPrincipal_profile.zip b/dom/quota/test/xpcshell/clearStoragesForPrincipal_profile.zip Binary files differnew file mode 100644 index 0000000000..7d7985ddd0 --- /dev/null +++ b/dom/quota/test/xpcshell/clearStoragesForPrincipal_profile.zip diff --git a/dom/quota/test/xpcshell/common/head.js b/dom/quota/test/xpcshell/common/head.js new file mode 100644 index 0000000000..3471b56de7 --- /dev/null +++ b/dom/quota/test/xpcshell/common/head.js @@ -0,0 +1,629 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const NS_OK = Cr.NS_OK; +const NS_ERROR_FAILURE = Cr.NS_ERROR_FAILURE; +const NS_ERROR_UNEXPECTED = Cr.NS_ERROR_UNEXPECTED; +const NS_ERROR_FILE_NO_DEVICE_SPACE = Cr.NS_ERROR_FILE_NO_DEVICE_SPACE; + +const loggingEnabled = false; + +var testGenerator; + +loadScript("dom/quota/test/common/xpcshell.js"); + +function log(msg) { + if (loggingEnabled) { + info(msg); + } +} + +function is(a, b, msg) { + Assert.equal(a, b, msg); +} + +function ok(cond, msg) { + Assert.ok(!!cond, msg); +} + +function todo(cond, msg) { + todo_check_true(cond); +} + +function run_test() { + runTest(); +} + +if (!this.runTest) { + this.runTest = function() { + do_get_profile(); + + enableStorageTesting(); + enableTesting(); + + Cu.importGlobalProperties(["indexedDB", "File", "Blob", "FileReader"]); + + // In order to support converting tests to using async functions from using + // generator functions, we detect async functions by checking the name of + // function's constructor. + Assert.ok( + typeof testSteps === "function", + "There should be a testSteps function" + ); + if (testSteps.constructor.name === "AsyncFunction") { + // Do run our existing cleanup function that would normally be called by + // the generator's call to finishTest(). + registerCleanupFunction(function() { + resetStorageTesting(); + resetTesting(); + }); + + add_task(testSteps); + + // Since we defined run_test, we must invoke run_next_test() to start the + // async test. + run_next_test(); + } else { + Assert.ok( + testSteps.constructor.name === "GeneratorFunction", + "Unsupported function type" + ); + + do_test_pending(); + + testGenerator = testSteps(); + testGenerator.next(); + } + }; +} + +function finishTest() { + resetStorageTesting(); + resetTesting(); + + executeSoon(function() { + do_test_finished(); + }); +} + +function grabArgAndContinueHandler(arg) { + testGenerator.next(arg); +} + +function continueToNextStep() { + executeSoon(function() { + testGenerator.next(); + }); +} + +function continueToNextStepSync() { + testGenerator.next(); +} + +function enableTesting() { + SpecialPowers.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + false + ); +} + +function resetTesting() { + SpecialPowers.clearUserPref( + "dom.storage.enable_unsupported_legacy_implementation" + ); +} + +function setGlobalLimit(globalLimit) { + SpecialPowers.setIntPref( + "dom.quotaManager.temporaryStorage.fixedLimit", + globalLimit + ); +} + +function resetGlobalLimit() { + SpecialPowers.clearUserPref("dom.quotaManager.temporaryStorage.fixedLimit"); +} + +function storageInitialized(callback) { + let request = SpecialPowers._getQuotaManager().storageInitialized(); + request.callback = callback; + + return request; +} + +function temporaryStorageInitialized(callback) { + let request = SpecialPowers._getQuotaManager().temporaryStorageInitialized(); + request.callback = callback; + + return request; +} + +function init(callback) { + let request = SpecialPowers._getQuotaManager().init(); + request.callback = callback; + + return request; +} + +function initTemporaryStorage(callback) { + let request = SpecialPowers._getQuotaManager().initTemporaryStorage(); + request.callback = callback; + + return request; +} + +function initPersistentOrigin(principal, callback) { + let request = SpecialPowers._getQuotaManager().initializePersistentOrigin( + principal + ); + request.callback = callback; + + return request; +} + +function initTemporaryOrigin(persistence, principal, callback) { + let request = SpecialPowers._getQuotaManager().initializeTemporaryOrigin( + persistence, + principal + ); + request.callback = callback; + + return request; +} + +function getFullOriginMetadata(persistence, principal, callback) { + const request = SpecialPowers._getQuotaManager().getFullOriginMetadata( + persistence, + principal + ); + request.callback = callback; + + return request; +} + +function clearClient(principal, persistence, client, callback) { + let request = SpecialPowers._getQuotaManager().clearStoragesForPrincipal( + principal, + persistence, + client + ); + request.callback = callback; + + return request; +} + +function clearOrigin(principal, persistence, callback) { + let request = SpecialPowers._getQuotaManager().clearStoragesForPrincipal( + principal, + persistence + ); + request.callback = callback; + + return request; +} + +function resetClient(principal, client) { + let request = Services.qms.resetStoragesForPrincipal( + principal, + "default", + client + ); + + return request; +} + +function persist(principal, callback) { + let request = SpecialPowers._getQuotaManager().persist(principal); + request.callback = callback; + + return request; +} + +function persisted(principal, callback) { + let request = SpecialPowers._getQuotaManager().persisted(principal); + request.callback = callback; + + return request; +} + +function estimateOrigin(principal, callback) { + let request = SpecialPowers._getQuotaManager().estimate(principal); + request.callback = callback; + + return request; +} + +function listOrigins(callback) { + let request = SpecialPowers._getQuotaManager().listOrigins(callback); + request.callback = callback; + + return request; +} + +function getPersistedFromMetadata(readBuffer) { + const persistedPosition = 8; // Persisted state is stored in the 9th byte + let view = + readBuffer instanceof Uint8Array ? readBuffer : new Uint8Array(readBuffer); + + return !!view[persistedPosition]; +} + +function grabResultAndContinueHandler(request) { + testGenerator.next(request.result); +} + +function grabUsageAndContinueHandler(request) { + testGenerator.next(request.result.usage); +} + +function getUsage(usageHandler, getAll) { + let request = SpecialPowers._getQuotaManager().getUsage(usageHandler, getAll); + + return request; +} + +function getOriginUsage(principal, fromMemory = false) { + let request = Services.qms.getUsageForPrincipal( + principal, + function() {}, + fromMemory + ); + + return request; +} + +function getCurrentUsage(usageHandler) { + let principal = Cc["@mozilla.org/systemprincipal;1"].createInstance( + Ci.nsIPrincipal + ); + let request = SpecialPowers._getQuotaManager().getUsageForPrincipal( + principal, + usageHandler + ); + + return request; +} + +function getPrincipal(url, attr = {}) { + let uri = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(url); + let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + return ssm.createContentPrincipal(uri, attr); +} + +var SpecialPowers = { + getBoolPref(prefName) { + return this._getPrefs().getBoolPref(prefName); + }, + + setBoolPref(prefName, value) { + this._getPrefs().setBoolPref(prefName, value); + }, + + setIntPref(prefName, value) { + this._getPrefs().setIntPref(prefName, value); + }, + + clearUserPref(prefName) { + this._getPrefs().clearUserPref(prefName); + }, + + _getPrefs() { + let prefService = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefService + ); + return prefService.getBranch(null); + }, + + _getQuotaManager() { + return Cc["@mozilla.org/dom/quota-manager-service;1"].getService( + Ci.nsIQuotaManagerService + ); + }, +}; + +function installPackages(packageRelativePaths) { + if (packageRelativePaths.length != 2) { + throw new Error("Unsupported number of package relative paths"); + } + + for (const packageRelativePath of packageRelativePaths) { + installPackage(packageRelativePath); + } +} + +// Take current storage structure on disk and compare it with the expected +// structure. The expected structure is defined in JSON and consists of a per +// test package definition and a shared package definition. The shared package +// definition should contain unknown stuff which needs to be properly handled +// in all situations. +function verifyStorage(packageDefinitionRelativePaths, key) { + if (packageDefinitionRelativePaths.length != 2) { + throw new Error("Unsupported number of package definition relative paths"); + } + + function verifyEntries(entries, name, indent = "") { + log(`${indent}Verifying ${name} entries`); + + indent += " "; + + for (const entry of entries) { + const maybeName = entry.name; + + log(`${indent}Verifying entry ${maybeName}`); + + let hasName = false; + let hasDir = false; + let hasEntries = false; + + for (const property in entry) { + switch (property) { + case "note": + case "todo": + break; + + case "name": + hasName = true; + break; + + case "dir": + hasDir = true; + break; + + case "entries": + hasEntries = true; + break; + + default: + throw new Error(`Unknown property ${property}`); + } + } + + if (!hasName) { + throw new Error("An entry must have the name property"); + } + + if (!hasDir) { + throw new Error("An entry must have the dir property"); + } + + if (hasEntries && !entry.dir) { + throw new Error("An entry can't have entries if it's not a directory"); + } + + if (hasEntries) { + verifyEntries(entry.entries, entry.name, indent); + } + } + } + + function getCurrentEntries() { + log("Getting current entries"); + + function getEntryForFile(file) { + let entry = { + name: file.leafName, + dir: file.isDirectory(), + }; + + if (file.isDirectory()) { + const enumerator = file.directoryEntries; + let nextFile; + while ((nextFile = enumerator.nextFile)) { + if (!entry.entries) { + entry.entries = []; + } + entry.entries.push(getEntryForFile(nextFile)); + } + } + + return entry; + } + + let entries = []; + + let file = getRelativeFile("indexedDB"); + if (file.exists()) { + entries.push(getEntryForFile(file)); + } + + file = getRelativeFile("storage"); + if (file.exists()) { + entries.push(getEntryForFile(file)); + } + + file = getRelativeFile("storage.sqlite"); + if (file.exists()) { + entries.push(getEntryForFile(file)); + } + + verifyEntries(entries, "current"); + + return entries; + } + + function getEntriesFromPackageDefinition( + packageDefinitionRelativePath, + lookupKey + ) { + log(`Getting ${lookupKey} entries from ${packageDefinitionRelativePath}`); + + const currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + const file = getRelativeFile( + packageDefinitionRelativePath + ".json", + currentDir + ); + + const fileInputStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileInputStream.init(file, -1, -1, 0); + + const scriptableInputStream = Cc[ + "@mozilla.org/scriptableinputstream;1" + ].createInstance(Ci.nsIScriptableInputStream); + scriptableInputStream.init(fileInputStream); + + const data = scriptableInputStream.readBytes( + scriptableInputStream.available() + ); + + const obj = JSON.parse(data); + + const result = obj.find(({ key: elementKey }) => elementKey == lookupKey); + + if (!result) { + throw new Error("The file doesn't contain an element for given key"); + } + + if (!result.entries) { + throw new Error("The element doesn't have the entries property"); + } + + verifyEntries(result.entries, lookupKey); + + return result.entries; + } + + function addSharedEntries(expectedEntries, sharedEntries, name, indent = "") { + log(`${indent}Checking common ${name} entries`); + + indent += " "; + + for (const sharedEntry of sharedEntries) { + const expectedEntry = expectedEntries.find( + ({ name: elementName }) => elementName == sharedEntry.name + ); + + if (expectedEntry) { + log(`${indent}Checking common entry ${sharedEntry.name}`); + + if (!expectedEntry.dir || !sharedEntry.dir) { + throw new Error("A common entry must be a directory"); + } + + if (!expectedEntry.entries && !sharedEntry.entries) { + throw new Error("A common entry must not be a leaf"); + } + + if (sharedEntry.entries) { + if (!expectedEntry.entries) { + expectedEntry.entries = []; + } + + addSharedEntries( + expectedEntry.entries, + sharedEntry.entries, + sharedEntry.name, + indent + ); + } + } else { + log(`${indent}Adding entry ${sharedEntry.name}`); + expectedEntries.push(sharedEntry); + } + } + } + + function compareEntries(currentEntries, expectedEntries, name, indent = "") { + log(`${indent}Comparing ${name} entries`); + + indent += " "; + + if (currentEntries.length != expectedEntries.length) { + throw new Error("Entries must have the same length"); + } + + for (const currentEntry of currentEntries) { + log(`${indent}Comparing entry ${currentEntry.name}`); + + const expectedEntry = expectedEntries.find( + ({ name: elementName }) => elementName == currentEntry.name + ); + + if (!expectedEntry) { + throw new Error("Cannot find a matching entry"); + } + + if (expectedEntry.dir != currentEntry.dir) { + throw new Error("The dir property doesn't match"); + } + + if ( + (expectedEntry.entries && !currentEntry.entries) || + (!expectedEntry.entries && currentEntry.entries) + ) { + throw new Error("The entries property doesn't match"); + } + + if (expectedEntry.entries) { + compareEntries( + currentEntry.entries, + expectedEntry.entries, + currentEntry.name, + indent + ); + } + } + } + + const currentEntries = getCurrentEntries(); + + log("Stringified current entries: " + JSON.stringify(currentEntries)); + + const expectedEntries = getEntriesFromPackageDefinition( + packageDefinitionRelativePaths[0], + key + ); + const sharedEntries = getEntriesFromPackageDefinition( + packageDefinitionRelativePaths[1], + key + ); + + addSharedEntries(expectedEntries, sharedEntries, key); + + log("Stringified expected entries: " + JSON.stringify(expectedEntries)); + + compareEntries(currentEntries, expectedEntries, key); +} + +async function verifyInitializationStatus( + expectStorageIsInitialized, + expectTemporaryStorageIsInitialized +) { + if (!expectStorageIsInitialized && expectTemporaryStorageIsInitialized) { + throw new Error("Invalid expectation"); + } + + let request = storageInitialized(); + await requestFinished(request); + + const storageIsInitialized = request.result; + + request = temporaryStorageInitialized(); + await requestFinished(request); + + const temporaryStorageIsInitialized = request.result; + + ok( + !(!storageIsInitialized && temporaryStorageIsInitialized), + "Initialization status is consistent" + ); + + if (expectStorageIsInitialized) { + ok(storageIsInitialized, "Storage is initialized"); + } else { + ok(!storageIsInitialized, "Storage is not initialized"); + } + + if (expectTemporaryStorageIsInitialized) { + ok(temporaryStorageIsInitialized, "Temporary storage is initialized"); + } else { + ok(!temporaryStorageIsInitialized, "Temporary storage is not initialized"); + } +} diff --git a/dom/quota/test/xpcshell/common/utils.js b/dom/quota/test/xpcshell/common/utils.js new file mode 100644 index 0000000000..ee21c90cf2 --- /dev/null +++ b/dom/quota/test/xpcshell/common/utils.js @@ -0,0 +1,47 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadScript("dom/quota/test/common/file.js"); + +function getOriginDir(persistence, origin) { + return getRelativeFile(`storage/${persistence}/${origin}`); +} + +function getMetadataFile(persistence, origin) { + const metadataFile = getOriginDir(persistence, origin); + metadataFile.append(".metadata-v2"); + return metadataFile; +} + +function populateRepository(persistence) { + const originDir = getOriginDir(persistence, "https+++good-example.com"); + originDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); +} + +function makeRepositoryUnusable(persistence) { + // For the purpose of testing, we make a repository unusable by creating an + // origin directory with the metadata file created as a directory (not a + // file). + const metadataFile = getMetadataFile(persistence, "https+++bad-example.com"); + metadataFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); +} + +async function fillOrigin(principal, size) { + let database = getSimpleDatabase(principal); + + let request = database.open("data"); + await requestFinished(request); + + try { + request = database.write(getBuffer(size)); + await requestFinished(request); + ok(true, "Should not have thrown"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + + request = database.close(); + await requestFinished(request); +} diff --git a/dom/quota/test/xpcshell/createLocalStorage_profile.zip b/dom/quota/test/xpcshell/createLocalStorage_profile.zip Binary files differnew file mode 100644 index 0000000000..d5958dbd59 --- /dev/null +++ b/dom/quota/test/xpcshell/createLocalStorage_profile.zip diff --git a/dom/quota/test/xpcshell/defaultStorageDirectory_shared.json b/dom/quota/test/xpcshell/defaultStorageDirectory_shared.json new file mode 100644 index 0000000000..a59cebdc87 --- /dev/null +++ b/dom/quota/test/xpcshell/defaultStorageDirectory_shared.json @@ -0,0 +1,109 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "permanent", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + }, + { + "name": "default", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + }, + { + "name": "temporary", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + } + ] + } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "permanent", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + }, + { + "name": "default", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + }, + { + "name": "temporary", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + } + ] + } + ] + }, + { + "key": "afterInitTemporaryStorage", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + {"name":"ls-archive.sqlite","dir":false}, + { + "name": "permanent", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + }, + { + "name": "default", + "dir": true, + "todo": "Add entry invalid+++example.com once bug 1594075 is fixed", + "entries": [ + { "name": "foo.bar", "dir": false } + ] + }, + { + "name": "temporary", + "dir": true, + "todo": "Add entry invalid+++example.com once bug 1594075 is fixed", + "entries": [ + { "name": "foo.bar", "dir": false } + ] + } + ] + } + ] + } +] diff --git a/dom/quota/test/xpcshell/defaultStorageDirectory_shared.zip b/dom/quota/test/xpcshell/defaultStorageDirectory_shared.zip Binary files differnew file mode 100644 index 0000000000..61e5b60a87 --- /dev/null +++ b/dom/quota/test/xpcshell/defaultStorageDirectory_shared.zip diff --git a/dom/quota/test/xpcshell/getUsage_profile.zip b/dom/quota/test/xpcshell/getUsage_profile.zip Binary files differnew file mode 100644 index 0000000000..5144112bde --- /dev/null +++ b/dom/quota/test/xpcshell/getUsage_profile.zip diff --git a/dom/quota/test/xpcshell/groupMismatch_profile.zip b/dom/quota/test/xpcshell/groupMismatch_profile.zip Binary files differnew file mode 100644 index 0000000000..182b013de0 --- /dev/null +++ b/dom/quota/test/xpcshell/groupMismatch_profile.zip diff --git a/dom/quota/test/xpcshell/head.js b/dom/quota/test/xpcshell/head.js new file mode 100644 index 0000000000..bf9ba22ce3 --- /dev/null +++ b/dom/quota/test/xpcshell/head.js @@ -0,0 +1,14 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// The path to the top level directory. +const depth = "../../../../"; + +loadScript("dom/quota/test/xpcshell/common/head.js"); + +function loadScript(path) { + let uri = Services.io.newFileURI(do_get_file(depth + path)); + Services.scriptloader.loadSubScript(uri.spec); +} diff --git a/dom/quota/test/xpcshell/indexedDBDirectory_shared.json b/dom/quota/test/xpcshell/indexedDBDirectory_shared.json new file mode 100644 index 0000000000..6e3d63bafc --- /dev/null +++ b/dom/quota/test/xpcshell/indexedDBDirectory_shared.json @@ -0,0 +1,35 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "indexedDB", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + } + ] + } + ] + } +] diff --git a/dom/quota/test/xpcshell/indexedDBDirectory_shared.zip b/dom/quota/test/xpcshell/indexedDBDirectory_shared.zip Binary files differnew file mode 100644 index 0000000000..6b959e7525 --- /dev/null +++ b/dom/quota/test/xpcshell/indexedDBDirectory_shared.zip diff --git a/dom/quota/test/xpcshell/make_unknownFiles.js b/dom/quota/test/xpcshell/make_unknownFiles.js new file mode 100644 index 0000000000..dd8213ade4 --- /dev/null +++ b/dom/quota/test/xpcshell/make_unknownFiles.js @@ -0,0 +1,172 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadScript("dom/quota/test/common/file.js"); + +async function testSteps() { + const principal = getPrincipal("http://example.com"); + + const repoRelativePath = "storage/default"; + const originRelativePath = `${repoRelativePath}/http+++example.com`; + + let unknownFileCounter = 1; + let unknownDirCounter = 1; + + function createUnknownFileIn(dirRelativePath, recursive) { + const dir = getRelativeFile(dirRelativePath); + + let file = dir.clone(); + file.append("foo" + unknownFileCounter + ".bar"); + + const ostream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + + ostream.init(file, -1, parseInt("0644", 8), 0); + + ostream.write("x".repeat(unknownFileCounter), unknownFileCounter); + + ostream.close(); + + unknownFileCounter++; + + if (recursive) { + const entries = dir.directoryEntries; + while ((file = entries.nextFile)) { + if (file.isDirectory()) { + createUnknownFileIn(dirRelativePath + "/" + file.leafName); + } + } + } + } + + function createUnknownDirectoryIn(dirRelativePath) { + createUnknownFileIn(dirRelativePath + "/foo" + unknownDirCounter++); + } + + // storage.sqlite and storage/ls-archive.sqlite + { + const request = init(); + await requestFinished(request); + } + + // Unknown file in the repository + { + createUnknownFileIn(repoRelativePath); + } + + // Unknown file and unknown directory in the origin directory + { + let request = init(); + await requestFinished(request); + + request = initTemporaryStorage(); + await requestFinished(request); + + request = initTemporaryOrigin("default", principal); + await requestFinished(request); + + ok(request.result === true, "The origin directory was created"); + + createUnknownFileIn(originRelativePath); + createUnknownDirectoryIn(originRelativePath); + } + + // Unknown files in idb client directory and its subdirectories and unknown + // directory in .files directory + { + const request = indexedDB.openForPrincipal(principal, "myIndexedDB"); + await openDBRequestUpgradeNeeded(request); + + const database = request.result; + + const objectStore = database.createObjectStore("Blobs", {}); + + objectStore.add(getNullBlob(200), 42); + + await openDBRequestSucceeded(request); + + database.close(); + + createUnknownFileIn(`${originRelativePath}/idb`); + createUnknownFileIn( + `${originRelativePath}/idb/2320029346mByDIdnedxe.files` + ); + createUnknownDirectoryIn( + `${originRelativePath}/idb/2320029346mByDIdnedxe.files` + ); + createUnknownFileIn( + `${originRelativePath}/idb/2320029346mByDIdnedxe.files/journals` + ); + } + + // Unknown files in cache client directory and its subdirectories + { + async function sandboxScript() { + const cache = await caches.open("myCache"); + const request = new Request("http://example.com/index.html"); + const response = new Response("hello world"); + await cache.put(request, response); + } + + const sandbox = new Cu.Sandbox(principal, { + wantGlobalProperties: ["caches", "fetch"], + }); + + const promise = new Promise(function(resolve, reject) { + sandbox.resolve = resolve; + sandbox.reject = reject; + }); + + Cu.evalInSandbox( + sandboxScript.toSource() + " sandboxScript().then(resolve, reject);", + sandbox + ); + await promise; + + createUnknownFileIn(`${originRelativePath}/cache`); + createUnknownFileIn( + `${originRelativePath}/cache/morgue`, + /* recursive */ true + ); + } + + // Unknown file and unknown directory in sdb client directory + { + const database = getSimpleDatabase(principal); + + let request = database.open("mySimpleDB"); + await requestFinished(request); + + request = database.write(getBuffer(100)); + await requestFinished(request); + + request = database.close(); + await requestFinished(request); + + createUnknownFileIn(`${originRelativePath}/sdb`); + createUnknownDirectoryIn(`${originRelativePath}/sdb`); + } + + // Unknown file and unknown directory in ls client directory + { + Services.prefs.setBoolPref("dom.storage.testing", true); + Services.prefs.setBoolPref("dom.storage.client_validation", false); + + const storage = Services.domStorageManager.createStorage( + null, + principal, + principal, + "" + ); + + storage.setItem("foo", "bar"); + + storage.close(); + + createUnknownFileIn(`${originRelativePath}/ls`); + createUnknownDirectoryIn(`${originRelativePath}/ls`); + } +} diff --git a/dom/quota/test/xpcshell/make_unsetLastAccessTime.js b/dom/quota/test/xpcshell/make_unsetLastAccessTime.js new file mode 100644 index 0000000000..9be377e4f3 --- /dev/null +++ b/dom/quota/test/xpcshell/make_unsetLastAccessTime.js @@ -0,0 +1,25 @@ +/* + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +async function testSteps() { + const originDirPath = "storage/default/https+++foo.example.com"; + + info("Initializing"); + + let request = init(); + await requestFinished(request); + + info("Creating an empty origin directory"); + + let originDir = getRelativeFile(originDirPath); + originDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + // The metadata file should be now restored. +} diff --git a/dom/quota/test/xpcshell/originMismatch_profile.json b/dom/quota/test/xpcshell/originMismatch_profile.json new file mode 100644 index 0000000000..5e1cb50631 --- /dev/null +++ b/dom/quota/test/xpcshell/originMismatch_profile.json @@ -0,0 +1,74 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "http+++www.example.com", + "dir": true, + "entries": [ + { "name": ".metadata-v2", "dir": false }, + { "name": "cache", + "dir": true, + "entries": [{ "name": ".padding", "dir": false }] + } + ] + }, + { "name": "http+++www.example.com.", "dir": true }, + { + "name": "http+++www.example.org", + "dir": true, + "entries": [ + { "name": ".metadata-v2", "dir": false }, + { "name": "cache", + "dir": true, + "entries": [{ "name": ".padding", "dir": false }] + } + ] + } + ] + } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + }, + { + "key": "afterInitTemporaryStorage", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { "name": "http+++www.example.com.", + "dir": true, + "entries": [{ "name": ".metadata-v2", "dir": false }] + }, + { + "name": "http+++www.example.org.", + "dir": true, + "entries": [ + { "name": ".metadata-v2", "dir": false }, + { "name": "cache", "dir": true } + ] + } + ] + } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/originMismatch_profile.zip b/dom/quota/test/xpcshell/originMismatch_profile.zip Binary files differnew file mode 100644 index 0000000000..dd5795736f --- /dev/null +++ b/dom/quota/test/xpcshell/originMismatch_profile.zip diff --git a/dom/quota/test/xpcshell/persistentStorageDirectory_shared.json b/dom/quota/test/xpcshell/persistentStorageDirectory_shared.json new file mode 100644 index 0000000000..8d36293bbf --- /dev/null +++ b/dom/quota/test/xpcshell/persistentStorageDirectory_shared.json @@ -0,0 +1,57 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "persistent", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + }, + { + "name": "temporary", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + } + ] + } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + }, + { + "name": "temporary", + "dir": true, + "entries": [ + { "name": "invalid+++example.com", "dir": true }, + { "name": "foo.bar", "dir": false } + ] + } + ] + } + ] + } +] diff --git a/dom/quota/test/xpcshell/persistentStorageDirectory_shared.zip b/dom/quota/test/xpcshell/persistentStorageDirectory_shared.zip Binary files differnew file mode 100644 index 0000000000..b80bfe904b --- /dev/null +++ b/dom/quota/test/xpcshell/persistentStorageDirectory_shared.zip diff --git a/dom/quota/test/xpcshell/removeLocalStorage1_profile.zip b/dom/quota/test/xpcshell/removeLocalStorage1_profile.zip Binary files differnew file mode 100644 index 0000000000..19e971433c --- /dev/null +++ b/dom/quota/test/xpcshell/removeLocalStorage1_profile.zip diff --git a/dom/quota/test/xpcshell/removeLocalStorage2_profile.zip b/dom/quota/test/xpcshell/removeLocalStorage2_profile.zip Binary files differnew file mode 100644 index 0000000000..04d1a3462b --- /dev/null +++ b/dom/quota/test/xpcshell/removeLocalStorage2_profile.zip diff --git a/dom/quota/test/xpcshell/telemetry/head.js b/dom/quota/test/xpcshell/telemetry/head.js new file mode 100644 index 0000000000..5c36d82ca6 --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/head.js @@ -0,0 +1,14 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// The path to the top level directory. +const depth = "../../../../../"; + +loadScript("dom/quota/test/xpcshell/common/head.js"); + +function loadScript(path) { + let uri = Services.io.newFileURI(do_get_file(depth + path)); + Services.scriptloader.loadSubScript(uri.spec); +} diff --git a/dom/quota/test/xpcshell/telemetry/test_qm_first_initialization_attempt.js b/dom/quota/test/xpcshell/telemetry/test_qm_first_initialization_attempt.js new file mode 100644 index 0000000000..eafbc95754 --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/test_qm_first_initialization_attempt.js @@ -0,0 +1,867 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const storageDirName = "storage"; +const storageFileName = "storage.sqlite"; +const indexedDBDirName = "indexedDB"; +const persistentStorageDirName = "storage/persistent"; +const histogramName = "QM_FIRST_INITIALIZATION_ATTEMPT"; + +const testcases = [ + { + mainKey: "Storage", + async setup(expectedInitResult) { + if (!expectedInitResult) { + // Make the database unusable by creating it as a directory (not a + // file). + const storageFile = getRelativeFile(storageFileName); + storageFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + }, + initFunction: init, + expectedSnapshots: { + initFailure: { + // mainKey + Storage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + // mainKey + Storage: { + values: [1, 1, 0], + }, + }, + }, + }, + { + mainKey: "TemporaryStorage", + async setup(expectedInitResult) { + // We need to initialize storage before populating the repositories. If + // we don't do that, the storage directory created for the repositories + // would trigger storage upgrades (from version 0 to current version). + let request = init(); + await requestFinished(request); + + populateRepository("temporary"); + populateRepository("default"); + + if (!expectedInitResult) { + makeRepositoryUnusable("temporary"); + makeRepositoryUnusable("default"); + } + }, + initFunction: initTemporaryStorage, + getExpectedSnapshots() { + const expectedSnapshotsInNightly = { + initFailure: { + Storage: { + values: [0, 1, 0], + }, + TemporaryRepository: { + values: [1, 0], + }, + DefaultRepository: { + values: [1, 0], + }, + // mainKey + TemporaryStorage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + Storage: { + values: [0, 2, 0], + }, + TemporaryRepository: { + values: [1, 1, 0], + }, + DefaultRepository: { + values: [1, 1, 0], + }, + // mainKey + TemporaryStorage: { + values: [1, 1, 0], + }, + }, + }; + + const expectedSnapshotsInOthers = { + initFailure: { + Storage: { + values: [0, 1, 0], + }, + TemporaryRepository: { + values: [1, 0], + }, + // mainKey + TemporaryStorage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + Storage: { + values: [0, 2, 0], + }, + TemporaryRepository: { + values: [1, 1, 0], + }, + DefaultRepository: { + values: [0, 1, 0], + }, + // mainKey + TemporaryStorage: { + values: [1, 1, 0], + }, + }, + }; + + return AppConstants.NIGHTLY_BUILD + ? expectedSnapshotsInNightly + : expectedSnapshotsInOthers; + }, + }, + { + mainKey: "DefaultRepository", + async setup(expectedInitResult) { + // See the comment for "TemporaryStorage". + let request = init(); + await requestFinished(request); + + populateRepository("default"); + + if (!expectedInitResult) { + makeRepositoryUnusable("default"); + } + }, + initFunction: initTemporaryStorage, + expectedSnapshots: { + initFailure: { + Storage: { + values: [0, 1, 0], + }, + TemporaryRepository: { + values: [0, 1, 0], + }, + // mainKey + DefaultRepository: { + values: [1, 0], + }, + TemporaryStorage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + Storage: { + values: [0, 2, 0], + }, + TemporaryRepository: { + values: [0, 2, 0], + }, + // mainKey + DefaultRepository: { + values: [1, 1, 0], + }, + TemporaryStorage: { + values: [1, 1, 0], + }, + }, + }, + }, + { + mainKey: "TemporaryRepository", + async setup(expectedInitResult) { + // See the comment for "TemporaryStorage". + let request = init(); + await requestFinished(request); + + populateRepository("temporary"); + + if (!expectedInitResult) { + makeRepositoryUnusable("temporary"); + } + }, + initFunction: initTemporaryStorage, + getExpectedSnapshots() { + const expectedSnapshotsInNightly = { + initFailure: { + Storage: { + values: [0, 1, 0], + }, + // mainKey + TemporaryRepository: { + values: [1, 0], + }, + DefaultRepository: { + values: [0, 1, 0], + }, + TemporaryStorage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + Storage: { + values: [0, 2, 0], + }, + // mainKey + TemporaryRepository: { + values: [1, 1, 0], + }, + DefaultRepository: { + values: [0, 2, 0], + }, + TemporaryStorage: { + values: [1, 1, 0], + }, + }, + }; + + const expectedSnapshotsInOthers = { + initFailure: { + Storage: { + values: [0, 1, 0], + }, + // mainKey + TemporaryRepository: { + values: [1, 0], + }, + TemporaryStorage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + Storage: { + values: [0, 2, 0], + }, + // mainKey + TemporaryRepository: { + values: [1, 1, 0], + }, + DefaultRepository: { + values: [0, 1, 0], + }, + TemporaryStorage: { + values: [1, 1, 0], + }, + }, + }; + + return AppConstants.NIGHTLY_BUILD + ? expectedSnapshotsInNightly + : expectedSnapshotsInOthers; + }, + }, + { + mainKey: "UpgradeStorageFrom0_0To1_0", + async setup(expectedInitResult) { + // storage used prior FF 49 (storage version 0.0) + installPackage("version0_0_profile"); + + if (!expectedInitResult) { + installPackage("version0_0_make_it_unusable"); + } + }, + initFunction: init, + expectedSnapshots: { + initFailure: { + // mainKey + UpgradeStorageFrom0_0To1_0: { + values: [1, 0], + }, + Storage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + // mainKey + UpgradeStorageFrom0_0To1_0: { + values: [1, 1, 0], + }, + UpgradeStorageFrom1_0To2_0: { + values: [0, 1, 0], + }, + UpgradeStorageFrom2_0To2_1: { + values: [0, 1, 0], + }, + UpgradeStorageFrom2_1To2_2: { + values: [0, 1, 0], + }, + UpgradeStorageFrom2_2To2_3: { + values: [0, 1, 0], + }, + Storage: { + values: [1, 1, 0], + }, + }, + }, + }, + { + mainKey: "UpgradeStorageFrom1_0To2_0", + async setup(expectedInitResult) { + // storage used by FF 49-54 (storage version 1.0) + installPackage("version1_0_profile"); + + if (!expectedInitResult) { + installPackage("version1_0_make_it_unusable"); + } + }, + initFunction: init, + expectedSnapshots: { + initFailure: { + // mainKey + UpgradeStorageFrom1_0To2_0: { + values: [1, 0], + }, + Storage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + // mainKey + UpgradeStorageFrom1_0To2_0: { + values: [1, 1, 0], + }, + UpgradeStorageFrom2_0To2_1: { + values: [0, 1, 0], + }, + UpgradeStorageFrom2_1To2_2: { + values: [0, 1, 0], + }, + UpgradeStorageFrom2_2To2_3: { + values: [0, 1, 0], + }, + Storage: { + values: [1, 1, 0], + }, + }, + }, + }, + { + mainKey: "UpgradeStorageFrom2_0To2_1", + async setup(expectedInitResult) { + // storage used by FF 55-56 (storage version 2.0) + installPackage("version2_0_profile"); + + if (!expectedInitResult) { + installPackage("version2_0_make_it_unusable"); + } + }, + initFunction: init, + expectedSnapshots: { + initFailure: { + // mainKey + UpgradeStorageFrom2_0To2_1: { + values: [1, 0], + }, + Storage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + // mainKey + UpgradeStorageFrom2_0To2_1: { + values: [1, 1, 0], + }, + UpgradeStorageFrom2_1To2_2: { + values: [0, 1, 0], + }, + UpgradeStorageFrom2_2To2_3: { + values: [0, 1, 0], + }, + Storage: { + values: [1, 1, 0], + }, + }, + }, + }, + { + mainKey: "UpgradeStorageFrom2_1To2_2", + async setup(expectedInitResult) { + // storage used by FF 57-67 (storage version 2.1) + installPackage("version2_1_profile"); + + if (!expectedInitResult) { + installPackage("version2_1_make_it_unusable"); + } + }, + initFunction: init, + expectedSnapshots: { + initFailure: { + // mainKey + UpgradeStorageFrom2_1To2_2: { + values: [1, 0], + }, + Storage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + // mainKey + UpgradeStorageFrom2_1To2_2: { + values: [1, 1, 0], + }, + UpgradeStorageFrom2_2To2_3: { + values: [0, 1, 0], + }, + Storage: { + values: [1, 1, 0], + }, + }, + }, + }, + { + mainKey: "UpgradeStorageFrom2_2To2_3", + async setup(expectedInitResult) { + // storage used by FF 68-69 (storage version 2.2) + installPackage("version2_2_profile"); + + if (!expectedInitResult) { + installPackage( + "version2_2_make_it_unusable", + /* allowFileOverwrites */ true + ); + } + }, + initFunction: init, + expectedSnapshots: { + initFailure: { + // mainKey + UpgradeStorageFrom2_2To2_3: { + values: [1, 0], + }, + Storage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + // mainKey + UpgradeStorageFrom2_2To2_3: { + values: [1, 1, 0], + }, + Storage: { + values: [1, 1, 0], + }, + }, + }, + }, + { + mainKey: "UpgradeFromIndexedDBDirectory", + async setup(expectedInitResult) { + const indexedDBDir = getRelativeFile(indexedDBDirName); + indexedDBDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + + if (!expectedInitResult) { + // "indexedDB" directory will be moved under "storage" directory and at + // the same time renamed to "persistent". Create a storage file to cause + // the moves to fail. + const storageFile = getRelativeFile(storageDirName); + storageFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + } + }, + initFunction: init, + expectedSnapshots: { + initFailure: { + // mainKey + UpgradeFromIndexedDBDirectory: { + values: [1, 0], + }, + Storage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + // mainKey + UpgradeFromIndexedDBDirectory: { + values: [1, 1, 0], + }, + UpgradeFromPersistentStorageDirectory: { + values: [0, 1, 0], + }, + UpgradeStorageFrom0_0To1_0: { + values: [0, 1, 0], + }, + UpgradeStorageFrom1_0To2_0: { + values: [0, 1, 0], + }, + UpgradeStorageFrom2_0To2_1: { + values: [0, 1, 0], + }, + UpgradeStorageFrom2_1To2_2: { + values: [0, 1, 0], + }, + UpgradeStorageFrom2_2To2_3: { + values: [0, 1, 0], + }, + Storage: { + values: [1, 1, 0], + }, + }, + }, + }, + { + mainKey: "UpgradeFromPersistentStorageDirectory", + async setup(expectedInitResult) { + const persistentStorageDir = getRelativeFile(persistentStorageDirName); + persistentStorageDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + + if (!expectedInitResult) { + // Create a metadata directory to break creating or upgrading directory + // metadata files. + const metadataDir = getRelativeFile( + "storage/persistent/https+++bad.example.com/.metadata" + ); + metadataDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + }, + initFunction: init, + expectedSnapshots: { + initFailure: { + // mainKey + UpgradeFromPersistentStorageDirectory: { + values: [1, 0], + }, + Storage: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + // mainKey + UpgradeFromPersistentStorageDirectory: { + values: [1, 1, 0], + }, + UpgradeStorageFrom0_0To1_0: { + values: [0, 1, 0], + }, + UpgradeStorageFrom1_0To2_0: { + values: [0, 1, 0], + }, + UpgradeStorageFrom2_0To2_1: { + values: [0, 1, 0], + }, + UpgradeStorageFrom2_1To2_2: { + values: [0, 1, 0], + }, + UpgradeStorageFrom2_2To2_3: { + values: [0, 1, 0], + }, + Storage: { + values: [1, 1, 0], + }, + }, + }, + }, + { + mainKey: "PersistentOrigin", + async setup(expectedInitResult) { + // We need to initialize storage before creating the origin files. If we + // don't do that, the storage directory created for the origin files + // would trigger storage upgrades (from version 0 to current version). + let request = init(); + await requestFinished(request); + + if (!expectedInitResult) { + const originFiles = [ + getRelativeFile("storage/permanent/https+++example.com"), + getRelativeFile("storage/permanent/https+++example1.com"), + getRelativeFile("storage/default/https+++example2.com"), + ]; + + for (const originFile of originFiles) { + originFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + } + } + + request = initTemporaryStorage(); + await requestFinished(request); + }, + initFunctions: [ + { + name: initPersistentOrigin, + args: [getPrincipal("https://example.com")], + }, + { + name: initPersistentOrigin, + args: [getPrincipal("https://example1.com")], + }, + { + name: initTemporaryOrigin, + args: ["default", getPrincipal("https://example2.com")], + }, + ], + expectedSnapshots: { + initFailure: { + Storage: { + values: [0, 1, 0], + }, + TemporaryRepository: { + values: [0, 1, 0], + }, + DefaultRepository: { + values: [0, 1, 0], + }, + TemporaryStorage: { + values: [0, 1, 0], + }, + // mainKey + PersistentOrigin: { + values: [2, 0], + }, + TemporaryOrigin: { + values: [1, 0], + }, + }, + initFailureThenSuccess: { + Storage: { + values: [0, 2, 0], + }, + TemporaryRepository: { + values: [0, 2, 0], + }, + DefaultRepository: { + values: [0, 2, 0], + }, + TemporaryStorage: { + values: [0, 2, 0], + }, + // mainKey + PersistentOrigin: { + values: [2, 2, 0], + }, + TemporaryOrigin: { + values: [1, 1, 0], + }, + }, + }, + }, + { + mainKey: "TemporaryOrigin", + async setup(expectedInitResult) { + // See the comment for "PersistentOrigin". + let request = init(); + await requestFinished(request); + + if (!expectedInitResult) { + const originFiles = [ + getRelativeFile("storage/temporary/https+++example.com"), + getRelativeFile("storage/default/https+++example.com"), + getRelativeFile("storage/default/https+++example1.com"), + getRelativeFile("storage/permanent/https+++example2.com"), + ]; + + for (const originFile of originFiles) { + originFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + } + } + + request = initTemporaryStorage(); + await requestFinished(request); + }, + initFunctions: [ + { + name: initTemporaryOrigin, + args: ["temporary", getPrincipal("https://example.com")], + }, + { + name: initTemporaryOrigin, + args: ["default", getPrincipal("https://example.com")], + }, + { + name: initTemporaryOrigin, + args: ["default", getPrincipal("https://example1.com")], + }, + { + name: initPersistentOrigin, + args: [getPrincipal("https://example2.com")], + }, + ], + // Only the first result of EnsureTemporaryOriginIsInitialized per origin + // should be reported. Thus, only the results for (temporary, example.com), + // and (default, example1.com) should be reported. + expectedSnapshots: { + initFailure: { + Storage: { + values: [0, 1, 0], + }, + TemporaryRepository: { + values: [0, 1, 0], + }, + DefaultRepository: { + values: [0, 1, 0], + }, + TemporaryStorage: { + values: [0, 1, 0], + }, + PersistentOrigin: { + values: [1, 0], + }, + // mainKey + TemporaryOrigin: { + values: [2, 0], + }, + }, + initFailureThenSuccess: { + Storage: { + values: [0, 2, 0], + }, + TemporaryRepository: { + values: [0, 2, 0], + }, + DefaultRepository: { + values: [0, 2, 0], + }, + TemporaryStorage: { + values: [0, 2, 0], + }, + PersistentOrigin: { + values: [1, 1, 0], + }, + // mainKey + TemporaryOrigin: { + values: [2, 2, 0], + }, + }, + }, + }, +]; + +loadScript("dom/quota/test/xpcshell/common/utils.js"); + +function verifyHistogram(histogram, mainKey, expectedSnapshot) { + const snapshot = histogram.snapshot(); + + ok( + mainKey in snapshot, + `The histogram ${histogram.name()} must contain the main key ${mainKey}` + ); + + const keys = Object.keys(snapshot); + + is( + keys.length, + Object.keys(expectedSnapshot).length, + `The number of keys must match the expected number of keys for ` + + `${histogram.name()}` + ); + + for (const key of keys) { + ok( + key in expectedSnapshot, + `The key ${key} must match the expected keys for ${histogram.name()}` + ); + + const values = Object.entries(snapshot[key].values); + const expectedValues = expectedSnapshot[key].values; + + is( + values.length, + expectedValues.length, + `The number of values should match the expected number of values for ` + + `${histogram.name()}` + ); + + for (let [i, val] of values) { + is( + val, + expectedValues[i], + `Expected counts should match for ${histogram.name()} at index ${i}` + ); + } + } +} + +async function testSteps() { + let request; + for (const testcase of testcases) { + const mainKey = testcase.mainKey; + + info(`Verifying ${histogramName} histogram for the main key ${mainKey}`); + + const histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + histogramName + ); + + for (const expectedInitResult of [false, true]) { + info( + `Verifying the histogram when the initialization ` + + `${expectedInitResult ? "failed and then succeeds" : "fails"}` + ); + + await testcase.setup(expectedInitResult); + + const msg = `Should ${expectedInitResult ? "not " : ""} have thrown`; + + // Call the initialization function twice, so we can verify below that + // only the first initialization attempt has been reported. + for (let i = 0; i < 2; ++i) { + let initFunctions; + + if (testcase.initFunctions) { + initFunctions = testcase.initFunctions; + } else { + initFunctions = [ + { + name: testcase.initFunction, + args: [], + }, + ]; + } + + for (const initFunction of initFunctions) { + request = initFunction.name(...initFunction.args); + try { + await requestFinished(request); + ok(expectedInitResult, msg); + } catch (ex) { + ok(!expectedInitResult, msg); + } + } + } + + const expectedSnapshots = testcase.getExpectedSnapshots + ? testcase.getExpectedSnapshots() + : testcase.expectedSnapshots; + + const expectedSnapshot = expectedInitResult + ? expectedSnapshots.initFailureThenSuccess + : expectedSnapshots.initFailure; + + verifyHistogram(histogram, mainKey, expectedSnapshot); + + // The first initialization attempt has been reported in the histogram + // and any new attemps wouldn't be reported if we didn't reset or clear + // the storage here. We need a clean profile for the next iteration + // anyway. + // However, the clear storage operation needs initialized storage, so + // clearing can fail if the storage is unusable and it can also increase + // some of the telemetry counters. Instead of calling clear, we can just + // call reset and clear profile manually. + request = reset(); + await requestFinished(request); + + const indexedDBDir = getRelativeFile(indexedDBDirName); + if (indexedDBDir.exists()) { + indexedDBDir.remove(false); + } + + const storageDir = getRelativeFile(storageDirName); + if (storageDir.exists()) { + storageDir.remove(true); + } + + const storageFile = getRelativeFile(storageFileName); + if (storageFile.exists()) { + // It could be a non empty directory, so remove it recursively. + storageFile.remove(true); + } + } + } +} diff --git a/dom/quota/test/xpcshell/telemetry/version0_0_make_it_unusable.zip b/dom/quota/test/xpcshell/telemetry/version0_0_make_it_unusable.zip Binary files differnew file mode 100644 index 0000000000..92dcfb777e --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/version0_0_make_it_unusable.zip diff --git a/dom/quota/test/xpcshell/telemetry/version0_0_profile.zip b/dom/quota/test/xpcshell/telemetry/version0_0_profile.zip Binary files differnew file mode 100644 index 0000000000..2fb0f525c2 --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/version0_0_profile.zip diff --git a/dom/quota/test/xpcshell/telemetry/version1_0_make_it_unusable.zip b/dom/quota/test/xpcshell/telemetry/version1_0_make_it_unusable.zip Binary files differnew file mode 100644 index 0000000000..92dcfb777e --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/version1_0_make_it_unusable.zip diff --git a/dom/quota/test/xpcshell/telemetry/version1_0_profile.zip b/dom/quota/test/xpcshell/telemetry/version1_0_profile.zip Binary files differnew file mode 100644 index 0000000000..6169f439c1 --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/version1_0_profile.zip diff --git a/dom/quota/test/xpcshell/telemetry/version2_0_make_it_unusable.zip b/dom/quota/test/xpcshell/telemetry/version2_0_make_it_unusable.zip Binary files differnew file mode 100644 index 0000000000..92dcfb777e --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/version2_0_make_it_unusable.zip diff --git a/dom/quota/test/xpcshell/telemetry/version2_0_profile.zip b/dom/quota/test/xpcshell/telemetry/version2_0_profile.zip Binary files differnew file mode 100644 index 0000000000..465f53cea9 --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/version2_0_profile.zip diff --git a/dom/quota/test/xpcshell/telemetry/version2_1_make_it_unusable.zip b/dom/quota/test/xpcshell/telemetry/version2_1_make_it_unusable.zip Binary files differnew file mode 100644 index 0000000000..92dcfb777e --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/version2_1_make_it_unusable.zip diff --git a/dom/quota/test/xpcshell/telemetry/version2_1_profile.zip b/dom/quota/test/xpcshell/telemetry/version2_1_profile.zip Binary files differnew file mode 100644 index 0000000000..81463235ab --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/version2_1_profile.zip diff --git a/dom/quota/test/xpcshell/telemetry/version2_2_make_it_unusable.zip b/dom/quota/test/xpcshell/telemetry/version2_2_make_it_unusable.zip Binary files differnew file mode 100644 index 0000000000..b6b8eecabf --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/version2_2_make_it_unusable.zip diff --git a/dom/quota/test/xpcshell/telemetry/version2_2_profile.zip b/dom/quota/test/xpcshell/telemetry/version2_2_profile.zip Binary files differnew file mode 100644 index 0000000000..e572726cca --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/version2_2_profile.zip diff --git a/dom/quota/test/xpcshell/telemetry/xpcshell.ini b/dom/quota/test/xpcshell/telemetry/xpcshell.ini new file mode 100644 index 0000000000..bb5655f103 --- /dev/null +++ b/dom/quota/test/xpcshell/telemetry/xpcshell.ini @@ -0,0 +1,20 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +[DEFAULT] +head = head.js +support-files = + version0_0_make_it_unusable.zip + version0_0_profile.zip + version1_0_make_it_unusable.zip + version1_0_profile.zip + version2_0_make_it_unusable.zip + version2_0_profile.zip + version2_1_make_it_unusable.zip + version2_1_profile.zip + version2_2_make_it_unusable.zip + version2_2_profile.zip + +[test_qm_first_initialization_attempt.js] +skip-if = appname == "thunderbird" diff --git a/dom/quota/test/xpcshell/tempMetadataCleanup_profile.zip b/dom/quota/test/xpcshell/tempMetadataCleanup_profile.zip Binary files differnew file mode 100644 index 0000000000..da1de0979b --- /dev/null +++ b/dom/quota/test/xpcshell/tempMetadataCleanup_profile.zip diff --git a/dom/quota/test/xpcshell/test_allowListFiles.js b/dom/quota/test/xpcshell/test_allowListFiles.js new file mode 100644 index 0000000000..04c64c2ef5 --- /dev/null +++ b/dom/quota/test/xpcshell/test_allowListFiles.js @@ -0,0 +1,61 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify thoes unexpected files are in the allow list of + * QuotaManager. They aren't expected in the repository but if there are, + * QuotaManager shouldn't fail to initialize an origin and getting usage, though + * those files aren't managed by QuotaManager. + */ + +async function testSteps() { + const allowListFiles = [ + ".dot-file", + "desktop.ini", + "Desktop.ini", + "Thumbs.db", + "thumbs.db", + ]; + + for (let allowListFile of allowListFiles) { + info("Testing " + allowListFile + " in the repository"); + + info("Initializing"); + + let request = init(); + await requestFinished(request); + + info("Creating unknown files"); + + for (let dir of ["persistenceType dir", "origin dir"]) { + let dirPath = + dir == "persistenceType dir" + ? "storage/default/" + : "storage/default/http+++example.com/"; + let file = getRelativeFile(dirPath + allowListFile); + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0644", 8)); + } + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Resetting"); + + request = reset(); + await requestFinished(request); + + info("Getting usage"); + + request = getCurrentUsage(continueToNextStepSync); + await requestFinished(request); + + info("Clearing"); + + request = clear(); + await requestFinished(request); + } +} diff --git a/dom/quota/test/xpcshell/test_bad_origin_directory.js b/dom/quota/test/xpcshell/test_bad_origin_directory.js new file mode 100644 index 0000000000..3058e31473 --- /dev/null +++ b/dom/quota/test/xpcshell/test_bad_origin_directory.js @@ -0,0 +1,36 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function* testSteps() { + const invalidOrigin = { + url: "ftp://ftp.invalid.origin", + path: "storage/default/ftp+++ftp.invalid.origin", + }; + + info("Persisting an invalid origin"); + + let invalidPrincipal = getPrincipal(invalidOrigin.url); + + let request = persist(invalidPrincipal, continueToNextStepSync); + yield undefined; + + ok( + request.resultCode === NS_ERROR_FAILURE, + "Persist() failed because of the invalid origin" + ); + ok(request.result === null, "The request result is null"); + + let originDir = getRelativeFile(invalidOrigin.path); + let exists = originDir.exists(); + ok(!exists, "Directory for invalid origin doesn't exist"); + + request = persisted(invalidPrincipal, continueToNextStepSync); + yield undefined; + + ok(request.resultCode === NS_OK, "Persisted() succeeded"); + ok(!request.result, "The origin isn't persisted since the operation failed"); + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/test_basics.js b/dom/quota/test/xpcshell/test_basics.js new file mode 100644 index 0000000000..b72c2feb9f --- /dev/null +++ b/dom/quota/test/xpcshell/test_basics.js @@ -0,0 +1,143 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function* testSteps() { + const storageFile = "storage.sqlite"; + + const metadataFiles = [ + { + path: "storage/permanent/chrome/.metadata", + shouldExistAfterInit: false, + }, + + { + path: "storage/permanent/chrome/.metadata-tmp", + shouldExistAfterInit: false, + }, + + { + path: "storage/permanent/chrome/.metadata-v2", + shouldExistAfterInit: true, + }, + + { + path: "storage/permanent/chrome/.metadata-v2-tmp", + shouldExistAfterInit: false, + }, + ]; + + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Verifying initialization status"); + + verifyInitializationStatus(false, false).then(continueToNextStepSync); + yield undefined; + + info("Getting usage"); + + getCurrentUsage(grabUsageAndContinueHandler); + let usage = yield undefined; + + ok(usage == 0, "Usage is zero"); + + info("Verifying initialization status"); + + verifyInitializationStatus(true, false).then(continueToNextStepSync); + yield undefined; + + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Verifying initialization status"); + + verifyInitializationStatus(false, false).then(continueToNextStepSync); + yield undefined; + + info("Installing package"); + + // The profile contains just one empty IndexedDB database. 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/quota/test/xpcshell/create_db.js + installPackage("basics_profile"); + + info("Getting usage"); + + getCurrentUsage(grabUsageAndContinueHandler); + usage = yield undefined; + + ok(usage > 0, "Usage is not zero"); + + info("Verifying initialization status"); + + verifyInitializationStatus(true, false).then(continueToNextStepSync); + yield undefined; + + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Checking storage file"); + + let file = getRelativeFile(storageFile); + + let exists = file.exists(); + ok(!exists, "Storage file doesn't exist"); + + info("Verifying initialization status"); + + verifyInitializationStatus(false, false).then(continueToNextStepSync); + yield undefined; + + info("Initializing"); + + request = init(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + exists = file.exists(); + ok(exists, "Storage file does exist"); + + info("Verifying initialization status"); + + verifyInitializationStatus(true, false).then(continueToNextStepSync); + yield undefined; + + info("Initializing origin"); + + request = initPersistentOrigin(getCurrentPrincipal(), continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + ok(request.result, "Origin directory was created"); + + for (let metadataFile of metadataFiles) { + file = getRelativeFile(metadataFile.path); + + exists = file.exists(); + + if (metadataFile.shouldExistAfterInit) { + ok(exists, "Metadata file does exist"); + } else { + ok(!exists, "Metadata file doesn't exist"); + } + } + + info("Verifying initialization status"); + + verifyInitializationStatus(true, false).then(continueToNextStepSync); + + yield undefined; + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/test_clearStoragesForOriginAttributesPattern.js b/dom/quota/test/xpcshell/test_clearStoragesForOriginAttributesPattern.js new file mode 100644 index 0000000000..096cf2be70 --- /dev/null +++ b/dom/quota/test/xpcshell/test_clearStoragesForOriginAttributesPattern.js @@ -0,0 +1,58 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() { + const baseRelativePath = "storage/default"; + const userContextForRemoval = 2; + + const origins = [ + { + userContextId: 1, + baseDirName: "https+++example.com", + }, + + { + userContextId: userContextForRemoval, + baseDirName: "https+++example.com", + }, + + // TODO: Uncomment this once bug 1638831 is fixed. + /* + { + userContextId: userContextForRemoval, + baseDirName: "https+++example.org", + }, + */ + ]; + + function getOriginDirectory(origin) { + return getRelativeFile( + `${baseRelativePath}/${origin.baseDirName}^userContextId=` + + `${origin.userContextId}` + ); + } + + let request = init(); + await requestFinished(request); + + for (const origin of origins) { + const directory = getOriginDirectory(origin); + directory.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } + + request = Services.qms.clearStoragesForOriginAttributesPattern( + `{ "userContextId": ${userContextForRemoval} }` + ); + await requestFinished(request); + + for (const origin of origins) { + const directory = getOriginDirectory(origin); + if (origin.userContextId === userContextForRemoval) { + ok(!directory.exists(), "Origin directory should have been removed"); + } else { + ok(directory.exists(), "Origin directory shouldn't have been removed"); + } + } +} diff --git a/dom/quota/test/xpcshell/test_clearStoragesForPrincipal.js b/dom/quota/test/xpcshell/test_clearStoragesForPrincipal.js new file mode 100644 index 0000000000..1e8bed54b7 --- /dev/null +++ b/dom/quota/test/xpcshell/test_clearStoragesForPrincipal.js @@ -0,0 +1,56 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is an xpcshell test for clearStoragesForPrincipal. It verifies that + * if the removing client is the last client in the targeting origin, then it + * is expected to remove the origin directory as well. + */ + +async function testSteps() { + const testingOrigins = [ + { + origin: "http://example.com", + path: "storage/default/http+++example.com/", + only_idb: false, + }, + { + origin: "http://www.mozilla.org", + path: "storage/default/http+++www.mozilla.org/", + only_idb: true, + }, + ]; + const removingClient = "idb"; + + info("Installing package to create the environment"); + // The package is manually created and it contains: + // - storage/default/http+++www.mozilla.org/idb/ + // - storage/default/http+++www.example.com/idb/ + // - storage/default/http+++www.example.com/cache/ + installPackage("clearStoragesForPrincipal_profile"); + + let request; + let file; + for (let i = 0; i < testingOrigins.length; ++i) { + info("Clearing"); + request = clearClient( + getPrincipal(testingOrigins[i].origin), + null, + removingClient + ); + await requestFinished(request); + + info("Verifying"); + file = getRelativeFile(testingOrigins[i].path + removingClient); + ok(!file.exists(), "Client file doesn't exist"); + + file = getRelativeFile(testingOrigins[i].path); + if (testingOrigins[i].only_idb) { + todo(!file.exists(), "Origin file doesn't exist"); + } else { + ok(file.exists(), "Origin file does exist"); + } + } +} diff --git a/dom/quota/test/xpcshell/test_createLocalStorage.js b/dom/quota/test/xpcshell/test_createLocalStorage.js new file mode 100644 index 0000000000..a99a458713 --- /dev/null +++ b/dom/quota/test/xpcshell/test_createLocalStorage.js @@ -0,0 +1,155 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() { + const webAppsStoreFile = "webappsstore.sqlite"; + const lsArchiveFile = "storage/ls-archive.sqlite"; + const lsArchiveTmpFile = "storage/ls-archive-tmp.sqlite"; + + function checkArchiveFileNotExists() { + info("Checking archive tmp file"); + + let archiveTmpFile = getRelativeFile(lsArchiveTmpFile); + + let exists = archiveTmpFile.exists(); + ok(!exists, "archive tmp file doesn't exist"); + + info("Checking archive file"); + + let archiveFile = getRelativeFile(lsArchiveFile); + + exists = archiveFile.exists(); + ok(!exists, "archive file doesn't exist"); + } + + function checkArchiveFileExists() { + info("Checking archive tmp file"); + + let archiveTmpFile = getRelativeFile(lsArchiveTmpFile); + + let exists = archiveTmpFile.exists(); + ok(!exists, "archive tmp file doesn't exist"); + + info("Checking archive file"); + + let archiveFile = getRelativeFile(lsArchiveFile); + + exists = archiveFile.exists(); + ok(exists, "archive file does exist"); + + info("Checking archive file size"); + + let fileSize = archiveFile.fileSize; + ok(fileSize > 0, "archive file size is greater than zero"); + } + + // Profile 1 - Nonexistent apps store file. + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + let appsStoreFile = getRelativeFile(webAppsStoreFile); + + let exists = appsStoreFile.exists(); + ok(!exists, "apps store file doesn't exist"); + + checkArchiveFileNotExists(); + + try { + request = init(); + await requestFinished(request); + + ok(true, "Should not have thrown"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + + checkArchiveFileExists(); + + // Profile 2 - apps store file is a directory. + info("Clearing"); + + request = clear(); + await requestFinished(request); + + appsStoreFile.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + + checkArchiveFileNotExists(); + + try { + request = init(); + await requestFinished(request); + + ok(true, "Should not have thrown"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + + checkArchiveFileExists(); + + appsStoreFile.remove(true); + + // Profile 3 - Corrupted apps store file. + info("Clearing"); + + request = clear(); + await requestFinished(request); + + let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init(appsStoreFile, -1, parseInt("0644", 8), 0); + ostream.write("foobar", 6); + ostream.close(); + + checkArchiveFileNotExists(); + + try { + request = init(); + await requestFinished(request); + + ok(true, "Should not have thrown"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + + checkArchiveFileExists(); + + appsStoreFile.remove(false); + + // Profile 4 - Nonupdateable apps store file. + info("Clearing"); + + request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite + // webappstore.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("createLocalStorage_profile"); + + let fileSize = appsStoreFile.fileSize; + ok(fileSize > 0, "apps store file size is greater than zero"); + + checkArchiveFileNotExists(); + + try { + request = init(); + await requestFinished(request); + + ok(true, "Should not have thrown"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + + checkArchiveFileExists(); + + appsStoreFile.remove(false); +} diff --git a/dom/quota/test/xpcshell/test_estimateOrigin.js b/dom/quota/test/xpcshell/test_estimateOrigin.js new file mode 100644 index 0000000000..31d6f01686 --- /dev/null +++ b/dom/quota/test/xpcshell/test_estimateOrigin.js @@ -0,0 +1,80 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadScript("dom/quota/test/xpcshell/common/utils.js"); + +async function verifyOriginEstimation(principal, expectedUsage, expectedLimit) { + info("Estimating origin"); + + const request = estimateOrigin(principal); + await requestFinished(request); + + is(request.result.usage, expectedUsage, "Correct usage"); + is(request.result.limit, expectedLimit, "Correct limit"); +} + +async function testSteps() { + // The group limit is calculated as 20% of the global limit and the minimum + // value of the group limit is 10 MB. + + const groupLimitKB = 10 * 1024; + const groupLimitBytes = groupLimitKB * 1024; + const globalLimitKB = groupLimitKB * 5; + const globalLimitBytes = globalLimitKB * 1024; + + info("Setting limits"); + + setGlobalLimit(globalLimitKB); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Filling origins"); + + await fillOrigin(getPrincipal("https://foo1.example1.com"), 100); + await fillOrigin(getPrincipal("https://foo2.example1.com"), 200); + await fillOrigin(getPrincipal("https://foo1.example2.com"), 300); + await fillOrigin(getPrincipal("https://foo2.example2.com"), 400); + + info("Verifying origin estimations"); + + await verifyOriginEstimation( + getPrincipal("https://foo1.example1.com"), + 300, + groupLimitBytes + ); + await verifyOriginEstimation( + getPrincipal("https://foo2.example1.com"), + 300, + groupLimitBytes + ); + await verifyOriginEstimation( + getPrincipal("https://foo1.example2.com"), + 700, + groupLimitBytes + ); + await verifyOriginEstimation( + getPrincipal("https://foo2.example2.com"), + 700, + groupLimitBytes + ); + + info("Persisting origin"); + + request = persist(getPrincipal("https://foo2.example2.com")); + await requestFinished(request); + + info("Verifying origin estimation"); + + await verifyOriginEstimation( + getPrincipal("https://foo2.example2.com"), + 1000, + globalLimitBytes + ); + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/test_getUsage.js b/dom/quota/test/xpcshell/test_getUsage.js new file mode 100644 index 0000000000..282894beeb --- /dev/null +++ b/dom/quota/test/xpcshell/test_getUsage.js @@ -0,0 +1,129 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function* testSteps() { + const origins = [ + { + origin: "http://example.com", + persisted: false, + usage: 49152, + }, + + { + origin: "http://localhost", + persisted: false, + usage: 147456, + }, + + { + origin: "http://www.mozilla.org", + persisted: true, + usage: 98304, + }, + ]; + + const allOrigins = [ + { + origin: "chrome", + persisted: false, + usage: 147456, + }, + + { + origin: "http://example.com", + persisted: false, + usage: 49152, + }, + + { + origin: "http://localhost", + persisted: false, + usage: 147456, + }, + + { + origin: "http://www.mozilla.org", + persisted: true, + usage: 98304, + }, + ]; + + function verifyResult(result, expectedOrigins) { + ok(result instanceof Array, "Got an array object"); + ok(result.length == expectedOrigins.length, "Correct number of elements"); + + info("Sorting elements"); + + result.sort(function(a, b) { + let originA = a.origin; + let originB = b.origin; + + if (originA < originB) { + return -1; + } + if (originA > originB) { + return 1; + } + return 0; + }); + + info("Verifying elements"); + + for (let i = 0; i < result.length; i++) { + let a = result[i]; + let b = expectedOrigins[i]; + ok(a.origin == b.origin, "Origin equals"); + ok(a.persisted == b.persisted, "Persisted equals"); + ok(a.usage == b.usage, "Usage equals"); + } + } + + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Getting usage"); + + getUsage(grabResultAndContinueHandler, /* getAll */ true); + let result = yield undefined; + + info("Verifying result"); + + verifyResult(result, []); + + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Installing package"); + + // The profile contains IndexedDB databases placed across the repositories. + // 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/quota/test/xpcshell/create_db.js + installPackage("getUsage_profile"); + + info("Getting usage"); + + getUsage(grabResultAndContinueHandler, /* getAll */ false); + result = yield undefined; + + info("Verifying result"); + + verifyResult(result, origins); + + info("Getting usage"); + + getUsage(grabResultAndContinueHandler, /* getAll */ true); + result = yield undefined; + + info("Verifying result"); + + verifyResult(result, allOrigins); + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/test_groupMismatch.js b/dom/quota/test/xpcshell/test_groupMismatch.js new file mode 100644 index 0000000000..41a72d51ad --- /dev/null +++ b/dom/quota/test/xpcshell/test_groupMismatch.js @@ -0,0 +1,74 @@ +/** + * 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. See bug 1535995. + */ + +loadScript("dom/quota/test/common/file.js"); + +async function testSteps() { + const metadataFile = getRelativeFile( + "storage/default/https+++foo.bar.mozilla-iot.org/.metadata-v2" + ); + + async function readMetadataFile() { + let file = await File.createFromNsIFile(metadataFile); + + let buffer = await new Promise(resolve => { + let reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsArrayBuffer(file); + }); + + return buffer; + } + + 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/xpcshell/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("Reading out contents of metadata file"); + + let metadataBuffer = await readMetadataFile(); + + info("Initializing"); + + request = init(); + await requestFinished(request); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Reading out contents of metadata file"); + + let metadataBuffer2 = await readMetadataFile(); + + info("Verifying blobs differ"); + + ok(!compareBuffers(metadataBuffer, metadataBuffer2), "Metadata differ"); +} diff --git a/dom/quota/test/xpcshell/test_initTemporaryStorage.js b/dom/quota/test/xpcshell/test_initTemporaryStorage.js new file mode 100644 index 0000000000..d6a6cfcb5f --- /dev/null +++ b/dom/quota/test/xpcshell/test_initTemporaryStorage.js @@ -0,0 +1,49 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify initTemporaryStorage() does call + * QuotaManager::EnsureTemporaryStorageIsInitialized() which does various + * things, for example, it restores the directory metadata if it's broken or + * missing. + */ + +async function testSteps() { + const originDirPath = "storage/default/https+++foo.example.com"; + const metadataFileName = ".metadata-v2"; + + info("Initializing"); + + let request = init(); + await requestFinished(request); + + info("Verifying initialization status"); + + await verifyInitializationStatus(true, false); + + info("Creating an empty directory"); + + let originDir = getRelativeFile(originDirPath); + originDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + + info("Initializing the temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info( + "Verifying directory metadata was restored after calling " + + "initTemporaryStorage()" + ); + + let metadataFile = originDir.clone(); + metadataFile.append(metadataFileName); + + ok(metadataFile.exists(), "Directory metadata file does exist"); + + info("Verifying initialization status"); + + await verifyInitializationStatus(true, true); +} diff --git a/dom/quota/test/xpcshell/test_listOrigins.js b/dom/quota/test/xpcshell/test_listOrigins.js new file mode 100644 index 0000000000..223e6a9401 --- /dev/null +++ b/dom/quota/test/xpcshell/test_listOrigins.js @@ -0,0 +1,80 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() { + const origins = [ + "https://example.com", + "https://localhost", + "https://www.mozilla.org", + ]; + + function verifyResult(result, expectedOrigins) { + ok(result instanceof Array, "Got an array object"); + ok(result.length == expectedOrigins.length, "Correct number of elements"); + + info("Sorting elements"); + + result.sort(function(a, b) { + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }); + + info("Verifying elements"); + + for (let i = 0; i < result.length; i++) { + ok(result[i] == expectedOrigins[i], "Result matches expected origin"); + } + } + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Listing origins"); + + request = listOrigins(); + await requestFinished(request); + + info("Verifying result"); + + verifyResult(request.result, []); + + info("Clearing"); + + request = clear(); + await requestFinished(request); + + info("Initializing"); + + request = init(); + await requestFinished(request); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Initializing origins"); + + for (const origin of origins) { + request = initTemporaryOrigin("default", getPrincipal(origin)); + await requestFinished(request); + } + + info("Listing origins"); + + request = listOrigins(); + await requestFinished(request); + + info("Verifying result"); + + verifyResult(request.result, origins); +} diff --git a/dom/quota/test/xpcshell/test_originEndsWithDot.js b/dom/quota/test/xpcshell/test_originEndsWithDot.js new file mode 100644 index 0000000000..8301b3292d --- /dev/null +++ b/dom/quota/test/xpcshell/test_originEndsWithDot.js @@ -0,0 +1,70 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadScript("dom/quota/test/common/file.js"); + +async function testSteps() { + // First, ensure the origin can be initialized and used by a client that uses + // SQLite databases. + + // Todo: consider using simpleDB once it supports using storage for SQLite. + info("Testing SQLite database with an origin that ends with a dot"); + + const principal = getPrincipal("https://example.com."); + let request = indexedDB.openForPrincipal(principal, "myIndexedDB"); + await openDBRequestUpgradeNeeded(request); + + info("Testing simple operations"); + + const database = request.result; + + const objectStore = database.createObjectStore("Blobs", {}); + + objectStore.add(getNullBlob(200), 42); + + await openDBRequestSucceeded(request); + + database.close(); + + info("Reseting"); + + request = reset(); + await requestFinished(request); + + let idbDB = getRelativeFile( + "storage/default/https+++example.com./idb/2320029346mByDIdnedxe.sqlite" + ); + ok(idbDB.exists(), "IDB database was created successfully"); + + // Second, ensure storage initialization works fine with the origin. + + info("Testing storage initialization and temporary storage initialization"); + + request = init(); + await requestFinished(request); + + request = initTemporaryStorage(); + await requestFinished(request); + + // Third, ensure QMS APIs that touch the client directory for the origin work + // fine. + + info("Testing getUsageForPrincipal"); + + request = getOriginUsage(principal); + await requestFinished(request); + + ok( + request.result instanceof Ci.nsIQuotaOriginUsageResult, + "The result is nsIQuotaOriginUsageResult instance" + ); + ok(request.result.usage > 0, "Total usage is not empty"); + ok(request.result.fileUsage > 0, "File usage is not empty"); + + info("Testing clearStoragesForPrincipal"); + + request = clearOrigin(principal, "default"); + await requestFinished(request); +} diff --git a/dom/quota/test/xpcshell/test_originMismatch.js b/dom/quota/test/xpcshell/test_originMismatch.js new file mode 100644 index 0000000000..23186977b2 --- /dev/null +++ b/dom/quota/test/xpcshell/test_originMismatch.js @@ -0,0 +1,75 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that temporary storage initialization should + * succeed while there is an origin directory that has an inconsistency between + * its directory name and the origin name in its directory metadata file. + */ + +async function testSteps() { + const packages = ["originMismatch_profile", "defaultStorageDirectory_shared"]; + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing package"); + + // The profile contains: + // - storage.sqlite (v2_3) + // (A) Verify we are okay while the directory that we want to restore has + // already existed. + // - storage/default/http+++www.example.com/.metadata-v2 + // (origin: http://www.example.com.) + // - storage/default/http+++www.example.com/cache/.padding + // - storage/default/http+++www.example.com./ + // (B) Verify restoring origin directory succeed. + // - storage/default/http+++www.example.org/.metadata-v2 + // (origin: http://www.example.org.) + // - storage/default/http+++www.example.org/cache/.padding + // + // ToDo: Test case like: + // - storage/default/http+++www.example.org(1)/.metadata-v2 + // (origin: http://www.example.org) + // - storage/default/http+++www.example.org/ + // + // - storage/default/http+++www.foo.com/.metadata-v2 + // (origin: http://www.bar.com) + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Initializing storage"); + + request = init(); + await requestFinished(request); + + // ToDo: Remove this code once we support unknown directories in respository + // (bug 1594075). + let invalidDir = getRelativeFile("storage/default/invalid+++example.com"); + invalidDir.remove(true); + invalidDir = getRelativeFile("storage/temporary/invalid+++example.com"); + invalidDir.remove(true); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "afterInitTemporaryStorage"); + + request = clear(); + await requestFinished(request); +} diff --git a/dom/quota/test/xpcshell/test_originWithCaret.js b/dom/quota/test/xpcshell/test_originWithCaret.js new file mode 100644 index 0000000000..7afce4f5cc --- /dev/null +++ b/dom/quota/test/xpcshell/test_originWithCaret.js @@ -0,0 +1,17 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +async function testSteps() { + Assert.throws( + () => { + const principal = getPrincipal("http://example.com^123"); + getSimpleDatabase(principal); + }, + /NS_ERROR_MALFORMED_URI/, + "^ is not allowed in the hostname" + ); +} diff --git a/dom/quota/test/xpcshell/test_orpahnedQuotaObject.js b/dom/quota/test/xpcshell/test_orpahnedQuotaObject.js new file mode 100644 index 0000000000..6cbca13d8c --- /dev/null +++ b/dom/quota/test/xpcshell/test_orpahnedQuotaObject.js @@ -0,0 +1,44 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() { + const principal = getPrincipal("https://example.com"); + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.client_validation", false); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Creating simpledb"); + + let database = getSimpleDatabase(principal); + + request = database.open("data"); + await requestFinished(request); + + info("Creating localStorage"); + + let storage = Services.domStorageManager.createStorage( + null, + principal, + principal, + "" + ); + storage.setItem("key", "value"); + + info("Clearing simpledb"); + + request = clearClient(principal, "default", "sdb"); + await requestFinished(request); + + info("Resetting localStorage"); + + request = resetClient(principal, "ls"); + await requestFinished(request); +} diff --git a/dom/quota/test/xpcshell/test_persist.js b/dom/quota/test/xpcshell/test_persist.js new file mode 100644 index 0000000000..0920522973 --- /dev/null +++ b/dom/quota/test/xpcshell/test_persist.js @@ -0,0 +1,121 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function* testSteps() { + const origin = { + url: "http://default.test.persist", + path: "storage/default/http+++default.test.persist", + persistence: "default", + }; + + const metadataFileName = ".metadata-v2"; + + let principal = getPrincipal(origin.url); + + info("Persisting an uninitialized origin"); + + // Origin directory doesn't exist yet, so only check the result for + // persisted(). + let request = persisted(principal, continueToNextStepSync); + yield undefined; + + ok(request.resultCode === NS_OK, "Persisted() succeeded"); + ok(!request.result, "The origin is not persisted"); + + info("Verifying persist() does update the metadata"); + + request = persist(principal, continueToNextStepSync); + yield undefined; + + ok(request.resultCode === NS_OK, "Persist() succeeded"); + + let originDir = getRelativeFile(origin.path); + let exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + + info("Reading out contents of metadata file"); + + let metadataFile = originDir.clone(); + metadataFile.append(metadataFileName); + + File.createFromNsIFile(metadataFile).then(grabArgAndContinueHandler); + let file = yield undefined; + + let fileReader = new FileReader(); + fileReader.onload = continueToNextStepSync; + fileReader.readAsArrayBuffer(file); + yield undefined; + + let originPersisted = getPersistedFromMetadata(fileReader.result); + ok(originPersisted, "The origin is persisted"); + + info("Verifying persisted()"); + + request = persisted(principal, continueToNextStepSync); + yield undefined; + + ok(request.resultCode === NS_OK, "Persisted() succeeded"); + ok(request.result === originPersisted, "Persisted() concurs with metadata"); + + info("Clearing the origin"); + + // Clear the origin since we'll test the same directory again under different + // circumstances. + clearOrigin(principal, origin.persistence, continueToNextStepSync); + yield undefined; + + info("Persisting an already initialized origin"); + + initTemporaryStorage(continueToNextStepSync); + yield undefined; + + initTemporaryOrigin(origin.persistence, principal, continueToNextStepSync); + yield undefined; + + info("Reading out contents of metadata file"); + + fileReader = new FileReader(); + fileReader.onload = continueToNextStepSync; + fileReader.readAsArrayBuffer(file); + yield undefined; + + originPersisted = getPersistedFromMetadata(fileReader.result); + ok(!originPersisted, "The origin isn't persisted after clearing"); + + info("Verifying persisted()"); + + request = persisted(principal, continueToNextStepSync); + yield undefined; + + ok(request.resultCode === NS_OK, "Persisted() succeeded"); + ok(request.result === originPersisted, "Persisted() concurs with metadata"); + + info("Verifying persist() does update the metadata"); + + request = persist(principal, continueToNextStepSync); + yield undefined; + + ok(request.resultCode === NS_OK, "Persist() succeeded"); + + info("Reading out contents of metadata file"); + + fileReader = new FileReader(); + fileReader.onload = continueToNextStepSync; + fileReader.readAsArrayBuffer(file); + yield undefined; + + originPersisted = getPersistedFromMetadata(fileReader.result); + ok(originPersisted, "The origin is persisted"); + + info("Verifying persisted()"); + + request = persisted(principal, continueToNextStepSync); + yield undefined; + + ok(request.resultCode === NS_OK, "Persisted() succeeded"); + ok(request.result === originPersisted, "Persisted() concurs with metadata"); + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/test_persist_eviction.js b/dom/quota/test/xpcshell/test_persist_eviction.js new file mode 100644 index 0000000000..9a62f91b50 --- /dev/null +++ b/dom/quota/test/xpcshell/test_persist_eviction.js @@ -0,0 +1,82 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that normally the oldest origin will be + * evicted if the global limit is reached, but if the oldest origin is + * persisted or is an extension origin, then it won't be evicted. + */ + +loadScript("dom/quota/test/xpcshell/common/utils.js"); + +async function testSteps() { + // The group limit is calculated as 20% of the global limit and the minimum + // value of the group limit is 10 MB. + + const groupLimitKB = 10 * 1024; + const globalLimitKB = groupLimitKB * 5; + + setGlobalLimit(globalLimitKB); + + let request = clear(); + await requestFinished(request); + + for (let persistOldestOrigin of [false, true]) { + info( + "Testing " + + (persistOldestOrigin ? "with" : "without") + + " persisting the oldest origin" + ); + + info( + "Step 0: Filling a moz-extension origin as the oldest origin with non-persisted data" + ); + + // Just a fake moz-extension origin to mock an extension origin. + let extUUID = "20445ca5-75f9-420e-a1d4-9cccccb5e891"; + let spec = `moz-extension://${extUUID}`; + await fillOrigin(getPrincipal(spec), groupLimitKB * 1024); + + info( + "Step 1: Filling five separate web origins to reach the global limit " + + "and trigger eviction" + ); + + for (let index = 1; index <= 5; index++) { + let spec = "http://example" + index + ".com"; + if (index == 1 && persistOldestOrigin) { + request = persist(getPrincipal(spec)); + await requestFinished(request); + } + await fillOrigin(getPrincipal(spec), groupLimitKB * 1024); + } + + info("Step 2: Verifying origin directories"); + + for (let index = 1; index <= 5; index++) { + let path = "storage/default/http+++example" + index + ".com"; + let file = getRelativeFile(path); + if (index == (persistOldestOrigin ? 2 : 1)) { + ok(!file.exists(), "The origin directory " + path + " doesn't exist"); + } else { + ok(file.exists(), "The origin directory " + path + " does exist"); + } + } + + // Verify that the extension storage data has not been evicted (even if it wasn't marked as + // persisted and it was the less recently used origin). + let path = `storage/default/moz-extension+++${extUUID}`; + let file = getRelativeFile(path); + ok(file.exists(), "The origin directory " + path + "does exist"); + + request = clear(); + await requestFinished(request); + } + + resetGlobalLimit(); + + request = reset(); + await requestFinished(request); +} diff --git a/dom/quota/test/xpcshell/test_persist_globalLimit.js b/dom/quota/test/xpcshell/test_persist_globalLimit.js new file mode 100644 index 0000000000..8b15c26eae --- /dev/null +++ b/dom/quota/test/xpcshell/test_persist_globalLimit.js @@ -0,0 +1,82 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that persisted origins are always bounded by + * the global limit. + */ + +loadScript("dom/quota/test/common/file.js"); + +async function testSteps() { + const globalLimitKB = 1; + + const principal = getPrincipal("https://persisted.example.com"); + + info("Setting limits"); + + setGlobalLimit(globalLimitKB); + + let request = clear(); + await requestFinished(request); + + for (let initializeStorageBeforePersist of [false, true]) { + if (initializeStorageBeforePersist) { + info("Initializing"); + + request = init(); + await requestFinished(request); + + info("Initializing the temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + } + + info("Persisting an origin"); + + request = persist(principal); + await requestFinished(request); + + info("Verifying the persisted origin is bounded by global limit"); + + let database = getSimpleDatabase(principal); + + info("Opening a database for the persisted origin"); + + request = database.open("data"); + await requestFinished(request); + + try { + info("Writing over the limit shouldn't succeed"); + + request = database.write(getBuffer(globalLimitKB * 1024 + 1)); + await requestFinished(request); + + ok(false, "Should have thrown"); + } catch (e) { + ok(true, "Should have thrown"); + ok( + e.resultCode === NS_ERROR_FILE_NO_DEVICE_SPACE, + "Threw right result code" + ); + } + + info("Closing the database and clearing"); + + request = database.close(); + await requestFinished(request); + + request = clear(); + await requestFinished(request); + } + + info("Resetting limits"); + + resetGlobalLimit(); + + request = reset(); + await requestFinished(request); +} diff --git a/dom/quota/test/xpcshell/test_persist_groupLimit.js b/dom/quota/test/xpcshell/test_persist_groupLimit.js new file mode 100644 index 0000000000..202726299f --- /dev/null +++ b/dom/quota/test/xpcshell/test_persist_groupLimit.js @@ -0,0 +1,104 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that persisted origins are not constrained by + * the group limit. It consits of these steps: + * - Set the limits as small as our limits allow. This does result in needing + * to perform 10 megs of writes which is a lot for a test but not horrible. + * - Create databases for 2 origins under the same group. + * - Have the foo2 origin use up the shared group quota. + * - Verify neither origin can write additional data (via a single byte write). + * - Do navigator.storage.persist() for that foo2 origin. + * - Verify that both origins can now write an additional byte. This + * demonstrates that: + * - foo2 no longer counts against the group limit at all since foo1 can + * write a byte. + * - foo2 is no longer constrained by the group limit itself. + */ +async function testSteps() { + // The group limit is calculated as 20% of the global limit and the minimum + // value of the group limit is 10 MB. + + const groupLimitKB = 10 * 1024; + const globalLimitKB = groupLimitKB * 5; + + const urls = ["http://foo1.example.com", "http://foo2.example.com"]; + + const foo2Index = 1; + + let index; + + info("Setting limits"); + + setGlobalLimit(globalLimitKB); + + let request = clear(); + await requestFinished(request); + + info("Opening databases"); + + let databases = []; + for (index = 0; index < urls.length; index++) { + let database = getSimpleDatabase(getPrincipal(urls[index])); + + request = database.open("data"); + await requestFinished(request); + + databases.push(database); + } + + info("Filling up the whole group"); + + try { + request = databases[foo2Index].write(new ArrayBuffer(groupLimitKB * 1024)); + await requestFinished(request); + ok(true, "Should not have thrown"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + + info("Verifying no more data can be written"); + + for (index = 0; index < urls.length; index++) { + try { + request = databases[index].write(new ArrayBuffer(1)); + await requestFinished(request); + ok(false, "Should have thrown"); + } catch (e) { + ok(true, "Should have thrown"); + ok( + e.resultCode == NS_ERROR_FILE_NO_DEVICE_SPACE, + "Threw right result code" + ); + } + } + + info("Persisting origin"); + + request = persist(getPrincipal(urls[foo2Index])); + await requestFinished(request); + + info("Verifying more data data can be written"); + + for (index = 0; index < urls.length; index++) { + try { + request = databases[index].write(new ArrayBuffer(1)); + await requestFinished(request); + ok(true, "Should not have thrown"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + } + + info("Closing databases"); + + for (index = 0; index < urls.length; index++) { + request = databases[index].close(); + await requestFinished(request); + } + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/test_removeLocalStorage.js b/dom/quota/test/xpcshell/test_removeLocalStorage.js new file mode 100644 index 0000000000..bae4ad1649 --- /dev/null +++ b/dom/quota/test/xpcshell/test_removeLocalStorage.js @@ -0,0 +1,89 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function* testSteps() { + const lsArchiveFile = "storage/ls-archive.sqlite"; + const lsArchiveTmpFile = "storage/ls-archive-tmp.sqlite"; + const lsDir = "storage/default/http+++localhost/ls"; + + info("Setting pref"); + + SpecialPowers.setBoolPref( + "dom.storage.enable_unsupported_legacy_implementation", + true + ); + + // Profile 1 + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Installing package"); + + installPackage("removeLocalStorage1_profile"); + + info("Checking ls archive tmp file"); + + let archiveTmpFile = getRelativeFile(lsArchiveTmpFile); + + let exists = archiveTmpFile.exists(); + ok(exists, "ls archive tmp file does exist"); + + info("Initializing"); + + let request = init(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Checking ls archive file"); + + exists = archiveTmpFile.exists(); + ok(!exists, "ls archive tmp file doesn't exist"); + + // Profile 2 + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Installing package"); + + installPackage("removeLocalStorage2_profile"); + + info("Checking ls archive file"); + + let archiveFile = getRelativeFile(lsArchiveFile); + + exists = archiveFile.exists(); + ok(exists, "ls archive file does exist"); + + info("Checking ls dir"); + + let dir = getRelativeFile(lsDir); + + exists = dir.exists(); + ok(exists, "ls directory does exist"); + + info("Initializing"); + + request = init(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Checking ls archive file"); + + exists = archiveFile.exists(); + ok(!exists, "ls archive file doesn't exist"); + + info("Checking ls dir"); + + exists = dir.exists(); + ok(!exists, "ls directory doesn't exist"); + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/test_simpledb.js b/dom/quota/test/xpcshell/test_simpledb.js new file mode 100644 index 0000000000..b698d778e0 --- /dev/null +++ b/dom/quota/test/xpcshell/test_simpledb.js @@ -0,0 +1,6 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadScript("dom/quota/test/common/test_simpledb.js"); diff --git a/dom/quota/test/xpcshell/test_specialOrigins.js b/dom/quota/test/xpcshell/test_specialOrigins.js new file mode 100644 index 0000000000..d66700d359 --- /dev/null +++ b/dom/quota/test/xpcshell/test_specialOrigins.js @@ -0,0 +1,55 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() { + const origins = [ + { + path: "storage/default/file+++UNIVERSAL_FILE_URI_ORIGIN", + url: "file:///Test/test.html", + persistence: "default", + }, + ]; + + info("Setting pref"); + + SpecialPowers.setBoolPref("security.fileuri.strict_origin_policy", false); + + info("Initializing"); + + let request = init(); + await requestFinished(request); + + info("Creating origin directories"); + + for (let origin of origins) { + let originDir = getRelativeFile(origin.path); + originDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Initializing origin directories"); + + for (let origin of origins) { + let result; + + try { + request = initTemporaryOrigin( + origin.persistence, + getPrincipal(origin.url) + ); + result = await requestFinished(request); + + ok(true, "Should not have thrown"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + + ok(!result, "Origin directory wasn't created"); + } +} diff --git a/dom/quota/test/xpcshell/test_storagePressure.js b/dom/quota/test/xpcshell/test_storagePressure.js new file mode 100644 index 0000000000..2e95b2d749 --- /dev/null +++ b/dom/quota/test/xpcshell/test_storagePressure.js @@ -0,0 +1,135 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that the storage pressure event is fired when + * the eviction process is not able to free some space when a quota client + * attempts to write over the global limit or when the global limit is reduced + * below the global usage. + */ + +loadScript("dom/quota/test/common/file.js"); + +function awaitStoragePressure() { + let promise_resolve; + + let promise = new Promise(function(resolve) { + promise_resolve = resolve; + }); + + function observer(subject, topic) { + ok(true, "Got the storage pressure event"); + + Services.obs.removeObserver(observer, topic); + + let usage = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + promise_resolve(usage); + } + + Services.obs.addObserver(observer, "QuotaManager::StoragePressure"); + + return promise; +} + +async function testSteps() { + const globalLimitKB = 2; + + const principal = getPrincipal("https://example.com"); + + info("Setting limits"); + + setGlobalLimit(globalLimitKB); + + info("Initializing"); + + let request = init(); + await requestFinished(request); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Persisting and filling an origin"); + + // We need to persist the origin first to omit the group limit checks. + // Otherwise, we would have to fill five separate origins. + request = persist(principal); + await requestFinished(request); + + let database = getSimpleDatabase(principal); + + request = database.open("data"); + await requestFinished(request); + + try { + request = database.write(getBuffer(globalLimitKB * 1024)); + await requestFinished(request); + + ok(true, "Should not have thrown"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + + info("Testing storage pressure by writing over the global limit"); + + info("Storing one more byte to get the storage pressure event while writing"); + + let promiseStoragePressure = awaitStoragePressure(); + + try { + request = database.write(getBuffer(1)); + await requestFinished(request); + + ok(false, "Should have thrown"); + } catch (e) { + ok(true, "Should have thrown"); + ok( + e.resultCode === NS_ERROR_FILE_NO_DEVICE_SPACE, + "Threw right result code" + ); + } + + info("Checking the storage pressure event"); + + let usage = await promiseStoragePressure; + ok(usage == globalLimitKB * 1024, "Got correct usage"); + + info("Testing storage pressure by reducing the global limit"); + + info( + "Reducing the global limit to get the storage pressuse event while the" + + " temporary storage is being initialized" + ); + + setGlobalLimit(globalLimitKB - 1); + + request = reset(); + await requestFinished(request); + + info("Initializing"); + + request = init(); + await requestFinished(request); + + promiseStoragePressure = awaitStoragePressure(); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Checking the storage pressure event"); + + usage = await promiseStoragePressure; + ok(usage == globalLimitKB * 1024, "Got correct usage"); + + info("Resetting limits"); + + resetGlobalLimit(); + + request = reset(); + await requestFinished(request); +} diff --git a/dom/quota/test/xpcshell/test_tempMetadataCleanup.js b/dom/quota/test/xpcshell/test_tempMetadataCleanup.js new file mode 100644 index 0000000000..981e1a14d3 --- /dev/null +++ b/dom/quota/test/xpcshell/test_tempMetadataCleanup.js @@ -0,0 +1,45 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function* testSteps() { + const tempMetadataFiles = [ + "storage/permanent/chrome/.metadata-tmp", + "storage/permanent/chrome/.metadata-v2-tmp", + ]; + + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Installing package"); + + installPackage("tempMetadataCleanup_profile"); + + info("Initializing"); + + let request = init(continueToNextStepSync); + yield undefined; + + info("Initializing origin"); + + request = initPersistentOrigin(getCurrentPrincipal(), continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + ok(!request.result, "Origin directory wasn't created"); + + for (let tempMetadataFile of tempMetadataFiles) { + info("Checking temp metadata file"); + + let file = getRelativeFile(tempMetadataFile); + + let exists = file.exists(); + ok(!exists, "Temp metadata file doesn't exist"); + } + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/test_unaccessedOrigins.js b/dom/quota/test/xpcshell/test_unaccessedOrigins.js new file mode 100644 index 0000000000..93b38501f4 --- /dev/null +++ b/dom/quota/test/xpcshell/test_unaccessedOrigins.js @@ -0,0 +1,168 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const SEC_PER_MONTH = 60 * 60 * 24 * 30; + +async function testSteps() { + function getHostname(index) { + return "www.example" + index + ".com"; + } + + function getOrigin(index) { + return "https://" + getHostname(index); + } + + function getOriginDir(index) { + return getRelativeFile("storage/default/https+++" + getHostname(index)); + } + + function updateOriginLastAccessTime(index, deltaSec) { + let originDir = getOriginDir(index); + + let metadataFile = originDir.clone(); + metadataFile.append(".metadata-v2"); + + let fileRandomAccessStream = Cc[ + "@mozilla.org/network/file-random-access-stream;1" + ].createInstance(Ci.nsIFileRandomAccessStream); + fileRandomAccessStream.init(metadataFile, -1, -1, 0); + + let binaryInputStream = Cc[ + "@mozilla.org/binaryinputstream;1" + ].createInstance(Ci.nsIBinaryInputStream); + binaryInputStream.setInputStream(fileRandomAccessStream); + + let lastAccessTime = binaryInputStream.read64(); + + let seekableStream = fileRandomAccessStream.QueryInterface( + Ci.nsISeekableStream + ); + seekableStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + + binaryOutputStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + binaryOutputStream.setOutputStream(fileRandomAccessStream); + + binaryOutputStream.write64(lastAccessTime + deltaSec * PR_USEC_PER_SEC); + + binaryOutputStream.close(); + + binaryInputStream.close(); + } + + function verifyOriginDir(index, shouldExist) { + let originDir = getOriginDir(index); + let exists = originDir.exists(); + if (shouldExist) { + ok(exists, "Origin directory does exist"); + } else { + ok(!exists, "Origin directory doesn't exist"); + } + } + + info("Setting prefs"); + + Services.prefs.setBoolPref("dom.quotaManager.loadQuotaFromCache", false); + Services.prefs.setBoolPref("dom.quotaManager.checkQuotaInfoLoadTime", true); + Services.prefs.setIntPref( + "dom.quotaManager.longQuotaInfoLoadTimeThresholdMs", + 0 + ); + + info("Initializing"); + + request = init(); + await requestFinished(request); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Initializing origins"); + + for (let index = 0; index < 30; index++) { + request = initTemporaryOrigin("default", getPrincipal(getOrigin(index))); + await requestFinished(request); + } + + info("Updating last access time of selected origins"); + + for (let index = 0; index < 10; index++) { + updateOriginLastAccessTime(index, -14 * SEC_PER_MONTH); + } + + for (let index = 10; index < 20; index++) { + updateOriginLastAccessTime(index, -7 * SEC_PER_MONTH); + } + + info("Resetting"); + + request = reset(); + await requestFinished(request); + + info("Setting pref"); + + Services.prefs.setIntPref( + "dom.quotaManager.unaccessedForLongTimeThresholdSec", + 13 * SEC_PER_MONTH + ); + + info("Initializing"); + + request = init(); + await requestFinished(request); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Verifying origin directories"); + + for (let index = 0; index < 10; index++) { + verifyOriginDir(index, false); + } + for (let index = 10; index < 30; index++) { + verifyOriginDir(index, true); + } + + info("Resetting"); + + request = reset(); + await requestFinished(request); + + info("Setting pref"); + + Services.prefs.setIntPref( + "dom.quotaManager.unaccessedForLongTimeThresholdSec", + 6 * SEC_PER_MONTH + ); + + info("Initializing"); + + request = init(); + await requestFinished(request); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Verifying origin directories"); + + for (let index = 0; index < 20; index++) { + verifyOriginDir(index, false); + } + for (let index = 20; index < 30; index++) { + verifyOriginDir(index, true); + } + + info("Resetting"); + + request = reset(); + await requestFinished(request); +} diff --git a/dom/quota/test/xpcshell/test_unknownFiles.js b/dom/quota/test/xpcshell/test_unknownFiles.js new file mode 100644 index 0000000000..9ddfbed56b --- /dev/null +++ b/dom/quota/test/xpcshell/test_unknownFiles.js @@ -0,0 +1,106 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that init, initTemporaryStorage, + * getUsageForPrincipal and clearStoragesForPrincipal are able to ignore + * unknown files and directories in the storage/default directory and its + * subdirectories. + */ +async function testSteps() { + const principal = getPrincipal("http://example.com"); + + async function testFunctionality(testFunction) { + const modes = [ + { + initializedStorage: false, + initializedTemporaryStorage: false, + }, + { + initializedStorage: true, + initializedTemporaryStorage: false, + }, + { + initializedStorage: true, + initializedTemporaryStorage: true, + }, + ]; + + for (const mode of modes) { + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains unknown files and unknown directories placed + // across the repositories, origin directories and client directories. + // The file make_unknownFiles.js was run locally, specifically it was + // temporarily enabled in xpcshell.ini and then executed: + // mach test --interactive dom/quota/test/xpcshell/make_unknownFiles.js + installPackage("unknownFiles_profile"); + + if (mode.initializedStorage) { + info("Initializing storage"); + + request = init(); + await requestFinished(request); + } + + if (mode.initializedTemporaryStorage) { + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + } + + info("Verifying initialization status"); + + await verifyInitializationStatus( + mode.initializedStorage, + mode.initializedTemporaryStorage + ); + + await testFunction( + mode.initializedStorage, + mode.initializedTemporaryStorage + ); + + info("Clearing"); + + request = clear(); + await requestFinished(request); + } + } + + // init and initTemporaryStorage functionality is tested in the + // testFunctionality function as part of the multi mode testing + + info("Testing getUsageForPrincipal functionality"); + + await testFunctionality(async function() { + info("Getting origin usage"); + + request = getOriginUsage(principal); + await requestFinished(request); + + ok( + request.result instanceof Ci.nsIQuotaOriginUsageResult, + "The result is nsIQuotaOriginUsageResult instance" + ); + is(request.result.usage, 115025, "Correct total usage"); + is(request.result.fileUsage, 200, "Correct file usage"); + }); + + info("Testing clearStoragesForPrincipal functionality"); + + await testFunctionality(async function() { + info("Clearing origin"); + + request = clearOrigin(principal, "default"); + await requestFinished(request); + }); +} diff --git a/dom/quota/test/xpcshell/test_unsetLastAccessTime.js b/dom/quota/test/xpcshell/test_unsetLastAccessTime.js new file mode 100644 index 0000000000..96155af397 --- /dev/null +++ b/dom/quota/test/xpcshell/test_unsetLastAccessTime.js @@ -0,0 +1,68 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() { + const metadataFile = getRelativeFile( + "storage/default/https+++foo.example.com/.metadata-v2" + ); + + function getLastAccessTime() { + let fileInputStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + + fileInputStream.init(metadataFile, -1, -1, 0); + + let binaryInputStream = Cc[ + "@mozilla.org/binaryinputstream;1" + ].createInstance(Ci.nsIBinaryInputStream); + + binaryInputStream.setInputStream(fileInputStream); + + let lastAccessTime = BigInt.asIntN(64, BigInt(binaryInputStream.read64())); + + binaryInputStream.close(); + + return lastAccessTime; + } + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains one initialized origin directory and the storage + // database: + // - storage/default/https+++foo.example.com + // - storage.sqlite + // The file make_unsetLastAccessTime.js was run locally, specifically it was + // temporarily enabled in xpcshell.ini and then executed: + // mach test --interactive dom/quota/test/xpcshell/make_unsetLastAccessTime.js + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Remove the folder "storage/temporary". + // 2. Remove the file "storage/ls-archive.sqlite". + installPackage("unsetLastAccessTime_profile"); + + info("Verifying last access time"); + + ok(getLastAccessTime() == INT64_MIN, "Correct last access time"); + + info("Initializing"); + + request = init(); + await requestFinished(request); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Verifying last access time"); + + ok(getLastAccessTime() != INT64_MIN, "Correct last access time"); +} diff --git a/dom/quota/test/xpcshell/test_validOrigins.js b/dom/quota/test/xpcshell/test_validOrigins.js new file mode 100644 index 0000000000..4c758d6bf5 --- /dev/null +++ b/dom/quota/test/xpcshell/test_validOrigins.js @@ -0,0 +1,99 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Use initOrigin to test the operation of the origin parser on a list of URLs +// we should support. If the origin doesn't parse, then initOrigin will throw an +// exception (and potentially MOZ_ASSERT under debug builds). Handling of +// obsolete or invalid origins is handled in other test files. +async function testSteps() { + const basePath = "storage/default/"; + const longExampleOriginSubstring = "a".repeat( + 255 - "https://example..com".length + ); + const origins = [ + // General + { + dirName: "https+++example.com", + url: "https://example.com", + }, + { + dirName: "https+++smaug----.github.io", + url: "https://smaug----.github.io/", + }, + // About + { + dirName: "about+home", + url: "about:home", + }, + { + dirName: "about+reader", + url: "about:reader", + }, + // IPv6 + { + dirName: "https+++[++]", + url: "https://[::]", + }, + { + dirName: "https+++[ffff+ffff+ffff+ffff+ffff+ffff+ffff+ffff]", + url: "https://[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]", + }, + { + dirName: "http+++[2010+836b+4179++836b+4179]", + url: "http://[2010:836B:4179::836B:4179]:80", + }, + { + dirName: "https+++[++ffff+8190+3426]", + url: "https://[::FFFF:129.144.52.38]", + }, + // MAX_PATH on Windows (260); storage/default/https+++example.{a....a}.com + // should have already exceeded the MAX_PATH limitation on Windows. + // There is a limitation (255) for each component on Windows so that we can + // only let the component be 255 chars and expect the wwhole path to be + // greater then 260. + { + dirName: `https+++example.${longExampleOriginSubstring}.com`, + url: `https://example.${longExampleOriginSubstring}.com`, + }, + // EndingWithPeriod + { + dirName: "https+++example.com.", + url: "https://example.com.", + }, + ]; + + info("Initializing"); + + let request = init(); + await requestFinished(request); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + for (let origin of origins) { + info(`Testing ${origin.url}`); + + try { + request = initTemporaryOrigin("default", getPrincipal(origin.url)); + await requestFinished(request); + + ok(true, "Should not have thrown"); + } catch (ex) { + ok(false, "Should not have thrown"); + } + + let dir = getRelativeFile(basePath + origin.dirName); + ok(dir.exists(), "Origin was created"); + ok( + origin.dirName === dir.leafName, + `Origin ${origin.dirName} was created expectedly` + ); + } + + request = clear(); + await requestFinished(request); +} diff --git a/dom/quota/test/xpcshell/unknownFiles_profile.zip b/dom/quota/test/xpcshell/unknownFiles_profile.zip Binary files differnew file mode 100644 index 0000000000..96d636780d --- /dev/null +++ b/dom/quota/test/xpcshell/unknownFiles_profile.zip diff --git a/dom/quota/test/xpcshell/unsetLastAccessTime_profile.zip b/dom/quota/test/xpcshell/unsetLastAccessTime_profile.zip Binary files differnew file mode 100644 index 0000000000..2b14ca7276 --- /dev/null +++ b/dom/quota/test/xpcshell/unsetLastAccessTime_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.json b/dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.json new file mode 100644 index 0000000000..eff5918e98 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.json @@ -0,0 +1,64 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "https+++www.mozilla.org^userContextId=1", + "dir": true, + "entries": [ + { + "name": "sdb", + "dir": true, + "entries": [{ "name": "data.sdb", "dir": false }] + }, + { "name": ".metadata-v2", "dir":false } + ] + } + ] + } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "https+++www.mozilla.org^userContextId=1", + "dir": true, + "entries": [ + { + "name": "sdb", + "dir": true, + "entries": [{ "name": "data.sdb", "dir": false }] + }, + { "name": ".metadata-v2", "dir":false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.zip b/dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.zip Binary files differnew file mode 100644 index 0000000000..c09b503c18 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/head.js b/dom/quota/test/xpcshell/upgrades/head.js new file mode 100644 index 0000000000..5c36d82ca6 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/head.js @@ -0,0 +1,14 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// The path to the top level directory. +const depth = "../../../../../"; + +loadScript("dom/quota/test/xpcshell/common/head.js"); + +function loadScript(path) { + let uri = Services.io.newFileURI(do_get_file(depth + path)); + Services.scriptloader.loadSubScript(uri.spec); +} diff --git a/dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.json b/dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.json new file mode 100644 index 0000000000..db66d824eb --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.json @@ -0,0 +1,63 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "indexedDB", + "dir": true, + "entries": [ + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + } + ] + }, + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "persistent", + "dir": true, + "entries": [ + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + } + ] + } + ] + } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.zip b/dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.zip Binary files differnew file mode 100644 index 0000000000..63936ecf9a --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.json b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.json new file mode 100644 index 0000000000..7916c25b73 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.json @@ -0,0 +1,55 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "indexedDB", + "dir": true, + "entries": [ + { "name": "1007+f+app+++system.gaiamobile.org", "dir": true }, + { "name": "http+++www.mozilla.org", "dir": true }, + { "name": "1007+t+https+++developer.cdn.mozilla.net", "dir": true } + ] + } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": "idb", "dir": true }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404", + "name": "https+++developer.cdn.mozilla.net", + "dir": true, + "entries": [ + { "name": "idb", "dir": true }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.zip b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.zip Binary files differnew file mode 100644 index 0000000000..a0a56a77df --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.json b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.json new file mode 100644 index 0000000000..715c954915 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.json @@ -0,0 +1,65 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "indexedDB", + "dir": true, + "entries": [ + { + "name": "1007+f+app+++system.gaiamobile.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "1007+t+https+++developer.cdn.mozilla.net", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + } + ] + } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404", + "name": "https+++developer.cdn.mozilla.net", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.zip b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.zip Binary files differnew file mode 100644 index 0000000000..589e65ec82 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/localStorageArchive1upgrade_profile.zip b/dom/quota/test/xpcshell/upgrades/localStorageArchive1upgrade_profile.zip Binary files differnew file mode 100644 index 0000000000..a17b90dedf --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/localStorageArchive1upgrade_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/localStorageArchive4upgrade_profile.zip b/dom/quota/test/xpcshell/upgrades/localStorageArchive4upgrade_profile.zip Binary files differnew file mode 100644 index 0000000000..cf5b29adae --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/localStorageArchive4upgrade_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/localStorageArchiveDowngrade_profile.zip b/dom/quota/test/xpcshell/upgrades/localStorageArchiveDowngrade_profile.zip Binary files differnew file mode 100644 index 0000000000..2f11abe858 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/localStorageArchiveDowngrade_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.json b/dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.json new file mode 100644 index 0000000000..a25f257573 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.json @@ -0,0 +1,63 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + } + ] + }, + { + "name": "persistent", + "dir": true, + "entries": [ + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + } + ] + } + ] + } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.zip b/dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.zip Binary files differnew file mode 100644 index 0000000000..9ddd9af6a9 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.json b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.json new file mode 100644 index 0000000000..c80a3cc283 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.json @@ -0,0 +1,64 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "persistent", + "dir": true, + "entries": [ + { "name": "1007+f+app+++system.gaiamobile.org", "dir": true }, + { + "name": "1007+t+https+++developer.cdn.mozilla.net", + "dir": true + }, + { "name": "http+++www.mozilla.org", "dir": true } + ] + } + ] + } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": "idb", "dir": true }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404", + "name": "https+++developer.cdn.mozilla.net", + "dir": true, + "entries": [ + { "name": "idb", "dir": true }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.zip b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.zip Binary files differnew file mode 100644 index 0000000000..436ebf9070 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.json b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.json new file mode 100644 index 0000000000..98d23af161 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.json @@ -0,0 +1,92 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "persistent", + "dir": true, + "entries": [ + { + "name": "1007+f+app+++system.gaiamobile.org", + "dir": true, + "entries": [ + { "name": "idb", "dir": true }, + { "name": ".metadata", "dir": false } + ] + }, + { + "name": "1007+t+https+++developer.cdn.mozilla.net", + "dir": true, + "entries": [ + { "name": "idb", "dir": true }, + { "name": ".metadata", "dir": false } + ] + }, + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": "idb", "dir": true }, + { "name": ".metadata", "dir": false } + ] + }, + { "name": "http+++www.mozilla.org+8080", "dir": true } + ] + } + ] + } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": "idb", "dir": true }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "http+++www.mozilla.org+8080", + "dir": true, + "entries": [ + { "name": "idb", "dir": true }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404", + "name": "https+++developer.cdn.mozilla.net", + "dir": true, + "entries": [ + { "name": "idb", "dir": true }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.zip b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.zip Binary files differnew file mode 100644 index 0000000000..0c5200adf4 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.json b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.json new file mode 100644 index 0000000000..f5d6a4a749 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.json @@ -0,0 +1,382 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "persistent", + "dir": true, + "entries": [ + { + "name": "1007+f+app+++system.gaiamobile.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "file++++Users+joe+c+++index.html", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "moz-safe-about+home", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "https+++www.mozilla.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "file++++", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "file++++Users+joe+index.html", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "1007+t+https+++developer.cdn.mozilla.net", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "http+++localhost", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "file++++c++", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "file++++c++Users+joe+index.html", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "file++++c++Users+joe+", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "chrome", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "http+++127.0.0.1", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "file++++++index.html", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "http+++www.mozilla.org+8080", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "moz-safe-about+++home", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "resource+++fx-share-addon-at-mozilla-dot-org-fx-share-addon-data", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "indexeddb+++fx-devtools", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "https+++www.mozilla.org+8080", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "file++++Users+joe+", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + } + ] + }, + { + "name": "temporary", + "dir": true, + "entries": [ + { + "name": "1007+f+app+++system.gaiamobile.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "1007+t+https+++developer.cdn.mozilla.net", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "http+++localhost", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "http+++localhost+82", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "chrome", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { "name": "http+++localhost+81", "dir": true } + ] + } + ] + } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "file++++Users+joe+c+++index.html", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "https+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "file++++", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "file++++Users+joe+index.html", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "http+++localhost", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "file++++c++", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404", + "name": "https+++developer.cdn.mozilla.net", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "file++++c++Users+joe+index.html", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "file++++c++Users+joe+", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "http+++127.0.0.1", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "file++++++index.html", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "http+++www.mozilla.org+8080", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "https+++www.mozilla.org+8080", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "file++++Users+joe+", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { + "name": "temporary", + "dir": true, + "entries": [ + { + "name": "http+++localhost", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "http+++localhost+82", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404", + "name": "https+++developer.cdn.mozilla.net", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "chrome", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "http+++localhost+81", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { + "name": "permanent", + "dir": true, + "entries": [ + { + "name": "moz-safe-about+home", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "chrome", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "resource+++fx-share-addon-at-mozilla-dot-org-fx-share-addon-data", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "indexeddb+++fx-devtools", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.zip b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.zip Binary files differnew file mode 100644 index 0000000000..ab8c045be9 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/test_localStorageArchive1upgrade.js b/dom/quota/test/xpcshell/upgrades/test_localStorageArchive1upgrade.js new file mode 100644 index 0000000000..f697d9167e --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_localStorageArchive1upgrade.js @@ -0,0 +1,65 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that local storage directories are removed + * during local storage archive upgrade from version 0 to version 1. + * See bug 1546305. + */ + +async function testSteps() { + const lsDirs = [ + "storage/default/http+++example.com/ls", + "storage/default/http+++localhost/ls", + "storage/default/http+++www.mozilla.org/ls", + ]; + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains three initialized origin directories with local + // storage data, local storage archive, a script for origin initialization, + // the storage database and the web apps store database: + // - storage/default/https+++example.com + // - storage/default/https+++localhost + // - storage/default/https+++www.mozilla.org + // - 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 that + // doesn't support local storage archive upgrades), specifically it was + // temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/xpcshell/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("localStorageArchive1upgrade_profile"); + + info("Checking ls dirs"); + + for (let lsDir of lsDirs) { + let dir = getRelativeFile(lsDir); + + exists = dir.exists(); + ok(exists, "ls directory does exist"); + } + + request = init(); + request = await requestFinished(request); + + info("Checking ls dirs"); + + for (let lsDir of lsDirs) { + let dir = getRelativeFile(lsDir); + + exists = dir.exists(); + ok(!exists, "ls directory doesn't exist"); + } +} diff --git a/dom/quota/test/xpcshell/upgrades/test_localStorageArchive4upgrade.js b/dom/quota/test/xpcshell/upgrades/test_localStorageArchive4upgrade.js new file mode 100644 index 0000000000..0b0fcfccd6 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_localStorageArchive4upgrade.js @@ -0,0 +1,107 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that local storage directories are removed + * during local storage archive upgrade from version 3 to version 4. + * See bug 1549654. + */ + +async function testSteps() { + const lsDirs = [ + "storage/default/http+++localhost/ls", + "storage/default/http+++www.mozilla.org/ls", + "storage/default/http+++example.com/ls", + ]; + + const principalInfos = [ + "http://localhost", + "http://www.mozilla.org", + "http://example.com", + ]; + + const data = [ + { key: "foo0", value: "bar" }, + { key: "foo1", value: "A" }, + { key: "foo2", value: "A".repeat(100) }, + ]; + + function getLocalStorage(principal) { + return Services.domStorageManager.createStorage( + null, + principal, + principal, + "" + ); + } + + info("Setting pref"); + + // xpcshell globals don't have associated clients in the Clients API sense, so + // we need to disable client validation so that this xpcshell test is allowed + // to use LocalStorage. + Services.prefs.setBoolPref("dom.storage.client_validation", false); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains three initialized origin directories with local + // storage data, local storage archive, a script for origin initialization, + // the storage database and the web apps store database: + // - storage/default/https+++example.com + // - storage/default/https+++localhost + // - storage/default/https+++www.mozilla.org + // - 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 3), specifically it was temporarily added to + // xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/xpcshell/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("localStorageArchive4upgrade_profile"); + + info("Checking ls dirs"); + + for (let lsDir of lsDirs) { + let dir = getRelativeFile(lsDir); + + exists = dir.exists(); + ok(exists, "ls directory does exist"); + } + + request = init(); + request = await requestFinished(request); + + info("Checking ls dirs"); + + for (let lsDir of lsDirs) { + let dir = getRelativeFile(lsDir); + + exists = dir.exists(); + ok(!exists, "ls directory doesn't exist"); + } + + info("Getting storages"); + + let storages = []; + for (let i = 0; i < principalInfos.length; i++) { + let storage = getLocalStorage(getPrincipal(principalInfos[i])); + storages.push(storage); + } + + info("Verifying data"); + + for (let i = 0; i < storages.length; i++) { + is(storages[i].getItem(data[i].key), data[i].value, "Correct value"); + } +} diff --git a/dom/quota/test/xpcshell/upgrades/test_localStorageArchiveDowngrade.js b/dom/quota/test/xpcshell/upgrades/test_localStorageArchiveDowngrade.js new file mode 100644 index 0000000000..8ca46f01d5 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_localStorageArchiveDowngrade.js @@ -0,0 +1,66 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that local storage directories are removed + * during local storage archive downgrade from any future version to current + * version. See bug 1546305. + */ + +async function testSteps() { + const lsDirs = [ + "storage/default/http+++example.com/ls", + "storage/default/http+++localhost/ls", + "storage/default/http+++www.mozilla.org/ls", + ]; + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains three initialized origin directories with local + // storage data, local storage archive, a script for origin initialization, + // the storage database and the web apps store database: + // - storage/default/https+++example.com + // - storage/default/https+++localhost + // - storage/default/https+++www.mozilla.org + // - 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 that + // supports local storage archive upgrades and local storage archive version + // set to max integer), specifically it was temporarily added to xpcshell.ini + // and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/xpcshell/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("localStorageArchiveDowngrade_profile"); + + info("Checking ls dirs"); + + for (let lsDir of lsDirs) { + let dir = getRelativeFile(lsDir); + + exists = dir.exists(); + ok(exists, "ls directory does exist"); + } + + request = init(); + request = await requestFinished(request); + + info("Checking ls dirs"); + + for (let lsDir of lsDirs) { + let dir = getRelativeFile(lsDir); + + exists = dir.exists(); + ok(!exists, "ls directory doesn't exist"); + } +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeCacheFrom1.js b/dom/quota/test/xpcshell/upgrades/test_upgradeCacheFrom1.js new file mode 100644 index 0000000000..e9424e20c6 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeCacheFrom1.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 UpgradeCacheFrom1To2 method. + */ + +async function testSteps() { + const packages = [ + // Storage used prior FF 88 (cache version 1). + // The profile contains one initialized origin directory with simple + // database data, a script for origin initialization and the storage + // database: + // - storage/default/https+++www.mozilla.org^userContextId=1 + // - 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 dom/quota/test/xpcshell/upgrades/create_db.js + // --interactive + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Remove the folder "storage/temporary". + // 2. Remove the file "storage/ls-archive.sqlite". + "cacheVersion1_profile", + "../defaultStorageDirectory_shared", + ]; + const principal = getPrincipal("https://www.mozilla.org", { + userContextId: 1, + }); + const originUsage = 100; + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing package"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Initializing"); + + request = init(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + // TODO: Remove this block once temporary storage initialization is able to + // ignore unknown directories. + getRelativeFile("storage/default/invalid+++example.com").remove(false); + getRelativeFile("storage/temporary/invalid+++example.com").remove(false); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(continueToNextStepSync); + await requestFinished(request); + + info("Getting origin usage"); + + request = getOriginUsage(principal, /* fromMemory */ true); + await requestFinished(request); + + info("Verifying origin usage"); + + is(request.result.usage, originUsage, "Correct origin usage"); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeFromFlatOriginDirectories.js b/dom/quota/test/xpcshell/upgrades/test_upgradeFromFlatOriginDirectories.js new file mode 100644 index 0000000000..f33ddac8ca --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeFromFlatOriginDirectories.js @@ -0,0 +1,187 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// A flat origin directory is an origin directory with no sub directories for +// quota clients. The upgrade was initially done lazily and an empty .metadata +// file was used to indicate a successful upgrade. + +function* testSteps() { + const setups = [ + { + packages: [ + // Storage used prior FF 22 (indexedDB/ directory with flat origin + // directories). + // FF 26 renamed indexedDB/ to storage/persistent and the lazy upgrade + // of flat origin directories remained. There's a test for that below. + "indexedDBDirectory_flatOriginDirectories_profile", + "../indexedDBDirectory_shared", + ], + origins: [ + { + oldPath: "indexedDB/1007+f+app+++system.gaiamobile.org", + }, + + { + oldPath: "indexedDB/1007+t+https+++developer.cdn.mozilla.net", + }, + + { + oldPath: "indexedDB/http+++www.mozilla.org", + newPath: "storage/default/http+++www.mozilla.org", + url: "http://www.mozilla.org", + persistence: "default", + }, + ], + }, + + { + packages: [ + // Storage used by FF 26-35 (storage/persistent/ directory with not yet + // upgraded flat origin directories). + // FF 36 renamed storage/persistent/ to storage/default/ and all not + // yet upgraded flat origin directories were upgraded. There's a + // separate test for that: + // test_upgradeFromPersistentStorageDirectory_upgradeOriginDirectories. + "persistentStorageDirectory_flatOriginDirectories_profile", + "../persistentStorageDirectory_shared", + ], + origins: [ + { + oldPath: "storage/persistent/1007+f+app+++system.gaiamobile.org", + }, + + { + oldPath: + "storage/persistent/1007+t+https+++developer.cdn.mozilla.net", + }, + + { + oldPath: "storage/persistent/http+++www.mozilla.org", + newPath: "storage/default/http+++www.mozilla.org", + url: "http://www.mozilla.org", + persistence: "default", + }, + ], + }, + ]; + + const metadataFileName = ".metadata"; + + for (const setup of setups) { + clear(continueToNextStepSync); + yield undefined; + + info("Verifying storage"); + + verifyStorage(setup.packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(setup.packages); + + info("Verifying storage"); + + verifyStorage(setup.packages, "afterInstall"); + + info("Checking origin directories"); + + for (const origin of setup.origins) { + let originDir = getRelativeFile(origin.oldPath); + let exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + + let idbDir = originDir.clone(); + idbDir.append("idb"); + + exists = idbDir.exists(); + ok(!exists, "idb directory doesn't exist"); + + let metadataFile = originDir.clone(); + metadataFile.append(metadataFileName); + + exists = metadataFile.exists(); + ok(!exists, "Metadata file doesn't exist"); + + if (origin.newPath) { + originDir = getRelativeFile(origin.newPath); + exists = originDir.exists(); + ok(!exists, "Origin directory doesn't exist"); + } + } + + info("Initializing"); + + let request = init(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Verifying storage"); + + verifyStorage(setup.packages, "afterInit"); + + // TODO: Remove this block once temporary storage initialization is able to + // ignore unknown directories. + getRelativeFile("storage/default/invalid+++example.com").remove(false); + try { + getRelativeFile("storage/temporary/invalid+++example.com").remove(false); + } catch (ex) {} + + info("Checking origin directories"); + + for (const origin of setup.origins) { + let originDir = getRelativeFile(origin.oldPath); + let exists = originDir.exists(); + ok(!exists, "Origin directory doesn't exist"); + + if (origin.newPath) { + originDir = getRelativeFile(origin.newPath); + exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + + let idbDir = originDir.clone(); + idbDir.append("idb"); + + exists = idbDir.exists(); + ok(exists, "idb directory does exist"); + + let metadataFile = originDir.clone(); + metadataFile.append(metadataFileName); + + exists = metadataFile.exists(); + ok(exists, "Metadata file does exist"); + } + } + + info("Initializing temporary storage"); + + request = initTemporaryStorage(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Initializing origins"); + + for (const origin of setup.origins) { + if (origin.newPath) { + info("Initializing origin"); + + let principal = getPrincipal(origin.url); + request = initTemporaryOrigin( + origin.persistence, + principal, + continueToNextStepSync + ); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + ok(!request.result, "Origin directory wasn't created"); + } + } + } + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory.js b/dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory.js new file mode 100644 index 0000000000..b1b0966d67 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory.js @@ -0,0 +1,121 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify + * MaybeUpgradeFromIndexedDBDirectoryToPersistentStorageDirectory method. + */ + +function* testSteps() { + const origins = [ + { + oldPath: "indexedDB/http+++www.mozilla.org", + newPath: "storage/default/http+++www.mozilla.org", + url: "http://www.mozilla.org", + persistence: "default", + }, + + { + oldPath: "indexedDB/1007+f+app+++system.gaiamobile.org", + }, + + { + oldPath: "indexedDB/1007+t+https+++developer.cdn.mozilla.net", + }, + ]; + + const packages = [ + // Storage used prior FF 26 (indexedDB/ directory). + "indexedDBDirectory_profile", + "../indexedDBDirectory_shared", + ]; + + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing package"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + for (let origin of origins) { + let originDir = getRelativeFile(origin.oldPath); + let exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + + if (origin.newPath) { + originDir = getRelativeFile(origin.newPath); + exists = originDir.exists(); + ok(!exists, "Origin directory doesn't exist"); + } + } + + info("Initializing"); + + let request = init(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + // TODO: Remove this block once temporary storage initialization is able to + // ignore unknown directories. + getRelativeFile("storage/default/invalid+++example.com").remove(false); + + info("Checking origin directories"); + + for (let origin of origins) { + let originDir = getRelativeFile(origin.oldPath); + let exists = originDir.exists(); + ok(!exists, "Origin directory doesn't exist"); + + if (origin.newPath) { + originDir = getRelativeFile(origin.newPath); + exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + } + } + + info("Initializing temporary storage"); + + request = initTemporaryStorage(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Initializing origins"); + + for (const origin of origins) { + if (origin.newPath) { + info("Initializing origin"); + + let principal = getPrincipal(origin.url); + request = initTemporaryOrigin( + origin.persistence, + principal, + continueToNextStepSync + ); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + ok(!request.result, "Origin directory wasn't created"); + } + } + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory_removeOldDirectory.js b/dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory_removeOldDirectory.js new file mode 100644 index 0000000000..a7beced885 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory_removeOldDirectory.js @@ -0,0 +1,86 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that the old directory is removed in + * MaybeUpgradeFromIndexedDBDirectoryToPersistentStorageDirectory method. + */ + +async function testSteps() { + const url = "http://www.mozilla.org"; + const persistence = "default"; + + const packages = [ + // Storage used by FF 26-35 (storage/persistent/ directory and re-created + // indexedDB directory by an older FF). + "indexedDBAndPersistentStorageDirectory_profile", + "../persistentStorageDirectory_shared", + ]; + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Checking directories"); + + let indexedDBDir = getRelativeFile("indexedDB"); + let exists = indexedDBDir.exists(); + ok(exists, "IndexedDB directory does exist"); + + let persistentStorageDir = getRelativeFile("storage/persistent"); + exists = persistentStorageDir.exists(); + ok(exists, "Persistent storage directory does exist"); + + info("Initializing"); + + request = init(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + // TODO: Remove this block once temporary storage initialization is able to + // ignore unknown directories. + getRelativeFile("storage/default/invalid+++example.com").remove(false); + getRelativeFile("storage/temporary/invalid+++example.com").remove(false); + + info("Checking directories"); + + indexedDBDir = getRelativeFile("indexedDB"); + exists = indexedDBDir.exists(); + ok(!exists, "IndexedDB directory doesn't exist"); + + // FF 36 renamed storage/persistent/ to storage/default/ so it can't exist + // either. + persistentStorageDir = getRelativeFile("storage/persistent"); + exists = persistentStorageDir.exists(); + ok(!exists, "Persistent storage directory doesn't exist"); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Initializing origin"); + + request = initTemporaryOrigin(persistence, getPrincipal(url)); + await requestFinished(request); + + ok(!request.result, "Origin directory wasn't created"); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory.js b/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory.js new file mode 100644 index 0000000000..efac150067 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory.js @@ -0,0 +1,378 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify + * MaybeUpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory method. + */ + +loadScript("dom/quota/test/common/file.js"); + +function* testSteps() { + const origins = [ + { + oldPath: "storage/persistent/1007+f+app+++system.gaiamobile.org", + }, + + { + oldPath: "storage/persistent/1007+t+https+++developer.cdn.mozilla.net", + }, + + { + oldPath: "storage/persistent/chrome", + newPath: "storage/permanent/chrome", + chrome: true, + persistence: "persistent", + }, + + { + oldPath: "storage/persistent/file++++", + newPath: "storage/default/file++++", + url: "file:///", + persistence: "default", + }, + + { + oldPath: "storage/persistent/file++++++index.html", + newPath: "storage/default/file++++++index.html", + url: "file:///+/index.html", + persistence: "default", + }, + + { + oldPath: "storage/persistent/file++++++index.html", + newPath: "storage/default/file++++++index.html", + url: "file://///index.html", + persistence: "default", + }, + + { + oldPath: "storage/persistent/file++++Users+joe+", + newPath: "storage/default/file++++Users+joe+", + url: "file:///Users/joe/", + persistence: "default", + }, + + { + oldPath: "storage/persistent/file++++Users+joe+c+++index.html", + newPath: "storage/default/file++++Users+joe+c+++index.html", + url: "file:///Users/joe/c++/index.html", + persistence: "default", + }, + + { + oldPath: "storage/persistent/file++++Users+joe+c+++index.html", + newPath: "storage/default/file++++Users+joe+c+++index.html", + url: "file:///Users/joe/c///index.html", + persistence: "default", + }, + + { + oldPath: "storage/persistent/file++++Users+joe+index.html", + newPath: "storage/default/file++++Users+joe+index.html", + url: "file:///Users/joe/index.html", + persistence: "default", + }, + + { + oldPath: "storage/persistent/file++++c++", + newPath: "storage/default/file++++c++", + url: "file:///c:/", + persistence: "default", + }, + + { + oldPath: "storage/persistent/file++++c++Users+joe+", + newPath: "storage/default/file++++c++Users+joe+", + url: "file:///c:/Users/joe/", + persistence: "default", + }, + + { + oldPath: "storage/persistent/file++++c++Users+joe+index.html", + newPath: "storage/default/file++++c++Users+joe+index.html", + url: "file:///c:/Users/joe/index.html", + persistence: "default", + }, + + { + oldPath: "storage/persistent/http+++127.0.0.1", + newPath: "storage/default/http+++127.0.0.1", + url: "http://127.0.0.1", + persistence: "default", + }, + + { + oldPath: "storage/persistent/http+++localhost", + newPath: "storage/default/http+++localhost", + url: "http://localhost", + persistence: "default", + }, + + { + oldPath: "storage/persistent/http+++www.mozilla.org", + newPath: "storage/default/http+++www.mozilla.org", + url: "http://www.mozilla.org", + persistence: "default", + }, + + { + oldPath: "storage/persistent/http+++www.mozilla.org+8080", + newPath: "storage/default/http+++www.mozilla.org+8080", + url: "http://www.mozilla.org:8080", + persistence: "default", + }, + + { + oldPath: "storage/persistent/https+++www.mozilla.org", + newPath: "storage/default/https+++www.mozilla.org", + url: "https://www.mozilla.org", + persistence: "default", + }, + + { + oldPath: "storage/persistent/https+++www.mozilla.org+8080", + newPath: "storage/default/https+++www.mozilla.org+8080", + url: "https://www.mozilla.org:8080", + persistence: "default", + }, + + { + oldPath: "storage/persistent/indexeddb+++fx-devtools", + newPath: "storage/permanent/indexeddb+++fx-devtools", + url: "indexeddb://fx-devtools", + persistence: "persistent", + }, + + { + oldPath: "storage/persistent/moz-safe-about+++home", + }, + + { + oldPath: "storage/persistent/moz-safe-about+home", + newPath: "storage/permanent/moz-safe-about+home", + url: "moz-safe-about:home", + persistence: "persistent", + }, + + { + oldPath: + "storage/persistent/resource+++fx-share-addon-at-mozilla-dot-org-fx-share-addon-data", + newPath: + "storage/permanent/resource+++fx-share-addon-at-mozilla-dot-org-fx-share-addon-data", + url: "resource://fx-share-addon-at-mozilla-dot-org-fx-share-addon-data", + persistence: "persistent", + }, + + { + oldPath: "storage/temporary/1007+f+app+++system.gaiamobile.org", + }, + + { + oldPath: "storage/temporary/1007+t+https+++developer.cdn.mozilla.net", + }, + + // The .metadata file was intentionally appended for this origin directory + // to test recovery from unfinished upgrades (some metadata files can be + // already upgraded). + { + oldPath: "storage/temporary/chrome", + newPath: "storage/temporary/chrome", + metadataUpgraded: true, + chrome: true, + persistence: "temporary", + }, + + { + oldPath: "storage/temporary/http+++localhost", + newPath: "storage/temporary/http+++localhost", + url: "http://localhost", + persistence: "temporary", + }, + + // The .metadata file was intentionally removed for this origin directory + // to test restoring during upgrade. + { + oldPath: "storage/temporary/http+++localhost+81", + newPath: "storage/temporary/http+++localhost+81", + metadataRemoved: true, + url: "http://localhost:81", + persistence: "temporary", + }, + + // The .metadata file was intentionally truncated for this origin directory + // to test restoring during upgrade. + { + oldPath: "storage/temporary/http+++localhost+82", + newPath: "storage/temporary/http+++localhost+82", + url: "http://localhost:82", + persistence: "temporary", + }, + ]; + + const metadataFileName = ".metadata"; + + const packages = [ + // Storage used by 26-35 (storage/persistent/ directory, tracked only + // timestamp in .metadata for persistent storage and isApp not tracked in + // .metadata for temporary storage). + "persistentStorageDirectory_profile", + "../persistentStorageDirectory_shared", + ]; + + let metadataBuffers = []; + + clear(continueToNextStepSync); + yield undefined; + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Checking origin directories"); + + for (let origin of origins) { + let originDir = getRelativeFile(origin.oldPath); + let exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + + if (origin.newPath) { + info("Reading out contents of metadata file"); + + let metadataFile = originDir.clone(); + metadataFile.append(metadataFileName); + + if (origin.metadataRemoved) { + metadataBuffers.push(new ArrayBuffer(0)); + } else { + File.createFromNsIFile(metadataFile).then(grabArgAndContinueHandler); + let file = yield undefined; + + let fileReader = new FileReader(); + fileReader.onload = continueToNextStepSync; + fileReader.readAsArrayBuffer(file); + + yield undefined; + + metadataBuffers.push(fileReader.result); + } + + if (origin.newPath != origin.oldPath) { + originDir = getRelativeFile(origin.newPath); + exists = originDir.exists(); + ok(!exists, "Origin directory doesn't exist"); + } + } + } + + info("Initializing"); + + let request = init(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + // TODO: Remove this block once temporary storage initialization is able to + // ignore unknown directories. + getRelativeFile("storage/default/invalid+++example.com").remove(false); + getRelativeFile("storage/temporary/invalid+++example.com").remove(false); + + info("Checking origin directories"); + + for (let origin of origins) { + if (!origin.newPath || origin.newPath != origin.oldPath) { + let originDir = getRelativeFile(origin.oldPath); + let exists = originDir.exists(); + ok(!exists, "Origin directory doesn't exist"); + } + + if (origin.newPath) { + let originDir = getRelativeFile(origin.newPath); + let exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + + info("Reading out contents of metadata file"); + + let metadataFile = originDir.clone(); + metadataFile.append(metadataFileName); + + File.createFromNsIFile(metadataFile).then(grabArgAndContinueHandler); + let file = yield undefined; + + let fileReader = new FileReader(); + fileReader.onload = continueToNextStepSync; + fileReader.readAsArrayBuffer(file); + + yield undefined; + + let metadataBuffer = fileReader.result; + + info("Verifying blobs differ"); + + if (origin.metadataUpgraded) { + ok( + compareBuffers(metadataBuffer, metadataBuffers.shift()), + "Metadata doesn't differ" + ); + } else { + ok( + !compareBuffers(metadataBuffer, metadataBuffers.shift()), + "Metadata differ" + ); + } + } + } + + info("Initializing"); + + request = initTemporaryStorage(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Initializing origins"); + + for (const origin of origins) { + if (origin.newPath) { + info("Initializing origin"); + + let principal; + if (origin.chrome) { + principal = getCurrentPrincipal(); + } else { + principal = getPrincipal(origin.url); + } + + if (origin.persistence == "persistent") { + request = initPersistentOrigin(principal, continueToNextStepSync); + } else { + request = initTemporaryOrigin( + origin.persistence, + principal, + continueToNextStepSync + ); + } + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + ok(!request.result, "Origin directory wasn't created"); + } + } + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_removeOldDirectory.js b/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_removeOldDirectory.js new file mode 100644 index 0000000000..e9f1ae2291 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_removeOldDirectory.js @@ -0,0 +1,102 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify that the old directory is removed in + * MaybeUpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory method. + */ + +async function testSteps() { + const url = "http://www.mozilla.org"; + const persistence = "default"; + const lastAccessed = 0x0005330925e07841; + + const packages = [ + // Storage used by FF 36-48 (storage/default/ directory and re-created + // storage/persistent/ directory by an older FF). + "persistentAndDefaultStorageDirectory_profile", + "../defaultStorageDirectory_shared", + ]; + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Checking directories"); + + let persistentStorageDir = getRelativeFile("storage/persistent"); + let exists = persistentStorageDir.exists(); + ok(exists, "Persistent storage directory does exist"); + + let defaultStorageDir = getRelativeFile("storage/default"); + exists = defaultStorageDir.exists(); + ok(exists, "Default storage directory does exist"); + + info("Initializing"); + + request = init(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + // TODO: Remove this block once temporary storage initialization and getting + // usage is able to ignore unknown directories. + getRelativeFile("storage/default/invalid+++example.com").remove(false); + getRelativeFile("storage/permanent/invalid+++example.com").remove(false); + getRelativeFile("storage/temporary/invalid+++example.com").remove(false); + + info("Checking directories"); + + persistentStorageDir = getRelativeFile("storage/persistent"); + exists = persistentStorageDir.exists(); + ok(!exists, "Persistent storage directory doesn't exist"); + + defaultStorageDir = getRelativeFile("storage/default"); + exists = defaultStorageDir.exists(); + ok(exists, "Default storage directory does exist"); + + info("Initializing temporary storage"); + + request = initTemporaryStorage(); + await requestFinished(request); + + info("Initializing origin"); + + request = initTemporaryOrigin(persistence, getPrincipal(url)); + await requestFinished(request); + + ok(!request.result, "Origin directory wasn't created"); + + info("Getting usage"); + + request = getUsage(function() {}, /* getAll */ true); + await requestFinished(request); + + info("Verifying result"); + + const result = request.result; + is(result.length, 1, "Correct number of usage results"); + + info("Verifying usage result"); + + const usageResult = result[0]; + ok(usageResult.origin == url, "Origin equals"); + ok(usageResult.lastAccessed == lastAccessed, "LastAccessed equals"); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_upgradeOriginDirectories.js b/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_upgradeOriginDirectories.js new file mode 100644 index 0000000000..cac8ec3b16 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_upgradeOriginDirectories.js @@ -0,0 +1,162 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function* testSteps() { + const origins = [ + { + oldPath: "storage/persistent/1007+f+app+++system.gaiamobile.org", + upgraded: true, + }, + + { + oldPath: "storage/persistent/1007+t+https+++developer.cdn.mozilla.net", + upgraded: true, + }, + + { + oldPath: "storage/persistent/http+++www.mozilla.org", + newPath: "storage/default/http+++www.mozilla.org", + url: "http://www.mozilla.org", + persistence: "default", + upgraded: true, + }, + { + oldPath: "storage/persistent/http+++www.mozilla.org+8080", + newPath: "storage/default/http+++www.mozilla.org+8080", + url: "http://www.mozilla.org:8080", + persistence: "default", + }, + ]; + + const metadataFileName = ".metadata"; + + const packages = [ + // Storage used by FF 26-35 (storage/persistent/ directory with already + // upgraded origin directories and not yet upgraded flat origin + // directories). + "persistentStorageDirectory_originDirectories_profile", + "../persistentStorageDirectory_shared", + ]; + + clear(continueToNextStepSync); + yield undefined; + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Checking origin directories"); + + for (const origin of origins) { + let originDir = getRelativeFile(origin.oldPath); + let exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + + let idbDir = originDir.clone(); + idbDir.append("idb"); + + exists = idbDir.exists(); + if (origin.upgraded) { + ok(exists, "idb directory does exist"); + } else { + ok(!exists, "idb directory doesn't exist"); + } + + let metadataFile = originDir.clone(); + metadataFile.append(metadataFileName); + + exists = metadataFile.exists(); + if (origin.upgraded) { + ok(exists, "Metadata file does exist"); + } else { + ok(!exists, "Metadata file doesn't exist"); + } + + if (origin.newPath) { + originDir = getRelativeFile(origin.newPath); + exists = originDir.exists(); + ok(!exists, "Origin directory doesn't exist"); + } + } + + info("Initializing"); + + let request = init(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + // TODO: Remove this block once temporary storage initialization and getting + // usage is able to ignore unknown directories. + getRelativeFile("storage/default/invalid+++example.com").remove(false); + getRelativeFile("storage/temporary/invalid+++example.com").remove(false); + + info("Checking origin directories"); + + for (const origin of origins) { + let originDir = getRelativeFile(origin.oldPath); + let exists = originDir.exists(); + ok(!exists, "Origin directory doesn't exist"); + + if (origin.newPath) { + originDir = getRelativeFile(origin.newPath); + exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + + let idbDir = originDir.clone(); + idbDir.append("idb"); + + exists = idbDir.exists(); + ok(exists, "idb directory does exist"); + + let metadataFile = originDir.clone(); + metadataFile.append(metadataFileName); + + exists = metadataFile.exists(); + ok(exists, "Metadata file does exist"); + } + } + + info("Initializing temporary storage"); + + request = initTemporaryStorage(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Initializing origins"); + + for (const origin of origins) { + if (origin.newPath) { + info("Initializing origin"); + + let principal = getPrincipal(origin.url); + request = initTemporaryOrigin( + origin.persistence, + principal, + continueToNextStepSync + ); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + ok(!request.result, "Origin directory wasn't created"); + } + } + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom0_0.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom0_0.js new file mode 100644 index 0000000000..bd6a010cf8 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom0_0.js @@ -0,0 +1,158 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify UpgradeStorageFrom0_0To1_0 method. + */ + +function* testSteps() { + const origins = [ + { + path: "storage/default/1007+f+app+++system.gaiamobile.org", + obsolete: true, + }, + + { + path: "storage/default/1007+t+https+++developer.cdn.mozilla.net", + obsolete: true, + }, + + { + path: "storage/default/http+++www.mozilla.org", + obsolete: false, + url: "http://www.mozilla.org", + persistence: "default", + }, + ]; + + const storageFileName = "storage.sqlite"; + const metadataFileName = ".metadata"; + const metadata2FileName = ".metadata-v2"; + + const packages = [ + // Storage used by FF 36-48 (storage/default/ directory, but no + // storage.sqlite and no .metadata-v2 files). + "version0_0_profile", + "../defaultStorageDirectory_shared", + ]; + + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Checking storage file"); + + let storageFile = getRelativeFile(storageFileName); + + let exists = storageFile.exists(); + ok(!exists, "Storage file doesn't exist"); + + info("Checking origin directories"); + + for (let origin of origins) { + let originDir = getRelativeFile(origin.path); + + exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + + let metadataFile = originDir.clone(); + metadataFile.append(metadataFileName); + + exists = metadataFile.exists(); + ok(exists, "Metadata file does exist"); + + let metadata2File = originDir.clone(); + metadata2File.append(metadata2FileName); + + exists = metadata2File.exists(); + ok(!exists, "Metadata file doesn't exist"); + } + + info("Initializing"); + + let request = init(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + // TODO: Remove this block once temporary storage initialization is able to + // ignore unknown directories. + getRelativeFile("storage/default/invalid+++example.com").remove(false); + getRelativeFile("storage/temporary/invalid+++example.com").remove(false); + + exists = storageFile.exists(); + ok(exists, "Storage file does exist"); + + info("Checking origin directories"); + + for (let origin of origins) { + let originDir = getRelativeFile(origin.path); + + exists = originDir.exists(); + if (origin.obsolete) { + ok(!exists, "Origin directory doesn't exist"); + } else { + ok(exists, "Origin directory does exist"); + + let metadataFile = originDir.clone(); + metadataFile.append(metadataFileName); + + exists = metadataFile.exists(); + ok(exists, "Metadata file does exist"); + + let metadata2File = originDir.clone(); + metadata2File.append(metadata2FileName); + + exists = metadata2File.exists(); + ok(exists, "Metadata file does exist"); + } + } + + info("Initializing temporary storage"); + + request = initTemporaryStorage(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Initializing origins"); + + for (const origin of origins) { + if (!origin.obsolete) { + info("Initializing origin"); + + let principal = getPrincipal(origin.url); + request = initTemporaryOrigin( + origin.persistence, + principal, + continueToNextStepSync + ); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + ok(!request.result, "Origin directory wasn't created"); + } + } + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_idb.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_idb.js new file mode 100644 index 0000000000..34508f85e8 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_idb.js @@ -0,0 +1,43 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify indexedDB::QuotaClient::UpgradeStorageFrom1_0To2_0 + * method. + */ + +async function testSteps() { + const packages = [ + // Storage used by FF 49-54 (storage version 1.0 with idb directory). + "version1_0_idb_profile", + "../defaultStorageDirectory_shared", + ]; + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Initializing"); + + request = init(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeAppsData.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeAppsData.js new file mode 100644 index 0000000000..17946ffff5 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeAppsData.js @@ -0,0 +1,101 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify MaybeRemoveAppsData method. + */ + +function* testSteps() { + const origins = [ + { + path: "storage/default/http+++www.mozilla.org", + obsolete: false, + }, + + { + path: "storage/default/app+++system.gaiamobile.org^appId=1007", + obsolete: true, + }, + + { + path: + "storage/default/https+++developer.cdn.mozilla.net^appId=1007&inBrowser=1", + obsolete: true, + }, + ]; + + const packages = [ + // Storage used by FF 49-54 (storage version 1.0 with apps data). + "version1_0_appsData_profile", + "../defaultStorageDirectory_shared", + ]; + + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Checking origin directories"); + + for (let origin of origins) { + let originDir = getRelativeFile(origin.path); + + let exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + } + + info("Initializing"); + + let request = init(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + // TODO: Remove this block once getting usage is able to ignore unknown + // directories. + getRelativeFile("storage/default/invalid+++example.com").remove(false); + getRelativeFile("storage/permanent/invalid+++example.com").remove(false); + getRelativeFile("storage/temporary/invalid+++example.com").remove(false); + + info("Checking origin directories"); + + for (let origin of origins) { + let originDir = getRelativeFile(origin.path); + + let exists = originDir.exists(); + if (origin.obsolete) { + ok(!exists, "Origin directory doesn't exist"); + } else { + ok(exists, "Origin directory does exist"); + } + } + + info("Getting usage"); + + getUsage(grabResultAndContinueHandler, /* getAll */ true); + let result = yield undefined; + + info("Verifying result"); + + is(result.length, 1, "Correct number of usage results"); + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeMorgueDirectory.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeMorgueDirectory.js new file mode 100644 index 0000000000..b37d00bbf8 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeMorgueDirectory.js @@ -0,0 +1,60 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify MaybeRemoveMorgueDirectory method. + */ + +function* testSteps() { + const morgueFile = "storage/default/http+++example.com/morgue"; + + const packages = [ + // Storage used by FF 49-54 (storage version 1.0 with morgue directory). + "version1_0_morgueDirectory_profile", + "../defaultStorageDirectory_shared", + ]; + + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Checking morgue file"); + + let file = getRelativeFile(morgueFile); + + let exists = file.exists(); + ok(exists, "Morgue file does exist"); + + info("Initializing"); + + let request = init(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + info("Checking morgue file"); + + exists = file.exists(); + ok(!exists, "Morgue file doesn't exist"); + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_stripObsoleteOriginAttributes.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_stripObsoleteOriginAttributes.js new file mode 100644 index 0000000000..407a04c1f8 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_stripObsoleteOriginAttributes.js @@ -0,0 +1,179 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify MaybeStripObsoleteOriginAttributes method. + */ + +loadScript("dom/quota/test/common/file.js"); + +function* testSteps() { + const origins = [ + { + oldPath: + "storage/permanent/moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com", + newPath: + "storage/permanent/moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f", + url: "moz-extension://8ea6d31b-917c-431f-a204-15b95e904d4f", + persistence: "persistent", + }, + + { + oldPath: + "storage/temporary/moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com", + newPath: + "storage/temporary/moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f", + url: "moz-extension://8ea6d31b-917c-431f-a204-15b95e904d4f", + persistence: "temporary", + }, + + { + oldPath: + "storage/default/moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com", + newPath: + "storage/default/moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f", + url: "moz-extension://8ea6d31b-917c-431f-a204-15b95e904d4f", + persistence: "default", + }, + ]; + + const metadataFileName = ".metadata-v2"; + + const packages = [ + // Storage used by FF 49-54 (storage version 1.0 with obsolete origin + // attributes). + "version1_0_obsoleteOriginAttributes_profile", + "../defaultStorageDirectory_shared", + ]; + + let metadataBuffers = []; + + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Checking origin directories"); + + for (let origin of origins) { + let originDir = getRelativeFile(origin.oldPath); + let exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + + info("Reading out contents of metadata file"); + + let metadataFile = originDir.clone(); + metadataFile.append(metadataFileName); + + File.createFromNsIFile(metadataFile).then(grabArgAndContinueHandler); + let file = yield undefined; + + let fileReader = new FileReader(); + fileReader.onload = continueToNextStepSync; + fileReader.readAsArrayBuffer(file); + + yield undefined; + + metadataBuffers.push(fileReader.result); + + originDir = getRelativeFile(origin.newPath); + exists = originDir.exists(); + ok(!exists, "Origin directory doesn't exist"); + } + + info("Initializing"); + + let request = init(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + // TODO: Remove this block once temporary storage initialization is able to + // ignore unknown directories. + getRelativeFile("storage/default/invalid+++example.com").remove(false); + getRelativeFile("storage/temporary/invalid+++example.com").remove(false); + + info("Checking origin directories"); + + for (let origin of origins) { + let originDir = getRelativeFile(origin.oldPath); + let exists = originDir.exists(); + ok(!exists, "Origin directory doesn't exist"); + + originDir = getRelativeFile(origin.newPath); + exists = originDir.exists(); + ok(exists, "Origin directory does exist"); + + info("Reading out contents of metadata file"); + + let metadataFile = originDir.clone(); + metadataFile.append(metadataFileName); + + File.createFromNsIFile(metadataFile).then(grabArgAndContinueHandler); + let file = yield undefined; + + let fileReader = new FileReader(); + fileReader.onload = continueToNextStepSync; + fileReader.readAsArrayBuffer(file); + + yield undefined; + + let metadataBuffer = fileReader.result; + + info("Verifying blobs differ"); + + ok( + !compareBuffers(metadataBuffer, metadataBuffers.shift()), + "Metadata differ" + ); + } + + info("Initializing temporary storage"); + + request = initTemporaryStorage(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Initializing origins"); + + for (const origin of origins) { + info("Initializing origin"); + + let principal = getPrincipal(origin.url); + if (origin.persistence == "persistent") { + request = initPersistentOrigin(principal, continueToNextStepSync); + } else { + request = initTemporaryOrigin( + origin.persistence, + principal, + continueToNextStepSync + ); + } + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + ok(!request.result, "Origin directory wasn't created"); + } + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_0.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_0.js new file mode 100644 index 0000000000..55edfa6055 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_0.js @@ -0,0 +1,97 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify UpgradeStorageFrom2_0To2_1 method. + */ + +function* testSteps() { + const origins = [ + "storage/default/chrome/", + "storage/default/http+++www.mozilla.org/", + ]; + const paddingFilePath = "cache/.padding"; + + const packages = [ + // Storage used by FF 55-56 (storage version 2.0). + // The profile contains two cache storages: + // - storage/default/chrome/cache, + // - storage/default/http+++www.mozilla.org/cache + // The file create_cache.js in the package was run locally, specifically it + // was temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/quota/test/xpcshell/create_cache.js + // Note: it only creates the directory "storage/default/chrome/cache". + // To make it become the profile in the test, two more manual steps are + // needed. + // 1. Remove the folder "storage/temporary". + // 2. Copy the content under the "storage/default/chrome" to + // "storage/default/http+++www.mozilla.org". + // 3. Manually create an asmjs folder under the + // "storage/default/http+++www.mozilla.org/". + "version2_0_profile", + "../defaultStorageDirectory_shared", + ]; + + info("Clearing"); + + clear(continueToNextStepSync); + yield undefined; + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Checking padding files before upgrade (storage version 2.0)"); + + for (let origin of origins) { + let paddingFile = getRelativeFile(origin + paddingFilePath); + let exists = paddingFile.exists(); + ok(!exists, "Padding file doesn't exist"); + } + + info("Initializing"); + + // Initialize to trigger storage upgrade from version 2.0. + let request = init(continueToNextStepSync); + yield undefined; + + ok(request.resultCode == NS_OK, "Initialization succeeded"); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + info("Checking padding files after upgrade"); + + for (let origin of origins) { + let paddingFile = getRelativeFile(origin + paddingFilePath); + let exists = paddingFile.exists(); + ok(exists, "Padding file does exist"); + + info("Reading out contents of padding file"); + + File.createFromNsIFile(paddingFile).then(grabArgAndContinueHandler); + let domFile = yield undefined; + + let fileReader = new FileReader(); + fileReader.onload = continueToNextStepSync; + fileReader.readAsArrayBuffer(domFile); + yield undefined; + + let paddingFileInfo = new Float64Array(fileReader.result); + ok(paddingFileInfo.length == 1, "Padding file does take 64 bytes."); + ok(paddingFileInfo[0] == 0, "Padding size does default to zero."); + } + + finishTest(); +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_1.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_1.js new file mode 100644 index 0000000000..eed7ed21d5 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_1.js @@ -0,0 +1,85 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify UpgradeStorageFrom2_1To2_2 method (removal of + * obsolete origins, deprecated clients and unknown temporary files). + */ + +async function testSteps() { + const filePaths = [ + // Obsolete origins: + "storage/default/chrome+++content+browser.xul", + + "storage/default/moz-safe-about+++home", + + // TODO: These three origins don't belong here! They were added one release + // later and the origin parser was fixed to handle these origins one + // release later as well, so users which already upgraded to 2.2 may + // still have issues related to these origins! + "storage/default/about+home+1", + + "storage/default/about+home+1+q", + + // about:reader?url=xxx (before bug 1422456) + "storage/default/about+reader+url=https%3A%2F%2Fexample.com", + + // Deprecated client: + "storage/default/https+++example.com/asmjs", + + // Unknown temporary file: + "storage/default/https+++example.com/idb/UUID123.tmp", + ]; + + const packages = [ + // Storage used by FF 57-67 (storage version 2.1 with obsolete origins, a + // deprecated client and an unknown temporary file). + "version2_1_profile", + "../defaultStorageDirectory_shared", + ]; + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + info("Checking files and directories before upgrade (storage version 2.1)"); + + for (const filePath of filePaths) { + let file = getRelativeFile(filePath); + let exists = file.exists(); + ok(exists, "File or directory does exist"); + } + + info("Initializing"); + + // Initialize to trigger storage upgrade from version 2.1 + request = init(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + info("Checking files and directories after upgrade"); + + for (const filePath of filePaths) { + let file = getRelativeFile(filePath); + let exists = file.exists(); + ok(!exists, "File or directory does not exist"); + } +} diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_2.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_2.js new file mode 100644 index 0000000000..8f41c05b49 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_2.js @@ -0,0 +1,64 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This test is mainly to verify UpgradeStorageFrom2_2To2_3 method. + */ + +async function testSteps() { + const packages = [ + // Storage used by FF 68-69 (storage version 2.2). + "version2_2_profile", + "../defaultStorageDirectory_shared", + ]; + + function verifyDatabaseTable(shouldExist) { + let file = getRelativeFile("storage.sqlite"); + let conn = Services.storage.openUnsharedDatabase(file); + + let exists = conn.tableExists("database"); + if (shouldExist) { + ok(exists, "Database table does exist"); + } else { + ok(!exists, "Database table does not exist"); + } + + conn.close(); + } + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "beforeInstall"); + + info("Installing packages"); + + installPackages(packages); + + info("Verifying storage"); + + verifyStorage(packages, "afterInstall"); + + verifyDatabaseTable(/* shouldExist */ false); + + info("Initializing"); + + // Initialize to trigger storage upgrade from version 2.2 + request = init(); + await requestFinished(request); + + info("Verifying storage"); + + verifyStorage(packages, "afterInit"); + + request = reset(); + await requestFinished(request); + + verifyDatabaseTable(/* shouldExist */ true); +} diff --git a/dom/quota/test/xpcshell/upgrades/version0_0_profile.json b/dom/quota/test/xpcshell/upgrades/version0_0_profile.json new file mode 100644 index 0000000000..05c0d27fe3 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version0_0_profile.json @@ -0,0 +1,88 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "1007+f+app+++system.gaiamobile.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "1007+t+https+++developer.cdn.mozilla.net", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "1007+t+https+++developer.cdn.mozilla.org", + "dir": true, + "entries": [{ "name": ".metadata", "dir": false }] + }, + { + "name": "https+++developer.cdn.mozilla.org", + "dir": true + } + ] + } + ] + } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404", + "name": "https+++developer.cdn.mozilla.net", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "https+++developer.cdn.mozilla.org", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/version0_0_profile.zip b/dom/quota/test/xpcshell/upgrades/version0_0_profile.zip Binary files differnew file mode 100644 index 0000000000..5ef577191c --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version0_0_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.json b/dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.json new file mode 100644 index 0000000000..88f5d5dcee --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.json @@ -0,0 +1,72 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "app+++system.gaiamobile.org^appId=1007", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "https+++developer.cdn.mozilla.net^appId=1007&inBrowser=1", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.zip b/dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.zip Binary files differnew file mode 100644 index 0000000000..582edb43af --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.json b/dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.json new file mode 100644 index 0000000000..a8fb8b2260 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.json @@ -0,0 +1,73 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "permanent", + "dir": true, + "entries": [ + { + "name": "moz-safe-about+home", + "dir": true, + "entries": [ + { + "name": "idb", + "dir": true, + "entries": [ + { "name": "631132235dGb", "dir": true }, + { "name": "631132235dGb.files", "dir": true }, + { "name": "631132235dGb.sqlite", "dir": false } + ] + }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir":false } + ] + } + ] + } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "permanent", + "dir": true, + "entries": [ + { + "name": "moz-safe-about+home", + "dir": true, + "entries": [ + { + "name": "idb", + "dir": true, + "entries": [ + { "name": "631132235dGb.files", "dir": true }, + { "name": "631132235dGb.sqlite", "dir": false } + ] + }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir":false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.zip b/dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.zip Binary files differnew file mode 100644 index 0000000000..8abfae79c2 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.json b/dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.json new file mode 100644 index 0000000000..855f7846bc --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.json @@ -0,0 +1,57 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "http+++example.com", + "dir": true, + "entries": [ + { "name": "morgue", "dir": true }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "http+++example.com", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.zip b/dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.zip Binary files differnew file mode 100644 index 0000000000..88543784ec --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.json b/dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.json new file mode 100644 index 0000000000..071c4413f4 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.json @@ -0,0 +1,112 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { + "name": "permanent", + "dir": true, + "entries": [ + { + "name": "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { + "name": "temporary", + "dir": true, + "entries": [ + { + "name": "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { + "name": "permanent", + "dir": true, + "entries": [ + { + "name": "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { + "name": "temporary", + "dir": true, + "entries": [ + { + "name": "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f", + "dir": true, + "entries": [ + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.zip b/dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.zip Binary files differnew file mode 100644 index 0000000000..2b4125edf9 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/version2_0_profile.json b/dom/quota/test/xpcshell/upgrades/version2_0_profile.json new file mode 100644 index 0000000000..04ad73eae3 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version2_0_profile.json @@ -0,0 +1,105 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "chrome", + "dir": true, + "entries": [ + { + "name": "cache", + "dir": true, + "entries": [ + { "name": "morgue", "dir": true }, + { "name": "caches.sqlite", "dir": false } + ] + }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { "name": "asmjs", "dir": true }, + { + "name": "cache", + "dir": true, + "entries": [ + { "name": "morgue", "dir": true }, + { "name": "caches.sqlite", "dir": false } + ] + } + ] + } + ] + } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "chrome", + "dir": true, + "entries": [ + { + "name": "cache", + "dir": true, + "entries": [ + { "name": "morgue", "dir": true }, + { "name": ".padding", "dir": false }, + { "name": "caches.sqlite", "dir": false } + ] + }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + }, + { + "name": "http+++www.mozilla.org", + "dir": true, + "entries": [ + { + "name": "cache", + "dir": true, + "entries": [ + { "name": "morgue", "dir": true }, + { "name": ".padding", "dir": false }, + { "name": "caches.sqlite", "dir": false } + ] + }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/version2_0_profile.zip b/dom/quota/test/xpcshell/upgrades/version2_0_profile.zip Binary files differnew file mode 100644 index 0000000000..c140df56e4 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version2_0_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/version2_1_profile.json b/dom/quota/test/xpcshell/upgrades/version2_1_profile.json new file mode 100644 index 0000000000..a7866d1123 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version2_1_profile.json @@ -0,0 +1,69 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { "name": "about+home+1", "dir": true }, + { "name": "about+home+1+q", "dir": true }, + { + "name": "about+reader+url=https%3A%2F%2Fexample.com", + "dir": true + }, + { "name": "chrome+++content+browser.xul", "dir": true }, + { + "name": "https+++example.com", + "dir": true, + "entries": [ + { "name": "asmjs", "dir": true }, + { + "name": "idb", + "dir": true, + "entries": [{ "name": "UUID123.tmp", "dir": false }] + } + ] + }, + { "name": "moz-safe-about+++home", "dir": true } + ] + } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [ + { + "name": "default", + "dir": true, + "entries": [ + { + "name": "https+++example.com", + "dir": true, + "entries": [ + { "name": "idb", "dir": true }, + { "name": ".metadata", "dir": false }, + { "name": ".metadata-v2", "dir": false } + ] + } + ] + }, + { "name": "ls-archive.sqlite", "dir": false } + ] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/version2_1_profile.zip b/dom/quota/test/xpcshell/upgrades/version2_1_profile.zip Binary files differnew file mode 100644 index 0000000000..908dac7058 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version2_1_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/version2_2_profile.json b/dom/quota/test/xpcshell/upgrades/version2_2_profile.json new file mode 100644 index 0000000000..4b7265e3b4 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version2_2_profile.json @@ -0,0 +1,18 @@ +[ + { "key": "beforeInstall", "entries": [] }, + { + "key": "afterInstall", + "entries": [{ "name": "storage.sqlite", "dir": false }] + }, + { + "key": "afterInit", + "entries": [ + { + "name": "storage", + "dir": true, + "entries": [{ "name": "ls-archive.sqlite", "dir": false }] + }, + { "name": "storage.sqlite", "dir": false } + ] + } +] diff --git a/dom/quota/test/xpcshell/upgrades/version2_2_profile.zip b/dom/quota/test/xpcshell/upgrades/version2_2_profile.zip Binary files differnew file mode 100644 index 0000000000..b6ae7e7d76 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/version2_2_profile.zip diff --git a/dom/quota/test/xpcshell/upgrades/xpcshell.ini b/dom/quota/test/xpcshell/upgrades/xpcshell.ini new file mode 100644 index 0000000000..8698e4bff4 --- /dev/null +++ b/dom/quota/test/xpcshell/upgrades/xpcshell.ini @@ -0,0 +1,61 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +[DEFAULT] +head = head.js +support-files = + cacheVersion1_profile.json + cacheVersion1_profile.zip + indexedDBAndPersistentStorageDirectory_profile.json + indexedDBAndPersistentStorageDirectory_profile.zip + indexedDBDirectory_flatOriginDirectories_profile.json + indexedDBDirectory_flatOriginDirectories_profile.zip + indexedDBDirectory_profile.json + indexedDBDirectory_profile.zip + localStorageArchive1upgrade_profile.zip + localStorageArchive4upgrade_profile.zip + localStorageArchiveDowngrade_profile.zip + persistentAndDefaultStorageDirectory_profile.json + persistentAndDefaultStorageDirectory_profile.zip + persistentStorageDirectory_flatOriginDirectories_profile.json + persistentStorageDirectory_flatOriginDirectories_profile.zip + persistentStorageDirectory_originDirectories_profile.json + persistentStorageDirectory_originDirectories_profile.zip + persistentStorageDirectory_profile.json + persistentStorageDirectory_profile.zip + version0_0_profile.json + version0_0_profile.zip + version1_0_appsData_profile.json + version1_0_appsData_profile.zip + version1_0_idb_profile.json + version1_0_idb_profile.zip + version1_0_morgueDirectory_profile.json + version1_0_morgueDirectory_profile.zip + version1_0_obsoleteOriginAttributes_profile.json + version1_0_obsoleteOriginAttributes_profile.zip + version2_0_profile.json + version2_0_profile.zip + version2_1_profile.json + version2_1_profile.zip + version2_2_profile.json + version2_2_profile.zip + +[test_localStorageArchive1upgrade.js] +[test_localStorageArchive4upgrade.js] +[test_localStorageArchiveDowngrade.js] +[test_upgradeCacheFrom1.js] +[test_upgradeFromFlatOriginDirectories.js] +[test_upgradeFromIndexedDBDirectory.js] +[test_upgradeFromIndexedDBDirectory_removeOldDirectory.js] +[test_upgradeFromPersistentStorageDirectory.js] +[test_upgradeFromPersistentStorageDirectory_removeOldDirectory.js] +[test_upgradeFromPersistentStorageDirectory_upgradeOriginDirectories.js] +[test_upgradeStorageFrom0_0.js] +[test_upgradeStorageFrom1_0_idb.js] +[test_upgradeStorageFrom1_0_removeAppsData.js] +[test_upgradeStorageFrom1_0_removeMorgueDirectory.js] +[test_upgradeStorageFrom1_0_stripObsoleteOriginAttributes.js] +[test_upgradeStorageFrom2_0.js] +[test_upgradeStorageFrom2_1.js] +[test_upgradeStorageFrom2_2.js] diff --git a/dom/quota/test/xpcshell/xpcshell.ini b/dom/quota/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..2b85faed16 --- /dev/null +++ b/dom/quota/test/xpcshell/xpcshell.ini @@ -0,0 +1,61 @@ +# 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 = + basics_profile.zip + clearStoragesForPrincipal_profile.zip + createLocalStorage_profile.zip + defaultStorageDirectory_shared.json + defaultStorageDirectory_shared.zip + getUsage_profile.zip + groupMismatch_profile.zip + indexedDBDirectory_shared.json + indexedDBDirectory_shared.zip + originMismatch_profile.json + originMismatch_profile.zip + persistentStorageDirectory_shared.json + persistentStorageDirectory_shared.zip + removeLocalStorage1_profile.zip + removeLocalStorage2_profile.zip + tempMetadataCleanup_profile.zip + unknownFiles_profile.zip + +[make_unknownFiles.js] +skip-if = true # Only used for recreating unknownFiles_profile.zip +[make_unsetLastAccessTime.js] +skip-if = true # Only used for recreating unsetLastAccessTime_profile.zip +[test_allowListFiles.js] +[test_basics.js] +[test_bad_origin_directory.js] +[test_createLocalStorage.js] +[test_clearStoragesForPrincipal.js] +[test_clearStoragesForOriginAttributesPattern.js] +[test_estimateOrigin.js] +[test_getUsage.js] +[test_groupMismatch.js] +[test_initTemporaryStorage.js] +[test_listOrigins.js] +[test_originEndsWithDot.js] +[test_originMismatch.js] +[test_originWithCaret.js] +[test_orpahnedQuotaObject.js] +[test_persist.js] +[test_persist_eviction.js] +[test_persist_globalLimit.js] +[test_persist_groupLimit.js] +[test_removeLocalStorage.js] +[test_simpledb.js] +[test_specialOrigins.js] +[test_storagePressure.js] +skip-if = condprof +[test_tempMetadataCleanup.js] +[test_unaccessedOrigins.js] +[test_unknownFiles.js] +[test_unsetLastAccessTime.js] +support-files = + unsetLastAccessTime_profile.zip +[test_validOrigins.js] |