commit 193a6084807c7c2397f6eba2910cca1717a8b780
parent fb79c5cf992b6e71fc8b1794fbb0db54d974d70c
Author: Randell Jesup <rjesup@mozilla.com>
Date: Wed, 1 Oct 2025 18:45:54 +0000
Bug 1917982: Support use_as_dictionary response headers r=necko-reviewers,kershaw
Differential Revision: https://phabricator.services.mozilla.com/D258122
Diffstat:
12 files changed, 416 insertions(+), 1 deletion(-)
diff --git a/netwerk/base/nsNetUtil.cpp b/netwerk/base/nsNetUtil.cpp
@@ -4044,6 +4044,128 @@ void WarnIgnoredPreload(const mozilla::dom::Document& aDoc, nsIURI& aURI) {
"PreloadIgnoredInvalidAttr", params);
}
+bool NS_ParseUseAsDictionary(const nsACString& aValue, nsACString& aMatch,
+ nsACString& aMatchId,
+ nsTArray<nsCString>& aMatchDestItems,
+ nsACString& aType) {
+ // Note: match= is required
+ // Use-As-Dictionary = %s"match" /
+ // %il"match-dest" /
+ // %s"id" \
+ // %t"type" ; case-sensitive
+
+ nsCOMPtr<nsISFVService> sfv = GetSFVService();
+
+ nsCOMPtr<nsISFVDictionary> parsedHeader;
+ nsresult rv;
+ if (NS_FAILED(
+ rv = sfv->ParseDictionary(aValue, getter_AddRefs(parsedHeader)))) {
+ return false;
+ }
+
+ nsCOMPtr<nsISFVItemOrInnerList> match;
+ rv = parsedHeader->Get("match"_ns, getter_AddRefs(match));
+ if (NS_FAILED(rv)) {
+ return false; // match is required, fail if not found
+ }
+ if (nsCOMPtr<nsISFVItem> listItem = do_QueryInterface(match)) {
+ nsCOMPtr<nsISFVBareItem> value;
+ rv = listItem->GetValue(getter_AddRefs(value));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ if (nsCOMPtr<nsISFVString> stringVal = do_QueryInterface(value)) {
+ if (NS_FAILED(stringVal->GetValue(aMatch))) {
+ return false;
+ }
+ if (aMatch.IsEmpty()) {
+ return false; // match is required, fail if not found
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+
+ nsCOMPtr<nsISFVItemOrInnerList> matchdest;
+ rv = parsedHeader->Get("match-dest"_ns, getter_AddRefs(matchdest));
+ if (NS_SUCCEEDED(rv)) {
+ if (nsCOMPtr<nsISFVInnerList> innerList = do_QueryInterface(matchdest)) {
+ // Extract the first entry of each inner list, which should contain the
+ // endpoint's URL string
+ nsTArray<RefPtr<nsISFVItem>> items;
+ if (NS_FAILED(innerList->GetItems(items))) {
+ return false;
+ }
+ // Don't check items.IsEmpty() because an empty list is valid
+
+ for (auto& item : items) {
+ nsCOMPtr<nsISFVBareItem> value;
+ if (NS_FAILED(item->GetValue(getter_AddRefs(value)))) {
+ return false;
+ }
+ if (nsCOMPtr<nsISFVString> stringVal = do_QueryInterface(value)) {
+ nsAutoCString string;
+ if (NS_FAILED(stringVal->GetValue(string))) {
+ return false;
+ }
+ aMatchDestItems.AppendElement(string);
+ } else {
+ return false; // match-dest is an inner list of strings
+ }
+ }
+ }
+ }
+
+ nsCOMPtr<nsISFVItemOrInnerList> matchid;
+ rv = parsedHeader->Get("id"_ns, getter_AddRefs(matchid));
+ if (NS_SUCCEEDED(rv)) {
+ if (nsCOMPtr<nsISFVItem> listItem = do_QueryInterface(matchid)) {
+ nsCOMPtr<nsISFVBareItem> value;
+ rv = listItem->GetValue(getter_AddRefs(value));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ if (nsCOMPtr<nsISFVString> stringVal = do_QueryInterface(value)) {
+ if (NS_FAILED(stringVal->GetValue(aMatchId))) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ nsCOMPtr<nsISFVItemOrInnerList> type;
+ rv = parsedHeader->Get("type"_ns, getter_AddRefs(type));
+ if (NS_SUCCEEDED(rv)) {
+ if (nsCOMPtr<nsISFVItem> listItem = do_QueryInterface(type)) {
+ nsCOMPtr<nsISFVBareItem> value;
+ rv = listItem->GetValue(getter_AddRefs(value));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ if (nsCOMPtr<nsISFVToken> tokenVal = do_QueryInterface(value)) {
+ if (NS_FAILED(tokenVal->GetValue(aType))) {
+ return false;
+ }
+ if (!aType.Equals("raw"_ns)) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+}
+
nsresult HasRootDomain(const nsACString& aInput, const nsACString& aHost,
bool* aResult) {
if (NS_WARN_IF(!aResult)) {
diff --git a/netwerk/base/nsNetUtil.h b/netwerk/base/nsNetUtil.h
@@ -1165,6 +1165,13 @@ bool CheckPreloadAttrs(const nsAttrValue& aAs, const nsAString& aType,
mozilla::dom::Document* aDocument);
void WarnIgnoredPreload(const mozilla::dom::Document&, nsIURI&);
+// Implements parsing of Use-As-Dictionary headers for Compression Dictionary
+// support.
+bool NS_ParseUseAsDictionary(const nsACString& aValue, nsACString& aMatch,
+ nsACString& aMatchId,
+ nsTArray<nsCString>& aMatchDestItems,
+ nsACString& aType);
+
/**
* Returns true if the |aInput| in is part of the root domain of |aHost|.
* For example, if |aInput| is "www.mozilla.org", and we pass in
diff --git a/netwerk/cache2/CacheEntry.h b/netwerk/cache2/CacheEntry.h
@@ -24,6 +24,7 @@
#include "mozilla/Attributes.h"
#include "mozilla/Mutex.h"
#include "mozilla/TimeStamp.h"
+#include "Dictionary.h"
static inline uint32_t PRTimeToSeconds(PRTime t_usec) {
return uint32_t(t_usec / PR_USEC_PER_SEC);
@@ -130,6 +131,8 @@ class CacheEntry final : public nsIRunnable,
TimeStamp const& LoadStart() const { return mLoadStart; }
+ void SetDictionary(DictionaryCacheEntry* aDict) { mDict = aDict; }
+
enum EPurge {
PURGE_DATA_ONLY_DISK_BACKED,
PURGE_WHOLE_ONLY_DISK_BACKED,
@@ -427,6 +430,8 @@ class CacheEntry final : public nsIRunnable,
mozilla::TimeStamp mLoadStart;
uint32_t mUseCount{0};
+ RefPtr<DictionaryCacheEntry> mDict;
+
const uint64_t mCacheEntryId;
};
diff --git a/netwerk/cache2/CacheFileIOManager.cpp b/netwerk/cache2/CacheFileIOManager.cpp
@@ -3384,6 +3384,7 @@ nsresult CacheFileIOManager::EvictByContext(
LOG(("CacheFileIOManager::EvictByContext() [loadContextInfo=%p]",
aLoadContextInfo));
+ // XXX evict dictionary data from memory cache
nsresult rv;
RefPtr<CacheFileIOManager> ioMan = gInstance;
diff --git a/netwerk/cache2/Dictionary.h b/netwerk/cache2/Dictionary.h
@@ -19,6 +19,8 @@
#include "nsString.h"
#include "nsTArray.h"
#include "mozilla/TimeStamp.h"
+#include "nsTHashMap.h"
+#include "nsHashKeys.h"
class nsICacheStorage;
class nsIIOService;
diff --git a/netwerk/protocol/http/HttpBaseChannel.h b/netwerk/protocol/http/HttpBaseChannel.h
@@ -874,6 +874,10 @@ class HttpBaseChannel : public nsHashPropertyBag,
nsIRequest::TRRMode mEffectiveTRRMode = nsIRequest::TRR_DEFAULT_MODE;
TRRSkippedReason mTRRSkipReason = TRRSkippedReason::TRR_UNSET;
+ // Dictionary entry - retain while we're saving the data, and while we're
+ // fetching data possibly encoded with the entry
+ RefPtr<DictionaryCacheEntry> mDict;
+
public:
void SetEarlyHints(
nsTArray<mozilla::net::EarlyHintConnectArgs>&& aEarlyHints);
diff --git a/netwerk/protocol/http/nsHttpAtomList.h b/netwerk/protocol/http/nsHttpAtomList.h
@@ -104,6 +104,7 @@ HTTP_ATOM(Transfer_Encoding, "Transfer-Encoding")
HTTP_ATOM(URI, "URI")
HTTP_ATOM(Upgrade, "Upgrade")
HTTP_ATOM(User_Agent, "User-Agent")
+HTTP_ATOM(Use_As_Dictionary, "Use-As-Dictionary")
HTTP_ATOM(Vary, "Vary")
HTTP_ATOM(Version, "Version")
HTTP_ATOM(WWW_Authenticate, "WWW-Authenticate")
diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -113,6 +113,7 @@
#include "nsISocketProvider.h"
#include "mozilla/extensions/StreamFilterParent.h"
#include "mozilla/net/Predictor.h"
+#include "mozilla/net/SFVService.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/NullPrincipal.h"
#include "CacheControlParser.h"
@@ -5899,12 +5900,50 @@ nsresult DoAddCacheEntryHeaders(nsHttpChannel* self, nsICacheEntry* entry,
rv = entry->SetMetaDataElement("original-response-headers", head.get());
if (NS_FAILED(rv)) return rv;
+ // If this is being marked as a dictionary, add it to the list
+ self->ParseDictionary(entry, responseHead);
+
// Indicate we have successfully finished setting metadata on the cache entry.
rv = entry->MetaDataReady();
return rv;
}
+bool nsHttpChannel::ParseDictionary(nsICacheEntry* aEntry,
+ nsHttpResponseHead* aResponseHead) {
+ nsAutoCString val;
+ if (NS_SUCCEEDED(aResponseHead->GetHeader(nsHttp::Use_As_Dictionary, val))) {
+ nsAutoCStringN<128> matchVal;
+ nsAutoCStringN<64> matchIdVal;
+ nsTArray<nsCString> matchDestItems;
+ nsAutoCString typeVal;
+
+ if (!NS_ParseUseAsDictionary(val, matchVal, matchIdVal, matchDestItems,
+ typeVal)) {
+ return false;
+ }
+
+ nsCString key;
+ nsresult rv;
+ if (NS_FAILED(rv = aEntry->GetKey(key))) {
+ return false;
+ }
+ nsCString hash;
+ // Available now for use
+ RefPtr<DictionaryCache> dicts(DictionaryCache::GetInstance());
+ LOG(
+ ("Adding DictionaryCache entry for %s: key %s, matchval %s, id=%s, "
+ "type=%s",
+ mURI->GetSpecOrDefault().get(), key.get(), matchVal.get(),
+ matchIdVal.get(), typeVal.get()));
+ dicts->AddEntry(mURI, key, matchVal, matchIdVal, Some(hash),
+ getter_AddRefs(mDict));
+ mDict->InUse();
+ return true;
+ }
+ return false;
+}
+
nsresult nsHttpChannel::AddCacheEntryHeaders(nsICacheEntry* entry) {
return DoAddCacheEntryHeaders(this, entry, &mRequestHead, mResponseHead.get(),
mSecurityInfo);
diff --git a/netwerk/protocol/http/nsHttpChannel.h b/netwerk/protocol/http/nsHttpChannel.h
@@ -412,6 +412,8 @@ class nsHttpChannel final : public HttpBaseChannel,
void CloseCacheEntry(bool doomOnFailure);
[[nodiscard]] nsresult InitCacheEntry();
void UpdateInhibitPersistentCachingFlag();
+ bool ParseDictionary(nsICacheEntry* aEntry,
+ nsHttpResponseHead* aResponseHead);
[[nodiscard]] nsresult AddCacheEntryHeaders(nsICacheEntry* entry);
[[nodiscard]] nsresult FinalizeCacheEntry();
[[nodiscard]] nsresult InstallCacheListener(int64_t offset = 0);
diff --git a/netwerk/protocol/http/nsHttpHandler.cpp b/netwerk/protocol/http/nsHttpHandler.cpp
@@ -670,8 +670,11 @@ nsresult nsHttpHandler::AddAcceptAndDictionaryHeaders(
if (NS_FAILED(rv)) {
return rv;
}
+ LOG(("Setting Accept-Encoding: %s",
+ PromiseFlatCString(mDictionaryAcceptEncodings).get()));
- LOG(("Found dictionary %s", PromiseFlatCString(dict->GetHash()).get()));
+ LOG(("Setting Available-Dictionary: %s",
+ PromiseFlatCString(dict->GetHash()).get()));
rv = aRequest->SetHeader(nsHttp::Available_Dictionary, dict->GetHash(),
false, nsHttpHeaderArray::eVarietyRequestOverride);
if (NS_FAILED(rv)) {
@@ -683,6 +686,8 @@ nsresult nsHttpHandler::AddAcceptAndDictionaryHeaders(
if (NS_FAILED(rv)) {
return rv;
}
+ LOG(("Setting Dictionary-Id: %s",
+ PromiseFlatCString(dict->GetId()).get()));
}
// Need to retain access to the dictionary until the request completes.
// Note that this includes if the dictionary we offered gets replaced
diff --git a/netwerk/test/gtest/TestUseAsDictionary.cpp b/netwerk/test/gtest/TestUseAsDictionary.cpp
@@ -0,0 +1,226 @@
+/* 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 <ostream>
+
+#include "gtest/gtest-param-test.h"
+#include "gtest/gtest.h"
+
+#include "mozilla/gtest/MozAssertions.h"
+#include "nsNetUtil.h"
+
+using namespace mozilla::net;
+
+struct TestData {
+ bool mResult;
+ const nsCString mHeader;
+ // output to match:
+ const nsCString mMatchVal;
+ const nsCString mMatchIdVal;
+ const nsCString mTypeVal;
+ // matchDestVal ends with ""_ns
+ const nsCString mMatchDestVal[5];
+};
+
+TEST(TestUseAsDictionary, Match)
+{
+ // Note: we're not trying to test Structured Fields
+ // (https://datatracker.ietf.org/doc/html/rfc8941) here, but the data within
+ // it, so generally we aren't looking for format errors
+ const struct TestData gTestArray[] = {
+ {true,
+ "match=\"/app/*/main.js\""_ns,
+ "/app/*/main.js"_ns,
+ ""_ns,
+ ""_ns,
+ {""_ns}},
+ {true,
+ "match=\"/app/*/main.js\", id=\"some_id\""_ns,
+ "/app/*/main.js"_ns,
+ "some_id"_ns,
+ ""_ns,
+ {""_ns}},
+ // match= is required
+ {false, "id=\"some_id\""_ns, ""_ns, "some_id"_ns, ""_ns, {""_ns}},
+ {true,
+ "match=\"/app/*/main.js\", id=\"some_id\", type=raw"_ns,
+ "/app/*/main.js"_ns,
+ "some_id"_ns,
+ "raw"_ns,
+ {""_ns}},
+ // only raw is supported for type
+ {false,
+ "match=\"/app/*/main.js\", id=\"some_id\", type=not_raw"_ns,
+ "/app/*/main.js"_ns,
+ "some_id"_ns,
+ "raw"_ns,
+ {""_ns}},
+ {true,
+ "match=\"/app/*/main.js\", id=\"some_id\", match-dest=(\"style\")"_ns,
+ "/app/*/main.js"_ns,
+ "some_id"_ns,
+ ""_ns,
+ {"style"_ns, ""_ns}},
+ {true,
+ "match=\"/app/*/main.js\", id=\"some_id\", match-dest=(\"style\"), type=raw"_ns,
+ "/app/*/main.js"_ns,
+ "some_id"_ns,
+ "raw"_ns,
+ {"style"_ns, ""_ns}},
+ {true,
+ "match=\"/app/*/main.js\", id=\"some_id\", match-dest=(\"style\" \"document\"), type=raw"_ns,
+ "/app/*/main.js"_ns,
+ "some_id"_ns,
+ "raw"_ns,
+ {"style"_ns, "document"_ns, ""_ns}},
+ // adding the comma after style is a syntax error for structured fields
+ {false,
+ "match=\"/app/*/main.js\", id=\"some_id\", match-dest=(\"style\", \"document\"), type=raw"_ns,
+ "/app/*/main.js"_ns,
+ "some_id"_ns,
+ "raw"_ns,
+ {"style"_ns, "document"_ns, ""_ns}},
+ // --- Additional tests for spec compliance and edge cases ---
+ // 1. Missing quotes around match value
+ {false,
+ "match=/app/*/main.js, id=\"id1\""_ns,
+ ""_ns,
+ "id1"_ns,
+ ""_ns,
+ {""_ns}},
+ // 2. Extra unknown parameter
+ {true,
+ "match=\"/foo.js\", foo=bar"_ns,
+ "/foo.js"_ns,
+ ""_ns,
+ ""_ns,
+ {""_ns}},
+ // 3. Whitespace variations
+ {true,
+ " match=\"/foo.js\" , id=\"id2\" "_ns,
+ "/foo.js"_ns,
+ "id2"_ns,
+ ""_ns,
+ {""_ns}},
+ // 4. Empty match value
+ {false, "match=\"\""_ns, ""_ns, ""_ns, ""_ns, {""_ns}},
+ // 5. Duplicate match parameter (should use the last)
+ {true,
+ "match=\"/foo.js\", match=\"/bar.js\""_ns,
+ "/bar.js"_ns,
+ ""_ns,
+ ""_ns,
+ {""_ns}},
+ // 6. Duplicate id parameter (should use the last)
+ {true,
+ "match=\"/foo.js\", id=\"id1\", id=\"id2\""_ns,
+ "/foo.js"_ns,
+ "id2"_ns,
+ ""_ns,
+ {""_ns}},
+ // 7. Parameter order: id before match
+ {true,
+ "id=\"id3\", match=\"/foo.js\""_ns,
+ "/foo.js"_ns,
+ "id3"_ns,
+ ""_ns,
+ {""_ns}},
+ // 8. Non-raw type (should fail)
+ {false,
+ "match=\"/foo.js\", type=compressed"_ns,
+ "/foo.js"_ns,
+ ""_ns,
+ "raw"_ns,
+ {""_ns}},
+ // 9. Empty header
+ {false, ""_ns, ""_ns, ""_ns, ""_ns, {""_ns}},
+ // 10. match-dest with empty list
+ {true,
+ "match=\"/foo.js\", match-dest=()"_ns,
+ "/foo.js"_ns,
+ ""_ns,
+ ""_ns,
+ {""_ns}},
+ // 11. match-dest with whitespace and multiple values
+ {true,
+ "match=\"/foo.js\", match-dest=( \"a\" \"b\" )"_ns,
+ "/foo.js"_ns,
+ ""_ns,
+ ""_ns,
+ {"a"_ns, "b"_ns, ""_ns}},
+ // 12. match-dest with invalid value (missing quotes)
+ {false,
+ "match=\"/foo.js\", match-dest=(a)"_ns,
+ "/foo.js"_ns,
+ ""_ns,
+ ""_ns,
+ {""_ns}},
+ // 13. match-dest with duplicate values
+ {true,
+ "match=\"/foo.js\", match-dest=(\"a\" \"a\")"_ns,
+ "/foo.js"_ns,
+ ""_ns,
+ ""_ns,
+ {"a"_ns, "a"_ns, ""_ns}},
+ // 14. Case sensitivity: type=RAW (should fail, only 'raw' allowed)
+ {false,
+ "match=\"/foo.js\", type=RAW"_ns,
+ "/foo.js"_ns,
+ ""_ns,
+ "raw"_ns,
+ {""_ns}},
+
+ // Note: Structured Fields requires all input to be ASCII
+
+ // 18. match-dest with trailing whitespace
+ {true,
+ "match=\"/foo.js\", match-dest=(\"a\" )"_ns,
+ "/foo.js"_ns,
+ ""_ns,
+ ""_ns,
+ {"a"_ns, ""_ns}},
+ // 19. match-dest with only whitespace in list (should be empty)
+ {true,
+ "match=\"/foo.js\", match-dest=( )"_ns,
+ "/foo.js"_ns,
+ ""_ns,
+ ""_ns,
+ {""_ns}},
+ // 20. match-dest with comma and whitespace (invalid)
+ {false,
+ "match=\"/foo.js\", match-dest=(\"a\", \"b\")"_ns,
+ "/foo.js"_ns,
+ ""_ns,
+ ""_ns,
+ {"a"_ns, "b"_ns, ""_ns}},
+ };
+
+ for (auto& test : gTestArray) {
+ nsCString match, matchId, type;
+ nsTArray<nsCString> matchDest;
+ nsTArray<nsCString> matchDestVal;
+
+ for (auto& dest : test.mMatchDestVal) {
+ if (dest.IsEmpty()) {
+ break;
+ }
+ matchDestVal.AppendElement(dest);
+ }
+
+ fprintf(stderr, "Testing %s\n", test.mHeader.get());
+ ASSERT_EQ(
+ NS_ParseUseAsDictionary(test.mHeader, match, matchId, matchDest, type),
+ test.mResult);
+
+ if (test.mResult) {
+ ASSERT_EQ(match, test.mMatchVal);
+ ASSERT_EQ(matchId, test.mMatchIdVal);
+ ASSERT_EQ(matchDest.Length(), matchDestVal.Length());
+ for (size_t i = 0; i < matchDest.Length(); i++) {
+ ASSERT_EQ(matchDest[i], matchDestVal[i]);
+ }
+ ASSERT_EQ(type, test.mTypeVal);
+ }
+ }
+}
diff --git a/netwerk/test/gtest/moz.build b/netwerk/test/gtest/moz.build
@@ -41,6 +41,7 @@ UNIFIED_SOURCES += [
"TestURIMutator.cpp",
"TestUriTemplate.cpp",
"TestURLPatternGlue.cpp",
+ "TestUseAsDictionary.cpp",
"TestWebTransportFlowControl.cpp",
]