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