diff options
Diffstat (limited to 'dom/file/FileReaderSync.cpp')
-rw-r--r-- | dom/file/FileReaderSync.cpp | 489 |
1 files changed, 489 insertions, 0 deletions
diff --git a/dom/file/FileReaderSync.cpp b/dom/file/FileReaderSync.cpp new file mode 100644 index 0000000000..e72d22b7c8 --- /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. + mozilla::Unused << 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; +} |