/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "FileReader.h" #include "nsIGlobalObject.h" #include "nsITimer.h" #include "js/ArrayBuffer.h" // JS::NewArrayBufferWithContents #include "mozilla/Base64.h" #include "mozilla/CheckedInt.h" #include "mozilla/dom/DOMException.h" #include "mozilla/dom/DOMExceptionBinding.h" #include "mozilla/dom/File.h" #include "mozilla/dom/FileReaderBinding.h" #include "mozilla/dom/ProgressEvent.h" #include "mozilla/dom/UnionTypes.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/WorkerCommon.h" #include "mozilla/dom/WorkerRef.h" #include "mozilla/dom/WorkerScope.h" #include "mozilla/Encoding.h" #include "mozilla/HoldDropJSObjects.h" #include "nsAlgorithm.h" #include "nsCycleCollectionParticipant.h" #include "nsDOMJSUtils.h" #include "nsError.h" #include "nsNetUtil.h" #include "nsStreamUtils.h" #include "nsThreadUtils.h" #include "xpcpublic.h" #include "nsReadableUtils.h" namespace mozilla::dom { #define ABORT_STR u"abort" #define LOAD_STR u"load" #define LOADSTART_STR u"loadstart" #define LOADEND_STR u"loadend" #define ERROR_STR u"error" #define PROGRESS_STR u"progress" const uint64_t kUnknownSize = uint64_t(-1); NS_IMPL_CYCLE_COLLECTION_CLASS(FileReader) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(FileReader, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBlob) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mProgressNotifier) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mError) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(FileReader, DOMEventTargetHelper) tmp->Shutdown(); NS_IMPL_CYCLE_COLLECTION_UNLINK(mBlob) NS_IMPL_CYCLE_COLLECTION_UNLINK(mProgressNotifier) NS_IMPL_CYCLE_COLLECTION_UNLINK(mError) NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(FileReader, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mResultArrayBuffer) NS_IMPL_CYCLE_COLLECTION_TRACE_END NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileReader) NS_INTERFACE_MAP_ENTRY_CONCRETE(FileReader) NS_INTERFACE_MAP_ENTRY(nsITimerCallback) NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY(nsINamed) NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) NS_IMPL_ADDREF_INHERITED(FileReader, DOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(FileReader, DOMEventTargetHelper) class MOZ_RAII FileReaderDecreaseBusyCounter { RefPtr mFileReader; public: explicit FileReaderDecreaseBusyCounter(FileReader* aFileReader) : mFileReader(aFileReader) {} ~FileReaderDecreaseBusyCounter() { mFileReader->DecreaseBusyCounter(); } }; class FileReader::AsyncWaitRunnable final : public CancelableRunnable { public: explicit AsyncWaitRunnable(FileReader* aReader) : CancelableRunnable("FileReader::AsyncWaitRunnable"), mReader(aReader) {} NS_IMETHOD Run() override { if (mReader) { mReader->InitialAsyncWait(); } return NS_OK; } nsresult Cancel() override { mReader = nullptr; return NS_OK; } public: RefPtr mReader; }; void FileReader::RootResultArrayBuffer() { mozilla::HoldJSObjects(this); } // FileReader constructors/initializers FileReader::FileReader(nsIGlobalObject* aGlobal, WeakWorkerRef* aWorkerRef) : DOMEventTargetHelper(aGlobal), mFileData(nullptr), mDataLen(0), mDataFormat(FILE_AS_BINARY), mResultArrayBuffer(nullptr), mProgressEventWasDelayed(false), mTimerIsActive(false), mReadyState(EMPTY), mTotal(0), mTransferred(0), mBusyCount(0), mWeakWorkerRef(aWorkerRef) { MOZ_ASSERT(aGlobal); MOZ_ASSERT_IF(NS_IsMainThread(), !mWeakWorkerRef); if (NS_IsMainThread()) { mTarget = aGlobal->EventTargetFor(TaskCategory::Other); } else { mTarget = GetCurrentSerialEventTarget(); } SetDOMStringToNull(mResult); } FileReader::~FileReader() { Shutdown(); DropJSObjects(this); } /* static */ already_AddRefed FileReader::Constructor( const GlobalObject& aGlobal) { nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); RefPtr workerRef; if (!NS_IsMainThread()) { JSContext* cx = aGlobal.Context(); WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); workerRef = WeakWorkerRef::Create(workerPrivate); } RefPtr fileReader = new FileReader(global, workerRef); return fileReader.forget(); } // nsIInterfaceRequestor NS_IMETHODIMP FileReader::GetInterface(const nsIID& aIID, void** aResult) { return QueryInterface(aIID, aResult); } void FileReader::GetResult(JSContext* aCx, Nullable& aResult) { JS::Rooted result(aCx); if (mDataFormat == FILE_AS_ARRAYBUFFER) { if (mReadyState != DONE || !mResultArrayBuffer || !aResult.SetValue().SetAsArrayBuffer().Init(mResultArrayBuffer)) { aResult.SetNull(); } return; } if (mReadyState != DONE || mResult.IsVoid()) { aResult.SetNull(); return; } aResult.SetValue().SetAsString() = mResult; } void FileReader::OnLoadEndArrayBuffer() { AutoJSAPI jsapi; if (!jsapi.Init(GetParentObject())) { FreeDataAndDispatchError(NS_ERROR_FAILURE); return; } RootResultArrayBuffer(); JSContext* cx = jsapi.cx(); mResultArrayBuffer = JS::NewArrayBufferWithContents(cx, mDataLen, mFileData); if (mResultArrayBuffer) { mFileData = nullptr; // Transfer ownership FreeDataAndDispatchSuccess(); return; } // Let's handle the error status. JS::Rooted exceptionValue(cx); if (!JS_GetPendingException(cx, &exceptionValue) || // This should not really happen, exception should always be an object. !exceptionValue.isObject()) { JS_ClearPendingException(jsapi.cx()); FreeDataAndDispatchError(NS_ERROR_OUT_OF_MEMORY); return; } JS_ClearPendingException(jsapi.cx()); JS::Rooted exceptionObject(cx, &exceptionValue.toObject()); JSErrorReport* er = JS_ErrorFromException(cx, exceptionObject); if (!er || er->message()) { FreeDataAndDispatchError(NS_ERROR_OUT_OF_MEMORY); return; } nsAutoString errorName; JSLinearString* name = js::GetErrorTypeName(cx, er->exnType); if (name) { AssignJSLinearString(errorName, name); } nsAutoCString errorMsg(er->message().c_str()); nsAutoCString errorNameC = NS_LossyConvertUTF16toASCII(errorName); // XXX Code selected arbitrarily mError = new DOMException(NS_ERROR_DOM_INVALID_STATE_ERR, errorMsg, errorNameC, DOMException_Binding::INVALID_STATE_ERR); FreeDataAndDispatchError(); } nsresult FileReader::DoAsyncWait() { nsresult rv = IncreaseBusyCounter(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mAsyncStream->AsyncWait(this, /* aFlags*/ 0, /* aRequestedCount */ 0, mTarget); if (NS_WARN_IF(NS_FAILED(rv))) { DecreaseBusyCounter(); return rv; } return NS_OK; } namespace { void PopulateBufferForBinaryString(char16_t* aDest, const char* aSource, uint32_t aCount) { // Zero-extend each char to char16_t. ConvertLatin1toUtf16(Span(aSource, aCount), Span(aDest, aCount)); } nsresult ReadFuncBinaryString(nsIInputStream* aInputStream, void* aClosure, const char* aFromRawSegment, uint32_t aToOffset, uint32_t aCount, uint32_t* aWriteCount) { char16_t* dest = static_cast(aClosure) + aToOffset; PopulateBufferForBinaryString(dest, aFromRawSegment, aCount); *aWriteCount = aCount; return NS_OK; } } // namespace nsresult FileReader::DoReadData(uint64_t aCount) { MOZ_ASSERT(mAsyncStream); uint32_t bytesRead = 0; if (mDataFormat == FILE_AS_BINARY) { // Continuously update our binary string as data comes in CheckedInt size{mResult.Length()}; size += aCount; if (!size.isValid() || size.value() > UINT32_MAX || size.value() > mTotal) { return NS_ERROR_OUT_OF_MEMORY; } uint32_t lenBeforeRead = mResult.Length(); MOZ_ASSERT(lenBeforeRead == mDataLen, "unexpected mResult length"); mResult.SetLength(lenBeforeRead + aCount); char16_t* currentPos = mResult.BeginWriting() + lenBeforeRead; if (NS_InputStreamIsBuffered(mAsyncStream)) { nsresult rv = mAsyncStream->ReadSegments(ReadFuncBinaryString, currentPos, aCount, &bytesRead); NS_ENSURE_SUCCESS(rv, NS_OK); } else { while (aCount > 0) { char tmpBuffer[4096]; uint32_t minCount = XPCOM_MIN(aCount, static_cast(sizeof(tmpBuffer))); uint32_t read = 0; nsresult rv = mAsyncStream->Read(tmpBuffer, minCount, &read); if (rv == NS_BASE_STREAM_CLOSED) { rv = NS_OK; } NS_ENSURE_SUCCESS(rv, NS_OK); if (read == 0) { // The stream finished too early. return NS_ERROR_OUT_OF_MEMORY; } PopulateBufferForBinaryString(currentPos, tmpBuffer, read); currentPos += read; aCount -= read; bytesRead += read; } } MOZ_ASSERT(size.value() == lenBeforeRead + bytesRead); mResult.Truncate(size.value()); } else { CheckedInt size = mDataLen; size += aCount; // Update memory buffer to reflect the contents of the file if (!size.isValid() || // PR_Realloc doesn't support over 4GB memory size even if 64-bit OS // XXX: it's likely that this check is unnecessary and the comment is // wrong because we no longer use PR_Realloc outside of NSPR and NSS. size.value() > UINT32_MAX || size.value() > mTotal) { return NS_ERROR_OUT_OF_MEMORY; } MOZ_DIAGNOSTIC_ASSERT(mFileData); MOZ_RELEASE_ASSERT((mDataLen + aCount) <= mTotal); nsresult rv = mAsyncStream->Read(mFileData + mDataLen, aCount, &bytesRead); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } mDataLen += bytesRead; return NS_OK; } // Helper methods void FileReader::ReadFileContent(Blob& aBlob, const nsAString& aCharset, eDataFormat aDataFormat, ErrorResult& aRv) { if (IsCurrentThreadRunningWorker() && !mWeakWorkerRef) { // The worker is already shutting down. return; } if (mReadyState == LOADING) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } mError = nullptr; SetDOMStringToNull(mResult); mResultArrayBuffer = nullptr; mAsyncStream = nullptr; mTransferred = 0; mTotal = 0; mReadyState = EMPTY; FreeFileData(); mBlob = &aBlob; mDataFormat = aDataFormat; CopyUTF16toUTF8(aCharset, mCharset); { nsCOMPtr stream; mBlob->CreateInputStream(getter_AddRefs(stream), aRv); if (NS_WARN_IF(aRv.Failed())) { return; } aRv = NS_MakeAsyncNonBlockingInputStream(stream.forget(), getter_AddRefs(mAsyncStream)); if (NS_WARN_IF(aRv.Failed())) { return; } } MOZ_ASSERT(mAsyncStream); mTotal = mBlob->GetSize(aRv); if (NS_WARN_IF(aRv.Failed())) { return; } // Binary Format doesn't need a post-processing of the data. Everything is // written directly into mResult. if (mDataFormat != FILE_AS_BINARY) { if (mDataFormat == FILE_AS_ARRAYBUFFER) { mFileData = js_pod_malloc(mTotal); } else { mFileData = (char*)malloc(mTotal); } if (!mFileData) { NS_WARNING("Preallocation failed for ReadFileData"); aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return; } } mAsyncWaitRunnable = new AsyncWaitRunnable(this); aRv = NS_DispatchToCurrentThread(mAsyncWaitRunnable); if (NS_WARN_IF(aRv.Failed())) { FreeFileData(); return; } // FileReader should be in loading state here mReadyState = LOADING; } void FileReader::InitialAsyncWait() { mAsyncWaitRunnable = nullptr; nsresult rv = DoAsyncWait(); if (NS_WARN_IF(NS_FAILED(rv))) { mReadyState = EMPTY; FreeFileData(); return; } DispatchProgressEvent(nsLiteralString(LOADSTART_STR)); } nsresult FileReader::GetAsText(Blob* aBlob, const nsACString& aCharset, const char* aFileData, uint32_t aDataLen, nsAString& aResult) { // Try the API argument. const Encoding* encoding = Encoding::ForLabel(aCharset); if (!encoding) { // API argument failed. Try the type property of the blob. nsAutoString type16; aBlob->GetType(type16); NS_ConvertUTF16toUTF8 type(type16); nsAutoCString specifiedCharset; bool haveCharset; int32_t charsetStart, charsetEnd; NS_ExtractCharsetFromContentType(type, specifiedCharset, &haveCharset, &charsetStart, &charsetEnd); encoding = Encoding::ForLabel(specifiedCharset); if (!encoding) { // Type property failed. Use UTF-8. encoding = UTF_8_ENCODING; } } auto data = Span(reinterpret_cast(aFileData), aDataLen); nsresult rv; std::tie(rv, std::ignore) = encoding->Decode(data, aResult); return NS_FAILED(rv) ? rv : NS_OK; } nsresult FileReader::GetAsDataURL(Blob* aBlob, const char* aFileData, uint32_t aDataLen, nsAString& aResult) { aResult.AssignLiteral("data:"); nsAutoString contentType; aBlob->GetType(contentType); if (!contentType.IsEmpty()) { aResult.Append(contentType); } else { aResult.AppendLiteral("application/octet-stream"); } aResult.AppendLiteral(";base64,"); return Base64EncodeAppend(aFileData, aDataLen, aResult); } /* virtual */ JSObject* FileReader::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return FileReader_Binding::Wrap(aCx, this, aGivenProto); } void FileReader::StartProgressEventTimer() { if (!NS_IsMainThread() && !mWeakWorkerRef) { // The worker is possibly shutting down if dispatching a DOM event right // before this call triggered an InterruptCallback call. // XXX Note, the check is limited to workers for now, since it is unclear // in the spec how FileReader should behave in this case on the main thread. return; } if (!mProgressNotifier) { mProgressNotifier = NS_NewTimer(mTarget); } if (mProgressNotifier) { mProgressEventWasDelayed = false; mTimerIsActive = true; mProgressNotifier->Cancel(); mProgressNotifier->InitWithCallback(this, NS_PROGRESS_EVENT_INTERVAL, nsITimer::TYPE_ONE_SHOT); } } void FileReader::ClearProgressEventTimer() { mProgressEventWasDelayed = false; mTimerIsActive = false; if (mProgressNotifier) { mProgressNotifier->Cancel(); } } void FileReader::FreeFileData() { if (mFileData) { if (mDataFormat == FILE_AS_ARRAYBUFFER) { js_free(mFileData); } else { free(mFileData); } mFileData = nullptr; } mDataLen = 0; } void FileReader::FreeDataAndDispatchSuccess() { FreeFileData(); mResult.SetIsVoid(false); mAsyncStream = nullptr; mBlob = nullptr; // Dispatch event to signify end of a successful operation DispatchProgressEvent(nsLiteralString(LOAD_STR)); DispatchProgressEvent(nsLiteralString(LOADEND_STR)); } void FileReader::FreeDataAndDispatchError() { MOZ_ASSERT(mError); FreeFileData(); mResult.SetIsVoid(true); mAsyncStream = nullptr; mBlob = nullptr; // Dispatch error event to signify load failure DispatchProgressEvent(nsLiteralString(ERROR_STR)); DispatchProgressEvent(nsLiteralString(LOADEND_STR)); } void FileReader::FreeDataAndDispatchError(nsresult aRv) { // Set the status attribute, and dispatch the error event switch (aRv) { case NS_ERROR_FILE_NOT_FOUND: mError = DOMException::Create(NS_ERROR_DOM_NOT_FOUND_ERR); break; case NS_ERROR_FILE_ACCESS_DENIED: mError = DOMException::Create(NS_ERROR_DOM_SECURITY_ERR); break; default: mError = DOMException::Create(NS_ERROR_DOM_FILE_NOT_READABLE_ERR); break; } FreeDataAndDispatchError(); } nsresult FileReader::DispatchProgressEvent(const nsAString& aType) { ProgressEventInit init; init.mBubbles = false; init.mCancelable = false; init.mLoaded = mTransferred; if (mTotal != kUnknownSize) { init.mLengthComputable = true; init.mTotal = mTotal; } else { init.mLengthComputable = false; init.mTotal = 0; } RefPtr event = ProgressEvent::Constructor(this, aType, init); event->SetTrusted(true); ErrorResult rv; DispatchEvent(*event, rv); return rv.StealNSResult(); } // nsITimerCallback NS_IMETHODIMP FileReader::Notify(nsITimer* aTimer) { nsresult rv; mTimerIsActive = false; if (mProgressEventWasDelayed) { rv = DispatchProgressEvent(u"progress"_ns); NS_ENSURE_SUCCESS(rv, rv); StartProgressEventTimer(); } return NS_OK; } // InputStreamCallback NS_IMETHODIMP FileReader::OnInputStreamReady(nsIAsyncInputStream* aStream) { // We use this class to decrease the busy counter at the end of this method. // In theory we can do it immediatelly but, for debugging reasons, we want to // be 100% sure we have a workerRef when OnLoadEnd() is called. FileReaderDecreaseBusyCounter RAII(this); if (mReadyState != LOADING || aStream != mAsyncStream) { return NS_OK; } uint64_t count; nsresult rv = aStream->Available(&count); if (NS_SUCCEEDED(rv) && count) { rv = DoReadData(count); if (NS_SUCCEEDED(rv)) { rv = DoAsyncWait(); } } if (NS_FAILED(rv) || !count) { if (rv == NS_BASE_STREAM_CLOSED) { rv = NS_OK; } OnLoadEnd(rv); return NS_OK; } mTransferred += count; // Notify the timer is the appropriate timeframe has passed if (mTimerIsActive) { mProgressEventWasDelayed = true; } else { rv = DispatchProgressEvent(nsLiteralString(PROGRESS_STR)); NS_ENSURE_SUCCESS(rv, rv); StartProgressEventTimer(); } return NS_OK; } // nsINamed NS_IMETHODIMP FileReader::GetName(nsACString& aName) { aName.AssignLiteral("FileReader"); return NS_OK; } void FileReader::OnLoadEnd(nsresult aStatus) { // Cancel the progress event timer ClearProgressEventTimer(); // FileReader must be in DONE stage after an operation mReadyState = DONE; // Quick return, if failed. if (NS_FAILED(aStatus)) { FreeDataAndDispatchError(aStatus); return; } // In case we read a different number of bytes, we can assume that the // underlying storage has changed. We should not continue. if (mDataLen != mTotal) { FreeDataAndDispatchError(NS_ERROR_FAILURE); return; } // ArrayBuffer needs a custom handling. if (mDataFormat == FILE_AS_ARRAYBUFFER) { OnLoadEndArrayBuffer(); return; } nsresult rv = NS_OK; // We don't do anything special for Binary format. if (mDataFormat == FILE_AS_DATAURL) { rv = GetAsDataURL(mBlob, mFileData, mDataLen, mResult); } else if (mDataFormat == FILE_AS_TEXT) { if (!mFileData && mDataLen) { rv = NS_ERROR_OUT_OF_MEMORY; } else if (!mFileData) { rv = GetAsText(mBlob, mCharset, "", mDataLen, mResult); } else { rv = GetAsText(mBlob, mCharset, mFileData, mDataLen, mResult); } } if (NS_WARN_IF(NS_FAILED(rv))) { FreeDataAndDispatchError(rv); return; } FreeDataAndDispatchSuccess(); } void FileReader::Abort() { if (mReadyState == EMPTY || mReadyState == DONE) { return; } MOZ_ASSERT(mReadyState == LOADING); Cleanup(); // XXX The spec doesn't say this mError = DOMException::Create(NS_ERROR_DOM_ABORT_ERR); // Revert status and result attributes SetDOMStringToNull(mResult); mResultArrayBuffer = nullptr; mBlob = nullptr; // Dispatch the events DispatchProgressEvent(nsLiteralString(ABORT_STR)); DispatchProgressEvent(nsLiteralString(LOADEND_STR)); } nsresult FileReader::IncreaseBusyCounter() { if (mWeakWorkerRef && mBusyCount++ == 0) { if (NS_WARN_IF(!mWeakWorkerRef->GetPrivate())) { return NS_ERROR_FAILURE; } RefPtr self = this; RefPtr ref = StrongWorkerRef::Create(mWeakWorkerRef->GetPrivate(), "FileReader", [self]() { self->Shutdown(); }); if (NS_WARN_IF(!ref)) { return NS_ERROR_FAILURE; } mStrongWorkerRef = ref; } return NS_OK; } void FileReader::DecreaseBusyCounter() { MOZ_ASSERT_IF(mStrongWorkerRef, mBusyCount); if (mStrongWorkerRef && --mBusyCount == 0) { mStrongWorkerRef = nullptr; } } void FileReader::Cleanup() { mReadyState = DONE; if (mAsyncWaitRunnable) { mAsyncWaitRunnable->Cancel(); mAsyncWaitRunnable = nullptr; } if (mAsyncStream) { mAsyncStream->Close(); mAsyncStream = nullptr; } ClearProgressEventTimer(); FreeFileData(); mResultArrayBuffer = nullptr; } void FileReader::Shutdown() { Cleanup(); if (mWeakWorkerRef) { mWeakWorkerRef = nullptr; } } } // namespace mozilla::dom