/* 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 "js/BuildId.h"
#include "js/friend/ErrorMessages.h"  // JSMSG_*
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/dom/DOMException.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/InternalRequest.h"
#include "mozilla/dom/ReferrerInfo.h"
#include "mozilla/dom/Response.h"
#include "mozilla/dom/WorkerRef.h"
#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 "zlib.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::IncrementPendingKeepaliveRequestSize(
    nsILoadGroup* aLoadGroup, const uint64_t aBodyLength) {
  uint64_t pendingKeepaliveRequestSize = 0;
  MOZ_ASSERT(aLoadGroup);

  aLoadGroup->GetTotalKeepAliveBytes(&pendingKeepaliveRequestSize);
  pendingKeepaliveRequestSize += aBodyLength;

  if (pendingKeepaliveRequestSize > FETCH_KEEPALIVE_MAX_SIZE) {
    return false;
  }

  aLoadGroup->SetTotalKeepAliveBytes(pendingKeepaliveRequestSize);
  return true;
}

// static
void FetchUtil::DecrementPendingKeepaliveRequestSize(
    nsILoadGroup* aLoadGroup, const uint64_t aBodyLength) {
  MOZ_ASSERT(aLoadGroup);

  uint64_t pendingKeepaliveRequestSize = 0;
  aLoadGroup->GetTotalKeepAliveBytes(&pendingKeepaliveRequestSize);
  MOZ_ASSERT(pendingKeepaliveRequestSize >= aBodyLength);
  pendingKeepaliveRequestSize -= aBodyLength;
  aLoadGroup->SetTotalKeepAliveBytes(pendingKeepaliveRequestSize);
}

// static
nsCOMPtr<nsILoadGroup> FetchUtil::GetLoadGroupFromGlobal(
    nsIGlobalObject* aGlobalObject) {
  MOZ_ASSERT(NS_IsMainThread());
  nsCOMPtr<nsILoadGroup> loadGroup = nullptr;
  auto* innerWindow = aGlobalObject->GetAsInnerWindow();

  if (innerWindow) {
    Document* doc = innerWindow->GetExtantDoc();
    if (doc) {
      loadGroup = doc->GetDocumentLoadGroup();
    }
  }

  return loadGroup;
}

// 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;
  nsAutoCString 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);

  nsAutoCString computedReferrerSpec;
  referrerInfo = aChannel->GetReferrerInfo();
  if (referrerInfo) {
    (void)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::GetWasmAltDataType(),
                                             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 GlobalTeardownObserver {
 private:
  // Read from any thread but only set/cleared on the main thread. The lifecycle
  // of WindowStreamOwner prevents concurrent read/clear.
  nsCOMPtr<nsIAsyncInputStream> mStream;

  ~WindowStreamOwner() { MOZ_ASSERT(NS_IsMainThread()); }

 public:
  NS_DECL_ISUPPORTS

  WindowStreamOwner(nsIAsyncInputStream* aStream, nsIGlobalObject* aGlobal)
      : GlobalTeardownObserver(aGlobal), mStream(aStream) {
    MOZ_DIAGNOSTIC_ASSERT(aGlobal);
    MOZ_ASSERT(NS_IsMainThread());
  }

  // GlobalTeardownObserver:

  void DisconnectFromOwner() override {
    MOZ_ASSERT(NS_IsMainThread());

    if (!mStream) {
      return;
    }

    // mStream->Close() will call JSStreamConsumer::OnInputStreamReady which may
    // then destory itself, but GTO should be strongly grabbing us right as it's
    // calling DisconnectFromOwner.

    mStream->Close();
    mStream = nullptr;

    GlobalTeardownObserver::DisconnectFromOwner();
  }
};

NS_IMPL_ISUPPORTS0(WindowStreamOwner)

inline nsISupports* ToSupports(WindowStreamOwner* aObj) {
  return static_cast<GlobalTeardownObserver*>(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 =
          new WindowStreamOwner(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
constinit nsCString FetchUtil::WasmAltDataType;

// static
void FetchUtil::InitWasmAltDataType() {
  MOZ_ASSERT(WasmAltDataType.IsEmpty());

  RunOnShutdown([]() {
    // Avoid StringBuffer leak tests failures.
    WasmAltDataType.Truncate();
  });

  WasmAltDataType.Append(nsLiteralCString("wasm-"));

  JS::BuildIdCharVector buildId;
  if (!JS::GetOptimizedEncodingBuildId(&buildId)) {
    MOZ_CRASH("build id oom");
  }

  WasmAltDataType.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;
  }

  // For WASM, the Content-Type must be exactly "application/wasm" with no
  // parameters. Check the raw header value before parsing normalizes it.
  ErrorResult result;
  nsAutoCString mimeType;
  response->GetInternalHeaders()->Get("Content-Type"_ns, mimeType, result);
  MOZ_ALWAYS_TRUE(!result.Failed());
  ToLowerCase(mimeType);

  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:
      nsAutoCString 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);
      }
      aConsumer->noteResponseURLs(
          url.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
