Implement a new API for serializing data into QUIC wire format.

Also rewrite the Capsule serialization logic to use the new API.

I started working on WebTransport over HTTP/2 capsules recently, and realized that the existing code requires too much boilerplate per capsule to be sustainable.  This CL is an effort to reduce the cost of adding individual capsules.

PiperOrigin-RevId: 504643109
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 556b344..4d0a1fd 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -57,10 +57,12 @@
     "common/quiche_mem_slice_storage.h",
     "common/quiche_protocol_flags_list.h",
     "common/quiche_random.h",
+    "common/quiche_status_utils.h",
     "common/quiche_stream.h",
     "common/quiche_text_utils.h",
     "common/simple_buffer_allocator.h",
     "common/structured_headers.h",
+    "common/wire_serialization.h",
     "http2/adapter/data_source.h",
     "http2/adapter/event_forwarder.h",
     "http2/adapter/header_validator.h",
@@ -1050,6 +1052,7 @@
     "common/structured_headers_generated_test.cc",
     "common/structured_headers_test.cc",
     "common/test_tools/quiche_test_utils_test.cc",
+    "common/wire_serialization_test.cc",
     "http2/adapter/event_forwarder_test.cc",
     "http2/adapter/header_validator_test.cc",
     "http2/adapter/noop_header_validator_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index 22a646d..445844a 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -57,10 +57,12 @@
     "src/quiche/common/quiche_mem_slice_storage.h",
     "src/quiche/common/quiche_protocol_flags_list.h",
     "src/quiche/common/quiche_random.h",
+    "src/quiche/common/quiche_status_utils.h",
     "src/quiche/common/quiche_stream.h",
     "src/quiche/common/quiche_text_utils.h",
     "src/quiche/common/simple_buffer_allocator.h",
     "src/quiche/common/structured_headers.h",
+    "src/quiche/common/wire_serialization.h",
     "src/quiche/http2/adapter/data_source.h",
     "src/quiche/http2/adapter/event_forwarder.h",
     "src/quiche/http2/adapter/header_validator.h",
@@ -1050,6 +1052,7 @@
     "src/quiche/common/structured_headers_generated_test.cc",
     "src/quiche/common/structured_headers_test.cc",
     "src/quiche/common/test_tools/quiche_test_utils_test.cc",
+    "src/quiche/common/wire_serialization_test.cc",
     "src/quiche/http2/adapter/event_forwarder_test.cc",
     "src/quiche/http2/adapter/header_validator_test.cc",
     "src/quiche/http2/adapter/noop_header_validator_test.cc",
diff --git a/build/source_list.json b/build/source_list.json
index 495f110..d03d722 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -56,10 +56,12 @@
     "quiche/common/quiche_mem_slice_storage.h",
     "quiche/common/quiche_protocol_flags_list.h",
     "quiche/common/quiche_random.h",
+    "quiche/common/quiche_status_utils.h",
     "quiche/common/quiche_stream.h",
     "quiche/common/quiche_text_utils.h",
     "quiche/common/simple_buffer_allocator.h",
     "quiche/common/structured_headers.h",
+    "quiche/common/wire_serialization.h",
     "quiche/http2/adapter/data_source.h",
     "quiche/http2/adapter/event_forwarder.h",
     "quiche/http2/adapter/header_validator.h",
@@ -1049,6 +1051,7 @@
     "quiche/common/structured_headers_generated_test.cc",
     "quiche/common/structured_headers_test.cc",
     "quiche/common/test_tools/quiche_test_utils_test.cc",
+    "quiche/common/wire_serialization_test.cc",
     "quiche/http2/adapter/event_forwarder_test.cc",
     "quiche/http2/adapter/header_validator_test.cc",
     "quiche/http2/adapter/noop_header_validator_test.cc",
diff --git a/quiche/common/capsule.cc b/quiche/common/capsule.cc
index 0237a7f..fde1cda 100644
--- a/quiche/common/capsule.cc
+++ b/quiche/common/capsule.cc
@@ -6,14 +6,19 @@
 
 #include <type_traits>
 
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
 #include "absl/strings/escaping.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
+#include "absl/types/span.h"
 #include "quiche/common/platform/api/quiche_bug_tracker.h"
 #include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/quiche_buffer_allocator.h"
 #include "quiche/common/quiche_data_reader.h"
 #include "quiche/common/quiche_data_writer.h"
 #include "quiche/common/quiche_ip_address.h"
+#include "quiche/common/wire_serialization.h"
 #include "quiche/web_transport/web_transport.h"
 
 namespace quiche {
@@ -329,220 +334,128 @@
   QUICHE_DCHECK_NE(visitor_, nullptr);
 }
 
-quiche::QuicheBuffer SerializeCapsule(
+// Serialization logic for quiche::PrefixWithId.
+class WirePrefixWithId {
+ public:
+  using DataType = PrefixWithId;
+
+  WirePrefixWithId(const PrefixWithId& prefix) : prefix_(prefix) {}
+
+  size_t GetLengthOnWire() {
+    return ComputeLengthOnWire(
+        WireVarInt62(prefix_.request_id),
+        WireUint8(prefix_.ip_prefix.address().IsIPv4() ? 4 : 6),
+        WireBytes(prefix_.ip_prefix.address().ToPackedString()),
+        WireUint8(prefix_.ip_prefix.prefix_length()));
+  }
+
+  absl::Status SerializeIntoWriter(QuicheDataWriter& writer) {
+    return AppendToStatus(
+        quiche::SerializeIntoWriter(
+            writer, WireVarInt62(prefix_.request_id),
+            WireUint8(prefix_.ip_prefix.address().IsIPv4() ? 4 : 6),
+            WireBytes(prefix_.ip_prefix.address().ToPackedString()),
+            WireUint8(prefix_.ip_prefix.prefix_length())),
+        " while serializing a PrefixWithId");
+  }
+
+ private:
+  const PrefixWithId& prefix_;
+};
+
+// Serialization logic for quiche::IpAddressRange.
+class WireIpAddressRange {
+ public:
+  using DataType = IpAddressRange;
+
+  explicit WireIpAddressRange(const IpAddressRange& range) : range_(range) {}
+
+  size_t GetLengthOnWire() {
+    return ComputeLengthOnWire(
+        WireUint8(range_.start_ip_address.IsIPv4() ? 4 : 6),
+        WireBytes(range_.start_ip_address.ToPackedString()),
+        WireBytes(range_.end_ip_address.ToPackedString()),
+        WireUint8(range_.ip_protocol));
+  }
+
+  absl::Status SerializeIntoWriter(QuicheDataWriter& writer) {
+    return AppendToStatus(
+        ::quiche::SerializeIntoWriter(
+            writer, WireUint8(range_.start_ip_address.IsIPv4() ? 4 : 6),
+            WireBytes(range_.start_ip_address.ToPackedString()),
+            WireBytes(range_.end_ip_address.ToPackedString()),
+            WireUint8(range_.ip_protocol)),
+        " while serializing an IpAddressRange");
+  }
+
+ private:
+  const IpAddressRange& range_;
+};
+
+template <typename... T>
+absl::StatusOr<quiche::QuicheBuffer> SerializeCapsuleFields(
+    CapsuleType type, QuicheBufferAllocator* allocator, T... fields) {
+  size_t capsule_payload_size = ComputeLengthOnWire(fields...);
+  return SerializeIntoBuffer(allocator, WireVarInt62(type),
+                             WireVarInt62(capsule_payload_size), fields...);
+}
+
+absl::StatusOr<quiche::QuicheBuffer> SerializeCapsuleWithStatus(
     const Capsule& capsule, quiche::QuicheBufferAllocator* allocator) {
-  size_t capsule_type_length = QuicheDataWriter::GetVarInt62Len(
-      static_cast<uint64_t>(capsule.capsule_type()));
-  size_t capsule_data_length;
   switch (capsule.capsule_type()) {
     case CapsuleType::DATAGRAM:
-      capsule_data_length =
-          capsule.datagram_capsule().http_datagram_payload.length();
-      break;
+      return SerializeCapsuleFields(
+          capsule.capsule_type(), allocator,
+          WireBytes(capsule.datagram_capsule().http_datagram_payload));
     case CapsuleType::LEGACY_DATAGRAM:
-      capsule_data_length =
-          capsule.legacy_datagram_capsule().http_datagram_payload.length();
-      break;
+      return SerializeCapsuleFields(
+          capsule.capsule_type(), allocator,
+          WireBytes(capsule.legacy_datagram_capsule().http_datagram_payload));
     case CapsuleType::LEGACY_DATAGRAM_WITHOUT_CONTEXT:
-      capsule_data_length = capsule.legacy_datagram_without_context_capsule()
-                                .http_datagram_payload.length();
+      return SerializeCapsuleFields(
+          capsule.capsule_type(), allocator,
+          WireBytes(capsule.legacy_datagram_without_context_capsule()
+                        .http_datagram_payload));
       break;
     case CapsuleType::CLOSE_WEBTRANSPORT_SESSION:
-      capsule_data_length =
-          sizeof(webtransport::SessionErrorCode) +
-          capsule.close_web_transport_session_capsule().error_message.size();
+      return SerializeCapsuleFields(
+          capsule.capsule_type(), allocator,
+          WireUint32(capsule.close_web_transport_session_capsule().error_code),
+          WireBytes(
+              capsule.close_web_transport_session_capsule().error_message));
       break;
     case CapsuleType::ADDRESS_REQUEST:
-      capsule_data_length = 0;
-      for (auto requested_address :
-           capsule.address_request_capsule().requested_addresses) {
-        capsule_data_length +=
-            QuicheDataWriter::GetVarInt62Len(requested_address.request_id) + 1 +
-            (requested_address.ip_prefix.address().IsIPv4()
-                 ? QuicheIpAddress::kIPv4AddressSize
-                 : QuicheIpAddress::kIPv6AddressSize) +
-            1;
-      }
-      break;
+      return SerializeCapsuleFields(
+          capsule.capsule_type(), allocator,
+          WireSpan<WirePrefixWithId>(absl::MakeConstSpan(
+              capsule.address_request_capsule().requested_addresses)));
     case CapsuleType::ADDRESS_ASSIGN:
-      capsule_data_length = 0;
-      for (auto assigned_address :
-           capsule.address_assign_capsule().assigned_addresses) {
-        capsule_data_length +=
-            QuicheDataWriter::GetVarInt62Len(assigned_address.request_id) + 1 +
-            (assigned_address.ip_prefix.address().IsIPv4()
-                 ? QuicheIpAddress::kIPv4AddressSize
-                 : QuicheIpAddress::kIPv6AddressSize) +
-            1;
-      }
-      break;
+      return SerializeCapsuleFields(
+          capsule.capsule_type(), allocator,
+          WireSpan<WirePrefixWithId>(absl::MakeConstSpan(
+              capsule.address_assign_capsule().assigned_addresses)));
     case CapsuleType::ROUTE_ADVERTISEMENT:
-      capsule_data_length = 0;
-      for (auto ip_address_range :
-           capsule.route_advertisement_capsule().ip_address_ranges) {
-        capsule_data_length += 1 +
-                               (ip_address_range.start_ip_address.IsIPv4()
-                                    ? QuicheIpAddress::kIPv4AddressSize
-                                    : QuicheIpAddress::kIPv6AddressSize) *
-                                   2 +
-                               1;
-      }
-      break;
+      return SerializeCapsuleFields(
+          capsule.capsule_type(), allocator,
+          WireSpan<WireIpAddressRange>(absl::MakeConstSpan(
+              capsule.route_advertisement_capsule().ip_address_ranges)));
     default:
-      capsule_data_length = capsule.unknown_capsule_data().length();
-      break;
+      return SerializeCapsuleFields(capsule.capsule_type(), allocator,
+                                    WireBytes(capsule.unknown_capsule_data()));
   }
-  size_t capsule_length_length =
-      QuicheDataWriter::GetVarInt62Len(capsule_data_length);
-  size_t total_capsule_length =
-      capsule_type_length + capsule_length_length + capsule_data_length;
-  quiche::QuicheBuffer buffer(allocator, total_capsule_length);
-  QuicheDataWriter writer(buffer.size(), buffer.data());
-  if (!writer.WriteVarInt62(static_cast<uint64_t>(capsule.capsule_type()))) {
-    QUICHE_BUG(capsule type write fail) << "Failed to write CAPSULE type";
-    return {};
+}
+
+QuicheBuffer SerializeCapsule(const Capsule& capsule,
+                              quiche::QuicheBufferAllocator* allocator) {
+  absl::StatusOr<QuicheBuffer> serialized =
+      SerializeCapsuleWithStatus(capsule, allocator);
+  if (!serialized.ok()) {
+    QUICHE_BUG(capsule_serialization_failed)
+        << "Failed to serialize the following capsule:\n"
+        << capsule << "Serialization error: " << serialized.status();
+    return QuicheBuffer();
   }
-  if (!writer.WriteVarInt62(capsule_data_length)) {
-    QUICHE_BUG(capsule length write fail) << "Failed to write CAPSULE length";
-    return {};
-  }
-  switch (capsule.capsule_type()) {
-    case CapsuleType::DATAGRAM:
-      if (!writer.WriteStringPiece(
-              capsule.datagram_capsule().http_datagram_payload)) {
-        QUICHE_BUG(datagram capsule payload write fail)
-            << "Failed to write DATAGRAM CAPSULE payload";
-        return {};
-      }
-      break;
-    case CapsuleType::LEGACY_DATAGRAM:
-      if (!writer.WriteStringPiece(
-              capsule.legacy_datagram_capsule().http_datagram_payload)) {
-        QUICHE_BUG(datagram legacy capsule payload write fail)
-            << "Failed to write LEGACY_DATAGRAM CAPSULE payload";
-        return {};
-      }
-      break;
-    case CapsuleType::LEGACY_DATAGRAM_WITHOUT_CONTEXT:
-      if (!writer.WriteStringPiece(
-              capsule.legacy_datagram_without_context_capsule()
-                  .http_datagram_payload)) {
-        QUICHE_BUG(datagram legacy without context capsule payload write fail)
-            << "Failed to write LEGACY_DATAGRAM_WITHOUT_CONTEXT CAPSULE "
-               "payload";
-        return {};
-      }
-      break;
-    case CapsuleType::CLOSE_WEBTRANSPORT_SESSION:
-      if (!writer.WriteUInt32(
-              capsule.close_web_transport_session_capsule().error_code)) {
-        QUICHE_BUG(close webtransport session capsule error code write fail)
-            << "Failed to write CLOSE_WEBTRANSPORT_SESSION error code";
-        return {};
-      }
-      if (!writer.WriteStringPiece(
-              capsule.close_web_transport_session_capsule().error_message)) {
-        QUICHE_BUG(close webtransport session capsule error message write fail)
-            << "Failed to write CLOSE_WEBTRANSPORT_SESSION error message";
-        return {};
-      }
-      break;
-    case CapsuleType::ADDRESS_REQUEST:
-      for (auto requested_address :
-           capsule.address_request_capsule().requested_addresses) {
-        if (!writer.WriteVarInt62(requested_address.request_id)) {
-          QUICHE_BUG(address request capsule id write fail)
-              << "Failed to write ADDRESS_REQUEST ID";
-          return {};
-        }
-        if (!writer.WriteUInt8(
-                requested_address.ip_prefix.address().IsIPv4() ? 4 : 6)) {
-          QUICHE_BUG(address request capsule family write fail)
-              << "Failed to write ADDRESS_REQUEST family";
-          return {};
-        }
-        if (!writer.WriteStringPiece(
-                requested_address.ip_prefix.address().ToPackedString())) {
-          QUICHE_BUG(address request capsule address write fail)
-              << "Failed to write ADDRESS_REQUEST address";
-          return {};
-        }
-        if (!writer.WriteUInt8(requested_address.ip_prefix.prefix_length())) {
-          QUICHE_BUG(address request capsule prefix length write fail)
-              << "Failed to write ADDRESS_REQUEST prefix length";
-          return {};
-        }
-      }
-      break;
-    case CapsuleType::ADDRESS_ASSIGN:
-      for (auto assigned_address :
-           capsule.address_assign_capsule().assigned_addresses) {
-        if (!writer.WriteVarInt62(assigned_address.request_id)) {
-          QUICHE_BUG(address request capsule id write fail)
-              << "Failed to write ADDRESS_ASSIGN ID";
-          return {};
-        }
-        if (!writer.WriteUInt8(
-                assigned_address.ip_prefix.address().IsIPv4() ? 4 : 6)) {
-          QUICHE_BUG(address request capsule family write fail)
-              << "Failed to write ADDRESS_ASSIGN family";
-          return {};
-        }
-        if (!writer.WriteStringPiece(
-                assigned_address.ip_prefix.address().ToPackedString())) {
-          QUICHE_BUG(address request capsule address write fail)
-              << "Failed to write ADDRESS_ASSIGN address";
-          return {};
-        }
-        if (!writer.WriteUInt8(assigned_address.ip_prefix.prefix_length())) {
-          QUICHE_BUG(address request capsule prefix length write fail)
-              << "Failed to write ADDRESS_ASSIGN prefix length";
-          return {};
-        }
-      }
-      break;
-    case CapsuleType::ROUTE_ADVERTISEMENT:
-      for (auto ip_address_range :
-           capsule.route_advertisement_capsule().ip_address_ranges) {
-        if (!writer.WriteUInt8(
-                ip_address_range.start_ip_address.IsIPv4() ? 4 : 6)) {
-          QUICHE_BUG(route advertisement capsule family write fail)
-              << "Failed to write ROUTE_ADVERTISEMENT family";
-          return {};
-        }
-        if (!writer.WriteStringPiece(
-                ip_address_range.start_ip_address.ToPackedString())) {
-          QUICHE_BUG(route advertisement capsule start address write fail)
-              << "Failed to write ROUTE_ADVERTISEMENT start address";
-          return {};
-        }
-        if (!writer.WriteStringPiece(
-                ip_address_range.end_ip_address.ToPackedString())) {
-          QUICHE_BUG(route advertisement capsule end address write fail)
-              << "Failed to write ROUTE_ADVERTISEMENT end address";
-          return {};
-        }
-        if (!writer.WriteUInt8(ip_address_range.ip_protocol)) {
-          QUICHE_BUG(route advertisement capsule IP protocol write fail)
-              << "Failed to write ROUTE_ADVERTISEMENT IP protocol";
-          return {};
-        }
-      }
-      break;
-    default:
-      if (!writer.WriteStringPiece(capsule.unknown_capsule_data())) {
-        QUICHE_BUG(capsule data write fail) << "Failed to write CAPSULE data";
-        return {};
-      }
-      break;
-  }
-  if (writer.remaining() != 0) {
-    QUICHE_BUG(capsule write length mismatch)
-        << "CAPSULE serialization wrote " << writer.length() << " instead of "
-        << writer.capacity();
-    return {};
-  }
-  return buffer;
+  return *std::move(serialized);
 }
 
 bool CapsuleParser::IngestCapsuleFragment(absl::string_view capsule_fragment) {
diff --git a/quiche/common/quiche_status_utils.h b/quiche/common/quiche_status_utils.h
new file mode 100644
index 0000000..7b14e5d
--- /dev/null
+++ b/quiche/common/quiche_status_utils.h
@@ -0,0 +1,51 @@
+// 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.
+
+#ifndef QUICHE_COMMON_QUICHE_STATUS_UTILS_H_
+#define QUICHE_COMMON_QUICHE_STATUS_UTILS_H_
+
+#include <utility>
+
+#include "absl/base/optimization.h"
+#include "absl/status/status.h"
+#include "absl/strings/str_cat.h"
+
+namespace quiche {
+
+// A simplified version of the standard google3 "return if error" macro. Unlike
+// the standard version, this does not come with a StatusBuilder support; the
+// AppendToStatus() function below is meant to partially fill that gap.
+#define QUICHE_RETURN_IF_ERROR(expr)                           \
+  do {                                                         \
+    absl::Status quiche_status_macro_value = (expr);           \
+    if (ABSL_PREDICT_FALSE(!quiche_status_macro_value.ok())) { \
+      return quiche_status_macro_value;                        \
+    }                                                          \
+  } while (0)
+
+// Copies absl::Status payloads from `original` to `target`; required to copy a
+// status correctly.
+inline void CopyStatusPayloads(const absl::Status& original,
+                               absl::Status& target) {
+  original.ForEachPayload([&](absl::string_view key, const absl::Cord& value) {
+    target.SetPayload(key, value);
+  });
+}
+
+// Appends additional into to a status message if the status message is
+// an error.
+template <typename... T>
+absl::Status AppendToStatus(absl::Status input, T&&... args) {
+  if (ABSL_PREDICT_TRUE(input.ok())) {
+    return input;
+  }
+  absl::Status result = absl::Status(
+      input.code(), absl::StrCat(input.message(), std::forward<T>(args)...));
+  CopyStatusPayloads(input, result);
+  return result;
+}
+
+}  // namespace quiche
+
+#endif  // QUICHE_COMMON_QUICHE_STATUS_UTILS_H_
diff --git a/quiche/common/wire_serialization.h b/quiche/common/wire_serialization.h
new file mode 100644
index 0000000..4792ffe
--- /dev/null
+++ b/quiche/common/wire_serialization.h
@@ -0,0 +1,396 @@
+// 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)
+  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_
diff --git a/quiche/common/wire_serialization_test.cc b/quiche/common/wire_serialization_test.cc
new file mode 100644
index 0000000..b1dea91
--- /dev/null
+++ b/quiche/common/wire_serialization_test.cc
@@ -0,0 +1,256 @@
+// 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.
+
+#include "quiche/common/wire_serialization.h"
+
+#include <limits>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "quiche/common/platform/api/quiche_expect_bug.h"
+#include "quiche/common/platform/api/quiche_test.h"
+#include "quiche/common/quiche_buffer_allocator.h"
+#include "quiche/common/quiche_endian.h"
+#include "quiche/common/quiche_status_utils.h"
+#include "quiche/common/simple_buffer_allocator.h"
+#include "quiche/common/test_tools/quiche_test_utils.h"
+
+namespace quiche::test {
+namespace {
+
+using ::testing::ElementsAre;
+
+constexpr uint64_t kInvalidVarInt = std::numeric_limits<uint64_t>::max();
+
+template <typename... Ts>
+absl::StatusOr<quiche::QuicheBuffer> SerializeIntoSimpleBuffer(Ts... data) {
+  return SerializeIntoBuffer(quiche::SimpleBufferAllocator::Get(), data...);
+}
+
+template <typename... Ts>
+void ExpectEncoding(const std::string& description, absl::string_view expected,
+                    Ts... data) {
+  absl::StatusOr<quiche::QuicheBuffer> actual =
+      SerializeIntoSimpleBuffer(data...);
+  QUICHE_ASSERT_OK(actual);
+  quiche::test::CompareCharArraysWithHexError(description, actual->data(),
+                                              actual->size(), expected.data(),
+                                              expected.size());
+}
+
+template <typename... Ts>
+void ExpectEncodingHex(const std::string& description,
+                       absl::string_view expected_hex, Ts... data) {
+  ExpectEncoding(description, absl::HexStringToBytes(expected_hex), data...);
+}
+
+TEST(SerializationTest, SerializeStrings) {
+  absl::StatusOr<quiche::QuicheBuffer> one_string =
+      SerializeIntoSimpleBuffer(WireBytes("test"));
+  QUICHE_ASSERT_OK(one_string);
+  EXPECT_EQ(one_string->AsStringView(), "test");
+
+  absl::StatusOr<quiche::QuicheBuffer> two_strings =
+      SerializeIntoSimpleBuffer(WireBytes("Hello"), WireBytes("World"));
+  QUICHE_ASSERT_OK(two_strings);
+  EXPECT_EQ(two_strings->AsStringView(), "HelloWorld");
+}
+
+TEST(SerializationTest, SerializeIntegers) {
+  ExpectEncodingHex("one uint8_t value", "42", WireUint8(0x42));
+  ExpectEncodingHex("two uint8_t values", "ab01", WireUint8(0xab),
+                    WireUint8(0x01));
+  ExpectEncodingHex("one uint16_t value", "1234", WireUint16(0x1234));
+  ExpectEncodingHex("one uint32_t value", "12345678", WireUint32(0x12345678));
+  ExpectEncodingHex("one uint64_t value", "123456789abcdef0",
+                    WireUint64(UINT64_C(0x123456789abcdef0)));
+  ExpectEncodingHex("mix of values", "aabbcc000000dd", WireUint8(0xaa),
+                    WireUint16(0xbbcc), WireUint32(0xdd));
+}
+
+TEST(SerializationTest, SerializeLittleEndian) {
+  char buffer[4];
+  QuicheDataWriter writer(sizeof(buffer), buffer,
+                          quiche::Endianness::HOST_BYTE_ORDER);
+  QUICHE_ASSERT_OK(
+      SerializeIntoWriter(writer, WireUint16(0x1234), WireUint16(0xabcd)));
+  absl::string_view actual(writer.data(), writer.length());
+  EXPECT_EQ(actual, absl::HexStringToBytes("3412cdab"));
+}
+
+TEST(SerializationTest, SerializeVarInt62) {
+  // Test cases from RFC 9000, Appendix A.1
+  ExpectEncodingHex("1-byte varint", "25", WireVarInt62(37));
+  ExpectEncodingHex("2-byte varint", "7bbd", WireVarInt62(15293));
+  ExpectEncodingHex("4-byte varint", "9d7f3e7d", WireVarInt62(494878333));
+  ExpectEncodingHex("8-byte varint", "c2197c5eff14e88c",
+                    WireVarInt62(UINT64_C(151288809941952652)));
+}
+
+TEST(SerializationTest, SerializeStringWithVarInt62Length) {
+  ExpectEncodingHex("short string", "0474657374",
+                    WireStringWithVarInt62Length("test"));
+  const std::string long_string(15293, 'a');
+  ExpectEncoding("long string", absl::StrCat("\x7b\xbd", long_string),
+                 WireStringWithVarInt62Length(long_string));
+  ExpectEncodingHex("empty string", "00", WireStringWithVarInt62Length(""));
+}
+
+TEST(SerializationTest, SerializeOptionalValues) {
+  absl::optional<uint8_t> has_no_value;
+  absl::optional<uint8_t> has_value = 0x42;
+  ExpectEncodingHex("optional without value", "00", WireUint8(0),
+                    WireOptional<WireUint8>(has_no_value));
+  ExpectEncodingHex("optional with value", "0142", WireUint8(1),
+                    WireOptional<WireUint8>(has_value));
+  ExpectEncodingHex("empty data", "", WireOptional<WireUint8>(has_no_value));
+
+  absl::optional<std::string> has_no_string;
+  absl::optional<std::string> has_string = "\x42";
+  ExpectEncodingHex("optional no string", "",
+                    WireOptional<WireStringWithVarInt62Length>(has_no_string));
+  ExpectEncodingHex("optional string", "0142",
+                    WireOptional<WireStringWithVarInt62Length>(has_string));
+}
+
+enum class TestEnum {
+  kValue1 = 0x17,
+  kValue2 = 0x19,
+};
+
+TEST(SerializationTest, SerializeEnumValue) {
+  ExpectEncodingHex("enum value", "17", WireVarInt62(TestEnum::kValue1));
+}
+
+TEST(SerializationTest, SerializeLotsOfValues) {
+  ExpectEncodingHex("ten values", "00010203040506070809", WireUint8(0),
+                    WireUint8(1), WireUint8(2), WireUint8(3), WireUint8(4),
+                    WireUint8(5), WireUint8(6), WireUint8(7), WireUint8(8),
+                    WireUint8(9));
+}
+
+TEST(SerializationTest, FailDueToLackOfSpace) {
+  char buffer[4];
+  QuicheDataWriter writer(sizeof(buffer), buffer);
+  QUICHE_EXPECT_OK(SerializeIntoWriter(writer, WireUint32(0)));
+  ASSERT_EQ(writer.remaining(), 0u);
+  EXPECT_THAT(
+      SerializeIntoWriter(writer, WireUint32(0)),
+      StatusIs(absl::StatusCode::kInternal, "Failed to serialize field #0"));
+  EXPECT_THAT(
+      SerializeIntoWriter(writer, WireStringWithVarInt62Length("test")),
+      StatusIs(
+          absl::StatusCode::kInternal,
+          "Failed to serialize the length prefix while serializing field #0"));
+}
+
+TEST(SerializationTest, FailDueToInvalidValue) {
+  EXPECT_QUICHE_BUG(
+      ExpectEncoding("invalid varint", "", WireVarInt62(kInvalidVarInt)),
+      "too big for VarInt62");
+}
+
+TEST(SerializationTest, InvalidValueCausesPartialWrite) {
+  char buffer[3] = {'\0'};
+  QuicheDataWriter writer(sizeof(buffer), buffer);
+  QUICHE_EXPECT_OK(SerializeIntoWriter(writer, WireBytes("a")));
+  EXPECT_THAT(
+      SerializeIntoWriter(writer, WireBytes("b"),
+                          WireBytes("A considerably long string, writing which "
+                                    "will most likely cause ASAN to crash"),
+                          WireBytes("c")),
+      StatusIs(absl::StatusCode::kInternal, "Failed to serialize field #1"));
+  EXPECT_THAT(buffer, ElementsAre('a', 'b', '\0'));
+
+  QUICHE_EXPECT_OK(SerializeIntoWriter(writer, WireBytes("z")));
+  EXPECT_EQ(buffer[2], 'z');
+}
+
+TEST(SerializationTest, SerializeVector) {
+  std::vector<absl::string_view> strs = {"foo", "test", "bar"};
+  absl::StatusOr<quiche::QuicheBuffer> serialized =
+      SerializeIntoSimpleBuffer(WireSpan<WireBytes>(absl::MakeSpan(strs)));
+  QUICHE_ASSERT_OK(serialized);
+  EXPECT_EQ(serialized->AsStringView(), "footestbar");
+}
+
+struct AwesomeStruct {
+  uint64_t awesome_number;
+  std::string awesome_text;
+};
+
+class WireAwesomeStruct {
+ public:
+  using DataType = AwesomeStruct;
+
+  WireAwesomeStruct(const AwesomeStruct& awesome) : awesome_(awesome) {}
+
+  size_t GetLengthOnWire() {
+    return quiche::ComputeLengthOnWire(WireUint16(awesome_.awesome_number),
+                                       WireBytes(awesome_.awesome_text));
+  }
+  absl::Status SerializeIntoWriter(QuicheDataWriter& writer) {
+    return AppendToStatus(::quiche::SerializeIntoWriter(
+                              writer, WireUint16(awesome_.awesome_number),
+                              WireBytes(awesome_.awesome_text)),
+                          " while serializing AwesomeStruct");
+  }
+
+ private:
+  const AwesomeStruct& awesome_;
+};
+
+TEST(SerializationTest, CustomStruct) {
+  AwesomeStruct awesome;
+  awesome.awesome_number = 0xabcd;
+  awesome.awesome_text = "test";
+  ExpectEncodingHex("struct", "abcd74657374", WireAwesomeStruct(awesome));
+}
+
+TEST(SerializationTest, CustomStructSpan) {
+  std::array<AwesomeStruct, 2> awesome;
+  awesome[0].awesome_number = 0xabcd;
+  awesome[0].awesome_text = "test";
+  awesome[1].awesome_number = 0x1234;
+  awesome[1].awesome_text = std::string(3, '\0');
+  ExpectEncodingHex("struct", "abcd746573741234000000",
+                    WireSpan<WireAwesomeStruct>(absl::MakeSpan(awesome)));
+}
+
+class WireFormatterThatWritesTooLittle {
+ public:
+  using DataType = absl::string_view;
+
+  explicit WireFormatterThatWritesTooLittle(absl::string_view s) : s_(s) {}
+
+  size_t GetLengthOnWire() const { return s_.size(); }
+  bool SerializeIntoWriter(QuicheDataWriter& writer) {
+    return writer.WriteStringPiece(s_.substr(0, s_.size() - 1));
+  }
+
+ private:
+  absl::string_view s_;
+};
+
+TEST(SerializationTest, CustomStructWritesTooLittle) {
+  constexpr absl::string_view kStr = "\xaa\xbb\xcc\xdd";
+#if defined(NDEBUG)
+  absl::Status status =
+      SerializeIntoSimpleBuffer(WireFormatterThatWritesTooLittle(kStr))
+          .status();
+  EXPECT_THAT(status, StatusIs(absl::StatusCode::kInternal,
+                               ::testing::HasSubstr("Excess 1 bytes")));
+#else
+  EXPECT_DEATH(QUICHE_LOG(INFO) << SerializeIntoSimpleBuffer(
+                                       WireFormatterThatWritesTooLittle(kStr))
+                                       .status(),
+               "while serializing field #0");
+#endif
+}
+
+}  // namespace
+}  // namespace quiche::test