Enforce limit on total length of MoQT FullTrackName.

Also, since the separation of Track Name and Namespace now appears to be stable, rework the classes to distinguish them, instead of having "FullTrackName" sometimes be a namespace and others a full name.

PiperOrigin-RevId: 773836112
diff --git a/quiche/quic/moqt/moqt_framer.cc b/quiche/quic/moqt/moqt_framer.cc
index 139a6d7..93c9a31 100644
--- a/quiche/quic/moqt/moqt_framer.cc
+++ b/quiche/quic/moqt/moqt_framer.cc
@@ -111,33 +111,44 @@
   const KeyValuePairList& list_;
 };
 
-class WireFullTrackName {
+class WireTrackNamespace {
  public:
-  using DataType = FullTrackName;
-
-  // If |includes_name| is true, the last element in the tuple is the track
-  // name and is therefore not counted in the prefix of the namespace tuple.
-  WireFullTrackName(const FullTrackName& name, bool includes_name)
-      : name_(name), includes_name_(includes_name) {}
+  WireTrackNamespace(const TrackNamespace& name) : namespace_(name) {}
 
   size_t GetLengthOnWire() {
     return quiche::ComputeLengthOnWire(
-        WireVarInt62(num_elements()),
-        WireSpan<WireStringWithVarInt62Length, std::string>(name_.tuple()));
+        WireVarInt62(namespace_.number_of_elements()),
+        WireSpan<WireStringWithVarInt62Length, std::string>(
+            namespace_.tuple()));
   }
   absl::Status SerializeIntoWriter(quiche::QuicheDataWriter& writer) {
     return quiche::SerializeIntoWriter(
-        writer, WireVarInt62(num_elements()),
-        WireSpan<WireStringWithVarInt62Length, std::string>(name_.tuple()));
+        writer, WireVarInt62(namespace_.number_of_elements()),
+        WireSpan<WireStringWithVarInt62Length, std::string>(
+            namespace_.tuple()));
   }
 
  private:
-  size_t num_elements() const {
-    return includes_name_ ? (name_.tuple().size() - 1) : name_.tuple().size();
+  const TrackNamespace& namespace_;
+};
+
+class WireFullTrackName {
+ public:
+  WireFullTrackName(const FullTrackName& name) : name_(name) {}
+
+  size_t GetLengthOnWire() {
+    return quiche::ComputeLengthOnWire(
+        WireTrackNamespace(name_.track_namespace()),
+        WireStringWithVarInt62Length(name_.name()));
+  }
+  absl::Status SerializeIntoWriter(quiche::QuicheDataWriter& writer) {
+    return quiche::SerializeIntoWriter(
+        writer, WireTrackNamespace(name_.track_namespace()),
+        WireStringWithVarInt62Length(name_.name()));
   }
 
+ private:
   const FullTrackName& name_;
-  const bool includes_name_;
 };
 
 // Serializes data into buffer using the default allocator.  Invokes QUICHE_BUG
@@ -432,7 +443,7 @@
       return SerializeControlMessage(
           MoqtMessageType::kSubscribe, WireVarInt62(message.request_id),
           WireVarInt62(message.track_alias),
-          WireFullTrackName(message.full_track_name, true),
+          WireFullTrackName(message.full_track_name),
           WireUint8(message.subscriber_priority),
           WireDeliveryOrder(message.group_order), WireBoolean(message.forward),
           WireVarInt62(message.filter_type), WireKeyValuePairList(parameters));
@@ -443,7 +454,7 @@
       return SerializeControlMessage(
           MoqtMessageType::kSubscribe, WireVarInt62(message.request_id),
           WireVarInt62(message.track_alias),
-          WireFullTrackName(message.full_track_name, true),
+          WireFullTrackName(message.full_track_name),
           WireUint8(message.subscriber_priority),
           WireDeliveryOrder(message.group_order), WireBoolean(message.forward),
           WireVarInt62(message.filter_type), WireVarInt62(message.start->group),
@@ -460,7 +471,7 @@
       return SerializeControlMessage(
           MoqtMessageType::kSubscribe, WireVarInt62(message.request_id),
           WireVarInt62(message.track_alias),
-          WireFullTrackName(message.full_track_name, true),
+          WireFullTrackName(message.full_track_name),
           WireUint8(message.subscriber_priority),
           WireDeliveryOrder(message.group_order), WireBoolean(message.forward),
           WireVarInt62(message.filter_type), WireVarInt62(message.start->group),
@@ -550,24 +561,22 @@
         << "Serializing invalid MoQT parameters";
     return quiche::QuicheBuffer();
   }
-  return SerializeControlMessage(
-      MoqtMessageType::kAnnounce,
-      WireFullTrackName(message.track_namespace, false),
-      WireKeyValuePairList(parameters));
+  return SerializeControlMessage(MoqtMessageType::kAnnounce,
+                                 WireTrackNamespace(message.track_namespace),
+                                 WireKeyValuePairList(parameters));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeAnnounceOk(
     const MoqtAnnounceOk& message) {
-  return SerializeControlMessage(
-      MoqtMessageType::kAnnounceOk,
-      WireFullTrackName(message.track_namespace, false));
+  return SerializeControlMessage(MoqtMessageType::kAnnounceOk,
+                                 WireTrackNamespace(message.track_namespace));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeAnnounceError(
     const MoqtAnnounceError& message) {
   return SerializeControlMessage(
       MoqtMessageType::kAnnounceError,
-      WireFullTrackName(message.track_namespace, false),
+      WireTrackNamespace(message.track_namespace),
       WireVarInt62(message.error_code),
       WireStringWithVarInt62Length(message.reason_phrase));
 }
@@ -576,7 +585,7 @@
     const MoqtAnnounceCancel& message) {
   return SerializeControlMessage(
       MoqtMessageType::kAnnounceCancel,
-      WireFullTrackName(message.track_namespace, false),
+      WireTrackNamespace(message.track_namespace),
       WireVarInt62(message.error_code),
       WireStringWithVarInt62Length(message.reason_phrase));
 }
@@ -591,17 +600,15 @@
         << "Serializing invalid MoQT parameters";
     return quiche::QuicheBuffer();
   }
-  return SerializeControlMessage(
-      MoqtMessageType::kTrackStatusRequest,
-      WireFullTrackName(message.full_track_name, true),
-      WireKeyValuePairList(parameters));
+  return SerializeControlMessage(MoqtMessageType::kTrackStatusRequest,
+                                 WireFullTrackName(message.full_track_name),
+                                 WireKeyValuePairList(parameters));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeUnannounce(
     const MoqtUnannounce& message) {
-  return SerializeControlMessage(
-      MoqtMessageType::kUnannounce,
-      WireFullTrackName(message.track_namespace, false));
+  return SerializeControlMessage(MoqtMessageType::kUnannounce,
+                                 WireTrackNamespace(message.track_namespace));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeTrackStatus(
@@ -615,8 +622,7 @@
     return quiche::QuicheBuffer();
   }
   return SerializeControlMessage(
-      MoqtMessageType::kTrackStatus,
-      WireFullTrackName(message.full_track_name, true),
+      MoqtMessageType::kTrackStatus, WireFullTrackName(message.full_track_name),
       WireVarInt62(message.status_code), WireVarInt62(message.last_group),
       WireVarInt62(message.last_object), WireKeyValuePairList(parameters));
 }
@@ -637,33 +643,30 @@
         << "Serializing invalid MoQT parameters";
     return quiche::QuicheBuffer();
   }
-  return SerializeControlMessage(
-      MoqtMessageType::kSubscribeAnnounces,
-      WireFullTrackName(message.track_namespace, false),
-      WireKeyValuePairList(parameters));
+  return SerializeControlMessage(MoqtMessageType::kSubscribeAnnounces,
+                                 WireTrackNamespace(message.track_namespace),
+                                 WireKeyValuePairList(parameters));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeSubscribeAnnouncesOk(
     const MoqtSubscribeAnnouncesOk& message) {
-  return SerializeControlMessage(
-      MoqtMessageType::kSubscribeAnnouncesOk,
-      WireFullTrackName(message.track_namespace, false));
+  return SerializeControlMessage(MoqtMessageType::kSubscribeAnnouncesOk,
+                                 WireTrackNamespace(message.track_namespace));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeSubscribeAnnouncesError(
     const MoqtSubscribeAnnouncesError& message) {
   return SerializeControlMessage(
       MoqtMessageType::kSubscribeAnnouncesError,
-      WireFullTrackName(message.track_namespace, false),
+      WireTrackNamespace(message.track_namespace),
       WireVarInt62(message.error_code),
       WireStringWithVarInt62Length(message.reason_phrase));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeUnsubscribeAnnounces(
     const MoqtUnsubscribeAnnounces& message) {
-  return SerializeControlMessage(
-      MoqtMessageType::kUnsubscribeAnnounces,
-      WireFullTrackName(message.track_namespace, false));
+  return SerializeControlMessage(MoqtMessageType::kUnsubscribeAnnounces,
+                                 WireTrackNamespace(message.track_namespace));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeMaxRequestId(
@@ -703,7 +706,7 @@
       WireUint8(message.subscriber_priority),
       WireDeliveryOrder(message.group_order),
       WireVarInt62(FetchType::kStandalone),
-      WireFullTrackName(message.full_track_name, true),
+      WireFullTrackName(message.full_track_name),
       WireVarInt62(message.start_object.group),
       WireVarInt62(message.start_object.object),
       WireVarInt62(message.end_group),
diff --git a/quiche/quic/moqt/moqt_integration_test.cc b/quiche/quic/moqt/moqt_integration_test.cc
index b6c95ad..c1099ee 100644
--- a/quiche/quic/moqt/moqt_integration_test.cc
+++ b/quiche/quic/moqt/moqt_integration_test.cc
@@ -139,20 +139,20 @@
   auto parameters = std::make_optional<VersionSpecificParameters>(
       AuthTokenType::kOutOfBand, "foo");
   EXPECT_CALL(server_callbacks_.incoming_announce_callback,
-              Call(FullTrackName{"foo"}, parameters))
+              Call(TrackNamespace{"foo"}, parameters))
       .WillOnce(Return(std::nullopt));
   testing::MockFunction<void(
-      FullTrackName track_namespace,
+      TrackNamespace track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_callback;
-  client_->session()->Announce(FullTrackName{"foo"},
+  client_->session()->Announce(TrackNamespace{"foo"},
                                announce_callback.AsStdFunction(), *parameters);
   bool matches = false;
   EXPECT_CALL(announce_callback, Call(_, _))
-      .WillOnce([&](FullTrackName track_namespace,
+      .WillOnce([&](TrackNamespace track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
         matches = true;
-        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
+        EXPECT_EQ(track_namespace, TrackNamespace{"foo"});
         EXPECT_FALSE(error.has_value());
       });
   bool success =
@@ -160,14 +160,14 @@
   EXPECT_TRUE(success);
   matches = false;
   EXPECT_CALL(server_callbacks_.incoming_announce_callback, Call(_, _))
-      .WillOnce([&](FullTrackName name,
+      .WillOnce([&](TrackNamespace name,
                     std::optional<VersionSpecificParameters> parameters) {
         matches = true;
-        EXPECT_EQ(name, FullTrackName{"foo"});
+        EXPECT_EQ(name, TrackNamespace{"foo"});
         EXPECT_FALSE(parameters.has_value());
         return std::nullopt;
       });
-  client_->session()->Unannounce(FullTrackName{"foo"});
+  client_->session()->Unannounce(TrackNamespace{"foo"});
   success = test_harness_.RunUntilWithDefaultTimeout([&]() { return matches; });
   EXPECT_TRUE(success);
 }
@@ -177,20 +177,20 @@
   auto parameters = std::make_optional<VersionSpecificParameters>(
       AuthTokenType::kOutOfBand, "foo");
   EXPECT_CALL(server_callbacks_.incoming_announce_callback,
-              Call(FullTrackName{"foo"}, parameters))
+              Call(TrackNamespace{"foo"}, parameters))
       .WillOnce(Return(std::nullopt));
   testing::MockFunction<void(
-      FullTrackName track_namespace,
+      TrackNamespace track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_callback;
-  client_->session()->Announce(FullTrackName{"foo"},
+  client_->session()->Announce(TrackNamespace{"foo"},
                                announce_callback.AsStdFunction(), *parameters);
   bool matches = false;
   EXPECT_CALL(announce_callback, Call(_, _))
-      .WillOnce([&](FullTrackName track_namespace,
+      .WillOnce([&](TrackNamespace track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
         matches = true;
-        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
+        EXPECT_EQ(track_namespace, TrackNamespace{"foo"});
         EXPECT_FALSE(error.has_value());
       });
   bool success =
@@ -198,16 +198,17 @@
   EXPECT_TRUE(success);
   matches = false;
   EXPECT_CALL(announce_callback, Call(_, _))
-      .WillOnce([&](FullTrackName track_namespace,
+      .WillOnce([&](TrackNamespace track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
         matches = true;
-        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
+        EXPECT_EQ(track_namespace, TrackNamespace{"foo"});
         ASSERT_TRUE(error.has_value());
         EXPECT_EQ(error->error_code, RequestErrorCode::kInternalError);
         EXPECT_EQ(error->reason_phrase, "internal error");
       });
-  server_->session()->CancelAnnounce(
-      FullTrackName{"foo"}, RequestErrorCode::kInternalError, "internal error");
+  server_->session()->CancelAnnounce(TrackNamespace{"foo"},
+                                     RequestErrorCode::kInternalError,
+                                     "internal error");
   success = test_harness_.RunUntilWithDefaultTimeout([&]() { return matches; });
   EXPECT_TRUE(success);
 }
@@ -217,22 +218,21 @@
   auto parameters = std::make_optional<VersionSpecificParameters>(
       AuthTokenType::kOutOfBand, "foo");
   EXPECT_CALL(server_callbacks_.incoming_announce_callback,
-              Call(FullTrackName{"foo"}, parameters))
+              Call(TrackNamespace{"foo"}, parameters))
       .WillOnce(Return(std::nullopt));
   MockSubscribeRemoteTrackVisitor server_visitor;
   testing::MockFunction<void(
-      FullTrackName track_namespace,
+      TrackNamespace track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_callback;
-  client_->session()->Announce(FullTrackName{"foo"},
+  client_->session()->Announce(TrackNamespace{"foo"},
                                announce_callback.AsStdFunction(), *parameters);
   bool matches = false;
   EXPECT_CALL(announce_callback, Call(_, _))
-      .WillOnce([&](FullTrackName track_namespace,
+      .WillOnce([&](TrackNamespace track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
-        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
-        FullTrackName track_name = track_namespace;
-        track_name.AddElement("/catalog");
+        EXPECT_EQ(track_namespace, TrackNamespace{"foo"});
+        FullTrackName track_name(track_namespace, "/catalog");
         EXPECT_FALSE(error.has_value());
         server_->session()->SubscribeCurrentObject(track_name, &server_visitor,
                                                    VersionSpecificParameters());
@@ -254,10 +254,9 @@
       AuthTokenType::kOutOfBand, "foo");
   MockSubscribeRemoteTrackVisitor server_visitor;
   EXPECT_CALL(server_callbacks_.incoming_announce_callback, Call(_, parameters))
-      .WillOnce([&](const FullTrackName& track_namespace,
+      .WillOnce([&](const TrackNamespace& track_namespace,
                     std::optional<VersionSpecificParameters> /*parameters*/) {
-        FullTrackName track_name = track_namespace;
-        track_name.AddElement("data");
+        FullTrackName track_name(track_namespace, "data");
         server_->session()->SubscribeAbsolute(
             track_name, /*start_group=*/0, /*start_object=*/0, &server_visitor,
             VersionSpecificParameters());
@@ -274,8 +273,8 @@
     received_subscribe_ok = true;
   });
   client_->session()->Announce(
-      FullTrackName{"test"},
-      [](FullTrackName, std::optional<MoqtAnnounceErrorReason>) {},
+      TrackNamespace{"test"},
+      [](TrackNamespace, std::optional<MoqtAnnounceErrorReason>) {},
       *parameters);
   bool success = test_harness_.RunUntilWithDefaultTimeout(
       [&]() { return received_subscribe_ok; });
@@ -447,18 +446,18 @@
 TEST_F(MoqtIntegrationTest, AnnounceFailure) {
   EstablishSession();
   testing::MockFunction<void(
-      FullTrackName track_namespace,
+      TrackNamespace track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_callback;
-  client_->session()->Announce(FullTrackName{"foo"},
+  client_->session()->Announce(TrackNamespace{"foo"},
                                announce_callback.AsStdFunction(),
                                VersionSpecificParameters());
   bool matches = false;
   EXPECT_CALL(announce_callback, Call(_, _))
-      .WillOnce([&](FullTrackName track_namespace,
+      .WillOnce([&](TrackNamespace track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
         matches = true;
-        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
+        EXPECT_EQ(track_namespace, TrackNamespace{"foo"});
         ASSERT_TRUE(error.has_value());
         EXPECT_EQ(error->error_code, RequestErrorCode::kNotSupported);
       });
diff --git a/quiche/quic/moqt/moqt_messages.cc b/quiche/quic/moqt/moqt_messages.cc
index 96e50cd..3cb9923 100644
--- a/quiche/quic/moqt/moqt_messages.cc
+++ b/quiche/quic/moqt/moqt_messages.cc
@@ -7,7 +7,6 @@
 #include <array>
 #include <cstddef>
 #include <cstdint>
-#include <optional>
 #include <string>
 #include <utility>
 #include <vector>
@@ -307,29 +306,71 @@
   return "Unknown preference " + std::to_string(static_cast<int>(preference));
 }
 
-std::string FullTrackName::ToString() const {
+TrackNamespace::TrackNamespace(absl::Span<const absl::string_view> elements)
+    : tuple_(elements.begin(), elements.end()) {
+  if (std::size(elements) > kMaxNamespaceElements) {
+    tuple_.clear();
+    QUICHE_BUG(Moqt_namespace_too_large_01)
+        << "Constructing a namespace that is too large.";
+    return;
+  }
+  for (auto it : elements) {
+    length_ += it.size();
+    if (length_ > kMaxFullTrackNameSize) {
+      tuple_.clear();
+      QUICHE_BUG(Moqt_namespace_too_large_02)
+          << "Constructing a namespace that is too large.";
+      return;
+    }
+  }
+}
+
+bool TrackNamespace::InNamespace(const TrackNamespace& other) const {
+  if (tuple_.size() < other.tuple_.size()) {
+    return false;
+  }
+  for (int i = 0; i < other.tuple_.size(); ++i) {
+    if (tuple_[i] != other.tuple_[i]) {
+      return false;
+    }
+  }
+  return true;
+}
+
+void TrackNamespace::AddElement(absl::string_view element) {
+  if (!CanAddElement(element)) {
+    QUICHE_BUG(Moqt_namespace_too_large_03)
+        << "Constructing a namespace that is too large.";
+    return;
+  }
+  length_ += element.length();
+  tuple_.push_back(std::string(element));
+}
+
+std::string TrackNamespace::ToString() const {
   std::vector<std::string> bits;
   bits.reserve(tuple_.size());
   for (absl::string_view raw_bit : tuple_) {
     bits.push_back(absl::StrCat("\"", absl::CHexEscape(raw_bit), "\""));
   }
-  return absl::StrCat("{", absl::StrJoin(bits, ", "), "}");
+  return absl::StrCat("{", absl::StrJoin(bits, "::"), "}");
 }
 
-bool FullTrackName::operator==(const FullTrackName& other) const {
-  if (tuple_.size() != other.tuple_.size()) {
+bool TrackNamespace::operator==(const TrackNamespace& other) const {
+  if (number_of_elements() != other.number_of_elements()) {
     return false;
   }
   return absl::c_equal(tuple_, other.tuple_);
 }
-bool FullTrackName::operator<(const FullTrackName& other) const {
+
+bool TrackNamespace::operator<(const TrackNamespace& other) const {
   return absl::c_lexicographical_compare(tuple_, other.tuple_);
 }
-FullTrackName::FullTrackName(absl::Span<const absl::string_view> elements)
-    : tuple_(elements.begin(), elements.end()) {
-  QUICHE_BUG_IF(Moqt_namespace_too_large_03,
-                std::size(elements) > (kMaxNamespaceElements + 1))
-      << "Constructing a namespace that is too large.";
+
+void FullTrackName::set_name(absl::string_view name) {
+  QUIC_BUG_IF(Moqt_name_too_large_03, !CanAddName(name))
+      << "Setting a name that is too large.";
+  name_ = name;
 }
 
 absl::Status MoqtStreamErrorToStatus(webtransport::StreamErrorCode error_code,
diff --git a/quiche/quic/moqt/moqt_messages.h b/quiche/quic/moqt/moqt_messages.h
index 1b61d18..3a32cb9 100644
--- a/quiche/quic/moqt/moqt_messages.h
+++ b/quiche/quic/moqt/moqt_messages.h
@@ -18,6 +18,7 @@
 #include "absl/container/btree_map.h"
 #include "absl/container/inlined_vector.h"
 #include "absl/status/status.h"
+#include "absl/strings/str_cat.h"
 #include "absl/strings/str_format.h"
 #include "absl/strings/string_view.h"
 #include "absl/types/span.h"
@@ -47,6 +48,7 @@
 inline constexpr uint64_t kDefaultMaxAuthTokenCacheSize = 0;
 inline constexpr uint64_t kMinNamespaceElements = 1;
 inline constexpr uint64_t kMaxNamespaceElements = 32;
+inline constexpr size_t kMaxFullTrackNameSize = 1024;
 
 struct QUICHE_EXPORT MoqtSessionParameters {
   // TODO: support multiple versions.
@@ -265,67 +267,103 @@
 };
 using MoqtAnnounceErrorReason = MoqtSubscribeErrorReason;
 
-// Full track name represents a tuple of name elements. All higher order
-// elements MUST be present, but lower-order ones (like the name) can be
-// omitted.
-class FullTrackName {
+class TrackNamespace {
  public:
-  explicit FullTrackName(absl::Span<const absl::string_view> elements);
-  explicit FullTrackName(
+  explicit TrackNamespace(absl::Span<const absl::string_view> elements);
+  explicit TrackNamespace(
       std::initializer_list<const absl::string_view> elements)
-      : FullTrackName(absl::Span<const absl::string_view>(
-            std::data(elements), std::size(elements))) {
-    QUICHE_BUG_IF(Moqt_namespace_too_large_02,
-                  elements.size() > (kMaxNamespaceElements + 1))
-        << "Constructing a namespace that is too large.";
-  }
-  explicit FullTrackName(absl::string_view ns, absl::string_view name)
-      : FullTrackName({ns, name}) {}
-  FullTrackName() : FullTrackName({}) {}
+      : TrackNamespace(absl::Span<const absl::string_view>(
+            std::data(elements), std::size(elements))) {}
+  explicit TrackNamespace(absl::string_view ns) : TrackNamespace({ns}) {}
+  TrackNamespace() : TrackNamespace({}) {}
 
+  bool IsValid() const {
+    return !tuple_.empty() && tuple_.size() <= kMaxNamespaceElements &&
+           length_ <= kMaxFullTrackNameSize;
+  }
+  bool InNamespace(const TrackNamespace& other) const;
+  // Check if adding an element will exceed limits, without triggering a
+  // bug. Useful for the parser, which has to be robust to malformed data.
+  bool CanAddElement(absl::string_view element) {
+    return (tuple_.size() < kMaxNamespaceElements &&
+            length_ + element.length() <= kMaxFullTrackNameSize);
+  }
+  void AddElement(absl::string_view element);
   std::string ToString() const;
-
-  void AddElement(absl::string_view element) {
-    QUICHE_BUG_IF(Moqt_namespace_too_large_01,
-                  tuple_.size() > (kMaxNamespaceElements + 1))
-        << "Constructing a namespace that is too large.";
-    tuple_.push_back(std::string(element));
-  }
-  // Remove the last element to convert a name to a namespace.
-  void NameToNamespace() { tuple_.pop_back(); }
-  // returns true is |this| is a subdomain of |other|.
-  bool InNamespace(const FullTrackName& other) const {
-    if (tuple_.size() < other.tuple_.size()) {
-      return false;
-    }
-    for (int i = 0; i < other.tuple_.size(); ++i) {
-      if (tuple_[i] != other.tuple_[i]) {
-        return false;
-      }
-    }
-    return true;
-  }
-  absl::Span<const std::string> tuple() const {
-    return absl::MakeConstSpan(tuple_);
-  }
-
-  bool operator==(const FullTrackName& other) const;
-  bool operator<(const FullTrackName& other) const;
+  // Returns the number of elements in the tuple.
+  size_t number_of_elements() const { return tuple_.size(); }
+  // Returns the sum of the lengths of all elements in the tuple.
+  size_t total_length() const { return length_; }
+  bool operator==(const TrackNamespace& other) const;
+  bool operator<(const TrackNamespace& other) const;
+  const std::vector<std::string>& tuple() const { return tuple_; }
 
   template <typename H>
-  friend H AbslHashValue(H h, const FullTrackName& m) {
+  friend H AbslHashValue(H h, const TrackNamespace& m) {
     return H::combine(std::move(h), m.tuple_);
   }
-
   template <typename Sink>
-  friend void AbslStringify(Sink& sink, const FullTrackName& track_name) {
-    sink.Append(track_name.ToString());
+  friend void AbslStringify(Sink& sink, const TrackNamespace& track_namespace) {
+    sink.Append(track_namespace.ToString());
   }
 
-  bool empty() const { return tuple_.empty(); }
+ private:
+  std::vector<std::string> tuple_;
+  size_t length_ = 0;  // size in bytes.
+};
+
+class FullTrackName {
+ public:
+  FullTrackName(absl::string_view ns, absl::string_view name)
+      : namespace_(ns), name_(name) {
+    QUICHE_BUG_IF(Moqt_full_track_name_too_large_01, !IsValid())
+        << "Constructing a Full Track Name that is too large.";
+  }
+  FullTrackName(TrackNamespace ns, absl::string_view name)
+      : namespace_(ns), name_(name) {
+    QUICHE_BUG_IF(Moqt_full_track_name_too_large_02, !IsValid())
+        << "Constructing a Full Track Name that is too large.";
+  }
+  FullTrackName() = default;
+
+  bool IsValid() const {
+    return namespace_.IsValid() && length() <= kMaxFullTrackNameSize;
+  }
+  const TrackNamespace& track_namespace() const { return namespace_; }
+  TrackNamespace& track_namespace() { return namespace_; }
+  absl::string_view name() const { return name_; }
+  void AddElement(absl::string_view element) {
+    return namespace_.AddElement(element);
+  }
+  std::string ToString() const {
+    return absl::StrCat(namespace_.ToString(), "::", name_);
+  }
+  // Check if the name will exceed limits, without triggering a bug. Useful for
+  // the parser, which has to be robust to malformed data.
+  bool CanAddName(absl::string_view name) {
+    return (namespace_.total_length() + name.length() <= kMaxFullTrackNameSize);
+  }
+  void set_name(absl::string_view name);
+  size_t length() const { return namespace_.total_length() + name_.length(); }
+  bool operator==(const FullTrackName& other) const {
+    return name_ == other.name_ && namespace_ == other.namespace_;
+  }
+  bool operator<(const FullTrackName& other) const {
+    return namespace_ < other.namespace_ ||
+           (namespace_ == other.namespace_ && name_ < other.name_);
+  }
+  template <typename H>
+  friend H AbslHashValue(H h, const FullTrackName& m) {
+    return H::combine(std::move(h), m.namespace_.tuple(), m.name_);
+  }
+  template <typename Sink>
+  friend void AbslStringify(Sink& sink, const FullTrackName& full_track_name) {
+    sink.Append(full_track_name.ToString());
+  }
 
  private:
-  absl::InlinedVector<std::string, 2> tuple_;
+  TrackNamespace namespace_;
+  std::string name_ = "";
 };
 
 // These are absolute sequence numbers.
@@ -586,22 +624,22 @@
 };
 
 struct QUICHE_EXPORT MoqtAnnounce {
-  FullTrackName track_namespace;
+  TrackNamespace track_namespace;
   VersionSpecificParameters parameters;
 };
 
 struct QUICHE_EXPORT MoqtAnnounceOk {
-  FullTrackName track_namespace;
+  TrackNamespace track_namespace;
 };
 
 struct QUICHE_EXPORT MoqtAnnounceError {
-  FullTrackName track_namespace;
+  TrackNamespace track_namespace;
   RequestErrorCode error_code;
   std::string reason_phrase;
 };
 
 struct QUICHE_EXPORT MoqtUnannounce {
-  FullTrackName track_namespace;
+  TrackNamespace track_namespace;
 };
 
 enum class QUICHE_EXPORT MoqtTrackStatusCode : uint64_t {
@@ -634,7 +672,7 @@
 };
 
 struct QUICHE_EXPORT MoqtAnnounceCancel {
-  FullTrackName track_namespace;
+  TrackNamespace track_namespace;
   RequestErrorCode error_code;
   std::string reason_phrase;
 };
@@ -649,22 +687,22 @@
 };
 
 struct QUICHE_EXPORT MoqtSubscribeAnnounces {
-  FullTrackName track_namespace;
+  TrackNamespace track_namespace;
   VersionSpecificParameters parameters;
 };
 
 struct QUICHE_EXPORT MoqtSubscribeAnnouncesOk {
-  FullTrackName track_namespace;
+  TrackNamespace track_namespace;
 };
 
 struct QUICHE_EXPORT MoqtSubscribeAnnouncesError {
-  FullTrackName track_namespace;
+  TrackNamespace track_namespace;
   RequestErrorCode error_code;
   std::string reason_phrase;
 };
 
 struct QUICHE_EXPORT MoqtUnsubscribeAnnounces {
-  FullTrackName track_namespace;
+  TrackNamespace track_namespace;
 };
 
 struct QUICHE_EXPORT MoqtMaxRequestId {
diff --git a/quiche/quic/moqt/moqt_messages_test.cc b/quiche/quic/moqt/moqt_messages_test.cc
index 0b554f2..1eafaa7 100644
--- a/quiche/quic/moqt/moqt_messages_test.cc
+++ b/quiche/quic/moqt/moqt_messages_test.cc
@@ -8,44 +8,90 @@
 
 #include "absl/hash/hash.h"
 #include "absl/strings/string_view.h"
+#include "quiche/common/platform/api/quiche_expect_bug.h"
 #include "quiche/common/platform/api/quiche_test.h"
 
 namespace moqt::test {
 namespace {
 
-TEST(MoqtMessagesTest, FullTrackNameConstructors) {
-  FullTrackName name1({"foo", "bar"});
+TEST(MoqtMessagesTest, TrackNamespaceConstructors) {
+  TrackNamespace name1({"foo", "bar"});
   std::vector<absl::string_view> list = {"foo", "bar"};
-  FullTrackName name2(list);
+  TrackNamespace name2(list);
   EXPECT_EQ(name1, name2);
   EXPECT_EQ(absl::HashOf(name1), absl::HashOf(name2));
 }
 
-TEST(MoqtMessagesTest, FullTrackNameOrder) {
-  FullTrackName name1({"a", "b"});
-  FullTrackName name2({"a", "b", "c"});
-  FullTrackName name3({"b", "a"});
+TEST(MoqtMessagesTest, TrackNamespaceOrder) {
+  TrackNamespace name1({"a", "b"});
+  TrackNamespace name2({"a", "b", "c"});
+  TrackNamespace name3({"b", "a"});
   EXPECT_LT(name1, name2);
   EXPECT_LT(name2, name3);
   EXPECT_LT(name1, name3);
 }
 
-TEST(MoqtMessagesTest, FullTrackNameInNamespace) {
-  FullTrackName name1({"a", "b"});
-  FullTrackName name2({"a", "b", "c"});
-  FullTrackName name3({"d", "b"});
+TEST(MoqtMessagesTest, TrackNamespaceInNamespace) {
+  TrackNamespace name1({"a", "b"});
+  TrackNamespace name2({"a", "b", "c"});
+  TrackNamespace name3({"d", "b"});
   EXPECT_TRUE(name2.InNamespace(name1));
   EXPECT_FALSE(name1.InNamespace(name2));
   EXPECT_TRUE(name1.InNamespace(name1));
   EXPECT_FALSE(name2.InNamespace(name3));
 }
 
-TEST(MoqtMessagesTest, FullTrackNameToString) {
-  FullTrackName name1({"a", "b"});
-  EXPECT_EQ(name1.ToString(), R"({"a", "b"})");
+TEST(MoqtMessagesTest, TrackNamespaceToString) {
+  TrackNamespace name1({"a", "b"});
+  EXPECT_EQ(name1.ToString(), R"({"a"::"b"})");
 
-  FullTrackName name2({"\xff", "\x61"});
-  EXPECT_EQ(name2.ToString(), R"({"\xff", "a"})");
+  TrackNamespace name2({"\xff", "\x61"});
+  EXPECT_EQ(name2.ToString(), R"({"\xff"::"a"})");
+}
+
+TEST(MoqtMessagesTest, FullTrackNameToString) {
+  FullTrackName name1(TrackNamespace{"a", "b"}, "c");
+  EXPECT_EQ(name1.ToString(), R"({"a"::"b"}::c)");
+}
+
+TEST(MoqtMessagesTest, TooManyNamespaceElements) {
+  // 32 elements work.
+  TrackNamespace name1({"a", "b", "c",  "d",  "e",  "f",  "g",  "h",
+                        "i", "j", "k",  "l",  "m",  "n",  "o",  "p",
+                        "q", "r", "s",  "t",  "u",  "v",  "w",  "x",
+                        "y", "z", "aa", "bb", "cc", "dd", "ee", "ff"});
+  EXPECT_TRUE(name1.IsValid());
+  EXPECT_QUICHE_BUG(name1.AddElement("a"),
+                    "Constructing a namespace that is too large.");
+  EXPECT_EQ(name1.number_of_elements(), kMaxNamespaceElements);
+
+  // 33 elements fail,
+  TrackNamespace name2;
+  EXPECT_QUICHE_BUG(
+      name2 = TrackNamespace({"a",  "b",  "c",  "d",  "e",  "f", "g", "h", "i",
+                              "j",  "k",  "l",  "m",  "n",  "o", "p", "q", "r",
+                              "s",  "t",  "u",  "v",  "w",  "x", "y", "z", "aa",
+                              "bb", "cc", "dd", "ee", "ff", "gg"}),
+      "Constructing a namespace that is too large.");
+  EXPECT_FALSE(name2.IsValid());
+}
+
+TEST(MoqtMessagesTest, FullTrackNameTooLong) {
+  char raw_name[kMaxFullTrackNameSize + 1];
+  absl::string_view track_namespace(raw_name, kMaxFullTrackNameSize);
+  // Adding an element takes it over the length limit.
+  TrackNamespace max_length_namespace({track_namespace});
+  EXPECT_TRUE(max_length_namespace.IsValid());
+  EXPECT_QUICHE_BUG(max_length_namespace.AddElement("f"),
+                    "Constructing a namespace that is too large.");
+  // Constructing a FullTrackName where the name brings it over the length
+  // limit.
+  EXPECT_QUICHE_BUG(FullTrackName(max_length_namespace, "f"),
+                    "Constructing a Full Track Name that is too large.");
+  // The namespace is too long by itself..
+  absl::string_view big_namespace(raw_name, kMaxFullTrackNameSize + 1);
+  EXPECT_QUICHE_BUG(TrackNamespace({big_namespace}),
+                    "Constructing a namespace that is too large.");
 }
 
 }  // namespace
diff --git a/quiche/quic/moqt/moqt_parser.cc b/quiche/quic/moqt/moqt_parser.cc
index 608052b..01cd6c1 100644
--- a/quiche/quic/moqt/moqt_parser.cc
+++ b/quiche/quic/moqt/moqt_parser.cc
@@ -403,17 +403,14 @@
   MoqtSubscribe subscribe;
   uint64_t filter, group, object;
   uint8_t group_order, forward;
-  absl::string_view track_name;
   if (!reader.ReadVarInt62(&subscribe.request_id) ||
       !reader.ReadVarInt62(&subscribe.track_alias) ||
-      !ReadTrackNamespace(reader, subscribe.full_track_name) ||
-      !reader.ReadStringPieceVarInt62(&track_name) ||
+      !ReadFullTrackName(reader, subscribe.full_track_name) ||
       !reader.ReadUInt8(&subscribe.subscriber_priority) ||
       !reader.ReadUInt8(&group_order) || !reader.ReadUInt8(&forward) ||
       !reader.ReadVarInt62(&filter)) {
     return 0;
   }
-  subscribe.full_track_name.AddElement(track_name);
   if (!ParseDeliveryOrder(group_order, subscribe.group_order)) {
     ParseError("Invalid group order value in SUBSCRIBE");
     return 0;
@@ -653,14 +650,9 @@
 size_t MoqtControlParser::ProcessTrackStatusRequest(
     quic::QuicDataReader& reader) {
   MoqtTrackStatusRequest track_status_request;
-  if (!ReadTrackNamespace(reader, track_status_request.full_track_name)) {
+  if (!ReadFullTrackName(reader, track_status_request.full_track_name)) {
     return 0;
   }
-  absl::string_view name;
-  if (!reader.ReadStringPieceVarInt62(&name)) {
-    return 0;
-  }
-  track_status_request.full_track_name.AddElement(name);
   KeyValuePairList parameters;
   if (!ParseKeyValuePairList(reader, parameters)) {
     return 0;
@@ -689,14 +681,9 @@
 
 size_t MoqtControlParser::ProcessTrackStatus(quic::QuicDataReader& reader) {
   MoqtTrackStatus track_status;
-  if (!ReadTrackNamespace(reader, track_status.full_track_name)) {
+  if (!ReadFullTrackName(reader, track_status.full_track_name)) {
     return 0;
   }
-  absl::string_view name;
-  if (!reader.ReadStringPieceVarInt62(&name)) {
-    return 0;
-  }
-  track_status.full_track_name.AddElement(name);
   uint64_t value;
   if (!reader.ReadVarInt62(&value) ||
       !reader.ReadVarInt62(&track_status.last_group) ||
@@ -799,7 +786,6 @@
 
 size_t MoqtControlParser::ProcessFetch(quic::QuicDataReader& reader) {
   MoqtFetch fetch;
-  absl::string_view track_name;
   uint8_t group_order;
   uint64_t end_object;
   uint64_t type;
@@ -826,16 +812,13 @@
     }
     case FetchType::kStandalone: {
       fetch.joining_fetch = std::nullopt;
-      if (!ReadTrackNamespace(reader, fetch.full_track_name) ||
-          !reader.ReadStringPieceVarInt62(&track_name) ||
+      if (!ReadFullTrackName(reader, fetch.full_track_name) ||
           !reader.ReadVarInt62(&fetch.start_object.group) ||
           !reader.ReadVarInt62(&fetch.start_object.object) ||
           !reader.ReadVarInt62(&fetch.end_group) ||
           !reader.ReadVarInt62(&end_object)) {
         return 0;
       }
-      // Elements that have to be translated from the literal value.
-      fetch.full_track_name.AddElement(track_name);
       fetch.end_object =
           end_object == 0 ? std::optional<uint64_t>() : (end_object - 1);
       if (fetch.end_group < fetch.start_object.group ||
@@ -957,8 +940,8 @@
 }
 
 bool MoqtControlParser::ReadTrackNamespace(quic::QuicDataReader& reader,
-                                           FullTrackName& full_track_name) {
-  QUICHE_DCHECK(full_track_name.empty());
+                                           TrackNamespace& track_namespace) {
+  QUICHE_DCHECK(!track_namespace.IsValid());
   uint64_t num_elements;
   if (!reader.ReadVarInt62(&num_elements)) {
     return false;
@@ -973,8 +956,31 @@
     if (!reader.ReadStringPieceVarInt62(&element)) {
       return false;
     }
-    full_track_name.AddElement(element);
+    if (!track_namespace.CanAddElement(element)) {
+      ParseError(MoqtError::kProtocolViolation, "Full track name is too large");
+      return false;
+    }
+    track_namespace.AddElement(element);
   }
+  QUICHE_DCHECK(track_namespace.IsValid());
+  return true;
+}
+
+bool MoqtControlParser::ReadFullTrackName(quic::QuicDataReader& reader,
+                                          FullTrackName& full_track_name) {
+  QUICHE_DCHECK(!full_track_name.IsValid());
+  if (!ReadTrackNamespace(reader, full_track_name.track_namespace())) {
+    return false;
+  }
+  absl::string_view name;
+  if (!reader.ReadStringPieceVarInt62(&name)) {
+    return false;
+  }
+  if (!full_track_name.CanAddName(name)) {
+    ParseError(MoqtError::kProtocolViolation, "Full track name is too large");
+    return false;
+  }
+  full_track_name.set_name(name);
   return true;
 }
 
diff --git a/quiche/quic/moqt/moqt_parser.h b/quiche/quic/moqt/moqt_parser.h
index 1cb2fe3..030861d 100644
--- a/quiche/quic/moqt/moqt_parser.h
+++ b/quiche/quic/moqt/moqt_parser.h
@@ -136,11 +136,14 @@
   void ParseError(absl::string_view reason);
   void ParseError(MoqtError error, absl::string_view reason);
 
-  // Parses a message that a track namespace but not name. The last element of
-  // |full_track_name| will be set to the empty string. Returns false if it
-  // could not parse the full namespace field.
+  // Reads a TrackNamespace from the reader. Returns false if the namespace is
+  // too large. Sets a ParseError if the namespace is malformed.
   bool ReadTrackNamespace(quic::QuicDataReader& reader,
-                          FullTrackName& full_track_name);
+                          TrackNamespace& track_namespace);
+  // Reads a FullTrackName from the reader. Returns false if the name is too
+  // large. Sets a ParseError if the name is malformed.
+  bool ReadFullTrackName(quic::QuicDataReader& reader,
+                         FullTrackName& full_track_name);
   // Translates raw key/value pairs into semantically meaningful formats.
   // The spec defines many encoding errors in AUTHORIZATION TOKEN as
   // request level. This treats them as session-level, unless they are a result
diff --git a/quiche/quic/moqt/moqt_session.cc b/quiche/quic/moqt/moqt_session.cc
index 1e91479..8ad711c 100644
--- a/quiche/quic/moqt/moqt_session.cc
+++ b/quiche/quic/moqt/moqt_session.cc
@@ -89,6 +89,7 @@
 
   absl::StatusOr<std::shared_ptr<MoqtTrackPublisher>> GetTrack(
       const FullTrackName& track_name) override {
+    QUICHE_DCHECK(track_name.IsValid());
     return absl::NotFoundError("No tracks published");
   }
 };
@@ -249,9 +250,10 @@
 }
 
 bool MoqtSession::SubscribeAnnounces(
-    FullTrackName track_namespace,
+    TrackNamespace track_namespace,
     MoqtOutgoingSubscribeAnnouncesCallback callback,
     VersionSpecificParameters parameters) {
+  QUICHE_DCHECK(track_namespace.IsValid());
   if (received_goaway_ || sent_goaway_) {
     QUIC_DLOG(INFO) << ENDPOINT
                     << "Tried to send SUBSCRIBE_ANNOUNCES after GOAWAY";
@@ -267,7 +269,8 @@
   return true;
 }
 
-bool MoqtSession::UnsubscribeAnnounces(FullTrackName track_namespace) {
+bool MoqtSession::UnsubscribeAnnounces(TrackNamespace track_namespace) {
+  QUICHE_DCHECK(track_namespace.IsValid());
   if (!outgoing_subscribe_announces_.contains(track_namespace)) {
     return false;
   }
@@ -280,9 +283,10 @@
   return true;
 }
 
-void MoqtSession::Announce(FullTrackName track_namespace,
+void MoqtSession::Announce(TrackNamespace track_namespace,
                            MoqtOutgoingAnnounceCallback announce_callback,
                            VersionSpecificParameters parameters) {
+  QUICHE_DCHECK(track_namespace.IsValid());
   if (outgoing_announces_.contains(track_namespace)) {
     std::move(announce_callback)(
         track_namespace,
@@ -304,7 +308,8 @@
   outgoing_announces_[track_namespace] = std::move(announce_callback);
 }
 
-bool MoqtSession::Unannounce(FullTrackName track_namespace) {
+bool MoqtSession::Unannounce(TrackNamespace track_namespace) {
+  QUICHE_DCHECK(track_namespace.IsValid());
   auto it = outgoing_announces_.find(track_namespace);
   if (it == outgoing_announces_.end()) {
     return false;  // Could have been destroyed by ANNOUNCE_CANCEL.
@@ -318,9 +323,10 @@
   return true;
 }
 
-void MoqtSession::CancelAnnounce(FullTrackName track_namespace,
+void MoqtSession::CancelAnnounce(TrackNamespace track_namespace,
                                  RequestErrorCode code,
                                  absl::string_view reason) {
+  QUICHE_DCHECK(track_namespace.IsValid());
   MoqtAnnounceCancel message{track_namespace, code, std::string(reason)};
 
   SendControlMessage(framer_.SerializeAnnounceCancel(message));
@@ -332,6 +338,7 @@
                                     uint64_t start_group, uint64_t start_object,
                                     SubscribeRemoteTrack::Visitor* visitor,
                                     VersionSpecificParameters parameters) {
+  QUICHE_DCHECK(name.IsValid());
   MoqtSubscribe message;
   message.full_track_name = name;
   message.subscriber_priority = kDefaultSubscriberPriority;
@@ -349,6 +356,7 @@
                                     uint64_t end_group,
                                     SubscribeRemoteTrack::Visitor* visitor,
                                     VersionSpecificParameters parameters) {
+  QUICHE_DCHECK(name.IsValid());
   if (end_group < start_group) {
     QUIC_DLOG(ERROR) << "Subscription end is before beginning";
     return false;
@@ -368,6 +376,7 @@
 bool MoqtSession::SubscribeCurrentObject(const FullTrackName& name,
                                          SubscribeRemoteTrack::Visitor* visitor,
                                          VersionSpecificParameters parameters) {
+  QUICHE_DCHECK(name.IsValid());
   MoqtSubscribe message;
   message.full_track_name = name;
   message.subscriber_priority = kDefaultSubscriberPriority;
@@ -383,6 +392,7 @@
 bool MoqtSession::SubscribeNextGroup(const FullTrackName& name,
                                      SubscribeRemoteTrack::Visitor* visitor,
                                      VersionSpecificParameters parameters) {
+  QUICHE_DCHECK(name.IsValid());
   MoqtSubscribe message;
   message.full_track_name = name;
   message.subscriber_priority = kDefaultSubscriberPriority;
@@ -400,10 +410,12 @@
     std::optional<uint64_t> end_group,
     std::optional<MoqtPriority> subscriber_priority,
     std::optional<bool> forward, VersionSpecificParameters parameters) {
+  QUICHE_DCHECK(name.IsValid());
   auto it = subscribe_by_name_.find(name);
   if (it == subscribe_by_name_.end()) {
     return false;
   }
+  QUICHE_DCHECK(name.IsValid());
   SubscribeRemoteTrack* track = it->second;
   MoqtSubscribeUpdate subscribe_update;
   subscribe_update.request_id = track->request_id();
@@ -435,10 +447,12 @@
 };
 
 void MoqtSession::Unsubscribe(const FullTrackName& name) {
+  QUICHE_DCHECK(name.IsValid());
   SubscribeRemoteTrack* track = RemoteTrackByName(name);
   if (track == nullptr) {
     return;
   }
+  QUICHE_DCHECK(name.IsValid());
   QUIC_DLOG(INFO) << ENDPOINT << "Sent UNSUBSCRIBE message for " << name;
   MoqtUnsubscribe message;
   message.subscribe_id = track->request_id();
@@ -452,6 +466,7 @@
                         MoqtPriority priority,
                         std::optional<MoqtDeliveryOrder> delivery_order,
                         VersionSpecificParameters parameters) {
+  QUICHE_DCHECK(name.IsValid());
   if (next_request_id_ >= peer_max_request_id_) {
     QUIC_DLOG(INFO) << ENDPOINT << "Tried to send FETCH with ID "
                     << next_request_id_
@@ -485,6 +500,7 @@
                                SubscribeRemoteTrack::Visitor* visitor,
                                uint64_t num_previous_groups,
                                VersionSpecificParameters parameters) {
+  QUICHE_DCHECK(name.IsValid());
   return JoiningFetch(
       name, visitor,
       [this, id = next_request_id_](std::unique_ptr<MoqtFetchTask> fetch_task) {
@@ -509,6 +525,7 @@
                                MoqtPriority priority,
                                std::optional<MoqtDeliveryOrder> delivery_order,
                                VersionSpecificParameters parameters) {
+  QUICHE_DCHECK(name.IsValid());
   if ((next_request_id_ + 2) >= peer_max_request_id_) {
     QUIC_DLOG(INFO) << ENDPOINT << "Tried to send JOINING_FETCH with ID "
                     << (next_request_id_ + 2)
@@ -789,6 +806,7 @@
 
 SubscribeRemoteTrack* MoqtSession::RemoteTrackByName(
     const FullTrackName& name) {
+  QUICHE_DCHECK(name.IsValid());
   auto it = subscribe_by_name_.find(name);
   if (it == subscribe_by_name_.end()) {
     return nullptr;
diff --git a/quiche/quic/moqt/moqt_session.h b/quiche/quic/moqt/moqt_session.h
index ec0efe1..b7e5301 100644
--- a/quiche/quic/moqt/moqt_session.h
+++ b/quiche/quic/moqt/moqt_session.h
@@ -95,22 +95,22 @@
   quic::Perspective perspective() const { return parameters_.perspective; }
 
   // Returns true if message was sent.
-  bool SubscribeAnnounces(FullTrackName track_namespace,
+  bool SubscribeAnnounces(TrackNamespace track_namespace,
                           MoqtOutgoingSubscribeAnnouncesCallback callback,
                           VersionSpecificParameters parameters);
-  bool UnsubscribeAnnounces(FullTrackName track_namespace);
+  bool UnsubscribeAnnounces(TrackNamespace track_namespace);
 
   // Send an ANNOUNCE message for |track_namespace|, and call
   // |announce_callback| when the response arrives. Will fail immediately if
   // there is already an unresolved ANNOUNCE for that namespace.
-  void Announce(FullTrackName track_namespace,
+  void Announce(TrackNamespace track_namespace,
                 MoqtOutgoingAnnounceCallback announce_callback,
                 VersionSpecificParameters parameters);
   // Returns true if message was sent, false if there is no ANNOUNCE to cancel.
-  bool Unannounce(FullTrackName track_namespace);
+  bool Unannounce(TrackNamespace track_namespace);
   // Allows the subscriber to declare it will not subscribe to |track_namespace|
   // anymore.
-  void CancelAnnounce(FullTrackName track_namespace, RequestErrorCode code,
+  void CancelAnnounce(TrackNamespace track_namespace, RequestErrorCode code,
                       absl::string_view reason_phrase);
 
   // Returns true if SUBSCRIBE was sent. If there is already a subscription to
@@ -737,13 +737,13 @@
   // Indexed by track namespace. If the value is not nullptr, no OK or ERROR
   // has been received. The entry is deleted after sending UNANNOUNCE or
   // receiving ANNOUNCE_CANCEL.
-  absl::flat_hash_map<FullTrackName, MoqtOutgoingAnnounceCallback>
+  absl::flat_hash_map<TrackNamespace, MoqtOutgoingAnnounceCallback>
       outgoing_announces_;
   // The value is nullptr after OK or ERROR is received. The entry is deleted
   // when sending UNSUBSCRIBE_ANNOUNCES, to make sure the application doesn't
   // unsubscribe from something that it isn't subscribed to. ANNOUNCEs that
   // result from this subscription use incoming_announce_callback.
-  absl::flat_hash_map<FullTrackName, MoqtOutgoingSubscribeAnnouncesCallback>
+  absl::flat_hash_map<TrackNamespace, MoqtOutgoingSubscribeAnnouncesCallback>
       outgoing_subscribe_announces_;
 
   // The minimum request ID the peer can use that is monotonically increasing.
diff --git a/quiche/quic/moqt/moqt_session_callbacks.h b/quiche/quic/moqt/moqt_session_callbacks.h
index 3b0271a..447bc00 100644
--- a/quiche/quic/moqt/moqt_session_callbacks.h
+++ b/quiche/quic/moqt/moqt_session_callbacks.h
@@ -33,7 +33,7 @@
 // ANNOUNCE sets a value for |parameters|, UNANNOUNCE does not.
 using MoqtIncomingAnnounceCallback =
     quiche::MultiUseCallback<std::optional<MoqtAnnounceErrorReason>(
-        const FullTrackName& track_namespace,
+        const TrackNamespace& track_namespace,
         const std::optional<VersionSpecificParameters>& parameters)>;
 
 // Called whenever SUBSCRIBE_ANNOUNCES or UNSUBSCRIBE_ANNOUNCES is received from
@@ -43,11 +43,11 @@
 // UNSUBSCRIBE_ANNOUNCES does not.
 using MoqtIncomingSubscribeAnnouncesCallback =
     quiche::MultiUseCallback<std::optional<MoqtSubscribeErrorReason>(
-        const FullTrackName& track_namespace,
+        const TrackNamespace& track_namespace,
         std::optional<VersionSpecificParameters> parameters)>;
 
 inline std::optional<MoqtAnnounceErrorReason> DefaultIncomingAnnounceCallback(
-    const FullTrackName& /*track_namespace*/,
+    const TrackNamespace& /*track_namespace*/,
     std::optional<VersionSpecificParameters> /*parameters*/) {
   return std::optional(MoqtAnnounceErrorReason{
       RequestErrorCode::kNotSupported,
@@ -56,7 +56,7 @@
 
 inline std::optional<MoqtSubscribeErrorReason>
 DefaultIncomingSubscribeAnnouncesCallback(
-    const FullTrackName& track_namespace,
+    const TrackNamespace& track_namespace,
     std::optional<VersionSpecificParameters> /*parameters*/) {
   return MoqtSubscribeErrorReason{
       RequestErrorCode::kNotSupported,
diff --git a/quiche/quic/moqt/moqt_session_interface.h b/quiche/quic/moqt/moqt_session_interface.h
index 8f76a19..8e822f9 100644
--- a/quiche/quic/moqt/moqt_session_interface.h
+++ b/quiche/quic/moqt/moqt_session_interface.h
@@ -23,11 +23,11 @@
 // after calling this callback. Alternatively, the application can call
 // Unannounce() to delete the state.
 using MoqtOutgoingAnnounceCallback = quiche::MultiUseCallback<void(
-    FullTrackName track_namespace,
+    TrackNamespace track_namespace,
     std::optional<MoqtAnnounceErrorReason> error)>;
 
 using MoqtOutgoingSubscribeAnnouncesCallback = quiche::SingleUseCallback<void(
-    FullTrackName track_namespace, std::optional<RequestErrorCode> error,
+    TrackNamespace track_namespace, std::optional<RequestErrorCode> error,
     absl::string_view reason)>;
 
 class MoqtSessionInterface {
diff --git a/quiche/quic/moqt/moqt_session_test.cc b/quiche/quic/moqt/moqt_session_test.cc
index 8b4814b..ed23ccd 100644
--- a/quiche/quic/moqt/moqt_session_test.cc
+++ b/quiche/quic/moqt/moqt_session_test.cc
@@ -354,7 +354,7 @@
 
 TEST_F(MoqtSessionTest, AnnounceWithOkAndCancel) {
   testing::MockFunction<void(
-      FullTrackName track_namespace,
+      TrackNamespace track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_resolved_callback;
   std::unique_ptr<MoqtControlParserVisitor> stream_input =
@@ -362,42 +362,42 @@
   EXPECT_CALL(mock_session_, GetStreamById(_)).WillOnce(Return(&mock_stream_));
   EXPECT_CALL(mock_stream_,
               Writev(ControlMessageOfType(MoqtMessageType::kAnnounce), _));
-  session_.Announce(FullTrackName{"foo"},
+  session_.Announce(TrackNamespace("foo"),
                     announce_resolved_callback.AsStdFunction(),
                     VersionSpecificParameters());
 
   MoqtAnnounceOk ok = {
-      /*track_namespace=*/FullTrackName{"foo"},
+      TrackNamespace("foo"),
   };
   EXPECT_CALL(announce_resolved_callback, Call(_, _))
-      .WillOnce([&](FullTrackName track_namespace,
+      .WillOnce([&](TrackNamespace track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
-        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
+        EXPECT_EQ(track_namespace, TrackNamespace("foo"));
         EXPECT_FALSE(error.has_value());
       });
   stream_input->OnAnnounceOkMessage(ok);
 
   MoqtAnnounceCancel cancel = {
-      /*track_namespace=*/FullTrackName{"foo"},
-      /*error_code=*/RequestErrorCode::kInternalError,
+      TrackNamespace("foo"),
+      RequestErrorCode::kInternalError,
       /*reason_phrase=*/"Test error",
   };
   EXPECT_CALL(announce_resolved_callback, Call(_, _))
-      .WillOnce([&](FullTrackName track_namespace,
+      .WillOnce([&](TrackNamespace track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
-        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
+        EXPECT_EQ(track_namespace, TrackNamespace("foo"));
         ASSERT_TRUE(error.has_value());
         EXPECT_EQ(error->error_code, RequestErrorCode::kInternalError);
         EXPECT_EQ(error->reason_phrase, "Test error");
       });
   stream_input->OnAnnounceCancelMessage(cancel);
   // State is gone.
-  EXPECT_FALSE(session_.Unannounce(FullTrackName{"foo"}));
+  EXPECT_FALSE(session_.Unannounce(TrackNamespace("foo")));
 }
 
 TEST_F(MoqtSessionTest, AnnounceWithOkAndUnannounce) {
   testing::MockFunction<void(
-      FullTrackName track_namespace,
+      TrackNamespace track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_resolved_callback;
   std::unique_ptr<MoqtControlParserVisitor> stream_input =
@@ -405,17 +405,17 @@
   EXPECT_CALL(mock_session_, GetStreamById(_)).WillOnce(Return(&mock_stream_));
   EXPECT_CALL(mock_stream_,
               Writev(ControlMessageOfType(MoqtMessageType::kAnnounce), _));
-  session_.Announce(FullTrackName{"foo"},
+  session_.Announce(TrackNamespace{"foo"},
                     announce_resolved_callback.AsStdFunction(),
                     VersionSpecificParameters());
 
   MoqtAnnounceOk ok = {
-      /*track_namespace=*/FullTrackName{"foo"},
+      TrackNamespace{"foo"},
   };
   EXPECT_CALL(announce_resolved_callback, Call(_, _))
-      .WillOnce([&](FullTrackName track_namespace,
+      .WillOnce([&](TrackNamespace track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
-        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
+        EXPECT_EQ(track_namespace, TrackNamespace{"foo"});
         EXPECT_FALSE(error.has_value());
       });
   stream_input->OnAnnounceOkMessage(ok);
@@ -423,14 +423,14 @@
   EXPECT_CALL(mock_session_, GetStreamById(_)).WillOnce(Return(&mock_stream_));
   EXPECT_CALL(mock_stream_,
               Writev(ControlMessageOfType(MoqtMessageType::kUnannounce), _));
-  session_.Unannounce(FullTrackName{"foo"});
+  session_.Unannounce(TrackNamespace{"foo"});
   // State is gone.
-  EXPECT_FALSE(session_.Unannounce(FullTrackName{"foo"}));
+  EXPECT_FALSE(session_.Unannounce(TrackNamespace{"foo"}));
 }
 
 TEST_F(MoqtSessionTest, AnnounceWithError) {
   testing::MockFunction<void(
-      FullTrackName track_namespace,
+      TrackNamespace track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_resolved_callback;
   std::unique_ptr<MoqtControlParserVisitor> stream_input =
@@ -438,26 +438,26 @@
   EXPECT_CALL(mock_session_, GetStreamById(_)).WillOnce(Return(&mock_stream_));
   EXPECT_CALL(mock_stream_,
               Writev(ControlMessageOfType(MoqtMessageType::kAnnounce), _));
-  session_.Announce(FullTrackName{"foo"},
+  session_.Announce(TrackNamespace{"foo"},
                     announce_resolved_callback.AsStdFunction(),
                     VersionSpecificParameters());
 
   MoqtAnnounceError error = {
-      /*track_namespace=*/FullTrackName{"foo"},
+      /*track_namespace=*/TrackNamespace{"foo"},
       /*error_code=*/RequestErrorCode::kInternalError,
       /*reason_phrase=*/"Test error",
   };
   EXPECT_CALL(announce_resolved_callback, Call(_, _))
-      .WillOnce([&](FullTrackName track_namespace,
+      .WillOnce([&](TrackNamespace track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
-        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
+        EXPECT_EQ(track_namespace, TrackNamespace{"foo"});
         ASSERT_TRUE(error.has_value());
         EXPECT_EQ(error->error_code, RequestErrorCode::kInternalError);
         EXPECT_EQ(error->reason_phrase, "Test error");
       });
   stream_input->OnAnnounceErrorMessage(error);
   // State is gone.
-  EXPECT_FALSE(session_.Unannounce(FullTrackName{"foo"}));
+  EXPECT_FALSE(session_.Unannounce(TrackNamespace{"foo"}));
 }
 
 TEST_F(MoqtSessionTest, AsynchronousSubscribeReturnsOk) {
@@ -890,7 +890,7 @@
 }
 
 TEST_F(MoqtSessionTest, ReplyToAnnounceWithOkThenUnannounce) {
-  FullTrackName track_namespace{"foo"};
+  TrackNamespace track_namespace{"foo"};
   std::unique_ptr<MoqtControlParserVisitor> stream_input =
       MoqtSessionPeer::CreateControlStream(&session_, &mock_stream_);
   auto parameters = std::make_optional<VersionSpecificParameters>(
@@ -916,7 +916,7 @@
 }
 
 TEST_F(MoqtSessionTest, ReplyToAnnounceWithOkThenAnnounceCancel) {
-  FullTrackName track_namespace{"foo"};
+  TrackNamespace track_namespace{"foo"};
 
   std::unique_ptr<MoqtControlParserVisitor> stream_input =
       MoqtSessionPeer::CreateControlStream(&session_, &mock_stream_);
@@ -943,7 +943,7 @@
 }
 
 TEST_F(MoqtSessionTest, ReplyToAnnounceWithError) {
-  FullTrackName track_namespace{"foo"};
+  TrackNamespace track_namespace{"foo"};
 
   std::unique_ptr<MoqtControlParserVisitor> stream_input =
       MoqtSessionPeer::CreateControlStream(&session_, &mock_stream_);
@@ -971,18 +971,17 @@
 TEST_F(MoqtSessionTest, SubscribeAnnouncesLifeCycle) {
   std::unique_ptr<MoqtControlParserVisitor> stream_input =
       MoqtSessionPeer::CreateControlStream(&session_, &mock_stream_);
-  FullTrackName track_namespace("foo", "bar");
-  track_namespace.NameToNamespace();
+  TrackNamespace track_namespace("foo");
   bool got_callback = false;
   EXPECT_CALL(
       mock_stream_,
       Writev(ControlMessageOfType(MoqtMessageType::kSubscribeAnnounces), _));
   session_.SubscribeAnnounces(
       track_namespace,
-      [&](const FullTrackName& ftn, std::optional<RequestErrorCode> error,
+      [&](const TrackNamespace& ns, std::optional<RequestErrorCode> error,
           absl::string_view reason) {
         got_callback = true;
-        EXPECT_EQ(track_namespace, ftn);
+        EXPECT_EQ(track_namespace, ns);
         EXPECT_FALSE(error.has_value());
         EXPECT_EQ(reason, "");
       },
@@ -1002,26 +1001,25 @@
 TEST_F(MoqtSessionTest, SubscribeAnnouncesError) {
   std::unique_ptr<MoqtControlParserVisitor> stream_input =
       MoqtSessionPeer::CreateControlStream(&session_, &mock_stream_);
-  FullTrackName track_namespace("foo", "bar");
-  track_namespace.NameToNamespace();
+  TrackNamespace track_namespace("foo");
   bool got_callback = false;
   EXPECT_CALL(
       mock_stream_,
       Writev(ControlMessageOfType(MoqtMessageType::kSubscribeAnnounces), _));
   session_.SubscribeAnnounces(
       track_namespace,
-      [&](const FullTrackName& ftn, std::optional<RequestErrorCode> error,
+      [&](const TrackNamespace& ns, std::optional<RequestErrorCode> error,
           absl::string_view reason) {
         got_callback = true;
-        EXPECT_EQ(track_namespace, ftn);
+        EXPECT_EQ(track_namespace, ns);
         ASSERT_TRUE(error.has_value());
         EXPECT_EQ(*error, RequestErrorCode::kInvalidRange);
         EXPECT_EQ(reason, "deadbeef");
       },
       VersionSpecificParameters());
   MoqtSubscribeAnnouncesError error = {
-      /*track_namespace=*/track_namespace,
-      /*error_code=*/RequestErrorCode::kInvalidRange,
+      track_namespace,
+      RequestErrorCode::kInvalidRange,
       /*reason_phrase=*/"deadbeef",
   };
   stream_input->OnSubscribeAnnouncesErrorMessage(error);
@@ -2559,7 +2557,7 @@
 }
 
 TEST_F(MoqtSessionTest, IncomingSubscribeAnnounces) {
-  FullTrackName track_namespace = FullTrackName{"foo"};
+  TrackNamespace track_namespace = TrackNamespace{"foo"};
   auto parameters = std::make_optional<VersionSpecificParameters>(
       AuthTokenType::kOutOfBand, "foo");
   MoqtSubscribeAnnounces announces = {
@@ -2577,7 +2575,7 @@
       Writev(ControlMessageOfType(MoqtMessageType::kSubscribeAnnouncesOk), _));
   stream_input->OnSubscribeAnnouncesMessage(announces);
   MoqtUnsubscribeAnnounces unsubscribe_announces = {
-      /*track_namespace=*/FullTrackName{"foo"},
+      TrackNamespace{"foo"},
   };
   EXPECT_CALL(session_callbacks_.incoming_subscribe_announces_callback,
               Call(track_namespace, std::optional<VersionSpecificParameters>()))
@@ -2586,7 +2584,7 @@
 }
 
 TEST_F(MoqtSessionTest, IncomingSubscribeAnnouncesWithError) {
-  FullTrackName track_namespace = FullTrackName{"foo"};
+  TrackNamespace track_namespace{"foo"};
   auto parameters = std::make_optional<VersionSpecificParameters>(
       AuthTokenType::kOutOfBand, "foo");
   MoqtSubscribeAnnounces announces = {
@@ -3129,18 +3127,18 @@
                                                &remote_track_visitor,
                                                VersionSpecificParameters()));
   EXPECT_FALSE(session_.SubscribeAnnounces(
-      FullTrackName{"foo"},
-      +[](FullTrackName /*track_namespace*/,
+      TrackNamespace{"foo"},
+      +[](TrackNamespace /*track_namespace*/,
           std::optional<RequestErrorCode> /*error*/,
           absl::string_view /*reason*/) {},
       VersionSpecificParameters()));
   session_.Announce(
-      FullTrackName{"foo"},
-      +[](FullTrackName /*track_namespace*/,
+      TrackNamespace{"foo"},
+      +[](TrackNamespace /*track_namespace*/,
           std::optional<MoqtAnnounceErrorReason> /*error*/) {},
       VersionSpecificParameters());
   EXPECT_FALSE(session_.Fetch(
-      FullTrackName{"foo", "bar"},
+      FullTrackName{TrackNamespace("foo"), "bar"},
       +[](std::unique_ptr<MoqtFetchTask> /*fetch_task*/) {}, Location(0, 0), 5,
       std::nullopt, 127, std::nullopt, VersionSpecificParameters()));
   // Error on additional GOAWAY.
@@ -3171,7 +3169,7 @@
   EXPECT_CALL(mock_stream_,
               Writev(ControlMessageOfType(MoqtMessageType::kAnnounceError), _));
   stream_input->OnAnnounceMessage(
-      MoqtAnnounce(FullTrackName("foo", "bar"), VersionSpecificParameters()));
+      MoqtAnnounce(TrackNamespace("foo"), VersionSpecificParameters()));
   EXPECT_CALL(mock_stream_,
               Writev(ControlMessageOfType(MoqtMessageType::kFetchError), _));
   MoqtFetch fetch = DefaultFetch();
@@ -3182,26 +3180,26 @@
       Writev(ControlMessageOfType(MoqtMessageType::kSubscribeAnnouncesError),
              _));
   stream_input->OnSubscribeAnnouncesMessage(
-      MoqtSubscribeAnnounces(FullTrackName("foo", "bar")));
+      MoqtSubscribeAnnounces(TrackNamespace("foo")));
   // Block all outgoing SUBSCRIBE, ANNOUNCE, GOAWAY,etc.
   EXPECT_CALL(mock_stream_, Writev).Times(0);
   MockSubscribeRemoteTrackVisitor remote_track_visitor;
-  EXPECT_FALSE(session_.SubscribeCurrentObject(FullTrackName("foo", "bar"),
-                                               &remote_track_visitor,
-                                               VersionSpecificParameters()));
+  EXPECT_FALSE(session_.SubscribeCurrentObject(
+      FullTrackName(TrackNamespace("foo"), "bar"), &remote_track_visitor,
+      VersionSpecificParameters()));
   EXPECT_FALSE(session_.SubscribeAnnounces(
-      FullTrackName{"foo"},
-      +[](FullTrackName /*track_namespace*/,
+      TrackNamespace{"foo"},
+      +[](TrackNamespace /*track_namespace*/,
           std::optional<RequestErrorCode> /*error*/,
           absl::string_view /*reason*/) {},
       VersionSpecificParameters()));
   session_.Announce(
-      FullTrackName{"foo"},
-      +[](FullTrackName /*track_namespace*/,
+      TrackNamespace{"foo"},
+      +[](TrackNamespace /*track_namespace*/,
           std::optional<MoqtAnnounceErrorReason> /*error*/) {},
       VersionSpecificParameters());
   EXPECT_FALSE(session_.Fetch(
-      FullTrackName{"foo", "bar"},
+      FullTrackName(TrackNamespace("foo"), "bar"),
       +[](std::unique_ptr<MoqtFetchTask> /*fetch_task*/) {}, Location(0, 0), 5,
       std::nullopt, 127, std::nullopt, VersionSpecificParameters()));
   session_.GoAway("");
diff --git a/quiche/quic/moqt/test_tools/moqt_test_message.h b/quiche/quic/moqt/test_tools/moqt_test_message.h
index cf243c9..6614417 100644
--- a/quiche/quic/moqt/test_tools/moqt_test_message.h
+++ b/quiche/quic/moqt/test_tools/moqt_test_message.h
@@ -536,7 +536,7 @@
   MoqtSubscribe subscribe_ = {
       /*subscribe_id=*/1,
       /*track_alias=*/2,
-      /*full_track_name=*/FullTrackName({"foo", "abcd"}),
+      FullTrackName("foo", "abcd"),
       /*subscriber_priority=*/0x20,
       /*group_order=*/MoqtDeliveryOrder::kDescending,
       /*forward=*/true,
@@ -839,7 +839,7 @@
   };
 
   MoqtAnnounce announce_ = {
-      /*track_namespace=*/FullTrackName{"foo"},
+      TrackNamespace{"foo"},
       VersionSpecificParameters(AuthTokenType::kOutOfBand, "bar"),
   };
 };
@@ -872,7 +872,7 @@
   };
 
   MoqtAnnounceOk announce_ok_ = {
-      /*track_namespace=*/FullTrackName{"foo"},
+      TrackNamespace("foo"),
   };
 };
 
@@ -914,8 +914,8 @@
   };
 
   MoqtAnnounceError announce_error_ = {
-      /*track_namespace=*/FullTrackName{"foo"},
-      /*error_code=*/RequestErrorCode::kNotSupported,
+      TrackNamespace("foo"),
+      RequestErrorCode::kNotSupported,
       /*reason_phrase=*/"bar",
   };
 };
@@ -958,8 +958,8 @@
   };
 
   MoqtAnnounceCancel announce_cancel_ = {
-      /*track_namespace=*/FullTrackName{"foo"},
-      /*error_code=*/RequestErrorCode::kNotSupported,
+      TrackNamespace("foo"),
+      RequestErrorCode::kNotSupported,
       /*reason_phrase=*/"bar",
   };
 };
@@ -999,7 +999,7 @@
   };
 
   MoqtTrackStatusRequest track_status_request_ = {
-      /*full_track_name=*/FullTrackName({"foo", "abcd"}),
+      FullTrackName("foo", "abcd"),
       VersionSpecificParameters(AuthTokenType::kOutOfBand, "bar"),
   };
 };
@@ -1031,7 +1031,7 @@
   };
 
   MoqtUnannounce unannounce_ = {
-      /*track_namespace=*/FullTrackName{"foo"},
+      TrackNamespace("foo"),
   };
 };
 
@@ -1084,7 +1084,7 @@
   };
 
   MoqtTrackStatus track_status_ = {
-      /*full_track_name=*/FullTrackName({"foo", "abcd"}),
+      FullTrackName("foo", "abcd"),
       /*status_code=*/MoqtTrackStatusCode::kInProgress,
       /*last_group=*/12,
       /*last_object=*/20,
@@ -1157,7 +1157,7 @@
   };
 
   MoqtSubscribeAnnounces subscribe_namespace_ = {
-      /*track_namespace=*/FullTrackName{"foo"},
+      TrackNamespace("foo"),
       VersionSpecificParameters(AuthTokenType::kOutOfBand, "bar"),
   };
 };
@@ -1189,7 +1189,7 @@
   };
 
   MoqtSubscribeAnnouncesOk subscribe_namespace_ok_ = {
-      /*track_namespace=*/FullTrackName{"foo"},
+      TrackNamespace("foo"),
   };
 };
 
@@ -1231,7 +1231,7 @@
   };
 
   MoqtSubscribeAnnouncesError subscribe_namespace_error_ = {
-      /*track_namespace=*/FullTrackName{"foo"},
+      TrackNamespace("foo"),
       /*error_code=*/RequestErrorCode::kUnauthorized,
       /*reason_phrase=*/"bar",
   };
@@ -1264,7 +1264,7 @@
   };
 
   MoqtUnsubscribeAnnounces unsubscribe_namespace_ = {
-      /*track_namespace=*/FullTrackName{"foo"},
+      TrackNamespace("foo"),
   };
 };
 
@@ -1404,7 +1404,7 @@
       /*subscriber_priority=*/2,
       /*group_order=*/MoqtDeliveryOrder::kAscending,
       /*joining_fetch=*/std::optional<JoiningFetch>(),
-      /*full_track_name=*/FullTrackName{"foo", "bar"},
+      FullTrackName("foo", "bar"),
       /*start_object=*/Location{1, 2},
       /*end_group=*/5,
       /*end_object=*/6,
@@ -1503,7 +1503,7 @@
       /*group_order=*/MoqtDeliveryOrder::kAscending,
       /*joining_fetch=*/JoiningFetch{2, 2},
       /* the next four are ignored for joining fetches*/
-      /*full_track_name=*/FullTrackName{"foo", "bar"},
+      FullTrackName("foo", "bar"),
       /*start_object=*/Location{1, 2},
       /*end_group=*/5,
       /*end_object=*/6,
diff --git a/quiche/quic/moqt/tools/chat_client.cc b/quiche/quic/moqt/tools/chat_client.cc
index eb7ac99..f22ffda 100644
--- a/quiche/quic/moqt/tools/chat_client.cc
+++ b/quiche/quic/moqt/tools/chat_client.cc
@@ -39,7 +39,7 @@
 namespace moqt::moq_chat {
 
 std::optional<MoqtAnnounceErrorReason> ChatClient::OnIncomingAnnounce(
-    const moqt::FullTrackName& track_namespace,
+    const moqt::TrackNamespace& track_namespace,
     std::optional<VersionSpecificParameters> parameters) {
   if (track_namespace == GetUserNamespace(my_track_name_)) {
     // Ignore ANNOUNCE for my own track.
@@ -217,7 +217,7 @@
   publisher_.Add(queue_);
   session_->set_publisher(&publisher_);
   MoqtOutgoingAnnounceCallback announce_callback =
-      [this](FullTrackName track_namespace,
+      [this](TrackNamespace track_namespace,
              std::optional<MoqtAnnounceErrorReason> reason) {
         if (reason.has_value()) {
           std::cout << "ANNOUNCE rejected, " << reason->reason_phrase << "\n";
@@ -236,7 +236,7 @@
   // Send SUBSCRIBE_ANNOUNCE. Pop 3 levels of namespace to get to {moq-chat,
   // chat-id}
   MoqtOutgoingSubscribeAnnouncesCallback subscribe_announces_callback =
-      [this](FullTrackName track_namespace,
+      [this](TrackNamespace track_namespace,
              std::optional<RequestErrorCode> error, absl::string_view reason) {
         if (error.has_value()) {
           std::cout << "SUBSCRIBE_ANNOUNCES rejected, " << reason << "\n";
diff --git a/quiche/quic/moqt/tools/chat_client.h b/quiche/quic/moqt/tools/chat_client.h
index 90f8727..93d4ae5 100644
--- a/quiche/quic/moqt/tools/chat_client.h
+++ b/quiche/quic/moqt/tools/chat_client.h
@@ -127,7 +127,7 @@
   void RunEventLoop() { event_loop_->RunEventLoopOnce(kChatEventLoopDuration); }
   // Callback for incoming announces.
   std::optional<MoqtAnnounceErrorReason> OnIncomingAnnounce(
-      const moqt::FullTrackName& track_namespace,
+      const moqt::TrackNamespace& track_namespace,
       std::optional<VersionSpecificParameters> parameters);
 
   // Basic session information
diff --git a/quiche/quic/moqt/tools/chat_server.cc b/quiche/quic/moqt/tools/chat_server.cc
index 08bcd62..c3d9b74 100644
--- a/quiche/quic/moqt/tools/chat_server.cc
+++ b/quiche/quic/moqt/tools/chat_server.cc
@@ -26,7 +26,7 @@
 
 std::optional<MoqtAnnounceErrorReason>
 ChatServer::ChatServerSessionHandler::OnIncomingAnnounce(
-    const moqt::FullTrackName& track_namespace,
+    const moqt::TrackNamespace& track_namespace,
     std::optional<VersionSpecificParameters> parameters) {
   if (track_name_.has_value() &&
       GetUserNamespace(*track_name_) != track_namespace) {
@@ -58,7 +58,7 @@
 }
 
 void ChatServer::ChatServerSessionHandler::OnOutgoingAnnounceReply(
-    FullTrackName track_namespace,
+    TrackNamespace track_namespace,
     std::optional<MoqtAnnounceErrorReason> error_message) {
   // Log the result; the server doesn't really care.
   std::cout << "ANNOUNCE for " << track_namespace.ToString();
@@ -83,7 +83,7 @@
         }
       };
   session_->callbacks().incoming_subscribe_announces_callback =
-      [this](const moqt::FullTrackName& chat_namespace,
+      [this](const moqt::TrackNamespace& chat_namespace,
              std::optional<VersionSpecificParameters> parameters) {
         if (parameters.has_value()) {
           subscribed_namespaces_.insert(chat_namespace);
@@ -198,10 +198,8 @@
   user_queues_[track_name] = std::make_shared<MoqtLiveRelayQueue>(
       track_name, MoqtForwardingPreference::kSubgroup);
   publisher_.Add(user_queues_[track_name]);
-  FullTrackName track_namespace = track_name;
-  track_namespace.NameToNamespace();
   for (auto& session : sessions_) {
-    session.AnnounceIfSubscribed(track_namespace);
+    session.AnnounceIfSubscribed(track_name.track_namespace());
   }
 }
 
@@ -213,7 +211,7 @@
   user_queues_[track_name]->RemoveAllSubscriptions();
   user_queues_.erase(track_name);
   publisher_.Delete(track_name);
-  FullTrackName track_namespace = GetUserNamespace(track_name);
+  TrackNamespace track_namespace = GetUserNamespace(track_name);
   for (auto& session : sessions_) {
     session.UnannounceIfSubscribed(track_namespace);
   }
diff --git a/quiche/quic/moqt/tools/chat_server.h b/quiche/quic/moqt/tools/chat_server.h
index d8c8731..90e83e9 100644
--- a/quiche/quic/moqt/tools/chat_server.h
+++ b/quiche/quic/moqt/tools/chat_server.h
@@ -64,8 +64,9 @@
       it_ = it;
     }
 
-    void AnnounceIfSubscribed(FullTrackName track_namespace) {
-      for (const FullTrackName& subscribed_namespace : subscribed_namespaces_) {
+    void AnnounceIfSubscribed(TrackNamespace track_namespace) {
+      for (const TrackNamespace& subscribed_namespace :
+           subscribed_namespaces_) {
         if (track_namespace.InNamespace(subscribed_namespace)) {
           session_->Announce(
               track_namespace,
@@ -78,8 +79,9 @@
       }
     }
 
-    void UnannounceIfSubscribed(FullTrackName track_namespace) {
-      for (const FullTrackName& subscribed_namespace : subscribed_namespaces_) {
+    void UnannounceIfSubscribed(TrackNamespace track_namespace) {
+      for (const TrackNamespace& subscribed_namespace :
+           subscribed_namespaces_) {
         if (track_namespace.InNamespace(subscribed_namespace)) {
           session_->Unannounce(track_namespace);
           return;
@@ -90,10 +92,10 @@
    private:
     // Callback for incoming announces.
     std::optional<MoqtAnnounceErrorReason> OnIncomingAnnounce(
-        const moqt::FullTrackName& track_namespace,
+        const moqt::TrackNamespace& track_namespace,
         std::optional<VersionSpecificParameters> parameters);
     void OnOutgoingAnnounceReply(
-        FullTrackName track_namespace,
+        TrackNamespace track_namespace,
         std::optional<MoqtAnnounceErrorReason> error_message);
 
     MoqtSession* session_;  // Not owned.
@@ -101,7 +103,7 @@
     // in theory there could be multiple users on one session.
     std::optional<FullTrackName> track_name_;
     ChatServer* server_;  // Not owned.
-    absl::flat_hash_set<FullTrackName> subscribed_namespaces_;
+    absl::flat_hash_set<TrackNamespace> subscribed_namespaces_;
     // The iterator of this entry in ChatServer::sessions_, so it can destroy
     // itself later.
     std::list<ChatServerSessionHandler>::const_iterator it_;
diff --git a/quiche/quic/moqt/tools/moq_chat.cc b/quiche/quic/moqt/tools/moq_chat.cc
index 0ec5b64..ddf349c 100644
--- a/quiche/quic/moqt/tools/moq_chat.cc
+++ b/quiche/quic/moqt/tools/moq_chat.cc
@@ -17,12 +17,12 @@
 
 bool IsValidPath(absl::string_view path) { return path == kWebtransPath; }
 
-bool IsValidNamespace(const FullTrackName& track_namespace) {
-  return track_namespace.tuple().size() == kFullPathLength - 1 &&
+bool IsValidNamespace(const TrackNamespace& track_namespace) {
+  return track_namespace.number_of_elements() == kFullPathLength - 1 &&
          track_namespace.tuple()[0] == kBasePath;
 }
 
-bool IsValidChatNamespace(const FullTrackName& track_namespace) {
+bool IsValidChatNamespace(const TrackNamespace& track_namespace) {
   return track_namespace.tuple().size() == 2 &&
          track_namespace.tuple()[0] == kBasePath;
 }
@@ -30,47 +30,50 @@
 FullTrackName ConstructTrackName(absl::string_view chat_id,
                                  absl::string_view username,
                                  absl::string_view device_id) {
-  return FullTrackName{kBasePath,
-                       chat_id,
-                       username,
-                       device_id,
-                       absl::StrCat(ToUnixSeconds(::absl::Now())),
-                       kNameField};
+  return FullTrackName(
+      TrackNamespace({kBasePath, chat_id, username, device_id,
+                      absl::StrCat(ToUnixSeconds(::absl::Now()))}),
+      kNameField);
 }
 
 std::optional<FullTrackName> ConstructTrackNameFromNamespace(
-    const FullTrackName& track_namespace, absl::string_view chat_id) {
-  if (track_namespace.tuple().size() != kFullPathLength - 1) {
+    const TrackNamespace& track_namespace, absl::string_view chat_id) {
+  if (track_namespace.number_of_elements() != kFullPathLength - 1) {
     return std::nullopt;
   }
   if (track_namespace.tuple()[0] != kBasePath ||
       track_namespace.tuple()[1] != chat_id) {
     return std::nullopt;
   }
-  FullTrackName track_name = track_namespace;
-  track_name.AddElement(kNameField);
-  return track_name;
+  return FullTrackName(track_namespace, kNameField);
 }
 
+absl::string_view GetUsername(const TrackNamespace& track_namespace) {
+  QUICHE_DCHECK(track_namespace.number_of_elements() > 2);
+  return track_namespace.tuple()[2];
+}
 absl::string_view GetUsername(const FullTrackName& track_name) {
-  QUICHE_DCHECK(track_name.tuple().size() > 2);
-  return track_name.tuple()[2];
+  return GetUsername(track_name.track_namespace());
 }
 
+absl::string_view GetChatId(const TrackNamespace& track_namespace) {
+  QUICHE_DCHECK(track_namespace.number_of_elements() > 1);
+  return track_namespace.tuple()[1];
+}
 absl::string_view GetChatId(const FullTrackName& track_name) {
-  QUICHE_DCHECK(track_name.tuple().size() > 1);
-  return track_name.tuple()[1];
+  return GetChatId(track_name.track_namespace());
 }
 
-FullTrackName GetUserNamespace(const FullTrackName& track_name) {
-  QUICHE_DCHECK(track_name.tuple().size() == kFullPathLength);
-  FullTrackName track_namespace = track_name;
-  track_namespace.NameToNamespace();
-  return track_namespace;
+const TrackNamespace& GetUserNamespace(const FullTrackName& track_name) {
+  return track_name.track_namespace();
 }
 
-FullTrackName GetChatNamespace(const FullTrackName& track_name) {
-  return FullTrackName{track_name.tuple()[0], track_name.tuple()[1]};
+TrackNamespace GetChatNamespace(const TrackNamespace& track_namespace) {
+  return TrackNamespace(
+      {track_namespace.tuple()[0], track_namespace.tuple()[1]});
+}
+TrackNamespace GetChatNamespace(const FullTrackName& track_name) {
+  return GetChatNamespace(track_name.track_namespace());
 }
 
 }  // namespace moqt::moq_chat
diff --git a/quiche/quic/moqt/tools/moq_chat.h b/quiche/quic/moqt/tools/moq_chat.h
index 02f23e4..3478b8b 100644
--- a/quiche/quic/moqt/tools/moq_chat.h
+++ b/quiche/quic/moqt/tools/moq_chat.h
@@ -30,8 +30,8 @@
 // Verifies that the WebTransport path matches the spec.
 bool IsValidPath(absl::string_view path);
 
-bool IsValidTrackNamespace(const FullTrackName& track_namespace);
-bool IsValidChatNamespace(const FullTrackName& track_namespace);
+bool IsValidTrackNamespace(const TrackNamespace& track_namespace);
+bool IsValidChatNamespace(const TrackNamespace& track_namespace);
 
 // Given a chat-id and username, returns a full track name for moq-chat.
 FullTrackName ConstructTrackName(absl::string_view chat_id,
@@ -42,16 +42,19 @@
 // is syntactically incorrect, or does not match the expected value of
 // |chat-id|, returns nullopt
 std::optional<FullTrackName> ConstructTrackNameFromNamespace(
-    const FullTrackName& track_namespace, absl::string_view chat_id);
+    const TrackNamespace& track_namespace, absl::string_view chat_id);
 
 // Strips "chat" from the end of |track_name| to use in ANNOUNCE.
-FullTrackName GetUserNamespace(const FullTrackName& track_name);
+const TrackNamespace& GetUserNamespace(const FullTrackName& track_name);
 
 // Returns {"moq-chat", chat-id}, useful for SUBSCRIBE_ANNOUNCES.
-FullTrackName GetChatNamespace(const FullTrackName& track_name);
+TrackNamespace GetChatNamespace(const TrackNamespace& track_name);
+TrackNamespace GetChatNamespace(const FullTrackName& track_name);
 
+absl::string_view GetUsername(const TrackNamespace& track_namespace);
 absl::string_view GetUsername(const FullTrackName& track_name);
 
+absl::string_view GetChatId(const TrackNamespace& track_namespace);
 absl::string_view GetChatId(const FullTrackName& track_name);
 
 }  // namespace moq_chat
diff --git a/quiche/quic/moqt/tools/moq_chat_test.cc b/quiche/quic/moqt/tools/moq_chat_test.cc
index d402054..0579f94 100644
--- a/quiche/quic/moqt/tools/moq_chat_test.cc
+++ b/quiche/quic/moqt/tools/moq_chat_test.cc
@@ -26,42 +26,38 @@
   EXPECT_EQ(GetChatId(name), "chat-id");
   EXPECT_EQ(GetUsername(name), "user");
   // Check that the namespace passes validation.
-  name.NameToNamespace();
-  EXPECT_TRUE(ConstructTrackNameFromNamespace(name, "chat-id").has_value());
+  EXPECT_TRUE(ConstructTrackNameFromNamespace(GetUserNamespace(name), "chat-id")
+                  .has_value());
 }
 
 TEST_F(MoqChatTest, InvalidNamespace) {
-  FullTrackName track_namespace{kBasePath, "chat-id", "username", "device",
-                                "timestamp"};
+  TrackNamespace track_namespace(
+      {kBasePath, "chat-id", "username", "device", "timestamp"});
   // Wrong chat ID.
   EXPECT_FALSE(
       ConstructTrackNameFromNamespace(track_namespace, "chat-id2").has_value());
-  // Namespace includes name
-  track_namespace.AddElement("chat");
-  EXPECT_FALSE(
-      ConstructTrackNameFromNamespace(track_namespace, "chat-id").has_value());
-  track_namespace.NameToNamespace();  // Restore to correct value.
   // Namespace too short.
-  track_namespace.NameToNamespace();
+  TrackNamespace short_base_path({"moq-chat2", "chat-id", "user", "device"});
   EXPECT_FALSE(
-      ConstructTrackNameFromNamespace(track_namespace, "chat-id").has_value());
+      ConstructTrackNameFromNamespace(short_base_path, "chat-id").has_value());
   track_namespace.AddElement("chat");  // Restore to correct value.
   // Base Path is wrong.
-  FullTrackName bad_base_path{"moq-chat2", "chat-id", "user", "device",
-                              "timestamp"};
+  TrackNamespace bad_base_path(
+      {"moq-chat2", "chat-id", "user", "device", "timestamp"});
   EXPECT_FALSE(
       ConstructTrackNameFromNamespace(bad_base_path, "chat-id").has_value());
 }
 
 TEST_F(MoqChatTest, Queries) {
-  FullTrackName local_name{kBasePath, "chat-id",   "user",
-                           "device",  "timestamp", kNameField};
+  FullTrackName local_name(
+      TrackNamespace({kBasePath, "chat-id", "user", "device", "timestamp"}),
+      kNameField);
   EXPECT_EQ(GetChatId(local_name), "chat-id");
   EXPECT_EQ(GetUsername(local_name), "user");
-  FullTrackName track_namespace{"moq-chat", "chat-id", "user", "device",
-                                "timestamp"};
+  TrackNamespace track_namespace(
+      {"moq-chat", "chat-id", "user", "device", "timestamp"});
   EXPECT_EQ(GetUserNamespace(local_name), track_namespace);
-  FullTrackName chat_namespace{"moq-chat", "chat-id"};
+  TrackNamespace chat_namespace({kBasePath, "chat-id"});
   EXPECT_EQ(GetChatNamespace(local_name), chat_namespace);
 }
 
diff --git a/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc b/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc
index 4b4aefc..acc64a0 100644
--- a/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc
+++ b/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc
@@ -82,7 +82,7 @@
   return absl::ascii_isalnum(c) || c == '-' || c == '_';
 }
 
-bool IsValidTrackNamespace(FullTrackName track_namespace) {
+bool IsValidTrackNamespace(TrackNamespace track_namespace) {
   for (const auto& element : track_namespace.tuple()) {
     if (!absl::c_all_of(element, IsValidTrackNamespaceChar)) {
       return false;
@@ -91,8 +91,8 @@
   return true;
 }
 
-FullTrackName CleanUpTrackNamespace(FullTrackName track_namespace) {
-  FullTrackName output;
+TrackNamespace CleanUpTrackNamespace(TrackNamespace track_namespace) {
+  TrackNamespace output;
   for (auto& it : track_namespace.tuple()) {
     std::string element = it;
     for (char& c : element) {
@@ -117,7 +117,7 @@
 
   // TODO(martinduke): Handle when |announce| is false (UNANNOUNCE).
   std::optional<MoqtAnnounceErrorReason> OnAnnounceReceived(
-      FullTrackName track_namespace,
+      TrackNamespace track_namespace,
       std::optional<VersionSpecificParameters> /*parameters*/) {
     if (!IsValidTrackNamespace(track_namespace) &&
         !quiche::GetQuicheCommandLineFlag(
@@ -153,8 +153,7 @@
     std::vector<absl::string_view> tracks_to_subscribe =
         absl::StrSplit(track_list, ',', absl::AllowEmpty());
     for (absl::string_view track : tracks_to_subscribe) {
-      FullTrackName full_track_name = track_namespace;
-      full_track_name.AddElement(track);
+      FullTrackName full_track_name(track_namespace, track);
       session_->JoiningFetch(full_track_name, &it->second, 0,
                              VersionSpecificParameters());
     }
@@ -185,8 +184,9 @@
                           MoqtPriority /*publisher_priority*/,
                           MoqtObjectStatus /*status*/, absl::string_view object,
                           bool /*end_of_message*/) override {
-      std::string file_name = absl::StrCat(sequence.group, "-", sequence.object,
-                                           ".", full_track_name.tuple().back());
+      std::string file_name =
+          absl::StrCat(sequence.group, "-", sequence.object, ".",
+                       full_track_name.track_namespace().tuple().back());
       std::string file_path = quiche::JoinPath(directory_, file_name);
       std::ofstream output(file_path, std::ios::binary | std::ios::ate);
       output.write(object.data(), object.size());
@@ -201,7 +201,7 @@
 
   MoqtSession* session_;  // Not owned.
   std::string output_root_;
-  absl::node_hash_map<FullTrackName, NamespaceHandler> subscribed_namespaces_;
+  absl::node_hash_map<TrackNamespace, NamespaceHandler> subscribed_namespaces_;
 };
 
 absl::StatusOr<MoqtConfigureSessionCallback> IncomingSessionHandler(
diff --git a/quiche/quic/moqt/tools/moqt_mock_visitor.h b/quiche/quic/moqt/tools/moqt_mock_visitor.h
index 3b910ee..d45c99a 100644
--- a/quiche/quic/moqt/tools/moqt_mock_visitor.h
+++ b/quiche/quic/moqt/tools/moqt_mock_visitor.h
@@ -31,10 +31,10 @@
   testing::MockFunction<void(absl::string_view)> session_terminated_callback;
   testing::MockFunction<void()> session_deleted_callback;
   testing::MockFunction<std::optional<MoqtAnnounceErrorReason>(
-      const FullTrackName&, std::optional<VersionSpecificParameters>)>
+      const TrackNamespace&, std::optional<VersionSpecificParameters>)>
       incoming_announce_callback;
   testing::MockFunction<std::optional<MoqtSubscribeErrorReason>(
-      FullTrackName, std::optional<VersionSpecificParameters>)>
+      TrackNamespace, std::optional<VersionSpecificParameters>)>
       incoming_subscribe_announces_callback;
 
   MockSessionCallbacks() {