moqtap_codec/range_filter.rs
1//! The Range Filters draft-19 carries in five parameters.
2//!
3//! Section 5.1.3: "Range Filters are parameters in SUBSCRIBE, FETCH, or
4//! SUBSCRIBE_TRACKS that tell a publisher to filter tracks (via TRACK PROPERTY
5//! FILTER) and objects according to subscriber-provided criteria. Range filters
6//! are specified as ranges of integer values in Track and Object Properties and
7//! other Object header fields (Subgroup ID, Object ID, and Publisher Priority).
8//! There are five Range Filter parameter types, 0x25-0x29, as shown below."
9//!
10//! Draft-19 is the only draft with them, and the five share one value shape:
11//!
12//! ```text
13//! SUBGROUP_FILTER { Type=0x25, Length, [SetID], Range... }
14//! OBJECTID_FILTER { Type=0x26, Length, [SetID], Range... }
15//! PRIORITY_FILTER { Type=0x27, Length, [SetID], Range... }
16//! OBJECT_PROPERTY_FILTER { Type=0x28, Length, [SetID], [Property Type], Range... }
17//! TRACK_PROPERTY_FILTER { Type=0x29, Length, [SetID], [Property Type], Range... }
18//! Range { Start, [End] }
19//! ```
20//!
21//! # Why this is a reader and not a check
22//!
23//! Every consequence Section 5.1.3 states is a **reply**, not a session close:
24//! "MUST be rejected with REQUEST_ERROR with error code INVALID_FILTER" for a
25//! delta that overruns, for a repeated filter key, and for more Ranges than
26//! MAX_FILTER_RANGES allows. A reply is something an endpoint sends, and sending
27//! it needs the request decoded — including the Request ID the reply names.
28//!
29//! So nothing here is wired into `decode_parameters`, and [`RangeFilterError`](crate::range_filter::RangeFilterError)
30//! deliberately does not convert into [`CodecError`](crate::error::CodecError).
31//! A refusal at decode time would turn a frame the endpoint owes an answer to
32//! into a frame it never saw, and the subscriber would wait for a REQUEST_ERROR
33//! that no longer has anything to be about. The five types are registered in
34//! draft-19's parameter table as length-prefixed values, which is what keeps
35//! them carried rather than refused; this module is what makes the bytes mean
36//! something.
37//!
38//! # The delta encoding, which is not the one used elsewhere
39//!
40//! "Start is delta encoded from the prior Range's End or from 0 for the first
41//! Range, and End is delta encoded from the current Range's Start." So the
42//! baseline alternates: a Start counts from the previous End, and an End counts
43//! from the Start beside it. The draft's own example is ranges 3-5 and 10-15,
44//! written as 3, 2, 5, 5 — and reading it with one running baseline instead of
45//! two produces 3-5 and 8-13, a range that is wrong and well formed.
46//!
47//! "The final End in a sequence of Ranges can be omitted to indicate no end", so
48//! a value holding an odd number of integers ends in an unbounded range. Only
49//! the last one may be omitted, which is what makes the pairing unambiguous.
50
51use bytes::{Buf, BufMut};
52
53use crate::kvp::{KeyValuePair, KvpValue};
54use crate::varint::{MoqtProfile, VarInt};
55
56/// SUBGROUP_FILTER, matching an Object's Subgroup ID.
57pub const SUBGROUP_FILTER_PARAMETER: u64 = 0x25;
58/// OBJECTID_FILTER, matching an Object's Object ID.
59pub const OBJECT_ID_FILTER_PARAMETER: u64 = 0x26;
60/// PRIORITY_FILTER, matching an Object's Publisher Priority.
61pub const PRIORITY_FILTER_PARAMETER: u64 = 0x27;
62/// OBJECT_PROPERTY_FILTER, matching the value of one Object Property.
63pub const OBJECT_PROPERTY_FILTER_PARAMETER: u64 = 0x28;
64/// TRACK_PROPERTY_FILTER, matching the value of one Track Property.
65pub const TRACK_PROPERTY_FILTER_PARAMETER: u64 = 0x29;
66
67/// Whether `parameter_type` is one of the five Range Filters.
68pub fn is_range_filter(parameter_type: u64) -> bool {
69 (SUBGROUP_FILTER_PARAMETER..=TRACK_PROPERTY_FILTER_PARAMETER).contains(¶meter_type)
70}
71
72/// Whether a Range Filter of this type carries a Property Type after its SetID.
73///
74/// The two property filters do and the three header-field filters do not: a
75/// filter on Subgroup ID, Object ID or Publisher Priority already knows which
76/// field it is about from its own parameter type, and a filter on a property has
77/// to name which property. Reading the prefix for the wrong three shifts every
78/// Range in the value by one integer.
79pub fn carries_a_property_type(parameter_type: u64) -> bool {
80 parameter_type == OBJECT_PROPERTY_FILTER_PARAMETER
81 || parameter_type == TRACK_PROPERTY_FILTER_PARAMETER
82}
83
84/// One inclusive range of values, with the deltas already resolved.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct FilterRange {
87 /// The first value the range admits.
88 pub start: u64,
89 /// The last value the range admits, or `None` for a range with no end.
90 ///
91 /// Only the final Range in a filter may be unbounded, because the omission
92 /// is what the reader uses to tell a trailing Start from the next Range's.
93 pub end: Option<u64>,
94}
95
96impl FilterRange {
97 /// Whether `value` falls inside this range.
98 ///
99 /// Both ends are inclusive: Section 5.1.3 calls them "Start/End (vi64)
100 /// inclusive Range pairs", and the example spells 3-5 as a Start of 3 and an
101 /// End of 5 rather than as a half-open interval.
102 pub fn contains(&self, value: u64) -> bool {
103 value >= self.start && self.end.is_none_or(|end| value <= end)
104 }
105}
106
107/// A decoded Range Filter parameter value.
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct RangeFilter {
110 /// Which of the five filters this is.
111 pub parameter_type: u64,
112 /// The set this filter belongs to.
113 /// "All filter parameters with the same SetID value are combined using
114 /// logical 'AND' operations, then all the resulting sets are combined using
115 /// logical 'OR' operations." So the SetID is not decoration: two filters
116 /// under one SetID both have to pass, and two filters under different
117 /// SetIDs each pass on their own.
118 pub set_id: u8,
119 /// The property this filter is about, on the two property filters.
120 ///
121 /// `None` on SUBGROUP_FILTER, OBJECTID_FILTER and PRIORITY_FILTER, whose
122 /// subject is fixed by the parameter type.
123 pub property_type: Option<u64>,
124 /// The ranges, with every delta resolved to an absolute value.
125 pub ranges: Vec<FilterRange>,
126}
127
128/// What the key of a Range Filter is, for the rule about repeats.
129///
130/// "If the same combination of Parameter Type, SetID, and Property Type (only in
131/// the Track and Object Property Filters) repeat in any message, an endpoint
132/// MUST reject this with REQUEST_ERROR with error code INVALID_FILTER."
133pub type RangeFilterKey = (u64, u8, Option<u64>);
134
135/// A Range Filter value this codec could not turn into ranges.
136///
137/// Kept out of [`CodecError`](crate::error::CodecError) on purpose, and the
138/// absence of a `From` impl is the mechanism: draft-19 answers every one of
139/// these with a REQUEST_ERROR carrying INVALID_FILTER, which is a message the
140/// endpoint sends after decoding the request, so none of them may become a
141/// decoder refusal. See this module's header.
142#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
143pub enum RangeFilterError {
144 /// A parameter type outside 0x25 through 0x29 was handed to the reader.
145 #[error("parameter type {0} is not a Range Filter")]
146 NotARangeFilter(u64),
147 /// The value ran out before a field the shape requires.
148 #[error("range filter value is malformed: {detail}")]
149 Malformed {
150 /// Which field was missing, for a log that says what was short.
151 detail: &'static str,
152 },
153 /// A delta resolved past the end of the 64-bit space.
154 ///
155 /// "Any delta encoding that results in a value that exceeds 2^64-1 MUST be
156 /// rejected with REQUEST_ERROR with error code INVALID_FILTER." The sum is
157 /// checked rather than allowed to wrap: a wrapped Start is a small number,
158 /// and a filter that reads as 0-5 when its sender wrote something above the
159 /// end of the space passes Objects it was written to exclude.
160 #[error("range filter delta {delta} past {base} runs off the end of the 64-bit space")]
161 DeltaOverflow {
162 /// The value the delta was counted from.
163 base: u64,
164 /// The delta as it arrived.
165 delta: u64,
166 },
167 /// A PRIORITY_FILTER range names a value the field cannot hold.
168 ///
169 /// Section 10.2.12: "If a decoded value exceeds 255, the endpoint MUST
170 /// reject this with REQUEST_ERROR with error code INVALID_FILTER since
171 /// Publisher Priority is an 8-bit field."
172 ///
173 /// "A decoded value", so the resolved Start or End rather than the delta
174 /// that spelled it — a filter of 200 to 300 has both deltas well inside the
175 /// byte and one endpoint outside it.
176 #[error("priority filter names {0}, which a Publisher Priority cannot hold")]
177 PriorityAboveTheField(u64),
178 /// A property filter names a Property Type that is not an integer-valued one.
179 ///
180 /// Sections 10.2.13 and 10.2.14 say the same of both: the filter selects a
181 /// range "for a required Object Property Type which MUST be even, i.e. a
182 /// single integer value (see Figure 2), otherwise the endpoint MUST reject
183 /// this with REQUEST_ERROR with error code INVALID_FILTER".
184 ///
185 /// The parity is the Key-Value-Pair convention doing the work: an even Type
186 /// carries a bare varint and an odd one carries length-prefixed bytes, so an
187 /// odd Property Type is a property whose value is not a number and a range
188 /// over it has nothing to compare.
189 #[error("property filter names property type {0}, which is not an integer-valued one")]
190 PropertyTypeIsNotAnInteger(u64),
191}
192
193/// The value ran out before `detail`.
194const NO_SET_ID: &str = "it ends before the SetID";
195const NO_PROPERTY_TYPE: &str = "it ends before the Property Type this filter type carries";
196
197impl RangeFilter {
198 /// Read a Range Filter from the bytes of its parameter value.
199 ///
200 /// `bytes` is the value after the Key-Value-Pair's Length has been consumed,
201 /// which is what a decoded parameter holds — the Length in Section 5.1.3's
202 /// figure is that same prefix and not a second one inside the value.
203 /// An empty value decodes to a filter with no ranges rather than an error:
204 /// "In REQUEST_UPDATE, Length can be 0 to remove a filter parameter or
205 /// non-zero to replace that entire filter parameter including all sets and
206 /// Property Types." The three-field filters can express that in zero bytes
207 /// only if the SetID is also absent, so an empty value is taken as the
208 /// removal and anything shorter than its own prefix is not.
209 pub fn decode_moqt<P: MoqtProfile>(
210 parameter_type: u64,
211 bytes: &[u8],
212 ) -> Result<Self, RangeFilterError> {
213 if !is_range_filter(parameter_type) {
214 return Err(RangeFilterError::NotARangeFilter(parameter_type));
215 }
216 if bytes.is_empty() {
217 return Ok(RangeFilter {
218 parameter_type,
219 set_id: 0,
220 property_type: None,
221 ranges: Vec::new(),
222 });
223 }
224
225 let mut buf = bytes;
226 if buf.remaining() < 1 {
227 return Err(RangeFilterError::Malformed { detail: NO_SET_ID });
228 }
229 let set_id = buf.get_u8();
230
231 let property_type = if carries_a_property_type(parameter_type) {
232 Some(
233 VarInt::decode_moqt::<P>(&mut buf)
234 .map_err(|_| RangeFilterError::Malformed { detail: NO_PROPERTY_TYPE })?
235 .into_inner(),
236 )
237 } else {
238 None
239 };
240
241 // Two baselines, alternating. A Start counts from the previous Range's
242 // End and an End counts from the Start beside it, so the running value
243 // is reset by each field rather than carried across the pair.
244 let mut ranges = Vec::new();
245 let mut previous_end: u64 = 0;
246 while buf.has_remaining() {
247 let delta = VarInt::decode_moqt::<P>(&mut buf)
248 .map_err(|_| RangeFilterError::Malformed { detail: "a Range's Start is short" })?
249 .into_inner();
250 let start = previous_end
251 .checked_add(delta)
252 .ok_or(RangeFilterError::DeltaOverflow { base: previous_end, delta })?;
253
254 if !buf.has_remaining() {
255 // The final End is the only one that may be left off, and
256 // leaving it off means the range has no end.
257 ranges.push(FilterRange { start, end: None });
258 break;
259 }
260
261 let delta = VarInt::decode_moqt::<P>(&mut buf)
262 .map_err(|_| RangeFilterError::Malformed { detail: "a Range's End is short" })?
263 .into_inner();
264 let end = start
265 .checked_add(delta)
266 .ok_or(RangeFilterError::DeltaOverflow { base: start, delta })?;
267 ranges.push(FilterRange { start, end: Some(end) });
268 previous_end = end;
269 }
270
271 let filter = RangeFilter { parameter_type, set_id, property_type, ranges };
272 filter.check_its_own_types()?;
273 Ok(filter)
274 }
275
276 /// The two rules a Range Filter can break once it has decoded cleanly.
277 ///
278 /// Both are answered with the same REQUEST_ERROR as the delta rule, so they
279 /// belong beside it rather than in a separate query a caller has to know to
280 /// make. A filter that reaches an application from here has already been
281 /// held to everything Section 5.1.3 and Sections 10.2.12 through 10.2.14
282 /// state about its own contents; what is left for the session to decide is
283 /// the ceiling and the repeats, which need more than one parameter to see.
284 fn check_its_own_types(&self) -> Result<(), RangeFilterError> {
285 if let Some(property_type) = self.property_type {
286 if !property_type.is_multiple_of(2) {
287 return Err(RangeFilterError::PropertyTypeIsNotAnInteger(property_type));
288 }
289 }
290 if self.parameter_type == PRIORITY_FILTER_PARAMETER {
291 for range in &self.ranges {
292 for value in [Some(range.start), range.end].into_iter().flatten() {
293 if value > 255 {
294 return Err(RangeFilterError::PriorityAboveTheField(value));
295 }
296 }
297 }
298 }
299 Ok(())
300 }
301
302 /// Write this filter as a parameter value.
303 ///
304 /// Refuses what the decoder refuses, and one thing more: a range whose End is
305 /// below its Start, and an unbounded range anywhere but last. Both encode
306 /// perfectly well and neither reads back as what was written — a backwards
307 /// End wraps its delta into a nine-byte integer that resolves to an
308 /// unrelated value, and an unbounded range in the middle silently pairs its
309 /// successor's Start as its own End.
310 pub fn encode_moqt<P: MoqtProfile>(
311 &self,
312 buf: &mut impl BufMut,
313 ) -> Result<(), RangeFilterError> {
314 if !is_range_filter(self.parameter_type) {
315 return Err(RangeFilterError::NotARangeFilter(self.parameter_type));
316 }
317 match (carries_a_property_type(self.parameter_type), self.property_type) {
318 (true, None) => {
319 return Err(RangeFilterError::Malformed {
320 detail: "this filter type carries a Property Type and none was given",
321 })
322 }
323 (false, Some(_)) => {
324 return Err(RangeFilterError::Malformed {
325 detail: "this filter type carries no Property Type and one was given",
326 })
327 }
328 _ => {}
329 }
330 self.check_its_own_types()?;
331
332 let mut out = Vec::new();
333 out.push(self.set_id);
334 if let Some(property_type) = self.property_type {
335 VarInt::from_u64_moqt(property_type).encode_moqt::<P>(&mut out);
336 }
337
338 let mut previous_end: u64 = 0;
339 for (index, range) in self.ranges.iter().enumerate() {
340 let start_delta =
341 range.start.checked_sub(previous_end).ok_or(RangeFilterError::Malformed {
342 detail: "the Ranges are not in ascending order",
343 })?;
344 VarInt::from_u64_moqt(start_delta).encode_moqt::<P>(&mut out);
345
346 match range.end {
347 Some(end) => {
348 let end_delta =
349 end.checked_sub(range.start).ok_or(RangeFilterError::Malformed {
350 detail: "a Range ends before it starts",
351 })?;
352 VarInt::from_u64_moqt(end_delta).encode_moqt::<P>(&mut out);
353 previous_end = end;
354 }
355 None if index + 1 == self.ranges.len() => {}
356 None => {
357 return Err(RangeFilterError::Malformed {
358 detail: "only the final Range may be left open",
359 })
360 }
361 }
362 }
363
364 buf.put_slice(&out);
365 Ok(())
366 }
367
368 /// Whether `value` passes this filter.
369 ///
370 /// A filter with no ranges passes nothing, which is what a filter that lists
371 /// no acceptable values says. The removal form of a REQUEST_UPDATE is the
372 /// same shape on the wire and is not the same statement, so a caller acting
373 /// on an update reads the removal from the parameter's zero Length before it
374 /// asks anything to pass.
375 pub fn passes(&self, value: u64) -> bool {
376 self.ranges.iter().any(|range| range.contains(value))
377 }
378
379 /// This filter's key for the rule about repeats.
380 pub fn key(&self) -> RangeFilterKey {
381 (self.parameter_type, self.set_id, self.property_type)
382 }
383}
384
385/// Read every Range Filter in a decoded parameter list, in the order they arrive.
386///
387/// Parameters that are not Range Filters are skipped, so this can be handed the
388/// whole list a message carries.
389pub fn decode_all_moqt<P: MoqtProfile>(
390 parameters: &[KeyValuePair],
391) -> Result<Vec<RangeFilter>, RangeFilterError> {
392 let mut filters = Vec::new();
393 for parameter in parameters {
394 let parameter_type = parameter.key.into_inner();
395 if !is_range_filter(parameter_type) {
396 continue;
397 }
398 let bytes = match ¶meter.value {
399 KvpValue::Bytes(bytes) => bytes.as_slice(),
400 // Unreachable from draft-19's decoder, which picks the shape from
401 // the parameter table and finds all five length-prefixed. A caller
402 // that built the pair in memory can still get here.
403 KvpValue::Varint(_) => {
404 return Err(RangeFilterError::Malformed {
405 detail: "its value is a bare varint where the type defines a filter",
406 })
407 }
408 };
409 filters.push(RangeFilter::decode_moqt::<P>(parameter_type, bytes)?);
410 }
411 Ok(filters)
412}
413
414/// The total number of Ranges across `filters`.
415///
416/// This is the count MAX_FILTER_RANGES bounds: "Range Filters are only allowed
417/// if the setup option MAX_FILTER_RANGES is non-zero, which limits the total
418/// number of Ranges allowed in all Range Filter parameters for a given
419/// subscription or fetch." Across all of them, so a peer cannot spend the budget
420/// a parameter at a time.
421///
422/// The ceiling itself is not applied here. It comes from a Setup Option, it is
423/// per subscription rather than per message, and exceeding it is answered with a
424/// REQUEST_ERROR — three reasons it belongs to whatever holds the session.
425pub fn total_ranges(filters: &[RangeFilter]) -> usize {
426 filters.iter().map(|filter| filter.ranges.len()).sum()
427}
428
429/// The first key that appears twice, if any.
430///
431/// "If the same combination of Parameter Type, SetID, and Property Type (only in
432/// the Track and Object Property Filters) repeat in any message, an endpoint
433/// MUST reject this with REQUEST_ERROR with error code INVALID_FILTER."
434///
435/// Repeats of a Range Filter type are otherwise expected — "The Track Property
436/// filter parameter MAY appear multiple times in a SUBSCRIBE_TRACKS message" —
437/// so the type alone is not the key and a check written against it would refuse
438/// what the same section permits two paragraphs earlier.
439pub fn first_repeated_key(filters: &[RangeFilter]) -> Option<RangeFilterKey> {
440 let mut seen: Vec<RangeFilterKey> = Vec::with_capacity(filters.len());
441 for filter in filters {
442 let key = filter.key();
443 if seen.contains(&key) {
444 return Some(key);
445 }
446 seen.push(key);
447 }
448 None
449}