commit 3dfc96583b392d294604f0755cb507574254052f
parent fec1a197f44cb4e260ccd86069182491d70b7d42
Author: Randell Jesup <rjesup@mozilla.com>
Date: Tue, 7 Oct 2025 14:07:03 +0000
Bug 1917970: Support decoding brotli & zstd dictionary-encoded streams r=necko-reviewers,valentin
Differential Revision: https://phabricator.services.mozilla.com/D258125
Diffstat:
2 files changed, 139 insertions(+), 14 deletions(-)
diff --git a/netwerk/streamconv/converters/nsHTTPCompressConv.cpp b/netwerk/streamconv/converters/nsHTTPCompressConv.cpp
@@ -9,19 +9,21 @@
#include "nsCOMPtr.h"
#include "nsCRT.h"
#include "nsError.h"
+#include "nsIChannel.h"
+#include "nsIForcePendingChannel.h"
+#include "nsIHttpChannel.h"
+#include "nsIRequest.h"
+#include "nsIThreadRetargetableRequest.h"
#include "nsIThreadRetargetableStreamListener.h"
+#include "nsThreadUtils.h"
#include "nsStreamUtils.h"
#include "nsStringStream.h"
#include "nsComponentManagerUtils.h"
-#include "nsThreadUtils.h"
+#include "mozilla/net/Dictionary.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_network.h"
#include "mozilla/Logging.h"
-#include "nsIForcePendingChannel.h"
-#include "nsIRequest.h"
#include "mozilla/UniquePtrExtensions.h"
-#include "nsIThreadRetargetableRequest.h"
-#include "nsIChannel.h"
// brotli headers
#undef assert
@@ -35,17 +37,48 @@
namespace mozilla {
namespace net {
+class DictionaryCacheEntry;
+
extern LazyLogModule gHttpLog;
#define LOG(args) \
MOZ_LOG(mozilla::net::gHttpLog, mozilla::LogLevel::Debug, args)
+extern LazyLogModule gDictionaryLog;
+#define DICTIONARY_LOG(args) \
+ MOZ_LOG(mozilla::net::gDictionaryLog, mozilla::LogLevel::Debug, args)
+
class BrotliWrapper {
public:
- BrotliWrapper() {
- BrotliDecoderStateInit(&mState, nullptr, nullptr, nullptr);
- }
+ BrotliWrapper() {}
~BrotliWrapper() { BrotliDecoderStateCleanup(&mState); }
+ bool Init(nsIRequest* aRequest) {
+ if (!BrotliDecoderStateInit(&mState, nullptr, nullptr, nullptr)) {
+ return false;
+ }
+
+ nsCOMPtr<nsIHttpChannel> httpchannel(do_QueryInterface(aRequest));
+ if (!httpchannel) {
+ return false;
+ }
+ // XXX Wait for dictionary to be read into RAM!!
+ if (NS_SUCCEEDED(httpchannel->GetDictionary(getter_AddRefs(mDictionary))) &&
+ mDictionary) {
+ size_t length = mDictionary->GetDictionary().length();
+ DICTIONARY_LOG(("Brotli: dictionary %zu bytes", length));
+ if (length > 0) {
+ BROTLI_BOOL result = BrotliDecoderAttachDictionary(
+ &mState, BROTLI_SHARED_DICTIONARY_RAW, length,
+ mDictionary->GetDictionary().begin());
+ if (!result) {
+ DICTIONARY_LOG(("Brotli: AttachDictionary failed"));
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
BrotliDecoderState mState{};
Atomic<size_t, Relaxed> mTotalOut{0};
nsresult mStatus = NS_OK;
@@ -54,6 +87,11 @@ class BrotliWrapper {
nsIRequest* mRequest{nullptr};
nsISupports* mContext{nullptr};
uint64_t mSourceOffset{0};
+
+ RefPtr<DictionaryCacheEntry> mDictionary;
+
+ uint8_t mEaten{0};
+ uint8_t mHeader[36]; // \FF\44\43\42 + 32-byte SHA-256
};
#ifdef ZSTD_INFALLIBLE
@@ -67,7 +105,24 @@ ZSTD_customMem const zstd_allocators = {zstd_malloc, zstd_free, nullptr};
class ZstdWrapper {
public:
- ZstdWrapper() {
+ ZstdWrapper(nsIRequest* aRequest, nsHTTPCompressConv::CompressMode aMode) {
+ size_t length = 0;
+ if (aMode == nsHTTPCompressConv::HTTP_COMPRESS_ZSTD_DICTIONARY) {
+ nsCOMPtr<nsIHttpChannel> httpchannel(do_QueryInterface(aRequest));
+ if (httpchannel) {
+ // XXX Wait for dictionary to be read into RAM!!
+ if (NS_FAILED(
+ httpchannel->GetDictionary(getter_AddRefs(mDictionary))) ||
+ !mDictionary) {
+ return;
+ }
+ length = mDictionary->GetDictionary().length();
+ } else {
+ // Can't decode without a dictionary
+ return;
+ }
+ }
+
#ifdef ZSTD_INFALLIBLE
mDStream = ZSTD_createDStream_advanced(zstd_allocators); // infallible
#else
@@ -79,6 +134,15 @@ class ZstdWrapper {
return;
}
#endif
+ if (mDictionary) {
+ DICTIONARY_LOG(("zstd: dictionary %zu bytes", length));
+ ZSTD_DCtx_reset(mDStream, ZSTD_reset_session_only);
+ if (ZSTD_isError(ZSTD_DCtx_loadDictionary(
+ mDStream, mDictionary->GetDictionary().begin(), length))) {
+ return;
+ }
+ }
+
ZSTD_DCtx_setParameter(mDStream, ZSTD_d_windowLogMax, 23 /*8*1024*1024*/);
}
~ZstdWrapper() {
@@ -93,6 +157,8 @@ class ZstdWrapper {
nsISupports* mContext{nullptr};
uint64_t mSourceOffset{0};
ZSTD_DStream* mDStream{nullptr};
+
+ RefPtr<DictionaryCacheEntry> mDictionary;
};
// nsISupports implementation
@@ -160,6 +226,12 @@ nsHTTPCompressConv::AsyncConvertData(const char* aFromType, const char* aToType,
} else if (!nsCRT::strncasecmp(aFromType, HTTP_ZST_TYPE,
sizeof(HTTP_ZST_TYPE) - 1)) {
mMode = HTTP_COMPRESS_ZSTD;
+ } else if (!nsCRT::strncasecmp(aFromType, HTTP_BROTLI_DICTIONARY_TYPE,
+ sizeof(HTTP_BROTLI_DICTIONARY_TYPE) - 1)) {
+ mMode = HTTP_COMPRESS_BROTLI_DICTIONARY;
+ } else if (!nsCRT::strncasecmp(aFromType, HTTP_ZSTD_DICTIONARY_TYPE,
+ sizeof(HTTP_ZSTD_DICTIONARY_TYPE) - 1)) {
+ mMode = HTTP_COMPRESS_ZSTD_DICTIONARY;
}
LOG(("nsHttpCompresssConv %p AsyncConvertData %s %s mode %d\n", this,
aFromType, aToType, (CompressMode)mMode));
@@ -271,7 +343,8 @@ nsHTTPCompressConv::OnStopRequest(nsIRequest* request, nsresult aStatus) {
status = NS_ERROR_NET_PARTIAL_TRANSFER;
LOG(("nsHttpCompresssConv %p onstop partial gzip\n", this));
}
- if (NS_SUCCEEDED(status) && mMode == HTTP_COMPRESS_BROTLI) {
+ if (NS_SUCCEEDED(status) && (mMode == HTTP_COMPRESS_BROTLI ||
+ mMode == HTTP_COMPRESS_BROTLI_DICTIONARY)) {
nsCOMPtr<nsIForcePendingChannel> fpChannel = do_QueryInterface(request);
bool isPending = false;
if (request) {
@@ -289,6 +362,17 @@ nsHTTPCompressConv::OnStopRequest(nsIRequest* request, nsresult aStatus) {
fpChannel->ForcePending(false);
}
}
+ // We don't need the dictionary data anymore
+ if (mBrotli || mZstd) {
+ RefPtr<DictionaryCacheEntry> dict;
+ nsCOMPtr<nsIHttpChannel> httpchannel(do_QueryInterface(request));
+ if (httpchannel) {
+ httpchannel->SetDictionary(nullptr);
+ }
+ // paranoia
+ mBrotli = nullptr;
+ mZstd = nullptr;
+ }
nsCOMPtr<nsIStreamListener> listener;
{
@@ -319,6 +403,35 @@ nsresult nsHTTPCompressConv::BrotliHandler(nsIInputStream* stream,
return NS_OK;
}
+ // Dictionary-encoded brotli has a 36-byte header (4bit fixed + 32bit SHA-256)
+ if (self->mBrotli->mDictionary && self->mBrotli->mEaten < 36) {
+ uint8_t header_needed = 36 - self->mBrotli->mEaten;
+ if (avail >= header_needed) {
+ memcpy(&self->mBrotli->mHeader[self->mBrotli->mEaten], dataIn,
+ header_needed);
+ avail -= header_needed;
+ dataIn += header_needed;
+ self->mBrotli->mEaten = 36;
+
+ // Validate header
+ // XXX we could verify the SHA-256 matches what we offered
+ static uint8_t brotli_header[4] = {0xff, 0x44, 0x43, 0x42};
+ if (memcmp(self->mBrotli->mHeader, brotli_header, 4) != 0) {
+ DICTIONARY_LOG(
+ ("!! %p Brotli failed: bad magic header 0x%02x%02x%02x%02x", self,
+ self->mBrotli->mHeader[0], self->mBrotli->mHeader[1],
+ self->mBrotli->mHeader[2], self->mBrotli->mHeader[3]));
+ self->mBrotli->mStatus = NS_ERROR_INVALID_CONTENT_ENCODING;
+ return self->mBrotli->mStatus;
+ }
+ } else {
+ memcpy(&self->mBrotli->mHeader[self->mBrotli->mEaten], dataIn, aAvail);
+ self->mBrotli->mEaten += aAvail;
+ *countRead = aAvail;
+ return NS_OK;
+ }
+ }
+
auto outBuffer = MakeUniqueFallible<uint8_t[]>(kOutSize);
if (outBuffer == nullptr) {
self->mBrotli->mStatus = NS_ERROR_OUT_OF_MEMORY;
@@ -344,7 +457,10 @@ nsresult nsHTTPCompressConv::BrotliHandler(nsIInputStream* stream,
self, static_cast<uint32_t>(res), outSize));
if (res == BROTLI_DECODER_RESULT_ERROR) {
- LOG(("nsHttpCompressConv %p marking invalid encoding", self));
+ DICTIONARY_LOG(
+ ("nsHttpCompressConv %p decoding error: marking invalid encoding "
+ "(%zu)",
+ self, avail));
self->mBrotli->mStatus = NS_ERROR_INVALID_CONTENT_ENCODING;
return self->mBrotli->mStatus;
}
@@ -699,9 +815,13 @@ nsHTTPCompressConv::OnDataAvailable(nsIRequest* request, nsIInputStream* iStr,
} /* gzip */
break;
- case HTTP_COMPRESS_BROTLI: {
+ case HTTP_COMPRESS_BROTLI:
+ case HTTP_COMPRESS_BROTLI_DICTIONARY: {
if (!mBrotli) {
mBrotli = MakeUnique<BrotliWrapper>();
+ if (!mBrotli->Init(request)) {
+ return NS_ERROR_FAILURE;
+ }
}
mBrotli->mRequest = request;
@@ -718,9 +838,10 @@ nsHTTPCompressConv::OnDataAvailable(nsIRequest* request, nsIInputStream* iStr,
}
} break;
- case HTTP_COMPRESS_ZSTD: {
+ case HTTP_COMPRESS_ZSTD:
+ case HTTP_COMPRESS_ZSTD_DICTIONARY: {
if (!mZstd) {
- mZstd = MakeUnique<ZstdWrapper>();
+ mZstd = MakeUnique<ZstdWrapper>(request, mMode);
if (!mZstd->mDStream) {
return NS_ERROR_OUT_OF_MEMORY;
}
diff --git a/netwerk/streamconv/converters/nsHTTPCompressConv.h b/netwerk/streamconv/converters/nsHTTPCompressConv.h
@@ -35,6 +35,8 @@ class nsIStringInputStream;
# define HTTP_UNCOMPRESSED_TYPE "uncompressed"
# define HTTP_ZSTD_TYPE "zstd"
# define HTTP_ZST_TYPE "zst"
+# define HTTP_BROTLI_DICTIONARY_TYPE "dcb"
+# define HTTP_ZSTD_DICTIONARY_TYPE "dcz"
namespace mozilla {
namespace net {
@@ -64,6 +66,8 @@ class nsHTTPCompressConv : public nsIStreamConverter,
HTTP_COMPRESS_BROTLI,
HTTP_COMPRESS_IDENTITY,
HTTP_COMPRESS_ZSTD,
+ HTTP_COMPRESS_BROTLI_DICTIONARY,
+ HTTP_COMPRESS_ZSTD_DICTIONARY,
};
private: