0

Use SimpleUrlPatternMatcher for dictionary matching

The spec of Compression Dictionary Transport has been changed by [1] to
use URLPattern [2] for the matching algorithm of dictionaries. Also [3]
changed the Use-As-Dictionary header’s "match" value to be parsed as a
constructor string of URLPattern.

This CL changes the behavior of Chromium to follow those spec changes
when V2 backend is enabled.

[1]: https://github.com/httpwg/http-extensions/pull/2646
[2]: https://urlpattern.spec.whatwg.org/
[3]: https://github.com/httpwg/http-extensions/pull/2689

Fuchsia-Binary-Size: Size increase is unavoidable.
Bug: 1413922
Change-Id: I6ffba1c8c016145822643a18bc4f18bb7f0ac35f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5232339
Reviewed-by: Patrick Meenan <pmeenan@chromium.org>
Commit-Queue: Tsuyoshi Horo <horo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1256029}
This commit is contained in:
Tsuyoshi Horo
2024-02-05 01:35:01 +00:00
committed by Chromium LUCI CQ
parent a6665cf098
commit cdb7dfa61e
9 changed files with 238 additions and 48 deletions

@ -201,7 +201,7 @@ construction to catch up the following spec changes:
- Change Content-Encoding name "br-d" "zstd-d"
- Stauts: Implemented by https://crrev.com/c/5185977.
- Changed match to use URLPattern
- Stauts: Not yet implemented.
- Stauts: Implemented by https://crrev.com/c/5232339.
- Added support for a server-provided dictionary id
- Stauts: Not yet implemented.
- Stop using "expires" value of "Use-As-Dictionary" header, and use the cache

@ -160,18 +160,6 @@ void WriteDiskCacheEntry(SharedDictionaryManager* manager,
write_callback.callback(), /*truncate=*/false)));
}
base::UnguessableToken GetDiskCacheKeyTokenOfFirstDictionary(
const std::map<url::SchemeHostPort,
std::map<std::string, net::SharedDictionaryInfo>>&
dictionary_map,
const std::string& scheme_host_port_str) {
auto it =
dictionary_map.find(url::SchemeHostPort(GURL(scheme_host_port_str)));
CHECK(it != dictionary_map.end()) << scheme_host_port_str;
CHECK(!it->second.empty());
return it->second.begin()->second.disk_cache_key_token();
}
} // namespace
class SharedDictionaryManagerOnDiskTest
@ -207,6 +195,21 @@ class SharedDictionaryManagerOnDiskTest
features::CompressionDictionaryTransportBackendVersion GetVersion() const {
return GetParam();
}
static base::UnguessableToken GetDiskCacheKeyTokenOfFirstDictionary(
const std::map<
url::SchemeHostPort,
std::map<std::string,
SharedDictionaryStorageOnDisk::DictionaryInfoWithMatcher>>&
dictionary_map,
const std::string& scheme_host_port_str) {
auto it =
dictionary_map.find(url::SchemeHostPort(GURL(scheme_host_port_str)));
CHECK(it != dictionary_map.end()) << scheme_host_port_str;
CHECK(!it->second.empty());
return it->second.begin()->second.disk_cache_key_token();
}
std::unique_ptr<SharedDictionaryManager> CreateSharedDictionaryManager(
uint64_t cache_max_size = 0,
uint64_t cache_max_count =
@ -218,8 +221,10 @@ class SharedDictionaryManagerOnDiskTest
#endif // BUILDFLAG(IS_ANDROID)
/*file_operations_factory=*/nullptr);
}
const std::map<url::SchemeHostPort,
std::map<std::string, net::SharedDictionaryInfo>>&
const std::map<
url::SchemeHostPort,
std::map<std::string,
SharedDictionaryStorageOnDisk::DictionaryInfoWithMatcher>>&
GetOnDiskDictionaryMap(SharedDictionaryStorage* storage) {
return static_cast<SharedDictionaryStorageOnDisk*>(storage)
->GetDictionaryMapForTesting();

@ -229,8 +229,10 @@ class SharedDictionaryManagerTest
return static_cast<SharedDictionaryStorageInMemory*>(storage)
->GetDictionaryMap();
}
const std::map<url::SchemeHostPort,
std::map<std::string, net::SharedDictionaryInfo>>&
const std::map<
url::SchemeHostPort,
std::map<std::string,
SharedDictionaryStorageOnDisk::DictionaryInfoWithMatcher>>&
GetOnDiskDictionaryMap(SharedDictionaryStorage* storage) {
return static_cast<SharedDictionaryStorageOnDisk*>(storage)
->GetDictionaryMapForTesting();
@ -716,6 +718,52 @@ TEST_P(SharedDictionaryManagerTest, DictionaryLifetimeFromCacheControlHeader) {
}
}
TEST_P(SharedDictionaryManagerTest, InvalidMatch) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl1),
kSite1);
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
struct {
std::string header_string;
bool expect_success_v1;
bool expect_success_v2;
} kTestCases[] = {
// Invalid as a constructor string of URLPattern.
{"{", true, false},
// Unsupported regexp group.
{"(a|b)", true, false},
};
for (const auto& testcase : kTestCases) {
SCOPED_TRACE(
base::StringPrintf("match: %s", testcase.header_string.c_str()));
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"", testcase.header_string, "\"\n",
"cache-control:max-age=100\n\n"}));
ASSERT_TRUE(headers);
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
GURL("https://origin1.test/testfile.txt"),
/*request_time=*/base::Time::Now(), /*response_time=*/base::Time::Now(),
*headers,
/*was_fetched_via_cache=*/false,
/*access_allowed_check_callback=*/base::BindOnce([]() {
return true;
}));
switch (GetVersion()) {
case features::CompressionDictionaryTransportBackendVersion::kV1:
EXPECT_EQ(testcase.expect_success_v1, !!writer);
break;
case features::CompressionDictionaryTransportBackendVersion::kV2:
EXPECT_EQ(testcase.expect_success_v2, !!writer);
break;
}
}
}
TEST_P(SharedDictionaryManagerTest, AccessAllowedCheckReturnTrue) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
@ -987,6 +1035,54 @@ TEST_P(SharedDictionaryManagerTest, WriteAndReadDictionary) {
}
}
}
TEST_P(SharedDictionaryManagerTest, LongestMatchDictionaryWin) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl1),
kSite1);
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
WriteDictionary(storage.get(), GURL("https://origin1.test/dict"), "*estfile*",
{"Longer match"});
WriteDictionary(storage.get(), GURL("https://origin1.test/dict"), "test*",
{"Shorter match"});
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
auto dict = storage->GetDictionarySync(GURL("https://origin1.test/testfile"));
ASSERT_TRUE(dict);
net::TestCompletionCallback read_callback;
EXPECT_EQ(net::OK,
read_callback.GetResult(dict->ReadAll(read_callback.callback())));
EXPECT_EQ("Longer match", std::string(dict->data()->data(), dict->size()));
}
TEST_P(SharedDictionaryManagerTest, LatestDictionaryWin) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl1),
kSite1);
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
WriteDictionary(storage.get(), GURL("https://origin1.test/dict"), "test*",
{"Old match"});
task_environment_.FastForwardBy(base::Seconds(1));
WriteDictionary(storage.get(), GURL("https://origin1.test/dict"), "*est*",
{"New match"});
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
auto dict = storage->GetDictionarySync(GURL("https://origin1.test/testfile"));
ASSERT_TRUE(dict);
net::TestCompletionCallback read_callback;
EXPECT_EQ(net::OK,
read_callback.GetResult(dict->ReadAll(read_callback.callback())));
EXPECT_EQ("New match", std::string(dict->data()->data(), dict->size()));
}
TEST_P(SharedDictionaryManagerTest, OverrideDictionary) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();

@ -120,6 +120,12 @@ std::optional<DictionaryHeaderInfo> ParseDictionaryHeaderInfo(
} // namespace
// static
bool SharedDictionaryStorage::NeedToUseUrlPatternMatcher() {
return features::kCompressionDictionaryTransportBackendVersion.Get() !=
features::CompressionDictionaryTransportBackendVersion::kV1;
}
SharedDictionaryStorage::SharedDictionaryStorage() = default;
SharedDictionaryStorage::~SharedDictionaryStorage() = default;

@ -63,6 +63,10 @@ class COMPONENT_EXPORT(NETWORK_SERVICE) SharedDictionaryStorage
protected:
friend class base::RefCounted<SharedDictionaryStorage>;
// Returns true when V2 backend is used.
// TODO(crbug.com/1413922): Remove this when we remove V1 backend support.
static bool NeedToUseUrlPatternMatcher();
SharedDictionaryStorage();
virtual ~SharedDictionaryStorage();
@ -93,23 +97,29 @@ DictionaryInfoType* GetMatchingDictionaryFromDictionaryInfoMap(
if (it == dictionary_info_map.end()) {
return nullptr;
}
DictionaryInfoType* info = nullptr;
size_t mached_path_size = 0;
// TODO(crbug.com/1413922): If there are multiple matching dictionaries, this
// method currently returns the dictionary with the longest path pattern. But
// we should have a detailed description about `best-matching` in the spec.
DictionaryInfoType* matched_info = nullptr;
for (auto& item : it->second) {
// TODO(crbug.com/1413922): base::MatchPattern() is treating '?' in the
// pattern as an wildcard. We need to introduce a new flag in
// base::MatchPattern() to treat '?' as a normal character.
// TODO(crbug.com/1413922): Need support path expansion for relative paths.
if ((item.first.size() > mached_path_size) &&
base::MatchPattern(url.path(), item.first)) {
mached_path_size = item.first.size();
info = &item.second;
DictionaryInfoType& info = item.second;
CHECK_EQ(info.match(), item.first);
if (matched_info &&
((matched_info->match().size() > info.match().size()) ||
(matched_info->match().size() == info.match().size() &&
matched_info->response_time() > info.response_time()))) {
continue;
}
if (!info.matcher()) {
// This is for V1 backend.
// TODO(crbug.com/1413922): Remove this after V1 backend is removed.
if (base::MatchPattern(url.path(), info.match())) {
matched_info = &info;
}
} else {
if (info.matcher()->Match(url)) {
matched_info = &info;
}
}
}
return info;
return matched_info;
}
// Returns true if the same dictionary is already registered in

@ -14,6 +14,7 @@
#include "services/network/shared_dictionary/shared_dictionary_in_memory.h"
#include "services/network/shared_dictionary/shared_dictionary_manager_in_memory.h"
#include "services/network/shared_dictionary/shared_dictionary_writer_in_memory.h"
#include "services/network/shared_dictionary/simple_url_pattern_matcher.h"
#include "url/scheme_host_port.h"
namespace network {
@ -105,9 +106,18 @@ SharedDictionaryStorageInMemory::CreateWriter(const GURL& url,
base::Time response_time,
base::TimeDelta expiration,
const std::string& match) {
return base::MakeRefCounted<SharedDictionaryWriterInMemory>(base::BindOnce(
&SharedDictionaryStorageInMemory::OnDictionaryWritten,
weak_factory_.GetWeakPtr(), url, response_time, expiration, match));
std::unique_ptr<SimpleUrlPatternMatcher> matcher;
if (NeedToUseUrlPatternMatcher()) {
auto matcher_create_result = SimpleUrlPatternMatcher::Create(match, url);
if (!matcher_create_result.has_value()) {
return nullptr;
}
matcher = std::move(matcher_create_result.value());
}
return base::MakeRefCounted<SharedDictionaryWriterInMemory>(
base::BindOnce(&SharedDictionaryStorageInMemory::OnDictionaryWritten,
weak_factory_.GetWeakPtr(), url, response_time, expiration,
match, std::move(matcher)));
}
bool SharedDictionaryStorageInMemory::IsAlreadyRegistered(
@ -124,6 +134,7 @@ void SharedDictionaryStorageInMemory::OnDictionaryWritten(
base::Time response_time,
base::TimeDelta expiration,
const std::string& match,
std::unique_ptr<SimpleUrlPatternMatcher> matcher,
SharedDictionaryWriterInMemory::Result result,
scoped_refptr<net::IOBuffer> data,
size_t size,
@ -132,9 +143,9 @@ void SharedDictionaryStorageInMemory::OnDictionaryWritten(
return;
}
dictionary_info_map_[url::SchemeHostPort(url)].insert_or_assign(
match,
DictionaryInfo(url, response_time, expiration, match,
/*last_used_time=*/base::Time::Now(), data, size, hash));
match, DictionaryInfo(url, response_time, expiration, match,
/*last_used_time=*/base::Time::Now(), data, size,
hash, std::move(matcher)));
if (manager_) {
manager_->MaybeRunCacheEvictionPerSite(isolation_key_.top_frame_site());
manager_->MaybeRunCacheEviction();
@ -149,7 +160,8 @@ SharedDictionaryStorageInMemory::DictionaryInfo::DictionaryInfo(
base::Time last_used_time,
scoped_refptr<net::IOBuffer> data,
size_t size,
const net::SHA256HashValue& hash)
const net::SHA256HashValue& hash,
std::unique_ptr<SimpleUrlPatternMatcher> matcher)
: url_(url),
response_time_(response_time),
expiration_(expiration),
@ -157,7 +169,8 @@ SharedDictionaryStorageInMemory::DictionaryInfo::DictionaryInfo(
last_used_time_(last_used_time),
data_(std::move(data)),
size_(size),
hash_(hash) {}
hash_(hash),
matcher_(std::move(matcher)) {}
SharedDictionaryStorageInMemory::DictionaryInfo::DictionaryInfo(
DictionaryInfo&& other) = default;

@ -30,6 +30,7 @@ class CorsURLLoaderSharedDictionaryTest;
} // namespace cors
class SharedDictionaryManagerInMemory;
class SimpleUrlPatternMatcher;
// A SharedDictionaryStorage which is managed by
// SharedDictionaryManagerInMemory.
@ -45,7 +46,8 @@ class SharedDictionaryStorageInMemory : public SharedDictionaryStorage {
base::Time last_used_time,
scoped_refptr<net::IOBuffer> data,
size_t size,
const net::SHA256HashValue& hash);
const net::SHA256HashValue& hash,
std::unique_ptr<SimpleUrlPatternMatcher> matcher);
DictionaryInfo(const DictionaryInfo&) = delete;
DictionaryInfo& operator=(const DictionaryInfo&) = delete;
@ -63,6 +65,7 @@ class SharedDictionaryStorageInMemory : public SharedDictionaryStorage {
const scoped_refptr<net::IOBuffer>& data() const { return data_; }
size_t size() const { return size_; }
const net::SHA256HashValue& hash() const { return hash_; }
const SimpleUrlPatternMatcher* matcher() const { return matcher_.get(); }
void set_last_used_time(base::Time last_used_time) {
last_used_time_ = last_used_time;
@ -77,6 +80,7 @@ class SharedDictionaryStorageInMemory : public SharedDictionaryStorage {
scoped_refptr<net::IOBuffer> data_;
size_t size_;
net::SHA256HashValue hash_;
std::unique_ptr<SimpleUrlPatternMatcher> matcher_;
};
SharedDictionaryStorageInMemory(
@ -127,6 +131,7 @@ class SharedDictionaryStorageInMemory : public SharedDictionaryStorage {
base::Time response_time,
base::TimeDelta expiration,
const std::string& match,
std::unique_ptr<SimpleUrlPatternMatcher> matcher,
SharedDictionaryWriterInMemory::Result result,
scoped_refptr<net::IOBuffer> data,
size_t size,

@ -19,6 +19,7 @@
#include "services/network/shared_dictionary/shared_dictionary_manager_on_disk.h"
#include "services/network/shared_dictionary/shared_dictionary_on_disk.h"
#include "services/network/shared_dictionary/shared_dictionary_writer_on_disk.h"
#include "services/network/shared_dictionary/simple_url_pattern_matcher.h"
#include "url/scheme_host_port.h"
namespace network {
@ -104,6 +105,19 @@ class SharedDictionaryStorageOnDisk::WrappedSharedDictionary
scoped_refptr<RefCountedSharedDictionary> ref_counted_shared_dictionary_;
};
SharedDictionaryStorageOnDisk::DictionaryInfoWithMatcher::
DictionaryInfoWithMatcher(net::SharedDictionaryInfo info,
std::unique_ptr<SimpleUrlPatternMatcher> matcher)
: net::SharedDictionaryInfo(std::move(info)),
matcher_(std::move(matcher)) {}
SharedDictionaryStorageOnDisk::DictionaryInfoWithMatcher::
~DictionaryInfoWithMatcher() = default;
SharedDictionaryStorageOnDisk::DictionaryInfoWithMatcher::
DictionaryInfoWithMatcher(DictionaryInfoWithMatcher&&) = default;
SharedDictionaryStorageOnDisk::DictionaryInfoWithMatcher&
SharedDictionaryStorageOnDisk::DictionaryInfoWithMatcher::operator=(
DictionaryInfoWithMatcher&&) = default;
SharedDictionaryStorageOnDisk::SharedDictionaryStorageOnDisk(
base::WeakPtr<SharedDictionaryManagerOnDisk> manager,
const net::SharedDictionaryIsolationKey& isolation_key,
@ -197,10 +211,19 @@ SharedDictionaryStorageOnDisk::CreateWriter(const GURL& url,
if (!manager_) {
return nullptr;
}
std::unique_ptr<SimpleUrlPatternMatcher> matcher;
if (NeedToUseUrlPatternMatcher()) {
auto matcher_create_result = SimpleUrlPatternMatcher::Create(match, url);
if (!matcher_create_result.has_value()) {
return nullptr;
}
matcher = std::move(matcher_create_result.value());
}
return manager_->CreateWriter(
isolation_key_, url, response_time, expiration, match,
base::BindOnce(&SharedDictionaryStorageOnDisk::OnDictionaryWritten,
weak_factory_.GetWeakPtr()));
weak_factory_.GetWeakPtr(), std::move(matcher)));
}
bool SharedDictionaryStorageOnDisk::IsAlreadyRegistered(
@ -220,13 +243,24 @@ void SharedDictionaryStorageOnDisk::OnDatabaseRead(
if (!result.has_value()) {
return;
}
std::set<base::UnguessableToken> deleted_cache_tokens;
const bool need_matcher = NeedToUseUrlPatternMatcher();
for (auto& info : result.value()) {
const url::SchemeHostPort scheme_host_port =
url::SchemeHostPort(info.url());
const std::string match = info.match();
(dictionary_info_map_[scheme_host_port])
.insert(std::make_pair(match, std::move(info)));
std::unique_ptr<SimpleUrlPatternMatcher> matcher;
if (need_matcher) {
auto matcher_create_result =
SimpleUrlPatternMatcher::Create(match, info.url());
if (!matcher_create_result.has_value()) {
continue;
}
matcher = std::move(matcher_create_result.value());
}
dictionary_info_map_[scheme_host_port].insert(std::make_pair(
match, DictionaryInfoWithMatcher(std::move(info), std::move(matcher))));
}
auto callbacks = std::move(pending_get_dictionary_tasks_);
@ -236,11 +270,13 @@ void SharedDictionaryStorageOnDisk::OnDatabaseRead(
}
void SharedDictionaryStorageOnDisk::OnDictionaryWritten(
std::unique_ptr<SimpleUrlPatternMatcher> matcher,
net::SharedDictionaryInfo info) {
const url::SchemeHostPort scheme_host_port = url::SchemeHostPort(info.url());
const std::string match = info.match();
(dictionary_info_map_[scheme_host_port])
.insert_or_assign(match, std::move(info));
.insert_or_assign(match, DictionaryInfoWithMatcher(std::move(info),
std::move(matcher)));
}
void SharedDictionaryStorageOnDisk::OnRefCountedSharedDictionaryDeleted(

@ -28,6 +28,7 @@
namespace network {
class SharedDictionaryManagerOnDisk;
class SimpleUrlPatternMatcher;
// A SharedDictionaryStorage which is managed by SharedDictionaryManagerOnDisk.
class SharedDictionaryStorageOnDisk : public SharedDictionaryStorage {
@ -71,14 +72,32 @@ class SharedDictionaryStorageOnDisk : public SharedDictionaryStorage {
class RefCountedSharedDictionary;
class WrappedSharedDictionary;
class DictionaryInfoWithMatcher : public net::SharedDictionaryInfo {
public:
DictionaryInfoWithMatcher(net::SharedDictionaryInfo info,
std::unique_ptr<SimpleUrlPatternMatcher> matcher);
~DictionaryInfoWithMatcher();
DictionaryInfoWithMatcher(const DictionaryInfoWithMatcher&) = delete;
DictionaryInfoWithMatcher& operator=(const DictionaryInfoWithMatcher&) =
delete;
DictionaryInfoWithMatcher(DictionaryInfoWithMatcher&&);
DictionaryInfoWithMatcher& operator=(DictionaryInfoWithMatcher&&);
const SimpleUrlPatternMatcher* matcher() const { return matcher_.get(); }
private:
std::unique_ptr<SimpleUrlPatternMatcher> matcher_;
};
void OnDatabaseRead(
net::SQLitePersistentSharedDictionaryStore::DictionaryListOrError result);
void OnDictionaryWritten(net::SharedDictionaryInfo info);
void OnDictionaryWritten(std::unique_ptr<SimpleUrlPatternMatcher> matcher,
net::SharedDictionaryInfo info);
void OnRefCountedSharedDictionaryDeleted(
const base::UnguessableToken& disk_cache_key_token);
const std::map<url::SchemeHostPort,
std::map<std::string, net::SharedDictionaryInfo>>&
std::map<std::string, DictionaryInfoWithMatcher>>&
GetDictionaryMapForTesting() {
return dictionary_info_map_;
}
@ -87,7 +106,7 @@ class SharedDictionaryStorageOnDisk : public SharedDictionaryStorage {
const net::SharedDictionaryIsolationKey isolation_key_;
base::ScopedClosureRunner on_deleted_closure_runner_;
std::map<url::SchemeHostPort,
std::map<std::string, net::SharedDictionaryInfo>>
std::map<std::string, DictionaryInfoWithMatcher>>
dictionary_info_map_;
std::map<base::UnguessableToken, raw_ptr<RefCountedSharedDictionary>>
dictionaries_;