Use QuicheStringTuple for MOQT names.

Noteworthy changes:
- TrackNamespace is now always valid.
- Bump track name limit from 1024 to 4096 (which is the number in the spec).
- There are more validation checks for TrackNamespace / FullTrackName.

PiperOrigin-RevId: 868842042
diff --git a/quiche/common/quiche_string_tuple.h b/quiche/common/quiche_string_tuple.h
index 3f57412..20e383b 100644
--- a/quiche/common/quiche_string_tuple.h
+++ b/quiche/common/quiche_string_tuple.h
@@ -23,8 +23,10 @@
 #include "absl/strings/str_format.h"
 #include "absl/strings/str_join.h"
 #include "absl/strings/string_view.h"
+#include "absl/types/span.h"
 #include "quiche/common/platform/api/quiche_bug_tracker.h"
 #include "quiche/common/platform/api/quiche_export.h"
+#include "quiche/common/vectorized_io_utils.h"
 
 namespace quiche {
 
@@ -126,8 +128,59 @@
     if (element.size() + data_.size() > kMaxDataSize) {
       return false;
     }
-    elements_start_offsets_.push_back(data_.size());
-    data_.append(element.data(), element.size());
+    AddUnchecked(element);
+    return true;
+  }
+
+  // Appends another tuple to the end of this one.  Returns false if this
+  // operation results in the size limit being exceeded.
+  [[nodiscard]] bool Append(const QuicheStringTuple& suffix) {
+    if (data_.size() + suffix.data_.size() > kMaxDataSize) {
+      return false;
+    }
+    OffsetT current_offset = data_.size();
+    elements_start_offsets_.reserve(elements_start_offsets_.size() +
+                                    suffix.elements_start_offsets_.size());
+    for (const absl::string_view element : suffix) {
+      elements_start_offsets_.push_back(current_offset);
+      current_offset += element.size();
+    }
+    data_.append(suffix.data_);
+    return true;
+  }
+
+  // Appends a span of string_views to the end of the tuple.  Returns false if
+  // this operation results in the size limit being exceeded.
+  [[nodiscard]] bool Append(absl::Span<const absl::string_view> span) {
+    size_t total_size = TotalStringViewSpanSize(span);
+    if (data_.size() + total_size > kMaxDataSize) {
+      return false;
+    }
+    elements_start_offsets_.reserve(elements_start_offsets_.size() +
+                                    span.size());
+    data_.reserve(data_.size() + total_size);
+    for (absl::string_view element : span) {
+      AddUnchecked(element);
+    }
+    return true;
+  }
+
+  // If `prefix` is a prefix of this current tuple, the prefix is removed.
+  [[nodiscard]] bool ConsumePrefix(const QuicheStringTuple& prefix) {
+    if (!IsPrefix(prefix)) {
+      return false;
+    }
+
+    data_ = data_.substr(prefix.data_.size());
+    const OffsetT prefix_offset = prefix.data_.size();
+    const size_t prefix_element_count = prefix.elements_start_offsets_.size();
+    for (size_t i = prefix_element_count; i < elements_start_offsets_.size();
+         ++i) {
+      elements_start_offsets_[i - prefix_element_count] =
+          elements_start_offsets_[i] - prefix_offset;
+    }
+    elements_start_offsets_.resize(elements_start_offsets_.size() -
+                                   prefix_element_count);
     return true;
   }
 
@@ -192,6 +245,9 @@
   size_t TotalBytes() const { return data_.size(); }
   size_t BytesLeft() const { return kMaxDataSize - data_.size(); }
 
+  absl::string_view front() const { return (*this)[0]; }
+  absl::string_view back() const { return (*this)[size() - 1]; }
+
   iterator begin() const { return iterator(this, 0); }
   iterator end() const { return iterator(this, size()); }
 
@@ -219,6 +275,11 @@
   }
 
  private:
+  void AddUnchecked(absl::string_view element) {
+    elements_start_offsets_.push_back(data_.size());
+    data_.append(element.data(), element.size());
+  }
+
   std::string data_;
   absl::InlinedVector<OffsetT, InlinedSizes> elements_start_offsets_;
 };
diff --git a/quiche/common/quiche_string_tuple_test.cc b/quiche/common/quiche_string_tuple_test.cc
index 67f503b..51e854f 100644
--- a/quiche/common/quiche_string_tuple_test.cc
+++ b/quiche/common/quiche_string_tuple_test.cc
@@ -38,6 +38,12 @@
   EXPECT_EQ(tuple.size(), 1);
   EXPECT_TRUE(tuple.Add("bar"));
   EXPECT_EQ(tuple.size(), 2);
+
+  EXPECT_EQ(tuple[0], "foo");
+  EXPECT_EQ(tuple[1], "bar");
+  EXPECT_EQ(tuple.front(), "foo");
+  EXPECT_EQ(tuple.back(), "bar");
+
   EXPECT_TRUE(tuple.Pop());
   EXPECT_EQ(tuple.size(), 1);
   EXPECT_TRUE(tuple.Pop());
@@ -153,5 +159,34 @@
   EXPECT_FALSE(a.IsPrefix(abc));
 }
 
+TEST_F(QuicheStringTupleTest, AppendTuple) {
+  QuicheStringTuple<> prefix({"a", "bc"});
+  QuicheStringTuple<> suffix({"def", "ghijk"});
+  ASSERT_TRUE(prefix.Append(suffix));
+  EXPECT_THAT(prefix, ElementsAre("a", "bc", "def", "ghijk"));
+}
+
+TEST_F(QuicheStringTupleTest, AppendSpan) {
+  QuicheStringTuple<> prefix({"a", "bc"});
+  std::vector<absl::string_view> suffix({"def", "ghijk"});
+  ASSERT_TRUE(prefix.Append(suffix));
+  EXPECT_THAT(prefix, ElementsAre("a", "bc", "def", "ghijk"));
+}
+
+TEST_F(QuicheStringTupleTest, RemovePrefix) {
+  QuicheStringTuple<> prefix({"a", "bc"});
+  QuicheStringTuple<> full({"a", "bc", "def", "ghijk", "lmn"});
+  ASSERT_FALSE(prefix.ConsumePrefix(full));
+  ASSERT_TRUE(full.ConsumePrefix(prefix));
+  EXPECT_THAT(full, ElementsAre("def", "ghijk", "lmn"));
+}
+
+TEST_F(QuicheStringTupleTest, RemovePrefixThatIsTheSameTuple) {
+  QuicheStringTuple<> prefix({"a", "bc"});
+  QuicheStringTuple<> full({"a", "bc"});
+  ASSERT_TRUE(full.ConsumePrefix(prefix));
+  EXPECT_THAT(full, IsEmpty());
+}
+
 }  // namespace
 }  // namespace quiche::test
diff --git a/quiche/quic/moqt/moqt_framer.cc b/quiche/quic/moqt/moqt_framer.cc
index 8bb83a8..b3e5ca8 100644
--- a/quiche/quic/moqt/moqt_framer.cc
+++ b/quiche/quic/moqt/moqt_framer.cc
@@ -13,14 +13,17 @@
 #include <utility>
 #include <variant>
 
+#include "absl/container/fixed_array.h"
 #include "absl/functional/overload.h"
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
 #include "absl/strings/string_view.h"
+#include "absl/types/span.h"
 #include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/moqt/moqt_error.h"
 #include "quiche/quic/moqt/moqt_key_value_pair.h"
 #include "quiche/quic/moqt/moqt_messages.h"
+#include "quiche/quic/moqt/moqt_names.h"
 #include "quiche/quic/moqt/moqt_priority.h"
 #include "quiche/common/platform/api/quiche_bug_tracker.h"
 #include "quiche/common/platform/api/quiche_logging.h"
@@ -137,16 +140,20 @@
   WireTrackNamespace(const TrackNamespace& name) : namespace_(name) {}
 
   size_t GetLengthOnWire() {
+    absl::FixedArray<absl::string_view> tuple(namespace_.tuple().begin(),
+                                              namespace_.tuple().end());
     return quiche::ComputeLengthOnWire(
         WireVarInt62(namespace_.number_of_elements()),
-        WireSpan<WireStringWithVarInt62Length, std::string>(
-            namespace_.tuple()));
+        WireSpan<WireStringWithVarInt62Length, absl::string_view>(
+            absl::MakeSpan(tuple)));
   }
   absl::Status SerializeIntoWriter(quiche::QuicheDataWriter& writer) {
+    absl::FixedArray<absl::string_view> tuple(namespace_.tuple().begin(),
+                                              namespace_.tuple().end());
     return quiche::SerializeIntoWriter(
         writer, WireVarInt62(namespace_.number_of_elements()),
-        WireSpan<WireStringWithVarInt62Length, std::string>(
-            namespace_.tuple()));
+        WireSpan<WireStringWithVarInt62Length, absl::string_view>(
+            absl::MakeSpan(tuple)));
   }
 
  private:
@@ -493,9 +500,8 @@
                                       message.parameters, parameters)) {
     return quiche::QuicheBuffer();
   }
-  return SerializeControlMessage(
-      MoqtMessageType::kClientSetup,
-      WireKeyValuePairList(parameters));
+  return SerializeControlMessage(MoqtMessageType::kClientSetup,
+                                 WireKeyValuePairList(parameters));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeServerSetup(
@@ -612,7 +618,6 @@
   return SerializeSubscribe(message, MoqtMessageType::kTrackStatus);
 }
 
-
 quiche::QuicheBuffer MoqtFramer::SerializeGoAway(const MoqtGoAway& message) {
   return SerializeControlMessage(
       MoqtMessageType::kGoAway,
diff --git a/quiche/quic/moqt/moqt_integration_test.cc b/quiche/quic/moqt/moqt_integration_test.cc
index 1262b49..19053fc 100644
--- a/quiche/quic/moqt/moqt_integration_test.cc
+++ b/quiche/quic/moqt/moqt_integration_test.cc
@@ -11,6 +11,7 @@
 #include <variant>
 #include <vector>
 
+#include "absl/status/statusor.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/quic_bandwidth.h"
@@ -39,6 +40,7 @@
 #include "quic_trace/quic_trace.pb.h"
 #include "quiche/common/platform/api/quiche_test.h"
 #include "quiche/common/quiche_mem_slice.h"
+#include "quiche/common/test_tools/quiche_test_utils.h"
 
 namespace moqt::test {
 
@@ -257,9 +259,11 @@
                     const std::optional<MessageParameters>&,
                     MoqtResponseCallback callback) {
         std::move(callback)(std::nullopt);
-        FullTrackName track_name(track_namespace, "/catalog");
+        absl::StatusOr<FullTrackName> track_name =
+            FullTrackName::Create(track_namespace, "/catalog");
+        QUICHE_ASSERT_OK(track_name.status());
         MessageParameters parameters(MoqtFilterType::kLargestObject);
-        server_->session()->Subscribe(track_name, &subscribe_visitor_,
+        server_->session()->Subscribe(*track_name, &subscribe_visitor_,
                                       parameters);
       });
   testing::MockFunction<void(std::optional<MoqtRequestErrorInfo>)>
@@ -294,10 +298,12 @@
       .WillOnce([&](const TrackNamespace& track_namespace,
                     const std::optional<MessageParameters>&,
                     MoqtResponseCallback callback) {
-        FullTrackName track_name(track_namespace, "data");
+        absl::StatusOr<FullTrackName> track_name =
+            FullTrackName::Create(track_namespace, "data");
+        QUICHE_ASSERT_OK(track_name.status());
         std::move(callback)(std::nullopt);
         MessageParameters parameters;
-        server_->session()->Subscribe(track_name, &subscribe_visitor_,
+        server_->session()->Subscribe(*track_name, &subscribe_visitor_,
                                       parameters);
       });
 
diff --git a/quiche/quic/moqt/moqt_messages.h b/quiche/quic/moqt/moqt_messages.h
index 0d8580f..1b4e9cf 100644
--- a/quiche/quic/moqt/moqt_messages.h
+++ b/quiche/quic/moqt/moqt_messages.h
@@ -352,12 +352,6 @@
 };
 
 struct QUICHE_EXPORT MoqtSubscribe {
-  MoqtSubscribe() = default;
-  MoqtSubscribe(uint64_t request_id, FullTrackName full_track_name,
-                MessageParameters parameters)
-      : request_id(request_id),
-        full_track_name(full_track_name),
-        parameters(parameters) {}
   uint64_t request_id;
   FullTrackName full_track_name;
   MessageParameters parameters;
@@ -461,23 +455,12 @@
 };
 
 struct StandaloneFetch {
-  StandaloneFetch() = default;
-  StandaloneFetch(FullTrackName full_track_name, Location start_location,
-                  Location end_location)
-      : full_track_name(full_track_name),
-        start_location(start_location),
-        end_location(end_location) {}
   FullTrackName full_track_name;
   Location start_location;
   Location end_location;
-  bool operator==(const StandaloneFetch& other) const {
-    return full_track_name == other.full_track_name &&
-           start_location == other.start_location &&
-           end_location == other.end_location;
-  }
-  bool operator!=(const StandaloneFetch& other) const {
-    return !(*this == other);
-  }
+
+  bool operator==(const StandaloneFetch& other) const = default;
+  bool operator!=(const StandaloneFetch& other) const = default;
 };
 
 struct JoiningFetchRelative {
diff --git a/quiche/quic/moqt/moqt_names.cc b/quiche/quic/moqt/moqt_names.cc
index 93b7eea..fa52970 100644
--- a/quiche/quic/moqt/moqt_names.cc
+++ b/quiche/quic/moqt/moqt_names.cc
@@ -4,106 +4,111 @@
 
 #include "quiche/quic/moqt/moqt_names.h"
 
-#include <iterator>
+#include <cstddef>
+#include <initializer_list>
 #include <string>
-#include <vector>
+#include <utility>
 
-#include "absl/strings/escaping.h"
+#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/str_format.h"
 #include "absl/strings/string_view.h"
 #include "absl/types/span.h"
 #include "quiche/common/platform/api/quiche_bug_tracker.h"
 
 namespace moqt {
 
-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;
+absl::StatusOr<TrackNamespace> TrackNamespace::Create(MoqtStringTuple tuple) {
+  if (tuple.size() > kMaxNamespaceElements) {
+    return absl::OutOfRangeError(
+        absl::StrFormat("Tuple has %d elements, whereas MOQT only allows %d",
+                        tuple.size(), kMaxNamespaceElements));
   }
-  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;
-    }
-  }
+  return TrackNamespace(std::move(tuple));
 }
 
-TrackNamespace::TrackNamespace(absl::Span<const std::string> 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.";
+TrackNamespace::TrackNamespace(std::initializer_list<absl::string_view> tuple) {
+  bool success = tuple_.Append(tuple);
+  if (!success) {
+    QUICHE_BUG(TrackNamespace_constructor)
+        << "Invalid namespace supplied to the TrackNamspace constructor";
+    tuple_ = MoqtStringTuple();
     return;
   }
-  for (const 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;
+  return tuple_.IsPrefix(other.tuple_);
 }
 
-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;
+bool TrackNamespace::AddElement(absl::string_view element) {
+  if (tuple_.size() >= kMaxNamespaceElements) {
+    return false;
   }
-  length_ += element.length();
-  tuple_.push_back(std::string(element));
+  return tuple_.Add(element);
+}
+bool TrackNamespace::PopElement() {
+  if (tuple_.empty()) {
+    return false;
+  }
+  return tuple_.Pop();
 }
 
 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, "::"), "}");
+  // TODO(vasilvv): switch to the standard encoding.
+  return absl::StrCat(tuple_);
 }
 
-FullTrackName::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.";
+absl::StatusOr<FullTrackName> FullTrackName::Create(TrackNamespace ns,
+                                                    std::string name) {
+  const size_t total_length = ns.total_length() + name.size();
+  if (ns.total_length() + name.size() > kMaxFullTrackNameSize) {
+    return absl::OutOfRangeError(
+        absl::StrFormat("Attempting to create a full track name of size %d, "
+                        "whereas at most %d bytes are allowed by the protocol",
+                        total_length, kMaxFullTrackNameSize));
+  }
+  return FullTrackName(std::move(ns), std::move(name),
+                       FullTrackNameIsValidTag());
 }
+
 FullTrackName::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.";
+    : namespace_(std::move(ns)), name_(name) {
+  if (namespace_.total_length() + name.size() > kMaxFullTrackNameSize) {
+    QUICHE_BUG(Moqt_full_track_name_too_large_01)
+        << "Constructing a Full Track Name that is too large.";
+    namespace_.Clear();
+    name_.clear();
+  }
 }
+FullTrackName::FullTrackName(absl::string_view ns, absl::string_view name)
+    : namespace_(TrackNamespace({ns})), name_(name) {
+  if (ns.size() + name.size() > kMaxFullTrackNameSize) {
+    QUICHE_BUG(Moqt_full_track_name_too_large_02)
+        << "Constructing a Full Track Name that is too large.";
+    namespace_.Clear();
+    name_.clear();
+  }
+}
+FullTrackName::FullTrackName(std::initializer_list<absl::string_view> ns,
+                             absl::string_view name)
+    : namespace_(ns), name_(name) {
+  if (namespace_.total_length() + name.size() > kMaxFullTrackNameSize) {
+    QUICHE_BUG(Moqt_full_track_name_too_large_03)
+        << "Constructing a Full Track Name that is too large.";
+    namespace_.Clear();
+    name_.clear();
+  }
+}
+
+FullTrackName::FullTrackName(TrackNamespace ns, std::string name,
+                             FullTrackNameIsValidTag)
+    : namespace_(std::move(ns)), name_(std::move(name)) {}
 
 std::string FullTrackName::ToString() const {
+  // TODO(vasilvv): switch to the standard encoding.
   return absl::StrCat(namespace_.ToString(), "::", name_);
 }
-void FullTrackName::set_name(absl::string_view name) {
-  QUICHE_BUG_IF(Moqt_name_too_large_03, !CanAddName(name))
-      << "Setting a name that is too large.";
-  name_ = name;
-}
 
 }  // namespace moqt
diff --git a/quiche/quic/moqt/moqt_names.h b/quiche/quic/moqt/moqt_names.h
index 37a7fb2..7d91579 100644
--- a/quiche/quic/moqt/moqt_names.h
+++ b/quiche/quic/moqt/moqt_names.h
@@ -11,84 +11,81 @@
 #include <cstring>
 #include <initializer_list>
 #include <string>
-#include <vector>
+#include <utility>
 
+#include "absl/base/attributes.h"
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
 #include "absl/strings/string_view.h"
 #include "absl/types/span.h"
+#include "quiche/common/quiche_status_utils.h"
+#include "quiche/common/quiche_string_tuple.h"
 
 namespace moqt {
 
 // Protocol-specified limits on the length and structure of MoQT namespaces.
-inline constexpr uint64_t kMinNamespaceElements = 1;
 inline constexpr uint64_t kMaxNamespaceElements = 32;
-inline constexpr size_t kMaxFullTrackNameSize = 1024;
+inline constexpr size_t kMaxFullTrackNameSize = 4096;
 
+// MOQT does not operate on tuples larger than 4096, which means we can encode
+// tuple offsets as uint16_t.  We allow up to 8 inlined tuple elements, since
+// picking a smaller value would not shrink the inlined vector in question.
+using MoqtStringTuple = quiche::QuicheStringTuple<kMaxFullTrackNameSize, 8>;
+
+// TrackNamespace represents a valid MOQT track namespace.
 class TrackNamespace {
  public:
-  explicit TrackNamespace(absl::Span<const absl::string_view> elements);
-  explicit TrackNamespace(absl::Span<const std::string> elements);
-  explicit TrackNamespace(
-      std::initializer_list<const absl::string_view> elements)
-      : TrackNamespace(absl::Span<const absl::string_view>(
-            std::data(elements), std::size(elements))) {}
-  explicit TrackNamespace(absl::string_view ns) : TrackNamespace({ns}) {}
-  TrackNamespace() : TrackNamespace({}) {}
+  static absl::StatusOr<TrackNamespace> Create(MoqtStringTuple tuple);
 
-  bool IsValid() const {
-    return !tuple_.empty() && tuple_.size() <= kMaxNamespaceElements &&
-           length_ <= kMaxFullTrackNameSize;
-  }
+  TrackNamespace() = default;
+
+  explicit TrackNamespace(std::initializer_list<absl::string_view> tuple);
+
+  TrackNamespace(const TrackNamespace&) = default;
+  TrackNamespace(TrackNamespace&&) = default;
+  TrackNamespace& operator=(const TrackNamespace&) = default;
+  TrackNamespace& operator=(TrackNamespace&&) = default;
+
   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);
+  [[nodiscard]] bool AddElement(absl::string_view element);
+  bool PopElement();
+  void Clear() { tuple_.Clear(); }
+  void ReserveElements(size_t count) { tuple_.ReserveTupleElements(count); }
+
+  [[nodiscard]] bool Append(absl::Span<const absl::string_view> span) {
+    return tuple_.Append(span);
   }
-  void AddElement(absl::string_view element);
-  bool PopElement() {
-    if (tuple_.size() == 0) {
-      return false;
-    }
-    length_ -= tuple_.back().length();
-    tuple_.pop_back();
-    return true;
-  }
+
   absl::StatusOr<TrackNamespace> AddSuffix(const TrackNamespace& suffix) const {
     TrackNamespace result = *this;
-    result.tuple_.reserve(tuple_.size() + suffix.tuple_.size());
-    for (const auto& element : suffix.tuple()) {
-      if (!result.CanAddElement(element)) {
-        return absl::OutOfRangeError("Combined namespace is too large");
-      }
-      result.AddElement(element);
+    if (!result.tuple_.Append(suffix.tuple_)) {
+      return absl::OutOfRangeError("Combined namespace is too large");
     }
     return result;
   }
+
   absl::StatusOr<TrackNamespace> ExtractSuffix(
       const TrackNamespace& prefix) const {
-    if (!InNamespace(prefix)) {
+    TrackNamespace result = *this;
+    if (!result.tuple_.ConsumePrefix(prefix.tuple_)) {
       return absl::InvalidArgumentError("Prefix is not in namespace");
     }
-    return TrackNamespace(
-        absl::MakeSpan(tuple_).subspan(prefix.number_of_elements()));
+    return result;
   }
+
   std::string ToString() 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_; }
+  size_t total_length() const { return tuple_.TotalBytes(); }
+  bool empty() const { return tuple_.empty(); }
 
   auto operator<=>(const TrackNamespace& other) const {
-    return std::lexicographical_compare_three_way(
-        tuple_.cbegin(), tuple_.cend(), other.tuple_.cbegin(),
-        other.tuple_.cend());
+    return tuple_ <=> other.tuple_;
   }
   bool operator==(const TrackNamespace&) const = default;
 
-  const std::vector<std::string>& tuple() const { return tuple_; }
+  const MoqtStringTuple& tuple() const { return tuple_; }
 
   template <typename H>
   friend H AbslHashValue(H h, const TrackNamespace& m) {
@@ -100,34 +97,39 @@
   }
 
  private:
-  std::vector<std::string> tuple_;
-  size_t length_ = 0;  // size in bytes.
+  TrackNamespace(MoqtStringTuple tuple) : tuple_(std::move(tuple)) {}
+
+  MoqtStringTuple tuple_;
 };
 
+// FullTrackName represents a MOQT full track name.
 class FullTrackName {
  public:
-  FullTrackName(absl::string_view ns, absl::string_view name);
-  FullTrackName(TrackNamespace ns, absl::string_view name);
+  static absl::StatusOr<FullTrackName> Create(TrackNamespace ns,
+                                              std::string name);
+
   FullTrackName() = default;
 
-  bool IsValid() const {
-    return namespace_.IsValid() && length() <= kMaxFullTrackNameSize;
-  }
+  // Convenience constructor. QUICHE_BUGs if the resulting full track name is
+  // invalid.
+  FullTrackName(TrackNamespace ns, absl::string_view name);
+  FullTrackName(absl::string_view ns, absl::string_view name);
+  FullTrackName(std::initializer_list<absl::string_view> ns,
+                absl::string_view name);
+
+  FullTrackName(const FullTrackName&) = default;
+  FullTrackName(FullTrackName&&) = default;
+  FullTrackName& operator=(const FullTrackName&) = default;
+  FullTrackName& operator=(FullTrackName&&) = default;
+
+  bool IsValid() const { return !name_.empty(); }
+
   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;
-  // 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);
+  absl::string_view name() const ABSL_ATTRIBUTE_LIFETIME_BOUND { return name_; }
   size_t length() const { return namespace_.total_length() + name_.length(); }
 
+  std::string ToString() const;
+
   auto operator<=>(const FullTrackName&) const = default;
   template <typename H>
   friend H AbslHashValue(H h, const FullTrackName& m) {
@@ -139,8 +141,13 @@
   }
 
  private:
+  struct FullTrackNameIsValidTag {};
+
+  explicit FullTrackName(TrackNamespace ns, std::string name,
+                         FullTrackNameIsValidTag);
+
   TrackNamespace namespace_;
-  std::string name_ = "";
+  std::string name_;
 };
 
 }  // namespace moqt
diff --git a/quiche/quic/moqt/moqt_names_test.cc b/quiche/quic/moqt/moqt_names_test.cc
index 2d843ee..dd879cf 100644
--- a/quiche/quic/moqt/moqt_names_test.cc
+++ b/quiche/quic/moqt/moqt_names_test.cc
@@ -4,10 +4,12 @@
 
 #include "quiche/quic/moqt/moqt_names.h"
 
+#include <utility>
 #include <vector>
 
 #include "absl/hash/hash.h"
 #include "absl/status/status.h"
+#include "absl/status/statusor.h"
 #include "absl/strings/string_view.h"
 #include "quiche/common/platform/api/quiche_expect_bug.h"
 #include "quiche/common/platform/api/quiche_test.h"
@@ -16,12 +18,17 @@
 namespace moqt::test {
 namespace {
 
+using ::quiche::test::StatusIs;
+using ::testing::HasSubstr;
+
 TEST(MoqtNamesTest, TrackNamespaceConstructors) {
   TrackNamespace name1({"foo", "bar"});
-  std::vector<absl::string_view> list = {"foo", "bar"};
-  TrackNamespace name2(list);
-  EXPECT_EQ(name1, name2);
-  EXPECT_EQ(absl::HashOf(name1), absl::HashOf(name2));
+  MoqtStringTuple list({"foo", "bar"});
+  absl::StatusOr<TrackNamespace> name2 =
+      TrackNamespace::Create(std::move(list));
+  QUICHE_ASSERT_OK(name2);
+  ASSERT_EQ(name1, *name2);
+  EXPECT_EQ(absl::HashOf(name1), absl::HashOf(*name2));
 }
 
 TEST(MoqtNamesTest, TrackNamespaceOrder) {
@@ -46,7 +53,7 @@
 TEST(MoqtNamesTest, TrackNamespacePushPop) {
   TrackNamespace name({"a"});
   TrackNamespace original = name;
-  name.AddElement("b");
+  EXPECT_TRUE(name.AddElement("b"));
   EXPECT_TRUE(name.InNamespace(original));
   EXPECT_FALSE(original.InNamespace(name));
   EXPECT_TRUE(name.PopElement());
@@ -58,15 +65,15 @@
 
 TEST(MoqtNamesTest, TrackNamespaceToString) {
   TrackNamespace name1({"a", "b"});
-  EXPECT_EQ(name1.ToString(), R"({"a"::"b"})");
+  EXPECT_EQ(name1.ToString(), R"({"a", "b"})");
 
   TrackNamespace name2({"\xff", "\x61"});
-  EXPECT_EQ(name2.ToString(), R"({"\xff"::"a"})");
+  EXPECT_EQ(name2.ToString(), R"({"\xff", "a"})");
 }
 
 TEST(MoqtNamesTest, FullTrackNameToString) {
-  FullTrackName name1(TrackNamespace{"a", "b"}, "c");
-  EXPECT_EQ(name1.ToString(), R"({"a"::"b"}::c)");
+  FullTrackName name1({"a", "b"}, "c");
+  EXPECT_EQ(name1.ToString(), R"({"a", "b"}::c)");
 }
 
 TEST(MoqtNamesTest, TrackNamespaceSuffixes) {
@@ -87,24 +94,21 @@
 
 TEST(MoqtNamesTest, 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);
+  absl::StatusOr<TrackNamespace> name1 = TrackNamespace::Create(MoqtStringTuple(
+      {"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"}));
+  QUICHE_ASSERT_OK(name1);
+  EXPECT_FALSE(name1->AddElement("a"));
+  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());
+  // 33 elements fail.
+  absl::StatusOr<TrackNamespace> name2 = TrackNamespace::Create(MoqtStringTuple(
+      {"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"}));
+  EXPECT_THAT(name2.status(), StatusIs(absl::StatusCode::kOutOfRange,
+                                       HasSubstr("33 elements")));
 }
 
 TEST(MoqtNamesTest, FullTrackNameTooLong) {
@@ -112,17 +116,15 @@
   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.");
+  EXPECT_FALSE(max_length_namespace.AddElement("f"));
   // Constructing a FullTrackName where the name brings it over the length
   // limit.
-  EXPECT_QUICHE_BUG(FullTrackName(max_length_namespace, "f"),
+  EXPECT_QUICHE_BUG(FullTrackName(max_length_namespace.tuple()[0], "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.");
+                    "TrackNamspace constructor");
 }
 
 }  // namespace
diff --git a/quiche/quic/moqt/moqt_parser.cc b/quiche/quic/moqt/moqt_parser.cc
index 174c153..d32baf9 100644
--- a/quiche/quic/moqt/moqt_parser.cc
+++ b/quiche/quic/moqt/moqt_parser.cc
@@ -18,6 +18,7 @@
 #include "absl/base/casts.h"
 #include "absl/cleanup/cleanup.h"
 #include "absl/container/fixed_array.h"
+#include "absl/status/statusor.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "absl/types/span.h"
@@ -27,6 +28,7 @@
 #include "quiche/quic/moqt/moqt_error.h"
 #include "quiche/quic/moqt/moqt_key_value_pair.h"
 #include "quiche/quic/moqt/moqt_messages.h"
+#include "quiche/quic/moqt/moqt_names.h"
 #include "quiche/quic/moqt/moqt_priority.h"
 #include "quiche/common/platform/api/quiche_bug_tracker.h"
 #include "quiche/common/platform/api/quiche_logging.h"
@@ -1158,7 +1160,7 @@
 
 bool MoqtControlParser::ReadTrackNamespace(quic::QuicDataReader& reader,
                                            TrackNamespace& track_namespace) {
-  QUICHE_DCHECK(!track_namespace.IsValid());
+  QUICHE_DCHECK(track_namespace.empty());
   uint64_t num_elements;
   if (!reader.ReadVarInt62(&num_elements)) {
     return false;
@@ -1168,36 +1170,38 @@
                "Invalid number of namespace elements");
     return false;
   }
+  absl::FixedArray<absl::string_view> elements(num_elements);
   for (uint64_t i = 0; i < num_elements; ++i) {
-    absl::string_view element;
-    if (!reader.ReadStringPieceVarInt62(&element)) {
+    if (!reader.ReadStringPieceVarInt62(&elements[i])) {
       return false;
     }
-    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());
+  if (!track_namespace.Append(elements)) {
+    ParseError(MoqtError::kProtocolViolation, "Track namespace is too large");
+    return false;
+  }
   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())) {
+  TrackNamespace track_namespace;
+  if (!ReadTrackNamespace(reader, 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");
+  absl::StatusOr<FullTrackName> full_track_name_or =
+      FullTrackName::Create(std::move(track_namespace), std::string(name));
+  if (!full_track_name_or.ok()) {
+    ParseError(MoqtError::kProtocolViolation,
+               full_track_name_or.status().message());
     return false;
   }
-  full_track_name.set_name(name);
+  full_track_name = *std::move(full_track_name_or);
   return true;
 }
 
diff --git a/quiche/quic/moqt/moqt_session.cc b/quiche/quic/moqt/moqt_session.cc
index 06800ed..79c9df2 100644
--- a/quiche/quic/moqt/moqt_session.cc
+++ b/quiche/quic/moqt/moqt_session.cc
@@ -262,7 +262,6 @@
     TrackNamespace& prefix, SubscribeNamespaceOption option,
     const MessageParameters& parameters,
     MoqtResponseCallback response_callback) {
-  QUICHE_DCHECK(prefix.IsValid());
   if (received_goaway_ || sent_goaway_) {
     QUIC_DLOG(INFO) << ENDPOINT
                     << "Tried to send SUBSCRIBE_NAMESPACE after GOAWAY";
@@ -343,7 +342,6 @@
   if (is_closing_) {
     return false;
   }
-  QUICHE_DCHECK(track_namespace.IsValid());
   if (publish_namespace_by_namespace_.contains(track_namespace)) {
     return false;
   }
@@ -387,7 +385,6 @@
   if (is_closing_) {
     return false;
   }
-  QUICHE_DCHECK(track_namespace.IsValid());
   auto it = publish_namespace_by_namespace_.find(track_namespace);
   if (it == publish_namespace_by_namespace_.end()) {
     return false;  // Could have been destroyed by PUBLISH_NAMESPACE_CANCEL.
@@ -420,7 +417,6 @@
   if (is_closing_) {
     return false;
   }
-  QUICHE_DCHECK(track_namespace.IsValid());
   auto it = publish_namespace_by_namespace_.find(track_namespace);
   if (it == publish_namespace_by_namespace_.end()) {
     return false;  // Could have been destroyed by PUBLISH_NAMESPACE_CANCEL.
@@ -438,7 +434,6 @@
 bool MoqtSession::PublishNamespaceCancel(const TrackNamespace& track_namespace,
                                          RequestErrorCode code,
                                          absl::string_view reason) {
-  QUICHE_DCHECK(track_namespace.IsValid());
   auto it = incoming_publish_namespaces_by_namespace_.find(track_namespace);
   if (it == publish_namespace_by_namespace_.end()) {
     return false;  // Could have been destroyed by PUBLISH_NAMESPACE_DONE.
diff --git a/quiche/quic/moqt/moqt_session_test.cc b/quiche/quic/moqt/moqt_session_test.cc
index e0423ac..18150d6 100644
--- a/quiche/quic/moqt/moqt_session_test.cc
+++ b/quiche/quic/moqt/moqt_session_test.cc
@@ -442,7 +442,7 @@
       Writev(ControlMessageOfType(MoqtMessageType::kPublishNamespace), _));
   MoqtRequestErrorInfo cancel_error_info;
   session_.PublishNamespace(
-      TrackNamespace("foo"), MessageParameters(),
+      TrackNamespace({"foo"}), MessageParameters(),
       publish_namespace_response_callback.AsStdFunction(),
       [&](MoqtRequestErrorInfo info) { cancel_error_info = info; });
 
@@ -462,7 +462,7 @@
   EXPECT_EQ(cancel_error_info.error_code, RequestErrorCode::kInternalError);
   EXPECT_EQ(cancel_error_info.reason_phrase, "Test error");
   // State is gone.
-  EXPECT_FALSE(session_.PublishNamespaceDone(TrackNamespace("foo")));
+  EXPECT_FALSE(session_.PublishNamespaceDone(TrackNamespace({"foo"})));
 }
 
 TEST_F(MoqtSessionTest, PublishNamespaceWithOkAndPublishNamespaceDone) {
@@ -1039,7 +1039,7 @@
 }
 
 TEST_F(MoqtSessionTest, SubscribeNamespaceLifeCycle) {
-  TrackNamespace prefix("foo");
+  TrackNamespace prefix({"foo"});
   bool got_callback = false;
   EXPECT_CALL(mock_session_, CanOpenNextOutgoingBidirectionalStream())
       .WillOnce(Return(true));
@@ -1069,7 +1069,7 @@
 }
 
 TEST_F(MoqtSessionTest, SubscribeNamespaceError) {
-  TrackNamespace prefix("foo");
+  TrackNamespace prefix({"foo"});
   bool got_callback = false;
   EXPECT_CALL(mock_session_, CanOpenNextOutgoingBidirectionalStream())
       .WillOnce(Return(true));
@@ -1102,7 +1102,7 @@
 }
 
 TEST_F(MoqtSessionTest, SubscribeNamespacePublishOnly) {
-  TrackNamespace prefix("foo");
+  TrackNamespace prefix({"foo"});
   // kPublish is not allowed.
   EXPECT_EQ(session_.SubscribeNamespace(
                 prefix, SubscribeNamespaceOption::kPublish, MessageParameters(),
@@ -3520,9 +3520,9 @@
       +[](std::optional<MoqtRequestErrorInfo>) {},
       +[](MoqtRequestErrorInfo) {});
   EXPECT_FALSE(session_.Fetch(
-      FullTrackName{TrackNamespace("foo"), "bar"},
-      +[](std::unique_ptr<MoqtFetchTask>) {}, Location(0, 0), 5, std::nullopt,
-      127, std::nullopt, VersionSpecificParameters()));
+      FullTrackName{{"foo"}, "bar"}, +[](std::unique_ptr<MoqtFetchTask>) {},
+      Location(0, 0), 5, std::nullopt, 127, std::nullopt,
+      VersionSpecificParameters()));
   // Error on additional GOAWAY.
   EXPECT_CALL(mock_session_,
               CloseSession(static_cast<uint64_t>(MoqtError::kProtocolViolation),
@@ -3550,7 +3550,7 @@
   EXPECT_CALL(mock_stream_,
               Writev(ControlMessageOfType(MoqtMessageType::kRequestError), _));
   stream_input->OnPublishNamespaceMessage(
-      MoqtPublishNamespace(3, TrackNamespace("foo"), MessageParameters()));
+      MoqtPublishNamespace(3, TrackNamespace({"foo"}), MessageParameters()));
   EXPECT_CALL(mock_stream_,
               Writev(ControlMessageOfType(MoqtMessageType::kRequestError), _));
   MoqtFetch fetch = DefaultFetch();
@@ -3575,7 +3575,7 @@
   EXPECT_CALL(mock_stream_, Writev).Times(0);
   MessageParameters parameters = SubscribeForTest();
   parameters.subscription_filter.emplace(MoqtFilterType::kLargestObject);
-  EXPECT_FALSE(session_.Subscribe(FullTrackName(TrackNamespace("foo"), "bar"),
+  EXPECT_FALSE(session_.Subscribe(FullTrackName({"foo"}, "bar"),
                                   &remote_track_visitor_, parameters));
   TrackNamespace prefix({"foo"});
   EXPECT_EQ(
@@ -3588,9 +3588,9 @@
       +[](std::optional<MoqtRequestErrorInfo>) {},
       +[](MoqtRequestErrorInfo) {});
   EXPECT_FALSE(session_.Fetch(
-      FullTrackName(TrackNamespace("foo"), "bar"),
-      +[](std::unique_ptr<MoqtFetchTask>) {}, Location(0, 0), 5, std::nullopt,
-      127, std::nullopt, VersionSpecificParameters()));
+      FullTrackName({"foo"}, "bar"), +[](std::unique_ptr<MoqtFetchTask>) {},
+      Location(0, 0), 5, std::nullopt, 127, std::nullopt,
+      VersionSpecificParameters()));
   session_.GoAway("");
   // GoAway timer fires.
   auto* goaway_alarm =
diff --git a/quiche/quic/moqt/relay_namespace_tree.cc b/quiche/quic/moqt/relay_namespace_tree.cc
index 2ac4c07..1b682b1 100644
--- a/quiche/quic/moqt/relay_namespace_tree.cc
+++ b/quiche/quic/moqt/relay_namespace_tree.cc
@@ -20,6 +20,7 @@
 #include "quiche/quic/moqt/moqt_names.h"
 #include "quiche/quic/moqt/moqt_session_interface.h"
 #include "quiche/common/platform/api/quiche_bug_tracker.h"
+#include "quiche/common/platform/api/quiche_logging.h"
 #include "quiche/common/quiche_circular_deque.h"
 #include "quiche/common/quiche_weak_ptr.h"
 
@@ -209,7 +210,8 @@
   for (auto child = node->children.begin(); child != node->children.end();
        ++child) {
     if (std::optional<absl::string_view> element = (*child)->element) {
-      suffix.AddElement(*element);
+      bool success = suffix.AddElement(*element);
+      QUICHE_DCHECK(success);
       NotifyOfAllChildren(*child, suffix, listener);
       suffix.PopElement();
     }
diff --git a/quiche/quic/moqt/test_tools/moqt_test_message.h b/quiche/quic/moqt/test_tools/moqt_test_message.h
index 0ebf5fa..308dde5 100644
--- a/quiche/quic/moqt/test_tools/moqt_test_message.h
+++ b/quiche/quic/moqt/test_tools/moqt_test_message.h
@@ -1247,7 +1247,7 @@
 
   MoqtSubscribeNamespace subscribe_namespace_ = {
       /*request_id=*/1,
-      TrackNamespace("foo"),
+      TrackNamespace({"foo"}),
       SubscribeNamespaceOption::kBoth,
       MessageParameters(),  // set in constructor.
   };
diff --git a/quiche/quic/moqt/tools/moq_chat.cc b/quiche/quic/moqt/tools/moq_chat.cc
index ddf349c..54b35e7 100644
--- a/quiche/quic/moqt/tools/moq_chat.cc
+++ b/quiche/quic/moqt/tools/moq_chat.cc
@@ -6,11 +6,13 @@
 
 #include <optional>
 
+#include "absl/status/statusor.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "absl/time/clock.h"
 #include "absl/time/time.h"
 #include "quiche/quic/moqt/moqt_messages.h"
+#include "quiche/quic/moqt/moqt_names.h"
 #include "quiche/common/platform/api/quiche_logging.h"
 
 namespace moqt::moq_chat {
@@ -30,10 +32,9 @@
 FullTrackName ConstructTrackName(absl::string_view chat_id,
                                  absl::string_view username,
                                  absl::string_view device_id) {
-  return FullTrackName(
-      TrackNamespace({kBasePath, chat_id, username, device_id,
-                      absl::StrCat(ToUnixSeconds(::absl::Now()))}),
-      kNameField);
+  return FullTrackName({kBasePath, chat_id, username, device_id,
+                        absl::StrCat(ToUnixSeconds(::absl::Now()))},
+                       kNameField);
 }
 
 std::optional<FullTrackName> ConstructTrackNameFromNamespace(
@@ -45,7 +46,12 @@
       track_namespace.tuple()[1] != chat_id) {
     return std::nullopt;
   }
-  return FullTrackName(track_namespace, kNameField);
+  absl::StatusOr<FullTrackName> name =
+      FullTrackName::Create(track_namespace, std::string(kNameField));
+  if (!name.ok()) {
+    return std::nullopt;
+  }
+  return *std::move(name);
 }
 
 absl::string_view GetUsername(const TrackNamespace& track_namespace) {
diff --git a/quiche/quic/moqt/tools/moq_chat_end_to_end_test.cc b/quiche/quic/moqt/tools/moq_chat_end_to_end_test.cc
index 18d915c..75ffa99 100644
--- a/quiche/quic/moqt/tools/moq_chat_end_to_end_test.cc
+++ b/quiche/quic/moqt/tools/moq_chat_end_to_end_test.cc
@@ -151,7 +151,7 @@
   TransactionType last_type;
   std::unique_ptr<MoqtNamespaceTask> namespace_probe =
       relay_.publisher()->AddNamespaceSubscriber(
-          TrackNamespace(moq_chat::kBasePath), nullptr);
+          TrackNamespace({moq_chat::kBasePath}), nullptr);
   namespace_probe->SetObjectsAvailableCallback([&]() {
     while (namespace_probe->GetNextSuffix(last_suffix, last_type) == kSuccess) {
       if (last_type == TransactionType::kAdd) {
diff --git a/quiche/quic/moqt/tools/moq_chat_test.cc b/quiche/quic/moqt/tools/moq_chat_test.cc
index 0579f94..24e8915 100644
--- a/quiche/quic/moqt/tools/moq_chat_test.cc
+++ b/quiche/quic/moqt/tools/moq_chat_test.cc
@@ -40,7 +40,7 @@
   TrackNamespace short_base_path({"moq-chat2", "chat-id", "user", "device"});
   EXPECT_FALSE(
       ConstructTrackNameFromNamespace(short_base_path, "chat-id").has_value());
-  track_namespace.AddElement("chat");  // Restore to correct value.
+  (void)track_namespace.AddElement("chat");  // Restore to correct value.
   // Base Path is wrong.
   TrackNamespace bad_base_path(
       {"moq-chat2", "chat-id", "user", "device", "timestamp"});
@@ -50,8 +50,7 @@
 
 TEST_F(MoqChatTest, Queries) {
   FullTrackName local_name(
-      TrackNamespace({kBasePath, "chat-id", "user", "device", "timestamp"}),
-      kNameField);
+      {kBasePath, "chat-id", "user", "device", "timestamp"}, kNameField);
   EXPECT_EQ(GetChatId(local_name), "chat-id");
   EXPECT_EQ(GetUsername(local_name), "user");
   TrackNamespace track_namespace(
diff --git a/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc b/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc
index 80330b7..1ae59e1 100644
--- a/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc
+++ b/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc
@@ -88,7 +88,7 @@
 }
 
 bool IsValidTrackNamespace(TrackNamespace track_namespace) {
-  for (const auto& element : track_namespace.tuple()) {
+  for (const absl::string_view element : track_namespace.tuple()) {
     if (!absl::c_all_of(element, IsValidTrackNamespaceChar)) {
       return false;
     }
@@ -98,14 +98,16 @@
 
 TrackNamespace CleanUpTrackNamespace(TrackNamespace track_namespace) {
   TrackNamespace output;
-  for (auto& it : track_namespace.tuple()) {
-    std::string element = it;
+  for (absl::string_view input : track_namespace.tuple()) {
+    std::string element(input);
     for (char& c : element) {
       if (!IsValidTrackNamespaceChar(c)) {
         c = '_';
       }
     }
-    output.AddElement(element);
+    // The replacement above will not change the tuple size, which means that
+    // the result will be always valid.
+    (void)output.AddElement(element);
   }
   return output;
 }
@@ -165,8 +167,15 @@
     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, track);
-      session_->RelativeJoiningFetch(full_track_name, &it->second, 0,
+      absl::StatusOr<FullTrackName> full_track_name =
+          FullTrackName::Create({track_namespace}, std::string(track));
+      if (!full_track_name.ok()) {
+        std::move(callback)(
+            MoqtRequestErrorInfo{RequestErrorCode::kInternalError, std::nullopt,
+                                 "Namespace too long"});
+        return;
+      }
+      session_->RelativeJoiningFetch(*full_track_name, &it->second, 0,
                                      VersionSpecificParameters());
     }
     std::move(callback)(std::nullopt);