summaryrefslogtreecommitdiffstats
path: root/dom/fs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/fs/.clang-format36
-rw-r--r--dom/fs/api/FileSystemDirectoryHandle.cpp176
-rw-r--r--dom/fs/api/FileSystemDirectoryHandle.h86
-rw-r--r--dom/fs/api/FileSystemDirectoryIterator.cpp53
-rw-r--r--dom/fs/api/FileSystemDirectoryIterator.h74
-rw-r--r--dom/fs/api/FileSystemFileHandle.cpp122
-rw-r--r--dom/fs/api/FileSystemFileHandle.h61
-rw-r--r--dom/fs/api/FileSystemHandle.cpp318
-rw-r--r--dom/fs/api/FileSystemHandle.h107
-rw-r--r--dom/fs/api/FileSystemManager.cpp153
-rw-r--r--dom/fs/api/FileSystemManager.h102
-rw-r--r--dom/fs/api/FileSystemSyncAccessHandle.cpp645
-rw-r--r--dom/fs/api/FileSystemSyncAccessHandle.h133
-rw-r--r--dom/fs/api/FileSystemWritableFileStream.cpp1043
-rw-r--r--dom/fs/api/FileSystemWritableFileStream.h158
-rw-r--r--dom/fs/api/moz.build38
-rw-r--r--dom/fs/child/FileSystemAccessHandleChild.cpp34
-rw-r--r--dom/fs/child/FileSystemAccessHandleChild.h41
-rw-r--r--dom/fs/child/FileSystemAccessHandleControlChild.cpp37
-rw-r--r--dom/fs/child/FileSystemAccessHandleControlChild.h47
-rw-r--r--dom/fs/child/FileSystemAsyncCopy.cpp66
-rw-r--r--dom/fs/child/FileSystemBackgroundRequestHandler.cpp142
-rw-r--r--dom/fs/child/FileSystemBackgroundRequestHandler.h73
-rw-r--r--dom/fs/child/FileSystemChildFactory.cpp19
-rw-r--r--dom/fs/child/FileSystemDirectoryIteratorFactory.cpp243
-rw-r--r--dom/fs/child/FileSystemDirectoryIteratorFactory.h24
-rw-r--r--dom/fs/child/FileSystemEntryMetadataArray.h26
-rw-r--r--dom/fs/child/FileSystemManagerChild.cpp135
-rw-r--r--dom/fs/child/FileSystemManagerChild.h62
-rw-r--r--dom/fs/child/FileSystemRequestHandler.cpp641
-rw-r--r--dom/fs/child/FileSystemShutdownBlocker.cpp167
-rw-r--r--dom/fs/child/FileSystemThreadSafeStreamOwner.cpp104
-rw-r--r--dom/fs/child/FileSystemWritableFileStreamChild.cpp39
-rw-r--r--dom/fs/child/FileSystemWritableFileStreamChild.h42
-rw-r--r--dom/fs/child/moz.build35
-rw-r--r--dom/fs/include/fs/FileSystemAsyncCopy.h27
-rw-r--r--dom/fs/include/fs/FileSystemChildFactory.h31
-rw-r--r--dom/fs/include/fs/FileSystemConstants.h22
-rw-r--r--dom/fs/include/fs/FileSystemRequestHandler.h94
-rw-r--r--dom/fs/include/fs/FileSystemShutdownBlocker.h33
-rw-r--r--dom/fs/include/fs/FileSystemThreadSafeStreamOwner.h52
-rw-r--r--dom/fs/moz.build14
-rw-r--r--dom/fs/parent/FileSystemAccessHandle.cpp235
-rw-r--r--dom/fs/parent/FileSystemAccessHandle.h107
-rw-r--r--dom/fs/parent/FileSystemAccessHandleControlParent.cpp45
-rw-r--r--dom/fs/parent/FileSystemAccessHandleControlParent.h43
-rw-r--r--dom/fs/parent/FileSystemAccessHandleParent.cpp37
-rw-r--r--dom/fs/parent/FileSystemAccessHandleParent.h38
-rw-r--r--dom/fs/parent/FileSystemContentTypeGuess.cpp32
-rw-r--r--dom/fs/parent/FileSystemContentTypeGuess.h23
-rw-r--r--dom/fs/parent/FileSystemHashSource.cpp67
-rw-r--r--dom/fs/parent/FileSystemHashSource.h31
-rw-r--r--dom/fs/parent/FileSystemHashStorageFunction.cpp69
-rw-r--r--dom/fs/parent/FileSystemHashStorageFunction.h25
-rw-r--r--dom/fs/parent/FileSystemManagerParent.cpp519
-rw-r--r--dom/fs/parent/FileSystemManagerParent.h93
-rw-r--r--dom/fs/parent/FileSystemManagerParentFactory.cpp114
-rw-r--r--dom/fs/parent/FileSystemManagerParentFactory.h39
-rw-r--r--dom/fs/parent/FileSystemParentTypes.h46
-rw-r--r--dom/fs/parent/FileSystemQuotaClient.cpp167
-rw-r--r--dom/fs/parent/FileSystemQuotaClient.h69
-rw-r--r--dom/fs/parent/FileSystemQuotaClientFactory.cpp49
-rw-r--r--dom/fs/parent/FileSystemQuotaClientFactory.h49
-rw-r--r--dom/fs/parent/FileSystemStreamCallbacks.cpp38
-rw-r--r--dom/fs/parent/FileSystemStreamCallbacks.h38
-rw-r--r--dom/fs/parent/FileSystemWritableFileStreamParent.cpp85
-rw-r--r--dom/fs/parent/FileSystemWritableFileStreamParent.h62
-rw-r--r--dom/fs/parent/ResultConnection.h19
-rw-r--r--dom/fs/parent/ResultStatement.cpp23
-rw-r--r--dom/fs/parent/ResultStatement.h173
-rw-r--r--dom/fs/parent/StartedTransaction.cpp35
-rw-r--r--dom/fs/parent/StartedTransaction.h39
-rw-r--r--dom/fs/parent/datamodel/FileSystemDataManager.cpp672
-rw-r--r--dom/fs/parent/datamodel/FileSystemDataManager.h186
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp101
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManager.h229
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp1567
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h208
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp832
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.h75
-rw-r--r--dom/fs/parent/datamodel/FileSystemFileManager.cpp393
-rw-r--r--dom/fs/parent/datamodel/FileSystemFileManager.h175
-rw-r--r--dom/fs/parent/datamodel/SchemaVersion001.cpp193
-rw-r--r--dom/fs/parent/datamodel/SchemaVersion001.h31
-rw-r--r--dom/fs/parent/datamodel/SchemaVersion002.cpp618
-rw-r--r--dom/fs/parent/datamodel/SchemaVersion002.h28
-rw-r--r--dom/fs/parent/datamodel/moz.build28
-rw-r--r--dom/fs/parent/moz.build63
-rw-r--r--dom/fs/parent/rust/data-encoding-ffi/Cargo.toml9
-rw-r--r--dom/fs/parent/rust/data-encoding-ffi/cbindgen.toml11
-rw-r--r--dom/fs/parent/rust/data-encoding-ffi/src/lib.rs14
-rw-r--r--dom/fs/parent/rust/mime-guess-ffi/Cargo.toml10
-rw-r--r--dom/fs/parent/rust/mime-guess-ffi/cbindgen.toml11
-rw-r--r--dom/fs/parent/rust/mime-guess-ffi/src/lib.rs34
-rw-r--r--dom/fs/shared/FileSystemHelpers.cpp22
-rw-r--r--dom/fs/shared/FileSystemHelpers.h147
-rw-r--r--dom/fs/shared/FileSystemLog.cpp13
-rw-r--r--dom/fs/shared/FileSystemLog.h23
-rw-r--r--dom/fs/shared/FileSystemTypes.h29
-rw-r--r--dom/fs/shared/IPCRejectReporter.cpp36
-rw-r--r--dom/fs/shared/IPCRejectReporter.h20
-rw-r--r--dom/fs/shared/ManagedMozPromiseRequestHolder.h37
-rw-r--r--dom/fs/shared/PFileSystemAccessHandle.ipdl21
-rw-r--r--dom/fs/shared/PFileSystemAccessHandleControl.ipdl19
-rw-r--r--dom/fs/shared/PFileSystemManager.ipdl413
-rw-r--r--dom/fs/shared/PFileSystemWritableFileStream.ipdl23
-rw-r--r--dom/fs/shared/TargetPtrHolder.h59
-rw-r--r--dom/fs/shared/moz.build34
-rw-r--r--dom/fs/test/common/.eslintrc.js14
-rw-r--r--dom/fs/test/common/dummy.js0
-rw-r--r--dom/fs/test/common/mochitest.toml15
-rw-r--r--dom/fs/test/common/moz.build21
-rw-r--r--dom/fs/test/common/nsresult.js9
-rw-r--r--dom/fs/test/common/test_basics.js375
-rw-r--r--dom/fs/test/common/test_fileSystemDirectoryHandle.js196
-rw-r--r--dom/fs/test/common/test_syncAccessHandle.js237
-rw-r--r--dom/fs/test/common/test_writableFileStream.js153
-rw-r--r--dom/fs/test/common/xpcshell.toml10
-rw-r--r--dom/fs/test/crashtests/1798773.html19
-rw-r--r--dom/fs/test/crashtests/1800470.html28
-rw-r--r--dom/fs/test/crashtests/1809759.html10
-rw-r--r--dom/fs/test/crashtests/1816710.html8
-rw-r--r--dom/fs/test/crashtests/1841702.html16
-rw-r--r--dom/fs/test/crashtests/1844619.html11
-rw-r--r--dom/fs/test/crashtests/1858820.html19
-rw-r--r--dom/fs/test/crashtests/crashtests.list10
-rw-r--r--dom/fs/test/crashtests/sw1844619.js21
-rw-r--r--dom/fs/test/gtest/FileSystemMocks.cpp94
-rw-r--r--dom/fs/test/gtest/FileSystemMocks.h340
-rw-r--r--dom/fs/test/gtest/TestHelpers.cpp61
-rw-r--r--dom/fs/test/gtest/TestHelpers.h69
-rw-r--r--dom/fs/test/gtest/api/TestFileSystemDirectoryHandle.cpp234
-rw-r--r--dom/fs/test/gtest/api/TestFileSystemFileHandle.cpp144
-rw-r--r--dom/fs/test/gtest/api/TestFileSystemHandle.cpp131
-rw-r--r--dom/fs/test/gtest/api/moz.build21
-rw-r--r--dom/fs/test/gtest/child/TestFileSystemBackgroundRequestHandler.cpp64
-rw-r--r--dom/fs/test/gtest/child/TestFileSystemRequestHandler.cpp352
-rw-r--r--dom/fs/test/gtest/child/moz.build20
-rw-r--r--dom/fs/test/gtest/moz.build25
-rw-r--r--dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp183
-rw-r--r--dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp474
-rw-r--r--dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp185
-rw-r--r--dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp1059
-rw-r--r--dom/fs/test/gtest/parent/datamodel/moz.build22
-rw-r--r--dom/fs/test/gtest/parent/moz.build21
-rw-r--r--dom/fs/test/gtest/shared/TestFileSystemHelpers.cpp179
-rw-r--r--dom/fs/test/gtest/shared/moz.build16
-rw-r--r--dom/fs/test/mochitest/head.js82
-rw-r--r--dom/fs/test/mochitest/mochitest.toml35
-rw-r--r--dom/fs/test/mochitest/test_basics.html28
-rw-r--r--dom/fs/test/mochitest/test_basics_worker.html22
-rw-r--r--dom/fs/test/mochitest/test_fileSystemDirectoryHandle.html28
-rw-r--r--dom/fs/test/mochitest/test_fileSystemDirectoryHandle_worker.html22
-rw-r--r--dom/fs/test/mochitest/test_syncAccessHandle_worker.html22
-rw-r--r--dom/fs/test/mochitest/test_writableFileStream.html31
-rw-r--r--dom/fs/test/mochitest/test_writableFileStream_worker.html22
-rw-r--r--dom/fs/test/mochitest/worker/.eslintrc.js12
-rw-r--r--dom/fs/test/mochitest/worker/dummy.js0
-rw-r--r--dom/fs/test/mochitest/worker/head.js22
-rw-r--r--dom/fs/test/mochitest/worker/mochitest.toml15
-rw-r--r--dom/fs/test/mochitest/worker/test_basics_worker.js11
-rw-r--r--dom/fs/test/mochitest/worker/test_fileSystemDirectoryHandle_worker.js13
-rw-r--r--dom/fs/test/mochitest/worker/test_syncAccessHandle_worker.js16
-rw-r--r--dom/fs/test/mochitest/worker/test_writableFileStream_worker.js16
-rw-r--r--dom/fs/test/moz.build16
-rw-r--r--dom/fs/test/xpcshell/head.js100
-rw-r--r--dom/fs/test/xpcshell/moz.build13
-rw-r--r--dom/fs/test/xpcshell/test_basics.js14
-rw-r--r--dom/fs/test/xpcshell/test_basics_worker.js8
-rw-r--r--dom/fs/test/xpcshell/test_fileSystemDirectoryHandle.js14
-rw-r--r--dom/fs/test/xpcshell/test_fileSystemDirectoryHandle_worker.js8
-rw-r--r--dom/fs/test/xpcshell/test_syncAccessHandle_worker.js8
-rw-r--r--dom/fs/test/xpcshell/test_writableFileStream.js14
-rw-r--r--dom/fs/test/xpcshell/test_writableFileStream_worker.js8
-rw-r--r--dom/fs/test/xpcshell/worker/.eslintrc.js12
-rw-r--r--dom/fs/test/xpcshell/worker/dummy.js0
-rw-r--r--dom/fs/test/xpcshell/worker/head.js22
-rw-r--r--dom/fs/test/xpcshell/worker/moz.build13
-rw-r--r--dom/fs/test/xpcshell/worker/test_basics_worker.js11
-rw-r--r--dom/fs/test/xpcshell/worker/test_fileSystemDirectoryHandle_worker.js13
-rw-r--r--dom/fs/test/xpcshell/worker/test_syncAccessHandle_worker.js13
-rw-r--r--dom/fs/test/xpcshell/worker/test_writableFileStream_worker.js13
-rw-r--r--dom/fs/test/xpcshell/xpcshell.toml16
183 files changed, 19927 insertions, 0 deletions
diff --git a/dom/fs/.clang-format b/dom/fs/.clang-format
new file mode 100644
index 0000000000..68a99f4e52
--- /dev/null
+++ b/dom/fs/.clang-format
@@ -0,0 +1,36 @@
+BasedOnStyle: Google
+
+# Prevent the loss of indentation with these macros
+MacroBlockBegin: "^\
+JS_BEGIN_MACRO|\
+NS_INTERFACE_MAP_BEGIN|\
+NS_INTERFACE_TABLE_HEAD|\
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION|\
+NS_IMPL_CYCLE_COLLECTION_.*_BEGIN|\
+NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED|\
+NS_INTERFACE_TABLE_BEGIN|\
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED|\
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED|\
+NS_QUERYFRAME_HEAD$"
+MacroBlockEnd: "^\
+JS_END_MACRO|\
+NS_INTERFACE_MAP_END|\
+NS_IMPL_CYCLE_COLLECTION_.*_END|\
+NS_INTERFACE_TABLE_END|\
+NS_INTERFACE_TABLE_TAIL.*|\
+NS_INTERFACE_MAP_END_.*|\
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END_INHERITED|\
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED|\
+NS_QUERYFRAME_TAIL.*$"
+
+SortIncludes: true
+IndentPPDirectives: AfterHash
+StatementMacros: [MARKUPMAP, ASSERT_TRUE, ASSERT_FALSE, TEST, CHECK]
+
+# The Google coding style states:
+# You should do this consistently within a single file, so, when modifying an
+# existing file, use the style in that file.
+# Let's be more prescriptive and default to the one used in the Mozilla
+# coding style
+DerivePointerAlignment: false
+PointerAlignment: Left
diff --git a/dom/fs/api/FileSystemDirectoryHandle.cpp b/dom/fs/api/FileSystemDirectoryHandle.cpp
new file mode 100644
index 0000000000..3529f29e78
--- /dev/null
+++ b/dom/fs/api/FileSystemDirectoryHandle.cpp
@@ -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/. */
+
+#include "FileSystemDirectoryHandle.h"
+
+#include "FileSystemDirectoryIteratorFactory.h"
+#include "fs/FileSystemRequestHandler.h"
+#include "js/StructuredClone.h"
+#include "js/TypeDecls.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/FileSystemDirectoryHandleBinding.h"
+#include "mozilla/dom/FileSystemHandleBinding.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/StorageManager.h"
+#include "nsJSUtils.h"
+
+namespace mozilla::dom {
+
+FileSystemDirectoryHandle::FileSystemDirectoryHandle(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ const fs::FileSystemEntryMetadata& aMetadata,
+ fs::FileSystemRequestHandler* aRequestHandler)
+ : FileSystemHandle(aGlobal, aManager, aMetadata, aRequestHandler) {}
+
+FileSystemDirectoryHandle::FileSystemDirectoryHandle(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ const fs::FileSystemEntryMetadata& aMetadata)
+ : FileSystemDirectoryHandle(aGlobal, aManager, aMetadata,
+ new fs::FileSystemRequestHandler()) {}
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(FileSystemDirectoryHandle,
+ FileSystemHandle)
+NS_IMPL_CYCLE_COLLECTION_INHERITED(FileSystemDirectoryHandle, FileSystemHandle)
+
+// WebIDL Boilerplate
+
+JSObject* FileSystemDirectoryHandle::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return FileSystemDirectoryHandle_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// WebIDL Interface
+
+FileSystemHandleKind FileSystemDirectoryHandle::Kind() const {
+ return FileSystemHandleKind::Directory;
+}
+
+void FileSystemDirectoryHandle::InitAsyncIteratorData(
+ IteratorData& aData, iterator_t::IteratorType aType, ErrorResult& aError) {
+ aData.mImpl =
+ fs::FileSystemDirectoryIteratorFactory::Create(mMetadata, aType);
+}
+
+already_AddRefed<Promise> FileSystemDirectoryHandle::GetNextIterationResult(
+ FileSystemDirectoryHandle::iterator_t* aIterator, ErrorResult& aError) {
+ LOG_VERBOSE(("GetNextIterationResult"));
+ return aIterator->Data().mImpl->Next(mGlobal, mManager, aError);
+}
+
+already_AddRefed<Promise> FileSystemDirectoryHandle::GetFileHandle(
+ const nsAString& aName, const FileSystemGetFileOptions& aOptions,
+ ErrorResult& aError) {
+ MOZ_ASSERT(!mMetadata.entryId().IsEmpty());
+
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ fs::Name name(aName);
+ fs::FileSystemChildMetadata metadata(mMetadata.entryId(), name);
+ mRequestHandler->GetFileHandle(mManager, metadata, aOptions.mCreate, promise,
+ aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> FileSystemDirectoryHandle::GetDirectoryHandle(
+ const nsAString& aName, const FileSystemGetDirectoryOptions& aOptions,
+ ErrorResult& aError) {
+ MOZ_ASSERT(!mMetadata.entryId().IsEmpty());
+
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ fs::Name name(aName);
+ fs::FileSystemChildMetadata metadata(mMetadata.entryId(), name);
+ mRequestHandler->GetDirectoryHandle(mManager, metadata, aOptions.mCreate,
+ promise, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> FileSystemDirectoryHandle::RemoveEntry(
+ const nsAString& aName, const FileSystemRemoveOptions& aOptions,
+ ErrorResult& aError) {
+ MOZ_ASSERT(!mMetadata.entryId().IsEmpty());
+
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ fs::Name name(aName);
+ fs::FileSystemChildMetadata metadata(mMetadata.entryId(), name);
+
+ mRequestHandler->RemoveEntry(mManager, metadata, aOptions.mRecursive, promise,
+ aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> FileSystemDirectoryHandle::Resolve(
+ FileSystemHandle& aPossibleDescendant, ErrorResult& aError) {
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ LOG_VERBOSE(("Resolve"));
+
+ fs::FileSystemEntryPair pair(mMetadata.entryId(),
+ aPossibleDescendant.GetId());
+ mRequestHandler->Resolve(mManager, pair, promise, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+// [Serializable] implementation
+
+// static
+already_AddRefed<FileSystemDirectoryHandle>
+FileSystemDirectoryHandle::ReadStructuredClone(
+ JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader) {
+ uint32_t kind = static_cast<uint32_t>(FileSystemHandleKind::EndGuard_);
+
+ if (!JS_ReadBytes(aReader, reinterpret_cast<void*>(&kind),
+ sizeof(uint32_t))) {
+ return nullptr;
+ }
+
+ if (kind != static_cast<uint32_t>(FileSystemHandleKind::Directory)) {
+ return nullptr;
+ }
+
+ RefPtr<FileSystemDirectoryHandle> result =
+ FileSystemHandle::ConstructDirectoryHandle(aCx, aGlobal, aReader);
+ if (!result) {
+ return nullptr;
+ }
+
+ return result.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/api/FileSystemDirectoryHandle.h b/dom/fs/api/FileSystemDirectoryHandle.h
new file mode 100644
index 0000000000..1c7af3f52c
--- /dev/null
+++ b/dom/fs/api/FileSystemDirectoryHandle.h
@@ -0,0 +1,86 @@
+/* -*- 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_FS_FILESYSTEMDIRECTORYHANDLE_H_
+#define DOM_FS_FILESYSTEMDIRECTORYHANDLE_H_
+
+#include "mozilla/dom/FileSystemDirectoryIterator.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/IterableIterator.h"
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+struct FileSystemGetFileOptions;
+struct FileSystemGetDirectoryOptions;
+struct FileSystemRemoveOptions;
+
+class FileSystemDirectoryHandle final : public FileSystemHandle {
+ public:
+ using iterator_t = AsyncIterableIterator<FileSystemDirectoryHandle>;
+
+ FileSystemDirectoryHandle(nsIGlobalObject* aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ const fs::FileSystemEntryMetadata& aMetadata,
+ fs::FileSystemRequestHandler* aRequestHandler);
+
+ FileSystemDirectoryHandle(nsIGlobalObject* aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ const fs::FileSystemEntryMetadata& aMetadata);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FileSystemDirectoryHandle,
+ FileSystemHandle)
+
+ // WebIDL Boilerplate
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL Interface
+ FileSystemHandleKind Kind() const override;
+
+ struct IteratorData {
+ RefPtr<FileSystemDirectoryIterator::Impl> mImpl;
+ };
+
+ void InitAsyncIteratorData(IteratorData& aData,
+ iterator_t::IteratorType aType,
+ ErrorResult& aError);
+
+ [[nodiscard]] already_AddRefed<Promise> GetNextIterationResult(
+ iterator_t* aIterator, ErrorResult& aError);
+
+ already_AddRefed<Promise> GetFileHandle(
+ const nsAString& aName, const FileSystemGetFileOptions& aOptions,
+ ErrorResult& aError);
+
+ already_AddRefed<Promise> GetDirectoryHandle(
+ const nsAString& aName, const FileSystemGetDirectoryOptions& aOptions,
+ ErrorResult& aError);
+
+ already_AddRefed<Promise> RemoveEntry(const nsAString& aName,
+ const FileSystemRemoveOptions& aOptions,
+ ErrorResult& aError);
+
+ already_AddRefed<Promise> Resolve(FileSystemHandle& aPossibleDescendant,
+ ErrorResult& aError);
+
+ // [Serializable]
+ static already_AddRefed<FileSystemDirectoryHandle> ReadStructuredClone(
+ JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader);
+
+ private:
+ ~FileSystemDirectoryHandle() = default;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_FILESYSTEMDIRECTORYHANDLE_H_
diff --git a/dom/fs/api/FileSystemDirectoryIterator.cpp b/dom/fs/api/FileSystemDirectoryIterator.cpp
new file mode 100644
index 0000000000..663d5ecd57
--- /dev/null
+++ b/dom/fs/api/FileSystemDirectoryIterator.cpp
@@ -0,0 +1,53 @@
+/* -*- 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 "FileSystemDirectoryIterator.h"
+
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/FileSystemDirectoryIteratorBinding.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/Promise.h"
+
+namespace mozilla::dom {
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemDirectoryIterator)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FileSystemDirectoryIterator);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(FileSystemDirectoryIterator);
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FileSystemDirectoryIterator, mGlobal);
+
+FileSystemDirectoryIterator::FileSystemDirectoryIterator(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ RefPtr<Impl>& aImpl)
+ : mGlobal(aGlobal), mManager(aManager), mImpl(aImpl) {}
+
+// WebIDL Boilerplate
+
+nsIGlobalObject* FileSystemDirectoryIterator::GetParentObject() const {
+ return mGlobal;
+}
+
+JSObject* FileSystemDirectoryIterator::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return FileSystemDirectoryIterator_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// WebIDL Interface
+
+already_AddRefed<Promise> FileSystemDirectoryIterator::Next(
+ ErrorResult& aError) {
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(mImpl);
+ return mImpl->Next(mGlobal, mManager, aError);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/api/FileSystemDirectoryIterator.h b/dom/fs/api/FileSystemDirectoryIterator.h
new file mode 100644
index 0000000000..5357106e8b
--- /dev/null
+++ b/dom/fs/api/FileSystemDirectoryIterator.h
@@ -0,0 +1,74 @@
+/* -*- 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_FS_FILESYSTEMDIRECTORYITERATOR_H_
+#define DOM_FS_FILESYSTEMDIRECTORYITERATOR_H_
+
+#include "mozilla/dom/IterableIterator.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+class FileSystemManager;
+class IterableIteratorBase;
+class Promise;
+
+// XXX This class isn't used to support iteration anymore. `Impl` should be
+// extracted elsewhere and `FileSystemDirectoryIterator` should be removed
+// completely
+class FileSystemDirectoryIterator : public nsISupports, public nsWrapperCache {
+ public:
+ class Impl {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(Impl)
+
+ virtual already_AddRefed<Promise> Next(nsIGlobalObject* aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ ErrorResult& aError) = 0;
+
+ protected:
+ virtual ~Impl() = default;
+ };
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemDirectoryIterator)
+
+ explicit FileSystemDirectoryIterator(nsIGlobalObject* aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ RefPtr<Impl>& aImpl);
+
+ // WebIDL Boilerplate
+ nsIGlobalObject* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL Interface
+ already_AddRefed<Promise> Next(ErrorResult& aError);
+
+ protected:
+ virtual ~FileSystemDirectoryIterator() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+
+ RefPtr<FileSystemManager> mManager;
+
+ private:
+ RefPtr<Impl> mImpl;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_FILESYSTEMDIRECTORYITERATOR_H_
diff --git a/dom/fs/api/FileSystemFileHandle.cpp b/dom/fs/api/FileSystemFileHandle.cpp
new file mode 100644
index 0000000000..4d8306857f
--- /dev/null
+++ b/dom/fs/api/FileSystemFileHandle.cpp
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemFileHandle.h"
+
+#include "fs/FileSystemRequestHandler.h"
+#include "js/StructuredClone.h"
+#include "js/TypeDecls.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/FileSystemFileHandleBinding.h"
+#include "mozilla/dom/FileSystemHandleBinding.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/Promise.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(FileSystemFileHandle,
+ FileSystemHandle)
+NS_IMPL_CYCLE_COLLECTION_INHERITED(FileSystemFileHandle, FileSystemHandle)
+
+FileSystemFileHandle::FileSystemFileHandle(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ const fs::FileSystemEntryMetadata& aMetadata,
+ fs::FileSystemRequestHandler* aRequestHandler)
+ : FileSystemHandle(aGlobal, aManager, aMetadata, aRequestHandler) {}
+
+FileSystemFileHandle::FileSystemFileHandle(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ const fs::FileSystemEntryMetadata& aMetadata)
+ : FileSystemFileHandle(aGlobal, aManager, aMetadata,
+ new fs::FileSystemRequestHandler()) {}
+
+// WebIDL Boilerplate
+
+JSObject* FileSystemFileHandle::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return FileSystemFileHandle_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// WebIDL Interface
+
+FileSystemHandleKind FileSystemFileHandle::Kind() const {
+ return FileSystemHandleKind::File;
+}
+
+already_AddRefed<Promise> FileSystemFileHandle::GetFile(ErrorResult& aError) {
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ mRequestHandler->GetFile(mManager, mMetadata, promise, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> FileSystemFileHandle::CreateWritable(
+ const FileSystemCreateWritableOptions& aOptions, ErrorResult& aError) {
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ mRequestHandler->GetWritable(mManager, mMetadata, aOptions.mKeepExistingData,
+ promise, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> FileSystemFileHandle::CreateSyncAccessHandle(
+ ErrorResult& aError) {
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ mRequestHandler->GetAccessHandle(mManager, mMetadata, promise, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+// [Serializable] implementation
+
+// static
+already_AddRefed<FileSystemFileHandle>
+FileSystemFileHandle::ReadStructuredClone(JSContext* aCx,
+ nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader) {
+ uint32_t kind = static_cast<uint32_t>(FileSystemHandleKind::EndGuard_);
+
+ if (!JS_ReadBytes(aReader, reinterpret_cast<void*>(&kind),
+ sizeof(uint32_t))) {
+ return nullptr;
+ }
+
+ if (kind != static_cast<uint32_t>(FileSystemHandleKind::File)) {
+ return nullptr;
+ }
+
+ RefPtr<FileSystemFileHandle> result =
+ FileSystemHandle::ConstructFileHandle(aCx, aGlobal, aReader);
+ if (!result) {
+ return nullptr;
+ }
+
+ return result.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/api/FileSystemFileHandle.h b/dom/fs/api/FileSystemFileHandle.h
new file mode 100644
index 0000000000..c0606c85a6
--- /dev/null
+++ b/dom/fs/api/FileSystemFileHandle.h
@@ -0,0 +1,61 @@
+/* -*- 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_FS_FILESYSTEMFILEHANDLE_H_
+#define DOM_FS_FILESYSTEMFILEHANDLE_H_
+
+#include "mozilla/dom/FileSystemHandle.h"
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+struct FileSystemCreateWritableOptions;
+
+class FileSystemFileHandle final : public FileSystemHandle {
+ public:
+ FileSystemFileHandle(nsIGlobalObject* aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ const fs::FileSystemEntryMetadata& aMetadata,
+ fs::FileSystemRequestHandler* aRequestHandler);
+
+ FileSystemFileHandle(nsIGlobalObject* aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ const fs::FileSystemEntryMetadata& aMetadata);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FileSystemFileHandle,
+ FileSystemHandle)
+
+ // WebIDL Boilerplate
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL interface
+ FileSystemHandleKind Kind() const override;
+
+ already_AddRefed<Promise> GetFile(ErrorResult& aError);
+
+ already_AddRefed<Promise> CreateWritable(
+ const FileSystemCreateWritableOptions& aOptions, ErrorResult& aError);
+
+ already_AddRefed<Promise> CreateSyncAccessHandle(ErrorResult& aError);
+
+ // [Serializable]
+ static already_AddRefed<FileSystemFileHandle> ReadStructuredClone(
+ JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader);
+
+ private:
+ ~FileSystemFileHandle() = default;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_FILESYSTEMFILEHANDLE_H_
diff --git a/dom/fs/api/FileSystemHandle.cpp b/dom/fs/api/FileSystemHandle.cpp
new file mode 100644
index 0000000000..d0accdea00
--- /dev/null
+++ b/dom/fs/api/FileSystemHandle.cpp
@@ -0,0 +1,318 @@
+/* -*- 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 "FileSystemHandle.h"
+
+#include "FileSystemDirectoryHandle.h"
+#include "FileSystemFileHandle.h"
+#include "fs/FileSystemRequestHandler.h"
+#include "js/StructuredClone.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/FileSystemHandleBinding.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/Promise-inl.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/StorageManager.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "nsJSPrincipals.h"
+#include "nsString.h"
+#include "prio.h"
+#include "private/pprio.h"
+#include "xpcpublic.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+bool ConstructHandleMetadata(JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader,
+ const bool aDirectory,
+ fs::FileSystemEntryMetadata& aMetadata) {
+ using namespace mozilla::dom::fs;
+
+ EntryId entryId;
+ if (!entryId.SetLength(32u, fallible)) {
+ return false;
+ }
+
+ if (!JS_ReadBytes(aReader, static_cast<void*>(entryId.BeginWriting()), 32u)) {
+ return false;
+ }
+
+ Name name;
+ if (!StructuredCloneHolder::ReadString(aReader, name)) {
+ return false;
+ }
+
+ mozilla::ipc::PrincipalInfo storageKey;
+ if (!nsJSPrincipals::ReadPrincipalInfo(aReader, storageKey)) {
+ return false;
+ }
+
+ QM_TRY_UNWRAP(auto hasEqualStorageKey,
+ aGlobal->HasEqualStorageKey(storageKey), false);
+
+ if (!hasEqualStorageKey) {
+ LOG(("Blocking deserialization of %s due to cross-origin",
+ NS_ConvertUTF16toUTF8(name).get()));
+ return false;
+ }
+
+ LOG_VERBOSE(("Deserializing %s", NS_ConvertUTF16toUTF8(name).get()));
+
+ aMetadata = fs::FileSystemEntryMetadata(entryId, name, aDirectory);
+ return true;
+}
+
+} // namespace
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemHandle)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FileSystemHandle)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(FileSystemHandle)
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemHandle)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(FileSystemHandle)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
+ // Don't unlink mManager!
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(FileSystemHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mManager)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+FileSystemHandle::FileSystemHandle(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ const fs::FileSystemEntryMetadata& aMetadata,
+ fs::FileSystemRequestHandler* aRequestHandler)
+ : mGlobal(aGlobal),
+ mManager(aManager),
+ mMetadata(aMetadata),
+ mRequestHandler(aRequestHandler) {
+ MOZ_ASSERT(!mMetadata.entryId().IsEmpty());
+}
+
+// WebIDL Boilerplate
+
+nsIGlobalObject* FileSystemHandle::GetParentObject() const { return mGlobal; }
+
+JSObject* FileSystemHandle::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return FileSystemHandle_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// WebIDL Interface
+
+void FileSystemHandle::GetName(nsAString& aResult) {
+ aResult = mMetadata.entryName();
+}
+
+already_AddRefed<Promise> FileSystemHandle::IsSameEntry(
+ FileSystemHandle& aOther, ErrorResult& aError) const {
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ // Handles the case of "dir = createdir foo; removeEntry(foo); file =
+ // createfile foo; issameentry(dir, file)"
+ const bool result = mMetadata.entryId().Equals(aOther.mMetadata.entryId()) &&
+ Kind() == aOther.Kind();
+ promise->MaybeResolve(result);
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> FileSystemHandle::Move(const nsAString& aName,
+ ErrorResult& aError) {
+ LOG(("Move %s to %s", NS_ConvertUTF16toUTF8(mMetadata.entryName()).get(),
+ NS_ConvertUTF16toUTF8(aName).get()));
+
+ fs::EntryId parent; // empty means same directory
+ return Move(parent, aName, aError);
+}
+
+already_AddRefed<Promise> FileSystemHandle::Move(
+ FileSystemDirectoryHandle& aParent, ErrorResult& aError) {
+ LOG(("Move %s to %s/%s", NS_ConvertUTF16toUTF8(mMetadata.entryName()).get(),
+ NS_ConvertUTF16toUTF8(aParent.mMetadata.entryName()).get(),
+ NS_ConvertUTF16toUTF8(mMetadata.entryName()).get()));
+ return Move(aParent, mMetadata.entryName(), aError);
+}
+
+already_AddRefed<Promise> FileSystemHandle::Move(
+ FileSystemDirectoryHandle& aParent, const nsAString& aName,
+ ErrorResult& aError) {
+ LOG(("Move %s to %s/%s", NS_ConvertUTF16toUTF8(mMetadata.entryName()).get(),
+ NS_ConvertUTF16toUTF8(aParent.mMetadata.entryName()).get(),
+ NS_ConvertUTF16toUTF8(aName).get()));
+ return Move(aParent.mMetadata.entryId(), aName, aError);
+}
+
+already_AddRefed<Promise> FileSystemHandle::Move(const fs::EntryId& aParentId,
+ const nsAString& aName,
+ ErrorResult& aError) {
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ fs::FileSystemChildMetadata newMetadata;
+ newMetadata.parentId() = aParentId;
+ newMetadata.childName() = aName;
+ if (!aParentId.IsEmpty()) {
+ mRequestHandler->MoveEntry(mManager, this, &mMetadata, newMetadata, promise,
+ aError);
+ } else {
+ mRequestHandler->RenameEntry(mManager, this, &mMetadata,
+ newMetadata.childName(), promise, aError);
+ }
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ // Other handles to this will be broken, and the spec is ok with this, but we
+ // need to update our EntryId and name
+ promise->AddCallbacksWithCycleCollectedArgs(
+ [newMetadata](JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv, FileSystemHandle* aHandle) {
+ // XXX Fix entryId!
+ LOG(("Changing FileSystemHandle name from %s to %s",
+ NS_ConvertUTF16toUTF8(aHandle->mMetadata.entryName()).get(),
+ NS_ConvertUTF16toUTF8(newMetadata.childName()).get()));
+ aHandle->mMetadata.entryName() = newMetadata.childName();
+ },
+ [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv,
+ FileSystemHandle* aHandle) {
+ LOG(("reject of move for %s",
+ NS_ConvertUTF16toUTF8(aHandle->mMetadata.entryName()).get()));
+ },
+ RefPtr(this));
+
+ return promise.forget();
+}
+
+// [Serializable] implementation
+
+// static
+already_AddRefed<FileSystemHandle> FileSystemHandle::ReadStructuredClone(
+ JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader) {
+ LOG_VERBOSE(("Reading File/DirectoryHandle"));
+
+ uint32_t kind = static_cast<uint32_t>(FileSystemHandleKind::EndGuard_);
+
+ if (!JS_ReadBytes(aReader, reinterpret_cast<void*>(&kind),
+ sizeof(uint32_t))) {
+ return nullptr;
+ }
+
+ if (kind == static_cast<uint32_t>(FileSystemHandleKind::Directory)) {
+ RefPtr<FileSystemHandle> result =
+ FileSystemHandle::ConstructDirectoryHandle(aCx, aGlobal, aReader);
+ return result.forget();
+ }
+
+ if (kind == static_cast<uint32_t>(FileSystemHandleKind::File)) {
+ RefPtr<FileSystemHandle> result =
+ FileSystemHandle::ConstructFileHandle(aCx, aGlobal, aReader);
+ return result.forget();
+ }
+
+ return nullptr;
+}
+
+bool FileSystemHandle::WriteStructuredClone(
+ JSContext* aCx, JSStructuredCloneWriter* aWriter) const {
+ LOG_VERBOSE(("Writing File/DirectoryHandle"));
+ MOZ_ASSERT(mMetadata.entryId().Length() == 32);
+
+ auto kind = static_cast<uint32_t>(Kind());
+ if (NS_WARN_IF(!JS_WriteBytes(aWriter, static_cast<void*>(&kind),
+ sizeof(uint32_t)))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!JS_WriteBytes(
+ aWriter, static_cast<const void*>(mMetadata.entryId().get()),
+ mMetadata.entryId().Length()))) {
+ return false;
+ }
+
+ if (!StructuredCloneHolder::WriteString(aWriter, mMetadata.entryName())) {
+ return false;
+ }
+
+ // Needed to make sure the destination nsIGlobalObject is from the same
+ // origin/principal
+ QM_TRY_INSPECT(const auto& storageKey, mGlobal->GetStorageKey(), false);
+
+ return nsJSPrincipals::WritePrincipalInfo(aWriter, storageKey);
+}
+
+// static
+already_AddRefed<FileSystemFileHandle> FileSystemHandle::ConstructFileHandle(
+ JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader) {
+ LOG(("Reading FileHandle"));
+
+ fs::FileSystemEntryMetadata metadata;
+ if (!ConstructHandleMetadata(aCx, aGlobal, aReader, /* aDirectory */ false,
+ metadata)) {
+ return nullptr;
+ }
+
+ RefPtr<StorageManager> storageManager = aGlobal->GetStorageManager();
+ if (!storageManager) {
+ return nullptr;
+ }
+
+ // Note that the actor may not exist or may not be connected yet.
+ RefPtr<FileSystemManager> fileSystemManager =
+ storageManager->GetFileSystemManager();
+
+ RefPtr<FileSystemFileHandle> fsHandle =
+ new FileSystemFileHandle(aGlobal, fileSystemManager, metadata);
+
+ return fsHandle.forget();
+}
+
+// static
+already_AddRefed<FileSystemDirectoryHandle>
+FileSystemHandle::ConstructDirectoryHandle(JSContext* aCx,
+ nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader) {
+ LOG(("Reading DirectoryHandle"));
+
+ fs::FileSystemEntryMetadata metadata;
+ if (!ConstructHandleMetadata(aCx, aGlobal, aReader, /* aDirectory */ true,
+ metadata)) {
+ return nullptr;
+ }
+
+ RefPtr<StorageManager> storageManager = aGlobal->GetStorageManager();
+ if (!storageManager) {
+ return nullptr;
+ }
+
+ // Note that the actor may not exist or may not be connected yet.
+ RefPtr<FileSystemManager> fileSystemManager =
+ storageManager->GetFileSystemManager();
+
+ RefPtr<FileSystemDirectoryHandle> fsHandle =
+ new FileSystemDirectoryHandle(aGlobal, fileSystemManager, metadata);
+
+ return fsHandle.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/api/FileSystemHandle.h b/dom/fs/api/FileSystemHandle.h
new file mode 100644
index 0000000000..76ad66f5ea
--- /dev/null
+++ b/dom/fs/api/FileSystemHandle.h
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_FILESYSTEMHANDLE_H_
+#define DOM_FS_FILESYSTEMHANDLE_H_
+
+#include "mozilla/dom/PFileSystemManager.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+class FileSystemDirectoryHandle;
+class FileSystemFileHandle;
+enum class FileSystemHandleKind : uint8_t;
+class FileSystemManager;
+class FileSystemManagerChild;
+class Promise;
+
+namespace fs {
+class FileSystemRequestHandler;
+} // namespace fs
+
+class FileSystemHandle : public nsISupports, public nsWrapperCache {
+ public:
+ FileSystemHandle(nsIGlobalObject* aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ const fs::FileSystemEntryMetadata& aMetadata,
+ fs::FileSystemRequestHandler* aRequestHandler);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemHandle)
+
+ const fs::EntryId& GetId() const { return mMetadata.entryId(); }
+
+ // WebIDL Boilerplate
+ nsIGlobalObject* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL Interface
+ virtual FileSystemHandleKind Kind() const = 0;
+
+ void GetName(nsAString& aResult);
+
+ already_AddRefed<Promise> IsSameEntry(FileSystemHandle& aOther,
+ ErrorResult& aError) const;
+
+ // [Serializable] implementation
+ static already_AddRefed<FileSystemHandle> ReadStructuredClone(
+ JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader);
+
+ virtual bool WriteStructuredClone(JSContext* aCx,
+ JSStructuredCloneWriter* aWriter) const;
+
+ already_AddRefed<Promise> Move(const nsAString& aName, ErrorResult& aError);
+
+ already_AddRefed<Promise> Move(FileSystemDirectoryHandle& aParent,
+ ErrorResult& aError);
+
+ already_AddRefed<Promise> Move(FileSystemDirectoryHandle& aParent,
+ const nsAString& aName, ErrorResult& aError);
+
+ already_AddRefed<Promise> Move(const fs::EntryId& aParentId,
+ const nsAString& aName, ErrorResult& aError);
+
+ void UpdateMetadata(const fs::FileSystemEntryMetadata& aMetadata) {
+ mMetadata = aMetadata;
+ }
+
+ protected:
+ virtual ~FileSystemHandle() = default;
+
+ static already_AddRefed<FileSystemFileHandle> ConstructFileHandle(
+ JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader);
+
+ static already_AddRefed<FileSystemDirectoryHandle> ConstructDirectoryHandle(
+ JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader);
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+
+ RefPtr<FileSystemManager> mManager;
+
+ // move() can change names/directories
+ fs::FileSystemEntryMetadata mMetadata;
+
+ const UniquePtr<fs::FileSystemRequestHandler> mRequestHandler;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_FILESYSTEMHANDLE_H_
diff --git a/dom/fs/api/FileSystemManager.cpp b/dom/fs/api/FileSystemManager.cpp
new file mode 100644
index 0000000000..099739f8a1
--- /dev/null
+++ b/dom/fs/api/FileSystemManager.cpp
@@ -0,0 +1,153 @@
+/* -*- 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/FileSystemManager.h"
+
+#include "FileSystemBackgroundRequestHandler.h"
+#include "fs/FileSystemRequestHandler.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/FileSystemManagerChild.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/StorageManager.h"
+#include "mozilla/dom/fs/ManagedMozPromiseRequestHolder.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+
+namespace mozilla::dom {
+
+FileSystemManager::FileSystemManager(
+ nsIGlobalObject* aGlobal, RefPtr<StorageManager> aStorageManager,
+ RefPtr<FileSystemBackgroundRequestHandler> aBackgroundRequestHandler)
+ : mGlobal(aGlobal),
+ mStorageManager(std::move(aStorageManager)),
+ mBackgroundRequestHandler(std::move(aBackgroundRequestHandler)),
+ mRequestHandler(new fs::FileSystemRequestHandler()) {}
+
+FileSystemManager::FileSystemManager(nsIGlobalObject* aGlobal,
+ RefPtr<StorageManager> aStorageManager)
+ : FileSystemManager(aGlobal, std::move(aStorageManager),
+ MakeRefPtr<FileSystemBackgroundRequestHandler>()) {}
+
+FileSystemManager::~FileSystemManager() { MOZ_ASSERT(mShutdown); }
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemManager)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FileSystemManager);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(FileSystemManager);
+NS_IMPL_CYCLE_COLLECTION(FileSystemManager, mGlobal, mStorageManager);
+
+void FileSystemManager::Shutdown() {
+ mShutdown.Flip();
+
+ auto shutdownAndDisconnect = [self = RefPtr(this)]() {
+ self->mBackgroundRequestHandler->Shutdown();
+
+ for (RefPtr<PromiseRequestHolder<BoolPromise>> holder :
+ self->mPromiseRequestHolders.ForwardRange()) {
+ holder->DisconnectIfExists();
+ }
+ };
+
+ if (NS_IsMainThread()) {
+ if (mBackgroundRequestHandler->FileSystemManagerChildStrongRef()) {
+ mBackgroundRequestHandler->FileSystemManagerChildStrongRef()
+ ->CloseAllWritables(
+ [shutdownAndDisconnect = std::move(shutdownAndDisconnect)]() {
+ shutdownAndDisconnect();
+ });
+ } else {
+ shutdownAndDisconnect();
+ }
+ } else {
+ if (mBackgroundRequestHandler->FileSystemManagerChildStrongRef()) {
+ // FileSystemAccessHandles and FileSystemWritableFileStreams prevent
+ // shutdown until they are full closed, so at this point, they all should
+ // be closed.
+ MOZ_ASSERT(mBackgroundRequestHandler->FileSystemManagerChildStrongRef()
+ ->AllSyncAccessHandlesClosed());
+ MOZ_ASSERT(mBackgroundRequestHandler->FileSystemManagerChildStrongRef()
+ ->AllWritableFileStreamsClosed());
+ }
+
+ shutdownAndDisconnect();
+ }
+}
+
+const RefPtr<FileSystemManagerChild>& FileSystemManager::ActorStrongRef()
+ const {
+ return mBackgroundRequestHandler->FileSystemManagerChildStrongRef();
+}
+
+void FileSystemManager::RegisterPromiseRequestHolder(
+ PromiseRequestHolder<BoolPromise>* aHolder) {
+ mPromiseRequestHolders.AppendElement(aHolder);
+}
+
+void FileSystemManager::UnregisterPromiseRequestHolder(
+ PromiseRequestHolder<BoolPromise>* aHolder) {
+ mPromiseRequestHolders.RemoveElement(aHolder);
+}
+
+void FileSystemManager::BeginRequest(
+ std::function<void(const RefPtr<FileSystemManagerChild>&)>&& aSuccess,
+ std::function<void(nsresult)>&& aFailure) {
+ MOZ_ASSERT(!mShutdown);
+
+ MOZ_ASSERT(mGlobal);
+
+ // Check if we're allowed to use storage
+ if (mGlobal->GetStorageAccess() < StorageAccess::eSessionScoped) {
+ aFailure(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ if (mBackgroundRequestHandler->FileSystemManagerChildStrongRef()) {
+ aSuccess(mBackgroundRequestHandler->FileSystemManagerChildStrongRef());
+ return;
+ }
+
+ QM_TRY_INSPECT(const auto& principalInfo, mGlobal->GetStorageKey(), QM_VOID,
+ [&aFailure](nsresult rv) { aFailure(rv); });
+
+ auto holder = MakeRefPtr<PromiseRequestHolder<BoolPromise>>(this);
+
+ mBackgroundRequestHandler->CreateFileSystemManagerChild(principalInfo)
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<FileSystemManager>(this), holder,
+ success = std::move(aSuccess), failure = std::move(aFailure)](
+ const BoolPromise::ResolveOrRejectValue& aValue) {
+ holder->Complete();
+
+ if (aValue.IsResolve()) {
+ success(self->mBackgroundRequestHandler
+ ->FileSystemManagerChildStrongRef());
+ } else {
+ failure(aValue.RejectValue());
+ }
+ })
+ ->Track(*holder);
+}
+
+already_AddRefed<Promise> FileSystemManager::GetDirectory(ErrorResult& aError) {
+ MOZ_ASSERT(mGlobal);
+
+ RefPtr<Promise> promise = Promise::Create(mGlobal, aError);
+ if (NS_WARN_IF(aError.Failed())) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(promise);
+
+ mRequestHandler->GetRootHandle(this, promise, aError);
+ if (NS_WARN_IF(aError.Failed())) {
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/api/FileSystemManager.h b/dom/fs/api/FileSystemManager.h
new file mode 100644
index 0000000000..b910eacea2
--- /dev/null
+++ b/dom/fs/api/FileSystemManager.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 DOM_FS_CHILD_FILESYSTEMMANAGER_H_
+#define DOM_FS_CHILD_FILESYSTEMMANAGER_H_
+
+#include <functional>
+
+#include "mozilla/MozPromise.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsTObserverArray.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+class FileSystemManagerChild;
+class FileSystemBackgroundRequestHandler;
+class StorageManager;
+
+namespace fs {
+class FileSystemRequestHandler;
+template <typename Manager, typename PromiseType>
+class ManagedMozPromiseRequestHolder;
+} // namespace fs
+
+// `FileSystemManager` is supposed to be held by `StorageManager` and thus
+// there should always be only one `FileSystemManager` per `nsIGlobalObject`.
+// `FileSystemManager` is responsible for creating and eventually caching
+// `FileSystemManagerChild` which is required for communication with the parent
+// process. `FileSystemHandle` is also expected to hold `FileSystemManager`,
+// but it should never clear the strong reference during cycle collection's
+// unlink phase to keep the actor alive. `FileSystemSyncAccessHandle` and
+// `FileSystemWritableFileStream` are also expected to hold `FileSystemManager`,
+// and they shouldn't clear the strong reference during cycle collection's
+// unlink phase as well even though they have their own actor. Those actors
+// are managed by the top level actor, so if the top level actor is destroyed,
+// the whole chain of managed actors would be destroyed as well.
+class FileSystemManager : public nsISupports {
+ public:
+ template <typename PromiseType>
+ using PromiseRequestHolder =
+ fs::ManagedMozPromiseRequestHolder<FileSystemManager, PromiseType>;
+
+ FileSystemManager(
+ nsIGlobalObject* aGlobal, RefPtr<StorageManager> aStorageManager,
+ RefPtr<FileSystemBackgroundRequestHandler> aBackgroundRequestHandler);
+
+ FileSystemManager(nsIGlobalObject* aGlobal,
+ RefPtr<StorageManager> aStorageManager);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(FileSystemManager)
+
+ bool IsShutdown() const { return mShutdown; }
+
+ void Shutdown();
+
+ const RefPtr<FileSystemManagerChild>& ActorStrongRef() const;
+
+ void RegisterPromiseRequestHolder(PromiseRequestHolder<BoolPromise>* aHolder);
+
+ void UnregisterPromiseRequestHolder(
+ PromiseRequestHolder<BoolPromise>* aHolder);
+
+ void BeginRequest(
+ std::function<void(const RefPtr<FileSystemManagerChild>&)>&& aSuccess,
+ std::function<void(nsresult)>&& aFailure);
+
+ already_AddRefed<Promise> GetDirectory(ErrorResult& aError);
+
+ private:
+ virtual ~FileSystemManager();
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+
+ RefPtr<StorageManager> mStorageManager;
+
+ const RefPtr<FileSystemBackgroundRequestHandler> mBackgroundRequestHandler;
+ const UniquePtr<fs::FileSystemRequestHandler> mRequestHandler;
+
+ nsTObserverArray<PromiseRequestHolder<BoolPromise>*> mPromiseRequestHolders;
+
+ FlippedOnce<false> mShutdown;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_CHILD_FILESYSTEMMANAGER_H_
diff --git a/dom/fs/api/FileSystemSyncAccessHandle.cpp b/dom/fs/api/FileSystemSyncAccessHandle.cpp
new file mode 100644
index 0000000000..0c7082905d
--- /dev/null
+++ b/dom/fs/api/FileSystemSyncAccessHandle.cpp
@@ -0,0 +1,645 @@
+/* -*- 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 "FileSystemSyncAccessHandle.h"
+
+#include "fs/FileSystemAsyncCopy.h"
+#include "fs/FileSystemRequestHandler.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/FixedBufferOutputStream.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/dom/FileSystemAccessHandleChild.h"
+#include "mozilla/dom/FileSystemAccessHandleControlChild.h"
+#include "mozilla/dom/FileSystemHandleBinding.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/FileSystemManagerChild.h"
+#include "mozilla/dom/FileSystemSyncAccessHandleBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerRef.h"
+#include "mozilla/dom/fs/IPCRejectReporter.h"
+#include "mozilla/dom/fs/TargetPtrHolder.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/ipc/RandomAccessStreamUtils.h"
+#include "nsNetCID.h"
+#include "nsStringStream.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+using SizePromise = Int64Promise;
+const auto CreateAndRejectSizePromise = CreateAndRejectInt64Promise;
+
+} // namespace
+
+FileSystemSyncAccessHandle::FileSystemSyncAccessHandle(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
+ RefPtr<FileSystemAccessHandleChild> aActor,
+ RefPtr<FileSystemAccessHandleControlChild> aControlActor,
+ RefPtr<TaskQueue> aIOTaskQueue,
+ const fs::FileSystemEntryMetadata& aMetadata)
+ : mGlobal(aGlobal),
+ mManager(aManager),
+ mActor(std::move(aActor)),
+ mControlActor(std::move(aControlActor)),
+ mIOTaskQueue(std::move(aIOTaskQueue)),
+ mStreamParams(std::move(aStreamParams)),
+ mMetadata(aMetadata),
+ mState(State::Initial) {
+ LOG(("Created SyncAccessHandle %p", this));
+
+ // Connect with the actor directly in the constructor. This way the actor
+ // can call `FileSystemSyncAccessHandle::ClearActor` when we call
+ // `PFileSystemAccessHandleChild::Send__delete__` even when
+ // FileSystemSyncAccessHandle::Create fails, in which case the not yet
+ // fully constructed FileSystemSyncAccessHandle is being destroyed.
+ mActor->SetAccessHandle(this);
+
+ mControlActor->SetAccessHandle(this);
+}
+
+FileSystemSyncAccessHandle::~FileSystemSyncAccessHandle() {
+ MOZ_ASSERT(!mActor);
+ MOZ_ASSERT(IsClosed());
+}
+
+// static
+Result<RefPtr<FileSystemSyncAccessHandle>, nsresult>
+FileSystemSyncAccessHandle::Create(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
+ mozilla::ipc::ManagedEndpoint<PFileSystemAccessHandleChild>&&
+ aAccessHandleChildEndpoint,
+ mozilla::ipc::Endpoint<PFileSystemAccessHandleControlChild>&&
+ aAccessHandleControlChildEndpoint,
+ const fs::FileSystemEntryMetadata& aMetadata) {
+ WorkerPrivate* const workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+
+ auto accessHandleChild = MakeRefPtr<FileSystemAccessHandleChild>();
+
+ QM_TRY(MOZ_TO_RESULT(
+ aManager->ActorStrongRef()->BindPFileSystemAccessHandleEndpoint(
+ std::move(aAccessHandleChildEndpoint), accessHandleChild)));
+
+ auto accessHandleControlChild =
+ MakeRefPtr<FileSystemAccessHandleControlChild>();
+
+ aAccessHandleControlChildEndpoint.Bind(accessHandleControlChild,
+ workerPrivate->ControlEventTarget());
+
+ QM_TRY_UNWRAP(auto streamTransportService,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIEventTarget>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ NS_STREAMTRANSPORTSERVICE_CONTRACTID));
+
+ RefPtr<TaskQueue> ioTaskQueue = TaskQueue::Create(
+ streamTransportService.forget(), "FileSystemSyncAccessHandle");
+ QM_TRY(MOZ_TO_RESULT(ioTaskQueue));
+
+ RefPtr<FileSystemSyncAccessHandle> result = new FileSystemSyncAccessHandle(
+ aGlobal, aManager, std::move(aStreamParams), std::move(accessHandleChild),
+ std::move(accessHandleControlChild), std::move(ioTaskQueue), aMetadata);
+
+ auto autoClose = MakeScopeExit([result] {
+ MOZ_ASSERT(result->mState == State::Initial);
+ result->mState = State::Closed;
+ result->mActor->SendClose();
+ });
+
+ workerPrivate->AssertIsOnWorkerThread();
+
+ RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create(
+ workerPrivate, "FileSystemSyncAccessHandle", [result]() {
+ if (result->IsOpen()) {
+ // We don't need to use the result, we just need to begin the closing
+ // process.
+ Unused << result->BeginClose();
+ }
+ });
+ QM_TRY(MOZ_TO_RESULT(workerRef));
+
+ autoClose.release();
+
+ result->mWorkerRef = std::move(workerRef);
+ result->mState = State::Open;
+
+ return result;
+}
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemSyncAccessHandle)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FileSystemSyncAccessHandle)
+NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(FileSystemSyncAccessHandle,
+ LastRelease())
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemSyncAccessHandle)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(FileSystemSyncAccessHandle)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
+ // Don't unlink mManager!
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+ if (tmp->IsOpen()) {
+ // We don't need to use the result, we just need to begin the closing
+ // process.
+ Unused << tmp->BeginClose();
+ }
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(FileSystemSyncAccessHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mManager)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+void FileSystemSyncAccessHandle::LastRelease() {
+ // We can't call `FileSystemSyncAccessHandle::Close` here because it may need
+ // to keep FileSystemSyncAccessHandle object alive which isn't possible when
+ // the object is about to be deleted. There are other mechanisms which ensure
+ // that the object is correctly closed before destruction. For example the
+ // object unlinking and the worker shutdown (we get notified about it via the
+ // callback passed to `StrongWorkerRef`) are used to close the object if it
+ // hasn't been closed yet.
+
+ if (mActor) {
+ PFileSystemAccessHandleChild::Send__delete__(mActor);
+
+ // `PFileSystemAccessHandleChild::Send__delete__` is supposed to call
+ // `FileSystemAccessHandleChild::ActorDestroy` which in turn calls
+ // `FileSystemSyncAccessHandle::ClearActor`, so `mActor` should be be null
+ // at this point.
+ MOZ_ASSERT(!mActor);
+ }
+
+ if (mControlActor) {
+ mControlActor->Close();
+
+ // `FileSystemAccessHandleControlChild::Close` is supposed to call
+ // `FileSystemAccessHandleControlChild::ActorDestroy` which in turn calls
+ // `FileSystemSyncAccessHandle::ClearControlActor`, so `mControlActor`
+ // should be be null at this point.
+ MOZ_ASSERT(!mControlActor);
+ }
+}
+
+void FileSystemSyncAccessHandle::ClearActor() {
+ MOZ_ASSERT(mActor);
+
+ mActor = nullptr;
+}
+
+void FileSystemSyncAccessHandle::ClearControlActor() {
+ // `mControlActor` is initialized in the constructor and this method is
+ // supposed to be called only once.
+ MOZ_ASSERT(mControlActor);
+
+ mControlActor = nullptr;
+}
+
+bool FileSystemSyncAccessHandle::IsOpen() const {
+ MOZ_ASSERT(mState != State::Initial);
+
+ return mState == State::Open;
+}
+
+bool FileSystemSyncAccessHandle::IsClosing() const {
+ MOZ_ASSERT(mState != State::Initial);
+
+ return mState == State::Closing;
+}
+
+bool FileSystemSyncAccessHandle::IsClosed() const {
+ MOZ_ASSERT(mState != State::Initial);
+
+ return mState == State::Closed;
+}
+
+RefPtr<BoolPromise> FileSystemSyncAccessHandle::BeginClose() {
+ MOZ_ASSERT(IsOpen());
+
+ mState = State::Closing;
+
+ InvokeAsync(mIOTaskQueue, __func__,
+ [selfHolder = fs::TargetPtrHolder(this)]() {
+ if (selfHolder->mStream) {
+ LOG(("%p: Closing", selfHolder->mStream.get()));
+
+ selfHolder->mStream->OutputStream()->Close();
+ selfHolder->mStream = nullptr;
+ } else {
+ LOG(("Closing (no stream)"));
+
+ // If the stream was not deserialized, `mStreamParams` still
+ // contains a pre-opened file descriptor which needs to be
+ // closed here by moving `mStreamParams` to a local variable
+ // (the file descriptor will be closed for real when
+ // `streamParams` goes out of scope).
+
+ mozilla::ipc::RandomAccessStreamParams streamParams(
+ std::move(selfHolder->mStreamParams));
+ }
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(mWorkerRef->Private()->ControlEventTarget(), __func__,
+ [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue&) {
+ return self->mIOTaskQueue->BeginShutdown();
+ })
+ ->Then(
+ mWorkerRef->Private()->ControlEventTarget(), __func__,
+ [self = RefPtr(this)](const ShutdownPromise::ResolveOrRejectValue&) {
+ if (self->mControlActor) {
+ RefPtr<BoolPromise::Private> promise =
+ new BoolPromise::Private(__func__);
+
+ self->mControlActor->SendClose(
+ [promise](void_t&&) { promise->Resolve(true, __func__); },
+ [promise](const mozilla::ipc::ResponseRejectReason& aReason) {
+ fs::IPCRejectReporter(aReason);
+
+ promise->Reject(NS_ERROR_FAILURE, __func__);
+ });
+
+ return RefPtr<BoolPromise>(promise);
+ }
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(mWorkerRef->Private()->ControlEventTarget(), __func__,
+ [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue&) {
+ self->mWorkerRef = nullptr;
+
+ self->mState = State::Closed;
+
+ self->mClosePromiseHolder.ResolveIfExists(true, __func__);
+ });
+
+ return OnClose();
+}
+
+RefPtr<BoolPromise> FileSystemSyncAccessHandle::OnClose() {
+ MOZ_ASSERT(mState == State::Closing);
+
+ return mClosePromiseHolder.Ensure(__func__);
+}
+
+// WebIDL Boilerplate
+
+nsIGlobalObject* FileSystemSyncAccessHandle::GetParentObject() const {
+ return mGlobal;
+}
+
+JSObject* FileSystemSyncAccessHandle::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return FileSystemSyncAccessHandle_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// WebIDL Interface
+
+uint64_t FileSystemSyncAccessHandle::Read(
+ const MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer& aBuffer,
+ const FileSystemReadWriteOptions& aOptions, ErrorResult& aRv) {
+ return ReadOrWrite(aBuffer, aOptions, /* aRead */ true, aRv);
+}
+
+uint64_t FileSystemSyncAccessHandle::Write(
+ const MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer& aBuffer,
+ const FileSystemReadWriteOptions& aOptions, ErrorResult& aRv) {
+ return ReadOrWrite(aBuffer, aOptions, /* aRead */ false, aRv);
+}
+
+void FileSystemSyncAccessHandle::Truncate(uint64_t aSize, ErrorResult& aError) {
+ if (!IsOpen()) {
+ aError.ThrowInvalidStateError("SyncAccessHandle is closed");
+ return;
+ }
+
+ MOZ_ASSERT(mWorkerRef);
+
+ AutoSyncLoopHolder syncLoop(mWorkerRef->Private(), Canceling);
+
+ nsCOMPtr<nsISerialEventTarget> syncLoopTarget =
+ syncLoop.GetSerialEventTarget();
+ QM_TRY(MOZ_TO_RESULT(syncLoopTarget), [&aError](nsresult) {
+ aError.ThrowInvalidStateError("Worker is shutting down");
+ });
+
+ InvokeAsync(
+ mIOTaskQueue, __func__,
+ [selfHolder = fs::TargetPtrHolder(this), aSize]() {
+ QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()),
+ CreateAndRejectBoolPromise);
+
+ LOG(("%p: Truncate to %" PRIu64, selfHolder->mStream.get(), aSize));
+ int64_t offset = 0;
+ QM_TRY(MOZ_TO_RESULT(selfHolder->mStream->Tell(&offset)),
+ CreateAndRejectBoolPromise);
+ QM_TRY(MOZ_TO_RESULT(selfHolder->mStream->Seek(
+ nsISeekableStream::NS_SEEK_SET, aSize)),
+ CreateAndRejectBoolPromise);
+
+ QM_TRY(MOZ_TO_RESULT(selfHolder->mStream->SetEOF()),
+ CreateAndRejectBoolPromise);
+ // restore cursor position (clamp to end of file)
+ QM_TRY(MOZ_TO_RESULT(selfHolder->mStream->Seek(
+ nsISeekableStream::NS_SEEK_SET,
+ std::min((uint64_t)offset, aSize))),
+ CreateAndRejectBoolPromise);
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(syncLoopTarget, __func__,
+ [this, &syncLoopTarget](
+ const BoolPromise::ResolveOrRejectValue& aValue) {
+ MOZ_ASSERT(mWorkerRef);
+
+ mWorkerRef->Private()->AssertIsOnWorkerThread();
+
+ mWorkerRef->Private()->StopSyncLoop(
+ syncLoopTarget,
+ aValue.IsResolve() ? NS_OK : aValue.RejectValue());
+ });
+
+ QM_TRY(MOZ_TO_RESULT(syncLoop.Run()),
+ [&aError](const nsresult rv) { aError.Throw(rv); });
+}
+
+uint64_t FileSystemSyncAccessHandle::GetSize(ErrorResult& aError) {
+ if (!IsOpen()) {
+ aError.ThrowInvalidStateError("SyncAccessHandle is closed");
+ return 0;
+ }
+
+ MOZ_ASSERT(mWorkerRef);
+
+ AutoSyncLoopHolder syncLoop(mWorkerRef->Private(), Canceling);
+
+ nsCOMPtr<nsISerialEventTarget> syncLoopTarget =
+ syncLoop.GetSerialEventTarget();
+ QM_TRY(MOZ_TO_RESULT(syncLoopTarget), [&aError](nsresult) {
+ aError.ThrowInvalidStateError("Worker is shutting down");
+ return 0;
+ });
+
+ // XXX Could we somehow pass the size to `StopSyncLoop` and then get it via
+ // `QM_TRY_INSPECT(const auto& size, syncLoop.Run)` ?
+ // Could we use Result<UniquePtr<...>, nsresult> for that ?
+ int64_t size;
+
+ InvokeAsync(mIOTaskQueue, __func__,
+ [selfHolder = fs::TargetPtrHolder(this)]() {
+ QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()),
+ CreateAndRejectSizePromise);
+
+ nsCOMPtr<nsIFileMetadata> fileMetadata =
+ do_QueryInterface(selfHolder->mStream);
+ MOZ_ASSERT(fileMetadata);
+
+ QM_TRY_INSPECT(
+ const auto& size,
+ MOZ_TO_RESULT_INVOKE_MEMBER(fileMetadata, GetSize),
+ CreateAndRejectSizePromise);
+
+ LOG(("%p: GetSize %" PRIu64, selfHolder->mStream.get(), size));
+
+ return SizePromise::CreateAndResolve(size, __func__);
+ })
+ ->Then(syncLoopTarget, __func__,
+ [this, &syncLoopTarget,
+ &size](const Int64Promise::ResolveOrRejectValue& aValue) {
+ MOZ_ASSERT(mWorkerRef);
+
+ mWorkerRef->Private()->AssertIsOnWorkerThread();
+
+ if (aValue.IsResolve()) {
+ size = aValue.ResolveValue();
+
+ mWorkerRef->Private()->StopSyncLoop(syncLoopTarget, NS_OK);
+ } else {
+ mWorkerRef->Private()->StopSyncLoop(syncLoopTarget,
+ aValue.RejectValue());
+ }
+ });
+
+ QM_TRY(MOZ_TO_RESULT(syncLoop.Run()), [&aError](const nsresult rv) {
+ aError.Throw(rv);
+ return 0;
+ });
+
+ return size;
+}
+
+void FileSystemSyncAccessHandle::Flush(ErrorResult& aError) {
+ if (!IsOpen()) {
+ aError.ThrowInvalidStateError("SyncAccessHandle is closed");
+ return;
+ }
+
+ MOZ_ASSERT(mWorkerRef);
+
+ AutoSyncLoopHolder syncLoop(mWorkerRef->Private(), Canceling);
+
+ nsCOMPtr<nsISerialEventTarget> syncLoopTarget =
+ syncLoop.GetSerialEventTarget();
+ QM_TRY(MOZ_TO_RESULT(syncLoopTarget), [&aError](nsresult) {
+ aError.ThrowInvalidStateError("Worker is shutting down");
+ });
+
+ InvokeAsync(mIOTaskQueue, __func__,
+ [selfHolder = fs::TargetPtrHolder(this)]() {
+ QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()),
+ CreateAndRejectBoolPromise);
+
+ LOG(("%p: Flush", selfHolder->mStream.get()));
+
+ QM_TRY(
+ MOZ_TO_RESULT(selfHolder->mStream->OutputStream()->Flush()),
+ CreateAndRejectBoolPromise);
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(syncLoopTarget, __func__,
+ [this, &syncLoopTarget](
+ const BoolPromise::ResolveOrRejectValue& aValue) {
+ MOZ_ASSERT(mWorkerRef);
+
+ mWorkerRef->Private()->AssertIsOnWorkerThread();
+
+ mWorkerRef->Private()->StopSyncLoop(
+ syncLoopTarget,
+ aValue.IsResolve() ? NS_OK : aValue.RejectValue());
+ });
+
+ QM_TRY(MOZ_TO_RESULT(syncLoop.Run()),
+ [&aError](const nsresult rv) { aError.Throw(rv); });
+}
+
+void FileSystemSyncAccessHandle::Close() {
+ if (!(IsOpen() || IsClosing())) {
+ return;
+ }
+
+ MOZ_ASSERT(mWorkerRef);
+
+ // Normally mWorkerRef can be used directly for stopping the sync loop, but
+ // the async close is special because mWorkerRef is cleared as part of the
+ // operation. That's why we need to use this extra strong ref to the
+ // `StrongWorkerRef`.
+ RefPtr<StrongWorkerRef> workerRef = mWorkerRef;
+
+ AutoSyncLoopHolder syncLoop(workerRef->Private(), Killing);
+
+ nsCOMPtr<nsISerialEventTarget> syncLoopTarget =
+ syncLoop.GetSerialEventTarget();
+ MOZ_ASSERT(syncLoopTarget);
+
+ InvokeAsync(syncLoopTarget, __func__, [self = RefPtr(this)]() {
+ if (self->IsOpen()) {
+ return self->BeginClose();
+ }
+ return self->OnClose();
+ })->Then(syncLoopTarget, __func__, [&workerRef, &syncLoopTarget]() {
+ MOZ_ASSERT(workerRef);
+
+ workerRef->Private()->AssertIsOnWorkerThread();
+
+ workerRef->Private()->StopSyncLoop(syncLoopTarget, NS_OK);
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(syncLoop.Run());
+}
+
+uint64_t FileSystemSyncAccessHandle::ReadOrWrite(
+ const MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer& aBuffer,
+ const FileSystemReadWriteOptions& aOptions, const bool aRead,
+ ErrorResult& aRv) {
+ if (!IsOpen()) {
+ aRv.ThrowInvalidStateError("SyncAccessHandle is closed");
+ return 0;
+ }
+
+ MOZ_ASSERT(mWorkerRef);
+
+ auto throwAndReturn = [&aRv](const nsresult rv) {
+ aRv.Throw(rv);
+ return 0;
+ };
+
+ // Handle seek before read ('at')
+ const auto at = [&aOptions]() -> uint64_t {
+ if (aOptions.mAt.WasPassed()) {
+ return aOptions.mAt.Value();
+ }
+ // Spec says default for at is 0 (2.6)
+ return 0;
+ }();
+
+ const auto offset = CheckedInt<int64_t>(at);
+ QM_TRY(MOZ_TO_RESULT(offset.isValid()), throwAndReturn);
+
+ AutoSyncLoopHolder syncLoop(mWorkerRef->Private(), Canceling);
+
+ nsCOMPtr<nsISerialEventTarget> syncLoopTarget =
+ syncLoop.GetSerialEventTarget();
+ QM_TRY(MOZ_TO_RESULT(syncLoopTarget), [&aRv](nsresult) {
+ aRv.ThrowInvalidStateError("Worker is shutting down");
+ return 0;
+ });
+
+ uint64_t totalCount = 0;
+
+ ProcessTypedArraysFixed(aBuffer, [&](const Span<uint8_t> aData) {
+ InvokeAsync(
+ mIOTaskQueue, __func__,
+ [selfHolder = fs::TargetPtrHolder(this), aData,
+ use_offset = aOptions.mAt.WasPassed(), offset, aRead, syncLoopTarget,
+ &totalCount]() {
+ QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()),
+ CreateAndRejectBoolPromise);
+ if (use_offset) {
+ LOG_VERBOSE(("%p: Seeking to %" PRIu64, selfHolder->mStream.get(),
+ offset.value()));
+
+ QM_TRY(MOZ_TO_RESULT(selfHolder->mStream->Seek(
+ nsISeekableStream::NS_SEEK_SET, offset.value())),
+ CreateAndRejectBoolPromise);
+ }
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsCOMPtr<nsIOutputStream> outputStream;
+
+ if (aRead) {
+ LOG_VERBOSE(("%p: Reading %zu bytes", selfHolder->mStream.get(),
+ aData.Length()));
+
+ inputStream = selfHolder->mStream->InputStream();
+ outputStream =
+ FixedBufferOutputStream::Create(AsWritableChars(aData));
+ } else {
+ LOG_VERBOSE(("%p: Writing %zu bytes", selfHolder->mStream.get(),
+ aData.Length()));
+
+ QM_TRY(MOZ_TO_RESULT(NS_NewByteInputStream(
+ getter_AddRefs(inputStream), AsChars(aData),
+ NS_ASSIGNMENT_DEPEND)),
+ CreateAndRejectBoolPromise);
+
+ outputStream = selfHolder->mStream->OutputStream();
+ }
+
+ auto promiseHolder = MakeUnique<MozPromiseHolder<BoolPromise>>();
+ RefPtr<BoolPromise> promise = promiseHolder->Ensure(__func__);
+
+ QM_TRY(MOZ_TO_RESULT(fs::AsyncCopy(
+ inputStream, outputStream, GetCurrentSerialEventTarget(),
+ aRead ? NS_ASYNCCOPY_VIA_WRITESEGMENTS
+ : NS_ASYNCCOPY_VIA_READSEGMENTS,
+ /* aCloseSource */ !aRead, /* aCloseSink */ aRead,
+ [&totalCount](uint32_t count) { totalCount += count; },
+ [promiseHolder = std::move(promiseHolder)](nsresult rv) {
+ promiseHolder->ResolveIfExists(true, __func__);
+ })),
+ CreateAndRejectBoolPromise);
+
+ return promise;
+ })
+ ->Then(syncLoopTarget, __func__,
+ [this, &syncLoopTarget](
+ const BoolPromise::ResolveOrRejectValue& aValue) {
+ MOZ_ASSERT(mWorkerRef);
+
+ mWorkerRef->Private()->AssertIsOnWorkerThread();
+
+ mWorkerRef->Private()->StopSyncLoop(syncLoopTarget, NS_OK);
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(syncLoop.Run());
+ });
+
+ return totalCount;
+}
+
+nsresult FileSystemSyncAccessHandle::EnsureStream() {
+ if (!mStream) {
+ QM_TRY_UNWRAP(mStream, DeserializeRandomAccessStream(mStreamParams),
+ NS_ERROR_FAILURE);
+
+ mozilla::ipc::RandomAccessStreamParams streamParams(
+ std::move(mStreamParams));
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/api/FileSystemSyncAccessHandle.h b/dom/fs/api/FileSystemSyncAccessHandle.h
new file mode 100644
index 0000000000..49f470b60b
--- /dev/null
+++ b/dom/fs/api/FileSystemSyncAccessHandle.h
@@ -0,0 +1,133 @@
+/* -*- 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_FS_FILESYSTEMSYNCACCESSHANDLE_H_
+#define DOM_FS_FILESYSTEMSYNCACCESSHANDLE_H_
+
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+class ErrorResult;
+class TaskQueue;
+
+namespace dom {
+
+class FileSystemAccessHandleChild;
+class FileSystemAccessHandleControlChild;
+struct FileSystemReadWriteOptions;
+class FileSystemManager;
+class MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer;
+class Promise;
+class StrongWorkerRef;
+
+class FileSystemSyncAccessHandle final : public nsISupports,
+ public nsWrapperCache {
+ public:
+ enum struct State : uint8_t { Initial = 0, Open, Closing, Closed };
+
+ static Result<RefPtr<FileSystemSyncAccessHandle>, nsresult> Create(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
+ mozilla::ipc::ManagedEndpoint<PFileSystemAccessHandleChild>&&
+ aAccessHandleChildEndpoint,
+ mozilla::ipc::Endpoint<PFileSystemAccessHandleControlChild>&&
+ aAccessHandleControlChildEndpoint,
+ const fs::FileSystemEntryMetadata& aMetadata);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemSyncAccessHandle)
+
+ void LastRelease();
+
+ void ClearActor();
+
+ void ClearControlActor();
+
+ bool IsOpen() const;
+
+ bool IsClosing() const;
+
+ bool IsClosed() const;
+
+ [[nodiscard]] RefPtr<BoolPromise> BeginClose();
+
+ [[nodiscard]] RefPtr<BoolPromise> OnClose();
+
+ // WebIDL Boilerplate
+ nsIGlobalObject* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL Interface
+ uint64_t Read(
+ const MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer& aBuffer,
+ const FileSystemReadWriteOptions& aOptions, ErrorResult& aRv);
+
+ uint64_t Write(
+ const MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer& aBuffer,
+ const FileSystemReadWriteOptions& aOptions, ErrorResult& aRv);
+
+ void Truncate(uint64_t aSize, ErrorResult& aError);
+
+ uint64_t GetSize(ErrorResult& aError);
+
+ void Flush(ErrorResult& aError);
+
+ void Close();
+
+ private:
+ FileSystemSyncAccessHandle(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
+ RefPtr<FileSystemAccessHandleChild> aActor,
+ RefPtr<FileSystemAccessHandleControlChild> aControlActor,
+ RefPtr<TaskQueue> aIOTaskQueue,
+ const fs::FileSystemEntryMetadata& aMetadata);
+
+ virtual ~FileSystemSyncAccessHandle();
+
+ uint64_t ReadOrWrite(
+ const MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer& aBuffer,
+ const FileSystemReadWriteOptions& aOptions, const bool aRead,
+ ErrorResult& aRv);
+
+ nsresult EnsureStream();
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+
+ RefPtr<FileSystemManager> mManager;
+
+ RefPtr<FileSystemAccessHandleChild> mActor;
+
+ RefPtr<FileSystemAccessHandleControlChild> mControlActor;
+
+ RefPtr<TaskQueue> mIOTaskQueue;
+
+ nsCOMPtr<nsIRandomAccessStream> mStream;
+
+ RefPtr<StrongWorkerRef> mWorkerRef;
+
+ MozPromiseHolder<BoolPromise> mClosePromiseHolder;
+
+ mozilla::ipc::RandomAccessStreamParams mStreamParams;
+
+ const fs::FileSystemEntryMetadata mMetadata;
+
+ State mState;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_FILESYSTEMSYNCACCESSHANDLE_H_
diff --git a/dom/fs/api/FileSystemWritableFileStream.cpp b/dom/fs/api/FileSystemWritableFileStream.cpp
new file mode 100644
index 0000000000..ab133b2707
--- /dev/null
+++ b/dom/fs/api/FileSystemWritableFileStream.cpp
@@ -0,0 +1,1043 @@
+/* -*- 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 "FileSystemWritableFileStream.h"
+
+#include "fs/FileSystemAsyncCopy.h"
+#include "fs/FileSystemShutdownBlocker.h"
+#include "fs/FileSystemThreadSafeStreamOwner.h"
+#include "mozilla/Buffer.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/InputStreamLengthHelper.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/dom/Blob.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/FileSystemWritableFileStreamBinding.h"
+#include "mozilla/dom/FileSystemWritableFileStreamChild.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerRef.h"
+#include "mozilla/dom/WritableStreamDefaultController.h"
+#include "mozilla/dom/fs/TargetPtrHolder.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/ipc/RandomAccessStreamUtils.h"
+#include "nsAsyncStreamCopier.h"
+#include "nsIInputStream.h"
+#include "nsIRequestObserver.h"
+#include "nsISupportsImpl.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsStreamUtils.h"
+#include "nsStringStream.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+CopyableErrorResult RejectWithConvertedErrors(nsresult aRv) {
+ CopyableErrorResult err;
+ switch (aRv) {
+ case NS_ERROR_DOM_FILE_NOT_FOUND_ERR:
+ [[fallthrough]];
+ case NS_ERROR_FILE_NOT_FOUND:
+ err.ThrowNotFoundError("File not found");
+ break;
+ case NS_ERROR_FILE_NO_DEVICE_SPACE:
+ err.ThrowQuotaExceededError("Quota exceeded");
+ break;
+ default:
+ err.Throw(aRv);
+ }
+
+ return err;
+}
+
+RefPtr<FileSystemWritableFileStream::WriteDataPromise> ResolvePromise(
+ const Int64Promise::ResolveOrRejectValue& aValue) {
+ MOZ_ASSERT(aValue.IsResolve());
+ return FileSystemWritableFileStream::WriteDataPromise::CreateAndResolve(
+ Some(aValue.ResolveValue()), __func__);
+}
+
+RefPtr<FileSystemWritableFileStream::WriteDataPromise> ResolvePromise(
+ const BoolPromise::ResolveOrRejectValue& aValue) {
+ MOZ_ASSERT(aValue.IsResolve());
+ return FileSystemWritableFileStream::WriteDataPromise::CreateAndResolve(
+ Nothing(), __func__);
+}
+
+class WritableFileStreamUnderlyingSinkAlgorithms final
+ : public UnderlyingSinkAlgorithmsWrapper {
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
+ WritableFileStreamUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase)
+
+ explicit WritableFileStreamUnderlyingSinkAlgorithms(
+ FileSystemWritableFileStream& aStream)
+ : mStream(&aStream) {}
+
+ already_AddRefed<Promise> WriteCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aChunk,
+ WritableStreamDefaultController& aController, ErrorResult& aRv) override;
+
+ already_AddRefed<Promise> CloseCallbackImpl(JSContext* aCx,
+ ErrorResult& aRv) override;
+
+ already_AddRefed<Promise> AbortCallbackImpl(
+ JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason,
+ ErrorResult& aRv) override;
+
+ void ReleaseObjects() override;
+
+ private:
+ ~WritableFileStreamUnderlyingSinkAlgorithms() = default;
+
+ RefPtr<FileSystemWritableFileStream> mStream;
+};
+
+} // namespace
+
+class FileSystemWritableFileStream::Command {
+ public:
+ explicit Command(RefPtr<FileSystemWritableFileStream> aWritableFileStream)
+ : mWritableFileStream(std::move(aWritableFileStream)) {
+ MOZ_ASSERT(mWritableFileStream);
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(FileSystemWritableFileStream::Command)
+
+ private:
+ ~Command() { mWritableFileStream->NoteFinishedCommand(); }
+
+ RefPtr<FileSystemWritableFileStream> mWritableFileStream;
+};
+
+class FileSystemWritableFileStream::CloseHandler {
+ enum struct State : uint8_t { Initial = 0, Open, Closing, Closed };
+
+ public:
+ CloseHandler()
+ : mShutdownBlocker(fs::FileSystemShutdownBlocker::CreateForWritable()),
+ mClosePromiseHolder(),
+ mState(State::Initial) {}
+
+ NS_INLINE_DECL_REFCOUNTING(FileSystemWritableFileStream::CloseHandler)
+
+ /**
+ * @brief Are we not yet closing?
+ */
+ bool IsOpen() const { return State::Open == mState; }
+
+ /**
+ * @brief Are we not open and not closed?
+ */
+ bool IsClosing() const { return State::Closing == mState; }
+
+ /**
+ * @brief Are we already fully closed?
+ */
+ bool IsClosed() const { return State::Closed == mState; }
+
+ /**
+ * @brief Transition from open to closing state
+ *
+ * @return true if the state was open and became closing after the call
+ * @return false in all the other cases the previous state is preserved
+ */
+ bool SetClosing() {
+ const bool isOpen = State::Open == mState;
+
+ if (isOpen) {
+ mState = State::Closing;
+ }
+
+ return isOpen;
+ }
+
+ RefPtr<BoolPromise> GetClosePromise() const {
+ MOZ_ASSERT(State::Open != mState,
+ "Please call SetClosing before GetClosePromise");
+
+ if (State::Closing == mState) {
+ return mClosePromiseHolder.Ensure(__func__);
+ }
+
+ // Instant resolve for initial state due to early shutdown or closed state
+ return BoolPromise::CreateAndResolve(true, __func__);
+ }
+
+ /**
+ * @brief Transition from initial to open state. In initial state
+ *
+ */
+ void Open() {
+ MOZ_ASSERT(State::Initial == mState);
+ mShutdownBlocker->Block();
+
+ mState = State::Open;
+ }
+
+ /**
+ * @brief Transition to closed state and resolve all pending promises.
+ *
+ */
+ void Close() {
+ mShutdownBlocker->Unblock();
+ mState = State::Closed;
+ mClosePromiseHolder.ResolveIfExists(true, __func__);
+ }
+
+ protected:
+ virtual ~CloseHandler() = default;
+
+ private:
+ RefPtr<fs::FileSystemShutdownBlocker> mShutdownBlocker;
+
+ mutable MozPromiseHolder<BoolPromise> mClosePromiseHolder;
+
+ State mState;
+};
+
+FileSystemWritableFileStream::FileSystemWritableFileStream(
+ const nsCOMPtr<nsIGlobalObject>& aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
+ RefPtr<FileSystemWritableFileStreamChild> aActor,
+ already_AddRefed<TaskQueue> aTaskQueue,
+ const fs::FileSystemEntryMetadata& aMetadata)
+ : WritableStream(aGlobal, HoldDropJSObjectsCaller::Explicit),
+ mManager(aManager),
+ mActor(std::move(aActor)),
+ mTaskQueue(aTaskQueue),
+ mStreamParams(std::move(aStreamParams)),
+ mMetadata(std::move(aMetadata)),
+ mCloseHandler(MakeAndAddRef<CloseHandler>()),
+ mCommandActive(false) {
+ LOG(("Created WritableFileStream %p", this));
+
+ // Connect with the actor directly in the constructor. This way the actor
+ // can call `FileSystemWritableFileStream::ClearActor` when we call
+ // `PFileSystemWritableFileStreamChild::Send__delete__` even when
+ // FileSystemWritableFileStream::Create fails, in which case the not yet
+ // fully constructed FileSystemWritableFileStream is being destroyed.
+ mActor->SetStream(this);
+
+ mozilla::HoldJSObjects(this);
+}
+
+FileSystemWritableFileStream::~FileSystemWritableFileStream() {
+ MOZ_ASSERT(!mCommandActive);
+ MOZ_ASSERT(IsDone());
+
+ mozilla::DropJSObjects(this);
+}
+
+// https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream
+// * This is fallible because of OOM handling of JSAPI. See bug 1762233.
+// XXX(krosylight): _BOUNDARY because SetUpNative here can't run script because
+// StartCallback here is no-op. Can we let the static check automatically detect
+// this situation?
+/* static */
+MOZ_CAN_RUN_SCRIPT_BOUNDARY
+Result<RefPtr<FileSystemWritableFileStream>, nsresult>
+FileSystemWritableFileStream::Create(
+ const nsCOMPtr<nsIGlobalObject>& aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
+ RefPtr<FileSystemWritableFileStreamChild> aActor,
+ const fs::FileSystemEntryMetadata& aMetadata) {
+ MOZ_ASSERT(aGlobal);
+
+ QM_TRY_UNWRAP(auto streamTransportService,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIEventTarget>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ NS_STREAMTRANSPORTSERVICE_CONTRACTID));
+
+ RefPtr<TaskQueue> taskQueue =
+ TaskQueue::Create(streamTransportService.forget(), "WritableStreamQueue");
+ MOZ_ASSERT(taskQueue);
+
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(aGlobal)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ JSContext* cx = jsapi.cx();
+
+ // Step 1. Let stream be a new FileSystemWritableFileStream in realm.
+ // Step 2. Set stream.[[file]] to file. (Covered by the constructor)
+ RefPtr<FileSystemWritableFileStream> stream =
+ new FileSystemWritableFileStream(
+ aGlobal, aManager, std::move(aStreamParams), std::move(aActor),
+ taskQueue.forget(), aMetadata);
+
+ auto autoClose = MakeScopeExit([stream] {
+ stream->mCloseHandler->Close();
+ stream->mActor->SendClose(/* aAbort */ true);
+ });
+
+ QM_TRY_UNWRAP(
+ RefPtr<StrongWorkerRef> workerRef,
+ ([stream]() -> Result<RefPtr<StrongWorkerRef>, nsresult> {
+ WorkerPrivate* const workerPrivate = GetCurrentThreadWorkerPrivate();
+ if (!workerPrivate) {
+ return RefPtr<StrongWorkerRef>();
+ }
+
+ RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create(
+ workerPrivate, "FileSystemWritableFileStream::Create", [stream]() {
+ if (stream->IsOpen()) {
+ // We don't need the promise, we just
+ // begin the closing process.
+ Unused << stream->BeginAbort();
+ }
+ });
+ QM_TRY(MOZ_TO_RESULT(workerRef));
+
+ return workerRef;
+ }()));
+
+ // Step 3 - 5
+ auto algorithms =
+ MakeRefPtr<WritableFileStreamUnderlyingSinkAlgorithms>(*stream);
+
+ // Step 8: Set up stream with writeAlgorithm set to writeAlgorithm,
+ // closeAlgorithm set to closeAlgorithm, abortAlgorithm set to
+ // abortAlgorithm, highWaterMark set to highWaterMark, and
+ // sizeAlgorithm set to sizeAlgorithm.
+ IgnoredErrorResult rv;
+ stream->SetUpNative(cx, *algorithms,
+ // Step 6. Let highWaterMark be 1.
+ Some(1),
+ // Step 7. Let sizeAlgorithm be an algorithm
+ // that returns 1. (nullptr returns 1, See
+ // WritableStream::Constructor for details)
+ nullptr, rv);
+ if (rv.Failed()) {
+ return Err(rv.StealNSResult());
+ }
+
+ autoClose.release();
+
+ stream->mWorkerRef = std::move(workerRef);
+ stream->mCloseHandler->Open();
+
+ // Step 9: Return stream.
+ return stream;
+}
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(FileSystemWritableFileStream,
+ WritableStream)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(FileSystemWritableFileStream)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(FileSystemWritableFileStream,
+ WritableStream)
+ // Per the comment for the FileSystemManager class, don't unlink mManager!
+ if (tmp->IsOpen()) {
+ Unused << tmp->BeginAbort();
+ }
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(FileSystemWritableFileStream,
+ WritableStream)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mManager)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+void FileSystemWritableFileStream::LastRelease() {
+ // We can't call `FileSystemWritableFileStream::Close` here because it may
+ // need to keep FileSystemWritableFileStream object alive which isn't possible
+ // when the object is about to be deleted. There are other mechanisms which
+ // ensure that the object is correctly closed before destruction. For example
+ // the object unlinking and the worker shutdown (we get notified about it via
+ // the callback passed to `StrongWorkerRef`) are used to close the object if
+ // it hasn't been closed yet.
+
+ if (mActor) {
+ PFileSystemWritableFileStreamChild::Send__delete__(mActor);
+ MOZ_ASSERT(!mActor);
+ }
+}
+
+RefPtr<FileSystemWritableFileStream::Command>
+FileSystemWritableFileStream::CreateCommand() {
+ MOZ_ASSERT(!mCommandActive);
+
+ mCommandActive = true;
+
+ return MakeRefPtr<Command>(this);
+}
+
+bool FileSystemWritableFileStream::IsCommandActive() const {
+ return mCommandActive;
+}
+
+void FileSystemWritableFileStream::ClearActor() {
+ MOZ_ASSERT(mActor);
+
+ mActor = nullptr;
+}
+
+bool FileSystemWritableFileStream::IsOpen() const {
+ return mCloseHandler->IsOpen();
+}
+
+bool FileSystemWritableFileStream::IsFinishing() const {
+ return mCloseHandler->IsClosing();
+}
+
+bool FileSystemWritableFileStream::IsDone() const {
+ return mCloseHandler->IsClosed();
+}
+
+RefPtr<BoolPromise> FileSystemWritableFileStream::BeginFinishing(
+ bool aShouldAbort) {
+ using ClosePromise = PFileSystemWritableFileStreamChild::ClosePromise;
+ MOZ_ASSERT(IsOpen());
+
+ if (mCloseHandler->SetClosing()) {
+ Finish()
+ ->Then(mTaskQueue, __func__,
+ [selfHolder = fs::TargetPtrHolder(this)]() mutable {
+ if (selfHolder->mStreamOwner) {
+ selfHolder->mStreamOwner->Close();
+ } else {
+ // If the stream was not deserialized, `mStreamParams` still
+ // contains a pre-opened file descriptor which needs to be
+ // closed here by moving `mStreamParams` to a local variable
+ // (the file descriptor will be closed for real when
+ // `streamParams` goes out of scope).
+
+ mozilla::ipc::RandomAccessStreamParams streamParams(
+ std::move(selfHolder->mStreamParams));
+ }
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue&) {
+ return self->mTaskQueue->BeginShutdown();
+ })
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [aShouldAbort, self = RefPtr(this)](
+ const ShutdownPromise::ResolveOrRejectValue& /* aValue */) {
+ if (!self->mActor) {
+ return ClosePromise::CreateAndResolve(void_t(), __func__);
+ }
+
+ return self->mActor->SendClose(aShouldAbort);
+ })
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr(this)](
+ const ClosePromise::ResolveOrRejectValue& aValue) {
+ self->mWorkerRef = nullptr;
+ self->mCloseHandler->Close();
+
+ QM_TRY(OkIf(aValue.IsResolve()), QM_VOID);
+ });
+ }
+
+ return mCloseHandler->GetClosePromise();
+}
+
+RefPtr<BoolPromise> FileSystemWritableFileStream::BeginClose() {
+ MOZ_ASSERT(IsOpen());
+ return BeginFinishing(/* aShouldAbort */ false);
+}
+
+RefPtr<BoolPromise> FileSystemWritableFileStream::BeginAbort() {
+ MOZ_ASSERT(IsOpen());
+ return BeginFinishing(/* aShouldAbort */ true);
+}
+
+RefPtr<BoolPromise> FileSystemWritableFileStream::OnDone() {
+ MOZ_ASSERT(!IsOpen());
+
+ return mCloseHandler->GetClosePromise();
+}
+
+already_AddRefed<Promise> FileSystemWritableFileStream::Write(
+ JSContext* aCx, JS::Handle<JS::Value> aChunk, ErrorResult& aError) {
+ // https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream
+ // Step 3. Let writeAlgorithm be an algorithm which takes a chunk argument
+ // and returns the result of running the write a chunk algorithm with stream
+ // and chunk.
+
+ aError.MightThrowJSException();
+
+ // https://fs.spec.whatwg.org/#write-a-chunk
+ // Step 1. Let input be the result of converting chunk to a
+ // FileSystemWriteChunkType.
+
+ ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams data;
+ if (!data.Init(aCx, aChunk)) {
+ aError.StealExceptionFromJSContext(aCx);
+ return nullptr;
+ }
+
+ // Step 2. Let p be a new promise.
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ RefPtr<Promise> innerPromise = Promise::Create(GetParentObject(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ RefPtr<Command> command = CreateCommand();
+
+ // Step 3.3.
+ Write(data)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this}, command,
+ promise](const WriteDataPromise::ResolveOrRejectValue& aValue) {
+ MOZ_ASSERT(!aValue.IsNothing());
+ if (aValue.IsResolve()) {
+ const Maybe<int64_t>& maybeWritten = aValue.ResolveValue();
+ if (maybeWritten.isSome()) {
+ promise->MaybeResolve(maybeWritten.value());
+ return;
+ }
+
+ promise->MaybeResolveWithUndefined();
+ return;
+ }
+
+ CopyableErrorResult err = aValue.RejectValue();
+
+ if (self->IsOpen()) {
+ self->BeginAbort()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise, err = std::move(err)](
+ const BoolPromise::ResolveOrRejectValue&) mutable {
+ // Do not capture command to this context:
+ // close cannot proceed
+ promise->MaybeReject(std::move(err));
+ });
+ } else if (self->IsFinishing()) {
+ self->OnDone()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise, err = std::move(err)](
+ const BoolPromise::ResolveOrRejectValue&) mutable {
+ // Do not capture command to this context:
+ // close cannot proceed
+ promise->MaybeReject(std::move(err));
+ });
+
+ } else {
+ promise->MaybeReject(std::move(err));
+ }
+ });
+
+ return promise.forget();
+}
+
+RefPtr<FileSystemWritableFileStream::WriteDataPromise>
+FileSystemWritableFileStream::Write(
+ ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams& aData) {
+ auto rejectWithTypeError = [](const auto& aMessage) {
+ CopyableErrorResult err;
+ err.ThrowTypeError(aMessage);
+ return WriteDataPromise::CreateAndReject(err, __func__);
+ };
+
+ auto rejectWithSyntaxError = [](const auto& aMessage) {
+ CopyableErrorResult err;
+ err.ThrowSyntaxError(aMessage);
+ return WriteDataPromise::CreateAndReject(err, __func__);
+ };
+
+ if (!IsOpen()) {
+ return rejectWithTypeError("WritableFileStream closed");
+ }
+
+ auto tryResolve = [self = RefPtr{this}](const auto& aValue)
+ -> RefPtr<FileSystemWritableFileStream::WriteDataPromise> {
+ MOZ_ASSERT(self->IsCommandActive());
+
+ if (aValue.IsResolve()) {
+ return ResolvePromise(aValue);
+ }
+
+ MOZ_ASSERT(aValue.IsReject());
+ return WriteDataPromise::CreateAndReject(
+ RejectWithConvertedErrors(aValue.RejectValue()), __func__);
+ };
+
+ auto tryResolveInt64 =
+ [tryResolve](const Int64Promise::ResolveOrRejectValue& aValue) {
+ return tryResolve(aValue);
+ };
+
+ auto tryResolveBool =
+ [tryResolve](const BoolPromise::ResolveOrRejectValue& aValue) {
+ return tryResolve(aValue);
+ };
+
+ // Step 3.3. Let command be input.type if input is a WriteParams, ...
+ if (aData.IsWriteParams()) {
+ const WriteParams& params = aData.GetAsWriteParams();
+ switch (params.mType) {
+ // Step 3.4. If command is "write":
+ case WriteCommandType::Write: {
+ if (!params.mData.WasPassed()) {
+ return rejectWithSyntaxError("write() requires data");
+ }
+
+ // Step 3.4.2. If data is undefined, reject p with a TypeError and
+ // abort.
+ if (params.mData.Value().IsNull()) {
+ return rejectWithTypeError("write() of null data");
+ }
+
+ Maybe<uint64_t> position;
+
+ if (params.mPosition.WasPassed()) {
+ if (params.mPosition.Value().IsNull()) {
+ return rejectWithTypeError("write() with null position");
+ }
+
+ position = Some(params.mPosition.Value().Value());
+ }
+
+ return Write(params.mData.Value().Value(), position)
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ std::move(tryResolveInt64));
+ }
+
+ // Step 3.5. Otherwise, if command is "seek":
+ case WriteCommandType::Seek:
+ if (!params.mPosition.WasPassed()) {
+ return rejectWithSyntaxError("seek() requires a position");
+ }
+
+ // Step 3.5.1. If chunk.position is undefined, reject p with a
+ // TypeError and abort.
+ if (params.mPosition.Value().IsNull()) {
+ return rejectWithTypeError("seek() with null position");
+ }
+
+ return Seek(params.mPosition.Value().Value())
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ std::move(tryResolveBool));
+
+ // Step 3.6. Otherwise, if command is "truncate":
+ case WriteCommandType::Truncate:
+ if (!params.mSize.WasPassed()) {
+ return rejectWithSyntaxError("truncate() requires a size");
+ }
+
+ // Step 3.6.1. If chunk.size is undefined, reject p with a TypeError
+ // and abort.
+ if (params.mSize.Value().IsNull()) {
+ return rejectWithTypeError("truncate() with null size");
+ }
+
+ return Truncate(params.mSize.Value().Value())
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ std::move(tryResolveBool));
+
+ default:
+ MOZ_CRASH("Bad WriteParams value!");
+ }
+ }
+
+ // Step 3.3. ... and "write" otherwise.
+ // Step 3.4. If command is "write":
+ return Write(aData, Nothing())
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ std::move(tryResolveInt64));
+}
+
+// WebIDL Boilerplate
+
+JSObject* FileSystemWritableFileStream::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return FileSystemWritableFileStream_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// WebIDL Interface
+
+already_AddRefed<Promise> FileSystemWritableFileStream::Write(
+ const ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams& aData,
+ ErrorResult& aError) {
+ // https://fs.spec.whatwg.org/#dom-filesystemwritablefilestream-write
+ // Step 1. Let writer be the result of getting a writer for this.
+ RefPtr<WritableStreamDefaultWriter> writer = GetWriter(aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ // Step 2. Let result be the result of writing a chunk to writer given data.
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(GetParentObject())) {
+ aError.ThrowUnknownError("Internal error");
+ return nullptr;
+ }
+
+ JSContext* cx = jsapi.cx();
+
+ JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
+
+ JS::Rooted<JS::Value> val(cx);
+ if (!aData.ToJSVal(cx, global, &val)) {
+ aError.ThrowUnknownError("Internal error");
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise = writer->Write(cx, val, aError);
+
+ // Step 3. Release writer.
+ writer->ReleaseLock(cx);
+
+ // Step 4. Return result.
+ return promise.forget();
+}
+
+already_AddRefed<Promise> FileSystemWritableFileStream::Seek(
+ uint64_t aPosition, ErrorResult& aError) {
+ // https://fs.spec.whatwg.org/#dom-filesystemwritablefilestream-seek
+ // Step 1. Let writer be the result of getting a writer for this.
+ RefPtr<WritableStreamDefaultWriter> writer = GetWriter(aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ // Step 2. Let result be the result of writing a chunk to writer given
+ // «[ "type" → "seek", "position" → position ]».
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(GetParentObject())) {
+ aError.ThrowUnknownError("Internal error");
+ return nullptr;
+ }
+
+ JSContext* cx = jsapi.cx();
+
+ RootedDictionary<WriteParams> writeParams(cx);
+ writeParams.mType = WriteCommandType::Seek;
+ writeParams.mPosition.Construct(aPosition);
+
+ JS::Rooted<JS::Value> val(cx);
+ if (!ToJSValue(cx, writeParams, &val)) {
+ aError.ThrowUnknownError("Internal error");
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise = writer->Write(cx, val, aError);
+
+ // Step 3. Release writer.
+ writer->ReleaseLock(cx);
+
+ // Step 4. Return result.
+ return promise.forget();
+}
+
+already_AddRefed<Promise> FileSystemWritableFileStream::Truncate(
+ uint64_t aSize, ErrorResult& aError) {
+ // https://fs.spec.whatwg.org/#dom-filesystemwritablefilestream-truncate
+ // Step 1. Let writer be the result of getting a writer for this.
+ RefPtr<WritableStreamDefaultWriter> writer = GetWriter(aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ // Step 2. Let result be the result of writing a chunk to writer given
+ // «[ "type" → "truncate", "size" → size ]».
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(GetParentObject())) {
+ aError.ThrowUnknownError("Internal error");
+ return nullptr;
+ }
+
+ JSContext* cx = jsapi.cx();
+
+ RootedDictionary<WriteParams> writeParams(cx);
+ writeParams.mType = WriteCommandType::Truncate;
+ writeParams.mSize.Construct(aSize);
+
+ JS::Rooted<JS::Value> val(cx);
+ if (!ToJSValue(cx, writeParams, &val)) {
+ aError.ThrowUnknownError("Internal error");
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise = writer->Write(cx, val, aError);
+
+ // Step 3. Release writer.
+ writer->ReleaseLock(cx);
+
+ // Step 4. Return result.
+ return promise.forget();
+}
+
+template <typename T>
+RefPtr<Int64Promise> FileSystemWritableFileStream::Write(
+ const T& aData, const Maybe<uint64_t> aPosition) {
+ MOZ_ASSERT(IsOpen());
+
+ nsCOMPtr<nsIInputStream> inputStream;
+
+ // https://fs.spec.whatwg.org/#write-a-chunk
+ // Step 3.4.6 If data is a BufferSource, let dataBytes be a copy of data.
+ auto vectorFromTypedArray = CreateFromTypedArrayData<Vector<uint8_t>>(aData);
+ if (vectorFromTypedArray.isSome()) {
+ Maybe<Vector<uint8_t>>& maybeVector = vectorFromTypedArray.ref();
+ QM_TRY(MOZ_TO_RESULT(maybeVector.isSome()), CreateAndRejectInt64Promise);
+
+ // Here we copy
+
+ size_t length = maybeVector->length();
+ QM_TRY(MOZ_TO_RESULT(NS_NewByteInputStream(
+ getter_AddRefs(inputStream),
+ AsChars(Span(maybeVector->extractOrCopyRawBuffer(), length)),
+ NS_ASSIGNMENT_ADOPT)),
+ CreateAndRejectInt64Promise);
+
+ return WriteImpl(std::move(inputStream), aPosition);
+ }
+
+ // Step 3.4.7 Otherwise, if data is a Blob ...
+ if (aData.IsBlob()) {
+ Blob& blob = aData.GetAsBlob();
+
+ ErrorResult error;
+ blob.CreateInputStream(getter_AddRefs(inputStream), error);
+ QM_TRY((MOZ_TO_RESULT(!error.Failed()).mapErr([&error](const nsresult rv) {
+ return error.StealNSResult();
+ })),
+ CreateAndRejectInt64Promise);
+
+ return WriteImpl(std::move(inputStream), aPosition);
+ }
+
+ // Step 3.4.8 Otherwise ...
+ MOZ_ASSERT(aData.IsUTF8String());
+
+ // Here we copy
+ nsCString dataString;
+ if (!dataString.Assign(aData.GetAsUTF8String(), mozilla::fallible)) {
+ return Int64Promise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+
+ // Input stream takes ownership
+ QM_TRY(MOZ_TO_RESULT(NS_NewCStringInputStream(getter_AddRefs(inputStream),
+ std::move(dataString))),
+ CreateAndRejectInt64Promise);
+
+ return WriteImpl(std::move(inputStream), aPosition);
+}
+
+RefPtr<Int64Promise> FileSystemWritableFileStream::WriteImpl(
+ nsCOMPtr<nsIInputStream> aInputStream, const Maybe<uint64_t> aPosition) {
+ return InvokeAsync(
+ mTaskQueue, __func__,
+ [selfHolder = fs::TargetPtrHolder(this),
+ inputStream = std::move(aInputStream), aPosition]() {
+ QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()),
+ CreateAndRejectInt64Promise);
+
+ if (aPosition.isSome()) {
+ LOG(("%p: Seeking to %" PRIu64, selfHolder->mStreamOwner.get(),
+ aPosition.value()));
+
+ QM_TRY(
+ MOZ_TO_RESULT(selfHolder->mStreamOwner->Seek(aPosition.value())),
+ CreateAndRejectInt64Promise);
+ }
+
+ nsCOMPtr<nsIOutputStream> streamSink =
+ selfHolder->mStreamOwner->OutputStream();
+
+ auto written = std::make_shared<int64_t>(0);
+ auto writingProgress = [written](uint32_t aDelta) {
+ *written += static_cast<int64_t>(aDelta);
+ };
+
+ auto promiseHolder = MakeUnique<MozPromiseHolder<Int64Promise>>();
+ RefPtr<Int64Promise> promise = promiseHolder->Ensure(__func__);
+
+ auto writingCompletion =
+ [written,
+ promiseHolder = std::move(promiseHolder)](nsresult aStatus) {
+ if (NS_SUCCEEDED(aStatus)) {
+ promiseHolder->ResolveIfExists(*written, __func__);
+ return;
+ }
+
+ promiseHolder->RejectIfExists(aStatus, __func__);
+ };
+
+ QM_TRY(MOZ_TO_RESULT(fs::AsyncCopy(
+ inputStream, streamSink, selfHolder->mTaskQueue,
+ nsAsyncCopyMode::NS_ASYNCCOPY_VIA_READSEGMENTS,
+ /* aCloseSource */ true, /* aCloseSink */ false,
+ std::move(writingProgress), std::move(writingCompletion))),
+ CreateAndRejectInt64Promise);
+
+ return promise;
+ });
+}
+
+RefPtr<BoolPromise> FileSystemWritableFileStream::Seek(uint64_t aPosition) {
+ MOZ_ASSERT(IsOpen());
+
+ LOG_VERBOSE(("%p: Seeking to %" PRIu64, mStreamOwner.get(), aPosition));
+
+ return InvokeAsync(
+ mTaskQueue, __func__,
+ [selfHolder = fs::TargetPtrHolder(this), aPosition]() mutable {
+ QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()),
+ CreateAndRejectBoolPromise);
+
+ QM_TRY(MOZ_TO_RESULT(selfHolder->mStreamOwner->Seek(aPosition)),
+ CreateAndRejectBoolPromise);
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+RefPtr<BoolPromise> FileSystemWritableFileStream::Truncate(uint64_t aSize) {
+ MOZ_ASSERT(IsOpen());
+
+ return InvokeAsync(
+ mTaskQueue, __func__,
+ [selfHolder = fs::TargetPtrHolder(this), aSize]() mutable {
+ QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()),
+ CreateAndRejectBoolPromise);
+
+ QM_TRY(MOZ_TO_RESULT(selfHolder->mStreamOwner->Truncate(aSize)),
+ CreateAndRejectBoolPromise);
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+nsresult FileSystemWritableFileStream::EnsureStream() {
+ if (!mStreamOwner) {
+ QM_TRY_UNWRAP(MovingNotNull<nsCOMPtr<nsIRandomAccessStream>> stream,
+ DeserializeRandomAccessStream(mStreamParams),
+ NS_ERROR_FAILURE);
+
+ mozilla::ipc::RandomAccessStreamParams streamParams(
+ std::move(mStreamParams));
+
+ mStreamOwner = MakeRefPtr<fs::FileSystemThreadSafeStreamOwner>(
+ this, std::move(stream));
+ }
+
+ return NS_OK;
+}
+
+void FileSystemWritableFileStream::NoteFinishedCommand() {
+ MOZ_ASSERT(mCommandActive);
+
+ mCommandActive = false;
+
+ mFinishPromiseHolder.ResolveIfExists(true, __func__);
+}
+
+RefPtr<BoolPromise> FileSystemWritableFileStream::Finish() {
+ if (!mCommandActive) {
+ return BoolPromise::CreateAndResolve(true, __func__);
+ }
+
+ return mFinishPromiseHolder.Ensure(__func__);
+}
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(
+ WritableFileStreamUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase)
+NS_IMPL_CYCLE_COLLECTION_INHERITED(WritableFileStreamUnderlyingSinkAlgorithms,
+ UnderlyingSinkAlgorithmsBase, mStream)
+
+// Step 3 of
+// https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream
+already_AddRefed<Promise>
+WritableFileStreamUnderlyingSinkAlgorithms::WriteCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aChunk,
+ WritableStreamDefaultController& aController, ErrorResult& aRv) {
+ return mStream->Write(aCx, aChunk, aRv);
+}
+
+// Step 4 of
+// https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream
+already_AddRefed<Promise>
+WritableFileStreamUnderlyingSinkAlgorithms::CloseCallbackImpl(
+ JSContext* aCx, ErrorResult& aRv) {
+ RefPtr<Promise> promise = Promise::Create(mStream->GetParentObject(), aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (!mStream->IsOpen()) {
+ promise->MaybeRejectWithTypeError("WritableFileStream closed");
+ return promise.forget();
+ }
+
+ mStream->BeginClose()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise](const BoolPromise::ResolveOrRejectValue& aValue) {
+ // Step 2.3. Return a promise resolved with undefined.
+ if (aValue.IsResolve()) {
+ promise->MaybeResolveWithUndefined();
+ return;
+ }
+ promise->MaybeRejectWithAbortError(
+ "Internal error closing file stream");
+ });
+
+ return promise.forget();
+}
+
+// Step 5 of
+// https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream
+already_AddRefed<Promise>
+WritableFileStreamUnderlyingSinkAlgorithms::AbortCallbackImpl(
+ JSContext* aCx, const Optional<JS::Handle<JS::Value>>& /* aReason */,
+ ErrorResult& aRv) {
+ // https://streams.spec.whatwg.org/#writablestream-set-up
+ // Step 3. Let abortAlgorithmWrapper be an algorithm that runs these steps:
+ // Step 3.3. Return a promise resolved with undefined.
+
+ RefPtr<Promise> promise = Promise::Create(mStream->GetParentObject(), aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (!mStream->IsOpen()) {
+ promise->MaybeRejectWithTypeError("WritableFileStream closed");
+ return promise.forget();
+ }
+
+ mStream->BeginAbort()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise](const BoolPromise::ResolveOrRejectValue& aValue) {
+ // Step 2.3. Return a promise resolved with undefined.
+ if (aValue.IsResolve()) {
+ promise->MaybeResolveWithUndefined();
+ return;
+ }
+ promise->MaybeRejectWithAbortError(
+ "Internal error closing file stream");
+ });
+
+ return promise.forget();
+}
+
+void WritableFileStreamUnderlyingSinkAlgorithms::ReleaseObjects() {
+ // WritableStream transitions to errored state whenever a rejected promise is
+ // returned. At the end of the transition, ReleaseObjects is called.
+ // Because there is no way to release the locks synchronously,
+ // we assume this has been initiated before the rejected promise is returned.
+ MOZ_ASSERT(!mStream->IsOpen());
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/api/FileSystemWritableFileStream.h b/dom/fs/api/FileSystemWritableFileStream.h
new file mode 100644
index 0000000000..e681840962
--- /dev/null
+++ b/dom/fs/api/FileSystemWritableFileStream.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 DOM_FS_FILESYSTEMWRITABLEFILESTREAM_H_
+#define DOM_FS_FILESYSTEMWRITABLEFILESTREAM_H_
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/WritableStream.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+
+class nsIGlobalObject;
+class nsIRandomAccessStream;
+
+namespace mozilla {
+
+template <typename T>
+class Buffer;
+class ErrorResult;
+class TaskQueue;
+
+namespace ipc {
+class RandomAccessStreamParams;
+}
+
+namespace dom {
+
+class ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams;
+class Blob;
+class FileSystemManager;
+class FileSystemWritableFileStreamChild;
+class OwningArrayBufferViewOrArrayBufferOrBlobOrUSVString;
+class Promise;
+class StrongWorkerRef;
+
+namespace fs {
+class FileSystemThreadSafeStreamOwner;
+}
+
+class FileSystemWritableFileStream final : public WritableStream {
+ public:
+ using WriteDataPromise =
+ MozPromise<Maybe<int64_t>, CopyableErrorResult, /* IsExclusive */ true>;
+
+ static Result<RefPtr<FileSystemWritableFileStream>, nsresult> Create(
+ const nsCOMPtr<nsIGlobalObject>& aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
+ RefPtr<FileSystemWritableFileStreamChild> aActor,
+ const fs::FileSystemEntryMetadata& aMetadata);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FileSystemWritableFileStream,
+ WritableStream)
+
+ void LastRelease() override;
+
+ void ClearActor();
+
+ class Command;
+ RefPtr<Command> CreateCommand();
+
+ bool IsCommandActive() const;
+
+ bool IsOpen() const;
+
+ bool IsFinishing() const;
+
+ bool IsDone() const;
+
+ [[nodiscard]] RefPtr<BoolPromise> BeginAbort();
+
+ [[nodiscard]] RefPtr<BoolPromise> BeginClose();
+
+ [[nodiscard]] RefPtr<BoolPromise> OnDone();
+
+ already_AddRefed<Promise> Write(JSContext* aCx, JS::Handle<JS::Value> aChunk,
+ ErrorResult& aError);
+
+ // WebIDL Boilerplate
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL Interface
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> Write(
+ const ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams& aData,
+ ErrorResult& aError);
+
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> Seek(uint64_t aPosition,
+ ErrorResult& aError);
+
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> Truncate(uint64_t aSize,
+ ErrorResult& aError);
+
+ private:
+ class CloseHandler;
+
+ FileSystemWritableFileStream(
+ const nsCOMPtr<nsIGlobalObject>& aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
+ RefPtr<FileSystemWritableFileStreamChild> aActor,
+ already_AddRefed<TaskQueue> aTaskQueue,
+ const fs::FileSystemEntryMetadata& aMetadata);
+
+ virtual ~FileSystemWritableFileStream();
+
+ [[nodiscard]] RefPtr<BoolPromise> BeginFinishing(bool aShouldAbort);
+
+ RefPtr<WriteDataPromise> Write(
+ ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams& aData);
+
+ template <typename T>
+ RefPtr<Int64Promise> Write(const T& aData, const Maybe<uint64_t> aPosition);
+
+ RefPtr<Int64Promise> WriteImpl(nsCOMPtr<nsIInputStream> aInputStream,
+ const Maybe<uint64_t> aPosition);
+
+ RefPtr<BoolPromise> Seek(uint64_t aPosition);
+
+ RefPtr<BoolPromise> Truncate(uint64_t aSize);
+
+ nsresult EnsureStream();
+
+ void NoteFinishedCommand();
+
+ [[nodiscard]] RefPtr<BoolPromise> Finish();
+
+ RefPtr<FileSystemManager> mManager;
+
+ RefPtr<FileSystemWritableFileStreamChild> mActor;
+
+ RefPtr<TaskQueue> mTaskQueue;
+
+ RefPtr<fs::FileSystemThreadSafeStreamOwner> mStreamOwner;
+
+ RefPtr<StrongWorkerRef> mWorkerRef;
+
+ mozilla::ipc::RandomAccessStreamParams mStreamParams;
+
+ fs::FileSystemEntryMetadata mMetadata;
+
+ RefPtr<CloseHandler> mCloseHandler;
+
+ MozPromiseHolder<BoolPromise> mFinishPromiseHolder;
+
+ bool mCommandActive;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_FILESYSTEMWRITABLEFILESTREAM_H_
diff --git a/dom/fs/api/moz.build b/dom/fs/api/moz.build
new file mode 100644
index 0000000000..1b9a64996d
--- /dev/null
+++ b/dom/fs/api/moz.build
@@ -0,0 +1,38 @@
+# -*- 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 += [
+ "FileSystemDirectoryHandle.h",
+ "FileSystemDirectoryIterator.h",
+ "FileSystemFileHandle.h",
+ "FileSystemHandle.h",
+ "FileSystemManager.h",
+ "FileSystemSyncAccessHandle.h",
+ "FileSystemWritableFileStream.h",
+]
+
+UNIFIED_SOURCES += [
+ "FileSystemDirectoryHandle.cpp",
+ "FileSystemDirectoryIterator.cpp",
+ "FileSystemFileHandle.cpp",
+ "FileSystemHandle.cpp",
+ "FileSystemManager.cpp",
+ "FileSystemSyncAccessHandle.cpp",
+ "FileSystemWritableFileStream.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/fs/child",
+ "/dom/fs/include",
+ "/netwerk/base",
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/fs/child/FileSystemAccessHandleChild.cpp b/dom/fs/child/FileSystemAccessHandleChild.cpp
new file mode 100644
index 0000000000..b32d9b2dc6
--- /dev/null
+++ b/dom/fs/child/FileSystemAccessHandleChild.cpp
@@ -0,0 +1,34 @@
+/* -*- 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 "FileSystemAccessHandleChild.h"
+
+#include "mozilla/dom/FileSystemSyncAccessHandle.h"
+#include "private/pprio.h"
+
+namespace mozilla::dom {
+
+FileSystemAccessHandleChild::FileSystemAccessHandleChild()
+ : mAccessHandle(nullptr) {}
+
+FileSystemAccessHandleChild::~FileSystemAccessHandleChild() = default;
+
+void FileSystemAccessHandleChild::SetAccessHandle(
+ FileSystemSyncAccessHandle* aAccessHandle) {
+ MOZ_ASSERT(aAccessHandle);
+ MOZ_ASSERT(!mAccessHandle);
+
+ mAccessHandle = aAccessHandle;
+}
+
+void FileSystemAccessHandleChild::ActorDestroy(ActorDestroyReason aWhy) {
+ if (mAccessHandle) {
+ mAccessHandle->ClearActor();
+ mAccessHandle = nullptr;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/child/FileSystemAccessHandleChild.h b/dom/fs/child/FileSystemAccessHandleChild.h
new file mode 100644
index 0000000000..d616178646
--- /dev/null
+++ b/dom/fs/child/FileSystemAccessHandleChild.h
@@ -0,0 +1,41 @@
+/* -*- 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_FS_CHILD_FILESYSTEMACCESSHANDLECHILD_H_
+#define DOM_FS_CHILD_FILESYSTEMACCESSHANDLECHILD_H_
+
+#include "mozilla/dom/PFileSystemAccessHandleChild.h"
+
+namespace mozilla::dom {
+
+class FileSystemSyncAccessHandle;
+
+class FileSystemAccessHandleChild : public PFileSystemAccessHandleChild {
+ public:
+ FileSystemAccessHandleChild();
+
+ NS_INLINE_DECL_REFCOUNTING(FileSystemAccessHandleChild, override)
+
+ FileSystemSyncAccessHandle* MutableAccessHandlePtr() const {
+ MOZ_ASSERT(mAccessHandle);
+ return mAccessHandle;
+ }
+
+ void SetAccessHandle(FileSystemSyncAccessHandle* aAccessHandle);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ virtual ~FileSystemAccessHandleChild();
+
+ // Use a weak ref so actor does not hold DOM object alive past content use.
+ // The weak reference is cleared in FileSystemSyncAccessHandle::LastRelease.
+ FileSystemSyncAccessHandle* MOZ_NON_OWNING_REF mAccessHandle;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_FS_CHILD_FILESYSTEMACCESSHANDLECHILD_H_
diff --git a/dom/fs/child/FileSystemAccessHandleControlChild.cpp b/dom/fs/child/FileSystemAccessHandleControlChild.cpp
new file mode 100644
index 0000000000..60cb4e621b
--- /dev/null
+++ b/dom/fs/child/FileSystemAccessHandleControlChild.cpp
@@ -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/. */
+
+#include "FileSystemAccessHandleControlChild.h"
+
+#include "mozilla/dom/FileSystemSyncAccessHandle.h"
+
+namespace mozilla::dom {
+
+void FileSystemAccessHandleControlChild::SetAccessHandle(
+ FileSystemSyncAccessHandle* aAccessHandle) {
+ MOZ_ASSERT(aAccessHandle);
+ MOZ_ASSERT(!mAccessHandle);
+
+ mAccessHandle = aAccessHandle;
+}
+
+void FileSystemAccessHandleControlChild::Shutdown() {
+ if (!CanSend()) {
+ return;
+ }
+
+ Close();
+}
+
+void FileSystemAccessHandleControlChild::ActorDestroy(
+ ActorDestroyReason /* aWhy */) {
+ if (mAccessHandle) {
+ mAccessHandle->ClearControlActor();
+ mAccessHandle = nullptr;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/child/FileSystemAccessHandleControlChild.h b/dom/fs/child/FileSystemAccessHandleControlChild.h
new file mode 100644
index 0000000000..93ddb36e55
--- /dev/null
+++ b/dom/fs/child/FileSystemAccessHandleControlChild.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_FS_CHILD_FILESYSTEMACCESSHANDLECONTROLCHILD_H_
+#define DOM_FS_CHILD_FILESYSTEMACCESSHANDLECONTROLCHILD_H_
+
+#include "mozilla/dom/PFileSystemAccessHandleControlChild.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla::dom {
+
+class FileSystemSyncAccessHandle;
+
+class FileSystemAccessHandleControlChild
+ : public PFileSystemAccessHandleControlChild {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_WITH_DESTROY(FileSystemAccessHandleControlChild,
+ Destroy(), override)
+
+ void SetAccessHandle(FileSystemSyncAccessHandle* aAccessHandle);
+
+ void Shutdown();
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ protected:
+ virtual ~FileSystemAccessHandleControlChild() = default;
+
+ // This is called when the object's refcount drops to zero. We use this custom
+ // callback to close the top level actor if it hasn't been explicitly closed
+ // yet. For example when `FileSystemSyncAccessHandle::Create` fails after
+ // creating and binding the top level actor.
+ virtual void Destroy() {
+ Shutdown();
+ delete this;
+ }
+
+ // The weak reference is cleared in ActorDestroy.
+ FileSystemSyncAccessHandle* MOZ_NON_OWNING_REF mAccessHandle;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_FS_CHILD_FILESYSTEMACCESSHANDLECONTROLCHILD_H_
diff --git a/dom/fs/child/FileSystemAsyncCopy.cpp b/dom/fs/child/FileSystemAsyncCopy.cpp
new file mode 100644
index 0000000000..782d67b5cd
--- /dev/null
+++ b/dom/fs/child/FileSystemAsyncCopy.cpp
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "fs/FileSystemAsyncCopy.h"
+
+#include "fs/FileSystemConstants.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+
+namespace mozilla::dom::fs {
+
+nsresult AsyncCopy(nsIInputStream* aSource, nsIOutputStream* aSink,
+ nsISerialEventTarget* aIOTarget, const nsAsyncCopyMode aMode,
+ const bool aCloseSource, const bool aCloseSink,
+ std::function<void(uint32_t)>&& aProgressCallback,
+ MoveOnlyFunction<void(nsresult)>&& aCompleteCallback) {
+ struct CallbackClosure {
+ CallbackClosure(std::function<void(uint32_t)>&& aProgressCallback,
+ MoveOnlyFunction<void(nsresult)>&& aCompleteCallback) {
+ mProgressCallbackWrapper = MakeUnique<std::function<void(uint32_t)>>(
+ [progressCallback = std::move(aProgressCallback)](uint32_t count) {
+ progressCallback(count);
+ });
+
+ mCompleteCallbackWrapper = MakeUnique<MoveOnlyFunction<void(nsresult)>>(
+ [completeCallback =
+ std::move(aCompleteCallback)](nsresult rv) mutable {
+ auto callback = std::move(completeCallback);
+ callback(rv);
+ });
+ }
+
+ UniquePtr<std::function<void(uint32_t)>> mProgressCallbackWrapper;
+ UniquePtr<MoveOnlyFunction<void(nsresult)>> mCompleteCallbackWrapper;
+ };
+
+ auto* callbackClosure = new CallbackClosure(std::move(aProgressCallback),
+ std::move(aCompleteCallback));
+
+ QM_TRY(
+ MOZ_TO_RESULT(NS_AsyncCopy(
+ aSource, aSink, aIOTarget, aMode, kStreamCopyBlockSize,
+ [](void* aClosure, nsresult aRv) {
+ auto* callbackClosure = static_cast<CallbackClosure*>(aClosure);
+ (*callbackClosure->mCompleteCallbackWrapper)(aRv);
+ delete callbackClosure;
+ },
+ callbackClosure, aCloseSource, aCloseSink, /* aCopierCtx */ nullptr,
+ [](void* aClosure, uint32_t aCount) {
+ auto* callbackClosure = static_cast<CallbackClosure*>(aClosure);
+ (*callbackClosure->mProgressCallbackWrapper)(aCount);
+ })),
+ [callbackClosure](nsresult rv) {
+ delete callbackClosure;
+ return rv;
+ });
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/child/FileSystemBackgroundRequestHandler.cpp b/dom/fs/child/FileSystemBackgroundRequestHandler.cpp
new file mode 100644
index 0000000000..d88ec05a8c
--- /dev/null
+++ b/dom/fs/child/FileSystemBackgroundRequestHandler.cpp
@@ -0,0 +1,142 @@
+/* -*- 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 "FileSystemBackgroundRequestHandler.h"
+
+#include "fs/FileSystemChildFactory.h"
+#include "mozilla/dom/FileSystemManagerChild.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+
+namespace mozilla::dom {
+
+FileSystemBackgroundRequestHandler::FileSystemBackgroundRequestHandler(
+ fs::FileSystemChildFactory* aChildFactory)
+ : mChildFactory(aChildFactory), mCreatingFileSystemManagerChild(false) {}
+
+FileSystemBackgroundRequestHandler::FileSystemBackgroundRequestHandler(
+ RefPtr<FileSystemManagerChild> aFileSystemManagerChild)
+ : mFileSystemManagerChild(std::move(aFileSystemManagerChild)),
+ mCreatingFileSystemManagerChild(false) {}
+
+FileSystemBackgroundRequestHandler::FileSystemBackgroundRequestHandler()
+ : FileSystemBackgroundRequestHandler(new fs::FileSystemChildFactory()) {}
+
+FileSystemBackgroundRequestHandler::~FileSystemBackgroundRequestHandler() =
+ default;
+
+void FileSystemBackgroundRequestHandler::ClearActor() {
+ MOZ_ASSERT(mFileSystemManagerChild);
+
+ mFileSystemManagerChild = nullptr;
+}
+
+void FileSystemBackgroundRequestHandler::Shutdown() {
+ mShutdown.Flip();
+
+ if (mFileSystemManagerChild) {
+ MOZ_ASSERT(!mCreatingFileSystemManagerChild);
+
+ mFileSystemManagerChild->Shutdown();
+
+ mFileSystemManagerChild = nullptr;
+
+ return;
+ }
+
+ if (mCreatingFileSystemManagerChild) {
+ MOZ_ASSERT(!mFileSystemManagerChild);
+
+ mCreateFileSystemManagerParentPromiseRequestHolder.Disconnect();
+
+ mCreatingFileSystemManagerChild = false;
+
+ // We must either resolve/reject the promise or steal the internal promise
+ // before the holder is destroyed. The former isn't possible during
+ // shutdown.
+ Unused << mCreateFileSystemManagerChildPromiseHolder.Steal();
+ }
+}
+
+const RefPtr<FileSystemManagerChild>&
+FileSystemBackgroundRequestHandler::FileSystemManagerChildStrongRef() const {
+ return mFileSystemManagerChild;
+}
+
+RefPtr<BoolPromise>
+FileSystemBackgroundRequestHandler::CreateFileSystemManagerChild(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo) {
+ MOZ_ASSERT(!mFileSystemManagerChild);
+ MOZ_ASSERT(!mShutdown);
+
+ using mozilla::ipc::BackgroundChild;
+ using mozilla::ipc::Endpoint;
+ using mozilla::ipc::PBackgroundChild;
+
+ if (!mCreatingFileSystemManagerChild) {
+ PBackgroundChild* backgroundChild =
+ BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!backgroundChild)) {
+ return BoolPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ // Create a new IPC connection
+ Endpoint<PFileSystemManagerParent> parentEndpoint;
+ Endpoint<PFileSystemManagerChild> childEndpoint;
+ MOZ_ALWAYS_SUCCEEDS(
+ PFileSystemManager::CreateEndpoints(&parentEndpoint, &childEndpoint));
+
+ RefPtr<FileSystemManagerChild> child = mChildFactory->Create();
+ if (!childEndpoint.Bind(child)) {
+ return BoolPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ mCreatingFileSystemManagerChild = true;
+
+ backgroundChild
+ ->SendCreateFileSystemManagerParent(aPrincipalInfo,
+ std::move(parentEndpoint))
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<FileSystemBackgroundRequestHandler>(this),
+ child](nsresult rv) {
+ self->mCreateFileSystemManagerParentPromiseRequestHolder
+ .Complete();
+
+ self->mCreatingFileSystemManagerChild = false;
+
+ if (NS_FAILED(rv)) {
+ self->mCreateFileSystemManagerChildPromiseHolder.RejectIfExists(
+ rv, __func__);
+ } else {
+ self->mFileSystemManagerChild = child;
+
+ self->mFileSystemManagerChild->SetBackgroundRequestHandler(
+ self);
+
+ self->mCreateFileSystemManagerChildPromiseHolder
+ .ResolveIfExists(true, __func__);
+ }
+ },
+ [self = RefPtr<FileSystemBackgroundRequestHandler>(this)](
+ const mozilla::ipc::ResponseRejectReason&) {
+ self->mCreateFileSystemManagerParentPromiseRequestHolder
+ .Complete();
+
+ self->mCreatingFileSystemManagerChild = false;
+
+ self->mCreateFileSystemManagerChildPromiseHolder.RejectIfExists(
+ NS_ERROR_FAILURE, __func__);
+ })
+ ->Track(mCreateFileSystemManagerParentPromiseRequestHolder);
+ }
+
+ return mCreateFileSystemManagerChildPromiseHolder.Ensure(__func__);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/child/FileSystemBackgroundRequestHandler.h b/dom/fs/child/FileSystemBackgroundRequestHandler.h
new file mode 100644
index 0000000000..a206375384
--- /dev/null
+++ b/dom/fs/child/FileSystemBackgroundRequestHandler.h
@@ -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/. */
+
+#ifndef DOM_FS_CHILD_FILESYSTEMBACKGROUNDREQUESTHANDLER_H_
+#define DOM_FS_CHILD_FILESYSTEMBACKGROUNDREQUESTHANDLER_H_
+
+#include "mozilla/MozPromise.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+
+template <class T>
+class RefPtr;
+
+namespace mozilla {
+
+namespace ipc {
+class PrincipalInfo;
+} // namespace ipc
+
+namespace dom {
+
+class FileSystemManagerChild;
+
+namespace fs {
+class FileSystemChildFactory;
+}
+
+class FileSystemBackgroundRequestHandler {
+ public:
+ explicit FileSystemBackgroundRequestHandler(
+ fs::FileSystemChildFactory* aChildFactory);
+
+ explicit FileSystemBackgroundRequestHandler(
+ RefPtr<FileSystemManagerChild> aFileSystemManagerChild);
+
+ FileSystemBackgroundRequestHandler();
+
+ NS_INLINE_DECL_REFCOUNTING(FileSystemBackgroundRequestHandler)
+
+ void ClearActor();
+
+ void Shutdown();
+
+ const RefPtr<FileSystemManagerChild>& FileSystemManagerChildStrongRef() const;
+
+ virtual RefPtr<mozilla::BoolPromise> CreateFileSystemManagerChild(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo);
+
+ protected:
+ virtual ~FileSystemBackgroundRequestHandler();
+
+ const UniquePtr<fs::FileSystemChildFactory> mChildFactory;
+
+ MozPromiseRequestHolder<
+ mozilla::ipc::PBackgroundChild::CreateFileSystemManagerParentPromise>
+ mCreateFileSystemManagerParentPromiseRequestHolder;
+ MozPromiseHolder<BoolPromise> mCreateFileSystemManagerChildPromiseHolder;
+
+ RefPtr<FileSystemManagerChild> mFileSystemManagerChild;
+
+ FlippedOnce<false> mShutdown;
+
+ bool mCreatingFileSystemManagerChild;
+}; // class FileSystemBackgroundRequestHandler
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_CHILD_FILESYSTEMBACKGROUNDREQUESTHANDLER_H_
diff --git a/dom/fs/child/FileSystemChildFactory.cpp b/dom/fs/child/FileSystemChildFactory.cpp
new file mode 100644
index 0000000000..bf4c493dd8
--- /dev/null
+++ b/dom/fs/child/FileSystemChildFactory.cpp
@@ -0,0 +1,19 @@
+/* -*- 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 "fs/FileSystemChildFactory.h"
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/FileSystemManagerChild.h"
+
+namespace mozilla::dom::fs {
+
+already_AddRefed<FileSystemManagerChild> FileSystemChildFactory::Create()
+ const {
+ return MakeAndAddRef<FileSystemManagerChild>();
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/child/FileSystemDirectoryIteratorFactory.cpp b/dom/fs/child/FileSystemDirectoryIteratorFactory.cpp
new file mode 100644
index 0000000000..5c9b9e60bc
--- /dev/null
+++ b/dom/fs/child/FileSystemDirectoryIteratorFactory.cpp
@@ -0,0 +1,243 @@
+/* -*- 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 "FileSystemDirectoryIteratorFactory.h"
+
+#include "FileSystemEntryMetadataArray.h"
+#include "fs/FileSystemRequestHandler.h"
+#include "jsapi.h"
+#include "mozilla/dom/FileSystemDirectoryHandle.h"
+#include "mozilla/dom/FileSystemDirectoryIterator.h"
+#include "mozilla/dom/FileSystemFileHandle.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/IterableIterator.h"
+#include "mozilla/dom/Promise-inl.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+
+namespace mozilla::dom::fs {
+
+namespace {
+
+template <IterableIteratorBase::IteratorType Type>
+struct ValueResolver;
+
+template <>
+struct ValueResolver<IterableIteratorBase::Keys> {
+ void operator()(nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ const FileSystemEntryMetadata& aValue,
+ const RefPtr<Promise>& aPromise) {
+ aPromise->MaybeResolve(aValue.entryName());
+ }
+};
+
+template <>
+struct ValueResolver<IterableIteratorBase::Values> {
+ void operator()(nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ const FileSystemEntryMetadata& aValue,
+ const RefPtr<Promise>& aPromise) {
+ RefPtr<FileSystemHandle> handle;
+
+ if (aValue.directory()) {
+ handle = new FileSystemDirectoryHandle(aGlobal, aManager, aValue);
+ } else {
+ handle = new FileSystemFileHandle(aGlobal, aManager, aValue);
+ }
+
+ aPromise->MaybeResolve(std::move(handle));
+ }
+};
+
+template <>
+struct ValueResolver<IterableIteratorBase::Entries> {
+ void operator()(nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ const FileSystemEntryMetadata& aValue,
+ const RefPtr<Promise>& aPromise) {
+ RefPtr<FileSystemHandle> handle;
+
+ if (aValue.directory()) {
+ handle = new FileSystemDirectoryHandle(aGlobal, aManager, aValue);
+ } else {
+ handle = new FileSystemFileHandle(aGlobal, aManager, aValue);
+ }
+
+ iterator_utils::ResolvePromiseWithKeyAndValue(aPromise, aValue.entryName(),
+ handle);
+ }
+};
+
+// TODO: PageSize could be compile-time shared between content and parent
+template <class ValueResolver, size_t PageSize = 1024u>
+class DoubleBufferQueueImpl
+ : public mozilla::dom::FileSystemDirectoryIterator::Impl {
+ static_assert(PageSize > 0u);
+
+ public:
+ using DataType = FileSystemEntryMetadata;
+ explicit DoubleBufferQueueImpl(const FileSystemEntryMetadata& aMetadata)
+ : mEntryId(aMetadata.entryId()),
+ mWithinPageEnd(0u),
+ mWithinPageIndex(0u),
+ mCurrentPageIsLastPage(true),
+ mPageNumber(0u) {}
+
+ // XXX This doesn't have to be public
+ void ResolveValue(nsIGlobalObject* aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ const Maybe<DataType>& aValue, RefPtr<Promise> aPromise) {
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(aPromise.get());
+
+ if (!aValue) {
+ iterator_utils::ResolvePromiseForFinished(aPromise);
+ return;
+ }
+
+ ValueResolver{}(aGlobal, aManager, *aValue, aPromise);
+ }
+
+ already_AddRefed<Promise> Next(nsIGlobalObject* aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ ErrorResult& aError) override {
+ RefPtr<Promise> promise = Promise::Create(aGlobal, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ next(aGlobal, aManager, promise, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ return promise.forget();
+ }
+
+ protected:
+ ~DoubleBufferQueueImpl() override = default;
+
+ void next(nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ RefPtr<Promise> aResult, ErrorResult& aError) {
+ LOG_VERBOSE(("next"));
+ MOZ_ASSERT(aResult);
+
+ Maybe<DataType> rawValue;
+
+ // TODO: Would it be better to prefetch the items before
+ // we hit the end of a page?
+ // How likely it is that it would that lead to wasted fetch operations?
+ if (0u == mWithinPageIndex) {
+ RefPtr<Promise> promise = Promise::Create(aGlobal, aError);
+ if (aError.Failed()) {
+ return;
+ }
+
+ auto newPage = MakeRefPtr<FileSystemEntryMetadataArray>();
+
+ promise->AddCallbacksWithCycleCollectedArgs(
+ [self = RefPtr(this), newPage](
+ JSContext*, JS::Handle<JS::Value>, ErrorResult&,
+ RefPtr<FileSystemManager>& aManager, RefPtr<Promise>& aResult) {
+ MOZ_ASSERT(0u == self->mWithinPageIndex);
+ MOZ_ASSERT(newPage->Length() <= PageSize);
+
+ const size_t startPos =
+ self->mCurrentPageIsLastPage ? 0u : PageSize;
+ if (self->mData.Length() < 2 * PageSize) {
+ self->mData.InsertElementsAt(startPos, newPage->Elements(),
+ newPage->Length());
+ } else {
+ self->mData.ReplaceElementsAt(startPos, newPage->Length(),
+ newPage->Elements(),
+ newPage->Length());
+ }
+ MOZ_ASSERT(self->mData.Length() <= 2 * PageSize);
+ self->mWithinPageEnd = newPage->Length();
+
+ Maybe<DataType> value;
+ if (0 != newPage->Length()) {
+ self->nextInternal(value);
+ }
+
+ self->ResolveValue(aResult->GetGlobalObject(), aManager, value,
+ aResult);
+ },
+ [](JSContext*, JS::Handle<JS::Value> aValue, ErrorResult&,
+ RefPtr<FileSystemManager>&,
+ RefPtr<Promise>& aResult) { aResult->MaybeReject(aValue); },
+ aManager, aResult);
+
+ FileSystemRequestHandler{}.GetEntries(aManager, mEntryId, mPageNumber,
+ promise, newPage, aError);
+ if (aError.Failed()) {
+ return;
+ }
+
+ ++mPageNumber;
+ return;
+ }
+
+ nextInternal(rawValue);
+
+ ResolveValue(aGlobal, aManager, rawValue, aResult);
+ }
+
+ bool nextInternal(Maybe<DataType>& aNext) {
+ if (mWithinPageIndex >= mWithinPageEnd) {
+ return false;
+ }
+
+ const size_t previous =
+ mWithinPageIndex + (mCurrentPageIsLastPage ? 0 : PageSize);
+ MOZ_ASSERT(2u * PageSize > previous);
+ MOZ_ASSERT(previous < mData.Length());
+
+ ++mWithinPageIndex;
+
+ if (mWithinPageIndex == PageSize) {
+ // Page end reached
+ mWithinPageIndex = 0u;
+ mCurrentPageIsLastPage = !mCurrentPageIsLastPage;
+ }
+
+ aNext = Some(mData[previous]);
+ return true;
+ }
+
+ const EntryId mEntryId;
+
+ nsTArray<DataType> mData; // TODO: Fixed size above one page?
+
+ size_t mWithinPageEnd = 0u;
+ size_t mWithinPageIndex = 0u;
+ bool mCurrentPageIsLastPage = true; // In the beginning, first page is free
+ PageNumber mPageNumber = 0u;
+};
+
+template <IterableIteratorBase::IteratorType Type>
+using UnderlyingQueue = DoubleBufferQueueImpl<ValueResolver<Type>>;
+
+} // namespace
+
+already_AddRefed<mozilla::dom::FileSystemDirectoryIterator::Impl>
+FileSystemDirectoryIteratorFactory::Create(
+ const FileSystemEntryMetadata& aMetadata,
+ IterableIteratorBase::IteratorType aType) {
+ if (IterableIteratorBase::Entries == aType) {
+ return MakeAndAddRef<UnderlyingQueue<IterableIteratorBase::Entries>>(
+ aMetadata);
+ }
+
+ if (IterableIteratorBase::Values == aType) {
+ return MakeAndAddRef<UnderlyingQueue<IterableIteratorBase::Values>>(
+ aMetadata);
+ }
+
+ return MakeAndAddRef<UnderlyingQueue<IterableIteratorBase::Keys>>(aMetadata);
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/child/FileSystemDirectoryIteratorFactory.h b/dom/fs/child/FileSystemDirectoryIteratorFactory.h
new file mode 100644
index 0000000000..54574ab5d2
--- /dev/null
+++ b/dom/fs/child/FileSystemDirectoryIteratorFactory.h
@@ -0,0 +1,24 @@
+/* -*- 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_FS_CHILD_FILESYSTEMDIRECTORYITERATORFACTORY_H_
+#define DOM_FS_CHILD_FILESYSTEMDIRECTORYITERATORFACTORY_H_
+
+#include "mozilla/dom/FileSystemDirectoryIterator.h"
+
+namespace mozilla::dom::fs {
+
+class FileSystemEntryMetadata;
+
+struct FileSystemDirectoryIteratorFactory {
+ static already_AddRefed<mozilla::dom::FileSystemDirectoryIterator::Impl>
+ Create(const FileSystemEntryMetadata& aMetadata,
+ IterableIteratorBase::IteratorType aType);
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_CHILD_FILESYSTEMDIRECTORYITERATORFACTORY_H_
diff --git a/dom/fs/child/FileSystemEntryMetadataArray.h b/dom/fs/child/FileSystemEntryMetadataArray.h
new file mode 100644
index 0000000000..cd5370846a
--- /dev/null
+++ b/dom/fs/child/FileSystemEntryMetadataArray.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 DOM_FS_CHILD_FILESYSTEMENTRYMETADATAARRAY_H_
+#define DOM_FS_CHILD_FILESYSTEMENTRYMETADATAARRAY_H_
+
+#include "nsTArray.h"
+
+namespace mozilla::dom::fs {
+
+class FileSystemEntryMetadata;
+
+class FileSystemEntryMetadataArray : public nsTArray<FileSystemEntryMetadata> {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(FileSystemEntryMetadataArray);
+
+ private:
+ ~FileSystemEntryMetadataArray() = default;
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_CHILD_FILESYSTEMENTRYMETADATAARRAY_H_
diff --git a/dom/fs/child/FileSystemManagerChild.cpp b/dom/fs/child/FileSystemManagerChild.cpp
new file mode 100644
index 0000000000..5c1ecc3eea
--- /dev/null
+++ b/dom/fs/child/FileSystemManagerChild.cpp
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemManagerChild.h"
+
+#include "FileSystemAccessHandleChild.h"
+#include "FileSystemBackgroundRequestHandler.h"
+#include "FileSystemWritableFileStreamChild.h"
+#include "mozilla/dom/FileSystemSyncAccessHandle.h"
+#include "mozilla/dom/FileSystemWritableFileStream.h"
+
+namespace mozilla::dom {
+
+void FileSystemManagerChild::SetBackgroundRequestHandler(
+ FileSystemBackgroundRequestHandler* aBackgroundRequestHandler) {
+ MOZ_ASSERT(aBackgroundRequestHandler);
+ MOZ_ASSERT(!mBackgroundRequestHandler);
+
+ mBackgroundRequestHandler = aBackgroundRequestHandler;
+}
+
+void FileSystemManagerChild::CloseAllWritables(
+ std::function<void()>&& aCallback) {
+ nsTArray<RefPtr<BoolPromise>> promises;
+ CloseAllWritablesImpl(promises);
+
+ BoolPromise::AllSettled(GetCurrentSerialEventTarget(), promises)
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [callback = std::move(aCallback)](
+ const BoolPromise::AllSettledPromiseType::ResolveOrRejectValue&
+ /* aValues */) { callback(); });
+}
+
+#ifdef DEBUG
+bool FileSystemManagerChild::AllSyncAccessHandlesClosed() const {
+ for (const auto& item : ManagedPFileSystemAccessHandleChild()) {
+ auto* child = static_cast<FileSystemAccessHandleChild*>(item);
+ auto* handle = child->MutableAccessHandlePtr();
+
+ if (!handle->IsClosed()) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool FileSystemManagerChild::AllWritableFileStreamsClosed() const {
+ for (const auto& item : ManagedPFileSystemWritableFileStreamChild()) {
+ auto* const child = static_cast<FileSystemWritableFileStreamChild*>(item);
+ auto* const handle = child->MutableWritableFileStreamPtr();
+ if (!handle) {
+ continue;
+ }
+
+ if (!handle->IsDone()) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+#endif
+
+void FileSystemManagerChild::Shutdown() {
+ if (!CanSend()) {
+ return;
+ }
+
+ Close();
+}
+
+already_AddRefed<PFileSystemWritableFileStreamChild>
+FileSystemManagerChild::AllocPFileSystemWritableFileStreamChild() {
+ return MakeAndAddRef<FileSystemWritableFileStreamChild>();
+}
+
+::mozilla::ipc::IPCResult FileSystemManagerChild::RecvCloseAll(
+ CloseAllResolver&& aResolver) {
+ nsTArray<RefPtr<BoolPromise>> promises;
+
+ // NOTE: getFile() creates blobs that read the data from the child;
+ // we'll need to abort any reads and resolve this call only when all
+ // blobs are closed.
+
+ for (const auto& item : ManagedPFileSystemAccessHandleChild()) {
+ auto* child = static_cast<FileSystemAccessHandleChild*>(item);
+ auto* handle = child->MutableAccessHandlePtr();
+
+ if (handle->IsOpen()) {
+ promises.AppendElement(handle->BeginClose());
+ } else if (handle->IsClosing()) {
+ promises.AppendElement(handle->OnClose());
+ }
+ }
+
+ CloseAllWritablesImpl(promises);
+
+ BoolPromise::AllSettled(GetCurrentSerialEventTarget(), promises)
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [resolver = std::move(aResolver)](
+ const BoolPromise::AllSettledPromiseType::ResolveOrRejectValue&
+ /* aValues */) { resolver(NS_OK); });
+
+ return IPC_OK();
+}
+
+void FileSystemManagerChild::ActorDestroy(ActorDestroyReason aWhy) {
+ if (mBackgroundRequestHandler) {
+ mBackgroundRequestHandler->ClearActor();
+ mBackgroundRequestHandler = nullptr;
+ }
+}
+
+template <class T>
+void FileSystemManagerChild::CloseAllWritablesImpl(T& aPromises) {
+ for (const auto& item : ManagedPFileSystemWritableFileStreamChild()) {
+ auto* const child = static_cast<FileSystemWritableFileStreamChild*>(item);
+ auto* const handle = child->MutableWritableFileStreamPtr();
+
+ if (handle) {
+ if (handle->IsOpen()) {
+ aPromises.AppendElement(handle->BeginAbort());
+ } else if (handle->IsFinishing()) {
+ aPromises.AppendElement(handle->OnDone());
+ }
+ }
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/child/FileSystemManagerChild.h b/dom/fs/child/FileSystemManagerChild.h
new file mode 100644
index 0000000000..bd5520a982
--- /dev/null
+++ b/dom/fs/child/FileSystemManagerChild.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_CHILD_FILESYSTEMMANAGERCHILD_H_
+#define DOM_FS_CHILD_FILESYSTEMMANAGERCHILD_H_
+
+#include "mozilla/dom/FileSystemWritableFileStreamChild.h"
+#include "mozilla/dom/PFileSystemManagerChild.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+
+namespace mozilla::dom {
+
+class FileSystemBackgroundRequestHandler;
+
+class FileSystemManagerChild : public PFileSystemManagerChild {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_WITH_DESTROY(FileSystemManagerChild, Destroy(),
+ override)
+
+ void SetBackgroundRequestHandler(
+ FileSystemBackgroundRequestHandler* aBackgroundRequestHandler);
+
+ void CloseAllWritables(std::function<void()>&& aCallback);
+
+#ifdef DEBUG
+ virtual bool AllSyncAccessHandlesClosed() const;
+
+ virtual bool AllWritableFileStreamsClosed() const;
+#endif
+
+ virtual void Shutdown();
+
+ already_AddRefed<PFileSystemWritableFileStreamChild>
+ AllocPFileSystemWritableFileStreamChild();
+
+ ::mozilla::ipc::IPCResult RecvCloseAll(CloseAllResolver&& aResolver);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ protected:
+ virtual ~FileSystemManagerChild() = default;
+
+ virtual void Destroy() {
+ Shutdown();
+ delete this;
+ }
+
+ // The weak reference is cleared in ActorDestroy.
+ FileSystemBackgroundRequestHandler* MOZ_NON_OWNING_REF
+ mBackgroundRequestHandler;
+
+ private:
+ template <class T>
+ void CloseAllWritablesImpl(T& aPromises);
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_FS_CHILD_FILESYSTEMMANAGERCHILD_H_
diff --git a/dom/fs/child/FileSystemRequestHandler.cpp b/dom/fs/child/FileSystemRequestHandler.cpp
new file mode 100644
index 0000000000..939bd41115
--- /dev/null
+++ b/dom/fs/child/FileSystemRequestHandler.cpp
@@ -0,0 +1,641 @@
+/* -*- 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 "fs/FileSystemRequestHandler.h"
+
+#include "FileSystemEntryMetadataArray.h"
+#include "fs/FileSystemConstants.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/FileSystemAccessHandleChild.h"
+#include "mozilla/dom/FileSystemDirectoryHandle.h"
+#include "mozilla/dom/FileSystemFileHandle.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/FileSystemHelpers.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/FileSystemManagerChild.h"
+#include "mozilla/dom/FileSystemSyncAccessHandle.h"
+#include "mozilla/dom/FileSystemWritableFileStream.h"
+#include "mozilla/dom/FileSystemWritableFileStreamChild.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerRef.h"
+#include "mozilla/dom/fs/IPCRejectReporter.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/ipc/RandomAccessStreamUtils.h"
+
+namespace mozilla::dom::fs {
+
+using mozilla::ipc::RejectCallback;
+
+namespace {
+
+void HandleFailedStatus(nsresult aError, const RefPtr<Promise>& aPromise) {
+ switch (aError) {
+ case NS_ERROR_FILE_ACCESS_DENIED:
+ aPromise->MaybeRejectWithNotAllowedError("Permission denied");
+ break;
+ case NS_ERROR_FILE_NOT_FOUND:
+ [[fallthrough]];
+ case NS_ERROR_DOM_NOT_FOUND_ERR:
+ aPromise->MaybeRejectWithNotFoundError("Entry not found");
+ break;
+ case NS_ERROR_DOM_FILESYSTEM_NO_MODIFICATION_ALLOWED_ERR:
+ aPromise->MaybeRejectWithInvalidModificationError("Disallowed by system");
+ break;
+ case NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR:
+ aPromise->MaybeRejectWithNoModificationAllowedError(
+ "No modification allowed");
+ break;
+ case NS_ERROR_DOM_TYPE_MISMATCH_ERR:
+ aPromise->MaybeRejectWithTypeMismatchError("Wrong type");
+ break;
+ case NS_ERROR_DOM_INVALID_MODIFICATION_ERR:
+ aPromise->MaybeRejectWithInvalidModificationError("Invalid modification");
+ break;
+ default:
+ if (NS_FAILED(aError)) {
+ aPromise->MaybeRejectWithUnknownError("Unknown failure");
+ } else {
+ aPromise->MaybeResolveWithUndefined();
+ }
+ break;
+ }
+}
+
+bool MakeResolution(nsIGlobalObject* aGlobal,
+ FileSystemGetEntriesResponse&& aResponse,
+ const bool& /* aResult */,
+ RefPtr<FileSystemEntryMetadataArray>& aSink) {
+ // TODO: Add page size to FileSystemConstants, preallocate and handle overflow
+ const auto& listing = aResponse.get_FileSystemDirectoryListing();
+
+ for (const auto& it : listing.files()) {
+ aSink->AppendElement(it);
+ }
+
+ for (const auto& it : listing.directories()) {
+ aSink->AppendElement(it);
+ }
+
+ return true;
+}
+
+RefPtr<FileSystemDirectoryHandle> MakeResolution(
+ nsIGlobalObject* aGlobal, FileSystemGetHandleResponse&& aResponse,
+ const RefPtr<FileSystemDirectoryHandle>& /* aResult */,
+ RefPtr<FileSystemManager>& aManager) {
+ RefPtr<FileSystemDirectoryHandle> result = new FileSystemDirectoryHandle(
+ aGlobal, aManager,
+ FileSystemEntryMetadata(aResponse.get_EntryId(), kRootName,
+ /* directory */ true));
+ return result;
+}
+
+RefPtr<FileSystemDirectoryHandle> MakeResolution(
+ nsIGlobalObject* aGlobal, FileSystemGetHandleResponse&& aResponse,
+ const RefPtr<FileSystemDirectoryHandle>& /* aResult */, const Name& aName,
+ RefPtr<FileSystemManager>& aManager) {
+ RefPtr<FileSystemDirectoryHandle> result = new FileSystemDirectoryHandle(
+ aGlobal, aManager,
+ FileSystemEntryMetadata(aResponse.get_EntryId(), aName,
+ /* directory */ true));
+
+ return result;
+}
+
+RefPtr<FileSystemFileHandle> MakeResolution(
+ nsIGlobalObject* aGlobal, FileSystemGetHandleResponse&& aResponse,
+ const RefPtr<FileSystemFileHandle>& /* aResult */, const Name& aName,
+ RefPtr<FileSystemManager>& aManager) {
+ RefPtr<FileSystemFileHandle> result = new FileSystemFileHandle(
+ aGlobal, aManager,
+ FileSystemEntryMetadata(aResponse.get_EntryId(), aName,
+ /* directory */ false));
+ return result;
+}
+
+RefPtr<FileSystemSyncAccessHandle> MakeResolution(
+ nsIGlobalObject* aGlobal, FileSystemGetAccessHandleResponse&& aResponse,
+ const RefPtr<FileSystemSyncAccessHandle>& /* aReturns */,
+ const FileSystemEntryMetadata& aMetadata,
+ RefPtr<FileSystemManager>& aManager) {
+ auto& properties = aResponse.get_FileSystemAccessHandleProperties();
+
+ QM_TRY_UNWRAP(
+ RefPtr<FileSystemSyncAccessHandle> result,
+ FileSystemSyncAccessHandle::Create(
+ aGlobal, aManager, std::move(properties.streamParams()),
+ std::move(properties.accessHandleChildEndpoint()),
+ std::move(properties.accessHandleControlChildEndpoint()), aMetadata),
+ nullptr);
+
+ return result;
+}
+
+RefPtr<FileSystemWritableFileStream> MakeResolution(
+ nsIGlobalObject* aGlobal,
+ FileSystemGetWritableFileStreamResponse&& aResponse,
+ const RefPtr<FileSystemWritableFileStream>& /* aReturns */,
+ const FileSystemEntryMetadata& aMetadata,
+ RefPtr<FileSystemManager>& aManager) {
+ auto& properties = aResponse.get_FileSystemWritableFileStreamProperties();
+
+ auto* const actor = static_cast<FileSystemWritableFileStreamChild*>(
+ properties.writableFileStream().AsChild().get());
+
+ QM_TRY_UNWRAP(RefPtr<FileSystemWritableFileStream> result,
+ FileSystemWritableFileStream::Create(
+ aGlobal, aManager, std::move(properties.streamParams()),
+ actor, aMetadata),
+ nullptr);
+
+ return result;
+}
+
+RefPtr<File> MakeResolution(nsIGlobalObject* aGlobal,
+ FileSystemGetFileResponse&& aResponse,
+ const RefPtr<File>& /* aResult */,
+ const Name& aName,
+ RefPtr<FileSystemManager>& aManager) {
+ auto& fileProperties = aResponse.get_FileSystemFileProperties();
+
+ RefPtr<BlobImpl> blobImpl = IPCBlobUtils::Deserialize(fileProperties.file());
+ MOZ_ASSERT(blobImpl);
+ RefPtr<File> result = File::Create(aGlobal, blobImpl);
+ return result;
+}
+
+template <class TResponse, class... Args>
+void ResolveCallback(
+ TResponse&& aResponse,
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ Args&&... args) {
+ MOZ_ASSERT(aPromise);
+ QM_TRY(OkIf(Promise::PromiseState::Pending == aPromise->State()), QM_VOID);
+
+ if (TResponse::Tnsresult == aResponse.type()) {
+ HandleFailedStatus(aResponse.get_nsresult(), aPromise);
+ return;
+ }
+
+ auto resolution = MakeResolution(aPromise->GetParentObject(),
+ std::forward<TResponse>(aResponse),
+ std::forward<Args>(args)...);
+ if (!resolution) {
+ aPromise->MaybeRejectWithUnknownError("Could not complete request");
+ return;
+ }
+
+ aPromise->MaybeResolve(resolution);
+}
+
+template <>
+void ResolveCallback(
+ FileSystemRemoveEntryResponse&& aResponse,
+ RefPtr<Promise> aPromise) { // NOLINT(performance-unnecessary-value-param)
+ MOZ_ASSERT(aPromise);
+ QM_TRY(OkIf(Promise::PromiseState::Pending == aPromise->State()), QM_VOID);
+
+ if (FileSystemRemoveEntryResponse::Tvoid_t == aResponse.type()) {
+ aPromise->MaybeResolveWithUndefined();
+ return;
+ }
+
+ MOZ_ASSERT(FileSystemRemoveEntryResponse::Tnsresult == aResponse.type());
+ HandleFailedStatus(aResponse.get_nsresult(), aPromise);
+}
+
+template <>
+void ResolveCallback(
+ FileSystemMoveEntryResponse&& aResponse,
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ FileSystemEntryMetadata* const& aEntry, const Name& aName) {
+ MOZ_ASSERT(aPromise);
+ QM_TRY(OkIf(Promise::PromiseState::Pending == aPromise->State()), QM_VOID);
+
+ if (FileSystemMoveEntryResponse::TEntryId == aResponse.type()) {
+ if (aEntry) {
+ aEntry->entryId() = std::move(aResponse.get_EntryId());
+ aEntry->entryName() = aName;
+ }
+
+ aPromise->MaybeResolveWithUndefined();
+ return;
+ }
+ MOZ_ASSERT(FileSystemMoveEntryResponse::Tnsresult == aResponse.type());
+ const auto& status = aResponse.get_nsresult();
+ MOZ_ASSERT(NS_FAILED(status));
+ HandleFailedStatus(status, aPromise);
+}
+
+template <>
+void ResolveCallback(FileSystemResolveResponse&& aResponse,
+ // NOLINTNEXTLINE(performance-unnecessary-value-param)
+ RefPtr<Promise> aPromise) {
+ MOZ_ASSERT(aPromise);
+ QM_TRY(OkIf(Promise::PromiseState::Pending == aPromise->State()), QM_VOID);
+
+ if (FileSystemResolveResponse::Tnsresult == aResponse.type()) {
+ HandleFailedStatus(aResponse.get_nsresult(), aPromise);
+ return;
+ }
+
+ auto& maybePath = aResponse.get_MaybeFileSystemPath();
+ if (maybePath.isSome()) {
+ aPromise->MaybeResolve(maybePath.value().path());
+ return;
+ }
+
+ // Spec says if there is no parent/child relationship, return null
+ aPromise->MaybeResolve(JS::NullHandleValue);
+}
+
+template <class TResponse, class TReturns, class... Args,
+ std::enable_if_t<std::is_same<TReturns, void>::value, bool> = true>
+mozilla::ipc::ResolveCallback<TResponse> SelectResolveCallback(
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ Args&&... args) {
+ using TOverload = void (*)(TResponse&&, RefPtr<Promise>, Args...);
+ return static_cast<std::function<void(TResponse&&)>>(
+ // NOLINTNEXTLINE(modernize-avoid-bind)
+ std::bind(static_cast<TOverload>(ResolveCallback), std::placeholders::_1,
+ aPromise, std::forward<Args>(args)...));
+}
+
+template <class TResponse, class TReturns, class... Args,
+ std::enable_if_t<!std::is_same<TReturns, void>::value, bool> = true>
+mozilla::ipc::ResolveCallback<TResponse> SelectResolveCallback(
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ Args&&... args) {
+ using TOverload =
+ void (*)(TResponse&&, RefPtr<Promise>, const TReturns&, Args...);
+ return static_cast<std::function<void(TResponse&&)>>(
+ // NOLINTNEXTLINE(modernize-avoid-bind)
+ std::bind(static_cast<TOverload>(ResolveCallback), std::placeholders::_1,
+ aPromise, TReturns(), std::forward<Args>(args)...));
+}
+
+void RejectCallback(
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ mozilla::ipc::ResponseRejectReason aReason) {
+ IPCRejectReporter(aReason);
+ QM_TRY(OkIf(Promise::PromiseState::Pending == aPromise->State()), QM_VOID);
+ aPromise->MaybeRejectWithUndefined();
+}
+
+mozilla::ipc::RejectCallback GetRejectCallback(
+ RefPtr<Promise> aPromise) { // NOLINT(performance-unnecessary-value-param)
+ return static_cast<mozilla::ipc::RejectCallback>(
+ // NOLINTNEXTLINE(modernize-avoid-bind)
+ std::bind(RejectCallback, aPromise, std::placeholders::_1));
+}
+
+struct BeginRequestFailureCallback {
+ explicit BeginRequestFailureCallback(RefPtr<Promise> aPromise)
+ : mPromise(std::move(aPromise)) {}
+
+ void operator()(nsresult aRv) const {
+ if (aRv == NS_ERROR_DOM_SECURITY_ERR) {
+ mPromise->MaybeRejectWithSecurityError(
+ "Security error when calling GetDirectory");
+ return;
+ }
+ mPromise->MaybeRejectWithUnknownError("Could not create actor");
+ }
+
+ RefPtr<Promise> mPromise;
+};
+
+} // namespace
+
+void FileSystemRequestHandler::GetRootHandle(
+ RefPtr<FileSystemManager>
+ aManager, // NOLINT(performance-unnecessary-value-param)
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ ErrorResult& aError) {
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(aPromise);
+ LOG(("GetRootHandle"));
+
+ if (aManager->IsShutdown()) {
+ aError.Throw(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+ return;
+ }
+
+ aManager->BeginRequest(
+ [onResolve = SelectResolveCallback<FileSystemGetHandleResponse,
+ RefPtr<FileSystemDirectoryHandle>>(
+ aPromise, aManager),
+ onReject = GetRejectCallback(aPromise)](const auto& actor) mutable {
+ actor->SendGetRootHandle(std::move(onResolve), std::move(onReject));
+ },
+ BeginRequestFailureCallback(aPromise));
+}
+
+void FileSystemRequestHandler::GetDirectoryHandle(
+ RefPtr<FileSystemManager>& aManager,
+ const FileSystemChildMetadata& aDirectory, bool aCreate,
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ ErrorResult& aError) {
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(!aDirectory.parentId().IsEmpty());
+ MOZ_ASSERT(aPromise);
+ LOG(("GetDirectoryHandle"));
+
+ if (aManager->IsShutdown()) {
+ aError.Throw(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+ return;
+ }
+
+ if (!IsValidName(aDirectory.childName())) {
+ aPromise->MaybeRejectWithTypeError("Invalid directory name");
+ return;
+ }
+
+ aManager->BeginRequest(
+ [request = FileSystemGetHandleRequest(aDirectory, aCreate),
+ onResolve = SelectResolveCallback<FileSystemGetHandleResponse,
+ RefPtr<FileSystemDirectoryHandle>>(
+ aPromise, aDirectory.childName(), aManager),
+ onReject = GetRejectCallback(aPromise)](const auto& actor) mutable {
+ actor->SendGetDirectoryHandle(request, std::move(onResolve),
+ std::move(onReject));
+ },
+ BeginRequestFailureCallback(aPromise));
+}
+
+void FileSystemRequestHandler::GetFileHandle(
+ RefPtr<FileSystemManager>& aManager, const FileSystemChildMetadata& aFile,
+ bool aCreate,
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ ErrorResult& aError) {
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(!aFile.parentId().IsEmpty());
+ MOZ_ASSERT(aPromise);
+ LOG(("GetFileHandle"));
+
+ if (aManager->IsShutdown()) {
+ aError.Throw(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+ return;
+ }
+
+ if (!IsValidName(aFile.childName())) {
+ aPromise->MaybeRejectWithTypeError("Invalid filename");
+ return;
+ }
+
+ aManager->BeginRequest(
+ [request = FileSystemGetHandleRequest(aFile, aCreate),
+ onResolve = SelectResolveCallback<FileSystemGetHandleResponse,
+ RefPtr<FileSystemFileHandle>>(
+ aPromise, aFile.childName(), aManager),
+ onReject = GetRejectCallback(aPromise)](const auto& actor) mutable {
+ actor->SendGetFileHandle(request, std::move(onResolve),
+ std::move(onReject));
+ },
+ BeginRequestFailureCallback(aPromise));
+}
+
+void FileSystemRequestHandler::GetAccessHandle(
+ RefPtr<FileSystemManager>& aManager, const FileSystemEntryMetadata& aFile,
+ const RefPtr<Promise>& aPromise, ErrorResult& aError) {
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(aPromise);
+ LOG(("GetAccessHandle %s", NS_ConvertUTF16toUTF8(aFile.entryName()).get()));
+
+ if (aManager->IsShutdown()) {
+ aError.Throw(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+ return;
+ }
+
+ aManager->BeginRequest(
+ [request = FileSystemGetAccessHandleRequest(aFile.entryId()),
+ onResolve = SelectResolveCallback<FileSystemGetAccessHandleResponse,
+ RefPtr<FileSystemSyncAccessHandle>>(
+ aPromise, aFile, aManager),
+ onReject = GetRejectCallback(aPromise)](const auto& actor) mutable {
+ actor->SendGetAccessHandle(request, std::move(onResolve),
+ std::move(onReject));
+ },
+ BeginRequestFailureCallback(aPromise));
+}
+
+void FileSystemRequestHandler::GetWritable(RefPtr<FileSystemManager>& aManager,
+ const FileSystemEntryMetadata& aFile,
+ bool aKeepData,
+ const RefPtr<Promise>& aPromise,
+ ErrorResult& aError) {
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(aPromise);
+ LOG(("GetWritable %s keep %d", NS_ConvertUTF16toUTF8(aFile.entryName()).get(),
+ aKeepData));
+
+ // XXX This should be removed once bug 1798513 is fixed.
+ if (!StaticPrefs::dom_fs_writable_file_stream_enabled()) {
+ aError.Throw(NS_ERROR_NOT_IMPLEMENTED);
+ return;
+ }
+
+ if (aManager->IsShutdown()) {
+ aError.Throw(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+ return;
+ }
+
+ aManager->BeginRequest(
+ [request = FileSystemGetWritableRequest(aFile.entryId(), aKeepData),
+ onResolve =
+ SelectResolveCallback<FileSystemGetWritableFileStreamResponse,
+ RefPtr<FileSystemWritableFileStream>>(
+ aPromise, aFile, aManager),
+ onReject = GetRejectCallback(aPromise)](const auto& actor) mutable {
+ actor->SendGetWritable(request, std::move(onResolve),
+ std::move(onReject));
+ },
+ [promise = aPromise](const auto&) {
+ promise->MaybeRejectWithUnknownError("Could not create actor");
+ });
+}
+
+void FileSystemRequestHandler::GetFile(
+ RefPtr<FileSystemManager>& aManager, const FileSystemEntryMetadata& aFile,
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ ErrorResult& aError) {
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(!aFile.entryId().IsEmpty());
+ MOZ_ASSERT(aPromise);
+ LOG(("GetFile %s", NS_ConvertUTF16toUTF8(aFile.entryName()).get()));
+
+ if (aManager->IsShutdown()) {
+ aError.Throw(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+ return;
+ }
+
+ aManager->BeginRequest(
+ [request = FileSystemGetFileRequest(aFile.entryId()),
+ onResolve =
+ SelectResolveCallback<FileSystemGetFileResponse, RefPtr<File>>(
+ aPromise, aFile.entryName(), aManager),
+ onReject = GetRejectCallback(aPromise)](const auto& actor) mutable {
+ actor->SendGetFile(request, std::move(onResolve), std::move(onReject));
+ },
+ BeginRequestFailureCallback(aPromise));
+}
+
+void FileSystemRequestHandler::GetEntries(
+ RefPtr<FileSystemManager>& aManager, const EntryId& aDirectory,
+ PageNumber aPage,
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ RefPtr<FileSystemEntryMetadataArray>& aSink, ErrorResult& aError) {
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(!aDirectory.IsEmpty());
+ MOZ_ASSERT(aPromise);
+ LOG(("GetEntries, page %u", aPage));
+
+ if (aManager->IsShutdown()) {
+ aError.Throw(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+ return;
+ }
+
+ aManager->BeginRequest(
+ [request = FileSystemGetEntriesRequest(aDirectory, aPage),
+ onResolve = SelectResolveCallback<FileSystemGetEntriesResponse, bool>(
+ aPromise, aSink),
+ onReject = GetRejectCallback(aPromise)](const auto& actor) mutable {
+ actor->SendGetEntries(request, std::move(onResolve),
+ std::move(onReject));
+ },
+ BeginRequestFailureCallback(aPromise));
+}
+
+void FileSystemRequestHandler::RemoveEntry(
+ RefPtr<FileSystemManager>& aManager, const FileSystemChildMetadata& aEntry,
+ bool aRecursive,
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ ErrorResult& aError) {
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(!aEntry.parentId().IsEmpty());
+ MOZ_ASSERT(aPromise);
+ LOG(("RemoveEntry"));
+
+ if (aManager->IsShutdown()) {
+ aError.Throw(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+ return;
+ }
+
+ if (!IsValidName(aEntry.childName())) {
+ aPromise->MaybeRejectWithTypeError("Invalid name");
+ return;
+ }
+
+ aManager->BeginRequest(
+ [request = FileSystemRemoveEntryRequest(aEntry, aRecursive),
+ onResolve =
+ SelectResolveCallback<FileSystemRemoveEntryResponse, void>(aPromise),
+ onReject = GetRejectCallback(aPromise)](const auto& actor) mutable {
+ actor->SendRemoveEntry(request, std::move(onResolve),
+ std::move(onReject));
+ },
+ BeginRequestFailureCallback(aPromise));
+}
+
+void FileSystemRequestHandler::MoveEntry(
+ RefPtr<FileSystemManager>& aManager, FileSystemHandle* aHandle,
+ FileSystemEntryMetadata* const aEntry,
+ const FileSystemChildMetadata& aNewEntry,
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ ErrorResult& aError) {
+ MOZ_ASSERT(aEntry);
+ MOZ_ASSERT(!aEntry->entryId().IsEmpty());
+ MOZ_ASSERT(aPromise);
+ LOG(("MoveEntry"));
+
+ if (aManager->IsShutdown()) {
+ aError.Throw(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+ return;
+ }
+
+ // reject invalid names: empty, path separators, current & parent directories
+ if (!IsValidName(aNewEntry.childName())) {
+ aPromise->MaybeRejectWithTypeError("Invalid name");
+ return;
+ }
+
+ aManager->BeginRequest(
+ [request = FileSystemMoveEntryRequest(*aEntry, aNewEntry),
+ onResolve = SelectResolveCallback<FileSystemMoveEntryResponse, void>(
+ aPromise, aEntry, aNewEntry.childName()),
+ onReject = GetRejectCallback(aPromise)](const auto& actor) mutable {
+ actor->SendMoveEntry(request, std::move(onResolve),
+ std::move(onReject));
+ },
+ BeginRequestFailureCallback(aPromise));
+}
+
+void FileSystemRequestHandler::RenameEntry(
+ RefPtr<FileSystemManager>& aManager, FileSystemHandle* aHandle,
+ FileSystemEntryMetadata* const aEntry, const Name& aName,
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ ErrorResult& aError) {
+ MOZ_ASSERT(aEntry);
+ MOZ_ASSERT(!aEntry->entryId().IsEmpty());
+ MOZ_ASSERT(aPromise);
+ LOG(("RenameEntry"));
+
+ if (aManager->IsShutdown()) {
+ aError.Throw(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+ return;
+ }
+
+ // reject invalid names: empty, path separators, current & parent directories
+ if (!IsValidName(aName)) {
+ aPromise->MaybeRejectWithTypeError("Invalid name");
+ return;
+ }
+
+ aManager->BeginRequest(
+ [request = FileSystemRenameEntryRequest(*aEntry, aName),
+ onResolve = SelectResolveCallback<FileSystemMoveEntryResponse, void>(
+ aPromise, aEntry, aName),
+ onReject = GetRejectCallback(aPromise)](const auto& actor) mutable {
+ actor->SendRenameEntry(request, std::move(onResolve),
+ std::move(onReject));
+ },
+ BeginRequestFailureCallback(aPromise));
+}
+
+void FileSystemRequestHandler::Resolve(
+ RefPtr<FileSystemManager>& aManager,
+ // NOLINTNEXTLINE(performance-unnecessary-value-param)
+ const FileSystemEntryPair& aEndpoints, RefPtr<Promise> aPromise,
+ ErrorResult& aError) {
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(!aEndpoints.parentId().IsEmpty());
+ MOZ_ASSERT(!aEndpoints.childId().IsEmpty());
+ MOZ_ASSERT(aPromise);
+ LOG(("Resolve"));
+
+ if (aManager->IsShutdown()) {
+ aError.Throw(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+ return;
+ }
+
+ aManager->BeginRequest(
+ [request = FileSystemResolveRequest(aEndpoints),
+ onResolve =
+ SelectResolveCallback<FileSystemResolveResponse, void>(aPromise),
+ onReject = GetRejectCallback(aPromise)](const auto& actor) mutable {
+ actor->SendResolve(request, std::move(onResolve), std::move(onReject));
+ },
+ BeginRequestFailureCallback(aPromise));
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/child/FileSystemShutdownBlocker.cpp b/dom/fs/child/FileSystemShutdownBlocker.cpp
new file mode 100644
index 0000000000..bddbf18c6b
--- /dev/null
+++ b/dom/fs/child/FileSystemShutdownBlocker.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 "fs/FileSystemShutdownBlocker.h"
+
+#include "MainThreadUtils.h"
+#include "mozilla/Services.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIAsyncShutdown.h"
+#include "nsISupportsImpl.h"
+#include "nsIWritablePropertyBag2.h"
+#include "nsStringFwd.h"
+
+namespace mozilla::dom::fs {
+
+namespace {
+
+nsString CreateBlockerName() {
+ const int32_t blockerIdLength = 32;
+ nsAutoCString blockerId;
+ blockerId.SetLength(blockerIdLength);
+ NS_MakeRandomString(blockerId.BeginWriting(), blockerIdLength);
+
+ nsString blockerName = u"OPFS_"_ns;
+ blockerName.Append(NS_ConvertUTF8toUTF16(blockerId));
+
+ return blockerName;
+}
+
+class FileSystemWritableBlocker : public FileSystemShutdownBlocker {
+ public:
+ FileSystemWritableBlocker() : mName(CreateBlockerName()) {}
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+
+ NS_IMETHODIMP Block() override;
+
+ NS_IMETHODIMP Unblock() override;
+
+ protected:
+ virtual ~FileSystemWritableBlocker() = default;
+
+ Result<already_AddRefed<nsIAsyncShutdownClient>, nsresult> GetBarrier() const;
+
+ private:
+ const nsString mName;
+};
+
+NS_IMPL_ISUPPORTS_INHERITED(FileSystemWritableBlocker,
+ FileSystemShutdownBlocker, nsIAsyncShutdownBlocker)
+
+NS_IMETHODIMP FileSystemWritableBlocker::Block() {
+ MOZ_ASSERT(NS_IsMainThread());
+ QM_TRY_UNWRAP(nsCOMPtr<nsIAsyncShutdownClient> barrier, GetBarrier());
+
+ QM_TRY(MOZ_TO_RESULT(barrier->AddBlocker(
+ this, NS_ConvertUTF8toUTF16(nsCString(__FILE__)), __LINE__,
+ NS_ConvertUTF8toUTF16(nsCString(__func__)))));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP FileSystemWritableBlocker::Unblock() {
+ MOZ_ASSERT(NS_IsMainThread());
+ QM_TRY_UNWRAP(nsCOMPtr<nsIAsyncShutdownClient> barrier, GetBarrier());
+
+ MOZ_ASSERT(NS_SUCCEEDED(barrier->RemoveBlocker(this)));
+
+ return NS_OK;
+}
+
+Result<already_AddRefed<nsIAsyncShutdownClient>, nsresult>
+FileSystemWritableBlocker::GetBarrier() const {
+ nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
+ QM_TRY(OkIf(svc), Err(NS_ERROR_FAILURE));
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier;
+ QM_TRY(MOZ_TO_RESULT(svc->GetXpcomWillShutdown(getter_AddRefs(barrier))));
+
+ return barrier.forget();
+}
+
+NS_IMETHODIMP
+FileSystemWritableBlocker::GetName(nsAString& aName) {
+ aName = mName;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FileSystemWritableBlocker::GetState(nsIPropertyBag** aBagOut) {
+ MOZ_ASSERT(aBagOut);
+
+ nsCOMPtr<nsIWritablePropertyBag2> propertyBag =
+ do_CreateInstance("@mozilla.org/hash-property-bag;1");
+
+ QM_TRY(OkIf(propertyBag), NS_ERROR_OUT_OF_MEMORY)
+
+ propertyBag.forget(aBagOut);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FileSystemWritableBlocker::BlockShutdown(
+ nsIAsyncShutdownClient* /* aBarrier */) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+class FileSystemNullBlocker : public FileSystemShutdownBlocker {
+ public:
+ NS_IMETHODIMP Block() override { return NS_OK; }
+
+ NS_IMETHODIMP Unblock() override { return NS_OK; }
+
+ protected:
+ virtual ~FileSystemNullBlocker() = default;
+};
+
+} // namespace
+
+/* static */
+already_AddRefed<FileSystemShutdownBlocker>
+FileSystemShutdownBlocker::CreateForWritable() {
+// The shutdown blocker watches for xpcom-will-shutdown which is not fired
+// during content process shutdown in release builds.
+#ifdef DEBUG
+ if (NS_IsMainThread()) {
+ RefPtr<FileSystemShutdownBlocker> shutdownBlocker =
+ new FileSystemWritableBlocker();
+
+ return shutdownBlocker.forget();
+ }
+#endif
+
+ RefPtr<FileSystemShutdownBlocker> shutdownBlocker =
+ new FileSystemNullBlocker();
+
+ return shutdownBlocker.forget();
+}
+
+NS_IMPL_ISUPPORTS(FileSystemShutdownBlocker, nsIAsyncShutdownBlocker)
+
+/* nsIAsyncShutdownBlocker methods */
+NS_IMETHODIMP
+FileSystemShutdownBlocker::GetName(nsAString& /* aName */) { return NS_OK; }
+
+NS_IMETHODIMP
+FileSystemShutdownBlocker::GetState(nsIPropertyBag** /* aBagOut */) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FileSystemShutdownBlocker::BlockShutdown(
+ nsIAsyncShutdownClient* /* aBarrier */) {
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/child/FileSystemThreadSafeStreamOwner.cpp b/dom/fs/child/FileSystemThreadSafeStreamOwner.cpp
new file mode 100644
index 0000000000..2b2d2913d9
--- /dev/null
+++ b/dom/fs/child/FileSystemThreadSafeStreamOwner.cpp
@@ -0,0 +1,104 @@
+/* -*- 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 "fs/FileSystemThreadSafeStreamOwner.h"
+
+#include "mozilla/CheckedInt.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemWritableFileStream.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsIOutputStream.h"
+#include "nsIRandomAccessStream.h"
+
+namespace mozilla::dom::fs {
+
+namespace {
+
+nsresult TruncFile(nsCOMPtr<nsIRandomAccessStream>& aStream, int64_t aEOF) {
+ int64_t offset = 0;
+ QM_TRY(MOZ_TO_RESULT(aStream->Tell(&offset)));
+
+ QM_TRY(MOZ_TO_RESULT(aStream->Seek(nsISeekableStream::NS_SEEK_SET, aEOF)));
+
+ QM_TRY(MOZ_TO_RESULT(aStream->SetEOF()));
+
+ QM_TRY(MOZ_TO_RESULT(aStream->Seek(nsISeekableStream::NS_SEEK_END, 0)));
+
+ // Restore original offset
+ QM_TRY(MOZ_TO_RESULT(aStream->Seek(nsISeekableStream::NS_SEEK_SET, offset)));
+
+ return NS_OK;
+}
+
+} // namespace
+
+FileSystemThreadSafeStreamOwner::FileSystemThreadSafeStreamOwner(
+ FileSystemWritableFileStream* aWritableFileStream,
+ nsCOMPtr<nsIRandomAccessStream>&& aStream)
+ :
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mWritableFileStream(aWritableFileStream),
+#endif
+ mStream(std::forward<nsCOMPtr<nsIRandomAccessStream>>(aStream)),
+ mClosed(false) {
+ MOZ_ASSERT(mWritableFileStream);
+}
+
+nsresult FileSystemThreadSafeStreamOwner::Truncate(uint64_t aSize) {
+ MOZ_DIAGNOSTIC_ASSERT(mWritableFileStream->IsCommandActive());
+
+ if (mClosed) { // Multiple closes can end up in a queue
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ int64_t where = 0;
+ QM_TRY(MOZ_TO_RESULT(mStream->Tell(&where)));
+
+ // Truncate filehandle (can extend with 0's)
+ LOG(("%p: Truncate to %" PRIu64, this, aSize));
+ QM_TRY(MOZ_TO_RESULT(TruncFile(mStream, aSize)));
+
+ // Per non-normative text in the spec (2.5.3) we should adjust
+ // the cursor position to be within the new file size
+ if (static_cast<uint64_t>(where) > aSize) {
+ QM_TRY(MOZ_TO_RESULT(mStream->Seek(nsISeekableStream::NS_SEEK_END, 0)));
+ }
+
+ return NS_OK;
+}
+
+nsresult FileSystemThreadSafeStreamOwner::Seek(uint64_t aPosition) {
+ MOZ_DIAGNOSTIC_ASSERT(mWritableFileStream->IsCommandActive());
+
+ if (mClosed) { // Multiple closes can end up in a queue
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ const auto checkedPosition = CheckedInt<int64_t>(aPosition);
+ if (!checkedPosition.isValid()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return mStream->Seek(nsISeekableStream::NS_SEEK_SET, checkedPosition.value());
+}
+
+void FileSystemThreadSafeStreamOwner::Close() {
+ if (mClosed) { // Multiple closes can end up in a queue
+ return;
+ }
+
+ mClosed = true;
+ mStream->OutputStream()->Close();
+}
+
+nsCOMPtr<nsIOutputStream> FileSystemThreadSafeStreamOwner::OutputStream() {
+ MOZ_DIAGNOSTIC_ASSERT(mWritableFileStream->IsCommandActive());
+
+ return mStream->OutputStream();
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/child/FileSystemWritableFileStreamChild.cpp b/dom/fs/child/FileSystemWritableFileStreamChild.cpp
new file mode 100644
index 0000000000..862e8f345e
--- /dev/null
+++ b/dom/fs/child/FileSystemWritableFileStreamChild.cpp
@@ -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/. */
+
+#include "FileSystemWritableFileStreamChild.h"
+
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemWritableFileStream.h"
+
+namespace mozilla::dom {
+
+FileSystemWritableFileStreamChild::FileSystemWritableFileStreamChild()
+ : mStream(nullptr) {
+ LOG(("Created new WritableFileStreamChild %p", this));
+}
+
+FileSystemWritableFileStreamChild::~FileSystemWritableFileStreamChild() =
+ default;
+
+void FileSystemWritableFileStreamChild::SetStream(
+ FileSystemWritableFileStream* aStream) {
+ MOZ_ASSERT(aStream);
+ MOZ_ASSERT(!mStream);
+
+ mStream = aStream;
+}
+
+void FileSystemWritableFileStreamChild::ActorDestroy(ActorDestroyReason aWhy) {
+ LOG(("Destroy WritableFileStreamChild %p", this));
+
+ if (mStream) {
+ mStream->ClearActor();
+ mStream = nullptr;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/child/FileSystemWritableFileStreamChild.h b/dom/fs/child/FileSystemWritableFileStreamChild.h
new file mode 100644
index 0000000000..9728fc4c78
--- /dev/null
+++ b/dom/fs/child/FileSystemWritableFileStreamChild.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 DOM_FS_CHILD_FILESYSTEMWRITABLEFILESTREAM_H_
+#define DOM_FS_CHILD_FILESYSTEMWRITABLEFILESTREAM_H_
+
+#include "mozilla/dom/PFileSystemWritableFileStreamChild.h"
+
+namespace mozilla::dom {
+
+class FileSystemWritableFileStream;
+
+class FileSystemWritableFileStreamChild
+ : public PFileSystemWritableFileStreamChild {
+ public:
+ FileSystemWritableFileStreamChild();
+
+ NS_INLINE_DECL_REFCOUNTING(FileSystemWritableFileStreamChild, override)
+
+ FileSystemWritableFileStream* MutableWritableFileStreamPtr() const {
+ MOZ_ASSERT(mStream);
+ return mStream;
+ }
+
+ void SetStream(FileSystemWritableFileStream* aStream);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ virtual ~FileSystemWritableFileStreamChild();
+
+ // Use a weak ref so actor does not hold DOM object alive past content use.
+ // The weak reference is cleared in FileSystemWritableFileStream::LastRelease.
+ FileSystemWritableFileStream* MOZ_NON_OWNING_REF mStream;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_FS_CHILD_FILESYSTEMWRITABLEFILESTREAM_H_
diff --git a/dom/fs/child/moz.build b/dom/fs/child/moz.build
new file mode 100644
index 0000000000..e52cb6b206
--- /dev/null
+++ b/dom/fs/child/moz.build
@@ -0,0 +1,35 @@
+# -*- 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 += [
+ "FileSystemAccessHandleChild.h",
+ "FileSystemAccessHandleControlChild.h",
+ "FileSystemManagerChild.h",
+ "FileSystemWritableFileStreamChild.h",
+]
+
+UNIFIED_SOURCES += [
+ "FileSystemAccessHandleChild.cpp",
+ "FileSystemAccessHandleControlChild.cpp",
+ "FileSystemAsyncCopy.cpp",
+ "FileSystemBackgroundRequestHandler.cpp",
+ "FileSystemChildFactory.cpp",
+ "FileSystemDirectoryIteratorFactory.cpp",
+ "FileSystemManagerChild.cpp",
+ "FileSystemRequestHandler.cpp",
+ "FileSystemShutdownBlocker.cpp",
+ "FileSystemThreadSafeStreamOwner.cpp",
+ "FileSystemWritableFileStreamChild.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/fs/include",
+ "/netwerk/base",
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/dom/fs/include/fs/FileSystemAsyncCopy.h b/dom/fs/include/fs/FileSystemAsyncCopy.h
new file mode 100644
index 0000000000..a82a7e9007
--- /dev/null
+++ b/dom/fs/include/fs/FileSystemAsyncCopy.h
@@ -0,0 +1,27 @@
+/* -*- 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_FS_ASYNCCOPY_H_
+#define DOM_FS_ASYNCCOPY_H_
+
+#include "mozilla/MoveOnlyFunction.h"
+#include "nsStreamUtils.h"
+
+class nsIInputStream;
+class nsIOutputStream;
+class nsISerialEventTarget;
+
+namespace mozilla::dom::fs {
+
+nsresult AsyncCopy(nsIInputStream* aSource, nsIOutputStream* aSink,
+ nsISerialEventTarget* aIOTarget, const nsAsyncCopyMode aMode,
+ const bool aCloseSource, const bool aCloseSink,
+ std::function<void(uint32_t)>&& aProgressCallback,
+ MoveOnlyFunction<void(nsresult)>&& aCompleteCallback);
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_ASYNCCOPY_H_
diff --git a/dom/fs/include/fs/FileSystemChildFactory.h b/dom/fs/include/fs/FileSystemChildFactory.h
new file mode 100644
index 0000000000..7fe58d65e9
--- /dev/null
+++ b/dom/fs/include/fs/FileSystemChildFactory.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_FILESYSTEMCHILDFACTORY_H_
+#define DOM_FS_FILESYSTEMCHILDFACTORY_H_
+
+#include "mozilla/AlreadyAddRefed.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+class FileSystemManagerChild;
+} // namespace dom
+} // namespace mozilla
+
+namespace mozilla::dom::fs {
+
+class FileSystemChildFactory {
+ public:
+ virtual already_AddRefed<FileSystemManagerChild> Create() const;
+
+ virtual ~FileSystemChildFactory() = default;
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_FILESYSTEMCHILDFACTORY_H_
diff --git a/dom/fs/include/fs/FileSystemConstants.h b/dom/fs/include/fs/FileSystemConstants.h
new file mode 100644
index 0000000000..761d333cdc
--- /dev/null
+++ b/dom/fs/include/fs/FileSystemConstants.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_FILESYSTEMCONSTANTS_H_
+#define DOM_FS_FILESYSTEMCONSTANTS_H_
+
+#include "nsLiteralString.h"
+
+namespace mozilla::dom::fs {
+
+constexpr nsLiteralString kRootName = u""_ns;
+
+constexpr nsLiteralString kRootString = u"root"_ns;
+
+constexpr uint32_t kStreamCopyBlockSize = 1024 * 1024;
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_FILESYSTEMCONSTANTS_H_
diff --git a/dom/fs/include/fs/FileSystemRequestHandler.h b/dom/fs/include/fs/FileSystemRequestHandler.h
new file mode 100644
index 0000000000..04d03daad9
--- /dev/null
+++ b/dom/fs/include/fs/FileSystemRequestHandler.h
@@ -0,0 +1,94 @@
+/* -*- 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_FS_CHILD_FILESYSTEMREQUESTHANDLER_H_
+#define DOM_FS_CHILD_FILESYSTEMREQUESTHANDLER_H_
+
+#include "mozilla/dom/FileSystemTypes.h"
+
+template <class T>
+class RefPtr;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+class FileSystemHandle;
+class FileSystemManager;
+class Promise;
+
+namespace fs {
+
+class FileSystemChildMetadata;
+class FileSystemEntryMetadata;
+class FileSystemEntryMetadataArray;
+class FileSystemEntryPair;
+
+class FileSystemRequestHandler {
+ public:
+ virtual void GetRootHandle(RefPtr<FileSystemManager> aManager,
+ RefPtr<Promise> aPromise, ErrorResult& aError);
+
+ virtual void GetDirectoryHandle(RefPtr<FileSystemManager>& aManager,
+ const FileSystemChildMetadata& aDirectory,
+ bool aCreate, RefPtr<Promise> aPromise,
+ ErrorResult& aError);
+
+ virtual void GetFileHandle(RefPtr<FileSystemManager>& aManager,
+ const FileSystemChildMetadata& aFile, bool aCreate,
+ RefPtr<Promise> aPromise, ErrorResult& aError);
+
+ virtual void GetFile(RefPtr<FileSystemManager>& aManager,
+ const FileSystemEntryMetadata& aFile,
+ RefPtr<Promise> aPromise, ErrorResult& aError);
+
+ virtual void GetAccessHandle(RefPtr<FileSystemManager>& aManager,
+ const FileSystemEntryMetadata& aFile,
+ const RefPtr<Promise>& aPromise,
+ ErrorResult& aError);
+
+ virtual void GetWritable(RefPtr<FileSystemManager>& aManager,
+ const FileSystemEntryMetadata& aFile, bool aKeepData,
+ const RefPtr<Promise>& aPromise,
+ ErrorResult& aError);
+
+ virtual void GetEntries(RefPtr<FileSystemManager>& aManager,
+ const EntryId& aDirectory, PageNumber aPage,
+ RefPtr<Promise> aPromise,
+ RefPtr<FileSystemEntryMetadataArray>& aSink,
+ ErrorResult& aError);
+
+ virtual void RemoveEntry(RefPtr<FileSystemManager>& aManager,
+ const FileSystemChildMetadata& aEntry,
+ bool aRecursive, RefPtr<Promise> aPromise,
+ ErrorResult& aError);
+
+ virtual void MoveEntry(RefPtr<FileSystemManager>& aManager,
+ FileSystemHandle* aHandle,
+ FileSystemEntryMetadata* const aEntry,
+ const FileSystemChildMetadata& aNewEntry,
+ RefPtr<Promise> aPromise, ErrorResult& aError);
+
+ virtual void RenameEntry(RefPtr<FileSystemManager>& aManager,
+ FileSystemHandle* aHandle,
+ FileSystemEntryMetadata* const aEntry,
+ const Name& aName, RefPtr<Promise> aPromise,
+ ErrorResult& aError);
+
+ virtual void Resolve(RefPtr<FileSystemManager>& aManager,
+ const FileSystemEntryPair& aEndpoints,
+ RefPtr<Promise> aPromise, ErrorResult& aError);
+
+ virtual ~FileSystemRequestHandler() = default;
+}; // class FileSystemRequestHandler
+
+} // namespace fs
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_CHILD_FILESYSTEMREQUESTHANDLER_H_
diff --git a/dom/fs/include/fs/FileSystemShutdownBlocker.h b/dom/fs/include/fs/FileSystemShutdownBlocker.h
new file mode 100644
index 0000000000..f11d263f4e
--- /dev/null
+++ b/dom/fs/include/fs/FileSystemShutdownBlocker.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_CHILD_FILESYSTEMMANAGERCHILDBLOCKER_H_
+#define DOM_FS_CHILD_FILESYSTEMMANAGERCHILDBLOCKER_H_
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "nsIAsyncShutdown.h"
+#include "nsISupports.h"
+
+namespace mozilla::dom::fs {
+
+class FileSystemShutdownBlocker : public nsIAsyncShutdownBlocker {
+ public:
+ static already_AddRefed<FileSystemShutdownBlocker> CreateForWritable();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+
+ virtual NS_IMETHODIMP Block() = 0;
+
+ virtual NS_IMETHODIMP Unblock() = 0;
+
+ protected:
+ virtual ~FileSystemShutdownBlocker() = default;
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_CHILD_FILESYSTEMMANAGERCHILDBLOCKER_H_
diff --git a/dom/fs/include/fs/FileSystemThreadSafeStreamOwner.h b/dom/fs/include/fs/FileSystemThreadSafeStreamOwner.h
new file mode 100644
index 0000000000..3ef7639f36
--- /dev/null
+++ b/dom/fs/include/fs/FileSystemThreadSafeStreamOwner.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_FS_FILESYSTEMTHREADSAFESTREAMOWNER_H_
+#define DOM_FS_FILESYSTEMTHREADSAFESTREAMOWNER_H_
+
+#include "nsCOMPtr.h"
+
+class nsIOutputStream;
+class nsIRandomAccessStream;
+
+namespace mozilla::dom {
+
+class FileSystemWritableFileStream;
+
+namespace fs {
+
+class FileSystemThreadSafeStreamOwner {
+ public:
+ FileSystemThreadSafeStreamOwner(
+ FileSystemWritableFileStream* aWritableFileStream,
+ nsCOMPtr<nsIRandomAccessStream>&& aStream);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemThreadSafeStreamOwner)
+
+ nsresult Truncate(uint64_t aSize);
+
+ nsresult Seek(uint64_t aPosition);
+
+ void Close();
+
+ nsCOMPtr<nsIOutputStream> OutputStream();
+
+ protected:
+ virtual ~FileSystemThreadSafeStreamOwner() = default;
+
+ private:
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ FileSystemWritableFileStream* MOZ_NON_OWNING_REF mWritableFileStream;
+#endif
+ nsCOMPtr<nsIRandomAccessStream> mStream;
+
+ bool mClosed;
+};
+
+} // namespace fs
+} // namespace mozilla::dom
+
+#endif // DOM_FS_FILESYSTEMTHREADSAFESTREAMOWNER_H_
diff --git a/dom/fs/moz.build b/dom/fs/moz.build
new file mode 100644
index 0000000000..1a1ba49ab7
--- /dev/null
+++ b/dom/fs/moz.build
@@ -0,0 +1,14 @@
+# -*- 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/.
+
+DIRS += [
+ "api",
+ "child",
+ "parent",
+ "shared",
+]
+
+TEST_DIRS += ["test"]
diff --git a/dom/fs/parent/FileSystemAccessHandle.cpp b/dom/fs/parent/FileSystemAccessHandle.cpp
new file mode 100644
index 0000000000..07430ce476
--- /dev/null
+++ b/dom/fs/parent/FileSystemAccessHandle.cpp
@@ -0,0 +1,235 @@
+/* -*- 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 "FileSystemAccessHandle.h"
+
+#include "FileSystemDatabaseManager.h"
+#include "FileSystemParentTypes.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/FileSystemDataManager.h"
+#include "mozilla/dom/FileSystemHelpers.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/quota/FileStreams.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/RemoteQuotaObjectParent.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/ipc/RandomAccessStreamParams.h"
+#include "mozilla/ipc/RandomAccessStreamUtils.h"
+#include "nsIFileStreams.h"
+
+namespace mozilla::dom {
+
+FileSystemAccessHandle::FileSystemAccessHandle(
+ RefPtr<fs::data::FileSystemDataManager> aDataManager,
+ const fs::EntryId& aEntryId, MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue)
+ : mEntryId(aEntryId),
+ mDataManager(std::move(aDataManager)),
+ mIOTaskQueue(std::move(aIOTaskQueue)),
+ mActor(nullptr),
+ mControlActor(nullptr),
+ mRegCount(0),
+ mLocked(false),
+ mRegistered(false),
+ mClosed(false) {}
+
+FileSystemAccessHandle::~FileSystemAccessHandle() {
+ MOZ_DIAGNOSTIC_ASSERT(mClosed);
+}
+
+// static
+RefPtr<FileSystemAccessHandle::CreatePromise> FileSystemAccessHandle::Create(
+ RefPtr<fs::data::FileSystemDataManager> aDataManager,
+ const fs::EntryId& aEntryId) {
+ MOZ_ASSERT(aDataManager);
+ aDataManager->AssertIsOnIOTarget();
+
+ RefPtr<TaskQueue> ioTaskQueue = TaskQueue::Create(
+ do_AddRef(aDataManager->MutableIOTargetPtr()), "FileSystemAccessHandle");
+
+ RefPtr<FileSystemAccessHandle> accessHandle = new FileSystemAccessHandle(
+ std::move(aDataManager), aEntryId, WrapMovingNotNull(ioTaskQueue));
+
+ return accessHandle->BeginInit()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [accessHandle = fs::Registered<FileSystemAccessHandle>(accessHandle)](
+ InitPromise::ResolveOrRejectValue&& value) mutable {
+ if (value.IsReject()) {
+ return CreatePromise::CreateAndReject(value.RejectValue(), __func__);
+ }
+
+ mozilla::ipc::RandomAccessStreamParams streamParams =
+ std::move(value.ResolveValue());
+
+ return CreatePromise::CreateAndResolve(
+ std::pair(std::move(accessHandle), std::move(streamParams)),
+ __func__);
+ });
+}
+
+NS_IMPL_ISUPPORTS_INHERITED0(FileSystemAccessHandle, FileSystemStreamCallbacks)
+
+void FileSystemAccessHandle::Register() { ++mRegCount; }
+
+void FileSystemAccessHandle::Unregister() {
+ MOZ_ASSERT(mRegCount > 0);
+
+ --mRegCount;
+
+ if (IsInactive() && IsOpen()) {
+ BeginClose();
+ }
+}
+
+void FileSystemAccessHandle::RegisterActor(
+ NotNull<FileSystemAccessHandleParent*> aActor) {
+ MOZ_ASSERT(!mActor);
+
+ mActor = aActor;
+}
+
+void FileSystemAccessHandle::UnregisterActor(
+ NotNull<FileSystemAccessHandleParent*> aActor) {
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mActor == aActor);
+
+ mActor = nullptr;
+
+ if (IsInactive() && IsOpen()) {
+ BeginClose();
+ }
+}
+
+void FileSystemAccessHandle::RegisterControlActor(
+ NotNull<FileSystemAccessHandleControlParent*> aControlActor) {
+ MOZ_ASSERT(!mControlActor);
+
+ mControlActor = aControlActor;
+}
+
+void FileSystemAccessHandle::UnregisterControlActor(
+ NotNull<FileSystemAccessHandleControlParent*> aControlActor) {
+ MOZ_ASSERT(mControlActor);
+ MOZ_ASSERT(mControlActor == aControlActor);
+
+ mControlActor = nullptr;
+
+ if (IsInactive() && IsOpen()) {
+ BeginClose();
+ }
+}
+
+bool FileSystemAccessHandle::IsOpen() const { return !mClosed; }
+
+RefPtr<BoolPromise> FileSystemAccessHandle::BeginClose() {
+ MOZ_ASSERT(IsOpen());
+
+ LOG(("Closing AccessHandle"));
+
+ mClosed = true;
+
+ return InvokeAsync(mIOTaskQueue.get(), __func__,
+ [self = RefPtr(this)]() {
+ if (self->mRemoteQuotaObjectParent) {
+ self->mRemoteQuotaObjectParent->Close();
+ }
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue&) {
+ return self->mIOTaskQueue->BeginShutdown();
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr(this)](const ShutdownPromise::ResolveOrRejectValue&) {
+ if (self->mLocked) {
+ self->mDataManager->UnlockExclusive(self->mEntryId);
+ }
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(mDataManager->MutableBackgroundTargetPtr(), __func__,
+ [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue&) {
+ if (self->mRegistered) {
+ self->mDataManager->UnregisterAccessHandle(WrapNotNull(self));
+ }
+
+ self->mDataManager = nullptr;
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+bool FileSystemAccessHandle::IsInactive() const {
+ return !mRegCount && !mActor && !mControlActor;
+}
+
+RefPtr<FileSystemAccessHandle::InitPromise>
+FileSystemAccessHandle::BeginInit() {
+ QM_TRY_UNWRAP(fs::FileId fileId, mDataManager->LockExclusive(mEntryId),
+ [](const auto& aRv) {
+ return InitPromise::CreateAndReject(ToNSResult(aRv),
+ __func__);
+ });
+
+ mLocked = true;
+
+ auto CreateAndRejectInitPromise = [](const char* aFunc, nsresult aRv) {
+ return CreateAndRejectMozPromise<InitPromise>(aFunc, aRv);
+ };
+
+ fs::ContentType type;
+ fs::TimeStamp lastModifiedMilliSeconds;
+ fs::Path path;
+ nsCOMPtr<nsIFile> file;
+ QM_TRY(MOZ_TO_RESULT(mDataManager->MutableDatabaseManagerPtr()->GetFile(
+ mEntryId, fileId, fs::FileMode::EXCLUSIVE, type,
+ lastModifiedMilliSeconds, path, file)),
+ CreateAndRejectInitPromise);
+
+ if (LOG_ENABLED()) {
+ nsAutoString path;
+ if (NS_SUCCEEDED(file->GetPath(path))) {
+ LOG(("Opening SyncAccessHandle %s", NS_ConvertUTF16toUTF8(path).get()));
+ }
+ }
+
+ return InvokeAsync(
+ mDataManager->MutableBackgroundTargetPtr(), __func__,
+ [self = RefPtr(this)]() {
+ self->mDataManager->RegisterAccessHandle(WrapNotNull(self));
+
+ self->mRegistered = true;
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(mIOTaskQueue.get(), __func__,
+ [self = RefPtr(this), CreateAndRejectInitPromise,
+ file = std::move(file)](
+ const BoolPromise::ResolveOrRejectValue& value) {
+ if (value.IsReject()) {
+ return InitPromise::CreateAndReject(value.RejectValue(),
+ __func__);
+ }
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIRandomAccessStream> stream,
+ CreateFileRandomAccessStream(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ self->mDataManager->OriginMetadataRef(),
+ quota::Client::FILESYSTEM, file, -1, -1,
+ nsIFileRandomAccessStream::DEFER_OPEN),
+ CreateAndRejectInitPromise);
+
+ mozilla::ipc::RandomAccessStreamParams streamParams =
+ mozilla::ipc::SerializeRandomAccessStream(
+ WrapMovingNotNullUnchecked(std::move(stream)), self);
+
+ return InitPromise::CreateAndResolve(std::move(streamParams),
+ __func__);
+ });
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/parent/FileSystemAccessHandle.h b/dom/fs/parent/FileSystemAccessHandle.h
new file mode 100644
index 0000000000..74b177d04d
--- /dev/null
+++ b/dom/fs/parent/FileSystemAccessHandle.h
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_PARENT_FILESYSTEMACCESSHANDLE_H_
+#define DOM_FS_PARENT_FILESYSTEMACCESSHANDLE_H_
+
+#include "FileSystemStreamCallbacks.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "nsISupportsImpl.h"
+#include "nsString.h"
+
+enum class nsresult : uint32_t;
+
+namespace mozilla {
+
+class TaskQueue;
+
+namespace ipc {
+class RandomAccessStreamParams;
+}
+
+namespace dom {
+
+class FileSystemAccessHandleControlParent;
+class FileSystemAccessHandleParent;
+
+namespace fs {
+
+template <class T>
+class Registered;
+
+namespace data {
+
+class FileSystemDataManager;
+
+} // namespace data
+} // namespace fs
+
+class FileSystemAccessHandle : public FileSystemStreamCallbacks {
+ public:
+ using CreateResult = std::pair<fs::Registered<FileSystemAccessHandle>,
+ mozilla::ipc::RandomAccessStreamParams>;
+ // IsExclusive is true because we want to allow moving of CreateResult.
+ // There's always just one consumer anyway (When IsExclusive is true, there
+ // can be at most one call to either Then or ChainTo).
+ using CreatePromise = MozPromise<CreateResult, nsresult,
+ /* IsExclusive */ true>;
+ static RefPtr<CreatePromise> Create(
+ RefPtr<fs::data::FileSystemDataManager> aDataManager,
+ const fs::EntryId& aEntryId);
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ void Register();
+
+ void Unregister();
+
+ void RegisterActor(NotNull<FileSystemAccessHandleParent*> aActor);
+
+ void UnregisterActor(NotNull<FileSystemAccessHandleParent*> aActor);
+
+ void RegisterControlActor(
+ NotNull<FileSystemAccessHandleControlParent*> aControlActor);
+
+ void UnregisterControlActor(
+ NotNull<FileSystemAccessHandleControlParent*> aControlActor);
+
+ bool IsOpen() const;
+
+ RefPtr<BoolPromise> BeginClose();
+
+ private:
+ FileSystemAccessHandle(RefPtr<fs::data::FileSystemDataManager> aDataManager,
+ const fs::EntryId& aEntryId,
+ MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue);
+
+ ~FileSystemAccessHandle();
+
+ bool IsInactive() const;
+
+ using InitPromise =
+ MozPromise<mozilla::ipc::RandomAccessStreamParams, nsresult,
+ /* IsExclusive */ true>;
+ RefPtr<InitPromise> BeginInit();
+
+ const fs::EntryId mEntryId;
+ RefPtr<fs::data::FileSystemDataManager> mDataManager;
+ const NotNull<RefPtr<TaskQueue>> mIOTaskQueue;
+ FileSystemAccessHandleParent* mActor;
+ FileSystemAccessHandleControlParent* mControlActor;
+ nsAutoRefCnt mRegCount;
+ bool mLocked;
+ bool mRegistered;
+ bool mClosed;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_PARENT_FILESYSTEMACCESSHANDLE_H_
diff --git a/dom/fs/parent/FileSystemAccessHandleControlParent.cpp b/dom/fs/parent/FileSystemAccessHandleControlParent.cpp
new file mode 100644
index 0000000000..90f325aeb5
--- /dev/null
+++ b/dom/fs/parent/FileSystemAccessHandleControlParent.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 "FileSystemAccessHandleControlParent.h"
+
+#include "mozilla/dom/FileSystemAccessHandle.h"
+#include "mozilla/ipc/IPCCore.h"
+
+namespace mozilla::dom {
+
+FileSystemAccessHandleControlParent::FileSystemAccessHandleControlParent(
+ RefPtr<FileSystemAccessHandle> aAccessHandle)
+ : mAccessHandle(std::move(aAccessHandle)) {}
+
+FileSystemAccessHandleControlParent::~FileSystemAccessHandleControlParent() {
+ MOZ_ASSERT(mActorDestroyed);
+}
+
+mozilla::ipc::IPCResult FileSystemAccessHandleControlParent::RecvClose(
+ CloseResolver&& aResolver) {
+ mAccessHandle->BeginClose()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [resolver = std::move(aResolver)](
+ const BoolPromise::ResolveOrRejectValue&) { resolver(void_t()); });
+
+ return IPC_OK();
+}
+
+void FileSystemAccessHandleControlParent::ActorDestroy(
+ ActorDestroyReason /* aWhy */) {
+ MOZ_ASSERT(!mActorDestroyed);
+
+#ifdef DEBUG
+ mActorDestroyed = true;
+#endif
+
+ mAccessHandle->UnregisterControlActor(WrapNotNullUnchecked(this));
+
+ mAccessHandle = nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/parent/FileSystemAccessHandleControlParent.h b/dom/fs/parent/FileSystemAccessHandleControlParent.h
new file mode 100644
index 0000000000..80e76a6e53
--- /dev/null
+++ b/dom/fs/parent/FileSystemAccessHandleControlParent.h
@@ -0,0 +1,43 @@
+/* -*- 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_FS_PARENT_FILESYSTEMACCESSHANDLECONTROLPARENT_H_
+#define DOM_FS_PARENT_FILESYSTEMACCESSHANDLECONTROLPARENT_H_
+
+#include "mozilla/dom/PFileSystemAccessHandleControlParent.h"
+#include "nsISupportsUtils.h"
+
+namespace mozilla::dom {
+
+class FileSystemAccessHandle;
+
+class FileSystemAccessHandleControlParent
+ : public PFileSystemAccessHandleControlParent {
+ public:
+ explicit FileSystemAccessHandleControlParent(
+ RefPtr<FileSystemAccessHandle> aAccessHandle);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemAccessHandleControlParent,
+ override)
+
+ mozilla::ipc::IPCResult RecvClose(CloseResolver&& aResolver);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ protected:
+ virtual ~FileSystemAccessHandleControlParent();
+
+ private:
+ RefPtr<FileSystemAccessHandle> mAccessHandle;
+
+#ifdef DEBUG
+ bool mActorDestroyed = false;
+#endif
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_FS_PARENT_FILESYSTEMACCESSHANDLECONTROLPARENT_H_
diff --git a/dom/fs/parent/FileSystemAccessHandleParent.cpp b/dom/fs/parent/FileSystemAccessHandleParent.cpp
new file mode 100644
index 0000000000..36d21fdd2d
--- /dev/null
+++ b/dom/fs/parent/FileSystemAccessHandleParent.cpp
@@ -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/. */
+
+#include "FileSystemAccessHandleParent.h"
+
+#include "mozilla/dom/FileSystemAccessHandle.h"
+
+namespace mozilla::dom {
+
+FileSystemAccessHandleParent::FileSystemAccessHandleParent(
+ RefPtr<FileSystemAccessHandle> aAccessHandle)
+ : mAccessHandle(std::move(aAccessHandle)) {}
+
+FileSystemAccessHandleParent::~FileSystemAccessHandleParent() {
+ MOZ_ASSERT(mActorDestroyed);
+}
+
+mozilla::ipc::IPCResult FileSystemAccessHandleParent::RecvClose() {
+ mAccessHandle->BeginClose();
+
+ return IPC_OK();
+}
+
+void FileSystemAccessHandleParent::ActorDestroy(ActorDestroyReason aWhy) {
+ MOZ_ASSERT(!mActorDestroyed);
+
+ DEBUGONLY(mActorDestroyed = true);
+
+ mAccessHandle->UnregisterActor(WrapNotNullUnchecked(this));
+
+ mAccessHandle = nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/parent/FileSystemAccessHandleParent.h b/dom/fs/parent/FileSystemAccessHandleParent.h
new file mode 100644
index 0000000000..0efeab2539
--- /dev/null
+++ b/dom/fs/parent/FileSystemAccessHandleParent.h
@@ -0,0 +1,38 @@
+/* -*- 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_FS_PARENT_FILESYSTEMACCESSHANDLEPARENT_H_
+#define DOM_FS_PARENT_FILESYSTEMACCESSHANDLEPARENT_H_
+
+#include "mozilla/dom/PFileSystemAccessHandleParent.h"
+#include "mozilla/dom/quota/DebugOnlyMacro.h"
+
+namespace mozilla::dom {
+
+class FileSystemAccessHandle;
+
+class FileSystemAccessHandleParent : public PFileSystemAccessHandleParent {
+ public:
+ explicit FileSystemAccessHandleParent(
+ RefPtr<FileSystemAccessHandle> aAccessHandle);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemAccessHandleParent, override)
+
+ mozilla::ipc::IPCResult RecvClose();
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ virtual ~FileSystemAccessHandleParent();
+
+ RefPtr<FileSystemAccessHandle> mAccessHandle;
+
+ DEBUGONLY(bool mActorDestroyed = false);
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_FS_PARENT_FILESYSTEMACCESSHANDLEPARENT_H_
diff --git a/dom/fs/parent/FileSystemContentTypeGuess.cpp b/dom/fs/parent/FileSystemContentTypeGuess.cpp
new file mode 100644
index 0000000000..e8ff8a760b
--- /dev/null
+++ b/dom/fs/parent/FileSystemContentTypeGuess.cpp
@@ -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/. */
+
+#include "FileSystemContentTypeGuess.h"
+
+#include "ErrorList.h"
+#include "mozilla/dom/QMResult.h"
+#include "mozilla/dom/mime_guess_ffi_generated.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsString.h"
+
+namespace mozilla::dom::fs {
+
+Result<ContentType, QMResult> FileSystemContentTypeGuess::FromPath(
+ const Name& aPath) {
+ NS_ConvertUTF16toUTF8 path(aPath);
+ ContentType contentType;
+ nsresult rv = mimeGuessFromPath(&path, &contentType);
+
+ // QM_TRY is too verbose.
+ if (NS_FAILED(rv)) {
+ return Err(QMResult(rv));
+ }
+
+ return contentType;
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/parent/FileSystemContentTypeGuess.h b/dom/fs/parent/FileSystemContentTypeGuess.h
new file mode 100644
index 0000000000..8b6d13f78c
--- /dev/null
+++ b/dom/fs/parent/FileSystemContentTypeGuess.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_FS_PARENT_FILESYSTEMCONTENTTYPEGUESS_H_
+#define DOM_FS_PARENT_FILESYSTEMCONTENTTYPEGUESS_H_
+
+#include "mozilla/Result.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "nsStringFwd.h"
+
+namespace mozilla::dom::fs {
+
+struct FileSystemContentTypeGuess {
+ static Result<ContentType, QMResult> FromPath(const Name& aPath);
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_PARENT_FILESYSTEMCONTENTTYPEGUESS_H_
diff --git a/dom/fs/parent/FileSystemHashSource.cpp b/dom/fs/parent/FileSystemHashSource.cpp
new file mode 100644
index 0000000000..67a5a79c8c
--- /dev/null
+++ b/dom/fs/parent/FileSystemHashSource.cpp
@@ -0,0 +1,67 @@
+/* -*- 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 "FileSystemHashSource.h"
+
+#include "FileSystemParentTypes.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/data_encoding_ffi_generated.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "nsComponentManagerUtils.h"
+#include "nsICryptoHash.h"
+#include "nsNetCID.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+
+namespace mozilla::dom::fs::data {
+
+Result<EntryId, QMResult> FileSystemHashSource::GenerateHash(
+ const EntryId& aParent, const Name& aName) {
+ auto makeHasher = [](nsresult* aRv) {
+ return do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, aRv);
+ };
+ QM_TRY_INSPECT(const auto& hasher,
+ QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED(
+ nsCOMPtr<nsICryptoHash>, makeHasher)));
+
+ QM_TRY(QM_TO_RESULT(hasher->Init(nsICryptoHash::SHA256)));
+
+ QM_TRY(QM_TO_RESULT(
+ hasher->Update(reinterpret_cast<const uint8_t*>(aName.BeginReading()),
+ sizeof(char16_t) * aName.Length())));
+
+ QM_TRY(QM_TO_RESULT(
+ hasher->Update(reinterpret_cast<const uint8_t*>(aParent.BeginReading()),
+ aParent.Length())));
+
+ EntryId entryId;
+ QM_TRY(QM_TO_RESULT(hasher->Finish(/* aASCII */ false, entryId)));
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ return entryId;
+}
+
+Result<Name, QMResult> FileSystemHashSource::EncodeHash(const FileId& aFileId) {
+ MOZ_ASSERT(32u == aFileId.Value().Length());
+ nsCString encoded;
+ base32encode(&aFileId.Value(), &encoded);
+
+ // We are stripping last four padding characters because
+ // it may not be allowed in some file systems.
+ MOZ_ASSERT(56u == encoded.Length() && '=' == encoded[52u] &&
+ '=' == encoded[53u] && '=' == encoded[54u] && '=' == encoded[55u]);
+ encoded.SetLength(52u);
+
+ Name result;
+ QM_TRY(OkIf(result.SetCapacity(encoded.Length(), mozilla::fallible)),
+ Err(QMResult(NS_ERROR_OUT_OF_MEMORY)));
+
+ result.AppendASCII(encoded);
+
+ return result;
+}
+
+} // namespace mozilla::dom::fs::data
diff --git a/dom/fs/parent/FileSystemHashSource.h b/dom/fs/parent/FileSystemHashSource.h
new file mode 100644
index 0000000000..722500342d
--- /dev/null
+++ b/dom/fs/parent/FileSystemHashSource.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_PARENT_FILESYSTEMHASHSOURCE_H_
+#define DOM_FS_PARENT_FILESYSTEMHASHSOURCE_H_
+
+#include "mozilla/Result.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/QMResult.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+
+namespace mozilla::dom::fs {
+
+struct FileId;
+
+namespace data {
+
+struct FileSystemHashSource {
+ static Result<EntryId, QMResult> GenerateHash(const EntryId& aParent,
+ const Name& aName);
+
+ static Result<Name, QMResult> EncodeHash(const FileId& aFileId);
+};
+
+} // namespace data
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_PARENT_FILESYSTEMHASHSOURCE_H_
diff --git a/dom/fs/parent/FileSystemHashStorageFunction.cpp b/dom/fs/parent/FileSystemHashStorageFunction.cpp
new file mode 100644
index 0000000000..601f66b9c7
--- /dev/null
+++ b/dom/fs/parent/FileSystemHashStorageFunction.cpp
@@ -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/. */
+
+#include "FileSystemHashStorageFunction.h"
+
+#include "FileSystemHashSource.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/storage/Variant.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+
+namespace mozilla::dom::fs::data {
+
+NS_IMPL_ISUPPORTS(FileSystemHashStorageFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+FileSystemHashStorageFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ MOZ_ASSERT(aFunctionArguments);
+ MOZ_ASSERT(aResult);
+
+ const int32_t parentIndex = 0;
+ const int32_t childIndex = 1;
+
+#ifdef DEBUG
+ {
+ uint32_t argCount;
+ MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetNumEntries(&argCount));
+ MOZ_ASSERT(argCount == 2u);
+
+ int32_t parentType = mozIStorageValueArray::VALUE_TYPE_INTEGER;
+ MOZ_ALWAYS_SUCCEEDS(
+ aFunctionArguments->GetTypeOfIndex(parentIndex, &parentType));
+ MOZ_ASSERT(parentType == mozIStorageValueArray::VALUE_TYPE_BLOB);
+
+ int32_t childType = mozIStorageValueArray::VALUE_TYPE_INTEGER;
+ MOZ_ALWAYS_SUCCEEDS(
+ aFunctionArguments->GetTypeOfIndex(childIndex, &childType));
+ MOZ_ASSERT(childType == mozIStorageValueArray::VALUE_TYPE_BLOB);
+ }
+#endif
+
+ QM_TRY_INSPECT(
+ const EntryId& parentHash,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, aFunctionArguments,
+ GetBlobAsUTF8String, parentIndex));
+
+ QM_TRY_INSPECT(const Name& childName, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsString, aFunctionArguments,
+ GetBlobAsString, childIndex));
+
+ QM_TRY_INSPECT(const EntryId& buffer,
+ FileSystemHashSource::GenerateHash(parentHash, childName)
+ .mapErr([](const auto& aRv) { return ToNSResult(aRv); }));
+
+ nsCOMPtr<nsIVariant> result =
+ new mozilla::storage::BlobVariant(std::make_pair(
+ static_cast<const void*>(buffer.get()), int(buffer.Length())));
+
+ result.forget(aResult);
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::fs::data
diff --git a/dom/fs/parent/FileSystemHashStorageFunction.h b/dom/fs/parent/FileSystemHashStorageFunction.h
new file mode 100644
index 0000000000..7f7f9aa26c
--- /dev/null
+++ b/dom/fs/parent/FileSystemHashStorageFunction.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_FS_PARENT_FILESYSTEMHASHSTORAGEFUNCTION_H_
+#define DOM_FS_PARENT_FILESYSTEMHASHSTORAGEFUNCTION_H_
+
+#include "mozIStorageFunction.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla::dom::fs::data {
+
+class FileSystemHashStorageFunction final : public mozIStorageFunction {
+ private:
+ ~FileSystemHashStorageFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+} // namespace mozilla::dom::fs::data
+
+#endif // DOM_FS_PARENT_FILESYSTEMHASHSTORAGEFUNCTION_H_
diff --git a/dom/fs/parent/FileSystemManagerParent.cpp b/dom/fs/parent/FileSystemManagerParent.cpp
new file mode 100644
index 0000000000..6f59c179f9
--- /dev/null
+++ b/dom/fs/parent/FileSystemManagerParent.cpp
@@ -0,0 +1,519 @@
+/* -*- 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 "FileSystemManagerParent.h"
+
+#include "FileSystemDatabaseManager.h"
+#include "FileSystemParentTypes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/dom/FileBlobImpl.h"
+#include "mozilla/dom/FileSystemAccessHandle.h"
+#include "mozilla/dom/FileSystemAccessHandleControlParent.h"
+#include "mozilla/dom/FileSystemAccessHandleParent.h"
+#include "mozilla/dom/FileSystemDataManager.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/FileSystemWritableFileStreamParent.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include "mozilla/dom/QMResult.h"
+#include "mozilla/dom/quota/FileStreams.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ipc/FileDescriptorUtils.h"
+#include "mozilla/ipc/RandomAccessStreamUtils.h"
+#include "nsNetUtil.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+using IPCResult = mozilla::ipc::IPCResult;
+
+namespace mozilla::dom {
+
+FileSystemManagerParent::FileSystemManagerParent(
+ RefPtr<fs::data::FileSystemDataManager> aDataManager,
+ const EntryId& aRootEntry)
+ : mDataManager(std::move(aDataManager)), mRootResponse(aRootEntry) {}
+
+FileSystemManagerParent::~FileSystemManagerParent() {
+ LOG(("Destroying FileSystemManagerParent %p", this));
+ MOZ_ASSERT(!mRegistered);
+}
+
+void FileSystemManagerParent::AssertIsOnIOTarget() const {
+ MOZ_ASSERT(mDataManager);
+
+ mDataManager->AssertIsOnIOTarget();
+}
+
+const RefPtr<fs::data::FileSystemDataManager>&
+FileSystemManagerParent::DataManagerStrongRef() const {
+ MOZ_ASSERT(!mActorDestroyed);
+ MOZ_ASSERT(mDataManager);
+
+ return mDataManager;
+}
+
+IPCResult FileSystemManagerParent::RecvGetRootHandle(
+ GetRootHandleResolver&& aResolver) {
+ AssertIsOnIOTarget();
+
+ aResolver(mRootResponse);
+
+ return IPC_OK();
+}
+
+IPCResult FileSystemManagerParent::RecvGetDirectoryHandle(
+ FileSystemGetHandleRequest&& aRequest,
+ GetDirectoryHandleResolver&& aResolver) {
+ LOG(("GetDirectoryHandle %s ",
+ NS_ConvertUTF16toUTF8(aRequest.handle().childName()).get()));
+ AssertIsOnIOTarget();
+ MOZ_ASSERT(!aRequest.handle().parentId().IsEmpty());
+ MOZ_ASSERT(mDataManager);
+
+ auto reportError = [&aResolver](const QMResult& aRv) {
+ FileSystemGetHandleResponse response(ToNSResult(aRv));
+ aResolver(response);
+ };
+
+ QM_TRY_UNWRAP(fs::EntryId entryId,
+ mDataManager->MutableDatabaseManagerPtr()->GetOrCreateDirectory(
+ aRequest.handle(), aRequest.create()),
+ IPC_OK(), reportError);
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ FileSystemGetHandleResponse response(entryId);
+ aResolver(response);
+
+ return IPC_OK();
+}
+
+IPCResult FileSystemManagerParent::RecvGetFileHandle(
+ FileSystemGetHandleRequest&& aRequest, GetFileHandleResolver&& aResolver) {
+ AssertIsOnIOTarget();
+ MOZ_ASSERT(!aRequest.handle().parentId().IsEmpty());
+ MOZ_ASSERT(mDataManager);
+
+ auto reportError = [&aResolver](const QMResult& aRv) {
+ FileSystemGetHandleResponse response(ToNSResult(aRv));
+ aResolver(response);
+ };
+
+ QM_TRY_UNWRAP(fs::EntryId entryId,
+ mDataManager->MutableDatabaseManagerPtr()->GetOrCreateFile(
+ aRequest.handle(), aRequest.create()),
+ IPC_OK(), reportError);
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ FileSystemGetHandleResponse response(entryId);
+ aResolver(response);
+ return IPC_OK();
+}
+
+// Could use a template, but you need several types
+mozilla::ipc::IPCResult FileSystemManagerParent::RecvGetAccessHandle(
+ FileSystemGetAccessHandleRequest&& aRequest,
+ GetAccessHandleResolver&& aResolver) {
+ AssertIsOnIOTarget();
+ MOZ_ASSERT(mDataManager);
+
+ EntryId entryId = aRequest.entryId();
+
+ FileSystemAccessHandle::Create(mDataManager, entryId)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr(this), request = std::move(aRequest),
+ resolver = std::move(aResolver)](
+ FileSystemAccessHandle::CreatePromise::ResolveOrRejectValue&&
+ aValue) {
+ if (!self->CanSend()) {
+ return;
+ }
+
+ if (aValue.IsReject()) {
+ resolver(aValue.RejectValue());
+ return;
+ }
+
+ FileSystemAccessHandle::CreateResult result =
+ std::move(aValue.ResolveValue());
+
+ fs::Registered<FileSystemAccessHandle> accessHandle =
+ std::move(result.first);
+
+ RandomAccessStreamParams streamParams = std::move(result.second);
+
+ auto accessHandleParent = MakeRefPtr<FileSystemAccessHandleParent>(
+ accessHandle.inspect());
+
+ auto resolveAndReturn = [&resolver](nsresult rv) { resolver(rv); };
+
+ ManagedEndpoint<PFileSystemAccessHandleChild>
+ accessHandleChildEndpoint =
+ self->OpenPFileSystemAccessHandleEndpoint(
+ accessHandleParent);
+ QM_TRY(MOZ_TO_RESULT(accessHandleChildEndpoint.IsValid()),
+ resolveAndReturn);
+
+ accessHandle->RegisterActor(WrapNotNull(accessHandleParent));
+
+ auto accessHandleControlParent =
+ MakeRefPtr<FileSystemAccessHandleControlParent>(
+ accessHandle.inspect());
+
+ Endpoint<PFileSystemAccessHandleControlParent>
+ accessHandleControlParentEndpoint;
+ Endpoint<PFileSystemAccessHandleControlChild>
+ accessHandleControlChildEndpoint;
+ MOZ_ALWAYS_SUCCEEDS(PFileSystemAccessHandleControl::CreateEndpoints(
+ &accessHandleControlParentEndpoint,
+ &accessHandleControlChildEndpoint));
+
+ accessHandleControlParentEndpoint.Bind(accessHandleControlParent);
+
+ accessHandle->RegisterControlActor(
+ WrapNotNull(accessHandleControlParent));
+
+ resolver(FileSystemAccessHandleProperties(
+ std::move(streamParams), std::move(accessHandleChildEndpoint),
+ std::move(accessHandleControlChildEndpoint)));
+ });
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult FileSystemManagerParent::RecvGetWritable(
+ FileSystemGetWritableRequest&& aRequest, GetWritableResolver&& aResolver) {
+ AssertIsOnIOTarget();
+ MOZ_ASSERT(mDataManager);
+
+ const fs::FileMode mode = mDataManager->GetMode(aRequest.keepData());
+
+ auto reportError = [aResolver](const auto& aRv) {
+ aResolver(ToNSResult(aRv));
+ };
+
+ // TODO: Get rid of mode and switching based on it, have the right unlocking
+ // automatically
+ const fs::EntryId& entryId = aRequest.entryId();
+ QM_TRY_UNWRAP(
+ fs::FileId fileId,
+ (mode == fs::FileMode::EXCLUSIVE ? mDataManager->LockExclusive(entryId)
+ : mDataManager->LockShared(entryId)),
+ IPC_OK(), reportError);
+ MOZ_ASSERT(!fileId.IsEmpty());
+
+ auto autoUnlock = MakeScopeExit(
+ [self = RefPtr<FileSystemManagerParent>(this), &entryId, &fileId, mode] {
+ if (mode == fs::FileMode::EXCLUSIVE) {
+ self->mDataManager->UnlockExclusive(entryId);
+ } else {
+ self->mDataManager->UnlockShared(entryId, fileId, /* aAbort */ true);
+ }
+ });
+
+ fs::ContentType type;
+ fs::TimeStamp lastModifiedMilliSeconds;
+ fs::Path path;
+ nsCOMPtr<nsIFile> file;
+ QM_TRY(
+ MOZ_TO_RESULT(mDataManager->MutableDatabaseManagerPtr()->GetFile(
+ entryId, fileId, mode, type, lastModifiedMilliSeconds, path, file)),
+ IPC_OK(), reportError);
+
+ if (LOG_ENABLED()) {
+ nsAutoString path;
+ if (NS_SUCCEEDED(file->GetPath(path))) {
+ LOG(("Opening Writable %s", NS_ConvertUTF16toUTF8(path).get()));
+ }
+ }
+
+ auto writableFileStreamParent =
+ MakeNotNull<RefPtr<FileSystemWritableFileStreamParent>>(
+ this, aRequest.entryId(), fileId, mode == fs::FileMode::EXCLUSIVE);
+
+ QM_TRY_UNWRAP(
+ nsCOMPtr<nsIRandomAccessStream> stream,
+ CreateFileRandomAccessStream(quota::PERSISTENCE_TYPE_DEFAULT,
+ mDataManager->OriginMetadataRef(),
+ quota::Client::FILESYSTEM, file, -1, -1,
+ nsIFileRandomAccessStream::DEFER_OPEN),
+ IPC_OK(), reportError);
+
+ RandomAccessStreamParams streamParams =
+ mozilla::ipc::SerializeRandomAccessStream(
+ WrapMovingNotNullUnchecked(std::move(stream)),
+ writableFileStreamParent->GetOrCreateStreamCallbacks());
+
+ // Release the auto unlock helper just before calling
+ // SendPFileSystemWritableFileStreamConstructor which is responsible for
+ // destroying the actor if the sending fails (we call `UnlockExclusive` when
+ // the actor is destroyed).
+ autoUnlock.release();
+
+ if (!SendPFileSystemWritableFileStreamConstructor(writableFileStreamParent)) {
+ aResolver(NS_ERROR_FAILURE);
+ return IPC_OK();
+ }
+
+ aResolver(FileSystemWritableFileStreamProperties(std::move(streamParams),
+ writableFileStreamParent));
+
+ return IPC_OK();
+}
+
+IPCResult FileSystemManagerParent::RecvGetFile(
+ FileSystemGetFileRequest&& aRequest, GetFileResolver&& aResolver) {
+ AssertIsOnIOTarget();
+
+ // XXX Spec https://www.w3.org/TR/FileAPI/#dfn-file wants us to snapshot the
+ // state of the file at getFile() time
+
+ // You can create a File with getFile() even if the file is locked
+ // XXX factor out this part of the code for accesshandle/ and getfile
+ auto reportError = [aResolver](const auto& rv) {
+ LOG(("getFile() Failed!"));
+ aResolver(ToNSResult(rv));
+ };
+
+ const auto& entryId = aRequest.entryId();
+
+ QM_TRY_INSPECT(
+ const fs::FileId& fileId,
+ mDataManager->MutableDatabaseManagerPtr()->EnsureFileId(entryId),
+ IPC_OK(), reportError);
+
+ fs::ContentType type;
+ fs::TimeStamp lastModifiedMilliSeconds;
+ fs::Path path;
+ nsCOMPtr<nsIFile> fileObject;
+ QM_TRY(MOZ_TO_RESULT(mDataManager->MutableDatabaseManagerPtr()->GetFile(
+ entryId, fileId, fs::FileMode::EXCLUSIVE, type,
+ lastModifiedMilliSeconds, path, fileObject)),
+ IPC_OK(), reportError);
+
+ if (LOG_ENABLED()) {
+ nsAutoString path;
+ if (NS_SUCCEEDED(fileObject->GetPath(path))) {
+ LOG(("Opening File as blob: %s", NS_ConvertUTF16toUTF8(path).get()));
+ }
+ }
+
+ // TODO: Currently, there is no way to assign type and it is empty.
+ // See bug 1826780.
+ RefPtr<BlobImpl> blob = MakeRefPtr<FileBlobImpl>(
+ fileObject, path.LastElement(), NS_ConvertUTF8toUTF16(type));
+
+ IPCBlob ipcBlob;
+ QM_TRY(MOZ_TO_RESULT(IPCBlobUtils::Serialize(blob, ipcBlob)), IPC_OK(),
+ reportError);
+
+ aResolver(
+ FileSystemFileProperties(lastModifiedMilliSeconds, ipcBlob, type, path));
+ return IPC_OK();
+}
+
+IPCResult FileSystemManagerParent::RecvResolve(
+ FileSystemResolveRequest&& aRequest, ResolveResolver&& aResolver) {
+ AssertIsOnIOTarget();
+ MOZ_ASSERT(!aRequest.endpoints().parentId().IsEmpty());
+ MOZ_ASSERT(!aRequest.endpoints().childId().IsEmpty());
+ MOZ_ASSERT(mDataManager);
+
+ fs::Path filePath;
+ if (aRequest.endpoints().parentId() == aRequest.endpoints().childId()) {
+ FileSystemResolveResponse response(Some(FileSystemPath(filePath)));
+ aResolver(response);
+
+ return IPC_OK();
+ }
+
+ auto reportError = [&aResolver](const QMResult& aRv) {
+ FileSystemResolveResponse response(ToNSResult(aRv));
+ aResolver(response);
+ };
+
+ QM_TRY_UNWRAP(
+ filePath,
+ mDataManager->MutableDatabaseManagerPtr()->Resolve(aRequest.endpoints()),
+ IPC_OK(), reportError);
+
+ if (LOG_ENABLED()) {
+ nsString path;
+ for (auto& entry : filePath) {
+ path.Append(entry);
+ }
+ LOG(("Resolve path: %s", NS_ConvertUTF16toUTF8(path).get()));
+ }
+
+ if (filePath.IsEmpty()) {
+ FileSystemResolveResponse response(Nothing{});
+ aResolver(response);
+
+ return IPC_OK();
+ }
+
+ FileSystemResolveResponse response(Some(FileSystemPath(filePath)));
+ aResolver(response);
+
+ return IPC_OK();
+}
+
+IPCResult FileSystemManagerParent::RecvGetEntries(
+ FileSystemGetEntriesRequest&& aRequest, GetEntriesResolver&& aResolver) {
+ AssertIsOnIOTarget();
+ MOZ_ASSERT(!aRequest.parentId().IsEmpty());
+ MOZ_ASSERT(mDataManager);
+
+ auto reportError = [&aResolver](const QMResult& aRv) {
+ FileSystemGetEntriesResponse response(ToNSResult(aRv));
+ aResolver(response);
+ };
+
+ QM_TRY_UNWRAP(FileSystemDirectoryListing entries,
+ mDataManager->MutableDatabaseManagerPtr()->GetDirectoryEntries(
+ aRequest.parentId(), aRequest.page()),
+ IPC_OK(), reportError);
+
+ FileSystemGetEntriesResponse response(entries);
+ aResolver(response);
+
+ return IPC_OK();
+}
+
+IPCResult FileSystemManagerParent::RecvRemoveEntry(
+ FileSystemRemoveEntryRequest&& aRequest, RemoveEntryResolver&& aResolver) {
+ LOG(("RemoveEntry %s",
+ NS_ConvertUTF16toUTF8(aRequest.handle().childName()).get()));
+ AssertIsOnIOTarget();
+ MOZ_ASSERT(!aRequest.handle().parentId().IsEmpty());
+ MOZ_ASSERT(mDataManager);
+
+ auto reportError = [&aResolver](const QMResult& aRv) {
+ FileSystemRemoveEntryResponse response(ToNSResult(aRv));
+ aResolver(response);
+ };
+
+ QM_TRY_UNWRAP(
+ bool isDeleted,
+ mDataManager->MutableDatabaseManagerPtr()->RemoveFile(aRequest.handle()),
+ IPC_OK(), reportError);
+
+ if (isDeleted) {
+ FileSystemRemoveEntryResponse response(void_t{});
+ aResolver(response);
+
+ return IPC_OK();
+ }
+
+ QM_TRY_UNWRAP(isDeleted,
+ mDataManager->MutableDatabaseManagerPtr()->RemoveDirectory(
+ aRequest.handle(), aRequest.recursive()),
+ IPC_OK(), reportError);
+
+ if (!isDeleted) {
+ FileSystemRemoveEntryResponse response(NS_ERROR_DOM_NOT_FOUND_ERR);
+ aResolver(response);
+
+ return IPC_OK();
+ }
+
+ FileSystemRemoveEntryResponse response(void_t{});
+ aResolver(response);
+
+ return IPC_OK();
+}
+
+IPCResult FileSystemManagerParent::RecvMoveEntry(
+ FileSystemMoveEntryRequest&& aRequest, MoveEntryResolver&& aResolver) {
+ LOG(("MoveEntry %s to %s",
+ NS_ConvertUTF16toUTF8(aRequest.handle().entryName()).get(),
+ NS_ConvertUTF16toUTF8(aRequest.destHandle().childName()).get()));
+ MOZ_ASSERT(!aRequest.handle().entryId().IsEmpty());
+ MOZ_ASSERT(!aRequest.destHandle().parentId().IsEmpty());
+ MOZ_ASSERT(mDataManager);
+
+ auto reportError = [&aResolver](const QMResult& aRv) {
+ FileSystemMoveEntryResponse response(ToNSResult(aRv));
+ aResolver(response);
+ };
+
+ QM_TRY_INSPECT(const EntryId& newId,
+ mDataManager->MutableDatabaseManagerPtr()->MoveEntry(
+ aRequest.handle(), aRequest.destHandle()),
+ IPC_OK(), reportError);
+
+ fs::FileSystemMoveEntryResponse response(newId);
+ aResolver(response);
+ return IPC_OK();
+}
+
+IPCResult FileSystemManagerParent::RecvRenameEntry(
+ FileSystemRenameEntryRequest&& aRequest, MoveEntryResolver&& aResolver) {
+ // if destHandle's parentId is empty, then we're renaming in the same
+ // directory
+ LOG(("RenameEntry %s to %s",
+ NS_ConvertUTF16toUTF8(aRequest.handle().entryName()).get(),
+ NS_ConvertUTF16toUTF8(aRequest.name()).get()));
+ MOZ_ASSERT(!aRequest.handle().entryId().IsEmpty());
+ MOZ_ASSERT(mDataManager);
+
+ auto reportError = [&aResolver](const QMResult& aRv) {
+ FileSystemMoveEntryResponse response(ToNSResult(aRv));
+ aResolver(response);
+ };
+
+ QM_TRY_INSPECT(const EntryId& newId,
+ mDataManager->MutableDatabaseManagerPtr()->RenameEntry(
+ aRequest.handle(), aRequest.name()),
+ IPC_OK(), reportError);
+
+ fs::FileSystemMoveEntryResponse response(newId);
+ aResolver(response);
+ return IPC_OK();
+}
+
+void FileSystemManagerParent::RequestAllowToClose() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ if (mRequestedAllowToClose) {
+ return;
+ }
+
+ mRequestedAllowToClose.Flip();
+
+ InvokeAsync(mDataManager->MutableIOTaskQueuePtr(), __func__,
+ [self = RefPtr<FileSystemManagerParent>(this)]() {
+ return self->SendCloseAll();
+ })
+ ->Then(mDataManager->MutableIOTaskQueuePtr(), __func__,
+ [self = RefPtr<FileSystemManagerParent>(this)](
+ const CloseAllPromise::ResolveOrRejectValue& aValue) {
+ self->Close();
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+void FileSystemManagerParent::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnIOTarget();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ DEBUGONLY(mActorDestroyed = true);
+
+ InvokeAsync(mDataManager->MutableBackgroundTargetPtr(), __func__,
+ [self = RefPtr<FileSystemManagerParent>(this)]() {
+ self->mDataManager->UnregisterActor(WrapNotNull(self));
+
+ self->mDataManager = nullptr;
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/parent/FileSystemManagerParent.h b/dom/fs/parent/FileSystemManagerParent.h
new file mode 100644
index 0000000000..01f9f23f6b
--- /dev/null
+++ b/dom/fs/parent/FileSystemManagerParent.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 DOM_FS_PARENT_FILESYSTEMMANAGERPARENT_H_
+#define DOM_FS_PARENT_FILESYSTEMMANAGERPARENT_H_
+
+#include "ErrorList.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "mozilla/dom/PFileSystemManagerParent.h"
+#include "mozilla/dom/quota/DebugOnlyMacro.h"
+#include "nsISupports.h"
+
+namespace mozilla::dom {
+
+namespace fs::data {
+class FileSystemDataManager;
+} // namespace fs::data
+
+class FileSystemManagerParent : public PFileSystemManagerParent {
+ public:
+ FileSystemManagerParent(RefPtr<fs::data::FileSystemDataManager> aDataManager,
+ const EntryId& aRootEntry);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemManagerParent, override)
+
+ void AssertIsOnIOTarget() const;
+
+#ifdef DEBUG
+ void SetRegistered(bool aRegistered) { mRegistered = aRegistered; }
+#endif
+
+ // Safe to call while the actor is live.
+ const RefPtr<fs::data::FileSystemDataManager>& DataManagerStrongRef() const;
+
+ mozilla::ipc::IPCResult RecvGetRootHandle(GetRootHandleResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvGetDirectoryHandle(
+ FileSystemGetHandleRequest&& aRequest,
+ GetDirectoryHandleResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvGetFileHandle(
+ FileSystemGetHandleRequest&& aRequest, GetFileHandleResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvGetAccessHandle(
+ FileSystemGetAccessHandleRequest&& aRequest,
+ GetAccessHandleResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvGetWritable(
+ FileSystemGetWritableRequest&& aRequest, GetWritableResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvGetFile(FileSystemGetFileRequest&& aRequest,
+ GetFileResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvResolve(FileSystemResolveRequest&& aRequest,
+ ResolveResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvGetEntries(FileSystemGetEntriesRequest&& aRequest,
+ GetEntriesResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvRemoveEntry(
+ FileSystemRemoveEntryRequest&& aRequest, RemoveEntryResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvMoveEntry(FileSystemMoveEntryRequest&& aRequest,
+ MoveEntryResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvRenameEntry(
+ FileSystemRenameEntryRequest&& aRequest, MoveEntryResolver&& aResolver);
+
+ void RequestAllowToClose();
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ protected:
+ virtual ~FileSystemManagerParent();
+
+ private:
+ RefPtr<fs::data::FileSystemDataManager> mDataManager;
+
+ FileSystemGetHandleResponse mRootResponse;
+
+ FlippedOnce<false> mRequestedAllowToClose;
+
+ DEBUGONLY(bool mRegistered = false);
+
+ DEBUGONLY(bool mActorDestroyed = false);
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_FS_PARENT_FILESYSTEMMANAGERPARENT_H_
diff --git a/dom/fs/parent/FileSystemManagerParentFactory.cpp b/dom/fs/parent/FileSystemManagerParentFactory.cpp
new file mode 100644
index 0000000000..a7a4b13664
--- /dev/null
+++ b/dom/fs/parent/FileSystemManagerParentFactory.cpp
@@ -0,0 +1,114 @@
+/* -*- 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 "FileSystemManagerParentFactory.h"
+
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/FileSystemDataManager.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemManagerParent.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "nsIScriptObjectPrincipal.h"
+#include "nsString.h"
+
+namespace mozilla::dom {
+mozilla::ipc::IPCResult CreateFileSystemManagerParent(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
+ mozilla::ipc::Endpoint<PFileSystemManagerParent>&& aParentEndpoint,
+ std::function<void(const nsresult&)>&& aResolver) {
+ using CreateActorPromise =
+ MozPromise<RefPtr<FileSystemManagerParent>, nsresult, true>;
+
+ QM_TRY(OkIf(StaticPrefs::dom_fs_enabled()), IPC_OK(),
+ [aResolver](const auto&) { aResolver(NS_ERROR_DOM_NOT_ALLOWED_ERR); });
+
+ QM_TRY(OkIf(aParentEndpoint.IsValid()), IPC_OK(),
+ [aResolver](const auto&) { aResolver(NS_ERROR_INVALID_ARG); });
+
+ // This blocks Null and Expanded principals
+ QM_TRY(OkIf(quota::QuotaManager::IsPrincipalInfoValid(aPrincipalInfo)),
+ IPC_OK(),
+ [aResolver](const auto&) { aResolver(NS_ERROR_DOM_SECURITY_ERR); });
+
+ QM_TRY(quota::QuotaManager::EnsureCreated(), IPC_OK(),
+ [aResolver](const auto rv) { aResolver(rv); });
+
+ auto* const quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY_UNWRAP(auto principalMetadata,
+ quotaManager->GetInfoFromValidatedPrincipalInfo(aPrincipalInfo),
+ IPC_OK(), [aResolver](const auto rv) { aResolver(rv); });
+
+ quota::OriginMetadata originMetadata(std::move(principalMetadata),
+ quota::PERSISTENCE_TYPE_DEFAULT);
+
+ // Block use for now in PrivateBrowsing
+ QM_TRY(OkIf(!OriginAttributes::IsPrivateBrowsing(originMetadata.mOrigin)),
+ IPC_OK(),
+ [aResolver](const auto&) { aResolver(NS_ERROR_DOM_NOT_ALLOWED_ERR); });
+
+ LOG(("CreateFileSystemManagerParent, origin: %s",
+ originMetadata.mOrigin.get()));
+
+ // This creates the file system data manager, which has to be done on
+ // PBackground
+ fs::data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ originMetadata)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [origin = originMetadata.mOrigin,
+ parentEndpoint = std::move(aParentEndpoint),
+ aResolver](const fs::Registered<fs::data::FileSystemDataManager>&
+ dataManager) mutable {
+ QM_TRY_UNWRAP(
+ fs::EntryId rootId, fs::data::GetRootHandle(origin), QM_VOID,
+ [aResolver](const auto& aRv) { aResolver(ToNSResult(aRv)); });
+
+ InvokeAsync(
+ dataManager->MutableIOTaskQueuePtr(), __func__,
+ [dataManager =
+ RefPtr<fs::data::FileSystemDataManager>(dataManager),
+ rootId, parentEndpoint = std::move(parentEndpoint)]() mutable {
+ RefPtr<FileSystemManagerParent> parent =
+ new FileSystemManagerParent(std::move(dataManager),
+ rootId);
+
+ LOG(("Binding parent endpoint"));
+ if (!parentEndpoint.Bind(parent)) {
+ return CreateActorPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+
+ return CreateActorPromise::CreateAndResolve(std::move(parent),
+ __func__);
+ })
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [dataManager = dataManager, aResolver](
+ CreateActorPromise::ResolveOrRejectValue&& aValue) {
+ if (aValue.IsReject()) {
+ aResolver(aValue.RejectValue());
+ } else {
+ RefPtr<FileSystemManagerParent> parent =
+ std::move(aValue.ResolveValue());
+
+ dataManager->RegisterActor(WrapNotNull(parent));
+
+ aResolver(NS_OK);
+ }
+ });
+ },
+ [aResolver](nsresult aRejectValue) { aResolver(aRejectValue); });
+
+ return IPC_OK();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/parent/FileSystemManagerParentFactory.h b/dom/fs/parent/FileSystemManagerParentFactory.h
new file mode 100644
index 0000000000..6d581ffa3f
--- /dev/null
+++ b/dom/fs/parent/FileSystemManagerParentFactory.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_FS_PARENT_FILESYSTEMMANAGER_H_
+#define DOM_FS_PARENT_FILESYSTEMMANAGER_H_
+
+#include <functional>
+
+enum class nsresult : uint32_t;
+
+namespace mozilla {
+
+namespace ipc {
+
+template <class T>
+class Endpoint;
+
+class IPCResult;
+class PrincipalInfo;
+
+} // namespace ipc
+
+namespace dom {
+
+class PFileSystemManagerParent;
+
+mozilla::ipc::IPCResult CreateFileSystemManagerParent(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
+ mozilla::ipc::Endpoint<mozilla::dom::PFileSystemManagerParent>&&
+ aParentEndpoint,
+ std::function<void(const nsresult&)>&& aResolver);
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_PARENT_FILESYSTEMMANAGER_H_
diff --git a/dom/fs/parent/FileSystemParentTypes.h b/dom/fs/parent/FileSystemParentTypes.h
new file mode 100644
index 0000000000..15cfe42bb0
--- /dev/null
+++ b/dom/fs/parent/FileSystemParentTypes.h
@@ -0,0 +1,46 @@
+/* -*- 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_FS_PARENT_FILESYSTEMPARENTTYPES_H_
+#define DOM_FS_PARENT_FILESYSTEMPARENTTYPES_H_
+
+#include "nsStringFwd.h"
+#include "nsTString.h"
+
+namespace mozilla::dom::fs {
+
+/**
+ * @brief FileId refers to a file on disk while EntryId refers to a path.
+ * Same user input path will always generate the same EntryId while the FileId
+ * can be different. Move methods can change the FileId which underlies
+ * an EntryId and multiple FileIds for temporary files can all map to the same
+ * EntryId.
+ */
+struct FileId {
+ explicit FileId(const nsCString& aValue) : mValue(aValue) {}
+
+ explicit FileId(nsCString&& aValue) : mValue(std::move(aValue)) {}
+
+ constexpr bool IsEmpty() const { return mValue.IsEmpty(); }
+
+ constexpr const nsCString& Value() const { return mValue; }
+
+ nsCString mValue;
+};
+
+inline bool operator==(const FileId& aLhs, const FileId& aRhs) {
+ return aLhs.mValue == aRhs.mValue;
+}
+
+inline bool operator!=(const FileId& aLhs, const FileId& aRhs) {
+ return aLhs.mValue != aRhs.mValue;
+}
+
+enum class FileMode { EXCLUSIVE, SHARED_FROM_EMPTY, SHARED_FROM_COPY };
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_PARENT_FILESYSTEMPARENTTYPES_H_
diff --git a/dom/fs/parent/FileSystemQuotaClient.cpp b/dom/fs/parent/FileSystemQuotaClient.cpp
new file mode 100644
index 0000000000..fbe61b59df
--- /dev/null
+++ b/dom/fs/parent/FileSystemQuotaClient.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 "FileSystemQuotaClient.h"
+
+#include "datamodel/FileSystemDatabaseManager.h"
+#include "datamodel/FileSystemFileManager.h"
+#include "mozilla/dom/FileSystemDataManager.h"
+#include "mozilla/dom/quota/Assertions.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/dom/quota/UsageInfo.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "nsIFile.h"
+
+namespace mozilla::dom::fs {
+
+namespace {
+
+auto toNSResult = [](const auto& aRv) { return ToNSResult(aRv); };
+
+} // namespace
+
+FileSystemQuotaClient::FileSystemQuotaClient() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+}
+
+quota::Client::Type FileSystemQuotaClient::GetType() {
+ return quota::Client::Type::FILESYSTEM;
+}
+
+Result<quota::UsageInfo, nsresult> FileSystemQuotaClient::InitOrigin(
+ quota::PersistenceType aPersistenceType,
+ const quota::OriginMetadata& aOriginMetadata, const AtomicBool& aCanceled) {
+ quota::AssertIsOnIOThread();
+
+ {
+ QM_TRY_INSPECT(const nsCOMPtr<nsIFile>& databaseFile,
+ data::GetDatabaseFile(aOriginMetadata).mapErr(toNSResult));
+
+ bool exists = false;
+ QM_TRY(MOZ_TO_RESULT(databaseFile->Exists(&exists)));
+ // If database doesn't already exist, we do not create it
+ if (!exists) {
+ return quota::UsageInfo();
+ }
+ }
+
+ QM_TRY_INSPECT(
+ const ResultConnection& conn,
+ data::GetStorageConnection(aOriginMetadata, /* aDirectoryLockId */ -1)
+ .mapErr(toNSResult));
+
+ QM_TRY(MOZ_TO_RESULT(
+ data::FileSystemDatabaseManager::RescanUsages(conn, aOriginMetadata)));
+
+ return data::FileSystemDatabaseManager::GetUsage(conn, aOriginMetadata)
+ .mapErr(toNSResult);
+}
+
+nsresult FileSystemQuotaClient::InitOriginWithoutTracking(
+ quota::PersistenceType /* aPersistenceType */,
+ const quota::OriginMetadata& /* aOriginMetadata */,
+ const AtomicBool& /* aCanceled */) {
+ quota::AssertIsOnIOThread();
+
+ // This is called when a storage/permanent/${origin}/fs directory exists. Even
+ // though this shouldn't happen with a "good" profile, we shouldn't return an
+ // error here, since that would cause origin initialization to fail. We just
+ // warn and otherwise ignore that.
+ UNKNOWN_FILE_WARNING(
+ NS_LITERAL_STRING_FROM_CSTRING(FILESYSTEM_DIRECTORY_NAME));
+
+ return NS_OK;
+}
+
+Result<quota::UsageInfo, nsresult> FileSystemQuotaClient::GetUsageForOrigin(
+ quota::PersistenceType aPersistenceType,
+ const quota::OriginMetadata& aOriginMetadata,
+ const AtomicBool& /* aCanceled */) {
+ quota::AssertIsOnIOThread();
+
+ MOZ_ASSERT(aPersistenceType ==
+ quota::PersistenceType::PERSISTENCE_TYPE_DEFAULT);
+
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ // We can't open the database at this point because the quota manager may not
+ // allow it. Use the cached value instead.
+ return quotaManager->GetUsageForClient(aPersistenceType, aOriginMetadata,
+ quota::Client::FILESYSTEM);
+}
+
+void FileSystemQuotaClient::OnOriginClearCompleted(
+ quota::PersistenceType aPersistenceType, const nsACString& aOrigin) {
+ quota::AssertIsOnIOThread();
+}
+
+void FileSystemQuotaClient::OnRepositoryClearCompleted(
+ quota::PersistenceType aPersistenceType) {
+ quota::AssertIsOnIOThread();
+}
+
+void FileSystemQuotaClient::ReleaseIOThreadObjects() {
+ quota::AssertIsOnIOThread();
+}
+
+void FileSystemQuotaClient::AbortOperationsForLocks(
+ const DirectoryLockIdTable& aDirectoryLockIds) {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ data::FileSystemDataManager::AbortOperationsForLocks(aDirectoryLockIds);
+}
+
+void FileSystemQuotaClient::AbortOperationsForProcess(
+ ContentParentId aContentParentId) {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+}
+
+void FileSystemQuotaClient::AbortAllOperations() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+}
+
+void FileSystemQuotaClient::StartIdleMaintenance() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+}
+
+void FileSystemQuotaClient::StopIdleMaintenance() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+}
+
+void FileSystemQuotaClient::InitiateShutdown() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ data::FileSystemDataManager::InitiateShutdown();
+}
+
+nsCString FileSystemQuotaClient::GetShutdownStatus() const {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ return "Not implemented"_ns;
+}
+
+bool FileSystemQuotaClient::IsShutdownCompleted() const {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ return data::FileSystemDataManager::IsShutdownCompleted();
+}
+
+void FileSystemQuotaClient::ForceKillActors() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ // Hopefully not needed.
+}
+
+void FileSystemQuotaClient::FinalizeShutdown() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ // Empty for now.
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/parent/FileSystemQuotaClient.h b/dom/fs/parent/FileSystemQuotaClient.h
new file mode 100644
index 0000000000..e0eced35b9
--- /dev/null
+++ b/dom/fs/parent/FileSystemQuotaClient.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_FS_PARENT_FILESYSTEMQUOTACLIENT_H_
+#define DOM_FS_PARENT_FILESYSTEMQUOTACLIENT_H_
+
+#include "mozilla/dom/quota/Client.h"
+
+namespace mozilla::dom::fs {
+
+class FileSystemQuotaClient : public quota::Client {
+ public:
+ FileSystemQuotaClient();
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::fs::FileSystemQuotaClient,
+ override)
+
+ Type GetType() override;
+
+ Result<quota::UsageInfo, nsresult> InitOrigin(
+ quota::PersistenceType aPersistenceType,
+ const quota::OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) override;
+
+ nsresult InitOriginWithoutTracking(
+ quota::PersistenceType aPersistenceType,
+ const quota::OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) override;
+
+ Result<quota::UsageInfo, nsresult> GetUsageForOrigin(
+ quota::PersistenceType aPersistenceType,
+ const quota::OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) override;
+
+ void OnOriginClearCompleted(quota::PersistenceType aPersistenceType,
+ const nsACString& aOrigin) override;
+
+ void OnRepositoryClearCompleted(
+ quota::PersistenceType aPersistenceType) override;
+
+ void ReleaseIOThreadObjects() override;
+
+ void AbortOperationsForLocks(
+ const DirectoryLockIdTable& aDirectoryLockIds) override;
+
+ void AbortOperationsForProcess(ContentParentId aContentParentId) override;
+
+ void AbortAllOperations() override;
+
+ void StartIdleMaintenance() override;
+
+ void StopIdleMaintenance() override;
+
+ protected:
+ ~FileSystemQuotaClient() = default;
+
+ void InitiateShutdown() override;
+ bool IsShutdownCompleted() const override;
+ nsCString GetShutdownStatus() const override;
+ void ForceKillActors() override;
+ void FinalizeShutdown() override;
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_PARENT_FILESYSTEMQUOTACLIENT_H_
diff --git a/dom/fs/parent/FileSystemQuotaClientFactory.cpp b/dom/fs/parent/FileSystemQuotaClientFactory.cpp
new file mode 100644
index 0000000000..5d4897dfee
--- /dev/null
+++ b/dom/fs/parent/FileSystemQuotaClientFactory.cpp
@@ -0,0 +1,49 @@
+/* -*- 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 "FileSystemQuotaClientFactory.h"
+
+#include "FileSystemQuotaClient.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ipc/BackgroundParent.h"
+
+namespace mozilla::dom::fs {
+
+namespace {
+
+StaticRefPtr<FileSystemQuotaClientFactory> gCustomFactory;
+
+} // namespace
+
+// static
+void FileSystemQuotaClientFactory::SetCustomFactory(
+ RefPtr<FileSystemQuotaClientFactory> aCustomFactory) {
+ gCustomFactory = std::move(aCustomFactory);
+}
+
+// static
+already_AddRefed<quota::Client>
+FileSystemQuotaClientFactory::CreateQuotaClient() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ if (gCustomFactory) {
+ return gCustomFactory->AllocQuotaClient();
+ }
+
+ auto factory = MakeRefPtr<FileSystemQuotaClientFactory>();
+
+ return factory->AllocQuotaClient();
+}
+
+already_AddRefed<quota::Client>
+FileSystemQuotaClientFactory::AllocQuotaClient() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ RefPtr<FileSystemQuotaClient> result = new FileSystemQuotaClient();
+ return result.forget();
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/parent/FileSystemQuotaClientFactory.h b/dom/fs/parent/FileSystemQuotaClientFactory.h
new file mode 100644
index 0000000000..f71587fac0
--- /dev/null
+++ b/dom/fs/parent/FileSystemQuotaClientFactory.h
@@ -0,0 +1,49 @@
+/* -*- 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_FS_PARENT_FILESYSTEMQUOTACLIENTFACTORY_H_
+#define DOM_FS_PARENT_FILESYSTEMQUOTACLIENTFACTORY_H_
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "nsISupportsUtils.h"
+
+template <class>
+class RefPtr;
+
+namespace mozilla::dom {
+
+namespace quota {
+
+class Client;
+
+} // namespace quota
+
+namespace fs {
+
+class FileSystemQuotaClientFactory {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(
+ mozilla::dom::fs::FileSystemQuotaClientFactory);
+
+ static void SetCustomFactory(
+ RefPtr<FileSystemQuotaClientFactory> aCustomFactory);
+
+ static already_AddRefed<quota::Client> CreateQuotaClient();
+
+ protected:
+ virtual ~FileSystemQuotaClientFactory() = default;
+
+ virtual already_AddRefed<quota::Client> AllocQuotaClient();
+};
+
+inline already_AddRefed<quota::Client> CreateQuotaClient() {
+ return FileSystemQuotaClientFactory::CreateQuotaClient();
+}
+
+} // namespace fs
+} // namespace mozilla::dom
+
+#endif // DOM_FS_PARENT_FILESYSTEMQUOTACLIENTFACTORY_H_
diff --git a/dom/fs/parent/FileSystemStreamCallbacks.cpp b/dom/fs/parent/FileSystemStreamCallbacks.cpp
new file mode 100644
index 0000000000..fbe9d5f67b
--- /dev/null
+++ b/dom/fs/parent/FileSystemStreamCallbacks.cpp
@@ -0,0 +1,38 @@
+/* -*- 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 "FileSystemStreamCallbacks.h"
+
+#include "mozilla/dom/quota/RemoteQuotaObjectParent.h"
+
+namespace mozilla::dom {
+
+FileSystemStreamCallbacks::FileSystemStreamCallbacks()
+ : mRemoteQuotaObjectParent(nullptr) {}
+
+NS_IMPL_ISUPPORTS(FileSystemStreamCallbacks, nsIInterfaceRequestor,
+ quota::RemoteQuotaObjectParentTracker)
+
+NS_IMETHODIMP
+FileSystemStreamCallbacks::GetInterface(const nsIID& aIID, void** aResult) {
+ return QueryInterface(aIID, aResult);
+}
+
+void FileSystemStreamCallbacks::RegisterRemoteQuotaObjectParent(
+ NotNull<quota::RemoteQuotaObjectParent*> aActor) {
+ MOZ_ASSERT(!mRemoteQuotaObjectParent);
+
+ mRemoteQuotaObjectParent = aActor;
+}
+
+void FileSystemStreamCallbacks::UnregisterRemoteQuotaObjectParent(
+ NotNull<quota::RemoteQuotaObjectParent*> aActor) {
+ MOZ_ASSERT(mRemoteQuotaObjectParent);
+
+ mRemoteQuotaObjectParent = nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/parent/FileSystemStreamCallbacks.h b/dom/fs/parent/FileSystemStreamCallbacks.h
new file mode 100644
index 0000000000..98e4713faf
--- /dev/null
+++ b/dom/fs/parent/FileSystemStreamCallbacks.h
@@ -0,0 +1,38 @@
+/* -*- 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_FS_PARENT_FILESYSTEMSTREAMCALLBACKS_H_
+#define DOM_FS_PARENT_FILESYSTEMSTREAMCALLBACKS_H_
+
+#include "mozilla/dom/quota/RemoteQuotaObjectParentTracker.h"
+#include "nsIInterfaceRequestor.h"
+
+namespace mozilla::dom {
+
+class FileSystemStreamCallbacks : public nsIInterfaceRequestor,
+ public quota::RemoteQuotaObjectParentTracker {
+ public:
+ FileSystemStreamCallbacks();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_DECL_NSIINTERFACEREQUESTOR
+
+ void RegisterRemoteQuotaObjectParent(
+ NotNull<quota::RemoteQuotaObjectParent*> aActor) override;
+
+ void UnregisterRemoteQuotaObjectParent(
+ NotNull<quota::RemoteQuotaObjectParent*> aActor) override;
+
+ protected:
+ virtual ~FileSystemStreamCallbacks() = default;
+
+ quota::RemoteQuotaObjectParent* mRemoteQuotaObjectParent;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_FS_PARENT_FILESYSTEMSTREAMCALLBACKS_H_
diff --git a/dom/fs/parent/FileSystemWritableFileStreamParent.cpp b/dom/fs/parent/FileSystemWritableFileStreamParent.cpp
new file mode 100644
index 0000000000..f0e9a3852c
--- /dev/null
+++ b/dom/fs/parent/FileSystemWritableFileStreamParent.cpp
@@ -0,0 +1,85 @@
+/* -*- 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 "FileSystemWritableFileStreamParent.h"
+
+#include "FileSystemDataManager.h"
+#include "FileSystemStreamCallbacks.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemManagerParent.h"
+#include "mozilla/dom/quota/RemoteQuotaObjectParent.h"
+
+namespace mozilla::dom {
+
+class FileSystemWritableFileStreamParent::FileSystemWritableFileStreamCallbacks
+ : public FileSystemStreamCallbacks {
+ public:
+ void CloseRemoteQuotaObjectParent() {
+ if (mRemoteQuotaObjectParent) {
+ mRemoteQuotaObjectParent->Close();
+ }
+ }
+};
+
+FileSystemWritableFileStreamParent::FileSystemWritableFileStreamParent(
+ RefPtr<FileSystemManagerParent> aManager, const fs::EntryId& aEntryId,
+ const fs::FileId& aTemporaryFileId, bool aIsExclusive)
+ : mManager(std::move(aManager)),
+ mEntryId(aEntryId),
+ mTemporaryFileId(aTemporaryFileId),
+ mIsExclusive(aIsExclusive) {}
+
+FileSystemWritableFileStreamParent::~FileSystemWritableFileStreamParent() {
+ MOZ_ASSERT(mClosed);
+}
+
+mozilla::ipc::IPCResult FileSystemWritableFileStreamParent::RecvClose(
+ bool aAbort, CloseResolver&& aResolver) {
+ Close(aAbort);
+
+ aResolver(void_t());
+
+ return IPC_OK();
+}
+
+void FileSystemWritableFileStreamParent::ActorDestroy(ActorDestroyReason aWhy) {
+ if (mStreamCallbacks) {
+ mStreamCallbacks->CloseRemoteQuotaObjectParent();
+ mStreamCallbacks = nullptr;
+ }
+
+ if (!IsClosed()) {
+ Close(/* aAbort */ true);
+ }
+}
+
+nsIInterfaceRequestor*
+FileSystemWritableFileStreamParent::GetOrCreateStreamCallbacks() {
+ if (!mStreamCallbacks) {
+ if (mClosed) {
+ return nullptr;
+ }
+
+ mStreamCallbacks = MakeRefPtr<FileSystemWritableFileStreamCallbacks>();
+ }
+
+ return mStreamCallbacks.get();
+}
+
+void FileSystemWritableFileStreamParent::Close(bool aAbort) {
+ LOG(("Closing WritableFileStream"));
+
+ mClosed.Flip();
+
+ if (mIsExclusive) {
+ mManager->DataManagerStrongRef()->UnlockExclusive(mEntryId);
+ } else {
+ mManager->DataManagerStrongRef()->UnlockShared(mEntryId, mTemporaryFileId,
+ aAbort);
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/parent/FileSystemWritableFileStreamParent.h b/dom/fs/parent/FileSystemWritableFileStreamParent.h
new file mode 100644
index 0000000000..bf24f5d146
--- /dev/null
+++ b/dom/fs/parent/FileSystemWritableFileStreamParent.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_PARENT_FILESYSTEMWRITABLEFILESTREAM_H_
+#define DOM_FS_PARENT_FILESYSTEMWRITABLEFILESTREAM_H_
+
+#include "mozilla/dom/FileSystemParentTypes.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "mozilla/dom/PFileSystemWritableFileStreamParent.h"
+
+class nsIInterfaceRequestor;
+
+namespace mozilla::dom {
+
+class FileSystemManagerParent;
+
+class FileSystemWritableFileStreamParent
+ : public PFileSystemWritableFileStreamParent {
+ public:
+ FileSystemWritableFileStreamParent(RefPtr<FileSystemManagerParent> aManager,
+ const fs::EntryId& aEntryId,
+ const fs::FileId& aTemporaryFileId,
+ bool aIsExclusive);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemWritableFileStreamParent,
+ override)
+
+ mozilla::ipc::IPCResult RecvClose(bool aAbort, CloseResolver&& aResolver);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ nsIInterfaceRequestor* GetOrCreateStreamCallbacks();
+
+ private:
+ class FileSystemWritableFileStreamCallbacks;
+
+ virtual ~FileSystemWritableFileStreamParent();
+
+ bool IsClosed() const { return mClosed; }
+
+ void Close(bool aAbort);
+
+ const RefPtr<FileSystemManagerParent> mManager;
+
+ RefPtr<FileSystemWritableFileStreamCallbacks> mStreamCallbacks;
+
+ const fs::EntryId mEntryId;
+
+ const fs::FileId mTemporaryFileId;
+
+ const bool mIsExclusive;
+
+ FlippedOnce<false> mClosed;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_FS_PARENT_FILESYSTEMWRITABLEFILESTREAM_H_
diff --git a/dom/fs/parent/ResultConnection.h b/dom/fs/parent/ResultConnection.h
new file mode 100644
index 0000000000..3606a55a6b
--- /dev/null
+++ b/dom/fs/parent/ResultConnection.h
@@ -0,0 +1,19 @@
+/* -*- 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_FS_PARENT_RESULTCONNECTION_H_
+#define DOM_FS_PARENT_RESULTCONNECTION_H_
+
+#include "mozIStorageConnection.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla::dom::fs {
+
+using ResultConnection = nsCOMPtr<mozIStorageConnection>;
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_PARENT_RESULTCONNECTION_H_
diff --git a/dom/fs/parent/ResultStatement.cpp b/dom/fs/parent/ResultStatement.cpp
new file mode 100644
index 0000000000..0b0e2cf4a8
--- /dev/null
+++ b/dom/fs/parent/ResultStatement.cpp
@@ -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/. */
+
+#include "ResultStatement.h"
+
+#include "mozIStorageConnection.h"
+
+namespace mozilla::dom::fs {
+
+Result<ResultStatement, QMResult> ResultStatement::Create(
+ const ResultConnection& aConnection, const nsACString& aSQLStatement) {
+ nsCOMPtr<mozIStorageStatement> stmt;
+
+ QM_TRY(QM_TO_RESULT(
+ aConnection->CreateStatement(aSQLStatement, getter_AddRefs(stmt))));
+
+ return ResultStatement(stmt);
+};
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/parent/ResultStatement.h b/dom/fs/parent/ResultStatement.h
new file mode 100644
index 0000000000..3e532f3ae4
--- /dev/null
+++ b/dom/fs/parent/ResultStatement.h
@@ -0,0 +1,173 @@
+/* -*- 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_FS_PARENT_RESULTSTATEMENT_H_
+#define DOM_FS_PARENT_RESULTSTATEMENT_H_
+
+#include "FileSystemParentTypes.h"
+#include "mozIStorageStatement.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+class mozIStorageConnection;
+
+namespace mozilla::dom::fs {
+
+using Column = uint32_t;
+
+using ResultConnection = nsCOMPtr<mozIStorageConnection>;
+
+/**
+ * @brief ResultStatement
+ * - provides error monad Result<T, E> compatible interface to the lower level
+ * error code-based statement implementation in order to enable remote
+ * debugging with error stack traces
+ * - converts between OPFS internal data types and the generic data types of
+ * the lower level implementation
+ * - provides a customization point for requests aimed at the lower level
+ * implementation allowing for example to remap errors or implement mocks
+ */
+class ResultStatement {
+ public:
+ using underlying_t = nsCOMPtr<mozIStorageStatement>;
+
+ explicit ResultStatement(underlying_t aStmt) : mStmt(std::move(aStmt)) {}
+
+ ResultStatement(const ResultStatement& aOther)
+ : ResultStatement(aOther.mStmt) {}
+
+ ResultStatement(ResultStatement&& aOther) noexcept
+ : ResultStatement(std::move(aOther.mStmt)) {}
+
+ ResultStatement& operator=(const ResultStatement& aOther) = default;
+
+ ResultStatement& operator=(ResultStatement&& aOther) noexcept {
+ mStmt = std::move(aOther.mStmt);
+ return *this;
+ }
+
+ static Result<ResultStatement, QMResult> Create(
+ const ResultConnection& aConnection, const nsACString& aSQLStatement);
+
+ // XXX Consider moving all these "inline" methods into a separate file
+ // called ResultStatementInlines.h. ResultStatement.h wouldn't have to then
+ // include ResultExtensions.h, QuotaCommon.h and mozIStorageStatement.h
+ // which are quite large and should be preferable only included from cpp
+ // files or special headers like ResultStatementInlines.h. So in the end,
+ // other headers would include ResultStatement.h only and other cpp files
+ // would include ResultStatementInlines.h. See also IndedexDababase.h and
+ // IndexedDatabaseInlines.h to see how it's done.
+
+ inline nsresult BindEntryIdByName(const nsACString& aField,
+ const EntryId& aValue) {
+ return mStmt->BindUTF8StringAsBlobByName(aField, aValue);
+ }
+
+ inline nsresult BindFileIdByName(const nsACString& aField,
+ const FileId& aValue) {
+ return mStmt->BindUTF8StringAsBlobByName(aField, aValue.Value());
+ }
+
+ inline nsresult BindContentTypeByName(const nsACString& aField,
+ const ContentType& aValue) {
+ if (aValue.IsVoid()) {
+ return mStmt->BindNullByName(aField);
+ }
+
+ return mStmt->BindUTF8StringByName(aField, aValue);
+ }
+
+ inline nsresult BindNameByName(const nsACString& aField, const Name& aValue) {
+ return mStmt->BindStringAsBlobByName(aField, aValue);
+ }
+
+ inline nsresult BindPageNumberByName(const nsACString& aField,
+ PageNumber aValue) {
+ return mStmt->BindInt32ByName(aField, aValue);
+ }
+
+ inline nsresult BindUsageByName(const nsACString& aField, Usage aValue) {
+ return mStmt->BindInt64ByName(aField, aValue);
+ }
+
+ inline nsresult BindBooleanByName(const nsACString& aField, bool aValue) {
+ return mStmt->BindInt32ByName(aField, aValue ? 1 : 0);
+ }
+
+ inline Result<bool, QMResult> GetBooleanByColumn(Column aColumn) {
+ int32_t value = 0;
+ QM_TRY(QM_TO_RESULT(mStmt->GetInt32(aColumn, &value)));
+
+ return 0 != value;
+ }
+
+ inline Result<ContentType, QMResult> GetContentTypeByColumn(Column aColumn) {
+ ContentType value;
+ QM_TRY(QM_TO_RESULT(mStmt->GetUTF8String(aColumn, value)));
+
+ return value;
+ }
+
+ inline Result<EntryId, QMResult> GetEntryIdByColumn(Column aColumn) {
+ EntryId value;
+ QM_TRY(QM_TO_RESULT(mStmt->GetBlobAsUTF8String(aColumn, value)));
+
+ return value;
+ }
+
+ inline Result<FileId, QMResult> GetFileIdByColumn(Column aColumn) {
+ nsCString value;
+ QM_TRY(QM_TO_RESULT(mStmt->GetBlobAsUTF8String(aColumn, value)));
+
+ return FileId(std::move(value));
+ }
+
+ inline Result<Name, QMResult> GetNameByColumn(Column aColumn) {
+ Name value;
+ QM_TRY(QM_TO_RESULT(mStmt->GetBlobAsString(aColumn, value)));
+
+ return value;
+ }
+
+ inline Result<Usage, QMResult> GetUsageByColumn(Column aColumn) {
+ Usage value = 0;
+ QM_TRY(QM_TO_RESULT(mStmt->GetInt64(aColumn, &value)));
+
+ return value;
+ }
+
+ inline bool IsNullByColumn(Column aColumn) const {
+ bool value = mStmt->IsNull(aColumn);
+
+ return value;
+ }
+
+ inline nsresult Execute() { return mStmt->Execute(); }
+
+ inline Result<bool, QMResult> ExecuteStep() {
+ bool hasEntries = false;
+ QM_TRY(QM_TO_RESULT(mStmt->ExecuteStep(&hasEntries)));
+
+ return hasEntries;
+ }
+
+ inline Result<bool, QMResult> YesOrNoQuery() {
+ bool hasEntries = false;
+ QM_TRY(QM_TO_RESULT(mStmt->ExecuteStep(&hasEntries)));
+ MOZ_ALWAYS_TRUE(hasEntries);
+ return GetBooleanByColumn(0u);
+ }
+
+ private:
+ underlying_t mStmt;
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_PARENT_RESULTSTATEMENT_H_
diff --git a/dom/fs/parent/StartedTransaction.cpp b/dom/fs/parent/StartedTransaction.cpp
new file mode 100644
index 0000000000..7fee51e61e
--- /dev/null
+++ b/dom/fs/parent/StartedTransaction.cpp
@@ -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/. */
+
+#include "StartedTransaction.h"
+
+#include "ResultConnection.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+
+namespace mozilla::dom::fs {
+
+/* static */
+Result<StartedTransaction, QMResult> StartedTransaction::Create(
+ const ResultConnection& aConn) {
+ auto transaction = MakeUnique<mozStorageTransaction>(
+ aConn.get(), /* aCommitOnComplete */ false,
+ mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ QM_TRY(QM_TO_RESULT(transaction->Start()));
+
+ return StartedTransaction(std::move(transaction));
+}
+
+nsresult StartedTransaction::Commit() { return mTransaction->Commit(); }
+
+nsresult StartedTransaction::Rollback() { return mTransaction->Rollback(); }
+
+StartedTransaction::StartedTransaction(
+ UniquePtr<mozStorageTransaction>&& aTransaction)
+ : mTransaction(std::move(aTransaction)) {}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/parent/StartedTransaction.h b/dom/fs/parent/StartedTransaction.h
new file mode 100644
index 0000000000..95a01326bb
--- /dev/null
+++ b/dom/fs/parent/StartedTransaction.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_FS_PARENT_STARTEDTRANSACTION_H_
+#define DOM_FS_PARENT_STARTEDTRANSACTION_H_
+
+#include "ResultConnection.h"
+#include "mozStorageHelper.h"
+#include "mozilla/dom/QMResult.h"
+
+namespace mozilla::dom::fs {
+
+class StartedTransaction {
+ public:
+ static Result<StartedTransaction, QMResult> Create(
+ const ResultConnection& aConn);
+
+ StartedTransaction(StartedTransaction&& aOther) = default;
+
+ StartedTransaction(const StartedTransaction& aOther) = delete;
+
+ nsresult Commit();
+
+ nsresult Rollback();
+
+ ~StartedTransaction() = default;
+
+ private:
+ explicit StartedTransaction(UniquePtr<mozStorageTransaction>&& aTransaction);
+
+ UniquePtr<mozStorageTransaction> mTransaction;
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_PARENT_STARTEDTRANSACTION_H_
diff --git a/dom/fs/parent/datamodel/FileSystemDataManager.cpp b/dom/fs/parent/datamodel/FileSystemDataManager.cpp
new file mode 100644
index 0000000000..549a8d5865
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDataManager.cpp
@@ -0,0 +1,672 @@
+/* -*- 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 "FileSystemDataManager.h"
+
+#include "ErrorList.h"
+#include "FileSystemDatabaseManager.h"
+#include "FileSystemDatabaseManagerVersion001.h"
+#include "FileSystemDatabaseManagerVersion002.h"
+#include "FileSystemFileManager.h"
+#include "FileSystemHashSource.h"
+#include "FileSystemParentTypes.h"
+#include "ResultStatement.h"
+#include "SchemaVersion001.h"
+#include "SchemaVersion002.h"
+#include "fs/FileSystemConstants.h"
+#include "mozIStorageService.h"
+#include "mozStorageCID.h"
+#include "mozilla/Result.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemManagerParent.h"
+#include "mozilla/dom/QMResult.h"
+#include "mozilla/dom/quota/ClientImpl.h"
+#include "mozilla/dom/quota/DirectoryLock.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/dom/quota/UsageInfo.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "nsBaseHashtable.h"
+#include "nsCOMPtr.h"
+#include "nsHashKeys.h"
+#include "nsIFile.h"
+#include "nsIFileURL.h"
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::dom::fs::data {
+
+namespace {
+
+// nsCStringHashKey with disabled memmove
+class nsCStringHashKeyDM : public nsCStringHashKey {
+ public:
+ explicit nsCStringHashKeyDM(const nsCStringHashKey::KeyTypePointer aKey)
+ : nsCStringHashKey(aKey) {}
+ enum { ALLOW_MEMMOVE = false };
+};
+
+// When CheckedUnsafePtr's checking is enabled, it's necessary to ensure that
+// the hashtable uses the copy constructor instead of memmove for moving entries
+// since memmove will break CheckedUnsafePtr in a memory-corrupting way.
+using FileSystemDataManagerHashKey =
+ std::conditional<DiagnosticAssertEnabled::value, nsCStringHashKeyDM,
+ nsCStringHashKey>::type;
+
+// Raw (but checked when the diagnostic assert is enabled) references as we
+// don't want to keep FileSystemDataManager objects alive forever. When a
+// FileSystemDataManager is destroyed it calls RemoveFileSystemDataManager
+// to clear itself.
+using FileSystemDataManagerHashtable =
+ nsBaseHashtable<FileSystemDataManagerHashKey,
+ NotNull<CheckedUnsafePtr<FileSystemDataManager>>,
+ MovingNotNull<CheckedUnsafePtr<FileSystemDataManager>>>;
+
+// This hashtable isn't protected by any mutex but it is only ever touched on
+// the PBackground thread.
+StaticAutoPtr<FileSystemDataManagerHashtable> gDataManagers;
+
+RefPtr<FileSystemDataManager> GetFileSystemDataManager(const Origin& aOrigin) {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ if (gDataManagers) {
+ auto maybeDataManager = gDataManagers->MaybeGet(aOrigin);
+ if (maybeDataManager) {
+ RefPtr<FileSystemDataManager> result(
+ std::move(*maybeDataManager).unwrapBasePtr());
+ return result;
+ }
+ }
+
+ return nullptr;
+}
+
+void AddFileSystemDataManager(
+ const Origin& aOrigin, const RefPtr<FileSystemDataManager>& aDataManager) {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!quota::QuotaManager::IsShuttingDown());
+
+ if (!gDataManagers) {
+ gDataManagers = new FileSystemDataManagerHashtable();
+ }
+
+ MOZ_ASSERT(!gDataManagers->Contains(aOrigin));
+ gDataManagers->InsertOrUpdate(aOrigin,
+ WrapMovingNotNullUnchecked(aDataManager));
+}
+
+void RemoveFileSystemDataManager(const Origin& aOrigin) {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ MOZ_ASSERT(gDataManagers);
+ const DebugOnly<bool> removed = gDataManagers->Remove(aOrigin);
+ MOZ_ASSERT(removed);
+
+ if (!gDataManagers->Count()) {
+ gDataManagers = nullptr;
+ }
+}
+
+} // namespace
+
+Result<ResultConnection, QMResult> GetStorageConnection(
+ const quota::OriginMetadata& aOriginMetadata,
+ const int64_t aDirectoryLockId) {
+ MOZ_ASSERT(aDirectoryLockId >= -1);
+
+ // Ensure that storage is initialized and file system folder exists!
+ QM_TRY_INSPECT(const auto& dbFileUrl,
+ GetDatabaseFileURL(aOriginMetadata, aDirectoryLockId));
+
+ QM_TRY_INSPECT(
+ const auto& storageService,
+ QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED(
+ nsCOMPtr<mozIStorageService>, MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID)));
+
+ QM_TRY_UNWRAP(auto connection,
+ QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, storageService,
+ OpenDatabaseWithFileURL, dbFileUrl, ""_ns,
+ mozIStorageService::CONNECTION_DEFAULT)));
+
+ ResultConnection result(connection);
+
+ return result;
+}
+
+Result<EntryId, QMResult> GetRootHandle(const Origin& origin) {
+ MOZ_ASSERT(!origin.IsEmpty());
+
+ return FileSystemHashSource::GenerateHash(origin, kRootString);
+}
+
+Result<EntryId, QMResult> GetEntryHandle(
+ const FileSystemChildMetadata& aHandle) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ return FileSystemHashSource::GenerateHash(aHandle.parentId(),
+ aHandle.childName());
+}
+
+FileSystemDataManager::FileSystemDataManager(
+ const quota::OriginMetadata& aOriginMetadata,
+ RefPtr<quota::QuotaManager> aQuotaManager,
+ MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget,
+ MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue)
+ : mOriginMetadata(aOriginMetadata),
+ mQuotaManager(std::move(aQuotaManager)),
+ mBackgroundTarget(WrapNotNull(GetCurrentSerialEventTarget())),
+ mIOTarget(std::move(aIOTarget)),
+ mIOTaskQueue(std::move(aIOTaskQueue)),
+ mRegCount(0),
+ mVersion(0),
+ mState(State::Initial) {}
+
+FileSystemDataManager::~FileSystemDataManager() {
+ NS_ASSERT_OWNINGTHREAD(FileSystemDataManager);
+ MOZ_ASSERT(mState == State::Closed);
+ MOZ_ASSERT(!mDatabaseManager);
+}
+
+RefPtr<FileSystemDataManager::CreatePromise>
+FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ const quota::OriginMetadata& aOriginMetadata) {
+ if (quota::QuotaManager::IsShuttingDown()) {
+ return CreatePromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ if (RefPtr<FileSystemDataManager> dataManager =
+ GetFileSystemDataManager(aOriginMetadata.mOrigin)) {
+ if (dataManager->IsOpening()) {
+ // We have to wait for the open to be finished before resolving the
+ // promise. The manager can't close itself in the meantime because we
+ // add a new registration in the lambda capture list.
+ return dataManager->OnOpen()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [dataManager = Registered<FileSystemDataManager>(dataManager)](
+ const BoolPromise::ResolveOrRejectValue& aValue) {
+ if (aValue.IsReject()) {
+ return CreatePromise::CreateAndReject(aValue.RejectValue(),
+ __func__);
+ }
+ return CreatePromise::CreateAndResolve(dataManager, __func__);
+ });
+ }
+
+ if (dataManager->IsClosing()) {
+ // First, we need to wait for the close to be finished. After that the
+ // manager is closed and it can't be opened again. The only option is
+ // to create a new manager and open it. However, all this stuff is
+ // asynchronous, so it can happen that something else called
+ // `GetOrCreateFileSystemManager` in the meantime. For that reason, we
+ // shouldn't try to create a new manager and open it here, a "recursive"
+ // call to `GetOrCreateFileSystemManager` will handle any new situation.
+ return dataManager->OnClose()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aOriginMetadata](const BoolPromise::ResolveOrRejectValue&) {
+ return GetOrCreateFileSystemDataManager(aOriginMetadata);
+ });
+ }
+
+ return CreatePromise::CreateAndResolve(
+ Registered<FileSystemDataManager>(std::move(dataManager)), __func__);
+ }
+
+ RefPtr<quota::QuotaManager> quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY_UNWRAP(auto streamTransportService,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIEventTarget>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ NS_STREAMTRANSPORTSERVICE_CONTRACTID),
+ CreatePromise::CreateAndReject(NS_ERROR_FAILURE, __func__));
+
+ nsCString taskQueueName("OPFS "_ns + aOriginMetadata.mOrigin);
+
+ RefPtr<TaskQueue> ioTaskQueue =
+ TaskQueue::Create(do_AddRef(streamTransportService), taskQueueName.get());
+
+ auto dataManager = MakeRefPtr<FileSystemDataManager>(
+ aOriginMetadata, std::move(quotaManager),
+ WrapMovingNotNull(streamTransportService),
+ WrapMovingNotNull(ioTaskQueue));
+
+ AddFileSystemDataManager(aOriginMetadata.mOrigin, dataManager);
+
+ return dataManager->BeginOpen()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [dataManager = Registered<FileSystemDataManager>(dataManager)](
+ const BoolPromise::ResolveOrRejectValue& aValue) {
+ if (aValue.IsReject()) {
+ return CreatePromise::CreateAndReject(aValue.RejectValue(), __func__);
+ }
+
+ return CreatePromise::CreateAndResolve(dataManager, __func__);
+ });
+}
+
+// static
+void FileSystemDataManager::AbortOperationsForLocks(
+ const quota::Client::DirectoryLockIdTable& aDirectoryLockIds) {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ // XXX Share the iteration code with `InitiateShutdown`, for example by
+ // creating a helper function which would take a predicate function.
+
+ if (!gDataManagers) {
+ return;
+ }
+
+ for (const auto& dataManager : gDataManagers->Values()) {
+ // Check if the Manager holds an acquired DirectoryLock. Origin clearing
+ // can't be blocked by this Manager if there is no acquired DirectoryLock.
+ // If there is an acquired DirectoryLock, check if the table contains the
+ // lock for the Manager.
+ if (quota::Client::IsLockForObjectAcquiredAndContainedInLockTable(
+ *dataManager, aDirectoryLockIds)) {
+ dataManager->RequestAllowToClose();
+ }
+ }
+}
+
+// static
+void FileSystemDataManager::InitiateShutdown() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ if (!gDataManagers) {
+ return;
+ }
+
+ for (const auto& dataManager : gDataManagers->Values()) {
+ dataManager->RequestAllowToClose();
+ }
+}
+
+// static
+bool FileSystemDataManager::IsShutdownCompleted() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ return !gDataManagers;
+}
+
+void FileSystemDataManager::AssertIsOnIOTarget() const {
+ DebugOnly<bool> current = false;
+ MOZ_ASSERT(NS_SUCCEEDED(mIOTarget->IsOnCurrentThread(&current)));
+ MOZ_ASSERT(current);
+}
+
+void FileSystemDataManager::Register() { mRegCount++; }
+
+void FileSystemDataManager::Unregister() {
+ MOZ_ASSERT(mRegCount > 0);
+
+ mRegCount--;
+
+ if (IsInactive()) {
+ BeginClose();
+ }
+}
+
+void FileSystemDataManager::RegisterActor(
+ NotNull<FileSystemManagerParent*> aActor) {
+ MOZ_ASSERT(!mBackgroundThreadAccessible.Access()->mActors.Contains(aActor));
+
+ mBackgroundThreadAccessible.Access()->mActors.Insert(aActor);
+
+#ifdef DEBUG
+ aActor->SetRegistered(true);
+#endif
+}
+
+void FileSystemDataManager::UnregisterActor(
+ NotNull<FileSystemManagerParent*> aActor) {
+ MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mActors.Contains(aActor));
+
+ mBackgroundThreadAccessible.Access()->mActors.Remove(aActor);
+
+#ifdef DEBUG
+ aActor->SetRegistered(false);
+#endif
+
+ if (IsInactive()) {
+ BeginClose();
+ }
+}
+
+void FileSystemDataManager::RegisterAccessHandle(
+ NotNull<FileSystemAccessHandle*> aAccessHandle) {
+ MOZ_ASSERT(!mBackgroundThreadAccessible.Access()->mAccessHandles.Contains(
+ aAccessHandle));
+
+ mBackgroundThreadAccessible.Access()->mAccessHandles.Insert(aAccessHandle);
+}
+
+void FileSystemDataManager::UnregisterAccessHandle(
+ NotNull<FileSystemAccessHandle*> aAccessHandle) {
+ MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mAccessHandles.Contains(
+ aAccessHandle));
+
+ mBackgroundThreadAccessible.Access()->mAccessHandles.Remove(aAccessHandle);
+
+ if (IsInactive()) {
+ BeginClose();
+ }
+}
+
+RefPtr<BoolPromise> FileSystemDataManager::OnOpen() {
+ MOZ_ASSERT(mState == State::Opening);
+
+ return mOpenPromiseHolder.Ensure(__func__);
+}
+
+RefPtr<BoolPromise> FileSystemDataManager::OnClose() {
+ MOZ_ASSERT(mState == State::Closing);
+
+ return mClosePromiseHolder.Ensure(__func__);
+}
+
+// Note: Input can be temporary or main file id
+Result<bool, QMResult> FileSystemDataManager::IsLocked(
+ const FileId& aFileId) const {
+ auto checkIfEntryIdIsLocked = [this, &aFileId]() -> Result<bool, QMResult> {
+ QM_TRY_INSPECT(const EntryId& entryId,
+ mDatabaseManager->GetEntryId(aFileId));
+
+ return IsLocked(entryId);
+ };
+
+ auto valueToSome = [](auto aValue) { return Some(std::move(aValue)); };
+
+ QM_TRY_UNWRAP(Maybe<bool> maybeLocked,
+ QM_OR_ELSE_LOG_VERBOSE_IF(
+ // Expression.
+ (checkIfEntryIdIsLocked().map(valueToSome)),
+ // Predicate.
+ IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>,
+ // Fallback.
+ ([](const auto&) -> Result<Maybe<bool>, QMResult> {
+ return Some(false); // Non-existent files are not locked.
+ })));
+
+ if (!maybeLocked) {
+ // If the metadata is inaccessible, we block modifications.
+ return true;
+ }
+
+ return *maybeLocked;
+}
+
+Result<bool, QMResult> FileSystemDataManager::IsLocked(
+ const EntryId& aEntryId) const {
+ return mExclusiveLocks.Contains(aEntryId) || mSharedLocks.Contains(aEntryId);
+}
+
+Result<FileId, QMResult> FileSystemDataManager::LockExclusive(
+ const EntryId& aEntryId) {
+ QM_TRY_UNWRAP(const bool isLocked, IsLocked(aEntryId));
+ if (isLocked) {
+ return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
+ }
+
+ QM_TRY_INSPECT(const FileId& fileId,
+ mDatabaseManager->EnsureFileId(aEntryId));
+
+ // If the file has been removed, we should get a file not found error.
+ // Otherwise, if usage tracking cannot be started because file size is not
+ // known and attempts to read it are failing, lock is denied to freeze the
+ // quota usage until the (external) blocker is gone or the file is removed.
+ QM_TRY(QM_TO_RESULT(mDatabaseManager->BeginUsageTracking(fileId)));
+
+ LOG_VERBOSE(("ExclusiveLock"));
+ mExclusiveLocks.Insert(aEntryId);
+
+ return fileId;
+}
+
+// TODO: Improve reporting of failures, see bug 1840811.
+void FileSystemDataManager::UnlockExclusive(const EntryId& aEntryId) {
+ MOZ_ASSERT(mExclusiveLocks.Contains(aEntryId));
+
+ LOG_VERBOSE(("ExclusiveUnlock"));
+ mExclusiveLocks.Remove(aEntryId);
+
+ QM_TRY_INSPECT(const FileId& fileId, mDatabaseManager->GetFileId(aEntryId),
+ QM_VOID);
+
+ // On error, usage tracking remains on to prevent writes until usage is
+ // updated successfully.
+ QM_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(fileId)), QM_VOID);
+ QM_TRY(MOZ_TO_RESULT(mDatabaseManager->EndUsageTracking(fileId)), QM_VOID);
+}
+
+Result<FileId, QMResult> FileSystemDataManager::LockShared(
+ const EntryId& aEntryId) {
+ if (mExclusiveLocks.Contains(aEntryId)) {
+ return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
+ }
+
+ auto& count = mSharedLocks.LookupOrInsert(aEntryId);
+ if (!(1u + CheckedUint32(count)).isValid()) { // don't make the count invalid
+ return Err(QMResult(NS_ERROR_UNEXPECTED));
+ }
+
+ QM_TRY_INSPECT(const FileId& fileId,
+ mDatabaseManager->EnsureTemporaryFileId(aEntryId));
+
+ // If the file has been removed, we should get a file not found error.
+ // Otherwise, if usage tracking cannot be started because file size is not
+ // known and attempts to read it are failing, lock is denied to freeze the
+ // quota usage until the (external) blocker is gone or the file is removed.
+ QM_TRY(QM_TO_RESULT(mDatabaseManager->BeginUsageTracking(fileId)));
+
+ ++count;
+ LOG_VERBOSE(("SharedLock %u", count));
+
+ return fileId;
+}
+
+// TODO: Improve reporting of failures, see bug 1840811.
+void FileSystemDataManager::UnlockShared(const EntryId& aEntryId,
+ const FileId& aFileId, bool aAbort) {
+ MOZ_ASSERT(!mExclusiveLocks.Contains(aEntryId));
+ MOZ_ASSERT(mSharedLocks.Contains(aEntryId));
+
+ auto entry = mSharedLocks.Lookup(aEntryId);
+ MOZ_ASSERT(entry);
+
+ MOZ_ASSERT(entry.Data() > 0);
+ --entry.Data();
+
+ LOG_VERBOSE(("SharedUnlock %u", *entry));
+
+ if (0u == entry.Data()) {
+ entry.Remove();
+ }
+
+ // On error, usage tracking remains on to prevent writes until usage is
+ // updated successfully.
+ QM_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(aFileId)), QM_VOID);
+ QM_TRY(MOZ_TO_RESULT(mDatabaseManager->EndUsageTracking(aFileId)), QM_VOID);
+ QM_TRY(
+ MOZ_TO_RESULT(mDatabaseManager->MergeFileId(aEntryId, aFileId, aAbort)),
+ QM_VOID);
+}
+
+FileMode FileSystemDataManager::GetMode(bool aKeepData) const {
+ if (1 == mVersion) {
+ return FileMode::EXCLUSIVE;
+ }
+
+ return aKeepData ? FileMode::SHARED_FROM_COPY : FileMode::SHARED_FROM_EMPTY;
+}
+
+bool FileSystemDataManager::IsInactive() const {
+ auto data = mBackgroundThreadAccessible.Access();
+ return !mRegCount && !data->mActors.Count() && !data->mAccessHandles.Count();
+}
+
+void FileSystemDataManager::RequestAllowToClose() {
+ for (const auto& actor : mBackgroundThreadAccessible.Access()->mActors) {
+ actor->RequestAllowToClose();
+ }
+}
+
+RefPtr<BoolPromise> FileSystemDataManager::BeginOpen() {
+ MOZ_ASSERT(mQuotaManager);
+ MOZ_ASSERT(mState == State::Initial);
+
+ mState = State::Opening;
+
+ mQuotaManager
+ ->OpenClientDirectory(
+ {mOriginMetadata, mozilla::dom::quota::Client::FILESYSTEM})
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)](
+ quota::ClientDirectoryLockPromise::ResolveOrRejectValue&& value) {
+ if (value.IsReject()) {
+ return BoolPromise::CreateAndReject(value.RejectValue(),
+ __func__);
+ }
+
+ self->mDirectoryLock = std::move(value.ResolveValue());
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(mQuotaManager->IOThread(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)](
+ const BoolPromise::ResolveOrRejectValue& value) {
+ if (value.IsReject()) {
+ return BoolPromise::CreateAndReject(value.RejectValue(),
+ __func__);
+ }
+
+ QM_TRY(MOZ_TO_RESULT(
+ EnsureFileSystemDirectory(self->mOriginMetadata)),
+ CreateAndRejectBoolPromise);
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(
+ MutableIOTaskQueuePtr(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)](
+ const BoolPromise::ResolveOrRejectValue& value) {
+ if (value.IsReject()) {
+ return BoolPromise::CreateAndReject(value.RejectValue(),
+ __func__);
+ }
+
+ QM_TRY_UNWRAP(auto connection,
+ GetStorageConnection(self->mOriginMetadata,
+ self->mDirectoryLock->Id()),
+ CreateAndRejectBoolPromiseFromQMResult);
+
+ QM_TRY_UNWRAP(UniquePtr<FileSystemFileManager> fmPtr,
+ FileSystemFileManager::CreateFileSystemFileManager(
+ self->mOriginMetadata),
+ CreateAndRejectBoolPromiseFromQMResult);
+
+ QM_TRY_UNWRAP(
+ self->mVersion,
+ QM_OR_ELSE_WARN_IF(
+ // Expression.
+ SchemaVersion002::InitializeConnection(
+ connection, *fmPtr, self->mOriginMetadata.mOrigin),
+ // Predicate.
+ ([](const auto&) { return true; }),
+ // Fallback.
+ ([&self, &connection](const auto&) {
+ QM_TRY_RETURN(SchemaVersion001::InitializeConnection(
+ connection, self->mOriginMetadata.mOrigin));
+ })),
+ CreateAndRejectBoolPromiseFromQMResult);
+
+ QM_TRY_UNWRAP(
+ EntryId rootId,
+ fs::data::GetRootHandle(self->mOriginMetadata.mOrigin),
+ CreateAndRejectBoolPromiseFromQMResult);
+
+ switch (self->mVersion) {
+ case 1: {
+ self->mDatabaseManager =
+ MakeUnique<FileSystemDatabaseManagerVersion001>(
+ self, std::move(connection), std::move(fmPtr), rootId);
+ break;
+ }
+
+ case 2: {
+ self->mDatabaseManager =
+ MakeUnique<FileSystemDatabaseManagerVersion002>(
+ self, std::move(connection), std::move(fmPtr), rootId);
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)](
+ const BoolPromise::ResolveOrRejectValue& value) {
+ if (value.IsReject()) {
+ self->mState = State::Initial;
+
+ self->mOpenPromiseHolder.RejectIfExists(value.RejectValue(),
+ __func__);
+
+ } else {
+ self->mState = State::Open;
+
+ self->mOpenPromiseHolder.ResolveIfExists(true, __func__);
+ }
+ });
+
+ return OnOpen();
+}
+
+RefPtr<BoolPromise> FileSystemDataManager::BeginClose() {
+ MOZ_ASSERT(mState != State::Closing && mState != State::Closed);
+ MOZ_ASSERT(IsInactive());
+
+ mState = State::Closing;
+
+ InvokeAsync(MutableIOTaskQueuePtr(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)]() {
+ if (self->mDatabaseManager) {
+ self->mDatabaseManager->Close();
+ self->mDatabaseManager = nullptr;
+ }
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(MutableBackgroundTargetPtr(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)](
+ const BoolPromise::ResolveOrRejectValue&) {
+ return self->mIOTaskQueue->BeginShutdown();
+ })
+ ->Then(MutableBackgroundTargetPtr(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)](
+ const ShutdownPromise::ResolveOrRejectValue&) {
+ self->mDirectoryLock = nullptr;
+
+ RemoveFileSystemDataManager(self->mOriginMetadata.mOrigin);
+
+ self->mState = State::Closed;
+
+ self->mClosePromiseHolder.ResolveIfExists(true, __func__);
+ });
+
+ return OnClose();
+}
+
+} // namespace mozilla::dom::fs::data
diff --git a/dom/fs/parent/datamodel/FileSystemDataManager.h b/dom/fs/parent/datamodel/FileSystemDataManager.h
new file mode 100644
index 0000000000..ab0c603700
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDataManager.h
@@ -0,0 +1,186 @@
+/* -*- 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_FS_PARENT_DATAMODEL_FILESYSTEMDATAMANAGER_H_
+#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATAMANAGER_H_
+
+#include "FileSystemParentTypes.h"
+#include "ResultConnection.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/ThreadBound.h"
+#include "mozilla/dom/FileSystemHelpers.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/CheckedUnsafePtr.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "nsCOMPtr.h"
+#include "nsISupportsUtils.h"
+#include "nsString.h"
+#include "nsTHashSet.h"
+
+namespace mozilla {
+
+template <typename V, typename E>
+class Result;
+
+namespace dom {
+
+class FileSystemAccessHandle;
+class FileSystemManagerParent;
+
+namespace fs {
+struct FileId;
+class FileSystemChildMetadata;
+} // namespace fs
+
+namespace quota {
+class DirectoryLock;
+class QuotaManager;
+} // namespace quota
+
+namespace fs::data {
+
+class FileSystemDatabaseManager;
+
+Result<EntryId, QMResult> GetRootHandle(const Origin& origin);
+
+Result<EntryId, QMResult> GetEntryHandle(
+ const FileSystemChildMetadata& aHandle);
+
+Result<ResultConnection, QMResult> GetStorageConnection(
+ const quota::OriginMetadata& aOriginMetadata,
+ const int64_t aDirectoryLockId);
+
+class FileSystemDataManager
+ : public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
+ public:
+ enum struct State : uint8_t { Initial = 0, Opening, Open, Closing, Closed };
+
+ FileSystemDataManager(const quota::OriginMetadata& aOriginMetadata,
+ RefPtr<quota::QuotaManager> aQuotaManager,
+ MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget,
+ MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue);
+
+ // IsExclusive is true because we want to allow the move operations. There's
+ // always just one consumer anyway.
+ using CreatePromise = MozPromise<Registered<FileSystemDataManager>, nsresult,
+ /* IsExclusive */ true>;
+ static RefPtr<CreatePromise> GetOrCreateFileSystemDataManager(
+ const quota::OriginMetadata& aOriginMetadata);
+
+ static void AbortOperationsForLocks(
+ const quota::Client::DirectoryLockIdTable& aDirectoryLockIds);
+
+ static void InitiateShutdown();
+
+ static bool IsShutdownCompleted();
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemDataManager)
+
+ void AssertIsOnIOTarget() const;
+
+ const quota::OriginMetadata& OriginMetadataRef() const {
+ return mOriginMetadata;
+ }
+
+ nsISerialEventTarget* MutableBackgroundTargetPtr() const {
+ return mBackgroundTarget.get();
+ }
+
+ nsIEventTarget* MutableIOTargetPtr() const { return mIOTarget.get(); }
+
+ nsISerialEventTarget* MutableIOTaskQueuePtr() const {
+ return mIOTaskQueue.get();
+ }
+
+ Maybe<quota::DirectoryLock&> MaybeDirectoryLockRef() const {
+ return ToMaybeRef(mDirectoryLock.get());
+ }
+
+ FileSystemDatabaseManager* MutableDatabaseManagerPtr() const {
+ MOZ_ASSERT(mDatabaseManager);
+
+ return mDatabaseManager.get();
+ }
+
+ void Register();
+
+ void Unregister();
+
+ void RegisterActor(NotNull<FileSystemManagerParent*> aActor);
+
+ void UnregisterActor(NotNull<FileSystemManagerParent*> aActor);
+
+ void RegisterAccessHandle(NotNull<FileSystemAccessHandle*> aAccessHandle);
+
+ void UnregisterAccessHandle(NotNull<FileSystemAccessHandle*> aAccessHandle);
+
+ bool IsOpen() const { return mState == State::Open; }
+
+ RefPtr<BoolPromise> OnOpen();
+
+ RefPtr<BoolPromise> OnClose();
+
+ Result<bool, QMResult> IsLocked(const FileId& aFileId) const;
+
+ Result<bool, QMResult> IsLocked(const EntryId& aEntryId) const;
+
+ Result<FileId, QMResult> LockExclusive(const EntryId& aEntryId);
+
+ void UnlockExclusive(const EntryId& aEntryId);
+
+ Result<FileId, QMResult> LockShared(const EntryId& aEntryId);
+
+ void UnlockShared(const EntryId& aEntryId, const FileId& aFileId,
+ bool aAbort);
+
+ FileMode GetMode(bool aKeepData) const;
+
+ protected:
+ virtual ~FileSystemDataManager();
+
+ bool IsInactive() const;
+
+ bool IsOpening() const { return mState == State::Opening; }
+
+ bool IsClosing() const { return mState == State::Closing; }
+
+ void RequestAllowToClose();
+
+ RefPtr<BoolPromise> BeginOpen();
+
+ RefPtr<BoolPromise> BeginClose();
+
+ // Things touched on background thread only.
+ struct BackgroundThreadAccessible {
+ nsTHashSet<FileSystemManagerParent*> mActors;
+ nsTHashSet<FileSystemAccessHandle*> mAccessHandles;
+ };
+ ThreadBound<BackgroundThreadAccessible> mBackgroundThreadAccessible;
+
+ const quota::OriginMetadata mOriginMetadata;
+ nsTHashSet<EntryId> mExclusiveLocks;
+ nsTHashMap<EntryId, uint32_t> mSharedLocks;
+ NS_DECL_OWNINGEVENTTARGET
+ const RefPtr<quota::QuotaManager> mQuotaManager;
+ const NotNull<nsCOMPtr<nsISerialEventTarget>> mBackgroundTarget;
+ const NotNull<nsCOMPtr<nsIEventTarget>> mIOTarget;
+ const NotNull<RefPtr<TaskQueue>> mIOTaskQueue;
+ RefPtr<quota::DirectoryLock> mDirectoryLock;
+ UniquePtr<FileSystemDatabaseManager> mDatabaseManager;
+ MozPromiseHolder<BoolPromise> mOpenPromiseHolder;
+ MozPromiseHolder<BoolPromise> mClosePromiseHolder;
+ uint32_t mRegCount;
+ DatabaseVersion mVersion;
+ State mState;
+};
+
+} // namespace fs::data
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATAMANAGER_H_
diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp b/dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp
new file mode 100644
index 0000000000..2b76a3b09d
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp
@@ -0,0 +1,101 @@
+/* -*- 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 "FileSystemDatabaseManager.h"
+
+#include "FileSystemDatabaseManagerVersion001.h"
+#include "FileSystemDatabaseManagerVersion002.h"
+#include "FileSystemFileManager.h"
+#include "ResultConnection.h"
+#include "ResultStatement.h"
+#include "SchemaVersion001.h"
+#include "SchemaVersion002.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+
+namespace mozilla::dom::fs::data {
+
+namespace {
+
+Result<Usage, QMResult> GetFileUsage(const ResultConnection& aConnection) {
+ DatabaseVersion version = 0;
+ QM_TRY(QM_TO_RESULT(aConnection->GetSchemaVersion(&version)));
+
+ switch (version) {
+ case 0: {
+ return 0;
+ }
+
+ case 1: {
+ QM_TRY_RETURN(
+ FileSystemDatabaseManagerVersion001::GetFileUsage(aConnection));
+ }
+
+ case 2: {
+ QM_TRY_RETURN(
+ FileSystemDatabaseManagerVersion002::GetFileUsage(aConnection));
+ }
+
+ default:
+ break;
+ }
+
+ return Err(QMResult(NS_ERROR_NOT_IMPLEMENTED));
+}
+
+} // namespace
+
+/* static */
+nsresult FileSystemDatabaseManager::RescanUsages(
+ const ResultConnection& aConnection,
+ const quota::OriginMetadata& aOriginMetadata) {
+ DatabaseVersion version = 0;
+ QM_TRY(MOZ_TO_RESULT(aConnection->GetSchemaVersion(&version)));
+
+ switch (version) {
+ case 0: {
+ return NS_OK;
+ }
+
+ case 1:
+ return FileSystemDatabaseManagerVersion001::RescanTrackedUsages(
+ aConnection, aOriginMetadata);
+
+ case 2: {
+ return FileSystemDatabaseManagerVersion002::RescanTrackedUsages(
+ aConnection, aOriginMetadata);
+ }
+
+ default:
+ break;
+ }
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* static */
+Result<quota::UsageInfo, QMResult> FileSystemDatabaseManager::GetUsage(
+ const ResultConnection& aConnection,
+ const quota::OriginMetadata& aOriginMetadata) {
+ QM_TRY_INSPECT(const auto& databaseFile, GetDatabaseFile(aOriginMetadata));
+
+ // If database is deleted between connection creation and now, error
+ int64_t dbSize = 0;
+ QM_TRY(QM_TO_RESULT(databaseFile->GetFileSize(&dbSize)));
+
+ quota::UsageInfo result(quota::DatabaseUsageType(Some(dbSize)));
+
+ QM_TRY_INSPECT(const Usage& fileUsage, GetFileUsage(aConnection));
+
+ // XXX: DatabaseUsage is currently total usage for most forms of storage
+ result += quota::DatabaseUsageType(Some(fileUsage));
+
+ return result;
+}
+
+} // namespace mozilla::dom::fs::data
diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManager.h b/dom/fs/parent/datamodel/FileSystemDatabaseManager.h
new file mode 100644
index 0000000000..b7a4e352fe
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.h
@@ -0,0 +1,229 @@
+/* -*- 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_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGER_H_
+#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGER_H_
+
+#include "ResultConnection.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "mozilla/dom/quota/UsageInfo.h"
+#include "nsStringFwd.h"
+
+template <class T>
+class nsCOMPtr;
+
+class nsIFile;
+
+namespace mozilla {
+
+template <typename V, typename E>
+class Result;
+
+namespace dom {
+
+namespace quota {
+
+struct OriginMetadata;
+
+} // namespace quota
+
+namespace fs {
+
+struct FileId;
+enum class FileMode;
+class FileSystemChildMetadata;
+class FileSystemEntryMetadata;
+class FileSystemDirectoryListing;
+class FileSystemEntryPair;
+
+namespace data {
+
+using FileSystemConnection = fs::ResultConnection;
+
+class FileSystemDatabaseManager {
+ public:
+ /**
+ * @brief Updates stored usage data for all tracked files.
+ *
+ * @return nsresult error code
+ */
+ static nsresult RescanUsages(const ResultConnection& aConnection,
+ const quota::OriginMetadata& aOriginMetadata);
+
+ /**
+ * @brief Obtains the current total usage for origin and connection.
+ *
+ * @return Result<quota::UsageInfo, QMResult> On success,
+ * - field UsageInfo::DatabaseUsage contains the sum of current
+ * total database and file usage,
+ * - field UsageInfo::FileUsage is not used and should be equal to Nothing.
+ *
+ * If the disk is inaccessible, various IO related errors may be returned.
+ */
+ static Result<quota::UsageInfo, QMResult> GetUsage(
+ const ResultConnection& aConnection,
+ const quota::OriginMetadata& aOriginMetadata);
+
+ /**
+ * @brief Refreshes the stored file size.
+ *
+ * @param aEntry EntryId of the file whose size is refreshed.
+ */
+ virtual nsresult UpdateUsage(const FileId& aFileId) = 0;
+
+ /**
+ * @brief Returns directory identifier, optionally creating it if it doesn't
+ * exist
+ *
+ * @param aHandle Current directory and filename
+ * @return Result<bool, QMResult> Directory identifier or error
+ */
+ virtual Result<EntryId, QMResult> GetOrCreateDirectory(
+ const FileSystemChildMetadata& aHandle, bool aCreate) = 0;
+
+ /**
+ * @brief Returns file identifier, optionally creating it if it doesn't exist
+ *
+ * @param aHandle Current directory and filename
+ * @param aType Content type which is ignored if the file already exists
+ * @param aCreate true if file is to be created when it does not already exist
+ * @return Result<bool, QMResult> File identifier or error
+ */
+ virtual Result<EntryId, QMResult> GetOrCreateFile(
+ const FileSystemChildMetadata& aHandle, bool aCreate) = 0;
+
+ /**
+ * @brief Returns the properties of a file corresponding to a file handle
+ */
+ virtual nsresult GetFile(const EntryId& aEntryId, const FileId& aFileId,
+ const FileMode& aMode, ContentType& aType,
+ TimeStamp& lastModifiedMilliSeconds, Path& aPath,
+ nsCOMPtr<nsIFile>& aFile) const = 0;
+
+ virtual Result<FileSystemDirectoryListing, QMResult> GetDirectoryEntries(
+ const EntryId& aParent, PageNumber aPage) const = 0;
+
+ /**
+ * @brief Removes a directory
+ *
+ * @param aHandle Current directory and filename
+ * @return Result<bool, QMResult> False if file did not exist, otherwise true
+ * or error
+ */
+ virtual Result<bool, QMResult> RemoveDirectory(
+ const FileSystemChildMetadata& aHandle, bool aRecursive) = 0;
+
+ /**
+ * @brief Removes a file
+ *
+ * @param aHandle Current directory and filename
+ * @return Result<bool, QMResult> False if file did not exist, otherwise true
+ * or error
+ */
+ virtual Result<bool, QMResult> RemoveFile(
+ const FileSystemChildMetadata& aHandle) = 0;
+
+ /**
+ * @brief Rename a file/directory
+ *
+ * @param aHandle Source directory or file
+ * @param aNewName New entry name
+ * @return Result<EntryId, QMResult> The relevant entry id or error
+ */
+ virtual Result<EntryId, QMResult> RenameEntry(
+ const FileSystemEntryMetadata& aHandle, const Name& aNewName) = 0;
+
+ /**
+ * @brief Move a file/directory
+ *
+ * @param aHandle Source directory or file
+ * @param aNewDesignation Destination directory and entry name
+ * @return Result<EntryId, QMResult> The relevant entry id or error
+ */
+ virtual Result<EntryId, QMResult> MoveEntry(
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation) = 0;
+
+ /**
+ * @brief Tries to connect a parent directory to a file system item with a
+ * path, excluding the parent directory
+ *
+ * @param aHandle Pair of parent directory and child item candidates
+ * @return Result<Path, QMResult> Path or error if no it didn't exists
+ */
+ virtual Result<Path, QMResult> Resolve(
+ const FileSystemEntryPair& aEndpoints) const = 0;
+
+ /**
+ * @brief Generates an EntryId for a given parent EntryId and filename.
+ */
+ virtual Result<EntryId, QMResult> GetEntryId(
+ const FileSystemChildMetadata& aHandle) const = 0;
+
+ /**
+ * @brief To check if a file under a directory is locked, we need to map
+ * fileId's to entries.
+ *
+ * @param aFileId a FileId
+ * @return Result<EntryId, QMResult> Entry id of a temporary or main file
+ */
+ virtual Result<EntryId, QMResult> GetEntryId(const FileId& aFileId) const = 0;
+
+ /**
+ * @brief Make sure EntryId maps to a FileId. This method should be called
+ * before exclusive locking is attempted.
+ */
+ virtual Result<FileId, QMResult> EnsureFileId(const EntryId& aEntryId) = 0;
+
+ /**
+ * @brief Make sure EntryId maps to a temporary FileId. This method should be
+ * called before shared locking is attempted.
+ */
+ virtual Result<FileId, QMResult> EnsureTemporaryFileId(
+ const EntryId& aEntryId) = 0;
+
+ /**
+ * @brief To support moves in metadata, the actual files on disk are tagged
+ * with file id's which are mapped to entry id's which represent paths.
+ * This function returns the main file corresponding to an entry.
+ *
+ * @param aEntryId An id of an entry
+ * @return Result<EntryId, QMResult> Main file id, used by exclusive locks
+ */
+ virtual Result<FileId, QMResult> GetFileId(const EntryId& aEntryId) const = 0;
+
+ /**
+ * @brief Flag aFileId as the main file for aEntryId or abort. Removes the
+ * file which did not get flagged as the main file.
+ */
+ virtual nsresult MergeFileId(const EntryId& aEntryId, const FileId& aFileId,
+ bool aAbort) = 0;
+
+ /**
+ * @brief Close database connection.
+ */
+ virtual void Close() = 0;
+
+ /**
+ * @brief Start tracking file's usage.
+ */
+ virtual nsresult BeginUsageTracking(const FileId& aFileId) = 0;
+
+ /**
+ * @brief Stop tracking file's usage.
+ */
+ virtual nsresult EndUsageTracking(const FileId& aFileId) = 0;
+
+ virtual ~FileSystemDatabaseManager() = default;
+};
+
+} // namespace data
+} // namespace fs
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGER_H_
diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp
new file mode 100644
index 0000000000..c65bf01508
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp
@@ -0,0 +1,1567 @@
+/* -*- 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 "FileSystemDatabaseManagerVersion001.h"
+
+#include "ErrorList.h"
+#include "FileSystemContentTypeGuess.h"
+#include "FileSystemDataManager.h"
+#include "FileSystemFileManager.h"
+#include "FileSystemParentTypes.h"
+#include "ResultStatement.h"
+#include "StartedTransaction.h"
+#include "mozStorageHelper.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/dom/FileSystemDataManager.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/quota/Client.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/QuotaObject.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsString.h"
+
+namespace mozilla::dom {
+
+using FileSystemEntries = nsTArray<fs::FileSystemEntryMetadata>;
+
+namespace fs::data {
+
+namespace {
+
+constexpr const nsLiteralCString gDescendantsQuery =
+ "WITH RECURSIVE traceChildren(handle, parent) AS ( "
+ "SELECT handle, parent "
+ "FROM Entries "
+ "WHERE handle=:handle "
+ "UNION "
+ "SELECT Entries.handle, Entries.parent FROM traceChildren, Entries "
+ "WHERE traceChildren.handle=Entries.parent ) "
+ "SELECT handle "
+ "FROM traceChildren INNER JOIN Files "
+ "USING(handle) "
+ ";"_ns;
+
+Result<bool, QMResult> IsDirectoryEmpty(const FileSystemConnection& mConnection,
+ const EntryId& aEntryId) {
+ const nsLiteralCString isDirEmptyQuery =
+ "SELECT EXISTS ("
+ "SELECT 1 FROM Entries WHERE parent = :parent "
+ ");"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, isDirEmptyQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aEntryId)));
+ QM_TRY_UNWRAP(bool childrenExist, stmt.YesOrNoQuery());
+
+ return !childrenExist;
+}
+
+Result<bool, QMResult> DoesDirectoryExist(
+ const FileSystemConnection& mConnection,
+ const FileSystemChildMetadata& aHandle) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ const nsCString existsQuery =
+ "SELECT EXISTS "
+ "(SELECT 1 FROM Directories INNER JOIN Entries USING (handle) "
+ "WHERE Directories.name = :name AND Entries.parent = :parent ) "
+ ";"_ns;
+
+ QM_TRY_RETURN(ApplyEntryExistsQuery(mConnection, existsQuery, aHandle));
+}
+
+Result<bool, QMResult> DoesDirectoryExist(
+ const FileSystemConnection& mConnection, const EntryId& aEntry) {
+ MOZ_ASSERT(!aEntry.IsEmpty());
+
+ const nsCString existsQuery =
+ "SELECT EXISTS "
+ "(SELECT 1 FROM Directories WHERE handle = :handle ) "
+ ";"_ns;
+
+ QM_TRY_RETURN(ApplyEntryExistsQuery(mConnection, existsQuery, aEntry));
+}
+
+Result<bool, QMResult> IsAncestor(const FileSystemConnection& aConnection,
+ const FileSystemEntryPair& aEndpoints) {
+ const nsCString pathQuery =
+ "WITH RECURSIVE followPath(handle, parent) AS ( "
+ "SELECT handle, parent "
+ "FROM Entries "
+ "WHERE handle=:entryId "
+ "UNION "
+ "SELECT Entries.handle, Entries.parent FROM followPath, Entries "
+ "WHERE followPath.parent=Entries.handle ) "
+ "SELECT EXISTS "
+ "(SELECT 1 FROM followPath "
+ "WHERE handle=:possibleAncestor ) "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, pathQuery));
+ QM_TRY(
+ QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEndpoints.childId())));
+ QM_TRY(QM_TO_RESULT(
+ stmt.BindEntryIdByName("possibleAncestor"_ns, aEndpoints.parentId())));
+
+ return stmt.YesOrNoQuery();
+}
+
+Result<bool, QMResult> DoesFileExist(const FileSystemConnection& aConnection,
+ const FileSystemChildMetadata& aHandle) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ const nsCString existsQuery =
+ "SELECT EXISTS "
+ "(SELECT 1 FROM Files INNER JOIN Entries USING (handle) "
+ "WHERE Files.name = :name AND Entries.parent = :parent ) "
+ ";"_ns;
+
+ QM_TRY_RETURN(ApplyEntryExistsQuery(aConnection, existsQuery, aHandle));
+}
+
+nsresult GetEntries(const FileSystemConnection& aConnection,
+ const nsACString& aUnboundStmt, const EntryId& aParent,
+ PageNumber aPage, bool aDirectory,
+ FileSystemEntries& aEntries) {
+ // The entries inside a directory are sent to the child process in batches
+ // of pageSize items. Large value ensures that iteration is less often delayed
+ // by IPC messaging and querying the database.
+ // TODO: The current value 1024 is not optimized.
+ // TODO: Value "pageSize" is shared with the iterator implementation and
+ // should be defined in a common place.
+ const int32_t pageSize = 1024;
+
+ QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(aConnection, aParent));
+ if (!exists) {
+ return NS_ERROR_DOM_NOT_FOUND_ERR;
+ }
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, aUnboundStmt));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aParent)));
+ QM_TRY(QM_TO_RESULT(stmt.BindPageNumberByName("pageSize"_ns, pageSize)));
+ QM_TRY(QM_TO_RESULT(
+ stmt.BindPageNumberByName("pageOffset"_ns, aPage * pageSize)));
+
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ while (moreResults) {
+ QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 0u));
+ QM_TRY_UNWRAP(Name entryName, stmt.GetNameByColumn(/* Column */ 1u));
+
+ FileSystemEntryMetadata metadata(entryId, entryName, aDirectory);
+ aEntries.AppendElement(metadata);
+
+ QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep());
+ }
+
+ return NS_OK;
+}
+
+Result<EntryId, QMResult> GetUniqueEntryId(
+ const FileSystemConnection& aConnection,
+ const FileSystemChildMetadata& aHandle) {
+ const nsCString existsQuery =
+ "SELECT EXISTS "
+ "(SELECT 1 FROM Entries "
+ "WHERE handle = :handle )"
+ ";"_ns;
+
+ FileSystemChildMetadata generatorInput = aHandle;
+
+ const size_t maxRounds = 1024u;
+
+ for (size_t hangGuard = 0u; hangGuard < maxRounds; ++hangGuard) {
+ QM_TRY_UNWRAP(EntryId entryId, fs::data::GetEntryHandle(generatorInput));
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, existsQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+
+ QM_TRY_UNWRAP(bool alreadyInUse, stmt.YesOrNoQuery());
+
+ if (!alreadyInUse) {
+ return entryId;
+ }
+
+ generatorInput.parentId() = entryId;
+ }
+
+ return Err(QMResult(NS_ERROR_UNEXPECTED));
+}
+
+nsresult PerformRename(const FileSystemConnection& aConnection,
+ const FileSystemEntryMetadata& aHandle,
+ const Name& aNewName, const ContentType& aNewType,
+ const nsLiteralCString& aNameUpdateQuery) {
+ MOZ_ASSERT(!aHandle.entryId().IsEmpty());
+ MOZ_ASSERT(IsValidName(aHandle.entryName()));
+
+ // same-name is checked in RenameEntry()
+ if (!IsValidName(aNewName)) {
+ return NS_ERROR_DOM_TYPE_MISMATCH_ERR;
+ }
+
+ // TODO: This should fail when handle doesn't exist - the
+ // explicit file or directory existence queries are redundant
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, aNameUpdateQuery)
+ .mapErr(toNSResult));
+ if (!aNewType.IsVoid()) {
+ QM_TRY(MOZ_TO_RESULT(stmt.BindContentTypeByName("type"_ns, aNewType)));
+ }
+ QM_TRY(MOZ_TO_RESULT(stmt.BindNameByName("name"_ns, aNewName)));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aHandle.entryId())));
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+
+ return NS_OK;
+}
+
+nsresult PerformRenameDirectory(const FileSystemConnection& aConnection,
+ const FileSystemEntryMetadata& aHandle,
+ const Name& aNewName) {
+ const nsLiteralCString updateDirectoryNameQuery =
+ "UPDATE Directories "
+ "SET name = :name "
+ "WHERE handle = :handle "
+ ";"_ns;
+
+ return PerformRename(aConnection, aHandle, aNewName, VoidCString(),
+ updateDirectoryNameQuery);
+}
+
+nsresult PerformRenameFile(const FileSystemConnection& aConnection,
+ const FileSystemEntryMetadata& aHandle,
+ const Name& aNewName, const ContentType& aNewType) {
+ const nsLiteralCString updateFileTypeAndNameQuery =
+ "UPDATE Files SET type = :type, name = :name "
+ "WHERE handle = :handle ;"_ns;
+
+ const nsLiteralCString updateFileNameQuery =
+ "UPDATE Files SET name = :name WHERE handle = :handle ;"_ns;
+
+ if (aNewType.IsVoid()) {
+ return PerformRename(aConnection, aHandle, aNewName, aNewType,
+ updateFileNameQuery);
+ }
+
+ return PerformRename(aConnection, aHandle, aNewName, aNewType,
+ updateFileTypeAndNameQuery);
+}
+
+template <class HandlerType>
+nsresult SetUsageTrackingImpl(const FileSystemConnection& aConnection,
+ const FileId& aFileId, bool aTracked,
+ HandlerType&& aOnMissingFile) {
+ const nsLiteralCString setTrackedQuery =
+ "INSERT INTO Usages "
+ "( handle, tracked ) "
+ "VALUES "
+ "( :handle, :tracked ) "
+ "ON CONFLICT(handle) DO "
+ "UPDATE SET tracked = excluded.tracked "
+ ";"_ns;
+
+ const nsresult customReturnValue =
+ aTracked ? NS_ERROR_DOM_NOT_FOUND_ERR : NS_OK;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, setTrackedQuery));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId)));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindBooleanByName("tracked"_ns, aTracked)));
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()), customReturnValue,
+ std::forward<HandlerType>(aOnMissingFile));
+
+ return NS_OK;
+}
+
+Result<nsTArray<FileId>, QMResult> GetTrackedFiles(
+ const FileSystemConnection& aConnection) {
+ // The same query works for both 001 and 002 schemas because handle is
+ // an entry id and later on a file id, respectively.
+ static const nsLiteralCString getTrackedFilesQuery =
+ "SELECT handle FROM Usages WHERE tracked = TRUE;"_ns;
+ nsTArray<FileId> trackedFiles;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, getTrackedFilesQuery));
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ while (moreResults) {
+ QM_TRY_UNWRAP(FileId fileId, stmt.GetFileIdByColumn(/* Column */ 0u));
+
+ trackedFiles.AppendElement(fileId); // TODO: fallible?
+
+ QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep());
+ }
+
+ return trackedFiles;
+}
+
+/** This handles the file not found error by assigning 0 usage to the dangling
+ * handle and puts the handle to a non-tracked state. Otherwise, when the
+ * file or database cannot be reached, the file remains in the tracked state.
+ */
+template <class QuotaCacheUpdate>
+nsresult UpdateUsageForFileEntry(const FileSystemConnection& aConnection,
+ const FileSystemFileManager& aFileManager,
+ const FileId& aFileId,
+ const nsLiteralCString& aUpdateQuery,
+ QuotaCacheUpdate&& aUpdateCache) {
+ QM_TRY_INSPECT(const auto& fileHandle, aFileManager.GetFile(aFileId));
+
+ // A file could have changed in a way which doesn't allow to read its size.
+ QM_TRY_UNWRAP(
+ const Usage fileSize,
+ QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT_INVOKE_MEMBER(fileHandle, GetFileSize),
+ // Predicate.
+ ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }),
+ // Fallback. If the file does no longer exist, treat it as 0-sized.
+ ErrToDefaultOk<Usage>));
+
+ QM_TRY(MOZ_TO_RESULT(aUpdateCache(fileSize)));
+
+ // No transaction as one statement succeeds or fails atomically
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, aUpdateQuery));
+
+ QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt.BindUsageByName("usage"_ns, fileSize)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+
+ return NS_OK;
+}
+
+nsresult UpdateUsageUnsetTracked(const FileSystemConnection& aConnection,
+ const FileSystemFileManager& aFileManager,
+ const FileId& aFileId) {
+ static const nsLiteralCString updateUsagesUnsetTrackedQuery =
+ "UPDATE Usages SET usage = :usage, tracked = FALSE "
+ "WHERE handle = :handle;"_ns;
+
+ auto noCacheUpdateNeeded = [](auto) { return NS_OK; };
+
+ return UpdateUsageForFileEntry(aConnection, aFileManager, aFileId,
+ updateUsagesUnsetTrackedQuery,
+ std::move(noCacheUpdateNeeded));
+}
+
+/**
+ * @brief Get the recorded usage only if the file is in tracked state.
+ * During origin initialization, if the usage on disk is unreadable, the latest
+ * recorded usage is reported to the quota manager for the tracked files.
+ * To allow writing, we attempt to update the real usage with one database and
+ * one file size query.
+ */
+Result<Maybe<Usage>, QMResult> GetMaybeTrackedUsage(
+ const FileSystemConnection& aConnection, const FileId& aFileId) {
+ const nsLiteralCString trackedUsageQuery =
+ "SELECT usage FROM Usages WHERE tracked = TRUE AND handle = :handle "
+ ");"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, trackedUsageQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId)));
+
+ QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep());
+ if (!moreResults) {
+ return Maybe<Usage>(Nothing());
+ }
+
+ QM_TRY_UNWRAP(Usage trackedUsage, stmt.GetUsageByColumn(/* Column */ 0u));
+
+ return Some(trackedUsage);
+}
+
+Result<bool, nsresult> ScanTrackedFiles(
+ const FileSystemConnection& aConnection,
+ const FileSystemFileManager& aFileManager) {
+ QM_TRY_INSPECT(const nsTArray<FileId>& trackedFiles,
+ GetTrackedFiles(aConnection).mapErr(toNSResult));
+
+ bool ok = true;
+ for (const auto& fileId : trackedFiles) {
+ // On success, tracked is set to false, otherwise its value is kept (= true)
+ QM_WARNONLY_TRY(MOZ_TO_RESULT(UpdateUsageUnsetTracked(
+ aConnection, aFileManager, fileId)),
+ [&ok](const auto& /*aRv*/) { ok = false; });
+ }
+
+ return ok;
+}
+
+Result<Ok, QMResult> DeleteEntry(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId) {
+ // If it's a directory, deleting the handle will cascade
+ const nsLiteralCString deleteEntryQuery =
+ "DELETE FROM Entries "
+ "WHERE handle = :handle "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, deleteEntryQuery));
+
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId)));
+
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+
+ return Ok{};
+}
+
+Result<int32_t, QMResult> GetTrackedFilesCount(
+ const FileSystemConnection& aConnection) {
+ // TODO: We could query the count directly
+ QM_TRY_INSPECT(const auto& trackedFiles, GetTrackedFiles(aConnection));
+
+ CheckedInt32 checkedFileCount = trackedFiles.Length();
+ QM_TRY(OkIf(checkedFileCount.isValid()),
+ Err(QMResult(NS_ERROR_ILLEGAL_VALUE)));
+
+ return checkedFileCount.value();
+}
+
+void LogWithFilename(const FileSystemFileManager& aFileManager,
+ const char* aFormat, const FileId& aFileId) {
+ if (!LOG_ENABLED()) {
+ return;
+ }
+
+ QM_TRY_INSPECT(const auto& localFile, aFileManager.GetFile(aFileId), QM_VOID);
+
+ nsAutoString localPath;
+ QM_TRY(MOZ_TO_RESULT(localFile->GetPath(localPath)), QM_VOID);
+ LOG((aFormat, NS_ConvertUTF16toUTF8(localPath).get()));
+}
+
+Result<bool, QMResult> IsAnyDescendantLocked(
+ const FileSystemConnection& aConnection,
+ const FileSystemDataManager& aDataManager, const EntryId& aEntryId) {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, gDescendantsQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId)));
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ while (moreResults) {
+ // Works only for version 001
+ QM_TRY_INSPECT(const EntryId& entryId,
+ stmt.GetEntryIdByColumn(/* Column */ 0u));
+
+ QM_TRY_UNWRAP(const bool isLocked, aDataManager.IsLocked(entryId), true);
+ if (isLocked) {
+ return true;
+ }
+
+ QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep());
+ }
+
+ return false;
+}
+
+} // namespace
+
+FileSystemDatabaseManagerVersion001::FileSystemDatabaseManagerVersion001(
+ FileSystemDataManager* aDataManager, FileSystemConnection&& aConnection,
+ UniquePtr<FileSystemFileManager>&& aFileManager, const EntryId& aRootEntry)
+ : mDataManager(aDataManager),
+ mConnection(aConnection),
+ mFileManager(std::move(aFileManager)),
+ mRootEntry(aRootEntry),
+ mClientMetadata(aDataManager->OriginMetadataRef(),
+ quota::Client::FILESYSTEM),
+ mFilesOfUnknownUsage(-1) {}
+
+/* static */
+nsresult FileSystemDatabaseManagerVersion001::RescanTrackedUsages(
+ const FileSystemConnection& aConnection,
+ const quota::OriginMetadata& aOriginMetadata) {
+ QM_TRY_UNWRAP(UniquePtr<FileSystemFileManager> fileManager,
+ data::FileSystemFileManager::CreateFileSystemFileManager(
+ aOriginMetadata));
+
+ QM_TRY_UNWRAP(bool ok, ScanTrackedFiles(aConnection, *fileManager));
+ if (ok) {
+ return NS_OK;
+ }
+
+ // Retry once without explicit delay
+ QM_TRY_UNWRAP(ok, ScanTrackedFiles(aConnection, *fileManager));
+ if (!ok) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return NS_OK;
+}
+
+/* static */
+Result<Usage, QMResult> FileSystemDatabaseManagerVersion001::GetFileUsage(
+ const FileSystemConnection& aConnection) {
+ const nsLiteralCString sumUsagesQuery = "SELECT sum(usage) FROM Usages;"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, sumUsagesQuery));
+
+ QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep());
+ if (!moreResults) {
+ return Err(QMResult(NS_ERROR_DOM_FILE_NOT_READABLE_ERR));
+ }
+
+ QM_TRY_UNWRAP(Usage totalFiles, stmt.GetUsageByColumn(/* Column */ 0u));
+
+ return totalFiles;
+}
+
+nsresult FileSystemDatabaseManagerVersion001::UpdateUsage(
+ const FileId& aFileId) {
+ // We don't track directories or non-existent files.
+ QM_TRY_UNWRAP(bool fileExists, DoesFileIdExist(aFileId).mapErr(toNSResult));
+ if (!fileExists) {
+ return NS_OK; // May be deleted before update, no assert
+ }
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> file, mFileManager->GetFile(aFileId));
+ MOZ_ASSERT(file);
+
+ Usage fileSize = 0;
+ bool exists = false;
+ QM_TRY(MOZ_TO_RESULT(file->Exists(&exists)));
+ if (exists) {
+ QM_TRY(MOZ_TO_RESULT(file->GetFileSize(&fileSize)));
+ }
+
+ QM_TRY(MOZ_TO_RESULT(UpdateUsageInDatabase(aFileId, fileSize)));
+
+ return NS_OK;
+}
+
+Result<EntryId, QMResult>
+FileSystemDatabaseManagerVersion001::GetOrCreateDirectory(
+ const FileSystemChildMetadata& aHandle, bool aCreate) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ const auto& name = aHandle.childName();
+ // Belt and suspenders: check here as well as in child.
+ if (!IsValidName(name)) {
+ return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR));
+ }
+ MOZ_ASSERT(!(name.IsVoid() || name.IsEmpty()));
+
+ bool exists = true;
+ QM_TRY_UNWRAP(exists, DoesFileExist(mConnection, aHandle));
+
+ // By spec, we don't allow a file and a directory
+ // to have the same name and parent
+ if (exists) {
+ return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR));
+ }
+
+ QM_TRY_UNWRAP(exists, DoesDirectoryExist(mConnection, aHandle));
+
+ // exists as directory
+ if (exists) {
+ return FindEntryId(mConnection, aHandle, false);
+ }
+
+ if (!aCreate) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ const nsLiteralCString insertEntryQuery =
+ "INSERT OR IGNORE INTO Entries "
+ "( handle, parent ) "
+ "VALUES "
+ "( :handle, :parent ) "
+ ";"_ns;
+
+ const nsLiteralCString insertDirectoryQuery =
+ "INSERT OR IGNORE INTO Directories "
+ "( handle, name ) "
+ "VALUES "
+ "( :handle, :name ) "
+ ";"_ns;
+
+ QM_TRY_INSPECT(const EntryId& entryId, GetEntryId(aHandle));
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection));
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, insertEntryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(
+ QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, insertDirectoryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, name)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ QM_TRY_UNWRAP(DebugOnly<bool> doesItExistNow,
+ DoesDirectoryExist(mConnection, aHandle));
+ MOZ_ASSERT(doesItExistNow);
+
+ return entryId;
+}
+
+Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::GetOrCreateFile(
+ const FileSystemChildMetadata& aHandle, bool aCreate) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ const auto& name = aHandle.childName();
+ // Belt and suspenders: check here as well as in child.
+ if (!IsValidName(name)) {
+ return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR));
+ }
+ MOZ_ASSERT(!(name.IsVoid() || name.IsEmpty()));
+
+ QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(mConnection, aHandle));
+
+ // By spec, we don't allow a file and a directory
+ // to have the same name and parent
+ QM_TRY(OkIf(!exists), Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR)));
+
+ QM_TRY_UNWRAP(exists, DoesFileExist(mConnection, aHandle));
+
+ if (exists) {
+ QM_TRY_RETURN(FindEntryId(mConnection, aHandle, /* aIsFile */ true));
+ }
+
+ if (!aCreate) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ const nsLiteralCString insertEntryQuery =
+ "INSERT INTO Entries "
+ "( handle, parent ) "
+ "VALUES "
+ "( :handle, :parent ) "
+ ";"_ns;
+
+ const nsLiteralCString insertFileQuery =
+ "INSERT INTO Files "
+ "( handle, type, name ) "
+ "VALUES "
+ "( :handle, :type, :name ) "
+ ";"_ns;
+
+ QM_TRY_INSPECT(const EntryId& entryId, GetEntryId(aHandle));
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ const ContentType type = DetermineContentType(name);
+
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection));
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, insertEntryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(
+ QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()), QM_PROPAGATE,
+ ([this, &aHandle](const auto& aRv) {
+ QM_TRY_UNWRAP(bool parentExists,
+ DoesDirectoryExist(mConnection, aHandle.parentId()),
+ QM_VOID);
+ QM_TRY(OkIf(parentExists), QM_VOID);
+ }));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, insertFileQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(QM_TO_RESULT(stmt.BindContentTypeByName("type"_ns, type)));
+ QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, name)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return entryId;
+}
+
+nsresult FileSystemDatabaseManagerVersion001::GetFile(
+ const EntryId& aEntryId, const FileId& aFileId, const FileMode& aMode,
+ ContentType& aType, TimeStamp& lastModifiedMilliSeconds,
+ nsTArray<Name>& aPath, nsCOMPtr<nsIFile>& aFile) const {
+ MOZ_ASSERT(!aFileId.IsEmpty());
+ MOZ_ASSERT(aMode == FileMode::EXCLUSIVE);
+
+ const FileSystemEntryPair endPoints(mRootEntry, aEntryId);
+ QM_TRY_UNWRAP(aPath, ResolveReversedPath(mConnection, endPoints));
+ if (aPath.IsEmpty()) {
+ return NS_ERROR_DOM_NOT_FOUND_ERR;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(GetFileAttributes(mConnection, aEntryId, aType)));
+ QM_TRY_UNWRAP(aFile, mFileManager->GetOrCreateFile(aFileId));
+
+ PRTime lastModTime = 0;
+ QM_TRY(MOZ_TO_RESULT(aFile->GetLastModifiedTime(&lastModTime)));
+ lastModifiedMilliSeconds = static_cast<TimeStamp>(lastModTime);
+
+ aPath.Reverse();
+
+ return NS_OK;
+}
+
+Result<FileSystemDirectoryListing, QMResult>
+FileSystemDatabaseManagerVersion001::GetDirectoryEntries(
+ const EntryId& aParent, PageNumber aPage) const {
+ // TODO: Offset is reported to have bad performance - see Bug 1780386.
+ const nsCString directoriesQuery =
+ "SELECT Dirs.handle, Dirs.name "
+ "FROM Directories AS Dirs "
+ "INNER JOIN ( "
+ "SELECT handle "
+ "FROM Entries "
+ "WHERE parent = :parent "
+ "LIMIT :pageSize "
+ "OFFSET :pageOffset ) "
+ "AS Ents "
+ "ON Dirs.handle = Ents.handle "
+ ";"_ns;
+ const nsCString filesQuery =
+ "SELECT Files.handle, Files.name "
+ "FROM Files "
+ "INNER JOIN ( "
+ "SELECT handle "
+ "FROM Entries "
+ "WHERE parent = :parent "
+ "LIMIT :pageSize "
+ "OFFSET :pageOffset ) "
+ "AS Ents "
+ "ON Files.handle = Ents.handle "
+ ";"_ns;
+
+ FileSystemDirectoryListing entries;
+ QM_TRY(
+ QM_TO_RESULT(GetEntries(mConnection, directoriesQuery, aParent, aPage,
+ /* aDirectory */ true, entries.directories())));
+
+ QM_TRY(QM_TO_RESULT(GetEntries(mConnection, filesQuery, aParent, aPage,
+ /* aDirectory */ false, entries.files())));
+
+ return entries;
+}
+
+Result<bool, QMResult> FileSystemDatabaseManagerVersion001::RemoveDirectory(
+ const FileSystemChildMetadata& aHandle, bool aRecursive) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ if (aHandle.childName().IsEmpty()) {
+ return false;
+ }
+
+ DebugOnly<Name> name = aHandle.childName();
+ MOZ_ASSERT(!name.inspect().IsVoid());
+
+ QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(mConnection, aHandle));
+
+ if (!exists) {
+ return false;
+ }
+
+ // At this point, entry exists and is a directory.
+ QM_TRY_UNWRAP(EntryId entryId, FindEntryId(mConnection, aHandle, false));
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ QM_TRY_UNWRAP(bool isEmpty, IsDirectoryEmpty(mConnection, entryId));
+
+ MOZ_ASSERT(mDataManager);
+ QM_TRY_UNWRAP(const bool isLocked,
+ IsAnyDescendantLocked(mConnection, *mDataManager, entryId));
+
+ QM_TRY(OkIf(!isLocked),
+ Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR)));
+
+ if (!aRecursive && !isEmpty) {
+ return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR));
+ }
+
+ QM_TRY_UNWRAP(Usage usage, GetUsagesOfDescendants(entryId));
+
+ QM_TRY_INSPECT(const nsTArray<FileId>& descendants,
+ FindFilesUnderEntry(entryId));
+
+ nsTArray<FileId> failedRemovals;
+ QM_TRY_UNWRAP(DebugOnly<Usage> removedUsage,
+ mFileManager->RemoveFiles(descendants, failedRemovals));
+
+ // Usage is for the current main file but we remove temporary files too.
+ MOZ_ASSERT_IF(failedRemovals.IsEmpty() && (0 == mFilesOfUnknownUsage),
+ usage <= removedUsage);
+
+ TryRemoveDuringIdleMaintenance(failedRemovals);
+
+ auto isInFailedRemovals = [&failedRemovals](const auto& aFileId) {
+ return failedRemovals.cend() !=
+ std::find_if(failedRemovals.cbegin(), failedRemovals.cend(),
+ [&aFileId](const auto& aFailedRemoval) {
+ return aFileId == aFailedRemoval;
+ });
+ };
+
+ for (const auto& fileId : descendants) {
+ if (!isInFailedRemovals(fileId)) {
+ QM_WARNONLY_TRY(QM_TO_RESULT(RemoveFileId(fileId)));
+ }
+ }
+
+ if (usage > 0) { // Performance!
+ DecreaseCachedQuotaUsage(usage);
+ }
+
+ QM_TRY(DeleteEntry(mConnection, entryId));
+
+ return true;
+}
+
+Result<bool, QMResult> FileSystemDatabaseManagerVersion001::RemoveFile(
+ const FileSystemChildMetadata& aHandle) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ if (aHandle.childName().IsEmpty()) {
+ return false;
+ }
+
+ DebugOnly<Name> name = aHandle.childName();
+ MOZ_ASSERT(!name.inspect().IsVoid());
+
+ // Make it more evident that we won't remove directories
+ QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, aHandle));
+
+ if (!exists) {
+ return false;
+ }
+
+ // At this point, entry exists and is a file
+ QM_TRY_UNWRAP(EntryId entryId, FindEntryId(mConnection, aHandle, true));
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ // XXX This code assumes the spec question is resolved to state
+ // removing an in-use file should fail. If it shouldn't fail, we need to
+ // do something to neuter all the extant FileAccessHandles/WritableFileStreams
+ // that reference it
+ QM_TRY_UNWRAP(const bool isLocked, mDataManager->IsLocked(entryId));
+ if (isLocked) {
+ LOG(("Trying to remove in-use file"));
+ return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
+ }
+
+ QM_TRY_INSPECT(const nsTArray<FileId>& diskItems,
+ FindFilesUnderEntry(entryId));
+
+ QM_TRY_UNWRAP(Usage usage, GetUsagesOfDescendants(entryId));
+
+ nsTArray<FileId> failedRemovals;
+ QM_TRY_UNWRAP(DebugOnly<Usage> removedUsage,
+ mFileManager->RemoveFiles(diskItems, failedRemovals));
+
+ // We only check the most common case. This can fail spuriously if an external
+ // application writes to the file, or OS reports zero size due to corruption.
+ MOZ_ASSERT_IF(failedRemovals.IsEmpty() && (0 == mFilesOfUnknownUsage),
+ usage == removedUsage);
+
+ TryRemoveDuringIdleMaintenance(failedRemovals);
+
+ auto isInFailedRemovals = [&failedRemovals](const auto& aFileId) {
+ return failedRemovals.cend() !=
+ std::find_if(failedRemovals.cbegin(), failedRemovals.cend(),
+ [&aFileId](const auto& aFailedRemoval) {
+ return aFileId == aFailedRemoval;
+ });
+ };
+
+ for (const auto& fileId : diskItems) {
+ if (!isInFailedRemovals(fileId)) {
+ QM_WARNONLY_TRY(QM_TO_RESULT(RemoveFileId(fileId)));
+ }
+ }
+
+ if (usage > 0) { // Performance!
+ DecreaseCachedQuotaUsage(usage);
+ }
+
+ QM_TRY(DeleteEntry(mConnection, entryId));
+
+ return true;
+}
+
+Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::RenameEntry(
+ const FileSystemEntryMetadata& aHandle, const Name& aNewName) {
+ const auto& entryId = aHandle.entryId();
+
+ // Can't rename root
+ if (mRootEntry == entryId) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ // Verify the source exists
+ QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId),
+ Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)));
+
+ // Are we actually renaming?
+ if (aHandle.entryName() == aNewName) {
+ return entryId;
+ }
+
+ QM_TRY(QM_TO_RESULT(PrepareRenameEntry(mConnection, mDataManager, aHandle,
+ aNewName, isFile)));
+
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection));
+
+ if (isFile) {
+ const ContentType type = DetermineContentType(aNewName);
+ QM_TRY(
+ QM_TO_RESULT(PerformRenameFile(mConnection, aHandle, aNewName, type)));
+ } else {
+ QM_TRY(
+ QM_TO_RESULT(PerformRenameDirectory(mConnection, aHandle, aNewName)));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return entryId;
+}
+
+Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::MoveEntry(
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation) {
+ const auto& entryId = aHandle.entryId();
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ if (mRootEntry == entryId) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ // Verify the source exists
+ QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId),
+ Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)));
+
+ // If the rename doesn't change the name or directory, just return success.
+ // XXX Needs to be added to the spec
+ QM_WARNONLY_TRY_UNWRAP(Maybe<bool> maybeSame,
+ IsSame(mConnection, aHandle, aNewDesignation, isFile));
+ if (maybeSame && maybeSame.value()) {
+ return entryId;
+ }
+
+ QM_TRY(QM_TO_RESULT(PrepareMoveEntry(mConnection, mDataManager, aHandle,
+ aNewDesignation, isFile)));
+
+ const nsLiteralCString updateEntryParentQuery =
+ "UPDATE Entries "
+ "SET parent = :parent "
+ "WHERE handle = :handle "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection));
+
+ {
+ // We always change the parent because it's simpler than checking if the
+ // parent needs to be changed
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, updateEntryParentQuery));
+ QM_TRY(QM_TO_RESULT(
+ stmt.BindEntryIdByName("parent"_ns, aNewDesignation.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ const Name& newName = aNewDesignation.childName();
+
+ // Are we actually renaming?
+ if (aHandle.entryName() == newName) {
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return entryId;
+ }
+
+ if (isFile) {
+ const ContentType type = DetermineContentType(newName);
+ QM_TRY(
+ QM_TO_RESULT(PerformRenameFile(mConnection, aHandle, newName, type)));
+ } else {
+ QM_TRY(QM_TO_RESULT(PerformRenameDirectory(mConnection, aHandle, newName)));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return entryId;
+}
+
+Result<Path, QMResult> FileSystemDatabaseManagerVersion001::Resolve(
+ const FileSystemEntryPair& aEndpoints) const {
+ QM_TRY_UNWRAP(Path path, ResolveReversedPath(mConnection, aEndpoints));
+ // Note: if not an ancestor, returns null
+
+ path.Reverse();
+ return path;
+}
+
+Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::GetEntryId(
+ const FileSystemChildMetadata& aHandle) const {
+ return GetUniqueEntryId(mConnection, aHandle);
+}
+
+Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::GetEntryId(
+ const FileId& aFileId) const {
+ return aFileId.Value();
+}
+
+Result<FileId, QMResult> FileSystemDatabaseManagerVersion001::EnsureFileId(
+ const EntryId& aEntryId) {
+ return FileId(aEntryId);
+}
+
+Result<FileId, QMResult>
+FileSystemDatabaseManagerVersion001::EnsureTemporaryFileId(
+ const EntryId& aEntryId) {
+ return FileId(aEntryId);
+}
+
+Result<FileId, QMResult> FileSystemDatabaseManagerVersion001::GetFileId(
+ const EntryId& aEntryId) const {
+ return FileId(aEntryId);
+}
+
+nsresult FileSystemDatabaseManagerVersion001::MergeFileId(
+ const EntryId& /* aEntryId */, const FileId& /* aFileId */,
+ bool /* aAbort */) {
+ // Version 001 should always use exclusive mode and not get here.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+void FileSystemDatabaseManagerVersion001::Close() { mConnection->Close(); }
+
+nsresult FileSystemDatabaseManagerVersion001::BeginUsageTracking(
+ const FileId& aFileId) {
+ MOZ_ASSERT(!aFileId.IsEmpty());
+
+ // If file is already tracked but we cannot read its size, error.
+ // If file does not exist, this will succeed because usage is zero.
+ QM_TRY(EnsureUsageIsKnown(aFileId));
+
+ // If file does not exist, set usage tracking to true fails with
+ // file not found error.
+ QM_TRY(MOZ_TO_RESULT(SetUsageTracking(aFileId, true)));
+
+ return NS_OK;
+}
+
+nsresult FileSystemDatabaseManagerVersion001::EndUsageTracking(
+ const FileId& aFileId) {
+ // This is expected to fail only if database is unreachable.
+ QM_TRY(MOZ_TO_RESULT(SetUsageTracking(aFileId, false)));
+
+ return NS_OK;
+}
+
+Result<bool, QMResult> FileSystemDatabaseManagerVersion001::DoesFileIdExist(
+ const FileId& aFileId) const {
+ MOZ_ASSERT(!aFileId.IsEmpty());
+
+ QM_TRY_RETURN(DoesFileExist(mConnection, aFileId.Value()));
+}
+
+nsresult FileSystemDatabaseManagerVersion001::RemoveFileId(
+ const FileId& /* aFileId */) {
+ return NS_OK;
+}
+
+/**
+ * @brief Get the sum of usages for all file descendants of a directory entry.
+ * We obtain the value with one query, which is presumably better than having a
+ * separate query for each individual descendant.
+ * TODO: Check if this is true
+ *
+ * Please see GetFileUsage documentation for why we use the latest recorded
+ * value from the database instead of the file size property from the disk.
+ */
+Result<Usage, QMResult>
+FileSystemDatabaseManagerVersion001::GetUsagesOfDescendants(
+ const EntryId& aEntryId) const {
+ const nsLiteralCString descendantUsagesQuery =
+ "WITH RECURSIVE traceChildren(handle, parent) AS ( "
+ "SELECT handle, parent "
+ "FROM Entries "
+ "WHERE handle=:handle "
+ "UNION "
+ "SELECT Entries.handle, Entries.parent FROM traceChildren, Entries "
+ "WHERE traceChildren.handle=Entries.parent ) "
+ "SELECT sum(Usages.usage) "
+ "FROM traceChildren INNER JOIN Usages "
+ "USING(handle) "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, descendantUsagesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId)));
+ QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep());
+ if (!moreResults) {
+ return 0;
+ }
+
+ QM_TRY_RETURN(stmt.GetUsageByColumn(/* Column */ 0u));
+}
+
+Result<nsTArray<FileId>, QMResult>
+FileSystemDatabaseManagerVersion001::FindFilesUnderEntry(
+ const EntryId& aEntryId) const {
+ nsTArray<FileId> descendants;
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, gDescendantsQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId)));
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ while (moreResults) {
+ // Works only for version 001
+ QM_TRY_INSPECT(const FileId& fileId,
+ stmt.GetFileIdByColumn(/* Column */ 0u));
+
+ descendants.AppendElement(fileId);
+
+ QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep());
+ }
+ }
+
+ return descendants;
+}
+
+nsresult FileSystemDatabaseManagerVersion001::SetUsageTracking(
+ const FileId& aFileId, bool aTracked) {
+ auto onMissingFile = [this, &aFileId](const auto& aRv) {
+ // Usages constrains entryId to be present in Files
+ MOZ_ASSERT(NS_ERROR_STORAGE_CONSTRAINT == ToNSResult(aRv));
+
+ // The query *should* fail if and only if file does not exist
+ QM_TRY_UNWRAP(DebugOnly<bool> fileExists, DoesFileIdExist(aFileId),
+ QM_VOID);
+ MOZ_ASSERT(!fileExists);
+ };
+
+ return SetUsageTrackingImpl(mConnection, aFileId, aTracked, onMissingFile);
+}
+
+nsresult FileSystemDatabaseManagerVersion001::UpdateUsageInDatabase(
+ const FileId& aFileId, Usage aNewDiskUsage) {
+ const nsLiteralCString updateUsageQuery =
+ "INSERT INTO Usages "
+ "( handle, usage ) "
+ "VALUES "
+ "( :handle, :usage ) "
+ "ON CONFLICT(handle) DO "
+ "UPDATE SET usage = excluded.usage "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, updateUsageQuery));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindUsageByName("usage"_ns, aNewDiskUsage)));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId)));
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+
+ return NS_OK;
+}
+
+Result<Ok, QMResult> FileSystemDatabaseManagerVersion001::EnsureUsageIsKnown(
+ const FileId& aFileId) {
+ if (mFilesOfUnknownUsage < 0) { // Lazy initialization
+ QM_TRY_UNWRAP(mFilesOfUnknownUsage, GetTrackedFilesCount(mConnection));
+ }
+
+ if (mFilesOfUnknownUsage == 0) {
+ return Ok{};
+ }
+
+ QM_TRY_UNWRAP(Maybe<Usage> oldUsage,
+ GetMaybeTrackedUsage(mConnection, aFileId));
+ if (oldUsage.isNothing()) {
+ return Ok{}; // Usage is 0 or it was successfully recorded at unlocking.
+ }
+
+ auto quotaCacheUpdate = [this, &aFileId,
+ oldSize = oldUsage.value()](Usage aNewSize) {
+ return UpdateCachedQuotaUsage(aFileId, oldSize, aNewSize);
+ };
+
+ static const nsLiteralCString updateUsagesKeepTrackedQuery =
+ "UPDATE Usages SET usage = :usage WHERE handle = :handle;"_ns;
+
+ // If usage update fails, we log an error and keep things the way they were.
+ QM_TRY(QM_TO_RESULT(UpdateUsageForFileEntry(
+ mConnection, *mFileManager, aFileId, updateUsagesKeepTrackedQuery,
+ std::move(quotaCacheUpdate))),
+ Err(QMResult(NS_ERROR_DOM_FILE_NOT_READABLE_ERR)),
+ ([this, &aFileId](const auto& /*aRv*/) {
+ LogWithFilename(*mFileManager, "Could not read the size of file %s",
+ aFileId);
+ }));
+
+ // We read and updated the quota usage successfully.
+ --mFilesOfUnknownUsage;
+ MOZ_ASSERT(mFilesOfUnknownUsage >= 0);
+
+ return Ok{};
+}
+
+void FileSystemDatabaseManagerVersion001::DecreaseCachedQuotaUsage(
+ int64_t aDelta) {
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ quotaManager->DecreaseUsageForClient(mClientMetadata, aDelta);
+}
+
+nsresult FileSystemDatabaseManagerVersion001::UpdateCachedQuotaUsage(
+ const FileId& aFileId, Usage aOldUsage, Usage aNewUsage) {
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> fileObj,
+ mFileManager->GetFile(aFileId).mapErr(toNSResult));
+
+ RefPtr<quota::QuotaObject> quotaObject = quotaManager->GetQuotaObject(
+ quota::PERSISTENCE_TYPE_DEFAULT, mClientMetadata,
+ quota::Client::FILESYSTEM, fileObj, aOldUsage);
+ MOZ_ASSERT(quotaObject);
+
+ QM_TRY(OkIf(quotaObject->MaybeUpdateSize(aNewUsage, /* aTruncate */ true)),
+ NS_ERROR_FILE_NO_DEVICE_SPACE);
+
+ return NS_OK;
+}
+
+nsresult FileSystemDatabaseManagerVersion001::ClearDestinationIfNotLocked(
+ const FileSystemConnection& aConnection,
+ const FileSystemDataManager* const aDataManager,
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation) {
+ // If the destination file exists, fail explicitly. Spec author plans to
+ // revise the spec
+ QM_TRY_UNWRAP(bool exists, DoesFileExist(aConnection, aNewDesignation));
+ if (exists) {
+ QM_TRY_INSPECT(const EntryId& destId,
+ FindEntryId(aConnection, aNewDesignation, true));
+ QM_TRY_UNWRAP(const bool isLocked, aDataManager->IsLocked(destId));
+ if (isLocked) {
+ LOG(("Trying to overwrite in-use file"));
+ return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR;
+ }
+
+ QM_TRY_UNWRAP(DebugOnly<bool> isRemoved, RemoveFile(aNewDesignation));
+ MOZ_ASSERT(isRemoved);
+ } else {
+ QM_TRY_UNWRAP(exists, DoesDirectoryExist(aConnection, aNewDesignation));
+ if (exists) {
+ // Fails if directory contains locked files, otherwise total wipeout
+ QM_TRY_UNWRAP(DebugOnly<bool> isRemoved,
+ MOZ_TO_RESULT(RemoveDirectory(aNewDesignation,
+ /* recursive */ true)));
+ MOZ_ASSERT(isRemoved);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult FileSystemDatabaseManagerVersion001::PrepareRenameEntry(
+ const FileSystemConnection& aConnection,
+ const FileSystemDataManager* const aDataManager,
+ const FileSystemEntryMetadata& aHandle, const Name& aNewName,
+ bool aIsFile) {
+ const EntryId& entryId = aHandle.entryId();
+
+ // At this point, entry exists
+ if (aIsFile) {
+ QM_TRY_UNWRAP(const bool isLocked, aDataManager->IsLocked(entryId));
+ if (isLocked) {
+ LOG(("Trying to move in-use file"));
+ return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR;
+ }
+ }
+
+ // If the destination file exists, fail explicitly.
+ FileSystemChildMetadata destination;
+ QM_TRY_UNWRAP(EntryId parent, FindParent(mConnection, entryId));
+ destination.parentId() = parent;
+ destination.childName() = aNewName;
+
+ QM_TRY(MOZ_TO_RESULT(ClearDestinationIfNotLocked(mConnection, mDataManager,
+ aHandle, destination)));
+
+ return NS_OK;
+}
+
+nsresult FileSystemDatabaseManagerVersion001::PrepareMoveEntry(
+ const FileSystemConnection& aConnection,
+ const FileSystemDataManager* const aDataManager,
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation, bool aIsFile) {
+ const EntryId& entryId = aHandle.entryId();
+
+ // At this point, entry exists
+ if (aIsFile) {
+ QM_TRY_UNWRAP(const bool isLocked, aDataManager->IsLocked(entryId));
+ if (isLocked) {
+ LOG(("Trying to move in-use file"));
+ return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR;
+ }
+ }
+
+ QM_TRY(QM_TO_RESULT(ClearDestinationIfNotLocked(aConnection, aDataManager,
+ aHandle, aNewDesignation)));
+
+ // XXX: This should be before clearing the target
+
+ // To prevent cyclic paths, we check that there is no path from
+ // the item to be moved to the destination folder.
+ QM_TRY_UNWRAP(const bool isDestinationUnderSelf,
+ IsAncestor(aConnection, {entryId, aNewDesignation.parentId()}));
+ if (isDestinationUnderSelf) {
+ return NS_ERROR_DOM_INVALID_MODIFICATION_ERR;
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Free functions
+ */
+
+Result<bool, QMResult> ApplyEntryExistsQuery(
+ const FileSystemConnection& aConnection, const nsACString& aQuery,
+ const FileSystemChildMetadata& aHandle) {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, aQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, aHandle.childName())));
+
+ return stmt.YesOrNoQuery();
+}
+
+Result<bool, QMResult> ApplyEntryExistsQuery(
+ const FileSystemConnection& aConnection, const nsACString& aQuery,
+ const EntryId& aEntry) {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, aQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntry)));
+
+ return stmt.YesOrNoQuery();
+}
+
+Result<bool, QMResult> DoesFileExist(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId) {
+ MOZ_ASSERT(!aEntryId.IsEmpty());
+
+ const nsCString existsQuery =
+ "SELECT EXISTS "
+ "(SELECT 1 FROM Files WHERE handle = :handle ) "
+ ";"_ns;
+
+ QM_TRY_RETURN(ApplyEntryExistsQuery(aConnection, existsQuery, aEntryId));
+}
+
+Result<bool, QMResult> IsFile(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId) {
+ QM_TRY_UNWRAP(bool exists, DoesFileExist(aConnection, aEntryId));
+ if (exists) {
+ return true;
+ }
+
+ QM_TRY_UNWRAP(exists, DoesDirectoryExist(aConnection, aEntryId));
+ if (exists) {
+ return false;
+ }
+
+ // Doesn't exist
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+}
+
+Result<EntryId, QMResult> FindEntryId(const FileSystemConnection& aConnection,
+ const FileSystemChildMetadata& aHandle,
+ bool aIsFile) {
+ const nsCString aDirectoryQuery =
+ "SELECT Entries.handle FROM Directories "
+ "INNER JOIN Entries USING (handle) "
+ "WHERE Directories.name = :name AND Entries.parent = :parent "
+ ";"_ns;
+
+ const nsCString aFileQuery =
+ "SELECT Entries.handle FROM Files INNER JOIN Entries USING (handle) "
+ "WHERE Files.name = :name AND Entries.parent = :parent "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(
+ aConnection, aIsFile ? aFileQuery : aDirectoryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, aHandle.childName())));
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ if (!moreResults) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 0u));
+
+ return entryId;
+}
+
+Result<EntryId, QMResult> FindParent(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId) {
+ const nsCString aParentQuery =
+ "SELECT handle FROM Entries "
+ "WHERE handle IN ( "
+ "SELECT parent FROM Entries WHERE "
+ "handle = :entryId ) "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, aParentQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId)));
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ if (!moreResults) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ QM_TRY_UNWRAP(EntryId parentId, stmt.GetEntryIdByColumn(/* Column */ 0u));
+ return parentId;
+}
+
+Result<bool, QMResult> IsSame(const FileSystemConnection& aConnection,
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewHandle,
+ bool aIsFile) {
+ MOZ_ASSERT(!aNewHandle.parentId().IsEmpty());
+
+ // Typically aNewHandle does not exist which is not an error
+ QM_TRY_RETURN(QM_OR_ELSE_LOG_VERBOSE_IF(
+ // Expression.
+ FindEntryId(aConnection, aNewHandle, aIsFile)
+ .map([&aHandle](const EntryId& entryId) {
+ return entryId == aHandle.entryId();
+ }),
+ // Predicate.
+ IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>,
+ // Fallback.
+ ErrToOkFromQMResult<false>));
+}
+
+Result<Path, QMResult> ResolveReversedPath(
+ const FileSystemConnection& aConnection,
+ const FileSystemEntryPair& aEndpoints) {
+ const nsLiteralCString pathQuery =
+ "WITH RECURSIVE followPath(handle, parent) AS ( "
+ "SELECT handle, parent "
+ "FROM Entries "
+ "WHERE handle=:entryId "
+ "UNION "
+ "SELECT Entries.handle, Entries.parent FROM followPath, Entries "
+ "WHERE followPath.parent=Entries.handle ) "
+ "SELECT COALESCE(Directories.name, Files.name), handle "
+ "FROM followPath "
+ "LEFT JOIN Directories USING(handle) "
+ "LEFT JOIN Files USING(handle);"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, pathQuery));
+ QM_TRY(
+ QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEndpoints.childId())));
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ Path pathResult;
+ while (moreResults) {
+ QM_TRY_UNWRAP(Name entryName, stmt.GetNameByColumn(/* Column */ 0u));
+ QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 1u));
+
+ if (aEndpoints.parentId() == entryId) {
+ return pathResult;
+ }
+ pathResult.AppendElement(entryName);
+
+ QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep());
+ }
+
+ // Spec wants us to return 'null' for not-an-ancestor case
+ pathResult.Clear();
+ return pathResult;
+}
+
+nsresult GetFileAttributes(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId, ContentType& aType) {
+ const nsLiteralCString getFileLocation =
+ "SELECT type FROM Files INNER JOIN Entries USING(handle) "
+ "WHERE handle = :entryId "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, getFileLocation));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId)));
+ QM_TRY_UNWRAP(bool hasEntries, stmt.ExecuteStep());
+
+ // Type is an optional attribute
+ if (!hasEntries || stmt.IsNullByColumn(/* Column */ 0u)) {
+ return NS_OK;
+ }
+
+ QM_TRY_UNWRAP(aType, stmt.GetContentTypeByColumn(/* Column */ 0u));
+
+ return NS_OK;
+}
+
+// TODO: Implement idle maintenance
+void TryRemoveDuringIdleMaintenance(
+ const nsTArray<FileId>& /* aItemToRemove */) {
+ // Not implemented
+}
+
+ContentType DetermineContentType(const Name& aName) {
+ QM_TRY_UNWRAP(
+ auto typeResult,
+ QM_OR_ELSE_LOG_VERBOSE(
+ FileSystemContentTypeGuess::FromPath(aName),
+ ([](const auto& aRv) -> Result<ContentType, QMResult> {
+ const nsresult rv = ToNSResult(aRv);
+ switch (rv) {
+ case NS_ERROR_FAILURE: /* There is an unknown new extension. */
+ return ContentType(""_ns); /* We clear the old extension. */
+
+ case NS_ERROR_INVALID_ARG: /* The name is garbled. */
+ [[fallthrough]];
+ case NS_ERROR_NOT_AVAILABLE: /* There is no extension. */
+ return VoidCString(); /* We keep the old extension. */
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("Should never get here!");
+ return Err(aRv);
+ }
+ })),
+ ContentType(""_ns));
+
+ return typeResult;
+}
+
+} // namespace fs::data
+
+} // namespace mozilla::dom
diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h
new file mode 100644
index 0000000000..333c5af6c2
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h
@@ -0,0 +1,208 @@
+/* -*- 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_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_
+#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_
+
+#include "FileSystemDatabaseManager.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsString.h"
+
+namespace mozilla::dom::fs {
+
+struct FileId;
+
+namespace data {
+
+class FileSystemDataManager;
+class FileSystemFileManager;
+
+/**
+ * @brief Versioned implementation of database interface enables backwards
+ * support after the schema has changed. Version number 0 refers to
+ * uninitialized database, and versions after that are sequential upgrades.
+ *
+ * To change the schema to the next version x,
+ * - a new implementation FileSystemDatabaseManagerVersion00x is derived from
+ * the previous version and the required methods are overridden
+ * - a new apppropriate schema initialization class SchemaVersion00x is created
+ * or derived
+ * - the factory method of FileSystemDatabaseManager is extended to try to
+ * migrate the data from the previous version to version x, and to return
+ * FileSystemDatabaseManagerVersion00x implementation if the database version
+ * after the migrations is x
+ * - note that if the migration fails at some old version, the corresponding
+ * old implementation should be returned: this way the users whose migrations
+ * fail systematically due to hardware or other issues will not get locked out
+ */
+class FileSystemDatabaseManagerVersion001 : public FileSystemDatabaseManager {
+ public:
+ FileSystemDatabaseManagerVersion001(
+ FileSystemDataManager* aDataManager, FileSystemConnection&& aConnection,
+ UniquePtr<FileSystemFileManager>&& aFileManager,
+ const EntryId& aRootEntry);
+
+ /* Static to allow use by quota client without instantiation */
+ static nsresult RescanTrackedUsages(
+ const FileSystemConnection& aConnection,
+ const quota::OriginMetadata& aOriginMetadata);
+
+ /* Static to allow use by quota client without instantiation */
+ static Result<Usage, QMResult> GetFileUsage(
+ const FileSystemConnection& aConnection);
+
+ nsresult UpdateUsage(const FileId& aFileId) override;
+
+ Result<EntryId, QMResult> GetOrCreateDirectory(
+ const FileSystemChildMetadata& aHandle, bool aCreate) override;
+
+ Result<EntryId, QMResult> GetOrCreateFile(
+ const FileSystemChildMetadata& aHandle, bool aCreate) override;
+
+ nsresult GetFile(const EntryId& aEntryId, const FileId& aFileId,
+ const FileMode& aMode, ContentType& aType,
+ TimeStamp& lastModifiedMilliSeconds, Path& aPath,
+ nsCOMPtr<nsIFile>& aFile) const override;
+
+ Result<FileSystemDirectoryListing, QMResult> GetDirectoryEntries(
+ const EntryId& aParent, PageNumber aPage) const override;
+
+ Result<bool, QMResult> RemoveDirectory(const FileSystemChildMetadata& aHandle,
+ bool aRecursive) override;
+
+ Result<bool, QMResult> RemoveFile(
+ const FileSystemChildMetadata& aHandle) override;
+
+ Result<EntryId, QMResult> RenameEntry(const FileSystemEntryMetadata& aHandle,
+ const Name& aNewName) override;
+
+ Result<EntryId, QMResult> MoveEntry(
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation) override;
+
+ Result<Path, QMResult> Resolve(
+ const FileSystemEntryPair& aEndpoints) const override;
+
+ Result<EntryId, QMResult> GetEntryId(
+ const FileSystemChildMetadata& aHandle) const override;
+
+ Result<EntryId, QMResult> GetEntryId(const FileId& aFileId) const override;
+
+ Result<FileId, QMResult> EnsureFileId(const EntryId& aEntryId) override;
+
+ Result<FileId, QMResult> EnsureTemporaryFileId(
+ const EntryId& aEntryId) override;
+
+ Result<FileId, QMResult> GetFileId(const EntryId& aEntryId) const override;
+
+ nsresult MergeFileId(const EntryId& aEntryId, const FileId& aFileId,
+ bool aAbort) override;
+
+ void Close() override;
+
+ nsresult BeginUsageTracking(const FileId& aFileId) override;
+
+ nsresult EndUsageTracking(const FileId& aFileId) override;
+
+ virtual ~FileSystemDatabaseManagerVersion001() = default;
+
+ protected:
+ virtual Result<bool, QMResult> DoesFileIdExist(const FileId& aFileId) const;
+
+ virtual nsresult RemoveFileId(const FileId& aFileId);
+
+ virtual Result<Usage, QMResult> GetUsagesOfDescendants(
+ const EntryId& aEntryId) const;
+
+ virtual Result<nsTArray<FileId>, QMResult> FindFilesUnderEntry(
+ const EntryId& aEntryId) const;
+
+ nsresult SetUsageTracking(const FileId& aFileId, bool aTracked);
+
+ nsresult UpdateUsageInDatabase(const FileId& aFileId, Usage aNewDiskUsage);
+
+ Result<Ok, QMResult> EnsureUsageIsKnown(const FileId& aFileId);
+
+ void DecreaseCachedQuotaUsage(int64_t aDelta);
+
+ nsresult UpdateCachedQuotaUsage(const FileId& aFileId, Usage aOldUsage,
+ Usage aNewUsage);
+
+ nsresult ClearDestinationIfNotLocked(
+ const FileSystemConnection& aConnection,
+ const FileSystemDataManager* const aDataManager,
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation);
+
+ nsresult PrepareRenameEntry(const FileSystemConnection& aConnection,
+ const FileSystemDataManager* const aDataManager,
+ const FileSystemEntryMetadata& aHandle,
+ const Name& aNewName, bool aIsFile);
+
+ nsresult PrepareMoveEntry(const FileSystemConnection& aConnection,
+ const FileSystemDataManager* const aDataManager,
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation,
+ bool aIsFile);
+
+ // This is a raw pointer since we're owned by the FileSystemDataManager.
+ FileSystemDataManager* MOZ_NON_OWNING_REF mDataManager;
+
+ FileSystemConnection mConnection;
+
+ UniquePtr<FileSystemFileManager> mFileManager;
+
+ const EntryId mRootEntry;
+
+ const quota::ClientMetadata mClientMetadata;
+
+ int32_t mFilesOfUnknownUsage;
+};
+
+inline auto toNSResult = [](const auto& aRv) { return ToNSResult(aRv); };
+
+Result<bool, QMResult> ApplyEntryExistsQuery(
+ const FileSystemConnection& aConnection, const nsACString& aQuery,
+ const FileSystemChildMetadata& aHandle);
+
+Result<bool, QMResult> ApplyEntryExistsQuery(
+ const FileSystemConnection& aConnection, const nsACString& aQuery,
+ const EntryId& aEntry);
+
+Result<bool, QMResult> DoesFileExist(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId);
+
+Result<bool, QMResult> IsFile(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId);
+
+Result<EntryId, QMResult> FindEntryId(const FileSystemConnection& aConnection,
+ const FileSystemChildMetadata& aHandle,
+ bool aIsFile);
+
+Result<EntryId, QMResult> FindParent(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId);
+
+Result<bool, QMResult> IsSame(const FileSystemConnection& aConnection,
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewHandle,
+ bool aIsFile);
+
+Result<Path, QMResult> ResolveReversedPath(
+ const FileSystemConnection& aConnection,
+ const FileSystemEntryPair& aEndpoints);
+
+nsresult GetFileAttributes(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId, ContentType& aType);
+
+void TryRemoveDuringIdleMaintenance(const nsTArray<FileId>& aItemToRemove);
+
+ContentType DetermineContentType(const Name& aName);
+
+} // namespace data
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_
diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp
new file mode 100644
index 0000000000..3543346ff0
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp
@@ -0,0 +1,832 @@
+/* -*- 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 "FileSystemDatabaseManagerVersion002.h"
+
+#include "ErrorList.h"
+#include "FileSystemContentTypeGuess.h"
+#include "FileSystemDataManager.h"
+#include "FileSystemFileManager.h"
+#include "FileSystemHashSource.h"
+#include "FileSystemHashStorageFunction.h"
+#include "FileSystemParentTypes.h"
+#include "ResultStatement.h"
+#include "StartedTransaction.h"
+#include "mozStorageHelper.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/dom/FileSystemDataManager.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/QMResult.h"
+#include "mozilla/dom/quota/Client.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/QuotaObject.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+
+namespace mozilla::dom::fs::data {
+
+namespace {
+
+Result<FileId, QMResult> GetFileId002(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId) {
+ const nsLiteralCString fileIdQuery =
+ "SELECT fileId FROM MainFiles WHERE handle = :entryId ;"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, fileIdQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId)));
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ if (!moreResults) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ QM_TRY_INSPECT(const FileId& fileId, stmt.GetFileIdByColumn(/* Column */ 0u));
+
+ return fileId;
+}
+
+Result<bool, QMResult> DoesFileIdExist(const FileSystemConnection& aConnection,
+ const FileId& aFileId) {
+ MOZ_ASSERT(!aFileId.IsEmpty());
+
+ const nsLiteralCString existsQuery =
+ "SELECT EXISTS "
+ "(SELECT 1 FROM FileIds WHERE fileId = :handle ) "
+ ";"_ns;
+
+ QM_TRY_RETURN(
+ ApplyEntryExistsQuery(aConnection, existsQuery, aFileId.Value()));
+}
+
+nsresult RehashFile(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId,
+ const FileSystemChildMetadata& aNewDesignation,
+ const ContentType& aNewType) {
+ QM_TRY_INSPECT(const EntryId& newId,
+ FileSystemHashSource::GenerateHash(
+ aNewDesignation.parentId(), aNewDesignation.childName()));
+
+ // The destination should be empty at this point: either we exited because
+ // overwrite was not desired, or the existing content was removed.
+ const nsLiteralCString insertNewEntryQuery =
+ "INSERT INTO Entries ( handle, parent ) "
+ "VALUES ( :newId, :newParent ) "
+ ";"_ns;
+
+ const nsLiteralCString insertNewFileAndTypeQuery =
+ "INSERT INTO Files ( handle, type, name ) "
+ "VALUES ( :newId, :type, :newName ) "
+ ";"_ns;
+
+ const nsLiteralCString insertNewFileKeepTypeQuery =
+ "INSERT INTO Files ( handle, type, name ) "
+ "SELECT :newId, type, :newName FROM Files "
+ "WHERE handle = :oldId ;"_ns;
+
+ const auto& insertNewFileQuery = aNewType.IsVoid()
+ ? insertNewFileKeepTypeQuery
+ : insertNewFileAndTypeQuery;
+
+ const nsLiteralCString updateFileMappingsQuery =
+ "UPDATE FileIds SET handle = :newId WHERE handle = :handle ;"_ns;
+
+ const nsLiteralCString updateMainFilesQuery =
+ "UPDATE MainFiles SET handle = :newId WHERE handle = :handle ;"_ns;
+
+ const nsLiteralCString cleanupOldEntryQuery =
+ "DELETE FROM Entries WHERE handle = :handle ;"_ns;
+
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConnection));
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, insertNewEntryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("newId"_ns, newId)));
+ QM_TRY(QM_TO_RESULT(
+ stmt.BindEntryIdByName("newParent"_ns, aNewDesignation.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, insertNewFileQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("newId"_ns, newId)));
+ if (aNewType.IsVoid()) {
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("oldId"_ns, aEntryId)));
+ } else {
+ QM_TRY(QM_TO_RESULT(stmt.BindContentTypeByName("type"_ns, aNewType)));
+ }
+ QM_TRY(QM_TO_RESULT(
+ stmt.BindNameByName("newName"_ns, aNewDesignation.childName())));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(
+ ResultStatement stmt,
+ ResultStatement::Create(aConnection, updateFileMappingsQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("newId"_ns, newId)));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, updateMainFilesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("newId"_ns, newId)));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, cleanupOldEntryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return NS_OK;
+}
+
+nsresult RehashDirectory(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId,
+ const FileSystemChildMetadata& aNewDesignation) {
+ // This name won't match up with the entryId for the old path but
+ // it will be removed at the end
+ const nsLiteralCString updateNameQuery =
+ "UPDATE Directories SET name = :newName WHERE handle = :handle "
+ "; "_ns;
+
+ const nsLiteralCString calculateHashesQuery =
+ "CREATE TEMPORARY TABLE ParentChildHash AS "
+ "WITH RECURSIVE "
+ "rehashMap( depth, isFile, handle, parent, name, hash ) AS ( "
+ "SELECT 0, isFile, handle, parent, name, hashEntry( :newParent, name ) "
+ "FROM EntryNames WHERE handle = :handle UNION SELECT "
+ "1 + depth, EntryNames.isFile, EntryNames.handle, EntryNames.parent, "
+ "EntryNames.name, hashEntry( rehashMap.hash, EntryNames.name ) "
+ "FROM rehashMap, EntryNames WHERE rehashMap.handle = EntryNames.parent ) "
+ "SELECT depth, isFile, handle, parent, name, hash FROM rehashMap "
+ ";"_ns;
+
+ const nsLiteralCString createIndexByDepthQuery =
+ "CREATE INDEX indexOnDepth ON ParentChildHash ( depth ); "_ns;
+
+ // To avoid constraint violation, we insert new entries under the old parent.
+ // The destination should be empty at this point: either we exited because
+ // overwrite was not desired, or the existing content was removed.
+ const nsLiteralCString insertNewEntriesQuery =
+ "INSERT INTO Entries ( handle, parent ) "
+ "SELECT hash, :parent FROM ParentChildHash "
+ ";"_ns;
+
+ const nsLiteralCString insertNewDirectoriesQuery =
+ "INSERT INTO Directories ( handle, name ) "
+ "SELECT hash, name FROM ParentChildHash WHERE isFile = 0 "
+ "ORDER BY depth "
+ ";"_ns;
+
+ const nsLiteralCString insertNewFilesQuery =
+ "INSERT INTO Files ( handle, type, name ) "
+ "SELECT ParentChildHash.hash, Files.type, ParentChildHash.name "
+ "FROM ParentChildHash INNER JOIN Files USING (handle) "
+ "WHERE ParentChildHash.isFile = 1 "
+ ";"_ns;
+
+ const nsLiteralCString updateFileMappingsQuery =
+ "UPDATE FileIds SET handle = hash "
+ "FROM ( SELECT handle, hash FROM ParentChildHash ) AS replacement "
+ "WHERE FileIds.handle = replacement.handle "
+ ";"_ns;
+
+ const nsLiteralCString updateMainFilesQuery =
+ "UPDATE MainFiles SET handle = hash "
+ "FROM ( SELECT handle, hash FROM ParentChildHash ) AS replacement "
+ "WHERE MainFiles.handle = replacement.handle "
+ ";"_ns;
+
+ // Now fix the parents
+ const nsLiteralCString updateEntryMappingsQuery =
+ "UPDATE Entries SET parent = hash "
+ "FROM ( SELECT Lhs.hash AS handle, Rhs.hash AS hash, Lhs.depth AS depth "
+ "FROM ParentChildHash AS Lhs "
+ "INNER JOIN ParentChildHash AS Rhs "
+ "ON Rhs.handle = Lhs.parent ORDER BY depth ) AS replacement "
+ "WHERE Entries.handle = replacement.handle "
+ ";"_ns;
+
+ const nsLiteralCString cleanupOldEntriesQuery =
+ "DELETE FROM Entries WHERE handle = :handle "
+ ";"_ns;
+
+ // Index is automatically deleted
+ const nsLiteralCString cleanupTemporaries =
+ "DROP TABLE ParentChildHash "
+ ";"_ns;
+
+ nsCOMPtr<mozIStorageFunction> rehashFunction =
+ new data::FileSystemHashStorageFunction();
+ QM_TRY(MOZ_TO_RESULT(aConnection->CreateFunction("hashEntry"_ns,
+ /* number of arguments */ 2,
+ rehashFunction)));
+ auto finallyRemoveFunction = MakeScopeExit([&aConnection]() {
+ QM_WARNONLY_TRY(MOZ_TO_RESULT(aConnection->RemoveFunction("hashEntry"_ns)));
+ });
+
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConnection));
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, updateNameQuery));
+ QM_TRY(QM_TO_RESULT(
+ stmt.BindNameByName("newName"_ns, aNewDesignation.childName())));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, calculateHashesQuery));
+ QM_TRY(QM_TO_RESULT(
+ stmt.BindEntryIdByName("newParent"_ns, aNewDesignation.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(aConnection->ExecuteSimpleSQL(createIndexByDepthQuery)));
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, insertNewEntriesQuery));
+ QM_TRY(QM_TO_RESULT(
+ stmt.BindEntryIdByName("parent"_ns, aNewDesignation.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(
+ ResultStatement stmt,
+ ResultStatement::Create(aConnection, insertNewDirectoriesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, insertNewFilesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(
+ ResultStatement stmt,
+ ResultStatement::Create(aConnection, updateFileMappingsQuery));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, updateMainFilesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(
+ ResultStatement stmt,
+ ResultStatement::Create(aConnection, updateEntryMappingsQuery));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, cleanupOldEntriesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, cleanupTemporaries));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return NS_OK;
+}
+
+/**
+ * @brief Each entryId is interpreted as a large integer, which is increased
+ * until an unused value is found. This process is in principle infallible.
+ * The files associated with a given path will form a cluster next to the
+ * entryId which could be used for recovery because our hash function is
+ * expected to distribute all clusters far from each other.
+ */
+Result<FileId, QMResult> GetNextFreeFileId(
+ const FileSystemConnection& aConnection,
+ const FileSystemFileManager& aFileManager, const EntryId& aEntryId) {
+ MOZ_ASSERT(32u == aEntryId.Length());
+
+ auto DoesExist = [&aConnection, &aFileManager](
+ const FileId& aId) -> Result<bool, QMResult> {
+ QM_TRY_INSPECT(const nsCOMPtr<nsIFile>& diskFile,
+ aFileManager.GetFile(aId));
+
+ bool result = true;
+ QM_TRY(QM_TO_RESULT(diskFile->Exists(&result)));
+ if (result) {
+ return true;
+ }
+
+ QM_TRY_RETURN(DoesFileIdExist(aConnection, aId));
+ };
+
+ auto Next = [](FileId& aId) {
+ // Using a larger integer would make fileIds depend on platform endianness.
+ using IntegerType = uint8_t;
+ constexpr int32_t bufferSize = 32 / sizeof(IntegerType);
+ using IdBuffer = std::array<IntegerType, bufferSize>;
+
+ auto Increase = [](IdBuffer& aIn) {
+ for (int i = 0; i < bufferSize; ++i) {
+ if (1u + aIn[i] != 0u) {
+ ++aIn[i];
+ return;
+ }
+ aIn[i] = 0u;
+ }
+ };
+
+ DebugOnly<nsCString> original = aId.Value();
+ Increase(*reinterpret_cast<IdBuffer*>(aId.mValue.BeginWriting()));
+ MOZ_ASSERT(!aId.Value().Equals(original));
+ };
+
+ FileId id = FileId(aEntryId);
+
+ while (true) {
+ QM_WARNONLY_TRY_UNWRAP(Maybe<bool> maybeExists, DoesExist(id));
+ if (maybeExists.isSome() && !maybeExists.value()) {
+ return id;
+ }
+
+ Next(id);
+ }
+}
+
+Result<FileId, QMResult> AddNewFileId(const FileSystemConnection& aConnection,
+ const FileSystemFileManager& aFileManager,
+ const EntryId& aEntryId) {
+ QM_TRY_INSPECT(const FileId& nextFreeId,
+ GetNextFreeFileId(aConnection, aFileManager, aEntryId));
+
+ const nsLiteralCString insertNewFileIdQuery =
+ "INSERT INTO FileIds ( fileId, handle ) "
+ "VALUES ( :fileId, :entryId ) "
+ "; "_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, insertNewFileIdQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, nextFreeId)));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId)));
+
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+
+ return nextFreeId;
+}
+
+/**
+ * @brief Get recorded usage or zero if nothing was ever written to the file.
+ * Removing files is only allowed when there is no lock on the file, and their
+ * usage is either correctly recorded in the database during unlock, or nothing,
+ * or they remain in tracked state and the quota manager assumes their usage to
+ * be equal to the latest recorded value. In all cases, the latest recorded
+ * value (or nothing) is the correct amount of quota to be released.
+ */
+Result<Usage, QMResult> GetKnownUsage(const FileSystemConnection& aConnection,
+ const FileId& aFileId) {
+ const nsLiteralCString trackedUsageQuery =
+ "SELECT usage FROM Usages WHERE handle = :handle ;"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, trackedUsageQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId)));
+
+ QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep());
+ if (!moreResults) {
+ return 0;
+ }
+
+ QM_TRY_RETURN(stmt.GetUsageByColumn(/* Column */ 0u));
+}
+
+} // namespace
+
+/* static */
+nsresult FileSystemDatabaseManagerVersion002::RescanTrackedUsages(
+ const FileSystemConnection& aConnection,
+ const quota::OriginMetadata& aOriginMetadata) {
+ return FileSystemDatabaseManagerVersion001::RescanTrackedUsages(
+ aConnection, aOriginMetadata);
+}
+
+/* static */
+Result<Usage, QMResult> FileSystemDatabaseManagerVersion002::GetFileUsage(
+ const FileSystemConnection& aConnection) {
+ return FileSystemDatabaseManagerVersion001::GetFileUsage(aConnection);
+}
+
+nsresult FileSystemDatabaseManagerVersion002::GetFile(
+ const EntryId& aEntryId, const FileId& aFileId, const FileMode& aMode,
+ ContentType& aType, TimeStamp& lastModifiedMilliSeconds,
+ nsTArray<Name>& aPath, nsCOMPtr<nsIFile>& aFile) const {
+ MOZ_ASSERT(!aFileId.IsEmpty());
+
+ const FileSystemEntryPair endPoints(mRootEntry, aEntryId);
+ QM_TRY_UNWRAP(aPath, ResolveReversedPath(mConnection, endPoints));
+ if (aPath.IsEmpty()) {
+ return NS_ERROR_DOM_NOT_FOUND_ERR;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(GetFileAttributes(mConnection, aEntryId, aType)));
+
+ if (aMode == FileMode::SHARED_FROM_COPY) {
+ QM_WARNONLY_TRY_UNWRAP(Maybe<FileId> mainFileId, GetFileId(aEntryId));
+ if (mainFileId) {
+ QM_TRY_UNWRAP(aFile,
+ mFileManager->CreateFileFrom(aFileId, mainFileId.value()));
+ } else {
+ // LockShared/EnsureTemporaryFileId has provided a brand new fileId.
+ QM_TRY_UNWRAP(aFile, mFileManager->GetOrCreateFile(aFileId));
+ }
+ } else {
+ MOZ_ASSERT(aMode == FileMode::EXCLUSIVE ||
+ aMode == FileMode::SHARED_FROM_EMPTY);
+
+ QM_TRY_UNWRAP(aFile, mFileManager->GetOrCreateFile(aFileId));
+ }
+
+ PRTime lastModTime = 0;
+ QM_TRY(MOZ_TO_RESULT(aFile->GetLastModifiedTime(&lastModTime)));
+ lastModifiedMilliSeconds = static_cast<TimeStamp>(lastModTime);
+
+ aPath.Reverse();
+
+ return NS_OK;
+}
+
+Result<EntryId, QMResult> FileSystemDatabaseManagerVersion002::RenameEntry(
+ const FileSystemEntryMetadata& aHandle, const Name& aNewName) {
+ MOZ_ASSERT(!aNewName.IsEmpty());
+
+ const auto& entryId = aHandle.entryId();
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ // Can't rename root
+ if (mRootEntry == entryId) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ // Verify the source exists
+ QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId),
+ Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)));
+
+ // Are we actually renaming?
+ if (aHandle.entryName() == aNewName) {
+ return entryId;
+ }
+
+ QM_TRY(QM_TO_RESULT(PrepareRenameEntry(mConnection, mDataManager, aHandle,
+ aNewName, isFile)));
+
+ QM_TRY_UNWRAP(EntryId parentId, FindParent(mConnection, entryId));
+ FileSystemChildMetadata newDesignation(parentId, aNewName);
+
+ if (isFile) {
+ const ContentType type = DetermineContentType(aNewName);
+ QM_TRY(
+ QM_TO_RESULT(RehashFile(mConnection, entryId, newDesignation, type)));
+ } else {
+ QM_TRY(QM_TO_RESULT(RehashDirectory(mConnection, entryId, newDesignation)));
+ }
+
+ QM_TRY_UNWRAP(DebugOnly<EntryId> dbId,
+ FindEntryId(mConnection, newDesignation, isFile));
+ QM_TRY_UNWRAP(EntryId generated,
+ FileSystemHashSource::GenerateHash(parentId, aNewName));
+ MOZ_ASSERT(static_cast<EntryId&>(dbId).Equals(generated));
+
+ return generated;
+}
+
+Result<EntryId, QMResult> FileSystemDatabaseManagerVersion002::MoveEntry(
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation) {
+ MOZ_ASSERT(!aHandle.entryId().IsEmpty());
+
+ const auto& entryId = aHandle.entryId();
+
+ if (mRootEntry == entryId) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ // Verify the source exists
+ QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId),
+ Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)));
+
+ // If the rename doesn't change the name or directory, just return success.
+ // XXX Needs to be added to the spec
+ QM_WARNONLY_TRY_UNWRAP(Maybe<bool> maybeSame,
+ IsSame(mConnection, aHandle, aNewDesignation, isFile));
+ if (maybeSame && maybeSame.value()) {
+ return entryId;
+ }
+
+ QM_TRY(QM_TO_RESULT(PrepareMoveEntry(mConnection, mDataManager, aHandle,
+ aNewDesignation, isFile)));
+
+ if (isFile) {
+ const ContentType type = DetermineContentType(aNewDesignation.childName());
+ QM_TRY(
+ QM_TO_RESULT(RehashFile(mConnection, entryId, aNewDesignation, type)));
+ } else {
+ QM_TRY(
+ QM_TO_RESULT(RehashDirectory(mConnection, entryId, aNewDesignation)));
+ }
+
+ QM_TRY_UNWRAP(DebugOnly<EntryId> dbId,
+ FindEntryId(mConnection, aNewDesignation, isFile));
+ QM_TRY_UNWRAP(EntryId generated,
+ FileSystemHashSource::GenerateHash(
+ aNewDesignation.parentId(), aNewDesignation.childName()));
+ MOZ_ASSERT(static_cast<EntryId&>(dbId).Equals(generated));
+
+ return generated;
+}
+
+Result<EntryId, QMResult> FileSystemDatabaseManagerVersion002::GetEntryId(
+ const FileSystemChildMetadata& aHandle) const {
+ return fs::data::GetEntryHandle(aHandle);
+}
+
+Result<EntryId, QMResult> FileSystemDatabaseManagerVersion002::GetEntryId(
+ const FileId& aFileId) const {
+ const nsLiteralCString getEntryIdQuery =
+ "SELECT handle FROM FileIds WHERE fileId = :fileId ;"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, getEntryIdQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, aFileId)));
+ QM_TRY_UNWRAP(bool hasEntries, stmt.ExecuteStep());
+
+ if (!hasEntries || stmt.IsNullByColumn(/* Column */ 0u)) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ QM_TRY_RETURN(stmt.GetEntryIdByColumn(/* Column */ 0u));
+}
+
+Result<FileId, QMResult> FileSystemDatabaseManagerVersion002::EnsureFileId(
+ const EntryId& aEntryId) {
+ QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, aEntryId));
+ if (!exists) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ QM_TRY_UNWRAP(Maybe<FileId> maybeMainFileId,
+ QM_OR_ELSE_LOG_VERBOSE_IF(
+ // Expression.
+ GetFileId(aEntryId).map([](auto mainFileId) {
+ return Some(std::move(mainFileId));
+ }),
+ // Predicate.
+ IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>,
+ // Fallback.
+ ([](const auto&) -> Result<Maybe<FileId>, QMResult> {
+ return Maybe<FileId>{};
+ })));
+
+ if (maybeMainFileId) {
+ return *maybeMainFileId;
+ }
+
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection));
+
+ QM_TRY_INSPECT(const FileId& fileId,
+ AddNewFileId(mConnection, *mFileManager, aEntryId));
+
+ QM_TRY(QM_TO_RESULT(MergeFileId(aEntryId, fileId, /* aAbort */ false)));
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return fileId;
+}
+
+Result<FileId, QMResult>
+FileSystemDatabaseManagerVersion002::EnsureTemporaryFileId(
+ const EntryId& aEntryId) {
+ QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, aEntryId));
+ if (!exists) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ QM_TRY_RETURN(AddNewFileId(mConnection, *mFileManager, aEntryId));
+}
+
+Result<FileId, QMResult> FileSystemDatabaseManagerVersion002::GetFileId(
+ const EntryId& aEntryId) const {
+ MOZ_ASSERT(mConnection);
+ return data::GetFileId002(mConnection, aEntryId);
+}
+
+nsresult FileSystemDatabaseManagerVersion002::MergeFileId(
+ const EntryId& aEntryId, const FileId& aFileId, bool aAbort) {
+ MOZ_ASSERT(mConnection);
+
+ auto doCleanUp = [this](const FileId& aCleanable) -> nsresult {
+ // We need to clean up the old main file.
+ QM_TRY_UNWRAP(Usage usage,
+ GetKnownUsage(mConnection, aCleanable).mapErr(toNSResult));
+
+ QM_WARNONLY_TRY_UNWRAP(Maybe<Usage> removedUsage,
+ mFileManager->RemoveFile(aCleanable));
+
+ if (removedUsage) {
+ // Removal of file data was ok, update the related fileId and usage
+ QM_WARNONLY_TRY(QM_TO_RESULT(RemoveFileId(aCleanable)));
+
+ if (usage > 0) { // Performance!
+ DecreaseCachedQuotaUsage(usage);
+ }
+
+ // We only check the most common case. This can fail spuriously if an
+ // external application writes to the file, or OS reports zero size due to
+ // corruption.
+ MOZ_ASSERT_IF(0 == mFilesOfUnknownUsage, usage == removedUsage.value());
+
+ return NS_OK;
+ }
+
+ // Removal failed
+ const nsLiteralCString forgetCleanable =
+ "UPDATE FileIds SET handle = NULL WHERE fileId = :fileId ; "_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, forgetCleanable)
+ .mapErr(toNSResult));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, aCleanable)));
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+
+ TryRemoveDuringIdleMaintenance({aCleanable});
+
+ return NS_OK;
+ };
+
+ if (aAbort) {
+ QM_TRY(MOZ_TO_RESULT(doCleanUp(aFileId)));
+
+ return NS_OK;
+ }
+
+ QM_TRY_UNWRAP(
+ Maybe<FileId> maybeOldFileId,
+ QM_OR_ELSE_LOG_VERBOSE_IF(
+ // Expression.
+ GetFileId(aEntryId)
+ .map([](auto oldFileId) { return Some(std::move(oldFileId)); })
+ .mapErr(toNSResult),
+ // Predicate.
+ IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>,
+ // Fallback.
+ ErrToDefaultOk<Maybe<FileId>>));
+
+ if (maybeOldFileId && *maybeOldFileId == aFileId) {
+ return NS_OK; // Nothing to do
+ }
+
+ // Main file changed
+ const nsLiteralCString flagAsMainFileQuery =
+ "INSERT INTO MainFiles ( handle, fileId ) "
+ "VALUES ( :entryId, :fileId ) "
+ "ON CONFLICT (handle) "
+ "DO UPDATE SET fileId = excluded.fileId "
+ "; "_ns;
+
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection));
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, flagAsMainFileQuery)
+ .mapErr(toNSResult));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId)));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, aFileId)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+
+ if (!maybeOldFileId) {
+ // We successfully added a new main file and there is nothing to clean up.
+ QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
+
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(maybeOldFileId);
+ MOZ_ASSERT(*maybeOldFileId != aFileId);
+
+ QM_TRY(MOZ_TO_RESULT(doCleanUp(*maybeOldFileId)));
+
+ // If the old fileId and usage were not deleted, main file update fails.
+ QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
+
+ return NS_OK;
+}
+
+Result<bool, QMResult> FileSystemDatabaseManagerVersion002::DoesFileIdExist(
+ const FileId& aFileId) const {
+ QM_TRY_RETURN(data::DoesFileIdExist(mConnection, aFileId));
+}
+
+nsresult FileSystemDatabaseManagerVersion002::RemoveFileId(
+ const FileId& aFileId) {
+ const nsLiteralCString removeFileIdQuery =
+ "DELETE FROM FileIds "
+ "WHERE fileId = :fileId "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, removeFileIdQuery)
+ .mapErr(toNSResult));
+
+ QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("fileId"_ns, aFileId.Value())));
+
+ return stmt.Execute();
+}
+
+Result<Usage, QMResult>
+FileSystemDatabaseManagerVersion002::GetUsagesOfDescendants(
+ const EntryId& aEntryId) const {
+ const nsLiteralCString descendantUsagesQuery =
+ "WITH RECURSIVE traceChildren(handle, parent) AS ( "
+ "SELECT handle, parent FROM Entries WHERE handle = :handle "
+ "UNION "
+ "SELECT Entries.handle, Entries.parent FROM traceChildren, Entries "
+ "WHERE traceChildren.handle=Entries.parent ) "
+ "SELECT sum(Usages.usage) "
+ "FROM traceChildren "
+ "INNER JOIN FileIds ON traceChildren.handle = FileIds.handle "
+ "INNER JOIN Usages ON Usages.handle = FileIds.fileId "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, descendantUsagesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId)));
+ QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep());
+ if (!moreResults) {
+ return 0;
+ }
+
+ QM_TRY_RETURN(stmt.GetUsageByColumn(/* Column */ 0u));
+}
+
+Result<nsTArray<FileId>, QMResult>
+FileSystemDatabaseManagerVersion002::FindFilesUnderEntry(
+ const EntryId& aEntryId) const {
+ const nsLiteralCString descendantsQuery =
+ "WITH RECURSIVE traceChildren(handle, parent) AS ( "
+ "SELECT handle, parent FROM Entries WHERE handle = :handle "
+ "UNION "
+ "SELECT Entries.handle, Entries.parent FROM traceChildren, Entries "
+ "WHERE traceChildren.handle = Entries.parent ) "
+ "SELECT FileIds.fileId "
+ "FROM traceChildren INNER JOIN FileIds USING (handle) "
+ ";"_ns;
+
+ nsTArray<FileId> descendants;
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, descendantsQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId)));
+
+ while (true) {
+ QM_TRY_INSPECT(const bool& moreResults, stmt.ExecuteStep());
+ if (!moreResults) {
+ break;
+ }
+
+ QM_TRY_INSPECT(const FileId& fileId,
+ stmt.GetFileIdByColumn(/* Column */ 0u));
+ descendants.AppendElement(fileId);
+ }
+ }
+
+ return descendants;
+}
+
+} // namespace mozilla::dom::fs::data
diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.h b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.h
new file mode 100644
index 0000000000..6dec629632
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.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_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION002_H_
+#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION002_H_
+
+#include "FileSystemDatabaseManagerVersion001.h"
+
+namespace mozilla::dom::fs::data {
+
+class FileSystemDatabaseManagerVersion002
+ : public FileSystemDatabaseManagerVersion001 {
+ public:
+ FileSystemDatabaseManagerVersion002(
+ FileSystemDataManager* aDataManager, FileSystemConnection&& aConnection,
+ UniquePtr<FileSystemFileManager>&& aFileManager,
+ const EntryId& aRootEntry)
+ : FileSystemDatabaseManagerVersion001(
+ aDataManager, std::move(aConnection), std::move(aFileManager),
+ aRootEntry) {}
+
+ /* Static to allow use by quota client without instantiation */
+ static nsresult RescanTrackedUsages(
+ const FileSystemConnection& aConnection,
+ const quota::OriginMetadata& aOriginMetadata);
+
+ /* Static to allow use by quota client without instantiation */
+ static Result<Usage, QMResult> GetFileUsage(
+ const FileSystemConnection& aConnection);
+
+ nsresult GetFile(const EntryId& aEntryId, const FileId& aFileId,
+ const FileMode& aMode, ContentType& aType,
+ TimeStamp& lastModifiedMilliSeconds, Path& aPath,
+ nsCOMPtr<nsIFile>& aFile) const override;
+
+ Result<EntryId, QMResult> RenameEntry(const FileSystemEntryMetadata& aHandle,
+ const Name& aNewName) override;
+
+ Result<EntryId, QMResult> MoveEntry(
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation) override;
+
+ Result<EntryId, QMResult> GetEntryId(
+ const FileSystemChildMetadata& aHandle) const override;
+
+ Result<EntryId, QMResult> GetEntryId(const FileId& aFileId) const override;
+
+ Result<FileId, QMResult> EnsureFileId(const EntryId& aEntryId) override;
+
+ Result<FileId, QMResult> EnsureTemporaryFileId(
+ const EntryId& aEntryId) override;
+
+ Result<FileId, QMResult> GetFileId(const EntryId& aEntryId) const override;
+
+ nsresult MergeFileId(const EntryId& aEntryId, const FileId& aFileId,
+ bool aAbort) override;
+
+ protected:
+ Result<bool, QMResult> DoesFileIdExist(const FileId& aFileId) const override;
+
+ nsresult RemoveFileId(const FileId& aFileId) override;
+
+ Result<Usage, QMResult> GetUsagesOfDescendants(
+ const EntryId& aEntryId) const override;
+
+ Result<nsTArray<FileId>, QMResult> FindFilesUnderEntry(
+ const EntryId& aEntryId) const override;
+};
+
+} // namespace mozilla::dom::fs::data
+
+#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION002_H_
diff --git a/dom/fs/parent/datamodel/FileSystemFileManager.cpp b/dom/fs/parent/datamodel/FileSystemFileManager.cpp
new file mode 100644
index 0000000000..02a69467dc
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemFileManager.cpp
@@ -0,0 +1,393 @@
+/* -*- 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 "FileSystemFileManager.h"
+
+#include "FileSystemDataManager.h"
+#include "FileSystemHashSource.h"
+#include "FileSystemParentTypes.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsCOMPtr.h"
+#include "nsHashKeys.h"
+#include "nsIFile.h"
+#include "nsIFileProtocolHandler.h"
+#include "nsIFileURL.h"
+#include "nsIURIMutator.h"
+#include "nsTHashMap.h"
+#include "nsXPCOM.h"
+
+namespace mozilla::dom::fs::data {
+
+namespace {
+
+constexpr nsLiteralString kDatabaseFileName = u"metadata.sqlite"_ns;
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetFileDestination(
+ const nsCOMPtr<nsIFile>& aTopDirectory, const FileId& aFileId) {
+ MOZ_ASSERT(32u == aFileId.Value().Length());
+
+ nsCOMPtr<nsIFile> destination;
+
+ // nsIFile Clone is not a constant method
+ QM_TRY(QM_TO_RESULT(aTopDirectory->Clone(getter_AddRefs(destination))));
+
+ QM_TRY_UNWRAP(Name encoded, FileSystemHashSource::EncodeHash(aFileId));
+
+ MOZ_ALWAYS_TRUE(IsAscii(encoded));
+
+ nsString relativePath;
+ relativePath.Append(Substring(encoded, 0, 2));
+
+ QM_TRY(QM_TO_RESULT(destination->AppendRelativePath(relativePath)));
+
+ QM_TRY(QM_TO_RESULT(destination->AppendRelativePath(encoded)));
+
+ return destination;
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFileImpl(
+ const nsAString& aFilePath) {
+ MOZ_ASSERT(!aFilePath.IsEmpty());
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> result,
+ QM_TO_RESULT_TRANSFORM(quota::QM_NewLocalFile(aFilePath)));
+
+ bool exists = true;
+ QM_TRY(QM_TO_RESULT(result->Exists(&exists)));
+
+ if (!exists) {
+ QM_TRY(QM_TO_RESULT(result->Create(nsIFile::NORMAL_FILE_TYPE, 0644)));
+
+ return result;
+ }
+
+ bool isDirectory = true;
+ QM_TRY(QM_TO_RESULT(result->IsDirectory(&isDirectory)));
+ QM_TRY(OkIf(!isDirectory), Err(QMResult(NS_ERROR_FILE_IS_DIRECTORY)));
+
+ return result;
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetFile(
+ const nsCOMPtr<nsIFile>& aTopDirectory, const FileId& aFileId) {
+ MOZ_ASSERT(!aFileId.IsEmpty());
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject,
+ GetFileDestination(aTopDirectory, aFileId));
+
+ nsString desiredPath;
+ QM_TRY(QM_TO_RESULT(pathObject->GetPath(desiredPath)));
+
+ QM_TRY_RETURN(QM_TO_RESULT_TRANSFORM(quota::QM_NewLocalFile(desiredPath)));
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFile(
+ const nsCOMPtr<nsIFile>& aTopDirectory, const FileId& aFileId) {
+ MOZ_ASSERT(!aFileId.IsEmpty());
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject,
+ GetFileDestination(aTopDirectory, aFileId));
+
+ nsString desiredPath;
+ QM_TRY(QM_TO_RESULT(pathObject->GetPath(desiredPath)));
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> result, GetOrCreateFileImpl(desiredPath));
+
+ return result;
+}
+
+nsresult RemoveFileObject(const nsCOMPtr<nsIFile>& aFilePtr) {
+ // If we cannot tell whether the object is file or directory, or it is a
+ // directory, it is abandoned as an unknown object. If an attempt is made to
+ // create a new object with the same path on disk, we regenerate the FileId
+ // until the collision is resolved.
+
+ bool isFile = false;
+ QM_TRY(MOZ_TO_RESULT(aFilePtr->IsFile(&isFile)));
+
+ QM_TRY(OkIf(isFile), NS_ERROR_FILE_IS_DIRECTORY);
+
+ QM_TRY(QM_TO_RESULT(aFilePtr->Remove(/* recursive */ false)));
+
+ return NS_OK;
+}
+
+#ifdef DEBUG
+// Unused in release builds
+Result<Usage, QMResult> GetFileSize(const nsCOMPtr<nsIFile>& aFileObject) {
+ bool exists = false;
+ QM_TRY(QM_TO_RESULT(aFileObject->Exists(&exists)));
+
+ if (!exists) {
+ return 0;
+ }
+
+ bool isFile = false;
+ QM_TRY(QM_TO_RESULT(aFileObject->IsFile(&isFile)));
+
+ // We never create directories with this path: this is an unknown object
+ // and the file does not exist
+ QM_TRY(OkIf(isFile), 0);
+
+ QM_TRY_UNWRAP(Usage fileSize,
+ QM_TO_RESULT_INVOKE_MEMBER(aFileObject, GetFileSize));
+
+ return fileSize;
+}
+#endif
+
+} // namespace
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetFileSystemDirectory(
+ const quota::OriginMetadata& aOriginMetadata) {
+ MOZ_ASSERT(aOriginMetadata.mPersistenceType ==
+ quota::PERSISTENCE_TYPE_DEFAULT);
+
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> fileSystemDirectory,
+ QM_TO_RESULT_TRANSFORM(
+ quotaManager->GetOriginDirectory(aOriginMetadata)));
+
+ QM_TRY(QM_TO_RESULT(fileSystemDirectory->AppendRelativePath(
+ NS_LITERAL_STRING_FROM_CSTRING(FILESYSTEM_DIRECTORY_NAME))));
+
+ return fileSystemDirectory;
+}
+
+nsresult EnsureFileSystemDirectory(
+ const quota::OriginMetadata& aOriginMetadata) {
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY(MOZ_TO_RESULT(
+ quotaManager->EnsureTemporaryStorageIsInitializedInternal()));
+
+ QM_TRY_INSPECT(const auto& fileSystemDirectory,
+ quotaManager
+ ->EnsureTemporaryOriginIsInitialized(
+ quota::PERSISTENCE_TYPE_DEFAULT, aOriginMetadata)
+ .map([](const auto& aPair) { return aPair.first; }));
+
+ QM_TRY(QM_TO_RESULT(fileSystemDirectory->AppendRelativePath(
+ NS_LITERAL_STRING_FROM_CSTRING(FILESYSTEM_DIRECTORY_NAME))));
+
+ bool exists = true;
+ QM_TRY(QM_TO_RESULT(fileSystemDirectory->Exists(&exists)));
+
+ if (!exists) {
+ QM_TRY(QM_TO_RESULT(
+ fileSystemDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755)));
+
+ return NS_OK;
+ }
+
+ bool isDirectory = true;
+ QM_TRY(QM_TO_RESULT(fileSystemDirectory->IsDirectory(&isDirectory)));
+ QM_TRY(OkIf(isDirectory), NS_ERROR_FILE_NOT_DIRECTORY);
+
+ return NS_OK;
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetDatabaseFile(
+ const quota::OriginMetadata& aOriginMetadata) {
+ MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty());
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> databaseFile,
+ GetFileSystemDirectory(aOriginMetadata));
+
+ QM_TRY(QM_TO_RESULT(databaseFile->AppendRelativePath(kDatabaseFileName)));
+
+ return databaseFile;
+}
+
+/**
+ * TODO: This is almost identical to the corresponding function of IndexedDB
+ */
+Result<nsCOMPtr<nsIFileURL>, QMResult> GetDatabaseFileURL(
+ const quota::OriginMetadata& aOriginMetadata,
+ const int64_t aDirectoryLockId) {
+ MOZ_ASSERT(aDirectoryLockId >= -1);
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> databaseFile,
+ GetDatabaseFile(aOriginMetadata));
+
+ QM_TRY_INSPECT(
+ const auto& protocolHandler,
+ QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED(
+ nsCOMPtr<nsIProtocolHandler>, MOZ_SELECT_OVERLOAD(do_GetService),
+ NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "file")));
+
+ QM_TRY_INSPECT(const auto& fileHandler,
+ QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED(
+ nsCOMPtr<nsIFileProtocolHandler>,
+ MOZ_SELECT_OVERLOAD(do_QueryInterface), protocolHandler)));
+
+ QM_TRY_INSPECT(const auto& mutator,
+ QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<nsIURIMutator>, fileHandler, NewFileURIMutator,
+ databaseFile)));
+
+ // aDirectoryLockId should only be -1 when we are called from
+ // FileSystemQuotaClient::InitOrigin when the temporary storage hasn't been
+ // initialized yet. At that time, the in-memory objects (e.g. OriginInfo) are
+ // only being created so it doesn't make sense to tunnel quota information to
+ // QuotaVFS to get corresponding QuotaObject instances for SQLite files.
+ const nsCString directoryLockIdClause =
+ "&directoryLockId="_ns + IntToCString(aDirectoryLockId);
+
+ nsCOMPtr<nsIFileURL> result;
+ QM_TRY(QM_TO_RESULT(NS_MutateURI(mutator)
+ .SetQuery("cache=private"_ns + directoryLockIdClause)
+ .Finalize(result)));
+
+ return result;
+}
+
+/* static */
+Result<FileSystemFileManager, QMResult>
+FileSystemFileManager::CreateFileSystemFileManager(
+ nsCOMPtr<nsIFile>&& topDirectory) {
+ return FileSystemFileManager(std::move(topDirectory));
+}
+
+/* static */
+Result<UniquePtr<FileSystemFileManager>, QMResult>
+FileSystemFileManager::CreateFileSystemFileManager(
+ const quota::OriginMetadata& aOriginMetadata) {
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> topDirectory,
+ GetFileSystemDirectory(aOriginMetadata));
+
+ return MakeUnique<FileSystemFileManager>(
+ FileSystemFileManager(std::move(topDirectory)));
+}
+
+FileSystemFileManager::FileSystemFileManager(nsCOMPtr<nsIFile>&& aTopDirectory)
+ : mTopDirectory(std::move(aTopDirectory)) {}
+
+Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::GetFile(
+ const FileId& aFileId) const {
+ return data::GetFile(mTopDirectory, aFileId);
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::GetOrCreateFile(
+ const FileId& aFileId) {
+ return data::GetOrCreateFile(mTopDirectory, aFileId);
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::CreateFileFrom(
+ const FileId& aDestinationFileId, const FileId& aSourceFileId) {
+ MOZ_ASSERT(!aDestinationFileId.IsEmpty());
+ MOZ_ASSERT(!aSourceFileId.IsEmpty());
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> original, GetFile(aSourceFileId));
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> destination,
+ GetFileDestination(mTopDirectory, aDestinationFileId));
+
+ nsAutoString leafName;
+ QM_TRY(QM_TO_RESULT(destination->GetLeafName(leafName)));
+
+ nsCOMPtr<nsIFile> destParent;
+ QM_TRY(QM_TO_RESULT(destination->GetParent(getter_AddRefs(destParent))));
+
+ QM_TRY(QM_TO_RESULT(original->CopyTo(destParent, leafName)));
+
+#ifdef DEBUG
+ bool exists = false;
+ QM_TRY(QM_TO_RESULT(destination->Exists(&exists)));
+ MOZ_ASSERT(exists);
+
+ int64_t destSize = 0;
+ QM_TRY(QM_TO_RESULT(destination->GetFileSize(&destSize)));
+
+ int64_t origSize = 0;
+ QM_TRY(QM_TO_RESULT(original->GetFileSize(&origSize)));
+
+ MOZ_ASSERT(destSize == origSize);
+#endif
+
+ return destination;
+}
+
+Result<Usage, QMResult> FileSystemFileManager::RemoveFile(
+ const FileId& aFileId) {
+ MOZ_ASSERT(!aFileId.IsEmpty());
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject,
+ GetFileDestination(mTopDirectory, aFileId));
+
+ bool exists = false;
+ QM_TRY(QM_TO_RESULT(pathObject->Exists(&exists)));
+
+ if (!exists) {
+ return 0;
+ }
+
+ bool isFile = false;
+ QM_TRY(QM_TO_RESULT(pathObject->IsFile(&isFile)));
+
+ // We could handle this also as a nonexistent file.
+ if (!isFile) {
+ return Err(QMResult(NS_ERROR_FILE_IS_DIRECTORY));
+ }
+
+ Usage totalUsage = 0;
+#ifdef DEBUG
+ QM_TRY_UNWRAP(totalUsage,
+ QM_TO_RESULT_INVOKE_MEMBER(pathObject, GetFileSize));
+#endif
+
+ QM_TRY(QM_TO_RESULT(pathObject->Remove(/* recursive */ false)));
+
+ return totalUsage;
+}
+
+Result<DebugOnly<Usage>, QMResult> FileSystemFileManager::RemoveFiles(
+ const nsTArray<FileId>& aFileIds, nsTArray<FileId>& aFailedRemovals) {
+ if (aFileIds.IsEmpty()) {
+ return DebugOnly<Usage>(0);
+ }
+
+ CheckedInt64 totalUsage = 0;
+ for (const auto& someId : aFileIds) {
+ QM_WARNONLY_TRY_UNWRAP(Maybe<nsCOMPtr<nsIFile>> maybeFile,
+ GetFileDestination(mTopDirectory, someId));
+ if (!maybeFile) {
+ aFailedRemovals.AppendElement(someId);
+ continue;
+ }
+ nsCOMPtr<nsIFile> fileObject = maybeFile.value();
+
+// Size recorded at close is checked to be equal to the sum of sizes on disk
+#ifdef DEBUG
+ QM_WARNONLY_TRY_UNWRAP(Maybe<Usage> fileSize, GetFileSize(fileObject));
+ if (!fileSize) {
+ aFailedRemovals.AppendElement(someId);
+ continue;
+ }
+ totalUsage += fileSize.value();
+#endif
+
+ QM_WARNONLY_TRY_UNWRAP(Maybe<Ok> ok,
+ MOZ_TO_RESULT(RemoveFileObject(fileObject)));
+ if (!ok) {
+ aFailedRemovals.AppendElement(someId);
+ }
+ }
+
+ MOZ_ASSERT(totalUsage.isValid());
+
+ return DebugOnly<Usage>(totalUsage.value());
+}
+
+} // namespace mozilla::dom::fs::data
diff --git a/dom/fs/parent/datamodel/FileSystemFileManager.h b/dom/fs/parent/datamodel/FileSystemFileManager.h
new file mode 100644
index 0000000000..46391db6f7
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemFileManager.h
@@ -0,0 +1,175 @@
+/* -*- 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_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_
+#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_
+
+#include "ErrorList.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/QMResult.h"
+#include "nsIFile.h"
+#include "nsString.h"
+
+template <class T>
+class nsCOMPtr;
+
+class nsIFileURL;
+
+namespace mozilla::dom {
+
+namespace quota {
+
+struct OriginMetadata;
+
+} // namespace quota
+
+namespace fs {
+
+struct FileId;
+
+namespace data {
+
+/**
+ * @brief Get the directory for file system items of specified origin.
+ * Use this instead of constructing the path from quota manager's storage path.
+ *
+ * @param aOrigin Specified origin
+ * @return Result<nsCOMPtr<nsIFile>, QMResult> Top file system directory
+ */
+Result<nsCOMPtr<nsIFile>, QMResult> GetFileSystemDirectory(
+ const quota::OriginMetadata& aOriginMetadata);
+
+/**
+ * @brief Ensure that the origin-specific directory for file system exists.
+ *
+ * @param aOriginMetadata Specified origin metadata
+ * @return nsresult Error if operation failed
+ */
+nsresult EnsureFileSystemDirectory(
+ const quota::OriginMetadata& aOriginMetadata);
+
+/**
+ * @brief Get file system's database path for specified origin.
+ * Use this to get the database path instead of constructing it from
+ * quota manager's storage path - without the side effect of potentially
+ * creating it.
+ *
+ * @param aOrigin Specified origin
+ * @return Result<nsCOMPtr<nsIFile>, QMResult> Database file object
+ */
+Result<nsCOMPtr<nsIFile>, QMResult> GetDatabaseFile(
+ const quota::OriginMetadata& aOriginMetadata);
+
+/**
+ * @brief Get file system's database url with directory lock parameter for
+ * specified origin. Use this to open a database connection and have the quota
+ * manager guard against its deletion or busy errors due to other connections.
+ *
+ * @param aOrigin Specified origin
+ * @param aDirectoryLockId Directory lock id from the quota manager
+ * @return Result<nsCOMPtr<nsIFileURL>, QMResult> Database file URL object
+ */
+Result<nsCOMPtr<nsIFileURL>, QMResult> GetDatabaseFileURL(
+ const quota::OriginMetadata& aOriginMetadata,
+ const int64_t aDirectoryLockId);
+
+/**
+ * @brief Creates and removes disk-backed representations of the file systems'
+ * file entries for a specified origin.
+ *
+ * Other components should not depend on how the files are organized on disk
+ * but instead rely on the entry id and have access to the local file using the
+ * GetOrCreateFile result.
+ *
+ * The local paths are not necessarily stable in the long term and if they
+ * absolutely must be cached, there should be a way to repopulate the cache
+ * after an internal reorganization of the file entry represenations on disk,
+ * for some currently unforeseen maintenance reason.
+ *
+ * Example: if GetOrCreateFile used to map entryId 'abc' to path '/c/u/1/123'
+ * and now it maps it to '/d/u/1/12/123', the cache should either update all
+ * paths at once through a migration, or purge them and save a new value
+ * whenever a call to GetOrCreateFile is made.
+ */
+class FileSystemFileManager {
+ public:
+ /**
+ * @brief Create a File System File Manager object for a specified origin.
+ *
+ * @param aOrigin
+ * @return Result<FileSystemFileManager, QMResult>
+ */
+ static Result<UniquePtr<FileSystemFileManager>, QMResult>
+ CreateFileSystemFileManager(const quota::OriginMetadata& aOriginMetadata);
+
+ /**
+ * @brief Create a File System File Manager object which keeps file entries
+ * under a specified directory instead of quota manager's storage path.
+ * This should only be used for testing and preferably removed.
+ *
+ * @param topDirectory
+ * @return Result<FileSystemFileManager, QMResult>
+ */
+ static Result<FileSystemFileManager, QMResult> CreateFileSystemFileManager(
+ nsCOMPtr<nsIFile>&& topDirectory);
+
+ /**
+ * @brief Get a file object for a specified entry id. If a file for the entry
+ * does not exist, returns an appropriate error.
+ *
+ * @param aEntryId Specified id of a file system entry
+ * @return Result<nsCOMPtr<nsIFile>, QMResult> File or error.
+ */
+ Result<nsCOMPtr<nsIFile>, QMResult> GetFile(const FileId& aFileId) const;
+
+ /**
+ * @brief Get or create a disk-backed file object for a specified entry id.
+ *
+ * @param aFileId Specified id of a file system entry
+ * @return Result<nsCOMPtr<nsIFile>, QMResult> File abstraction or IO error
+ */
+ Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFile(const FileId& aFileId);
+
+ /**
+ * @brief Create a disk-backed file object as a copy.
+ *
+ * @param aDestinationFileId Specified id of file to be created
+ * @param aSourceFileId Specified id of the file from which we make a copy
+ * @return Result<nsCOMPtr<nsIFile>, QMResult> File abstraction or IO error
+ */
+ Result<nsCOMPtr<nsIFile>, QMResult> CreateFileFrom(
+ const FileId& aDestinationFileId, const FileId& aSourceFileId);
+
+ /**
+ * @brief Remove the disk-backed file object for a specified entry id.
+ * Note: The returned value is 0 in release builds.
+ *
+ * @param aFileId Specified id of a file system entry
+ * @return Result<Usage, QMResult> Error or file size
+ */
+ Result<Usage, QMResult> RemoveFile(const FileId& aFileId);
+
+ /**
+ * @brief This method can be used to try to delete a group of files from the
+ * disk. In debug builds, the sum of the usages is provided ad return value,
+ * in release builds the sum is not calculated.
+ * The method attempts to remove all the files requested.
+ */
+ Result<DebugOnly<Usage>, QMResult> RemoveFiles(
+ const nsTArray<FileId>& aFileIds, nsTArray<FileId>& aFailedRemovals);
+
+ private:
+ explicit FileSystemFileManager(nsCOMPtr<nsIFile>&& aTopDirectory);
+
+ nsCOMPtr<nsIFile> mTopDirectory;
+};
+
+} // namespace data
+} // namespace fs
+} // namespace mozilla::dom
+
+#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_
diff --git a/dom/fs/parent/datamodel/SchemaVersion001.cpp b/dom/fs/parent/datamodel/SchemaVersion001.cpp
new file mode 100644
index 0000000000..cf6eee72cc
--- /dev/null
+++ b/dom/fs/parent/datamodel/SchemaVersion001.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 "SchemaVersion001.h"
+
+#include "FileSystemHashSource.h"
+#include "ResultStatement.h"
+#include "StartedTransaction.h"
+#include "fs/FileSystemConstants.h"
+#include "mozStorageHelper.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+
+namespace mozilla::dom::fs {
+
+namespace {
+
+nsresult CreateEntries(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "CREATE TABLE IF NOT EXISTS Entries ( "
+ "handle BLOB PRIMARY KEY, " // Generated from parent + name, unique
+ "parent BLOB, " // Not null due to constraint
+ "CONSTRAINT parent_is_a_directory "
+ "FOREIGN KEY (parent) "
+ "REFERENCES Directories (handle) "
+ "ON DELETE CASCADE ) "
+ ";"_ns);
+}
+
+nsresult CreateDirectories(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "CREATE TABLE IF NOT EXISTS Directories ( "
+ "handle BLOB PRIMARY KEY, "
+ "name BLOB NOT NULL, "
+ "CONSTRAINT directories_are_entries "
+ "FOREIGN KEY (handle) "
+ "REFERENCES Entries (handle) "
+ "ON DELETE CASCADE ) "
+ ";"_ns);
+}
+
+nsresult CreateFiles(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "CREATE TABLE IF NOT EXISTS Files ( "
+ "handle BLOB PRIMARY KEY, "
+ "type TEXT, "
+ "name BLOB NOT NULL, "
+ "CONSTRAINT files_are_entries "
+ "FOREIGN KEY (handle) "
+ "REFERENCES Entries (handle) "
+ "ON DELETE CASCADE ) "
+ ";"_ns);
+}
+
+nsresult CreateUsages(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "CREATE TABLE IF NOT EXISTS Usages ( "
+ "handle BLOB PRIMARY KEY, "
+ "usage INTEGER NOT NULL DEFAULT 0, "
+ "tracked BOOLEAN NOT NULL DEFAULT 0 CHECK (tracked IN (0, 1)), "
+ "CONSTRAINT handles_are_files "
+ "FOREIGN KEY (handle) "
+ "REFERENCES Files (handle) "
+ "ON DELETE CASCADE ) "
+ ";"_ns);
+}
+
+class KeepForeignKeysOffUntilScopeExit final {
+ public:
+ explicit KeepForeignKeysOffUntilScopeExit(const ResultConnection& aConn)
+ : mConn(aConn) {}
+
+ static Result<KeepForeignKeysOffUntilScopeExit, QMResult> Create(
+ const ResultConnection& aConn) {
+ QM_TRY(
+ QM_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns)));
+ KeepForeignKeysOffUntilScopeExit result(aConn);
+ return result;
+ }
+
+ ~KeepForeignKeysOffUntilScopeExit() {
+ auto maskResult = [this]() -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(
+ mConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns)));
+
+ return Ok{};
+ };
+ QM_WARNONLY_TRY(maskResult());
+ }
+
+ private:
+ ResultConnection mConn;
+};
+
+nsresult CreateRootEntry(ResultConnection& aConn, const Origin& aOrigin) {
+ KeepForeignKeysOffUntilScopeExit foreignKeysGuard(aConn);
+
+ const nsLiteralCString createRootQuery =
+ "INSERT OR IGNORE INTO Entries "
+ "( handle, parent ) "
+ "VALUES ( :handle, NULL );"_ns;
+
+ const nsLiteralCString flagRootAsDirectoryQuery =
+ "INSERT OR IGNORE INTO Directories "
+ "( handle, name ) "
+ "VALUES ( :handle, :name );"_ns;
+
+ QM_TRY_UNWRAP(EntryId rootId,
+ data::FileSystemHashSource::GenerateHash(aOrigin, kRootString));
+
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn));
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConn, createRootQuery));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, rootId)));
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConn, flagRootAsDirectoryQuery));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, rootId)));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindNameByName("name"_ns, kRootString)));
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+ }
+
+ return transaction.Commit();
+}
+
+} // namespace
+
+nsresult SetEncoding(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(R"(PRAGMA encoding = "UTF-16";)"_ns);
+}
+
+Result<bool, QMResult> CheckIfEmpty(ResultConnection& aConn) {
+ const nsLiteralCString areThereTablesQuery =
+ "SELECT EXISTS ("
+ "SELECT 1 FROM sqlite_master "
+ ");"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConn, areThereTablesQuery));
+
+ QM_TRY_UNWRAP(bool tablesExist, stmt.YesOrNoQuery());
+
+ return !tablesExist;
+};
+
+nsresult SchemaVersion001::CreateTables(ResultConnection& aConn,
+ const Origin& aOrigin) {
+ QM_TRY(MOZ_TO_RESULT(CreateEntries(aConn)));
+ QM_TRY(MOZ_TO_RESULT(CreateDirectories(aConn)));
+ QM_TRY(MOZ_TO_RESULT(CreateFiles(aConn)));
+ QM_TRY(MOZ_TO_RESULT(CreateUsages(aConn)));
+ QM_TRY(MOZ_TO_RESULT(CreateRootEntry(aConn, aOrigin)));
+
+ return NS_OK;
+}
+
+Result<DatabaseVersion, QMResult> SchemaVersion001::InitializeConnection(
+ ResultConnection& aConn, const Origin& aOrigin) {
+ QM_TRY_UNWRAP(bool isEmpty, CheckIfEmpty(aConn));
+
+ DatabaseVersion currentVersion = 0;
+
+ if (isEmpty) {
+ QM_TRY(QM_TO_RESULT(SetEncoding(aConn)));
+ } else {
+ QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(&currentVersion)));
+ }
+
+ if (currentVersion < sVersion) {
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn));
+
+ QM_TRY(QM_TO_RESULT(SchemaVersion001::CreateTables(aConn, aOrigin)));
+ QM_TRY(QM_TO_RESULT(aConn->SetSchemaVersion(sVersion)));
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+ }
+
+ QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns)));
+
+ QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(&currentVersion)));
+
+ return currentVersion;
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/parent/datamodel/SchemaVersion001.h b/dom/fs/parent/datamodel/SchemaVersion001.h
new file mode 100644
index 0000000000..72d38e4dfd
--- /dev/null
+++ b/dom/fs/parent/datamodel/SchemaVersion001.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_
+#define DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_
+
+#include "ResultConnection.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+
+namespace mozilla::dom::fs {
+
+struct SchemaVersion001 {
+ static nsresult CreateTables(ResultConnection& aConn, const Origin& aOrigin);
+
+ static Result<DatabaseVersion, QMResult> InitializeConnection(
+ ResultConnection& aConn, const Origin& aOrigin);
+
+ static constexpr DatabaseVersion sVersion = 1;
+};
+
+nsresult SetEncoding(ResultConnection& aConn);
+
+Result<bool, QMResult> CheckIfEmpty(ResultConnection& aConn);
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_
diff --git a/dom/fs/parent/datamodel/SchemaVersion002.cpp b/dom/fs/parent/datamodel/SchemaVersion002.cpp
new file mode 100644
index 0000000000..57d4736b88
--- /dev/null
+++ b/dom/fs/parent/datamodel/SchemaVersion002.cpp
@@ -0,0 +1,618 @@
+/* -*- 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 "SchemaVersion002.h"
+
+#include "FileSystemFileManager.h"
+#include "FileSystemHashSource.h"
+#include "FileSystemHashStorageFunction.h"
+#include "ResultStatement.h"
+#include "StartedTransaction.h"
+#include "fs/FileSystemConstants.h"
+#include "mozStorageHelper.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsID.h"
+
+namespace mozilla::dom::fs {
+
+namespace {
+
+nsresult CreateFileIds(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "CREATE TABLE IF NOT EXISTS FileIds ( "
+ "fileId BLOB PRIMARY KEY, "
+ "handle BLOB, "
+ "FOREIGN KEY (handle) "
+ "REFERENCES Files (handle) "
+ "ON DELETE SET NULL ) "
+ ";"_ns);
+}
+
+nsresult CreateMainFiles(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "CREATE TABLE IF NOT EXISTS MainFiles ( "
+ "handle BLOB UNIQUE, "
+ "fileId BLOB UNIQUE, "
+ "FOREIGN KEY (handle) REFERENCES Files (handle) "
+ "ON DELETE CASCADE, "
+ "FOREIGN KEY (fileId) REFERENCES FileIds (fileId) "
+ "ON DELETE SET NULL ) "
+ ";"_ns);
+}
+
+nsresult PopulateFileIds(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "INSERT OR IGNORE INTO FileIds ( fileId, handle ) "
+ "SELECT handle, handle FROM Files "
+ ";"_ns);
+}
+
+nsresult PopulateMainFiles(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "INSERT OR IGNORE INTO MainFiles ( fileId, handle ) "
+ "SELECT handle, handle FROM Files "
+ ";"_ns);
+}
+
+Result<Ok, QMResult> ClearInvalidFileIds(
+ ResultConnection& aConn, data::FileSystemFileManager& aFileManager) {
+ // We cant't just clear all file ids because if a file was accessed using
+ // writable file stream a new file id was created which is not the same as
+ // entry id.
+
+ // Get all file ids first.
+ QM_TRY_INSPECT(
+ const auto& allFileIds,
+ ([&aConn]() -> Result<nsTArray<FileId>, QMResult> {
+ const nsLiteralCString allFileIdsQuery =
+ "SELECT fileId FROM FileIds;"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConn, allFileIdsQuery));
+
+ nsTArray<FileId> fileIds;
+
+ while (true) {
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+ if (!moreResults) {
+ break;
+ }
+
+ QM_TRY_UNWRAP(FileId fileId, stmt.GetFileIdByColumn(/* Column */ 0u));
+
+ fileIds.AppendElement(fileId);
+ }
+
+ return std::move(fileIds);
+ }()));
+
+ // Filter out file ids which have non-zero-sized files on disk.
+ QM_TRY_INSPECT(const auto& invalidFileIds,
+ ([&aFileManager](const nsTArray<FileId>& aFileIds)
+ -> Result<nsTArray<FileId>, QMResult> {
+ nsTArray<FileId> fileIds;
+
+ for (const auto& fileId : aFileIds) {
+ QM_TRY_UNWRAP(auto file, aFileManager.GetFile(fileId));
+
+ QM_TRY_INSPECT(const bool& exists,
+ QM_TO_RESULT_INVOKE_MEMBER(file, Exists));
+
+ if (exists) {
+ QM_TRY_INSPECT(
+ const int64_t& fileSize,
+ QM_TO_RESULT_INVOKE_MEMBER(file, GetFileSize));
+
+ if (fileSize != 0) {
+ continue;
+ }
+
+ QM_TRY(QM_TO_RESULT(file->Remove(false)));
+ }
+
+ fileIds.AppendElement(fileId);
+ }
+
+ return std::move(fileIds);
+ }(allFileIds)));
+
+ // Finally, clear invalid file ids.
+ QM_TRY(([&aConn](const nsTArray<FileId>& aFileIds) -> Result<Ok, QMResult> {
+ for (const auto& fileId : aFileIds) {
+ const nsLiteralCString clearFileIdsQuery =
+ "DELETE FROM FileIds "
+ "WHERE fileId = :fileId "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConn, clearFileIdsQuery));
+
+ QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, fileId)));
+
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ return Ok{};
+ }(invalidFileIds)));
+
+ return Ok{};
+}
+
+Result<Ok, QMResult> ClearInvalidMainFiles(
+ ResultConnection& aConn, data::FileSystemFileManager& aFileManager) {
+ // We cant't just clear all main files because if a file was accessed using
+ // writable file stream a new main file was created which is not the same as
+ // entry id.
+
+ // Get all main files first.
+ QM_TRY_INSPECT(
+ const auto& allMainFiles,
+ ([&aConn]() -> Result<nsTArray<std::pair<EntryId, FileId>>, QMResult> {
+ const nsLiteralCString allMainFilesQuery =
+ "SELECT handle, fileId FROM MainFiles;"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConn, allMainFilesQuery));
+
+ nsTArray<std::pair<EntryId, FileId>> mainFiles;
+
+ while (true) {
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+ if (!moreResults) {
+ break;
+ }
+
+ QM_TRY_UNWRAP(EntryId entryId,
+ stmt.GetEntryIdByColumn(/* Column */ 0u));
+ QM_TRY_UNWRAP(FileId fileId, stmt.GetFileIdByColumn(/* Column */ 1u));
+
+ mainFiles.AppendElement(std::pair<EntryId, FileId>(entryId, fileId));
+ }
+
+ return std::move(mainFiles);
+ }()));
+
+ // Filter out main files which have non-zero-sized files on disk.
+ QM_TRY_INSPECT(
+ const auto& invalidMainFiles,
+ ([&aFileManager](const nsTArray<std::pair<EntryId, FileId>>& aMainFiles)
+ -> Result<nsTArray<std::pair<EntryId, FileId>>, QMResult> {
+ nsTArray<std::pair<EntryId, FileId>> mainFiles;
+
+ for (const auto& mainFile : aMainFiles) {
+ QM_TRY_UNWRAP(auto file, aFileManager.GetFile(mainFile.second));
+
+ QM_TRY_INSPECT(const bool& exists,
+ QM_TO_RESULT_INVOKE_MEMBER(file, Exists));
+
+ if (exists) {
+ QM_TRY_INSPECT(const int64_t& fileSize,
+ QM_TO_RESULT_INVOKE_MEMBER(file, GetFileSize));
+
+ if (fileSize != 0) {
+ continue;
+ }
+
+ QM_TRY(QM_TO_RESULT(file->Remove(false)));
+ }
+
+ mainFiles.AppendElement(mainFile);
+ }
+
+ return std::move(mainFiles);
+ }(allMainFiles)));
+
+ // Finally, clear invalid main files.
+ QM_TRY(([&aConn](const nsTArray<std::pair<EntryId, FileId>>& aMainFiles)
+ -> Result<Ok, QMResult> {
+ for (const auto& mainFile : aMainFiles) {
+ const nsLiteralCString clearMainFilesQuery =
+ "DELETE FROM MainFiles "
+ "WHERE handle = :entryId AND fileId = :fileId "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConn, clearMainFilesQuery));
+
+ QM_TRY(
+ QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, mainFile.first)));
+ QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, mainFile.second)));
+
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ return Ok{};
+ }(invalidMainFiles)));
+
+ return Ok{};
+}
+
+nsresult ConnectUsagesToFileIds(ResultConnection& aConn) {
+ QM_TRY(
+ MOZ_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns)));
+
+ auto turnForeignKeysBackOn = MakeScopeExit([&aConn]() {
+ QM_WARNONLY_TRY(
+ MOZ_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns)));
+ });
+
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn));
+
+ QM_TRY(MOZ_TO_RESULT(
+ aConn->ExecuteSimpleSQL("DROP TABLE IF EXISTS migrateUsages ;"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConn->ExecuteSimpleSQL(
+ "CREATE TABLE migrateUsages ( "
+ "handle BLOB PRIMARY KEY, "
+ "usage INTEGER NOT NULL DEFAULT 0, "
+ "tracked BOOLEAN NOT NULL DEFAULT 0 CHECK (tracked IN (0, 1)), "
+ "CONSTRAINT handles_are_fileIds "
+ "FOREIGN KEY (handle) "
+ "REFERENCES FileIds (fileId) "
+ "ON DELETE CASCADE ) "
+ ";"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConn->ExecuteSimpleSQL(
+ "INSERT INTO migrateUsages ( handle, usage, tracked ) "
+ "SELECT handle, usage, tracked FROM Usages ;"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConn->ExecuteSimpleSQL("DROP TABLE Usages;"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConn->ExecuteSimpleSQL(
+ "ALTER TABLE migrateUsages RENAME TO Usages;"_ns)));
+
+ QM_TRY(
+ MOZ_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_key_check;"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
+
+ return NS_OK;
+}
+
+nsresult CreateEntryNamesView(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "CREATE VIEW IF NOT EXISTS EntryNames AS "
+ "SELECT isFile, handle, parent, name FROM Entries INNER JOIN ( "
+ "SELECT 1 AS isFile, handle, name FROM Files UNION "
+ "SELECT 0, handle, name FROM Directories ) "
+ "USING (handle) "
+ ";"_ns);
+}
+
+nsresult FixEntryIds(const ResultConnection& aConnection,
+ const EntryId& aRootEntry) {
+ const nsLiteralCString calculateHashesQuery =
+ "CREATE TEMPORARY TABLE EntryMigrationTable AS "
+ "WITH RECURSIVE "
+ "rehashMap( depth, isFile, handle, parent, name, hash ) AS ( "
+ "SELECT 0, isFile, handle, parent, name, hashEntry( :rootEntry, name ) "
+ "FROM EntryNames WHERE parent = :rootEntry UNION SELECT "
+ "1 + depth, EntryNames.isFile, EntryNames.handle, EntryNames.parent, "
+ "EntryNames.name, hashEntry( rehashMap.hash, EntryNames.name ) "
+ "FROM rehashMap, EntryNames WHERE rehashMap.handle = EntryNames.parent ) "
+ "SELECT depth, isFile, handle, parent, name, hash FROM rehashMap "
+ ";"_ns;
+
+ const nsLiteralCString createIndexByDepthQuery =
+ "CREATE INDEX indexOnDepth ON EntryMigrationTable ( depth ); "_ns;
+
+ // To avoid constraint violation, new entries are inserted under a temporary
+ // parent.
+
+ const nsLiteralCString insertTemporaryParentEntry =
+ "INSERT INTO Entries ( handle, parent ) "
+ "VALUES ( :tempParent, :rootEntry ) ;"_ns;
+
+ const nsLiteralCString flagTemporaryParentAsDir =
+ "INSERT INTO Directories ( handle, name ) "
+ "VALUES ( :tempParent, 'temp' ) ;"_ns;
+
+ const nsLiteralCString insertNewEntriesQuery =
+ "INSERT INTO Entries ( handle, parent ) "
+ "SELECT hash, :tempParent FROM EntryMigrationTable WHERE hash != handle "
+ ";"_ns;
+
+ const nsLiteralCString insertNewDirectoriesQuery =
+ "INSERT INTO Directories ( handle, name ) "
+ "SELECT hash, name FROM EntryMigrationTable "
+ "WHERE isFile = 0 AND hash != handle "
+ "ORDER BY depth "
+ ";"_ns;
+
+ const nsLiteralCString insertNewFilesQuery =
+ "INSERT INTO Files ( handle, type, name ) "
+ "SELECT EntryMigrationTable.hash, Files.type, EntryMigrationTable.name "
+ "FROM EntryMigrationTable INNER JOIN Files USING (handle) "
+ "WHERE EntryMigrationTable.isFile = 1 AND hash != handle "
+ ";"_ns;
+
+ const nsLiteralCString updateFileMappingsQuery =
+ "UPDATE FileIds SET handle = hash "
+ "FROM ( SELECT handle, hash FROM EntryMigrationTable WHERE hash != "
+ "handle ) "
+ "AS replacement WHERE FileIds.handle = replacement.handle "
+ ";"_ns;
+
+ const nsLiteralCString updateMainFilesQuery =
+ "UPDATE MainFiles SET handle = hash "
+ "FROM ( SELECT handle, hash FROM EntryMigrationTable WHERE hash != "
+ "handle ) "
+ "AS replacement WHERE MainFiles.handle = replacement.handle "
+ ";"_ns;
+
+ // Now fix the parents.
+ const nsLiteralCString updateEntryMappingsQuery =
+ "UPDATE Entries SET parent = hash "
+ "FROM ( SELECT Lhs.hash AS handle, Rhs.hash AS hash, Lhs.depth AS depth "
+ "FROM EntryMigrationTable AS Lhs "
+ "INNER JOIN EntryMigrationTable AS Rhs "
+ "ON Rhs.handle = Lhs.parent ORDER BY depth ) AS replacement "
+ "WHERE Entries.handle = replacement.handle "
+ "AND Entries.parent = :tempParent "
+ ";"_ns;
+
+ const nsLiteralCString cleanupOldEntriesQuery =
+ "DELETE FROM Entries WHERE handle IN "
+ "( SELECT handle FROM EntryMigrationTable WHERE hash != handle ) "
+ ";"_ns;
+
+ const nsLiteralCString cleanupTemporaryParent =
+ "DELETE FROM Entries WHERE handle = :tempParent ;"_ns;
+
+ const nsLiteralCString dropIndexByDepthQuery =
+ "DROP INDEX indexOnDepth ; "_ns;
+
+ // Index is automatically deleted
+ const nsLiteralCString cleanupTemporaries =
+ "DROP TABLE EntryMigrationTable ;"_ns;
+
+ EntryId tempParent(nsCString(nsID::GenerateUUID().ToString().get()));
+
+ nsCOMPtr<mozIStorageFunction> rehashFunction =
+ new data::FileSystemHashStorageFunction();
+ QM_TRY(MOZ_TO_RESULT(aConnection->CreateFunction("hashEntry"_ns,
+ /* number of arguments */ 2,
+ rehashFunction)));
+ auto finallyRemoveFunction = MakeScopeExit([&aConnection]() {
+ QM_WARNONLY_TRY(MOZ_TO_RESULT(aConnection->RemoveFunction("hashEntry"_ns)));
+ });
+
+ // We need this to make sure the old entries get removed
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns)));
+
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConnection));
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, calculateHashesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("rootEntry"_ns, aRootEntry)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(aConnection->ExecuteSimpleSQL(createIndexByDepthQuery)));
+
+ {
+ QM_TRY_UNWRAP(
+ ResultStatement stmt,
+ ResultStatement::Create(aConnection, insertTemporaryParentEntry));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent)));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("rootEntry"_ns, aRootEntry)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(
+ ResultStatement stmt,
+ ResultStatement::Create(aConnection, flagTemporaryParentAsDir));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, insertNewEntriesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(
+ ResultStatement stmt,
+ ResultStatement::Create(aConnection, insertNewDirectoriesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, insertNewFilesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(
+ ResultStatement stmt,
+ ResultStatement::Create(aConnection, updateFileMappingsQuery));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, updateMainFilesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(
+ ResultStatement stmt,
+ ResultStatement::Create(aConnection, updateEntryMappingsQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, cleanupOldEntriesQuery));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, cleanupTemporaryParent));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(aConnection->ExecuteSimpleSQL(dropIndexByDepthQuery)));
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, cleanupTemporaries));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ QM_WARNONLY_TRY(QM_TO_RESULT(aConnection->ExecuteSimpleSQL("VACUUM;"_ns)));
+
+ return NS_OK;
+}
+
+} // namespace
+
+Result<DatabaseVersion, QMResult> SchemaVersion002::InitializeConnection(
+ ResultConnection& aConn, data::FileSystemFileManager& aFileManager,
+ const Origin& aOrigin) {
+ QM_TRY_UNWRAP(const bool wasEmpty, CheckIfEmpty(aConn));
+
+ DatabaseVersion currentVersion = 0;
+
+ if (wasEmpty) {
+ QM_TRY(QM_TO_RESULT(SetEncoding(aConn)));
+ } else {
+ QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(&currentVersion)));
+ }
+
+ if (currentVersion < sVersion) {
+ MOZ_ASSERT_IF(0 != currentVersion, 1 == currentVersion);
+
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn));
+
+ if (0 == currentVersion) {
+ QM_TRY(QM_TO_RESULT(SchemaVersion001::CreateTables(aConn, aOrigin)));
+ }
+
+ QM_TRY(QM_TO_RESULT(CreateFileIds(aConn)));
+
+ if (!wasEmpty) {
+ QM_TRY(QM_TO_RESULT(PopulateFileIds(aConn)));
+ }
+
+ QM_TRY(QM_TO_RESULT(ConnectUsagesToFileIds(aConn)));
+
+ QM_TRY(QM_TO_RESULT(CreateMainFiles(aConn)));
+ if (!wasEmpty) {
+ QM_TRY(QM_TO_RESULT(PopulateMainFiles(aConn)));
+ }
+
+ QM_TRY(QM_TO_RESULT(CreateEntryNamesView(aConn)));
+
+ QM_TRY(QM_TO_RESULT(aConn->SetSchemaVersion(sVersion)));
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ if (!wasEmpty) {
+ QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL("VACUUM;"_ns)));
+ }
+ }
+
+ // The upgrade from version 1 to version 2 was buggy, so we have to check if
+ // the Usages table still references the Files table which is a sign that
+ // the upgrade wasn't complete. This extra query has only negligible perf
+ // impact. See bug 1847989.
+ auto UsagesTableRefsFilesTable = [&aConn]() -> Result<bool, QMResult> {
+ const nsLiteralCString query =
+ "SELECT pragma_foreign_key_list.'table'=='Files' "
+ "FROM pragma_foreign_key_list('Usages');"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt, ResultStatement::Create(aConn, query));
+
+ return stmt.YesOrNoQuery();
+ };
+
+ QM_TRY_UNWRAP(auto usagesTableRefsFilesTable, UsagesTableRefsFilesTable());
+
+ if (usagesTableRefsFilesTable) {
+ QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn));
+
+ // The buggy upgrade didn't call PopulateFileIds, ConnectUsagesToFileIds
+ // and PopulateMainFiles was completely missing. Since invalid file ids
+ // and main files could be inserted when the profile was broken, we need
+ // to clear them before populating.
+ QM_TRY(ClearInvalidFileIds(aConn, aFileManager));
+ QM_TRY(QM_TO_RESULT(PopulateFileIds(aConn)));
+ QM_TRY(QM_TO_RESULT(ConnectUsagesToFileIds(aConn)));
+ QM_TRY(ClearInvalidMainFiles(aConn, aFileManager));
+ QM_TRY(QM_TO_RESULT(PopulateMainFiles(aConn)));
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL("VACUUM;"_ns)));
+
+ QM_TRY_UNWRAP(usagesTableRefsFilesTable, UsagesTableRefsFilesTable());
+ MOZ_ASSERT(!usagesTableRefsFilesTable);
+ }
+
+ // In schema version 001, entryId was unique but not necessarily related to
+ // a path. For schema 002, we have to fix all entryIds to be derived from
+ // the underlying path.
+ auto OneTimeRehashingDone = [&aConn]() -> Result<bool, QMResult> {
+ const nsLiteralCString query =
+ "SELECT EXISTS (SELECT 1 FROM sqlite_master "
+ "WHERE type='table' AND name='RehashedFrom001to002' ) ;"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt, ResultStatement::Create(aConn, query));
+
+ return stmt.YesOrNoQuery();
+ };
+
+ QM_TRY_UNWRAP(auto oneTimeRehashingDone, OneTimeRehashingDone());
+
+ if (!oneTimeRehashingDone) {
+ const nsLiteralCString findRootEntry =
+ "SELECT handle FROM Entries WHERE parent IS NULL ;"_ns;
+
+ EntryId rootId;
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConn, findRootEntry));
+
+ QM_TRY_UNWRAP(DebugOnly<bool> moreResults, stmt.ExecuteStep());
+ MOZ_ASSERT(moreResults);
+
+ QM_TRY_UNWRAP(rootId, stmt.GetEntryIdByColumn(/* Column */ 0u));
+ }
+
+ MOZ_ASSERT(!rootId.IsEmpty());
+
+ QM_TRY(QM_TO_RESULT(FixEntryIds(aConn, rootId)));
+
+ QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL(
+ "CREATE TABLE RehashedFrom001to002 (id INTEGER PRIMARY KEY);"_ns)));
+
+ QM_TRY_UNWRAP(DebugOnly<bool> isDoneNow, OneTimeRehashingDone());
+ MOZ_ASSERT(isDoneNow);
+ }
+
+ QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns)));
+
+ QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(&currentVersion)));
+
+ return currentVersion;
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/parent/datamodel/SchemaVersion002.h b/dom/fs/parent/datamodel/SchemaVersion002.h
new file mode 100644
index 0000000000..60f2aa53cb
--- /dev/null
+++ b/dom/fs/parent/datamodel/SchemaVersion002.h
@@ -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/. */
+
+#ifndef DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION002_H_
+#define DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION002_H_
+
+#include "SchemaVersion001.h"
+
+namespace mozilla::dom::fs {
+
+namespace data {
+class FileSystemFileManager;
+} // namespace data
+
+struct SchemaVersion002 {
+ static Result<DatabaseVersion, QMResult> InitializeConnection(
+ ResultConnection& aConn, data::FileSystemFileManager& aFileManager,
+ const Origin& aOrigin);
+
+ static constexpr DatabaseVersion sVersion = 2;
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION002_H_
diff --git a/dom/fs/parent/datamodel/moz.build b/dom/fs/parent/datamodel/moz.build
new file mode 100644
index 0000000000..8bb5d35381
--- /dev/null
+++ b/dom/fs/parent/datamodel/moz.build
@@ -0,0 +1,28 @@
+# -*- 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 += [
+ "FileSystemDataManager.h",
+]
+
+UNIFIED_SOURCES += [
+ "FileSystemDatabaseManager.cpp",
+ "FileSystemDatabaseManagerVersion001.cpp",
+ "FileSystemDatabaseManagerVersion002.cpp",
+ "FileSystemDataManager.cpp",
+ "FileSystemFileManager.cpp",
+ "SchemaVersion001.cpp",
+ "SchemaVersion002.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/fs/include",
+ "/dom/fs/parent",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/fs/parent/moz.build b/dom/fs/parent/moz.build
new file mode 100644
index 0000000000..95298ad0f2
--- /dev/null
+++ b/dom/fs/parent/moz.build
@@ -0,0 +1,63 @@
+# -*- 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/.
+
+DIRS += [
+ "datamodel",
+]
+
+EXPORTS.mozilla.dom += [
+ "FileSystemAccessHandle.h",
+ "FileSystemAccessHandleControlParent.h",
+ "FileSystemAccessHandleParent.h",
+ "FileSystemManagerParent.h",
+ "FileSystemManagerParentFactory.h",
+ "FileSystemParentTypes.h",
+ "FileSystemQuotaClient.h",
+ "FileSystemQuotaClientFactory.h",
+ "FileSystemWritableFileStreamParent.h",
+]
+
+UNIFIED_SOURCES += [
+ "FileSystemAccessHandle.cpp",
+ "FileSystemAccessHandleControlParent.cpp",
+ "FileSystemAccessHandleParent.cpp",
+ "FileSystemContentTypeGuess.cpp",
+ "FileSystemHashSource.cpp",
+ "FileSystemHashStorageFunction.cpp",
+ "FileSystemManagerParent.cpp",
+ "FileSystemManagerParentFactory.cpp",
+ "FileSystemQuotaClient.cpp",
+ "FileSystemQuotaClientFactory.cpp",
+ "FileSystemStreamCallbacks.cpp",
+ "FileSystemWritableFileStreamParent.cpp",
+ "ResultStatement.cpp",
+ "StartedTransaction.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/fs/include",
+ "/dom/fs/parent/datamodel",
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+if CONFIG["COMPILE_ENVIRONMENT"]:
+ CbindgenHeader(
+ "data_encoding_ffi_generated.h",
+ inputs=["/dom/fs/parent/rust/data-encoding-ffi"],
+ )
+
+ CbindgenHeader(
+ "mime_guess_ffi_generated.h",
+ inputs=["/dom/fs/parent/rust/mime-guess-ffi"],
+ )
+
+ EXPORTS.mozilla.dom += [
+ "!data_encoding_ffi_generated.h",
+ "!mime_guess_ffi_generated.h",
+ ]
diff --git a/dom/fs/parent/rust/data-encoding-ffi/Cargo.toml b/dom/fs/parent/rust/data-encoding-ffi/Cargo.toml
new file mode 100644
index 0000000000..1a641e5c7b
--- /dev/null
+++ b/dom/fs/parent/rust/data-encoding-ffi/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "data-encoding-ffi"
+version = "0.1.0"
+license = "MPL-2.0"
+authors = ["The Mozilla Project Developers"]
+
+[dependencies]
+data-encoding = "2.2.1"
+nsstring = { path = "../../../../../xpcom/rust/nsstring" }
diff --git a/dom/fs/parent/rust/data-encoding-ffi/cbindgen.toml b/dom/fs/parent/rust/data-encoding-ffi/cbindgen.toml
new file mode 100644
index 0000000000..37da12d3b7
--- /dev/null
+++ b/dom/fs/parent/rust/data-encoding-ffi/cbindgen.toml
@@ -0,0 +1,11 @@
+header = """/* 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/. */"""
+autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */"""
+include_guard = "DOM_FS_PARENT_RUST_DATA_ENCODING_FFI_H_"
+include_version = true
+braces = "SameLine"
+line_length = 100
+tab_width = 2
+language = "C++"
+namespaces = ["mozilla", "dom", "fs"]
diff --git a/dom/fs/parent/rust/data-encoding-ffi/src/lib.rs b/dom/fs/parent/rust/data-encoding-ffi/src/lib.rs
new file mode 100644
index 0000000000..74900b62ae
--- /dev/null
+++ b/dom/fs/parent/rust/data-encoding-ffi/src/lib.rs
@@ -0,0 +1,14 @@
+/* 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/. */
+
+extern crate data_encoding;
+extern crate nsstring;
+
+use data_encoding::BASE32;
+use nsstring::{nsACString, nsCString};
+
+#[no_mangle]
+pub extern "C" fn base32encode(unencoded: &nsACString, encoded: &mut nsCString) {
+ encoded.assign(&BASE32.encode(&unencoded[..]));
+}
diff --git a/dom/fs/parent/rust/mime-guess-ffi/Cargo.toml b/dom/fs/parent/rust/mime-guess-ffi/Cargo.toml
new file mode 100644
index 0000000000..d97f8202d7
--- /dev/null
+++ b/dom/fs/parent/rust/mime-guess-ffi/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "mime-guess-ffi"
+version = "0.1.0"
+license = "MPL-2.0"
+authors = ["The Mozilla Project Developers"]
+
+[dependencies]
+mime_guess = "2.0.4"
+nserror = { path = "../../../../../xpcom/rust/nserror" }
+nsstring = { path = "../../../../../xpcom/rust/nsstring" }
diff --git a/dom/fs/parent/rust/mime-guess-ffi/cbindgen.toml b/dom/fs/parent/rust/mime-guess-ffi/cbindgen.toml
new file mode 100644
index 0000000000..d7f94927ab
--- /dev/null
+++ b/dom/fs/parent/rust/mime-guess-ffi/cbindgen.toml
@@ -0,0 +1,11 @@
+header = """/* 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/. */"""
+autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */"""
+include_guard = "DOM_FS_PARENT_RUST_MIME_GUESS_FFI_H_"
+include_version = true
+braces = "SameLine"
+line_length = 100
+tab_width = 2
+language = "C++"
+namespaces = ["mozilla", "dom", "fs"]
diff --git a/dom/fs/parent/rust/mime-guess-ffi/src/lib.rs b/dom/fs/parent/rust/mime-guess-ffi/src/lib.rs
new file mode 100644
index 0000000000..19833fd202
--- /dev/null
+++ b/dom/fs/parent/rust/mime-guess-ffi/src/lib.rs
@@ -0,0 +1,34 @@
+/* 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/. */
+
+extern crate mime_guess;
+extern crate nserror;
+extern crate nsstring;
+
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_ERROR_NOT_AVAILABLE, NS_OK};
+use nsstring::{nsACString, nsCString};
+use std::path::Path;
+use std::str;
+
+#[no_mangle]
+pub extern "C" fn mimeGuessFromPath(path: &nsACString, content_type: &mut nsCString) -> nsresult {
+ let path_data = str::from_utf8(path.as_ref());
+ if path_data.is_err() {
+ return NS_ERROR_INVALID_ARG; // Not UTF8
+ }
+
+ let content_path = Path::new(path_data.unwrap());
+ if content_path.extension().is_none() {
+ return NS_ERROR_NOT_AVAILABLE; // No mime type information
+ }
+
+ let maybe_mime = mime_guess::from_path(content_path).first_raw();
+ if maybe_mime.is_none() {
+ return NS_ERROR_FAILURE; // Not recognized
+ }
+
+ content_type.assign(maybe_mime.unwrap());
+
+ NS_OK
+}
diff --git a/dom/fs/shared/FileSystemHelpers.cpp b/dom/fs/shared/FileSystemHelpers.cpp
new file mode 100644
index 0000000000..24f1fc7fb8
--- /dev/null
+++ b/dom/fs/shared/FileSystemHelpers.cpp
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemHelpers.h"
+
+#include "nsString.h"
+
+namespace mozilla::dom::fs {
+
+bool IsValidName(const mozilla::dom::fs::Name& aName) {
+ return !(aName.IsVoid() || aName.Length() == 0 ||
+#ifdef XP_WIN
+ aName.FindChar('\\') != kNotFound ||
+#endif
+ aName.FindChar('/') != kNotFound || aName.EqualsLiteral(".") ||
+ aName.EqualsLiteral(".."));
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/shared/FileSystemHelpers.h b/dom/fs/shared/FileSystemHelpers.h
new file mode 100644
index 0000000000..26a820f343
--- /dev/null
+++ b/dom/fs/shared/FileSystemHelpers.h
@@ -0,0 +1,147 @@
+/* -*- 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_FS_SHARED_FILESYSTEMHELPERS_H_
+#define DOM_FS_SHARED_FILESYSTEMHELPERS_H_
+
+#include "FileSystemTypes.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla::dom::fs {
+
+// XXX Consider moving this class template to MFBT.
+
+// A wrapper class template on top of the RefPtr. The RefPtr provides us the
+// automatic reference counting of objects with AddRef() and Release() methods.
+// `Registered` provides automatic registration counting of objects with
+// Register() and Unregister() methods. Registration counting works similarly
+// as reference counting, but objects are not deleted when the number of
+// registrations drops to zero (that's managed by reference counting). Instead,
+// an object can trigger an asynchronous close operation which still needs to
+// hold and use the referenced object. Example:
+//
+// using BoolPromise = MozPromise<bool, nsresult, false>;
+//
+// class MyObject {
+// public:
+// NS_INLINE_DECL_REFCOUNTING(MyObject)
+//
+// void Register() {
+// mRegCnt++;
+// }
+//
+// void Unregister() {
+// mRegCnt--;
+// if (mRegCnt == 0) {
+// BeginClose();
+// }
+// }
+//
+// private:
+// RefPtr<BoolPromise> BeginClose() {
+// return InvokeAsync(mIOTaskQueue, __func__,
+// []() {
+// return BoolPromise::CreateAndResolve(true, __func__);
+// })
+// ->Then(GetCurrentSerialEventTarget(), __func__,
+// [self = RefPtr<MyObject>(this)](
+// const BoolPromise::ResolveOrRejectValue&) {
+// return self->mIOTaskQueue->BeginShutdown();
+// })
+// ->Then(GetCurrentSerialEventTarget(), __func__,
+// [self = RefPtr<MyObject>(this)](
+// const ShutdownPromise::ResolveOrRejectValue&) {
+// return BoolPromise::CreateAndResolve(true, __func__);
+// });
+// }
+//
+// RefPtr<TaskQueue> mIOTaskQueue;
+// uint32_t mRegCnt = 0;
+// };
+
+template <class T>
+class Registered {
+ private:
+ RefPtr<T> mObject;
+
+ public:
+ ~Registered() {
+ if (mObject) {
+ mObject->Unregister();
+ }
+ }
+
+ Registered() = default;
+
+ Registered(const Registered& aOther) : mObject(aOther.mObject) {
+ mObject->Register();
+ }
+
+ Registered(Registered&& aOther) noexcept = default;
+
+ MOZ_IMPLICIT Registered(RefPtr<T> aObject) : mObject(std::move(aObject)) {
+ if (mObject) {
+ mObject->Register();
+ }
+ }
+
+ Registered<T>& operator=(decltype(nullptr)) {
+ RefPtr<T> oldObject = std::move(mObject);
+ mObject = nullptr;
+ if (oldObject) {
+ oldObject->Unregister();
+ }
+ return *this;
+ }
+
+ Registered<T>& operator=(const Registered<T>& aRhs) {
+ if (aRhs.mObject) {
+ aRhs.mObject->Register();
+ }
+ RefPtr<T> oldObject = std::move(mObject);
+ mObject = aRhs.mObject;
+ if (oldObject) {
+ oldObject->Unregister();
+ }
+ return *this;
+ }
+
+ Registered<T>& operator=(Registered<T>&& aRhs) noexcept {
+ RefPtr<T> oldObject = std::move(mObject);
+ mObject = std::move(aRhs.mObject);
+ aRhs.mObject = nullptr;
+ if (oldObject) {
+ oldObject->Unregister();
+ }
+ return *this;
+ }
+
+ const RefPtr<T>& inspect() const { return mObject; }
+
+ RefPtr<T> unwrap() {
+ RefPtr<T> oldObject = std::move(mObject);
+ mObject = nullptr;
+ if (oldObject) {
+ oldObject->Unregister();
+ }
+ return oldObject;
+ }
+
+ T* get() const { return mObject; }
+
+ operator T*() const& { return get(); }
+
+ T* operator->() const { return get(); }
+};
+
+// Spec says valid names don't include (os-dependent) path separators,
+// and is not equal to a dot . or two dots ..
+// We want to use the same validator from both child and parent.
+bool IsValidName(const fs::Name& aName);
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_SHARED_FILESYSTEMHELPERS_H_
diff --git a/dom/fs/shared/FileSystemLog.cpp b/dom/fs/shared/FileSystemLog.cpp
new file mode 100644
index 0000000000..91afb3fc8f
--- /dev/null
+++ b/dom/fs/shared/FileSystemLog.cpp
@@ -0,0 +1,13 @@
+/* -*- 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 "FileSystemLog.h"
+
+namespace mozilla {
+
+LazyLogModule gOPFSLog("OPFS");
+
+}
diff --git a/dom/fs/shared/FileSystemLog.h b/dom/fs/shared/FileSystemLog.h
new file mode 100644
index 0000000000..6ea6d4ae77
--- /dev/null
+++ b/dom/fs/shared/FileSystemLog.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_FS_SHARED_FILESYSTEMLOG_H_
+#define DOM_FS_SHARED_FILESYSTEMLOG_H_
+
+#include "mozilla/Logging.h"
+
+namespace mozilla {
+extern LazyLogModule gOPFSLog;
+}
+
+#define LOG(args) MOZ_LOG(mozilla::gOPFSLog, mozilla::LogLevel::Debug, args)
+
+#define LOG_VERBOSE(args) \
+ MOZ_LOG(mozilla::gOPFSLog, mozilla::LogLevel::Verbose, args)
+
+#define LOG_ENABLED() MOZ_LOG_TEST(mozilla::gOPFSLog, mozilla::LogLevel::Debug)
+
+#endif // DOM_FS_SHARED_FILESYSTEMLOG_H
diff --git a/dom/fs/shared/FileSystemTypes.h b/dom/fs/shared/FileSystemTypes.h
new file mode 100644
index 0000000000..3841698c71
--- /dev/null
+++ b/dom/fs/shared/FileSystemTypes.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_FILESYSTEMTYPES_H_
+#define DOM_FS_FILESYSTEMTYPES_H_
+
+#include "nsStringFwd.h"
+
+template <class T>
+class nsTArray;
+
+namespace mozilla::dom::fs {
+
+using ContentType = nsCString;
+using DatabaseVersion = int32_t;
+using EntryId = nsCString;
+using Name = nsString;
+using Origin = nsCString;
+using PageNumber = int32_t;
+using Path = nsTArray<Name>;
+using TimeStamp = int64_t;
+using Usage = int64_t;
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_FILESYSTEMTYPES_H_
diff --git a/dom/fs/shared/IPCRejectReporter.cpp b/dom/fs/shared/IPCRejectReporter.cpp
new file mode 100644
index 0000000000..6f5f40b95c
--- /dev/null
+++ b/dom/fs/shared/IPCRejectReporter.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 "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/ipc/MessageChannel.h"
+
+namespace mozilla::dom::fs {
+
+// TODO: Find a better way to deal with these errors
+void IPCRejectReporter(mozilla::ipc::ResponseRejectReason aReason) {
+ switch (aReason) {
+ case mozilla::ipc::ResponseRejectReason::ActorDestroyed:
+ // This is ok
+ break;
+ case mozilla::ipc::ResponseRejectReason::HandlerRejected:
+ QM_TRY(OkIf(false), QM_VOID);
+ break;
+ case mozilla::ipc::ResponseRejectReason::ChannelClosed:
+ QM_TRY(OkIf(false), QM_VOID);
+ break;
+ case mozilla::ipc::ResponseRejectReason::ResolverDestroyed:
+ QM_TRY(OkIf(false), QM_VOID);
+ break;
+ case mozilla::ipc::ResponseRejectReason::SendError:
+ QM_TRY(OkIf(false), QM_VOID);
+ break;
+ default:
+ QM_TRY(OkIf(false), QM_VOID);
+ break;
+ }
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/shared/IPCRejectReporter.h b/dom/fs/shared/IPCRejectReporter.h
new file mode 100644
index 0000000000..db98ac917c
--- /dev/null
+++ b/dom/fs/shared/IPCRejectReporter.h
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+namespace mozilla {
+
+namespace ipc {
+
+enum class ResponseRejectReason;
+
+} // namespace ipc
+
+namespace dom::fs {
+
+void IPCRejectReporter(mozilla::ipc::ResponseRejectReason aReason);
+
+} // namespace dom::fs
+} // namespace mozilla
diff --git a/dom/fs/shared/ManagedMozPromiseRequestHolder.h b/dom/fs/shared/ManagedMozPromiseRequestHolder.h
new file mode 100644
index 0000000000..1b039577a3
--- /dev/null
+++ b/dom/fs/shared/ManagedMozPromiseRequestHolder.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_FS_SHARED_MANAGEDMOZPROMISEREQUESTHOLDER_H_
+#define DOM_FS_SHARED_MANAGEDMOZPROMISEREQUESTHOLDER_H_
+
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla::dom::fs {
+
+template <typename Manager, typename PromiseType>
+class ManagedMozPromiseRequestHolder final
+ : public MozPromiseRequestHolder<PromiseType> {
+ public:
+ explicit ManagedMozPromiseRequestHolder(Manager* aManager)
+ : mManager(aManager) {
+ mManager->RegisterPromiseRequestHolder(this);
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(ManagedMozPromiseRequestHolder)
+
+ private:
+ ~ManagedMozPromiseRequestHolder() {
+ mManager->UnregisterPromiseRequestHolder(this);
+ }
+
+ RefPtr<Manager> mManager;
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_SHARED_MANAGEDMOZPROMISEREQUESTHOLDER_H_
diff --git a/dom/fs/shared/PFileSystemAccessHandle.ipdl b/dom/fs/shared/PFileSystemAccessHandle.ipdl
new file mode 100644
index 0000000000..164f5f9062
--- /dev/null
+++ b/dom/fs/shared/PFileSystemAccessHandle.ipdl
@@ -0,0 +1,21 @@
+/* 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 PFileSystemManager;
+
+namespace mozilla {
+namespace dom {
+
+async protocol PFileSystemAccessHandle
+{
+ manager PFileSystemManager;
+
+ parent:
+ async Close();
+
+ async __delete__();
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/fs/shared/PFileSystemAccessHandleControl.ipdl b/dom/fs/shared/PFileSystemAccessHandleControl.ipdl
new file mode 100644
index 0000000000..d74c7c4b4e
--- /dev/null
+++ b/dom/fs/shared/PFileSystemAccessHandleControl.ipdl
@@ -0,0 +1,19 @@
+/* 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/. */
+
+using struct mozilla::void_t from "mozilla/ipc/IPCCore.h";
+
+namespace mozilla {
+namespace dom {
+
+[ChildProc=anydom]
+async protocol PFileSystemAccessHandleControl
+{
+ parent:
+ async Close()
+ returns(void_t ok);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/fs/shared/PFileSystemManager.ipdl b/dom/fs/shared/PFileSystemManager.ipdl
new file mode 100644
index 0000000000..9058820fd2
--- /dev/null
+++ b/dom/fs/shared/PFileSystemManager.ipdl
@@ -0,0 +1,413 @@
+/* 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 PFileSystemAccessHandle;
+include protocol PFileSystemAccessHandleControl;
+include protocol PFileSystemWritableFileStream;
+
+include IPCBlob;
+include RandomAccessStreamParams;
+
+using mozilla::dom::fs::ContentType from "mozilla/dom/FileSystemTypes.h";
+using mozilla::dom::fs::EntryId from "mozilla/dom/FileSystemTypes.h";
+using mozilla::dom::fs::Name from "mozilla/dom/FileSystemTypes.h";
+using mozilla::dom::fs::Origin from "mozilla/dom/FileSystemTypes.h";
+using mozilla::dom::fs::PageNumber from "mozilla/dom/FileSystemTypes.h";
+using mozilla::dom::fs::TimeStamp from "mozilla/dom/FileSystemTypes.h";
+using struct mozilla::void_t from "mozilla/ipc/IPCCore.h";
+
+namespace mozilla {
+namespace dom {
+namespace fs {
+
+/**
+ * Identifies a file or a directory and contains its user provided name.
+ */
+struct FileSystemEntryMetadata
+{
+ EntryId entryId;
+ Name entryName;
+ bool directory;
+};
+
+/**
+ * Identifies a file or a directory with its parent identifier and
+ * user provided name.
+ */
+struct FileSystemChildMetadata
+{
+ EntryId parentId;
+ Name childName;
+};
+
+/**
+ * Identifies a file with its parent directory and name, and
+ * indicates whether the file may be created if it is missing.
+ */
+struct FileSystemGetHandleRequest
+{
+ FileSystemChildMetadata handle;
+ bool create;
+};
+
+/**
+ * Contains a file or directory or an error.
+ */
+union FileSystemGetHandleResponse
+{
+ nsresult;
+ EntryId;
+};
+
+/**
+ * Contains an identifier for a parent directory and a page number
+ * which is used to fetch the next set of entries when the directory
+ * contains so many items that communicating all of them in one message
+ * is an impractical.
+ */
+struct FileSystemGetEntriesRequest
+{
+ EntryId parentId;
+ PageNumber page;
+};
+
+/**
+ * Contains a set of directories and files
+ * under the same parent directory.
+ */
+struct FileSystemDirectoryListing
+{
+ FileSystemEntryMetadata[] directories;
+ FileSystemEntryMetadata[] files;
+};
+
+/**
+ * Contains a set of entries or an error.
+ */
+union FileSystemGetEntriesResponse
+{
+ nsresult;
+ FileSystemDirectoryListing;
+};
+
+/**
+ * Contains entry handle information.
+ */
+struct FileSystemGetFileRequest
+{
+ EntryId entryId;
+};
+
+/**
+ * Contains the properties of a file and a file descriptor.
+ * The properties may differ from the properties of the
+ * underlying object of the file descriptor.
+ */
+struct FileSystemFileProperties
+{
+ TimeStamp last_modified_ms;
+ IPCBlob file;
+ ContentType type;
+ Name[] path;
+};
+
+/**
+ * Contains file properties or an error.
+ */
+union FileSystemGetFileResponse
+{
+ nsresult;
+ FileSystemFileProperties;
+};
+
+/**
+ * Contains entry handle information.
+ */
+struct FileSystemGetAccessHandleRequest
+{
+ EntryId entryId;
+};
+
+struct FileSystemAccessHandleProperties
+{
+ RandomAccessStreamParams streamParams;
+ ManagedEndpoint<PFileSystemAccessHandleChild> accessHandleChildEndpoint;
+ Endpoint<PFileSystemAccessHandleControlChild> accessHandleControlChildEndpoint;
+};
+
+union FileSystemGetAccessHandleResponse
+{
+ nsresult;
+ FileSystemAccessHandleProperties;
+};
+
+/**
+ * Contains entry handle information.
+ */
+struct FileSystemGetWritableRequest
+{
+ EntryId entryId;
+ bool keepData;
+};
+
+struct FileSystemWritableFileStreamProperties
+{
+ RandomAccessStreamParams streamParams;
+ PFileSystemWritableFileStream writableFileStream;
+};
+
+union FileSystemGetWritableFileStreamResponse
+{
+ nsresult;
+ FileSystemWritableFileStreamProperties;
+};
+
+/**
+ * Represents a pair of file system entries which
+ * are not necessarily connected by a path.
+ */
+struct FileSystemEntryPair
+{
+ EntryId parentId;
+ EntryId childId;
+};
+
+/**
+ * Contains a pair of file system entries.
+ */
+struct FileSystemResolveRequest
+{
+ FileSystemEntryPair endpoints;
+};
+
+/**
+ * Contains a file system path.
+ */
+struct FileSystemPath
+{
+ Name[] path;
+};
+
+/**
+ * Contains a potentially empty path or an error.
+ */
+union FileSystemResolveResponse
+{
+ nsresult;
+ FileSystemPath?;
+};
+
+/**
+ * Identifies a file with its parent directory and name, and
+ * indicates whether all the children of a directory may be removed.
+ */
+struct FileSystemRemoveEntryRequest
+{
+ FileSystemChildMetadata handle;
+ bool recursive;
+};
+
+/**
+ * Contains an error or nothing.
+ */
+union FileSystemRemoveEntryResponse
+{
+ nsresult;
+ void_t;
+};
+
+/**
+ * Identifies a file/directory to be moved and the new name, and the
+ * destination directory
+ */
+struct FileSystemMoveEntryRequest
+{
+ FileSystemEntryMetadata handle;
+ FileSystemChildMetadata destHandle;
+};
+
+/**
+ * Identifies a file/directory to be renamed and the new name
+ */
+struct FileSystemRenameEntryRequest
+{
+ FileSystemEntryMetadata handle;
+ Name name;
+};
+
+/**
+ * Contains an error or the new entryId
+ */
+union FileSystemMoveEntryResponse
+{
+ nsresult;
+ EntryId;
+};
+
+} // namespace fs
+
+[ChildProc=anydom]
+async protocol PFileSystemManager
+{
+ manages PFileSystemAccessHandle;
+ manages PFileSystemWritableFileStream;
+
+ parent:
+ /**
+ * TODO: documentation
+ */
+ [VirtualSendImpl]
+ async GetRootHandle()
+ returns(FileSystemGetHandleResponse response);
+
+ /**
+ * Initiates an asynchronous request for the handle of
+ * a subdirectory with a given name under the current directory.
+ *
+ * Invalid names are rejected with an appropriate error.
+ *
+ * If the subdirectory exists, a handle to it is always returned.
+ *
+ * If no child of any kind with the given name exists and
+ * the create-flag of the input is set, the subdirectory will be created,
+ * otherwise an appropriate error is returned.
+ *
+ * @param[in] handle request containing a create flag
+ *
+ * @returns error or entry handle
+ */
+ [VirtualSendImpl]
+ async GetDirectoryHandle(FileSystemGetHandleRequest request)
+ returns(FileSystemGetHandleResponse handle);
+
+ /**
+ * Initiates an asynchronous request for the handle to
+ * a file with a given name under the current directory.
+ *
+ * Invalid names are rejected with an appropriate error.
+ *
+ * If the file exists, a handle to it is always returned.
+ *
+ * If no child of any kind with the given name exists and
+ * the create-flag of the input is set, the file will be created,
+ * otherwise an appropriate error is returned.
+ *
+ * @param[in] handle request containing a create flag
+ *
+ * @returns error or entry handle
+ */
+ [VirtualSendImpl]
+ async GetFileHandle(FileSystemGetHandleRequest request)
+ returns(FileSystemGetHandleResponse handle);
+
+ /**
+ * Initiates an asynchronous request for a read-only object representing the
+ * file corresponding to the current file handle.
+ *
+ * The returned object provides read-only access.
+ *
+ * If the underlying file object is modified through a mutable interface,
+ * the returned value is considered stale. Concurrent changes are not
+ * guaranteed to be visible or invisible. Using a stale object
+ * returns appropriate errors when the results are unpredictable.
+ *
+ * @param[in] request for a file object
+ *
+ * @returns error or file object
+ */
+ [VirtualSendImpl]
+ async GetFile(FileSystemGetFileRequest request)
+ returns(FileSystemGetFileResponse response);
+
+ /**
+ * TODO: documentation
+ */
+ [VirtualSendImpl]
+ async GetAccessHandle(FileSystemGetAccessHandleRequest request)
+ returns(FileSystemGetAccessHandleResponse response);
+
+ /**
+ * TODO: documentation
+ */
+ [VirtualSendImpl]
+ async GetWritable(FileSystemGetWritableRequest request)
+ returns(FileSystemGetWritableFileStreamResponse fileData);
+
+ /**
+ * Initiates an asynchronous request for the file system path
+ * associated with a file system entry.
+ *
+ * @param[in] request identifying a file object
+ *
+ * @returns error or file system path
+ */
+ [VirtualSendImpl]
+ async Resolve(FileSystemResolveRequest request)
+ returns(FileSystemResolveResponse response);
+
+ /**
+ * Initiates an asynchronous request for an iterator to the child entries
+ * under the calling directory handle.
+ *
+ * If the directory item names or the directory structure is modified while
+ * the iterator is in use, the iterator remains safe to use but no guarantees
+ * are made regarding the visibility of the concurrent changes.
+ * It is possible that a file which is added after the iteration has begun
+ * will not be returned, or that among the values there are invalid file
+ * handles whose underlying objects have been removed after the iteration
+ * started.
+ *
+ * @param[in] request for a iterator
+ *
+ * @returns error or iterator
+ */
+ [VirtualSendImpl]
+ async GetEntries(FileSystemGetEntriesRequest request)
+ returns(FileSystemGetEntriesResponse entries);
+
+ /**
+ * Initiates an asynchronous request to delete a directory or file with a
+ * given name under the calling directory handle.
+ *
+ * If recursive flag of the request is not set, a request to remove a
+ * non-empty directory returns an appropriate error, otherwise all the child
+ * files and directories are made to vanish.
+ *
+ * The recursive flag has no impact on files.
+ *
+ * @param[in] request containing a recursive flag
+ *
+ * @returns error information
+ */
+ [VirtualSendImpl]
+ async RemoveEntry(FileSystemRemoveEntryRequest request)
+ returns(FileSystemRemoveEntryResponse response);
+
+ /**
+ * Initiates an asynchronous request to move a directory or file with a
+ * given name to a given destination and new name.
+ *
+ * @returns error information
+ */
+ async MoveEntry(FileSystemMoveEntryRequest request)
+ returns(FileSystemMoveEntryResponse response);
+
+ /**
+ * Initiates an asynchronous request to rename a directory or file
+ *
+ * @returns error information
+ */
+ async RenameEntry(FileSystemRenameEntryRequest request)
+ returns(FileSystemMoveEntryResponse response);
+
+ child:
+ async PFileSystemWritableFileStream();
+
+ async CloseAll()
+ returns(nsresult rv);
+};
+
+} // namespace dom
+} // namespace mozilla
+
diff --git a/dom/fs/shared/PFileSystemWritableFileStream.ipdl b/dom/fs/shared/PFileSystemWritableFileStream.ipdl
new file mode 100644
index 0000000000..bb931f83ec
--- /dev/null
+++ b/dom/fs/shared/PFileSystemWritableFileStream.ipdl
@@ -0,0 +1,23 @@
+/* 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 PFileSystemManager;
+
+using struct mozilla::void_t from "mozilla/ipc/IPCCore.h";
+
+namespace mozilla {
+namespace dom {
+
+async protocol PFileSystemWritableFileStream
+{
+ manager PFileSystemManager;
+
+ parent:
+ async Close(bool aAbort) returns(void_t ok);
+
+ async __delete__();
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/fs/shared/TargetPtrHolder.h b/dom/fs/shared/TargetPtrHolder.h
new file mode 100644
index 0000000000..1d6543e577
--- /dev/null
+++ b/dom/fs/shared/TargetPtrHolder.h
@@ -0,0 +1,59 @@
+/* -*- 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_FS_SHARED_TARGETPTRHOLDER_H_
+#define DOM_FS_SHARED_TARGETPTRHOLDER_H_
+
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::dom::fs {
+
+// TODO: Remove this ad hoc class when bug 1805830 is fixed.
+template <typename T>
+class TargetPtrHolder {
+ public:
+ MOZ_IMPLICIT TargetPtrHolder(T* aRawPtr)
+ : mTarget(GetCurrentSerialEventTarget()), mPtr(aRawPtr) {
+ MOZ_ASSERT(mPtr);
+ }
+
+ TargetPtrHolder(const TargetPtrHolder&) = default;
+
+ TargetPtrHolder& operator=(const TargetPtrHolder&) = default;
+
+ TargetPtrHolder(TargetPtrHolder&&) = default;
+
+ TargetPtrHolder& operator=(TargetPtrHolder&&) = default;
+
+ ~TargetPtrHolder() {
+ if (!mPtr) {
+ return;
+ }
+
+ NS_ProxyRelease("TargetPtrHolder::mPtr", mTarget, mPtr.forget());
+ }
+
+ T* get() const {
+ MOZ_ASSERT(mPtr);
+
+ return mPtr.get();
+ }
+
+ T* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN { return get(); }
+
+ bool operator!() { return !mPtr.get(); }
+
+ private:
+ nsCOMPtr<nsISerialEventTarget> mTarget;
+ RefPtr<T> mPtr;
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_SHARED_TARGETPTRHOLDER_H_
diff --git a/dom/fs/shared/moz.build b/dom/fs/shared/moz.build
new file mode 100644
index 0000000000..1aaa2574ae
--- /dev/null
+++ b/dom/fs/shared/moz.build
@@ -0,0 +1,34 @@
+# -*- 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 += [
+ "FileSystemHelpers.h",
+ "FileSystemLog.h",
+ "FileSystemTypes.h",
+]
+
+EXPORTS.mozilla.dom.fs += [
+ "IPCRejectReporter.h",
+ "ManagedMozPromiseRequestHolder.h",
+ "TargetPtrHolder.h",
+]
+
+UNIFIED_SOURCES += [
+ "FileSystemHelpers.cpp",
+ "FileSystemLog.cpp",
+ "IPCRejectReporter.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+IPDL_SOURCES += [
+ "PFileSystemAccessHandle.ipdl",
+ "PFileSystemAccessHandleControl.ipdl",
+ "PFileSystemManager.ipdl",
+ "PFileSystemWritableFileStream.ipdl",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/dom/fs/test/common/.eslintrc.js b/dom/fs/test/common/.eslintrc.js
new file mode 100644
index 0000000000..d805cf6e0f
--- /dev/null
+++ b/dom/fs/test/common/.eslintrc.js
@@ -0,0 +1,14 @@
+/* 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/. */
+
+"use strict";
+
+module.exports = {
+ globals: {
+ Assert: true,
+ exported_symbols: true,
+ require_module: true,
+ Utils: true,
+ },
+};
diff --git a/dom/fs/test/common/dummy.js b/dom/fs/test/common/dummy.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/fs/test/common/dummy.js
diff --git a/dom/fs/test/common/mochitest.toml b/dom/fs/test/common/mochitest.toml
new file mode 100644
index 0000000000..3da39b96b8
--- /dev/null
+++ b/dom/fs/test/common/mochitest.toml
@@ -0,0 +1,15 @@
+# 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 = [
+ "nsresult.js",
+ "test_basics.js",
+ "test_fileSystemDirectoryHandle.js",
+ "test_syncAccessHandle.js",
+ "test_writableFileStream.js",
+]
+
+["dummy.js"]
+skip-if = ["true"]
diff --git a/dom/fs/test/common/moz.build b/dom/fs/test/common/moz.build
new file mode 100644
index 0000000000..65d62c9cda
--- /dev/null
+++ b/dom/fs/test/common/moz.build
@@ -0,0 +1,21 @@
+# -*- 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/.
+
+MOCHITEST_MANIFESTS += [
+ "mochitest.toml",
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "xpcshell.toml",
+]
+
+TESTING_JS_MODULES.dom.fs.test.common += [
+ "nsresult.js",
+ "test_basics.js",
+ "test_fileSystemDirectoryHandle.js",
+ "test_syncAccessHandle.js",
+ "test_writableFileStream.js",
+]
diff --git a/dom/fs/test/common/nsresult.js b/dom/fs/test/common/nsresult.js
new file mode 100644
index 0000000000..6e59b947a1
--- /dev/null
+++ b/dom/fs/test/common/nsresult.js
@@ -0,0 +1,9 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const nsresult = {
+ NS_ERROR_NOT_IMPLEMENTED: Cr.NS_ERROR_NOT_IMPLEMENTED,
+};
+exported_symbols.nsresult = nsresult;
diff --git a/dom/fs/test/common/test_basics.js b/dom/fs/test/common/test_basics.js
new file mode 100644
index 0000000000..f1cb1c222e
--- /dev/null
+++ b/dom/fs/test/common/test_basics.js
@@ -0,0 +1,375 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This test must be first, since we need the actor not to be created already.
+exported_symbols.testGetDirectoryTwice = async function () {
+ const promise1 = navigator.storage.getDirectory();
+ const promise2 = navigator.storage.getDirectory();
+
+ await Promise.all([promise1, promise2]);
+
+ Assert.ok(true, "Should not have thrown");
+};
+
+exported_symbols.testGetDirectoryDoesNotThrow = async function () {
+ await navigator.storage.getDirectory();
+
+ Assert.ok(true, "Should not have thrown");
+};
+
+exported_symbols.testGetDirectoryKindIsDirectory = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ Assert.equal(root.kind, "directory");
+};
+
+exported_symbols.testDirectoryHandleStringConversion = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ Assert.equal(
+ "" + root,
+ "[object FileSystemDirectoryHandle]",
+ "Is directoryHandle convertible to string?"
+ );
+};
+
+exported_symbols.testNewDirectoryHandleFromPrototype = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ try {
+ Object.create(root.prototype);
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(true, "Should have thrown");
+ Assert.ok(ex instanceof TypeError, "Threw the right error type");
+ }
+};
+
+exported_symbols.testIsSameEntryRoot = async function () {
+ const root = await navigator.storage.getDirectory();
+ try {
+ await root.move(root);
+ Assert.ok(false, "root should not be movable");
+ } catch (ex) {
+ Assert.ok(true, "root isn't movable");
+ }
+};
+
+exported_symbols.testDirectoryHandleSupportsKeysIterator = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ const it = await root.keys();
+ Assert.ok(!!it, "Does root support keys iterator?");
+};
+
+exported_symbols.testKeysIteratorNextIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ const it = await root.keys();
+ Assert.ok(!!it, "Does root support keys iterator?");
+
+ const item = await it.next();
+ Assert.ok(!!item, "Should return an item");
+};
+
+exported_symbols.testDirectoryHandleSupportsValuesIterator = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ const it = await root.values();
+ Assert.ok(!!it, "Does root support values iterator?");
+};
+
+exported_symbols.testValuesIteratorNextIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ const it = await root.values();
+ Assert.ok(!!it, "Does root support values iterator?");
+
+ const item = await it.next();
+ Assert.ok(!!item, "Should return an item");
+};
+
+exported_symbols.testDirectoryHandleSupportsEntriesIterator =
+ async function () {
+ const root = await navigator.storage.getDirectory();
+
+ const it = await root.entries();
+ Assert.ok(!!it, "Does root support entries iterator?");
+ };
+
+exported_symbols.testEntriesIteratorNextIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ const it = await root.entries();
+ Assert.ok(!!it, "Does root support entries iterator?");
+
+ const item = await it.next();
+ Assert.ok(!!item, "Should return an item");
+};
+
+exported_symbols.testGetFileHandleIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+ const allowCreate = { create: true };
+
+ const item = await root.getFileHandle("fileName", allowCreate);
+ Assert.ok(!!item, "Should return an item");
+
+ await root.removeEntry("fileName");
+};
+
+exported_symbols.testGetDirectoryHandleIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+ const allowCreate = { create: true };
+
+ const item = await root.getDirectoryHandle("dirName", allowCreate);
+ Assert.ok(!!item, "Should return an item");
+
+ await root.removeEntry("dirName");
+};
+
+exported_symbols.testRemoveEntryIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+ const removeOptions = { recursive: true };
+ const allowCreate = { create: true };
+
+ // Ensure file and directory items exists
+ await root.getFileHandle("fileName", allowCreate);
+ await root.getDirectoryHandle("dirName", allowCreate);
+ await root.removeEntry("fileName", removeOptions);
+ await root.removeEntry("dirName", removeOptions);
+ try {
+ await root.removeEntry("doesNotExist", removeOptions);
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(true, "Should have thrown");
+ Assert.equal(
+ ex.message,
+ "Entry not found",
+ "Threw the right error message"
+ );
+ }
+};
+
+exported_symbols.testResolveIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+ const allowCreate = { create: true };
+ const item = await root.getFileHandle("fileName", allowCreate);
+
+ let path = await root.resolve(item);
+ Assert.equal(path.length, 1);
+ Assert.equal(path[0], "fileName", "Resolve got the right path");
+
+ await root.removeEntry("fileName");
+};
+
+exported_symbols.testFileType = async function () {
+ const root = await navigator.storage.getDirectory();
+ const allowCreate = { create: true };
+ const nameStem = "testFileType";
+ const empty = "";
+
+ const extensions = [
+ "txt",
+ "jS",
+ "JSON",
+ "css",
+ "html",
+ "htm",
+ "xhtml",
+ "xml",
+ "xhtml+xml",
+ "png",
+ "apng",
+ "jPg",
+ "Jpeg",
+ "pdF",
+ "out",
+ "sh",
+ "ExE",
+ "psid",
+ "EXE ",
+ " EXE",
+ "EX\uff65",
+ "\udbff\udbff\udbff",
+ // XXX: Invalid surrogate combos like "\udc00\udc00\udc00" may map to the same names impacting cleanup.
+ "js\udbff",
+ "\udc00js",
+ "???",
+ "\root",
+ empty,
+ "AXS",
+ "dll",
+ "ocx",
+ "1",
+ "ps1",
+ "cmd",
+ "xpi",
+ "swf",
+ ];
+
+ const expectedTypes = [
+ "text/plain",
+ "application/javascript",
+ "application/json",
+ "text/css",
+ "text/html",
+ "text/html",
+ "application/xhtml+xml",
+ "text/xml",
+ empty,
+ "image/png",
+ "image/apng",
+ "image/jpeg",
+ "image/jpeg",
+ "application/pdf",
+ empty,
+ "application/x-sh",
+ "application/octet-stream",
+ empty,
+ empty,
+ empty,
+ empty,
+ empty,
+ empty,
+ empty,
+ empty,
+ empty,
+ empty,
+ "application/olescript",
+ "application/x-msdownload",
+ "application/octet-stream",
+ empty,
+ empty,
+ "text/plain",
+ "application/x-xpinstall",
+ "application/x-shockwave-flash",
+ ];
+
+ Assert.equal(extensions.length, expectedTypes.length);
+
+ await Promise.all(
+ extensions.map(async (ext, i) => {
+ const fileName = nameStem + "." + ext;
+ const fileHandle = await root.getFileHandle(fileName, allowCreate);
+ const fileObject = await fileHandle.getFile();
+ Assert.equal(fileObject.name, fileHandle.name);
+ Assert.equal(fileObject.type, expectedTypes[i]);
+ await root.removeEntry(fileName);
+ })
+ );
+};
+
+exported_symbols.testContentTypeChangesOnFileMove = async function () {
+ const allowCreate = { create: true };
+ const root = await navigator.storage.getDirectory();
+ const oldName = "testFile.txt";
+ const oldType = "text/plain";
+ const subdir = await root.getDirectoryHandle("subdir", allowCreate);
+
+ const fileHandle = await root.getFileHandle(oldName, allowCreate);
+
+ async function checkMove(newName, newType) {
+ Assert.equal(fileHandle.name, newName, "Has filename changed?");
+ {
+ const fileObject = await fileHandle.getFile();
+ Assert.equal(fileObject.name, newName, "Is the fileobject renamed?");
+ Assert.equal(fileObject.type, newType, "Is the fileobject type updated?");
+ }
+ }
+
+ async function restoreTest() {
+ await fileHandle.move(root, oldName);
+ await checkMove(oldName, oldType);
+ }
+
+ // No name change
+ await checkMove(oldName, oldType);
+ await fileHandle.move(subdir);
+ await checkMove(oldName, oldType);
+ await restoreTest();
+
+ // With name change
+
+ async function testMoveWithParams(testName, testType) {
+ async function testFileMoveCall(...combo) {
+ await fileHandle.move(...combo);
+ await checkMove(testName, testType);
+ await restoreTest();
+ }
+
+ await testFileMoveCall(subdir, testName);
+ await testFileMoveCall(root, testName);
+ await testFileMoveCall(testName);
+ }
+
+ const testParams = {
+ "testFile.json": "application/json",
+ testFile: oldType,
+ "testFile.äüö": "",
+ };
+
+ for (const [aName, aType] of Object.entries(testParams)) {
+ await testMoveWithParams(aName, aType);
+ }
+};
+
+exported_symbols.testContentTypeChangesOnDirMove = async function () {
+ const allowCreate = { create: true };
+ const root = await navigator.storage.getDirectory();
+ const oldName = "testFile.txt";
+ const oldType = "text/plain";
+ const subDirOrig = await root.getDirectoryHandle("subDirOrig", allowCreate);
+ const subDirOther = await root.getDirectoryHandle("subDirOther", allowCreate);
+ const subSubDir = await subDirOrig.getDirectoryHandle(
+ "subSubDir",
+ allowCreate
+ );
+
+ const testName = "testFile.json";
+ const testType = "application/json";
+
+ async function checkMove(newName, newType) {
+ const fileHandle = await subSubDir.getFileHandle(newName, allowCreate);
+
+ Assert.equal(fileHandle.name, newName, "Has filename changed?");
+ {
+ const fileObject = await fileHandle.getFile();
+ Assert.equal(fileObject.name, newName, "Is the fileobject renamed?");
+ Assert.equal(fileObject.type, newType, "Is the fileobject type updated?");
+ }
+ }
+
+ async function restoreTest() {
+ await subSubDir.move(subDirOrig, "subSubDir");
+ await checkMove(oldName, oldType);
+ }
+
+ await checkMove(oldName, oldType);
+
+ // No name change
+ await subSubDir.move(subDirOther, "other");
+ await checkMove(oldName, oldType);
+ await restoreTest();
+
+ // With name change
+
+ async function testDirMoveCall(...combo) {
+ await subSubDir.move(...combo);
+ await checkMove(testName, testType);
+ await restoreTest();
+ }
+
+ await testDirMoveCall(subDirOther);
+ await testDirMoveCall(subDirOther, testName);
+ await testDirMoveCall(subDirOrig, testName);
+ await testDirMoveCall(subDirOrig);
+};
+
+for (const [key, value] of Object.entries(exported_symbols)) {
+ Object.defineProperty(value, "name", {
+ value: key,
+ writable: false,
+ });
+}
diff --git a/dom/fs/test/common/test_fileSystemDirectoryHandle.js b/dom/fs/test/common/test_fileSystemDirectoryHandle.js
new file mode 100644
index 0000000000..9142e47380
--- /dev/null
+++ b/dom/fs/test/common/test_fileSystemDirectoryHandle.js
@@ -0,0 +1,196 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+exported_symbols.smokeTest = async function smokeTest() {
+ const storage = navigator.storage;
+ const subdirectoryNames = new Set(["Documents", "Downloads", "Music"]);
+ const allowCreate = { create: true };
+
+ {
+ let root = await storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ let it = await root.values();
+ Assert.ok(!!it, "Does root have values iterator?");
+
+ let elem = await it.next();
+ Assert.ok(elem.done, "Is root directory empty?");
+
+ for (let dirName of subdirectoryNames) {
+ await root.getDirectoryHandle(dirName, allowCreate);
+ Assert.ok(true, "Was it possible to add subdirectory " + dirName + "?");
+ }
+ }
+
+ {
+ let root = await storage.getDirectory();
+ Assert.ok(root, "Can we refresh the root directory?");
+
+ let it = await root.values();
+ Assert.ok(!!it, "Does root have values iterator?");
+
+ let hasElements = false;
+ let hangGuard = 0;
+ for await (let [key, elem] of root.entries()) {
+ Assert.ok(elem, "Is element not non-empty?");
+ Assert.equal("directory", elem.kind, "Is found item a directory?");
+ Assert.ok(
+ elem.name.length >= 1 && elem.name.match("^[A-Za-z]{1,64}"),
+ "Are names of the elements strings?"
+ );
+ Assert.equal(key, elem.name);
+ Assert.ok(subdirectoryNames.has(elem.name), "Is name among known names?");
+ hasElements = true;
+ ++hangGuard;
+ if (hangGuard == 10) {
+ break; // Exit if there is a hang
+ }
+ }
+
+ Assert.ok(hasElements, "Is values container now non-empty?");
+ Assert.equal(3, hangGuard, "Do we only have three elements?");
+
+ {
+ it = await root.values();
+ Assert.ok(!!it, "Does root have values iterator?");
+ let elem = await it.next();
+
+ await elem.value.getDirectoryHandle("Trash", allowCreate);
+ let subit = elem.value.values();
+ Assert.ok(!!elem, "Is element not non-empty?");
+ let subdirResult = await subit.next();
+ let subdir = subdirResult.value;
+ Assert.ok(!!subdir, "Is element not non-empty?");
+ Assert.equal("directory", subdir.kind, "Is found item a directory?");
+ Assert.equal("Trash", subdir.name, "Is found item a directory?");
+ }
+
+ const wipeEverything = { recursive: true };
+ for (let dirName of subdirectoryNames) {
+ await root.removeEntry(dirName, wipeEverything);
+ Assert.ok(
+ true,
+ "Was it possible to remove subdirectory " + dirName + "?"
+ );
+ }
+ }
+
+ {
+ let root = await storage.getDirectory();
+ Assert.ok(root, "Can we refresh the root directory?");
+
+ let it = root.values();
+ Assert.ok(!!it, "Does root have values iterator?");
+
+ let elem = await it.next();
+ Assert.ok(elem.done, "Is root directory empty?");
+ }
+};
+
+exported_symbols.quotaTest = async function () {
+ const storage = navigator.storage;
+ const allowCreate = { create: true };
+
+ {
+ let root = await storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ const fileHandle = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!fileHandle, "Can we get file handle?");
+
+ const usageAtStart = await Utils.getCachedOriginUsage();
+ Assert.ok(true, "usageAtStart: " + usageAtStart);
+
+ const writable = await fileHandle.createWritable();
+ Assert.ok(!!writable, "Can we create writable file stream?");
+
+ const usageAtWritableCreated = await Utils.getCachedOriginUsage();
+ Assert.equal(
+ usageAtWritableCreated - usageAtStart,
+ 0,
+ "Did usage increase when writable was created?"
+ );
+
+ const buffer = new ArrayBuffer(42);
+ Assert.ok(!!buffer, "Can we create array buffer?");
+
+ const result = await writable.write(buffer);
+ Assert.equal(result, undefined, "Can we write entire buffer?");
+
+ const usageAtWriteDone = await Utils.getCachedOriginUsage();
+ // Note: Usage should change only on close after 1824305
+ Assert.equal(
+ usageAtWriteDone - usageAtWritableCreated,
+ buffer.byteLength,
+ "Is write immediately reflected in usage?"
+ );
+
+ await writable.close();
+
+ const usageAtWritableClosed = await Utils.getCachedOriginUsage();
+
+ Assert.equal(
+ usageAtWritableClosed - usageAtWritableCreated,
+ buffer.byteLength,
+ "Did usage increase by the amount of bytes written?"
+ );
+
+ await root.removeEntry("test.txt");
+
+ const usageAtFileDeleted = await Utils.getCachedOriginUsage();
+
+ Assert.equal(
+ usageAtFileDeleted,
+ usageAtWritableCreated,
+ "Is usage back to the value before any writing when the file is removed?"
+ );
+ }
+};
+
+exported_symbols.pagedIterationTest = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ for await (let contentItem of root.keys()) {
+ await root.removeEntry(contentItem, { recursive: true });
+ }
+
+ const allowCreate = { create: true };
+
+ // When half of the buffer is iterated, a request for the second half is sent.
+ // We test that the this boundary is crossed smoothly.
+ // After the buffer is filled, a request for more items is sent. The
+ // items are placed in the first half of the buffer.
+ // This boundary should also be crossed without problems.
+ // Currently, the buffer is half-filled at 1024.
+ const itemBatch = 3 + 2 * 1024;
+ for (let i = 0; i <= itemBatch; ++i) {
+ await root.getDirectoryHandle("" + i, allowCreate);
+ }
+
+ let result = 0;
+ let sum = 0;
+ const handles = new Set();
+ let isUnique = true;
+ for await (let [key, elem] of root.entries()) {
+ result += key.length;
+ sum += parseInt(elem.name);
+ if (handles.has(key)) {
+ // Asserting here is slow and verbose
+ isUnique = false;
+ break;
+ }
+ handles.add(key);
+ }
+ Assert.ok(isUnique);
+ Assert.equal(result, 7098);
+ Assert.equal(sum, (itemBatch * (itemBatch + 1)) / 2);
+};
+
+for (const [key, value] of Object.entries(exported_symbols)) {
+ Object.defineProperty(value, "name", {
+ value: key,
+ writable: false,
+ });
+}
diff --git a/dom/fs/test/common/test_syncAccessHandle.js b/dom/fs/test/common/test_syncAccessHandle.js
new file mode 100644
index 0000000000..ac7e0ef769
--- /dev/null
+++ b/dom/fs/test/common/test_syncAccessHandle.js
@@ -0,0 +1,237 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const allowCreate = { create: true };
+
+exported_symbols.test0 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ try {
+ await root.getFileHandle("test.txt");
+ Assert.ok(false, "Opened file that shouldn't exist");
+ } catch (e) {
+ dump("caught exception when we tried to open a non-existant file\n");
+ }
+};
+
+exported_symbols.test1 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ const testFile = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't create file");
+ let handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create SyncAccessHandle");
+ await handle.close();
+ handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create second SyncAccessHandle to same file");
+ await handle.close();
+};
+
+exported_symbols.test2 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ const testFile = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't open file");
+ let handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create SyncAccessHandle");
+ await handle.close();
+
+ await root.removeEntry("test.txt");
+ try {
+ handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Didn't remove file!");
+ if (handle) {
+ await handle.close();
+ }
+ } catch (e) {
+ dump("Caught exception trying to create accesshandle to deleted file\n");
+ }
+};
+
+exported_symbols.test3 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(!!root, "Can we access the root directory?");
+
+ let dir = await root.getDirectoryHandle("dir", allowCreate);
+ Assert.ok(!!dir, "Can we create a directory?");
+
+ // XXX not implemented yet
+ //const path = await root.resolve(dir);
+ //Assert.ok(path == ["dir"], "Wrong path: " + path);
+
+ let dir2 = await dir.getDirectoryHandle("dir", allowCreate);
+ Assert.ok(!!dir, "Can we create dir/dir?");
+
+ // XXX not implemented yet
+ //const path = await root.resolve(dir2);
+ //Assert.ok(path == ["dir", "dir"], "Wrong path: " + path);
+
+ let dir3 = await dir.getDirectoryHandle("bar", allowCreate);
+ Assert.ok(!!dir3, "Can we create dir/bar?");
+
+ // This should fail
+ try {
+ await root.getDirectoryHandle("bar");
+ Assert.ok(!dir, "we shouldn't be able to get bar unless we create it");
+ } catch (e) {
+ dump("caught exception when we tried to get a non-existant dir\n");
+ }
+
+ const testFile = await dir2.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't create file in dir2");
+ let handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create SyncAccessHandle in dir2");
+ await handle.close();
+};
+
+exported_symbols.test4 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(!!root, "Can we access the root directory?");
+
+ const testFile = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't access existing file");
+ let handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create SyncAccessHandle to existing file");
+
+ // Write a sentence to the end of the file.
+ const encoder = new TextEncoder();
+ const writeBuffer = encoder.encode("Thank you for reading this.");
+ const writeSize = handle.write(writeBuffer);
+ Assert.ok(!!writeSize);
+
+ // Read it back
+ // Get size of the file.
+ let fileSize = await handle.getSize();
+ Assert.ok(fileSize == writeBuffer.byteLength);
+ // Read file content to a buffer.
+ const readBuffer = new ArrayBuffer(fileSize);
+ const readSize = handle.read(readBuffer, { at: 0 });
+ Assert.ok(!!readSize);
+ //Assert.ok(readBuffer == writeBuffer);
+
+ await handle.truncate(5);
+ fileSize = await handle.getSize();
+ Assert.ok(fileSize == 5);
+
+ await handle.flush();
+ await handle.close();
+};
+
+exported_symbols.test5 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(!!root, "Can we access the root directory?");
+
+ const testFile = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't create file");
+ let handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create SyncAccessHandle");
+
+ try {
+ const testFile2 = await root.getFileHandle("test2.txt", allowCreate);
+ let handle2 = await testFile2.createSyncAccessHandle();
+ Assert.ok(!!handle2, "can't create SyncAccessHandle to second file!");
+ if (handle2) {
+ await handle2.close();
+ }
+ } catch (e) {
+ Assert.ok(false, "Failed to create second file");
+ }
+
+ await handle.close();
+};
+
+exported_symbols.test6 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ const testFile = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't get file");
+ let handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create SyncAccessHandle");
+
+ try {
+ let handle2 = await testFile.createSyncAccessHandle();
+ Assert.ok(!handle2, "Shouldn't create SyncAccessHandle!");
+ if (handle2) {
+ await handle2.close();
+ }
+ } catch (e) {
+ // should always happen
+ dump("caught exception when we tried to get 2 SyncAccessHandles\n");
+ }
+
+ // test that locks work across multiple connections for an origin
+ try {
+ let root2 = await navigator.storage.getDirectory();
+ Assert.ok(root2, "Can we access the root2 directory?");
+
+ const testFile2 = await root2.getFileHandle("test.txt");
+ Assert.ok(!!testFile2, "Can't get file");
+ let handle2 = await testFile2.createSyncAccessHandle();
+ Assert.ok(!handle2, "Shouldn't create SyncAccessHandle (2)!");
+ if (handle2) {
+ await handle2.close();
+ }
+ } catch (e) {
+ // should always happen
+ dump("caught exception when we tried to get 2 SyncAccessHandles\n");
+ }
+
+ if (handle) {
+ await handle.close();
+ }
+};
+
+exported_symbols.quotaTest = async function () {
+ const shrinkedStorageSizeKB = 5 * 1024;
+ const defaultDatabaseSize = 491520;
+
+ // Shrink storage size to 5MB.
+ await Utils.shrinkStorageSize(shrinkedStorageSizeKB);
+
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ // Fill entire storage.
+ const fileHandle = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!fileHandle, "Can we get file handle?");
+
+ const accessHandle = await fileHandle.createSyncAccessHandle();
+ Assert.ok(!!accessHandle, "Can we create sync access handle?");
+
+ const buffer = new ArrayBuffer(
+ shrinkedStorageSizeKB * 1024 - defaultDatabaseSize
+ );
+ Assert.ok(!!buffer, "Can we create array buffer?");
+
+ const written = accessHandle.write(buffer);
+ Assert.equal(written, buffer.byteLength, "Can we write entire buffer?");
+
+ // Try to write one more byte.
+ const fileHandle2 = await root.getFileHandle("test2.txt", allowCreate);
+ Assert.ok(!!fileHandle2, "Can we get file handle?");
+
+ const accessHandle2 = await fileHandle2.createSyncAccessHandle();
+ Assert.ok(!!accessHandle2, "Can we create sync access handle?");
+
+ const buffer2 = new ArrayBuffer(1);
+ Assert.ok(!!buffer2, "Can we create array buffer?");
+
+ const written2 = accessHandle2.write(buffer2);
+ Assert.equal(written2, 0, "Can we write beyond the limit?");
+
+ await accessHandle.close();
+ await accessHandle2.close();
+
+ await Utils.restoreStorageSize();
+};
+
+for (const [key, value] of Object.entries(exported_symbols)) {
+ Object.defineProperty(value, "name", {
+ value: key,
+ writable: false,
+ });
+}
diff --git a/dom/fs/test/common/test_writableFileStream.js b/dom/fs/test/common/test_writableFileStream.js
new file mode 100644
index 0000000000..016c53bf3b
--- /dev/null
+++ b/dom/fs/test/common/test_writableFileStream.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const allowCreate = { create: true };
+
+exported_symbols.test0 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(!!root, "Can we access the root directory?");
+
+ const testFile = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't access existing file");
+ let writable = await testFile.createWritable();
+ Assert.ok(!!writable, "Can't create WritableFileStream to existing file");
+
+ // Write a sentence to the end of the file.
+ const encoder = new TextEncoder();
+ const writeBuffer = encoder.encode("Thank you for reading this.");
+ try {
+ dump("Trying to write...\n");
+ await writable.write(writeBuffer);
+ dump("closing...\n");
+ await writable.close();
+ } catch (e) {
+ Assert.ok(false, "Couldn't write to WritableFileStream: " + e);
+ }
+
+ // Read it back
+ // Get size of the file.
+ let file = await testFile.getFile();
+ Assert.ok(
+ !!file,
+ "Can't create File to file written with WritableFileStream"
+ );
+ let fileSize = file.size;
+ Assert.ok(fileSize == writeBuffer.byteLength);
+};
+
+exported_symbols.quotaTest = async function () {
+ const shrinkedStorageSizeKB = 5 * 1024;
+ const defaultDatabaseSize = 491547;
+
+ // Shrink storage size to 5MB.
+ await Utils.shrinkStorageSize(shrinkedStorageSizeKB);
+
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ // Fill entire storage.
+ const fileHandle = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!fileHandle, "Can we get file handle?");
+
+ const writable = await fileHandle.createWritable();
+ Assert.ok(!!writable, "Can we create writable file stream?");
+
+ const buffer = new ArrayBuffer(
+ shrinkedStorageSizeKB * 1024 - defaultDatabaseSize
+ );
+ Assert.ok(!!buffer, "Can we create array buffer?");
+
+ const result = await writable.write(buffer);
+ Assert.equal(result, undefined, "Can we write entire buffer?");
+
+ // Try to write one more byte.
+ const fileHandle2 = await root.getFileHandle("test2.txt", allowCreate);
+ Assert.ok(!!fileHandle2, "Can we get file handle?");
+
+ const writable2 = await fileHandle2.createWritable();
+ Assert.ok(!!writable2, "Can we create writable file stream?");
+
+ const buffer2 = new ArrayBuffer(1);
+ Assert.ok(!!buffer2, "Can we create array buffer?");
+
+ try {
+ await writable2.write(buffer2);
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(true, "Did throw");
+ Assert.ok(DOMException.isInstance(ex), "Threw DOMException");
+ Assert.equal(ex.name, "QuotaExceededError", "Threw right DOMException");
+ }
+
+ await writable.close();
+ // writable2 is already closed because of the failed write above
+
+ await Utils.restoreStorageSize();
+};
+
+exported_symbols.bug1823445 = async function () {
+ const root = await navigator.storage.getDirectory();
+ const testFileName = "test1823445.txt";
+ let handle = await root.getFileHandle(testFileName, allowCreate);
+ let writable = await handle.createWritable();
+ await writable.write("abcdefghijklmnop");
+ await writable.close();
+
+ handle = await root.getFileHandle(testFileName);
+ writable = await handle.createWritable({ keepExistingData: false });
+ await writable.write("12345");
+ await writable.close();
+
+ handle = await root.getFileHandle(testFileName);
+ const file = await handle.getFile();
+ const text = await file.text();
+ Assert.equal(text, "12345");
+};
+
+exported_symbols.bug1824993 = async function () {
+ const root = await navigator.storage.getDirectory();
+ const testFileName = "test1824993.txt";
+ const handle = await root.getFileHandle(testFileName, allowCreate);
+ {
+ const writable = await handle.createWritable();
+ await writable.write("test");
+
+ {
+ const file = await handle.getFile();
+ const contents = await file.text();
+ Assert.equal(contents, "");
+ }
+
+ await writable.abort();
+ }
+
+ const file = await handle.getFile();
+ const contents = await file.text();
+ Assert.equal(contents, "");
+};
+
+exported_symbols.bug1825018 = async function () {
+ const root = await navigator.storage.getDirectory();
+ const testFileName = "test1825018.txt";
+ const handle = await root.getFileHandle(testFileName, allowCreate);
+ const writable = await handle.createWritable();
+ try {
+ await writable.write({ type: "truncate" });
+ } catch (e) {
+ // Called write without size throws an error as expected
+ }
+
+ try {
+ await writable.abort();
+ await root.removeEntry(testFileName);
+ } catch (e) {
+ Assert.ok(false, e.message);
+ }
+};
+
+for (const [key, value] of Object.entries(exported_symbols)) {
+ Object.defineProperty(value, "name", {
+ value: key,
+ writable: false,
+ });
+}
diff --git a/dom/fs/test/common/xpcshell.toml b/dom/fs/test/common/xpcshell.toml
new file mode 100644
index 0000000000..9f086b65fe
--- /dev/null
+++ b/dom/fs/test/common/xpcshell.toml
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files = [
+ "nsresult.js",
+ "test_basics.js",
+ "test_fileSystemDirectoryHandle.js",
+ "test_writableFileStream.js",
+]
+
+["dummy.js"]
+skip-if = ["true"]
diff --git a/dom/fs/test/crashtests/1798773.html b/dom/fs/test/crashtests/1798773.html
new file mode 100644
index 0000000000..893dfcfc59
--- /dev/null
+++ b/dom/fs/test/crashtests/1798773.html
@@ -0,0 +1,19 @@
+<script id="worker1" type="javascript/worker">
+self.onmessage = async function () {
+ const xhr = new XMLHttpRequest()
+ self.onerror = () => {
+ xhr.open("POST", "FOOBAR", false)
+ xhr.send()
+ }
+ self.reportError(undefined)
+ self.dir = await self.navigator.storage.getDirectory()
+}
+</script>
+<script>
+window.addEventListener('load', async () => {
+ const blob = new Blob([document.querySelector('#worker1').textContent], { type: "text/javascript" })
+ let worker = new Worker(window.URL.createObjectURL(blob))
+ worker.postMessage([], [])
+ setTimeout(() => {window.location.reload(true)})
+})
+</script>
diff --git a/dom/fs/test/crashtests/1800470.html b/dom/fs/test/crashtests/1800470.html
new file mode 100644
index 0000000000..a7d5dfa8bb
--- /dev/null
+++ b/dom/fs/test/crashtests/1800470.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <script id="worker1" type="javascript/worker">
+ self.onmessage = async function (e) {
+ const directory = await navigator.storage.getDirectory();
+ const file = await directory.getFileHandle("500014c3-f683-4551-bb26-08025c9be332", {
+ create: true,
+ });
+ const stream = await file.createWritable({});
+ const regex = new RegExp(".*");
+ await stream.abort(regex);
+ self.postMessage("done");
+ self.close();
+ }
+ </script>
+ <script>
+ var worker;
+ document.addEventListener('DOMContentLoaded', () => {
+ const buffer = new ArrayBuffer(1);
+ const blob = new Blob([document.querySelector('#worker1').textContent], { type: 'text/javascript' });
+ worker = new Worker(window.URL.createObjectURL(blob));
+ worker.postMessage([buffer], [buffer]);
+ worker.onmessage = function() {document.documentElement.removeAttribute("class"); }
+ });
+ </script>
+</head>
+</html>
diff --git a/dom/fs/test/crashtests/1809759.html b/dom/fs/test/crashtests/1809759.html
new file mode 100644
index 0000000000..b9df8de02f
--- /dev/null
+++ b/dom/fs/test/crashtests/1809759.html
@@ -0,0 +1,10 @@
+<script>
+document.addEventListener('DOMContentLoaded', async () => {
+ document.location.search = '?'
+ let a = self.navigator.storage
+ let xhr = new XMLHttpRequest()
+ xhr.open('POST', 'FOOBAR', false)
+ xhr.send()
+ await a.getDirectory()
+})
+</script>
diff --git a/dom/fs/test/crashtests/1816710.html b/dom/fs/test/crashtests/1816710.html
new file mode 100644
index 0000000000..f7641e009d
--- /dev/null
+++ b/dom/fs/test/crashtests/1816710.html
@@ -0,0 +1,8 @@
+<script>
+ window.addEventListener('load', async () => {
+ const dir = await navigator.storage.getDirectory();
+ const file = await dir.getFileHandle('555b8afb-96ac-4fe3-8cec', { create: true });
+ const writable = await file.createWritable({});
+ setTimeout('self.close()', 2000)
+ })
+</script>
diff --git a/dom/fs/test/crashtests/1841702.html b/dom/fs/test/crashtests/1841702.html
new file mode 100644
index 0000000000..0509972ae8
--- /dev/null
+++ b/dom/fs/test/crashtests/1841702.html
@@ -0,0 +1,16 @@
+<script id="worker1" type="javascript/worker">
+self.onmessage = async function(e) {
+ let a = await e.data[0].getFileHandle("c21deba4-fb73-4407-94f8-2e3782bf3f23", {"create": true})
+ self.close()
+ await a.createWritable({})
+}
+</script>
+
+<script>
+window.addEventListener("load", async () => {
+ let a = await self.clientInformation.storage.getDirectory()
+ const blob = new Blob([document.querySelector('#worker1').textContent], { type: "text/javascript" })
+ let worker = new Worker(window.URL.createObjectURL(blob))
+ worker.postMessage([a], [])
+})
+</script>
diff --git a/dom/fs/test/crashtests/1844619.html b/dom/fs/test/crashtests/1844619.html
new file mode 100644
index 0000000000..43a85e94d9
--- /dev/null
+++ b/dom/fs/test/crashtests/1844619.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ window.addEventListener('load', async () => {
+ await navigator.serviceWorker.register('sw1844619.js?1619678955', {})
+ await navigator.serviceWorker.register('sw1844619.js?4246054133', {})
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/fs/test/crashtests/1858820.html b/dom/fs/test/crashtests/1858820.html
new file mode 100644
index 0000000000..ad758b96e0
--- /dev/null
+++ b/dom/fs/test/crashtests/1858820.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <script>
+ window.addEventListener("load", async () => {
+ let a = document.createElement("iframe")
+ document.documentElement.appendChild(a)
+ let b = await a.contentWindow.clientInformation.storage.getDirectory()
+ let c = await b.getFileHandle("80e2d2c3-0712-4ccd-94b5-e2dd1732ea09", {"create": true})
+ let d = await c.createWritable({ })
+ setTimeout(async () => {
+ document.documentElement.removeAttribute("class");
+ await d.truncate(1);
+ }, 1000)
+ document.replaceChildren(document.documentElement, document.documentElement)
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/fs/test/crashtests/crashtests.list b/dom/fs/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..a083cb80b2
--- /dev/null
+++ b/dom/fs/test/crashtests/crashtests.list
@@ -0,0 +1,10 @@
+# StorageManager isn't enabled on Android
+defaults skip-if(Android) pref(dom.fs.enabled,true) pref(dom.fs.writable_file_stream.enabled,true)
+
+load 1798773.html
+load 1800470.html
+load 1809759.html
+load 1816710.html
+load 1841702.html
+HTTP load 1844619.html
+HTTP load 1858820.html
diff --git a/dom/fs/test/crashtests/sw1844619.js b/dom/fs/test/crashtests/sw1844619.js
new file mode 100644
index 0000000000..dd221844f2
--- /dev/null
+++ b/dom/fs/test/crashtests/sw1844619.js
@@ -0,0 +1,21 @@
+async function timeout (cmd) {
+ const timer = new Promise((resolve, reject) => {
+ const id = setTimeout(() => {
+ clearTimeout(id)
+ reject(new Error('Promise timed out!'))
+ }, 750)
+ })
+ return Promise.race([cmd, timer])
+}
+
+(async () => {
+ const root = await navigator.storage.getDirectory()
+ const blob = new Blob(['A'])
+ const sub = await root.getDirectoryHandle('a', { 'create': true })
+ const file = await root.getFileHandle('b', { 'create': true })
+ await file.move(sub)
+ const stream = await file.createWritable({})
+ await stream.write(blob)
+ const sub2 = await root.getDirectoryHandle('a', {})
+ await sub2.move(root, 'X')
+})()
diff --git a/dom/fs/test/gtest/FileSystemMocks.cpp b/dom/fs/test/gtest/FileSystemMocks.cpp
new file mode 100644
index 0000000000..1ad8e77c56
--- /dev/null
+++ b/dom/fs/test/gtest/FileSystemMocks.cpp
@@ -0,0 +1,94 @@
+/* -*- 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 "FileSystemMocks.h"
+
+#include <string>
+
+#include "ErrorList.h"
+#include "gtest/gtest-assertion-result.h"
+#include "js/RootingAPI.h"
+#include "jsapi.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "nsContentUtils.h"
+#include "nsISupports.h"
+
+namespace mozilla::dom::fs::test {
+
+nsIGlobalObject* GetGlobal() {
+ AutoJSAPI jsapi;
+ DebugOnly<bool> ok = jsapi.Init(xpc::PrivilegedJunkScope());
+ MOZ_ASSERT(ok);
+
+ JSContext* cx = jsapi.cx();
+ mozilla::dom::GlobalObject globalObject(cx, JS::CurrentGlobalOrNull(cx));
+ nsCOMPtr<nsIGlobalObject> global =
+ do_QueryInterface(globalObject.GetAsSupports());
+ MOZ_ASSERT(global);
+
+ return global.get();
+}
+
+nsresult GetAsString(const RefPtr<Promise>& aPromise, nsAString& aString) {
+ AutoJSAPI jsapi;
+ DebugOnly<bool> ok = jsapi.Init(xpc::PrivilegedJunkScope());
+ MOZ_ASSERT(ok);
+
+ JSContext* cx = jsapi.cx();
+
+ JS::Rooted<JSObject*> promiseObj(cx, aPromise->PromiseObj());
+ JS::Rooted<JS::Value> vp(cx, JS::GetPromiseResult(promiseObj));
+
+ switch (aPromise->State()) {
+ case Promise::PromiseState::Pending: {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ case Promise::PromiseState::Resolved: {
+ if (nsContentUtils::StringifyJSON(cx, vp, aString,
+ UndefinedIsNullStringLiteral)) {
+ return NS_OK;
+ }
+
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ case Promise::PromiseState::Rejected: {
+ if (vp.isInt32()) {
+ int32_t errorCode = vp.toInt32();
+ aString.AppendInt(errorCode);
+
+ return NS_OK;
+ }
+
+ if (!vp.isObject()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<Exception> exception;
+ UNWRAP_OBJECT(Exception, &vp, exception);
+ if (!exception) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ aString.Append(NS_ConvertUTF8toUTF16(
+ GetStaticErrorName(static_cast<nsresult>(exception->Result()))));
+
+ return NS_OK;
+ }
+
+ default:
+ break;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+mozilla::ipc::PrincipalInfo GetPrincipalInfo() {
+ return mozilla::ipc::PrincipalInfo{mozilla::ipc::SystemPrincipalInfo{}};
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/FileSystemMocks.h b/dom/fs/test/gtest/FileSystemMocks.h
new file mode 100644
index 0000000000..1926b2e86a
--- /dev/null
+++ b/dom/fs/test/gtest/FileSystemMocks.h
@@ -0,0 +1,340 @@
+/* -*- 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_FS_TEST_GTEST_FILESYSTEMMOCKS_H_
+#define DOM_FS_TEST_GTEST_FILESYSTEMMOCKS_H_
+
+#include <memory> // We don't have a mozilla shared pointer for pod types
+
+#include "TestHelpers.h"
+#include "fs/FileSystemChildFactory.h"
+#include "fs/FileSystemRequestHandler.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "js/Promise.h"
+#include "js/RootingAPI.h"
+#include "jsapi.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/DOMExceptionBinding.h"
+#include "mozilla/dom/FileSystemManagerChild.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "nsIGlobalObject.h"
+#include "nsISupports.h"
+#include "nsISupportsImpl.h"
+#include "nsITimer.h"
+
+namespace mozilla::dom::fs {
+
+inline std::ostream& operator<<(std::ostream& aOut,
+ const FileSystemEntryMetadata& aMetadata) {
+ return aOut;
+}
+
+namespace test {
+
+nsIGlobalObject* GetGlobal();
+
+nsresult GetAsString(const RefPtr<Promise>& aPromise, nsAString& aString);
+
+mozilla::ipc::PrincipalInfo GetPrincipalInfo();
+
+class MockFileSystemRequestHandler : public FileSystemRequestHandler {
+ public:
+ MOCK_METHOD(void, GetRootHandle,
+ (RefPtr<FileSystemManager> aManager, RefPtr<Promise> aPromise,
+ ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, GetDirectoryHandle,
+ (RefPtr<FileSystemManager> & aManager,
+ const FileSystemChildMetadata& aDirectory, bool aCreate,
+ RefPtr<Promise> aPromise, ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, GetFileHandle,
+ (RefPtr<FileSystemManager> & aManager,
+ const FileSystemChildMetadata& aFile, bool aCreate,
+ RefPtr<Promise> aPromise, ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, GetFile,
+ (RefPtr<FileSystemManager> & aManager,
+ const FileSystemEntryMetadata& aFile, RefPtr<Promise> aPromise,
+ ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, GetEntries,
+ (RefPtr<FileSystemManager> & aManager, const EntryId& aDirectory,
+ PageNumber aPage, RefPtr<Promise> aPromise,
+ RefPtr<FileSystemEntryMetadataArray>& aSink,
+ ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, RemoveEntry,
+ (RefPtr<FileSystemManager> & aManager,
+ const FileSystemChildMetadata& aEntry, bool aRecursive,
+ RefPtr<Promise> aPromise, ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, MoveEntry,
+ (RefPtr<FileSystemManager> & aManager, FileSystemHandle* aHandle,
+ FileSystemEntryMetadata* const aEntry,
+ const FileSystemChildMetadata& aNewEntry,
+ RefPtr<Promise> aPromise, ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, RenameEntry,
+ (RefPtr<FileSystemManager> & aManager, FileSystemHandle* aHandle,
+ FileSystemEntryMetadata* const aEntry, const Name& aName,
+ RefPtr<Promise> aPromise, ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, Resolve,
+ (RefPtr<FileSystemManager> & aManager,
+ const FileSystemEntryPair& aEndpoints, RefPtr<Promise> aPromise,
+ ErrorResult& aError),
+ (override));
+};
+
+class WaitablePromiseListener {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual void ClearDone() = 0;
+
+ virtual bool IsDone() const = 0;
+
+ virtual PromiseNativeHandler* AsHandler() = 0;
+
+ protected:
+ virtual ~WaitablePromiseListener() = default;
+};
+
+template <class SuccessHandler, class ErrorHandler,
+ uint32_t MilliSeconds = 2000u>
+class TestPromiseListener : public PromiseNativeHandler,
+ public WaitablePromiseListener {
+ public:
+ TestPromiseListener()
+ : mIsDone(std::make_shared<bool>(false)), mOnSuccess(), mOnError() {
+ ClearDone();
+ }
+
+ // nsISupports implementation
+
+ NS_IMETHODIMP QueryInterface(REFNSIID aIID, void** aInstancePtr) override {
+ nsresult rv = NS_ERROR_UNEXPECTED;
+ NS_INTERFACE_TABLE0(TestPromiseListener)
+
+ return rv;
+ }
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TestPromiseListener, override)
+
+ // PromiseNativeHandler implementation
+
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aError) override {
+ mozilla::ScopeExit flagAsDone([isDone = mIsDone, timer = mTimer] {
+ timer->Cancel();
+ *isDone = true;
+ });
+
+ mOnSuccess();
+ }
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aError) override {
+ mozilla::ScopeExit flagAsDone([isDone = mIsDone, timer = mTimer] {
+ timer->Cancel();
+ *isDone = true;
+ });
+
+ if (aValue.isInt32()) {
+ mOnError(static_cast<nsresult>(aValue.toInt32()));
+ return;
+ }
+
+ ASSERT_TRUE(aValue.isObject());
+ JS::Rooted<JSObject*> exceptionObject(aCx, &aValue.toObject());
+
+ RefPtr<Exception> exception;
+ UNWRAP_OBJECT(Exception, exceptionObject, exception);
+ if (exception) {
+ mOnError(static_cast<nsresult>(exception->Result()));
+ return;
+ }
+ }
+
+ // WaitablePromiseListener implementation
+
+ void ClearDone() override {
+ *mIsDone = false;
+ if (mTimer) {
+ mTimer->Cancel();
+ }
+ auto timerCallback = [isDone = mIsDone](nsITimer* aTimer) {
+ *isDone = true;
+ FAIL() << "Timed out!";
+ };
+ const char* timerName = "fs::TestPromiseListener::ClearDone";
+ auto res = NS_NewTimerWithCallback(timerCallback, MilliSeconds,
+ nsITimer::TYPE_ONE_SHOT, timerName);
+ if (res.isOk()) {
+ mTimer = res.unwrap();
+ }
+ }
+
+ bool IsDone() const override { return *mIsDone; }
+
+ PromiseNativeHandler* AsHandler() override { return this; }
+
+ SuccessHandler& GetSuccessHandler() { return mOnSuccess; }
+
+ SuccessHandler& GetErrorHandler() { return mOnError; }
+
+ protected:
+ virtual ~TestPromiseListener() = default;
+
+ std::shared_ptr<bool> mIsDone; // We pass this to a callback
+
+ nsCOMPtr<nsITimer> mTimer;
+
+ SuccessHandler mOnSuccess;
+
+ ErrorHandler mOnError;
+};
+
+class TestFileSystemManagerChild : public FileSystemManagerChild {
+ public:
+ MOCK_METHOD(void, SendGetRootHandle,
+ (mozilla::ipc::ResolveCallback<FileSystemGetHandleResponse> &&
+ aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendGetDirectoryHandle,
+ (const FileSystemGetHandleRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemGetHandleResponse>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendGetFileHandle,
+ (const FileSystemGetHandleRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemGetHandleResponse>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendGetAccessHandle,
+ (const FileSystemGetAccessHandleRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemGetAccessHandleResponse>&&
+ aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendGetWritable,
+ (const FileSystemGetWritableRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemGetWritableFileStreamResponse>&&
+ aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendGetFile,
+ (const FileSystemGetFileRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemGetFileResponse>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendResolve,
+ (const FileSystemResolveRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemResolveResponse>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendGetEntries,
+ (const FileSystemGetEntriesRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemGetEntriesResponse>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendRemoveEntry,
+ (const FileSystemRemoveEntryRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemRemoveEntryResponse>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(void, Shutdown, (), (override));
+
+ protected:
+ virtual ~TestFileSystemManagerChild() = default;
+};
+
+class TestFileSystemChildFactory final : public FileSystemChildFactory {
+ public:
+ explicit TestFileSystemChildFactory(TestFileSystemManagerChild* aChild)
+ : mChild(aChild) {}
+
+ already_AddRefed<FileSystemManagerChild> Create() const override {
+ return RefPtr<TestFileSystemManagerChild>(mChild).forget();
+ }
+
+ ~TestFileSystemChildFactory() = default;
+
+ private:
+ TestFileSystemManagerChild* mChild;
+};
+
+struct MockExpectMe {
+ MOCK_METHOD0(InvokeMe, void());
+
+ template <class... Args>
+ void operator()(Args...) {
+ InvokeMe();
+ }
+};
+
+template <nsresult Expected>
+struct NSErrorMatcher {
+ void operator()(nsresult aErr) { ASSERT_NSEQ(Expected, aErr); }
+};
+
+struct FailOnCall {
+ template <class... Args>
+ void operator()(Args...) {
+ FAIL();
+ }
+};
+
+} // namespace test
+} // namespace mozilla::dom::fs
+
+#define MOCK_PROMISE_LISTENER(name, ...) \
+ using name = mozilla::dom::fs::test::TestPromiseListener<__VA_ARGS__>;
+
+MOCK_PROMISE_LISTENER(
+ ExpectNotImplemented, mozilla::dom::fs::test::FailOnCall,
+ mozilla::dom::fs::test::NSErrorMatcher<NS_ERROR_NOT_IMPLEMENTED>);
+
+MOCK_PROMISE_LISTENER(ExpectResolveCalled, mozilla::dom::fs::test::MockExpectMe,
+ mozilla::dom::fs::test::FailOnCall);
+
+#endif // DOM_FS_TEST_GTEST_FILESYSTEMMOCKS_H_
diff --git a/dom/fs/test/gtest/TestHelpers.cpp b/dom/fs/test/gtest/TestHelpers.cpp
new file mode 100644
index 0000000000..b028530f19
--- /dev/null
+++ b/dom/fs/test/gtest/TestHelpers.cpp
@@ -0,0 +1,61 @@
+/* -*- 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 "TestHelpers.h"
+
+#include "gtest/gtest.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "nsString.h"
+
+namespace testing::internal {
+
+GTEST_API_ ::testing::AssertionResult CmpHelperSTREQ(const char* s1_expression,
+ const char* s2_expression,
+ const nsAString& s1,
+ const nsAString& s2) {
+ if (s1.Equals(s2)) {
+ return ::testing::AssertionSuccess();
+ }
+
+ return ::testing::internal::EqFailure(
+ s1_expression, s2_expression,
+ std::string(NS_ConvertUTF16toUTF8(s1).get()),
+ std::string(NS_ConvertUTF16toUTF8(s2).get()),
+ /* ignore case */ false);
+}
+
+GTEST_API_ ::testing::AssertionResult CmpHelperSTREQ(const char* s1_expression,
+ const char* s2_expression,
+ const nsACString& s1,
+ const nsACString& s2) {
+ if (s1.Equals(s2)) {
+ return ::testing::AssertionSuccess();
+ }
+
+ return ::testing::internal::EqFailure(s1_expression, s2_expression,
+ std::string(s1), std::string(s2),
+ /* ignore case */ false);
+}
+
+} // namespace testing::internal
+
+namespace mozilla::dom::fs::test {
+
+quota::OriginMetadata GetTestOriginMetadata() {
+ return quota::OriginMetadata{""_ns,
+ "example.com"_ns,
+ "http://example.com"_ns,
+ "http://example.com"_ns,
+ /* aIsPrivate */ false,
+ quota::PERSISTENCE_TYPE_DEFAULT};
+}
+
+const Origin& GetTestOrigin() {
+ static const Origin origin = "http://example.com"_ns;
+ return origin;
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/TestHelpers.h b/dom/fs/test/gtest/TestHelpers.h
new file mode 100644
index 0000000000..bfbcb9840c
--- /dev/null
+++ b/dom/fs/test/gtest/TestHelpers.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_FS_TEST_GTEST_TESTHELPERS_H_
+#define DOM_FS_TEST_GTEST_TESTHELPERS_H_
+
+#include "ErrorList.h"
+#include "gtest/gtest.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+
+namespace testing::internal {
+
+GTEST_API_ ::testing::AssertionResult CmpHelperSTREQ(const char* s1_expression,
+ const char* s2_expression,
+ const nsAString& s1,
+ const nsAString& s2);
+
+GTEST_API_ ::testing::AssertionResult CmpHelperSTREQ(const char* s1_expression,
+ const char* s2_expression,
+ const nsACString& s1,
+ const nsACString& s2);
+
+} // namespace testing::internal
+
+#define ASSERT_NSEQ(lhs, rhs) \
+ ASSERT_STREQ(GetStaticErrorName((lhs)), GetStaticErrorName((rhs)))
+
+#define TEST_TRY_UNWRAP_META(tempVar, target, expr) \
+ auto MOZ_REMOVE_PAREN(tempVar) = (expr); \
+ ASSERT_TRUE(MOZ_REMOVE_PAREN(tempVar).isOk()) \
+ << GetStaticErrorName( \
+ mozilla::ToNSResult(MOZ_REMOVE_PAREN(tempVar).unwrapErr())); \
+ MOZ_REMOVE_PAREN(target) = MOZ_REMOVE_PAREN(tempVar).unwrap();
+
+#define TEST_TRY_UNWRAP_ERR_META(tempVar, target, expr) \
+ auto MOZ_REMOVE_PAREN(tempVar) = (expr); \
+ ASSERT_TRUE(MOZ_REMOVE_PAREN(tempVar).isErr()); \
+ MOZ_REMOVE_PAREN(target) = \
+ mozilla::ToNSResult(MOZ_REMOVE_PAREN(tempVar).unwrapErr());
+
+#define TEST_TRY_UNWRAP(target, expr) \
+ TEST_TRY_UNWRAP_META(MOZ_UNIQUE_VAR(testVar), target, expr)
+
+#define TEST_TRY_UNWRAP_ERR(target, expr) \
+ TEST_TRY_UNWRAP_ERR_META(MOZ_UNIQUE_VAR(testVar), target, expr)
+
+namespace mozilla::dom {
+
+namespace quota {
+
+struct OriginMetadata;
+
+} // namespace quota
+
+namespace fs::test {
+
+quota::OriginMetadata GetTestOriginMetadata();
+
+const Origin& GetTestOrigin();
+
+} // namespace fs::test
+} // namespace mozilla::dom
+
+#endif // DOM_FS_TEST_GTEST_TESTHELPERS_H_
diff --git a/dom/fs/test/gtest/api/TestFileSystemDirectoryHandle.cpp b/dom/fs/test/gtest/api/TestFileSystemDirectoryHandle.cpp
new file mode 100644
index 0000000000..6aadd226e9
--- /dev/null
+++ b/dom/fs/test/gtest/api/TestFileSystemDirectoryHandle.cpp
@@ -0,0 +1,234 @@
+/* -*- 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 "FileSystemMocks.h"
+#include "gtest/gtest.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/FileSystemDirectoryHandle.h"
+#include "mozilla/dom/FileSystemDirectoryHandleBinding.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/FileSystemHandleBinding.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/StorageManager.h"
+#include "nsIGlobalObject.h"
+
+using ::testing::_;
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemDirectoryHandle : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ // TODO: Fix the test to not depend on CreateFileSystemManagerParent
+ // failure because of the pref set to false.
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", false);
+
+ mRequestHandler = MakeUnique<MockFileSystemRequestHandler>();
+ mMetadata = FileSystemEntryMetadata("dir"_ns, u"Directory"_ns,
+ /* directory */ true);
+ mName = u"testDir"_ns;
+ mManager = MakeAndAddRef<FileSystemManager>(mGlobal, nullptr);
+ }
+
+ void TearDown() override {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", true);
+
+ if (!mManager->IsShutdown()) {
+ mManager->Shutdown();
+ }
+ }
+
+ nsIGlobalObject* mGlobal = GetGlobal();
+ const IterableIteratorBase::IteratorType mIteratorType =
+ IterableIteratorBase::IteratorType::Keys;
+ UniquePtr<MockFileSystemRequestHandler> mRequestHandler;
+ FileSystemEntryMetadata mMetadata;
+ nsString mName;
+ RefPtr<FileSystemManager> mManager;
+};
+
+TEST_F(TestFileSystemDirectoryHandle, constructDirectoryHandleRefPointer) {
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata);
+
+ ASSERT_TRUE(dirHandle);
+}
+
+TEST_F(TestFileSystemDirectoryHandle, initIterator) {
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ RefPtr<FileSystemDirectoryHandle::iterator_t> iterator =
+ new FileSystemDirectoryHandle::iterator_t(dirHandle.get(), mIteratorType);
+ IgnoredErrorResult rv;
+ dirHandle->InitAsyncIteratorData(iterator->Data(), mIteratorType, rv);
+ ASSERT_TRUE(iterator->Data().mImpl);
+}
+
+class MockFileSystemDirectoryIteratorImpl final
+ : public FileSystemDirectoryIterator::Impl {
+ public:
+ MOCK_METHOD(already_AddRefed<Promise>, Next,
+ (nsIGlobalObject * aGlobal, RefPtr<FileSystemManager>& aManager,
+ ErrorResult& aError),
+ (override));
+};
+
+TEST_F(TestFileSystemDirectoryHandle, isNextPromiseReturned) {
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ auto mockIter = MakeRefPtr<MockFileSystemDirectoryIteratorImpl>();
+ IgnoredErrorResult error;
+ EXPECT_CALL(*mockIter, Next(_, _, _))
+ .WillOnce(::testing::Return(Promise::Create(mGlobal, error)));
+
+ RefPtr<FileSystemDirectoryHandle::iterator_t> iterator =
+ MakeAndAddRef<FileSystemDirectoryHandle::iterator_t>(dirHandle.get(),
+ mIteratorType);
+ iterator->Data().mImpl = std::move(mockIter);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise =
+ dirHandle->GetNextIterationResult(iterator.get(), rv);
+ ASSERT_TRUE(promise);
+}
+
+TEST_F(TestFileSystemDirectoryHandle, isHandleKindDirectory) {
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ ASSERT_EQ(FileSystemHandleKind::Directory, dirHandle->Kind());
+}
+
+TEST_F(TestFileSystemDirectoryHandle, isFileHandleReturned) {
+ EXPECT_CALL(*mRequestHandler, GetFileHandle(_, _, _, _, _))
+ .WillOnce(::testing::ReturnArg<3>());
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ FileSystemGetFileOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->GetFileHandle(mName, options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, doesGetFileHandleFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata);
+
+ ASSERT_TRUE(dirHandle);
+
+ FileSystemGetFileOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->GetFileHandle(mName, options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, isDirectoryHandleReturned) {
+ EXPECT_CALL(*mRequestHandler, GetDirectoryHandle(_, _, _, _, _))
+ .WillOnce(::testing::ReturnArg<3>());
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ FileSystemGetDirectoryOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->GetDirectoryHandle(mName, options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, doesGetDirectoryHandleFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata);
+
+ ASSERT_TRUE(dirHandle);
+
+ FileSystemGetDirectoryOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->GetDirectoryHandle(mName, options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, isRemoveEntrySuccessful) {
+ EXPECT_CALL(*mRequestHandler, RemoveEntry(_, _, _, _, _))
+ .WillOnce(::testing::ReturnArg<3>());
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ FileSystemRemoveOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->RemoveEntry(mName, options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, doesRemoveEntryFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata);
+
+ ASSERT_TRUE(dirHandle);
+
+ FileSystemRemoveOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->RemoveEntry(mName, options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, isResolveSuccessful) {
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->Resolve(*dirHandle, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, doesResolveFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata);
+
+ ASSERT_TRUE(dirHandle);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->Resolve(*dirHandle, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/api/TestFileSystemFileHandle.cpp b/dom/fs/test/gtest/api/TestFileSystemFileHandle.cpp
new file mode 100644
index 0000000000..263c1f2ed1
--- /dev/null
+++ b/dom/fs/test/gtest/api/TestFileSystemFileHandle.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 "FileSystemMocks.h"
+#include "fs/FileSystemChildFactory.h"
+#include "gtest/gtest.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/FileSystemFileHandle.h"
+#include "mozilla/dom/FileSystemFileHandleBinding.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/FileSystemHandleBinding.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/StorageManager.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemFileHandle : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ // TODO: Fix the test to not depend on CreateFileSystemManagerParent
+ // failure because of the pref set to false.
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", false);
+
+ mRequestHandler = MakeUnique<MockFileSystemRequestHandler>();
+ mMetadata =
+ FileSystemEntryMetadata("file"_ns, u"File"_ns, /* directory */ false);
+ mManager = MakeAndAddRef<FileSystemManager>(mGlobal, nullptr);
+ }
+
+ void TearDown() override {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", true);
+
+ if (!mManager->IsShutdown()) {
+ mManager->Shutdown();
+ }
+ }
+
+ nsIGlobalObject* mGlobal = GetGlobal();
+ UniquePtr<MockFileSystemRequestHandler> mRequestHandler;
+ FileSystemEntryMetadata mMetadata;
+ RefPtr<FileSystemManager> mManager;
+};
+
+TEST_F(TestFileSystemFileHandle, constructFileHandleRefPointer) {
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+}
+
+TEST_F(TestFileSystemFileHandle, isHandleKindFile) {
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ ASSERT_EQ(FileSystemHandleKind::File, fileHandle->Kind());
+}
+
+TEST_F(TestFileSystemFileHandle, isFileReturned) {
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->GetFile(rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemFileHandle, doesGetFileFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->GetFile(rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+TEST_F(TestFileSystemFileHandle, isWritableReturned) {
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ FileSystemCreateWritableOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->CreateWritable(options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemFileHandle, doesCreateWritableFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ FileSystemCreateWritableOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->CreateWritable(options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+TEST_F(TestFileSystemFileHandle, isSyncAccessHandleReturned) {
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->CreateSyncAccessHandle(rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemFileHandle, doesCreateSyncAccessHandleFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->CreateSyncAccessHandle(rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/api/TestFileSystemHandle.cpp b/dom/fs/test/gtest/api/TestFileSystemHandle.cpp
new file mode 100644
index 0000000000..19cdc98a84
--- /dev/null
+++ b/dom/fs/test/gtest/api/TestFileSystemHandle.cpp
@@ -0,0 +1,131 @@
+/* -*- 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 "FileSystemMocks.h"
+#include "fs/FileSystemChildFactory.h"
+#include "gtest/gtest.h"
+#include "mozilla/dom/FileSystemDirectoryHandle.h"
+#include "mozilla/dom/FileSystemFileHandle.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/FileSystemHandleBinding.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/StorageManager.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemHandle : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ // TODO: Fix the test to not depend on CreateFileSystemManagerParent
+ // failure because of the pref set to false.
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", false);
+
+ mDirMetadata = FileSystemEntryMetadata("dir"_ns, u"Directory"_ns,
+ /* directory */ true);
+ mFileMetadata =
+ FileSystemEntryMetadata("file"_ns, u"File"_ns, /* directory */ false);
+ mManager = MakeAndAddRef<FileSystemManager>(mGlobal, nullptr);
+ }
+
+ void TearDown() override {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", true);
+
+ if (!mManager->IsShutdown()) {
+ mManager->Shutdown();
+ }
+ }
+
+ nsIGlobalObject* mGlobal = GetGlobal();
+ FileSystemEntryMetadata mDirMetadata;
+ FileSystemEntryMetadata mFileMetadata;
+ RefPtr<FileSystemManager> mManager;
+};
+
+TEST_F(TestFileSystemHandle, createAndDestroyHandles) {
+ RefPtr<FileSystemHandle> dirHandle =
+ new FileSystemDirectoryHandle(mGlobal, mManager, mDirMetadata);
+ RefPtr<FileSystemHandle> fileHandle =
+ new FileSystemFileHandle(mGlobal, mManager, mFileMetadata);
+
+ EXPECT_TRUE(dirHandle);
+ EXPECT_TRUE(fileHandle);
+}
+
+TEST_F(TestFileSystemHandle, areFileNamesAsExpected) {
+ RefPtr<FileSystemHandle> dirHandle =
+ new FileSystemDirectoryHandle(mGlobal, mManager, mDirMetadata);
+ RefPtr<FileSystemHandle> fileHandle =
+ new FileSystemFileHandle(mGlobal, mManager, mFileMetadata);
+
+ auto GetEntryName = [](const RefPtr<FileSystemHandle>& aHandle) {
+ DOMString domName;
+ aHandle->GetName(domName);
+ nsString result;
+ domName.ToString(result);
+ return result;
+ };
+
+ const nsString& dirName = GetEntryName(dirHandle);
+ EXPECT_TRUE(mDirMetadata.entryName().Equals(dirName));
+
+ const nsString& fileName = GetEntryName(fileHandle);
+ EXPECT_TRUE(mFileMetadata.entryName().Equals(fileName));
+}
+
+TEST_F(TestFileSystemHandle, isParentObjectReturned) {
+ ASSERT_TRUE(mGlobal);
+ RefPtr<FileSystemHandle> dirHandle =
+ new FileSystemDirectoryHandle(mGlobal, mManager, mDirMetadata);
+
+ ASSERT_EQ(mGlobal, dirHandle->GetParentObject());
+}
+
+TEST_F(TestFileSystemHandle, areHandleKindsAsExpected) {
+ RefPtr<FileSystemHandle> dirHandle =
+ new FileSystemDirectoryHandle(mGlobal, mManager, mDirMetadata);
+ RefPtr<FileSystemHandle> fileHandle =
+ new FileSystemFileHandle(mGlobal, mManager, mFileMetadata);
+
+ EXPECT_EQ(FileSystemHandleKind::Directory, dirHandle->Kind());
+ EXPECT_EQ(FileSystemHandleKind::File, fileHandle->Kind());
+}
+
+TEST_F(TestFileSystemHandle, isDifferentEntry) {
+ RefPtr<FileSystemHandle> dirHandle =
+ new FileSystemDirectoryHandle(mGlobal, mManager, mDirMetadata);
+ RefPtr<FileSystemHandle> fileHandle =
+ new FileSystemFileHandle(mGlobal, mManager, mFileMetadata);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->IsSameEntry(*fileHandle, rv);
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+ ASSERT_TRUE(promise);
+ ASSERT_EQ(Promise::PromiseState::Resolved, promise->State());
+
+ nsString result;
+ ASSERT_NSEQ(NS_OK, GetAsString(promise, result));
+ ASSERT_STREQ(u"false"_ns, result);
+}
+
+TEST_F(TestFileSystemHandle, isSameEntry) {
+ RefPtr<FileSystemHandle> fileHandle =
+ new FileSystemFileHandle(mGlobal, mManager, mFileMetadata);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->IsSameEntry(*fileHandle, rv);
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+ ASSERT_TRUE(promise);
+ ASSERT_EQ(Promise::PromiseState::Resolved, promise->State());
+
+ nsString result;
+ ASSERT_NSEQ(NS_OK, GetAsString(promise, result));
+ ASSERT_STREQ(u"true"_ns, result);
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/api/moz.build b/dom/fs/test/gtest/api/moz.build
new file mode 100644
index 0000000000..eb8416a3ba
--- /dev/null
+++ b/dom/fs/test/gtest/api/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES = [
+ "TestFileSystemDirectoryHandle.cpp",
+ "TestFileSystemFileHandle.cpp",
+ "TestFileSystemHandle.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/api",
+ "/dom/fs/include",
+ "/dom/fs/test/gtest",
+]
diff --git a/dom/fs/test/gtest/child/TestFileSystemBackgroundRequestHandler.cpp b/dom/fs/test/gtest/child/TestFileSystemBackgroundRequestHandler.cpp
new file mode 100644
index 0000000000..48d63cfc36
--- /dev/null
+++ b/dom/fs/test/gtest/child/TestFileSystemBackgroundRequestHandler.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 "FileSystemBackgroundRequestHandler.h"
+#include "FileSystemMocks.h"
+#include "gtest/gtest.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/FileSystemManagerChild.h"
+#include "mozilla/dom/PFileSystemManager.h"
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemBackgroundRequestHandler : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ // TODO: Fix the test to not depend on CreateFileSystemManagerParent
+ // failure because of the pref set to false.
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", false);
+
+ mFileSystemManagerChild = MakeAndAddRef<TestFileSystemManagerChild>();
+ }
+
+ void TearDown() override {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", true);
+ }
+
+ RefPtr<FileSystemBackgroundRequestHandler>
+ GetFileSystemBackgroundRequestHandler() {
+ return MakeRefPtr<FileSystemBackgroundRequestHandler>(
+ new TestFileSystemChildFactory(mFileSystemManagerChild));
+ }
+
+ mozilla::ipc::PrincipalInfo mPrincipalInfo = GetPrincipalInfo();
+ RefPtr<TestFileSystemManagerChild> mFileSystemManagerChild;
+};
+
+TEST_F(TestFileSystemBackgroundRequestHandler,
+ isCreateFileSystemManagerChildSuccessful) {
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce([fileSystemManagerChild =
+ static_cast<void*>(mFileSystemManagerChild.get())]() {
+ static_cast<TestFileSystemManagerChild*>(fileSystemManagerChild)
+ ->FileSystemManagerChild::Shutdown();
+ });
+
+ bool done = false;
+ auto testable = GetFileSystemBackgroundRequestHandler();
+ testable->CreateFileSystemManagerChild(mPrincipalInfo)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&done](bool) { done = true; }, [&done](nsresult) { done = true; });
+ // MozPromise should be rejected
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [&done]() { return done; });
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/child/TestFileSystemRequestHandler.cpp b/dom/fs/test/gtest/child/TestFileSystemRequestHandler.cpp
new file mode 100644
index 0000000000..c2832103af
--- /dev/null
+++ b/dom/fs/test/gtest/child/TestFileSystemRequestHandler.cpp
@@ -0,0 +1,352 @@
+/* -*- 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 "FileSystemBackgroundRequestHandler.h"
+#include "FileSystemEntryMetadataArray.h"
+#include "FileSystemMocks.h"
+#include "fs/FileSystemRequestHandler.h"
+#include "gtest/gtest.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/FileBlobImpl.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/FileSystemManagerChild.h"
+#include "mozilla/dom/IPCBlob.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/StorageManager.h"
+#include "mozilla/ipc/FileDescriptorUtils.h"
+#include "mozilla/ipc/IPCCore.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIFile.h"
+
+using ::testing::_;
+using ::testing::ByRef;
+using ::testing::Invoke;
+using ::testing::Return;
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemRequestHandler : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ mListener = MakeAndAddRef<ExpectResolveCalled>();
+
+ mChild = FileSystemChildMetadata("parent"_ns, u"ChildName"_ns);
+ mEntry = FileSystemEntryMetadata("myid"_ns, u"EntryName"_ns,
+ /* directory */ false);
+ mName = u"testDir"_ns;
+ mFileSystemManagerChild = MakeAndAddRef<TestFileSystemManagerChild>();
+ mManager = MakeAndAddRef<FileSystemManager>(
+ mGlobal, nullptr,
+ MakeRefPtr<FileSystemBackgroundRequestHandler>(
+ mFileSystemManagerChild));
+ }
+
+ void TearDown() override {
+ if (!mManager->IsShutdown()) {
+ EXPECT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+ }
+ }
+
+ already_AddRefed<Promise> GetDefaultPromise() {
+ IgnoredErrorResult rv;
+ RefPtr<Promise> result = Promise::Create(mGlobal, rv);
+ mListener->ClearDone();
+ result->AppendNativeHandler(mListener->AsHandler());
+
+ return result.forget();
+ }
+
+ already_AddRefed<Promise> GetSimplePromise() {
+ IgnoredErrorResult rv;
+ RefPtr<Promise> result = Promise::Create(mGlobal, rv);
+
+ return result.forget();
+ }
+
+ already_AddRefed<Promise> GetShutdownPromise() {
+ RefPtr<Promise> promise = GetDefaultPromise();
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce(Invoke([promise]() { promise->MaybeResolveWithUndefined(); }))
+ .WillOnce(Return());
+ EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
+
+ return promise.forget();
+ }
+
+ UniquePtr<FileSystemRequestHandler> GetFileSystemRequestHandler() {
+ return MakeUnique<FileSystemRequestHandler>();
+ }
+
+ void ShutdownFileSystemManager() {
+ RefPtr<Promise> promise = GetShutdownPromise();
+
+ mManager->Shutdown();
+
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+ ASSERT_TRUE(mManager->IsShutdown());
+ }
+
+ nsIGlobalObject* mGlobal = GetGlobal();
+ RefPtr<ExpectResolveCalled> mListener;
+
+ FileSystemChildMetadata mChild;
+ FileSystemEntryMetadata mEntry;
+ nsString mName;
+ RefPtr<TestFileSystemManagerChild> mFileSystemManagerChild;
+ RefPtr<FileSystemManager> mManager;
+};
+
+TEST_F(TestFileSystemRequestHandler, isGetRootHandleSuccessful) {
+ auto fakeResponse = [](auto&& aResolve, auto&& /* aReject */) {
+ EntryId expected = "expected"_ns;
+ FileSystemGetHandleResponse response(expected);
+ aResolve(std::move(response));
+ };
+
+ EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
+ EXPECT_CALL(*mFileSystemManagerChild, SendGetRootHandle(_, _))
+ .WillOnce(Invoke(fakeResponse));
+
+ RefPtr<Promise> promise = GetDefaultPromise();
+ auto testable = GetFileSystemRequestHandler();
+ testable->GetRootHandle(mManager, promise, IgnoredErrorResult());
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetRootHandleBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetRootHandle(mManager, GetSimplePromise(),
+ error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetDirectoryHandleSuccessful) {
+ auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
+ auto&& /* aReject */) {
+ EntryId expected = "expected"_ns;
+ FileSystemGetHandleResponse response(expected);
+ aResolve(std::move(response));
+ };
+
+ EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
+ EXPECT_CALL(*mFileSystemManagerChild, SendGetDirectoryHandle(_, _, _))
+ .WillOnce(Invoke(fakeResponse));
+
+ RefPtr<Promise> promise = GetDefaultPromise();
+ auto testable = GetFileSystemRequestHandler();
+ testable->GetDirectoryHandle(mManager, mChild,
+ /* create */ true, promise,
+ IgnoredErrorResult());
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetDirectoryHandleBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetDirectoryHandle(
+ mManager, mChild, /* aCreate */ true, GetSimplePromise(), error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetFileHandleSuccessful) {
+ auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
+ auto&& /* aReject */) {
+ EntryId expected = "expected"_ns;
+ FileSystemGetHandleResponse response(expected);
+ aResolve(std::move(response));
+ };
+
+ EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
+ EXPECT_CALL(*mFileSystemManagerChild, SendGetFileHandle(_, _, _))
+ .WillOnce(Invoke(fakeResponse));
+
+ RefPtr<Promise> promise = GetDefaultPromise();
+ auto testable = GetFileSystemRequestHandler();
+ testable->GetFileHandle(mManager, mChild, /* create */ true, promise,
+ IgnoredErrorResult());
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetFileHandleBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetFileHandle(
+ mManager, mChild, /* aCreate */ true, GetSimplePromise(), error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetFileSuccessful) {
+ auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
+ auto&& /* aReject */) {
+ // We have to create a temporary file
+ nsCOMPtr<nsIFile> tmpfile;
+ nsresult rv =
+ NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpfile));
+ ASSERT_EQ(NS_SUCCEEDED(rv), true);
+
+ rv = tmpfile->AppendNative("GetFileTestBlob"_ns);
+ ASSERT_EQ(NS_SUCCEEDED(rv), true);
+
+ rv = tmpfile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0666);
+ ASSERT_EQ(NS_SUCCEEDED(rv), true);
+
+ auto blob = MakeRefPtr<FileBlobImpl>(tmpfile);
+
+ TimeStamp last_modified_ms = 0;
+ ContentType type = "txt"_ns;
+ IPCBlob file;
+ IPCBlobUtils::Serialize(blob, file);
+
+ nsTArray<Name> path;
+ path.AppendElement(u"root"_ns);
+ path.AppendElement(u"Trash"_ns);
+
+ FileSystemFileProperties properties(last_modified_ms, file, type, path);
+ FileSystemGetFileResponse response(properties);
+ aResolve(std::move(response));
+ };
+
+ EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
+ EXPECT_CALL(*mFileSystemManagerChild, SendGetFile(_, _, _))
+ .WillOnce(Invoke(fakeResponse));
+
+ RefPtr<Promise> promise = GetDefaultPromise();
+ auto testable = GetFileSystemRequestHandler();
+ testable->GetFile(mManager, mEntry, promise, IgnoredErrorResult());
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetFileBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetFile(mManager, mEntry, GetSimplePromise(),
+ error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetAccessHandleBlockedAfterShutdown) {
+ RefPtr<Promise> promise = GetShutdownPromise();
+
+ mManager->Shutdown();
+
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+ ASSERT_TRUE(mManager->IsShutdown());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetAccessHandle(mManager, mEntry,
+ GetSimplePromise(), error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetWritableBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetWritable(
+ mManager, mEntry, /* aKeepData */ false, GetSimplePromise(), error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetEntriesSuccessful) {
+ auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
+ auto&& /* aReject */) {
+ nsTArray<FileSystemEntryMetadata> files;
+ nsTArray<FileSystemEntryMetadata> directories;
+ FileSystemDirectoryListing listing(files, directories);
+ FileSystemGetEntriesResponse response(listing);
+ aResolve(std::move(response));
+ };
+
+ RefPtr<ExpectResolveCalled> listener = MakeAndAddRef<ExpectResolveCalled>();
+ IgnoredErrorResult rv;
+ listener->ClearDone();
+ EXPECT_CALL(listener->GetSuccessHandler(), InvokeMe());
+
+ RefPtr<Promise> promise = Promise::Create(mGlobal, rv);
+ promise->AppendNativeHandler(listener);
+
+ EXPECT_CALL(*mFileSystemManagerChild, SendGetEntries(_, _, _))
+ .WillOnce(Invoke(fakeResponse));
+
+ auto testable = GetFileSystemRequestHandler();
+ RefPtr<FileSystemEntryMetadataArray> sink;
+
+ testable->GetEntries(mManager, mEntry.entryId(), /* page */ 0, promise, sink,
+ IgnoredErrorResult());
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [listener]() { return listener->IsDone(); });
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetEntriesBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ RefPtr<FileSystemEntryMetadataArray> sink;
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetEntries(mManager, mEntry.entryId(),
+ /* aPage */ 0, GetSimplePromise(),
+ sink, error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isRemoveEntrySuccessful) {
+ auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
+ auto&& /* aReject */) {
+ FileSystemRemoveEntryResponse response(mozilla::void_t{});
+ aResolve(std::move(response));
+ };
+
+ EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
+ EXPECT_CALL(*mFileSystemManagerChild, SendRemoveEntry(_, _, _))
+ .WillOnce(Invoke(fakeResponse));
+
+ auto testable = GetFileSystemRequestHandler();
+ RefPtr<Promise> promise = GetDefaultPromise();
+ testable->RemoveEntry(mManager, mChild, /* recursive */ true, promise,
+ IgnoredErrorResult());
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+}
+
+TEST_F(TestFileSystemRequestHandler, isRemoveEntryBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->RemoveEntry(
+ mManager, mChild, /* aRecursive */ true, GetSimplePromise(), error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/child/moz.build b/dom/fs/test/gtest/child/moz.build
new file mode 100644
index 0000000000..c305ab1f2e
--- /dev/null
+++ b/dom/fs/test/gtest/child/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES = [
+ "TestFileSystemBackgroundRequestHandler.cpp",
+ "TestFileSystemRequestHandler.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/child",
+ "/dom/fs/include",
+ "/dom/fs/test/gtest",
+]
diff --git a/dom/fs/test/gtest/moz.build b/dom/fs/test/gtest/moz.build
new file mode 100644
index 0000000000..81be2a3d33
--- /dev/null
+++ b/dom/fs/test/gtest/moz.build
@@ -0,0 +1,25 @@
+# -*- 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 += [
+ "api",
+ "child",
+ "parent",
+ "shared",
+]
+
+UNIFIED_SOURCES = [
+ "FileSystemMocks.cpp",
+ "TestHelpers.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/include",
+]
diff --git a/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp b/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp
new file mode 100644
index 0000000000..1764763dd5
--- /dev/null
+++ b/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp
@@ -0,0 +1,183 @@
+/* -*- 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 "FileSystemHashSource.h"
+#include "FileSystemParentTypes.h"
+#include "TestHelpers.h"
+#include "gtest/gtest.h"
+#include "mozilla/Array.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "nsContentUtils.h"
+#include "nsLiteralString.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTHashSet.h"
+
+namespace mozilla::dom::fs::test {
+
+using mozilla::dom::fs::data::FileSystemHashSource;
+
+namespace {
+
+constexpr size_t sha256ByteLength = 32u;
+
+constexpr size_t kExpectedLength = 52u;
+
+std::wstring asWide(const nsString& aStr) {
+ std::wstring result;
+ result.reserve(aStr.Length());
+ for (const auto* it = aStr.BeginReading(); it != aStr.EndReading(); ++it) {
+ result.push_back(static_cast<wchar_t>(*it));
+ }
+ return result;
+}
+
+} // namespace
+
+TEST(TestFileSystemHashSource, isHashLengthAsExpected)
+{
+ EntryId parent = "a"_ns;
+ Name name = u"b"_ns;
+ TEST_TRY_UNWRAP(EntryId result,
+ FileSystemHashSource::GenerateHash(parent, name));
+ ASSERT_EQ(sha256ByteLength, result.Length());
+};
+
+TEST(TestFileSystemHashSource, areNestedNameHashesValidAndUnequal)
+{
+ EntryId emptyParent = ""_ns;
+ Name name = u"a"_ns;
+ const size_t nestingNumber = 500u;
+
+ nsTHashSet<EntryId> results;
+ nsTHashSet<Name> names;
+
+ auto previousParent = emptyParent;
+ for (size_t i = 0; i < nestingNumber; ++i) {
+ TEST_TRY_UNWRAP(EntryId result,
+ FileSystemHashSource::GenerateHash(previousParent, name));
+
+ TEST_TRY_UNWRAP(Name encoded,
+ FileSystemHashSource::EncodeHash(FileId(result)));
+
+ // Validity checks
+ ASSERT_TRUE(mozilla::IsAscii(encoded))
+ << encoded;
+ Name upperCaseVersion;
+ nsContentUtils::ASCIIToUpper(encoded, upperCaseVersion);
+ ASSERT_STREQ(asWide(upperCaseVersion).c_str(), asWide(encoded).c_str());
+
+ // Is the same hash encountered?
+ ASSERT_FALSE(results.Contains(result));
+ ASSERT_TRUE(results.Insert(result, mozilla::fallible));
+
+ // Is the same name encountered?
+ ASSERT_FALSE(names.Contains(encoded));
+ ASSERT_TRUE(names.Insert(encoded, mozilla::fallible));
+
+ previousParent = result;
+ }
+};
+
+TEST(TestFileSystemHashSource, areNameCombinationHashesUnequal)
+{
+ EntryId emptyParent = ""_ns;
+
+ mozilla::Array<Name, 2> inputs = {u"a"_ns, u"b"_ns};
+ nsTArray<EntryId> results;
+ nsTArray<Name> names;
+
+ for (const auto& name : inputs) {
+ TEST_TRY_UNWRAP(EntryId result,
+ FileSystemHashSource::GenerateHash(emptyParent, name));
+ TEST_TRY_UNWRAP(Name encoded,
+ FileSystemHashSource::EncodeHash(FileId(result)));
+
+ // Validity checks
+ ASSERT_TRUE(mozilla::IsAscii(encoded))
+ << encoded;
+ Name upperCaseVersion;
+ nsContentUtils::ASCIIToUpper(encoded, upperCaseVersion);
+ ASSERT_STREQ(asWide(upperCaseVersion).c_str(), asWide(encoded).c_str());
+
+ results.AppendElement(result);
+ names.AppendElement(encoded);
+ }
+
+ nsTArray<EntryId> more_results;
+ nsTArray<Name> more_names;
+ for (const auto& parent : results) {
+ for (const auto& name : inputs) {
+ TEST_TRY_UNWRAP(EntryId result,
+ FileSystemHashSource::GenerateHash(parent, name));
+ TEST_TRY_UNWRAP(Name encoded,
+ FileSystemHashSource::EncodeHash(FileId(result)));
+
+ // Validity checks
+ ASSERT_TRUE(mozilla::IsAscii(encoded))
+ << encoded;
+ Name upperCaseVersion;
+ nsContentUtils::ASCIIToUpper(encoded, upperCaseVersion);
+ ASSERT_STREQ(asWide(upperCaseVersion).c_str(), asWide(encoded).c_str());
+
+ more_results.AppendElement(result);
+ more_names.AppendElement(encoded);
+ }
+ }
+
+ results.AppendElements(more_results);
+ names.AppendElements(more_names);
+
+ // Is the same hash encountered?
+ for (size_t i = 0; i < results.Length(); ++i) {
+ for (size_t j = i + 1; j < results.Length(); ++j) {
+ ASSERT_STRNE(results[i].get(), results[j].get());
+ }
+ }
+
+ // Is the same name encountered?
+ for (size_t i = 0; i < names.Length(); ++i) {
+ for (size_t j = i + 1; j < names.Length(); ++j) {
+ ASSERT_STRNE(asWide(names[i]).c_str(), asWide(names[j]).c_str());
+ }
+ }
+};
+
+TEST(TestFileSystemHashSource, encodeGeneratedHash)
+{
+ Name expected = u"HF6FOFV72G3NMDEJKYMVRIFJO4X5ZNZCF2GM7Q4Y5Q3E7NPQKSLA"_ns;
+ ASSERT_EQ(kExpectedLength, expected.Length());
+
+ EntryId parent = "a"_ns;
+ Name name = u"b"_ns;
+ TEST_TRY_UNWRAP(EntryId entry,
+ FileSystemHashSource::GenerateHash(parent, name));
+ ASSERT_EQ(sha256ByteLength, entry.Length());
+
+ TEST_TRY_UNWRAP(Name result, FileSystemHashSource::EncodeHash(FileId(entry)));
+ ASSERT_EQ(kExpectedLength, result.Length());
+ ASSERT_STREQ(asWide(expected).c_str(), asWide(result).c_str());
+
+ // Generate further hashes
+ TEST_TRY_UNWRAP(entry, FileSystemHashSource::GenerateHash(entry, result));
+ ASSERT_EQ(sha256ByteLength, entry.Length());
+
+ TEST_TRY_UNWRAP(result, FileSystemHashSource::EncodeHash(FileId(entry)));
+
+ // Always the same length
+ ASSERT_EQ(kExpectedLength, result.Length());
+
+ // Encoded versions should differ
+ ASSERT_STRNE(asWide(expected).c_str(), asWide(result).c_str());
+
+ // Padding length should have been stripped
+ char16_t padding = u"="_ns[0];
+ const int32_t paddingStart = result.FindChar(padding);
+ ASSERT_EQ(-1, paddingStart);
+};
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp b/dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp
new file mode 100644
index 0000000000..d62dfb1229
--- /dev/null
+++ b/dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp
@@ -0,0 +1,474 @@
+/* -*- 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 "FileSystemParentTypes.h"
+#include "TestHelpers.h"
+#include "datamodel/FileSystemDataManager.h"
+#include "datamodel/FileSystemDatabaseManager.h"
+#include "datamodel/FileSystemFileManager.h"
+#include "gtest/gtest.h"
+#include "mozIStorageService.h"
+#include "mozStorageCID.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/dom/FileSystemQuotaClientFactory.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/quota/FileStreams.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/QuotaManagerService.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/dom/quota/UsageInfo.h"
+#include "mozilla/dom/quota/test/QuotaManagerDependencyFixture.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "nsIFile.h"
+#include "nsIFileURL.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIQuotaCallbacks.h"
+#include "nsIQuotaRequests.h"
+#include "nsNetUtil.h"
+#include "nsScriptSecurityManager.h"
+
+namespace mozilla::dom::fs::test {
+
+quota::OriginMetadata GetTestQuotaOriginMetadata() {
+ return quota::OriginMetadata{""_ns,
+ "quotaexample.com"_ns,
+ "http://quotaexample.com"_ns,
+ "http://quotaexample.com"_ns,
+ /* aIsPrivate */ false,
+ quota::PERSISTENCE_TYPE_DEFAULT};
+}
+
+class TestFileSystemQuotaClient
+ : public quota::test::QuotaManagerDependencyFixture {
+ public:
+ static const int sPage = 64 * 512;
+ // ExceedsPreallocation value may depend on platform and sqlite version!
+ static const int sExceedsPreallocation = sPage;
+
+ protected:
+ void SetUp() override { ASSERT_NO_FATAL_FAILURE(InitializeFixture()); }
+
+ void TearDown() override {
+ EXPECT_NO_FATAL_FAILURE(
+ ClearStoragesForOrigin(GetTestQuotaOriginMetadata()));
+ ASSERT_NO_FATAL_FAILURE(ShutdownFixture());
+ }
+
+ static const Name& GetTestFileName() {
+ static Name testFileName = []() {
+ nsCString testCFileName;
+ testCFileName.SetLength(sExceedsPreallocation);
+ std::fill(testCFileName.BeginWriting(), testCFileName.EndWriting(), 'x');
+ return NS_ConvertASCIItoUTF16(testCFileName.BeginReading(),
+ sExceedsPreallocation);
+ }();
+
+ return testFileName;
+ }
+
+ static uint64_t BytesOfName(const Name& aName) {
+ return static_cast<uint64_t>(aName.Length() * sizeof(Name::char_type));
+ }
+
+ static const nsCString& GetTestData() {
+ static const nsCString sTestData = "There is a way out of every box"_ns;
+ return sTestData;
+ }
+
+ static void CreateNewEmptyFile(
+ data::FileSystemDatabaseManager* const aDatabaseManager,
+ const FileSystemChildMetadata& aFileSlot, EntryId& aEntryId) {
+ // The file should not exist yet
+ Result<EntryId, QMResult> existingTestFile =
+ aDatabaseManager->GetOrCreateFile(aFileSlot, /* create */ false);
+ ASSERT_TRUE(existingTestFile.isErr());
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR,
+ ToNSResult(existingTestFile.unwrapErr()));
+
+ // Create a new file
+ TEST_TRY_UNWRAP(aEntryId, aDatabaseManager->GetOrCreateFile(
+ aFileSlot, /* create */ true));
+ }
+
+ static void WriteDataToFile(
+ data::FileSystemDatabaseManager* const aDatabaseManager,
+ const EntryId& aEntryId, const nsCString& aData) {
+ TEST_TRY_UNWRAP(FileId fileId, aDatabaseManager->EnsureFileId(aEntryId));
+ ASSERT_FALSE(fileId.IsEmpty());
+
+ ContentType type;
+ TimeStamp lastModMilliS = 0;
+ Path path;
+ nsCOMPtr<nsIFile> fileObj;
+ ASSERT_NSEQ(NS_OK,
+ aDatabaseManager->GetFile(aEntryId, fileId, FileMode::EXCLUSIVE,
+ type, lastModMilliS, path, fileObj));
+
+ uint32_t written = 0;
+ ASSERT_NE(written, aData.Length());
+
+ const quota::OriginMetadata& testOriginMeta = GetTestQuotaOriginMetadata();
+
+ TEST_TRY_UNWRAP(nsCOMPtr<nsIOutputStream> fileStream,
+ quota::CreateFileOutputStream(
+ quota::PERSISTENCE_TYPE_DEFAULT, testOriginMeta,
+ quota::Client::FILESYSTEM, fileObj));
+
+ auto finallyClose = MakeScopeExit(
+ [&fileStream]() { ASSERT_NSEQ(NS_OK, fileStream->Close()); });
+ ASSERT_NSEQ(NS_OK,
+ fileStream->Write(aData.get(), aData.Length(), &written));
+
+ ASSERT_EQ(aData.Length(), written);
+ }
+
+ /* Static for use in callbacks */
+ static void CreateRegisteredDataManager(
+ Registered<data::FileSystemDataManager>& aRegisteredDataManager) {
+ bool done = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestQuotaOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&aRegisteredDataManager,
+ &done](Registered<data::FileSystemDataManager>
+ registeredDataManager) mutable {
+ auto doneOnReturn = MakeScopeExit([&done]() { done = true; });
+
+ ASSERT_TRUE(registeredDataManager->IsOpen());
+ aRegisteredDataManager = std::move(registeredDataManager);
+ },
+ [&done](nsresult rejectValue) {
+ auto doneOnReturn = MakeScopeExit([&done]() { done = true; });
+
+ ASSERT_NSEQ(NS_OK, rejectValue);
+ });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+
+ ASSERT_TRUE(aRegisteredDataManager);
+ ASSERT_TRUE(aRegisteredDataManager->IsOpen());
+ ASSERT_TRUE(aRegisteredDataManager->MutableDatabaseManagerPtr());
+ }
+
+ static void CheckUsageEqualTo(const quota::UsageInfo& aUsage,
+ uint64_t expected) {
+ EXPECT_TRUE(aUsage.FileUsage().isNothing());
+ auto dbUsage = aUsage.DatabaseUsage();
+ ASSERT_TRUE(dbUsage.isSome());
+ const auto actual = dbUsage.value();
+ ASSERT_EQ(actual, expected);
+ }
+
+ static void CheckUsageGreaterThan(const quota::UsageInfo& aUsage,
+ uint64_t expected) {
+ EXPECT_TRUE(aUsage.FileUsage().isNothing());
+ auto dbUsage = aUsage.DatabaseUsage();
+ ASSERT_TRUE(dbUsage.isSome());
+ const auto actual = dbUsage.value();
+ ASSERT_GT(actual, expected);
+ }
+};
+
+TEST_F(TestFileSystemQuotaClient, CheckUsageBeforeAnyFilesOnDisk) {
+ auto backgroundTask = []() {
+ mozilla::Atomic<bool> isCanceled{false};
+ auto ioTask = [&isCanceled](const RefPtr<quota::Client>& quotaClient,
+ data::FileSystemDatabaseManager* dbm) {
+ ASSERT_FALSE(isCanceled);
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+ const Origin& testOrigin = testOriginMeta.mOrigin;
+
+ // After initialization,
+ // * database size is not zero
+ // * GetUsageForOrigin and InitOrigin should agree
+ TEST_TRY_UNWRAP(quota::UsageInfo usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageGreaterThan(usageNow, 0u));
+ const auto initialDbUsage = usageNow.DatabaseUsage().value();
+
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, initialDbUsage));
+
+ // Create a new file
+ TEST_TRY_UNWRAP(const EntryId rootId, data::GetRootHandle(testOrigin));
+ FileSystemChildMetadata fileData(rootId, GetTestFileName());
+
+ EntryId testFileId;
+ ASSERT_NO_FATAL_FAILURE(CreateNewEmptyFile(dbm, fileData, testFileId));
+
+ // After a new file has been created (only in the database),
+ // * database size has increased
+ // * GetUsageForOrigin and InitOrigin should agree
+ const auto expectedUse = initialDbUsage + 2 * sPage;
+
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedUse));
+
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedUse));
+ };
+
+ RefPtr<mozilla::dom::quota::Client> quotaClient = fs::CreateQuotaClient();
+ ASSERT_TRUE(quotaClient);
+
+ // For uninitialized database, file usage is nothing
+ auto checkTask =
+ [&isCanceled](const RefPtr<mozilla::dom::quota::Client>& quotaClient) {
+ TEST_TRY_UNWRAP(quota::UsageInfo usageNow,
+ quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ GetTestQuotaOriginMetadata(), isCanceled));
+
+ ASSERT_TRUE(usageNow.DatabaseUsage().isNothing());
+ EXPECT_TRUE(usageNow.FileUsage().isNothing());
+ };
+
+ PerformOnIOThread(std::move(checkTask),
+ RefPtr<mozilla::dom::quota::Client>{quotaClient});
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ // Run tests with an initialized database
+ PerformOnIOThread(std::move(ioTask), std::move(quotaClient),
+ rdm->MutableDatabaseManagerPtr());
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemQuotaClient, WritesToFilesShouldIncreaseUsage) {
+ auto backgroundTask = []() {
+ mozilla::Atomic<bool> isCanceled{false};
+ auto ioTask = [&isCanceled](
+ const RefPtr<mozilla::dom::quota::Client>& quotaClient,
+ data::FileSystemDatabaseManager* dbm) {
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+ const Origin& testOrigin = testOriginMeta.mOrigin;
+
+ TEST_TRY_UNWRAP(const EntryId rootId, data::GetRootHandle(testOrigin));
+ FileSystemChildMetadata fileData(rootId, GetTestFileName());
+
+ EntryId testFileId;
+ ASSERT_NO_FATAL_FAILURE(CreateNewEmptyFile(dbm, fileData, testFileId));
+ // const auto testFileDbUsage = usageNow.DatabaseUsage().value();
+
+ TEST_TRY_UNWRAP(
+ quota::UsageInfo usageNow,
+ quotaClient->GetUsageForOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_TRUE(usageNow.DatabaseUsage().isSome());
+ const auto testFileDbUsage = usageNow.DatabaseUsage().value();
+
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, testFileDbUsage));
+
+ // Fill the file with some content
+ const nsCString& testData = GetTestData();
+
+ ASSERT_NO_FATAL_FAILURE(WriteDataToFile(dbm, testFileId, testData));
+
+ // In this test we don't lock the file -> no rescan is expected
+ // and InitOrigin should return the previous value
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, testFileDbUsage));
+
+ // When data manager unlocks the file, it should call update
+ // but in this test we call it directly
+ ASSERT_NSEQ(NS_OK, dbm->UpdateUsage(FileId(testFileId)));
+
+ const auto expectedTotalUsage = testFileDbUsage + testData.Length();
+
+ // Disk usage should have increased after writing
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedTotalUsage));
+
+ // The usage values should now agree
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedTotalUsage));
+ };
+
+ RefPtr<mozilla::dom::quota::Client> quotaClient = fs::CreateQuotaClient();
+ ASSERT_TRUE(quotaClient);
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ // Run tests with an initialized database
+ PerformOnIOThread(std::move(ioTask), std::move(quotaClient),
+ rdm->MutableDatabaseManagerPtr());
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemQuotaClient, TrackedFilesOnInitOriginShouldCauseRescan) {
+ auto backgroundTask = []() {
+ mozilla::Atomic<bool> isCanceled{false};
+ EntryId* testFileId = new EntryId();
+ auto cleanupFileId = MakeScopeExit([&testFileId] { delete testFileId; });
+
+ auto fileCreation = [&testFileId](data::FileSystemDatabaseManager* dbm) {
+ const Origin& testOrigin = GetTestQuotaOriginMetadata().mOrigin;
+
+ TEST_TRY_UNWRAP(const EntryId rootId, data::GetRootHandle(testOrigin));
+ FileSystemChildMetadata fileData(rootId, GetTestFileName());
+
+ EntryId someId;
+ ASSERT_NO_FATAL_FAILURE(CreateNewEmptyFile(dbm, fileData, someId));
+ testFileId->Append(someId);
+ };
+
+ auto writingToFile =
+ [&isCanceled, testFileId](
+ const RefPtr<mozilla::dom::quota::Client>& quotaClient,
+ data::FileSystemDatabaseManager* dbm) {
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+ TEST_TRY_UNWRAP(
+ quota::UsageInfo usageNow,
+ quotaClient->GetUsageForOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_TRUE(usageNow.DatabaseUsage().isSome());
+ const auto testFileDbUsage = usageNow.DatabaseUsage().value();
+
+ // Fill the file with some content
+ const auto& testData = GetTestData();
+ const auto expectedTotalUsage = testFileDbUsage + testData.Length();
+
+ ASSERT_NO_FATAL_FAILURE(WriteDataToFile(dbm, *testFileId, testData));
+
+ // We don't call update now - because the file is tracked and
+ // InitOrigin should perform a rescan
+ TEST_TRY_UNWRAP(
+ usageNow, quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(
+ CheckUsageEqualTo(usageNow, expectedTotalUsage));
+
+ // As always, the cached and scanned values should agree
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(
+ CheckUsageEqualTo(usageNow, expectedTotalUsage));
+ };
+
+ RefPtr<mozilla::dom::quota::Client> quotaClient = fs::CreateQuotaClient();
+ ASSERT_TRUE(quotaClient);
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ PerformOnIOThread(std::move(fileCreation),
+ rdm->MutableDatabaseManagerPtr());
+
+ // This should force a rescan
+ TEST_TRY_UNWRAP(FileId fileId, rdm->LockExclusive(*testFileId));
+ ASSERT_FALSE(fileId.IsEmpty());
+ PerformOnIOThread(std::move(writingToFile), std::move(quotaClient),
+ rdm->MutableDatabaseManagerPtr());
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemQuotaClient, RemovingFileShouldDecreaseUsage) {
+ auto backgroundTask = []() {
+ mozilla::Atomic<bool> isCanceled{false};
+ auto ioTask = [&isCanceled](
+ const RefPtr<mozilla::dom::quota::Client>& quotaClient,
+ data::FileSystemDatabaseManager* dbm) {
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+ const Origin& testOrigin = testOriginMeta.mOrigin;
+
+ TEST_TRY_UNWRAP(const EntryId rootId, data::GetRootHandle(testOrigin));
+ FileSystemChildMetadata fileData(rootId, GetTestFileName());
+
+ EntryId testFileId;
+ ASSERT_NO_FATAL_FAILURE(CreateNewEmptyFile(dbm, fileData, testFileId));
+ TEST_TRY_UNWRAP(quota::UsageInfo usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_TRUE(usageNow.DatabaseUsage().isSome());
+ const auto testFileDbUsage = usageNow.DatabaseUsage().value();
+
+ // Fill the file with some content
+ const nsCString& testData = GetTestData();
+ const auto expectedTotalUsage = testFileDbUsage + testData.Length();
+
+ ASSERT_NO_FATAL_FAILURE(WriteDataToFile(dbm, testFileId, testData));
+
+ // Currently, usage is expected to be updated on unlock by data manager
+ // but here UpdateUsage() is called directly
+ ASSERT_NSEQ(NS_OK, dbm->UpdateUsage(FileId(testFileId)));
+
+ // At least some file disk usage should have appeared after unlocking
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedTotalUsage));
+
+ TEST_TRY_UNWRAP(bool wasRemoved,
+ dbm->RemoveFile({rootId, GetTestFileName()}));
+ ASSERT_TRUE(wasRemoved);
+
+ // Removes cascade and usage table should be up to date immediately
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, testFileDbUsage));
+
+ // GetUsageForOrigin should agree
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, testFileDbUsage));
+ };
+
+ RefPtr<mozilla::dom::quota::Client> quotaClient = fs::CreateQuotaClient();
+ ASSERT_TRUE(quotaClient);
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ // Run tests with an initialized database
+ PerformOnIOThread(std::move(ioTask), std::move(quotaClient),
+ rdm->MutableDatabaseManagerPtr());
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp
new file mode 100644
index 0000000000..484def797f
--- /dev/null
+++ b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp
@@ -0,0 +1,185 @@
+/* -*- 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 "FileSystemDataManager.h"
+#include "TestHelpers.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "mozilla/dom/quota/test/QuotaManagerDependencyFixture.h"
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemDataManager
+ : public quota::test::QuotaManagerDependencyFixture {
+ public:
+ static void SetUpTestCase() { ASSERT_NO_FATAL_FAILURE(InitializeFixture()); }
+
+ static void TearDownTestCase() {
+ EXPECT_NO_FATAL_FAILURE(ClearStoragesForOrigin(GetTestOriginMetadata()));
+ ASSERT_NO_FATAL_FAILURE(ShutdownFixture());
+ }
+};
+
+TEST_F(TestFileSystemDataManager, GetOrCreateFileSystemDataManager) {
+ auto backgroundTask = []() {
+ bool done = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [](Registered<data::FileSystemDataManager> registeredDataManager) {
+ RefPtr<data::FileSystemDataManager> dataManager =
+ registeredDataManager.get();
+
+ registeredDataManager = nullptr;
+
+ return dataManager->OnClose();
+ },
+ [](nsresult rejectValue) {
+ return BoolPromise::CreateAndReject(rejectValue, __func__);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&done](const BoolPromise::ResolveOrRejectValue&) { done = true; });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemDataManager,
+ GetOrCreateFileSystemDataManager_PendingOpen) {
+ auto backgroundTask = []() {
+ Registered<data::FileSystemDataManager> rdm1;
+
+ Registered<data::FileSystemDataManager> rdm2;
+
+ {
+ bool done1 = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&rdm1, &done1](Registered<data::FileSystemDataManager>
+ registeredDataManager) {
+ ASSERT_TRUE(registeredDataManager->IsOpen());
+
+ rdm1 = std::move(registeredDataManager);
+
+ done1 = true;
+ },
+ [&done1](nsresult rejectValue) { done1 = true; });
+
+ bool done2 = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&rdm2, &done2](Registered<data::FileSystemDataManager>
+ registeredDataManager) {
+ ASSERT_TRUE(registeredDataManager->IsOpen());
+
+ rdm2 = std::move(registeredDataManager);
+
+ done2 = true;
+ },
+ [&done2](nsresult rejectValue) { done2 = true; });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns,
+ [&done1, &done2]() { return done1 && done2; });
+ }
+
+ RefPtr<data::FileSystemDataManager> dm1 = rdm1.unwrap();
+
+ RefPtr<data::FileSystemDataManager> dm2 = rdm2.unwrap();
+
+ {
+ bool done1 = false;
+
+ dm1->OnClose()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&done1](const BoolPromise::ResolveOrRejectValue&) { done1 = true; });
+
+ bool done2 = false;
+
+ dm2->OnClose()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&done2](const BoolPromise::ResolveOrRejectValue&) { done2 = true; });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns,
+ [&done1, &done2]() { return done1 && done2; });
+ }
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemDataManager,
+ GetOrCreateFileSystemDataManager_PendingClose) {
+ auto backgroundTask = []() {
+ Registered<data::FileSystemDataManager> rdm;
+
+ {
+ bool done = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&rdm, &done](Registered<data::FileSystemDataManager>
+ registeredDataManager) {
+ ASSERT_TRUE(registeredDataManager->IsOpen());
+
+ rdm = std::move(registeredDataManager);
+
+ done = true;
+ },
+ [&done](nsresult rejectValue) { done = true; });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+ }
+
+ RefPtr<data::FileSystemDataManager> dm = rdm.unwrap();
+
+ Unused << dm;
+
+ {
+ bool done = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [](Registered<data::FileSystemDataManager>
+ registeredDataManager) {
+ RefPtr<data::FileSystemDataManager> dataManager =
+ registeredDataManager.get();
+
+ registeredDataManager = nullptr;
+
+ return dataManager->OnClose();
+ },
+ [](nsresult rejectValue) {
+ return BoolPromise::CreateAndReject(rejectValue, __func__);
+ })
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [&done](const BoolPromise::ResolveOrRejectValue&) {
+ done = true;
+ });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+ }
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp
new file mode 100644
index 0000000000..9782e534e9
--- /dev/null
+++ b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp
@@ -0,0 +1,1059 @@
+/* -*- 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 <algorithm>
+
+#include "ErrorList.h"
+#include "FileSystemDataManager.h"
+#include "FileSystemDatabaseManagerVersion001.h"
+#include "FileSystemDatabaseManagerVersion002.h"
+#include "FileSystemFileManager.h"
+#include "FileSystemHashSource.h"
+#include "ResultStatement.h"
+#include "SchemaVersion001.h"
+#include "SchemaVersion002.h"
+#include "TestHelpers.h"
+#include "gtest/gtest.h"
+#include "mozIStorageService.h"
+#include "mozStorageCID.h"
+#include "mozStorageHelper.h"
+#include "mozilla/Array.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "mozilla/dom/quota/test/QuotaManagerDependencyFixture.h"
+#include "nsContentUtils.h"
+#include "nsIFile.h"
+#include "nsLiteralString.h"
+#include "nsNetCID.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTHashSet.h"
+
+namespace mozilla::dom::fs::test {
+
+using data::FileSystemDatabaseManagerVersion001;
+using data::FileSystemDatabaseManagerVersion002;
+using data::FileSystemFileManager;
+
+quota::OriginMetadata GetOriginMetadataSample() {
+ return quota::OriginMetadata{""_ns,
+ "firefox.com"_ns,
+ "http://firefox.com"_ns,
+ "http://firefox.com"_ns,
+ /* aIsPrivate */ false,
+ quota::PERSISTENCE_TYPE_DEFAULT};
+}
+
+class TestFileSystemDatabaseManagerVersionsBase
+ : public quota::test::QuotaManagerDependencyFixture {
+ public:
+ void SetUp() override { ASSERT_NO_FATAL_FAILURE(InitializeFixture()); }
+
+ void TearDown() override {
+ EXPECT_NO_FATAL_FAILURE(ClearStoragesForOrigin(GetOriginMetadataSample()));
+ ASSERT_NO_FATAL_FAILURE(ShutdownFixture());
+ }
+};
+
+class TestFileSystemDatabaseManagerVersions
+ : public TestFileSystemDatabaseManagerVersionsBase,
+ public ::testing::WithParamInterface<DatabaseVersion> {
+ public:
+ static void AssertEntryIdMoved(const EntryId& aOriginal,
+ const EntryId& aMoved) {
+ switch (sVersion) {
+ case 1: {
+ ASSERT_EQ(aOriginal, aMoved);
+ break;
+ }
+ case 2: {
+ ASSERT_NE(aOriginal, aMoved);
+ break;
+ }
+ default: {
+ ASSERT_FALSE(false)
+ << "Unknown database version";
+ }
+ }
+ }
+
+ static void AssertEntryIdCollision(const EntryId& aOriginal,
+ const EntryId& aMoved) {
+ switch (sVersion) {
+ case 1: {
+ // We generated a new entryId
+ ASSERT_NE(aOriginal, aMoved);
+ break;
+ }
+ case 2: {
+ // We get the same entryId for the same input
+ ASSERT_EQ(aOriginal, aMoved);
+ break;
+ }
+ default: {
+ ASSERT_FALSE(false)
+ << "Unknown database version";
+ }
+ }
+ }
+
+ static DatabaseVersion sVersion;
+};
+
+// This is a minimal mock to allow us to safely call the lock methods
+// while avoiding assertions
+class MockFileSystemDataManager final : public data::FileSystemDataManager {
+ public:
+ MockFileSystemDataManager(const quota::OriginMetadata& aOriginMetadata,
+ MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget,
+ MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue)
+ : FileSystemDataManager(aOriginMetadata, nullptr, std::move(aIOTarget),
+ std::move(aIOTaskQueue)) {}
+
+ void SetDatabaseManager(data::FileSystemDatabaseManager* aDatabaseManager) {
+ mDatabaseManager =
+ UniquePtr<data::FileSystemDatabaseManager>(aDatabaseManager);
+ }
+
+ virtual ~MockFileSystemDataManager() {
+ mDatabaseManager->Close();
+ mDatabaseManager = nullptr;
+
+ // Need to avoid assertions
+ mState = State::Closed;
+ }
+};
+
+static void MakeDatabaseManagerVersions(
+ const DatabaseVersion aVersion,
+ RefPtr<MockFileSystemDataManager>& aDataManager,
+ FileSystemDatabaseManagerVersion001*& aDatabaseManager) {
+ TEST_TRY_UNWRAP(auto storageService,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ const auto flags = mozIStorageService::CONNECTION_DEFAULT;
+ ResultConnection connection;
+
+ nsresult rv = storageService->OpenSpecialDatabase(kMozStorageMemoryStorageKey,
+ VoidCString(), flags,
+ getter_AddRefs(connection));
+ ASSERT_NSEQ(NS_OK, rv);
+
+ auto fmRes = FileSystemFileManager::CreateFileSystemFileManager(
+ GetOriginMetadataSample());
+ ASSERT_FALSE(fmRes.isErr());
+
+ const Origin& testOrigin = GetTestOrigin();
+
+ if (1 == aVersion) {
+ TEST_TRY_UNWRAP(
+ TestFileSystemDatabaseManagerVersions::sVersion,
+ SchemaVersion001::InitializeConnection(connection, testOrigin));
+ } else {
+ ASSERT_EQ(2, aVersion);
+
+ TEST_TRY_UNWRAP(TestFileSystemDatabaseManagerVersions::sVersion,
+ SchemaVersion002::InitializeConnection(
+ connection, *fmRes.inspect(), testOrigin));
+ }
+ ASSERT_NE(0, TestFileSystemDatabaseManagerVersions::sVersion);
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ QM_TRY_UNWRAP(auto streamTransportService,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIEventTarget>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ NS_STREAMTRANSPORTSERVICE_CONTRACTID),
+ QM_VOID);
+
+ quota::OriginMetadata originMetadata = GetOriginMetadataSample();
+
+ nsCString taskQueueName("OPFS "_ns + originMetadata.mOrigin);
+
+ RefPtr<TaskQueue> ioTaskQueue =
+ TaskQueue::Create(do_AddRef(streamTransportService), taskQueueName.get());
+
+ aDataManager = MakeRefPtr<MockFileSystemDataManager>(
+ originMetadata, WrapMovingNotNull(streamTransportService),
+ WrapMovingNotNull(ioTaskQueue));
+
+ if (1 == aVersion) {
+ aDatabaseManager = new FileSystemDatabaseManagerVersion001(
+ aDataManager, std::move(connection), fmRes.unwrap(), rootId);
+ } else {
+ ASSERT_EQ(2, aVersion);
+
+ aDatabaseManager = new FileSystemDatabaseManagerVersion002(
+ aDataManager, std::move(connection), fmRes.unwrap(), rootId);
+ }
+
+ aDataManager->SetDatabaseManager(aDatabaseManager);
+}
+
+DatabaseVersion TestFileSystemDatabaseManagerVersions::sVersion = 0;
+
+TEST_P(TestFileSystemDatabaseManagerVersions,
+ smokeTestCreateRemoveDirectories) {
+ const DatabaseVersion version = GetParam();
+
+ auto ioTask = [version]() {
+ nsresult rv = NS_OK;
+ // Ensure that FileSystemDataManager lives for the lifetime of the test
+ RefPtr<MockFileSystemDataManager> dataManager;
+ FileSystemDatabaseManagerVersion001* dm = nullptr;
+ ASSERT_NO_FATAL_FAILURE(
+ MakeDatabaseManagerVersions(version, dataManager, dm));
+ ASSERT_TRUE(dm);
+ // if any of these exit early, we have to close
+ auto autoClose = MakeScopeExit([dm] { dm->Close(); });
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ FileSystemChildMetadata firstChildMeta(rootId, u"First"_ns);
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(EntryId firstChild, dm->GetOrCreateDirectory(
+ firstChildMeta, /* create */ true));
+
+ int32_t dbVersion = 0;
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing entries,
+ dm->GetDirectoryEntries(rootId, dbVersion));
+ ASSERT_EQ(1u, entries.directories().Length());
+ ASSERT_EQ(0u, entries.files().Length());
+
+ const auto& firstItemRef = entries.directories()[0];
+ ASSERT_TRUE(u"First"_ns == firstItemRef.entryName())
+ << firstItemRef.entryName();
+ ASSERT_EQ(firstChild, firstItemRef.entryId());
+
+ TEST_TRY_UNWRAP(
+ EntryId firstChildClone,
+ dm->GetOrCreateDirectory(firstChildMeta, /* create */ true));
+ ASSERT_EQ(firstChild, firstChildClone);
+
+ FileSystemChildMetadata secondChildMeta(firstChild, u"Second"_ns);
+ TEST_TRY_UNWRAP(
+ EntryId secondChild,
+ dm->GetOrCreateDirectory(secondChildMeta, /* create */ true));
+
+ FileSystemEntryPair shortPair(firstChild, secondChild);
+ TEST_TRY_UNWRAP(Path shortPath, dm->Resolve(shortPair));
+ ASSERT_EQ(1u, shortPath.Length());
+ ASSERT_EQ(u"Second"_ns, shortPath[0]);
+
+ FileSystemEntryPair longPair(rootId, secondChild);
+ TEST_TRY_UNWRAP(Path longPath, dm->Resolve(longPair));
+ ASSERT_EQ(2u, longPath.Length());
+ ASSERT_EQ(u"First"_ns, longPath[0]);
+ ASSERT_EQ(u"Second"_ns, longPath[1]);
+
+ FileSystemEntryPair wrongPair(secondChild, rootId);
+ TEST_TRY_UNWRAP(Path emptyPath, dm->Resolve(wrongPair));
+ ASSERT_TRUE(emptyPath.IsEmpty());
+
+ PageNumber page = 0;
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing fEntries,
+ dm->GetDirectoryEntries(firstChild, page));
+ ASSERT_EQ(1u, fEntries.directories().Length());
+ ASSERT_EQ(0u, fEntries.files().Length());
+
+ const auto& secItemRef = fEntries.directories()[0];
+ ASSERT_TRUE(u"Second"_ns == secItemRef.entryName())
+ << secItemRef.entryName();
+ ASSERT_EQ(secondChild, secItemRef.entryId());
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->RemoveDirectory(firstChildMeta, /* recursive */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+
+ TEST_TRY_UNWRAP(bool isDeleted,
+ dm->RemoveDirectory(firstChildMeta, /* recursive */ true));
+ ASSERT_TRUE(isDeleted);
+
+ FileSystemChildMetadata thirdChildMeta(secondChild, u"Second"_ns);
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(thirdChildMeta, /* create */ true));
+ ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error?
+
+ dm->Close();
+ };
+
+ PerformOnIOThread(std::move(ioTask));
+}
+
+TEST_P(TestFileSystemDatabaseManagerVersions, smokeTestCreateRemoveFiles) {
+ const DatabaseVersion version = GetParam();
+
+ auto ioTask = [version]() {
+ nsresult rv = NS_OK;
+ // Ensure that FileSystemDataManager lives for the lifetime of the test
+ RefPtr<MockFileSystemDataManager> datamanager;
+ FileSystemDatabaseManagerVersion001* dm = nullptr;
+ ASSERT_NO_FATAL_FAILURE(
+ MakeDatabaseManagerVersions(version, datamanager, dm));
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ FileSystemChildMetadata firstChildMeta(rootId, u"First"_ns);
+ // If creating is not allowed, getting a file from empty root fails
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateFile(firstChildMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ // Creating a file under empty root succeeds
+ TEST_TRY_UNWRAP(EntryId firstChild,
+ dm->GetOrCreateFile(firstChildMeta, /* create */ true));
+
+ // Second time, the same file is returned
+ TEST_TRY_UNWRAP(EntryId firstChildClone,
+ dm->GetOrCreateFile(firstChildMeta, /* create */ true));
+ ASSERT_EQ(firstChild, firstChildClone);
+
+ // Directory listing returns the created file
+ PageNumber page = 0;
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing entries,
+ dm->GetDirectoryEntries(rootId, page));
+ ASSERT_EQ(0u, entries.directories().Length());
+ ASSERT_EQ(1u, entries.files().Length());
+
+ auto& firstItemRef = entries.files()[0];
+ ASSERT_TRUE(u"First"_ns == firstItemRef.entryName())
+ << firstItemRef.entryName();
+ ASSERT_EQ(firstChild, firstItemRef.entryId());
+
+ FileId fileId = FileId(firstItemRef.entryId()); // Default
+
+ ContentType type;
+ TimeStamp lastModifiedMilliSeconds;
+ Path path;
+ nsCOMPtr<nsIFile> file;
+ rv = dm->GetFile(firstItemRef.entryId(), fileId, FileMode::EXCLUSIVE, type,
+ lastModifiedMilliSeconds, path, file);
+ ASSERT_NSEQ(NS_OK, rv);
+
+ const int64_t nowMilliSeconds = PR_Now() / 1000;
+ ASSERT_GE(nowMilliSeconds, lastModifiedMilliSeconds);
+ const int64_t expectedMaxDelayMilliSeconds = 100;
+ const int64_t actualDelay = nowMilliSeconds - lastModifiedMilliSeconds;
+ ASSERT_LT(actualDelay, expectedMaxDelayMilliSeconds);
+
+ ASSERT_EQ(1u, path.Length());
+ ASSERT_STREQ(u"First"_ns, path[0]);
+
+ ASSERT_NE(nullptr, file);
+
+ // Getting the file entry as directory fails
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv);
+
+ // Getting or creating the file entry as directory also fails
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ true));
+ ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv);
+
+ // Creating a file with non existing parent hash fails
+
+ EntryId notAChildHash = "0123456789abcdef0123456789abcdef"_ns;
+ FileSystemChildMetadata notAChildMeta(notAChildHash, u"Dummy"_ns);
+ TEST_TRY_UNWRAP_ERR(rv,
+ dm->GetOrCreateFile(notAChildMeta, /* create */ true));
+ ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error?
+
+ // We create a directory under root
+ FileSystemChildMetadata secondChildMeta(rootId, u"Second"_ns);
+ TEST_TRY_UNWRAP(
+ EntryId secondChild,
+ dm->GetOrCreateDirectory(secondChildMeta, /* create */ true));
+
+ // The root should now contain the existing file and the new directory
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing fEntries,
+ dm->GetDirectoryEntries(rootId, page));
+ ASSERT_EQ(1u, fEntries.directories().Length());
+ ASSERT_EQ(1u, fEntries.files().Length());
+
+ const auto& secItemRef = fEntries.directories()[0];
+ ASSERT_TRUE(u"Second"_ns == secItemRef.entryName())
+ << secItemRef.entryName();
+ ASSERT_EQ(secondChild, secItemRef.entryId());
+
+ // Create a file under the new directory
+ FileSystemChildMetadata thirdChildMeta(secondChild, u"Third"_ns);
+ TEST_TRY_UNWRAP(EntryId thirdChild,
+ dm->GetOrCreateFile(thirdChildMeta, /* create */ true));
+
+ FileSystemEntryPair entryPair(rootId, thirdChild);
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve(entryPair));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_EQ(u"Second"_ns, entryPath[0]);
+ ASSERT_EQ(u"Third"_ns, entryPath[1]);
+
+ // If recursion is not allowed, the non-empty new directory may not be
+ // removed
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->RemoveDirectory(secondChildMeta, /* recursive */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+
+ // If recursion is allowed, the new directory goes away.
+ TEST_TRY_UNWRAP(bool isDeleted,
+ dm->RemoveDirectory(secondChildMeta, /* recursive */ true));
+ ASSERT_TRUE(isDeleted);
+
+ // The file under the removed directory is no longer accessible.
+ TEST_TRY_UNWRAP_ERR(rv,
+ dm->GetOrCreateFile(thirdChildMeta, /* create */ true));
+ ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error?
+
+ // The deletion is reflected by the root directory listing
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing nEntries,
+ dm->GetDirectoryEntries(rootId, 0));
+ ASSERT_EQ(0u, nEntries.directories().Length());
+ ASSERT_EQ(1u, nEntries.files().Length());
+
+ const auto& fileItemRef = nEntries.files()[0];
+ ASSERT_TRUE(u"First"_ns == fileItemRef.entryName())
+ << fileItemRef.entryName();
+ ASSERT_EQ(firstChild, fileItemRef.entryId());
+
+ dm->Close();
+ };
+
+ PerformOnIOThread(std::move(ioTask));
+}
+
+TEST_P(TestFileSystemDatabaseManagerVersions, smokeTestCreateMoveDirectories) {
+ const DatabaseVersion version = GetParam();
+
+ auto ioTask = [version]() {
+ // Ensure that FileSystemDataManager lives for the lifetime of the test
+ RefPtr<MockFileSystemDataManager> datamanager;
+ FileSystemDatabaseManagerVersion001* dm = nullptr;
+ ASSERT_NO_FATAL_FAILURE(
+ MakeDatabaseManagerVersions(version, datamanager, dm));
+ auto closeAtExit = MakeScopeExit([&dm]() { dm->Close(); });
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ FileSystemEntryMetadata rootMeta{rootId, u"root"_ns,
+ /* is directory */ true};
+
+ {
+ // Sanity check: no existing items should be found
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(rootId, /* page */ 0u));
+ ASSERT_TRUE(contents.directories().IsEmpty());
+ ASSERT_TRUE(contents.files().IsEmpty());
+ }
+
+ // Create subdirectory
+ FileSystemChildMetadata firstChildMeta(rootId, u"First"_ns);
+ TEST_TRY_UNWRAP(
+ EntryId firstChildDir,
+ dm->GetOrCreateDirectory(firstChildMeta, /* create */ true));
+
+ {
+ // Check that directory listing is as expected
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(rootId, /* page */ 0u));
+ ASSERT_TRUE(contents.files().IsEmpty());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ // Try to move subdirectory to its current location
+ FileSystemEntryMetadata src{firstChildDir, firstChildMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{rootId, src.entryName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDir = moved;
+ }
+
+ {
+ // Try to move subdirectory under itself
+ FileSystemEntryMetadata src{firstChildDir, firstChildMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{src.entryId(), src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ {
+ // Try to move root under its subdirectory
+ FileSystemEntryMetadata src{rootId, rootMeta.entryName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir, src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+ }
+
+ // Create subsubdirectory
+ FileSystemChildMetadata firstChildDescendantMeta(firstChildDir,
+ u"Descendant"_ns);
+ TEST_TRY_UNWRAP(EntryId firstChildDescendant,
+ dm->GetOrCreateDirectory(firstChildDescendantMeta,
+ /* create */ true));
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDir, /* page */ 0u));
+ ASSERT_TRUE(contents.files().IsEmpty());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildDescendantMeta.childName(),
+ contents.directories()[0].entryName());
+
+ TEST_TRY_UNWRAP(
+ Path subSubDirPath,
+ dm->Resolve({rootId, contents.directories()[0].entryId()}));
+ ASSERT_EQ(2u, subSubDirPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), subSubDirPath[0]);
+ ASSERT_STREQ(firstChildDescendantMeta.childName(), subSubDirPath[1]);
+ }
+
+ {
+ // Try to move subsubdirectory to its current location
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir, src.entryName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ {
+ // Try to move subsubdirectory under itself
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{src.entryId(), src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ {
+ // Try to move subdirectory under its descendant
+ FileSystemEntryMetadata src{firstChildDir, firstChildMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDescendant, src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ {
+ // Try to move root under its subsubdirectory
+ FileSystemEntryMetadata src{rootId, rootMeta.entryName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDescendant, src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+ }
+
+ // Create file in the subdirectory with already existing subsubdirectory
+ FileSystemChildMetadata testFileMeta(firstChildDir, u"Subfile"_ns);
+ TEST_TRY_UNWRAP(EntryId testFile,
+ dm->GetOrCreateFile(testFileMeta, /* create */ true));
+
+ // Get handles to the original locations of the entries
+ FileSystemEntryMetadata subSubDir;
+ FileSystemEntryMetadata subSubFile;
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDir, /* page */ 0u));
+ ASSERT_EQ(1u, contents.files().Length());
+ ASSERT_EQ(1u, contents.directories().Length());
+
+ subSubDir = contents.directories()[0];
+ ASSERT_STREQ(firstChildDescendantMeta.childName(), subSubDir.entryName());
+
+ subSubFile = contents.files()[0];
+ ASSERT_STREQ(testFileMeta.childName(), subSubFile.entryName());
+ ASSERT_EQ(testFile, subSubFile.entryId());
+ }
+
+ {
+ TEST_TRY_UNWRAP(Path entryPath,
+ dm->Resolve({rootId, subSubFile.entryId()}));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[1]);
+ }
+
+ {
+ // Try to move file to its current location
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir, src.entryName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Try to move subsubdirectory under a file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{testFile,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv);
+ }
+
+ {
+ // Silently overwrite a directory with a file using rename
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ const FileSystemChildMetadata& dest = firstChildDescendantMeta;
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Move file back and recreate the directory
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, testFileMeta));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+
+ TEST_TRY_UNWRAP(EntryId firstChildDescendantCheck,
+ dm->GetOrCreateDirectory(firstChildDescendantMeta,
+ /* create */ true));
+ ASSERT_EQ(firstChildDescendant, firstChildDescendantCheck);
+ }
+
+ {
+ // Try to rename directory to quietly overwrite a file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ const FileSystemChildMetadata& dest = testFileMeta;
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ {
+ // Move directory back and recreate the file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ testFileMeta.childName(),
+ /* is directory */ true};
+
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+
+ TEST_TRY_UNWRAP(EntryId testFileCheck,
+ dm->GetOrCreateFile(testFileMeta, /* create */ true));
+ ASSERT_EQ(testFile, testFileCheck);
+ }
+
+ {
+ // Move file one level up
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId, src.entryName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Check that listings are as expected
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDir, 0u));
+ ASSERT_TRUE(contents.files().IsEmpty());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildDescendantMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(rootId, 0u));
+ ASSERT_EQ(1u, contents.files().Length());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(testFileMeta.childName(), contents.files()[0].entryName());
+ ASSERT_STREQ(firstChildMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ ASSERT_NO_FATAL_FAILURE(
+ AssertEntryIdMoved(subSubFile.entryId(), testFile));
+ TEST_TRY_UNWRAP(Path entryPath,
+ dm->Resolve({rootId, subSubFile.entryId()}));
+ if (1 == sVersion) {
+ ASSERT_EQ(1u, entryPath.Length());
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[0]);
+ } else {
+ ASSERT_EQ(2, sVersion);
+ // Per spec, path result is empty when no path exists.
+ ASSERT_TRUE(entryPath.IsEmpty());
+ }
+ }
+
+ {
+ // Try to get a handle to the old item
+ TEST_TRY_UNWRAP_ERR(
+ nsresult rv, dm->GetOrCreateFile(testFileMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+ }
+
+ {
+ // Try to move + rename file one level down to collide with a
+ // subSubDirectory, silently overwriting it
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Restore filename, move file to its original location and recreate
+ // the overwritten directory
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId, testFileMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+
+ FileSystemChildMetadata oldLocation{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ // Is there still something out there?
+ TEST_TRY_UNWRAP_ERR(nsresult rv,
+ dm->GetOrCreateFile(oldLocation, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(EntryId firstChildDescendantCheck,
+ dm->GetOrCreateDirectory(oldLocation, /* create */ true));
+ ASSERT_EQ(firstChildDescendant, firstChildDescendantCheck);
+ }
+
+ // Rename file first and then try to move it to collide with
+ // subSubDirectory, silently overwriting it
+ {
+ // Rename
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Try to move one level down
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Move the file back and recreate the directory
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+
+ FileSystemChildMetadata oldLocation{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ // Is there still something out there?
+ TEST_TRY_UNWRAP_ERR(nsresult rv,
+ dm->GetOrCreateFile(oldLocation, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(EntryId firstChildDescendantCheck,
+ dm->GetOrCreateDirectory(oldLocation, /* create */ true));
+ ASSERT_EQ(firstChildDescendant, firstChildDescendantCheck);
+ }
+
+ {
+ // Try to move subSubDirectory one level up to quietly overwrite a file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{rootId,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ {
+ // Move subSubDirectory back one level down and recreate the file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+
+ FileSystemChildMetadata oldLocation{rootId,
+ firstChildDescendantMeta.childName()};
+
+ // We should no longer find anything there
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->GetOrCreateDirectory(
+ oldLocation, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(EntryId testFileCheck,
+ dm->GetOrCreateFile(oldLocation, /* create */ true));
+ ASSERT_NO_FATAL_FAILURE(AssertEntryIdCollision(testFile, testFileCheck));
+ testFile = testFileCheck;
+ }
+
+ // Create a new file in the subsubdirectory
+ FileSystemChildMetadata newFileMeta{firstChildDescendant,
+ testFileMeta.childName()};
+ EntryId oldFirstChildDescendant = firstChildDescendant;
+
+ TEST_TRY_UNWRAP(EntryId newFile,
+ dm->GetOrCreateFile(newFileMeta, /* create */ true));
+
+ {
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve({rootId, newFile}));
+ ASSERT_EQ(3u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(firstChildDescendantMeta.childName(), entryPath[1]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[2]);
+ }
+
+ {
+ // Move subSubDirectory one level up and rename it to testFile's old name
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{rootId, testFileMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ {
+ // Try to get handles to the moved items
+ TEST_TRY_UNWRAP_ERR(nsresult rv,
+ dm->GetOrCreateDirectory(firstChildDescendantMeta,
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ // Still under the same parent which was moved
+ if (1 == sVersion) {
+ TEST_TRY_UNWRAP(EntryId handle,
+ dm->GetOrCreateFile(newFileMeta, /* create */ false));
+ ASSERT_EQ(handle, newFile);
+
+ TEST_TRY_UNWRAP(
+ handle, dm->GetOrCreateDirectory({rootId, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_EQ(handle, firstChildDescendant);
+ } else if (2 == sVersion) {
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateFile(newFileMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(
+ EntryId newFileCheck,
+ dm->GetOrCreateFile({firstChildDescendant, newFileMeta.childName()},
+ /* create */ false));
+ ASSERT_FALSE(newFileCheck.IsEmpty());
+ } else {
+ ASSERT_FALSE(false)
+ << "Unknown database version";
+ }
+ }
+
+ {
+ // Check that new file path is as expected
+ TEST_TRY_UNWRAP(
+ EntryId newFileCheck,
+ dm->GetOrCreateFile({firstChildDescendant, newFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NO_FATAL_FAILURE(AssertEntryIdMoved(newFileCheck, newFile));
+ newFile = newFileCheck;
+
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve({rootId, newFile}));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[1]);
+ }
+
+ // Move first file and subSubDirectory back one level down keeping the names
+ {
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ // Flag is ignored
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Then move the directory
+ FileSystemEntryMetadata src{firstChildDescendant,
+ testFileMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir, testFileMeta.childName()};
+
+ // Flag is ignored
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ // Check that listings are as expected
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(rootId, 0u));
+ ASSERT_TRUE(contents.files().IsEmpty());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDir, 0u));
+ ASSERT_EQ(1u, contents.files().Length());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildDescendantMeta.childName(),
+ contents.files()[0].entryName());
+ ASSERT_STREQ(testFileMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDescendant, 0u));
+ ASSERT_EQ(1u, contents.files().Length());
+ ASSERT_TRUE(contents.directories().IsEmpty());
+ ASSERT_STREQ(testFileMeta.childName(), contents.files()[0].entryName());
+ }
+
+ // Names are swapped
+ {
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve({rootId, testFile}));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(firstChildDescendantMeta.childName(), entryPath[1]);
+ }
+
+ {
+ TEST_TRY_UNWRAP(Path entryPath,
+ dm->Resolve({rootId, firstChildDescendant}));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[1]);
+ }
+
+ {
+ // Check that new file path is also as expected
+ TEST_TRY_UNWRAP(
+ EntryId newFileCheck,
+ dm->GetOrCreateFile({firstChildDescendant, newFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NO_FATAL_FAILURE(AssertEntryIdMoved(newFileCheck, newFile));
+ newFile = newFileCheck;
+
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve({rootId, newFile}));
+ ASSERT_EQ(3u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[1]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[2]);
+ }
+
+ {
+ // Try to get handles to the old items
+ TEST_TRY_UNWRAP_ERR(
+ nsresult rv, dm->GetOrCreateFile({rootId, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv,
+ dm->GetOrCreateFile({rootId, firstChildDescendantMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory({rootId, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(rv,
+ dm->GetOrCreateDirectory(
+ {rootId, firstChildDescendantMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateFile({firstChildDir, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(
+ {firstChildDir, firstChildDescendantMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateFile({testFile, newFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+ }
+ };
+
+ PerformOnIOThread(std::move(ioTask));
+}
+
+INSTANTIATE_TEST_SUITE_P(TestDatabaseManagerVersions,
+ TestFileSystemDatabaseManagerVersions,
+ testing::Values(1, 2));
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/parent/datamodel/moz.build b/dom/fs/test/gtest/parent/datamodel/moz.build
new file mode 100644
index 0000000000..6369aec649
--- /dev/null
+++ b/dom/fs/test/gtest/parent/datamodel/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES = [
+ "TestFileSystemDataManager.cpp",
+ "TestFileSystemDataManagerVersions.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/include",
+ "/dom/fs/parent",
+ "/dom/fs/parent/datamodel",
+ "/dom/fs/test/gtest",
+ "/dom/fs/test/gtest/parent",
+]
diff --git a/dom/fs/test/gtest/parent/moz.build b/dom/fs/test/gtest/parent/moz.build
new file mode 100644
index 0000000000..9197c6ade2
--- /dev/null
+++ b/dom/fs/test/gtest/parent/moz.build
@@ -0,0 +1,21 @@
+# -*- 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 += ["datamodel"]
+
+UNIFIED_SOURCES = [
+ "TestFileSystemHashSource.cpp",
+ "TestFileSystemQuotaClient.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/parent",
+ "/dom/fs/test/gtest",
+]
diff --git a/dom/fs/test/gtest/shared/TestFileSystemHelpers.cpp b/dom/fs/test/gtest/shared/TestFileSystemHelpers.cpp
new file mode 100644
index 0000000000..ed38026634
--- /dev/null
+++ b/dom/fs/test/gtest/shared/TestFileSystemHelpers.cpp
@@ -0,0 +1,179 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/FileSystemHelpers.h"
+
+namespace mozilla::dom::fs {
+
+namespace {
+
+class TestObject {
+ public:
+ explicit TestObject(uint32_t aExpectedAddRefCnt = 0,
+ uint32_t aExpectedAddRegCnt = 0)
+ : mExpectedAddRefCnt(aExpectedAddRefCnt),
+ mExpectedAddRegCnt(aExpectedAddRegCnt),
+ mAddRefCnt(0),
+ mAddRegCnt(0),
+ mRefCnt(0),
+ mRegCnt(0),
+ mClosed(false) {}
+
+ uint32_t AddRef() {
+ mRefCnt++;
+ mAddRefCnt++;
+ return mRefCnt;
+ }
+
+ uint32_t Release() {
+ EXPECT_TRUE(mRefCnt > 0);
+ mRefCnt--;
+ if (mRefCnt == 0) {
+ delete this;
+ return 0;
+ }
+ return mRefCnt;
+ }
+
+ void Register() {
+ EXPECT_FALSE(mClosed);
+ mRegCnt++;
+ mAddRegCnt++;
+ }
+
+ void Unregister() {
+ EXPECT_FALSE(mClosed);
+ EXPECT_TRUE(mRegCnt > 0);
+ mRegCnt--;
+ if (mRegCnt == 0) {
+ mClosed = true;
+ }
+ }
+
+ void Foo() const {}
+
+ private:
+ ~TestObject() {
+ if (mExpectedAddRefCnt > 0) {
+ EXPECT_EQ(mAddRefCnt, mExpectedAddRefCnt);
+ }
+ if (mExpectedAddRegCnt > 0) {
+ EXPECT_EQ(mAddRegCnt, mExpectedAddRegCnt);
+ }
+ }
+
+ uint32_t mExpectedAddRefCnt;
+ uint32_t mExpectedAddRegCnt;
+ uint32_t mAddRefCnt;
+ uint32_t mAddRegCnt;
+ uint32_t mRefCnt;
+ uint32_t mRegCnt;
+ bool mClosed;
+};
+
+} // namespace
+
+TEST(TestFileSystemHelpers_Registered, Construct_Default)
+{
+ { Registered<TestObject> testObject; }
+}
+
+TEST(TestFileSystemHelpers_Registered, Construct_Copy)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(2, 2));
+ Registered<TestObject> testObject2(testObject1);
+ testObject2 = nullptr;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Construct_Move)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(1, 1));
+ Registered<TestObject> testObject2(std::move(testObject1));
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Construct_FromRefPtr)
+{
+ { Registered<TestObject> testObject(MakeRefPtr<TestObject>(1, 1)); }
+}
+
+TEST(TestFileSystemHelpers_Registered, Operator_Assign_FromNullPtr)
+{
+ {
+ Registered<TestObject> testObject;
+ testObject = nullptr;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Operator_Assign_Copy)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(2, 2));
+ Registered<TestObject> testObject2;
+ testObject2 = testObject1;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Operator_Assign_Move)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(1, 1));
+ Registered<TestObject> testObject2;
+ testObject2 = std::move(testObject1);
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Method_Inspect)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(1, 1));
+ const RefPtr<TestObject>& testObject2 = testObject1.inspect();
+ Unused << testObject2;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Method_Unwrap)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(1, 1));
+ RefPtr<TestObject> testObject2 = testObject1.unwrap();
+ Unused << testObject2;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Method_Get)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(1, 1));
+ TestObject* testObject2 = testObject1.get();
+ Unused << testObject2;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Operator_Conversion_ToRawPtr)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(1, 1));
+ TestObject* testObject2 = testObject1;
+ Unused << testObject2;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Operator_Dereference_ToRawPtr)
+{
+ {
+ Registered<TestObject> testObject(MakeRefPtr<TestObject>(1, 1));
+ testObject->Foo();
+ }
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/test/gtest/shared/moz.build b/dom/fs/test/gtest/shared/moz.build
new file mode 100644
index 0000000000..e8feae8c57
--- /dev/null
+++ b/dom/fs/test/gtest/shared/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES = [
+ "TestFileSystemHelpers.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/shared",
+ "/dom/fs/test/gtest",
+]
diff --git a/dom/fs/test/mochitest/head.js b/dom/fs/test/mochitest/head.js
new file mode 100644
index 0000000000..167e7c0c52
--- /dev/null
+++ b/dom/fs/test/mochitest/head.js
@@ -0,0 +1,82 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function require_module(id) {
+ if (!require_module.moduleLoader) {
+ const { ModuleLoader } = await import(
+ "/tests/dom/quota/test/modules/ModuleLoader.mjs"
+ );
+
+ const base = window.location.href;
+
+ const depth = "../../../../";
+
+ const { Assert } = await import("/tests/dom/quota/test/modules/Assert.mjs");
+
+ const { Utils } = await import("/tests/dom/quota/test/modules/Utils.mjs");
+
+ const proto = {
+ Assert,
+ Cr: SpecialPowers.Cr,
+ navigator,
+ TextEncoder,
+ Utils,
+ };
+
+ require_module.moduleLoader = new ModuleLoader(base, depth, proto);
+ }
+
+ return require_module.moduleLoader.require(id);
+}
+
+async function run_test_in_worker(script) {
+ const { runTestInWorker } = await import(
+ "/tests/dom/quota/test/modules/WorkerDriver.mjs"
+ );
+
+ const base = window.location.href;
+
+ const listener = {
+ onOk(value, message) {
+ ok(value, message);
+ },
+ onIs(a, b, message) {
+ is(a, b, message);
+ },
+ onInfo(message) {
+ info(message);
+ },
+ };
+
+ await runTestInWorker(script, base, listener);
+}
+
+// XXX This can be removed once we use <profile>/storage. See bug 1798015.
+async function removeAllEntries() {
+ const root = await navigator.storage.getDirectory();
+ for await (const value of root.values()) {
+ root.removeEntry(value.name, { recursive: true });
+ }
+}
+
+add_setup(async function () {
+ const { setStoragePrefs, clearStoragesForOrigin } = await import(
+ "/tests/dom/quota/test/modules/StorageUtils.mjs"
+ );
+
+ const optionalPrefsToSet = [
+ ["dom.fs.enabled", true],
+ ["dom.fs.writable_file_stream.enabled", true],
+ ["dom.workers.modules.enabled", true],
+ ];
+
+ await setStoragePrefs(optionalPrefsToSet);
+
+ SimpleTest.registerCleanupFunction(async function () {
+ await removeAllEntries();
+
+ await clearStoragesForOrigin(SpecialPowers.wrap(document).nodePrincipal);
+ });
+});
diff --git a/dom/fs/test/mochitest/mochitest.toml b/dom/fs/test/mochitest/mochitest.toml
new file mode 100644
index 0000000000..e138469768
--- /dev/null
+++ b/dom/fs/test/mochitest/mochitest.toml
@@ -0,0 +1,35 @@
+# 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]
+skip-if = ["xorigin"]
+support-files = ["head.js"]
+
+# Skip all tests if xorigin since we'll fail GetStorage() with ePartitionForeignOrDeny
+
+["test_basics.html"]
+scheme = "https"
+skip-if = [
+ "os == 'win'", # Bug 1841281
+ "os == 'linux' && debug", # Bug 1841281
+ "os == 'mac' && debug", # Bug 1841281
+]
+
+["test_basics_worker.html"]
+scheme = "https"
+
+["test_fileSystemDirectoryHandle.html"]
+scheme = "https"
+
+["test_fileSystemDirectoryHandle_worker.html"]
+scheme = "https"
+
+["test_syncAccessHandle_worker.html"]
+scheme = "https"
+
+["test_writableFileStream.html"]
+scheme = "https"
+
+["test_writableFileStream_worker.html"]
+scheme = "https"
diff --git a/dom/fs/test/mochitest/test_basics.html b/dom/fs/test/mochitest/test_basics.html
new file mode 100644
index 0000000000..5c609233b5
--- /dev/null
+++ b/dom/fs/test/mochitest/test_basics.html
@@ -0,0 +1,28 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System 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="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function init() {
+ const testSet = "dom/fs/test/common/test_basics.js";
+
+ const testCases = await require_module(testSet);
+
+ Object.values(testCases).forEach(testItem => {
+ add_task(testItem);
+ });
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/test_basics_worker.html b/dom/fs/test/mochitest/test_basics_worker.html
new file mode 100644
index 0000000000..72ad7b8c41
--- /dev/null
+++ b/dom/fs/test/mochitest/test_basics_worker.html
@@ -0,0 +1,22 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System 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="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function worker() {
+ await run_test_in_worker("worker/test_basics_worker.js");
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/test_fileSystemDirectoryHandle.html b/dom/fs/test/mochitest/test_fileSystemDirectoryHandle.html
new file mode 100644
index 0000000000..35f3a72e2f
--- /dev/null
+++ b/dom/fs/test/mochitest/test_fileSystemDirectoryHandle.html
@@ -0,0 +1,28 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System 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="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function init() {
+ const testSet = "dom/fs/test/common/test_fileSystemDirectoryHandle.js";
+
+ const testCases = await require_module(testSet);
+
+ Object.values(testCases).forEach(testItem => {
+ add_task(testItem);
+ });
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/test_fileSystemDirectoryHandle_worker.html b/dom/fs/test/mochitest/test_fileSystemDirectoryHandle_worker.html
new file mode 100644
index 0000000000..2f52079435
--- /dev/null
+++ b/dom/fs/test/mochitest/test_fileSystemDirectoryHandle_worker.html
@@ -0,0 +1,22 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System 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="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function worker() {
+ await run_test_in_worker("worker/test_fileSystemDirectoryHandle_worker.js");
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/test_syncAccessHandle_worker.html b/dom/fs/test/mochitest/test_syncAccessHandle_worker.html
new file mode 100644
index 0000000000..42980b1d55
--- /dev/null
+++ b/dom/fs/test/mochitest/test_syncAccessHandle_worker.html
@@ -0,0 +1,22 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System 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="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function worker() {
+ await run_test_in_worker("worker/test_syncAccessHandle_worker.js");
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/test_writableFileStream.html b/dom/fs/test/mochitest/test_writableFileStream.html
new file mode 100644
index 0000000000..3d39349ef5
--- /dev/null
+++ b/dom/fs/test/mochitest/test_writableFileStream.html
@@ -0,0 +1,31 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System 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="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function init() {
+ const testSet = "dom/fs/test/common/test_writableFileStream.js";
+
+ const testCases = await require_module(testSet);
+
+ Object.values(testCases).forEach(testItem => {
+ // We can't shrink storage size in a mochitest.
+ if (testItem.name != "quotaTest") {
+ add_task(testItem);
+ }
+ });
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/test_writableFileStream_worker.html b/dom/fs/test/mochitest/test_writableFileStream_worker.html
new file mode 100644
index 0000000000..a762e78c3f
--- /dev/null
+++ b/dom/fs/test/mochitest/test_writableFileStream_worker.html
@@ -0,0 +1,22 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System 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="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function worker() {
+ await run_test_in_worker("worker/test_writableFileStream_worker.js");
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/worker/.eslintrc.js b/dom/fs/test/mochitest/worker/.eslintrc.js
new file mode 100644
index 0000000000..93bf938654
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/.eslintrc.js
@@ -0,0 +1,12 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+module.exports = {
+ env: {
+ worker: true,
+ },
+};
diff --git a/dom/fs/test/mochitest/worker/dummy.js b/dom/fs/test/mochitest/worker/dummy.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/dummy.js
diff --git a/dom/fs/test/mochitest/worker/head.js b/dom/fs/test/mochitest/worker/head.js
new file mode 100644
index 0000000000..72e1869bde
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/head.js
@@ -0,0 +1,22 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function require_module(id) {
+ if (!require_module.moduleLoader) {
+ importScripts("/tests/dom/quota/test/modules/worker/ModuleLoader.js");
+
+ const base = location.href;
+
+ const depth = "../../../../../";
+
+ importScripts("/tests/dom/quota/test/modules/worker/Assert.js");
+
+ importScripts("/tests/dom/quota/test/modules/worker/Utils.js");
+
+ require_module.moduleLoader = new globalThis.ModuleLoader(base, depth);
+ }
+
+ return require_module.moduleLoader.require(id);
+}
diff --git a/dom/fs/test/mochitest/worker/mochitest.toml b/dom/fs/test/mochitest/worker/mochitest.toml
new file mode 100644
index 0000000000..830c8f267d
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/mochitest.toml
@@ -0,0 +1,15 @@
+# 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 = [
+ "head.js",
+ "test_basics_worker.js",
+ "test_fileSystemDirectoryHandle_worker.js",
+ "test_syncAccessHandle_worker.js",
+ "test_writableFileStream_worker.js",
+]
+
+["dummy.js"]
+skip-if = ["true"]
diff --git a/dom/fs/test/mochitest/worker/test_basics_worker.js b/dom/fs/test/mochitest/worker/test_basics_worker.js
new file mode 100644
index 0000000000..e4a4958071
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/test_basics_worker.js
@@ -0,0 +1,11 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module("dom/fs/test/common/test_basics.js");
+ Object.values(testCases).forEach(async testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/mochitest/worker/test_fileSystemDirectoryHandle_worker.js b/dom/fs/test/mochitest/worker/test_fileSystemDirectoryHandle_worker.js
new file mode 100644
index 0000000000..d4ba0b387c
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/test_fileSystemDirectoryHandle_worker.js
@@ -0,0 +1,13 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module(
+ "dom/fs/test/common/test_fileSystemDirectoryHandle.js"
+ );
+ Object.values(testCases).forEach(async testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/mochitest/worker/test_syncAccessHandle_worker.js b/dom/fs/test/mochitest/worker/test_syncAccessHandle_worker.js
new file mode 100644
index 0000000000..be53a5a7e1
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/test_syncAccessHandle_worker.js
@@ -0,0 +1,16 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module(
+ "dom/fs/test/common/test_syncAccessHandle.js"
+ );
+ Object.values(testCases).forEach(async testItem => {
+ // We can't shrink storage size in a mochitest.
+ if (testItem.name != "quotaTest") {
+ add_task(testItem);
+ }
+ });
+});
diff --git a/dom/fs/test/mochitest/worker/test_writableFileStream_worker.js b/dom/fs/test/mochitest/worker/test_writableFileStream_worker.js
new file mode 100644
index 0000000000..f294a719db
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/test_writableFileStream_worker.js
@@ -0,0 +1,16 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module(
+ "dom/fs/test/common/test_writableFileStream.js"
+ );
+ Object.values(testCases).forEach(async testItem => {
+ // We can't shrink storage size in a mochitest.
+ if (testItem.name != "quotaTest") {
+ add_task(testItem);
+ }
+ });
+});
diff --git a/dom/fs/test/moz.build b/dom/fs/test/moz.build
new file mode 100644
index 0000000000..5f4ac34fca
--- /dev/null
+++ b/dom/fs/test/moz.build
@@ -0,0 +1,16 @@
+# -*- 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 += [
+ "common",
+ "gtest",
+ "xpcshell",
+]
+
+MOCHITEST_MANIFESTS += [
+ "mochitest/mochitest.toml",
+ "mochitest/worker/mochitest.toml",
+]
diff --git a/dom/fs/test/xpcshell/head.js b/dom/fs/test/xpcshell/head.js
new file mode 100644
index 0000000000..4138e46ac9
--- /dev/null
+++ b/dom/fs/test/xpcshell/head.js
@@ -0,0 +1,100 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function require_module(id) {
+ if (!require_module.moduleLoader) {
+ const { ModuleLoader } = ChromeUtils.importESModule(
+ "resource://testing-common/dom/quota/test/modules/ModuleLoader.sys.mjs"
+ );
+
+ const base = Services.io.newFileURI(do_get_file("")).spec;
+
+ const depth = "../../../../";
+
+ Cu.importGlobalProperties(["storage"]);
+
+ const { Utils } = ChromeUtils.importESModule(
+ "resource://testing-common/dom/quota/test/modules/Utils.sys.mjs"
+ );
+
+ const proto = {
+ Assert,
+ Cr,
+ DOMException,
+ navigator: {
+ storage,
+ },
+ TextEncoder,
+ Utils,
+ };
+
+ require_module.moduleLoader = new ModuleLoader(base, depth, proto);
+ }
+
+ return require_module.moduleLoader.require(id);
+}
+
+async function run_test_in_worker(script) {
+ const { runTestInWorker } = ChromeUtils.importESModule(
+ "resource://testing-common/dom/quota/test/modules/WorkerDriver.sys.mjs"
+ );
+
+ const base = "resource://testing-common/dom/fs/test/xpcshell/";
+
+ const listener = {
+ onOk(value, message) {
+ ok(value, message);
+ },
+ onIs(a, b, message) {
+ Assert.equal(a, b, message);
+ },
+ onInfo(message) {
+ info(message);
+ },
+ };
+
+ await runTestInWorker(script, base, listener);
+}
+
+add_setup(async function () {
+ const {
+ setStoragePrefs,
+ clearStoragePrefs,
+ clearStoragesForOrigin,
+ resetStorage,
+ } = ChromeUtils.importESModule(
+ "resource://testing-common/dom/quota/test/modules/StorageUtils.sys.mjs"
+ );
+
+ const optionalPrefsToSet = [
+ ["dom.fs.enabled", true],
+ ["dom.fs.writable_file_stream.enabled", true],
+ ];
+
+ setStoragePrefs(optionalPrefsToSet);
+
+ registerCleanupFunction(async function () {
+ const principal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+ );
+
+ await clearStoragesForOrigin(principal);
+
+ Services.prefs.clearUserPref(
+ "dom.quotaManager.temporaryStorage.fixedLimit"
+ );
+
+ await resetStorage();
+
+ const optionalPrefsToClear = [
+ "dom.fs.enabled",
+ "dom.fs.writable_file_stream.enabled",
+ ];
+
+ clearStoragePrefs(optionalPrefsToClear);
+ });
+
+ do_get_profile();
+});
diff --git a/dom/fs/test/xpcshell/moz.build b/dom/fs/test/xpcshell/moz.build
new file mode 100644
index 0000000000..f18cc1c466
--- /dev/null
+++ b/dom/fs/test/xpcshell/moz.build
@@ -0,0 +1,13 @@
+# -*- 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 += [
+ "worker",
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "xpcshell.toml",
+]
diff --git a/dom/fs/test/xpcshell/test_basics.js b/dom/fs/test/xpcshell/test_basics.js
new file mode 100644
index 0000000000..9c70eaceb3
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_basics.js
@@ -0,0 +1,14 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testSet = "dom/fs/test/common/test_basics.js";
+
+ const testCases = await require_module(testSet);
+
+ Object.values(testCases).forEach(testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/test_basics_worker.js b/dom/fs/test/xpcshell/test_basics_worker.js
new file mode 100644
index 0000000000..4569a6a88c
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_basics_worker.js
@@ -0,0 +1,8 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function worker() {
+ await run_test_in_worker("worker/test_basics_worker.js");
+});
diff --git a/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle.js b/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle.js
new file mode 100644
index 0000000000..6349d9aab8
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle.js
@@ -0,0 +1,14 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testSet = "dom/fs/test/common/test_fileSystemDirectoryHandle.js";
+
+ const testCases = await require_module(testSet);
+
+ Object.values(testCases).forEach(testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle_worker.js b/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle_worker.js
new file mode 100644
index 0000000000..524b7352b4
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle_worker.js
@@ -0,0 +1,8 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function worker() {
+ await run_test_in_worker("worker/test_fileSystemDirectoryHandle_worker.js");
+});
diff --git a/dom/fs/test/xpcshell/test_syncAccessHandle_worker.js b/dom/fs/test/xpcshell/test_syncAccessHandle_worker.js
new file mode 100644
index 0000000000..377efab598
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_syncAccessHandle_worker.js
@@ -0,0 +1,8 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function worker() {
+ await run_test_in_worker("worker/test_syncAccessHandle_worker.js");
+});
diff --git a/dom/fs/test/xpcshell/test_writableFileStream.js b/dom/fs/test/xpcshell/test_writableFileStream.js
new file mode 100644
index 0000000000..1ac9fdb793
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_writableFileStream.js
@@ -0,0 +1,14 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testSet = "dom/fs/test/common/test_writableFileStream.js";
+
+ const testCases = await require_module(testSet);
+
+ Object.values(testCases).forEach(testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/test_writableFileStream_worker.js b/dom/fs/test/xpcshell/test_writableFileStream_worker.js
new file mode 100644
index 0000000000..879ca6c0ca
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_writableFileStream_worker.js
@@ -0,0 +1,8 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function worker() {
+ await run_test_in_worker("worker/test_writableFileStream_worker.js");
+});
diff --git a/dom/fs/test/xpcshell/worker/.eslintrc.js b/dom/fs/test/xpcshell/worker/.eslintrc.js
new file mode 100644
index 0000000000..93bf938654
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/.eslintrc.js
@@ -0,0 +1,12 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+module.exports = {
+ env: {
+ worker: true,
+ },
+};
diff --git a/dom/fs/test/xpcshell/worker/dummy.js b/dom/fs/test/xpcshell/worker/dummy.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/dummy.js
diff --git a/dom/fs/test/xpcshell/worker/head.js b/dom/fs/test/xpcshell/worker/head.js
new file mode 100644
index 0000000000..06a779841f
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/head.js
@@ -0,0 +1,22 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function require_module(id) {
+ if (!require_module.moduleLoader) {
+ importScripts("/dom/quota/test/modules/worker/ModuleLoader.js");
+
+ const base = location.href;
+
+ const depth = "../../../../../";
+
+ importScripts("/dom/quota/test/modules/worker/Assert.js");
+
+ importScripts("/dom/quota/test/modules/worker/Utils.js");
+
+ require_module.moduleLoader = new globalThis.ModuleLoader(base, depth);
+ }
+
+ return require_module.moduleLoader.require(id);
+}
diff --git a/dom/fs/test/xpcshell/worker/moz.build b/dom/fs/test/xpcshell/worker/moz.build
new file mode 100644
index 0000000000..c0230d1927
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/moz.build
@@ -0,0 +1,13 @@
+# -*- 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/.
+
+TESTING_JS_MODULES.dom.fs.test.xpcshell.worker += [
+ "head.js",
+ "test_basics_worker.js",
+ "test_fileSystemDirectoryHandle_worker.js",
+ "test_syncAccessHandle_worker.js",
+ "test_writableFileStream_worker.js",
+]
diff --git a/dom/fs/test/xpcshell/worker/test_basics_worker.js b/dom/fs/test/xpcshell/worker/test_basics_worker.js
new file mode 100644
index 0000000000..e4a4958071
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/test_basics_worker.js
@@ -0,0 +1,11 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module("dom/fs/test/common/test_basics.js");
+ Object.values(testCases).forEach(async testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/worker/test_fileSystemDirectoryHandle_worker.js b/dom/fs/test/xpcshell/worker/test_fileSystemDirectoryHandle_worker.js
new file mode 100644
index 0000000000..d4ba0b387c
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/test_fileSystemDirectoryHandle_worker.js
@@ -0,0 +1,13 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module(
+ "dom/fs/test/common/test_fileSystemDirectoryHandle.js"
+ );
+ Object.values(testCases).forEach(async testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/worker/test_syncAccessHandle_worker.js b/dom/fs/test/xpcshell/worker/test_syncAccessHandle_worker.js
new file mode 100644
index 0000000000..e6c6a96143
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/test_syncAccessHandle_worker.js
@@ -0,0 +1,13 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module(
+ "dom/fs/test/common/test_syncAccessHandle.js"
+ );
+ Object.values(testCases).forEach(async testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/worker/test_writableFileStream_worker.js b/dom/fs/test/xpcshell/worker/test_writableFileStream_worker.js
new file mode 100644
index 0000000000..1e9bb12ae8
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/test_writableFileStream_worker.js
@@ -0,0 +1,13 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module(
+ "dom/fs/test/common/test_writableFileStream.js"
+ );
+ Object.values(testCases).forEach(async testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/xpcshell.toml b/dom/fs/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..564c4f819b
--- /dev/null
+++ b/dom/fs/test/xpcshell/xpcshell.toml
@@ -0,0 +1,16 @@
+[DEFAULT]
+head = "head.js"
+
+["test_basics.js"]
+
+["test_basics_worker.js"]
+
+["test_fileSystemDirectoryHandle.js"]
+
+["test_fileSystemDirectoryHandle_worker.js"]
+
+["test_syncAccessHandle_worker.js"]
+
+["test_writableFileStream.js"]
+
+["test_writableFileStream_worker.js"]