blob: 4371e6e885be00b7123f30fa3f5045eb0a9d06d3 [file] [log] [blame]
// 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/http/no_vary_search_cache.h"
#include <algorithm>
#include <map>
#include <ostream>
#include <tuple>
#include <type_traits>
#include <utility>
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/map_util.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "base/pickle.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/types/expected_macros.h"
#include "net/base/pickle.h"
#include "net/base/pickle_base_types.h"
#include "net/http/http_cache.h"
#include "net/http/http_no_vary_search_data.h"
namespace net {
namespace {
// We need to use a separate enum for the
// HttpCache.NoVarySearch.HeaderParseResult than
// HttpNoVarySearchData::ParseErrorEnum, as that enum does not have a value for
// a successful parse.
//
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
//
// LINT.IfChange(NoVarySearchHeaderParseResult)
enum class NoVarySearchHeaderParseResult {
kSuccess = 0,
kNoHeader = 1,
kDefaultValue = 2,
kNotDictionary = 3,
kNonBooleanKeyOrder = 4,
kParamsNotStringList = 5,
kExceptNotStringList = 6,
kExceptWithoutTrueParams = 7,
kMaxValue = kExceptWithoutTrueParams,
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/net/enums.xml:NoVarySearchHeaderParseResult)
NoVarySearchHeaderParseResult MapParseErrorEnum(
HttpNoVarySearchData::ParseErrorEnum error) {
using enum HttpNoVarySearchData::ParseErrorEnum;
switch (error) {
case kOk:
return NoVarySearchHeaderParseResult::kNoHeader;
case kDefaultValue:
return NoVarySearchHeaderParseResult::kDefaultValue;
case kNotDictionary:
return NoVarySearchHeaderParseResult::kNotDictionary;
case kUnknownDictionaryKey:
NOTREACHED(); // No longer used.
case kNonBooleanKeyOrder:
return NoVarySearchHeaderParseResult::kNonBooleanKeyOrder;
case kParamsNotStringList:
return NoVarySearchHeaderParseResult::kParamsNotStringList;
case kExceptNotStringList:
return NoVarySearchHeaderParseResult::kExceptNotStringList;
case kExceptWithoutTrueParams:
return NoVarySearchHeaderParseResult::kExceptWithoutTrueParams;
}
NOTREACHED();
}
void EmitNoVarySearchHeaderParseResultHistogram(
const base::expected<HttpNoVarySearchData,
HttpNoVarySearchData::ParseErrorEnum>& result) {
auto value = NoVarySearchHeaderParseResult::kSuccess;
if (!result.has_value()) {
value = MapParseErrorEnum(result.error());
}
UMA_HISTOGRAM_ENUMERATION("HttpCache.NoVarySearch.HeaderParseResult", value);
}
// Stripping the URL of its query and fragment (ref) needs to be done for every
// request, so we want to avoid allocating memory for a GURL in the case of a
// cache miss. The return value points at memory owned by `url`, so should not
// outlive it.
std::string_view ExtractBaseURL(const GURL& url) {
CHECK(url.is_valid());
const std::string_view spec = url.spec();
const size_t query_pos = spec.find('?');
if (query_pos != std::string_view::npos) {
return spec.substr(0, query_pos);
}
const size_t ref_pos = spec.find('#');
if (ref_pos != std::string_view::npos) {
return spec.substr(0, ref_pos);
}
return spec;
}
bool URLIsAcceptable(const GURL& url) {
// HTTP(S) URLs always have a path starting with "/" after canonicalization.
return url.is_valid() && url.has_path() && !url.has_username() &&
!url.has_password();
}
bool BaseURLIsAcceptable(const GURL& base_url) {
return URLIsAcceptable(base_url) && !base_url.has_query() &&
!base_url.has_ref();
}
// Given `base_url` and `query`, return the original URL that would have been
// used to construct them.
GURL ReconstructOriginalURLFromQuery(std::string_view base_url,
const std::optional<std::string>& query) {
if (!query.has_value()) {
return GURL(base_url);
}
return GURL(base::StrCat({base_url, "?", query.value()}));
}
} // namespace
// Query is the leaf entry type for the cache. Its main purpose is to hold the
// query string, ie. everything between the "?" and the "#" in the original URL.
// Combined with the `base_url`, this can be used to reconstruct the original
// URL that was used to store the original request in the disk cache.
class NoVarySearchCache::Query final : public base::LinkNode<Query> {
public:
using PassKey = base::PassKey<Query>;
// Creates a Query and adds it to `lru_list`.
static std::unique_ptr<Query> CreateAndInsert(
std::optional<std::string> query,
base::LinkedList<Query>& lru_list,
base::Time update_time,
const std::string* canonicalized_query,
Queries* queries) {
DCHECK(!query || query->find('#') == std::string_view::npos)
<< "Query contained a '#' character, meaning that the URL reassembly "
"will not work correctly because the '#' will be re-interpreted as "
"the start of a fragment. This should not happen. Query was '"
<< query.value() << "'";
auto created = std::make_unique<Query>(PassKey(), query, update_time);
created->canonicalized_query_ = canonicalized_query;
created->queries_ = queries;
created->InsertBefore(lru_list.head());
return created;
}
Query(PassKey, std::optional<std::string_view> query, base::Time update_time)
: query_(query), update_time_(update_time) {}
// Not copyable or movable.
Query(const Query&) = delete;
Query& operator=(const Query&) = delete;
// Removes this object from the LRU list.
~Query() {
// During deserialization a Query 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 (next()) {
CHECK(previous());
RemoveFromList();
}
}
// Moves this object to the head of `linked_list`.
void MoveToHead(base::LinkedList<Query>& linked_list) {
auto* head = linked_list.head();
if (head != this) {
MoveBeforeNode(linked_list.head()->value());
}
}
const std::optional<std::string>& query() const { return query_; }
const std::string& canonicalized_query() const {
return *canonicalized_query_;
}
Queries& queries() const { return *queries_; }
base::Time update_time() const { return update_time_; }
void set_update_time(base::Time update_time) { update_time_ = update_time; }
// Return the original GURL that this entry was constructed from (not
// including any fragment). It's important to use this method to correctly
// reconstruct URLs that have an empty query (end in '?').
GURL ReconstructOriginalURL(const std::string& base_url) const {
return ReconstructOriginalURLFromQuery(base_url, query_);
}
EraseHandle CreateEraseHandle() {
return EraseHandle(weak_factory_.GetWeakPtr());
}
void set_canonicalized_query(const std::string* canonicalized_query) {
canonicalized_query_ = canonicalized_query;
}
void set_queries(Queries* queries) { queries_ = queries; }
private:
friend struct PickleTraits<std::unique_ptr<Query>>;
// Deserialization is implemented here for easy access to the PassKey.
static std::optional<std::unique_ptr<Query>> Deserialize(
base::PickleIterator& iter) {
std::optional<std::string> query;
base::Time update_time;
if (!ReadPickleInto(iter, query, update_time)) {
return std::nullopt;
}
// The other fields will be filled in in a second pass while deserializing
// the top-level NoVarySearchCache object.
return std::make_unique<Query>(PassKey(), std::move(query), update_time);
}
// Moves this object in front of `node`.
void MoveBeforeNode(Query* node) {
CHECK_NE(node, this);
RemoveFromList();
InsertBefore(node);
}
// No-Vary-Search treats "http://www.example.com/" and
// "http://www.example.com/?" as the same URL, but the disk cache key treats
// them as different URLs, so we need to be able to distinguish them to
// correctly reconstruct the original URL. `query_ == std::nullopt` means that
// there was no `?` in the original URL, and `query_ == ""` means there was.
const std::optional<std::string> query_;
// `canonicalized_query_` allows this entry to be located in the query_map so
// that it can be erased efficiently. The pointed-to std::string is this
// object's key in the map, so it is always deleted after this object.
raw_ptr<const std::string> canonicalized_query_ = nullptr;
// `queries_` provides a pointer to the structure with the other pointers
// we need to find this entry in the cache. It is owned by the
// NVSDataToQueriesMap. This object is always deleted before `queries_`
// because this object is owned by the `query_map` inside Queries.
raw_ptr<Queries> queries_ = nullptr;
// `update_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
// disk cache.
base::Time update_time_;
// EraseHandle uses weak pointers to Query objects to enable an entry to
// be deleted from the cache if it is found not to be readable from the disk
// cache.
base::WeakPtrFactory<Query> weak_factory_{this};
};
NoVarySearchCache::EraseHandle::EraseHandle(base::WeakPtr<Query> query)
: query_(std::move(query)) {}
NoVarySearchCache::EraseHandle::~EraseHandle() = default;
NoVarySearchCache::EraseHandle::EraseHandle(EraseHandle&& rhs) = default;
NoVarySearchCache::EraseHandle& NoVarySearchCache::EraseHandle::operator=(
EraseHandle&& rhs) = default;
bool NoVarySearchCache::EraseHandle::EqualsForTesting(
const EraseHandle& rhs) const {
return query_.get() == rhs.query_.get();
}
bool NoVarySearchCache::EraseHandle::IsGoneForTesting() const {
return !query_;
}
NoVarySearchCache::Journal::~Journal() = default;
NoVarySearchCache::Queries::Queries() = default;
NoVarySearchCache::Queries::~Queries() = default;
NoVarySearchCache::Queries::Queries(Queries&&) = default;
NoVarySearchCache::NoVarySearchCache(size_t max_size) : max_size_(max_size) {
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)
: partitions_(std::move(rhs.partitions_)),
lru_(std::move(rhs.lru_)),
size_(std::exchange(rhs.size_, 0u)),
max_size_(rhs.max_size_) {}
NoVarySearchCache::~NoVarySearchCache() {
partitions_.clear();
// Clearing the map should have freed all the Query objects.
CHECK(lru_.empty());
}
std::optional<NoVarySearchCache::LookupResult> NoVarySearchCache::Lookup(
const HttpRequestInfo& request) {
bool unused = true;
return Lookup(request, /*out_base_url_matched=*/unused);
}
std::optional<NoVarySearchCache::LookupResult> NoVarySearchCache::Lookup(
const HttpRequestInfo& request,
bool& out_base_url_matched) {
out_base_url_matched = false;
const GURL& url = request.url;
if (!URLIsAcceptable(url)) {
return std::nullopt;
}
TRACE_EVENT("net", "NoVarySearchCache::Lookup");
// TODO(https://crbug.com/388956603): This does a lot of allocations and
// string copies. Try to reduce the amount of work done for a miss.
ASSIGN_OR_RETURN(
const std::string cache_partition_key,
HttpCache::GenerateCachePartitionKeyForRequest(request),
[]() -> std::optional<NoVarySearchCache::LookupResult> { return {}; });
const auto partition_it = partitions_.find(cache_partition_key);
if (partition_it == partitions_.end()) {
return std::nullopt;
}
auto& [cache_partition_key_ref, base_url_map] = *partition_it;
const std::string_view base_url_view = ExtractBaseURL(url);
const auto base_url_map_it = base_url_map.find(base_url_view);
if (base_url_map_it == base_url_map.end()) {
return std::nullopt;
}
out_base_url_matched = true;
auto& [base_url_ref, nvs_data_to_queries_map] = *base_url_map_it;
Query* best_match = nullptr;
// `nvs_data_to_queries_map` should ideally only have one entry, but if the
// site has sent different No-Vary-Search header values for the same base URL
// we need to check them all.
for (auto& [nvs_data, queries] : nvs_data_to_queries_map) {
Query* result = FindQuery(queries.query_map, url, nvs_data);
if (result &&
(!best_match || best_match->update_time() < result->update_time())) {
best_match = result;
}
}
if (!best_match) {
return std::nullopt;
}
// This is a hit. Move to head of `lru_` list.
best_match->MoveToHead(lru_);
return LookupResult(best_match->ReconstructOriginalURL(base_url_ref),
best_match->CreateEraseHandle());
}
void NoVarySearchCache::MaybeInsert(const HttpRequestInfo& request,
const HttpResponseHeaders& headers) {
const GURL& url = request.url;
if (!URLIsAcceptable(url)) {
return;
}
auto maybe_nvs_data = HttpNoVarySearchData::ParseFromHeaders(headers);
EmitNoVarySearchHeaderParseResultHistogram(maybe_nvs_data);
if (!maybe_nvs_data.has_value()) {
return;
}
const std::string_view base_url = ExtractBaseURL(url);
std::optional<std::string> query =
url.has_query() ? std::make_optional(url.GetQuery()) : std::nullopt;
ASSIGN_OR_RETURN(const std::string cache_partition_key,
HttpCache::GenerateCachePartitionKeyForRequest(request),
[] { return; });
const base::Time update_time = base::Time::Now();
DoInsert(url, std::move(cache_partition_key), std::string(base_url),
std::move(maybe_nvs_data.value()), std::move(query), update_time,
journal_);
}
bool NoVarySearchCache::ClearData(UrlFilterType filter_type,
const base::flat_set<url::Origin>& origins,
const base::flat_set<std::string>& domains,
base::Time delete_begin,
base::Time delete_end) {
// For simplicity, first collect a list of matching Query objects to erase and
// then erase them.
// TODO(https://crbug.com/382394774): Make this algorithm more efficient.
std::vector<Query*> pending_erase;
for (auto& [cache_partition_key, base_url_map] : partitions_) {
for (auto& [base_url_ref, nvs_data_to_queries_map] : base_url_map) {
const GURL base_url(base_url_ref);
CHECK(base_url.is_valid());
// DoesUrlMatchFilter() only looks at the origin of the URL, which is why
// we don't need to worry about reconstructing the full URL with query.
if (DoesUrlMatchFilter(filter_type, origins, domains, base_url)) {
FindQuerysInTimeRange(nvs_data_to_queries_map, delete_begin, delete_end,
pending_erase);
}
}
}
for (Query* query : pending_erase) {
EraseQuery(query);
}
return !pending_erase.empty();
}
void NoVarySearchCache::Erase(EraseHandle handle) {
if (Query* query = handle.query_.get()) {
if (journal_) {
const Queries& queries = query->queries();
journal_->OnErase(*queries.cache_partition_key_ptr, *queries.base_url_ptr,
*queries.nvs_data_ptr, query->query());
}
EraseQuery(query);
}
}
void NoVarySearchCache::SetJournal(Journal* journal) {
journal_ = journal;
}
void NoVarySearchCache::ReplayInsert(std::string partition_key,
std::string base_url,
HttpNoVarySearchData nvs_data,
std::optional<std::string> query,
base::Time update_time) {
const GURL base_gurl(base_url);
if (!BaseURLIsAcceptable(base_gurl)) {
return;
}
// The URL should have been stored in its canonical form.
if (base_url != base_gurl.spec()) {
return;
}
if (query && query->find('#') != std::string::npos) {
return;
}
// To be extra careful to avoid re-entrancy, explicitly set `journal` to
// nullptr so that no notification is fired for this insertion.
ReconstructURLAndDoInsert(std::move(partition_key), std::move(base_url),
std::move(nvs_data), std::move(query), update_time,
/*journal=*/nullptr);
}
void NoVarySearchCache::ReplayErase(const std::string& partition_key,
const std::string& base_url,
const HttpNoVarySearchData& nvs_data,
const std::optional<std::string>& query) {
const auto map_it = partitions_.find(partition_key);
if (map_it == partitions_.end()) {
return;
}
BaseUrlToNVSDataMap& base_url_map = map_it->second;
const auto base_url_it = base_url_map.find(base_url);
if (base_url_it == base_url_map.end()) {
return;
}
NVSDataToQueriesMap& nvs_data_to_queries_map = base_url_it->second;
const auto data_it = nvs_data_to_queries_map.find(nvs_data);
if (data_it == nvs_data_to_queries_map.end()) {
return;
}
Queries& queries = data_it->second;
const GURL original_url = ReconstructOriginalURLFromQuery(base_url, query);
// Since `base_url` was already in the map, it must have been valid. There is
// no value of `query` that GURL will not canonicalize. So the resulting URL
// is always valid (even if the query itself was corrupted).
CHECK(original_url.is_valid());
const std::string canonicalized_query =
nvs_data.CanonicalizeQuery(original_url);
const auto query_it = queries.query_map.find(canonicalized_query);
if (query_it == queries.query_map.end()) {
return;
}
Query* query_ptr = query_it->second.get();
// TODO(https://crbug.com/382394774): This could be made more efficient in the
// case when the map keys need to be deleted since we have `map_it` and
// `data_it` already available.
EraseQuery(query_ptr);
}
void NoVarySearchCache::MergeFrom(const NoVarySearchCache& newer) {
// We cannot use ForEachQuery() here as we need to iterate through the
// `lru_` linked list in reverse order.
const auto& newer_lru = newer.lru_;
for (auto* node = newer_lru.tail(); node != newer_lru.end();
node = node->previous()) {
Query* query = node->value();
const Queries& queries = query->queries();
const std::string& base_url = *queries.base_url_ptr;
std::optional<std::string> query_string = query->query();
CHECK(!query_string || query_string->find('#') == std::string::npos);
// Pass `journal_` so the merged entries are journalled as insertions.
ReconstructURLAndDoInsert(*queries.cache_partition_key_ptr, base_url,
*queries.nvs_data_ptr, std::move(query_string),
query->update_time(), journal_);
}
}
void NoVarySearchCache::SetMaxSize(size_t max_size) {
if (max_size == max_size_) {
return;
}
CHECK_GE(max_size, 1u);
// Evict entries while size_ > max_size_.
max_size_ = max_size;
while (size_ > max_size_) {
EraseQuery(lru_.tail()->value());
}
}
bool NoVarySearchCache::IsTopLevelMapEmptyForTesting() const {
return partitions_.empty();
}
void NoVarySearchCache::EvictIfOverfull() {
CHECK_LE(size_, max_size_ + 1);
if (size_ == max_size_ + 1) {
// This happens when an entry is added when the cache is already full.
// Remove an entry to make `size_` == `max_size_` again.
EraseQuery(lru_.tail()->value());
}
}
void NoVarySearchCache::EraseQuery(Query* query) {
CHECK_GT(size_, 0u);
--size_;
Queries& queries = query->queries();
const std::string& canonicalized_query = query->canonicalized_query();
const auto query_map_it = queries.query_map.find(canonicalized_query);
CHECK(query_map_it != queries.query_map.end());
queries.query_map.erase(query_map_it);
if (!queries.query_map.empty()) {
return;
}
// The Queries object is now empty, so we should delete it from its parent
// map. First we have to find its parent map.
const HttpNoVarySearchData& nvs_data = *queries.nvs_data_ptr;
const std::string& base_url = *queries.base_url_ptr;
const std::string& partition_key = *queries.cache_partition_key_ptr;
const auto partition_it = partitions_.find(partition_key);
CHECK(partition_it != partitions_.end());
auto& base_url_map = partition_it->second;
const auto base_url_it = base_url_map.find(base_url);
CHECK(base_url_it != base_url_map.end());
NVSDataToQueriesMap& nvs_data_to_queries_map = base_url_it->second;
const auto data_it = nvs_data_to_queries_map.find(nvs_data);
CHECK(data_it != nvs_data_to_queries_map.end());
nvs_data_to_queries_map.erase(data_it);
if (!nvs_data_to_queries_map.empty()) {
return;
}
base_url_map.erase(base_url_it);
if (!base_url_map.empty()) {
return;
}
partitions_.erase(partition_it);
}
void NoVarySearchCache::DoInsert(const GURL& url,
std::string partition_key,
std::string base_url,
HttpNoVarySearchData nvs_data,
std::optional<std::string> query,
base::Time update_time,
Journal* journal) {
auto [partition_it, partition_inserted] =
partitions_.try_emplace(std::move(partition_key), BaseUrlToNVSDataMap());
auto& [cache_partition_key_ref, base_url_map] = *partition_it;
CHECK(partition_inserted || !base_url_map.empty());
auto [base_url_it, base_url_inserted] =
base_url_map.try_emplace(std::move(base_url), NVSDataToQueriesMap());
auto& [base_url_ref, nvs_data_to_queries_map] = *base_url_it;
CHECK(base_url_inserted || !nvs_data_to_queries_map.empty());
auto [nvs_data_it, nvs_data_inserted] =
nvs_data_to_queries_map.try_emplace(std::move(nvs_data), Queries());
auto& [nvs_data_ref, queries] = *nvs_data_it;
if (nvs_data_inserted) {
queries.nvs_data_ptr = &nvs_data_ref;
queries.base_url_ptr = &base_url_ref;
queries.cache_partition_key_ptr = &cache_partition_key_ref;
} else {
CHECK(!queries.query_map.empty());
CHECK_EQ(queries.nvs_data_ptr, &nvs_data_ref);
CHECK_EQ(queries.base_url_ptr, &base_url_ref);
CHECK_EQ(queries.cache_partition_key_ptr, &cache_partition_key_ref);
}
auto call_journal = [&journal, &cache_partition_key_ref, &base_url_ref,
&nvs_data_ref,
update_time](const std::optional<std::string>& query) {
if (journal) {
journal->OnInsert(cache_partition_key_ref, base_url_ref, nvs_data_ref,
query, update_time);
}
};
const std::string canonicalized_query = nvs_data_ref.CanonicalizeQuery(url);
auto [query_it, query_inserted] =
queries.query_map.try_emplace(canonicalized_query, nullptr);
if (!query_inserted) {
// There was already an entry for this `canonicalized_query`. We need
// to check if it is an exact match for the URL we're trying to insert. If
// it is, we don't need to replace it.
auto& query_ptr = query_it->second;
if (query_ptr->query() == query) {
// It's an exact match. Just mark as freshly updated and used.
query_ptr->set_update_time(update_time);
query_ptr->MoveToHead(lru_);
call_journal(query_ptr->query());
return;
}
// Otherwise, replace it with a newly-created Query.
query_ptr = nullptr;
CHECK_GT(size_, 0u);
--size_;
}
auto& [canonicalized_query_ref, query_ptr] = *query_it;
query_ptr = Query::CreateAndInsert(std::move(query), lru_, update_time,
&canonicalized_query_ref, &queries);
CHECK_LE(size_, max_size_);
++size_;
call_journal(query_ptr->query());
EvictIfOverfull();
}
void NoVarySearchCache::ReconstructURLAndDoInsert(
std::string partition_key,
std::string base_url,
HttpNoVarySearchData nvs_data,
std::optional<std::string> query,
base::Time update_time,
Journal* journal) {
const GURL url = ReconstructOriginalURLFromQuery(base_url, query);
DoInsert(url, std::move(partition_key), std::move(base_url),
std::move(nvs_data), std::move(query), update_time, journal);
}
// static
void NoVarySearchCache::FindQuerysInTimeRange(
NVSDataToQueriesMap& nvs_data_to_queries_map,
base::Time delete_begin,
base::Time delete_end,
std::vector<Query*>& matches) {
for (auto& [_, queries] : nvs_data_to_queries_map) {
ForEachQuery(queries, [&](Query* query) {
const base::Time update_time = query->update_time();
if ((delete_begin.is_null() || delete_begin <= update_time) &&
(delete_end.is_max() || delete_end > update_time)) {
matches.push_back(query);
}
});
}
}
// static
NoVarySearchCache::Query* NoVarySearchCache::FindQuery(
CanonicalizedQueryToQueryMap& query_map,
const GURL& url,
const HttpNoVarySearchData& nvs_data) {
const std::string canonicalized_query = nvs_data.CanonicalizeQuery(url);
return base::FindPtrOrNull(query_map, std::move(canonicalized_query));
}
// static
void NoVarySearchCache::ForEachQuery(Queries& queries,
base::FunctionRef<void(Query*)> f) {
for (auto& [_, query_ptr] : queries.query_map) {
f(query_ptr.get());
}
}
template <>
struct PickleTraits<std::unique_ptr<NoVarySearchCache::Query>> {
using Query = NoVarySearchCache::Query;
static void Serialize(base::Pickle& pickle,
const std::unique_ptr<Query>& query) {
WriteToPickle(pickle, query->query(), query->update_time());
}
static std::optional<std::unique_ptr<Query>> Deserialize(
base::PickleIterator& iter) {
return Query::Deserialize(iter);
}
static size_t PickleSize(const std::unique_ptr<Query>& query) {
return EstimatePickleSize(query->query(), query->update_time());
}
};
template <>
struct PickleTraits<NoVarySearchCache::Queries> {
static void Serialize(base::Pickle& pickle,
const NoVarySearchCache::Queries& queries) {
WriteToPickle(pickle, queries.query_map);
}
static std::optional<NoVarySearchCache::Queries> Deserialize(
base::PickleIterator& iter) {
NoVarySearchCache::Queries queries;
if (!ReadPickleInto(iter, queries.query_map)) {
return std::nullopt;
}
return queries;
}
static size_t PickleSize(const NoVarySearchCache::Queries& queries) {
return EstimatePickleSize(queries.query_map);
}
};
// 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.partitions_);
}
// 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.partitions_)) {
return std::nullopt;
}
using Query = NoVarySearchCache::Query;
// Get a list of every Query object in the map so that we can sort
// them to reconstruct the `lru_` list. std::multimap is used here as a
// workaround for the excessive binary size cost of std::sort.
std::multimap<base::Time, Query*> all_queries;
for (auto& [cache_partition_key, base_url_map] : cache.partitions_) {
for (auto& [base_url_ref, nvs_data_to_queries_map] : base_url_map) {
for (auto& [nvs_data, queries] : nvs_data_to_queries_map) {
queries.nvs_data_ptr = &nvs_data;
queries.base_url_ptr = &base_url_ref;
queries.cache_partition_key_ptr = &cache_partition_key;
for (auto& [canonicalized_query, query_ptr] : queries.query_map) {
query_ptr->set_canonicalized_query(&canonicalized_query);
query_ptr->set_queries(&queries);
all_queries.emplace(query_ptr->update_time(), query_ptr.get());
}
}
}
}
if (size != all_queries.size()) {
return std::nullopt;
}
// Insert each entry at the head of the list, so that the oldest entry ends
// up at the tail.
for (auto [_, qs] : all_queries) {
qs->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.partitions_);
}
} // namespace net