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);