0

Implement serialization for NoVarySearchCache

Add a specialization of net::PickleTraits for net::NoVarySearchCache so
that it can be serialized and deserialized to/from a base::Pickle. This
will be used to implement persistence.

Since we need to be able to serialize base::Time objects for this, add a
new header file "net/base/pickle_base_types.h" which adds serialization
support for types from //base that we need. This uses the deprecated
FromInternalValue()/ToInternalValue() methods to allow migrating existing
serialization code in //net to use PickleTraits without changing the
serialization format.

BUG=399562754,382394774

Change-Id: Ic81083995b7661749fcfd84e83841f5944ec3818
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6341087
Reviewed-by: Kenichi Ishibashi <bashi@chromium.org>
Commit-Queue: Adam Rice <ricea@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1435857}
This commit is contained in:
Adam Rice
2025-03-20 21:31:42 -07:00
committed by Chromium LUCI CQ
parent 07d1b87030
commit 94f4065f85
6 changed files with 525 additions and 44 deletions

@@ -251,6 +251,7 @@ component("net") {
"base/parse_number.cc", "base/parse_number.cc",
"base/parse_number.h", "base/parse_number.h",
"base/pickle.h", "base/pickle.h",
"base/pickle_base_types.h",
"base/pickle_traits.h", "base/pickle_traits.h",
"base/platform_mime_util.h", "base/platform_mime_util.h",
"base/port_util.cc", "base/port_util.cc",
@@ -2674,6 +2675,7 @@ target(_test_target_type, "net_unittests") {
"base/network_interfaces_unittest.cc", "base/network_interfaces_unittest.cc",
"base/network_isolation_key_unittest.cc", "base/network_isolation_key_unittest.cc",
"base/parse_number_unittest.cc", "base/parse_number_unittest.cc",
"base/pickle_base_types_unittest.cc",
"base/pickle_unittest.cc", "base/pickle_unittest.cc",
"base/port_util_unittest.cc", "base/port_util_unittest.cc",
"base/prioritized_dispatcher_unittest.cc", "base/prioritized_dispatcher_unittest.cc",

@@ -0,0 +1,43 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Serialization and deserialization code for some //base types that are used in
// //net. This can't be put in //base because //base can't depend on //net.
#ifndef NET_BASE_PICKLE_BASE_TYPES_H_
#define NET_BASE_PICKLE_BASE_TYPES_H_
#include <stdint.h>
#include <optional>
#include "base/time/time.h"
#include "net/base/pickle_traits.h"
namespace net {
template <>
struct PickleTraits<base::Time> {
static void Serialize(base::Pickle& pickle, const base::Time& time) {
// For compatibility with existing serialization code in //net, use the
// deprecated `ToInternalValue()` method.
pickle.WriteInt64(time.ToInternalValue());
}
static std::optional<base::Time> Deserialize(base::PickleIterator& iter) {
int64_t time_as_int64;
if (!iter.ReadInt64(&time_as_int64)) {
return std::nullopt;
}
return base::Time::FromInternalValue(time_as_int64);
}
static size_t PickleSize(const base::Time& time) {
return EstimatePickleSize(int64_t{0});
}
};
} // namespace net
#endif // NET_BASE_PICKLE_BASE_TYPES_H_

@@ -0,0 +1,38 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/base/pickle_base_types.h"
#include <array>
#include "base/pickle.h"
#include "base/time/time.h"
#include "net/base/pickle.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace net {
namespace {
using ::testing::Optional;
TEST(PickleBaseTypesTest, Time) {
base::Time nov1994;
ASSERT_TRUE(
base::Time::FromUTCString("Tue, 15 Nov 1994 12:45:26 GMT", &nov1994));
static const auto kCases = std::to_array<base::Time>(
{base::Time(), base::Time::Max(), base::Time::UnixEpoch(), nov1994});
for (base::Time test_case : kCases) {
SCOPED_TRACE(test_case);
base::Pickle pickle;
WriteToPickle(pickle, test_case);
EXPECT_EQ(EstimatePickleSize(test_case), pickle.payload_size());
EXPECT_THAT(ReadValueFromPickle<base::Time>(pickle), Optional(test_case));
}
}
} // namespace
} // namespace net

@@ -4,8 +4,10 @@
#include "net/http/no_vary_search_cache.h" #include "net/http/no_vary_search_cache.h"
#include <algorithm>
#include <compare> #include <compare>
#include <iostream> #include <iostream>
#include <limits>
#include <tuple> #include <tuple>
#include <type_traits> #include <type_traits>
#include <utility> #include <utility>
@@ -15,7 +17,10 @@
#include "base/memory/weak_ptr.h" #include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h" #include "base/metrics/histogram_macros.h"
#include "base/notreached.h" #include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "base/time/time.h" #include "base/time/time.h"
#include "net/base/pickle.h"
#include "net/base/pickle_base_types.h"
#include "net/http/http_cache.h" #include "net/http/http_cache.h"
#include "net/http/http_no_vary_search_data.h" #include "net/http/http_no_vary_search_data.h"
@@ -184,7 +189,13 @@ class NoVarySearchCache::QueryString final
// Removes this object from both lists and deletes it. // Removes this object from both lists and deletes it.
void RemoveAndDelete() { void RemoveAndDelete() {
LruNode::RemoveFromList(); // During deserialization a QueryString is not inserted into the `lru_` list
// until the end. If deserialization fails before then, it can be deleted
// without ever being inserted into the `lru_` list.
if (LruNode::next()) {
CHECK(LruNode::previous());
LruNode::RemoveFromList();
}
QueryStringListNode::RemoveFromList(); QueryStringListNode::RemoveFromList();
delete this; delete this;
} }
@@ -200,9 +211,7 @@ class NoVarySearchCache::QueryString final
const std::optional<std::string>& query() const { return query_; } const std::optional<std::string>& query() const { return query_; }
QueryStringList& query_string_list_ref() { QueryStringList& query_string_list_ref() { return *query_string_list_ref_; }
return query_string_list_ref_.get();
}
base::Time insertion_time() const { return insertion_time_; } base::Time insertion_time() const { return insertion_time_; }
@@ -225,12 +234,21 @@ class NoVarySearchCache::QueryString final
return EraseHandle(weak_factory_.GetWeakPtr()); return EraseHandle(weak_factory_.GetWeakPtr());
} }
void set_query_string_list_ref(
base::PassKey<NoVarySearchCache::QueryStringList>,
QueryStringList* query_string_list) {
query_string_list_ref_ = query_string_list;
}
private: private:
friend struct PickleTraits<NoVarySearchCache::QueryStringList>;
QueryString(std::optional<std::string_view> query, QueryString(std::optional<std::string_view> query,
QueryStringList& query_string_list) QueryStringList& query_string_list,
base::Time insertion_time = base::Time::Now())
: query_(query), : query_(query),
query_string_list_ref_(query_string_list), query_string_list_ref_(&query_string_list),
insertion_time_(base::Time::Now()) {} insertion_time_(insertion_time) {}
// Must only be called from RemoveAndDelete(). // Must only be called from RemoveAndDelete().
~QueryString() = default; ~QueryString() = default;
@@ -254,8 +272,9 @@ class NoVarySearchCache::QueryString final
const std::optional<std::string> query_; const std::optional<std::string> query_;
// `query_string_list_ref_` allows the keys for this entry to be located in // `query_string_list_ref_` allows the keys for this entry to be located in
// the cache so that it can be erased efficiently. // the cache so that it can be erased efficiently. It is modified when a
const raw_ref<QueryStringList> query_string_list_ref_; // QueryStringList object is moved.
raw_ptr<QueryStringList> query_string_list_ref_ = nullptr;
// `insertion_time_` breaks ties when there are multiple possible matches. The // `insertion_time_` breaks ties when there are multiple possible matches. The
// most recent entry will be used as it is most likely to still exist in the // most recent entry will be used as it is most likely to still exist in the
@@ -300,9 +319,17 @@ bool NoVarySearchCache::EraseHandle::IsGoneForTesting() const {
} }
NoVarySearchCache::NoVarySearchCache(size_t max_size) : max_size_(max_size) { NoVarySearchCache::NoVarySearchCache(size_t max_size) : max_size_(max_size) {
CHECK_GT(max_size_, 0u); CHECK_GE(max_size_, 1u);
// We can't serialize if `max_size` won't fit in an int.
CHECK(base::IsValueInRangeForNumericType<int>(max_size));
} }
NoVarySearchCache::NoVarySearchCache(NoVarySearchCache&& rhs)
: map_(std::move(rhs.map_)),
lru_(std::move(rhs.lru_)),
size_(std::exchange(rhs.size_, 0u)),
max_size_(rhs.max_size_) {}
NoVarySearchCache::~NoVarySearchCache() { NoVarySearchCache::~NoVarySearchCache() {
map_.clear(); map_.clear();
// Clearing the map should have freed all the QueryString objects. // Clearing the map should have freed all the QueryString objects.
@@ -423,7 +450,7 @@ bool NoVarySearchCache::ClearData(UrlFilterType filter_type,
// then erase them. // then erase them.
// TODO(https://crbug.com/382394774): Make this algorithm more efficient. // TODO(https://crbug.com/382394774): Make this algorithm more efficient.
std::vector<QueryString*> pending_erase; std::vector<QueryString*> pending_erase;
for (const auto& [cache_key, data_map] : map_) { for (auto& [cache_key, data_map] : map_) {
const std::string base_url_string = const std::string base_url_string =
HttpCache::GetResourceURLFromHttpCacheKey(cache_key.value()); HttpCache::GetResourceURLFromHttpCacheKey(cache_key.value());
const GURL base_url(base_url_string); const GURL base_url(base_url_string);
@@ -458,9 +485,27 @@ bool NoVarySearchCache::IsTopLevelMapEmptyForTesting() const {
} }
NoVarySearchCache::QueryStringList::QueryStringList(const BaseURLCacheKey& key) NoVarySearchCache::QueryStringList::QueryStringList(const BaseURLCacheKey& key)
: key_ref(key) {} : key_ref(&key) {}
NoVarySearchCache::QueryStringList::QueryStringList() = default;
NoVarySearchCache::QueryStringList::QueryStringList(QueryStringList&& rhs)
: list(std::move(rhs.list)) {
// We should not move a list after the key references have been assigned.
CHECK(!rhs.nvs_data_ref);
CHECK(!rhs.key_ref);
// We have to patch up all the references to `rhs` in our QueryString objects
// to point to us instead.
ForEachQueryString(list, [&](QueryString* query_string) {
query_string->set_query_string_list_ref(base::PassKey<QueryStringList>(),
this);
});
}
NoVarySearchCache::QueryStringList::~QueryStringList() { NoVarySearchCache::QueryStringList::~QueryStringList() {
// The `list.head()` check works around the unfortunate fact that moving from
// a base::LinkedList leaves it in an invalid state where `list.empty()` is
// false.
while (!list.empty()) { while (!list.empty()) {
list.head()->value()->ToQueryString()->RemoveAndDelete(); list.head()->value()->ToQueryString()->RemoveAndDelete();
} }
@@ -481,11 +526,11 @@ void NoVarySearchCache::EraseQuery(QueryString* query_string) {
const QueryStringList& query_strings = query_string->query_string_list_ref(); const QueryStringList& query_strings = query_string->query_string_list_ref();
query_string->RemoveAndDelete(); query_string->RemoveAndDelete();
if (query_strings.list.empty()) { if (query_strings.list.empty()) {
const HttpNoVarySearchData* nvs_data_ref = query_strings.nvs_data_ref.get(); const HttpNoVarySearchData& nvs_data_ref = *query_strings.nvs_data_ref;
const BaseURLCacheKey& key_ref = query_strings.key_ref.get(); const BaseURLCacheKey& key_ref = *query_strings.key_ref;
const auto map_it = map_.find(key_ref); const auto map_it = map_.find(key_ref);
CHECK(map_it != map_.end()); CHECK(map_it != map_.end());
const size_t removed_count = map_it->second.erase(*nvs_data_ref); const size_t removed_count = map_it->second.erase(nvs_data_ref);
CHECK_EQ(removed_count, 1u); CHECK_EQ(removed_count, 1u);
if (map_it->second.empty()) { if (map_it->second.empty()) {
map_.erase(map_it); map_.erase(map_it);
@@ -495,20 +540,18 @@ void NoVarySearchCache::EraseQuery(QueryString* query_string) {
// static // static
void NoVarySearchCache::FindQueryStringsInTimeRange( void NoVarySearchCache::FindQueryStringsInTimeRange(
const DataMapType& data_map, DataMapType& data_map,
base::Time delete_begin, base::Time delete_begin,
base::Time delete_end, base::Time delete_end,
std::vector<QueryString*>& matches) { std::vector<QueryString*>& matches) {
for (const auto& [_, query_string_list] : data_map) { for (auto& [_, query_string_list] : data_map) {
for (auto* node = query_string_list.list.head(); ForEachQueryString(query_string_list.list, [&](QueryString* query_string) {
node != query_string_list.list.end(); node = node->next()) {
QueryString* query_string = node->value()->ToQueryString();
const base::Time insertion_time = query_string->insertion_time(); const base::Time insertion_time = query_string->insertion_time();
if ((delete_begin.is_null() || delete_begin <= insertion_time) && if ((delete_begin.is_null() || delete_begin <= insertion_time) &&
(delete_end.is_max() || delete_end > insertion_time)) { (delete_end.is_max() || delete_end > insertion_time)) {
matches.push_back(query_string); matches.push_back(query_string);
} }
} });
} }
} }
@@ -531,4 +574,189 @@ NoVarySearchCache::FindQueryStringInList(QueryStringList& query_strings,
return std::nullopt; return std::nullopt;
} }
// static
void NoVarySearchCache::ForEachQueryString(
base::LinkedList<QueryStringListNode>& list,
base::FunctionRef<void(QueryString*)> f) {
for (auto* node = list.head(); node != list.end(); node = node->next()) {
QueryString* query_string = node->value()->ToQueryString();
f(query_string);
}
}
// static
void NoVarySearchCache::ForEachQueryString(
const base::LinkedList<QueryStringListNode>& list,
base::FunctionRef<void(const QueryString*)> f) {
for (auto* node = list.head(); node != list.end(); node = node->next()) {
const QueryString* query_string = node->value()->ToQueryString();
f(query_string);
}
}
template <>
struct PickleTraits<NoVarySearchCache::QueryStringList> {
static void Serialize(
base::Pickle& pickle,
const NoVarySearchCache::QueryStringList& query_strings) {
// base::LinkedList doesn't keep an element count, so we need to count them
// ourselves.
size_t size = 0u;
for (auto* node = query_strings.list.head();
node != query_strings.list.end(); node = node->next()) {
++size;
}
WriteToPickle(pickle, base::checked_cast<int>(size));
NoVarySearchCache::ForEachQueryString(
query_strings.list,
[&](const NoVarySearchCache::QueryString* query_string) {
WriteToPickle(pickle, query_string->query_,
query_string->insertion_time_);
});
}
static std::optional<NoVarySearchCache::QueryStringList> Deserialize(
base::PickleIterator& iter) {
NoVarySearchCache::QueryStringList query_string_list;
size_t size = 0;
if (!iter.ReadLength(&size)) {
return std::nullopt;
}
for (size_t i = 0; i < size; ++i) {
// QueryString is not movable or copyable, so it won't work well with
// PickleTraits. Deserialize it inline instead.
auto result =
ReadValuesFromPickle<std::optional<std::string>, base::Time>(iter);
if (!result) {
return std::nullopt;
}
auto [query, insertion_time] = std::move(result).value();
if (query && query->find('#') != std::string_view::npos) {
// A '#' character must not appear in the query.
return std::nullopt;
}
auto* query_string = new NoVarySearchCache::QueryString(
std::move(query), query_string_list, insertion_time);
// Serialization happens from head to tail, so to deserialize in the same
// order, we add elements at the tail of the list.
query_string_list.list.Append(query_string);
}
return query_string_list;
}
static size_t PickleSize(
const NoVarySearchCache::QueryStringList& query_strings) {
size_t estimate = EstimatePickleSize(int{});
NoVarySearchCache::ForEachQueryString(
query_strings.list,
[&](const NoVarySearchCache::QueryString* query_string) {
estimate += EstimatePickleSize(query_string->query_,
query_string->insertion_time_);
});
return estimate;
}
};
template <>
struct PickleTraits<NoVarySearchCache::BaseURLCacheKey> {
static void Serialize(base::Pickle& pickle,
const NoVarySearchCache::BaseURLCacheKey& key) {
WriteToPickle(pickle, *key);
}
static std::optional<NoVarySearchCache::BaseURLCacheKey> Deserialize(
base::PickleIterator& iter) {
NoVarySearchCache::BaseURLCacheKey key;
if (!ReadPickleInto(iter, *key)) {
return std::nullopt;
}
return key;
}
static size_t PickleSize(const NoVarySearchCache::BaseURLCacheKey& key) {
return EstimatePickleSize(*key);
}
};
// static
void PickleTraits<NoVarySearchCache>::Serialize(
base::Pickle& pickle,
const NoVarySearchCache& cache) {
// `size_t` is different sizes on 32-bit and 64-bit platforms. For a
// consistent format, serialize as int. This will crash if someone creates a
// NoVarySearchCache which supports over 2 billion entries, which would be a
// terrible idea anyway.
int max_size_as_int = base::checked_cast<int>(cache.max_size_);
int size_as_int = base::checked_cast<int>(cache.size_);
// `lru_` is reconstructed during deserialization and so doesn't need to be
// stored explicitly.
WriteToPickle(pickle, size_as_int, max_size_as_int, cache.map_);
}
// static
std::optional<NoVarySearchCache> PickleTraits<NoVarySearchCache>::Deserialize(
base::PickleIterator& iter) {
const std::optional<int> maybe_size = ReadValueFromPickle<int>(iter);
if (!maybe_size || *maybe_size < 0) {
return std::nullopt;
}
const size_t size = static_cast<size_t>(*maybe_size);
const std::optional<int> maybe_max_size = ReadValueFromPickle<int>(iter);
if (!maybe_max_size || *maybe_max_size < 1) {
return std::nullopt;
}
const size_t max_size = static_cast<size_t>(*maybe_max_size);
if (size > max_size) {
return std::nullopt;
}
NoVarySearchCache cache(max_size);
cache.size_ = size;
if (!ReadPickleInto(iter, cache.map_)) {
return std::nullopt;
}
using QueryString = NoVarySearchCache::QueryString;
// Get a list of every QueryString object in the map so that we can sort
// them to reconstruct the `lru_` list.
std::vector<QueryString*> all_query_strings;
all_query_strings.reserve(size);
for (auto& [base_url_cache_key, data_map] : cache.map_) {
for (auto& [nvs_data, query_string_list] : data_map) {
query_string_list.nvs_data_ref = &nvs_data;
query_string_list.key_ref = &base_url_cache_key;
NoVarySearchCache::ForEachQueryString(
query_string_list.list, [&](QueryString* query_string) {
all_query_strings.push_back(query_string);
});
}
}
if (size != all_query_strings.size()) {
return std::nullopt;
}
// Sort by `insertion_time`, which we use as an approximation of `use_time`
// during deserialization on the assumption that it won't make much
// difference.
std::ranges::sort(all_query_strings, std::less<base::Time>(),
[](QueryString* qs) { return qs->insertion_time(); });
// Insert each entry at the head of the list, so that the oldest entry ends
// up at the tail.
for (QueryString* qs : all_query_strings) {
qs->LruNode::InsertBefore(cache.lru_.head());
}
return cache;
}
// static
size_t PickleTraits<NoVarySearchCache>::PickleSize(
const NoVarySearchCache& cache) {
// `size_` and `max_size_` are pickled as ints.
return EstimatePickleSize(int{}, int{}, cache.map_);
}
} // namespace net } // namespace net

@@ -17,6 +17,7 @@
#include <vector> #include <vector>
#include "base/containers/linked_list.h" #include "base/containers/linked_list.h"
#include "base/functional/function_ref.h"
#include "base/memory/raw_ptr.h" #include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h" #include "base/memory/raw_ref.h"
#include "base/memory/stack_allocated.h" #include "base/memory/stack_allocated.h"
@@ -24,6 +25,7 @@
#include "base/types/strong_alias.h" #include "base/types/strong_alias.h"
#include "net/base/does_url_match_filter.h" #include "net/base/does_url_match_filter.h"
#include "net/base/net_export.h" #include "net/base/net_export.h"
#include "net/base/pickle_traits.h"
#include "net/http/http_no_vary_search_data.h" #include "net/http/http_no_vary_search_data.h"
#include "net/http/http_request_info.h" #include "net/http/http_request_info.h"
#include "url/gurl.h" #include "url/gurl.h"
@@ -90,9 +92,13 @@ class NET_EXPORT_PRIVATE NoVarySearchCache {
// bytes. // bytes.
explicit NoVarySearchCache(size_t max_size); explicit NoVarySearchCache(size_t max_size);
// Not copyable, assignable or movable. // Move-constructible to permit deserialization and passing between threads.
NoVarySearchCache(NoVarySearchCache&&);
// Not copyable or assignable.
NoVarySearchCache(const NoVarySearchCache&) = delete; NoVarySearchCache(const NoVarySearchCache&) = delete;
NoVarySearchCache& operator=(const NoVarySearchCache&) = delete; NoVarySearchCache& operator=(const NoVarySearchCache&) = delete;
NoVarySearchCache& operator=(NoVarySearchCache&&) = delete;
~NoVarySearchCache(); ~NoVarySearchCache();
@@ -137,23 +143,39 @@ class NET_EXPORT_PRIVATE NoVarySearchCache {
bool IsTopLevelMapEmptyForTesting() const; bool IsTopLevelMapEmptyForTesting() const;
private: private:
class LruNode; friend struct PickleTraits<NoVarySearchCache>;
class QueryStringListNode;
struct QueryStringList;
friend struct PickleTraits<NoVarySearchCache::QueryStringList>;
using BaseURLCacheKey = using BaseURLCacheKey =
base::StrongAlias<struct BaseURLCacheKeyTagType, std::string>; base::StrongAlias<struct BaseURLCacheKeyTagType, std::string>;
friend struct PickleTraits<NoVarySearchCache::BaseURLCacheKey>;
class LruNode;
class QueryStringListNode;
struct QueryStringList { struct QueryStringList {
base::LinkedList<QueryStringListNode> list; base::LinkedList<QueryStringListNode> list;
// nvs_data_ref can't be raw_ref because it needs to be lazily initialized // nvs_data_ref can't be raw_ref because it needs to be lazily initialized
// after the QueryStringList has been added to the map. // after the QueryStringList has been added to the map.
raw_ptr<const HttpNoVarySearchData> nvs_data_ref; raw_ptr<const HttpNoVarySearchData> nvs_data_ref = nullptr;
raw_ref<const BaseURLCacheKey> key_ref;
// key_ref can't be raw_ref because it needs to be added in a second pass
// during deserialization.
raw_ptr<const BaseURLCacheKey> key_ref = nullptr;
// The referent of this reference has to be the actual key in the map. It is // The referent of this reference has to be the actual key in the map. It is
// not sufficient for the value to match, because the lifetime has to be the // not sufficient for the value to match, because the lifetime has to be the
// same. // same.
explicit QueryStringList(const BaseURLCacheKey& key); explicit QueryStringList(const BaseURLCacheKey& key);
// Needed during deserialization.
QueryStringList();
// Only used during deserialization. This is O(N) in the size of `list`.
QueryStringList(QueryStringList&&);
// base::LinkedList<> does not do memory management, so make sure the // base::LinkedList<> does not do memory management, so make sure the
// contents of `list` are deleted on destruction. // contents of `list` are deleted on destruction.
~QueryStringList(); ~QueryStringList();
@@ -178,8 +200,11 @@ class NET_EXPORT_PRIVATE NoVarySearchCache {
void EraseQuery(QueryString* query_string); void EraseQuery(QueryString* query_string);
// Scans all the QueryStrings in `data_map` to find ones in the range // Scans all the QueryStrings in `data_map` to find ones in the range
// [delete_begin, delete_end) and appends them to `matches`. // [delete_begin, delete_end) and appends them to `matches`. `data_map` is
static void FindQueryStringsInTimeRange(const DataMapType& data_map, // mutable to reflect that it is returning mutable pointers to QueryString
// objects that it owns. The returned QueryString objects are mutable so the
// caller can erase them.
static void FindQueryStringsInTimeRange(DataMapType& data_map,
base::Time delete_begin, base::Time delete_begin,
base::Time delete_end, base::Time delete_end,
std::vector<QueryString*>& matches); std::vector<QueryString*>& matches);
@@ -190,6 +215,15 @@ class NET_EXPORT_PRIVATE NoVarySearchCache {
const GURL& url, const GURL& url,
const HttpNoVarySearchData& nvs_data); const HttpNoVarySearchData& nvs_data);
// Calls f(query_string_ptr) for every QueryString in `list`.
static void ForEachQueryString(base::LinkedList<QueryStringListNode>& list,
base::FunctionRef<void(QueryString*)> f);
// Calls f(const_query_string_ptr) for every QueryString in `list`.
static void ForEachQueryString(
const base::LinkedList<QueryStringListNode>& list,
base::FunctionRef<void(const QueryString*)> f);
// The main cache data structure. // The main cache data structure.
OuterMapType map_; OuterMapType map_;
@@ -203,6 +237,16 @@ class NET_EXPORT_PRIVATE NoVarySearchCache {
const size_t max_size_; const size_t max_size_;
}; };
template <>
struct NET_EXPORT_PRIVATE PickleTraits<NoVarySearchCache> {
static void Serialize(base::Pickle& pickle, const NoVarySearchCache& cache);
static std::optional<NoVarySearchCache> Deserialize(
base::PickleIterator& iter);
static size_t PickleSize(const NoVarySearchCache& cache);
};
} // namespace net } // namespace net
#endif // NET_HTTP_NO_VARY_SEARCH_CACHE_H_ #endif // NET_HTTP_NO_VARY_SEARCH_CACHE_H_

@@ -10,6 +10,7 @@
#include <string_view> #include <string_view>
#include <utility> #include <utility>
#include "base/pickle.h"
#include "base/strings/strcat.h" #include "base/strings/strcat.h"
#include "base/test/scoped_feature_list.h" #include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h" #include "base/test/task_environment.h"
@@ -18,6 +19,8 @@
#include "net/base/features.h" #include "net/base/features.h"
#include "net/base/load_flags.h" #include "net/base/load_flags.h"
#include "net/base/network_isolation_key.h" #include "net/base/network_isolation_key.h"
#include "net/base/pickle.h"
#include "net/base/pickle_traits.h"
#include "net/base/schemeful_site.h" #include "net/base/schemeful_site.h"
#include "net/http/http_cache.h" #include "net/http/http_cache.h"
#include "net/http/http_response_headers.h" #include "net/http/http_response_headers.h"
@@ -171,6 +174,19 @@ TEST_P(NoVarySearchCacheTest, InsertLookupErase) {
EXPECT_TRUE(cache().IsTopLevelMapEmptyForTesting()); EXPECT_TRUE(cache().IsTopLevelMapEmptyForTesting());
} }
TEST_P(NoVarySearchCacheTest, MoveConstruct) {
Insert("a=b", "key-order");
NoVarySearchCache new_cache = std::move(cache());
EXPECT_TRUE(new_cache.Lookup(TestRequest("a=b")));
// NOLINTNEXTLINE(bugprone-use-after-move)
EXPECT_EQ(cache().GetSizeForTesting(), 0u);
// NOLINTNEXTLINE(bugprone-use-after-move)
EXPECT_TRUE(cache().IsTopLevelMapEmptyForTesting());
}
// An asan build will find leaks, but this test works on any build. // An asan build will find leaks, but this test works on any build.
TEST_P(NoVarySearchCacheTest, QueryNotLeaked) { TEST_P(NoVarySearchCacheTest, QueryNotLeaked) {
std::optional<NoVarySearchCache::LookupResult> result; std::optional<NoVarySearchCache::LookupResult> result;
@@ -185,10 +201,16 @@ TEST_P(NoVarySearchCacheTest, QueryNotLeaked) {
EXPECT_TRUE(result->erase_handle.IsGoneForTesting()); EXPECT_TRUE(result->erase_handle.IsGoneForTesting());
} }
std::string QueryWithIParameter(size_t i) {
return "i=" + base::NumberToString(i);
}
constexpr std::string_view kVaryOnIParameter = "params, except=(\"i\")";
TEST_P(NoVarySearchCacheTest, OldestItemIsEvicted) { TEST_P(NoVarySearchCacheTest, OldestItemIsEvicted) {
for (size_t i = 0; i < kMaxSize + 1; ++i) { for (size_t i = 0; i < kMaxSize + 1; ++i) {
std::string query = "i=" + base::NumberToString(i); std::string query = QueryWithIParameter(i);
Insert(query, "params, except=(\"i\")"); Insert(query, kVaryOnIParameter);
EXPECT_TRUE(Exists(query)); EXPECT_TRUE(Exists(query));
} }
@@ -199,8 +221,8 @@ TEST_P(NoVarySearchCacheTest, OldestItemIsEvicted) {
TEST_P(NoVarySearchCacheTest, RecentlyUsedItemIsNotEvicted) { TEST_P(NoVarySearchCacheTest, RecentlyUsedItemIsNotEvicted) {
for (size_t i = 0; i < kMaxSize + 1; ++i) { for (size_t i = 0; i < kMaxSize + 1; ++i) {
std::string query = "i=" + base::NumberToString(i); std::string query = QueryWithIParameter(i);
Insert(query, "params, except=(\"i\")"); Insert(query, kVaryOnIParameter);
EXPECT_TRUE(Exists(query)); EXPECT_TRUE(Exists(query));
// Exists() calls Lookup(), which makes an entry "used". // Exists() calls Lookup(), which makes an entry "used".
EXPECT_TRUE(Exists("i=0")); EXPECT_TRUE(Exists("i=0"));
@@ -213,11 +235,10 @@ TEST_P(NoVarySearchCacheTest, RecentlyUsedItemIsNotEvicted) {
} }
TEST_P(NoVarySearchCacheTest, MostRecentlyUsedItemIsNotEvicted) { TEST_P(NoVarySearchCacheTest, MostRecentlyUsedItemIsNotEvicted) {
static constexpr char kNoVarySearchValue[] = "params, except=(\"i\")"; const auto query = QueryWithIParameter;
const auto query = [](int i) { return "i=" + base::NumberToString(i); };
// Fill the cache. // Fill the cache.
for (size_t i = 0; i < kMaxSize; ++i) { for (size_t i = 0; i < kMaxSize; ++i) {
Insert(query(i), kNoVarySearchValue); Insert(query(i), kVaryOnIParameter);
} }
EXPECT_EQ(cache().GetSizeForTesting(), kMaxSize); EXPECT_EQ(cache().GetSizeForTesting(), kMaxSize);
@@ -226,7 +247,7 @@ TEST_P(NoVarySearchCacheTest, MostRecentlyUsedItemIsNotEvicted) {
// Evict kMaxSize - 1 items. // Evict kMaxSize - 1 items.
for (size_t i = kMaxSize; i < kMaxSize * 2 - 1; ++i) { for (size_t i = kMaxSize; i < kMaxSize * 2 - 1; ++i) {
Insert(query(i), kNoVarySearchValue); Insert(query(i), kVaryOnIParameter);
EXPECT_TRUE(Exists(query(i))); EXPECT_TRUE(Exists(query(i)));
} }
@@ -236,11 +257,10 @@ TEST_P(NoVarySearchCacheTest, MostRecentlyUsedItemIsNotEvicted) {
} }
TEST_P(NoVarySearchCacheTest, LeastRecentlyUsedItemIsEvicted) { TEST_P(NoVarySearchCacheTest, LeastRecentlyUsedItemIsEvicted) {
static constexpr char kNoVarySearchValue[] = "params, except=(\"i\")"; const auto query = QueryWithIParameter;
const auto query = [](int i) { return "i=" + base::NumberToString(i); };
// Fill the cache. // Fill the cache.
for (size_t i = 0; i < kMaxSize; ++i) { for (size_t i = 0; i < kMaxSize; ++i) {
Insert(query(i), kNoVarySearchValue); Insert(query(i), kVaryOnIParameter);
} }
EXPECT_EQ(cache().GetSizeForTesting(), kMaxSize); EXPECT_EQ(cache().GetSizeForTesting(), kMaxSize);
@@ -250,7 +270,7 @@ TEST_P(NoVarySearchCacheTest, LeastRecentlyUsedItemIsEvicted) {
} }
// Evict one item. // Evict one item.
Insert(query(kMaxSize), kNoVarySearchValue); Insert(query(kMaxSize), kVaryOnIParameter);
// Verify it was the least-recently-used item. // Verify it was the least-recently-used item.
EXPECT_FALSE(Exists(query(kMaxSize - 1))); EXPECT_FALSE(Exists(query(kMaxSize - 1)));
@@ -319,8 +339,7 @@ TEST_P(NoVarySearchCacheTest, InsertWithBaseURLMatchingEvicted) {
cache().MaybeInsert(my_test_request("will-be-evicted"), cache().MaybeInsert(my_test_request("will-be-evicted"),
TestHeaders("key-order")); TestHeaders("key-order"));
for (size_t i = 1; i < kMaxSize; ++i) { for (size_t i = 1; i < kMaxSize; ++i) {
std::string query = "i=" + base::NumberToString(i); Insert(QueryWithIParameter(i), kVaryOnIParameter);
Insert(query, "params, except=(\"i\")");
} }
EXPECT_EQ(cache().GetSizeForTesting(), kMaxSize); EXPECT_EQ(cache().GetSizeForTesting(), kMaxSize);
@@ -336,8 +355,7 @@ TEST_P(NoVarySearchCacheTest, InsertWithBaseURLMatchingEvicted) {
TEST_P(NoVarySearchCacheTest, InsertWithNoVarySearchValueMatchingEvicted) { TEST_P(NoVarySearchCacheTest, InsertWithNoVarySearchValueMatchingEvicted) {
Insert("will-be-evicted", "params=(\"ignored\")"); Insert("will-be-evicted", "params=(\"ignored\")");
for (size_t i = 1; i < kMaxSize; ++i) { for (size_t i = 1; i < kMaxSize; ++i) {
std::string query = "i=" + base::NumberToString(i); Insert(QueryWithIParameter(i), kVaryOnIParameter);
Insert(query, "params, except=(\"i\")");
} }
EXPECT_EQ(cache().GetSizeForTesting(), kMaxSize); EXPECT_EQ(cache().GetSizeForTesting(), kMaxSize);
@@ -746,6 +764,114 @@ TEST_P(NoVarySearchCacheTest, ClearDataNoMatch) {
EXPECT_TRUE(Exists("a=1")); EXPECT_TRUE(Exists("a=1"));
} }
std::optional<NoVarySearchCache> TestPickleRoundTrip(
const NoVarySearchCache& cache) {
base::Pickle pickle;
WriteToPickle(pickle, cache);
// The estimate of PickeSize should always be correct.
EXPECT_EQ(EstimatePickleSize(cache), pickle.payload_size());
auto maybe_cache = ReadValueFromPickle<NoVarySearchCache>(pickle);
if (!maybe_cache) {
return std::nullopt;
}
EXPECT_EQ(cache.GetSizeForTesting(), maybe_cache->GetSizeForTesting());
return maybe_cache;
}
TEST_P(NoVarySearchCacheTest, SerializeDeserializeEmpty) {
EXPECT_TRUE(TestPickleRoundTrip(cache()));
}
TEST_P(NoVarySearchCacheTest, SerializeDeserializeSimple) {
Insert("b=1", "key-order");
Insert("c&d", "key-order");
Insert("f=3", "params=(\"a\")");
auto new_cache = TestPickleRoundTrip(cache());
ASSERT_TRUE(new_cache);
const auto lookup = [&](std::string_view params) {
return new_cache->Lookup(TestRequest(params));
};
auto maybe_handle1 = lookup("b=1");
auto maybe_handle2 = lookup("d&c");
auto maybe_handle3 = lookup("f=3&a=7");
ASSERT_TRUE(maybe_handle1);
ASSERT_TRUE(maybe_handle2);
ASSERT_TRUE(maybe_handle3);
new_cache->Erase(std::move(maybe_handle1->erase_handle));
new_cache->Erase(std::move(maybe_handle2->erase_handle));
new_cache->Erase(std::move(maybe_handle3->erase_handle));
EXPECT_EQ(new_cache->GetSizeForTesting(), 0u);
EXPECT_TRUE(new_cache->IsTopLevelMapEmptyForTesting());
}
TEST_P(NoVarySearchCacheTest, SerializeDeserializeFull) {
for (size_t i = 0; i < kMaxSize; ++i) {
Insert(QueryWithIParameter(i), kVaryOnIParameter);
}
auto new_cache = TestPickleRoundTrip(cache());
ASSERT_TRUE(new_cache);
for (size_t i = 0; i < kMaxSize; ++i) {
EXPECT_TRUE(new_cache->Lookup(TestRequest(QueryWithIParameter(i))));
}
}
TEST_P(NoVarySearchCacheTest, DeserializeBadSizes) {
struct TestCase {
std::string_view test_description;
int size;
int max_size;
int map_size;
};
static constexpr auto kTestCases = std::to_array<TestCase>({
{"Negative size", -1, 1, 0},
{"Size larger than max_size", 2, 1, 0},
{"Size bigger than map contents", 1, 1, 0},
{"Negative max_size", 0, -1, 0},
{"Zero max_size", 0, 0, 0},
{"Negative map size", 0, 1, -1},
{"Map size larger than map contents", 0, 1, 1},
});
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.test_description);
base::Pickle pickle;
// This uses the fact that containers use an integer for size.
WriteToPickle(pickle, test_case.size, test_case.max_size,
test_case.map_size);
EXPECT_FALSE(ReadValueFromPickle<NoVarySearchCache>(pickle));
}
}
// A truncated Pickle should never deserialize to a NoVarySearchCache object.
// This tests covers many different checks for bad data during deserialization.
TEST_P(NoVarySearchCacheTest, TruncatedPickle) {
Insert("a=9&b=1", "params=(\"a\")");
Insert("a=8&b=2", "params=(\"a\")");
Insert("f=3", "params, except=(\"f\")");
Insert("", "params, except=(\"f\")");
base::Pickle pickle;
WriteToPickle(pickle, cache());
// Go up in increments of 4 bytes because a Pickle with a size that is not a
// multiple of 4 is invalid in a way that is not interesting to this test.
for (size_t bytes = 4u; bytes < pickle.payload_size(); bytes += 4) {
SCOPED_TRACE(bytes);
base::Pickle truncated;
truncated.WriteBytes(pickle.payload_bytes().first(bytes));
EXPECT_FALSE(ReadValueFromPickle<NoVarySearchCache>(truncated));
}
}
INSTANTIATE_TEST_SUITE_P(All, INSTANTIATE_TEST_SUITE_P(All,
NoVarySearchCacheTest, NoVarySearchCacheTest,
::testing::Bool(), ::testing::Bool(),