blob: 89a54f2711e136bb8e806c72981cb63127c2c011 [file] [log] [blame]
// Copyright (c) 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// wire_serialization.h -- absl::StrCat()-like interface for QUICHE wire format.
//
// When serializing a data structure, there are two common approaches:
// (1) Allocate into a dynamically sized buffer and incur the costs of memory
// allocations.
// (2) Precompute the length of the structure, allocate a buffer of the
// exact required size and then write into the said buffer.
// QUICHE generally takes the second approach, but as a result, a lot of
// serialization code is written twice. This API avoids this issue by letting
// the caller declaratively describe the wire format; the description provided
// is used both for the size computation and for the serialization.
//
// Consider the following struct in RFC 9000 language:
// Test Struct {
// Magic Value (32),
// Some Number (i),
// [Optional Number (i)],
// Magical String Length (i),
// Magical String (..),
// }
//
// Using the functions in this header, it can be serialized as follows:
// absl::StatusOr<quiche::QuicheBuffer> test_struct = SerializeIntoBuffer(
// WireUint32(magic_value),
// WireVarInt62(some_number),
// WireOptional<WireVarint62>(optional_number),
// WireStringWithVarInt62Length(magical_string)
// );
//
// This header provides three main functions with fairly self-explanatory names:
// - size_t ComputeLengthOnWire(d1, d2, ... dN)
// - absl::Status SerializeIntoWriter(writer, d1, d2, ... dN)
// - absl::StatusOr<QuicheBuffer> SerializeIntoBuffer(allocator, d1, ... dN)
//
// It is possible to define a custom serializer for individual structs. Those
// would normally look like this:
//
// struct AwesomeStruct { ... }
// class WireAwesomeStruct {
// public:
// using DataType = AwesomeStruct;
// WireAwesomeStruct(const AwesomeStruct& awesome) : awesome_(awesome) {}
// size_t GetLengthOnWire() { ... }
// absl::Status SerializeIntoWriter(QuicheDataWriter& writer) { ... }
// };
//
// See the unit test for the full version of the example above.
#ifndef QUICHE_COMMON_WIRE_SERIALIZATION_H_
#define QUICHE_COMMON_WIRE_SERIALIZATION_H_
#include <cstddef>
#include <cstdint>
#include <optional>
#include <tuple>
#include <type_traits>
#include <utility>
#include "absl/base/attributes.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "absl/types/optional.h"
#include "quiche/common/platform/api/quiche_logging.h"
#include "quiche/common/quiche_buffer_allocator.h"
#include "quiche/common/quiche_data_writer.h"
#include "quiche/common/quiche_status_utils.h"
namespace quiche {
// T::SerializeIntoWriter() is allowed to return both a bool and an
// absl::Status. There are two reasons for that:
// 1. Most QuicheDataWriter methods return a bool.
// 2. While cheap, absl::Status has a non-trivial destructor and thus is not
// as free as a bool is.
// To accomodate this, SerializeIntoWriterStatus<T> provides a way to deduce
// what is the status type returned by the SerializeIntoWriter method.
template <typename T>
class QUICHE_NO_EXPORT SerializeIntoWriterStatus {
public:
static_assert(std::is_trivially_copyable_v<T> && sizeof(T) <= 32,
"The types passed into SerializeInto() APIs are passed by "
"value; if your type has non-trivial copy costs, it should be "
"wrapped into a type that carries a pointer");
using Type = decltype(std::declval<T>().SerializeIntoWriter(
std::declval<QuicheDataWriter&>()));
static constexpr bool kIsBool = std::is_same_v<Type, bool>;
static constexpr bool kIsStatus = std::is_same_v<Type, absl::Status>;
static_assert(
kIsBool || kIsStatus,
"SerializeIntoWriter() has to return either a bool or an absl::Status");
static ABSL_ATTRIBUTE_ALWAYS_INLINE Type OkValue() {
if constexpr (kIsStatus) {
return absl::OkStatus();
} else {
return true;
}
}
};
inline ABSL_ATTRIBUTE_ALWAYS_INLINE bool IsWriterStatusOk(bool status) {
return status;
}
inline ABSL_ATTRIBUTE_ALWAYS_INLINE bool IsWriterStatusOk(
const absl::Status& status) {
return status.ok();
}
// ------------------- WireType() wrapper definitions -------------------
// Base class for WireUint8/16/32/64.
template <typename T>
class QUICHE_EXPORT WireFixedSizeIntBase {
public:
using DataType = T;
static_assert(std::is_integral_v<DataType>,
"WireFixedSizeIntBase is only usable with integral types");
explicit WireFixedSizeIntBase(T value) { value_ = value; }
size_t GetLengthOnWire() const { return sizeof(T); }
T value() const { return value_; }
private:
T value_;
};
// Fixed-size integer fields. Correspond to (8), (16), (32) and (64) fields in
// RFC 9000 language.
class QUICHE_EXPORT WireUint8 : public WireFixedSizeIntBase<uint8_t> {
public:
using WireFixedSizeIntBase::WireFixedSizeIntBase;
bool SerializeIntoWriter(QuicheDataWriter& writer) const {
return writer.WriteUInt8(value());
}
};
class QUICHE_EXPORT WireUint16 : public WireFixedSizeIntBase<uint16_t> {
public:
using WireFixedSizeIntBase::WireFixedSizeIntBase;
bool SerializeIntoWriter(QuicheDataWriter& writer) const {
return writer.WriteUInt16(value());
}
};
class QUICHE_EXPORT WireUint32 : public WireFixedSizeIntBase<uint32_t> {
public:
using WireFixedSizeIntBase::WireFixedSizeIntBase;
bool SerializeIntoWriter(QuicheDataWriter& writer) const {
return writer.WriteUInt32(value());
}
};
class QUICHE_EXPORT WireUint64 : public WireFixedSizeIntBase<uint64_t> {
public:
using WireFixedSizeIntBase::WireFixedSizeIntBase;
bool SerializeIntoWriter(QuicheDataWriter& writer) const {
return writer.WriteUInt64(value());
}
};
// Represents a 62-bit variable-length non-negative integer. Those are
// described in the Section 16 of RFC 9000, and are denoted as (i) in type
// descriptions.
class QUICHE_EXPORT WireVarInt62 {
public:
using DataType = uint64_t;
explicit WireVarInt62(uint64_t value) { value_ = value; }
// Convenience wrapper. This is safe, since it is clear from the context that
// the enum is being treated as an integer.
template <typename T>
explicit WireVarInt62(T value) {
static_assert(std::is_enum_v<T> || std::is_convertible_v<T, uint64_t>);
value_ = static_cast<uint64_t>(value);
}
size_t GetLengthOnWire() const {
return QuicheDataWriter::GetVarInt62Len(value_);
}
bool SerializeIntoWriter(QuicheDataWriter& writer) const {
return writer.WriteVarInt62(value_);
}
private:
uint64_t value_;
};
// Represents unframed raw string.
class QUICHE_EXPORT WireBytes {
public:
using DataType = absl::string_view;
explicit WireBytes(absl::string_view value) { value_ = value; }
size_t GetLengthOnWire() { return value_.size(); }
bool SerializeIntoWriter(QuicheDataWriter& writer) {
return writer.WriteStringPiece(value_);
}
private:
absl::string_view value_;
};
// Represents a string where another wire type is used as a length prefix.
template <class LengthWireType>
class QUICHE_EXPORT WireStringWithLengthPrefix {
public:
using DataType = absl::string_view;
explicit WireStringWithLengthPrefix(absl::string_view value) {
value_ = value;
}
size_t GetLengthOnWire() {
return LengthWireType(value_.size()).GetLengthOnWire() + value_.size();
}
absl::Status SerializeIntoWriter(QuicheDataWriter& writer) {
if (!LengthWireType(value_.size()).SerializeIntoWriter(writer)) {
return absl::InternalError("Failed to serialize the length prefix");
}
if (!writer.WriteStringPiece(value_)) {
return absl::InternalError("Failed to serialize the string proper");
}
return absl::OkStatus();
}
private:
absl::string_view value_;
};
// Represents varint62-prefixed strings.
using WireStringWithVarInt62Length = WireStringWithLengthPrefix<WireVarInt62>;
// Allows absl::optional to be used with this API. For instance, if the spec
// defines
// [Context ID (i)]
// and the value is stored as absl::optional<uint64> context_id, this can be
// recorded as
// WireOptional<WireVarInt62>(context_id)
// When optional is absent, nothing is written onto the wire.
template <typename WireType, typename InnerType = typename WireType::DataType>
class QUICHE_EXPORT WireOptional {
public:
using DataType = absl::optional<InnerType>;
using Status = SerializeIntoWriterStatus<WireType>;
explicit WireOptional(DataType value) { value_ = value; }
size_t GetLengthOnWire() const {
return value_.has_value() ? WireType(*value_).GetLengthOnWire() : 0;
}
typename Status::Type SerializeIntoWriter(QuicheDataWriter& writer) const {
if (value_.has_value()) {
return WireType(*value_).SerializeIntoWriter(writer);
}
return Status::OkValue();
}
private:
DataType value_;
};
// Allows multiple entries of the same type to be serialized in a single call.
template <typename WireType,
typename SpanElementType = typename WireType::DataType>
class QUICHE_EXPORT WireSpan {
public:
using DataType = absl::Span<const SpanElementType>;
explicit WireSpan(DataType value) { value_ = value; }
size_t GetLengthOnWire() const {
size_t total = 0;
for (const SpanElementType& value : value_) {
total += WireType(value).GetLengthOnWire();
}
return total;
}
absl::Status SerializeIntoWriter(QuicheDataWriter& writer) const {
for (size_t i = 0; i < value_.size(); i++) {
// `status` here can be either a bool or an absl::Status.
auto status = WireType(value_[i]).SerializeIntoWriter(writer);
if (IsWriterStatusOk(status)) {
continue;
}
if constexpr (SerializeIntoWriterStatus<WireType>::kIsStatus) {
return AppendToStatus(std::move(status),
" while serializing the value #", i);
} else {
return absl::InternalError(
absl::StrCat("Failed to serialize vector value #", i));
}
}
return absl::OkStatus();
}
private:
DataType value_;
};
// ------------------- Top-level serialization API -------------------
namespace wire_serialization_internal {
template <typename T>
auto SerializeIntoWriterWrapper(QuicheDataWriter& writer, int argno, T data) {
#if defined(NDEBUG)
(void)argno;
(void)data;
return data.SerializeIntoWriter(writer);
#else
// When running in the debug build, we check that the length reported by
// GetLengthOnWire() matches what is actually being written onto the wire.
// While any mismatch will most likely lead to an error further down the line,
// this simplifies the debugging process.
const size_t initial_offset = writer.length();
const size_t expected_size = data.GetLengthOnWire();
auto result = data.SerializeIntoWriter(writer);
const size_t final_offset = writer.length();
if (IsWriterStatusOk(result)) {
QUICHE_DCHECK_EQ(initial_offset + expected_size, final_offset)
<< "while serializing field #" << argno;
}
return result;
#endif
}
template <typename T>
std::enable_if_t<SerializeIntoWriterStatus<T>::kIsBool, absl::Status>
SerializeIntoWriterCore(QuicheDataWriter& writer, int argno, T data) {
const bool success = SerializeIntoWriterWrapper(writer, argno, data);
if (!success) {
return absl::InternalError(
absl::StrCat("Failed to serialize field #", argno));
}
return absl::OkStatus();
}
template <typename T>
std::enable_if_t<SerializeIntoWriterStatus<T>::kIsStatus, absl::Status>
SerializeIntoWriterCore(QuicheDataWriter& writer, int argno, T data) {
return AppendToStatus(SerializeIntoWriterWrapper(writer, argno, data),
" while serializing field #", argno);
}
template <typename T1, typename... Ts>
absl::Status SerializeIntoWriterCore(QuicheDataWriter& writer, int argno,
T1 data1, Ts... rest) {
QUICHE_RETURN_IF_ERROR(SerializeIntoWriterCore(writer, argno, data1));
return SerializeIntoWriterCore(writer, argno + 1, rest...);
}
} // namespace wire_serialization_internal
// SerializeIntoWriter(writer, d1, d2, ... dN) serializes all of supplied data
// into the writer |writer|. True is returned on success, and false is returned
// if serialization fails (typically because the writer ran out of buffer). This
// is conceptually similar to absl::StrAppend().
template <typename... Ts>
absl::Status SerializeIntoWriter(QuicheDataWriter& writer, Ts... data) {
return wire_serialization_internal::SerializeIntoWriterCore(
writer, /*argno=*/0, data...);
}
// ComputeLengthOnWire(writer, d1, d2, ... dN) calculates the number of bytes
// necessary to serialize the supplied data.
template <typename T>
size_t ComputeLengthOnWire(T data) {
return data.GetLengthOnWire();
}
template <typename T1, typename... Ts>
size_t ComputeLengthOnWire(T1 data1, Ts... rest) {
return data1.GetLengthOnWire() + ComputeLengthOnWire(rest...);
}
// SerializeIntoBuffer(allocator, d1, d2, ... dN) computes the length required
// to store the supplied data, allocates the buffer of appropriate size using
// |allocator|, and serializes the result into it. In a rare event that the
// serialization fails (e.g. due to invalid varint62 value), an empty buffer is
// returned.
template <typename... Ts>
absl::StatusOr<QuicheBuffer> SerializeIntoBuffer(
QuicheBufferAllocator* allocator, Ts... data) {
size_t buffer_size = ComputeLengthOnWire(data...);
if (buffer_size == 0) {
return QuicheBuffer();
}
QuicheBuffer buffer(allocator, buffer_size);
QuicheDataWriter writer(buffer.size(), buffer.data());
QUICHE_RETURN_IF_ERROR(SerializeIntoWriter(writer, data...));
if (writer.remaining() != 0) {
return absl::InternalError(absl::StrCat(
"Excess ", writer.remaining(), " bytes allocated while serializing"));
}
return buffer;
}
} // namespace quiche
#endif // QUICHE_COMMON_WIRE_SERIALIZATION_H_