Skip to main content

moqtap_codec/
subscription_filter.rs

1//! The subscription filter that drafts 15 and later carry inside a parameter.
2//!
3//! Through draft-14 a subscription's filter is a group of fields on SUBSCRIBE
4//! and its relatives: a Filter Type, and the Start Location and End Group that
5//! type promises. Draft-15 moved the whole group into one length-prefixed
6//! parameter, and draft-19 renamed that parameter from SUBSCRIPTION_FILTER to
7//! LOCATION_FILTER while leaving both its type number, 0x21, and its contents
8//! alone.
9//!
10//! The move is why this module exists. A parameter value is a run of bytes, and
11//! a codec that carries it as bytes carries the Filter Type with it — including
12//! the values the drafts require a receiver to close the session over, and
13//! including the Start Location a relay needs in order to know which Objects it
14//! was asked for.
15
16use bytes::{Buf, BufMut};
17
18use crate::error::CodecError;
19use crate::kvp::{KeyValuePair, KvpValue};
20use crate::types::{FilterType, Location};
21use crate::varint::{MoqtProfile, VarInt, VarIntError};
22
23/// The parameter type carrying a subscription filter on drafts 15 and later.
24///
25/// Named SUBSCRIPTION_FILTER on drafts 15 through 18 and LOCATION_FILTER on
26/// draft-19, which introduced a second family of filter parameters and renamed
27/// this one to say which kind it is. The number did not move.
28pub const SUBSCRIPTION_FILTER_PARAMETER: u64 = 0x21;
29
30/// The end of an AbsoluteRange filter, in whichever of the two forms the draft
31/// that carried it writes.
32///
33/// Drafts 15 and 16 write the End Group out: "AbsoluteRange (0x4): The filter
34/// Start Location and End Group are specified explicitly... End Group MUST
35/// specify the same or a larger Group than specified in Start Location."
36///
37/// Drafts 17 and later write a delta instead: "If the specified End Group Delta
38/// is zero, the remainder of that Group passes the filter. Otherwise, the last
39/// Group ID to be delivered will be the Group ID in Start Location plus the End
40/// Group Delta."
41///
42/// The two spell the same intent and do not spell it the same way. A delta of
43/// zero bounds the range to the starting group; an absolute End Group of zero
44/// bounds it to group zero, which is a range that usually excludes its own start.
45/// Resolving one into the other is [`SubscriptionFilter::last_group`], and it is
46/// the only place the two forms meet.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum FilterEnd {
49    /// The End Group as drafts 15 and 16 put it on the wire: the last Group ID
50    /// that passes the filter, in full.
51    Group(u64),
52    /// The End Group Delta as drafts 17 and later put it on the wire, measured
53    /// from the Start Location's Group.
54    GroupDelta(u64),
55}
56
57/// A decoded subscription filter.
58///
59/// The fields after the Filter Type are the ones that type promises, which is
60/// why they are optional here and why both directions check them against it: an
61/// AbsoluteStart filter puts a Start Location on the wire and no End Group, and
62/// the two open-ended types put neither.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct SubscriptionFilter {
65    /// Which of the fields below the filter carries, and where the subscription
66    /// starts when it carries none.
67    pub filter_type: FilterType,
68    /// Present on AbsoluteStart and AbsoluteRange.
69    pub start_location: Option<Location>,
70    /// Present on AbsoluteRange alone.
71    pub end_group: Option<FilterEnd>,
72}
73
74/// Report a parameter value that is not a filter.
75///
76/// Drafts 15 and 16 answer this with PROTOCOL_VIOLATION and drafts 17 through 19
77/// with KEY_VALUE_FORMATTING_ERROR, so the variant carries the malformation and
78/// each draft's session table carries the code.
79fn malformed(detail: &'static str) -> CodecError {
80    CodecError::SubscriptionFilterMalformed { detail }
81}
82
83const NO_FILTER_TYPE: &str = "it carries no Filter Type";
84const NO_START: &str = "its Filter Type promises a Start Location and the value ends first";
85const NO_END: &str = "its Filter Type promises an End Group and the value ends first";
86const TRAILING: &str = "bytes follow the filter inside the parameter";
87const START_MISSING: &str = "its Filter Type promises a Start Location and none is set";
88const START_SURPLUS: &str = "its Filter Type promises no Start Location and one is set";
89const END_MISSING: &str = "its Filter Type promises an End Group and none is set";
90const END_SURPLUS: &str = "its Filter Type promises no End Group and one is set";
91const END_IS_DELTA: &str = "its End Group is a delta where this draft writes the group in full";
92const END_IS_ABSOLUTE: &str = "its End Group is written in full where this draft writes a delta";
93
94impl SubscriptionFilter {
95    /// Whether this Filter Type puts a Start Location on the wire.
96    fn wants_start(filter_type: FilterType) -> bool {
97        matches!(filter_type, FilterType::AbsoluteStart | FilterType::AbsoluteRange)
98    }
99
100    /// Whether this Filter Type puts an End Group on the wire.
101    fn wants_end(filter_type: FilterType) -> bool {
102        matches!(filter_type, FilterType::AbsoluteRange)
103    }
104
105    /// Decode a filter from a draft-15 or draft-16 parameter value: QUIC
106    /// variable-length integers, and an End Group written out in full.
107    pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
108        Self::decode_with(bytes, FilterEnd::Group, |buf: &mut &[u8]| VarInt::decode(buf))
109    }
110
111    /// Decode a filter from a draft-17, draft-18 or draft-19 parameter value:
112    /// MoQT variable-length integers, and an End Group Delta.
113    pub fn decode_moqt<P: MoqtProfile>(bytes: &[u8]) -> Result<Self, CodecError> {
114        Self::decode_with(bytes, FilterEnd::GroupDelta, |buf: &mut &[u8]| {
115            VarInt::decode_moqt::<P>(buf)
116        })
117    }
118
119    /// The body both readers share, taking the integer encoding and the End
120    /// Group spelling as parameters.
121    ///
122    /// A read that runs out of bytes is not [`CodecError::UnexpectedEnd`] here.
123    /// The frame is intact and the parameter's declared length was satisfied;
124    /// what ran out is the filter inside it, which is the rule drafts 15 and 16
125    /// state in as many words — "If the length of the Subscription Filter does
126    /// not match the parameter length" — and which the general key-value rule
127    /// covers on the drafts after them. Reporting a truncated frame instead
128    /// would send it to a table arm that answers with no close at all.
129    ///
130    /// Only running out is treated that way. An integer that is present and not
131    /// a legal integer — a seven-byte length on a draft-17 session, which that
132    /// draft calls an invalid code point and answers with PROTOCOL_VIOLATION —
133    /// is a rule of its own, and it is reported as itself so that each draft's
134    /// table can answer it as its own draft does rather than as a filter that
135    /// did not match its type.
136    fn decode_with<F>(
137        bytes: &[u8],
138        end: fn(u64) -> FilterEnd,
139        mut read: F,
140    ) -> Result<Self, CodecError>
141    where
142        F: FnMut(&mut &[u8]) -> Result<VarInt, VarIntError>,
143    {
144        /// Map only *the value ended here* onto the filter's own rule.
145        fn ran_out(err: VarIntError, detail: &'static str) -> CodecError {
146            match err {
147                VarIntError::UnexpectedEnd => malformed(detail),
148                other => CodecError::VarInt(other),
149            }
150        }
151
152        let mut buf = bytes;
153        let raw = read(&mut buf).map_err(|e| ran_out(e, NO_FILTER_TYPE))?.into_inner();
154        let filter_type = FilterType::from_u64(raw).ok_or(CodecError::InvalidFilterType(raw))?;
155
156        let start_location = if Self::wants_start(filter_type) {
157            let group = read(&mut buf).map_err(|e| ran_out(e, NO_START))?;
158            let object = read(&mut buf).map_err(|e| ran_out(e, NO_START))?;
159            Some(Location { group, object })
160        } else {
161            None
162        };
163
164        let end_group = if Self::wants_end(filter_type) {
165            Some(end(read(&mut buf).map_err(|e| ran_out(e, NO_END))?.into_inner()))
166        } else {
167            None
168        };
169
170        if buf.has_remaining() {
171            return Err(malformed(TRAILING));
172        }
173
174        Ok(SubscriptionFilter { filter_type, start_location, end_group })
175    }
176
177    /// Write this filter as a draft-15 or draft-16 parameter value.
178    ///
179    /// Refuses a filter whose fields disagree with its own Filter Type, for the
180    /// reason the decoder derives presence from that type: a filter with a
181    /// surplus field is written out and read back short, and one with a missing
182    /// field is written short and read back out of whatever follows it in the
183    /// parameter block.
184    /// Also refuses a value the QUIC integer encoding cannot spell. That
185    /// encoding stops at 2^62 - 1 and the fields here are 64-bit, so a Start
186    /// Location group above the limit would otherwise go out as an unrelated
187    /// number with the length bits folded into it.
188    pub fn encode(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
189        self.encode_with(false, buf, |v, out| {
190            VarInt::from_u64(v)?.encode(out);
191            Ok(())
192        })
193    }
194
195    /// Write this filter as a draft-17, draft-18 or draft-19 parameter value.
196    ///
197    /// The same field checks, and no range check: the MoQT integer encoding
198    /// reaches the whole 64-bit range, so every value these fields can hold has
199    /// a representation.
200    pub fn encode_moqt<P: MoqtProfile>(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
201        self.encode_with(true, buf, |v, out| {
202            VarInt::from_u64_moqt(v).encode_moqt::<P>(out);
203            Ok(())
204        })
205    }
206
207    fn encode_with<W>(
208        &self,
209        end_is_delta: bool,
210        buf: &mut impl BufMut,
211        write: W,
212    ) -> Result<(), CodecError>
213    where
214        W: Fn(u64, &mut Vec<u8>) -> Result<(), CodecError>,
215    {
216        let wants_start = Self::wants_start(self.filter_type);
217        if wants_start && self.start_location.is_none() {
218            return Err(malformed(START_MISSING));
219        }
220        if !wants_start && self.start_location.is_some() {
221            return Err(malformed(START_SURPLUS));
222        }
223
224        let wants_end = Self::wants_end(self.filter_type);
225        let end = match (wants_end, self.end_group) {
226            (true, None) => return Err(malformed(END_MISSING)),
227            (false, Some(_)) => return Err(malformed(END_SURPLUS)),
228            (_, value) => value,
229        };
230        match end {
231            Some(FilterEnd::GroupDelta(_)) if !end_is_delta => {
232                return Err(malformed(END_IS_DELTA));
233            }
234            Some(FilterEnd::Group(_)) if end_is_delta => {
235                return Err(malformed(END_IS_ABSOLUTE));
236            }
237            _ => {}
238        }
239
240        let mut out = Vec::new();
241        write(self.filter_type as u64, &mut out)?;
242        if let Some(start) = self.start_location {
243            write(start.group.into_inner(), &mut out)?;
244            write(start.object.into_inner(), &mut out)?;
245        }
246        if let Some(FilterEnd::Group(v) | FilterEnd::GroupDelta(v)) = end {
247            write(v, &mut out)?;
248        }
249        buf.put_slice(&out);
250        Ok(())
251    }
252
253    /// This filter as the parameter a draft-15 or draft-16 message carries it
254    /// in.
255    ///
256    /// The reason to have it: from draft-15 a client asking for a range hands
257    /// its endpoint a parameter list, and the filter inside it is a run of bytes
258    /// the caller has to assemble. Assembled by hand it is assembled wrong — the
259    /// fields present depend on the Filter Type, and a frame short by the ones
260    /// it promised is one the peer reads off the end of.
261    pub fn parameter(&self) -> Result<KeyValuePair, CodecError> {
262        let mut value = Vec::new();
263        self.encode(&mut value)?;
264        Ok(KeyValuePair {
265            key: VarInt::from_u64_moqt(SUBSCRIPTION_FILTER_PARAMETER),
266            value: KvpValue::Bytes(value),
267        })
268    }
269
270    /// This filter as the parameter a draft-17, draft-18 or draft-19 message
271    /// carries it in.
272    pub fn parameter_moqt<P: MoqtProfile>(&self) -> Result<KeyValuePair, CodecError> {
273        let mut value = Vec::new();
274        self.encode_moqt::<P>(&mut value)?;
275        Ok(KeyValuePair {
276            key: VarInt::from_u64_moqt(SUBSCRIPTION_FILTER_PARAMETER),
277            value: KvpValue::Bytes(value),
278        })
279    }
280
281    /// The filter carried by a message's parameter list, if it carries one.
282    ///
283    /// `None` when no filter parameter is present, which every draft from 15 on
284    /// defines as an unfiltered subscription rather than as an omission.
285    pub fn from_parameters(parameters: &[KeyValuePair]) -> Option<Result<Self, CodecError>> {
286        Self::from_parameters_with(parameters, Self::decode)
287    }
288
289    /// The same, reading the MoQT integer encoding of drafts 17 and later.
290    pub fn from_parameters_moqt<P: MoqtProfile>(
291        parameters: &[KeyValuePair],
292    ) -> Option<Result<Self, CodecError>> {
293        Self::from_parameters_with(parameters, Self::decode_moqt::<P>)
294    }
295
296    fn from_parameters_with(
297        parameters: &[KeyValuePair],
298        decode: fn(&[u8]) -> Result<Self, CodecError>,
299    ) -> Option<Result<Self, CodecError>> {
300        let parameter =
301            parameters.iter().find(|p| p.key.into_inner() == SUBSCRIPTION_FILTER_PARAMETER)?;
302        match &parameter.value {
303            KvpValue::Bytes(value) => Some(decode(value)),
304            KvpValue::Varint(_) => {
305                Some(Err(malformed("its value is a bare varint where the type defines a filter")))
306            }
307        }
308    }
309
310    /// The last Group ID that passes this filter, or `None` when the filter is
311    /// open ended.
312    ///
313    /// Errors when the sum leaves the number space, which is a rule drafts 18
314    /// and 19 state and draft-17, which introduced the delta, does not. The
315    /// caller decides whether its draft answers that with a close; every draft
316    /// needs the addition itself, because a subscription bounded by a delta is
317    /// bounded by nothing a comparison can use until the delta is resolved.
318    pub fn last_group(&self) -> Result<Option<u64>, CodecError> {
319        match self.end_group {
320            None => Ok(None),
321            Some(FilterEnd::Group(group)) => Ok(Some(group)),
322            Some(FilterEnd::GroupDelta(delta)) => {
323                let start_group =
324                    self.start_location.map(|l| l.group.into_inner()).unwrap_or_default();
325                start_group
326                    .checked_add(delta)
327                    .map(Some)
328                    .ok_or(CodecError::FilterEndGroupOverflow { start_group, delta })
329            }
330        }
331    }
332}