blob: 96f063e06787597fd224172a0bd09e198e1fdedc [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.
// Implementation of serialization and deserialization for common types, and
// extension points for supporting serialization for additional types.
// Common serialization formats in Chrome:
// - base::Pickle is fast, produces compact output, but has zero
// interoperability. No intrinsic support for forwards/backwards compatibility.
// Corruption that affects size fields will generally be detected and safely
// rejected. Other kinds of corruption will result in a valid instance of the
// data type with corrupted data. There are two systems to extend base::Pickle
// to handle new types: this one, and IPC::ParamTraits.
// - Mojo provides an undiscoverable serialization facility for Mojo types.
// This has similar performance and interoperability characteristics to
// base::Pickle, but is safer and easier to use. However, code in //net cannot
// depend on Mojo.
// - protobuf: slow, compact output, good interoperability. Excellent support
// for forwards/backwards compatibility. Very large impact on binary size due
// to generated code. Corruption may be silently ignored, detected and
// rejected, or give corrupted data.
// - JSON (base::Value, etc.) is very slow, very large output, excellent
// interoperability. Usually straightforward to implement forwards/backwards
// compatibility. Corruption may be detected or produce corrupt data.
// - SQLite is not actually a serialization format, but is often used for
// persistence. It is moderately slow due to the need to round-trip through
// SQL. Good tool availability. Good support for forwards/backwards
// compatibility. Resistant to disk corruption, but needs a recovery path that
// recreates an empty database.
// - Structured headers should be used for HTTP headers.
#ifndef NET_BASE_PICKLE_TRAITS_H_
#define NET_BASE_PICKLE_TRAITS_H_
#include <stddef.h>
#include <concepts>
#include <optional>
#include <ranges>
#include <tuple>
#include <type_traits>
#include <utility>
#include "base/bits.h"
#include "base/containers/span.h"
#include "base/pickle.h"
namespace net {
// To make a type serializable by WriteToPickle and deserializable by
// ReadValueFromPickle, add a specialization of PickleTraits with a Serialize
// method that takes a base::Pickle and a value, and a Deserialize method that
// takes a base::PickleIterator and returns the deserialized value wrapped in
// std::optional, or std::nullopt if the input pickle was invalid.
//
// If the type to be deserialized can be completely reproduced by a call to the
// constructor, it is simple to use ReadValuesFromPickle for deserialization.
//
// For example, suppose your type is:
//
// class MyHostPortPair {
// public:
// MyHostPortPair(std::string_view host, uint16_t port);
//
// const std::string& host() const { return host_; }
// uint16_t port() const { return port_; }
//
// private:
// std::string host_;
// uint16_t port_;
// };
//
// Then you can make it serializable with:
//
// 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;
// }
//
// // Perform any additional validation of `host` and `port`and return
// // std::nullopt if invalid.
// if (host.empty()) {
// return std::nullopt;
// }
//
// auto [host, port] = std::move(result).value();
// return MyHostPortPair(host, port);
// }
// };
//
// If the state of your type cannot be reconstructed by a constructor call, it
// is probably easier to use ReadPickleInto. For example, suppose your class
// looks like this:
//
// class MyHeaders {
// public:
// MyHeaders();
//
// void Add(std::string_view name, std::string_view value);
//
// private:
// std::vector<std::pair<std::string, std::string>> headers_;
// };
//
// In the private section, add:
//
// friend struct PickleTraits<MyHeaders>;
//
// Then you can make it serializable with:
//
// 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;
// }
// // Perform any additional validation of `headers_` and return
// // std::nullopt if invalid.
// if (std::ranges::any_of(
// headers.headers_,
// [](std::string_view name) { return name.empty(); },
// &std::pair<std::string, std::string>::first)) {
// return std::nullopt;
// }
//
// return headers;
// }
//
// static size_t PickleSize(const MyHeaders& value) {
// return EstimatePickleSize(value.headers_);
// }
// };
//
// Providing an implementation of PickleSize() is optional, but will permit the
// right amount of memory to be allocated for the Pickle in advance. It is
// particularly useful for types that will be placed in containers.
//
// There's no need to provide a specialization for "const T" as the const will
// always be removed before looking up a PickleTraits specialization.
//
// Simple structs containing only types for which kPickleAsBytes is true and
// which have no padding bytes can be serialized by copying the underlying
// bytes. This is very fast, particularly when stored in a vector. Beware that
// this may give different results from serializing the members individually, so
// you have to make the choice before serializing anything in production. Also,
// there is no way to verify that the result verifies the constraints for the
// type, so it is only suitable for plain old data.
//
// Suppose you have a struct like this:
//
// struct MyHttpVersion {
// uint16_t major;
// uint16_t minor;
// };
//
// Then you can make it serializable with:
//
// template <>
// constexpr bool kPickleAsBytes<MyHttpVersion> = true;
//
// The declarations to serialize a type should always appear in the same header
// file as the type itself, to ensure that it is always serialized and
// deserialized consistently.
//
// Main implementation of serialization and deserialization, and customization
// point for types to add their own serialization. Types are not serializable by
// default.
template <typename T>
struct PickleTraits {};
// This is automatically set to true for built-in integer types. It can be
// specialized to true for structs of integer types. Some mistaken usage can be
// caught by the compiler, for example using on a struct with padding. But other
// kinds, like using on a struct containing pointers will not be automatically
// caught, so use with care.
template <typename T>
constexpr inline bool kPickleAsBytes = false;
// This is useful in implementations of PickleTraits::PickleSize().
// For many types, the size is known at compile time, so use constexpr to help
// the compiler optimize those cases.
template <typename T>
constexpr size_t EstimatePickleSize(const T& value);
// Multiple-value version for predicting the size of WriteToPickle().
template <typename... Args>
requires(sizeof...(Args) > 1u)
constexpr size_t EstimatePickleSize(const Args&... args);
namespace internal {
// CanSerialize and CanDeserialize are used to determine whether a type can be
// serialized or deserialized. They work by literally checking whether the
// PickleTraits<T>::Serialize and PickleTraits<T>::Deserialize methods are
// callable. As a result, they will give the wrong answer if you try to use them
// on the type you're currently defining PickleTraits for.
template <typename T>
concept CanSerialize = requires(base::Pickle& pickle, const T& value) {
PickleTraits<std::remove_const_t<T>>::Serialize(pickle, value);
};
template <typename T>
concept CanDeserialize = requires(base::PickleIterator& iter) {
PickleTraits<std::remove_const_t<T>>::Deserialize(iter);
};
template <typename T>
concept CanSerializeDeserialize = CanSerialize<T> && CanDeserialize<T>;
// These types can be implicitly converted to char and back without loss of
// precision. This permits highly efficient deserialization of contiguous
// containers of these types.
template <typename T>
concept IsCharLike = std::same_as<T, char> || std::same_as<T, uint8_t> ||
std::same_as<T, int8_t>;
// A shorthand for the type a range contains.
template <typename T>
using ValueType = std::ranges::range_value_t<T>;
// True for std::string, std::vector<uint8_t>, etc.
template <typename T>
concept IsConstructableFromCharLikeIteratorPair =
IsCharLike<ValueType<T>> &&
std::constructible_from<T, const char*, const char*>;
// True for std::u16string, std::vector<int>, etc.
template <typename T>
concept CanResizeAndCopyFromBytes =
std::ranges::contiguous_range<T> && kPickleAsBytes<ValueType<T>> &&
std::default_initializable<T> &&
requires(T t, size_t size, base::span<const uint8_t> byte_span) {
t.resize(size);
base::as_writable_byte_span(t).copy_from(byte_span);
};
// True for std::vector and similar containers.
template <typename T>
concept IsReserveAndPushBackable =
std::default_initializable<T> &&
requires(T t, size_t size, const ValueType<T>& value) {
t.reserve(size);
t.push_back(value);
};
// True for std::list, std::map, std::unordered_set, base::flat_set, etc.
template <typename T>
concept IsInsertAtEndable =
std::default_initializable<T> &&
requires(T t, const ValueType<T>& value) { t.insert(t.end(), value); };
// We only consider a range serializable if we know a way to deserialize it.
template <typename T>
concept IsSerializableRange =
std::ranges::sized_range<T> && CanSerializeDeserialize<ValueType<T>> &&
(IsConstructableFromCharLikeIteratorPair<T> ||
CanResizeAndCopyFromBytes<T> || IsReserveAndPushBackable<T> ||
IsInsertAtEndable<T>);
// True for std::tuple, std::pair and std::array.
template <typename T>
constexpr inline bool kIsTupleLike = false;
// std::tuple_size_v<T> does not work here. Clang refuses to ignore the error
// for non-tuple types.
template <typename T>
requires std::same_as<decltype(std::tuple_size<T>::value), const size_t>
constexpr inline bool kIsTupleLike<T> = true;
// CanSerializeDeserializeTuple is implemented using a consteval function to
// make convenient use of std::index_sequence.
template <typename TupleLike, size_t... I>
requires(kIsTupleLike<TupleLike> &&
std::tuple_size_v<TupleLike> == sizeof...(I))
consteval bool CanSerializeDeserializeTupleImpl(std::index_sequence<I...>) {
return (CanSerializeDeserialize<std::tuple_element_t<I, TupleLike>> && ...);
}
template <typename TupleLike>
requires kIsTupleLike<TupleLike>
consteval bool CanSerializeDeserializeTupleImpl() {
return CanSerializeDeserializeTupleImpl<TupleLike>(
std::make_index_sequence<std::tuple_size_v<TupleLike>>());
}
// A tuple-like type is serializable if all of its elements are serializable.
template <typename TupleLike>
concept CanSerializeDeserializeTuple =
CanSerializeDeserializeTupleImpl<TupleLike>();
// Convenient shortcut when implementing PickleTraits::PickleSize().
// base::Pickle aligns everything to 32-bit boundaries, so we need to round up
// to a multiple of 4 when calculating how big something will be. See
// base::Pickle::ClaimUninitializedBytesInternal().
constexpr size_t RoundUp(size_t size) {
return base::bits::AlignUp(size, sizeof(uint32_t));
}
} // namespace internal
template <typename T>
constexpr size_t EstimatePickleSize(const T& value) {
if constexpr (requires {
{
PickleTraits<T>::PickleSize(value)
} -> std::same_as<size_t>;
}) {
return PickleTraits<T>::PickleSize(value);
} else {
return internal::RoundUp(1); // Everything is padded to at least 4 bytes.
}
}
template <typename... Args>
requires(sizeof...(Args) > 1u)
constexpr size_t EstimatePickleSize(const Args&... args) {
return (EstimatePickleSize(args) + ...);
}
// These are defined in //net/base/pickle.h, but also declared here so that we
// can reference them in non-dependant contexts.
template <typename... Args>
requires(internal::CanSerialize<Args> && ...)
void WriteToPickle(base::Pickle& pickle, const Args&... args);
template <typename T>
requires(internal::CanDeserialize<T>)
std::optional<T> ReadValueFromPickle(base::PickleIterator& iter);
// Built-in non-pointer types can be copied if they have unique representations.
// has_unique_object_representations_v is true for bool and enums but it is not
// safe to deserialize them by copying so specifically exclude them. Exclude
// pointers since we shouldn't ever serialize them. Can be specialized to true
// for structs that contain only types for which kCopyAsBytes is true and which
// have no padding bytes.
template <typename T>
requires(std::has_unique_object_representations_v<T> &&
std::is_trivially_destructible_v<T> &&
std::is_trivially_default_constructible_v<T> &&
!std::is_aggregate_v<T> && !std::is_pointer_v<T> &&
!std::is_member_pointer_v<T> && !std::is_enum_v<T> &&
!std::same_as<T, bool>)
constexpr inline bool kPickleAsBytes<T> = true;
// Implementation of PickleTraits for types for which kPickleAsBytes is true,
// including all built-in integer types.
template <typename T>
requires(kPickleAsBytes<T> && !std::is_const_v<T>)
struct PickleTraits<T> {
static void Serialize(base::Pickle& pickle, const T& value) {
// These are intentionally static_asserts rather than requirements to avoid
// hiding cases where kPickleAsBytes is set incorrectly.
static_assert(std::has_unique_object_representations_v<T>,
"do not set kPickleAsBytes for types where byte equality "
"isn't identical to object equality");
static_assert(std::is_trivially_destructible_v<T>,
"do not set kPickleAsBytes for types that are not trivially "
"destructible");
static_assert(std::is_trivially_default_constructible_v<T>,
"do not set kPickleAsBytes for types that are not trivially "
"default constructible");
pickle.WriteBytes(base::byte_span_from_ref(value));
}
static std::optional<T> Deserialize(base::PickleIterator& iter) {
static_assert(std::has_unique_object_representations_v<T>,
"do not set kPickleAsBytes for types where byte equality "
"isn't identical to object equality");
static_assert(std::is_trivially_destructible_v<T>,
"do not set kPickleAsBytes for types that are not trivially "
"destructible");
static_assert(std::is_trivially_default_constructible_v<T>,
"do not set kPickleAsBytes for types that are not trivially "
"default constructible");
const char* data = nullptr;
if (!iter.ReadBytes(&data, sizeof(T))) {
return std::nullopt;
}
CHECK(data);
T t;
// SAFETY: The `data` pointer is guaranteed to point to at least `sizeof(T)`
// bytes by the ReadBytes call above.
base::byte_span_from_ref(t).copy_from(
base::as_bytes(UNSAFE_BUFFERS(base::span(data, sizeof(T)))));
return t;
}
static constexpr size_t PickleSize(const T& value) {
return internal::RoundUp(sizeof(value));
}
};
// Implementation of PickleTraits for standard containers and types that behave
// like them.
template <typename T>
requires(internal::IsSerializableRange<T>)
struct PickleTraits<T> {
using Value = internal::ValueType<T>;
static void Serialize(base::Pickle& pickle, const T& value) {
// Intentionally crash for containers that are too large to fit in an int.
pickle.WriteInt(base::checked_cast<int>(value.size()));
if constexpr (internal::CanResizeAndCopyFromBytes<T>) {
// This handles string types and vectors of integers.
pickle.WriteBytes(base::as_byte_span(value));
} else {
// This handles non-contiguous containers and values that need to be
// written one at a time.
for (const auto& v : value) {
WriteToPickle(pickle, v);
}
}
}
static std::optional<T> Deserialize(base::PickleIterator& iter) {
int size_as_int = 0;
if (!iter.ReadInt(&size_as_int) || size_as_int < 0) {
return std::nullopt;
}
// Every non-negative integer will fit in a size_t.
const size_t size = static_cast<size_t>(size_as_int);
if (size > iter.RemainingBytes()) {
// Every item in the container must consume at least 1 byte, so this size
// cannot possibly be correct.
return std::nullopt;
}
if constexpr (internal::IsConstructableFromCharLikeIteratorPair<T>) {
// Highly efficient path for std::string, std::vector<uint8_t>, etc.
const char* data = nullptr;
static_assert(sizeof(Value) == 1);
if (!iter.ReadBytes(&data, size)) {
return std::nullopt;
}
CHECK(data);
// SAFETY: The `data` pointer is guaranteed to point to at least `size`
// bytes by the ReadBytes call above.
return T(data, UNSAFE_BUFFERS(data + size));
} else if constexpr (internal::CanResizeAndCopyFromBytes<T>) {
// Slightly less efficient path for std::u16string, std::vector<int>, etc.
T t;
t.resize(size); // Pointlessly zero-fills the container, but avoids UB.
const char* data = nullptr;
const size_t size_in_bytes = size * sizeof(Value);
if (!iter.ReadBytes(&data, size_in_bytes)) {
return std::nullopt;
}
CHECK(data);
// SAFETY: The `data` pointer is guaranteed to point to at least
// `size_in_bytes` bytes by the ReadBytes call above.
base::as_writable_byte_span(t).copy_from(
base::as_bytes(UNSAFE_BUFFERS(base::span(data, size_in_bytes))));
return t;
} else if constexpr (internal::IsReserveAndPushBackable<T>) {
// Slower path for vectors of types that have non-trivial deserialization
// semantics. Also works for vector-like types like absl::InlinedVector.
T t;
t.reserve(size);
for (size_t i = 0; i < size; ++i) {
auto maybe_v = ReadValueFromPickle<Value>(iter);
if (!maybe_v) {
return std::nullopt;
}
t.push_back(std::move(maybe_v).value());
}
return t;
} else {
// std::list, std::map, std::unordered_set, base::flat_set, etc.
static_assert(internal::IsInsertAtEndable<T>);
T t;
for (size_t i = 0; i < size; ++i) {
auto maybe_v = ReadValueFromPickle<Value>(iter);
if (!maybe_v) {
return std::nullopt;
}
t.insert(t.end(), std::move(maybe_v).value());
}
return t;
}
}
static constexpr size_t PickleSize(const T& value) {
if constexpr (internal::CanResizeAndCopyFromBytes<T>) {
return internal::RoundUp(value.size() * sizeof(Value)) + sizeof(int);
} else {
size_t size = sizeof(int);
for (const auto& v : value) {
// If the elements of the container are containers, each one may be a
// different size. If not, the compiler should optimize this down to a
// multiplication.
size += EstimatePickleSize(v);
}
return size;
}
}
};
// Tuple-like types like std::tuple and std::pair.
template <typename T>
requires internal::CanSerializeDeserializeTuple<T>
struct PickleTraits<T> {
static void Serialize(base::Pickle& pickle, const T& value) {
SerializeImpl(pickle, value, kIndexSequence);
}
static std::optional<T> Deserialize(base::PickleIterator& iter) {
return DeserializeImpl(iter, kIndexSequence);
}
static constexpr size_t PickleSize(const T& value) {
return PickleSizeImpl(value, kIndexSequence);
}
private:
template <size_t I>
using ElementType = std::tuple_element_t<I, T>;
template <size_t... I>
static void SerializeImpl(base::Pickle& pickle,
const T& value,
std::index_sequence<I...>) {
(WriteToPickle(pickle, std::get<I>(value)), ...);
}
template <size_t... I>
static std::optional<T> DeserializeImpl(base::PickleIterator& iter,
std::index_sequence<I...>) {
// This is tricky. We cannot expand the template pack directly into the
// parameters of std::make_tuple() because parameter evaluation order is
// different on Windows and Linux, and ReadValueFromPickle() has the
// side-effect of advancing the iterator so order matters. However,
// initializer elements are guaranteed to be evaluated in order, so we can
// safely use initializer syntax.
using TupleOfOptionals = std::tuple<std::optional<ElementType<I>>...>;
TupleOfOptionals tuple_of_optionals = {
ReadValueFromPickle<ElementType<I>>(iter)...};
if (!(std::get<I>(tuple_of_optionals).has_value() && ...)) {
return std::nullopt;
}
return T(std::move(std::get<I>(tuple_of_optionals)).value()...);
}
template <size_t... I>
static constexpr size_t PickleSizeImpl(const T& value,
std::index_sequence<I...>) {
return (EstimatePickleSize(std::get<I>(value)) + ...);
}
static constexpr std::make_index_sequence<std::tuple_size_v<T>>
kIndexSequence{};
};
// bool is treated specially by base::Pickle.
template <>
struct PickleTraits<bool> {
static void Serialize(base::Pickle& pickle, bool value) {
pickle.WriteBool(value);
}
static std::optional<bool> Deserialize(base::PickleIterator& iter) {
bool b;
if (!iter.ReadBool(&b)) {
return std::nullopt;
}
return b;
}
static constexpr size_t PickleSize(bool value) {
return internal::RoundUp(1);
}
};
template <typename T>
requires(internal::CanSerializeDeserialize<std::remove_const_t<T>>)
struct PickleTraits<std::optional<T>> {
static void Serialize(base::Pickle& pickle, std::optional<T> value) {
// Write as `uint8_t` to match Deserialize().
WriteToPickle(pickle, static_cast<uint8_t>(value.has_value()));
if (value.has_value()) {
WriteToPickle(pickle, *value);
}
}
static std::optional<std::optional<T>> Deserialize(
base::PickleIterator& iter) {
auto maybe_has_value = ReadValueFromPickle<uint8_t>(iter);
if (!maybe_has_value) {
return std::nullopt;
}
uint8_t has_value = maybe_has_value.value();
// This is more strict than base::PickleIterator::ReadBool() as it is useful
// to notice data corruption.
if (has_value != 0 && has_value != 1) {
return std::nullopt;
}
if (!has_value) {
// Use the default constructor instead of std::nullopt to avoid confusion.
return std::optional<T>();
}
return ReadValueFromPickle<T>(iter);
}
static constexpr size_t PickleSize(const std::optional<T>& value) {
return EstimatePickleSize(value.has_value()) +
(value.has_value() ? EstimatePickleSize(value.value()) : 0u);
}
};
} // namespace net
#endif // NET_BASE_PICKLE_TRAITS_H_