diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/file | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/file')
138 files changed, 16544 insertions, 0 deletions
diff --git a/dom/file/BaseBlobImpl.cpp b/dom/file/BaseBlobImpl.cpp new file mode 100644 index 0000000000..5871b9da04 --- /dev/null +++ b/dom/file/BaseBlobImpl.cpp @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/BaseBlobImpl.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "nsRFPService.h" +#include "prtime.h" + +namespace mozilla::dom { + +void BaseBlobImpl::GetName(nsAString& aName) const { + MOZ_ASSERT(mIsFile, "Should only be called on files"); + aName = mName; +} + +void BaseBlobImpl::GetDOMPath(nsAString& aPath) const { + MOZ_ASSERT(mIsFile, "Should only be called on files"); + aPath = mPath; +} + +void BaseBlobImpl::SetDOMPath(const nsAString& aPath) { + MOZ_ASSERT(mIsFile, "Should only be called on files"); + mPath = aPath; +} + +void BaseBlobImpl::GetMozFullPath(nsAString& aFileName, + SystemCallerGuarantee /* unused */, + ErrorResult& aRv) { + MOZ_ASSERT(mIsFile, "Should only be called on files"); + + GetMozFullPathInternal(aFileName, aRv); +} + +void BaseBlobImpl::GetMozFullPathInternal(nsAString& aFileName, + ErrorResult& aRv) { + if (!mIsFile) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + aFileName.Truncate(); +} + +void BaseBlobImpl::GetType(nsAString& aType) { aType = mContentType; } + +int64_t BaseBlobImpl::GetLastModified(ErrorResult& aRv) { + MOZ_ASSERT(mIsFile, "Should only be called on files"); + return mLastModificationDate / PR_USEC_PER_MSEC; +} + +int64_t BaseBlobImpl::GetFileId() const { return -1; } + +/* static */ +uint64_t BaseBlobImpl::NextSerialNumber() { + static Atomic<uint64_t> nextSerialNumber; + return nextSerialNumber++; +} + +void BaseBlobImpl::SetLastModificationDatePrecisely(int64_t aDate) { + MOZ_ASSERT(mIsFile, "Should only be called on files"); + mLastModificationDate = aDate; +} + +void BaseBlobImpl::SetLastModificationDate(RTPCallerType aRTPCallerType, + int64_t aDate) { + return SetLastModificationDatePrecisely( + nsRFPService::ReduceTimePrecisionAsUSecs(aDate, 0, aRTPCallerType)); + // mLastModificationDate is an absolute timestamp so we supply a zero + // context mix-in +} + +} // namespace mozilla::dom diff --git a/dom/file/BaseBlobImpl.h b/dom/file/BaseBlobImpl.h new file mode 100644 index 0000000000..7265fc2104 --- /dev/null +++ b/dom/file/BaseBlobImpl.h @@ -0,0 +1,158 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_BaseBlobImpl_h +#define mozilla_dom_BaseBlobImpl_h + +#include "nsIGlobalObject.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/ErrorResult.h" + +namespace mozilla::dom { + +class FileBlobImpl; + +class BaseBlobImpl : public BlobImpl { + friend class FileBlobImpl; + + public: + // File constructor. + BaseBlobImpl(const nsAString& aName, const nsAString& aContentType, + uint64_t aLength, int64_t aLastModifiedDate) + : mIsFile(true), + mContentType(aContentType), + mName(aName), + mStart(0), + mLength(aLength), + mSerialNumber(NextSerialNumber()), + mLastModificationDate(aLastModifiedDate) { + // Ensure non-null mContentType by default + mContentType.SetIsVoid(false); + } + + // Blob constructor without starting point. + BaseBlobImpl(const nsAString& aContentType, uint64_t aLength) + : mIsFile(false), + mContentType(aContentType), + mStart(0), + mLength(aLength), + mSerialNumber(NextSerialNumber()), + mLastModificationDate(0) { + // Ensure non-null mContentType by default + mContentType.SetIsVoid(false); + } + + // Blob constructor with starting point. + BaseBlobImpl(const nsAString& aContentType, uint64_t aStart, uint64_t aLength) + : mIsFile(false), + mContentType(aContentType), + mStart(aStart), + mLength(aLength), + mSerialNumber(NextSerialNumber()), + mLastModificationDate(0) { + // Ensure non-null mContentType by default + mContentType.SetIsVoid(false); + } + + void GetName(nsAString& aName) const override; + + void GetDOMPath(nsAString& aPath) const override; + + void SetDOMPath(const nsAString& aPath) override; + + int64_t GetLastModified(ErrorResult& aRv) override; + + void GetMozFullPath(nsAString& aFileName, SystemCallerGuarantee /* unused */, + ErrorResult& aRv) override; + + void GetMozFullPathInternal(nsAString& aFileName, ErrorResult& aRv) override; + + uint64_t GetSize(ErrorResult& aRv) override { return mLength; } + + void GetType(nsAString& aType) override; + + size_t GetAllocationSize() const override { return 0; } + + size_t GetAllocationSize( + FallibleTArray<BlobImpl*>& aVisitedBlobImpls) const override { + return GetAllocationSize(); + } + + uint64_t GetSerialNumber() const override { return mSerialNumber; } + + already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const override { + return nullptr; + } + + const nsTArray<RefPtr<BlobImpl>>* GetSubBlobImpls() const override { + return nullptr; + } + + void CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const override { + aRv.Throw(NS_ERROR_NOT_IMPLEMENTED); + } + + int64_t GetFileId() const override; + + void SetLazyData(const nsAString& aName, const nsAString& aContentType, + uint64_t aLength, int64_t aLastModifiedDate) override { + mName = aName; + mContentType = aContentType; + mLength = aLength; + SetLastModificationDatePrecisely(aLastModifiedDate); + mIsFile = !aName.IsVoid(); + } + + bool IsMemoryFile() const override { return false; } + + bool IsFile() const override { return mIsFile; } + + void GetBlobImplType(nsAString& aBlobImplType) const override { + aBlobImplType = u"BaseBlobImpl"_ns; + } + + protected: + ~BaseBlobImpl() override = default; + + /** + * Returns a new, effectively-unique serial number. This should be used + * by implementations to obtain a serial number for GetSerialNumber(). + * The implementation is thread safe. + */ + static uint64_t NextSerialNumber(); + + void SetLastModificationDate(RTPCallerType aRTPCallerType, int64_t aDate); + void SetLastModificationDatePrecisely(int64_t aDate); + +#ifdef DEBUG + bool IsLastModificationDateUnset() const { + return mLastModificationDate == INT64_MAX; + } +#endif + + const nsString mBlobImplType; + + bool mIsFile; + + nsString mContentType; + nsString mName; + nsString mPath; // The path relative to a directory chosen by the user + + uint64_t mStart; + uint64_t mLength; + + const uint64_t mSerialNumber; + + private: + int64_t mLastModificationDate; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_BaseBlobImpl_h diff --git a/dom/file/Blob.cpp b/dom/file/Blob.cpp new file mode 100644 index 0000000000..dc3a70c74c --- /dev/null +++ b/dom/file/Blob.cpp @@ -0,0 +1,338 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Blob.h" +#include "EmptyBlobImpl.h" +#include "File.h" +#include "MemoryBlobImpl.h" +#include "mozilla/dom/BlobBinding.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/HoldDropJSObjects.h" +#include "MultipartBlobImpl.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIGlobalObject.h" +#include "nsIInputStream.h" +#include "nsPIDOMWindow.h" +#include "StreamBlobImpl.h" +#include "StringBlobImpl.h" +#include "js/GCAPI.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(Blob) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Blob) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Blob) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Blob) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Blob) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY_CONCRETE(Blob) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Blob) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Blob) + +void Blob::MakeValidBlobType(nsAString& aType) { + char16_t* iter = aType.BeginWriting(); + char16_t* end = aType.EndWriting(); + + for (; iter != end; ++iter) { + char16_t c = *iter; + if (c < 0x20 || c > 0x7E) { + // Non-ASCII char, bail out. + aType.Truncate(); + return; + } + + if (c >= 'A' && c <= 'Z') { + *iter = c + ('a' - 'A'); + } + } +} + +/* static */ +Blob* Blob::Create(nsIGlobalObject* aGlobal, BlobImpl* aImpl) { + MOZ_ASSERT(aImpl); + + MOZ_ASSERT(aGlobal); + if (NS_WARN_IF(!aGlobal)) { + return nullptr; + } + + return aImpl->IsFile() ? new File(aGlobal, aImpl) : new Blob(aGlobal, aImpl); +} + +/* static */ +already_AddRefed<Blob> Blob::CreateStringBlob(nsIGlobalObject* aGlobal, + const nsACString& aData, + const nsAString& aContentType) { + MOZ_ASSERT(aGlobal); + if (NS_WARN_IF(!aGlobal)) { + return nullptr; + } + + RefPtr<BlobImpl> blobImpl = StringBlobImpl::Create(aData, aContentType); + RefPtr<Blob> blob = Blob::Create(aGlobal, blobImpl); + MOZ_ASSERT(!blob->mImpl->IsFile()); + return blob.forget(); +} + +/* static */ +already_AddRefed<Blob> Blob::CreateMemoryBlob(nsIGlobalObject* aGlobal, + void* aMemoryBuffer, + uint64_t aLength, + const nsAString& aContentType) { + MOZ_ASSERT(aGlobal); + if (NS_WARN_IF(!aGlobal)) { + return nullptr; + } + + RefPtr<Blob> blob = Blob::Create( + aGlobal, new MemoryBlobImpl(aMemoryBuffer, aLength, aContentType)); + MOZ_ASSERT(!blob->mImpl->IsFile()); + return blob.forget(); +} + +Blob::Blob(nsIGlobalObject* aGlobal, BlobImpl* aImpl) + : mImpl(aImpl), mGlobal(aGlobal) { + MOZ_ASSERT(mImpl); + MOZ_ASSERT(mGlobal); +} + +Blob::~Blob() = default; + +bool Blob::IsFile() const { return mImpl->IsFile(); } + +const nsTArray<RefPtr<BlobImpl>>* Blob::GetSubBlobImpls() const { + return mImpl->GetSubBlobImpls(); +} + +already_AddRefed<File> Blob::ToFile() { + if (!mImpl->IsFile()) { + return nullptr; + } + + RefPtr<File> file; + if (HasFileInterface()) { + file = static_cast<File*>(this); + } else { + file = new File(mGlobal, mImpl); + } + + return file.forget(); +} + +already_AddRefed<File> Blob::ToFile(const nsAString& aName, + ErrorResult& aRv) const { + AutoTArray<RefPtr<BlobImpl>, 1> blobImpls({mImpl}); + + nsAutoString contentType; + mImpl->GetType(contentType); + + RefPtr<MultipartBlobImpl> impl = + MultipartBlobImpl::Create(std::move(blobImpls), aName, contentType, + mGlobal->GetRTPCallerType(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<File> file = new File(mGlobal, impl); + return file.forget(); +} + +already_AddRefed<Blob> Blob::CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const { + RefPtr<BlobImpl> impl = + mImpl->CreateSlice(aStart, aLength, aContentType, aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<Blob> blob = Blob::Create(mGlobal, impl); + return blob.forget(); +} + +uint64_t Blob::GetSize(ErrorResult& aRv) { return mImpl->GetSize(aRv); } + +void Blob::GetType(nsAString& aType) { mImpl->GetType(aType); } + +void Blob::GetBlobImplType(nsAString& aBlobImplType) { + mImpl->GetBlobImplType(aBlobImplType); +} + +already_AddRefed<Blob> Blob::Slice(const Optional<int64_t>& aStart, + const Optional<int64_t>& aEnd, + const Optional<nsAString>& aContentType, + ErrorResult& aRv) { + nsAutoString contentType; + if (aContentType.WasPassed()) { + contentType = aContentType.Value(); + } + + RefPtr<BlobImpl> impl = mImpl->Slice(aStart, aEnd, contentType, aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<Blob> blob = Blob::Create(mGlobal, impl); + return blob.forget(); +} + +size_t Blob::GetAllocationSize() const { return mImpl->GetAllocationSize(); } + +// contentTypeWithCharset can be set to the contentType or +// contentType+charset based on what the spec says. +// See: https://fetch.spec.whatwg.org/#concept-bodyinit-extract +nsresult Blob::GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength, + nsACString& aContentType, + nsACString& aCharset) const { + return mImpl->GetSendInfo(aBody, aContentLength, aContentType, aCharset); +} + +JSObject* Blob::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return Blob_Binding::Wrap(aCx, this, aGivenProto); +} + +/* static */ +already_AddRefed<Blob> Blob::Constructor( + const GlobalObject& aGlobal, const Optional<Sequence<BlobPart>>& aData, + const BlobPropertyBag& aBag, ErrorResult& aRv) { + RefPtr<MultipartBlobImpl> impl = new MultipartBlobImpl(); + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + MOZ_ASSERT(global); + if (aData.WasPassed()) { + nsAutoString type(aBag.mType); + MakeValidBlobType(type); + impl->InitializeBlob(aData.Value(), type, + aBag.mEndings == EndingType::Native, + global->GetRTPCallerType(), aRv); + } else { + impl->InitializeBlob(global->GetRTPCallerType(), aRv); + } + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + MOZ_ASSERT(!impl->IsFile()); + + RefPtr<Blob> blob = Blob::Create(global, impl); + return blob.forget(); +} + +int64_t Blob::GetFileId() const { return mImpl->GetFileId(); } + +bool Blob::IsMemoryFile() const { return mImpl->IsMemoryFile(); } + +void Blob::CreateInputStream(nsIInputStream** aStream, ErrorResult& aRv) const { + mImpl->CreateInputStream(aStream, aRv); +} + +size_t BindingJSObjectMallocBytes(Blob* aBlob) { + MOZ_ASSERT(aBlob); + + // TODO: The hazard analysis currently can't see that none of the + // implementations of the GetAllocationSize virtual method call can GC (see + // bug 1531951). + JS::AutoSuppressGCAnalysis nogc; + + return aBlob->GetAllocationSize(); +} + +already_AddRefed<Promise> Blob::Text(ErrorResult& aRv) const { + return ConsumeBody(BodyConsumer::CONSUME_TEXT, aRv); +} + +already_AddRefed<Promise> Blob::ArrayBuffer(ErrorResult& aRv) const { + return ConsumeBody(BodyConsumer::CONSUME_ARRAYBUFFER, aRv); +} + +already_AddRefed<Promise> Blob::ConsumeBody( + BodyConsumer::ConsumeType aConsumeType, ErrorResult& aRv) const { + if (NS_WARN_IF(!mGlobal)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr<nsISerialEventTarget> mainThreadEventTarget; + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + mainThreadEventTarget = workerPrivate->MainThreadEventTarget(); + } else { + mainThreadEventTarget = mGlobal->EventTargetFor(TaskCategory::Other); + } + + MOZ_ASSERT(mainThreadEventTarget); + + nsCOMPtr<nsIInputStream> inputStream; + CreateInputStream(getter_AddRefs(inputStream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return BodyConsumer::Create(mGlobal, mainThreadEventTarget, inputStream, + nullptr, aConsumeType, VoidCString(), + VoidString(), VoidCString(), VoidCString(), + MutableBlobStorage::eOnlyInMemory, aRv); +} + +// https://w3c.github.io/FileAPI/#stream-method-algo +// "The stream() method, when invoked, must return the result of calling get +// stream on this." +// And that's https://w3c.github.io/FileAPI/#blob-get-stream. +already_AddRefed<ReadableStream> Blob::Stream(JSContext* aCx, + ErrorResult& aRv) const { + nsCOMPtr<nsIInputStream> stream; + CreateInputStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (NS_WARN_IF(!mGlobal)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + auto algorithms = + MakeRefPtr<NonAsyncInputToReadableStreamAlgorithms>(*stream); + + // Step 1: Let stream be a new ReadableStream created in blob’s relevant + // Realm. + // Step 2: Set up stream with byte reading support. + // Step 3: ... + // (The spec here does not define pullAlgorithm and instead greedily enqueues + // everything into the stream when .stream() is called, but here we only reads + // the data when actual read request happens, via + // InputToReadableStreamAlgorithms. See + // https://github.com/w3c/FileAPI/issues/194.) + RefPtr<ReadableStream> body = ReadableStream::CreateByteNative( + aCx, mGlobal, *algorithms, Nothing(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + return body.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/file/Blob.h b/dom/file/Blob.h new file mode 100644 index 0000000000..2314df32b3 --- /dev/null +++ b/dom/file/Blob.h @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_Blob_h +#define mozilla_dom_Blob_h + +#include "mozilla/dom/BodyConsumer.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" +#include "nsWrapperCache.h" +#include "nsWeakReference.h" + +class nsIGlobalObject; +class nsIInputStream; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +struct BlobPropertyBag; +class BlobImpl; +class File; +class GlobalObject; +class OwningArrayBufferViewOrArrayBufferOrBlobOrUTF8String; +class Promise; + +class ReadableStream; + +#define NS_DOM_BLOB_IID \ + { \ + 0x648c2a83, 0xbdb1, 0x4a7d, { \ + 0xb5, 0x0a, 0xca, 0xcd, 0x92, 0x87, 0x45, 0xc2 \ + } \ + } + +class Blob : public nsSupportsWeakReference, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS_FINAL + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Blob) + NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOM_BLOB_IID) + + using BlobPart = OwningArrayBufferViewOrArrayBufferOrBlobOrUTF8String; + + // This creates a Blob or a File based on the type of BlobImpl. + static Blob* Create(nsIGlobalObject* aGlobal, BlobImpl* aImpl); + + static already_AddRefed<Blob> CreateStringBlob(nsIGlobalObject* aGlobal, + const nsACString& aData, + const nsAString& aContentType); + + // The returned Blob takes ownership of aMemoryBuffer. aMemoryBuffer will be + // freed by free so it must be allocated by malloc or something + // compatible with it. + static already_AddRefed<Blob> CreateMemoryBlob(nsIGlobalObject* aGlobal, + void* aMemoryBuffer, + uint64_t aLength, + const nsAString& aContentType); + + BlobImpl* Impl() const { return mImpl; } + + bool IsFile() const; + + const nsTArray<RefPtr<BlobImpl>>* GetSubBlobImpls() const; + + // This method returns null if this Blob is not a File; it returns + // the same object in case this Blob already implements the File interface; + // otherwise it returns a new File object with the same BlobImpl. + already_AddRefed<File> ToFile(); + + // This method creates a new File object with the given name and the same + // BlobImpl. + already_AddRefed<File> ToFile(const nsAString& aName, ErrorResult& aRv) const; + + already_AddRefed<Blob> CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const; + + void CreateInputStream(nsIInputStream** aStream, ErrorResult& aRv) const; + + int64_t GetFileId() const; + + // A utility function that enforces the spec constraints on the type of a + // blob: no codepoints outside the ASCII range (otherwise type becomes empty) + // and lowercase ASCII only. We can't just use our existing nsContentUtils + // ASCII-related helpers because we need the "outside ASCII range" check, and + // we can't use NS_IsAscii because its definition of "ASCII" (chars all <= + // 0x7E) differs from the file API definition (which excludes control chars). + static void MakeValidBlobType(nsAString& aType); + + // WebIDL methods + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + bool IsMemoryFile() const; + + // Blob constructor + static already_AddRefed<Blob> Constructor( + const GlobalObject& aGlobal, const Optional<Sequence<BlobPart>>& aData, + const BlobPropertyBag& aBag, ErrorResult& aRv); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + uint64_t GetSize(ErrorResult& aRv); + + void GetType(nsAString& aType); + + void GetBlobImplType(nsAString& aBlobImplType); + + already_AddRefed<Blob> Slice(const Optional<int64_t>& aStart, + const Optional<int64_t>& aEnd, + const Optional<nsAString>& aContentType, + ErrorResult& aRv); + + size_t GetAllocationSize() const; + + nsresult GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) const; + + already_AddRefed<ReadableStream> Stream(JSContext* aCx, + ErrorResult& aRv) const; + already_AddRefed<Promise> Text(ErrorResult& aRv) const; + already_AddRefed<Promise> ArrayBuffer(ErrorResult& aRv) const; + + protected: + // File constructor should never be used directly. Use Blob::Create instead. + Blob(nsIGlobalObject* aGlobal, BlobImpl* aImpl); + virtual ~Blob(); + + virtual bool HasFileInterface() const { return false; } + + already_AddRefed<Promise> ConsumeBody(BodyConsumer::ConsumeType aConsumeType, + ErrorResult& aRv) const; + + // The member is the real backend implementation of this File/Blob. + // It's thread-safe and not CC-able and it's the only element that is moved + // between threads. + // Note: we should not store any other state in this class! + RefPtr<BlobImpl> mImpl; + + private: + nsCOMPtr<nsIGlobalObject> mGlobal; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(Blob, NS_DOM_BLOB_IID) + +// Override BindingJSObjectMallocBytes for blobs to tell the JS GC how much +// memory is held live by the binding object. +size_t BindingJSObjectMallocBytes(Blob* aBlob); + +} // namespace dom +} // namespace mozilla + +inline nsISupports* ToSupports(mozilla::dom::Blob* aBlob) { + return static_cast<nsISupportsWeakReference*>(aBlob); +} + +#endif // mozilla_dom_Blob_h diff --git a/dom/file/BlobImpl.cpp b/dom/file/BlobImpl.cpp new file mode 100644 index 0000000000..fa31737c9d --- /dev/null +++ b/dom/file/BlobImpl.cpp @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BlobImpl.h" +#include "File.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/ErrorResult.h" +#include "nsIInputStream.h" + +namespace mozilla::dom { + +// Makes sure that aStart and aEnd is less then or equal to aSize and greater +// than 0 +static void ParseSize(int64_t aSize, int64_t& aStart, int64_t& aEnd) { + CheckedInt64 newStartOffset = aStart; + if (aStart < -aSize) { + newStartOffset = 0; + } else if (aStart < 0) { + newStartOffset += aSize; + } else if (aStart > aSize) { + newStartOffset = aSize; + } + + CheckedInt64 newEndOffset = aEnd; + if (aEnd < -aSize) { + newEndOffset = 0; + } else if (aEnd < 0) { + newEndOffset += aSize; + } else if (aEnd > aSize) { + newEndOffset = aSize; + } + + if (!newStartOffset.isValid() || !newEndOffset.isValid() || + newStartOffset.value() >= newEndOffset.value()) { + aStart = aEnd = 0; + } else { + aStart = newStartOffset.value(); + aEnd = newEndOffset.value(); + } +} + +already_AddRefed<BlobImpl> BlobImpl::Slice(const Optional<int64_t>& aStart, + const Optional<int64_t>& aEnd, + const nsAString& aContentType, + ErrorResult& aRv) { + // Truncate aStart and aEnd so that we stay within this file. + uint64_t thisLength = GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + int64_t start = aStart.WasPassed() ? aStart.Value() : 0; + int64_t end = aEnd.WasPassed() ? aEnd.Value() : (int64_t)thisLength; + + ParseSize((int64_t)thisLength, start, end); + + nsAutoString type(aContentType); + Blob::MakeValidBlobType(type); + return CreateSlice((uint64_t)start, (uint64_t)(end - start), type, aRv); +} + +nsresult BlobImpl::GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) { + MOZ_ASSERT(aContentLength); + + ErrorResult rv; + + nsCOMPtr<nsIInputStream> stream; + CreateInputStream(getter_AddRefs(stream), rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + *aContentLength = GetSize(rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + nsAutoString contentType; + GetType(contentType); + + if (contentType.IsEmpty()) { + aContentType.SetIsVoid(true); + } else { + CopyUTF16toUTF8(contentType, aContentType); + } + + aCharset.Truncate(); + + stream.forget(aBody); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(BlobImpl, BlobImpl) + +} // namespace mozilla::dom diff --git a/dom/file/BlobImpl.h b/dom/file/BlobImpl.h new file mode 100644 index 0000000000..720c398cdd --- /dev/null +++ b/dom/file/BlobImpl.h @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_BlobImpl_h +#define mozilla_dom_BlobImpl_h + +#include "nsISupports.h" +#include "nsString.h" + +#define BLOBIMPL_IID \ + { \ + 0xbccb3275, 0x6778, 0x4ac5, { \ + 0xaf, 0x03, 0x90, 0xed, 0x37, 0xad, 0xdf, 0x5d \ + } \ + } + +class nsIInputStream; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class SystemCallerGuarantee; +template <typename T> +class Optional; + +// This is the abstract class for any File backend. It must be nsISupports +// because this class must be ref-counted and it has to work with IPC. +class BlobImpl : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(BLOBIMPL_IID) + NS_DECL_THREADSAFE_ISUPPORTS + + BlobImpl() = default; + + virtual void GetName(nsAString& aName) const = 0; + + virtual void GetDOMPath(nsAString& aName) const = 0; + + virtual void SetDOMPath(const nsAString& aName) = 0; + + virtual int64_t GetLastModified(ErrorResult& aRv) = 0; + + virtual void GetMozFullPath(nsAString& aName, + SystemCallerGuarantee /* unused */, + ErrorResult& aRv) = 0; + + virtual void GetMozFullPathInternal(nsAString& aFileName, + ErrorResult& aRv) = 0; + + virtual uint64_t GetSize(ErrorResult& aRv) = 0; + + virtual void GetType(nsAString& aType) = 0; + + virtual void GetBlobImplType(nsAString& aBlobImplType) const = 0; + + virtual size_t GetAllocationSize() const = 0; + virtual size_t GetAllocationSize( + FallibleTArray<BlobImpl*>& aVisitedBlobImpls) const = 0; + + /** + * An effectively-unique serial number identifying this instance of FileImpl. + * + * Implementations should obtain a serial number from + * FileImplBase::NextSerialNumber(). + */ + virtual uint64_t GetSerialNumber() const = 0; + + already_AddRefed<BlobImpl> Slice(const Optional<int64_t>& aStart, + const Optional<int64_t>& aEnd, + const nsAString& aContentType, + ErrorResult& aRv); + + virtual already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, + uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const = 0; + + virtual const nsTArray<RefPtr<BlobImpl>>* GetSubBlobImpls() const = 0; + + virtual void CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const = 0; + + virtual int64_t GetFileId() const = 0; + + nsresult GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset); + + virtual void SetLazyData(const nsAString& aName, + const nsAString& aContentType, uint64_t aLength, + int64_t aLastModifiedDate) = 0; + + virtual bool IsMemoryFile() const = 0; + + virtual bool IsFile() const = 0; + + // Returns true if the BlobImpl is backed by an nsIFile and the underlying + // file is a directory. + virtual bool IsDirectory() const { return false; } + + protected: + virtual ~BlobImpl() = default; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(BlobImpl, BLOBIMPL_IID) + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_BlobImpl_h diff --git a/dom/file/BlobSet.cpp b/dom/file/BlobSet.cpp new file mode 100644 index 0000000000..6ae1774296 --- /dev/null +++ b/dom/file/BlobSet.cpp @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/BlobSet.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/dom/File.h" +#include "MemoryBlobImpl.h" +#include "MultipartBlobImpl.h" +#include "StringBlobImpl.h" + +namespace mozilla::dom { + +nsresult BlobSet::AppendVoidPtr(const void* aData, uint32_t aLength) { + NS_ENSURE_ARG_POINTER(aData); + if (!aLength) { + return NS_OK; + } + + void* data = malloc(aLength); + if (!data) { + return NS_ERROR_OUT_OF_MEMORY; + } + + memcpy((char*)data, aData, aLength); + + RefPtr<BlobImpl> blobImpl = new MemoryBlobImpl(data, aLength, u""_ns); + return AppendBlobImpl(blobImpl); +} + +nsresult BlobSet::AppendUTF8String(const nsACString& aUTF8String, + bool nativeEOL) { + nsCString utf8Str; + if (NS_WARN_IF(!utf8Str.Assign(aUTF8String, mozilla::fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (nativeEOL) { + if (utf8Str.Contains('\r')) { + if (NS_WARN_IF( + !utf8Str.ReplaceSubstring("\r\n", "\n", mozilla::fallible) || + !utf8Str.ReplaceSubstring("\r", "\n", mozilla::fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + } +#ifdef XP_WIN + if (NS_WARN_IF( + !utf8Str.ReplaceSubstring("\n", "\r\n", mozilla::fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } +#endif + } + + RefPtr<StringBlobImpl> blobImpl = StringBlobImpl::Create(utf8Str, u""_ns); + return AppendBlobImpl(blobImpl); +} + +nsresult BlobSet::AppendBlobImpl(BlobImpl* aBlobImpl) { + NS_ENSURE_ARG_POINTER(aBlobImpl); + + // If aBlobImpl is a MultipartBlobImpl, let's append the sub-blobImpls + // instead. + const nsTArray<RefPtr<BlobImpl>>* subBlobs = aBlobImpl->GetSubBlobImpls(); + if (subBlobs) { + for (BlobImpl* subBlob : *subBlobs) { + nsresult rv = AppendBlobImpl(subBlob); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; + } + + if (NS_WARN_IF(!mBlobImpls.AppendElement(aBlobImpl, fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/file/BlobSet.h b/dom/file/BlobSet.h new file mode 100644 index 0000000000..43955c4c88 --- /dev/null +++ b/dom/file/BlobSet.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_BlobSet_h +#define mozilla_dom_BlobSet_h + +#include "jsapi.h" +#include "mozilla/RefPtr.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +class BlobImpl; + +class BlobSet final { + public: + [[nodiscard]] nsresult AppendVoidPtr(const void* aData, uint32_t aLength); + + [[nodiscard]] nsresult AppendUTF8String(const nsACString& aUTF8String, + bool nativeEOL); + + [[nodiscard]] nsresult AppendBlobImpl(BlobImpl* aBlobImpl); + + FallibleTArray<RefPtr<BlobImpl>>& GetBlobImpls() { return mBlobImpls; } + + private: + FallibleTArray<RefPtr<BlobImpl>> mBlobImpls; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_BlobSet_h diff --git a/dom/file/EmptyBlobImpl.cpp b/dom/file/EmptyBlobImpl.cpp new file mode 100644 index 0000000000..5fa1055b4d --- /dev/null +++ b/dom/file/EmptyBlobImpl.cpp @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "EmptyBlobImpl.h" +#include "nsStringStream.h" + +namespace mozilla::dom { + +already_AddRefed<BlobImpl> EmptyBlobImpl::CreateSlice( + uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + ErrorResult& aRv) const { + MOZ_ASSERT(!aStart && !aLength); + RefPtr<BlobImpl> impl = new EmptyBlobImpl(aContentType); + return impl.forget(); +} + +void EmptyBlobImpl::CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const { + if (NS_WARN_IF(!aStream)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsresult rv = NS_NewCStringInputStream(aStream, ""_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } +} + +} // namespace mozilla::dom diff --git a/dom/file/EmptyBlobImpl.h b/dom/file/EmptyBlobImpl.h new file mode 100644 index 0000000000..37dd39da18 --- /dev/null +++ b/dom/file/EmptyBlobImpl.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_EmptyBlobImpl_h +#define mozilla_dom_EmptyBlobImpl_h + +#include "BaseBlobImpl.h" + +namespace mozilla::dom { + +class EmptyBlobImpl final : public BaseBlobImpl { + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(EmptyBlobImpl, BaseBlobImpl) + + // Blob constructor. + explicit EmptyBlobImpl(const nsAString& aContentType) + : BaseBlobImpl(aContentType, 0 /* aLength */) {} + + void CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const override; + + already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const override; + + bool IsMemoryFile() const override { return true; } + + void GetBlobImplType(nsAString& aBlobImplType) const override { + aBlobImplType = u"EmptyBlobImpl"_ns; + } + + private: + ~EmptyBlobImpl() override = default; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_EmptyBlobImpl_h diff --git a/dom/file/File.cpp b/dom/file/File.cpp new file mode 100644 index 0000000000..9995d35c52 --- /dev/null +++ b/dom/file/File.cpp @@ -0,0 +1,202 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "File.h" +#include "FileBlobImpl.h" +#include "MemoryBlobImpl.h" +#include "MultipartBlobImpl.h" +#include "mozilla/dom/BlobBinding.h" +#include "mozilla/dom/FileBinding.h" +#include "mozilla/dom/FileCreatorHelper.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "mozilla/dom/Promise.h" +#include "nsIFile.h" +#include "nsContentUtils.h" +#include "nsXULAppAPI.h" + +namespace mozilla::dom { + +File::File(nsIGlobalObject* aGlobal, BlobImpl* aImpl) : Blob(aGlobal, aImpl) { + MOZ_ASSERT(aImpl->IsFile()); +} + +File::~File() = default; + +/* static */ +File* File::Create(nsIGlobalObject* aGlobal, BlobImpl* aImpl) { + MOZ_ASSERT(aImpl); + MOZ_ASSERT(aImpl->IsFile()); + + MOZ_ASSERT(aGlobal); + if (NS_WARN_IF(!aGlobal)) { + return nullptr; + } + + return new File(aGlobal, aImpl); +} + +/* static */ +already_AddRefed<File> File::CreateMemoryFileWithCustomLastModified( + nsIGlobalObject* aGlobal, void* aMemoryBuffer, uint64_t aLength, + const nsAString& aName, const nsAString& aContentType, + int64_t aLastModifiedDate) { + RefPtr<MemoryBlobImpl> blobImpl = + MemoryBlobImpl::CreateWithCustomLastModified( + aMemoryBuffer, aLength, aName, aContentType, aLastModifiedDate); + MOZ_ASSERT(blobImpl); + + RefPtr<File> file = File::Create(aGlobal, blobImpl); + return file.forget(); +} + +/* static */ +already_AddRefed<File> File::CreateMemoryFileWithLastModifiedNow( + nsIGlobalObject* aGlobal, void* aMemoryBuffer, uint64_t aLength, + const nsAString& aName, const nsAString& aContentType) { + MOZ_ASSERT(aGlobal); + + RefPtr<MemoryBlobImpl> blobImpl = MemoryBlobImpl::CreateWithLastModifiedNow( + aMemoryBuffer, aLength, aName, aContentType, aGlobal->GetRTPCallerType()); + MOZ_ASSERT(blobImpl); + + RefPtr<File> file = File::Create(aGlobal, blobImpl); + return file.forget(); +} + +/* static */ +already_AddRefed<File> File::CreateFromFile(nsIGlobalObject* aGlobal, + nsIFile* aFile) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + MOZ_ASSERT(aGlobal); + if (NS_WARN_IF(!aGlobal)) { + return nullptr; + } + + RefPtr<File> file = new File(aGlobal, new FileBlobImpl(aFile)); + return file.forget(); +} + +/* static */ +already_AddRefed<File> File::CreateFromFile(nsIGlobalObject* aGlobal, + nsIFile* aFile, + const nsAString& aName, + const nsAString& aContentType) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + MOZ_ASSERT(aGlobal); + if (NS_WARN_IF(!aGlobal)) { + return nullptr; + } + + RefPtr<File> file = + new File(aGlobal, new FileBlobImpl(aFile, aName, aContentType)); + return file.forget(); +} + +JSObject* File::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return File_Binding::Wrap(aCx, this, aGivenProto); +} + +void File::GetName(nsAString& aFileName) const { mImpl->GetName(aFileName); } + +void File::GetRelativePath(nsAString& aPath) const { + aPath.Truncate(); + + nsAutoString path; + mImpl->GetDOMPath(path); + + // WebkitRelativePath doesn't start with '/' + if (!path.IsEmpty()) { + MOZ_ASSERT(path[0] == FILESYSTEM_DOM_PATH_SEPARATOR_CHAR); + aPath.Assign(Substring(path, 1)); + } +} + +int64_t File::GetLastModified(ErrorResult& aRv) { + return mImpl->GetLastModified(aRv); +} + +void File::GetMozFullPath(nsAString& aFilename, + SystemCallerGuarantee aGuarantee, ErrorResult& aRv) { + mImpl->GetMozFullPath(aFilename, aGuarantee, aRv); +} + +void File::GetMozFullPathInternal(nsAString& aFileName, ErrorResult& aRv) { + mImpl->GetMozFullPathInternal(aFileName, aRv); +} + +/* static */ +already_AddRefed<File> File::Constructor(const GlobalObject& aGlobal, + const Sequence<BlobPart>& aData, + const nsAString& aName, + const FilePropertyBag& aBag, + ErrorResult& aRv) { + RefPtr<MultipartBlobImpl> impl = new MultipartBlobImpl(aName); + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + MOZ_ASSERT(global); + + nsAutoString type(aBag.mType); + MakeValidBlobType(type); + impl->InitializeBlob(aData, type, aBag.mEndings == EndingType::Native, + global->GetRTPCallerType(), aRv); + if (aRv.Failed()) { + return nullptr; + } + MOZ_ASSERT(impl->IsFile()); + + if (aBag.mLastModified.WasPassed()) { + impl->SetLastModified(aBag.mLastModified.Value()); + } + + RefPtr<File> file = new File(global, impl); + return file.forget(); +} + +/* static */ +already_AddRefed<Promise> File::CreateFromNsIFile( + const GlobalObject& aGlobal, nsIFile* aData, + const ChromeFilePropertyBag& aBag, SystemCallerGuarantee aGuarantee, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + + MOZ_ASSERT(global); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<Promise> promise = + FileCreatorHelper::CreateFile(global, aData, aBag, true, aRv); + return promise.forget(); +} + +/* static */ +already_AddRefed<Promise> File::CreateFromFileName( + const GlobalObject& aGlobal, const nsAString& aPath, + const ChromeFilePropertyBag& aBag, SystemCallerGuarantee aGuarantee, + ErrorResult& aRv) { + nsCOMPtr<nsIFile> file; + aRv = NS_NewLocalFile(aPath, false, getter_AddRefs(file)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + + MOZ_ASSERT(global); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<Promise> promise = + FileCreatorHelper::CreateFile(global, file, aBag, false, aRv); + return promise.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/file/File.h b/dom/file/File.h new file mode 100644 index 0000000000..279b91dd93 --- /dev/null +++ b/dom/file/File.h @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_File_h +#define mozilla_dom_File_h + +#include "mozilla/dom/Blob.h" + +class nsIFile; + +namespace mozilla::dom { + +struct ChromeFilePropertyBag; +struct FilePropertyBag; +class Promise; + +class File final : public Blob { + friend class Blob; + + public: + // Note: BlobImpl must be a File in order to use this method. + // Check impl->IsFile(). + static File* Create(nsIGlobalObject* aGlobal, BlobImpl* aImpl); + + // The returned File takes ownership of aMemoryBuffer. aMemoryBuffer will be + // freed by free so it must be allocated by malloc or something + // compatible with it. + static already_AddRefed<File> CreateMemoryFileWithLastModifiedNow( + nsIGlobalObject* aGlobal, void* aMemoryBuffer, uint64_t aLength, + const nsAString& aName, const nsAString& aContentType); + + // You should not use this method! Please consider to use the + // CreateMemoryFileWithLastModifiedNow. + static already_AddRefed<File> CreateMemoryFileWithCustomLastModified( + nsIGlobalObject* aGlobal, void* aMemoryBuffer, uint64_t aLength, + const nsAString& aName, const nsAString& aContentType, + int64_t aLastModifiedDate); + + // This method creates a BlobFileImpl for the new File object. This is + // thread-safe, cross-process, cross-thread as any other BlobImpl, but, when + // GetType() is called, it must dispatch a runnable to the main-thread in + // order to use nsIMIMEService. + // Would be nice if we try to avoid to use this method outside the + // main-thread to avoid extra runnables. + static already_AddRefed<File> CreateFromFile(nsIGlobalObject* aGlobal, + nsIFile* aFile); + + static already_AddRefed<File> CreateFromFile(nsIGlobalObject* aGlobal, + nsIFile* aFile, + const nsAString& aName, + const nsAString& aContentType); + + // WebIDL methods + + JSObject* WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) override; + + // File constructor + static already_AddRefed<File> Constructor(const GlobalObject& aGlobal, + const Sequence<BlobPart>& aData, + const nsAString& aName, + const FilePropertyBag& aBag, + ErrorResult& aRv); + + // ChromeOnly + static already_AddRefed<Promise> CreateFromFileName( + const GlobalObject& aGlobal, const nsAString& aPath, + const ChromeFilePropertyBag& aBag, SystemCallerGuarantee aGuarantee, + ErrorResult& aRv); + + // ChromeOnly + static already_AddRefed<Promise> CreateFromNsIFile( + const GlobalObject& aGlobal, nsIFile* aData, + const ChromeFilePropertyBag& aBag, SystemCallerGuarantee aGuarantee, + ErrorResult& aRv); + + void GetName(nsAString& aFileName) const; + + int64_t GetLastModified(ErrorResult& aRv); + + void GetRelativePath(nsAString& aPath) const; + + void GetMozFullPath(nsAString& aFilename, SystemCallerGuarantee aGuarantee, + ErrorResult& aRv); + + void GetMozFullPathInternal(nsAString& aFileName, ErrorResult& aRv); + + protected: + bool HasFileInterface() const override { return true; } + + private: + // File constructor should never be used directly. Use Blob::Create or + // File::Create. + File(nsIGlobalObject* aGlobal, BlobImpl* aImpl); + ~File() override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_File_h diff --git a/dom/file/FileBlobImpl.cpp b/dom/file/FileBlobImpl.cpp new file mode 100644 index 0000000000..af80476e1a --- /dev/null +++ b/dom/file/FileBlobImpl.cpp @@ -0,0 +1,294 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileBlobImpl.h" +#include "BaseBlobImpl.h" +#include "mozilla/SlicedInputStream.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "nsCExternalHandlerService.h" +#include "nsIFile.h" +#include "nsIFileStreams.h" +#include "nsIMIMEService.h" +#include "nsNetUtil.h" +#include "nsStreamUtils.h" + +namespace mozilla::dom { + +FileBlobImpl::FileBlobImpl(nsIFile* aFile) + : mMutex("FileBlobImpl::mMutex"), + mFile(aFile), + mSerialNumber(BaseBlobImpl::NextSerialNumber()), + mStart(0), + mFileId(-1), + mIsFile(true), + mWholeFile(true) { + MOZ_ASSERT(mFile, "must have file"); + MOZ_ASSERT(XRE_IsParentProcess()); + // Lazily get the content type and size + mContentType.SetIsVoid(true); + mMozFullPath.SetIsVoid(true); + mFile->GetLeafName(mName); +} + +FileBlobImpl::FileBlobImpl(const nsAString& aName, + const nsAString& aContentType, uint64_t aLength, + nsIFile* aFile) + : mMutex("FileBlobImpl::mMutex"), + mFile(aFile), + mContentType(aContentType), + mName(aName), + mSerialNumber(BaseBlobImpl::NextSerialNumber()), + mStart(0), + mFileId(-1), + mLength(Some(aLength)), + mIsFile(true), + mWholeFile(true) { + MOZ_ASSERT(mFile, "must have file"); + MOZ_ASSERT(XRE_IsParentProcess()); + mMozFullPath.SetIsVoid(true); +} + +FileBlobImpl::FileBlobImpl(const nsAString& aName, + const nsAString& aContentType, uint64_t aLength, + nsIFile* aFile, int64_t aLastModificationDate) + : mMutex("FileBlobImpl::mMutex"), + mFile(aFile), + mContentType(aContentType), + mName(aName), + mSerialNumber(BaseBlobImpl::NextSerialNumber()), + mStart(0), + mFileId(-1), + mLength(Some(aLength)), + mLastModified(Some(aLastModificationDate)), + mIsFile(true), + mWholeFile(true) { + MOZ_ASSERT(mFile, "must have file"); + MOZ_ASSERT(XRE_IsParentProcess()); + mMozFullPath.SetIsVoid(true); +} + +FileBlobImpl::FileBlobImpl(nsIFile* aFile, const nsAString& aName, + const nsAString& aContentType) + : mMutex("FileBlobImpl::mMutex"), + mFile(aFile), + mContentType(aContentType), + mName(aName), + mSerialNumber(BaseBlobImpl::NextSerialNumber()), + mStart(0), + mFileId(-1), + mIsFile(true), + mWholeFile(true) { + MOZ_ASSERT(mFile, "must have file"); + MOZ_ASSERT(XRE_IsParentProcess()); + if (aContentType.IsEmpty()) { + // Lazily get the content type and size + mContentType.SetIsVoid(true); + } + + mMozFullPath.SetIsVoid(true); +} + +FileBlobImpl::FileBlobImpl(const FileBlobImpl* aOther, uint64_t aStart, + uint64_t aLength, const nsAString& aContentType) + : mMutex("FileBlobImpl::mMutex"), + mFile(aOther->mFile), + mContentType(aContentType), + mSerialNumber(BaseBlobImpl::NextSerialNumber()), + mStart(aOther->mStart + aStart), + mFileId(-1), + mLength(Some(aLength)), + mIsFile(false), + mWholeFile(false) { + MOZ_ASSERT(mFile, "must have file"); + MOZ_ASSERT(XRE_IsParentProcess()); + mMozFullPath = aOther->mMozFullPath; +} + +already_AddRefed<BlobImpl> FileBlobImpl::CreateSlice( + uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + ErrorResult& aRv) const { + RefPtr<FileBlobImpl> impl = + new FileBlobImpl(this, aStart, aLength, aContentType); + return impl.forget(); +} + +void FileBlobImpl::GetMozFullPathInternal(nsAString& aFilename, + ErrorResult& aRv) { + MOZ_ASSERT(mIsFile, "Should only be called on files"); + + MutexAutoLock lock(mMutex); + + if (!mMozFullPath.IsVoid()) { + aFilename = mMozFullPath; + return; + } + + aRv = mFile->GetPath(aFilename); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + mMozFullPath = aFilename; +} + +uint64_t FileBlobImpl::GetSize(ErrorResult& aRv) { + MutexAutoLock lock(mMutex); + + if (mLength.isNothing()) { + MOZ_ASSERT(mWholeFile, + "Should only use lazy size when using the whole file"); + int64_t fileSize; + aRv = mFile->GetFileSize(&fileSize); + if (NS_WARN_IF(aRv.Failed())) { + return 0; + } + + if (fileSize < 0) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + + mLength.emplace(fileSize); + } + + return mLength.value(); +} + +class FileBlobImpl::GetTypeRunnable final : public WorkerMainThreadRunnable { + public: + GetTypeRunnable(WorkerPrivate* aWorkerPrivate, FileBlobImpl* aBlobImpl, + const MutexAutoLock& aProofOfLock) + : WorkerMainThreadRunnable(aWorkerPrivate, "FileBlobImpl :: GetType"_ns), + mBlobImpl(aBlobImpl), + mProofOfLock(aProofOfLock) { + MOZ_ASSERT(aBlobImpl); + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + bool MainThreadRun() override { + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoString type; + mBlobImpl->GetTypeInternal(type, mProofOfLock); + return true; + } + + private: + ~GetTypeRunnable() override = default; + + RefPtr<FileBlobImpl> mBlobImpl; + const MutexAutoLock& mProofOfLock; +}; + +void FileBlobImpl::GetType(nsAString& aType) { + MutexAutoLock lock(mMutex); + GetTypeInternal(aType, lock); +} + +void FileBlobImpl::GetTypeInternal(nsAString& aType, + const MutexAutoLock& aProofOfLock) { + aType.Truncate(); + + if (mContentType.IsVoid()) { + MOZ_ASSERT(mWholeFile, + "Should only use lazy ContentType when using the whole file"); + + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + if (!workerPrivate) { + // I have no idea in which thread this method is called. We cannot + // return any valid value. + return; + } + + RefPtr<GetTypeRunnable> runnable = + new GetTypeRunnable(workerPrivate, this, aProofOfLock); + + ErrorResult rv; + runnable->Dispatch(Canceling, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return; + } + } else { + nsresult rv; + nsCOMPtr<nsIMIMEService> mimeService = + do_GetService(NS_MIMESERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsAutoCString mimeType; + rv = mimeService->GetTypeFromFile(mFile, mimeType); + if (NS_FAILED(rv)) { + mimeType.Truncate(); + } + + AppendUTF8toUTF16(mimeType, mContentType); + mContentType.SetIsVoid(false); + } + } + + aType = mContentType; +} + +void FileBlobImpl::GetBlobImplType(nsAString& aBlobImplType) const { + aBlobImplType = u"FileBlobImpl"_ns; +} + +int64_t FileBlobImpl::GetLastModified(ErrorResult& aRv) { + MOZ_ASSERT(mIsFile, "Should only be called on files"); + + MutexAutoLock lock(mMutex); + + if (mLastModified.isNothing()) { + PRTime msecs; + aRv = mFile->GetLastModifiedTime(&msecs); + if (NS_WARN_IF(aRv.Failed())) { + return 0; + } + + mLastModified.emplace(int64_t(msecs)); + } + + return mLastModified.value(); +} + +const uint32_t sFileStreamFlags = + nsIFileInputStream::CLOSE_ON_EOF | nsIFileInputStream::REOPEN_ON_REWIND | + nsIFileInputStream::DEFER_OPEN | nsIFileInputStream::SHARE_DELETE; + +void FileBlobImpl::CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const { + nsCOMPtr<nsIInputStream> stream; + aRv = NS_NewLocalFileInputStream(getter_AddRefs(stream), mFile, -1, -1, + sFileStreamFlags); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (mWholeFile) { + stream.forget(aStream); + return; + } + + MOZ_ASSERT(mLength.isSome()); + + RefPtr<SlicedInputStream> slicedInputStream = + new SlicedInputStream(stream.forget(), mStart, mLength.value()); + slicedInputStream.forget(aStream); +} + +bool FileBlobImpl::IsDirectory() const { + bool isDirectory = false; + if (mFile) { + mFile->IsDirectory(&isDirectory); + } + return isDirectory; +} + +} // namespace mozilla::dom diff --git a/dom/file/FileBlobImpl.h b/dom/file/FileBlobImpl.h new file mode 100644 index 0000000000..84565dbd4b --- /dev/null +++ b/dom/file/FileBlobImpl.h @@ -0,0 +1,157 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileBlobImpl_h +#define mozilla_dom_FileBlobImpl_h + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/Maybe.h" +#include "mozilla/Mutex.h" +#include "nsCOMPtr.h" + +class nsIFile; + +namespace mozilla::dom { + +class FileBlobImpl : public BlobImpl { + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(FileBlobImpl, BlobImpl) + + // Create as a file + explicit FileBlobImpl(nsIFile* aFile); + + // Create as a file + FileBlobImpl(const nsAString& aName, const nsAString& aContentType, + uint64_t aLength, nsIFile* aFile); + + FileBlobImpl(const nsAString& aName, const nsAString& aContentType, + uint64_t aLength, nsIFile* aFile, int64_t aLastModificationDate); + + // Create as a file with custom name + FileBlobImpl(nsIFile* aFile, const nsAString& aName, + const nsAString& aContentType); + + void GetName(nsAString& aName) const override { + MOZ_ASSERT(mIsFile, "Should only be called on files"); + aName = mName; + } + + void SetName(const nsAString& aName) { mName = aName; } + + void GetDOMPath(nsAString& aPath) const override { + MOZ_ASSERT(mIsFile, "Should only be called on files"); + aPath = mPath; + } + + void SetDOMPath(const nsAString& aPath) override { + MOZ_ASSERT(mIsFile, "Should only be called on files"); + mPath = aPath; + } + + int64_t GetLastModified(ErrorResult& aRv) override; + + void GetMozFullPath(nsAString& aFileName, SystemCallerGuarantee /* unused */, + ErrorResult& aRv) override { + MOZ_ASSERT(mIsFile, "Should only be called on files"); + + GetMozFullPathInternal(aFileName, aRv); + } + + void GetMozFullPathInternal(nsAString& aFilename, ErrorResult& aRv) override; + + uint64_t GetSize(ErrorResult& aRv) override; + + void GetType(nsAString& aType) override; + + void GetBlobImplType(nsAString& aBlobImplType) const override; + + size_t GetAllocationSize() const override { return 0; } + + size_t GetAllocationSize( + FallibleTArray<BlobImpl*>& aVisitedBlobImpls) const override { + return GetAllocationSize(); + } + + uint64_t GetSerialNumber() const override { return mSerialNumber; } + + const nsTArray<RefPtr<BlobImpl>>* GetSubBlobImpls() const override { + return nullptr; + } + + void CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const override; + + int64_t GetFileId() const override { return mFileId; } + + void SetLazyData(const nsAString& aName, const nsAString& aContentType, + uint64_t aLength, int64_t aLastModifiedDate) override { + mName = aName; + mContentType = aContentType; + mIsFile = !aName.IsVoid(); + mLength.emplace(aLength); + mLastModified.emplace(aLastModifiedDate); + } + + bool IsMemoryFile() const override { return false; } + + bool IsFile() const override { return mIsFile; } + + bool IsDirectory() const override; + + void SetType(const nsAString& aType) { mContentType = aType; } + + void SetFileId(int64_t aFileId) { mFileId = aFileId; } + + void SetEmptySize() { mLength.emplace(0); } + + void SetMozFullPath(const nsAString& aPath) { mMozFullPath = aPath; } + + void SetLastModified(int64_t aLastModified) { + mLastModified.emplace(aLastModified); + } + + protected: + ~FileBlobImpl() override = default; + + // Create slice + FileBlobImpl(const FileBlobImpl* aOther, uint64_t aStart, uint64_t aLength, + const nsAString& aContentType); + + already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const override; + + class GetTypeRunnable; + void GetTypeInternal(nsAString& aType, const MutexAutoLock& aProofOfLock); + + // FileBlobImpl has getter methods with lazy initialization. Because any + // BlobImpl must work thread-safe, we use a mutex. + Mutex mMutex MOZ_UNANNOTATED; + + nsCOMPtr<nsIFile> mFile; + + nsString mContentType; + nsString mName; + nsString mPath; // The path relative to a directory chosen by the user + nsString mMozFullPath; + + const uint64_t mSerialNumber; + uint64_t mStart; + + int64_t mFileId; + + Maybe<uint64_t> mLength; + + Maybe<int64_t> mLastModified; + + bool mIsFile; + bool mWholeFile; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileBlobImpl_h diff --git a/dom/file/FileCreatorHelper.cpp b/dom/file/FileCreatorHelper.cpp new file mode 100644 index 0000000000..2c4ee4c041 --- /dev/null +++ b/dom/file/FileCreatorHelper.cpp @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileCreatorHelper.h" + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/FileBinding.h" +#include "mozilla/dom/FileCreatorChild.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/Promise.h" +#include "nsContentUtils.h" +#include "nsPIDOMWindow.h" +#include "nsProxyRelease.h" +#include "nsIFile.h" + +// Undefine the macro of CreateFile to avoid FileCreatorHelper#CreateFile being +// replaced by FileCreatorHelper#CreateFileW. +#ifdef CreateFile +# undef CreateFile +#endif + +namespace mozilla::dom { + +/* static */ +already_AddRefed<Promise> FileCreatorHelper::CreateFile( + nsIGlobalObject* aGlobalObject, nsIFile* aFile, + const ChromeFilePropertyBag& aBag, bool aIsFromNsIFile, ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + RefPtr<Promise> promise = Promise::Create(aGlobalObject, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsAutoString path; + aRv = aFile->GetPath(path); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // Register this component to PBackground. + mozilla::ipc::PBackgroundChild* actorChild = + mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actorChild)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + Maybe<int64_t> lastModified; + if (aBag.mLastModified.WasPassed()) { + lastModified.emplace(aBag.mLastModified.Value()); + } + + PFileCreatorChild* actor = actorChild->SendPFileCreatorConstructor( + path, aBag.mType, aBag.mName, lastModified, aBag.mExistenceCheck, + aIsFromNsIFile); + + static_cast<FileCreatorChild*>(actor)->SetPromise(promise); + return promise.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/file/FileCreatorHelper.h b/dom/file/FileCreatorHelper.h new file mode 100644 index 0000000000..f0b666cc58 --- /dev/null +++ b/dom/file/FileCreatorHelper.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileCreatorHelper_h +#define mozilla_dom_FileCreatorHelper_h + +#include "mozilla/Attributes.h" +#include "mozilla/RefPtr.h" +#include "nsISupportsImpl.h" + +// Undefine the macro of CreateFile to avoid FileCreatorHelper#CreateFile being +// replaced by FileCreatorHelper#CreateFileW. +#ifdef CreateFile +# undef CreateFile +#endif + +class nsIFile; +class nsIGlobalObject; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +struct ChromeFilePropertyBag; +class Promise; + +class FileCreatorHelper final { + public: + static already_AddRefed<Promise> CreateFile(nsIGlobalObject* aGlobalObject, + nsIFile* aFile, + const ChromeFilePropertyBag& aBag, + bool aIsFromNsIFile, + ErrorResult& aRv); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_FileCreatorHelper_h diff --git a/dom/file/FileList.cpp b/dom/file/FileList.cpp new file mode 100644 index 0000000000..2cc7b0c7f1 --- /dev/null +++ b/dom/file/FileList.cpp @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/FileList.h" + +#include <new> +#include <utility> +#include "ErrorList.h" +#include "js/RootingAPI.h" +#include "mozilla/Assertions.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FileListBinding.h" +#include "mozilla/fallible.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCycleCollectionTraversalCallback.h" +#include "nsISupports.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FileList, mFiles, mParent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileList) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(FileList) +NS_IMPL_CYCLE_COLLECTING_RELEASE(FileList) + +FileList::FileList(nsISupports* aParent) : mParent(aParent) {} + +FileList::~FileList() = default; + +JSObject* FileList::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return mozilla::dom::FileList_Binding::Wrap(aCx, this, aGivenProto); +} + +bool FileList::Append(File* aFile) { + MOZ_ASSERT(aFile); + return mFiles.AppendElement(aFile, fallible); +} + +bool FileList::Remove(uint32_t aIndex) { + if (aIndex < mFiles.Length()) { + mFiles.RemoveElementAt(aIndex); + return true; + } + + return false; +} + +void FileList::Clear() { return mFiles.Clear(); } + +File* FileList::Item(uint32_t aIndex) const { + if (aIndex >= mFiles.Length()) { + return nullptr; + } + + return mFiles[aIndex]; +} + +File* FileList::IndexedGetter(uint32_t aIndex, bool& aFound) const { + aFound = aIndex < mFiles.Length(); + return Item(aIndex); +} + +void FileList::ToSequence(Sequence<RefPtr<File>>& aSequence, + ErrorResult& aRv) const { + MOZ_ASSERT(aSequence.IsEmpty()); + if (mFiles.IsEmpty()) { + return; + } + + if (!aSequence.SetLength(mFiles.Length(), mozilla::fallible_t())) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (uint32_t i = 0; i < mFiles.Length(); ++i) { + aSequence[i] = mFiles[i]; + } +} + +} // namespace mozilla::dom diff --git a/dom/file/FileList.h b/dom/file/FileList.h new file mode 100644 index 0000000000..f36b25ba07 --- /dev/null +++ b/dom/file/FileList.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileList_h +#define mozilla_dom_FileList_h + +#include <cstdint> +#include "js/TypeDecls.h" +#include "mozilla/Assertions.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" + +class nsCycleCollectionTraversalCallback; +template <class T> +class RefPtr; + +namespace mozilla { +class ErrorResult; +namespace dom { + +class BlobImpls; +class File; +template <typename T> +class Sequence; + +class FileList final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileList) + + explicit FileList(nsISupports* aParent); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsISupports* GetParentObject() { return mParent; } + + bool Append(File* aFile); + + bool Remove(uint32_t aIndex); + + void Clear(); + + File* Item(uint32_t aIndex) const; + + File* IndexedGetter(uint32_t aIndex, bool& aFound) const; + + uint32_t Length() const { return mFiles.Length(); } + + void ToSequence(Sequence<RefPtr<File>>& aSequence, ErrorResult& aRv) const; + + private: + ~FileList(); + + FallibleTArray<RefPtr<File>> mFiles; + nsCOMPtr<nsISupports> mParent; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_FileList_h diff --git a/dom/file/FileReader.cpp b/dom/file/FileReader.cpp new file mode 100644 index 0000000000..93948671df --- /dev/null +++ b/dom/file/FileReader.cpp @@ -0,0 +1,803 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileReader.h" + +#include "nsIGlobalObject.h" +#include "nsITimer.h" + +#include "js/ArrayBuffer.h" // JS::NewArrayBufferWithContents +#include "mozilla/Base64.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/DOMExceptionBinding.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FileReaderBinding.h" +#include "mozilla/dom/ProgressEvent.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/Encoding.h" +#include "mozilla/HoldDropJSObjects.h" +#include "nsAlgorithm.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDOMJSUtils.h" +#include "nsError.h" +#include "nsNetUtil.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "xpcpublic.h" +#include "nsReadableUtils.h" + +namespace mozilla::dom { + +#define ABORT_STR u"abort" +#define LOAD_STR u"load" +#define LOADSTART_STR u"loadstart" +#define LOADEND_STR u"loadend" +#define ERROR_STR u"error" +#define PROGRESS_STR u"progress" + +const uint64_t kUnknownSize = uint64_t(-1); + +NS_IMPL_CYCLE_COLLECTION_CLASS(FileReader) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(FileReader, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBlob) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mProgressNotifier) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mError) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(FileReader, + DOMEventTargetHelper) + tmp->Shutdown(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mBlob) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mProgressNotifier) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mError) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(FileReader, DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mResultArrayBuffer) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileReader) + NS_INTERFACE_MAP_ENTRY_CONCRETE(FileReader) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsINamed) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(FileReader, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(FileReader, DOMEventTargetHelper) + +class MOZ_RAII FileReaderDecreaseBusyCounter { + RefPtr<FileReader> mFileReader; + + public: + explicit FileReaderDecreaseBusyCounter(FileReader* aFileReader) + : mFileReader(aFileReader) {} + + ~FileReaderDecreaseBusyCounter() { mFileReader->DecreaseBusyCounter(); } +}; + +class FileReader::AsyncWaitRunnable final : public CancelableRunnable { + public: + explicit AsyncWaitRunnable(FileReader* aReader) + : CancelableRunnable("FileReader::AsyncWaitRunnable"), mReader(aReader) {} + + NS_IMETHOD + Run() override { + if (mReader) { + mReader->InitialAsyncWait(); + } + return NS_OK; + } + + nsresult Cancel() override { + mReader = nullptr; + return NS_OK; + } + + public: + RefPtr<FileReader> mReader; +}; + +void FileReader::RootResultArrayBuffer() { mozilla::HoldJSObjects(this); } + +// FileReader constructors/initializers + +FileReader::FileReader(nsIGlobalObject* aGlobal, WeakWorkerRef* aWorkerRef) + : DOMEventTargetHelper(aGlobal), + mFileData(nullptr), + mDataLen(0), + mDataFormat(FILE_AS_BINARY), + mResultArrayBuffer(nullptr), + mProgressEventWasDelayed(false), + mTimerIsActive(false), + mReadyState(EMPTY), + mTotal(0), + mTransferred(0), + mBusyCount(0), + mWeakWorkerRef(aWorkerRef) { + MOZ_ASSERT(aGlobal); + MOZ_ASSERT_IF(NS_IsMainThread(), !mWeakWorkerRef); + + if (NS_IsMainThread()) { + mTarget = aGlobal->EventTargetFor(TaskCategory::Other); + } else { + mTarget = GetCurrentSerialEventTarget(); + } + + SetDOMStringToNull(mResult); +} + +FileReader::~FileReader() { + Shutdown(); + DropJSObjects(this); +} + +/* static */ +already_AddRefed<FileReader> FileReader::Constructor( + const GlobalObject& aGlobal) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<WeakWorkerRef> workerRef; + + if (!NS_IsMainThread()) { + JSContext* cx = aGlobal.Context(); + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); + + workerRef = WeakWorkerRef::Create(workerPrivate); + } + + RefPtr<FileReader> fileReader = new FileReader(global, workerRef); + + return fileReader.forget(); +} + +// nsIInterfaceRequestor + +NS_IMETHODIMP +FileReader::GetInterface(const nsIID& aIID, void** aResult) { + return QueryInterface(aIID, aResult); +} + +void FileReader::GetResult(JSContext* aCx, + Nullable<OwningStringOrArrayBuffer>& aResult) { + JS::Rooted<JS::Value> result(aCx); + + if (mDataFormat == FILE_AS_ARRAYBUFFER) { + if (mReadyState != DONE || !mResultArrayBuffer || + !aResult.SetValue().SetAsArrayBuffer().Init(mResultArrayBuffer)) { + aResult.SetNull(); + } + + return; + } + + if (mReadyState != DONE || mResult.IsVoid()) { + aResult.SetNull(); + return; + } + + aResult.SetValue().SetAsString() = mResult; +} + +void FileReader::OnLoadEndArrayBuffer() { + AutoJSAPI jsapi; + if (!jsapi.Init(GetParentObject())) { + FreeDataAndDispatchError(NS_ERROR_FAILURE); + return; + } + + RootResultArrayBuffer(); + + JSContext* cx = jsapi.cx(); + + mResultArrayBuffer = JS::NewArrayBufferWithContents(cx, mDataLen, mFileData); + if (mResultArrayBuffer) { + mFileData = nullptr; // Transfer ownership + FreeDataAndDispatchSuccess(); + return; + } + + // Let's handle the error status. + + JS::Rooted<JS::Value> exceptionValue(cx); + if (!JS_GetPendingException(cx, &exceptionValue) || + // This should not really happen, exception should always be an object. + !exceptionValue.isObject()) { + JS_ClearPendingException(jsapi.cx()); + FreeDataAndDispatchError(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS_ClearPendingException(jsapi.cx()); + + JS::Rooted<JSObject*> exceptionObject(cx, &exceptionValue.toObject()); + JSErrorReport* er = JS_ErrorFromException(cx, exceptionObject); + if (!er || er->message()) { + FreeDataAndDispatchError(NS_ERROR_OUT_OF_MEMORY); + return; + } + + nsAutoString errorName; + JSLinearString* name = js::GetErrorTypeName(cx, er->exnType); + if (name) { + AssignJSLinearString(errorName, name); + } + + nsAutoCString errorMsg(er->message().c_str()); + nsAutoCString errorNameC = NS_LossyConvertUTF16toASCII(errorName); + // XXX Code selected arbitrarily + mError = + new DOMException(NS_ERROR_DOM_INVALID_STATE_ERR, errorMsg, errorNameC, + DOMException_Binding::INVALID_STATE_ERR); + + FreeDataAndDispatchError(); +} + +nsresult FileReader::DoAsyncWait() { + nsresult rv = IncreaseBusyCounter(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mAsyncStream->AsyncWait(this, + /* aFlags*/ 0, + /* aRequestedCount */ 0, mTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + DecreaseBusyCounter(); + return rv; + } + + return NS_OK; +} + +namespace { + +void PopulateBufferForBinaryString(char16_t* aDest, const char* aSource, + uint32_t aCount) { + // Zero-extend each char to char16_t. + ConvertLatin1toUtf16(Span(aSource, aCount), Span(aDest, aCount)); +} + +nsresult ReadFuncBinaryString(nsIInputStream* aInputStream, void* aClosure, + const char* aFromRawSegment, uint32_t aToOffset, + uint32_t aCount, uint32_t* aWriteCount) { + char16_t* dest = static_cast<char16_t*>(aClosure) + aToOffset; + PopulateBufferForBinaryString(dest, aFromRawSegment, aCount); + *aWriteCount = aCount; + return NS_OK; +} + +} // namespace + +nsresult FileReader::DoReadData(uint64_t aCount) { + MOZ_ASSERT(mAsyncStream); + + uint32_t bytesRead = 0; + + if (mDataFormat == FILE_AS_BINARY) { + // Continuously update our binary string as data comes in + CheckedInt<uint64_t> size{mResult.Length()}; + size += aCount; + + if (!size.isValid() || size.value() > UINT32_MAX || size.value() > mTotal) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t lenBeforeRead = mResult.Length(); + MOZ_ASSERT(lenBeforeRead == mDataLen, "unexpected mResult length"); + + mResult.SetLength(lenBeforeRead + aCount); + char16_t* currentPos = mResult.BeginWriting() + lenBeforeRead; + + if (NS_InputStreamIsBuffered(mAsyncStream)) { + nsresult rv = mAsyncStream->ReadSegments(ReadFuncBinaryString, currentPos, + aCount, &bytesRead); + NS_ENSURE_SUCCESS(rv, NS_OK); + } else { + while (aCount > 0) { + char tmpBuffer[4096]; + uint32_t minCount = + XPCOM_MIN(aCount, static_cast<uint64_t>(sizeof(tmpBuffer))); + uint32_t read = 0; + + nsresult rv = mAsyncStream->Read(tmpBuffer, minCount, &read); + if (rv == NS_BASE_STREAM_CLOSED) { + rv = NS_OK; + } + + NS_ENSURE_SUCCESS(rv, NS_OK); + + if (read == 0) { + // The stream finished too early. + return NS_ERROR_OUT_OF_MEMORY; + } + + PopulateBufferForBinaryString(currentPos, tmpBuffer, read); + + currentPos += read; + aCount -= read; + bytesRead += read; + } + } + + MOZ_ASSERT(size.value() == lenBeforeRead + bytesRead); + mResult.Truncate(size.value()); + } else { + CheckedInt<uint64_t> size = mDataLen; + size += aCount; + + // Update memory buffer to reflect the contents of the file + if (!size.isValid() || + // PR_Realloc doesn't support over 4GB memory size even if 64-bit OS + // XXX: it's likely that this check is unnecessary and the comment is + // wrong because we no longer use PR_Realloc outside of NSPR and NSS. + size.value() > UINT32_MAX || size.value() > mTotal) { + return NS_ERROR_OUT_OF_MEMORY; + } + + MOZ_DIAGNOSTIC_ASSERT(mFileData); + MOZ_RELEASE_ASSERT((mDataLen + aCount) <= mTotal); + + nsresult rv = mAsyncStream->Read(mFileData + mDataLen, aCount, &bytesRead); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + mDataLen += bytesRead; + return NS_OK; +} + +// Helper methods + +void FileReader::ReadFileContent(Blob& aBlob, const nsAString& aCharset, + eDataFormat aDataFormat, ErrorResult& aRv) { + if (IsCurrentThreadRunningWorker() && !mWeakWorkerRef) { + // The worker is already shutting down. + return; + } + + if (mReadyState == LOADING) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + mError = nullptr; + + SetDOMStringToNull(mResult); + mResultArrayBuffer = nullptr; + + mAsyncStream = nullptr; + + mTransferred = 0; + mTotal = 0; + mReadyState = EMPTY; + FreeFileData(); + + mBlob = &aBlob; + mDataFormat = aDataFormat; + CopyUTF16toUTF8(aCharset, mCharset); + + { + nsCOMPtr<nsIInputStream> stream; + mBlob->CreateInputStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + aRv = NS_MakeAsyncNonBlockingInputStream(stream.forget(), + getter_AddRefs(mAsyncStream)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + MOZ_ASSERT(mAsyncStream); + + mTotal = mBlob->GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // Binary Format doesn't need a post-processing of the data. Everything is + // written directly into mResult. + if (mDataFormat != FILE_AS_BINARY) { + if (mDataFormat == FILE_AS_ARRAYBUFFER) { + mFileData = js_pod_malloc<char>(mTotal); + } else { + mFileData = (char*)malloc(mTotal); + } + + if (!mFileData) { + NS_WARNING("Preallocation failed for ReadFileData"); + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + + mAsyncWaitRunnable = new AsyncWaitRunnable(this); + aRv = NS_DispatchToCurrentThread(mAsyncWaitRunnable); + if (NS_WARN_IF(aRv.Failed())) { + FreeFileData(); + return; + } + + // FileReader should be in loading state here + mReadyState = LOADING; +} + +void FileReader::InitialAsyncWait() { + mAsyncWaitRunnable = nullptr; + + nsresult rv = DoAsyncWait(); + if (NS_WARN_IF(NS_FAILED(rv))) { + mReadyState = EMPTY; + FreeFileData(); + return; + } + + DispatchProgressEvent(nsLiteralString(LOADSTART_STR)); +} + +nsresult FileReader::GetAsText(Blob* aBlob, const nsACString& aCharset, + const char* aFileData, uint32_t aDataLen, + nsAString& aResult) { + // Try the API argument. + const Encoding* encoding = Encoding::ForLabel(aCharset); + if (!encoding) { + // API argument failed. Try the type property of the blob. + nsAutoString type16; + aBlob->GetType(type16); + NS_ConvertUTF16toUTF8 type(type16); + nsAutoCString specifiedCharset; + bool haveCharset; + int32_t charsetStart, charsetEnd; + NS_ExtractCharsetFromContentType(type, specifiedCharset, &haveCharset, + &charsetStart, &charsetEnd); + encoding = Encoding::ForLabel(specifiedCharset); + if (!encoding) { + // Type property failed. Use UTF-8. + encoding = UTF_8_ENCODING; + } + } + + auto data = Span(reinterpret_cast<const uint8_t*>(aFileData), aDataLen); + nsresult rv; + std::tie(rv, std::ignore) = encoding->Decode(data, aResult); + return NS_FAILED(rv) ? rv : NS_OK; +} + +nsresult FileReader::GetAsDataURL(Blob* aBlob, const char* aFileData, + uint32_t aDataLen, nsAString& aResult) { + aResult.AssignLiteral("data:"); + + nsAutoString contentType; + aBlob->GetType(contentType); + if (!contentType.IsEmpty()) { + aResult.Append(contentType); + } else { + aResult.AppendLiteral("application/octet-stream"); + } + aResult.AppendLiteral(";base64,"); + + return Base64EncodeAppend(aFileData, aDataLen, aResult); +} + +/* virtual */ +JSObject* FileReader::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return FileReader_Binding::Wrap(aCx, this, aGivenProto); +} + +void FileReader::StartProgressEventTimer() { + if (!NS_IsMainThread() && !mWeakWorkerRef) { + // The worker is possibly shutting down if dispatching a DOM event right + // before this call triggered an InterruptCallback call. + // XXX Note, the check is limited to workers for now, since it is unclear + // in the spec how FileReader should behave in this case on the main thread. + return; + } + + if (!mProgressNotifier) { + mProgressNotifier = NS_NewTimer(mTarget); + } + + if (mProgressNotifier) { + mProgressEventWasDelayed = false; + mTimerIsActive = true; + mProgressNotifier->Cancel(); + mProgressNotifier->InitWithCallback(this, NS_PROGRESS_EVENT_INTERVAL, + nsITimer::TYPE_ONE_SHOT); + } +} + +void FileReader::ClearProgressEventTimer() { + mProgressEventWasDelayed = false; + mTimerIsActive = false; + if (mProgressNotifier) { + mProgressNotifier->Cancel(); + } +} + +void FileReader::FreeFileData() { + if (mFileData) { + if (mDataFormat == FILE_AS_ARRAYBUFFER) { + js_free(mFileData); + } else { + free(mFileData); + } + mFileData = nullptr; + } + + mDataLen = 0; +} + +void FileReader::FreeDataAndDispatchSuccess() { + FreeFileData(); + mResult.SetIsVoid(false); + mAsyncStream = nullptr; + mBlob = nullptr; + + // Dispatch event to signify end of a successful operation + DispatchProgressEvent(nsLiteralString(LOAD_STR)); + DispatchProgressEvent(nsLiteralString(LOADEND_STR)); +} + +void FileReader::FreeDataAndDispatchError() { + MOZ_ASSERT(mError); + + FreeFileData(); + mResult.SetIsVoid(true); + mAsyncStream = nullptr; + mBlob = nullptr; + + // Dispatch error event to signify load failure + DispatchProgressEvent(nsLiteralString(ERROR_STR)); + DispatchProgressEvent(nsLiteralString(LOADEND_STR)); +} + +void FileReader::FreeDataAndDispatchError(nsresult aRv) { + // Set the status attribute, and dispatch the error event + switch (aRv) { + case NS_ERROR_FILE_NOT_FOUND: + mError = DOMException::Create(NS_ERROR_DOM_NOT_FOUND_ERR); + break; + case NS_ERROR_FILE_ACCESS_DENIED: + mError = DOMException::Create(NS_ERROR_DOM_SECURITY_ERR); + break; + default: + mError = DOMException::Create(NS_ERROR_DOM_FILE_NOT_READABLE_ERR); + break; + } + + FreeDataAndDispatchError(); +} + +nsresult FileReader::DispatchProgressEvent(const nsAString& aType) { + ProgressEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mLoaded = mTransferred; + + if (mTotal != kUnknownSize) { + init.mLengthComputable = true; + init.mTotal = mTotal; + } else { + init.mLengthComputable = false; + init.mTotal = 0; + } + RefPtr<ProgressEvent> event = ProgressEvent::Constructor(this, aType, init); + event->SetTrusted(true); + + ErrorResult rv; + DispatchEvent(*event, rv); + return rv.StealNSResult(); +} + +// nsITimerCallback +NS_IMETHODIMP +FileReader::Notify(nsITimer* aTimer) { + nsresult rv; + mTimerIsActive = false; + + if (mProgressEventWasDelayed) { + rv = DispatchProgressEvent(u"progress"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + StartProgressEventTimer(); + } + + return NS_OK; +} + +// InputStreamCallback +NS_IMETHODIMP +FileReader::OnInputStreamReady(nsIAsyncInputStream* aStream) { + // We use this class to decrease the busy counter at the end of this method. + // In theory we can do it immediatelly but, for debugging reasons, we want to + // be 100% sure we have a workerRef when OnLoadEnd() is called. + FileReaderDecreaseBusyCounter RAII(this); + + if (mReadyState != LOADING || aStream != mAsyncStream) { + return NS_OK; + } + + uint64_t count; + nsresult rv = aStream->Available(&count); + + if (NS_SUCCEEDED(rv) && count) { + rv = DoReadData(count); + + if (NS_SUCCEEDED(rv)) { + rv = DoAsyncWait(); + } + } + + if (NS_FAILED(rv) || !count) { + if (rv == NS_BASE_STREAM_CLOSED) { + rv = NS_OK; + } + OnLoadEnd(rv); + return NS_OK; + } + + mTransferred += count; + + // Notify the timer is the appropriate timeframe has passed + if (mTimerIsActive) { + mProgressEventWasDelayed = true; + } else { + rv = DispatchProgressEvent(nsLiteralString(PROGRESS_STR)); + NS_ENSURE_SUCCESS(rv, rv); + + StartProgressEventTimer(); + } + + return NS_OK; +} + +// nsINamed +NS_IMETHODIMP +FileReader::GetName(nsACString& aName) { + aName.AssignLiteral("FileReader"); + return NS_OK; +} + +void FileReader::OnLoadEnd(nsresult aStatus) { + // Cancel the progress event timer + ClearProgressEventTimer(); + + // FileReader must be in DONE stage after an operation + mReadyState = DONE; + + // Quick return, if failed. + if (NS_FAILED(aStatus)) { + FreeDataAndDispatchError(aStatus); + return; + } + + // In case we read a different number of bytes, we can assume that the + // underlying storage has changed. We should not continue. + if (mDataLen != mTotal) { + FreeDataAndDispatchError(NS_ERROR_FAILURE); + return; + } + + // ArrayBuffer needs a custom handling. + if (mDataFormat == FILE_AS_ARRAYBUFFER) { + OnLoadEndArrayBuffer(); + return; + } + + nsresult rv = NS_OK; + + // We don't do anything special for Binary format. + + if (mDataFormat == FILE_AS_DATAURL) { + rv = GetAsDataURL(mBlob, mFileData, mDataLen, mResult); + } else if (mDataFormat == FILE_AS_TEXT) { + if (!mFileData && mDataLen) { + rv = NS_ERROR_OUT_OF_MEMORY; + } else if (!mFileData) { + rv = GetAsText(mBlob, mCharset, "", mDataLen, mResult); + } else { + rv = GetAsText(mBlob, mCharset, mFileData, mDataLen, mResult); + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + FreeDataAndDispatchError(rv); + return; + } + + FreeDataAndDispatchSuccess(); +} + +void FileReader::Abort() { + if (mReadyState == EMPTY || mReadyState == DONE) { + return; + } + + MOZ_ASSERT(mReadyState == LOADING); + + Cleanup(); + + // XXX The spec doesn't say this + mError = DOMException::Create(NS_ERROR_DOM_ABORT_ERR); + + // Revert status and result attributes + SetDOMStringToNull(mResult); + mResultArrayBuffer = nullptr; + + mBlob = nullptr; + + // Dispatch the events + DispatchProgressEvent(nsLiteralString(ABORT_STR)); + DispatchProgressEvent(nsLiteralString(LOADEND_STR)); +} + +nsresult FileReader::IncreaseBusyCounter() { + if (mWeakWorkerRef && mBusyCount++ == 0) { + if (NS_WARN_IF(!mWeakWorkerRef->GetPrivate())) { + return NS_ERROR_FAILURE; + } + + RefPtr<FileReader> self = this; + + RefPtr<StrongWorkerRef> ref = + StrongWorkerRef::Create(mWeakWorkerRef->GetPrivate(), "FileReader", + [self]() { self->Shutdown(); }); + if (NS_WARN_IF(!ref)) { + return NS_ERROR_FAILURE; + } + + mStrongWorkerRef = ref; + } + + return NS_OK; +} + +void FileReader::DecreaseBusyCounter() { + MOZ_ASSERT_IF(mStrongWorkerRef, mBusyCount); + if (mStrongWorkerRef && --mBusyCount == 0) { + mStrongWorkerRef = nullptr; + } +} + +void FileReader::Cleanup() { + mReadyState = DONE; + + if (mAsyncWaitRunnable) { + mAsyncWaitRunnable->Cancel(); + mAsyncWaitRunnable = nullptr; + } + + if (mAsyncStream) { + mAsyncStream->Close(); + mAsyncStream = nullptr; + } + + ClearProgressEventTimer(); + FreeFileData(); + mResultArrayBuffer = nullptr; +} + +void FileReader::Shutdown() { + Cleanup(); + if (mWeakWorkerRef) { + mWeakWorkerRef = nullptr; + } +} + +} // namespace mozilla::dom diff --git a/dom/file/FileReader.h b/dom/file/FileReader.h new file mode 100644 index 0000000000..1c07b6193f --- /dev/null +++ b/dom/file/FileReader.h @@ -0,0 +1,207 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileReader_h +#define mozilla_dom_FileReader_h + +#include "mozilla/Attributes.h" +#include "mozilla/DOMEventTargetHelper.h" + +#include "nsIAsyncInputStream.h" +#include "nsIInterfaceRequestor.h" +#include "nsINamed.h" +#include "nsITimer.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsWeakReference.h" + +#define NS_PROGRESS_EVENT_INTERVAL 50 + +class nsITimer; +class nsIEventTarget; + +namespace mozilla::dom { + +class Blob; +class DOMException; +class OwningStringOrArrayBuffer; +class StrongWorkerRef; +class WeakWorkerRef; + +extern const uint64_t kUnknownSize; + +class FileReaderDecreaseBusyCounter; + +// 26a79031-c94b-47e9-850a-f04fe17bc026 +#define FILEREADER_ID \ + { \ + 0x26a79031, 0xc94b, 0x47e9, { \ + 0x85, 0x0a, 0xf0, 0x4f, 0xe1, 0x7b, 0xc0, 0x26 \ + } \ + } + +class FileReader final : public DOMEventTargetHelper, + public nsIInterfaceRequestor, + public nsSupportsWeakReference, + public nsIInputStreamCallback, + public nsITimerCallback, + public nsINamed { + friend class FileReaderDecreaseBusyCounter; + + public: + FileReader(nsIGlobalObject* aGlobal, WeakWorkerRef* aWorkerRef); + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSINAMED + + NS_DECLARE_STATIC_IID_ACCESSOR(FILEREADER_ID) + + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(FileReader, + DOMEventTargetHelper) + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL + static already_AddRefed<FileReader> Constructor(const GlobalObject& aGlobal); + void ReadAsArrayBuffer(JSContext* aCx, Blob& aBlob, ErrorResult& aRv) { + ReadFileContent(aBlob, u""_ns, FILE_AS_ARRAYBUFFER, aRv); + } + + void ReadAsText(Blob& aBlob, const Optional<nsAString>& aLabel, + ErrorResult& aRv) { + if (aLabel.WasPassed()) { + ReadFileContent(aBlob, aLabel.Value(), FILE_AS_TEXT, aRv); + } else { + ReadFileContent(aBlob, u""_ns, FILE_AS_TEXT, aRv); + } + } + + void ReadAsDataURL(Blob& aBlob, ErrorResult& aRv) { + ReadFileContent(aBlob, u""_ns, FILE_AS_DATAURL, aRv); + } + + void Abort(); + + uint16_t ReadyState() const { return static_cast<uint16_t>(mReadyState); } + + DOMException* GetError() const { return mError; } + + void GetResult(JSContext* aCx, Nullable<OwningStringOrArrayBuffer>& aResult); + + IMPL_EVENT_HANDLER(loadstart) + IMPL_EVENT_HANDLER(progress) + IMPL_EVENT_HANDLER(load) + IMPL_EVENT_HANDLER(abort) + IMPL_EVENT_HANDLER(error) + IMPL_EVENT_HANDLER(loadend) + + void ReadAsBinaryString(Blob& aBlob, ErrorResult& aRv) { + ReadFileContent(aBlob, u""_ns, FILE_AS_BINARY, aRv); + } + + enum eDataFormat { + FILE_AS_ARRAYBUFFER, + FILE_AS_BINARY, + FILE_AS_TEXT, + FILE_AS_DATAURL + }; + + eDataFormat DataFormat() const { return mDataFormat; } + const nsString& Result() const { return mResult; } + + void InitialAsyncWait(); + + private: + ~FileReader() override; + + // This must be in sync with dom/webidl/FileReader.webidl + enum eReadyState { EMPTY = 0, LOADING = 1, DONE = 2 }; + + void RootResultArrayBuffer(); + + void ReadFileContent(Blob& aBlob, const nsAString& aCharset, + eDataFormat aDataFormat, ErrorResult& aRv); + nsresult GetAsText(Blob* aBlob, const nsACString& aCharset, + const char* aFileData, uint32_t aDataLen, + nsAString& aResult); + nsresult GetAsDataURL(Blob* aBlob, const char* aFileData, uint32_t aDataLen, + nsAString& aResult); + + void OnLoadEnd(nsresult aStatus); + + void StartProgressEventTimer(); + void ClearProgressEventTimer(); + + void FreeDataAndDispatchSuccess(); + void FreeDataAndDispatchError(); + void FreeDataAndDispatchError(nsresult aRv); + nsresult DispatchProgressEvent(const nsAString& aType); + + nsresult DoAsyncWait(); + nsresult DoReadData(uint64_t aCount); + + void OnLoadEndArrayBuffer(); + + void FreeFileData(); + + nsresult IncreaseBusyCounter(); + void DecreaseBusyCounter(); + + void Cleanup(); + void Shutdown(); + + char* mFileData; + RefPtr<Blob> mBlob; + nsCString mCharset; + uint32_t mDataLen; + + eDataFormat mDataFormat; + + nsString mResult; + + JS::Heap<JSObject*> mResultArrayBuffer; + + nsCOMPtr<nsITimer> mProgressNotifier; + bool mProgressEventWasDelayed; + bool mTimerIsActive; + + nsCOMPtr<nsIAsyncInputStream> mAsyncStream; + + RefPtr<DOMException> mError; + + eReadyState mReadyState; + + uint64_t mTotal; + uint64_t mTransferred; + + nsCOMPtr<nsIEventTarget> mTarget; + + uint64_t mBusyCount; + + // This is set if FileReader is created on workers, but it is null if the + // worker is shutting down. The null value is checked in ReadFileContent() + // before starting any reading. + RefPtr<WeakWorkerRef> mWeakWorkerRef; + + // This value is set when the reading starts in order to keep the worker alive + // during the process. + RefPtr<StrongWorkerRef> mStrongWorkerRef; + + // Runnable to start the reading asynchronous. + class AsyncWaitRunnable; + RefPtr<AsyncWaitRunnable> mAsyncWaitRunnable; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(FileReader, FILEREADER_ID) + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileReader_h diff --git a/dom/file/FileReaderSync.cpp b/dom/file/FileReaderSync.cpp new file mode 100644 index 0000000000..59a925f439 --- /dev/null +++ b/dom/file/FileReaderSync.cpp @@ -0,0 +1,489 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileReaderSync.h" + +#include "js/ArrayBuffer.h" // JS::NewArrayBufferWithContents +#include "js/RootingAPI.h" // JS::{,Mutable}Handle +#include "js/Utility.h" // js::ArrayBufferContentsArena, JS::FreePolicy, js_pod_arena_malloc +#include "mozilla/Unused.h" +#include "mozilla/Base64.h" +#include "mozilla/dom/File.h" +#include "mozilla/Encoding.h" +#include "mozilla/dom/FileReaderSyncBinding.h" +#include "nsCExternalHandlerService.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsError.h" +#include "nsIConverterInputStream.h" +#include "nsIInputStream.h" +#include "nsIMultiplexInputStream.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "nsISupportsImpl.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsIAsyncInputStream.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRunnable.h" + +using namespace mozilla; +using namespace mozilla::dom; +using mozilla::dom::GlobalObject; +using mozilla::dom::Optional; + +// static +already_AddRefed<FileReaderSync> FileReaderSync::Constructor( + const GlobalObject& aGlobal) { + RefPtr<FileReaderSync> frs = new FileReaderSync(); + + return frs.forget(); +} + +bool FileReaderSync::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aReflector) { + return FileReaderSync_Binding::Wrap(aCx, this, aGivenProto, aReflector); +} + +void FileReaderSync::ReadAsArrayBuffer(JSContext* aCx, + JS::Handle<JSObject*> aScopeObj, + Blob& aBlob, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + uint64_t blobSize = aBlob.GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + UniquePtr<char[], JS::FreePolicy> bufferData( + js_pod_arena_malloc<char>(js::ArrayBufferContentsArena, blobSize)); + if (!bufferData) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + nsCOMPtr<nsIInputStream> stream; + aBlob.CreateInputStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + uint32_t numRead; + aRv = SyncRead(stream, bufferData.get(), blobSize, &numRead); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // The file is changed in the meantime? + if (numRead != blobSize) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + JSObject* arrayBuffer = + JS::NewArrayBufferWithContents(aCx, blobSize, bufferData.get()); + if (!arrayBuffer) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + // arrayBuffer takes the ownership when it is not null. Otherwise we + // need to release it explicitly. + (void)bufferData.release(); + + aRetval.set(arrayBuffer); +} + +void FileReaderSync::ReadAsBinaryString(Blob& aBlob, nsAString& aResult, + ErrorResult& aRv) { + nsCOMPtr<nsIInputStream> stream; + aBlob.CreateInputStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + uint32_t numRead; + do { + char readBuf[4096]; + aRv = SyncRead(stream, readBuf, sizeof(readBuf), &numRead); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + uint32_t oldLength = aResult.Length(); + AppendASCIItoUTF16(Substring(readBuf, readBuf + numRead), aResult); + if (aResult.Length() - oldLength != numRead) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } while (numRead > 0); +} + +void FileReaderSync::ReadAsText(Blob& aBlob, + const Optional<nsAString>& aEncoding, + nsAString& aResult, ErrorResult& aRv) { + nsCOMPtr<nsIInputStream> stream; + aBlob.CreateInputStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCString sniffBuf; + if (!sniffBuf.SetLength(3, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + uint32_t numRead = 0; + aRv = SyncRead(stream, sniffBuf.BeginWriting(), sniffBuf.Length(), &numRead); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // No data, we don't need to continue. + if (numRead == 0) { + aResult.Truncate(); + return; + } + + // Try the API argument. + const Encoding* encoding = + aEncoding.WasPassed() ? Encoding::ForLabel(aEncoding.Value()) : nullptr; + if (!encoding) { + // API argument failed. Try the type property of the blob. + nsAutoString type16; + aBlob.GetType(type16); + NS_ConvertUTF16toUTF8 type(type16); + nsAutoCString specifiedCharset; + bool haveCharset; + int32_t charsetStart, charsetEnd; + NS_ExtractCharsetFromContentType(type, specifiedCharset, &haveCharset, + &charsetStart, &charsetEnd); + encoding = Encoding::ForLabel(specifiedCharset); + if (!encoding) { + // Type property failed. Use UTF-8. + encoding = UTF_8_ENCODING; + } + } + + if (numRead < sniffBuf.Length()) { + sniffBuf.Truncate(numRead); + } + + // Let's recreate the full stream using a: + // multiplexStream(syncStream + original stream) + // In theory, we could try to see if the inputStream is a nsISeekableStream, + // but this doesn't work correctly for nsPipe3 - See bug 1349570. + + nsCOMPtr<nsIMultiplexInputStream> multiplexStream = + do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1"); + if (NS_WARN_IF(!multiplexStream)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsCOMPtr<nsIInputStream> sniffStringStream; + aRv = NS_NewCStringInputStream(getter_AddRefs(sniffStringStream), sniffBuf); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + aRv = multiplexStream->AppendStream(sniffStringStream); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + uint64_t blobSize = aBlob.GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr<nsIInputStream> syncStream; + aRv = ConvertAsyncToSyncStream(blobSize - sniffBuf.Length(), stream.forget(), + getter_AddRefs(syncStream)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // ConvertAsyncToSyncStream returns a null syncStream if the stream has been + // already closed or there is nothing to read. + if (syncStream) { + aRv = multiplexStream->AppendStream(syncStream); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + nsAutoCString charset; + encoding->Name(charset); + + nsCOMPtr<nsIInputStream> multiplex(do_QueryInterface(multiplexStream)); + aRv = ConvertStream(multiplex, charset.get(), aResult); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + +void FileReaderSync::ReadAsDataURL(Blob& aBlob, nsAString& aResult, + ErrorResult& aRv) { + nsAutoString scratchResult; + scratchResult.AssignLiteral("data:"); + + nsString contentType; + aBlob.GetType(contentType); + + if (contentType.IsEmpty()) { + scratchResult.AppendLiteral("application/octet-stream"); + } else { + scratchResult.Append(contentType); + } + scratchResult.AppendLiteral(";base64,"); + + nsCOMPtr<nsIInputStream> stream; + aBlob.CreateInputStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + uint64_t blobSize = aBlob.GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr<nsIInputStream> syncStream; + aRv = ConvertAsyncToSyncStream(blobSize, stream.forget(), + getter_AddRefs(syncStream)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + MOZ_ASSERT(syncStream); + + uint64_t size; + aRv = syncStream->Available(&size); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // The file is changed in the meantime? + if (blobSize != size) { + return; + } + + nsAutoString encodedData; + aRv = Base64EncodeInputStream(syncStream, encodedData, size); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + scratchResult.Append(encodedData); + + aResult = scratchResult; +} + +nsresult FileReaderSync::ConvertStream(nsIInputStream* aStream, + const char* aCharset, + nsAString& aResult) { + nsCOMPtr<nsIConverterInputStream> converterStream = + do_CreateInstance("@mozilla.org/intl/converter-input-stream;1"); + NS_ENSURE_TRUE(converterStream, NS_ERROR_FAILURE); + + nsresult rv = converterStream->Init( + aStream, aCharset, 8192, + nsIConverterInputStream::DEFAULT_REPLACEMENT_CHARACTER); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIUnicharInputStream> unicharStream = converterStream; + NS_ENSURE_TRUE(unicharStream, NS_ERROR_FAILURE); + + uint32_t numChars; + nsString result; + while (NS_SUCCEEDED(unicharStream->ReadString(8192, result, &numChars)) && + numChars > 0) { + uint32_t oldLength = aResult.Length(); + aResult.Append(result); + if (aResult.Length() - oldLength != result.Length()) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + return rv; +} + +namespace { + +// This runnable is used to terminate the sync event loop. +class ReadReadyRunnable final : public WorkerSyncRunnable { + public: + ReadReadyRunnable(WorkerPrivate* aWorkerPrivate, + nsIEventTarget* aSyncLoopTarget) + : WorkerSyncRunnable(aWorkerPrivate, aSyncLoopTarget) {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mSyncLoopTarget); + + nsCOMPtr<nsIEventTarget> syncLoopTarget; + mSyncLoopTarget.swap(syncLoopTarget); + + aWorkerPrivate->StopSyncLoop(syncLoopTarget, NS_OK); + return true; + } + + private: + ~ReadReadyRunnable() override = default; +}; + +// This class implements nsIInputStreamCallback and it will be called when the +// stream is ready to be read. +class ReadCallback final : public nsIInputStreamCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + ReadCallback(WorkerPrivate* aWorkerPrivate, nsIEventTarget* aEventTarget) + : mWorkerPrivate(aWorkerPrivate), mEventTarget(aEventTarget) {} + + NS_IMETHOD + OnInputStreamReady(nsIAsyncInputStream* aStream) override { + // I/O Thread. Now we need to block the sync event loop. + RefPtr<ReadReadyRunnable> runnable = + new ReadReadyRunnable(mWorkerPrivate, mEventTarget); + return mEventTarget->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + } + + private: + ~ReadCallback() = default; + + // The worker is kept alive because of the sync event loop. + WorkerPrivate* mWorkerPrivate; + nsCOMPtr<nsIEventTarget> mEventTarget; +}; + +NS_IMPL_ADDREF(ReadCallback); +NS_IMPL_RELEASE(ReadCallback); + +NS_INTERFACE_MAP_BEGIN(ReadCallback) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStreamCallback) +NS_INTERFACE_MAP_END + +} // namespace + +nsresult FileReaderSync::SyncRead(nsIInputStream* aStream, char* aBuffer, + uint32_t aBufferSize, + uint32_t* aTotalBytesRead) { + MOZ_ASSERT(aStream); + MOZ_ASSERT(aBuffer); + MOZ_ASSERT(aTotalBytesRead); + + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + *aTotalBytesRead = 0; + + nsCOMPtr<nsIAsyncInputStream> asyncStream; + nsCOMPtr<nsIEventTarget> target; + + while (*aTotalBytesRead < aBufferSize) { + uint32_t currentBytesRead = 0; + + // Let's read something. + nsresult rv = + aStream->Read(aBuffer + *aTotalBytesRead, + aBufferSize - *aTotalBytesRead, ¤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<nsISerialEventTarget> syncLoopTarget = + syncLoop.GetSerialEventTarget(); + if (!syncLoopTarget) { + // SyncLoop creation can fail if the worker is shutting down. + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + RefPtr<ReadCallback> callback = + new ReadCallback(workerPrivate, syncLoopTarget); + + if (!target) { + target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target); + } + + rv = asyncStream->AsyncWait(callback, 0, aBufferSize - *aTotalBytesRead, + target); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(NS_FAILED(syncLoop.Run()))) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + } + + return NS_OK; +} + +nsresult FileReaderSync::ConvertAsyncToSyncStream( + uint64_t aStreamSize, already_AddRefed<nsIInputStream> aAsyncStream, + nsIInputStream** aSyncStream) { + nsCOMPtr<nsIInputStream> asyncInputStream = std::move(aAsyncStream); + + // If the stream is not async, we just need it to be bufferable. + nsCOMPtr<nsIAsyncInputStream> asyncStream = + do_QueryInterface(asyncInputStream); + if (!asyncStream) { + return NS_NewBufferedInputStream(aSyncStream, asyncInputStream.forget(), + 4096); + } + + nsAutoCString buffer; + if (!buffer.SetLength(aStreamSize, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t read; + nsresult rv = + SyncRead(asyncInputStream, buffer.BeginWriting(), aStreamSize, &read); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (read != aStreamSize) { + return NS_ERROR_FAILURE; + } + + rv = NS_NewCStringInputStream(aSyncStream, std::move(buffer)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} diff --git a/dom/file/FileReaderSync.h b/dom/file/FileReaderSync.h new file mode 100644 index 0000000000..84eb5a73a8 --- /dev/null +++ b/dom/file/FileReaderSync.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_filereadersync_h__ +#define mozilla_dom_filereadersync_h__ + +#include "mozilla/dom/WorkerCommon.h" +#include "nsISupports.h" + +class nsIInputStream; + +namespace mozilla { +class ErrorResult; + +namespace dom { +class Blob; +class GlobalObject; +template <typename> +class Optional; + +class FileReaderSync final { + NS_INLINE_DECL_REFCOUNTING(FileReaderSync) + + private: + // Private destructor, to discourage deletion outside of Release(): + ~FileReaderSync() = default; + + nsresult ConvertStream(nsIInputStream* aStream, const char* aCharset, + nsAString& aResult); + + nsresult ConvertAsyncToSyncStream( + uint64_t aStreamSize, already_AddRefed<nsIInputStream> aAsyncStream, + nsIInputStream** aSyncStream); + + nsresult SyncRead(nsIInputStream* aStream, char* aBuffer, + uint32_t aBufferSize, uint32_t* aTotalBytesRead); + + public: + static already_AddRefed<FileReaderSync> Constructor( + const GlobalObject& aGlobal); + + bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aReflector); + + void ReadAsArrayBuffer(JSContext* aCx, JS::Handle<JSObject*> aScopeObj, + Blob& aBlob, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + void ReadAsBinaryString(Blob& aBlob, nsAString& aResult, ErrorResult& aRv); + void ReadAsText(Blob& aBlob, const Optional<nsAString>& aEncoding, + nsAString& aResult, ErrorResult& aRv); + void ReadAsDataURL(Blob& aBlob, nsAString& aResult, ErrorResult& aRv); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_filereadersync_h__ diff --git a/dom/file/MemoryBlobImpl.cpp b/dom/file/MemoryBlobImpl.cpp new file mode 100644 index 0000000000..b6d5c442d1 --- /dev/null +++ b/dom/file/MemoryBlobImpl.cpp @@ -0,0 +1,168 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MemoryBlobImpl.h" +#include "mozilla/ipc/InputStreamUtils.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/SHA1.h" +#include "nsIMemoryReporter.h" +#include "nsPrintfCString.h" +#include "nsRFPService.h" +#include "nsStringStream.h" +#include "prtime.h" + +namespace mozilla::dom { + +// static +already_AddRefed<MemoryBlobImpl> MemoryBlobImpl::CreateWithCustomLastModified( + void* aMemoryBuffer, uint64_t aLength, const nsAString& aName, + const nsAString& aContentType, int64_t aLastModifiedDate) { + RefPtr<MemoryBlobImpl> blobImpl = new MemoryBlobImpl( + aMemoryBuffer, aLength, aName, aContentType, aLastModifiedDate); + return blobImpl.forget(); +} + +// static +already_AddRefed<MemoryBlobImpl> MemoryBlobImpl::CreateWithLastModifiedNow( + void* aMemoryBuffer, uint64_t aLength, const nsAString& aName, + const nsAString& aContentType, RTPCallerType aRTPCallerType) { + int64_t lastModificationDate = + nsRFPService::ReduceTimePrecisionAsUSecs(PR_Now(), 0, aRTPCallerType); + return CreateWithCustomLastModified(aMemoryBuffer, aLength, aName, + aContentType, lastModificationDate); +} + +nsresult MemoryBlobImpl::DataOwnerAdapter::Create(DataOwner* aDataOwner, + size_t aStart, size_t aLength, + nsIInputStream** _retval) { + MOZ_ASSERT(aDataOwner, "Uh ..."); + Span data{static_cast<const char*>(aDataOwner->mData) + aStart, aLength}; + RefPtr adapter = new MemoryBlobImpl::DataOwnerAdapter(aDataOwner, data); + return NS_NewByteInputStream(_retval, adapter); +} + +already_AddRefed<BlobImpl> MemoryBlobImpl::CreateSlice( + uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + ErrorResult& aRv) const { + RefPtr<BlobImpl> impl = + new MemoryBlobImpl(this, aStart, aLength, aContentType); + return impl.forget(); +} + +void MemoryBlobImpl::CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const { + if (mLength >= INT32_MAX) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + aRv = MemoryBlobImpl::DataOwnerAdapter::Create(mDataOwner, mStart, mLength, + aStream); +} + +/* static */ +StaticMutex MemoryBlobImpl::DataOwner::sDataOwnerMutex; + +/* static */ StaticAutoPtr<LinkedList<MemoryBlobImpl::DataOwner>> + MemoryBlobImpl::DataOwner::sDataOwners; + +/* static */ +bool MemoryBlobImpl::DataOwner::sMemoryReporterRegistered = false; + +MOZ_DEFINE_MALLOC_SIZE_OF(MemoryFileDataOwnerMallocSizeOf) + +class MemoryBlobImplDataOwnerMemoryReporter final : public nsIMemoryReporter { + ~MemoryBlobImplDataOwnerMemoryReporter() = default; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) override { + using DataOwner = MemoryBlobImpl::DataOwner; + + StaticMutexAutoLock lock(DataOwner::sDataOwnerMutex); + + if (!DataOwner::sDataOwners) { + return NS_OK; + } + + const size_t LARGE_OBJECT_MIN_SIZE = 8 * 1024; + size_t smallObjectsTotal = 0; + + for (DataOwner* owner = DataOwner::sDataOwners->getFirst(); owner; + owner = owner->getNext()) { + size_t size = MemoryFileDataOwnerMallocSizeOf(owner->mData); + + if (size < LARGE_OBJECT_MIN_SIZE) { + smallObjectsTotal += size; + } else { + SHA1Sum sha1; + sha1.update(owner->mData, owner->mLength); + uint8_t digest[SHA1Sum::kHashSize]; // SHA1 digests are 20 bytes long. + sha1.finish(digest); + + nsAutoCString digestString; + for (size_t i = 0; i < sizeof(digest); i++) { + digestString.AppendPrintf("%02x", digest[i]); + } + + aHandleReport->Callback( + /* process */ ""_ns, + nsPrintfCString( + "explicit/dom/memory-file-data/large/file(length=%" PRIu64 + ", sha1=%s)", + owner->mLength, + aAnonymize ? "<anonymized>" : digestString.get()), + KIND_HEAP, UNITS_BYTES, size, + nsPrintfCString( + "Memory used to back a memory file of length %" PRIu64 + " bytes. The file " + "has a sha1 of %s.\n\n" + "Note that the allocator may round up a memory file's length " + "-- " + "that is, an N-byte memory file may take up more than N bytes " + "of " + "memory.", + owner->mLength, digestString.get()), + aData); + } + } + + if (smallObjectsTotal > 0) { + aHandleReport->Callback( + /* process */ ""_ns, "explicit/dom/memory-file-data/small"_ns, + KIND_HEAP, UNITS_BYTES, smallObjectsTotal, + nsPrintfCString( + "Memory used to back small memory files (i.e. those taking up " + "less " + "than %zu bytes of memory each).\n\n" + "Note that the allocator may round up a memory file's length -- " + "that is, an N-byte memory file may take up more than N bytes of " + "memory.", + LARGE_OBJECT_MIN_SIZE), + aData); + } + + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(MemoryBlobImplDataOwnerMemoryReporter, nsIMemoryReporter) + +/* static */ +void MemoryBlobImpl::DataOwner::EnsureMemoryReporterRegistered() { + sDataOwnerMutex.AssertCurrentThreadOwns(); + if (sMemoryReporterRegistered) { + return; + } + + RegisterStrongMemoryReporter(new MemoryBlobImplDataOwnerMemoryReporter()); + + sMemoryReporterRegistered = true; +} + +} // namespace mozilla::dom diff --git a/dom/file/MemoryBlobImpl.h b/dom/file/MemoryBlobImpl.h new file mode 100644 index 0000000000..29d812feee --- /dev/null +++ b/dom/file/MemoryBlobImpl.h @@ -0,0 +1,160 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_MemoryBlobImpl_h +#define mozilla_dom_MemoryBlobImpl_h + +#include "mozilla/dom/BaseBlobImpl.h" +#include "mozilla/LinkedList.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/StreamBufferSource.h" +#include "nsCOMPtr.h" +#include "nsICloneableInputStream.h" +#include "nsIInputStream.h" +#include "nsIIPCSerializableInputStream.h" +#include "nsISeekableStream.h" + +namespace mozilla::dom { + +class MemoryBlobImpl final : public BaseBlobImpl { + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(MemoryBlobImpl, BaseBlobImpl) + + // File constructor. + static already_AddRefed<MemoryBlobImpl> CreateWithLastModifiedNow( + void* aMemoryBuffer, uint64_t aLength, const nsAString& aName, + const nsAString& aContentType, RTPCallerType aRTPCallerType); + + // File constructor with custom lastModified attribue value. You should + // probably use CreateWithLastModifiedNow() instead of this one. + static already_AddRefed<MemoryBlobImpl> CreateWithCustomLastModified( + void* aMemoryBuffer, uint64_t aLength, const nsAString& aName, + const nsAString& aContentType, int64_t aLastModifiedDate); + + // Blob constructor. + MemoryBlobImpl(void* aMemoryBuffer, uint64_t aLength, + const nsAString& aContentType) + : BaseBlobImpl(aContentType, aLength), + mDataOwner(new DataOwner(aMemoryBuffer, aLength)) { + MOZ_ASSERT(mDataOwner && mDataOwner->mData, "must have data"); + } + + void CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const override; + + already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const override; + + bool IsMemoryFile() const override { return true; } + + size_t GetAllocationSize() const override { return mLength; } + + size_t GetAllocationSize( + FallibleTArray<BlobImpl*>& aVisitedBlobImpls) const override { + return GetAllocationSize(); + } + + void GetBlobImplType(nsAString& aBlobImplType) const override { + aBlobImplType = u"MemoryBlobImpl"_ns; + } + + class DataOwner final : public mozilla::LinkedListElement<DataOwner> { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DataOwner) + DataOwner(void* aMemoryBuffer, uint64_t aLength) + : mData(aMemoryBuffer), mLength(aLength) { + mozilla::StaticMutexAutoLock lock(sDataOwnerMutex); + + if (!sDataOwners) { + sDataOwners = new mozilla::LinkedList<DataOwner>(); + EnsureMemoryReporterRegistered(); + } + sDataOwners->insertBack(this); + } + + private: + // Private destructor, to discourage deletion outside of Release(): + ~DataOwner() { + mozilla::StaticMutexAutoLock lock(sDataOwnerMutex); + + remove(); + if (sDataOwners->isEmpty()) { + // Free the linked list if it's empty. + sDataOwners = nullptr; + } + + free(mData); + } + + public: + static void EnsureMemoryReporterRegistered(); + + // sDataOwners and sMemoryReporterRegistered may only be accessed while + // holding sDataOwnerMutex! You also must hold the mutex while touching + // elements of the linked list that DataOwner inherits from. + static mozilla::StaticMutex sDataOwnerMutex MOZ_UNANNOTATED; + static mozilla::StaticAutoPtr<mozilla::LinkedList<DataOwner> > sDataOwners; + static bool sMemoryReporterRegistered; + + void* mData; + uint64_t mLength; + }; + + class DataOwnerAdapter final : public StreamBufferSource { + using DataOwner = MemoryBlobImpl::DataOwner; + + public: + static nsresult Create(DataOwner* aDataOwner, size_t aStart, size_t aLength, + nsIInputStream** _retval); + + Span<const char> Data() override { return mData; } + + // This StreamBufferSource is owning, as the `mData` span references the + // immutable data buffer owned by `mDataOwner` which is being kept alive. + bool Owning() override { return true; } + + // The memory usage from `DataOwner` is reported elsewhere, so we don't need + // to record it here. + size_t SizeOfExcludingThisEvenIfShared(MallocSizeOf) override { return 0; } + + private: + ~DataOwnerAdapter() override = default; + + DataOwnerAdapter(DataOwner* aDataOwner, Span<const char> aData) + : mDataOwner(aDataOwner), mData(aData) {} + + RefPtr<DataOwner> mDataOwner; + Span<const char> mData; + }; + + private: + // File constructor. + MemoryBlobImpl(void* aMemoryBuffer, uint64_t aLength, const nsAString& aName, + const nsAString& aContentType, int64_t aLastModifiedDate) + : BaseBlobImpl(aName, aContentType, aLength, aLastModifiedDate), + mDataOwner(new DataOwner(aMemoryBuffer, aLength)) { + MOZ_ASSERT(mDataOwner && mDataOwner->mData, "must have data"); + } + + // Create slice + MemoryBlobImpl(const MemoryBlobImpl* aOther, uint64_t aStart, + uint64_t aLength, const nsAString& aContentType) + : BaseBlobImpl(aContentType, aOther->mStart + aStart, aLength), + mDataOwner(aOther->mDataOwner) { + MOZ_ASSERT(mDataOwner && mDataOwner->mData, "must have data"); + } + + ~MemoryBlobImpl() override = default; + + // Used when backed by a memory store + RefPtr<DataOwner> mDataOwner; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_MemoryBlobImpl_h diff --git a/dom/file/MultipartBlobImpl.cpp b/dom/file/MultipartBlobImpl.cpp new file mode 100644 index 0000000000..b795793aef --- /dev/null +++ b/dom/file/MultipartBlobImpl.cpp @@ -0,0 +1,338 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MultipartBlobImpl.h" +#include "jsfriendapi.h" +#include "mozilla/dom/BlobSet.h" +#include "mozilla/dom/FileBinding.h" +#include "mozilla/dom/UnionTypes.h" +#include "nsComponentManagerUtils.h" +#include "nsIGlobalObject.h" +#include "nsIMultiplexInputStream.h" +#include "nsReadableUtils.h" +#include "nsRFPService.h" +#include "nsStringStream.h" +#include "nsTArray.h" +#include "nsJSUtils.h" +#include "nsContentUtils.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::dom; + +/* static */ +already_AddRefed<MultipartBlobImpl> MultipartBlobImpl::Create( + nsTArray<RefPtr<BlobImpl>>&& aBlobImpls, const nsAString& aName, + const nsAString& aContentType, RTPCallerType aRTPCallerType, + ErrorResult& aRv) { + RefPtr<MultipartBlobImpl> blobImpl = + new MultipartBlobImpl(std::move(aBlobImpls), aName, aContentType); + blobImpl->SetLengthAndModifiedDate(Some(aRTPCallerType), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return blobImpl.forget(); +} + +/* static */ +already_AddRefed<MultipartBlobImpl> MultipartBlobImpl::Create( + nsTArray<RefPtr<BlobImpl>>&& aBlobImpls, const nsAString& aContentType, + ErrorResult& aRv) { + RefPtr<MultipartBlobImpl> blobImpl = + new MultipartBlobImpl(std::move(aBlobImpls), aContentType); + blobImpl->SetLengthAndModifiedDate(/* aRTPCallerType */ Nothing(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return blobImpl.forget(); +} + +void MultipartBlobImpl::CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const { + *aStream = nullptr; + + uint32_t length = mBlobImpls.Length(); + if (length == 0 || mLength == 0) { + aRv = NS_NewCStringInputStream(aStream, ""_ns); + return; + } + + if (length == 1) { + BlobImpl* blobImpl = mBlobImpls.ElementAt(0); + blobImpl->CreateInputStream(aStream, aRv); + return; + } + + nsCOMPtr<nsIMultiplexInputStream> stream = + do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1"); + if (NS_WARN_IF(!stream)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + uint32_t i; + for (i = 0; i < length; i++) { + nsCOMPtr<nsIInputStream> scratchStream; + BlobImpl* blobImpl = mBlobImpls.ElementAt(i).get(); + + // nsIMultiplexInputStream doesn't work well with empty sub streams. Let's + // skip the empty blobs. + uint32_t size = blobImpl->GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (size == 0) { + continue; + } + + blobImpl->CreateInputStream(getter_AddRefs(scratchStream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + aRv = stream->AppendStream(scratchStream); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + CallQueryInterface(stream, aStream); +} + +already_AddRefed<BlobImpl> MultipartBlobImpl::CreateSlice( + uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + ErrorResult& aRv) const { + // If we clamped to nothing we create an empty blob + nsTArray<RefPtr<BlobImpl>> blobImpls; + + uint64_t length = aLength; + uint64_t skipStart = aStart; + + // Prune the list of blobs if we can + uint32_t i; + for (i = 0; length && skipStart && i < mBlobImpls.Length(); i++) { + BlobImpl* blobImpl = mBlobImpls[i].get(); + + uint64_t l = blobImpl->GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (skipStart < l) { + uint64_t upperBound = std::min<uint64_t>(l - skipStart, length); + + RefPtr<BlobImpl> firstBlobImpl = + blobImpl->CreateSlice(skipStart, upperBound, aContentType, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // Avoid wrapping a single blob inside an MultipartBlobImpl + if (length == upperBound) { + return firstBlobImpl.forget(); + } + + blobImpls.AppendElement(firstBlobImpl); + length -= upperBound; + i++; + break; + } + skipStart -= l; + } + + // Now append enough blobs until we're done + for (; length && i < mBlobImpls.Length(); i++) { + BlobImpl* blobImpl = mBlobImpls[i].get(); + + uint64_t l = blobImpl->GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (length < l) { + RefPtr<BlobImpl> lastBlobImpl = + blobImpl->CreateSlice(0, length, aContentType, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + blobImpls.AppendElement(lastBlobImpl); + } else { + blobImpls.AppendElement(blobImpl); + } + length -= std::min<uint64_t>(l, length); + } + + // we can create our blob now + RefPtr<BlobImpl> impl = Create(std::move(blobImpls), aContentType, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return impl.forget(); +} + +void MultipartBlobImpl::InitializeBlob(RTPCallerType aRTPCallerType, + ErrorResult& aRv) { + SetLengthAndModifiedDate(Some(aRTPCallerType), aRv); + NS_WARNING_ASSERTION(!aRv.Failed(), "SetLengthAndModifiedDate failed"); +} + +void MultipartBlobImpl::InitializeBlob(const Sequence<Blob::BlobPart>& aData, + const nsAString& aContentType, + bool aNativeEOL, + RTPCallerType aRTPCallerType, + ErrorResult& aRv) { + mContentType = aContentType; + BlobSet blobSet; + + for (uint32_t i = 0, len = aData.Length(); i < len; ++i) { + const Blob::BlobPart& data = aData[i]; + + if (data.IsBlob()) { + RefPtr<Blob> blob = data.GetAsBlob().get(); + aRv = blobSet.AppendBlobImpl(blob->Impl()); + if (aRv.Failed()) { + return; + } + } + + else if (data.IsUTF8String()) { + aRv = blobSet.AppendUTF8String(data.GetAsUTF8String(), aNativeEOL); + if (aRv.Failed()) { + return; + } + } + + else if (data.IsArrayBuffer()) { + const ArrayBuffer& buffer = data.GetAsArrayBuffer(); + buffer.ComputeState(); + aRv = blobSet.AppendVoidPtr(buffer.Data(), buffer.Length()); + if (aRv.Failed()) { + return; + } + } + + else if (data.IsArrayBufferView()) { + const ArrayBufferView& buffer = data.GetAsArrayBufferView(); + buffer.ComputeState(); + aRv = blobSet.AppendVoidPtr(buffer.Data(), buffer.Length()); + if (aRv.Failed()) { + return; + } + } + + else { + MOZ_CRASH("Impossible blob data type."); + } + } + + mBlobImpls = blobSet.GetBlobImpls(); + SetLengthAndModifiedDate(Some(aRTPCallerType), aRv); + NS_WARNING_ASSERTION(!aRv.Failed(), "SetLengthAndModifiedDate failed"); +} + +void MultipartBlobImpl::SetLengthAndModifiedDate( + const Maybe<RTPCallerType>& aRTPCallerType, ErrorResult& aRv) { + MOZ_ASSERT(mLength == MULTIPARTBLOBIMPL_UNKNOWN_LENGTH); + MOZ_ASSERT_IF(mIsFile, IsLastModificationDateUnset()); + + uint64_t totalLength = 0; + int64_t lastModified = 0; + bool lastModifiedSet = false; + + for (uint32_t index = 0, count = mBlobImpls.Length(); index < count; + index++) { + RefPtr<BlobImpl>& blob = mBlobImpls[index]; + + uint64_t subBlobLength = blob->GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + MOZ_ASSERT(UINT64_MAX - subBlobLength >= totalLength); + totalLength += subBlobLength; + + if (blob->IsFile()) { + int64_t partLastModified = blob->GetLastModified(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (lastModified < partLastModified) { + lastModified = partLastModified * PR_USEC_PER_MSEC; + lastModifiedSet = true; + } + } + } + + mLength = totalLength; + + if (mIsFile) { + if (lastModifiedSet) { + SetLastModificationDatePrecisely(lastModified); + } else { + MOZ_ASSERT(aRTPCallerType.isSome()); + + // We cannot use PR_Now() because bug 493756 and, for this reason: + // var x = new Date(); var f = new File(...); + // x.getTime() < f.dateModified.getTime() + // could fail. + SetLastModificationDate(aRTPCallerType.value(), JS_Now()); + } + } +} + +size_t MultipartBlobImpl::GetAllocationSize() const { + FallibleTArray<BlobImpl*> visitedBlobs; + + // We want to report the unique blob allocation, avoiding duplicated blobs in + // the multipart blob tree. + size_t total = 0; + for (uint32_t i = 0; i < mBlobImpls.Length(); ++i) { + total += mBlobImpls[i]->GetAllocationSize(visitedBlobs); + } + + return total; +} + +size_t MultipartBlobImpl::GetAllocationSize( + FallibleTArray<BlobImpl*>& aVisitedBlobs) const { + FallibleTArray<BlobImpl*> visitedBlobs; + + size_t total = 0; + for (BlobImpl* blobImpl : mBlobImpls) { + if (!aVisitedBlobs.Contains(blobImpl)) { + if (NS_WARN_IF(!aVisitedBlobs.AppendElement(blobImpl, fallible))) { + return 0; + } + total += blobImpl->GetAllocationSize(aVisitedBlobs); + } + } + + return total; +} + +void MultipartBlobImpl::GetBlobImplType(nsAString& aBlobImplType) const { + aBlobImplType.AssignLiteral("MultipartBlobImpl["); + + StringJoinAppend(aBlobImplType, u", "_ns, mBlobImpls, + [](nsAString& dest, BlobImpl* subBlobImpl) { + nsAutoString blobImplType; + subBlobImpl->GetBlobImplType(blobImplType); + + dest.Append(blobImplType); + }); + + aBlobImplType.AppendLiteral("]"); +} + +void MultipartBlobImpl::SetLastModified(int64_t aLastModified) { + SetLastModificationDatePrecisely(aLastModified * PR_USEC_PER_MSEC); +} diff --git a/dom/file/MultipartBlobImpl.h b/dom/file/MultipartBlobImpl.h new file mode 100644 index 0000000000..cb62f2adb5 --- /dev/null +++ b/dom/file/MultipartBlobImpl.h @@ -0,0 +1,106 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_MultipartBlobImpl_h +#define mozilla_dom_MultipartBlobImpl_h + +#include <utility> + +#include "Blob.h" +#include "nsContentUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/dom/BaseBlobImpl.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +// This is just a sentinel value to be sure that we don't call +// SetLengthAndModifiedDate more than once. +constexpr int64_t MULTIPARTBLOBIMPL_UNKNOWN_LAST_MODIFIED = INT64_MAX; +constexpr uint64_t MULTIPARTBLOBIMPL_UNKNOWN_LENGTH = UINT64_MAX; + +class MultipartBlobImpl final : public BaseBlobImpl { + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(MultipartBlobImpl, BaseBlobImpl) + + // Create as a file + static already_AddRefed<MultipartBlobImpl> Create( + nsTArray<RefPtr<BlobImpl>>&& aBlobImpls, const nsAString& aName, + const nsAString& aContentType, RTPCallerType aRTPCallerType, + ErrorResult& aRv); + + // Create as a blob + static already_AddRefed<MultipartBlobImpl> Create( + nsTArray<RefPtr<BlobImpl>>&& aBlobImpls, const nsAString& aContentType, + ErrorResult& aRv); + + // Create as a file to be later initialized + explicit MultipartBlobImpl(const nsAString& aName) + : BaseBlobImpl(aName, u""_ns, MULTIPARTBLOBIMPL_UNKNOWN_LENGTH, + MULTIPARTBLOBIMPL_UNKNOWN_LAST_MODIFIED) {} + + // Create as a blob to be later initialized + MultipartBlobImpl() + : BaseBlobImpl(u""_ns, MULTIPARTBLOBIMPL_UNKNOWN_LENGTH) {} + + void InitializeBlob(RTPCallerType aRTPCallerType, ErrorResult& aRv); + + void InitializeBlob(const Sequence<Blob::BlobPart>& aData, + const nsAString& aContentType, bool aNativeEOL, + RTPCallerType aRTPCallerType, ErrorResult& aRv); + + already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const override; + + uint64_t GetSize(ErrorResult& aRv) override { return mLength; } + + void CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const override; + + const nsTArray<RefPtr<BlobImpl>>* GetSubBlobImpls() const override { + return mBlobImpls.Length() ? &mBlobImpls : nullptr; + } + + void SetName(const nsAString& aName) { mName = aName; } + + size_t GetAllocationSize() const override; + size_t GetAllocationSize( + FallibleTArray<BlobImpl*>& aVisitedBlobs) const override; + + void GetBlobImplType(nsAString& aBlobImplType) const override; + + void SetLastModified(int64_t aLastModified); + + protected: + // File constructor. + MultipartBlobImpl(nsTArray<RefPtr<BlobImpl>>&& aBlobImpls, + const nsAString& aName, const nsAString& aContentType) + : BaseBlobImpl(aName, aContentType, MULTIPARTBLOBIMPL_UNKNOWN_LENGTH, + MULTIPARTBLOBIMPL_UNKNOWN_LAST_MODIFIED), + mBlobImpls(std::move(aBlobImpls)) {} + + // Blob constructor. + MultipartBlobImpl(nsTArray<RefPtr<BlobImpl>>&& aBlobImpls, + const nsAString& aContentType) + : BaseBlobImpl(aContentType, MULTIPARTBLOBIMPL_UNKNOWN_LENGTH), + mBlobImpls(std::move(aBlobImpls)) {} + + ~MultipartBlobImpl() override = default; + + void SetLengthAndModifiedDate(const Maybe<RTPCallerType>& aRTPCallerType, + ErrorResult& aRv); + + nsTArray<RefPtr<BlobImpl>> mBlobImpls; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MultipartBlobImpl_h diff --git a/dom/file/MutableBlobStorage.cpp b/dom/file/MutableBlobStorage.cpp new file mode 100644 index 0000000000..75d0dce6de --- /dev/null +++ b/dom/file/MutableBlobStorage.cpp @@ -0,0 +1,667 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "EmptyBlobImpl.h" +#include "MutableBlobStorage.h" +#include "MemoryBlobImpl.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/dom/TemporaryIPCBlobChild.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/Preferences.h" +#include "mozilla/TaskQueue.h" +#include "File.h" +#include "nsAnonymousTemporaryFile.h" +#include "nsNetCID.h" +#include "nsProxyRelease.h" + +#define BLOB_MEMORY_TEMPORARY_FILE 1048576 + +namespace mozilla::dom { + +namespace { + +// This class uses the callback to inform when the Blob is created or when the +// error must be propagated. +class BlobCreationDoneRunnable final : public Runnable { + public: + BlobCreationDoneRunnable(MutableBlobStorage* aBlobStorage, + MutableBlobStorageCallback* aCallback, + BlobImpl* aBlobImpl, nsresult aRv) + : Runnable("dom::BlobCreationDoneRunnable"), + mBlobStorage(aBlobStorage), + mCallback(aCallback), + mBlobImpl(aBlobImpl), + mRv(aRv) { + MOZ_ASSERT(aBlobStorage); + MOZ_ASSERT(aCallback); + MOZ_ASSERT((NS_FAILED(aRv) && !aBlobImpl) || + (NS_SUCCEEDED(aRv) && aBlobImpl)); + } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mBlobStorage); + mCallback->BlobStoreCompleted(mBlobStorage, mBlobImpl, mRv); + mCallback = nullptr; + mBlobImpl = nullptr; + return NS_OK; + } + + private: + ~BlobCreationDoneRunnable() override { + MOZ_ASSERT(mBlobStorage); + // If something when wrong, we still have to release these objects in the + // correct thread. + NS_ProxyRelease("BlobCreationDoneRunnable::mCallback", + mBlobStorage->EventTarget(), mCallback.forget()); + } + + RefPtr<MutableBlobStorage> mBlobStorage; + RefPtr<MutableBlobStorageCallback> mCallback; + RefPtr<BlobImpl> mBlobImpl; + nsresult mRv; +}; + +// Simple runnable to propagate the error to the BlobStorage. +class ErrorPropagationRunnable final : public Runnable { + public: + ErrorPropagationRunnable(MutableBlobStorage* aBlobStorage, nsresult aRv) + : Runnable("dom::ErrorPropagationRunnable"), + mBlobStorage(aBlobStorage), + mRv(aRv) {} + + NS_IMETHOD + Run() override { + mBlobStorage->ErrorPropagated(mRv); + return NS_OK; + } + + private: + RefPtr<MutableBlobStorage> mBlobStorage; + nsresult mRv; +}; + +// This runnable moves a buffer to the IO thread and there, it writes it into +// the temporary file, if its File Descriptor has not been already closed. +class WriteRunnable final : public Runnable { + public: + static WriteRunnable* CopyBuffer(MutableBlobStorage* aBlobStorage, + const void* aData, uint32_t aLength) { + MOZ_ASSERT(aBlobStorage); + MOZ_ASSERT(aData); + + // We have to take a copy of this buffer. + void* data = malloc(aLength); + if (!data) { + return nullptr; + } + + memcpy((char*)data, aData, aLength); + return new WriteRunnable(aBlobStorage, data, aLength); + } + + static WriteRunnable* AdoptBuffer(MutableBlobStorage* aBlobStorage, + void* aData, uint32_t aLength) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aBlobStorage); + MOZ_ASSERT(aData); + + return new WriteRunnable(aBlobStorage, aData, aLength); + } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mBlobStorage); + + PRFileDesc* fd = mBlobStorage->GetFD(); + if (!fd) { + // The file descriptor has been closed in the meantime. + return NS_OK; + } + + int32_t written = PR_Write(fd, mData, mLength); + if (NS_WARN_IF(written < 0 || uint32_t(written) != mLength)) { + mBlobStorage->CloseFD(); + return mBlobStorage->EventTarget()->Dispatch( + new ErrorPropagationRunnable(mBlobStorage, NS_ERROR_FAILURE), + NS_DISPATCH_NORMAL); + } + + return NS_OK; + } + + private: + WriteRunnable(MutableBlobStorage* aBlobStorage, void* aData, uint32_t aLength) + : Runnable("dom::WriteRunnable"), + mBlobStorage(aBlobStorage), + mData(aData), + mLength(aLength) { + MOZ_ASSERT(mBlobStorage); + MOZ_ASSERT(aData); + } + + ~WriteRunnable() override { free(mData); } + + RefPtr<MutableBlobStorage> mBlobStorage; + void* mData; + uint32_t mLength; +}; + +// This runnable closes the FD in case something goes wrong or the temporary +// file is not needed anymore. +class CloseFileRunnable final : public Runnable { + public: + explicit CloseFileRunnable(PRFileDesc* aFD) + : Runnable("dom::CloseFileRunnable"), mFD(aFD) {} + + NS_IMETHOD + Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + PR_Close(mFD); + mFD = nullptr; + return NS_OK; + } + + private: + ~CloseFileRunnable() override { + if (mFD) { + PR_Close(mFD); + } + } + + PRFileDesc* mFD; +}; + +// This runnable is dispatched to the main-thread from the IO thread and its +// task is to create the blob and inform the callback. +class CreateBlobRunnable final : public Runnable, + public TemporaryIPCBlobChildCallback { + public: + // We need to always declare refcounting because + // TemporaryIPCBlobChildCallback has pure-virtual refcounting. + NS_DECL_ISUPPORTS_INHERITED + + CreateBlobRunnable(MutableBlobStorage* aBlobStorage, + const nsACString& aContentType, + already_AddRefed<MutableBlobStorageCallback> aCallback) + : Runnable("dom::CreateBlobRunnable"), + mBlobStorage(aBlobStorage), + mContentType(aContentType), + mCallback(aCallback) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aBlobStorage); + } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mBlobStorage); + mBlobStorage->AskForBlob(this, mContentType); + return NS_OK; + } + + void OperationSucceeded(BlobImpl* aBlobImpl) override { + RefPtr<MutableBlobStorageCallback> callback(std::move(mCallback)); + callback->BlobStoreCompleted(mBlobStorage, aBlobImpl, NS_OK); + } + + void OperationFailed(nsresult aRv) override { + RefPtr<MutableBlobStorageCallback> callback(std::move(mCallback)); + callback->BlobStoreCompleted(mBlobStorage, nullptr, aRv); + } + + private: + ~CreateBlobRunnable() override { + MOZ_ASSERT(mBlobStorage); + // If something when wrong, we still have to release data in the correct + // thread. + NS_ProxyRelease("CreateBlobRunnable::mCallback", + mBlobStorage->EventTarget(), mCallback.forget()); + } + + RefPtr<MutableBlobStorage> mBlobStorage; + nsCString mContentType; + RefPtr<MutableBlobStorageCallback> mCallback; +}; + +NS_IMPL_ISUPPORTS_INHERITED0(CreateBlobRunnable, Runnable) + +// This task is used to know when the writing is completed. From the IO thread +// it dispatches a CreateBlobRunnable to the main-thread. +class LastRunnable final : public Runnable { + public: + LastRunnable(MutableBlobStorage* aBlobStorage, const nsACString& aContentType, + MutableBlobStorageCallback* aCallback) + : Runnable("dom::LastRunnable"), + mBlobStorage(aBlobStorage), + mContentType(aContentType), + mCallback(aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mBlobStorage); + MOZ_ASSERT(aCallback); + } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + + RefPtr<Runnable> runnable = + new CreateBlobRunnable(mBlobStorage, mContentType, mCallback.forget()); + return mBlobStorage->EventTarget()->Dispatch(runnable, NS_DISPATCH_NORMAL); + } + + private: + ~LastRunnable() override { + MOZ_ASSERT(mBlobStorage); + // If something when wrong, we still have to release data in the correct + // thread. + NS_ProxyRelease("LastRunnable::mCallback", mBlobStorage->EventTarget(), + mCallback.forget()); + } + + RefPtr<MutableBlobStorage> mBlobStorage; + nsCString mContentType; + RefPtr<MutableBlobStorageCallback> mCallback; +}; + +} // anonymous namespace + +MutableBlobStorage::MutableBlobStorage(MutableBlobStorageType aType, + nsIEventTarget* aEventTarget, + uint32_t aMaxMemory) + : mMutex("MutableBlobStorage::mMutex"), + mData(nullptr), + mDataLen(0), + mDataBufferLen(0), + mStorageState(aType == eOnlyInMemory ? eKeepInMemory : eInMemory), + mFD(nullptr), + mErrorResult(NS_OK), + mEventTarget(aEventTarget), + mMaxMemory(aMaxMemory) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mEventTarget) { + mEventTarget = GetMainThreadSerialEventTarget(); + } + + if (aMaxMemory == 0 && aType == eCouldBeInTemporaryFile) { + mMaxMemory = Preferences::GetUint("dom.blob.memoryToTemporaryFile", + BLOB_MEMORY_TEMPORARY_FILE); + } + + MOZ_ASSERT(mEventTarget); +} + +MutableBlobStorage::~MutableBlobStorage() { + free(mData); + + if (mFD) { + RefPtr<Runnable> runnable = new CloseFileRunnable(mFD); + (void)DispatchToIOThread(runnable.forget()); + } + + if (mTaskQueue) { + mTaskQueue->BeginShutdown(); + } + + if (mActor) { + NS_ProxyRelease("MutableBlobStorage::mActor", EventTarget(), + mActor.forget()); + } +} + +void MutableBlobStorage::GetBlobImplWhenReady( + const nsACString& aContentType, MutableBlobStorageCallback* aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aCallback); + + MutexAutoLock lock(mMutex); + + // GetBlob can be called just once. + MOZ_ASSERT(mStorageState != eClosed); + StorageState previousState = mStorageState; + mStorageState = eClosed; + + if (previousState == eInTemporaryFile) { + if (NS_FAILED(mErrorResult)) { + MOZ_ASSERT(!mActor); + + RefPtr<Runnable> runnable = + new BlobCreationDoneRunnable(this, aCallback, nullptr, mErrorResult); + EventTarget()->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + return; + } + + MOZ_ASSERT(mActor); + + // We want to wait until all the WriteRunnable are completed. The way we do + // this is to go to the I/O thread and then we come back: the runnables are + // executed in order and this LastRunnable will be... the last one. + // This Runnable will also close the FD on the I/O thread. + RefPtr<Runnable> runnable = new LastRunnable(this, aContentType, aCallback); + + // If the dispatching fails, we are shutting down and it's fine to do not + // run the callback. + (void)DispatchToIOThread(runnable.forget()); + return; + } + + // If we are waiting for the temporary file, it's better to wait... + if (previousState == eWaitingForTemporaryFile) { + mPendingContentType = aContentType; + mPendingCallback = aCallback; + return; + } + + RefPtr<BlobImpl> blobImpl; + + if (mData) { + blobImpl = new MemoryBlobImpl(mData, mDataLen, + NS_ConvertUTF8toUTF16(aContentType)); + + mData = nullptr; // The MemoryBlobImpl takes ownership of the buffer + mDataLen = 0; + mDataBufferLen = 0; + } else { + blobImpl = new EmptyBlobImpl(NS_ConvertUTF8toUTF16(aContentType)); + } + + RefPtr<BlobCreationDoneRunnable> runnable = + new BlobCreationDoneRunnable(this, aCallback, blobImpl, NS_OK); + + nsresult error = + EventTarget()->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(error))) { + return; + } +} + +nsresult MutableBlobStorage::Append(const void* aData, uint32_t aLength) { + // This method can be called on any thread. + + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mStorageState != eClosed); + NS_ENSURE_ARG_POINTER(aData); + + if (!aLength) { + return NS_OK; + } + + // If eInMemory is the current Storage state, we could maybe migrate to + // a temporary file. + if (mStorageState == eInMemory && ShouldBeTemporaryStorage(lock, aLength) && + !MaybeCreateTemporaryFile(lock)) { + return NS_ERROR_FAILURE; + } + + // If we are already in the temporaryFile mode, we have to dispatch a + // runnable. + if (mStorageState == eInTemporaryFile) { + // If a previous operation failed, let's return that error now. + if (NS_FAILED(mErrorResult)) { + return mErrorResult; + } + + RefPtr<WriteRunnable> runnable = + WriteRunnable::CopyBuffer(this, aData, aLength); + if (NS_WARN_IF(!runnable)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsresult rv = DispatchToIOThread(runnable.forget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mDataLen += aLength; + return NS_OK; + } + + // By default, we store in memory. + + uint64_t offset = mDataLen; + + if (!ExpandBufferSize(lock, aLength)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + memcpy((char*)mData + offset, aData, aLength); + return NS_OK; +} + +bool MutableBlobStorage::ExpandBufferSize(const MutexAutoLock& aProofOfLock, + uint64_t aSize) { + MOZ_ASSERT(mStorageState < eInTemporaryFile); + + if (mDataBufferLen >= mDataLen + aSize) { + mDataLen += aSize; + return true; + } + + // Start at 1 or we'll loop forever. + CheckedUint32 bufferLen = + std::max<uint32_t>(static_cast<uint32_t>(mDataBufferLen), 1); + while (bufferLen.isValid() && bufferLen.value() < mDataLen + aSize) { + bufferLen *= 2; + } + + if (!bufferLen.isValid()) { + return false; + } + + void* data = realloc(mData, bufferLen.value()); + if (!data) { + return false; + } + + mData = data; + mDataBufferLen = bufferLen.value(); + mDataLen += aSize; + return true; +} + +bool MutableBlobStorage::ShouldBeTemporaryStorage( + const MutexAutoLock& aProofOfLock, uint64_t aSize) const { + MOZ_ASSERT(mStorageState == eInMemory); + + CheckedUint32 bufferSize = mDataLen; + bufferSize += aSize; + + if (!bufferSize.isValid()) { + return false; + } + + return bufferSize.value() >= mMaxMemory; +} + +bool MutableBlobStorage::MaybeCreateTemporaryFile( + const MutexAutoLock& aProofOfLock) { + mStorageState = eWaitingForTemporaryFile; + + if (!NS_IsMainThread()) { + RefPtr<MutableBlobStorage> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "MutableBlobStorage::MaybeCreateTemporaryFile", [self]() { + MutexAutoLock lock(self->mMutex); + self->MaybeCreateTemporaryFileOnMainThread(lock); + if (!self->mActor) { + self->ErrorPropagated(NS_ERROR_FAILURE); + } + }); + EventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); + return true; + } + + MaybeCreateTemporaryFileOnMainThread(aProofOfLock); + return !!mActor; +} + +void MutableBlobStorage::MaybeCreateTemporaryFileOnMainThread( + const MutexAutoLock& aProofOfLock) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mActor); + + mozilla::ipc::PBackgroundChild* actorChild = + mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actorChild)) { + return; + } + + mActor = new TemporaryIPCBlobChild(this); + actorChild->SendPTemporaryIPCBlobConstructor(mActor); + + // We need manually to increase the reference for this actor because the + // IPC allocator method is not triggered. The Release() is called by IPDL + // when the actor is deleted. + mActor.get()->AddRef(); + + // The actor will call us when the FileDescriptor is received. +} + +void MutableBlobStorage::TemporaryFileCreated(PRFileDesc* aFD) { + MOZ_ASSERT(NS_IsMainThread()); + + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mStorageState == eWaitingForTemporaryFile || + mStorageState == eClosed); + MOZ_ASSERT_IF(mPendingCallback, mStorageState == eClosed); + MOZ_ASSERT(mActor); + MOZ_ASSERT(aFD); + + // If the object has been already closed and we don't need to execute a + // callback, we need just to close the file descriptor in the correct thread. + if (mStorageState == eClosed && !mPendingCallback) { + RefPtr<Runnable> runnable = new CloseFileRunnable(aFD); + + // If this dispatching fails, CloseFileRunnable will close the FD in the + // DTOR on the current thread. + (void)DispatchToIOThread(runnable.forget()); + + // Let's inform the parent that we have nothing else to do. + mActor->SendOperationFailed(); + mActor = nullptr; + return; + } + + // If we still receiving data, we can proceed in temporary-file mode. + if (mStorageState == eWaitingForTemporaryFile) { + mStorageState = eInTemporaryFile; + } + + mFD = aFD; + MOZ_ASSERT(NS_SUCCEEDED(mErrorResult)); + + // This runnable takes the ownership of mData and it will write this buffer + // into the temporary file. + RefPtr<WriteRunnable> runnable = + WriteRunnable::AdoptBuffer(this, mData, mDataLen); + MOZ_ASSERT(runnable); + + mData = nullptr; + + nsresult rv = DispatchToIOThread(runnable.forget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + // Shutting down, we cannot continue. + return; + } + + // If we are closed, it means that GetBlobImplWhenReady() has been called when + // we were already waiting for a temporary file-descriptor. Finally we are + // here, AdoptBuffer runnable is going to write the current buffer into this + // file. After that, there is nothing else to write, and we dispatch + // LastRunnable which ends up calling mPendingCallback via CreateBlobRunnable. + if (mStorageState == eClosed) { + MOZ_ASSERT(mPendingCallback); + + RefPtr<Runnable> runnable = + new LastRunnable(this, mPendingContentType, mPendingCallback); + (void)DispatchToIOThread(runnable.forget()); + + mPendingCallback = nullptr; + } +} + +void MutableBlobStorage::AskForBlob(TemporaryIPCBlobChildCallback* aCallback, + const nsACString& aContentType) { + MOZ_ASSERT(NS_IsMainThread()); + + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mStorageState == eClosed); + MOZ_ASSERT(mFD); + MOZ_ASSERT(mActor); + MOZ_ASSERT(aCallback); + + // Let's pass the FileDescriptor to the parent actor in order to keep the file + // locked on windows. + mActor->AskForBlob(aCallback, aContentType, mFD); + + // The previous operation has duplicated the file descriptor. Now we can close + // mFD. The parent will take care of closing the duplicated file descriptor on + // its side. + RefPtr<Runnable> runnable = new CloseFileRunnable(mFD); + (void)DispatchToIOThread(runnable.forget()); + + mFD = nullptr; + mActor = nullptr; +} + +void MutableBlobStorage::ErrorPropagated(nsresult aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + MutexAutoLock lock(mMutex); + mErrorResult = aRv; + + if (mActor) { + mActor->SendOperationFailed(); + mActor = nullptr; + } +} + +nsresult MutableBlobStorage::DispatchToIOThread( + already_AddRefed<nsIRunnable> aRunnable) { + if (!mTaskQueue) { + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target); + + mTaskQueue = TaskQueue::Create(target.forget(), "BlobStorage"); + } + + nsCOMPtr<nsIRunnable> runnable(aRunnable); + nsresult rv = mTaskQueue->Dispatch(runnable.forget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +size_t MutableBlobStorage::SizeOfCurrentMemoryBuffer() { + MOZ_ASSERT(NS_IsMainThread()); + MutexAutoLock lock(mMutex); + return mStorageState < eInTemporaryFile ? mDataLen : 0; +} + +PRFileDesc* MutableBlobStorage::GetFD() { + MOZ_ASSERT(!NS_IsMainThread()); + MutexAutoLock lock(mMutex); + return mFD; +} + +void MutableBlobStorage::CloseFD() { + MOZ_ASSERT(!NS_IsMainThread()); + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mFD); + + PR_Close(mFD); + mFD = nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/file/MutableBlobStorage.h b/dom/file/MutableBlobStorage.h new file mode 100644 index 0000000000..aa0d42c75e --- /dev/null +++ b/dom/file/MutableBlobStorage.h @@ -0,0 +1,136 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_MutableBlobStorage_h +#define mozilla_dom_MutableBlobStorage_h + +#include "mozilla/RefPtr.h" +#include "mozilla/Mutex.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "prio.h" + +class nsIEventTarget; +class nsIRunnable; + +namespace mozilla { + +class TaskQueue; + +namespace dom { + +class Blob; +class BlobImpl; +class MutableBlobStorage; +class TemporaryIPCBlobChild; +class TemporaryIPCBlobChildCallback; + +class MutableBlobStorageCallback { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void BlobStoreCompleted(MutableBlobStorage* aBlobStorage, + BlobImpl* aBlob, nsresult aRv) = 0; +}; + +// This class is must be created and used on main-thread, except for Append() +// that can be called on any thread. +class MutableBlobStorage final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MutableBlobStorage) + + enum MutableBlobStorageType { + eOnlyInMemory, + eCouldBeInTemporaryFile, + }; + + explicit MutableBlobStorage(MutableBlobStorageType aType, + nsIEventTarget* aEventTarget = nullptr, + uint32_t aMaxMemory = 0); + + nsresult Append(const void* aData, uint32_t aLength); + + // This method can be called just once. + // The callback will be called when the BlobImpl is ready. + void GetBlobImplWhenReady(const nsACString& aContentType, + MutableBlobStorageCallback* aCallback); + + void TemporaryFileCreated(PRFileDesc* aFD); + + void AskForBlob(TemporaryIPCBlobChildCallback* aCallback, + const nsACString& aContentType); + + void ErrorPropagated(nsresult aRv); + + nsIEventTarget* EventTarget() { + MOZ_ASSERT(mEventTarget); + return mEventTarget; + } + + // Returns the heap size in bytes of our internal buffers. + // Note that this intentionally ignores the data in the temp file. + size_t SizeOfCurrentMemoryBuffer(); + + PRFileDesc* GetFD(); + + void CloseFD(); + + private: + ~MutableBlobStorage(); + + bool ExpandBufferSize(const MutexAutoLock& aProofOfLock, uint64_t aSize); + + bool ShouldBeTemporaryStorage(const MutexAutoLock& aProofOfLock, + uint64_t aSize) const; + + bool MaybeCreateTemporaryFile(const MutexAutoLock& aProofOfLock); + void MaybeCreateTemporaryFileOnMainThread(const MutexAutoLock& aProofOfLock); + + [[nodiscard]] nsresult DispatchToIOThread( + already_AddRefed<nsIRunnable> aRunnable); + + Mutex mMutex MOZ_UNANNOTATED; + + // All these variables are touched on the main thread only or in the + // retargeted thread when used by Append(). They are protected by mMutex. + + void* mData; + uint64_t mDataLen; + uint64_t mDataBufferLen; + + enum StorageState { + eKeepInMemory, + eInMemory, + eWaitingForTemporaryFile, + eInTemporaryFile, + eClosed + }; + + StorageState mStorageState; + + PRFileDesc* mFD; + + nsresult mErrorResult; + + RefPtr<TaskQueue> mTaskQueue; + nsCOMPtr<nsIEventTarget> mEventTarget; + + nsCString mPendingContentType; + RefPtr<MutableBlobStorageCallback> mPendingCallback; + + RefPtr<TemporaryIPCBlobChild> mActor; + + // This value is used when we go from eInMemory to eWaitingForTemporaryFile + // and eventually eInTemporaryFile. If the size of the buffer is >= + // mMaxMemory, the creation of the temporary file will start. + // It's not used if mStorageState is eKeepInMemory. + uint32_t mMaxMemory; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MutableBlobStorage_h diff --git a/dom/file/MutableBlobStreamListener.cpp b/dom/file/MutableBlobStreamListener.cpp new file mode 100644 index 0000000000..f5f6a5bae3 --- /dev/null +++ b/dom/file/MutableBlobStreamListener.cpp @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MutableBlobStreamListener.h" +#include "MutableBlobStorage.h" +#include "nsIInputStream.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom { + +MutableBlobStreamListener::MutableBlobStreamListener( + MutableBlobStorage::MutableBlobStorageType aStorageType, + const nsACString& aContentType, MutableBlobStorageCallback* aCallback, + nsIEventTarget* aEventTarget) + : mCallback(aCallback), + mStorageType(aStorageType), + mContentType(aContentType), + mEventTarget(aEventTarget) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aCallback); + + if (!mEventTarget) { + mEventTarget = GetMainThreadSerialEventTarget(); + } + + MOZ_ASSERT(mEventTarget); +} + +MutableBlobStreamListener::~MutableBlobStreamListener() { + MOZ_ASSERT(NS_IsMainThread()); +} + +NS_IMPL_ISUPPORTS(MutableBlobStreamListener, nsIStreamListener, + nsIThreadRetargetableStreamListener, nsIRequestObserver) + +NS_IMETHODIMP +MutableBlobStreamListener::OnStartRequest(nsIRequest* aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mStorage); + MOZ_ASSERT(mEventTarget); + + mStorage = new MutableBlobStorage(mStorageType, mEventTarget); + return NS_OK; +} + +NS_IMETHODIMP +MutableBlobStreamListener::OnStopRequest(nsIRequest* aRequest, + nsresult aStatus) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mStorage); + + // Resetting mStorage to nullptr. + RefPtr<MutableBlobStorage> storage; + storage.swap(mStorage); + + // Let's propagate the error simulating a failure of the storage. + if (NS_FAILED(aStatus)) { + mCallback->BlobStoreCompleted(storage, nullptr, aStatus); + return NS_OK; + } + + storage->GetBlobImplWhenReady(mContentType, mCallback); + return NS_OK; +} + +NS_IMETHODIMP +MutableBlobStreamListener::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aStream, + uint64_t aSourceOffset, + uint32_t aCount) { + // This method could be called on any thread. + MOZ_ASSERT(mStorage); + + uint32_t countRead; + return aStream->ReadSegments(WriteSegmentFun, this, aCount, &countRead); +} + +nsresult MutableBlobStreamListener::WriteSegmentFun( + nsIInputStream* aWriterStream, void* aClosure, const char* aFromSegment, + uint32_t aToOffset, uint32_t aCount, uint32_t* aWriteCount) { + // This method could be called on any thread. + + MutableBlobStreamListener* self = + static_cast<MutableBlobStreamListener*>(aClosure); + MOZ_ASSERT(self->mStorage); + + nsresult rv = self->mStorage->Append(aFromSegment, aCount); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aWriteCount = aCount; + return NS_OK; +} + +NS_IMETHODIMP +MutableBlobStreamListener::CheckListenerChain() { return NS_OK; } + +} // namespace mozilla::dom diff --git a/dom/file/MutableBlobStreamListener.h b/dom/file/MutableBlobStreamListener.h new file mode 100644 index 0000000000..6fd32848a7 --- /dev/null +++ b/dom/file/MutableBlobStreamListener.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_MutableBlobStreamListener_h +#define mozilla_dom_MutableBlobStreamListener_h + +#include "nsIStreamListener.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsTString.h" +#include "mozilla/dom/MutableBlobStorage.h" + +class nsIEventTarget; + +namespace mozilla::dom { + +class MutableBlobStreamListener final + : public nsIStreamListener, + public nsIThreadRetargetableStreamListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + MutableBlobStreamListener( + MutableBlobStorage::MutableBlobStorageType aStorageType, + const nsACString& aContentType, MutableBlobStorageCallback* aCallback, + nsIEventTarget* aEventTarget = nullptr); + + private: + ~MutableBlobStreamListener(); + + static nsresult WriteSegmentFun(nsIInputStream* aWriter, void* aClosure, + const char* aFromSegment, uint32_t aOffset, + uint32_t aCount, uint32_t* aWriteCount); + + RefPtr<MutableBlobStorage> mStorage; + RefPtr<MutableBlobStorageCallback> mCallback; + + MutableBlobStorage::MutableBlobStorageType mStorageType; + nsCString mContentType; + nsCOMPtr<nsIEventTarget> mEventTarget; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_MutableBlobStreamListener_h diff --git a/dom/file/StreamBlobImpl.cpp b/dom/file/StreamBlobImpl.cpp new file mode 100644 index 0000000000..9d0c8d86d1 --- /dev/null +++ b/dom/file/StreamBlobImpl.cpp @@ -0,0 +1,224 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "EmptyBlobImpl.h" +#include "mozilla/InputStreamLengthWrapper.h" +#include "mozilla/SlicedInputStream.h" +#include "StreamBlobImpl.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsICloneableInputStream.h" +#include "nsIEventTarget.h" +#include "nsIPipe.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS_INHERITED(StreamBlobImpl, BlobImpl, nsIMemoryReporter) + +static already_AddRefed<nsICloneableInputStream> EnsureCloneableStream( + nsIInputStream* aInputStream, uint64_t aLength) { + nsCOMPtr<nsICloneableInputStream> cloneable = do_QueryInterface(aInputStream); + if (cloneable && cloneable->GetCloneable()) { + return cloneable.forget(); + } + + // If the stream we're copying is known to be small, specify the size of the + // pipe's segments precisely to limit wasted space. An extra byte above length + // is included to avoid allocating an extra segment immediately before reading + // EOF from the source stream. Otherwise, allocate 64k buffers rather than + // the default of 4k buffers to reduce segment counts for very large payloads. + static constexpr uint32_t kBaseSegmentSize = 64 * 1024; + uint32_t segmentSize = kBaseSegmentSize; + if (aLength + 1 <= kBaseSegmentSize * 4) { + segmentSize = aLength + 1; + } + + // NOTE: We specify unlimited segments to eagerly build a complete copy of the + // source stream locally without waiting for the blob to be consumed. + nsCOMPtr<nsIAsyncInputStream> reader; + nsCOMPtr<nsIAsyncOutputStream> writer; + NS_NewPipe2(getter_AddRefs(reader), getter_AddRefs(writer), true, true, + segmentSize, UINT32_MAX); + + nsresult rv; + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + rv = NS_AsyncCopy(aInputStream, writer, target, + NS_ASYNCCOPY_VIA_WRITESEGMENTS, segmentSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + cloneable = do_QueryInterface(reader); + MOZ_ASSERT(cloneable && cloneable->GetCloneable()); + return cloneable.forget(); +} + +/* static */ +already_AddRefed<StreamBlobImpl> StreamBlobImpl::Create( + already_AddRefed<nsIInputStream> aInputStream, + const nsAString& aContentType, uint64_t aLength, + const nsAString& aBlobImplType) { + nsCOMPtr<nsIInputStream> inputStream = std::move(aInputStream); + nsCOMPtr<nsICloneableInputStream> cloneable = + EnsureCloneableStream(inputStream, aLength); + + RefPtr<StreamBlobImpl> blobImplStream = new StreamBlobImpl( + cloneable.forget(), aContentType, aLength, aBlobImplType); + blobImplStream->MaybeRegisterMemoryReporter(); + return blobImplStream.forget(); +} + +/* static */ +already_AddRefed<StreamBlobImpl> StreamBlobImpl::Create( + already_AddRefed<nsIInputStream> aInputStream, const nsAString& aName, + const nsAString& aContentType, int64_t aLastModifiedDate, uint64_t aLength, + const nsAString& aBlobImplType) { + nsCOMPtr<nsIInputStream> inputStream = std::move(aInputStream); + nsCOMPtr<nsICloneableInputStream> cloneable = + EnsureCloneableStream(inputStream, aLength); + + RefPtr<StreamBlobImpl> blobImplStream = + new StreamBlobImpl(cloneable.forget(), aName, aContentType, + aLastModifiedDate, aLength, aBlobImplType); + blobImplStream->MaybeRegisterMemoryReporter(); + return blobImplStream.forget(); +} + +StreamBlobImpl::StreamBlobImpl( + already_AddRefed<nsICloneableInputStream> aInputStream, + const nsAString& aContentType, uint64_t aLength, + const nsAString& aBlobImplType) + : BaseBlobImpl(aContentType, aLength), + mInputStream(std::move(aInputStream)), + mBlobImplType(aBlobImplType), + mIsDirectory(false), + mFileId(-1) {} + +StreamBlobImpl::StreamBlobImpl( + already_AddRefed<nsICloneableInputStream> aInputStream, + const nsAString& aName, const nsAString& aContentType, + int64_t aLastModifiedDate, uint64_t aLength, const nsAString& aBlobImplType) + : BaseBlobImpl(aName, aContentType, aLength, aLastModifiedDate), + mInputStream(std::move(aInputStream)), + mBlobImplType(aBlobImplType), + mIsDirectory(false), + mFileId(-1) {} + +StreamBlobImpl::~StreamBlobImpl() { + if (mInputStream) { + nsCOMPtr<nsIInputStream> stream = do_QueryInterface(mInputStream); + stream->Close(); + } + UnregisterWeakMemoryReporter(this); +} + +void StreamBlobImpl::CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const { + if (!mInputStream) { + // We failed to clone the input stream in EnsureCloneableStream. + *aStream = nullptr; + aRv.ThrowUnknownError("failed to read blob data"); + return; + } + + nsCOMPtr<nsIInputStream> clonedStream; + aRv = mInputStream->Clone(getter_AddRefs(clonedStream)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr<nsIInputStream> wrappedStream = + InputStreamLengthWrapper::MaybeWrap(clonedStream.forget(), mLength); + + wrappedStream.forget(aStream); +} + +already_AddRefed<BlobImpl> StreamBlobImpl::CreateSlice( + uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + ErrorResult& aRv) const { + if (!aLength) { + RefPtr<BlobImpl> impl = new EmptyBlobImpl(aContentType); + return impl.forget(); + } + + nsCOMPtr<nsIInputStream> clonedStream; + + nsCOMPtr<nsICloneableInputStreamWithRange> stream = + do_QueryInterface(mInputStream); + if (stream) { + aRv = stream->CloneWithRange(aStart, aLength, getter_AddRefs(clonedStream)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } else { + CreateInputStream(getter_AddRefs(clonedStream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + clonedStream = + new SlicedInputStream(clonedStream.forget(), aStart, aLength); + } + + MOZ_ASSERT(clonedStream); + + return StreamBlobImpl::Create(clonedStream.forget(), aContentType, aLength, + mBlobImplType); +} + +void StreamBlobImpl::MaybeRegisterMemoryReporter() { + // We report only stringInputStream. + nsCOMPtr<nsIStringInputStream> stringInputStream = + do_QueryInterface(mInputStream); + if (!stringInputStream) { + return; + } + + RegisterWeakMemoryReporter(this); +} + +NS_IMETHODIMP +StreamBlobImpl::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) { + nsCOMPtr<nsIStringInputStream> stringInputStream = + do_QueryInterface(mInputStream); + if (!stringInputStream) { + return NS_OK; + } + + MOZ_COLLECT_REPORT( + "explicit/dom/memory-file-data/stream", KIND_HEAP, UNITS_BYTES, + stringInputStream->SizeOfIncludingThisIfUnshared(MallocSizeOf), + "Memory used to back a File/Blob based on an input stream."); + + return NS_OK; +} + +size_t StreamBlobImpl::GetAllocationSize() const { + nsCOMPtr<nsIStringInputStream> stringInputStream = + do_QueryInterface(mInputStream); + if (!stringInputStream) { + return 0; + } + + return stringInputStream->SizeOfIncludingThisEvenIfShared(MallocSizeOf); +} + +void StreamBlobImpl::GetBlobImplType(nsAString& aBlobImplType) const { + aBlobImplType.AssignLiteral("StreamBlobImpl["); + aBlobImplType.Append(mBlobImplType); + aBlobImplType.AppendLiteral("]"); +} + +} // namespace mozilla::dom diff --git a/dom/file/StreamBlobImpl.h b/dom/file/StreamBlobImpl.h new file mode 100644 index 0000000000..6f35638463 --- /dev/null +++ b/dom/file/StreamBlobImpl.h @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_StreamBlobImpl_h +#define mozilla_dom_StreamBlobImpl_h + +#include "BaseBlobImpl.h" +#include "nsCOMPtr.h" +#include "nsIMemoryReporter.h" +#include "nsICloneableInputStream.h" + +namespace mozilla::dom { + +class StreamBlobImpl final : public BaseBlobImpl, public nsIMemoryReporter { + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIMEMORYREPORTER + + // Blob constructor. + static already_AddRefed<StreamBlobImpl> Create( + already_AddRefed<nsIInputStream> aInputStream, + const nsAString& aContentType, uint64_t aLength, + const nsAString& aBlobImplType); + + // File constructor. + static already_AddRefed<StreamBlobImpl> Create( + already_AddRefed<nsIInputStream> aInputStream, const nsAString& aName, + const nsAString& aContentType, int64_t aLastModifiedDate, + uint64_t aLength, const nsAString& aBlobImplType); + + void CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const override; + + already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const override; + + bool IsMemoryFile() const override { return true; } + + int64_t GetFileId() const override { return mFileId; } + + void SetFileId(int64_t aFileId) { mFileId = aFileId; } + + void SetFullPath(const nsAString& aFullPath) { mFullPath = aFullPath; } + + void GetMozFullPathInternal(nsAString& aFullPath, ErrorResult& aRv) override { + aFullPath = mFullPath; + } + + void SetIsDirectory(bool aIsDirectory) { mIsDirectory = aIsDirectory; } + + bool IsDirectory() const override { return mIsDirectory; } + + size_t GetAllocationSize() const override; + + size_t GetAllocationSize( + FallibleTArray<BlobImpl*>& aVisitedBlobImpls) const override { + return GetAllocationSize(); + } + + void GetBlobImplType(nsAString& aBlobImplType) const override; + + private: + // Blob constructor. + StreamBlobImpl(already_AddRefed<nsICloneableInputStream> aInputStream, + const nsAString& aContentType, uint64_t aLength, + const nsAString& aBlobImplType); + + // File constructor. + StreamBlobImpl(already_AddRefed<nsICloneableInputStream> aInputStream, + const nsAString& aName, const nsAString& aContentType, + int64_t aLastModifiedDate, uint64_t aLength, + const nsAString& aBlobImplType); + + ~StreamBlobImpl() override; + + void MaybeRegisterMemoryReporter(); + + nsCOMPtr<nsICloneableInputStream> mInputStream; + + nsString mBlobImplType; + + nsString mFullPath; + bool mIsDirectory; + int64_t mFileId; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_StreamBlobImpl_h diff --git a/dom/file/StringBlobImpl.cpp b/dom/file/StringBlobImpl.cpp new file mode 100644 index 0000000000..8cb657ca5f --- /dev/null +++ b/dom/file/StringBlobImpl.cpp @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "StringBlobImpl.h" +#include "nsStringStream.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS_INHERITED(StringBlobImpl, BlobImpl, nsIMemoryReporter) + +/* static */ +already_AddRefed<StringBlobImpl> StringBlobImpl::Create( + const nsACString& aData, const nsAString& aContentType) { + RefPtr<StringBlobImpl> blobImpl = new StringBlobImpl(aData, aContentType); + RegisterWeakMemoryReporter(blobImpl); + return blobImpl.forget(); +} + +StringBlobImpl::StringBlobImpl(const nsACString& aData, + const nsAString& aContentType) + : BaseBlobImpl(aContentType, aData.Length()), mData(aData) {} + +StringBlobImpl::~StringBlobImpl() { UnregisterWeakMemoryReporter(this); } + +already_AddRefed<BlobImpl> StringBlobImpl::CreateSlice( + uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + ErrorResult& aRv) const { + RefPtr<BlobImpl> impl = + new StringBlobImpl(Substring(mData, aStart, aLength), aContentType); + return impl.forget(); +} + +void StringBlobImpl::CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const { + aRv = NS_NewCStringInputStream(aStream, mData); +} + +NS_IMETHODIMP +StringBlobImpl::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) { + MOZ_COLLECT_REPORT("explicit/dom/memory-file-data/string", KIND_HEAP, + UNITS_BYTES, + mData.SizeOfExcludingThisIfUnshared(MallocSizeOf), + "Memory used to back a File/Blob based on a string."); + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/file/StringBlobImpl.h b/dom/file/StringBlobImpl.h new file mode 100644 index 0000000000..3b3037e693 --- /dev/null +++ b/dom/file/StringBlobImpl.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_StringBlobImpl_h +#define mozilla_dom_StringBlobImpl_h + +#include "BaseBlobImpl.h" +#include "nsIMemoryReporter.h" + +namespace mozilla::dom { + +class StringBlobImpl final : public BaseBlobImpl, public nsIMemoryReporter { + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIMEMORYREPORTER + + static already_AddRefed<StringBlobImpl> Create(const nsACString& aData, + const nsAString& aContentType); + + void CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const override; + + already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const override; + + size_t GetAllocationSize() const override { return mData.Length(); } + + size_t GetAllocationSize( + FallibleTArray<BlobImpl*>& aVisitedBlobImpls) const override { + return GetAllocationSize(); + } + + void GetBlobImplType(nsAString& aBlobImplType) const override { + aBlobImplType = u"StringBlobImpl"_ns; + } + + private: + StringBlobImpl(const nsACString& aData, const nsAString& aContentType); + + ~StringBlobImpl() override; + + nsCString mData; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_StringBlobImpl_h diff --git a/dom/file/TemporaryFileBlobImpl.cpp b/dom/file/TemporaryFileBlobImpl.cpp new file mode 100644 index 0000000000..505d4a96d4 --- /dev/null +++ b/dom/file/TemporaryFileBlobImpl.cpp @@ -0,0 +1,126 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TemporaryFileBlobImpl.h" + +#include "RemoteLazyInputStreamThread.h" +#include "mozilla/ErrorResult.h" +#include "nsFileStreams.h" +#include "nsIFile.h" +#include "nsIFileStreams.h" +#include "nsNetUtil.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" + +using namespace mozilla::ipc; + +namespace mozilla::dom { + +namespace { + +// Here the flags needed in order to keep the temporary file opened. +// 1. REOPEN_ON_REWIND -> otherwise the stream is not serializable more than +// once. +// 2. no DEFER_OPEN -> the file must be kept open on windows in order to be +// deleted when used. +// 3. no CLOSE_ON_EOF -> the file will be closed by the DTOR. No needs. Also +// because the inputStream will not be read directly. +// 4. no SHARE_DELETE -> We don't want to allow this file to be deleted. +const uint32_t sTemporaryFileStreamFlags = nsIFileInputStream::REOPEN_ON_REWIND; + +class TemporaryFileInputStream final : public nsFileInputStream { + public: + static nsresult Create(nsIFile* aFile, nsIInputStream** aInputStream) { + MOZ_ASSERT(aFile); + MOZ_ASSERT(aInputStream); + MOZ_ASSERT(XRE_IsParentProcess()); + + RefPtr<TemporaryFileInputStream> stream = + new TemporaryFileInputStream(aFile); + + nsresult rv = stream->Init(aFile, -1, -1, sTemporaryFileStreamFlags); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stream.forget(aInputStream); + return NS_OK; + } + + void Serialize(InputStreamParams& aParams, uint32_t aMaxSize, + uint32_t* aSizeUsed) override { + MOZ_CRASH("This inputStream cannot be serialized."); + } + + bool Deserialize(const InputStreamParams& aParams) override { + MOZ_CRASH("This inputStream cannot be deserialized."); + return false; + } + + private: + explicit TemporaryFileInputStream(nsIFile* aFile) : mFile(aFile) { + MOZ_ASSERT(XRE_IsParentProcess()); + } + + ~TemporaryFileInputStream() override { + // Let's delete the file on the RemoteLazyInputStream Thread. + RefPtr<RemoteLazyInputStreamThread> thread = + RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return; + } + + nsCOMPtr<nsIFile> file = std::move(mFile); + thread->Dispatch( + NS_NewRunnableFunction("TemporaryFileInputStream::Runnable", + [file]() { file->Remove(false); })); + } + + nsCOMPtr<nsIFile> mFile; +}; + +} // namespace + +TemporaryFileBlobImpl::TemporaryFileBlobImpl(nsIFile* aFile, + const nsAString& aContentType) + : FileBlobImpl(aFile, u""_ns, aContentType) +#ifdef DEBUG + , + mInputStreamCreated(false) +#endif +{ + MOZ_ASSERT(XRE_IsParentProcess()); + + // This must be considered a blob. + mIsFile = false; +} + +TemporaryFileBlobImpl::~TemporaryFileBlobImpl() { + MOZ_ASSERT(mInputStreamCreated); +} + +already_AddRefed<BlobImpl> TemporaryFileBlobImpl::CreateSlice( + uint64_t aStart, uint64_t aLength, const nsAString& aContentType, + ErrorResult& aRv) const { + MOZ_CRASH("This BlobImpl is not meant to be sliced!"); + return nullptr; +} + +void TemporaryFileBlobImpl::CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const { +#ifdef DEBUG + MOZ_ASSERT(!mInputStreamCreated); + // CreateInputStream can be called only once. + mInputStreamCreated = true; +#endif + + aRv = TemporaryFileInputStream::Create(mFile, aStream); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + +} // namespace mozilla::dom diff --git a/dom/file/TemporaryFileBlobImpl.h b/dom/file/TemporaryFileBlobImpl.h new file mode 100644 index 0000000000..d11c67f955 --- /dev/null +++ b/dom/file/TemporaryFileBlobImpl.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_TemporaryFileBlobImpl_h +#define mozilla_dom_TemporaryFileBlobImpl_h + +#include "FileBlobImpl.h" + +namespace mozilla::dom { + +// This class is meant to be used by TemporaryIPCBlobParent only. +// Don't use it for anything else, please! +// Note that CreateInputStream() _must_ be called, and called just once! + +// This class is a BlobImpl because it needs to be sent via IPC using +// IPCBlobUtils. +class TemporaryFileBlobImpl final : public FileBlobImpl { +#ifdef DEBUG + mutable bool mInputStreamCreated; +#endif + + public: + explicit TemporaryFileBlobImpl(nsIFile* aFile, const nsAString& aContentType); + + // Overrides + void CreateInputStream(nsIInputStream** aStream, + ErrorResult& aRv) const override; + + void GetBlobImplType(nsAString& aBlobImplType) const override { + aBlobImplType = u"TemporaryFileBlobImpl"_ns; + } + + protected: + ~TemporaryFileBlobImpl() override; + + private: + already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TemporaryFileBlobImpl_h diff --git a/dom/file/ipc/FileCreatorChild.cpp b/dom/file/ipc/FileCreatorChild.cpp new file mode 100644 index 0000000000..95ac19f17b --- /dev/null +++ b/dom/file/ipc/FileCreatorChild.cpp @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileCreatorChild.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/IPCBlobUtils.h" + +namespace mozilla::dom { + +FileCreatorChild::FileCreatorChild() = default; + +FileCreatorChild::~FileCreatorChild() { MOZ_ASSERT(!mPromise); } + +void FileCreatorChild::SetPromise(Promise* aPromise) { + MOZ_ASSERT(aPromise); + MOZ_ASSERT(!mPromise); + + mPromise = aPromise; +} + +mozilla::ipc::IPCResult FileCreatorChild::Recv__delete__( + const FileCreationResult& aResult) { + MOZ_ASSERT(mPromise); + + RefPtr<Promise> promise; + promise.swap(mPromise); + + if (aResult.type() == FileCreationResult::TFileCreationErrorResult) { + promise->MaybeReject(aResult.get_FileCreationErrorResult().errorCode()); + return IPC_OK(); + } + + MOZ_ASSERT(aResult.type() == FileCreationResult::TFileCreationSuccessResult); + + RefPtr<dom::BlobImpl> impl = dom::IPCBlobUtils::Deserialize( + aResult.get_FileCreationSuccessResult().blob()); + + RefPtr<File> file = File::Create(promise->GetParentObject(), impl); + if (NS_WARN_IF(!file)) { + promise->MaybeReject(NS_ERROR_FAILURE); + return IPC_OK(); + } + + promise->MaybeResolve(file); + return IPC_OK(); +} + +void FileCreatorChild::ActorDestroy(ActorDestroyReason aWhy) { + if (mPromise) { + mPromise->MaybeReject(NS_ERROR_FAILURE); + mPromise = nullptr; + } +}; + +} // namespace mozilla::dom diff --git a/dom/file/ipc/FileCreatorChild.h b/dom/file/ipc/FileCreatorChild.h new file mode 100644 index 0000000000..eb5dc09d92 --- /dev/null +++ b/dom/file/ipc/FileCreatorChild.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileCreatorChild_h +#define mozilla_dom_FileCreatorChild_h + +#include "mozilla/dom/PFileCreatorChild.h" + +namespace mozilla::dom { + +class FileCreatorChild final : public mozilla::dom::PFileCreatorChild { + friend class mozilla::dom::PFileCreatorChild; + + public: + FileCreatorChild(); + ~FileCreatorChild() override; + + void SetPromise(Promise* aPromise); + + private: + mozilla::ipc::IPCResult Recv__delete__(const FileCreationResult& aResult); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + RefPtr<Promise> mPromise; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileCreatorChild_h diff --git a/dom/file/ipc/FileCreatorParent.cpp b/dom/file/ipc/FileCreatorParent.cpp new file mode 100644 index 0000000000..77f8efd483 --- /dev/null +++ b/dom/file/ipc/FileCreatorParent.cpp @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileCreatorParent.h" +#include "mozilla/dom/FileBlobImpl.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/MultipartBlobImpl.h" +#include "nsIFile.h" + +namespace mozilla::dom { + +FileCreatorParent::FileCreatorParent() + : mBackgroundEventTarget(GetCurrentSerialEventTarget()), mIPCActive(true) {} + +FileCreatorParent::~FileCreatorParent() = default; + +mozilla::ipc::IPCResult FileCreatorParent::CreateAndShareFile( + const nsAString& aFullPath, const nsAString& aType, const nsAString& aName, + const Maybe<int64_t>& aLastModified, const bool& aExistenceCheck, + const bool& aIsFromNsIFile) { + RefPtr<dom::BlobImpl> blobImpl; + nsresult rv = + CreateBlobImpl(aFullPath, aType, aName, aLastModified.isSome(), + aLastModified.isSome() ? aLastModified.value() : 0, + aExistenceCheck, aIsFromNsIFile, getter_AddRefs(blobImpl)); + if (NS_WARN_IF(NS_FAILED(rv))) { + (void)Send__delete__(this, FileCreationErrorResult(rv)); + return IPC_OK(); + } + + MOZ_ASSERT(blobImpl); + + // FileBlobImpl is unable to return the correct type on this thread because + // nsIMIMEService is not thread-safe. We must exec the 'type' getter on + // main-thread before send the blob to the child actor. + + RefPtr<FileCreatorParent> self = this; + NS_DispatchToMainThread(NS_NewRunnableFunction( + "FileCreatorParent::CreateAndShareFile", [self, blobImpl]() { + nsAutoString type; + blobImpl->GetType(type); + + self->mBackgroundEventTarget->Dispatch(NS_NewRunnableFunction( + "FileCreatorParent::CreateAndShareFile return", [self, blobImpl]() { + if (self->mIPCActive) { + IPCBlob ipcBlob; + nsresult rv = dom::IPCBlobUtils::Serialize(blobImpl, ipcBlob); + if (NS_WARN_IF(NS_FAILED(rv))) { + (void)Send__delete__(self, FileCreationErrorResult(rv)); + return; + } + + (void)Send__delete__(self, FileCreationSuccessResult(ipcBlob)); + } + })); + })); + + return IPC_OK(); +} + +void FileCreatorParent::ActorDestroy(ActorDestroyReason aWhy) { + mIPCActive = false; +} + +/* static */ +nsresult FileCreatorParent::CreateBlobImpl( + const nsAString& aPath, const nsAString& aType, const nsAString& aName, + bool aLastModifiedPassed, int64_t aLastModified, bool aExistenceCheck, + bool aIsFromNsIFile, BlobImpl** aBlobImpl) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsIFile> file; + nsresult rv = NS_NewLocalFile(aPath, true, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool exists; + rv = file->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aExistenceCheck) { + if (!exists) { + return NS_ERROR_FILE_NOT_FOUND; + } + + bool isDir; + rv = file->IsDirectory(&isDir); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (isDir) { + return NS_ERROR_FILE_IS_DIRECTORY; + } + } + + RefPtr<FileBlobImpl> impl = new FileBlobImpl(file); + + // If the file doesn't exist, we cannot have its path, its size and so on. + // Let's set them now. + if (!exists) { + MOZ_ASSERT(!aExistenceCheck); + + impl->SetMozFullPath(aPath); + impl->SetLastModified(0); + impl->SetEmptySize(); + } + + if (!aName.IsEmpty()) { + impl->SetName(aName); + } + + if (!aType.IsEmpty()) { + impl->SetType(aType); + } + + if (aLastModifiedPassed) { + impl->SetLastModified(aLastModified); + } + + if (!aIsFromNsIFile) { + impl->SetMozFullPath(u""_ns); + } + + impl.forget(aBlobImpl); + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/file/ipc/FileCreatorParent.h b/dom/file/ipc/FileCreatorParent.h new file mode 100644 index 0000000000..f43dbee886 --- /dev/null +++ b/dom/file/ipc/FileCreatorParent.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileCreatorParent_h +#define mozilla_dom_FileCreatorParent_h + +#include "mozilla/dom/PFileCreatorParent.h" + +class nsIFile; + +namespace mozilla::dom { + +class BlobImpl; + +class FileCreatorParent final : public mozilla::dom::PFileCreatorParent { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileCreatorParent) + + FileCreatorParent(); + + mozilla::ipc::IPCResult CreateAndShareFile( + const nsAString& aFullPath, const nsAString& aType, + const nsAString& aName, const Maybe<int64_t>& aLastModified, + const bool& aExistenceCheck, const bool& aIsFromNsIFile); + + private: + ~FileCreatorParent() override; + + void ActorDestroy(ActorDestroyReason aWhy) override; + + nsresult CreateBlobImpl(const nsAString& aPath, const nsAString& aType, + const nsAString& aName, bool aLastModifiedPassed, + int64_t aLastModified, bool aExistenceCheck, + bool aIsFromNsIFile, BlobImpl** aBlobImpl); + + nsCOMPtr<nsIEventTarget> mBackgroundEventTarget; + bool mIPCActive; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileCreatorParent_h diff --git a/dom/file/ipc/IPCBlob.ipdlh b/dom/file/ipc/IPCBlob.ipdlh new file mode 100644 index 0000000000..4db1c662bd --- /dev/null +++ b/dom/file/ipc/IPCBlob.ipdlh @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include IPCStream; +include ProtocolTypes; + +using struct mozilla::void_t from "mozilla/ipc/IPCCore.h"; +[RefCounted] using class mozilla::RemoteLazyInputStream from "mozilla/RemoteLazyInputStream.h"; + +namespace mozilla { + +union RemoteLazyStream +{ + // Parent to Child: The child will receive a RemoteLazyInputStream. Nothing + // can be done with it except retrieving the size. + nullable RemoteLazyInputStream; + + // Child to Parent: Normal serialization. + IPCStream; +}; + +namespace dom { + +// This contains any extra bit for making a File out of a Blob. +// For more information about Blobs and IPC, please read the comments in +// IPCBlobUtils.h + +struct IPCFile +{ + nsString name; + int64_t lastModified; + nsString DOMPath; + nsString fullPath; + + // Useful for Entries API. + bool isDirectory; +}; + +struct IPCBlob +{ + nsString type; + uint64_t size; + nsString blobImplType; + + RemoteLazyStream inputStream; + + // Nothing is for Blob + IPCFile? file; + + // This ID is used only by indexedDB tests. + int64_t fileId; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/file/ipc/IPCBlobUtils.cpp b/dom/file/ipc/IPCBlobUtils.cpp new file mode 100644 index 0000000000..78409da529 --- /dev/null +++ b/dom/file/ipc/IPCBlobUtils.cpp @@ -0,0 +1,179 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "IPCBlobUtils.h" +#include "RemoteLazyInputStream.h" +#include "RemoteLazyInputStreamChild.h" +#include "RemoteLazyInputStreamParent.h" +#include "mozilla/dom/IPCBlob.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "RemoteLazyInputStreamStorage.h" +#include "StreamBlobImpl.h" +#include "prtime.h" + +namespace mozilla::dom::IPCBlobUtils { + +already_AddRefed<BlobImpl> Deserialize(const IPCBlob& aIPCBlob) { + nsCOMPtr<nsIInputStream> inputStream; + + const RemoteLazyStream& stream = aIPCBlob.inputStream(); + switch (stream.type()) { + // Parent to child: when an nsIInputStream is sent from parent to child, the + // child receives a RemoteLazyInputStream actor. + case RemoteLazyStream::TRemoteLazyInputStream: { + inputStream = stream.get_RemoteLazyInputStream(); + break; + } + + // Child to Parent: when a blob is created on the content process send it's + // sent to the parent, we have an IPCStream object. + case RemoteLazyStream::TIPCStream: + MOZ_ASSERT(XRE_IsParentProcess()); + inputStream = DeserializeIPCStream(stream.get_IPCStream()); + break; + + default: + MOZ_CRASH("Unknown type."); + break; + } + + MOZ_ASSERT(inputStream); + + RefPtr<StreamBlobImpl> blobImpl; + + if (aIPCBlob.file().isNothing()) { + blobImpl = StreamBlobImpl::Create(inputStream.forget(), aIPCBlob.type(), + aIPCBlob.size(), aIPCBlob.blobImplType()); + } else { + const IPCFile& file = aIPCBlob.file().ref(); + blobImpl = StreamBlobImpl::Create(inputStream.forget(), file.name(), + aIPCBlob.type(), file.lastModified(), + aIPCBlob.size(), aIPCBlob.blobImplType()); + blobImpl->SetDOMPath(file.DOMPath()); + blobImpl->SetFullPath(file.fullPath()); + blobImpl->SetIsDirectory(file.isDirectory()); + } + + blobImpl->SetFileId(aIPCBlob.fileId()); + + return blobImpl.forget(); +} + +nsresult Serialize(BlobImpl* aBlobImpl, IPCBlob& aIPCBlob) { + MOZ_ASSERT(aBlobImpl); + + nsAutoString value; + aBlobImpl->GetType(value); + aIPCBlob.type() = value; + + aBlobImpl->GetBlobImplType(value); + aIPCBlob.blobImplType() = value; + + ErrorResult rv; + aIPCBlob.size() = aBlobImpl->GetSize(rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + if (!aBlobImpl->IsFile()) { + aIPCBlob.file() = Nothing(); + } else { + IPCFile file; + + aBlobImpl->GetName(value); + file.name() = value; + + file.lastModified() = aBlobImpl->GetLastModified(rv) * PR_USEC_PER_MSEC; + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + aBlobImpl->GetDOMPath(value); + file.DOMPath() = value; + + aBlobImpl->GetMozFullPathInternal(value, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + file.fullPath() = value; + + file.isDirectory() = aBlobImpl->IsDirectory(); + + aIPCBlob.file() = Some(file); + } + + aIPCBlob.fileId() = aBlobImpl->GetFileId(); + + nsCOMPtr<nsIInputStream> inputStream; + aBlobImpl->CreateInputStream(getter_AddRefs(inputStream), rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + if (XRE_IsParentProcess()) { + RefPtr<RemoteLazyInputStream> stream = + RemoteLazyInputStream::WrapStream(inputStream); + if (NS_WARN_IF(!stream)) { + return NS_ERROR_FAILURE; + } + + aIPCBlob.inputStream() = stream; + return NS_OK; + } + + mozilla::ipc::IPCStream stream; + if (!mozilla::ipc::SerializeIPCStream(inputStream.forget(), stream, + /* aAllowLazy */ true)) { + return NS_ERROR_FAILURE; + } + aIPCBlob.inputStream() = stream; + return NS_OK; +} + +} // namespace mozilla::dom::IPCBlobUtils + +namespace IPC { + +void ParamTraits<mozilla::dom::BlobImpl*>::Write( + IPC::MessageWriter* aWriter, mozilla::dom::BlobImpl* aParam) { + nsresult rv; + mozilla::dom::IPCBlob ipcblob; + if (aParam) { + rv = mozilla::dom::IPCBlobUtils::Serialize(aParam, ipcblob); + } + if (!aParam || NS_WARN_IF(NS_FAILED(rv))) { + WriteParam(aWriter, false); + } else { + WriteParam(aWriter, true); + WriteParam(aWriter, ipcblob); + } +} + +bool ParamTraits<mozilla::dom::BlobImpl*>::Read( + IPC::MessageReader* aReader, RefPtr<mozilla::dom::BlobImpl>* aResult) { + *aResult = nullptr; + + bool notnull = false; + if (!ReadParam(aReader, ¬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<BlobImpl> Deserialize(const IPCBlob& aIPCBlob); + +nsresult Serialize(BlobImpl* aBlobImpl, IPCBlob& aIPCBlob); + +} // namespace IPCBlobUtils +} // namespace mozilla::dom + +namespace IPC { + +// ParamTraits implementation for BlobImpl. N.B: If the original BlobImpl cannot +// be successfully serialized, a warning will be produced and a nullptr will be +// sent over the wire. When Read()-ing a BlobImpl, +// __always make sure to handle null!__ +template <> +struct ParamTraits<mozilla::dom::BlobImpl*> { + static void Write(IPC::MessageWriter* aWriter, + mozilla::dom::BlobImpl* aParam); + static bool Read(IPC::MessageReader* aReader, + RefPtr<mozilla::dom::BlobImpl>* aResult); +}; + +} // namespace IPC + +#endif // mozilla_dom_IPCBlobUtils_h diff --git a/dom/file/ipc/PFileCreator.ipdl b/dom/file/ipc/PFileCreator.ipdl new file mode 100644 index 0000000000..d690820906 --- /dev/null +++ b/dom/file/ipc/PFileCreator.ipdl @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; + +include IPCBlob; + +namespace mozilla { +namespace dom { + +struct FileCreationSuccessResult +{ + IPCBlob blob; +}; + +struct FileCreationErrorResult +{ + nsresult errorCode; +}; + +union FileCreationResult +{ + FileCreationSuccessResult; + FileCreationErrorResult; +}; + +[ManualDealloc] +protocol PFileCreator +{ + manager PBackground; + +child: + async __delete__(FileCreationResult aResult); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/file/ipc/PRemoteLazyInputStream.ipdl b/dom/file/ipc/PRemoteLazyInputStream.ipdl new file mode 100644 index 0000000000..82fab20cdb --- /dev/null +++ b/dom/file/ipc/PRemoteLazyInputStream.ipdl @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include IPCStream; + +namespace mozilla { + +protocol PRemoteLazyInputStream +{ +parent: + async Clone(Endpoint<PRemoteLazyInputStreamParent> aCloneEndpoint); + + async StreamNeeded(uint64_t aStart, uint64_t aLength) returns (IPCStream? stream); + + async LengthNeeded() returns (int64_t length); + + async Goodbye(); +}; + +} // namespace mozilla diff --git a/dom/file/ipc/PTemporaryIPCBlob.ipdl b/dom/file/ipc/PTemporaryIPCBlob.ipdl new file mode 100644 index 0000000000..2645d6fd50 --- /dev/null +++ b/dom/file/ipc/PTemporaryIPCBlob.ipdl @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; + +include IPCBlob; + +namespace mozilla { +namespace dom { + +union IPCBlobOrError +{ + IPCBlob; + nsresult; +}; + +[ManualDealloc] +protocol PTemporaryIPCBlob +{ + manager PBackground; + + // When this actor is created on the child side, the parent will send + // immediatelly back a FileDescriptor or a __delete__ in case of error. + // When the FileDescriptor is received, the child has to call + // OperationDone(). When OperationDone() is received on the parent side, the + // parent actor will send a __delete__. + +child: + async FileDesc(FileDescriptor aFD); + async __delete__(IPCBlobOrError aBlobOrError); + +parent: + async OperationFailed(); + + async OperationDone(nsCString aContentType, FileDescriptor aFD); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/file/ipc/RemoteLazyInputStream.cpp b/dom/file/ipc/RemoteLazyInputStream.cpp new file mode 100644 index 0000000000..936438df13 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStream.cpp @@ -0,0 +1,1458 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteLazyInputStream.h" +#include "RemoteLazyInputStreamChild.h" +#include "RemoteLazyInputStreamParent.h" +#include "chrome/common/ipc_message_utils.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/Logging.h" +#include "mozilla/PRemoteLazyInputStream.h" +#include "mozilla/ipc/Endpoint.h" +#include "mozilla/ipc/InputStreamParams.h" +#include "mozilla/ipc/MessageChannel.h" +#include "mozilla/ipc/ProtocolMessageUtils.h" +#include "mozilla/net/SocketProcessParent.h" +#include "mozilla/SlicedInputStream.h" +#include "mozilla/NonBlockingAsyncInputStream.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsID.h" +#include "nsIInputStream.h" +#include "nsIPipe.h" +#include "nsNetUtil.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "RemoteLazyInputStreamStorage.h" +#include "RemoteLazyInputStreamThread.h" + +namespace mozilla { + +mozilla::LazyLogModule gRemoteLazyStreamLog("RemoteLazyStream"); + +namespace { + +class InputStreamCallbackRunnable final : public DiscardableRunnable { + public: + // Note that the execution can be synchronous in case the event target is + // null. + static void Execute(already_AddRefed<nsIInputStreamCallback> aCallback, + already_AddRefed<nsIEventTarget> aEventTarget, + RemoteLazyInputStream* aStream) { + RefPtr<InputStreamCallbackRunnable> runnable = + new InputStreamCallbackRunnable(std::move(aCallback), aStream); + + nsCOMPtr<nsIEventTarget> target = std::move(aEventTarget); + if (target) { + target->Dispatch(runnable, NS_DISPATCH_NORMAL); + } else { + runnable->Run(); + } + } + + NS_IMETHOD + Run() override { + mCallback->OnInputStreamReady(mStream); + mCallback = nullptr; + mStream = nullptr; + return NS_OK; + } + + private: + InputStreamCallbackRunnable( + already_AddRefed<nsIInputStreamCallback> aCallback, + RemoteLazyInputStream* aStream) + : DiscardableRunnable("dom::InputStreamCallbackRunnable"), + mCallback(std::move(aCallback)), + mStream(aStream) { + MOZ_ASSERT(mCallback); + MOZ_ASSERT(mStream); + } + + RefPtr<nsIInputStreamCallback> mCallback; + RefPtr<RemoteLazyInputStream> mStream; +}; + +class FileMetadataCallbackRunnable final : public DiscardableRunnable { + public: + static void Execute(nsIFileMetadataCallback* aCallback, + nsIEventTarget* aEventTarget, + RemoteLazyInputStream* aStream) { + MOZ_ASSERT(aCallback); + MOZ_ASSERT(aEventTarget); + + RefPtr<FileMetadataCallbackRunnable> runnable = + new FileMetadataCallbackRunnable(aCallback, aStream); + + nsCOMPtr<nsIEventTarget> target = aEventTarget; + target->Dispatch(runnable, NS_DISPATCH_NORMAL); + } + + NS_IMETHOD + Run() override { + mCallback->OnFileMetadataReady(mStream); + mCallback = nullptr; + mStream = nullptr; + return NS_OK; + } + + private: + FileMetadataCallbackRunnable(nsIFileMetadataCallback* aCallback, + RemoteLazyInputStream* aStream) + : DiscardableRunnable("dom::FileMetadataCallbackRunnable"), + mCallback(aCallback), + mStream(aStream) { + MOZ_ASSERT(mCallback); + MOZ_ASSERT(mStream); + } + + nsCOMPtr<nsIFileMetadataCallback> mCallback; + RefPtr<RemoteLazyInputStream> mStream; +}; + +} // namespace + +NS_IMPL_ADDREF(RemoteLazyInputStream); +NS_IMPL_RELEASE(RemoteLazyInputStream); + +NS_INTERFACE_MAP_BEGIN(RemoteLazyInputStream) + NS_INTERFACE_MAP_ENTRY(nsIInputStream) + NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStream) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsICloneableInputStream) + NS_INTERFACE_MAP_ENTRY(nsICloneableInputStreamWithRange) + NS_INTERFACE_MAP_ENTRY(nsIIPCSerializableInputStream) + NS_INTERFACE_MAP_ENTRY(nsIFileMetadata) + NS_INTERFACE_MAP_ENTRY(nsIAsyncFileMetadata) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamLength) + NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStreamLength) + NS_INTERFACE_MAP_ENTRY(mozIRemoteLazyInputStream) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream) +NS_INTERFACE_MAP_END + +RemoteLazyInputStream::RemoteLazyInputStream(RemoteLazyInputStreamChild* aActor, + uint64_t aStart, uint64_t aLength) + : mStart(aStart), mLength(aLength), mState(eInit), mActor(aActor) { + MOZ_ASSERT(aActor); + + mActor->StreamCreated(); + + auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr); + if (storage) { + nsCOMPtr<nsIInputStream> stream; + storage->GetStream(mActor->StreamID(), mStart, mLength, + getter_AddRefs(stream)); + if (stream) { + mState = eRunning; + mInnerStream = stream; + } + } +} + +RemoteLazyInputStream::RemoteLazyInputStream(nsIInputStream* aStream) + : mStart(0), mLength(UINT64_MAX), mState(eRunning), mInnerStream(aStream) {} + +static already_AddRefed<RemoteLazyInputStreamChild> BindChildActor( + nsID aId, mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> aEndpoint) { + auto* thread = RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return nullptr; + } + auto actor = MakeRefPtr<RemoteLazyInputStreamChild>(aId); + thread->Dispatch( + NS_NewRunnableFunction("RemoteLazyInputStream::BindChildActor", + [actor, childEp = std::move(aEndpoint)]() mutable { + bool ok = childEp.Bind(actor); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Binding child actor for %s (%p): %s", + nsIDToCString(actor->StreamID()).get(), + actor.get(), ok ? "OK" : "ERROR")); + })); + + return actor.forget(); +} + +already_AddRefed<RemoteLazyInputStream> RemoteLazyInputStream::WrapStream( + nsIInputStream* aInputStream) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (nsCOMPtr<mozIRemoteLazyInputStream> lazyStream = + do_QueryInterface(aInputStream)) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Returning already-wrapped stream")); + return lazyStream.forget().downcast<RemoteLazyInputStream>(); + } + + // If we have a stream and are in the parent process, create a new actor pair + // and transfer ownership of the stream into storage. + auto streamStorage = RemoteLazyInputStreamStorage::Get(); + if (NS_WARN_IF(streamStorage.isErr())) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("Cannot wrap with no storage!")); + return nullptr; + } + + nsID id = nsID::GenerateUUID(); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Wrapping stream %p as %s", aInputStream, nsIDToCString(id).get())); + streamStorage.inspect()->AddStream(aInputStream, id); + + mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent> parentEp; + mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> childEp; + MOZ_ALWAYS_SUCCEEDS( + PRemoteLazyInputStream::CreateEndpoints(&parentEp, &childEp)); + + // Bind the actor on our background thread. + streamStorage.inspect()->TaskQueue()->Dispatch(NS_NewRunnableFunction( + "RemoteLazyInputStreamParent::Bind", + [parentEp = std::move(parentEp), id]() mutable { + auto actor = MakeRefPtr<RemoteLazyInputStreamParent>(id); + bool ok = parentEp.Bind(actor); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Binding parent actor for %s (%p): %s", + nsIDToCString(id).get(), actor.get(), ok ? "OK" : "ERROR")); + })); + + RefPtr<RemoteLazyInputStreamChild> actor = + BindChildActor(id, std::move(childEp)); + + if (!actor) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("Wrapping stream failed as we are probably late in shutdown!")); + return do_AddRef(new RemoteLazyInputStream()); + } + + return do_AddRef(new RemoteLazyInputStream(actor)); +} + +NS_IMETHODIMP RemoteLazyInputStream::TakeInternalStream( + nsIInputStream** aStream) { + RefPtr<RemoteLazyInputStreamChild> actor; + { + MutexAutoLock lock(mMutex); + if (mState == eInit || mState == ePending) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + if (mState == eClosed) { + return NS_BASE_STREAM_CLOSED; + } + if (mInputStreamCallback) { + MOZ_ASSERT_UNREACHABLE( + "Do not call TakeInternalStream after calling AsyncWait"); + return NS_ERROR_UNEXPECTED; + } + + // Take the inner stream and return it, then close ourselves. + if (mInnerStream) { + mInnerStream.forget(aStream); + } else if (mAsyncInnerStream) { + mAsyncInnerStream.forget(aStream); + } + mState = eClosed; + actor = mActor.forget(); + } + if (actor) { + actor->StreamConsumed(); + } + return NS_OK; +} + +NS_IMETHODIMP RemoteLazyInputStream::GetInternalStreamID(nsID& aID) { + MutexAutoLock lock(mMutex); + if (!mActor) { + return NS_ERROR_NOT_AVAILABLE; + } + + aID = mActor->StreamID(); + return NS_OK; +} + +RemoteLazyInputStream::~RemoteLazyInputStream() { Close(); } + +nsCString RemoteLazyInputStream::Describe() { + const char* state = "?"; + switch (mState) { + case eInit: + state = "i"; + break; + case ePending: + state = "p"; + break; + case eRunning: + state = "r"; + break; + case eClosed: + state = "c"; + break; + } + return nsPrintfCString( + "[%p, %s, %s, %p%s, %s%s|%s%s]", this, state, + mActor ? nsIDToCString(mActor->StreamID()).get() : "<no actor>", + mInnerStream ? mInnerStream.get() : mAsyncInnerStream.get(), + mAsyncInnerStream ? "(A)" : "", mInputStreamCallback ? "I" : "", + mInputStreamCallbackEventTarget ? "+" : "", + mFileMetadataCallback ? "F" : "", + mFileMetadataCallbackEventTarget ? "+" : ""); +} + +// nsIInputStream interface + +NS_IMETHODIMP +RemoteLazyInputStream::Available(uint64_t* aLength) { + nsCOMPtr<nsIAsyncInputStream> stream; + { + MutexAutoLock lock(mMutex); + + // We don't have a remoteStream yet: let's return 0. + if (mState == eInit || mState == ePending) { + *aLength = 0; + return NS_OK; + } + + if (mState == eClosed) { + return NS_BASE_STREAM_CLOSED; + } + + MOZ_ASSERT(mState == eRunning); + MOZ_ASSERT(mInnerStream || mAsyncInnerStream); + + nsresult rv = EnsureAsyncRemoteStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stream = mAsyncInnerStream; + } + + MOZ_ASSERT(stream); + return stream->Available(aLength); +} + +NS_IMETHODIMP +RemoteLazyInputStream::StreamStatus() { + nsCOMPtr<nsIAsyncInputStream> stream; + { + MutexAutoLock lock(mMutex); + + // We don't have a remoteStream yet: let's return 0. + if (mState == eInit || mState == ePending) { + return NS_OK; + } + + if (mState == eClosed) { + return NS_BASE_STREAM_CLOSED; + } + + MOZ_ASSERT(mState == eRunning); + MOZ_ASSERT(mInnerStream || mAsyncInnerStream); + + nsresult rv = EnsureAsyncRemoteStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stream = mAsyncInnerStream; + } + + MOZ_ASSERT(stream); + return stream->StreamStatus(); +} + +NS_IMETHODIMP +RemoteLazyInputStream::Read(char* aBuffer, uint32_t aCount, + uint32_t* aReadCount) { + nsCOMPtr<nsIAsyncInputStream> stream; + { + MutexAutoLock lock(mMutex); + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Read(%u) %s", aCount, Describe().get())); + + // Read is not available is we don't have a remoteStream. + if (mState == eInit || mState == ePending) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + if (mState == eClosed) { + return NS_BASE_STREAM_CLOSED; + } + + MOZ_ASSERT(mState == eRunning); + MOZ_ASSERT(mInnerStream || mAsyncInnerStream); + + nsresult rv = EnsureAsyncRemoteStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stream = mAsyncInnerStream; + } + + MOZ_ASSERT(stream); + nsresult rv = stream->Read(aBuffer, aCount, aReadCount); + if (NS_FAILED(rv)) { + return rv; + } + + // If some data has been read, we mark the stream as consumed. + if (*aReadCount > 0) { + MarkConsumed(); + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Read %u/%u bytes", *aReadCount, aCount)); + + return NS_OK; +} + +NS_IMETHODIMP +RemoteLazyInputStream::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* aResult) { + nsCOMPtr<nsIAsyncInputStream> stream; + { + MutexAutoLock lock(mMutex); + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("ReadSegments(%u) %s", aCount, Describe().get())); + + // ReadSegments is not available is we don't have a remoteStream. + if (mState == eInit || mState == ePending) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + if (mState == eClosed) { + return NS_BASE_STREAM_CLOSED; + } + + MOZ_ASSERT(mState == eRunning); + MOZ_ASSERT(mInnerStream || mAsyncInnerStream); + + nsresult rv = EnsureAsyncRemoteStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("EnsureAsyncRemoteStream failed! %s %s", + mozilla::GetStaticErrorName(rv), Describe().get())); + return rv; + } + + stream = mAsyncInnerStream; + } + + MOZ_ASSERT(stream); + nsresult rv = stream->ReadSegments(aWriter, aClosure, aCount, aResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // If some data has been read, we mark the stream as consumed. + if (*aResult != 0) { + MarkConsumed(); + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("ReadSegments %u/%u bytes", *aResult, aCount)); + + return NS_OK; +} + +void RemoteLazyInputStream::MarkConsumed() { + RefPtr<RemoteLazyInputStreamChild> actor; + { + MutexAutoLock lock(mMutex); + if (mActor) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("MarkConsumed %s", Describe().get())); + } + + actor = mActor.forget(); + } + if (actor) { + actor->StreamConsumed(); + } +} + +NS_IMETHODIMP +RemoteLazyInputStream::IsNonBlocking(bool* aNonBlocking) { + *aNonBlocking = true; + return NS_OK; +} + +NS_IMETHODIMP +RemoteLazyInputStream::Close() { + RefPtr<RemoteLazyInputStreamChild> actor; + + nsCOMPtr<nsIAsyncInputStream> asyncInnerStream; + nsCOMPtr<nsIInputStream> innerStream; + + RefPtr<nsIInputStreamCallback> inputStreamCallback; + nsCOMPtr<nsIEventTarget> inputStreamCallbackEventTarget; + + { + MutexAutoLock lock(mMutex); + if (mState == eClosed) { + return NS_OK; + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Close %s", Describe().get())); + + actor = mActor.forget(); + + asyncInnerStream = mAsyncInnerStream.forget(); + innerStream = mInnerStream.forget(); + + // TODO(Bug 1737783): Notify to the mFileMetadataCallback that this + // lazy input stream has been closed. + mFileMetadataCallback = nullptr; + mFileMetadataCallbackEventTarget = nullptr; + + inputStreamCallback = mInputStreamCallback.forget(); + inputStreamCallbackEventTarget = mInputStreamCallbackEventTarget.forget(); + + mState = eClosed; + } + + if (actor) { + actor->StreamConsumed(); + } + + if (inputStreamCallback) { + InputStreamCallbackRunnable::Execute( + inputStreamCallback.forget(), inputStreamCallbackEventTarget.forget(), + this); + } + + if (asyncInnerStream) { + asyncInnerStream->CloseWithStatus(NS_BASE_STREAM_CLOSED); + } + + if (innerStream) { + innerStream->Close(); + } + + return NS_OK; +} + +// nsICloneableInputStream interface + +NS_IMETHODIMP +RemoteLazyInputStream::GetCloneable(bool* aCloneable) { + *aCloneable = true; + return NS_OK; +} + +NS_IMETHODIMP +RemoteLazyInputStream::Clone(nsIInputStream** aResult) { + return CloneWithRange(0, UINT64_MAX, aResult); +} + +// nsICloneableInputStreamWithRange interface + +NS_IMETHODIMP +RemoteLazyInputStream::CloneWithRange(uint64_t aStart, uint64_t aLength, + nsIInputStream** aResult) { + MutexAutoLock lock(mMutex); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("CloneWithRange %" PRIu64 " %" PRIu64 " %s", aStart, aLength, + Describe().get())); + + nsresult rv; + + RefPtr<RemoteLazyInputStream> stream; + if (mState == eClosed) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Cloning closed stream")); + stream = new RemoteLazyInputStream(); + stream.forget(aResult); + return NS_OK; + } + + uint64_t start = 0; + uint64_t length = 0; + auto maxLength = CheckedUint64(mLength) - aStart; + if (maxLength.isValid()) { + start = mStart + aStart; + length = std::min(maxLength.value(), aLength); + } + + // If the slice would be empty, wrap an empty input stream and return it. + if (length == 0) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Creating empty stream")); + + nsCOMPtr<nsIInputStream> emptyStream; + rv = NS_NewCStringInputStream(getter_AddRefs(emptyStream), ""_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stream = new RemoteLazyInputStream(emptyStream); + stream.forget(aResult); + return NS_OK; + } + + // If we still have a connection to our actor, that means we haven't read any + // data yet, and can clone + slice by building a new stream backed by the same + // actor. + if (mActor) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Cloning stream with actor")); + + stream = new RemoteLazyInputStream(mActor, start, length); + stream.forget(aResult); + return NS_OK; + } + + // We no longer have our actor, either because we were constructed without + // one, or we've already begun reading. Perform the clone locally on our inner + // input stream. + + nsCOMPtr<nsIInputStream> innerStream = mInnerStream; + if (mAsyncInnerStream) { + innerStream = mAsyncInnerStream; + } + + nsCOMPtr<nsICloneableInputStream> cloneable = do_QueryInterface(innerStream); + if (!cloneable || !cloneable->GetCloneable()) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Cloning non-cloneable stream - copying to pipe")); + + // If our internal stream isn't cloneable, to perform a clone we'll need to + // copy into a pipe and replace our internal stream. + nsCOMPtr<nsIAsyncInputStream> pipeIn; + nsCOMPtr<nsIAsyncOutputStream> pipeOut; + NS_NewPipe2(getter_AddRefs(pipeIn), getter_AddRefs(pipeOut), true, true); + + RefPtr<RemoteLazyInputStreamThread> thread = + RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + mAsyncInnerStream = pipeIn; + mInnerStream = nullptr; + + // If we have a callback pending, we need to re-call AsyncWait on the inner + // stream. This should not re-enter us immediately, as `pipeIn` hasn't been + // sent any data yet, but we may be called again as soon as `NS_AsyncCopy` + // has begun copying. + if (mInputStreamCallback) { + mAsyncInnerStream->AsyncWait(this, mInputStreamCallbackFlags, + mInputStreamCallbackRequestedCount, + mInputStreamCallbackEventTarget); + } + + rv = NS_AsyncCopy(innerStream, pipeOut, thread, + NS_ASYNCCOPY_VIA_WRITESEGMENTS); + if (NS_WARN_IF(NS_FAILED(rv))) { + // The copy failed, revert the changes we did and restore our previous + // inner stream. + mAsyncInnerStream = nullptr; + mInnerStream = innerStream; + return rv; + } + + cloneable = do_QueryInterface(mAsyncInnerStream); + } + + MOZ_ASSERT(cloneable && cloneable->GetCloneable()); + + // Check if we can clone more efficiently with a range. + if (length < UINT64_MAX) { + if (nsCOMPtr<nsICloneableInputStreamWithRange> cloneableWithRange = + do_QueryInterface(cloneable)) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Cloning with range")); + nsCOMPtr<nsIInputStream> cloned; + rv = cloneableWithRange->CloneWithRange(start, length, + getter_AddRefs(cloned)); + if (NS_FAILED(rv)) { + return rv; + } + + stream = new RemoteLazyInputStream(cloned); + stream.forget(aResult); + return NS_OK; + } + } + + // Directly clone our inner stream, and then slice it if needed. + nsCOMPtr<nsIInputStream> cloned; + rv = cloneable->Clone(getter_AddRefs(cloned)); + if (NS_FAILED(rv)) { + return rv; + } + + if (length < UINT64_MAX) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Slicing stream with %" PRIu64 " %" PRIu64, start, length)); + cloned = new SlicedInputStream(cloned.forget(), start, length); + } + + stream = new RemoteLazyInputStream(cloned); + stream.forget(aResult); + return NS_OK; +} + +// nsIAsyncInputStream interface + +NS_IMETHODIMP +RemoteLazyInputStream::CloseWithStatus(nsresult aStatus) { return Close(); } + +NS_IMETHODIMP +RemoteLazyInputStream::AsyncWait(nsIInputStreamCallback* aCallback, + uint32_t aFlags, uint32_t aRequestedCount, + nsIEventTarget* aEventTarget) { + // Ensure we always have an event target for AsyncWait callbacks, so that + // calls to `AsyncWait` cannot reenter us with `OnInputStreamReady`. + nsCOMPtr<nsIEventTarget> eventTarget = aEventTarget; + if (aCallback && !eventTarget) { + eventTarget = RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!eventTarget)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + } + + { + MutexAutoLock lock(mMutex); + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("AsyncWait(%p, %u, %u, %p) %s", aCallback, aFlags, aRequestedCount, + aEventTarget, Describe().get())); + + // See RemoteLazyInputStream.h for more information about this state + // machine. + + nsCOMPtr<nsIAsyncInputStream> stream; + switch (mState) { + // First call, we need to retrieve the stream from the parent actor. + case eInit: + MOZ_ASSERT(mActor); + + mInputStreamCallback = aCallback; + mInputStreamCallbackEventTarget = eventTarget; + mInputStreamCallbackFlags = aFlags; + mInputStreamCallbackRequestedCount = aRequestedCount; + mState = ePending; + + StreamNeeded(); + return NS_OK; + + // We are still waiting for the remote inputStream + case ePending: { + if (NS_WARN_IF(mInputStreamCallback && aCallback && + mInputStreamCallback != aCallback)) { + return NS_ERROR_FAILURE; + } + + mInputStreamCallback = aCallback; + mInputStreamCallbackEventTarget = eventTarget; + mInputStreamCallbackFlags = aFlags; + mInputStreamCallbackRequestedCount = aRequestedCount; + return NS_OK; + } + + // We have the remote inputStream, let's check if we can execute the + // callback. + case eRunning: { + if (NS_WARN_IF(mInputStreamCallback && aCallback && + mInputStreamCallback != aCallback)) { + return NS_ERROR_FAILURE; + } + + nsresult rv = EnsureAsyncRemoteStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInputStreamCallback = aCallback; + mInputStreamCallbackEventTarget = eventTarget; + mInputStreamCallbackFlags = aFlags; + mInputStreamCallbackRequestedCount = aRequestedCount; + + stream = mAsyncInnerStream; + break; + } + + case eClosed: + [[fallthrough]]; + default: + MOZ_ASSERT(mState == eClosed); + if (NS_WARN_IF(mInputStreamCallback && aCallback && + mInputStreamCallback != aCallback)) { + return NS_ERROR_FAILURE; + } + break; + } + + if (stream) { + return stream->AsyncWait(aCallback ? this : nullptr, aFlags, + aRequestedCount, eventTarget); + } + } + + if (aCallback) { + // if stream is nullptr here, that probably means the stream has + // been closed and the callback can be executed immediately + InputStreamCallbackRunnable::Execute(do_AddRef(aCallback), + do_AddRef(eventTarget), this); + } + return NS_OK; +} + +void RemoteLazyInputStream::StreamNeeded() { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("StreamNeeded %s", Describe().get())); + + auto* thread = RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return; + } + thread->Dispatch(NS_NewRunnableFunction( + "RemoteLazyInputStream::StreamNeeded", + [self = RefPtr{this}, actor = mActor, start = mStart, length = mLength] { + MOZ_LOG( + gRemoteLazyStreamLog, LogLevel::Debug, + ("Sending StreamNeeded(%" PRIu64 " %" PRIu64 ") %s %d", start, + length, nsIDToCString(actor->StreamID()).get(), actor->CanSend())); + + actor->SendStreamNeeded( + start, length, + [self](const Maybe<mozilla::ipc::IPCStream>& aStream) { + // Try to deserialize the stream from our remote, and close our + // stream if it fails. + nsCOMPtr<nsIInputStream> stream = + mozilla::ipc::DeserializeIPCStream(aStream); + if (NS_WARN_IF(!stream)) { + NS_WARNING("Failed to deserialize IPC stream"); + self->Close(); + } + + // Lock our mutex to update the inner stream, and collect any + // callbacks which we need to invoke. + MutexAutoLock lock(self->mMutex); + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("ResolveStreamNeeded(%p) %s", stream.get(), + self->Describe().get())); + + if (self->mState == ePending) { + self->mInnerStream = stream.forget(); + self->mState = eRunning; + + // Notify any listeners that we've now acquired the underlying + // stream, so file metadata information will be available. + nsCOMPtr<nsIFileMetadataCallback> fileMetadataCallback = + self->mFileMetadataCallback.forget(); + nsCOMPtr<nsIEventTarget> fileMetadataCallbackEventTarget = + self->mFileMetadataCallbackEventTarget.forget(); + if (fileMetadataCallback) { + FileMetadataCallbackRunnable::Execute( + fileMetadataCallback, fileMetadataCallbackEventTarget, + self); + } + + // **NOTE** we can re-enter this class here **NOTE** + // If we already have an input stream callback, attempt to + // register ourselves with AsyncWait on the underlying stream. + if (self->mInputStreamCallback) { + if (NS_FAILED(self->EnsureAsyncRemoteStream()) || + NS_FAILED(self->mAsyncInnerStream->AsyncWait( + self, self->mInputStreamCallbackFlags, + self->mInputStreamCallbackRequestedCount, + self->mInputStreamCallbackEventTarget))) { + InputStreamCallbackRunnable::Execute( + self->mInputStreamCallback.forget(), + self->mInputStreamCallbackEventTarget.forget(), self); + } + } + } + + if (stream) { + NS_WARNING("Failed to save stream, closing it"); + stream->Close(); + } + }, + [self](mozilla::ipc::ResponseRejectReason) { + NS_WARNING("SendStreamNeeded rejected"); + self->Close(); + }); + })); +} + +// nsIInputStreamCallback + +NS_IMETHODIMP +RemoteLazyInputStream::OnInputStreamReady(nsIAsyncInputStream* aStream) { + RefPtr<nsIInputStreamCallback> callback; + nsCOMPtr<nsIEventTarget> callbackEventTarget; + { + MutexAutoLock lock(mMutex); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("OnInputStreamReady %s", Describe().get())); + + // We have been closed in the meantime. + if (mState == eClosed) { + return NS_OK; + } + + // We got a callback from the wrong stream, likely due to a `CloneWithRange` + // call while we were waiting. Ignore this callback. + if (mAsyncInnerStream != aStream) { + return NS_OK; + } + + MOZ_ASSERT(mState == eRunning); + + // The callback has been canceled in the meantime. + if (!mInputStreamCallback) { + return NS_OK; + } + + callback.swap(mInputStreamCallback); + callbackEventTarget.swap(mInputStreamCallbackEventTarget); + } + + // This must be the last operation because the execution of the callback can + // be synchronous. + MOZ_ASSERT(callback); + InputStreamCallbackRunnable::Execute(callback.forget(), + callbackEventTarget.forget(), this); + return NS_OK; +} + +// nsIIPCSerializableInputStream + +void RemoteLazyInputStream::SerializedComplexity(uint32_t aMaxSize, + uint32_t* aSizeUsed, + uint32_t* aNewPipes, + uint32_t* aTransferables) { + *aTransferables = 1; +} + +void RemoteLazyInputStream::Serialize(mozilla::ipc::InputStreamParams& aParams, + uint32_t aMaxSize, uint32_t* aSizeUsed) { + *aSizeUsed = 0; + aParams = mozilla::ipc::RemoteLazyInputStreamParams(this); +} + +bool RemoteLazyInputStream::Deserialize( + const mozilla::ipc::InputStreamParams& aParams) { + MOZ_CRASH("This should never be called."); + return false; +} + +// nsIAsyncFileMetadata + +NS_IMETHODIMP +RemoteLazyInputStream::AsyncFileMetadataWait(nsIFileMetadataCallback* aCallback, + nsIEventTarget* aEventTarget) { + MOZ_ASSERT(!!aCallback == !!aEventTarget); + + // If we have the callback, we must have the event target. + if (NS_WARN_IF(!!aCallback != !!aEventTarget)) { + return NS_ERROR_FAILURE; + } + + // See RemoteLazyInputStream.h for more information about this state + // machine. + + { + MutexAutoLock lock(mMutex); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("AsyncFileMetadataWait(%p, %p) %s", aCallback, aEventTarget, + Describe().get())); + + switch (mState) { + // First call, we need to retrieve the stream from the parent actor. + case eInit: + MOZ_ASSERT(mActor); + + mFileMetadataCallback = aCallback; + mFileMetadataCallbackEventTarget = aEventTarget; + mState = ePending; + + StreamNeeded(); + return NS_OK; + + // We are still waiting for the remote inputStream + case ePending: + if (mFileMetadataCallback && aCallback) { + return NS_ERROR_FAILURE; + } + + mFileMetadataCallback = aCallback; + mFileMetadataCallbackEventTarget = aEventTarget; + return NS_OK; + + // We have the remote inputStream, let's check if we can execute the + // callback. + case eRunning: + break; + + // Stream is closed. + default: + MOZ_ASSERT(mState == eClosed); + return NS_BASE_STREAM_CLOSED; + } + + MOZ_ASSERT(mState == eRunning); + } + + FileMetadataCallbackRunnable::Execute(aCallback, aEventTarget, this); + return NS_OK; +} + +// nsIFileMetadata + +NS_IMETHODIMP +RemoteLazyInputStream::GetSize(int64_t* aRetval) { + nsCOMPtr<nsIFileMetadata> fileMetadata; + { + MutexAutoLock lock(mMutex); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("GetSize %s", Describe().get())); + + fileMetadata = do_QueryInterface(mInnerStream); + if (!fileMetadata) { + return mState == eClosed ? NS_BASE_STREAM_CLOSED : NS_ERROR_FAILURE; + } + } + + return fileMetadata->GetSize(aRetval); +} + +NS_IMETHODIMP +RemoteLazyInputStream::GetLastModified(int64_t* aRetval) { + nsCOMPtr<nsIFileMetadata> fileMetadata; + { + MutexAutoLock lock(mMutex); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("GetLastModified %s", Describe().get())); + + fileMetadata = do_QueryInterface(mInnerStream); + if (!fileMetadata) { + return mState == eClosed ? NS_BASE_STREAM_CLOSED : NS_ERROR_FAILURE; + } + } + + return fileMetadata->GetLastModified(aRetval); +} + +NS_IMETHODIMP +RemoteLazyInputStream::GetFileDescriptor(PRFileDesc** aRetval) { + nsCOMPtr<nsIFileMetadata> fileMetadata; + { + MutexAutoLock lock(mMutex); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("GetFileDescriptor %s", Describe().get())); + + fileMetadata = do_QueryInterface(mInnerStream); + if (!fileMetadata) { + return mState == eClosed ? NS_BASE_STREAM_CLOSED : NS_ERROR_FAILURE; + } + } + + return fileMetadata->GetFileDescriptor(aRetval); +} + +nsresult RemoteLazyInputStream::EnsureAsyncRemoteStream() { + // We already have an async remote stream. + if (mAsyncInnerStream) { + return NS_OK; + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("EnsureAsyncRemoteStream %s", Describe().get())); + + if (NS_WARN_IF(!mInnerStream)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIInputStream> stream = mInnerStream; + + // Check if the stream is blocking, if it is, we want to make it non-blocking + // using a pipe. + bool nonBlocking = false; + nsresult rv = stream->IsNonBlocking(&nonBlocking); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // We don't return NS_ERROR_NOT_IMPLEMENTED from ReadSegments, + // so it's possible that callers are expecting us to succeed in the future. + // We need to make sure the stream we return here supports ReadSegments, + // so wrap if in a buffered stream if necessary. + // + // We only need to do this if we won't be wrapping the stream in a pipe, which + // will add buffering anyway. + if (nonBlocking && !NS_InputStreamIsBuffered(stream)) { + nsCOMPtr<nsIInputStream> bufferedStream; + nsresult rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream), + stream.forget(), 4096); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stream = bufferedStream; + } + + nsCOMPtr<nsIAsyncInputStream> asyncStream = do_QueryInterface(stream); + + // If non-blocking and non-async, let's use NonBlockingAsyncInputStream. + if (nonBlocking && !asyncStream) { + rv = NonBlockingAsyncInputStream::Create(stream.forget(), + getter_AddRefs(asyncStream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(asyncStream); + } + + if (!asyncStream) { + // Let's make the stream async using the DOMFile thread. + nsCOMPtr<nsIAsyncInputStream> pipeIn; + nsCOMPtr<nsIAsyncOutputStream> pipeOut; + NS_NewPipe2(getter_AddRefs(pipeIn), getter_AddRefs(pipeOut), true, true); + + RefPtr<RemoteLazyInputStreamThread> thread = + RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + rv = NS_AsyncCopy(stream, pipeOut, thread, NS_ASYNCCOPY_VIA_WRITESEGMENTS); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + asyncStream = pipeIn; + } + + MOZ_ASSERT(asyncStream); + mAsyncInnerStream = asyncStream; + mInnerStream = nullptr; + + return NS_OK; +} + +// nsIInputStreamLength + +NS_IMETHODIMP +RemoteLazyInputStream::Length(int64_t* aLength) { + MutexAutoLock lock(mMutex); + + if (mState == eClosed) { + return NS_BASE_STREAM_CLOSED; + } + + if (!mActor) { + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_BASE_STREAM_WOULD_BLOCK; +} + +namespace { + +class InputStreamLengthCallbackRunnable final : public DiscardableRunnable { + public: + static void Execute(nsIInputStreamLengthCallback* aCallback, + nsIEventTarget* aEventTarget, + RemoteLazyInputStream* aStream, int64_t aLength) { + MOZ_ASSERT(aCallback); + MOZ_ASSERT(aEventTarget); + + RefPtr<InputStreamLengthCallbackRunnable> runnable = + new InputStreamLengthCallbackRunnable(aCallback, aStream, aLength); + + nsCOMPtr<nsIEventTarget> target = aEventTarget; + target->Dispatch(runnable, NS_DISPATCH_NORMAL); + } + + NS_IMETHOD + Run() override { + mCallback->OnInputStreamLengthReady(mStream, mLength); + mCallback = nullptr; + mStream = nullptr; + return NS_OK; + } + + private: + InputStreamLengthCallbackRunnable(nsIInputStreamLengthCallback* aCallback, + RemoteLazyInputStream* aStream, + int64_t aLength) + : DiscardableRunnable("dom::InputStreamLengthCallbackRunnable"), + mCallback(aCallback), + mStream(aStream), + mLength(aLength) { + MOZ_ASSERT(mCallback); + MOZ_ASSERT(mStream); + } + + nsCOMPtr<nsIInputStreamLengthCallback> mCallback; + RefPtr<RemoteLazyInputStream> mStream; + const int64_t mLength; +}; + +} // namespace + +// nsIAsyncInputStreamLength + +NS_IMETHODIMP +RemoteLazyInputStream::AsyncLengthWait(nsIInputStreamLengthCallback* aCallback, + nsIEventTarget* aEventTarget) { + // If we have the callback, we must have the event target. + if (NS_WARN_IF(!!aCallback != !!aEventTarget)) { + return NS_ERROR_FAILURE; + } + + { + MutexAutoLock lock(mMutex); + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("AsyncLengthWait(%p, %p) %s", aCallback, aEventTarget, + Describe().get())); + + if (mActor) { + if (aCallback) { + auto* thread = RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + thread->Dispatch(NS_NewRunnableFunction( + "RemoteLazyInputStream::AsyncLengthWait", + [self = RefPtr{this}, actor = mActor, + callback = nsCOMPtr{aCallback}, + eventTarget = nsCOMPtr{aEventTarget}] { + actor->SendLengthNeeded( + [self, callback, eventTarget](int64_t aLength) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("AsyncLengthWait resolve %" PRId64, aLength)); + int64_t length = -1; + if (aLength > 0) { + uint64_t sourceLength = + aLength - std::min<uint64_t>(aLength, self->mStart); + length = int64_t( + std::min<uint64_t>(sourceLength, self->mLength)); + } + InputStreamLengthCallbackRunnable::Execute( + callback, eventTarget, self, length); + }, + [self, callback, + eventTarget](mozilla::ipc::ResponseRejectReason) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("AsyncLengthWait reject")); + InputStreamLengthCallbackRunnable::Execute( + callback, eventTarget, self, -1); + }); + })); + } + + return NS_OK; + } + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("AsyncLengthWait immediate")); + + // If execution has reached here, it means the stream is either closed or + // consumed, and therefore the callback can be executed immediately + InputStreamLengthCallbackRunnable::Execute(aCallback, aEventTarget, this, -1); + return NS_OK; +} + +void RemoteLazyInputStream::IPCWrite(IPC::MessageWriter* aWriter) { + // If we have an actor still, serialize efficiently by cloning our actor to + // maintain a reference to the parent side. + RefPtr<RemoteLazyInputStreamChild> actor; + + nsCOMPtr<nsIInputStream> innerStream; + + RefPtr<nsIInputStreamCallback> inputStreamCallback; + nsCOMPtr<nsIEventTarget> inputStreamCallbackEventTarget; + + { + MutexAutoLock lock(mMutex); + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Serialize %s", Describe().get())); + + actor = mActor.forget(); + + if (mAsyncInnerStream) { + MOZ_ASSERT(!mInnerStream); + innerStream = mAsyncInnerStream.forget(); + } else { + innerStream = mInnerStream.forget(); + } + + // TODO(Bug 1737783): Notify to the mFileMetadataCallback that this + // lazy input stream has been closed. + mFileMetadataCallback = nullptr; + mFileMetadataCallbackEventTarget = nullptr; + + inputStreamCallback = mInputStreamCallback.forget(); + inputStreamCallbackEventTarget = mInputStreamCallbackEventTarget.forget(); + + mState = eClosed; + } + + if (inputStreamCallback) { + InputStreamCallbackRunnable::Execute( + inputStreamCallback.forget(), inputStreamCallbackEventTarget.forget(), + this); + } + + bool closed = !actor && !innerStream; + IPC::WriteParam(aWriter, closed); + if (closed) { + return; + } + + // If we still have a connection to our remote actor, create a clone endpoint + // for it and tell it that the stream has been consumed. The clone of the + // connection can be transferred to another process. + if (actor) { + MOZ_LOG( + gRemoteLazyStreamLog, LogLevel::Debug, + ("Serializing as actor: %s", nsIDToCString(actor->StreamID()).get())); + // Create a clone of the actor, and then tell it that this stream is no + // longer referencing it. + mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent> parentEp; + mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> childEp; + MOZ_ALWAYS_SUCCEEDS( + PRemoteLazyInputStream::CreateEndpoints(&parentEp, &childEp)); + + auto* thread = RemoteLazyInputStreamThread::GetOrCreate(); + if (thread) { + thread->Dispatch(NS_NewRunnableFunction( + "RemoteLazyInputStreamChild::SendClone", + [actor, parentEp = std::move(parentEp)]() mutable { + bool ok = actor->SendClone(std::move(parentEp)); + MOZ_LOG( + gRemoteLazyStreamLog, LogLevel::Verbose, + ("SendClone for %s: %s", nsIDToCString(actor->StreamID()).get(), + ok ? "OK" : "ERR")); + })); + + } // else we are shutting down xpcom threads. + + // NOTE: Call `StreamConsumed` after dispatching the `SendClone` runnable, + // as this method may dispatch a runnable to `RemoteLazyInputStreamThread` + // to call `SendGoodbye`, which needs to happen after `SendClone`. + actor->StreamConsumed(); + + IPC::WriteParam(aWriter, actor->StreamID()); + IPC::WriteParam(aWriter, mStart); + IPC::WriteParam(aWriter, mLength); + IPC::WriteParam(aWriter, std::move(childEp)); + + if (innerStream) { + innerStream->Close(); + } + return; + } + + // If we have a stream and are in the parent process, create a new actor pair + // and transfer ownership of the stream into storage. + auto streamStorage = RemoteLazyInputStreamStorage::Get(); + if (streamStorage.isOk()) { + MOZ_ASSERT(XRE_IsParentProcess()); + nsID id = nsID::GenerateUUID(); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Serializing as new stream: %s", nsIDToCString(id).get())); + + streamStorage.inspect()->AddStream(innerStream, id); + + mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent> parentEp; + mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> childEp; + MOZ_ALWAYS_SUCCEEDS( + PRemoteLazyInputStream::CreateEndpoints(&parentEp, &childEp)); + + // Bind the actor on our background thread. + streamStorage.inspect()->TaskQueue()->Dispatch(NS_NewRunnableFunction( + "RemoteLazyInputStreamParent::Bind", + [parentEp = std::move(parentEp), id]() mutable { + auto stream = MakeRefPtr<RemoteLazyInputStreamParent>(id); + parentEp.Bind(stream); + })); + + IPC::WriteParam(aWriter, id); + IPC::WriteParam(aWriter, 0); + IPC::WriteParam(aWriter, UINT64_MAX); + IPC::WriteParam(aWriter, std::move(childEp)); + return; + } + + MOZ_CRASH("Cannot serialize new RemoteLazyInputStream from this process"); +} + +already_AddRefed<RemoteLazyInputStream> RemoteLazyInputStream::IPCRead( + IPC::MessageReader* aReader) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Deserialize")); + + bool closed; + if (NS_WARN_IF(!IPC::ReadParam(aReader, &closed))) { + return nullptr; + } + if (closed) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Deserialize closed stream")); + return do_AddRef(new RemoteLazyInputStream()); + } + + nsID id{}; + uint64_t start; + uint64_t length; + mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> endpoint; + if (NS_WARN_IF(!IPC::ReadParam(aReader, &id)) || + NS_WARN_IF(!IPC::ReadParam(aReader, &start)) || + NS_WARN_IF(!IPC::ReadParam(aReader, &length)) || + NS_WARN_IF(!IPC::ReadParam(aReader, &endpoint))) { + return nullptr; + } + + if (!endpoint.IsValid()) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("Deserialize failed due to invalid endpoint!")); + return do_AddRef(new RemoteLazyInputStream()); + } + + RefPtr<RemoteLazyInputStreamChild> actor = + BindChildActor(id, std::move(endpoint)); + + if (!actor) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("Deserialize failed as we are probably late in shutdown!")); + return do_AddRef(new RemoteLazyInputStream()); + } + + return do_AddRef(new RemoteLazyInputStream(actor, start, length)); +} + +} // namespace mozilla + +void IPC::ParamTraits<mozilla::RemoteLazyInputStream*>::Write( + IPC::MessageWriter* aWriter, mozilla::RemoteLazyInputStream* aParam) { + bool nonNull = !!aParam; + IPC::WriteParam(aWriter, nonNull); + if (aParam) { + aParam->IPCWrite(aWriter); + } +} + +bool IPC::ParamTraits<mozilla::RemoteLazyInputStream*>::Read( + IPC::MessageReader* aReader, + RefPtr<mozilla::RemoteLazyInputStream>* aResult) { + bool nonNull = false; + if (!IPC::ReadParam(aReader, &nonNull)) { + return false; + } + if (!nonNull) { + *aResult = nullptr; + return true; + } + *aResult = mozilla::RemoteLazyInputStream::IPCRead(aReader); + return *aResult; +} diff --git a/dom/file/ipc/RemoteLazyInputStream.h b/dom/file/ipc/RemoteLazyInputStream.h new file mode 100644 index 0000000000..08bb168e27 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStream.h @@ -0,0 +1,155 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_RemoteLazyInputStream_h +#define mozilla_RemoteLazyInputStream_h + +#include "chrome/common/ipc_message_utils.h" +#include "mozilla/Mutex.h" +#include "mozIRemoteLazyInputStream.h" +#include "nsIAsyncInputStream.h" +#include "nsICloneableInputStream.h" +#include "nsIFileStreams.h" +#include "nsIIPCSerializableInputStream.h" +#include "nsIInputStreamLength.h" +#include "nsCOMPtr.h" + +namespace mozilla { + +class RemoteLazyInputStreamChild; + +class RemoteLazyInputStream final : public nsIAsyncInputStream, + public nsIInputStreamCallback, + public nsICloneableInputStreamWithRange, + public nsIIPCSerializableInputStream, + public nsIAsyncFileMetadata, + public nsIInputStreamLength, + public nsIAsyncInputStreamLength, + public mozIRemoteLazyInputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSICLONEABLEINPUTSTREAM + NS_DECL_NSICLONEABLEINPUTSTREAMWITHRANGE + NS_DECL_NSIIPCSERIALIZABLEINPUTSTREAM + NS_DECL_NSIFILEMETADATA + NS_DECL_NSIASYNCFILEMETADATA + NS_DECL_NSIINPUTSTREAMLENGTH + NS_DECL_NSIASYNCINPUTSTREAMLENGTH + + // Create a new lazy RemoteLazyInputStream, and move the provided aInputStream + // into storage as referenced by it. May only be called in processes with + // RemoteLazyInputStreamStorage. + static already_AddRefed<RemoteLazyInputStream> WrapStream( + nsIInputStream* aInputStream); + + // mozIRemoteLazyInputStream + NS_IMETHOD TakeInternalStream(nsIInputStream** aStream) override; + NS_IMETHOD GetInternalStreamID(nsID& aID) override; + + private: + friend struct IPC::ParamTraits<mozilla::RemoteLazyInputStream*>; + + // Constructor for an already-closed RemoteLazyInputStream. + RemoteLazyInputStream() = default; + + explicit RemoteLazyInputStream(RemoteLazyInputStreamChild* aActor, + uint64_t aStart = 0, + uint64_t aLength = UINT64_MAX); + + explicit RemoteLazyInputStream(nsIInputStream* aStream); + + ~RemoteLazyInputStream(); + + void StreamNeeded() MOZ_REQUIRES(mMutex); + + // Upon receiving the stream from our actor, we will not wrap it into an async + // stream until needed. This allows callers to get access to the underlying + // potentially-sync stream using `TakeInternalStream` before reading. + nsresult EnsureAsyncRemoteStream() MOZ_REQUIRES(mMutex); + + // Note that data has been read from our input stream, and disconnect from our + // remote actor. + void MarkConsumed(); + + void IPCWrite(IPC::MessageWriter* aWriter); + static already_AddRefed<RemoteLazyInputStream> IPCRead( + IPC::MessageReader* aReader); + + // Helper method to generate a description of a stream for use in loggging. + nsCString Describe() MOZ_REQUIRES(mMutex); + + // Start and length of the slice to apply on this RemoteLazyInputStream when + // fetching the underlying stream with `SendStreamNeeded`. + const uint64_t mStart = 0; + const uint64_t mLength = UINT64_MAX; + + // Any non-const member of this class is protected by mutex because it is + // touched on multiple threads. + Mutex mMutex{"RemoteLazyInputStream::mMutex"}; + + // This is the list of possible states. + enum { + // The initial state. Only ::Available() can be used without receiving an + // error. The available size is known by the actor. + eInit, + + // AsyncWait() has been called for the first time. SendStreamNeeded() has + // been called and we are waiting for the 'real' inputStream. + ePending, + + // When the child receives the stream from the parent, we move to this + // state. The received stream is stored in mInnerStream. From now on, any + // method call will be forwared to mInnerStream or mAsyncInnerStream. + eRunning, + + // If Close() or CloseWithStatus() is called, we move to this state. + // mInnerStream is released and any method will return + // NS_BASE_STREAM_CLOSED. + eClosed, + } mState MOZ_GUARDED_BY(mMutex) = eClosed; + + // The actor which will be used to provide the underlying stream or length + // information when needed, as well as to efficiently allow transferring the + // stream over IPC. + // + // The connection to our actor will be cleared once the stream has been closed + // or has started reading, at which point this stream will be serialized and + // cloned as-if it was the underlying stream. + RefPtr<RemoteLazyInputStreamChild> mActor MOZ_GUARDED_BY(mMutex); + + nsCOMPtr<nsIInputStream> mInnerStream MOZ_GUARDED_BY(mMutex); + nsCOMPtr<nsIAsyncInputStream> mAsyncInnerStream MOZ_GUARDED_BY(mMutex); + + // These 2 values are set only if mState is ePending or eRunning. + // RefPtr is used instead of nsCOMPtr to avoid invoking QueryInterface when + // assigning in debug builds, as `mInputStreamCallback` may not be threadsafe. + RefPtr<nsIInputStreamCallback> mInputStreamCallback MOZ_GUARDED_BY(mMutex); + nsCOMPtr<nsIEventTarget> mInputStreamCallbackEventTarget + MOZ_GUARDED_BY(mMutex); + uint32_t mInputStreamCallbackFlags MOZ_GUARDED_BY(mMutex) = 0; + uint32_t mInputStreamCallbackRequestedCount MOZ_GUARDED_BY(mMutex) = 0; + + // These 2 values are set only if mState is ePending. + nsCOMPtr<nsIFileMetadataCallback> mFileMetadataCallback + MOZ_GUARDED_BY(mMutex); + nsCOMPtr<nsIEventTarget> mFileMetadataCallbackEventTarget + MOZ_GUARDED_BY(mMutex); +}; + +} // namespace mozilla + +template <> +struct IPC::ParamTraits<mozilla::RemoteLazyInputStream*> { + static void Write(IPC::MessageWriter* aWriter, + mozilla::RemoteLazyInputStream* aParam); + static bool Read(IPC::MessageReader* aReader, + RefPtr<mozilla::RemoteLazyInputStream>* aResult); +}; + +#endif // mozilla_RemoteLazyInputStream_h diff --git a/dom/file/ipc/RemoteLazyInputStreamChild.cpp b/dom/file/ipc/RemoteLazyInputStreamChild.cpp new file mode 100644 index 0000000000..f03aa47aad --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamChild.cpp @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteLazyInputStreamChild.h" +#include "RemoteLazyInputStreamThread.h" + +namespace mozilla { + +extern mozilla::LazyLogModule gRemoteLazyStreamLog; + +RemoteLazyInputStreamChild::RemoteLazyInputStreamChild(const nsID& aID) + : mID(aID) {} + +RemoteLazyInputStreamChild::~RemoteLazyInputStreamChild() = default; + +void RemoteLazyInputStreamChild::StreamCreated() { + size_t count = ++mStreamCount; + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Child::StreamCreated %s = %zu", nsIDToCString(mID).get(), count)); +} + +void RemoteLazyInputStreamChild::StreamConsumed() { + size_t count = --mStreamCount; + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Child::StreamConsumed %s = %zu", nsIDToCString(mID).get(), count)); + + // When the count reaches zero, close the underlying actor. + if (count == 0) { + auto* t = RemoteLazyInputStreamThread::Get(); + if (t) { + t->Dispatch( + NS_NewRunnableFunction("RemoteLazyInputStreamChild::StreamConsumed", + [self = RefPtr{this}]() { + if (self->CanSend()) { + self->SendGoodbye(); + } + })); + } // else the xpcom thread shutdown has already started. + } +} + +void RemoteLazyInputStreamChild::ActorDestroy(ActorDestroyReason aReason) { + if (mStreamCount != 0) { + NS_WARNING( + nsPrintfCString("RemoteLazyInputStreamChild disconnected unexpectedly " + "(%zu streams remaining)! %p %s", + size_t(mStreamCount), this, nsIDToCString(mID).get()) + .get()); + } +} + +} // namespace mozilla diff --git a/dom/file/ipc/RemoteLazyInputStreamChild.h b/dom/file/ipc/RemoteLazyInputStreamChild.h new file mode 100644 index 0000000000..c95ed9f058 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamChild.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_RemoteLazyInputStreamChild_h +#define mozilla_RemoteLazyInputStreamChild_h + +#include "mozilla/PRemoteLazyInputStreamChild.h" + +namespace mozilla { + +class RemoteLazyInputStream; + +class RemoteLazyInputStreamChild final : public PRemoteLazyInputStreamChild { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteLazyInputStreamChild, final) + + explicit RemoteLazyInputStreamChild(const nsID& aID); + + const nsID& StreamID() const { return mID; } + + // Manage the count of streams registered on this actor. When the count + // reaches 0 the connection to our remote process will be closed. + void StreamCreated(); + void StreamConsumed(); + + void ActorDestroy(ActorDestroyReason aReason) override; + + private: + ~RemoteLazyInputStreamChild() override; + + const nsID mID; + + std::atomic<size_t> mStreamCount{0}; +}; + +} // namespace mozilla + +#endif // mozilla_RemoteLazyInputStreamChild_h diff --git a/dom/file/ipc/RemoteLazyInputStreamParent.cpp b/dom/file/ipc/RemoteLazyInputStreamParent.cpp new file mode 100644 index 0000000000..d92c529546 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamParent.cpp @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteLazyInputStreamParent.h" +#include "RemoteLazyInputStreamStorage.h" +#include "mozilla/InputStreamLengthHelper.h" +#include "mozilla/ipc/Endpoint.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/InputStreamParams.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "nsStreamUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsNetCID.h" + +namespace mozilla { + +extern mozilla::LazyLogModule gRemoteLazyStreamLog; + +RemoteLazyInputStreamParent::RemoteLazyInputStreamParent(const nsID& aID) + : mID(aID) { + auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr); + if (storage) { + storage->ActorCreated(mID); + } +} + +void RemoteLazyInputStreamParent::ActorDestroy( + IProtocol::ActorDestroyReason aReason) { + auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr); + if (storage) { + storage->ActorDestroyed(mID); + } +} + +mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvClone( + mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent>&& aCloneEndpoint) { + if (!aCloneEndpoint.IsValid()) { + return IPC_FAIL(this, "Unexpected invalid endpoint in RecvClone"); + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Parent::RecvClone %s", nsIDToCString(mID).get())); + + auto* newActor = new RemoteLazyInputStreamParent(mID); + aCloneEndpoint.Bind(newActor); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvStreamNeeded( + uint64_t aStart, uint64_t aLength, StreamNeededResolver&& aResolver) { + nsCOMPtr<nsIInputStream> stream; + auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr); + if (storage) { + storage->GetStream(mID, aStart, aLength, getter_AddRefs(stream)); + } + + if (!stream) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("Parent::RecvStreamNeeded not available! %s", + nsIDToCString(mID).get())); + aResolver(Nothing()); + return IPC_OK(); + } + + Maybe<IPCStream> ipcStream; + if (NS_WARN_IF(!SerializeIPCStream(stream.forget(), ipcStream, + /* aAllowLazy */ false))) { + return IPC_FAIL(this, "IPCStream serialization failed!"); + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Parent::RecvStreamNeeded resolve %s", nsIDToCString(mID).get())); + aResolver(ipcStream); + return IPC_OK(); +} + +mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvLengthNeeded( + LengthNeededResolver&& aResolver) { + nsCOMPtr<nsIInputStream> stream; + auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr); + if (storage) { + storage->GetStream(mID, 0, UINT64_MAX, getter_AddRefs(stream)); + } + + if (!stream) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("Parent::RecvLengthNeeded not available! %s", + nsIDToCString(mID).get())); + aResolver(-1); + return IPC_OK(); + } + + int64_t length = -1; + if (InputStreamLengthHelper::GetSyncLength(stream, &length)) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Parent::RecvLengthNeeded sync resolve %" PRId64 "! %s", length, + nsIDToCString(mID).get())); + aResolver(length); + return IPC_OK(); + } + + InputStreamLengthHelper::GetAsyncLength( + stream, [aResolver, id = mID](int64_t aLength) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Parent::RecvLengthNeeded async resolve %" PRId64 "! %s", + aLength, nsIDToCString(id).get())); + aResolver(aLength); + }); + return IPC_OK(); +} + +mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvGoodbye() { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Parent::RecvGoodbye! %s", nsIDToCString(mID).get())); + Close(); + return IPC_OK(); +} + +} // namespace mozilla diff --git a/dom/file/ipc/RemoteLazyInputStreamParent.h b/dom/file/ipc/RemoteLazyInputStreamParent.h new file mode 100644 index 0000000000..cb5b1d4285 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamParent.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_RemoteLazyInputStreamParent_h +#define mozilla_RemoteLazyInputStreamParent_h + +#include "mozilla/PRemoteLazyInputStreamParent.h" + +class nsIInputStream; + +namespace mozilla { + +class RemoteLazyInputStreamParent final : public PRemoteLazyInputStreamParent { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteLazyInputStreamParent, final) + + explicit RemoteLazyInputStreamParent(const nsID& aID); + + const nsID& ID() const { return mID; } + + mozilla::ipc::IPCResult RecvClone( + mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent>&& aCloneEndpoint); + + mozilla::ipc::IPCResult RecvStreamNeeded(uint64_t aStart, uint64_t aLength, + StreamNeededResolver&& aResolver); + + mozilla::ipc::IPCResult RecvLengthNeeded(LengthNeededResolver&& aResolver); + + mozilla::ipc::IPCResult RecvGoodbye(); + + void ActorDestroy(IProtocol::ActorDestroyReason aReason) override; + + private: + ~RemoteLazyInputStreamParent() override = default; + + const nsID mID; +}; + +} // namespace mozilla + +#endif // mozilla_RemoteLazyInputStreamParent_h diff --git a/dom/file/ipc/RemoteLazyInputStreamStorage.cpp b/dom/file/ipc/RemoteLazyInputStreamStorage.cpp new file mode 100644 index 0000000000..8ce2c22657 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamStorage.cpp @@ -0,0 +1,243 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/SlicedInputStream.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "nsIPropertyBag2.h" +#include "nsStreamUtils.h" +#include "RemoteLazyInputStreamParent.h" +#include "RemoteLazyInputStreamStorage.h" + +namespace mozilla { + +using namespace hal; + +extern mozilla::LazyLogModule gRemoteLazyStreamLog; + +namespace { +StaticMutex gMutex; +StaticRefPtr<RemoteLazyInputStreamStorage> gStorage; +} // namespace + +NS_INTERFACE_MAP_BEGIN(RemoteLazyInputStreamStorage) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsIObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(RemoteLazyInputStreamStorage) +NS_IMPL_RELEASE(RemoteLazyInputStreamStorage) + +/* static */ +Result<RefPtr<RemoteLazyInputStreamStorage>, nsresult> +RemoteLazyInputStreamStorage::Get() { + mozilla::StaticMutexAutoLock lock(gMutex); + if (gStorage) { + RefPtr<RemoteLazyInputStreamStorage> storage = gStorage; + return storage; + } + + return Err(NS_ERROR_NOT_INITIALIZED); +} + +/* static */ +void RemoteLazyInputStreamStorage::Initialize() { + mozilla::StaticMutexAutoLock lock(gMutex); + MOZ_ASSERT(!gStorage); + + gStorage = new RemoteLazyInputStreamStorage(); + + MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue( + "RemoteLazyInputStreamStorage", getter_AddRefs(gStorage->mTaskQueue))); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->AddObserver(gStorage, "xpcom-shutdown", false); + } +} + +NS_IMETHODIMP +RemoteLazyInputStreamStorage::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(!strcmp(aTopic, "xpcom-shutdown")); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, "xpcom-shutdown"); + } + + mozilla::StaticMutexAutoLock lock(gMutex); + gStorage = nullptr; + return NS_OK; +} + +void RemoteLazyInputStreamStorage::AddStream(nsIInputStream* aInputStream, + const nsID& aID) { + MOZ_ASSERT(aInputStream); + + MOZ_LOG( + gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::AddStream(%s) = %p", nsIDToCString(aID).get(), aInputStream)); + + UniquePtr<StreamData> data = MakeUnique<StreamData>(); + data->mInputStream = aInputStream; + + mozilla::StaticMutexAutoLock lock(gMutex); + mStorage.InsertOrUpdate(aID, std::move(data)); +} + +nsCOMPtr<nsIInputStream> RemoteLazyInputStreamStorage::ForgetStream( + const nsID& aID) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::ForgetStream(%s)", nsIDToCString(aID).get())); + + UniquePtr<StreamData> entry; + + mozilla::StaticMutexAutoLock lock(gMutex); + mStorage.Remove(aID, &entry); + + if (!entry) { + return nullptr; + } + + return std::move(entry->mInputStream); +} + +bool RemoteLazyInputStreamStorage::HasStream(const nsID& aID) { + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + return !!data; +} + +void RemoteLazyInputStreamStorage::GetStream(const nsID& aID, uint64_t aStart, + uint64_t aLength, + nsIInputStream** aInputStream) { + *aInputStream = nullptr; + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::GetStream(%s, %" PRIu64 " %" PRIu64 ")", + nsIDToCString(aID).get(), aStart, aLength)); + + nsCOMPtr<nsIInputStream> inputStream; + + // NS_CloneInputStream cannot be called when the mutex is locked because it + // can, recursively call GetStream() in case the child actor lives on the + // parent process. + { + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + if (!data) { + return; + } + + inputStream = data->mInputStream; + } + + MOZ_ASSERT(inputStream); + + // We cannot return always the same inputStream because not all of them are + // able to be reused. Better to clone them. + + nsCOMPtr<nsIInputStream> clonedStream; + nsCOMPtr<nsIInputStream> replacementStream; + + nsresult rv = NS_CloneInputStream(inputStream, getter_AddRefs(clonedStream), + getter_AddRefs(replacementStream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (replacementStream) { + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + // data can be gone in the meantime. + if (!data) { + return; + } + + data->mInputStream = replacementStream; + } + + // Now it's the right time to apply a slice if needed. + if (aStart > 0 || aLength < UINT64_MAX) { + clonedStream = + new SlicedInputStream(clonedStream.forget(), aStart, aLength); + } + + clonedStream.forget(aInputStream); +} + +void RemoteLazyInputStreamStorage::StoreCallback( + const nsID& aID, RemoteLazyInputStreamParentCallback* aCallback) { + MOZ_ASSERT(aCallback); + + MOZ_LOG( + gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::StoreCallback(%s, %p)", nsIDToCString(aID).get(), aCallback)); + + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + if (data) { + MOZ_ASSERT(!data->mCallback); + data->mCallback = aCallback; + } +} + +already_AddRefed<RemoteLazyInputStreamParentCallback> +RemoteLazyInputStreamStorage::TakeCallback(const nsID& aID) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::TakeCallback(%s)", nsIDToCString(aID).get())); + + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + if (!data) { + return nullptr; + } + + RefPtr<RemoteLazyInputStreamParentCallback> callback; + data->mCallback.swap(callback); + return callback.forget(); +} + +void RemoteLazyInputStreamStorage::ActorCreated(const nsID& aID) { + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + if (!data) { + return; + } + + size_t count = ++data->mActorCount; + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::ActorCreated(%s) = %zu", nsIDToCString(aID).get(), count)); +} + +void RemoteLazyInputStreamStorage::ActorDestroyed(const nsID& aID) { + UniquePtr<StreamData> entry; + { + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + if (!data) { + return; + } + + auto newCount = --data->mActorCount; + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::ActorDestroyed(%s) = %zu (cb=%p)", + nsIDToCString(aID).get(), newCount, data->mCallback.get())); + + if (newCount == 0) { + mStorage.Remove(aID, &entry); + } + } + + if (entry && entry->mCallback) { + entry->mCallback->ActorDestroyed(aID); + } +} + +} // namespace mozilla diff --git a/dom/file/ipc/RemoteLazyInputStreamStorage.h b/dom/file/ipc/RemoteLazyInputStreamStorage.h new file mode 100644 index 0000000000..296a8d9313 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamStorage.h @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_RemoteLazyInputStreamStorage_h +#define mozilla_RemoteLazyInputStreamStorage_h + +#include "mozilla/RefPtr.h" +#include "nsClassHashtable.h" +#include "nsIObserver.h" + +class nsIInputStream; +struct nsID; + +namespace mozilla { + +class NS_NO_VTABLE RemoteLazyInputStreamParentCallback { + public: + virtual void ActorDestroyed(const nsID& aID) = 0; + + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + protected: + virtual ~RemoteLazyInputStreamParentCallback() = default; +}; + +class RemoteLazyInputStreamStorage final : public nsIObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + // This initializes the singleton and it must be called on the main-thread. + static void Initialize(); + + static Result<RefPtr<RemoteLazyInputStreamStorage>, nsresult> Get(); + + nsISerialEventTarget* TaskQueue() { return mTaskQueue; } + + void AddStream(nsIInputStream* aInputStream, const nsID& aID); + + // Removes and returns the stream corresponding to the nsID. May return a + // nullptr if there's no stream stored for the nsID. + nsCOMPtr<nsIInputStream> ForgetStream(const nsID& aID); + + bool HasStream(const nsID& aID); + + void GetStream(const nsID& aID, uint64_t aStart, uint64_t aLength, + nsIInputStream** aInputStream); + + void StoreCallback(const nsID& aID, + RemoteLazyInputStreamParentCallback* aCallback); + + already_AddRefed<RemoteLazyInputStreamParentCallback> TakeCallback( + const nsID& aID); + + void ActorCreated(const nsID& aID); + void ActorDestroyed(const nsID& aID); + + private: + RemoteLazyInputStreamStorage() = default; + ~RemoteLazyInputStreamStorage() = default; + + nsCOMPtr<nsISerialEventTarget> mTaskQueue; + + struct StreamData { + nsCOMPtr<nsIInputStream> mInputStream; + RefPtr<RemoteLazyInputStreamParentCallback> mCallback; + size_t mActorCount = 0; + }; + + nsClassHashtable<nsIDHashKey, StreamData> mStorage; +}; + +} // namespace mozilla + +#endif // mozilla_RemoteLazyInputStreamStorage_h diff --git a/dom/file/ipc/RemoteLazyInputStreamThread.cpp b/dom/file/ipc/RemoteLazyInputStreamThread.cpp new file mode 100644 index 0000000000..ec6c99952b --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamThread.cpp @@ -0,0 +1,237 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteLazyInputStreamThread.h" + +#include "ErrorList.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TaskCategory.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "nsXPCOMPrivate.h" + +using namespace mozilla::ipc; + +namespace mozilla { + +namespace { + +StaticMutex gRemoteLazyThreadMutex; +StaticRefPtr<RemoteLazyInputStreamThread> gRemoteLazyThread; + +class ThreadInitializeRunnable final : public Runnable { + public: + ThreadInitializeRunnable() : Runnable("dom::ThreadInitializeRunnable") {} + + NS_IMETHOD + Run() override { + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + MOZ_ASSERT(gRemoteLazyThread); + if (NS_WARN_IF(!gRemoteLazyThread->InitializeOnMainThread())) { + // RemoteLazyInputStreamThread::GetOrCreate might have handed out a + // pointer to our thread already at this point such that we cannot + // just do gRemoteLazyThread = nullptr; here. + MOZ_DIAGNOSTIC_ASSERT( + false, "Async gRemoteLazyThread->InitializeOnMainThread() failed."); + return NS_ERROR_FAILURE; + } + return NS_OK; + } +}; + +} // namespace + +NS_IMPL_ISUPPORTS(RemoteLazyInputStreamThread, nsIObserver, nsIEventTarget, + nsISerialEventTarget, nsIDirectTaskDispatcher) + +bool RLISThreadIsInOrBeyondShutdown() { + // ShutdownPhase::XPCOMShutdownThreads matches + // obs->AddObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, false); + return AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads); +} + +/* static */ +RemoteLazyInputStreamThread* RemoteLazyInputStreamThread::Get() { + if (RLISThreadIsInOrBeyondShutdown()) { + return nullptr; + } + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + return gRemoteLazyThread; +} + +/* static */ +RemoteLazyInputStreamThread* RemoteLazyInputStreamThread::GetOrCreate() { + if (RLISThreadIsInOrBeyondShutdown()) { + return nullptr; + } + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + if (!gRemoteLazyThread) { + gRemoteLazyThread = new RemoteLazyInputStreamThread(); + if (!gRemoteLazyThread->Initialize()) { + gRemoteLazyThread = nullptr; + } + } + + return gRemoteLazyThread; +} + +bool RemoteLazyInputStreamThread::Initialize() { + nsCOMPtr<nsIThread> thread; + nsresult rv = NS_NewNamedThread("RemoteLzyStream", getter_AddRefs(thread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + mThread = thread; + + if (!NS_IsMainThread()) { + RefPtr<Runnable> runnable = new ThreadInitializeRunnable(); + nsresult rv = + SchedulerGroup::Dispatch(TaskCategory::Other, runnable.forget()); + return !NS_WARN_IF(NS_FAILED(rv)); + } + + return InitializeOnMainThread(); +} + +bool RemoteLazyInputStreamThread::InitializeOnMainThread() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return false; + } + + nsresult rv = + obs->AddObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, false); + return !NS_WARN_IF(NS_FAILED(rv)); +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(!strcmp(aTopic, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID)); + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + if (mThread) { + mThread->Shutdown(); + mThread = nullptr; + } + + gRemoteLazyThread = nullptr; + + return NS_OK; +} + +// nsIEventTarget + +NS_IMETHODIMP_(bool) +RemoteLazyInputStreamThread::IsOnCurrentThreadInfallible() { + return mThread->IsOnCurrentThread(); +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::IsOnCurrentThread(bool* aRetval) { + return mThread->IsOnCurrentThread(aRetval); +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable, + uint32_t aFlags) { + if (RLISThreadIsInOrBeyondShutdown()) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + nsCOMPtr<nsIRunnable> runnable(aRunnable); + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + return mThread->Dispatch(runnable.forget(), aFlags); +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::DispatchFromScript(nsIRunnable* aRunnable, + uint32_t aFlags) { + nsCOMPtr<nsIRunnable> runnable(aRunnable); + return Dispatch(runnable.forget(), aFlags); +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::DelayedDispatch(already_AddRefed<nsIRunnable>, + uint32_t) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::RegisterShutdownTask(nsITargetShutdownTask*) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::UnregisterShutdownTask(nsITargetShutdownTask*) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::DispatchDirectTask( + already_AddRefed<nsIRunnable> aRunnable) { + nsCOMPtr<nsIRunnable> runnable(aRunnable); + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + nsCOMPtr<nsIDirectTaskDispatcher> dispatcher = do_QueryInterface(mThread); + + if (dispatcher) { + return dispatcher->DispatchDirectTask(runnable.forget()); + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP RemoteLazyInputStreamThread::DrainDirectTasks() { + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + nsCOMPtr<nsIDirectTaskDispatcher> dispatcher = do_QueryInterface(mThread); + + if (dispatcher) { + return dispatcher->DrainDirectTasks(); + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP RemoteLazyInputStreamThread::HaveDirectTasks(bool* aValue) { + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + nsCOMPtr<nsIDirectTaskDispatcher> dispatcher = do_QueryInterface(mThread); + + if (dispatcher) { + return dispatcher->HaveDirectTasks(aValue); + } + + return NS_ERROR_FAILURE; +} + +bool IsOnDOMFileThread() { + MOZ_ASSERT(!RLISThreadIsInOrBeyondShutdown()); + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + MOZ_ASSERT(gRemoteLazyThread); + + return gRemoteLazyThread->IsOnCurrentThreadInfallible(); +} + +void AssertIsOnDOMFileThread() { MOZ_ASSERT(IsOnDOMFileThread()); } + +} // namespace mozilla diff --git a/dom/file/ipc/RemoteLazyInputStreamThread.h b/dom/file/ipc/RemoteLazyInputStreamThread.h new file mode 100644 index 0000000000..378cb09009 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamThread.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_RemoteLazyInputStreamThread_h +#define mozilla_RemoteLazyInputStreamThread_h + +#include "mozilla/RemoteLazyInputStreamChild.h" +#include "nsIEventTarget.h" +#include "nsIObserver.h" +#include "nsTArray.h" + +class nsIThread; + +namespace mozilla { + +class RemoteLazyInputStreamChild; + +// XXX Rename this class since it's used by LSNG too. +class RemoteLazyInputStreamThread final : public nsIObserver, + public nsISerialEventTarget, + public nsIDirectTaskDispatcher { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIEVENTTARGET + NS_DECL_NSISERIALEVENTTARGET + NS_DECL_NSIDIRECTTASKDISPATCHER + + static RemoteLazyInputStreamThread* Get(); + + static RemoteLazyInputStreamThread* GetOrCreate(); + + bool Initialize(); + + bool InitializeOnMainThread(); + + private: + ~RemoteLazyInputStreamThread() = default; + + nsCOMPtr<nsIThread> mThread; +}; + +bool IsOnDOMFileThread(); + +void AssertIsOnDOMFileThread(); + +} // namespace mozilla + +#endif // mozilla_RemoteLazyInputStreamThread_h diff --git a/dom/file/ipc/TemporaryIPCBlobChild.cpp b/dom/file/ipc/TemporaryIPCBlobChild.cpp new file mode 100644 index 0000000000..7c7df55d81 --- /dev/null +++ b/dom/file/ipc/TemporaryIPCBlobChild.cpp @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TemporaryIPCBlobChild.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/MutableBlobStorage.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include <private/pprio.h> + +namespace mozilla::dom { + +TemporaryIPCBlobChild::TemporaryIPCBlobChild(MutableBlobStorage* aStorage) + : mMutableBlobStorage(aStorage), mActive(true) { + MOZ_ASSERT(aStorage); +} + +TemporaryIPCBlobChild::~TemporaryIPCBlobChild() = default; + +mozilla::ipc::IPCResult TemporaryIPCBlobChild::RecvFileDesc( + const FileDescriptor& aFD) { + MOZ_ASSERT(mActive); + + auto rawFD = aFD.ClonePlatformHandle(); + PRFileDesc* prfile = PR_ImportFile(PROsfd(rawFD.release())); + + mMutableBlobStorage->TemporaryFileCreated(prfile); + mMutableBlobStorage = nullptr; + return IPC_OK(); +} + +mozilla::ipc::IPCResult TemporaryIPCBlobChild::Recv__delete__( + const IPCBlobOrError& aBlobOrError) { + mActive = false; + mMutableBlobStorage = nullptr; + + if (aBlobOrError.type() == IPCBlobOrError::TIPCBlob) { + // This must be always deserialized. + RefPtr<BlobImpl> blobImpl = + IPCBlobUtils::Deserialize(aBlobOrError.get_IPCBlob()); + MOZ_ASSERT(blobImpl); + + if (mCallback) { + mCallback->OperationSucceeded(blobImpl); + } + } else if (mCallback) { + MOZ_ASSERT(aBlobOrError.type() == IPCBlobOrError::Tnsresult); + mCallback->OperationFailed(aBlobOrError.get_nsresult()); + } + + mCallback = nullptr; + + return IPC_OK(); +} + +void TemporaryIPCBlobChild::ActorDestroy(ActorDestroyReason aWhy) { + mActive = false; + mMutableBlobStorage = nullptr; + + if (mCallback) { + mCallback->OperationFailed(NS_ERROR_FAILURE); + mCallback = nullptr; + } +} + +void TemporaryIPCBlobChild::AskForBlob(TemporaryIPCBlobChildCallback* aCallback, + const nsACString& aContentType, + PRFileDesc* aFD) { + MOZ_ASSERT(aCallback); + MOZ_ASSERT(!mCallback); + + if (!mActive) { + aCallback->OperationFailed(NS_ERROR_FAILURE); + return; + } + + FileDescriptor fdd = FileDescriptor( + FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(aFD))); + + mCallback = aCallback; + SendOperationDone(aContentType, fdd); +} + +} // namespace mozilla::dom diff --git a/dom/file/ipc/TemporaryIPCBlobChild.h b/dom/file/ipc/TemporaryIPCBlobChild.h new file mode 100644 index 0000000000..a909ee0d53 --- /dev/null +++ b/dom/file/ipc/TemporaryIPCBlobChild.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_TemporaryIPCBlobChild_h +#define mozilla_dom_TemporaryIPCBlobChild_h + +#include "mozilla/dom/PTemporaryIPCBlob.h" +#include "mozilla/dom/PTemporaryIPCBlobChild.h" + +namespace mozilla::dom { + +class BlobImpl; +class MutableBlobStorage; + +class TemporaryIPCBlobChildCallback { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void OperationSucceeded(BlobImpl* aBlobImpl) = 0; + virtual void OperationFailed(nsresult aRv) = 0; +}; + +class TemporaryIPCBlobChild final : public PTemporaryIPCBlobChild { + friend class PTemporaryIPCBlobChild; + + public: + NS_INLINE_DECL_REFCOUNTING(TemporaryIPCBlobChild) + + explicit TemporaryIPCBlobChild(MutableBlobStorage* aStorage); + + void AskForBlob(TemporaryIPCBlobChildCallback* aCallback, + const nsACString& aContentType, PRFileDesc* aFD); + + private: + ~TemporaryIPCBlobChild() override; + + mozilla::ipc::IPCResult RecvFileDesc(const FileDescriptor& aFD); + + mozilla::ipc::IPCResult Recv__delete__(const IPCBlobOrError& aBlobOrError); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + RefPtr<MutableBlobStorage> mMutableBlobStorage; + RefPtr<TemporaryIPCBlobChildCallback> mCallback; + bool mActive; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TemporaryIPCBlobChild_h diff --git a/dom/file/ipc/TemporaryIPCBlobParent.cpp b/dom/file/ipc/TemporaryIPCBlobParent.cpp new file mode 100644 index 0000000000..bd2c6beefc --- /dev/null +++ b/dom/file/ipc/TemporaryIPCBlobParent.cpp @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "private/pprio.h" +#include "TemporaryIPCBlobParent.h" +#include "mozilla/dom/FileBlobImpl.h" +#include "nsAnonymousTemporaryFile.h" +#include "TemporaryFileBlobImpl.h" +#include "mozilla/dom/IPCBlobUtils.h" + +namespace mozilla::dom { + +TemporaryIPCBlobParent::TemporaryIPCBlobParent() : mActive(true) {} + +TemporaryIPCBlobParent::~TemporaryIPCBlobParent() { + // If we still have mFile, let's remove it. + if (mFile) { + mFile->Remove(false); + } +} + +mozilla::ipc::IPCResult TemporaryIPCBlobParent::CreateAndShareFile() { + MOZ_ASSERT(mActive); + MOZ_ASSERT(!mFile); + + nsresult rv = NS_OpenAnonymousTemporaryNsIFile(getter_AddRefs(mFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return SendDeleteError(rv); + } + + PRFileDesc* fd; + rv = mFile->OpenNSPRFileDesc(PR_RDWR, PR_IRWXU, &fd); + if (NS_WARN_IF(NS_FAILED(rv))) { + return SendDeleteError(rv); + } + + FileDescriptor fdd = FileDescriptor( + FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(fd))); + + // The FileDescriptor object owns a duplicate of the file handle; we + // must close the original (and clean up the NSPR descriptor). + PR_Close(fd); + + (void)SendFileDesc(fdd); + return IPC_OK(); +} + +mozilla::ipc::IPCResult TemporaryIPCBlobParent::RecvOperationFailed() { + MOZ_ASSERT(mActive); + mActive = false; + + // Nothing to do. + (void)Send__delete__(this, NS_ERROR_FAILURE); + return IPC_OK(); +} + +mozilla::ipc::IPCResult TemporaryIPCBlobParent::RecvOperationDone( + const nsCString& aContentType, const FileDescriptor& aFD) { + MOZ_ASSERT(mActive); + mActive = false; + + // We have received a file descriptor because in this way we have kept the + // file locked on windows during the IPC communication. After the creation of + // the TemporaryFileBlobImpl, this prfile can be closed. + auto rawFD = aFD.ClonePlatformHandle(); + PRFileDesc* prfile = PR_ImportFile(PROsfd(rawFD.release())); + + // Let's create the BlobImpl. + nsCOMPtr<nsIFile> file = std::move(mFile); + + RefPtr<TemporaryFileBlobImpl> blobImpl = + new TemporaryFileBlobImpl(file, NS_ConvertUTF8toUTF16(aContentType)); + + PR_Close(prfile); + + IPCBlob ipcBlob; + nsresult rv = IPCBlobUtils::Serialize(blobImpl, ipcBlob); + if (NS_WARN_IF(NS_FAILED(rv))) { + (void)Send__delete__(this, NS_ERROR_FAILURE); + return IPC_OK(); + } + + (void)Send__delete__(this, ipcBlob); + return IPC_OK(); +} + +void TemporaryIPCBlobParent::ActorDestroy(ActorDestroyReason aWhy) { + mActive = false; +} + +mozilla::ipc::IPCResult TemporaryIPCBlobParent::SendDeleteError(nsresult aRv) { + MOZ_ASSERT(mActive); + mActive = false; + + (void)Send__delete__(this, aRv); + return IPC_OK(); +} + +} // namespace mozilla::dom diff --git a/dom/file/ipc/TemporaryIPCBlobParent.h b/dom/file/ipc/TemporaryIPCBlobParent.h new file mode 100644 index 0000000000..2609e8d820 --- /dev/null +++ b/dom/file/ipc/TemporaryIPCBlobParent.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_TemporaryIPCBlobParent_h +#define mozilla_dom_TemporaryIPCBlobParent_h + +#include "mozilla/dom/PTemporaryIPCBlob.h" +#include "mozilla/dom/PTemporaryIPCBlobParent.h" + +class nsIFile; + +namespace mozilla::dom { + +class TemporaryIPCBlobParent final : public PTemporaryIPCBlobParent { + friend class PTemporaryIPCBlobParent; + + public: + explicit TemporaryIPCBlobParent(); + + mozilla::ipc::IPCResult CreateAndShareFile(); + + private: + ~TemporaryIPCBlobParent() override; + + mozilla::ipc::IPCResult RecvOperationFailed(); + + mozilla::ipc::IPCResult RecvOperationDone(const nsCString& aContentType, + const FileDescriptor& aFD); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult SendDeleteError(nsresult aRv); + + nsCOMPtr<nsIFile> mFile; + bool mActive; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TemporaryIPCBlobParent_h diff --git a/dom/file/ipc/moz.build b/dom/file/ipc/moz.build new file mode 100644 index 0000000000..21c2d50cac --- /dev/null +++ b/dom/file/ipc/moz.build @@ -0,0 +1,69 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: File") + +XPIDL_SOURCES += [ + "mozIRemoteLazyInputStream.idl", +] + +XPIDL_MODULE = "dom" + +EXPORTS.mozilla.dom += [ + "FileCreatorChild.h", + "FileCreatorParent.h", + "IPCBlobUtils.h", + "TemporaryIPCBlobChild.h", + "TemporaryIPCBlobParent.h", +] + +EXPORTS.mozilla += [ + "RemoteLazyInputStream.h", + "RemoteLazyInputStreamChild.h", + "RemoteLazyInputStreamParent.h", + "RemoteLazyInputStreamStorage.h", + "RemoteLazyInputStreamThread.h", +] + +UNIFIED_SOURCES += [ + "FileCreatorChild.cpp", + "FileCreatorParent.cpp", + "IPCBlobUtils.cpp", + "RemoteLazyInputStream.cpp", + "RemoteLazyInputStreamChild.cpp", + "RemoteLazyInputStreamParent.cpp", + "RemoteLazyInputStreamStorage.cpp", + "RemoteLazyInputStreamThread.cpp", + "TemporaryIPCBlobChild.cpp", + "TemporaryIPCBlobParent.cpp", +] + +IPDL_SOURCES += [ + "IPCBlob.ipdlh", + "PFileCreator.ipdl", + "PRemoteLazyInputStream.ipdl", + "PTemporaryIPCBlob.ipdl", +] + +LOCAL_INCLUDES += [ + "/dom/file", + "/dom/ipc", + "/xpcom/build", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +# Add libFuzzer configuration directives +include("/tools/fuzzing/libfuzzer-config.mozbuild") + +FINAL_LIBRARY = "xul" + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"] +MOCHITEST_MANIFESTS += ["tests/mochitest.ini"] diff --git a/dom/file/ipc/mozIRemoteLazyInputStream.idl b/dom/file/ipc/mozIRemoteLazyInputStream.idl new file mode 100644 index 0000000000..8303b2e0fc --- /dev/null +++ b/dom/file/ipc/mozIRemoteLazyInputStream.idl @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIInputStream; + +/* + * A simple interface to get the underlying stream from an + * RemoteLazyInputStream. + */ +[scriptable, builtinclass, uuid(4125585f-b0c2-4964-a83c-4b0d99f26d49)] +interface mozIRemoteLazyInputStream : nsISupports +{ + /** + * Attempts to take the internal stream out of this mozIRemoteLazyInputStream. + * Throws NS_BASE_STREAM_WOULD_BLOCK if the stream isn't available yet, and + * NS_BASE_STREAM_CLOSED if it was already closed. + */ + [noscript] nsIInputStream TakeInternalStream(); + + /** + * If this RemoteLazyInputStream is actively backed by an actor, get the + * underlying actor's ID. Will throw if the underlying actor is no longer + * available. + */ + [noscript] readonly attribute nsIDRef internalStreamID; +}; diff --git a/dom/file/ipc/tests/browser.ini b/dom/file/ipc/tests/browser.ini new file mode 100644 index 0000000000..f6fcdc37a4 --- /dev/null +++ b/dom/file/ipc/tests/browser.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + empty.html + +[browser_ipcBlob.js] +[browser_ipcBlob_temporary.js] +support-files = temporary.sjs diff --git a/dom/file/ipc/tests/browser_ipcBlob.js b/dom/file/ipc/tests/browser_ipcBlob.js new file mode 100644 index 0000000000..17fe31e0bd --- /dev/null +++ b/dom/file/ipc/tests/browser_ipcBlob.js @@ -0,0 +1,253 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */ + +requestLongerTimeout(3); + +const BASE_URI = "http://mochi.test:8888/browser/dom/file/ipc/tests/empty.html"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.bloburl_per_agent_cluster", false]], + }); +}); + +// More than 1mb memory blob childA-parent-childB. +add_task(async function test_CtoPtoC_big() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + let blob = await SpecialPowers.spawn(browser1, [], function () { + Cu.importGlobalProperties(["Blob"]); + let blob = new Blob([new Array(1024 * 1024).join("123456789ABCDEF")]); + return blob; + }); + + ok(blob, "CtoPtoC-big: We have a blob!"); + is( + blob.size, + new Array(1024 * 1024).join("123456789ABCDEF").length, + "CtoPtoC-big: The size matches" + ); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn(browser2, [blob], function (blob) { + return new Promise(resolve => { + let fr = new content.FileReader(); + fr.readAsText(blob); + fr.onloadend = function () { + resolve(fr.result == new Array(1024 * 1024).join("123456789ABCDEF")); + }; + }); + }); + + ok(status, "CtoPtoC-big: Data match!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// Less than 1mb memory blob childA-parent-childB. +add_task(async function test_CtoPtoC_small() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + let blob = await SpecialPowers.spawn(browser1, [], function () { + Cu.importGlobalProperties(["Blob"]); + let blob = new Blob(["hello world!"]); + return blob; + }); + + ok(blob, "CtoPtoC-small: We have a blob!"); + is(blob.size, "hello world!".length, "CtoPtoC-small: The size matches"); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn(browser2, [blob], function (blob) { + return new Promise(resolve => { + let fr = new content.FileReader(); + fr.readAsText(blob); + fr.onloadend = function () { + resolve(fr.result == "hello world!"); + }; + }); + }); + + ok(status, "CtoPtoC-small: Data match!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// More than 1mb memory blob childA-parent-childB: BroadcastChannel +add_task(async function test_CtoPtoC_bc_big() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + await SpecialPowers.spawn(browser1, [], function () { + Cu.importGlobalProperties(["Blob"]); + var bc = new content.BroadcastChannel("test"); + bc.onmessage = function () { + bc.postMessage( + new Blob([new Array(1024 * 1024).join("123456789ABCDEF")]) + ); + }; + }); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn(browser2, [], function () { + return new Promise(resolve => { + var bc = new content.BroadcastChannel("test"); + bc.onmessage = function (e) { + let fr = new content.FileReader(); + fr.readAsText(e.data); + fr.onloadend = function () { + resolve(fr.result == new Array(1024 * 1024).join("123456789ABCDEF")); + }; + }; + + bc.postMessage("GO!"); + }); + }); + + ok(status, "CtoPtoC-broadcastChannel-big: Data match!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// Less than 1mb memory blob childA-parent-childB: BroadcastChannel +add_task(async function test_CtoPtoC_bc_small() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + await SpecialPowers.spawn(browser1, [], function () { + Cu.importGlobalProperties(["Blob"]); + var bc = new content.BroadcastChannel("test"); + bc.onmessage = function () { + bc.postMessage(new Blob(["hello world!"])); + }; + }); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn(browser2, [], function () { + return new Promise(resolve => { + var bc = new content.BroadcastChannel("test"); + bc.onmessage = function (e) { + let fr = new content.FileReader(); + fr.readAsText(e.data); + fr.onloadend = function () { + resolve(fr.result == "hello world!"); + }; + }; + + bc.postMessage("GO!"); + }); + }); + + ok(status, "CtoPtoC-broadcastChannel-small: Data match!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// blob URL childA-parent-childB +add_task(async function test_CtoPtoC_bc_small() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + let blobURL = await SpecialPowers.spawn(browser1, [], function () { + Cu.importGlobalProperties(["Blob"]); + return content.URL.createObjectURL(new content.Blob(["hello world!"])); + }); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn( + browser2, + [blobURL], + function (blobURL) { + return new Promise(resolve => { + var xhr = new content.XMLHttpRequest(); + xhr.open("GET", blobURL); + xhr.onloadend = function () { + resolve(xhr.response == "hello world!"); + }; + + xhr.send(); + }); + } + ); + + ok(status, "CtoPtoC-blobURL: Data match!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// Multipart Blob childA-parent-childB. +add_task(async function test_CtoPtoC_multipart() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + let blob = await SpecialPowers.spawn(browser1, [], function () { + Cu.importGlobalProperties(["Blob"]); + return new Blob(["!"]); + }); + + ok(blob, "CtoPtoC-multipart: We have a blob!"); + is(blob.size, "!".length, "CtoPtoC-multipart: The size matches"); + + let newBlob = new Blob(["world", blob]); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn(browser2, [newBlob], function (blob) { + Cu.importGlobalProperties(["Blob"]); + return new Promise(resolve => { + let fr = new content.FileReader(); + fr.readAsText(new Blob(["hello ", blob])); + fr.onloadend = function () { + resolve(fr.result == "hello world!"); + }; + }); + }); + + ok(status, "CtoPtoC-multipart: Data match!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// Multipart Blob childA-parent with a max size +add_task(async function test_CtoPsize_multipart() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser = gBrowser.getBrowserForTab(tab); + + let blob = await SpecialPowers.spawn(browser, [], function () { + Cu.importGlobalProperties(["Blob"]); + + let data = new Array(1024 * 512).join("A"); + let blob1 = new Blob([data]); + let blob2 = new Blob([data]); + let blob3 = new Blob([data]); + + return new Blob([blob1, blob2, blob3]); + }); + + ok(blob, "CtoPsize-multipart: We have a blob!"); + is( + blob.size, + new Array(1024 * 512).join("A").length * 3, + "CtoPsize-multipart: The size matches" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/file/ipc/tests/browser_ipcBlob_temporary.js b/dom/file/ipc/tests/browser_ipcBlob_temporary.js new file mode 100644 index 0000000000..cc65768140 --- /dev/null +++ b/dom/file/ipc/tests/browser_ipcBlob_temporary.js @@ -0,0 +1,115 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */ + +requestLongerTimeout(3); + +const BASE_URI = "http://mochi.test:8888/browser/dom/file/ipc/tests/empty.html"; + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.blob.memoryToTemporaryFile", 1], + ["dom.ipc.processCount", 4], + ], + }); + + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + await SpecialPowers.spawn(browser2, [], function () { + content.window.testPromise = new content.window.Promise(resolve => { + let bc = new content.window.BroadcastChannel("foobar"); + bc.onmessage = e => { + function realTest() { + return new content.window.Promise(resolve => { + let count = 10; + for (let i = 0; i < count; ++i) { + info("FileReader at the same time: " + i); + let fr = new content.window.FileReader(); + fr.readAsText(e.data); + fr.onerror = () => { + ok(false, "Something wrong happened."); + }; + + fr.onloadend = () => { + is(fr.result.length, e.data.size, "FileReader worked fine."); + if (!--count) { + resolve(true); + } + }; + } + }); + } + + let promises = []; + for (let i = 0; i < 5; ++i) { + promises.push(realTest()); + } + + Promise.all(promises).then(() => { + resolve(true); + }); + }; + }); + }); + + let status = await SpecialPowers.spawn(browser1, [], function () { + let p = new content.window.Promise(resolve => { + let xhr = new content.window.XMLHttpRequest(); + xhr.open("GET", "temporary.sjs", true); + xhr.responseType = "blob"; + xhr.onload = () => { + resolve(xhr.response); + }; + xhr.send(); + }); + + return p.then(blob => { + function realTest() { + return new content.window.Promise(resolve => { + info("Let's broadcast the blob..."); + let bc = new content.window.BroadcastChannel("foobar"); + bc.postMessage(blob); + + info("Here the test..."); + let count = 10; + for (let i = 0; i < count; ++i) { + info("FileReader at the same time: " + i); + let fr = new content.window.FileReader(); + fr.readAsText(blob); + fr.onerror = () => { + ok(false, "Something wrong happened."); + }; + + fr.onloadend = () => { + is(fr.result.length, blob.size, "FileReader worked fine."); + if (!--count) { + resolve(true); + } + }; + } + }); + } + + let promises = []; + for (let i = 0; i < 5; ++i) { + promises.push(realTest()); + } + + return Promise.all(promises); + }); + }); + + ok(status, "All good for tab1!"); + + status = await SpecialPowers.spawn(browser2, [], function () { + return content.window.testPromise; + }); + + ok(status, "All good for tab2!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/dom/file/ipc/tests/empty.html b/dom/file/ipc/tests/empty.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/file/ipc/tests/empty.html diff --git a/dom/file/ipc/tests/green.jpg b/dom/file/ipc/tests/green.jpg Binary files differnew file mode 100644 index 0000000000..48c454d27c --- /dev/null +++ b/dom/file/ipc/tests/green.jpg diff --git a/dom/file/ipc/tests/mochitest.ini b/dom/file/ipc/tests/mochitest.ini new file mode 100644 index 0000000000..99b885762a --- /dev/null +++ b/dom/file/ipc/tests/mochitest.ini @@ -0,0 +1,12 @@ +[DEFAULT] +support-files = script_file.js + +[test_ipcBlob_fileReaderSync.html] +[test_ipcBlob_workers.html] +[test_ipcBlob_createImageBitmap.html] +support-files = green.jpg +skip-if = + http3 +[test_ipcBlob_emptyMultiplex.html] +[test_ipcBlob_mixedMultiplex.html] +support-files = ok.sjs diff --git a/dom/file/ipc/tests/ok.sjs b/dom/file/ipc/tests/ok.sjs new file mode 100644 index 0000000000..42f65733d9 --- /dev/null +++ b/dom/file/ipc/tests/ok.sjs @@ -0,0 +1,11 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/html", false); + response.write(request.getHeader("Content-Length")); +} diff --git a/dom/file/ipc/tests/script_file.js b/dom/file/ipc/tests/script_file.js new file mode 100644 index 0000000000..b671d46f39 --- /dev/null +++ b/dom/file/ipc/tests/script_file.js @@ -0,0 +1,53 @@ +/* eslint-env mozilla/chrome-script */ + +Cu.importGlobalProperties(["File"]); + +addMessageListener("file.open", function (e) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + testFile.append("ipc_fileReader_testing"); + testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + var outStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outStream.init( + testFile, + 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, + 0 + ); + + var fileData = "Hello World!"; + outStream.write(fileData, fileData.length); + outStream.close(); + + File.createFromNsIFile(testFile).then(function (file) { + sendAsyncMessage("file.opened", { file }); + }); +}); + +addMessageListener("emptyfile.open", function (e) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + testFile.append("ipc_fileReader_testing"); + testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + var outStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outStream.init( + testFile, + 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, + 0 + ); + + File.createFromNsIFile(testFile).then(function (file) { + sendAsyncMessage("emptyfile.opened", { file }); + }); +}); diff --git a/dom/file/ipc/tests/temporary.sjs b/dom/file/ipc/tests/temporary.sjs new file mode 100644 index 0000000000..76db4e2f2e --- /dev/null +++ b/dom/file/ipc/tests/temporary.sjs @@ -0,0 +1,6 @@ +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + + var data = new Array(1024 * 64).join("1234567890ABCDEF"); + response.bodyOutputStream.write(data, data.length); +} diff --git a/dom/file/ipc/tests/test_ipcBlob_createImageBitmap.html b/dom/file/ipc/tests/test_ipcBlob_createImageBitmap.html new file mode 100644 index 0000000000..868888c529 --- /dev/null +++ b/dom/file/ipc/tests/test_ipcBlob_createImageBitmap.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test IPCBlob and CreateImageBitmap</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function test_mainThread() { + let bc = new BroadcastChannel('testMainThread'); + bc.onmessage = e => { + createImageBitmap(e.data).then(image => { + ok(image.height, "this image has a valid size."); + }, () => { + ok(false, "error creating the image!"); + }).then(next); + } + + fetch('green.jpg').then(r => r.blob()).then(blob => { + let bc = new BroadcastChannel('testMainThread'); + bc.postMessage(blob); + }); +} + +function test_worker() { + function workerScript() { + function ok(a, msg) { postMessage({ type: 'test', status: !!a, msg }); }; + function finish() { postMessage({ type: 'finish' }); }; + + let bc = new BroadcastChannel('testWorker'); + bc.onmessage = e => { + createImageBitmap(e.data).then(image => { + ok(image.height, "this image has a valid size."); + }, () => { + ok(false, "error creating the image!"); + }).then(finish); + } + + fetch('http://mochi.test:8888/tests/dom/file/ipc/tests/green.jpg').then(r => r.blob()).then(blob => { + let bc = new BroadcastChannel('testWorker'); + bc.postMessage(blob); + }); + } + let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"])); + let worker = new Worker(workerUrl); + + worker.onmessage = event => { + if (event.data.type == 'test') { + ok(event.data.status, event.data.msg); + return; + } + + if (event.data.type == 'finish') { + next(); + } + } +} + +let tests = [ + test_mainThread, + test_worker, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + let test = tests.shift(); + test(); +} + +next(); + +</script> +</pre> +</body> +</html> diff --git a/dom/file/ipc/tests/test_ipcBlob_emptyMultiplex.html b/dom/file/ipc/tests/test_ipcBlob_emptyMultiplex.html new file mode 100644 index 0000000000..0487069467 --- /dev/null +++ b/dom/file/ipc/tests/test_ipcBlob_emptyMultiplex.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test an empty IPCBlob together with other parts</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +function checkContent(msg, content) { + return new Promise(resolve => { + let fr = new FileReader(); + fr.readAsText(new Blob(content)); + fr.onloadend = () => { + is(fr.result, "Hello world!", "The content matches: " + msg); + resolve(); + }; + }); +} + +SimpleTest.waitForExplicitFinish(); + +let url = SimpleTest.getTestFileURL("script_file.js"); + +let script = SpecialPowers.loadChromeScript(url); +script.addMessageListener("emptyfile.opened", message => { + checkContent("middle", ["Hello ", message.file, "world!"]). + then(() => checkContent("begin", [message.file, "Hello world!"])). + then(() => checkContent("end", ["Hello world!", message.file])). + then(() => checkContent("random", [message.file, message.file, "Hello world!", message.file])). + then(() => checkContent("random 2", [message.file, message.file, "Hello ", + message.file, "world", message.file, + message.file, "!", message.file, "", + message.file, message.file])). + then(SimpleTest.finish); +}); + +script.sendAsyncMessage("emptyfile.open"); + +</script> +</pre> +</body> +</html> diff --git a/dom/file/ipc/tests/test_ipcBlob_fileReaderSync.html b/dom/file/ipc/tests/test_ipcBlob_fileReaderSync.html new file mode 100644 index 0000000000..f37d4b79ed --- /dev/null +++ b/dom/file/ipc/tests/test_ipcBlob_fileReaderSync.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test IPCBlob and FileReaderSync</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +function workerScript() { + onmessage = function(event) { + let readerMemoryBlob = new FileReaderSync(); + let status = readerMemoryBlob.readAsText(new Blob(['hello world'])) == 'hello world'; + postMessage({ status, message: "FileReaderSync with memory blob still works" }); + + let readerIPCBlob1 = new FileReaderSync(); + postMessage({ blob: event.data, method: 'readAsText', + data: readerIPCBlob1.readAsText(event.data)}); + + let readerIPCBlob2 = new FileReaderSync(); + postMessage({ blob: event.data, method: 'readAsArrayBuffer', + data: readerIPCBlob2.readAsArrayBuffer(event.data)}); + + let readerIPCBlob3 = new FileReaderSync(); + postMessage({ blob: event.data, method: 'readAsDataURL', + data: readerIPCBlob3.readAsDataURL(event.data)}); + + let multipartBlob = new Blob(['wow', event.data]); + + let readerIPCBlobMultipart1 = new FileReaderSync(); + postMessage({ blob: multipartBlob, method: 'readAsText', + data: readerIPCBlobMultipart1.readAsText(multipartBlob)}); + + let readerIPCBlobMultipart2 = new FileReaderSync(); + postMessage({ blob: multipartBlob, method: 'readAsArrayBuffer', + data: readerIPCBlobMultipart2.readAsArrayBuffer(multipartBlob)}); + + let readerIPCBlobMultipart3 = new FileReaderSync(); + postMessage({ blob: multipartBlob, method: 'readAsDataURL', + data: readerIPCBlobMultipart3.readAsDataURL(multipartBlob)}); + + postMessage({ finish: true }); + } +} + +let completed = false; +let pendingTasks = 0; +function maybeFinish() { + if (completed && !pendingTasks) { + SimpleTest.finish(); + } +} + +let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"])); +let worker = new Worker(workerUrl); +worker.onmessage = event => { + if ("status" in event.data) { + ok(event.data.status, event.data.message); + return; + } + + if ("blob" in event.data) { + let fr = new FileReader(); + fr[event.data.method](event.data.blob); + ++pendingTasks; + fr.onload = () => { + if (event.data.method != 'readAsArrayBuffer') { + is(event.data.data, fr.result, "The file has been read"); + } else { + is(event.data.data.byteLength, fr.result.byteLength, "The file has been read"); + } + --pendingTasks; + maybeFinish(); + } + + return; + } + + if ("finish" in event.data) { + completed = true; + maybeFinish(); + } +}; + +let url = SimpleTest.getTestFileURL("script_file.js"); +let script = SpecialPowers.loadChromeScript(url); +script.addMessageListener("file.opened", message => { + worker.postMessage(message.file); +}); + +script.sendAsyncMessage("file.open"); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/file/ipc/tests/test_ipcBlob_mixedMultiplex.html b/dom/file/ipc/tests/test_ipcBlob_mixedMultiplex.html new file mode 100644 index 0000000000..c8e046fa88 --- /dev/null +++ b/dom/file/ipc/tests/test_ipcBlob_mixedMultiplex.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test an empty IPCBlob together with other parts</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +let url = SimpleTest.getTestFileURL("script_file.js"); +let data = new Array(1024*1024).join('A'); + +let script = SpecialPowers.loadChromeScript(url); +script.addMessageListener("file.opened", message => { + let blob = new Blob([data]); + + let form = new FormData(); + form.append("blob1", blob); + form.append("blob2", message.file); + form.append("blob3", blob); + + fetch("ok.sjs", { + method: "POST", + body: form, + }) + .then(r => r.text()) + .then(r => { + ok(parseInt(r, 10) > (data.length * 2), "We have data"); + }) + . then(SimpleTest.finish); +}); + +script.sendAsyncMessage("file.open"); +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/file/ipc/tests/test_ipcBlob_workers.html b/dom/file/ipc/tests/test_ipcBlob_workers.html new file mode 100644 index 0000000000..a473948f25 --- /dev/null +++ b/dom/file/ipc/tests/test_ipcBlob_workers.html @@ -0,0 +1,121 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test IPCBlob and Workers</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +function test_workerOwner() { + info("test_workerOwner"); + + function workerScript() { + onmessage = e => { + e.ports[0].onmessage = event => { + let reader = new FileReader(); + reader.readAsText(event.data); + reader.onloadend = () => { + let status = reader.result == 'hello world'; + postMessage(status); + } + } + } + } + + let mc = new MessageChannel(); + mc.port1.postMessage(new Blob(['hello world'])); + + let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"])); + let worker = new Worker(workerUrl); + + worker.postMessage("", [mc.port2]); + worker.onmessage = event => { + ok(event.data, "All is done!"); + next(); + } +} + +function test_workerToMainThread() { + info("test_workerToMainThread"); + function workerScript() { + onmessage = e => { + e.ports[0].onmessage = event => { + postMessage(event.data); + } + } + } + + let mc = new MessageChannel(); + mc.port1.postMessage(new Blob(['hello world'])); + + let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"])); + let worker = new Worker(workerUrl); + + worker.postMessage("", [mc.port2]); + worker.onmessage = event => { + info("Blob received back, terminate the worker and force GC"); + worker.terminate(); + worker = null; + SpecialPowers.forceGC(); + + var fr = new FileReader(); + fr.readAsText(event.data); + fr.onloadend = () => { + is(fr.result, "hello world", "Data matches"); + next(); + } + } +} + +function test_workerOwnerPlusFileReaderSync() { + info("test_workerOwnerPlusFileReaderSync"); + + function workerScript() { + onmessage = e => { + e.ports[0].onmessage = event => { + let reader = new FileReaderSync(); + let status = reader.readAsText(event.data) == 'hello world'; + postMessage(status); + } + } + } + + let mc = new MessageChannel(); + mc.port1.postMessage(new Blob(['hello world'])); + + let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"])); + let worker = new Worker(workerUrl); + + worker.postMessage("", [mc.port2]); + worker.onmessage = event => { + ok(event.data, "All is done!"); + next(); + } +} + +var tests = [ + test_workerOwner, + test_workerToMainThread, + test_workerOwnerPlusFileReaderSync, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +next(); + +</script> +</pre> +</body> +</html> diff --git a/dom/file/moz.build b/dom/file/moz.build new file mode 100644 index 0000000000..2a5831454b --- /dev/null +++ b/dom/file/moz.build @@ -0,0 +1,62 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: File") + +DIRS += ["ipc", "uri"] + +EXPORTS.mozilla.dom += [ + "BaseBlobImpl.h", + "Blob.h", + "BlobImpl.h", + "BlobSet.h", + "EmptyBlobImpl.h", + "File.h", + "FileBlobImpl.h", + "FileCreatorHelper.h", + "FileList.h", + "FileReader.h", + "FileReaderSync.h", + "MemoryBlobImpl.h", + "MultipartBlobImpl.h", + "MutableBlobStorage.h", + "MutableBlobStreamListener.h", + "StreamBlobImpl.h", +] + +UNIFIED_SOURCES += [ + "BaseBlobImpl.cpp", + "Blob.cpp", + "BlobImpl.cpp", + "BlobSet.cpp", + "EmptyBlobImpl.cpp", + "File.cpp", + "FileBlobImpl.cpp", + "FileCreatorHelper.cpp", + "FileList.cpp", + "FileReader.cpp", + "FileReaderSync.cpp", + "MemoryBlobImpl.cpp", + "MultipartBlobImpl.cpp", + "MutableBlobStorage.cpp", + "MutableBlobStreamListener.cpp", + "StreamBlobImpl.cpp", + "StringBlobImpl.cpp", + "TemporaryFileBlobImpl.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/file/ipc", +] + +MOCHITEST_MANIFESTS += ["tests/mochitest.ini"] + +XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell.ini"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/file/tests/common_blob.js b/dom/file/tests/common_blob.js new file mode 100644 index 0000000000..261909af0d --- /dev/null +++ b/dom/file/tests/common_blob.js @@ -0,0 +1,395 @@ +const RANGE_1 = 1; +const RANGE_2 = 2; + +function testBlob(file, contents, testName) { + // Load file using FileReader + return ( + new Promise(resolve => { + let r = new FileReader(); + r.onload = event => { + is( + event.target.readyState, + FileReader.DONE, + "[FileReader] readyState in test FileReader.readAsBinaryString of " + + testName + ); + is( + event.target.error, + null, + "[FileReader] no error in test FileReader.readAsBinaryString of " + + testName + ); + // Do not use |is(event.target.result, contents, "...");| that may output raw binary data. + is( + event.target.result.length, + contents.length, + "[FileReader] Length of result in test FileReader.readAsBinaryString of " + + testName + ); + ok( + event.target.result == contents, + "[FileReader] Content of result in test FileReader.readAsBinaryString of " + + testName + ); + is( + event.lengthComputable, + true, + "[FileReader] lengthComputable in test FileReader.readAsBinaryString of " + + testName + ); + is( + event.loaded, + contents.length, + "[FileReader] Loaded length in test FileReader.readAsBinaryString of " + + testName + ); + is( + event.total, + contents.length, + "[FileReader] Total length in test FileReader.readAsBinaryString of " + + testName + ); + resolve(); + }; + r.readAsBinaryString(file); + }) + + // Load file using URL.createObjectURL and XMLHttpRequest + .then(() => { + return new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.open("GET", URL.createObjectURL(file)); + xhr.onload = event => { + XHRLoadHandler( + event, + resolve, + contents, + "XMLHttpRequest load of " + testName + ); + }; + xhr.overrideMimeType("text/plain; charset=x-user-defined"); + xhr.send(); + }); + }) + + // Send file to server using FormData and XMLHttpRequest + .then(() => { + return new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.onload = function (event) { + checkMPSubmission(JSON.parse(event.target.responseText), [ + { name: "hello", value: "world" }, + { + name: "myfile", + value: contents, + fileName: file.name || "blob", + contentType: file.type || "application/octet-stream", + }, + ]); + resolve(); + }; + xhr.open("POST", "../../../dom/html/test/form_submit_server.sjs"); + + let fd = new FormData(); + fd.append("hello", "world"); + fd.append("myfile", file); + + xhr.send(fd); + }); + }) + + // Send file to server using plain XMLHttpRequest + .then(() => { + return new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.open("POST", "../../../dom/xhr/tests/file_XHRSendData.sjs"); + + xhr.onload = function (event) { + is( + event.target.getResponseHeader("Result-Content-Type"), + file.type ? file.type : null, + "request content-type in XMLHttpRequest send of " + testName + ); + is( + event.target.getResponseHeader("Result-Content-Length"), + String(file.size), + "request content-length in XMLHttpRequest send of " + testName + ); + }; + + xhr.addEventListener("load", event => { + XHRLoadHandler( + event, + resolve, + contents, + "XMLHttpRequest send of " + testName + ); + }); + xhr.overrideMimeType("text/plain; charset=x-user-defined"); + xhr.send(file); + }); + }) + ); +} + +function testSlice(file, size, type, contents, fileType, range) { + is(file.type, type, fileType + " file is correct type"); + is(file.size, size, fileType + " file is correct size"); + if (fileType == "fileFile") { + ok(file instanceof File, fileType + " file is a File"); + } else if (fileType == "memFile") { + ok(!(file instanceof File), fileType + " file is not a File"); + } + ok(file instanceof Blob, fileType + " file is also a Blob"); + + let slice = file.slice(0, size); + ok(slice instanceof Blob, fileType + " fullsize slice is a Blob"); + ok(!(slice instanceof File), fileType + " fullsize slice is not a File"); + + slice = file.slice(0, 1234); + ok(slice instanceof Blob, fileType + " sized slice is a Blob"); + ok(!(slice instanceof File), fileType + " sized slice is not a File"); + + slice = file.slice(0, size, "foo/bar"); + is(slice.type, "foo/bar", fileType + " fullsize slice foo/bar type"); + + slice = file.slice(0, 5432, "foo/bar"); + is(slice.type, "foo/bar", fileType + " sized slice foo/bar type"); + + is(slice.slice(0, 10).type, "", fileType + " slice-slice type"); + is(slice.slice(0, 10).size, 10, fileType + " slice-slice size"); + is( + slice.slice(0, 10, "hello/world").type, + "hello/world", + fileType + " slice-slice hello/world type" + ); + is( + slice.slice(0, 10, "hello/world").size, + 10, + fileType + " slice-slice hello/world size" + ); + + // Start, end, expected size + var indexes_range_1 = [ + [0, size, size], + [0, 1234, 1234], + [size - 500, size, 500], + [size - 500, size + 500, 500], + [size + 500, size + 1500, 0], + [0, 0, 0], + [1000, 1000, 0], + [size, size, 0], + [undefined, undefined, size], + [0, undefined, size], + ]; + + var indexes_range_2 = [ + [100, undefined, size - 100], + [-100, undefined, 100], + [100, -100, size - 200], + [-size - 100, undefined, size], + [-2 * size - 100, 500, 500], + [0, -size - 100, 0], + [100, -size - 100, 0], + [50, -size + 100, 50], + [0, 33000, 33000], + [1000, 34000, 33000], + ]; + + let indexes; + if (range == RANGE_1) { + indexes = indexes_range_1; + } else if (range == RANGE_2) { + indexes = indexes_range_2; + } else { + throw "Invalid range!"; + } + + function runNextTest() { + if (!indexes.length) { + return Promise.resolve(true); + } + + let index = indexes.shift(); + + let sliceContents; + let testName; + if (index[0] == undefined) { + slice = file.slice(); + sliceContents = contents.slice(); + testName = fileType + " slice()"; + } else if (index[1] == undefined) { + slice = file.slice(index[0]); + sliceContents = contents.slice(index[0]); + testName = fileType + " slice(" + index[0] + ")"; + } else { + slice = file.slice(index[0], index[1]); + sliceContents = contents.slice(index[0], index[1]); + testName = fileType + " slice(" + index[0] + ", " + index[1] + ")"; + } + + is(slice.type, "", testName + " type"); + is(slice.size, index[2], testName + " size"); + is(sliceContents.length, index[2], testName + " data size"); + + return testBlob(slice, sliceContents, testName).then(runNextTest); + } + + return runNextTest() + .then(() => { + // Slice of slice + let sliceOfSlice = file.slice(0, 40000); + return testBlob( + sliceOfSlice.slice(5000, 42000), + contents.slice(5000, 40000), + "file slice slice" + ); + }) + .then(() => { + // ...of slice of slice + let sliceOfSlice = file + .slice(0, 40000) + .slice(5000, 42000) + .slice(400, 700); + SpecialPowers.gc(); + return testBlob( + sliceOfSlice, + contents.slice(5400, 5700), + "file slice slice slice" + ); + }); +} + +function convertXHRBinary(s) { + let res = ""; + for (let i = 0; i < s.length; ++i) { + res += String.fromCharCode(s.charCodeAt(i) & 255); + } + return res; +} + +function XHRLoadHandler(event, resolve, contents, testName) { + is(event.target.readyState, 4, "[XHR] readyState in test " + testName); + is(event.target.status, 200, "[XHR] no error in test " + testName); + // Do not use |is(convertXHRBinary(event.target.responseText), contents, "...");| that may output raw binary data. + let convertedData = convertXHRBinary(event.target.responseText); + is( + convertedData.length, + contents.length, + "[XHR] Length of result in test " + testName + ); + ok(convertedData == contents, "[XHR] Content of result in test " + testName); + is( + event.lengthComputable, + event.total != 0, + "[XHR] lengthComputable in test " + testName + ); + is(event.loaded, contents.length, "[XHR] Loaded length in test " + testName); + is(event.total, contents.length, "[XHR] Total length in test " + testName); + resolve(); +} + +function checkMPSubmission(sub, expected) { + function getPropCount(o) { + let x, + l = 0; + for (x in o) { + ++l; + } + return l; + } + + is(sub.length, expected.length, "Correct number of items"); + let i; + for (i = 0; i < expected.length; ++i) { + if (!("fileName" in expected[i])) { + is( + sub[i].headers["Content-Disposition"], + 'form-data; name="' + expected[i].name + '"', + "Correct name (A)" + ); + is(getPropCount(sub[i].headers), 1, "Wrong number of headers (A)"); + } else { + is( + sub[i].headers["Content-Disposition"], + 'form-data; name="' + + expected[i].name + + '"; filename="' + + expected[i].fileName + + '"', + "Correct name (B)" + ); + is( + sub[i].headers["Content-Type"], + expected[i].contentType, + "Correct content type (B)" + ); + is(getPropCount(sub[i].headers), 2, "Wrong number of headers (B)"); + } + // Do not use |is(sub[i].body, expected[i].value, "...");| that may output raw binary data. + is(sub[i].body.length, expected[i].value.length, "Length of correct value"); + ok(sub[i].body == expected[i].value, "Content of correct value"); + } +} + +function createCanvasURL() { + return new Promise(resolve => { + // Create a decent-sized image + let cx = $("canvas").getContext("2d"); + let s = cx.canvas.width; + let grad = cx.createLinearGradient(0, 0, s - 1, s - 1); + for (i = 0; i < 0.95; i += 0.1) { + grad.addColorStop(i, "white"); + grad.addColorStop(i + 0.05, "black"); + } + grad.addColorStop(1, "white"); + cx.fillStyle = grad; + cx.fillRect(0, 0, s - 1, s - 1); + cx.fillStyle = "rgba(200, 0, 0, 0.9)"; + cx.fillRect(0.1 * s, 0.1 * s, 0.7 * s, 0.7 * s); + cx.strokeStyle = "rgba(0, 0, 130, 0.5)"; + cx.lineWidth = 0.14 * s; + cx.beginPath(); + cx.arc(0.6 * s, 0.6 * s, 0.3 * s, 0, Math.PI * 2, true); + cx.stroke(); + cx.closePath(); + cx.fillStyle = "rgb(0, 255, 0)"; + cx.beginPath(); + cx.arc(0.1 * s, 0.8 * s, 0.1 * s, 0, Math.PI * 2, true); + cx.fill(); + cx.closePath(); + + let data = atob( + cx.canvas + .toDataURL("image/png") + .substring("data:text/png;base64,".length + 1) + ); + + // This might fail if we dramatically improve the png encoder. If that happens + // please increase the complexity or size of the image generated above to ensure + // that we're testing with files that are large enough. + ok(data.length > 65536, "test data sufficiently large"); + + resolve(data); + }); +} + +function createFile(data, name) { + return new Promise(resolve => { + SpecialPowers.createFiles([{ name, data }], files => { + resolve(files[0]); + }); + }); +} + +function toBlobPromise(canvas) { + function BlobListener(callback, file) { + var reader = new FileReader(); + reader.onload = () => callback(file); + reader.readAsDataURL(file); + } + + return new Promise(resolve => { + canvas.toBlob(BlobListener.bind(undefined, resolve)); + }); +} diff --git a/dom/file/tests/common_blob_reading.js b/dom/file/tests/common_blob_reading.js new file mode 100644 index 0000000000..5df8419a30 --- /dev/null +++ b/dom/file/tests/common_blob_reading.js @@ -0,0 +1,50 @@ +async function testBlobText(blob, content) { + let text = await blob.text(); + is(text, content, "blob.text()"); +} + +async function testBlobArrayBuffer(blob, content) { + let ab = await blob.arrayBuffer(); + is(ab.byteLength, content.length, "blob.arrayBuffer()"); +} + +async function testBlobStream(blob, content) { + let s = await blob.stream(); + ok(s instanceof ReadableStream, "We have a ReadableStream"); + + let data = await s.getReader().read(); + ok(!data.done, "Nothing is done yet"); + for (let i = 0; i < data.value.length; ++i) { + is(String.fromCharCode(data.value[i]), content[i], "blob.stream() - " + i); + } +} + +function workify(func, blob, content) { + info("Workifying " + func); + + return new Promise((resolve, reject) => { + let worker = new Worker("worker_blob_reading.js"); + worker.postMessage({ func, blob, content }); + worker.onmessage = function (e) { + if (e.data.type == "done") { + resolve(); + return; + } + + if (e.data.type == "error") { + reject(e.data.message); + return; + } + + if (e.data.type == "test") { + ok(e.data.test, e.data.message); + return; + } + + if (e.data.type == "info") { + info(e.data.message); + return; + } + }; + }); +} diff --git a/dom/file/tests/common_blob_types.js b/dom/file/tests/common_blob_types.js new file mode 100644 index 0000000000..95501e58e5 --- /dev/null +++ b/dom/file/tests/common_blob_types.js @@ -0,0 +1,82 @@ +let blobTypes = [ + { + type: "memory", + factory: async content => { + return new Blob([content]); + }, + blobImplType: "MultipartBlobImpl[StringBlobImpl]", + }, + + { + type: "ipcBlob", + factory: async content => { + return new Promise(resolve => { + let bc1 = new BroadcastChannel("blob tests"); + bc1.onmessage = e => { + resolve(e.data); + }; + + let bc2 = new BroadcastChannel("blob tests"); + bc2.postMessage(new Blob([content])); + }); + }, + blobImplType: + "StreamBlobImpl[StreamBlobImpl[MultipartBlobImpl[StringBlobImpl]]]", + }, + + { + type: "memoryBlob", + factory: async content => { + return new Promise(resolve => { + var xhr = new XMLHttpRequest(); + xhr.open( + "POST", + "http://mochi.test:8888/browser/dom/xhr/tests/temporaryFileBlob.sjs" + ); + xhr.responseType = "blob"; + xhr.send(content); + xhr.onloadend = _ => { + resolve(xhr.response); + }; + }); + }, + blobImplType: "MemoryBlobImpl", + }, + + { + type: "temporaryBlob", + factory: async content => { + await SpecialPowers.pushPrefEnv({ + set: [["dom.blob.memoryToTemporaryFile", 1]], + }); + + return new Promise(resolve => { + var xhr = new XMLHttpRequest(); + xhr.open( + "POST", + "http://mochi.test:8888/browser/dom/xhr/tests/temporaryFileBlob.sjs" + ); + xhr.responseType = "blob"; + xhr.send(content); + xhr.onloadend = _ => { + resolve(xhr.response); + }; + }); + }, + blobImplType: "StreamBlobImpl[TemporaryFileBlobImpl]", + }, +]; + +async function forEachBlobType(content, cb) { + for (let i = 0; i < blobTypes.length; ++i) { + info("Running tests for " + blobTypes[i].type); + let blob = await blobTypes[i].factory(content); + is( + SpecialPowers.wrap(blob).blobImplType, + blobTypes[i].blobImplType, + "Correct blobImplType" + ); + ok(blob instanceof Blob, "Blob created"); + await cb(blob, content); + } +} diff --git a/dom/file/tests/common_fileReader.js b/dom/file/tests/common_fileReader.js new file mode 100644 index 0000000000..9ee6a32b93 --- /dev/null +++ b/dom/file/tests/common_fileReader.js @@ -0,0 +1,848 @@ +function test_setup() { + return new Promise(resolve => { + const minFileSize = 20000; + + // Create strings containing data we'll test with. We'll want long + // strings to ensure they span multiple buffers while loading + let testTextData = "asd b\tlah\u1234w\u00a0r"; + while (testTextData.length < minFileSize) { + testTextData = testTextData + testTextData; + } + + let testASCIIData = "abcdef 123456\n"; + while (testASCIIData.length < minFileSize) { + testASCIIData = testASCIIData + testASCIIData; + } + + let testBinaryData = ""; + for (let i = 0; i < 256; i++) { + testBinaryData += String.fromCharCode(i); + } + while (testBinaryData.length < minFileSize) { + testBinaryData = testBinaryData + testBinaryData; + } + + let dataurldata0 = testBinaryData.substr( + 0, + testBinaryData.length - (testBinaryData.length % 3) + ); + let dataurldata1 = testBinaryData.substr( + 0, + testBinaryData.length - 2 - (testBinaryData.length % 3) + ); + let dataurldata2 = testBinaryData.substr( + 0, + testBinaryData.length - 1 - (testBinaryData.length % 3) + ); + + //Set up files for testing + let openerURL = SimpleTest.getTestFileURL("fileapi_chromeScript.js"); + let opener = SpecialPowers.loadChromeScript(openerURL); + + opener.addMessageListener("files.opened", message => { + let [ + asciiFile, + binaryFile, + nonExistingFile, + utf8TextFile, + utf16TextFile, + emptyFile, + dataUrlFile0, + dataUrlFile1, + dataUrlFile2, + ] = message; + + resolve({ + blobs: { + asciiFile, + binaryFile, + nonExistingFile, + utf8TextFile, + utf16TextFile, + emptyFile, + dataUrlFile0, + dataUrlFile1, + dataUrlFile2, + }, + data: { + text: testTextData, + ascii: testASCIIData, + binary: testBinaryData, + url0: dataurldata0, + url1: dataurldata1, + url2: dataurldata2, + }, + }); + }); + + opener.sendAsyncMessage("files.open", [ + testASCIIData, + testBinaryData, + null, + convertToUTF8(testTextData), + convertToUTF16(testTextData), + "", + dataurldata0, + dataurldata1, + dataurldata2, + ]); + }); +} + +function runBasicTests(data) { + return test_basic() + .then(() => { + return test_readAsText(data.blobs.asciiFile, data.data.ascii); + }) + .then(() => { + return test_readAsBinaryString(data.blobs.binaryFile, data.data.binary); + }) + .then(() => { + return test_readAsArrayBuffer(data.blobs.binaryFile, data.data.binary); + }); +} + +function runEncodingTests(data) { + return test_readAsTextWithEncoding( + data.blobs.asciiFile, + data.data.ascii, + data.data.ascii.length, + "" + ) + .then(() => { + return test_readAsTextWithEncoding( + data.blobs.asciiFile, + data.data.ascii, + data.data.ascii.length, + "iso8859-1" + ); + }) + .then(() => { + return test_readAsTextWithEncoding( + data.blobs.utf8TextFile, + data.data.text, + convertToUTF8(data.data.text).length, + "utf8" + ); + }) + .then(() => { + return test_readAsTextWithEncoding( + data.blobs.utf16TextFile, + data.data.text, + convertToUTF16(data.data.text).length, + "utf-16" + ); + }) + .then(() => { + return test_readAsTextWithEncoding(data.blobs.emptyFile, "", 0, ""); + }) + .then(() => { + return test_readAsTextWithEncoding(data.blobs.emptyFile, "", 0, "utf8"); + }) + .then(() => { + return test_readAsTextWithEncoding(data.blobs.emptyFile, "", 0, "utf-16"); + }); +} + +function runEmptyTests(data) { + return test_onlyResult() + .then(() => { + return test_readAsText(data.blobs.emptyFile, ""); + }) + .then(() => { + return test_readAsBinaryString(data.blobs.emptyFile, ""); + }) + .then(() => { + return test_readAsArrayBuffer(data.blobs.emptyFile, ""); + }) + .then(() => { + return test_readAsDataURL(data.blobs.emptyFile, convertToDataURL(""), 0); + }); +} + +function runTwiceTests(data) { + return test_readAsTextTwice(data.blobs.asciiFile, data.data.ascii) + .then(() => { + return test_readAsBinaryStringTwice( + data.blobs.binaryFile, + data.data.binary + ); + }) + .then(() => { + return test_readAsDataURLTwice( + data.blobs.binaryFile, + convertToDataURL(data.data.binary), + data.data.binary.length + ); + }) + .then(() => { + return test_readAsArrayBufferTwice( + data.blobs.binaryFile, + data.data.binary + ); + }) + .then(() => { + return test_readAsArrayBufferTwice2( + data.blobs.binaryFile, + data.data.binary + ); + }); +} + +function runOtherTests(data) { + return test_readAsDataURL_customLength( + data.blobs.dataUrlFile0, + convertToDataURL(data.data.url0), + data.data.url0.length, + 0 + ) + .then(() => { + return test_readAsDataURL_customLength( + data.blobs.dataUrlFile1, + convertToDataURL(data.data.url1), + data.data.url1.length, + 1 + ); + }) + .then(() => { + return test_readAsDataURL_customLength( + data.blobs.dataUrlFile2, + convertToDataURL(data.data.url2), + data.data.url2.length, + 2 + ); + }) + .then(() => { + return test_abort(data.blobs.asciiFile); + }) + .then(() => { + return test_abort_readAsX(data.blobs.asciiFile, data.data.ascii); + }) + .then(() => { + return test_nonExisting(data.blobs.nonExistingFile); + }); +} + +function convertToUTF16(s) { + let res = ""; + for (let i = 0; i < s.length; ++i) { + c = s.charCodeAt(i); + res += String.fromCharCode(c & 255, c >>> 8); + } + return res; +} + +function convertToUTF8(s) { + return unescape(encodeURIComponent(s)); +} + +function convertToDataURL(s) { + return "data:application/octet-stream;base64," + btoa(s); +} + +function loadEventHandler_string( + event, + resolve, + reader, + data, + dataLength, + testName +) { + is(event.target, reader, "Correct target."); + is( + event.target.readyState, + FileReader.DONE, + "readyState in test " + testName + ); + is(event.target.error, null, "no error in test " + testName); + is(event.target.result, data, "result in test " + testName); + is(event.lengthComputable, true, "lengthComputable in test " + testName); + is(event.loaded, dataLength, "loaded in test " + testName); + is(event.total, dataLength, "total in test " + testName); + resolve(); +} + +function loadEventHandler_arrayBuffer(event, resolve, reader, data, testName) { + is( + event.target.readyState, + FileReader.DONE, + "readyState in test " + testName + ); + is(event.target.error, null, "no error in test " + testName); + is(event.lengthComputable, true, "lengthComputable in test " + testName); + is(event.loaded, data.length, "loaded in test " + testName); + is(event.total, data.length, "total in test " + testName); + is( + event.target.result.byteLength, + data.length, + "array buffer size in test " + testName + ); + + let u8v = new Uint8Array(event.target.result); + is( + String.fromCharCode.apply(String, u8v), + data, + "array buffer contents in test " + testName + ); + u8v = null; + + if ("SpecialPowers" in self) { + SpecialPowers.gc(); + + is( + event.target.result.byteLength, + data.length, + "array buffer size after gc in test " + testName + ); + u8v = new Uint8Array(event.target.result); + is( + String.fromCharCode.apply(String, u8v), + data, + "array buffer contents after gc in test " + testName + ); + } + + resolve(); +} + +function test_basic() { + return new Promise(resolve => { + is(FileReader.EMPTY, 0, "correct EMPTY value"); + is(FileReader.LOADING, 1, "correct LOADING value"); + is(FileReader.DONE, 2, "correct DONE value"); + resolve(); + }); +} + +function test_readAsText(blob, text) { + return new Promise(resolve => { + let onloadHasRun = false; + let onloadStartHasRun = false; + + let r = new FileReader(); + is(r.readyState, FileReader.EMPTY, "correct initial text readyState"); + + r.onload = event => { + loadEventHandler_string( + event, + resolve, + r, + text, + text.length, + "readAsText" + ); + }; + + r.addEventListener("load", () => { + onloadHasRun = true; + }); + r.addEventListener("loadstart", () => { + onloadStartHasRun = true; + }); + + r.readAsText(blob); + + is(r.readyState, FileReader.LOADING, "correct loading text readyState"); + is(onloadHasRun, false, "text loading must be async"); + is(onloadStartHasRun, false, "text loadstart should fire async"); + }); +} + +function test_readAsBinaryString(blob, text) { + return new Promise(resolve => { + let onloadHasRun = false; + let onloadStartHasRun = false; + + let r = new FileReader(); + is(r.readyState, FileReader.EMPTY, "correct initial binary readyState"); + + r.addEventListener("load", function () { + onloadHasRun = true; + }); + r.addEventListener("loadstart", function () { + onloadStartHasRun = true; + }); + + r.readAsBinaryString(blob); + + r.onload = event => { + loadEventHandler_string( + event, + resolve, + r, + text, + text.length, + "readAsBinaryString" + ); + }; + + is(r.readyState, FileReader.LOADING, "correct loading binary readyState"); + is(onloadHasRun, false, "binary loading must be async"); + is(onloadStartHasRun, false, "binary loadstart should fire async"); + }); +} + +function test_readAsArrayBuffer(blob, text) { + return new Promise(resolve => { + let onloadHasRun = false; + let onloadStartHasRun = false; + + r = new FileReader(); + is( + r.readyState, + FileReader.EMPTY, + "correct initial arrayBuffer readyState" + ); + + r.addEventListener("load", function () { + onloadHasRun = true; + }); + r.addEventListener("loadstart", function () { + onloadStartHasRun = true; + }); + + r.readAsArrayBuffer(blob); + + r.onload = event => { + loadEventHandler_arrayBuffer( + event, + resolve, + r, + text, + "readAsArrayBuffer" + ); + }; + + is( + r.readyState, + FileReader.LOADING, + "correct loading arrayBuffer readyState" + ); + is(onloadHasRun, false, "arrayBuffer loading must be async"); + is(onloadStartHasRun, false, "arrayBuffer loadstart should fire sync"); + }); +} + +// Test a variety of encodings, and make sure they work properly +function test_readAsTextWithEncoding(blob, text, length, charset) { + return new Promise(resolve => { + let r = new FileReader(); + r.onload = event => { + loadEventHandler_string( + event, + resolve, + r, + text, + length, + "readAsText-" + charset + ); + }; + r.readAsText(blob, charset); + }); +} + +// Test get result without reading +function test_onlyResult() { + return new Promise(resolve => { + let r = new FileReader(); + is( + r.readyState, + FileReader.EMPTY, + "readyState in test reader get result without reading" + ); + is(r.error, null, "no error in test reader get result without reading"); + is(r.result, null, "result in test reader get result without reading"); + resolve(); + }); +} + +function test_readAsDataURL(blob, text, length) { + return new Promise(resolve => { + let r = new FileReader(); + r.onload = event => { + loadEventHandler_string(event, resolve, r, text, length, "readAsDataURL"); + }; + r.readAsDataURL(blob); + }); +} + +// Test reusing a FileReader to read multiple times +function test_readAsTextTwice(blob, text) { + return new Promise(resolve => { + let r = new FileReader(); + r.onload = event => { + loadEventHandler_string( + event, + () => {}, + r, + text, + text.length, + "readAsText-reused-once" + ); + }; + + let anotherListener = event => { + let r1 = event.target; + r1.removeEventListener("load", anotherListener); + r1.onload = evt => { + loadEventHandler_string( + evt, + resolve, + r1, + text, + text.length, + "readAsText-reused-twice" + ); + }; + r1.readAsText(blob); + }; + + r.addEventListener("load", anotherListener); + r.readAsText(blob); + }); +} + +// Test reusing a FileReader to read multiple times +function test_readAsBinaryStringTwice(blob, text) { + return new Promise(resolve => { + let r = new FileReader(); + r.onload = event => { + loadEventHandler_string( + event, + () => {}, + r, + text, + text.length, + "readAsBinaryString-reused-once" + ); + }; + + let anotherListener = event => { + let r1 = event.target; + r1.removeEventListener("load", anotherListener); + r1.onload = evt => { + loadEventHandler_string( + evt, + resolve, + r1, + text, + text.length, + "readAsBinaryString-reused-twice" + ); + }; + r1.readAsBinaryString(blob); + }; + + r.addEventListener("load", anotherListener); + r.readAsBinaryString(blob); + }); +} + +function test_readAsDataURLTwice(blob, text, length) { + return new Promise(resolve => { + let r = new FileReader(); + r.onload = event => { + loadEventHandler_string( + event, + () => {}, + r, + text, + length, + "readAsDataURL-reused-once" + ); + }; + + let anotherListener = event => { + let r1 = event.target; + r1.removeEventListener("load", anotherListener); + r1.onload = evt => { + loadEventHandler_string( + evt, + resolve, + r1, + text, + length, + "readAsDataURL-reused-twice" + ); + }; + r1.readAsDataURL(blob); + }; + + r.addEventListener("load", anotherListener); + r.readAsDataURL(blob); + }); +} + +function test_readAsArrayBufferTwice(blob, text) { + return new Promise(resolve => { + let r = new FileReader(); + r.onload = event => { + loadEventHandler_arrayBuffer( + event, + () => {}, + r, + text, + "readAsArrayBuffer-reused-once" + ); + }; + + let anotherListener = event => { + let r1 = event.target; + r1.removeEventListener("load", anotherListener); + r1.onload = evt => { + loadEventHandler_arrayBuffer( + evt, + resolve, + r1, + text, + "readAsArrayBuffer-reused-twice" + ); + }; + r1.readAsArrayBuffer(blob); + }; + + r.addEventListener("load", anotherListener); + r.readAsArrayBuffer(blob); + }); +} + +// Test first reading as ArrayBuffer then read as something else (BinaryString) +// and doesn't crash +function test_readAsArrayBufferTwice2(blob, text) { + return new Promise(resolve => { + let r = new FileReader(); + r.onload = event => { + loadEventHandler_arrayBuffer( + event, + () => {}, + r, + text, + "readAsArrayBuffer-reused-once2" + ); + }; + + let anotherListener = event => { + let r1 = event.target; + r1.removeEventListener("load", anotherListener); + r1.onload = evt => { + loadEventHandler_string( + evt, + resolve, + r1, + text, + text.length, + "readAsArrayBuffer-reused-twice2" + ); + }; + r1.readAsBinaryString(blob); + }; + + r.addEventListener("load", anotherListener); + r.readAsArrayBuffer(blob); + }); +} + +function test_readAsDataURL_customLength(blob, text, length, numb) { + return new Promise(resolve => { + is(length % 3, numb, "Want to test data with length %3 == " + numb); + let r = new FileReader(); + r.onload = event => { + loadEventHandler_string( + event, + resolve, + r, + text, + length, + "dataurl reading, %3 = " + numb + ); + }; + r.readAsDataURL(blob); + }); +} + +// Test abort() +function test_abort(blob) { + return new Promise(resolve => { + let abortHasRun = false; + let loadEndHasRun = false; + + let r = new FileReader(); + + r.onabort = function (event) { + is(abortHasRun, false, "abort should only fire once"); + is(loadEndHasRun, false, "loadend shouldn't have fired yet"); + abortHasRun = true; + is( + event.target.readyState, + FileReader.DONE, + "should be DONE while firing onabort" + ); + is( + event.target.error.name, + "AbortError", + "error set to AbortError for aborted reads" + ); + is( + event.target.result, + null, + "file data should be null on aborted reads" + ); + }; + + r.onloadend = function (event) { + is(abortHasRun, true, "abort should fire before loadend"); + is(loadEndHasRun, false, "loadend should only fire once"); + loadEndHasRun = true; + is( + event.target.readyState, + FileReader.DONE, + "should be DONE while firing onabort" + ); + is( + event.target.error.name, + "AbortError", + "error set to AbortError for aborted reads" + ); + is( + event.target.result, + null, + "file data should be null on aborted reads" + ); + }; + + r.onload = function () { + ok(false, "load should not fire for aborted reads"); + }; + r.onerror = function () { + ok(false, "error should not fire for aborted reads"); + }; + r.onprogress = function () { + ok(false, "progress should not fire for aborted reads"); + }; + + let abortThrew = false; + try { + r.abort(); + } catch (e) { + abortThrew = true; + } + + is(abortThrew, false, "abort() doesn't throw"); + is(abortHasRun, false, "abort() is a no-op unless loading"); + + r.readAsText(blob); + r.abort(); + + is(abortHasRun, true, "abort should fire sync"); + is(loadEndHasRun, true, "loadend should fire sync"); + + resolve(); + }); +} + +// Test calling readAsX to cause abort() +function test_abort_readAsX(blob, text) { + return new Promise(resolve => { + let reuseAbortHasRun = false; + + let r = new FileReader(); + r.onabort = function (event) { + is(reuseAbortHasRun, false, "abort should only fire once"); + reuseAbortHasRun = true; + is( + event.target.readyState, + FileReader.DONE, + "should be DONE while firing onabort" + ); + is( + event.target.error.name, + "AbortError", + "error set to AbortError for aborted reads" + ); + is( + event.target.result, + null, + "file data should be null on aborted reads" + ); + }; + r.onload = function () { + ok(false, "load should fire for nested reads"); + }; + + let abortThrew = false; + try { + r.abort(); + } catch (e) { + abortThrew = true; + } + + is(abortThrew, false, "abort() should not throw"); + is(reuseAbortHasRun, false, "abort() is a no-op unless loading"); + r.readAsText(blob); + + let readThrew = false; + try { + r.readAsText(blob); + } catch (e) { + readThrew = true; + } + + is(readThrew, true, "readAsText() must throw if loading"); + is(reuseAbortHasRun, false, "abort should not fire"); + + r.onload = event => { + loadEventHandler_string( + event, + resolve, + r, + text, + text.length, + "reuse-as-abort reading" + ); + }; + }); +} + +// Test reading from nonexistent files +function test_nonExisting(blob) { + return new Promise(resolve => { + let r = new FileReader(); + + r.onerror = function (event) { + is( + event.target.readyState, + FileReader.DONE, + "should be DONE while firing onerror" + ); + is( + event.target.error.name, + "NotFoundError", + "error set to NotFoundError for nonexistent files" + ); + is( + event.target.result, + null, + "file data should be null on aborted reads" + ); + resolve(); + }; + r.onload = function (event) { + is(false, "nonexistent file shouldn't load! (FIXME: bug 1122788)"); + }; + + let didThrow = false; + try { + r.readAsDataURL(blob); + } catch (ex) { + didThrow = true; + } + + // Once this test passes, we should test that onerror gets called and + // that the FileReader object is in the right state during that call. + is( + didThrow, + false, + "shouldn't throw when opening nonexistent file, should fire error instead" + ); + }); +} diff --git a/dom/file/tests/crashtests/1480354.html b/dom/file/tests/crashtests/1480354.html new file mode 100644 index 0000000000..19e53bb1ca --- /dev/null +++ b/dom/file/tests/crashtests/1480354.html @@ -0,0 +1,14 @@ +<html> +<body> + <script> +function createBlob(blocksize) { + var blob = new Blob(); + while (blob.size < 25 * 1024 * 1024) { // 25 MB + blob = new Blob([blob, new Uint8Array(blocksize)]); + } + URL.createObjectURL(blob); +} +createBlob(1024 * 25); + </script> +</body> +</html> diff --git a/dom/file/tests/crashtests/1562891.html b/dom/file/tests/crashtests/1562891.html new file mode 100644 index 0000000000..fff7606a8a --- /dev/null +++ b/dom/file/tests/crashtests/1562891.html @@ -0,0 +1,16 @@ +<html> +<head> + <script> + function start () { + const canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas') + SpecialPowers.forceGC(); + canvas.toBlob(function (blob) { + blob.stream() + blob.arrayBuffer().then(() => {}) + }) + } + + window.addEventListener('load', start) + </script> +</head> +</html> diff --git a/dom/file/tests/crashtests/1747185.html b/dom/file/tests/crashtests/1747185.html new file mode 100644 index 0000000000..89af55504f --- /dev/null +++ b/dom/file/tests/crashtests/1747185.html @@ -0,0 +1,11 @@ +<html> +<head> + <script> + window.addEventListener('load', () => { + let a = new Blob([new Uint8Array(2147483647)]) + let b = new File([a], '') + b.stream() + }) + </script> +</head> +</html> diff --git a/dom/file/tests/crashtests/1748342.html b/dom/file/tests/crashtests/1748342.html new file mode 100644 index 0000000000..5f0811b5bb --- /dev/null +++ b/dom/file/tests/crashtests/1748342.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> + <script id="worker1" type="javascript/worker"> + self.onmessage = async function (e) { + self.close() + const reader = new FileReader() + for (let i = 0; i < 25; i++) { + try { reader.readAsBinaryString(e.data[0]) } catch (e) {} + } + reader.addEventListener("progress", () => {}, {}) + } + </script> + <script> + window.addEventListener("load", () => { + const script = new Blob([document.querySelector("#worker1").textContent], { type: "text/javascript" }) + const worker = new Worker(window.URL.createObjectURL(script)) + const data = new Blob(["70\nℽ㮼٠\0𛃧كe۰҅妽𝅧𡴶𝌋쮁偵97\r𐇽0🥂.\b፟+⍳፟D3𣚽🐾�c_߰a<<=𝅦9𝆭𛰅9ௌΐ0�⡖‑뢈/-᭰*٠٪᷁e �‑걢V*=**\u2028שּׁ&0󠄯e\n𛰵𫍰,𝅥t\nl𧶈a𦜠09k䴋�|󠄷🦻𖭄", "*۰0\u2029/\n\r+𖼣k*=\r٪\r慑B�\r\r\n\"\r\\۹c卑4鴗ꛌ\0⌕:\r\n𝚨9ꛅ٠\nJ9\r゙鈷P\u2029҉۹e \b緁︡𤆥^゚|٫揅ᷛ𩊜s2凅9c8H𦰤-\f%٠𨮫‑2𫈮P𝋄窥57\n-゙҄H𣃂-ᷢשּׁ貌솽|𝉃c㙡᭯mL\r"], { + "type": "image/png", + "endings": "transparent" + }) + worker.postMessage([data], []) + }) + </script> +</head> +</html> diff --git a/dom/file/tests/crashtests/crashtests.list b/dom/file/tests/crashtests/crashtests.list new file mode 100644 index 0000000000..22eec1962e --- /dev/null +++ b/dom/file/tests/crashtests/crashtests.list @@ -0,0 +1,4 @@ +skip-if(ThreadSanitizer) load 1480354.html +load 1562891.html +skip-if(Android||ThreadSanitizer) load 1747185.html # Crashes on Android, times out on TSan. +load 1748342.html diff --git a/dom/file/tests/create_file_objects.js b/dom/file/tests/create_file_objects.js new file mode 100644 index 0000000000..cf016b239d --- /dev/null +++ b/dom/file/tests/create_file_objects.js @@ -0,0 +1,19 @@ +/* eslint-env mozilla/chrome-script */ + +Cu.importGlobalProperties(["File"]); + +addMessageListener("create-file-objects", function (message) { + let files = []; + let promises = []; + for (fileName of message.fileNames) { + promises.push( + File.createFromFileName(fileName).then(function (file) { + files.push(file); + }) + ); + } + + Promise.all(promises).then(function () { + sendAsyncMessage("created-file-objects", files); + }); +}); diff --git a/dom/file/tests/file_blobURL_expiring.html b/dom/file/tests/file_blobURL_expiring.html new file mode 100644 index 0000000000..a1ae725709 --- /dev/null +++ b/dom/file/tests/file_blobURL_expiring.html @@ -0,0 +1,4 @@ +<script> +var blob = new Blob([123]); +parent.postMessage(URL.createObjectURL(blob), "*"); +</script> diff --git a/dom/file/tests/file_mozfiledataurl_audio.ogg b/dom/file/tests/file_mozfiledataurl_audio.ogg Binary files differnew file mode 100644 index 0000000000..88b2c1b5b2 --- /dev/null +++ b/dom/file/tests/file_mozfiledataurl_audio.ogg diff --git a/dom/file/tests/file_mozfiledataurl_doc.html b/dom/file/tests/file_mozfiledataurl_doc.html new file mode 100644 index 0000000000..763b20a0f9 --- /dev/null +++ b/dom/file/tests/file_mozfiledataurl_doc.html @@ -0,0 +1,6 @@ +<!doctype html> +<html> +<body> +<p>This here is a document!</p> +<img id=img src="file_mozfiledataurl_img.jpg"> +</html> diff --git a/dom/file/tests/file_mozfiledataurl_img.jpg b/dom/file/tests/file_mozfiledataurl_img.jpg Binary files differnew file mode 100644 index 0000000000..dcd99b9670 --- /dev/null +++ b/dom/file/tests/file_mozfiledataurl_img.jpg diff --git a/dom/file/tests/file_mozfiledataurl_inner.html b/dom/file/tests/file_mozfiledataurl_inner.html new file mode 100644 index 0000000000..a2e539bef7 --- /dev/null +++ b/dom/file/tests/file_mozfiledataurl_inner.html @@ -0,0 +1,76 @@ +<!doctype html> +<html> +<script type="application/javascript"> +var img; +var audio; +var iframe; + +addEventListener("message", function(e) { + mess = JSON.parse(e.data); + + if ("img" in mess) + img.src = mess.img; + else if ("audio" in mess) + audio.src = mess.audio + else if ("iframe" in mess) + iframe.src = mess.iframe; + else if ("xhr" in mess) { + let xhr = new XMLHttpRequest(); + xhr.onerror = function() { + sendItUp({ didError: true }); + } + xhr.onload = function() { + sendItUp({ text: xhr.responseText }); + } + try { + xhr.open("GET", mess.xhr); + xhr.send(); + } + catch (ex) { + sendItUp({ didThrow: true }); + } + } + +}, false); + +function sendItUp(obj) { + window.parent.postMessage(JSON.stringify(obj), "*"); +} + +function audioNotifyParent(e) { + sendItUp({ type: e.type }); +} + +function imgNotifyParent(e) { + sendItUp({ type: e.type, + width: e.target.width, + height: e.target.height }); +} + +function iframeNotifyParent(e) { + res = { type: e.type }; + try { + res.text = e.target.contentDocument.getElementsByTagName("p")[0].textContent; + } catch (ex) {} + try { + res.imgWidth = e.target.contentDocument.getElementById("img").width; + } catch (ex) {} + + sendItUp(res); +} + +onload = function() { + img = document.getElementById('img'); + img.onerror = img.onload = imgNotifyParent; + iframe = document.getElementById('iframe'); + iframe.onerror = iframe.onload = iframeNotifyParent; + audio = document.getElementById('audio'); + audio.onerror = audio.onloadeddata = audioNotifyParent; +} + +</script> +<body> +<img id=img> +<audio id=audio> +<iframe id=iframe></iframe> +</html> diff --git a/dom/file/tests/file_mozfiledataurl_text.txt b/dom/file/tests/file_mozfiledataurl_text.txt new file mode 100644 index 0000000000..315338aa9b --- /dev/null +++ b/dom/file/tests/file_mozfiledataurl_text.txt @@ -0,0 +1 @@ +Yarr, here be plaintext file, ya landlubber diff --git a/dom/file/tests/file_nonascii_blob_url.html b/dom/file/tests/file_nonascii_blob_url.html new file mode 100644 index 0000000000..89183f4613 --- /dev/null +++ b/dom/file/tests/file_nonascii_blob_url.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test blob URL for non-ascii domain</title> +</head> +<body> + <p id="result"></p> + <script type="application/javascript"> + +window.onmessage = function(e) { + var blob = new Blob([e.data]); + var url = URL.createObjectURL(blob); + + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); + xhr.send(null); + + parent.postMessage(xhr.responseText, '*'); +} + + </script> +</body> +</html> diff --git a/dom/file/tests/fileapi_chromeScript.js b/dom/file/tests/fileapi_chromeScript.js new file mode 100644 index 0000000000..f94b01698b --- /dev/null +++ b/dom/file/tests/fileapi_chromeScript.js @@ -0,0 +1,54 @@ +/* eslint-env mozilla/chrome-script */ + +Cu.importGlobalProperties(["File"]); + +function createFileWithData(fileData) { + var willDelete = fileData === null; + + var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService( + Ci.nsIProperties + ); + + var testFile = dirSvc.get("ProfD", Ci.nsIFile); + testFile.append("fileAPItestfile"); + testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + var outStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outStream.init( + testFile, + 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, + 0 + ); + if (willDelete) { + fileData = "some irrelevant test data\n"; + } + outStream.write(fileData, fileData.length); + outStream.close(); + return File.createFromNsIFile(testFile).then(domFile => { + if (willDelete) { + testFile.remove(/* recursive: */ false); + } + + return domFile; + }); +} + +addMessageListener("files.open", function (message) { + let promises = []; + let list = []; + + for (let fileData of message) { + promises.push( + createFileWithData(fileData).then(domFile => { + list.push(domFile); + }) + ); + } + + Promise.all(promises).then(() => { + sendAsyncMessage("files.opened", list); + }); +}); diff --git a/dom/file/tests/mochitest.ini b/dom/file/tests/mochitest.ini new file mode 100644 index 0000000000..495c0263d8 --- /dev/null +++ b/dom/file/tests/mochitest.ini @@ -0,0 +1,54 @@ +[DEFAULT] +support-files = + common_blob.js + create_file_objects.js + common_fileReader.js + common_blob_types.js + file_blobURL_expiring.html + file_mozfiledataurl_img.jpg + file_mozfiledataurl_audio.ogg + file_mozfiledataurl_doc.html + file_mozfiledataurl_text.txt + file_mozfiledataurl_inner.html + file_nonascii_blob_url.html + fileapi_chromeScript.js + worker_fileReader.js + !/dom/html/test/form_submit_server.sjs + !/dom/xhr/tests/file_XHRSendData.sjs + !/dom/xhr/tests/temporaryFileBlob.sjs + +[test_blob_fragment_and_query.html] +[test_blobconstructor.html] +[test_blobURL_expiring.html] +[test_file_from_blob.html] +[test_nonascii_blob_url.html] +skip-if = + http3 +[test_file_negative_date.html] +[test_fileapi_basic.html] +[test_fileapi_encoding.html] +[test_fileapi_twice.html] +[test_fileapi_other.html] +[test_fileapi_basic_worker.html] +[test_fileapi_encoding_worker.html] +[test_fileapi_twice_worker.html] +[test_fileapi_other_worker.html] +[test_fileapi_slice_realFile_1.html] +[test_fileapi_slice_realFile_2.html] +skip-if = (verify && !debug && (os == 'win')) +[test_fileapi_slice_memFile_1.html] +[test_fileapi_slice_memFile_2.html] +[test_fileapi_slice_image.html] +[test_mozfiledataurl.html] +skip-if = + toolkit == 'android' #TIMED_OUT + http3 +[test_bug1507893.html] +support-files = worker_bug1507893.js +[test_bug1742540.html] +support-files = worker_bug1742540.js +skip-if = toolkit == 'android' #TIMED_OUT +[test_blob_reading.html] +support-files = common_blob_reading.js worker_blob_reading.js +skip-if = + http3 diff --git a/dom/file/tests/test_agentcluster_bloburl.js b/dom/file/tests/test_agentcluster_bloburl.js new file mode 100644 index 0000000000..2a577116a2 --- /dev/null +++ b/dom/file/tests/test_agentcluster_bloburl.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +// Same agent cluster, all works fine: blobURLs can be opened. +add_task(async () => { + do_get_profile(); + + // CookieXPCShellUtils.createServer does not support https + Services.prefs.setBoolPref("dom.security.https_first", false); + + Services.prefs.setBoolPref( + "privacy.partition.bloburl_per_agent_cluster", + true + ); + + const server = CookieXPCShellUtils.createServer({ hosts: ["example.org"] }); + + let result = new Promise(resolve => { + server.registerPathHandler("/result", (metadata, response) => { + resolve(metadata.queryString == "ok"); + + const body = "Done"; + response.bodyOutputStream.write(body, body.length); + }); + }); + + server.registerPathHandler("/test", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + const body = `<script> + let b = new Blob(["Hello world!"]); + let u = URL.createObjectURL(b); + fetch(u).then(r => r.text()).then(t => { + if (t !== "Hello world!") { + throw new Error(42); + } + }).then(() => fetch("/result?ok"), () => fetch("/result?failure")).then(() => {}); + </script>`; + response.bodyOutputStream.write(body, body.length); + }); + + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/test" + ); + + Assert.ok(await result, "BlobURL works"); + await contentPage.close(); +}); + +// Same agent cluster: frames +add_task(async () => { + do_get_profile(); + + // CookieXPCShellUtils.createServer does not support https + Services.prefs.setBoolPref("dom.security.https_first", false); + + const server = CookieXPCShellUtils.createServer({ hosts: ["example.org"] }); + + let result = new Promise(resolve => { + server.registerPathHandler("/result", (metadata, response) => { + resolve(metadata.queryString == "ok"); + + const body = "Done"; + response.bodyOutputStream.write(body, body.length); + }); + }); + + server.registerPathHandler("/iframe", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + const body = `<script> + fetch("${metadata.queryString}").then(r => r.text()).then(t => { + if (t !== "Hello world!") { + throw new Error(42); + } + }).then(() => fetch("/result?ok"), () => fetch("/result?failure")).then(() => {}); + </script>`; + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/test", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + const body = `<iframe id="a"></iframe><script> + let b = new Blob(["Hello world!"]); + let u = URL.createObjectURL(b); + document.getElementById("a").src = "/iframe?" + u; + </script>`; + response.bodyOutputStream.write(body, body.length); + }); + + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/test" + ); + + Assert.ok(await result, "BlobURL works"); + await contentPage.close(); +}); + +// Cross agent cluster: different tabs +add_task(async () => { + do_get_profile(); + + const server = CookieXPCShellUtils.createServer({ hosts: ["example.org"] }); + + let result = new Promise(resolve => { + server.registerPathHandler("/result", (metadata, response) => { + resolve(metadata.queryString == "ok"); + + const body = "Done"; + response.bodyOutputStream.write(body, body.length); + }); + }); + + const step = new Promise(resolve => { + server.registerPathHandler("/step", (metadata, response) => { + resolve(metadata.queryString); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + const body = "Thanks!"; + response.bodyOutputStream.write(body, body.length); + }); + }); + + server.registerPathHandler("/test", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + const body = `<script> + let b = new Blob(["Hello world!"]); + let u = URL.createObjectURL(b); + fetch("/step?" + u).then(() => {}); + </script>`; + response.bodyOutputStream.write(body, body.length); + }); + + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/test" + ); + + const blobURL = await step; + Assert.ok(blobURL.length, "We have a blobURL"); + + server.registerPathHandler("/cross-test", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + const body = `<script> + fetch("${metadata.queryString}").then(r => r.text()).then(t => { + if (t !== "Hello world!") { + throw new Error(42); + } + }).then(() => fetch("/result?ok"), () => fetch("/result?failure")).then(() => {}); + </script>`; + response.bodyOutputStream.write(body, body.length); + }); + + let contentPage2 = await CookieXPCShellUtils.loadContentPage( + "http://example.org/cross-test?" + blobURL + ); + + Assert.ok(!(await result), "BlobURL should not work"); + await contentPage.close(); + await contentPage2.close(); +}); diff --git a/dom/file/tests/test_blobURL_expiring.html b/dom/file/tests/test_blobURL_expiring.html new file mode 100644 index 0000000000..7fdf461371 --- /dev/null +++ b/dom/file/tests/test_blobURL_expiring.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Blob URI expiration</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script> + +onmessage = function(e) { + var blobURL = e.data; + + (new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", blobURL); + xhr.send(); + xhr.onload = function() { + is(xhr.response, "123", "Response matches!"); + resolve(); + } + })).then(function() { + document.body.removeChild(iframe); + }).then(function() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", blobURL); + xhr.onerror = function() { + ok(true, "The URL should be done!"); + SimpleTest.finish(); + } + xhr.onload = function() { + ok(false, "The URL should be done!"); + SimpleTest.finish(); + } + xhr.send(); + }); +} + +var iframe = document.createElement('iframe'); +iframe.src = 'file_blobURL_expiring.html'; +document.body.appendChild(iframe); + +SimpleTest.waitForExplicitFinish(); + + </script> +</body> +</html> diff --git a/dom/file/tests/test_blob_fragment_and_query.html b/dom/file/tests/test_blob_fragment_and_query.html new file mode 100644 index 0000000000..fa3709c419 --- /dev/null +++ b/dom/file/tests/test_blob_fragment_and_query.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Blob URI with fragments</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script> + +var blob = new Blob(['hello world']); +ok(blob, "We have a blob."); + +var tests = [ + { part: "", revoke: false, ok: true }, + { part: "", revoke: true, ok: false }, + { part: "?aa", revoke: false, ok: false }, + { part: "?cc#dd", revoke: false, ok: false }, + // Stripping #fragment on fetch + { part: "#bb", revoke: false, ok: true }, + { part: "#ee?ff", revoke: false, ok: true } +]; + +function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var url = URL.createObjectURL(blob); + ok(url, "We have a URI"); + + var test = tests.shift(); + + if (test.revoke) { + URL.revokeObjectURL(url + test.part); + } + + var xhr = new XMLHttpRequest(); + xhr.open('GET', url + test.part); + + xhr.onload = function() { + ok(test.ok, `URL with "${test.part}" should send()`); + is(xhr.responseText, 'hello world', 'URL: ' + url + test.part); + runTest(); + } + + xhr.onerror = function() { + ok(!test.ok, `URL with "${test.part}" should fail on send()`); + runTest(); + } + + xhr.send(); +} + +SimpleTest.waitForExplicitFinish(); +runTest(); + + </script> +</body> +</html> diff --git a/dom/file/tests/test_blob_reading.html b/dom/file/tests/test_blob_reading.html new file mode 100644 index 0000000000..6efc5c4835 --- /dev/null +++ b/dom/file/tests/test_blob_reading.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Reading blobs</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="common_blob_reading.js"></script> + <script src="common_blob_types.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script> + +SimpleTest.waitForExplicitFinish(); + +async function runAllTests() { + let content = "hello world"; + await forEachBlobType(content, async blob => { + await testBlobText(blob, content); + await workify('testBlobText', blob, content); + + await testBlobArrayBuffer(blob, content); + await workify('testBlobArrayBuffer', blob, content); + + await testBlobStream(blob, content); + await workify('testBlobStream', blob, content); + }); +} + +runAllTests().then(SimpleTest.finish); + + </script> +</body> +</html> diff --git a/dom/file/tests/test_blobconstructor.html b/dom/file/tests/test_blobconstructor.html new file mode 100644 index 0000000000..d95444f9ea --- /dev/null +++ b/dom/file/tests/test_blobconstructor.html @@ -0,0 +1,246 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=721569 +--> +<head> + <title>Test for Blob constructor (Bug 721569)</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="common_blob.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=721569">Mozilla Bug 721569</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +"use strict"; +/** Test for Bug 721569 **/ +var blob = new Blob(); +ok(blob, "Blob should exist"); + +ok(blob.size !== undefined, "Blob should have a size property"); +ok(blob.type !== undefined, "Blob should have a type property"); +ok(blob.slice, "Blob should have a slice method"); + +blob = new Blob([], {type: null}); +ok(blob, "Blob should exist"); +is(blob.type, "null", "Blob type should be stringified"); + +blob = new Blob([], {type: undefined}); +ok(blob, "Blob should exist"); +is(blob.type, "", "Blob type should be treated as missing"); + +try { +blob = new Blob([]); +ok(true, "an empty blobParts argument should not throw"); +} catch(e) { +ok(false, "NOT REACHED"); +} + +try { +blob = new Blob(null); +ok(false, "NOT REACHED"); +} catch(e) { +ok(true, "a null blobParts member should throw"); +} + +try { +blob = new Blob([], null); +ok(true, "a null options member should not throw"); +} catch(e) { +ok(false, "NOT REACHED"); +} + +try { +blob = new Blob([], undefined); +ok(true, "an undefined options member should not throw"); +} catch(e) { +ok(false, "NOT REACHED"); +} + +try { +blob = new Blob([], false); +ok(false, "NOT REACHED"); +} catch(e) { +ok(true, "a boolean options member should throw"); +} + +try { +blob = new Blob([], 0); +ok(false, "NOT REACHED"); +} catch(e) { +ok(true, "a numeric options member should throw"); +} + +try { +blob = new Blob([], ""); +ok(false, "NOT REACHED"); +} catch(e) { +ok(true, "a string options member should throw"); +} + +/** Test for dictionary initialization order **/ +(function() { + var o = {}; + var p = {type: "text/plain", endings: "transparent"}; + var called = []; + function add_to_called(n) { + called.push(n); + return p[n]; + } + ["type", "endings"].forEach(function(n) { + Object.defineProperty(o, n, { get: add_to_called.bind(null, n) }); + }); + var b = new Blob([], o); + is(JSON.stringify(called), JSON.stringify(["endings", "type"]), "dictionary members should be get in lexicographical order"); +})(); + +let blob1 = new Blob(["squiggle"]); +ok(blob1 instanceof Blob, "Blob constructor should produce Blobs"); +ok(!(blob1 instanceof File), "Blob constructor should not produce Files"); +is(blob1.type, "", "Blob constructor with no options should return Blob with empty type"); +is(blob1.size, 8, "Blob constructor should return Blob with correct size"); + +let blob2 = new Blob(["steak"], {type: "content/type"}); +ok(blob2 instanceof Blob, "Blob constructor should produce Blobs"); +ok(!(blob2 instanceof File), "Blob constructor should not produce Files"); +is(blob2.type, "content/type", "Blob constructor with a type option should return Blob with the type"); +is(blob2.size, 5, "Blob constructor should return Blob with correct size"); + + +let aB = new ArrayBuffer(16); +var int8View = new Int8Array(aB); +for (var i = 0; i < 16; i++) { + int8View[i] = i+65; +} + +let testData = + [ + // Test 3 strings + [["foo", "bar", "baz"], {}, + [{start: 0, length: 9, contents: "foobarbaz"}, + {start: 0, length: 3, contents: "foo"}, + {start: 3, length:6, contents: "barbaz"}, + {start: 6, length: 3, contents: "baz"}, + {start: 6, length: 6, contents: "baz"}, + {start: 0, length: 9, contents: "foobarbaz"}, + {start: 0, length: 11, contents: "foobarbaz"}, + {start: 10, length: 5, contents: ""}]], + // Test string, Blob, string + [["foo", blob1, "baz"], {}, + [{start: 0, length: 3, contents: "foo"}, + {start: 3, length: 8, contents: "squiggle"}, + {start: 2, length: 2, contents: "os"}, + {start: 10, length: 2, contents: "eb"}]], + // Test blob, string, blob + [[blob1, "foo", blob1], {}, + [{start: 0, length: 8, contents: "squiggle"}, + {start: 7, length: 2, contents: "ef"}, + {start: 10, length: 2, contents: "os"}, + {start: 1, length: 3, contents: "qui"}, + {start: 12, length: 3, contents: "qui"}, + {start: 40, length: 20, contents: ""}]], + // Test blobs all the way down + [[blob2, blob1, blob2], {}, + [{start: 0, length: 5, contents: "steak"}, + {start: 5, length: 8, contents: "squiggle"}, + {start: 13, length: 5, contents: "steak"}, + {start: 1, length: 2, contents: "te"}, + {start: 6, length: 4, contents: "quig"}]], + // Test an array buffer + [[aB, blob1, "foo"], {}, + [{start: 0, length: 8, contents: "ABCDEFGH"}, + {start: 8, length:10, contents: "IJKLMNOPsq"}, + {start: 17, length: 3, contents: "qui"}, + {start: 4, length: 8, contents: "EFGHIJKL"}]], + // Test an ArrayBufferView + [[int8View, blob1, "foo"], {}, + [{start: 0, length: 8, contents: "ABCDEFGH"}, + {start: 8, length:10, contents: "IJKLMNOPsq"}, + {start: 17, length: 3, contents: "qui"}, + {start: 4, length: 8, contents: "EFGHIJKL"}]], + // Test a partial ArrayBufferView + [[new Uint8Array(aB, 3, 5), blob1, "foo"], {}, + [{start: 0, length: 8, contents: "DEFGHsqu"}, + {start: 8, length:10, contents: "igglefoo"}, + {start: 4, length: 8, contents: "Hsquiggl"}]], + // Test transparent line endings + [["foo\r\n", "bar\r", "baz\n"], { endings: "transparent" }, + [{start: 0, length: 5, contents: "foo\r\n"}, + {start: 5, length: 4, contents: "bar\r"}, + {start: 9, length: 4, contents: "baz\n"}]], + // Test transparent line endings when the second argument is omitted + [["foo\r\n", "bar\r", "baz\n"], undefined, + [{start: 0, length: 5, contents: "foo\r\n"}, + {start: 5, length: 4, contents: "bar\r"}, + {start: 9, length: 4, contents: "baz\n"}]], + // Test native line endings + [["foo\r\n", "bar\r", "baz\n"], { endings: "native" }, + navigator.platform.includes("Win") ? + [{start: 0, length: 5, contents: "foo\r\n"}, + {start: 5, length: 5, contents: "bar\r\n"}, + {start: 10, length: 5, contents: "baz\r\n"}] : + [{start: 0, length: 4, contents: "foo\n"}, + {start: 4, length: 4, contents: "bar\n"}, + {start: 8, length: 4, contents: "baz\n"}]], + // Test type coercion of a number + [[3, int8View, "foo"], {}, + [{start: 0, length: 8, contents: "3ABCDEFG"}, + {start: 8, length:10, contents: "HIJKLMNOPf"}, + {start: 17, length: 4, contents: "foo"}, + {start: 4, length: 8, contents: "DEFGHIJK"}]] + ]; + +let currentTest = null; +let testCounter = 0; + +function runTests() { + if (!currentTest || !currentTest[2].length) { + if (!testData.length) { + SimpleTest.finish(); + return; + } + + currentTest = testData.shift(); + ++testCounter; + } + + let [blobs, options] = currentTest; + let test = currentTest[2].shift(); + + let blob3; + if (options !== undefined) { + blob3 = new Blob(blobs, options); + } else { + blob3 = new Blob(blobs); + } + + ok(blob3, "Test " + testCounter + " got blob"); + ok(blob3 instanceof Blob, "Test " + testCounter + " blob is a Blob"); + ok(!(blob3 instanceof File), "Test " + testCounter + " blob is not a File"); + + let slice = blob3.slice(test.start, test.start + test.length); + ok(slice, "Test " + testCounter + " got slice"); + ok(slice instanceof Blob, "Test " + testCounter + " slice is a Blob"); + ok(!(slice instanceof File), "Test " + testCounter + " slice is not a File"); + is(slice.size, test.contents.length, "Test " + testCounter + " slice is correct size"); + + testBlob(slice, test.contents, "Test " + testCounter).then(() => { + SpecialPowers.gc(); + runTests(); + }); +} + +SimpleTest.requestLongerTimeout(2); +SimpleTest.waitForExplicitFinish(); +runTests(); + +</script> +</pre> +</body> +</html> diff --git a/dom/file/tests/test_bloburi.js b/dom/file/tests/test_bloburi.js new file mode 100644 index 0000000000..3485b8a681 --- /dev/null +++ b/dom/file/tests/test_bloburi.js @@ -0,0 +1,24 @@ +var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + +var uris = [ + { + uri: "blob:https://example.com/230d5d50-35f9-9745-a64a-15e47b731a81", + local: true, + }, + { + uri: "rstp://1.2.3.4/some_path?param=a", + local: false, + }, +]; + +function run_test() { + for (let i = 0; i < uris.length; i++) { + let uri = ios.newURI(uris[i].uri); + let flags = ios.getDynamicProtocolFlags(uri); + + Assert.equal( + Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE & flags, + uris[i].local ? Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE : 0 + ); + } +} diff --git a/dom/file/tests/test_bug1507893.html b/dom/file/tests/test_bug1507893.html new file mode 100644 index 0000000000..f0f83ff9ce --- /dev/null +++ b/dom/file/tests/test_bug1507893.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Blob URLs fetched in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script> + +SimpleTest.waitForExplicitFinish(); + +// Let's be positive. +Promise.resolve() + +// Create a file. +.then(_ => { + return new Promise(resolve => { + let openerURL = SimpleTest.getTestFileURL("fileapi_chromeScript.js"); + let opener = SpecialPowers.loadChromeScript(openerURL); + + opener.addMessageListener("files.opened", files => { + resolve(files[0]); + }); + + opener.sendAsyncMessage("files.open", [ "I am the blob content" ]); + }) +}) + +// Just a couple of checks +.then(file => { + ok(file instanceof File, "We want a file"); + ok(file.size > 0, "We have content"); + return file; +}) + +// Let's create a blobURL +.then(file => URL.createObjectURL(file)) + +// Let's send it to a worker. +.then(url => { + return new Promise(resolve => { + let w = new Worker('worker_bug1507893.js'); + w.onmessage = e => { + resolve(e.data); + }; + w.postMessage(url); + }); +}) + +// Let's check the worker's output +.then(blob => { + ok(blob instanceof File, "The worker sends us a blob"); + ok(blob.size > 0, "We have data"); +}) + +// All done. +.then(SimpleTest.finish); + + </script> +</body> +</html> diff --git a/dom/file/tests/test_bug1742540.html b/dom/file/tests/test_bug1742540.html new file mode 100644 index 0000000000..7516f38a85 --- /dev/null +++ b/dom/file/tests/test_bug1742540.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1742540</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <script> + +SimpleTest.waitForExplicitFinish(); + +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); +}); + +function get_file() { + return new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.open("GET", "/dynamic/getMyDirectory.sjs", false); + xhr.send(); + let basePath = xhr.responseText; + + let script = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("create_file_objects.js") + ); + script.addMessageListener("created-file-objects", files => { + resolve(files[0]); + }); + script.sendAsyncMessage("create-file-objects", { + fileNames: [basePath + "file_mozfiledataurl_audio.ogg"], + }); + }); +} + +function wait_for_message(port, expected_message) { + return new Promise(resolve => { + port.onmessage = event => { + port.onmessage = null; + ok(event.data === expected_message, event.data); + resolve(); + }; + }); +} + +function unregister_and_done(registration) { + return registration.unregister().then(() => { + ok(true, "Will find leaks of nsPipe in BloatView without fix."); + SimpleTest.finish; + }); +} + +add_task(async function send_file_to_serviceworker() { + let registration = await navigator.serviceWorker + .register("worker_bug1742540.js", { scope: "./" }) + .then(() => { + return navigator.serviceWorker.ready; + }); + + ok(registration.active, "ServiceWorker is activated"); + + let file = await get_file(); + ok(file.size > 100000, "File size is big enough."); + let message = "ServiceWorker receives a file and did not reference it."; + let channel = new MessageChannel(); + let received = wait_for_message(channel.port1, message); + registration.active.postMessage({ port: channel.port2, message, file }, [ + channel.port2, + ]); + await received; + + let finish = await unregister_and_done(registration); +}); + + </script> +</body> +</html> diff --git a/dom/file/tests/test_createFile.js b/dom/file/tests/test_createFile.js new file mode 100644 index 0000000000..ebda3d4dae --- /dev/null +++ b/dom/file/tests/test_createFile.js @@ -0,0 +1,52 @@ +add_task(async function () { + do_get_profile(); + + let existingFile = Services.dirsvc + .QueryInterface(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + existingFile.append("exists.js"); + existingFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + var outStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outStream.init( + existingFile, + 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, + 0 + ); + + var fileData = "Hello World!"; + outStream.write(fileData, fileData.length); + outStream.close(); + + ok(existingFile.exists(), "exists.js exists"); + + let unknownFile = Services.dirsvc + .QueryInterface(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + unknownFile.append("wow.txt"); + + ok(!unknownFile.exists(), unknownFile.path + " doesn't exist"); + + let a = await File.createFromNsIFile(existingFile, { existenceCheck: false }); + ok(a.size != 0, "The size is correctly set"); + + let b = await File.createFromNsIFile(unknownFile, { existenceCheck: false }); + ok(b.size == 0, "The size is 0 for unknown file"); + + let c = await File.createFromNsIFile(existingFile, { existenceCheck: true }); + ok(c.size != 0, "The size is correctly set"); + + let d = await File.createFromNsIFile(unknownFile, { + existenceCheck: true, + }).then( + _ => true, + _ => false + ); + ok(d === false, "Exception thrown"); + + existingFile.remove(true); + ok(!existingFile.exists(), "exists.js doesn't exist anymore"); +}); diff --git a/dom/file/tests/test_file_from_blob.html b/dom/file/tests/test_file_from_blob.html new file mode 100644 index 0000000000..8c12a2823a --- /dev/null +++ b/dom/file/tests/test_file_from_blob.html @@ -0,0 +1,110 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=819900 +--> + <head> +<title>Test for crash caused by unloading and reloading srcdoc iframes</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=819900">Mozilla Bug 819900</a> + +<pre id="test"> +<script> + + var b = new Blob(['1234567890']); + ok(b, 'Blob created'); + is(b.size, 10, 'Blob has the right size'); + + var status = false; + try { + f = new File(b); + } catch(e) { + status = true; + } + ok(status, "File throws if the second argument is missing"); + + status = false; + try { + f = new File(42, 'foobar.txt'); + } catch(e) { + status = true; + } + ok(status, "File throws if the argument is not an array"); + + status = false; + try { + f = new File({}, 'foobar.txt'); + } catch(e) { + status = true; + } + ok(status, "File throws if the argument is not an array"); + + status = false; + try { + f = new File("hello world", 'foobar.txt'); + } catch(e) { + status = true; + } + ok(status, "File throws if the argument is not an array"); + + f = new File(['1234567890'], ''); + ok(f, 'File created'); + is(f.size, 10, 'File has the right size'); + is(f.name, ''); + is(f.type, ''); + + f = new File(['1234567890'], 42); + ok(f, 'File created'); + is(f.size, 10, 'File has the right size'); + is(f.name, '42'); + is(f.type, ''); + + f = new File(['1234567890'], 'text.txt'); + ok(f, 'File created'); + is(f.size, 10, 'File has the right size'); + is(f.name, 'text.txt'); + is(f.type, ''); + + f = new File(['1234567890'], 'text.txt', { type: 'plain/text' }); + ok(f, 'File created'); + is(f.size, 10, 'File has the right size'); + is(f.name, 'text.txt'); + is(f.type, 'plain/text'); + + f = new File([b], 'text.txt'); + ok(f, 'File created'); + is(f.name, 'text.txt'); + is(f.type, ''); + is(f.size, b.size); + + f = new File([b], 'test.txt', { type: 'plain/text' }); + ok(f, 'File created'); + is(f.name, 'test.txt'); + is(f.type, 'plain/text'); + is(f.size, b.size); + + f = new File([b, b], 'test.txt', { type: 'plain/text' }); + ok(f, 'File created'); + is(f.name, 'test.txt'); + is(f.type, 'plain/text'); + is(f.size, b.size * 2); + + var f2 = new File([f, f], 'test.txt', { type: 'plain/text' }); + ok(f2, 'File created'); + is(f2.name, 'test.txt'); + is(f2.type, 'plain/text'); + is(f2.size, f.size * 2); + + var f2 = new File([f, f], 'test.txt', b); + ok(f2, 'File created'); + is(f2.name, 'test.txt'); + is(f2.type, b.type); + is(f2.size, f.size * 2); + +</script> +</pre> +</body> +</html> diff --git a/dom/file/tests/test_file_negative_date.html b/dom/file/tests/test_file_negative_date.html new file mode 100644 index 0000000000..2e8528b88d --- /dev/null +++ b/dom/file/tests/test_file_negative_date.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1158437 +--> +<head> + <title>Test for negative date in File (Bug 1158437)</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1158437">Mozilla Bug 1158437</a> + +<script class="testbody" type="text/javascript"> +"use strict"; + +var blob = new Blob(['hello world']); +var f1 = new File([blob], 'f1.txt', { lastModified: 0 }); +var f2 = new File([blob], 'f2.txt', { lastModified: -1 }); +var f3 = new File([blob], 'f3.txt', { lastModified: -1000 }); + +is(f1.lastModified, 0, "lastModified == 0 is supported"); +is(f2.lastModified, -1, "lastModified == -1 is supported"); +is(f3.lastModified, -1000, "lastModified == -1000 is supported"); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_basic.html b/dom/file/tests/test_fileapi_basic.html new file mode 100644 index 0000000000..7f7aed788e --- /dev/null +++ b/dom/file/tests/test_fileapi_basic.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for FileReader API</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="common_fileReader.js"></script> +</head> + +<body> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(3); + +test_setup() +.then(data => { + return runBasicTests(data); +}) +.then(SimpleTest.finish); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_basic_worker.html b/dom/file/tests/test_fileapi_basic_worker.html new file mode 100644 index 0000000000..03db1b4cb6 --- /dev/null +++ b/dom/file/tests/test_fileapi_basic_worker.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for FileReader API in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="common_fileReader.js"></script> +</head> + +<body> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(3); + +test_setup() +.then(data => { + let worker = new Worker('worker_fileReader.js'); + worker.postMessage({ tests: 'basic', data }); + + worker.onmessage = event => { + if (event.data.type == 'finish') { + SimpleTest.finish(); + return; + } + + if (event.data.type == 'check') { + ok(event.data.status, event.data.msg); + return; + } + + ok(false, "Unknown message."); + } +}); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_encoding.html b/dom/file/tests/test_fileapi_encoding.html new file mode 100644 index 0000000000..f20a46ac6c --- /dev/null +++ b/dom/file/tests/test_fileapi_encoding.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for FileReader API</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="common_fileReader.js"></script> +</head> + +<body> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(3); + +test_setup() +.then(data => { + return runEncodingTests(data); +}) +.then(SimpleTest.finish); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_encoding_worker.html b/dom/file/tests/test_fileapi_encoding_worker.html new file mode 100644 index 0000000000..ab6827f086 --- /dev/null +++ b/dom/file/tests/test_fileapi_encoding_worker.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for FileReader API in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="common_fileReader.js"></script> +</head> + +<body> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(3); + +test_setup() +.then(data => { + let worker = new Worker('worker_fileReader.js'); + worker.postMessage({ tests: 'encoding', data }); + + worker.onmessage = event => { + if (event.data.type == 'finish') { + SimpleTest.finish(); + return; + } + + if (event.data.type == 'check') { + ok(event.data.status, event.data.msg); + return; + } + + ok(false, "Unknown message."); + } +}); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_other.html b/dom/file/tests/test_fileapi_other.html new file mode 100644 index 0000000000..6c01415be8 --- /dev/null +++ b/dom/file/tests/test_fileapi_other.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for FileReader API</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="common_fileReader.js"></script> +</head> + +<body> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(3); + +test_setup() +.then(data => { + return runOtherTests(data); +}) +.then(SimpleTest.finish); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_other_worker.html b/dom/file/tests/test_fileapi_other_worker.html new file mode 100644 index 0000000000..a535d3fcb4 --- /dev/null +++ b/dom/file/tests/test_fileapi_other_worker.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for FileReader API in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="common_fileReader.js"></script> +</head> + +<body> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(3); + +test_setup() +.then(data => { + let worker = new Worker('worker_fileReader.js'); + worker.postMessage({ tests: 'other', data }); + + worker.onmessage = event => { + if (event.data.type == 'finish') { + SimpleTest.finish(); + return; + } + + if (event.data.type == 'check') { + ok(event.data.status, event.data.msg); + return; + } + + ok(false, "Unknown message."); + } +}); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_slice_image.html b/dom/file/tests/test_fileapi_slice_image.html new file mode 100644 index 0000000000..c873afa438 --- /dev/null +++ b/dom/file/tests/test_fileapi_slice_image.html @@ -0,0 +1,139 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for File API + Slice</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="common_blob.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + +<p id="display"> + <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas> + <canvas id=testcanvas hidden moz-opaque></canvas> +</p> + +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(4); + +// Create files containing data we'll test with. We'll want long +// strings to ensure they span multiple buffers while loading + +let canvasData; +let testBinaryData; + +function imageLoadHandler(event, resolve) { + let origcanvas = $("canvas"); + let testcanvas = $("testcanvas"); + let image = event.target; + is(image.naturalWidth, origcanvas.width, "width correct"); + is(image.naturalHeight, origcanvas.height, "height correct"); + + testcanvas.width = origcanvas.width; + testcanvas.height = origcanvas.height; + testcanvas.getContext("2d").drawImage(image, 0, 0); + // Do not use |is(testcanvas.toDataURL("image/png"), origcanvas.toDataURL("image/png"), "...");| that results in a _very_ long line. + let origDataURL = origcanvas.toDataURL("image/png"); + let testDataURL = testcanvas.toDataURL("image/png"); + is(testDataURL.length, origDataURL.length, + "Length of correct image data"); + ok(testDataURL == origDataURL, + "Content of correct image data"); + resolve(); +} + +createCanvasURL() +.then(data => { + for (var i = 0; i < 256; i++) { + testBinaryData += String.fromCharCode(i); + } + while (testBinaryData.length < 20000) { + testBinaryData += testBinaryData; + } + + canvasData = data; +}) + +// image in the middle +.then(() => { + return createFile(testBinaryData + canvasData + testBinaryData, "middleTestFile"); +}) + +// image in the middle - loader +.then(file => { + return new Promise(resolve => { + is(file.size, canvasData.length + testBinaryData.length * 2, "correct file size (middle)"); + + var img = new Image(); + img.src = URL.createObjectURL(file.slice(testBinaryData.length, + testBinaryData.length + canvasData.length)); + img.onload = event => { + imageLoadHandler(event, resolve); + } + }); +}) + +// image at start +.then(() => { + return createFile(canvasData + testBinaryData, "startTestFile"); +}) + +// image at start - loader +.then(file => { + return new Promise(resolve => { + is(file.size, canvasData.length + testBinaryData.length, "correct file size (start)"); + + var img = new Image(); + img.src = URL.createObjectURL(file.slice(0, canvasData.length)); + img.onload = event => { + imageLoadHandler(event, resolve); + } + }); +}) + +// image at end +.then(() => { + return createFile(testBinaryData + canvasData, "endTestFile"); +}) + +// image at end - loader +.then(file => { + return new Promise(resolve => { + is(file.size, canvasData.length + testBinaryData.length, "correct file size (end)"); + + var img = new Image(); + img.src = URL.createObjectURL(file.slice(testBinaryData.length, + testBinaryData.length + canvasData.length)); + img.onload = event => { + imageLoadHandler(event, resolve); + } + }); +}) + +// image past end +.then(() => { + return createFile(testBinaryData + canvasData, "pastEndTestFile"); +}) + +// image past end - loader +.then(file => { + return new Promise(resolve => { + is(file.size, canvasData.length + testBinaryData.length, "correct file size (end)"); + + var img = new Image(); + img.src = URL.createObjectURL(file.slice(testBinaryData.length, + testBinaryData.length + canvasData.length + 1000)); + img.onload = event => { + imageLoadHandler(event, resolve); + } + }); +}) + +.then(SimpleTest.finish); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_slice_memFile_1.html b/dom/file/tests/test_fileapi_slice_memFile_1.html new file mode 100644 index 0000000000..56d63d15ba --- /dev/null +++ b/dom/file/tests/test_fileapi_slice_memFile_1.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for File API + Slice (in memory)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="common_blob.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + +<p id="display"> + <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas> +</p> + +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(4); + +createCanvasURL() +.then(data => { + let cx = $("canvas").getContext('2d'); + return Promise.all([toBlobPromise(cx.canvas), + Promise.resolve(data)]); +}) + +.then(args => { + let [memFile, data] = args; + return testSlice(memFile, data.length, "image/png", data, "memFile", RANGE_1); +}) + +.then(SimpleTest.finish); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_slice_memFile_2.html b/dom/file/tests/test_fileapi_slice_memFile_2.html new file mode 100644 index 0000000000..aef8813bf5 --- /dev/null +++ b/dom/file/tests/test_fileapi_slice_memFile_2.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for File API + Slice (in memory)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="common_blob.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + +<p id="display"> + <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas> +</p> + +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(4); + +createCanvasURL() +.then(data => { + let cx = $("canvas").getContext('2d'); + return Promise.all([toBlobPromise(cx.canvas), + Promise.resolve(data)]); +}) + +.then(args => { + let [memFile, data] = args; + return testSlice(memFile, data.length, "image/png", data, "memFile", RANGE_2); +}) + +.then(SimpleTest.finish); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_slice_realFile_1.html b/dom/file/tests/test_fileapi_slice_realFile_1.html new file mode 100644 index 0000000000..97798d805a --- /dev/null +++ b/dom/file/tests/test_fileapi_slice_realFile_1.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for File API + Slice (in file)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="common_blob.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + +<p id="display"> + <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas> +</p> + +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(4); + +let canvasData; + +createCanvasURL() +.then(data => { + canvasData = data; + return createFile(data, "basicTestFile1"); +}) + +.then(file => { + return testSlice(file, canvasData.length, "", canvasData, "fileFile", RANGE_1); +}) + +.then(SimpleTest.finish); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_slice_realFile_2.html b/dom/file/tests/test_fileapi_slice_realFile_2.html new file mode 100644 index 0000000000..882ce61c03 --- /dev/null +++ b/dom/file/tests/test_fileapi_slice_realFile_2.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for File API + Slice (in file)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="common_blob.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + +<p id="display"> + <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas> +</p> + +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(4); + +let canvasData; + +createCanvasURL() +.then(data => { + canvasData = data; + return createFile(data, "basicTestFile2"); +}) + +.then(file => { + return testSlice(file, canvasData.length, "", canvasData, "fileFile", RANGE_2); +}) + +.then(SimpleTest.finish); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_twice.html b/dom/file/tests/test_fileapi_twice.html new file mode 100644 index 0000000000..96e7febdce --- /dev/null +++ b/dom/file/tests/test_fileapi_twice.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for FileReader API</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="common_fileReader.js"></script> +</head> + +<body> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(3); + +test_setup() +.then(data => { + return runTwiceTests(data); +}) +.then(SimpleTest.finish); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_fileapi_twice_worker.html b/dom/file/tests/test_fileapi_twice_worker.html new file mode 100644 index 0000000000..a79d3992b2 --- /dev/null +++ b/dom/file/tests/test_fileapi_twice_worker.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for FileReader API in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="common_fileReader.js"></script> +</head> + +<body> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(3); + +test_setup() +.then(data => { + let worker = new Worker('worker_fileReader.js'); + worker.postMessage({ tests: 'twice', data }); + + worker.onmessage = event => { + if (event.data.type == 'finish') { + SimpleTest.finish(); + return; + } + + if (event.data.type == 'check') { + ok(event.data.status, event.data.msg); + return; + } + + ok(false, "Unknown message."); + } +}); + +</script> +</body> +</html> diff --git a/dom/file/tests/test_ipc_messagemanager_blob.js b/dom/file/tests/test_ipc_messagemanager_blob.js new file mode 100644 index 0000000000..dacdc9e7bb --- /dev/null +++ b/dom/file/tests/test_ipc_messagemanager_blob.js @@ -0,0 +1,102 @@ +"use strict"; + +const { XPCShellContentUtils } = ChromeUtils.importESModule( + "resource://testing-common/XPCShellContentUtils.sys.mjs" +); + +XPCShellContentUtils.init(this); + +function childFrameScript() { + addMessageListener("test:ipcClonedMessage", function (message) { + if (!Blob.isInstance(message.json)) { + sendAsyncMessage(message.name, message.json); + return; + } + + let reader = new FileReader(); + reader.addEventListener("load", function () { + let response = + reader.result == "this is a great success!" ? message.json : "error"; + sendAsyncMessage(message.name, response); + }); + reader.readAsText(message.json); + }); +} + +add_task(async function test() { + let page = await XPCShellContentUtils.loadContentPage("about:blank", { + remote: true, + }); + + page.loadFrameScript(childFrameScript); + + const blobString = "this is a great success!"; + + const messages = [ + "hi!", + "", + 2, + -0.04, + 34329873249872400000000000000, + true, + false, + null, + 0, + + // Make sure this one is always last. + new Blob(["this ", "is ", "a ", "great ", "success!"], { + type: "text/plain", + }), + ]; + let receivedMessageIndex = 0; + + let mm = page.browser.messageManager; + let done = new Promise(resolve => { + mm.addMessageListener("test:ipcClonedMessage", async message => { + let data = message.json; + + if (Blob.isInstance(data)) { + equal(receivedMessageIndex, messages.length - 1, "Blob is last"); + equal( + data.size, + messages[receivedMessageIndex].size, + "Correct blob size" + ); + equal( + data.type, + messages[receivedMessageIndex].type, + "Correct blob type" + ); + + let reader1 = new FileReader(); + reader1.readAsText(data); + + let reader2 = new FileReader(); + reader2.readAsText(messages[receivedMessageIndex]); + + await Promise.all([ + new Promise(res => (reader1.onload = res)), + new Promise(res => (reader2.onload = res)), + ]); + + equal(reader1.result, blobString, "Result 1"); + equal(reader2.result, blobString, "Result 2"); + + resolve(); + } else { + equal( + data, + messages[receivedMessageIndex++], + "Got correct round-tripped response" + ); + } + }); + }); + + for (let message of messages) { + mm.sendAsyncMessage("test:ipcClonedMessage", message); + } + + await done; + await page.close(); +}); diff --git a/dom/file/tests/test_mozfiledataurl.html b/dom/file/tests/test_mozfiledataurl.html new file mode 100644 index 0000000000..68c88fa28c --- /dev/null +++ b/dom/file/tests/test_mozfiledataurl.html @@ -0,0 +1,224 @@ +<!DOCTYPE HTML> +<html> +<head> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8"> + <title>Test for File urls</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="start()"> +<p id="display"> +<iframe id=inner></iframe> +<iframe id=iframe></iframe> +<img id=img onload="gen.next(event);"> +<audio id=audio onloadeddata="gen.next(event);"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + +try { + URL.createObjectURL(undefined); +} catch(e) { } + +window.addEventListener("message", function(e) { + gen.next(JSON.parse(e.data)); +}); + +const innerSameSiteURI = "file_mozfiledataurl_inner.html"; +const innerCrossSiteURI = "http://example.com/tests/dom/file/tests/file_mozfiledataurl_inner.html" + +var fileNames = ["file_mozfiledataurl_img.jpg", + "file_mozfiledataurl_audio.ogg", + "file_mozfiledataurl_doc.html", + "file_mozfiledataurl_text.txt"]; + +function start() { + let xhr = new XMLHttpRequest; + xhr.open("GET", "/dynamic/getMyDirectory.sjs", false); + xhr.send(); + let basePath = xhr.responseText; + + let fullFileNames = []; + for (let name of fileNames) { + fullFileNames.push(basePath + name); + } + + var script = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("create_file_objects.js")); + + script.addMessageListener("created-file-objects", function handler(files) { + script.removeMessageListener("created-file-objects", handler); + gen = runTest(files); + gen.next(); + }); + + script.sendAsyncMessage("create-file-objects", {fileNames: fullFileNames}); +}; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.expectAssertions(0, 3); + +function* runTest([imgFile, audioFile, docFile, xhrFile]) { + inner = document.getElementById('inner'); + img = document.getElementById('img'); + audio = document.getElementById('audio'); + iframe = document.getElementById('iframe'); + inner.onload = function() { gen.next("inner loaded"); }; + + // Attempt to load a image in this document + var fileurl = URL.createObjectURL(imgFile); + img.src = fileurl; + var e = (yield); + is(e.type, "load", "loaded successfully"); + is(img.width, 120, "correct width"); + is(img.height, 90, "correct height"); + + // Revoke url and attempt to load a image in this document + img.src = "file_mozfiledataurl_img.jpg"; + is((yield).type, "load", "successfull reset image"); + URL.revokeObjectURL(fileurl); + todo(false, "urls need to act like 404s, not fail to parse"); +/* img.src = fileurl; + var e = (yield); + is(e.type, "error", "failed successfully"); + isnot(img.width, 120, "correct error width"); + isnot(img.height, 90, "correct error height"); +*/ + // Generate new fileurl and make sure it's different from the old + var oldFileurl = fileurl; + fileurl = URL.createObjectURL(imgFile); + isnot(fileurl, oldFileurl, "URL.createObjectURL generated the same url twice"); + + // Attempt to load an image in a different same-origin document + inner.src = innerSameSiteURI; + yield undefined; + inner.contentWindow.postMessage(JSON.stringify({img:fileurl}), "*"); + var res = (yield); + is(res.type, "load", "loaded successfully"); + is(res.width, 120, "correct width"); + is(res.height, 90, "correct height"); + + // Attempt to load an image in a different cross-origin document + inner.src = innerCrossSiteURI; + yield undefined; + inner.contentWindow.postMessage(JSON.stringify({img:fileurl}), "*"); + var res = (yield); + is(res.type, "error", "failed successfully"); + isnot(res.width, 120, "correct error width"); + isnot(res.height, 90, "correct error height"); + + // Attempt to load an audio in this document + fileurl = URL.createObjectURL(audioFile); + audio.src = fileurl; + var e = (yield); + is(e.type, "loadeddata", "loaded successfully"); + + // Revoke url and attempt to load a audio in this document + audio.src = "file_mozfiledataurl_audio.ogg"; + is((yield).type, "loadeddata", "successfully reset audio"); + URL.revokeObjectURL(fileurl); + todo(false, "urls need to act like 404s, not fail to parse"); +/* img.src = fileurl; + var e = (yield); + is(e.type, "error", "failed successfully"); + isnot(img.width, 120, "correct error width"); + isnot(img.height, 90, "correct error height"); +*/ + // Generate new fileurl and make sure it's different from the old + var oldFileurl = fileurl; + fileurl = URL.createObjectURL(audioFile); + isnot(fileurl, oldFileurl, "URL.createObjectURL generated the same url twice"); + + // Attempt to load an audio in a different same-origin document + inner.src = innerSameSiteURI; + yield undefined; + inner.contentWindow.postMessage(JSON.stringify({audio:fileurl}), "*"); + var res = (yield); + is(res.type, "loadeddata", "loaded successfully"); + + // Attempt to load an audio in a different cross-origin document + inner.src = innerCrossSiteURI; + yield undefined; + inner.contentWindow.postMessage(JSON.stringify({audio:fileurl}), "*"); + var res = (yield); + is(res.type, "error", "failed successfully"); + + // Attempt to load a HTML document in an iframe in this document + iframe.onload = function() { gen.next(); }; + iframe.src = "file_mozfiledataurl_doc.html"; + yield undefined; + is(iframe.contentDocument.getElementsByTagName("p")[0].textContent, + "This here is a document!", + "iframe loaded successfully"); + is(iframe.contentDocument.getElementById("img").width, 120, + "image in iframe width"); + is(iframe.contentDocument.getElementById("img").height, 90, + "image in iframe height"); + + // Attempt to load a HTML document in an iframe in this document, using file url + fileurl = URL.createObjectURL(docFile); + iframe.src = fileurl; + yield undefined; + is(iframe.contentDocument.getElementsByTagName("p")[0].textContent, + "This here is a document!", + "iframe loaded successfully"); + isnot(iframe.contentDocument.getElementById("img").width, 120, + "failed image in iframe width"); + isnot(iframe.contentDocument.getElementById("img").height, 90, + "failed image in iframe height"); + + // Attempt to load a HTML document in an iframe in inner document + inner.src = innerSameSiteURI; + is((yield), "inner loaded", "correct gen.next()"); + inner.contentWindow.postMessage(JSON.stringify({iframe:"file_mozfiledataurl_doc.html"}), "*"); + var res = (yield); + is(res.type, "load", "loaded successfully"); + is(res.text, "This here is a document!", "loaded successfully"); + is(res.imgWidth, 120, "correct width"); + + // Attempt to load a HTML document in an iframe in inner document, using file url + inner.contentWindow.postMessage(JSON.stringify({iframe:fileurl}), "*"); + var res = (yield); + is(res.type, "load", "loaded successfully"); + is(res.text, "This here is a document!", "loaded successfully"); + isnot(res.imgWidth, 120, "correct width"); + + // Attempt to load a HTML document in an iframe in inner cross-site document, using file url + inner.src = innerCrossSiteURI; + is((yield), "inner loaded", "correct gen.next()"); + inner.contentWindow.postMessage(JSON.stringify({iframe:fileurl}), "*"); + var res = (yield); + is(res.type, "error", "load failed successfully"); + + // Attempt to load file url using XHR + fileurl = URL.createObjectURL(xhrFile); + xhr = new XMLHttpRequest; + xhr.onload = function() { gen.next("XHR finished"); }; + xhr.open("GET", fileurl); + xhr.send(); + is((yield), "XHR finished", "correct gen.next()"); + xhr.responseText == "Yarr, here be plaintext file, ya landlubber\n"; + + // Attempt to load file url using XHR in inner document + inner.src = innerSameSiteURI; + is((yield), "inner loaded", "correct gen.next()"); + inner.contentWindow.postMessage(JSON.stringify({xhr:fileurl}), "*"); + var res = (yield); + is(res.didThrow, undefined, "load successful"); + is(res.text, "Yarr, here be plaintext file, ya landlubber\n", "load successful"); + + // Attempt to load file url using XHR + inner.src = innerCrossSiteURI; + is((yield), "inner loaded", "correct gen.next()"); + inner.contentWindow.postMessage(JSON.stringify({xhr:fileurl}), "*"); + var res = (yield); + is(res.didError, true, "load failed successfully"); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/dom/file/tests/test_nonascii_blob_url.html b/dom/file/tests/test_nonascii_blob_url.html new file mode 100644 index 0000000000..1c6c833958 --- /dev/null +++ b/dom/file/tests/test_nonascii_blob_url.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test blob URL for non-ascii domain</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content"></div> +<script class="testbody" type="text/javascript"> + +var iframe = document.createElement('iframe'); +iframe.src = 'http://xn--exmple-cua.test/tests/dom/file/tests/file_nonascii_blob_url.html'; +iframe.onload = function() { + iframe.contentWindow.postMessage('hello world', '*'); + onmessage = function(e) { + is(e.data, 'hello world', "Blob URL for non-ascii domain works"); + SimpleTest.finish(); + } +} + +document.getElementById('content').appendChild(iframe); +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/file/tests/worker_blob_reading.js b/dom/file/tests/worker_blob_reading.js new file mode 100644 index 0000000000..f57161c220 --- /dev/null +++ b/dom/file/tests/worker_blob_reading.js @@ -0,0 +1,26 @@ +importScripts("common_blob_reading.js"); + +function info(message) { + postMessage({ type: "info", message }); +} + +function ok(a, message) { + postMessage({ type: "test", test: !!a, message }); +} + +function is(a, b, message) { + ok(a === b, message); +} + +onmessage = function (e) { + self[e.data.func](e.data.blob, e.data.content).then( + () => { + postMessage({ type: "done" }); + }, + exc => { + dump(exc); + dump(exc.stack); + postMessage({ type: "error", message: exc.toString() }); + } + ); +}; diff --git a/dom/file/tests/worker_bug1507893.js b/dom/file/tests/worker_bug1507893.js new file mode 100644 index 0000000000..06fce2b2ef --- /dev/null +++ b/dom/file/tests/worker_bug1507893.js @@ -0,0 +1,5 @@ +onmessage = e => { + fetch(e.data) + .then(r => r.blob()) + .then(blob => postMessage(blob)); +}; diff --git a/dom/file/tests/worker_bug1742540.js b/dom/file/tests/worker_bug1742540.js new file mode 100644 index 0000000000..c0330ccb56 --- /dev/null +++ b/dom/file/tests/worker_bug1742540.js @@ -0,0 +1,5 @@ +onmessage = e => { + let file = e.data.file; + let port = e.data.port; + port.postMessage(e.data.message); +}; diff --git a/dom/file/tests/worker_fileReader.js b/dom/file/tests/worker_fileReader.js new file mode 100644 index 0000000000..2e8408d1bb --- /dev/null +++ b/dom/file/tests/worker_fileReader.js @@ -0,0 +1,30 @@ +importScripts("common_fileReader.js"); + +function ok(a, msg) { + postMessage({ type: "check", msg, status: !!a }); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +onmessage = event => { + let p; + + if (event.data.tests == "basic") { + p = runBasicTests(event.data.data); + } else if (event.data.tests == "encoding") { + p = runEncodingTests(event.data.data); + } else if (event.data.tests == "twice") { + p = runTwiceTests(event.data.data); + } else if (event.data.tests == "other") { + p = runOtherTests(event.data.data); + } else { + postMessage({ type: "error" }); + return; + } + + p.then(() => { + postMessage({ type: "finish" }); + }); +}; diff --git a/dom/file/tests/xpcshell.ini b/dom/file/tests/xpcshell.ini new file mode 100644 index 0000000000..2f817948d3 --- /dev/null +++ b/dom/file/tests/xpcshell.ini @@ -0,0 +1,8 @@ +[DEFAULT] + +[test_bloburi.js] +[test_createFile.js] +[test_ipc_messagemanager_blob.js] +skip-if = os == "android" +[test_agentcluster_bloburl.js] +skip-if = os == "android" diff --git a/dom/file/uri/BlobURL.cpp b/dom/file/uri/BlobURL.cpp new file mode 100644 index 0000000000..b3afd60437 --- /dev/null +++ b/dom/file/uri/BlobURL.cpp @@ -0,0 +1,171 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIClassInfoImpl.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" + +#include "mozilla/dom/BlobURL.h" +#include "mozilla/dom/BlobURLProtocolHandler.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/URIUtils.h" + +using namespace mozilla::dom; + +static NS_DEFINE_CID(kThisSimpleURIImplementationCID, + NS_THIS_SIMPLEURI_IMPLEMENTATION_CID); + +NS_IMPL_ADDREF_INHERITED(BlobURL, mozilla::net::nsSimpleURI) +NS_IMPL_RELEASE_INHERITED(BlobURL, mozilla::net::nsSimpleURI) + +NS_IMPL_CLASSINFO(BlobURL, nullptr, nsIClassInfo::THREADSAFE, + NS_HOSTOBJECTURI_CID); +// Empty CI getter. We only need nsIClassInfo for Serialization +NS_IMPL_CI_INTERFACE_GETTER0(BlobURL) + +NS_INTERFACE_MAP_BEGIN(BlobURL) + if (aIID.Equals(kHOSTOBJECTURICID)) + foundInterface = static_cast<nsIURI*>(this); + else if (aIID.Equals(kThisSimpleURIImplementationCID)) { + // Need to return explicitly here, because if we just set foundInterface + // to null the NS_INTERFACE_MAP_END_INHERITING will end up calling into + // nsSimplURI::QueryInterface and finding something for this CID. + *aInstancePtr = nullptr; + return NS_NOINTERFACE; + } else + NS_IMPL_QUERY_CLASSINFO(BlobURL) +NS_INTERFACE_MAP_END_INHERITING(mozilla::net::nsSimpleURI) + +BlobURL::BlobURL() : mRevoked(false) {} + +// nsISerializable methods: + +NS_IMETHODIMP +BlobURL::Read(nsIObjectInputStream* aStream) { + MOZ_ASSERT_UNREACHABLE("Use nsIURIMutator.read() instead"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult BlobURL::ReadPrivate(nsIObjectInputStream* aStream) { + nsresult rv = mozilla::net::nsSimpleURI::ReadPrivate(aStream); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aStream->ReadBoolean(&mRevoked); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +BlobURL::Write(nsIObjectOutputStream* aStream) { + nsresult rv = mozilla::net::nsSimpleURI::Write(aStream); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aStream->WriteBoolean(mRevoked); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP_(void) +BlobURL::Serialize(mozilla::ipc::URIParams& aParams) { + using namespace mozilla::ipc; + + HostObjectURIParams hostParams; + URIParams simpleParams; + + mozilla::net::nsSimpleURI::Serialize(simpleParams); + hostParams.simpleParams() = simpleParams; + + hostParams.revoked() = mRevoked; + + aParams = hostParams; +} + +bool BlobURL::Deserialize(const mozilla::ipc::URIParams& aParams) { + using namespace mozilla::ipc; + + if (aParams.type() != URIParams::THostObjectURIParams) { + NS_ERROR("Received unknown parameters from the other process!"); + return false; + } + + const HostObjectURIParams& hostParams = aParams.get_HostObjectURIParams(); + + if (!mozilla::net::nsSimpleURI::Deserialize(hostParams.simpleParams())) { + return false; + } + + mRevoked = hostParams.revoked(); + return true; +} + +nsresult BlobURL::SetScheme(const nsACString& aScheme) { + // Disallow setting the scheme, since that could cause us to be associated + // with a different protocol handler. + return NS_ERROR_FAILURE; +} + +// nsIURI methods: +nsresult BlobURL::CloneInternal( + mozilla::net::nsSimpleURI::RefHandlingEnum aRefHandlingMode, + const nsACString& newRef, nsIURI** aClone) { + nsCOMPtr<nsIURI> simpleClone; + nsresult rv = mozilla::net::nsSimpleURI::CloneInternal( + aRefHandlingMode, newRef, getter_AddRefs(simpleClone)); + NS_ENSURE_SUCCESS(rv, rv); + +#ifdef DEBUG + RefPtr<BlobURL> uriCheck; + rv = simpleClone->QueryInterface(kHOSTOBJECTURICID, getter_AddRefs(uriCheck)); + MOZ_ASSERT(NS_SUCCEEDED(rv) && uriCheck); +#endif + + BlobURL* u = static_cast<BlobURL*>(simpleClone.get()); + u->mRevoked = mRevoked; + + simpleClone.forget(aClone); + return NS_OK; +} + +/* virtual */ +nsresult BlobURL::EqualsInternal( + nsIURI* aOther, mozilla::net::nsSimpleURI::RefHandlingEnum aRefHandlingMode, + bool* aResult) { + if (!aOther) { + *aResult = false; + return NS_OK; + } + + RefPtr<BlobURL> otherUri; + aOther->QueryInterface(kHOSTOBJECTURICID, getter_AddRefs(otherUri)); + if (!otherUri) { + *aResult = false; + return NS_OK; + } + + // Compare the member data that our base class knows about. + *aResult = + mozilla::net::nsSimpleURI::EqualsInternal(otherUri, aRefHandlingMode); + + // We don't want to compare the revoked flag. + return NS_OK; +} + +// Queries this list of interfaces. If none match, it queries mURI. +NS_IMPL_NSIURIMUTATOR_ISUPPORTS(BlobURL::Mutator, nsIURISetters, nsIURIMutator, + nsISerializable, nsIBlobURLMutator) + +NS_IMETHODIMP +BlobURL::Mutate(nsIURIMutator** aMutator) { + RefPtr<BlobURL::Mutator> mutator = new BlobURL::Mutator(); + nsresult rv = mutator->InitFromURI(this); + if (NS_FAILED(rv)) { + return rv; + } + mutator.forget(aMutator); + return NS_OK; +} diff --git a/dom/file/uri/BlobURL.h b/dom/file/uri/BlobURL.h new file mode 100644 index 0000000000..1e7a91daa4 --- /dev/null +++ b/dom/file/uri/BlobURL.h @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_BlobURL_h +#define mozilla_dom_BlobURL_h + +#include "nsCOMPtr.h" +#include "nsISerializable.h" +#include "nsSimpleURI.h" +#include "prtime.h" + +#define NS_HOSTOBJECTURI_CID \ + { \ + 0xf5475c51, 0x59a7, 0x4757, { \ + 0xb3, 0xd9, 0xe2, 0x11, 0xa9, 0x41, 0x08, 0x72 \ + } \ + } + +#define NS_IBLOBURLMUTATOR_IID \ + { \ + 0xf91e646d, 0xe87b, 0x485e, { \ + 0xbb, 0xc8, 0x0e, 0x8a, 0x2e, 0xe9, 0x87, 0xa9 \ + } \ + } + +class NS_NO_VTABLE nsIBlobURLMutator : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IBLOBURLMUTATOR_IID) + NS_IMETHOD SetRevoked(bool aRevoked) = 0; +}; + +inline NS_DEFINE_CID(kHOSTOBJECTURICID, NS_HOSTOBJECTURI_CID); + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIBlobURLMutator, NS_IBLOBURLMUTATOR_IID) + +namespace mozilla::dom { + +/** + * These URIs refer to host objects with "blob" scheme. + */ +class BlobURL final : public mozilla::net::nsSimpleURI { + private: + BlobURL(); + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSISERIALIZABLE + + // Override CloneInternal() and EqualsInternal() + nsresult CloneInternal(RefHandlingEnum aRefHandlingMode, + const nsACString& newRef, nsIURI** aClone) override; + nsresult EqualsInternal(nsIURI* aOther, RefHandlingEnum aRefHandlingMode, + bool* aResult) override; + NS_IMETHOD_(void) Serialize(mozilla::ipc::URIParams& aParams) override; + + // Override StartClone to hand back a BlobURL + mozilla::net::nsSimpleURI* StartClone(RefHandlingEnum refHandlingMode, + const nsACString& newRef) override { + BlobURL* url = new BlobURL(); + SetRefOnClone(url, refHandlingMode, newRef); + return url; + } + + bool Revoked() const { return mRevoked; } + + NS_IMETHOD Mutate(nsIURIMutator** _retval) override; + + private: + ~BlobURL() override = default; + + nsresult SetScheme(const nsACString& aProtocol) override; + bool Deserialize(const mozilla::ipc::URIParams&); + nsresult ReadPrivate(nsIObjectInputStream* stream); + + bool mRevoked; + + public: + class Mutator final : public nsIURIMutator, + public BaseURIMutator<BlobURL>, + public nsIBlobURLMutator, + public nsISerializable { + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI) + NS_DEFINE_NSIMUTATOR_COMMON + + NS_IMETHOD + Write(nsIObjectOutputStream* aOutputStream) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + + [[nodiscard]] NS_IMETHOD Read(nsIObjectInputStream* aStream) override { + return InitFromInputStream(aStream); + } + + NS_IMETHOD SetRevoked(bool aRevoked) override { + mURI->mRevoked = aRevoked; + return NS_OK; + } + + Mutator() = default; + + private: + ~Mutator() = default; + + friend class BlobURL; + }; + + friend BaseURIMutator<BlobURL>; +}; + +#define NS_HOSTOBJECTURIMUTATOR_CID \ + { \ + 0xbbe50ef2, 0x80eb, 0x469d, { \ + 0xb7, 0x0d, 0x02, 0x85, 0x82, 0x75, 0x38, 0x9f \ + } \ + } + +} // namespace mozilla::dom + +#endif /* mozilla_dom_BlobURL_h */ diff --git a/dom/file/uri/BlobURLChannel.cpp b/dom/file/uri/BlobURLChannel.cpp new file mode 100644 index 0000000000..6a7d4f1be6 --- /dev/null +++ b/dom/file/uri/BlobURLChannel.cpp @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BlobURLChannel.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/BlobURL.h" +#include "mozilla/dom/BlobURLInputStream.h" + +using namespace mozilla::dom; + +BlobURLChannel::BlobURLChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo) + : mContentStreamOpened(false) { + SetURI(aURI); + SetOriginalURI(aURI); + SetLoadInfo(aLoadInfo); + + // If we're sandboxed, make sure to clear any owner the channel + // might already have. + if (aLoadInfo && aLoadInfo->GetLoadingSandboxed()) { + SetOwner(nullptr); + } +} + +BlobURLChannel::~BlobURLChannel() = default; + +NS_IMETHODIMP +BlobURLChannel::SetContentType(const nsACString& aContentType) { + // If the blob type is empty, set the content type of the channel to the + // empty string. + if (aContentType.IsEmpty()) { + mContentType.Truncate(); + return NS_OK; + } + + return nsBaseChannel::SetContentType(aContentType); +} + +nsresult BlobURLChannel::OpenContentStream(bool aAsync, + nsIInputStream** aResult, + nsIChannel** aChannel) { + if (mContentStreamOpened) { + return NS_ERROR_ALREADY_OPENED; + } + + mContentStreamOpened = true; + + nsCOMPtr<nsIURI> uri; + nsresult rv = GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI); + + RefPtr<BlobURL> blobURL; + rv = uri->QueryInterface(kHOSTOBJECTURICID, getter_AddRefs(blobURL)); + + if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(!blobURL)) { + return NS_ERROR_MALFORMED_URI; + } + + if (blobURL->Revoked()) { +#ifdef MOZ_WIDGET_ANDROID + nsCOMPtr<nsILoadInfo> loadInfo; + GetLoadInfo(getter_AddRefs(loadInfo)); + // if the channel was not triggered by the system principal, + // then we return here because the URL had been revoked + if (loadInfo && !loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) { + return NS_ERROR_MALFORMED_URI; + } +#else + return NS_ERROR_MALFORMED_URI; +#endif + } + + nsCOMPtr<nsIInputStream> inputStream = + BlobURLInputStream::Create(this, blobURL); + if (NS_WARN_IF(!inputStream)) { + return NS_ERROR_MALFORMED_URI; + } + + EnableSynthesizedProgressEvents(true); + + inputStream.forget(aResult); + + return NS_OK; +} diff --git a/dom/file/uri/BlobURLChannel.h b/dom/file/uri/BlobURLChannel.h new file mode 100644 index 0000000000..232537171e --- /dev/null +++ b/dom/file/uri/BlobURLChannel.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_BlobURLChannel_h +#define mozilla_dom_BlobURLChannel_h + +#include "nsBaseChannel.h" +#include "nsCOMPtr.h" +#include "nsIInputStream.h" + +class nsIURI; + +namespace mozilla::dom { + +class BlobImpl; + +class BlobURLChannel final : public nsBaseChannel { + public: + BlobURLChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo); + + NS_IMETHOD SetContentType(const nsACString& aContentType) override; + + private: + ~BlobURLChannel() override; + + nsresult OpenContentStream(bool aAsync, nsIInputStream** aResult, + nsIChannel** aChannel) override; + + bool mContentStreamOpened; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_BlobURLChannel_h */ diff --git a/dom/file/uri/BlobURLInputStream.cpp b/dom/file/uri/BlobURLInputStream.cpp new file mode 100644 index 0000000000..7610281afc --- /dev/null +++ b/dom/file/uri/BlobURLInputStream.cpp @@ -0,0 +1,589 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BlobURLInputStream.h" +#include "BlobURL.h" +#include "BlobURLChannel.h" +#include "BlobURLProtocolHandler.h" + +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "nsStreamUtils.h" +#include "nsMimeTypes.h" + +namespace mozilla::dom { + +NS_IMPL_ADDREF(BlobURLInputStream); +NS_IMPL_RELEASE(BlobURLInputStream); + +NS_INTERFACE_MAP_BEGIN(BlobURLInputStream) + NS_INTERFACE_MAP_ENTRY(nsIInputStream) + NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStream) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamLength) + NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStreamLength) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAsyncInputStream) +NS_INTERFACE_MAP_END + +/* static */ +already_AddRefed<nsIInputStream> BlobURLInputStream::Create( + BlobURLChannel* const aChannel, BlobURL* const aBlobURL) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!aChannel) || NS_WARN_IF(!aBlobURL)) { + return nullptr; + } + + nsAutoCString spec; + + nsresult rv = aBlobURL->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return MakeAndAddRef<BlobURLInputStream>(aChannel, spec); +} + +// from nsIInputStream interface +NS_IMETHODIMP BlobURLInputStream::Close() { + return CloseWithStatus(NS_BASE_STREAM_CLOSED); +} + +NS_IMETHODIMP BlobURLInputStream::Available(uint64_t* aLength) { + MutexAutoLock lock(mStateMachineMutex); + + if (mState == State::ERROR) { + MOZ_ASSERT(NS_FAILED(mError)); + return mError; + } + + if (mState == State::CLOSED) { + return NS_BASE_STREAM_CLOSED; + } + + if (mState == State::READY) { + MOZ_ASSERT(mAsyncInputStream); + return mAsyncInputStream->Available(aLength); + } + + return NS_OK; +} + +NS_IMETHODIMP BlobURLInputStream::StreamStatus() { + MutexAutoLock lock(mStateMachineMutex); + + if (mState == State::ERROR) { + MOZ_ASSERT(NS_FAILED(mError)); + return mError; + } + + if (mState == State::CLOSED) { + return NS_BASE_STREAM_CLOSED; + } + + if (mState == State::READY) { + MOZ_ASSERT(mAsyncInputStream); + return mAsyncInputStream->StreamStatus(); + } + + return NS_OK; +} + +NS_IMETHODIMP BlobURLInputStream::Read(char* aBuffer, uint32_t aCount, + uint32_t* aReadCount) { + MutexAutoLock lock(mStateMachineMutex); + if (mState == State::ERROR) { + MOZ_ASSERT(NS_FAILED(mError)); + return mError; + } + + // Read() should not return NS_BASE_STREAM_CLOSED if stream is closed. + // A read count of 0 should indicate closed or consumed stream. + // See: + // https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/xpcom/io/nsIInputStream.idl#104 + if (mState == State::CLOSED) { + *aReadCount = 0; + return NS_OK; + } + + if (mState == State::READY) { + MOZ_ASSERT(mAsyncInputStream); + nsresult rv = mAsyncInputStream->Read(aBuffer, aCount, aReadCount); + if (NS_SUCCEEDED(rv) && aReadCount && !*aReadCount) { + mState = State::CLOSED; + ReleaseUnderlyingStream(lock); + } + return rv; + } + + return NS_BASE_STREAM_WOULD_BLOCK; +} + +NS_IMETHODIMP BlobURLInputStream::ReadSegments(nsWriteSegmentFun aWriter, + void* aClosure, uint32_t aCount, + uint32_t* aResult) { + // This means the caller will have to wrap the stream in an + // nsBufferedInputStream in order to use ReadSegments + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP BlobURLInputStream::IsNonBlocking(bool* aNonBlocking) { + *aNonBlocking = true; + return NS_OK; +} + +// from nsIAsyncInputStream interface +NS_IMETHODIMP BlobURLInputStream::CloseWithStatus(nsresult aStatus) { + MutexAutoLock lock(mStateMachineMutex); + if (mState == State::READY) { + MOZ_ASSERT(mAsyncInputStream); + mAsyncInputStream->CloseWithStatus(aStatus); + } + + mState = State::CLOSED; + ReleaseUnderlyingStream(lock); + return NS_OK; +} + +NS_IMETHODIMP BlobURLInputStream::AsyncWait(nsIInputStreamCallback* aCallback, + uint32_t aFlags, + uint32_t aRequestedCount, + nsIEventTarget* aEventTarget) { + MutexAutoLock lock(mStateMachineMutex); + + if (mState == State::ERROR) { + MOZ_ASSERT(NS_FAILED(mError)); + return NS_ERROR_FAILURE; + } + + // Pre-empting a valid callback with another is not allowed. + if (NS_WARN_IF(mAsyncWaitCallback && aCallback && + mAsyncWaitCallback != aCallback)) { + return NS_ERROR_FAILURE; + } + + mAsyncWaitTarget = aEventTarget; + mAsyncWaitRequestedCount = aRequestedCount; + mAsyncWaitFlags = aFlags; + mAsyncWaitCallback = aCallback; + + if (mState == State::INITIAL) { + mState = State::WAITING; + // RetrieveBlobData will execute NotifyWWaitTarget() when retrieve succeeds + // or fails + if (NS_IsMainThread()) { + RetrieveBlobData(lock); + return NS_OK; + } + + nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod( + "BlobURLInputStream::CallRetrieveBlobData", this, + &BlobURLInputStream::CallRetrieveBlobData); + NS_DispatchToMainThread(runnable.forget(), NS_DISPATCH_NORMAL); + return NS_OK; + } + + if (mState == State::WAITING) { + // RetrieveBlobData is already in progress and will execute + // NotifyWaitTargets when retrieve succeeds or fails + return NS_OK; + } + + if (mState == State::READY) { + // Ask the blob's input stream if reading is possible or not + return mAsyncInputStream->AsyncWait( + mAsyncWaitCallback ? this : nullptr, mAsyncWaitFlags, + mAsyncWaitRequestedCount, mAsyncWaitTarget); + } + + MOZ_ASSERT(mState == State::CLOSED); + NotifyWaitTargets(lock); + return NS_OK; +} + +// from nsIInputStreamLength interface +NS_IMETHODIMP BlobURLInputStream::Length(int64_t* aLength) { + MutexAutoLock lock(mStateMachineMutex); + + if (mState == State::CLOSED) { + return NS_BASE_STREAM_CLOSED; + } + + if (mState == State::ERROR) { + MOZ_ASSERT(NS_FAILED(mError)); + return NS_ERROR_FAILURE; + } + + if (mState == State::READY) { + *aLength = mBlobSize; + return NS_OK; + } + return NS_BASE_STREAM_WOULD_BLOCK; +} + +// from nsIAsyncInputStreamLength interface +NS_IMETHODIMP BlobURLInputStream::AsyncLengthWait( + nsIInputStreamLengthCallback* aCallback, nsIEventTarget* aEventTarget) { + MutexAutoLock lock(mStateMachineMutex); + + if (mState == State::ERROR) { + MOZ_ASSERT(NS_FAILED(mError)); + return mError; + } + + // Pre-empting a valid callback with another is not allowed. + if (mAsyncLengthWaitCallback && aCallback) { + return NS_ERROR_FAILURE; + } + + mAsyncLengthWaitTarget = aEventTarget; + mAsyncLengthWaitCallback = aCallback; + + if (mState == State::INITIAL) { + mState = State::WAITING; + // RetrieveBlobData will execute NotifyWWaitTarget() when retrieve succeeds + // or fails + if (NS_IsMainThread()) { + RetrieveBlobData(lock); + return NS_OK; + } + + nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod( + "BlobURLInputStream::CallRetrieveBlobData", this, + &BlobURLInputStream::CallRetrieveBlobData); + NS_DispatchToMainThread(runnable.forget(), NS_DISPATCH_NORMAL); + return NS_OK; + } + + if (mState == State::WAITING) { + // RetrieveBlobData is already in progress and will execute + // NotifyWaitTargets when retrieve succeeds or fails + return NS_OK; + } + + // Since here the state must be READY (in which case the size of the blob is + // already known) or CLOSED, callback can be called immediately + NotifyWaitTargets(lock); + return NS_OK; +} + +// from nsIInputStreamCallback interface +NS_IMETHODIMP BlobURLInputStream::OnInputStreamReady( + nsIAsyncInputStream* aStream) { + nsCOMPtr<nsIInputStreamCallback> callback; + + { + MutexAutoLock lock(mStateMachineMutex); + MOZ_ASSERT_IF(mAsyncInputStream, aStream == mAsyncInputStream); + + // aborted in the meantime + if (!mAsyncWaitCallback) { + return NS_OK; + } + + mAsyncWaitCallback.swap(callback); + mAsyncWaitTarget = nullptr; + } + + MOZ_ASSERT(callback); + return callback->OnInputStreamReady(this); +} + +// from nsIInputStreamLengthCallback interface +NS_IMETHODIMP BlobURLInputStream::OnInputStreamLengthReady( + nsIAsyncInputStreamLength* aStream, int64_t aLength) { + nsCOMPtr<nsIInputStreamLengthCallback> callback; + { + MutexAutoLock lock(mStateMachineMutex); + + // aborted in the meantime + if (!mAsyncLengthWaitCallback) { + return NS_OK; + } + + mAsyncLengthWaitCallback.swap(callback); + mAsyncLengthWaitCallback = nullptr; + } + + return callback->OnInputStreamLengthReady(this, aLength); +} + +// private: +BlobURLInputStream::~BlobURLInputStream() { + if (mChannel) { + NS_ReleaseOnMainThread("BlobURLInputStream::mChannel", mChannel.forget()); + } +} + +BlobURLInputStream::BlobURLInputStream(BlobURLChannel* const aChannel, + nsACString& aBlobURLSpec) + : mChannel(aChannel), + mBlobURLSpec(std::move(aBlobURLSpec)), + mStateMachineMutex("BlobURLInputStream::mStateMachineMutex"), + mState(State::INITIAL), + mError(NS_OK), + mBlobSize(-1), + mAsyncWaitFlags(), + mAsyncWaitRequestedCount() {} + +void BlobURLInputStream::WaitOnUnderlyingStream( + const MutexAutoLock& aProofOfLock) { + if (mAsyncWaitCallback || mAsyncWaitTarget) { + // AsyncWait should be called on the underlying stream + mAsyncInputStream->AsyncWait(mAsyncWaitCallback ? this : nullptr, + mAsyncWaitFlags, mAsyncWaitRequestedCount, + mAsyncWaitTarget); + } + + if (mAsyncLengthWaitCallback || mAsyncLengthWaitTarget) { + // AsyncLengthWait should be called on the underlying stream + nsCOMPtr<nsIAsyncInputStreamLength> asyncStreamLength = + do_QueryInterface(mAsyncInputStream); + if (asyncStreamLength) { + asyncStreamLength->AsyncLengthWait( + mAsyncLengthWaitCallback ? this : nullptr, mAsyncLengthWaitTarget); + } + } +} + +void BlobURLInputStream::CallRetrieveBlobData() { + MutexAutoLock lock(mStateMachineMutex); + RetrieveBlobData(lock); +} + +void BlobURLInputStream::RetrieveBlobData(const MutexAutoLock& aProofOfLock) { + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); + + MOZ_ASSERT(mState == State::WAITING); + + auto cleanupOnEarlyExit = MakeScopeExit([&] { + mState = State::ERROR; + mError = NS_ERROR_FAILURE; + NS_ReleaseOnMainThread("BlobURLInputStream::mChannel", mChannel.forget()); + NotifyWaitTargets(aProofOfLock); + }); + + nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo(); + nsCOMPtr<nsIPrincipal> triggeringPrincipal; + nsCOMPtr<nsIPrincipal> loadingPrincipal; + if (NS_WARN_IF(NS_FAILED(loadInfo->GetTriggeringPrincipal( + getter_AddRefs(triggeringPrincipal)))) || + NS_WARN_IF(!triggeringPrincipal)) { + NS_WARNING("Failed to get owning channel's triggering principal"); + return; + } + + if (NS_WARN_IF(NS_FAILED( + loadInfo->GetLoadingPrincipal(getter_AddRefs(loadingPrincipal))))) { + NS_WARNING("Failed to get owning channel's loading principal"); + return; + } + + Maybe<nsID> agentClusterId; + Maybe<ClientInfo> clientInfo = loadInfo->GetClientInfo(); + if (clientInfo.isSome()) { + agentClusterId = clientInfo->AgentClusterId(); + } + + if (XRE_IsParentProcess() || !BlobURLSchemeIsHTTPOrHTTPS(mBlobURLSpec)) { + RefPtr<BlobImpl> blobImpl; + + // Since revoked blobs are also retrieved, it is possible that the blob no + // longer exists (due to the 5 second timeout) when execution reaches here + if (!BlobURLProtocolHandler::GetDataEntry( + mBlobURLSpec, getter_AddRefs(blobImpl), loadingPrincipal, + triggeringPrincipal, loadInfo->GetOriginAttributes(), + loadInfo->GetInnerWindowID(), agentClusterId, + true /* AlsoIfRevoked */)) { + NS_WARNING("Failed to get data entry principal. URL revoked?"); + return; + } + + if (NS_WARN_IF( + NS_FAILED(StoreBlobImplStream(blobImpl.forget(), aProofOfLock)))) { + return; + } + + mState = State::READY; + + // By design, execution can only reach here when a caller has called + // AsyncWait or AsyncLengthWait on this stream. The underlying stream is + // valid, but the caller should not be informed until that stream has data + // to read or it is closed. + WaitOnUnderlyingStream(aProofOfLock); + + cleanupOnEarlyExit.release(); + return; + } + + ContentChild* contentChild{ContentChild::GetSingleton()}; + MOZ_ASSERT(contentChild); + + const RefPtr<BlobURLInputStream> self = this; + + cleanupOnEarlyExit.release(); + + contentChild + ->SendBlobURLDataRequest(mBlobURLSpec, triggeringPrincipal, + loadingPrincipal, + loadInfo->GetOriginAttributes(), + loadInfo->GetInnerWindowID(), agentClusterId) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self](const BlobURLDataRequestResult& aResult) { + MutexAutoLock lock(self->mStateMachineMutex); + if (aResult.type() == BlobURLDataRequestResult::TIPCBlob) { + if (self->mState == State::WAITING) { + RefPtr<BlobImpl> blobImpl = + IPCBlobUtils::Deserialize(aResult.get_IPCBlob()); + if (blobImpl && self->StoreBlobImplStream(blobImpl.forget(), + lock) == NS_OK) { + self->mState = State::READY; + // By design, execution can only reach here when a caller has + // called AsyncWait or AsyncLengthWait on this stream. The + // underlying stream is valid, but the caller should not be + // informed until that stream has data to read or it is + // closed. + self->WaitOnUnderlyingStream(lock); + return; + } + } else { + MOZ_ASSERT(self->mState == State::CLOSED); + // Callback can be called immediately + self->NotifyWaitTargets(lock); + return; + } + } + NS_WARNING("Blob data was not retrieved!"); + self->mState = State::ERROR; + self->mError = aResult.type() == BlobURLDataRequestResult::Tnsresult + ? aResult.get_nsresult() + : NS_ERROR_FAILURE; + NS_ReleaseOnMainThread("BlobURLInputStream::mChannel", + self->mChannel.forget()); + self->NotifyWaitTargets(lock); + }, + [self](mozilla::ipc::ResponseRejectReason aReason) { + MutexAutoLock lock(self->mStateMachineMutex); + NS_WARNING("IPC call to SendBlobURLDataRequest failed!"); + self->mState = State::ERROR; + self->mError = NS_ERROR_FAILURE; + NS_ReleaseOnMainThread("BlobURLInputStream::mChannel", + self->mChannel.forget()); + self->NotifyWaitTargets(lock); + }); +} + +nsresult BlobURLInputStream::StoreBlobImplStream( + already_AddRefed<BlobImpl> aBlobImpl, const MutexAutoLock& aProofOfLock) { + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); + const RefPtr<BlobImpl> blobImpl = aBlobImpl; + nsAutoString blobContentType; + nsAutoCString channelContentType; + + blobImpl->GetType(blobContentType); + mChannel->GetContentType(channelContentType); + // A empty content type is the correct channel content type in the case of a + // fetch of a blob where the type was not set. It is invalid in others cases + // such as a XHR (See https://xhr.spec.whatwg.org/#response-mime-type). The + // XMLHttpRequestMainThread will set the channel content type to the correct + // fallback value before this point, so we need to be careful to only override + // it when the blob type is valid. + if (!blobContentType.IsEmpty() || + channelContentType.EqualsLiteral(UNKNOWN_CONTENT_TYPE)) { + mChannel->SetContentType(NS_ConvertUTF16toUTF8(blobContentType)); + } + + auto cleanupOnExit = MakeScopeExit([&] { mChannel = nullptr; }); + + if (blobImpl->IsFile()) { + nsAutoString filename; + blobImpl->GetName(filename); + + // Don't overwrite existing name. + nsString ignored; + bool hasName = + NS_SUCCEEDED(mChannel->GetContentDispositionFilename(ignored)); + + if (!filename.IsEmpty() && !hasName) { + mChannel->SetContentDispositionFilename(filename); + } + } + + mozilla::ErrorResult errorResult; + + mBlobSize = blobImpl->GetSize(errorResult); + + if (NS_WARN_IF(errorResult.Failed())) { + return errorResult.StealNSResult(); + } + + mChannel->SetContentLength(mBlobSize); + + nsCOMPtr<nsIInputStream> inputStream; + blobImpl->CreateInputStream(getter_AddRefs(inputStream), errorResult); + + if (NS_WARN_IF(errorResult.Failed())) { + return errorResult.StealNSResult(); + } + + if (NS_WARN_IF(!inputStream)) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = NS_MakeAsyncNonBlockingInputStream( + inputStream.forget(), getter_AddRefs(mAsyncInputStream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!mAsyncInputStream)) { + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_OK; +} + +void BlobURLInputStream::NotifyWaitTargets(const MutexAutoLock& aProofOfLock) { + if (mAsyncWaitCallback) { + auto callback = mAsyncWaitTarget + ? NS_NewInputStreamReadyEvent( + "BlobURLInputStream::OnInputStreamReady", + mAsyncWaitCallback, mAsyncWaitTarget) + : mAsyncWaitCallback; + + mAsyncWaitCallback = nullptr; + mAsyncWaitTarget = nullptr; + callback->OnInputStreamReady(this); + } + + if (mAsyncLengthWaitCallback) { + const RefPtr<BlobURLInputStream> self = this; + nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction( + "BlobURLInputStream::OnInputStreamLengthReady", [self] { + self->mAsyncLengthWaitCallback->OnInputStreamLengthReady( + self, self->mBlobSize); + }); + + mAsyncLengthWaitCallback = nullptr; + + if (mAsyncLengthWaitTarget) { + mAsyncLengthWaitTarget->Dispatch(runnable, NS_DISPATCH_NORMAL); + mAsyncLengthWaitTarget = nullptr; + } else { + runnable->Run(); + } + } +} + +void BlobURLInputStream::ReleaseUnderlyingStream( + const MutexAutoLock& aProofOfLock) { + mAsyncInputStream = nullptr; + mBlobSize = -1; +} + +} // namespace mozilla::dom diff --git a/dom/file/uri/BlobURLInputStream.h b/dom/file/uri/BlobURLInputStream.h new file mode 100644 index 0000000000..b9215e5ffd --- /dev/null +++ b/dom/file/uri/BlobURLInputStream.h @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_BlobURLInputStream_h +#define mozilla_dom_BlobURLInputStream_h + +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/Mutex.h" +#include "nsCOMPtr.h" +#include "nsIAsyncInputStream.h" +#include "nsIInputStreamLength.h" + +namespace mozilla::dom { + +class BlobURL; +class BlobURLChannel; +class BlobURLInputStream final : public nsIAsyncInputStream, + public nsIInputStreamLength, + public nsIAsyncInputStreamLength, + public nsIInputStreamCallback, + public nsIInputStreamLengthCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + NS_DECL_NSIINPUTSTREAMLENGTH + NS_DECL_NSIASYNCINPUTSTREAMLENGTH + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIINPUTSTREAMLENGTHCALLBACK + + static already_AddRefed<nsIInputStream> Create(BlobURLChannel* const aChannel, + BlobURL* const aBlobURL); + + BlobURLInputStream(BlobURLChannel* const aChannel, nsACString& aBlobURLSpec); + + private: + enum class State { INITIAL, READY, WAITING, CLOSED, ERROR }; + + ~BlobURLInputStream(); + + void WaitOnUnderlyingStream(const MutexAutoLock& aProofOfLock); + + // This method should only be used to call RetrieveBlobData in a different + // thread + void CallRetrieveBlobData(); + + void RetrieveBlobData(const MutexAutoLock& aProofOfLock); + + nsresult StoreBlobImplStream(already_AddRefed<BlobImpl> aBlobImpl, + const MutexAutoLock& aProofOfLock); + void NotifyWaitTargets(const MutexAutoLock& aProofOfLock); + void ReleaseUnderlyingStream(const MutexAutoLock& aProofOfLock); + + RefPtr<BlobURLChannel> mChannel; + const nsCString mBlobURLSpec; + + // Non-recursive mutex introduced in order to guard access to mState, mError + // and mAsyncInputStream + Mutex mStateMachineMutex MOZ_UNANNOTATED; + State mState; + // Stores the error code if stream is in error state + nsresult mError; + + int64_t mBlobSize; + + nsCOMPtr<nsIAsyncInputStream> mAsyncInputStream; + nsCOMPtr<nsIInputStreamCallback> mAsyncWaitCallback; + nsCOMPtr<nsIEventTarget> mAsyncWaitTarget; + uint32_t mAsyncWaitFlags; + uint32_t mAsyncWaitRequestedCount; + + nsCOMPtr<nsIInputStreamLengthCallback> mAsyncLengthWaitCallback; + nsCOMPtr<nsIEventTarget> mAsyncLengthWaitTarget; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_BlobURLInputStream_h */ diff --git a/dom/file/uri/BlobURLProtocolHandler.cpp b/dom/file/uri/BlobURLProtocolHandler.cpp new file mode 100644 index 0000000000..756ce01544 --- /dev/null +++ b/dom/file/uri/BlobURLProtocolHandler.cpp @@ -0,0 +1,990 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BlobURLProtocolHandler.h" +#include "BlobURLChannel.h" +#include "mozilla/dom/BlobURL.h" + +#include "mozilla/dom/ChromeUtils.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/MediaSource.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/Maybe.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/Preferences.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "nsClassHashtable.h" +#include "nsContentUtils.h" +#include "nsError.h" +#include "nsIAsyncShutdown.h" +#include "nsIDUtils.h" +#include "nsIException.h" // for nsIStackFrame +#include "nsIMemoryReporter.h" +#include "nsIPrincipal.h" +#include "nsIUUIDGenerator.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" + +#define RELEASING_TIMER 5000 + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +// ----------------------------------------------------------------------- +// Hash table +struct DataInfo { + enum ObjectType { eBlobImpl, eMediaSource }; + + DataInfo(mozilla::dom::BlobImpl* aBlobImpl, nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId) + : mObjectType(eBlobImpl), + mBlobImpl(aBlobImpl), + mPrincipal(aPrincipal), + mAgentClusterId(aAgentClusterId), + mRevoked(false) { + MOZ_ASSERT(aPrincipal); + } + + DataInfo(MediaSource* aMediaSource, nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId) + : mObjectType(eMediaSource), + mMediaSource(aMediaSource), + mPrincipal(aPrincipal), + mAgentClusterId(aAgentClusterId), + mRevoked(false) { + MOZ_ASSERT(aPrincipal); + } + + ObjectType mObjectType; + + RefPtr<BlobImpl> mBlobImpl; + RefPtr<MediaSource> mMediaSource; + + nsCOMPtr<nsIPrincipal> mPrincipal; + Maybe<nsID> mAgentClusterId; + + nsCString mStack; + + // When a blobURL is revoked, we keep it alive for RELEASING_TIMER + // milliseconds in order to support pending operations such as navigation, + // download and so on. + bool mRevoked; +}; + +// The mutex is locked whenever gDataTable is changed, or if gDataTable +// is accessed off-main-thread. +static StaticMutex sMutex MOZ_UNANNOTATED; + +// All changes to gDataTable must happen on the main thread, while locking +// sMutex. Reading from gDataTable on the main thread may happen without +// locking, since no changes are possible. Reading it from another thread +// must also lock sMutex to prevent data races. +static nsClassHashtable<nsCStringHashKey, mozilla::dom::DataInfo>* gDataTable; + +static mozilla::dom::DataInfo* GetDataInfo(const nsACString& aUri, + bool aAlsoIfRevoked = false) { + if (!gDataTable) { + return nullptr; + } + + // Let's remove any fragment from this URI. + int32_t fragmentPos = aUri.FindChar('#'); + + mozilla::dom::DataInfo* res; + if (fragmentPos < 0) { + res = gDataTable->Get(aUri); + } else { + res = gDataTable->Get(StringHead(aUri, fragmentPos)); + } + + if (!aAlsoIfRevoked && res && res->mRevoked) { + return nullptr; + } + + return res; +} + +static mozilla::dom::DataInfo* GetDataInfoFromURI(nsIURI* aURI, + bool aAlsoIfRevoked = false) { + if (!aURI) { + return nullptr; + } + + nsCString spec; + nsresult rv = aURI->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return GetDataInfo(spec, aAlsoIfRevoked); +} + +// Memory reporting for the hash table. +void BroadcastBlobURLRegistration(const nsACString& aURI, + mozilla::dom::BlobImpl* aBlobImpl, + nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aBlobImpl); + MOZ_ASSERT(aPrincipal); + + if (XRE_IsParentProcess()) { + dom::ContentParent::BroadcastBlobURLRegistration( + aURI, aBlobImpl, aPrincipal, aAgentClusterId); + return; + } + + IPCBlob ipcBlob; + nsresult rv = IPCBlobUtils::Serialize(aBlobImpl, ipcBlob); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + dom::ContentChild* cc = dom::ContentChild::GetSingleton(); + (void)NS_WARN_IF(!cc->SendStoreAndBroadcastBlobURLRegistration( + nsCString(aURI), ipcBlob, aPrincipal, aAgentClusterId)); +} + +void BroadcastBlobURLUnregistration(const nsCString& aURI, + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + + if (XRE_IsParentProcess()) { + dom::ContentParent::BroadcastBlobURLUnregistration(aURI, aPrincipal); + return; + } + + dom::ContentChild* cc = dom::ContentChild::GetSingleton(); + if (cc) { + (void)NS_WARN_IF( + !cc->SendUnstoreAndBroadcastBlobURLUnregistration(aURI, aPrincipal)); + } +} + +class BlobURLsReporter final : public nsIMemoryReporter { + public: + NS_DECL_ISUPPORTS + + NS_IMETHOD CollectReports(nsIHandleReportCallback* aCallback, + nsISupports* aData, bool aAnonymize) override { + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + if (!gDataTable) { + return NS_OK; + } + + nsTHashMap<nsPtrHashKey<mozilla::dom::BlobImpl>, uint32_t> refCounts; + + // Determine number of URLs per mozilla::dom::BlobImpl, to handle the case + // where it's > 1. + for (const auto& entry : *gDataTable) { + if (entry.GetWeak()->mObjectType != mozilla::dom::DataInfo::eBlobImpl) { + continue; + } + + mozilla::dom::BlobImpl* blobImpl = entry.GetWeak()->mBlobImpl; + MOZ_ASSERT(blobImpl); + + refCounts.LookupOrInsert(blobImpl, 0) += 1; + } + + for (const auto& entry : *gDataTable) { + nsCStringHashKey::KeyType key = entry.GetKey(); + mozilla::dom::DataInfo* info = entry.GetWeak(); + + if (entry.GetWeak()->mObjectType == mozilla::dom::DataInfo::eBlobImpl) { + mozilla::dom::BlobImpl* blobImpl = entry.GetWeak()->mBlobImpl; + MOZ_ASSERT(blobImpl); + + constexpr auto desc = + "A blob URL allocated with URL.createObjectURL; the referenced " + "blob cannot be freed until all URLs for it have been explicitly " + "invalidated with URL.revokeObjectURL."_ns; + nsAutoCString path, url, owner, specialDesc; + uint64_t size = 0; + uint32_t refCount = 1; + DebugOnly<bool> blobImplWasCounted; + + blobImplWasCounted = refCounts.Get(blobImpl, &refCount); + MOZ_ASSERT(blobImplWasCounted); + MOZ_ASSERT(refCount > 0); + + bool isMemoryFile = blobImpl->IsMemoryFile(); + + if (isMemoryFile) { + ErrorResult rv; + size = blobImpl->GetSize(rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + size = 0; + } + } + + path = isMemoryFile ? "memory-blob-urls/" : "file-blob-urls/"; + BuildPath(path, key, info, aAnonymize); + + if (refCount > 1) { + nsAutoCString addrStr; + + addrStr = "0x"; + addrStr.AppendInt((uint64_t)(mozilla::dom::BlobImpl*)blobImpl, 16); + + path += " "; + path.AppendInt(refCount); + path += "@"; + path += addrStr; + + specialDesc = desc; + specialDesc += "\n\nNOTE: This blob (address "; + specialDesc += addrStr; + specialDesc += ") has "; + specialDesc.AppendInt(refCount); + specialDesc += " URLs."; + if (isMemoryFile) { + specialDesc += " Its size is divided "; + specialDesc += refCount > 2 ? "among" : "between"; + specialDesc += " them in this report."; + } + } + + const nsACString& descString = + specialDesc.IsEmpty() ? static_cast<const nsACString&>(desc) + : static_cast<const nsACString&>(specialDesc); + if (isMemoryFile) { + aCallback->Callback(""_ns, path, KIND_OTHER, UNITS_BYTES, + size / refCount, descString, aData); + } else { + aCallback->Callback(""_ns, path, KIND_OTHER, UNITS_COUNT, 1, + descString, aData); + } + continue; + } + + // Just report the path for the MediaSource. + nsAutoCString path; + path = "media-source-urls/"; + BuildPath(path, key, info, aAnonymize); + + constexpr auto desc = + "An object URL allocated with URL.createObjectURL; the referenced " + "data cannot be freed until all URLs for it have been explicitly " + "invalidated with URL.revokeObjectURL."_ns; + + aCallback->Callback(""_ns, path, KIND_OTHER, UNITS_COUNT, 1, desc, aData); + } + + return NS_OK; + } + + // Initialize info->mStack to record JS stack info, if enabled. + // The string generated here is used in ReportCallback, below. + static void GetJSStackForBlob(mozilla::dom::DataInfo* aInfo) { + nsCString& stack = aInfo->mStack; + MOZ_ASSERT(stack.IsEmpty()); + const uint32_t maxFrames = + Preferences::GetUint("memory.blob_report.stack_frames"); + + if (maxFrames == 0) { + return; + } + + nsCOMPtr<nsIStackFrame> frame = dom::GetCurrentJSStack(maxFrames); + + nsAutoCString origin; + + aInfo->mPrincipal->GetPrePath(origin); + + // If we got a frame, we better have a current JSContext. This is cheating + // a bit; ideally we'd have our caller pass in a JSContext, or have + // GetCurrentJSStack() hand out the JSContext it found. + JSContext* cx = frame ? nsContentUtils::GetCurrentJSContext() : nullptr; + + while (frame) { + nsString fileNameUTF16; + frame->GetFilename(cx, fileNameUTF16); + + int32_t lineNumber = frame->GetLineNumber(cx); + + if (!fileNameUTF16.IsEmpty()) { + NS_ConvertUTF16toUTF8 fileName(fileNameUTF16); + stack += "js("; + if (!origin.IsEmpty()) { + // Make the file name root-relative for conciseness if possible. + const char* originData; + uint32_t originLen; + + originLen = origin.GetData(&originData); + // If fileName starts with origin + "/", cut up to that "/". + if (fileName.Length() >= originLen + 1 && + memcmp(fileName.get(), originData, originLen) == 0 && + fileName[originLen] == '/') { + fileName.Cut(0, originLen); + } + } + fileName.ReplaceChar('/', '\\'); + stack += fileName; + if (lineNumber > 0) { + stack += ", line="; + stack.AppendInt(lineNumber); + } + stack += ")/"; + } + + frame = frame->GetCaller(cx); + } + } + + private: + ~BlobURLsReporter() = default; + + static void BuildPath(nsAutoCString& path, nsCStringHashKey::KeyType aKey, + mozilla::dom::DataInfo* aInfo, bool anonymize) { + nsAutoCString url, owner; + aInfo->mPrincipal->GetAsciiSpec(owner); + if (!owner.IsEmpty()) { + owner.ReplaceChar('/', '\\'); + path += "owner("; + if (anonymize) { + path += "<anonymized>"; + } else { + path += owner; + } + path += ")"; + } else { + path += "owner unknown"; + } + path += "/"; + if (anonymize) { + path += "<anonymized-stack>"; + } else { + path += aInfo->mStack; + } + url = aKey; + url.ReplaceChar('/', '\\'); + if (anonymize) { + path += "<anonymized-url>"; + } else { + path += url; + } + } +}; + +NS_IMPL_ISUPPORTS(BlobURLsReporter, nsIMemoryReporter) + +class ReleasingTimerHolder final : public Runnable, + public nsITimerCallback, + public nsIAsyncShutdownBlocker { + public: + NS_DECL_ISUPPORTS_INHERITED + + static void Create(const nsACString& aURI) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ReleasingTimerHolder> holder = new ReleasingTimerHolder(aURI); + + // BlobURLProtocolHandler::RemoveDataEntry potentially happens late. We are + // prepared to RevokeUri synchronously if we run after XPCOMWillShutdown, + // but we need at least to be able to dispatch to the main thread here. + auto raii = MakeScopeExit([holder] { holder->CancelTimerAndRevokeURI(); }); + + nsresult rv = + SchedulerGroup::Dispatch(TaskCategory::Other, holder.forget()); + NS_ENSURE_SUCCESS_VOID(rv); + + raii.release(); + } + + // Runnable interface + + NS_IMETHOD + Run() override { + RefPtr<ReleasingTimerHolder> self = this; + auto raii = MakeScopeExit([self] { self->CancelTimerAndRevokeURI(); }); + + nsresult rv = NS_NewTimerWithCallback( + getter_AddRefs(mTimer), this, RELEASING_TIMER, nsITimer::TYPE_ONE_SHOT); + NS_ENSURE_SUCCESS(rv, NS_OK); + + nsCOMPtr<nsIAsyncShutdownClient> phase = GetShutdownPhase(); + NS_ENSURE_TRUE(!!phase, NS_OK); + + rv = phase->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), + __LINE__, u"ReleasingTimerHolder shutdown"_ns); + NS_ENSURE_SUCCESS(rv, NS_OK); + + raii.release(); + return NS_OK; + } + + // nsITimerCallback interface + + NS_IMETHOD + Notify(nsITimer* aTimer) override { + RevokeURI(); + return NS_OK; + } + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + using nsINamed::GetName; +#endif + + // nsIAsyncShutdownBlocker interface + + NS_IMETHOD + GetName(nsAString& aName) override { + aName.AssignLiteral("ReleasingTimerHolder for blobURL: "); + aName.Append(NS_ConvertUTF8toUTF16(mURI)); + return NS_OK; + } + + NS_IMETHOD + BlockShutdown(nsIAsyncShutdownClient* aClient) override { + CancelTimerAndRevokeURI(); + return NS_OK; + } + + NS_IMETHOD + GetState(nsIPropertyBag**) override { return NS_OK; } + + private: + explicit ReleasingTimerHolder(const nsACString& aURI) + : Runnable("ReleasingTimerHolder"), mURI(aURI) {} + + ~ReleasingTimerHolder() override = default; + + void RevokeURI() { + // Remove the shutting down blocker + nsCOMPtr<nsIAsyncShutdownClient> phase = GetShutdownPhase(); + if (phase) { + phase->RemoveBlocker(this); + } + + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + mozilla::dom::DataInfo* info = + GetDataInfo(mURI, true /* We care about revoked dataInfo */); + if (!info) { + // Already gone! + return; + } + + MOZ_ASSERT(info->mRevoked); + + StaticMutexAutoLock lock(sMutex); + gDataTable->Remove(mURI); + if (gDataTable->Count() == 0) { + delete gDataTable; + gDataTable = nullptr; + } + } + + void CancelTimerAndRevokeURI() { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + RevokeURI(); + } + + static nsCOMPtr<nsIAsyncShutdownClient> GetShutdownPhase() { + nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService(); + NS_ENSURE_TRUE(!!svc, nullptr); + + nsCOMPtr<nsIAsyncShutdownClient> phase; + nsresult rv = svc->GetXpcomWillShutdown(getter_AddRefs(phase)); + NS_ENSURE_SUCCESS(rv, nullptr); + + return phase; + } + + nsCString mURI; + nsCOMPtr<nsITimer> mTimer; +}; + +NS_IMPL_ISUPPORTS_INHERITED(ReleasingTimerHolder, Runnable, nsITimerCallback, + nsIAsyncShutdownBlocker) + +template <typename T> +static void AddDataEntryInternal(const nsACString& aURI, T aObject, + nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId) { + MOZ_ASSERT(NS_IsMainThread(), "changing gDataTable is main-thread only"); + StaticMutexAutoLock lock(sMutex); + if (!gDataTable) { + gDataTable = new nsClassHashtable<nsCStringHashKey, mozilla::dom::DataInfo>; + } + + mozilla::UniquePtr<mozilla::dom::DataInfo> info = + mozilla::MakeUnique<mozilla::dom::DataInfo>(aObject, aPrincipal, + aAgentClusterId); + BlobURLsReporter::GetJSStackForBlob(info.get()); + + gDataTable->InsertOrUpdate(aURI, std::move(info)); +} + +void BlobURLProtocolHandler::Init(void) { + static bool initialized = false; + + if (!initialized) { + initialized = true; + RegisterStrongMemoryReporter(new BlobURLsReporter()); + } +} + +BlobURLProtocolHandler::BlobURLProtocolHandler() { Init(); } + +BlobURLProtocolHandler::~BlobURLProtocolHandler() = default; + +/* static */ +nsresult BlobURLProtocolHandler::AddDataEntry( + mozilla::dom::BlobImpl* aBlobImpl, nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId, nsACString& aUri) { + MOZ_ASSERT(aBlobImpl); + MOZ_ASSERT(aPrincipal); + + Init(); + + nsresult rv = GenerateURIString(aPrincipal, aUri); + NS_ENSURE_SUCCESS(rv, rv); + + AddDataEntryInternal(aUri, aBlobImpl, aPrincipal, aAgentClusterId); + + BroadcastBlobURLRegistration(aUri, aBlobImpl, aPrincipal, aAgentClusterId); + return NS_OK; +} + +/* static */ +nsresult BlobURLProtocolHandler::AddDataEntry( + MediaSource* aMediaSource, nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId, nsACString& aUri) { + MOZ_ASSERT(aMediaSource); + MOZ_ASSERT(aPrincipal); + + Init(); + + nsresult rv = GenerateURIString(aPrincipal, aUri); + NS_ENSURE_SUCCESS(rv, rv); + + AddDataEntryInternal(aUri, aMediaSource, aPrincipal, aAgentClusterId); + return NS_OK; +} + +/* static */ +void BlobURLProtocolHandler::AddDataEntry(const nsACString& aURI, + nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId, + mozilla::dom::BlobImpl* aBlobImpl) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aBlobImpl); + AddDataEntryInternal(aURI, aBlobImpl, aPrincipal, aAgentClusterId); +} + +/* static */ +bool BlobURLProtocolHandler::ForEachBlobURL( + std::function<bool(mozilla::dom::BlobImpl*, nsIPrincipal*, + const Maybe<nsID>&, const nsACString&, bool aRevoked)>&& + aCb) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!gDataTable) { + return false; + } + + for (const auto& entry : *gDataTable) { + mozilla::dom::DataInfo* info = entry.GetWeak(); + MOZ_ASSERT(info); + + if (info->mObjectType != mozilla::dom::DataInfo::eBlobImpl) { + continue; + } + + MOZ_ASSERT(info->mBlobImpl); + if (!aCb(info->mBlobImpl, info->mPrincipal, info->mAgentClusterId, + entry.GetKey(), info->mRevoked)) { + return false; + } + } + + return true; +} + +/*static */ +void BlobURLProtocolHandler::RemoveDataEntry(const nsACString& aUri, + bool aBroadcastToOtherProcesses) { + MOZ_ASSERT(NS_IsMainThread(), "changing gDataTable is main-thread only"); + if (!gDataTable) { + return; + } + mozilla::dom::DataInfo* info = GetDataInfo(aUri); + if (!info) { + return; + } + + { + StaticMutexAutoLock lock(sMutex); + info->mRevoked = true; + } + + if (aBroadcastToOtherProcesses && + info->mObjectType == mozilla::dom::DataInfo::eBlobImpl) { + BroadcastBlobURLUnregistration(nsCString(aUri), info->mPrincipal); + } + + // The timer will take care of removing the entry for real after + // RELEASING_TIMER milliseconds. In the meantime, the mozilla::dom::DataInfo, + // marked as revoked, will not be exposed. + ReleasingTimerHolder::Create(aUri); +} + +/*static */ +bool BlobURLProtocolHandler::RemoveDataEntry( + const nsACString& aUri, nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId) { + MOZ_ASSERT(NS_IsMainThread(), "changing gDataTable is main-thread only"); + if (!gDataTable) { + return false; + } + + mozilla::dom::DataInfo* info = GetDataInfo(aUri); + if (!info) { + return false; + } + + if (!aPrincipal || !aPrincipal->Subsumes(info->mPrincipal)) { + return false; + } + + if (StaticPrefs::privacy_partition_bloburl_per_agent_cluster() && + aAgentClusterId.isSome() && info->mAgentClusterId.isSome() && + !aAgentClusterId.value().Equals(info->mAgentClusterId.value())) { + return false; + } + + RemoveDataEntry(aUri, true); + return true; +} + +/* static */ +void BlobURLProtocolHandler::RemoveDataEntries() { + MOZ_ASSERT(NS_IsMainThread(), "changing gDataTable is main-thread only"); + StaticMutexAutoLock lock(sMutex); + if (!gDataTable) { + return; + } + + gDataTable->Clear(); + delete gDataTable; + gDataTable = nullptr; +} + +/* static */ +bool BlobURLProtocolHandler::HasDataEntry(const nsACString& aUri) { + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + return !!GetDataInfo(aUri); +} + +/* static */ +nsresult BlobURLProtocolHandler::GenerateURIString(nsIPrincipal* aPrincipal, + nsACString& aUri) { + nsresult rv; + nsCOMPtr<nsIUUIDGenerator> uuidgen = + do_GetService("@mozilla.org/uuid-generator;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsID id; + rv = uuidgen->GenerateUUIDInPlace(&id); + NS_ENSURE_SUCCESS(rv, rv); + + aUri.AssignLiteral(BLOBURI_SCHEME); + aUri.Append(':'); + + if (aPrincipal) { + nsAutoCString origin; + rv = aPrincipal->GetAsciiOrigin(origin); + if (NS_FAILED(rv)) { + origin.AssignLiteral("null"); + } + + aUri.Append(origin); + aUri.Append('/'); + } + + aUri += NSID_TrimBracketsASCII(id); + + return NS_OK; +} + +/* static */ +bool BlobURLProtocolHandler::GetDataEntry( + const nsACString& aUri, mozilla::dom::BlobImpl** aBlobImpl, + nsIPrincipal* aLoadingPrincipal, nsIPrincipal* aTriggeringPrincipal, + const OriginAttributes& aOriginAttributes, uint64_t aInnerWindowId, + const Maybe<nsID>& aAgentClusterId, bool aAlsoIfRevoked) { + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + MOZ_ASSERT(aTriggeringPrincipal); + + if (!gDataTable) { + return false; + } + + mozilla::dom::DataInfo* info = GetDataInfo(aUri, aAlsoIfRevoked); + if (!info) { + return false; + } + + // We want to be sure that we stop the creation of the channel if the blob + // URL is copy-and-pasted on a different context (ex. private browsing or + // containers). + // + // We also allow the system principal to create the channel regardless of + // the OriginAttributes. This is primarily for the benefit of mechanisms + // like the Download API that explicitly create a channel with the system + // principal and which is never mutated to have a non-zero + // mPrivateBrowsingId or container. + + if ((NS_WARN_IF(!aLoadingPrincipal) || + !aLoadingPrincipal->IsSystemPrincipal()) && + NS_WARN_IF(!ChromeUtils::IsOriginAttributesEqualIgnoringFPD( + aOriginAttributes, + BasePrincipal::Cast(info->mPrincipal)->OriginAttributesRef()))) { + return false; + } + + if (NS_WARN_IF(!aTriggeringPrincipal->Subsumes(info->mPrincipal))) { + return false; + } + + // BlobURLs are openable on the same agent-cluster-id only. + if (StaticPrefs::privacy_partition_bloburl_per_agent_cluster() && + aAgentClusterId.isSome() && info->mAgentClusterId.isSome() && + NS_WARN_IF(!aAgentClusterId->Equals(info->mAgentClusterId.value()))) { + nsAutoString localizedMsg; + AutoTArray<nsString, 1> param; + CopyUTF8toUTF16(aUri, *param.AppendElement()); + nsresult rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "BlobDifferentClusterError", param, + localizedMsg); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + nsContentUtils::ReportToConsoleByWindowID( + localizedMsg, nsIScriptError::errorFlag, "DOM"_ns, aInnerWindowId); + return false; + } + + RefPtr<mozilla::dom::BlobImpl> blobImpl = info->mBlobImpl; + blobImpl.forget(aBlobImpl); + + return true; +} + +/* static */ +void BlobURLProtocolHandler::Traverse( + const nsACString& aUri, nsCycleCollectionTraversalCallback& aCallback) { + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + if (!gDataTable) { + return; + } + + mozilla::dom::DataInfo* res; + gDataTable->Get(aUri, &res); + if (!res) { + return; + } + + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME( + aCallback, "BlobURLProtocolHandler mozilla::dom::DataInfo.mBlobImpl"); + aCallback.NoteXPCOMChild(res->mBlobImpl); + + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME( + aCallback, "BlobURLProtocolHandler mozilla::dom::DataInfo.mMediaSource"); + aCallback.NoteXPCOMChild(static_cast<EventTarget*>(res->mMediaSource)); +} + +NS_IMPL_ISUPPORTS(BlobURLProtocolHandler, nsIProtocolHandler, + nsISupportsWeakReference) + +/* static */ nsresult BlobURLProtocolHandler::CreateNewURI( + const nsACString& aSpec, const char* aCharset, nsIURI* aBaseURI, + nsIURI** aResult) { + *aResult = nullptr; + + // This method can be called on any thread, which is why we lock the mutex + // for read access to gDataTable. + bool revoked = true; + { + StaticMutexAutoLock lock(sMutex); + mozilla::dom::DataInfo* info = GetDataInfo(aSpec); + if (info && info->mObjectType == mozilla::dom::DataInfo::eBlobImpl) { + revoked = info->mRevoked; + } + } + + return NS_MutateURI(new BlobURL::Mutator()) + .SetSpec(aSpec) + .Apply(&nsIBlobURLMutator::SetRevoked, revoked) + .Finalize(aResult); +} + +NS_IMETHODIMP +BlobURLProtocolHandler::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIChannel** aResult) { + auto channel = MakeRefPtr<BlobURLChannel>(aURI, aLoadInfo); + channel.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +BlobURLProtocolHandler::AllowPort(int32_t port, const char* scheme, + bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +BlobURLProtocolHandler::GetScheme(nsACString& result) { + result.AssignLiteral(BLOBURI_SCHEME); + return NS_OK; +} + +/* static */ +bool BlobURLProtocolHandler::GetBlobURLPrincipal(nsIURI* aURI, + nsIPrincipal** aPrincipal) { + MOZ_ASSERT(aURI); + MOZ_ASSERT(aPrincipal); + + RefPtr<BlobURL> blobURL; + nsresult rv = + aURI->QueryInterface(kHOSTOBJECTURICID, getter_AddRefs(blobURL)); + if (NS_FAILED(rv) || !blobURL) { + return false; + } + + StaticMutexAutoLock lock(sMutex); + mozilla::dom::DataInfo* info = + GetDataInfoFromURI(aURI, true /*aAlsoIfRevoked */); + if (!info || info->mObjectType != mozilla::dom::DataInfo::eBlobImpl || + !info->mBlobImpl) { + return false; + } + + nsCOMPtr<nsIPrincipal> principal; + + if (blobURL->Revoked()) { + principal = NullPrincipal::Create( + BasePrincipal::Cast(info->mPrincipal)->OriginAttributesRef()); + } else { + principal = info->mPrincipal; + } + + principal.forget(aPrincipal); + return true; +} + +bool BlobURLProtocolHandler::IsBlobURLBroadcastPrincipal( + nsIPrincipal* aPrincipal) { + return aPrincipal->IsSystemPrincipal() || + aPrincipal->GetIsAddonOrExpandedAddonPrincipal(); +} + +} // namespace dom +} // namespace mozilla + +nsresult NS_GetBlobForBlobURI(nsIURI* aURI, mozilla::dom::BlobImpl** aBlob) { + *aBlob = nullptr; + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + mozilla::dom::DataInfo* info = + mozilla::dom::GetDataInfoFromURI(aURI, false /* aAlsoIfRevoked */); + if (!info || info->mObjectType != mozilla::dom::DataInfo::eBlobImpl) { + return NS_ERROR_DOM_BAD_URI; + } + + RefPtr<mozilla::dom::BlobImpl> blob = info->mBlobImpl; + blob.forget(aBlob); + return NS_OK; +} + +nsresult NS_GetBlobForBlobURISpec(const nsACString& aSpec, + mozilla::dom::BlobImpl** aBlob, + bool aAlsoIfRevoked) { + *aBlob = nullptr; + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + + mozilla::dom::DataInfo* info = + mozilla::dom::GetDataInfo(aSpec, aAlsoIfRevoked); + if (!info || info->mObjectType != mozilla::dom::DataInfo::eBlobImpl || + !info->mBlobImpl) { + return NS_ERROR_DOM_BAD_URI; + } + + RefPtr<mozilla::dom::BlobImpl> blob = info->mBlobImpl; + blob.forget(aBlob); + return NS_OK; +} + +nsresult NS_GetSourceForMediaSourceURI(nsIURI* aURI, + mozilla::dom::MediaSource** aSource) { + *aSource = nullptr; + + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + mozilla::dom::DataInfo* info = mozilla::dom::GetDataInfoFromURI(aURI); + if (!info || info->mObjectType != mozilla::dom::DataInfo::eMediaSource) { + return NS_ERROR_DOM_BAD_URI; + } + + RefPtr<mozilla::dom::MediaSource> mediaSource = info->mMediaSource; + mediaSource.forget(aSource); + return NS_OK; +} + +namespace mozilla::dom { + +bool IsType(nsIURI* aUri, mozilla::dom::DataInfo::ObjectType aType) { + // We lock because this may be called off-main-thread + StaticMutexAutoLock lock(sMutex); + mozilla::dom::DataInfo* info = GetDataInfoFromURI(aUri); + if (!info) { + return false; + } + + return info->mObjectType == aType; +} + +bool IsBlobURI(nsIURI* aUri) { + return IsType(aUri, mozilla::dom::DataInfo::eBlobImpl); +} + +bool BlobURLSchemeIsHTTPOrHTTPS(const nsACString& aUri) { + return (StringBeginsWith(aUri, "blob:http://"_ns) || + StringBeginsWith(aUri, "blob:https://"_ns)); +} + +bool IsMediaSourceURI(nsIURI* aUri) { + return IsType(aUri, mozilla::dom::DataInfo::eMediaSource); +} + +} // namespace mozilla::dom diff --git a/dom/file/uri/BlobURLProtocolHandler.h b/dom/file/uri/BlobURLProtocolHandler.h new file mode 100644 index 0000000000..abb24f6fc0 --- /dev/null +++ b/dom/file/uri/BlobURLProtocolHandler.h @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_BlobURLProtocolHandler_h +#define mozilla_dom_BlobURLProtocolHandler_h + +#include "mozilla/Attributes.h" +#include "nsIProtocolHandler.h" +#include "nsIURI.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsWeakReference.h" +#include <functional> + +#define BLOBURI_SCHEME "blob" + +class nsIPrincipal; + +namespace mozilla { +class BlobURLsReporter; +class OriginAttributes; +template <class T> +class Maybe; + +namespace dom { + +class BlobImpl; +class BlobURLRegistrationData; +class ContentParent; +class MediaSource; + +class BlobURLProtocolHandler final : public nsIProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + + BlobURLProtocolHandler(); + + static nsresult CreateNewURI(const nsACString& aSpec, const char* aCharset, + nsIURI* aBaseURI, nsIURI** result); + + // Methods for managing uri->object mapping + // AddDataEntry creates the URI with the given scheme and returns it in aUri + static nsresult AddDataEntry(BlobImpl*, nsIPrincipal*, + const Maybe<nsID>& aAgentClusterId, + nsACString& aUri); + static nsresult AddDataEntry(MediaSource*, nsIPrincipal*, + const Maybe<nsID>& aAgentClusterId, + nsACString& aUri); + // IPC only + static void AddDataEntry(const nsACString& aURI, nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId, + BlobImpl* aBlobImpl); + + // These methods revoke a blobURL. Because some operations could still be in + // progress, the revoking consists in marking the blobURL as revoked and in + // removing it after RELEASING_TIMER milliseconds. + static void RemoveDataEntry(const nsACString& aUri, + bool aBroadcastToOTherProcesses = true); + // Returns true if the entry was allowed to be removed. + static bool RemoveDataEntry(const nsACString& aUri, nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId); + + static void RemoveDataEntries(); + + static bool HasDataEntry(const nsACString& aUri); + + static bool GetDataEntry(const nsACString& aUri, BlobImpl** aBlobImpl, + nsIPrincipal* aLoadingPrincipal, + nsIPrincipal* aTriggeringPrincipal, + const OriginAttributes& aOriginAttributes, + uint64_t aInnerWindowId, + const Maybe<nsID>& aAgentClusterId, + bool aAlsoIfRevoked = false); + + static void Traverse(const nsACString& aUri, + nsCycleCollectionTraversalCallback& aCallback); + + // Main-thread only method to invoke a helper function that gets called for + // every known and recently revoked Blob URL. The helper function should + // return true to keep going or false to stop enumerating (presumably because + // of an unexpected XPCOM or IPC error). This method returns false if already + // shutdown or if the helper method returns false, true otherwise. + static bool ForEachBlobURL( + std::function<bool(BlobImpl*, nsIPrincipal*, const Maybe<nsID>&, + const nsACString&, bool aRevoked)>&& aCb); + + // This method returns false if aURI is not a known BlobURL. Otherwise it + // returns true. + // + // When true is returned, the aPrincipal out param is meaningful. It gets + // set to the principal that a channel loaded from the blob would get if + // the blob is not already revoked and to a NullPrincipal if the blob is + // revoked. + // + // This means that for a revoked blob URL this method may either return + // false or return true and hand out a NullPrincipal in aPrincipal, + // depending on whether the "remove it from the hashtable" timer has + // fired. See RemoveDataEntry(). + static bool GetBlobURLPrincipal(nsIURI* aURI, nsIPrincipal** aPrincipal); + + // Check if metadata about Blob URLs created with this principal should be + // broadcast into every content process. This is currently the case for + // extension blob URLs and system principal blob URLs, as they can be loaded + // by system code and content scripts respectively. + static bool IsBlobURLBroadcastPrincipal(nsIPrincipal* aPrincipal); + + private: + ~BlobURLProtocolHandler(); + + static void Init(); + + // If principal is not null, its origin will be used to generate the URI. + static nsresult GenerateURIString(nsIPrincipal* aPrincipal, nsACString& aUri); +}; + +bool IsBlobURI(nsIURI* aUri); +bool IsMediaSourceURI(nsIURI* aUri); + +// Return true if inner scheme of blobURL is http or https, false otherwise. +bool BlobURLSchemeIsHTTPOrHTTPS(const nsACString& aUri); + +} // namespace dom +} // namespace mozilla + +extern nsresult NS_GetBlobForBlobURI(nsIURI* aURI, + mozilla::dom::BlobImpl** aBlob); + +extern nsresult NS_GetBlobForBlobURISpec(const nsACString& aSpec, + mozilla::dom::BlobImpl** aBlob, + bool aAlsoIfRevoked = false); + +extern nsresult NS_GetSourceForMediaSourceURI( + nsIURI* aURI, mozilla::dom::MediaSource** aSource); + +#endif /* mozilla_dom_BlobURLProtocolHandler_h */ diff --git a/dom/file/uri/components.conf b/dom/file/uri/components.conf new file mode 100644 index 0000000000..190a3a6c54 --- /dev/null +++ b/dom/file/uri/components.conf @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{b43964aa-a078-44b2-b06b-fd4d1b172e66}', + 'contract_ids': ['@mozilla.org/network/protocol;1?name=blob'], + 'type': 'mozilla::dom::BlobURLProtocolHandler', + 'headers': ['mozilla/dom/BlobURLProtocolHandler.h'], + 'protocol_config': { + 'scheme': 'blob', + 'flags': [ + 'URI_NORELATIVE', + 'URI_NOAUTH', + 'URI_LOADABLE_BY_SUBSUMERS', + 'URI_NON_PERSISTABLE', + 'URI_IS_LOCAL_RESOURCE', + ], + }, + }, +] diff --git a/dom/file/uri/moz.build b/dom/file/uri/moz.build new file mode 100644 index 0000000000..a1c4aed54c --- /dev/null +++ b/dom/file/uri/moz.build @@ -0,0 +1,34 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: File") + +EXPORTS.mozilla.dom += [ + "BlobURL.h", + "BlobURLInputStream.h", + "BlobURLProtocolHandler.h", +] + +UNIFIED_SOURCES += [ + "BlobURL.cpp", + "BlobURLChannel.cpp", + "BlobURLInputStream.cpp", + "BlobURLProtocolHandler.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +LOCAL_INCLUDES += [ + "/dom/file", + "/netwerk/base", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" |