moqtap_proxy/shape/matcher.rs
1//! Class matching — what a [`ClassRule`](super::ClassRule) claims.
2//!
3//! Two types live here: [`RangeSet`], the hand-rolled sorted/coalesced
4//! `u64` range container the proxy uses instead of pulling in a dependency
5//! (nothing in the workspace offers one), and [`Matcher`], the AND of the
6//! eight keys a rule may name.
7//!
8//! The one rule that shapes everything below: **an absent key never
9//! matches.** A unit whose `subgroup_id` the wire did not carry does not
10//! match a `subgroup_id` matcher — it falls to the default class. `None`
11//! as wildcard was rejected outright, because it silently widens a rule
12//! aimed at video to also match audio.
13
14use std::cmp::Ordering;
15use std::ops::RangeInclusive;
16
17use moqtap_codec::dispatch::AnyDatagramMeta;
18use moqtap_codec::version::DraftVersion;
19
20use crate::types::{DataStreamType, ObjectMeta, ProxySide};
21
22/// A sorted, coalesced set of inclusive `u64` ranges.
23///
24/// Built once from a configuration and then queried per unit, so
25/// construction sorts and coalesces (including *adjacent* ranges: `1..=3`
26/// and `4..=6` become `1..=6`) and [`RangeSet::contains`] is a binary
27/// search over the result.
28///
29/// Ranges whose start exceeds their end are empty and are discarded at
30/// construction rather than stored as a range that can never match.
31///
32/// `#[non_exhaustive]` with no `Default`: the fields are private and there
33/// are two constructors, so there is no meaningful zero value to derive.
34///
35/// # The written form is a plain list
36///
37/// Under the `serde` feature a range set is written as the list of ranges
38/// it was built from — `[{"start": 1, "end": 3}, {"start": 4, "end": 6}]` —
39/// and read back **through [`RangeSet::new`]**, which is what
40/// `#[serde(from = ...)]` buys. A derived `Deserialize` would fill the private
41/// `ranges` field straight from the file, and the invariant every method here
42/// relies on — sorted, disjoint, non-adjacent — would then hold only for files
43/// that happened to be written in order. [`RangeSet::contains`] is a binary
44/// search, so on an unsorted set it does not fail: it answers `false` for
45/// values that are in the set, and the class quietly stops claiming half its
46/// traffic.
47///
48/// Two consequences of routing through the constructor are worth knowing
49/// before reading a file back. The written form is *normalised*, so the two
50/// ranges above are one range when they are read and the file that comes back
51/// out says `[{"start": 1, "end": 6}]`. And an inverted range is dropped
52/// rather than stored, so a file whose only range is `{"start": 5, "end": 1}`
53/// produces an empty set — which
54/// [`ShapeProfile::try_new`](super::ShapeProfile::try_new) then refuses as
55/// [`ShapeError::InertMatcher`](super::ShapeError::InertMatcher) rather than
56/// arming a class that can never claim anything.
57#[derive(Debug, Clone, PartialEq, Eq)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59#[cfg_attr(
60 feature = "serde",
61 serde(from = "Vec<RangeInclusive<u64>>", into = "Vec<RangeInclusive<u64>>")
62)]
63#[non_exhaustive]
64pub struct RangeSet {
65 /// Disjoint, non-adjacent, ascending by start. The invariant every
66 /// method below relies on.
67 ranges: Vec<RangeInclusive<u64>>,
68}
69
70impl RangeSet {
71 /// Build a range set from any iterator of inclusive ranges.
72 ///
73 /// The input needs no ordering: overlapping, adjacent and duplicated
74 /// ranges are merged, and empty ranges (`start > end`) are dropped.
75 pub fn new(ranges: impl IntoIterator<Item = RangeInclusive<u64>>) -> Self {
76 let mut raw: Vec<RangeInclusive<u64>> =
77 ranges.into_iter().filter(|r| r.start() <= r.end()).collect();
78 raw.sort_by_key(|r| (*r.start(), *r.end()));
79
80 let mut merged: Vec<RangeInclusive<u64>> = Vec::with_capacity(raw.len());
81 for r in raw {
82 match merged.last_mut() {
83 // `+1` is what makes `1..=3` and `4..=6` one range rather
84 // than two: they are adjacent, not overlapping.
85 // `saturating_add` so a range ending at `u64::MAX` does not
86 // panic in a debug build.
87 Some(last) if *r.start() <= last.end().saturating_add(1) => {
88 if r.end() > last.end() {
89 *last = *last.start()..=*r.end();
90 }
91 }
92 _ => merged.push(r),
93 }
94 }
95 Self { ranges: merged }
96 }
97
98 /// A range set holding exactly one value.
99 pub fn single(v: u64) -> Self {
100 Self { ranges: vec![v..=v] }
101 }
102
103 /// The ranges as a plain list, which is also the written form.
104 ///
105 /// Consumes the set rather than borrowing it, because this is what
106 /// `#[serde(into = ...)]` calls and the alternative — serializing the
107 /// private field directly — would emit `{"ranges": [...]}` while the read
108 /// path expects `[...]`, so the two directions would disagree.
109 #[cfg(feature = "serde")]
110 fn into_ranges(self) -> Vec<RangeInclusive<u64>> {
111 self.ranges
112 }
113
114 /// Whether `v` falls in any of the ranges. Binary search.
115 pub fn contains(&self, v: u64) -> bool {
116 self.ranges
117 .binary_search_by(|r| {
118 if *r.end() < v {
119 Ordering::Less
120 } else if *r.start() > v {
121 Ordering::Greater
122 } else {
123 Ordering::Equal
124 }
125 })
126 .is_ok()
127 }
128
129 /// Whether the set holds no values at all. Such a set matches nothing.
130 pub fn is_empty(&self) -> bool {
131 self.ranges.is_empty()
132 }
133
134 /// The coalesced ranges, ascending and disjoint.
135 ///
136 /// Exposed because coalescing is a *claim* — that `1..=3` plus `4..=6`
137 /// is one range — and [`RangeSet::contains`] cannot falsify it: both
138 /// shapes answer every `contains` query identically. A test that can
139 /// only see `contains` cannot tell a working coalescer from none.
140 pub fn ranges(&self) -> &[RangeInclusive<u64>] {
141 &self.ranges
142 }
143}
144
145/// Every read of a range set goes through [`RangeSet::new`], so the sorted,
146/// coalesced invariant holds for a set that came from a file exactly as it
147/// does for one built in Rust.
148#[cfg(feature = "serde")]
149impl From<Vec<RangeInclusive<u64>>> for RangeSet {
150 fn from(ranges: Vec<RangeInclusive<u64>>) -> Self {
151 RangeSet::new(ranges)
152 }
153}
154
155#[cfg(feature = "serde")]
156impl From<RangeSet> for Vec<RangeInclusive<u64>> {
157 fn from(set: RangeSet) -> Self {
158 set.into_ranges()
159 }
160}
161
162/// What a [`Matcher`] can be aimed at.
163///
164/// `Datagram` exists so that aiming a rule at datagrams produces a report
165/// rather than silence; datagrams are not shapeable in this release, so a
166/// `Datagram` matcher never matches a unit. The report is the scheduler's
167/// job once this module is wired.
168///
169/// `Fetch` is live on all thirteen. It was not always: drafts 18 and 19
170/// write a fetch object's Group ID as a difference whose sign the fetch's
171/// Group Order settles, and while nothing carried that order to the framer a
172/// fetch stream there was bypassed at its header and produced no
173/// [`ObjectMeta`] for a rule to see. The session reads the order off the
174/// FETCH now — `capability::fetch_group_order_is_needed` — so what is left is
175/// one stream at a time rather than a whole draft, and a stream the session
176/// cannot resolve says so itself as
177/// `Impairment { FramerBypass { FetchGroupOrderUnknown } }`.
178///
179/// Being matchable is not being *mutable*, and the two are answered
180/// separately: whether a rule claims a unit is [`Matcher::matches`], and what
181/// may then be done to it is `capability::classify`.
182///
183/// `#[non_exhaustive]` with no `Default`: there is no meaningful default
184/// stream kind, and a wrong one would silently narrow every rule that
185/// omitted the key.
186///
187/// Written as `"subgroup"`, `"fetch"` or `"datagram"`.
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
189#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
190#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
191#[non_exhaustive]
192pub enum MatchKind {
193 /// A subgroup data stream.
194 Subgroup,
195 /// A fetch response data stream.
196 Fetch,
197 /// A datagram.
198 Datagram,
199}
200
201impl MatchKind {}
202
203/// One [`Matcher`] key, named so a report about it is typed rather than a
204/// string.
205///
206/// Only the keys that can be **unmatchable** are here, and that is the whole
207/// point of the type: it is carried by
208/// [`ImpairmentKind::ShapeRuleUnmatchable`](crate::event::ImpairmentKind::ShapeRuleUnmatchable),
209/// which fires when a rule keys on something this draft and stream kind
210/// cannot carry. The other four keys — `side`, `group_id`, `object_id` and
211/// `every_nth` — are present on every framed unit by construction, so a rule
212/// keyed on one of them that fails to match failed on its *value*, which is
213/// the rule working, not a rule that cannot work.
214///
215/// `#[non_exhaustive]` with no `Default`: a later draft that makes another
216/// key optional adds a variant, and no key is a meaningful zero.
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
218#[non_exhaustive]
219pub enum MatcherField {
220 /// [`Matcher::track_alias`], against a unit whose stream carried none —
221 /// every fetch stream.
222 TrackAlias,
223 /// [`Matcher::subgroup_id`], against a unit whose header carried none —
224 /// eight drafts in first-object mode, and 17-19 in reserved mode 3.
225 ///
226 /// Never reported about a datagram, which carries no subgroup ID on any
227 /// draft. That is not a fact about one header, so it is a pre-run
228 /// refusal — `Capabilities::admit_class` — rather than something a run
229 /// discovers. See `Matcher::unmatchable_fields_datagram`.
230 SubgroupId,
231 /// [`Matcher::priority`], against a unit whose header set the
232 /// default-priority bit — drafts 15-19, on a subgroup object and on a
233 /// datagram alike.
234 Priority,
235}
236
237impl MatcherField {
238 /// This field's bit in a per-class report-once mask.
239 ///
240 /// A four-variant enum fits one byte, so the "reported already" state
241 /// for a whole class is one `AtomicU8` and the check is one relaxed
242 /// `fetch_or` — taken only when a field is *actually* absent, so a
243 /// profile whose keys are all carried never touches it.
244 pub(crate) const fn bit(self) -> u8 {
245 match self {
246 MatcherField::TrackAlias => 1,
247 MatcherField::SubgroupId => 2,
248 MatcherField::Priority => 4,
249 }
250 }
251}
252
253/// Which units a [`ClassRule`](super::ClassRule) claims.
254///
255/// All present fields must match (AND). An absent field matches
256/// everything. A field the wire did not carry — `None` on
257/// [`ObjectMeta`] — **does not match**.
258///
259/// `#[non_exhaustive]` *with* a [`Default`], exactly as
260/// [`EgressConfig`](crate::action::EgressConfig) is. The pairing is
261/// load-bearing: `#[non_exhaustive]` alone would make this type
262/// unconstructible from an integration-test crate or from a scenario
263/// author's code, because struct-expression *and* functional-update syntax
264/// are both illegal outside the defining crate. Note what that leaves:
265/// `..Matcher::default()` is **also** illegal there (`E0639`), so an
266/// outside caller writes `let mut m = Matcher::default();` and then assigns
267/// per field — which is what `tests/actions_shaping.rs` does. Inside this
268/// crate both forms compile, which is why the unit tests below use the
269/// shorter one. The `Default` is all-`None` — a matcher that claims every
270/// unit.
271///
272/// In the written form every key defaults to absent, so a matcher naming one
273/// field is one line, and an unknown key is refused rather than skipped — a
274/// misspelled `group_id` would otherwise widen the rule to claim every unit
275/// on the stream instead of the ten groups it named.
276#[derive(Debug, Clone, PartialEq, Eq, Default)]
277#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
278#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
279#[non_exhaustive]
280pub struct Matcher {
281 /// The direction the unit arrived on.
282 ///
283 /// Only `ClientToProxy` and `RelayToProxy` are ever seen at a hook
284 /// site; the two egress labels are used for teardown reporting. Naming
285 /// an egress side here is rejected by
286 /// [`ShapeProfile::try_new`](super::ShapeProfile::try_new) rather than
287 /// left to match nothing.
288 ///
289 /// Written as the variant's own name in kebab-case —
290 /// `"client-to-proxy"` — by a mapping in the scenario module rather than
291 /// by a derive, because [`ProxySide`] lives in a module that carries no
292 /// serde dependency of its own.
293 #[cfg_attr(feature = "serde", serde(with = "side_serde"))]
294 pub side: Option<ProxySide>,
295 /// Track alias from the stream header. `None` on every fetch stream,
296 /// so a rule keyed here never claims fetch units.
297 pub track_alias: Option<RangeSet>,
298 /// Group ID. Always present on a framed object.
299 pub group_id: Option<RangeSet>,
300 /// Subgroup ID. `None` on eight drafts in first-object mode and on
301 /// 17-19 in reserved mode 3, so a rule keyed here claims nothing there.
302 pub subgroup_id: Option<RangeSet>,
303 /// Absolute object ID. Always present on a framed object.
304 pub object_id: Option<RangeSet>,
305 /// MoQT `publisher_priority`. `None` on drafts 15-19 whenever the
306 /// header set the default-priority bit.
307 pub priority: Option<RangeInclusive<u8>>,
308 /// Which kind of stream the unit came from.
309 pub stream_kind: Option<MatchKind>,
310 /// `(n, offset)` over a counter of **hook-visible units on this
311 /// stream**, scoped **per stream and not per class**, never
312 /// [`ObjectMeta::index_in_stream`] — which counts oversized objects
313 /// that never reach the hook and would silently shift the pattern.
314 ///
315 /// Matches when `unit_index % n == offset % n`. `n == 0` names no
316 /// units, so [`ShapeProfile::try_new`](super::ShapeProfile::try_new)
317 /// rejects it as [`ShapeError::InertMatcher`](super::ShapeError::InertMatcher)
318 /// rather than accepting a class that can never claim anything.
319 pub every_nth: Option<(u64, u64)>,
320}
321
322impl Matcher {
323 /// Whether this matcher claims one unit.
324 ///
325 /// `side` is the forwarding task's own direction label — [`ObjectMeta`]
326 /// has no `side` field. `unit_index` is the per-stream count of
327 /// hook-visible units described on [`Matcher::every_nth`], supplied by
328 /// the caller for the same reason `charge` takes `now`: this function
329 /// owns no state and reads no counter, so it can be tested exhaustively
330 /// from a table.
331 pub fn matches(&self, side: ProxySide, meta: &ObjectMeta, unit_index: u64) -> bool {
332 self.claims(
333 side,
334 kind_of(meta.stream_kind),
335 Keys {
336 track_alias: meta.track_alias,
337 group_id: meta.group_id,
338 subgroup_id: meta.subgroup_id,
339 object_id: meta.object_id,
340 priority: meta.publisher_priority,
341 },
342 unit_index,
343 )
344 }
345
346 /// Whether this matcher claims one datagram.
347 ///
348 /// [`Self::matches`]'s sibling, and the two answer through one
349 /// conjunction — see the private `Keys` it is written over. What differs
350 /// is what fills that in: a
351 /// datagram states its own track alias, Group ID, Object ID and (from
352 /// draft-15, conditionally) priority, and states **no subgroup ID** on
353 /// any draft, so a rule keyed there claims no datagram. That last one is
354 /// a fact about the carrier rather than about one header, so it is
355 /// refused before the run by
356 /// [`Capabilities::admit_class`](crate::capability::Capabilities::admit_class)
357 /// rather than discovered from one.
358 ///
359 /// `unit_index` counts hook-visible datagrams **per forwarding
360 /// direction**, which is the only scope a datagram has: it belongs to no
361 /// stream, so [`Matcher::every_nth`]'s per-stream reading has nothing to
362 /// key against here and the session's two directions count separately.
363 pub fn matches_datagram(
364 &self,
365 side: ProxySide,
366 meta: &AnyDatagramMeta,
367 unit_index: u64,
368 ) -> bool {
369 self.claims(
370 side,
371 MatchKind::Datagram,
372 Keys {
373 track_alias: Some(meta.track_alias),
374 group_id: meta.group_id,
375 subgroup_id: None,
376 object_id: meta.object_id,
377 priority: meta.publisher_priority,
378 },
379 unit_index,
380 )
381 }
382
383 /// The conjunction both carriers answer through.
384 ///
385 /// Written once rather than twice because a six-key AND copied is a
386 /// sixth key forgotten: a matcher key added to this struct and to only
387 /// one of the two callers would widen every rule on the other carrier,
388 /// silently, in the direction of claiming more than it named.
389 fn claims(&self, side: ProxySide, kind: MatchKind, keys: Keys, unit_index: u64) -> bool {
390 if let Some(want) = self.side {
391 if want != side {
392 return false;
393 }
394 }
395 if let Some(want) = self.stream_kind {
396 if want != kind {
397 return false;
398 }
399 }
400 // A keyed field the wire did not carry never matches: the three
401 // `Option` keys below take the `_ => return false` arm on `None`.
402 if let Some(set) = &self.track_alias {
403 match keys.track_alias {
404 Some(v) if set.contains(v) => {}
405 _ => return false,
406 }
407 }
408 if let Some(set) = &self.group_id {
409 if !set.contains(keys.group_id) {
410 return false;
411 }
412 }
413 if let Some(set) = &self.subgroup_id {
414 match keys.subgroup_id {
415 Some(v) if set.contains(v) => {}
416 _ => return false,
417 }
418 }
419 if let Some(set) = &self.object_id {
420 if !set.contains(keys.object_id) {
421 return false;
422 }
423 }
424 if let Some(range) = &self.priority {
425 match keys.priority {
426 Some(p) if range.contains(&p) => {}
427 _ => return false,
428 }
429 }
430 if let Some((n, offset)) = self.every_nth {
431 if n == 0 || unit_index % n != offset % n {
432 return false;
433 }
434 }
435 true
436 }
437
438 /// The keys this matcher names that `meta` **cannot carry**, so a
439 /// non-match against them is a rule that can never fire rather than a
440 /// rule that did not fire.
441 ///
442 /// The distinction is the whole point of this function: `None` never
443 /// matches, and a silent fall to the default class is precisely the
444 /// failure mode this project exists to prevent. The caller reports each
445 /// `(class, field)` once per session.
446 ///
447 /// Returns a fixed-size array rather than a `Vec` — it is called on the
448 /// data path, once per rule that failed to match, and must not
449 /// allocate. `[None; 3]` is the answer for a matcher whose keys are all
450 /// carried, which is the common case.
451 ///
452 /// Every row here is a key **this unit** did not carry. There is no row
453 /// for a key no unit of this draft could ever carry, because there is no
454 /// longer such a key: the one candidate was a `Fetch`-aimed class on
455 /// drafts 18 and 19, and a fetch stream there is addressed now — see
456 /// [`MatchKind::Fetch`].
457 ///
458 /// Only checked *after* [`Self::matches`] has answered `false`: a rule
459 /// that matched cannot have been defeated by an absent key.
460 pub(crate) fn unmatchable_fields(&self, meta: &ObjectMeta) -> [Option<MatcherField>; 3] {
461 [
462 (self.track_alias.is_some() && meta.track_alias.is_none())
463 .then_some(MatcherField::TrackAlias),
464 (self.subgroup_id.is_some() && meta.subgroup_id.is_none())
465 .then_some(MatcherField::SubgroupId),
466 (self.priority.is_some() && meta.publisher_priority.is_none())
467 .then_some(MatcherField::Priority),
468 ]
469 }
470
471 /// [`Self::unmatchable_fields`]'s datagram sibling: the keys this matcher
472 /// names that **this datagram** could not carry.
473 ///
474 /// One of the three is answered here and two are deliberately not.
475 ///
476 /// * [`MatcherField::Priority`] is reported on the same terms as on a
477 /// framed object — drafts 15 and later let a datagram's type byte set a
478 /// default-priority bit and leave the field off, and a rule keyed on
479 /// priority cannot claim one that did.
480 /// * [`MatcherField::TrackAlias`] never: every datagram of every draft
481 /// states one.
482 /// * [`MatcherField::SubgroupId`] never, and this is the interesting
483 /// one. No datagram carries a subgroup ID on any draft, so a
484 /// *datagram-aimed* rule keyed there is refused before the session
485 /// starts — `Capabilities::admit_class`, which is where a key a
486 /// carrier never has belongs, because rejecting beats reporting
487 /// wherever the answer exists without traffic. A rule that names no
488 /// stream kind and keys on `subgroup_id` is a live subgroup rule, and
489 /// reporting it here because a datagram went past would be a
490 /// diagnostic about a rule that works.
491 pub(crate) fn unmatchable_fields_datagram(
492 &self,
493 draft: DraftVersion,
494 meta: &AnyDatagramMeta,
495 ) -> [Option<MatcherField>; 3] {
496 let _ = draft;
497 [
498 None,
499 None,
500 (self.priority.is_some() && meta.publisher_priority.is_none())
501 .then_some(MatcherField::Priority),
502 ]
503 }
504
505 /// The first key this matcher names that can **never** claim a unit —
506 /// not because the wire withheld it, but because the key itself names
507 /// an empty set of values.
508 /// The crate-internal `unmatchable_fields`'s sibling, and the difference is
509 /// where each is answerable: an unmatchable *field* depends on the draft and
510 /// the unit, so it can only be reported during a run, while an inert *key*
511 /// is a property of the configuration alone and is therefore
512 /// [`ShapeProfile::try_new`](super::ShapeProfile::try_new)'s to reject
513 /// before a session ever starts. Rejecting is strictly better than
514 /// reporting: a class that can never fire is the *configuration that looks
515 /// applied and does nothing* that constructor exists to prevent, and here
516 /// it is knowable without a single byte of traffic.
517 ///
518 /// Public because a caller that builds matchers from its own configuration
519 /// needs the same pre-flight refusal `try_new` gets: a matcher handed to a
520 /// [`ProxyHook`](crate::hook::ProxyHook) rather than to a shaping class
521 /// reaches no constructor that could check it.
522 ///
523 /// The three shapes, each of which `try_new` used to accept:
524 ///
525 /// - a [`RangeSet`] built from an inverted range — [`RangeSet::new`]
526 /// drops `start > end`, so the set is empty and `contains` is always
527 /// `false`;
528 /// - an empty [`Matcher::priority`] range (`200..=100`);
529 /// - [`Matcher::every_nth`] with `n == 0`, which
530 /// [`Self::matches`] answers `false` for unconditionally.
531 ///
532 /// Returns the key's field name as it is spelled on this struct, so the
533 /// error message names something the author can search their own
534 /// configuration for.
535 pub fn inert_key(&self) -> Option<&'static str> {
536 let empty_set = |set: &Option<RangeSet>| set.as_ref().is_some_and(RangeSet::is_empty);
537 if empty_set(&self.track_alias) {
538 return Some("track_alias");
539 }
540 if empty_set(&self.group_id) {
541 return Some("group_id");
542 }
543 if empty_set(&self.subgroup_id) {
544 return Some("subgroup_id");
545 }
546 if empty_set(&self.object_id) {
547 return Some("object_id");
548 }
549 if self.priority.as_ref().is_some_and(RangeInclusive::is_empty) {
550 return Some("priority");
551 }
552 if matches!(self.every_nth, Some((0, _))) {
553 return Some("every_nth");
554 }
555 None
556 }
557}
558
559/// The keys one unit carries, whichever carrier it arrived on.
560///
561/// The argument to [`Matcher::claims`], and the reason a datagram and a
562/// framed object can be answered by one conjunction. Two of the five differ
563/// between the carriers and the difference is the type's whole content: a
564/// fetch object carries no track alias and a datagram carries no subgroup ID.
565struct Keys {
566 track_alias: Option<u64>,
567 group_id: u64,
568 subgroup_id: Option<u64>,
569 object_id: u64,
570 priority: Option<u8>,
571}
572
573/// Which [`MatchKind`] a framed object's stream is.
574///
575/// The `Fetch` arm is **live**, on all thirteen drafts:
576/// `detect_stream_type` maps stream type `0x05` to
577/// [`DataStreamType::Fetch`] and the framer produces ordinary [`ObjectMeta`]
578/// for it. On drafts 18 and 19 that needs the fetch's Group Order, which the
579/// session reads off the FETCH before the response opens; a response naming a
580/// request nobody made is bypassed and says so per stream, and produces no
581/// meta for a rule to be measured against either way.
582/// `the_fetch_arm_claims_a_fetch_object` is what keeps the arm from being
583/// deletable without a red.
584fn kind_of(stream_kind: DataStreamType) -> MatchKind {
585 match stream_kind {
586 DataStreamType::Subgroup => MatchKind::Subgroup,
587 DataStreamType::Fetch => MatchKind::Fetch,
588 }
589}
590
591/// The written form of [`ProxySide`](crate::event::ProxySide), which lives in
592/// a module this schema does not add derives to.
593///
594/// Four names, kebab-case, exactly the variant names. Written out here rather
595/// than derived so that the enum stays free of a serde dependency it has no
596/// other use for; the cost is that a fifth side would compile and be
597/// unwritable, which is why the mapping is exhaustive in both directions and
598/// has no wildcard arm.
599#[cfg(feature = "serde")]
600pub(crate) mod side_serde {
601 use serde::de::Error as _;
602 use serde::{Deserialize, Deserializer, Serializer};
603
604 use crate::types::ProxySide;
605
606 /// Every side, and the name it is written as.
607 const NAMES: [(ProxySide, &str); 4] = [
608 (ProxySide::ClientToProxy, "client-to-proxy"),
609 (ProxySide::ProxyToRelay, "proxy-to-relay"),
610 (ProxySide::RelayToProxy, "relay-to-proxy"),
611 (ProxySide::ProxyToClient, "proxy-to-client"),
612 ];
613
614 /// The name of one side. No wildcard arm: a fifth variant is a compile
615 /// error here rather than a side that writes itself as something else.
616 fn name(side: ProxySide) -> &'static str {
617 match side {
618 ProxySide::ClientToProxy => NAMES[0].1,
619 ProxySide::ProxyToRelay => NAMES[1].1,
620 ProxySide::RelayToProxy => NAMES[2].1,
621 ProxySide::ProxyToClient => NAMES[3].1,
622 }
623 }
624
625 /// Serialize an optional side as its name.
626 pub(crate) fn serialize<S: Serializer>(
627 side: &Option<ProxySide>,
628 serializer: S,
629 ) -> Result<S::Ok, S::Error> {
630 match side {
631 None => serializer.serialize_none(),
632 Some(side) => serializer.serialize_some(name(*side)),
633 }
634 }
635
636 /// Read an optional side, listing every accepted name if it is not one.
637 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
638 deserializer: D,
639 ) -> Result<Option<ProxySide>, D::Error> {
640 let written = <Option<String>>::deserialize(deserializer)?;
641 let Some(written) = written else { return Ok(None) };
642 NAMES.iter().find(|(_, name)| *name == written).map(|(side, _)| Some(*side)).ok_or_else(
643 || {
644 D::Error::custom(format!(
645 "unknown side {written:?}, expected one of {:?}",
646 NAMES.map(|(_, name)| name)
647 ))
648 },
649 )
650 }
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656 use moqtap_codec::version::DraftVersion;
657
658 /// All thirteen, so a claim about "every draft" is one rather than a
659 /// sample. Nothing here decodes, so an uncompiled draft is as
660 /// answerable as a compiled one.
661 const DRAFTS: [DraftVersion; 13] = [
662 DraftVersion::Draft07,
663 DraftVersion::Draft08,
664 DraftVersion::Draft09,
665 DraftVersion::Draft10,
666 DraftVersion::Draft11,
667 DraftVersion::Draft12,
668 DraftVersion::Draft13,
669 DraftVersion::Draft14,
670 DraftVersion::Draft15,
671 DraftVersion::Draft16,
672 DraftVersion::Draft17,
673 DraftVersion::Draft18,
674 DraftVersion::Draft19,
675 ];
676
677 /// A framed subgroup object with every optional key present, so a test
678 /// can knock exactly one out and attribute the result.
679 fn meta() -> ObjectMeta {
680 ObjectMeta {
681 draft: DraftVersion::Draft19,
682 stream_kind: DataStreamType::Subgroup,
683 track_alias: Some(7),
684 group_id: 3,
685 subgroup_id: Some(4),
686 object_id: 11,
687 publisher_priority: Some(128),
688 index_in_stream: 0,
689 payload_len: 16,
690 status: None,
691 end_of_range: None,
692 }
693 }
694
695 /// Coalescing, boundaries and the empty set.
696 ///
697 /// The `ranges()` assertions are the load-bearing half: `contains`
698 /// alone cannot tell `[1..=3, 4..=6]` from `[1..=6]`.
699 #[test]
700 fn a_range_set_contains_only_its_ranges() {
701 // Empty.
702 let empty = RangeSet::new([]);
703 assert!(empty.is_empty());
704 assert!(!empty.contains(0));
705 assert!(!empty.contains(u64::MAX));
706
707 // Single.
708 let one = RangeSet::single(7);
709 assert_eq!(one.ranges(), &[7..=7]);
710 assert!(one.contains(7));
711 assert!(!one.contains(6));
712 assert!(!one.contains(8));
713
714 // Unsorted input is sorted; disjoint ranges stay disjoint.
715 let two = RangeSet::new([5..=9, 1..=3]);
716 assert_eq!(two.ranges(), &[1..=3, 5..=9]);
717 for v in [1, 2, 3, 5, 6, 9] {
718 assert!(two.contains(v), "should contain {v}");
719 }
720 for v in [0, 4, 10] {
721 assert!(!two.contains(v), "should not contain {v}");
722 }
723
724 // Adjacency coalesces. This is the `+1`.
725 assert_eq!(RangeSet::new([1..=3, 4..=6]).ranges(), &[1..=6]);
726 // Overlap coalesces, and the wider end wins.
727 assert_eq!(RangeSet::new([1..=5, 3..=9]).ranges(), &[1..=9]);
728 // A range wholly inside another is absorbed, not appended.
729 assert_eq!(RangeSet::new([1..=9, 3..=5]).ranges(), &[1..=9]);
730 // A one-value gap is NOT adjacency.
731 assert_eq!(RangeSet::new([1..=3, 5..=6]).ranges(), &[1..=3, 5..=6]);
732
733 // Empty ranges are dropped rather than stored. Bound through
734 // locals: a literal `9..=1` is a clippy error at the call site,
735 // which is exactly the case a configuration built at runtime can
736 // still produce.
737 let (lo, hi) = (9u64, 1u64);
738 assert!(RangeSet::new([lo..=hi]).is_empty());
739 assert_eq!(RangeSet::new([lo..=hi, 2..=4]).ranges(), &[2..=4]);
740
741 // The top of the space does not panic and does not wrap.
742 let top = RangeSet::new([u64::MAX..=u64::MAX]);
743 assert!(top.contains(u64::MAX));
744 assert!(!top.contains(u64::MAX - 1));
745 }
746
747 /// An absent key does not match — `None` is never a wildcard.
748 #[test]
749 fn an_absent_key_does_not_match() {
750 let keyed = Matcher { subgroup_id: Some(RangeSet::single(4)), ..Matcher::default() };
751
752 // Positive control: the key is present and in range.
753 assert!(keyed.matches(ProxySide::ClientToProxy, &meta(), 0));
754
755 // The wire did not carry a subgroup ID: no match, and the value the
756 // codec would have stored (zero) is not consulted.
757 let absent = ObjectMeta { subgroup_id: None, ..meta() };
758 assert!(!keyed.matches(ProxySide::ClientToProxy, &absent, 0));
759
760 // Present but out of range: also no match, so the assertion above
761 // is about absence and not about the range.
762 let other = ObjectMeta { subgroup_id: Some(5), ..meta() };
763 assert!(!keyed.matches(ProxySide::ClientToProxy, &other, 0));
764
765 // The same rule on the other two `Option` keys.
766 let by_alias = Matcher { track_alias: Some(RangeSet::single(7)), ..Matcher::default() };
767 assert!(by_alias.matches(ProxySide::ClientToProxy, &meta(), 0));
768 let fetch = ObjectMeta { track_alias: None, ..meta() };
769 assert!(!by_alias.matches(ProxySide::ClientToProxy, &fetch, 0));
770
771 let by_priority = Matcher { priority: Some(0..=200), ..Matcher::default() };
772 assert!(by_priority.matches(ProxySide::ClientToProxy, &meta(), 0));
773 let defaulted = ObjectMeta { publisher_priority: None, ..meta() };
774 assert!(!by_priority.matches(ProxySide::ClientToProxy, &defaulted, 0));
775
776 // An all-absent matcher still claims everything, so "None never
777 // matches" is about a *keyed* field and not about the matcher.
778 let any = Matcher::default();
779 assert!(any.matches(ProxySide::ClientToProxy, &absent, 0));
780 assert!(any.matches(ProxySide::RelayToProxy, &fetch, 9));
781 }
782
783 /// The remaining matcher keys, so `matches` is not gated by
784 /// `an_absent_key_does_not_match` alone.
785 #[test]
786 fn the_other_keys_and_are_ends_a_match() {
787 let m = Matcher {
788 side: Some(ProxySide::RelayToProxy),
789 group_id: Some(RangeSet::new([0..=3])),
790 object_id: Some(RangeSet::new([10..=12])),
791 stream_kind: Some(MatchKind::Subgroup),
792 every_nth: Some((3, 1)),
793 ..Matcher::default()
794 };
795 assert!(m.matches(ProxySide::RelayToProxy, &meta(), 4));
796
797 // Each key alone flips the AND to false.
798 assert!(!m.matches(ProxySide::ClientToProxy, &meta(), 4));
799 assert!(!m.matches(ProxySide::RelayToProxy, &ObjectMeta { group_id: 4, ..meta() }, 4));
800 assert!(!m.matches(ProxySide::RelayToProxy, &ObjectMeta { object_id: 13, ..meta() }, 4));
801 let fetch = ObjectMeta { stream_kind: DataStreamType::Fetch, ..meta() };
802 assert!(!m.matches(ProxySide::RelayToProxy, &fetch, 4));
803 assert!(!m.matches(ProxySide::RelayToProxy, &meta(), 5));
804
805 // A datagram rule never claims a framed object, on either kind.
806 let dgram = Matcher { stream_kind: Some(MatchKind::Datagram), ..Matcher::default() };
807 assert!(!dgram.matches(ProxySide::ClientToProxy, &meta(), 0));
808 assert!(!dgram.matches(ProxySide::ClientToProxy, &fetch, 0));
809
810 // `n == 0` names no units.
811 let never = Matcher { every_nth: Some((0, 0)), ..Matcher::default() };
812 assert!(!never.matches(ProxySide::ClientToProxy, &meta(), 0));
813 }
814
815 /// **A `Fetch`-aimed rule claims a fetch object.** The positive half of
816 /// `kind_matches`, which nothing exercised: `the_other_keys_and_ends_a_match`
817 /// only asserts that a *Subgroup* rule rejects a fetch meta, and that
818 /// stays green with the `Fetch` arm deleted.
819 ///
820 /// The three assertions are the three things one arm has to get right:
821 /// the kind it names matches, the *other* stream kind does not, and the
822 /// `Datagram` kind matches neither — so a `kind_matches` rewritten as
823 /// `want != Datagram` would still redden.
824 ///
825 /// *Ablation, recorded:* delete `| (MatchKind::Fetch,
826 /// DataStreamType::Fetch)` from `kind_matches`. Before this test the
827 /// whole crate stayed green at 392 passed / 0 failed / 0 filtered out —
828 /// a matcher arm with no gate at all. With it:
829 ///
830 /// ```text
831 /// thread '...the_fetch_arm_claims_a_fetch_object' panicked at
832 /// crates\moqtap-proxy\src\shape\matcher.rs:597:9:
833 /// a Fetch-aimed class must claim a fetch object: the arm is live on the eight
834 /// drafts that have a fetch object codec
835 /// ```
836 #[test]
837 fn the_fetch_arm_claims_a_fetch_object() {
838 let fetch = ObjectMeta {
839 draft: DraftVersion::Draft14,
840 stream_kind: DataStreamType::Fetch,
841 track_alias: None,
842 ..meta()
843 };
844 let fetch_rule = Matcher { stream_kind: Some(MatchKind::Fetch), ..Matcher::default() };
845
846 assert!(
847 fetch_rule.matches(ProxySide::ClientToProxy, &fetch, 0),
848 "a Fetch-aimed class must claim a fetch object: the arm is live on \
849 the eight drafts that have a fetch object codec"
850 );
851 assert!(
852 !fetch_rule.matches(ProxySide::ClientToProxy, &meta(), 0),
853 "and must not claim a subgroup object, or `stream_kind` is not a key"
854 );
855 let dgram_rule = Matcher { stream_kind: Some(MatchKind::Datagram), ..Matcher::default() };
856 assert!(!dgram_rule.matches(ProxySide::ClientToProxy, &fetch, 0));
857 }
858
859 /// **Naming a stream kind never makes a rule unmatchable**, on any draft
860 /// and from either carrier.
861 ///
862 /// One pair used to be the exception: a `Fetch`-aimed class on drafts 18
863 /// and 19, where every fetch stream was bypassed at its header for want
864 /// of a Group Order and no `ObjectMeta` was ever built for the rule to
865 /// see. The session carries that order now, so the exception is gone and
866 /// with it the whole idea that a *kind* can be dead on a *draft* — what
867 /// is left is one unresolvable stream at a time, which reports itself as
868 /// `Impairment { FramerBypass { FetchGroupOrderUnknown } }`.
869 ///
870 /// The contrast is what keeps this from being a test that nothing can
871 /// fail: a key a fetch unit really cannot carry is still reported, on the
872 /// same drafts, through the same call. Every fetch header carries a
873 /// Request ID where a subgroup header carries a Track Alias, so a rule
874 /// keyed on the alias can never claim a fetch object on any draft — and
875 /// that is a fact about the *key*, which is the only kind of fact this
876 /// function still states.
877 ///
878 /// *Ablation:* drop the `TrackAlias` row from
879 /// [`Matcher::unmatchable_fields`]:
880 ///
881 /// ```text
882 /// assertion `left == right` failed: a fetch header carries a request id
883 /// where a subgroup header carries an alias, on every draft
884 /// left: None
885 /// right: Some(TrackAlias)
886 /// ```
887 #[test]
888 fn naming_a_stream_kind_never_makes_a_rule_unmatchable() {
889 let fetch_unit = |draft| ObjectMeta {
890 draft,
891 stream_kind: DataStreamType::Fetch,
892 track_alias: None,
893 ..meta()
894 };
895 let field = |m: &Matcher, unit: &ObjectMeta| {
896 m.unmatchable_fields(unit).into_iter().flatten().last()
897 };
898
899 for kind in [MatchKind::Subgroup, MatchKind::Fetch, MatchKind::Datagram] {
900 let rule = Matcher { stream_kind: Some(kind), ..Matcher::default() };
901 for draft in DRAFTS {
902 assert_eq!(
903 field(&rule, &ObjectMeta { draft, ..meta() }),
904 None,
905 "{draft:?}: a rule aimed at {kind:?} failed to match a subgroup unit, \
906 which is a rule working rather than a rule that cannot work"
907 );
908 assert_eq!(
909 field(&rule, &fetch_unit(draft)),
910 None,
911 "{draft:?}: nor against a fetch unit"
912 );
913 }
914 }
915
916 // The contrast, on every draft: a key the carrier withholds.
917 let by_alias = Matcher { track_alias: Some(RangeSet::single(7)), ..Matcher::default() };
918 for draft in DRAFTS {
919 assert_eq!(
920 field(&by_alias, &fetch_unit(draft)),
921 Some(MatcherField::TrackAlias),
922 "a fetch header carries a request id where a subgroup header carries an \
923 alias, on every draft"
924 );
925 }
926
927 // And from the datagram carrier, where the kind row also went: a
928 // datagram states its own alias and priority, so a rule aimed at any
929 // kind reports nothing about one.
930 let dgram = AnyDatagramMeta {
931 track_alias: 7,
932 group_id: 3,
933 object_id: 11,
934 publisher_priority: Some(128),
935 status: None,
936 };
937 for kind in [MatchKind::Subgroup, MatchKind::Fetch, MatchKind::Datagram] {
938 let rule = Matcher { stream_kind: Some(kind), ..Matcher::default() };
939 for draft in DRAFTS {
940 assert_eq!(
941 rule.unmatchable_fields_datagram(draft, &dgram).into_iter().flatten().last(),
942 None,
943 "{draft:?}: the answer must not depend on which carrier asked"
944 );
945 }
946 }
947 }
948
949 /// **Every key that can name an empty set of values is detected**, one
950 /// row per key, so `ShapeProfile::try_new` can reject the class rather
951 /// than ship one that looks applied and claims nothing.
952 ///
953 /// The positive control on each row is the same key holding a
954 /// *non-empty* value: without it the table would pass against an
955 /// `inert_key` that answered `Some` for any key that was set at all,
956 /// which would reject every working profile.
957 ///
958 /// *Ablation, recorded:* drop the `every_nth` arm — the `n == 0` row
959 /// reddens with `left: None / right: Some("every_nth")`.
960 #[test]
961 fn an_empty_value_set_is_an_inert_key() {
962 // A literal `9..=1` is a clippy error at the call site; a runtime
963 // configuration can still produce one, which is the whole case.
964 let (lo, hi) = (9u64, 1u64);
965 let inverted = || RangeSet::new([lo..=hi]);
966 let ok = || RangeSet::single(1);
967
968 let (top, bottom) = (200u8, 100u8);
969 // `&dyn Fn` and not a `fn` pointer: the rows close over the locals
970 // above, which is what keeps a reversed literal out of the source.
971 type Edit<'a> = &'a dyn Fn(&mut Matcher, bool);
972 let rows: [(&str, Edit); 6] = [
973 ("track_alias", &|m, bad| m.track_alias = Some(if bad { inverted() } else { ok() })),
974 ("group_id", &|m, bad| m.group_id = Some(if bad { inverted() } else { ok() })),
975 ("subgroup_id", &|m, bad| m.subgroup_id = Some(if bad { inverted() } else { ok() })),
976 ("object_id", &|m, bad| m.object_id = Some(if bad { inverted() } else { ok() })),
977 ("priority", &|m, bad| {
978 m.priority = Some(if bad { top..=bottom } else { bottom..=top });
979 }),
980 ("every_nth", &|m, bad| m.every_nth = Some((if bad { 0 } else { 2 }, 0))),
981 ];
982
983 for (key, edit) in rows {
984 let mut inert = Matcher::default();
985 edit(&mut inert, true);
986 assert_eq!(inert.inert_key(), Some(key), "{key} names no value at all");
987
988 let mut live = Matcher::default();
989 edit(&mut live, false);
990 assert_eq!(live.inert_key(), None, "{key} holding a real value is a working rule");
991 }
992
993 // A matcher that keys on nothing claims everything, which is not
994 // inert — it is the default.
995 assert_eq!(Matcher::default().inert_key(), None);
996 }
997
998 /// Every side has a written form, and an unknown one is refused with the
999 /// four names listed.
1000 #[cfg(feature = "serde")]
1001 #[test]
1002 fn sides_round_trip_by_name() {
1003 #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
1004 struct Holder {
1005 #[serde(with = "super::side_serde")]
1006 side: Option<crate::types::ProxySide>,
1007 }
1008
1009 let holder = Holder { side: Some(crate::types::ProxySide::RelayToProxy) };
1010 let json = serde_json::to_string(&holder).expect("serializes");
1011 assert_eq!(json, r#"{"side":"relay-to-proxy"}"#);
1012 assert_eq!(serde_json::from_str::<Holder>(&json).expect("reads back"), holder);
1013
1014 let refusal = serde_json::from_str::<Holder>(r#"{"side":"relay->proxy"}"#)
1015 .expect_err("an unknown side is refused");
1016 assert!(
1017 refusal.to_string().contains("relay-to-proxy"),
1018 "the refusal lists the accepted names: {refusal}"
1019 );
1020 }
1021}