summaryrefslogtreecommitdiffstats
path: root/dom/file
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/file/BaseBlobImpl.cpp75
-rw-r--r--dom/file/BaseBlobImpl.h158
-rw-r--r--dom/file/Blob.cpp338
-rw-r--r--dom/file/Blob.h161
-rw-r--r--dom/file/BlobImpl.cpp99
-rw-r--r--dom/file/BlobImpl.h114
-rw-r--r--dom/file/BlobSet.cpp83
-rw-r--r--dom/file/BlobSet.h36
-rw-r--r--dom/file/EmptyBlobImpl.cpp34
-rw-r--r--dom/file/EmptyBlobImpl.h41
-rw-r--r--dom/file/File.cpp202
-rw-r--r--dom/file/File.h103
-rw-r--r--dom/file/FileBlobImpl.cpp294
-rw-r--r--dom/file/FileBlobImpl.h157
-rw-r--r--dom/file/FileCreatorHelper.cpp67
-rw-r--r--dom/file/FileCreatorHelper.h43
-rw-r--r--dom/file/FileList.cpp94
-rw-r--r--dom/file/FileList.h68
-rw-r--r--dom/file/FileReader.cpp803
-rw-r--r--dom/file/FileReader.h207
-rw-r--r--dom/file/FileReaderSync.cpp489
-rw-r--r--dom/file/FileReaderSync.h60
-rw-r--r--dom/file/MemoryBlobImpl.cpp168
-rw-r--r--dom/file/MemoryBlobImpl.h160
-rw-r--r--dom/file/MultipartBlobImpl.cpp338
-rw-r--r--dom/file/MultipartBlobImpl.h106
-rw-r--r--dom/file/MutableBlobStorage.cpp667
-rw-r--r--dom/file/MutableBlobStorage.h136
-rw-r--r--dom/file/MutableBlobStreamListener.cpp102
-rw-r--r--dom/file/MutableBlobStreamListener.h50
-rw-r--r--dom/file/StreamBlobImpl.cpp224
-rw-r--r--dom/file/StreamBlobImpl.h95
-rw-r--r--dom/file/StringBlobImpl.cpp51
-rw-r--r--dom/file/StringBlobImpl.h53
-rw-r--r--dom/file/TemporaryFileBlobImpl.cpp126
-rw-r--r--dom/file/TemporaryFileBlobImpl.h47
-rw-r--r--dom/file/ipc/FileCreatorChild.cpp59
-rw-r--r--dom/file/ipc/FileCreatorChild.h33
-rw-r--r--dom/file/ipc/FileCreatorParent.cpp135
-rw-r--r--dom/file/ipc/FileCreatorParent.h45
-rw-r--r--dom/file/ipc/IPCBlob.ipdlh56
-rw-r--r--dom/file/ipc/IPCBlobUtils.cpp179
-rw-r--r--dom/file/ipc/IPCBlobUtils.h268
-rw-r--r--dom/file/ipc/PFileCreator.ipdl38
-rw-r--r--dom/file/ipc/PRemoteLazyInputStream.ipdl21
-rw-r--r--dom/file/ipc/PTemporaryIPCBlob.ipdl40
-rw-r--r--dom/file/ipc/RemoteLazyInputStream.cpp1458
-rw-r--r--dom/file/ipc/RemoteLazyInputStream.h155
-rw-r--r--dom/file/ipc/RemoteLazyInputStreamChild.cpp55
-rw-r--r--dom/file/ipc/RemoteLazyInputStreamChild.h41
-rw-r--r--dom/file/ipc/RemoteLazyInputStreamParent.cpp123
-rw-r--r--dom/file/ipc/RemoteLazyInputStreamParent.h44
-rw-r--r--dom/file/ipc/RemoteLazyInputStreamStorage.cpp243
-rw-r--r--dom/file/ipc/RemoteLazyInputStreamStorage.h78
-rw-r--r--dom/file/ipc/RemoteLazyInputStreamThread.cpp237
-rw-r--r--dom/file/ipc/RemoteLazyInputStreamThread.h52
-rw-r--r--dom/file/ipc/TemporaryIPCBlobChild.cpp86
-rw-r--r--dom/file/ipc/TemporaryIPCBlobChild.h53
-rw-r--r--dom/file/ipc/TemporaryIPCBlobParent.cpp102
-rw-r--r--dom/file/ipc/TemporaryIPCBlobParent.h43
-rw-r--r--dom/file/ipc/moz.build69
-rw-r--r--dom/file/ipc/mozIRemoteLazyInputStream.idl29
-rw-r--r--dom/file/ipc/tests/browser.ini7
-rw-r--r--dom/file/ipc/tests/browser_ipcBlob.js253
-rw-r--r--dom/file/ipc/tests/browser_ipcBlob_temporary.js115
-rw-r--r--dom/file/ipc/tests/empty.html0
-rw-r--r--dom/file/ipc/tests/green.jpgbin0 -> 361 bytes
-rw-r--r--dom/file/ipc/tests/mochitest.ini12
-rw-r--r--dom/file/ipc/tests/ok.sjs11
-rw-r--r--dom/file/ipc/tests/script_file.js53
-rw-r--r--dom/file/ipc/tests/temporary.sjs6
-rw-r--r--dom/file/ipc/tests/test_ipcBlob_createImageBitmap.html84
-rw-r--r--dom/file/ipc/tests/test_ipcBlob_emptyMultiplex.html45
-rw-r--r--dom/file/ipc/tests/test_ipcBlob_fileReaderSync.html100
-rw-r--r--dom/file/ipc/tests/test_ipcBlob_mixedMultiplex.html41
-rw-r--r--dom/file/ipc/tests/test_ipcBlob_workers.html121
-rw-r--r--dom/file/moz.build62
-rw-r--r--dom/file/tests/common_blob.js395
-rw-r--r--dom/file/tests/common_blob_reading.js50
-rw-r--r--dom/file/tests/common_blob_types.js82
-rw-r--r--dom/file/tests/common_fileReader.js848
-rw-r--r--dom/file/tests/crashtests/1480354.html14
-rw-r--r--dom/file/tests/crashtests/1562891.html16
-rw-r--r--dom/file/tests/crashtests/1747185.html11
-rw-r--r--dom/file/tests/crashtests/1748342.html26
-rw-r--r--dom/file/tests/crashtests/crashtests.list4
-rw-r--r--dom/file/tests/create_file_objects.js19
-rw-r--r--dom/file/tests/file_blobURL_expiring.html4
-rw-r--r--dom/file/tests/file_mozfiledataurl_audio.oggbin0 -> 135861 bytes
-rw-r--r--dom/file/tests/file_mozfiledataurl_doc.html6
-rw-r--r--dom/file/tests/file_mozfiledataurl_img.jpgbin0 -> 2711 bytes
-rw-r--r--dom/file/tests/file_mozfiledataurl_inner.html76
-rw-r--r--dom/file/tests/file_mozfiledataurl_text.txt1
-rw-r--r--dom/file/tests/file_nonascii_blob_url.html24
-rw-r--r--dom/file/tests/fileapi_chromeScript.js54
-rw-r--r--dom/file/tests/mochitest.ini54
-rw-r--r--dom/file/tests/test_agentcluster_bloburl.js170
-rw-r--r--dom/file/tests/test_blobURL_expiring.html48
-rw-r--r--dom/file/tests/test_blob_fragment_and_query.html62
-rw-r--r--dom/file/tests/test_blob_reading.html34
-rw-r--r--dom/file/tests/test_blobconstructor.html246
-rw-r--r--dom/file/tests/test_bloburi.js24
-rw-r--r--dom/file/tests/test_bug1507893.html63
-rw-r--r--dom/file/tests/test_bug1742540.html83
-rw-r--r--dom/file/tests/test_createFile.js52
-rw-r--r--dom/file/tests/test_file_from_blob.html110
-rw-r--r--dom/file/tests/test_file_negative_date.html29
-rw-r--r--dom/file/tests/test_fileapi_basic.html24
-rw-r--r--dom/file/tests/test_fileapi_basic_worker.html38
-rw-r--r--dom/file/tests/test_fileapi_encoding.html24
-rw-r--r--dom/file/tests/test_fileapi_encoding_worker.html38
-rw-r--r--dom/file/tests/test_fileapi_other.html24
-rw-r--r--dom/file/tests/test_fileapi_other_worker.html38
-rw-r--r--dom/file/tests/test_fileapi_slice_image.html139
-rw-r--r--dom/file/tests/test_fileapi_slice_memFile_1.html37
-rw-r--r--dom/file/tests/test_fileapi_slice_memFile_2.html37
-rw-r--r--dom/file/tests/test_fileapi_slice_realFile_1.html37
-rw-r--r--dom/file/tests/test_fileapi_slice_realFile_2.html37
-rw-r--r--dom/file/tests/test_fileapi_twice.html24
-rw-r--r--dom/file/tests/test_fileapi_twice_worker.html38
-rw-r--r--dom/file/tests/test_ipc_messagemanager_blob.js102
-rw-r--r--dom/file/tests/test_mozfiledataurl.html224
-rw-r--r--dom/file/tests/test_nonascii_blob_url.html28
-rw-r--r--dom/file/tests/worker_blob_reading.js26
-rw-r--r--dom/file/tests/worker_bug1507893.js5
-rw-r--r--dom/file/tests/worker_bug1742540.js5
-rw-r--r--dom/file/tests/worker_fileReader.js30
-rw-r--r--dom/file/tests/xpcshell.ini8
-rw-r--r--dom/file/uri/BlobURL.cpp171
-rw-r--r--dom/file/uri/BlobURL.h123
-rw-r--r--dom/file/uri/BlobURLChannel.cpp86
-rw-r--r--dom/file/uri/BlobURLChannel.h37
-rw-r--r--dom/file/uri/BlobURLInputStream.cpp589
-rw-r--r--dom/file/uri/BlobURLInputStream.h81
-rw-r--r--dom/file/uri/BlobURLProtocolHandler.cpp990
-rw-r--r--dom/file/uri/BlobURLProtocolHandler.h140
-rw-r--r--dom/file/uri/components.conf24
-rw-r--r--dom/file/uri/moz.build34
-rw-r--r--dom/filesystem/Directory.cpp198
-rw-r--r--dom/filesystem/Directory.h110
-rw-r--r--dom/filesystem/FileSystemBase.cpp143
-rw-r--r--dom/filesystem/FileSystemBase.h80
-rw-r--r--dom/filesystem/FileSystemRequestParent.cpp188
-rw-r--r--dom/filesystem/FileSystemRequestParent.h43
-rw-r--r--dom/filesystem/FileSystemSecurity.cpp106
-rw-r--r--dom/filesystem/FileSystemSecurity.h40
-rw-r--r--dom/filesystem/FileSystemTaskBase.cpp246
-rw-r--r--dom/filesystem/FileSystemTaskBase.h254
-rw-r--r--dom/filesystem/FileSystemUtils.cpp84
-rw-r--r--dom/filesystem/FileSystemUtils.h51
-rw-r--r--dom/filesystem/GetDirectoryListingTask.cpp371
-rw-r--r--dom/filesystem/GetDirectoryListingTask.h91
-rw-r--r--dom/filesystem/GetFileOrDirectoryTask.cpp270
-rw-r--r--dom/filesystem/GetFileOrDirectoryTask.h79
-rw-r--r--dom/filesystem/GetFilesHelper.cpp506
-rw-r--r--dom/filesystem/GetFilesHelper.h161
-rw-r--r--dom/filesystem/GetFilesTask.cpp246
-rw-r--r--dom/filesystem/GetFilesTask.h83
-rw-r--r--dom/filesystem/OSFileSystem.cpp88
-rw-r--r--dom/filesystem/OSFileSystem.h98
-rw-r--r--dom/filesystem/PFileSystemParams.ipdlh48
-rw-r--r--dom/filesystem/PFileSystemRequest.ipdl77
-rw-r--r--dom/filesystem/compat/CallbackRunnables.cpp280
-rw-r--r--dom/filesystem/compat/CallbackRunnables.h118
-rw-r--r--dom/filesystem/compat/FileSystem.cpp60
-rw-r--r--dom/filesystem/compat/FileSystem.h52
-rw-r--r--dom/filesystem/compat/FileSystemDirectoryEntry.cpp92
-rw-r--r--dom/filesystem/compat/FileSystemDirectoryEntry.h73
-rw-r--r--dom/filesystem/compat/FileSystemDirectoryReader.cpp181
-rw-r--r--dom/filesystem/compat/FileSystemDirectoryReader.h60
-rw-r--r--dom/filesystem/compat/FileSystemEntry.cpp80
-rw-r--r--dom/filesystem/compat/FileSystemEntry.h67
-rw-r--r--dom/filesystem/compat/FileSystemFileEntry.cpp94
-rw-r--r--dom/filesystem/compat/FileSystemFileEntry.h49
-rw-r--r--dom/filesystem/compat/FileSystemRootDirectoryEntry.cpp138
-rw-r--r--dom/filesystem/compat/FileSystemRootDirectoryEntry.h48
-rw-r--r--dom/filesystem/compat/FileSystemRootDirectoryReader.cpp93
-rw-r--r--dom/filesystem/compat/FileSystemRootDirectoryReader.h38
-rw-r--r--dom/filesystem/compat/moz.build30
-rw-r--r--dom/filesystem/compat/tests/mochitest.ini8
-rw-r--r--dom/filesystem/compat/tests/moz.build7
-rw-r--r--dom/filesystem/compat/tests/script_entries.js47
-rw-r--r--dom/filesystem/compat/tests/test_basic.html549
-rw-r--r--dom/filesystem/compat/tests/test_formSubmission.html271
-rw-r--r--dom/filesystem/compat/tests/test_no_dnd.html84
-rw-r--r--dom/filesystem/moz.build50
-rw-r--r--dom/filesystem/tests/filesystem_commons.js180
-rw-r--r--dom/filesystem/tests/mochitest.ini12
-rw-r--r--dom/filesystem/tests/moz.build7
-rw-r--r--dom/filesystem/tests/script_fileList.js176
-rw-r--r--dom/filesystem/tests/script_promptHandler.js18
-rw-r--r--dom/filesystem/tests/test_basic.html118
-rw-r--r--dom/filesystem/tests/test_bug1319088.html65
-rw-r--r--dom/filesystem/tests/test_webkitdirectory.html309
-rw-r--r--dom/filesystem/tests/test_worker_basic.html73
-rw-r--r--dom/filesystem/tests/worker_basic.js50
196 files changed, 23782 insertions, 0 deletions
diff --git a/dom/file/BaseBlobImpl.cpp b/dom/file/BaseBlobImpl.cpp
new file mode 100644
index 0000000000..5871b9da04
--- /dev/null
+++ b/dom/file/BaseBlobImpl.cpp
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/BaseBlobImpl.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsRFPService.h"
+#include "prtime.h"
+
+namespace mozilla::dom {
+
+void BaseBlobImpl::GetName(nsAString& aName) const {
+ MOZ_ASSERT(mIsFile, "Should only be called on files");
+ aName = mName;
+}
+
+void BaseBlobImpl::GetDOMPath(nsAString& aPath) const {
+ MOZ_ASSERT(mIsFile, "Should only be called on files");
+ aPath = mPath;
+}
+
+void BaseBlobImpl::SetDOMPath(const nsAString& aPath) {
+ MOZ_ASSERT(mIsFile, "Should only be called on files");
+ mPath = aPath;
+}
+
+void BaseBlobImpl::GetMozFullPath(nsAString& aFileName,
+ SystemCallerGuarantee /* unused */,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(mIsFile, "Should only be called on files");
+
+ GetMozFullPathInternal(aFileName, aRv);
+}
+
+void BaseBlobImpl::GetMozFullPathInternal(nsAString& aFileName,
+ ErrorResult& aRv) {
+ if (!mIsFile) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ aFileName.Truncate();
+}
+
+void BaseBlobImpl::GetType(nsAString& aType) { aType = mContentType; }
+
+int64_t BaseBlobImpl::GetLastModified(ErrorResult& aRv) {
+ MOZ_ASSERT(mIsFile, "Should only be called on files");
+ return mLastModificationDate / PR_USEC_PER_MSEC;
+}
+
+int64_t BaseBlobImpl::GetFileId() const { return -1; }
+
+/* static */
+uint64_t BaseBlobImpl::NextSerialNumber() {
+ static Atomic<uint64_t> nextSerialNumber;
+ return nextSerialNumber++;
+}
+
+void BaseBlobImpl::SetLastModificationDatePrecisely(int64_t aDate) {
+ MOZ_ASSERT(mIsFile, "Should only be called on files");
+ mLastModificationDate = aDate;
+}
+
+void BaseBlobImpl::SetLastModificationDate(RTPCallerType aRTPCallerType,
+ int64_t aDate) {
+ return SetLastModificationDatePrecisely(
+ nsRFPService::ReduceTimePrecisionAsUSecs(aDate, 0, aRTPCallerType));
+ // mLastModificationDate is an absolute timestamp so we supply a zero
+ // context mix-in
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/BaseBlobImpl.h b/dom/file/BaseBlobImpl.h
new file mode 100644
index 0000000000..7265fc2104
--- /dev/null
+++ b/dom/file/BaseBlobImpl.h
@@ -0,0 +1,158 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_BaseBlobImpl_h
+#define mozilla_dom_BaseBlobImpl_h
+
+#include "nsIGlobalObject.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/ErrorResult.h"
+
+namespace mozilla::dom {
+
+class FileBlobImpl;
+
+class BaseBlobImpl : public BlobImpl {
+ friend class FileBlobImpl;
+
+ public:
+ // File constructor.
+ BaseBlobImpl(const nsAString& aName, const nsAString& aContentType,
+ uint64_t aLength, int64_t aLastModifiedDate)
+ : mIsFile(true),
+ mContentType(aContentType),
+ mName(aName),
+ mStart(0),
+ mLength(aLength),
+ mSerialNumber(NextSerialNumber()),
+ mLastModificationDate(aLastModifiedDate) {
+ // Ensure non-null mContentType by default
+ mContentType.SetIsVoid(false);
+ }
+
+ // Blob constructor without starting point.
+ BaseBlobImpl(const nsAString& aContentType, uint64_t aLength)
+ : mIsFile(false),
+ mContentType(aContentType),
+ mStart(0),
+ mLength(aLength),
+ mSerialNumber(NextSerialNumber()),
+ mLastModificationDate(0) {
+ // Ensure non-null mContentType by default
+ mContentType.SetIsVoid(false);
+ }
+
+ // Blob constructor with starting point.
+ BaseBlobImpl(const nsAString& aContentType, uint64_t aStart, uint64_t aLength)
+ : mIsFile(false),
+ mContentType(aContentType),
+ mStart(aStart),
+ mLength(aLength),
+ mSerialNumber(NextSerialNumber()),
+ mLastModificationDate(0) {
+ // Ensure non-null mContentType by default
+ mContentType.SetIsVoid(false);
+ }
+
+ void GetName(nsAString& aName) const override;
+
+ void GetDOMPath(nsAString& aPath) const override;
+
+ void SetDOMPath(const nsAString& aPath) override;
+
+ int64_t GetLastModified(ErrorResult& aRv) override;
+
+ void GetMozFullPath(nsAString& aFileName, SystemCallerGuarantee /* unused */,
+ ErrorResult& aRv) override;
+
+ void GetMozFullPathInternal(nsAString& aFileName, ErrorResult& aRv) override;
+
+ uint64_t GetSize(ErrorResult& aRv) override { return mLength; }
+
+ void GetType(nsAString& aType) override;
+
+ size_t GetAllocationSize() const override { return 0; }
+
+ size_t GetAllocationSize(
+ FallibleTArray<BlobImpl*>& aVisitedBlobImpls) const override {
+ return GetAllocationSize();
+ }
+
+ uint64_t GetSerialNumber() const override { return mSerialNumber; }
+
+ already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength,
+ const nsAString& aContentType,
+ ErrorResult& aRv) const override {
+ return nullptr;
+ }
+
+ const nsTArray<RefPtr<BlobImpl>>* GetSubBlobImpls() const override {
+ return nullptr;
+ }
+
+ void CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const override {
+ aRv.Throw(NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ int64_t GetFileId() const override;
+
+ void SetLazyData(const nsAString& aName, const nsAString& aContentType,
+ uint64_t aLength, int64_t aLastModifiedDate) override {
+ mName = aName;
+ mContentType = aContentType;
+ mLength = aLength;
+ SetLastModificationDatePrecisely(aLastModifiedDate);
+ mIsFile = !aName.IsVoid();
+ }
+
+ bool IsMemoryFile() const override { return false; }
+
+ bool IsFile() const override { return mIsFile; }
+
+ void GetBlobImplType(nsAString& aBlobImplType) const override {
+ aBlobImplType = u"BaseBlobImpl"_ns;
+ }
+
+ protected:
+ ~BaseBlobImpl() override = default;
+
+ /**
+ * Returns a new, effectively-unique serial number. This should be used
+ * by implementations to obtain a serial number for GetSerialNumber().
+ * The implementation is thread safe.
+ */
+ static uint64_t NextSerialNumber();
+
+ void SetLastModificationDate(RTPCallerType aRTPCallerType, int64_t aDate);
+ void SetLastModificationDatePrecisely(int64_t aDate);
+
+#ifdef DEBUG
+ bool IsLastModificationDateUnset() const {
+ return mLastModificationDate == INT64_MAX;
+ }
+#endif
+
+ const nsString mBlobImplType;
+
+ bool mIsFile;
+
+ nsString mContentType;
+ nsString mName;
+ nsString mPath; // The path relative to a directory chosen by the user
+
+ uint64_t mStart;
+ uint64_t mLength;
+
+ const uint64_t mSerialNumber;
+
+ private:
+ int64_t mLastModificationDate;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_BaseBlobImpl_h
diff --git a/dom/file/Blob.cpp b/dom/file/Blob.cpp
new file mode 100644
index 0000000000..dc3a70c74c
--- /dev/null
+++ b/dom/file/Blob.cpp
@@ -0,0 +1,338 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "Blob.h"
+#include "EmptyBlobImpl.h"
+#include "File.h"
+#include "MemoryBlobImpl.h"
+#include "mozilla/dom/BlobBinding.h"
+#include "mozilla/dom/ReadableStream.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "MultipartBlobImpl.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIGlobalObject.h"
+#include "nsIInputStream.h"
+#include "nsPIDOMWindow.h"
+#include "StreamBlobImpl.h"
+#include "StringBlobImpl.h"
+#include "js/GCAPI.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(Blob)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Blob)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Blob)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Blob)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Blob)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+ NS_INTERFACE_MAP_ENTRY_CONCRETE(Blob)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(Blob)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(Blob)
+
+void Blob::MakeValidBlobType(nsAString& aType) {
+ char16_t* iter = aType.BeginWriting();
+ char16_t* end = aType.EndWriting();
+
+ for (; iter != end; ++iter) {
+ char16_t c = *iter;
+ if (c < 0x20 || c > 0x7E) {
+ // Non-ASCII char, bail out.
+ aType.Truncate();
+ return;
+ }
+
+ if (c >= 'A' && c <= 'Z') {
+ *iter = c + ('a' - 'A');
+ }
+ }
+}
+
+/* static */
+Blob* Blob::Create(nsIGlobalObject* aGlobal, BlobImpl* aImpl) {
+ MOZ_ASSERT(aImpl);
+
+ MOZ_ASSERT(aGlobal);
+ if (NS_WARN_IF(!aGlobal)) {
+ return nullptr;
+ }
+
+ return aImpl->IsFile() ? new File(aGlobal, aImpl) : new Blob(aGlobal, aImpl);
+}
+
+/* static */
+already_AddRefed<Blob> Blob::CreateStringBlob(nsIGlobalObject* aGlobal,
+ const nsACString& aData,
+ const nsAString& aContentType) {
+ MOZ_ASSERT(aGlobal);
+ if (NS_WARN_IF(!aGlobal)) {
+ return nullptr;
+ }
+
+ RefPtr<BlobImpl> blobImpl = StringBlobImpl::Create(aData, aContentType);
+ RefPtr<Blob> blob = Blob::Create(aGlobal, blobImpl);
+ MOZ_ASSERT(!blob->mImpl->IsFile());
+ return blob.forget();
+}
+
+/* static */
+already_AddRefed<Blob> Blob::CreateMemoryBlob(nsIGlobalObject* aGlobal,
+ void* aMemoryBuffer,
+ uint64_t aLength,
+ const nsAString& aContentType) {
+ MOZ_ASSERT(aGlobal);
+ if (NS_WARN_IF(!aGlobal)) {
+ return nullptr;
+ }
+
+ RefPtr<Blob> blob = Blob::Create(
+ aGlobal, new MemoryBlobImpl(aMemoryBuffer, aLength, aContentType));
+ MOZ_ASSERT(!blob->mImpl->IsFile());
+ return blob.forget();
+}
+
+Blob::Blob(nsIGlobalObject* aGlobal, BlobImpl* aImpl)
+ : mImpl(aImpl), mGlobal(aGlobal) {
+ MOZ_ASSERT(mImpl);
+ MOZ_ASSERT(mGlobal);
+}
+
+Blob::~Blob() = default;
+
+bool Blob::IsFile() const { return mImpl->IsFile(); }
+
+const nsTArray<RefPtr<BlobImpl>>* Blob::GetSubBlobImpls() const {
+ return mImpl->GetSubBlobImpls();
+}
+
+already_AddRefed<File> Blob::ToFile() {
+ if (!mImpl->IsFile()) {
+ return nullptr;
+ }
+
+ RefPtr<File> file;
+ if (HasFileInterface()) {
+ file = static_cast<File*>(this);
+ } else {
+ file = new File(mGlobal, mImpl);
+ }
+
+ return file.forget();
+}
+
+already_AddRefed<File> Blob::ToFile(const nsAString& aName,
+ ErrorResult& aRv) const {
+ AutoTArray<RefPtr<BlobImpl>, 1> blobImpls({mImpl});
+
+ nsAutoString contentType;
+ mImpl->GetType(contentType);
+
+ RefPtr<MultipartBlobImpl> impl =
+ MultipartBlobImpl::Create(std::move(blobImpls), aName, contentType,
+ mGlobal->GetRTPCallerType(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ RefPtr<File> file = new File(mGlobal, impl);
+ return file.forget();
+}
+
+already_AddRefed<Blob> Blob::CreateSlice(uint64_t aStart, uint64_t aLength,
+ const nsAString& aContentType,
+ ErrorResult& aRv) const {
+ RefPtr<BlobImpl> impl =
+ mImpl->CreateSlice(aStart, aLength, aContentType, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ RefPtr<Blob> blob = Blob::Create(mGlobal, impl);
+ return blob.forget();
+}
+
+uint64_t Blob::GetSize(ErrorResult& aRv) { return mImpl->GetSize(aRv); }
+
+void Blob::GetType(nsAString& aType) { mImpl->GetType(aType); }
+
+void Blob::GetBlobImplType(nsAString& aBlobImplType) {
+ mImpl->GetBlobImplType(aBlobImplType);
+}
+
+already_AddRefed<Blob> Blob::Slice(const Optional<int64_t>& aStart,
+ const Optional<int64_t>& aEnd,
+ const Optional<nsAString>& aContentType,
+ ErrorResult& aRv) {
+ nsAutoString contentType;
+ if (aContentType.WasPassed()) {
+ contentType = aContentType.Value();
+ }
+
+ RefPtr<BlobImpl> impl = mImpl->Slice(aStart, aEnd, contentType, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ RefPtr<Blob> blob = Blob::Create(mGlobal, impl);
+ return blob.forget();
+}
+
+size_t Blob::GetAllocationSize() const { return mImpl->GetAllocationSize(); }
+
+// contentTypeWithCharset can be set to the contentType or
+// contentType+charset based on what the spec says.
+// See: https://fetch.spec.whatwg.org/#concept-bodyinit-extract
+nsresult Blob::GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength,
+ nsACString& aContentType,
+ nsACString& aCharset) const {
+ return mImpl->GetSendInfo(aBody, aContentLength, aContentType, aCharset);
+}
+
+JSObject* Blob::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return Blob_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */
+already_AddRefed<Blob> Blob::Constructor(
+ const GlobalObject& aGlobal, const Optional<Sequence<BlobPart>>& aData,
+ const BlobPropertyBag& aBag, ErrorResult& aRv) {
+ RefPtr<MultipartBlobImpl> impl = new MultipartBlobImpl();
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ MOZ_ASSERT(global);
+ if (aData.WasPassed()) {
+ nsAutoString type(aBag.mType);
+ MakeValidBlobType(type);
+ impl->InitializeBlob(aData.Value(), type,
+ aBag.mEndings == EndingType::Native,
+ global->GetRTPCallerType(), aRv);
+ } else {
+ impl->InitializeBlob(global->GetRTPCallerType(), aRv);
+ }
+
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(!impl->IsFile());
+
+ RefPtr<Blob> blob = Blob::Create(global, impl);
+ return blob.forget();
+}
+
+int64_t Blob::GetFileId() const { return mImpl->GetFileId(); }
+
+bool Blob::IsMemoryFile() const { return mImpl->IsMemoryFile(); }
+
+void Blob::CreateInputStream(nsIInputStream** aStream, ErrorResult& aRv) const {
+ mImpl->CreateInputStream(aStream, aRv);
+}
+
+size_t BindingJSObjectMallocBytes(Blob* aBlob) {
+ MOZ_ASSERT(aBlob);
+
+ // TODO: The hazard analysis currently can't see that none of the
+ // implementations of the GetAllocationSize virtual method call can GC (see
+ // bug 1531951).
+ JS::AutoSuppressGCAnalysis nogc;
+
+ return aBlob->GetAllocationSize();
+}
+
+already_AddRefed<Promise> Blob::Text(ErrorResult& aRv) const {
+ return ConsumeBody(BodyConsumer::CONSUME_TEXT, aRv);
+}
+
+already_AddRefed<Promise> Blob::ArrayBuffer(ErrorResult& aRv) const {
+ return ConsumeBody(BodyConsumer::CONSUME_ARRAYBUFFER, aRv);
+}
+
+already_AddRefed<Promise> Blob::ConsumeBody(
+ BodyConsumer::ConsumeType aConsumeType, ErrorResult& aRv) const {
+ if (NS_WARN_IF(!mGlobal)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsISerialEventTarget> mainThreadEventTarget;
+ if (!NS_IsMainThread()) {
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+ mainThreadEventTarget = workerPrivate->MainThreadEventTarget();
+ } else {
+ mainThreadEventTarget = mGlobal->EventTargetFor(TaskCategory::Other);
+ }
+
+ MOZ_ASSERT(mainThreadEventTarget);
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ CreateInputStream(getter_AddRefs(inputStream), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return BodyConsumer::Create(mGlobal, mainThreadEventTarget, inputStream,
+ nullptr, aConsumeType, VoidCString(),
+ VoidString(), VoidCString(), VoidCString(),
+ MutableBlobStorage::eOnlyInMemory, aRv);
+}
+
+// https://w3c.github.io/FileAPI/#stream-method-algo
+// "The stream() method, when invoked, must return the result of calling get
+// stream on this."
+// And that's https://w3c.github.io/FileAPI/#blob-get-stream.
+already_AddRefed<ReadableStream> Blob::Stream(JSContext* aCx,
+ ErrorResult& aRv) const {
+ nsCOMPtr<nsIInputStream> stream;
+ CreateInputStream(getter_AddRefs(stream), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(!mGlobal)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ auto algorithms =
+ MakeRefPtr<NonAsyncInputToReadableStreamAlgorithms>(*stream);
+
+ // Step 1: Let stream be a new ReadableStream created in blob’s relevant
+ // Realm.
+ // Step 2: Set up stream with byte reading support.
+ // Step 3: ...
+ // (The spec here does not define pullAlgorithm and instead greedily enqueues
+ // everything into the stream when .stream() is called, but here we only reads
+ // the data when actual read request happens, via
+ // InputToReadableStreamAlgorithms. See
+ // https://github.com/w3c/FileAPI/issues/194.)
+ RefPtr<ReadableStream> body = ReadableStream::CreateByteNative(
+ aCx, mGlobal, *algorithms, Nothing(), aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ return body.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/Blob.h b/dom/file/Blob.h
new file mode 100644
index 0000000000..2314df32b3
--- /dev/null
+++ b/dom/file/Blob.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 mozilla_dom_Blob_h
+#define mozilla_dom_Blob_h
+
+#include "mozilla/dom/BodyConsumer.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsWrapperCache.h"
+#include "nsWeakReference.h"
+
+class nsIGlobalObject;
+class nsIInputStream;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+struct BlobPropertyBag;
+class BlobImpl;
+class File;
+class GlobalObject;
+class OwningArrayBufferViewOrArrayBufferOrBlobOrUTF8String;
+class Promise;
+
+class ReadableStream;
+
+#define NS_DOM_BLOB_IID \
+ { \
+ 0x648c2a83, 0xbdb1, 0x4a7d, { \
+ 0xb5, 0x0a, 0xca, 0xcd, 0x92, 0x87, 0x45, 0xc2 \
+ } \
+ }
+
+class Blob : public nsSupportsWeakReference, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS_FINAL
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Blob)
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOM_BLOB_IID)
+
+ using BlobPart = OwningArrayBufferViewOrArrayBufferOrBlobOrUTF8String;
+
+ // This creates a Blob or a File based on the type of BlobImpl.
+ static Blob* Create(nsIGlobalObject* aGlobal, BlobImpl* aImpl);
+
+ static already_AddRefed<Blob> CreateStringBlob(nsIGlobalObject* aGlobal,
+ const nsACString& aData,
+ const nsAString& aContentType);
+
+ // The returned Blob takes ownership of aMemoryBuffer. aMemoryBuffer will be
+ // freed by free so it must be allocated by malloc or something
+ // compatible with it.
+ static already_AddRefed<Blob> CreateMemoryBlob(nsIGlobalObject* aGlobal,
+ void* aMemoryBuffer,
+ uint64_t aLength,
+ const nsAString& aContentType);
+
+ BlobImpl* Impl() const { return mImpl; }
+
+ bool IsFile() const;
+
+ const nsTArray<RefPtr<BlobImpl>>* GetSubBlobImpls() const;
+
+ // This method returns null if this Blob is not a File; it returns
+ // the same object in case this Blob already implements the File interface;
+ // otherwise it returns a new File object with the same BlobImpl.
+ already_AddRefed<File> ToFile();
+
+ // This method creates a new File object with the given name and the same
+ // BlobImpl.
+ already_AddRefed<File> ToFile(const nsAString& aName, ErrorResult& aRv) const;
+
+ already_AddRefed<Blob> CreateSlice(uint64_t aStart, uint64_t aLength,
+ const nsAString& aContentType,
+ ErrorResult& aRv) const;
+
+ void CreateInputStream(nsIInputStream** aStream, ErrorResult& aRv) const;
+
+ int64_t GetFileId() const;
+
+ // A utility function that enforces the spec constraints on the type of a
+ // blob: no codepoints outside the ASCII range (otherwise type becomes empty)
+ // and lowercase ASCII only. We can't just use our existing nsContentUtils
+ // ASCII-related helpers because we need the "outside ASCII range" check, and
+ // we can't use NS_IsAscii because its definition of "ASCII" (chars all <=
+ // 0x7E) differs from the file API definition (which excludes control chars).
+ static void MakeValidBlobType(nsAString& aType);
+
+ // WebIDL methods
+ nsIGlobalObject* GetParentObject() const { return mGlobal; }
+
+ bool IsMemoryFile() const;
+
+ // Blob constructor
+ static already_AddRefed<Blob> Constructor(
+ const GlobalObject& aGlobal, const Optional<Sequence<BlobPart>>& aData,
+ const BlobPropertyBag& aBag, ErrorResult& aRv);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint64_t GetSize(ErrorResult& aRv);
+
+ void GetType(nsAString& aType);
+
+ void GetBlobImplType(nsAString& aBlobImplType);
+
+ already_AddRefed<Blob> Slice(const Optional<int64_t>& aStart,
+ const Optional<int64_t>& aEnd,
+ const Optional<nsAString>& aContentType,
+ ErrorResult& aRv);
+
+ size_t GetAllocationSize() const;
+
+ nsresult GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength,
+ nsACString& aContentType, nsACString& aCharset) const;
+
+ already_AddRefed<ReadableStream> Stream(JSContext* aCx,
+ ErrorResult& aRv) const;
+ already_AddRefed<Promise> Text(ErrorResult& aRv) const;
+ already_AddRefed<Promise> ArrayBuffer(ErrorResult& aRv) const;
+
+ protected:
+ // File constructor should never be used directly. Use Blob::Create instead.
+ Blob(nsIGlobalObject* aGlobal, BlobImpl* aImpl);
+ virtual ~Blob();
+
+ virtual bool HasFileInterface() const { return false; }
+
+ already_AddRefed<Promise> ConsumeBody(BodyConsumer::ConsumeType aConsumeType,
+ ErrorResult& aRv) const;
+
+ // The member is the real backend implementation of this File/Blob.
+ // It's thread-safe and not CC-able and it's the only element that is moved
+ // between threads.
+ // Note: we should not store any other state in this class!
+ RefPtr<BlobImpl> mImpl;
+
+ private:
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(Blob, NS_DOM_BLOB_IID)
+
+// Override BindingJSObjectMallocBytes for blobs to tell the JS GC how much
+// memory is held live by the binding object.
+size_t BindingJSObjectMallocBytes(Blob* aBlob);
+
+} // namespace dom
+} // namespace mozilla
+
+inline nsISupports* ToSupports(mozilla::dom::Blob* aBlob) {
+ return static_cast<nsISupportsWeakReference*>(aBlob);
+}
+
+#endif // mozilla_dom_Blob_h
diff --git a/dom/file/BlobImpl.cpp b/dom/file/BlobImpl.cpp
new file mode 100644
index 0000000000..fa31737c9d
--- /dev/null
+++ b/dom/file/BlobImpl.cpp
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "BlobImpl.h"
+#include "File.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/ErrorResult.h"
+#include "nsIInputStream.h"
+
+namespace mozilla::dom {
+
+// Makes sure that aStart and aEnd is less then or equal to aSize and greater
+// than 0
+static void ParseSize(int64_t aSize, int64_t& aStart, int64_t& aEnd) {
+ CheckedInt64 newStartOffset = aStart;
+ if (aStart < -aSize) {
+ newStartOffset = 0;
+ } else if (aStart < 0) {
+ newStartOffset += aSize;
+ } else if (aStart > aSize) {
+ newStartOffset = aSize;
+ }
+
+ CheckedInt64 newEndOffset = aEnd;
+ if (aEnd < -aSize) {
+ newEndOffset = 0;
+ } else if (aEnd < 0) {
+ newEndOffset += aSize;
+ } else if (aEnd > aSize) {
+ newEndOffset = aSize;
+ }
+
+ if (!newStartOffset.isValid() || !newEndOffset.isValid() ||
+ newStartOffset.value() >= newEndOffset.value()) {
+ aStart = aEnd = 0;
+ } else {
+ aStart = newStartOffset.value();
+ aEnd = newEndOffset.value();
+ }
+}
+
+already_AddRefed<BlobImpl> BlobImpl::Slice(const Optional<int64_t>& aStart,
+ const Optional<int64_t>& aEnd,
+ const nsAString& aContentType,
+ ErrorResult& aRv) {
+ // Truncate aStart and aEnd so that we stay within this file.
+ uint64_t thisLength = GetSize(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ int64_t start = aStart.WasPassed() ? aStart.Value() : 0;
+ int64_t end = aEnd.WasPassed() ? aEnd.Value() : (int64_t)thisLength;
+
+ ParseSize((int64_t)thisLength, start, end);
+
+ nsAutoString type(aContentType);
+ Blob::MakeValidBlobType(type);
+ return CreateSlice((uint64_t)start, (uint64_t)(end - start), type, aRv);
+}
+
+nsresult BlobImpl::GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength,
+ nsACString& aContentType, nsACString& aCharset) {
+ MOZ_ASSERT(aContentLength);
+
+ ErrorResult rv;
+
+ nsCOMPtr<nsIInputStream> stream;
+ CreateInputStream(getter_AddRefs(stream), rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ *aContentLength = GetSize(rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ nsAutoString contentType;
+ GetType(contentType);
+
+ if (contentType.IsEmpty()) {
+ aContentType.SetIsVoid(true);
+ } else {
+ CopyUTF16toUTF8(contentType, aContentType);
+ }
+
+ aCharset.Truncate();
+
+ stream.forget(aBody);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(BlobImpl, BlobImpl)
+
+} // namespace mozilla::dom
diff --git a/dom/file/BlobImpl.h b/dom/file/BlobImpl.h
new file mode 100644
index 0000000000..720c398cdd
--- /dev/null
+++ b/dom/file/BlobImpl.h
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_BlobImpl_h
+#define mozilla_dom_BlobImpl_h
+
+#include "nsISupports.h"
+#include "nsString.h"
+
+#define BLOBIMPL_IID \
+ { \
+ 0xbccb3275, 0x6778, 0x4ac5, { \
+ 0xaf, 0x03, 0x90, 0xed, 0x37, 0xad, 0xdf, 0x5d \
+ } \
+ }
+
+class nsIInputStream;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class SystemCallerGuarantee;
+template <typename T>
+class Optional;
+
+// This is the abstract class for any File backend. It must be nsISupports
+// because this class must be ref-counted and it has to work with IPC.
+class BlobImpl : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(BLOBIMPL_IID)
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ BlobImpl() = default;
+
+ virtual void GetName(nsAString& aName) const = 0;
+
+ virtual void GetDOMPath(nsAString& aName) const = 0;
+
+ virtual void SetDOMPath(const nsAString& aName) = 0;
+
+ virtual int64_t GetLastModified(ErrorResult& aRv) = 0;
+
+ virtual void GetMozFullPath(nsAString& aName,
+ SystemCallerGuarantee /* unused */,
+ ErrorResult& aRv) = 0;
+
+ virtual void GetMozFullPathInternal(nsAString& aFileName,
+ ErrorResult& aRv) = 0;
+
+ virtual uint64_t GetSize(ErrorResult& aRv) = 0;
+
+ virtual void GetType(nsAString& aType) = 0;
+
+ virtual void GetBlobImplType(nsAString& aBlobImplType) const = 0;
+
+ virtual size_t GetAllocationSize() const = 0;
+ virtual size_t GetAllocationSize(
+ FallibleTArray<BlobImpl*>& aVisitedBlobImpls) const = 0;
+
+ /**
+ * An effectively-unique serial number identifying this instance of FileImpl.
+ *
+ * Implementations should obtain a serial number from
+ * FileImplBase::NextSerialNumber().
+ */
+ virtual uint64_t GetSerialNumber() const = 0;
+
+ already_AddRefed<BlobImpl> Slice(const Optional<int64_t>& aStart,
+ const Optional<int64_t>& aEnd,
+ const nsAString& aContentType,
+ ErrorResult& aRv);
+
+ virtual already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart,
+ uint64_t aLength,
+ const nsAString& aContentType,
+ ErrorResult& aRv) const = 0;
+
+ virtual const nsTArray<RefPtr<BlobImpl>>* GetSubBlobImpls() const = 0;
+
+ virtual void CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const = 0;
+
+ virtual int64_t GetFileId() const = 0;
+
+ nsresult GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength,
+ nsACString& aContentType, nsACString& aCharset);
+
+ virtual void SetLazyData(const nsAString& aName,
+ const nsAString& aContentType, uint64_t aLength,
+ int64_t aLastModifiedDate) = 0;
+
+ virtual bool IsMemoryFile() const = 0;
+
+ virtual bool IsFile() const = 0;
+
+ // Returns true if the BlobImpl is backed by an nsIFile and the underlying
+ // file is a directory.
+ virtual bool IsDirectory() const { return false; }
+
+ protected:
+ virtual ~BlobImpl() = default;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(BlobImpl, BLOBIMPL_IID)
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_BlobImpl_h
diff --git a/dom/file/BlobSet.cpp b/dom/file/BlobSet.cpp
new file mode 100644
index 0000000000..6ae1774296
--- /dev/null
+++ b/dom/file/BlobSet.cpp
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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/BlobSet.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/dom/File.h"
+#include "MemoryBlobImpl.h"
+#include "MultipartBlobImpl.h"
+#include "StringBlobImpl.h"
+
+namespace mozilla::dom {
+
+nsresult BlobSet::AppendVoidPtr(const void* aData, uint32_t aLength) {
+ NS_ENSURE_ARG_POINTER(aData);
+ if (!aLength) {
+ return NS_OK;
+ }
+
+ void* data = malloc(aLength);
+ if (!data) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ memcpy((char*)data, aData, aLength);
+
+ RefPtr<BlobImpl> blobImpl = new MemoryBlobImpl(data, aLength, u""_ns);
+ return AppendBlobImpl(blobImpl);
+}
+
+nsresult BlobSet::AppendUTF8String(const nsACString& aUTF8String,
+ bool nativeEOL) {
+ nsCString utf8Str;
+ if (NS_WARN_IF(!utf8Str.Assign(aUTF8String, mozilla::fallible))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (nativeEOL) {
+ if (utf8Str.Contains('\r')) {
+ if (NS_WARN_IF(
+ !utf8Str.ReplaceSubstring("\r\n", "\n", mozilla::fallible) ||
+ !utf8Str.ReplaceSubstring("\r", "\n", mozilla::fallible))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+#ifdef XP_WIN
+ if (NS_WARN_IF(
+ !utf8Str.ReplaceSubstring("\n", "\r\n", mozilla::fallible))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+#endif
+ }
+
+ RefPtr<StringBlobImpl> blobImpl = StringBlobImpl::Create(utf8Str, u""_ns);
+ return AppendBlobImpl(blobImpl);
+}
+
+nsresult BlobSet::AppendBlobImpl(BlobImpl* aBlobImpl) {
+ NS_ENSURE_ARG_POINTER(aBlobImpl);
+
+ // If aBlobImpl is a MultipartBlobImpl, let's append the sub-blobImpls
+ // instead.
+ const nsTArray<RefPtr<BlobImpl>>* subBlobs = aBlobImpl->GetSubBlobImpls();
+ if (subBlobs) {
+ for (BlobImpl* subBlob : *subBlobs) {
+ nsresult rv = AppendBlobImpl(subBlob);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!mBlobImpls.AppendElement(aBlobImpl, fallible))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/BlobSet.h b/dom/file/BlobSet.h
new file mode 100644
index 0000000000..43955c4c88
--- /dev/null
+++ b/dom/file/BlobSet.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_BlobSet_h
+#define mozilla_dom_BlobSet_h
+
+#include "jsapi.h"
+#include "mozilla/RefPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom {
+
+class BlobImpl;
+
+class BlobSet final {
+ public:
+ [[nodiscard]] nsresult AppendVoidPtr(const void* aData, uint32_t aLength);
+
+ [[nodiscard]] nsresult AppendUTF8String(const nsACString& aUTF8String,
+ bool nativeEOL);
+
+ [[nodiscard]] nsresult AppendBlobImpl(BlobImpl* aBlobImpl);
+
+ FallibleTArray<RefPtr<BlobImpl>>& GetBlobImpls() { return mBlobImpls; }
+
+ private:
+ FallibleTArray<RefPtr<BlobImpl>> mBlobImpls;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_BlobSet_h
diff --git a/dom/file/EmptyBlobImpl.cpp b/dom/file/EmptyBlobImpl.cpp
new file mode 100644
index 0000000000..5fa1055b4d
--- /dev/null
+++ b/dom/file/EmptyBlobImpl.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 "EmptyBlobImpl.h"
+#include "nsStringStream.h"
+
+namespace mozilla::dom {
+
+already_AddRefed<BlobImpl> EmptyBlobImpl::CreateSlice(
+ uint64_t aStart, uint64_t aLength, const nsAString& aContentType,
+ ErrorResult& aRv) const {
+ MOZ_ASSERT(!aStart && !aLength);
+ RefPtr<BlobImpl> impl = new EmptyBlobImpl(aContentType);
+ return impl.forget();
+}
+
+void EmptyBlobImpl::CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const {
+ if (NS_WARN_IF(!aStream)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ nsresult rv = NS_NewCStringInputStream(aStream, ""_ns);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRv.Throw(rv);
+ return;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/EmptyBlobImpl.h b/dom/file/EmptyBlobImpl.h
new file mode 100644
index 0000000000..37dd39da18
--- /dev/null
+++ b/dom/file/EmptyBlobImpl.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 mozilla_dom_EmptyBlobImpl_h
+#define mozilla_dom_EmptyBlobImpl_h
+
+#include "BaseBlobImpl.h"
+
+namespace mozilla::dom {
+
+class EmptyBlobImpl final : public BaseBlobImpl {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(EmptyBlobImpl, BaseBlobImpl)
+
+ // Blob constructor.
+ explicit EmptyBlobImpl(const nsAString& aContentType)
+ : BaseBlobImpl(aContentType, 0 /* aLength */) {}
+
+ void CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const override;
+
+ already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength,
+ const nsAString& aContentType,
+ ErrorResult& aRv) const override;
+
+ bool IsMemoryFile() const override { return true; }
+
+ void GetBlobImplType(nsAString& aBlobImplType) const override {
+ aBlobImplType = u"EmptyBlobImpl"_ns;
+ }
+
+ private:
+ ~EmptyBlobImpl() override = default;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_EmptyBlobImpl_h
diff --git a/dom/file/File.cpp b/dom/file/File.cpp
new file mode 100644
index 0000000000..9995d35c52
--- /dev/null
+++ b/dom/file/File.cpp
@@ -0,0 +1,202 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "File.h"
+#include "FileBlobImpl.h"
+#include "MemoryBlobImpl.h"
+#include "MultipartBlobImpl.h"
+#include "mozilla/dom/BlobBinding.h"
+#include "mozilla/dom/FileBinding.h"
+#include "mozilla/dom/FileCreatorHelper.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/Promise.h"
+#include "nsIFile.h"
+#include "nsContentUtils.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla::dom {
+
+File::File(nsIGlobalObject* aGlobal, BlobImpl* aImpl) : Blob(aGlobal, aImpl) {
+ MOZ_ASSERT(aImpl->IsFile());
+}
+
+File::~File() = default;
+
+/* static */
+File* File::Create(nsIGlobalObject* aGlobal, BlobImpl* aImpl) {
+ MOZ_ASSERT(aImpl);
+ MOZ_ASSERT(aImpl->IsFile());
+
+ MOZ_ASSERT(aGlobal);
+ if (NS_WARN_IF(!aGlobal)) {
+ return nullptr;
+ }
+
+ return new File(aGlobal, aImpl);
+}
+
+/* static */
+already_AddRefed<File> File::CreateMemoryFileWithCustomLastModified(
+ nsIGlobalObject* aGlobal, void* aMemoryBuffer, uint64_t aLength,
+ const nsAString& aName, const nsAString& aContentType,
+ int64_t aLastModifiedDate) {
+ RefPtr<MemoryBlobImpl> blobImpl =
+ MemoryBlobImpl::CreateWithCustomLastModified(
+ aMemoryBuffer, aLength, aName, aContentType, aLastModifiedDate);
+ MOZ_ASSERT(blobImpl);
+
+ RefPtr<File> file = File::Create(aGlobal, blobImpl);
+ return file.forget();
+}
+
+/* static */
+already_AddRefed<File> File::CreateMemoryFileWithLastModifiedNow(
+ nsIGlobalObject* aGlobal, void* aMemoryBuffer, uint64_t aLength,
+ const nsAString& aName, const nsAString& aContentType) {
+ MOZ_ASSERT(aGlobal);
+
+ RefPtr<MemoryBlobImpl> blobImpl = MemoryBlobImpl::CreateWithLastModifiedNow(
+ aMemoryBuffer, aLength, aName, aContentType, aGlobal->GetRTPCallerType());
+ MOZ_ASSERT(blobImpl);
+
+ RefPtr<File> file = File::Create(aGlobal, blobImpl);
+ return file.forget();
+}
+
+/* static */
+already_AddRefed<File> File::CreateFromFile(nsIGlobalObject* aGlobal,
+ nsIFile* aFile) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+
+ MOZ_ASSERT(aGlobal);
+ if (NS_WARN_IF(!aGlobal)) {
+ return nullptr;
+ }
+
+ RefPtr<File> file = new File(aGlobal, new FileBlobImpl(aFile));
+ return file.forget();
+}
+
+/* static */
+already_AddRefed<File> File::CreateFromFile(nsIGlobalObject* aGlobal,
+ nsIFile* aFile,
+ const nsAString& aName,
+ const nsAString& aContentType) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+
+ MOZ_ASSERT(aGlobal);
+ if (NS_WARN_IF(!aGlobal)) {
+ return nullptr;
+ }
+
+ RefPtr<File> file =
+ new File(aGlobal, new FileBlobImpl(aFile, aName, aContentType));
+ return file.forget();
+}
+
+JSObject* File::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return File_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void File::GetName(nsAString& aFileName) const { mImpl->GetName(aFileName); }
+
+void File::GetRelativePath(nsAString& aPath) const {
+ aPath.Truncate();
+
+ nsAutoString path;
+ mImpl->GetDOMPath(path);
+
+ // WebkitRelativePath doesn't start with '/'
+ if (!path.IsEmpty()) {
+ MOZ_ASSERT(path[0] == FILESYSTEM_DOM_PATH_SEPARATOR_CHAR);
+ aPath.Assign(Substring(path, 1));
+ }
+}
+
+int64_t File::GetLastModified(ErrorResult& aRv) {
+ return mImpl->GetLastModified(aRv);
+}
+
+void File::GetMozFullPath(nsAString& aFilename,
+ SystemCallerGuarantee aGuarantee, ErrorResult& aRv) {
+ mImpl->GetMozFullPath(aFilename, aGuarantee, aRv);
+}
+
+void File::GetMozFullPathInternal(nsAString& aFileName, ErrorResult& aRv) {
+ mImpl->GetMozFullPathInternal(aFileName, aRv);
+}
+
+/* static */
+already_AddRefed<File> File::Constructor(const GlobalObject& aGlobal,
+ const Sequence<BlobPart>& aData,
+ const nsAString& aName,
+ const FilePropertyBag& aBag,
+ ErrorResult& aRv) {
+ RefPtr<MultipartBlobImpl> impl = new MultipartBlobImpl(aName);
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ MOZ_ASSERT(global);
+
+ nsAutoString type(aBag.mType);
+ MakeValidBlobType(type);
+ impl->InitializeBlob(aData, type, aBag.mEndings == EndingType::Native,
+ global->GetRTPCallerType(), aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ MOZ_ASSERT(impl->IsFile());
+
+ if (aBag.mLastModified.WasPassed()) {
+ impl->SetLastModified(aBag.mLastModified.Value());
+ }
+
+ RefPtr<File> file = new File(global, impl);
+ return file.forget();
+}
+
+/* static */
+already_AddRefed<Promise> File::CreateFromNsIFile(
+ const GlobalObject& aGlobal, nsIFile* aData,
+ const ChromeFilePropertyBag& aBag, SystemCallerGuarantee aGuarantee,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+
+ MOZ_ASSERT(global);
+ if (NS_WARN_IF(!global)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise =
+ FileCreatorHelper::CreateFile(global, aData, aBag, true, aRv);
+ return promise.forget();
+}
+
+/* static */
+already_AddRefed<Promise> File::CreateFromFileName(
+ const GlobalObject& aGlobal, const nsAString& aPath,
+ const ChromeFilePropertyBag& aBag, SystemCallerGuarantee aGuarantee,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIFile> file;
+ aRv = NS_NewLocalFile(aPath, false, getter_AddRefs(file));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+
+ MOZ_ASSERT(global);
+ if (NS_WARN_IF(!global)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise =
+ FileCreatorHelper::CreateFile(global, file, aBag, false, aRv);
+ return promise.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/File.h b/dom/file/File.h
new file mode 100644
index 0000000000..279b91dd93
--- /dev/null
+++ b/dom/file/File.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_File_h
+#define mozilla_dom_File_h
+
+#include "mozilla/dom/Blob.h"
+
+class nsIFile;
+
+namespace mozilla::dom {
+
+struct ChromeFilePropertyBag;
+struct FilePropertyBag;
+class Promise;
+
+class File final : public Blob {
+ friend class Blob;
+
+ public:
+ // Note: BlobImpl must be a File in order to use this method.
+ // Check impl->IsFile().
+ static File* Create(nsIGlobalObject* aGlobal, BlobImpl* aImpl);
+
+ // The returned File takes ownership of aMemoryBuffer. aMemoryBuffer will be
+ // freed by free so it must be allocated by malloc or something
+ // compatible with it.
+ static already_AddRefed<File> CreateMemoryFileWithLastModifiedNow(
+ nsIGlobalObject* aGlobal, void* aMemoryBuffer, uint64_t aLength,
+ const nsAString& aName, const nsAString& aContentType);
+
+ // You should not use this method! Please consider to use the
+ // CreateMemoryFileWithLastModifiedNow.
+ static already_AddRefed<File> CreateMemoryFileWithCustomLastModified(
+ nsIGlobalObject* aGlobal, void* aMemoryBuffer, uint64_t aLength,
+ const nsAString& aName, const nsAString& aContentType,
+ int64_t aLastModifiedDate);
+
+ // This method creates a BlobFileImpl for the new File object. This is
+ // thread-safe, cross-process, cross-thread as any other BlobImpl, but, when
+ // GetType() is called, it must dispatch a runnable to the main-thread in
+ // order to use nsIMIMEService.
+ // Would be nice if we try to avoid to use this method outside the
+ // main-thread to avoid extra runnables.
+ static already_AddRefed<File> CreateFromFile(nsIGlobalObject* aGlobal,
+ nsIFile* aFile);
+
+ static already_AddRefed<File> CreateFromFile(nsIGlobalObject* aGlobal,
+ nsIFile* aFile,
+ const nsAString& aName,
+ const nsAString& aContentType);
+
+ // WebIDL methods
+
+ JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // File constructor
+ static already_AddRefed<File> Constructor(const GlobalObject& aGlobal,
+ const Sequence<BlobPart>& aData,
+ const nsAString& aName,
+ const FilePropertyBag& aBag,
+ ErrorResult& aRv);
+
+ // ChromeOnly
+ static already_AddRefed<Promise> CreateFromFileName(
+ const GlobalObject& aGlobal, const nsAString& aPath,
+ const ChromeFilePropertyBag& aBag, SystemCallerGuarantee aGuarantee,
+ ErrorResult& aRv);
+
+ // ChromeOnly
+ static already_AddRefed<Promise> CreateFromNsIFile(
+ const GlobalObject& aGlobal, nsIFile* aData,
+ const ChromeFilePropertyBag& aBag, SystemCallerGuarantee aGuarantee,
+ ErrorResult& aRv);
+
+ void GetName(nsAString& aFileName) const;
+
+ int64_t GetLastModified(ErrorResult& aRv);
+
+ void GetRelativePath(nsAString& aPath) const;
+
+ void GetMozFullPath(nsAString& aFilename, SystemCallerGuarantee aGuarantee,
+ ErrorResult& aRv);
+
+ void GetMozFullPathInternal(nsAString& aFileName, ErrorResult& aRv);
+
+ protected:
+ bool HasFileInterface() const override { return true; }
+
+ private:
+ // File constructor should never be used directly. Use Blob::Create or
+ // File::Create.
+ File(nsIGlobalObject* aGlobal, BlobImpl* aImpl);
+ ~File() override;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_File_h
diff --git a/dom/file/FileBlobImpl.cpp b/dom/file/FileBlobImpl.cpp
new file mode 100644
index 0000000000..af80476e1a
--- /dev/null
+++ b/dom/file/FileBlobImpl.cpp
@@ -0,0 +1,294 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "FileBlobImpl.h"
+#include "BaseBlobImpl.h"
+#include "mozilla/SlicedInputStream.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "nsCExternalHandlerService.h"
+#include "nsIFile.h"
+#include "nsIFileStreams.h"
+#include "nsIMIMEService.h"
+#include "nsNetUtil.h"
+#include "nsStreamUtils.h"
+
+namespace mozilla::dom {
+
+FileBlobImpl::FileBlobImpl(nsIFile* aFile)
+ : mMutex("FileBlobImpl::mMutex"),
+ mFile(aFile),
+ mSerialNumber(BaseBlobImpl::NextSerialNumber()),
+ mStart(0),
+ mFileId(-1),
+ mIsFile(true),
+ mWholeFile(true) {
+ MOZ_ASSERT(mFile, "must have file");
+ MOZ_ASSERT(XRE_IsParentProcess());
+ // Lazily get the content type and size
+ mContentType.SetIsVoid(true);
+ mMozFullPath.SetIsVoid(true);
+ mFile->GetLeafName(mName);
+}
+
+FileBlobImpl::FileBlobImpl(const nsAString& aName,
+ const nsAString& aContentType, uint64_t aLength,
+ nsIFile* aFile)
+ : mMutex("FileBlobImpl::mMutex"),
+ mFile(aFile),
+ mContentType(aContentType),
+ mName(aName),
+ mSerialNumber(BaseBlobImpl::NextSerialNumber()),
+ mStart(0),
+ mFileId(-1),
+ mLength(Some(aLength)),
+ mIsFile(true),
+ mWholeFile(true) {
+ MOZ_ASSERT(mFile, "must have file");
+ MOZ_ASSERT(XRE_IsParentProcess());
+ mMozFullPath.SetIsVoid(true);
+}
+
+FileBlobImpl::FileBlobImpl(const nsAString& aName,
+ const nsAString& aContentType, uint64_t aLength,
+ nsIFile* aFile, int64_t aLastModificationDate)
+ : mMutex("FileBlobImpl::mMutex"),
+ mFile(aFile),
+ mContentType(aContentType),
+ mName(aName),
+ mSerialNumber(BaseBlobImpl::NextSerialNumber()),
+ mStart(0),
+ mFileId(-1),
+ mLength(Some(aLength)),
+ mLastModified(Some(aLastModificationDate)),
+ mIsFile(true),
+ mWholeFile(true) {
+ MOZ_ASSERT(mFile, "must have file");
+ MOZ_ASSERT(XRE_IsParentProcess());
+ mMozFullPath.SetIsVoid(true);
+}
+
+FileBlobImpl::FileBlobImpl(nsIFile* aFile, const nsAString& aName,
+ const nsAString& aContentType)
+ : mMutex("FileBlobImpl::mMutex"),
+ mFile(aFile),
+ mContentType(aContentType),
+ mName(aName),
+ mSerialNumber(BaseBlobImpl::NextSerialNumber()),
+ mStart(0),
+ mFileId(-1),
+ mIsFile(true),
+ mWholeFile(true) {
+ MOZ_ASSERT(mFile, "must have file");
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (aContentType.IsEmpty()) {
+ // Lazily get the content type and size
+ mContentType.SetIsVoid(true);
+ }
+
+ mMozFullPath.SetIsVoid(true);
+}
+
+FileBlobImpl::FileBlobImpl(const FileBlobImpl* aOther, uint64_t aStart,
+ uint64_t aLength, const nsAString& aContentType)
+ : mMutex("FileBlobImpl::mMutex"),
+ mFile(aOther->mFile),
+ mContentType(aContentType),
+ mSerialNumber(BaseBlobImpl::NextSerialNumber()),
+ mStart(aOther->mStart + aStart),
+ mFileId(-1),
+ mLength(Some(aLength)),
+ mIsFile(false),
+ mWholeFile(false) {
+ MOZ_ASSERT(mFile, "must have file");
+ MOZ_ASSERT(XRE_IsParentProcess());
+ mMozFullPath = aOther->mMozFullPath;
+}
+
+already_AddRefed<BlobImpl> FileBlobImpl::CreateSlice(
+ uint64_t aStart, uint64_t aLength, const nsAString& aContentType,
+ ErrorResult& aRv) const {
+ RefPtr<FileBlobImpl> impl =
+ new FileBlobImpl(this, aStart, aLength, aContentType);
+ return impl.forget();
+}
+
+void FileBlobImpl::GetMozFullPathInternal(nsAString& aFilename,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(mIsFile, "Should only be called on files");
+
+ MutexAutoLock lock(mMutex);
+
+ if (!mMozFullPath.IsVoid()) {
+ aFilename = mMozFullPath;
+ return;
+ }
+
+ aRv = mFile->GetPath(aFilename);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ mMozFullPath = aFilename;
+}
+
+uint64_t FileBlobImpl::GetSize(ErrorResult& aRv) {
+ MutexAutoLock lock(mMutex);
+
+ if (mLength.isNothing()) {
+ MOZ_ASSERT(mWholeFile,
+ "Should only use lazy size when using the whole file");
+ int64_t fileSize;
+ aRv = mFile->GetFileSize(&fileSize);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return 0;
+ }
+
+ if (fileSize < 0) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return 0;
+ }
+
+ mLength.emplace(fileSize);
+ }
+
+ return mLength.value();
+}
+
+class FileBlobImpl::GetTypeRunnable final : public WorkerMainThreadRunnable {
+ public:
+ GetTypeRunnable(WorkerPrivate* aWorkerPrivate, FileBlobImpl* aBlobImpl,
+ const MutexAutoLock& aProofOfLock)
+ : WorkerMainThreadRunnable(aWorkerPrivate, "FileBlobImpl :: GetType"_ns),
+ mBlobImpl(aBlobImpl),
+ mProofOfLock(aProofOfLock) {
+ MOZ_ASSERT(aBlobImpl);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ }
+
+ bool MainThreadRun() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoString type;
+ mBlobImpl->GetTypeInternal(type, mProofOfLock);
+ return true;
+ }
+
+ private:
+ ~GetTypeRunnable() override = default;
+
+ RefPtr<FileBlobImpl> mBlobImpl;
+ const MutexAutoLock& mProofOfLock;
+};
+
+void FileBlobImpl::GetType(nsAString& aType) {
+ MutexAutoLock lock(mMutex);
+ GetTypeInternal(aType, lock);
+}
+
+void FileBlobImpl::GetTypeInternal(nsAString& aType,
+ const MutexAutoLock& aProofOfLock) {
+ aType.Truncate();
+
+ if (mContentType.IsVoid()) {
+ MOZ_ASSERT(mWholeFile,
+ "Should only use lazy ContentType when using the whole file");
+
+ if (!NS_IsMainThread()) {
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ if (!workerPrivate) {
+ // I have no idea in which thread this method is called. We cannot
+ // return any valid value.
+ return;
+ }
+
+ RefPtr<GetTypeRunnable> runnable =
+ new GetTypeRunnable(workerPrivate, this, aProofOfLock);
+
+ ErrorResult rv;
+ runnable->Dispatch(Canceling, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ return;
+ }
+ } else {
+ nsresult rv;
+ nsCOMPtr<nsIMIMEService> mimeService =
+ do_GetService(NS_MIMESERVICE_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsAutoCString mimeType;
+ rv = mimeService->GetTypeFromFile(mFile, mimeType);
+ if (NS_FAILED(rv)) {
+ mimeType.Truncate();
+ }
+
+ AppendUTF8toUTF16(mimeType, mContentType);
+ mContentType.SetIsVoid(false);
+ }
+ }
+
+ aType = mContentType;
+}
+
+void FileBlobImpl::GetBlobImplType(nsAString& aBlobImplType) const {
+ aBlobImplType = u"FileBlobImpl"_ns;
+}
+
+int64_t FileBlobImpl::GetLastModified(ErrorResult& aRv) {
+ MOZ_ASSERT(mIsFile, "Should only be called on files");
+
+ MutexAutoLock lock(mMutex);
+
+ if (mLastModified.isNothing()) {
+ PRTime msecs;
+ aRv = mFile->GetLastModifiedTime(&msecs);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return 0;
+ }
+
+ mLastModified.emplace(int64_t(msecs));
+ }
+
+ return mLastModified.value();
+}
+
+const uint32_t sFileStreamFlags =
+ nsIFileInputStream::CLOSE_ON_EOF | nsIFileInputStream::REOPEN_ON_REWIND |
+ nsIFileInputStream::DEFER_OPEN | nsIFileInputStream::SHARE_DELETE;
+
+void FileBlobImpl::CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const {
+ nsCOMPtr<nsIInputStream> stream;
+ aRv = NS_NewLocalFileInputStream(getter_AddRefs(stream), mFile, -1, -1,
+ sFileStreamFlags);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ if (mWholeFile) {
+ stream.forget(aStream);
+ return;
+ }
+
+ MOZ_ASSERT(mLength.isSome());
+
+ RefPtr<SlicedInputStream> slicedInputStream =
+ new SlicedInputStream(stream.forget(), mStart, mLength.value());
+ slicedInputStream.forget(aStream);
+}
+
+bool FileBlobImpl::IsDirectory() const {
+ bool isDirectory = false;
+ if (mFile) {
+ mFile->IsDirectory(&isDirectory);
+ }
+ return isDirectory;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/FileBlobImpl.h b/dom/file/FileBlobImpl.h
new file mode 100644
index 0000000000..84565dbd4b
--- /dev/null
+++ b/dom/file/FileBlobImpl.h
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileBlobImpl_h
+#define mozilla_dom_FileBlobImpl_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Mutex.h"
+#include "nsCOMPtr.h"
+
+class nsIFile;
+
+namespace mozilla::dom {
+
+class FileBlobImpl : public BlobImpl {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(FileBlobImpl, BlobImpl)
+
+ // Create as a file
+ explicit FileBlobImpl(nsIFile* aFile);
+
+ // Create as a file
+ FileBlobImpl(const nsAString& aName, const nsAString& aContentType,
+ uint64_t aLength, nsIFile* aFile);
+
+ FileBlobImpl(const nsAString& aName, const nsAString& aContentType,
+ uint64_t aLength, nsIFile* aFile, int64_t aLastModificationDate);
+
+ // Create as a file with custom name
+ FileBlobImpl(nsIFile* aFile, const nsAString& aName,
+ const nsAString& aContentType);
+
+ void GetName(nsAString& aName) const override {
+ MOZ_ASSERT(mIsFile, "Should only be called on files");
+ aName = mName;
+ }
+
+ void SetName(const nsAString& aName) { mName = aName; }
+
+ void GetDOMPath(nsAString& aPath) const override {
+ MOZ_ASSERT(mIsFile, "Should only be called on files");
+ aPath = mPath;
+ }
+
+ void SetDOMPath(const nsAString& aPath) override {
+ MOZ_ASSERT(mIsFile, "Should only be called on files");
+ mPath = aPath;
+ }
+
+ int64_t GetLastModified(ErrorResult& aRv) override;
+
+ void GetMozFullPath(nsAString& aFileName, SystemCallerGuarantee /* unused */,
+ ErrorResult& aRv) override {
+ MOZ_ASSERT(mIsFile, "Should only be called on files");
+
+ GetMozFullPathInternal(aFileName, aRv);
+ }
+
+ void GetMozFullPathInternal(nsAString& aFilename, ErrorResult& aRv) override;
+
+ uint64_t GetSize(ErrorResult& aRv) override;
+
+ void GetType(nsAString& aType) override;
+
+ void GetBlobImplType(nsAString& aBlobImplType) const override;
+
+ size_t GetAllocationSize() const override { return 0; }
+
+ size_t GetAllocationSize(
+ FallibleTArray<BlobImpl*>& aVisitedBlobImpls) const override {
+ return GetAllocationSize();
+ }
+
+ uint64_t GetSerialNumber() const override { return mSerialNumber; }
+
+ const nsTArray<RefPtr<BlobImpl>>* GetSubBlobImpls() const override {
+ return nullptr;
+ }
+
+ void CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const override;
+
+ int64_t GetFileId() const override { return mFileId; }
+
+ void SetLazyData(const nsAString& aName, const nsAString& aContentType,
+ uint64_t aLength, int64_t aLastModifiedDate) override {
+ mName = aName;
+ mContentType = aContentType;
+ mIsFile = !aName.IsVoid();
+ mLength.emplace(aLength);
+ mLastModified.emplace(aLastModifiedDate);
+ }
+
+ bool IsMemoryFile() const override { return false; }
+
+ bool IsFile() const override { return mIsFile; }
+
+ bool IsDirectory() const override;
+
+ void SetType(const nsAString& aType) { mContentType = aType; }
+
+ void SetFileId(int64_t aFileId) { mFileId = aFileId; }
+
+ void SetEmptySize() { mLength.emplace(0); }
+
+ void SetMozFullPath(const nsAString& aPath) { mMozFullPath = aPath; }
+
+ void SetLastModified(int64_t aLastModified) {
+ mLastModified.emplace(aLastModified);
+ }
+
+ protected:
+ ~FileBlobImpl() override = default;
+
+ // Create slice
+ FileBlobImpl(const FileBlobImpl* aOther, uint64_t aStart, uint64_t aLength,
+ const nsAString& aContentType);
+
+ already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength,
+ const nsAString& aContentType,
+ ErrorResult& aRv) const override;
+
+ class GetTypeRunnable;
+ void GetTypeInternal(nsAString& aType, const MutexAutoLock& aProofOfLock);
+
+ // FileBlobImpl has getter methods with lazy initialization. Because any
+ // BlobImpl must work thread-safe, we use a mutex.
+ Mutex mMutex MOZ_UNANNOTATED;
+
+ nsCOMPtr<nsIFile> mFile;
+
+ nsString mContentType;
+ nsString mName;
+ nsString mPath; // The path relative to a directory chosen by the user
+ nsString mMozFullPath;
+
+ const uint64_t mSerialNumber;
+ uint64_t mStart;
+
+ int64_t mFileId;
+
+ Maybe<uint64_t> mLength;
+
+ Maybe<int64_t> mLastModified;
+
+ bool mIsFile;
+ bool mWholeFile;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileBlobImpl_h
diff --git a/dom/file/FileCreatorHelper.cpp b/dom/file/FileCreatorHelper.cpp
new file mode 100644
index 0000000000..2c4ee4c041
--- /dev/null
+++ b/dom/file/FileCreatorHelper.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 "FileCreatorHelper.h"
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/FileBinding.h"
+#include "mozilla/dom/FileCreatorChild.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/Promise.h"
+#include "nsContentUtils.h"
+#include "nsPIDOMWindow.h"
+#include "nsProxyRelease.h"
+#include "nsIFile.h"
+
+// Undefine the macro of CreateFile to avoid FileCreatorHelper#CreateFile being
+// replaced by FileCreatorHelper#CreateFileW.
+#ifdef CreateFile
+# undef CreateFile
+#endif
+
+namespace mozilla::dom {
+
+/* static */
+already_AddRefed<Promise> FileCreatorHelper::CreateFile(
+ nsIGlobalObject* aGlobalObject, nsIFile* aFile,
+ const ChromeFilePropertyBag& aBag, bool aIsFromNsIFile, ErrorResult& aRv) {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+
+ RefPtr<Promise> promise = Promise::Create(aGlobalObject, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ nsAutoString path;
+ aRv = aFile->GetPath(path);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ // Register this component to PBackground.
+ mozilla::ipc::PBackgroundChild* actorChild =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!actorChild)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ Maybe<int64_t> lastModified;
+ if (aBag.mLastModified.WasPassed()) {
+ lastModified.emplace(aBag.mLastModified.Value());
+ }
+
+ PFileCreatorChild* actor = actorChild->SendPFileCreatorConstructor(
+ path, aBag.mType, aBag.mName, lastModified, aBag.mExistenceCheck,
+ aIsFromNsIFile);
+
+ static_cast<FileCreatorChild*>(actor)->SetPromise(promise);
+ return promise.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/FileCreatorHelper.h b/dom/file/FileCreatorHelper.h
new file mode 100644
index 0000000000..f0b666cc58
--- /dev/null
+++ b/dom/file/FileCreatorHelper.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileCreatorHelper_h
+#define mozilla_dom_FileCreatorHelper_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/RefPtr.h"
+#include "nsISupportsImpl.h"
+
+// Undefine the macro of CreateFile to avoid FileCreatorHelper#CreateFile being
+// replaced by FileCreatorHelper#CreateFileW.
+#ifdef CreateFile
+# undef CreateFile
+#endif
+
+class nsIFile;
+class nsIGlobalObject;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+struct ChromeFilePropertyBag;
+class Promise;
+
+class FileCreatorHelper final {
+ public:
+ static already_AddRefed<Promise> CreateFile(nsIGlobalObject* aGlobalObject,
+ nsIFile* aFile,
+ const ChromeFilePropertyBag& aBag,
+ bool aIsFromNsIFile,
+ ErrorResult& aRv);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_FileCreatorHelper_h
diff --git a/dom/file/FileList.cpp b/dom/file/FileList.cpp
new file mode 100644
index 0000000000..2cc7b0c7f1
--- /dev/null
+++ b/dom/file/FileList.cpp
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/FileList.h"
+
+#include <new>
+#include <utility>
+#include "ErrorList.h"
+#include "js/RootingAPI.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/MacroForEach.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/FileListBinding.h"
+#include "mozilla/fallible.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCycleCollectionTraversalCallback.h"
+#include "nsISupports.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FileList, mFiles, mParent)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileList)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FileList)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(FileList)
+
+FileList::FileList(nsISupports* aParent) : mParent(aParent) {}
+
+FileList::~FileList() = default;
+
+JSObject* FileList::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return mozilla::dom::FileList_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+bool FileList::Append(File* aFile) {
+ MOZ_ASSERT(aFile);
+ return mFiles.AppendElement(aFile, fallible);
+}
+
+bool FileList::Remove(uint32_t aIndex) {
+ if (aIndex < mFiles.Length()) {
+ mFiles.RemoveElementAt(aIndex);
+ return true;
+ }
+
+ return false;
+}
+
+void FileList::Clear() { return mFiles.Clear(); }
+
+File* FileList::Item(uint32_t aIndex) const {
+ if (aIndex >= mFiles.Length()) {
+ return nullptr;
+ }
+
+ return mFiles[aIndex];
+}
+
+File* FileList::IndexedGetter(uint32_t aIndex, bool& aFound) const {
+ aFound = aIndex < mFiles.Length();
+ return Item(aIndex);
+}
+
+void FileList::ToSequence(Sequence<RefPtr<File>>& aSequence,
+ ErrorResult& aRv) const {
+ MOZ_ASSERT(aSequence.IsEmpty());
+ if (mFiles.IsEmpty()) {
+ return;
+ }
+
+ if (!aSequence.SetLength(mFiles.Length(), mozilla::fallible_t())) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ for (uint32_t i = 0; i < mFiles.Length(); ++i) {
+ aSequence[i] = mFiles[i];
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/FileList.h b/dom/file/FileList.h
new file mode 100644
index 0000000000..f36b25ba07
--- /dev/null
+++ b/dom/file/FileList.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileList_h
+#define mozilla_dom_FileList_h
+
+#include <cstdint>
+#include "js/TypeDecls.h"
+#include "mozilla/Assertions.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+
+class nsCycleCollectionTraversalCallback;
+template <class T>
+class RefPtr;
+
+namespace mozilla {
+class ErrorResult;
+namespace dom {
+
+class BlobImpls;
+class File;
+template <typename T>
+class Sequence;
+
+class FileList final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileList)
+
+ explicit FileList(nsISupports* aParent);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsISupports* GetParentObject() { return mParent; }
+
+ bool Append(File* aFile);
+
+ bool Remove(uint32_t aIndex);
+
+ void Clear();
+
+ File* Item(uint32_t aIndex) const;
+
+ File* IndexedGetter(uint32_t aIndex, bool& aFound) const;
+
+ uint32_t Length() const { return mFiles.Length(); }
+
+ void ToSequence(Sequence<RefPtr<File>>& aSequence, ErrorResult& aRv) const;
+
+ private:
+ ~FileList();
+
+ FallibleTArray<RefPtr<File>> mFiles;
+ nsCOMPtr<nsISupports> mParent;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_FileList_h
diff --git a/dom/file/FileReader.cpp b/dom/file/FileReader.cpp
new file mode 100644
index 0000000000..93948671df
--- /dev/null
+++ b/dom/file/FileReader.cpp
@@ -0,0 +1,803 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "FileReader.h"
+
+#include "nsIGlobalObject.h"
+#include "nsITimer.h"
+
+#include "js/ArrayBuffer.h" // JS::NewArrayBufferWithContents
+#include "mozilla/Base64.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/DOMExceptionBinding.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/FileReaderBinding.h"
+#include "mozilla/dom/ProgressEvent.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerRef.h"
+#include "mozilla/dom/WorkerScope.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "nsAlgorithm.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsDOMJSUtils.h"
+#include "nsError.h"
+#include "nsNetUtil.h"
+#include "nsStreamUtils.h"
+#include "nsThreadUtils.h"
+#include "xpcpublic.h"
+#include "nsReadableUtils.h"
+
+namespace mozilla::dom {
+
+#define ABORT_STR u"abort"
+#define LOAD_STR u"load"
+#define LOADSTART_STR u"loadstart"
+#define LOADEND_STR u"loadend"
+#define ERROR_STR u"error"
+#define PROGRESS_STR u"progress"
+
+const uint64_t kUnknownSize = uint64_t(-1);
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(FileReader)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(FileReader,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBlob)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mProgressNotifier)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mError)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(FileReader,
+ DOMEventTargetHelper)
+ tmp->Shutdown();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mBlob)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mProgressNotifier)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mError)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(FileReader, DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mResultArrayBuffer)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileReader)
+ NS_INTERFACE_MAP_ENTRY_CONCRETE(FileReader)
+ NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(FileReader, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(FileReader, DOMEventTargetHelper)
+
+class MOZ_RAII FileReaderDecreaseBusyCounter {
+ RefPtr<FileReader> mFileReader;
+
+ public:
+ explicit FileReaderDecreaseBusyCounter(FileReader* aFileReader)
+ : mFileReader(aFileReader) {}
+
+ ~FileReaderDecreaseBusyCounter() { mFileReader->DecreaseBusyCounter(); }
+};
+
+class FileReader::AsyncWaitRunnable final : public CancelableRunnable {
+ public:
+ explicit AsyncWaitRunnable(FileReader* aReader)
+ : CancelableRunnable("FileReader::AsyncWaitRunnable"), mReader(aReader) {}
+
+ NS_IMETHOD
+ Run() override {
+ if (mReader) {
+ mReader->InitialAsyncWait();
+ }
+ return NS_OK;
+ }
+
+ nsresult Cancel() override {
+ mReader = nullptr;
+ return NS_OK;
+ }
+
+ public:
+ RefPtr<FileReader> mReader;
+};
+
+void FileReader::RootResultArrayBuffer() { mozilla::HoldJSObjects(this); }
+
+// FileReader constructors/initializers
+
+FileReader::FileReader(nsIGlobalObject* aGlobal, WeakWorkerRef* aWorkerRef)
+ : DOMEventTargetHelper(aGlobal),
+ mFileData(nullptr),
+ mDataLen(0),
+ mDataFormat(FILE_AS_BINARY),
+ mResultArrayBuffer(nullptr),
+ mProgressEventWasDelayed(false),
+ mTimerIsActive(false),
+ mReadyState(EMPTY),
+ mTotal(0),
+ mTransferred(0),
+ mBusyCount(0),
+ mWeakWorkerRef(aWorkerRef) {
+ MOZ_ASSERT(aGlobal);
+ MOZ_ASSERT_IF(NS_IsMainThread(), !mWeakWorkerRef);
+
+ if (NS_IsMainThread()) {
+ mTarget = aGlobal->EventTargetFor(TaskCategory::Other);
+ } else {
+ mTarget = GetCurrentSerialEventTarget();
+ }
+
+ SetDOMStringToNull(mResult);
+}
+
+FileReader::~FileReader() {
+ Shutdown();
+ DropJSObjects(this);
+}
+
+/* static */
+already_AddRefed<FileReader> FileReader::Constructor(
+ const GlobalObject& aGlobal) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ RefPtr<WeakWorkerRef> workerRef;
+
+ if (!NS_IsMainThread()) {
+ JSContext* cx = aGlobal.Context();
+ WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
+
+ workerRef = WeakWorkerRef::Create(workerPrivate);
+ }
+
+ RefPtr<FileReader> fileReader = new FileReader(global, workerRef);
+
+ return fileReader.forget();
+}
+
+// nsIInterfaceRequestor
+
+NS_IMETHODIMP
+FileReader::GetInterface(const nsIID& aIID, void** aResult) {
+ return QueryInterface(aIID, aResult);
+}
+
+void FileReader::GetResult(JSContext* aCx,
+ Nullable<OwningStringOrArrayBuffer>& aResult) {
+ JS::Rooted<JS::Value> result(aCx);
+
+ if (mDataFormat == FILE_AS_ARRAYBUFFER) {
+ if (mReadyState != DONE || !mResultArrayBuffer ||
+ !aResult.SetValue().SetAsArrayBuffer().Init(mResultArrayBuffer)) {
+ aResult.SetNull();
+ }
+
+ return;
+ }
+
+ if (mReadyState != DONE || mResult.IsVoid()) {
+ aResult.SetNull();
+ return;
+ }
+
+ aResult.SetValue().SetAsString() = mResult;
+}
+
+void FileReader::OnLoadEndArrayBuffer() {
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(GetParentObject())) {
+ FreeDataAndDispatchError(NS_ERROR_FAILURE);
+ return;
+ }
+
+ RootResultArrayBuffer();
+
+ JSContext* cx = jsapi.cx();
+
+ mResultArrayBuffer = JS::NewArrayBufferWithContents(cx, mDataLen, mFileData);
+ if (mResultArrayBuffer) {
+ mFileData = nullptr; // Transfer ownership
+ FreeDataAndDispatchSuccess();
+ return;
+ }
+
+ // Let's handle the error status.
+
+ JS::Rooted<JS::Value> exceptionValue(cx);
+ if (!JS_GetPendingException(cx, &exceptionValue) ||
+ // This should not really happen, exception should always be an object.
+ !exceptionValue.isObject()) {
+ JS_ClearPendingException(jsapi.cx());
+ FreeDataAndDispatchError(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ JS_ClearPendingException(jsapi.cx());
+
+ JS::Rooted<JSObject*> exceptionObject(cx, &exceptionValue.toObject());
+ JSErrorReport* er = JS_ErrorFromException(cx, exceptionObject);
+ if (!er || er->message()) {
+ FreeDataAndDispatchError(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ nsAutoString errorName;
+ JSLinearString* name = js::GetErrorTypeName(cx, er->exnType);
+ if (name) {
+ AssignJSLinearString(errorName, name);
+ }
+
+ nsAutoCString errorMsg(er->message().c_str());
+ nsAutoCString errorNameC = NS_LossyConvertUTF16toASCII(errorName);
+ // XXX Code selected arbitrarily
+ mError =
+ new DOMException(NS_ERROR_DOM_INVALID_STATE_ERR, errorMsg, errorNameC,
+ DOMException_Binding::INVALID_STATE_ERR);
+
+ FreeDataAndDispatchError();
+}
+
+nsresult FileReader::DoAsyncWait() {
+ nsresult rv = IncreaseBusyCounter();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mAsyncStream->AsyncWait(this,
+ /* aFlags*/ 0,
+ /* aRequestedCount */ 0, mTarget);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ DecreaseBusyCounter();
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+namespace {
+
+void PopulateBufferForBinaryString(char16_t* aDest, const char* aSource,
+ uint32_t aCount) {
+ // Zero-extend each char to char16_t.
+ ConvertLatin1toUtf16(Span(aSource, aCount), Span(aDest, aCount));
+}
+
+nsresult ReadFuncBinaryString(nsIInputStream* aInputStream, void* aClosure,
+ const char* aFromRawSegment, uint32_t aToOffset,
+ uint32_t aCount, uint32_t* aWriteCount) {
+ char16_t* dest = static_cast<char16_t*>(aClosure) + aToOffset;
+ PopulateBufferForBinaryString(dest, aFromRawSegment, aCount);
+ *aWriteCount = aCount;
+ return NS_OK;
+}
+
+} // namespace
+
+nsresult FileReader::DoReadData(uint64_t aCount) {
+ MOZ_ASSERT(mAsyncStream);
+
+ uint32_t bytesRead = 0;
+
+ if (mDataFormat == FILE_AS_BINARY) {
+ // Continuously update our binary string as data comes in
+ CheckedInt<uint64_t> size{mResult.Length()};
+ size += aCount;
+
+ if (!size.isValid() || size.value() > UINT32_MAX || size.value() > mTotal) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ uint32_t lenBeforeRead = mResult.Length();
+ MOZ_ASSERT(lenBeforeRead == mDataLen, "unexpected mResult length");
+
+ mResult.SetLength(lenBeforeRead + aCount);
+ char16_t* currentPos = mResult.BeginWriting() + lenBeforeRead;
+
+ if (NS_InputStreamIsBuffered(mAsyncStream)) {
+ nsresult rv = mAsyncStream->ReadSegments(ReadFuncBinaryString, currentPos,
+ aCount, &bytesRead);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+ } else {
+ while (aCount > 0) {
+ char tmpBuffer[4096];
+ uint32_t minCount =
+ XPCOM_MIN(aCount, static_cast<uint64_t>(sizeof(tmpBuffer)));
+ uint32_t read = 0;
+
+ nsresult rv = mAsyncStream->Read(tmpBuffer, minCount, &read);
+ if (rv == NS_BASE_STREAM_CLOSED) {
+ rv = NS_OK;
+ }
+
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ if (read == 0) {
+ // The stream finished too early.
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ PopulateBufferForBinaryString(currentPos, tmpBuffer, read);
+
+ currentPos += read;
+ aCount -= read;
+ bytesRead += read;
+ }
+ }
+
+ MOZ_ASSERT(size.value() == lenBeforeRead + bytesRead);
+ mResult.Truncate(size.value());
+ } else {
+ CheckedInt<uint64_t> size = mDataLen;
+ size += aCount;
+
+ // Update memory buffer to reflect the contents of the file
+ if (!size.isValid() ||
+ // PR_Realloc doesn't support over 4GB memory size even if 64-bit OS
+ // XXX: it's likely that this check is unnecessary and the comment is
+ // wrong because we no longer use PR_Realloc outside of NSPR and NSS.
+ size.value() > UINT32_MAX || size.value() > mTotal) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(mFileData);
+ MOZ_RELEASE_ASSERT((mDataLen + aCount) <= mTotal);
+
+ nsresult rv = mAsyncStream->Read(mFileData + mDataLen, aCount, &bytesRead);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ mDataLen += bytesRead;
+ return NS_OK;
+}
+
+// Helper methods
+
+void FileReader::ReadFileContent(Blob& aBlob, const nsAString& aCharset,
+ eDataFormat aDataFormat, ErrorResult& aRv) {
+ if (IsCurrentThreadRunningWorker() && !mWeakWorkerRef) {
+ // The worker is already shutting down.
+ return;
+ }
+
+ if (mReadyState == LOADING) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ mError = nullptr;
+
+ SetDOMStringToNull(mResult);
+ mResultArrayBuffer = nullptr;
+
+ mAsyncStream = nullptr;
+
+ mTransferred = 0;
+ mTotal = 0;
+ mReadyState = EMPTY;
+ FreeFileData();
+
+ mBlob = &aBlob;
+ mDataFormat = aDataFormat;
+ CopyUTF16toUTF8(aCharset, mCharset);
+
+ {
+ nsCOMPtr<nsIInputStream> stream;
+ mBlob->CreateInputStream(getter_AddRefs(stream), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ aRv = NS_MakeAsyncNonBlockingInputStream(stream.forget(),
+ getter_AddRefs(mAsyncStream));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ }
+
+ MOZ_ASSERT(mAsyncStream);
+
+ mTotal = mBlob->GetSize(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ // Binary Format doesn't need a post-processing of the data. Everything is
+ // written directly into mResult.
+ if (mDataFormat != FILE_AS_BINARY) {
+ if (mDataFormat == FILE_AS_ARRAYBUFFER) {
+ mFileData = js_pod_malloc<char>(mTotal);
+ } else {
+ mFileData = (char*)malloc(mTotal);
+ }
+
+ if (!mFileData) {
+ NS_WARNING("Preallocation failed for ReadFileData");
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ }
+
+ mAsyncWaitRunnable = new AsyncWaitRunnable(this);
+ aRv = NS_DispatchToCurrentThread(mAsyncWaitRunnable);
+ if (NS_WARN_IF(aRv.Failed())) {
+ FreeFileData();
+ return;
+ }
+
+ // FileReader should be in loading state here
+ mReadyState = LOADING;
+}
+
+void FileReader::InitialAsyncWait() {
+ mAsyncWaitRunnable = nullptr;
+
+ nsresult rv = DoAsyncWait();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mReadyState = EMPTY;
+ FreeFileData();
+ return;
+ }
+
+ DispatchProgressEvent(nsLiteralString(LOADSTART_STR));
+}
+
+nsresult FileReader::GetAsText(Blob* aBlob, const nsACString& aCharset,
+ const char* aFileData, uint32_t aDataLen,
+ nsAString& aResult) {
+ // Try the API argument.
+ const Encoding* encoding = Encoding::ForLabel(aCharset);
+ if (!encoding) {
+ // API argument failed. Try the type property of the blob.
+ nsAutoString type16;
+ aBlob->GetType(type16);
+ NS_ConvertUTF16toUTF8 type(type16);
+ nsAutoCString specifiedCharset;
+ bool haveCharset;
+ int32_t charsetStart, charsetEnd;
+ NS_ExtractCharsetFromContentType(type, specifiedCharset, &haveCharset,
+ &charsetStart, &charsetEnd);
+ encoding = Encoding::ForLabel(specifiedCharset);
+ if (!encoding) {
+ // Type property failed. Use UTF-8.
+ encoding = UTF_8_ENCODING;
+ }
+ }
+
+ auto data = Span(reinterpret_cast<const uint8_t*>(aFileData), aDataLen);
+ nsresult rv;
+ std::tie(rv, std::ignore) = encoding->Decode(data, aResult);
+ return NS_FAILED(rv) ? rv : NS_OK;
+}
+
+nsresult FileReader::GetAsDataURL(Blob* aBlob, const char* aFileData,
+ uint32_t aDataLen, nsAString& aResult) {
+ aResult.AssignLiteral("data:");
+
+ nsAutoString contentType;
+ aBlob->GetType(contentType);
+ if (!contentType.IsEmpty()) {
+ aResult.Append(contentType);
+ } else {
+ aResult.AppendLiteral("application/octet-stream");
+ }
+ aResult.AppendLiteral(";base64,");
+
+ return Base64EncodeAppend(aFileData, aDataLen, aResult);
+}
+
+/* virtual */
+JSObject* FileReader::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return FileReader_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void FileReader::StartProgressEventTimer() {
+ if (!NS_IsMainThread() && !mWeakWorkerRef) {
+ // The worker is possibly shutting down if dispatching a DOM event right
+ // before this call triggered an InterruptCallback call.
+ // XXX Note, the check is limited to workers for now, since it is unclear
+ // in the spec how FileReader should behave in this case on the main thread.
+ return;
+ }
+
+ if (!mProgressNotifier) {
+ mProgressNotifier = NS_NewTimer(mTarget);
+ }
+
+ if (mProgressNotifier) {
+ mProgressEventWasDelayed = false;
+ mTimerIsActive = true;
+ mProgressNotifier->Cancel();
+ mProgressNotifier->InitWithCallback(this, NS_PROGRESS_EVENT_INTERVAL,
+ nsITimer::TYPE_ONE_SHOT);
+ }
+}
+
+void FileReader::ClearProgressEventTimer() {
+ mProgressEventWasDelayed = false;
+ mTimerIsActive = false;
+ if (mProgressNotifier) {
+ mProgressNotifier->Cancel();
+ }
+}
+
+void FileReader::FreeFileData() {
+ if (mFileData) {
+ if (mDataFormat == FILE_AS_ARRAYBUFFER) {
+ js_free(mFileData);
+ } else {
+ free(mFileData);
+ }
+ mFileData = nullptr;
+ }
+
+ mDataLen = 0;
+}
+
+void FileReader::FreeDataAndDispatchSuccess() {
+ FreeFileData();
+ mResult.SetIsVoid(false);
+ mAsyncStream = nullptr;
+ mBlob = nullptr;
+
+ // Dispatch event to signify end of a successful operation
+ DispatchProgressEvent(nsLiteralString(LOAD_STR));
+ DispatchProgressEvent(nsLiteralString(LOADEND_STR));
+}
+
+void FileReader::FreeDataAndDispatchError() {
+ MOZ_ASSERT(mError);
+
+ FreeFileData();
+ mResult.SetIsVoid(true);
+ mAsyncStream = nullptr;
+ mBlob = nullptr;
+
+ // Dispatch error event to signify load failure
+ DispatchProgressEvent(nsLiteralString(ERROR_STR));
+ DispatchProgressEvent(nsLiteralString(LOADEND_STR));
+}
+
+void FileReader::FreeDataAndDispatchError(nsresult aRv) {
+ // Set the status attribute, and dispatch the error event
+ switch (aRv) {
+ case NS_ERROR_FILE_NOT_FOUND:
+ mError = DOMException::Create(NS_ERROR_DOM_NOT_FOUND_ERR);
+ break;
+ case NS_ERROR_FILE_ACCESS_DENIED:
+ mError = DOMException::Create(NS_ERROR_DOM_SECURITY_ERR);
+ break;
+ default:
+ mError = DOMException::Create(NS_ERROR_DOM_FILE_NOT_READABLE_ERR);
+ break;
+ }
+
+ FreeDataAndDispatchError();
+}
+
+nsresult FileReader::DispatchProgressEvent(const nsAString& aType) {
+ ProgressEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+ init.mLoaded = mTransferred;
+
+ if (mTotal != kUnknownSize) {
+ init.mLengthComputable = true;
+ init.mTotal = mTotal;
+ } else {
+ init.mLengthComputable = false;
+ init.mTotal = 0;
+ }
+ RefPtr<ProgressEvent> event = ProgressEvent::Constructor(this, aType, init);
+ event->SetTrusted(true);
+
+ ErrorResult rv;
+ DispatchEvent(*event, rv);
+ return rv.StealNSResult();
+}
+
+// nsITimerCallback
+NS_IMETHODIMP
+FileReader::Notify(nsITimer* aTimer) {
+ nsresult rv;
+ mTimerIsActive = false;
+
+ if (mProgressEventWasDelayed) {
+ rv = DispatchProgressEvent(u"progress"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ StartProgressEventTimer();
+ }
+
+ return NS_OK;
+}
+
+// InputStreamCallback
+NS_IMETHODIMP
+FileReader::OnInputStreamReady(nsIAsyncInputStream* aStream) {
+ // We use this class to decrease the busy counter at the end of this method.
+ // In theory we can do it immediatelly but, for debugging reasons, we want to
+ // be 100% sure we have a workerRef when OnLoadEnd() is called.
+ FileReaderDecreaseBusyCounter RAII(this);
+
+ if (mReadyState != LOADING || aStream != mAsyncStream) {
+ return NS_OK;
+ }
+
+ uint64_t count;
+ nsresult rv = aStream->Available(&count);
+
+ if (NS_SUCCEEDED(rv) && count) {
+ rv = DoReadData(count);
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = DoAsyncWait();
+ }
+ }
+
+ if (NS_FAILED(rv) || !count) {
+ if (rv == NS_BASE_STREAM_CLOSED) {
+ rv = NS_OK;
+ }
+ OnLoadEnd(rv);
+ return NS_OK;
+ }
+
+ mTransferred += count;
+
+ // Notify the timer is the appropriate timeframe has passed
+ if (mTimerIsActive) {
+ mProgressEventWasDelayed = true;
+ } else {
+ rv = DispatchProgressEvent(nsLiteralString(PROGRESS_STR));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ StartProgressEventTimer();
+ }
+
+ return NS_OK;
+}
+
+// nsINamed
+NS_IMETHODIMP
+FileReader::GetName(nsACString& aName) {
+ aName.AssignLiteral("FileReader");
+ return NS_OK;
+}
+
+void FileReader::OnLoadEnd(nsresult aStatus) {
+ // Cancel the progress event timer
+ ClearProgressEventTimer();
+
+ // FileReader must be in DONE stage after an operation
+ mReadyState = DONE;
+
+ // Quick return, if failed.
+ if (NS_FAILED(aStatus)) {
+ FreeDataAndDispatchError(aStatus);
+ return;
+ }
+
+ // In case we read a different number of bytes, we can assume that the
+ // underlying storage has changed. We should not continue.
+ if (mDataLen != mTotal) {
+ FreeDataAndDispatchError(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // ArrayBuffer needs a custom handling.
+ if (mDataFormat == FILE_AS_ARRAYBUFFER) {
+ OnLoadEndArrayBuffer();
+ return;
+ }
+
+ nsresult rv = NS_OK;
+
+ // We don't do anything special for Binary format.
+
+ if (mDataFormat == FILE_AS_DATAURL) {
+ rv = GetAsDataURL(mBlob, mFileData, mDataLen, mResult);
+ } else if (mDataFormat == FILE_AS_TEXT) {
+ if (!mFileData && mDataLen) {
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ } else if (!mFileData) {
+ rv = GetAsText(mBlob, mCharset, "", mDataLen, mResult);
+ } else {
+ rv = GetAsText(mBlob, mCharset, mFileData, mDataLen, mResult);
+ }
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ FreeDataAndDispatchError(rv);
+ return;
+ }
+
+ FreeDataAndDispatchSuccess();
+}
+
+void FileReader::Abort() {
+ if (mReadyState == EMPTY || mReadyState == DONE) {
+ return;
+ }
+
+ MOZ_ASSERT(mReadyState == LOADING);
+
+ Cleanup();
+
+ // XXX The spec doesn't say this
+ mError = DOMException::Create(NS_ERROR_DOM_ABORT_ERR);
+
+ // Revert status and result attributes
+ SetDOMStringToNull(mResult);
+ mResultArrayBuffer = nullptr;
+
+ mBlob = nullptr;
+
+ // Dispatch the events
+ DispatchProgressEvent(nsLiteralString(ABORT_STR));
+ DispatchProgressEvent(nsLiteralString(LOADEND_STR));
+}
+
+nsresult FileReader::IncreaseBusyCounter() {
+ if (mWeakWorkerRef && mBusyCount++ == 0) {
+ if (NS_WARN_IF(!mWeakWorkerRef->GetPrivate())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<FileReader> self = this;
+
+ RefPtr<StrongWorkerRef> ref =
+ StrongWorkerRef::Create(mWeakWorkerRef->GetPrivate(), "FileReader",
+ [self]() { self->Shutdown(); });
+ if (NS_WARN_IF(!ref)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mStrongWorkerRef = ref;
+ }
+
+ return NS_OK;
+}
+
+void FileReader::DecreaseBusyCounter() {
+ MOZ_ASSERT_IF(mStrongWorkerRef, mBusyCount);
+ if (mStrongWorkerRef && --mBusyCount == 0) {
+ mStrongWorkerRef = nullptr;
+ }
+}
+
+void FileReader::Cleanup() {
+ mReadyState = DONE;
+
+ if (mAsyncWaitRunnable) {
+ mAsyncWaitRunnable->Cancel();
+ mAsyncWaitRunnable = nullptr;
+ }
+
+ if (mAsyncStream) {
+ mAsyncStream->Close();
+ mAsyncStream = nullptr;
+ }
+
+ ClearProgressEventTimer();
+ FreeFileData();
+ mResultArrayBuffer = nullptr;
+}
+
+void FileReader::Shutdown() {
+ Cleanup();
+ if (mWeakWorkerRef) {
+ mWeakWorkerRef = nullptr;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/FileReader.h b/dom/file/FileReader.h
new file mode 100644
index 0000000000..1c07b6193f
--- /dev/null
+++ b/dom/file/FileReader.h
@@ -0,0 +1,207 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileReader_h
+#define mozilla_dom_FileReader_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+
+#include "nsIAsyncInputStream.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsINamed.h"
+#include "nsITimer.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsWeakReference.h"
+
+#define NS_PROGRESS_EVENT_INTERVAL 50
+
+class nsITimer;
+class nsIEventTarget;
+
+namespace mozilla::dom {
+
+class Blob;
+class DOMException;
+class OwningStringOrArrayBuffer;
+class StrongWorkerRef;
+class WeakWorkerRef;
+
+extern const uint64_t kUnknownSize;
+
+class FileReaderDecreaseBusyCounter;
+
+// 26a79031-c94b-47e9-850a-f04fe17bc026
+#define FILEREADER_ID \
+ { \
+ 0x26a79031, 0xc94b, 0x47e9, { \
+ 0x85, 0x0a, 0xf0, 0x4f, 0xe1, 0x7b, 0xc0, 0x26 \
+ } \
+ }
+
+class FileReader final : public DOMEventTargetHelper,
+ public nsIInterfaceRequestor,
+ public nsSupportsWeakReference,
+ public nsIInputStreamCallback,
+ public nsITimerCallback,
+ public nsINamed {
+ friend class FileReaderDecreaseBusyCounter;
+
+ public:
+ FileReader(nsIGlobalObject* aGlobal, WeakWorkerRef* aWorkerRef);
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSIINPUTSTREAMCALLBACK
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSINAMED
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(FILEREADER_ID)
+
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(FileReader,
+ DOMEventTargetHelper)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL
+ static already_AddRefed<FileReader> Constructor(const GlobalObject& aGlobal);
+ void ReadAsArrayBuffer(JSContext* aCx, Blob& aBlob, ErrorResult& aRv) {
+ ReadFileContent(aBlob, u""_ns, FILE_AS_ARRAYBUFFER, aRv);
+ }
+
+ void ReadAsText(Blob& aBlob, const Optional<nsAString>& aLabel,
+ ErrorResult& aRv) {
+ if (aLabel.WasPassed()) {
+ ReadFileContent(aBlob, aLabel.Value(), FILE_AS_TEXT, aRv);
+ } else {
+ ReadFileContent(aBlob, u""_ns, FILE_AS_TEXT, aRv);
+ }
+ }
+
+ void ReadAsDataURL(Blob& aBlob, ErrorResult& aRv) {
+ ReadFileContent(aBlob, u""_ns, FILE_AS_DATAURL, aRv);
+ }
+
+ void Abort();
+
+ uint16_t ReadyState() const { return static_cast<uint16_t>(mReadyState); }
+
+ DOMException* GetError() const { return mError; }
+
+ void GetResult(JSContext* aCx, Nullable<OwningStringOrArrayBuffer>& aResult);
+
+ IMPL_EVENT_HANDLER(loadstart)
+ IMPL_EVENT_HANDLER(progress)
+ IMPL_EVENT_HANDLER(load)
+ IMPL_EVENT_HANDLER(abort)
+ IMPL_EVENT_HANDLER(error)
+ IMPL_EVENT_HANDLER(loadend)
+
+ void ReadAsBinaryString(Blob& aBlob, ErrorResult& aRv) {
+ ReadFileContent(aBlob, u""_ns, FILE_AS_BINARY, aRv);
+ }
+
+ enum eDataFormat {
+ FILE_AS_ARRAYBUFFER,
+ FILE_AS_BINARY,
+ FILE_AS_TEXT,
+ FILE_AS_DATAURL
+ };
+
+ eDataFormat DataFormat() const { return mDataFormat; }
+ const nsString& Result() const { return mResult; }
+
+ void InitialAsyncWait();
+
+ private:
+ ~FileReader() override;
+
+ // This must be in sync with dom/webidl/FileReader.webidl
+ enum eReadyState { EMPTY = 0, LOADING = 1, DONE = 2 };
+
+ void RootResultArrayBuffer();
+
+ void ReadFileContent(Blob& aBlob, const nsAString& aCharset,
+ eDataFormat aDataFormat, ErrorResult& aRv);
+ nsresult GetAsText(Blob* aBlob, const nsACString& aCharset,
+ const char* aFileData, uint32_t aDataLen,
+ nsAString& aResult);
+ nsresult GetAsDataURL(Blob* aBlob, const char* aFileData, uint32_t aDataLen,
+ nsAString& aResult);
+
+ void OnLoadEnd(nsresult aStatus);
+
+ void StartProgressEventTimer();
+ void ClearProgressEventTimer();
+
+ void FreeDataAndDispatchSuccess();
+ void FreeDataAndDispatchError();
+ void FreeDataAndDispatchError(nsresult aRv);
+ nsresult DispatchProgressEvent(const nsAString& aType);
+
+ nsresult DoAsyncWait();
+ nsresult DoReadData(uint64_t aCount);
+
+ void OnLoadEndArrayBuffer();
+
+ void FreeFileData();
+
+ nsresult IncreaseBusyCounter();
+ void DecreaseBusyCounter();
+
+ void Cleanup();
+ void Shutdown();
+
+ char* mFileData;
+ RefPtr<Blob> mBlob;
+ nsCString mCharset;
+ uint32_t mDataLen;
+
+ eDataFormat mDataFormat;
+
+ nsString mResult;
+
+ JS::Heap<JSObject*> mResultArrayBuffer;
+
+ nsCOMPtr<nsITimer> mProgressNotifier;
+ bool mProgressEventWasDelayed;
+ bool mTimerIsActive;
+
+ nsCOMPtr<nsIAsyncInputStream> mAsyncStream;
+
+ RefPtr<DOMException> mError;
+
+ eReadyState mReadyState;
+
+ uint64_t mTotal;
+ uint64_t mTransferred;
+
+ nsCOMPtr<nsIEventTarget> mTarget;
+
+ uint64_t mBusyCount;
+
+ // This is set if FileReader is created on workers, but it is null if the
+ // worker is shutting down. The null value is checked in ReadFileContent()
+ // before starting any reading.
+ RefPtr<WeakWorkerRef> mWeakWorkerRef;
+
+ // This value is set when the reading starts in order to keep the worker alive
+ // during the process.
+ RefPtr<StrongWorkerRef> mStrongWorkerRef;
+
+ // Runnable to start the reading asynchronous.
+ class AsyncWaitRunnable;
+ RefPtr<AsyncWaitRunnable> mAsyncWaitRunnable;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(FileReader, FILEREADER_ID)
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileReader_h
diff --git a/dom/file/FileReaderSync.cpp b/dom/file/FileReaderSync.cpp
new file mode 100644
index 0000000000..59a925f439
--- /dev/null
+++ b/dom/file/FileReaderSync.cpp
@@ -0,0 +1,489 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "FileReaderSync.h"
+
+#include "js/ArrayBuffer.h" // JS::NewArrayBufferWithContents
+#include "js/RootingAPI.h" // JS::{,Mutable}Handle
+#include "js/Utility.h" // js::ArrayBufferContentsArena, JS::FreePolicy, js_pod_arena_malloc
+#include "mozilla/Unused.h"
+#include "mozilla/Base64.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/dom/FileReaderSyncBinding.h"
+#include "nsCExternalHandlerService.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsError.h"
+#include "nsIConverterInputStream.h"
+#include "nsIInputStream.h"
+#include "nsIMultiplexInputStream.h"
+#include "nsStreamUtils.h"
+#include "nsStringStream.h"
+#include "nsISupportsImpl.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIAsyncInputStream.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerRunnable.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using mozilla::dom::GlobalObject;
+using mozilla::dom::Optional;
+
+// static
+already_AddRefed<FileReaderSync> FileReaderSync::Constructor(
+ const GlobalObject& aGlobal) {
+ RefPtr<FileReaderSync> frs = new FileReaderSync();
+
+ return frs.forget();
+}
+
+bool FileReaderSync::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto,
+ JS::MutableHandle<JSObject*> aReflector) {
+ return FileReaderSync_Binding::Wrap(aCx, this, aGivenProto, aReflector);
+}
+
+void FileReaderSync::ReadAsArrayBuffer(JSContext* aCx,
+ JS::Handle<JSObject*> aScopeObj,
+ Blob& aBlob,
+ JS::MutableHandle<JSObject*> aRetval,
+ ErrorResult& aRv) {
+ uint64_t blobSize = aBlob.GetSize(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ UniquePtr<char[], JS::FreePolicy> bufferData(
+ js_pod_arena_malloc<char>(js::ArrayBufferContentsArena, blobSize));
+ if (!bufferData) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ nsCOMPtr<nsIInputStream> stream;
+ aBlob.CreateInputStream(getter_AddRefs(stream), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ uint32_t numRead;
+ aRv = SyncRead(stream, bufferData.get(), blobSize, &numRead);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ // The file is changed in the meantime?
+ if (numRead != blobSize) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ JSObject* arrayBuffer =
+ JS::NewArrayBufferWithContents(aCx, blobSize, bufferData.get());
+ if (!arrayBuffer) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ // arrayBuffer takes the ownership when it is not null. Otherwise we
+ // need to release it explicitly.
+ (void)bufferData.release();
+
+ aRetval.set(arrayBuffer);
+}
+
+void FileReaderSync::ReadAsBinaryString(Blob& aBlob, nsAString& aResult,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIInputStream> stream;
+ aBlob.CreateInputStream(getter_AddRefs(stream), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ uint32_t numRead;
+ do {
+ char readBuf[4096];
+ aRv = SyncRead(stream, readBuf, sizeof(readBuf), &numRead);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ uint32_t oldLength = aResult.Length();
+ AppendASCIItoUTF16(Substring(readBuf, readBuf + numRead), aResult);
+ if (aResult.Length() - oldLength != numRead) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ } while (numRead > 0);
+}
+
+void FileReaderSync::ReadAsText(Blob& aBlob,
+ const Optional<nsAString>& aEncoding,
+ nsAString& aResult, ErrorResult& aRv) {
+ nsCOMPtr<nsIInputStream> stream;
+ aBlob.CreateInputStream(getter_AddRefs(stream), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsCString sniffBuf;
+ if (!sniffBuf.SetLength(3, fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ uint32_t numRead = 0;
+ aRv = SyncRead(stream, sniffBuf.BeginWriting(), sniffBuf.Length(), &numRead);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ // No data, we don't need to continue.
+ if (numRead == 0) {
+ aResult.Truncate();
+ return;
+ }
+
+ // Try the API argument.
+ const Encoding* encoding =
+ aEncoding.WasPassed() ? Encoding::ForLabel(aEncoding.Value()) : nullptr;
+ if (!encoding) {
+ // API argument failed. Try the type property of the blob.
+ nsAutoString type16;
+ aBlob.GetType(type16);
+ NS_ConvertUTF16toUTF8 type(type16);
+ nsAutoCString specifiedCharset;
+ bool haveCharset;
+ int32_t charsetStart, charsetEnd;
+ NS_ExtractCharsetFromContentType(type, specifiedCharset, &haveCharset,
+ &charsetStart, &charsetEnd);
+ encoding = Encoding::ForLabel(specifiedCharset);
+ if (!encoding) {
+ // Type property failed. Use UTF-8.
+ encoding = UTF_8_ENCODING;
+ }
+ }
+
+ if (numRead < sniffBuf.Length()) {
+ sniffBuf.Truncate(numRead);
+ }
+
+ // Let's recreate the full stream using a:
+ // multiplexStream(syncStream + original stream)
+ // In theory, we could try to see if the inputStream is a nsISeekableStream,
+ // but this doesn't work correctly for nsPipe3 - See bug 1349570.
+
+ nsCOMPtr<nsIMultiplexInputStream> multiplexStream =
+ do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1");
+ if (NS_WARN_IF(!multiplexStream)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ nsCOMPtr<nsIInputStream> sniffStringStream;
+ aRv = NS_NewCStringInputStream(getter_AddRefs(sniffStringStream), sniffBuf);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ aRv = multiplexStream->AppendStream(sniffStringStream);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ uint64_t blobSize = aBlob.GetSize(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsCOMPtr<nsIInputStream> syncStream;
+ aRv = ConvertAsyncToSyncStream(blobSize - sniffBuf.Length(), stream.forget(),
+ getter_AddRefs(syncStream));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ // ConvertAsyncToSyncStream returns a null syncStream if the stream has been
+ // already closed or there is nothing to read.
+ if (syncStream) {
+ aRv = multiplexStream->AppendStream(syncStream);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ }
+
+ nsAutoCString charset;
+ encoding->Name(charset);
+
+ nsCOMPtr<nsIInputStream> multiplex(do_QueryInterface(multiplexStream));
+ aRv = ConvertStream(multiplex, charset.get(), aResult);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+}
+
+void FileReaderSync::ReadAsDataURL(Blob& aBlob, nsAString& aResult,
+ ErrorResult& aRv) {
+ nsAutoString scratchResult;
+ scratchResult.AssignLiteral("data:");
+
+ nsString contentType;
+ aBlob.GetType(contentType);
+
+ if (contentType.IsEmpty()) {
+ scratchResult.AppendLiteral("application/octet-stream");
+ } else {
+ scratchResult.Append(contentType);
+ }
+ scratchResult.AppendLiteral(";base64,");
+
+ nsCOMPtr<nsIInputStream> stream;
+ aBlob.CreateInputStream(getter_AddRefs(stream), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ uint64_t blobSize = aBlob.GetSize(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsCOMPtr<nsIInputStream> syncStream;
+ aRv = ConvertAsyncToSyncStream(blobSize, stream.forget(),
+ getter_AddRefs(syncStream));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ MOZ_ASSERT(syncStream);
+
+ uint64_t size;
+ aRv = syncStream->Available(&size);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ // The file is changed in the meantime?
+ if (blobSize != size) {
+ return;
+ }
+
+ nsAutoString encodedData;
+ aRv = Base64EncodeInputStream(syncStream, encodedData, size);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ scratchResult.Append(encodedData);
+
+ aResult = scratchResult;
+}
+
+nsresult FileReaderSync::ConvertStream(nsIInputStream* aStream,
+ const char* aCharset,
+ nsAString& aResult) {
+ nsCOMPtr<nsIConverterInputStream> converterStream =
+ do_CreateInstance("@mozilla.org/intl/converter-input-stream;1");
+ NS_ENSURE_TRUE(converterStream, NS_ERROR_FAILURE);
+
+ nsresult rv = converterStream->Init(
+ aStream, aCharset, 8192,
+ nsIConverterInputStream::DEFAULT_REPLACEMENT_CHARACTER);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIUnicharInputStream> unicharStream = converterStream;
+ NS_ENSURE_TRUE(unicharStream, NS_ERROR_FAILURE);
+
+ uint32_t numChars;
+ nsString result;
+ while (NS_SUCCEEDED(unicharStream->ReadString(8192, result, &numChars)) &&
+ numChars > 0) {
+ uint32_t oldLength = aResult.Length();
+ aResult.Append(result);
+ if (aResult.Length() - oldLength != result.Length()) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ return rv;
+}
+
+namespace {
+
+// This runnable is used to terminate the sync event loop.
+class ReadReadyRunnable final : public WorkerSyncRunnable {
+ public:
+ ReadReadyRunnable(WorkerPrivate* aWorkerPrivate,
+ nsIEventTarget* aSyncLoopTarget)
+ : WorkerSyncRunnable(aWorkerPrivate, aSyncLoopTarget) {}
+
+ bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(mSyncLoopTarget);
+
+ nsCOMPtr<nsIEventTarget> syncLoopTarget;
+ mSyncLoopTarget.swap(syncLoopTarget);
+
+ aWorkerPrivate->StopSyncLoop(syncLoopTarget, NS_OK);
+ return true;
+ }
+
+ private:
+ ~ReadReadyRunnable() override = default;
+};
+
+// This class implements nsIInputStreamCallback and it will be called when the
+// stream is ready to be read.
+class ReadCallback final : public nsIInputStreamCallback {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ ReadCallback(WorkerPrivate* aWorkerPrivate, nsIEventTarget* aEventTarget)
+ : mWorkerPrivate(aWorkerPrivate), mEventTarget(aEventTarget) {}
+
+ NS_IMETHOD
+ OnInputStreamReady(nsIAsyncInputStream* aStream) override {
+ // I/O Thread. Now we need to block the sync event loop.
+ RefPtr<ReadReadyRunnable> runnable =
+ new ReadReadyRunnable(mWorkerPrivate, mEventTarget);
+ return mEventTarget->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+ }
+
+ private:
+ ~ReadCallback() = default;
+
+ // The worker is kept alive because of the sync event loop.
+ WorkerPrivate* mWorkerPrivate;
+ nsCOMPtr<nsIEventTarget> mEventTarget;
+};
+
+NS_IMPL_ADDREF(ReadCallback);
+NS_IMPL_RELEASE(ReadCallback);
+
+NS_INTERFACE_MAP_BEGIN(ReadCallback)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStreamCallback)
+NS_INTERFACE_MAP_END
+
+} // namespace
+
+nsresult FileReaderSync::SyncRead(nsIInputStream* aStream, char* aBuffer,
+ uint32_t aBufferSize,
+ uint32_t* aTotalBytesRead) {
+ MOZ_ASSERT(aStream);
+ MOZ_ASSERT(aBuffer);
+ MOZ_ASSERT(aTotalBytesRead);
+
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+
+ *aTotalBytesRead = 0;
+
+ nsCOMPtr<nsIAsyncInputStream> asyncStream;
+ nsCOMPtr<nsIEventTarget> target;
+
+ while (*aTotalBytesRead < aBufferSize) {
+ uint32_t currentBytesRead = 0;
+
+ // Let's read something.
+ nsresult rv =
+ aStream->Read(aBuffer + *aTotalBytesRead,
+ aBufferSize - *aTotalBytesRead, &currentBytesRead);
+
+ // Nothing else to read.
+ if (rv == NS_BASE_STREAM_CLOSED ||
+ (NS_SUCCEEDED(rv) && currentBytesRead == 0)) {
+ return NS_OK;
+ }
+
+ // An error.
+ if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) {
+ return rv;
+ }
+
+ // All good.
+ if (NS_SUCCEEDED(rv)) {
+ *aTotalBytesRead += currentBytesRead;
+ continue;
+ }
+
+ // We need to proceed async.
+ if (!asyncStream) {
+ asyncStream = do_QueryInterface(aStream);
+ if (!asyncStream) {
+ return rv;
+ }
+ }
+
+ AutoSyncLoopHolder syncLoop(workerPrivate, Canceling);
+
+ nsCOMPtr<nsISerialEventTarget> syncLoopTarget =
+ syncLoop.GetSerialEventTarget();
+ if (!syncLoopTarget) {
+ // SyncLoop creation can fail if the worker is shutting down.
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ RefPtr<ReadCallback> callback =
+ new ReadCallback(workerPrivate, syncLoopTarget);
+
+ if (!target) {
+ target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ MOZ_ASSERT(target);
+ }
+
+ rv = asyncStream->AsyncWait(callback, 0, aBufferSize - *aTotalBytesRead,
+ target);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(syncLoop.Run()))) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult FileReaderSync::ConvertAsyncToSyncStream(
+ uint64_t aStreamSize, already_AddRefed<nsIInputStream> aAsyncStream,
+ nsIInputStream** aSyncStream) {
+ nsCOMPtr<nsIInputStream> asyncInputStream = std::move(aAsyncStream);
+
+ // If the stream is not async, we just need it to be bufferable.
+ nsCOMPtr<nsIAsyncInputStream> asyncStream =
+ do_QueryInterface(asyncInputStream);
+ if (!asyncStream) {
+ return NS_NewBufferedInputStream(aSyncStream, asyncInputStream.forget(),
+ 4096);
+ }
+
+ nsAutoCString buffer;
+ if (!buffer.SetLength(aStreamSize, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ uint32_t read;
+ nsresult rv =
+ SyncRead(asyncInputStream, buffer.BeginWriting(), aStreamSize, &read);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (read != aStreamSize) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = NS_NewCStringInputStream(aSyncStream, std::move(buffer));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
diff --git a/dom/file/FileReaderSync.h b/dom/file/FileReaderSync.h
new file mode 100644
index 0000000000..84eb5a73a8
--- /dev/null
+++ b/dom/file/FileReaderSync.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_filereadersync_h__
+#define mozilla_dom_filereadersync_h__
+
+#include "mozilla/dom/WorkerCommon.h"
+#include "nsISupports.h"
+
+class nsIInputStream;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+class Blob;
+class GlobalObject;
+template <typename>
+class Optional;
+
+class FileReaderSync final {
+ NS_INLINE_DECL_REFCOUNTING(FileReaderSync)
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~FileReaderSync() = default;
+
+ nsresult ConvertStream(nsIInputStream* aStream, const char* aCharset,
+ nsAString& aResult);
+
+ nsresult ConvertAsyncToSyncStream(
+ uint64_t aStreamSize, already_AddRefed<nsIInputStream> aAsyncStream,
+ nsIInputStream** aSyncStream);
+
+ nsresult SyncRead(nsIInputStream* aStream, char* aBuffer,
+ uint32_t aBufferSize, uint32_t* aTotalBytesRead);
+
+ public:
+ static already_AddRefed<FileReaderSync> Constructor(
+ const GlobalObject& aGlobal);
+
+ bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto,
+ JS::MutableHandle<JSObject*> aReflector);
+
+ void ReadAsArrayBuffer(JSContext* aCx, JS::Handle<JSObject*> aScopeObj,
+ Blob& aBlob, JS::MutableHandle<JSObject*> aRetval,
+ ErrorResult& aRv);
+ void ReadAsBinaryString(Blob& aBlob, nsAString& aResult, ErrorResult& aRv);
+ void ReadAsText(Blob& aBlob, const Optional<nsAString>& aEncoding,
+ nsAString& aResult, ErrorResult& aRv);
+ void ReadAsDataURL(Blob& aBlob, nsAString& aResult, ErrorResult& aRv);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_filereadersync_h__
diff --git a/dom/file/MemoryBlobImpl.cpp b/dom/file/MemoryBlobImpl.cpp
new file mode 100644
index 0000000000..b6d5c442d1
--- /dev/null
+++ b/dom/file/MemoryBlobImpl.cpp
@@ -0,0 +1,168 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "MemoryBlobImpl.h"
+#include "mozilla/ipc/InputStreamUtils.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/SHA1.h"
+#include "nsIMemoryReporter.h"
+#include "nsPrintfCString.h"
+#include "nsRFPService.h"
+#include "nsStringStream.h"
+#include "prtime.h"
+
+namespace mozilla::dom {
+
+// static
+already_AddRefed<MemoryBlobImpl> MemoryBlobImpl::CreateWithCustomLastModified(
+ void* aMemoryBuffer, uint64_t aLength, const nsAString& aName,
+ const nsAString& aContentType, int64_t aLastModifiedDate) {
+ RefPtr<MemoryBlobImpl> blobImpl = new MemoryBlobImpl(
+ aMemoryBuffer, aLength, aName, aContentType, aLastModifiedDate);
+ return blobImpl.forget();
+}
+
+// static
+already_AddRefed<MemoryBlobImpl> MemoryBlobImpl::CreateWithLastModifiedNow(
+ void* aMemoryBuffer, uint64_t aLength, const nsAString& aName,
+ const nsAString& aContentType, RTPCallerType aRTPCallerType) {
+ int64_t lastModificationDate =
+ nsRFPService::ReduceTimePrecisionAsUSecs(PR_Now(), 0, aRTPCallerType);
+ return CreateWithCustomLastModified(aMemoryBuffer, aLength, aName,
+ aContentType, lastModificationDate);
+}
+
+nsresult MemoryBlobImpl::DataOwnerAdapter::Create(DataOwner* aDataOwner,
+ size_t aStart, size_t aLength,
+ nsIInputStream** _retval) {
+ MOZ_ASSERT(aDataOwner, "Uh ...");
+ Span data{static_cast<const char*>(aDataOwner->mData) + aStart, aLength};
+ RefPtr adapter = new MemoryBlobImpl::DataOwnerAdapter(aDataOwner, data);
+ return NS_NewByteInputStream(_retval, adapter);
+}
+
+already_AddRefed<BlobImpl> MemoryBlobImpl::CreateSlice(
+ uint64_t aStart, uint64_t aLength, const nsAString& aContentType,
+ ErrorResult& aRv) const {
+ RefPtr<BlobImpl> impl =
+ new MemoryBlobImpl(this, aStart, aLength, aContentType);
+ return impl.forget();
+}
+
+void MemoryBlobImpl::CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const {
+ if (mLength >= INT32_MAX) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ aRv = MemoryBlobImpl::DataOwnerAdapter::Create(mDataOwner, mStart, mLength,
+ aStream);
+}
+
+/* static */
+StaticMutex MemoryBlobImpl::DataOwner::sDataOwnerMutex;
+
+/* static */ StaticAutoPtr<LinkedList<MemoryBlobImpl::DataOwner>>
+ MemoryBlobImpl::DataOwner::sDataOwners;
+
+/* static */
+bool MemoryBlobImpl::DataOwner::sMemoryReporterRegistered = false;
+
+MOZ_DEFINE_MALLOC_SIZE_OF(MemoryFileDataOwnerMallocSizeOf)
+
+class MemoryBlobImplDataOwnerMemoryReporter final : public nsIMemoryReporter {
+ ~MemoryBlobImplDataOwnerMemoryReporter() = default;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) override {
+ using DataOwner = MemoryBlobImpl::DataOwner;
+
+ StaticMutexAutoLock lock(DataOwner::sDataOwnerMutex);
+
+ if (!DataOwner::sDataOwners) {
+ return NS_OK;
+ }
+
+ const size_t LARGE_OBJECT_MIN_SIZE = 8 * 1024;
+ size_t smallObjectsTotal = 0;
+
+ for (DataOwner* owner = DataOwner::sDataOwners->getFirst(); owner;
+ owner = owner->getNext()) {
+ size_t size = MemoryFileDataOwnerMallocSizeOf(owner->mData);
+
+ if (size < LARGE_OBJECT_MIN_SIZE) {
+ smallObjectsTotal += size;
+ } else {
+ SHA1Sum sha1;
+ sha1.update(owner->mData, owner->mLength);
+ uint8_t digest[SHA1Sum::kHashSize]; // SHA1 digests are 20 bytes long.
+ sha1.finish(digest);
+
+ nsAutoCString digestString;
+ for (size_t i = 0; i < sizeof(digest); i++) {
+ digestString.AppendPrintf("%02x", digest[i]);
+ }
+
+ aHandleReport->Callback(
+ /* process */ ""_ns,
+ nsPrintfCString(
+ "explicit/dom/memory-file-data/large/file(length=%" PRIu64
+ ", sha1=%s)",
+ owner->mLength,
+ aAnonymize ? "<anonymized>" : digestString.get()),
+ KIND_HEAP, UNITS_BYTES, size,
+ nsPrintfCString(
+ "Memory used to back a memory file of length %" PRIu64
+ " bytes. The file "
+ "has a sha1 of %s.\n\n"
+ "Note that the allocator may round up a memory file's length "
+ "-- "
+ "that is, an N-byte memory file may take up more than N bytes "
+ "of "
+ "memory.",
+ owner->mLength, digestString.get()),
+ aData);
+ }
+ }
+
+ if (smallObjectsTotal > 0) {
+ aHandleReport->Callback(
+ /* process */ ""_ns, "explicit/dom/memory-file-data/small"_ns,
+ KIND_HEAP, UNITS_BYTES, smallObjectsTotal,
+ nsPrintfCString(
+ "Memory used to back small memory files (i.e. those taking up "
+ "less "
+ "than %zu bytes of memory each).\n\n"
+ "Note that the allocator may round up a memory file's length -- "
+ "that is, an N-byte memory file may take up more than N bytes of "
+ "memory.",
+ LARGE_OBJECT_MIN_SIZE),
+ aData);
+ }
+
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(MemoryBlobImplDataOwnerMemoryReporter, nsIMemoryReporter)
+
+/* static */
+void MemoryBlobImpl::DataOwner::EnsureMemoryReporterRegistered() {
+ sDataOwnerMutex.AssertCurrentThreadOwns();
+ if (sMemoryReporterRegistered) {
+ return;
+ }
+
+ RegisterStrongMemoryReporter(new MemoryBlobImplDataOwnerMemoryReporter());
+
+ sMemoryReporterRegistered = true;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/MemoryBlobImpl.h b/dom/file/MemoryBlobImpl.h
new file mode 100644
index 0000000000..29d812feee
--- /dev/null
+++ b/dom/file/MemoryBlobImpl.h
@@ -0,0 +1,160 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MemoryBlobImpl_h
+#define mozilla_dom_MemoryBlobImpl_h
+
+#include "mozilla/dom/BaseBlobImpl.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/StreamBufferSource.h"
+#include "nsCOMPtr.h"
+#include "nsICloneableInputStream.h"
+#include "nsIInputStream.h"
+#include "nsIIPCSerializableInputStream.h"
+#include "nsISeekableStream.h"
+
+namespace mozilla::dom {
+
+class MemoryBlobImpl final : public BaseBlobImpl {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(MemoryBlobImpl, BaseBlobImpl)
+
+ // File constructor.
+ static already_AddRefed<MemoryBlobImpl> CreateWithLastModifiedNow(
+ void* aMemoryBuffer, uint64_t aLength, const nsAString& aName,
+ const nsAString& aContentType, RTPCallerType aRTPCallerType);
+
+ // File constructor with custom lastModified attribue value. You should
+ // probably use CreateWithLastModifiedNow() instead of this one.
+ static already_AddRefed<MemoryBlobImpl> CreateWithCustomLastModified(
+ void* aMemoryBuffer, uint64_t aLength, const nsAString& aName,
+ const nsAString& aContentType, int64_t aLastModifiedDate);
+
+ // Blob constructor.
+ MemoryBlobImpl(void* aMemoryBuffer, uint64_t aLength,
+ const nsAString& aContentType)
+ : BaseBlobImpl(aContentType, aLength),
+ mDataOwner(new DataOwner(aMemoryBuffer, aLength)) {
+ MOZ_ASSERT(mDataOwner && mDataOwner->mData, "must have data");
+ }
+
+ void CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const override;
+
+ already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength,
+ const nsAString& aContentType,
+ ErrorResult& aRv) const override;
+
+ bool IsMemoryFile() const override { return true; }
+
+ size_t GetAllocationSize() const override { return mLength; }
+
+ size_t GetAllocationSize(
+ FallibleTArray<BlobImpl*>& aVisitedBlobImpls) const override {
+ return GetAllocationSize();
+ }
+
+ void GetBlobImplType(nsAString& aBlobImplType) const override {
+ aBlobImplType = u"MemoryBlobImpl"_ns;
+ }
+
+ class DataOwner final : public mozilla::LinkedListElement<DataOwner> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DataOwner)
+ DataOwner(void* aMemoryBuffer, uint64_t aLength)
+ : mData(aMemoryBuffer), mLength(aLength) {
+ mozilla::StaticMutexAutoLock lock(sDataOwnerMutex);
+
+ if (!sDataOwners) {
+ sDataOwners = new mozilla::LinkedList<DataOwner>();
+ EnsureMemoryReporterRegistered();
+ }
+ sDataOwners->insertBack(this);
+ }
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~DataOwner() {
+ mozilla::StaticMutexAutoLock lock(sDataOwnerMutex);
+
+ remove();
+ if (sDataOwners->isEmpty()) {
+ // Free the linked list if it's empty.
+ sDataOwners = nullptr;
+ }
+
+ free(mData);
+ }
+
+ public:
+ static void EnsureMemoryReporterRegistered();
+
+ // sDataOwners and sMemoryReporterRegistered may only be accessed while
+ // holding sDataOwnerMutex! You also must hold the mutex while touching
+ // elements of the linked list that DataOwner inherits from.
+ static mozilla::StaticMutex sDataOwnerMutex MOZ_UNANNOTATED;
+ static mozilla::StaticAutoPtr<mozilla::LinkedList<DataOwner> > sDataOwners;
+ static bool sMemoryReporterRegistered;
+
+ void* mData;
+ uint64_t mLength;
+ };
+
+ class DataOwnerAdapter final : public StreamBufferSource {
+ using DataOwner = MemoryBlobImpl::DataOwner;
+
+ public:
+ static nsresult Create(DataOwner* aDataOwner, size_t aStart, size_t aLength,
+ nsIInputStream** _retval);
+
+ Span<const char> Data() override { return mData; }
+
+ // This StreamBufferSource is owning, as the `mData` span references the
+ // immutable data buffer owned by `mDataOwner` which is being kept alive.
+ bool Owning() override { return true; }
+
+ // The memory usage from `DataOwner` is reported elsewhere, so we don't need
+ // to record it here.
+ size_t SizeOfExcludingThisEvenIfShared(MallocSizeOf) override { return 0; }
+
+ private:
+ ~DataOwnerAdapter() override = default;
+
+ DataOwnerAdapter(DataOwner* aDataOwner, Span<const char> aData)
+ : mDataOwner(aDataOwner), mData(aData) {}
+
+ RefPtr<DataOwner> mDataOwner;
+ Span<const char> mData;
+ };
+
+ private:
+ // File constructor.
+ MemoryBlobImpl(void* aMemoryBuffer, uint64_t aLength, const nsAString& aName,
+ const nsAString& aContentType, int64_t aLastModifiedDate)
+ : BaseBlobImpl(aName, aContentType, aLength, aLastModifiedDate),
+ mDataOwner(new DataOwner(aMemoryBuffer, aLength)) {
+ MOZ_ASSERT(mDataOwner && mDataOwner->mData, "must have data");
+ }
+
+ // Create slice
+ MemoryBlobImpl(const MemoryBlobImpl* aOther, uint64_t aStart,
+ uint64_t aLength, const nsAString& aContentType)
+ : BaseBlobImpl(aContentType, aOther->mStart + aStart, aLength),
+ mDataOwner(aOther->mDataOwner) {
+ MOZ_ASSERT(mDataOwner && mDataOwner->mData, "must have data");
+ }
+
+ ~MemoryBlobImpl() override = default;
+
+ // Used when backed by a memory store
+ RefPtr<DataOwner> mDataOwner;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MemoryBlobImpl_h
diff --git a/dom/file/MultipartBlobImpl.cpp b/dom/file/MultipartBlobImpl.cpp
new file mode 100644
index 0000000000..b795793aef
--- /dev/null
+++ b/dom/file/MultipartBlobImpl.cpp
@@ -0,0 +1,338 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "MultipartBlobImpl.h"
+#include "jsfriendapi.h"
+#include "mozilla/dom/BlobSet.h"
+#include "mozilla/dom/FileBinding.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIGlobalObject.h"
+#include "nsIMultiplexInputStream.h"
+#include "nsReadableUtils.h"
+#include "nsRFPService.h"
+#include "nsStringStream.h"
+#include "nsTArray.h"
+#include "nsJSUtils.h"
+#include "nsContentUtils.h"
+#include <algorithm>
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+/* static */
+already_AddRefed<MultipartBlobImpl> MultipartBlobImpl::Create(
+ nsTArray<RefPtr<BlobImpl>>&& aBlobImpls, const nsAString& aName,
+ const nsAString& aContentType, RTPCallerType aRTPCallerType,
+ ErrorResult& aRv) {
+ RefPtr<MultipartBlobImpl> blobImpl =
+ new MultipartBlobImpl(std::move(aBlobImpls), aName, aContentType);
+ blobImpl->SetLengthAndModifiedDate(Some(aRTPCallerType), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return blobImpl.forget();
+}
+
+/* static */
+already_AddRefed<MultipartBlobImpl> MultipartBlobImpl::Create(
+ nsTArray<RefPtr<BlobImpl>>&& aBlobImpls, const nsAString& aContentType,
+ ErrorResult& aRv) {
+ RefPtr<MultipartBlobImpl> blobImpl =
+ new MultipartBlobImpl(std::move(aBlobImpls), aContentType);
+ blobImpl->SetLengthAndModifiedDate(/* aRTPCallerType */ Nothing(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return blobImpl.forget();
+}
+
+void MultipartBlobImpl::CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const {
+ *aStream = nullptr;
+
+ uint32_t length = mBlobImpls.Length();
+ if (length == 0 || mLength == 0) {
+ aRv = NS_NewCStringInputStream(aStream, ""_ns);
+ return;
+ }
+
+ if (length == 1) {
+ BlobImpl* blobImpl = mBlobImpls.ElementAt(0);
+ blobImpl->CreateInputStream(aStream, aRv);
+ return;
+ }
+
+ nsCOMPtr<nsIMultiplexInputStream> stream =
+ do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1");
+ if (NS_WARN_IF(!stream)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ uint32_t i;
+ for (i = 0; i < length; i++) {
+ nsCOMPtr<nsIInputStream> scratchStream;
+ BlobImpl* blobImpl = mBlobImpls.ElementAt(i).get();
+
+ // nsIMultiplexInputStream doesn't work well with empty sub streams. Let's
+ // skip the empty blobs.
+ uint32_t size = blobImpl->GetSize(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ if (size == 0) {
+ continue;
+ }
+
+ blobImpl->CreateInputStream(getter_AddRefs(scratchStream), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ aRv = stream->AppendStream(scratchStream);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ }
+
+ CallQueryInterface(stream, aStream);
+}
+
+already_AddRefed<BlobImpl> MultipartBlobImpl::CreateSlice(
+ uint64_t aStart, uint64_t aLength, const nsAString& aContentType,
+ ErrorResult& aRv) const {
+ // If we clamped to nothing we create an empty blob
+ nsTArray<RefPtr<BlobImpl>> blobImpls;
+
+ uint64_t length = aLength;
+ uint64_t skipStart = aStart;
+
+ // Prune the list of blobs if we can
+ uint32_t i;
+ for (i = 0; length && skipStart && i < mBlobImpls.Length(); i++) {
+ BlobImpl* blobImpl = mBlobImpls[i].get();
+
+ uint64_t l = blobImpl->GetSize(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (skipStart < l) {
+ uint64_t upperBound = std::min<uint64_t>(l - skipStart, length);
+
+ RefPtr<BlobImpl> firstBlobImpl =
+ blobImpl->CreateSlice(skipStart, upperBound, aContentType, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ // Avoid wrapping a single blob inside an MultipartBlobImpl
+ if (length == upperBound) {
+ return firstBlobImpl.forget();
+ }
+
+ blobImpls.AppendElement(firstBlobImpl);
+ length -= upperBound;
+ i++;
+ break;
+ }
+ skipStart -= l;
+ }
+
+ // Now append enough blobs until we're done
+ for (; length && i < mBlobImpls.Length(); i++) {
+ BlobImpl* blobImpl = mBlobImpls[i].get();
+
+ uint64_t l = blobImpl->GetSize(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (length < l) {
+ RefPtr<BlobImpl> lastBlobImpl =
+ blobImpl->CreateSlice(0, length, aContentType, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ blobImpls.AppendElement(lastBlobImpl);
+ } else {
+ blobImpls.AppendElement(blobImpl);
+ }
+ length -= std::min<uint64_t>(l, length);
+ }
+
+ // we can create our blob now
+ RefPtr<BlobImpl> impl = Create(std::move(blobImpls), aContentType, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return impl.forget();
+}
+
+void MultipartBlobImpl::InitializeBlob(RTPCallerType aRTPCallerType,
+ ErrorResult& aRv) {
+ SetLengthAndModifiedDate(Some(aRTPCallerType), aRv);
+ NS_WARNING_ASSERTION(!aRv.Failed(), "SetLengthAndModifiedDate failed");
+}
+
+void MultipartBlobImpl::InitializeBlob(const Sequence<Blob::BlobPart>& aData,
+ const nsAString& aContentType,
+ bool aNativeEOL,
+ RTPCallerType aRTPCallerType,
+ ErrorResult& aRv) {
+ mContentType = aContentType;
+ BlobSet blobSet;
+
+ for (uint32_t i = 0, len = aData.Length(); i < len; ++i) {
+ const Blob::BlobPart& data = aData[i];
+
+ if (data.IsBlob()) {
+ RefPtr<Blob> blob = data.GetAsBlob().get();
+ aRv = blobSet.AppendBlobImpl(blob->Impl());
+ if (aRv.Failed()) {
+ return;
+ }
+ }
+
+ else if (data.IsUTF8String()) {
+ aRv = blobSet.AppendUTF8String(data.GetAsUTF8String(), aNativeEOL);
+ if (aRv.Failed()) {
+ return;
+ }
+ }
+
+ else if (data.IsArrayBuffer()) {
+ const ArrayBuffer& buffer = data.GetAsArrayBuffer();
+ buffer.ComputeState();
+ aRv = blobSet.AppendVoidPtr(buffer.Data(), buffer.Length());
+ if (aRv.Failed()) {
+ return;
+ }
+ }
+
+ else if (data.IsArrayBufferView()) {
+ const ArrayBufferView& buffer = data.GetAsArrayBufferView();
+ buffer.ComputeState();
+ aRv = blobSet.AppendVoidPtr(buffer.Data(), buffer.Length());
+ if (aRv.Failed()) {
+ return;
+ }
+ }
+
+ else {
+ MOZ_CRASH("Impossible blob data type.");
+ }
+ }
+
+ mBlobImpls = blobSet.GetBlobImpls();
+ SetLengthAndModifiedDate(Some(aRTPCallerType), aRv);
+ NS_WARNING_ASSERTION(!aRv.Failed(), "SetLengthAndModifiedDate failed");
+}
+
+void MultipartBlobImpl::SetLengthAndModifiedDate(
+ const Maybe<RTPCallerType>& aRTPCallerType, ErrorResult& aRv) {
+ MOZ_ASSERT(mLength == MULTIPARTBLOBIMPL_UNKNOWN_LENGTH);
+ MOZ_ASSERT_IF(mIsFile, IsLastModificationDateUnset());
+
+ uint64_t totalLength = 0;
+ int64_t lastModified = 0;
+ bool lastModifiedSet = false;
+
+ for (uint32_t index = 0, count = mBlobImpls.Length(); index < count;
+ index++) {
+ RefPtr<BlobImpl>& blob = mBlobImpls[index];
+
+ uint64_t subBlobLength = blob->GetSize(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ MOZ_ASSERT(UINT64_MAX - subBlobLength >= totalLength);
+ totalLength += subBlobLength;
+
+ if (blob->IsFile()) {
+ int64_t partLastModified = blob->GetLastModified(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ if (lastModified < partLastModified) {
+ lastModified = partLastModified * PR_USEC_PER_MSEC;
+ lastModifiedSet = true;
+ }
+ }
+ }
+
+ mLength = totalLength;
+
+ if (mIsFile) {
+ if (lastModifiedSet) {
+ SetLastModificationDatePrecisely(lastModified);
+ } else {
+ MOZ_ASSERT(aRTPCallerType.isSome());
+
+ // We cannot use PR_Now() because bug 493756 and, for this reason:
+ // var x = new Date(); var f = new File(...);
+ // x.getTime() < f.dateModified.getTime()
+ // could fail.
+ SetLastModificationDate(aRTPCallerType.value(), JS_Now());
+ }
+ }
+}
+
+size_t MultipartBlobImpl::GetAllocationSize() const {
+ FallibleTArray<BlobImpl*> visitedBlobs;
+
+ // We want to report the unique blob allocation, avoiding duplicated blobs in
+ // the multipart blob tree.
+ size_t total = 0;
+ for (uint32_t i = 0; i < mBlobImpls.Length(); ++i) {
+ total += mBlobImpls[i]->GetAllocationSize(visitedBlobs);
+ }
+
+ return total;
+}
+
+size_t MultipartBlobImpl::GetAllocationSize(
+ FallibleTArray<BlobImpl*>& aVisitedBlobs) const {
+ FallibleTArray<BlobImpl*> visitedBlobs;
+
+ size_t total = 0;
+ for (BlobImpl* blobImpl : mBlobImpls) {
+ if (!aVisitedBlobs.Contains(blobImpl)) {
+ if (NS_WARN_IF(!aVisitedBlobs.AppendElement(blobImpl, fallible))) {
+ return 0;
+ }
+ total += blobImpl->GetAllocationSize(aVisitedBlobs);
+ }
+ }
+
+ return total;
+}
+
+void MultipartBlobImpl::GetBlobImplType(nsAString& aBlobImplType) const {
+ aBlobImplType.AssignLiteral("MultipartBlobImpl[");
+
+ StringJoinAppend(aBlobImplType, u", "_ns, mBlobImpls,
+ [](nsAString& dest, BlobImpl* subBlobImpl) {
+ nsAutoString blobImplType;
+ subBlobImpl->GetBlobImplType(blobImplType);
+
+ dest.Append(blobImplType);
+ });
+
+ aBlobImplType.AppendLiteral("]");
+}
+
+void MultipartBlobImpl::SetLastModified(int64_t aLastModified) {
+ SetLastModificationDatePrecisely(aLastModified * PR_USEC_PER_MSEC);
+}
diff --git a/dom/file/MultipartBlobImpl.h b/dom/file/MultipartBlobImpl.h
new file mode 100644
index 0000000000..cb62f2adb5
--- /dev/null
+++ b/dom/file/MultipartBlobImpl.h
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MultipartBlobImpl_h
+#define mozilla_dom_MultipartBlobImpl_h
+
+#include <utility>
+
+#include "Blob.h"
+#include "nsContentUtils.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/dom/BaseBlobImpl.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+// This is just a sentinel value to be sure that we don't call
+// SetLengthAndModifiedDate more than once.
+constexpr int64_t MULTIPARTBLOBIMPL_UNKNOWN_LAST_MODIFIED = INT64_MAX;
+constexpr uint64_t MULTIPARTBLOBIMPL_UNKNOWN_LENGTH = UINT64_MAX;
+
+class MultipartBlobImpl final : public BaseBlobImpl {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(MultipartBlobImpl, BaseBlobImpl)
+
+ // Create as a file
+ static already_AddRefed<MultipartBlobImpl> Create(
+ nsTArray<RefPtr<BlobImpl>>&& aBlobImpls, const nsAString& aName,
+ const nsAString& aContentType, RTPCallerType aRTPCallerType,
+ ErrorResult& aRv);
+
+ // Create as a blob
+ static already_AddRefed<MultipartBlobImpl> Create(
+ nsTArray<RefPtr<BlobImpl>>&& aBlobImpls, const nsAString& aContentType,
+ ErrorResult& aRv);
+
+ // Create as a file to be later initialized
+ explicit MultipartBlobImpl(const nsAString& aName)
+ : BaseBlobImpl(aName, u""_ns, MULTIPARTBLOBIMPL_UNKNOWN_LENGTH,
+ MULTIPARTBLOBIMPL_UNKNOWN_LAST_MODIFIED) {}
+
+ // Create as a blob to be later initialized
+ MultipartBlobImpl()
+ : BaseBlobImpl(u""_ns, MULTIPARTBLOBIMPL_UNKNOWN_LENGTH) {}
+
+ void InitializeBlob(RTPCallerType aRTPCallerType, ErrorResult& aRv);
+
+ void InitializeBlob(const Sequence<Blob::BlobPart>& aData,
+ const nsAString& aContentType, bool aNativeEOL,
+ RTPCallerType aRTPCallerType, ErrorResult& aRv);
+
+ already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength,
+ const nsAString& aContentType,
+ ErrorResult& aRv) const override;
+
+ uint64_t GetSize(ErrorResult& aRv) override { return mLength; }
+
+ void CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const override;
+
+ const nsTArray<RefPtr<BlobImpl>>* GetSubBlobImpls() const override {
+ return mBlobImpls.Length() ? &mBlobImpls : nullptr;
+ }
+
+ void SetName(const nsAString& aName) { mName = aName; }
+
+ size_t GetAllocationSize() const override;
+ size_t GetAllocationSize(
+ FallibleTArray<BlobImpl*>& aVisitedBlobs) const override;
+
+ void GetBlobImplType(nsAString& aBlobImplType) const override;
+
+ void SetLastModified(int64_t aLastModified);
+
+ protected:
+ // File constructor.
+ MultipartBlobImpl(nsTArray<RefPtr<BlobImpl>>&& aBlobImpls,
+ const nsAString& aName, const nsAString& aContentType)
+ : BaseBlobImpl(aName, aContentType, MULTIPARTBLOBIMPL_UNKNOWN_LENGTH,
+ MULTIPARTBLOBIMPL_UNKNOWN_LAST_MODIFIED),
+ mBlobImpls(std::move(aBlobImpls)) {}
+
+ // Blob constructor.
+ MultipartBlobImpl(nsTArray<RefPtr<BlobImpl>>&& aBlobImpls,
+ const nsAString& aContentType)
+ : BaseBlobImpl(aContentType, MULTIPARTBLOBIMPL_UNKNOWN_LENGTH),
+ mBlobImpls(std::move(aBlobImpls)) {}
+
+ ~MultipartBlobImpl() override = default;
+
+ void SetLengthAndModifiedDate(const Maybe<RTPCallerType>& aRTPCallerType,
+ ErrorResult& aRv);
+
+ nsTArray<RefPtr<BlobImpl>> mBlobImpls;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MultipartBlobImpl_h
diff --git a/dom/file/MutableBlobStorage.cpp b/dom/file/MutableBlobStorage.cpp
new file mode 100644
index 0000000000..75d0dce6de
--- /dev/null
+++ b/dom/file/MutableBlobStorage.cpp
@@ -0,0 +1,667 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "EmptyBlobImpl.h"
+#include "MutableBlobStorage.h"
+#include "MemoryBlobImpl.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/dom/TemporaryIPCBlobChild.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TaskQueue.h"
+#include "File.h"
+#include "nsAnonymousTemporaryFile.h"
+#include "nsNetCID.h"
+#include "nsProxyRelease.h"
+
+#define BLOB_MEMORY_TEMPORARY_FILE 1048576
+
+namespace mozilla::dom {
+
+namespace {
+
+// This class uses the callback to inform when the Blob is created or when the
+// error must be propagated.
+class BlobCreationDoneRunnable final : public Runnable {
+ public:
+ BlobCreationDoneRunnable(MutableBlobStorage* aBlobStorage,
+ MutableBlobStorageCallback* aCallback,
+ BlobImpl* aBlobImpl, nsresult aRv)
+ : Runnable("dom::BlobCreationDoneRunnable"),
+ mBlobStorage(aBlobStorage),
+ mCallback(aCallback),
+ mBlobImpl(aBlobImpl),
+ mRv(aRv) {
+ MOZ_ASSERT(aBlobStorage);
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT((NS_FAILED(aRv) && !aBlobImpl) ||
+ (NS_SUCCEEDED(aRv) && aBlobImpl));
+ }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mBlobStorage);
+ mCallback->BlobStoreCompleted(mBlobStorage, mBlobImpl, mRv);
+ mCallback = nullptr;
+ mBlobImpl = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ ~BlobCreationDoneRunnable() override {
+ MOZ_ASSERT(mBlobStorage);
+ // If something when wrong, we still have to release these objects in the
+ // correct thread.
+ NS_ProxyRelease("BlobCreationDoneRunnable::mCallback",
+ mBlobStorage->EventTarget(), mCallback.forget());
+ }
+
+ RefPtr<MutableBlobStorage> mBlobStorage;
+ RefPtr<MutableBlobStorageCallback> mCallback;
+ RefPtr<BlobImpl> mBlobImpl;
+ nsresult mRv;
+};
+
+// Simple runnable to propagate the error to the BlobStorage.
+class ErrorPropagationRunnable final : public Runnable {
+ public:
+ ErrorPropagationRunnable(MutableBlobStorage* aBlobStorage, nsresult aRv)
+ : Runnable("dom::ErrorPropagationRunnable"),
+ mBlobStorage(aBlobStorage),
+ mRv(aRv) {}
+
+ NS_IMETHOD
+ Run() override {
+ mBlobStorage->ErrorPropagated(mRv);
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<MutableBlobStorage> mBlobStorage;
+ nsresult mRv;
+};
+
+// This runnable moves a buffer to the IO thread and there, it writes it into
+// the temporary file, if its File Descriptor has not been already closed.
+class WriteRunnable final : public Runnable {
+ public:
+ static WriteRunnable* CopyBuffer(MutableBlobStorage* aBlobStorage,
+ const void* aData, uint32_t aLength) {
+ MOZ_ASSERT(aBlobStorage);
+ MOZ_ASSERT(aData);
+
+ // We have to take a copy of this buffer.
+ void* data = malloc(aLength);
+ if (!data) {
+ return nullptr;
+ }
+
+ memcpy((char*)data, aData, aLength);
+ return new WriteRunnable(aBlobStorage, data, aLength);
+ }
+
+ static WriteRunnable* AdoptBuffer(MutableBlobStorage* aBlobStorage,
+ void* aData, uint32_t aLength) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aBlobStorage);
+ MOZ_ASSERT(aData);
+
+ return new WriteRunnable(aBlobStorage, aData, aLength);
+ }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mBlobStorage);
+
+ PRFileDesc* fd = mBlobStorage->GetFD();
+ if (!fd) {
+ // The file descriptor has been closed in the meantime.
+ return NS_OK;
+ }
+
+ int32_t written = PR_Write(fd, mData, mLength);
+ if (NS_WARN_IF(written < 0 || uint32_t(written) != mLength)) {
+ mBlobStorage->CloseFD();
+ return mBlobStorage->EventTarget()->Dispatch(
+ new ErrorPropagationRunnable(mBlobStorage, NS_ERROR_FAILURE),
+ NS_DISPATCH_NORMAL);
+ }
+
+ return NS_OK;
+ }
+
+ private:
+ WriteRunnable(MutableBlobStorage* aBlobStorage, void* aData, uint32_t aLength)
+ : Runnable("dom::WriteRunnable"),
+ mBlobStorage(aBlobStorage),
+ mData(aData),
+ mLength(aLength) {
+ MOZ_ASSERT(mBlobStorage);
+ MOZ_ASSERT(aData);
+ }
+
+ ~WriteRunnable() override { free(mData); }
+
+ RefPtr<MutableBlobStorage> mBlobStorage;
+ void* mData;
+ uint32_t mLength;
+};
+
+// This runnable closes the FD in case something goes wrong or the temporary
+// file is not needed anymore.
+class CloseFileRunnable final : public Runnable {
+ public:
+ explicit CloseFileRunnable(PRFileDesc* aFD)
+ : Runnable("dom::CloseFileRunnable"), mFD(aFD) {}
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ PR_Close(mFD);
+ mFD = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ ~CloseFileRunnable() override {
+ if (mFD) {
+ PR_Close(mFD);
+ }
+ }
+
+ PRFileDesc* mFD;
+};
+
+// This runnable is dispatched to the main-thread from the IO thread and its
+// task is to create the blob and inform the callback.
+class CreateBlobRunnable final : public Runnable,
+ public TemporaryIPCBlobChildCallback {
+ public:
+ // We need to always declare refcounting because
+ // TemporaryIPCBlobChildCallback has pure-virtual refcounting.
+ NS_DECL_ISUPPORTS_INHERITED
+
+ CreateBlobRunnable(MutableBlobStorage* aBlobStorage,
+ const nsACString& aContentType,
+ already_AddRefed<MutableBlobStorageCallback> aCallback)
+ : Runnable("dom::CreateBlobRunnable"),
+ mBlobStorage(aBlobStorage),
+ mContentType(aContentType),
+ mCallback(aCallback) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aBlobStorage);
+ }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mBlobStorage);
+ mBlobStorage->AskForBlob(this, mContentType);
+ return NS_OK;
+ }
+
+ void OperationSucceeded(BlobImpl* aBlobImpl) override {
+ RefPtr<MutableBlobStorageCallback> callback(std::move(mCallback));
+ callback->BlobStoreCompleted(mBlobStorage, aBlobImpl, NS_OK);
+ }
+
+ void OperationFailed(nsresult aRv) override {
+ RefPtr<MutableBlobStorageCallback> callback(std::move(mCallback));
+ callback->BlobStoreCompleted(mBlobStorage, nullptr, aRv);
+ }
+
+ private:
+ ~CreateBlobRunnable() override {
+ MOZ_ASSERT(mBlobStorage);
+ // If something when wrong, we still have to release data in the correct
+ // thread.
+ NS_ProxyRelease("CreateBlobRunnable::mCallback",
+ mBlobStorage->EventTarget(), mCallback.forget());
+ }
+
+ RefPtr<MutableBlobStorage> mBlobStorage;
+ nsCString mContentType;
+ RefPtr<MutableBlobStorageCallback> mCallback;
+};
+
+NS_IMPL_ISUPPORTS_INHERITED0(CreateBlobRunnable, Runnable)
+
+// This task is used to know when the writing is completed. From the IO thread
+// it dispatches a CreateBlobRunnable to the main-thread.
+class LastRunnable final : public Runnable {
+ public:
+ LastRunnable(MutableBlobStorage* aBlobStorage, const nsACString& aContentType,
+ MutableBlobStorageCallback* aCallback)
+ : Runnable("dom::LastRunnable"),
+ mBlobStorage(aBlobStorage),
+ mContentType(aContentType),
+ mCallback(aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mBlobStorage);
+ MOZ_ASSERT(aCallback);
+ }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ RefPtr<Runnable> runnable =
+ new CreateBlobRunnable(mBlobStorage, mContentType, mCallback.forget());
+ return mBlobStorage->EventTarget()->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ }
+
+ private:
+ ~LastRunnable() override {
+ MOZ_ASSERT(mBlobStorage);
+ // If something when wrong, we still have to release data in the correct
+ // thread.
+ NS_ProxyRelease("LastRunnable::mCallback", mBlobStorage->EventTarget(),
+ mCallback.forget());
+ }
+
+ RefPtr<MutableBlobStorage> mBlobStorage;
+ nsCString mContentType;
+ RefPtr<MutableBlobStorageCallback> mCallback;
+};
+
+} // anonymous namespace
+
+MutableBlobStorage::MutableBlobStorage(MutableBlobStorageType aType,
+ nsIEventTarget* aEventTarget,
+ uint32_t aMaxMemory)
+ : mMutex("MutableBlobStorage::mMutex"),
+ mData(nullptr),
+ mDataLen(0),
+ mDataBufferLen(0),
+ mStorageState(aType == eOnlyInMemory ? eKeepInMemory : eInMemory),
+ mFD(nullptr),
+ mErrorResult(NS_OK),
+ mEventTarget(aEventTarget),
+ mMaxMemory(aMaxMemory) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mEventTarget) {
+ mEventTarget = GetMainThreadSerialEventTarget();
+ }
+
+ if (aMaxMemory == 0 && aType == eCouldBeInTemporaryFile) {
+ mMaxMemory = Preferences::GetUint("dom.blob.memoryToTemporaryFile",
+ BLOB_MEMORY_TEMPORARY_FILE);
+ }
+
+ MOZ_ASSERT(mEventTarget);
+}
+
+MutableBlobStorage::~MutableBlobStorage() {
+ free(mData);
+
+ if (mFD) {
+ RefPtr<Runnable> runnable = new CloseFileRunnable(mFD);
+ (void)DispatchToIOThread(runnable.forget());
+ }
+
+ if (mTaskQueue) {
+ mTaskQueue->BeginShutdown();
+ }
+
+ if (mActor) {
+ NS_ProxyRelease("MutableBlobStorage::mActor", EventTarget(),
+ mActor.forget());
+ }
+}
+
+void MutableBlobStorage::GetBlobImplWhenReady(
+ const nsACString& aContentType, MutableBlobStorageCallback* aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aCallback);
+
+ MutexAutoLock lock(mMutex);
+
+ // GetBlob can be called just once.
+ MOZ_ASSERT(mStorageState != eClosed);
+ StorageState previousState = mStorageState;
+ mStorageState = eClosed;
+
+ if (previousState == eInTemporaryFile) {
+ if (NS_FAILED(mErrorResult)) {
+ MOZ_ASSERT(!mActor);
+
+ RefPtr<Runnable> runnable =
+ new BlobCreationDoneRunnable(this, aCallback, nullptr, mErrorResult);
+ EventTarget()->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ MOZ_ASSERT(mActor);
+
+ // We want to wait until all the WriteRunnable are completed. The way we do
+ // this is to go to the I/O thread and then we come back: the runnables are
+ // executed in order and this LastRunnable will be... the last one.
+ // This Runnable will also close the FD on the I/O thread.
+ RefPtr<Runnable> runnable = new LastRunnable(this, aContentType, aCallback);
+
+ // If the dispatching fails, we are shutting down and it's fine to do not
+ // run the callback.
+ (void)DispatchToIOThread(runnable.forget());
+ return;
+ }
+
+ // If we are waiting for the temporary file, it's better to wait...
+ if (previousState == eWaitingForTemporaryFile) {
+ mPendingContentType = aContentType;
+ mPendingCallback = aCallback;
+ return;
+ }
+
+ RefPtr<BlobImpl> blobImpl;
+
+ if (mData) {
+ blobImpl = new MemoryBlobImpl(mData, mDataLen,
+ NS_ConvertUTF8toUTF16(aContentType));
+
+ mData = nullptr; // The MemoryBlobImpl takes ownership of the buffer
+ mDataLen = 0;
+ mDataBufferLen = 0;
+ } else {
+ blobImpl = new EmptyBlobImpl(NS_ConvertUTF8toUTF16(aContentType));
+ }
+
+ RefPtr<BlobCreationDoneRunnable> runnable =
+ new BlobCreationDoneRunnable(this, aCallback, blobImpl, NS_OK);
+
+ nsresult error =
+ EventTarget()->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(error))) {
+ return;
+ }
+}
+
+nsresult MutableBlobStorage::Append(const void* aData, uint32_t aLength) {
+ // This method can be called on any thread.
+
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mStorageState != eClosed);
+ NS_ENSURE_ARG_POINTER(aData);
+
+ if (!aLength) {
+ return NS_OK;
+ }
+
+ // If eInMemory is the current Storage state, we could maybe migrate to
+ // a temporary file.
+ if (mStorageState == eInMemory && ShouldBeTemporaryStorage(lock, aLength) &&
+ !MaybeCreateTemporaryFile(lock)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If we are already in the temporaryFile mode, we have to dispatch a
+ // runnable.
+ if (mStorageState == eInTemporaryFile) {
+ // If a previous operation failed, let's return that error now.
+ if (NS_FAILED(mErrorResult)) {
+ return mErrorResult;
+ }
+
+ RefPtr<WriteRunnable> runnable =
+ WriteRunnable::CopyBuffer(this, aData, aLength);
+ if (NS_WARN_IF(!runnable)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ nsresult rv = DispatchToIOThread(runnable.forget());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mDataLen += aLength;
+ return NS_OK;
+ }
+
+ // By default, we store in memory.
+
+ uint64_t offset = mDataLen;
+
+ if (!ExpandBufferSize(lock, aLength)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ memcpy((char*)mData + offset, aData, aLength);
+ return NS_OK;
+}
+
+bool MutableBlobStorage::ExpandBufferSize(const MutexAutoLock& aProofOfLock,
+ uint64_t aSize) {
+ MOZ_ASSERT(mStorageState < eInTemporaryFile);
+
+ if (mDataBufferLen >= mDataLen + aSize) {
+ mDataLen += aSize;
+ return true;
+ }
+
+ // Start at 1 or we'll loop forever.
+ CheckedUint32 bufferLen =
+ std::max<uint32_t>(static_cast<uint32_t>(mDataBufferLen), 1);
+ while (bufferLen.isValid() && bufferLen.value() < mDataLen + aSize) {
+ bufferLen *= 2;
+ }
+
+ if (!bufferLen.isValid()) {
+ return false;
+ }
+
+ void* data = realloc(mData, bufferLen.value());
+ if (!data) {
+ return false;
+ }
+
+ mData = data;
+ mDataBufferLen = bufferLen.value();
+ mDataLen += aSize;
+ return true;
+}
+
+bool MutableBlobStorage::ShouldBeTemporaryStorage(
+ const MutexAutoLock& aProofOfLock, uint64_t aSize) const {
+ MOZ_ASSERT(mStorageState == eInMemory);
+
+ CheckedUint32 bufferSize = mDataLen;
+ bufferSize += aSize;
+
+ if (!bufferSize.isValid()) {
+ return false;
+ }
+
+ return bufferSize.value() >= mMaxMemory;
+}
+
+bool MutableBlobStorage::MaybeCreateTemporaryFile(
+ const MutexAutoLock& aProofOfLock) {
+ mStorageState = eWaitingForTemporaryFile;
+
+ if (!NS_IsMainThread()) {
+ RefPtr<MutableBlobStorage> self = this;
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "MutableBlobStorage::MaybeCreateTemporaryFile", [self]() {
+ MutexAutoLock lock(self->mMutex);
+ self->MaybeCreateTemporaryFileOnMainThread(lock);
+ if (!self->mActor) {
+ self->ErrorPropagated(NS_ERROR_FAILURE);
+ }
+ });
+ EventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+ return true;
+ }
+
+ MaybeCreateTemporaryFileOnMainThread(aProofOfLock);
+ return !!mActor;
+}
+
+void MutableBlobStorage::MaybeCreateTemporaryFileOnMainThread(
+ const MutexAutoLock& aProofOfLock) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mActor);
+
+ mozilla::ipc::PBackgroundChild* actorChild =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!actorChild)) {
+ return;
+ }
+
+ mActor = new TemporaryIPCBlobChild(this);
+ actorChild->SendPTemporaryIPCBlobConstructor(mActor);
+
+ // We need manually to increase the reference for this actor because the
+ // IPC allocator method is not triggered. The Release() is called by IPDL
+ // when the actor is deleted.
+ mActor.get()->AddRef();
+
+ // The actor will call us when the FileDescriptor is received.
+}
+
+void MutableBlobStorage::TemporaryFileCreated(PRFileDesc* aFD) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mStorageState == eWaitingForTemporaryFile ||
+ mStorageState == eClosed);
+ MOZ_ASSERT_IF(mPendingCallback, mStorageState == eClosed);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(aFD);
+
+ // If the object has been already closed and we don't need to execute a
+ // callback, we need just to close the file descriptor in the correct thread.
+ if (mStorageState == eClosed && !mPendingCallback) {
+ RefPtr<Runnable> runnable = new CloseFileRunnable(aFD);
+
+ // If this dispatching fails, CloseFileRunnable will close the FD in the
+ // DTOR on the current thread.
+ (void)DispatchToIOThread(runnable.forget());
+
+ // Let's inform the parent that we have nothing else to do.
+ mActor->SendOperationFailed();
+ mActor = nullptr;
+ return;
+ }
+
+ // If we still receiving data, we can proceed in temporary-file mode.
+ if (mStorageState == eWaitingForTemporaryFile) {
+ mStorageState = eInTemporaryFile;
+ }
+
+ mFD = aFD;
+ MOZ_ASSERT(NS_SUCCEEDED(mErrorResult));
+
+ // This runnable takes the ownership of mData and it will write this buffer
+ // into the temporary file.
+ RefPtr<WriteRunnable> runnable =
+ WriteRunnable::AdoptBuffer(this, mData, mDataLen);
+ MOZ_ASSERT(runnable);
+
+ mData = nullptr;
+
+ nsresult rv = DispatchToIOThread(runnable.forget());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // Shutting down, we cannot continue.
+ return;
+ }
+
+ // If we are closed, it means that GetBlobImplWhenReady() has been called when
+ // we were already waiting for a temporary file-descriptor. Finally we are
+ // here, AdoptBuffer runnable is going to write the current buffer into this
+ // file. After that, there is nothing else to write, and we dispatch
+ // LastRunnable which ends up calling mPendingCallback via CreateBlobRunnable.
+ if (mStorageState == eClosed) {
+ MOZ_ASSERT(mPendingCallback);
+
+ RefPtr<Runnable> runnable =
+ new LastRunnable(this, mPendingContentType, mPendingCallback);
+ (void)DispatchToIOThread(runnable.forget());
+
+ mPendingCallback = nullptr;
+ }
+}
+
+void MutableBlobStorage::AskForBlob(TemporaryIPCBlobChildCallback* aCallback,
+ const nsACString& aContentType) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mStorageState == eClosed);
+ MOZ_ASSERT(mFD);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(aCallback);
+
+ // Let's pass the FileDescriptor to the parent actor in order to keep the file
+ // locked on windows.
+ mActor->AskForBlob(aCallback, aContentType, mFD);
+
+ // The previous operation has duplicated the file descriptor. Now we can close
+ // mFD. The parent will take care of closing the duplicated file descriptor on
+ // its side.
+ RefPtr<Runnable> runnable = new CloseFileRunnable(mFD);
+ (void)DispatchToIOThread(runnable.forget());
+
+ mFD = nullptr;
+ mActor = nullptr;
+}
+
+void MutableBlobStorage::ErrorPropagated(nsresult aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MutexAutoLock lock(mMutex);
+ mErrorResult = aRv;
+
+ if (mActor) {
+ mActor->SendOperationFailed();
+ mActor = nullptr;
+ }
+}
+
+nsresult MutableBlobStorage::DispatchToIOThread(
+ already_AddRefed<nsIRunnable> aRunnable) {
+ if (!mTaskQueue) {
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ MOZ_ASSERT(target);
+
+ mTaskQueue = TaskQueue::Create(target.forget(), "BlobStorage");
+ }
+
+ nsCOMPtr<nsIRunnable> runnable(aRunnable);
+ nsresult rv = mTaskQueue->Dispatch(runnable.forget());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+size_t MutableBlobStorage::SizeOfCurrentMemoryBuffer() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MutexAutoLock lock(mMutex);
+ return mStorageState < eInTemporaryFile ? mDataLen : 0;
+}
+
+PRFileDesc* MutableBlobStorage::GetFD() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MutexAutoLock lock(mMutex);
+ return mFD;
+}
+
+void MutableBlobStorage::CloseFD() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mFD);
+
+ PR_Close(mFD);
+ mFD = nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/MutableBlobStorage.h b/dom/file/MutableBlobStorage.h
new file mode 100644
index 0000000000..aa0d42c75e
--- /dev/null
+++ b/dom/file/MutableBlobStorage.h
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MutableBlobStorage_h
+#define mozilla_dom_MutableBlobStorage_h
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/Mutex.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "prio.h"
+
+class nsIEventTarget;
+class nsIRunnable;
+
+namespace mozilla {
+
+class TaskQueue;
+
+namespace dom {
+
+class Blob;
+class BlobImpl;
+class MutableBlobStorage;
+class TemporaryIPCBlobChild;
+class TemporaryIPCBlobChildCallback;
+
+class MutableBlobStorageCallback {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual void BlobStoreCompleted(MutableBlobStorage* aBlobStorage,
+ BlobImpl* aBlob, nsresult aRv) = 0;
+};
+
+// This class is must be created and used on main-thread, except for Append()
+// that can be called on any thread.
+class MutableBlobStorage final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MutableBlobStorage)
+
+ enum MutableBlobStorageType {
+ eOnlyInMemory,
+ eCouldBeInTemporaryFile,
+ };
+
+ explicit MutableBlobStorage(MutableBlobStorageType aType,
+ nsIEventTarget* aEventTarget = nullptr,
+ uint32_t aMaxMemory = 0);
+
+ nsresult Append(const void* aData, uint32_t aLength);
+
+ // This method can be called just once.
+ // The callback will be called when the BlobImpl is ready.
+ void GetBlobImplWhenReady(const nsACString& aContentType,
+ MutableBlobStorageCallback* aCallback);
+
+ void TemporaryFileCreated(PRFileDesc* aFD);
+
+ void AskForBlob(TemporaryIPCBlobChildCallback* aCallback,
+ const nsACString& aContentType);
+
+ void ErrorPropagated(nsresult aRv);
+
+ nsIEventTarget* EventTarget() {
+ MOZ_ASSERT(mEventTarget);
+ return mEventTarget;
+ }
+
+ // Returns the heap size in bytes of our internal buffers.
+ // Note that this intentionally ignores the data in the temp file.
+ size_t SizeOfCurrentMemoryBuffer();
+
+ PRFileDesc* GetFD();
+
+ void CloseFD();
+
+ private:
+ ~MutableBlobStorage();
+
+ bool ExpandBufferSize(const MutexAutoLock& aProofOfLock, uint64_t aSize);
+
+ bool ShouldBeTemporaryStorage(const MutexAutoLock& aProofOfLock,
+ uint64_t aSize) const;
+
+ bool MaybeCreateTemporaryFile(const MutexAutoLock& aProofOfLock);
+ void MaybeCreateTemporaryFileOnMainThread(const MutexAutoLock& aProofOfLock);
+
+ [[nodiscard]] nsresult DispatchToIOThread(
+ already_AddRefed<nsIRunnable> aRunnable);
+
+ Mutex mMutex MOZ_UNANNOTATED;
+
+ // All these variables are touched on the main thread only or in the
+ // retargeted thread when used by Append(). They are protected by mMutex.
+
+ void* mData;
+ uint64_t mDataLen;
+ uint64_t mDataBufferLen;
+
+ enum StorageState {
+ eKeepInMemory,
+ eInMemory,
+ eWaitingForTemporaryFile,
+ eInTemporaryFile,
+ eClosed
+ };
+
+ StorageState mStorageState;
+
+ PRFileDesc* mFD;
+
+ nsresult mErrorResult;
+
+ RefPtr<TaskQueue> mTaskQueue;
+ nsCOMPtr<nsIEventTarget> mEventTarget;
+
+ nsCString mPendingContentType;
+ RefPtr<MutableBlobStorageCallback> mPendingCallback;
+
+ RefPtr<TemporaryIPCBlobChild> mActor;
+
+ // This value is used when we go from eInMemory to eWaitingForTemporaryFile
+ // and eventually eInTemporaryFile. If the size of the buffer is >=
+ // mMaxMemory, the creation of the temporary file will start.
+ // It's not used if mStorageState is eKeepInMemory.
+ uint32_t mMaxMemory;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MutableBlobStorage_h
diff --git a/dom/file/MutableBlobStreamListener.cpp b/dom/file/MutableBlobStreamListener.cpp
new file mode 100644
index 0000000000..f5f6a5bae3
--- /dev/null
+++ b/dom/file/MutableBlobStreamListener.cpp
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MutableBlobStreamListener.h"
+#include "MutableBlobStorage.h"
+#include "nsIInputStream.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::dom {
+
+MutableBlobStreamListener::MutableBlobStreamListener(
+ MutableBlobStorage::MutableBlobStorageType aStorageType,
+ const nsACString& aContentType, MutableBlobStorageCallback* aCallback,
+ nsIEventTarget* aEventTarget)
+ : mCallback(aCallback),
+ mStorageType(aStorageType),
+ mContentType(aContentType),
+ mEventTarget(aEventTarget) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aCallback);
+
+ if (!mEventTarget) {
+ mEventTarget = GetMainThreadSerialEventTarget();
+ }
+
+ MOZ_ASSERT(mEventTarget);
+}
+
+MutableBlobStreamListener::~MutableBlobStreamListener() {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+NS_IMPL_ISUPPORTS(MutableBlobStreamListener, nsIStreamListener,
+ nsIThreadRetargetableStreamListener, nsIRequestObserver)
+
+NS_IMETHODIMP
+MutableBlobStreamListener::OnStartRequest(nsIRequest* aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mStorage);
+ MOZ_ASSERT(mEventTarget);
+
+ mStorage = new MutableBlobStorage(mStorageType, mEventTarget);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MutableBlobStreamListener::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatus) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mStorage);
+
+ // Resetting mStorage to nullptr.
+ RefPtr<MutableBlobStorage> storage;
+ storage.swap(mStorage);
+
+ // Let's propagate the error simulating a failure of the storage.
+ if (NS_FAILED(aStatus)) {
+ mCallback->BlobStoreCompleted(storage, nullptr, aStatus);
+ return NS_OK;
+ }
+
+ storage->GetBlobImplWhenReady(mContentType, mCallback);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MutableBlobStreamListener::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aStream,
+ uint64_t aSourceOffset,
+ uint32_t aCount) {
+ // This method could be called on any thread.
+ MOZ_ASSERT(mStorage);
+
+ uint32_t countRead;
+ return aStream->ReadSegments(WriteSegmentFun, this, aCount, &countRead);
+}
+
+nsresult MutableBlobStreamListener::WriteSegmentFun(
+ nsIInputStream* aWriterStream, void* aClosure, const char* aFromSegment,
+ uint32_t aToOffset, uint32_t aCount, uint32_t* aWriteCount) {
+ // This method could be called on any thread.
+
+ MutableBlobStreamListener* self =
+ static_cast<MutableBlobStreamListener*>(aClosure);
+ MOZ_ASSERT(self->mStorage);
+
+ nsresult rv = self->mStorage->Append(aFromSegment, aCount);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ *aWriteCount = aCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MutableBlobStreamListener::CheckListenerChain() { return NS_OK; }
+
+} // namespace mozilla::dom
diff --git a/dom/file/MutableBlobStreamListener.h b/dom/file/MutableBlobStreamListener.h
new file mode 100644
index 0000000000..6fd32848a7
--- /dev/null
+++ b/dom/file/MutableBlobStreamListener.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MutableBlobStreamListener_h
+#define mozilla_dom_MutableBlobStreamListener_h
+
+#include "nsIStreamListener.h"
+#include "nsIThreadRetargetableStreamListener.h"
+#include "nsTString.h"
+#include "mozilla/dom/MutableBlobStorage.h"
+
+class nsIEventTarget;
+
+namespace mozilla::dom {
+
+class MutableBlobStreamListener final
+ : public nsIStreamListener,
+ public nsIThreadRetargetableStreamListener {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+
+ MutableBlobStreamListener(
+ MutableBlobStorage::MutableBlobStorageType aStorageType,
+ const nsACString& aContentType, MutableBlobStorageCallback* aCallback,
+ nsIEventTarget* aEventTarget = nullptr);
+
+ private:
+ ~MutableBlobStreamListener();
+
+ static nsresult WriteSegmentFun(nsIInputStream* aWriter, void* aClosure,
+ const char* aFromSegment, uint32_t aOffset,
+ uint32_t aCount, uint32_t* aWriteCount);
+
+ RefPtr<MutableBlobStorage> mStorage;
+ RefPtr<MutableBlobStorageCallback> mCallback;
+
+ MutableBlobStorage::MutableBlobStorageType mStorageType;
+ nsCString mContentType;
+ nsCOMPtr<nsIEventTarget> mEventTarget;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MutableBlobStreamListener_h
diff --git a/dom/file/StreamBlobImpl.cpp b/dom/file/StreamBlobImpl.cpp
new file mode 100644
index 0000000000..9d0c8d86d1
--- /dev/null
+++ b/dom/file/StreamBlobImpl.cpp
@@ -0,0 +1,224 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "EmptyBlobImpl.h"
+#include "mozilla/InputStreamLengthWrapper.h"
+#include "mozilla/SlicedInputStream.h"
+#include "StreamBlobImpl.h"
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStreamUtils.h"
+#include "nsStringStream.h"
+#include "nsIAsyncOutputStream.h"
+#include "nsICloneableInputStream.h"
+#include "nsIEventTarget.h"
+#include "nsIPipe.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_ISUPPORTS_INHERITED(StreamBlobImpl, BlobImpl, nsIMemoryReporter)
+
+static already_AddRefed<nsICloneableInputStream> EnsureCloneableStream(
+ nsIInputStream* aInputStream, uint64_t aLength) {
+ nsCOMPtr<nsICloneableInputStream> cloneable = do_QueryInterface(aInputStream);
+ if (cloneable && cloneable->GetCloneable()) {
+ return cloneable.forget();
+ }
+
+ // If the stream we're copying is known to be small, specify the size of the
+ // pipe's segments precisely to limit wasted space. An extra byte above length
+ // is included to avoid allocating an extra segment immediately before reading
+ // EOF from the source stream. Otherwise, allocate 64k buffers rather than
+ // the default of 4k buffers to reduce segment counts for very large payloads.
+ static constexpr uint32_t kBaseSegmentSize = 64 * 1024;
+ uint32_t segmentSize = kBaseSegmentSize;
+ if (aLength + 1 <= kBaseSegmentSize * 4) {
+ segmentSize = aLength + 1;
+ }
+
+ // NOTE: We specify unlimited segments to eagerly build a complete copy of the
+ // source stream locally without waiting for the blob to be consumed.
+ nsCOMPtr<nsIAsyncInputStream> reader;
+ nsCOMPtr<nsIAsyncOutputStream> writer;
+ NS_NewPipe2(getter_AddRefs(reader), getter_AddRefs(writer), true, true,
+ segmentSize, UINT32_MAX);
+
+ nsresult rv;
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ rv = NS_AsyncCopy(aInputStream, writer, target,
+ NS_ASYNCCOPY_VIA_WRITESEGMENTS, segmentSize);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ cloneable = do_QueryInterface(reader);
+ MOZ_ASSERT(cloneable && cloneable->GetCloneable());
+ return cloneable.forget();
+}
+
+/* static */
+already_AddRefed<StreamBlobImpl> StreamBlobImpl::Create(
+ already_AddRefed<nsIInputStream> aInputStream,
+ const nsAString& aContentType, uint64_t aLength,
+ const nsAString& aBlobImplType) {
+ nsCOMPtr<nsIInputStream> inputStream = std::move(aInputStream);
+ nsCOMPtr<nsICloneableInputStream> cloneable =
+ EnsureCloneableStream(inputStream, aLength);
+
+ RefPtr<StreamBlobImpl> blobImplStream = new StreamBlobImpl(
+ cloneable.forget(), aContentType, aLength, aBlobImplType);
+ blobImplStream->MaybeRegisterMemoryReporter();
+ return blobImplStream.forget();
+}
+
+/* static */
+already_AddRefed<StreamBlobImpl> StreamBlobImpl::Create(
+ already_AddRefed<nsIInputStream> aInputStream, const nsAString& aName,
+ const nsAString& aContentType, int64_t aLastModifiedDate, uint64_t aLength,
+ const nsAString& aBlobImplType) {
+ nsCOMPtr<nsIInputStream> inputStream = std::move(aInputStream);
+ nsCOMPtr<nsICloneableInputStream> cloneable =
+ EnsureCloneableStream(inputStream, aLength);
+
+ RefPtr<StreamBlobImpl> blobImplStream =
+ new StreamBlobImpl(cloneable.forget(), aName, aContentType,
+ aLastModifiedDate, aLength, aBlobImplType);
+ blobImplStream->MaybeRegisterMemoryReporter();
+ return blobImplStream.forget();
+}
+
+StreamBlobImpl::StreamBlobImpl(
+ already_AddRefed<nsICloneableInputStream> aInputStream,
+ const nsAString& aContentType, uint64_t aLength,
+ const nsAString& aBlobImplType)
+ : BaseBlobImpl(aContentType, aLength),
+ mInputStream(std::move(aInputStream)),
+ mBlobImplType(aBlobImplType),
+ mIsDirectory(false),
+ mFileId(-1) {}
+
+StreamBlobImpl::StreamBlobImpl(
+ already_AddRefed<nsICloneableInputStream> aInputStream,
+ const nsAString& aName, const nsAString& aContentType,
+ int64_t aLastModifiedDate, uint64_t aLength, const nsAString& aBlobImplType)
+ : BaseBlobImpl(aName, aContentType, aLength, aLastModifiedDate),
+ mInputStream(std::move(aInputStream)),
+ mBlobImplType(aBlobImplType),
+ mIsDirectory(false),
+ mFileId(-1) {}
+
+StreamBlobImpl::~StreamBlobImpl() {
+ if (mInputStream) {
+ nsCOMPtr<nsIInputStream> stream = do_QueryInterface(mInputStream);
+ stream->Close();
+ }
+ UnregisterWeakMemoryReporter(this);
+}
+
+void StreamBlobImpl::CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const {
+ if (!mInputStream) {
+ // We failed to clone the input stream in EnsureCloneableStream.
+ *aStream = nullptr;
+ aRv.ThrowUnknownError("failed to read blob data");
+ return;
+ }
+
+ nsCOMPtr<nsIInputStream> clonedStream;
+ aRv = mInputStream->Clone(getter_AddRefs(clonedStream));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsCOMPtr<nsIInputStream> wrappedStream =
+ InputStreamLengthWrapper::MaybeWrap(clonedStream.forget(), mLength);
+
+ wrappedStream.forget(aStream);
+}
+
+already_AddRefed<BlobImpl> StreamBlobImpl::CreateSlice(
+ uint64_t aStart, uint64_t aLength, const nsAString& aContentType,
+ ErrorResult& aRv) const {
+ if (!aLength) {
+ RefPtr<BlobImpl> impl = new EmptyBlobImpl(aContentType);
+ return impl.forget();
+ }
+
+ nsCOMPtr<nsIInputStream> clonedStream;
+
+ nsCOMPtr<nsICloneableInputStreamWithRange> stream =
+ do_QueryInterface(mInputStream);
+ if (stream) {
+ aRv = stream->CloneWithRange(aStart, aLength, getter_AddRefs(clonedStream));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ } else {
+ CreateInputStream(getter_AddRefs(clonedStream), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ clonedStream =
+ new SlicedInputStream(clonedStream.forget(), aStart, aLength);
+ }
+
+ MOZ_ASSERT(clonedStream);
+
+ return StreamBlobImpl::Create(clonedStream.forget(), aContentType, aLength,
+ mBlobImplType);
+}
+
+void StreamBlobImpl::MaybeRegisterMemoryReporter() {
+ // We report only stringInputStream.
+ nsCOMPtr<nsIStringInputStream> stringInputStream =
+ do_QueryInterface(mInputStream);
+ if (!stringInputStream) {
+ return;
+ }
+
+ RegisterWeakMemoryReporter(this);
+}
+
+NS_IMETHODIMP
+StreamBlobImpl::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) {
+ nsCOMPtr<nsIStringInputStream> stringInputStream =
+ do_QueryInterface(mInputStream);
+ if (!stringInputStream) {
+ return NS_OK;
+ }
+
+ MOZ_COLLECT_REPORT(
+ "explicit/dom/memory-file-data/stream", KIND_HEAP, UNITS_BYTES,
+ stringInputStream->SizeOfIncludingThisIfUnshared(MallocSizeOf),
+ "Memory used to back a File/Blob based on an input stream.");
+
+ return NS_OK;
+}
+
+size_t StreamBlobImpl::GetAllocationSize() const {
+ nsCOMPtr<nsIStringInputStream> stringInputStream =
+ do_QueryInterface(mInputStream);
+ if (!stringInputStream) {
+ return 0;
+ }
+
+ return stringInputStream->SizeOfIncludingThisEvenIfShared(MallocSizeOf);
+}
+
+void StreamBlobImpl::GetBlobImplType(nsAString& aBlobImplType) const {
+ aBlobImplType.AssignLiteral("StreamBlobImpl[");
+ aBlobImplType.Append(mBlobImplType);
+ aBlobImplType.AppendLiteral("]");
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/StreamBlobImpl.h b/dom/file/StreamBlobImpl.h
new file mode 100644
index 0000000000..6f35638463
--- /dev/null
+++ b/dom/file/StreamBlobImpl.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_StreamBlobImpl_h
+#define mozilla_dom_StreamBlobImpl_h
+
+#include "BaseBlobImpl.h"
+#include "nsCOMPtr.h"
+#include "nsIMemoryReporter.h"
+#include "nsICloneableInputStream.h"
+
+namespace mozilla::dom {
+
+class StreamBlobImpl final : public BaseBlobImpl, public nsIMemoryReporter {
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIMEMORYREPORTER
+
+ // Blob constructor.
+ static already_AddRefed<StreamBlobImpl> Create(
+ already_AddRefed<nsIInputStream> aInputStream,
+ const nsAString& aContentType, uint64_t aLength,
+ const nsAString& aBlobImplType);
+
+ // File constructor.
+ static already_AddRefed<StreamBlobImpl> Create(
+ already_AddRefed<nsIInputStream> aInputStream, const nsAString& aName,
+ const nsAString& aContentType, int64_t aLastModifiedDate,
+ uint64_t aLength, const nsAString& aBlobImplType);
+
+ void CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const override;
+
+ already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength,
+ const nsAString& aContentType,
+ ErrorResult& aRv) const override;
+
+ bool IsMemoryFile() const override { return true; }
+
+ int64_t GetFileId() const override { return mFileId; }
+
+ void SetFileId(int64_t aFileId) { mFileId = aFileId; }
+
+ void SetFullPath(const nsAString& aFullPath) { mFullPath = aFullPath; }
+
+ void GetMozFullPathInternal(nsAString& aFullPath, ErrorResult& aRv) override {
+ aFullPath = mFullPath;
+ }
+
+ void SetIsDirectory(bool aIsDirectory) { mIsDirectory = aIsDirectory; }
+
+ bool IsDirectory() const override { return mIsDirectory; }
+
+ size_t GetAllocationSize() const override;
+
+ size_t GetAllocationSize(
+ FallibleTArray<BlobImpl*>& aVisitedBlobImpls) const override {
+ return GetAllocationSize();
+ }
+
+ void GetBlobImplType(nsAString& aBlobImplType) const override;
+
+ private:
+ // Blob constructor.
+ StreamBlobImpl(already_AddRefed<nsICloneableInputStream> aInputStream,
+ const nsAString& aContentType, uint64_t aLength,
+ const nsAString& aBlobImplType);
+
+ // File constructor.
+ StreamBlobImpl(already_AddRefed<nsICloneableInputStream> aInputStream,
+ const nsAString& aName, const nsAString& aContentType,
+ int64_t aLastModifiedDate, uint64_t aLength,
+ const nsAString& aBlobImplType);
+
+ ~StreamBlobImpl() override;
+
+ void MaybeRegisterMemoryReporter();
+
+ nsCOMPtr<nsICloneableInputStream> mInputStream;
+
+ nsString mBlobImplType;
+
+ nsString mFullPath;
+ bool mIsDirectory;
+ int64_t mFileId;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_StreamBlobImpl_h
diff --git a/dom/file/StringBlobImpl.cpp b/dom/file/StringBlobImpl.cpp
new file mode 100644
index 0000000000..8cb657ca5f
--- /dev/null
+++ b/dom/file/StringBlobImpl.cpp
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "StringBlobImpl.h"
+#include "nsStringStream.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_ISUPPORTS_INHERITED(StringBlobImpl, BlobImpl, nsIMemoryReporter)
+
+/* static */
+already_AddRefed<StringBlobImpl> StringBlobImpl::Create(
+ const nsACString& aData, const nsAString& aContentType) {
+ RefPtr<StringBlobImpl> blobImpl = new StringBlobImpl(aData, aContentType);
+ RegisterWeakMemoryReporter(blobImpl);
+ return blobImpl.forget();
+}
+
+StringBlobImpl::StringBlobImpl(const nsACString& aData,
+ const nsAString& aContentType)
+ : BaseBlobImpl(aContentType, aData.Length()), mData(aData) {}
+
+StringBlobImpl::~StringBlobImpl() { UnregisterWeakMemoryReporter(this); }
+
+already_AddRefed<BlobImpl> StringBlobImpl::CreateSlice(
+ uint64_t aStart, uint64_t aLength, const nsAString& aContentType,
+ ErrorResult& aRv) const {
+ RefPtr<BlobImpl> impl =
+ new StringBlobImpl(Substring(mData, aStart, aLength), aContentType);
+ return impl.forget();
+}
+
+void StringBlobImpl::CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const {
+ aRv = NS_NewCStringInputStream(aStream, mData);
+}
+
+NS_IMETHODIMP
+StringBlobImpl::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) {
+ MOZ_COLLECT_REPORT("explicit/dom/memory-file-data/string", KIND_HEAP,
+ UNITS_BYTES,
+ mData.SizeOfExcludingThisIfUnshared(MallocSizeOf),
+ "Memory used to back a File/Blob based on a string.");
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/StringBlobImpl.h b/dom/file/StringBlobImpl.h
new file mode 100644
index 0000000000..3b3037e693
--- /dev/null
+++ b/dom/file/StringBlobImpl.h
@@ -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/. */
+
+#ifndef mozilla_dom_StringBlobImpl_h
+#define mozilla_dom_StringBlobImpl_h
+
+#include "BaseBlobImpl.h"
+#include "nsIMemoryReporter.h"
+
+namespace mozilla::dom {
+
+class StringBlobImpl final : public BaseBlobImpl, public nsIMemoryReporter {
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIMEMORYREPORTER
+
+ static already_AddRefed<StringBlobImpl> Create(const nsACString& aData,
+ const nsAString& aContentType);
+
+ void CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const override;
+
+ already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength,
+ const nsAString& aContentType,
+ ErrorResult& aRv) const override;
+
+ size_t GetAllocationSize() const override { return mData.Length(); }
+
+ size_t GetAllocationSize(
+ FallibleTArray<BlobImpl*>& aVisitedBlobImpls) const override {
+ return GetAllocationSize();
+ }
+
+ void GetBlobImplType(nsAString& aBlobImplType) const override {
+ aBlobImplType = u"StringBlobImpl"_ns;
+ }
+
+ private:
+ StringBlobImpl(const nsACString& aData, const nsAString& aContentType);
+
+ ~StringBlobImpl() override;
+
+ nsCString mData;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_StringBlobImpl_h
diff --git a/dom/file/TemporaryFileBlobImpl.cpp b/dom/file/TemporaryFileBlobImpl.cpp
new file mode 100644
index 0000000000..505d4a96d4
--- /dev/null
+++ b/dom/file/TemporaryFileBlobImpl.cpp
@@ -0,0 +1,126 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "TemporaryFileBlobImpl.h"
+
+#include "RemoteLazyInputStreamThread.h"
+#include "mozilla/ErrorResult.h"
+#include "nsFileStreams.h"
+#include "nsIFile.h"
+#include "nsIFileStreams.h"
+#include "nsNetUtil.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+
+using namespace mozilla::ipc;
+
+namespace mozilla::dom {
+
+namespace {
+
+// Here the flags needed in order to keep the temporary file opened.
+// 1. REOPEN_ON_REWIND -> otherwise the stream is not serializable more than
+// once.
+// 2. no DEFER_OPEN -> the file must be kept open on windows in order to be
+// deleted when used.
+// 3. no CLOSE_ON_EOF -> the file will be closed by the DTOR. No needs. Also
+// because the inputStream will not be read directly.
+// 4. no SHARE_DELETE -> We don't want to allow this file to be deleted.
+const uint32_t sTemporaryFileStreamFlags = nsIFileInputStream::REOPEN_ON_REWIND;
+
+class TemporaryFileInputStream final : public nsFileInputStream {
+ public:
+ static nsresult Create(nsIFile* aFile, nsIInputStream** aInputStream) {
+ MOZ_ASSERT(aFile);
+ MOZ_ASSERT(aInputStream);
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ RefPtr<TemporaryFileInputStream> stream =
+ new TemporaryFileInputStream(aFile);
+
+ nsresult rv = stream->Init(aFile, -1, -1, sTemporaryFileStreamFlags);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ stream.forget(aInputStream);
+ return NS_OK;
+ }
+
+ void Serialize(InputStreamParams& aParams, uint32_t aMaxSize,
+ uint32_t* aSizeUsed) override {
+ MOZ_CRASH("This inputStream cannot be serialized.");
+ }
+
+ bool Deserialize(const InputStreamParams& aParams) override {
+ MOZ_CRASH("This inputStream cannot be deserialized.");
+ return false;
+ }
+
+ private:
+ explicit TemporaryFileInputStream(nsIFile* aFile) : mFile(aFile) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ }
+
+ ~TemporaryFileInputStream() override {
+ // Let's delete the file on the RemoteLazyInputStream Thread.
+ RefPtr<RemoteLazyInputStreamThread> thread =
+ RemoteLazyInputStreamThread::GetOrCreate();
+ if (NS_WARN_IF(!thread)) {
+ return;
+ }
+
+ nsCOMPtr<nsIFile> file = std::move(mFile);
+ thread->Dispatch(
+ NS_NewRunnableFunction("TemporaryFileInputStream::Runnable",
+ [file]() { file->Remove(false); }));
+ }
+
+ nsCOMPtr<nsIFile> mFile;
+};
+
+} // namespace
+
+TemporaryFileBlobImpl::TemporaryFileBlobImpl(nsIFile* aFile,
+ const nsAString& aContentType)
+ : FileBlobImpl(aFile, u""_ns, aContentType)
+#ifdef DEBUG
+ ,
+ mInputStreamCreated(false)
+#endif
+{
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ // This must be considered a blob.
+ mIsFile = false;
+}
+
+TemporaryFileBlobImpl::~TemporaryFileBlobImpl() {
+ MOZ_ASSERT(mInputStreamCreated);
+}
+
+already_AddRefed<BlobImpl> TemporaryFileBlobImpl::CreateSlice(
+ uint64_t aStart, uint64_t aLength, const nsAString& aContentType,
+ ErrorResult& aRv) const {
+ MOZ_CRASH("This BlobImpl is not meant to be sliced!");
+ return nullptr;
+}
+
+void TemporaryFileBlobImpl::CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const {
+#ifdef DEBUG
+ MOZ_ASSERT(!mInputStreamCreated);
+ // CreateInputStream can be called only once.
+ mInputStreamCreated = true;
+#endif
+
+ aRv = TemporaryFileInputStream::Create(mFile, aStream);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/TemporaryFileBlobImpl.h b/dom/file/TemporaryFileBlobImpl.h
new file mode 100644
index 0000000000..d11c67f955
--- /dev/null
+++ b/dom/file/TemporaryFileBlobImpl.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 mozilla_dom_TemporaryFileBlobImpl_h
+#define mozilla_dom_TemporaryFileBlobImpl_h
+
+#include "FileBlobImpl.h"
+
+namespace mozilla::dom {
+
+// This class is meant to be used by TemporaryIPCBlobParent only.
+// Don't use it for anything else, please!
+// Note that CreateInputStream() _must_ be called, and called just once!
+
+// This class is a BlobImpl because it needs to be sent via IPC using
+// IPCBlobUtils.
+class TemporaryFileBlobImpl final : public FileBlobImpl {
+#ifdef DEBUG
+ mutable bool mInputStreamCreated;
+#endif
+
+ public:
+ explicit TemporaryFileBlobImpl(nsIFile* aFile, const nsAString& aContentType);
+
+ // Overrides
+ void CreateInputStream(nsIInputStream** aStream,
+ ErrorResult& aRv) const override;
+
+ void GetBlobImplType(nsAString& aBlobImplType) const override {
+ aBlobImplType = u"TemporaryFileBlobImpl"_ns;
+ }
+
+ protected:
+ ~TemporaryFileBlobImpl() override;
+
+ private:
+ already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength,
+ const nsAString& aContentType,
+ ErrorResult& aRv) const override;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_TemporaryFileBlobImpl_h
diff --git a/dom/file/ipc/FileCreatorChild.cpp b/dom/file/ipc/FileCreatorChild.cpp
new file mode 100644
index 0000000000..95ac19f17b
--- /dev/null
+++ b/dom/file/ipc/FileCreatorChild.cpp
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileCreatorChild.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+
+namespace mozilla::dom {
+
+FileCreatorChild::FileCreatorChild() = default;
+
+FileCreatorChild::~FileCreatorChild() { MOZ_ASSERT(!mPromise); }
+
+void FileCreatorChild::SetPromise(Promise* aPromise) {
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(!mPromise);
+
+ mPromise = aPromise;
+}
+
+mozilla::ipc::IPCResult FileCreatorChild::Recv__delete__(
+ const FileCreationResult& aResult) {
+ MOZ_ASSERT(mPromise);
+
+ RefPtr<Promise> promise;
+ promise.swap(mPromise);
+
+ if (aResult.type() == FileCreationResult::TFileCreationErrorResult) {
+ promise->MaybeReject(aResult.get_FileCreationErrorResult().errorCode());
+ return IPC_OK();
+ }
+
+ MOZ_ASSERT(aResult.type() == FileCreationResult::TFileCreationSuccessResult);
+
+ RefPtr<dom::BlobImpl> impl = dom::IPCBlobUtils::Deserialize(
+ aResult.get_FileCreationSuccessResult().blob());
+
+ RefPtr<File> file = File::Create(promise->GetParentObject(), impl);
+ if (NS_WARN_IF(!file)) {
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ return IPC_OK();
+ }
+
+ promise->MaybeResolve(file);
+ return IPC_OK();
+}
+
+void FileCreatorChild::ActorDestroy(ActorDestroyReason aWhy) {
+ if (mPromise) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ mPromise = nullptr;
+ }
+};
+
+} // namespace mozilla::dom
diff --git a/dom/file/ipc/FileCreatorChild.h b/dom/file/ipc/FileCreatorChild.h
new file mode 100644
index 0000000000..eb5dc09d92
--- /dev/null
+++ b/dom/file/ipc/FileCreatorChild.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileCreatorChild_h
+#define mozilla_dom_FileCreatorChild_h
+
+#include "mozilla/dom/PFileCreatorChild.h"
+
+namespace mozilla::dom {
+
+class FileCreatorChild final : public mozilla::dom::PFileCreatorChild {
+ friend class mozilla::dom::PFileCreatorChild;
+
+ public:
+ FileCreatorChild();
+ ~FileCreatorChild() override;
+
+ void SetPromise(Promise* aPromise);
+
+ private:
+ mozilla::ipc::IPCResult Recv__delete__(const FileCreationResult& aResult);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ RefPtr<Promise> mPromise;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileCreatorChild_h
diff --git a/dom/file/ipc/FileCreatorParent.cpp b/dom/file/ipc/FileCreatorParent.cpp
new file mode 100644
index 0000000000..77f8efd483
--- /dev/null
+++ b/dom/file/ipc/FileCreatorParent.cpp
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileCreatorParent.h"
+#include "mozilla/dom/FileBlobImpl.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include "mozilla/dom/MultipartBlobImpl.h"
+#include "nsIFile.h"
+
+namespace mozilla::dom {
+
+FileCreatorParent::FileCreatorParent()
+ : mBackgroundEventTarget(GetCurrentSerialEventTarget()), mIPCActive(true) {}
+
+FileCreatorParent::~FileCreatorParent() = default;
+
+mozilla::ipc::IPCResult FileCreatorParent::CreateAndShareFile(
+ const nsAString& aFullPath, const nsAString& aType, const nsAString& aName,
+ const Maybe<int64_t>& aLastModified, const bool& aExistenceCheck,
+ const bool& aIsFromNsIFile) {
+ RefPtr<dom::BlobImpl> blobImpl;
+ nsresult rv =
+ CreateBlobImpl(aFullPath, aType, aName, aLastModified.isSome(),
+ aLastModified.isSome() ? aLastModified.value() : 0,
+ aExistenceCheck, aIsFromNsIFile, getter_AddRefs(blobImpl));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ (void)Send__delete__(this, FileCreationErrorResult(rv));
+ return IPC_OK();
+ }
+
+ MOZ_ASSERT(blobImpl);
+
+ // FileBlobImpl is unable to return the correct type on this thread because
+ // nsIMIMEService is not thread-safe. We must exec the 'type' getter on
+ // main-thread before send the blob to the child actor.
+
+ RefPtr<FileCreatorParent> self = this;
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "FileCreatorParent::CreateAndShareFile", [self, blobImpl]() {
+ nsAutoString type;
+ blobImpl->GetType(type);
+
+ self->mBackgroundEventTarget->Dispatch(NS_NewRunnableFunction(
+ "FileCreatorParent::CreateAndShareFile return", [self, blobImpl]() {
+ if (self->mIPCActive) {
+ IPCBlob ipcBlob;
+ nsresult rv = dom::IPCBlobUtils::Serialize(blobImpl, ipcBlob);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ (void)Send__delete__(self, FileCreationErrorResult(rv));
+ return;
+ }
+
+ (void)Send__delete__(self, FileCreationSuccessResult(ipcBlob));
+ }
+ }));
+ }));
+
+ return IPC_OK();
+}
+
+void FileCreatorParent::ActorDestroy(ActorDestroyReason aWhy) {
+ mIPCActive = false;
+}
+
+/* static */
+nsresult FileCreatorParent::CreateBlobImpl(
+ const nsAString& aPath, const nsAString& aType, const nsAString& aName,
+ bool aLastModifiedPassed, int64_t aLastModified, bool aExistenceCheck,
+ bool aIsFromNsIFile, BlobImpl** aBlobImpl) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_NewLocalFile(aPath, true, getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ bool exists;
+ rv = file->Exists(&exists);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (aExistenceCheck) {
+ if (!exists) {
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+
+ bool isDir;
+ rv = file->IsDirectory(&isDir);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (isDir) {
+ return NS_ERROR_FILE_IS_DIRECTORY;
+ }
+ }
+
+ RefPtr<FileBlobImpl> impl = new FileBlobImpl(file);
+
+ // If the file doesn't exist, we cannot have its path, its size and so on.
+ // Let's set them now.
+ if (!exists) {
+ MOZ_ASSERT(!aExistenceCheck);
+
+ impl->SetMozFullPath(aPath);
+ impl->SetLastModified(0);
+ impl->SetEmptySize();
+ }
+
+ if (!aName.IsEmpty()) {
+ impl->SetName(aName);
+ }
+
+ if (!aType.IsEmpty()) {
+ impl->SetType(aType);
+ }
+
+ if (aLastModifiedPassed) {
+ impl->SetLastModified(aLastModified);
+ }
+
+ if (!aIsFromNsIFile) {
+ impl->SetMozFullPath(u""_ns);
+ }
+
+ impl.forget(aBlobImpl);
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/ipc/FileCreatorParent.h b/dom/file/ipc/FileCreatorParent.h
new file mode 100644
index 0000000000..f43dbee886
--- /dev/null
+++ b/dom/file/ipc/FileCreatorParent.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 mozilla_dom_FileCreatorParent_h
+#define mozilla_dom_FileCreatorParent_h
+
+#include "mozilla/dom/PFileCreatorParent.h"
+
+class nsIFile;
+
+namespace mozilla::dom {
+
+class BlobImpl;
+
+class FileCreatorParent final : public mozilla::dom::PFileCreatorParent {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileCreatorParent)
+
+ FileCreatorParent();
+
+ mozilla::ipc::IPCResult CreateAndShareFile(
+ const nsAString& aFullPath, const nsAString& aType,
+ const nsAString& aName, const Maybe<int64_t>& aLastModified,
+ const bool& aExistenceCheck, const bool& aIsFromNsIFile);
+
+ private:
+ ~FileCreatorParent() override;
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ nsresult CreateBlobImpl(const nsAString& aPath, const nsAString& aType,
+ const nsAString& aName, bool aLastModifiedPassed,
+ int64_t aLastModified, bool aExistenceCheck,
+ bool aIsFromNsIFile, BlobImpl** aBlobImpl);
+
+ nsCOMPtr<nsIEventTarget> mBackgroundEventTarget;
+ bool mIPCActive;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileCreatorParent_h
diff --git a/dom/file/ipc/IPCBlob.ipdlh b/dom/file/ipc/IPCBlob.ipdlh
new file mode 100644
index 0000000000..4db1c662bd
--- /dev/null
+++ b/dom/file/ipc/IPCBlob.ipdlh
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 IPCStream;
+include ProtocolTypes;
+
+using struct mozilla::void_t from "mozilla/ipc/IPCCore.h";
+[RefCounted] using class mozilla::RemoteLazyInputStream from "mozilla/RemoteLazyInputStream.h";
+
+namespace mozilla {
+
+union RemoteLazyStream
+{
+ // Parent to Child: The child will receive a RemoteLazyInputStream. Nothing
+ // can be done with it except retrieving the size.
+ nullable RemoteLazyInputStream;
+
+ // Child to Parent: Normal serialization.
+ IPCStream;
+};
+
+namespace dom {
+
+// This contains any extra bit for making a File out of a Blob.
+// For more information about Blobs and IPC, please read the comments in
+// IPCBlobUtils.h
+
+struct IPCFile
+{
+ nsString name;
+ int64_t lastModified;
+ nsString DOMPath;
+ nsString fullPath;
+
+ // Useful for Entries API.
+ bool isDirectory;
+};
+
+struct IPCBlob
+{
+ nsString type;
+ uint64_t size;
+ nsString blobImplType;
+
+ RemoteLazyStream inputStream;
+
+ // Nothing is for Blob
+ IPCFile? file;
+
+ // This ID is used only by indexedDB tests.
+ int64_t fileId;
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/file/ipc/IPCBlobUtils.cpp b/dom/file/ipc/IPCBlobUtils.cpp
new file mode 100644
index 0000000000..78409da529
--- /dev/null
+++ b/dom/file/ipc/IPCBlobUtils.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 "IPCBlobUtils.h"
+#include "RemoteLazyInputStream.h"
+#include "RemoteLazyInputStreamChild.h"
+#include "RemoteLazyInputStreamParent.h"
+#include "mozilla/dom/IPCBlob.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ipc/PBackgroundParent.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/ipc/IPCStreamUtils.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "RemoteLazyInputStreamStorage.h"
+#include "StreamBlobImpl.h"
+#include "prtime.h"
+
+namespace mozilla::dom::IPCBlobUtils {
+
+already_AddRefed<BlobImpl> Deserialize(const IPCBlob& aIPCBlob) {
+ nsCOMPtr<nsIInputStream> inputStream;
+
+ const RemoteLazyStream& stream = aIPCBlob.inputStream();
+ switch (stream.type()) {
+ // Parent to child: when an nsIInputStream is sent from parent to child, the
+ // child receives a RemoteLazyInputStream actor.
+ case RemoteLazyStream::TRemoteLazyInputStream: {
+ inputStream = stream.get_RemoteLazyInputStream();
+ break;
+ }
+
+ // Child to Parent: when a blob is created on the content process send it's
+ // sent to the parent, we have an IPCStream object.
+ case RemoteLazyStream::TIPCStream:
+ MOZ_ASSERT(XRE_IsParentProcess());
+ inputStream = DeserializeIPCStream(stream.get_IPCStream());
+ break;
+
+ default:
+ MOZ_CRASH("Unknown type.");
+ break;
+ }
+
+ MOZ_ASSERT(inputStream);
+
+ RefPtr<StreamBlobImpl> blobImpl;
+
+ if (aIPCBlob.file().isNothing()) {
+ blobImpl = StreamBlobImpl::Create(inputStream.forget(), aIPCBlob.type(),
+ aIPCBlob.size(), aIPCBlob.blobImplType());
+ } else {
+ const IPCFile& file = aIPCBlob.file().ref();
+ blobImpl = StreamBlobImpl::Create(inputStream.forget(), file.name(),
+ aIPCBlob.type(), file.lastModified(),
+ aIPCBlob.size(), aIPCBlob.blobImplType());
+ blobImpl->SetDOMPath(file.DOMPath());
+ blobImpl->SetFullPath(file.fullPath());
+ blobImpl->SetIsDirectory(file.isDirectory());
+ }
+
+ blobImpl->SetFileId(aIPCBlob.fileId());
+
+ return blobImpl.forget();
+}
+
+nsresult Serialize(BlobImpl* aBlobImpl, IPCBlob& aIPCBlob) {
+ MOZ_ASSERT(aBlobImpl);
+
+ nsAutoString value;
+ aBlobImpl->GetType(value);
+ aIPCBlob.type() = value;
+
+ aBlobImpl->GetBlobImplType(value);
+ aIPCBlob.blobImplType() = value;
+
+ ErrorResult rv;
+ aIPCBlob.size() = aBlobImpl->GetSize(rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ if (!aBlobImpl->IsFile()) {
+ aIPCBlob.file() = Nothing();
+ } else {
+ IPCFile file;
+
+ aBlobImpl->GetName(value);
+ file.name() = value;
+
+ file.lastModified() = aBlobImpl->GetLastModified(rv) * PR_USEC_PER_MSEC;
+ if (NS_WARN_IF(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ aBlobImpl->GetDOMPath(value);
+ file.DOMPath() = value;
+
+ aBlobImpl->GetMozFullPathInternal(value, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+ file.fullPath() = value;
+
+ file.isDirectory() = aBlobImpl->IsDirectory();
+
+ aIPCBlob.file() = Some(file);
+ }
+
+ aIPCBlob.fileId() = aBlobImpl->GetFileId();
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ aBlobImpl->CreateInputStream(getter_AddRefs(inputStream), rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ if (XRE_IsParentProcess()) {
+ RefPtr<RemoteLazyInputStream> stream =
+ RemoteLazyInputStream::WrapStream(inputStream);
+ if (NS_WARN_IF(!stream)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aIPCBlob.inputStream() = stream;
+ return NS_OK;
+ }
+
+ mozilla::ipc::IPCStream stream;
+ if (!mozilla::ipc::SerializeIPCStream(inputStream.forget(), stream,
+ /* aAllowLazy */ true)) {
+ return NS_ERROR_FAILURE;
+ }
+ aIPCBlob.inputStream() = stream;
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::IPCBlobUtils
+
+namespace IPC {
+
+void ParamTraits<mozilla::dom::BlobImpl*>::Write(
+ IPC::MessageWriter* aWriter, mozilla::dom::BlobImpl* aParam) {
+ nsresult rv;
+ mozilla::dom::IPCBlob ipcblob;
+ if (aParam) {
+ rv = mozilla::dom::IPCBlobUtils::Serialize(aParam, ipcblob);
+ }
+ if (!aParam || NS_WARN_IF(NS_FAILED(rv))) {
+ WriteParam(aWriter, false);
+ } else {
+ WriteParam(aWriter, true);
+ WriteParam(aWriter, ipcblob);
+ }
+}
+
+bool ParamTraits<mozilla::dom::BlobImpl*>::Read(
+ IPC::MessageReader* aReader, RefPtr<mozilla::dom::BlobImpl>* aResult) {
+ *aResult = nullptr;
+
+ bool notnull = false;
+ if (!ReadParam(aReader, &notnull)) {
+ return false;
+ }
+ if (notnull) {
+ mozilla::dom::IPCBlob ipcblob;
+ if (!ReadParam(aReader, &ipcblob)) {
+ return false;
+ }
+ *aResult = mozilla::dom::IPCBlobUtils::Deserialize(ipcblob);
+ }
+ return true;
+}
+
+} // namespace IPC
diff --git a/dom/file/ipc/IPCBlobUtils.h b/dom/file/ipc/IPCBlobUtils.h
new file mode 100644
index 0000000000..17fce3195a
--- /dev/null
+++ b/dom/file/ipc/IPCBlobUtils.h
@@ -0,0 +1,268 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_IPCBlobUtils_h
+#define mozilla_dom_IPCBlobUtils_h
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/ipc/IPDLParamTraits.h"
+
+/*
+ * Blobs and IPC
+ * ~~~~~~~~~~~~~
+ *
+ * Simplifying, DOM Blob objects are chunks of data with a content type and a
+ * size. DOM Files are Blobs with a name. They are are used in many APIs and
+ * they can be cloned and sent cross threads and cross processes.
+ *
+ * If we see Blobs from a platform point of view, the main (and often, the only)
+ * interesting part is how to retrieve data from it. This is done via
+ * nsIInputStream and, except for a couple of important details, this stream is
+ * used in the parent process.
+ *
+ * For this reason, when we consider the serialization of a blob via IPC
+ * messages, the biggest effort is put in how to manage the nsInputStream
+ * correctly. To serialize, we use the IPCBlob data struct: basically, the blob
+ * properties (size, type, name if it's a file) and the nsIInputStream.
+ *
+ * Before talking about the nsIInputStream it's important to say that we have
+ * different kinds of Blobs, based on the different kinds of sources. A non
+ * exaustive list is:
+ * - a memory buffer: MemoryBlobImpl
+ * - a string: StringBlobImpl
+ * - a real OS file: FileBlobImpl
+ * - a generic nsIInputStream: StreamBlobImpl
+ * - an empty blob: EmptyBlobImpl
+ * - more blobs combined together: MultipartBlobImpl
+ * Each one of these implementations has a custom ::CreateInputStream method.
+ * So, basically, each one has a different kind of nsIInputStream (nsFileStream,
+ * nsIStringInputStream, SlicedInputStream, and so on).
+ *
+ * Another important point to keep in mind is that a Blob can be created on the
+ * content process (for example: |new Blob([123])|) or it can be created on the
+ * parent process and sent to content (a FilePicker creates Blobs and it runs on
+ * the parent process).
+ *
+ * DocumentLoadListener uses blobs to serialize the POST data back to the
+ * content process (for insertion into session history). This lets it correctly
+ * handle OS files by reference, and avoid copying the underlying buffer data
+ * unless it is read. This can hopefully be removed once SessionHistory is
+ * handled in the parent process.
+ *
+ * Child to Parent Blob Serialization
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * When a document creates a blob, this can be sent, for different reasons to
+ * the parent process. For instance it can be sent as part of a FormData, or it
+ * can be converted to a BlobURL and broadcasted to any other existing
+ * processes.
+ *
+ * When this happens, we use the IPCStream data struct for the serialization
+ * of the nsIInputStream. This means that, if the stream is fully serializable
+ * and its size is lower than 1Mb, we are able to recreate the stream completely
+ * on the parent side. This happens, basically with any kind of child-to-parent
+ * stream except for huge memory streams. In this case we end up using
+ * DataPipe. See more information in IPCStreamUtils.h.
+ *
+ * In order to populate IPCStream correctly, we use SerializeIPCStream as
+ * documented in IPCStreamUtils.h.
+ *
+ * Parent to Child Blob Serialization
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * This scenario is common when we talk about Blobs pointing to real files:
+ * HTMLInputElement (type=file), or Entries API, DataTransfer and so on. But we
+ * also have this scenario when a content process creates a Blob and it
+ * broadcasts it because of a BlobURL or because BroadcastChannel API is used.
+ *
+ * The approach here is this: normally, the content process doesn't really read
+ * data from the blob nsIInputStream. The content process needs to have the
+ * nsIInputStream and be able to send it back to the parent process when the
+ * "real" work needs to be done. This is true except for 2 usecases: FileReader
+ * API and BlobURL usage. So, if we ignore these 2, normally, the parent sends a
+ * blob nsIInputStream to a content process, and then, it will receive it back
+ * in order to do some networking, or whatever.
+ *
+ * For this reason, IPCBlobUtils uses a particular protocol for serializing
+ * nsIInputStream parent to child: PRemoteLazyInputStream. This protocol keeps
+ * the original nsIInputStream alive on the parent side, and gives its size and
+ * a UUID to the child side. The child side creates a RemoteLazyInputStream and
+ * that is incapsulated into a StreamBlobImpl.
+ *
+ * The UUID is useful when the content process sends the same nsIInputStream
+ * back to the parent process because, the only information it has to share is
+ * the UUID. Each nsIInputStream sent via PRemoteLazyInputStream, is registered
+ * into the RemoteLazyInputStreamStorage.
+ *
+ * On the content process side, RemoteLazyInputStream is a special inputStream:
+ * the only reliable methods are:
+ * - nsIInputStream.available() - the size is shared by PRemoteLazyInputStream
+ * actor.
+ * - nsIIPCSerializableInputStream.serialize() - we can give back this stream to
+ * the parent because we know its UUID.
+ * - nsICloneableInputStream.cloneable() and nsICloneableInputStream.clone() -
+ * this stream can be cloned. We just need to have a reference of the
+ * PRemoteLazyInputStream actor and its UUID.
+ * - nsIAsyncInputStream.asyncWait() - see next section.
+ *
+ * Any other method (read, readSegment and so on) will fail if asyncWait() is
+ * not previously called (see the next section). Basically, this inputStream
+ * cannot be used synchronously for any 'real' reading operation.
+ *
+ * When the parent receives the serialization of a RemoteLazyInputStream, it is
+ * able to retrieve the correct nsIInputStream using the UUID and
+ * RemoteLazyInputStreamStorage.
+ *
+ * Parent to Child Streams, FileReader and BlobURL
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * The FileReader and BlobURL scenarios are described here.
+ *
+ * When content process needs to read data from a Blob sent from the parent
+ * process, it must do it asynchronously using RemoteLazyInputStream as a
+ * nsIAsyncInputStream stream. This happens calling
+ * RemoteLazyInputStream.asyncWait(). At that point, the child actor will send a
+ * StreamNeeded() IPC message to the parent side. When this is received, the
+ * parent retrieves the 'real' stream from RemoteLazyInputStreamStorage using
+ * the UUID, it will serialize the 'real' stream, and it will send it to the
+ * child side.
+ *
+ * When the 'real' stream is received (RecvStreamReady()), the asyncWait
+ * callback will be executed and, from that moment, any RemoteLazyInputStream
+ * method will be forwarded to the 'real' stream ones. This means that the
+ * reading will be available.
+ *
+ * RemoteLazyInputStream Thread
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * RemoteLazyInputStreamChild actor can be created in any thread (sort of) and
+ * their top-level IPDL protocol is PBackground. These actors are wrapped by 1
+ * or more RemoteLazyInputStream objects in order to expose nsIInputStream
+ * interface and be thread-safe.
+ *
+ * But IPDL actors are not thread-safe and any SendFoo() method must be executed
+ * on the owning thread. This means that this thread must be kept alive for the
+ * life-time of the RemoteLazyInputStream.
+ *
+ * In doing this, there are 2 main issues:
+ * a. if a remote Blob is created on a worker (because of a
+ * BroadcastChannel/MessagePort for instance) and it sent to the main-thread
+ * via PostMessage(), we have to keep that worker alive.
+ * b. if the remote Blob is created on the main-thread, any SendFoo() has to be
+ * executed on the main-thread. This is true also when the inputStream is
+ * used on another thread (note that nsIInputStream could do I/O and usually
+ * they are used on special I/O threads).
+ *
+ * In order to avoid this, RemoteLazyInputStreamChild are 'migrated' to a
+ * DOM-File thread. This is done in this way:
+ *
+ * 1. If RemoteLazyInputStreamChild actor is not already owned by DOM-File
+ * thread, it calls Send__delete__ in order to inform the parent side that we
+ * don't need this IPC channel on the current thread.
+ * 2. A new RemoteLazyInputStreamChild is created. RemoteLazyInputStreamThread
+ * is used to assign this actor to the DOM-File thread.
+ * RemoteLazyInputStreamThread::GetOrCreate() creates the DOM-File thread if
+ * it doesn't exist yet. Pending operations and RemoteLazyInputStreams are
+ * moved onto the new actor.
+ * 3. RemoteLazyInputStreamParent::Recv__delete__ is called on the parent side
+ * and the parent actor is deleted. Doing this we don't remove the UUID from
+ * RemoteLazyInputStreamStorage.
+ * 4. The RemoteLazyInputStream constructor is sent with the new
+ * RemoteLazyInputStreamChild actor, with the DOM-File thread's PBackground
+ * as its manager.
+ * 5. When the new RemoteLazyInputStreamParent actor is created, it will receive
+ * the same UUID of the previous parent actor. The nsIInputStream will be
+ * retrieved from RemoteLazyInputStreamStorage.
+ * 6. In order to avoid leaks, RemoteLazyInputStreamStorage will monitor child
+ * processes and in case one of them dies, it will release the
+ * nsIInputStream objects belonging to that process.
+ *
+ * If any API wants to retrieve a 'real inputStream when the migration is in
+ * progress, that operation is stored in a pending queue and processed at the
+ * end of the migration.
+ *
+ * IPCBlob and nsIAsyncInputStream
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * RemoteLazyInputStream is always async. If the remote inputStream is not
+ * async, RemoteLazyInputStream will create a pipe stream around it in order to
+ * be consistently async.
+ *
+ * Slicing IPCBlob
+ * ~~~~~~~~~~~~~~~
+ *
+ * Normally, slicing a blob consists of the creation of a new Blob, with a
+ * SlicedInputStream() wrapping a clone of the original inputStream. But this
+ * approach is extremely inefficient with IPCBlob, because it could be that we
+ * wrap the pipe stream and not the remote inputStream (See the previous section
+ * of this documentation). If we end up doing so, also if the remote
+ * inputStream is seekable, the pipe will not be, and in order to reach the
+ * starting point, SlicedInputStream will do consecutive read()s.
+ *
+ * This problem is fixed implmenting nsICloneableWithRange in
+ * RemoteLazyInputStream and using cloneWithRange() when a StreamBlobImpl is
+ * sliced. When the remote stream is received, it will be sliced directly.
+ *
+ * If we want to represent the hierarchy of the InputStream classes, instead
+ * of having: |SlicedInputStream(RemoteLazyInputStream(Async
+ * Pipe(RemoteStream)))|, we have: |RemoteLazyInputStream(Async
+ * Pipe(SlicedInputStream(RemoteStream)))|.
+ *
+ * When RemoteLazyInputStream is serialized and sent to the parent process,
+ * start and range are sent too and SlicedInputStream is used in the parent side
+ * as well.
+ *
+ * Socket Process
+ * ~~~~~~~~~~~~~~
+ *
+ * The socket process is a separate process used to do networking operations.
+ * When a website sends a blob as the body of a POST/PUT request, we need to
+ * send the corresponding RemoteLazyInputStream to the socket process.
+ *
+ * This is the only serialization of RemoteLazyInputStream from parent to child
+ * process and it works _only_ for the socket process. Do not expose this
+ * serialization to PContent or PBackground or any other top-level IPDL protocol
+ * without a DOM File peer review!
+ *
+ * The main difference between Socket Process is that DOM-File thread is not
+ * used. Here is a list of reasons:
+ * - DOM-File moves the ownership of the RemoteLazyInputStream actors to
+ * PBackground, but in the Socket Process we don't have PBackground (yet?)
+ * - Socket Process is a stable process with a simple life-time configuration:
+ * we can keep the actors on the main-thread because no Workers are involved.
+ */
+
+namespace mozilla::dom {
+
+class IPCBlob;
+
+namespace IPCBlobUtils {
+
+already_AddRefed<BlobImpl> Deserialize(const IPCBlob& aIPCBlob);
+
+nsresult Serialize(BlobImpl* aBlobImpl, IPCBlob& aIPCBlob);
+
+} // namespace IPCBlobUtils
+} // namespace mozilla::dom
+
+namespace IPC {
+
+// ParamTraits implementation for BlobImpl. N.B: If the original BlobImpl cannot
+// be successfully serialized, a warning will be produced and a nullptr will be
+// sent over the wire. When Read()-ing a BlobImpl,
+// __always make sure to handle null!__
+template <>
+struct ParamTraits<mozilla::dom::BlobImpl*> {
+ static void Write(IPC::MessageWriter* aWriter,
+ mozilla::dom::BlobImpl* aParam);
+ static bool Read(IPC::MessageReader* aReader,
+ RefPtr<mozilla::dom::BlobImpl>* aResult);
+};
+
+} // namespace IPC
+
+#endif // mozilla_dom_IPCBlobUtils_h
diff --git a/dom/file/ipc/PFileCreator.ipdl b/dom/file/ipc/PFileCreator.ipdl
new file mode 100644
index 0000000000..d690820906
--- /dev/null
+++ b/dom/file/ipc/PFileCreator.ipdl
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PBackground;
+
+include IPCBlob;
+
+namespace mozilla {
+namespace dom {
+
+struct FileCreationSuccessResult
+{
+ IPCBlob blob;
+};
+
+struct FileCreationErrorResult
+{
+ nsresult errorCode;
+};
+
+union FileCreationResult
+{
+ FileCreationSuccessResult;
+ FileCreationErrorResult;
+};
+
+[ManualDealloc]
+protocol PFileCreator
+{
+ manager PBackground;
+
+child:
+ async __delete__(FileCreationResult aResult);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/file/ipc/PRemoteLazyInputStream.ipdl b/dom/file/ipc/PRemoteLazyInputStream.ipdl
new file mode 100644
index 0000000000..82fab20cdb
--- /dev/null
+++ b/dom/file/ipc/PRemoteLazyInputStream.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 IPCStream;
+
+namespace mozilla {
+
+protocol PRemoteLazyInputStream
+{
+parent:
+ async Clone(Endpoint<PRemoteLazyInputStreamParent> aCloneEndpoint);
+
+ async StreamNeeded(uint64_t aStart, uint64_t aLength) returns (IPCStream? stream);
+
+ async LengthNeeded() returns (int64_t length);
+
+ async Goodbye();
+};
+
+} // namespace mozilla
diff --git a/dom/file/ipc/PTemporaryIPCBlob.ipdl b/dom/file/ipc/PTemporaryIPCBlob.ipdl
new file mode 100644
index 0000000000..2645d6fd50
--- /dev/null
+++ b/dom/file/ipc/PTemporaryIPCBlob.ipdl
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PBackground;
+
+include IPCBlob;
+
+namespace mozilla {
+namespace dom {
+
+union IPCBlobOrError
+{
+ IPCBlob;
+ nsresult;
+};
+
+[ManualDealloc]
+protocol PTemporaryIPCBlob
+{
+ manager PBackground;
+
+ // When this actor is created on the child side, the parent will send
+ // immediatelly back a FileDescriptor or a __delete__ in case of error.
+ // When the FileDescriptor is received, the child has to call
+ // OperationDone(). When OperationDone() is received on the parent side, the
+ // parent actor will send a __delete__.
+
+child:
+ async FileDesc(FileDescriptor aFD);
+ async __delete__(IPCBlobOrError aBlobOrError);
+
+parent:
+ async OperationFailed();
+
+ async OperationDone(nsCString aContentType, FileDescriptor aFD);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/file/ipc/RemoteLazyInputStream.cpp b/dom/file/ipc/RemoteLazyInputStream.cpp
new file mode 100644
index 0000000000..936438df13
--- /dev/null
+++ b/dom/file/ipc/RemoteLazyInputStream.cpp
@@ -0,0 +1,1458 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "RemoteLazyInputStream.h"
+#include "RemoteLazyInputStreamChild.h"
+#include "RemoteLazyInputStreamParent.h"
+#include "chrome/common/ipc_message_utils.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/Logging.h"
+#include "mozilla/PRemoteLazyInputStream.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/ipc/InputStreamParams.h"
+#include "mozilla/ipc/MessageChannel.h"
+#include "mozilla/ipc/ProtocolMessageUtils.h"
+#include "mozilla/net/SocketProcessParent.h"
+#include "mozilla/SlicedInputStream.h"
+#include "mozilla/NonBlockingAsyncInputStream.h"
+#include "nsIAsyncInputStream.h"
+#include "nsIAsyncOutputStream.h"
+#include "nsID.h"
+#include "nsIInputStream.h"
+#include "nsIPipe.h"
+#include "nsNetUtil.h"
+#include "nsStreamUtils.h"
+#include "nsStringStream.h"
+#include "RemoteLazyInputStreamStorage.h"
+#include "RemoteLazyInputStreamThread.h"
+
+namespace mozilla {
+
+mozilla::LazyLogModule gRemoteLazyStreamLog("RemoteLazyStream");
+
+namespace {
+
+class InputStreamCallbackRunnable final : public DiscardableRunnable {
+ public:
+ // Note that the execution can be synchronous in case the event target is
+ // null.
+ static void Execute(already_AddRefed<nsIInputStreamCallback> aCallback,
+ already_AddRefed<nsIEventTarget> aEventTarget,
+ RemoteLazyInputStream* aStream) {
+ RefPtr<InputStreamCallbackRunnable> runnable =
+ new InputStreamCallbackRunnable(std::move(aCallback), aStream);
+
+ nsCOMPtr<nsIEventTarget> target = std::move(aEventTarget);
+ if (target) {
+ target->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ } else {
+ runnable->Run();
+ }
+ }
+
+ NS_IMETHOD
+ Run() override {
+ mCallback->OnInputStreamReady(mStream);
+ mCallback = nullptr;
+ mStream = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ InputStreamCallbackRunnable(
+ already_AddRefed<nsIInputStreamCallback> aCallback,
+ RemoteLazyInputStream* aStream)
+ : DiscardableRunnable("dom::InputStreamCallbackRunnable"),
+ mCallback(std::move(aCallback)),
+ mStream(aStream) {
+ MOZ_ASSERT(mCallback);
+ MOZ_ASSERT(mStream);
+ }
+
+ RefPtr<nsIInputStreamCallback> mCallback;
+ RefPtr<RemoteLazyInputStream> mStream;
+};
+
+class FileMetadataCallbackRunnable final : public DiscardableRunnable {
+ public:
+ static void Execute(nsIFileMetadataCallback* aCallback,
+ nsIEventTarget* aEventTarget,
+ RemoteLazyInputStream* aStream) {
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(aEventTarget);
+
+ RefPtr<FileMetadataCallbackRunnable> runnable =
+ new FileMetadataCallbackRunnable(aCallback, aStream);
+
+ nsCOMPtr<nsIEventTarget> target = aEventTarget;
+ target->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ }
+
+ NS_IMETHOD
+ Run() override {
+ mCallback->OnFileMetadataReady(mStream);
+ mCallback = nullptr;
+ mStream = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ FileMetadataCallbackRunnable(nsIFileMetadataCallback* aCallback,
+ RemoteLazyInputStream* aStream)
+ : DiscardableRunnable("dom::FileMetadataCallbackRunnable"),
+ mCallback(aCallback),
+ mStream(aStream) {
+ MOZ_ASSERT(mCallback);
+ MOZ_ASSERT(mStream);
+ }
+
+ nsCOMPtr<nsIFileMetadataCallback> mCallback;
+ RefPtr<RemoteLazyInputStream> mStream;
+};
+
+} // namespace
+
+NS_IMPL_ADDREF(RemoteLazyInputStream);
+NS_IMPL_RELEASE(RemoteLazyInputStream);
+
+NS_INTERFACE_MAP_BEGIN(RemoteLazyInputStream)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStream)
+ NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStream)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback)
+ NS_INTERFACE_MAP_ENTRY(nsICloneableInputStream)
+ NS_INTERFACE_MAP_ENTRY(nsICloneableInputStreamWithRange)
+ NS_INTERFACE_MAP_ENTRY(nsIIPCSerializableInputStream)
+ NS_INTERFACE_MAP_ENTRY(nsIFileMetadata)
+ NS_INTERFACE_MAP_ENTRY(nsIAsyncFileMetadata)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStreamLength)
+ NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStreamLength)
+ NS_INTERFACE_MAP_ENTRY(mozIRemoteLazyInputStream)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream)
+NS_INTERFACE_MAP_END
+
+RemoteLazyInputStream::RemoteLazyInputStream(RemoteLazyInputStreamChild* aActor,
+ uint64_t aStart, uint64_t aLength)
+ : mStart(aStart), mLength(aLength), mState(eInit), mActor(aActor) {
+ MOZ_ASSERT(aActor);
+
+ mActor->StreamCreated();
+
+ auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr);
+ if (storage) {
+ nsCOMPtr<nsIInputStream> stream;
+ storage->GetStream(mActor->StreamID(), mStart, mLength,
+ getter_AddRefs(stream));
+ if (stream) {
+ mState = eRunning;
+ mInnerStream = stream;
+ }
+ }
+}
+
+RemoteLazyInputStream::RemoteLazyInputStream(nsIInputStream* aStream)
+ : mStart(0), mLength(UINT64_MAX), mState(eRunning), mInnerStream(aStream) {}
+
+static already_AddRefed<RemoteLazyInputStreamChild> BindChildActor(
+ nsID aId, mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> aEndpoint) {
+ auto* thread = RemoteLazyInputStreamThread::GetOrCreate();
+ if (NS_WARN_IF(!thread)) {
+ return nullptr;
+ }
+ auto actor = MakeRefPtr<RemoteLazyInputStreamChild>(aId);
+ thread->Dispatch(
+ NS_NewRunnableFunction("RemoteLazyInputStream::BindChildActor",
+ [actor, childEp = std::move(aEndpoint)]() mutable {
+ bool ok = childEp.Bind(actor);
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("Binding child actor for %s (%p): %s",
+ nsIDToCString(actor->StreamID()).get(),
+ actor.get(), ok ? "OK" : "ERROR"));
+ }));
+
+ return actor.forget();
+}
+
+already_AddRefed<RemoteLazyInputStream> RemoteLazyInputStream::WrapStream(
+ nsIInputStream* aInputStream) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (nsCOMPtr<mozIRemoteLazyInputStream> lazyStream =
+ do_QueryInterface(aInputStream)) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("Returning already-wrapped stream"));
+ return lazyStream.forget().downcast<RemoteLazyInputStream>();
+ }
+
+ // If we have a stream and are in the parent process, create a new actor pair
+ // and transfer ownership of the stream into storage.
+ auto streamStorage = RemoteLazyInputStreamStorage::Get();
+ if (NS_WARN_IF(streamStorage.isErr())) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning,
+ ("Cannot wrap with no storage!"));
+ return nullptr;
+ }
+
+ nsID id = nsID::GenerateUUID();
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Wrapping stream %p as %s", aInputStream, nsIDToCString(id).get()));
+ streamStorage.inspect()->AddStream(aInputStream, id);
+
+ mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent> parentEp;
+ mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> childEp;
+ MOZ_ALWAYS_SUCCEEDS(
+ PRemoteLazyInputStream::CreateEndpoints(&parentEp, &childEp));
+
+ // Bind the actor on our background thread.
+ streamStorage.inspect()->TaskQueue()->Dispatch(NS_NewRunnableFunction(
+ "RemoteLazyInputStreamParent::Bind",
+ [parentEp = std::move(parentEp), id]() mutable {
+ auto actor = MakeRefPtr<RemoteLazyInputStreamParent>(id);
+ bool ok = parentEp.Bind(actor);
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("Binding parent actor for %s (%p): %s",
+ nsIDToCString(id).get(), actor.get(), ok ? "OK" : "ERROR"));
+ }));
+
+ RefPtr<RemoteLazyInputStreamChild> actor =
+ BindChildActor(id, std::move(childEp));
+
+ if (!actor) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning,
+ ("Wrapping stream failed as we are probably late in shutdown!"));
+ return do_AddRef(new RemoteLazyInputStream());
+ }
+
+ return do_AddRef(new RemoteLazyInputStream(actor));
+}
+
+NS_IMETHODIMP RemoteLazyInputStream::TakeInternalStream(
+ nsIInputStream** aStream) {
+ RefPtr<RemoteLazyInputStreamChild> actor;
+ {
+ MutexAutoLock lock(mMutex);
+ if (mState == eInit || mState == ePending) {
+ return NS_BASE_STREAM_WOULD_BLOCK;
+ }
+ if (mState == eClosed) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+ if (mInputStreamCallback) {
+ MOZ_ASSERT_UNREACHABLE(
+ "Do not call TakeInternalStream after calling AsyncWait");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // Take the inner stream and return it, then close ourselves.
+ if (mInnerStream) {
+ mInnerStream.forget(aStream);
+ } else if (mAsyncInnerStream) {
+ mAsyncInnerStream.forget(aStream);
+ }
+ mState = eClosed;
+ actor = mActor.forget();
+ }
+ if (actor) {
+ actor->StreamConsumed();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP RemoteLazyInputStream::GetInternalStreamID(nsID& aID) {
+ MutexAutoLock lock(mMutex);
+ if (!mActor) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ aID = mActor->StreamID();
+ return NS_OK;
+}
+
+RemoteLazyInputStream::~RemoteLazyInputStream() { Close(); }
+
+nsCString RemoteLazyInputStream::Describe() {
+ const char* state = "?";
+ switch (mState) {
+ case eInit:
+ state = "i";
+ break;
+ case ePending:
+ state = "p";
+ break;
+ case eRunning:
+ state = "r";
+ break;
+ case eClosed:
+ state = "c";
+ break;
+ }
+ return nsPrintfCString(
+ "[%p, %s, %s, %p%s, %s%s|%s%s]", this, state,
+ mActor ? nsIDToCString(mActor->StreamID()).get() : "<no actor>",
+ mInnerStream ? mInnerStream.get() : mAsyncInnerStream.get(),
+ mAsyncInnerStream ? "(A)" : "", mInputStreamCallback ? "I" : "",
+ mInputStreamCallbackEventTarget ? "+" : "",
+ mFileMetadataCallback ? "F" : "",
+ mFileMetadataCallbackEventTarget ? "+" : "");
+}
+
+// nsIInputStream interface
+
+NS_IMETHODIMP
+RemoteLazyInputStream::Available(uint64_t* aLength) {
+ nsCOMPtr<nsIAsyncInputStream> stream;
+ {
+ MutexAutoLock lock(mMutex);
+
+ // We don't have a remoteStream yet: let's return 0.
+ if (mState == eInit || mState == ePending) {
+ *aLength = 0;
+ return NS_OK;
+ }
+
+ if (mState == eClosed) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ MOZ_ASSERT(mState == eRunning);
+ MOZ_ASSERT(mInnerStream || mAsyncInnerStream);
+
+ nsresult rv = EnsureAsyncRemoteStream();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ stream = mAsyncInnerStream;
+ }
+
+ MOZ_ASSERT(stream);
+ return stream->Available(aLength);
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStream::StreamStatus() {
+ nsCOMPtr<nsIAsyncInputStream> stream;
+ {
+ MutexAutoLock lock(mMutex);
+
+ // We don't have a remoteStream yet: let's return 0.
+ if (mState == eInit || mState == ePending) {
+ return NS_OK;
+ }
+
+ if (mState == eClosed) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ MOZ_ASSERT(mState == eRunning);
+ MOZ_ASSERT(mInnerStream || mAsyncInnerStream);
+
+ nsresult rv = EnsureAsyncRemoteStream();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ stream = mAsyncInnerStream;
+ }
+
+ MOZ_ASSERT(stream);
+ return stream->StreamStatus();
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStream::Read(char* aBuffer, uint32_t aCount,
+ uint32_t* aReadCount) {
+ nsCOMPtr<nsIAsyncInputStream> stream;
+ {
+ MutexAutoLock lock(mMutex);
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Read(%u) %s", aCount, Describe().get()));
+
+ // Read is not available is we don't have a remoteStream.
+ if (mState == eInit || mState == ePending) {
+ return NS_BASE_STREAM_WOULD_BLOCK;
+ }
+
+ if (mState == eClosed) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ MOZ_ASSERT(mState == eRunning);
+ MOZ_ASSERT(mInnerStream || mAsyncInnerStream);
+
+ nsresult rv = EnsureAsyncRemoteStream();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ stream = mAsyncInnerStream;
+ }
+
+ MOZ_ASSERT(stream);
+ nsresult rv = stream->Read(aBuffer, aCount, aReadCount);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // If some data has been read, we mark the stream as consumed.
+ if (*aReadCount > 0) {
+ MarkConsumed();
+ }
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Read %u/%u bytes", *aReadCount, aCount));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStream::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure,
+ uint32_t aCount, uint32_t* aResult) {
+ nsCOMPtr<nsIAsyncInputStream> stream;
+ {
+ MutexAutoLock lock(mMutex);
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("ReadSegments(%u) %s", aCount, Describe().get()));
+
+ // ReadSegments is not available is we don't have a remoteStream.
+ if (mState == eInit || mState == ePending) {
+ return NS_BASE_STREAM_WOULD_BLOCK;
+ }
+
+ if (mState == eClosed) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ MOZ_ASSERT(mState == eRunning);
+ MOZ_ASSERT(mInnerStream || mAsyncInnerStream);
+
+ nsresult rv = EnsureAsyncRemoteStream();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning,
+ ("EnsureAsyncRemoteStream failed! %s %s",
+ mozilla::GetStaticErrorName(rv), Describe().get()));
+ return rv;
+ }
+
+ stream = mAsyncInnerStream;
+ }
+
+ MOZ_ASSERT(stream);
+ nsresult rv = stream->ReadSegments(aWriter, aClosure, aCount, aResult);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // If some data has been read, we mark the stream as consumed.
+ if (*aResult != 0) {
+ MarkConsumed();
+ }
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("ReadSegments %u/%u bytes", *aResult, aCount));
+
+ return NS_OK;
+}
+
+void RemoteLazyInputStream::MarkConsumed() {
+ RefPtr<RemoteLazyInputStreamChild> actor;
+ {
+ MutexAutoLock lock(mMutex);
+ if (mActor) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("MarkConsumed %s", Describe().get()));
+ }
+
+ actor = mActor.forget();
+ }
+ if (actor) {
+ actor->StreamConsumed();
+ }
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStream::IsNonBlocking(bool* aNonBlocking) {
+ *aNonBlocking = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStream::Close() {
+ RefPtr<RemoteLazyInputStreamChild> actor;
+
+ nsCOMPtr<nsIAsyncInputStream> asyncInnerStream;
+ nsCOMPtr<nsIInputStream> innerStream;
+
+ RefPtr<nsIInputStreamCallback> inputStreamCallback;
+ nsCOMPtr<nsIEventTarget> inputStreamCallbackEventTarget;
+
+ {
+ MutexAutoLock lock(mMutex);
+ if (mState == eClosed) {
+ return NS_OK;
+ }
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("Close %s", Describe().get()));
+
+ actor = mActor.forget();
+
+ asyncInnerStream = mAsyncInnerStream.forget();
+ innerStream = mInnerStream.forget();
+
+ // TODO(Bug 1737783): Notify to the mFileMetadataCallback that this
+ // lazy input stream has been closed.
+ mFileMetadataCallback = nullptr;
+ mFileMetadataCallbackEventTarget = nullptr;
+
+ inputStreamCallback = mInputStreamCallback.forget();
+ inputStreamCallbackEventTarget = mInputStreamCallbackEventTarget.forget();
+
+ mState = eClosed;
+ }
+
+ if (actor) {
+ actor->StreamConsumed();
+ }
+
+ if (inputStreamCallback) {
+ InputStreamCallbackRunnable::Execute(
+ inputStreamCallback.forget(), inputStreamCallbackEventTarget.forget(),
+ this);
+ }
+
+ if (asyncInnerStream) {
+ asyncInnerStream->CloseWithStatus(NS_BASE_STREAM_CLOSED);
+ }
+
+ if (innerStream) {
+ innerStream->Close();
+ }
+
+ return NS_OK;
+}
+
+// nsICloneableInputStream interface
+
+NS_IMETHODIMP
+RemoteLazyInputStream::GetCloneable(bool* aCloneable) {
+ *aCloneable = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStream::Clone(nsIInputStream** aResult) {
+ return CloneWithRange(0, UINT64_MAX, aResult);
+}
+
+// nsICloneableInputStreamWithRange interface
+
+NS_IMETHODIMP
+RemoteLazyInputStream::CloneWithRange(uint64_t aStart, uint64_t aLength,
+ nsIInputStream** aResult) {
+ MutexAutoLock lock(mMutex);
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("CloneWithRange %" PRIu64 " %" PRIu64 " %s", aStart, aLength,
+ Describe().get()));
+
+ nsresult rv;
+
+ RefPtr<RemoteLazyInputStream> stream;
+ if (mState == eClosed) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Cloning closed stream"));
+ stream = new RemoteLazyInputStream();
+ stream.forget(aResult);
+ return NS_OK;
+ }
+
+ uint64_t start = 0;
+ uint64_t length = 0;
+ auto maxLength = CheckedUint64(mLength) - aStart;
+ if (maxLength.isValid()) {
+ start = mStart + aStart;
+ length = std::min(maxLength.value(), aLength);
+ }
+
+ // If the slice would be empty, wrap an empty input stream and return it.
+ if (length == 0) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Creating empty stream"));
+
+ nsCOMPtr<nsIInputStream> emptyStream;
+ rv = NS_NewCStringInputStream(getter_AddRefs(emptyStream), ""_ns);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ stream = new RemoteLazyInputStream(emptyStream);
+ stream.forget(aResult);
+ return NS_OK;
+ }
+
+ // If we still have a connection to our actor, that means we haven't read any
+ // data yet, and can clone + slice by building a new stream backed by the same
+ // actor.
+ if (mActor) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Cloning stream with actor"));
+
+ stream = new RemoteLazyInputStream(mActor, start, length);
+ stream.forget(aResult);
+ return NS_OK;
+ }
+
+ // We no longer have our actor, either because we were constructed without
+ // one, or we've already begun reading. Perform the clone locally on our inner
+ // input stream.
+
+ nsCOMPtr<nsIInputStream> innerStream = mInnerStream;
+ if (mAsyncInnerStream) {
+ innerStream = mAsyncInnerStream;
+ }
+
+ nsCOMPtr<nsICloneableInputStream> cloneable = do_QueryInterface(innerStream);
+ if (!cloneable || !cloneable->GetCloneable()) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Cloning non-cloneable stream - copying to pipe"));
+
+ // If our internal stream isn't cloneable, to perform a clone we'll need to
+ // copy into a pipe and replace our internal stream.
+ nsCOMPtr<nsIAsyncInputStream> pipeIn;
+ nsCOMPtr<nsIAsyncOutputStream> pipeOut;
+ NS_NewPipe2(getter_AddRefs(pipeIn), getter_AddRefs(pipeOut), true, true);
+
+ RefPtr<RemoteLazyInputStreamThread> thread =
+ RemoteLazyInputStreamThread::GetOrCreate();
+ if (NS_WARN_IF(!thread)) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ mAsyncInnerStream = pipeIn;
+ mInnerStream = nullptr;
+
+ // If we have a callback pending, we need to re-call AsyncWait on the inner
+ // stream. This should not re-enter us immediately, as `pipeIn` hasn't been
+ // sent any data yet, but we may be called again as soon as `NS_AsyncCopy`
+ // has begun copying.
+ if (mInputStreamCallback) {
+ mAsyncInnerStream->AsyncWait(this, mInputStreamCallbackFlags,
+ mInputStreamCallbackRequestedCount,
+ mInputStreamCallbackEventTarget);
+ }
+
+ rv = NS_AsyncCopy(innerStream, pipeOut, thread,
+ NS_ASYNCCOPY_VIA_WRITESEGMENTS);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // The copy failed, revert the changes we did and restore our previous
+ // inner stream.
+ mAsyncInnerStream = nullptr;
+ mInnerStream = innerStream;
+ return rv;
+ }
+
+ cloneable = do_QueryInterface(mAsyncInnerStream);
+ }
+
+ MOZ_ASSERT(cloneable && cloneable->GetCloneable());
+
+ // Check if we can clone more efficiently with a range.
+ if (length < UINT64_MAX) {
+ if (nsCOMPtr<nsICloneableInputStreamWithRange> cloneableWithRange =
+ do_QueryInterface(cloneable)) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Cloning with range"));
+ nsCOMPtr<nsIInputStream> cloned;
+ rv = cloneableWithRange->CloneWithRange(start, length,
+ getter_AddRefs(cloned));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ stream = new RemoteLazyInputStream(cloned);
+ stream.forget(aResult);
+ return NS_OK;
+ }
+ }
+
+ // Directly clone our inner stream, and then slice it if needed.
+ nsCOMPtr<nsIInputStream> cloned;
+ rv = cloneable->Clone(getter_AddRefs(cloned));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (length < UINT64_MAX) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Slicing stream with %" PRIu64 " %" PRIu64, start, length));
+ cloned = new SlicedInputStream(cloned.forget(), start, length);
+ }
+
+ stream = new RemoteLazyInputStream(cloned);
+ stream.forget(aResult);
+ return NS_OK;
+}
+
+// nsIAsyncInputStream interface
+
+NS_IMETHODIMP
+RemoteLazyInputStream::CloseWithStatus(nsresult aStatus) { return Close(); }
+
+NS_IMETHODIMP
+RemoteLazyInputStream::AsyncWait(nsIInputStreamCallback* aCallback,
+ uint32_t aFlags, uint32_t aRequestedCount,
+ nsIEventTarget* aEventTarget) {
+ // Ensure we always have an event target for AsyncWait callbacks, so that
+ // calls to `AsyncWait` cannot reenter us with `OnInputStreamReady`.
+ nsCOMPtr<nsIEventTarget> eventTarget = aEventTarget;
+ if (aCallback && !eventTarget) {
+ eventTarget = RemoteLazyInputStreamThread::GetOrCreate();
+ if (NS_WARN_IF(!eventTarget)) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+ }
+
+ {
+ MutexAutoLock lock(mMutex);
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("AsyncWait(%p, %u, %u, %p) %s", aCallback, aFlags, aRequestedCount,
+ aEventTarget, Describe().get()));
+
+ // See RemoteLazyInputStream.h for more information about this state
+ // machine.
+
+ nsCOMPtr<nsIAsyncInputStream> stream;
+ switch (mState) {
+ // First call, we need to retrieve the stream from the parent actor.
+ case eInit:
+ MOZ_ASSERT(mActor);
+
+ mInputStreamCallback = aCallback;
+ mInputStreamCallbackEventTarget = eventTarget;
+ mInputStreamCallbackFlags = aFlags;
+ mInputStreamCallbackRequestedCount = aRequestedCount;
+ mState = ePending;
+
+ StreamNeeded();
+ return NS_OK;
+
+ // We are still waiting for the remote inputStream
+ case ePending: {
+ if (NS_WARN_IF(mInputStreamCallback && aCallback &&
+ mInputStreamCallback != aCallback)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mInputStreamCallback = aCallback;
+ mInputStreamCallbackEventTarget = eventTarget;
+ mInputStreamCallbackFlags = aFlags;
+ mInputStreamCallbackRequestedCount = aRequestedCount;
+ return NS_OK;
+ }
+
+ // We have the remote inputStream, let's check if we can execute the
+ // callback.
+ case eRunning: {
+ if (NS_WARN_IF(mInputStreamCallback && aCallback &&
+ mInputStreamCallback != aCallback)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = EnsureAsyncRemoteStream();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mInputStreamCallback = aCallback;
+ mInputStreamCallbackEventTarget = eventTarget;
+ mInputStreamCallbackFlags = aFlags;
+ mInputStreamCallbackRequestedCount = aRequestedCount;
+
+ stream = mAsyncInnerStream;
+ break;
+ }
+
+ case eClosed:
+ [[fallthrough]];
+ default:
+ MOZ_ASSERT(mState == eClosed);
+ if (NS_WARN_IF(mInputStreamCallback && aCallback &&
+ mInputStreamCallback != aCallback)) {
+ return NS_ERROR_FAILURE;
+ }
+ break;
+ }
+
+ if (stream) {
+ return stream->AsyncWait(aCallback ? this : nullptr, aFlags,
+ aRequestedCount, eventTarget);
+ }
+ }
+
+ if (aCallback) {
+ // if stream is nullptr here, that probably means the stream has
+ // been closed and the callback can be executed immediately
+ InputStreamCallbackRunnable::Execute(do_AddRef(aCallback),
+ do_AddRef(eventTarget), this);
+ }
+ return NS_OK;
+}
+
+void RemoteLazyInputStream::StreamNeeded() {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("StreamNeeded %s", Describe().get()));
+
+ auto* thread = RemoteLazyInputStreamThread::GetOrCreate();
+ if (NS_WARN_IF(!thread)) {
+ return;
+ }
+ thread->Dispatch(NS_NewRunnableFunction(
+ "RemoteLazyInputStream::StreamNeeded",
+ [self = RefPtr{this}, actor = mActor, start = mStart, length = mLength] {
+ MOZ_LOG(
+ gRemoteLazyStreamLog, LogLevel::Debug,
+ ("Sending StreamNeeded(%" PRIu64 " %" PRIu64 ") %s %d", start,
+ length, nsIDToCString(actor->StreamID()).get(), actor->CanSend()));
+
+ actor->SendStreamNeeded(
+ start, length,
+ [self](const Maybe<mozilla::ipc::IPCStream>& aStream) {
+ // Try to deserialize the stream from our remote, and close our
+ // stream if it fails.
+ nsCOMPtr<nsIInputStream> stream =
+ mozilla::ipc::DeserializeIPCStream(aStream);
+ if (NS_WARN_IF(!stream)) {
+ NS_WARNING("Failed to deserialize IPC stream");
+ self->Close();
+ }
+
+ // Lock our mutex to update the inner stream, and collect any
+ // callbacks which we need to invoke.
+ MutexAutoLock lock(self->mMutex);
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("ResolveStreamNeeded(%p) %s", stream.get(),
+ self->Describe().get()));
+
+ if (self->mState == ePending) {
+ self->mInnerStream = stream.forget();
+ self->mState = eRunning;
+
+ // Notify any listeners that we've now acquired the underlying
+ // stream, so file metadata information will be available.
+ nsCOMPtr<nsIFileMetadataCallback> fileMetadataCallback =
+ self->mFileMetadataCallback.forget();
+ nsCOMPtr<nsIEventTarget> fileMetadataCallbackEventTarget =
+ self->mFileMetadataCallbackEventTarget.forget();
+ if (fileMetadataCallback) {
+ FileMetadataCallbackRunnable::Execute(
+ fileMetadataCallback, fileMetadataCallbackEventTarget,
+ self);
+ }
+
+ // **NOTE** we can re-enter this class here **NOTE**
+ // If we already have an input stream callback, attempt to
+ // register ourselves with AsyncWait on the underlying stream.
+ if (self->mInputStreamCallback) {
+ if (NS_FAILED(self->EnsureAsyncRemoteStream()) ||
+ NS_FAILED(self->mAsyncInnerStream->AsyncWait(
+ self, self->mInputStreamCallbackFlags,
+ self->mInputStreamCallbackRequestedCount,
+ self->mInputStreamCallbackEventTarget))) {
+ InputStreamCallbackRunnable::Execute(
+ self->mInputStreamCallback.forget(),
+ self->mInputStreamCallbackEventTarget.forget(), self);
+ }
+ }
+ }
+
+ if (stream) {
+ NS_WARNING("Failed to save stream, closing it");
+ stream->Close();
+ }
+ },
+ [self](mozilla::ipc::ResponseRejectReason) {
+ NS_WARNING("SendStreamNeeded rejected");
+ self->Close();
+ });
+ }));
+}
+
+// nsIInputStreamCallback
+
+NS_IMETHODIMP
+RemoteLazyInputStream::OnInputStreamReady(nsIAsyncInputStream* aStream) {
+ RefPtr<nsIInputStreamCallback> callback;
+ nsCOMPtr<nsIEventTarget> callbackEventTarget;
+ {
+ MutexAutoLock lock(mMutex);
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("OnInputStreamReady %s", Describe().get()));
+
+ // We have been closed in the meantime.
+ if (mState == eClosed) {
+ return NS_OK;
+ }
+
+ // We got a callback from the wrong stream, likely due to a `CloneWithRange`
+ // call while we were waiting. Ignore this callback.
+ if (mAsyncInnerStream != aStream) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mState == eRunning);
+
+ // The callback has been canceled in the meantime.
+ if (!mInputStreamCallback) {
+ return NS_OK;
+ }
+
+ callback.swap(mInputStreamCallback);
+ callbackEventTarget.swap(mInputStreamCallbackEventTarget);
+ }
+
+ // This must be the last operation because the execution of the callback can
+ // be synchronous.
+ MOZ_ASSERT(callback);
+ InputStreamCallbackRunnable::Execute(callback.forget(),
+ callbackEventTarget.forget(), this);
+ return NS_OK;
+}
+
+// nsIIPCSerializableInputStream
+
+void RemoteLazyInputStream::SerializedComplexity(uint32_t aMaxSize,
+ uint32_t* aSizeUsed,
+ uint32_t* aNewPipes,
+ uint32_t* aTransferables) {
+ *aTransferables = 1;
+}
+
+void RemoteLazyInputStream::Serialize(mozilla::ipc::InputStreamParams& aParams,
+ uint32_t aMaxSize, uint32_t* aSizeUsed) {
+ *aSizeUsed = 0;
+ aParams = mozilla::ipc::RemoteLazyInputStreamParams(this);
+}
+
+bool RemoteLazyInputStream::Deserialize(
+ const mozilla::ipc::InputStreamParams& aParams) {
+ MOZ_CRASH("This should never be called.");
+ return false;
+}
+
+// nsIAsyncFileMetadata
+
+NS_IMETHODIMP
+RemoteLazyInputStream::AsyncFileMetadataWait(nsIFileMetadataCallback* aCallback,
+ nsIEventTarget* aEventTarget) {
+ MOZ_ASSERT(!!aCallback == !!aEventTarget);
+
+ // If we have the callback, we must have the event target.
+ if (NS_WARN_IF(!!aCallback != !!aEventTarget)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // See RemoteLazyInputStream.h for more information about this state
+ // machine.
+
+ {
+ MutexAutoLock lock(mMutex);
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("AsyncFileMetadataWait(%p, %p) %s", aCallback, aEventTarget,
+ Describe().get()));
+
+ switch (mState) {
+ // First call, we need to retrieve the stream from the parent actor.
+ case eInit:
+ MOZ_ASSERT(mActor);
+
+ mFileMetadataCallback = aCallback;
+ mFileMetadataCallbackEventTarget = aEventTarget;
+ mState = ePending;
+
+ StreamNeeded();
+ return NS_OK;
+
+ // We are still waiting for the remote inputStream
+ case ePending:
+ if (mFileMetadataCallback && aCallback) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mFileMetadataCallback = aCallback;
+ mFileMetadataCallbackEventTarget = aEventTarget;
+ return NS_OK;
+
+ // We have the remote inputStream, let's check if we can execute the
+ // callback.
+ case eRunning:
+ break;
+
+ // Stream is closed.
+ default:
+ MOZ_ASSERT(mState == eClosed);
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ MOZ_ASSERT(mState == eRunning);
+ }
+
+ FileMetadataCallbackRunnable::Execute(aCallback, aEventTarget, this);
+ return NS_OK;
+}
+
+// nsIFileMetadata
+
+NS_IMETHODIMP
+RemoteLazyInputStream::GetSize(int64_t* aRetval) {
+ nsCOMPtr<nsIFileMetadata> fileMetadata;
+ {
+ MutexAutoLock lock(mMutex);
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("GetSize %s", Describe().get()));
+
+ fileMetadata = do_QueryInterface(mInnerStream);
+ if (!fileMetadata) {
+ return mState == eClosed ? NS_BASE_STREAM_CLOSED : NS_ERROR_FAILURE;
+ }
+ }
+
+ return fileMetadata->GetSize(aRetval);
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStream::GetLastModified(int64_t* aRetval) {
+ nsCOMPtr<nsIFileMetadata> fileMetadata;
+ {
+ MutexAutoLock lock(mMutex);
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("GetLastModified %s", Describe().get()));
+
+ fileMetadata = do_QueryInterface(mInnerStream);
+ if (!fileMetadata) {
+ return mState == eClosed ? NS_BASE_STREAM_CLOSED : NS_ERROR_FAILURE;
+ }
+ }
+
+ return fileMetadata->GetLastModified(aRetval);
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStream::GetFileDescriptor(PRFileDesc** aRetval) {
+ nsCOMPtr<nsIFileMetadata> fileMetadata;
+ {
+ MutexAutoLock lock(mMutex);
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("GetFileDescriptor %s", Describe().get()));
+
+ fileMetadata = do_QueryInterface(mInnerStream);
+ if (!fileMetadata) {
+ return mState == eClosed ? NS_BASE_STREAM_CLOSED : NS_ERROR_FAILURE;
+ }
+ }
+
+ return fileMetadata->GetFileDescriptor(aRetval);
+}
+
+nsresult RemoteLazyInputStream::EnsureAsyncRemoteStream() {
+ // We already have an async remote stream.
+ if (mAsyncInnerStream) {
+ return NS_OK;
+ }
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("EnsureAsyncRemoteStream %s", Describe().get()));
+
+ if (NS_WARN_IF(!mInnerStream)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIInputStream> stream = mInnerStream;
+
+ // Check if the stream is blocking, if it is, we want to make it non-blocking
+ // using a pipe.
+ bool nonBlocking = false;
+ nsresult rv = stream->IsNonBlocking(&nonBlocking);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // We don't return NS_ERROR_NOT_IMPLEMENTED from ReadSegments,
+ // so it's possible that callers are expecting us to succeed in the future.
+ // We need to make sure the stream we return here supports ReadSegments,
+ // so wrap if in a buffered stream if necessary.
+ //
+ // We only need to do this if we won't be wrapping the stream in a pipe, which
+ // will add buffering anyway.
+ if (nonBlocking && !NS_InputStreamIsBuffered(stream)) {
+ nsCOMPtr<nsIInputStream> bufferedStream;
+ nsresult rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream),
+ stream.forget(), 4096);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ stream = bufferedStream;
+ }
+
+ nsCOMPtr<nsIAsyncInputStream> asyncStream = do_QueryInterface(stream);
+
+ // If non-blocking and non-async, let's use NonBlockingAsyncInputStream.
+ if (nonBlocking && !asyncStream) {
+ rv = NonBlockingAsyncInputStream::Create(stream.forget(),
+ getter_AddRefs(asyncStream));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(asyncStream);
+ }
+
+ if (!asyncStream) {
+ // Let's make the stream async using the DOMFile thread.
+ nsCOMPtr<nsIAsyncInputStream> pipeIn;
+ nsCOMPtr<nsIAsyncOutputStream> pipeOut;
+ NS_NewPipe2(getter_AddRefs(pipeIn), getter_AddRefs(pipeOut), true, true);
+
+ RefPtr<RemoteLazyInputStreamThread> thread =
+ RemoteLazyInputStreamThread::GetOrCreate();
+ if (NS_WARN_IF(!thread)) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ rv = NS_AsyncCopy(stream, pipeOut, thread, NS_ASYNCCOPY_VIA_WRITESEGMENTS);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ asyncStream = pipeIn;
+ }
+
+ MOZ_ASSERT(asyncStream);
+ mAsyncInnerStream = asyncStream;
+ mInnerStream = nullptr;
+
+ return NS_OK;
+}
+
+// nsIInputStreamLength
+
+NS_IMETHODIMP
+RemoteLazyInputStream::Length(int64_t* aLength) {
+ MutexAutoLock lock(mMutex);
+
+ if (mState == eClosed) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ if (!mActor) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return NS_BASE_STREAM_WOULD_BLOCK;
+}
+
+namespace {
+
+class InputStreamLengthCallbackRunnable final : public DiscardableRunnable {
+ public:
+ static void Execute(nsIInputStreamLengthCallback* aCallback,
+ nsIEventTarget* aEventTarget,
+ RemoteLazyInputStream* aStream, int64_t aLength) {
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(aEventTarget);
+
+ RefPtr<InputStreamLengthCallbackRunnable> runnable =
+ new InputStreamLengthCallbackRunnable(aCallback, aStream, aLength);
+
+ nsCOMPtr<nsIEventTarget> target = aEventTarget;
+ target->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ }
+
+ NS_IMETHOD
+ Run() override {
+ mCallback->OnInputStreamLengthReady(mStream, mLength);
+ mCallback = nullptr;
+ mStream = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ InputStreamLengthCallbackRunnable(nsIInputStreamLengthCallback* aCallback,
+ RemoteLazyInputStream* aStream,
+ int64_t aLength)
+ : DiscardableRunnable("dom::InputStreamLengthCallbackRunnable"),
+ mCallback(aCallback),
+ mStream(aStream),
+ mLength(aLength) {
+ MOZ_ASSERT(mCallback);
+ MOZ_ASSERT(mStream);
+ }
+
+ nsCOMPtr<nsIInputStreamLengthCallback> mCallback;
+ RefPtr<RemoteLazyInputStream> mStream;
+ const int64_t mLength;
+};
+
+} // namespace
+
+// nsIAsyncInputStreamLength
+
+NS_IMETHODIMP
+RemoteLazyInputStream::AsyncLengthWait(nsIInputStreamLengthCallback* aCallback,
+ nsIEventTarget* aEventTarget) {
+ // If we have the callback, we must have the event target.
+ if (NS_WARN_IF(!!aCallback != !!aEventTarget)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ {
+ MutexAutoLock lock(mMutex);
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("AsyncLengthWait(%p, %p) %s", aCallback, aEventTarget,
+ Describe().get()));
+
+ if (mActor) {
+ if (aCallback) {
+ auto* thread = RemoteLazyInputStreamThread::GetOrCreate();
+ if (NS_WARN_IF(!thread)) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+ thread->Dispatch(NS_NewRunnableFunction(
+ "RemoteLazyInputStream::AsyncLengthWait",
+ [self = RefPtr{this}, actor = mActor,
+ callback = nsCOMPtr{aCallback},
+ eventTarget = nsCOMPtr{aEventTarget}] {
+ actor->SendLengthNeeded(
+ [self, callback, eventTarget](int64_t aLength) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("AsyncLengthWait resolve %" PRId64, aLength));
+ int64_t length = -1;
+ if (aLength > 0) {
+ uint64_t sourceLength =
+ aLength - std::min<uint64_t>(aLength, self->mStart);
+ length = int64_t(
+ std::min<uint64_t>(sourceLength, self->mLength));
+ }
+ InputStreamLengthCallbackRunnable::Execute(
+ callback, eventTarget, self, length);
+ },
+ [self, callback,
+ eventTarget](mozilla::ipc::ResponseRejectReason) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning,
+ ("AsyncLengthWait reject"));
+ InputStreamLengthCallbackRunnable::Execute(
+ callback, eventTarget, self, -1);
+ });
+ }));
+ }
+
+ return NS_OK;
+ }
+ }
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("AsyncLengthWait immediate"));
+
+ // If execution has reached here, it means the stream is either closed or
+ // consumed, and therefore the callback can be executed immediately
+ InputStreamLengthCallbackRunnable::Execute(aCallback, aEventTarget, this, -1);
+ return NS_OK;
+}
+
+void RemoteLazyInputStream::IPCWrite(IPC::MessageWriter* aWriter) {
+ // If we have an actor still, serialize efficiently by cloning our actor to
+ // maintain a reference to the parent side.
+ RefPtr<RemoteLazyInputStreamChild> actor;
+
+ nsCOMPtr<nsIInputStream> innerStream;
+
+ RefPtr<nsIInputStreamCallback> inputStreamCallback;
+ nsCOMPtr<nsIEventTarget> inputStreamCallbackEventTarget;
+
+ {
+ MutexAutoLock lock(mMutex);
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Serialize %s", Describe().get()));
+
+ actor = mActor.forget();
+
+ if (mAsyncInnerStream) {
+ MOZ_ASSERT(!mInnerStream);
+ innerStream = mAsyncInnerStream.forget();
+ } else {
+ innerStream = mInnerStream.forget();
+ }
+
+ // TODO(Bug 1737783): Notify to the mFileMetadataCallback that this
+ // lazy input stream has been closed.
+ mFileMetadataCallback = nullptr;
+ mFileMetadataCallbackEventTarget = nullptr;
+
+ inputStreamCallback = mInputStreamCallback.forget();
+ inputStreamCallbackEventTarget = mInputStreamCallbackEventTarget.forget();
+
+ mState = eClosed;
+ }
+
+ if (inputStreamCallback) {
+ InputStreamCallbackRunnable::Execute(
+ inputStreamCallback.forget(), inputStreamCallbackEventTarget.forget(),
+ this);
+ }
+
+ bool closed = !actor && !innerStream;
+ IPC::WriteParam(aWriter, closed);
+ if (closed) {
+ return;
+ }
+
+ // If we still have a connection to our remote actor, create a clone endpoint
+ // for it and tell it that the stream has been consumed. The clone of the
+ // connection can be transferred to another process.
+ if (actor) {
+ MOZ_LOG(
+ gRemoteLazyStreamLog, LogLevel::Debug,
+ ("Serializing as actor: %s", nsIDToCString(actor->StreamID()).get()));
+ // Create a clone of the actor, and then tell it that this stream is no
+ // longer referencing it.
+ mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent> parentEp;
+ mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> childEp;
+ MOZ_ALWAYS_SUCCEEDS(
+ PRemoteLazyInputStream::CreateEndpoints(&parentEp, &childEp));
+
+ auto* thread = RemoteLazyInputStreamThread::GetOrCreate();
+ if (thread) {
+ thread->Dispatch(NS_NewRunnableFunction(
+ "RemoteLazyInputStreamChild::SendClone",
+ [actor, parentEp = std::move(parentEp)]() mutable {
+ bool ok = actor->SendClone(std::move(parentEp));
+ MOZ_LOG(
+ gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("SendClone for %s: %s", nsIDToCString(actor->StreamID()).get(),
+ ok ? "OK" : "ERR"));
+ }));
+
+ } // else we are shutting down xpcom threads.
+
+ // NOTE: Call `StreamConsumed` after dispatching the `SendClone` runnable,
+ // as this method may dispatch a runnable to `RemoteLazyInputStreamThread`
+ // to call `SendGoodbye`, which needs to happen after `SendClone`.
+ actor->StreamConsumed();
+
+ IPC::WriteParam(aWriter, actor->StreamID());
+ IPC::WriteParam(aWriter, mStart);
+ IPC::WriteParam(aWriter, mLength);
+ IPC::WriteParam(aWriter, std::move(childEp));
+
+ if (innerStream) {
+ innerStream->Close();
+ }
+ return;
+ }
+
+ // If we have a stream and are in the parent process, create a new actor pair
+ // and transfer ownership of the stream into storage.
+ auto streamStorage = RemoteLazyInputStreamStorage::Get();
+ if (streamStorage.isOk()) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ nsID id = nsID::GenerateUUID();
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("Serializing as new stream: %s", nsIDToCString(id).get()));
+
+ streamStorage.inspect()->AddStream(innerStream, id);
+
+ mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent> parentEp;
+ mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> childEp;
+ MOZ_ALWAYS_SUCCEEDS(
+ PRemoteLazyInputStream::CreateEndpoints(&parentEp, &childEp));
+
+ // Bind the actor on our background thread.
+ streamStorage.inspect()->TaskQueue()->Dispatch(NS_NewRunnableFunction(
+ "RemoteLazyInputStreamParent::Bind",
+ [parentEp = std::move(parentEp), id]() mutable {
+ auto stream = MakeRefPtr<RemoteLazyInputStreamParent>(id);
+ parentEp.Bind(stream);
+ }));
+
+ IPC::WriteParam(aWriter, id);
+ IPC::WriteParam(aWriter, 0);
+ IPC::WriteParam(aWriter, UINT64_MAX);
+ IPC::WriteParam(aWriter, std::move(childEp));
+ return;
+ }
+
+ MOZ_CRASH("Cannot serialize new RemoteLazyInputStream from this process");
+}
+
+already_AddRefed<RemoteLazyInputStream> RemoteLazyInputStream::IPCRead(
+ IPC::MessageReader* aReader) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Deserialize"));
+
+ bool closed;
+ if (NS_WARN_IF(!IPC::ReadParam(aReader, &closed))) {
+ return nullptr;
+ }
+ if (closed) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Deserialize closed stream"));
+ return do_AddRef(new RemoteLazyInputStream());
+ }
+
+ nsID id{};
+ uint64_t start;
+ uint64_t length;
+ mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> endpoint;
+ if (NS_WARN_IF(!IPC::ReadParam(aReader, &id)) ||
+ NS_WARN_IF(!IPC::ReadParam(aReader, &start)) ||
+ NS_WARN_IF(!IPC::ReadParam(aReader, &length)) ||
+ NS_WARN_IF(!IPC::ReadParam(aReader, &endpoint))) {
+ return nullptr;
+ }
+
+ if (!endpoint.IsValid()) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning,
+ ("Deserialize failed due to invalid endpoint!"));
+ return do_AddRef(new RemoteLazyInputStream());
+ }
+
+ RefPtr<RemoteLazyInputStreamChild> actor =
+ BindChildActor(id, std::move(endpoint));
+
+ if (!actor) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning,
+ ("Deserialize failed as we are probably late in shutdown!"));
+ return do_AddRef(new RemoteLazyInputStream());
+ }
+
+ return do_AddRef(new RemoteLazyInputStream(actor, start, length));
+}
+
+} // namespace mozilla
+
+void IPC::ParamTraits<mozilla::RemoteLazyInputStream*>::Write(
+ IPC::MessageWriter* aWriter, mozilla::RemoteLazyInputStream* aParam) {
+ bool nonNull = !!aParam;
+ IPC::WriteParam(aWriter, nonNull);
+ if (aParam) {
+ aParam->IPCWrite(aWriter);
+ }
+}
+
+bool IPC::ParamTraits<mozilla::RemoteLazyInputStream*>::Read(
+ IPC::MessageReader* aReader,
+ RefPtr<mozilla::RemoteLazyInputStream>* aResult) {
+ bool nonNull = false;
+ if (!IPC::ReadParam(aReader, &nonNull)) {
+ return false;
+ }
+ if (!nonNull) {
+ *aResult = nullptr;
+ return true;
+ }
+ *aResult = mozilla::RemoteLazyInputStream::IPCRead(aReader);
+ return *aResult;
+}
diff --git a/dom/file/ipc/RemoteLazyInputStream.h b/dom/file/ipc/RemoteLazyInputStream.h
new file mode 100644
index 0000000000..08bb168e27
--- /dev/null
+++ b/dom/file/ipc/RemoteLazyInputStream.h
@@ -0,0 +1,155 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_RemoteLazyInputStream_h
+#define mozilla_RemoteLazyInputStream_h
+
+#include "chrome/common/ipc_message_utils.h"
+#include "mozilla/Mutex.h"
+#include "mozIRemoteLazyInputStream.h"
+#include "nsIAsyncInputStream.h"
+#include "nsICloneableInputStream.h"
+#include "nsIFileStreams.h"
+#include "nsIIPCSerializableInputStream.h"
+#include "nsIInputStreamLength.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla {
+
+class RemoteLazyInputStreamChild;
+
+class RemoteLazyInputStream final : public nsIAsyncInputStream,
+ public nsIInputStreamCallback,
+ public nsICloneableInputStreamWithRange,
+ public nsIIPCSerializableInputStream,
+ public nsIAsyncFileMetadata,
+ public nsIInputStreamLength,
+ public nsIAsyncInputStreamLength,
+ public mozIRemoteLazyInputStream {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIINPUTSTREAM
+ NS_DECL_NSIASYNCINPUTSTREAM
+ NS_DECL_NSIINPUTSTREAMCALLBACK
+ NS_DECL_NSICLONEABLEINPUTSTREAM
+ NS_DECL_NSICLONEABLEINPUTSTREAMWITHRANGE
+ NS_DECL_NSIIPCSERIALIZABLEINPUTSTREAM
+ NS_DECL_NSIFILEMETADATA
+ NS_DECL_NSIASYNCFILEMETADATA
+ NS_DECL_NSIINPUTSTREAMLENGTH
+ NS_DECL_NSIASYNCINPUTSTREAMLENGTH
+
+ // Create a new lazy RemoteLazyInputStream, and move the provided aInputStream
+ // into storage as referenced by it. May only be called in processes with
+ // RemoteLazyInputStreamStorage.
+ static already_AddRefed<RemoteLazyInputStream> WrapStream(
+ nsIInputStream* aInputStream);
+
+ // mozIRemoteLazyInputStream
+ NS_IMETHOD TakeInternalStream(nsIInputStream** aStream) override;
+ NS_IMETHOD GetInternalStreamID(nsID& aID) override;
+
+ private:
+ friend struct IPC::ParamTraits<mozilla::RemoteLazyInputStream*>;
+
+ // Constructor for an already-closed RemoteLazyInputStream.
+ RemoteLazyInputStream() = default;
+
+ explicit RemoteLazyInputStream(RemoteLazyInputStreamChild* aActor,
+ uint64_t aStart = 0,
+ uint64_t aLength = UINT64_MAX);
+
+ explicit RemoteLazyInputStream(nsIInputStream* aStream);
+
+ ~RemoteLazyInputStream();
+
+ void StreamNeeded() MOZ_REQUIRES(mMutex);
+
+ // Upon receiving the stream from our actor, we will not wrap it into an async
+ // stream until needed. This allows callers to get access to the underlying
+ // potentially-sync stream using `TakeInternalStream` before reading.
+ nsresult EnsureAsyncRemoteStream() MOZ_REQUIRES(mMutex);
+
+ // Note that data has been read from our input stream, and disconnect from our
+ // remote actor.
+ void MarkConsumed();
+
+ void IPCWrite(IPC::MessageWriter* aWriter);
+ static already_AddRefed<RemoteLazyInputStream> IPCRead(
+ IPC::MessageReader* aReader);
+
+ // Helper method to generate a description of a stream for use in loggging.
+ nsCString Describe() MOZ_REQUIRES(mMutex);
+
+ // Start and length of the slice to apply on this RemoteLazyInputStream when
+ // fetching the underlying stream with `SendStreamNeeded`.
+ const uint64_t mStart = 0;
+ const uint64_t mLength = UINT64_MAX;
+
+ // Any non-const member of this class is protected by mutex because it is
+ // touched on multiple threads.
+ Mutex mMutex{"RemoteLazyInputStream::mMutex"};
+
+ // This is the list of possible states.
+ enum {
+ // The initial state. Only ::Available() can be used without receiving an
+ // error. The available size is known by the actor.
+ eInit,
+
+ // AsyncWait() has been called for the first time. SendStreamNeeded() has
+ // been called and we are waiting for the 'real' inputStream.
+ ePending,
+
+ // When the child receives the stream from the parent, we move to this
+ // state. The received stream is stored in mInnerStream. From now on, any
+ // method call will be forwared to mInnerStream or mAsyncInnerStream.
+ eRunning,
+
+ // If Close() or CloseWithStatus() is called, we move to this state.
+ // mInnerStream is released and any method will return
+ // NS_BASE_STREAM_CLOSED.
+ eClosed,
+ } mState MOZ_GUARDED_BY(mMutex) = eClosed;
+
+ // The actor which will be used to provide the underlying stream or length
+ // information when needed, as well as to efficiently allow transferring the
+ // stream over IPC.
+ //
+ // The connection to our actor will be cleared once the stream has been closed
+ // or has started reading, at which point this stream will be serialized and
+ // cloned as-if it was the underlying stream.
+ RefPtr<RemoteLazyInputStreamChild> mActor MOZ_GUARDED_BY(mMutex);
+
+ nsCOMPtr<nsIInputStream> mInnerStream MOZ_GUARDED_BY(mMutex);
+ nsCOMPtr<nsIAsyncInputStream> mAsyncInnerStream MOZ_GUARDED_BY(mMutex);
+
+ // These 2 values are set only if mState is ePending or eRunning.
+ // RefPtr is used instead of nsCOMPtr to avoid invoking QueryInterface when
+ // assigning in debug builds, as `mInputStreamCallback` may not be threadsafe.
+ RefPtr<nsIInputStreamCallback> mInputStreamCallback MOZ_GUARDED_BY(mMutex);
+ nsCOMPtr<nsIEventTarget> mInputStreamCallbackEventTarget
+ MOZ_GUARDED_BY(mMutex);
+ uint32_t mInputStreamCallbackFlags MOZ_GUARDED_BY(mMutex) = 0;
+ uint32_t mInputStreamCallbackRequestedCount MOZ_GUARDED_BY(mMutex) = 0;
+
+ // These 2 values are set only if mState is ePending.
+ nsCOMPtr<nsIFileMetadataCallback> mFileMetadataCallback
+ MOZ_GUARDED_BY(mMutex);
+ nsCOMPtr<nsIEventTarget> mFileMetadataCallbackEventTarget
+ MOZ_GUARDED_BY(mMutex);
+};
+
+} // namespace mozilla
+
+template <>
+struct IPC::ParamTraits<mozilla::RemoteLazyInputStream*> {
+ static void Write(IPC::MessageWriter* aWriter,
+ mozilla::RemoteLazyInputStream* aParam);
+ static bool Read(IPC::MessageReader* aReader,
+ RefPtr<mozilla::RemoteLazyInputStream>* aResult);
+};
+
+#endif // mozilla_RemoteLazyInputStream_h
diff --git a/dom/file/ipc/RemoteLazyInputStreamChild.cpp b/dom/file/ipc/RemoteLazyInputStreamChild.cpp
new file mode 100644
index 0000000000..f03aa47aad
--- /dev/null
+++ b/dom/file/ipc/RemoteLazyInputStreamChild.cpp
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RemoteLazyInputStreamChild.h"
+#include "RemoteLazyInputStreamThread.h"
+
+namespace mozilla {
+
+extern mozilla::LazyLogModule gRemoteLazyStreamLog;
+
+RemoteLazyInputStreamChild::RemoteLazyInputStreamChild(const nsID& aID)
+ : mID(aID) {}
+
+RemoteLazyInputStreamChild::~RemoteLazyInputStreamChild() = default;
+
+void RemoteLazyInputStreamChild::StreamCreated() {
+ size_t count = ++mStreamCount;
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Child::StreamCreated %s = %zu", nsIDToCString(mID).get(), count));
+}
+
+void RemoteLazyInputStreamChild::StreamConsumed() {
+ size_t count = --mStreamCount;
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Child::StreamConsumed %s = %zu", nsIDToCString(mID).get(), count));
+
+ // When the count reaches zero, close the underlying actor.
+ if (count == 0) {
+ auto* t = RemoteLazyInputStreamThread::Get();
+ if (t) {
+ t->Dispatch(
+ NS_NewRunnableFunction("RemoteLazyInputStreamChild::StreamConsumed",
+ [self = RefPtr{this}]() {
+ if (self->CanSend()) {
+ self->SendGoodbye();
+ }
+ }));
+ } // else the xpcom thread shutdown has already started.
+ }
+}
+
+void RemoteLazyInputStreamChild::ActorDestroy(ActorDestroyReason aReason) {
+ if (mStreamCount != 0) {
+ NS_WARNING(
+ nsPrintfCString("RemoteLazyInputStreamChild disconnected unexpectedly "
+ "(%zu streams remaining)! %p %s",
+ size_t(mStreamCount), this, nsIDToCString(mID).get())
+ .get());
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/file/ipc/RemoteLazyInputStreamChild.h b/dom/file/ipc/RemoteLazyInputStreamChild.h
new file mode 100644
index 0000000000..c95ed9f058
--- /dev/null
+++ b/dom/file/ipc/RemoteLazyInputStreamChild.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 mozilla_RemoteLazyInputStreamChild_h
+#define mozilla_RemoteLazyInputStreamChild_h
+
+#include "mozilla/PRemoteLazyInputStreamChild.h"
+
+namespace mozilla {
+
+class RemoteLazyInputStream;
+
+class RemoteLazyInputStreamChild final : public PRemoteLazyInputStreamChild {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteLazyInputStreamChild, final)
+
+ explicit RemoteLazyInputStreamChild(const nsID& aID);
+
+ const nsID& StreamID() const { return mID; }
+
+ // Manage the count of streams registered on this actor. When the count
+ // reaches 0 the connection to our remote process will be closed.
+ void StreamCreated();
+ void StreamConsumed();
+
+ void ActorDestroy(ActorDestroyReason aReason) override;
+
+ private:
+ ~RemoteLazyInputStreamChild() override;
+
+ const nsID mID;
+
+ std::atomic<size_t> mStreamCount{0};
+};
+
+} // namespace mozilla
+
+#endif // mozilla_RemoteLazyInputStreamChild_h
diff --git a/dom/file/ipc/RemoteLazyInputStreamParent.cpp b/dom/file/ipc/RemoteLazyInputStreamParent.cpp
new file mode 100644
index 0000000000..d92c529546
--- /dev/null
+++ b/dom/file/ipc/RemoteLazyInputStreamParent.cpp
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "RemoteLazyInputStreamParent.h"
+#include "RemoteLazyInputStreamStorage.h"
+#include "mozilla/InputStreamLengthHelper.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/ipc/IPCStreamUtils.h"
+#include "mozilla/ipc/InputStreamParams.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "nsStreamUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsNetCID.h"
+
+namespace mozilla {
+
+extern mozilla::LazyLogModule gRemoteLazyStreamLog;
+
+RemoteLazyInputStreamParent::RemoteLazyInputStreamParent(const nsID& aID)
+ : mID(aID) {
+ auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr);
+ if (storage) {
+ storage->ActorCreated(mID);
+ }
+}
+
+void RemoteLazyInputStreamParent::ActorDestroy(
+ IProtocol::ActorDestroyReason aReason) {
+ auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr);
+ if (storage) {
+ storage->ActorDestroyed(mID);
+ }
+}
+
+mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvClone(
+ mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent>&& aCloneEndpoint) {
+ if (!aCloneEndpoint.IsValid()) {
+ return IPC_FAIL(this, "Unexpected invalid endpoint in RecvClone");
+ }
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug,
+ ("Parent::RecvClone %s", nsIDToCString(mID).get()));
+
+ auto* newActor = new RemoteLazyInputStreamParent(mID);
+ aCloneEndpoint.Bind(newActor);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvStreamNeeded(
+ uint64_t aStart, uint64_t aLength, StreamNeededResolver&& aResolver) {
+ nsCOMPtr<nsIInputStream> stream;
+ auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr);
+ if (storage) {
+ storage->GetStream(mID, aStart, aLength, getter_AddRefs(stream));
+ }
+
+ if (!stream) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning,
+ ("Parent::RecvStreamNeeded not available! %s",
+ nsIDToCString(mID).get()));
+ aResolver(Nothing());
+ return IPC_OK();
+ }
+
+ Maybe<IPCStream> ipcStream;
+ if (NS_WARN_IF(!SerializeIPCStream(stream.forget(), ipcStream,
+ /* aAllowLazy */ false))) {
+ return IPC_FAIL(this, "IPCStream serialization failed!");
+ }
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Parent::RecvStreamNeeded resolve %s", nsIDToCString(mID).get()));
+ aResolver(ipcStream);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvLengthNeeded(
+ LengthNeededResolver&& aResolver) {
+ nsCOMPtr<nsIInputStream> stream;
+ auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr);
+ if (storage) {
+ storage->GetStream(mID, 0, UINT64_MAX, getter_AddRefs(stream));
+ }
+
+ if (!stream) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning,
+ ("Parent::RecvLengthNeeded not available! %s",
+ nsIDToCString(mID).get()));
+ aResolver(-1);
+ return IPC_OK();
+ }
+
+ int64_t length = -1;
+ if (InputStreamLengthHelper::GetSyncLength(stream, &length)) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Parent::RecvLengthNeeded sync resolve %" PRId64 "! %s", length,
+ nsIDToCString(mID).get()));
+ aResolver(length);
+ return IPC_OK();
+ }
+
+ InputStreamLengthHelper::GetAsyncLength(
+ stream, [aResolver, id = mID](int64_t aLength) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Parent::RecvLengthNeeded async resolve %" PRId64 "! %s",
+ aLength, nsIDToCString(id).get()));
+ aResolver(aLength);
+ });
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvGoodbye() {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Parent::RecvGoodbye! %s", nsIDToCString(mID).get()));
+ Close();
+ return IPC_OK();
+}
+
+} // namespace mozilla
diff --git a/dom/file/ipc/RemoteLazyInputStreamParent.h b/dom/file/ipc/RemoteLazyInputStreamParent.h
new file mode 100644
index 0000000000..cb5b1d4285
--- /dev/null
+++ b/dom/file/ipc/RemoteLazyInputStreamParent.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 mozilla_RemoteLazyInputStreamParent_h
+#define mozilla_RemoteLazyInputStreamParent_h
+
+#include "mozilla/PRemoteLazyInputStreamParent.h"
+
+class nsIInputStream;
+
+namespace mozilla {
+
+class RemoteLazyInputStreamParent final : public PRemoteLazyInputStreamParent {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteLazyInputStreamParent, final)
+
+ explicit RemoteLazyInputStreamParent(const nsID& aID);
+
+ const nsID& ID() const { return mID; }
+
+ mozilla::ipc::IPCResult RecvClone(
+ mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent>&& aCloneEndpoint);
+
+ mozilla::ipc::IPCResult RecvStreamNeeded(uint64_t aStart, uint64_t aLength,
+ StreamNeededResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvLengthNeeded(LengthNeededResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvGoodbye();
+
+ void ActorDestroy(IProtocol::ActorDestroyReason aReason) override;
+
+ private:
+ ~RemoteLazyInputStreamParent() override = default;
+
+ const nsID mID;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_RemoteLazyInputStreamParent_h
diff --git a/dom/file/ipc/RemoteLazyInputStreamStorage.cpp b/dom/file/ipc/RemoteLazyInputStreamStorage.cpp
new file mode 100644
index 0000000000..8ce2c22657
--- /dev/null
+++ b/dom/file/ipc/RemoteLazyInputStreamStorage.cpp
@@ -0,0 +1,243 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/SlicedInputStream.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "nsIPropertyBag2.h"
+#include "nsStreamUtils.h"
+#include "RemoteLazyInputStreamParent.h"
+#include "RemoteLazyInputStreamStorage.h"
+
+namespace mozilla {
+
+using namespace hal;
+
+extern mozilla::LazyLogModule gRemoteLazyStreamLog;
+
+namespace {
+StaticMutex gMutex;
+StaticRefPtr<RemoteLazyInputStreamStorage> gStorage;
+} // namespace
+
+NS_INTERFACE_MAP_BEGIN(RemoteLazyInputStreamStorage)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(RemoteLazyInputStreamStorage)
+NS_IMPL_RELEASE(RemoteLazyInputStreamStorage)
+
+/* static */
+Result<RefPtr<RemoteLazyInputStreamStorage>, nsresult>
+RemoteLazyInputStreamStorage::Get() {
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ if (gStorage) {
+ RefPtr<RemoteLazyInputStreamStorage> storage = gStorage;
+ return storage;
+ }
+
+ return Err(NS_ERROR_NOT_INITIALIZED);
+}
+
+/* static */
+void RemoteLazyInputStreamStorage::Initialize() {
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ MOZ_ASSERT(!gStorage);
+
+ gStorage = new RemoteLazyInputStreamStorage();
+
+ MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue(
+ "RemoteLazyInputStreamStorage", getter_AddRefs(gStorage->mTaskQueue)));
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(gStorage, "xpcom-shutdown", false);
+ }
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStreamStorage::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(!strcmp(aTopic, "xpcom-shutdown"));
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, "xpcom-shutdown");
+ }
+
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ gStorage = nullptr;
+ return NS_OK;
+}
+
+void RemoteLazyInputStreamStorage::AddStream(nsIInputStream* aInputStream,
+ const nsID& aID) {
+ MOZ_ASSERT(aInputStream);
+
+ MOZ_LOG(
+ gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Storage::AddStream(%s) = %p", nsIDToCString(aID).get(), aInputStream));
+
+ UniquePtr<StreamData> data = MakeUnique<StreamData>();
+ data->mInputStream = aInputStream;
+
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ mStorage.InsertOrUpdate(aID, std::move(data));
+}
+
+nsCOMPtr<nsIInputStream> RemoteLazyInputStreamStorage::ForgetStream(
+ const nsID& aID) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Storage::ForgetStream(%s)", nsIDToCString(aID).get()));
+
+ UniquePtr<StreamData> entry;
+
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ mStorage.Remove(aID, &entry);
+
+ if (!entry) {
+ return nullptr;
+ }
+
+ return std::move(entry->mInputStream);
+}
+
+bool RemoteLazyInputStreamStorage::HasStream(const nsID& aID) {
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ StreamData* data = mStorage.Get(aID);
+ return !!data;
+}
+
+void RemoteLazyInputStreamStorage::GetStream(const nsID& aID, uint64_t aStart,
+ uint64_t aLength,
+ nsIInputStream** aInputStream) {
+ *aInputStream = nullptr;
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Storage::GetStream(%s, %" PRIu64 " %" PRIu64 ")",
+ nsIDToCString(aID).get(), aStart, aLength));
+
+ nsCOMPtr<nsIInputStream> inputStream;
+
+ // NS_CloneInputStream cannot be called when the mutex is locked because it
+ // can, recursively call GetStream() in case the child actor lives on the
+ // parent process.
+ {
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ StreamData* data = mStorage.Get(aID);
+ if (!data) {
+ return;
+ }
+
+ inputStream = data->mInputStream;
+ }
+
+ MOZ_ASSERT(inputStream);
+
+ // We cannot return always the same inputStream because not all of them are
+ // able to be reused. Better to clone them.
+
+ nsCOMPtr<nsIInputStream> clonedStream;
+ nsCOMPtr<nsIInputStream> replacementStream;
+
+ nsresult rv = NS_CloneInputStream(inputStream, getter_AddRefs(clonedStream),
+ getter_AddRefs(replacementStream));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ if (replacementStream) {
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ StreamData* data = mStorage.Get(aID);
+ // data can be gone in the meantime.
+ if (!data) {
+ return;
+ }
+
+ data->mInputStream = replacementStream;
+ }
+
+ // Now it's the right time to apply a slice if needed.
+ if (aStart > 0 || aLength < UINT64_MAX) {
+ clonedStream =
+ new SlicedInputStream(clonedStream.forget(), aStart, aLength);
+ }
+
+ clonedStream.forget(aInputStream);
+}
+
+void RemoteLazyInputStreamStorage::StoreCallback(
+ const nsID& aID, RemoteLazyInputStreamParentCallback* aCallback) {
+ MOZ_ASSERT(aCallback);
+
+ MOZ_LOG(
+ gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Storage::StoreCallback(%s, %p)", nsIDToCString(aID).get(), aCallback));
+
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ StreamData* data = mStorage.Get(aID);
+ if (data) {
+ MOZ_ASSERT(!data->mCallback);
+ data->mCallback = aCallback;
+ }
+}
+
+already_AddRefed<RemoteLazyInputStreamParentCallback>
+RemoteLazyInputStreamStorage::TakeCallback(const nsID& aID) {
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Storage::TakeCallback(%s)", nsIDToCString(aID).get()));
+
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ StreamData* data = mStorage.Get(aID);
+ if (!data) {
+ return nullptr;
+ }
+
+ RefPtr<RemoteLazyInputStreamParentCallback> callback;
+ data->mCallback.swap(callback);
+ return callback.forget();
+}
+
+void RemoteLazyInputStreamStorage::ActorCreated(const nsID& aID) {
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ StreamData* data = mStorage.Get(aID);
+ if (!data) {
+ return;
+ }
+
+ size_t count = ++data->mActorCount;
+
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Storage::ActorCreated(%s) = %zu", nsIDToCString(aID).get(), count));
+}
+
+void RemoteLazyInputStreamStorage::ActorDestroyed(const nsID& aID) {
+ UniquePtr<StreamData> entry;
+ {
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ StreamData* data = mStorage.Get(aID);
+ if (!data) {
+ return;
+ }
+
+ auto newCount = --data->mActorCount;
+ MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose,
+ ("Storage::ActorDestroyed(%s) = %zu (cb=%p)",
+ nsIDToCString(aID).get(), newCount, data->mCallback.get()));
+
+ if (newCount == 0) {
+ mStorage.Remove(aID, &entry);
+ }
+ }
+
+ if (entry && entry->mCallback) {
+ entry->mCallback->ActorDestroyed(aID);
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/file/ipc/RemoteLazyInputStreamStorage.h b/dom/file/ipc/RemoteLazyInputStreamStorage.h
new file mode 100644
index 0000000000..296a8d9313
--- /dev/null
+++ b/dom/file/ipc/RemoteLazyInputStreamStorage.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_RemoteLazyInputStreamStorage_h
+#define mozilla_RemoteLazyInputStreamStorage_h
+
+#include "mozilla/RefPtr.h"
+#include "nsClassHashtable.h"
+#include "nsIObserver.h"
+
+class nsIInputStream;
+struct nsID;
+
+namespace mozilla {
+
+class NS_NO_VTABLE RemoteLazyInputStreamParentCallback {
+ public:
+ virtual void ActorDestroyed(const nsID& aID) = 0;
+
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ protected:
+ virtual ~RemoteLazyInputStreamParentCallback() = default;
+};
+
+class RemoteLazyInputStreamStorage final : public nsIObserver {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ // This initializes the singleton and it must be called on the main-thread.
+ static void Initialize();
+
+ static Result<RefPtr<RemoteLazyInputStreamStorage>, nsresult> Get();
+
+ nsISerialEventTarget* TaskQueue() { return mTaskQueue; }
+
+ void AddStream(nsIInputStream* aInputStream, const nsID& aID);
+
+ // Removes and returns the stream corresponding to the nsID. May return a
+ // nullptr if there's no stream stored for the nsID.
+ nsCOMPtr<nsIInputStream> ForgetStream(const nsID& aID);
+
+ bool HasStream(const nsID& aID);
+
+ void GetStream(const nsID& aID, uint64_t aStart, uint64_t aLength,
+ nsIInputStream** aInputStream);
+
+ void StoreCallback(const nsID& aID,
+ RemoteLazyInputStreamParentCallback* aCallback);
+
+ already_AddRefed<RemoteLazyInputStreamParentCallback> TakeCallback(
+ const nsID& aID);
+
+ void ActorCreated(const nsID& aID);
+ void ActorDestroyed(const nsID& aID);
+
+ private:
+ RemoteLazyInputStreamStorage() = default;
+ ~RemoteLazyInputStreamStorage() = default;
+
+ nsCOMPtr<nsISerialEventTarget> mTaskQueue;
+
+ struct StreamData {
+ nsCOMPtr<nsIInputStream> mInputStream;
+ RefPtr<RemoteLazyInputStreamParentCallback> mCallback;
+ size_t mActorCount = 0;
+ };
+
+ nsClassHashtable<nsIDHashKey, StreamData> mStorage;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_RemoteLazyInputStreamStorage_h
diff --git a/dom/file/ipc/RemoteLazyInputStreamThread.cpp b/dom/file/ipc/RemoteLazyInputStreamThread.cpp
new file mode 100644
index 0000000000..ec6c99952b
--- /dev/null
+++ b/dom/file/ipc/RemoteLazyInputStreamThread.cpp
@@ -0,0 +1,237 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "RemoteLazyInputStreamThread.h"
+
+#include "ErrorList.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TaskCategory.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "nsXPCOMPrivate.h"
+
+using namespace mozilla::ipc;
+
+namespace mozilla {
+
+namespace {
+
+StaticMutex gRemoteLazyThreadMutex;
+StaticRefPtr<RemoteLazyInputStreamThread> gRemoteLazyThread;
+
+class ThreadInitializeRunnable final : public Runnable {
+ public:
+ ThreadInitializeRunnable() : Runnable("dom::ThreadInitializeRunnable") {}
+
+ NS_IMETHOD
+ Run() override {
+ StaticMutexAutoLock lock(gRemoteLazyThreadMutex);
+ MOZ_ASSERT(gRemoteLazyThread);
+ if (NS_WARN_IF(!gRemoteLazyThread->InitializeOnMainThread())) {
+ // RemoteLazyInputStreamThread::GetOrCreate might have handed out a
+ // pointer to our thread already at this point such that we cannot
+ // just do gRemoteLazyThread = nullptr; here.
+ MOZ_DIAGNOSTIC_ASSERT(
+ false, "Async gRemoteLazyThread->InitializeOnMainThread() failed.");
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+ }
+};
+
+} // namespace
+
+NS_IMPL_ISUPPORTS(RemoteLazyInputStreamThread, nsIObserver, nsIEventTarget,
+ nsISerialEventTarget, nsIDirectTaskDispatcher)
+
+bool RLISThreadIsInOrBeyondShutdown() {
+ // ShutdownPhase::XPCOMShutdownThreads matches
+ // obs->AddObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, false);
+ return AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads);
+}
+
+/* static */
+RemoteLazyInputStreamThread* RemoteLazyInputStreamThread::Get() {
+ if (RLISThreadIsInOrBeyondShutdown()) {
+ return nullptr;
+ }
+
+ StaticMutexAutoLock lock(gRemoteLazyThreadMutex);
+
+ return gRemoteLazyThread;
+}
+
+/* static */
+RemoteLazyInputStreamThread* RemoteLazyInputStreamThread::GetOrCreate() {
+ if (RLISThreadIsInOrBeyondShutdown()) {
+ return nullptr;
+ }
+
+ StaticMutexAutoLock lock(gRemoteLazyThreadMutex);
+
+ if (!gRemoteLazyThread) {
+ gRemoteLazyThread = new RemoteLazyInputStreamThread();
+ if (!gRemoteLazyThread->Initialize()) {
+ gRemoteLazyThread = nullptr;
+ }
+ }
+
+ return gRemoteLazyThread;
+}
+
+bool RemoteLazyInputStreamThread::Initialize() {
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewNamedThread("RemoteLzyStream", getter_AddRefs(thread));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ mThread = thread;
+
+ if (!NS_IsMainThread()) {
+ RefPtr<Runnable> runnable = new ThreadInitializeRunnable();
+ nsresult rv =
+ SchedulerGroup::Dispatch(TaskCategory::Other, runnable.forget());
+ return !NS_WARN_IF(NS_FAILED(rv));
+ }
+
+ return InitializeOnMainThread();
+}
+
+bool RemoteLazyInputStreamThread::InitializeOnMainThread() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (NS_WARN_IF(!obs)) {
+ return false;
+ }
+
+ nsresult rv =
+ obs->AddObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, false);
+ return !NS_WARN_IF(NS_FAILED(rv));
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStreamThread::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(!strcmp(aTopic, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID));
+
+ StaticMutexAutoLock lock(gRemoteLazyThreadMutex);
+
+ if (mThread) {
+ mThread->Shutdown();
+ mThread = nullptr;
+ }
+
+ gRemoteLazyThread = nullptr;
+
+ return NS_OK;
+}
+
+// nsIEventTarget
+
+NS_IMETHODIMP_(bool)
+RemoteLazyInputStreamThread::IsOnCurrentThreadInfallible() {
+ return mThread->IsOnCurrentThread();
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStreamThread::IsOnCurrentThread(bool* aRetval) {
+ return mThread->IsOnCurrentThread(aRetval);
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStreamThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable,
+ uint32_t aFlags) {
+ if (RLISThreadIsInOrBeyondShutdown()) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ nsCOMPtr<nsIRunnable> runnable(aRunnable);
+
+ StaticMutexAutoLock lock(gRemoteLazyThreadMutex);
+
+ return mThread->Dispatch(runnable.forget(), aFlags);
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStreamThread::DispatchFromScript(nsIRunnable* aRunnable,
+ uint32_t aFlags) {
+ nsCOMPtr<nsIRunnable> runnable(aRunnable);
+ return Dispatch(runnable.forget(), aFlags);
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStreamThread::DelayedDispatch(already_AddRefed<nsIRunnable>,
+ uint32_t) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStreamThread::RegisterShutdownTask(nsITargetShutdownTask*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStreamThread::UnregisterShutdownTask(nsITargetShutdownTask*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+RemoteLazyInputStreamThread::DispatchDirectTask(
+ already_AddRefed<nsIRunnable> aRunnable) {
+ nsCOMPtr<nsIRunnable> runnable(aRunnable);
+
+ StaticMutexAutoLock lock(gRemoteLazyThreadMutex);
+
+ nsCOMPtr<nsIDirectTaskDispatcher> dispatcher = do_QueryInterface(mThread);
+
+ if (dispatcher) {
+ return dispatcher->DispatchDirectTask(runnable.forget());
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP RemoteLazyInputStreamThread::DrainDirectTasks() {
+ StaticMutexAutoLock lock(gRemoteLazyThreadMutex);
+
+ nsCOMPtr<nsIDirectTaskDispatcher> dispatcher = do_QueryInterface(mThread);
+
+ if (dispatcher) {
+ return dispatcher->DrainDirectTasks();
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP RemoteLazyInputStreamThread::HaveDirectTasks(bool* aValue) {
+ StaticMutexAutoLock lock(gRemoteLazyThreadMutex);
+
+ nsCOMPtr<nsIDirectTaskDispatcher> dispatcher = do_QueryInterface(mThread);
+
+ if (dispatcher) {
+ return dispatcher->HaveDirectTasks(aValue);
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+bool IsOnDOMFileThread() {
+ MOZ_ASSERT(!RLISThreadIsInOrBeyondShutdown());
+
+ StaticMutexAutoLock lock(gRemoteLazyThreadMutex);
+ MOZ_ASSERT(gRemoteLazyThread);
+
+ return gRemoteLazyThread->IsOnCurrentThreadInfallible();
+}
+
+void AssertIsOnDOMFileThread() { MOZ_ASSERT(IsOnDOMFileThread()); }
+
+} // namespace mozilla
diff --git a/dom/file/ipc/RemoteLazyInputStreamThread.h b/dom/file/ipc/RemoteLazyInputStreamThread.h
new file mode 100644
index 0000000000..378cb09009
--- /dev/null
+++ b/dom/file/ipc/RemoteLazyInputStreamThread.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_RemoteLazyInputStreamThread_h
+#define mozilla_RemoteLazyInputStreamThread_h
+
+#include "mozilla/RemoteLazyInputStreamChild.h"
+#include "nsIEventTarget.h"
+#include "nsIObserver.h"
+#include "nsTArray.h"
+
+class nsIThread;
+
+namespace mozilla {
+
+class RemoteLazyInputStreamChild;
+
+// XXX Rename this class since it's used by LSNG too.
+class RemoteLazyInputStreamThread final : public nsIObserver,
+ public nsISerialEventTarget,
+ public nsIDirectTaskDispatcher {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIEVENTTARGET
+ NS_DECL_NSISERIALEVENTTARGET
+ NS_DECL_NSIDIRECTTASKDISPATCHER
+
+ static RemoteLazyInputStreamThread* Get();
+
+ static RemoteLazyInputStreamThread* GetOrCreate();
+
+ bool Initialize();
+
+ bool InitializeOnMainThread();
+
+ private:
+ ~RemoteLazyInputStreamThread() = default;
+
+ nsCOMPtr<nsIThread> mThread;
+};
+
+bool IsOnDOMFileThread();
+
+void AssertIsOnDOMFileThread();
+
+} // namespace mozilla
+
+#endif // mozilla_RemoteLazyInputStreamThread_h
diff --git a/dom/file/ipc/TemporaryIPCBlobChild.cpp b/dom/file/ipc/TemporaryIPCBlobChild.cpp
new file mode 100644
index 0000000000..7c7df55d81
--- /dev/null
+++ b/dom/file/ipc/TemporaryIPCBlobChild.cpp
@@ -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/. */
+
+#include "TemporaryIPCBlobChild.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/MutableBlobStorage.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include <private/pprio.h>
+
+namespace mozilla::dom {
+
+TemporaryIPCBlobChild::TemporaryIPCBlobChild(MutableBlobStorage* aStorage)
+ : mMutableBlobStorage(aStorage), mActive(true) {
+ MOZ_ASSERT(aStorage);
+}
+
+TemporaryIPCBlobChild::~TemporaryIPCBlobChild() = default;
+
+mozilla::ipc::IPCResult TemporaryIPCBlobChild::RecvFileDesc(
+ const FileDescriptor& aFD) {
+ MOZ_ASSERT(mActive);
+
+ auto rawFD = aFD.ClonePlatformHandle();
+ PRFileDesc* prfile = PR_ImportFile(PROsfd(rawFD.release()));
+
+ mMutableBlobStorage->TemporaryFileCreated(prfile);
+ mMutableBlobStorage = nullptr;
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult TemporaryIPCBlobChild::Recv__delete__(
+ const IPCBlobOrError& aBlobOrError) {
+ mActive = false;
+ mMutableBlobStorage = nullptr;
+
+ if (aBlobOrError.type() == IPCBlobOrError::TIPCBlob) {
+ // This must be always deserialized.
+ RefPtr<BlobImpl> blobImpl =
+ IPCBlobUtils::Deserialize(aBlobOrError.get_IPCBlob());
+ MOZ_ASSERT(blobImpl);
+
+ if (mCallback) {
+ mCallback->OperationSucceeded(blobImpl);
+ }
+ } else if (mCallback) {
+ MOZ_ASSERT(aBlobOrError.type() == IPCBlobOrError::Tnsresult);
+ mCallback->OperationFailed(aBlobOrError.get_nsresult());
+ }
+
+ mCallback = nullptr;
+
+ return IPC_OK();
+}
+
+void TemporaryIPCBlobChild::ActorDestroy(ActorDestroyReason aWhy) {
+ mActive = false;
+ mMutableBlobStorage = nullptr;
+
+ if (mCallback) {
+ mCallback->OperationFailed(NS_ERROR_FAILURE);
+ mCallback = nullptr;
+ }
+}
+
+void TemporaryIPCBlobChild::AskForBlob(TemporaryIPCBlobChildCallback* aCallback,
+ const nsACString& aContentType,
+ PRFileDesc* aFD) {
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(!mCallback);
+
+ if (!mActive) {
+ aCallback->OperationFailed(NS_ERROR_FAILURE);
+ return;
+ }
+
+ FileDescriptor fdd = FileDescriptor(
+ FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(aFD)));
+
+ mCallback = aCallback;
+ SendOperationDone(aContentType, fdd);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/ipc/TemporaryIPCBlobChild.h b/dom/file/ipc/TemporaryIPCBlobChild.h
new file mode 100644
index 0000000000..a909ee0d53
--- /dev/null
+++ b/dom/file/ipc/TemporaryIPCBlobChild.h
@@ -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/. */
+
+#ifndef mozilla_dom_TemporaryIPCBlobChild_h
+#define mozilla_dom_TemporaryIPCBlobChild_h
+
+#include "mozilla/dom/PTemporaryIPCBlob.h"
+#include "mozilla/dom/PTemporaryIPCBlobChild.h"
+
+namespace mozilla::dom {
+
+class BlobImpl;
+class MutableBlobStorage;
+
+class TemporaryIPCBlobChildCallback {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual void OperationSucceeded(BlobImpl* aBlobImpl) = 0;
+ virtual void OperationFailed(nsresult aRv) = 0;
+};
+
+class TemporaryIPCBlobChild final : public PTemporaryIPCBlobChild {
+ friend class PTemporaryIPCBlobChild;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(TemporaryIPCBlobChild)
+
+ explicit TemporaryIPCBlobChild(MutableBlobStorage* aStorage);
+
+ void AskForBlob(TemporaryIPCBlobChildCallback* aCallback,
+ const nsACString& aContentType, PRFileDesc* aFD);
+
+ private:
+ ~TemporaryIPCBlobChild() override;
+
+ mozilla::ipc::IPCResult RecvFileDesc(const FileDescriptor& aFD);
+
+ mozilla::ipc::IPCResult Recv__delete__(const IPCBlobOrError& aBlobOrError);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ RefPtr<MutableBlobStorage> mMutableBlobStorage;
+ RefPtr<TemporaryIPCBlobChildCallback> mCallback;
+ bool mActive;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_TemporaryIPCBlobChild_h
diff --git a/dom/file/ipc/TemporaryIPCBlobParent.cpp b/dom/file/ipc/TemporaryIPCBlobParent.cpp
new file mode 100644
index 0000000000..bd2c6beefc
--- /dev/null
+++ b/dom/file/ipc/TemporaryIPCBlobParent.cpp
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "private/pprio.h"
+#include "TemporaryIPCBlobParent.h"
+#include "mozilla/dom/FileBlobImpl.h"
+#include "nsAnonymousTemporaryFile.h"
+#include "TemporaryFileBlobImpl.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+
+namespace mozilla::dom {
+
+TemporaryIPCBlobParent::TemporaryIPCBlobParent() : mActive(true) {}
+
+TemporaryIPCBlobParent::~TemporaryIPCBlobParent() {
+ // If we still have mFile, let's remove it.
+ if (mFile) {
+ mFile->Remove(false);
+ }
+}
+
+mozilla::ipc::IPCResult TemporaryIPCBlobParent::CreateAndShareFile() {
+ MOZ_ASSERT(mActive);
+ MOZ_ASSERT(!mFile);
+
+ nsresult rv = NS_OpenAnonymousTemporaryNsIFile(getter_AddRefs(mFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return SendDeleteError(rv);
+ }
+
+ PRFileDesc* fd;
+ rv = mFile->OpenNSPRFileDesc(PR_RDWR, PR_IRWXU, &fd);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return SendDeleteError(rv);
+ }
+
+ FileDescriptor fdd = FileDescriptor(
+ FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(fd)));
+
+ // The FileDescriptor object owns a duplicate of the file handle; we
+ // must close the original (and clean up the NSPR descriptor).
+ PR_Close(fd);
+
+ (void)SendFileDesc(fdd);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult TemporaryIPCBlobParent::RecvOperationFailed() {
+ MOZ_ASSERT(mActive);
+ mActive = false;
+
+ // Nothing to do.
+ (void)Send__delete__(this, NS_ERROR_FAILURE);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult TemporaryIPCBlobParent::RecvOperationDone(
+ const nsCString& aContentType, const FileDescriptor& aFD) {
+ MOZ_ASSERT(mActive);
+ mActive = false;
+
+ // We have received a file descriptor because in this way we have kept the
+ // file locked on windows during the IPC communication. After the creation of
+ // the TemporaryFileBlobImpl, this prfile can be closed.
+ auto rawFD = aFD.ClonePlatformHandle();
+ PRFileDesc* prfile = PR_ImportFile(PROsfd(rawFD.release()));
+
+ // Let's create the BlobImpl.
+ nsCOMPtr<nsIFile> file = std::move(mFile);
+
+ RefPtr<TemporaryFileBlobImpl> blobImpl =
+ new TemporaryFileBlobImpl(file, NS_ConvertUTF8toUTF16(aContentType));
+
+ PR_Close(prfile);
+
+ IPCBlob ipcBlob;
+ nsresult rv = IPCBlobUtils::Serialize(blobImpl, ipcBlob);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ (void)Send__delete__(this, NS_ERROR_FAILURE);
+ return IPC_OK();
+ }
+
+ (void)Send__delete__(this, ipcBlob);
+ return IPC_OK();
+}
+
+void TemporaryIPCBlobParent::ActorDestroy(ActorDestroyReason aWhy) {
+ mActive = false;
+}
+
+mozilla::ipc::IPCResult TemporaryIPCBlobParent::SendDeleteError(nsresult aRv) {
+ MOZ_ASSERT(mActive);
+ mActive = false;
+
+ (void)Send__delete__(this, aRv);
+ return IPC_OK();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/ipc/TemporaryIPCBlobParent.h b/dom/file/ipc/TemporaryIPCBlobParent.h
new file mode 100644
index 0000000000..2609e8d820
--- /dev/null
+++ b/dom/file/ipc/TemporaryIPCBlobParent.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_TemporaryIPCBlobParent_h
+#define mozilla_dom_TemporaryIPCBlobParent_h
+
+#include "mozilla/dom/PTemporaryIPCBlob.h"
+#include "mozilla/dom/PTemporaryIPCBlobParent.h"
+
+class nsIFile;
+
+namespace mozilla::dom {
+
+class TemporaryIPCBlobParent final : public PTemporaryIPCBlobParent {
+ friend class PTemporaryIPCBlobParent;
+
+ public:
+ explicit TemporaryIPCBlobParent();
+
+ mozilla::ipc::IPCResult CreateAndShareFile();
+
+ private:
+ ~TemporaryIPCBlobParent() override;
+
+ mozilla::ipc::IPCResult RecvOperationFailed();
+
+ mozilla::ipc::IPCResult RecvOperationDone(const nsCString& aContentType,
+ const FileDescriptor& aFD);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult SendDeleteError(nsresult aRv);
+
+ nsCOMPtr<nsIFile> mFile;
+ bool mActive;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_TemporaryIPCBlobParent_h
diff --git a/dom/file/ipc/moz.build b/dom/file/ipc/moz.build
new file mode 100644
index 0000000000..21c2d50cac
--- /dev/null
+++ b/dom/file/ipc/moz.build
@@ -0,0 +1,69 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: File")
+
+XPIDL_SOURCES += [
+ "mozIRemoteLazyInputStream.idl",
+]
+
+XPIDL_MODULE = "dom"
+
+EXPORTS.mozilla.dom += [
+ "FileCreatorChild.h",
+ "FileCreatorParent.h",
+ "IPCBlobUtils.h",
+ "TemporaryIPCBlobChild.h",
+ "TemporaryIPCBlobParent.h",
+]
+
+EXPORTS.mozilla += [
+ "RemoteLazyInputStream.h",
+ "RemoteLazyInputStreamChild.h",
+ "RemoteLazyInputStreamParent.h",
+ "RemoteLazyInputStreamStorage.h",
+ "RemoteLazyInputStreamThread.h",
+]
+
+UNIFIED_SOURCES += [
+ "FileCreatorChild.cpp",
+ "FileCreatorParent.cpp",
+ "IPCBlobUtils.cpp",
+ "RemoteLazyInputStream.cpp",
+ "RemoteLazyInputStreamChild.cpp",
+ "RemoteLazyInputStreamParent.cpp",
+ "RemoteLazyInputStreamStorage.cpp",
+ "RemoteLazyInputStreamThread.cpp",
+ "TemporaryIPCBlobChild.cpp",
+ "TemporaryIPCBlobParent.cpp",
+]
+
+IPDL_SOURCES += [
+ "IPCBlob.ipdlh",
+ "PFileCreator.ipdl",
+ "PRemoteLazyInputStream.ipdl",
+ "PTemporaryIPCBlob.ipdl",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/file",
+ "/dom/ipc",
+ "/xpcom/build",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"]
+MOCHITEST_MANIFESTS += ["tests/mochitest.ini"]
diff --git a/dom/file/ipc/mozIRemoteLazyInputStream.idl b/dom/file/ipc/mozIRemoteLazyInputStream.idl
new file mode 100644
index 0000000000..8303b2e0fc
--- /dev/null
+++ b/dom/file/ipc/mozIRemoteLazyInputStream.idl
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIInputStream;
+
+/*
+ * A simple interface to get the underlying stream from an
+ * RemoteLazyInputStream.
+ */
+[scriptable, builtinclass, uuid(4125585f-b0c2-4964-a83c-4b0d99f26d49)]
+interface mozIRemoteLazyInputStream : nsISupports
+{
+ /**
+ * Attempts to take the internal stream out of this mozIRemoteLazyInputStream.
+ * Throws NS_BASE_STREAM_WOULD_BLOCK if the stream isn't available yet, and
+ * NS_BASE_STREAM_CLOSED if it was already closed.
+ */
+ [noscript] nsIInputStream TakeInternalStream();
+
+ /**
+ * If this RemoteLazyInputStream is actively backed by an actor, get the
+ * underlying actor's ID. Will throw if the underlying actor is no longer
+ * available.
+ */
+ [noscript] readonly attribute nsIDRef internalStreamID;
+};
diff --git a/dom/file/ipc/tests/browser.ini b/dom/file/ipc/tests/browser.ini
new file mode 100644
index 0000000000..f6fcdc37a4
--- /dev/null
+++ b/dom/file/ipc/tests/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+ empty.html
+
+[browser_ipcBlob.js]
+[browser_ipcBlob_temporary.js]
+support-files = temporary.sjs
diff --git a/dom/file/ipc/tests/browser_ipcBlob.js b/dom/file/ipc/tests/browser_ipcBlob.js
new file mode 100644
index 0000000000..17fe31e0bd
--- /dev/null
+++ b/dom/file/ipc/tests/browser_ipcBlob.js
@@ -0,0 +1,253 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */
+
+requestLongerTimeout(3);
+
+const BASE_URI = "http://mochi.test:8888/browser/dom/file/ipc/tests/empty.html";
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.partition.bloburl_per_agent_cluster", false]],
+ });
+});
+
+// More than 1mb memory blob childA-parent-childB.
+add_task(async function test_CtoPtoC_big() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser1 = gBrowser.getBrowserForTab(tab1);
+
+ let blob = await SpecialPowers.spawn(browser1, [], function () {
+ Cu.importGlobalProperties(["Blob"]);
+ let blob = new Blob([new Array(1024 * 1024).join("123456789ABCDEF")]);
+ return blob;
+ });
+
+ ok(blob, "CtoPtoC-big: We have a blob!");
+ is(
+ blob.size,
+ new Array(1024 * 1024).join("123456789ABCDEF").length,
+ "CtoPtoC-big: The size matches"
+ );
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser2 = gBrowser.getBrowserForTab(tab2);
+
+ let status = await SpecialPowers.spawn(browser2, [blob], function (blob) {
+ return new Promise(resolve => {
+ let fr = new content.FileReader();
+ fr.readAsText(blob);
+ fr.onloadend = function () {
+ resolve(fr.result == new Array(1024 * 1024).join("123456789ABCDEF"));
+ };
+ });
+ });
+
+ ok(status, "CtoPtoC-big: Data match!");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Less than 1mb memory blob childA-parent-childB.
+add_task(async function test_CtoPtoC_small() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser1 = gBrowser.getBrowserForTab(tab1);
+
+ let blob = await SpecialPowers.spawn(browser1, [], function () {
+ Cu.importGlobalProperties(["Blob"]);
+ let blob = new Blob(["hello world!"]);
+ return blob;
+ });
+
+ ok(blob, "CtoPtoC-small: We have a blob!");
+ is(blob.size, "hello world!".length, "CtoPtoC-small: The size matches");
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser2 = gBrowser.getBrowserForTab(tab2);
+
+ let status = await SpecialPowers.spawn(browser2, [blob], function (blob) {
+ return new Promise(resolve => {
+ let fr = new content.FileReader();
+ fr.readAsText(blob);
+ fr.onloadend = function () {
+ resolve(fr.result == "hello world!");
+ };
+ });
+ });
+
+ ok(status, "CtoPtoC-small: Data match!");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// More than 1mb memory blob childA-parent-childB: BroadcastChannel
+add_task(async function test_CtoPtoC_bc_big() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser1 = gBrowser.getBrowserForTab(tab1);
+
+ await SpecialPowers.spawn(browser1, [], function () {
+ Cu.importGlobalProperties(["Blob"]);
+ var bc = new content.BroadcastChannel("test");
+ bc.onmessage = function () {
+ bc.postMessage(
+ new Blob([new Array(1024 * 1024).join("123456789ABCDEF")])
+ );
+ };
+ });
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser2 = gBrowser.getBrowserForTab(tab2);
+
+ let status = await SpecialPowers.spawn(browser2, [], function () {
+ return new Promise(resolve => {
+ var bc = new content.BroadcastChannel("test");
+ bc.onmessage = function (e) {
+ let fr = new content.FileReader();
+ fr.readAsText(e.data);
+ fr.onloadend = function () {
+ resolve(fr.result == new Array(1024 * 1024).join("123456789ABCDEF"));
+ };
+ };
+
+ bc.postMessage("GO!");
+ });
+ });
+
+ ok(status, "CtoPtoC-broadcastChannel-big: Data match!");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Less than 1mb memory blob childA-parent-childB: BroadcastChannel
+add_task(async function test_CtoPtoC_bc_small() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser1 = gBrowser.getBrowserForTab(tab1);
+
+ await SpecialPowers.spawn(browser1, [], function () {
+ Cu.importGlobalProperties(["Blob"]);
+ var bc = new content.BroadcastChannel("test");
+ bc.onmessage = function () {
+ bc.postMessage(new Blob(["hello world!"]));
+ };
+ });
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser2 = gBrowser.getBrowserForTab(tab2);
+
+ let status = await SpecialPowers.spawn(browser2, [], function () {
+ return new Promise(resolve => {
+ var bc = new content.BroadcastChannel("test");
+ bc.onmessage = function (e) {
+ let fr = new content.FileReader();
+ fr.readAsText(e.data);
+ fr.onloadend = function () {
+ resolve(fr.result == "hello world!");
+ };
+ };
+
+ bc.postMessage("GO!");
+ });
+ });
+
+ ok(status, "CtoPtoC-broadcastChannel-small: Data match!");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// blob URL childA-parent-childB
+add_task(async function test_CtoPtoC_bc_small() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser1 = gBrowser.getBrowserForTab(tab1);
+
+ let blobURL = await SpecialPowers.spawn(browser1, [], function () {
+ Cu.importGlobalProperties(["Blob"]);
+ return content.URL.createObjectURL(new content.Blob(["hello world!"]));
+ });
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser2 = gBrowser.getBrowserForTab(tab2);
+
+ let status = await SpecialPowers.spawn(
+ browser2,
+ [blobURL],
+ function (blobURL) {
+ return new Promise(resolve => {
+ var xhr = new content.XMLHttpRequest();
+ xhr.open("GET", blobURL);
+ xhr.onloadend = function () {
+ resolve(xhr.response == "hello world!");
+ };
+
+ xhr.send();
+ });
+ }
+ );
+
+ ok(status, "CtoPtoC-blobURL: Data match!");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Multipart Blob childA-parent-childB.
+add_task(async function test_CtoPtoC_multipart() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser1 = gBrowser.getBrowserForTab(tab1);
+
+ let blob = await SpecialPowers.spawn(browser1, [], function () {
+ Cu.importGlobalProperties(["Blob"]);
+ return new Blob(["!"]);
+ });
+
+ ok(blob, "CtoPtoC-multipart: We have a blob!");
+ is(blob.size, "!".length, "CtoPtoC-multipart: The size matches");
+
+ let newBlob = new Blob(["world", blob]);
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser2 = gBrowser.getBrowserForTab(tab2);
+
+ let status = await SpecialPowers.spawn(browser2, [newBlob], function (blob) {
+ Cu.importGlobalProperties(["Blob"]);
+ return new Promise(resolve => {
+ let fr = new content.FileReader();
+ fr.readAsText(new Blob(["hello ", blob]));
+ fr.onloadend = function () {
+ resolve(fr.result == "hello world!");
+ };
+ });
+ });
+
+ ok(status, "CtoPtoC-multipart: Data match!");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Multipart Blob childA-parent with a max size
+add_task(async function test_CtoPsize_multipart() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ let blob = await SpecialPowers.spawn(browser, [], function () {
+ Cu.importGlobalProperties(["Blob"]);
+
+ let data = new Array(1024 * 512).join("A");
+ let blob1 = new Blob([data]);
+ let blob2 = new Blob([data]);
+ let blob3 = new Blob([data]);
+
+ return new Blob([blob1, blob2, blob3]);
+ });
+
+ ok(blob, "CtoPsize-multipart: We have a blob!");
+ is(
+ blob.size,
+ new Array(1024 * 512).join("A").length * 3,
+ "CtoPsize-multipart: The size matches"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/file/ipc/tests/browser_ipcBlob_temporary.js b/dom/file/ipc/tests/browser_ipcBlob_temporary.js
new file mode 100644
index 0000000000..cc65768140
--- /dev/null
+++ b/dom/file/ipc/tests/browser_ipcBlob_temporary.js
@@ -0,0 +1,115 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */
+
+requestLongerTimeout(3);
+
+const BASE_URI = "http://mochi.test:8888/browser/dom/file/ipc/tests/empty.html";
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.blob.memoryToTemporaryFile", 1],
+ ["dom.ipc.processCount", 4],
+ ],
+ });
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser1 = gBrowser.getBrowserForTab(tab1);
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI);
+ let browser2 = gBrowser.getBrowserForTab(tab2);
+
+ await SpecialPowers.spawn(browser2, [], function () {
+ content.window.testPromise = new content.window.Promise(resolve => {
+ let bc = new content.window.BroadcastChannel("foobar");
+ bc.onmessage = e => {
+ function realTest() {
+ return new content.window.Promise(resolve => {
+ let count = 10;
+ for (let i = 0; i < count; ++i) {
+ info("FileReader at the same time: " + i);
+ let fr = new content.window.FileReader();
+ fr.readAsText(e.data);
+ fr.onerror = () => {
+ ok(false, "Something wrong happened.");
+ };
+
+ fr.onloadend = () => {
+ is(fr.result.length, e.data.size, "FileReader worked fine.");
+ if (!--count) {
+ resolve(true);
+ }
+ };
+ }
+ });
+ }
+
+ let promises = [];
+ for (let i = 0; i < 5; ++i) {
+ promises.push(realTest());
+ }
+
+ Promise.all(promises).then(() => {
+ resolve(true);
+ });
+ };
+ });
+ });
+
+ let status = await SpecialPowers.spawn(browser1, [], function () {
+ let p = new content.window.Promise(resolve => {
+ let xhr = new content.window.XMLHttpRequest();
+ xhr.open("GET", "temporary.sjs", true);
+ xhr.responseType = "blob";
+ xhr.onload = () => {
+ resolve(xhr.response);
+ };
+ xhr.send();
+ });
+
+ return p.then(blob => {
+ function realTest() {
+ return new content.window.Promise(resolve => {
+ info("Let's broadcast the blob...");
+ let bc = new content.window.BroadcastChannel("foobar");
+ bc.postMessage(blob);
+
+ info("Here the test...");
+ let count = 10;
+ for (let i = 0; i < count; ++i) {
+ info("FileReader at the same time: " + i);
+ let fr = new content.window.FileReader();
+ fr.readAsText(blob);
+ fr.onerror = () => {
+ ok(false, "Something wrong happened.");
+ };
+
+ fr.onloadend = () => {
+ is(fr.result.length, blob.size, "FileReader worked fine.");
+ if (!--count) {
+ resolve(true);
+ }
+ };
+ }
+ });
+ }
+
+ let promises = [];
+ for (let i = 0; i < 5; ++i) {
+ promises.push(realTest());
+ }
+
+ return Promise.all(promises);
+ });
+ });
+
+ ok(status, "All good for tab1!");
+
+ status = await SpecialPowers.spawn(browser2, [], function () {
+ return content.window.testPromise;
+ });
+
+ ok(status, "All good for tab2!");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/dom/file/ipc/tests/empty.html b/dom/file/ipc/tests/empty.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/file/ipc/tests/empty.html
diff --git a/dom/file/ipc/tests/green.jpg b/dom/file/ipc/tests/green.jpg
new file mode 100644
index 0000000000..48c454d27c
--- /dev/null
+++ b/dom/file/ipc/tests/green.jpg
Binary files differ
diff --git a/dom/file/ipc/tests/mochitest.ini b/dom/file/ipc/tests/mochitest.ini
new file mode 100644
index 0000000000..99b885762a
--- /dev/null
+++ b/dom/file/ipc/tests/mochitest.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files = script_file.js
+
+[test_ipcBlob_fileReaderSync.html]
+[test_ipcBlob_workers.html]
+[test_ipcBlob_createImageBitmap.html]
+support-files = green.jpg
+skip-if =
+ http3
+[test_ipcBlob_emptyMultiplex.html]
+[test_ipcBlob_mixedMultiplex.html]
+support-files = ok.sjs
diff --git a/dom/file/ipc/tests/ok.sjs b/dom/file/ipc/tests/ok.sjs
new file mode 100644
index 0000000000..42f65733d9
--- /dev/null
+++ b/dom/file/ipc/tests/ok.sjs
@@ -0,0 +1,11 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(request.getHeader("Content-Length"));
+}
diff --git a/dom/file/ipc/tests/script_file.js b/dom/file/ipc/tests/script_file.js
new file mode 100644
index 0000000000..b671d46f39
--- /dev/null
+++ b/dom/file/ipc/tests/script_file.js
@@ -0,0 +1,53 @@
+/* eslint-env mozilla/chrome-script */
+
+Cu.importGlobalProperties(["File"]);
+
+addMessageListener("file.open", function (e) {
+ var testFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIDirectoryService)
+ .QueryInterface(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ testFile.append("ipc_fileReader_testing");
+ testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ var outStream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ outStream.init(
+ testFile,
+ 0x02 | 0x08 | 0x20, // write, create, truncate
+ 0o666,
+ 0
+ );
+
+ var fileData = "Hello World!";
+ outStream.write(fileData, fileData.length);
+ outStream.close();
+
+ File.createFromNsIFile(testFile).then(function (file) {
+ sendAsyncMessage("file.opened", { file });
+ });
+});
+
+addMessageListener("emptyfile.open", function (e) {
+ var testFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIDirectoryService)
+ .QueryInterface(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ testFile.append("ipc_fileReader_testing");
+ testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ var outStream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ outStream.init(
+ testFile,
+ 0x02 | 0x08 | 0x20, // write, create, truncate
+ 0o666,
+ 0
+ );
+
+ File.createFromNsIFile(testFile).then(function (file) {
+ sendAsyncMessage("emptyfile.opened", { file });
+ });
+});
diff --git a/dom/file/ipc/tests/temporary.sjs b/dom/file/ipc/tests/temporary.sjs
new file mode 100644
index 0000000000..76db4e2f2e
--- /dev/null
+++ b/dom/file/ipc/tests/temporary.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+
+ var data = new Array(1024 * 64).join("1234567890ABCDEF");
+ response.bodyOutputStream.write(data, data.length);
+}
diff --git a/dom/file/ipc/tests/test_ipcBlob_createImageBitmap.html b/dom/file/ipc/tests/test_ipcBlob_createImageBitmap.html
new file mode 100644
index 0000000000..868888c529
--- /dev/null
+++ b/dom/file/ipc/tests/test_ipcBlob_createImageBitmap.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test IPCBlob and CreateImageBitmap</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function test_mainThread() {
+ let bc = new BroadcastChannel('testMainThread');
+ bc.onmessage = e => {
+ createImageBitmap(e.data).then(image => {
+ ok(image.height, "this image has a valid size.");
+ }, () => {
+ ok(false, "error creating the image!");
+ }).then(next);
+ }
+
+ fetch('green.jpg').then(r => r.blob()).then(blob => {
+ let bc = new BroadcastChannel('testMainThread');
+ bc.postMessage(blob);
+ });
+}
+
+function test_worker() {
+ function workerScript() {
+ function ok(a, msg) { postMessage({ type: 'test', status: !!a, msg }); };
+ function finish() { postMessage({ type: 'finish' }); };
+
+ let bc = new BroadcastChannel('testWorker');
+ bc.onmessage = e => {
+ createImageBitmap(e.data).then(image => {
+ ok(image.height, "this image has a valid size.");
+ }, () => {
+ ok(false, "error creating the image!");
+ }).then(finish);
+ }
+
+ fetch('http://mochi.test:8888/tests/dom/file/ipc/tests/green.jpg').then(r => r.blob()).then(blob => {
+ let bc = new BroadcastChannel('testWorker');
+ bc.postMessage(blob);
+ });
+ }
+ let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"]));
+ let worker = new Worker(workerUrl);
+
+ worker.onmessage = event => {
+ if (event.data.type == 'test') {
+ ok(event.data.status, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == 'finish') {
+ next();
+ }
+ }
+}
+
+let tests = [
+ test_mainThread,
+ test_worker,
+];
+
+function next() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ let test = tests.shift();
+ test();
+}
+
+next();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/file/ipc/tests/test_ipcBlob_emptyMultiplex.html b/dom/file/ipc/tests/test_ipcBlob_emptyMultiplex.html
new file mode 100644
index 0000000000..0487069467
--- /dev/null
+++ b/dom/file/ipc/tests/test_ipcBlob_emptyMultiplex.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test an empty IPCBlob together with other parts</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="text/javascript">
+
+function checkContent(msg, content) {
+ return new Promise(resolve => {
+ let fr = new FileReader();
+ fr.readAsText(new Blob(content));
+ fr.onloadend = () => {
+ is(fr.result, "Hello world!", "The content matches: " + msg);
+ resolve();
+ };
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+let url = SimpleTest.getTestFileURL("script_file.js");
+
+let script = SpecialPowers.loadChromeScript(url);
+script.addMessageListener("emptyfile.opened", message => {
+ checkContent("middle", ["Hello ", message.file, "world!"]).
+ then(() => checkContent("begin", [message.file, "Hello world!"])).
+ then(() => checkContent("end", ["Hello world!", message.file])).
+ then(() => checkContent("random", [message.file, message.file, "Hello world!", message.file])).
+ then(() => checkContent("random 2", [message.file, message.file, "Hello ",
+ message.file, "world", message.file,
+ message.file, "!", message.file, "",
+ message.file, message.file])).
+ then(SimpleTest.finish);
+});
+
+script.sendAsyncMessage("emptyfile.open");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/file/ipc/tests/test_ipcBlob_fileReaderSync.html b/dom/file/ipc/tests/test_ipcBlob_fileReaderSync.html
new file mode 100644
index 0000000000..f37d4b79ed
--- /dev/null
+++ b/dom/file/ipc/tests/test_ipcBlob_fileReaderSync.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test IPCBlob and FileReaderSync</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="text/javascript">
+
+function workerScript() {
+ onmessage = function(event) {
+ let readerMemoryBlob = new FileReaderSync();
+ let status = readerMemoryBlob.readAsText(new Blob(['hello world'])) == 'hello world';
+ postMessage({ status, message: "FileReaderSync with memory blob still works" });
+
+ let readerIPCBlob1 = new FileReaderSync();
+ postMessage({ blob: event.data, method: 'readAsText',
+ data: readerIPCBlob1.readAsText(event.data)});
+
+ let readerIPCBlob2 = new FileReaderSync();
+ postMessage({ blob: event.data, method: 'readAsArrayBuffer',
+ data: readerIPCBlob2.readAsArrayBuffer(event.data)});
+
+ let readerIPCBlob3 = new FileReaderSync();
+ postMessage({ blob: event.data, method: 'readAsDataURL',
+ data: readerIPCBlob3.readAsDataURL(event.data)});
+
+ let multipartBlob = new Blob(['wow', event.data]);
+
+ let readerIPCBlobMultipart1 = new FileReaderSync();
+ postMessage({ blob: multipartBlob, method: 'readAsText',
+ data: readerIPCBlobMultipart1.readAsText(multipartBlob)});
+
+ let readerIPCBlobMultipart2 = new FileReaderSync();
+ postMessage({ blob: multipartBlob, method: 'readAsArrayBuffer',
+ data: readerIPCBlobMultipart2.readAsArrayBuffer(multipartBlob)});
+
+ let readerIPCBlobMultipart3 = new FileReaderSync();
+ postMessage({ blob: multipartBlob, method: 'readAsDataURL',
+ data: readerIPCBlobMultipart3.readAsDataURL(multipartBlob)});
+
+ postMessage({ finish: true });
+ }
+}
+
+let completed = false;
+let pendingTasks = 0;
+function maybeFinish() {
+ if (completed && !pendingTasks) {
+ SimpleTest.finish();
+ }
+}
+
+let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"]));
+let worker = new Worker(workerUrl);
+worker.onmessage = event => {
+ if ("status" in event.data) {
+ ok(event.data.status, event.data.message);
+ return;
+ }
+
+ if ("blob" in event.data) {
+ let fr = new FileReader();
+ fr[event.data.method](event.data.blob);
+ ++pendingTasks;
+ fr.onload = () => {
+ if (event.data.method != 'readAsArrayBuffer') {
+ is(event.data.data, fr.result, "The file has been read");
+ } else {
+ is(event.data.data.byteLength, fr.result.byteLength, "The file has been read");
+ }
+ --pendingTasks;
+ maybeFinish();
+ }
+
+ return;
+ }
+
+ if ("finish" in event.data) {
+ completed = true;
+ maybeFinish();
+ }
+};
+
+let url = SimpleTest.getTestFileURL("script_file.js");
+let script = SpecialPowers.loadChromeScript(url);
+script.addMessageListener("file.opened", message => {
+ worker.postMessage(message.file);
+});
+
+script.sendAsyncMessage("file.open");
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/file/ipc/tests/test_ipcBlob_mixedMultiplex.html b/dom/file/ipc/tests/test_ipcBlob_mixedMultiplex.html
new file mode 100644
index 0000000000..c8e046fa88
--- /dev/null
+++ b/dom/file/ipc/tests/test_ipcBlob_mixedMultiplex.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test an empty IPCBlob together with other parts</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="text/javascript">
+
+let url = SimpleTest.getTestFileURL("script_file.js");
+let data = new Array(1024*1024).join('A');
+
+let script = SpecialPowers.loadChromeScript(url);
+script.addMessageListener("file.opened", message => {
+ let blob = new Blob([data]);
+
+ let form = new FormData();
+ form.append("blob1", blob);
+ form.append("blob2", message.file);
+ form.append("blob3", blob);
+
+ fetch("ok.sjs", {
+ method: "POST",
+ body: form,
+ })
+ .then(r => r.text())
+ .then(r => {
+ ok(parseInt(r, 10) > (data.length * 2), "We have data");
+ })
+ . then(SimpleTest.finish);
+});
+
+script.sendAsyncMessage("file.open");
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/file/ipc/tests/test_ipcBlob_workers.html b/dom/file/ipc/tests/test_ipcBlob_workers.html
new file mode 100644
index 0000000000..a473948f25
--- /dev/null
+++ b/dom/file/ipc/tests/test_ipcBlob_workers.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test IPCBlob and Workers</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="text/javascript">
+
+function test_workerOwner() {
+ info("test_workerOwner");
+
+ function workerScript() {
+ onmessage = e => {
+ e.ports[0].onmessage = event => {
+ let reader = new FileReader();
+ reader.readAsText(event.data);
+ reader.onloadend = () => {
+ let status = reader.result == 'hello world';
+ postMessage(status);
+ }
+ }
+ }
+ }
+
+ let mc = new MessageChannel();
+ mc.port1.postMessage(new Blob(['hello world']));
+
+ let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"]));
+ let worker = new Worker(workerUrl);
+
+ worker.postMessage("", [mc.port2]);
+ worker.onmessage = event => {
+ ok(event.data, "All is done!");
+ next();
+ }
+}
+
+function test_workerToMainThread() {
+ info("test_workerToMainThread");
+ function workerScript() {
+ onmessage = e => {
+ e.ports[0].onmessage = event => {
+ postMessage(event.data);
+ }
+ }
+ }
+
+ let mc = new MessageChannel();
+ mc.port1.postMessage(new Blob(['hello world']));
+
+ let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"]));
+ let worker = new Worker(workerUrl);
+
+ worker.postMessage("", [mc.port2]);
+ worker.onmessage = event => {
+ info("Blob received back, terminate the worker and force GC");
+ worker.terminate();
+ worker = null;
+ SpecialPowers.forceGC();
+
+ var fr = new FileReader();
+ fr.readAsText(event.data);
+ fr.onloadend = () => {
+ is(fr.result, "hello world", "Data matches");
+ next();
+ }
+ }
+}
+
+function test_workerOwnerPlusFileReaderSync() {
+ info("test_workerOwnerPlusFileReaderSync");
+
+ function workerScript() {
+ onmessage = e => {
+ e.ports[0].onmessage = event => {
+ let reader = new FileReaderSync();
+ let status = reader.readAsText(event.data) == 'hello world';
+ postMessage(status);
+ }
+ }
+ }
+
+ let mc = new MessageChannel();
+ mc.port1.postMessage(new Blob(['hello world']));
+
+ let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"]));
+ let worker = new Worker(workerUrl);
+
+ worker.postMessage("", [mc.port2]);
+ worker.onmessage = event => {
+ ok(event.data, "All is done!");
+ next();
+ }
+}
+
+var tests = [
+ test_workerOwner,
+ test_workerToMainThread,
+ test_workerOwnerPlusFileReaderSync,
+];
+
+function next() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+SimpleTest.waitForExplicitFinish();
+next();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/file/moz.build b/dom/file/moz.build
new file mode 100644
index 0000000000..2a5831454b
--- /dev/null
+++ b/dom/file/moz.build
@@ -0,0 +1,62 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: File")
+
+DIRS += ["ipc", "uri"]
+
+EXPORTS.mozilla.dom += [
+ "BaseBlobImpl.h",
+ "Blob.h",
+ "BlobImpl.h",
+ "BlobSet.h",
+ "EmptyBlobImpl.h",
+ "File.h",
+ "FileBlobImpl.h",
+ "FileCreatorHelper.h",
+ "FileList.h",
+ "FileReader.h",
+ "FileReaderSync.h",
+ "MemoryBlobImpl.h",
+ "MultipartBlobImpl.h",
+ "MutableBlobStorage.h",
+ "MutableBlobStreamListener.h",
+ "StreamBlobImpl.h",
+]
+
+UNIFIED_SOURCES += [
+ "BaseBlobImpl.cpp",
+ "Blob.cpp",
+ "BlobImpl.cpp",
+ "BlobSet.cpp",
+ "EmptyBlobImpl.cpp",
+ "File.cpp",
+ "FileBlobImpl.cpp",
+ "FileCreatorHelper.cpp",
+ "FileList.cpp",
+ "FileReader.cpp",
+ "FileReaderSync.cpp",
+ "MemoryBlobImpl.cpp",
+ "MultipartBlobImpl.cpp",
+ "MutableBlobStorage.cpp",
+ "MutableBlobStreamListener.cpp",
+ "StreamBlobImpl.cpp",
+ "StringBlobImpl.cpp",
+ "TemporaryFileBlobImpl.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/file/ipc",
+]
+
+MOCHITEST_MANIFESTS += ["tests/mochitest.ini"]
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell.ini"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/file/tests/common_blob.js b/dom/file/tests/common_blob.js
new file mode 100644
index 0000000000..261909af0d
--- /dev/null
+++ b/dom/file/tests/common_blob.js
@@ -0,0 +1,395 @@
+const RANGE_1 = 1;
+const RANGE_2 = 2;
+
+function testBlob(file, contents, testName) {
+ // Load file using FileReader
+ return (
+ new Promise(resolve => {
+ let r = new FileReader();
+ r.onload = event => {
+ is(
+ event.target.readyState,
+ FileReader.DONE,
+ "[FileReader] readyState in test FileReader.readAsBinaryString of " +
+ testName
+ );
+ is(
+ event.target.error,
+ null,
+ "[FileReader] no error in test FileReader.readAsBinaryString of " +
+ testName
+ );
+ // Do not use |is(event.target.result, contents, "...");| that may output raw binary data.
+ is(
+ event.target.result.length,
+ contents.length,
+ "[FileReader] Length of result in test FileReader.readAsBinaryString of " +
+ testName
+ );
+ ok(
+ event.target.result == contents,
+ "[FileReader] Content of result in test FileReader.readAsBinaryString of " +
+ testName
+ );
+ is(
+ event.lengthComputable,
+ true,
+ "[FileReader] lengthComputable in test FileReader.readAsBinaryString of " +
+ testName
+ );
+ is(
+ event.loaded,
+ contents.length,
+ "[FileReader] Loaded length in test FileReader.readAsBinaryString of " +
+ testName
+ );
+ is(
+ event.total,
+ contents.length,
+ "[FileReader] Total length in test FileReader.readAsBinaryString of " +
+ testName
+ );
+ resolve();
+ };
+ r.readAsBinaryString(file);
+ })
+
+ // Load file using URL.createObjectURL and XMLHttpRequest
+ .then(() => {
+ return new Promise(resolve => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", URL.createObjectURL(file));
+ xhr.onload = event => {
+ XHRLoadHandler(
+ event,
+ resolve,
+ contents,
+ "XMLHttpRequest load of " + testName
+ );
+ };
+ xhr.overrideMimeType("text/plain; charset=x-user-defined");
+ xhr.send();
+ });
+ })
+
+ // Send file to server using FormData and XMLHttpRequest
+ .then(() => {
+ return new Promise(resolve => {
+ let xhr = new XMLHttpRequest();
+ xhr.onload = function (event) {
+ checkMPSubmission(JSON.parse(event.target.responseText), [
+ { name: "hello", value: "world" },
+ {
+ name: "myfile",
+ value: contents,
+ fileName: file.name || "blob",
+ contentType: file.type || "application/octet-stream",
+ },
+ ]);
+ resolve();
+ };
+ xhr.open("POST", "../../../dom/html/test/form_submit_server.sjs");
+
+ let fd = new FormData();
+ fd.append("hello", "world");
+ fd.append("myfile", file);
+
+ xhr.send(fd);
+ });
+ })
+
+ // Send file to server using plain XMLHttpRequest
+ .then(() => {
+ return new Promise(resolve => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("POST", "../../../dom/xhr/tests/file_XHRSendData.sjs");
+
+ xhr.onload = function (event) {
+ is(
+ event.target.getResponseHeader("Result-Content-Type"),
+ file.type ? file.type : null,
+ "request content-type in XMLHttpRequest send of " + testName
+ );
+ is(
+ event.target.getResponseHeader("Result-Content-Length"),
+ String(file.size),
+ "request content-length in XMLHttpRequest send of " + testName
+ );
+ };
+
+ xhr.addEventListener("load", event => {
+ XHRLoadHandler(
+ event,
+ resolve,
+ contents,
+ "XMLHttpRequest send of " + testName
+ );
+ });
+ xhr.overrideMimeType("text/plain; charset=x-user-defined");
+ xhr.send(file);
+ });
+ })
+ );
+}
+
+function testSlice(file, size, type, contents, fileType, range) {
+ is(file.type, type, fileType + " file is correct type");
+ is(file.size, size, fileType + " file is correct size");
+ if (fileType == "fileFile") {
+ ok(file instanceof File, fileType + " file is a File");
+ } else if (fileType == "memFile") {
+ ok(!(file instanceof File), fileType + " file is not a File");
+ }
+ ok(file instanceof Blob, fileType + " file is also a Blob");
+
+ let slice = file.slice(0, size);
+ ok(slice instanceof Blob, fileType + " fullsize slice is a Blob");
+ ok(!(slice instanceof File), fileType + " fullsize slice is not a File");
+
+ slice = file.slice(0, 1234);
+ ok(slice instanceof Blob, fileType + " sized slice is a Blob");
+ ok(!(slice instanceof File), fileType + " sized slice is not a File");
+
+ slice = file.slice(0, size, "foo/bar");
+ is(slice.type, "foo/bar", fileType + " fullsize slice foo/bar type");
+
+ slice = file.slice(0, 5432, "foo/bar");
+ is(slice.type, "foo/bar", fileType + " sized slice foo/bar type");
+
+ is(slice.slice(0, 10).type, "", fileType + " slice-slice type");
+ is(slice.slice(0, 10).size, 10, fileType + " slice-slice size");
+ is(
+ slice.slice(0, 10, "hello/world").type,
+ "hello/world",
+ fileType + " slice-slice hello/world type"
+ );
+ is(
+ slice.slice(0, 10, "hello/world").size,
+ 10,
+ fileType + " slice-slice hello/world size"
+ );
+
+ // Start, end, expected size
+ var indexes_range_1 = [
+ [0, size, size],
+ [0, 1234, 1234],
+ [size - 500, size, 500],
+ [size - 500, size + 500, 500],
+ [size + 500, size + 1500, 0],
+ [0, 0, 0],
+ [1000, 1000, 0],
+ [size, size, 0],
+ [undefined, undefined, size],
+ [0, undefined, size],
+ ];
+
+ var indexes_range_2 = [
+ [100, undefined, size - 100],
+ [-100, undefined, 100],
+ [100, -100, size - 200],
+ [-size - 100, undefined, size],
+ [-2 * size - 100, 500, 500],
+ [0, -size - 100, 0],
+ [100, -size - 100, 0],
+ [50, -size + 100, 50],
+ [0, 33000, 33000],
+ [1000, 34000, 33000],
+ ];
+
+ let indexes;
+ if (range == RANGE_1) {
+ indexes = indexes_range_1;
+ } else if (range == RANGE_2) {
+ indexes = indexes_range_2;
+ } else {
+ throw "Invalid range!";
+ }
+
+ function runNextTest() {
+ if (!indexes.length) {
+ return Promise.resolve(true);
+ }
+
+ let index = indexes.shift();
+
+ let sliceContents;
+ let testName;
+ if (index[0] == undefined) {
+ slice = file.slice();
+ sliceContents = contents.slice();
+ testName = fileType + " slice()";
+ } else if (index[1] == undefined) {
+ slice = file.slice(index[0]);
+ sliceContents = contents.slice(index[0]);
+ testName = fileType + " slice(" + index[0] + ")";
+ } else {
+ slice = file.slice(index[0], index[1]);
+ sliceContents = contents.slice(index[0], index[1]);
+ testName = fileType + " slice(" + index[0] + ", " + index[1] + ")";
+ }
+
+ is(slice.type, "", testName + " type");
+ is(slice.size, index[2], testName + " size");
+ is(sliceContents.length, index[2], testName + " data size");
+
+ return testBlob(slice, sliceContents, testName).then(runNextTest);
+ }
+
+ return runNextTest()
+ .then(() => {
+ // Slice of slice
+ let sliceOfSlice = file.slice(0, 40000);
+ return testBlob(
+ sliceOfSlice.slice(5000, 42000),
+ contents.slice(5000, 40000),
+ "file slice slice"
+ );
+ })
+ .then(() => {
+ // ...of slice of slice
+ let sliceOfSlice = file
+ .slice(0, 40000)
+ .slice(5000, 42000)
+ .slice(400, 700);
+ SpecialPowers.gc();
+ return testBlob(
+ sliceOfSlice,
+ contents.slice(5400, 5700),
+ "file slice slice slice"
+ );
+ });
+}
+
+function convertXHRBinary(s) {
+ let res = "";
+ for (let i = 0; i < s.length; ++i) {
+ res += String.fromCharCode(s.charCodeAt(i) & 255);
+ }
+ return res;
+}
+
+function XHRLoadHandler(event, resolve, contents, testName) {
+ is(event.target.readyState, 4, "[XHR] readyState in test " + testName);
+ is(event.target.status, 200, "[XHR] no error in test " + testName);
+ // Do not use |is(convertXHRBinary(event.target.responseText), contents, "...");| that may output raw binary data.
+ let convertedData = convertXHRBinary(event.target.responseText);
+ is(
+ convertedData.length,
+ contents.length,
+ "[XHR] Length of result in test " + testName
+ );
+ ok(convertedData == contents, "[XHR] Content of result in test " + testName);
+ is(
+ event.lengthComputable,
+ event.total != 0,
+ "[XHR] lengthComputable in test " + testName
+ );
+ is(event.loaded, contents.length, "[XHR] Loaded length in test " + testName);
+ is(event.total, contents.length, "[XHR] Total length in test " + testName);
+ resolve();
+}
+
+function checkMPSubmission(sub, expected) {
+ function getPropCount(o) {
+ let x,
+ l = 0;
+ for (x in o) {
+ ++l;
+ }
+ return l;
+ }
+
+ is(sub.length, expected.length, "Correct number of items");
+ let i;
+ for (i = 0; i < expected.length; ++i) {
+ if (!("fileName" in expected[i])) {
+ is(
+ sub[i].headers["Content-Disposition"],
+ 'form-data; name="' + expected[i].name + '"',
+ "Correct name (A)"
+ );
+ is(getPropCount(sub[i].headers), 1, "Wrong number of headers (A)");
+ } else {
+ is(
+ sub[i].headers["Content-Disposition"],
+ 'form-data; name="' +
+ expected[i].name +
+ '"; filename="' +
+ expected[i].fileName +
+ '"',
+ "Correct name (B)"
+ );
+ is(
+ sub[i].headers["Content-Type"],
+ expected[i].contentType,
+ "Correct content type (B)"
+ );
+ is(getPropCount(sub[i].headers), 2, "Wrong number of headers (B)");
+ }
+ // Do not use |is(sub[i].body, expected[i].value, "...");| that may output raw binary data.
+ is(sub[i].body.length, expected[i].value.length, "Length of correct value");
+ ok(sub[i].body == expected[i].value, "Content of correct value");
+ }
+}
+
+function createCanvasURL() {
+ return new Promise(resolve => {
+ // Create a decent-sized image
+ let cx = $("canvas").getContext("2d");
+ let s = cx.canvas.width;
+ let grad = cx.createLinearGradient(0, 0, s - 1, s - 1);
+ for (i = 0; i < 0.95; i += 0.1) {
+ grad.addColorStop(i, "white");
+ grad.addColorStop(i + 0.05, "black");
+ }
+ grad.addColorStop(1, "white");
+ cx.fillStyle = grad;
+ cx.fillRect(0, 0, s - 1, s - 1);
+ cx.fillStyle = "rgba(200, 0, 0, 0.9)";
+ cx.fillRect(0.1 * s, 0.1 * s, 0.7 * s, 0.7 * s);
+ cx.strokeStyle = "rgba(0, 0, 130, 0.5)";
+ cx.lineWidth = 0.14 * s;
+ cx.beginPath();
+ cx.arc(0.6 * s, 0.6 * s, 0.3 * s, 0, Math.PI * 2, true);
+ cx.stroke();
+ cx.closePath();
+ cx.fillStyle = "rgb(0, 255, 0)";
+ cx.beginPath();
+ cx.arc(0.1 * s, 0.8 * s, 0.1 * s, 0, Math.PI * 2, true);
+ cx.fill();
+ cx.closePath();
+
+ let data = atob(
+ cx.canvas
+ .toDataURL("image/png")
+ .substring("data:text/png;base64,".length + 1)
+ );
+
+ // This might fail if we dramatically improve the png encoder. If that happens
+ // please increase the complexity or size of the image generated above to ensure
+ // that we're testing with files that are large enough.
+ ok(data.length > 65536, "test data sufficiently large");
+
+ resolve(data);
+ });
+}
+
+function createFile(data, name) {
+ return new Promise(resolve => {
+ SpecialPowers.createFiles([{ name, data }], files => {
+ resolve(files[0]);
+ });
+ });
+}
+
+function toBlobPromise(canvas) {
+ function BlobListener(callback, file) {
+ var reader = new FileReader();
+ reader.onload = () => callback(file);
+ reader.readAsDataURL(file);
+ }
+
+ return new Promise(resolve => {
+ canvas.toBlob(BlobListener.bind(undefined, resolve));
+ });
+}
diff --git a/dom/file/tests/common_blob_reading.js b/dom/file/tests/common_blob_reading.js
new file mode 100644
index 0000000000..5df8419a30
--- /dev/null
+++ b/dom/file/tests/common_blob_reading.js
@@ -0,0 +1,50 @@
+async function testBlobText(blob, content) {
+ let text = await blob.text();
+ is(text, content, "blob.text()");
+}
+
+async function testBlobArrayBuffer(blob, content) {
+ let ab = await blob.arrayBuffer();
+ is(ab.byteLength, content.length, "blob.arrayBuffer()");
+}
+
+async function testBlobStream(blob, content) {
+ let s = await blob.stream();
+ ok(s instanceof ReadableStream, "We have a ReadableStream");
+
+ let data = await s.getReader().read();
+ ok(!data.done, "Nothing is done yet");
+ for (let i = 0; i < data.value.length; ++i) {
+ is(String.fromCharCode(data.value[i]), content[i], "blob.stream() - " + i);
+ }
+}
+
+function workify(func, blob, content) {
+ info("Workifying " + func);
+
+ return new Promise((resolve, reject) => {
+ let worker = new Worker("worker_blob_reading.js");
+ worker.postMessage({ func, blob, content });
+ worker.onmessage = function (e) {
+ if (e.data.type == "done") {
+ resolve();
+ return;
+ }
+
+ if (e.data.type == "error") {
+ reject(e.data.message);
+ return;
+ }
+
+ if (e.data.type == "test") {
+ ok(e.data.test, e.data.message);
+ return;
+ }
+
+ if (e.data.type == "info") {
+ info(e.data.message);
+ return;
+ }
+ };
+ });
+}
diff --git a/dom/file/tests/common_blob_types.js b/dom/file/tests/common_blob_types.js
new file mode 100644
index 0000000000..95501e58e5
--- /dev/null
+++ b/dom/file/tests/common_blob_types.js
@@ -0,0 +1,82 @@
+let blobTypes = [
+ {
+ type: "memory",
+ factory: async content => {
+ return new Blob([content]);
+ },
+ blobImplType: "MultipartBlobImpl[StringBlobImpl]",
+ },
+
+ {
+ type: "ipcBlob",
+ factory: async content => {
+ return new Promise(resolve => {
+ let bc1 = new BroadcastChannel("blob tests");
+ bc1.onmessage = e => {
+ resolve(e.data);
+ };
+
+ let bc2 = new BroadcastChannel("blob tests");
+ bc2.postMessage(new Blob([content]));
+ });
+ },
+ blobImplType:
+ "StreamBlobImpl[StreamBlobImpl[MultipartBlobImpl[StringBlobImpl]]]",
+ },
+
+ {
+ type: "memoryBlob",
+ factory: async content => {
+ return new Promise(resolve => {
+ var xhr = new XMLHttpRequest();
+ xhr.open(
+ "POST",
+ "http://mochi.test:8888/browser/dom/xhr/tests/temporaryFileBlob.sjs"
+ );
+ xhr.responseType = "blob";
+ xhr.send(content);
+ xhr.onloadend = _ => {
+ resolve(xhr.response);
+ };
+ });
+ },
+ blobImplType: "MemoryBlobImpl",
+ },
+
+ {
+ type: "temporaryBlob",
+ factory: async content => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.blob.memoryToTemporaryFile", 1]],
+ });
+
+ return new Promise(resolve => {
+ var xhr = new XMLHttpRequest();
+ xhr.open(
+ "POST",
+ "http://mochi.test:8888/browser/dom/xhr/tests/temporaryFileBlob.sjs"
+ );
+ xhr.responseType = "blob";
+ xhr.send(content);
+ xhr.onloadend = _ => {
+ resolve(xhr.response);
+ };
+ });
+ },
+ blobImplType: "StreamBlobImpl[TemporaryFileBlobImpl]",
+ },
+];
+
+async function forEachBlobType(content, cb) {
+ for (let i = 0; i < blobTypes.length; ++i) {
+ info("Running tests for " + blobTypes[i].type);
+ let blob = await blobTypes[i].factory(content);
+ is(
+ SpecialPowers.wrap(blob).blobImplType,
+ blobTypes[i].blobImplType,
+ "Correct blobImplType"
+ );
+ ok(blob instanceof Blob, "Blob created");
+ await cb(blob, content);
+ }
+}
diff --git a/dom/file/tests/common_fileReader.js b/dom/file/tests/common_fileReader.js
new file mode 100644
index 0000000000..9ee6a32b93
--- /dev/null
+++ b/dom/file/tests/common_fileReader.js
@@ -0,0 +1,848 @@
+function test_setup() {
+ return new Promise(resolve => {
+ const minFileSize = 20000;
+
+ // Create strings containing data we'll test with. We'll want long
+ // strings to ensure they span multiple buffers while loading
+ let testTextData = "asd b\tlah\u1234w\u00a0r";
+ while (testTextData.length < minFileSize) {
+ testTextData = testTextData + testTextData;
+ }
+
+ let testASCIIData = "abcdef 123456\n";
+ while (testASCIIData.length < minFileSize) {
+ testASCIIData = testASCIIData + testASCIIData;
+ }
+
+ let testBinaryData = "";
+ for (let i = 0; i < 256; i++) {
+ testBinaryData += String.fromCharCode(i);
+ }
+ while (testBinaryData.length < minFileSize) {
+ testBinaryData = testBinaryData + testBinaryData;
+ }
+
+ let dataurldata0 = testBinaryData.substr(
+ 0,
+ testBinaryData.length - (testBinaryData.length % 3)
+ );
+ let dataurldata1 = testBinaryData.substr(
+ 0,
+ testBinaryData.length - 2 - (testBinaryData.length % 3)
+ );
+ let dataurldata2 = testBinaryData.substr(
+ 0,
+ testBinaryData.length - 1 - (testBinaryData.length % 3)
+ );
+
+ //Set up files for testing
+ let openerURL = SimpleTest.getTestFileURL("fileapi_chromeScript.js");
+ let opener = SpecialPowers.loadChromeScript(openerURL);
+
+ opener.addMessageListener("files.opened", message => {
+ let [
+ asciiFile,
+ binaryFile,
+ nonExistingFile,
+ utf8TextFile,
+ utf16TextFile,
+ emptyFile,
+ dataUrlFile0,
+ dataUrlFile1,
+ dataUrlFile2,
+ ] = message;
+
+ resolve({
+ blobs: {
+ asciiFile,
+ binaryFile,
+ nonExistingFile,
+ utf8TextFile,
+ utf16TextFile,
+ emptyFile,
+ dataUrlFile0,
+ dataUrlFile1,
+ dataUrlFile2,
+ },
+ data: {
+ text: testTextData,
+ ascii: testASCIIData,
+ binary: testBinaryData,
+ url0: dataurldata0,
+ url1: dataurldata1,
+ url2: dataurldata2,
+ },
+ });
+ });
+
+ opener.sendAsyncMessage("files.open", [
+ testASCIIData,
+ testBinaryData,
+ null,
+ convertToUTF8(testTextData),
+ convertToUTF16(testTextData),
+ "",
+ dataurldata0,
+ dataurldata1,
+ dataurldata2,
+ ]);
+ });
+}
+
+function runBasicTests(data) {
+ return test_basic()
+ .then(() => {
+ return test_readAsText(data.blobs.asciiFile, data.data.ascii);
+ })
+ .then(() => {
+ return test_readAsBinaryString(data.blobs.binaryFile, data.data.binary);
+ })
+ .then(() => {
+ return test_readAsArrayBuffer(data.blobs.binaryFile, data.data.binary);
+ });
+}
+
+function runEncodingTests(data) {
+ return test_readAsTextWithEncoding(
+ data.blobs.asciiFile,
+ data.data.ascii,
+ data.data.ascii.length,
+ ""
+ )
+ .then(() => {
+ return test_readAsTextWithEncoding(
+ data.blobs.asciiFile,
+ data.data.ascii,
+ data.data.ascii.length,
+ "iso8859-1"
+ );
+ })
+ .then(() => {
+ return test_readAsTextWithEncoding(
+ data.blobs.utf8TextFile,
+ data.data.text,
+ convertToUTF8(data.data.text).length,
+ "utf8"
+ );
+ })
+ .then(() => {
+ return test_readAsTextWithEncoding(
+ data.blobs.utf16TextFile,
+ data.data.text,
+ convertToUTF16(data.data.text).length,
+ "utf-16"
+ );
+ })
+ .then(() => {
+ return test_readAsTextWithEncoding(data.blobs.emptyFile, "", 0, "");
+ })
+ .then(() => {
+ return test_readAsTextWithEncoding(data.blobs.emptyFile, "", 0, "utf8");
+ })
+ .then(() => {
+ return test_readAsTextWithEncoding(data.blobs.emptyFile, "", 0, "utf-16");
+ });
+}
+
+function runEmptyTests(data) {
+ return test_onlyResult()
+ .then(() => {
+ return test_readAsText(data.blobs.emptyFile, "");
+ })
+ .then(() => {
+ return test_readAsBinaryString(data.blobs.emptyFile, "");
+ })
+ .then(() => {
+ return test_readAsArrayBuffer(data.blobs.emptyFile, "");
+ })
+ .then(() => {
+ return test_readAsDataURL(data.blobs.emptyFile, convertToDataURL(""), 0);
+ });
+}
+
+function runTwiceTests(data) {
+ return test_readAsTextTwice(data.blobs.asciiFile, data.data.ascii)
+ .then(() => {
+ return test_readAsBinaryStringTwice(
+ data.blobs.binaryFile,
+ data.data.binary
+ );
+ })
+ .then(() => {
+ return test_readAsDataURLTwice(
+ data.blobs.binaryFile,
+ convertToDataURL(data.data.binary),
+ data.data.binary.length
+ );
+ })
+ .then(() => {
+ return test_readAsArrayBufferTwice(
+ data.blobs.binaryFile,
+ data.data.binary
+ );
+ })
+ .then(() => {
+ return test_readAsArrayBufferTwice2(
+ data.blobs.binaryFile,
+ data.data.binary
+ );
+ });
+}
+
+function runOtherTests(data) {
+ return test_readAsDataURL_customLength(
+ data.blobs.dataUrlFile0,
+ convertToDataURL(data.data.url0),
+ data.data.url0.length,
+ 0
+ )
+ .then(() => {
+ return test_readAsDataURL_customLength(
+ data.blobs.dataUrlFile1,
+ convertToDataURL(data.data.url1),
+ data.data.url1.length,
+ 1
+ );
+ })
+ .then(() => {
+ return test_readAsDataURL_customLength(
+ data.blobs.dataUrlFile2,
+ convertToDataURL(data.data.url2),
+ data.data.url2.length,
+ 2
+ );
+ })
+ .then(() => {
+ return test_abort(data.blobs.asciiFile);
+ })
+ .then(() => {
+ return test_abort_readAsX(data.blobs.asciiFile, data.data.ascii);
+ })
+ .then(() => {
+ return test_nonExisting(data.blobs.nonExistingFile);
+ });
+}
+
+function convertToUTF16(s) {
+ let res = "";
+ for (let i = 0; i < s.length; ++i) {
+ c = s.charCodeAt(i);
+ res += String.fromCharCode(c & 255, c >>> 8);
+ }
+ return res;
+}
+
+function convertToUTF8(s) {
+ return unescape(encodeURIComponent(s));
+}
+
+function convertToDataURL(s) {
+ return "data:application/octet-stream;base64," + btoa(s);
+}
+
+function loadEventHandler_string(
+ event,
+ resolve,
+ reader,
+ data,
+ dataLength,
+ testName
+) {
+ is(event.target, reader, "Correct target.");
+ is(
+ event.target.readyState,
+ FileReader.DONE,
+ "readyState in test " + testName
+ );
+ is(event.target.error, null, "no error in test " + testName);
+ is(event.target.result, data, "result in test " + testName);
+ is(event.lengthComputable, true, "lengthComputable in test " + testName);
+ is(event.loaded, dataLength, "loaded in test " + testName);
+ is(event.total, dataLength, "total in test " + testName);
+ resolve();
+}
+
+function loadEventHandler_arrayBuffer(event, resolve, reader, data, testName) {
+ is(
+ event.target.readyState,
+ FileReader.DONE,
+ "readyState in test " + testName
+ );
+ is(event.target.error, null, "no error in test " + testName);
+ is(event.lengthComputable, true, "lengthComputable in test " + testName);
+ is(event.loaded, data.length, "loaded in test " + testName);
+ is(event.total, data.length, "total in test " + testName);
+ is(
+ event.target.result.byteLength,
+ data.length,
+ "array buffer size in test " + testName
+ );
+
+ let u8v = new Uint8Array(event.target.result);
+ is(
+ String.fromCharCode.apply(String, u8v),
+ data,
+ "array buffer contents in test " + testName
+ );
+ u8v = null;
+
+ if ("SpecialPowers" in self) {
+ SpecialPowers.gc();
+
+ is(
+ event.target.result.byteLength,
+ data.length,
+ "array buffer size after gc in test " + testName
+ );
+ u8v = new Uint8Array(event.target.result);
+ is(
+ String.fromCharCode.apply(String, u8v),
+ data,
+ "array buffer contents after gc in test " + testName
+ );
+ }
+
+ resolve();
+}
+
+function test_basic() {
+ return new Promise(resolve => {
+ is(FileReader.EMPTY, 0, "correct EMPTY value");
+ is(FileReader.LOADING, 1, "correct LOADING value");
+ is(FileReader.DONE, 2, "correct DONE value");
+ resolve();
+ });
+}
+
+function test_readAsText(blob, text) {
+ return new Promise(resolve => {
+ let onloadHasRun = false;
+ let onloadStartHasRun = false;
+
+ let r = new FileReader();
+ is(r.readyState, FileReader.EMPTY, "correct initial text readyState");
+
+ r.onload = event => {
+ loadEventHandler_string(
+ event,
+ resolve,
+ r,
+ text,
+ text.length,
+ "readAsText"
+ );
+ };
+
+ r.addEventListener("load", () => {
+ onloadHasRun = true;
+ });
+ r.addEventListener("loadstart", () => {
+ onloadStartHasRun = true;
+ });
+
+ r.readAsText(blob);
+
+ is(r.readyState, FileReader.LOADING, "correct loading text readyState");
+ is(onloadHasRun, false, "text loading must be async");
+ is(onloadStartHasRun, false, "text loadstart should fire async");
+ });
+}
+
+function test_readAsBinaryString(blob, text) {
+ return new Promise(resolve => {
+ let onloadHasRun = false;
+ let onloadStartHasRun = false;
+
+ let r = new FileReader();
+ is(r.readyState, FileReader.EMPTY, "correct initial binary readyState");
+
+ r.addEventListener("load", function () {
+ onloadHasRun = true;
+ });
+ r.addEventListener("loadstart", function () {
+ onloadStartHasRun = true;
+ });
+
+ r.readAsBinaryString(blob);
+
+ r.onload = event => {
+ loadEventHandler_string(
+ event,
+ resolve,
+ r,
+ text,
+ text.length,
+ "readAsBinaryString"
+ );
+ };
+
+ is(r.readyState, FileReader.LOADING, "correct loading binary readyState");
+ is(onloadHasRun, false, "binary loading must be async");
+ is(onloadStartHasRun, false, "binary loadstart should fire async");
+ });
+}
+
+function test_readAsArrayBuffer(blob, text) {
+ return new Promise(resolve => {
+ let onloadHasRun = false;
+ let onloadStartHasRun = false;
+
+ r = new FileReader();
+ is(
+ r.readyState,
+ FileReader.EMPTY,
+ "correct initial arrayBuffer readyState"
+ );
+
+ r.addEventListener("load", function () {
+ onloadHasRun = true;
+ });
+ r.addEventListener("loadstart", function () {
+ onloadStartHasRun = true;
+ });
+
+ r.readAsArrayBuffer(blob);
+
+ r.onload = event => {
+ loadEventHandler_arrayBuffer(
+ event,
+ resolve,
+ r,
+ text,
+ "readAsArrayBuffer"
+ );
+ };
+
+ is(
+ r.readyState,
+ FileReader.LOADING,
+ "correct loading arrayBuffer readyState"
+ );
+ is(onloadHasRun, false, "arrayBuffer loading must be async");
+ is(onloadStartHasRun, false, "arrayBuffer loadstart should fire sync");
+ });
+}
+
+// Test a variety of encodings, and make sure they work properly
+function test_readAsTextWithEncoding(blob, text, length, charset) {
+ return new Promise(resolve => {
+ let r = new FileReader();
+ r.onload = event => {
+ loadEventHandler_string(
+ event,
+ resolve,
+ r,
+ text,
+ length,
+ "readAsText-" + charset
+ );
+ };
+ r.readAsText(blob, charset);
+ });
+}
+
+// Test get result without reading
+function test_onlyResult() {
+ return new Promise(resolve => {
+ let r = new FileReader();
+ is(
+ r.readyState,
+ FileReader.EMPTY,
+ "readyState in test reader get result without reading"
+ );
+ is(r.error, null, "no error in test reader get result without reading");
+ is(r.result, null, "result in test reader get result without reading");
+ resolve();
+ });
+}
+
+function test_readAsDataURL(blob, text, length) {
+ return new Promise(resolve => {
+ let r = new FileReader();
+ r.onload = event => {
+ loadEventHandler_string(event, resolve, r, text, length, "readAsDataURL");
+ };
+ r.readAsDataURL(blob);
+ });
+}
+
+// Test reusing a FileReader to read multiple times
+function test_readAsTextTwice(blob, text) {
+ return new Promise(resolve => {
+ let r = new FileReader();
+ r.onload = event => {
+ loadEventHandler_string(
+ event,
+ () => {},
+ r,
+ text,
+ text.length,
+ "readAsText-reused-once"
+ );
+ };
+
+ let anotherListener = event => {
+ let r1 = event.target;
+ r1.removeEventListener("load", anotherListener);
+ r1.onload = evt => {
+ loadEventHandler_string(
+ evt,
+ resolve,
+ r1,
+ text,
+ text.length,
+ "readAsText-reused-twice"
+ );
+ };
+ r1.readAsText(blob);
+ };
+
+ r.addEventListener("load", anotherListener);
+ r.readAsText(blob);
+ });
+}
+
+// Test reusing a FileReader to read multiple times
+function test_readAsBinaryStringTwice(blob, text) {
+ return new Promise(resolve => {
+ let r = new FileReader();
+ r.onload = event => {
+ loadEventHandler_string(
+ event,
+ () => {},
+ r,
+ text,
+ text.length,
+ "readAsBinaryString-reused-once"
+ );
+ };
+
+ let anotherListener = event => {
+ let r1 = event.target;
+ r1.removeEventListener("load", anotherListener);
+ r1.onload = evt => {
+ loadEventHandler_string(
+ evt,
+ resolve,
+ r1,
+ text,
+ text.length,
+ "readAsBinaryString-reused-twice"
+ );
+ };
+ r1.readAsBinaryString(blob);
+ };
+
+ r.addEventListener("load", anotherListener);
+ r.readAsBinaryString(blob);
+ });
+}
+
+function test_readAsDataURLTwice(blob, text, length) {
+ return new Promise(resolve => {
+ let r = new FileReader();
+ r.onload = event => {
+ loadEventHandler_string(
+ event,
+ () => {},
+ r,
+ text,
+ length,
+ "readAsDataURL-reused-once"
+ );
+ };
+
+ let anotherListener = event => {
+ let r1 = event.target;
+ r1.removeEventListener("load", anotherListener);
+ r1.onload = evt => {
+ loadEventHandler_string(
+ evt,
+ resolve,
+ r1,
+ text,
+ length,
+ "readAsDataURL-reused-twice"
+ );
+ };
+ r1.readAsDataURL(blob);
+ };
+
+ r.addEventListener("load", anotherListener);
+ r.readAsDataURL(blob);
+ });
+}
+
+function test_readAsArrayBufferTwice(blob, text) {
+ return new Promise(resolve => {
+ let r = new FileReader();
+ r.onload = event => {
+ loadEventHandler_arrayBuffer(
+ event,
+ () => {},
+ r,
+ text,
+ "readAsArrayBuffer-reused-once"
+ );
+ };
+
+ let anotherListener = event => {
+ let r1 = event.target;
+ r1.removeEventListener("load", anotherListener);
+ r1.onload = evt => {
+ loadEventHandler_arrayBuffer(
+ evt,
+ resolve,
+ r1,
+ text,
+ "readAsArrayBuffer-reused-twice"
+ );
+ };
+ r1.readAsArrayBuffer(blob);
+ };
+
+ r.addEventListener("load", anotherListener);
+ r.readAsArrayBuffer(blob);
+ });
+}
+
+// Test first reading as ArrayBuffer then read as something else (BinaryString)
+// and doesn't crash
+function test_readAsArrayBufferTwice2(blob, text) {
+ return new Promise(resolve => {
+ let r = new FileReader();
+ r.onload = event => {
+ loadEventHandler_arrayBuffer(
+ event,
+ () => {},
+ r,
+ text,
+ "readAsArrayBuffer-reused-once2"
+ );
+ };
+
+ let anotherListener = event => {
+ let r1 = event.target;
+ r1.removeEventListener("load", anotherListener);
+ r1.onload = evt => {
+ loadEventHandler_string(
+ evt,
+ resolve,
+ r1,
+ text,
+ text.length,
+ "readAsArrayBuffer-reused-twice2"
+ );
+ };
+ r1.readAsBinaryString(blob);
+ };
+
+ r.addEventListener("load", anotherListener);
+ r.readAsArrayBuffer(blob);
+ });
+}
+
+function test_readAsDataURL_customLength(blob, text, length, numb) {
+ return new Promise(resolve => {
+ is(length % 3, numb, "Want to test data with length %3 == " + numb);
+ let r = new FileReader();
+ r.onload = event => {
+ loadEventHandler_string(
+ event,
+ resolve,
+ r,
+ text,
+ length,
+ "dataurl reading, %3 = " + numb
+ );
+ };
+ r.readAsDataURL(blob);
+ });
+}
+
+// Test abort()
+function test_abort(blob) {
+ return new Promise(resolve => {
+ let abortHasRun = false;
+ let loadEndHasRun = false;
+
+ let r = new FileReader();
+
+ r.onabort = function (event) {
+ is(abortHasRun, false, "abort should only fire once");
+ is(loadEndHasRun, false, "loadend shouldn't have fired yet");
+ abortHasRun = true;
+ is(
+ event.target.readyState,
+ FileReader.DONE,
+ "should be DONE while firing onabort"
+ );
+ is(
+ event.target.error.name,
+ "AbortError",
+ "error set to AbortError for aborted reads"
+ );
+ is(
+ event.target.result,
+ null,
+ "file data should be null on aborted reads"
+ );
+ };
+
+ r.onloadend = function (event) {
+ is(abortHasRun, true, "abort should fire before loadend");
+ is(loadEndHasRun, false, "loadend should only fire once");
+ loadEndHasRun = true;
+ is(
+ event.target.readyState,
+ FileReader.DONE,
+ "should be DONE while firing onabort"
+ );
+ is(
+ event.target.error.name,
+ "AbortError",
+ "error set to AbortError for aborted reads"
+ );
+ is(
+ event.target.result,
+ null,
+ "file data should be null on aborted reads"
+ );
+ };
+
+ r.onload = function () {
+ ok(false, "load should not fire for aborted reads");
+ };
+ r.onerror = function () {
+ ok(false, "error should not fire for aborted reads");
+ };
+ r.onprogress = function () {
+ ok(false, "progress should not fire for aborted reads");
+ };
+
+ let abortThrew = false;
+ try {
+ r.abort();
+ } catch (e) {
+ abortThrew = true;
+ }
+
+ is(abortThrew, false, "abort() doesn't throw");
+ is(abortHasRun, false, "abort() is a no-op unless loading");
+
+ r.readAsText(blob);
+ r.abort();
+
+ is(abortHasRun, true, "abort should fire sync");
+ is(loadEndHasRun, true, "loadend should fire sync");
+
+ resolve();
+ });
+}
+
+// Test calling readAsX to cause abort()
+function test_abort_readAsX(blob, text) {
+ return new Promise(resolve => {
+ let reuseAbortHasRun = false;
+
+ let r = new FileReader();
+ r.onabort = function (event) {
+ is(reuseAbortHasRun, false, "abort should only fire once");
+ reuseAbortHasRun = true;
+ is(
+ event.target.readyState,
+ FileReader.DONE,
+ "should be DONE while firing onabort"
+ );
+ is(
+ event.target.error.name,
+ "AbortError",
+ "error set to AbortError for aborted reads"
+ );
+ is(
+ event.target.result,
+ null,
+ "file data should be null on aborted reads"
+ );
+ };
+ r.onload = function () {
+ ok(false, "load should fire for nested reads");
+ };
+
+ let abortThrew = false;
+ try {
+ r.abort();
+ } catch (e) {
+ abortThrew = true;
+ }
+
+ is(abortThrew, false, "abort() should not throw");
+ is(reuseAbortHasRun, false, "abort() is a no-op unless loading");
+ r.readAsText(blob);
+
+ let readThrew = false;
+ try {
+ r.readAsText(blob);
+ } catch (e) {
+ readThrew = true;
+ }
+
+ is(readThrew, true, "readAsText() must throw if loading");
+ is(reuseAbortHasRun, false, "abort should not fire");
+
+ r.onload = event => {
+ loadEventHandler_string(
+ event,
+ resolve,
+ r,
+ text,
+ text.length,
+ "reuse-as-abort reading"
+ );
+ };
+ });
+}
+
+// Test reading from nonexistent files
+function test_nonExisting(blob) {
+ return new Promise(resolve => {
+ let r = new FileReader();
+
+ r.onerror = function (event) {
+ is(
+ event.target.readyState,
+ FileReader.DONE,
+ "should be DONE while firing onerror"
+ );
+ is(
+ event.target.error.name,
+ "NotFoundError",
+ "error set to NotFoundError for nonexistent files"
+ );
+ is(
+ event.target.result,
+ null,
+ "file data should be null on aborted reads"
+ );
+ resolve();
+ };
+ r.onload = function (event) {
+ is(false, "nonexistent file shouldn't load! (FIXME: bug 1122788)");
+ };
+
+ let didThrow = false;
+ try {
+ r.readAsDataURL(blob);
+ } catch (ex) {
+ didThrow = true;
+ }
+
+ // Once this test passes, we should test that onerror gets called and
+ // that the FileReader object is in the right state during that call.
+ is(
+ didThrow,
+ false,
+ "shouldn't throw when opening nonexistent file, should fire error instead"
+ );
+ });
+}
diff --git a/dom/file/tests/crashtests/1480354.html b/dom/file/tests/crashtests/1480354.html
new file mode 100644
index 0000000000..19e53bb1ca
--- /dev/null
+++ b/dom/file/tests/crashtests/1480354.html
@@ -0,0 +1,14 @@
+<html>
+<body>
+ <script>
+function createBlob(blocksize) {
+ var blob = new Blob();
+ while (blob.size < 25 * 1024 * 1024) { // 25 MB
+ blob = new Blob([blob, new Uint8Array(blocksize)]);
+ }
+ URL.createObjectURL(blob);
+}
+createBlob(1024 * 25);
+ </script>
+</body>
+</html>
diff --git a/dom/file/tests/crashtests/1562891.html b/dom/file/tests/crashtests/1562891.html
new file mode 100644
index 0000000000..fff7606a8a
--- /dev/null
+++ b/dom/file/tests/crashtests/1562891.html
@@ -0,0 +1,16 @@
+<html>
+<head>
+ <script>
+ function start () {
+ const canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas')
+ SpecialPowers.forceGC();
+ canvas.toBlob(function (blob) {
+ blob.stream()
+ blob.arrayBuffer().then(() => {})
+ })
+ }
+
+ window.addEventListener('load', start)
+ </script>
+</head>
+</html>
diff --git a/dom/file/tests/crashtests/1747185.html b/dom/file/tests/crashtests/1747185.html
new file mode 100644
index 0000000000..89af55504f
--- /dev/null
+++ b/dom/file/tests/crashtests/1747185.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+ <script>
+ window.addEventListener('load', () => {
+ let a = new Blob([new Uint8Array(2147483647)])
+ let b = new File([a], '')
+ b.stream()
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/file/tests/crashtests/1748342.html b/dom/file/tests/crashtests/1748342.html
new file mode 100644
index 0000000000..5f0811b5bb
--- /dev/null
+++ b/dom/file/tests/crashtests/1748342.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script id="worker1" type="javascript/worker">
+ self.onmessage = async function (e) {
+ self.close()
+ const reader = new FileReader()
+ for (let i = 0; i < 25; i++) {
+ try { reader.readAsBinaryString(e.data[0]) } catch (e) {}
+ }
+ reader.addEventListener("progress", () => {}, {})
+ }
+ </script>
+ <script>
+ window.addEventListener("load", () => {
+ const script = new Blob([document.querySelector("#worker1").textContent], { type: "text/javascript" })
+ const worker = new Worker(window.URL.createObjectURL(script))
+ const data = new Blob(["70𯸟\nℽ㮼٠𛪇\0𛃧كe۰҅妽󠶖󠙻𝅧𡴶𝌋쮁偵9󠰾󠋼7\r𐇽0🥂.\b፟+⍳፟D󠢪3𣚽󠜎🐾�c_߰a<<=𝅦9𝆭𛰅9󠵼ௌ󠌌𖓄ΐ0�⡖‑뢈/-᭰*٠٪᷁e �𯯐𯬓𯕏‫‑걢V*=­￿**󠛐\u2028שּׁ&‭0󠄯𒡯e\n𛰵󠥿⁤𫍰,󠶒𝅥t\nl𧶈a𦜠09k𯔆䴋�|󠄷🦻𛝫󠁥𖭄", "*𒗬۰0\u2029‌/\n\r+󠎔𖼣k*=🿹𯀊\r٪\r󠳐𝲍慑B�\r󠮱\r\n\"\r\\𯉆۹c卑4󠣰󠻬鴗ꛌ\0⌕:\r\n𝚨9ꛅ٠󠃭𯋾\nJ󠯲9\r゙鈷P\u2029҉󠷒۹e \b緁︡𤆥^𯏂゚੿|٫󠕉揅ᷛ𩊜s𯑳2凅9c8H𦰤-\f%٠𨮫󠚵‑2𫈮P𝋄窥57\n-゙҄H󠭗𯪞𣃂-ᷢשּׁ貌솽|𝉃‬c㙡𯐉᭯mL\r"], {
+ "type": "image/png",
+ "endings": "transparent"
+ })
+ worker.postMessage([data], [])
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/file/tests/crashtests/crashtests.list b/dom/file/tests/crashtests/crashtests.list
new file mode 100644
index 0000000000..22eec1962e
--- /dev/null
+++ b/dom/file/tests/crashtests/crashtests.list
@@ -0,0 +1,4 @@
+skip-if(ThreadSanitizer) load 1480354.html
+load 1562891.html
+skip-if(Android||ThreadSanitizer) load 1747185.html # Crashes on Android, times out on TSan.
+load 1748342.html
diff --git a/dom/file/tests/create_file_objects.js b/dom/file/tests/create_file_objects.js
new file mode 100644
index 0000000000..cf016b239d
--- /dev/null
+++ b/dom/file/tests/create_file_objects.js
@@ -0,0 +1,19 @@
+/* eslint-env mozilla/chrome-script */
+
+Cu.importGlobalProperties(["File"]);
+
+addMessageListener("create-file-objects", function (message) {
+ let files = [];
+ let promises = [];
+ for (fileName of message.fileNames) {
+ promises.push(
+ File.createFromFileName(fileName).then(function (file) {
+ files.push(file);
+ })
+ );
+ }
+
+ Promise.all(promises).then(function () {
+ sendAsyncMessage("created-file-objects", files);
+ });
+});
diff --git a/dom/file/tests/file_blobURL_expiring.html b/dom/file/tests/file_blobURL_expiring.html
new file mode 100644
index 0000000000..a1ae725709
--- /dev/null
+++ b/dom/file/tests/file_blobURL_expiring.html
@@ -0,0 +1,4 @@
+<script>
+var blob = new Blob([123]);
+parent.postMessage(URL.createObjectURL(blob), "*");
+</script>
diff --git a/dom/file/tests/file_mozfiledataurl_audio.ogg b/dom/file/tests/file_mozfiledataurl_audio.ogg
new file mode 100644
index 0000000000..88b2c1b5b2
--- /dev/null
+++ b/dom/file/tests/file_mozfiledataurl_audio.ogg
Binary files differ
diff --git a/dom/file/tests/file_mozfiledataurl_doc.html b/dom/file/tests/file_mozfiledataurl_doc.html
new file mode 100644
index 0000000000..763b20a0f9
--- /dev/null
+++ b/dom/file/tests/file_mozfiledataurl_doc.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+<body>
+<p>This here is a document!</p>
+<img id=img src="file_mozfiledataurl_img.jpg">
+</html>
diff --git a/dom/file/tests/file_mozfiledataurl_img.jpg b/dom/file/tests/file_mozfiledataurl_img.jpg
new file mode 100644
index 0000000000..dcd99b9670
--- /dev/null
+++ b/dom/file/tests/file_mozfiledataurl_img.jpg
Binary files differ
diff --git a/dom/file/tests/file_mozfiledataurl_inner.html b/dom/file/tests/file_mozfiledataurl_inner.html
new file mode 100644
index 0000000000..a2e539bef7
--- /dev/null
+++ b/dom/file/tests/file_mozfiledataurl_inner.html
@@ -0,0 +1,76 @@
+<!doctype html>
+<html>
+<script type="application/javascript">
+var img;
+var audio;
+var iframe;
+
+addEventListener("message", function(e) {
+ mess = JSON.parse(e.data);
+
+ if ("img" in mess)
+ img.src = mess.img;
+ else if ("audio" in mess)
+ audio.src = mess.audio
+ else if ("iframe" in mess)
+ iframe.src = mess.iframe;
+ else if ("xhr" in mess) {
+ let xhr = new XMLHttpRequest();
+ xhr.onerror = function() {
+ sendItUp({ didError: true });
+ }
+ xhr.onload = function() {
+ sendItUp({ text: xhr.responseText });
+ }
+ try {
+ xhr.open("GET", mess.xhr);
+ xhr.send();
+ }
+ catch (ex) {
+ sendItUp({ didThrow: true });
+ }
+ }
+
+}, false);
+
+function sendItUp(obj) {
+ window.parent.postMessage(JSON.stringify(obj), "*");
+}
+
+function audioNotifyParent(e) {
+ sendItUp({ type: e.type });
+}
+
+function imgNotifyParent(e) {
+ sendItUp({ type: e.type,
+ width: e.target.width,
+ height: e.target.height });
+}
+
+function iframeNotifyParent(e) {
+ res = { type: e.type };
+ try {
+ res.text = e.target.contentDocument.getElementsByTagName("p")[0].textContent;
+ } catch (ex) {}
+ try {
+ res.imgWidth = e.target.contentDocument.getElementById("img").width;
+ } catch (ex) {}
+
+ sendItUp(res);
+}
+
+onload = function() {
+ img = document.getElementById('img');
+ img.onerror = img.onload = imgNotifyParent;
+ iframe = document.getElementById('iframe');
+ iframe.onerror = iframe.onload = iframeNotifyParent;
+ audio = document.getElementById('audio');
+ audio.onerror = audio.onloadeddata = audioNotifyParent;
+}
+
+</script>
+<body>
+<img id=img>
+<audio id=audio>
+<iframe id=iframe></iframe>
+</html>
diff --git a/dom/file/tests/file_mozfiledataurl_text.txt b/dom/file/tests/file_mozfiledataurl_text.txt
new file mode 100644
index 0000000000..315338aa9b
--- /dev/null
+++ b/dom/file/tests/file_mozfiledataurl_text.txt
@@ -0,0 +1 @@
+Yarr, here be plaintext file, ya landlubber
diff --git a/dom/file/tests/file_nonascii_blob_url.html b/dom/file/tests/file_nonascii_blob_url.html
new file mode 100644
index 0000000000..89183f4613
--- /dev/null
+++ b/dom/file/tests/file_nonascii_blob_url.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test blob URL for non-ascii domain</title>
+</head>
+<body>
+ <p id="result"></p>
+ <script type="application/javascript">
+
+window.onmessage = function(e) {
+ var blob = new Blob([e.data]);
+ var url = URL.createObjectURL(blob);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url, false);
+ xhr.send(null);
+
+ parent.postMessage(xhr.responseText, '*');
+}
+
+ </script>
+</body>
+</html>
diff --git a/dom/file/tests/fileapi_chromeScript.js b/dom/file/tests/fileapi_chromeScript.js
new file mode 100644
index 0000000000..f94b01698b
--- /dev/null
+++ b/dom/file/tests/fileapi_chromeScript.js
@@ -0,0 +1,54 @@
+/* eslint-env mozilla/chrome-script */
+
+Cu.importGlobalProperties(["File"]);
+
+function createFileWithData(fileData) {
+ var willDelete = fileData === null;
+
+ var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
+ Ci.nsIProperties
+ );
+
+ var testFile = dirSvc.get("ProfD", Ci.nsIFile);
+ testFile.append("fileAPItestfile");
+ testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ var outStream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ outStream.init(
+ testFile,
+ 0x02 | 0x08 | 0x20, // write, create, truncate
+ 0o666,
+ 0
+ );
+ if (willDelete) {
+ fileData = "some irrelevant test data\n";
+ }
+ outStream.write(fileData, fileData.length);
+ outStream.close();
+ return File.createFromNsIFile(testFile).then(domFile => {
+ if (willDelete) {
+ testFile.remove(/* recursive: */ false);
+ }
+
+ return domFile;
+ });
+}
+
+addMessageListener("files.open", function (message) {
+ let promises = [];
+ let list = [];
+
+ for (let fileData of message) {
+ promises.push(
+ createFileWithData(fileData).then(domFile => {
+ list.push(domFile);
+ })
+ );
+ }
+
+ Promise.all(promises).then(() => {
+ sendAsyncMessage("files.opened", list);
+ });
+});
diff --git a/dom/file/tests/mochitest.ini b/dom/file/tests/mochitest.ini
new file mode 100644
index 0000000000..495c0263d8
--- /dev/null
+++ b/dom/file/tests/mochitest.ini
@@ -0,0 +1,54 @@
+[DEFAULT]
+support-files =
+ common_blob.js
+ create_file_objects.js
+ common_fileReader.js
+ common_blob_types.js
+ file_blobURL_expiring.html
+ file_mozfiledataurl_img.jpg
+ file_mozfiledataurl_audio.ogg
+ file_mozfiledataurl_doc.html
+ file_mozfiledataurl_text.txt
+ file_mozfiledataurl_inner.html
+ file_nonascii_blob_url.html
+ fileapi_chromeScript.js
+ worker_fileReader.js
+ !/dom/html/test/form_submit_server.sjs
+ !/dom/xhr/tests/file_XHRSendData.sjs
+ !/dom/xhr/tests/temporaryFileBlob.sjs
+
+[test_blob_fragment_and_query.html]
+[test_blobconstructor.html]
+[test_blobURL_expiring.html]
+[test_file_from_blob.html]
+[test_nonascii_blob_url.html]
+skip-if =
+ http3
+[test_file_negative_date.html]
+[test_fileapi_basic.html]
+[test_fileapi_encoding.html]
+[test_fileapi_twice.html]
+[test_fileapi_other.html]
+[test_fileapi_basic_worker.html]
+[test_fileapi_encoding_worker.html]
+[test_fileapi_twice_worker.html]
+[test_fileapi_other_worker.html]
+[test_fileapi_slice_realFile_1.html]
+[test_fileapi_slice_realFile_2.html]
+skip-if = (verify && !debug && (os == 'win'))
+[test_fileapi_slice_memFile_1.html]
+[test_fileapi_slice_memFile_2.html]
+[test_fileapi_slice_image.html]
+[test_mozfiledataurl.html]
+skip-if =
+ toolkit == 'android' #TIMED_OUT
+ http3
+[test_bug1507893.html]
+support-files = worker_bug1507893.js
+[test_bug1742540.html]
+support-files = worker_bug1742540.js
+skip-if = toolkit == 'android' #TIMED_OUT
+[test_blob_reading.html]
+support-files = common_blob_reading.js worker_blob_reading.js
+skip-if =
+ http3
diff --git a/dom/file/tests/test_agentcluster_bloburl.js b/dom/file/tests/test_agentcluster_bloburl.js
new file mode 100644
index 0000000000..2a577116a2
--- /dev/null
+++ b/dom/file/tests/test_agentcluster_bloburl.js
@@ -0,0 +1,170 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+
+// Same agent cluster, all works fine: blobURLs can be opened.
+add_task(async () => {
+ do_get_profile();
+
+ // CookieXPCShellUtils.createServer does not support https
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ Services.prefs.setBoolPref(
+ "privacy.partition.bloburl_per_agent_cluster",
+ true
+ );
+
+ const server = CookieXPCShellUtils.createServer({ hosts: ["example.org"] });
+
+ let result = new Promise(resolve => {
+ server.registerPathHandler("/result", (metadata, response) => {
+ resolve(metadata.queryString == "ok");
+
+ const body = "Done";
+ response.bodyOutputStream.write(body, body.length);
+ });
+ });
+
+ server.registerPathHandler("/test", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ const body = `<script>
+ let b = new Blob(["Hello world!"]);
+ let u = URL.createObjectURL(b);
+ fetch(u).then(r => r.text()).then(t => {
+ if (t !== "Hello world!") {
+ throw new Error(42);
+ }
+ }).then(() => fetch("/result?ok"), () => fetch("/result?failure")).then(() => {});
+ </script>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/test"
+ );
+
+ Assert.ok(await result, "BlobURL works");
+ await contentPage.close();
+});
+
+// Same agent cluster: frames
+add_task(async () => {
+ do_get_profile();
+
+ // CookieXPCShellUtils.createServer does not support https
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ const server = CookieXPCShellUtils.createServer({ hosts: ["example.org"] });
+
+ let result = new Promise(resolve => {
+ server.registerPathHandler("/result", (metadata, response) => {
+ resolve(metadata.queryString == "ok");
+
+ const body = "Done";
+ response.bodyOutputStream.write(body, body.length);
+ });
+ });
+
+ server.registerPathHandler("/iframe", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ const body = `<script>
+ fetch("${metadata.queryString}").then(r => r.text()).then(t => {
+ if (t !== "Hello world!") {
+ throw new Error(42);
+ }
+ }).then(() => fetch("/result?ok"), () => fetch("/result?failure")).then(() => {});
+ </script>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ server.registerPathHandler("/test", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ const body = `<iframe id="a"></iframe><script>
+ let b = new Blob(["Hello world!"]);
+ let u = URL.createObjectURL(b);
+ document.getElementById("a").src = "/iframe?" + u;
+ </script>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/test"
+ );
+
+ Assert.ok(await result, "BlobURL works");
+ await contentPage.close();
+});
+
+// Cross agent cluster: different tabs
+add_task(async () => {
+ do_get_profile();
+
+ const server = CookieXPCShellUtils.createServer({ hosts: ["example.org"] });
+
+ let result = new Promise(resolve => {
+ server.registerPathHandler("/result", (metadata, response) => {
+ resolve(metadata.queryString == "ok");
+
+ const body = "Done";
+ response.bodyOutputStream.write(body, body.length);
+ });
+ });
+
+ const step = new Promise(resolve => {
+ server.registerPathHandler("/step", (metadata, response) => {
+ resolve(metadata.queryString);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ const body = "Thanks!";
+ response.bodyOutputStream.write(body, body.length);
+ });
+ });
+
+ server.registerPathHandler("/test", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ const body = `<script>
+ let b = new Blob(["Hello world!"]);
+ let u = URL.createObjectURL(b);
+ fetch("/step?" + u).then(() => {});
+ </script>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/test"
+ );
+
+ const blobURL = await step;
+ Assert.ok(blobURL.length, "We have a blobURL");
+
+ server.registerPathHandler("/cross-test", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ const body = `<script>
+ fetch("${metadata.queryString}").then(r => r.text()).then(t => {
+ if (t !== "Hello world!") {
+ throw new Error(42);
+ }
+ }).then(() => fetch("/result?ok"), () => fetch("/result?failure")).then(() => {});
+ </script>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ let contentPage2 = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/cross-test?" + blobURL
+ );
+
+ Assert.ok(!(await result), "BlobURL should not work");
+ await contentPage.close();
+ await contentPage2.close();
+});
diff --git a/dom/file/tests/test_blobURL_expiring.html b/dom/file/tests/test_blobURL_expiring.html
new file mode 100644
index 0000000000..7fdf461371
--- /dev/null
+++ b/dom/file/tests/test_blobURL_expiring.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Blob URI expiration</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <script>
+
+onmessage = function(e) {
+ var blobURL = e.data;
+
+ (new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", blobURL);
+ xhr.send();
+ xhr.onload = function() {
+ is(xhr.response, "123", "Response matches!");
+ resolve();
+ }
+ })).then(function() {
+ document.body.removeChild(iframe);
+ }).then(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", blobURL);
+ xhr.onerror = function() {
+ ok(true, "The URL should be done!");
+ SimpleTest.finish();
+ }
+ xhr.onload = function() {
+ ok(false, "The URL should be done!");
+ SimpleTest.finish();
+ }
+ xhr.send();
+ });
+}
+
+var iframe = document.createElement('iframe');
+iframe.src = 'file_blobURL_expiring.html';
+document.body.appendChild(iframe);
+
+SimpleTest.waitForExplicitFinish();
+
+ </script>
+</body>
+</html>
diff --git a/dom/file/tests/test_blob_fragment_and_query.html b/dom/file/tests/test_blob_fragment_and_query.html
new file mode 100644
index 0000000000..fa3709c419
--- /dev/null
+++ b/dom/file/tests/test_blob_fragment_and_query.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Blob URI with fragments</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <script>
+
+var blob = new Blob(['hello world']);
+ok(blob, "We have a blob.");
+
+var tests = [
+ { part: "", revoke: false, ok: true },
+ { part: "", revoke: true, ok: false },
+ { part: "?aa", revoke: false, ok: false },
+ { part: "?cc#dd", revoke: false, ok: false },
+ // Stripping #fragment on fetch
+ { part: "#bb", revoke: false, ok: true },
+ { part: "#ee?ff", revoke: false, ok: true }
+];
+
+function runTest() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var url = URL.createObjectURL(blob);
+ ok(url, "We have a URI");
+
+ var test = tests.shift();
+
+ if (test.revoke) {
+ URL.revokeObjectURL(url + test.part);
+ }
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url + test.part);
+
+ xhr.onload = function() {
+ ok(test.ok, `URL with "${test.part}" should send()`);
+ is(xhr.responseText, 'hello world', 'URL: ' + url + test.part);
+ runTest();
+ }
+
+ xhr.onerror = function() {
+ ok(!test.ok, `URL with "${test.part}" should fail on send()`);
+ runTest();
+ }
+
+ xhr.send();
+}
+
+SimpleTest.waitForExplicitFinish();
+runTest();
+
+ </script>
+</body>
+</html>
diff --git a/dom/file/tests/test_blob_reading.html b/dom/file/tests/test_blob_reading.html
new file mode 100644
index 0000000000..6efc5c4835
--- /dev/null
+++ b/dom/file/tests/test_blob_reading.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Reading blobs</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="common_blob_reading.js"></script>
+ <script src="common_blob_types.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <script>
+
+SimpleTest.waitForExplicitFinish();
+
+async function runAllTests() {
+ let content = "hello world";
+ await forEachBlobType(content, async blob => {
+ await testBlobText(blob, content);
+ await workify('testBlobText', blob, content);
+
+ await testBlobArrayBuffer(blob, content);
+ await workify('testBlobArrayBuffer', blob, content);
+
+ await testBlobStream(blob, content);
+ await workify('testBlobStream', blob, content);
+ });
+}
+
+runAllTests().then(SimpleTest.finish);
+
+ </script>
+</body>
+</html>
diff --git a/dom/file/tests/test_blobconstructor.html b/dom/file/tests/test_blobconstructor.html
new file mode 100644
index 0000000000..d95444f9ea
--- /dev/null
+++ b/dom/file/tests/test_blobconstructor.html
@@ -0,0 +1,246 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=721569
+-->
+<head>
+ <title>Test for Blob constructor (Bug 721569)</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common_blob.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=721569">Mozilla Bug 721569</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+"use strict";
+/** Test for Bug 721569 **/
+var blob = new Blob();
+ok(blob, "Blob should exist");
+
+ok(blob.size !== undefined, "Blob should have a size property");
+ok(blob.type !== undefined, "Blob should have a type property");
+ok(blob.slice, "Blob should have a slice method");
+
+blob = new Blob([], {type: null});
+ok(blob, "Blob should exist");
+is(blob.type, "null", "Blob type should be stringified");
+
+blob = new Blob([], {type: undefined});
+ok(blob, "Blob should exist");
+is(blob.type, "", "Blob type should be treated as missing");
+
+try {
+blob = new Blob([]);
+ok(true, "an empty blobParts argument should not throw");
+} catch(e) {
+ok(false, "NOT REACHED");
+}
+
+try {
+blob = new Blob(null);
+ok(false, "NOT REACHED");
+} catch(e) {
+ok(true, "a null blobParts member should throw");
+}
+
+try {
+blob = new Blob([], null);
+ok(true, "a null options member should not throw");
+} catch(e) {
+ok(false, "NOT REACHED");
+}
+
+try {
+blob = new Blob([], undefined);
+ok(true, "an undefined options member should not throw");
+} catch(e) {
+ok(false, "NOT REACHED");
+}
+
+try {
+blob = new Blob([], false);
+ok(false, "NOT REACHED");
+} catch(e) {
+ok(true, "a boolean options member should throw");
+}
+
+try {
+blob = new Blob([], 0);
+ok(false, "NOT REACHED");
+} catch(e) {
+ok(true, "a numeric options member should throw");
+}
+
+try {
+blob = new Blob([], "");
+ok(false, "NOT REACHED");
+} catch(e) {
+ok(true, "a string options member should throw");
+}
+
+/** Test for dictionary initialization order **/
+(function() {
+ var o = {};
+ var p = {type: "text/plain", endings: "transparent"};
+ var called = [];
+ function add_to_called(n) {
+ called.push(n);
+ return p[n];
+ }
+ ["type", "endings"].forEach(function(n) {
+ Object.defineProperty(o, n, { get: add_to_called.bind(null, n) });
+ });
+ var b = new Blob([], o);
+ is(JSON.stringify(called), JSON.stringify(["endings", "type"]), "dictionary members should be get in lexicographical order");
+})();
+
+let blob1 = new Blob(["squiggle"]);
+ok(blob1 instanceof Blob, "Blob constructor should produce Blobs");
+ok(!(blob1 instanceof File), "Blob constructor should not produce Files");
+is(blob1.type, "", "Blob constructor with no options should return Blob with empty type");
+is(blob1.size, 8, "Blob constructor should return Blob with correct size");
+
+let blob2 = new Blob(["steak"], {type: "content/type"});
+ok(blob2 instanceof Blob, "Blob constructor should produce Blobs");
+ok(!(blob2 instanceof File), "Blob constructor should not produce Files");
+is(blob2.type, "content/type", "Blob constructor with a type option should return Blob with the type");
+is(blob2.size, 5, "Blob constructor should return Blob with correct size");
+
+
+let aB = new ArrayBuffer(16);
+var int8View = new Int8Array(aB);
+for (var i = 0; i < 16; i++) {
+ int8View[i] = i+65;
+}
+
+let testData =
+ [
+ // Test 3 strings
+ [["foo", "bar", "baz"], {},
+ [{start: 0, length: 9, contents: "foobarbaz"},
+ {start: 0, length: 3, contents: "foo"},
+ {start: 3, length:6, contents: "barbaz"},
+ {start: 6, length: 3, contents: "baz"},
+ {start: 6, length: 6, contents: "baz"},
+ {start: 0, length: 9, contents: "foobarbaz"},
+ {start: 0, length: 11, contents: "foobarbaz"},
+ {start: 10, length: 5, contents: ""}]],
+ // Test string, Blob, string
+ [["foo", blob1, "baz"], {},
+ [{start: 0, length: 3, contents: "foo"},
+ {start: 3, length: 8, contents: "squiggle"},
+ {start: 2, length: 2, contents: "os"},
+ {start: 10, length: 2, contents: "eb"}]],
+ // Test blob, string, blob
+ [[blob1, "foo", blob1], {},
+ [{start: 0, length: 8, contents: "squiggle"},
+ {start: 7, length: 2, contents: "ef"},
+ {start: 10, length: 2, contents: "os"},
+ {start: 1, length: 3, contents: "qui"},
+ {start: 12, length: 3, contents: "qui"},
+ {start: 40, length: 20, contents: ""}]],
+ // Test blobs all the way down
+ [[blob2, blob1, blob2], {},
+ [{start: 0, length: 5, contents: "steak"},
+ {start: 5, length: 8, contents: "squiggle"},
+ {start: 13, length: 5, contents: "steak"},
+ {start: 1, length: 2, contents: "te"},
+ {start: 6, length: 4, contents: "quig"}]],
+ // Test an array buffer
+ [[aB, blob1, "foo"], {},
+ [{start: 0, length: 8, contents: "ABCDEFGH"},
+ {start: 8, length:10, contents: "IJKLMNOPsq"},
+ {start: 17, length: 3, contents: "qui"},
+ {start: 4, length: 8, contents: "EFGHIJKL"}]],
+ // Test an ArrayBufferView
+ [[int8View, blob1, "foo"], {},
+ [{start: 0, length: 8, contents: "ABCDEFGH"},
+ {start: 8, length:10, contents: "IJKLMNOPsq"},
+ {start: 17, length: 3, contents: "qui"},
+ {start: 4, length: 8, contents: "EFGHIJKL"}]],
+ // Test a partial ArrayBufferView
+ [[new Uint8Array(aB, 3, 5), blob1, "foo"], {},
+ [{start: 0, length: 8, contents: "DEFGHsqu"},
+ {start: 8, length:10, contents: "igglefoo"},
+ {start: 4, length: 8, contents: "Hsquiggl"}]],
+ // Test transparent line endings
+ [["foo\r\n", "bar\r", "baz\n"], { endings: "transparent" },
+ [{start: 0, length: 5, contents: "foo\r\n"},
+ {start: 5, length: 4, contents: "bar\r"},
+ {start: 9, length: 4, contents: "baz\n"}]],
+ // Test transparent line endings when the second argument is omitted
+ [["foo\r\n", "bar\r", "baz\n"], undefined,
+ [{start: 0, length: 5, contents: "foo\r\n"},
+ {start: 5, length: 4, contents: "bar\r"},
+ {start: 9, length: 4, contents: "baz\n"}]],
+ // Test native line endings
+ [["foo\r\n", "bar\r", "baz\n"], { endings: "native" },
+ navigator.platform.includes("Win") ?
+ [{start: 0, length: 5, contents: "foo\r\n"},
+ {start: 5, length: 5, contents: "bar\r\n"},
+ {start: 10, length: 5, contents: "baz\r\n"}] :
+ [{start: 0, length: 4, contents: "foo\n"},
+ {start: 4, length: 4, contents: "bar\n"},
+ {start: 8, length: 4, contents: "baz\n"}]],
+ // Test type coercion of a number
+ [[3, int8View, "foo"], {},
+ [{start: 0, length: 8, contents: "3ABCDEFG"},
+ {start: 8, length:10, contents: "HIJKLMNOPf"},
+ {start: 17, length: 4, contents: "foo"},
+ {start: 4, length: 8, contents: "DEFGHIJK"}]]
+ ];
+
+let currentTest = null;
+let testCounter = 0;
+
+function runTests() {
+ if (!currentTest || !currentTest[2].length) {
+ if (!testData.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ currentTest = testData.shift();
+ ++testCounter;
+ }
+
+ let [blobs, options] = currentTest;
+ let test = currentTest[2].shift();
+
+ let blob3;
+ if (options !== undefined) {
+ blob3 = new Blob(blobs, options);
+ } else {
+ blob3 = new Blob(blobs);
+ }
+
+ ok(blob3, "Test " + testCounter + " got blob");
+ ok(blob3 instanceof Blob, "Test " + testCounter + " blob is a Blob");
+ ok(!(blob3 instanceof File), "Test " + testCounter + " blob is not a File");
+
+ let slice = blob3.slice(test.start, test.start + test.length);
+ ok(slice, "Test " + testCounter + " got slice");
+ ok(slice instanceof Blob, "Test " + testCounter + " slice is a Blob");
+ ok(!(slice instanceof File), "Test " + testCounter + " slice is not a File");
+ is(slice.size, test.contents.length, "Test " + testCounter + " slice is correct size");
+
+ testBlob(slice, test.contents, "Test " + testCounter).then(() => {
+ SpecialPowers.gc();
+ runTests();
+ });
+}
+
+SimpleTest.requestLongerTimeout(2);
+SimpleTest.waitForExplicitFinish();
+runTests();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/file/tests/test_bloburi.js b/dom/file/tests/test_bloburi.js
new file mode 100644
index 0000000000..3485b8a681
--- /dev/null
+++ b/dom/file/tests/test_bloburi.js
@@ -0,0 +1,24 @@
+var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+var uris = [
+ {
+ uri: "blob:https://example.com/230d5d50-35f9-9745-a64a-15e47b731a81",
+ local: true,
+ },
+ {
+ uri: "rstp://1.2.3.4/some_path?param=a",
+ local: false,
+ },
+];
+
+function run_test() {
+ for (let i = 0; i < uris.length; i++) {
+ let uri = ios.newURI(uris[i].uri);
+ let flags = ios.getDynamicProtocolFlags(uri);
+
+ Assert.equal(
+ Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE & flags,
+ uris[i].local ? Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE : 0
+ );
+ }
+}
diff --git a/dom/file/tests/test_bug1507893.html b/dom/file/tests/test_bug1507893.html
new file mode 100644
index 0000000000..f0f83ff9ce
--- /dev/null
+++ b/dom/file/tests/test_bug1507893.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Blob URLs fetched in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <script>
+
+SimpleTest.waitForExplicitFinish();
+
+// Let's be positive.
+Promise.resolve()
+
+// Create a file.
+.then(_ => {
+ return new Promise(resolve => {
+ let openerURL = SimpleTest.getTestFileURL("fileapi_chromeScript.js");
+ let opener = SpecialPowers.loadChromeScript(openerURL);
+
+ opener.addMessageListener("files.opened", files => {
+ resolve(files[0]);
+ });
+
+ opener.sendAsyncMessage("files.open", [ "I am the blob content" ]);
+ })
+})
+
+// Just a couple of checks
+.then(file => {
+ ok(file instanceof File, "We want a file");
+ ok(file.size > 0, "We have content");
+ return file;
+})
+
+// Let's create a blobURL
+.then(file => URL.createObjectURL(file))
+
+// Let's send it to a worker.
+.then(url => {
+ return new Promise(resolve => {
+ let w = new Worker('worker_bug1507893.js');
+ w.onmessage = e => {
+ resolve(e.data);
+ };
+ w.postMessage(url);
+ });
+})
+
+// Let's check the worker's output
+.then(blob => {
+ ok(blob instanceof File, "The worker sends us a blob");
+ ok(blob.size > 0, "We have data");
+})
+
+// All done.
+.then(SimpleTest.finish);
+
+ </script>
+</body>
+</html>
diff --git a/dom/file/tests/test_bug1742540.html b/dom/file/tests/test_bug1742540.html
new file mode 100644
index 0000000000..7516f38a85
--- /dev/null
+++ b/dom/file/tests/test_bug1742540.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1742540</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <script>
+
+SimpleTest.waitForExplicitFinish();
+
+add_task(function setupPrefs() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+});
+
+function get_file() {
+ return new Promise(resolve => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", "/dynamic/getMyDirectory.sjs", false);
+ xhr.send();
+ let basePath = xhr.responseText;
+
+ let script = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("create_file_objects.js")
+ );
+ script.addMessageListener("created-file-objects", files => {
+ resolve(files[0]);
+ });
+ script.sendAsyncMessage("create-file-objects", {
+ fileNames: [basePath + "file_mozfiledataurl_audio.ogg"],
+ });
+ });
+}
+
+function wait_for_message(port, expected_message) {
+ return new Promise(resolve => {
+ port.onmessage = event => {
+ port.onmessage = null;
+ ok(event.data === expected_message, event.data);
+ resolve();
+ };
+ });
+}
+
+function unregister_and_done(registration) {
+ return registration.unregister().then(() => {
+ ok(true, "Will find leaks of nsPipe in BloatView without fix.");
+ SimpleTest.finish;
+ });
+}
+
+add_task(async function send_file_to_serviceworker() {
+ let registration = await navigator.serviceWorker
+ .register("worker_bug1742540.js", { scope: "./" })
+ .then(() => {
+ return navigator.serviceWorker.ready;
+ });
+
+ ok(registration.active, "ServiceWorker is activated");
+
+ let file = await get_file();
+ ok(file.size > 100000, "File size is big enough.");
+ let message = "ServiceWorker receives a file and did not reference it.";
+ let channel = new MessageChannel();
+ let received = wait_for_message(channel.port1, message);
+ registration.active.postMessage({ port: channel.port2, message, file }, [
+ channel.port2,
+ ]);
+ await received;
+
+ let finish = await unregister_and_done(registration);
+});
+
+ </script>
+</body>
+</html>
diff --git a/dom/file/tests/test_createFile.js b/dom/file/tests/test_createFile.js
new file mode 100644
index 0000000000..ebda3d4dae
--- /dev/null
+++ b/dom/file/tests/test_createFile.js
@@ -0,0 +1,52 @@
+add_task(async function () {
+ do_get_profile();
+
+ let existingFile = Services.dirsvc
+ .QueryInterface(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ existingFile.append("exists.js");
+ existingFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ var outStream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ outStream.init(
+ existingFile,
+ 0x02 | 0x08 | 0x20, // write, create, truncate
+ 0o666,
+ 0
+ );
+
+ var fileData = "Hello World!";
+ outStream.write(fileData, fileData.length);
+ outStream.close();
+
+ ok(existingFile.exists(), "exists.js exists");
+
+ let unknownFile = Services.dirsvc
+ .QueryInterface(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+ unknownFile.append("wow.txt");
+
+ ok(!unknownFile.exists(), unknownFile.path + " doesn't exist");
+
+ let a = await File.createFromNsIFile(existingFile, { existenceCheck: false });
+ ok(a.size != 0, "The size is correctly set");
+
+ let b = await File.createFromNsIFile(unknownFile, { existenceCheck: false });
+ ok(b.size == 0, "The size is 0 for unknown file");
+
+ let c = await File.createFromNsIFile(existingFile, { existenceCheck: true });
+ ok(c.size != 0, "The size is correctly set");
+
+ let d = await File.createFromNsIFile(unknownFile, {
+ existenceCheck: true,
+ }).then(
+ _ => true,
+ _ => false
+ );
+ ok(d === false, "Exception thrown");
+
+ existingFile.remove(true);
+ ok(!existingFile.exists(), "exists.js doesn't exist anymore");
+});
diff --git a/dom/file/tests/test_file_from_blob.html b/dom/file/tests/test_file_from_blob.html
new file mode 100644
index 0000000000..8c12a2823a
--- /dev/null
+++ b/dom/file/tests/test_file_from_blob.html
@@ -0,0 +1,110 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=819900
+-->
+ <head>
+<title>Test for crash caused by unloading and reloading srcdoc iframes</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=819900">Mozilla Bug 819900</a>
+
+<pre id="test">
+<script>
+
+ var b = new Blob(['1234567890']);
+ ok(b, 'Blob created');
+ is(b.size, 10, 'Blob has the right size');
+
+ var status = false;
+ try {
+ f = new File(b);
+ } catch(e) {
+ status = true;
+ }
+ ok(status, "File throws if the second argument is missing");
+
+ status = false;
+ try {
+ f = new File(42, 'foobar.txt');
+ } catch(e) {
+ status = true;
+ }
+ ok(status, "File throws if the argument is not an array");
+
+ status = false;
+ try {
+ f = new File({}, 'foobar.txt');
+ } catch(e) {
+ status = true;
+ }
+ ok(status, "File throws if the argument is not an array");
+
+ status = false;
+ try {
+ f = new File("hello world", 'foobar.txt');
+ } catch(e) {
+ status = true;
+ }
+ ok(status, "File throws if the argument is not an array");
+
+ f = new File(['1234567890'], '');
+ ok(f, 'File created');
+ is(f.size, 10, 'File has the right size');
+ is(f.name, '');
+ is(f.type, '');
+
+ f = new File(['1234567890'], 42);
+ ok(f, 'File created');
+ is(f.size, 10, 'File has the right size');
+ is(f.name, '42');
+ is(f.type, '');
+
+ f = new File(['1234567890'], 'text.txt');
+ ok(f, 'File created');
+ is(f.size, 10, 'File has the right size');
+ is(f.name, 'text.txt');
+ is(f.type, '');
+
+ f = new File(['1234567890'], 'text.txt', { type: 'plain/text' });
+ ok(f, 'File created');
+ is(f.size, 10, 'File has the right size');
+ is(f.name, 'text.txt');
+ is(f.type, 'plain/text');
+
+ f = new File([b], 'text.txt');
+ ok(f, 'File created');
+ is(f.name, 'text.txt');
+ is(f.type, '');
+ is(f.size, b.size);
+
+ f = new File([b], 'test.txt', { type: 'plain/text' });
+ ok(f, 'File created');
+ is(f.name, 'test.txt');
+ is(f.type, 'plain/text');
+ is(f.size, b.size);
+
+ f = new File([b, b], 'test.txt', { type: 'plain/text' });
+ ok(f, 'File created');
+ is(f.name, 'test.txt');
+ is(f.type, 'plain/text');
+ is(f.size, b.size * 2);
+
+ var f2 = new File([f, f], 'test.txt', { type: 'plain/text' });
+ ok(f2, 'File created');
+ is(f2.name, 'test.txt');
+ is(f2.type, 'plain/text');
+ is(f2.size, f.size * 2);
+
+ var f2 = new File([f, f], 'test.txt', b);
+ ok(f2, 'File created');
+ is(f2.name, 'test.txt');
+ is(f2.type, b.type);
+ is(f2.size, f.size * 2);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/file/tests/test_file_negative_date.html b/dom/file/tests/test_file_negative_date.html
new file mode 100644
index 0000000000..2e8528b88d
--- /dev/null
+++ b/dom/file/tests/test_file_negative_date.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1158437
+-->
+<head>
+ <title>Test for negative date in File (Bug 1158437)</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1158437">Mozilla Bug 1158437</a>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+var blob = new Blob(['hello world']);
+var f1 = new File([blob], 'f1.txt', { lastModified: 0 });
+var f2 = new File([blob], 'f2.txt', { lastModified: -1 });
+var f3 = new File([blob], 'f3.txt', { lastModified: -1000 });
+
+is(f1.lastModified, 0, "lastModified == 0 is supported");
+is(f2.lastModified, -1, "lastModified == -1 is supported");
+is(f3.lastModified, -1000, "lastModified == -1000 is supported");
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_basic.html b/dom/file/tests/test_fileapi_basic.html
new file mode 100644
index 0000000000..7f7aed788e
--- /dev/null
+++ b/dom/file/tests/test_fileapi_basic.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for FileReader API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+ return runBasicTests(data);
+})
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_basic_worker.html b/dom/file/tests/test_fileapi_basic_worker.html
new file mode 100644
index 0000000000..03db1b4cb6
--- /dev/null
+++ b/dom/file/tests/test_fileapi_basic_worker.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for FileReader API in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+ let worker = new Worker('worker_fileReader.js');
+ worker.postMessage({ tests: 'basic', data });
+
+ worker.onmessage = event => {
+ if (event.data.type == 'finish') {
+ SimpleTest.finish();
+ return;
+ }
+
+ if (event.data.type == 'check') {
+ ok(event.data.status, event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message.");
+ }
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_encoding.html b/dom/file/tests/test_fileapi_encoding.html
new file mode 100644
index 0000000000..f20a46ac6c
--- /dev/null
+++ b/dom/file/tests/test_fileapi_encoding.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for FileReader API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+ return runEncodingTests(data);
+})
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_encoding_worker.html b/dom/file/tests/test_fileapi_encoding_worker.html
new file mode 100644
index 0000000000..ab6827f086
--- /dev/null
+++ b/dom/file/tests/test_fileapi_encoding_worker.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for FileReader API in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+ let worker = new Worker('worker_fileReader.js');
+ worker.postMessage({ tests: 'encoding', data });
+
+ worker.onmessage = event => {
+ if (event.data.type == 'finish') {
+ SimpleTest.finish();
+ return;
+ }
+
+ if (event.data.type == 'check') {
+ ok(event.data.status, event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message.");
+ }
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_other.html b/dom/file/tests/test_fileapi_other.html
new file mode 100644
index 0000000000..6c01415be8
--- /dev/null
+++ b/dom/file/tests/test_fileapi_other.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for FileReader API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+ return runOtherTests(data);
+})
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_other_worker.html b/dom/file/tests/test_fileapi_other_worker.html
new file mode 100644
index 0000000000..a535d3fcb4
--- /dev/null
+++ b/dom/file/tests/test_fileapi_other_worker.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for FileReader API in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+ let worker = new Worker('worker_fileReader.js');
+ worker.postMessage({ tests: 'other', data });
+
+ worker.onmessage = event => {
+ if (event.data.type == 'finish') {
+ SimpleTest.finish();
+ return;
+ }
+
+ if (event.data.type == 'check') {
+ ok(event.data.status, event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message.");
+ }
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_slice_image.html b/dom/file/tests/test_fileapi_slice_image.html
new file mode 100644
index 0000000000..c873afa438
--- /dev/null
+++ b/dom/file/tests/test_fileapi_slice_image.html
@@ -0,0 +1,139 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for File API + Slice</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common_blob.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<p id="display">
+ <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas>
+ <canvas id=testcanvas hidden moz-opaque></canvas>
+</p>
+
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(4);
+
+// Create files containing data we'll test with. We'll want long
+// strings to ensure they span multiple buffers while loading
+
+let canvasData;
+let testBinaryData;
+
+function imageLoadHandler(event, resolve) {
+ let origcanvas = $("canvas");
+ let testcanvas = $("testcanvas");
+ let image = event.target;
+ is(image.naturalWidth, origcanvas.width, "width correct");
+ is(image.naturalHeight, origcanvas.height, "height correct");
+
+ testcanvas.width = origcanvas.width;
+ testcanvas.height = origcanvas.height;
+ testcanvas.getContext("2d").drawImage(image, 0, 0);
+ // Do not use |is(testcanvas.toDataURL("image/png"), origcanvas.toDataURL("image/png"), "...");| that results in a _very_ long line.
+ let origDataURL = origcanvas.toDataURL("image/png");
+ let testDataURL = testcanvas.toDataURL("image/png");
+ is(testDataURL.length, origDataURL.length,
+ "Length of correct image data");
+ ok(testDataURL == origDataURL,
+ "Content of correct image data");
+ resolve();
+}
+
+createCanvasURL()
+.then(data => {
+ for (var i = 0; i < 256; i++) {
+ testBinaryData += String.fromCharCode(i);
+ }
+ while (testBinaryData.length < 20000) {
+ testBinaryData += testBinaryData;
+ }
+
+ canvasData = data;
+})
+
+// image in the middle
+.then(() => {
+ return createFile(testBinaryData + canvasData + testBinaryData, "middleTestFile");
+})
+
+// image in the middle - loader
+.then(file => {
+ return new Promise(resolve => {
+ is(file.size, canvasData.length + testBinaryData.length * 2, "correct file size (middle)");
+
+ var img = new Image();
+ img.src = URL.createObjectURL(file.slice(testBinaryData.length,
+ testBinaryData.length + canvasData.length));
+ img.onload = event => {
+ imageLoadHandler(event, resolve);
+ }
+ });
+})
+
+// image at start
+.then(() => {
+ return createFile(canvasData + testBinaryData, "startTestFile");
+})
+
+// image at start - loader
+.then(file => {
+ return new Promise(resolve => {
+ is(file.size, canvasData.length + testBinaryData.length, "correct file size (start)");
+
+ var img = new Image();
+ img.src = URL.createObjectURL(file.slice(0, canvasData.length));
+ img.onload = event => {
+ imageLoadHandler(event, resolve);
+ }
+ });
+})
+
+// image at end
+.then(() => {
+ return createFile(testBinaryData + canvasData, "endTestFile");
+})
+
+// image at end - loader
+.then(file => {
+ return new Promise(resolve => {
+ is(file.size, canvasData.length + testBinaryData.length, "correct file size (end)");
+
+ var img = new Image();
+ img.src = URL.createObjectURL(file.slice(testBinaryData.length,
+ testBinaryData.length + canvasData.length));
+ img.onload = event => {
+ imageLoadHandler(event, resolve);
+ }
+ });
+})
+
+// image past end
+.then(() => {
+ return createFile(testBinaryData + canvasData, "pastEndTestFile");
+})
+
+// image past end - loader
+.then(file => {
+ return new Promise(resolve => {
+ is(file.size, canvasData.length + testBinaryData.length, "correct file size (end)");
+
+ var img = new Image();
+ img.src = URL.createObjectURL(file.slice(testBinaryData.length,
+ testBinaryData.length + canvasData.length + 1000));
+ img.onload = event => {
+ imageLoadHandler(event, resolve);
+ }
+ });
+})
+
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_slice_memFile_1.html b/dom/file/tests/test_fileapi_slice_memFile_1.html
new file mode 100644
index 0000000000..56d63d15ba
--- /dev/null
+++ b/dom/file/tests/test_fileapi_slice_memFile_1.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for File API + Slice (in memory)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common_blob.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<p id="display">
+ <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas>
+</p>
+
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(4);
+
+createCanvasURL()
+.then(data => {
+ let cx = $("canvas").getContext('2d');
+ return Promise.all([toBlobPromise(cx.canvas),
+ Promise.resolve(data)]);
+})
+
+.then(args => {
+ let [memFile, data] = args;
+ return testSlice(memFile, data.length, "image/png", data, "memFile", RANGE_1);
+})
+
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_slice_memFile_2.html b/dom/file/tests/test_fileapi_slice_memFile_2.html
new file mode 100644
index 0000000000..aef8813bf5
--- /dev/null
+++ b/dom/file/tests/test_fileapi_slice_memFile_2.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for File API + Slice (in memory)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common_blob.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<p id="display">
+ <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas>
+</p>
+
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(4);
+
+createCanvasURL()
+.then(data => {
+ let cx = $("canvas").getContext('2d');
+ return Promise.all([toBlobPromise(cx.canvas),
+ Promise.resolve(data)]);
+})
+
+.then(args => {
+ let [memFile, data] = args;
+ return testSlice(memFile, data.length, "image/png", data, "memFile", RANGE_2);
+})
+
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_slice_realFile_1.html b/dom/file/tests/test_fileapi_slice_realFile_1.html
new file mode 100644
index 0000000000..97798d805a
--- /dev/null
+++ b/dom/file/tests/test_fileapi_slice_realFile_1.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for File API + Slice (in file)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common_blob.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<p id="display">
+ <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas>
+</p>
+
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(4);
+
+let canvasData;
+
+createCanvasURL()
+.then(data => {
+ canvasData = data;
+ return createFile(data, "basicTestFile1");
+})
+
+.then(file => {
+ return testSlice(file, canvasData.length, "", canvasData, "fileFile", RANGE_1);
+})
+
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_slice_realFile_2.html b/dom/file/tests/test_fileapi_slice_realFile_2.html
new file mode 100644
index 0000000000..882ce61c03
--- /dev/null
+++ b/dom/file/tests/test_fileapi_slice_realFile_2.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for File API + Slice (in file)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="common_blob.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<p id="display">
+ <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas>
+</p>
+
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(4);
+
+let canvasData;
+
+createCanvasURL()
+.then(data => {
+ canvasData = data;
+ return createFile(data, "basicTestFile2");
+})
+
+.then(file => {
+ return testSlice(file, canvasData.length, "", canvasData, "fileFile", RANGE_2);
+})
+
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_twice.html b/dom/file/tests/test_fileapi_twice.html
new file mode 100644
index 0000000000..96e7febdce
--- /dev/null
+++ b/dom/file/tests/test_fileapi_twice.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for FileReader API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+ return runTwiceTests(data);
+})
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_fileapi_twice_worker.html b/dom/file/tests/test_fileapi_twice_worker.html
new file mode 100644
index 0000000000..a79d3992b2
--- /dev/null
+++ b/dom/file/tests/test_fileapi_twice_worker.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for FileReader API in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+ let worker = new Worker('worker_fileReader.js');
+ worker.postMessage({ tests: 'twice', data });
+
+ worker.onmessage = event => {
+ if (event.data.type == 'finish') {
+ SimpleTest.finish();
+ return;
+ }
+
+ if (event.data.type == 'check') {
+ ok(event.data.status, event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message.");
+ }
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/test_ipc_messagemanager_blob.js b/dom/file/tests/test_ipc_messagemanager_blob.js
new file mode 100644
index 0000000000..dacdc9e7bb
--- /dev/null
+++ b/dom/file/tests/test_ipc_messagemanager_blob.js
@@ -0,0 +1,102 @@
+"use strict";
+
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+
+XPCShellContentUtils.init(this);
+
+function childFrameScript() {
+ addMessageListener("test:ipcClonedMessage", function (message) {
+ if (!Blob.isInstance(message.json)) {
+ sendAsyncMessage(message.name, message.json);
+ return;
+ }
+
+ let reader = new FileReader();
+ reader.addEventListener("load", function () {
+ let response =
+ reader.result == "this is a great success!" ? message.json : "error";
+ sendAsyncMessage(message.name, response);
+ });
+ reader.readAsText(message.json);
+ });
+}
+
+add_task(async function test() {
+ let page = await XPCShellContentUtils.loadContentPage("about:blank", {
+ remote: true,
+ });
+
+ page.loadFrameScript(childFrameScript);
+
+ const blobString = "this is a great success!";
+
+ const messages = [
+ "hi!",
+ "",
+ 2,
+ -0.04,
+ 34329873249872400000000000000,
+ true,
+ false,
+ null,
+ 0,
+
+ // Make sure this one is always last.
+ new Blob(["this ", "is ", "a ", "great ", "success!"], {
+ type: "text/plain",
+ }),
+ ];
+ let receivedMessageIndex = 0;
+
+ let mm = page.browser.messageManager;
+ let done = new Promise(resolve => {
+ mm.addMessageListener("test:ipcClonedMessage", async message => {
+ let data = message.json;
+
+ if (Blob.isInstance(data)) {
+ equal(receivedMessageIndex, messages.length - 1, "Blob is last");
+ equal(
+ data.size,
+ messages[receivedMessageIndex].size,
+ "Correct blob size"
+ );
+ equal(
+ data.type,
+ messages[receivedMessageIndex].type,
+ "Correct blob type"
+ );
+
+ let reader1 = new FileReader();
+ reader1.readAsText(data);
+
+ let reader2 = new FileReader();
+ reader2.readAsText(messages[receivedMessageIndex]);
+
+ await Promise.all([
+ new Promise(res => (reader1.onload = res)),
+ new Promise(res => (reader2.onload = res)),
+ ]);
+
+ equal(reader1.result, blobString, "Result 1");
+ equal(reader2.result, blobString, "Result 2");
+
+ resolve();
+ } else {
+ equal(
+ data,
+ messages[receivedMessageIndex++],
+ "Got correct round-tripped response"
+ );
+ }
+ });
+ });
+
+ for (let message of messages) {
+ mm.sendAsyncMessage("test:ipcClonedMessage", message);
+ }
+
+ await done;
+ await page.close();
+});
diff --git a/dom/file/tests/test_mozfiledataurl.html b/dom/file/tests/test_mozfiledataurl.html
new file mode 100644
index 0000000000..68c88fa28c
--- /dev/null
+++ b/dom/file/tests/test_mozfiledataurl.html
@@ -0,0 +1,224 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
+ <title>Test for File urls</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="start()">
+<p id="display">
+<iframe id=inner></iframe>
+<iframe id=iframe></iframe>
+<img id=img onload="gen.next(event);">
+<audio id=audio onloadeddata="gen.next(event);">
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+
+try {
+ URL.createObjectURL(undefined);
+} catch(e) { }
+
+window.addEventListener("message", function(e) {
+ gen.next(JSON.parse(e.data));
+});
+
+const innerSameSiteURI = "file_mozfiledataurl_inner.html";
+const innerCrossSiteURI = "http://example.com/tests/dom/file/tests/file_mozfiledataurl_inner.html"
+
+var fileNames = ["file_mozfiledataurl_img.jpg",
+ "file_mozfiledataurl_audio.ogg",
+ "file_mozfiledataurl_doc.html",
+ "file_mozfiledataurl_text.txt"];
+
+function start() {
+ let xhr = new XMLHttpRequest;
+ xhr.open("GET", "/dynamic/getMyDirectory.sjs", false);
+ xhr.send();
+ let basePath = xhr.responseText;
+
+ let fullFileNames = [];
+ for (let name of fileNames) {
+ fullFileNames.push(basePath + name);
+ }
+
+ var script = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("create_file_objects.js"));
+
+ script.addMessageListener("created-file-objects", function handler(files) {
+ script.removeMessageListener("created-file-objects", handler);
+ gen = runTest(files);
+ gen.next();
+ });
+
+ script.sendAsyncMessage("create-file-objects", {fileNames: fullFileNames});
+};
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.expectAssertions(0, 3);
+
+function* runTest([imgFile, audioFile, docFile, xhrFile]) {
+ inner = document.getElementById('inner');
+ img = document.getElementById('img');
+ audio = document.getElementById('audio');
+ iframe = document.getElementById('iframe');
+ inner.onload = function() { gen.next("inner loaded"); };
+
+ // Attempt to load a image in this document
+ var fileurl = URL.createObjectURL(imgFile);
+ img.src = fileurl;
+ var e = (yield);
+ is(e.type, "load", "loaded successfully");
+ is(img.width, 120, "correct width");
+ is(img.height, 90, "correct height");
+
+ // Revoke url and attempt to load a image in this document
+ img.src = "file_mozfiledataurl_img.jpg";
+ is((yield).type, "load", "successfull reset image");
+ URL.revokeObjectURL(fileurl);
+ todo(false, "urls need to act like 404s, not fail to parse");
+/* img.src = fileurl;
+ var e = (yield);
+ is(e.type, "error", "failed successfully");
+ isnot(img.width, 120, "correct error width");
+ isnot(img.height, 90, "correct error height");
+*/
+ // Generate new fileurl and make sure it's different from the old
+ var oldFileurl = fileurl;
+ fileurl = URL.createObjectURL(imgFile);
+ isnot(fileurl, oldFileurl, "URL.createObjectURL generated the same url twice");
+
+ // Attempt to load an image in a different same-origin document
+ inner.src = innerSameSiteURI;
+ yield undefined;
+ inner.contentWindow.postMessage(JSON.stringify({img:fileurl}), "*");
+ var res = (yield);
+ is(res.type, "load", "loaded successfully");
+ is(res.width, 120, "correct width");
+ is(res.height, 90, "correct height");
+
+ // Attempt to load an image in a different cross-origin document
+ inner.src = innerCrossSiteURI;
+ yield undefined;
+ inner.contentWindow.postMessage(JSON.stringify({img:fileurl}), "*");
+ var res = (yield);
+ is(res.type, "error", "failed successfully");
+ isnot(res.width, 120, "correct error width");
+ isnot(res.height, 90, "correct error height");
+
+ // Attempt to load an audio in this document
+ fileurl = URL.createObjectURL(audioFile);
+ audio.src = fileurl;
+ var e = (yield);
+ is(e.type, "loadeddata", "loaded successfully");
+
+ // Revoke url and attempt to load a audio in this document
+ audio.src = "file_mozfiledataurl_audio.ogg";
+ is((yield).type, "loadeddata", "successfully reset audio");
+ URL.revokeObjectURL(fileurl);
+ todo(false, "urls need to act like 404s, not fail to parse");
+/* img.src = fileurl;
+ var e = (yield);
+ is(e.type, "error", "failed successfully");
+ isnot(img.width, 120, "correct error width");
+ isnot(img.height, 90, "correct error height");
+*/
+ // Generate new fileurl and make sure it's different from the old
+ var oldFileurl = fileurl;
+ fileurl = URL.createObjectURL(audioFile);
+ isnot(fileurl, oldFileurl, "URL.createObjectURL generated the same url twice");
+
+ // Attempt to load an audio in a different same-origin document
+ inner.src = innerSameSiteURI;
+ yield undefined;
+ inner.contentWindow.postMessage(JSON.stringify({audio:fileurl}), "*");
+ var res = (yield);
+ is(res.type, "loadeddata", "loaded successfully");
+
+ // Attempt to load an audio in a different cross-origin document
+ inner.src = innerCrossSiteURI;
+ yield undefined;
+ inner.contentWindow.postMessage(JSON.stringify({audio:fileurl}), "*");
+ var res = (yield);
+ is(res.type, "error", "failed successfully");
+
+ // Attempt to load a HTML document in an iframe in this document
+ iframe.onload = function() { gen.next(); };
+ iframe.src = "file_mozfiledataurl_doc.html";
+ yield undefined;
+ is(iframe.contentDocument.getElementsByTagName("p")[0].textContent,
+ "This here is a document!",
+ "iframe loaded successfully");
+ is(iframe.contentDocument.getElementById("img").width, 120,
+ "image in iframe width");
+ is(iframe.contentDocument.getElementById("img").height, 90,
+ "image in iframe height");
+
+ // Attempt to load a HTML document in an iframe in this document, using file url
+ fileurl = URL.createObjectURL(docFile);
+ iframe.src = fileurl;
+ yield undefined;
+ is(iframe.contentDocument.getElementsByTagName("p")[0].textContent,
+ "This here is a document!",
+ "iframe loaded successfully");
+ isnot(iframe.contentDocument.getElementById("img").width, 120,
+ "failed image in iframe width");
+ isnot(iframe.contentDocument.getElementById("img").height, 90,
+ "failed image in iframe height");
+
+ // Attempt to load a HTML document in an iframe in inner document
+ inner.src = innerSameSiteURI;
+ is((yield), "inner loaded", "correct gen.next()");
+ inner.contentWindow.postMessage(JSON.stringify({iframe:"file_mozfiledataurl_doc.html"}), "*");
+ var res = (yield);
+ is(res.type, "load", "loaded successfully");
+ is(res.text, "This here is a document!", "loaded successfully");
+ is(res.imgWidth, 120, "correct width");
+
+ // Attempt to load a HTML document in an iframe in inner document, using file url
+ inner.contentWindow.postMessage(JSON.stringify({iframe:fileurl}), "*");
+ var res = (yield);
+ is(res.type, "load", "loaded successfully");
+ is(res.text, "This here is a document!", "loaded successfully");
+ isnot(res.imgWidth, 120, "correct width");
+
+ // Attempt to load a HTML document in an iframe in inner cross-site document, using file url
+ inner.src = innerCrossSiteURI;
+ is((yield), "inner loaded", "correct gen.next()");
+ inner.contentWindow.postMessage(JSON.stringify({iframe:fileurl}), "*");
+ var res = (yield);
+ is(res.type, "error", "load failed successfully");
+
+ // Attempt to load file url using XHR
+ fileurl = URL.createObjectURL(xhrFile);
+ xhr = new XMLHttpRequest;
+ xhr.onload = function() { gen.next("XHR finished"); };
+ xhr.open("GET", fileurl);
+ xhr.send();
+ is((yield), "XHR finished", "correct gen.next()");
+ xhr.responseText == "Yarr, here be plaintext file, ya landlubber\n";
+
+ // Attempt to load file url using XHR in inner document
+ inner.src = innerSameSiteURI;
+ is((yield), "inner loaded", "correct gen.next()");
+ inner.contentWindow.postMessage(JSON.stringify({xhr:fileurl}), "*");
+ var res = (yield);
+ is(res.didThrow, undefined, "load successful");
+ is(res.text, "Yarr, here be plaintext file, ya landlubber\n", "load successful");
+
+ // Attempt to load file url using XHR
+ inner.src = innerCrossSiteURI;
+ is((yield), "inner loaded", "correct gen.next()");
+ inner.contentWindow.postMessage(JSON.stringify({xhr:fileurl}), "*");
+ var res = (yield);
+ is(res.didError, true, "load failed successfully");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/file/tests/test_nonascii_blob_url.html b/dom/file/tests/test_nonascii_blob_url.html
new file mode 100644
index 0000000000..1c6c833958
--- /dev/null
+++ b/dom/file/tests/test_nonascii_blob_url.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test blob URL for non-ascii domain</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="content"></div>
+<script class="testbody" type="text/javascript">
+
+var iframe = document.createElement('iframe');
+iframe.src = 'http://xn--exmple-cua.test/tests/dom/file/tests/file_nonascii_blob_url.html';
+iframe.onload = function() {
+ iframe.contentWindow.postMessage('hello world', '*');
+ onmessage = function(e) {
+ is(e.data, 'hello world', "Blob URL for non-ascii domain works");
+ SimpleTest.finish();
+ }
+}
+
+document.getElementById('content').appendChild(iframe);
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/file/tests/worker_blob_reading.js b/dom/file/tests/worker_blob_reading.js
new file mode 100644
index 0000000000..f57161c220
--- /dev/null
+++ b/dom/file/tests/worker_blob_reading.js
@@ -0,0 +1,26 @@
+importScripts("common_blob_reading.js");
+
+function info(message) {
+ postMessage({ type: "info", message });
+}
+
+function ok(a, message) {
+ postMessage({ type: "test", test: !!a, message });
+}
+
+function is(a, b, message) {
+ ok(a === b, message);
+}
+
+onmessage = function (e) {
+ self[e.data.func](e.data.blob, e.data.content).then(
+ () => {
+ postMessage({ type: "done" });
+ },
+ exc => {
+ dump(exc);
+ dump(exc.stack);
+ postMessage({ type: "error", message: exc.toString() });
+ }
+ );
+};
diff --git a/dom/file/tests/worker_bug1507893.js b/dom/file/tests/worker_bug1507893.js
new file mode 100644
index 0000000000..06fce2b2ef
--- /dev/null
+++ b/dom/file/tests/worker_bug1507893.js
@@ -0,0 +1,5 @@
+onmessage = e => {
+ fetch(e.data)
+ .then(r => r.blob())
+ .then(blob => postMessage(blob));
+};
diff --git a/dom/file/tests/worker_bug1742540.js b/dom/file/tests/worker_bug1742540.js
new file mode 100644
index 0000000000..c0330ccb56
--- /dev/null
+++ b/dom/file/tests/worker_bug1742540.js
@@ -0,0 +1,5 @@
+onmessage = e => {
+ let file = e.data.file;
+ let port = e.data.port;
+ port.postMessage(e.data.message);
+};
diff --git a/dom/file/tests/worker_fileReader.js b/dom/file/tests/worker_fileReader.js
new file mode 100644
index 0000000000..2e8408d1bb
--- /dev/null
+++ b/dom/file/tests/worker_fileReader.js
@@ -0,0 +1,30 @@
+importScripts("common_fileReader.js");
+
+function ok(a, msg) {
+ postMessage({ type: "check", msg, status: !!a });
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+onmessage = event => {
+ let p;
+
+ if (event.data.tests == "basic") {
+ p = runBasicTests(event.data.data);
+ } else if (event.data.tests == "encoding") {
+ p = runEncodingTests(event.data.data);
+ } else if (event.data.tests == "twice") {
+ p = runTwiceTests(event.data.data);
+ } else if (event.data.tests == "other") {
+ p = runOtherTests(event.data.data);
+ } else {
+ postMessage({ type: "error" });
+ return;
+ }
+
+ p.then(() => {
+ postMessage({ type: "finish" });
+ });
+};
diff --git a/dom/file/tests/xpcshell.ini b/dom/file/tests/xpcshell.ini
new file mode 100644
index 0000000000..2f817948d3
--- /dev/null
+++ b/dom/file/tests/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+
+[test_bloburi.js]
+[test_createFile.js]
+[test_ipc_messagemanager_blob.js]
+skip-if = os == "android"
+[test_agentcluster_bloburl.js]
+skip-if = os == "android"
diff --git a/dom/file/uri/BlobURL.cpp b/dom/file/uri/BlobURL.cpp
new file mode 100644
index 0000000000..b3afd60437
--- /dev/null
+++ b/dom/file/uri/BlobURL.cpp
@@ -0,0 +1,171 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsIClassInfoImpl.h"
+#include "nsIObjectInputStream.h"
+#include "nsIObjectOutputStream.h"
+
+#include "mozilla/dom/BlobURL.h"
+#include "mozilla/dom/BlobURLProtocolHandler.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+#include "mozilla/ipc/URIUtils.h"
+
+using namespace mozilla::dom;
+
+static NS_DEFINE_CID(kThisSimpleURIImplementationCID,
+ NS_THIS_SIMPLEURI_IMPLEMENTATION_CID);
+
+NS_IMPL_ADDREF_INHERITED(BlobURL, mozilla::net::nsSimpleURI)
+NS_IMPL_RELEASE_INHERITED(BlobURL, mozilla::net::nsSimpleURI)
+
+NS_IMPL_CLASSINFO(BlobURL, nullptr, nsIClassInfo::THREADSAFE,
+ NS_HOSTOBJECTURI_CID);
+// Empty CI getter. We only need nsIClassInfo for Serialization
+NS_IMPL_CI_INTERFACE_GETTER0(BlobURL)
+
+NS_INTERFACE_MAP_BEGIN(BlobURL)
+ if (aIID.Equals(kHOSTOBJECTURICID))
+ foundInterface = static_cast<nsIURI*>(this);
+ else if (aIID.Equals(kThisSimpleURIImplementationCID)) {
+ // Need to return explicitly here, because if we just set foundInterface
+ // to null the NS_INTERFACE_MAP_END_INHERITING will end up calling into
+ // nsSimplURI::QueryInterface and finding something for this CID.
+ *aInstancePtr = nullptr;
+ return NS_NOINTERFACE;
+ } else
+ NS_IMPL_QUERY_CLASSINFO(BlobURL)
+NS_INTERFACE_MAP_END_INHERITING(mozilla::net::nsSimpleURI)
+
+BlobURL::BlobURL() : mRevoked(false) {}
+
+// nsISerializable methods:
+
+NS_IMETHODIMP
+BlobURL::Read(nsIObjectInputStream* aStream) {
+ MOZ_ASSERT_UNREACHABLE("Use nsIURIMutator.read() instead");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult BlobURL::ReadPrivate(nsIObjectInputStream* aStream) {
+ nsresult rv = mozilla::net::nsSimpleURI::ReadPrivate(aStream);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aStream->ReadBoolean(&mRevoked);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BlobURL::Write(nsIObjectOutputStream* aStream) {
+ nsresult rv = mozilla::net::nsSimpleURI::Write(aStream);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aStream->WriteBoolean(mRevoked);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+BlobURL::Serialize(mozilla::ipc::URIParams& aParams) {
+ using namespace mozilla::ipc;
+
+ HostObjectURIParams hostParams;
+ URIParams simpleParams;
+
+ mozilla::net::nsSimpleURI::Serialize(simpleParams);
+ hostParams.simpleParams() = simpleParams;
+
+ hostParams.revoked() = mRevoked;
+
+ aParams = hostParams;
+}
+
+bool BlobURL::Deserialize(const mozilla::ipc::URIParams& aParams) {
+ using namespace mozilla::ipc;
+
+ if (aParams.type() != URIParams::THostObjectURIParams) {
+ NS_ERROR("Received unknown parameters from the other process!");
+ return false;
+ }
+
+ const HostObjectURIParams& hostParams = aParams.get_HostObjectURIParams();
+
+ if (!mozilla::net::nsSimpleURI::Deserialize(hostParams.simpleParams())) {
+ return false;
+ }
+
+ mRevoked = hostParams.revoked();
+ return true;
+}
+
+nsresult BlobURL::SetScheme(const nsACString& aScheme) {
+ // Disallow setting the scheme, since that could cause us to be associated
+ // with a different protocol handler.
+ return NS_ERROR_FAILURE;
+}
+
+// nsIURI methods:
+nsresult BlobURL::CloneInternal(
+ mozilla::net::nsSimpleURI::RefHandlingEnum aRefHandlingMode,
+ const nsACString& newRef, nsIURI** aClone) {
+ nsCOMPtr<nsIURI> simpleClone;
+ nsresult rv = mozilla::net::nsSimpleURI::CloneInternal(
+ aRefHandlingMode, newRef, getter_AddRefs(simpleClone));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifdef DEBUG
+ RefPtr<BlobURL> uriCheck;
+ rv = simpleClone->QueryInterface(kHOSTOBJECTURICID, getter_AddRefs(uriCheck));
+ MOZ_ASSERT(NS_SUCCEEDED(rv) && uriCheck);
+#endif
+
+ BlobURL* u = static_cast<BlobURL*>(simpleClone.get());
+ u->mRevoked = mRevoked;
+
+ simpleClone.forget(aClone);
+ return NS_OK;
+}
+
+/* virtual */
+nsresult BlobURL::EqualsInternal(
+ nsIURI* aOther, mozilla::net::nsSimpleURI::RefHandlingEnum aRefHandlingMode,
+ bool* aResult) {
+ if (!aOther) {
+ *aResult = false;
+ return NS_OK;
+ }
+
+ RefPtr<BlobURL> otherUri;
+ aOther->QueryInterface(kHOSTOBJECTURICID, getter_AddRefs(otherUri));
+ if (!otherUri) {
+ *aResult = false;
+ return NS_OK;
+ }
+
+ // Compare the member data that our base class knows about.
+ *aResult =
+ mozilla::net::nsSimpleURI::EqualsInternal(otherUri, aRefHandlingMode);
+
+ // We don't want to compare the revoked flag.
+ return NS_OK;
+}
+
+// Queries this list of interfaces. If none match, it queries mURI.
+NS_IMPL_NSIURIMUTATOR_ISUPPORTS(BlobURL::Mutator, nsIURISetters, nsIURIMutator,
+ nsISerializable, nsIBlobURLMutator)
+
+NS_IMETHODIMP
+BlobURL::Mutate(nsIURIMutator** aMutator) {
+ RefPtr<BlobURL::Mutator> mutator = new BlobURL::Mutator();
+ nsresult rv = mutator->InitFromURI(this);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ mutator.forget(aMutator);
+ return NS_OK;
+}
diff --git a/dom/file/uri/BlobURL.h b/dom/file/uri/BlobURL.h
new file mode 100644
index 0000000000..1e7a91daa4
--- /dev/null
+++ b/dom/file/uri/BlobURL.h
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_BlobURL_h
+#define mozilla_dom_BlobURL_h
+
+#include "nsCOMPtr.h"
+#include "nsISerializable.h"
+#include "nsSimpleURI.h"
+#include "prtime.h"
+
+#define NS_HOSTOBJECTURI_CID \
+ { \
+ 0xf5475c51, 0x59a7, 0x4757, { \
+ 0xb3, 0xd9, 0xe2, 0x11, 0xa9, 0x41, 0x08, 0x72 \
+ } \
+ }
+
+#define NS_IBLOBURLMUTATOR_IID \
+ { \
+ 0xf91e646d, 0xe87b, 0x485e, { \
+ 0xbb, 0xc8, 0x0e, 0x8a, 0x2e, 0xe9, 0x87, 0xa9 \
+ } \
+ }
+
+class NS_NO_VTABLE nsIBlobURLMutator : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IBLOBURLMUTATOR_IID)
+ NS_IMETHOD SetRevoked(bool aRevoked) = 0;
+};
+
+inline NS_DEFINE_CID(kHOSTOBJECTURICID, NS_HOSTOBJECTURI_CID);
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIBlobURLMutator, NS_IBLOBURLMUTATOR_IID)
+
+namespace mozilla::dom {
+
+/**
+ * These URIs refer to host objects with "blob" scheme.
+ */
+class BlobURL final : public mozilla::net::nsSimpleURI {
+ private:
+ BlobURL();
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSISERIALIZABLE
+
+ // Override CloneInternal() and EqualsInternal()
+ nsresult CloneInternal(RefHandlingEnum aRefHandlingMode,
+ const nsACString& newRef, nsIURI** aClone) override;
+ nsresult EqualsInternal(nsIURI* aOther, RefHandlingEnum aRefHandlingMode,
+ bool* aResult) override;
+ NS_IMETHOD_(void) Serialize(mozilla::ipc::URIParams& aParams) override;
+
+ // Override StartClone to hand back a BlobURL
+ mozilla::net::nsSimpleURI* StartClone(RefHandlingEnum refHandlingMode,
+ const nsACString& newRef) override {
+ BlobURL* url = new BlobURL();
+ SetRefOnClone(url, refHandlingMode, newRef);
+ return url;
+ }
+
+ bool Revoked() const { return mRevoked; }
+
+ NS_IMETHOD Mutate(nsIURIMutator** _retval) override;
+
+ private:
+ ~BlobURL() override = default;
+
+ nsresult SetScheme(const nsACString& aProtocol) override;
+ bool Deserialize(const mozilla::ipc::URIParams&);
+ nsresult ReadPrivate(nsIObjectInputStream* stream);
+
+ bool mRevoked;
+
+ public:
+ class Mutator final : public nsIURIMutator,
+ public BaseURIMutator<BlobURL>,
+ public nsIBlobURLMutator,
+ public nsISerializable {
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI)
+ NS_DEFINE_NSIMUTATOR_COMMON
+
+ NS_IMETHOD
+ Write(nsIObjectOutputStream* aOutputStream) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ [[nodiscard]] NS_IMETHOD Read(nsIObjectInputStream* aStream) override {
+ return InitFromInputStream(aStream);
+ }
+
+ NS_IMETHOD SetRevoked(bool aRevoked) override {
+ mURI->mRevoked = aRevoked;
+ return NS_OK;
+ }
+
+ Mutator() = default;
+
+ private:
+ ~Mutator() = default;
+
+ friend class BlobURL;
+ };
+
+ friend BaseURIMutator<BlobURL>;
+};
+
+#define NS_HOSTOBJECTURIMUTATOR_CID \
+ { \
+ 0xbbe50ef2, 0x80eb, 0x469d, { \
+ 0xb7, 0x0d, 0x02, 0x85, 0x82, 0x75, 0x38, 0x9f \
+ } \
+ }
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_BlobURL_h */
diff --git a/dom/file/uri/BlobURLChannel.cpp b/dom/file/uri/BlobURLChannel.cpp
new file mode 100644
index 0000000000..6a7d4f1be6
--- /dev/null
+++ b/dom/file/uri/BlobURLChannel.cpp
@@ -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/. */
+
+#include "BlobURLChannel.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/BlobURL.h"
+#include "mozilla/dom/BlobURLInputStream.h"
+
+using namespace mozilla::dom;
+
+BlobURLChannel::BlobURLChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo)
+ : mContentStreamOpened(false) {
+ SetURI(aURI);
+ SetOriginalURI(aURI);
+ SetLoadInfo(aLoadInfo);
+
+ // If we're sandboxed, make sure to clear any owner the channel
+ // might already have.
+ if (aLoadInfo && aLoadInfo->GetLoadingSandboxed()) {
+ SetOwner(nullptr);
+ }
+}
+
+BlobURLChannel::~BlobURLChannel() = default;
+
+NS_IMETHODIMP
+BlobURLChannel::SetContentType(const nsACString& aContentType) {
+ // If the blob type is empty, set the content type of the channel to the
+ // empty string.
+ if (aContentType.IsEmpty()) {
+ mContentType.Truncate();
+ return NS_OK;
+ }
+
+ return nsBaseChannel::SetContentType(aContentType);
+}
+
+nsresult BlobURLChannel::OpenContentStream(bool aAsync,
+ nsIInputStream** aResult,
+ nsIChannel** aChannel) {
+ if (mContentStreamOpened) {
+ return NS_ERROR_ALREADY_OPENED;
+ }
+
+ mContentStreamOpened = true;
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+
+ RefPtr<BlobURL> blobURL;
+ rv = uri->QueryInterface(kHOSTOBJECTURICID, getter_AddRefs(blobURL));
+
+ if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(!blobURL)) {
+ return NS_ERROR_MALFORMED_URI;
+ }
+
+ if (blobURL->Revoked()) {
+#ifdef MOZ_WIDGET_ANDROID
+ nsCOMPtr<nsILoadInfo> loadInfo;
+ GetLoadInfo(getter_AddRefs(loadInfo));
+ // if the channel was not triggered by the system principal,
+ // then we return here because the URL had been revoked
+ if (loadInfo && !loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) {
+ return NS_ERROR_MALFORMED_URI;
+ }
+#else
+ return NS_ERROR_MALFORMED_URI;
+#endif
+ }
+
+ nsCOMPtr<nsIInputStream> inputStream =
+ BlobURLInputStream::Create(this, blobURL);
+ if (NS_WARN_IF(!inputStream)) {
+ return NS_ERROR_MALFORMED_URI;
+ }
+
+ EnableSynthesizedProgressEvents(true);
+
+ inputStream.forget(aResult);
+
+ return NS_OK;
+}
diff --git a/dom/file/uri/BlobURLChannel.h b/dom/file/uri/BlobURLChannel.h
new file mode 100644
index 0000000000..232537171e
--- /dev/null
+++ b/dom/file/uri/BlobURLChannel.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_BlobURLChannel_h
+#define mozilla_dom_BlobURLChannel_h
+
+#include "nsBaseChannel.h"
+#include "nsCOMPtr.h"
+#include "nsIInputStream.h"
+
+class nsIURI;
+
+namespace mozilla::dom {
+
+class BlobImpl;
+
+class BlobURLChannel final : public nsBaseChannel {
+ public:
+ BlobURLChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo);
+
+ NS_IMETHOD SetContentType(const nsACString& aContentType) override;
+
+ private:
+ ~BlobURLChannel() override;
+
+ nsresult OpenContentStream(bool aAsync, nsIInputStream** aResult,
+ nsIChannel** aChannel) override;
+
+ bool mContentStreamOpened;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_BlobURLChannel_h */
diff --git a/dom/file/uri/BlobURLInputStream.cpp b/dom/file/uri/BlobURLInputStream.cpp
new file mode 100644
index 0000000000..7610281afc
--- /dev/null
+++ b/dom/file/uri/BlobURLInputStream.cpp
@@ -0,0 +1,589 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "BlobURLInputStream.h"
+#include "BlobURL.h"
+#include "BlobURLChannel.h"
+#include "BlobURLProtocolHandler.h"
+
+#include "mozilla/ScopeExit.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include "nsStreamUtils.h"
+#include "nsMimeTypes.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_ADDREF(BlobURLInputStream);
+NS_IMPL_RELEASE(BlobURLInputStream);
+
+NS_INTERFACE_MAP_BEGIN(BlobURLInputStream)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStream)
+ NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStream)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStreamLength)
+ NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStreamLength)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAsyncInputStream)
+NS_INTERFACE_MAP_END
+
+/* static */
+already_AddRefed<nsIInputStream> BlobURLInputStream::Create(
+ BlobURLChannel* const aChannel, BlobURL* const aBlobURL) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aChannel) || NS_WARN_IF(!aBlobURL)) {
+ return nullptr;
+ }
+
+ nsAutoCString spec;
+
+ nsresult rv = aBlobURL->GetSpec(spec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ return MakeAndAddRef<BlobURLInputStream>(aChannel, spec);
+}
+
+// from nsIInputStream interface
+NS_IMETHODIMP BlobURLInputStream::Close() {
+ return CloseWithStatus(NS_BASE_STREAM_CLOSED);
+}
+
+NS_IMETHODIMP BlobURLInputStream::Available(uint64_t* aLength) {
+ MutexAutoLock lock(mStateMachineMutex);
+
+ if (mState == State::ERROR) {
+ MOZ_ASSERT(NS_FAILED(mError));
+ return mError;
+ }
+
+ if (mState == State::CLOSED) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ if (mState == State::READY) {
+ MOZ_ASSERT(mAsyncInputStream);
+ return mAsyncInputStream->Available(aLength);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP BlobURLInputStream::StreamStatus() {
+ MutexAutoLock lock(mStateMachineMutex);
+
+ if (mState == State::ERROR) {
+ MOZ_ASSERT(NS_FAILED(mError));
+ return mError;
+ }
+
+ if (mState == State::CLOSED) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ if (mState == State::READY) {
+ MOZ_ASSERT(mAsyncInputStream);
+ return mAsyncInputStream->StreamStatus();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP BlobURLInputStream::Read(char* aBuffer, uint32_t aCount,
+ uint32_t* aReadCount) {
+ MutexAutoLock lock(mStateMachineMutex);
+ if (mState == State::ERROR) {
+ MOZ_ASSERT(NS_FAILED(mError));
+ return mError;
+ }
+
+ // Read() should not return NS_BASE_STREAM_CLOSED if stream is closed.
+ // A read count of 0 should indicate closed or consumed stream.
+ // See:
+ // https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/xpcom/io/nsIInputStream.idl#104
+ if (mState == State::CLOSED) {
+ *aReadCount = 0;
+ return NS_OK;
+ }
+
+ if (mState == State::READY) {
+ MOZ_ASSERT(mAsyncInputStream);
+ nsresult rv = mAsyncInputStream->Read(aBuffer, aCount, aReadCount);
+ if (NS_SUCCEEDED(rv) && aReadCount && !*aReadCount) {
+ mState = State::CLOSED;
+ ReleaseUnderlyingStream(lock);
+ }
+ return rv;
+ }
+
+ return NS_BASE_STREAM_WOULD_BLOCK;
+}
+
+NS_IMETHODIMP BlobURLInputStream::ReadSegments(nsWriteSegmentFun aWriter,
+ void* aClosure, uint32_t aCount,
+ uint32_t* aResult) {
+ // This means the caller will have to wrap the stream in an
+ // nsBufferedInputStream in order to use ReadSegments
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP BlobURLInputStream::IsNonBlocking(bool* aNonBlocking) {
+ *aNonBlocking = true;
+ return NS_OK;
+}
+
+// from nsIAsyncInputStream interface
+NS_IMETHODIMP BlobURLInputStream::CloseWithStatus(nsresult aStatus) {
+ MutexAutoLock lock(mStateMachineMutex);
+ if (mState == State::READY) {
+ MOZ_ASSERT(mAsyncInputStream);
+ mAsyncInputStream->CloseWithStatus(aStatus);
+ }
+
+ mState = State::CLOSED;
+ ReleaseUnderlyingStream(lock);
+ return NS_OK;
+}
+
+NS_IMETHODIMP BlobURLInputStream::AsyncWait(nsIInputStreamCallback* aCallback,
+ uint32_t aFlags,
+ uint32_t aRequestedCount,
+ nsIEventTarget* aEventTarget) {
+ MutexAutoLock lock(mStateMachineMutex);
+
+ if (mState == State::ERROR) {
+ MOZ_ASSERT(NS_FAILED(mError));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Pre-empting a valid callback with another is not allowed.
+ if (NS_WARN_IF(mAsyncWaitCallback && aCallback &&
+ mAsyncWaitCallback != aCallback)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mAsyncWaitTarget = aEventTarget;
+ mAsyncWaitRequestedCount = aRequestedCount;
+ mAsyncWaitFlags = aFlags;
+ mAsyncWaitCallback = aCallback;
+
+ if (mState == State::INITIAL) {
+ mState = State::WAITING;
+ // RetrieveBlobData will execute NotifyWWaitTarget() when retrieve succeeds
+ // or fails
+ if (NS_IsMainThread()) {
+ RetrieveBlobData(lock);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod(
+ "BlobURLInputStream::CallRetrieveBlobData", this,
+ &BlobURLInputStream::CallRetrieveBlobData);
+ NS_DispatchToMainThread(runnable.forget(), NS_DISPATCH_NORMAL);
+ return NS_OK;
+ }
+
+ if (mState == State::WAITING) {
+ // RetrieveBlobData is already in progress and will execute
+ // NotifyWaitTargets when retrieve succeeds or fails
+ return NS_OK;
+ }
+
+ if (mState == State::READY) {
+ // Ask the blob's input stream if reading is possible or not
+ return mAsyncInputStream->AsyncWait(
+ mAsyncWaitCallback ? this : nullptr, mAsyncWaitFlags,
+ mAsyncWaitRequestedCount, mAsyncWaitTarget);
+ }
+
+ MOZ_ASSERT(mState == State::CLOSED);
+ NotifyWaitTargets(lock);
+ return NS_OK;
+}
+
+// from nsIInputStreamLength interface
+NS_IMETHODIMP BlobURLInputStream::Length(int64_t* aLength) {
+ MutexAutoLock lock(mStateMachineMutex);
+
+ if (mState == State::CLOSED) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ if (mState == State::ERROR) {
+ MOZ_ASSERT(NS_FAILED(mError));
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mState == State::READY) {
+ *aLength = mBlobSize;
+ return NS_OK;
+ }
+ return NS_BASE_STREAM_WOULD_BLOCK;
+}
+
+// from nsIAsyncInputStreamLength interface
+NS_IMETHODIMP BlobURLInputStream::AsyncLengthWait(
+ nsIInputStreamLengthCallback* aCallback, nsIEventTarget* aEventTarget) {
+ MutexAutoLock lock(mStateMachineMutex);
+
+ if (mState == State::ERROR) {
+ MOZ_ASSERT(NS_FAILED(mError));
+ return mError;
+ }
+
+ // Pre-empting a valid callback with another is not allowed.
+ if (mAsyncLengthWaitCallback && aCallback) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mAsyncLengthWaitTarget = aEventTarget;
+ mAsyncLengthWaitCallback = aCallback;
+
+ if (mState == State::INITIAL) {
+ mState = State::WAITING;
+ // RetrieveBlobData will execute NotifyWWaitTarget() when retrieve succeeds
+ // or fails
+ if (NS_IsMainThread()) {
+ RetrieveBlobData(lock);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod(
+ "BlobURLInputStream::CallRetrieveBlobData", this,
+ &BlobURLInputStream::CallRetrieveBlobData);
+ NS_DispatchToMainThread(runnable.forget(), NS_DISPATCH_NORMAL);
+ return NS_OK;
+ }
+
+ if (mState == State::WAITING) {
+ // RetrieveBlobData is already in progress and will execute
+ // NotifyWaitTargets when retrieve succeeds or fails
+ return NS_OK;
+ }
+
+ // Since here the state must be READY (in which case the size of the blob is
+ // already known) or CLOSED, callback can be called immediately
+ NotifyWaitTargets(lock);
+ return NS_OK;
+}
+
+// from nsIInputStreamCallback interface
+NS_IMETHODIMP BlobURLInputStream::OnInputStreamReady(
+ nsIAsyncInputStream* aStream) {
+ nsCOMPtr<nsIInputStreamCallback> callback;
+
+ {
+ MutexAutoLock lock(mStateMachineMutex);
+ MOZ_ASSERT_IF(mAsyncInputStream, aStream == mAsyncInputStream);
+
+ // aborted in the meantime
+ if (!mAsyncWaitCallback) {
+ return NS_OK;
+ }
+
+ mAsyncWaitCallback.swap(callback);
+ mAsyncWaitTarget = nullptr;
+ }
+
+ MOZ_ASSERT(callback);
+ return callback->OnInputStreamReady(this);
+}
+
+// from nsIInputStreamLengthCallback interface
+NS_IMETHODIMP BlobURLInputStream::OnInputStreamLengthReady(
+ nsIAsyncInputStreamLength* aStream, int64_t aLength) {
+ nsCOMPtr<nsIInputStreamLengthCallback> callback;
+ {
+ MutexAutoLock lock(mStateMachineMutex);
+
+ // aborted in the meantime
+ if (!mAsyncLengthWaitCallback) {
+ return NS_OK;
+ }
+
+ mAsyncLengthWaitCallback.swap(callback);
+ mAsyncLengthWaitCallback = nullptr;
+ }
+
+ return callback->OnInputStreamLengthReady(this, aLength);
+}
+
+// private:
+BlobURLInputStream::~BlobURLInputStream() {
+ if (mChannel) {
+ NS_ReleaseOnMainThread("BlobURLInputStream::mChannel", mChannel.forget());
+ }
+}
+
+BlobURLInputStream::BlobURLInputStream(BlobURLChannel* const aChannel,
+ nsACString& aBlobURLSpec)
+ : mChannel(aChannel),
+ mBlobURLSpec(std::move(aBlobURLSpec)),
+ mStateMachineMutex("BlobURLInputStream::mStateMachineMutex"),
+ mState(State::INITIAL),
+ mError(NS_OK),
+ mBlobSize(-1),
+ mAsyncWaitFlags(),
+ mAsyncWaitRequestedCount() {}
+
+void BlobURLInputStream::WaitOnUnderlyingStream(
+ const MutexAutoLock& aProofOfLock) {
+ if (mAsyncWaitCallback || mAsyncWaitTarget) {
+ // AsyncWait should be called on the underlying stream
+ mAsyncInputStream->AsyncWait(mAsyncWaitCallback ? this : nullptr,
+ mAsyncWaitFlags, mAsyncWaitRequestedCount,
+ mAsyncWaitTarget);
+ }
+
+ if (mAsyncLengthWaitCallback || mAsyncLengthWaitTarget) {
+ // AsyncLengthWait should be called on the underlying stream
+ nsCOMPtr<nsIAsyncInputStreamLength> asyncStreamLength =
+ do_QueryInterface(mAsyncInputStream);
+ if (asyncStreamLength) {
+ asyncStreamLength->AsyncLengthWait(
+ mAsyncLengthWaitCallback ? this : nullptr, mAsyncLengthWaitTarget);
+ }
+ }
+}
+
+void BlobURLInputStream::CallRetrieveBlobData() {
+ MutexAutoLock lock(mStateMachineMutex);
+ RetrieveBlobData(lock);
+}
+
+void BlobURLInputStream::RetrieveBlobData(const MutexAutoLock& aProofOfLock) {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+
+ MOZ_ASSERT(mState == State::WAITING);
+
+ auto cleanupOnEarlyExit = MakeScopeExit([&] {
+ mState = State::ERROR;
+ mError = NS_ERROR_FAILURE;
+ NS_ReleaseOnMainThread("BlobURLInputStream::mChannel", mChannel.forget());
+ NotifyWaitTargets(aProofOfLock);
+ });
+
+ nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo();
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal;
+ nsCOMPtr<nsIPrincipal> loadingPrincipal;
+ if (NS_WARN_IF(NS_FAILED(loadInfo->GetTriggeringPrincipal(
+ getter_AddRefs(triggeringPrincipal)))) ||
+ NS_WARN_IF(!triggeringPrincipal)) {
+ NS_WARNING("Failed to get owning channel's triggering principal");
+ return;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(
+ loadInfo->GetLoadingPrincipal(getter_AddRefs(loadingPrincipal))))) {
+ NS_WARNING("Failed to get owning channel's loading principal");
+ return;
+ }
+
+ Maybe<nsID> agentClusterId;
+ Maybe<ClientInfo> clientInfo = loadInfo->GetClientInfo();
+ if (clientInfo.isSome()) {
+ agentClusterId = clientInfo->AgentClusterId();
+ }
+
+ if (XRE_IsParentProcess() || !BlobURLSchemeIsHTTPOrHTTPS(mBlobURLSpec)) {
+ RefPtr<BlobImpl> blobImpl;
+
+ // Since revoked blobs are also retrieved, it is possible that the blob no
+ // longer exists (due to the 5 second timeout) when execution reaches here
+ if (!BlobURLProtocolHandler::GetDataEntry(
+ mBlobURLSpec, getter_AddRefs(blobImpl), loadingPrincipal,
+ triggeringPrincipal, loadInfo->GetOriginAttributes(),
+ loadInfo->GetInnerWindowID(), agentClusterId,
+ true /* AlsoIfRevoked */)) {
+ NS_WARNING("Failed to get data entry principal. URL revoked?");
+ return;
+ }
+
+ if (NS_WARN_IF(
+ NS_FAILED(StoreBlobImplStream(blobImpl.forget(), aProofOfLock)))) {
+ return;
+ }
+
+ mState = State::READY;
+
+ // By design, execution can only reach here when a caller has called
+ // AsyncWait or AsyncLengthWait on this stream. The underlying stream is
+ // valid, but the caller should not be informed until that stream has data
+ // to read or it is closed.
+ WaitOnUnderlyingStream(aProofOfLock);
+
+ cleanupOnEarlyExit.release();
+ return;
+ }
+
+ ContentChild* contentChild{ContentChild::GetSingleton()};
+ MOZ_ASSERT(contentChild);
+
+ const RefPtr<BlobURLInputStream> self = this;
+
+ cleanupOnEarlyExit.release();
+
+ contentChild
+ ->SendBlobURLDataRequest(mBlobURLSpec, triggeringPrincipal,
+ loadingPrincipal,
+ loadInfo->GetOriginAttributes(),
+ loadInfo->GetInnerWindowID(), agentClusterId)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self](const BlobURLDataRequestResult& aResult) {
+ MutexAutoLock lock(self->mStateMachineMutex);
+ if (aResult.type() == BlobURLDataRequestResult::TIPCBlob) {
+ if (self->mState == State::WAITING) {
+ RefPtr<BlobImpl> blobImpl =
+ IPCBlobUtils::Deserialize(aResult.get_IPCBlob());
+ if (blobImpl && self->StoreBlobImplStream(blobImpl.forget(),
+ lock) == NS_OK) {
+ self->mState = State::READY;
+ // By design, execution can only reach here when a caller has
+ // called AsyncWait or AsyncLengthWait on this stream. The
+ // underlying stream is valid, but the caller should not be
+ // informed until that stream has data to read or it is
+ // closed.
+ self->WaitOnUnderlyingStream(lock);
+ return;
+ }
+ } else {
+ MOZ_ASSERT(self->mState == State::CLOSED);
+ // Callback can be called immediately
+ self->NotifyWaitTargets(lock);
+ return;
+ }
+ }
+ NS_WARNING("Blob data was not retrieved!");
+ self->mState = State::ERROR;
+ self->mError = aResult.type() == BlobURLDataRequestResult::Tnsresult
+ ? aResult.get_nsresult()
+ : NS_ERROR_FAILURE;
+ NS_ReleaseOnMainThread("BlobURLInputStream::mChannel",
+ self->mChannel.forget());
+ self->NotifyWaitTargets(lock);
+ },
+ [self](mozilla::ipc::ResponseRejectReason aReason) {
+ MutexAutoLock lock(self->mStateMachineMutex);
+ NS_WARNING("IPC call to SendBlobURLDataRequest failed!");
+ self->mState = State::ERROR;
+ self->mError = NS_ERROR_FAILURE;
+ NS_ReleaseOnMainThread("BlobURLInputStream::mChannel",
+ self->mChannel.forget());
+ self->NotifyWaitTargets(lock);
+ });
+}
+
+nsresult BlobURLInputStream::StoreBlobImplStream(
+ already_AddRefed<BlobImpl> aBlobImpl, const MutexAutoLock& aProofOfLock) {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+ const RefPtr<BlobImpl> blobImpl = aBlobImpl;
+ nsAutoString blobContentType;
+ nsAutoCString channelContentType;
+
+ blobImpl->GetType(blobContentType);
+ mChannel->GetContentType(channelContentType);
+ // A empty content type is the correct channel content type in the case of a
+ // fetch of a blob where the type was not set. It is invalid in others cases
+ // such as a XHR (See https://xhr.spec.whatwg.org/#response-mime-type). The
+ // XMLHttpRequestMainThread will set the channel content type to the correct
+ // fallback value before this point, so we need to be careful to only override
+ // it when the blob type is valid.
+ if (!blobContentType.IsEmpty() ||
+ channelContentType.EqualsLiteral(UNKNOWN_CONTENT_TYPE)) {
+ mChannel->SetContentType(NS_ConvertUTF16toUTF8(blobContentType));
+ }
+
+ auto cleanupOnExit = MakeScopeExit([&] { mChannel = nullptr; });
+
+ if (blobImpl->IsFile()) {
+ nsAutoString filename;
+ blobImpl->GetName(filename);
+
+ // Don't overwrite existing name.
+ nsString ignored;
+ bool hasName =
+ NS_SUCCEEDED(mChannel->GetContentDispositionFilename(ignored));
+
+ if (!filename.IsEmpty() && !hasName) {
+ mChannel->SetContentDispositionFilename(filename);
+ }
+ }
+
+ mozilla::ErrorResult errorResult;
+
+ mBlobSize = blobImpl->GetSize(errorResult);
+
+ if (NS_WARN_IF(errorResult.Failed())) {
+ return errorResult.StealNSResult();
+ }
+
+ mChannel->SetContentLength(mBlobSize);
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ blobImpl->CreateInputStream(getter_AddRefs(inputStream), errorResult);
+
+ if (NS_WARN_IF(errorResult.Failed())) {
+ return errorResult.StealNSResult();
+ }
+
+ if (NS_WARN_IF(!inputStream)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv = NS_MakeAsyncNonBlockingInputStream(
+ inputStream.forget(), getter_AddRefs(mAsyncInputStream));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(!mAsyncInputStream)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return NS_OK;
+}
+
+void BlobURLInputStream::NotifyWaitTargets(const MutexAutoLock& aProofOfLock) {
+ if (mAsyncWaitCallback) {
+ auto callback = mAsyncWaitTarget
+ ? NS_NewInputStreamReadyEvent(
+ "BlobURLInputStream::OnInputStreamReady",
+ mAsyncWaitCallback, mAsyncWaitTarget)
+ : mAsyncWaitCallback;
+
+ mAsyncWaitCallback = nullptr;
+ mAsyncWaitTarget = nullptr;
+ callback->OnInputStreamReady(this);
+ }
+
+ if (mAsyncLengthWaitCallback) {
+ const RefPtr<BlobURLInputStream> self = this;
+ nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
+ "BlobURLInputStream::OnInputStreamLengthReady", [self] {
+ self->mAsyncLengthWaitCallback->OnInputStreamLengthReady(
+ self, self->mBlobSize);
+ });
+
+ mAsyncLengthWaitCallback = nullptr;
+
+ if (mAsyncLengthWaitTarget) {
+ mAsyncLengthWaitTarget->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ mAsyncLengthWaitTarget = nullptr;
+ } else {
+ runnable->Run();
+ }
+ }
+}
+
+void BlobURLInputStream::ReleaseUnderlyingStream(
+ const MutexAutoLock& aProofOfLock) {
+ mAsyncInputStream = nullptr;
+ mBlobSize = -1;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/uri/BlobURLInputStream.h b/dom/file/uri/BlobURLInputStream.h
new file mode 100644
index 0000000000..b9215e5ffd
--- /dev/null
+++ b/dom/file/uri/BlobURLInputStream.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_BlobURLInputStream_h
+#define mozilla_dom_BlobURLInputStream_h
+
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/Mutex.h"
+#include "nsCOMPtr.h"
+#include "nsIAsyncInputStream.h"
+#include "nsIInputStreamLength.h"
+
+namespace mozilla::dom {
+
+class BlobURL;
+class BlobURLChannel;
+class BlobURLInputStream final : public nsIAsyncInputStream,
+ public nsIInputStreamLength,
+ public nsIAsyncInputStreamLength,
+ public nsIInputStreamCallback,
+ public nsIInputStreamLengthCallback {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIINPUTSTREAM
+ NS_DECL_NSIASYNCINPUTSTREAM
+ NS_DECL_NSIINPUTSTREAMLENGTH
+ NS_DECL_NSIASYNCINPUTSTREAMLENGTH
+ NS_DECL_NSIINPUTSTREAMCALLBACK
+ NS_DECL_NSIINPUTSTREAMLENGTHCALLBACK
+
+ static already_AddRefed<nsIInputStream> Create(BlobURLChannel* const aChannel,
+ BlobURL* const aBlobURL);
+
+ BlobURLInputStream(BlobURLChannel* const aChannel, nsACString& aBlobURLSpec);
+
+ private:
+ enum class State { INITIAL, READY, WAITING, CLOSED, ERROR };
+
+ ~BlobURLInputStream();
+
+ void WaitOnUnderlyingStream(const MutexAutoLock& aProofOfLock);
+
+ // This method should only be used to call RetrieveBlobData in a different
+ // thread
+ void CallRetrieveBlobData();
+
+ void RetrieveBlobData(const MutexAutoLock& aProofOfLock);
+
+ nsresult StoreBlobImplStream(already_AddRefed<BlobImpl> aBlobImpl,
+ const MutexAutoLock& aProofOfLock);
+ void NotifyWaitTargets(const MutexAutoLock& aProofOfLock);
+ void ReleaseUnderlyingStream(const MutexAutoLock& aProofOfLock);
+
+ RefPtr<BlobURLChannel> mChannel;
+ const nsCString mBlobURLSpec;
+
+ // Non-recursive mutex introduced in order to guard access to mState, mError
+ // and mAsyncInputStream
+ Mutex mStateMachineMutex MOZ_UNANNOTATED;
+ State mState;
+ // Stores the error code if stream is in error state
+ nsresult mError;
+
+ int64_t mBlobSize;
+
+ nsCOMPtr<nsIAsyncInputStream> mAsyncInputStream;
+ nsCOMPtr<nsIInputStreamCallback> mAsyncWaitCallback;
+ nsCOMPtr<nsIEventTarget> mAsyncWaitTarget;
+ uint32_t mAsyncWaitFlags;
+ uint32_t mAsyncWaitRequestedCount;
+
+ nsCOMPtr<nsIInputStreamLengthCallback> mAsyncLengthWaitCallback;
+ nsCOMPtr<nsIEventTarget> mAsyncLengthWaitTarget;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_BlobURLInputStream_h */
diff --git a/dom/file/uri/BlobURLProtocolHandler.cpp b/dom/file/uri/BlobURLProtocolHandler.cpp
new file mode 100644
index 0000000000..756ce01544
--- /dev/null
+++ b/dom/file/uri/BlobURLProtocolHandler.cpp
@@ -0,0 +1,990 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "BlobURLProtocolHandler.h"
+#include "BlobURLChannel.h"
+#include "mozilla/dom/BlobURL.h"
+
+#include "mozilla/dom/ChromeUtils.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Exceptions.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include "mozilla/dom/MediaSource.h"
+#include "mozilla/ipc/IPCStreamUtils.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/ScopeExit.h"
+#include "nsClassHashtable.h"
+#include "nsContentUtils.h"
+#include "nsError.h"
+#include "nsIAsyncShutdown.h"
+#include "nsIDUtils.h"
+#include "nsIException.h" // for nsIStackFrame
+#include "nsIMemoryReporter.h"
+#include "nsIPrincipal.h"
+#include "nsIUUIDGenerator.h"
+#include "nsNetUtil.h"
+#include "nsReadableUtils.h"
+
+#define RELEASING_TIMER 5000
+
+namespace mozilla {
+
+using namespace ipc;
+
+namespace dom {
+
+// -----------------------------------------------------------------------
+// Hash table
+struct DataInfo {
+ enum ObjectType { eBlobImpl, eMediaSource };
+
+ DataInfo(mozilla::dom::BlobImpl* aBlobImpl, nsIPrincipal* aPrincipal,
+ const Maybe<nsID>& aAgentClusterId)
+ : mObjectType(eBlobImpl),
+ mBlobImpl(aBlobImpl),
+ mPrincipal(aPrincipal),
+ mAgentClusterId(aAgentClusterId),
+ mRevoked(false) {
+ MOZ_ASSERT(aPrincipal);
+ }
+
+ DataInfo(MediaSource* aMediaSource, nsIPrincipal* aPrincipal,
+ const Maybe<nsID>& aAgentClusterId)
+ : mObjectType(eMediaSource),
+ mMediaSource(aMediaSource),
+ mPrincipal(aPrincipal),
+ mAgentClusterId(aAgentClusterId),
+ mRevoked(false) {
+ MOZ_ASSERT(aPrincipal);
+ }
+
+ ObjectType mObjectType;
+
+ RefPtr<BlobImpl> mBlobImpl;
+ RefPtr<MediaSource> mMediaSource;
+
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ Maybe<nsID> mAgentClusterId;
+
+ nsCString mStack;
+
+ // When a blobURL is revoked, we keep it alive for RELEASING_TIMER
+ // milliseconds in order to support pending operations such as navigation,
+ // download and so on.
+ bool mRevoked;
+};
+
+// The mutex is locked whenever gDataTable is changed, or if gDataTable
+// is accessed off-main-thread.
+static StaticMutex sMutex MOZ_UNANNOTATED;
+
+// All changes to gDataTable must happen on the main thread, while locking
+// sMutex. Reading from gDataTable on the main thread may happen without
+// locking, since no changes are possible. Reading it from another thread
+// must also lock sMutex to prevent data races.
+static nsClassHashtable<nsCStringHashKey, mozilla::dom::DataInfo>* gDataTable;
+
+static mozilla::dom::DataInfo* GetDataInfo(const nsACString& aUri,
+ bool aAlsoIfRevoked = false) {
+ if (!gDataTable) {
+ return nullptr;
+ }
+
+ // Let's remove any fragment from this URI.
+ int32_t fragmentPos = aUri.FindChar('#');
+
+ mozilla::dom::DataInfo* res;
+ if (fragmentPos < 0) {
+ res = gDataTable->Get(aUri);
+ } else {
+ res = gDataTable->Get(StringHead(aUri, fragmentPos));
+ }
+
+ if (!aAlsoIfRevoked && res && res->mRevoked) {
+ return nullptr;
+ }
+
+ return res;
+}
+
+static mozilla::dom::DataInfo* GetDataInfoFromURI(nsIURI* aURI,
+ bool aAlsoIfRevoked = false) {
+ if (!aURI) {
+ return nullptr;
+ }
+
+ nsCString spec;
+ nsresult rv = aURI->GetSpec(spec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ return GetDataInfo(spec, aAlsoIfRevoked);
+}
+
+// Memory reporting for the hash table.
+void BroadcastBlobURLRegistration(const nsACString& aURI,
+ mozilla::dom::BlobImpl* aBlobImpl,
+ nsIPrincipal* aPrincipal,
+ const Maybe<nsID>& aAgentClusterId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aBlobImpl);
+ MOZ_ASSERT(aPrincipal);
+
+ if (XRE_IsParentProcess()) {
+ dom::ContentParent::BroadcastBlobURLRegistration(
+ aURI, aBlobImpl, aPrincipal, aAgentClusterId);
+ return;
+ }
+
+ IPCBlob ipcBlob;
+ nsresult rv = IPCBlobUtils::Serialize(aBlobImpl, ipcBlob);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ dom::ContentChild* cc = dom::ContentChild::GetSingleton();
+ (void)NS_WARN_IF(!cc->SendStoreAndBroadcastBlobURLRegistration(
+ nsCString(aURI), ipcBlob, aPrincipal, aAgentClusterId));
+}
+
+void BroadcastBlobURLUnregistration(const nsCString& aURI,
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (XRE_IsParentProcess()) {
+ dom::ContentParent::BroadcastBlobURLUnregistration(aURI, aPrincipal);
+ return;
+ }
+
+ dom::ContentChild* cc = dom::ContentChild::GetSingleton();
+ if (cc) {
+ (void)NS_WARN_IF(
+ !cc->SendUnstoreAndBroadcastBlobURLUnregistration(aURI, aPrincipal));
+ }
+}
+
+class BlobURLsReporter final : public nsIMemoryReporter {
+ public:
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD CollectReports(nsIHandleReportCallback* aCallback,
+ nsISupports* aData, bool aAnonymize) override {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "without locking gDataTable is main-thread only");
+ if (!gDataTable) {
+ return NS_OK;
+ }
+
+ nsTHashMap<nsPtrHashKey<mozilla::dom::BlobImpl>, uint32_t> refCounts;
+
+ // Determine number of URLs per mozilla::dom::BlobImpl, to handle the case
+ // where it's > 1.
+ for (const auto& entry : *gDataTable) {
+ if (entry.GetWeak()->mObjectType != mozilla::dom::DataInfo::eBlobImpl) {
+ continue;
+ }
+
+ mozilla::dom::BlobImpl* blobImpl = entry.GetWeak()->mBlobImpl;
+ MOZ_ASSERT(blobImpl);
+
+ refCounts.LookupOrInsert(blobImpl, 0) += 1;
+ }
+
+ for (const auto& entry : *gDataTable) {
+ nsCStringHashKey::KeyType key = entry.GetKey();
+ mozilla::dom::DataInfo* info = entry.GetWeak();
+
+ if (entry.GetWeak()->mObjectType == mozilla::dom::DataInfo::eBlobImpl) {
+ mozilla::dom::BlobImpl* blobImpl = entry.GetWeak()->mBlobImpl;
+ MOZ_ASSERT(blobImpl);
+
+ constexpr auto desc =
+ "A blob URL allocated with URL.createObjectURL; the referenced "
+ "blob cannot be freed until all URLs for it have been explicitly "
+ "invalidated with URL.revokeObjectURL."_ns;
+ nsAutoCString path, url, owner, specialDesc;
+ uint64_t size = 0;
+ uint32_t refCount = 1;
+ DebugOnly<bool> blobImplWasCounted;
+
+ blobImplWasCounted = refCounts.Get(blobImpl, &refCount);
+ MOZ_ASSERT(blobImplWasCounted);
+ MOZ_ASSERT(refCount > 0);
+
+ bool isMemoryFile = blobImpl->IsMemoryFile();
+
+ if (isMemoryFile) {
+ ErrorResult rv;
+ size = blobImpl->GetSize(rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ size = 0;
+ }
+ }
+
+ path = isMemoryFile ? "memory-blob-urls/" : "file-blob-urls/";
+ BuildPath(path, key, info, aAnonymize);
+
+ if (refCount > 1) {
+ nsAutoCString addrStr;
+
+ addrStr = "0x";
+ addrStr.AppendInt((uint64_t)(mozilla::dom::BlobImpl*)blobImpl, 16);
+
+ path += " ";
+ path.AppendInt(refCount);
+ path += "@";
+ path += addrStr;
+
+ specialDesc = desc;
+ specialDesc += "\n\nNOTE: This blob (address ";
+ specialDesc += addrStr;
+ specialDesc += ") has ";
+ specialDesc.AppendInt(refCount);
+ specialDesc += " URLs.";
+ if (isMemoryFile) {
+ specialDesc += " Its size is divided ";
+ specialDesc += refCount > 2 ? "among" : "between";
+ specialDesc += " them in this report.";
+ }
+ }
+
+ const nsACString& descString =
+ specialDesc.IsEmpty() ? static_cast<const nsACString&>(desc)
+ : static_cast<const nsACString&>(specialDesc);
+ if (isMemoryFile) {
+ aCallback->Callback(""_ns, path, KIND_OTHER, UNITS_BYTES,
+ size / refCount, descString, aData);
+ } else {
+ aCallback->Callback(""_ns, path, KIND_OTHER, UNITS_COUNT, 1,
+ descString, aData);
+ }
+ continue;
+ }
+
+ // Just report the path for the MediaSource.
+ nsAutoCString path;
+ path = "media-source-urls/";
+ BuildPath(path, key, info, aAnonymize);
+
+ constexpr auto desc =
+ "An object URL allocated with URL.createObjectURL; the referenced "
+ "data cannot be freed until all URLs for it have been explicitly "
+ "invalidated with URL.revokeObjectURL."_ns;
+
+ aCallback->Callback(""_ns, path, KIND_OTHER, UNITS_COUNT, 1, desc, aData);
+ }
+
+ return NS_OK;
+ }
+
+ // Initialize info->mStack to record JS stack info, if enabled.
+ // The string generated here is used in ReportCallback, below.
+ static void GetJSStackForBlob(mozilla::dom::DataInfo* aInfo) {
+ nsCString& stack = aInfo->mStack;
+ MOZ_ASSERT(stack.IsEmpty());
+ const uint32_t maxFrames =
+ Preferences::GetUint("memory.blob_report.stack_frames");
+
+ if (maxFrames == 0) {
+ return;
+ }
+
+ nsCOMPtr<nsIStackFrame> frame = dom::GetCurrentJSStack(maxFrames);
+
+ nsAutoCString origin;
+
+ aInfo->mPrincipal->GetPrePath(origin);
+
+ // If we got a frame, we better have a current JSContext. This is cheating
+ // a bit; ideally we'd have our caller pass in a JSContext, or have
+ // GetCurrentJSStack() hand out the JSContext it found.
+ JSContext* cx = frame ? nsContentUtils::GetCurrentJSContext() : nullptr;
+
+ while (frame) {
+ nsString fileNameUTF16;
+ frame->GetFilename(cx, fileNameUTF16);
+
+ int32_t lineNumber = frame->GetLineNumber(cx);
+
+ if (!fileNameUTF16.IsEmpty()) {
+ NS_ConvertUTF16toUTF8 fileName(fileNameUTF16);
+ stack += "js(";
+ if (!origin.IsEmpty()) {
+ // Make the file name root-relative for conciseness if possible.
+ const char* originData;
+ uint32_t originLen;
+
+ originLen = origin.GetData(&originData);
+ // If fileName starts with origin + "/", cut up to that "/".
+ if (fileName.Length() >= originLen + 1 &&
+ memcmp(fileName.get(), originData, originLen) == 0 &&
+ fileName[originLen] == '/') {
+ fileName.Cut(0, originLen);
+ }
+ }
+ fileName.ReplaceChar('/', '\\');
+ stack += fileName;
+ if (lineNumber > 0) {
+ stack += ", line=";
+ stack.AppendInt(lineNumber);
+ }
+ stack += ")/";
+ }
+
+ frame = frame->GetCaller(cx);
+ }
+ }
+
+ private:
+ ~BlobURLsReporter() = default;
+
+ static void BuildPath(nsAutoCString& path, nsCStringHashKey::KeyType aKey,
+ mozilla::dom::DataInfo* aInfo, bool anonymize) {
+ nsAutoCString url, owner;
+ aInfo->mPrincipal->GetAsciiSpec(owner);
+ if (!owner.IsEmpty()) {
+ owner.ReplaceChar('/', '\\');
+ path += "owner(";
+ if (anonymize) {
+ path += "<anonymized>";
+ } else {
+ path += owner;
+ }
+ path += ")";
+ } else {
+ path += "owner unknown";
+ }
+ path += "/";
+ if (anonymize) {
+ path += "<anonymized-stack>";
+ } else {
+ path += aInfo->mStack;
+ }
+ url = aKey;
+ url.ReplaceChar('/', '\\');
+ if (anonymize) {
+ path += "<anonymized-url>";
+ } else {
+ path += url;
+ }
+ }
+};
+
+NS_IMPL_ISUPPORTS(BlobURLsReporter, nsIMemoryReporter)
+
+class ReleasingTimerHolder final : public Runnable,
+ public nsITimerCallback,
+ public nsIAsyncShutdownBlocker {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ static void Create(const nsACString& aURI) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ReleasingTimerHolder> holder = new ReleasingTimerHolder(aURI);
+
+ // BlobURLProtocolHandler::RemoveDataEntry potentially happens late. We are
+ // prepared to RevokeUri synchronously if we run after XPCOMWillShutdown,
+ // but we need at least to be able to dispatch to the main thread here.
+ auto raii = MakeScopeExit([holder] { holder->CancelTimerAndRevokeURI(); });
+
+ nsresult rv =
+ SchedulerGroup::Dispatch(TaskCategory::Other, holder.forget());
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ raii.release();
+ }
+
+ // Runnable interface
+
+ NS_IMETHOD
+ Run() override {
+ RefPtr<ReleasingTimerHolder> self = this;
+ auto raii = MakeScopeExit([self] { self->CancelTimerAndRevokeURI(); });
+
+ nsresult rv = NS_NewTimerWithCallback(
+ getter_AddRefs(mTimer), this, RELEASING_TIMER, nsITimer::TYPE_ONE_SHOT);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsCOMPtr<nsIAsyncShutdownClient> phase = GetShutdownPhase();
+ NS_ENSURE_TRUE(!!phase, NS_OK);
+
+ rv = phase->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__),
+ __LINE__, u"ReleasingTimerHolder shutdown"_ns);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ raii.release();
+ return NS_OK;
+ }
+
+ // nsITimerCallback interface
+
+ NS_IMETHOD
+ Notify(nsITimer* aTimer) override {
+ RevokeURI();
+ return NS_OK;
+ }
+
+#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
+ using nsINamed::GetName;
+#endif
+
+ // nsIAsyncShutdownBlocker interface
+
+ NS_IMETHOD
+ GetName(nsAString& aName) override {
+ aName.AssignLiteral("ReleasingTimerHolder for blobURL: ");
+ aName.Append(NS_ConvertUTF8toUTF16(mURI));
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ BlockShutdown(nsIAsyncShutdownClient* aClient) override {
+ CancelTimerAndRevokeURI();
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ GetState(nsIPropertyBag**) override { return NS_OK; }
+
+ private:
+ explicit ReleasingTimerHolder(const nsACString& aURI)
+ : Runnable("ReleasingTimerHolder"), mURI(aURI) {}
+
+ ~ReleasingTimerHolder() override = default;
+
+ void RevokeURI() {
+ // Remove the shutting down blocker
+ nsCOMPtr<nsIAsyncShutdownClient> phase = GetShutdownPhase();
+ if (phase) {
+ phase->RemoveBlocker(this);
+ }
+
+ MOZ_ASSERT(NS_IsMainThread(),
+ "without locking gDataTable is main-thread only");
+ mozilla::dom::DataInfo* info =
+ GetDataInfo(mURI, true /* We care about revoked dataInfo */);
+ if (!info) {
+ // Already gone!
+ return;
+ }
+
+ MOZ_ASSERT(info->mRevoked);
+
+ StaticMutexAutoLock lock(sMutex);
+ gDataTable->Remove(mURI);
+ if (gDataTable->Count() == 0) {
+ delete gDataTable;
+ gDataTable = nullptr;
+ }
+ }
+
+ void CancelTimerAndRevokeURI() {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ RevokeURI();
+ }
+
+ static nsCOMPtr<nsIAsyncShutdownClient> GetShutdownPhase() {
+ nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
+ NS_ENSURE_TRUE(!!svc, nullptr);
+
+ nsCOMPtr<nsIAsyncShutdownClient> phase;
+ nsresult rv = svc->GetXpcomWillShutdown(getter_AddRefs(phase));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return phase;
+ }
+
+ nsCString mURI;
+ nsCOMPtr<nsITimer> mTimer;
+};
+
+NS_IMPL_ISUPPORTS_INHERITED(ReleasingTimerHolder, Runnable, nsITimerCallback,
+ nsIAsyncShutdownBlocker)
+
+template <typename T>
+static void AddDataEntryInternal(const nsACString& aURI, T aObject,
+ nsIPrincipal* aPrincipal,
+ const Maybe<nsID>& aAgentClusterId) {
+ MOZ_ASSERT(NS_IsMainThread(), "changing gDataTable is main-thread only");
+ StaticMutexAutoLock lock(sMutex);
+ if (!gDataTable) {
+ gDataTable = new nsClassHashtable<nsCStringHashKey, mozilla::dom::DataInfo>;
+ }
+
+ mozilla::UniquePtr<mozilla::dom::DataInfo> info =
+ mozilla::MakeUnique<mozilla::dom::DataInfo>(aObject, aPrincipal,
+ aAgentClusterId);
+ BlobURLsReporter::GetJSStackForBlob(info.get());
+
+ gDataTable->InsertOrUpdate(aURI, std::move(info));
+}
+
+void BlobURLProtocolHandler::Init(void) {
+ static bool initialized = false;
+
+ if (!initialized) {
+ initialized = true;
+ RegisterStrongMemoryReporter(new BlobURLsReporter());
+ }
+}
+
+BlobURLProtocolHandler::BlobURLProtocolHandler() { Init(); }
+
+BlobURLProtocolHandler::~BlobURLProtocolHandler() = default;
+
+/* static */
+nsresult BlobURLProtocolHandler::AddDataEntry(
+ mozilla::dom::BlobImpl* aBlobImpl, nsIPrincipal* aPrincipal,
+ const Maybe<nsID>& aAgentClusterId, nsACString& aUri) {
+ MOZ_ASSERT(aBlobImpl);
+ MOZ_ASSERT(aPrincipal);
+
+ Init();
+
+ nsresult rv = GenerateURIString(aPrincipal, aUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AddDataEntryInternal(aUri, aBlobImpl, aPrincipal, aAgentClusterId);
+
+ BroadcastBlobURLRegistration(aUri, aBlobImpl, aPrincipal, aAgentClusterId);
+ return NS_OK;
+}
+
+/* static */
+nsresult BlobURLProtocolHandler::AddDataEntry(
+ MediaSource* aMediaSource, nsIPrincipal* aPrincipal,
+ const Maybe<nsID>& aAgentClusterId, nsACString& aUri) {
+ MOZ_ASSERT(aMediaSource);
+ MOZ_ASSERT(aPrincipal);
+
+ Init();
+
+ nsresult rv = GenerateURIString(aPrincipal, aUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AddDataEntryInternal(aUri, aMediaSource, aPrincipal, aAgentClusterId);
+ return NS_OK;
+}
+
+/* static */
+void BlobURLProtocolHandler::AddDataEntry(const nsACString& aURI,
+ nsIPrincipal* aPrincipal,
+ const Maybe<nsID>& aAgentClusterId,
+ mozilla::dom::BlobImpl* aBlobImpl) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aBlobImpl);
+ AddDataEntryInternal(aURI, aBlobImpl, aPrincipal, aAgentClusterId);
+}
+
+/* static */
+bool BlobURLProtocolHandler::ForEachBlobURL(
+ std::function<bool(mozilla::dom::BlobImpl*, nsIPrincipal*,
+ const Maybe<nsID>&, const nsACString&, bool aRevoked)>&&
+ aCb) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!gDataTable) {
+ return false;
+ }
+
+ for (const auto& entry : *gDataTable) {
+ mozilla::dom::DataInfo* info = entry.GetWeak();
+ MOZ_ASSERT(info);
+
+ if (info->mObjectType != mozilla::dom::DataInfo::eBlobImpl) {
+ continue;
+ }
+
+ MOZ_ASSERT(info->mBlobImpl);
+ if (!aCb(info->mBlobImpl, info->mPrincipal, info->mAgentClusterId,
+ entry.GetKey(), info->mRevoked)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/*static */
+void BlobURLProtocolHandler::RemoveDataEntry(const nsACString& aUri,
+ bool aBroadcastToOtherProcesses) {
+ MOZ_ASSERT(NS_IsMainThread(), "changing gDataTable is main-thread only");
+ if (!gDataTable) {
+ return;
+ }
+ mozilla::dom::DataInfo* info = GetDataInfo(aUri);
+ if (!info) {
+ return;
+ }
+
+ {
+ StaticMutexAutoLock lock(sMutex);
+ info->mRevoked = true;
+ }
+
+ if (aBroadcastToOtherProcesses &&
+ info->mObjectType == mozilla::dom::DataInfo::eBlobImpl) {
+ BroadcastBlobURLUnregistration(nsCString(aUri), info->mPrincipal);
+ }
+
+ // The timer will take care of removing the entry for real after
+ // RELEASING_TIMER milliseconds. In the meantime, the mozilla::dom::DataInfo,
+ // marked as revoked, will not be exposed.
+ ReleasingTimerHolder::Create(aUri);
+}
+
+/*static */
+bool BlobURLProtocolHandler::RemoveDataEntry(
+ const nsACString& aUri, nsIPrincipal* aPrincipal,
+ const Maybe<nsID>& aAgentClusterId) {
+ MOZ_ASSERT(NS_IsMainThread(), "changing gDataTable is main-thread only");
+ if (!gDataTable) {
+ return false;
+ }
+
+ mozilla::dom::DataInfo* info = GetDataInfo(aUri);
+ if (!info) {
+ return false;
+ }
+
+ if (!aPrincipal || !aPrincipal->Subsumes(info->mPrincipal)) {
+ return false;
+ }
+
+ if (StaticPrefs::privacy_partition_bloburl_per_agent_cluster() &&
+ aAgentClusterId.isSome() && info->mAgentClusterId.isSome() &&
+ !aAgentClusterId.value().Equals(info->mAgentClusterId.value())) {
+ return false;
+ }
+
+ RemoveDataEntry(aUri, true);
+ return true;
+}
+
+/* static */
+void BlobURLProtocolHandler::RemoveDataEntries() {
+ MOZ_ASSERT(NS_IsMainThread(), "changing gDataTable is main-thread only");
+ StaticMutexAutoLock lock(sMutex);
+ if (!gDataTable) {
+ return;
+ }
+
+ gDataTable->Clear();
+ delete gDataTable;
+ gDataTable = nullptr;
+}
+
+/* static */
+bool BlobURLProtocolHandler::HasDataEntry(const nsACString& aUri) {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "without locking gDataTable is main-thread only");
+ return !!GetDataInfo(aUri);
+}
+
+/* static */
+nsresult BlobURLProtocolHandler::GenerateURIString(nsIPrincipal* aPrincipal,
+ nsACString& aUri) {
+ nsresult rv;
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ do_GetService("@mozilla.org/uuid-generator;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsID id;
+ rv = uuidgen->GenerateUUIDInPlace(&id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aUri.AssignLiteral(BLOBURI_SCHEME);
+ aUri.Append(':');
+
+ if (aPrincipal) {
+ nsAutoCString origin;
+ rv = aPrincipal->GetAsciiOrigin(origin);
+ if (NS_FAILED(rv)) {
+ origin.AssignLiteral("null");
+ }
+
+ aUri.Append(origin);
+ aUri.Append('/');
+ }
+
+ aUri += NSID_TrimBracketsASCII(id);
+
+ return NS_OK;
+}
+
+/* static */
+bool BlobURLProtocolHandler::GetDataEntry(
+ const nsACString& aUri, mozilla::dom::BlobImpl** aBlobImpl,
+ nsIPrincipal* aLoadingPrincipal, nsIPrincipal* aTriggeringPrincipal,
+ const OriginAttributes& aOriginAttributes, uint64_t aInnerWindowId,
+ const Maybe<nsID>& aAgentClusterId, bool aAlsoIfRevoked) {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "without locking gDataTable is main-thread only");
+ MOZ_ASSERT(aTriggeringPrincipal);
+
+ if (!gDataTable) {
+ return false;
+ }
+
+ mozilla::dom::DataInfo* info = GetDataInfo(aUri, aAlsoIfRevoked);
+ if (!info) {
+ return false;
+ }
+
+ // We want to be sure that we stop the creation of the channel if the blob
+ // URL is copy-and-pasted on a different context (ex. private browsing or
+ // containers).
+ //
+ // We also allow the system principal to create the channel regardless of
+ // the OriginAttributes. This is primarily for the benefit of mechanisms
+ // like the Download API that explicitly create a channel with the system
+ // principal and which is never mutated to have a non-zero
+ // mPrivateBrowsingId or container.
+
+ if ((NS_WARN_IF(!aLoadingPrincipal) ||
+ !aLoadingPrincipal->IsSystemPrincipal()) &&
+ NS_WARN_IF(!ChromeUtils::IsOriginAttributesEqualIgnoringFPD(
+ aOriginAttributes,
+ BasePrincipal::Cast(info->mPrincipal)->OriginAttributesRef()))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!aTriggeringPrincipal->Subsumes(info->mPrincipal))) {
+ return false;
+ }
+
+ // BlobURLs are openable on the same agent-cluster-id only.
+ if (StaticPrefs::privacy_partition_bloburl_per_agent_cluster() &&
+ aAgentClusterId.isSome() && info->mAgentClusterId.isSome() &&
+ NS_WARN_IF(!aAgentClusterId->Equals(info->mAgentClusterId.value()))) {
+ nsAutoString localizedMsg;
+ AutoTArray<nsString, 1> param;
+ CopyUTF8toUTF16(aUri, *param.AppendElement());
+ nsresult rv = nsContentUtils::FormatLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "BlobDifferentClusterError", param,
+ localizedMsg);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ nsContentUtils::ReportToConsoleByWindowID(
+ localizedMsg, nsIScriptError::errorFlag, "DOM"_ns, aInnerWindowId);
+ return false;
+ }
+
+ RefPtr<mozilla::dom::BlobImpl> blobImpl = info->mBlobImpl;
+ blobImpl.forget(aBlobImpl);
+
+ return true;
+}
+
+/* static */
+void BlobURLProtocolHandler::Traverse(
+ const nsACString& aUri, nsCycleCollectionTraversalCallback& aCallback) {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "without locking gDataTable is main-thread only");
+ if (!gDataTable) {
+ return;
+ }
+
+ mozilla::dom::DataInfo* res;
+ gDataTable->Get(aUri, &res);
+ if (!res) {
+ return;
+ }
+
+ NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(
+ aCallback, "BlobURLProtocolHandler mozilla::dom::DataInfo.mBlobImpl");
+ aCallback.NoteXPCOMChild(res->mBlobImpl);
+
+ NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(
+ aCallback, "BlobURLProtocolHandler mozilla::dom::DataInfo.mMediaSource");
+ aCallback.NoteXPCOMChild(static_cast<EventTarget*>(res->mMediaSource));
+}
+
+NS_IMPL_ISUPPORTS(BlobURLProtocolHandler, nsIProtocolHandler,
+ nsISupportsWeakReference)
+
+/* static */ nsresult BlobURLProtocolHandler::CreateNewURI(
+ const nsACString& aSpec, const char* aCharset, nsIURI* aBaseURI,
+ nsIURI** aResult) {
+ *aResult = nullptr;
+
+ // This method can be called on any thread, which is why we lock the mutex
+ // for read access to gDataTable.
+ bool revoked = true;
+ {
+ StaticMutexAutoLock lock(sMutex);
+ mozilla::dom::DataInfo* info = GetDataInfo(aSpec);
+ if (info && info->mObjectType == mozilla::dom::DataInfo::eBlobImpl) {
+ revoked = info->mRevoked;
+ }
+ }
+
+ return NS_MutateURI(new BlobURL::Mutator())
+ .SetSpec(aSpec)
+ .Apply(&nsIBlobURLMutator::SetRevoked, revoked)
+ .Finalize(aResult);
+}
+
+NS_IMETHODIMP
+BlobURLProtocolHandler::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo,
+ nsIChannel** aResult) {
+ auto channel = MakeRefPtr<BlobURLChannel>(aURI, aLoadInfo);
+ channel.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BlobURLProtocolHandler::AllowPort(int32_t port, const char* scheme,
+ bool* _retval) {
+ // don't override anything.
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BlobURLProtocolHandler::GetScheme(nsACString& result) {
+ result.AssignLiteral(BLOBURI_SCHEME);
+ return NS_OK;
+}
+
+/* static */
+bool BlobURLProtocolHandler::GetBlobURLPrincipal(nsIURI* aURI,
+ nsIPrincipal** aPrincipal) {
+ MOZ_ASSERT(aURI);
+ MOZ_ASSERT(aPrincipal);
+
+ RefPtr<BlobURL> blobURL;
+ nsresult rv =
+ aURI->QueryInterface(kHOSTOBJECTURICID, getter_AddRefs(blobURL));
+ if (NS_FAILED(rv) || !blobURL) {
+ return false;
+ }
+
+ StaticMutexAutoLock lock(sMutex);
+ mozilla::dom::DataInfo* info =
+ GetDataInfoFromURI(aURI, true /*aAlsoIfRevoked */);
+ if (!info || info->mObjectType != mozilla::dom::DataInfo::eBlobImpl ||
+ !info->mBlobImpl) {
+ return false;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+
+ if (blobURL->Revoked()) {
+ principal = NullPrincipal::Create(
+ BasePrincipal::Cast(info->mPrincipal)->OriginAttributesRef());
+ } else {
+ principal = info->mPrincipal;
+ }
+
+ principal.forget(aPrincipal);
+ return true;
+}
+
+bool BlobURLProtocolHandler::IsBlobURLBroadcastPrincipal(
+ nsIPrincipal* aPrincipal) {
+ return aPrincipal->IsSystemPrincipal() ||
+ aPrincipal->GetIsAddonOrExpandedAddonPrincipal();
+}
+
+} // namespace dom
+} // namespace mozilla
+
+nsresult NS_GetBlobForBlobURI(nsIURI* aURI, mozilla::dom::BlobImpl** aBlob) {
+ *aBlob = nullptr;
+ MOZ_ASSERT(NS_IsMainThread(),
+ "without locking gDataTable is main-thread only");
+ mozilla::dom::DataInfo* info =
+ mozilla::dom::GetDataInfoFromURI(aURI, false /* aAlsoIfRevoked */);
+ if (!info || info->mObjectType != mozilla::dom::DataInfo::eBlobImpl) {
+ return NS_ERROR_DOM_BAD_URI;
+ }
+
+ RefPtr<mozilla::dom::BlobImpl> blob = info->mBlobImpl;
+ blob.forget(aBlob);
+ return NS_OK;
+}
+
+nsresult NS_GetBlobForBlobURISpec(const nsACString& aSpec,
+ mozilla::dom::BlobImpl** aBlob,
+ bool aAlsoIfRevoked) {
+ *aBlob = nullptr;
+ MOZ_ASSERT(NS_IsMainThread(),
+ "without locking gDataTable is main-thread only");
+
+ mozilla::dom::DataInfo* info =
+ mozilla::dom::GetDataInfo(aSpec, aAlsoIfRevoked);
+ if (!info || info->mObjectType != mozilla::dom::DataInfo::eBlobImpl ||
+ !info->mBlobImpl) {
+ return NS_ERROR_DOM_BAD_URI;
+ }
+
+ RefPtr<mozilla::dom::BlobImpl> blob = info->mBlobImpl;
+ blob.forget(aBlob);
+ return NS_OK;
+}
+
+nsresult NS_GetSourceForMediaSourceURI(nsIURI* aURI,
+ mozilla::dom::MediaSource** aSource) {
+ *aSource = nullptr;
+
+ MOZ_ASSERT(NS_IsMainThread(),
+ "without locking gDataTable is main-thread only");
+ mozilla::dom::DataInfo* info = mozilla::dom::GetDataInfoFromURI(aURI);
+ if (!info || info->mObjectType != mozilla::dom::DataInfo::eMediaSource) {
+ return NS_ERROR_DOM_BAD_URI;
+ }
+
+ RefPtr<mozilla::dom::MediaSource> mediaSource = info->mMediaSource;
+ mediaSource.forget(aSource);
+ return NS_OK;
+}
+
+namespace mozilla::dom {
+
+bool IsType(nsIURI* aUri, mozilla::dom::DataInfo::ObjectType aType) {
+ // We lock because this may be called off-main-thread
+ StaticMutexAutoLock lock(sMutex);
+ mozilla::dom::DataInfo* info = GetDataInfoFromURI(aUri);
+ if (!info) {
+ return false;
+ }
+
+ return info->mObjectType == aType;
+}
+
+bool IsBlobURI(nsIURI* aUri) {
+ return IsType(aUri, mozilla::dom::DataInfo::eBlobImpl);
+}
+
+bool BlobURLSchemeIsHTTPOrHTTPS(const nsACString& aUri) {
+ return (StringBeginsWith(aUri, "blob:http://"_ns) ||
+ StringBeginsWith(aUri, "blob:https://"_ns));
+}
+
+bool IsMediaSourceURI(nsIURI* aUri) {
+ return IsType(aUri, mozilla::dom::DataInfo::eMediaSource);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/file/uri/BlobURLProtocolHandler.h b/dom/file/uri/BlobURLProtocolHandler.h
new file mode 100644
index 0000000000..abb24f6fc0
--- /dev/null
+++ b/dom/file/uri/BlobURLProtocolHandler.h
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_BlobURLProtocolHandler_h
+#define mozilla_dom_BlobURLProtocolHandler_h
+
+#include "mozilla/Attributes.h"
+#include "nsIProtocolHandler.h"
+#include "nsIURI.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "nsWeakReference.h"
+#include <functional>
+
+#define BLOBURI_SCHEME "blob"
+
+class nsIPrincipal;
+
+namespace mozilla {
+class BlobURLsReporter;
+class OriginAttributes;
+template <class T>
+class Maybe;
+
+namespace dom {
+
+class BlobImpl;
+class BlobURLRegistrationData;
+class ContentParent;
+class MediaSource;
+
+class BlobURLProtocolHandler final : public nsIProtocolHandler,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPROTOCOLHANDLER
+
+ BlobURLProtocolHandler();
+
+ static nsresult CreateNewURI(const nsACString& aSpec, const char* aCharset,
+ nsIURI* aBaseURI, nsIURI** result);
+
+ // Methods for managing uri->object mapping
+ // AddDataEntry creates the URI with the given scheme and returns it in aUri
+ static nsresult AddDataEntry(BlobImpl*, nsIPrincipal*,
+ const Maybe<nsID>& aAgentClusterId,
+ nsACString& aUri);
+ static nsresult AddDataEntry(MediaSource*, nsIPrincipal*,
+ const Maybe<nsID>& aAgentClusterId,
+ nsACString& aUri);
+ // IPC only
+ static void AddDataEntry(const nsACString& aURI, nsIPrincipal* aPrincipal,
+ const Maybe<nsID>& aAgentClusterId,
+ BlobImpl* aBlobImpl);
+
+ // These methods revoke a blobURL. Because some operations could still be in
+ // progress, the revoking consists in marking the blobURL as revoked and in
+ // removing it after RELEASING_TIMER milliseconds.
+ static void RemoveDataEntry(const nsACString& aUri,
+ bool aBroadcastToOTherProcesses = true);
+ // Returns true if the entry was allowed to be removed.
+ static bool RemoveDataEntry(const nsACString& aUri, nsIPrincipal* aPrincipal,
+ const Maybe<nsID>& aAgentClusterId);
+
+ static void RemoveDataEntries();
+
+ static bool HasDataEntry(const nsACString& aUri);
+
+ static bool GetDataEntry(const nsACString& aUri, BlobImpl** aBlobImpl,
+ nsIPrincipal* aLoadingPrincipal,
+ nsIPrincipal* aTriggeringPrincipal,
+ const OriginAttributes& aOriginAttributes,
+ uint64_t aInnerWindowId,
+ const Maybe<nsID>& aAgentClusterId,
+ bool aAlsoIfRevoked = false);
+
+ static void Traverse(const nsACString& aUri,
+ nsCycleCollectionTraversalCallback& aCallback);
+
+ // Main-thread only method to invoke a helper function that gets called for
+ // every known and recently revoked Blob URL. The helper function should
+ // return true to keep going or false to stop enumerating (presumably because
+ // of an unexpected XPCOM or IPC error). This method returns false if already
+ // shutdown or if the helper method returns false, true otherwise.
+ static bool ForEachBlobURL(
+ std::function<bool(BlobImpl*, nsIPrincipal*, const Maybe<nsID>&,
+ const nsACString&, bool aRevoked)>&& aCb);
+
+ // This method returns false if aURI is not a known BlobURL. Otherwise it
+ // returns true.
+ //
+ // When true is returned, the aPrincipal out param is meaningful. It gets
+ // set to the principal that a channel loaded from the blob would get if
+ // the blob is not already revoked and to a NullPrincipal if the blob is
+ // revoked.
+ //
+ // This means that for a revoked blob URL this method may either return
+ // false or return true and hand out a NullPrincipal in aPrincipal,
+ // depending on whether the "remove it from the hashtable" timer has
+ // fired. See RemoveDataEntry().
+ static bool GetBlobURLPrincipal(nsIURI* aURI, nsIPrincipal** aPrincipal);
+
+ // Check if metadata about Blob URLs created with this principal should be
+ // broadcast into every content process. This is currently the case for
+ // extension blob URLs and system principal blob URLs, as they can be loaded
+ // by system code and content scripts respectively.
+ static bool IsBlobURLBroadcastPrincipal(nsIPrincipal* aPrincipal);
+
+ private:
+ ~BlobURLProtocolHandler();
+
+ static void Init();
+
+ // If principal is not null, its origin will be used to generate the URI.
+ static nsresult GenerateURIString(nsIPrincipal* aPrincipal, nsACString& aUri);
+};
+
+bool IsBlobURI(nsIURI* aUri);
+bool IsMediaSourceURI(nsIURI* aUri);
+
+// Return true if inner scheme of blobURL is http or https, false otherwise.
+bool BlobURLSchemeIsHTTPOrHTTPS(const nsACString& aUri);
+
+} // namespace dom
+} // namespace mozilla
+
+extern nsresult NS_GetBlobForBlobURI(nsIURI* aURI,
+ mozilla::dom::BlobImpl** aBlob);
+
+extern nsresult NS_GetBlobForBlobURISpec(const nsACString& aSpec,
+ mozilla::dom::BlobImpl** aBlob,
+ bool aAlsoIfRevoked = false);
+
+extern nsresult NS_GetSourceForMediaSourceURI(
+ nsIURI* aURI, mozilla::dom::MediaSource** aSource);
+
+#endif /* mozilla_dom_BlobURLProtocolHandler_h */
diff --git a/dom/file/uri/components.conf b/dom/file/uri/components.conf
new file mode 100644
index 0000000000..190a3a6c54
--- /dev/null
+++ b/dom/file/uri/components.conf
@@ -0,0 +1,24 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{b43964aa-a078-44b2-b06b-fd4d1b172e66}',
+ 'contract_ids': ['@mozilla.org/network/protocol;1?name=blob'],
+ 'type': 'mozilla::dom::BlobURLProtocolHandler',
+ 'headers': ['mozilla/dom/BlobURLProtocolHandler.h'],
+ 'protocol_config': {
+ 'scheme': 'blob',
+ 'flags': [
+ 'URI_NORELATIVE',
+ 'URI_NOAUTH',
+ 'URI_LOADABLE_BY_SUBSUMERS',
+ 'URI_NON_PERSISTABLE',
+ 'URI_IS_LOCAL_RESOURCE',
+ ],
+ },
+ },
+]
diff --git a/dom/file/uri/moz.build b/dom/file/uri/moz.build
new file mode 100644
index 0000000000..a1c4aed54c
--- /dev/null
+++ b/dom/file/uri/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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: File")
+
+EXPORTS.mozilla.dom += [
+ "BlobURL.h",
+ "BlobURLInputStream.h",
+ "BlobURLProtocolHandler.h",
+]
+
+UNIFIED_SOURCES += [
+ "BlobURL.cpp",
+ "BlobURLChannel.cpp",
+ "BlobURLInputStream.cpp",
+ "BlobURLProtocolHandler.cpp",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/file",
+ "/netwerk/base",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/filesystem/Directory.cpp b/dom/filesystem/Directory.cpp
new file mode 100644
index 0000000000..da5d6e5515
--- /dev/null
+++ b/dom/filesystem/Directory.cpp
@@ -0,0 +1,198 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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/Directory.h"
+
+#include "GetDirectoryListingTask.h"
+#include "GetFilesTask.h"
+
+#include "nsIFile.h"
+#include "nsString.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/DirectoryBinding.h"
+#include "mozilla/dom/FileSystemBase.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/OSFileSystem.h"
+#include "mozilla/dom/WorkerPrivate.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Directory)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Directory)
+ if (tmp->mFileSystem) {
+ tmp->mFileSystem->Unlink();
+ tmp->mFileSystem = nullptr;
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Directory)
+ if (tmp->mFileSystem) {
+ tmp->mFileSystem->Traverse(cb);
+ }
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(Directory)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(Directory)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Directory)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/* static */
+already_AddRefed<Directory> Directory::Constructor(const GlobalObject& aGlobal,
+ const nsAString& aRealPath,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIFile> path;
+ aRv = NS_NewLocalFile(aRealPath, true, getter_AddRefs(path));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (NS_WARN_IF(!global)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ return Create(global, path);
+}
+
+/* static */
+already_AddRefed<Directory> Directory::Create(nsIGlobalObject* aGlobal,
+ nsIFile* aFile,
+ FileSystemBase* aFileSystem) {
+ MOZ_ASSERT(aGlobal);
+ MOZ_ASSERT(aFile);
+
+ RefPtr<Directory> directory = new Directory(aGlobal, aFile, aFileSystem);
+ return directory.forget();
+}
+
+Directory::Directory(nsIGlobalObject* aGlobal, nsIFile* aFile,
+ FileSystemBase* aFileSystem)
+ : mGlobal(aGlobal), mFile(aFile) {
+ MOZ_ASSERT(aFile);
+
+ // aFileSystem can be null. In this case we create a OSFileSystem when needed.
+ if (aFileSystem) {
+ // More likely, this is a OSFileSystem. This object keeps a reference of
+ // mGlobal but it's not cycle collectable and to avoid manual
+ // addref/release, it's better to have 1 object per directory. For this
+ // reason we clone it here.
+ mFileSystem = aFileSystem->Clone();
+ }
+}
+
+Directory::~Directory() = default;
+
+nsIGlobalObject* Directory::GetParentObject() const { return mGlobal; }
+
+JSObject* Directory::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return Directory_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void Directory::GetName(nsAString& aRetval, ErrorResult& aRv) {
+ aRetval.Truncate();
+
+ RefPtr<FileSystemBase> fs = GetFileSystem(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ fs->GetDirectoryName(mFile, aRetval, aRv);
+}
+
+void Directory::GetPath(nsAString& aRetval, ErrorResult& aRv) {
+ // This operation is expensive. Better to cache the result.
+ if (mPath.IsEmpty()) {
+ RefPtr<FileSystemBase> fs = GetFileSystem(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ fs->GetDOMPath(mFile, mPath, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ }
+
+ aRetval = mPath;
+}
+
+nsresult Directory::GetFullRealPath(nsAString& aPath) {
+ nsresult rv = mFile->GetPath(aPath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<Promise> Directory::GetFilesAndDirectories(ErrorResult& aRv) {
+ RefPtr<FileSystemBase> fs = GetFileSystem(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ RefPtr<GetDirectoryListingTaskChild> task =
+ GetDirectoryListingTaskChild::Create(fs, this, mFile, mFilters, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ task->Start();
+
+ return task->GetPromise();
+}
+
+already_AddRefed<Promise> Directory::GetFiles(bool aRecursiveFlag,
+ ErrorResult& aRv) {
+ ErrorResult rv;
+ RefPtr<FileSystemBase> fs = GetFileSystem(rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ RefPtr<GetFilesTaskChild> task =
+ GetFilesTaskChild::Create(fs, this, mFile, aRecursiveFlag, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ task->Start();
+
+ return task->GetPromise();
+}
+
+void Directory::SetContentFilters(const nsAString& aFilters) {
+ mFilters = aFilters;
+}
+
+FileSystemBase* Directory::GetFileSystem(ErrorResult& aRv) {
+ if (!mFileSystem) {
+ nsAutoString path;
+ aRv = mFile->GetPath(path);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ RefPtr<OSFileSystem> fs = new OSFileSystem(path);
+ fs->Init(mGlobal);
+
+ mFileSystem = fs;
+ }
+
+ return mFileSystem;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/Directory.h b/dom/filesystem/Directory.h
new file mode 100644
index 0000000000..b8ee0e68e1
--- /dev/null
+++ b/dom/filesystem/Directory.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_Directory_h
+#define mozilla_dom_Directory_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/File.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class FileSystemBase;
+class Promise;
+class StringOrFileOrDirectory;
+
+class Directory final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Directory)
+
+ static already_AddRefed<Directory> Constructor(const GlobalObject& aGlobal,
+ const nsAString& aRealPath,
+ ErrorResult& aRv);
+
+ static already_AddRefed<Directory> Create(nsIGlobalObject* aGlobal,
+ nsIFile* aDirectory,
+ FileSystemBase* aFileSystem = 0);
+
+ // ========= Begin WebIDL bindings. ===========
+
+ nsIGlobalObject* GetParentObject() const;
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void GetName(nsAString& aRetval, ErrorResult& aRv);
+
+ // From
+ // https://microsoftedge.github.io/directory-upload/proposal.html#directory-interface
+ // :
+
+ void GetPath(nsAString& aRetval, ErrorResult& aRv);
+
+ nsresult GetFullRealPath(nsAString& aPath);
+
+ already_AddRefed<Promise> GetFilesAndDirectories(ErrorResult& aRv);
+
+ already_AddRefed<Promise> GetFiles(bool aRecursiveFlag, ErrorResult& aRv);
+
+ // =========== End WebIDL bindings.============
+
+ /**
+ * Sets a semi-colon separated list of filters to filter-in or filter-out
+ * certain types of files when the contents of this directory are requested
+ * via a GetFilesAndDirectories() call.
+ *
+ * Currently supported keywords:
+ *
+ * * filter-out-sensitive
+ * This keyword filters out files or directories that we don't wish to
+ * make available to Web content because we are concerned that there is
+ * a risk that users may unwittingly give Web content access to them
+ * and suffer undesirable consequences. The details of what is
+ * filtered out can be found in GetDirectoryListingTask::Work.
+ *
+ * In future, we will likely support filtering based on filename extensions
+ * (for example, aFilters could be "*.jpg; *.jpeg; *.gif"), but that isn't
+ * supported yet. Once supported, files that don't match a specified
+ * extension (if any are specified) would be filtered out. This
+ * functionality would allow us to apply the 'accept' attribute from
+ * <input type=file directory accept="..."> to the results of a directory
+ * picker operation.
+ */
+ void SetContentFilters(const nsAString& aFilters);
+
+ FileSystemBase* GetFileSystem(ErrorResult& aRv);
+
+ nsIFile* GetInternalNsIFile() const { return mFile; }
+
+ private:
+ Directory(nsIGlobalObject* aGlobal, nsIFile* aFile,
+ FileSystemBase* aFileSystem = nullptr);
+ ~Directory();
+
+ /*
+ * Convert relative DOM path to the absolute real path.
+ */
+ nsresult DOMPathToRealPath(const nsAString& aPath, nsIFile** aFile) const;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<FileSystemBase> mFileSystem;
+ nsCOMPtr<nsIFile> mFile;
+
+ nsString mFilters;
+ nsString mPath;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_Directory_h
diff --git a/dom/filesystem/FileSystemBase.cpp b/dom/filesystem/FileSystemBase.cpp
new file mode 100644
index 0000000000..776ecc9db5
--- /dev/null
+++ b/dom/filesystem/FileSystemBase.cpp
@@ -0,0 +1,143 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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/FileSystemBase.h"
+
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "nsIFile.h"
+#include "OSFileSystem.h"
+
+namespace mozilla::dom {
+
+FileSystemBase::FileSystemBase() : mShutdown(false) {}
+
+FileSystemBase::~FileSystemBase() { AssertIsOnOwningThread(); }
+
+void FileSystemBase::Shutdown() {
+ AssertIsOnOwningThread();
+ mShutdown = true;
+}
+
+nsIGlobalObject* FileSystemBase::GetParentObject() const {
+ AssertIsOnOwningThread();
+ return nullptr;
+}
+
+bool FileSystemBase::GetRealPath(BlobImpl* aFile, nsIFile** aPath) const {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aFile, "aFile Should not be null.");
+ MOZ_ASSERT(aPath);
+
+ nsAutoString filePath;
+ ErrorResult rv;
+ aFile->GetMozFullPathInternal(filePath, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ return false;
+ }
+
+ rv = NS_NewLocalFile(filePath, true, aPath);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ return false;
+ }
+
+ return true;
+}
+
+bool FileSystemBase::IsSafeFile(nsIFile* aFile) const {
+ AssertIsOnOwningThread();
+ return false;
+}
+
+bool FileSystemBase::IsSafeDirectory(Directory* aDir) const {
+ AssertIsOnOwningThread();
+ return false;
+}
+
+void FileSystemBase::GetDirectoryName(nsIFile* aFile, nsAString& aRetval,
+ ErrorResult& aRv) const {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aFile);
+
+ aRv = aFile->GetLeafName(aRetval);
+ NS_WARNING_ASSERTION(!aRv.Failed(), "GetLeafName failed");
+}
+
+void FileSystemBase::GetDOMPath(nsIFile* aFile, nsAString& aRetval,
+ ErrorResult& aRv) const {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aFile);
+
+ aRetval.Truncate();
+
+ nsCOMPtr<nsIFile> fileSystemPath;
+ aRv = NS_NewLocalFile(LocalRootPath(), true, getter_AddRefs(fileSystemPath));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsCOMPtr<nsIFile> path;
+ aRv = aFile->Clone(getter_AddRefs(path));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsTArray<nsString> parts;
+
+ while (true) {
+ nsAutoString leafName;
+ aRv = path->GetLeafName(leafName);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ if (!leafName.IsEmpty()) {
+ parts.AppendElement(leafName);
+ }
+
+ bool equal = false;
+ aRv = fileSystemPath->Equals(path, &equal);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ if (equal) {
+ break;
+ }
+
+ nsCOMPtr<nsIFile> parentPath;
+ aRv = path->GetParent(getter_AddRefs(parentPath));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ MOZ_ASSERT(parentPath);
+
+ aRv = parentPath->Clone(getter_AddRefs(path));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ }
+
+ if (parts.IsEmpty()) {
+ aRetval.AppendLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
+ return;
+ }
+
+ for (int32_t i = parts.Length() - 1; i >= 0; --i) {
+ aRetval.AppendLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
+ aRetval.Append(parts[i]);
+ }
+}
+
+void FileSystemBase::AssertIsOnOwningThread() const {
+ NS_ASSERT_OWNINGTHREAD(FileSystemBase);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/FileSystemBase.h b/dom/filesystem/FileSystemBase.h
new file mode 100644
index 0000000000..b78fe1736b
--- /dev/null
+++ b/dom/filesystem/FileSystemBase.h
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileSystemBase_h
+#define mozilla_dom_FileSystemBase_h
+
+#include "nsString.h"
+#include "Directory.h"
+
+namespace mozilla::dom {
+
+class BlobImpl;
+
+class FileSystemBase {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(FileSystemBase)
+
+ FileSystemBase();
+
+ virtual void Shutdown();
+
+ // SerializeDOMPath the FileSystem to string.
+ virtual void SerializeDOMPath(nsAString& aOutput) const = 0;
+
+ virtual already_AddRefed<FileSystemBase> Clone() = 0;
+
+ virtual bool ShouldCreateDirectory() = 0;
+
+ virtual nsIGlobalObject* GetParentObject() const;
+
+ virtual void GetDirectoryName(nsIFile* aFile, nsAString& aRetval,
+ ErrorResult& aRv) const;
+
+ void GetDOMPath(nsIFile* aFile, nsAString& aRetval, ErrorResult& aRv) const;
+
+ /*
+ * Return the local root path of the FileSystem implementation.
+ * For OSFileSystem, this is equal to the path of the root Directory;
+ * For DeviceStorageFileSystem, this is the path of the SDCard, parent
+ * directory of the exposed root Directory (per type).
+ */
+ const nsAString& LocalRootPath() const { return mLocalRootPath; }
+
+ bool IsShutdown() const { return mShutdown; }
+
+ virtual bool IsSafeFile(nsIFile* aFile) const;
+
+ virtual bool IsSafeDirectory(Directory* aDir) const;
+
+ bool GetRealPath(BlobImpl* aFile, nsIFile** aPath) const;
+
+ // CC methods
+ virtual void Unlink() {}
+ virtual void Traverse(nsCycleCollectionTraversalCallback& cb) {}
+
+ void AssertIsOnOwningThread() const;
+
+ protected:
+ virtual ~FileSystemBase();
+
+ // The local path of the root (i.e. the OS path, with OS path separators, of
+ // the OS directory that acts as the root of this OSFileSystem).
+ // This path must be set by the FileSystem implementation immediately
+ // because it will be used for the validation of any FileSystemTaskChildBase.
+ // The concept of this path is that, any task will never go out of it and this
+ // must be considered the OS 'root' of the current FileSystem. Different
+ // Directory object can have different OS 'root' path.
+ // To be more clear, any path managed by this FileSystem implementation must
+ // be discendant of this local root path.
+ nsString mLocalRootPath;
+
+ bool mShutdown;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileSystemBase_h
diff --git a/dom/filesystem/FileSystemRequestParent.cpp b/dom/filesystem/FileSystemRequestParent.cpp
new file mode 100644
index 0000000000..25211dbd23
--- /dev/null
+++ b/dom/filesystem/FileSystemRequestParent.cpp
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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/FileSystemRequestParent.h"
+#include "mozilla/dom/PFileSystemParams.h"
+
+#include "GetDirectoryListingTask.h"
+#include "GetFileOrDirectoryTask.h"
+#include "GetFilesTask.h"
+
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/FileSystemBase.h"
+#include "mozilla/dom/FileSystemSecurity.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/dom/OSFileSystem.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Unused.h"
+#include "nsProxyRelease.h"
+
+using namespace mozilla::ipc;
+
+namespace mozilla::dom {
+
+FileSystemRequestParent::FileSystemRequestParent() : mDestroyed(false) {
+ AssertIsOnBackgroundThread();
+}
+
+FileSystemRequestParent::~FileSystemRequestParent() {
+ AssertIsOnBackgroundThread();
+}
+
+#define FILESYSTEM_REQUEST_PARENT_DISPATCH_ENTRY(name) \
+ case FileSystemParams::TFileSystem##name##Params: { \
+ const FileSystem##name##Params& p = aParams; \
+ mFileSystem = new OSFileSystemParent(p.filesystem()); \
+ MOZ_ASSERT(mFileSystem); \
+ mTask = name##TaskParent::Create(mFileSystem, p, this, rv); \
+ if (NS_WARN_IF(rv.Failed())) { \
+ rv.SuppressException(); \
+ return false; \
+ } \
+ break; \
+ }
+
+bool FileSystemRequestParent::Initialize(const FileSystemParams& aParams) {
+ AssertIsOnBackgroundThread();
+
+ ErrorResult rv;
+
+ switch (aParams.type()) {
+ FILESYSTEM_REQUEST_PARENT_DISPATCH_ENTRY(GetDirectoryListing)
+ FILESYSTEM_REQUEST_PARENT_DISPATCH_ENTRY(GetFileOrDirectory)
+ FILESYSTEM_REQUEST_PARENT_DISPATCH_ENTRY(GetFiles)
+
+ default: {
+ MOZ_CRASH("not reached");
+ break;
+ }
+ }
+
+ if (NS_WARN_IF(!mTask || !mFileSystem)) {
+ // Should never reach here.
+ return false;
+ }
+
+ return true;
+}
+
+namespace {
+
+class CheckPermissionRunnable final : public Runnable {
+ public:
+ CheckPermissionRunnable(
+ already_AddRefed<ThreadsafeContentParentHandle> aParent,
+ FileSystemRequestParent* aActor, FileSystemTaskParentBase* aTask,
+ const nsAString& aPath)
+ : Runnable("dom::CheckPermissionRunnable"),
+ mContentHandle(aParent),
+ mActor(aActor),
+ mTask(aTask),
+ mPath(aPath),
+ mBackgroundEventTarget(GetCurrentSerialEventTarget()) {
+ AssertIsInMainProcess();
+ AssertIsOnBackgroundThread();
+
+ MOZ_ASSERT(mContentHandle);
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mTask);
+ MOZ_ASSERT(mBackgroundEventTarget);
+ }
+
+ NS_IMETHOD
+ Run() override {
+ if (NS_IsMainThread()) {
+ if (!mozilla::Preferences::GetBool("dom.filesystem.pathcheck.disabled",
+ false)) {
+ RefPtr<FileSystemSecurity> fss = FileSystemSecurity::Get();
+ if (NS_WARN_IF(!fss || !fss->ContentProcessHasAccessTo(
+ mContentHandle->ChildID(), mPath))) {
+ AssertIsOnMainThread();
+ if (RefPtr<ContentParent> contentParent =
+ mContentHandle->GetContentParent()) {
+ contentParent->KillHard("This path is not allowed.");
+ }
+ return NS_OK;
+ }
+ }
+
+ return mBackgroundEventTarget->Dispatch(this, NS_DISPATCH_NORMAL);
+ }
+
+ AssertIsOnBackgroundThread();
+
+ // It can happen that this actor has been destroyed in the meantime we were
+ // on the main-thread.
+ if (!mActor->Destroyed()) {
+ mTask->Start();
+ }
+
+ return NS_OK;
+ }
+
+ private:
+ ~CheckPermissionRunnable() {
+ NS_ProxyRelease("CheckPermissionRunnable::mActor", mBackgroundEventTarget,
+ mActor.forget());
+ }
+
+ RefPtr<ThreadsafeContentParentHandle> mContentHandle;
+ RefPtr<FileSystemRequestParent> mActor;
+ RefPtr<FileSystemTaskParentBase> mTask;
+ const nsString mPath;
+
+ nsCOMPtr<nsIEventTarget> mBackgroundEventTarget;
+};
+
+} // namespace
+
+void FileSystemRequestParent::Start() {
+ AssertIsInMainProcess();
+ AssertIsOnBackgroundThread();
+
+ MOZ_ASSERT(!mDestroyed);
+ MOZ_ASSERT(mFileSystem);
+ MOZ_ASSERT(mTask);
+
+ nsAutoString path;
+ if (NS_WARN_IF(NS_FAILED(mTask->GetTargetPath(path)))) {
+ (void)Send__delete__(this,
+ FileSystemErrorResponse(NS_ERROR_DOM_SECURITY_ERR));
+ return;
+ }
+
+ RefPtr<ThreadsafeContentParentHandle> parent =
+ BackgroundParent::GetContentParentHandle(Manager());
+
+ // If the ThreadsafeContentParentHandle is null we are dealing with a
+ // same-process actor.
+ if (!parent) {
+ mTask->Start();
+ return;
+ }
+
+ RefPtr<Runnable> runnable =
+ new CheckPermissionRunnable(parent.forget(), this, mTask, path);
+ NS_DispatchToMainThread(runnable);
+}
+
+void FileSystemRequestParent::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mDestroyed);
+
+ if (!mFileSystem) {
+ return;
+ }
+
+ mFileSystem->Shutdown();
+ mFileSystem = nullptr;
+ mTask = nullptr;
+ mDestroyed = true;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/FileSystemRequestParent.h b/dom/filesystem/FileSystemRequestParent.h
new file mode 100644
index 0000000000..7ac1547d27
--- /dev/null
+++ b/dom/filesystem/FileSystemRequestParent.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileSystemRequestParent_h
+#define mozilla_dom_FileSystemRequestParent_h
+
+#include "mozilla/dom/PFileSystemRequestParent.h"
+#include "mozilla/dom/FileSystemBase.h"
+
+namespace mozilla::dom {
+
+class FileSystemParams;
+class FileSystemTaskParentBase;
+
+class FileSystemRequestParent final : public PFileSystemRequestParent {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemRequestParent, final)
+
+ public:
+ FileSystemRequestParent();
+
+ bool Initialize(const FileSystemParams& aParams);
+
+ void Start();
+
+ bool Destroyed() const { return mDestroyed; }
+
+ virtual void ActorDestroy(ActorDestroyReason why) override;
+
+ private:
+ ~FileSystemRequestParent();
+
+ RefPtr<FileSystemBase> mFileSystem;
+ RefPtr<FileSystemTaskParentBase> mTask;
+
+ bool mDestroyed;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileSystemRequestParent_h
diff --git a/dom/filesystem/FileSystemSecurity.cpp b/dom/filesystem/FileSystemSecurity.cpp
new file mode 100644
index 0000000000..b9468270d4
--- /dev/null
+++ b/dom/filesystem/FileSystemSecurity.cpp
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "FileSystemSecurity.h"
+#include "FileSystemUtils.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/StaticPtr.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+StaticRefPtr<FileSystemSecurity> gFileSystemSecurity;
+
+} // namespace
+
+/* static */
+already_AddRefed<FileSystemSecurity> FileSystemSecurity::Get() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mozilla::ipc::AssertIsInMainProcess();
+
+ RefPtr<FileSystemSecurity> service = gFileSystemSecurity.get();
+ return service.forget();
+}
+
+/* static */
+already_AddRefed<FileSystemSecurity> FileSystemSecurity::GetOrCreate() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mozilla::ipc::AssertIsInMainProcess();
+
+ if (!gFileSystemSecurity) {
+ gFileSystemSecurity = new FileSystemSecurity();
+ ClearOnShutdown(&gFileSystemSecurity);
+ }
+
+ RefPtr<FileSystemSecurity> service = gFileSystemSecurity.get();
+ return service.forget();
+}
+
+FileSystemSecurity::FileSystemSecurity() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mozilla::ipc::AssertIsInMainProcess();
+}
+
+FileSystemSecurity::~FileSystemSecurity() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mozilla::ipc::AssertIsInMainProcess();
+}
+
+void FileSystemSecurity::GrantAccessToContentProcess(
+ ContentParentId aId, const nsAString& aDirectoryPath) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mozilla::ipc::AssertIsInMainProcess();
+
+ mPaths.WithEntryHandle(aId, [&](auto&& entry) {
+ if (entry && entry.Data()->Contains(aDirectoryPath)) {
+ return;
+ }
+
+ entry.OrInsertWith([] { return MakeUnique<nsTArray<nsString>>(); })
+ ->AppendElement(aDirectoryPath);
+ });
+}
+
+void FileSystemSecurity::Forget(ContentParentId aId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mozilla::ipc::AssertIsInMainProcess();
+
+ mPaths.Remove(aId);
+}
+
+bool FileSystemSecurity::ContentProcessHasAccessTo(ContentParentId aId,
+ const nsAString& aPath) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mozilla::ipc::AssertIsInMainProcess();
+
+#if defined(XP_WIN)
+ if (StringBeginsWith(aPath, u"..\\"_ns) ||
+ FindInReadable(u"\\..\\"_ns, aPath)) {
+ return false;
+ }
+#elif defined(XP_UNIX)
+ if (StringBeginsWith(aPath, u"../"_ns) || FindInReadable(u"/../"_ns, aPath)) {
+ return false;
+ }
+#endif
+
+ nsTArray<nsString>* paths;
+ if (!mPaths.Get(aId, &paths)) {
+ return false;
+ }
+
+ for (uint32_t i = 0, len = paths->Length(); i < len; ++i) {
+ if (FileSystemUtils::IsDescendantPath(paths->ElementAt(i), aPath)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/FileSystemSecurity.h b/dom/filesystem/FileSystemSecurity.h
new file mode 100644
index 0000000000..381d156777
--- /dev/null
+++ b/dom/filesystem/FileSystemSecurity.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileSystemSecurity_h
+#define mozilla_dom_FileSystemSecurity_h
+
+#include "mozilla/dom/ipc/IdType.h"
+#include "nsClassHashtable.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla::dom {
+
+class FileSystemSecurity final {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(FileSystemSecurity)
+
+ static already_AddRefed<FileSystemSecurity> Get();
+
+ static already_AddRefed<FileSystemSecurity> GetOrCreate();
+
+ void GrantAccessToContentProcess(ContentParentId aId,
+ const nsAString& aDirectoryPath);
+
+ void Forget(ContentParentId aId);
+
+ bool ContentProcessHasAccessTo(ContentParentId aId, const nsAString& aPath);
+
+ private:
+ FileSystemSecurity();
+ ~FileSystemSecurity();
+
+ nsClassHashtable<nsUint64HashKey, nsTArray<nsString>> mPaths;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileSystemSecurity_h
diff --git a/dom/filesystem/FileSystemTaskBase.cpp b/dom/filesystem/FileSystemTaskBase.cpp
new file mode 100644
index 0000000000..34cfd8f018
--- /dev/null
+++ b/dom/filesystem/FileSystemTaskBase.cpp
@@ -0,0 +1,246 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/FileSystemTaskBase.h"
+
+#include "nsNetCID.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/FileSystemBase.h"
+#include "mozilla/dom/FileSystemRequestParent.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/Unused.h"
+#include "nsProxyRelease.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+nsresult FileSystemErrorFromNsError(const nsresult& aErrorValue) {
+ uint16_t module = NS_ERROR_GET_MODULE(aErrorValue);
+ if (module == NS_ERROR_MODULE_DOM_FILESYSTEM ||
+ module == NS_ERROR_MODULE_DOM_FILE || module == NS_ERROR_MODULE_DOM) {
+ return aErrorValue;
+ }
+
+ switch (aErrorValue) {
+ case NS_OK:
+ return NS_OK;
+
+ case NS_ERROR_FILE_INVALID_PATH:
+ case NS_ERROR_FILE_UNRECOGNIZED_PATH:
+ return NS_ERROR_DOM_FILESYSTEM_INVALID_PATH_ERR;
+
+ case NS_ERROR_FILE_DESTINATION_NOT_DIR:
+ return NS_ERROR_DOM_FILESYSTEM_INVALID_MODIFICATION_ERR;
+
+ case NS_ERROR_FILE_ACCESS_DENIED:
+ case NS_ERROR_FILE_DIR_NOT_EMPTY:
+ return NS_ERROR_DOM_FILESYSTEM_NO_MODIFICATION_ALLOWED_ERR;
+
+ case NS_ERROR_FILE_NOT_FOUND:
+ case NS_ERROR_NOT_AVAILABLE:
+ return NS_ERROR_DOM_FILE_NOT_FOUND_ERR;
+
+ case NS_ERROR_FILE_ALREADY_EXISTS:
+ return NS_ERROR_DOM_FILESYSTEM_PATH_EXISTS_ERR;
+
+ case NS_ERROR_FILE_NOT_DIRECTORY:
+ return NS_ERROR_DOM_FILESYSTEM_TYPE_MISMATCH_ERR;
+
+ case NS_ERROR_UNEXPECTED:
+ default:
+ return NS_ERROR_DOM_FILESYSTEM_UNKNOWN_ERR;
+ }
+}
+
+nsresult DispatchToIOThread(nsIRunnable* aRunnable) {
+ MOZ_ASSERT(aRunnable);
+
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ MOZ_ASSERT(target);
+
+ return target->Dispatch(aRunnable, NS_DISPATCH_NORMAL);
+}
+
+} // anonymous namespace
+
+/**
+ * FileSystemTaskBase class
+ */
+
+FileSystemTaskChildBase::FileSystemTaskChildBase(nsIGlobalObject* aGlobalObject,
+ FileSystemBase* aFileSystem)
+ : mErrorValue(NS_OK),
+ mFileSystem(aFileSystem),
+ mGlobalObject(aGlobalObject) {
+ MOZ_ASSERT(aFileSystem, "aFileSystem should not be null.");
+ aFileSystem->AssertIsOnOwningThread();
+ MOZ_ASSERT(aGlobalObject);
+}
+
+FileSystemTaskChildBase::~FileSystemTaskChildBase() {
+ mFileSystem->AssertIsOnOwningThread();
+}
+
+FileSystemBase* FileSystemTaskChildBase::GetFileSystem() const {
+ mFileSystem->AssertIsOnOwningThread();
+ return mFileSystem.get();
+}
+
+void FileSystemTaskChildBase::Start() {
+ mFileSystem->AssertIsOnOwningThread();
+
+ mozilla::ipc::PBackgroundChild* actor =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!actor)) {
+ // We are probably shutting down.
+ return;
+ }
+
+ nsAutoString serialization;
+ mFileSystem->SerializeDOMPath(serialization);
+
+ ErrorResult rv;
+ FileSystemParams params = GetRequestParams(serialization, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ return;
+ }
+
+ actor->SendPFileSystemRequestConstructor(this, params);
+}
+
+void FileSystemTaskChildBase::SetRequestResult(
+ const FileSystemResponseValue& aValue) {
+ mFileSystem->AssertIsOnOwningThread();
+
+ if (aValue.type() == FileSystemResponseValue::TFileSystemErrorResponse) {
+ FileSystemErrorResponse r = aValue;
+ mErrorValue = r.error();
+ } else {
+ ErrorResult rv;
+ SetSuccessRequestResult(aValue, rv);
+ mErrorValue = rv.StealNSResult();
+ }
+}
+
+mozilla::ipc::IPCResult FileSystemTaskChildBase::Recv__delete__(
+ const FileSystemResponseValue& aValue) {
+ mFileSystem->AssertIsOnOwningThread();
+
+ SetRequestResult(aValue);
+ HandlerCallback();
+ return IPC_OK();
+}
+
+void FileSystemTaskChildBase::SetError(const nsresult& aErrorValue) {
+ mErrorValue = FileSystemErrorFromNsError(aErrorValue);
+}
+
+/**
+ * FileSystemTaskParentBase class
+ */
+
+FileSystemTaskParentBase::FileSystemTaskParentBase(
+ FileSystemBase* aFileSystem, const FileSystemParams& aParam,
+ FileSystemRequestParent* aParent)
+ : Runnable("dom::FileSystemTaskParentBase"),
+ mErrorValue(NS_OK),
+ mFileSystem(aFileSystem),
+ mRequestParent(aParent),
+ mBackgroundEventTarget(GetCurrentSerialEventTarget()) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!");
+ MOZ_ASSERT(aFileSystem, "aFileSystem should not be null.");
+ MOZ_ASSERT(aParent);
+ MOZ_ASSERT(mBackgroundEventTarget);
+ mozilla::ipc::AssertIsOnBackgroundThread();
+}
+
+FileSystemTaskParentBase::~FileSystemTaskParentBase() {
+ // This task can be released on different threads because we dispatch it (as
+ // runnable) to main-thread, I/O and then back to the PBackground thread.
+ NS_ProxyRelease("FileSystemTaskParentBase::mFileSystem",
+ mBackgroundEventTarget, mFileSystem.forget());
+ NS_ProxyRelease("FileSystemTaskParentBase::mRequestParent",
+ mBackgroundEventTarget, mRequestParent.forget());
+}
+
+void FileSystemTaskParentBase::Start() {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ mFileSystem->AssertIsOnOwningThread();
+
+ DebugOnly<nsresult> rv = DispatchToIOThread(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "DispatchToIOThread failed");
+}
+
+void FileSystemTaskParentBase::HandleResult() {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ mFileSystem->AssertIsOnOwningThread();
+
+ if (mFileSystem->IsShutdown()) {
+ return;
+ }
+
+ MOZ_ASSERT(mRequestParent);
+ (void)mRequestParent->Send__delete__(mRequestParent, GetRequestResult());
+}
+
+FileSystemResponseValue FileSystemTaskParentBase::GetRequestResult() const {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ mFileSystem->AssertIsOnOwningThread();
+
+ if (HasError()) {
+ return FileSystemErrorResponse(mErrorValue);
+ }
+
+ ErrorResult rv;
+ FileSystemResponseValue value = GetSuccessRequestResult(rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return FileSystemErrorResponse(rv.StealNSResult());
+ }
+
+ return value;
+}
+
+void FileSystemTaskParentBase::SetError(const nsresult& aErrorValue) {
+ mErrorValue = FileSystemErrorFromNsError(aErrorValue);
+}
+
+NS_IMETHODIMP
+FileSystemTaskParentBase::Run() {
+ // This method can run in 2 different threads. Here why:
+ // 1. We are are on the I/O thread and we call IOWork().
+ // 2. After step 1, it returns back to the PBackground thread.
+
+ // Run I/O thread tasks
+ if (!mozilla::ipc::IsOnBackgroundThread()) {
+ nsresult rv = IOWork();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ SetError(rv);
+ }
+
+ // Let's go back to PBackground thread to finish the work.
+ rv = mBackgroundEventTarget->Dispatch(this, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+ }
+
+ // If we are here, it's because the I/O work has been done and we have to
+ // handle the result back via IPC.
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ HandleResult();
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/FileSystemTaskBase.h b/dom/filesystem/FileSystemTaskBase.h
new file mode 100644
index 0000000000..3d440403cc
--- /dev/null
+++ b/dom/filesystem/FileSystemTaskBase.h
@@ -0,0 +1,254 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileSystemTaskBase_h
+#define mozilla_dom_FileSystemTaskBase_h
+
+#include "mozilla/dom/FileSystemRequestParent.h"
+#include "mozilla/dom/PFileSystemRequestChild.h"
+#include "nsIGlobalObject.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class BlobImpl;
+class FileSystemBase;
+class FileSystemParams;
+
+/*
+ * The base class to implement a Task class.
+ * The file system operations can only be performed in the parent process. In
+ * order to avoid duplicated code, we used PBackground for child-parent and
+ * parent-parent communications.
+ *
+ * The following diagram illustrates the how a API call from the content page
+ * starts a task and gets call back results.
+ *
+ * The left block is the call sequence inside any process loading content, while
+ * the right block is the call sequence only inside the parent process.
+ *
+ * Page
+ * |
+ * | (1)
+ * ______|_________________________ | _________________________________
+ * | | | | | |
+ * | | | | | |
+ * | V | IPC | PBackground thread on |
+ * | [new FileSystemTaskChildBase()] | | | the parent process |
+ * | | | | | |
+ * | | (2) | | |
+ * | V | (3) | |
+ * | [GetRequestParams]------------------->[new FileSystemTaskParentBase()] |
+ * | | | | |
+ * | | | | | (4) _____________ |
+ * | | | | | | | |
+ * | | | | | | I/O Thread | |
+ * | | | | | | | |
+ * | | | | ---------> [IOWork] | |
+ * | | IPC | | | | |
+ * | | | | | | (5) | |
+ * | | | | -------------- | |
+ * | | | | | |_____________| |
+ * | | | | | |
+ * | | | | V |
+ * | | | | [HandleResult] |
+ * | | | | | |
+ * | | | | (6) |
+ * | | (7) | V |
+ * | [SetRequestResult]<---------------------[GetRequestResult] |
+ * | | | | |
+ * | | (8) | | | |
+ * | V | | | |
+ * |[HandlerCallback] | IPC | |
+ * |_______|_________________________| | |_________________________________|
+ * | |
+ * V
+ * Page
+ *
+ * 1. From the process that is handling the request
+ * Child/Parent (it can be in any process):
+ * (1) Call FileSystem API from content page with JS. Create a task and run.
+ * The base constructor [FileSystemTaskChildBase()] of the task should be
+ * called.
+ * (2) Forward the task to the parent process through the IPC and call
+ * [GetRequestParams] to prepare the parameters of the IPC.
+ * Parent:
+ * (3) The parent process receives IPC and handle it in
+ * FileystemRequestParent. Get the IPC parameters and create a task to run the
+ * IPC task.
+ * (4) The task operation will be performed in the member function of
+ * [IOWork]. A I/O thread will be created to run that function. If error occurs
+ * during the operation, call [SetError] to record the error and then abort.
+ * (5) After finishing the task operation, call [HandleResult] to send the
+ * result back to the child process though the IPC.
+ * (6) Call [GetRequestResult] request result to prepare the parameters of the
+ * IPC. Because the formats of the error result for different task are the
+ * same, FileSystemTaskChildBase can handle the error message without
+ * interfering.
+ * Each task only needs to implement its specific success result preparation
+ * function -[GetSuccessRequestResult].
+ * Child/Parent:
+ * (7) The process receives IPC and calls [SetRequestResult] to get the
+ * task result. Each task needs to implement its specific success result
+ * parsing function [SetSuccessRequestResult] to get the success result.
+ * (8) Call [HandlerCallback] to send the task result to the content page.
+ */
+class FileSystemTaskChildBase : public PFileSystemRequestChild {
+ friend class PFileSystemRequestChild;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(FileSystemTaskChildBase, final)
+
+ /*
+ * Start the task. It will dispatch all the information to the parent process,
+ * PBackground thread. This method must be called from the owning thread.
+ */
+ void Start();
+
+ /*
+ * The error codes are defined in xpcom/base/ErrorList.h and their
+ * corresponding error name and message are defined in dom/base/domerr.msg.
+ */
+ void SetError(const nsresult& aErrorCode);
+
+ FileSystemBase* GetFileSystem() const;
+
+ /*
+ * After the task is completed, this function will be called to pass the task
+ * result to the content page. This method is called in the owning thread.
+ * Override this function to handle the call back to the content page.
+ */
+ virtual void HandlerCallback() = 0;
+
+ bool HasError() const { return NS_FAILED(mErrorValue); }
+
+ protected:
+ /*
+ * To create a task to handle the page content request.
+ */
+ FileSystemTaskChildBase(nsIGlobalObject* aGlobalObject,
+ FileSystemBase* aFileSystem);
+
+ virtual ~FileSystemTaskChildBase();
+
+ /*
+ * Wrap the task parameter to FileSystemParams for sending it through IPC.
+ * It will be called when we need to forward a task from the child process to
+ * the parent process. This method runs in the owning thread.
+ * @param filesystem The string representation of the file system.
+ */
+ virtual FileSystemParams GetRequestParams(const nsString& aSerializedDOMPath,
+ ErrorResult& aRv) const = 0;
+
+ /*
+ * Unwrap the IPC message to get the task success result.
+ * It will be called when the task is completed successfully and an IPC
+ * message is received in the child process and we want to get the task
+ * success result. This method runs in the owning thread.
+ */
+ virtual void SetSuccessRequestResult(const FileSystemResponseValue& aValue,
+ ErrorResult& aRv) = 0;
+
+ // Overrides PFileSystemRequestChild
+ virtual mozilla::ipc::IPCResult Recv__delete__(
+ const FileSystemResponseValue& value) final;
+
+ nsresult mErrorValue;
+ RefPtr<FileSystemBase> mFileSystem;
+ nsCOMPtr<nsIGlobalObject> mGlobalObject;
+
+ private:
+ /*
+ * Unwrap the IPC message to get the task result.
+ * It will be called when the task is completed and an IPC message is received
+ * in the content process and we want to get the task result. This runs on the
+ * owning thread.
+ */
+ void SetRequestResult(const FileSystemResponseValue& aValue);
+};
+
+// This class is the 'alter ego' of FileSystemTaskChildBase in the PBackground
+// world.
+class FileSystemTaskParentBase : public Runnable {
+ public:
+ FileSystemTaskParentBase()
+ : Runnable("FileSystemTaskParentBase"),
+ mErrorValue(NS_ERROR_NOT_INITIALIZED) {}
+
+ /*
+ * Start the task. This must be called from the PBackground thread only.
+ */
+ void Start();
+
+ /*
+ * The error codes are defined in xpcom/base/ErrorList.h and their
+ * corresponding error name and message are defined in dom/base/domerr.msg.
+ */
+ void SetError(const nsresult& aErrorCode);
+
+ /*
+ * The function to perform task operation. It will be run on the I/O
+ * thread of the parent process.
+ * Overrides this function to define the task operation for individual task.
+ */
+ virtual nsresult IOWork() = 0;
+
+ /*
+ * Wrap the task success result to FileSystemResponseValue for sending it
+ * through IPC. This method runs in the PBackground thread.
+ * It will be called when the task is completed successfully and we need to
+ * send the task success result back to the child process.
+ */
+ virtual FileSystemResponseValue GetSuccessRequestResult(
+ ErrorResult& aRv) const = 0;
+
+ /*
+ * After finishing the task operation, handle the task result.
+ * If it is an IPC task, send back the IPC result. It runs on the PBackground
+ * thread.
+ */
+ void HandleResult();
+
+ bool HasError() const { return NS_FAILED(mErrorValue); }
+
+ NS_IMETHOD
+ Run() override;
+
+ virtual nsresult GetTargetPath(nsAString& aPath) const = 0;
+
+ private:
+ /*
+ * Wrap the task result to FileSystemResponseValue for sending it through IPC.
+ * It will be called when the task is completed and we need to
+ * send the task result back to the content. This runs on the PBackground
+ * thread.
+ */
+ FileSystemResponseValue GetRequestResult() const;
+
+ protected:
+ /*
+ * To create a parent process task delivered from the child process through
+ * IPC.
+ */
+ FileSystemTaskParentBase(FileSystemBase* aFileSystem,
+ const FileSystemParams& aParam,
+ FileSystemRequestParent* aParent);
+
+ virtual ~FileSystemTaskParentBase();
+
+ nsresult mErrorValue;
+ RefPtr<FileSystemBase> mFileSystem;
+ RefPtr<FileSystemRequestParent> mRequestParent;
+ nsCOMPtr<nsIEventTarget> mBackgroundEventTarget;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_FileSystemTaskBase_h
diff --git a/dom/filesystem/FileSystemUtils.cpp b/dom/filesystem/FileSystemUtils.cpp
new file mode 100644
index 0000000000..5d4b391596
--- /dev/null
+++ b/dom/filesystem/FileSystemUtils.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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/FileSystemUtils.h"
+#include "nsCharSeparatedTokenizer.h"
+#include "nsIEventTarget.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::dom {
+
+/* static */
+bool FileSystemUtils::IsDescendantPath(const nsAString& aPath,
+ const nsAString& aDescendantPath) {
+ // Check the sub-directory path to see if it has the parent path as prefix.
+ if (!aDescendantPath.Equals(aPath) &&
+ !StringBeginsWith(aDescendantPath, aPath)) {
+ return false;
+ }
+
+ return true;
+}
+
+/* static */
+bool FileSystemUtils::IsValidRelativeDOMPath(const nsAString& aPath,
+ nsTArray<nsString>& aParts) {
+ // We don't allow empty relative path to access the root.
+ if (aPath.IsEmpty()) {
+ return false;
+ }
+
+ // Leading and trailing "/" are not allowed.
+ if (aPath.First() == FILESYSTEM_DOM_PATH_SEPARATOR_CHAR ||
+ aPath.Last() == FILESYSTEM_DOM_PATH_SEPARATOR_CHAR) {
+ return false;
+ }
+
+ constexpr auto kCurrentDir = u"."_ns;
+ constexpr auto kParentDir = u".."_ns;
+
+ // Split path and check each path component.
+ for (const nsAString& pathComponent :
+ nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>{
+ aPath, FILESYSTEM_DOM_PATH_SEPARATOR_CHAR}
+ .ToRange()) {
+ // The path containing empty components, such as "foo//bar", is invalid.
+ // We don't allow paths, such as "../foo", "foo/./bar" and "foo/../bar",
+ // to walk up the directory.
+ if (pathComponent.IsEmpty() || pathComponent.Equals(kCurrentDir) ||
+ pathComponent.Equals(kParentDir)) {
+ return false;
+ }
+
+ aParts.AppendElement(pathComponent);
+ }
+
+ return true;
+}
+
+/* static */
+nsresult FileSystemUtils::DispatchRunnable(
+ nsIGlobalObject* aGlobal, already_AddRefed<nsIRunnable>&& aRunnable) {
+ nsCOMPtr<nsIRunnable> runnable = aRunnable;
+
+ nsCOMPtr<nsIEventTarget> target;
+ if (!aGlobal) {
+ target = GetMainThreadSerialEventTarget();
+ } else {
+ target = aGlobal->EventTargetFor(TaskCategory::Other);
+ }
+
+ MOZ_ASSERT(target);
+
+ nsresult rv = target->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/FileSystemUtils.h b/dom/filesystem/FileSystemUtils.h
new file mode 100644
index 0000000000..7f82e8474f
--- /dev/null
+++ b/dom/filesystem/FileSystemUtils.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileSystemUtils_h
+#define mozilla_dom_FileSystemUtils_h
+
+#include "nsIGlobalObject.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+class nsIFile;
+class nsIRunnable;
+
+namespace mozilla::dom {
+
+#define FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL "/"
+#define FILESYSTEM_DOM_PATH_SEPARATOR_CHAR '/'
+
+/*
+ * This class is for error handling.
+ * All methods in this class are static.
+ */
+class FileSystemUtils {
+ public:
+ /*
+ * Return true if aDescendantPath is a descendant of aPath.
+ */
+ static bool IsDescendantPath(const nsAString& aPath,
+ const nsAString& aDescendantPath);
+
+ /**
+ * Return true if this is valid DOMPath. It also splits the path in
+ * subdirectories and stores them in aParts.
+ */
+ static bool IsValidRelativeDOMPath(const nsAString& aPath,
+ nsTArray<nsString>& aParts);
+
+ /**
+ * Helper method. If aGlobal is null, the SystemGroup EventTarget will be
+ * used.
+ */
+ static nsresult DispatchRunnable(nsIGlobalObject* aGlobal,
+ already_AddRefed<nsIRunnable>&& aRunnable);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileSystemUtils_h
diff --git a/dom/filesystem/GetDirectoryListingTask.cpp b/dom/filesystem/GetDirectoryListingTask.cpp
new file mode 100644
index 0000000000..f78a04a9d0
--- /dev/null
+++ b/dom/filesystem/GetDirectoryListingTask.cpp
@@ -0,0 +1,371 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "GetDirectoryListingTask.h"
+
+#include "HTMLSplitOnSpacesTokenizer.h"
+#include "js/Value.h"
+#include "mozilla/dom/FileBlobImpl.h"
+#include "mozilla/dom/FileSystemBase.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include "mozilla/dom/PFileSystemParams.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "nsIFile.h"
+#include "nsString.h"
+
+namespace mozilla::dom {
+
+/**
+ * GetDirectoryListingTaskChild
+ */
+
+/* static */
+already_AddRefed<GetDirectoryListingTaskChild>
+GetDirectoryListingTaskChild::Create(FileSystemBase* aFileSystem,
+ Directory* aDirectory,
+ nsIFile* aTargetPath,
+ const nsAString& aFilters,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(aFileSystem);
+ MOZ_ASSERT(aDirectory);
+ aFileSystem->AssertIsOnOwningThread();
+
+ nsCOMPtr<nsIGlobalObject> globalObject = aFileSystem->GetParentObject();
+ MOZ_ASSERT(globalObject);
+
+ RefPtr<GetDirectoryListingTaskChild> task = new GetDirectoryListingTaskChild(
+ globalObject, aFileSystem, aDirectory, aTargetPath, aFilters);
+
+ // aTargetPath can be null. In this case SetError will be called.
+
+ task->mPromise = Promise::Create(globalObject, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return task.forget();
+}
+
+GetDirectoryListingTaskChild::GetDirectoryListingTaskChild(
+ nsIGlobalObject* aGlobalObject, FileSystemBase* aFileSystem,
+ Directory* aDirectory, nsIFile* aTargetPath, const nsAString& aFilters)
+ : FileSystemTaskChildBase(aGlobalObject, aFileSystem),
+ mDirectory(aDirectory),
+ mTargetPath(aTargetPath),
+ mFilters(aFilters) {
+ MOZ_ASSERT(aFileSystem);
+ aFileSystem->AssertIsOnOwningThread();
+}
+
+GetDirectoryListingTaskChild::~GetDirectoryListingTaskChild() {
+ mFileSystem->AssertIsOnOwningThread();
+}
+
+already_AddRefed<Promise> GetDirectoryListingTaskChild::GetPromise() {
+ mFileSystem->AssertIsOnOwningThread();
+ return RefPtr<Promise>(mPromise).forget();
+}
+
+FileSystemParams GetDirectoryListingTaskChild::GetRequestParams(
+ const nsString& aSerializedDOMPath, ErrorResult& aRv) const {
+ mFileSystem->AssertIsOnOwningThread();
+
+ // this is the real path.
+ nsAutoString path;
+ aRv = mTargetPath->GetPath(path);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return FileSystemGetDirectoryListingParams();
+ }
+
+ // this is the dom path.
+ nsAutoString directoryPath;
+ mDirectory->GetPath(directoryPath, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return FileSystemGetDirectoryListingParams();
+ }
+
+ return FileSystemGetDirectoryListingParams(aSerializedDOMPath, path,
+ directoryPath, mFilters);
+}
+
+void GetDirectoryListingTaskChild::SetSuccessRequestResult(
+ const FileSystemResponseValue& aValue, ErrorResult& aRv) {
+ mFileSystem->AssertIsOnOwningThread();
+ MOZ_ASSERT(aValue.type() ==
+ FileSystemResponseValue::TFileSystemDirectoryListingResponse);
+
+ FileSystemDirectoryListingResponse r = aValue;
+ for (uint32_t i = 0; i < r.data().Length(); ++i) {
+ const FileSystemDirectoryListingResponseData& data = r.data()[i];
+
+ OwningFileOrDirectory* ofd = mTargetData.AppendElement(fallible);
+ if (!ofd) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ if (data.type() == FileSystemDirectoryListingResponseData::
+ TFileSystemDirectoryListingResponseFile) {
+ const FileSystemDirectoryListingResponseFile& d =
+ data.get_FileSystemDirectoryListingResponseFile();
+
+ RefPtr<BlobImpl> blobImpl = IPCBlobUtils::Deserialize(d.blob());
+ MOZ_ASSERT(blobImpl);
+
+ nsCOMPtr<nsIGlobalObject> globalObject = mFileSystem->GetParentObject();
+ MOZ_ASSERT(globalObject);
+
+ RefPtr<File> file = File::Create(globalObject, blobImpl);
+ MOZ_ASSERT(file);
+
+ ofd->SetAsFile() = file;
+ } else {
+ MOZ_ASSERT(data.type() ==
+ FileSystemDirectoryListingResponseData::
+ TFileSystemDirectoryListingResponseDirectory);
+ const FileSystemDirectoryListingResponseDirectory& d =
+ data.get_FileSystemDirectoryListingResponseDirectory();
+
+ nsCOMPtr<nsIFile> path;
+ aRv = NS_NewLocalFile(d.directoryRealPath(), true, getter_AddRefs(path));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ RefPtr<Directory> directory =
+ Directory::Create(mFileSystem->GetParentObject(), path, mFileSystem);
+ MOZ_ASSERT(directory);
+
+ ofd->SetAsDirectory() = directory;
+ }
+ }
+}
+
+void GetDirectoryListingTaskChild::HandlerCallback() {
+ mFileSystem->AssertIsOnOwningThread();
+
+ if (mFileSystem->IsShutdown()) {
+ mPromise = nullptr;
+ return;
+ }
+
+ if (HasError()) {
+ mPromise->MaybeReject(mErrorValue);
+ mPromise = nullptr;
+ return;
+ }
+
+ mPromise->MaybeResolve(mTargetData);
+ mPromise = nullptr;
+}
+
+/**
+ * GetDirectoryListingTaskParent
+ */
+
+/* static */
+already_AddRefed<GetDirectoryListingTaskParent>
+GetDirectoryListingTaskParent::Create(
+ FileSystemBase* aFileSystem,
+ const FileSystemGetDirectoryListingParams& aParam,
+ FileSystemRequestParent* aParent, ErrorResult& aRv) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!");
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aFileSystem);
+
+ RefPtr<GetDirectoryListingTaskParent> task =
+ new GetDirectoryListingTaskParent(aFileSystem, aParam, aParent);
+
+ aRv = NS_NewLocalFile(aParam.realPath(), true,
+ getter_AddRefs(task->mTargetPath));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return task.forget();
+}
+
+GetDirectoryListingTaskParent::GetDirectoryListingTaskParent(
+ FileSystemBase* aFileSystem,
+ const FileSystemGetDirectoryListingParams& aParam,
+ FileSystemRequestParent* aParent)
+ : FileSystemTaskParentBase(aFileSystem, aParam, aParent),
+ mDOMPath(aParam.domPath()),
+ mFilters(aParam.filters()) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!");
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aFileSystem);
+}
+
+FileSystemResponseValue GetDirectoryListingTaskParent::GetSuccessRequestResult(
+ ErrorResult& aRv) const {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+
+ nsTArray<FileSystemDirectoryListingResponseData> inputs;
+
+ for (unsigned i = 0; i < mTargetData.Length(); i++) {
+ if (mTargetData[i].mType == FileOrDirectoryPath::eFilePath) {
+ nsCOMPtr<nsIFile> path;
+ nsresult rv =
+ NS_NewLocalFile(mTargetData[i].mPath, true, getter_AddRefs(path));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ FileSystemDirectoryListingResponseFile fileData;
+ RefPtr<BlobImpl> blobImpl = new FileBlobImpl(path);
+
+ nsAutoString filePath;
+ filePath.Assign(mDOMPath);
+
+ // This is specific for unix root filesystem.
+ if (!mDOMPath.EqualsLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL)) {
+ filePath.AppendLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
+ }
+
+ nsAutoString name;
+ blobImpl->GetName(name);
+ filePath.Append(name);
+ blobImpl->SetDOMPath(filePath);
+
+ IPCBlob ipcBlob;
+ rv = IPCBlobUtils::Serialize(blobImpl, ipcBlob);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ fileData.blob() = ipcBlob;
+ inputs.AppendElement(fileData);
+ } else {
+ MOZ_ASSERT(mTargetData[i].mType == FileOrDirectoryPath::eDirectoryPath);
+ FileSystemDirectoryListingResponseDirectory directoryData;
+ directoryData.directoryRealPath() = mTargetData[i].mPath;
+ inputs.AppendElement(directoryData);
+ }
+ }
+
+ FileSystemDirectoryListingResponse response;
+ response.data() = std::move(inputs);
+ return response;
+}
+
+nsresult GetDirectoryListingTaskParent::IOWork() {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!");
+ MOZ_ASSERT(!NS_IsMainThread(), "Only call on worker thread!");
+
+ if (mFileSystem->IsShutdown()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool exists;
+ nsresult rv = mTargetPath->Exists(&exists);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!exists) {
+ if (!mFileSystem->ShouldCreateDirectory()) {
+ return NS_ERROR_DOM_FILE_NOT_FOUND_ERR;
+ }
+
+ rv = mTargetPath->Create(nsIFile::DIRECTORY_TYPE, 0777);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ // Get isDirectory.
+ bool isDir;
+ rv = mTargetPath->IsDirectory(&isDir);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!isDir) {
+ return NS_ERROR_DOM_FILESYSTEM_TYPE_MISMATCH_ERR;
+ }
+
+ nsCOMPtr<nsIDirectoryEnumerator> entries;
+ rv = mTargetPath->GetDirectoryEntries(getter_AddRefs(entries));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ bool filterOutSensitive = false;
+ {
+ HTMLSplitOnSpacesTokenizer tokenizer(mFilters, ';');
+ nsAutoString token;
+ while (tokenizer.hasMoreTokens()) {
+ token = tokenizer.nextToken();
+ if (token.EqualsLiteral("filter-out-sensitive")) {
+ filterOutSensitive = true;
+ } else {
+ MOZ_CRASH("Unrecognized filter");
+ }
+ }
+ }
+
+ for (;;) {
+ nsCOMPtr<nsIFile> currFile;
+ if (NS_WARN_IF(NS_FAILED(entries->GetNextFile(getter_AddRefs(currFile)))) ||
+ !currFile) {
+ break;
+ }
+ bool isLink, isSpecial, isFile;
+ if (NS_WARN_IF(NS_FAILED(currFile->IsSymlink(&isLink)) ||
+ NS_FAILED(currFile->IsSpecial(&isSpecial))) ||
+ // Although we allow explicit individual selection of symlinks via the
+ // file picker, we do not process symlinks in directory traversal. Our
+ // specific policy decision is documented at
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1813299#c20
+ isLink || isSpecial) {
+ continue;
+ }
+ if (NS_WARN_IF(NS_FAILED(currFile->IsFile(&isFile)) ||
+ NS_FAILED(currFile->IsDirectory(&isDir))) ||
+ !(isFile || isDir)) {
+ continue;
+ }
+
+ if (filterOutSensitive) {
+ bool isHidden;
+ if (NS_WARN_IF(NS_FAILED(currFile->IsHidden(&isHidden))) || isHidden) {
+ continue;
+ }
+ nsAutoString leafName;
+ if (NS_WARN_IF(NS_FAILED(currFile->GetLeafName(leafName)))) {
+ continue;
+ }
+ if (leafName[0] == char16_t('.')) {
+ continue;
+ }
+ }
+
+ nsAutoString path;
+ if (NS_WARN_IF(NS_FAILED(currFile->GetPath(path)))) {
+ continue;
+ }
+
+ FileOrDirectoryPath element;
+ element.mPath = path;
+ element.mType = isDir ? FileOrDirectoryPath::eDirectoryPath
+ : FileOrDirectoryPath::eFilePath;
+
+ if (!mTargetData.AppendElement(element, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult GetDirectoryListingTaskParent::GetTargetPath(nsAString& aPath) const {
+ return mTargetPath->GetPath(aPath);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/GetDirectoryListingTask.h b/dom/filesystem/GetDirectoryListingTask.h
new file mode 100644
index 0000000000..afb5296ac5
--- /dev/null
+++ b/dom/filesystem/GetDirectoryListingTask.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_GetDirectoryListing_h
+#define mozilla_dom_GetDirectoryListing_h
+
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/FileSystemTaskBase.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class BlobImpl;
+class FileSystemGetDirectoryListingParams;
+class OwningFileOrDirectory;
+
+class GetDirectoryListingTaskChild final : public FileSystemTaskChildBase {
+ public:
+ static already_AddRefed<GetDirectoryListingTaskChild> Create(
+ FileSystemBase* aFileSystem, Directory* aDirectory, nsIFile* aTargetPath,
+ const nsAString& aFilters, ErrorResult& aRv);
+
+ virtual ~GetDirectoryListingTaskChild();
+
+ already_AddRefed<Promise> GetPromise();
+
+ private:
+ // If aDirectoryOnly is set, we should ensure that the target is a directory.
+ GetDirectoryListingTaskChild(nsIGlobalObject* aGlobalObject,
+ FileSystemBase* aFileSystem,
+ Directory* aDirectory, nsIFile* aTargetPath,
+ const nsAString& aFilters);
+
+ virtual FileSystemParams GetRequestParams(const nsString& aSerializedDOMPath,
+ ErrorResult& aRv) const override;
+
+ virtual void SetSuccessRequestResult(const FileSystemResponseValue& aValue,
+ ErrorResult& aRv) override;
+
+ virtual void HandlerCallback() override;
+
+ RefPtr<Promise> mPromise;
+ RefPtr<Directory> mDirectory;
+ nsCOMPtr<nsIFile> mTargetPath;
+ nsString mFilters;
+
+ FallibleTArray<OwningFileOrDirectory> mTargetData;
+};
+
+class GetDirectoryListingTaskParent final : public FileSystemTaskParentBase {
+ public:
+ static already_AddRefed<GetDirectoryListingTaskParent> Create(
+ FileSystemBase* aFileSystem,
+ const FileSystemGetDirectoryListingParams& aParam,
+ FileSystemRequestParent* aParent, ErrorResult& aRv);
+
+ nsresult GetTargetPath(nsAString& aPath) const override;
+
+ private:
+ GetDirectoryListingTaskParent(
+ FileSystemBase* aFileSystem,
+ const FileSystemGetDirectoryListingParams& aParam,
+ FileSystemRequestParent* aParent);
+
+ virtual FileSystemResponseValue GetSuccessRequestResult(
+ ErrorResult& aRv) const override;
+
+ virtual nsresult IOWork() override;
+
+ nsCOMPtr<nsIFile> mTargetPath;
+ nsString mDOMPath;
+ nsString mFilters;
+
+ struct FileOrDirectoryPath {
+ nsString mPath;
+
+ enum { eFilePath, eDirectoryPath } mType;
+ };
+
+ FallibleTArray<FileOrDirectoryPath> mTargetData;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_GetDirectoryListing_h
diff --git a/dom/filesystem/GetFileOrDirectoryTask.cpp b/dom/filesystem/GetFileOrDirectoryTask.cpp
new file mode 100644
index 0000000000..7f7d1c2816
--- /dev/null
+++ b/dom/filesystem/GetFileOrDirectoryTask.cpp
@@ -0,0 +1,270 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "GetFileOrDirectoryTask.h"
+
+#include "js/Value.h"
+#include "mozilla/dom/FileBlobImpl.h"
+#include "mozilla/dom/FileSystemBase.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/PFileSystemParams.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "nsIFile.h"
+#include "nsString.h"
+
+namespace mozilla::dom {
+
+/**
+ * GetFileOrDirectoryTaskChild
+ */
+
+/* static */
+already_AddRefed<GetFileOrDirectoryTaskChild>
+GetFileOrDirectoryTaskChild::Create(FileSystemBase* aFileSystem,
+ nsIFile* aTargetPath, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+ MOZ_ASSERT(aFileSystem);
+
+ nsCOMPtr<nsIGlobalObject> globalObject = aFileSystem->GetParentObject();
+ if (NS_WARN_IF(!globalObject)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<GetFileOrDirectoryTaskChild> task =
+ new GetFileOrDirectoryTaskChild(globalObject, aFileSystem, aTargetPath);
+
+ // aTargetPath can be null. In this case SetError will be called.
+
+ task->mPromise = Promise::Create(globalObject, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return task.forget();
+}
+
+GetFileOrDirectoryTaskChild::GetFileOrDirectoryTaskChild(
+ nsIGlobalObject* aGlobalObject, FileSystemBase* aFileSystem,
+ nsIFile* aTargetPath)
+ : FileSystemTaskChildBase(aGlobalObject, aFileSystem),
+ mTargetPath(aTargetPath) {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+ MOZ_ASSERT(aFileSystem);
+}
+
+GetFileOrDirectoryTaskChild::~GetFileOrDirectoryTaskChild() {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+already_AddRefed<Promise> GetFileOrDirectoryTaskChild::GetPromise() {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+ return RefPtr<Promise>(mPromise).forget();
+}
+
+FileSystemParams GetFileOrDirectoryTaskChild::GetRequestParams(
+ const nsString& aSerializedDOMPath, ErrorResult& aRv) const {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+
+ nsAutoString path;
+ aRv = mTargetPath->GetPath(path);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return FileSystemGetFileOrDirectoryParams();
+ }
+
+ return FileSystemGetFileOrDirectoryParams(aSerializedDOMPath, path);
+}
+
+void GetFileOrDirectoryTaskChild::SetSuccessRequestResult(
+ const FileSystemResponseValue& aValue, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+ switch (aValue.type()) {
+ case FileSystemResponseValue::TFileSystemFileResponse: {
+ FileSystemFileResponse r = aValue;
+
+ RefPtr<BlobImpl> blobImpl = IPCBlobUtils::Deserialize(r.blob());
+ MOZ_ASSERT(blobImpl);
+
+ nsCOMPtr<nsIGlobalObject> globalObject = mFileSystem->GetParentObject();
+ MOZ_ASSERT(globalObject);
+
+ mResultFile = File::Create(globalObject, blobImpl);
+ if (NS_WARN_IF(!mResultFile)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ }
+ break;
+ }
+ case FileSystemResponseValue::TFileSystemDirectoryResponse: {
+ FileSystemDirectoryResponse r = aValue;
+
+ nsCOMPtr<nsIFile> file;
+ aRv = NS_NewLocalFile(r.realPath(), true, getter_AddRefs(file));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ mResultDirectory =
+ Directory::Create(mFileSystem->GetParentObject(), file, mFileSystem);
+ MOZ_ASSERT(mResultDirectory);
+ break;
+ }
+ default: {
+ MOZ_CRASH("not reached");
+ break;
+ }
+ }
+}
+
+void GetFileOrDirectoryTaskChild::HandlerCallback() {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+ if (mFileSystem->IsShutdown()) {
+ mPromise = nullptr;
+ return;
+ }
+
+ if (HasError()) {
+ mPromise->MaybeReject(mErrorValue);
+ mPromise = nullptr;
+ return;
+ }
+
+ if (mResultDirectory) {
+ mPromise->MaybeResolve(mResultDirectory);
+ mResultDirectory = nullptr;
+ mPromise = nullptr;
+ return;
+ }
+
+ MOZ_ASSERT(mResultFile);
+ mPromise->MaybeResolve(mResultFile);
+ mResultFile = nullptr;
+ mPromise = nullptr;
+}
+
+/**
+ * GetFileOrDirectoryTaskParent
+ */
+
+/* static */
+already_AddRefed<GetFileOrDirectoryTaskParent>
+GetFileOrDirectoryTaskParent::Create(
+ FileSystemBase* aFileSystem,
+ const FileSystemGetFileOrDirectoryParams& aParam,
+ FileSystemRequestParent* aParent, ErrorResult& aRv) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!");
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aFileSystem);
+
+ RefPtr<GetFileOrDirectoryTaskParent> task =
+ new GetFileOrDirectoryTaskParent(aFileSystem, aParam, aParent);
+
+ aRv = NS_NewLocalFile(aParam.realPath(), true,
+ getter_AddRefs(task->mTargetPath));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return task.forget();
+}
+
+GetFileOrDirectoryTaskParent::GetFileOrDirectoryTaskParent(
+ FileSystemBase* aFileSystem,
+ const FileSystemGetFileOrDirectoryParams& aParam,
+ FileSystemRequestParent* aParent)
+ : FileSystemTaskParentBase(aFileSystem, aParam, aParent),
+ mIsDirectory(false) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!");
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aFileSystem);
+}
+
+FileSystemResponseValue GetFileOrDirectoryTaskParent::GetSuccessRequestResult(
+ ErrorResult& aRv) const {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+
+ nsAutoString path;
+ aRv = mTargetPath->GetPath(path);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return FileSystemDirectoryResponse();
+ }
+
+ if (mIsDirectory) {
+ return FileSystemDirectoryResponse(path);
+ }
+
+ RefPtr<BlobImpl> blobImpl = new FileBlobImpl(mTargetPath);
+
+ IPCBlob ipcBlob;
+ aRv = IPCBlobUtils::Serialize(blobImpl, ipcBlob);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return FileSystemDirectoryResponse();
+ }
+
+ return FileSystemFileResponse(ipcBlob);
+}
+
+nsresult GetFileOrDirectoryTaskParent::IOWork() {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!");
+ MOZ_ASSERT(!NS_IsMainThread(), "Only call on worker thread!");
+
+ if (mFileSystem->IsShutdown()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Whether we want to get the root directory.
+ bool exists;
+ nsresult rv = mTargetPath->Exists(&exists);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!exists) {
+ if (!mFileSystem->ShouldCreateDirectory()) {
+ return NS_ERROR_DOM_FILE_NOT_FOUND_ERR;
+ }
+
+ rv = mTargetPath->Create(nsIFile::DIRECTORY_TYPE, 0777);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ // Get isDirectory.
+ rv = mTargetPath->IsDirectory(&mIsDirectory);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (mIsDirectory) {
+ return NS_OK;
+ }
+
+ bool isFile;
+ // Get isFile
+ rv = mTargetPath->IsFile(&isFile);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!isFile) {
+ // Neither directory or file.
+ return NS_ERROR_DOM_FILESYSTEM_TYPE_MISMATCH_ERR;
+ }
+
+ if (!mFileSystem->IsSafeFile(mTargetPath)) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+
+ return NS_OK;
+}
+
+nsresult GetFileOrDirectoryTaskParent::GetTargetPath(nsAString& aPath) const {
+ return mTargetPath->GetPath(aPath);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/GetFileOrDirectoryTask.h b/dom/filesystem/GetFileOrDirectoryTask.h
new file mode 100644
index 0000000000..8d15d9804f
--- /dev/null
+++ b/dom/filesystem/GetFileOrDirectoryTask.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_GetFileOrDirectory_h
+#define mozilla_dom_GetFileOrDirectory_h
+
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/FileSystemTaskBase.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class BlobImpl;
+class FileSystemGetFileOrDirectoryParams;
+
+class GetFileOrDirectoryTaskChild final : public FileSystemTaskChildBase {
+ public:
+ static already_AddRefed<GetFileOrDirectoryTaskChild> Create(
+ FileSystemBase* aFileSystem, nsIFile* aTargetPath, ErrorResult& aRv);
+
+ virtual ~GetFileOrDirectoryTaskChild();
+
+ already_AddRefed<Promise> GetPromise();
+
+ protected:
+ virtual FileSystemParams GetRequestParams(const nsString& aSerializedDOMPath,
+ ErrorResult& aRv) const override;
+
+ virtual void SetSuccessRequestResult(const FileSystemResponseValue& aValue,
+ ErrorResult& aRv) override;
+ virtual void HandlerCallback() override;
+
+ private:
+ GetFileOrDirectoryTaskChild(nsIGlobalObject* aGlobalObject,
+ FileSystemBase* aFileSystem,
+ nsIFile* aTargetPath);
+
+ RefPtr<Promise> mPromise;
+ nsCOMPtr<nsIFile> mTargetPath;
+
+ RefPtr<File> mResultFile;
+ RefPtr<Directory> mResultDirectory;
+};
+
+class GetFileOrDirectoryTaskParent final : public FileSystemTaskParentBase {
+ public:
+ static already_AddRefed<GetFileOrDirectoryTaskParent> Create(
+ FileSystemBase* aFileSystem,
+ const FileSystemGetFileOrDirectoryParams& aParam,
+ FileSystemRequestParent* aParent, ErrorResult& aRv);
+
+ nsresult GetTargetPath(nsAString& aPath) const override;
+
+ protected:
+ virtual FileSystemResponseValue GetSuccessRequestResult(
+ ErrorResult& aRv) const override;
+
+ virtual nsresult IOWork() override;
+
+ private:
+ GetFileOrDirectoryTaskParent(FileSystemBase* aFileSystem,
+ const FileSystemGetFileOrDirectoryParams& aParam,
+ FileSystemRequestParent* aParent);
+
+ nsCOMPtr<nsIFile> mTargetPath;
+
+ // Whether we get a directory.
+ bool mIsDirectory;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_GetFileOrDirectory_h
diff --git a/dom/filesystem/GetFilesHelper.cpp b/dom/filesystem/GetFilesHelper.cpp
new file mode 100644
index 0000000000..5c91b4cfe5
--- /dev/null
+++ b/dom/filesystem/GetFilesHelper.cpp
@@ -0,0 +1,506 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "GetFilesHelper.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/FileBlobImpl.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include "mozilla/ipc/IPCStreamUtils.h"
+#include "FileSystemUtils.h"
+#include "nsNetCID.h"
+#include "nsProxyRelease.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+// This class is used in the DTOR of GetFilesHelper to release resources in the
+// correct thread.
+class ReleaseRunnable final : public Runnable {
+ public:
+ static void MaybeReleaseOnMainThread(
+ nsTArray<RefPtr<Promise>>&& aPromises,
+ nsTArray<RefPtr<GetFilesCallback>>&& aCallbacks) {
+ if (NS_IsMainThread()) {
+ return;
+ }
+
+ RefPtr<ReleaseRunnable> runnable =
+ new ReleaseRunnable(std::move(aPromises), std::move(aCallbacks));
+ FileSystemUtils::DispatchRunnable(nullptr, runnable.forget());
+ }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mPromises.Clear();
+ mCallbacks.Clear();
+
+ return NS_OK;
+ }
+
+ private:
+ ReleaseRunnable(nsTArray<RefPtr<Promise>>&& aPromises,
+ nsTArray<RefPtr<GetFilesCallback>>&& aCallbacks)
+ : Runnable("dom::ReleaseRunnable"),
+ mPromises(std::move(aPromises)),
+ mCallbacks(std::move(aCallbacks)) {}
+
+ nsTArray<RefPtr<Promise>> mPromises;
+ nsTArray<RefPtr<GetFilesCallback>> mCallbacks;
+};
+
+} // namespace
+
+///////////////////////////////////////////////////////////////////////////////
+// GetFilesHelper Base class
+
+already_AddRefed<GetFilesHelper> GetFilesHelper::Create(
+ const nsTArray<OwningFileOrDirectory>& aFilesOrDirectory,
+ bool aRecursiveFlag, ErrorResult& aRv) {
+ RefPtr<GetFilesHelper> helper;
+
+ if (XRE_IsParentProcess()) {
+ helper = new GetFilesHelper(aRecursiveFlag);
+ } else {
+ helper = new GetFilesHelperChild(aRecursiveFlag);
+ }
+
+ nsAutoString directoryPath;
+
+ for (uint32_t i = 0; i < aFilesOrDirectory.Length(); ++i) {
+ const OwningFileOrDirectory& data = aFilesOrDirectory[i];
+ if (data.IsFile()) {
+ if (!helper->mTargetBlobImplArray.AppendElement(data.GetAsFile()->Impl(),
+ fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+ } else {
+ MOZ_ASSERT(data.IsDirectory());
+
+ // We support the upload of only 1 top-level directory from our
+ // directory picker. This means that we cannot have more than 1
+ // Directory object in aFilesOrDirectory array.
+ MOZ_ASSERT(directoryPath.IsEmpty());
+
+ RefPtr<Directory> directory = data.GetAsDirectory();
+ MOZ_ASSERT(directory);
+
+ aRv = directory->GetFullRealPath(directoryPath);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ }
+ }
+
+ // No directories to explore.
+ if (directoryPath.IsEmpty()) {
+ helper->mListingCompleted = true;
+ return helper.forget();
+ }
+
+ MOZ_ASSERT(helper->mTargetBlobImplArray.IsEmpty());
+ helper->SetDirectoryPath(directoryPath);
+
+ helper->Work(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return helper.forget();
+}
+
+GetFilesHelper::GetFilesHelper(bool aRecursiveFlag)
+ : Runnable("GetFilesHelper"),
+ GetFilesHelperBase(aRecursiveFlag),
+ mListingCompleted(false),
+ mErrorResult(NS_OK),
+ mMutex("GetFilesHelper::mMutex"),
+ mCanceled(false) {}
+
+GetFilesHelper::~GetFilesHelper() {
+ ReleaseRunnable::MaybeReleaseOnMainThread(std::move(mPromises),
+ std::move(mCallbacks));
+}
+
+void GetFilesHelper::AddPromise(Promise* aPromise) {
+ MOZ_ASSERT(aPromise);
+
+ // Still working.
+ if (!mListingCompleted) {
+ mPromises.AppendElement(aPromise);
+ return;
+ }
+
+ MOZ_ASSERT(mPromises.IsEmpty());
+ ResolveOrRejectPromise(aPromise);
+}
+
+void GetFilesHelper::AddCallback(GetFilesCallback* aCallback) {
+ MOZ_ASSERT(aCallback);
+
+ // Still working.
+ if (!mListingCompleted) {
+ mCallbacks.AppendElement(aCallback);
+ return;
+ }
+
+ MOZ_ASSERT(mCallbacks.IsEmpty());
+ RunCallback(aCallback);
+}
+
+void GetFilesHelper::Unlink() {
+ mPromises.Clear();
+ mCallbacks.Clear();
+
+ {
+ MutexAutoLock lock(mMutex);
+ mCanceled = true;
+ }
+
+ Cancel();
+}
+
+void GetFilesHelper::Traverse(nsCycleCollectionTraversalCallback& cb) {
+ GetFilesHelper* tmp = this;
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromises);
+}
+
+void GetFilesHelper::Work(ErrorResult& aRv) {
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ MOZ_ASSERT(target);
+
+ aRv = target->Dispatch(this, NS_DISPATCH_NORMAL);
+}
+
+NS_IMETHODIMP
+GetFilesHelper::Run() {
+ MOZ_ASSERT(!mDirectoryPath.IsEmpty());
+ MOZ_ASSERT(!mListingCompleted);
+
+ // First step is to retrieve the list of file paths.
+ // This happens in the I/O thread.
+ if (!NS_IsMainThread()) {
+ RunIO();
+
+ // If this operation has been canceled, we don't have to go back to
+ // main-thread.
+ if (IsCanceled()) {
+ return NS_OK;
+ }
+
+ RefPtr<Runnable> runnable = this;
+ return FileSystemUtils::DispatchRunnable(nullptr, runnable.forget());
+ }
+
+ // We are here, but we should not do anything on this thread because, in the
+ // meantime, the operation has been canceled.
+ if (IsCanceled()) {
+ return NS_OK;
+ }
+
+ OperationCompleted();
+ return NS_OK;
+}
+
+void GetFilesHelper::OperationCompleted() {
+ // We mark the operation as completed here.
+ mListingCompleted = true;
+
+ // Let's process the pending promises.
+ nsTArray<RefPtr<Promise>> promises = std::move(mPromises);
+
+ for (uint32_t i = 0; i < promises.Length(); ++i) {
+ ResolveOrRejectPromise(promises[i]);
+ }
+
+ // Let's process the pending callbacks.
+ nsTArray<RefPtr<GetFilesCallback>> callbacks = std::move(mCallbacks);
+
+ for (uint32_t i = 0; i < callbacks.Length(); ++i) {
+ RunCallback(callbacks[i]);
+ }
+}
+
+void GetFilesHelper::RunIO() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(!mDirectoryPath.IsEmpty());
+ MOZ_ASSERT(!mListingCompleted);
+
+ nsCOMPtr<nsIFile> file;
+ mErrorResult = NS_NewLocalFile(mDirectoryPath, true, getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(mErrorResult))) {
+ return;
+ }
+
+ nsAutoString leafName;
+ mErrorResult = file->GetLeafName(leafName);
+ if (NS_WARN_IF(NS_FAILED(mErrorResult))) {
+ return;
+ }
+
+ nsAutoString domPath;
+ domPath.AssignLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
+ domPath.Append(leafName);
+
+ mErrorResult = ExploreDirectory(domPath, file);
+}
+
+nsresult GetFilesHelperBase::ExploreDirectory(const nsAString& aDOMPath,
+ nsIFile* aFile) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aFile);
+
+ // We check if this operation has to be terminated at each recursion.
+ if (IsCanceled()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDirectoryEnumerator> entries;
+ nsresult rv = aFile->GetDirectoryEntries(getter_AddRefs(entries));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ for (;;) {
+ nsCOMPtr<nsIFile> currFile;
+ if (NS_WARN_IF(NS_FAILED(entries->GetNextFile(getter_AddRefs(currFile)))) ||
+ !currFile) {
+ break;
+ }
+ bool isLink, isSpecial, isFile, isDir;
+ if (NS_WARN_IF(NS_FAILED(currFile->IsSymlink(&isLink)) ||
+ NS_FAILED(currFile->IsSpecial(&isSpecial))) ||
+ isSpecial ||
+ // Although we allow explicit individual selection of symlinks via the
+ // file picker, we do not process symlinks in directory traversal. Our
+ // specific policy decision is documented at
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1813299#c20
+ isLink) {
+ continue;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(currFile->IsFile(&isFile)) ||
+ NS_FAILED(currFile->IsDirectory(&isDir))) ||
+ !(isFile || isDir)) {
+ continue;
+ }
+
+ // The new domPath
+ nsAutoString domPath;
+ domPath.Assign(aDOMPath);
+ if (!aDOMPath.EqualsLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL)) {
+ domPath.AppendLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
+ }
+
+ nsAutoString leafName;
+ if (NS_WARN_IF(NS_FAILED(currFile->GetLeafName(leafName)))) {
+ continue;
+ }
+ domPath.Append(leafName);
+
+ if (isFile) {
+ RefPtr<BlobImpl> blobImpl = new FileBlobImpl(currFile);
+ blobImpl->SetDOMPath(domPath);
+
+ if (!mTargetBlobImplArray.AppendElement(blobImpl, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ continue;
+ }
+
+ MOZ_ASSERT(isDir);
+ if (!mRecursiveFlag) {
+ continue;
+ }
+
+ // Recursive.
+ rv = ExploreDirectory(domPath, currFile);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+void GetFilesHelper::ResolveOrRejectPromise(Promise* aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mListingCompleted);
+ MOZ_ASSERT(aPromise);
+
+ Sequence<RefPtr<File>> files;
+
+ if (NS_SUCCEEDED(mErrorResult)) {
+ for (uint32_t i = 0; i < mTargetBlobImplArray.Length(); ++i) {
+ RefPtr<File> domFile =
+ File::Create(aPromise->GetParentObject(), mTargetBlobImplArray[i]);
+ if (NS_WARN_IF(!domFile)) {
+ mErrorResult = NS_ERROR_FAILURE;
+ files.Clear();
+ break;
+ }
+
+ if (!files.AppendElement(domFile, fallible)) {
+ mErrorResult = NS_ERROR_OUT_OF_MEMORY;
+ files.Clear();
+ break;
+ }
+ }
+ }
+
+ // Error propagation.
+ if (NS_FAILED(mErrorResult)) {
+ aPromise->MaybeReject(mErrorResult);
+ return;
+ }
+
+ aPromise->MaybeResolve(files);
+}
+
+void GetFilesHelper::RunCallback(GetFilesCallback* aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mListingCompleted);
+ MOZ_ASSERT(aCallback);
+
+ aCallback->Callback(mErrorResult, mTargetBlobImplArray);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// GetFilesHelperChild class
+
+void GetFilesHelperChild::Work(ErrorResult& aRv) {
+ ContentChild* cc = ContentChild::GetSingleton();
+ if (NS_WARN_IF(!cc)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ aRv = nsID::GenerateUUIDInPlace(mUUID);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ mPendingOperation = true;
+ cc->CreateGetFilesRequest(mDirectoryPath, mRecursiveFlag, mUUID, this);
+}
+
+void GetFilesHelperChild::Cancel() {
+ if (!mPendingOperation) {
+ return;
+ }
+
+ ContentChild* cc = ContentChild::GetSingleton();
+ if (NS_WARN_IF(!cc)) {
+ return;
+ }
+
+ mPendingOperation = false;
+ cc->DeleteGetFilesRequest(mUUID, this);
+}
+
+bool GetFilesHelperChild::AppendBlobImpl(BlobImpl* aBlobImpl) {
+ MOZ_ASSERT(mPendingOperation);
+ MOZ_ASSERT(aBlobImpl);
+ MOZ_ASSERT(aBlobImpl->IsFile());
+
+ return mTargetBlobImplArray.AppendElement(aBlobImpl, fallible);
+}
+
+void GetFilesHelperChild::Finished(nsresult aError) {
+ MOZ_ASSERT(mPendingOperation);
+ MOZ_ASSERT(NS_SUCCEEDED(mErrorResult));
+
+ mPendingOperation = false;
+ mErrorResult = aError;
+
+ OperationCompleted();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// GetFilesHelperParent class
+
+class GetFilesHelperParentCallback final : public GetFilesCallback {
+ public:
+ explicit GetFilesHelperParentCallback(GetFilesHelperParent* aParent)
+ : mParent(aParent) {
+ MOZ_ASSERT(aParent);
+ }
+
+ void Callback(nsresult aStatus,
+ const FallibleTArray<RefPtr<BlobImpl>>& aBlobImpls) override {
+ if (NS_FAILED(aStatus)) {
+ mParent->mContentParent->SendGetFilesResponseAndForget(
+ mParent->mUUID, GetFilesResponseFailure(aStatus));
+ return;
+ }
+
+ GetFilesResponseSuccess success;
+
+ nsTArray<IPCBlob>& ipcBlobs = success.blobs();
+ ipcBlobs.SetLength(aBlobImpls.Length());
+
+ for (uint32_t i = 0; i < aBlobImpls.Length(); ++i) {
+ nsresult rv = IPCBlobUtils::Serialize(aBlobImpls[i], ipcBlobs[i]);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mParent->mContentParent->SendGetFilesResponseAndForget(
+ mParent->mUUID, GetFilesResponseFailure(NS_ERROR_OUT_OF_MEMORY));
+ return;
+ }
+ }
+
+ mParent->mContentParent->SendGetFilesResponseAndForget(mParent->mUUID,
+ success);
+ }
+
+ private:
+ // Raw pointer because this callback is kept alive by this parent object.
+ GetFilesHelperParent* mParent;
+};
+
+GetFilesHelperParent::GetFilesHelperParent(const nsID& aUUID,
+ ContentParent* aContentParent,
+ bool aRecursiveFlag)
+ : GetFilesHelper(aRecursiveFlag),
+ mContentParent(aContentParent),
+ mUUID(aUUID) {}
+
+GetFilesHelperParent::~GetFilesHelperParent() {
+ NS_ReleaseOnMainThread("GetFilesHelperParent::mContentParent",
+ mContentParent.forget());
+}
+
+/* static */
+already_AddRefed<GetFilesHelperParent> GetFilesHelperParent::Create(
+ const nsID& aUUID, const nsAString& aDirectoryPath, bool aRecursiveFlag,
+ ContentParent* aContentParent, ErrorResult& aRv) {
+ MOZ_ASSERT(aContentParent);
+
+ RefPtr<GetFilesHelperParent> helper =
+ new GetFilesHelperParent(aUUID, aContentParent, aRecursiveFlag);
+ helper->SetDirectoryPath(aDirectoryPath);
+
+ helper->Work(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ RefPtr<GetFilesHelperParentCallback> callback =
+ new GetFilesHelperParentCallback(helper);
+ helper->AddCallback(callback);
+
+ return helper.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/GetFilesHelper.h b/dom/filesystem/GetFilesHelper.h
new file mode 100644
index 0000000000..2b15e69059
--- /dev/null
+++ b/dom/filesystem/GetFilesHelper.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 mozilla_dom_GetFilesHelper_h
+#define mozilla_dom_GetFilesHelper_h
+
+#include "mozilla/Mutex.h"
+#include "mozilla/RefPtr.h"
+#include "nsCycleCollectionTraversalCallback.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class BlobImpl;
+class ContentParent;
+class File;
+class GetFilesHelperParent;
+class OwningFileOrDirectory;
+class Promise;
+
+class GetFilesCallback {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(GetFilesCallback);
+
+ virtual void Callback(nsresult aStatus,
+ const FallibleTArray<RefPtr<BlobImpl>>& aBlobImpls) = 0;
+
+ protected:
+ virtual ~GetFilesCallback() = default;
+};
+
+class GetFilesHelperBase {
+ protected:
+ explicit GetFilesHelperBase(bool aRecursiveFlag)
+ : mRecursiveFlag(aRecursiveFlag) {}
+
+ virtual ~GetFilesHelperBase() = default;
+
+ virtual bool IsCanceled() { return false; }
+
+ nsresult ExploreDirectory(const nsAString& aDOMPath, nsIFile* aFile);
+
+ bool mRecursiveFlag;
+
+ // We populate this array in the I/O thread with the BlobImpl.
+ FallibleTArray<RefPtr<BlobImpl>> mTargetBlobImplArray;
+};
+
+// Retrieving the list of files can be very time/IO consuming. We use this
+// helper class to do it just once.
+class GetFilesHelper : public Runnable, public GetFilesHelperBase {
+ friend class GetFilesHelperParent;
+
+ public:
+ static already_AddRefed<GetFilesHelper> Create(
+ const nsTArray<OwningFileOrDirectory>& aFilesOrDirectory,
+ bool aRecursiveFlag, ErrorResult& aRv);
+
+ void AddPromise(Promise* aPromise);
+
+ void AddCallback(GetFilesCallback* aCallback);
+
+ // CC methods
+ void Unlink();
+ void Traverse(nsCycleCollectionTraversalCallback& cb);
+
+ protected:
+ explicit GetFilesHelper(bool aRecursiveFlag);
+
+ virtual ~GetFilesHelper();
+
+ void SetDirectoryPath(const nsAString& aDirectoryPath) {
+ mDirectoryPath = aDirectoryPath;
+ }
+
+ virtual bool IsCanceled() override {
+ MutexAutoLock lock(mMutex);
+ return mCanceled;
+ }
+
+ virtual void Work(ErrorResult& aRv);
+
+ virtual void Cancel(){};
+
+ NS_IMETHOD
+ Run() override;
+
+ void RunIO();
+
+ void OperationCompleted();
+
+ void ResolveOrRejectPromise(Promise* aPromise);
+
+ void RunCallback(GetFilesCallback* aCallback);
+
+ bool mListingCompleted;
+ nsString mDirectoryPath;
+
+ // Error code to propagate.
+ nsresult mErrorResult;
+
+ nsTArray<RefPtr<Promise>> mPromises;
+ nsTArray<RefPtr<GetFilesCallback>> mCallbacks;
+
+ Mutex mMutex MOZ_UNANNOTATED;
+
+ // This variable is protected by mutex.
+ bool mCanceled;
+};
+
+class GetFilesHelperChild final : public GetFilesHelper {
+ public:
+ explicit GetFilesHelperChild(bool aRecursiveFlag)
+ : GetFilesHelper(aRecursiveFlag), mPendingOperation(false) {}
+
+ virtual void Work(ErrorResult& aRv) override;
+
+ virtual void Cancel() override;
+
+ bool AppendBlobImpl(BlobImpl* aBlobImpl);
+
+ void Finished(nsresult aResult);
+
+ private:
+ nsID mUUID;
+ bool mPendingOperation;
+};
+
+class GetFilesHelperParentCallback;
+
+class GetFilesHelperParent final : public GetFilesHelper {
+ friend class GetFilesHelperParentCallback;
+
+ public:
+ static already_AddRefed<GetFilesHelperParent> Create(
+ const nsID& aUUID, const nsAString& aDirectoryPath, bool aRecursiveFlag,
+ ContentParent* aContentParent, ErrorResult& aRv);
+
+ private:
+ GetFilesHelperParent(const nsID& aUUID, ContentParent* aContentParent,
+ bool aRecursiveFlag);
+
+ ~GetFilesHelperParent();
+
+ RefPtr<ContentParent> mContentParent;
+ nsID mUUID;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_GetFilesHelper_h
diff --git a/dom/filesystem/GetFilesTask.cpp b/dom/filesystem/GetFilesTask.cpp
new file mode 100644
index 0000000000..280df0b0bc
--- /dev/null
+++ b/dom/filesystem/GetFilesTask.cpp
@@ -0,0 +1,246 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GetFilesTask.h"
+
+#include "HTMLSplitOnSpacesTokenizer.h"
+#include "js/Value.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/FileSystemBase.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include "mozilla/dom/PFileSystemParams.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "nsIFile.h"
+#include "nsString.h"
+
+namespace mozilla::dom {
+
+/**
+ * GetFilesTaskChild
+ */
+
+/* static */
+already_AddRefed<GetFilesTaskChild> GetFilesTaskChild::Create(
+ FileSystemBase* aFileSystem, Directory* aDirectory, nsIFile* aTargetPath,
+ bool aRecursiveFlag, ErrorResult& aRv) {
+ MOZ_ASSERT(aFileSystem);
+ MOZ_ASSERT(aDirectory);
+ aFileSystem->AssertIsOnOwningThread();
+
+ nsCOMPtr<nsIGlobalObject> globalObject = aFileSystem->GetParentObject();
+ if (NS_WARN_IF(!globalObject)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<GetFilesTaskChild> task = new GetFilesTaskChild(
+ globalObject, aFileSystem, aDirectory, aTargetPath, aRecursiveFlag);
+
+ // aTargetPath can be null. In this case SetError will be called.
+
+ task->mPromise = Promise::Create(globalObject, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return task.forget();
+}
+
+GetFilesTaskChild::GetFilesTaskChild(nsIGlobalObject* aGlobalObject,
+ FileSystemBase* aFileSystem,
+ Directory* aDirectory,
+ nsIFile* aTargetPath, bool aRecursiveFlag)
+ : FileSystemTaskChildBase(aGlobalObject, aFileSystem),
+ mDirectory(aDirectory),
+ mTargetPath(aTargetPath),
+ mRecursiveFlag(aRecursiveFlag) {
+ MOZ_ASSERT(aFileSystem);
+ MOZ_ASSERT(aDirectory);
+ aFileSystem->AssertIsOnOwningThread();
+}
+
+GetFilesTaskChild::~GetFilesTaskChild() {
+ mFileSystem->AssertIsOnOwningThread();
+}
+
+already_AddRefed<Promise> GetFilesTaskChild::GetPromise() {
+ mFileSystem->AssertIsOnOwningThread();
+ return RefPtr<Promise>(mPromise).forget();
+}
+
+FileSystemParams GetFilesTaskChild::GetRequestParams(
+ const nsString& aSerializedDOMPath, ErrorResult& aRv) const {
+ mFileSystem->AssertIsOnOwningThread();
+
+ nsAutoString path;
+ aRv = mTargetPath->GetPath(path);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return FileSystemGetFilesParams();
+ }
+
+ nsAutoString domPath;
+ mDirectory->GetPath(domPath, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return FileSystemGetFilesParams();
+ }
+
+ return FileSystemGetFilesParams(aSerializedDOMPath, path, domPath,
+ mRecursiveFlag);
+}
+
+void GetFilesTaskChild::SetSuccessRequestResult(
+ const FileSystemResponseValue& aValue, ErrorResult& aRv) {
+ mFileSystem->AssertIsOnOwningThread();
+ MOZ_ASSERT(aValue.type() ==
+ FileSystemResponseValue::TFileSystemFilesResponse);
+
+ FileSystemFilesResponse r = aValue;
+
+ if (!mTargetData.SetLength(r.data().Length(), mozilla::fallible_t())) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ nsCOMPtr<nsIGlobalObject> globalObject = mFileSystem->GetParentObject();
+ MOZ_ASSERT(globalObject);
+
+ for (uint32_t i = 0; i < r.data().Length(); ++i) {
+ const FileSystemFileResponse& data = r.data()[i];
+ RefPtr<BlobImpl> blobImpl = IPCBlobUtils::Deserialize(data.blob());
+ MOZ_ASSERT(blobImpl);
+
+ mTargetData[i] = File::Create(globalObject, blobImpl);
+ if (NS_WARN_IF(!mTargetData[i])) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ }
+}
+
+void GetFilesTaskChild::HandlerCallback() {
+ mFileSystem->AssertIsOnOwningThread();
+ if (mFileSystem->IsShutdown()) {
+ mPromise = nullptr;
+ return;
+ }
+
+ if (HasError()) {
+ mPromise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ mPromise = nullptr;
+ return;
+ }
+
+ mPromise->MaybeResolve(mTargetData);
+ mPromise = nullptr;
+}
+
+/**
+ * GetFilesTaskParent
+ */
+
+/* static */
+already_AddRefed<GetFilesTaskParent> GetFilesTaskParent::Create(
+ FileSystemBase* aFileSystem, const FileSystemGetFilesParams& aParam,
+ FileSystemRequestParent* aParent, ErrorResult& aRv) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!");
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aFileSystem);
+
+ RefPtr<GetFilesTaskParent> task =
+ new GetFilesTaskParent(aFileSystem, aParam, aParent);
+
+ aRv = NS_NewLocalFile(aParam.realPath(), true,
+ getter_AddRefs(task->mTargetPath));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return task.forget();
+}
+
+GetFilesTaskParent::GetFilesTaskParent(FileSystemBase* aFileSystem,
+ const FileSystemGetFilesParams& aParam,
+ FileSystemRequestParent* aParent)
+ : FileSystemTaskParentBase(aFileSystem, aParam, aParent),
+ GetFilesHelperBase(aParam.recursiveFlag()),
+ mDirectoryDOMPath(aParam.domPath()) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!");
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aFileSystem);
+}
+
+FileSystemResponseValue GetFilesTaskParent::GetSuccessRequestResult(
+ ErrorResult& aRv) const {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+
+ FallibleTArray<FileSystemFileResponse> inputs;
+ if (!inputs.SetLength(mTargetBlobImplArray.Length(), mozilla::fallible_t())) {
+ FileSystemFilesResponse response;
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return response;
+ }
+
+ for (unsigned i = 0; i < mTargetBlobImplArray.Length(); i++) {
+ IPCBlob ipcBlob;
+ aRv = IPCBlobUtils::Serialize(mTargetBlobImplArray[i], ipcBlob);
+ if (NS_WARN_IF(aRv.Failed())) {
+ FileSystemFilesResponse response;
+ return response;
+ }
+
+ inputs[i] = FileSystemFileResponse(ipcBlob);
+ }
+
+ FileSystemFilesResponse response;
+ response.data() = std::move(inputs);
+ return response;
+}
+
+nsresult GetFilesTaskParent::IOWork() {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!");
+ MOZ_ASSERT(!NS_IsMainThread(), "Only call on I/O thread!");
+
+ if (mFileSystem->IsShutdown()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool exists;
+ nsresult rv = mTargetPath->Exists(&exists);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!exists) {
+ return NS_OK;
+ }
+
+ bool isDir;
+ rv = mTargetPath->IsDirectory(&isDir);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!isDir) {
+ return NS_ERROR_DOM_FILESYSTEM_TYPE_MISMATCH_ERR;
+ }
+
+ // Get isDirectory.
+ rv = ExploreDirectory(mDirectoryDOMPath, mTargetPath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult GetFilesTaskParent::GetTargetPath(nsAString& aPath) const {
+ return mTargetPath->GetPath(aPath);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/GetFilesTask.h b/dom/filesystem/GetFilesTask.h
new file mode 100644
index 0000000000..6f6f15e879
--- /dev/null
+++ b/dom/filesystem/GetFilesTask.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_GetFilesTask_h
+#define mozilla_dom_GetFilesTask_h
+
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/FileSystemTaskBase.h"
+#include "mozilla/dom/GetFilesHelper.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class BlobImpl;
+class FileSystemGetFilesParams;
+
+class GetFilesTaskChild final : public FileSystemTaskChildBase {
+ public:
+ static already_AddRefed<GetFilesTaskChild> Create(FileSystemBase* aFileSystem,
+ Directory* aDirectory,
+ nsIFile* aTargetPath,
+ bool aRecursiveFlag,
+ ErrorResult& aRv);
+
+ virtual ~GetFilesTaskChild();
+
+ already_AddRefed<Promise> GetPromise();
+
+ private:
+ // If aDirectoryOnly is set, we should ensure that the target is a directory.
+ GetFilesTaskChild(nsIGlobalObject* aGlobalObject, FileSystemBase* aFileSystem,
+ Directory* aDirectory, nsIFile* aTargetPath,
+ bool aRecursiveFlag);
+
+ virtual FileSystemParams GetRequestParams(const nsString& aSerializedDOMPath,
+ ErrorResult& aRv) const override;
+
+ virtual void SetSuccessRequestResult(const FileSystemResponseValue& aValue,
+ ErrorResult& aRv) override;
+
+ virtual void HandlerCallback() override;
+
+ RefPtr<Promise> mPromise;
+ RefPtr<Directory> mDirectory;
+ nsCOMPtr<nsIFile> mTargetPath;
+ bool mRecursiveFlag;
+
+ // We store the fullpath and the dom path of Files.
+ FallibleTArray<RefPtr<File>> mTargetData;
+};
+
+class GetFilesTaskParent final : public FileSystemTaskParentBase,
+ public GetFilesHelperBase {
+ public:
+ static already_AddRefed<GetFilesTaskParent> Create(
+ FileSystemBase* aFileSystem, const FileSystemGetFilesParams& aParam,
+ FileSystemRequestParent* aParent, ErrorResult& aRv);
+
+ nsresult GetTargetPath(nsAString& aPath) const override;
+
+ private:
+ GetFilesTaskParent(FileSystemBase* aFileSystem,
+ const FileSystemGetFilesParams& aParam,
+ FileSystemRequestParent* aParent);
+
+ virtual FileSystemResponseValue GetSuccessRequestResult(
+ ErrorResult& aRv) const override;
+
+ virtual nsresult IOWork() override;
+
+ nsString mDirectoryDOMPath;
+ nsCOMPtr<nsIFile> mTargetPath;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_GetFilesTask_h
diff --git a/dom/filesystem/OSFileSystem.cpp b/dom/filesystem/OSFileSystem.cpp
new file mode 100644
index 0000000000..2322d07b93
--- /dev/null
+++ b/dom/filesystem/OSFileSystem.cpp
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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/OSFileSystem.h"
+
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "nsIGlobalObject.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsIFile.h"
+
+namespace mozilla::dom {
+
+OSFileSystem::OSFileSystem(const nsAString& aRootDir) {
+ mLocalRootPath = aRootDir;
+}
+
+already_AddRefed<FileSystemBase> OSFileSystem::Clone() {
+ AssertIsOnOwningThread();
+
+ RefPtr<OSFileSystem> fs = new OSFileSystem(mLocalRootPath);
+ if (mGlobal) {
+ fs->Init(mGlobal);
+ }
+
+ return fs.forget();
+}
+
+void OSFileSystem::Init(nsIGlobalObject* aGlobal) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mGlobal, "No duple Init() calls");
+ MOZ_ASSERT(aGlobal);
+
+ mGlobal = aGlobal;
+}
+
+nsIGlobalObject* OSFileSystem::GetParentObject() const {
+ AssertIsOnOwningThread();
+ return mGlobal;
+}
+
+bool OSFileSystem::IsSafeFile(nsIFile* aFile) const {
+ // The concept of "safe files" is specific to the Device Storage API where
+ // files are only "safe" if they're of a type that is appropriate for the
+ // area of device storage that is being used.
+ MOZ_CRASH("Don't use OSFileSystem with the Device Storage API");
+ return true;
+}
+
+bool OSFileSystem::IsSafeDirectory(Directory* aDir) const {
+ // The concept of "safe directory" is specific to the Device Storage API
+ // where a directory is only "safe" if it belongs to the area of device
+ // storage that it is being used with.
+ MOZ_CRASH("Don't use OSFileSystem with the Device Storage API");
+ return true;
+}
+
+void OSFileSystem::Unlink() {
+ AssertIsOnOwningThread();
+ mGlobal = nullptr;
+}
+
+void OSFileSystem::Traverse(nsCycleCollectionTraversalCallback& cb) {
+ AssertIsOnOwningThread();
+
+ OSFileSystem* tmp = this;
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal);
+}
+
+void OSFileSystem::SerializeDOMPath(nsAString& aOutput) const {
+ AssertIsOnOwningThread();
+ aOutput = mLocalRootPath;
+}
+
+/**
+ * OSFileSystemParent
+ */
+
+OSFileSystemParent::OSFileSystemParent(const nsAString& aRootDir) {
+ mLocalRootPath = aRootDir;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/OSFileSystem.h b/dom/filesystem/OSFileSystem.h
new file mode 100644
index 0000000000..cb2ade7fc5
--- /dev/null
+++ b/dom/filesystem/OSFileSystem.h
@@ -0,0 +1,98 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_OSFileSystem_h
+#define mozilla_dom_OSFileSystem_h
+
+#include "mozilla/dom/FileSystemBase.h"
+
+namespace mozilla::dom {
+
+class OSFileSystem final : public FileSystemBase {
+ public:
+ explicit OSFileSystem(const nsAString& aRootDir);
+
+ void Init(nsIGlobalObject* aGlobal);
+
+ // Overrides FileSystemBase
+
+ virtual already_AddRefed<FileSystemBase> Clone() override;
+
+ virtual bool ShouldCreateDirectory() override {
+ MOZ_CRASH("This should not be called.");
+ // Because OSFileSystem should not be used when the creation of directories
+ // is needed. For that we have OSFileSystemParent.
+ return false;
+ }
+
+ virtual nsIGlobalObject* GetParentObject() const override;
+
+ virtual bool IsSafeFile(nsIFile* aFile) const override;
+
+ virtual bool IsSafeDirectory(Directory* aDir) const override;
+
+ virtual void SerializeDOMPath(nsAString& aOutput) const override;
+
+ // CC methods
+ virtual void Unlink() override;
+ virtual void Traverse(nsCycleCollectionTraversalCallback& cb) override;
+
+ private:
+ virtual ~OSFileSystem() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+};
+
+class OSFileSystemParent final : public FileSystemBase {
+ public:
+ explicit OSFileSystemParent(const nsAString& aRootDir);
+
+ // Overrides FileSystemBase
+
+ virtual already_AddRefed<FileSystemBase> Clone() override {
+ MOZ_CRASH("This should not be called on the PBackground thread.");
+ return nullptr;
+ }
+
+ virtual bool ShouldCreateDirectory() override { return false; }
+
+ virtual nsIGlobalObject* GetParentObject() const override {
+ MOZ_CRASH("This should not be called on the PBackground thread.");
+ return nullptr;
+ }
+
+ virtual void GetDirectoryName(nsIFile* aFile, nsAString& aRetval,
+ ErrorResult& aRv) const override {
+ MOZ_CRASH("This should not be called on the PBackground thread.");
+ }
+
+ virtual bool IsSafeFile(nsIFile* aFile) const override { return true; }
+
+ virtual bool IsSafeDirectory(Directory* aDir) const override {
+ MOZ_CRASH("This should not be called on the PBackground thread.");
+ return true;
+ }
+
+ virtual void SerializeDOMPath(nsAString& aOutput) const override {
+ MOZ_CRASH("This should not be called on the PBackground thread.");
+ }
+
+ // CC methods
+ virtual void Unlink() override {
+ MOZ_CRASH("This should not be called on the PBackground thread.");
+ }
+
+ virtual void Traverse(nsCycleCollectionTraversalCallback& cb) override {
+ MOZ_CRASH("This should not be called on the PBackground thread.");
+ }
+
+ private:
+ virtual ~OSFileSystemParent() = default;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_OSFileSystem_h
diff --git a/dom/filesystem/PFileSystemParams.ipdlh b/dom/filesystem/PFileSystemParams.ipdlh
new file mode 100644
index 0000000000..40b1e095f1
--- /dev/null
+++ b/dom/filesystem/PFileSystemParams.ipdlh
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+namespace mozilla {
+namespace dom {
+
+struct FileSystemGetDirectoryListingParams
+{
+ nsString filesystem;
+ nsString realPath;
+ nsString domPath;
+
+ // 'filters' could be an array rather than a semicolon separated string
+ // (we'd then use nsTArray<nsString> internally), but that is
+ // wasteful. E10s requires us to pass the filters over as a string anyway,
+ // so avoiding using an array avoids serialization on the side passing the
+ // filters. Since an nsString can share its buffer when copied,
+ // using that instead of nsTArray<nsString> makes copying the filters
+ // around in any given process a bit more efficient too, since copying a
+ // single nsString is cheaper than copying nsTArray member data and
+ // each nsString that it contains.
+ nsString filters;
+};
+
+struct FileSystemGetFilesParams
+{
+ nsString filesystem;
+ nsString realPath;
+ nsString domPath;
+ bool recursiveFlag;
+};
+
+struct FileSystemGetFileOrDirectoryParams
+{
+ nsString filesystem;
+ nsString realPath;
+};
+
+union FileSystemParams
+{
+ FileSystemGetDirectoryListingParams;
+ FileSystemGetFilesParams;
+ FileSystemGetFileOrDirectoryParams;
+};
+
+} // dom namespace
+} // mozilla namespace
diff --git a/dom/filesystem/PFileSystemRequest.ipdl b/dom/filesystem/PFileSystemRequest.ipdl
new file mode 100644
index 0000000000..3ae24e269b
--- /dev/null
+++ b/dom/filesystem/PFileSystemRequest.ipdl
@@ -0,0 +1,77 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PBackground;
+
+include IPCBlob;
+
+include "mozilla/dom/FileSystemTaskBase.h";
+
+namespace mozilla {
+namespace dom {
+
+struct FileSystemFileResponse
+{
+ IPCBlob blob;
+};
+
+struct FileSystemDirectoryResponse
+{
+ nsString realPath;
+};
+
+struct FileSystemDirectoryListingResponseFile
+{
+ IPCBlob blob;
+};
+
+struct FileSystemDirectoryListingResponseDirectory
+{
+ // This is the full real path for the directory that we are sending via IPC.
+ nsString directoryRealPath;
+};
+
+union FileSystemDirectoryListingResponseData
+{
+ FileSystemDirectoryListingResponseFile;
+ FileSystemDirectoryListingResponseDirectory;
+};
+
+struct FileSystemDirectoryListingResponse
+{
+ FileSystemDirectoryListingResponseData[] data;
+};
+
+struct FileSystemFilesResponse
+{
+ FileSystemFileResponse[] data;
+};
+
+struct FileSystemErrorResponse
+{
+ nsresult error;
+};
+
+union FileSystemResponseValue
+{
+ FileSystemDirectoryResponse;
+ FileSystemDirectoryListingResponse;
+ FileSystemFileResponse;
+ FileSystemFilesResponse;
+ FileSystemErrorResponse;
+};
+
+[ChildImpl="FileSystemTaskChildBase"]
+protocol PFileSystemRequest
+{
+ manager PBackground;
+
+child:
+ async __delete__(FileSystemResponseValue response);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/filesystem/compat/CallbackRunnables.cpp b/dom/filesystem/compat/CallbackRunnables.cpp
new file mode 100644
index 0000000000..cbdd2f2f56
--- /dev/null
+++ b/dom/filesystem/compat/CallbackRunnables.cpp
@@ -0,0 +1,280 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "CallbackRunnables.h"
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/DirectoryBinding.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/FileBinding.h"
+#include "mozilla/dom/FileSystem.h"
+#include "mozilla/dom/FileSystemDirectoryReaderBinding.h"
+#include "mozilla/dom/FileSystemFileEntry.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/Unused.h"
+#include "nsIGlobalObject.h"
+#include "nsIFile.h"
+#include "nsPIDOMWindow.h"
+
+#include "../GetFileOrDirectoryTask.h"
+
+namespace mozilla::dom {
+
+EntryCallbackRunnable::EntryCallbackRunnable(FileSystemEntryCallback* aCallback,
+ FileSystemEntry* aEntry)
+ : Runnable("EntryCallbackRunnable"), mCallback(aCallback), mEntry(aEntry) {
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(aEntry);
+}
+
+NS_IMETHODIMP
+EntryCallbackRunnable::Run() {
+ mCallback->Call(*mEntry);
+ return NS_OK;
+}
+
+ErrorCallbackRunnable::ErrorCallbackRunnable(nsIGlobalObject* aGlobalObject,
+ ErrorCallback* aCallback,
+ nsresult aError)
+ : Runnable("ErrorCallbackRunnable"),
+ mGlobal(aGlobalObject),
+ mCallback(aCallback),
+ mError(aError) {
+ MOZ_ASSERT(aGlobalObject);
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(NS_FAILED(aError));
+}
+
+NS_IMETHODIMP
+ErrorCallbackRunnable::Run() {
+ RefPtr<DOMException> exception = DOMException::Create(mError);
+ mCallback->Call(*exception);
+ return NS_OK;
+}
+
+EmptyEntriesCallbackRunnable::EmptyEntriesCallbackRunnable(
+ FileSystemEntriesCallback* aCallback)
+ : Runnable("EmptyEntriesCallbackRunnable"), mCallback(aCallback) {
+ MOZ_ASSERT(aCallback);
+}
+
+NS_IMETHODIMP
+EmptyEntriesCallbackRunnable::Run() {
+ Sequence<OwningNonNull<FileSystemEntry>> sequence;
+ mCallback->Call(sequence);
+ return NS_OK;
+}
+
+GetEntryHelper::GetEntryHelper(FileSystemDirectoryEntry* aParentEntry,
+ Directory* aDirectory,
+ nsTArray<nsString>& aParts,
+ FileSystem* aFileSystem,
+ FileSystemEntryCallback* aSuccessCallback,
+ ErrorCallback* aErrorCallback,
+ FileSystemDirectoryEntry::GetInternalType aType)
+ : mParentEntry(aParentEntry),
+ mDirectory(aDirectory),
+ mParts(aParts.Clone()),
+ mFileSystem(aFileSystem),
+ mSuccessCallback(aSuccessCallback),
+ mErrorCallback(aErrorCallback),
+ mType(aType) {
+ MOZ_ASSERT(aParentEntry);
+ MOZ_ASSERT(aDirectory);
+ MOZ_ASSERT(!aParts.IsEmpty());
+ MOZ_ASSERT(aFileSystem);
+ MOZ_ASSERT(aSuccessCallback || aErrorCallback);
+}
+
+GetEntryHelper::~GetEntryHelper() = default;
+
+namespace {
+
+nsresult DOMPathToRealPath(Directory* aDirectory, const nsAString& aPath,
+ nsIFile** aFile) {
+ nsString relativePath;
+ relativePath = aPath;
+
+ // Trim white spaces.
+ static const char kWhitespace[] = "\b\t\r\n ";
+ relativePath.Trim(kWhitespace);
+
+ nsTArray<nsString> parts;
+ if (!FileSystemUtils::IsValidRelativeDOMPath(relativePath, parts)) {
+ return NS_ERROR_DOM_FILESYSTEM_INVALID_PATH_ERR;
+ }
+
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = aDirectory->GetInternalNsIFile()->Clone(getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ for (uint32_t i = 0; i < parts.Length(); ++i) {
+ rv = file->AppendRelativePath(parts[i]);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ file.forget(aFile);
+ return NS_OK;
+}
+
+} // namespace
+
+void GetEntryHelper::Run() {
+ MOZ_ASSERT(!mParts.IsEmpty());
+
+ nsCOMPtr<nsIFile> realPath;
+ nsresult error =
+ DOMPathToRealPath(mDirectory, mParts[0], getter_AddRefs(realPath));
+
+ ErrorResult rv;
+ RefPtr<FileSystemBase> fs = mDirectory->GetFileSystem(rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ Error(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ RefPtr<GetFileOrDirectoryTaskChild> task =
+ GetFileOrDirectoryTaskChild::Create(fs, realPath, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ Error(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ task->SetError(error);
+ task->Start();
+
+ RefPtr<Promise> promise = task->GetPromise();
+
+ mParts.RemoveElementAt(0);
+ promise->AppendNativeHandler(this);
+}
+
+void GetEntryHelper::ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ if (NS_WARN_IF(!aValue.isObject())) {
+ return;
+ }
+
+ JS::Rooted<JSObject*> obj(aCx, &aValue.toObject());
+
+ // This is not the last part of the path.
+ if (!mParts.IsEmpty()) {
+ ContinueRunning(obj);
+ return;
+ }
+
+ CompleteOperation(obj);
+}
+
+void GetEntryHelper::CompleteOperation(JSObject* aObj) {
+ MOZ_ASSERT(mParts.IsEmpty());
+
+ if (mType == FileSystemDirectoryEntry::eGetFile) {
+ RefPtr<File> file;
+ if (NS_FAILED(UNWRAP_OBJECT(File, aObj, file))) {
+ Error(NS_ERROR_DOM_TYPE_MISMATCH_ERR);
+ return;
+ }
+
+ RefPtr<FileSystemFileEntry> entry = new FileSystemFileEntry(
+ mParentEntry->GetParentObject(), file, mParentEntry, mFileSystem);
+ mSuccessCallback->Call(*entry);
+ return;
+ }
+
+ MOZ_ASSERT(mType == FileSystemDirectoryEntry::eGetDirectory);
+
+ RefPtr<Directory> directory;
+ if (NS_FAILED(UNWRAP_OBJECT(Directory, aObj, directory))) {
+ Error(NS_ERROR_DOM_TYPE_MISMATCH_ERR);
+ return;
+ }
+
+ RefPtr<FileSystemDirectoryEntry> entry = new FileSystemDirectoryEntry(
+ mParentEntry->GetParentObject(), directory, mParentEntry, mFileSystem);
+ mSuccessCallback->Call(*entry);
+}
+
+void GetEntryHelper::ContinueRunning(JSObject* aObj) {
+ MOZ_ASSERT(!mParts.IsEmpty());
+
+ RefPtr<Directory> directory;
+ if (NS_FAILED(UNWRAP_OBJECT(Directory, aObj, directory))) {
+ Error(NS_ERROR_DOM_TYPE_MISMATCH_ERR);
+ return;
+ }
+
+ RefPtr<FileSystemDirectoryEntry> entry = new FileSystemDirectoryEntry(
+ mParentEntry->GetParentObject(), directory, mParentEntry, mFileSystem);
+
+ // Update the internal values.
+ mParentEntry = entry;
+ mDirectory = directory;
+
+ Run();
+}
+
+void GetEntryHelper::RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ Error(NS_ERROR_DOM_NOT_FOUND_ERR);
+}
+
+void GetEntryHelper::Error(nsresult aError) {
+ MOZ_ASSERT(NS_FAILED(aError));
+
+ if (mErrorCallback) {
+ RefPtr<ErrorCallbackRunnable> runnable = new ErrorCallbackRunnable(
+ mParentEntry->GetParentObject(), mErrorCallback, aError);
+
+ FileSystemUtils::DispatchRunnable(mParentEntry->GetParentObject(),
+ runnable.forget());
+ }
+}
+
+NS_IMPL_ISUPPORTS0(GetEntryHelper);
+
+/* static */
+void FileSystemEntryCallbackHelper::Call(
+ nsIGlobalObject* aGlobalObject,
+ const Optional<OwningNonNull<FileSystemEntryCallback>>& aEntryCallback,
+ FileSystemEntry* aEntry) {
+ MOZ_ASSERT(aGlobalObject);
+ MOZ_ASSERT(aEntry);
+
+ if (aEntryCallback.WasPassed()) {
+ RefPtr<EntryCallbackRunnable> runnable =
+ new EntryCallbackRunnable(&aEntryCallback.Value(), aEntry);
+
+ FileSystemUtils::DispatchRunnable(aGlobalObject, runnable.forget());
+ }
+}
+
+/* static */
+void ErrorCallbackHelper::Call(
+ nsIGlobalObject* aGlobal,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback,
+ nsresult aError) {
+ MOZ_ASSERT(aGlobal);
+ MOZ_ASSERT(NS_FAILED(aError));
+
+ if (aErrorCallback.WasPassed()) {
+ RefPtr<ErrorCallbackRunnable> runnable =
+ new ErrorCallbackRunnable(aGlobal, &aErrorCallback.Value(), aError);
+
+ FileSystemUtils::DispatchRunnable(aGlobal, runnable.forget());
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/compat/CallbackRunnables.h b/dom/filesystem/compat/CallbackRunnables.h
new file mode 100644
index 0000000000..e2e1c47913
--- /dev/null
+++ b/dom/filesystem/compat/CallbackRunnables.h
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ErrorCallbackRunnable_h
+#define mozilla_dom_ErrorCallbackRunnable_h
+
+#include "FileSystemDirectoryEntry.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "nsThreadUtils.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::dom {
+
+class FileSystemEntriesCallback;
+
+class EntryCallbackRunnable final : public Runnable {
+ public:
+ EntryCallbackRunnable(FileSystemEntryCallback* aCallback,
+ FileSystemEntry* aEntry);
+
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See
+ // bug 1535398.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override;
+
+ private:
+ const RefPtr<FileSystemEntryCallback> mCallback;
+ const RefPtr<FileSystemEntry> mEntry;
+};
+
+class ErrorCallbackRunnable final : public Runnable {
+ public:
+ ErrorCallbackRunnable(nsIGlobalObject* aGlobalObject,
+ ErrorCallback* aCallback, nsresult aError);
+
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See
+ // bug 1535398.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override;
+
+ private:
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ const RefPtr<ErrorCallback> mCallback;
+ nsresult mError;
+};
+
+class EmptyEntriesCallbackRunnable final : public Runnable {
+ public:
+ explicit EmptyEntriesCallbackRunnable(FileSystemEntriesCallback* aCallback);
+
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See
+ // bug 1535398.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override;
+
+ private:
+ const RefPtr<FileSystemEntriesCallback> mCallback;
+};
+
+class GetEntryHelper final : public PromiseNativeHandler {
+ public:
+ NS_DECL_ISUPPORTS
+
+ GetEntryHelper(FileSystemDirectoryEntry* aParentEntry, Directory* aDirectory,
+ nsTArray<nsString>& aParts, FileSystem* aFileSystem,
+ FileSystemEntryCallback* aSuccessCallback,
+ ErrorCallback* aErrorCallback,
+ FileSystemDirectoryEntry::GetInternalType aType);
+
+ void Run();
+
+ MOZ_CAN_RUN_SCRIPT
+ virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ private:
+ ~GetEntryHelper();
+
+ void Error(nsresult aError);
+
+ void ContinueRunning(JSObject* aObj);
+
+ MOZ_CAN_RUN_SCRIPT void CompleteOperation(JSObject* aObj);
+
+ RefPtr<FileSystemDirectoryEntry> mParentEntry;
+ RefPtr<Directory> mDirectory;
+ nsTArray<nsString> mParts;
+ RefPtr<FileSystem> mFileSystem;
+
+ const RefPtr<FileSystemEntryCallback> mSuccessCallback;
+ RefPtr<ErrorCallback> mErrorCallback;
+
+ FileSystemDirectoryEntry::GetInternalType mType;
+};
+
+class FileSystemEntryCallbackHelper {
+ public:
+ static void Call(
+ nsIGlobalObject* aGlobalObject,
+ const Optional<OwningNonNull<FileSystemEntryCallback>>& aEntryCallback,
+ FileSystemEntry* aEntry);
+};
+
+class ErrorCallbackHelper {
+ public:
+ static void Call(nsIGlobalObject* aGlobal,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback,
+ nsresult aError);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_CallbackRunnables_h
diff --git a/dom/filesystem/compat/FileSystem.cpp b/dom/filesystem/compat/FileSystem.cpp
new file mode 100644
index 0000000000..734b3ec600
--- /dev/null
+++ b/dom/filesystem/compat/FileSystem.cpp
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "FileSystem.h"
+#include "FileSystemRootDirectoryEntry.h"
+#include "mozilla/dom/FileSystemBinding.h"
+#include "nsIDUtils.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FileSystem, mParent, mRoot)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FileSystem)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(FileSystem)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystem)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/* static */
+already_AddRefed<FileSystem> FileSystem::Create(nsIGlobalObject* aGlobalObject)
+
+{
+ MOZ_ASSERT(aGlobalObject);
+
+ nsID id;
+ nsresult rv = nsID::GenerateUUIDInPlace(id);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ NSID_TrimBracketsUTF16 name(id);
+
+ RefPtr<FileSystem> fs = new FileSystem(aGlobalObject, name);
+
+ return fs.forget();
+}
+
+FileSystem::FileSystem(nsIGlobalObject* aGlobal, const nsAString& aName)
+ : mParent(aGlobal), mName(aName) {
+ MOZ_ASSERT(aGlobal);
+}
+
+FileSystem::~FileSystem() = default;
+
+JSObject* FileSystem::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return FileSystem_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void FileSystem::CreateRoot(const Sequence<RefPtr<FileSystemEntry>>& aEntries) {
+ MOZ_ASSERT(!mRoot);
+ mRoot = new FileSystemRootDirectoryEntry(mParent, aEntries, this);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/compat/FileSystem.h b/dom/filesystem/compat/FileSystem.h
new file mode 100644
index 0000000000..92cb563ef4
--- /dev/null
+++ b/dom/filesystem/compat/FileSystem.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileSystem_h
+#define mozilla_dom_FileSystem_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::dom {
+
+class FileSystemDirectoryEntry;
+class FileSystemEntry;
+class OwningFileOrDirectory;
+
+class FileSystem final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystem)
+
+ static already_AddRefed<FileSystem> Create(nsIGlobalObject* aGlobalObject);
+
+ nsIGlobalObject* GetParentObject() const { return mParent; }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void GetName(nsAString& aName) const { aName = mName; }
+
+ FileSystemDirectoryEntry* Root() const { return mRoot; }
+
+ void CreateRoot(const Sequence<RefPtr<FileSystemEntry>>& aEntries);
+
+ private:
+ explicit FileSystem(nsIGlobalObject* aGlobalObject, const nsAString& aName);
+ ~FileSystem();
+
+ nsCOMPtr<nsIGlobalObject> mParent;
+ RefPtr<FileSystemDirectoryEntry> mRoot;
+ nsString mName;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileSystem_h
diff --git a/dom/filesystem/compat/FileSystemDirectoryEntry.cpp b/dom/filesystem/compat/FileSystemDirectoryEntry.cpp
new file mode 100644
index 0000000000..7358f997f0
--- /dev/null
+++ b/dom/filesystem/compat/FileSystemDirectoryEntry.cpp
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "FileSystemDirectoryEntry.h"
+#include "CallbackRunnables.h"
+#include "FileSystemDirectoryReader.h"
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/FileSystemDirectoryEntryBinding.h"
+#include "mozilla/dom/FileSystemUtils.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(FileSystemDirectoryEntry, FileSystemEntry,
+ mDirectory)
+
+NS_IMPL_ADDREF_INHERITED(FileSystemDirectoryEntry, FileSystemEntry)
+NS_IMPL_RELEASE_INHERITED(FileSystemDirectoryEntry, FileSystemEntry)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemDirectoryEntry)
+NS_INTERFACE_MAP_END_INHERITING(FileSystemEntry)
+
+FileSystemDirectoryEntry::FileSystemDirectoryEntry(
+ nsIGlobalObject* aGlobal, Directory* aDirectory,
+ FileSystemDirectoryEntry* aParentEntry, FileSystem* aFileSystem)
+ : FileSystemEntry(aGlobal, aParentEntry, aFileSystem),
+ mDirectory(aDirectory) {
+ MOZ_ASSERT(aGlobal);
+}
+
+FileSystemDirectoryEntry::~FileSystemDirectoryEntry() = default;
+
+JSObject* FileSystemDirectoryEntry::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return FileSystemDirectoryEntry_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void FileSystemDirectoryEntry::GetName(nsAString& aName,
+ ErrorResult& aRv) const {
+ MOZ_ASSERT(mDirectory);
+ mDirectory->GetName(aName, aRv);
+}
+
+void FileSystemDirectoryEntry::GetFullPath(nsAString& aPath,
+ ErrorResult& aRv) const {
+ MOZ_ASSERT(mDirectory);
+ mDirectory->GetPath(aPath, aRv);
+}
+
+already_AddRefed<FileSystemDirectoryReader>
+FileSystemDirectoryEntry::CreateReader() {
+ MOZ_ASSERT(mDirectory);
+
+ RefPtr<FileSystemDirectoryReader> reader =
+ new FileSystemDirectoryReader(this, Filesystem(), mDirectory);
+ return reader.forget();
+}
+
+void FileSystemDirectoryEntry::GetInternal(
+ const nsAString& aPath, const FileSystemFlags& aFlag,
+ const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback,
+ GetInternalType aType) {
+ MOZ_ASSERT(mDirectory);
+
+ if (!aSuccessCallback.WasPassed() && !aErrorCallback.WasPassed()) {
+ return;
+ }
+
+ if (aFlag.mCreate) {
+ ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback,
+ NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ nsTArray<nsString> parts;
+ if (!FileSystemUtils::IsValidRelativeDOMPath(aPath, parts)) {
+ ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback,
+ NS_ERROR_DOM_NOT_FOUND_ERR);
+ return;
+ }
+
+ RefPtr<GetEntryHelper> helper = new GetEntryHelper(
+ this, mDirectory, parts, Filesystem(),
+ aSuccessCallback.WasPassed() ? &aSuccessCallback.Value() : nullptr,
+ aErrorCallback.WasPassed() ? &aErrorCallback.Value() : nullptr, aType);
+ helper->Run();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/compat/FileSystemDirectoryEntry.h b/dom/filesystem/compat/FileSystemDirectoryEntry.h
new file mode 100644
index 0000000000..1b772691e1
--- /dev/null
+++ b/dom/filesystem/compat/FileSystemDirectoryEntry.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 mozilla_dom_FileSystemDirectoryEntry_h
+#define mozilla_dom_FileSystemDirectoryEntry_h
+
+#include "mozilla/dom/FileSystemEntry.h"
+
+namespace mozilla::dom {
+
+class Directory;
+class FileSystemDirectoryReader;
+
+class FileSystemDirectoryEntry : public FileSystemEntry {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FileSystemDirectoryEntry,
+ FileSystemEntry)
+
+ FileSystemDirectoryEntry(nsIGlobalObject* aGlobalObject,
+ Directory* aDirectory,
+ FileSystemDirectoryEntry* aParentEntry,
+ FileSystem* aFileSystem);
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual bool IsDirectory() const override { return true; }
+
+ virtual void GetName(nsAString& aName, ErrorResult& aRv) const override;
+
+ virtual void GetFullPath(nsAString& aFullPath,
+ ErrorResult& aRv) const override;
+
+ virtual already_AddRefed<FileSystemDirectoryReader> CreateReader();
+
+ void GetFile(
+ const Optional<nsAString>& aPath, const FileSystemFlags& aFlag,
+ const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback) {
+ GetInternal(aPath.WasPassed() ? aPath.Value() : u""_ns, aFlag,
+ aSuccessCallback, aErrorCallback, eGetFile);
+ }
+
+ void GetDirectory(
+ const Optional<nsAString>& aPath, const FileSystemFlags& aFlag,
+ const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback) {
+ GetInternal(aPath.WasPassed() ? aPath.Value() : u""_ns, aFlag,
+ aSuccessCallback, aErrorCallback, eGetDirectory);
+ }
+
+ enum GetInternalType { eGetFile, eGetDirectory };
+
+ virtual void GetInternal(
+ const nsAString& aPath, const FileSystemFlags& aFlag,
+ const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback,
+ GetInternalType aType);
+
+ protected:
+ virtual ~FileSystemDirectoryEntry();
+
+ private:
+ RefPtr<Directory> mDirectory;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileSystemDirectoryEntry_h
diff --git a/dom/filesystem/compat/FileSystemDirectoryReader.cpp b/dom/filesystem/compat/FileSystemDirectoryReader.cpp
new file mode 100644
index 0000000000..533faae413
--- /dev/null
+++ b/dom/filesystem/compat/FileSystemDirectoryReader.cpp
@@ -0,0 +1,181 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemDirectoryReader.h"
+#include "CallbackRunnables.h"
+#include "FileSystemFileEntry.h"
+#include "js/Array.h" // JS::NewArrayObject
+#include "js/PropertyAndElement.h" // JS_GetElement
+#include "mozilla/dom/FileBinding.h"
+#include "mozilla/dom/FileSystem.h"
+#include "mozilla/dom/FileSystemDirectoryReaderBinding.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/DirectoryBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+class PromiseHandler final : public PromiseNativeHandler {
+ public:
+ NS_DECL_ISUPPORTS
+
+ PromiseHandler(FileSystemDirectoryEntry* aParentEntry,
+ FileSystem* aFileSystem,
+ FileSystemEntriesCallback* aSuccessCallback,
+ ErrorCallback* aErrorCallback)
+ : mParentEntry(aParentEntry),
+ mFileSystem(aFileSystem),
+ mSuccessCallback(aSuccessCallback),
+ mErrorCallback(aErrorCallback) {
+ MOZ_ASSERT(aParentEntry);
+ MOZ_ASSERT(aFileSystem);
+ MOZ_ASSERT(aSuccessCallback);
+ }
+
+ MOZ_CAN_RUN_SCRIPT
+ virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+ if (NS_WARN_IF(!aValue.isObject())) {
+ return;
+ }
+
+ JS::Rooted<JSObject*> obj(aCx, &aValue.toObject());
+
+ uint32_t length;
+ if (NS_WARN_IF(!JS::GetArrayLength(aCx, obj, &length))) {
+ return;
+ }
+
+ Sequence<OwningNonNull<FileSystemEntry>> sequence;
+ if (NS_WARN_IF(!sequence.SetLength(length, fallible))) {
+ return;
+ }
+
+ for (uint32_t i = 0; i < length; ++i) {
+ JS::Rooted<JS::Value> value(aCx);
+ if (NS_WARN_IF(!JS_GetElement(aCx, obj, i, &value))) {
+ return;
+ }
+
+ if (NS_WARN_IF(!value.isObject())) {
+ return;
+ }
+
+ JS::Rooted<JSObject*> valueObj(aCx, &value.toObject());
+
+ RefPtr<File> file;
+ if (NS_SUCCEEDED(UNWRAP_OBJECT(File, valueObj, file))) {
+ RefPtr<FileSystemFileEntry> entry = new FileSystemFileEntry(
+ mParentEntry->GetParentObject(), file, mParentEntry, mFileSystem);
+ sequence[i] = entry;
+ continue;
+ }
+
+ RefPtr<Directory> directory;
+ if (NS_WARN_IF(
+ NS_FAILED(UNWRAP_OBJECT(Directory, valueObj, directory)))) {
+ return;
+ }
+
+ RefPtr<FileSystemDirectoryEntry> entry =
+ new FileSystemDirectoryEntry(mParentEntry->GetParentObject(),
+ directory, mParentEntry, mFileSystem);
+ sequence[i] = entry;
+ }
+
+ mSuccessCallback->Call(sequence);
+ }
+
+ virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+ if (mErrorCallback) {
+ RefPtr<ErrorCallbackRunnable> runnable = new ErrorCallbackRunnable(
+ mParentEntry->GetParentObject(), mErrorCallback,
+ NS_ERROR_DOM_INVALID_STATE_ERR);
+
+ FileSystemUtils::DispatchRunnable(mParentEntry->GetParentObject(),
+ runnable.forget());
+ }
+ }
+
+ private:
+ ~PromiseHandler() = default;
+
+ RefPtr<FileSystemDirectoryEntry> mParentEntry;
+ RefPtr<FileSystem> mFileSystem;
+ const RefPtr<FileSystemEntriesCallback> mSuccessCallback;
+ RefPtr<ErrorCallback> mErrorCallback;
+};
+
+NS_IMPL_ISUPPORTS0(PromiseHandler);
+
+} // anonymous namespace
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FileSystemDirectoryReader, mParentEntry,
+ mDirectory, mFileSystem)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FileSystemDirectoryReader)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(FileSystemDirectoryReader)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemDirectoryReader)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+FileSystemDirectoryReader::FileSystemDirectoryReader(
+ FileSystemDirectoryEntry* aParentEntry, FileSystem* aFileSystem,
+ Directory* aDirectory)
+ : mParentEntry(aParentEntry),
+ mFileSystem(aFileSystem),
+ mDirectory(aDirectory),
+ mAlreadyRead(false) {
+ MOZ_ASSERT(aParentEntry);
+ MOZ_ASSERT(aFileSystem);
+}
+
+FileSystemDirectoryReader::~FileSystemDirectoryReader() = default;
+
+JSObject* FileSystemDirectoryReader::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return FileSystemDirectoryReader_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void FileSystemDirectoryReader::ReadEntries(
+ FileSystemEntriesCallback& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(mDirectory);
+
+ if (mAlreadyRead) {
+ RefPtr<EmptyEntriesCallbackRunnable> runnable =
+ new EmptyEntriesCallbackRunnable(&aSuccessCallback);
+
+ FileSystemUtils::DispatchRunnable(GetParentObject(), runnable.forget());
+ return;
+ }
+
+ // This object can be used only once.
+ mAlreadyRead = true;
+
+ ErrorResult rv;
+ RefPtr<Promise> promise = mDirectory->GetFilesAndDirectories(rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback,
+ rv.StealNSResult());
+ return;
+ }
+
+ RefPtr<PromiseHandler> handler = new PromiseHandler(
+ mParentEntry, mFileSystem, &aSuccessCallback,
+ aErrorCallback.WasPassed() ? &aErrorCallback.Value() : nullptr);
+ promise->AppendNativeHandler(handler);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/compat/FileSystemDirectoryReader.h b/dom/filesystem/compat/FileSystemDirectoryReader.h
new file mode 100644
index 0000000000..90274943b4
--- /dev/null
+++ b/dom/filesystem/compat/FileSystemDirectoryReader.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileSystemDirectoryReader_h
+#define mozilla_dom_FileSystemDirectoryReader_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/FileSystemDirectoryEntry.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class Directory;
+class FileSystem;
+class FileSystemEntriesCallback;
+
+class FileSystemDirectoryReader : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemDirectoryReader)
+
+ explicit FileSystemDirectoryReader(FileSystemDirectoryEntry* aDirectoryEntry,
+ FileSystem* aFileSystem,
+ Directory* aDirectory);
+
+ nsIGlobalObject* GetParentObject() const {
+ return mParentEntry->GetParentObject();
+ }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual void ReadEntries(
+ FileSystemEntriesCallback& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback,
+ ErrorResult& aRv);
+
+ protected:
+ virtual ~FileSystemDirectoryReader();
+
+ private:
+ RefPtr<FileSystemDirectoryEntry> mParentEntry;
+ RefPtr<FileSystem> mFileSystem;
+ RefPtr<Directory> mDirectory;
+
+ bool mAlreadyRead;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_FileSystemDirectoryReader_h
diff --git a/dom/filesystem/compat/FileSystemEntry.cpp b/dom/filesystem/compat/FileSystemEntry.cpp
new file mode 100644
index 0000000000..3a69e2537a
--- /dev/null
+++ b/dom/filesystem/compat/FileSystemEntry.cpp
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemEntry.h"
+#include "CallbackRunnables.h"
+#include "FileSystem.h"
+#include "FileSystemDirectoryEntry.h"
+#include "FileSystemFileEntry.h"
+#include "mozilla/dom/FileSystemEntryBinding.h"
+#include "mozilla/dom/UnionTypes.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FileSystemEntry, mParent, mParentEntry,
+ mFileSystem)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FileSystemEntry)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(FileSystemEntry)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemEntry)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/* static */
+already_AddRefed<FileSystemEntry> FileSystemEntry::Create(
+ nsIGlobalObject* aGlobalObject,
+ const OwningFileOrDirectory& aFileOrDirectory, FileSystem* aFileSystem) {
+ MOZ_ASSERT(aGlobalObject);
+ MOZ_ASSERT(aFileSystem);
+
+ RefPtr<FileSystemEntry> entry;
+ if (aFileOrDirectory.IsFile()) {
+ entry = new FileSystemFileEntry(aGlobalObject, aFileOrDirectory.GetAsFile(),
+ nullptr, aFileSystem);
+ } else {
+ MOZ_ASSERT(aFileOrDirectory.IsDirectory());
+ entry = new FileSystemDirectoryEntry(
+ aGlobalObject, aFileOrDirectory.GetAsDirectory(), nullptr, aFileSystem);
+ }
+
+ return entry.forget();
+}
+
+FileSystemEntry::FileSystemEntry(nsIGlobalObject* aGlobal,
+ FileSystemEntry* aParentEntry,
+ FileSystem* aFileSystem)
+ : mParent(aGlobal), mParentEntry(aParentEntry), mFileSystem(aFileSystem) {
+ MOZ_ASSERT(aGlobal);
+ MOZ_ASSERT(aFileSystem);
+}
+
+FileSystemEntry::~FileSystemEntry() = default;
+
+JSObject* FileSystemEntry::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return FileSystemEntry_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void FileSystemEntry::GetParent(
+ const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback) {
+ if (!aSuccessCallback.WasPassed() && !aErrorCallback.WasPassed()) {
+ return;
+ }
+
+ if (mParentEntry) {
+ FileSystemEntryCallbackHelper::Call(GetParentObject(), aSuccessCallback,
+ mParentEntry);
+ return;
+ }
+
+ FileSystemEntryCallbackHelper::Call(GetParentObject(), aSuccessCallback,
+ this);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/compat/FileSystemEntry.h b/dom/filesystem/compat/FileSystemEntry.h
new file mode 100644
index 0000000000..2275c600a9
--- /dev/null
+++ b/dom/filesystem/compat/FileSystemEntry.h
@@ -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/. */
+
+#ifndef mozilla_dom_FileSystemEntry_h
+#define mozilla_dom_FileSystemEntry_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/FileSystemBinding.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIGlobalObject.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class FileSystem;
+class OwningFileOrDirectory;
+
+class FileSystemEntry : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemEntry)
+
+ static already_AddRefed<FileSystemEntry> Create(
+ nsIGlobalObject* aGlobalObject,
+ const OwningFileOrDirectory& aFileOrDirectory, FileSystem* aFileSystem);
+
+ nsIGlobalObject* GetParentObject() const { return mParent; }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual bool IsFile() const { return false; }
+
+ virtual bool IsDirectory() const { return false; }
+
+ virtual void GetName(nsAString& aName, ErrorResult& aRv) const = 0;
+
+ virtual void GetFullPath(nsAString& aFullPath, ErrorResult& aRv) const = 0;
+
+ void GetParent(
+ const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback);
+
+ FileSystem* Filesystem() const { return mFileSystem; }
+
+ protected:
+ FileSystemEntry(nsIGlobalObject* aGlobalObject, FileSystemEntry* aParentEntry,
+ FileSystem* aFileSystem);
+ virtual ~FileSystemEntry();
+
+ private:
+ nsCOMPtr<nsIGlobalObject> mParent;
+ RefPtr<FileSystemEntry> mParentEntry;
+ RefPtr<FileSystem> mFileSystem;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_FileSystemEntry_h
diff --git a/dom/filesystem/compat/FileSystemFileEntry.cpp b/dom/filesystem/compat/FileSystemFileEntry.cpp
new file mode 100644
index 0000000000..59216641cc
--- /dev/null
+++ b/dom/filesystem/compat/FileSystemFileEntry.cpp
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemFileEntry.h"
+#include "CallbackRunnables.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/MultipartBlobImpl.h"
+#include "mozilla/dom/FileSystemFileEntryBinding.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+class FileCallbackRunnable final : public Runnable {
+ public:
+ FileCallbackRunnable(FileCallback* aCallback, File* aFile)
+ : Runnable("FileCallbackRunnable"), mCallback(aCallback), mFile(aFile) {
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(aFile);
+ }
+
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See
+ // bug 1535398.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
+ // Here we clone the File object.
+
+ RefPtr<File> file = File::Create(mFile->GetParentObject(), mFile->Impl());
+ mCallback->Call(*file);
+ return NS_OK;
+ }
+
+ private:
+ const RefPtr<FileCallback> mCallback;
+ RefPtr<File> mFile;
+};
+
+} // anonymous namespace
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(FileSystemFileEntry, FileSystemEntry, mFile)
+
+NS_IMPL_ADDREF_INHERITED(FileSystemFileEntry, FileSystemEntry)
+NS_IMPL_RELEASE_INHERITED(FileSystemFileEntry, FileSystemEntry)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemFileEntry)
+NS_INTERFACE_MAP_END_INHERITING(FileSystemEntry)
+
+FileSystemFileEntry::FileSystemFileEntry(nsIGlobalObject* aGlobal, File* aFile,
+ FileSystemDirectoryEntry* aParentEntry,
+ FileSystem* aFileSystem)
+ : FileSystemEntry(aGlobal, aParentEntry, aFileSystem), mFile(aFile) {
+ MOZ_ASSERT(aGlobal);
+ MOZ_ASSERT(mFile);
+}
+
+FileSystemFileEntry::~FileSystemFileEntry() = default;
+
+JSObject* FileSystemFileEntry::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return FileSystemFileEntry_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void FileSystemFileEntry::GetName(nsAString& aName, ErrorResult& aRv) const {
+ mFile->GetName(aName);
+}
+
+void FileSystemFileEntry::GetFullPath(nsAString& aPath,
+ ErrorResult& aRv) const {
+ mFile->Impl()->GetDOMPath(aPath);
+ if (aPath.IsEmpty()) {
+ // We're under the root directory. webkitRelativePath
+ // (implemented as GetPath) is for cases when file is selected because its
+ // ancestor directory is selected. But that is not the case here, so need to
+ // manually prepend '/'.
+ nsAutoString name;
+ mFile->GetName(name);
+ aPath.AssignLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
+ aPath.Append(name);
+ }
+}
+
+void FileSystemFileEntry::GetFile(
+ FileCallback& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback) const {
+ RefPtr<FileCallbackRunnable> runnable =
+ new FileCallbackRunnable(&aSuccessCallback, mFile);
+
+ FileSystemUtils::DispatchRunnable(GetParentObject(), runnable.forget());
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/compat/FileSystemFileEntry.h b/dom/filesystem/compat/FileSystemFileEntry.h
new file mode 100644
index 0000000000..1fe244a66b
--- /dev/null
+++ b/dom/filesystem/compat/FileSystemFileEntry.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileSystemFileEntry_h
+#define mozilla_dom_FileSystemFileEntry_h
+
+#include "mozilla/dom/FileSystemEntry.h"
+
+namespace mozilla::dom {
+
+class File;
+class FileCallback;
+class FileSystemDirectoryEntry;
+
+class FileSystemFileEntry final : public FileSystemEntry {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FileSystemFileEntry, FileSystemEntry)
+
+ FileSystemFileEntry(nsIGlobalObject* aGlobalObject, File* aFile,
+ FileSystemDirectoryEntry* aParentEntry,
+ FileSystem* aFileSystem);
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual bool IsFile() const override { return true; }
+
+ virtual void GetName(nsAString& aName, ErrorResult& aRv) const override;
+
+ virtual void GetFullPath(nsAString& aFullPath,
+ ErrorResult& aRv) const override;
+
+ void GetFile(
+ FileCallback& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback) const;
+
+ private:
+ ~FileSystemFileEntry();
+
+ RefPtr<File> mFile;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileSystemFileEntry_h
diff --git a/dom/filesystem/compat/FileSystemRootDirectoryEntry.cpp b/dom/filesystem/compat/FileSystemRootDirectoryEntry.cpp
new file mode 100644
index 0000000000..fad6243d15
--- /dev/null
+++ b/dom/filesystem/compat/FileSystemRootDirectoryEntry.cpp
@@ -0,0 +1,138 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemRootDirectoryEntry.h"
+#include "FileSystemRootDirectoryReader.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "CallbackRunnables.h"
+#include "nsReadableUtils.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(FileSystemRootDirectoryEntry,
+ FileSystemDirectoryEntry, mEntries)
+
+NS_IMPL_ADDREF_INHERITED(FileSystemRootDirectoryEntry, FileSystemDirectoryEntry)
+NS_IMPL_RELEASE_INHERITED(FileSystemRootDirectoryEntry,
+ FileSystemDirectoryEntry)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemRootDirectoryEntry)
+NS_INTERFACE_MAP_END_INHERITING(FileSystemDirectoryEntry)
+
+FileSystemRootDirectoryEntry::FileSystemRootDirectoryEntry(
+ nsIGlobalObject* aGlobal, Sequence<RefPtr<FileSystemEntry>> aEntries,
+ FileSystem* aFileSystem)
+ : FileSystemDirectoryEntry(aGlobal, nullptr, nullptr, aFileSystem),
+ mEntries(std::move(aEntries)) {
+ MOZ_ASSERT(aGlobal);
+}
+
+FileSystemRootDirectoryEntry::~FileSystemRootDirectoryEntry() = default;
+
+void FileSystemRootDirectoryEntry::GetName(nsAString& aName,
+ ErrorResult& aRv) const {
+ aName.Truncate();
+}
+
+void FileSystemRootDirectoryEntry::GetFullPath(nsAString& aPath,
+ ErrorResult& aRv) const {
+ aPath.AssignLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
+}
+
+already_AddRefed<FileSystemDirectoryReader>
+FileSystemRootDirectoryEntry::CreateReader() {
+ RefPtr<FileSystemDirectoryReader> reader =
+ new FileSystemRootDirectoryReader(this, Filesystem(), mEntries);
+ return reader.forget();
+}
+
+void FileSystemRootDirectoryEntry::GetInternal(
+ const nsAString& aPath, const FileSystemFlags& aFlag,
+ const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback,
+ GetInternalType aType) {
+ if (!aSuccessCallback.WasPassed() && !aErrorCallback.WasPassed()) {
+ return;
+ }
+
+ if (aFlag.mCreate) {
+ ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback,
+ NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ nsTArray<nsString> parts;
+ if (!FileSystemUtils::IsValidRelativeDOMPath(aPath, parts)) {
+ ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback,
+ NS_ERROR_DOM_NOT_FOUND_ERR);
+ return;
+ }
+
+ MOZ_ASSERT(!parts.IsEmpty());
+
+ RefPtr<FileSystemEntry> entry;
+ for (uint32_t i = 0; i < mEntries.Length(); ++i) {
+ ErrorResult rv;
+ nsAutoString name;
+ mEntries[i]->GetName(name, rv);
+
+ if (NS_WARN_IF(rv.Failed())) {
+ ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback,
+ rv.StealNSResult());
+ return;
+ }
+
+ if (name == parts[0]) {
+ entry = mEntries[i];
+ break;
+ }
+ }
+
+ // Not found.
+ if (!entry) {
+ ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback,
+ NS_ERROR_DOM_NOT_FOUND_ERR);
+ return;
+ }
+
+ // No subdirectory in the path.
+ if (parts.Length() == 1) {
+ if ((entry->IsFile() && aType == eGetDirectory) ||
+ (entry->IsDirectory() && aType == eGetFile)) {
+ ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback,
+ NS_ERROR_DOM_TYPE_MISMATCH_ERR);
+ return;
+ }
+
+ if (aSuccessCallback.WasPassed()) {
+ RefPtr<EntryCallbackRunnable> runnable =
+ new EntryCallbackRunnable(&aSuccessCallback.Value(), entry);
+
+ FileSystemUtils::DispatchRunnable(GetParentObject(), runnable.forget());
+ }
+ return;
+ }
+
+ // Subdirectories, but this is a file.
+ if (entry->IsFile()) {
+ ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback,
+ NS_ERROR_DOM_NOT_FOUND_ERR);
+ return;
+ }
+
+ // Let's recreate a path without the first directory.
+ nsAutoString path;
+ StringJoinAppend(
+ path,
+ NS_LITERAL_STRING_FROM_CSTRING(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL),
+ Span{parts}.From(1));
+
+ auto* directoryEntry = static_cast<FileSystemDirectoryEntry*>(entry.get());
+ directoryEntry->GetInternal(path, aFlag, aSuccessCallback, aErrorCallback,
+ aType);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/compat/FileSystemRootDirectoryEntry.h b/dom/filesystem/compat/FileSystemRootDirectoryEntry.h
new file mode 100644
index 0000000000..b6ffb976b8
--- /dev/null
+++ b/dom/filesystem/compat/FileSystemRootDirectoryEntry.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileSystemRootDirectoryEntry_h
+#define mozilla_dom_FileSystemRootDirectoryEntry_h
+
+#include "mozilla/dom/FileSystemDirectoryEntry.h"
+
+namespace mozilla::dom {
+
+class FileSystemRootDirectoryEntry final : public FileSystemDirectoryEntry {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FileSystemRootDirectoryEntry,
+ FileSystemDirectoryEntry)
+
+ FileSystemRootDirectoryEntry(nsIGlobalObject* aGlobalObject,
+ Sequence<RefPtr<FileSystemEntry>> aEntries,
+ FileSystem* aFileSystem);
+
+ virtual void GetName(nsAString& aName, ErrorResult& aRv) const override;
+
+ virtual void GetFullPath(nsAString& aFullPath,
+ ErrorResult& aRv) const override;
+
+ virtual already_AddRefed<FileSystemDirectoryReader> CreateReader() override;
+
+ private:
+ ~FileSystemRootDirectoryEntry();
+
+ virtual void GetInternal(
+ const nsAString& aPath, const FileSystemFlags& aFlag,
+ const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback,
+ GetInternalType aType) override;
+
+ void Error(const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback,
+ nsresult aError) const;
+
+ Sequence<RefPtr<FileSystemEntry>> mEntries;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileSystemRootDirectoryEntry_h
diff --git a/dom/filesystem/compat/FileSystemRootDirectoryReader.cpp b/dom/filesystem/compat/FileSystemRootDirectoryReader.cpp
new file mode 100644
index 0000000000..5d30f0c1cb
--- /dev/null
+++ b/dom/filesystem/compat/FileSystemRootDirectoryReader.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 "FileSystemRootDirectoryReader.h"
+#include "CallbackRunnables.h"
+#include "nsIGlobalObject.h"
+#include "mozilla/dom/FileSystemDirectoryReaderBinding.h"
+#include "mozilla/dom/FileSystemUtils.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+class EntriesCallbackRunnable final : public Runnable {
+ public:
+ EntriesCallbackRunnable(FileSystemEntriesCallback* aCallback,
+ const Sequence<RefPtr<FileSystemEntry>>& aEntries)
+ : Runnable("EntriesCallbackRunnable"),
+ mCallback(aCallback),
+ mEntries(aEntries) {
+ MOZ_ASSERT(aCallback);
+ }
+
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See
+ // bug 1535398.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
+ Sequence<OwningNonNull<FileSystemEntry>> entries;
+ for (uint32_t i = 0; i < mEntries.Length(); ++i) {
+ if (!entries.AppendElement(mEntries[i].forget(), fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ mCallback->Call(entries);
+ return NS_OK;
+ }
+
+ private:
+ const RefPtr<FileSystemEntriesCallback> mCallback;
+ Sequence<RefPtr<FileSystemEntry>> mEntries;
+};
+
+} // anonymous namespace
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(FileSystemRootDirectoryReader,
+ FileSystemDirectoryReader, mEntries)
+
+NS_IMPL_ADDREF_INHERITED(FileSystemRootDirectoryReader,
+ FileSystemDirectoryReader)
+NS_IMPL_RELEASE_INHERITED(FileSystemRootDirectoryReader,
+ FileSystemDirectoryReader)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemRootDirectoryReader)
+NS_INTERFACE_MAP_END_INHERITING(FileSystemDirectoryReader)
+
+FileSystemRootDirectoryReader::FileSystemRootDirectoryReader(
+ FileSystemDirectoryEntry* aParentEntry, FileSystem* aFileSystem,
+ const Sequence<RefPtr<FileSystemEntry>>& aEntries)
+ : FileSystemDirectoryReader(aParentEntry, aFileSystem, nullptr),
+ mEntries(aEntries),
+ mAlreadyRead(false) {
+ MOZ_ASSERT(aParentEntry);
+ MOZ_ASSERT(aFileSystem);
+}
+
+FileSystemRootDirectoryReader::~FileSystemRootDirectoryReader() = default;
+
+void FileSystemRootDirectoryReader::ReadEntries(
+ FileSystemEntriesCallback& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback,
+ ErrorResult& aRv) {
+ if (mAlreadyRead) {
+ RefPtr<EmptyEntriesCallbackRunnable> runnable =
+ new EmptyEntriesCallbackRunnable(&aSuccessCallback);
+
+ aRv =
+ FileSystemUtils::DispatchRunnable(GetParentObject(), runnable.forget());
+ return;
+ }
+
+ // This object can be used only once.
+ mAlreadyRead = true;
+
+ RefPtr<EntriesCallbackRunnable> runnable =
+ new EntriesCallbackRunnable(&aSuccessCallback, mEntries);
+
+ aRv = FileSystemUtils::DispatchRunnable(GetParentObject(), runnable.forget());
+}
+
+} // namespace mozilla::dom
diff --git a/dom/filesystem/compat/FileSystemRootDirectoryReader.h b/dom/filesystem/compat/FileSystemRootDirectoryReader.h
new file mode 100644
index 0000000000..7429de8a9c
--- /dev/null
+++ b/dom/filesystem/compat/FileSystemRootDirectoryReader.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileSystemRootDirectoryReader_h
+#define mozilla_dom_FileSystemRootDirectoryReader_h
+
+#include "FileSystemDirectoryReader.h"
+
+namespace mozilla::dom {
+
+class FileSystemRootDirectoryReader final : public FileSystemDirectoryReader {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FileSystemRootDirectoryReader,
+ FileSystemDirectoryReader)
+
+ explicit FileSystemRootDirectoryReader(
+ FileSystemDirectoryEntry* aParentEntry, FileSystem* aFileSystem,
+ const Sequence<RefPtr<FileSystemEntry>>& aEntries);
+
+ virtual void ReadEntries(
+ FileSystemEntriesCallback& aSuccessCallback,
+ const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback,
+ ErrorResult& aRv) override;
+
+ private:
+ ~FileSystemRootDirectoryReader();
+
+ Sequence<RefPtr<FileSystemEntry>> mEntries;
+ bool mAlreadyRead;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FileSystemRootDirectoryReader_h
diff --git a/dom/filesystem/compat/moz.build b/dom/filesystem/compat/moz.build
new file mode 100644
index 0000000000..6757a73d9e
--- /dev/null
+++ b/dom/filesystem/compat/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/.
+
+TEST_DIRS += ["tests"]
+
+EXPORTS.mozilla.dom += [
+ "FileSystem.h",
+ "FileSystemDirectoryEntry.h",
+ "FileSystemDirectoryReader.h",
+ "FileSystemEntry.h",
+ "FileSystemFileEntry.h",
+]
+
+UNIFIED_SOURCES += [
+ "CallbackRunnables.cpp",
+ "FileSystem.cpp",
+ "FileSystemDirectoryEntry.cpp",
+ "FileSystemDirectoryReader.cpp",
+ "FileSystemEntry.cpp",
+ "FileSystemFileEntry.cpp",
+ "FileSystemRootDirectoryEntry.cpp",
+ "FileSystemRootDirectoryReader.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/dom/filesystem/compat/tests/mochitest.ini b/dom/filesystem/compat/tests/mochitest.ini
new file mode 100644
index 0000000000..2b6eafb550
--- /dev/null
+++ b/dom/filesystem/compat/tests/mochitest.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+support-files =
+ script_entries.js
+ !/dom/html/test/form_submit_server.sjs
+
+[test_basic.html]
+[test_no_dnd.html]
+[test_formSubmission.html]
diff --git a/dom/filesystem/compat/tests/moz.build b/dom/filesystem/compat/tests/moz.build
new file mode 100644
index 0000000000..7c990fbc62
--- /dev/null
+++ b/dom/filesystem/compat/tests/moz.build
@@ -0,0 +1,7 @@
+# -*- 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"]
diff --git a/dom/filesystem/compat/tests/script_entries.js b/dom/filesystem/compat/tests/script_entries.js
new file mode 100644
index 0000000000..7f52fe6bf2
--- /dev/null
+++ b/dom/filesystem/compat/tests/script_entries.js
@@ -0,0 +1,47 @@
+/* eslint-env mozilla/chrome-script */
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+Cu.importGlobalProperties(["File", "Directory"]);
+var tmpFile, tmpDir;
+
+addMessageListener("entries.open", function (e) {
+ tmpFile = Services.dirsvc
+ .QueryInterface(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+ tmpFile.append("file.txt");
+ tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ tmpDir = Services.dirsvc
+ .QueryInterface(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+
+ tmpDir.append("dir-test");
+ tmpDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
+
+ var file1 = tmpDir.clone();
+ file1.append("foo.txt");
+ file1.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ var dir1 = tmpDir.clone();
+ dir1.append("subdir");
+ dir1.create(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
+
+ var file2 = dir1.clone();
+ file2.append("bar..txt"); // Note the double ..
+ file2.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ var dir2 = dir1.clone();
+ dir2.append("subsubdir");
+ dir2.create(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
+
+ File.createFromNsIFile(tmpFile).then(function (file) {
+ sendAsyncMessage("entries.opened", {
+ data: [new Directory(tmpDir.path), file],
+ });
+ });
+});
+
+addMessageListener("entries.delete", function (e) {
+ tmpFile.remove(true);
+ tmpDir.remove(true);
+ sendAsyncMessage("entries.deleted");
+});
diff --git a/dom/filesystem/compat/tests/test_basic.html b/dom/filesystem/compat/tests/test_basic.html
new file mode 100644
index 0000000000..4ad0c37d67
--- /dev/null
+++ b/dom/filesystem/compat/tests/test_basic.html
@@ -0,0 +1,549 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Blink FileSystem API - subset</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<input id="entries" type="file"></input>
+<script type="application/javascript">
+var fileEntry;
+var directoryEntry;
+var script;
+
+function setup_tests() {
+ SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true],
+ ["dom.filesystem.pathcheck.disabled", true],
+ ["dom.webkitBlink.filesystem.enabled", true]]}, next);
+}
+
+function populate_entries() {
+ var url = SimpleTest.getTestFileURL("script_entries.js");
+ script = SpecialPowers.loadChromeScript(url);
+
+ function onOpened(message) {
+ var entries = document.getElementById("entries");
+ SpecialPowers.wrap(entries).mozSetDndFilesAndDirectories(message.data);
+ next();
+ }
+
+ script.addMessageListener("entries.opened", onOpened);
+ script.sendAsyncMessage("entries.open");
+}
+
+function test_entries() {
+ var entries = document.getElementById("entries");
+ ok("webkitEntries" in entries, "HTMLInputElement.webkitEntries");
+ is(entries.webkitEntries.length, 2, "HTMLInputElement.webkitEntries.length == 2");
+ is(entries.files.length, 1, "HTMLInputElement.files is still populated");
+
+ for (var i = 0; i < entries.webkitEntries.length; ++i) {
+ if (entries.webkitEntries[i].isFile) {
+ ok(!fileEntry, "We just want 1 fileEntry");
+ fileEntry = entries.webkitEntries[i];
+ } else {
+ ok(entries.webkitEntries[i].isDirectory, "If not a file, we have a directory.");
+ ok(!directoryEntry, "We just want 1 directoryEntry");
+ directoryEntry = entries.webkitEntries[i];
+ }
+ }
+
+ next();
+}
+
+function test_fileEntry() {
+ ok("name" in fileEntry, "We have a name.");
+ ok("fullPath" in fileEntry, "We have a fullPath.");
+ ok("filesystem" in fileEntry, "We have a filesystem.");
+
+ next();
+}
+
+function test_fileEntry_file() {
+ fileEntry.file(function(file) {
+ ok(file, "We have a file here!");
+ is(file.name, fileEntry.name, "Same file name.");
+ next();
+ }, function() {
+ ok(false, "Something when wrong!");
+ });
+}
+
+function test_fileEntry_getParent() {
+ fileEntry.getParent(function(entry) {
+ is(fileEntry.fullPath, entry.fullPath, "Top level FileEntry should return itself as parent.");
+ next();
+ }, function() {
+ ok(false, "This is wrong.");
+ });
+}
+
+function test_directoryEntry() {
+ ok("name" in directoryEntry, "We have a name.");
+ ok("fullPath" in directoryEntry, "We have a fullPath.");
+ ok("filesystem" in directoryEntry, "We have a filesystem.");
+
+ next();
+}
+
+function test_directoryEntry_createReader() {
+ var reader = directoryEntry.createReader();
+ ok(reader, "We have a DirectoryReader");
+
+ reader.readEntries(function(a) {
+ ok(Array.isArray(a), "We want an array.");
+ is(a.length, 2, "reader.readyEntries returns 2 elements.");
+
+ for (var i = 0; i < 2; ++i) {
+ ok(a[i].name == "subdir" || a[i].name == "foo.txt", "Correct names");
+ is(a[i].fullPath, directoryEntry.fullPath + "/" + a[i].name, "FullPath is correct");
+ }
+
+ // Called twice:
+ reader.readEntries(function(a1) {
+ ok(Array.isArray(a1), "We want an array.");
+ is(a1.length, 0, "reader.readyEntries returns 0 elements.");
+ next();
+ }, function() {
+ ok(false, "Something when wrong!");
+ });
+ }, function() {
+ ok(false, "Something when wrong!");
+ });
+}
+
+function test_directoryEntry_getParent() {
+ directoryEntry.getParent(function(entry) {
+ is(directoryEntry.fullPath, entry.fullPath, "Top level FileEntry should return itself as parent.");
+ next();
+ }, function() {
+ ok(false, "This is wrong.");
+ });
+}
+
+function test_directoryEntry_getFile_securityError() {
+ directoryEntry.getFile("foo", { create: true },
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "SecurityError", "This must generate a SecurityError.");
+ next();
+ });
+}
+
+function test_directoryEntry_getFile_typeMismatchError() {
+ directoryEntry.getFile("subdir", {},
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "TypeMismatchError", "This must generate a TypeMismatchError.");
+ next();
+ });
+}
+
+function test_directoryEntry_getFile_nonValidPath() {
+ directoryEntry.getFile("../../", {},
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "NotFoundError", "This must generate a NotFoundError.");
+ next();
+ });
+}
+
+function test_directoryEntry_getFile_nonExistingPath() {
+ directoryEntry.getFile("foo_bar.txt", {},
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "NotFoundError", "This must generate a NotFoundError.");
+ next();
+ });
+}
+
+function test_directoryEntry_getFile_simple() {
+ directoryEntry.getFile("foo.txt", {},
+ function(e) {
+ is(e.name, "foo.txt", "We have the right FileEntry.");
+ test_getParent(e, directoryEntry, /* nested */ false);
+ }, function(e) {
+ ok(false, "This should not happen.");
+ });
+}
+
+function test_directoryEntry_getFile_deep() {
+ directoryEntry.getFile("subdir/bar..txt", {},
+ function(e) {
+ is(e.name, "bar..txt", "We have the right FileEntry.");
+ test_getParent(e, directoryEntry, /* nested */ true);
+ }, function(e) {
+ ok(false, "This should not happen.");
+ });
+}
+
+function test_directoryEntry_getDirectory_securityError() {
+ directoryEntry.getDirectory("foo", { create: true },
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "SecurityError", "This must generate a SecurityError.");
+ next();
+ });
+}
+
+function test_directoryEntry_getDirectory_typeMismatchError() {
+ directoryEntry.getDirectory("foo.txt", {},
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "TypeMismatchError", "This must generate a TypeMismatchError.");
+ next();
+ });
+}
+
+function test_directoryEntry_getDirectory_nonValidPath() {
+ directoryEntry.getDirectory("../../", {},
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "NotFoundError", "This must generate a NotFoundError.");
+ next();
+ });
+}
+
+function test_directoryEntry_getDirectory_nonExistingPath() {
+ directoryEntry.getDirectory("non_existing_dir", {},
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "NotFoundError", "This must generate a NotFoundError.");
+ next();
+ });
+}
+
+function test_directoryEntry_getDirectory_simple() {
+ directoryEntry.getDirectory("subdir", {},
+ function(e) {
+ is(e.name, "subdir", "We have the right DirectoryEntry.");
+ test_getParent(e, directoryEntry, /* nested */ false);
+ }, function(e) {
+ ok(false, "This should not happen.");
+ });
+}
+
+function test_directoryEntry_getDirectory_deep() {
+ directoryEntry.getDirectory("subdir/subsubdir", {},
+ function(e) {
+ is(e.name, "subsubdir", "We have the right DirectoryEntry.");
+ test_getParent(e, directoryEntry, /* nested */ true);
+ }, function(e) {
+ ok(false, "This should not happen.");
+ });
+}
+
+function test_filesystem() {
+ is(fileEntry.filesystem, directoryEntry.filesystem, "FileSystem object is shared.");
+
+ var fs = fileEntry.filesystem;
+ ok(fs.name, "FileSystem.name exists.");
+ ok(fs.root, "FileSystem has a root.");
+
+ is(fs.root.name, "", "FileSystem.root.name must be an empty string.");
+ is(fs.root.fullPath, "/", "FileSystem.root.fullPath must be '/'");
+
+ var reader = fs.root.createReader();
+ reader.readEntries(function(a) {
+ ok(Array.isArray(a), "We want an array.");
+ is(a.length, 2, "reader.readyEntries returns 2 elements.");
+ next();
+ }, function() {
+ ok(false, "Something when wrong!");
+ });
+}
+
+function test_root_getFile_securityError() {
+ fileEntry.filesystem.root.getFile("foo", { create: true },
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "SecurityError", "This must generate a SecurityError.");
+ next();
+ });
+}
+
+function test_root_getFile_typeMismatchError() {
+ fileEntry.filesystem.root.getFile(directoryEntry.name, {},
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "TypeMismatchError", "This must generate a TypeMismatchError.");
+ next();
+ });
+}
+
+function test_root_getFile_nonValidPath() {
+ fileEntry.filesystem.root.getFile("../../", {},
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "NotFoundError", "This must generate a NotFoundError.");
+ next();
+ });
+}
+
+function test_root_getFile_nonExistingPath() {
+ fileEntry.filesystem.root.getFile("existing.txt", {},
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "NotFoundError", "This must generate a NotFoundError.");
+ next();
+ });
+}
+
+function test_root_getFile_simple() {
+ fileEntry.filesystem.root.getFile(fileEntry.name, {},
+ function(e) {
+ is(e.name, fileEntry.name, "We have the right FileEntry.");
+ next();
+ }, function(e) {
+ ok(false, "This should not happen.");
+ });
+}
+
+function test_root_getFile_deep() {
+ fileEntry.filesystem.root.getFile(directoryEntry.name + "/subdir/bar..txt", {},
+ function(e) {
+ is(e.name, "bar..txt", "We have the right FileEntry.");
+ next();
+ }, function(e) {
+ ok(false, "This should not happen.");
+ });
+}
+
+function test_root_getDirectory_securityError() {
+ fileEntry.filesystem.root.getDirectory("foo", { create: true },
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "SecurityError", "This must generate a SecurityError.");
+ next();
+ });
+}
+
+function test_root_getDirectory_typeMismatchError() {
+ fileEntry.filesystem.root.getDirectory(fileEntry.name, {},
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "TypeMismatchError", "This must generate a TypeMismatchError.");
+ next();
+ });
+}
+
+function test_root_getDirectory_nonValidPath() {
+ fileEntry.filesystem.root.getDirectory("../../", {},
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "NotFoundError", "This must generate a NotFoundError.");
+ next();
+ });
+}
+
+function test_root_getDirectory_nonExistingPath() {
+ fileEntry.filesystem.root.getDirectory("404", {},
+ function() {
+ ok(false, "This should not happen.");
+ }, function(e) {
+ is(e.name, "NotFoundError", "This must generate a NotFoundError.");
+ next();
+ });
+}
+
+function test_root_getDirectory_simple() {
+ fileEntry.filesystem.root.getDirectory(directoryEntry.name, {},
+ function(e) {
+ is(e.name, directoryEntry.name, "We have the right DirectoryEntry.");
+ next();
+ }, function(e) {
+ ok(false, "This should not happen.");
+ });
+}
+
+function test_root_getDirectory_deep() {
+ fileEntry.filesystem.root.getDirectory(directoryEntry.name + "/subdir/subsubdir", {},
+ function(e) {
+ is(e.name, "subsubdir", "We have the right DirectoryEntry.");
+ next();
+ }, function(e) {
+ ok(false, "This should not happen.");
+ });
+}
+
+function cleanUpTestingFiles() {
+ script.addMessageListener("entries.deleted", function onDeleted() {
+ script.removeMessageListener("entries.deleted");
+ script.destroy();
+ next();
+ });
+
+ script.sendAsyncMessage("entries.delete");
+}
+
+function test_getParent(entry, parentEntry, nested) {
+ entry.getParent(function(e) {
+ ok(e, "We have a parent Entry.");
+ if (!nested) {
+ is(e, parentEntry, "Parent entry matches");
+ next();
+ } else {
+ test_getParent(e, parentEntry, false);
+ }
+ }, function(e) {
+ ok(false, "This should not happen.");
+ });
+}
+
+function test_webkitRelativePath() {
+ fileEntry.file(function(file1) {
+ ok(file1, "We have a file here!");
+ ok(!file1.webkitRelativePath, "webkitRelativePath is an empty string");
+
+ fileEntry.file(function(file2) {
+ ok(file2, "We have a file here!");
+ ok(!file2.webkitRelativePath, "webkitRelativePath is an empty string");
+ isnot(file1, file2, "The 2 files are not the same");
+
+ next();
+ }, function() {
+ ok(false, "Something when wrong!");
+ });
+ }, function() {
+ ok(false, "Something when wrong!");
+ });
+}
+
+function test_deprecatedCallbacks() {
+ try {
+ fileEntry.file({ handleEvent: _ => { ok(false, "This function should not be called!"); }});
+ ok(false, "fileEntry.file() should throw with wrong arguments");
+ } catch (e) {
+ ok(true, "fileEntry.file() should throw with wrong arguments");
+ is(e.name, "TypeError", "Correct exception");
+ }
+
+ try {
+ fileEntry.getParent({ handleEvent: _ => { ok(false, "This function should not be called!"); }});
+ ok(false, "fileEntry.getParent() should throw with wrong arguments");
+ } catch (e) {
+ ok(true, "fileEntry.getParent() should throw with wrong arguments");
+ is(e.name, "TypeError", "Correct exception");
+ }
+
+ try {
+ directoryEntry.getFile("file.txt", {}, { handleEvent: _ => { ok(false, "This function should not be called!"); }});
+ ok(false, "directoryEntry.getFile() should throw with wrong arguments");
+ } catch (e) {
+ ok(true, "directoryEntry.getFile() should throw with wrong arguments");
+ is(e.name, "TypeError", "Correct exception");
+ }
+
+ try {
+ directoryEntry.getDirectory("foo", { create: true }, { handleEvent: _ => { ok(false, "This function should not be called!"); }});
+ ok(false, "directoryEntry.getDirectory() should throw with wrong arguments");
+ } catch (e) {
+ ok(true, "directoryEntry.getDirectory() should throw with wrong arguments");
+ is(e.name, "TypeError", "Correct exception");
+ }
+
+ try {
+ directoryEntry.getParent({ handleEvent: _ => { ok(false, "This function should not be called!"); }});
+ ok(false, "directoryEntry.getParent() should throw with wrong arguments");
+ } catch (e) {
+ ok(true, "directoryEntry.getParent() should throw with wrong arguments");
+ is(e.name, "TypeError", "Correct exception");
+ }
+
+ let reader = directoryEntry.createReader();
+ ok(reader, "We have a DirectoryReader");
+
+ try {
+ reader.readEntries({ handleEvent: _ => { ok(false, "This function should not be called!"); }});
+ ok(false, "reader.readEntries() should throw with wrong arguments");
+ } catch (e) {
+ ok(true, "reader.readEntries() should throw with wrong arguments");
+ is(e.name, "TypeError", "Correct exception");
+ }
+
+ next();
+}
+
+var tests = [
+ setup_tests,
+ populate_entries,
+
+ test_entries,
+
+ test_fileEntry,
+ test_fileEntry_file,
+ test_fileEntry_getParent,
+
+ test_directoryEntry,
+ test_directoryEntry_createReader,
+ test_directoryEntry_getParent,
+
+ test_directoryEntry_getFile_securityError,
+ test_directoryEntry_getFile_typeMismatchError,
+ test_directoryEntry_getFile_nonValidPath,
+ test_directoryEntry_getFile_nonExistingPath,
+ test_directoryEntry_getFile_simple,
+ test_directoryEntry_getFile_deep,
+
+ test_directoryEntry_getDirectory_securityError,
+ test_directoryEntry_getDirectory_typeMismatchError,
+ test_directoryEntry_getDirectory_nonValidPath,
+ test_directoryEntry_getDirectory_nonExistingPath,
+ test_directoryEntry_getDirectory_simple,
+ test_directoryEntry_getDirectory_deep,
+
+ test_filesystem,
+
+ test_root_getFile_securityError,
+ test_root_getFile_typeMismatchError,
+ test_root_getFile_nonValidPath,
+ test_root_getFile_nonExistingPath,
+ test_root_getFile_simple,
+ test_root_getFile_deep,
+
+ test_root_getDirectory_securityError,
+ test_root_getDirectory_typeMismatchError,
+ test_root_getDirectory_nonValidPath,
+ test_root_getDirectory_nonExistingPath,
+ test_root_getDirectory_simple,
+ test_root_getDirectory_deep,
+
+ test_webkitRelativePath,
+
+ test_deprecatedCallbacks,
+
+ cleanUpTestingFiles,
+];
+
+function next() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+SimpleTest.waitForExplicitFinish();
+next();
+</script>
+</body>
+</html>
diff --git a/dom/filesystem/compat/tests/test_formSubmission.html b/dom/filesystem/compat/tests/test_formSubmission.html
new file mode 100644
index 0000000000..2447e7a071
--- /dev/null
+++ b/dom/filesystem/compat/tests/test_formSubmission.html
@@ -0,0 +1,271 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Directory form submission</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
+</head>
+<body onload="return next();">
+
+<iframe name="target_iframe" id="target_iframe"></iframe>
+
+<form action="../../../html/test/form_submit_server.sjs" target="target_iframe" id="form"
+ method="POST" enctype="multipart/form-data">
+</form>
+
+<script class="testbody" type="text/javascript">
+var form;
+var iframe;
+var input;
+var script;
+var xhr;
+
+function setup_tests() {
+ form = document.getElementById("form");
+
+ iframe = document.getElementById("target_iframe");
+ iframe.onload = function() {
+ info("Frame loaded!");
+ next();
+ };
+
+ SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true],
+ ["dom.filesystem.pathcheck.disabled", true],
+ ["dom.webkitBlink.filesystem.enabled", true]]}, next);
+}
+
+function populate_entries(webkitDirectory) {
+ var url = SimpleTest.getTestFileURL("script_entries.js");
+ script = SpecialPowers.loadChromeScript(url);
+
+ if (input) {
+ form.removeChild(input);
+ }
+
+ input = document.createElement("input");
+ input.setAttribute("id", "input");
+ input.setAttribute("type", "file");
+ input.setAttribute("name", "input");
+
+ if (webkitDirectory) {
+ input.setAttribute("webkitdirectory", "true");
+ }
+
+ form.appendChild(input);
+
+ function onOpened(message) {
+ input.addEventListener("change", function() {
+ next();
+ }, {once: true});
+
+ SpecialPowers.wrap(input).mozSetDndFilesAndDirectories([message.data[0]]);
+ }
+
+ script.addMessageListener("entries.opened", onOpened);
+ script.sendAsyncMessage("entries.open");
+}
+
+function delete_entries() {
+ script.sendAsyncMessage("entries.delete");
+ script.addMessageListener("entries.deleted", function() {
+ script.destroy();
+ next();
+ });
+}
+
+function setup_plain() {
+ info("Preparing for a plain text submission...");
+ form.action = "../../../html/test/form_submit_server.sjs?plain";
+ form.method = "POST";
+ form.enctype = "text/plain";
+ form.submit();
+}
+
+function test_plain() {
+ var content = iframe.contentDocument.documentElement.textContent;
+ var submission = JSON.parse(content);
+ info(submission);
+ is(submission, input.webkitEntries.map(function(v) {
+ return "input=" + v.name + "\r\n";
+ }).join(""), "Data match");
+
+ next();
+}
+
+function setup_urlencoded() {
+ info("Preparing for a urlencoded submission...");
+ form.action = "../../../html/test/form_submit_server.sjs?url";
+ form.method = "POST";
+ form.enctype = "application/x-www-form-urlencoded";
+ form.submit();
+}
+
+function setup_urlencoded_get() {
+ info("Preparing for a urlencoded+GET submission...");
+ form.action = "../../../html/test/form_submit_server.sjs?xxyy";
+ form.method = "GET";
+ form.enctype = "";
+ form.submit();
+}
+
+function setup_urlencoded_empty() {
+ info("Preparing for a urlencoded+default values submission...");
+ form.action = "../../../html/test/form_submit_server.sjs";
+ form.method = "";
+ form.enctype = "";
+ form.submit();
+}
+
+function test_urlencoded() {
+ var content = iframe.contentDocument.documentElement.textContent;
+ var submission = JSON.parse(content);
+ info(submission);
+ is(submission, input.webkitEntries.map(function(v) {
+ return "input=" + v.name;
+ }).join("&"), "Data match");
+
+ next();
+}
+
+function setup_formData() {
+ info("Preparing for a fromData submission...");
+
+ xhr = new XMLHttpRequest();
+ xhr.onload = next;
+ xhr.open("POST", "../../../html/test/form_submit_server.sjs");
+ xhr.send(new FormData(form));
+}
+
+function test_multipart() {
+ var submission = JSON.parse(xhr.responseText);
+
+ var array = input.webkitEntries;
+ is(submission.length, array.length, "Same length");
+ info(submission);
+
+ for (var i = 0; i < array.length; ++i) {
+ if (array[i].isDirectory) {
+ is(submission[i].headers["Content-Disposition"],
+ "form-data; name=\"input\"; filename=\"/" + array[i].name + "\"",
+ "Correct Content-Disposition");
+ is(submission[i].headers["Content-Type"], "application/octet-stream",
+ "Correct Content-Type");
+ is(submission[i].body, "", "Correct body");
+ } else {
+ ok(array[i].isFile);
+ is(submission[i].headers["Content-Disposition"],
+ "form-data; name=\"input\"; filename=\"" + array[i].name + "\"",
+ "Correct Content-Disposition");
+ is(submission[i].headers["Content-Type"], array[i].type,
+ "Correct Content-Type");
+ is(submission[i].body, "", "Correct body");
+ }
+ }
+
+ next();
+}
+
+function getInputFiles(inputElement) {
+ var array = [];
+ for (var i = 0; i < inputElement.files.length; ++i) {
+ array.push(inputElement.files[i]);
+ }
+ return array;
+}
+
+function test_webkit_plain() {
+ var content = iframe.contentDocument.documentElement.textContent;
+ var submission = JSON.parse(content);
+
+ is(submission, getInputFiles(input).map(function(v) {
+ return "input=" + v.name + "\r\n";
+ }).join(""), "Data match");
+
+ next();
+}
+
+function test_webkit_urlencoded() {
+ var content = iframe.contentDocument.documentElement.textContent;
+ var submission = JSON.parse(content);
+ is(submission, getInputFiles(input).map(function(v) {
+ return "input=" + v.name;
+ }).join("&"), "Data match");
+
+ next();
+}
+
+function test_webkit_multipart() {
+ var submission = JSON.parse(xhr.responseText);
+ var array = getInputFiles(input);
+ is(submission.length, array.length, "Same length");
+
+ for (var i = 0; i < array.length; ++i) {
+ ok(array[i] instanceof File);
+ is(submission[i].headers["Content-Disposition"],
+ "form-data; name=\"input\"; filename=\"" + array[i].webkitRelativePath + "\"",
+ "Correct Content-Disposition");
+ is(submission[i].headers["Content-Type"], array[i].type,
+ "Correct Content-Type");
+ is(submission[i].body, "", "Correct body");
+ }
+ next();
+}
+
+var tests = [
+ setup_tests,
+
+ function() { populate_entries(false); },
+
+ setup_plain,
+ test_plain,
+
+ setup_urlencoded,
+ test_urlencoded,
+
+ setup_urlencoded_get,
+ test_urlencoded,
+
+ setup_urlencoded_empty,
+ test_urlencoded,
+
+ setup_formData,
+ test_multipart,
+
+ delete_entries,
+
+ function() { populate_entries(true); },
+
+ setup_plain,
+ test_webkit_plain,
+
+ setup_urlencoded,
+ test_webkit_urlencoded,
+
+ setup_urlencoded_get,
+ test_webkit_urlencoded,
+
+ setup_urlencoded_empty,
+ test_webkit_urlencoded,
+
+ setup_formData,
+ test_webkit_multipart,
+
+ delete_entries,
+];
+
+function next() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/filesystem/compat/tests/test_no_dnd.html b/dom/filesystem/compat/tests/test_no_dnd.html
new file mode 100644
index 0000000000..c49dd5d40f
--- /dev/null
+++ b/dom/filesystem/compat/tests/test_no_dnd.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Blink FileSystem API - no DND == no webkitEntries</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<script type="application/javascript">
+var fileEntry;
+var directoryEntry;
+var script;
+var entries;
+
+function setup_tests() {
+ SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true],
+ ["dom.filesystem.pathcheck.disabled", true],
+ ["dom.webkitBlink.filesystem.enabled", true]]}, next);
+}
+
+function populate_entries() {
+ entries = document.createElement("input");
+ entries.setAttribute("type", "file");
+ document.body.appendChild(entries);
+
+ var url = SimpleTest.getTestFileURL("script_entries.js");
+ script = SpecialPowers.loadChromeScript(url);
+
+ function onOpened(message) {
+ for (var i = 0 ; i < message.data.length; ++i) {
+ if (message.data[i] instanceof File) {
+ SpecialPowers.wrap(entries).mozSetFileArray([message.data[i]]);
+ next();
+ }
+ }
+ }
+
+ script.addMessageListener("entries.opened", onOpened);
+ script.sendAsyncMessage("entries.open");
+}
+
+function test_entries() {
+ ok("webkitEntries" in entries, "HTMLInputElement.webkitEntries");
+ is(entries.webkitEntries.length, 0, "HTMLInputElement.webkitEntries.length == 0");
+ is(entries.files.length, 1, "HTMLInputElement.files is still populated");
+
+ next();
+}
+
+function cleanUpTestingFiles() {
+ script.addMessageListener("entries.deleted", function onDeleted() {
+ script.removeMessageListener("entries.deleted");
+ script.destroy();
+ next();
+ });
+
+ script.sendAsyncMessage("entries.delete");
+}
+
+var tests = [
+ setup_tests,
+ populate_entries,
+
+ test_entries,
+
+ cleanUpTestingFiles,
+];
+
+function next() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+SimpleTest.waitForExplicitFinish();
+next();
+</script>
+</body>
+</html>
diff --git a/dom/filesystem/moz.build b/dom/filesystem/moz.build
new file mode 100644
index 0000000000..8287f85777
--- /dev/null
+++ b/dom/filesystem/moz.build
@@ -0,0 +1,50 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: File")
+
+DIRS += ["compat"]
+
+TEST_DIRS += ["tests"]
+
+EXPORTS.mozilla.dom += [
+ "Directory.h",
+ "FileSystemBase.h",
+ "FileSystemRequestParent.h",
+ "FileSystemSecurity.h",
+ "FileSystemTaskBase.h",
+ "FileSystemUtils.h",
+ "GetFilesHelper.h",
+ "OSFileSystem.h",
+]
+
+UNIFIED_SOURCES += [
+ "Directory.cpp",
+ "FileSystemBase.cpp",
+ "FileSystemRequestParent.cpp",
+ "FileSystemSecurity.cpp",
+ "FileSystemTaskBase.cpp",
+ "FileSystemUtils.cpp",
+ "GetDirectoryListingTask.cpp",
+ "GetFileOrDirectoryTask.cpp",
+ "GetFilesHelper.cpp",
+ "GetFilesTask.cpp",
+ "OSFileSystem.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+IPDL_SOURCES += [
+ "PFileSystemParams.ipdlh",
+ "PFileSystemRequest.ipdl",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+]
diff --git a/dom/filesystem/tests/filesystem_commons.js b/dom/filesystem/tests/filesystem_commons.js
new file mode 100644
index 0000000000..af0ce36339
--- /dev/null
+++ b/dom/filesystem/tests/filesystem_commons.js
@@ -0,0 +1,180 @@
+function createPath(parentDir, dirOrFile) {
+ return parentDir.path + (parentDir.path == "/" ? "" : "/") + dirOrFile.name;
+}
+
+function createRelativePath(parentDir, dirOrFile) {
+ let path = createPath(parentDir, dirOrFile);
+ is(path[0], "/", "The full path should start with '/'");
+ return path.substring(1);
+}
+
+function setup_tests(aNext) {
+ SimpleTest.requestLongerTimeout(2);
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.filesystem.pathcheck.disabled", true],
+ ["dom.webkitBlink.dirPicker.enabled", true],
+ ],
+ },
+ aNext
+ );
+}
+
+function test_basic(aDirectory, aNext) {
+ ok(aDirectory, "Directory exists.");
+ ok(aDirectory instanceof Directory, "We have a directory.");
+ is(aDirectory.path, "/" + aDirectory.name, "directory.path must be '/'+name");
+ aNext();
+}
+
+function test_getFilesAndDirectories(aDirectory, aRecursive, aNext) {
+ function checkSubDir(dir) {
+ return dir.getFilesAndDirectories().then(function (data) {
+ for (var i = 0; i < data.length; ++i) {
+ ok(
+ data[i] instanceof File || data[i] instanceof Directory,
+ "Just Files or Directories"
+ );
+ if (data[i] instanceof Directory) {
+ isnot(
+ data[i].name,
+ "/",
+ "Subdirectory should be called with the leafname"
+ );
+ isnot(
+ data[i].path,
+ "/",
+ "Subdirectory path should be called with the leafname"
+ );
+ isnot(
+ data[i].path,
+ dir.path,
+ "Subdirectory path should contain the parent path."
+ );
+ is(
+ data[i].path,
+ createPath(dir, data[i]),
+ "Subdirectory path should be called parentdir.path + '/' + leafname: " +
+ data[i].path
+ );
+ }
+
+ if (data[i] instanceof File) {
+ is(
+ data[i].webkitRelativePath,
+ createRelativePath(dir, data[i]),
+ "File.webkitRelativePath should be called: parentdir.path + '/' + file.name: " +
+ data[i].webkitRelativePath
+ );
+ ok(
+ !data[i].webkitRelativePath.endsWith("symlink.txt"),
+ "We should never see a path ending with symlink.txt, our symlink sentinel."
+ );
+ }
+ }
+ });
+ }
+
+ aDirectory
+ .getFilesAndDirectories()
+ .then(
+ function (data) {
+ ok(data.length, "We should have some data.");
+ var promises = [];
+ for (var i = 0; i < data.length; ++i) {
+ ok(
+ data[i] instanceof File || data[i] instanceof Directory,
+ "Just Files or Directories: " + data[i].name
+ );
+ if (data[i] instanceof Directory) {
+ isnot(
+ data[i].name,
+ "/",
+ "Subdirectory should be called with the leafname"
+ );
+ is(
+ data[i].path,
+ createPath(aDirectory, data[i]),
+ "Subdirectory path should be called parentdir.path + '/' + leafname: " +
+ data[i].path
+ );
+ if (aRecursive) {
+ promises.push(checkSubDir(data[i]));
+ }
+ }
+
+ if (data[i] instanceof File) {
+ is(
+ data[i].webkitRelativePath,
+ createRelativePath(aDirectory, data[i]),
+ "File.webkitRelativePath should be called file.name: " +
+ data[i].webkitRelativePath
+ );
+ }
+ }
+
+ return Promise.all(promises);
+ },
+ function () {
+ ok(false, "Something when wrong");
+ }
+ )
+ .then(aNext);
+}
+
+function test_getFiles(aDirectory, aRecursive, aNext) {
+ aDirectory
+ .getFiles(aRecursive)
+ .then(
+ function (data) {
+ for (var i = 0; i < data.length; ++i) {
+ ok(data[i] instanceof File, "File: " + data[i].name);
+ is(aDirectory.path[0], "/", "Directory path must start with '/'");
+ ok(
+ data[i].webkitRelativePath.indexOf(aDirectory.path.substring(1)) ==
+ 0 &&
+ data[i].webkitRelativePath.indexOf("/" + data[i].name) +
+ ("/" + data[i].name).length ==
+ data[i].webkitRelativePath.length,
+ "File.webkitRelativePath should be called dir.path + '/' + file.name: " +
+ data[i].webkitRelativePath
+ );
+ }
+ },
+ function () {
+ ok(false, "Something when wrong");
+ }
+ )
+ .then(aNext);
+}
+
+function test_getFiles_recursiveComparison(aDirectory, aNext) {
+ aDirectory
+ .getFiles(true)
+ .then(function (data) {
+ is(data.length, 2, "Only 2 files for this test.");
+ ok(
+ data[0].name == "foo.txt" || data[0].name == "bar.txt",
+ "First filename matches"
+ );
+ ok(
+ data[1].name == "foo.txt" || data[1].name == "bar.txt",
+ "Second filename matches"
+ );
+ })
+ .then(function () {
+ return aDirectory.getFiles(false);
+ })
+ .then(function (data) {
+ is(data.length, 1, "Only 1 file for this test.");
+ ok(
+ data[0].name == "foo.txt" || data[0].name == "bar.txt",
+ "First filename matches"
+ );
+ })
+ .catch(function () {
+ ok(false, "Something when wrong");
+ })
+ .then(aNext);
+}
diff --git a/dom/filesystem/tests/mochitest.ini b/dom/filesystem/tests/mochitest.ini
new file mode 100644
index 0000000000..00326a6e0d
--- /dev/null
+++ b/dom/filesystem/tests/mochitest.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files =
+ filesystem_commons.js
+ script_fileList.js
+ worker_basic.js
+
+[test_basic.html]
+[test_webkitdirectory.html]
+skip-if = os == "android" # Bug 1674428
+support-files = script_promptHandler.js
+[test_worker_basic.html]
+[test_bug1319088.html]
diff --git a/dom/filesystem/tests/moz.build b/dom/filesystem/tests/moz.build
new file mode 100644
index 0000000000..7c990fbc62
--- /dev/null
+++ b/dom/filesystem/tests/moz.build
@@ -0,0 +1,7 @@
+# -*- 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"]
diff --git a/dom/filesystem/tests/script_fileList.js b/dom/filesystem/tests/script_fileList.js
new file mode 100644
index 0000000000..47438aa7b3
--- /dev/null
+++ b/dom/filesystem/tests/script_fileList.js
@@ -0,0 +1,176 @@
+/* eslint-env mozilla/chrome-script */
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+Cu.importGlobalProperties(["File"]);
+function createProfDFile() {
+ return Services.dirsvc
+ .QueryInterface(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+}
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+// Creates a parametric arity directory hierarchy as a function of depth.
+// Each directory contains one leaf file, and subdirectories of depth [1, depth).
+// e.g. for depth 3:
+//
+// subdir3
+// - file.txt
+// - subdir2
+// - file.txt
+// - subdir1
+// - file.txt
+// - subdir1
+// - file.txt
+//
+// Returns the parent directory of the subtree.
+function createTreeFile(depth, parent) {
+ if (!parent) {
+ parent = Services.dirsvc
+ .QueryInterface(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+ parent.append("dir-tree-test");
+ parent.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
+ }
+
+ var nextFile = parent.clone();
+ if (depth == 0) {
+ nextFile.append("file.txt");
+ nextFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ // It's not possible to create symlinks on windows by default or on our
+ // Android platforms, so we can't create the symlink file there. Our
+ // callers that care are aware of this and also check AppConstants.
+ if (
+ AppConstants.platform !== "win" &&
+ AppConstants.platform !== "android"
+ ) {
+ var linkFile = parent.clone();
+ linkFile.append("symlink.txt");
+ createSymLink(nextFile.path, linkFile.path);
+ }
+ } else {
+ nextFile.append("subdir" + depth);
+ nextFile.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
+ // Decrement the maximal depth by one for each level of nesting.
+ for (var i = 0; i < depth; i++) {
+ createTreeFile(i, nextFile);
+ }
+ }
+
+ return parent;
+}
+
+function createRootFile() {
+ var testFile = createProfDFile();
+
+ // Let's go back to the root of the FileSystem
+ while (true) {
+ var parent = testFile.parent;
+ if (!parent) {
+ break;
+ }
+
+ testFile = parent;
+ }
+
+ return testFile;
+}
+
+var process;
+function createSymLink(target, linkName) {
+ if (!process) {
+ var ln = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ ln.initWithPath("/bin/ln");
+
+ process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(ln);
+ }
+
+ const args = ["-s", target, linkName];
+ process.run(true, args, args.length);
+ Assert.equal(process.exitValue, 0);
+}
+
+function createTestFile() {
+ var tmpFile = Services.dirsvc
+ .QueryInterface(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+ tmpFile.append("dir-test");
+ tmpFile.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
+
+ var file1 = tmpFile.clone();
+ file1.append("foo.txt");
+ file1.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ var dir = tmpFile.clone();
+ dir.append("subdir");
+ dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
+
+ var file2 = dir.clone();
+ file2.append("bar.txt");
+ file2.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ // It's not possible to create symlinks on windows by default or on our
+ // Android platforms, so we can't create the symlink file there. Our
+ // callers that care are aware of this and also check AppConstants.
+ if (AppConstants.platform !== "win" && AppConstants.platform !== "android") {
+ var linkFile = dir.clone();
+ linkFile.append("symlink.txt");
+ createSymLink(file1.path, linkFile.path);
+ }
+
+ return tmpFile;
+}
+
+addMessageListener("dir.open", function (e) {
+ var testFile;
+
+ switch (e.path) {
+ case "ProfD":
+ // Note that files in the profile directory are not guaranteed to persist-
+ // see bug 1284742.
+ testFile = createProfDFile();
+ break;
+
+ case "root":
+ testFile = createRootFile();
+ break;
+
+ case "test":
+ testFile = createTestFile();
+ break;
+
+ case "tree":
+ testFile = createTreeFile(3);
+ break;
+ }
+
+ sendAsyncMessage("dir.opened", {
+ dir: testFile.path,
+ name: testFile.leafName,
+ });
+});
+
+addMessageListener("file.open", function (e) {
+ var testFile = Services.dirsvc
+ .QueryInterface(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ testFile.append("prefs.js");
+
+ File.createFromNsIFile(testFile).then(function (file) {
+ sendAsyncMessage("file.opened", { file });
+ });
+});
+
+addMessageListener("symlink.open", function (e) {
+ let testDir = createTestFile();
+ let testFile = testDir.clone();
+ testFile.append("subdir");
+ testFile.append("symlink.txt");
+
+ File.createFromNsIFile(testFile).then(function (file) {
+ sendAsyncMessage("symlink.opened", { dir: testDir.path, file });
+ });
+});
diff --git a/dom/filesystem/tests/script_promptHandler.js b/dom/filesystem/tests/script_promptHandler.js
new file mode 100644
index 0000000000..ed3bda7404
--- /dev/null
+++ b/dom/filesystem/tests/script_promptHandler.js
@@ -0,0 +1,18 @@
+/* eslint-env mozilla/chrome-script */
+
+let dialogObserverTopic = "common-dialog-loaded";
+
+function dialogObserver(subj, topic, data) {
+ subj.document.querySelector("dialog").acceptDialog();
+ sendAsyncMessage("promptAccepted");
+}
+
+addMessageListener("init", message => {
+ Services.obs.addObserver(dialogObserver, dialogObserverTopic);
+ sendAsyncMessage("initDone");
+});
+
+addMessageListener("cleanup", message => {
+ Services.obs.removeObserver(dialogObserver, dialogObserverTopic);
+ sendAsyncMessage("cleanupDone");
+});
diff --git a/dom/filesystem/tests/test_basic.html b/dom/filesystem/tests/test_basic.html
new file mode 100644
index 0000000000..b4daea79c9
--- /dev/null
+++ b/dom/filesystem/tests/test_basic.html
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Directory API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="filesystem_commons.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<script type="application/javascript">
+
+var directory;
+var fileList;
+
+function create_fileList(aPath) {
+ fileList = document.createElement("input");
+ fileList.setAttribute("type", "file");
+ document.body.appendChild(fileList);
+
+ var url = SimpleTest.getTestFileURL("script_fileList.js");
+ var script = SpecialPowers.loadChromeScript(url);
+
+ function onOpened(message) {
+ SpecialPowers.wrap(fileList).mozSetDirectory(message.dir);
+ fileList.setAttribute("data-name", message.name);
+
+ SpecialPowers.wrap(fileList).getFilesAndDirectories().then(function(array) {
+ array = SpecialPowers.unwrap(array);
+ is(array.length, 1, "We want just 1 directory.");
+ ok(array[0] instanceof Directory, "We want just 1 directory.");
+
+ directory = array[0];
+ script.destroy();
+ next();
+ });
+ }
+
+ script.addMessageListener("dir.opened", onOpened);
+ script.sendAsyncMessage("dir.open", { path: aPath });
+}
+
+function test_simpleFilePicker(aPath) {
+ var url = SimpleTest.getTestFileURL("script_fileList.js");
+ var script = SpecialPowers.loadChromeScript(url);
+
+ function onOpened(message) {
+ SpecialPowers.wrap(fileList).mozSetFileArray([message.file]);
+
+ is(fileList.files.length, 1, "we want 1 element");
+ ok(fileList.files[0] instanceof File, "we want 1 file");
+ ok("webkitRelativePath" in fileList.files[0], "we have webkitRelativePath attribute");
+ is(fileList.files[0].webkitRelativePath, "", "No webkit relative path for normal filePicker");
+
+ script.destroy();
+ next();
+ }
+
+ script.addMessageListener("file.opened", onOpened);
+ script.sendAsyncMessage("file.open");
+}
+
+function test_duplicateGetFilesAndDirectories() {
+ var url = SimpleTest.getTestFileURL("script_fileList.js");
+ var script = SpecialPowers.loadChromeScript(url);
+
+ function onOpened(message) {
+ SpecialPowers.wrap(fileList).mozSetDirectory(message.dir);
+
+ var p1 = SpecialPowers.wrap(fileList).getFilesAndDirectories();
+ var p2 = SpecialPowers.wrap(fileList).getFilesAndDirectories();
+
+ isnot(p1, p2, "We create 2 different promises");
+
+ script.destroy();
+ next();
+ }
+
+ script.addMessageListener("dir.opened", onOpened);
+ script.sendAsyncMessage("dir.open", { path: "test" });
+}
+
+var tests = [
+ function() { setup_tests(next); },
+
+ function() { create_fileList("tree"); },
+ function() { test_basic(directory, next); },
+ function() { test_getFilesAndDirectories(directory, true, next); },
+ function() { test_getFiles(directory, false, next); },
+ function() { test_getFiles(directory, true, next); },
+
+ function() { create_fileList("test"); },
+ function() { test_getFiles_recursiveComparison(directory, next); },
+
+ function() { create_fileList("root"); },
+ function() { test_basic(directory, next); },
+ function() { test_getFilesAndDirectories(directory, false, next); },
+ function() { test_getFiles(directory, false, next); },
+
+ test_duplicateGetFilesAndDirectories,
+ test_simpleFilePicker,
+];
+
+function next() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+SimpleTest.waitForExplicitFinish();
+next();
+</script>
+</body>
+</html>
diff --git a/dom/filesystem/tests/test_bug1319088.html b/dom/filesystem/tests/test_bug1319088.html
new file mode 100644
index 0000000000..98e0ad46f0
--- /dev/null
+++ b/dom/filesystem/tests/test_bug1319088.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for bug 1319088</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<input id="input" type="file"></input>
+
+<script type="application/javascript">
+
+function testSetup() {
+ SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true]]}, next);
+}
+
+function populateInputFile() {
+ var url = SimpleTest.getTestFileURL("script_fileList.js");
+ var script = SpecialPowers.loadChromeScript(url);
+
+ function onOpened(message) {
+ var input = document.getElementById("input");
+ SpecialPowers.wrap(input).mozSetFileArray([message.file]);
+
+ script.destroy();
+ next();
+ }
+
+ script.addMessageListener("file.opened", onOpened);
+ script.sendAsyncMessage("file.open");
+}
+
+function checkBug() {
+ var input = document.getElementById("input");
+ is(input.files[0].webkitRelativePath, "", "No relative path!");
+
+ let form = document.createElement("form");
+ form.appendChild(input);
+
+ is(input.files[0].webkitRelativePath, "", "No relative path!");
+ SimpleTest.finish();
+}
+
+var tests = [
+ testSetup,
+ populateInputFile,
+ checkBug,
+];
+
+function next() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+SimpleTest.waitForExplicitFinish();
+next();
+</script>
+</body>
+</html>
diff --git a/dom/filesystem/tests/test_webkitdirectory.html b/dom/filesystem/tests/test_webkitdirectory.html
new file mode 100644
index 0000000000..50080ad7ba
--- /dev/null
+++ b/dom/filesystem/tests/test_webkitdirectory.html
@@ -0,0 +1,309 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for webkitdirectory and webkitRelativePath</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<input id="inputFileWebkitDirectory" type="file" webkitdirectory></input>
+<input id="inputFileWebkitFile" type="file"></input>
+<input id="inputFileDirectoryChange" type="file" webkitdirectory></input>
+
+<script type="application/javascript">
+
+const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+let promptHandler;
+
+function waitForEvent(element, eventName) {
+ return new Promise(function(resolve) {
+ element.addEventListener(eventName, e => resolve(e.detail), { once: true });
+ });
+}
+
+function waitForPromptHandled() {
+ return new Promise(resolve => promptHandler.addMessageListener("promptAccepted", resolve));
+}
+
+// Populate the given input type=file `aInputFile`'s `files` attribute by:
+// - loading `script_fileList.js` in the parent process
+// - telling it to generate the "test" template directory pattern which will
+// create "foo.txt", "subdir/bar.txt", and if symlinks are available on the
+// platform, "symlink.txt" which will be a symlink to "foo.txt". (Note that
+// we explicitly expect the symlink to be filtered out if generated, and
+// during the enhancement of the test we verified the file was created on
+// linux by running the test before fixing the GetFilesHelper logic to filter
+// the symlink out and verifying the subsequent `test_fileList` check failed.)
+// - Triggering the mock file picker with the base directory of the "test"
+// template directory.
+//
+// It's expected that `test_fileList` will be used after this step completes in
+// order to validate the results.
+function populateInputFile(aInputFile) {
+ var url = SimpleTest.getTestFileURL("script_fileList.js");
+ var script = SpecialPowers.loadChromeScript(url);
+
+ var MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeGetFolder);
+
+ async function onOpened(message) {
+ MockFilePicker.useDirectory(message.dir);
+
+ let input = document.getElementById(aInputFile);
+ input.setAttribute("data-name", message.name);
+
+ let promptHandled = waitForPromptHandled();
+ let changeEvent = waitForEvent(input, "change");
+
+ input.click();
+
+ await promptHandled;
+ await changeEvent;
+
+ MockFilePicker.cleanup();
+ script.destroy();
+ next();
+ }
+
+ script.addMessageListener("dir.opened", onOpened);
+ script.sendAsyncMessage("dir.open", { path: "test" });
+}
+
+function checkFile(file, fileList, dirName) {
+ for (var i = 0; i < fileList.length; ++i) {
+ ok(fileList[i] instanceof File, "We want just files.");
+ if (fileList[i].name == file.name) {
+ is(fileList[i].webkitRelativePath, dirName + file.path, "Path matches");
+ return;
+ }
+ }
+
+ ok(false, "File not found.");
+}
+
+// Validate the contents of the given input type=file `aInputFile`'s' `files`
+// property against the expected list of files `aWhat`.
+function test_fileList(aInputFile, aWhat) {
+ var input = document.getElementById(aInputFile);
+ var fileList = input.files;
+
+ if (aWhat == null) {
+ is(fileList, null, "We want a null fileList for " + aInputFile);
+ next();
+ return;
+ }
+
+ is(fileList.length, aWhat.length, "We want just " + aWhat.length + " elements for " + aInputFile);
+ for (var i = 0; i < aWhat.length; ++i) {
+ checkFile(aWhat[i], fileList, input.dataset.name);
+ }
+
+ next();
+}
+
+// Verify that we can explicitly select a symlink and it will not be filtered
+// out. This is really a verification that GetFileHelper's file-handling logic
+// https://searchfox.org/mozilla-central/rev/065102493dfc49234120c37fc6a334a5b1d86d9e/dom/filesystem/GetFilesHelper.cpp#81-86
+// does not proactively take an action to filter out a selected symlink.
+//
+// This is a glass box test that is not entirely realistic for our actual system
+// file pickers but does reflect what will happen in the drag-and-drop case
+// for `HTMLInputElement::MozSetDndFilesAndDirectories` and this helps ensure
+// that future implementation changes will behave as expected. Specifically,
+// the presence of webkitdirectory will result in the file picker using
+// `modeGetFolder` which will only allow selection of a directory and forbid
+// file selection.
+//
+// This test explicitly does not validate HTMLInputElement's non-webkitdirectory
+// file selection mechanism because it does not involve GetFileHelper.
+async function test_individualSymlink(aInputFile) {
+ const input = document.getElementById(aInputFile);
+
+ // -- Create the symlink and get a `File` instance pointing at it.
+ const url = SimpleTest.getTestFileURL("script_fileList.js");
+ const script = SpecialPowers.loadChromeScript(url);
+
+ let opened = new Promise(resolve => script.addMessageListener("symlink.opened", resolve));
+ script.sendAsyncMessage("symlink.open", {});
+ let { dir, file: symlinkFile } = await opened;
+ info(`symlink.open provided dir: ${dir}`)
+
+ // -- Have the picker pick it
+ var MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeOpen);
+
+ MockFilePicker.displayDirectory = dir;
+ let pickerShown = new Promise(resolve => {
+ MockFilePicker.showCallback = function() {
+ // This is where we are diverging from a realistic scenario in order to get
+ // the expected coverage.
+ MockFilePicker.setFiles([symlinkFile]);
+ resolve();
+ }
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ let changeEvent = waitForEvent(input, "change");
+
+ input.click();
+
+ await pickerShown;
+ await changeEvent;
+
+ MockFilePicker.cleanup();
+ script.destroy();
+
+ // -- Verify that we see the symlink.
+ let fileList = input.files;
+ is(fileList.length, 1, "There should be 1 file.");
+ is(fileList[0].name, "symlink.txt", "The file should be the symlink.");
+ next();
+}
+
+function test_webkitdirectory_attribute() {
+ var a = document.createElement("input");
+ a.setAttribute("type", "file");
+
+ ok("webkitdirectory" in a, "HTMLInputElement.webkitdirectory exists");
+
+ ok(!a.hasAttribute("webkitdirectory"), "No webkitdirectory DOM attribute by default");
+ ok(!a.webkitdirectory, "No webkitdirectory attribute by default");
+
+ a.webkitdirectory = true;
+
+ ok(a.hasAttribute("webkitdirectory"), "Webkitdirectory DOM attribute is set");
+ ok(a.webkitdirectory, "Webkitdirectory attribute is set");
+
+ next();
+}
+
+function test_changeDataWhileWorking() {
+ var url = SimpleTest.getTestFileURL("script_fileList.js");
+ var script = SpecialPowers.loadChromeScript(url);
+
+ var MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeGetFolder);
+ let promptHandled;
+
+ // Let's start retrieving the root nsIFile object
+ new Promise(function(resolve) {
+ function onOpened(message) {
+ script.removeMessageListener("dir.opened", onOpened);
+ resolve(message.dir);
+ }
+
+ script.addMessageListener("dir.opened", onOpened);
+ script.sendAsyncMessage("dir.open", { path: "root" });
+ })
+
+ // input.click() pointing to the root dir
+ .then(async function(aDir) {
+ MockFilePicker.cleanup();
+ MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeGetFolder);
+ MockFilePicker.useDirectory(aDir);
+ var input = document.getElementById("inputFileDirectoryChange");
+
+ promptHandled = waitForPromptHandled();
+ input.click();
+ })
+
+ // Before onchange, let's take the 'test' directory
+ .then(function() {
+ return new Promise(function(resolve) {
+ function onOpened(message) {
+ script.removeMessageListener("dir.opened", onOpened);
+ script.destroy();
+ resolve(message.dir);
+ }
+
+ script.addMessageListener("dir.opened", onOpened);
+ script.sendAsyncMessage("dir.open", { path: "test" });
+ });
+ })
+
+ // Now let's click again and wait for onchange.
+ .then(async function(aDir) {
+ MockFilePicker.cleanup();
+ MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeGetFolder);
+ MockFilePicker.useDirectory(aDir);
+
+ let input = document.getElementById("inputFileDirectoryChange");
+ let changeEvent = waitForEvent(input, "change");
+
+ input.click();
+
+ await promptHandled;
+ await changeEvent;
+
+ MockFilePicker.cleanup();
+ })
+ .then(function() {
+ test_fileList("inputFileWebkitDirectory", testDirData);
+ });
+}
+
+async function test_setup() {
+ let promptHandlerUrl = SimpleTest.getTestFileURL("script_promptHandler.js")
+ promptHandler = SpecialPowers.loadChromeScript(promptHandlerUrl);
+
+ let promptHandlerReady = new Promise(resolve => promptHandler.addMessageListener("initDone", resolve));
+ promptHandler.sendAsyncMessage("init");
+ await promptHandlerReady;
+
+ SpecialPowers.pushPrefEnv({"set": [["dom.filesystem.pathcheck.disabled", true],
+ ["dom.webkitBlink.dirPicker.enabled", true]]}, next);
+}
+
+async function test_cleanup() {
+ let promptHandlerDone = new Promise(resolve => promptHandler.addMessageListener("cleanupDone", resolve));
+ promptHandler.sendAsyncMessage("cleanup");
+ await promptHandlerDone;
+ promptHandler.destroy();
+}
+
+var testDirData = [ { name: "foo.txt", path: "/foo.txt" },
+ { name: "bar.txt", path: "/subdir/bar.txt" }];
+
+var tests = [
+ test_setup,
+
+ function() { populateInputFile("inputFileWebkitDirectory"); },
+
+ function() { test_fileList("inputFileWebkitDirectory", testDirData); },
+
+ function() {
+ // Symlinks are not available on Windows and so will not be created.
+ if (AppConstants.platform === "win" || AppConstants.platform === "android") {
+ info("Skipping individual symlink check on Windows and Android.");
+ next();
+ return;
+ }
+
+ test_individualSymlink("inputFileWebkitFile").catch(err => ok(false, `Problem in symlink case: ${err}`));
+ },
+
+ test_webkitdirectory_attribute,
+
+ test_changeDataWhileWorking,
+];
+
+async function next() {
+ if (!tests.length) {
+ await test_cleanup();
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ await test();
+}
+
+SimpleTest.waitForExplicitFinish();
+next();
+</script>
+</body>
+</html>
diff --git a/dom/filesystem/tests/test_worker_basic.html b/dom/filesystem/tests/test_worker_basic.html
new file mode 100644
index 0000000000..920f32719b
--- /dev/null
+++ b/dom/filesystem/tests/test_worker_basic.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Directory API in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="filesystem_commons.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<script type="application/javascript">
+
+var fileList;
+
+function create_fileList() {
+ fileList = document.createElement("input");
+ fileList.setAttribute("type", "file");
+ document.body.appendChild(fileList);
+
+ var url = SimpleTest.getTestFileURL("script_fileList.js");
+ var script = SpecialPowers.loadChromeScript(url);
+
+ function onOpened(message) {
+ SpecialPowers.wrap(fileList).mozSetDirectory(message.dir);
+ script.destroy();
+ next();
+ }
+
+ script.addMessageListener("dir.opened", onOpened);
+ script.sendAsyncMessage("dir.open", { path: "test" });
+}
+
+function test_worker() {
+ SpecialPowers.wrap(fileList).getFilesAndDirectories().then(function(array) {
+ array = SpecialPowers.unwrap(array);
+ var worker = new Worker("worker_basic.js");
+ worker.onmessage = function(e) {
+ if (e.data.type == "finish") {
+ next();
+ return;
+ }
+
+ if (e.data.type == "test") {
+ ok(e.data.test, e.data.message);
+ }
+ };
+
+ worker.postMessage(array[0]);
+ });
+}
+
+var tests = [
+ function() { setup_tests(next); },
+
+ create_fileList,
+ test_worker,
+];
+
+function next() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+SimpleTest.waitForExplicitFinish();
+next();
+</script>
+</body>
+</html>
diff --git a/dom/filesystem/tests/worker_basic.js b/dom/filesystem/tests/worker_basic.js
new file mode 100644
index 0000000000..2771c778fd
--- /dev/null
+++ b/dom/filesystem/tests/worker_basic.js
@@ -0,0 +1,50 @@
+/* eslint-env worker */
+importScripts("filesystem_commons.js");
+
+function finish() {
+ postMessage({ type: "finish" });
+}
+
+function ok(a, msg) {
+ postMessage({ type: "test", test: !!a, message: msg });
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+function isnot(a, b, msg) {
+ ok(a != b, msg);
+}
+
+var tests = [
+ function () {
+ test_basic(directory, next);
+ },
+ function () {
+ test_getFilesAndDirectories(directory, true, next);
+ },
+ function () {
+ test_getFiles(directory, false, next);
+ },
+ function () {
+ test_getFiles(directory, true, next);
+ },
+];
+
+function next() {
+ if (!tests.length) {
+ finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+var directory;
+
+onmessage = function (e) {
+ directory = e.data;
+ next();
+};