summaryrefslogtreecommitdiffstats
path: root/dom/file/FileReaderSync.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/file/FileReaderSync.cpp')
-rw-r--r--dom/file/FileReaderSync.cpp489
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, &currentBytesRead);
+
+ // 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;
+}