blob: 5ccf686cbf119cc1d2106f8e065ba96b8fac2360 [file] [log] [blame] [edit]
// 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.h"
#include <stddef.h>
#include <stdint.h>
#include <deque>
#include <list>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "base/compiler_specific.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/memory/scoped_refptr.h"
#include "base/pickle.h"
#include "net/base/pickle_traits.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/container/inlined_vector.h"
#include "third_party/fuzztest/src/fuzztest/fuzztest.h"
namespace net {
namespace {
using ::testing::Optional;
// Tests that base::Pickle methods and WriteToPickle() interoperate. There's no
// clean way to avoid the boilerplate, so just use macros.
#define PICKLE_READ_WRITE_COMPATIBLE_TEST(PickleTypeName, value) \
TEST(PickleTest, PickleReadCompatible##PickleTypeName) { \
base::Pickle pickle; \
pickle.Write##PickleTypeName(value); \
using Value = decltype(value); \
base::PickleIterator iter(pickle); \
EXPECT_THAT(ReadValueFromPickle<Value>(iter), Optional(value)); \
} \
\
TEST(PickleTest, PickleWriteCompatible##PickleTypeName) { \
base::Pickle pickle1; \
pickle1.Write##PickleTypeName(value); \
base::Pickle pickle2; \
WriteToPickle(pickle2, value); \
EXPECT_EQ(base::span(pickle1), base::span(pickle2)); \
} \
static_assert(true, "eat colon")
PICKLE_READ_WRITE_COMPATIBLE_TEST(Bool, true);
PICKLE_READ_WRITE_COMPATIBLE_TEST(Int, 123);
// Don't test Long. No-one should be using Long.
PICKLE_READ_WRITE_COMPATIBLE_TEST(UInt16, uint16_t{123});
PICKLE_READ_WRITE_COMPATIBLE_TEST(UInt32, uint32_t{0xFFFFFFFF});
PICKLE_READ_WRITE_COMPATIBLE_TEST(Int64, int64_t{-42});
PICKLE_READ_WRITE_COMPATIBLE_TEST(UInt64, uint64_t{77});
// Float and Double are not supported by WriteToPickle().
PICKLE_READ_WRITE_COMPATIBLE_TEST(String, std::string("walla"));
// Generic implementation of a test that converts a value to a pickle and back
// again and checks the result matches the original.
template <typename T>
void PerformRoundTripTest(const T& value) {
SCOPED_TRACE(PRETTY_FUNCTION);
base::Pickle pickle;
WriteToPickle(pickle, value);
EXPECT_THAT(ReadValueFromPickle<T>(pickle), Optional(value));
}
// Returns a large set of test values of different types to be used in the
// following tests. These are stored in a tuple as a convenient heterogenous
// container.
auto TestData() {
return std::make_tuple(
true, false, 123, 0xFFFFFFFF, -42, uint8_t{0}, std::string("five"),
std::vector<int>{1, 2, 3}, std::vector<std::string>{"foo", "bar"},
std::optional<int>(42), std::optional<std::string>("hello"),
std::make_pair(1, 'a'), std::make_tuple(1, 'a', 2),
std::map<std::string, std::list<int16_t>>{
{"foo", {1, 2, 3}}, {"bar", {}}, {"", {0x7fff}}},
base::flat_set<uint64_t>{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
base::flat_map<std::string, std::vector<std::string>>{
{"foo", {"bar", "baz"}}, {"", {"qux", "quux"}}},
absl::InlinedVector<char, 5>{'a', 'c'}, std::vector<uint8_t>{1, 2, 3},
std::deque<int16_t>{1, -1}, std::vector<bool>{true, false, true},
absl::InlinedVector<int8_t, 256>{1, 2, 3, 4},
std::set<int64_t>{81, 12, 17}, std::unordered_set<int64_t>{9, 3, 6},
std::unordered_map<int, std::string>{{1, "foo"}, {2, "bar"}, {3, "baz"}},
std::multiset<uint32_t>{1, 1, 1, 2, 2},
std::multimap<std::string, int>{{"foo", 1}, {"foo", 2}, {"foot", 3}},
std::unordered_multiset<int>{{1, 2, 3, 4, 5, 6, 1, 2, 3}},
std::unordered_multimap<int, int>{{1, 2}, {1, 3}},
std::pair<const int, int>(1, 2), std::optional<const int>(3),
std::map<char, const std::string>{{'f', "foo"}, {'b', "bar"}},
std::u16string(u"nicode"));
}
template <typename Functor, size_t... I>
void ForEachTestDataImpl(Functor f, std::index_sequence<I...>) {
const auto data = TestData();
(f(std::get<I>(data)), ...);
}
// Calls f(value) for each value in TestData().
template <typename Functor>
void ForEachTestData(Functor f) {
ForEachTestDataImpl(
f, std::make_index_sequence<std::tuple_size_v<decltype(TestData())>>());
}
TEST(PickleTest, RoundTrip) {
ForEachTestData([](const auto& value) { PerformRoundTripTest(value); });
}
TEST(PickleTest, ReadValuesFromPickle) {
base::Pickle pickle;
WriteToPickle(pickle, 1, std::string("foo"), 'a');
auto result = ReadValuesFromPickle<int, std::string, char>(pickle);
EXPECT_THAT(result, Optional(std::make_tuple(1, "foo", 'a')));
}
TEST(PickleTest, RoundTripTuple) {
base::Pickle pickle;
WriteToPickle(pickle, TestData());
EXPECT_THAT(ReadValueFromPickle<decltype(TestData())>(pickle),
Optional(TestData()));
}
TEST(PickleTest, RejectEmptyPickle) {
ForEachTestData([](const auto& value) {
SCOPED_TRACE(PRETTY_FUNCTION);
base::Pickle pickle;
EXPECT_FALSE(
ReadValueFromPickle<std::remove_cvref_t<decltype(value)>>(pickle));
});
}
TEST(PickleTest, RejectPickleWithTooLittleData) {
ForEachTestData([](const auto& value) {
// Skip cases that might have enough data. base::Pickle aligns all
// allocations to 4 bytes, so the smallest non-empty base::Pickle is 4 bytes
// in size.
if (sizeof(value) <= 4) {
return;
}
SCOPED_TRACE(PRETTY_FUNCTION);
base::Pickle pickle;
WriteToPickle(pickle, 'a');
EXPECT_FALSE(
ReadValueFromPickle<std::remove_cvref_t<decltype(value)>>(pickle));
});
}
TEST(PickleTest, EstimatePickleSize) {
ForEachTestData([](const auto& value) {
SCOPED_TRACE(PRETTY_FUNCTION);
base::Pickle pickle;
WriteToPickle(pickle, value);
// EstimatePickleSize() is not guaranteed to be exact in general, but it
// happens to be correct for the cases were are testing.
EXPECT_EQ(pickle.payload_size(), EstimatePickleSize(value));
});
}
TEST(PickleTest, EstimatePickleSizeMultipleValues) {
int i = 3;
std::string s = "word";
bool b = true;
EXPECT_EQ(
EstimatePickleSize(i, s, b),
EstimatePickleSize(i) + EstimatePickleSize(s) + EstimatePickleSize(b));
}
// Fuzz test. To cover all the cases, we need to include these types:
// * int (for kCopyAsBytes codepath)
// * std::string (for IsConstructableFromCharLikeIteratorPair path)
// * std::vector<uint16_t> (for CanResizeAndCopyFromBytes path)
// * std::vector<T> for some T that is not kCopyAsBytes, for the
// IsReserveAndPushBackable path
// * std::set for the IsInsertAndEnable path.
// * std::tuple for the CanSerializeDeserializeTuple path,
// * bool for the bool specialization,
// * std::optional for the std::optional specialization.
using FuzzType = std::set<std::tuple<bool,
std::optional<int>,
std::vector<std::string>,
std::vector<uint16_t>>>;
void FuzzRoundTrip(const FuzzType& value) {
base::Pickle pickle;
WriteToPickle(pickle, value);
EXPECT_EQ(ReadValueFromPickle<FuzzType>(pickle), value);
}
FUZZ_TEST(PickleTest, FuzzRoundTrip);
// Tests for user-defined serialization.
class MyHostPortPair {
public:
MyHostPortPair(std::string_view host, uint16_t port)
: host_(host), port_(port) {}
const std::string& host() const { return host_; }
uint16_t port() const { return port_; }
bool operator==(const MyHostPortPair& other) const = default;
private:
std::string host_;
uint16_t port_;
};
class MyHeaders {
public:
MyHeaders() = default;
void Add(std::string_view name, std::string_view value) {
headers_.emplace_back(name, value);
}
bool operator==(const MyHeaders& other) const = default;
private:
friend struct PickleTraits<MyHeaders>;
std::vector<std::pair<std::string, std::string>> headers_;
};
struct MyHttpVersion {
uint16_t major;
uint16_t minor;
bool operator==(const MyHttpVersion& other) const = default;
};
} // namespace
// We cannot specialize PickleTraits from inside the anonymous namespace, so we
// had to leave it.
template <>
struct PickleTraits<MyHostPortPair> {
static void Serialize(base::Pickle& pickle, const MyHostPortPair& value) {
WriteToPickle(pickle, value.host(), value.port());
}
static std::optional<MyHostPortPair> Deserialize(base::PickleIterator& iter) {
auto result = ReadValuesFromPickle<std::string, uint16_t>(iter);
if (!result) {
return std::nullopt;
}
auto [host, port] = std::move(result).value();
return MyHostPortPair(host, port);
}
};
template <>
struct PickleTraits<MyHeaders> {
static void Serialize(base::Pickle& pickle, const MyHeaders& value) {
WriteToPickle(pickle, value.headers_);
}
static std::optional<MyHeaders> Deserialize(base::PickleIterator& iter) {
MyHeaders headers;
if (!ReadPickleInto(iter, headers.headers_)) {
return std::nullopt;
}
return headers;
}
static size_t PickleSize(const MyHeaders& value) {
return EstimatePickleSize(value.headers_);
}
};
template <>
constexpr bool kPickleAsBytes<MyHttpVersion> = true;
namespace {
TEST(PickleTraitsTest, MyHostPortPair) {
MyHostPortPair pair("foo", 42);
PerformRoundTripTest(pair);
}
TEST(PickleTraitsTest, MyHeaders) {
MyHeaders headers;
headers.Add("User-Agent", "Mozilla/5.0");
headers.Add("Content-Type", "application/octet-stream");
PerformRoundTripTest(headers);
}
TEST(PickleTraitsTest, MyHttpVersion) {
MyHttpVersion version = {1, 2};
PerformRoundTripTest(version);
}
} // namespace
} // namespace net