diff options
Diffstat (limited to 'dom/fetch/FetchUtil.cpp')
-rw-r--r-- | dom/fetch/FetchUtil.cpp | 813 |
1 files changed, 813 insertions, 0 deletions
diff --git a/dom/fetch/FetchUtil.cpp b/dom/fetch/FetchUtil.cpp new file mode 100644 index 0000000000..040e23e4dc --- /dev/null +++ b/dom/fetch/FetchUtil.cpp @@ -0,0 +1,813 @@ +/* -*- 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 "FetchUtil.h" + +#include "zlib.h" + +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "nsCRT.h" +#include "nsError.h" +#include "nsIAsyncInputStream.h" +#include "nsICloneableInputStream.h" +#include "nsIHttpChannel.h" +#include "nsNetUtil.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "js/BuildId.h" +#include "mozilla/dom/Document.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/InternalRequest.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/dom/WorkerRef.h" + +namespace mozilla::dom { + +// static +nsresult FetchUtil::GetValidRequestMethod(const nsACString& aMethod, + nsCString& outMethod) { + nsAutoCString upperCaseMethod(aMethod); + ToUpperCase(upperCaseMethod); + if (!NS_IsValidHTTPToken(aMethod)) { + outMethod.SetIsVoid(true); + return NS_ERROR_DOM_SYNTAX_ERR; + } + + if (upperCaseMethod.EqualsLiteral("CONNECT") || + upperCaseMethod.EqualsLiteral("TRACE") || + upperCaseMethod.EqualsLiteral("TRACK")) { + outMethod.SetIsVoid(true); + return NS_ERROR_DOM_SECURITY_ERR; + } + + if (upperCaseMethod.EqualsLiteral("DELETE") || + upperCaseMethod.EqualsLiteral("GET") || + upperCaseMethod.EqualsLiteral("HEAD") || + upperCaseMethod.EqualsLiteral("OPTIONS") || + upperCaseMethod.EqualsLiteral("POST") || + upperCaseMethod.EqualsLiteral("PUT")) { + outMethod = upperCaseMethod; + } else { + outMethod = aMethod; // Case unchanged for non-standard methods + } + return NS_OK; +} + +static bool FindCRLF(nsACString::const_iterator& aStart, + nsACString::const_iterator& aEnd) { + nsACString::const_iterator end(aEnd); + return FindInReadable("\r\n"_ns, aStart, end); +} + +// Reads over a CRLF and positions start after it. +static bool PushOverLine(nsACString::const_iterator& aStart, + const nsACString::const_iterator& aEnd) { + if (*aStart == nsCRT::CR && (aEnd - aStart > 1) && *(++aStart) == nsCRT::LF) { + ++aStart; // advance to after CRLF + return true; + } + + return false; +} + +// static +bool FetchUtil::ExtractHeader(nsACString::const_iterator& aStart, + nsACString::const_iterator& aEnd, + nsCString& aHeaderName, nsCString& aHeaderValue, + bool* aWasEmptyHeader) { + MOZ_ASSERT(aWasEmptyHeader); + // Set it to a valid value here so we don't forget later. + *aWasEmptyHeader = false; + + const char* beginning = aStart.get(); + nsACString::const_iterator end(aEnd); + if (!FindCRLF(aStart, end)) { + return false; + } + + if (aStart.get() == beginning) { + *aWasEmptyHeader = true; + return true; + } + + nsAutoCString header(beginning, aStart.get() - beginning); + + nsACString::const_iterator headerStart, iter, headerEnd; + header.BeginReading(headerStart); + header.EndReading(headerEnd); + iter = headerStart; + if (!FindCharInReadable(':', iter, headerEnd)) { + return false; + } + + aHeaderName.Assign(StringHead(header, iter - headerStart)); + aHeaderName.CompressWhitespace(); + if (!NS_IsValidHTTPToken(aHeaderName)) { + return false; + } + + aHeaderValue.Assign(Substring(++iter, headerEnd)); + if (!NS_IsReasonableHTTPHeaderValue(aHeaderValue)) { + return false; + } + aHeaderValue.CompressWhitespace(); + + return PushOverLine(aStart, aEnd); +} + +// static +nsresult FetchUtil::SetRequestReferrer(nsIPrincipal* aPrincipal, Document* aDoc, + nsIHttpChannel* aChannel, + InternalRequest& aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = NS_OK; + nsAutoString referrer; + aRequest.GetReferrer(referrer); + + ReferrerPolicy policy = aRequest.ReferrerPolicy_(); + nsCOMPtr<nsIReferrerInfo> referrerInfo; + if (referrer.IsEmpty()) { + // This is the case request’s referrer is "no-referrer" + referrerInfo = new ReferrerInfo(nullptr, ReferrerPolicy::No_referrer); + } else if (referrer.EqualsLiteral(kFETCH_CLIENT_REFERRER_STR)) { + referrerInfo = ReferrerInfo::CreateForFetch(aPrincipal, aDoc); + // In the first step, we should use referrer info from requetInit + referrerInfo = static_cast<ReferrerInfo*>(referrerInfo.get()) + ->CloneWithNewPolicy(policy); + } else { + // From "Determine request's Referrer" step 3 + // "If request's referrer is a URL, let referrerSource be request's + // referrer." + nsCOMPtr<nsIURI> referrerURI; + rv = NS_NewURI(getter_AddRefs(referrerURI), referrer); + NS_ENSURE_SUCCESS(rv, rv); + referrerInfo = new ReferrerInfo(referrerURI, policy); + } + + rv = aChannel->SetReferrerInfoWithoutClone(referrerInfo); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString computedReferrerSpec; + referrerInfo = aChannel->GetReferrerInfo(); + if (referrerInfo) { + Unused << referrerInfo->GetComputedReferrerSpec(computedReferrerSpec); + } + + // Step 8 https://fetch.spec.whatwg.org/#main-fetch + // If request’s referrer is not "no-referrer", set request’s referrer to + // the result of invoking determine request’s referrer. + aRequest.SetReferrer(computedReferrerSpec); + + return NS_OK; +} + +class StoreOptimizedEncodingRunnable final : public Runnable { + nsMainThreadPtrHandle<nsICacheInfoChannel> mCache; + Vector<uint8_t> mBytes; + + public: + StoreOptimizedEncodingRunnable( + nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache, + Vector<uint8_t>&& aBytes) + : Runnable("StoreOptimizedEncodingRunnable"), + mCache(std::move(aCache)), + mBytes(std::move(aBytes)) {} + + NS_IMETHOD Run() override { + nsresult rv; + + nsCOMPtr<nsIAsyncOutputStream> stream; + rv = mCache->OpenAlternativeOutputStream(FetchUtil::WasmAltDataType, + int64_t(mBytes.length()), + getter_AddRefs(stream)); + if (NS_FAILED(rv)) { + return rv; + } + + auto closeStream = MakeScopeExit([&]() { stream->CloseWithStatus(rv); }); + + uint32_t written; + rv = stream->Write((char*)mBytes.begin(), mBytes.length(), &written); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_RELEASE_ASSERT(mBytes.length() == written); + return NS_OK; + }; +}; + +class WindowStreamOwner final : public nsIObserver, + public nsSupportsWeakReference { + // Read from any thread but only set/cleared on the main thread. The lifecycle + // of WindowStreamOwner prevents concurrent read/clear. + nsCOMPtr<nsIAsyncInputStream> mStream; + + nsCOMPtr<nsIGlobalObject> mGlobal; + + ~WindowStreamOwner() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, DOM_WINDOW_DESTROYED_TOPIC); + } + } + + public: + NS_DECL_ISUPPORTS + + WindowStreamOwner(nsIAsyncInputStream* aStream, nsIGlobalObject* aGlobal) + : mStream(aStream), mGlobal(aGlobal) { + MOZ_DIAGNOSTIC_ASSERT(mGlobal); + MOZ_ASSERT(NS_IsMainThread()); + } + + static already_AddRefed<WindowStreamOwner> Create( + nsIAsyncInputStream* aStream, nsIGlobalObject* aGlobal) { + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!os)) { + return nullptr; + } + + RefPtr<WindowStreamOwner> self = new WindowStreamOwner(aStream, aGlobal); + + // Holds nsIWeakReference to self. + nsresult rv = os->AddObserver(self, DOM_WINDOW_DESTROYED_TOPIC, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return self.forget(); + } + + // nsIObserver: + + NS_IMETHOD + Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) == 0); + + if (!mStream) { + return NS_OK; + } + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal); + if (!SameCOMIdentity(aSubject, window)) { + return NS_OK; + } + + // mStream->Close() will call JSStreamConsumer::OnInputStreamReady which may + // then destory itself, dropping the last reference to 'this'. + RefPtr<WindowStreamOwner> keepAlive(this); + + mStream->Close(); + mStream = nullptr; + mGlobal = nullptr; + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(WindowStreamOwner, nsIObserver, nsISupportsWeakReference) + +inline nsISupports* ToSupports(WindowStreamOwner* aObj) { + return static_cast<nsIObserver*>(aObj); +} + +class WorkerStreamOwner final { + public: + NS_INLINE_DECL_REFCOUNTING(WorkerStreamOwner) + + explicit WorkerStreamOwner(nsIAsyncInputStream* aStream, + nsCOMPtr<nsIEventTarget>&& target) + : mStream(aStream), mOwningEventTarget(std::move(target)) {} + + static already_AddRefed<WorkerStreamOwner> Create( + nsIAsyncInputStream* aStream, WorkerPrivate* aWorker, + nsCOMPtr<nsIEventTarget>&& target) { + RefPtr<WorkerStreamOwner> self = + new WorkerStreamOwner(aStream, std::move(target)); + + self->mWorkerRef = + StrongWorkerRef::Create(aWorker, "JSStreamConsumer", [self]() { + if (self->mStream) { + // If this Close() calls JSStreamConsumer::OnInputStreamReady and + // drops the last reference to the JSStreamConsumer, 'this' will not + // be destroyed since ~JSStreamConsumer() only enqueues a release + // proxy. + self->mStream->Close(); + self->mStream = nullptr; + } + }); + + if (!self->mWorkerRef) { + return nullptr; + } + + return self.forget(); + } + + static void ProxyRelease(already_AddRefed<WorkerStreamOwner> aDoomed) { + RefPtr<WorkerStreamOwner> doomed = aDoomed; + nsIEventTarget* target = doomed->mOwningEventTarget; + NS_ProxyRelease("WorkerStreamOwner", target, doomed.forget(), + /* aAlwaysProxy = */ true); + } + + private: + ~WorkerStreamOwner() = default; + + // Read from any thread but only set/cleared on the worker thread. The + // lifecycle of WorkerStreamOwner prevents concurrent read/clear. + nsCOMPtr<nsIAsyncInputStream> mStream; + RefPtr<StrongWorkerRef> mWorkerRef; + nsCOMPtr<nsIEventTarget> mOwningEventTarget; +}; + +class JSStreamConsumer final : public nsIInputStreamCallback, + public JS::OptimizedEncodingListener { + // A LengthPrefixType is stored at the start of the compressed optimized + // encoding, allowing the decompressed buffer to be allocated to exactly + // the right size. + using LengthPrefixType = uint32_t; + static const unsigned PrefixBytes = sizeof(LengthPrefixType); + + RefPtr<WindowStreamOwner> mWindowStreamOwner; + RefPtr<WorkerStreamOwner> mWorkerStreamOwner; + nsMainThreadPtrHandle<nsICacheInfoChannel> mCache; + const bool mOptimizedEncoding; + z_stream mZStream; + bool mZStreamInitialized; + Vector<uint8_t> mOptimizedEncodingBytes; + JS::StreamConsumer* mConsumer; + bool mConsumerAborted; + + JSStreamConsumer(already_AddRefed<WindowStreamOwner> aWindowStreamOwner, + nsIGlobalObject* aGlobal, JS::StreamConsumer* aConsumer, + nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache, + bool aOptimizedEncoding) + : mWindowStreamOwner(aWindowStreamOwner), + mCache(std::move(aCache)), + mOptimizedEncoding(aOptimizedEncoding), + mZStreamInitialized(false), + mConsumer(aConsumer), + mConsumerAborted(false) { + MOZ_DIAGNOSTIC_ASSERT(mWindowStreamOwner); + MOZ_DIAGNOSTIC_ASSERT(mConsumer); + } + + JSStreamConsumer(RefPtr<WorkerStreamOwner> aWorkerStreamOwner, + nsIGlobalObject* aGlobal, JS::StreamConsumer* aConsumer, + nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache, + bool aOptimizedEncoding) + : mWorkerStreamOwner(std::move(aWorkerStreamOwner)), + mCache(std::move(aCache)), + mOptimizedEncoding(aOptimizedEncoding), + mZStreamInitialized(false), + mConsumer(aConsumer), + mConsumerAborted(false) { + MOZ_DIAGNOSTIC_ASSERT(mWorkerStreamOwner); + MOZ_DIAGNOSTIC_ASSERT(mConsumer); + } + + ~JSStreamConsumer() { + if (mZStreamInitialized) { + inflateEnd(&mZStream); + } + + // Both WindowStreamOwner and WorkerStreamOwner need to be destroyed on + // their global's event target thread. + + if (mWindowStreamOwner) { + MOZ_DIAGNOSTIC_ASSERT(!mWorkerStreamOwner); + NS_ReleaseOnMainThread("JSStreamConsumer::mWindowStreamOwner", + mWindowStreamOwner.forget(), + /* aAlwaysProxy = */ true); + } else { + MOZ_DIAGNOSTIC_ASSERT(mWorkerStreamOwner); + WorkerStreamOwner::ProxyRelease(mWorkerStreamOwner.forget()); + } + + // Bug 1733674: these annotations currently do nothing, because they are + // member variables and the annotation mechanism only applies to locals. But + // the analysis could be extended so that these could replace the big-hammer + // ~JSStreamConsumer annotation and thus the analysis could check that + // nothing is added that might GC for a different reason. + JS_HAZ_VALUE_IS_GC_SAFE(mWindowStreamOwner); + JS_HAZ_VALUE_IS_GC_SAFE(mWorkerStreamOwner); + } + + static nsresult WriteSegment(nsIInputStream* aStream, void* aClosure, + const char* aFromSegment, uint32_t aToOffset, + uint32_t aCount, uint32_t* aWriteCount) { + JSStreamConsumer* self = reinterpret_cast<JSStreamConsumer*>(aClosure); + MOZ_DIAGNOSTIC_ASSERT(!self->mConsumerAborted); + + if (self->mOptimizedEncoding) { + if (!self->mZStreamInitialized) { + // mOptimizedEncodingBytes is used as temporary storage until we have + // the full prefix. + MOZ_ASSERT(self->mOptimizedEncodingBytes.length() < PrefixBytes); + uint32_t remain = PrefixBytes - self->mOptimizedEncodingBytes.length(); + uint32_t consume = std::min(remain, aCount); + + if (!self->mOptimizedEncodingBytes.append(aFromSegment, consume)) { + return NS_ERROR_UNEXPECTED; + } + + if (consume == remain) { + // Initialize zlib once all prefix bytes are loaded. + LengthPrefixType length; + memcpy(&length, self->mOptimizedEncodingBytes.begin(), PrefixBytes); + + if (!self->mOptimizedEncodingBytes.resizeUninitialized(length)) { + return NS_ERROR_UNEXPECTED; + } + + memset(&self->mZStream, 0, sizeof(self->mZStream)); + self->mZStream.avail_out = length; + self->mZStream.next_out = self->mOptimizedEncodingBytes.begin(); + + if (inflateInit(&self->mZStream) != Z_OK) { + return NS_ERROR_UNEXPECTED; + } + self->mZStreamInitialized = true; + } + + *aWriteCount = consume; + return NS_OK; + } + + // Zlib is initialized, overwrite the prefix with the inflated data. + + MOZ_DIAGNOSTIC_ASSERT(aCount > 0); + self->mZStream.avail_in = aCount; + self->mZStream.next_in = (uint8_t*)aFromSegment; + + int ret = inflate(&self->mZStream, Z_NO_FLUSH); + + MOZ_DIAGNOSTIC_ASSERT(ret == Z_OK || ret == Z_STREAM_END, + "corrupt optimized wasm cache file: data"); + MOZ_DIAGNOSTIC_ASSERT(self->mZStream.avail_in == 0, + "corrupt optimized wasm cache file: input"); + MOZ_DIAGNOSTIC_ASSERT_IF(ret == Z_STREAM_END, + self->mZStream.avail_out == 0); + // Gracefully handle corruption in release. + bool ok = + (ret == Z_OK || ret == Z_STREAM_END) && self->mZStream.avail_in == 0; + if (!ok) { + return NS_ERROR_UNEXPECTED; + } + } else { + // This callback can be called on any thread which is explicitly allowed + // by this particular JS API call. + if (!self->mConsumer->consumeChunk((const uint8_t*)aFromSegment, + aCount)) { + self->mConsumerAborted = true; + return NS_ERROR_UNEXPECTED; + } + } + + *aWriteCount = aCount; + return NS_OK; + } + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + static bool Start(nsCOMPtr<nsIInputStream> aStream, nsIGlobalObject* aGlobal, + WorkerPrivate* aMaybeWorker, JS::StreamConsumer* aConsumer, + nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache, + bool aOptimizedEncoding) { + nsCOMPtr<nsIAsyncInputStream> asyncStream; + nsresult rv = NS_MakeAsyncNonBlockingInputStream( + aStream.forget(), getter_AddRefs(asyncStream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + RefPtr<JSStreamConsumer> consumer; + if (aMaybeWorker) { + RefPtr<WorkerStreamOwner> owner = WorkerStreamOwner::Create( + asyncStream, aMaybeWorker, aGlobal->SerialEventTarget()); + if (!owner) { + return false; + } + + consumer = new JSStreamConsumer(std::move(owner), aGlobal, aConsumer, + std::move(aCache), aOptimizedEncoding); + } else { + RefPtr<WindowStreamOwner> owner = + WindowStreamOwner::Create(asyncStream, aGlobal); + if (!owner) { + return false; + } + + consumer = new JSStreamConsumer(owner.forget(), aGlobal, aConsumer, + std::move(aCache), aOptimizedEncoding); + } + + // This AsyncWait() creates a ref-cycle between asyncStream and consumer: + // + // asyncStream -> consumer -> (Window|Worker)StreamOwner -> asyncStream + // + // The cycle is broken when the stream completes or errors out and + // asyncStream drops its reference to consumer. + return NS_SUCCEEDED(asyncStream->AsyncWait(consumer, 0, 0, nullptr)); + } + + // nsIInputStreamCallback: + + NS_IMETHOD + OnInputStreamReady(nsIAsyncInputStream* aStream) override { + // Can be called on any stream. The JS API calls made below explicitly + // support being called from any thread. + MOZ_DIAGNOSTIC_ASSERT(!mConsumerAborted); + + nsresult rv; + + uint64_t available = 0; + rv = aStream->Available(&available); + if (NS_SUCCEEDED(rv) && available == 0) { + rv = NS_BASE_STREAM_CLOSED; + } + + if (rv == NS_BASE_STREAM_CLOSED) { + if (mOptimizedEncoding) { + // Gracefully handle corruption of compressed data stream in release. + // From on investigations in bug 1738987, the incomplete data cases + // mostly happen during shutdown. Some corruptions in the cache entry + // can still happen and will be handled in the WriteSegment above. + bool ok = mZStreamInitialized && mZStream.avail_out == 0; + if (!ok) { + mConsumer->streamError(size_t(NS_ERROR_UNEXPECTED)); + return NS_OK; + } + + mConsumer->consumeOptimizedEncoding(mOptimizedEncodingBytes.begin(), + mOptimizedEncodingBytes.length()); + } else { + // If there is cache entry associated with this stream, then listen for + // an optimized encoding so we can store it in the alt data. By JS API + // contract, the compilation process will hold a refcount to 'this' + // until it's done, optionally calling storeOptimizedEncoding(). + mConsumer->streamEnd(mCache ? this : nullptr); + } + return NS_OK; + } + + if (NS_FAILED(rv)) { + mConsumer->streamError(size_t(rv)); + return NS_OK; + } + + // Check mConsumerAborted before NS_FAILED to avoid calling streamError() + // if consumeChunk() returned false per JS API contract. + uint32_t written = 0; + rv = aStream->ReadSegments(WriteSegment, this, available, &written); + if (mConsumerAborted) { + return NS_OK; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + mConsumer->streamError(size_t(rv)); + return NS_OK; + } + + rv = aStream->AsyncWait(this, 0, 0, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + mConsumer->streamError(size_t(rv)); + return NS_OK; + } + + return NS_OK; + } + + // JS::OptimizedEncodingListener + + void storeOptimizedEncoding(const uint8_t* aSrcBytes, + size_t aSrcLength) override { + MOZ_ASSERT(mCache, "we only listen if there's a cache entry"); + + z_stream zstream; + memset(&zstream, 0, sizeof(zstream)); + zstream.avail_in = aSrcLength; + zstream.next_in = (uint8_t*)aSrcBytes; + + // The wins from increasing compression levels are tiny, while the time + // to compress increases drastically. For example, for a 148mb alt-data + // produced by a 40mb .wasm file, the level 2 takes 2.5s to get a 3.7x size + // reduction while level 9 takes 22.5s to get a 4x size reduction. Read-time + // wins from smaller compressed cache files are not found to be + // significant, thus the fastest compression level is used. (On test + // workloads, level 2 actually was faster *and* smaller than level 1.) + const int COMPRESSION = 2; + if (deflateInit(&zstream, COMPRESSION) != Z_OK) { + return; + } + auto autoDestroy = MakeScopeExit([&]() { deflateEnd(&zstream); }); + + Vector<uint8_t> dstBytes; + if (!dstBytes.resizeUninitialized(PrefixBytes + + deflateBound(&zstream, aSrcLength))) { + return; + } + + MOZ_RELEASE_ASSERT(LengthPrefixType(aSrcLength) == aSrcLength); + LengthPrefixType srcLength = aSrcLength; + memcpy(dstBytes.begin(), &srcLength, PrefixBytes); + + uint8_t* compressBegin = dstBytes.begin() + PrefixBytes; + zstream.next_out = compressBegin; + zstream.avail_out = dstBytes.length() - PrefixBytes; + + int ret = deflate(&zstream, Z_FINISH); + if (ret == Z_MEM_ERROR) { + return; + } + MOZ_RELEASE_ASSERT(ret == Z_STREAM_END); + + dstBytes.shrinkTo(zstream.next_out - dstBytes.begin()); + + NS_DispatchToMainThread(new StoreOptimizedEncodingRunnable( + std::move(mCache), std::move(dstBytes))); + } +}; + +NS_IMPL_ISUPPORTS(JSStreamConsumer, nsIInputStreamCallback) + +// static +const nsCString FetchUtil::WasmAltDataType; + +// static +void FetchUtil::InitWasmAltDataType() { + nsCString& type = const_cast<nsCString&>(WasmAltDataType); + MOZ_ASSERT(type.IsEmpty()); + + RunOnShutdown([]() { + // Avoid nsStringBuffer leak tests failures. + const_cast<nsCString&>(WasmAltDataType).Truncate(); + }); + + type.Append(nsLiteralCString("wasm-")); + + JS::BuildIdCharVector buildId; + if (!JS::GetOptimizedEncodingBuildId(&buildId)) { + MOZ_CRASH("build id oom"); + } + + type.Append(buildId.begin(), buildId.length()); +} + +static bool ThrowException(JSContext* aCx, unsigned errorNumber) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, errorNumber); + return false; +} + +// static +bool FetchUtil::StreamResponseToJS(JSContext* aCx, JS::Handle<JSObject*> aObj, + JS::MimeType aMimeType, + JS::StreamConsumer* aConsumer, + WorkerPrivate* aMaybeWorker) { + MOZ_ASSERT(!WasmAltDataType.IsEmpty()); + MOZ_ASSERT(!aMaybeWorker == NS_IsMainThread()); + + RefPtr<Response> response; + nsresult rv = UNWRAP_OBJECT(Response, aObj, response); + if (NS_FAILED(rv)) { + return ThrowException(aCx, JSMSG_WASM_BAD_RESPONSE_VALUE); + } + + const char* requiredMimeType = nullptr; + switch (aMimeType) { + case JS::MimeType::Wasm: + requiredMimeType = WASM_CONTENT_TYPE; + break; + } + + nsAutoCString mimeType; + nsAutoCString mixedCaseMimeType; // unused + response->GetMimeType(mimeType, mixedCaseMimeType); + + if (!mimeType.EqualsASCII(requiredMimeType)) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_WASM_BAD_RESPONSE_MIME_TYPE, mimeType.get(), + requiredMimeType); + return false; + } + + if (response->Type() != ResponseType::Basic && + response->Type() != ResponseType::Cors && + response->Type() != ResponseType::Default) { + return ThrowException(aCx, JSMSG_WASM_BAD_RESPONSE_CORS_SAME_ORIGIN); + } + + if (!response->Ok()) { + return ThrowException(aCx, JSMSG_WASM_BAD_RESPONSE_STATUS); + } + + if (response->BodyUsed()) { + return ThrowException(aCx, JSMSG_WASM_RESPONSE_ALREADY_CONSUMED); + } + + switch (aMimeType) { + case JS::MimeType::Wasm: + nsAutoString url; + response->GetUrl(url); + + IgnoredErrorResult result; + nsCString sourceMapUrl; + response->GetInternalHeaders()->Get("SourceMap"_ns, sourceMapUrl, result); + if (NS_WARN_IF(result.Failed())) { + return ThrowException(aCx, JSMSG_WASM_ERROR_CONSUMING_RESPONSE); + } + NS_ConvertUTF16toUTF8 urlUTF8(url); + aConsumer->noteResponseURLs( + urlUTF8.get(), sourceMapUrl.IsVoid() ? nullptr : sourceMapUrl.get()); + break; + } + + SafeRefPtr<InternalResponse> ir = response->GetInternalResponse(); + if (NS_WARN_IF(!ir)) { + return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); + } + + nsCOMPtr<nsIInputStream> stream; + + nsMainThreadPtrHandle<nsICacheInfoChannel> cache; + bool optimizedEncoding = false; + if (ir->HasCacheInfoChannel()) { + cache = ir->TakeCacheInfoChannel(); + + nsAutoCString altDataType; + if (NS_SUCCEEDED(cache->GetAlternativeDataType(altDataType)) && + WasmAltDataType.Equals(altDataType)) { + optimizedEncoding = true; + rv = cache->GetAlternativeDataInputStream(getter_AddRefs(stream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); + } + if (ir->HasBeenCloned()) { + // If `Response` is cloned, clone alternative data stream instance. + // The cache entry does not clone automatically, and multiple + // JSStreamConsumer instances will collide during read if not cloned. + nsCOMPtr<nsICloneableInputStream> original = do_QueryInterface(stream); + if (NS_WARN_IF(!original)) { + return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); + } + rv = original->Clone(getter_AddRefs(stream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); + } + } + } + } + + if (!optimizedEncoding) { + ir->GetUnfilteredBody(getter_AddRefs(stream)); + if (!stream) { + aConsumer->streamEnd(); + return true; + } + } + + MOZ_ASSERT(stream); + + IgnoredErrorResult error; + response->SetBodyUsed(aCx, error); + if (NS_WARN_IF(error.Failed())) { + return ThrowException(aCx, JSMSG_WASM_ERROR_CONSUMING_RESPONSE); + } + + nsIGlobalObject* global = xpc::NativeGlobal(js::UncheckedUnwrap(aObj)); + + if (!JSStreamConsumer::Start(stream, global, aMaybeWorker, aConsumer, + std::move(cache), optimizedEncoding)) { + return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); + } + + return true; +} + +// static +void FetchUtil::ReportJSStreamError(JSContext* aCx, size_t aErrorCode) { + // For now, convert *all* errors into AbortError. + + RefPtr<DOMException> e = DOMException::Create(NS_ERROR_DOM_ABORT_ERR); + + JS::Rooted<JS::Value> value(aCx); + if (!GetOrCreateDOMReflector(aCx, e, &value)) { + return; + } + + JS_SetPendingException(aCx, value); +} + +} // namespace mozilla::dom |