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 ¶meter.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}