Skip to content

Commit 949946d

Browse files
vasilvvcopybara-github
authored andcommitted
Implement a parser for MOQT track name format.
PiperOrigin-RevId: 869414824
1 parent cb7f0c2 commit 949946d

File tree

3 files changed

+240
-0
lines changed

3 files changed

+240
-0
lines changed

quiche/quic/moqt/moqt_names.cc

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@
99
#include <string>
1010
#include <utility>
1111

12+
#include "absl/container/fixed_array.h"
1213
#include "absl/status/status.h"
1314
#include "absl/status/statusor.h"
1415
#include "absl/strings/ascii.h"
1516
#include "absl/strings/str_cat.h"
1617
#include "absl/strings/str_format.h"
18+
#include "absl/strings/str_split.h"
1719
#include "absl/strings/string_view.h"
1820
#include "absl/types/span.h"
1921
#include "quiche/common/platform/api/quiche_bug_tracker.h"
22+
#include "quiche/common/platform/api/quiche_logging.h"
23+
#include "quiche/common/quiche_status_utils.h"
2024

2125
namespace moqt {
2226

@@ -53,6 +57,109 @@ void AppendEscapedTrackNameTuple(const MoqtStringTuple& tuple,
5357
}
5458
}
5559

60+
[[nodiscard]] bool HexCharToLowerHalfOfByte(char c, char& output) {
61+
if (c >= '0' && c <= '9') {
62+
output |= c - '0';
63+
return true;
64+
}
65+
if (c >= 'a' && c <= 'f') {
66+
output |= (c - 'a') + 0xa;
67+
return true;
68+
}
69+
return false;
70+
}
71+
72+
// Custom hex-to-binary converter that disallows uppercase hex, as the
73+
// specification explicitly requires lowercase hex.
74+
[[nodiscard]] bool HexEncodedBitToByte(char c0, char c1, char& output) {
75+
output = 0;
76+
if (!HexCharToLowerHalfOfByte(c0, output)) {
77+
return false;
78+
}
79+
output <<= 4;
80+
if (!HexCharToLowerHalfOfByte(c1, output)) {
81+
return false;
82+
}
83+
return true;
84+
}
85+
86+
// The MOQT specification is currently ambiguous regarding how permissive the
87+
// parsing should be. We are intentionally taking a "strict" approach, in which
88+
// any track name that this code parses successfully will result in a byte-exact
89+
// serialization from `ToString()`. For the discussion of this issue, see
90+
// <https://github.com/moq-wg/moq-transport/issues/1501>.
91+
//
92+
// It is up to the caller to reserve the capacity in `output_tuple` in advance.
93+
absl::Status UnescapeTrackNameComponent(absl::string_view input,
94+
MoqtStringTuple& output_tuple) {
95+
// The unescaping algorithm always results in strings of the same or smaller
96+
// size, so the fixed array of size `input.size()` will always fit the output.
97+
absl::FixedArray<char> output_buffer(input.size());
98+
absl::Span<char> output = absl::MakeSpan(output_buffer);
99+
while (!input.empty()) {
100+
if (IsTrackNameSafeCharacter(input[0])) {
101+
output[0] = input[0];
102+
input.remove_prefix(1);
103+
output.remove_prefix(1);
104+
continue;
105+
}
106+
if (input[0] == '.') {
107+
if (input.size() < 3) {
108+
return absl::InvalidArgumentError("Incomplete escape sequence");
109+
}
110+
if (!HexEncodedBitToByte(input[1], input[2], output[0])) {
111+
return absl::InvalidArgumentError("Invalid hex in an escape sequence");
112+
}
113+
if (IsTrackNameSafeCharacter(output[0])) {
114+
return absl::InvalidArgumentError("Hex-encoding a safe character");
115+
}
116+
input.remove_prefix(3);
117+
output.remove_prefix(1);
118+
continue;
119+
}
120+
return absl::InvalidArgumentError(
121+
absl::StrFormat("Invalid character 0x%02x encountered", input[0]));
122+
}
123+
size_t output_size = output_buffer.size() - output.size();
124+
if (!output_tuple.Add(absl::string_view(output_buffer.data(), output_size))) {
125+
return absl::OutOfRangeError("Maximum tuple size exceeded");
126+
}
127+
return absl::OkStatus();
128+
}
129+
130+
absl::StatusOr<MoqtStringTuple> ParseNameTuple(absl::string_view input) {
131+
// Special-case empty namespace, to indicate it is {} and not {""}.
132+
if (input.empty()) {
133+
return MoqtStringTuple();
134+
}
135+
136+
ssize_t bytes_to_reserve = input.size();
137+
ssize_t elements_to_reserve = 1;
138+
for (char c : input) {
139+
if (c == '-') {
140+
++elements_to_reserve;
141+
--bytes_to_reserve;
142+
}
143+
if (c == '.') {
144+
bytes_to_reserve -= 2;
145+
}
146+
}
147+
148+
MoqtStringTuple tuple;
149+
if (bytes_to_reserve > 0) {
150+
// A malformed name such as `......` will result in `bytes_to_reserve` being
151+
// negative.
152+
tuple.ReserveDataBytes(bytes_to_reserve);
153+
}
154+
tuple.ReserveTupleElements(elements_to_reserve);
155+
for (absl::string_view bit : absl::StrSplit(input, '-')) {
156+
QUICHE_RETURN_IF_ERROR(UnescapeTrackNameComponent(bit, tuple));
157+
}
158+
QUICHE_DCHECK_EQ(tuple.TotalBytes(), bytes_to_reserve);
159+
QUICHE_DCHECK_EQ(tuple.size(), elements_to_reserve);
160+
return tuple;
161+
}
162+
56163
} // namespace
57164

58165
absl::StatusOr<TrackNamespace> TrackNamespace::Create(MoqtStringTuple tuple) {
@@ -64,6 +171,12 @@ absl::StatusOr<TrackNamespace> TrackNamespace::Create(MoqtStringTuple tuple) {
64171
return TrackNamespace(std::move(tuple));
65172
}
66173

174+
absl::StatusOr<TrackNamespace> TrackNamespace::Parse(absl::string_view input) {
175+
absl::StatusOr<MoqtStringTuple> tuple = ParseNameTuple(input);
176+
QUICHE_RETURN_IF_ERROR(tuple.status());
177+
return Create(*std::move(tuple));
178+
}
179+
67180
TrackNamespace::TrackNamespace(std::initializer_list<absl::string_view> tuple) {
68181
bool success = tuple_.Append(tuple);
69182
if (!success) {
@@ -111,6 +224,28 @@ absl::StatusOr<FullTrackName> FullTrackName::Create(TrackNamespace ns,
111224
FullTrackNameIsValidTag());
112225
}
113226

227+
absl::StatusOr<FullTrackName> FullTrackName::Parse(absl::string_view input) {
228+
absl::StatusOr<MoqtStringTuple> tuple = ParseNameTuple(input);
229+
QUICHE_RETURN_IF_ERROR(tuple.status());
230+
if (tuple->size() < 3) {
231+
return absl::InvalidArgumentError("Full track name is missing elements");
232+
}
233+
std::string name(tuple->back());
234+
tuple->Pop();
235+
if (!tuple->back().empty()) {
236+
return absl::InvalidArgumentError(
237+
"Full track name must use -- as a separator");
238+
}
239+
tuple->Pop();
240+
if (tuple->size() == 1 && tuple->ValueAt(0) == "") {
241+
// Special case handling for empty namespace.
242+
tuple->Pop();
243+
}
244+
absl::StatusOr<TrackNamespace> ns = TrackNamespace::Create(*std::move(tuple));
245+
QUICHE_RETURN_IF_ERROR(ns.status());
246+
return Create(*std::move(ns), std::move(name));
247+
}
248+
114249
FullTrackName::FullTrackName(TrackNamespace ns, absl::string_view name)
115250
: namespace_(std::move(ns)), name_(name) {
116251
if (namespace_.total_length() + name.size() > kMaxFullTrackNameSize) {

quiche/quic/moqt/moqt_names.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class TrackNamespace {
3636
public:
3737
static absl::StatusOr<TrackNamespace> Create(MoqtStringTuple tuple);
3838

39+
static absl::StatusOr<TrackNamespace> Parse(absl::string_view input);
40+
3941
TrackNamespace() = default;
4042

4143
explicit TrackNamespace(std::initializer_list<absl::string_view> tuple);
@@ -110,6 +112,8 @@ class FullTrackName {
110112
static absl::StatusOr<FullTrackName> Create(TrackNamespace ns,
111113
std::string name);
112114

115+
static absl::StatusOr<FullTrackName> Parse(absl::string_view input);
116+
113117
FullTrackName() = default;
114118

115119
// Convenience constructor. QUICHE_BUGs if the resulting full track name is

quiche/quic/moqt/moqt_names_test.cc

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,26 @@
44

55
#include "quiche/quic/moqt/moqt_names.h"
66

7+
#include <string>
78
#include <utility>
9+
#include <vector>
810

911
#include "absl/hash/hash.h"
1012
#include "absl/status/status.h"
1113
#include "absl/status/statusor.h"
1214
#include "absl/strings/string_view.h"
1315
#include "quiche/common/platform/api/quiche_expect_bug.h"
16+
#include "quiche/common/platform/api/quiche_fuzztest.h"
1417
#include "quiche/common/platform/api/quiche_test.h"
18+
#include "quiche/common/quiche_status_utils.h"
1519
#include "quiche/common/test_tools/quiche_test_utils.h"
1620

1721
namespace moqt::test {
1822
namespace {
1923

24+
using ::quiche::test::IsOkAndHolds;
2025
using ::quiche::test::StatusIs;
26+
using ::testing::ElementsAre;
2127
using ::testing::HasSubstr;
2228

2329
TEST(MoqtNamesTest, TrackNamespaceConstructors) {
@@ -129,5 +135,100 @@ TEST(MoqtNamesTest, FullTrackNameTooLong) {
129135
"TrackNamspace constructor");
130136
}
131137

138+
absl::StatusOr<MoqtStringTuple> ParseNamespace(absl::string_view input) {
139+
absl::StatusOr<TrackNamespace> ns = TrackNamespace::Parse(input);
140+
QUICHE_RETURN_IF_ERROR(ns.status());
141+
return ns->tuple();
142+
}
143+
144+
TEST(MoqtNamesTest, ParseNamespace) {
145+
EXPECT_THAT(ParseNamespace(""), IsOkAndHolds(ElementsAre()));
146+
EXPECT_THAT(ParseNamespace("foo"), IsOkAndHolds(ElementsAre("foo")));
147+
EXPECT_THAT(ParseNamespace("foo-bar"),
148+
IsOkAndHolds(ElementsAre("foo", "bar")));
149+
EXPECT_THAT(ParseNamespace("foo-bar--test"),
150+
IsOkAndHolds(ElementsAre("foo", "bar", "", "test")));
151+
EXPECT_THAT(ParseNamespace("foo-.ff"),
152+
IsOkAndHolds(ElementsAre("foo", "\xff")));
153+
EXPECT_THAT(ParseNamespace("foo-.f"),
154+
StatusIs(absl::StatusCode::kInvalidArgument,
155+
"Incomplete escape sequence"));
156+
EXPECT_THAT(
157+
ParseNamespace("foo-.zz"),
158+
StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("Invalid hex")));
159+
EXPECT_THAT(
160+
ParseNamespace("foo-.FF"),
161+
StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("Invalid hex")));
162+
EXPECT_THAT(
163+
ParseNamespace("foo-.0z"),
164+
StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("Invalid hex")));
165+
EXPECT_THAT(ParseNamespace("foo-.61"),
166+
StatusIs(absl::StatusCode::kInvalidArgument,
167+
HasSubstr("Hex-encoding a safe character")));
168+
EXPECT_THAT(
169+
ParseNamespace("................"),
170+
StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("Invalid hex")));
171+
EXPECT_THAT(ParseNamespace("foo-\xff"),
172+
StatusIs(absl::StatusCode::kInvalidArgument,
173+
HasSubstr("Invalid character 0xff")));
174+
std::string long_string(2 * kMaxFullTrackNameSize, 'a');
175+
EXPECT_THAT(
176+
ParseNamespace(long_string),
177+
StatusIs(absl::StatusCode::kOutOfRange, "Maximum tuple size exceeded"));
178+
}
179+
180+
TEST(MoqtNamesTest, ParseFullTrackName) {
181+
EXPECT_THAT(FullTrackName::Parse("foo-bar--test"),
182+
IsOkAndHolds(FullTrackName({"foo", "bar"}, "test")));
183+
EXPECT_THAT(FullTrackName::Parse("foo--bar--test"),
184+
IsOkAndHolds(FullTrackName({"foo", "", "bar"}, "test")));
185+
EXPECT_THAT(FullTrackName::Parse("--test"),
186+
IsOkAndHolds(FullTrackName({}, "test")));
187+
EXPECT_THAT(
188+
FullTrackName::Parse("a-b-c-d-e-f"),
189+
StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("must use --")));
190+
EXPECT_THAT(FullTrackName::Parse("foo-bar"),
191+
StatusIs(absl::StatusCode::kInvalidArgument,
192+
HasSubstr("missing elements")));
193+
}
194+
195+
void FuzzParseNamespace(absl::string_view encoded_namespace) {
196+
(void)TrackNamespace::Parse(encoded_namespace);
197+
}
198+
199+
void FuzzParseFullTrackName(absl::string_view encoded_track_name) {
200+
(void)FullTrackName::Parse(encoded_track_name);
201+
}
202+
203+
void SerializeAndParseNamespace(const std::vector<std::string>& elements) {
204+
if (elements.empty() || elements[0].empty()) {
205+
return;
206+
}
207+
208+
TrackNamespace ns;
209+
for (const std::string& element : elements) {
210+
if (!ns.AddElement(element)) {
211+
return;
212+
}
213+
}
214+
absl::StatusOr<TrackNamespace> round_trip_result =
215+
TrackNamespace::Parse(ns.ToString());
216+
QUICHE_ASSERT_OK(round_trip_result.status());
217+
EXPECT_EQ(ns, *round_trip_result);
218+
}
219+
220+
void ParseAndSerializeNamespace(absl::string_view encoded_namespace) {
221+
absl::StatusOr<TrackNamespace> ns = TrackNamespace::Parse(encoded_namespace);
222+
if (!ns.ok()) {
223+
return;
224+
}
225+
EXPECT_EQ(ns->ToString(), encoded_namespace);
226+
}
227+
228+
FUZZ_TEST(MoqtNamesTest, FuzzParseNamespace);
229+
FUZZ_TEST(MoqtNamesTest, FuzzParseFullTrackName);
230+
FUZZ_TEST(MoqtNamesTest, SerializeAndParseNamespace);
231+
FUZZ_TEST(MoqtNamesTest, ParseAndSerializeNamespace);
232+
132233
} // namespace
133234
} // namespace moqt::test

0 commit comments

Comments
 (0)