diff options
Diffstat (limited to 'dom/fs')
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(¤t))); + 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(¤tVersion))); + } + + 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(¤tVersion))); + + 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] |