Skip to main content

moqtap_codec/
object_properties.rs

1//! Object Properties, called Extension Headers before draft-17, read as an
2//! ordered list.
3//!
4//! The object readers keep a property block as the bytes the wire carried, and
5//! a relay forwards those untouched. [`parse_object_properties`] reads them by
6//! the rules of one draft. A list rather than a map: a Type may repeat, an odd
7//! Type carries bytes rather than a number, and a Type the draft does not name
8//! still has to be kept.
9
10use crate::error::CodecError;
11use crate::kvp::KvpError;
12use crate::varint::{Moqt17, Moqt18, VarInt, VarIntError};
13use crate::version::{DraftVersion, VarIntEncoding};
14
15/// What separates one draft's property block from another's.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct PropertyRules {
18    /// The varint every Type, Length and even value is written in.
19    pub varint: VarIntEncoding,
20    /// Whether each Type is a delta from the one before it rather than
21    /// absolute.
22    pub delta_types: bool,
23    /// The largest value an odd Type may carry, where the draft sets one.
24    pub max_value_len: Option<u64>,
25    /// The Type that holds Immutable Properties (Immutable Extensions), where
26    /// the draft defines one.
27    pub immutable_type: Option<u64>,
28}
29
30impl PropertyRules {
31    /// The rules `draft` gives its object property block. `None` on draft-07,
32    /// whose objects carry none.
33    pub fn for_draft(draft: DraftVersion) -> Option<Self> {
34        let varint = draft.varint_encoding();
35        let rules = |delta_types, max_value_len, immutable_type| {
36            Some(Self { varint, delta_types, max_value_len, immutable_type })
37        };
38        match draft {
39            DraftVersion::Draft07 => None,
40            // Draft-08 and 09 Section 8.1.1.2, draft-10 Section 9.1.1.2.
41            DraftVersion::Draft08 | DraftVersion::Draft09 | DraftVersion::Draft10 => {
42                rules(false, None, None)
43            }
44            // Section 1.3.2: "The maximum length of a value is 2^16-1 bytes."
45            DraftVersion::Draft11 | DraftVersion::Draft12 | DraftVersion::Draft13 => {
46                rules(false, Some(MAX_VALUE_LEN), None)
47            }
48            // Section 1.4.2. Immutable Extensions: draft-14 Section 11.2,
49            // draft-15 Section 11.1.
50            DraftVersion::Draft14 | DraftVersion::Draft15 => {
51                rules(false, Some(MAX_VALUE_LEN), Some(IMMUTABLE))
52            }
53            // Delta Types: draft-16 Section 1.4.2, drafts 17-20 Section 1.4.3,
54            // drafts 21-23 Section 8.3. Immutable Extensions: draft-16 Section
55            // 11.2; Immutable Properties: draft-17 Section 11.6, drafts 18-20
56            // Section 12.7, drafts 21-23 Section 10.7.
57            DraftVersion::Draft16
58            | DraftVersion::Draft17
59            | DraftVersion::Draft18
60            | DraftVersion::Draft19
61            | DraftVersion::Draft20
62            | DraftVersion::Draft21
63            | DraftVersion::Draft22
64            | DraftVersion::Draft23 => rules(true, Some(MAX_VALUE_LEN), Some(IMMUTABLE)),
65        }
66    }
67}
68
69const MAX_VALUE_LEN: u64 = 0xFFFF;
70const IMMUTABLE: u64 = 0x0B;
71
72/// One property, as the wire carried it.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct ObjectProperty<'a> {
75    /// The absolute Type, with any delta encoding resolved.
76    pub property_type: u64,
77    /// The value.
78    pub value: PropertyValue<'a>,
79}
80
81/// A property's value.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum PropertyValue<'a> {
84    /// The value of an even Type.
85    Varint(u64),
86    /// The value of an odd Type, borrowed from the block.
87    Bytes(&'a [u8]),
88    /// Immutable Properties: the raw value, and its contents read by the same
89    /// rules.
90    Immutable {
91        /// The value as the wire carried it.
92        bytes: &'a [u8],
93        /// The properties inside it, in wire order.
94        properties: Vec<ObjectProperty<'a>>,
95    },
96}
97
98/// Read `block`, the contents of one object's property block without its
99/// length or count prefix, by the rules of `draft`.
100///
101/// Refuses a block the draft calls malformed: a property that runs past the
102/// end of the block, including a varint cut short inside it, and Immutable
103/// Properties inside Immutable Properties are
104/// [`CodecError::MalformedObjectProperties`]; an odd value longer than the
105/// draft allows is [`KvpError::ValueTooLong`]; a Type delta past 2^64 - 1 is
106/// [`CodecError::KeyDeltaOverflow`].
107pub fn parse_object_properties(
108    draft: DraftVersion,
109    block: &[u8],
110) -> Result<Vec<ObjectProperty<'_>>, CodecError> {
111    match PropertyRules::for_draft(draft) {
112        Some(rules) => parse_with(&rules, block),
113        None if block.is_empty() => Ok(Vec::new()),
114        None => Err(malformed(0, format!("{draft:?} objects carry no properties"))),
115    }
116}
117
118/// [`parse_object_properties`] with the rules given directly.
119pub fn parse_with<'a>(
120    rules: &PropertyRules,
121    block: &'a [u8],
122) -> Result<Vec<ObjectProperty<'a>>, CodecError> {
123    read_run(rules, block, 0, false)
124}
125
126fn read_run<'a>(
127    rules: &PropertyRules,
128    run: &'a [u8],
129    base: usize,
130    nested: bool,
131) -> Result<Vec<ObjectProperty<'a>>, CodecError> {
132    let mut properties = Vec::new();
133    let mut cursor = run;
134    let mut prev_type = 0u64;
135    while !cursor.is_empty() {
136        let offset = base + (run.len() - cursor.len());
137        let raw = read_varint(rules, &mut cursor, offset)?;
138        let property_type = if rules.delta_types {
139            prev_type.checked_add(raw).ok_or(CodecError::KeyDeltaOverflow(prev_type, raw))?
140        } else {
141            raw
142        };
143        prev_type = property_type;
144
145        if property_type % 2 == 0 {
146            let value = read_varint(rules, &mut cursor, offset)?;
147            properties.push(ObjectProperty { property_type, value: PropertyValue::Varint(value) });
148            continue;
149        }
150
151        let len = read_varint(rules, &mut cursor, offset)?;
152        if rules.max_value_len.is_some_and(|max| len > max) {
153            return Err(KvpError::ValueTooLong(usize::try_from(len).unwrap_or(usize::MAX)).into());
154        }
155        let Some(bytes) = usize::try_from(len).ok().and_then(|len| cursor.get(..len)) else {
156            return Err(malformed(
157                offset,
158                format!(
159                    "property 0x{property_type:02X} claims {len} bytes but the property block has \
160                     {} left",
161                    cursor.len()
162                ),
163            ));
164        };
165        let value_base = base + (run.len() - cursor.len());
166        cursor = &cursor[bytes.len()..];
167
168        let value = if rules.immutable_type == Some(property_type) {
169            if nested {
170                return Err(malformed(
171                    offset,
172                    "Immutable Properties inside Immutable Properties".to_string(),
173                ));
174            }
175            PropertyValue::Immutable {
176                bytes,
177                properties: read_run(rules, bytes, value_base, true)?,
178            }
179        } else {
180            PropertyValue::Bytes(bytes)
181        };
182        properties.push(ObjectProperty { property_type, value });
183    }
184    Ok(properties)
185}
186
187fn read_varint(
188    rules: &PropertyRules,
189    cursor: &mut &[u8],
190    offset: usize,
191) -> Result<u64, CodecError> {
192    let decoded = match rules.varint {
193        VarIntEncoding::Rfc9000 => VarInt::decode(cursor),
194        VarIntEncoding::Moqt17 => VarInt::decode_moqt::<Moqt17>(cursor),
195        VarIntEncoding::Moqt18 => VarInt::decode_moqt::<Moqt18>(cursor),
196    };
197    match decoded {
198        Ok(v) => Ok(v.into_inner()),
199        // The block's length is already known, so this is a property running
200        // past it rather than input still to arrive.
201        Err(VarIntError::UnexpectedEnd) => {
202            Err(malformed(offset, "a property runs past the end of the property block".to_string()))
203        }
204        Err(e) => Err(e.into()),
205    }
206}
207
208fn malformed(offset: usize, detail: String) -> CodecError {
209    CodecError::MalformedObjectProperties { offset, detail }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    fn rules(draft: DraftVersion) -> PropertyRules {
217        PropertyRules::for_draft(draft).unwrap()
218    }
219
220    #[test]
221    fn reads_odd_even_and_repeated_types_in_wire_order() {
222        // draft-17 subgroup-properties-odd-type: 0x0D = cafe, then delta 0x2F to 0x3C = 2.
223        let props =
224            parse_with(&rules(DraftVersion::Draft17), &[0x0d, 0x02, 0xca, 0xfe, 0x2f, 0x02])
225                .unwrap();
226        assert_eq!(
227            props,
228            vec![
229                ObjectProperty { property_type: 0x0d, value: PropertyValue::Bytes(&[0xca, 0xfe]) },
230                ObjectProperty { property_type: 0x3c, value: PropertyValue::Varint(2) },
231            ]
232        );
233        let repeated =
234            parse_with(&rules(DraftVersion::Draft17), &[0x0d, 0x01, 0xaa, 0x00, 0x01, 0xbb])
235                .unwrap();
236        assert_eq!(repeated.len(), 2);
237        assert_eq!(repeated[1].property_type, 0x0d);
238    }
239
240    #[test]
241    fn absolute_types_before_draft_16() {
242        let props = parse_with(&rules(DraftVersion::Draft15), &[0x3c, 0x01, 0x3e, 0x02]).unwrap();
243        assert_eq!(props[1].property_type, 0x3e);
244        let delta = parse_with(&rules(DraftVersion::Draft16), &[0x3c, 0x01, 0x02, 0x02]).unwrap();
245        assert_eq!(delta[1].property_type, 0x3e);
246    }
247
248    #[test]
249    fn immutable_contents_restart_their_delta_at_zero() {
250        let props = parse_with(&rules(DraftVersion::Draft17), &[0x0b, 0x02, 0x3c, 0x02]).unwrap();
251        let PropertyValue::Immutable { bytes, properties } = &props[0].value else {
252            panic!("0xB not read as Immutable Properties: {props:?}");
253        };
254        assert_eq!(*bytes, &[0x3c, 0x02]);
255        assert_eq!(properties[0].property_type, 0x3c);
256        // Draft-13 has no Immutable Extensions, so 0xB is plain bytes there.
257        let plain = parse_with(&rules(DraftVersion::Draft13), &[0x0b, 0x02, 0x3c, 0x02]).unwrap();
258        assert_eq!(plain[0].value, PropertyValue::Bytes(&[0x3c, 0x02]));
259    }
260
261    #[test]
262    fn refuses_what_the_drafts_call_malformed() {
263        let d17 = rules(DraftVersion::Draft17);
264        // draft-17 subgroup-properties-overrun.
265        assert!(matches!(
266            parse_with(&d17, &[0x0d, 0x05, 0xca]),
267            Err(CodecError::MalformedObjectProperties { offset: 0, .. })
268        ));
269        // Even value cut short.
270        assert!(matches!(
271            parse_with(&d17, &[0x3c, 0x02, 0x3c]),
272            Err(CodecError::MalformedObjectProperties { offset: 2, .. })
273        ));
274        assert!(matches!(
275            parse_with(&d17, &[0x0b, 0x03, 0x0b, 0x01, 0x00]),
276            Err(CodecError::MalformedObjectProperties { offset: 2, .. })
277        ));
278        let mut too_long = vec![0x0d];
279        VarInt::from_u64_moqt(0x10000).encode_moqt::<Moqt17>(&mut too_long);
280        assert_eq!(
281            parse_with(&d17, &too_long),
282            Err(CodecError::Kvp(KvpError::ValueTooLong(0x10000)))
283        );
284        let mut overflow = vec![0x3c, 0x01];
285        VarInt::from_u64_moqt(u64::MAX).encode_moqt::<Moqt17>(&mut overflow);
286        assert_eq!(parse_with(&d17, &overflow), Err(CodecError::KeyDeltaOverflow(0x3c, u64::MAX)));
287        // No limit before draft-11.
288        assert!(matches!(
289            parse_with(&rules(DraftVersion::Draft10), &[0x0d, 0x80, 0x01, 0x00, 0x00]),
290            Err(CodecError::MalformedObjectProperties { .. })
291        ));
292    }
293
294    #[test]
295    fn draft_07_has_no_block() {
296        assert_eq!(parse_object_properties(DraftVersion::Draft07, &[]), Ok(Vec::new()));
297        assert!(parse_object_properties(DraftVersion::Draft07, &[0x02, 0x00]).is_err());
298    }
299}