diff options
Diffstat (limited to 'dom/base/FileReader.cpp')
-rw-r--r-- | dom/base/FileReader.cpp | 790 |
1 files changed, 790 insertions, 0 deletions
diff --git a/dom/base/FileReader.cpp b/dom/base/FileReader.cpp new file mode 100644 index 000000000..003edc61f --- /dev/null +++ b/dom/base/FileReader.cpp @@ -0,0 +1,790 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "nsIEventTarget.h" +#include "nsIGlobalObject.h" +#include "nsITimer.h" +#include "nsITransport.h" +#include "nsIStreamTransportService.h" + +#include "mozilla/Base64.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/dom/DOMError.h" +#include "mozilla/dom/EncodingUtils.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FileReaderBinding.h" +#include "mozilla/dom/ProgressEvent.h" +#include "nsContentUtils.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDOMJSUtils.h" +#include "nsError.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "xpcpublic.h" + +#include "WorkerPrivate.h" +#include "WorkerScope.h" + +namespace mozilla { +namespace dom { + +using namespace workers; + +#define ABORT_STR "abort" +#define LOAD_STR "load" +#define LOADSTART_STR "loadstart" +#define LOADEND_STR "loadend" +#define ERROR_STR "error" +#define PROGRESS_STR "progress" + +const uint64_t kUnknownSize = uint64_t(-1); + +static NS_DEFINE_CID(kStreamTransportServiceCID, NS_STREAMTRANSPORTSERVICE_CID); + +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_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_INHERITED(FileReader) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +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(); + } +}; + +void +FileReader::RootResultArrayBuffer() +{ + mozilla::HoldJSObjects(this); +} + +//FileReader constructors/initializers + +FileReader::FileReader(nsIGlobalObject* aGlobal, + WorkerPrivate* aWorkerPrivate) + : DOMEventTargetHelper(aGlobal) + , mFileData(nullptr) + , mDataLen(0) + , mDataFormat(FILE_AS_BINARY) + , mResultArrayBuffer(nullptr) + , mProgressEventWasDelayed(false) + , mTimerIsActive(false) + , mReadyState(EMPTY) + , mTotal(0) + , mTransferred(0) + , mTarget(do_GetCurrentThread()) + , mBusyCount(0) + , mWorkerPrivate(aWorkerPrivate) +{ + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(NS_IsMainThread() == !mWorkerPrivate); + SetDOMStringToNull(mResult); +} + +FileReader::~FileReader() +{ + Shutdown(); + DropJSObjects(this); +} + +/* static */ already_AddRefed<FileReader> +FileReader::Constructor(const GlobalObject& aGlobal, ErrorResult& aRv) +{ + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + WorkerPrivate* workerPrivate = nullptr; + + if (!NS_IsMainThread()) { + JSContext* cx = aGlobal.Context(); + workerPrivate = GetWorkerPrivateFromContext(cx); + MOZ_ASSERT(workerPrivate); + } + + RefPtr<FileReader> fileReader = new FileReader(global, workerPrivate); + + return fileReader.forget(); +} + +// nsIInterfaceRequestor + +NS_IMETHODIMP +FileReader::GetInterface(const nsIID & aIID, void **aResult) +{ + return QueryInterface(aIID, aResult); +} + +void +FileReader::GetResult(JSContext* aCx, + JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) +{ + JS::Rooted<JS::Value> result(aCx); + + if (mDataFormat == FILE_AS_ARRAYBUFFER) { + if (mReadyState == DONE && mResultArrayBuffer) { + result.setObject(*mResultArrayBuffer); + } else { + result.setNull(); + } + + if (!JS_WrapValue(aCx, &result)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + aResult.set(result); + return; + } + + nsString tmpResult = mResult; + if (!xpc::StringToJsval(aCx, tmpResult, aResult)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } +} + +static nsresult +ReadFuncBinaryString(nsIInputStream* in, + void* closure, + const char* fromRawSegment, + uint32_t toOffset, + uint32_t count, + uint32_t *writeCount) +{ + char16_t* dest = static_cast<char16_t*>(closure) + toOffset; + char16_t* end = dest + count; + const unsigned char* source = (const unsigned char*)fromRawSegment; + while (dest != end) { + *dest = *source; + ++dest; + ++source; + } + *writeCount = count; + + return NS_OK; +} + +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; + JSFlatString* name = js::GetErrorTypeName(cx, er->exnType); + if (name) { + AssignJSFlatString(errorName, name); + } + + mError = + new DOMError(GetOwner(), errorName, + NS_ConvertUTF8toUTF16(er->message().c_str())); + + 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; +} + +nsresult +FileReader::DoReadData(uint64_t aCount) +{ + MOZ_ASSERT(mAsyncStream); + + if (mDataFormat == FILE_AS_BINARY) { + //Continuously update our binary string as data comes in + uint32_t oldLen = mResult.Length(); + NS_ASSERTION(mResult.Length() == mDataLen, "unexpected mResult length"); + if (uint64_t(oldLen) + aCount > UINT32_MAX) + return NS_ERROR_OUT_OF_MEMORY; + char16_t *buf = nullptr; + mResult.GetMutableData(&buf, oldLen + aCount, fallible); + NS_ENSURE_TRUE(buf, NS_ERROR_OUT_OF_MEMORY); + + uint32_t bytesRead = 0; + mAsyncStream->ReadSegments(ReadFuncBinaryString, buf + oldLen, aCount, + &bytesRead); + NS_ASSERTION(bytesRead == aCount, "failed to read data"); + } + 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 + size.value() > UINT32_MAX || + size.value() > mTotal) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (mDataFormat != FILE_AS_ARRAYBUFFER) { + mFileData = (char *) realloc(mFileData, mDataLen + aCount); + NS_ENSURE_TRUE(mFileData, NS_ERROR_OUT_OF_MEMORY); + } + + uint32_t bytesRead = 0; + MOZ_DIAGNOSTIC_ASSERT(mFileData); + mAsyncStream->Read(mFileData + mDataLen, aCount, &bytesRead); + NS_ASSERTION(bytesRead == aCount, "failed to read data"); + } + + mDataLen += aCount; + return NS_OK; +} + +// Helper methods + +void +FileReader::ReadFileContent(Blob& aBlob, + const nsAString &aCharset, + eDataFormat aDataFormat, + ErrorResult& aRv) +{ + //Implicit abort to clear any other activity going on + ErrorResult error; + Abort(error); + error.SuppressException(); + + if (mReadyState == LOADING) { + // A nested ReadAsSomething() as been called during one of the events + // dispatched by Abort(). We have to terminate this operation in order to + // continue the nested one. + aRv.Throw(NS_ERROR_ABORT); + return; + } + + mError = nullptr; + SetDOMStringToNull(mResult); + mTransferred = 0; + mTotal = 0; + mReadyState = EMPTY; + FreeFileData(); + + mBlob = &aBlob; + mDataFormat = aDataFormat; + CopyUTF16toUTF8(aCharset, mCharset); + + nsresult rv; + nsCOMPtr<nsIStreamTransportService> sts = + do_GetService(kStreamTransportServiceCID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + + nsCOMPtr<nsIInputStream> stream; + mBlob->GetInternalStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr<nsITransport> transport; + aRv = sts->CreateInputTransport(stream, + /* aStartOffset */ 0, + /* aReadLimit */ -1, + /* aCloseWhenDone */ true, + getter_AddRefs(transport)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr<nsIInputStream> wrapper; + aRv = transport->OpenInputStream(/* aFlags */ 0, + /* aSegmentSize */ 0, + /* aSegmentCount */ 0, + getter_AddRefs(wrapper)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + MOZ_ASSERT(!mAsyncStream); + mAsyncStream = do_QueryInterface(wrapper); + MOZ_ASSERT(mAsyncStream); + + mTotal = mBlob->GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (mDataFormat == FILE_AS_ARRAYBUFFER) { + mFileData = js_pod_malloc<char>(mTotal); + if (!mFileData) { + NS_WARNING("Preallocation failed for ReadFileData"); + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + + aRv = DoAsyncWait(); + if (NS_WARN_IF(aRv.Failed())) { + FreeFileData(); + return; + } + + //FileReader should be in loading state here + mReadyState = LOADING; + DispatchProgressEvent(NS_LITERAL_STRING(LOADSTART_STR)); +} + +nsresult +FileReader::GetAsText(Blob *aBlob, + const nsACString &aCharset, + const char *aFileData, + uint32_t aDataLen, + nsAString& aResult) +{ + // The BOM sniffing is baked into the "decode" part of the Encoding + // Standard, which the File API references. + nsAutoCString encoding; + if (!nsContentUtils::CheckForBOM( + reinterpret_cast<const unsigned char *>(aFileData), + aDataLen, + encoding)) { + // BOM sniffing failed. Try the API argument. + if (!EncodingUtils::FindEncodingForLabel(aCharset, + 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); + if (!EncodingUtils::FindEncodingForLabel(specifiedCharset, encoding)) { + // Type property failed. Use UTF-8. + encoding.AssignLiteral("UTF-8"); + } + } + } + + nsDependentCSubstring data(aFileData, aDataLen); + return nsContentUtils::ConvertStringFromEncoding(encoding, data, aResult); +} + +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,"); + + nsCString encodedData; + nsresult rv = Base64Encode(Substring(aFileData, aDataLen), encodedData); + NS_ENSURE_SUCCESS(rv, rv); + + if (!AppendASCIItoUTF16(encodedData, aResult, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +/* virtual */ JSObject* +FileReader::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return FileReaderBinding::Wrap(aCx, this, aGivenProto); +} + +void +FileReader::StartProgressEventTimer() +{ + if (!mProgressNotifier) { + mProgressNotifier = do_CreateInstance(NS_TIMER_CONTRACTID); + } + + if (mProgressNotifier) { + mProgressEventWasDelayed = false; + mTimerIsActive = true; + mProgressNotifier->Cancel(); + mProgressNotifier->SetTarget(mTarget); + mProgressNotifier->InitWithCallback(this, NS_PROGRESS_EVENT_INTERVAL, + nsITimer::TYPE_ONE_SHOT); + } +} + +void +FileReader::ClearProgressEventTimer() +{ + mProgressEventWasDelayed = false; + mTimerIsActive = false; + if (mProgressNotifier) { + mProgressNotifier->Cancel(); + } +} + +void +FileReader::FreeDataAndDispatchSuccess() +{ + FreeFileData(); + mResult.SetIsVoid(false); + mAsyncStream = nullptr; + mBlob = nullptr; + + // Dispatch event to signify end of a successful operation + DispatchProgressEvent(NS_LITERAL_STRING(LOAD_STR)); + DispatchProgressEvent(NS_LITERAL_STRING(LOADEND_STR)); +} + +void +FileReader::FreeDataAndDispatchError() +{ + MOZ_ASSERT(mError); + + FreeFileData(); + mResult.SetIsVoid(true); + mAsyncStream = nullptr; + mBlob = nullptr; + + // Dispatch error event to signify load failure + DispatchProgressEvent(NS_LITERAL_STRING(ERROR_STR)); + DispatchProgressEvent(NS_LITERAL_STRING(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 = new DOMError(GetOwner(), NS_LITERAL_STRING("NotFoundError")); + break; + case NS_ERROR_FILE_ACCESS_DENIED: + mError = new DOMError(GetOwner(), NS_LITERAL_STRING("SecurityError")); + break; + default: + mError = new DOMError(GetOwner(), NS_LITERAL_STRING("NotReadableError")); + 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); + + return DispatchDOMEvent(nullptr, event, nullptr, nullptr); +} + +// nsITimerCallback +NS_IMETHODIMP +FileReader::Notify(nsITimer* aTimer) +{ + nsresult rv; + mTimerIsActive = false; + + if (mProgressEventWasDelayed) { + rv = DispatchProgressEvent(NS_LITERAL_STRING("progress")); + NS_ENSURE_SUCCESS(rv, rv); + + StartProgressEventTimer(); + } + + return NS_OK; +} + +// InputStreamCallback +NS_IMETHODIMP +FileReader::OnInputStreamReady(nsIAsyncInputStream* aStream) +{ + if (mReadyState != LOADING || aStream != mAsyncStream) { + return NS_OK; + } + + // 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 workerHolder when OnLoadEnd() is called. + FileReaderDecreaseBusyCounter RAII(this); + + uint64_t aCount; + nsresult rv = aStream->Available(&aCount); + + if (NS_SUCCEEDED(rv) && aCount) { + rv = DoReadData(aCount); + } + + if (NS_SUCCEEDED(rv)) { + rv = DoAsyncWait(); + } + + if (NS_FAILED(rv) || !aCount) { + if (rv == NS_BASE_STREAM_CLOSED) { + rv = NS_OK; + } + return OnLoadEnd(rv); + } + + mTransferred += aCount; + + //Notify the timer is the appropriate timeframe has passed + if (mTimerIsActive) { + mProgressEventWasDelayed = true; + } else { + rv = DispatchProgressEvent(NS_LITERAL_STRING(PROGRESS_STR)); + NS_ENSURE_SUCCESS(rv, rv); + + StartProgressEventTimer(); + } + + return NS_OK; +} + +nsresult +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 NS_OK; + } + + // 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 NS_OK; + } + + // ArrayBuffer needs a custom handling. + if (mDataFormat == FILE_AS_ARRAYBUFFER) { + OnLoadEndArrayBuffer(); + return NS_OK; + } + + 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 NS_OK; + } + + FreeDataAndDispatchSuccess(); + return NS_OK; +} + +void +FileReader::Abort(ErrorResult& aRv) +{ + if (mReadyState != LOADING) { + // XXX The spec doesn't say this + aRv.Throw(NS_ERROR_DOM_FILE_ABORT_ERR); + return; + } + + ClearProgressEventTimer(); + + mReadyState = DONE; + + // XXX The spec doesn't say this + mError = new DOMError(GetOwner(), NS_LITERAL_STRING("AbortError")); + + // Revert status and result attributes + SetDOMStringToNull(mResult); + mResultArrayBuffer = nullptr; + + mAsyncStream = nullptr; + mBlob = nullptr; + + //Clean up memory buffer + FreeFileData(); + + // Dispatch the events + DispatchProgressEvent(NS_LITERAL_STRING(ABORT_STR)); + DispatchProgressEvent(NS_LITERAL_STRING(LOADEND_STR)); +} + +nsresult +FileReader::IncreaseBusyCounter() +{ + if (mWorkerPrivate && mBusyCount++ == 0 && + !HoldWorker(mWorkerPrivate, Closing)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void +FileReader::DecreaseBusyCounter() +{ + MOZ_ASSERT_IF(mWorkerPrivate, mBusyCount); + if (mWorkerPrivate && --mBusyCount == 0) { + ReleaseWorker(); + } +} + +bool +FileReader::Notify(Status aStatus) +{ + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (aStatus > Running) { + Shutdown(); + } + + return true; +} + +void +FileReader::Shutdown() +{ + mReadyState = DONE; + + if (mAsyncStream) { + mAsyncStream->Close(); + mAsyncStream = nullptr; + } + + FreeFileData(); + mResultArrayBuffer = nullptr; + + if (mWorkerPrivate && mBusyCount != 0) { + ReleaseWorker(); + mWorkerPrivate = nullptr; + mBusyCount = 0; + } +} + +} // dom namespace +} // mozilla namespace |