Add functions to handle WebTransport subprotocol negotiation headers.

Based on https://github.com/ietf-wg-webtrans/draft-ietf-webtrans-http3/pull/144

PiperOrigin-RevId: 583029408
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 3615c89..df02f45 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -394,6 +394,7 @@
     "web_transport/complete_buffer_visitor.h",
     "web_transport/encapsulated/encapsulated_web_transport.h",
     "web_transport/web_transport.h",
+    "web_transport/web_transport_headers.h",
 ]
 quiche_core_srcs = [
     "common/capsule.cc",
@@ -682,6 +683,7 @@
     "spdy/core/spdy_protocol.cc",
     "web_transport/complete_buffer_visitor.cc",
     "web_transport/encapsulated/encapsulated_web_transport.cc",
+    "web_transport/web_transport_headers.cc",
 ]
 quiche_tool_support_hdrs = [
     "common/platform/api/quiche_command_line_flags.h",
@@ -1307,6 +1309,7 @@
     "spdy/core/spdy_prefixed_buffer_reader_test.cc",
     "spdy/core/spdy_protocol_test.cc",
     "web_transport/encapsulated/encapsulated_web_transport_test.cc",
+    "web_transport/web_transport_headers_test.cc",
 ]
 io_tests_hdrs = [
 ]
diff --git a/build/source_list.gni b/build/source_list.gni
index 472aede..16a7eea 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -394,6 +394,7 @@
     "src/quiche/web_transport/complete_buffer_visitor.h",
     "src/quiche/web_transport/encapsulated/encapsulated_web_transport.h",
     "src/quiche/web_transport/web_transport.h",
+    "src/quiche/web_transport/web_transport_headers.h",
 ]
 quiche_core_srcs = [
     "src/quiche/common/capsule.cc",
@@ -682,6 +683,7 @@
     "src/quiche/spdy/core/spdy_protocol.cc",
     "src/quiche/web_transport/complete_buffer_visitor.cc",
     "src/quiche/web_transport/encapsulated/encapsulated_web_transport.cc",
+    "src/quiche/web_transport/web_transport_headers.cc",
 ]
 quiche_tool_support_hdrs = [
     "src/quiche/common/platform/api/quiche_command_line_flags.h",
@@ -1308,6 +1310,7 @@
     "src/quiche/spdy/core/spdy_prefixed_buffer_reader_test.cc",
     "src/quiche/spdy/core/spdy_protocol_test.cc",
     "src/quiche/web_transport/encapsulated/encapsulated_web_transport_test.cc",
+    "src/quiche/web_transport/web_transport_headers_test.cc",
 ]
 io_tests_hdrs = [
 
diff --git a/build/source_list.json b/build/source_list.json
index fc9ff8b..41946db 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -392,7 +392,8 @@
     "quiche/spdy/core/zero_copy_output_buffer.h",
     "quiche/web_transport/complete_buffer_visitor.h",
     "quiche/web_transport/encapsulated/encapsulated_web_transport.h",
-    "quiche/web_transport/web_transport.h"
+    "quiche/web_transport/web_transport.h",
+    "quiche/web_transport/web_transport_headers.h"
   ],
   "quiche_core_srcs": [
     "quiche/common/capsule.cc",
@@ -680,7 +681,8 @@
     "quiche/spdy/core/spdy_prefixed_buffer_reader.cc",
     "quiche/spdy/core/spdy_protocol.cc",
     "quiche/web_transport/complete_buffer_visitor.cc",
-    "quiche/web_transport/encapsulated/encapsulated_web_transport.cc"
+    "quiche/web_transport/encapsulated/encapsulated_web_transport.cc",
+    "quiche/web_transport/web_transport_headers.cc"
   ],
   "quiche_tool_support_hdrs": [
     "quiche/common/platform/api/quiche_command_line_flags.h",
@@ -1306,7 +1308,8 @@
     "quiche/spdy/core/spdy_pinnable_buffer_piece_test.cc",
     "quiche/spdy/core/spdy_prefixed_buffer_reader_test.cc",
     "quiche/spdy/core/spdy_protocol_test.cc",
-    "quiche/web_transport/encapsulated/encapsulated_web_transport_test.cc"
+    "quiche/web_transport/encapsulated/encapsulated_web_transport_test.cc",
+    "quiche/web_transport/web_transport_headers_test.cc"
   ],
   "io_tests_hdrs": [
 
diff --git a/quiche/common/structured_headers.cc b/quiche/common/structured_headers.cc
index 0762d0d..164894b 100644
--- a/quiche/common/structured_headers.cc
+++ b/quiche/common/structured_headers.cc
@@ -5,9 +5,13 @@
 #include "quiche/common/structured_headers.h"
 
 #include <cmath>
+#include <cstddef>
+#include <cstdint>
 #include <optional>
+#include <sstream>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "absl/algorithm/container.h"
 #include "absl/container/flat_hash_set.h"
@@ -574,12 +578,9 @@
     }
     if (value.is_token()) {
       // Serializes a Token ([RFC8941] 4.1.7).
-      if (value.GetString().empty() ||
-          !(absl::ascii_isalpha(value.GetString().front()) ||
-            value.GetString().front() == '*'))
+      if (!IsValidToken(value.GetString())) {
         return false;
-      if (value.GetString().find_first_not_of(kTokenChars) != std::string::npos)
-        return false;
+      }
       output_ << value.GetString();
       return true;
     }
@@ -720,6 +721,38 @@
 
 }  // namespace
 
+absl::string_view ItemTypeToString(Item::ItemType type) {
+  switch (type) {
+    case Item::kNullType:
+      return "null";
+    case Item::kIntegerType:
+      return "integer";
+    case Item::kDecimalType:
+      return "decimal";
+    case Item::kStringType:
+      return "string";
+    case Item::kTokenType:
+      return "token";
+    case Item::kByteSequenceType:
+      return "byte sequence";
+    case Item::kBooleanType:
+      return "boolean";
+  }
+  return "[invalid type]";
+}
+
+bool IsValidToken(absl::string_view str) {
+  // Validate Token value per [RFC8941] 4.1.7.
+  if (str.empty() ||
+      !(absl::ascii_isalpha(str.front()) || str.front() == '*')) {
+    return false;
+  }
+  if (str.find_first_not_of(kTokenChars) != std::string::npos) {
+    return false;
+  }
+  return true;
+}
+
 Item::Item() {}
 Item::Item(std::string value, Item::ItemType type) {
   switch (type) {
diff --git a/quiche/common/structured_headers.h b/quiche/common/structured_headers.h
index af1b199..a9a79cd 100644
--- a/quiche/common/structured_headers.h
+++ b/quiche/common/structured_headers.h
@@ -5,7 +5,8 @@
 #ifndef QUICHE_COMMON_STRUCTURED_HEADERS_H_
 #define QUICHE_COMMON_STRUCTURED_HEADERS_H_
 
-#include <algorithm>
+#include <cstddef>
+#include <cstdint>
 #include <map>
 #include <optional>
 #include <string>
@@ -141,6 +142,12 @@
       value_;
 };
 
+// Returns a human-readable representation of an ItemType.
+QUICHE_EXPORT absl::string_view ItemTypeToString(Item::ItemType type);
+
+// Returns `true` if the string is a valid Token value.
+QUICHE_EXPORT bool IsValidToken(absl::string_view str);
+
 // Holds a ParameterizedIdentifier (draft 9 only). The contained Item must be a
 // Token, and there may be any number of parameters. Parameter ordering is not
 // significant.
diff --git a/quiche/web_transport/web_transport_headers.cc b/quiche/web_transport/web_transport_headers.cc
new file mode 100644
index 0000000..7112f25
--- /dev/null
+++ b/quiche/web_transport/web_transport_headers.cc
@@ -0,0 +1,65 @@
+// Copyright 2023 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/web_transport/web_transport_headers.h"
+
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_join.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/span.h"
+#include "quiche/common/structured_headers.h"
+
+namespace webtransport {
+
+using ::quiche::structured_headers::ItemTypeToString;
+using ::quiche::structured_headers::List;
+using ::quiche::structured_headers::ParameterizedItem;
+using ::quiche::structured_headers::ParameterizedMember;
+
+absl::StatusOr<std::vector<std::string>> ParseSubprotocolRequestHeader(
+    absl::string_view value) {
+  std::optional<List> parsed = quiche::structured_headers::ParseList(value);
+  if (!parsed.has_value()) {
+    return absl::InvalidArgumentError(
+        "Failed to parse the header as an sf-list");
+  }
+
+  std::vector<std::string> result;
+  result.reserve(parsed->size());
+  for (ParameterizedMember& member : *parsed) {
+    if (member.member_is_inner_list || member.member.size() != 1) {
+      return absl::InvalidArgumentError(
+          "Expected all members to be tokens, found a nested list instead");
+    }
+    ParameterizedItem& item = member.member[0];
+    if (!item.item.is_token()) {
+      return absl::InvalidArgumentError(
+          absl::StrCat("Expected all members to be tokens, found ",
+                       ItemTypeToString(item.item.Type()), " instead"));
+    }
+    result.push_back(std::move(item).item.TakeString());
+  }
+  return result;
+}
+
+absl::StatusOr<std::string> SerializeSubprotocolRequestHeader(
+    absl::Span<const std::string> subprotocols) {
+  // Serialize tokens manually via a simple StrJoin call; this lets us provide
+  // better error messages, and is probably more efficient too.
+  for (const std::string& token : subprotocols) {
+    if (!quiche::structured_headers::IsValidToken(token)) {
+      return absl::InvalidArgumentError(absl::StrCat("Invalid token: ", token));
+    }
+  }
+  return absl::StrJoin(subprotocols, ", ");
+}
+
+}  // namespace webtransport
diff --git a/quiche/web_transport/web_transport_headers.h b/quiche/web_transport/web_transport_headers.h
new file mode 100644
index 0000000..b3b16b2
--- /dev/null
+++ b/quiche/web_transport/web_transport_headers.h
@@ -0,0 +1,30 @@
+// Copyright 2023 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_WEB_TRANSPORT_WEB_TRANSPORT_HEADERS_H_
+#define QUICHE_WEB_TRANSPORT_WEB_TRANSPORT_HEADERS_H_
+
+#include <string>
+#include <vector>
+
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/span.h"
+#include "quiche/common/platform/api/quiche_export.h"
+
+namespace webtransport {
+
+inline constexpr absl::string_view kSubprotocolRequestHeader =
+    "WebTransport-Subprotocols-Available";
+inline constexpr absl::string_view kSubprotocolResponseHeader =
+    "WebTransport-Subprotocol";
+
+QUICHE_EXPORT absl::StatusOr<std::vector<std::string>>
+ParseSubprotocolRequestHeader(absl::string_view value);
+QUICHE_EXPORT absl::StatusOr<std::string> SerializeSubprotocolRequestHeader(
+    absl::Span<const std::string> subprotocols);
+
+}  // namespace webtransport
+
+#endif  // QUICHE_WEB_TRANSPORT_WEB_TRANSPORT_HEADERS_H_
diff --git a/quiche/web_transport/web_transport_headers_test.cc b/quiche/web_transport/web_transport_headers_test.cc
new file mode 100644
index 0000000..38cc2c4
--- /dev/null
+++ b/quiche/web_transport/web_transport_headers_test.cc
@@ -0,0 +1,61 @@
+// Copyright 2023 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/web_transport/web_transport_headers.h"
+
+#include "absl/status/status.h"
+#include "quiche/common/platform/api/quiche_test.h"
+#include "quiche/common/test_tools/quiche_test_utils.h"
+
+namespace webtransport {
+namespace {
+
+using ::quiche::test::IsOkAndHolds;
+using ::quiche::test::StatusIs;
+using ::testing::ElementsAre;
+using ::testing::HasSubstr;
+
+TEST(WebTransportHeaders, ParseSubprotocolRequestHeader) {
+  EXPECT_THAT(ParseSubprotocolRequestHeader("test"),
+              IsOkAndHolds(ElementsAre("test")));
+  EXPECT_THAT(ParseSubprotocolRequestHeader("moqt-draft01, moqt-draft02"),
+              IsOkAndHolds(ElementsAre("moqt-draft01", "moqt-draft02")));
+  EXPECT_THAT(ParseSubprotocolRequestHeader("moqt-draft01; a=b, moqt-draft02"),
+              IsOkAndHolds(ElementsAre("moqt-draft01", "moqt-draft02")));
+  EXPECT_THAT(ParseSubprotocolRequestHeader("moqt-draft01, moqt-draft02; a=b"),
+              IsOkAndHolds(ElementsAre("moqt-draft01", "moqt-draft02")));
+  EXPECT_THAT(ParseSubprotocolRequestHeader("\"test\""),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("found string instead")));
+  EXPECT_THAT(ParseSubprotocolRequestHeader("42"),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("found integer instead")));
+  EXPECT_THAT(ParseSubprotocolRequestHeader("a, (b)"),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("found a nested list instead")));
+  EXPECT_THAT(ParseSubprotocolRequestHeader("a, (b c)"),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("found a nested list instead")));
+  EXPECT_THAT(ParseSubprotocolRequestHeader("foo, ?1, bar"),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("found boolean instead")));
+  EXPECT_THAT(ParseSubprotocolRequestHeader("(a"),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("parse the header as an sf-list")));
+}
+
+TEST(WebTransportHeaders, SerializeSubprotocolRequestHeader) {
+  EXPECT_THAT(SerializeSubprotocolRequestHeader({"test"}),
+              IsOkAndHolds("test"));
+  EXPECT_THAT(SerializeSubprotocolRequestHeader({"foo", "bar"}),
+              IsOkAndHolds("foo, bar"));
+  EXPECT_THAT(SerializeSubprotocolRequestHeader({"moqt-draft01", "a/b/c"}),
+              IsOkAndHolds("moqt-draft01, a/b/c"));
+  EXPECT_THAT(
+      SerializeSubprotocolRequestHeader({"abcd", "0123", "efgh"}),
+      StatusIs(absl::StatusCode::kInvalidArgument, "Invalid token: 0123"));
+}
+
+}  // namespace
+}  // namespace webtransport