Skip to main content

moqtap_codec/
subscription_filter.rs

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