summaryrefslogtreecommitdiffstats
path: root/dom/cache/FileUtils.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/cache/FileUtils.cpp')
-rw-r--r--dom/cache/FileUtils.cpp814
1 files changed, 814 insertions, 0 deletions
diff --git a/dom/cache/FileUtils.cpp b/dom/cache/FileUtils.cpp
new file mode 100644
index 0000000000..cd99591c91
--- /dev/null
+++ b/dom/cache/FileUtils.cpp
@@ -0,0 +1,814 @@
+/* -*- 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 "FileUtilsImpl.h"
+
+#include "CacheCipherKeyManager.h"
+#include "DBSchema.h"
+#include "mozilla/dom/InternalResponse.h"
+#include "mozilla/dom/quota/DecryptingInputStream.h"
+#include "mozilla/dom/quota/DecryptingInputStream_impl.h"
+#include "mozilla/dom/quota/EncryptingOutputStream.h"
+#include "mozilla/dom/quota/EncryptingOutputStream_impl.h"
+#include "mozilla/dom/quota/FileStreams.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/QuotaObject.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/SnappyCompressOutputStream.h"
+#include "mozilla/Unused.h"
+#include "nsIObjectInputStream.h"
+#include "nsIObjectOutputStream.h"
+#include "nsIFile.h"
+#include "nsIUUIDGenerator.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "snappy/snappy.h"
+
+namespace mozilla::dom::cache {
+
+static_assert(SNAPPY_VERSION == 0x010109);
+
+using mozilla::dom::quota::Client;
+using mozilla::dom::quota::CloneFileAndAppend;
+using mozilla::dom::quota::GetDirEntryKind;
+using mozilla::dom::quota::nsIFileKind;
+using mozilla::dom::quota::QuotaManager;
+using mozilla::dom::quota::QuotaObject;
+
+namespace {
+
+// Const variable for generate padding size.
+// XXX This will be tweaked to something more meaningful in Bug 1383656.
+const int64_t kRoundUpNumber = 20480;
+
+// At the moment, the encrypted stream block size is assumed to be unchangeable
+// between encrypting and decrypting blobs. This assumptions holds as long as we
+// only encrypt in private browsing mode, but when we support encryption for
+// persistent storage, this needs to be changed.
+constexpr uint32_t kEncryptedStreamBlockSize = 4096;
+
+enum BodyFileType { BODY_FILE_FINAL, BODY_FILE_TMP };
+
+Result<NotNull<nsCOMPtr<nsIFile>>, nsresult> BodyIdToFile(nsIFile& aBaseDir,
+ const nsID& aId,
+ BodyFileType aType);
+
+int64_t RoundUp(int64_t aX, int64_t aY);
+
+// The alogrithm for generating padding refers to the mitigation approach in
+// https://github.com/whatwg/storage/issues/31.
+// First, generate a random number between 0 and 100kB.
+// Next, round up the sum of random number and response size to the nearest
+// 20kB.
+// Finally, the virtual padding size will be the result minus the response size.
+int64_t BodyGeneratePadding(int64_t aBodyFileSize, uint32_t aPaddingInfo);
+
+nsresult DirectoryPaddingWrite(nsIFile& aBaseDir,
+ DirPaddingFile aPaddingFileType,
+ int64_t aPaddingSize);
+
+const auto kMorgueDirectory = u"morgue"_ns;
+
+bool IsFileNotFoundError(const nsresult aRv) {
+ return aRv == NS_ERROR_FILE_NOT_FOUND;
+}
+
+Result<NotNull<nsCOMPtr<nsIFile>>, nsresult> BodyGetCacheDir(nsIFile& aBaseDir,
+ const nsID& aId) {
+ QM_TRY_UNWRAP(auto cacheDir, CloneFileAndAppend(aBaseDir, kMorgueDirectory));
+
+ // Some file systems have poor performance when there are too many files
+ // in a single directory. Mitigate this issue by spreading the body
+ // files out into sub-directories. We use the last byte of the ID for
+ // the name of the sub-directory.
+ QM_TRY(MOZ_TO_RESULT(cacheDir->Append(IntToString(aId.m3[7]))));
+
+ // 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(QM_OR_ELSE_LOG_VERBOSE_IF(
+ // Expression.
+ MOZ_TO_RESULT(cacheDir->Create(nsIFile::DIRECTORY_TYPE, 0755)),
+ // Predicate.
+ IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>,
+ // Fallback.
+ ErrToDefaultOk<>));
+
+ return WrapNotNullUnchecked(std::move(cacheDir));
+}
+
+} // namespace
+
+nsresult BodyCreateDir(nsIFile& aBaseDir) {
+ QM_TRY_INSPECT(const auto& bodyDir,
+ CloneFileAndAppend(aBaseDir, kMorgueDirectory));
+
+ // 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(QM_OR_ELSE_LOG_VERBOSE_IF(
+ // Expression.
+ MOZ_TO_RESULT(bodyDir->Create(nsIFile::DIRECTORY_TYPE, 0755)),
+ // Predicate.
+ IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>,
+ // Fallback.
+ ErrToDefaultOk<>));
+
+ return NS_OK;
+}
+
+nsresult BodyDeleteDir(const CacheDirectoryMetadata& aDirectoryMetadata,
+ nsIFile& aBaseDir) {
+ QM_TRY_INSPECT(const auto& bodyDir,
+ CloneFileAndAppend(aBaseDir, kMorgueDirectory));
+
+ QM_TRY(MOZ_TO_RESULT(RemoveNsIFileRecursively(aDirectoryMetadata, *bodyDir)));
+
+ return NS_OK;
+}
+
+Result<nsCOMPtr<nsISupports>, nsresult> BodyStartWriteStream(
+ const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile& aBaseDir,
+ const nsID& aBodyId, Maybe<CipherKey> aMaybeCipherKey,
+ nsIInputStream& aSource, void* aClosure, nsAsyncCopyCallbackFun aCallback) {
+ MOZ_DIAGNOSTIC_ASSERT(aClosure);
+ MOZ_DIAGNOSTIC_ASSERT(aCallback);
+
+ QM_TRY_INSPECT(const auto& finalFile,
+ BodyIdToFile(aBaseDir, aBodyId, BODY_FILE_FINAL));
+
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(*finalFile, Exists));
+
+ QM_TRY(OkIf(!exists), Err(NS_ERROR_FILE_ALREADY_EXISTS));
+ }
+
+ QM_TRY_INSPECT(const auto& tmpFile,
+ BodyIdToFile(aBaseDir, aBodyId, BODY_FILE_TMP));
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIOutputStream> fileStream,
+ CreateFileOutputStream(aDirectoryMetadata.mPersistenceType,
+ aDirectoryMetadata, Client::DOMCACHE,
+ tmpFile.get()));
+
+ const auto privateBody = aDirectoryMetadata.mIsPrivate;
+ if (privateBody) {
+ MOZ_DIAGNOSTIC_ASSERT(aMaybeCipherKey);
+
+ fileStream = MakeRefPtr<quota::EncryptingOutputStream<CipherStrategy>>(
+ std::move(fileStream), kEncryptedStreamBlockSize, *aMaybeCipherKey);
+ }
+
+ const auto compressed = MakeRefPtr<SnappyCompressOutputStream>(fileStream);
+
+ const nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+
+ nsCOMPtr<nsISupports> copyContext;
+ QM_TRY(MOZ_TO_RESULT(
+ NS_AsyncCopy(&aSource, compressed, target, NS_ASYNCCOPY_VIA_WRITESEGMENTS,
+ compressed->BlockSize(), aCallback, aClosure, true,
+ true, // close streams
+ getter_AddRefs(copyContext))));
+
+ return std::move(copyContext);
+}
+
+void BodyCancelWrite(nsISupports& aCopyContext) {
+ QM_WARNONLY_TRY(
+ QM_TO_RESULT(NS_CancelAsyncCopy(&aCopyContext, NS_ERROR_ABORT)));
+
+ // TODO The partially written file must be cleaned up after the async copy
+ // makes its callback.
+}
+
+Result<int64_t, nsresult> BodyFinalizeWrite(nsIFile& aBaseDir,
+ const nsID& aId) {
+ QM_TRY_INSPECT(const auto& tmpFile,
+ BodyIdToFile(aBaseDir, aId, BODY_FILE_TMP));
+
+ QM_TRY_INSPECT(const auto& finalFile,
+ BodyIdToFile(aBaseDir, aId, BODY_FILE_FINAL));
+
+ nsAutoString finalFileName;
+ QM_TRY(MOZ_TO_RESULT(finalFile->GetLeafName(finalFileName)));
+
+ // It's fine to not notify the QuotaManager that the path has been changed,
+ // because its path will be updated and its size will be recalculated when
+ // opening file next time.
+ QM_TRY(MOZ_TO_RESULT(tmpFile->RenameTo(nullptr, finalFileName)));
+
+ QM_TRY_INSPECT(const int64_t& fileSize,
+ MOZ_TO_RESULT_INVOKE_MEMBER(*finalFile, GetFileSize));
+
+ return fileSize;
+}
+
+Result<int64_t, nsresult> GetBodyDiskSize(nsIFile& aBaseDir, const nsID& aId) {
+ QM_TRY_INSPECT(const auto& finalFile,
+ BodyIdToFile(aBaseDir, aId, BODY_FILE_FINAL));
+
+ QM_TRY_INSPECT(const int64_t& fileSize,
+ MOZ_TO_RESULT_INVOKE_MEMBER(*finalFile, GetFileSize));
+
+ return fileSize;
+}
+
+Result<MovingNotNull<nsCOMPtr<nsIInputStream>>, nsresult> BodyOpen(
+ const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile& aBaseDir,
+ const nsID& aId, Maybe<CipherKey> aMaybeCipherKey) {
+ QM_TRY_INSPECT(const auto& finalFile,
+ BodyIdToFile(aBaseDir, aId, BODY_FILE_FINAL));
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIInputStream> fileInputStream,
+ CreateFileInputStream(aDirectoryMetadata.mPersistenceType,
+ aDirectoryMetadata, Client::DOMCACHE,
+ finalFile.get()));
+
+ auto privateBody = aDirectoryMetadata.mIsPrivate;
+ if (privateBody) {
+ MOZ_DIAGNOSTIC_ASSERT(aMaybeCipherKey);
+
+ fileInputStream = new quota::DecryptingInputStream<CipherStrategy>(
+ WrapNotNull(std::move(fileInputStream)), kEncryptedStreamBlockSize,
+ *aMaybeCipherKey);
+ }
+ return WrapMovingNotNull(std::move(fileInputStream));
+}
+
+nsresult BodyMaybeUpdatePaddingSize(
+ const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile& aBaseDir,
+ const nsID& aId, const uint32_t aPaddingInfo, int64_t* aPaddingSizeInOut) {
+ MOZ_DIAGNOSTIC_ASSERT(aPaddingSizeInOut);
+
+ QM_TRY_INSPECT(const auto& bodyFile,
+ BodyIdToFile(aBaseDir, aId, BODY_FILE_TMP));
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_DIAGNOSTIC_ASSERT(quotaManager);
+
+ int64_t fileSize = 0;
+ RefPtr<QuotaObject> quotaObject = quotaManager->GetQuotaObject(
+ aDirectoryMetadata.mPersistenceType, aDirectoryMetadata, Client::DOMCACHE,
+ bodyFile.get(), -1, &fileSize);
+ MOZ_DIAGNOSTIC_ASSERT(quotaObject);
+ MOZ_DIAGNOSTIC_ASSERT(fileSize >= 0);
+ // XXXtt: bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1422815
+ if (!quotaObject) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (*aPaddingSizeInOut == InternalResponse::UNKNOWN_PADDING_SIZE) {
+ *aPaddingSizeInOut = BodyGeneratePadding(fileSize, aPaddingInfo);
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(*aPaddingSizeInOut >= 0);
+
+ if (!quotaObject->IncreaseSize(*aPaddingSizeInOut)) {
+ return NS_ERROR_FILE_NO_DEVICE_SPACE;
+ }
+
+ return NS_OK;
+}
+
+nsresult BodyDeleteFiles(const CacheDirectoryMetadata& aDirectoryMetadata,
+ nsIFile& aBaseDir, const nsTArray<nsID>& aIdList) {
+ for (const auto id : aIdList) {
+ QM_TRY_INSPECT(const auto& bodyDir, BodyGetCacheDir(aBaseDir, id));
+
+ const auto removeFileForId =
+ [&aDirectoryMetadata, &id](
+ nsIFile& bodyFile,
+ const nsACString& leafName) -> Result<bool, nsresult> {
+ nsID fileId{};
+ QM_TRY(OkIf(fileId.Parse(leafName.BeginReading())), true,
+ ([&aDirectoryMetadata, &bodyFile](const auto) {
+ DebugOnly<nsresult> result = RemoveNsIFile(
+ aDirectoryMetadata, bodyFile, /* aTrackQuota */ false);
+ MOZ_ASSERT(NS_SUCCEEDED(result));
+ }));
+
+ if (id.Equals(fileId)) {
+ DebugOnly<nsresult> result =
+ RemoveNsIFile(aDirectoryMetadata, bodyFile);
+ MOZ_ASSERT(NS_SUCCEEDED(result));
+ return true;
+ }
+
+ return false;
+ };
+ QM_TRY(MOZ_TO_RESULT(BodyTraverseFiles(aDirectoryMetadata, *bodyDir,
+ removeFileForId,
+ /* aCanRemoveFiles */ false,
+ /* aTrackQuota */ true)));
+ }
+
+ return NS_OK;
+}
+
+namespace {
+
+Result<NotNull<nsCOMPtr<nsIFile>>, nsresult> BodyIdToFile(
+ nsIFile& aBaseDir, const nsID& aId, const BodyFileType aType) {
+ QM_TRY_UNWRAP(auto bodyFile, BodyGetCacheDir(aBaseDir, aId));
+
+ char idString[NSID_LENGTH];
+ aId.ToProvidedString(idString);
+
+ NS_ConvertASCIItoUTF16 fileName(idString);
+
+ if (aType == BODY_FILE_FINAL) {
+ fileName.AppendLiteral(".final");
+ } else {
+ fileName.AppendLiteral(".tmp");
+ }
+
+ QM_TRY(MOZ_TO_RESULT(bodyFile->Append(fileName)));
+
+ return bodyFile;
+}
+
+int64_t RoundUp(const int64_t aX, const int64_t aY) {
+ MOZ_DIAGNOSTIC_ASSERT(aX >= 0);
+ MOZ_DIAGNOSTIC_ASSERT(aY > 0);
+
+ MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - ((aX - 1) / aY) * aY >= aY);
+ return aY + ((aX - 1) / aY) * aY;
+}
+
+int64_t BodyGeneratePadding(const int64_t aBodyFileSize,
+ const uint32_t aPaddingInfo) {
+ // Generate padding
+ int64_t randomSize = static_cast<int64_t>(aPaddingInfo);
+ MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - aBodyFileSize >= randomSize);
+ randomSize += aBodyFileSize;
+
+ return RoundUp(randomSize, kRoundUpNumber) - aBodyFileSize;
+}
+
+nsresult DirectoryPaddingWrite(nsIFile& aBaseDir,
+ DirPaddingFile aPaddingFileType,
+ int64_t aPaddingSize) {
+ MOZ_DIAGNOSTIC_ASSERT(aPaddingSize >= 0);
+
+ QM_TRY_INSPECT(
+ const auto& file,
+ CloneFileAndAppend(aBaseDir, aPaddingFileType == DirPaddingFile::TMP_FILE
+ ? nsLiteralString(PADDING_TMP_FILE_NAME)
+ : nsLiteralString(PADDING_FILE_NAME)));
+
+ QM_TRY_INSPECT(const auto& outputStream, NS_NewLocalFileOutputStream(file));
+
+ nsCOMPtr<nsIObjectOutputStream> objectStream =
+ NS_NewObjectOutputStream(outputStream);
+
+ QM_TRY(MOZ_TO_RESULT(objectStream->Write64(aPaddingSize)));
+
+ return NS_OK;
+}
+
+} // namespace
+
+nsresult BodyDeleteOrphanedFiles(
+ const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile& aBaseDir,
+ const nsTArray<nsID>& aKnownBodyIdList) {
+ // body files are stored in a directory structure like:
+ //
+ // /morgue/01/{01fdddb2-884d-4c3d-95ba-0c8062f6c325}.final
+ // /morgue/02/{02fdddb2-884d-4c3d-95ba-0c8062f6c325}.tmp
+
+ QM_TRY_INSPECT(const auto& dir,
+ CloneFileAndAppend(aBaseDir, kMorgueDirectory));
+
+ // Iterate over all the intermediate morgue subdirs
+ QM_TRY(quota::CollectEachFile(
+ *dir,
+ [&aDirectoryMetadata, &aKnownBodyIdList](
+ const nsCOMPtr<nsIFile>& subdir) -> Result<Ok, nsresult> {
+ QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*subdir));
+
+ switch (dirEntryKind) {
+ case nsIFileKind::ExistsAsDirectory: {
+ const auto removeOrphanedFiles =
+ [&aDirectoryMetadata, &aKnownBodyIdList](
+ nsIFile& bodyFile,
+ const nsACString& leafName) -> Result<bool, nsresult> {
+ // Finally, parse the uuid out of the name. If it fails to parse,
+ // then ignore the file.
+ auto cleanup = MakeScopeExit([&aDirectoryMetadata, &bodyFile] {
+ DebugOnly<nsresult> result =
+ RemoveNsIFile(aDirectoryMetadata, bodyFile);
+ MOZ_ASSERT(NS_SUCCEEDED(result));
+ });
+
+ nsID id;
+ QM_TRY(OkIf(id.Parse(leafName.BeginReading())), true);
+
+ if (!aKnownBodyIdList.Contains(id)) {
+ return true;
+ }
+
+ cleanup.release();
+
+ return false;
+ };
+
+ // QM_OR_ELSE_WARN_IF is not used here since we just want to log
+ // NS_ERROR_FILE_FS_CORRUPTED result and not spam the reports (even
+ // a warning in the reports is not desired).
+ QM_TRY(QM_OR_ELSE_LOG_VERBOSE_IF(
+ // Expression.
+ MOZ_TO_RESULT(BodyTraverseFiles(aDirectoryMetadata, *subdir,
+ removeOrphanedFiles,
+ /* aCanRemoveFiles */ true,
+ /* aTrackQuota */ true)),
+ // Predicate.
+ IsSpecificError<NS_ERROR_FILE_FS_CORRUPTED>,
+ // Fallback. We treat NS_ERROR_FILE_FS_CORRUPTED as if the
+ // directory did not exist at all.
+ ErrToDefaultOk<>));
+ break;
+ }
+
+ case nsIFileKind::ExistsAsFile: {
+ // If a file got in here somehow, try to remove it and move on
+ DebugOnly<nsresult> result =
+ RemoveNsIFile(aDirectoryMetadata, *subdir,
+ /* aTrackQuota */ false);
+ MOZ_ASSERT(NS_SUCCEEDED(result));
+ break;
+ }
+
+ case nsIFileKind::DoesNotExist:
+ // Ignore files that got removed externally while iterating.
+ break;
+ }
+
+ return Ok{};
+ }));
+
+ return NS_OK;
+}
+
+namespace {
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetMarkerFileHandle(
+ const CacheDirectoryMetadata& aDirectoryMetadata) {
+ QM_TRY_UNWRAP(auto marker,
+ CloneFileAndAppend(*aDirectoryMetadata.mDir, u"cache"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(marker->Append(u"context_open.marker"_ns)));
+
+ return marker;
+}
+
+} // namespace
+
+nsresult CreateMarkerFile(const CacheDirectoryMetadata& aDirectoryMetadata) {
+ QM_TRY_INSPECT(const auto& marker, GetMarkerFileHandle(aDirectoryMetadata));
+
+ // Callers call this function without checking if the file 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.
+ //
+ // TODO: In theory if this file exists, then Context::~Context should have
+ // cleaned it up, but obviously we can crash and not clean it up, which is
+ // the whole point of the marker file. In that case, we'll realize the marker
+ // file exists in SetupAction::RunSyncWithDBOnTarget and do some cleanup, but
+ // we won't delete the marker file, so if we see this marker file, it is part
+ // of our standard operating procedure to redundantly try and create the
+ // marker here. We currently treat this as idempotent usage, but we could
+ // make sure to delete the marker file when handling the existing marker
+ // file in SetupAction::RunSyncWithDBOnTarget and change
+ // QM_OR_ELSE_LOG_VERBOSE_IF to QM_OR_ELSE_WARN_IF in the end.
+ QM_TRY(QM_OR_ELSE_LOG_VERBOSE_IF(
+ // Expression.
+ MOZ_TO_RESULT(marker->Create(nsIFile::NORMAL_FILE_TYPE, 0644)),
+ // Predicate.
+ IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>,
+ // Fallback.
+ ErrToDefaultOk<>));
+
+ // Note, we don't need to fsync here. We only care about actually
+ // writing the marker if later modifications to the Cache are
+ // actually flushed to the disk. If the OS crashes before the marker
+ // is written then we are ensured no other changes to the Cache were
+ // flushed either.
+
+ return NS_OK;
+}
+
+nsresult DeleteMarkerFile(const CacheDirectoryMetadata& aDirectoryMetadata) {
+ QM_TRY_INSPECT(const auto& marker, GetMarkerFileHandle(aDirectoryMetadata));
+
+ DebugOnly<nsresult> result =
+ RemoveNsIFile(aDirectoryMetadata, *marker, /* aTrackQuota */ false);
+ MOZ_ASSERT(NS_SUCCEEDED(result));
+
+ // Again, no fsync is necessary. If the OS crashes before the file
+ // removal is flushed, then the Cache will search for stale data on
+ // startup. This will cause the next Cache access to be a bit slow, but
+ // it seems appropriate after an OS crash.
+
+ return NS_OK;
+}
+
+bool MarkerFileExists(const CacheDirectoryMetadata& aDirectoryMetadata) {
+ QM_TRY_INSPECT(const auto& marker, GetMarkerFileHandle(aDirectoryMetadata),
+ false);
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(marker, Exists), false);
+}
+
+nsresult RemoveNsIFileRecursively(
+ const Maybe<CacheDirectoryMetadata>& aDirectoryMetadata, nsIFile& aFile,
+ const bool aTrackQuota) {
+ // XXX This assertion proves that we can remove aTrackQuota and just check
+ // aClientMetadata
+ MOZ_DIAGNOSTIC_ASSERT_IF(aTrackQuota, aDirectoryMetadata);
+
+ QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(aFile));
+
+ switch (dirEntryKind) {
+ case nsIFileKind::ExistsAsDirectory:
+ // Unfortunately, we need to traverse all the entries and delete files one
+ // by
+ // one to update their usages to the QuotaManager.
+ QM_TRY(quota::CollectEachFile(
+ aFile,
+ [&aDirectoryMetadata, &aTrackQuota](
+ const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(RemoveNsIFileRecursively(aDirectoryMetadata,
+ *file, aTrackQuota)));
+
+ return Ok{};
+ }));
+
+ // In the end, remove the folder
+ QM_TRY(MOZ_TO_RESULT(aFile.Remove(/* recursive */ false)));
+
+ break;
+
+ case nsIFileKind::ExistsAsFile:
+ return RemoveNsIFile(aDirectoryMetadata, aFile, aTrackQuota);
+
+ case nsIFileKind::DoesNotExist:
+ // Ignore files that got removed externally while iterating.
+ break;
+ }
+
+ return NS_OK;
+}
+
+nsresult RemoveNsIFile(const Maybe<CacheDirectoryMetadata>& aDirectoryMetadata,
+ nsIFile& aFile, const bool aTrackQuota) {
+ // XXX This assertion proves that we can remove aTrackQuota and just check
+ // aClientMetadata
+ MOZ_DIAGNOSTIC_ASSERT_IF(aTrackQuota, aDirectoryMetadata);
+
+ int64_t fileSize = 0;
+ if (aTrackQuota) {
+ QM_TRY_INSPECT(
+ const auto& maybeFileSize,
+ QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT_INVOKE_MEMBER(aFile, GetFileSize).map(Some<int64_t>),
+ // Predicate.
+ IsFileNotFoundError,
+ // Fallback.
+ ErrToDefaultOk<Maybe<int64_t>>));
+
+ if (!maybeFileSize) {
+ return NS_OK;
+ }
+
+ fileSize = *maybeFileSize;
+ }
+
+ QM_TRY(QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT(aFile.Remove(/* recursive */ false)),
+ // Predicate.
+ IsFileNotFoundError,
+ // Fallback.
+ ErrToDefaultOk<>));
+
+ if (fileSize > 0) {
+ MOZ_ASSERT(aTrackQuota);
+ DecreaseUsageForDirectoryMetadata(*aDirectoryMetadata, fileSize);
+ }
+
+ return NS_OK;
+}
+
+void DecreaseUsageForDirectoryMetadata(
+ const CacheDirectoryMetadata& aDirectoryMetadata,
+ const int64_t aUpdatingSize) {
+ MOZ_DIAGNOSTIC_ASSERT(aUpdatingSize > 0);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_DIAGNOSTIC_ASSERT(quotaManager);
+
+ quotaManager->DecreaseUsageForClient(
+ quota::ClientMetadata{aDirectoryMetadata, Client::DOMCACHE},
+ aUpdatingSize);
+}
+
+bool DirectoryPaddingFileExists(nsIFile& aBaseDir,
+ DirPaddingFile aPaddingFileType) {
+ QM_TRY_INSPECT(
+ const auto& file,
+ CloneFileAndAppend(aBaseDir, aPaddingFileType == DirPaddingFile::TMP_FILE
+ ? nsLiteralString(PADDING_TMP_FILE_NAME)
+ : nsLiteralString(PADDING_FILE_NAME)),
+ false);
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(file, Exists), false);
+}
+
+Result<int64_t, nsresult> DirectoryPaddingGet(nsIFile& aBaseDir) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ !DirectoryPaddingFileExists(aBaseDir, DirPaddingFile::TMP_FILE));
+
+ QM_TRY_INSPECT(
+ const auto& file,
+ CloneFileAndAppend(aBaseDir, nsLiteralString(PADDING_FILE_NAME)));
+
+ QM_TRY_UNWRAP(auto stream, NS_NewLocalFileInputStream(file));
+
+ QM_TRY_INSPECT(const auto& bufferedStream,
+ NS_NewBufferedInputStream(stream.forget(), 512));
+
+ const nsCOMPtr<nsIObjectInputStream> objectStream =
+ NS_NewObjectInputStream(bufferedStream);
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(objectStream, Read64)
+ .map([](const uint64_t val) { return int64_t(val); }));
+}
+
+nsresult DirectoryPaddingInit(nsIFile& aBaseDir) {
+ QM_TRY(
+ MOZ_TO_RESULT(DirectoryPaddingWrite(aBaseDir, DirPaddingFile::FILE, 0)));
+
+ return NS_OK;
+}
+
+nsresult UpdateDirectoryPaddingFile(nsIFile& aBaseDir,
+ mozIStorageConnection& aConn,
+ const int64_t aIncreaseSize,
+ const int64_t aDecreaseSize,
+ const bool aTemporaryFileExist) {
+ MOZ_DIAGNOSTIC_ASSERT(aIncreaseSize >= 0);
+ MOZ_DIAGNOSTIC_ASSERT(aDecreaseSize >= 0);
+
+ const auto directoryPaddingGetResult =
+ aTemporaryFileExist ? Maybe<int64_t>{} : [&aBaseDir] {
+ QM_TRY_RETURN(QM_OR_ELSE_WARN_IF(
+ // Expression.
+ DirectoryPaddingGet(aBaseDir).map(Some<int64_t>),
+ // Predicate.
+ IsFileNotFoundError,
+ // Fallback.
+ ErrToDefaultOk<Maybe<int64_t>>),
+ Maybe<int64_t>{});
+ }();
+
+ QM_TRY_INSPECT(
+ const int64_t& currentPaddingSize,
+ ([directoryPaddingGetResult, &aBaseDir, &aConn, aIncreaseSize,
+ aDecreaseSize]() -> Result<int64_t, nsresult> {
+ if (!directoryPaddingGetResult) {
+ // Fail to read padding size from the dir padding file, so try to
+ // restore.
+
+ // Not delete the temporary padding file here, because we're going
+ // to overwrite it below anyway.
+ QM_TRY(MOZ_TO_RESULT(
+ DirectoryPaddingDeleteFile(aBaseDir, DirPaddingFile::FILE)));
+
+ // We don't need to add the aIncreaseSize or aDecreaseSize here,
+ // because it's already encompassed within the database.
+ QM_TRY_RETURN(db::FindOverallPaddingSize(aConn));
+ }
+
+ int64_t currentPaddingSize = directoryPaddingGetResult.value();
+ bool shouldRevise = false;
+
+ if (aIncreaseSize > 0) {
+ if (INT64_MAX - currentPaddingSize < aDecreaseSize) {
+ shouldRevise = true;
+ } else {
+ currentPaddingSize += aIncreaseSize;
+ }
+ }
+
+ if (aDecreaseSize > 0) {
+ if (currentPaddingSize < aDecreaseSize) {
+ shouldRevise = true;
+ } else if (!shouldRevise) {
+ currentPaddingSize -= aDecreaseSize;
+ }
+ }
+
+ if (shouldRevise) {
+ // If somehow runing into this condition, the tracking padding size is
+ // incorrect.
+ // Delete padding file to indicate the padding size is incorrect for
+ // avoiding error happening in the following lines.
+ QM_TRY(MOZ_TO_RESULT(
+ DirectoryPaddingDeleteFile(aBaseDir, DirPaddingFile::FILE)));
+
+ QM_TRY_UNWRAP(currentPaddingSize, db::FindOverallPaddingSize(aConn));
+
+ // XXXtt: we should have an easy way to update (increase or
+ // recalulate) padding size in the QM. For now, only correct the
+ // padding size in padding file and make QM be able to get the correct
+ // size in the next QM initialization. We still want to catch this in
+ // the debug build.
+ MOZ_ASSERT(false, "The padding size is unsync with QM");
+ }
+
+#ifdef DEBUG
+ const int64_t lastPaddingSize = currentPaddingSize;
+ QM_TRY_UNWRAP(currentPaddingSize, db::FindOverallPaddingSize(aConn));
+
+ MOZ_DIAGNOSTIC_ASSERT(currentPaddingSize == lastPaddingSize);
+#endif // DEBUG
+
+ return currentPaddingSize;
+ }()));
+
+ MOZ_DIAGNOSTIC_ASSERT(currentPaddingSize >= 0);
+
+ QM_TRY(MOZ_TO_RESULT(DirectoryPaddingWrite(aBaseDir, DirPaddingFile::TMP_FILE,
+ currentPaddingSize)));
+
+ return NS_OK;
+}
+
+nsresult DirectoryPaddingFinalizeWrite(nsIFile& aBaseDir) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ DirectoryPaddingFileExists(aBaseDir, DirPaddingFile::TMP_FILE));
+
+ QM_TRY_INSPECT(
+ const auto& file,
+ CloneFileAndAppend(aBaseDir, nsLiteralString(PADDING_TMP_FILE_NAME)));
+
+ QM_TRY(MOZ_TO_RESULT(
+ file->RenameTo(nullptr, nsLiteralString(PADDING_FILE_NAME))));
+
+ return NS_OK;
+}
+
+Result<int64_t, nsresult> DirectoryPaddingRestore(nsIFile& aBaseDir,
+ mozIStorageConnection& aConn,
+ const bool aMustRestore) {
+ // The content of padding file is untrusted, so remove it here.
+ QM_TRY(MOZ_TO_RESULT(
+ DirectoryPaddingDeleteFile(aBaseDir, DirPaddingFile::FILE)));
+
+ QM_TRY_INSPECT(const int64_t& paddingSize, db::FindOverallPaddingSize(aConn));
+ MOZ_DIAGNOSTIC_ASSERT(paddingSize >= 0);
+
+ QM_TRY(MOZ_TO_RESULT(DirectoryPaddingWrite(aBaseDir, DirPaddingFile::FILE,
+ paddingSize)),
+ (aMustRestore ? Err(tryTempError)
+ : Result<int64_t, nsresult>{paddingSize}));
+
+ QM_TRY(MOZ_TO_RESULT(
+ DirectoryPaddingDeleteFile(aBaseDir, DirPaddingFile::TMP_FILE)));
+
+ return paddingSize;
+}
+
+nsresult DirectoryPaddingDeleteFile(nsIFile& aBaseDir,
+ DirPaddingFile aPaddingFileType) {
+ QM_TRY_INSPECT(
+ const auto& file,
+ CloneFileAndAppend(aBaseDir, aPaddingFileType == DirPaddingFile::TMP_FILE
+ ? nsLiteralString(PADDING_TMP_FILE_NAME)
+ : nsLiteralString(PADDING_FILE_NAME)));
+
+ QM_TRY(QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT(file->Remove(/* recursive */ false)),
+ // Predicate.
+ IsFileNotFoundError,
+ // Fallback.
+ ErrToDefaultOk<>));
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::cache