gecko-dev/dom/file/FileReader.cpp

838 строки
20 KiB
C++

/* -*- 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 "mozilla/Base64.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/dom/DOMException.h"
#include "mozilla/dom/File.h"
#include "mozilla/dom/FileReaderBinding.h"
#include "mozilla/dom/ProgressEvent.h"
#include "mozilla/dom/WorkerCommon.h"
#include "mozilla/dom/WorkerRef.h"
#include "mozilla/dom/WorkerScope.h"
#include "mozilla/Encoding.h"
#include "nsAlgorithm.h"
#include "nsCycleCollectionParticipant.h"
#include "nsDOMJSUtils.h"
#include "nsError.h"
#include "nsNetUtil.h"
#include "xpcpublic.h"
namespace mozilla {
namespace dom {
#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);
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(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();
}
};
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 = GetCurrentThreadSerialEventTarget();
}
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());
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,
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;
}
}
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);
}
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::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)
{
const unsigned char* source = (const unsigned char*)aSource;
char16_t* end = aDest + aCount;
while (aDest != end) {
*aDest = *source;
++aDest;
++source;
}
}
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;
}
} // anonymous
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 oldLen = mResult.Length();
MOZ_ASSERT(oldLen == mDataLen, "unexpected mResult length");
char16_t* dest = nullptr;
mResult.GetMutableData(&dest, size.value(), fallible);
NS_ENSURE_TRUE(dest, NS_ERROR_OUT_OF_MEMORY);
dest += oldLen;
if (NS_InputStreamIsBuffered(mAsyncStream)) {
nsresult rv = mAsyncStream->ReadSegments(ReadFuncBinaryString, dest,
aCount, &bytesRead);
NS_ENSURE_SUCCESS(rv, rv);
} else {
while (aCount > 0) {
char tmpBuffer[4096];
uint32_t minCount =
XPCOM_MIN(aCount, static_cast<uint64_t>(sizeof(tmpBuffer)));
uint32_t read;
nsresult rv = mAsyncStream->Read(tmpBuffer, minCount, &read);
if (rv == NS_BASE_STREAM_CLOSED) {
rv = NS_OK;
}
NS_ENSURE_SUCCESS(rv, rv);
if (read == 0) {
// The stream finished too early.
return NS_ERROR_OUT_OF_MEMORY;
}
PopulateBufferForBinaryString(dest, tmpBuffer, read);
dest += read;
aCount -= read;
bytesRead += read;
}
}
MOZ_ASSERT(size.value() == oldLen + 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;
}
}
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)
{
// 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 = MakeSpan(reinterpret_cast<const uint8_t*>(aFileData),
aDataLen);
nsresult rv;
Tie(rv, encoding) = 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,");
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 = NS_NewTimer();
}
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 = 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);
bool dummy;
return DispatchEvent(event, &dummy);
}
// 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 workerRef when OnLoadEnd() is called.
FileReaderDecreaseBusyCounter RAII(this);
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;
}
return OnLoadEnd(rv);
}
mTransferred += count;
//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;
}
// nsINamed
NS_IMETHODIMP
FileReader::GetName(nsACString& aName)
{
aName.AssignLiteral("FileReader");
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()
{
if (mReadyState == EMPTY || mReadyState == DONE) {
return;
}
MOZ_ASSERT(mReadyState == LOADING);
ClearProgressEventTimer();
mReadyState = DONE;
// XXX The spec doesn't say this
mError = DOMException::Create(NS_ERROR_DOM_ABORT_ERR);
// 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 (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::Shutdown()
{
mReadyState = DONE;
if (mAsyncStream) {
mAsyncStream->Close();
mAsyncStream = nullptr;
}
FreeFileData();
mResultArrayBuffer = nullptr;
if (mWeakWorkerRef && mBusyCount != 0) {
mStrongWorkerRef = nullptr;
mWeakWorkerRef = nullptr;
mBusyCount = 0;
}
}
} // dom namespace
} // mozilla namespace