summaryrefslogtreecommitdiffstats
path: root/dom/fs
diff options
context:
space:
mode:
Diffstat (limited to 'dom/fs')
-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.h70
-rw-r--r--dom/fs/api/FileSystemFileHandle.cpp122
-rw-r--r--dom/fs/api/FileSystemFileHandle.h61
-rw-r--r--dom/fs/api/FileSystemHandle.cpp319
-rw-r--r--dom/fs/api/FileSystemHandle.h107
-rw-r--r--dom/fs/api/FileSystemManager.cpp117
-rw-r--r--dom/fs/api/FileSystemManager.h89
-rw-r--r--dom/fs/api/FileSystemSyncAccessHandle.cpp627
-rw-r--r--dom/fs/api/FileSystemSyncAccessHandle.h124
-rw-r--r--dom/fs/api/FileSystemWritableFileStream.cpp706
-rw-r--r--dom/fs/api/FileSystemWritableFileStream.h107
-rw-r--r--dom/fs/api/moz.build34
-rw-r--r--dom/fs/child/FileSystemAccessHandleChild.cpp34
-rw-r--r--dom/fs/child/FileSystemAccessHandleChild.h41
-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.cpp238
-rw-r--r--dom/fs/child/FileSystemDirectoryIteratorFactory.h24
-rw-r--r--dom/fs/child/FileSystemEntryMetadataArray.h26
-rw-r--r--dom/fs/child/FileSystemManagerChild.cpp103
-rw-r--r--dom/fs/child/FileSystemManagerChild.h58
-rw-r--r--dom/fs/child/FileSystemRequestHandler.cpp648
-rw-r--r--dom/fs/child/FileSystemWritableFileStreamChild.cpp39
-rw-r--r--dom/fs/child/FileSystemWritableFileStreamChild.h42
-rw-r--r--dom/fs/child/moz.build29
-rw-r--r--dom/fs/include/fs/FileSystemChildFactory.h31
-rw-r--r--dom/fs/include/fs/FileSystemConstants.h18
-rw-r--r--dom/fs/include/fs/FileSystemRequestHandler.h94
-rw-r--r--dom/fs/moz.build14
-rw-r--r--dom/fs/parent/FileSystemAccessHandleParent.cpp43
-rw-r--r--dom/fs/parent/FileSystemAccessHandleParent.h45
-rw-r--r--dom/fs/parent/FileSystemHashSource.cpp67
-rw-r--r--dom/fs/parent/FileSystemHashSource.h26
-rw-r--r--dom/fs/parent/FileSystemManagerParent.cpp521
-rw-r--r--dom/fs/parent/FileSystemManagerParent.h97
-rw-r--r--dom/fs/parent/FileSystemManagerParentFactory.cpp105
-rw-r--r--dom/fs/parent/FileSystemManagerParentFactory.h39
-rw-r--r--dom/fs/parent/FileSystemQuotaClient.cpp235
-rw-r--r--dom/fs/parent/FileSystemQuotaClient.h25
-rw-r--r--dom/fs/parent/FileSystemStreamCallbacks.cpp46
-rw-r--r--dom/fs/parent/FileSystemStreamCallbacks.h44
-rw-r--r--dom/fs/parent/FileSystemWritableFileStreamParent.cpp43
-rw-r--r--dom/fs/parent/FileSystemWritableFileStreamParent.h47
-rw-r--r--dom/fs/parent/ResultConnection.h19
-rw-r--r--dom/fs/parent/ResultStatement.cpp23
-rw-r--r--dom/fs/parent/ResultStatement.h156
-rw-r--r--dom/fs/parent/datamodel/FileSystemDataManager.cpp529
-rw-r--r--dom/fs/parent/datamodel/FileSystemDataManager.h161
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp58
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManager.h151
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp1002
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h101
-rw-r--r--dom/fs/parent/datamodel/FileSystemFileManager.cpp267
-rw-r--r--dom/fs/parent/datamodel/FileSystemFileManager.h147
-rw-r--r--dom/fs/parent/datamodel/SchemaVersion001.cpp185
-rw-r--r--dom/fs/parent/datamodel/SchemaVersion001.h25
-rw-r--r--dom/fs/parent/datamodel/moz.build26
-rw-r--r--dom/fs/parent/moz.build47
-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/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/PFileSystemAccessHandle.ipdl21
-rw-r--r--dom/fs/shared/PFileSystemManager.ipdl425
-rw-r--r--dom/fs/shared/PFileSystemWritableFileStream.ipdl21
-rw-r--r--dom/fs/shared/TargetPtrHolder.h58
-rw-r--r--dom/fs/shared/moz.build30
-rw-r--r--dom/fs/test/common/.eslintrc.js13
-rw-r--r--dom/fs/test/common/dummy.js0
-rw-r--r--dom/fs/test/common/mochitest.ini14
-rw-r--r--dom/fs/test/common/moz.build20
-rw-r--r--dom/fs/test/common/nsresult.js9
-rw-r--r--dom/fs/test/common/test_basics.js151
-rw-r--r--dom/fs/test/common/test_fileSystemDirectoryHandle.js97
-rw-r--r--dom/fs/test/common/test_syncAccessHandle.js193
-rw-r--r--dom/fs/test/common/test_writableFileStream.js43
-rw-r--r--dom/fs/test/common/xpcshell.ini13
-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/crashtests.list6
-rw-r--r--dom/fs/test/gtest/FileSystemMocks.cpp93
-rw-r--r--dom/fs/test/gtest/FileSystemMocks.h343
-rw-r--r--dom/fs/test/gtest/TestHelpers.cpp57
-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.cpp149
-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.cpp374
-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.cpp179
-rw-r--r--dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp534
-rw-r--r--dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp182
-rw-r--r--dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersion001.cpp845
-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.js78
-rw-r--r--dom/fs/test/mochitest/mochitest.ini25
-rw-r--r--dom/fs/test/mochitest/test_basics.html32
-rw-r--r--dom/fs/test/mochitest/test_basics_worker.html22
-rw-r--r--dom/fs/test/mochitest/test_fileSystemDirectoryHandle.html32
-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.html32
-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.js20
-rw-r--r--dom/fs/test/mochitest/worker/mochitest.ini14
-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.js13
-rw-r--r--dom/fs/test/mochitest/worker/test_writableFileStream_worker.js13
-rw-r--r--dom/fs/test/moz.build16
-rw-r--r--dom/fs/test/xpcshell/head.js87
-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.js20
-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.ini15
146 files changed, 14467 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..36ac7455c5
--- /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 {
+ UniquePtr<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..45e7511e4a
--- /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,
+ UniquePtr<Impl> aImpl)
+ : mGlobal(aGlobal), mManager(aManager), mImpl(std::move(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..d761ae6d2b
--- /dev/null
+++ b/dom/fs/api/FileSystemDirectoryIterator.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_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:
+ virtual already_AddRefed<Promise> Next(nsIGlobalObject* aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ ErrorResult& aError) = 0;
+ virtual ~Impl() = default;
+ };
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemDirectoryIterator)
+
+ explicit FileSystemDirectoryIterator(nsIGlobalObject* aGlobal,
+ RefPtr<FileSystemManager>& aManager,
+ UniquePtr<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:
+ UniquePtr<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..beca83d0bf
--- /dev/null
+++ b/dom/fs/api/FileSystemHandle.cpp
@@ -0,0 +1,319 @@
+/* -*- 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::Name name(aName);
+ if (!aParentId.IsEmpty()) {
+ fs::FileSystemChildMetadata newMetadata;
+ newMetadata.parentId() = aParentId;
+ newMetadata.childName() = aName;
+ mRequestHandler->MoveEntry(mManager, this, mMetadata, newMetadata, promise,
+ aError);
+ } else {
+ mRequestHandler->RenameEntry(mManager, this, mMetadata, name, 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(
+ [name](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(name).get()));
+ aHandle->mMetadata.entryName() = name;
+ },
+ [](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..9c2b10f462
--- /dev/null
+++ b/dom/fs/api/FileSystemManager.cpp
@@ -0,0 +1,117 @@
+/* -*- 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/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();
+
+ if (mBackgroundRequestHandler->FileSystemManagerChildStrongRef()) {
+ // FileSystemAccessHandles prevent shutdown until they are full closed, so
+ // at this point, we should see no open FileSystemAccessHandles.
+ MOZ_ASSERT(mBackgroundRequestHandler->FileSystemManagerChildStrongRef()
+ ->AllSyncAccessHandlesClosed());
+
+ mBackgroundRequestHandler->FileSystemManagerChildStrongRef()
+ ->CloseAllWritableFileStreams();
+ }
+
+ mBackgroundRequestHandler->Shutdown();
+
+ mCreateFileSystemManagerChildPromiseRequestHolder.DisconnectIfExists();
+}
+
+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); });
+
+ mBackgroundRequestHandler->CreateFileSystemManagerChild(principalInfo)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<FileSystemManager>(this),
+ success = std::move(aSuccess), failure = std::move(aFailure)](
+ const BoolPromise::ResolveOrRejectValue& aValue) {
+ self->mCreateFileSystemManagerChildPromiseRequestHolder.Complete();
+
+ if (aValue.IsResolve()) {
+ success(self->mBackgroundRequestHandler
+ ->FileSystemManagerChildStrongRef());
+ } else {
+ failure(aValue.RejectValue());
+ }
+ })
+ ->Track(mCreateFileSystemManagerChildPromiseRequestHolder);
+}
+
+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..c30d193467
--- /dev/null
+++ b/dom/fs/api/FileSystemManager.h
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef 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"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+class FileSystemManagerChild;
+class FileSystemBackgroundRequestHandler;
+class StorageManager;
+
+namespace fs {
+class FileSystemRequestHandler;
+} // 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:
+ 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();
+
+ 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;
+
+ MozPromiseRequestHolder<BoolPromise>
+ mCreateFileSystemManagerChildPromiseRequestHolder;
+
+ 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..12d9545332
--- /dev/null
+++ b/dom/fs/api/FileSystemSyncAccessHandle.cpp
@@ -0,0 +1,627 @@
+/* -*- 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/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/FileSystemHandleBinding.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemManager.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/TargetPtrHolder.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/ipc/RandomAccessStreamUtils.h"
+#include "nsNetCID.h"
+#include "nsStreamUtils.h"
+#include "nsStringStream.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+const uint32_t kStreamCopyBlockSize = 1024 * 1024;
+
+using SizePromise = Int64Promise;
+const auto CreateAndRejectSizePromise = CreateAndRejectInt64Promise;
+
+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
+
+FileSystemSyncAccessHandle::FileSystemSyncAccessHandle(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ RefPtr<FileSystemAccessHandleChild> aActor, RefPtr<TaskQueue> aIOTaskQueue,
+ mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
+ const fs::FileSystemEntryMetadata& aMetadata)
+ : mGlobal(aGlobal),
+ mManager(aManager),
+ mActor(std::move(aActor)),
+ 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);
+}
+
+FileSystemSyncAccessHandle::~FileSystemSyncAccessHandle() {
+ MOZ_ASSERT(!mActor);
+ MOZ_ASSERT(IsClosed());
+}
+
+// static
+Result<RefPtr<FileSystemSyncAccessHandle>, nsresult>
+FileSystemSyncAccessHandle::Create(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ RefPtr<FileSystemAccessHandleChild> aActor,
+ mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
+ const fs::FileSystemEntryMetadata& aMetadata) {
+ 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(aActor), std::move(ioTaskQueue),
+ std::move(aStreamParams), aMetadata);
+
+ auto autoClose = MakeScopeExit([result] {
+ MOZ_ASSERT(result->mState == State::Initial);
+ result->mState = State::Closed;
+ result->mActor->SendClose();
+ });
+
+ WorkerPrivate* const workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+
+ 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);
+ MOZ_ASSERT(!mActor);
+ }
+}
+
+void FileSystemSyncAccessHandle::ClearActor() {
+ MOZ_ASSERT(mActor);
+
+ mActor = 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)]() {
+ QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()),
+ CreateAndRejectBoolPromise);
+
+ LOG(("%p: Closing", selfHolder->mStream.get()));
+
+ selfHolder->mStream->OutputStream()->Close();
+ selfHolder->mStream = nullptr;
+
+ 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->mActor) {
+ self->mActor->SendClose();
+ }
+
+ 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));
+
+ QM_TRY(MOZ_TO_RESULT(selfHolder->mStream->Seek(
+ nsISeekableStream::NS_SEEK_SET, aSize)),
+ CreateAndRejectBoolPromise);
+
+ QM_TRY(MOZ_TO_RESULT(selfHolder->mStream->SetEOF()),
+ 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;
+ };
+
+ const auto dataSpan = [&aBuffer]() {
+ if (aBuffer.IsArrayBuffer()) {
+ const ArrayBuffer& buffer = aBuffer.GetAsArrayBuffer();
+ buffer.ComputeState();
+ return Span{buffer.Data(), buffer.Length()};
+ }
+ MOZ_ASSERT(aBuffer.IsArrayBufferView());
+ const ArrayBufferView& buffer = aBuffer.GetAsArrayBufferView();
+ buffer.ComputeState();
+ return Span{buffer.Data(), buffer.Length()};
+ }();
+
+ // 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;
+
+ InvokeAsync(
+ mIOTaskQueue, __func__,
+ [selfHolder = fs::TargetPtrHolder(this), dataSpan, offset, aRead,
+ &totalCount]() {
+ QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()),
+ CreateAndRejectBoolPromise);
+
+ 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(),
+ dataSpan.Length()));
+
+ inputStream = selfHolder->mStream->InputStream();
+
+ outputStream =
+ FixedBufferOutputStream::Create(AsWritableChars(dataSpan));
+ } else {
+ LOG_VERBOSE(("%p: Writing %zu bytes", selfHolder->mStream.get(),
+ dataSpan.Length()));
+
+ QM_TRY(MOZ_TO_RESULT(NS_NewByteInputStream(
+ getter_AddRefs(inputStream), AsChars(dataSpan),
+ NS_ASSIGNMENT_DEPEND)),
+ CreateAndRejectBoolPromise);
+
+ outputStream = selfHolder->mStream->OutputStream();
+ }
+
+ auto promiseHolder = MakeUnique<MozPromiseHolder<BoolPromise>>();
+ RefPtr<BoolPromise> promise = promiseHolder->Ensure(__func__);
+
+ QM_TRY(MOZ_TO_RESULT(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..ec2ca9bd76
--- /dev/null
+++ b/dom/fs/api/FileSystemSyncAccessHandle.h
@@ -0,0 +1,124 @@
+/* -*- 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;
+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,
+ RefPtr<FileSystemAccessHandleChild> aActor,
+ mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
+ const fs::FileSystemEntryMetadata& aMetadata);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemSyncAccessHandle)
+
+ void LastRelease();
+
+ void ClearActor();
+
+ 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,
+ RefPtr<FileSystemAccessHandleChild> aActor,
+ RefPtr<TaskQueue> aIOTaskQueue,
+ mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
+ 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<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..69d1181c16
--- /dev/null
+++ b/dom/fs/api/FileSystemWritableFileStream.cpp
@@ -0,0 +1,706 @@
+/* -*- 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 "mozilla/Buffer.h"
+#include "mozilla/ErrorResult.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/WritableStreamDefaultController.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsIInputStream.h"
+#include "nsNetUtil.h"
+#include "private/pprio.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+class WritableFileStreamUnderlyingSinkAlgorithms final
+ : public UnderlyingSinkAlgorithmsBase {
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
+ WritableFileStreamUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase)
+
+ explicit WritableFileStreamUnderlyingSinkAlgorithms(
+ FileSystemWritableFileStream& aStream)
+ : mStream(&aStream) {}
+
+ // Streams algorithms
+ void StartCallback(JSContext* aCx,
+ WritableStreamDefaultController& aController,
+ JS::MutableHandle<JS::Value> aRetVal,
+ ErrorResult& aRv) override;
+
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> WriteCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aChunk,
+ WritableStreamDefaultController& aController, ErrorResult& aRv) override;
+
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CloseCallback(
+ JSContext* aCx, ErrorResult& aRv) override;
+
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> AbortCallback(
+ JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason,
+ ErrorResult& aRv) override;
+
+ void ReleaseObjects() override;
+
+ private:
+ ~WritableFileStreamUnderlyingSinkAlgorithms() = default;
+
+ RefPtr<FileSystemWritableFileStream> mStream;
+};
+
+/**
+ * TODO: Duplicated from netwerk/cache2/CacheFileIOManager.cpp
+ * Please remove after bug 1286601 is fixed,
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1286601
+ */
+nsresult TruncFile(PRFileDesc* aFD, int64_t aEOF) {
+#if defined(XP_UNIX)
+ if (ftruncate(PR_FileDesc2NativeHandle(aFD), aEOF) != 0) {
+ NS_ERROR("ftruncate failed");
+ return NS_ERROR_FAILURE;
+ }
+#elif defined(XP_WIN)
+ const int64_t currentOffset = PR_Seek64(aFD, 0, PR_SEEK_CUR);
+ if (currentOffset == -1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int64_t cnt = PR_Seek64(aFD, aEOF, PR_SEEK_SET);
+ if (cnt == -1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!SetEndOfFile((HANDLE)PR_FileDesc2NativeHandle(aFD))) {
+ NS_ERROR("SetEndOfFile failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (PR_Seek64(aFD, currentOffset, PR_SEEK_SET) == -1) {
+ NS_ERROR("Restoring seek offset failed");
+ return NS_ERROR_FAILURE;
+ }
+
+#else
+ MOZ_ASSERT(false, "Not implemented!");
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+
+ return NS_OK;
+}
+
+} // namespace
+
+FileSystemWritableFileStream::FileSystemWritableFileStream(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ RefPtr<FileSystemWritableFileStreamChild> aActor,
+ const ::mozilla::ipc::FileDescriptor& aFileDescriptor,
+ const fs::FileSystemEntryMetadata& aMetadata)
+ : WritableStream(aGlobal, HoldDropJSObjectsCaller::Explicit),
+ mManager(aManager),
+ mActor(std::move(aActor)),
+ mFileDesc(nullptr),
+ mMetadata(aMetadata),
+ mClosed(false) {
+ auto rawFD = aFileDescriptor.ClonePlatformHandle();
+ mFileDesc = PR_ImportFile(PROsfd(rawFD.release()));
+
+ LOG(("Created WritableFileStream %p for fd %p", this, mFileDesc));
+
+ // 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(!mActor);
+ MOZ_ASSERT(mClosed);
+
+ mozilla::DropJSObjects(this);
+}
+
+// https://streams.spec.whatwg.org/#writablestream-set-up
+// * This is fallible because of OOM handling of JSAPI. See bug 1762233.
+// * Consider extracting this as UnderlyingSinkAlgorithmsWrapper if more classes
+// start subclassing WritableStream.
+// For now this skips step 2 - 4 as they are not required here.
+// XXX(krosylight): _BOUNDARY because SetUpWritableStreamDefaultController 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 already_AddRefed<FileSystemWritableFileStream>
+FileSystemWritableFileStream::Create(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ RefPtr<FileSystemWritableFileStreamChild> aActor,
+ const ::mozilla::ipc::FileDescriptor& aFileDescriptor,
+ const fs::FileSystemEntryMetadata& aMetadata) {
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(aGlobal)) {
+ return nullptr;
+ }
+ JSContext* cx = jsapi.cx();
+
+ // Step 5. Perform ! InitializeWritableStream(stream).
+ // (Done by the constructor)
+ RefPtr<FileSystemWritableFileStream> stream =
+ new FileSystemWritableFileStream(aGlobal, aManager, std::move(aActor),
+ aFileDescriptor, aMetadata);
+
+ // Step 1 - 3
+ auto algorithms =
+ MakeRefPtr<WritableFileStreamUnderlyingSinkAlgorithms>(*stream);
+
+ // Step 6. Let controller be a new WritableStreamDefaultController.
+ auto controller =
+ MakeRefPtr<WritableStreamDefaultController>(aGlobal, *stream);
+
+ // Step 7. Perform ! SetUpWritableStreamDefaultController(stream, controller,
+ // startAlgorithm, writeAlgorithm, closeAlgorithmWrapper,
+ // abortAlgorithmWrapper, highWaterMark, sizeAlgorithm).
+ IgnoredErrorResult rv;
+ SetUpWritableStreamDefaultController(
+ cx, stream, controller, algorithms,
+ // https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream
+ // Step 6. Let highWaterMark be 1.
+ 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 nullptr;
+ }
+ return stream.forget();
+}
+
+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!
+ tmp->Close();
+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() {
+ Close();
+
+ if (mActor) {
+ PFileSystemWritableFileStreamChild::Send__delete__(mActor);
+ MOZ_ASSERT(!mActor);
+ }
+}
+
+void FileSystemWritableFileStream::ClearActor() {
+ MOZ_ASSERT(mActor);
+
+ mActor = nullptr;
+}
+
+void FileSystemWritableFileStream::Close() {
+ // https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream
+ // Step 4. Let closeAlgorithm be these steps:
+
+ if (mClosed) {
+ return;
+ }
+
+ LOG(("%p: Closing", mFileDesc));
+
+ mClosed = true;
+
+ PR_Close(mFileDesc);
+ mFileDesc = nullptr;
+
+ if (mActor) {
+ mActor->SendClose();
+ }
+}
+
+already_AddRefed<Promise> FileSystemWritableFileStream::Write(
+ JSContext* aCx, JS::Handle<JS::Value> aChunk, ErrorResult& aError) {
+ MOZ_ASSERT(!mClosed);
+
+ // 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.
+
+ // https://fs.spec.whatwg.org/#write-a-chunk
+ // Step 1. Let input be the result of converting chunk to a
+ // FileSystemWriteChunkType.
+
+ aError.MightThrowJSException();
+
+ 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;
+ }
+
+ // Step 3.3. Let command be input.type if input is a WriteParams, ...
+ if (data.IsWriteParams()) {
+ const WriteParams& params = data.GetAsWriteParams();
+ switch (params.mType) {
+ // Step 3.4. If command is "write":
+ case WriteCommandType::Write: {
+ if (!params.mData.WasPassed()) {
+ promise->MaybeRejectWithSyntaxError("write() requires data");
+ return promise.forget();
+ }
+
+ // Step 3.4.2. If data is undefined, reject p with a TypeError and
+ // abort.
+ if (params.mData.Value().IsNull()) {
+ promise->MaybeRejectWithTypeError("write() of null data");
+ return promise.forget();
+ }
+
+ Maybe<uint64_t> position;
+
+ if (params.mPosition.WasPassed()) {
+ if (params.mPosition.Value().IsNull()) {
+ promise->MaybeRejectWithTypeError("write() with null position");
+ return promise.forget();
+ }
+
+ position = Some(params.mPosition.Value().Value());
+ }
+
+ Write(params.mData.Value().Value(), position, promise);
+ return promise.forget();
+ }
+
+ // Step 3.5. Otherwise, if command is "seek":
+ case WriteCommandType::Seek:
+ if (!params.mPosition.WasPassed()) {
+ promise->MaybeRejectWithSyntaxError("seek() requires a position");
+ return promise.forget();
+ }
+
+ // Step 3.5.1. If chunk.position is undefined, reject p with a TypeError
+ // and abort.
+ if (params.mPosition.Value().IsNull()) {
+ promise->MaybeRejectWithTypeError("seek() with null position");
+ return promise.forget();
+ }
+
+ Seek(params.mPosition.Value().Value(), promise);
+ return promise.forget();
+
+ // Step 3.6. Otherwise, if command is "truncate":
+ case WriteCommandType::Truncate:
+ if (!params.mSize.WasPassed()) {
+ promise->MaybeRejectWithSyntaxError("truncate() requires a size");
+ return promise.forget();
+ }
+
+ // Step 3.6.1. If chunk.size is undefined, reject p with a TypeError
+ // and abort.
+ if (params.mSize.Value().IsNull()) {
+ promise->MaybeRejectWithTypeError("truncate() with null size");
+ return promise.forget();
+ }
+
+ Truncate(params.mSize.Value().Value(), promise);
+ return promise.forget();
+
+ default:
+ MOZ_CRASH("Bad WriteParams value!");
+ }
+ }
+
+ // Step 3.3. ... and "write" otherwise.
+ // Step 3.4. If command is "write":
+ Write(data, Nothing(), promise);
+ return promise.forget();
+}
+
+// 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.
+ {
+ IgnoredErrorResult error;
+ writer->ReleaseLock(cx, error);
+ }
+
+ // 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.
+ {
+ IgnoredErrorResult error;
+ writer->ReleaseLock(cx, error);
+ }
+
+ // 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.
+ {
+ IgnoredErrorResult error;
+ writer->ReleaseLock(cx, error);
+ }
+
+ // Step 4. Return result.
+ return promise.forget();
+}
+
+template <typename T>
+void FileSystemWritableFileStream::Write(const T& aData,
+ const Maybe<uint64_t> aPosition,
+ RefPtr<Promise> aPromise) {
+ MOZ_ASSERT(!mClosed);
+
+ auto rejectAndReturn = [&aPromise](const nsresult rv) {
+ if (rv == NS_ERROR_FILE_NOT_FOUND) {
+ aPromise->MaybeRejectWithNotFoundError("File not found");
+ } else {
+ aPromise->MaybeReject(rv);
+ }
+ };
+
+ // https://fs.spec.whatwg.org/#write-a-chunk
+ // Step 3.4.6 If data is a BufferSource, let dataBytes be a copy of data.
+ if (aData.IsArrayBuffer() || aData.IsArrayBufferView()) {
+ const auto dataSpan = [&aData]() {
+ if (aData.IsArrayBuffer()) {
+ const ArrayBuffer& buffer = aData.GetAsArrayBuffer();
+ buffer.ComputeState();
+ return Span{buffer.Data(), buffer.Length()};
+ }
+ MOZ_ASSERT(aData.IsArrayBufferView());
+ const ArrayBufferView& buffer = aData.GetAsArrayBufferView();
+ buffer.ComputeState();
+ return Span{buffer.Data(), buffer.Length()};
+ }();
+
+ nsCString dataBuffer;
+ QM_TRY(MOZ_TO_RESULT(dataBuffer.Assign(
+ AsChars(dataSpan).data(), dataSpan.Length(), mozilla::fallible)),
+ rejectAndReturn);
+ QM_TRY_INSPECT(const auto& written, WriteBuffer(dataBuffer, aPosition),
+ rejectAndReturn);
+
+ LOG_VERBOSE(("WritableFileStream: Wrote %" PRId64, written));
+ aPromise->MaybeResolve(written);
+ return;
+ }
+
+ // Step 3.4.7 Otherwise, if data is a Blob ...
+ if (aData.IsBlob()) {
+ Blob& blob = aData.GetAsBlob();
+
+ nsCOMPtr<nsIInputStream> stream;
+ ErrorResult error;
+ blob.CreateInputStream(getter_AddRefs(stream), error);
+ QM_TRY((MOZ_TO_RESULT(!error.Failed()).mapErr([&error](const nsresult rv) {
+ return error.StealNSResult();
+ })),
+ rejectAndReturn);
+
+ QM_TRY_INSPECT(const auto& written,
+ WriteStream(std::move(stream), aPosition), rejectAndReturn);
+
+ LOG_VERBOSE(("WritableFileStream: Wrote %" PRId64, written));
+ aPromise->MaybeResolve(written);
+ return;
+ }
+
+ // Step 3.4.8 Otherwise ...
+ MOZ_ASSERT(aData.IsUTF8String());
+
+ QM_TRY_INSPECT(const auto& written,
+ WriteBuffer(aData.GetAsUTF8String(), aPosition),
+ rejectAndReturn);
+
+ LOG_VERBOSE(("WritableFileStream: Wrote %" PRId64, written));
+ aPromise->MaybeResolve(written);
+}
+
+void FileSystemWritableFileStream::Seek(uint64_t aPosition,
+ RefPtr<Promise> aPromise) {
+ MOZ_ASSERT(!mClosed);
+
+ LOG_VERBOSE(("%p: Seeking to %" PRIu64, mFileDesc, aPosition));
+
+ QM_TRY(SeekPosition(aPosition), [&aPromise](const nsresult rv) {
+ aPromise->MaybeReject(rv);
+ return;
+ });
+
+ aPromise->MaybeResolveWithUndefined();
+}
+
+void FileSystemWritableFileStream::Truncate(uint64_t aSize,
+ RefPtr<Promise> aPromise) {
+ MOZ_ASSERT(!mClosed);
+
+ // truncate filehandle (can extend with 0's)
+ LOG_VERBOSE(("%p: Truncate to %" PRIu64, mFileDesc, aSize));
+ if (NS_WARN_IF(NS_FAILED(TruncFile(mFileDesc, aSize)))) {
+ aPromise->MaybeReject(NS_ErrorAccordingToNSPR());
+ return;
+ }
+
+ // We truncated; per non-normative text in the spec (2.5.3) we should
+ // adjust the cursor position to be within the new file size
+ int64_t where = PR_Seek(mFileDesc, 0, PR_SEEK_CUR);
+ if (where == -1) {
+ aPromise->MaybeReject(NS_ErrorAccordingToNSPR());
+ return;
+ }
+
+ if (where > (int64_t)aSize) {
+ where = PR_Seek(mFileDesc, 0, PR_SEEK_END);
+ if (where == -1) {
+ aPromise->MaybeReject(NS_ErrorAccordingToNSPR());
+ return;
+ }
+ }
+
+ aPromise->MaybeResolveWithUndefined();
+}
+
+Result<uint64_t, nsresult> FileSystemWritableFileStream::WriteBuffer(
+ const nsACString& aBuffer, const Maybe<uint64_t> aPosition) {
+ MOZ_ASSERT(!mClosed);
+
+ const auto checkedLength = CheckedInt<PRInt32>(aBuffer.Length());
+ QM_TRY(MOZ_TO_RESULT(checkedLength.isValid()));
+
+ if (aPosition) {
+ QM_TRY(SeekPosition(*aPosition));
+ }
+
+ return PR_Write(mFileDesc, aBuffer.BeginReading(), checkedLength.value());
+}
+
+Result<uint64_t, nsresult> FileSystemWritableFileStream::WriteStream(
+ nsCOMPtr<nsIInputStream> aStream, const Maybe<uint64_t> aPosition) {
+ MOZ_ASSERT(aStream);
+ MOZ_ASSERT(!mClosed);
+
+ nsCString rawBuffer;
+ QM_TRY(MOZ_TO_RESULT(NS_ReadInputStreamToString(aStream, rawBuffer, -1)));
+ QM_TRY_RETURN(WriteBuffer(rawBuffer, aPosition));
+}
+
+Result<Ok, nsresult> FileSystemWritableFileStream::SeekPosition(
+ uint64_t aPosition) {
+ MOZ_ASSERT(!mClosed);
+
+ const auto checkedPosition = CheckedInt<int64_t>(aPosition);
+ QM_TRY(MOZ_TO_RESULT(checkedPosition.isValid()));
+
+ int64_t cnt = PR_Seek64(mFileDesc, checkedPosition.value(), PR_SEEK_SET);
+ if (cnt == int64_t(-1)) {
+ LOG(("Failed to seek to %" PRIu64 " (errno %d)", aPosition, errno));
+ return Err(NS_ErrorAccordingToNSPR());
+ }
+
+ if (cnt != checkedPosition.value()) {
+ LOG(("Failed to seek to %" PRIu64 " (errno %d), ended up at %" PRId64,
+ aPosition, errno, cnt));
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ return Ok{};
+}
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(
+ WritableFileStreamUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase)
+NS_IMPL_CYCLE_COLLECTION_INHERITED(WritableFileStreamUnderlyingSinkAlgorithms,
+ UnderlyingSinkAlgorithmsBase, mStream)
+
+void WritableFileStreamUnderlyingSinkAlgorithms::StartCallback(
+ JSContext* aCx, WritableStreamDefaultController& aController,
+ JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv) {
+ // https://streams.spec.whatwg.org/#writablestream-set-up
+ // Step 1. Let startAlgorithm be an algorithm that returns undefined.
+ aRetVal.setUndefined();
+}
+
+already_AddRefed<Promise>
+WritableFileStreamUnderlyingSinkAlgorithms::WriteCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aChunk,
+ WritableStreamDefaultController& aController, ErrorResult& aRv) {
+ return mStream->Write(aCx, aChunk, aRv);
+}
+
+already_AddRefed<Promise>
+WritableFileStreamUnderlyingSinkAlgorithms::CloseCallback(JSContext* aCx,
+ ErrorResult& aRv) {
+ // https://streams.spec.whatwg.org/#writablestream-set-up
+ // Step 2. Let closeAlgorithmWrapper be an algorithm that runs these steps:
+
+ RefPtr<Promise> promise = Promise::Create(mStream->GetParentObject(), aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (mStream->IsClosed()) {
+ promise->MaybeRejectWithTypeError("WritableFileStream closed");
+ return promise.forget();
+ }
+
+ // XXX The close should be async. For now we have to always fallback to the
+ // Step 2.3 below.
+ mStream->Close();
+
+ // Step 2.3. Return a promise resolved with undefined.
+ promise->MaybeResolveWithUndefined();
+ return promise.forget();
+}
+
+already_AddRefed<Promise>
+WritableFileStreamUnderlyingSinkAlgorithms::AbortCallback(
+ 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:
+
+ // XXX The close or rather a dedicated abort should be async. For now we have
+ // to always fall back to the Step 3.3 below.
+ mStream->Close();
+
+ // Step 3.3. Return a promise resolved with undefined.
+ return Promise::CreateResolvedWithUndefined(mStream->GetParentObject(), aRv);
+}
+
+void WritableFileStreamUnderlyingSinkAlgorithms::ReleaseObjects() {
+ // XXX We shouldn't be calling close here. We should just release the lock.
+ // However, calling close here is not a big issue for now because we don't
+ // write to a temporary file which would atomically replace the real file
+ // during close.
+ mStream->Close();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/api/FileSystemWritableFileStream.h b/dom/fs/api/FileSystemWritableFileStream.h
new file mode 100644
index 0000000000..630dd9184f
--- /dev/null
+++ b/dom/fs/api/FileSystemWritableFileStream.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_FILESYSTEMWRITABLEFILESTREAM_H_
+#define DOM_FS_FILESYSTEMWRITABLEFILESTREAM_H_
+
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/WritableStream.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+template <typename T>
+class Buffer;
+class ErrorResult;
+
+namespace dom {
+
+class ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams;
+class Blob;
+class FileSystemManager;
+class FileSystemWritableFileStreamChild;
+class OwningArrayBufferViewOrArrayBufferOrBlobOrUSVString;
+class Promise;
+
+class FileSystemWritableFileStream final : public WritableStream {
+ public:
+ static already_AddRefed<FileSystemWritableFileStream> Create(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ RefPtr<FileSystemWritableFileStreamChild> aActor,
+ const ::mozilla::ipc::FileDescriptor& aFileDescriptor,
+ const fs::FileSystemEntryMetadata& aMetadata);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FileSystemWritableFileStream,
+ WritableStream)
+
+ void LastRelease() override;
+
+ void ClearActor();
+
+ bool IsClosed() const { return mClosed; }
+
+ void Close();
+
+ 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:
+ FileSystemWritableFileStream(
+ nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
+ RefPtr<FileSystemWritableFileStreamChild> aActor,
+ const ::mozilla::ipc::FileDescriptor& aFileDescriptor,
+ const fs::FileSystemEntryMetadata& aMetadata);
+
+ virtual ~FileSystemWritableFileStream();
+
+ template <typename T>
+ void Write(const T& aData, const Maybe<uint64_t> aPosition,
+ RefPtr<Promise> aPromise);
+
+ void Seek(uint64_t aPosition, RefPtr<Promise> aPromise);
+
+ void Truncate(uint64_t aSize, RefPtr<Promise> aPromise);
+
+ Result<uint64_t, nsresult> WriteBuffer(const nsACString& aBuffer,
+ const Maybe<uint64_t> aPosition);
+
+ Result<uint64_t, nsresult> WriteStream(nsCOMPtr<nsIInputStream> aStream,
+ const Maybe<uint64_t> aPosition);
+
+ Result<Ok, nsresult> SeekPosition(uint64_t aPosition);
+
+ RefPtr<FileSystemManager> mManager;
+
+ RefPtr<FileSystemWritableFileStreamChild> mActor;
+
+ PRFileDesc* mFileDesc;
+
+ fs::FileSystemEntryMetadata mMetadata;
+
+ bool mClosed;
+};
+
+} // 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..09c0bc450a
--- /dev/null
+++ b/dom/fs/api/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 += [
+ "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",
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-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/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..5bb3e13255
--- /dev/null
+++ b/dom/fs/child/FileSystemDirectoryIteratorFactory.cpp
@@ -0,0 +1,238 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#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.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()),
+ mData(),
+ 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();
+ }
+
+ ~DoubleBufferQueueImpl() = default;
+
+ protected:
+ 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>();
+
+ RefPtr<DomPromiseListener> listener = new DomPromiseListener(
+ [global = nsCOMPtr<nsIGlobalObject>(aGlobal),
+ manager = RefPtr<FileSystemManager>(aManager), newPage, aResult,
+ this](JSContext* aCx, JS::Handle<JS::Value> aValue) mutable {
+ MOZ_ASSERT(0u == mWithinPageIndex);
+
+ // XXX Do we need this extra copy ?
+ nsTArray<DataType> batch;
+ for (const auto& it : *newPage) {
+ batch.AppendElement(it);
+ }
+
+ const size_t batchSize = std::min(PageSize, newPage->Length());
+ mData.InsertElementsAt(
+ PageSize * static_cast<size_t>(!mCurrentPageIsLastPage),
+ batch.Elements(), batchSize);
+ mWithinPageEnd += batchSize;
+
+ Maybe<DataType> value;
+ if (0 != newPage->Length()) {
+ nextInternal(value);
+ }
+
+ ResolveValue(global, manager, value, aResult);
+ },
+ [aResult](nsresult aRv) { aResult->MaybeReject(aRv); });
+ promise->AppendNativeHandler(listener);
+
+ 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 auto previous =
+ static_cast<size_t>(!mCurrentPageIsLastPage) * PageSize +
+ mWithinPageIndex;
+ 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
+
+UniquePtr<mozilla::dom::FileSystemDirectoryIterator::Impl>
+FileSystemDirectoryIteratorFactory::Create(
+ const FileSystemEntryMetadata& aMetadata,
+ IterableIteratorBase::IteratorType aType) {
+ if (IterableIteratorBase::Entries == aType) {
+ return MakeUnique<UnderlyingQueue<IterableIteratorBase::Entries>>(
+ aMetadata);
+ }
+
+ if (IterableIteratorBase::Values == aType) {
+ return MakeUnique<UnderlyingQueue<IterableIteratorBase::Values>>(aMetadata);
+ }
+
+ return MakeUnique<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..278f0a9ef0
--- /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 UniquePtr<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..045f50cf7f
--- /dev/null
+++ b/dom/fs/child/FileSystemManagerChild.cpp
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#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;
+}
+
+#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;
+}
+#endif
+
+void FileSystemManagerChild::CloseAllWritableFileStreams() {
+ for (const auto& item : ManagedPFileSystemWritableFileStreamChild()) {
+ auto* child = static_cast<FileSystemWritableFileStreamChild*>(item);
+
+ child->MutableWritableFileStreamPtr()->Close();
+ }
+}
+
+void FileSystemManagerChild::Shutdown() {
+ if (!CanSend()) {
+ return;
+ }
+
+ Close();
+}
+
+already_AddRefed<PFileSystemAccessHandleChild>
+FileSystemManagerChild::AllocPFileSystemAccessHandleChild() {
+ return MakeAndAddRef<FileSystemAccessHandleChild>();
+}
+
+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());
+ }
+ }
+
+ CloseAllWritableFileStreams();
+
+ 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;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/child/FileSystemManagerChild.h b/dom/fs/child/FileSystemManagerChild.h
new file mode 100644
index 0000000000..7000dfab67
--- /dev/null
+++ b/dom/fs/child/FileSystemManagerChild.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_CHILD_FILESYSTEMMANAGERCHILD_H_
+#define DOM_FS_CHILD_FILESYSTEMMANAGERCHILD_H_
+
+#include "mozilla/dom/PFileSystemManagerChild.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla::dom {
+
+class FileSystemBackgroundRequestHandler;
+
+class FileSystemManagerChild : public PFileSystemManagerChild {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_WITH_DESTROY(FileSystemManagerChild, Destroy(),
+ override)
+
+ void SetBackgroundRequestHandler(
+ FileSystemBackgroundRequestHandler* aBackgroundRequestHandler);
+
+#ifdef DEBUG
+ virtual bool AllSyncAccessHandlesClosed() const;
+#endif
+
+ virtual void CloseAllWritableFileStreams();
+
+ virtual void Shutdown();
+
+ already_AddRefed<PFileSystemAccessHandleChild>
+ AllocPFileSystemAccessHandleChild();
+
+ 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;
+};
+
+} // 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..87c06e409a
--- /dev/null
+++ b/dom/fs/child/FileSystemRequestHandler.cpp
@@ -0,0 +1,648 @@
+/* -*- 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/quota/QuotaCommon.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_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();
+
+ auto* const actor =
+ static_cast<FileSystemAccessHandleChild*>(properties.accessHandleChild());
+
+ QM_TRY_UNWRAP(RefPtr<FileSystemSyncAccessHandle> result,
+ FileSystemSyncAccessHandle::Create(
+ aGlobal, aManager, actor,
+ std::move(properties.streamParams()), aMetadata),
+ nullptr);
+
+ return result;
+}
+
+RefPtr<FileSystemWritableFileStream> MakeResolution(
+ nsIGlobalObject* aGlobal,
+ FileSystemGetWritableFileStreamResponse&& aResponse,
+ const RefPtr<FileSystemWritableFileStream>& /* aReturns */,
+ const FileSystemEntryMetadata& aMetadata,
+ RefPtr<FileSystemManager>& aManager) {
+ const auto& properties =
+ aResponse.get_FileSystemWritableFileStreamProperties();
+
+ auto* const actor = static_cast<FileSystemWritableFileStreamChild*>(
+ properties.writableFileStreamChild());
+
+ RefPtr<FileSystemWritableFileStream> result =
+ FileSystemWritableFileStream::Create(
+ aGlobal, aManager, actor, properties.fileDescriptor(), aMetadata);
+
+ 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)
+ MOZ_ASSERT(aPromise);
+ QM_TRY(OkIf(Promise::PromiseState::Pending == aPromise->State()), QM_VOID);
+
+ MOZ_ASSERT(FileSystemMoveEntryResponse::Tnsresult == aResponse.type());
+ const auto& status = aResponse.get_nsresult();
+ if (NS_OK == status) {
+ aPromise->MaybeResolveWithUndefined();
+ return;
+ }
+ 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)...));
+}
+
+// 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;
+ }
+}
+
+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,
+ const FileSystemEntryMetadata& aEntry,
+ const FileSystemChildMetadata& aNewEntry,
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ ErrorResult& aError) {
+ 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),
+ 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,
+ const FileSystemEntryMetadata& aEntry, const Name& aName,
+ RefPtr<Promise> aPromise, // NOLINT(performance-unnecessary-value-param)
+ ErrorResult& aError) {
+ 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),
+ 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/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..7cff598101
--- /dev/null
+++ b/dom/fs/child/moz.build
@@ -0,0 +1,29 @@
+# -*- 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",
+ "FileSystemManagerChild.h",
+ "FileSystemWritableFileStreamChild.h",
+]
+
+UNIFIED_SOURCES += [
+ "FileSystemAccessHandleChild.cpp",
+ "FileSystemBackgroundRequestHandler.cpp",
+ "FileSystemChildFactory.cpp",
+ "FileSystemDirectoryIteratorFactory.cpp",
+ "FileSystemManagerChild.cpp",
+ "FileSystemRequestHandler.cpp",
+ "FileSystemWritableFileStreamChild.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/fs/include",
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
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..fc9e1840c4
--- /dev/null
+++ b/dom/fs/include/fs/FileSystemConstants.h
@@ -0,0 +1,18 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_FILESYSTEMCONSTANTS_H_
+#define DOM_FS_FILESYSTEMCONSTANTS_H_
+
+#include "nsLiteralString.h"
+
+namespace mozilla::dom::fs {
+
+constexpr nsLiteralString kRootName = u"root"_ns;
+
+} // 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..d535d75164
--- /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,
+ const FileSystemEntryMetadata& aEntry,
+ const FileSystemChildMetadata& aNewEntry,
+ RefPtr<Promise> aPromise, ErrorResult& aError);
+
+ virtual void RenameEntry(RefPtr<FileSystemManager>& aManager,
+ FileSystemHandle* aHandle,
+ const FileSystemEntryMetadata& 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/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/FileSystemAccessHandleParent.cpp b/dom/fs/parent/FileSystemAccessHandleParent.cpp
new file mode 100644
index 0000000000..1dd6dccb9f
--- /dev/null
+++ b/dom/fs/parent/FileSystemAccessHandleParent.cpp
@@ -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/. */
+
+#include "FileSystemAccessHandleParent.h"
+
+#include "FileSystemDataManager.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemManagerParent.h"
+
+namespace mozilla::dom {
+
+FileSystemAccessHandleParent::FileSystemAccessHandleParent(
+ RefPtr<FileSystemManagerParent> aManager, const fs::EntryId& aEntryId)
+ : mManager(std::move(aManager)), mEntryId(aEntryId) {}
+
+FileSystemAccessHandleParent::~FileSystemAccessHandleParent() {
+ MOZ_ASSERT(mClosed);
+}
+
+mozilla::ipc::IPCResult FileSystemAccessHandleParent::RecvClose() {
+ Close();
+
+ return IPC_OK();
+}
+
+void FileSystemAccessHandleParent::ActorDestroy(ActorDestroyReason aWhy) {
+ if (!IsClosed()) {
+ Close();
+ }
+}
+
+void FileSystemAccessHandleParent::Close() {
+ LOG(("Closing SyncAccessHandle"));
+
+ mClosed.Flip();
+
+ mManager->DataManagerStrongRef()->UnlockExclusive(mEntryId);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/parent/FileSystemAccessHandleParent.h b/dom/fs/parent/FileSystemAccessHandleParent.h
new file mode 100644
index 0000000000..f1d0917a3a
--- /dev/null
+++ b/dom/fs/parent/FileSystemAccessHandleParent.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_PARENT_FILESYSTEMACCESSHANDLEPARENT_H_
+#define DOM_FS_PARENT_FILESYSTEMACCESSHANDLEPARENT_H_
+
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "mozilla/dom/PFileSystemAccessHandleParent.h"
+
+namespace mozilla::dom {
+
+class FileSystemManagerParent;
+
+class FileSystemAccessHandleParent : public PFileSystemAccessHandleParent {
+ public:
+ FileSystemAccessHandleParent(RefPtr<FileSystemManagerParent> aManager,
+ const fs::EntryId& aEntryId);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemAccessHandleParent, override)
+
+ mozilla::ipc::IPCResult RecvClose();
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ virtual ~FileSystemAccessHandleParent();
+
+ bool IsClosed() const { return mClosed; }
+
+ void Close();
+
+ const RefPtr<FileSystemManagerParent> mManager;
+
+ const fs::EntryId mEntryId;
+
+ FlippedOnce<false> mClosed;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_FS_PARENT_FILESYSTEMACCESSHANDLEPARENT_H_
diff --git a/dom/fs/parent/FileSystemHashSource.cpp b/dom/fs/parent/FileSystemHashSource.cpp
new file mode 100644
index 0000000000..eb43575371
--- /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 "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 EntryId& aEntryId) {
+ MOZ_ASSERT(32u == aEntryId.Length());
+ nsCString encoded;
+ base32encode(&aEntryId, &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..2b7ca0136f
--- /dev/null
+++ b/dom/fs/parent/FileSystemHashSource.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_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::data {
+
+struct FileSystemHashSource {
+ static Result<EntryId, QMResult> GenerateHash(const EntryId& aParent,
+ const Name& aName);
+
+ static Result<Name, QMResult> EncodeHash(const EntryId& aEntryId);
+};
+
+} // namespace mozilla::dom::fs::data
+
+#endif // DOM_FS_PARENT_FILESYSTEMHASHSOURCE_H_
diff --git a/dom/fs/parent/FileSystemManagerParent.cpp b/dom/fs/parent/FileSystemManagerParent.cpp
new file mode 100644
index 0000000000..7f53346893
--- /dev/null
+++ b/dom/fs/parent/FileSystemManagerParent.cpp
@@ -0,0 +1,521 @@
+/* -*- 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 "FileSystemStreamCallbacks.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/dom/FileBlobImpl.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));
+}
+
+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);
+
+ if (!mDataManager->LockExclusive(aRequest.entryId())) {
+ aResolver(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR);
+ return IPC_OK();
+ }
+
+ auto autoUnlock =
+ MakeScopeExit([self = RefPtr<FileSystemManagerParent>(this), aRequest] {
+ self->mDataManager->UnlockExclusive(aRequest.entryId());
+ });
+
+ auto reportError = [aResolver](nsresult rv) { aResolver(rv); };
+
+ nsString type;
+ fs::TimeStamp lastModifiedMilliSeconds;
+ fs::Path path;
+ nsCOMPtr<nsIFile> file;
+ QM_TRY(MOZ_TO_RESULT(mDataManager->MutableDatabaseManagerPtr()->GetFile(
+ aRequest.entryId(), type, lastModifiedMilliSeconds, path, file)),
+ IPC_OK(), reportError);
+
+ if (LOG_ENABLED()) {
+ nsAutoString path;
+ if (NS_SUCCEEDED(file->GetPath(path))) {
+ LOG(("Opening SyncAccessHandle %s", NS_ConvertUTF16toUTF8(path).get()));
+ }
+ }
+
+ 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);
+
+ EnsureStreamCallbacks();
+
+ RandomAccessStreamParams streamParams =
+ mozilla::ipc::SerializeRandomAccessStream(
+ WrapMovingNotNullUnchecked(std::move(stream)), mStreamCallbacks);
+
+ auto accessHandleParent =
+ MakeRefPtr<FileSystemAccessHandleParent>(this, aRequest.entryId());
+
+ // Release the auto unlock helper just before calling
+ // SendPFileSystemAccessHandleConstructor which is responsible for destroying
+ // the actor if the sending fails (we call `UnlockExclusive` when the actor is
+ // destroyed).
+ autoUnlock.release();
+
+ if (!SendPFileSystemAccessHandleConstructor(accessHandleParent)) {
+ aResolver(NS_ERROR_FAILURE);
+ return IPC_OK();
+ }
+
+ aResolver(FileSystemAccessHandleProperties(std::move(streamParams),
+ accessHandleParent, nullptr));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult FileSystemManagerParent::RecvGetWritable(
+ FileSystemGetWritableRequest&& aRequest, GetWritableResolver&& aResolver) {
+ AssertIsOnIOTarget();
+ MOZ_ASSERT(mDataManager);
+
+ if (!mDataManager->LockShared(aRequest.entryId())) {
+ aResolver(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR);
+ return IPC_OK();
+ }
+
+ auto autoUnlock =
+ MakeScopeExit([self = RefPtr<FileSystemManagerParent>(this), aRequest] {
+ self->mDataManager->UnlockShared(aRequest.entryId());
+ });
+
+ auto reportError = [aResolver](nsresult rv) { aResolver(rv); };
+
+ nsString type;
+ fs::TimeStamp lastModifiedMilliSeconds;
+ fs::Path path;
+ nsCOMPtr<nsIFile> file;
+ QM_TRY(MOZ_TO_RESULT(mDataManager->MutableDatabaseManagerPtr()->GetFile(
+ aRequest.entryId(), 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()));
+ }
+ }
+
+ FILE* fileHandle;
+ QM_TRY(MOZ_TO_RESULT(file->OpenANSIFileDesc(aRequest.keepData() ? "r+" : "w",
+ &fileHandle)),
+ IPC_OK(), reportError);
+
+ auto autoClose = MakeScopeExit([fileHandle]() {
+ QM_WARNONLY_TRY(MOZ_TO_RESULT(0 == fclose(fileHandle)));
+ });
+
+ FileDescriptor fileDescriptor =
+ mozilla::ipc::FILEToFileDescriptor(fileHandle);
+
+ LOG(("Opened"));
+
+ auto writableFileStreamParent =
+ MakeRefPtr<FileSystemWritableFileStreamParent>(this, aRequest.entryId());
+
+ // 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(
+ fileDescriptor, writableFileStreamParent, nullptr));
+ 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](nsresult rv) {
+ LOG(("getFile() Failed!"));
+ aResolver(rv);
+ };
+
+ nsString type;
+ fs::TimeStamp lastModifiedMilliSeconds;
+ fs::Path path;
+ nsCOMPtr<nsIFile> fileObject;
+ QM_TRY(MOZ_TO_RESULT(mDataManager->MutableDatabaseManagerPtr()->GetFile(
+ aRequest.entryId(), 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()));
+ }
+ }
+
+ RefPtr<BlobImpl> blob = MakeRefPtr<FileBlobImpl>(fileObject);
+
+ 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(NS_OK);
+ 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(NS_OK);
+ 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_UNWRAP(bool moved,
+ mDataManager->MutableDatabaseManagerPtr()->MoveEntry(
+ aRequest.handle(), aRequest.destHandle()),
+ IPC_OK(), reportError);
+
+ fs::FileSystemMoveEntryResponse response(moved ? NS_OK : NS_ERROR_FAILURE);
+ 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_UNWRAP(bool moved,
+ mDataManager->MutableDatabaseManagerPtr()->RenameEntry(
+ aRequest.handle(), aRequest.name()),
+ IPC_OK(), reportError);
+
+ fs::FileSystemMoveEntryResponse response(moved ? NS_OK : NS_ERROR_FAILURE);
+ aResolver(response);
+ return IPC_OK();
+}
+
+IPCResult FileSystemManagerParent::RecvNeedQuota(
+ FileSystemQuotaRequest&& aRequest, NeedQuotaResolver&& aResolver) {
+ AssertIsOnIOTarget();
+
+ aResolver(0u);
+
+ return IPC_OK();
+}
+
+void FileSystemManagerParent::RequestAllowToClose() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ if (mRequestedAllowToClose) {
+ return;
+ }
+
+ mRequestedAllowToClose.Flip();
+
+ InvokeAsync(mDataManager->MutableIOTargetPtr(), __func__,
+ [self = RefPtr<FileSystemManagerParent>(this)]() {
+ return self->SendCloseAll();
+ })
+ ->Then(mDataManager->MutableIOTargetPtr(), __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);
+
+ mActorDestroyed = true;
+
+ if (!mStreamCallbacks || mStreamCallbacks->HasNoRemoteQuotaObjectParents()) {
+ CleanupAfterClose();
+ }
+}
+
+void FileSystemManagerParent::EnsureStreamCallbacks() {
+ if (mStreamCallbacks) {
+ return;
+ }
+
+ mStreamCallbacks = MakeRefPtr<FileSystemStreamCallbacks>();
+
+ mStreamCallbacks->SetRemoteQuotaObjectParentCallback([self = RefPtr(this)]() {
+ if (self->mActorDestroyed) {
+ self->CleanupAfterClose();
+ }
+ });
+}
+
+void FileSystemManagerParent::CleanupAfterClose() {
+ MOZ_ASSERT(mActorDestroyed);
+ MOZ_ASSERT_IF(mStreamCallbacks,
+ mStreamCallbacks->HasNoRemoteQuotaObjectParents());
+
+ mStreamCallbacks = nullptr;
+
+ 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..98931e79af
--- /dev/null
+++ b/dom/fs/parent/FileSystemManagerParent.h
@@ -0,0 +1,97 @@
+/* -*- 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 "nsISupports.h"
+
+namespace mozilla::dom {
+
+class FileSystemStreamCallbacks;
+
+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;
+
+ // 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);
+
+ mozilla::ipc::IPCResult RecvNeedQuota(FileSystemQuotaRequest&& aRequest,
+ NeedQuotaResolver&& aResolver);
+
+ void RequestAllowToClose();
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ protected:
+ virtual ~FileSystemManagerParent();
+
+ private:
+ void EnsureStreamCallbacks();
+
+ void CleanupAfterClose();
+
+ RefPtr<fs::data::FileSystemDataManager> mDataManager;
+
+ RefPtr<FileSystemStreamCallbacks> mStreamCallbacks;
+
+ FileSystemGetHandleResponse mRootResponse;
+
+ FlippedOnce<false> mRequestedAllowToClose;
+
+ 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..1da98a7bc5
--- /dev/null
+++ b/dom/fs/parent/FileSystemManagerParentFactory.cpp
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#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); });
+
+ quota::OriginMetadata originMetadata(
+ quota::QuotaManager::GetInfoFromValidatedPrincipalInfo(aPrincipalInfo),
+ 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->MutableIOTargetPtr(), __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/FileSystemQuotaClient.cpp b/dom/fs/parent/FileSystemQuotaClient.cpp
new file mode 100644
index 0000000000..6c0271060f
--- /dev/null
+++ b/dom/fs/parent/FileSystemQuotaClient.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 "FileSystemQuotaClient.h"
+
+#include "ResultStatement.h"
+#include "datamodel/FileSystemDatabaseManager.h"
+#include "datamodel/FileSystemFileManager.h"
+#include "mozIStorageService.h"
+#include "mozStorageCID.h"
+#include "mozilla/dom/FileSystemDataManager.h"
+#include "mozilla/dom/quota/Assertions.h"
+#include "mozilla/dom/quota/Client.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); };
+
+class QuotaClient final : public mozilla::dom::quota::Client {
+ public:
+ QuotaClient();
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::fs::QuotaClient, 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 ReleaseIOThreadObjects() override;
+
+ void AbortOperationsForLocks(
+ const DirectoryLockIdTable& aDirectoryLockIds) override;
+
+ void AbortOperationsForProcess(ContentParentId aContentParentId) override;
+
+ void AbortAllOperations() override;
+
+ void StartIdleMaintenance() override;
+
+ void StopIdleMaintenance() override;
+
+ private:
+ ~QuotaClient() = default;
+
+ void InitiateShutdown() override;
+ bool IsShutdownCompleted() const override;
+ nsCString GetShutdownStatus() const override;
+ void ForceKillActors() override;
+ void FinalizeShutdown() override;
+};
+
+Result<ResultConnection, QMResult> GetStorageConnection(const Origin& aOrigin) {
+ QM_TRY_INSPECT(const nsCOMPtr<nsIFile>& databaseFile,
+ data::GetDatabaseFile(aOrigin));
+
+ 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, OpenDatabase,
+ databaseFile, mozIStorageService::CONNECTION_DEFAULT)));
+
+ ResultConnection result(connection);
+
+ return result;
+}
+
+} // namespace
+
+QuotaClient::QuotaClient() { ::mozilla::ipc::AssertIsOnBackgroundThread(); }
+
+mozilla::dom::quota::Client::Type QuotaClient::GetType() {
+ return quota::Client::Type::FILESYSTEM;
+}
+
+Result<quota::UsageInfo, nsresult> QuotaClient::InitOrigin(
+ quota::PersistenceType aPersistenceType,
+ const quota::OriginMetadata& aOriginMetadata, const AtomicBool& aCanceled) {
+ quota::AssertIsOnIOThread();
+
+ const Origin& origin = aOriginMetadata.mOrigin;
+
+ {
+ QM_TRY_INSPECT(const nsCOMPtr<nsIFile>& databaseFile,
+ data::GetDatabaseFile(origin).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,
+ GetStorageConnection(origin).mapErr(toNSResult));
+
+ return data::FileSystemDatabaseManager::GetUsage(conn, origin)
+ .mapErr(toNSResult);
+}
+
+nsresult QuotaClient::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> QuotaClient::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 QuotaClient::OnOriginClearCompleted(
+ quota::PersistenceType aPersistenceType, const nsACString& aOrigin) {
+ quota::AssertIsOnIOThread();
+}
+
+void QuotaClient::ReleaseIOThreadObjects() { quota::AssertIsOnIOThread(); }
+
+void QuotaClient::AbortOperationsForLocks(
+ const DirectoryLockIdTable& aDirectoryLockIds) {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ data::FileSystemDataManager::AbortOperationsForLocks(aDirectoryLockIds);
+}
+
+void QuotaClient::AbortOperationsForProcess(ContentParentId aContentParentId) {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+}
+
+void QuotaClient::AbortAllOperations() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+}
+
+void QuotaClient::StartIdleMaintenance() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+}
+
+void QuotaClient::StopIdleMaintenance() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+}
+
+void QuotaClient::InitiateShutdown() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ data::FileSystemDataManager::InitiateShutdown();
+}
+
+bool QuotaClient::IsShutdownCompleted() const {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ return data::FileSystemDataManager::IsShutdownCompleted();
+}
+
+nsCString QuotaClient::GetShutdownStatus() const {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ return "Not implemented"_ns;
+}
+
+void QuotaClient::ForceKillActors() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ // Hopefully not needed.
+}
+
+void QuotaClient::FinalizeShutdown() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ // Empty for now.
+}
+
+already_AddRefed<mozilla::dom::quota::Client> CreateQuotaClient() {
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ RefPtr<QuotaClient> client = new fs::QuotaClient();
+ return client.forget();
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/parent/FileSystemQuotaClient.h b/dom/fs/parent/FileSystemQuotaClient.h
new file mode 100644
index 0000000000..9afc04ee7b
--- /dev/null
+++ b/dom/fs/parent/FileSystemQuotaClient.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_FILESYSTEMQUOTACLIENT_H_
+#define DOM_FS_PARENT_FILESYSTEMQUOTACLIENT_H_
+
+#include "mozilla/AlreadyAddRefed.h"
+
+namespace mozilla::dom {
+
+namespace quota {
+class Client;
+} // namespace quota
+
+namespace fs {
+
+already_AddRefed<mozilla::dom::quota::Client> CreateQuotaClient();
+
+} // namespace fs
+} // namespace mozilla::dom
+
+#endif // DOM_FS_PARENT_FILESYSTEMQUOTACLIENT_H_
diff --git a/dom/fs/parent/FileSystemStreamCallbacks.cpp b/dom/fs/parent/FileSystemStreamCallbacks.cpp
new file mode 100644
index 0000000000..8986b49358
--- /dev/null
+++ b/dom/fs/parent/FileSystemStreamCallbacks.cpp
@@ -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/. */
+
+#include "FileSystemStreamCallbacks.h"
+
+namespace mozilla::dom {
+
+bool FileSystemStreamCallbacks::HasNoRemoteQuotaObjectParents() const {
+ return !mRemoteQuotaObjectParents.Count();
+}
+
+void FileSystemStreamCallbacks::SetRemoteQuotaObjectParentCallback(
+ std::function<void()>&& aCallback) {
+ mRemoteQuotaObjectParentCallback = std::move(aCallback);
+}
+
+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(!mRemoteQuotaObjectParents.Contains(aActor));
+
+ mRemoteQuotaObjectParents.Insert(aActor);
+}
+
+void FileSystemStreamCallbacks::UnregisterRemoteQuotaObjectParent(
+ NotNull<quota::RemoteQuotaObjectParent*> aActor) {
+ MOZ_ASSERT(mRemoteQuotaObjectParents.Contains(aActor));
+
+ mRemoteQuotaObjectParents.Remove(aActor);
+
+ if (!mRemoteQuotaObjectParents.Count() && mRemoteQuotaObjectParentCallback) {
+ mRemoteQuotaObjectParentCallback();
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/parent/FileSystemStreamCallbacks.h b/dom/fs/parent/FileSystemStreamCallbacks.h
new file mode 100644
index 0000000000..37028b9f9c
--- /dev/null
+++ b/dom/fs/parent/FileSystemStreamCallbacks.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_PARENT_FILESYSTEMSTREAMCALLBACKS_H_
+#define DOM_FS_PARENT_FILESYSTEMSTREAMCALLBACKS_H_
+
+#include "mozilla/dom/quota/RemoteQuotaObjectParentTracker.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsTHashSet.h"
+
+namespace mozilla::dom {
+
+class FileSystemStreamCallbacks : public nsIInterfaceRequestor,
+ public quota::RemoteQuotaObjectParentTracker {
+ public:
+ FileSystemStreamCallbacks() = default;
+
+ bool HasNoRemoteQuotaObjectParents() const;
+
+ void SetRemoteQuotaObjectParentCallback(std::function<void()>&& aCallback);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_DECL_NSIINTERFACEREQUESTOR
+
+ void RegisterRemoteQuotaObjectParent(
+ NotNull<quota::RemoteQuotaObjectParent*> aActor) override;
+
+ void UnregisterRemoteQuotaObjectParent(
+ NotNull<quota::RemoteQuotaObjectParent*> aActor) override;
+
+ private:
+ virtual ~FileSystemStreamCallbacks() = default;
+
+ nsTHashSet<quota::RemoteQuotaObjectParent*> mRemoteQuotaObjectParents;
+ std::function<void()> mRemoteQuotaObjectParentCallback;
+};
+
+} // 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..d490ef4994
--- /dev/null
+++ b/dom/fs/parent/FileSystemWritableFileStreamParent.cpp
@@ -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/. */
+
+#include "FileSystemWritableFileStreamParent.h"
+
+#include "FileSystemDataManager.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemManagerParent.h"
+
+namespace mozilla::dom {
+
+FileSystemWritableFileStreamParent::FileSystemWritableFileStreamParent(
+ RefPtr<FileSystemManagerParent> aManager, const fs::EntryId& aEntryId)
+ : mManager(std::move(aManager)), mEntryId(aEntryId) {}
+
+FileSystemWritableFileStreamParent::~FileSystemWritableFileStreamParent() {
+ MOZ_ASSERT(mClosed);
+}
+
+mozilla::ipc::IPCResult FileSystemWritableFileStreamParent::RecvClose() {
+ Close();
+
+ return IPC_OK();
+}
+
+void FileSystemWritableFileStreamParent::ActorDestroy(ActorDestroyReason aWhy) {
+ if (!IsClosed()) {
+ Close();
+ }
+}
+
+void FileSystemWritableFileStreamParent::Close() {
+ LOG(("Closing WritableFileStream"));
+
+ mClosed.Flip();
+
+ mManager->DataManagerStrongRef()->UnlockShared(mEntryId);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/fs/parent/FileSystemWritableFileStreamParent.h b/dom/fs/parent/FileSystemWritableFileStreamParent.h
new file mode 100644
index 0000000000..bd7db23902
--- /dev/null
+++ b/dom/fs/parent/FileSystemWritableFileStreamParent.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_PARENT_FILESYSTEMWRITABLEFILESTREAM_H_
+#define DOM_FS_PARENT_FILESYSTEMWRITABLEFILESTREAM_H_
+
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "mozilla/dom/PFileSystemWritableFileStreamParent.h"
+
+namespace mozilla::dom {
+
+class FileSystemManagerParent;
+
+class FileSystemWritableFileStreamParent
+ : public PFileSystemWritableFileStreamParent {
+ public:
+ FileSystemWritableFileStreamParent(RefPtr<FileSystemManagerParent> aManager,
+ const fs::EntryId& aEntryId);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemWritableFileStreamParent,
+ override)
+
+ mozilla::ipc::IPCResult RecvClose();
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ virtual ~FileSystemWritableFileStreamParent();
+
+ bool IsClosed() const { return mClosed; }
+
+ void Close();
+
+ const RefPtr<FileSystemManagerParent> mManager;
+
+ const fs::EntryId mEntryId;
+
+ 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..c294826ec0
--- /dev/null
+++ b/dom/fs/parent/ResultStatement.h
@@ -0,0 +1,156 @@
+/* -*- 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 "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 BindContentTypeByName(const nsACString& aField,
+ const ContentType& aValue) {
+ return mStmt->BindStringByName(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->GetString(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<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/datamodel/FileSystemDataManager.cpp b/dom/fs/parent/datamodel/FileSystemDataManager.cpp
new file mode 100644
index 0000000000..73b5327d77
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDataManager.cpp
@@ -0,0 +1,529 @@
+/* -*- 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 "FileSystemDatabaseManager.h"
+#include "FileSystemDatabaseManagerVersion001.h"
+#include "FileSystemFileManager.h"
+#include "FileSystemHashSource.h"
+#include "ResultStatement.h"
+#include "SchemaVersion001.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/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 "nsProxyRelease.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;
+ }
+}
+
+Result<ResultConnection, QMResult> GetStorageConnection(
+ const Origin& aOrigin, const int64_t aDirectoryLockId) {
+ MOZ_ASSERT(aDirectoryLockId >= 0);
+
+ // Ensure that storage is initialized and file system folder exists!
+ QM_TRY_INSPECT(const auto& dbFileUrl,
+ GetDatabaseFileURL(aOrigin, 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;
+}
+
+} // namespace
+
+Result<EntryId, QMResult> GetRootHandle(const Origin& origin) {
+ MOZ_ASSERT(!origin.IsEmpty());
+
+ return FileSystemHashSource::GenerateHash(origin, kRootName);
+}
+
+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,
+ MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget,
+ MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue)
+ : mOriginMetadata(aOriginMetadata),
+ mBackgroundTarget(WrapNotNull(GetCurrentSerialEventTarget())),
+ mIOTarget(std::move(aIOTarget)),
+ mIOTaskQueue(std::move(aIOTaskQueue)),
+ mRegCount(0),
+ mState(State::Initial) {}
+
+FileSystemDataManager::~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&) {
+ 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__);
+ }
+
+ 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, WrapMovingNotNull(streamTransportService),
+ WrapMovingNotNull(ioTaskQueue));
+
+ AddFileSystemDataManager(aOriginMetadata.mOrigin, dataManager);
+
+ return dataManager->BeginOpen()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [dataManager = Registered<FileSystemDataManager>(dataManager)](
+ const BoolPromise::ResolveOrRejectValue&) {
+ 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);
+}
+
+void FileSystemDataManager::UnregisterActor(
+ NotNull<FileSystemManagerParent*> aActor) {
+ MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mActors.Contains(aActor));
+
+ mBackgroundThreadAccessible.Access()->mActors.Remove(aActor);
+
+ 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__);
+}
+
+bool FileSystemDataManager::IsLocked(const EntryId& aEntryId) const {
+ return mExclusiveLocks.Contains(aEntryId);
+}
+
+bool FileSystemDataManager::LockExclusive(const EntryId& aEntryId) {
+ if (IsLocked(aEntryId)) {
+ return false;
+ }
+
+ LOG_VERBOSE(("ExclusiveLock"));
+ mExclusiveLocks.Insert(aEntryId);
+
+ return true;
+}
+
+void FileSystemDataManager::UnlockExclusive(const EntryId& aEntryId) {
+ MOZ_ASSERT(mExclusiveLocks.Contains(aEntryId));
+
+ LOG_VERBOSE(("ExclusiveUnlock"));
+ mExclusiveLocks.Remove(aEntryId);
+
+ QM_WARNONLY_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(aEntryId)));
+}
+
+bool FileSystemDataManager::LockShared(const EntryId& aEntryId) {
+ return LockExclusive(aEntryId);
+}
+
+void FileSystemDataManager::UnlockShared(const EntryId& aEntryId) {
+ UnlockExclusive(aEntryId);
+}
+
+bool FileSystemDataManager::IsInactive() const {
+ return !mRegCount && !mBackgroundThreadAccessible.Access()->mActors.Count();
+}
+
+void FileSystemDataManager::RequestAllowToClose() {
+ for (const auto& actor : mBackgroundThreadAccessible.Access()->mActors) {
+ actor->RequestAllowToClose();
+ }
+}
+
+RefPtr<BoolPromise> FileSystemDataManager::BeginOpen() {
+ MOZ_ASSERT(mState == State::Initial);
+
+ mState = State::Opening;
+
+ QM_TRY_UNWRAP(const NotNull<RefPtr<quota::QuotaManager>> quotaManager,
+ quota::QuotaManager::GetOrCreate(), CreateAndRejectBoolPromise);
+
+ RefPtr<quota::ClientDirectoryLock> directoryLock =
+ quotaManager->CreateDirectoryLock(quota::PERSISTENCE_TYPE_DEFAULT,
+ mOriginMetadata,
+ mozilla::dom::quota::Client::FILESYSTEM,
+ /* aExclusive */ false);
+
+ directoryLock->Acquire()
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this),
+ directoryLock = directoryLock](
+ const BoolPromise::ResolveOrRejectValue& value) mutable {
+ if (value.IsReject()) {
+ return BoolPromise::CreateAndReject(value.RejectValue(),
+ __func__);
+ }
+
+ self->mDirectoryLock = std::move(directoryLock);
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(quotaManager->IOThread(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)](
+ const BoolPromise::ResolveOrRejectValue& value) mutable {
+ auto autoProxyReleaseManager = MakeScopeExit([&self] {
+ nsCOMPtr<nsISerialEventTarget> target =
+ self->MutableBackgroundTargetPtr();
+
+ NS_ProxyRelease("ReleaseFileSystemDataManager", target,
+ self.forget());
+ });
+
+ if (value.IsReject()) {
+ return BoolPromise::CreateAndReject(value.RejectValue(),
+ __func__);
+ }
+
+ QM_TRY(MOZ_TO_RESULT(
+ EnsureFileSystemDirectory(self->mOriginMetadata)),
+ CreateAndRejectBoolPromise);
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(MutableIOTargetPtr(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)](
+ const BoolPromise::ResolveOrRejectValue& value) mutable {
+ auto autoProxyReleaseManager = MakeScopeExit([&self] {
+ nsCOMPtr<nsISerialEventTarget> target =
+ self->MutableBackgroundTargetPtr();
+
+ NS_ProxyRelease("ReleaseFileSystemDataManager", target,
+ self.forget());
+ });
+
+ if (value.IsReject()) {
+ return BoolPromise::CreateAndReject(value.RejectValue(),
+ __func__);
+ }
+
+ QM_TRY_UNWRAP(
+ auto connection,
+ fs::data::GetStorageConnection(self->mOriginMetadata.mOrigin,
+ self->mDirectoryLock->Id()),
+ CreateAndRejectBoolPromiseFromQMResult);
+
+ QM_TRY_UNWRAP(DatabaseVersion version,
+ SchemaVersion001::InitializeConnection(
+ connection, self->mOriginMetadata.mOrigin),
+ CreateAndRejectBoolPromiseFromQMResult);
+
+ if (1 == version) {
+ QM_TRY_UNWRAP(
+ FileSystemFileManager fmRes,
+ FileSystemFileManager::CreateFileSystemFileManager(
+ self->mOriginMetadata.mOrigin),
+ CreateAndRejectBoolPromiseFromQMResult);
+
+ QM_TRY_UNWRAP(
+ EntryId rootId,
+ fs::data::GetRootHandle(self->mOriginMetadata.mOrigin),
+ CreateAndRejectBoolPromiseFromQMResult);
+
+ self->mDatabaseManager =
+ MakeUnique<FileSystemDatabaseManagerVersion001>(
+ self, std::move(connection),
+ MakeUnique<FileSystemFileManager>(std::move(fmRes)),
+ rootId);
+ }
+
+ 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(MutableIOTargetPtr(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)]() mutable {
+ auto autoProxyReleaseManager = MakeScopeExit([&self] {
+ nsCOMPtr<nsISerialEventTarget> target =
+ self->MutableBackgroundTargetPtr();
+
+ NS_ProxyRelease("ReleaseFileSystemDataManager", target,
+ self.forget());
+ });
+
+ 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..12ebbd2b41
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDataManager.h
@@ -0,0 +1,161 @@
+/* -*- 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 "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 FileSystemManagerParent;
+
+namespace fs {
+class FileSystemChildMetadata;
+} // namespace fs
+
+namespace quota {
+class DirectoryLock;
+} // namespace quota
+
+namespace fs::data {
+
+class FileSystemDatabaseManager;
+
+Result<EntryId, QMResult> GetRootHandle(const Origin& origin);
+
+Result<EntryId, QMResult> GetEntryHandle(
+ const FileSystemChildMetadata& aHandle);
+
+class FileSystemDataManager
+ : public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
+ public:
+ enum struct State : uint8_t { Initial = 0, Opening, Open, Closing, Closed };
+
+ FileSystemDataManager(const quota::OriginMetadata& aOriginMetadata,
+ 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_REFCOUNTING(FileSystemDataManager)
+
+ void AssertIsOnIOTarget() const;
+
+ const quota::OriginMetadata& OriginMetadataRef() const {
+ return mOriginMetadata;
+ }
+
+ nsISerialEventTarget* MutableBackgroundTargetPtr() const {
+ return mBackgroundTarget.get();
+ }
+
+ nsISerialEventTarget* MutableIOTargetPtr() 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);
+
+ bool IsOpen() const { return mState == State::Open; }
+
+ RefPtr<BoolPromise> OnOpen();
+
+ RefPtr<BoolPromise> OnClose();
+
+ bool IsLocked(const EntryId& aEntryId) const;
+
+ bool LockExclusive(const EntryId& aEntryId);
+
+ void UnlockExclusive(const EntryId& aEntryId);
+
+ bool LockShared(const EntryId& aEntryId);
+
+ void UnlockShared(const EntryId& aEntryId);
+
+ 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;
+ };
+ ThreadBound<BackgroundThreadAccessible> mBackgroundThreadAccessible;
+
+ const quota::OriginMetadata mOriginMetadata;
+ nsTHashSet<EntryId> mExclusiveLocks;
+ 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;
+ 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..7fc37da8d5
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemDatabaseManager.h"
+
+#include "FileSystemDatabaseManagerVersion001.h"
+#include "FileSystemFileManager.h"
+#include "ResultConnection.h"
+#include "ResultStatement.h"
+#include "SchemaVersion001.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+
+namespace mozilla::dom::fs::data {
+
+/* static */
+Result<quota::UsageInfo, QMResult> FileSystemDatabaseManager::GetUsage(
+ const ResultConnection& aConnection, const Origin& aOrigin) {
+ QM_TRY_INSPECT(const auto& databaseFile, GetDatabaseFile(aOrigin));
+
+ // 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)));
+
+ DatabaseVersion version = 0;
+ QM_TRY(QM_TO_RESULT(aConnection->GetSchemaVersion(&version)));
+
+ switch (version) {
+ case 0: {
+ return result;
+ }
+
+ case 1: {
+ QM_TRY_INSPECT(
+ const Usage& fileUsage,
+ FileSystemDatabaseManagerVersion001::GetFileUsage(aConnection));
+
+ // XXX: DatabaseUsage is currently total usage for most forms of storage
+ result += quota::DatabaseUsageType(Some(fileUsage));
+
+ return result;
+ }
+
+ default:
+ break;
+ }
+
+ return Err(QMResult(NS_ERROR_NOT_IMPLEMENTED));
+}
+
+} // 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..259cd4d392
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.h
@@ -0,0 +1,151 @@
+/* -*- 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::fs {
+
+class FileSystemChildMetadata;
+class FileSystemEntryMetadata;
+class FileSystemDirectoryListing;
+class FileSystemEntryPair;
+
+namespace data {
+
+class FileSystemDatabaseManager {
+ public:
+ /**
+ * @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 Origin& aOrigin);
+
+ /**
+ * @brief Refreshes the stored file size.
+ *
+ * @param aEntry EntryId of the file whose size is refreshed.
+ */
+ virtual nsresult UpdateUsage(const EntryId& aEntry) = 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
+ * @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, nsString& 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<bool, QMResult> False if entry didn't exist, otherwise true
+ * or error
+ */
+ virtual Result<bool, 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<bool, QMResult> False if entry didn't exist, otherwise true
+ * or error
+ */
+ virtual Result<bool, 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 Close database connection.
+ */
+ virtual void Close() = 0;
+
+ virtual ~FileSystemDatabaseManager() = default;
+};
+
+} // namespace data
+} // namespace dom::fs
+} // 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..87105a668b
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp
@@ -0,0 +1,1002 @@
+/* -*- 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 "FileSystemFileManager.h"
+#include "ResultStatement.h"
+#include "mozStorageHelper.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/QuotaCommon.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+
+namespace mozilla::dom {
+
+using FileSystemEntries = nsTArray<fs::FileSystemEntryMetadata>;
+
+namespace fs::data {
+
+namespace {
+
+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> IsDirectoryEmpty(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 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<Path, QMResult> ResolveReversedPath(
+ 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 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;
+}
+
+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 JOIN Entries USING (handle) "
+ "WHERE Files.name = :name AND Entries.parent = :parent ) "
+ ";"_ns;
+
+ QM_TRY_RETURN(ApplyEntryExistsQuery(aConnection, existsQuery, aHandle));
+}
+
+Result<bool, QMResult> DoesFileExist(const FileSystemConnection& aConnection,
+ const EntryId& aEntry) {
+ MOZ_ASSERT(!aEntry.IsEmpty());
+
+ const nsCString existsQuery =
+ "SELECT EXISTS "
+ "(SELECT 1 FROM Files WHERE handle = :handle ) "
+ ";"_ns;
+
+ QM_TRY_RETURN(ApplyEntryExistsQuery(aConnection, existsQuery, aEntry));
+}
+
+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;
+}
+
+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;
+}
+
+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));
+}
+
+Result<EntryId, QMResult> FindEntryId(const FileSystemConnection& aConnection,
+ const FileSystemChildMetadata& aHandle,
+ bool aIsFile) {
+ const nsCString aDirectoryQuery =
+ "SELECT Entries.handle FROM Directories JOIN Entries USING (handle) "
+ "WHERE Directories.name = :name AND Entries.parent = :parent "
+ ";"_ns;
+
+ const nsCString aFileQuery =
+ "SELECT Entries.handle FROM Files 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;
+}
+
+bool IsSame(const FileSystemConnection& aConnection,
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewHandle, bool aIsFile) {
+ MOZ_ASSERT(!aNewHandle.parentId().IsEmpty());
+
+ QM_TRY_UNWRAP(EntryId entryId, FindEntryId(aConnection, aNewHandle, aIsFile),
+ false);
+ return entryId == aHandle.entryId();
+}
+
+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));
+}
+
+nsresult PerformRename(const FileSystemConnection& aConnection,
+ const FileSystemEntryMetadata& aHandle,
+ const Name& aNewName,
+ 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;
+ }
+
+ auto toNSResult = [](const auto& aRv) { return ToNSResult(aRv); };
+
+ // 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));
+ 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,
+ updateDirectoryNameQuery);
+}
+
+nsresult PerformRenameFile(const FileSystemConnection& aConnection,
+ const FileSystemEntryMetadata& aHandle,
+ const Name& aNewName) {
+ const nsLiteralCString updateFileNameQuery =
+ "UPDATE Files "
+ "SET name = :name "
+ "WHERE handle = :handle "
+ ";"_ns;
+
+ return PerformRename(aConnection, aHandle, aNewName, updateFileNameQuery);
+}
+
+} // namespace
+
+/* 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::UpdateUsageInDatabase(
+ const EntryId& aEntry, int64_t 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.BindEntryIdByName("handle"_ns, aEntry)));
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+
+ 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_UNWRAP(EntryId entryId, GetUniqueEntryId(mConnection, aHandle));
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ mozStorageTransaction transaction(
+ mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+ {
+ 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
+ if (exists) {
+ return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR));
+ }
+
+ QM_TRY_UNWRAP(exists, DoesFileExist(mConnection, aHandle));
+
+ if (exists) {
+ return FindEntryId(mConnection, aHandle, 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, name ) "
+ "VALUES "
+ "( :handle, :name ) "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(EntryId entryId, GetUniqueEntryId(mConnection, aHandle));
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ mozStorageTransaction transaction(
+ mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+ {
+ 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, insertFileQuery));
+ 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()));
+
+ return entryId;
+}
+
+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;
+}
+
+nsresult FileSystemDatabaseManagerVersion001::GetFile(
+ const EntryId& aEntryId, nsString& aType,
+ TimeStamp& lastModifiedMilliSeconds, nsTArray<Name>& aPath,
+ nsCOMPtr<nsIFile>& aFile) const {
+ MOZ_ASSERT(!aEntryId.IsEmpty());
+
+ QM_TRY_UNWRAP(aFile, mFileManager->GetOrCreateFile(aEntryId));
+
+ QM_TRY(MOZ_TO_RESULT(GetFileAttributes(mConnection, aEntryId, aType)));
+
+ PRTime lastModTime = 0;
+ QM_TRY(MOZ_TO_RESULT(aFile->GetLastModifiedTime(&lastModTime)));
+ lastModifiedMilliSeconds = static_cast<TimeStamp>(lastModTime);
+
+ FileSystemEntryPair endPoints(mRootEntry, aEntryId);
+ QM_TRY_UNWRAP(aPath, ResolveReversedPath(mConnection, endPoints));
+ if (aPath.IsEmpty()) {
+ return NS_ERROR_DOM_NOT_FOUND_ERR;
+ }
+ aPath.Reverse();
+
+ return NS_OK;
+}
+
+nsresult FileSystemDatabaseManagerVersion001::UpdateUsage(
+ const EntryId& aEntry) {
+ auto toNSResult = [](const auto& aRv) { return ToNSResult(aRv); };
+
+ // We don't track directories or non-existent files.
+ QM_TRY_UNWRAP(bool fileExists,
+ DoesFileExist(mConnection, aEntry).mapErr(toNSResult));
+ if (!fileExists) {
+ return NS_OK; // May be deleted before update, no assert
+ }
+
+ QM_TRY_UNWRAP(bool isFolder,
+ DoesDirectoryExist(mConnection, aEntry).mapErr(toNSResult));
+ if (isFolder) {
+ return NS_OK; // May be deleted and replaced by a folder, no assert
+ }
+
+ nsCOMPtr<nsIFile> file;
+ QM_TRY_UNWRAP(file, mFileManager->GetOrCreateFile(aEntry));
+ MOZ_ASSERT(file);
+
+ int64_t fileSize = 0;
+ QM_TRY(MOZ_TO_RESULT(file->GetFileSize(&fileSize)));
+
+ QM_TRY(MOZ_TO_RESULT(UpdateUsageInDatabase(aEntry, fileSize)));
+
+ return NS_OK;
+}
+
+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));
+
+ if (!aRecursive && !isEmpty) {
+ return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR));
+ }
+ // If it's empty or we can delete recursively, deleting the handle will
+ // cascade
+
+ 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 handle "
+ "FROM traceChildren INNER JOIN Files "
+ "USING(handle) "
+ ";"_ns;
+
+ const nsLiteralCString deleteEntryQuery =
+ "DELETE FROM Entries "
+ "WHERE handle = :handle "
+ ";"_ns;
+
+ mozStorageTransaction transaction(
+ mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ nsTArray<EntryId> descendants;
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, descendantsQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ while (moreResults) {
+ QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 0u));
+
+ descendants.AppendElement(entryId);
+
+ QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep());
+ }
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, deleteEntryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ for (const auto& child : descendants) {
+ QM_WARNONLY_TRY_UNWRAP(const auto maybeFileSize,
+ mFileManager->RemoveFile(child));
+
+ if (maybeFileSize) {
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ quotaManager->DecreaseUsageForClient(
+ quota::ClientMetadata{mDataManager->OriginMetadataRef(),
+ quota::Client::FILESYSTEM},
+ *maybeFileSize);
+ }
+ }
+
+ 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
+ if (mDataManager->IsLocked(entryId)) {
+ LOG(("Trying to remove in-use file"));
+ return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR));
+ }
+
+ const nsLiteralCString deleteEntryQuery =
+ "DELETE FROM Entries "
+ "WHERE handle = :handle "
+ ";"_ns;
+
+ mozStorageTransaction transaction(
+ mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, deleteEntryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ QM_WARNONLY_TRY_UNWRAP(const auto maybeFileSize,
+ mFileManager->RemoveFile(entryId));
+
+ if (maybeFileSize) {
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ quotaManager->DecreaseUsageForClient(
+ quota::ClientMetadata{mDataManager->OriginMetadataRef(),
+ quota::Client::FILESYSTEM},
+ *maybeFileSize);
+ }
+
+ return true;
+}
+
+Result<bool, QMResult> FileSystemDatabaseManagerVersion001::RenameEntry(
+ const FileSystemEntryMetadata& aHandle, const Name& aNewName) {
+ // Can't rename root
+ if (mRootEntry == aHandle.entryId()) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ // Verify the source exists
+ QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, aHandle.entryId()), false);
+
+ // At this point, entry exists
+ if (isFile && mDataManager->IsLocked(aHandle.entryId())) {
+ LOG(("Trying to move in-use file"));
+ return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
+ }
+
+ // Are we actually renaming?
+ if (aHandle.entryName() == aNewName) {
+ return true;
+ }
+
+ // If the destination file exists, fail explicitly.
+ FileSystemChildMetadata destination;
+ QM_TRY_UNWRAP(EntryId parent, FindParent(mConnection, aHandle.entryId()));
+ destination.parentId() = parent;
+ destination.childName() = aNewName;
+
+ QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, destination));
+ if (exists) {
+ // If the destination file exists, check if it is in use
+ QM_TRY_INSPECT(const EntryId& destId,
+ FindEntryId(mConnection, destination, true));
+ if (mDataManager->IsLocked(destId)) {
+ LOG(("Trying to overwrite in-use file"));
+ return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
+ }
+
+ QM_TRY_UNWRAP(DebugOnly<bool> isRemoved, RemoveFile(destination));
+ MOZ_ASSERT(isRemoved);
+ } else {
+ QM_TRY_UNWRAP(exists, DoesDirectoryExist(mConnection, destination));
+ if (exists) {
+ return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR));
+ }
+ }
+
+ mozStorageTransaction transaction(
+ mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ if (isFile) {
+ QM_TRY(QM_TO_RESULT(PerformRenameFile(mConnection, aHandle, aNewName)));
+ } else {
+ QM_TRY(
+ QM_TO_RESULT(PerformRenameDirectory(mConnection, aHandle, aNewName)));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return true;
+}
+
+Result<bool, QMResult> FileSystemDatabaseManagerVersion001::MoveEntry(
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation) {
+ MOZ_ASSERT(!aHandle.entryId().IsEmpty());
+
+ const EntryId& entryId = aHandle.entryId();
+ const Name& newName = aNewDesignation.childName();
+
+ if (mRootEntry == entryId) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ // Verify the source exists
+ QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId), false);
+
+ // If the rename doesn't change the name or directory, just return success.
+ // XXX Needs to be added to the spec
+ if (IsSame(mConnection, aHandle, aNewDesignation, isFile)) {
+ return true;
+ }
+
+ // At this point, entry exists
+ if (isFile && mDataManager->IsLocked(entryId)) {
+ LOG(("Trying to move in-use file"));
+ return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
+ }
+
+ // If the destination file exists, fail explicitly. Spec author plans to
+ // revise the spec
+ QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, aNewDesignation));
+ if (exists) {
+ QM_TRY_INSPECT(const EntryId& destId,
+ FindEntryId(mConnection, aNewDesignation, true));
+ if (mDataManager->IsLocked(destId)) {
+ LOG(("Trying to overwrite in-use file"));
+ return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
+ }
+
+ QM_TRY_UNWRAP(DebugOnly<bool> isRemoved, RemoveFile(aNewDesignation));
+ MOZ_ASSERT(isRemoved);
+ } else {
+ QM_TRY_UNWRAP(exists, DoesDirectoryExist(mConnection, aNewDesignation));
+ if (exists) {
+ return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR));
+ }
+ }
+
+ // 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(mConnection, {aHandle.entryId(), aNewDesignation.parentId()}));
+ if (isDestinationUnderSelf) {
+ return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR));
+ }
+
+ const nsLiteralCString updateEntryParentQuery =
+ "UPDATE Entries "
+ "SET parent = :parent "
+ "WHERE handle = :handle "
+ ";"_ns;
+
+ mozStorageTransaction transaction(
+ mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ {
+ // 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()));
+ }
+
+ // Are we actually renaming?
+ if (aHandle.entryName() == newName) {
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return true;
+ }
+
+ if (isFile) {
+ QM_TRY(QM_TO_RESULT(PerformRenameFile(mConnection, aHandle, newName)));
+ } else {
+ QM_TRY(QM_TO_RESULT(PerformRenameDirectory(mConnection, aHandle, newName)));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return true;
+}
+
+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;
+}
+
+void FileSystemDatabaseManagerVersion001::Close() { mConnection->Close(); }
+
+} // 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..e6edc4ca8a
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h
@@ -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/. */
+
+#ifndef DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_
+#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_
+
+#include "FileSystemDatabaseManager.h"
+#include "nsString.h"
+
+namespace mozilla::dom::fs::data {
+
+class FileSystemDataManager;
+class FileSystemFileManager;
+using FileSystemConnection = fs::ResultConnection;
+
+/**
+ * @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)
+ : mDataManager(aDataManager),
+ mConnection(aConnection),
+ mFileManager(std::move(aFileManager)),
+ mRootEntry(aRootEntry) {}
+
+ static Result<Usage, QMResult> GetFileUsage(
+ const FileSystemConnection& aConnection);
+
+ virtual nsresult UpdateUsage(const EntryId& aEntry) override;
+
+ virtual Result<EntryId, QMResult> GetOrCreateDirectory(
+ const FileSystemChildMetadata& aHandle, bool aCreate) override;
+
+ virtual Result<EntryId, QMResult> GetOrCreateFile(
+ const FileSystemChildMetadata& aHandle, bool aCreate) override;
+
+ virtual nsresult GetFile(const EntryId& aEntryId, nsString& aType,
+ TimeStamp& lastModifiedMilliSeconds, Path& aPath,
+ nsCOMPtr<nsIFile>& aFile) const override;
+
+ virtual Result<FileSystemDirectoryListing, QMResult> GetDirectoryEntries(
+ const EntryId& aParent, PageNumber aPage) const override;
+
+ virtual Result<bool, QMResult> RenameEntry(
+ const FileSystemEntryMetadata& aHandle, const Name& aNewName) override;
+
+ virtual Result<bool, QMResult> MoveEntry(
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation) override;
+
+ virtual Result<bool, QMResult> RemoveDirectory(
+ const FileSystemChildMetadata& aHandle, bool aRecursive) override;
+
+ virtual Result<bool, QMResult> RemoveFile(
+ const FileSystemChildMetadata& aHandle) override;
+
+ virtual Result<Path, QMResult> Resolve(
+ const FileSystemEntryPair& aEndpoints) const override;
+
+ virtual void Close() override;
+
+ virtual ~FileSystemDatabaseManagerVersion001() = default;
+
+ private:
+ nsresult UpdateUsageInDatabase(const EntryId& aEntry, int64_t aNewDiskUsage);
+
+ // 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;
+};
+
+} // namespace mozilla::dom::fs::data
+
+#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_
diff --git a/dom/fs/parent/datamodel/FileSystemFileManager.cpp b/dom/fs/parent/datamodel/FileSystemFileManager.cpp
new file mode 100644
index 0000000000..4a70e634e4
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemFileManager.cpp
@@ -0,0 +1,267 @@
+/* -*- 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 "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 "nsIFile.h"
+#include "nsIFileProtocolHandler.h"
+#include "nsIFileURL.h"
+#include "nsIURIMutator.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 EntryId& aEntryId) {
+ MOZ_ASSERT(32u == aEntryId.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(aEntryId));
+
+ 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());
+
+ nsCOMPtr<nsIFile> result;
+ QM_TRY(QM_TO_RESULT(NS_NewLocalFile(aFilePath,
+ /* aFollowLinks */ false,
+ getter_AddRefs(result))));
+
+ 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 EntryId& aEntryId) {
+ MOZ_ASSERT(!aEntryId.IsEmpty());
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject,
+ GetFileDestination(aTopDirectory, aEntryId));
+
+ nsString desiredPath;
+ QM_TRY(QM_TO_RESULT(pathObject->GetPath(desiredPath)));
+
+ nsCOMPtr<nsIFile> result;
+ QM_TRY(QM_TO_RESULT(NS_NewLocalFile(desiredPath,
+ /* aFollowLinks */ false,
+ getter_AddRefs(result))));
+
+ return result;
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFile(
+ const nsCOMPtr<nsIFile>& aTopDirectory, const EntryId& aEntryId) {
+ MOZ_ASSERT(!aEntryId.IsEmpty());
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject,
+ GetFileDestination(aTopDirectory, aEntryId));
+
+ nsString desiredPath;
+ QM_TRY(QM_TO_RESULT(pathObject->GetPath(desiredPath)));
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> result, GetOrCreateFileImpl(desiredPath));
+
+ return result;
+}
+
+} // namespace
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetFileSystemDirectory(
+ const Origin& aOrigin) {
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> fileSystemDirectory,
+ QM_TO_RESULT_TRANSFORM(quotaManager->GetDirectoryForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT, aOrigin)));
+
+ 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->EnsureStorageIsInitialized()));
+
+ QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureTemporaryStorageIsInitialized()));
+
+ 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 Origin& aOrigin) {
+ MOZ_ASSERT(!aOrigin.IsEmpty());
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> databaseFile,
+ GetFileSystemDirectory(aOrigin));
+
+ 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 Origin& aOrigin, const int64_t aDirectoryLockId) {
+ MOZ_ASSERT(aDirectoryLockId >= 0);
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> databaseFile, GetDatabaseFile(aOrigin));
+
+ 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)));
+
+ nsCString directoryLockIdClause = "&directoryLockId="_ns;
+ directoryLockIdClause.AppendInt(aDirectoryLockId);
+
+ nsCOMPtr<nsIFileURL> result;
+ QM_TRY(QM_TO_RESULT(
+ NS_MutateURI(mutator).SetQuery(directoryLockIdClause).Finalize(result)));
+
+ return result;
+}
+
+/* static */
+Result<FileSystemFileManager, QMResult>
+FileSystemFileManager::CreateFileSystemFileManager(
+ nsCOMPtr<nsIFile>&& topDirectory) {
+ return FileSystemFileManager(std::move(topDirectory));
+}
+
+/* static */
+Result<FileSystemFileManager, QMResult>
+FileSystemFileManager::CreateFileSystemFileManager(const Origin& aOrigin) {
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> topDirectory,
+ GetFileSystemDirectory(aOrigin));
+
+ return FileSystemFileManager(std::move(topDirectory));
+}
+
+FileSystemFileManager::FileSystemFileManager(nsCOMPtr<nsIFile>&& aTopDirectory)
+ : mTopDirectory(std::move(aTopDirectory)) {}
+
+Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::GetFile(
+ const EntryId& aEntryId) const {
+ return data::GetFile(mTopDirectory, aEntryId);
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::GetOrCreateFile(
+ const EntryId& aEntryId) {
+ return data::GetOrCreateFile(mTopDirectory, aEntryId);
+}
+
+Result<int64_t, QMResult> FileSystemFileManager::RemoveFile(
+ const EntryId& aEntryId) {
+ MOZ_ASSERT(!aEntryId.IsEmpty());
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject,
+ GetFileDestination(mTopDirectory, aEntryId));
+
+ 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)));
+
+ if (!isFile) {
+ return Err(QMResult(NS_ERROR_FILE_IS_DIRECTORY));
+ }
+
+ QM_TRY_UNWRAP(int64_t fileSize,
+ QM_TO_RESULT_INVOKE_MEMBER(pathObject, GetFileSize));
+
+ QM_TRY(QM_TO_RESULT(pathObject->Remove(/* recursive */ false)));
+
+ return fileSize;
+}
+
+} // 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..9df92b04f8
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemFileManager.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_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_
+#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_
+
+#include "ErrorList.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::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 Origin& aOrigin);
+
+/**
+ * @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 Origin& aOrigin);
+
+/**
+ * @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 Origin& aOrigin, 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<FileSystemFileManager, QMResult> CreateFileSystemFileManager(
+ const Origin& aOrigin);
+
+ /**
+ * @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 EntryId& aEntryId) const;
+
+ /**
+ * @brief Get or create a disk-backed file object for a specified entry id.
+ *
+ * @param aEntryId Specified id of a file system entry
+ * @return Result<nsCOMPtr<nsIFile>, QMResult> File abstraction or IO error
+ */
+ Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFile(const EntryId& aEntryId);
+
+ /**
+ * @brief Remove the disk-backed file object for a specified entry id.
+ *
+ * @param aEntryId Specified id of a file system entry
+ * @return Result<int64_t, QMResult> Error or file size
+ */
+ Result<int64_t, QMResult> RemoveFile(const EntryId& aEntryId);
+
+ private:
+ explicit FileSystemFileManager(nsCOMPtr<nsIFile>&& aTopDirectory);
+
+ nsCOMPtr<nsIFile> mTopDirectory;
+};
+
+} // namespace fs::data
+} // 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..435b2a6dd1
--- /dev/null
+++ b/dom/fs/parent/datamodel/SchemaVersion001.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 "SchemaVersion001.h"
+
+#include "FileSystemHashSource.h"
+#include "ResultStatement.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 SetEncoding(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(R"(PRAGMA encoding = "UTF-16";)"_ns);
+}
+
+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, kRootName));
+
+ mozStorageTransaction transaction(
+ aConn.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ {
+ 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, kRootName)));
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+ }
+
+ return transaction.Commit();
+}
+
+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));
+
+ return stmt.YesOrNoQuery();
+};
+
+} // namespace
+
+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) {
+ mozStorageTransaction transaction(
+ aConn.get(),
+ /* commit on complete */ false,
+ mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ QM_TRY(QM_TO_RESULT(CreateEntries(aConn)));
+ QM_TRY(QM_TO_RESULT(CreateDirectories(aConn)));
+ QM_TRY(QM_TO_RESULT(CreateFiles(aConn)));
+ QM_TRY(QM_TO_RESULT(CreateUsages(aConn)));
+ QM_TRY(QM_TO_RESULT(CreateRootEntry(aConn, aOrigin)));
+ QM_TRY(QM_TO_RESULT(aConn->SetSchemaVersion(sVersion)));
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+ }
+
+ 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..606cada50d
--- /dev/null
+++ b/dom/fs/parent/datamodel/SchemaVersion001.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_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 Result<DatabaseVersion, QMResult> InitializeConnection(
+ ResultConnection& aConn, const Origin& aOrigin);
+
+ static const DatabaseVersion sVersion = 1;
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_
diff --git a/dom/fs/parent/datamodel/moz.build b/dom/fs/parent/datamodel/moz.build
new file mode 100644
index 0000000000..111a793722
--- /dev/null
+++ b/dom/fs/parent/datamodel/moz.build
@@ -0,0 +1,26 @@
+# -*- 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",
+ "FileSystemDataManager.cpp",
+ "FileSystemFileManager.cpp",
+ "SchemaVersion001.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..c8a254dcff
--- /dev/null
+++ b/dom/fs/parent/moz.build
@@ -0,0 +1,47 @@
+# -*- 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 += [
+ "FileSystemAccessHandleParent.h",
+ "FileSystemManagerParent.h",
+ "FileSystemManagerParentFactory.h",
+ "FileSystemQuotaClient.h",
+ "FileSystemWritableFileStreamParent.h",
+]
+
+UNIFIED_SOURCES += [
+ "FileSystemAccessHandleParent.cpp",
+ "FileSystemHashSource.cpp",
+ "FileSystemManagerParent.cpp",
+ "FileSystemManagerParentFactory.cpp",
+ "FileSystemQuotaClient.cpp",
+ "FileSystemStreamCallbacks.cpp",
+ "FileSystemWritableFileStreamParent.cpp",
+ "ResultStatement.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"],
+ )
+
+ EXPORTS.mozilla.dom += [
+ "!data_encoding_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/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..afa51651c9
--- /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 = nsString;
+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/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/PFileSystemManager.ipdl b/dom/fs/shared/PFileSystemManager.ipdl
new file mode 100644
index 0000000000..7a02bdfddb
--- /dev/null
+++ b/dom/fs/shared/PFileSystemManager.ipdl
@@ -0,0 +1,425 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 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;
+ PFileSystemAccessHandle accessHandle;
+};
+
+union FileSystemGetAccessHandleResponse
+{
+ nsresult;
+ FileSystemAccessHandleProperties;
+};
+
+/**
+ * Contains entry handle information.
+ */
+struct FileSystemGetWritableRequest
+{
+ EntryId entryId;
+ bool keepData;
+};
+
+struct FileSystemWritableFileStreamProperties
+{
+ FileDescriptor fileDescriptor;
+ 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;
+ void_t;
+};
+
+struct FileSystemQuotaRequest
+{
+ FileSystemChildMetadata handle;
+ uint64_t quotaNeeded;
+};
+
+} // namespace fs
+
+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);
+
+ /**
+ * Request for quota needed to finish a write, beyond the amount preallocated
+ * at creation of the AccessHandle. While officially async, this is used in a
+ * sync manner from write() by spinning an event loop.
+ */
+ async NeedQuota(FileSystemQuotaRequest aRequest)
+ returns (uint64_t aQuotaGranted);
+
+ child:
+ async PFileSystemAccessHandle();
+ 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..43b661f961
--- /dev/null
+++ b/dom/fs/shared/PFileSystemWritableFileStream.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 PFileSystemWritableFileStream
+{
+ manager PFileSystemManager;
+
+ parent:
+ async Close();
+
+ 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..952fc075b9
--- /dev/null
+++ b/dom/fs/shared/TargetPtrHolder.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_SHARED_TARGETPTRHOLDER_H_
+#define DOM_FS_SHARED_TARGETPTRHOLDER_H_
+
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.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..a7ede11cd4
--- /dev/null
+++ b/dom/fs/shared/moz.build
@@ -0,0 +1,30 @@
+# -*- 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 += [
+ "TargetPtrHolder.h",
+]
+
+UNIFIED_SOURCES += [
+ "FileSystemHelpers.cpp",
+ "FileSystemLog.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+IPDL_SOURCES += [
+ "PFileSystemAccessHandle.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..020f91bb02
--- /dev/null
+++ b/dom/fs/test/common/.eslintrc.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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,
+ },
+};
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.ini b/dom/fs/test/common/mochitest.ini
new file mode 100644
index 0000000000..4926b81a48
--- /dev/null
+++ b/dom/fs/test/common/mochitest.ini
@@ -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/.
+
+[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..c5191e9424
--- /dev/null
+++ b/dom/fs/test/common/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/.
+
+MOCHITEST_MANIFESTS += [
+ "mochitest.ini",
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "xpcshell.ini",
+]
+
+TESTING_JS_MODULES.dom.fs.test.common += [
+ "nsresult.js",
+ "test_basics.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..7a6b398ab4
--- /dev/null
+++ b/dom/fs/test/common/test_basics.js
@@ -0,0 +1,151 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+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");
+};
+
+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");
+};
+
+exported_symbols.testRemoveEntryIsCallable = async function() {
+ const root = await navigator.storage.getDirectory();
+ const removeOptions = { recursive: true };
+
+ 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");
+};
+
+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..192373b010
--- /dev/null
+++ b/dom/fs/test/common/test_fileSystemDirectoryHandle.js
@@ -0,0 +1,97 @@
+/**
+ * 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?");
+ }
+};
+
+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..df5788cd8e
--- /dev/null
+++ b/dom/fs/test/common/test_syncAccessHandle.js
@@ -0,0 +1,193 @@
+/* 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");
+ 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");
+ 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");
+ 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();
+ }
+};
+
+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..7ddf553533
--- /dev/null
+++ b/dom/fs/test/common/test_writableFileStream.js
@@ -0,0 +1,43 @@
+/* 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);
+};
+
+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.ini b/dom/fs/test/common/xpcshell.ini
new file mode 100644
index 0000000000..b63970a370
--- /dev/null
+++ b/dom/fs/test/common/xpcshell.ini
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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_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/crashtests.list b/dom/fs/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..a2e0c23d3d
--- /dev/null
+++ b/dom/fs/test/crashtests/crashtests.list
@@ -0,0 +1,6 @@
+# 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
diff --git a/dom/fs/test/gtest/FileSystemMocks.cpp b/dom/fs/test/gtest/FileSystemMocks.cpp
new file mode 100644
index 0000000000..0ae10d6929
--- /dev/null
+++ b/dom/fs/test/gtest/FileSystemMocks.cpp
@@ -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/. */
+
+#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)) {
+ 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..867302908b
--- /dev/null
+++ b/dom/fs/test/gtest/FileSystemMocks.h
@@ -0,0 +1,343 @@
+/* -*- 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,
+ const FileSystemEntryMetadata& aEntry,
+ const FileSystemChildMetadata& aNewEntry,
+ RefPtr<Promise> aPromise, ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, RenameEntry,
+ (RefPtr<FileSystemManager> & aManager, FileSystemHandle* aHandle,
+ const FileSystemEntryMetadata& 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)),
+ mTimer(),
+ 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..b9138c4a89
--- /dev/null
+++ b/dom/fs/test/gtest/TestHelpers.cpp
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "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,
+ 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..9114f3bb33
--- /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 = MakeUnique<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..615d907f48
--- /dev/null
+++ b/dom/fs/test/gtest/api/TestFileSystemFileHandle.cpp
@@ -0,0 +1,149 @@
+/* -*- 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);
+
+ // XXX This should be reverted back to check NS_OK once bug 1798513 is fixed.
+#if 0
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+#else
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_NOT_IMPLEMENTED));
+#endif
+}
+
+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..3d37414318
--- /dev/null
+++ b/dom/fs/test/gtest/child/TestFileSystemRequestHandler.cpp
@@ -0,0 +1,374 @@
+/* -*- 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:
+ struct FileSystemManagerChildShutdown {
+ explicit FileSystemManagerChildShutdown(
+ TestFileSystemManagerChild* aFileSystemManagerChild)
+ : mFileSystemManagerChild(aFileSystemManagerChild) {}
+
+ void operator()() const {
+ mFileSystemManagerChild->FileSystemManagerChild::Shutdown();
+ }
+
+ TestFileSystemManagerChild* mFileSystemManagerChild;
+ };
+
+ 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_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce(FileSystemManagerChildShutdown(mFileSystemManagerChild));
+
+ mManager->Shutdown();
+ }
+
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce(FileSystemManagerChildShutdown(mFileSystemManagerChild));
+ }
+
+ 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();
+ }
+
+ UniquePtr<FileSystemRequestHandler> GetFileSystemRequestHandler() {
+ return MakeUnique<FileSystemRequestHandler>();
+ }
+
+ 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) {
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce(FileSystemManagerChildShutdown(mFileSystemManagerChild));
+
+ mManager->Shutdown();
+
+ 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) {
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce(FileSystemManagerChildShutdown(mFileSystemManagerChild));
+
+ mManager->Shutdown();
+
+ 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) {
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce(FileSystemManagerChildShutdown(mFileSystemManagerChild));
+
+ mManager->Shutdown();
+
+ 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 = u"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) {
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce(FileSystemManagerChildShutdown(mFileSystemManagerChild));
+
+ mManager->Shutdown();
+
+ 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) {
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce(FileSystemManagerChildShutdown(mFileSystemManagerChild));
+
+ mManager->Shutdown();
+
+ 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) {
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce(FileSystemManagerChildShutdown(mFileSystemManagerChild));
+
+ mManager->Shutdown();
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetWritable(
+ mManager, mEntry, /* aKeepData */ false, GetSimplePromise(), error);
+
+ ASSERT_TRUE(error.Failed());
+ // XXX This should be reverted back to check NS_ERROR_ILLEGAL_DURING_SHUTDOWN
+ // once bug 1798513 is fixed.
+#if 0
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+#else
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_NOT_IMPLEMENTED));
+#endif
+}
+
+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) {
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce(FileSystemManagerChildShutdown(mFileSystemManagerChild));
+
+ mManager->Shutdown();
+
+ 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) {
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce(FileSystemManagerChildShutdown(mFileSystemManagerChild));
+
+ mManager->Shutdown();
+
+ 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..2a680838a8
--- /dev/null
+++ b/dom/fs/test/gtest/parent/TestFileSystemHashSource.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 "FileSystemHashSource.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(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(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(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(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(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..bfae05a873
--- /dev/null
+++ b/dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp
@@ -0,0 +1,534 @@
+/* -*- 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 "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/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,
+ quota::PERSISTENCE_TYPE_DEFAULT};
+}
+
+class TestFileSystemQuotaClient
+ : public quota::test::QuotaManagerDependencyFixture {
+ public:
+ // ExceedsPreallocation value may depend on platform and sqlite version!
+ static const int sExceedsPreallocation = 32 * 1024;
+
+ protected:
+ void SetUp() override { ASSERT_NO_FATAL_FAILURE(InitializeFixture()); }
+
+ void TearDown() override {
+ PerformOnIOThread([]() {
+ // We use QM_TRY here to avoid failures if this cleanup is unnecessary
+ const auto& originMeta = GetTestQuotaOriginMetadata();
+
+ QM_TRY_INSPECT(const nsCOMPtr<nsIFile>& dbFile,
+ data::GetDatabaseFile(originMeta.mOrigin), QM_VOID);
+ ASSERT_TRUE(dbFile);
+
+ bool exists = false;
+ QM_TRY(MOZ_TO_RESULT(dbFile->Exists(&exists)), QM_VOID);
+ QM_TRY(OkIf(exists), QM_VOID);
+
+ int64_t dbSize = 0;
+ QM_TRY(MOZ_TO_RESULT(dbFile->GetFileSize(&dbSize)), QM_VOID);
+
+ quota::QuotaManager* qm = quota::QuotaManager::Get();
+ QM_TRY(OkIf(qm), QM_VOID);
+ qm->DecreaseUsageForClient(
+ quota::ClientMetadata{originMeta, quota::Client::FILESYSTEM}, dbSize);
+
+ QM_WARNONLY_TRY(MOZ_TO_RESULT(dbFile->Remove(/* recursive */ false)));
+
+ exists = true;
+ QM_TRY(MOZ_TO_RESULT(dbFile->Exists(&exists)), QM_VOID);
+ ASSERT_FALSE(exists);
+ });
+
+ ASSERT_NO_FATAL_FAILURE(
+ ClearStoragesForOrigin(GetTestQuotaOriginMetadata()));
+ ASSERT_NO_FATAL_FAILURE(ShutdownFixture());
+ }
+
+ static void InitializeStorage() {
+ auto backgroundTask = []() {
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ ASSERT_TRUE(quotaManager);
+
+ quotaManager->IOThread()->Dispatch(
+ NS_NewRunnableFunction(
+ "TestFileSystemQuotaClient",
+ []() {
+ quota::QuotaManager* qm = quota::QuotaManager::Get();
+ ASSERT_TRUE(qm);
+
+ ASSERT_NSEQ(NS_OK, qm->EnsureStorageIsInitialized());
+
+ ASSERT_NSEQ(NS_OK, qm->EnsureTemporaryStorageIsInitialized());
+
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+
+ auto dirInfoRes = qm->EnsureTemporaryOriginIsInitialized(
+ quota::PERSISTENCE_TYPE_DEFAULT, testOriginMeta);
+ if (dirInfoRes.isErr()) {
+ ASSERT_NSEQ(NS_OK, dirInfoRes.unwrapErr());
+ }
+
+ qm->EnsureQuotaForOrigin(testOriginMeta);
+ }),
+ NS_DISPATCH_SYNC);
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+ }
+
+ 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& aFileId) {
+ // 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(aFileId, aDatabaseManager->GetOrCreateFile(
+ aFileSlot, /* create */ true));
+ }
+
+ static void WriteDataToFile(
+ data::FileSystemDatabaseManager* const aDatabaseManager,
+ const EntryId& aFileId, const nsCString& aData) {
+ nsString type;
+ TimeStamp lastModMilliS = 0;
+ Path path;
+ nsCOMPtr<nsIFile> fileObj;
+
+ ASSERT_NSEQ(NS_OK, aDatabaseManager->GetFile(aFileId, 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 + BytesOfName(GetTestFileName());
+
+ 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();
+ const auto expectedTotalUsage = testFileDbUsage + testData.Length();
+
+ 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(testFileId));
+
+ // 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);
+
+ // Storage initialization
+ ASSERT_NO_FATAL_FAILURE(InitializeStorage());
+
+ // 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,
+ DISABLED_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);
+
+ // Storage initialization
+ ASSERT_NO_FATAL_FAILURE(InitializeStorage());
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ PerformOnIOThread(std::move(fileCreation),
+ rdm->MutableDatabaseManagerPtr());
+
+ // This should force a rescan
+ ASSERT_TRUE(rdm->LockExclusive(*testFileId));
+ 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(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);
+
+ // Storage initialization
+ ASSERT_NO_FATAL_FAILURE(InitializeStorage());
+
+ // 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..3082508540
--- /dev/null
+++ b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "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() { 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/TestFileSystemDataManagerVersion001.cpp b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersion001.cpp
new file mode 100644
index 0000000000..ba0f120422
--- /dev/null
+++ b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersion001.cpp
@@ -0,0 +1,845 @@
+/* -*- 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 "ErrorList.h"
+#include "FileSystemDataManager.h"
+#include "FileSystemDatabaseManagerVersion001.h"
+#include "FileSystemFileManager.h"
+#include "FileSystemHashSource.h"
+#include "ResultStatement.h"
+#include "SchemaVersion001.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::FileSystemFileManager;
+
+// 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, std::move(aIOTarget),
+ std::move(aIOTaskQueue)) {}
+
+ virtual ~MockFileSystemDataManager() {
+ // Need to avoid assertions
+ mState = State::Closed;
+ }
+};
+
+static void MakeDatabaseManagerVersion001(
+ 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);
+
+ const Origin& testOrigin = GetTestOrigin();
+
+ TEST_TRY_UNWRAP(
+ DatabaseVersion version,
+ SchemaVersion001::InitializeConnection(connection, testOrigin));
+ ASSERT_EQ(1, version);
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ auto fmRes =
+ FileSystemFileManager::CreateFileSystemFileManager(GetTestOrigin());
+ ASSERT_FALSE(fmRes.isErr());
+
+ 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 = GetTestOriginMetadata();
+
+ nsCString taskQueueName("OPFS "_ns + originMetadata.mOrigin);
+
+ RefPtr<TaskQueue> ioTaskQueue =
+ TaskQueue::Create(do_AddRef(streamTransportService), taskQueueName.get());
+
+ aDataManager = MakeRefPtr<MockFileSystemDataManager>(
+ originMetadata, WrapMovingNotNull(streamTransportService),
+ WrapMovingNotNull(ioTaskQueue));
+
+ aDatabaseManager = new FileSystemDatabaseManagerVersion001(
+ aDataManager, std::move(connection),
+ MakeUnique<FileSystemFileManager>(fmRes.unwrap()), rootId);
+}
+
+class TestFileSystemDatabaseManagerVersion001
+ : public quota::test::QuotaManagerDependencyFixture {
+ void SetUp() override { ASSERT_NO_FATAL_FAILURE(InitializeFixture()); }
+
+ void TearDown() override {
+ ASSERT_NO_FATAL_FAILURE(ClearStoragesForOrigin(GetTestOriginMetadata()));
+ ASSERT_NO_FATAL_FAILURE(ShutdownFixture());
+ }
+};
+
+TEST_F(TestFileSystemDatabaseManagerVersion001,
+ smokeTestCreateRemoveDirectories) {
+ auto ioTask = []() {
+ nsresult rv = NS_OK;
+ // Ensure that FileSystemDataManager lives for the lifetime of the test
+ RefPtr<MockFileSystemDataManager> dataManager;
+ FileSystemDatabaseManagerVersion001* rdm = nullptr;
+ ASSERT_NO_FATAL_FAILURE(MakeDatabaseManagerVersion001(dataManager, rdm));
+ UniquePtr<FileSystemDatabaseManagerVersion001> dm(rdm);
+ // if any of these exit early, we have to close
+ auto autoClose = MakeScopeExit([rdm] { rdm->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_F(TestFileSystemDatabaseManagerVersion001, smokeTestCreateRemoveFiles) {
+ auto ioTask = []() {
+ nsresult rv = NS_OK;
+ // Ensure that FileSystemDataManager lives for the lifetime of the test
+ RefPtr<MockFileSystemDataManager> datamanager;
+ FileSystemDatabaseManagerVersion001* rdm = nullptr;
+ ASSERT_NO_FATAL_FAILURE(MakeDatabaseManagerVersion001(datamanager, rdm));
+ UniquePtr<FileSystemDatabaseManagerVersion001> dm(rdm);
+
+ 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_STREQ(firstChild.get(), firstChildClone.get());
+
+ // 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_STREQ(firstChild.get(), firstItemRef.entryId().get());
+
+ nsString type;
+ TimeStamp lastModifiedMilliSeconds;
+ Path path;
+ nsCOMPtr<nsIFile> file;
+ rv = dm->GetFile(firstItemRef.entryId(), type, lastModifiedMilliSeconds,
+ path, file);
+ ASSERT_NSEQ(NS_OK, rv);
+
+ ASSERT_TRUE(type.IsEmpty());
+
+ 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_F(TestFileSystemDatabaseManagerVersion001,
+ smokeTestCreateMoveDirectories) {
+ auto ioTask = []() {
+ // Ensure that FileSystemDataManager lives for the lifetime of the test
+ RefPtr<MockFileSystemDataManager> datamanager;
+ FileSystemDatabaseManagerVersion001* rdm = nullptr;
+ ASSERT_NO_FATAL_FAILURE(MakeDatabaseManagerVersion001(datamanager, rdm));
+ UniquePtr<FileSystemDatabaseManagerVersion001> dm(rdm);
+ 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(bool moved, dm->MoveEntry(src, dest));
+ ASSERT_TRUE(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(bool moved, dm->MoveEntry(src, dest));
+ ASSERT_TRUE(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(bool isMoved, dm->MoveEntry(src, dest));
+ ASSERT_TRUE(isMoved);
+ }
+
+ {
+ // Try to rename file to a directory
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ const FileSystemChildMetadata& dest = firstChildDescendantMeta;
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ {
+ // 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);
+ }
+
+ {
+ // Try to rename directory to quietly overwrite a file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ const FileSystemChildMetadata& dest = testFileMeta;
+ TEST_TRY_UNWRAP(bool isMoved, dm->MoveEntry(src, dest));
+ ASSERT_TRUE(isMoved);
+ }
+
+ {
+ // Move directory back and recreate the file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ testFileMeta.childName(),
+ /* is directory */ true};
+
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ TEST_TRY_UNWRAP(bool isMoved, dm->MoveEntry(src, dest));
+ ASSERT_TRUE(isMoved);
+
+ TEST_TRY_UNWRAP(EntryId testFileCheck,
+ dm->GetOrCreateFile(testFileMeta, /* create */ true));
+ ASSERT_STREQ(testFile, testFileCheck);
+ }
+
+ {
+ // Move file one level up
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId, src.entryName()};
+ TEST_TRY_UNWRAP(bool isMoved, dm->MoveEntry(src, dest));
+ ASSERT_TRUE(isMoved);
+ }
+
+ {
+ // 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.files().Length());
+ ASSERT_STREQ(testFileMeta.childName(), contents.files()[0].entryName());
+ }
+
+ {
+ TEST_TRY_UNWRAP(Path entryPath,
+ dm->Resolve({rootId, subSubFile.entryId()}));
+ ASSERT_EQ(1u, entryPath.Length());
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[0]);
+ }
+
+ {
+ // 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
+ // subSubDirectory
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ // Rename file first and then try to move it to collide with subSubDirectory
+ {
+ // Rename
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(bool isMoved, dm->MoveEntry(src, dest));
+ ASSERT_TRUE(isMoved);
+ }
+
+ {
+ // Try to move one level down
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ {
+ // 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(bool isMoved, dm->MoveEntry(src, dest));
+ ASSERT_TRUE(isMoved);
+ }
+
+ {
+ // 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(bool isMoved, dm->MoveEntry(src, dest));
+ ASSERT_TRUE(isMoved);
+
+ TEST_TRY_UNWRAP(
+ EntryId testFileCheck,
+ dm->GetOrCreateFile({rootId, firstChildDescendantMeta.childName()},
+ /* create */ true));
+ ASSERT_NE(testFile, testFileCheck);
+ testFile = testFileCheck;
+ }
+
+ // Create a new file in the subsubdirectory
+ FileSystemChildMetadata newFileMeta{firstChildDescendant,
+ testFileMeta.childName()};
+ 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(bool isMoved, dm->MoveEntry(src, dest));
+ ASSERT_TRUE(isMoved);
+ }
+
+ {
+ // 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
+ TEST_TRY_UNWRAP(EntryId handle, dm->GetOrCreateFile(newFileMeta,
+ /* create */ false));
+ ASSERT_STREQ(handle, newFile);
+
+ TEST_TRY_UNWRAP(
+ handle, dm->GetOrCreateDirectory({rootId, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_STREQ(handle, firstChildDescendant);
+ }
+
+ {
+ // Check that new file path is as expected
+ 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]);
+ }
+
+ {
+ // Try to overwrite subDirectory with subSubDirectory with rename
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{rootId, firstChildMeta.childName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ // 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(bool isMoved, dm->MoveEntry(src, dest));
+ ASSERT_TRUE(isMoved);
+ }
+
+ {
+ // Then move the directory
+ FileSystemEntryMetadata src{firstChildDescendant,
+ testFileMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir, testFileMeta.childName()};
+
+ // Flag is ignored
+ TEST_TRY_UNWRAP(bool isMoved, dm->MoveEntry(src, dest));
+ ASSERT_TRUE(isMoved);
+ }
+
+ // 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, subSubDir.entryId()}));
+ 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(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));
+}
+
+} // 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..a1a75c7b65
--- /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",
+ "TestFileSystemDataManagerVersion001.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..98221f6495
--- /dev/null
+++ b/dom/fs/test/mochitest/head.js
@@ -0,0 +1,78 @@
+/**
+ * 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.js"
+ );
+
+ const base = window.location.href;
+
+ const depth = "../../../../";
+
+ const { Assert } = await import("/tests/dom/quota/test/modules/Assert.js");
+
+ const proto = {
+ Assert,
+ Cr: SpecialPowers.Cr,
+ navigator,
+ TextEncoder,
+ };
+
+ 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.js"
+ );
+
+ 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.js"
+ );
+
+ const optionalPrefsToSet = [
+ ["dom.fs.enabled", true],
+ ["dom.fs.writable_file_stream.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.ini b/dom/fs/test/mochitest/mochitest.ini
new file mode 100644
index 0000000000..c9fe53dcd4
--- /dev/null
+++ b/dom/fs/test/mochitest/mochitest.ini
@@ -0,0 +1,25 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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
+[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
+skip-if = true # This should be enabled once bug 1798513 is fixed.
+[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..cdada431aa
--- /dev/null
+++ b/dom/fs/test/mochitest/test_basics.html
@@ -0,0 +1,32 @@
+<!--
+ 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">
+ // XXX This should be done in a task, but SimpleTest currently doesn't
+ // support adding new tasks during a task execution.
+ 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);
+ });
+ }
+
+ init();
+ </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..235cd3c6de
--- /dev/null
+++ b/dom/fs/test/mochitest/test_fileSystemDirectoryHandle.html
@@ -0,0 +1,32 @@
+<!--
+ 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">
+ // XXX This should be done in a task, but SimpleTest currently doesn't
+ // support adding new tasks during a task execution.
+ 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);
+ });
+ }
+
+ init();
+ </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..b2fe6a9382
--- /dev/null
+++ b/dom/fs/test/mochitest/test_writableFileStream.html
@@ -0,0 +1,32 @@
+<!--
+ 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">
+ // XXX This should be done in a task, but SimpleTest currently doesn't
+ // support adding new tasks during a task execution.
+ 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);
+ });
+ }
+
+ init();
+ </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..5c8e25113f
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/head.js
@@ -0,0 +1,20 @@
+/**
+ * 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");
+
+ require_module.moduleLoader = new globalThis.ModuleLoader(base, depth);
+ }
+
+ return require_module.moduleLoader.require(id);
+}
diff --git a/dom/fs/test/mochitest/worker/mochitest.ini b/dom/fs/test/mochitest/worker/mochitest.ini
new file mode 100644
index 0000000000..9b5b2cbd98
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/mochitest.ini
@@ -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/.
+
+[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..e6c6a96143
--- /dev/null
+++ b/dom/fs/test/mochitest/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/mochitest/worker/test_writableFileStream_worker.js b/dom/fs/test/mochitest/worker/test_writableFileStream_worker.js
new file mode 100644
index 0000000000..1e9bb12ae8
--- /dev/null
+++ b/dom/fs/test/mochitest/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/moz.build b/dom/fs/test/moz.build
new file mode 100644
index 0000000000..640712c670
--- /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.ini",
+ "mochitest/worker/mochitest.ini",
+]
diff --git a/dom/fs/test/xpcshell/head.js b/dom/fs/test/xpcshell/head.js
new file mode 100644
index 0000000000..03dc5cdf01
--- /dev/null
+++ b/dom/fs/test/xpcshell/head.js
@@ -0,0 +1,87 @@
+/**
+ * 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 proto = {
+ Assert,
+ Cr,
+ navigator: {
+ storage,
+ },
+ TextEncoder,
+ };
+
+ 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,
+ } = 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);
+
+ 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..108d89b0a9
--- /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.ini",
+]
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..5c79ae3ff3
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/head.js
@@ -0,0 +1,20 @@
+/**
+ * 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");
+
+ 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.ini b/dom/fs/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..c08974bf54
--- /dev/null
+++ b/dom/fs/test/xpcshell/xpcshell.ini
@@ -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]
+head = head.js
+
+[test_basics.js]
+[test_basics_worker.js]
+[test_fileSystemDirectoryHandle.js]
+[test_fileSystemDirectoryHandle_worker.js]
+[test_syncAccessHandle_worker.js]
+[test_writableFileStream.js]
+skip-if = true # This should be enabled once bug 1798513 is fixed.
+[test_writableFileStream_worker.js]