Modernizes HTTP/2 frame representations in spdy_protocol.h Introduces value-semantic, standard-layout POD structs, C++20 type traits, concepts, compile-time size calculation and serialization, and a 64-byte single-cache-line SpdyFrame variant based on the analysis in go/http2-frame-representations. This change introduces the types, but does not use them anywhere, yet. Key changes: - Defined standard-layout POD structs for all 14 HTTP/2 frame types (DataFrame, HeadersFrame, PriorityFrame, RstStreamFrame, SettingsFrame, PushPromiseFrame, PingFrame, GoAwayFrame, WindowUpdateFrame, ContinuationFrame, AltSvcFrame, PriorityUpdateFrame, AcceptChFrame, UnknownFrame). - Added C++20 FrameTraits<T> and Http2Frame concept. - Added compile-time FrameSize<T> and SerializeFrame<T> in SpdyFrameBuilder. - Added SpdyFrame variant fitting in 64 bytes (1 cache line) and dispatch helpers. - Maintained backward compatibility for all existing SpdyFrameIR classes and SpdyFrameVisitor without changing callers. PiperOrigin-RevId: 981852294
diff --git a/quiche/http2/core/spdy_frame_builder.h b/quiche/http2/core/spdy_frame_builder.h index ee879a4..5628753 100644 --- a/quiche/http2/core/spdy_frame_builder.h +++ b/quiche/http2/core/spdy_frame_builder.h
@@ -135,6 +135,335 @@ size_t offset_; // Position at which the latest frame begins. }; +// Packs the exclusive dependency bit (bit 31) and the 31-bit parent stream ID +// into a single 32-bit unsigned integer in network wire format for PRIORITY and +// HEADERS frames. +inline uint32_t PackStreamDependencyValues(bool exclusive, + SpdyStreamId parent_stream_id) { + uint32_t parent = parent_stream_id & 0x7fffffff; + uint32_t e_bit = exclusive ? 0x80000000 : 0; + return parent | e_bit; +} + +inline bool SerializeDataFrame(const DataFrame& frame, + SpdyFrameBuilder& builder) { + uint8_t flags = frame.flags; + if (frame.fin()) { + flags |= DATA_FLAG_FIN; + } + if (frame.padded()) { + flags |= DATA_FLAG_PADDED; + } + size_t payload_len = frame.data.size() + + (frame.padded() ? (1 + frame.padding_payload_len) : 0); + if (!builder.BeginNewFrame(SpdyFrameType::DATA, flags, frame.stream_id, + payload_len)) { + return false; + } + if (frame.padded()) { + if (!builder.WriteUInt8(frame.padding_payload_len)) { + return false; + } + } + if (!builder.WriteBytes(frame.data.data(), frame.data.size())) { + return false; + } + if (frame.padded() && frame.padding_payload_len > 0) { + std::string padding(frame.padding_payload_len, 0); + if (!builder.WriteBytes(padding.data(), padding.length())) { + return false; + } + } + return true; +} + +inline bool SerializeHeadersFrame(const HeadersFrame& frame, + SpdyFrameBuilder& builder) { + uint8_t flags = frame.flags; + if (frame.fin()) { + flags |= CONTROL_FLAG_FIN; + } + if (frame.end_headers()) { + flags |= HEADERS_FLAG_END_HEADERS; + } + if (frame.padded()) { + flags |= HEADERS_FLAG_PADDED; + } + if (frame.has_priority) { + flags |= HEADERS_FLAG_PRIORITY; + } + size_t payload_len = frame.hpack_block.size() + + (frame.padded() ? (1 + frame.padding_payload_len) : 0) + + (frame.has_priority ? 5 : 0); + if (!builder.BeginNewFrame(SpdyFrameType::HEADERS, flags, frame.stream_id, + payload_len)) { + return false; + } + if (frame.padded()) { + if (!builder.WriteUInt8(frame.padding_payload_len)) { + return false; + } + } + if (frame.has_priority) { + int weight = ClampHttp2Weight(frame.priority.weight); + if (!builder.WriteUInt32(PackStreamDependencyValues( + frame.priority.exclusive, frame.priority.parent_stream_id))) { + return false; + } + if (!builder.WriteUInt8(static_cast<uint8_t>(weight - 1))) { + return false; + } + } + if (!builder.WriteBytes(frame.hpack_block.data(), frame.hpack_block.size())) { + return false; + } + if (frame.padded() && frame.padding_payload_len > 0) { + std::string padding(frame.padding_payload_len, 0); + if (!builder.WriteBytes(padding.data(), padding.length())) { + return false; + } + } + return true; +} + +inline bool SerializePriorityFrame(const PriorityFrame& frame, + SpdyFrameBuilder& builder) { + if (!builder.BeginNewFrame(SpdyFrameType::PRIORITY, 0, frame.stream_id, 5)) { + return false; + } + int weight = ClampHttp2Weight(frame.priority.weight); + if (!builder.WriteUInt32(PackStreamDependencyValues( + frame.priority.exclusive, frame.priority.parent_stream_id))) { + return false; + } + return builder.WriteUInt8(static_cast<uint8_t>(weight - 1)); +} + +inline bool SerializeRstStreamFrame(const RstStreamFrame& frame, + SpdyFrameBuilder& builder) { + if (!builder.BeginNewFrame(SpdyFrameType::RST_STREAM, 0, frame.stream_id, + 4)) { + return false; + } + return builder.WriteUInt32(static_cast<uint32_t>(frame.error_code)); +} + +inline bool SerializeSettingsFrame(const SettingsFrame& frame, + SpdyFrameBuilder& builder) { + uint8_t flags = frame.is_ack ? SETTINGS_FLAG_ACK : 0; + size_t payload_len = + frame.is_ack ? 0 : (frame.values.size() * kSettingsOneSettingSize); + if (!builder.BeginNewFrame(SpdyFrameType::SETTINGS, flags, 0, payload_len)) { + return false; + } + if (!frame.is_ack) { + for (const auto& param : frame.values) { + if (!builder.WriteUInt16(param.id) || !builder.WriteUInt32(param.value)) { + return false; + } + } + } + return true; +} + +inline bool SerializePushPromiseFrame(const PushPromiseFrame& frame, + SpdyFrameBuilder& builder) { + uint8_t flags = frame.flags; + if (frame.end_headers()) { + flags |= PUSH_PROMISE_FLAG_END_PUSH_PROMISE; + } + if (frame.padded()) { + flags |= PUSH_PROMISE_FLAG_PADDED; + } + size_t payload_len = 4 + frame.hpack_block.size() + + (frame.padded() ? (1 + frame.padding_payload_len) : 0); + if (!builder.BeginNewFrame(SpdyFrameType::PUSH_PROMISE, flags, + frame.stream_id, payload_len)) { + return false; + } + if (frame.padded()) { + if (!builder.WriteUInt8(frame.padding_payload_len)) { + return false; + } + } + if (!builder.WriteUInt32(frame.promised_stream_id & 0x7fffffff)) { + return false; + } + if (!builder.WriteBytes(frame.hpack_block.data(), frame.hpack_block.size())) { + return false; + } + if (frame.padded() && frame.padding_payload_len > 0) { + std::string padding(frame.padding_payload_len, 0); + if (!builder.WriteBytes(padding.data(), padding.length())) { + return false; + } + } + return true; +} + +inline bool SerializePingFrame(const PingFrame& frame, + SpdyFrameBuilder& builder) { + uint8_t flags = frame.is_ack ? PING_FLAG_ACK : 0; + if (!builder.BeginNewFrame(SpdyFrameType::PING, flags, 0, 8)) { + return false; + } + return builder.WriteUInt64(frame.opaque_data); +} + +inline bool SerializeGoAwayFrame(const GoAwayFrame& frame, + SpdyFrameBuilder& builder) { + size_t payload_len = 8 + frame.debug_data.size(); + if (!builder.BeginNewFrame(SpdyFrameType::GOAWAY, 0, 0, payload_len)) { + return false; + } + if (!builder.WriteUInt32(frame.last_good_stream_id & 0x7fffffff)) { + return false; + } + if (!builder.WriteUInt32(static_cast<uint32_t>(frame.error_code))) { + return false; + } + if (!frame.debug_data.empty()) { + if (!builder.WriteBytes(frame.debug_data.data(), frame.debug_data.size())) { + return false; + } + } + return true; +} + +inline bool SerializeWindowUpdateFrame(const WindowUpdateFrame& frame, + SpdyFrameBuilder& builder) { + if (!builder.BeginNewFrame(SpdyFrameType::WINDOW_UPDATE, 0, frame.stream_id, + 4)) { + return false; + } + return builder.WriteUInt32(frame.delta & 0x7fffffff); +} + +inline bool SerializeContinuationFrame(const ContinuationFrame& frame, + SpdyFrameBuilder& builder) { + uint8_t flags = frame.flags; + if (frame.end_headers()) { + flags |= HEADERS_FLAG_END_HEADERS; + } + if (!builder.BeginNewFrame(SpdyFrameType::CONTINUATION, flags, + frame.stream_id, frame.hpack_block.size())) { + return false; + } + return builder.WriteBytes(frame.hpack_block.data(), frame.hpack_block.size()); +} + +inline bool SerializeAltSvcFrame(const AltSvcFrame& frame, + SpdyFrameBuilder& builder) { + size_t payload_len = 2 + frame.origin.size() + frame.value.size(); + if (!builder.BeginNewFrame(SpdyFrameType::ALTSVC, 0, frame.stream_id, + payload_len)) { + return false; + } + if (!builder.WriteUInt16(frame.origin.size())) { + return false; + } + if (!builder.WriteBytes(frame.origin.data(), frame.origin.size())) { + return false; + } + return builder.WriteBytes(frame.value.data(), frame.value.size()); +} + +inline bool SerializePriorityUpdateFrame(const PriorityUpdateFrame& frame, + SpdyFrameBuilder& builder) { + size_t payload_len = 4 + frame.priority_field_value.size(); + if (!builder.BeginNewFrame(SpdyFrameType::PRIORITY_UPDATE, 0, 0, + payload_len)) { + return false; + } + if (!builder.WriteUInt32(frame.prioritized_stream_id & 0x7fffffff)) { + return false; + } + return builder.WriteBytes(frame.priority_field_value.data(), + frame.priority_field_value.size()); +} + +inline bool SerializeAcceptChFrame(const AcceptChFrame& frame, + SpdyFrameBuilder& builder) { + size_t payload_len = 0; + for (size_t i = 0; i < frame.num_entries && i < frame.entries.size(); ++i) { + payload_len += + 4 + frame.entries[i].origin.size() + frame.entries[i].value.size(); + } + if (!builder.BeginNewFrame(SpdyFrameType::ACCEPT_CH, 0, 0, payload_len)) { + return false; + } + for (size_t i = 0; i < frame.num_entries && i < frame.entries.size(); ++i) { + if (!builder.WriteUInt16(frame.entries[i].origin.size()) || + !builder.WriteBytes(frame.entries[i].origin.data(), + frame.entries[i].origin.size()) || + !builder.WriteUInt16(frame.entries[i].value.size()) || + !builder.WriteBytes(frame.entries[i].value.data(), + frame.entries[i].value.size())) { + return false; + } + } + return true; +} + +inline bool SerializeUnknownFrame(const UnknownFrame& frame, + SpdyFrameBuilder& builder) { + // Handles other unknown frame types, where the payload is opaque. + if (!builder.BeginNewUncheckedFrame(frame.type, frame.flags, frame.stream_id, + frame.payload.size())) { + return false; + } + return builder.WriteBytes(frame.payload.data(), frame.payload.size()); +} + +// Serializes a strongly-typed HTTP/2 frame representation directly into the +// provided `SpdyFrameBuilder`. +// +// This generic template writes HTTP/2 wire-format frames (RFC 9113) without +// allocating intermediate heap objects. Returns true on success, or false if +// the buffer cannot accommodate the frame or serialization fails. +template <Http2FrameConcept T> +bool SerializeFrame(const T& frame, SpdyFrameBuilder& builder) { + if constexpr (std::is_same_v<T, DataFrame>) { + return SerializeDataFrame(frame, builder); + } else if constexpr (std::is_same_v<T, HeadersFrame>) { + return SerializeHeadersFrame(frame, builder); + } else if constexpr (std::is_same_v<T, PriorityFrame>) { + return SerializePriorityFrame(frame, builder); + } else if constexpr (std::is_same_v<T, RstStreamFrame>) { + return SerializeRstStreamFrame(frame, builder); + } else if constexpr (std::is_same_v<T, SettingsFrame>) { + return SerializeSettingsFrame(frame, builder); + } else if constexpr (std::is_same_v<T, PushPromiseFrame>) { + return SerializePushPromiseFrame(frame, builder); + } else if constexpr (std::is_same_v<T, PingFrame>) { + return SerializePingFrame(frame, builder); + } else if constexpr (std::is_same_v<T, GoAwayFrame>) { + return SerializeGoAwayFrame(frame, builder); + } else if constexpr (std::is_same_v<T, WindowUpdateFrame>) { + return SerializeWindowUpdateFrame(frame, builder); + } else if constexpr (std::is_same_v<T, ContinuationFrame>) { + return SerializeContinuationFrame(frame, builder); + } else if constexpr (std::is_same_v<T, AltSvcFrame>) { + return SerializeAltSvcFrame(frame, builder); + } else if constexpr (std::is_same_v<T, PriorityUpdateFrame>) { + return SerializePriorityUpdateFrame(frame, builder); + } else if constexpr (std::is_same_v<T, AcceptChFrame>) { + return SerializeAcceptChFrame(frame, builder); + } else if constexpr (std::is_same_v<T, UnknownFrame>) { + return SerializeUnknownFrame(frame, builder); + } + return false; +} + +// Serializes a unified `SpdyFrame` variant (containing any standard-layout +// HTTP/2 frame type) into the provided `SpdyFrameBuilder` using `std::visit`. +// Returns true on success, or false if serialization fails. +inline bool SerializeSpdyFrame(const SpdyFrame& frame, + SpdyFrameBuilder& builder) { + return std::visit( + [&builder](const auto& f) { return SerializeFrame(f, builder); }, frame); +} + } // namespace spdy #endif // QUICHE_HTTP2_CORE_SPDY_FRAME_BUILDER_H_
diff --git a/quiche/http2/core/spdy_framer.cc b/quiche/http2/core/spdy_framer.cc index 9f400c4..699eb96 100644 --- a/quiche/http2/core/spdy_framer.cc +++ b/quiche/http2/core/spdy_framer.cc
@@ -27,17 +27,6 @@ namespace { -// Pack parent stream ID and exclusive flag into the format used by HTTP/2 -// headers and priority frames. -uint32_t PackStreamDependencyValues(bool exclusive, - SpdyStreamId parent_stream_id) { - // Make sure the highest-order bit in the parent stream id is zeroed out. - uint32_t parent = parent_stream_id & 0x7fffffff; - // Set the one-bit exclusivity flag. - uint32_t e_bit = exclusive ? 0x80000000 : 0; - return parent | e_bit; -} - // Used to indicate no flags in a HTTP2 flags field. const uint8_t kNoFlags = 0;
diff --git a/quiche/http2/core/spdy_protocol.h b/quiche/http2/core/spdy_protocol.h index a5ec174..8ee1af2 100644 --- a/quiche/http2/core/spdy_protocol.h +++ b/quiche/http2/core/spdy_protocol.h
@@ -9,6 +9,8 @@ #ifndef QUICHE_HTTP2_CORE_SPDY_PROTOCOL_H_ #define QUICHE_HTTP2_CORE_SPDY_PROTOCOL_H_ +#include <array> +#include <concepts> #include <cstddef> #include <cstdint> #include <cstring> @@ -17,10 +19,12 @@ #include <memory> #include <optional> #include <string> +#include <type_traits> #include <utility> #include <variant> #include <vector> +#include "absl/container/inlined_vector.h" #include "absl/strings/string_view.h" #include "quiche/http2/core/spdy_alt_svc_wire_format.h" #include "quiche/http2/core/spdy_bitmasks.h" @@ -447,6 +451,367 @@ typedef StreamPrecedence<SpdyStreamId> SpdyStreamPrecedence; +// ============================================================================= +// Value-Semantic POD Frame Structs +// ============================================================================= + +// Contains the fields specific to any priority-conveying frames. +struct QUICHE_EXPORT PriorityFields { + SpdyStreamId parent_stream_id = 0; + uint16_t weight = kHttp2DefaultStreamWeight; + bool exclusive = false; + constexpr bool operator==(const PriorityFields&) const = default; +}; + +// A container for a single SETTINGS field. +struct QUICHE_EXPORT SettingParameter { + SpdySettingsId id = 0; + uint32_t value = 0; + constexpr bool operator==(const SettingParameter&) const = default; +}; + +// A container for a single ACCEPT_CH entry. +struct QUICHE_EXPORT AcceptChEntryView { + absl::string_view origin; + absl::string_view value; + constexpr bool operator==(const AcceptChEntryView&) const = default; +}; + +struct QUICHE_EXPORT DataFrame { + SpdyStreamId stream_id = 0; + uint8_t flags = 0; + uint8_t padding_payload_len = 0; + absl::string_view data; + + constexpr bool HasFin() const { return (flags & DATA_FLAG_FIN) != 0; } + ABSL_DEPRECATED("Use HasFin() instead.") constexpr bool fin() const { + return HasFin(); + } + constexpr bool IsPadded() const { + return (flags & DATA_FLAG_PADDED) != 0 || padding_payload_len > 0; + } + ABSL_DEPRECATED("Use IsPadded() instead.") constexpr bool padded() const { + return IsPadded(); + } + constexpr bool operator==(const DataFrame&) const = default; +}; +static_assert(sizeof(DataFrame) == (sizeof(void*) == 8 ? 24 : 16)); + +struct QUICHE_EXPORT HeadersFrame { + SpdyStreamId stream_id = 0; + uint8_t flags = 0; + uint8_t padding_payload_len = 0; + bool has_priority = false; + PriorityFields priority{}; + absl::string_view hpack_block; + + constexpr bool HasFin() const { return (flags & CONTROL_FLAG_FIN) != 0; } + ABSL_DEPRECATED("Use HasFin() instead.") constexpr bool fin() const { + return HasFin(); + } + constexpr bool HasEndHeaders() const { + return (flags & HEADERS_FLAG_END_HEADERS) != 0; + } + ABSL_DEPRECATED("Use HasEndHeaders() instead.") + constexpr bool end_headers() const { return HasEndHeaders(); } + constexpr bool IsPadded() const { + return (flags & HEADERS_FLAG_PADDED) != 0 || padding_payload_len > 0; + } + ABSL_DEPRECATED("Use IsPadded() instead.") constexpr bool padded() const { + return IsPadded(); + } + constexpr bool operator==(const HeadersFrame&) const = default; +}; +static_assert(sizeof(HeadersFrame) == (sizeof(void*) == 8 ? 32 : 24)); + +struct QUICHE_EXPORT PriorityFrame { + SpdyStreamId stream_id = 0; + PriorityFields priority{}; + constexpr bool operator==(const PriorityFrame&) const = default; +}; +static_assert(sizeof(PriorityFrame) == 12); + +struct QUICHE_EXPORT RstStreamFrame { + SpdyStreamId stream_id = 0; + SpdyErrorCode error_code = ERROR_CODE_NO_ERROR; + constexpr bool operator==(const RstStreamFrame&) const = default; +}; +static_assert(sizeof(RstStreamFrame) == 8); + +struct QUICHE_EXPORT SettingsFrame { + static constexpr size_t kMaxInlineSettings = 5; + bool is_ack = false; + absl::InlinedVector<SettingParameter, kMaxInlineSettings> values; + bool operator==(const SettingsFrame&) const = default; +}; +static_assert(sizeof(SettingsFrame) == (sizeof(void*) == 8 ? 56 : 48)); + +struct QUICHE_EXPORT PushPromiseFrame { + SpdyStreamId stream_id = 0; + SpdyStreamId promised_stream_id = 0; + uint8_t flags = 0; + uint8_t padding_payload_len = 0; + absl::string_view hpack_block; + + constexpr bool HasEndHeaders() const { + return (flags & PUSH_PROMISE_FLAG_END_PUSH_PROMISE) != 0; + } + ABSL_DEPRECATED("Use HasEndHeaders() instead.") + constexpr bool end_headers() const { return HasEndHeaders(); } + constexpr bool IsPadded() const { + return (flags & PUSH_PROMISE_FLAG_PADDED) != 0 || padding_payload_len > 0; + } + ABSL_DEPRECATED("Use IsPadded() instead.") constexpr bool padded() const { + return IsPadded(); + } + constexpr bool operator==(const PushPromiseFrame&) const = default; +}; +static_assert(sizeof(PushPromiseFrame) == (sizeof(void*) == 8 ? 32 : 20)); + +struct QUICHE_EXPORT PingFrame { + uint64_t opaque_data = 0; + bool is_ack = false; + constexpr bool operator==(const PingFrame&) const = default; +}; +static_assert(sizeof(PingFrame) == 16); + +struct QUICHE_EXPORT GoAwayFrame { + SpdyStreamId last_good_stream_id = 0; + SpdyErrorCode error_code = ERROR_CODE_NO_ERROR; + absl::string_view debug_data; + constexpr bool operator==(const GoAwayFrame&) const = default; +}; +static_assert(sizeof(GoAwayFrame) == (sizeof(void*) == 8 ? 24 : 16)); + +struct QUICHE_EXPORT WindowUpdateFrame { + SpdyStreamId stream_id = 0; + uint32_t delta = 0; + constexpr bool operator==(const WindowUpdateFrame&) const = default; +}; +static_assert(sizeof(WindowUpdateFrame) == 8); + +struct QUICHE_EXPORT ContinuationFrame { + SpdyStreamId stream_id = 0; + uint8_t flags = 0; + absl::string_view hpack_block; + + constexpr bool HasEndHeaders() const { + return (flags & HEADERS_FLAG_END_HEADERS) != 0; + } + ABSL_DEPRECATED("Use HasEndHeaders() instead.") + constexpr bool end_headers() const { return HasEndHeaders(); } + constexpr bool operator==(const ContinuationFrame&) const = default; +}; +static_assert(sizeof(ContinuationFrame) == (sizeof(void*) == 8 ? 24 : 16)); + +struct QUICHE_EXPORT AltSvcFrame { + SpdyStreamId stream_id = 0; + absl::string_view origin; + absl::string_view value; + constexpr bool operator==(const AltSvcFrame&) const = default; +}; +static_assert(sizeof(AltSvcFrame) == (sizeof(void*) == 8 ? 40 : 20)); + +struct QUICHE_EXPORT PriorityUpdateFrame { + SpdyStreamId prioritized_stream_id = 0; + absl::string_view priority_field_value; + constexpr bool operator==(const PriorityUpdateFrame&) const = default; +}; +static_assert(sizeof(PriorityUpdateFrame) == (sizeof(void*) == 8 ? 24 : 12)); + +struct QUICHE_EXPORT AcceptChFrame { + static constexpr size_t kMaxInlineEntries = 1; + uint8_t num_entries = 0; + std::array<AcceptChEntryView, kMaxInlineEntries> entries{}; + constexpr bool operator==(const AcceptChFrame&) const = default; +}; +static_assert(sizeof(AcceptChFrame) == (sizeof(void*) == 8 ? 40 : 20)); + +struct QUICHE_EXPORT UnknownFrame { + SpdyStreamId stream_id = 0; + uint8_t type = 0; + uint8_t flags = 0; + absl::string_view payload; + constexpr bool operator==(const UnknownFrame&) const = default; +}; +static_assert(sizeof(UnknownFrame) == (sizeof(void*) == 8 ? 24 : 16)); + +// Compile-Time Type Traits +template <typename T> +struct FrameTraits; + +#define REGISTER_FRAME_TRAITS(Type, EnumVal, HasStreamId, HasFin, HasPadding, \ + IsFixed, FixedSize, ConsumesFC) \ + template <> \ + struct FrameTraits<Type> { \ + static constexpr SpdyFrameType frame_type = EnumVal; \ + static constexpr bool has_stream_id = HasStreamId; \ + static constexpr bool has_fin = HasFin; \ + static constexpr bool has_padding = HasPadding; \ + static constexpr bool is_fixed_size = IsFixed; \ + static constexpr size_t fixed_payload_size = FixedSize; \ + static constexpr bool consumes_flow_control = ConsumesFC; \ + }; + +REGISTER_FRAME_TRAITS(DataFrame, SpdyFrameType::DATA, true, true, true, false, + 0, true) +REGISTER_FRAME_TRAITS(HeadersFrame, SpdyFrameType::HEADERS, true, true, true, + false, 0, false) +REGISTER_FRAME_TRAITS(PriorityFrame, SpdyFrameType::PRIORITY, true, false, + false, true, 5, false) +REGISTER_FRAME_TRAITS(RstStreamFrame, SpdyFrameType::RST_STREAM, true, false, + false, true, 4, false) +REGISTER_FRAME_TRAITS(SettingsFrame, SpdyFrameType::SETTINGS, false, false, + false, false, 0, false) +REGISTER_FRAME_TRAITS(PushPromiseFrame, SpdyFrameType::PUSH_PROMISE, true, + false, true, false, 0, false) +REGISTER_FRAME_TRAITS(PingFrame, SpdyFrameType::PING, false, false, false, true, + 8, false) +REGISTER_FRAME_TRAITS(GoAwayFrame, SpdyFrameType::GOAWAY, false, false, false, + false, 0, false) +REGISTER_FRAME_TRAITS(WindowUpdateFrame, SpdyFrameType::WINDOW_UPDATE, true, + false, false, true, 4, false) +REGISTER_FRAME_TRAITS(ContinuationFrame, SpdyFrameType::CONTINUATION, true, + false, false, false, 0, false) +REGISTER_FRAME_TRAITS(AltSvcFrame, SpdyFrameType::ALTSVC, true, false, false, + false, 0, false) +REGISTER_FRAME_TRAITS(PriorityUpdateFrame, SpdyFrameType::PRIORITY_UPDATE, + false, false, false, false, 0, false) +REGISTER_FRAME_TRAITS(AcceptChFrame, SpdyFrameType::ACCEPT_CH, false, false, + false, false, 0, false) +REGISTER_FRAME_TRAITS(UnknownFrame, SpdyFrameType::DATA, true, false, false, + false, 0, false) + +#undef REGISTER_FRAME_TRAITS + +template <typename T> +inline constexpr SpdyFrameType frame_type_v = FrameTraits<T>::frame_type; + +template <typename T> +inline constexpr bool is_fixed_size_v = FrameTraits<T>::is_fixed_size; + +template <typename T> +inline constexpr bool has_stream_id_v = FrameTraits<T>::has_stream_id; + +template <typename T> +inline constexpr bool has_fin_v = FrameTraits<T>::has_fin; + +template <typename T> +inline constexpr bool has_padding_v = FrameTraits<T>::has_padding; + +template <typename T> +inline constexpr bool consumes_flow_control_v = + FrameTraits<T>::consumes_flow_control; + +template <typename T> +concept Http2FrameConcept = requires { + { FrameTraits<T>::frame_type } -> std::convertible_to<SpdyFrameType>; + { FrameTraits<T>::is_fixed_size } -> std::convertible_to<bool>; + { FrameTraits<T>::has_stream_id } -> std::convertible_to<bool>; + { FrameTraits<T>::has_fin } -> std::convertible_to<bool>; + { FrameTraits<T>::has_padding } -> std::convertible_to<bool>; + { FrameTraits<T>::consumes_flow_control } -> std::convertible_to<bool>; +}; + +// Generic Size Calculation +template <Http2FrameConcept T> +constexpr size_t FrameSize(const T& frame) { + if constexpr (is_fixed_size_v<T>) { + return kFrameHeaderSize + FrameTraits<T>::fixed_payload_size; + } else if constexpr (std::is_same_v<T, DataFrame>) { + return kDataFrameMinimumSize + frame.data.size() + + (frame.padded() ? (1 + frame.padding_payload_len) : 0); + } else if constexpr (std::is_same_v<T, HeadersFrame>) { + size_t size = kHeadersFrameMinimumSize + frame.hpack_block.size(); + if (frame.padded()) { + size += 1 + frame.padding_payload_len; + } + if (frame.has_priority) { + size += 5; + } + return size; + } else if constexpr (std::is_same_v<T, SettingsFrame>) { + if (frame.is_ack) { + return kSettingsFrameMinimumSize; + } + return kSettingsFrameMinimumSize + + (frame.values.size() * kSettingsOneSettingSize); + } else if constexpr (std::is_same_v<T, PushPromiseFrame>) { + size_t size = kPushPromiseFrameMinimumSize + frame.hpack_block.size(); + if (frame.padded()) { + size += 1 + frame.padding_payload_len; + } + return size; + } else if constexpr (std::is_same_v<T, GoAwayFrame>) { + return kGoawayFrameMinimumSize + frame.debug_data.size(); + } else if constexpr (std::is_same_v<T, ContinuationFrame>) { + return kContinuationFrameMinimumSize + frame.hpack_block.size(); + } else if constexpr (std::is_same_v<T, AltSvcFrame>) { + return kGetAltSvcFrameMinimumSize + frame.origin.size() + + frame.value.size(); + } else if constexpr (std::is_same_v<T, PriorityUpdateFrame>) { + return kPriorityUpdateFrameMinimumSize + frame.priority_field_value.size(); + } else if constexpr (std::is_same_v<T, AcceptChFrame>) { + size_t total_size = kAcceptChFrameMinimumSize; + for (size_t i = 0; i < frame.num_entries && i < frame.entries.size(); ++i) { + total_size += frame.entries[i].origin.size() + + frame.entries[i].value.size() + + kAcceptChFramePerEntryOverhead; + } + return total_size; + } else if constexpr (std::is_same_v<T, UnknownFrame>) { + return kFrameHeaderSize + frame.payload.size(); + } else { + return kFrameHeaderSize; + } +} + +// Forward declares SpdyFrameBuilder for SerializeFrame. +class SpdyFrameBuilder; + +template <Http2FrameConcept T> +bool SerializeFrame(const T& frame, SpdyFrameBuilder& builder); + +// Unified variant. +using SpdyFrame = + std::variant<DataFrame, HeadersFrame, PriorityFrame, RstStreamFrame, + SettingsFrame, PushPromiseFrame, PingFrame, GoAwayFrame, + WindowUpdateFrame, ContinuationFrame, AltSvcFrame, + PriorityUpdateFrame, AcceptChFrame, UnknownFrame>; + +static_assert(sizeof(SpdyFrame) <= 64, + "SpdyFrame must fit within a 64-byte cache line"); + +inline SpdyStreamId GetFrameStreamId(const SpdyFrame& frame) { + return std::visit( + [](const auto& f) -> SpdyStreamId { + using T = std::decay_t<decltype(f)>; + if constexpr (has_stream_id_v<T>) { + return f.stream_id; + } + return 0; + }, + frame); +} + +inline SpdyFrameType GetFrameType(const SpdyFrame& frame) { + return std::visit( + [](const auto& f) -> SpdyFrameType { + using T = std::decay_t<decltype(f)>; + if constexpr (std::is_same_v<T, UnknownFrame>) { + return static_cast<SpdyFrameType>(f.type); + } + return frame_type_v<T>; + }, + frame); +} + +inline size_t GetFrameSize(const SpdyFrame& frame) { + return std::visit([](const auto& f) { return FrameSize(f); }, frame); +} + +bool SerializeSpdyFrame(const SpdyFrame& frame, SpdyFrameBuilder& builder); + class SpdyFrameVisitor; // Intermediate representation for HTTP2 frames.
diff --git a/quiche/http2/core/spdy_protocol_test.cc b/quiche/http2/core/spdy_protocol_test.cc index 1adef29..82100e3 100644 --- a/quiche/http2/core/spdy_protocol_test.cc +++ b/quiche/http2/core/spdy_protocol_test.cc
@@ -6,9 +6,12 @@ #include <iostream> #include <string> +#include <type_traits> #include <utility> #include "absl/strings/string_view.h" +#include "quiche/http2/core/spdy_frame_builder.h" +#include "quiche/http2/core/spdy_framer.h" #include "quiche/common/platform/api/quiche_expect_bug.h" #include "quiche/common/platform/api/quiche_test.h" @@ -282,5 +285,408 @@ EXPECT_EQ(frame.end(), frame.data() + frame.size()); } +// ============================================================================= +// Modern HTTP/2 Frame Representations Tests +// ============================================================================= + +TEST(ModernFrameTest, StandardLayoutAndTriviallyCopyable) { + static_assert(std::is_standard_layout_v<PriorityFields>); + static_assert(std::is_trivially_copyable_v<PriorityFields>); + + static_assert(std::is_standard_layout_v<SettingParameter>); + static_assert(std::is_trivially_copyable_v<SettingParameter>); + + static_assert(std::is_standard_layout_v<AcceptChEntryView>); + static_assert(std::is_trivially_copyable_v<AcceptChEntryView>); + + static_assert(std::is_standard_layout_v<DataFrame>); + static_assert(std::is_trivially_copyable_v<DataFrame>); + + static_assert(std::is_standard_layout_v<HeadersFrame>); + static_assert(std::is_trivially_copyable_v<HeadersFrame>); + + static_assert(std::is_standard_layout_v<PriorityFrame>); + static_assert(std::is_trivially_copyable_v<PriorityFrame>); + + static_assert(std::is_standard_layout_v<RstStreamFrame>); + static_assert(std::is_trivially_copyable_v<RstStreamFrame>); + + static_assert(std::is_standard_layout_v<SettingsFrame>); + + static_assert(std::is_standard_layout_v<PushPromiseFrame>); + static_assert(std::is_trivially_copyable_v<PushPromiseFrame>); + + static_assert(std::is_standard_layout_v<PingFrame>); + static_assert(std::is_trivially_copyable_v<PingFrame>); + + static_assert(std::is_standard_layout_v<GoAwayFrame>); + static_assert(std::is_trivially_copyable_v<GoAwayFrame>); + + static_assert(std::is_standard_layout_v<WindowUpdateFrame>); + static_assert(std::is_trivially_copyable_v<WindowUpdateFrame>); + + static_assert(std::is_standard_layout_v<ContinuationFrame>); + static_assert(std::is_trivially_copyable_v<ContinuationFrame>); + + static_assert(std::is_standard_layout_v<AltSvcFrame>); + static_assert(std::is_trivially_copyable_v<AltSvcFrame>); + + static_assert(std::is_standard_layout_v<PriorityUpdateFrame>); + static_assert(std::is_trivially_copyable_v<PriorityUpdateFrame>); + + static_assert(std::is_standard_layout_v<AcceptChFrame>); + static_assert(std::is_trivially_copyable_v<AcceptChFrame>); + + static_assert(std::is_standard_layout_v<UnknownFrame>); + static_assert(std::is_trivially_copyable_v<UnknownFrame>); + + static_assert(sizeof(SpdyFrame) <= 64, + "SpdyFrame must fit in a 64-byte cache line"); +} + +TEST(ModernFrameTest, Http2FrameConcept) { + static_assert(Http2FrameConcept<DataFrame>); + static_assert(Http2FrameConcept<HeadersFrame>); + static_assert(Http2FrameConcept<PriorityFrame>); + static_assert(Http2FrameConcept<RstStreamFrame>); + static_assert(Http2FrameConcept<SettingsFrame>); + static_assert(Http2FrameConcept<PushPromiseFrame>); + static_assert(Http2FrameConcept<PingFrame>); + static_assert(Http2FrameConcept<GoAwayFrame>); + static_assert(Http2FrameConcept<WindowUpdateFrame>); + static_assert(Http2FrameConcept<ContinuationFrame>); + static_assert(Http2FrameConcept<AltSvcFrame>); + static_assert(Http2FrameConcept<PriorityUpdateFrame>); + static_assert(Http2FrameConcept<AcceptChFrame>); + static_assert(Http2FrameConcept<UnknownFrame>); + + // Non-frame types should not satisfy the concept. + static_assert(!Http2FrameConcept<int>); + static_assert(!Http2FrameConcept<std::string>); + static_assert(!Http2FrameConcept<PriorityFields>); + static_assert(!Http2FrameConcept<SpdyDataIR>); +} + +TEST(ModernFrameTest, FrameTraits) { + EXPECT_EQ(SpdyFrameType::DATA, frame_type_v<DataFrame>); + EXPECT_EQ(SpdyFrameType::HEADERS, frame_type_v<HeadersFrame>); + EXPECT_EQ(SpdyFrameType::PRIORITY, frame_type_v<PriorityFrame>); + EXPECT_EQ(SpdyFrameType::RST_STREAM, frame_type_v<RstStreamFrame>); + EXPECT_EQ(SpdyFrameType::SETTINGS, frame_type_v<SettingsFrame>); + EXPECT_EQ(SpdyFrameType::PUSH_PROMISE, frame_type_v<PushPromiseFrame>); + EXPECT_EQ(SpdyFrameType::PING, frame_type_v<PingFrame>); + EXPECT_EQ(SpdyFrameType::GOAWAY, frame_type_v<GoAwayFrame>); + EXPECT_EQ(SpdyFrameType::WINDOW_UPDATE, frame_type_v<WindowUpdateFrame>); + EXPECT_EQ(SpdyFrameType::CONTINUATION, frame_type_v<ContinuationFrame>); + EXPECT_EQ(SpdyFrameType::ALTSVC, frame_type_v<AltSvcFrame>); + EXPECT_EQ(SpdyFrameType::PRIORITY_UPDATE, frame_type_v<PriorityUpdateFrame>); + EXPECT_EQ(SpdyFrameType::ACCEPT_CH, frame_type_v<AcceptChFrame>); + + // Fixed size checks + EXPECT_TRUE(is_fixed_size_v<PriorityFrame>); + EXPECT_TRUE(is_fixed_size_v<RstStreamFrame>); + EXPECT_TRUE(is_fixed_size_v<PingFrame>); + EXPECT_TRUE(is_fixed_size_v<WindowUpdateFrame>); + EXPECT_FALSE(is_fixed_size_v<DataFrame>); + EXPECT_FALSE(is_fixed_size_v<HeadersFrame>); + EXPECT_FALSE(is_fixed_size_v<SettingsFrame>); + + // Stream ID presence + EXPECT_TRUE(has_stream_id_v<DataFrame>); + EXPECT_TRUE(has_stream_id_v<HeadersFrame>); + EXPECT_TRUE(has_stream_id_v<PriorityFrame>); + EXPECT_TRUE(has_stream_id_v<RstStreamFrame>); + EXPECT_TRUE(has_stream_id_v<PushPromiseFrame>); + EXPECT_TRUE(has_stream_id_v<WindowUpdateFrame>); + EXPECT_FALSE(has_stream_id_v<SettingsFrame>); + EXPECT_FALSE(has_stream_id_v<PingFrame>); + EXPECT_FALSE(has_stream_id_v<GoAwayFrame>); + + // Fin flag presence + EXPECT_TRUE(has_fin_v<DataFrame>); + EXPECT_TRUE(has_fin_v<HeadersFrame>); + EXPECT_FALSE(has_fin_v<RstStreamFrame>); + EXPECT_FALSE(has_fin_v<SettingsFrame>); + + // Padding presence + EXPECT_TRUE(has_padding_v<DataFrame>); + EXPECT_TRUE(has_padding_v<HeadersFrame>); + EXPECT_TRUE(has_padding_v<PushPromiseFrame>); + EXPECT_FALSE(has_padding_v<PingFrame>); + + // Flow control consumption + EXPECT_TRUE(consumes_flow_control_v<DataFrame>); + EXPECT_FALSE(consumes_flow_control_v<HeadersFrame>); + EXPECT_FALSE(consumes_flow_control_v<SettingsFrame>); +} + +TEST(ModernFrameTest, FrameSizeCalculation) { + DataFrame data{.stream_id = 1, .data = "hello"}; + EXPECT_EQ(14u, FrameSize(data)); + data.flags = DATA_FLAG_PADDED; + data.padding_payload_len = 3; + EXPECT_EQ(18u, FrameSize(data)); + + HeadersFrame headers{.stream_id = 1, .hpack_block = "0123456789"}; + EXPECT_EQ(19u, FrameSize(headers)); + headers.has_priority = true; + EXPECT_EQ(24u, FrameSize(headers)); + + PriorityFrame priority{.stream_id = 1}; + EXPECT_EQ(14u, FrameSize(priority)); + + RstStreamFrame rst{.stream_id = 1}; + EXPECT_EQ(13u, FrameSize(rst)); + + SettingsFrame settings_ack{.is_ack = true, .values = {}}; + EXPECT_EQ(9u, FrameSize(settings_ack)); + SettingsFrame settings_data{ + .is_ack = false, + .values = {{SETTINGS_HEADER_TABLE_SIZE, 4096}, + {SETTINGS_MAX_CONCURRENT_STREAMS, 100}}}; + EXPECT_EQ(21u, FrameSize(settings_data)); + + PushPromiseFrame push{ + .stream_id = 1, .promised_stream_id = 2, .hpack_block = "12345678"}; + EXPECT_EQ(21u, FrameSize(push)); + + PingFrame ping{}; + EXPECT_EQ(17u, FrameSize(ping)); + + GoAwayFrame goaway{.debug_data = "abcd"}; + EXPECT_EQ(21u, FrameSize(goaway)); + + WindowUpdateFrame win{.stream_id = 1, .delta = 100}; + EXPECT_EQ(13u, FrameSize(win)); + + ContinuationFrame cont{.stream_id = 1, .hpack_block = "abcdef"}; + EXPECT_EQ(15u, FrameSize(cont)); + + AltSvcFrame altsvc{.stream_id = 1, .origin = "foo", .value = "h2=\":443\""}; + EXPECT_EQ(9u + 2u + 3u + 9u, FrameSize(altsvc)); + + PriorityUpdateFrame prio_up{.prioritized_stream_id = 3, + .priority_field_value = "u=3,i"}; + EXPECT_EQ(9u + 4u + 5u, FrameSize(prio_up)); + + AcceptChFrame accept_ch{.num_entries = 1, + .entries = {{{.origin = "foo", .value = "bar"}}}}; + EXPECT_EQ(9u + 4u + 3u + 3u, FrameSize(accept_ch)); + + UnknownFrame unknown{.type = 0x99, .payload = "payload"}; + EXPECT_EQ(16u, FrameSize(unknown)); +} + +TEST(ModernFrameTest, SerializeDataFrame) { + SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION); + + DataFrame df{.stream_id = 3, .flags = DATA_FLAG_FIN, .data = "hello world"}; + SpdyFrameBuilder builder(FrameSize(df)); + EXPECT_TRUE(SerializeFrame(df, builder)); + SpdySerializedFrame serialized = builder.take(); + + SpdyDataIR data_ir(3, "hello world"); + data_ir.set_fin(true); + SpdySerializedFrame expected = framer.SerializeData(data_ir); + + EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized)); +} + +TEST(ModernFrameTest, SerializePriorityFrame) { + SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION); + + PriorityFrame pf{ + .stream_id = 5, + .priority = {.parent_stream_id = 1, .weight = 32, .exclusive = true}}; + SpdyFrameBuilder builder(FrameSize(pf)); + EXPECT_TRUE(SerializeFrame(pf, builder)); + SpdySerializedFrame serialized = builder.take(); + + SpdyPriorityIR prio_ir(5, 1, 32, true); + SpdySerializedFrame expected = framer.SerializePriority(prio_ir); + + EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized)); +} + +TEST(ModernFrameTest, SerializeRstStreamFrame) { + SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION); + + // 3. RST_STREAM frame + RstStreamFrame rf{.stream_id = 7, .error_code = ERROR_CODE_CANCEL}; + SpdyFrameBuilder builder(FrameSize(rf)); + EXPECT_TRUE(SerializeFrame(rf, builder)); + SpdySerializedFrame serialized = builder.take(); + + SpdyRstStreamIR rst_ir(7, ERROR_CODE_CANCEL); + SpdySerializedFrame expected = framer.SerializeRstStream(rst_ir); + + EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized)); +} + +TEST(ModernFrameTest, SerializeSettingsFrame) { + SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION); + + // SETTINGS ack. + { + SettingsFrame sf_ack{.is_ack = true, .values = {}}; + SpdyFrameBuilder builder(FrameSize(sf_ack)); + EXPECT_TRUE(SerializeFrame(sf_ack, builder)); + SpdySerializedFrame serialized = builder.take(); + + SpdySettingsIR settings_ack_ir; + settings_ack_ir.set_is_ack(true); + SpdySerializedFrame expected = framer.SerializeSettings(settings_ack_ir); + + EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized)); + } + // SETTINGS with values. + { + SettingsFrame sf_vals{.is_ack = false, + .values = {{SETTINGS_HEADER_TABLE_SIZE, 4096}, + {SETTINGS_MAX_CONCURRENT_STREAMS, 100}}}; + SpdyFrameBuilder builder(FrameSize(sf_vals)); + EXPECT_TRUE(SerializeFrame(sf_vals, builder)); + SpdySerializedFrame serialized = builder.take(); + + SpdySettingsIR settings_ir; + settings_ir.AddSetting(SETTINGS_HEADER_TABLE_SIZE, 4096); + settings_ir.AddSetting(SETTINGS_MAX_CONCURRENT_STREAMS, 100); + SpdySerializedFrame expected = framer.SerializeSettings(settings_ir); + + EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized)); + } +} + +TEST(ModernFrameTest, SerializePingFrame) { + SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION); + + PingFrame ping{.opaque_data = 0x0102030405060708ULL, .is_ack = true}; + SpdyFrameBuilder builder(FrameSize(ping)); + EXPECT_TRUE(SerializeFrame(ping, builder)); + SpdySerializedFrame serialized = builder.take(); + + SpdyPingIR ping_ir(0x0102030405060708ULL); + ping_ir.set_is_ack(true); + SpdySerializedFrame expected = framer.SerializePing(ping_ir); + + EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized)); +} + +TEST(ModernFrameTest, SerializeGoAwayFrame) { + SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION); + + GoAwayFrame goaway{.last_good_stream_id = 9, + .error_code = ERROR_CODE_PROTOCOL_ERROR, + .debug_data = "protocol error occurred"}; + SpdyFrameBuilder builder(FrameSize(goaway)); + EXPECT_TRUE(SerializeFrame(goaway, builder)); + SpdySerializedFrame serialized = builder.take(); + + SpdyGoAwayIR goaway_ir(9, ERROR_CODE_PROTOCOL_ERROR, + "protocol error occurred"); + SpdySerializedFrame expected = framer.SerializeGoAway(goaway_ir); + + EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized)); +} + +TEST(ModernFrameTest, SerializeWindowUpdateFrame) { + SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION); + + WindowUpdateFrame win{.stream_id = 11, .delta = 65535}; + SpdyFrameBuilder builder(FrameSize(win)); + EXPECT_TRUE(SerializeFrame(win, builder)); + SpdySerializedFrame serialized = builder.take(); + + SpdyWindowUpdateIR win_ir(11, 65535); + SpdySerializedFrame expected = framer.SerializeWindowUpdate(win_ir); + + EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized)); +} + +TEST(ModernFrameTest, SerializeContinuationFrame) { + SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION); + + ContinuationFrame cont{.stream_id = 13, + .flags = HEADERS_FLAG_END_HEADERS, + .hpack_block = "continuation_block"}; + SpdyFrameBuilder builder(FrameSize(cont)); + EXPECT_TRUE(SerializeFrame(cont, builder)); + SpdySerializedFrame serialized = builder.take(); + + SpdyContinuationIR cont_ir(13); + cont_ir.set_end_headers(true); + cont_ir.take_encoding("continuation_block"); + SpdySerializedFrame expected = framer.SerializeContinuation(cont_ir); + + EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized)); +} + +TEST(ModernFrameTest, SerializePriorityUpdateFrame) { + SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION); + + PriorityUpdateFrame prio_up{.prioritized_stream_id = 5, + .priority_field_value = "u=1,i"}; + SpdyFrameBuilder builder(FrameSize(prio_up)); + EXPECT_TRUE(SerializeFrame(prio_up, builder)); + SpdySerializedFrame serialized = builder.take(); + + SpdyPriorityUpdateIR prio_up_ir(0, 5, "u=1,i"); + SpdySerializedFrame expected = framer.SerializePriorityUpdate(prio_up_ir); + + EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized)); +} + +TEST(ModernFrameTest, SerializeAcceptChFrame) { + SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION); + + AcceptChFrame accept_ch{ + .num_entries = 1, + .entries = {{{.origin = "example.com", .value = "sec-ch-ua"}}}}; + SpdyFrameBuilder builder(FrameSize(accept_ch)); + EXPECT_TRUE(SerializeFrame(accept_ch, builder)); + SpdySerializedFrame serialized = builder.take(); + + SpdyAcceptChIR accept_ch_ir({{"example.com", "sec-ch-ua"}}); + SpdySerializedFrame expected = framer.SerializeAcceptCh(accept_ch_ir); + + EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized)); +} + +TEST(ModernFrameTest, SerializeUnknownFrame) { + SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION); + + // 11. UNKNOWN frame + { + UnknownFrame unknown{.stream_id = 1, + .type = 0xfe, + .flags = 0x05, + .payload = "custom_payload"}; + SpdyFrameBuilder builder(FrameSize(unknown)); + EXPECT_TRUE(SerializeFrame(unknown, builder)); + SpdySerializedFrame serialized = builder.take(); + + SpdyUnknownIR unknown_ir(1, 0xfe, 0x05, "custom_payload"); + SpdySerializedFrame expected = framer.SerializeUnknown(unknown_ir); + + EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized)); + } +} + +TEST(ModernFrameTest, SpdyFrameVariant) { + SpdyFrame frame = DataFrame{.stream_id = 42, .data = "payload"}; + EXPECT_EQ(42u, GetFrameStreamId(frame)); + EXPECT_EQ(SpdyFrameType::DATA, GetFrameType(frame)); + EXPECT_EQ(16u, GetFrameSize(frame)); + + frame = PingFrame{.opaque_data = 123}; + EXPECT_EQ(0u, GetFrameStreamId(frame)); + EXPECT_EQ(SpdyFrameType::PING, GetFrameType(frame)); + EXPECT_EQ(17u, GetFrameSize(frame)); + + SpdyFrameBuilder builder(GetFrameSize(frame)); + EXPECT_TRUE(SerializeSpdyFrame(frame, builder)); + EXPECT_EQ(17u, builder.length()); +} + } // namespace test } // namespace spdy