// 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 "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 { … }; inline ABSL_ATTRIBUTE_ALWAYS_INLINE bool IsWriterStatusOk(bool status) { … } inline ABSL_ATTRIBUTE_ALWAYS_INLINE bool IsWriterStatusOk( const absl::Status& status) { … } // ------------------- WireType() wrapper definitions ------------------- // Base class for WireUint8/16/32/64. template <typename T> class QUICHE_EXPORT WireFixedSizeIntBase { … }; // Fixed-size integer fields. Correspond to (8), (16), (32) and (64) fields in // RFC 9000 language. class QUICHE_EXPORT WireUint8 : public WireFixedSizeIntBase<uint8_t> { … }; class QUICHE_EXPORT WireUint16 : public WireFixedSizeIntBase<uint16_t> { … }; class QUICHE_EXPORT WireUint32 : public WireFixedSizeIntBase<uint32_t> { … }; class QUICHE_EXPORT WireUint64 : public WireFixedSizeIntBase<uint64_t> { … }; // 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 { … }; // Represents unframed raw string. class QUICHE_EXPORT WireBytes { … }; // Represents a string where another wire type is used as a length prefix. template <class LengthWireType> class QUICHE_EXPORT WireStringWithLengthPrefix { … }; // Represents varint62-prefixed strings. WireStringWithVarInt62Length; // Allows std::optional to be used with this API. For instance, if the spec // defines // [Context ID (i)] // and the value is stored as std::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 { … }; // 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 { … }; // ------------------- Top-level serialization API ------------------- namespace wire_serialization_internal { template <typename T> auto SerializeIntoWriterWrapper(QuicheDataWriter& writer, int argno, T data) { … } template <typename T> std::enable_if_t<SerializeIntoWriterStatus<T>::kIsBool, absl::Status> SerializeIntoWriterCore(QuicheDataWriter& writer, int argno, T data) { … } template <typename T> std::enable_if_t<SerializeIntoWriterStatus<T>::kIsStatus, absl::Status> SerializeIntoWriterCore(QuicheDataWriter& writer, int argno, T data) { … } template <typename T1, typename... Ts> absl::Status SerializeIntoWriterCore(QuicheDataWriter& writer, int argno, T1 data1, Ts... rest) { … } inline absl::Status SerializeIntoWriterCore(QuicheDataWriter&, int) { … } } // 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) { … } // ComputeLengthOnWire(writer, d1, d2, ... dN) calculates the number of bytes // necessary to serialize the supplied data. template <typename T> size_t ComputeLengthOnWire(T data) { … } template <typename T1, typename... Ts> size_t ComputeLengthOnWire(T1 data1, Ts... rest) { … } inline size_t ComputeLengthOnWire() { … } // 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) { … } // SerializeIntoBuffer() that returns std::string instead of QuicheBuffer. template <typename... Ts> absl::StatusOr<std::string> SerializeIntoString(Ts... data) { … } } // namespace quiche #endif // QUICHE_COMMON_WIRE_SERIALIZATION_H_