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 every draft. Drafts 18, 19 and 20 write a fetch
170/// object's Group ID as a difference whose sign the fetch's Group Order
171/// settles; the session reads that order off the FETCH —
172/// `capability::fetch_group_order_is_needed` — and hands it to the framer, so
173/// what a rule can miss is one stream at a time rather than a whole draft. A
174/// stream the session cannot resolve is bypassed at its header and produces
175/// no [`ObjectMeta`] for a rule to see, and says so itself as
176/// `Impairment { FramerBypass { FetchGroupOrderUnknown } }`.
177///
178/// Being matchable is not being *mutable*, and the two are answered
179/// separately: whether a rule claims a unit is [`Matcher::matches`], and what
180/// may then be done to it is `capability::classify`.
181///
182/// `#[non_exhaustive]` with no `Default`: there is no meaningful default
183/// stream kind, and a wrong one would silently narrow every rule that
184/// omitted the key.
185///
186/// Written as `"subgroup"`, `"fetch"` or `"datagram"`.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
188#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
189#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
190#[non_exhaustive]
191pub enum MatchKind {
192 /// A subgroup data stream.
193 Subgroup,
194 /// A fetch response data stream.
195 Fetch,
196 /// A datagram.
197 Datagram,
198}
199
200impl MatchKind {}
201
202/// One [`Matcher`] key, named so a report about it is typed rather than a
203/// string.
204///
205/// Only the keys that can be **unmatchable** are here, and that is the whole
206/// point of the type: it is carried by
207/// [`ImpairmentKind::ShapeRuleUnmatchable`](crate::event::ImpairmentKind::ShapeRuleUnmatchable),
208/// which fires when a rule keys on something this draft and stream kind
209/// cannot carry. The other four keys — `side`, `group_id`, `object_id` and
210/// `every_nth` — are present on every framed unit by construction, so a rule
211/// keyed on one of them that fails to match failed on its *value*, which is
212/// the rule working, not a rule that cannot work.
213///
214/// `#[non_exhaustive]` with no `Default`: a later draft that makes another
215/// key optional adds a variant, and no key is a meaningful zero.
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
217#[non_exhaustive]
218pub enum MatcherField {
219 /// [`Matcher::track_alias`], against a unit whose stream carried none —
220 /// every fetch stream.
221 TrackAlias,
222 /// [`Matcher::subgroup_id`], against a unit whose header carried none —
223 /// ten drafts in first-object mode, and 16-21 in reserved mode 3.
224 ///
225 /// Never reported about a datagram, which carries no subgroup ID on any
226 /// draft. That is not a fact about one header, so it is a pre-run
227 /// refusal — `Capabilities::admit_class` — rather than something a run
228 /// discovers. See `Matcher::unmatchable_fields_datagram`.
229 SubgroupId,
230 /// [`Matcher::priority`], against a unit whose header set the
231 /// default-priority bit — drafts 15-19, on a subgroup object and on a
232 /// datagram alike.
233 Priority,
234}
235
236impl MatcherField {
237 /// This field's bit in a per-class report-once mask.
238 ///
239 /// A four-variant enum fits one byte, so the "reported already" state
240 /// for a whole class is one `AtomicU8` and the check is one relaxed
241 /// `fetch_or` — taken only when a field is *actually* absent, so a
242 /// profile whose keys are all carried never touches it.
243 pub(crate) const fn bit(self) -> u8 {
244 match self {
245 MatcherField::TrackAlias => 1,
246 MatcherField::SubgroupId => 2,
247 MatcherField::Priority => 4,
248 }
249 }
250}
251
252/// Which units a [`ClassRule`](super::ClassRule) claims.
253///
254/// All present fields must match (AND). An absent field matches
255/// everything. A field the wire did not carry — `None` on
256/// [`ObjectMeta`] — **does not match**.
257///
258/// `#[non_exhaustive]` *with* a [`Default`], exactly as
259/// [`EgressConfig`](crate::action::EgressConfig) is. The pairing is
260/// load-bearing: `#[non_exhaustive]` alone would make this type
261/// unconstructible from an integration-test crate or from a caller's
262/// code, because struct-expression *and* functional-update syntax
263/// are both illegal outside the defining crate. Note what that leaves:
264/// `..Matcher::default()` is **also** illegal there (`E0639`), so an
265/// outside caller writes `let mut m = Matcher::default();` and then assigns
266/// per field — which is what `tests/actions_shaping.rs` does. Inside this
267/// crate both forms compile, which is why the unit tests below use the
268/// shorter one. The `Default` is all-`None` — a matcher that claims every
269/// unit.
270///
271/// In the written form every key defaults to absent, so a matcher naming one
272/// field is one line, and an unknown key is refused rather than skipped — a
273/// misspelled `group_id` would otherwise widen the rule to claim every unit
274/// on the stream instead of the ten groups it named.
275#[derive(Debug, Clone, PartialEq, Eq, Default)]
276#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
277#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
278#[non_exhaustive]
279pub struct Matcher {
280 /// The direction the unit arrived on.
281 ///
282 /// Only `ClientToProxy` and `RelayToProxy` are ever seen at a hook
283 /// site; the two egress labels are used for teardown reporting. Naming
284 /// an egress side here is rejected by
285 /// [`ShapeProfile::try_new`](super::ShapeProfile::try_new) rather than
286 /// left to match nothing.
287 ///
288 /// Written as the variant's own name in kebab-case —
289 /// `"client-to-proxy"` — by the `side_serde` mapping below rather than
290 /// by a derive, because [`ProxySide`] lives in a module that carries no
291 /// serde dependency of its own.
292 #[cfg_attr(feature = "serde", serde(with = "side_serde"))]
293 pub side: Option<ProxySide>,
294 /// Track alias from the stream header. `None` on every fetch stream,
295 /// so a rule keyed here never claims fetch units.
296 pub track_alias: Option<RangeSet>,
297 /// Group ID. Always present on a framed object.
298 pub group_id: Option<RangeSet>,
299 /// Subgroup ID. `None` on ten drafts in first-object mode and on
300 /// 16-20 in reserved mode 3, so a rule keyed here claims nothing there.
301 pub subgroup_id: Option<RangeSet>,
302 /// Absolute object ID. Always present on a framed object.
303 pub object_id: Option<RangeSet>,
304 /// MoQT `publisher_priority`. `None` on drafts 15-19 whenever the
305 /// header set the default-priority bit.
306 pub priority: Option<RangeInclusive<u8>>,
307 /// Which kind of stream the unit came from.
308 pub stream_kind: Option<MatchKind>,
309 /// `(n, offset)` over a counter of **hook-visible units on this
310 /// stream**, scoped **per stream and not per class**, never
311 /// [`ObjectMeta::index_in_stream`] — which counts oversized objects
312 /// that never reach the hook and would silently shift the pattern.
313 ///
314 /// Matches when `unit_index % n == offset % n`. `n == 0` names no
315 /// units, so [`ShapeProfile::try_new`](super::ShapeProfile::try_new)
316 /// rejects it as [`ShapeError::InertMatcher`](super::ShapeError::InertMatcher)
317 /// rather than accepting a class that can never claim anything.
318 pub every_nth: Option<(u64, u64)>,
319}
320
321impl Matcher {
322 /// Whether this matcher claims one unit.
323 ///
324 /// `side` is the forwarding task's own direction label — [`ObjectMeta`]
325 /// has no `side` field. `unit_index` is the per-stream count of
326 /// hook-visible units described on [`Matcher::every_nth`], supplied by
327 /// the caller for the same reason `charge` takes `now`: this function
328 /// owns no state and reads no counter, so it can be tested exhaustively
329 /// from a table.
330 pub fn matches(&self, side: ProxySide, meta: &ObjectMeta, unit_index: u64) -> bool {
331 self.claims(
332 side,
333 kind_of(meta.stream_kind),
334 Keys {
335 track_alias: meta.track_alias,
336 group_id: meta.group_id,
337 subgroup_id: meta.subgroup_id,
338 object_id: meta.object_id,
339 priority: meta.publisher_priority,
340 },
341 unit_index,
342 )
343 }
344
345 /// Whether this matcher claims one datagram.
346 ///
347 /// [`Self::matches`]'s sibling, and the two answer through one
348 /// conjunction — see the private `Keys` it is written over. What differs
349 /// is what fills that in: a
350 /// datagram states its own track alias, Group ID, Object ID and (from
351 /// draft-15, conditionally) priority, and states **no subgroup ID** on
352 /// any draft, so a rule keyed there claims no datagram. That last one is
353 /// a fact about the carrier rather than about one header, so it is
354 /// refused before the run by
355 /// [`Capabilities::admit_class`](crate::capability::Capabilities::admit_class)
356 /// rather than discovered from one.
357 ///
358 /// `unit_index` counts hook-visible datagrams **per forwarding
359 /// direction**, which is the only scope a datagram has: it belongs to no
360 /// stream, so [`Matcher::every_nth`]'s per-stream reading has nothing to
361 /// key against here and the session's two directions count separately.
362 pub fn matches_datagram(
363 &self,
364 side: ProxySide,
365 meta: &AnyDatagramMeta,
366 unit_index: u64,
367 ) -> bool {
368 self.claims(
369 side,
370 MatchKind::Datagram,
371 Keys {
372 track_alias: Some(meta.track_alias),
373 group_id: meta.group_id,
374 subgroup_id: None,
375 object_id: meta.object_id,
376 priority: meta.publisher_priority,
377 },
378 unit_index,
379 )
380 }
381
382 /// The conjunction both carriers answer through.
383 ///
384 /// Written once rather than twice because a six-key AND copied is a
385 /// sixth key forgotten: a matcher key added to this struct and to only
386 /// one of the two callers would widen every rule on the other carrier,
387 /// silently, in the direction of claiming more than it named.
388 fn claims(&self, side: ProxySide, kind: MatchKind, keys: Keys, unit_index: u64) -> bool {
389 if let Some(want) = self.side {
390 if want != side {
391 return false;
392 }
393 }
394 if let Some(want) = self.stream_kind {
395 if want != kind {
396 return false;
397 }
398 }
399 // A keyed field the wire did not carry never matches: the three
400 // `Option` keys below take the `_ => return false` arm on `None`.
401 if let Some(set) = &self.track_alias {
402 match keys.track_alias {
403 Some(v) if set.contains(v) => {}
404 _ => return false,
405 }
406 }
407 if let Some(set) = &self.group_id {
408 if !set.contains(keys.group_id) {
409 return false;
410 }
411 }
412 if let Some(set) = &self.subgroup_id {
413 match keys.subgroup_id {
414 Some(v) if set.contains(v) => {}
415 _ => return false,
416 }
417 }
418 if let Some(set) = &self.object_id {
419 if !set.contains(keys.object_id) {
420 return false;
421 }
422 }
423 if let Some(range) = &self.priority {
424 match keys.priority {
425 Some(p) if range.contains(&p) => {}
426 _ => return false,
427 }
428 }
429 if let Some((n, offset)) = self.every_nth {
430 if n == 0 || unit_index % n != offset % n {
431 return false;
432 }
433 }
434 true
435 }
436
437 /// The keys this matcher names that `meta` **cannot carry**, so a
438 /// non-match against them is a rule that can never fire rather than a
439 /// rule that did not fire.
440 ///
441 /// The distinction is the whole point of this function: `None` never
442 /// matches, and a silent fall to the default class is precisely the
443 /// failure mode this project exists to prevent. The caller reports each
444 /// `(class, field)` once per session.
445 ///
446 /// Returns a fixed-size array rather than a `Vec` — it is called on the
447 /// data path, once per rule that failed to match, and must not
448 /// allocate. `[None; 3]` is the answer for a matcher whose keys are all
449 /// carried, which is the common case.
450 ///
451 /// Every row here is a key **this unit** did not carry, and a unit is the
452 /// only scope this function answers at. No row says a key is dead for a
453 /// whole draft: a `Fetch`-aimed class is matchable on every draft, and a
454 /// fetch stream whose Group Order the session cannot resolve is bypassed
455 /// at its header and produces no [`ObjectMeta`] for a rule to be measured
456 /// against — see [`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 this catches:
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 the 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, 19 and 20 that needs the fetch's Group Order, which
579/// the session reads off the FETCH before the response opens; a response
580/// naming a request nobody made is bypassed and says so per stream, and
581/// produces no 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 /// Every draft, 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 ///
662 /// Taken from [`DraftVersion::ALL`] rather than transcribed. A sweep whose
663 /// draft list is written out can quietly cover one draft fewer than it
664 /// claims to, and that is not a failing test, it is a smaller one.
665 const DRAFTS: [DraftVersion; DraftVersion::ALL.len()] = DraftVersion::ALL;
666
667 /// A framed subgroup object with every optional key present, so a test
668 /// can knock exactly one out and attribute the result.
669 fn meta() -> ObjectMeta {
670 ObjectMeta {
671 draft: DraftVersion::Draft19,
672 stream_kind: DataStreamType::Subgroup,
673 track_alias: Some(7),
674 group_id: 3,
675 subgroup_id: Some(4),
676 object_id: 11,
677 publisher_priority: Some(128),
678 index_in_stream: 0,
679 payload_len: 16,
680 status: None,
681 end_of_range: None,
682 }
683 }
684
685 /// Coalescing, boundaries and the empty set.
686 ///
687 /// The `ranges()` assertions are the load-bearing half: `contains`
688 /// alone cannot tell `[1..=3, 4..=6]` from `[1..=6]`.
689 #[test]
690 fn a_range_set_contains_only_its_ranges() {
691 // Empty.
692 let empty = RangeSet::new([]);
693 assert!(empty.is_empty());
694 assert!(!empty.contains(0));
695 assert!(!empty.contains(u64::MAX));
696
697 // Single.
698 let one = RangeSet::single(7);
699 assert_eq!(one.ranges(), &[7..=7]);
700 assert!(one.contains(7));
701 assert!(!one.contains(6));
702 assert!(!one.contains(8));
703
704 // Unsorted input is sorted; disjoint ranges stay disjoint.
705 let two = RangeSet::new([5..=9, 1..=3]);
706 assert_eq!(two.ranges(), &[1..=3, 5..=9]);
707 for v in [1, 2, 3, 5, 6, 9] {
708 assert!(two.contains(v), "should contain {v}");
709 }
710 for v in [0, 4, 10] {
711 assert!(!two.contains(v), "should not contain {v}");
712 }
713
714 // Adjacency coalesces. This is the `+1`.
715 assert_eq!(RangeSet::new([1..=3, 4..=6]).ranges(), &[1..=6]);
716 // Overlap coalesces, and the wider end wins.
717 assert_eq!(RangeSet::new([1..=5, 3..=9]).ranges(), &[1..=9]);
718 // A range wholly inside another is absorbed, not appended.
719 assert_eq!(RangeSet::new([1..=9, 3..=5]).ranges(), &[1..=9]);
720 // A one-value gap is NOT adjacency.
721 assert_eq!(RangeSet::new([1..=3, 5..=6]).ranges(), &[1..=3, 5..=6]);
722
723 // Empty ranges are dropped rather than stored. Bound through
724 // locals: a literal `9..=1` is a clippy error at the call site,
725 // which is exactly the case a configuration built at runtime can
726 // still produce.
727 let (lo, hi) = (9u64, 1u64);
728 assert!(RangeSet::new([lo..=hi]).is_empty());
729 assert_eq!(RangeSet::new([lo..=hi, 2..=4]).ranges(), &[2..=4]);
730
731 // The top of the space does not panic and does not wrap.
732 let top = RangeSet::new([u64::MAX..=u64::MAX]);
733 assert!(top.contains(u64::MAX));
734 assert!(!top.contains(u64::MAX - 1));
735 }
736
737 /// An absent key does not match — `None` is never a wildcard.
738 #[test]
739 fn an_absent_key_does_not_match() {
740 let keyed = Matcher { subgroup_id: Some(RangeSet::single(4)), ..Matcher::default() };
741
742 // Positive control: the key is present and in range.
743 assert!(keyed.matches(ProxySide::ClientToProxy, &meta(), 0));
744
745 // The wire did not carry a subgroup ID: no match, and the value the
746 // codec would have stored (zero) is not consulted.
747 let absent = ObjectMeta { subgroup_id: None, ..meta() };
748 assert!(!keyed.matches(ProxySide::ClientToProxy, &absent, 0));
749
750 // Present but out of range: also no match, so the assertion above
751 // is about absence and not about the range.
752 let other = ObjectMeta { subgroup_id: Some(5), ..meta() };
753 assert!(!keyed.matches(ProxySide::ClientToProxy, &other, 0));
754
755 // The same rule on the other two `Option` keys.
756 let by_alias = Matcher { track_alias: Some(RangeSet::single(7)), ..Matcher::default() };
757 assert!(by_alias.matches(ProxySide::ClientToProxy, &meta(), 0));
758 let fetch = ObjectMeta { track_alias: None, ..meta() };
759 assert!(!by_alias.matches(ProxySide::ClientToProxy, &fetch, 0));
760
761 let by_priority = Matcher { priority: Some(0..=200), ..Matcher::default() };
762 assert!(by_priority.matches(ProxySide::ClientToProxy, &meta(), 0));
763 let defaulted = ObjectMeta { publisher_priority: None, ..meta() };
764 assert!(!by_priority.matches(ProxySide::ClientToProxy, &defaulted, 0));
765
766 // An all-absent matcher still claims everything, so "None never
767 // matches" is about a *keyed* field and not about the matcher.
768 let any = Matcher::default();
769 assert!(any.matches(ProxySide::ClientToProxy, &absent, 0));
770 assert!(any.matches(ProxySide::RelayToProxy, &fetch, 9));
771 }
772
773 /// The remaining matcher keys, so `matches` is not gated by
774 /// `an_absent_key_does_not_match` alone.
775 #[test]
776 fn the_other_keys_and_are_ends_a_match() {
777 let m = Matcher {
778 side: Some(ProxySide::RelayToProxy),
779 group_id: Some(RangeSet::new([0..=3])),
780 object_id: Some(RangeSet::new([10..=12])),
781 stream_kind: Some(MatchKind::Subgroup),
782 every_nth: Some((3, 1)),
783 ..Matcher::default()
784 };
785 assert!(m.matches(ProxySide::RelayToProxy, &meta(), 4));
786
787 // Each key alone flips the AND to false.
788 assert!(!m.matches(ProxySide::ClientToProxy, &meta(), 4));
789 assert!(!m.matches(ProxySide::RelayToProxy, &ObjectMeta { group_id: 4, ..meta() }, 4));
790 assert!(!m.matches(ProxySide::RelayToProxy, &ObjectMeta { object_id: 13, ..meta() }, 4));
791 let fetch = ObjectMeta { stream_kind: DataStreamType::Fetch, ..meta() };
792 assert!(!m.matches(ProxySide::RelayToProxy, &fetch, 4));
793 assert!(!m.matches(ProxySide::RelayToProxy, &meta(), 5));
794
795 // A datagram rule never claims a framed object, on either kind.
796 let dgram = Matcher { stream_kind: Some(MatchKind::Datagram), ..Matcher::default() };
797 assert!(!dgram.matches(ProxySide::ClientToProxy, &meta(), 0));
798 assert!(!dgram.matches(ProxySide::ClientToProxy, &fetch, 0));
799
800 // `n == 0` names no units.
801 let never = Matcher { every_nth: Some((0, 0)), ..Matcher::default() };
802 assert!(!never.matches(ProxySide::ClientToProxy, &meta(), 0));
803 }
804
805 /// **A `Fetch`-aimed rule claims a fetch object.** The positive half of
806 /// `kind_matches`, which nothing exercised: `the_other_keys_and_ends_a_match`
807 /// only asserts that a *Subgroup* rule rejects a fetch meta, and that
808 /// stays green with the `Fetch` arm deleted.
809 ///
810 /// The three assertions are the three things one arm has to get right:
811 /// the kind it names matches, the *other* stream kind does not, and the
812 /// `Datagram` kind matches neither — so a `kind_matches` rewritten as
813 /// `want != Datagram` would still redden.
814 ///
815 /// *Ablation, recorded:* delete `| (MatchKind::Fetch,
816 /// DataStreamType::Fetch)` from `kind_matches`. Before this test the
817 /// whole crate stayed green at 392 passed / 0 failed / 0 filtered out —
818 /// a matcher arm with no gate at all. With it:
819 ///
820 /// ```text
821 /// thread '...the_fetch_arm_claims_a_fetch_object' panicked at
822 /// crates\moqtap-proxy\src\shape\matcher.rs:
823 /// a Fetch-aimed class must claim a fetch object: the arm is live on all
824 /// drafts, every one of which has a fetch object codec
825 /// ```
826 #[test]
827 fn the_fetch_arm_claims_a_fetch_object() {
828 let fetch = ObjectMeta {
829 draft: DraftVersion::Draft14,
830 stream_kind: DataStreamType::Fetch,
831 track_alias: None,
832 ..meta()
833 };
834 let fetch_rule = Matcher { stream_kind: Some(MatchKind::Fetch), ..Matcher::default() };
835
836 assert!(
837 fetch_rule.matches(ProxySide::ClientToProxy, &fetch, 0),
838 "a Fetch-aimed class must claim a fetch object: the arm is live on all \
839 drafts, every one of which has a fetch object codec"
840 );
841 assert!(
842 !fetch_rule.matches(ProxySide::ClientToProxy, &meta(), 0),
843 "and must not claim a subgroup object, or `stream_kind` is not a key"
844 );
845 let dgram_rule = Matcher { stream_kind: Some(MatchKind::Datagram), ..Matcher::default() };
846 assert!(!dgram_rule.matches(ProxySide::ClientToProxy, &fetch, 0));
847 }
848
849 /// **Naming a stream kind never makes a rule unmatchable**, on any draft
850 /// and from either carrier.
851 ///
852 /// A *kind* is never dead on a *draft*: what can go missing is one fetch
853 /// stream at a time. A stream whose Group Order the session cannot
854 /// resolve is bypassed at its header, so no `ObjectMeta` is ever built
855 /// for a rule to see, and it reports itself as
856 /// `Impairment { FramerBypass { FetchGroupOrderUnknown } }`.
857 ///
858 /// The contrast is what keeps this from being a test that nothing can
859 /// fail: a key a fetch unit really cannot carry is still reported, on the
860 /// same drafts, through the same call. Every fetch header carries a
861 /// Request ID where a subgroup header carries a Track Alias, so a rule
862 /// keyed on the alias can never claim a fetch object on any draft — and
863 /// that is a fact about the *key* rather than about one header, which is
864 /// what makes it hold wherever the rule is measured.
865 ///
866 /// *Ablation:* drop the `TrackAlias` row from
867 /// [`Matcher::unmatchable_fields`]:
868 ///
869 /// ```text
870 /// assertion `left == right` failed: a fetch header carries a request id
871 /// where a subgroup header carries an alias, on every draft
872 /// left: None
873 /// right: Some(TrackAlias)
874 /// ```
875 #[test]
876 fn naming_a_stream_kind_never_makes_a_rule_unmatchable() {
877 let fetch_unit = |draft| ObjectMeta {
878 draft,
879 stream_kind: DataStreamType::Fetch,
880 track_alias: None,
881 ..meta()
882 };
883 let field = |m: &Matcher, unit: &ObjectMeta| {
884 m.unmatchable_fields(unit).into_iter().flatten().last()
885 };
886
887 for kind in [MatchKind::Subgroup, MatchKind::Fetch, MatchKind::Datagram] {
888 let rule = Matcher { stream_kind: Some(kind), ..Matcher::default() };
889 for draft in DRAFTS {
890 assert_eq!(
891 field(&rule, &ObjectMeta { draft, ..meta() }),
892 None,
893 "{draft:?}: a rule aimed at {kind:?} failed to match a subgroup unit, \
894 which is a rule working rather than a rule that cannot work"
895 );
896 assert_eq!(
897 field(&rule, &fetch_unit(draft)),
898 None,
899 "{draft:?}: nor against a fetch unit"
900 );
901 }
902 }
903
904 // The contrast, on every draft: a key the carrier withholds.
905 let by_alias = Matcher { track_alias: Some(RangeSet::single(7)), ..Matcher::default() };
906 for draft in DRAFTS {
907 assert_eq!(
908 field(&by_alias, &fetch_unit(draft)),
909 Some(MatcherField::TrackAlias),
910 "a fetch header carries a request id where a subgroup header carries an \
911 alias, on every draft"
912 );
913 }
914
915 // And from the datagram carrier, where the kind row also went: a
916 // datagram states its own alias and priority, so a rule aimed at any
917 // kind reports nothing about one.
918 let dgram = AnyDatagramMeta {
919 track_alias: 7,
920 group_id: 3,
921 object_id: 11,
922 publisher_priority: Some(128),
923 status: None,
924 };
925 for kind in [MatchKind::Subgroup, MatchKind::Fetch, MatchKind::Datagram] {
926 let rule = Matcher { stream_kind: Some(kind), ..Matcher::default() };
927 for draft in DRAFTS {
928 assert_eq!(
929 rule.unmatchable_fields_datagram(draft, &dgram).into_iter().flatten().last(),
930 None,
931 "{draft:?}: the answer must not depend on which carrier asked"
932 );
933 }
934 }
935 }
936
937 /// **Every key that can name an empty set of values is detected**, one
938 /// row per key, so `ShapeProfile::try_new` can reject the class rather
939 /// than ship one that looks applied and claims nothing.
940 ///
941 /// The positive control on each row is the same key holding a
942 /// *non-empty* value: without it the table would pass against an
943 /// `inert_key` that answered `Some` for any key that was set at all,
944 /// which would reject every working profile.
945 ///
946 /// *Ablation, recorded:* drop the `every_nth` arm — the `n == 0` row
947 /// reddens with `left: None / right: Some("every_nth")`.
948 #[test]
949 fn an_empty_value_set_is_an_inert_key() {
950 // A literal `9..=1` is a clippy error at the call site; a runtime
951 // configuration can still produce one, which is the whole case.
952 let (lo, hi) = (9u64, 1u64);
953 let inverted = || RangeSet::new([lo..=hi]);
954 let ok = || RangeSet::single(1);
955
956 let (top, bottom) = (200u8, 100u8);
957 // `&dyn Fn` and not a `fn` pointer: the rows close over the locals
958 // above, which is what keeps a reversed literal out of the source.
959 type Edit<'a> = &'a dyn Fn(&mut Matcher, bool);
960 let rows: [(&str, Edit); 6] = [
961 ("track_alias", &|m, bad| m.track_alias = Some(if bad { inverted() } else { ok() })),
962 ("group_id", &|m, bad| m.group_id = Some(if bad { inverted() } else { ok() })),
963 ("subgroup_id", &|m, bad| m.subgroup_id = Some(if bad { inverted() } else { ok() })),
964 ("object_id", &|m, bad| m.object_id = Some(if bad { inverted() } else { ok() })),
965 ("priority", &|m, bad| {
966 m.priority = Some(if bad { top..=bottom } else { bottom..=top });
967 }),
968 ("every_nth", &|m, bad| m.every_nth = Some((if bad { 0 } else { 2 }, 0))),
969 ];
970
971 for (key, edit) in rows {
972 let mut inert = Matcher::default();
973 edit(&mut inert, true);
974 assert_eq!(inert.inert_key(), Some(key), "{key} names no value at all");
975
976 let mut live = Matcher::default();
977 edit(&mut live, false);
978 assert_eq!(live.inert_key(), None, "{key} holding a real value is a working rule");
979 }
980
981 // A matcher that keys on nothing claims everything, which is not
982 // inert — it is the default.
983 assert_eq!(Matcher::default().inert_key(), None);
984 }
985
986 /// Every side has a written form, and an unknown one is refused with the
987 /// four names listed.
988 #[cfg(feature = "serde")]
989 #[test]
990 fn sides_round_trip_by_name() {
991 #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
992 struct Holder {
993 #[serde(with = "super::side_serde")]
994 side: Option<crate::types::ProxySide>,
995 }
996
997 let holder = Holder { side: Some(crate::types::ProxySide::RelayToProxy) };
998 let json = serde_json::to_string(&holder).expect("serializes");
999 assert_eq!(json, r#"{"side":"relay-to-proxy"}"#);
1000 assert_eq!(serde_json::from_str::<Holder>(&json).expect("reads back"), holder);
1001
1002 let refusal = serde_json::from_str::<Holder>(r#"{"side":"relay->proxy"}"#)
1003 .expect_err("an unknown side is refused");
1004 assert!(
1005 refusal.to_string().contains("relay-to-proxy"),
1006 "the refusal lists the accepted names: {refusal}"
1007 );
1008 }
1009}