From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- dom/file/BaseBlobImpl.cpp | 75 + dom/file/BaseBlobImpl.h | 158 +++ dom/file/Blob.cpp | 338 +++++ dom/file/Blob.h | 161 +++ dom/file/BlobImpl.cpp | 99 ++ dom/file/BlobImpl.h | 114 ++ dom/file/BlobSet.cpp | 83 ++ dom/file/BlobSet.h | 36 + dom/file/EmptyBlobImpl.cpp | 34 + dom/file/EmptyBlobImpl.h | 41 + dom/file/File.cpp | 202 +++ dom/file/File.h | 103 ++ dom/file/FileBlobImpl.cpp | 294 ++++ dom/file/FileBlobImpl.h | 157 +++ dom/file/FileCreatorHelper.cpp | 67 + dom/file/FileCreatorHelper.h | 43 + dom/file/FileList.cpp | 94 ++ dom/file/FileList.h | 68 + dom/file/FileReader.cpp | 803 +++++++++++ dom/file/FileReader.h | 207 +++ dom/file/FileReaderSync.cpp | 489 +++++++ dom/file/FileReaderSync.h | 60 + dom/file/MemoryBlobImpl.cpp | 168 +++ dom/file/MemoryBlobImpl.h | 160 +++ dom/file/MultipartBlobImpl.cpp | 338 +++++ dom/file/MultipartBlobImpl.h | 106 ++ dom/file/MutableBlobStorage.cpp | 667 +++++++++ dom/file/MutableBlobStorage.h | 136 ++ dom/file/MutableBlobStreamListener.cpp | 102 ++ dom/file/MutableBlobStreamListener.h | 50 + dom/file/StreamBlobImpl.cpp | 224 +++ dom/file/StreamBlobImpl.h | 95 ++ dom/file/StringBlobImpl.cpp | 51 + dom/file/StringBlobImpl.h | 53 + dom/file/TemporaryFileBlobImpl.cpp | 126 ++ dom/file/TemporaryFileBlobImpl.h | 47 + dom/file/ipc/FileCreatorChild.cpp | 59 + dom/file/ipc/FileCreatorChild.h | 33 + dom/file/ipc/FileCreatorParent.cpp | 135 ++ dom/file/ipc/FileCreatorParent.h | 45 + dom/file/ipc/IPCBlob.ipdlh | 56 + dom/file/ipc/IPCBlobUtils.cpp | 179 +++ dom/file/ipc/IPCBlobUtils.h | 268 ++++ dom/file/ipc/PFileCreator.ipdl | 38 + dom/file/ipc/PRemoteLazyInputStream.ipdl | 21 + dom/file/ipc/PTemporaryIPCBlob.ipdl | 40 + dom/file/ipc/RemoteLazyInputStream.cpp | 1458 ++++++++++++++++++++ dom/file/ipc/RemoteLazyInputStream.h | 155 +++ dom/file/ipc/RemoteLazyInputStreamChild.cpp | 55 + dom/file/ipc/RemoteLazyInputStreamChild.h | 41 + dom/file/ipc/RemoteLazyInputStreamParent.cpp | 123 ++ dom/file/ipc/RemoteLazyInputStreamParent.h | 44 + dom/file/ipc/RemoteLazyInputStreamStorage.cpp | 243 ++++ dom/file/ipc/RemoteLazyInputStreamStorage.h | 78 ++ dom/file/ipc/RemoteLazyInputStreamThread.cpp | 237 ++++ dom/file/ipc/RemoteLazyInputStreamThread.h | 52 + dom/file/ipc/TemporaryIPCBlobChild.cpp | 86 ++ dom/file/ipc/TemporaryIPCBlobChild.h | 53 + dom/file/ipc/TemporaryIPCBlobParent.cpp | 102 ++ dom/file/ipc/TemporaryIPCBlobParent.h | 43 + dom/file/ipc/moz.build | 69 + dom/file/ipc/mozIRemoteLazyInputStream.idl | 29 + dom/file/ipc/tests/browser.ini | 7 + dom/file/ipc/tests/browser_ipcBlob.js | 253 ++++ dom/file/ipc/tests/browser_ipcBlob_temporary.js | 115 ++ dom/file/ipc/tests/empty.html | 0 dom/file/ipc/tests/green.jpg | Bin 0 -> 361 bytes dom/file/ipc/tests/mochitest.ini | 12 + dom/file/ipc/tests/ok.sjs | 11 + dom/file/ipc/tests/script_file.js | 53 + dom/file/ipc/tests/temporary.sjs | 6 + .../ipc/tests/test_ipcBlob_createImageBitmap.html | 84 ++ .../ipc/tests/test_ipcBlob_emptyMultiplex.html | 45 + .../ipc/tests/test_ipcBlob_fileReaderSync.html | 100 ++ .../ipc/tests/test_ipcBlob_mixedMultiplex.html | 41 + dom/file/ipc/tests/test_ipcBlob_workers.html | 121 ++ dom/file/moz.build | 62 + dom/file/tests/common_blob.js | 395 ++++++ dom/file/tests/common_blob_reading.js | 50 + dom/file/tests/common_blob_types.js | 82 ++ dom/file/tests/common_fileReader.js | 848 ++++++++++++ dom/file/tests/crashtests/1480354.html | 14 + dom/file/tests/crashtests/1562891.html | 16 + dom/file/tests/crashtests/1747185.html | 11 + dom/file/tests/crashtests/1748342.html | 26 + dom/file/tests/crashtests/crashtests.list | 4 + dom/file/tests/create_file_objects.js | 19 + dom/file/tests/file_blobURL_expiring.html | 4 + dom/file/tests/file_mozfiledataurl_audio.ogg | Bin 0 -> 135861 bytes dom/file/tests/file_mozfiledataurl_doc.html | 6 + dom/file/tests/file_mozfiledataurl_img.jpg | Bin 0 -> 2711 bytes dom/file/tests/file_mozfiledataurl_inner.html | 76 + dom/file/tests/file_mozfiledataurl_text.txt | 1 + dom/file/tests/file_nonascii_blob_url.html | 24 + dom/file/tests/fileapi_chromeScript.js | 54 + dom/file/tests/mochitest.ini | 54 + dom/file/tests/test_agentcluster_bloburl.js | 170 +++ dom/file/tests/test_blobURL_expiring.html | 48 + dom/file/tests/test_blob_fragment_and_query.html | 62 + dom/file/tests/test_blob_reading.html | 34 + dom/file/tests/test_blobconstructor.html | 246 ++++ dom/file/tests/test_bloburi.js | 24 + dom/file/tests/test_bug1507893.html | 63 + dom/file/tests/test_bug1742540.html | 83 ++ dom/file/tests/test_createFile.js | 52 + dom/file/tests/test_file_from_blob.html | 110 ++ dom/file/tests/test_file_negative_date.html | 29 + dom/file/tests/test_fileapi_basic.html | 24 + dom/file/tests/test_fileapi_basic_worker.html | 38 + dom/file/tests/test_fileapi_encoding.html | 24 + dom/file/tests/test_fileapi_encoding_worker.html | 38 + dom/file/tests/test_fileapi_other.html | 24 + dom/file/tests/test_fileapi_other_worker.html | 38 + dom/file/tests/test_fileapi_slice_image.html | 139 ++ dom/file/tests/test_fileapi_slice_memFile_1.html | 37 + dom/file/tests/test_fileapi_slice_memFile_2.html | 37 + dom/file/tests/test_fileapi_slice_realFile_1.html | 37 + dom/file/tests/test_fileapi_slice_realFile_2.html | 37 + dom/file/tests/test_fileapi_twice.html | 24 + dom/file/tests/test_fileapi_twice_worker.html | 38 + dom/file/tests/test_ipc_messagemanager_blob.js | 102 ++ dom/file/tests/test_mozfiledataurl.html | 224 +++ dom/file/tests/test_nonascii_blob_url.html | 28 + dom/file/tests/worker_blob_reading.js | 26 + dom/file/tests/worker_bug1507893.js | 5 + dom/file/tests/worker_bug1742540.js | 5 + dom/file/tests/worker_fileReader.js | 30 + dom/file/tests/xpcshell.ini | 8 + dom/file/uri/BlobURL.cpp | 171 +++ dom/file/uri/BlobURL.h | 123 ++ dom/file/uri/BlobURLChannel.cpp | 86 ++ dom/file/uri/BlobURLChannel.h | 37 + dom/file/uri/BlobURLInputStream.cpp | 589 ++++++++ dom/file/uri/BlobURLInputStream.h | 81 ++ dom/file/uri/BlobURLProtocolHandler.cpp | 990 +++++++++++++ dom/file/uri/BlobURLProtocolHandler.h | 140 ++ dom/file/uri/components.conf | 24 + dom/file/uri/moz.build | 34 + 138 files changed, 16544 insertions(+) create mode 100644 dom/file/BaseBlobImpl.cpp create mode 100644 dom/file/BaseBlobImpl.h create mode 100644 dom/file/Blob.cpp create mode 100644 dom/file/Blob.h create mode 100644 dom/file/BlobImpl.cpp create mode 100644 dom/file/BlobImpl.h create mode 100644 dom/file/BlobSet.cpp create mode 100644 dom/file/BlobSet.h create mode 100644 dom/file/EmptyBlobImpl.cpp create mode 100644 dom/file/EmptyBlobImpl.h create mode 100644 dom/file/File.cpp create mode 100644 dom/file/File.h create mode 100644 dom/file/FileBlobImpl.cpp create mode 100644 dom/file/FileBlobImpl.h create mode 100644 dom/file/FileCreatorHelper.cpp create mode 100644 dom/file/FileCreatorHelper.h create mode 100644 dom/file/FileList.cpp create mode 100644 dom/file/FileList.h create mode 100644 dom/file/FileReader.cpp create mode 100644 dom/file/FileReader.h create mode 100644 dom/file/FileReaderSync.cpp create mode 100644 dom/file/FileReaderSync.h create mode 100644 dom/file/MemoryBlobImpl.cpp create mode 100644 dom/file/MemoryBlobImpl.h create mode 100644 dom/file/MultipartBlobImpl.cpp create mode 100644 dom/file/MultipartBlobImpl.h create mode 100644 dom/file/MutableBlobStorage.cpp create mode 100644 dom/file/MutableBlobStorage.h create mode 100644 dom/file/MutableBlobStreamListener.cpp create mode 100644 dom/file/MutableBlobStreamListener.h create mode 100644 dom/file/StreamBlobImpl.cpp create mode 100644 dom/file/StreamBlobImpl.h create mode 100644 dom/file/StringBlobImpl.cpp create mode 100644 dom/file/StringBlobImpl.h create mode 100644 dom/file/TemporaryFileBlobImpl.cpp create mode 100644 dom/file/TemporaryFileBlobImpl.h create mode 100644 dom/file/ipc/FileCreatorChild.cpp create mode 100644 dom/file/ipc/FileCreatorChild.h create mode 100644 dom/file/ipc/FileCreatorParent.cpp create mode 100644 dom/file/ipc/FileCreatorParent.h create mode 100644 dom/file/ipc/IPCBlob.ipdlh create mode 100644 dom/file/ipc/IPCBlobUtils.cpp create mode 100644 dom/file/ipc/IPCBlobUtils.h create mode 100644 dom/file/ipc/PFileCreator.ipdl create mode 100644 dom/file/ipc/PRemoteLazyInputStream.ipdl create mode 100644 dom/file/ipc/PTemporaryIPCBlob.ipdl create mode 100644 dom/file/ipc/RemoteLazyInputStream.cpp create mode 100644 dom/file/ipc/RemoteLazyInputStream.h create mode 100644 dom/file/ipc/RemoteLazyInputStreamChild.cpp create mode 100644 dom/file/ipc/RemoteLazyInputStreamChild.h create mode 100644 dom/file/ipc/RemoteLazyInputStreamParent.cpp create mode 100644 dom/file/ipc/RemoteLazyInputStreamParent.h create mode 100644 dom/file/ipc/RemoteLazyInputStreamStorage.cpp create mode 100644 dom/file/ipc/RemoteLazyInputStreamStorage.h create mode 100644 dom/file/ipc/RemoteLazyInputStreamThread.cpp create mode 100644 dom/file/ipc/RemoteLazyInputStreamThread.h create mode 100644 dom/file/ipc/TemporaryIPCBlobChild.cpp create mode 100644 dom/file/ipc/TemporaryIPCBlobChild.h create mode 100644 dom/file/ipc/TemporaryIPCBlobParent.cpp create mode 100644 dom/file/ipc/TemporaryIPCBlobParent.h create mode 100644 dom/file/ipc/moz.build create mode 100644 dom/file/ipc/mozIRemoteLazyInputStream.idl create mode 100644 dom/file/ipc/tests/browser.ini create mode 100644 dom/file/ipc/tests/browser_ipcBlob.js create mode 100644 dom/file/ipc/tests/browser_ipcBlob_temporary.js create mode 100644 dom/file/ipc/tests/empty.html create mode 100644 dom/file/ipc/tests/green.jpg create mode 100644 dom/file/ipc/tests/mochitest.ini create mode 100644 dom/file/ipc/tests/ok.sjs create mode 100644 dom/file/ipc/tests/script_file.js create mode 100644 dom/file/ipc/tests/temporary.sjs create mode 100644 dom/file/ipc/tests/test_ipcBlob_createImageBitmap.html create mode 100644 dom/file/ipc/tests/test_ipcBlob_emptyMultiplex.html create mode 100644 dom/file/ipc/tests/test_ipcBlob_fileReaderSync.html create mode 100644 dom/file/ipc/tests/test_ipcBlob_mixedMultiplex.html create mode 100644 dom/file/ipc/tests/test_ipcBlob_workers.html create mode 100644 dom/file/moz.build create mode 100644 dom/file/tests/common_blob.js create mode 100644 dom/file/tests/common_blob_reading.js create mode 100644 dom/file/tests/common_blob_types.js create mode 100644 dom/file/tests/common_fileReader.js create mode 100644 dom/file/tests/crashtests/1480354.html create mode 100644 dom/file/tests/crashtests/1562891.html create mode 100644 dom/file/tests/crashtests/1747185.html create mode 100644 dom/file/tests/crashtests/1748342.html create mode 100644 dom/file/tests/crashtests/crashtests.list create mode 100644 dom/file/tests/create_file_objects.js create mode 100644 dom/file/tests/file_blobURL_expiring.html create mode 100644 dom/file/tests/file_mozfiledataurl_audio.ogg create mode 100644 dom/file/tests/file_mozfiledataurl_doc.html create mode 100644 dom/file/tests/file_mozfiledataurl_img.jpg create mode 100644 dom/file/tests/file_mozfiledataurl_inner.html create mode 100644 dom/file/tests/file_mozfiledataurl_text.txt create mode 100644 dom/file/tests/file_nonascii_blob_url.html create mode 100644 dom/file/tests/fileapi_chromeScript.js create mode 100644 dom/file/tests/mochitest.ini create mode 100644 dom/file/tests/test_agentcluster_bloburl.js create mode 100644 dom/file/tests/test_blobURL_expiring.html create mode 100644 dom/file/tests/test_blob_fragment_and_query.html create mode 100644 dom/file/tests/test_blob_reading.html create mode 100644 dom/file/tests/test_blobconstructor.html create mode 100644 dom/file/tests/test_bloburi.js create mode 100644 dom/file/tests/test_bug1507893.html create mode 100644 dom/file/tests/test_bug1742540.html create mode 100644 dom/file/tests/test_createFile.js create mode 100644 dom/file/tests/test_file_from_blob.html create mode 100644 dom/file/tests/test_file_negative_date.html create mode 100644 dom/file/tests/test_fileapi_basic.html create mode 100644 dom/file/tests/test_fileapi_basic_worker.html create mode 100644 dom/file/tests/test_fileapi_encoding.html create mode 100644 dom/file/tests/test_fileapi_encoding_worker.html create mode 100644 dom/file/tests/test_fileapi_other.html create mode 100644 dom/file/tests/test_fileapi_other_worker.html create mode 100644 dom/file/tests/test_fileapi_slice_image.html create mode 100644 dom/file/tests/test_fileapi_slice_memFile_1.html create mode 100644 dom/file/tests/test_fileapi_slice_memFile_2.html create mode 100644 dom/file/tests/test_fileapi_slice_realFile_1.html create mode 100644 dom/file/tests/test_fileapi_slice_realFile_2.html create mode 100644 dom/file/tests/test_fileapi_twice.html create mode 100644 dom/file/tests/test_fileapi_twice_worker.html create mode 100644 dom/file/tests/test_ipc_messagemanager_blob.js create mode 100644 dom/file/tests/test_mozfiledataurl.html create mode 100644 dom/file/tests/test_nonascii_blob_url.html create mode 100644 dom/file/tests/worker_blob_reading.js create mode 100644 dom/file/tests/worker_bug1507893.js create mode 100644 dom/file/tests/worker_bug1742540.js create mode 100644 dom/file/tests/worker_fileReader.js create mode 100644 dom/file/tests/xpcshell.ini create mode 100644 dom/file/uri/BlobURL.cpp create mode 100644 dom/file/uri/BlobURL.h create mode 100644 dom/file/uri/BlobURLChannel.cpp create mode 100644 dom/file/uri/BlobURLChannel.h create mode 100644 dom/file/uri/BlobURLInputStream.cpp create mode 100644 dom/file/uri/BlobURLInputStream.h create mode 100644 dom/file/uri/BlobURLProtocolHandler.cpp create mode 100644 dom/file/uri/BlobURLProtocolHandler.h create mode 100644 dom/file/uri/components.conf create mode 100644 dom/file/uri/moz.build (limited to 'dom/file') 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 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& aVisitedBlobImpls) const override { + return GetAllocationSize(); + } + + uint64_t GetSerialNumber() const override { return mSerialNumber; } + + already_AddRefed CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const override { + return nullptr; + } + + const nsTArray>* 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::CreateStringBlob(nsIGlobalObject* aGlobal, + const nsACString& aData, + const nsAString& aContentType) { + MOZ_ASSERT(aGlobal); + if (NS_WARN_IF(!aGlobal)) { + return nullptr; + } + + RefPtr blobImpl = StringBlobImpl::Create(aData, aContentType); + RefPtr blob = Blob::Create(aGlobal, blobImpl); + MOZ_ASSERT(!blob->mImpl->IsFile()); + return blob.forget(); +} + +/* static */ +already_AddRefed 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::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>* Blob::GetSubBlobImpls() const { + return mImpl->GetSubBlobImpls(); +} + +already_AddRefed Blob::ToFile() { + if (!mImpl->IsFile()) { + return nullptr; + } + + RefPtr file; + if (HasFileInterface()) { + file = static_cast(this); + } else { + file = new File(mGlobal, mImpl); + } + + return file.forget(); +} + +already_AddRefed Blob::ToFile(const nsAString& aName, + ErrorResult& aRv) const { + AutoTArray, 1> blobImpls({mImpl}); + + nsAutoString contentType; + mImpl->GetType(contentType); + + RefPtr impl = + MultipartBlobImpl::Create(std::move(blobImpls), aName, contentType, + mGlobal->GetRTPCallerType(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr file = new File(mGlobal, impl); + return file.forget(); +} + +already_AddRefed Blob::CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const { + RefPtr impl = + mImpl->CreateSlice(aStart, aLength, aContentType, aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr 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::Slice(const Optional& aStart, + const Optional& aEnd, + const Optional& aContentType, + ErrorResult& aRv) { + nsAutoString contentType; + if (aContentType.WasPassed()) { + contentType = aContentType.Value(); + } + + RefPtr impl = mImpl->Slice(aStart, aEnd, contentType, aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr 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 aGivenProto) { + return Blob_Binding::Wrap(aCx, this, aGivenProto); +} + +/* static */ +already_AddRefed Blob::Constructor( + const GlobalObject& aGlobal, const Optional>& aData, + const BlobPropertyBag& aBag, ErrorResult& aRv) { + RefPtr impl = new MultipartBlobImpl(); + + nsCOMPtr 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::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 Blob::Text(ErrorResult& aRv) const { + return ConsumeBody(BodyConsumer::CONSUME_TEXT, aRv); +} + +already_AddRefed Blob::ArrayBuffer(ErrorResult& aRv) const { + return ConsumeBody(BodyConsumer::CONSUME_ARRAYBUFFER, aRv); +} + +already_AddRefed Blob::ConsumeBody( + BodyConsumer::ConsumeType aConsumeType, ErrorResult& aRv) const { + if (NS_WARN_IF(!mGlobal)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr mainThreadEventTarget; + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + mainThreadEventTarget = workerPrivate->MainThreadEventTarget(); + } else { + mainThreadEventTarget = mGlobal->EventTargetFor(TaskCategory::Other); + } + + MOZ_ASSERT(mainThreadEventTarget); + + nsCOMPtr 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 Blob::Stream(JSContext* aCx, + ErrorResult& aRv) const { + nsCOMPtr 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(*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 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 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 CreateMemoryBlob(nsIGlobalObject* aGlobal, + void* aMemoryBuffer, + uint64_t aLength, + const nsAString& aContentType); + + BlobImpl* Impl() const { return mImpl; } + + bool IsFile() const; + + const nsTArray>* 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 ToFile(); + + // This method creates a new File object with the given name and the same + // BlobImpl. + already_AddRefed ToFile(const nsAString& aName, ErrorResult& aRv) const; + + already_AddRefed 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 Constructor( + const GlobalObject& aGlobal, const Optional>& aData, + const BlobPropertyBag& aBag, ErrorResult& aRv); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + uint64_t GetSize(ErrorResult& aRv); + + void GetType(nsAString& aType); + + void GetBlobImplType(nsAString& aBlobImplType); + + already_AddRefed Slice(const Optional& aStart, + const Optional& aEnd, + const Optional& aContentType, + ErrorResult& aRv); + + size_t GetAllocationSize() const; + + nsresult GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) const; + + already_AddRefed Stream(JSContext* aCx, + ErrorResult& aRv) const; + already_AddRefed Text(ErrorResult& aRv) const; + already_AddRefed 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 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 mImpl; + + private: + nsCOMPtr 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(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::Slice(const Optional& aStart, + const Optional& 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 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 +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& 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 Slice(const Optional& aStart, + const Optional& aEnd, + const nsAString& aContentType, + ErrorResult& aRv); + + virtual already_AddRefed CreateSlice(uint64_t aStart, + uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const = 0; + + virtual const nsTArray>* 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 = 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 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>* 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>& GetBlobImpls() { return mBlobImpls; } + + private: + FallibleTArray> 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 EmptyBlobImpl::CreateSlice( + uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + ErrorResult& aRv) const { + MOZ_ASSERT(!aStart && !aLength); + RefPtr 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 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::CreateMemoryFileWithCustomLastModified( + nsIGlobalObject* aGlobal, void* aMemoryBuffer, uint64_t aLength, + const nsAString& aName, const nsAString& aContentType, + int64_t aLastModifiedDate) { + RefPtr blobImpl = + MemoryBlobImpl::CreateWithCustomLastModified( + aMemoryBuffer, aLength, aName, aContentType, aLastModifiedDate); + MOZ_ASSERT(blobImpl); + + RefPtr file = File::Create(aGlobal, blobImpl); + return file.forget(); +} + +/* static */ +already_AddRefed File::CreateMemoryFileWithLastModifiedNow( + nsIGlobalObject* aGlobal, void* aMemoryBuffer, uint64_t aLength, + const nsAString& aName, const nsAString& aContentType) { + MOZ_ASSERT(aGlobal); + + RefPtr blobImpl = MemoryBlobImpl::CreateWithLastModifiedNow( + aMemoryBuffer, aLength, aName, aContentType, aGlobal->GetRTPCallerType()); + MOZ_ASSERT(blobImpl); + + RefPtr file = File::Create(aGlobal, blobImpl); + return file.forget(); +} + +/* static */ +already_AddRefed File::CreateFromFile(nsIGlobalObject* aGlobal, + nsIFile* aFile) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + MOZ_ASSERT(aGlobal); + if (NS_WARN_IF(!aGlobal)) { + return nullptr; + } + + RefPtr file = new File(aGlobal, new FileBlobImpl(aFile)); + return file.forget(); +} + +/* static */ +already_AddRefed 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 = + new File(aGlobal, new FileBlobImpl(aFile, aName, aContentType)); + return file.forget(); +} + +JSObject* File::WrapObject(JSContext* aCx, JS::Handle 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::Constructor(const GlobalObject& aGlobal, + const Sequence& aData, + const nsAString& aName, + const FilePropertyBag& aBag, + ErrorResult& aRv) { + RefPtr impl = new MultipartBlobImpl(aName); + + nsCOMPtr 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 = new File(global, impl); + return file.forget(); +} + +/* static */ +already_AddRefed File::CreateFromNsIFile( + const GlobalObject& aGlobal, nsIFile* aData, + const ChromeFilePropertyBag& aBag, SystemCallerGuarantee aGuarantee, + ErrorResult& aRv) { + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + + MOZ_ASSERT(global); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr promise = + FileCreatorHelper::CreateFile(global, aData, aBag, true, aRv); + return promise.forget(); +} + +/* static */ +already_AddRefed File::CreateFromFileName( + const GlobalObject& aGlobal, const nsAString& aPath, + const ChromeFilePropertyBag& aBag, SystemCallerGuarantee aGuarantee, + ErrorResult& aRv) { + nsCOMPtr file; + aRv = NS_NewLocalFile(aPath, false, getter_AddRefs(file)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + + MOZ_ASSERT(global); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr 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 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 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 CreateFromFile(nsIGlobalObject* aGlobal, + nsIFile* aFile); + + static already_AddRefed CreateFromFile(nsIGlobalObject* aGlobal, + nsIFile* aFile, + const nsAString& aName, + const nsAString& aContentType); + + // WebIDL methods + + JSObject* WrapObject(JSContext* cx, + JS::Handle aGivenProto) override; + + // File constructor + static already_AddRefed Constructor(const GlobalObject& aGlobal, + const Sequence& aData, + const nsAString& aName, + const FilePropertyBag& aBag, + ErrorResult& aRv); + + // ChromeOnly + static already_AddRefed CreateFromFileName( + const GlobalObject& aGlobal, const nsAString& aPath, + const ChromeFilePropertyBag& aBag, SystemCallerGuarantee aGuarantee, + ErrorResult& aRv); + + // ChromeOnly + static already_AddRefed 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 FileBlobImpl::CreateSlice( + uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + ErrorResult& aRv) const { + RefPtr 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 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 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 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 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 = + 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& aVisitedBlobImpls) const override { + return GetAllocationSize(); + } + + uint64_t GetSerialNumber() const override { return mSerialNumber; } + + const nsTArray>* 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 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 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 mLength; + + Maybe 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 FileCreatorHelper::CreateFile( + nsIGlobalObject* aGlobalObject, nsIFile* aFile, + const ChromeFilePropertyBag& aBag, bool aIsFromNsIFile, ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + RefPtr 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 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(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 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 +#include +#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 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>& 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 +#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 RefPtr; + +namespace mozilla { +class ErrorResult; +namespace dom { + +class BlobImpls; +class File; +template +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 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>& aSequence, ErrorResult& aRv) const; + + private: + ~FileList(); + + FallibleTArray> mFiles; + nsCOMPtr 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 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 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::Constructor( + const GlobalObject& aGlobal) { + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr workerRef; + + if (!NS_IsMainThread()) { + JSContext* cx = aGlobal.Context(); + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); + + workerRef = WeakWorkerRef::Create(workerPrivate); + } + + RefPtr 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& aResult) { + JS::Rooted 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 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 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(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 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(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 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 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(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(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 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 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 self = this; + + RefPtr 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 aGivenProto) override; + + // WebIDL + static already_AddRefed 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& 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(mReadyState); } + + DOMException* GetError() const { return mError; } + + void GetResult(JSContext* aCx, Nullable& 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 mBlob; + nsCString mCharset; + uint32_t mDataLen; + + eDataFormat mDataFormat; + + nsString mResult; + + JS::Heap mResultArrayBuffer; + + nsCOMPtr mProgressNotifier; + bool mProgressEventWasDelayed; + bool mTimerIsActive; + + nsCOMPtr mAsyncStream; + + RefPtr mError; + + eReadyState mReadyState; + + uint64_t mTotal; + uint64_t mTransferred; + + nsCOMPtr 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 mWeakWorkerRef; + + // This value is set when the reading starts in order to keep the worker alive + // during the process. + RefPtr mStrongWorkerRef; + + // Runnable to start the reading asynchronous. + class AsyncWaitRunnable; + RefPtr 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::Constructor( + const GlobalObject& aGlobal) { + RefPtr frs = new FileReaderSync(); + + return frs.forget(); +} + +bool FileReaderSync::WrapObject(JSContext* aCx, + JS::Handle aGivenProto, + JS::MutableHandle aReflector) { + return FileReaderSync_Binding::Wrap(aCx, this, aGivenProto, aReflector); +} + +void FileReaderSync::ReadAsArrayBuffer(JSContext* aCx, + JS::Handle aScopeObj, + Blob& aBlob, + JS::MutableHandle aRetval, + ErrorResult& aRv) { + uint64_t blobSize = aBlob.GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + UniquePtr bufferData( + js_pod_arena_malloc(js::ArrayBufferContentsArena, blobSize)); + if (!bufferData) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + nsCOMPtr 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 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& aEncoding, + nsAString& aResult, ErrorResult& aRv) { + nsCOMPtr 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 multiplexStream = + do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1"); + if (NS_WARN_IF(!multiplexStream)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsCOMPtr 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 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 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 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 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 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 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 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 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 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 asyncStream; + nsCOMPtr target; + + while (*aTotalBytesRead < aBufferSize) { + uint32_t currentBytesRead = 0; + + // Let's read something. + nsresult rv = + aStream->Read(aBuffer + *aTotalBytesRead, + aBufferSize - *aTotalBytesRead, ¤tBytesRead); + + // 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 syncLoopTarget = + syncLoop.GetSerialEventTarget(); + if (!syncLoopTarget) { + // SyncLoop creation can fail if the worker is shutting down. + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + RefPtr 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 aAsyncStream, + nsIInputStream** aSyncStream) { + nsCOMPtr asyncInputStream = std::move(aAsyncStream); + + // If the stream is not async, we just need it to be bufferable. + nsCOMPtr 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 +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 aAsyncStream, + nsIInputStream** aSyncStream); + + nsresult SyncRead(nsIInputStream* aStream, char* aBuffer, + uint32_t aBufferSize, uint32_t* aTotalBytesRead); + + public: + static already_AddRefed Constructor( + const GlobalObject& aGlobal); + + bool WrapObject(JSContext* aCx, JS::Handle aGivenProto, + JS::MutableHandle aReflector); + + void ReadAsArrayBuffer(JSContext* aCx, JS::Handle aScopeObj, + Blob& aBlob, JS::MutableHandle aRetval, + ErrorResult& aRv); + void ReadAsBinaryString(Blob& aBlob, nsAString& aResult, ErrorResult& aRv); + void ReadAsText(Blob& aBlob, const Optional& 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::CreateWithCustomLastModified( + void* aMemoryBuffer, uint64_t aLength, const nsAString& aName, + const nsAString& aContentType, int64_t aLastModifiedDate) { + RefPtr blobImpl = new MemoryBlobImpl( + aMemoryBuffer, aLength, aName, aContentType, aLastModifiedDate); + return blobImpl.forget(); +} + +// static +already_AddRefed 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(aDataOwner->mData) + aStart, aLength}; + RefPtr adapter = new MemoryBlobImpl::DataOwnerAdapter(aDataOwner, data); + return NS_NewByteInputStream(_retval, adapter); +} + +already_AddRefed MemoryBlobImpl::CreateSlice( + uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + ErrorResult& aRv) const { + RefPtr 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> + 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 ? "" : 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 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 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 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& aVisitedBlobImpls) const override { + return GetAllocationSize(); + } + + void GetBlobImplType(nsAString& aBlobImplType) const override { + aBlobImplType = u"MemoryBlobImpl"_ns; + } + + class DataOwner final : public mozilla::LinkedListElement { + 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(); + 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 > 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 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 aData) + : mDataOwner(aDataOwner), mData(aData) {} + + RefPtr mDataOwner; + Span 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 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 + +using namespace mozilla; +using namespace mozilla::dom; + +/* static */ +already_AddRefed MultipartBlobImpl::Create( + nsTArray>&& aBlobImpls, const nsAString& aName, + const nsAString& aContentType, RTPCallerType aRTPCallerType, + ErrorResult& aRv) { + RefPtr 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::Create( + nsTArray>&& aBlobImpls, const nsAString& aContentType, + ErrorResult& aRv) { + RefPtr 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 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 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 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> 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(l - skipStart, length); + + RefPtr 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 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(l, length); + } + + // we can create our blob now + RefPtr 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& 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 = 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& 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& 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 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& aVisitedBlobs) const { + FallibleTArray 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 + +#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 Create( + nsTArray>&& aBlobImpls, const nsAString& aName, + const nsAString& aContentType, RTPCallerType aRTPCallerType, + ErrorResult& aRv); + + // Create as a blob + static already_AddRefed Create( + nsTArray>&& 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& aData, + const nsAString& aContentType, bool aNativeEOL, + RTPCallerType aRTPCallerType, ErrorResult& aRv); + + already_AddRefed 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>* GetSubBlobImpls() const override { + return mBlobImpls.Length() ? &mBlobImpls : nullptr; + } + + void SetName(const nsAString& aName) { mName = aName; } + + size_t GetAllocationSize() const override; + size_t GetAllocationSize( + FallibleTArray& aVisitedBlobs) const override; + + void GetBlobImplType(nsAString& aBlobImplType) const override; + + void SetLastModified(int64_t aLastModified); + + protected: + // File constructor. + MultipartBlobImpl(nsTArray>&& 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>&& aBlobImpls, + const nsAString& aContentType) + : BaseBlobImpl(aContentType, MULTIPARTBLOBIMPL_UNKNOWN_LENGTH), + mBlobImpls(std::move(aBlobImpls)) {} + + ~MultipartBlobImpl() override = default; + + void SetLengthAndModifiedDate(const Maybe& aRTPCallerType, + ErrorResult& aRv); + + nsTArray> 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 mBlobStorage; + RefPtr mCallback; + RefPtr 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 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 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 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 callback(std::move(mCallback)); + callback->BlobStoreCompleted(mBlobStorage, aBlobImpl, NS_OK); + } + + void OperationFailed(nsresult aRv) override { + RefPtr 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 mBlobStorage; + nsCString mContentType; + RefPtr 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 = + 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 mBlobStorage; + nsCString mContentType; + RefPtr 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 = 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 = + 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 = 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; + + 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 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 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(static_cast(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 self = this; + nsCOMPtr 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 = 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 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 = + 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 = 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 aRunnable) { + if (!mTaskQueue) { + nsCOMPtr target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target); + + mTaskQueue = TaskQueue::Create(target.forget(), "BlobStorage"); + } + + nsCOMPtr 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 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 mTaskQueue; + nsCOMPtr mEventTarget; + + nsCString mPendingContentType; + RefPtr mPendingCallback; + + RefPtr 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 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(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 mStorage; + RefPtr mCallback; + + MutableBlobStorage::MutableBlobStorageType mStorageType; + nsCString mContentType; + nsCOMPtr 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 EnsureCloneableStream( + nsIInputStream* aInputStream, uint64_t aLength) { + nsCOMPtr 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 reader; + nsCOMPtr writer; + NS_NewPipe2(getter_AddRefs(reader), getter_AddRefs(writer), true, true, + segmentSize, UINT32_MAX); + + nsresult rv; + nsCOMPtr 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::Create( + already_AddRefed aInputStream, + const nsAString& aContentType, uint64_t aLength, + const nsAString& aBlobImplType) { + nsCOMPtr inputStream = std::move(aInputStream); + nsCOMPtr cloneable = + EnsureCloneableStream(inputStream, aLength); + + RefPtr blobImplStream = new StreamBlobImpl( + cloneable.forget(), aContentType, aLength, aBlobImplType); + blobImplStream->MaybeRegisterMemoryReporter(); + return blobImplStream.forget(); +} + +/* static */ +already_AddRefed StreamBlobImpl::Create( + already_AddRefed aInputStream, const nsAString& aName, + const nsAString& aContentType, int64_t aLastModifiedDate, uint64_t aLength, + const nsAString& aBlobImplType) { + nsCOMPtr inputStream = std::move(aInputStream); + nsCOMPtr cloneable = + EnsureCloneableStream(inputStream, aLength); + + RefPtr blobImplStream = + new StreamBlobImpl(cloneable.forget(), aName, aContentType, + aLastModifiedDate, aLength, aBlobImplType); + blobImplStream->MaybeRegisterMemoryReporter(); + return blobImplStream.forget(); +} + +StreamBlobImpl::StreamBlobImpl( + already_AddRefed 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 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 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 clonedStream; + aRv = mInputStream->Clone(getter_AddRefs(clonedStream)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr wrappedStream = + InputStreamLengthWrapper::MaybeWrap(clonedStream.forget(), mLength); + + wrappedStream.forget(aStream); +} + +already_AddRefed StreamBlobImpl::CreateSlice( + uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + ErrorResult& aRv) const { + if (!aLength) { + RefPtr impl = new EmptyBlobImpl(aContentType); + return impl.forget(); + } + + nsCOMPtr clonedStream; + + nsCOMPtr 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 stringInputStream = + do_QueryInterface(mInputStream); + if (!stringInputStream) { + return; + } + + RegisterWeakMemoryReporter(this); +} + +NS_IMETHODIMP +StreamBlobImpl::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) { + nsCOMPtr 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 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 Create( + already_AddRefed aInputStream, + const nsAString& aContentType, uint64_t aLength, + const nsAString& aBlobImplType); + + // File constructor. + static already_AddRefed Create( + already_AddRefed 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 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& aVisitedBlobImpls) const override { + return GetAllocationSize(); + } + + void GetBlobImplType(nsAString& aBlobImplType) const override; + + private: + // Blob constructor. + StreamBlobImpl(already_AddRefed aInputStream, + const nsAString& aContentType, uint64_t aLength, + const nsAString& aBlobImplType); + + // File constructor. + StreamBlobImpl(already_AddRefed aInputStream, + const nsAString& aName, const nsAString& aContentType, + int64_t aLastModifiedDate, uint64_t aLength, + const nsAString& aBlobImplType); + + ~StreamBlobImpl() override; + + void MaybeRegisterMemoryReporter(); + + nsCOMPtr 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::Create( + const nsACString& aData, const nsAString& aContentType) { + RefPtr 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 StringBlobImpl::CreateSlice( + uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + ErrorResult& aRv) const { + RefPtr 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 Create(const nsACString& aData, + const nsAString& aContentType); + + void CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const override; + + already_AddRefed 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& 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 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 thread = + RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return; + } + + nsCOMPtr file = std::move(mFile); + thread->Dispatch( + NS_NewRunnableFunction("TemporaryFileInputStream::Runnable", + [file]() { file->Remove(false); })); + } + + nsCOMPtr 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 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 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.swap(mPromise); + + if (aResult.type() == FileCreationResult::TFileCreationErrorResult) { + promise->MaybeReject(aResult.get_FileCreationErrorResult().errorCode()); + return IPC_OK(); + } + + MOZ_ASSERT(aResult.type() == FileCreationResult::TFileCreationSuccessResult); + + RefPtr impl = dom::IPCBlobUtils::Deserialize( + aResult.get_FileCreationSuccessResult().blob()); + + RefPtr 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 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& aLastModified, const bool& aExistenceCheck, + const bool& aIsFromNsIFile) { + RefPtr 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 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 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 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& 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 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 Deserialize(const IPCBlob& aIPCBlob) { + nsCOMPtr 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 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 inputStream; + aBlobImpl->CreateInputStream(getter_AddRefs(inputStream), rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + if (XRE_IsParentProcess()) { + RefPtr 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::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::Read( + IPC::MessageReader* aReader, RefPtr* aResult) { + *aResult = nullptr; + + bool notnull = false; + if (!ReadParam(aReader, ¬null)) { + 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 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 { + static void Write(IPC::MessageWriter* aWriter, + mozilla::dom::BlobImpl* aParam); + static bool Read(IPC::MessageReader* aReader, + RefPtr* 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 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 aCallback, + already_AddRefed aEventTarget, + RemoteLazyInputStream* aStream) { + RefPtr runnable = + new InputStreamCallbackRunnable(std::move(aCallback), aStream); + + nsCOMPtr 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 aCallback, + RemoteLazyInputStream* aStream) + : DiscardableRunnable("dom::InputStreamCallbackRunnable"), + mCallback(std::move(aCallback)), + mStream(aStream) { + MOZ_ASSERT(mCallback); + MOZ_ASSERT(mStream); + } + + RefPtr mCallback; + RefPtr mStream; +}; + +class FileMetadataCallbackRunnable final : public DiscardableRunnable { + public: + static void Execute(nsIFileMetadataCallback* aCallback, + nsIEventTarget* aEventTarget, + RemoteLazyInputStream* aStream) { + MOZ_ASSERT(aCallback); + MOZ_ASSERT(aEventTarget); + + RefPtr runnable = + new FileMetadataCallbackRunnable(aCallback, aStream); + + nsCOMPtr 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 mCallback; + RefPtr 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 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 BindChildActor( + nsID aId, mozilla::ipc::Endpoint aEndpoint) { + auto* thread = RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return nullptr; + } + auto actor = MakeRefPtr(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::WrapStream( + nsIInputStream* aInputStream) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (nsCOMPtr lazyStream = + do_QueryInterface(aInputStream)) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Returning already-wrapped stream")); + return lazyStream.forget().downcast(); + } + + // 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 parentEp; + mozilla::ipc::Endpoint 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(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 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 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() : "", + mInnerStream ? mInnerStream.get() : mAsyncInnerStream.get(), + mAsyncInnerStream ? "(A)" : "", mInputStreamCallback ? "I" : "", + mInputStreamCallbackEventTarget ? "+" : "", + mFileMetadataCallback ? "F" : "", + mFileMetadataCallbackEventTarget ? "+" : ""); +} + +// nsIInputStream interface + +NS_IMETHODIMP +RemoteLazyInputStream::Available(uint64_t* aLength) { + nsCOMPtr 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 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 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 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 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 actor; + + nsCOMPtr asyncInnerStream; + nsCOMPtr innerStream; + + RefPtr inputStreamCallback; + nsCOMPtr 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 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 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 innerStream = mInnerStream; + if (mAsyncInnerStream) { + innerStream = mAsyncInnerStream; + } + + nsCOMPtr 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 pipeIn; + nsCOMPtr pipeOut; + NS_NewPipe2(getter_AddRefs(pipeIn), getter_AddRefs(pipeOut), true, true); + + RefPtr 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 cloneableWithRange = + do_QueryInterface(cloneable)) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Cloning with range")); + nsCOMPtr 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 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 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 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& aStream) { + // Try to deserialize the stream from our remote, and close our + // stream if it fails. + nsCOMPtr 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 fileMetadataCallback = + self->mFileMetadataCallback.forget(); + nsCOMPtr 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 callback; + nsCOMPtr 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 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 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 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 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 bufferedStream; + nsresult rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream), + stream.forget(), 4096); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stream = bufferedStream; + } + + nsCOMPtr 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 pipeIn; + nsCOMPtr pipeOut; + NS_NewPipe2(getter_AddRefs(pipeIn), getter_AddRefs(pipeOut), true, true); + + RefPtr 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 runnable = + new InputStreamLengthCallbackRunnable(aCallback, aStream, aLength); + + nsCOMPtr 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 mCallback; + RefPtr 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(aLength, self->mStart); + length = int64_t( + std::min(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 actor; + + nsCOMPtr innerStream; + + RefPtr inputStreamCallback; + nsCOMPtr 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 parentEp; + mozilla::ipc::Endpoint 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 parentEp; + mozilla::ipc::Endpoint 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(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::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 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 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::Write( + IPC::MessageWriter* aWriter, mozilla::RemoteLazyInputStream* aParam) { + bool nonNull = !!aParam; + IPC::WriteParam(aWriter, nonNull); + if (aParam) { + aParam->IPCWrite(aWriter); + } +} + +bool IPC::ParamTraits::Read( + IPC::MessageReader* aReader, + RefPtr* 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 WrapStream( + nsIInputStream* aInputStream); + + // mozIRemoteLazyInputStream + NS_IMETHOD TakeInternalStream(nsIInputStream** aStream) override; + NS_IMETHOD GetInternalStreamID(nsID& aID) override; + + private: + friend struct IPC::ParamTraits; + + // 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 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 mActor MOZ_GUARDED_BY(mMutex); + + nsCOMPtr mInnerStream MOZ_GUARDED_BY(mMutex); + nsCOMPtr 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 mInputStreamCallback MOZ_GUARDED_BY(mMutex); + nsCOMPtr 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 mFileMetadataCallback + MOZ_GUARDED_BY(mMutex); + nsCOMPtr mFileMetadataCallbackEventTarget + MOZ_GUARDED_BY(mMutex); +}; + +} // namespace mozilla + +template <> +struct IPC::ParamTraits { + static void Write(IPC::MessageWriter* aWriter, + mozilla::RemoteLazyInputStream* aParam); + static bool Read(IPC::MessageReader* aReader, + RefPtr* 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 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&& 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 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; + 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 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&& 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 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, nsresult> +RemoteLazyInputStreamStorage::Get() { + mozilla::StaticMutexAutoLock lock(gMutex); + if (gStorage) { + RefPtr 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 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 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 data = MakeUnique(); + data->mInputStream = aInputStream; + + mozilla::StaticMutexAutoLock lock(gMutex); + mStorage.InsertOrUpdate(aID, std::move(data)); +} + +nsCOMPtr RemoteLazyInputStreamStorage::ForgetStream( + const nsID& aID) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::ForgetStream(%s)", nsIDToCString(aID).get())); + + UniquePtr 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 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 clonedStream; + nsCOMPtr 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 +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 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 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, 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 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 TakeCallback( + const nsID& aID); + + void ActorCreated(const nsID& aID); + void ActorDestroyed(const nsID& aID); + + private: + RemoteLazyInputStreamStorage() = default; + ~RemoteLazyInputStreamStorage() = default; + + nsCOMPtr mTaskQueue; + + struct StreamData { + nsCOMPtr mInputStream; + RefPtr mCallback; + size_t mActorCount = 0; + }; + + nsClassHashtable 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 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 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 = 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 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 aRunnable, + uint32_t aFlags) { + if (RLISThreadIsInOrBeyondShutdown()) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + nsCOMPtr runnable(aRunnable); + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + return mThread->Dispatch(runnable.forget(), aFlags); +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::DispatchFromScript(nsIRunnable* aRunnable, + uint32_t aFlags) { + nsCOMPtr runnable(aRunnable); + return Dispatch(runnable.forget(), aFlags); +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::DelayedDispatch(already_AddRefed, + 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 aRunnable) { + nsCOMPtr runnable(aRunnable); + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + nsCOMPtr dispatcher = do_QueryInterface(mThread); + + if (dispatcher) { + return dispatcher->DispatchDirectTask(runnable.forget()); + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP RemoteLazyInputStreamThread::DrainDirectTasks() { + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + nsCOMPtr dispatcher = do_QueryInterface(mThread); + + if (dispatcher) { + return dispatcher->DrainDirectTasks(); + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP RemoteLazyInputStreamThread::HaveDirectTasks(bool* aValue) { + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + nsCOMPtr 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 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 + +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 = + 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 mMutableBlobStorage; + RefPtr 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 file = std::move(mFile); + + RefPtr 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 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 diff --git a/dom/file/ipc/tests/green.jpg b/dom/file/ipc/tests/green.jpg new file mode 100644 index 0000000000..48c454d27c Binary files /dev/null and b/dom/file/ipc/tests/green.jpg 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 @@ + + + + Test IPCBlob and CreateImageBitmap + + + + + + + + + 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 @@ + + + + Test an empty IPCBlob together with other parts + + + + + + + + + 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 @@ + + + + Test IPCBlob and FileReaderSync + + + + + + + + + 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 @@ + + + + Test an empty IPCBlob together with other parts + + + + + + + + + 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 @@ + + + + Test IPCBlob and Workers + + + + + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + + + 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 @@ + 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 Binary files /dev/null and b/dom/file/tests/file_mozfiledataurl_audio.ogg 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 @@ + + + +

This here is a document!

+ + 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 Binary files /dev/null and b/dom/file/tests/file_mozfiledataurl_img.jpg 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 @@ + + + + + +