Skip to main content

moqtap_codec/draft17/
data_stream.rs

1//! Draft-17 data stream header encoding and decoding.
2//!
3//! Subgroup header type byte: 0b00X1XXXX (bit 4 always set)
4//!   - bit 0 (0x01): PROPERTIES
5//!   - bits 1-2 (0x06): SUBGROUP_ID_MODE (0=zero, 1=first_obj, 2=explicit, 3=reserved)
6//!   - bit 3 (0x08): END_OF_GROUP
7//!   - bit 5 (0x20): DEFAULT_PRIORITY (no priority byte)
8//!
9//! Datagram type byte: 0b00X0XXXX (bit 4 always 0)
10//!   - bit 0 (0x01): PROPERTIES
11//!   - bit 1 (0x02): END_OF_GROUP
12//!   - bit 2 (0x04): ZERO_OBJECT_ID (object_id=0, field omitted)
13//!   - bit 3 (0x08): DEFAULT_PRIORITY (no priority byte)
14//!   - bit 5 (0x20): STATUS (status byte replaces payload)
15//!
16//! Neither range is fully populated. Draft-17 Sections 10.4.2 and 10.3.1 each
17//! close with a list of type values an endpoint "MUST close the session with a
18//! PROTOCOL_VIOLATION" on receiving, and the two figures spell the surviving
19//! values out: `0x10..0x15 / 0x18..0x1D / 0x30..0x35 / 0x38..0x3D` for a
20//! subgroup header, `0x00..0x0F / 0x20..0x21 / 0x24..0x25 / 0x28..0x29 /
21//! 0x2C..0x2D` for a datagram. Both decoders refuse everything else, so a
22//! header this module hands back always describes a framing the draft defines.
23//!
24//! Fetch header: stream type 0x05 + request_id, followed by objects whose
25//! fields are named by a Serialization Flags varint rather than all being
26//! present — see [`FetchObjectHeader`].
27
28use bytes::{Buf, BufMut};
29
30use super::types::ObjectStatus;
31use crate::error::CodecError;
32use crate::varint::{Moqt17 as Wire, VarInt};
33
34/// Advance `buf` past `len` bytes without copying them.
35fn skip(buf: &mut impl Buf, len: u64) -> Result<(), CodecError> {
36    let len = usize::try_from(len).map_err(|_| CodecError::UnexpectedEnd)?;
37    if buf.remaining() < len {
38        return Err(CodecError::UnexpectedEnd);
39    }
40    buf.advance(len);
41    Ok(())
42}
43
44/// Turn a wire Object Status code into an [`ObjectStatus`], refusing one
45/// draft-17 does not assign.
46///
47/// Draft-17 Section 10.2.1.1 lists the codes an object may carry and says any
48/// other value SHOULD be treated as a protocol error and the session closed
49/// with a PROTOCOL_VIOLATION. Every place this module reads a status converts
50/// it here, so a decoded [`SubgroupObject::object_status`] or
51/// [`DatagramHeader::object_status`] is always a status the draft assigns, and
52/// [`SubgroupObjectMeta::status`] — which stays a raw code because a relay may
53/// carry it to a draft that numbers the set differently — holds one because it
54/// comes from the same conversion.
55fn decoded_status(code: u64) -> Result<ObjectStatus, CodecError> {
56    ObjectStatus::from_u64(code).ok_or(CodecError::InvalidField)
57}
58
59/// Whether an object carrying a given status is allowed a non-empty payload.
60///
61/// Draft-17 Section 10.2.1.1 states the rule arithmetically rather than as a
62/// table: "Any object with a status code other than zero MUST have an empty
63/// payload." So the answer is a property of the status code, and every code but
64/// Normal forbids a payload.
65///
66/// Forbidding is the strict half; permitting is not requiring. A Normal object
67/// with no payload is well formed, and this draft's encodings have a way to
68/// spell it — a zero Object Payload Length followed by the status code 0x0.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum PayloadPermission {
71    /// The status permits a payload but does not require one.
72    Permitted,
73    /// An object with this status has an empty payload, and one carrying bytes
74    /// is malformed.
75    Forbidden,
76}
77
78impl PayloadPermission {
79    /// `true` for [`PayloadPermission::Permitted`].
80    ///
81    /// The permission answers on its own, without a payload length in hand,
82    /// which is the point of reading it off the status.
83    pub fn permits(self) -> bool {
84        matches!(self, PayloadPermission::Permitted)
85    }
86}
87
88// ── Subgroup ──────────────────────────────────────────────────
89
90const SUBGROUP_PROPERTIES_BIT: u8 = 0x01;
91const SUBGROUP_ID_MODE_MASK: u8 = 0x06;
92const SUBGROUP_END_OF_GROUP_BIT: u8 = 0x08;
93const SUBGROUP_BASE_BIT: u8 = 0x10;
94const SUBGROUP_DEFAULT_PRIORITY_BIT: u8 = 0x20;
95/// The bits a subgroup header type must fix: 7 and 6 clear, 4 set. That is the
96/// form `0b00X1XXXX` written as a mask, leaving bit 5 and the low nibble free.
97const SUBGROUP_FORM_MASK: u8 = 0xD0;
98/// SUBGROUP_ID_MODE `0b11`, the value draft-17 reserves.
99const SUBGROUP_ID_MODE_RESERVED: u8 = 0x06;
100
101/// Whether `header_type` is one of the subgroup header types draft-17 defines.
102///
103/// Section 10.4.2 gives the field as
104/// `Type (i) = 0x10..0x15 / 0x18..0x1D / 0x30..0x35 / 0x38..0x3D` and then
105/// names the two ways a byte falls outside it, each of which "MUST close the
106/// session with a PROTOCOL_VIOLATION":
107///
108/// - "Type values with SUBGROUP_ID_MODE set to 0b11: 0x16, 0x17, 0x1E, 0x1F,
109///   0x36, 0x37, 0x3E, 0x3F. This mode is reserved for future use."
110/// - "Type values that do not match the form 0b00X1XXXX (i.e., Type values
111///   outside the ranges 0x10..0x1F and 0x30..0x3F, or values where bit 4 is not
112///   set)."
113///
114/// The two conditions are checked here rather than the eight-value list, and
115/// they enumerate exactly the same bytes: the form fixes bits 7, 6 and 4, and
116/// excluding the reserved mode removes eight of the remaining thirty-two.
117///
118/// The reserved mode is the one worth naming. Its Subgroup ID field is not on
119/// the wire and the draft does not say what the ID would be, so accepting it
120/// means answering with a value nothing in the header stands behind — a zero a
121/// consumer cannot tell from the zero mode `0b00` genuinely means.
122fn subgroup_type_is_valid(raw: u64) -> bool {
123    raw <= 0xFF && {
124        let t = raw as u8;
125        t & SUBGROUP_FORM_MASK == SUBGROUP_BASE_BIT
126            && t & SUBGROUP_ID_MODE_MASK != SUBGROUP_ID_MODE_RESERVED
127    }
128}
129
130/// Whether `raw` sits inside the subgroup form but names the reserved
131/// SUBGROUP_ID_MODE — the first of the two lists quoted above.
132fn subgroup_type_is_reserved_mode(raw: u64) -> bool {
133    raw <= 0xFF && {
134        let t = raw as u8;
135        t & SUBGROUP_FORM_MASK == SUBGROUP_BASE_BIT
136            && t & SUBGROUP_ID_MODE_MASK == SUBGROUP_ID_MODE_RESERVED
137    }
138}
139
140/// The unidirectional stream Type draft-17 Section 9.4 gives the control
141/// stream.
142///
143/// Draft-17 is where the control stream became a pair of unidirectional streams
144/// with a type of their own, which is why Table 3 has an entry a data reader can
145/// be handed and drafts 16 and below do not.
146const SETUP_STREAM_TYPE: u64 = 0x2F00;
147
148/// Refuse a Type field spelled in more than one byte, before anything narrows
149/// it to a byte.
150///
151/// Returns `Ok(None)` when the next Type is a single byte and the caller should
152/// read it itself, `Ok(Some(err))` when it is wider and `refusal` has named the
153/// failure, and `Err` only when the buffer does not hold the whole field yet.
154///
155/// Every Type the subgroup and datagram forms admit is below 0x80 and so
156/// occupies one byte under the MoQT variable-length integer encoding. A wider
157/// spelling is one of three things, and none of them may be read as a header:
158/// an assigned Type that is not a data stream — SETUP, a Type no table
159/// assigns, or a non-minimal spelling of a Type that is valid. The last is the
160/// dangerous one — narrowing a two-byte 0x8001 to its low octet turns it into
161/// an assigned Type, so a peer could name any Type it liked and have it parsed
162/// as another.
163///
164/// The full varint is decoded before `refusal` sees it, which is what lets the
165/// first case be told from the second. Only the second ends the session.
166fn wide_type_refusal(
167    buf: &mut impl Buf,
168    refusal: fn(u64) -> CodecError,
169) -> Result<Option<CodecError>, CodecError> {
170    if !buf.has_remaining() {
171        return Err(CodecError::UnexpectedEnd);
172    }
173    // Under the MoQT encoding the field's length is the number of leading 1
174    // bits in its first byte plus one, so a first byte below 0x80 is the whole
175    // of it.
176    if buf.chunk()[0] < 0x80 {
177        return Ok(None);
178    }
179    let raw = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
180    Ok(Some(refusal(raw)))
181}
182
183/// Which failure a leading unidirectional stream Type that is not the one a
184/// reader wants is.
185///
186/// Draft-17 states two rules about such a Type and answers both with a close,
187/// and telling them apart is the whole job of this function.
188///
189/// Section 3.4 is about the table: "An endpoint that receives an unknown stream
190/// type MUST close the session." A Type Table 3 does not assign is
191/// [`CodecError::UnknownStreamType`].
192///
193/// Section 10.4.2 is about the subgroup form specifically, and the eight Types
194/// inside it that name the reserved SUBGROUP_ID_MODE. Those are not unknown —
195/// the form is assigned and the draft lists the values outright — but they are
196/// unreadable, and they are [`CodecError::InvalidTypeValue`].
197///
198/// Table 3 assigns three things, and two of them are not data streams at all:
199/// FETCH_HEADER, the subgroup form, and SETUP. A subgroup reader handed any of
200/// them refuses it as [`CodecError::InvalidField`] — the value is one this
201/// draft defines, the disagreement is with the reader that was called, and the
202/// session survives it. Reporting SETUP as an unknown stream type would end a
203/// session over the peer's control stream.
204fn stream_type_error(raw: u64) -> CodecError {
205    if raw == FETCH_STREAM_TYPE || raw == SETUP_STREAM_TYPE || subgroup_type_is_valid(raw) {
206        CodecError::InvalidField
207    } else if subgroup_type_is_reserved_mode(raw) {
208        CodecError::InvalidTypeValue {
209            raw,
210            detail: "its SUBGROUP_ID_MODE is 0b11, which this draft reserves",
211        }
212    } else {
213        CodecError::UnknownStreamType(raw)
214    }
215}
216
217#[derive(Debug, Clone)]
218pub struct SubgroupHeader {
219    pub header_type: u8,
220    pub track_alias: VarInt,
221    pub group_id: VarInt,
222    pub subgroup_id: VarInt,
223    pub publisher_priority: Option<u8>,
224}
225
226impl SubgroupHeader {
227    /// Decode a subgroup header, Type field included.
228    ///
229    /// A Type spelled in more than one byte is refused before it is narrowed,
230    /// by `wide_type_refusal`: every Type the subgroup form admits is a
231    /// single byte, and the wider Types Table 3 assigns are not subgroup
232    /// headers.
233    pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
234        if let Some(err) = wide_type_refusal(buf, stream_type_error)? {
235            return Err(err);
236        }
237        let raw = buf.get_u8() as u64;
238        if !subgroup_type_is_valid(raw) {
239            return Err(stream_type_error(raw));
240        }
241        let header_type = raw as u8;
242
243        let track_alias = VarInt::decode_moqt::<Wire>(buf)?;
244        let group_id = VarInt::decode_moqt::<Wire>(buf)?;
245
246        let subgroup_id_mode = (header_type & SUBGROUP_ID_MODE_MASK) >> 1;
247        let subgroup_id = match subgroup_id_mode {
248            0 => VarInt::from_u64_moqt(0),
249            2 => VarInt::decode_moqt::<Wire>(buf)?,
250            // Mode 1: the Subgroup ID is the first object's Object ID, which is
251            // not in the header. Stored as 0 and resolved by whoever reads the
252            // first object; [`Self::subgroup_id_mode`] is what tells a caller
253            // the stored value is a placeholder. Mode 3 cannot arrive here —
254            // `subgroup_type_is_valid` refused it above.
255            _ => VarInt::from_u64_moqt(0),
256        };
257
258        let publisher_priority = if header_type & SUBGROUP_DEFAULT_PRIORITY_BIT == 0 {
259            if buf.remaining() < 1 {
260                return Err(CodecError::UnexpectedEnd);
261            }
262            Some(buf.get_u8())
263        } else {
264            None
265        };
266
267        Ok(SubgroupHeader { header_type, track_alias, group_id, subgroup_id, publisher_priority })
268    }
269
270    /// Serialize the header exactly as its type byte describes it.
271    ///
272    /// The type byte is taken as the authority on framing and is written
273    /// through unexamined, which is what makes this infallible. A value built
274    /// by hand can therefore name a type draft-17 forbids — the reserved
275    /// SUBGROUP_ID_MODE `0b11` above all — and produce a stream the receiver
276    /// must answer with a PROTOCOL_VIOLATION. Prefer [`Self::encode_checked`],
277    /// which refuses such a header instead of emitting it.
278    pub fn encode(&self, buf: &mut impl BufMut) {
279        buf.put_u8(self.header_type);
280        self.track_alias.encode_moqt::<Wire>(buf);
281        self.group_id.encode_moqt::<Wire>(buf);
282
283        let subgroup_id_mode = (self.header_type & SUBGROUP_ID_MODE_MASK) >> 1;
284        if subgroup_id_mode == 2 {
285            self.subgroup_id.encode_moqt::<Wire>(buf);
286        }
287
288        if self.header_type & SUBGROUP_DEFAULT_PRIORITY_BIT == 0 {
289            buf.put_u8(self.publisher_priority.unwrap_or(128));
290        }
291    }
292
293    /// Serialize the header, refusing a type value draft-17 does not define.
294    ///
295    /// The accepted set is the one [`Self::decode`] accepts, so bytes this
296    /// writes always parse back through this module rather than being refused
297    /// by the peer. Section 10.4.2 lists the excluded values under "The
298    /// following Type values are invalid", and a receiver that follows it
299    /// closes the session rather than reading the stream, so writing one loses
300    /// the whole session and not merely the stream.
301    ///
302    /// Errors with [`CodecError::InvalidField`] before any byte is written, so
303    /// a refused header leaves `buf` untouched rather than half a header the
304    /// next write would run into.
305    pub fn encode_checked(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
306        if !subgroup_type_is_valid(self.header_type as u64) {
307            return Err(stream_type_error(self.header_type as u64));
308        }
309        self.encode(buf);
310        Ok(())
311    }
312
313    pub fn has_properties(&self) -> bool {
314        self.header_type & SUBGROUP_PROPERTIES_BIT != 0
315    }
316
317    /// The subgroup-ID mode: `(header_type & 0x06) >> 1`.
318    ///
319    /// `0` = no subgroup ID on the wire and it is zero; `1` = the subgroup ID
320    /// is the first object's ID; `2` = an explicit ID follows the Group ID;
321    /// `3` = reserved. Exposed because the mask is module-private and
322    /// `dispatch::AnySubgroupHeader::subgroup_id_mode` cannot read it.
323    ///
324    /// A decoded header never reports `3`: draft-17 Section 10.4.2 lists every
325    /// type value carrying that mode as invalid and [`Self::decode`] refuses
326    /// them. It remains reachable on a header built by hand, which is what
327    /// [`Self::encode_checked`] exists to catch.
328    pub fn subgroup_id_mode(&self) -> u8 {
329        (self.header_type & SUBGROUP_ID_MODE_MASK) >> 1
330    }
331
332    pub fn is_end_of_group(&self) -> bool {
333        self.header_type & SUBGROUP_END_OF_GROUP_BIT != 0
334    }
335}
336
337// ── Subgroup objects (stateful) ───────────────────────────────
338
339/// One object within a draft-17 subgroup stream. Object IDs are
340/// delta-encoded and the presence of a "properties" block (the
341/// draft-17 equivalent of extension headers) is determined by the
342/// PROPERTIES bit on the enclosing [`SubgroupHeader`]. Use
343/// [`SubgroupObjectReader`] to encode/decode.
344#[derive(Debug, Clone)]
345pub struct SubgroupObject {
346    pub object_id: VarInt,
347    /// Raw properties bytes, excluding the byte-length prefix that precedes
348    /// them on the wire. Empty unless the subgroup header sets the
349    /// PROPERTIES bit, or when the block is present but zero-length.
350    /// Opaque: [`SubgroupObjectReader::write_object`] re-emits the prefix
351    /// and these bytes verbatim.
352    pub extension_headers: Vec<u8>,
353    pub payload_length: VarInt,
354    /// The object's status, carried on the wire only when `payload_length` is
355    /// zero: a zero-length object encodes a status code in place of its
356    /// payload. `None` with a zero `payload_length` is written as
357    /// [`ObjectStatus::Normal`], the status draft-17 Section 10.2.1.1 gives an
358    /// empty object.
359    ///
360    /// Typed rather than a raw code. The wire field is a varint with room for
361    /// any value, and draft-17 assigns three of them; the decoder refuses the
362    /// rest, and this type is that same refusal on the encode side — 0x1 and
363    /// 0x2 cannot be named here, so [`SubgroupObjectReader::write_object`]
364    /// cannot emit a status this module's own decoder would reject.
365    pub object_status: Option<ObjectStatus>,
366    pub payload: Vec<u8>,
367}
368
369impl SubgroupObject {
370    /// The object's status, with the one draft-17's encoding elides filled in.
371    ///
372    /// A subgroup object states its status only when its Object Payload Length
373    /// is zero. An object that carries bytes therefore has no status field, and
374    /// its status is [`ObjectStatus::Normal`] — the only status draft-17
375    /// Section 10.2.1.1 permits a payload, so the only one such an object could
376    /// have had.
377    pub fn status(&self) -> ObjectStatus {
378        self.object_status.unwrap_or(ObjectStatus::Normal)
379    }
380
381    /// Whether this object's status is allowed to carry the properties it has.
382    ///
383    /// Draft-17 Section 10.2.1.2: "Any Object with status Normal can have
384    /// properties (Section 2.5). If an endpoint receives properties on an Object with status
385    /// that is not Normal, it MUST close the session with a
386    /// PROTOCOL_VIOLATION."
387    ///
388    /// So this is `false` for exactly one shape: a non-empty properties block
389    /// on an object whose status is not [`ObjectStatus::Normal`]. An object
390    /// with no properties is fine at any status, and an object at Normal may
391    /// carry any properties. A zero-length block is "no properties" here and
392    /// not a violation — Section 10.4.2 requires it of an object on a
393    /// PROPERTIES subgroup stream that has none: "Objects with no properties
394    /// set Properties Length to 0."
395    ///
396    /// Neither [`SubgroupObjectReader::read_object`] nor
397    /// [`SubgroupObjectReader::write_object`] applies this itself, which is a
398    /// deliberate contrast with the payload rule beside it. A status next to a
399    /// payload has no encoding — the two share a position on the wire — so the
400    /// writer refuses it as unrepresentable. Properties next to a status encode
401    /// fine; the frame is well formed and merely non-conforming, and a codec
402    /// that could not read or write it could not reproduce a capture containing
403    /// one. The rule addresses an endpoint receiving such an Object, so the
404    /// endpoint is where it is enforced, and this is what it asks.
405    ///
406    /// The datagram carrier is the exception, and the draft is what makes it
407    /// one: Section 10.3.1 states the same rule again as a per-datagram
408    /// framing rule, so [`DatagramHeader::decode`] refuses it outright.
409    pub fn properties_permitted(&self) -> bool {
410        self.extension_headers.is_empty() || self.status() == ObjectStatus::Normal
411    }
412}
413
414/// The framing of one draft-17 subgroup object, without its payload.
415///
416/// Produced by [`SubgroupObjectReader::read_object_meta`] for callers that
417/// forward an object's bytes verbatim and never inspect the payload.
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
419pub struct SubgroupObjectMeta {
420    /// Resolved absolute Object ID.
421    pub object_id: u64,
422    /// Byte length of the properties block's contents, excluding its length
423    /// prefix.
424    pub extension_headers_len: u64,
425    /// Declared payload length. Zero when `status` is `Some`.
426    pub payload_length: u64,
427    /// Object status wire code, present only when the payload is empty.
428    pub status: Option<u64>,
429    /// Total bytes this object occupies on the wire, prefix fields included.
430    pub wire_len: u64,
431}
432
433impl SubgroupObjectMeta {
434    /// Whether this object's stated status allows it a non-empty payload.
435    ///
436    /// `None` when the object states no status at all. On a subgroup stream
437    /// that is every object that carries bytes: draft-17 Section 10.4.2 puts
438    /// the status field on the wire only when the Object Payload Length is
439    /// zero, and Section 10.2.1.1 says Normal "is implicit for any non-zero
440    /// length object". An absent status is therefore not an unknown one — it is
441    /// Normal, spelled by the payload's own presence — but it is not a *stated*
442    /// permission, and this method reports only what the object states.
443    ///
444    /// `Some` otherwise, reading draft-17 Section 10.2.1.1's rule off the code:
445    /// "Any object with a status code other than zero MUST have an empty
446    /// payload." Zero permits, everything else forbids, which holds for codes
447    /// beyond the three the draft assigns as well — the rule is arithmetic, not
448    /// a lookup, so it does not go stale if a later draft assigns more.
449    ///
450    /// Worth having even though a stated status here always sits beside a zero
451    /// payload length, because the framing is not the destination. A caller
452    /// forwarding this object onto a datagram, where the payload is whatever
453    /// follows the header rather than a counted field, needs to know the object
454    /// may not be given bytes there either; the length it read on this stream
455    /// says nothing about that.
456    pub fn payload_permission(&self) -> Option<PayloadPermission> {
457        self.status.map(|code| {
458            if code == ObjectStatus::Normal.as_u64() {
459                PayloadPermission::Permitted
460            } else {
461                PayloadPermission::Forbidden
462            }
463        })
464    }
465}
466
467#[derive(Debug, Clone)]
468pub struct SubgroupObjectReader {
469    extensions_present: bool,
470    prev_object_id: Option<u64>,
471}
472
473impl SubgroupObjectReader {
474    pub fn new(header: &SubgroupHeader) -> Self {
475        Self { extensions_present: header.has_properties(), prev_object_id: None }
476    }
477
478    pub fn read_object(&mut self, buf: &mut impl Buf) -> Result<SubgroupObject, CodecError> {
479        let delta = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
480        let object_id_val = match self.prev_object_id {
481            None => delta,
482            Some(prev) => prev
483                .checked_add(1)
484                .and_then(|v| v.checked_add(delta))
485                .ok_or(CodecError::ObjectIdOverflow(prev, delta))?,
486        };
487        self.prev_object_id = Some(object_id_val);
488        let object_id = VarInt::from_u64(object_id_val).map_err(|_| CodecError::InvalidField)?;
489
490        // The properties block is a byte-length-prefixed opaque blob. We
491        // copy the blob verbatim; callers that want structured properties
492        // can parse the returned bytes.
493        let extension_headers = if self.extensions_present {
494            let ext_len = VarInt::decode_moqt::<Wire>(buf)?.into_inner() as usize;
495            crate::types::read_bytes(buf, ext_len)?
496        } else {
497            Vec::new()
498        };
499
500        let payload_length_vi = VarInt::decode_moqt::<Wire>(buf)?;
501        let payload_length_val = payload_length_vi.into_inner() as usize;
502        let (object_status, payload) = if payload_length_val == 0 {
503            let status = VarInt::decode_moqt::<Wire>(buf)?;
504            (Some(decoded_status(status.into_inner())?), Vec::new())
505        } else {
506            let payload = crate::types::read_bytes(buf, payload_length_val)?;
507            (None, payload)
508        };
509
510        Ok(SubgroupObject {
511            object_id,
512            extension_headers,
513            payload_length: payload_length_vi,
514            object_status,
515            payload,
516        })
517    }
518
519    /// Decode the next object's framing without copying its payload.
520    ///
521    /// Consumes exactly the bytes [`Self::read_object`] consumes and leaves
522    /// the same delta state behind, so the two are interchangeable on a
523    /// given stream.
524    pub fn read_object_meta(
525        &mut self,
526        buf: &mut impl Buf,
527    ) -> Result<SubgroupObjectMeta, CodecError> {
528        let start = buf.remaining();
529        let delta = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
530        let object_id_val = match self.prev_object_id {
531            None => delta,
532            Some(prev) => prev
533                .checked_add(1)
534                .and_then(|v| v.checked_add(delta))
535                .ok_or(CodecError::ObjectIdOverflow(prev, delta))?,
536        };
537        self.prev_object_id = Some(object_id_val);
538        let object_id =
539            VarInt::from_u64(object_id_val).map_err(|_| CodecError::InvalidField)?.into_inner();
540
541        let extension_headers_len = if self.extensions_present {
542            let ext_len = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
543            skip(buf, ext_len)?;
544            ext_len
545        } else {
546            0
547        };
548
549        let payload_length = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
550        let status = if payload_length == 0 {
551            let code = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
552            Some(decoded_status(code)?.as_u64())
553        } else {
554            skip(buf, payload_length)?;
555            None
556        };
557
558        Ok(SubgroupObjectMeta {
559            object_id,
560            extension_headers_len,
561            payload_length,
562            status,
563            wire_len: (start - buf.remaining()) as u64,
564        })
565    }
566
567    /// Serialize an object, producing the correct delta encoding.
568    ///
569    /// A zero `payload_length` writes the object's status, defaulting to
570    /// [`ObjectStatus::Normal`] when it is `None`. The status is typed, so
571    /// every value that can reach this method is one draft-17 assigns and one
572    /// [`Self::read_object`] accepts; there is no status-related error to
573    /// return.
574    ///
575    /// Errors with [`CodecError::InvalidField`] when `object.object_id` is
576    /// not strictly greater than the previously written object's ID, since
577    /// no valid delta exists for that case.
578    ///
579    /// Errors with [`CodecError::InvalidField`] when `payload_length` is not
580    /// exactly `payload.len()`. The declared length is written ahead of the
581    /// payload, so a mismatch is a frame [`Self::read_object`] cannot parse
582    /// and one no caller could fix by appending bytes.
583    pub fn write_object(
584        &mut self,
585        object: &SubgroupObject,
586        buf: &mut impl BufMut,
587    ) -> Result<(), CodecError> {
588        // A declared length that disagrees with the payload framed under it
589        // produces bytes no reader can parse and no caller can repair: the
590        // length is already on the wire ahead of the payload. Checked before
591        // anything is written, so a refused object leaves `buf` untouched
592        // rather than half an object the next read would run into.
593        //
594        // Zero is not "an empty payload" here; it is the marker that puts a
595        // status code where the payload would go, so an object carrying bytes
596        // under it is asking for two framings at once.
597        let declared = object.payload_length.into_inner();
598        if declared != object.payload.len() as u64 {
599            return Err(CodecError::InvalidField);
600        }
601
602        let oid = object.object_id.into_inner();
603        let delta = match self.prev_object_id {
604            None => oid,
605            Some(prev) => oid
606                .checked_sub(prev)
607                .and_then(|v| v.checked_sub(1))
608                .ok_or(CodecError::InvalidField)?,
609        };
610        VarInt::from_u64(delta).map_err(|_| CodecError::InvalidField)?.encode_moqt::<Wire>(buf);
611        if self.extensions_present {
612            VarInt::from_u64(object.extension_headers.len() as u64)
613                .map_err(|_| CodecError::InvalidField)?
614                .encode_moqt::<Wire>(buf);
615            buf.put_slice(&object.extension_headers);
616        }
617        object.payload_length.encode_moqt::<Wire>(buf);
618        if object.payload_length.into_inner() == 0 {
619            let status = object.object_status.unwrap_or(ObjectStatus::Normal);
620            VarInt::from_u64_moqt(status.as_u64()).encode_moqt::<Wire>(buf);
621        } else {
622            buf.put_slice(&object.payload);
623        }
624        self.prev_object_id = Some(oid);
625        Ok(())
626    }
627}
628
629// ── Datagram ──────────────────────────────────────────────────
630
631const DATAGRAM_PROPERTIES_BIT: u8 = 0x01;
632const DATAGRAM_END_OF_GROUP_BIT: u8 = 0x02;
633const DATAGRAM_ZERO_OBJECT_ID_BIT: u8 = 0x04;
634const DATAGRAM_DEFAULT_PRIORITY_BIT: u8 = 0x08;
635const DATAGRAM_STATUS_BIT: u8 = 0x20;
636/// The bits a datagram type must fix: 7, 6 and 4 all clear. That is the form
637/// `0b00X0XXXX` written as a mask, leaving bit 5 and the low nibble free.
638const DATAGRAM_FORM_MASK: u8 = 0xD0;
639
640/// Whether `datagram_type` is one of the datagram types draft-17 defines.
641///
642/// Section 10.3.1 gives the field as
643/// `Type (i) = 0x00..0x0F / 0x20..0x21 / 0x24..0x25 / 0x28..0x29 / 0x2C..0x2D`
644/// and then names the two ways a byte falls outside it, each of which "MUST
645/// close the session with a PROTOCOL_VIOLATION":
646///
647/// - "Type values with both the STATUS bit (0x20) and END_OF_GROUP bit (0x02)
648///   set: 0x22, 0x23, 0x26, 0x27, 0x2A, 0x2B, 0x2E, 0x2F. An object status
649///   message cannot signal end of group."
650/// - "Type values that do not match the form 0b00X0XXXX (i.e., Type values
651///   outside the ranges 0x00..0x0F and 0x20..0x2F)."
652///
653/// The two conditions are checked here rather than the eight-value list, and
654/// they enumerate exactly the same bytes.
655///
656/// Bit 4 is what separates a datagram from a subgroup header: the subgroup form
657/// requires it set and this one requires it clear, so the same octet can never
658/// be read as both.
659fn datagram_type_is_valid(raw: u64) -> bool {
660    raw <= 0xFF && {
661        let t = raw as u8;
662        t & DATAGRAM_FORM_MASK == 0
663            && t & (DATAGRAM_STATUS_BIT | DATAGRAM_END_OF_GROUP_BIT)
664                != (DATAGRAM_STATUS_BIT | DATAGRAM_END_OF_GROUP_BIT)
665    }
666}
667
668/// Which failure a leading datagram Type that is not one a reader wants is.
669///
670/// The same two-rule split as `stream_type_error`, read against the datagram
671/// table. A Type outside the datagram form is one no table assigns, so it is
672/// [`CodecError::UnknownDatagramType`]. A Type inside the form that sets both
673/// STATUS and END_OF_GROUP is named by Section 10.3.1 and forbidden there — an
674/// object status message cannot also mark the end of a group — so it is
675/// [`CodecError::InvalidTypeValue`].
676///
677/// Nothing reaches the [`CodecError::InvalidField`] arm from a decoder: the
678/// stream Types all set bit 4 or exceed a byte, so they fail the form rather
679/// than passing it, and draft-17 defines no padding datagram for the datagram
680/// table to share.
681fn datagram_type_error(raw: u64) -> CodecError {
682    if datagram_type_is_valid(raw) {
683        CodecError::InvalidField
684    } else if raw <= 0xFF
685        && raw as u8 & DATAGRAM_FORM_MASK == 0
686        && raw as u8 & (DATAGRAM_STATUS_BIT | DATAGRAM_END_OF_GROUP_BIT)
687            == (DATAGRAM_STATUS_BIT | DATAGRAM_END_OF_GROUP_BIT)
688    {
689        CodecError::InvalidTypeValue {
690            raw,
691            detail: "it sets both the STATUS bit and the END_OF_GROUP bit",
692        }
693    } else {
694        CodecError::UnknownDatagramType(raw)
695    }
696}
697
698#[derive(Debug, Clone)]
699pub struct DatagramHeader {
700    pub datagram_type: u8,
701    pub track_alias: VarInt,
702    pub group_id: VarInt,
703    pub object_id: VarInt,
704    pub publisher_priority: Option<u8>,
705    /// Raw properties bytes, excluding the byte-length prefix that precedes
706    /// them on the wire. Present only when `datagram_type` sets the PROPERTIES
707    /// bit (0x01), and empty otherwise — the bit is what puts the block on the
708    /// wire, so contents held here with the bit clear are not written.
709    ///
710    /// Opaque: [`Self::encode`] re-emits the prefix and these bytes verbatim,
711    /// and [`Self::decode`] copies them out the same way, so a datagram can be
712    /// decoded and re-encoded without understanding what its properties mean.
713    /// The block sits between the publisher priority and the status field, so
714    /// leaving it out of the struct would put the status where the decoder
715    /// looks for the properties length.
716    pub properties: Vec<u8>,
717    /// The object's status, carried on the wire only when `datagram_type` sets
718    /// the STATUS bit (0x20): such a datagram holds a one-byte status code in
719    /// place of a payload. `None` with the bit set is written as
720    /// [`ObjectStatus::Normal`]; a status with the bit clear is not written at
721    /// all, because the bit is what puts the field on the wire.
722    ///
723    /// Typed rather than a bare byte. The wire field is one octet with 256
724    /// values, and draft-17 Section 10.2.1.1 assigns three of them; the
725    /// decoder refuses the other 253, and this type is that same refusal on
726    /// the encode side — [`Self::encode`] is infallible precisely because a
727    /// status it could not legally write cannot be built.
728    pub object_status: Option<ObjectStatus>,
729}
730
731impl DatagramHeader {
732    /// Decode the header and stop, leaving whatever follows it in `buf`.
733    ///
734    /// A datagram's payload has no length field — draft-17 Section 10.3.1:
735    /// "There is no explicit length field for the Object Payload; the entirety
736    /// of the transport datagram following the Object header contains the
737    /// payload." So the header alone cannot say how many bytes belong to the
738    /// object, and this method deliberately does not try: the caller holds the
739    /// transport datagram and the tail is theirs.
740    ///
741    /// That makes it the wrong entry point for validating the object as a
742    /// whole. Use [`Self::decode_object`] when `buf` holds exactly one
743    /// datagram; it consumes the tail and can therefore refuse a payload the
744    /// framing forbids.
745    pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
746        if let Some(err) = wide_type_refusal(buf, datagram_type_error)? {
747            return Err(err);
748        }
749        let raw = buf.get_u8() as u64;
750        if !datagram_type_is_valid(raw) {
751            return Err(datagram_type_error(raw));
752        }
753        let datagram_type = raw as u8;
754
755        let track_alias = VarInt::decode_moqt::<Wire>(buf)?;
756        let group_id = VarInt::decode_moqt::<Wire>(buf)?;
757
758        let object_id = if datagram_type & DATAGRAM_ZERO_OBJECT_ID_BIT != 0 {
759            VarInt::from_usize(0)
760        } else {
761            VarInt::decode_moqt::<Wire>(buf)?
762        };
763
764        let publisher_priority = if datagram_type & DATAGRAM_DEFAULT_PRIORITY_BIT == 0 {
765            if buf.remaining() < 1 {
766                return Err(CodecError::UnexpectedEnd);
767            }
768            Some(buf.get_u8())
769        } else {
770            None
771        };
772
773        let properties = if datagram_type & DATAGRAM_PROPERTIES_BIT != 0 {
774            let props_len = VarInt::decode_moqt::<Wire>(buf)?.into_inner() as usize;
775            crate::types::read_bytes(buf, props_len)?
776        } else {
777            Vec::new()
778        };
779
780        let object_status = if datagram_type & DATAGRAM_STATUS_BIT != 0 {
781            if buf.remaining() < 1 {
782                return Err(CodecError::UnexpectedEnd);
783            }
784            let status = buf.get_u8();
785            Some(decoded_status(status as u64)?)
786        } else {
787            None
788        };
789
790        // Two rules of draft-17 Section 10.3.1 reach the properties block just
791        // read, and neither is applied here — [`Self::properties_permitted`]
792        // and [`Self::properties_block_well_formed`] report them instead, and
793        // [`Self::encode_checked`] refuses to write either shape:
794        //
795        //   - "If an endpoint receives a datagram with the PROPERTIES bit set
796        //     and an Properties Length of 0, it MUST close the session with a
797        //     PROTOCOL_VIOLATION."
798        //   - "If an Object Datagram includes both the STATUS bit and
799        //     PROPERTIES bit, and the Object Status is not Normal (0x0), the
800        //     endpoint MUST close the session with a PROTOCOL_VIOLATION,
801        //     because only Normal Objects can have Properties."
802        //
803        // Both describe a datagram that is well framed and non-conforming: the
804        // fields are all where the layout puts them and every one of them
805        // parses, so a decoder can read the datagram back exactly as it
806        // arrived. Refusing here would leave this module unable to reproduce a
807        // capture containing one, and both rules address an endpoint receiving
808        // such a datagram, so the endpoint is where they are enforced.
809        //
810        // The Type rules above are the contrast, and the contrast is what
811        // decides it: an invalid Type names no layout at all, so reading on
812        // invents the fields behind it rather than reporting them.
813
814        Ok(DatagramHeader {
815            datagram_type,
816            track_alias,
817            group_id,
818            object_id,
819            publisher_priority,
820            properties,
821            object_status,
822        })
823    }
824
825    /// Decode one whole datagram: the header, then the payload that runs to the
826    /// end of `buf`.
827    ///
828    /// `buf` must hold exactly one transport datagram and nothing else, since
829    /// that boundary is the only thing that delimits the payload — draft-17
830    /// Section 10.3.1: "There is no explicit length field for the Object
831    /// Payload; the entirety of the transport datagram following the Object
832    /// header contains the payload."
833    ///
834    /// Which is why the refusal lives here and not in [`Self::decode`]. A
835    /// datagram whose type sets the STATUS bit has no payload at all — the same
836    /// section: "When set to 1, the Object Status field is present and there is
837    /// no Object Payload" — so trailing bytes after its status are not a short
838    /// payload or an odd one, they are bytes the frame does not define. A
839    /// decoder that stops at the header cannot see them, and a caller that
840    /// treats whatever is left as the payload hands the application content the
841    /// publisher never framed as content. That is the case this refuses.
842    ///
843    /// The same refusal covers a status the draft forbids a payload to: an
844    /// object marked End of Group or End of Track may not carry one, per
845    /// Section 10.2.1.1's "Any object with a status code other than zero MUST
846    /// have an empty payload".
847    ///
848    /// Errors with [`CodecError::PayloadNotPermitted`] when bytes remain and
849    /// the header forbids them, naming which of the two rules refused them.
850    ///
851    /// # The two Properties rules are enforced here too
852    ///
853    /// Section 10.3.1 states them of a receiving endpoint, and this is the
854    /// endpoint's read:
855    ///
856    /// * "If an endpoint receives a datagram with the PROPERTIES bit set and an
857    ///   Properties Length of 0, it MUST close the session with a
858    ///   PROTOCOL_VIOLATION." The bit and a zero length are two ways to spell
859    ///   *no properties* and a datagram may use only the first, because a
860    ///   datagram with none has a Type that says so and the block costs bytes
861    ///   the Type already saved. A subgroup stream says the opposite in
862    ///   Section 10.4.2 — there the PROPERTIES bit is fixed for the whole stream,
863    ///   so an object with no properties has nowhere else to say so and a
864    ///   zero-length block is the required spelling.
865    /// * "If an Object Datagram includes both the STATUS bit and PROPERTIES
866    ///   bit, and the Object Status is not Normal (0x0), the endpoint MUST close
867    ///   the session with a PROTOCOL_VIOLATION, because only Normal Objects can
868    ///   have Properties."
869    ///
870    /// Both errors are [`CodecError::InvalidField`].
871    ///
872    /// [`Self::decode`] does **not** apply them, and the split is deliberate.
873    /// Both describe a datagram that is well framed and non-conforming: every
874    /// field is where the layout puts it and every one of them parses, so the
875    /// header reads back exactly as it arrived and a tool reproducing a capture
876    /// can re-emit it. What it may not do is hand such a datagram to an
877    /// application as an ordinary Object, which is what this entry point would
878    /// be doing. [`Self::properties_block_well_formed`] and
879    /// [`Self::properties_permitted`] report the two for a caller that wants
880    /// the header without the judgement, and [`Self::encode_checked`] refuses
881    /// to write either shape.
882    pub fn decode_object(buf: &mut impl Buf) -> Result<(Self, Vec<u8>), CodecError> {
883        let header = Self::decode(buf)?;
884        if !header.properties_block_well_formed() || !header.properties_permitted() {
885            return Err(CodecError::InvalidField);
886        }
887        let payload = crate::types::read_bytes(buf, buf.remaining())?;
888        if !payload.is_empty() && !header.permits_payload() {
889            return Err(CodecError::PayloadNotPermitted {
890                status: header.status().as_u64(),
891                len: payload.len(),
892                detail: if header.has_status() {
893                    "its type states a status in place of a payload"
894                } else {
895                    "its status is registered as forbidding one"
896                },
897            });
898        }
899        Ok((header, payload))
900    }
901
902    /// Serialize the header, refusing a status the framing cannot carry.
903    ///
904    /// A datagram states a status only when its type byte sets the STATUS bit
905    /// (0x20). With the bit clear there is no status field on the wire, so an
906    /// `object_status` of anything but [`ObjectStatus::Normal`] has nowhere to
907    /// go: [`Self::encode`] drops it, and the datagram parses back as an
908    /// ordinary payload object. An End of Group marker written that way does
909    /// not arrive late or malformed — it does not arrive at all, and the
910    /// receiver sees a normal object in its place.
911    ///
912    /// Draft-17 Section 10.3.1 puts the framing side plainly — "The STATUS bit
913    /// (0x20) indicates whether the datagram contains an Object Status or
914    /// Object Payload" — and Section 10.2.1.1 the conformance side: "Any object
915    /// with a status code other than zero MUST have an empty payload." Between
916    /// them there is no datagram that carries a non-zero status and a payload,
917    /// so the pair being refused here is not one this encoder merely declines
918    /// to spell.
919    ///
920    /// [`ObjectStatus::Normal`] with the bit clear is not that case and is
921    /// accepted. It is the status the encoding elides for every datagram that
922    /// carries a payload, so stating it asks for exactly the bytes leaving it
923    /// out asks for, and nothing is lost.
924    ///
925    /// A type value Section 10.3.1 lists as invalid is refused here too, on the
926    /// same grounds: a receiver that follows the draft answers one with a
927    /// PROTOCOL_VIOLATION, so writing it costs the session and not merely the
928    /// datagram. The accepted set is the one [`Self::decode`] accepts.
929    ///
930    /// Errors with [`CodecError::InvalidField`] on either, before any byte is
931    /// written, so a refused header leaves `buf` untouched. The status half is
932    /// the datagram counterpart of the rule
933    /// [`SubgroupObjectReader::write_object`] applies on a subgroup stream,
934    /// where the status and the payload share a wire position.
935    pub fn encode_checked(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
936        if !datagram_type_is_valid(self.datagram_type as u64) {
937            return Err(datagram_type_error(self.datagram_type as u64));
938        }
939        if !self.has_status() && matches!(self.object_status, Some(s) if s != ObjectStatus::Normal)
940        {
941            return Err(CodecError::InvalidField);
942        }
943        // The two properties rules of Section 10.3.1. [`Self::decode`] reports
944        // both rather than refusing them, because the datagrams they describe
945        // are well framed and a codec that could not read one could not
946        // reproduce a capture containing it. Writing one is the other
947        // direction and has no such excuse: a conforming peer answers either
948        // with a PROTOCOL_VIOLATION, so emitting one costs the session and not
949        // merely the datagram.
950        if !self.properties_block_well_formed() || !self.properties_permitted() {
951            return Err(CodecError::InvalidField);
952        }
953        self.encode(buf);
954        Ok(())
955    }
956
957    /// Serialize the header exactly as its type byte describes it.
958    ///
959    /// Every field the type byte announces is written, in the order
960    /// [`Self::decode`] reads them, so the bytes this produces always parse
961    /// back. The properties block in particular has to be written here: it
962    /// sits ahead of the status field, and a datagram that skipped it would
963    /// offer the status byte where the decoder reads the block's length.
964    ///
965    /// The type byte is taken as the authority on framing, which is what makes
966    /// this infallible — and what makes it lossy when the struct disagrees with
967    /// itself. An `object_status` set while the type byte leaves the STATUS bit
968    /// clear is discarded here without a word. Prefer [`Self::encode_checked`],
969    /// which refuses that combination instead of resolving it.
970    pub fn encode(&self, buf: &mut impl BufMut) {
971        buf.put_u8(self.datagram_type);
972        self.track_alias.encode_moqt::<Wire>(buf);
973        self.group_id.encode_moqt::<Wire>(buf);
974
975        if self.datagram_type & DATAGRAM_ZERO_OBJECT_ID_BIT == 0 {
976            self.object_id.encode_moqt::<Wire>(buf);
977        }
978
979        if self.datagram_type & DATAGRAM_DEFAULT_PRIORITY_BIT == 0 {
980            buf.put_u8(self.publisher_priority.unwrap_or(128));
981        }
982
983        if self.datagram_type & DATAGRAM_PROPERTIES_BIT != 0 {
984            VarInt::from_usize(self.properties.len()).encode_moqt::<Wire>(buf);
985            buf.put_slice(&self.properties);
986        }
987
988        if self.datagram_type & DATAGRAM_STATUS_BIT != 0 {
989            buf.put_u8(self.object_status.unwrap_or(ObjectStatus::Normal).as_u8());
990        }
991    }
992
993    pub fn is_end_of_group(&self) -> bool {
994        self.datagram_type & DATAGRAM_END_OF_GROUP_BIT != 0
995    }
996
997    pub fn has_status(&self) -> bool {
998        self.datagram_type & DATAGRAM_STATUS_BIT != 0
999    }
1000
1001    /// `true` when the type byte sets the PROPERTIES bit (0x01), which is what
1002    /// puts the properties block on the wire.
1003    ///
1004    /// Reports the framing, not the contents. A decoded datagram with this set
1005    /// always has a non-empty [`Self::properties`], because
1006    /// [`Self::decode`] refuses a zero-length block; a header built by hand can
1007    /// hold the two apart, and [`Self::encode_checked`] is what refuses that.
1008    pub fn has_properties(&self) -> bool {
1009        self.datagram_type & DATAGRAM_PROPERTIES_BIT != 0
1010    }
1011
1012    /// The object's status, with the one the encoding elides filled in.
1013    ///
1014    /// A datagram states a status only when its type sets the STATUS bit, and
1015    /// such a datagram has no payload. One without the bit is all payload, and
1016    /// the status of an object that carries a payload is
1017    /// [`ObjectStatus::Normal`] — draft-17 Section 10.2.1.1: "Any object with a
1018    /// status code other than zero MUST have an empty payload."
1019    pub fn status(&self) -> ObjectStatus {
1020        self.object_status.unwrap_or(ObjectStatus::Normal)
1021    }
1022
1023    /// Whether this datagram's status is allowed to carry the properties it
1024    /// has.
1025    ///
1026    /// The same rule the subgroup form obeys. Draft-17 Section 10.3.1 builds
1027    /// the datagram's Properties field out of "the Object Properties structure
1028    /// defined in Section 10.2.1.2", and that section is where the general rule
1029    /// sits: "If an endpoint receives properties on an Object with status that
1030    /// is not Normal, it MUST close the session with a PROTOCOL_VIOLATION."
1031    /// Section 10.3.1 then states it again for this carrier in terms of the two
1032    /// bits, which is why [`Self::decode`] refuses the shape rather than merely
1033    /// reporting it — see [`SubgroupObject::properties_permitted`] for why the
1034    /// subgroup carrier is the other way round.
1035    pub fn properties_permitted(&self) -> bool {
1036        self.properties.is_empty() || self.status() == ObjectStatus::Normal
1037    }
1038
1039    /// Whether the properties block is framed the way a datagram may frame it.
1040    ///
1041    /// Draft-17 Section 10.3.1: "If an endpoint receives a datagram with the
1042    /// PROPERTIES bit set and an Properties Length of 0, it MUST close the
1043    /// session with a PROTOCOL_VIOLATION."
1044    ///
1045    /// The bit and a zero length are two ways to spell "no properties", and on
1046    /// a datagram they are not interchangeable: a datagram with none has a type
1047    /// byte that says so, and the block costs bytes the type byte already
1048    /// saved. This rule is the datagram's alone. A subgroup stream says the
1049    /// opposite in Section 10.4.2 — "Objects with no properties set Properties
1050    /// Length to 0" — because there the PROPERTIES bit is fixed for the whole
1051    /// stream, so an object with no properties has nowhere else to say so and a
1052    /// zero-length block is the required spelling rather than a violation.
1053    ///
1054    /// The mirror case is not a wire state but is a state this struct can hold:
1055    /// properties with the bit clear. [`Self::encode`] drops them without a
1056    /// word, so this reports that too, and [`Self::encode_checked`] refuses
1057    /// both.
1058    pub fn properties_block_well_formed(&self) -> bool {
1059        self.has_properties() != self.properties.is_empty()
1060    }
1061
1062    /// Whether the bytes after this datagram's header are allowed to exist.
1063    ///
1064    /// Two independent rules forbid them, and this reports both:
1065    ///
1066    /// - The framing. Draft-17 Section 10.3.1: "The STATUS bit (0x20) indicates
1067    ///   whether the datagram contains an Object Status or Object Payload. When
1068    ///   set to 1, the Object Status field is present and there is no Object
1069    ///   Payload." A datagram that states a status has no payload field at all,
1070    ///   whichever status it states — so a STATUS datagram carrying the Normal
1071    ///   code 0x0 has no more room for bytes than one carrying End of Group.
1072    /// - The status. Section 10.2.1.1: "Any object with a status code other than
1073    ///   zero MUST have an empty payload." This one reaches a datagram whose
1074    ///   type byte leaves the STATUS bit clear while the value claims a
1075    ///   non-Normal status — a disagreement [`Self::encode_checked`] refuses to
1076    ///   write, and one a decoded header never shows.
1077    ///
1078    /// The first is the rule a decoded datagram can actually trip, and reading
1079    /// the status alone misses it: `Some(ObjectStatus::Normal)` under a type
1080    /// byte with the STATUS bit set is exactly the case where the payload the
1081    /// draft says does not exist would otherwise be handed to the application
1082    /// as the object's content.
1083    ///
1084    /// Distinct from [`Self::has_status`], which reports how the datagram is
1085    /// framed rather than whether a payload may follow. A caller holding the
1086    /// bytes after the header wants this one; [`Self::decode_object`] applies it
1087    /// for a caller who would rather the decode simply fail.
1088    pub fn permits_payload(&self) -> bool {
1089        if self.has_status() {
1090            return false;
1091        }
1092        match self.object_status {
1093            None => true,
1094            Some(status) => status == ObjectStatus::Normal,
1095        }
1096    }
1097}
1098
1099// ── Fetch Header ──────────────────────────────────────────────
1100
1101const FETCH_STREAM_TYPE: u64 = 0x05;
1102
1103#[derive(Debug, Clone)]
1104pub struct FetchHeader {
1105    pub request_id: VarInt,
1106}
1107
1108impl FetchHeader {
1109    pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
1110        let stream_type = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
1111        if stream_type != FETCH_STREAM_TYPE {
1112            return Err(stream_type_error(stream_type));
1113        }
1114        let request_id = VarInt::decode_moqt::<Wire>(buf)?;
1115        Ok(FetchHeader { request_id })
1116    }
1117
1118    pub fn encode(&self, buf: &mut impl BufMut) {
1119        VarInt::from_usize(FETCH_STREAM_TYPE as usize).encode_moqt::<Wire>(buf);
1120        self.request_id.encode_moqt::<Wire>(buf);
1121    }
1122}
1123
1124// ── Fetch objects ─────────────────────────────────────────────
1125
1126/// SUBGROUP mode, the two least significant Serialization Flags bits
1127/// (draft-17 Section 10.4.4.1, Table 7).
1128const FETCH_SUBGROUP_MODE_MASK: u64 = 0x03;
1129/// SUBGROUP mode `0x01`: the Subgroup ID is the prior object's. Mode `0x00`,
1130/// the remaining value, fixes the Subgroup ID at zero and needs no constant —
1131/// nothing tests for it.
1132const FETCH_SUBGROUP_MODE_PRIOR: u64 = 0x01;
1133/// SUBGROUP mode `0x02`: the Subgroup ID is the prior object's plus one.
1134const FETCH_SUBGROUP_MODE_PRIOR_PLUS_ONE: u64 = 0x02;
1135/// SUBGROUP mode `0x03`: an explicit Subgroup ID field is present.
1136const FETCH_SUBGROUP_MODE_EXPLICIT: u64 = 0x03;
1137/// Object ID field present; clear means the prior object's ID plus one.
1138const FETCH_OBJECT_ID_BIT: u64 = 0x04;
1139/// Group ID field present; clear means the prior object's Group ID.
1140const FETCH_GROUP_ID_BIT: u64 = 0x08;
1141/// Priority field present; clear means the prior object's priority.
1142const FETCH_PRIORITY_BIT: u64 = 0x10;
1143/// Properties field present.
1144const FETCH_PROPERTIES_BIT: u64 = 0x20;
1145/// The object was forwarded as a datagram and has no Subgroup ID; the two
1146/// least significant bits are to be ignored.
1147const FETCH_DATAGRAM_BIT: u64 = 0x40;
1148/// The largest Serialization Flags value read as a set of bits. Draft-17
1149/// Section 10.4.4: "When less than 128, the bits represent flags described
1150/// below."
1151const FETCH_FLAGS_BIT_FORM_MAX: u64 = 0x7f;
1152/// Serialization Flags `0x8C`, End of Non-Existent Range.
1153const FETCH_END_OF_NON_EXISTENT_RANGE: u64 = 0x8c;
1154/// Serialization Flags `0x10C`, End of Unknown Range.
1155const FETCH_END_OF_UNKNOWN_RANGE: u64 = 0x10c;
1156
1157/// The two Serialization Flags values that mark a range of Objects rather than
1158/// carrying one, from draft-17 Section 10.4.4 Table 6 and Section 10.4.4.2.
1159///
1160/// Both state a Group ID and an Object ID and nothing else, and both mean the
1161/// same thing about the span from the last serialized Object to that Location
1162/// inclusive: it will not be serialized. They differ only in what the publisher
1163/// claims to know about it.
1164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1165pub enum EndOfRange {
1166    /// `0x8C`. The Objects in the span "do not exist".
1167    NonExistent,
1168    /// `0x10C`. The Objects in the span "are unknown".
1169    Unknown,
1170}
1171
1172/// One object on a draft-17 fetch stream, without its payload.
1173///
1174/// Draft-17 Section 10.4.4, Figure 27:
1175///
1176/// ```text
1177/// {
1178///   Serialization Flags (vi64),
1179///   [Group ID (vi64),]
1180///   [Subgroup ID (vi64),]
1181///   [Object ID (vi64),]
1182///   [Publisher Priority (8),]
1183///   [Properties (..),]
1184///   Object Payload Length (vi64),
1185///   [Object Payload (..),]
1186/// }
1187/// ```
1188///
1189/// # Why the fields are optional
1190///
1191/// Every bracketed field above is one the Serialization Flags may leave off the
1192/// wire, and leaving it off does not mean the object lacks it — Table 8 gives
1193/// each absent field a meaning drawn from the object before it on the stream
1194/// ("Object ID is the prior Object's ID plus one", "Group ID is the prior
1195/// Object's Group ID", "Priority is the prior Object's Priority"), and Table 7
1196/// does the same for the Subgroup ID. A single object's bytes therefore do not
1197/// determine its Location; only the run of objects before it does.
1198///
1199/// So this type reports presence rather than inventing a value: `None` means
1200/// *the wire did not say*, and resolving it is the job of a caller that has
1201/// been following the stream. [`Self::references_prior_object`] is how such a
1202/// caller learns whether resolution is even needed, and it is what makes the
1203/// draft's rule about the first object checkable: "If the first Object in the
1204/// FETCH response uses a flag that references fields in the prior Object, the
1205/// Subscriber MUST close the session with a PROTOCOL_VIOLATION."
1206///
1207/// # What draft-17 changed
1208///
1209/// Through draft-13 a fetch object spelled out Group ID, Subgroup ID, Object ID
1210/// and Publisher Priority on every object and carried an Object Status beside a
1211/// zero-length payload. Draft-17 has neither habit: the flags replace the four
1212/// unconditional fields, and there is no status field at all — Section 10.2.1.1
1213/// says the Object Status "is only present in objects that are delivered via a
1214/// SUBSCRIPTION, and is absent in Objects delivered via a FETCH". A zero
1215/// `payload_length` here is simply an object with no bytes.
1216#[derive(Debug, Clone, PartialEq, Eq)]
1217pub struct FetchObjectHeader {
1218    /// The Serialization Flags varint, verbatim.
1219    ///
1220    /// Kept whole rather than split into the fields below because it says more
1221    /// than which fields are present: the two low bits pick between four
1222    /// Subgroup ID meanings that share one absent field, and the values `0x8C`
1223    /// and `0x10C` are not bit patterns at all. It is also the authority
1224    /// [`Self::encode`] writes from.
1225    pub serialization_flags: VarInt,
1226    /// Group ID, when the flags put it on the wire.
1227    pub group_id: Option<VarInt>,
1228    /// Subgroup ID, present only under SUBGROUP mode `0x03`. The other three
1229    /// modes leave it off the wire with a meaning of their own, which
1230    /// [`Self::subgroup_id_mode`] reports.
1231    pub subgroup_id: Option<VarInt>,
1232    /// Object ID, when the flags put it on the wire.
1233    pub object_id: Option<VarInt>,
1234    /// Publisher priority, when the flags put it on the wire.
1235    pub publisher_priority: Option<u8>,
1236    /// Raw properties bytes, excluding the byte-length prefix that precedes
1237    /// them on the wire. Present only when the flags set the PROPERTIES bit
1238    /// (0x20), and empty otherwise — the bit is what puts the block on the
1239    /// wire, so contents held here with the bit clear are not written.
1240    ///
1241    /// Opaque, exactly as [`DatagramHeader::properties`] is: the prefix and
1242    /// these bytes are re-emitted verbatim.
1243    pub properties: Vec<u8>,
1244    /// Declared byte length of the payload that follows this header.
1245    pub payload_length: VarInt,
1246}
1247
1248impl FetchObjectHeader {
1249    /// Decode one fetch object's framing, stopping after the Object Payload
1250    /// Length.
1251    ///
1252    /// The payload itself is left in `buf` — `payload_length` says how many
1253    /// bytes of it there are, and a caller streaming objects usually wants to
1254    /// forward or skip them rather than copy them.
1255    ///
1256    /// Errors with [`CodecError::InvalidField`] on a Serialization Flags value
1257    /// draft-17 does not define. Section 10.4.4 reads the value as a bit set
1258    /// only "when less than 128", names `0x8C` and `0x10C` as the two larger
1259    /// values that mean anything, and then says of the rest: "Any other value is
1260    /// a PROTOCOL_VIOLATION."
1261    pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
1262        let serialization_flags = VarInt::decode_moqt::<Wire>(buf)?;
1263        let flags = serialization_flags.into_inner();
1264        if !fetch_flags_are_defined(flags) {
1265            return Err(CodecError::InvalidField);
1266        }
1267
1268        // Read in the order Figure 27 lists them. The presence tests are the
1269        // ones the accessors below use, applied to the flags just read rather
1270        // than to a half-built value.
1271        let group_id =
1272            if fetch_has_group_id(flags) { Some(VarInt::decode_moqt::<Wire>(buf)?) } else { None };
1273        let subgroup_id = if fetch_has_subgroup_id(flags) {
1274            Some(VarInt::decode_moqt::<Wire>(buf)?)
1275        } else {
1276            None
1277        };
1278        let object_id =
1279            if fetch_has_object_id(flags) { Some(VarInt::decode_moqt::<Wire>(buf)?) } else { None };
1280        let publisher_priority = if fetch_has_priority(flags) {
1281            if buf.remaining() < 1 {
1282                return Err(CodecError::UnexpectedEnd);
1283            }
1284            Some(buf.get_u8())
1285        } else {
1286            None
1287        };
1288        let properties = if fetch_has_properties(flags) {
1289            let props_len = VarInt::decode_moqt::<Wire>(buf)?.into_inner() as usize;
1290            crate::types::read_bytes(buf, props_len)?
1291        } else {
1292            Vec::new()
1293        };
1294        let payload_length = VarInt::decode_moqt::<Wire>(buf)?;
1295
1296        Ok(FetchObjectHeader {
1297            serialization_flags,
1298            group_id,
1299            subgroup_id,
1300            object_id,
1301            publisher_priority,
1302            properties,
1303            payload_length,
1304        })
1305    }
1306
1307    /// Serialize the framing, stopping after the Object Payload Length.
1308    ///
1309    /// The payload is the caller's to append, matching [`Self::decode`] leaving
1310    /// it in the buffer.
1311    ///
1312    /// Fallible, unlike the other encoders here, because the flags and the
1313    /// fields can disagree in a way no default resolves. [`DatagramHeader`] can
1314    /// write a priority the type byte demands and the value omits, because
1315    /// draft-17 has a default priority to write; a Group ID the flags demand and
1316    /// the value omits has no such stand-in — every candidate is a Location this
1317    /// object does not have. Writing nothing there instead would slide the next
1318    /// field into its place and desynchronize the whole stream, which no reader
1319    /// downstream could detect, let alone repair.
1320    ///
1321    /// Errors with [`CodecError::InvalidField`], before any byte is written, on:
1322    ///
1323    /// - a Serialization Flags value draft-17 does not define, the set
1324    ///   [`Self::decode`] refuses;
1325    /// - a field the flags announce and the value leaves `None`;
1326    /// - a field the value supplies and the flags do not announce, including
1327    ///   non-empty `properties` with the PROPERTIES bit clear — silently
1328    ///   dropping it would emit an object stripped of metadata that the caller
1329    ///   believes it sent.
1330    pub fn encode(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
1331        let flags = self.serialization_flags.into_inner();
1332        if !fetch_flags_are_defined(flags) {
1333            return Err(CodecError::InvalidField);
1334        }
1335        if fetch_has_group_id(flags) != self.group_id.is_some()
1336            || fetch_has_subgroup_id(flags) != self.subgroup_id.is_some()
1337            || fetch_has_object_id(flags) != self.object_id.is_some()
1338            || fetch_has_priority(flags) != self.publisher_priority.is_some()
1339            || (!fetch_has_properties(flags) && !self.properties.is_empty())
1340        {
1341            return Err(CodecError::InvalidField);
1342        }
1343
1344        self.serialization_flags.encode_moqt::<Wire>(buf);
1345        if let Some(group_id) = self.group_id {
1346            group_id.encode_moqt::<Wire>(buf);
1347        }
1348        if let Some(subgroup_id) = self.subgroup_id {
1349            subgroup_id.encode_moqt::<Wire>(buf);
1350        }
1351        if let Some(object_id) = self.object_id {
1352            object_id.encode_moqt::<Wire>(buf);
1353        }
1354        if let Some(priority) = self.publisher_priority {
1355            buf.put_u8(priority);
1356        }
1357        if fetch_has_properties(flags) {
1358            VarInt::from_usize(self.properties.len()).encode_moqt::<Wire>(buf);
1359            buf.put_slice(&self.properties);
1360        }
1361        self.payload_length.encode_moqt::<Wire>(buf);
1362        Ok(())
1363    }
1364
1365    /// The end-of-range marker this object is, if it is one.
1366    ///
1367    /// `None` for every flags value below 128, which is every object that
1368    /// carries content.
1369    pub fn end_of_range(&self) -> Option<EndOfRange> {
1370        match self.serialization_flags.into_inner() {
1371            FETCH_END_OF_NON_EXISTENT_RANGE => Some(EndOfRange::NonExistent),
1372            FETCH_END_OF_UNKNOWN_RANGE => Some(EndOfRange::Unknown),
1373            _ => None,
1374        }
1375    }
1376
1377    /// The SUBGROUP mode: `serialization_flags & 0x03`, per draft-17
1378    /// Section 10.4.4.1 Table 7.
1379    ///
1380    /// `0x00` = the Subgroup ID is zero; `0x01` = it is the prior object's;
1381    /// `0x02` = it is the prior object's plus one; `0x03` = it is present in
1382    /// [`Self::subgroup_id`].
1383    ///
1384    /// Meaningless when [`Self::is_datagram`] holds — Section 10.4.4.1 says of
1385    /// the DATAGRAM bit that the subscriber "MUST ignore the bits" — and this
1386    /// reports the raw two bits regardless, so check that first.
1387    pub fn subgroup_id_mode(&self) -> u64 {
1388        self.serialization_flags.into_inner() & FETCH_SUBGROUP_MODE_MASK
1389    }
1390
1391    /// Whether the DATAGRAM bit (0x40) is set: the object was forwarded with an
1392    /// Object Forwarding Preference of Datagram and so has no Subgroup ID at
1393    /// all.
1394    pub fn is_datagram(&self) -> bool {
1395        self.serialization_flags.into_inner() & FETCH_DATAGRAM_BIT != 0
1396    }
1397
1398    /// Whether the flags set the PROPERTIES bit (0x20), which is what puts
1399    /// [`Self::properties`] on the wire.
1400    pub fn has_properties(&self) -> bool {
1401        fetch_has_properties(self.serialization_flags.into_inner())
1402    }
1403
1404    /// Whether resolving this object's fields requires the object before it on
1405    /// the stream.
1406    ///
1407    /// True when any of the Group ID, Object ID or Priority fields is absent —
1408    /// draft-17 Section 10.4.4.1 Table 8 defines each absent field in terms of
1409    /// "the prior Object" — or when the SUBGROUP mode names the prior object's
1410    /// Subgroup ID or that ID plus one. Mode `0x00` is not such a case: it fixes
1411    /// the Subgroup ID at zero without consulting anything.
1412    ///
1413    /// This is the predicate Section 10.4.4 makes load-bearing: "If the first
1414    /// Object in the FETCH response uses a flag that references fields in the
1415    /// prior Object, the Subscriber MUST close the session with a
1416    /// PROTOCOL_VIOLATION." A single object's bytes cannot tell whether it is
1417    /// the first, so the decoder cannot enforce that; a caller reading the
1418    /// stream can, and this is what it asks.
1419    ///
1420    /// False for both end-of-range markers. Section 10.4.4.2 fixes their fields
1421    /// outright — "the Group ID and Object ID fields are present. Subgroup ID,
1422    /// Priority and Properties are not present" — so a marker inherits nothing,
1423    /// and the section's own phrase "the last serialized Object, if any" allows
1424    /// one to open a response.
1425    pub fn references_prior_object(&self) -> bool {
1426        if self.end_of_range().is_some() {
1427            return false;
1428        }
1429        let flags = self.serialization_flags.into_inner();
1430        let inherits_subgroup = !self.is_datagram()
1431            && matches!(
1432                flags & FETCH_SUBGROUP_MODE_MASK,
1433                FETCH_SUBGROUP_MODE_PRIOR | FETCH_SUBGROUP_MODE_PRIOR_PLUS_ONE
1434            );
1435        !fetch_has_group_id(flags)
1436            || !fetch_has_object_id(flags)
1437            || !fetch_has_priority(flags)
1438            || inherits_subgroup
1439    }
1440}
1441
1442/// Whether draft-17 defines this Serialization Flags value.
1443///
1444/// Section 10.4.4: the value is a bit set "when less than 128"; `0x8C` and
1445/// `0x10C` are the two larger values Table 6 defines; "Any other value is a
1446/// PROTOCOL_VIOLATION."
1447fn fetch_flags_are_defined(flags: u64) -> bool {
1448    flags <= FETCH_FLAGS_BIT_FORM_MAX
1449        || flags == FETCH_END_OF_NON_EXISTENT_RANGE
1450        || flags == FETCH_END_OF_UNKNOWN_RANGE
1451}
1452
1453fn fetch_has_group_id(flags: u64) -> bool {
1454    flags & FETCH_GROUP_ID_BIT != 0
1455}
1456
1457/// Whether an explicit Subgroup ID field follows the Group ID.
1458///
1459/// The DATAGRAM bit wins over the mode bits. Section 10.4.4.1: "When 0x40 is
1460/// set, it SHOULD set the two least significant bits to zero and the subscriber
1461/// MUST ignore the bits." A publisher that sets the bit and leaves the mode at
1462/// `0x03` anyway has written no Subgroup ID field, so reading one would consume
1463/// the Object ID in its place.
1464///
1465/// Both end-of-range values carry mode `0x00`, which is also what
1466/// Section 10.4.4.2 requires of them, so they need no case of their own here.
1467fn fetch_has_subgroup_id(flags: u64) -> bool {
1468    flags & FETCH_DATAGRAM_BIT == 0
1469        && flags & FETCH_SUBGROUP_MODE_MASK == FETCH_SUBGROUP_MODE_EXPLICIT
1470}
1471
1472fn fetch_has_object_id(flags: u64) -> bool {
1473    flags & FETCH_OBJECT_ID_BIT != 0
1474}
1475
1476fn fetch_has_priority(flags: u64) -> bool {
1477    flags & FETCH_PRIORITY_BIT != 0
1478}
1479
1480fn fetch_has_properties(flags: u64) -> bool {
1481    flags & FETCH_PROPERTIES_BIT != 0
1482}
1483
1484/// One frame from a FETCH stream with the fields its Serialization Flags left
1485/// off the wire filled in from the frames before it.
1486///
1487/// The header is kept alongside the resolved values so that a caller can
1488/// forward the frame's bytes unchanged while acting on what they mean.
1489#[derive(Debug, Clone, PartialEq, Eq)]
1490pub struct FetchObject {
1491    /// The frame as it appeared on the wire.
1492    pub header: FetchObjectHeader,
1493    /// Resolved absolute Group ID. On an End of Range marker, the Group ID of
1494    /// the Location the marker names.
1495    pub group_id: u64,
1496    /// Resolved Subgroup ID. `None` for an End of Range marker, which has
1497    /// none, and for an Object whose forwarding preference is Datagram.
1498    pub subgroup_id: Option<u64>,
1499    /// Resolved absolute Object ID. On an End of Range marker, the Object ID of
1500    /// the Location the marker names.
1501    pub object_id: u64,
1502    /// The Publisher Priority in force for this frame, whether this frame wrote
1503    /// it or an earlier one did, and `None` while no frame has written one.
1504    ///
1505    /// An End of Range marker carries no Priority field of its own, so what it
1506    /// reports is the one still in force from the last Object before it —
1507    /// Section 10.4.4.2: "Prior Priority: The Priority from the last actual
1508    /// Object before the End of Range indicator."
1509    ///
1510    /// The fallback for a subscription that never stated a priority is left to
1511    /// the caller rather than substituted here, so that "no frame has said"
1512    /// stays distinguishable from "a frame said 128".
1513    pub publisher_priority: Option<u8>,
1514}
1515
1516/// Resolves the elided fields of the frames on one FETCH stream.
1517///
1518/// Draft-17 Section 10.4.4.1, Table 8 defines every field a frame omits as the
1519/// prior Object's — a Group ID repeated, an Object ID stepped by one, a
1520/// Priority carried over — and Table 7 does the same for the Subgroup ID, so no
1521/// frame after the first can be understood on its own. This holds what the
1522/// frames so far established, in the two parts the draft keeps separate:
1523/// Section 10.4.4.2 says that after an End of Range marker the prior Group ID
1524/// and Object ID are the marker's, while the prior Subgroup ID and Priority are
1525/// still "from the last actual Object before the End of Range indicator".
1526///
1527/// Nothing here is a delta. The fields that *are* on the wire hold values
1528/// rather than differences, which is what separates this from draft-18's reader
1529/// of the same name: draft-18 renamed both ID fields to deltas and gave them
1530/// arithmetic, which is also why this reader needs no Group Order and
1531/// draft-18's does.
1532#[derive(Debug, Clone, Default)]
1533pub struct FetchObjectReader {
1534    /// Group ID and Object ID of the last frame, marker or Object.
1535    prior_location: Option<(u64, u64)>,
1536    /// Subgroup ID of the last actual Object that had one.
1537    prior_subgroup_id: Option<u64>,
1538    /// Publisher Priority of the last actual Object.
1539    prior_publisher_priority: Option<u8>,
1540}
1541
1542impl FetchObjectReader {
1543    /// A reader positioned before the first frame of a fetch stream, with no
1544    /// prior Object to inherit from.
1545    pub fn new() -> Self {
1546        Self::default()
1547    }
1548
1549    /// Decode the next frame's header and resolve its fields.
1550    ///
1551    /// Consumes the header only. The Object Payload is
1552    /// `header.payload_length` bytes and stays in `buf`, so a caller that
1553    /// forwards payloads never copies them and one that ignores them can skip.
1554    ///
1555    /// Errors with [`CodecError::InvalidField`] when a frame names a field of a
1556    /// prior Object that does not exist — Section 10.4.4.1: "If the first
1557    /// Object in the FETCH response uses a flag that references fields in the
1558    /// prior Object, the Subscriber MUST close the session with a
1559    /// PROTOCOL_VIOLATION" — and when a Subgroup ID or Object ID one past the
1560    /// prior one would leave the 64-bit range.
1561    pub fn read_object_header(&mut self, buf: &mut impl Buf) -> Result<FetchObject, CodecError> {
1562        let header = FetchObjectHeader::decode(buf)?;
1563
1564        // An End of Range marker states a Location outright and inherits
1565        // nothing, so it is resolved before any of the prior-Object rules.
1566        if header.end_of_range().is_some() {
1567            let group_id = header.group_id.ok_or(CodecError::InvalidField)?.into_inner();
1568            let object_id = header.object_id.ok_or(CodecError::InvalidField)?.into_inner();
1569            self.prior_location = Some((group_id, object_id));
1570            let publisher_priority = self.prior_publisher_priority;
1571            return Ok(FetchObject {
1572                header,
1573                group_id,
1574                subgroup_id: None,
1575                object_id,
1576                publisher_priority,
1577            });
1578        }
1579
1580        let group_id = match header.group_id {
1581            Some(v) => v.into_inner(),
1582            None => self.prior_location.ok_or(CodecError::InvalidField)?.0,
1583        };
1584        let object_id = match header.object_id {
1585            Some(v) => v.into_inner(),
1586            None => self
1587                .prior_location
1588                .ok_or(CodecError::InvalidField)?
1589                .1
1590                .checked_add(1)
1591                .ok_or(CodecError::InvalidField)?,
1592        };
1593        let subgroup_id = if header.is_datagram() {
1594            None
1595        } else {
1596            Some(match header.subgroup_id_mode() {
1597                0x00 => 0,
1598                0x01 => self.prior_subgroup_id.ok_or(CodecError::InvalidField)?,
1599                0x02 => self
1600                    .prior_subgroup_id
1601                    .ok_or(CodecError::InvalidField)?
1602                    .checked_add(1)
1603                    .ok_or(CodecError::InvalidField)?,
1604                // Mode 0x03, the only value left: the field is on the wire.
1605                _ => header.subgroup_id.ok_or(CodecError::InvalidField)?.into_inner(),
1606            })
1607        };
1608        let publisher_priority = match header.publisher_priority {
1609            Some(p) => p,
1610            None => self.prior_publisher_priority.ok_or(CodecError::InvalidField)?,
1611        };
1612
1613        self.prior_location = Some((group_id, object_id));
1614        // A Datagram-forwarded object has no Subgroup ID to leave behind, so it
1615        // does not clear the running one: the object after it inherits from the
1616        // last object that had one.
1617        if let Some(subgroup_id) = subgroup_id {
1618            self.prior_subgroup_id = Some(subgroup_id);
1619        }
1620        self.prior_publisher_priority = Some(publisher_priority);
1621
1622        Ok(FetchObject {
1623            header,
1624            group_id,
1625            subgroup_id,
1626            object_id,
1627            publisher_priority: Some(publisher_priority),
1628        })
1629    }
1630}
1631
1632/// Re-encodes resolved fetch frames onto one FETCH stream.
1633///
1634/// The exact inverse of [`FetchObjectReader`], and it exists for one caller:
1635/// something that has read a stream and is writing a different stream from the
1636/// same frames. Removing a frame changes what the frames after it are read
1637/// against, and draft-17 Section 10.4.4.1 lets an Object leave out its Group
1638/// ID, Object ID, Subgroup ID and Priority and take the prior Object's, so the
1639/// survivor that follows a removed run cannot keep its original bytes: a field
1640/// it left off has to appear, and a flag bit with it.
1641///
1642/// # Draft-17 states these fields, it does not delta them
1643///
1644/// A Group ID or Object ID that is on the wire here is the absolute value, not
1645/// a difference — the deltas arrive at draft-18. What is stateful is the
1646/// *omission*: no Group ID means the prior Object's, and no Object ID means the
1647/// prior Object's plus one. That is enough to make removal a re-encode, and it
1648/// is why this writer refuses nothing an Object can be: every identity has an
1649/// encoding here, however the predecessor moved.
1650///
1651/// # Why this is not a general encoder
1652///
1653/// Every frame it writes came off a stream, so the caller holds the frame's own
1654/// [`FetchObjectHeader`] alongside the resolved values, and that header is used
1655/// as the preference: wherever the original shape still says the same thing
1656/// against the new predecessor it is kept, so a stream with nothing removed is
1657/// reproduced byte for byte.
1658#[derive(Debug, Clone, Default)]
1659pub struct FetchObjectWriter {
1660    /// Group ID and Object ID of the last frame written, marker or Object.
1661    prior_location: Option<(u64, u64)>,
1662    /// Subgroup ID of the last actual Object written that had one.
1663    prior_subgroup_id: Option<u64>,
1664    /// Publisher Priority of the last actual Object written.
1665    prior_publisher_priority: Option<u8>,
1666}
1667
1668impl FetchObjectWriter {
1669    /// A writer positioned before the first Object of a fetch stream, with no
1670    /// prior Object for anything to be encoded against.
1671    pub fn new() -> Self {
1672        Self::default()
1673    }
1674
1675    /// The header that encodes `frame` against everything written so far.
1676    ///
1677    /// Does not advance the writer — [`Self::write_object_header`] is the call
1678    /// that does both.
1679    ///
1680    /// # Errors
1681    ///
1682    /// [`CodecError::InvalidField`] for an Object with neither a Subgroup ID
1683    /// nor the Datagram bit, and for one with no Priority: both are frames no
1684    /// draft-17 stream could carry, and inventing a value would put a different
1685    /// Object on the wire than the one this was handed.
1686    pub fn header_for(&self, frame: &FetchObject) -> Result<FetchObjectHeader, CodecError> {
1687        let original = &frame.header;
1688
1689        // An End of Range marker states its Location outright and inherits
1690        // nothing, so its two fields are the same whatever precedes it.
1691        if original.end_of_range().is_some() {
1692            return Ok(FetchObjectHeader {
1693                serialization_flags: original.serialization_flags,
1694                group_id: Some(VarInt::from_u64(frame.group_id)?),
1695                subgroup_id: None,
1696                object_id: Some(VarInt::from_u64(frame.object_id)?),
1697                publisher_priority: None,
1698                properties: Vec::new(),
1699                payload_length: original.payload_length,
1700            });
1701        }
1702
1703        let (group_id, object_id) = self.identity_fields(frame, original)?;
1704        let (subgroup_mode, subgroup_id) = self.subgroup_field(frame, original)?;
1705        let publisher_priority = self.priority_field(frame, original)?;
1706
1707        let flags = original.serialization_flags.into_inner();
1708        let mut new_flags = subgroup_mode;
1709        if flags & FETCH_DATAGRAM_BIT != 0 {
1710            new_flags |= FETCH_DATAGRAM_BIT;
1711        }
1712        if group_id.is_some() {
1713            new_flags |= FETCH_GROUP_ID_BIT;
1714        }
1715        if object_id.is_some() {
1716            new_flags |= FETCH_OBJECT_ID_BIT;
1717        }
1718        if publisher_priority.is_some() {
1719            new_flags |= FETCH_PRIORITY_BIT;
1720        }
1721        if flags & FETCH_PROPERTIES_BIT != 0 {
1722            new_flags |= FETCH_PROPERTIES_BIT;
1723        }
1724
1725        Ok(FetchObjectHeader {
1726            serialization_flags: VarInt::from_u64(new_flags)?,
1727            group_id,
1728            subgroup_id,
1729            object_id,
1730            publisher_priority,
1731            properties: original.properties.clone(),
1732            payload_length: original.payload_length,
1733        })
1734    }
1735
1736    /// The Group ID and Object ID fields, present only where leaving them off
1737    /// would say something else.
1738    ///
1739    /// Both are absolute when written, so the choice is only whether to write
1740    /// them, and it is made in favour of the shape the frame arrived in: an
1741    /// Object that stated its Group ID keeps stating it even where the
1742    /// predecessor now shares it, which costs the same bytes it already cost.
1743    fn identity_fields(
1744        &self,
1745        frame: &FetchObject,
1746        original: &FetchObjectHeader,
1747    ) -> Result<(Option<VarInt>, Option<VarInt>), CodecError> {
1748        let group_id = match self.prior_location {
1749            Some((prior_group, _))
1750                if original.group_id.is_none() && prior_group == frame.group_id =>
1751            {
1752                None
1753            }
1754            _ => Some(VarInt::from_u64(frame.group_id)?),
1755        };
1756        let object_id = match self.prior_location {
1757            Some((_, prior_object))
1758                if original.object_id.is_none()
1759                    && prior_object.checked_add(1) == Some(frame.object_id) =>
1760            {
1761                None
1762            }
1763            _ => Some(VarInt::from_u64(frame.object_id)?),
1764        };
1765        Ok((group_id, object_id))
1766    }
1767
1768    /// The Subgroup ID mode bits and the explicit field, if one is needed.
1769    ///
1770    /// The frame's own mode is tried first, so a run of Objects that inherited
1771    /// their Subgroup ID keeps inheriting it and its bytes do not move.
1772    fn subgroup_field(
1773        &self,
1774        frame: &FetchObject,
1775        original: &FetchObjectHeader,
1776    ) -> Result<(u64, Option<VarInt>), CodecError> {
1777        // With the Datagram bit set the two low bits say nothing and no field
1778        // is on the wire, so the frame's own bits are carried across untouched.
1779        if original.is_datagram() {
1780            return Ok((
1781                original.serialization_flags.into_inner() & FETCH_SUBGROUP_MODE_MASK,
1782                None,
1783            ));
1784        }
1785
1786        let subgroup_id = frame.subgroup_id.ok_or(CodecError::InvalidField)?;
1787        let inherits = self.prior_subgroup_id == Some(subgroup_id);
1788        let successor =
1789            self.prior_subgroup_id.is_some_and(|p| p.checked_add(1) == Some(subgroup_id));
1790
1791        // Mode 0x00 is *the Subgroup ID is zero*; the reader reads it that way
1792        // and there is no named constant for it beside the other three.
1793        let kept = match original.subgroup_id_mode() {
1794            0x00 if subgroup_id == 0 => Some((0x00, None)),
1795            FETCH_SUBGROUP_MODE_PRIOR if inherits => Some((FETCH_SUBGROUP_MODE_PRIOR, None)),
1796            FETCH_SUBGROUP_MODE_PRIOR_PLUS_ONE if successor => {
1797                Some((FETCH_SUBGROUP_MODE_PRIOR_PLUS_ONE, None))
1798            }
1799            FETCH_SUBGROUP_MODE_EXPLICIT => Some((FETCH_SUBGROUP_MODE_EXPLICIT, Some(subgroup_id))),
1800            _ => None,
1801        };
1802        let (mode, explicit) = match kept {
1803            Some(pair) => pair,
1804            None if subgroup_id == 0 => (0x00, None),
1805            None if inherits => (FETCH_SUBGROUP_MODE_PRIOR, None),
1806            None if successor => (FETCH_SUBGROUP_MODE_PRIOR_PLUS_ONE, None),
1807            None => (FETCH_SUBGROUP_MODE_EXPLICIT, Some(subgroup_id)),
1808        };
1809        Ok((mode, explicit.map(VarInt::from_u64).transpose()?))
1810    }
1811
1812    /// The Publisher Priority field, or `None` when the predecessor already
1813    /// carries it.
1814    fn priority_field(
1815        &self,
1816        frame: &FetchObject,
1817        original: &FetchObjectHeader,
1818    ) -> Result<Option<u8>, CodecError> {
1819        let priority = frame.publisher_priority.ok_or(CodecError::InvalidField)?;
1820        if original.publisher_priority.is_some() || self.prior_publisher_priority != Some(priority)
1821        {
1822            return Ok(Some(priority));
1823        }
1824        Ok(None)
1825    }
1826
1827    /// Encode `frame` against everything written so far and advance.
1828    ///
1829    /// Writes the header only. The payload is `frame.header.payload_length`
1830    /// bytes and is the caller's to copy, unchanged.
1831    ///
1832    /// # Errors
1833    ///
1834    /// [`CodecError::InvalidField`] for a frame with no encoding at all; the
1835    /// writer is left untouched when this happens.
1836    pub fn write_object_header(
1837        &mut self,
1838        frame: &FetchObject,
1839        out: &mut impl BufMut,
1840    ) -> Result<FetchObjectHeader, CodecError> {
1841        let header = self.header_for(frame)?;
1842        header.encode(out)?;
1843        self.advance(frame);
1844        Ok(header)
1845    }
1846
1847    /// Record `frame` as the predecessor of whatever is written next.
1848    ///
1849    /// Public because a re-emitting caller has a second way of putting a frame
1850    /// on the wire: when the framing it arrived in still encodes the same
1851    /// meaning against the frame before it, its own bytes are forwarded
1852    /// untouched — no header is produced and nothing is copied. The writer
1853    /// still has to move, or the frame after it is encoded against a
1854    /// predecessor one frame stale.
1855    pub fn advance(&mut self, frame: &FetchObject) {
1856        self.prior_location = Some((frame.group_id, frame.object_id));
1857        // Mirrors the reader: a Datagram-forwarded Object leaves no Subgroup ID
1858        // behind, so the running one survives it.
1859        if let Some(subgroup_id) = frame.subgroup_id {
1860            self.prior_subgroup_id = Some(subgroup_id);
1861        }
1862        if frame.header.end_of_range().is_none() {
1863            if let Some(priority) = frame.publisher_priority {
1864                self.prior_publisher_priority = Some(priority);
1865            }
1866        }
1867    }
1868}
1869
1870#[cfg(test)]
1871mod tests {
1872    use super::*;
1873
1874    /// Canonically encoded subgroup stream vectors from
1875    /// `test-vectors/transport/draft17/codec/data-streams/subgroup.json`.
1876    /// `subgroup-explicit-subgroup-id` is omitted: it encodes group_id 100 as a
1877    /// two-byte varint, which does not survive a minimal-width re-encode.
1878    const VECTORS: &[&str] = &[
1879        // subgroup-single-object
1880        "100100800004deadbeef",
1881        // subgroup-two-objects
1882        "100100800004deadbeef0002cafe",
1883        // subgroup-no-priority
1884        "3001000004deadbeef",
1885        // subgroup-with-extensions
1886        "11010080000004deadbeef",
1887        // subgroup-end-of-group
1888        "180105800004deadbeef",
1889        // subgroup-id-mode-01
1890        "120100800504deadbeef",
1891        // subgroup-with-object-properties
1892        "1101008000043c02020104deadbeef",
1893        // subgroup-object-status-end-of-group
1894        "100100800004deadbeef000003",
1895        // subgroup-object-status-end-of-track
1896        "10010080000004",
1897        // subgroup-properties-two-objects-empty
1898        "11010080000004deadbeef000002cafe",
1899        // subgroup-properties-two-objects-nonempty
1900        "1101008000023c0204deadbeef00023c0302cafe",
1901        // subgroup-properties-status-object
1902        "1101008000023c010003",
1903    ];
1904
1905    fn vi(v: u64) -> VarInt {
1906        VarInt::from_u64_moqt(v)
1907    }
1908
1909    fn hex(s: &str) -> Vec<u8> {
1910        (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
1911    }
1912
1913    /// Decode a whole subgroup stream: the header, then every object up to
1914    /// the end of the buffer.
1915    fn decode_all(bytes: &[u8]) -> (SubgroupHeader, Vec<SubgroupObject>) {
1916        let mut cursor = bytes;
1917        let header = SubgroupHeader::decode(&mut cursor)
1918            .unwrap_or_else(|e| panic!("header decode failed: {e:?}"));
1919        let mut reader = SubgroupObjectReader::new(&header);
1920        let mut objects = Vec::new();
1921        while cursor.has_remaining() {
1922            objects.push(
1923                reader
1924                    .read_object(&mut cursor)
1925                    .unwrap_or_else(|e| panic!("object {} decode failed: {e:?}", objects.len())),
1926            );
1927        }
1928        (header, objects)
1929    }
1930
1931    fn encode_all(header: &SubgroupHeader, objects: &[SubgroupObject]) -> Vec<u8> {
1932        let mut buf = Vec::new();
1933        header.encode(&mut buf);
1934        let mut writer = SubgroupObjectReader::new(header);
1935        for o in objects {
1936            writer.write_object(o, &mut buf).unwrap_or_else(|e| panic!("write failed: {e:?}"));
1937        }
1938        buf
1939    }
1940
1941    fn object(id: u64, extensions: Vec<u8>, payload: Vec<u8>) -> SubgroupObject {
1942        SubgroupObject {
1943            object_id: vi(id),
1944            extension_headers: extensions,
1945            payload_length: vi(payload.len() as u64),
1946            object_status: None,
1947            payload,
1948        }
1949    }
1950
1951    // ── Object ID deltas ────────────────────────────────────
1952
1953    #[test]
1954    fn two_objects_with_properties_have_distinct_ids() {
1955        // Vector `subgroup-properties-two-objects-empty`: two objects, each
1956        // carrying an empty properties block and a delta of 0. The delta is
1957        // biased by one whether or not the properties bit is set, so the IDs
1958        // are 0 and 1 — not 0 and 0.
1959        let bytes = hex("11010080000004deadbeef000002cafe");
1960        let (header, objects) = decode_all(&bytes);
1961        assert!(header.has_properties());
1962        assert_eq!(objects.len(), 2);
1963        assert_eq!(objects[0].object_id.into_inner(), 0);
1964        assert_eq!(objects[1].object_id.into_inner(), 1);
1965        assert_eq!(objects[0].payload, hex("deadbeef"));
1966        assert_eq!(objects[1].payload, hex("cafe"));
1967        assert!(objects.iter().all(|o| o.extension_headers.is_empty()));
1968    }
1969
1970    #[test]
1971    fn deltas_resolve_sparse_ids() {
1972        let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
1973        let objects: Vec<_> =
1974            [3u64, 4, 40].iter().map(|&id| object(id, vec![], vec![0xAA, id as u8])).collect();
1975        let (_, decoded) = decode_all(&encode_all(&header, &objects));
1976        let ids: Vec<u64> = decoded.iter().map(|o| o.object_id.into_inner()).collect();
1977        assert_eq!(ids, vec![3, 4, 40]);
1978    }
1979
1980    #[test]
1981    fn write_rejects_non_increasing_ids() {
1982        let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
1983        let mut writer = SubgroupObjectReader::new(&header);
1984        let mut buf = Vec::new();
1985        writer.write_object(&object(7, vec![], vec![0x01]), &mut buf).unwrap();
1986        for id in [7u64, 6, 0] {
1987            let err = writer.write_object(&object(id, vec![], vec![0x01]), &mut buf).unwrap_err();
1988            assert!(matches!(err, CodecError::InvalidField), "id {id} gave {err:?}");
1989        }
1990    }
1991
1992    #[test]
1993    fn eliding_an_object_renumbers_its_successor() {
1994        let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
1995        let all: Vec<_> = (0..5u64).map(|id| object(id, vec![], vec![id as u8])).collect();
1996        for elided in 0..5u64 {
1997            let kept: Vec<_> =
1998                all.iter().filter(|o| o.object_id.into_inner() != elided).cloned().collect();
1999            let (_, decoded) = decode_all(&encode_all(&header, &kept));
2000            let ids: Vec<u64> = decoded.iter().map(|o| o.object_id.into_inner()).collect();
2001            let expected: Vec<u64> = (0..5u64).filter(|&i| i != elided).collect();
2002            assert_eq!(ids, expected, "eliding object {elided}");
2003        }
2004    }
2005
2006    // ── Properties blocks ──────────────────────────────
2007
2008    #[test]
2009    fn properties_blob_excludes_its_length_prefix() {
2010        // Vector `subgroup-properties-two-objects-nonempty`: each
2011        // object carries a two-byte block, so the blob is those two bytes
2012        // with the `02` length prefix stripped.
2013        let bytes = hex("1101008000023c0204deadbeef00023c0302cafe");
2014        let (_, objects) = decode_all(&bytes);
2015        assert_eq!(objects.len(), 2);
2016        assert_eq!(objects[0].object_id.into_inner(), 0);
2017        assert_eq!(objects[1].object_id.into_inner(), 1);
2018        assert_eq!(objects[0].extension_headers, hex("3c02"));
2019        assert_eq!(objects[1].extension_headers, hex("3c03"));
2020        assert_eq!(objects[0].payload, hex("deadbeef"));
2021        assert_eq!(objects[1].payload, hex("cafe"));
2022    }
2023
2024    #[test]
2025    fn status_object_carries_its_properties_block() {
2026        let (_, objects) = decode_all(&hex("1101008000023c010003"));
2027        assert_eq!(objects.len(), 1);
2028        assert_eq!(objects[0].extension_headers, hex("3c01"));
2029        assert_eq!(objects[0].payload_length.into_inner(), 0);
2030        assert_eq!(objects[0].object_status.map(ObjectStatus::as_u64), Some(3));
2031        assert!(objects[0].payload.is_empty());
2032    }
2033
2034    // ── Re-encoding ─────────────────────────────────────────
2035
2036    #[test]
2037    fn vectors_re_encode_byte_identically() {
2038        for vector in VECTORS {
2039            let bytes = hex(vector);
2040            let (header, objects) = decode_all(&bytes);
2041            assert_eq!(encode_all(&header, &objects), bytes, "[{vector}] re-encode");
2042        }
2043    }
2044
2045    // ── Payload-free framing ────────────────────────────────
2046
2047    #[test]
2048    fn meta_matches_read_object() {
2049        for vector in VECTORS {
2050            let bytes = hex(vector);
2051            let mut cursor = &bytes[..];
2052            let header = SubgroupHeader::decode(&mut cursor).unwrap();
2053            let mut full_reader = SubgroupObjectReader::new(&header);
2054            let mut meta_reader = SubgroupObjectReader::new(&header);
2055            let mut full_cursor = cursor;
2056            let mut meta_cursor = cursor;
2057            while meta_cursor.has_remaining() {
2058                let before = meta_cursor.remaining();
2059                let object = full_reader.read_object(&mut full_cursor).unwrap();
2060                let meta = meta_reader.read_object_meta(&mut meta_cursor).unwrap();
2061                assert_eq!(meta.object_id, object.object_id.into_inner(), "[{vector}]");
2062                assert_eq!(
2063                    meta.extension_headers_len,
2064                    object.extension_headers.len() as u64,
2065                    "[{vector}]"
2066                );
2067                assert_eq!(meta.payload_length, object.payload_length.into_inner(), "[{vector}]");
2068                assert_eq!(
2069                    meta.status,
2070                    object.object_status.map(ObjectStatus::as_u64),
2071                    "[{vector}]"
2072                );
2073                assert_eq!(meta.wire_len, (before - meta_cursor.remaining()) as u64, "[{vector}]");
2074                assert_eq!(full_cursor.remaining(), meta_cursor.remaining(), "[{vector}]");
2075            }
2076        }
2077    }
2078
2079    #[test]
2080    fn short_buffers_report_unexpected_end() {
2081        let bytes = hex("1101008000023c0204deadbeef00023c0302cafe");
2082        let mut cursor = &bytes[..];
2083        let header = SubgroupHeader::decode(&mut cursor).unwrap();
2084        let objects_start = bytes.len() - cursor.len();
2085        for cut in objects_start..bytes.len() {
2086            let mut reader = SubgroupObjectReader::new(&header);
2087            let mut meta_reader = SubgroupObjectReader::new(&header);
2088            let mut cursor = &bytes[objects_start..cut];
2089            let mut meta_cursor = cursor;
2090            while cursor.has_remaining() {
2091                if let Err(err) = reader.read_object(&mut cursor) {
2092                    assert!(
2093                        matches!(err, CodecError::UnexpectedEnd | CodecError::VarInt(_)),
2094                        "cut {cut} gave {err:?}"
2095                    );
2096                    break;
2097                }
2098            }
2099            while meta_cursor.has_remaining() {
2100                if let Err(err) = meta_reader.read_object_meta(&mut meta_cursor) {
2101                    assert!(
2102                        matches!(err, CodecError::UnexpectedEnd | CodecError::VarInt(_)),
2103                        "cut {cut} gave {err:?}"
2104                    );
2105                    break;
2106                }
2107            }
2108        }
2109    }
2110
2111    // ── Object status ───────────────────────────────────────
2112
2113    /// A one-object subgroup stream whose object carries `status` in place of
2114    /// a payload: header type 0x10 (no properties, subgroup-ID mode 0), track
2115    /// alias 1, group 0, publisher priority 128; then an Object ID delta of 0,
2116    /// a payload length of 0, and the status code.
2117    fn subgroup_status_stream(status: u64) -> Vec<u8> {
2118        vec![0x10, 0x01, 0x00, 0x80, 0x00, 0x00, status as u8]
2119    }
2120
2121    /// A status datagram carrying `status`: type 0x20 (STATUS bit set,
2122    /// explicit Object ID, explicit priority), track alias 1, group 0, object
2123    /// 0, priority 128, then the status byte.
2124    fn status_datagram(status: u64) -> Vec<u8> {
2125        vec![0x20, 0x01, 0x00, 0x00, 0x80, status as u8]
2126    }
2127
2128    /// The object [`subgroup_status_stream`] describes, as a value.
2129    fn status_object(status: Option<ObjectStatus>) -> SubgroupObject {
2130        SubgroupObject {
2131            object_id: vi(0),
2132            extension_headers: Vec::new(),
2133            payload_length: vi(0),
2134            object_status: status,
2135            payload: Vec::new(),
2136        }
2137    }
2138
2139    /// The datagram [`status_datagram`] describes, as a value.
2140    fn status_datagram_header(status: Option<ObjectStatus>) -> DatagramHeader {
2141        DatagramHeader {
2142            datagram_type: 0x20,
2143            track_alias: vi(1),
2144            group_id: vi(0),
2145            object_id: vi(0),
2146            publisher_priority: Some(128),
2147            properties: Vec::new(),
2148            object_status: status,
2149        }
2150    }
2151
2152    /// Every status draft-17 assigns can be written and read back as the same
2153    /// status, on both a subgroup stream and a status datagram.
2154    ///
2155    /// The set is read from `ObjectStatus::ALL` rather than restated here, so
2156    /// this moves with the draft if a code is ever reassigned. It is the gate
2157    /// on typing the two `object_status` fields: a typed field that silently
2158    /// narrowed or renumbered the set would fail here even though it still
2159    /// compiled.
2160    ///
2161    /// Writing `ObjectStatus::Normal` when a zero-length object's status is
2162    /// `None` is checked too — without it the encoder emits an object whose
2163    /// declared payload length promises a status field that never arrives.
2164    ///
2165    /// Made `write_object` encode a constant `ObjectStatus::Normal` instead of
2166    /// the object's own status, ran it, and got:
2167    ///
2168    /// ```text
2169    /// assertion `left == right` failed: subgroup object status
2170    ///   left: Some(Normal)
2171    ///  right: Some(EndOfGroup)
2172    /// ```
2173    ///
2174    /// The same change to `DatagramHeader::encode` gives:
2175    ///
2176    /// ```text
2177    /// assertion `left == right` failed: datagram object status
2178    ///   left: Some(Normal)
2179    ///  right: Some(EndOfGroup)
2180    /// ```
2181    #[test]
2182    fn every_assigned_status_survives_a_round_trip() {
2183        let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
2184        for &status in ObjectStatus::ALL {
2185            let mut buf = Vec::new();
2186            SubgroupObjectReader::new(&header)
2187                .write_object(&status_object(Some(status)), &mut buf)
2188                .unwrap_or_else(|e| panic!("write_object refused {status:?}: {e:?}"));
2189
2190            let mut cursor = &buf[..];
2191            let object =
2192                SubgroupObjectReader::new(&header).read_object(&mut cursor).unwrap_or_else(|e| {
2193                    panic!("read_object refused the bytes written for {status:?}: {e:?}")
2194                });
2195            assert_eq!(object.object_status, Some(status), "subgroup object status");
2196            assert!(!cursor.has_remaining(), "{status:?}: bytes left over after read_object");
2197
2198            let meta =
2199                SubgroupObjectReader::new(&header).read_object_meta(&mut &buf[..]).unwrap_or_else(
2200                    |e| panic!("read_object_meta refused the bytes written for {status:?}: {e:?}"),
2201                );
2202            assert_eq!(meta.status, Some(status.as_u64()), "subgroup meta status");
2203
2204            let mut datagram = Vec::new();
2205            status_datagram_header(Some(status)).encode(&mut datagram);
2206            let decoded = DatagramHeader::decode(&mut &datagram[..]).unwrap_or_else(|e| {
2207                panic!("datagram decode refused the bytes written for {status:?}: {e:?}")
2208            });
2209            assert_eq!(decoded.object_status, Some(status), "datagram object status");
2210        }
2211
2212        let mut buf = Vec::new();
2213        SubgroupObjectReader::new(&header).write_object(&status_object(None), &mut buf).unwrap();
2214        let object = SubgroupObjectReader::new(&header)
2215            .read_object(&mut &buf[..])
2216            .expect("a zero-length object with no status must still decode");
2217        assert_eq!(object.object_status, Some(ObjectStatus::Normal));
2218
2219        let mut datagram = Vec::new();
2220        status_datagram_header(None).encode(&mut datagram);
2221        let decoded = DatagramHeader::decode(&mut &datagram[..])
2222            .expect("a status datagram with no status must still decode");
2223        assert_eq!(decoded.object_status, Some(ObjectStatus::Normal));
2224    }
2225
2226    /// The encoder writes exactly the frames the decoder accepts.
2227    ///
2228    /// Sweeps every status code `0x00..=0x3f` — one wire byte under both the
2229    /// varint on a subgroup stream and the bare byte on a datagram, and wide
2230    /// enough to contain the gap at `0x2` and the `0x1` draft-16 dropped. For
2231    /// a code the draft assigns, the hand-built frame must decode *and* the
2232    /// encoder handed that status must reproduce those exact bytes. For a code
2233    /// it does not assign, the same frame must be refused at all three decode
2234    /// sites — and no `ObjectStatus` exists to hand the encoder, so the frame
2235    /// has no way to be produced in the first place.
2236    ///
2237    /// # What this catches, observed by making each change and running it
2238    ///
2239    /// Encoding a constant `ObjectStatus::Normal` in `write_object` instead of
2240    /// the object's own status:
2241    ///
2242    /// ```text
2243    /// assertion `left == right` failed: the encoder must produce the frame the decoder accepted for status 0x3
2244    ///   left: [16, 1, 0, 128, 0, 0, 0]
2245    ///  right: [16, 1, 0, 128, 0, 0, 3]
2246    /// ```
2247    ///
2248    /// The same change in `DatagramHeader::encode`:
2249    ///
2250    /// ```text
2251    /// assertion `left == right` failed: the encoder must produce the datagram the decoder accepted for status 0x3
2252    ///   left: [32, 1, 0, 0, 128, 0]
2253    ///  right: [32, 1, 0, 0, 128, 3]
2254    /// ```
2255    ///
2256    /// The decoder drifting away from `ALL` — adding `0x2` to
2257    /// `ObjectStatus::from_u64`, so a code the draft does not assign starts
2258    /// decoding:
2259    ///
2260    /// ```text
2261    /// subgroup read_object accepted status 0x2, which the draft does not assign
2262    /// ```
2263    ///
2264    /// # The encode-side refusal is a type, not an assertion
2265    ///
2266    /// Once `object_status` is typed there is no runtime path that offers the
2267    /// encoder a `0x2`, so no test here can watch one be refused. Reverting
2268    /// `DatagramHeader::object_status` to `Option<u8>` with an `unwrap_or(0)`
2269    /// encoder does not make this test fail — it makes it stop compiling,
2270    /// which is the guarantee:
2271    ///
2272    /// ```text
2273    /// error[E0308]: mismatched types
2274    ///     = note: expected enum `Option<u8>`
2275    ///                found enum `Option<draft17::types::ObjectStatus>`
2276    /// ```
2277    #[test]
2278    fn the_encoder_writes_exactly_the_frames_the_decoder_accepts() {
2279        for code in 0x00u64..=0x3f {
2280            let assigned = ObjectStatus::ALL.iter().copied().find(|s| s.as_u64() == code);
2281
2282            let stream = subgroup_status_stream(code);
2283            let mut cursor: &[u8] = &stream;
2284            let header = SubgroupHeader::decode(&mut cursor).unwrap();
2285            let objects = cursor;
2286            let read = SubgroupObjectReader::new(&header).read_object(&mut { objects });
2287            let meta = SubgroupObjectReader::new(&header).read_object_meta(&mut { objects });
2288
2289            let datagram = status_datagram(code);
2290            let decoded = DatagramHeader::decode(&mut &datagram[..]);
2291
2292            match assigned {
2293                Some(status) => {
2294                    let object = read.unwrap_or_else(|e| {
2295                        panic!(
2296                            "read_object refused status {code:#x}, which the draft assigns: {e:?}"
2297                        )
2298                    });
2299                    assert_eq!(object.object_status, Some(status));
2300                    assert_eq!(meta.unwrap().status, Some(code));
2301                    assert_eq!(decoded.unwrap().object_status, Some(status));
2302
2303                    let mut written = Vec::new();
2304                    header.encode(&mut written);
2305                    SubgroupObjectReader::new(&header)
2306                        .write_object(&status_object(Some(status)), &mut written)
2307                        .unwrap();
2308                    assert_eq!(
2309                        written, stream,
2310                        "the encoder must produce the frame the decoder accepted for status {code:#x}"
2311                    );
2312
2313                    let mut written = Vec::new();
2314                    status_datagram_header(Some(status)).encode(&mut written);
2315                    assert_eq!(
2316                        written, datagram,
2317                        "the encoder must produce the datagram the decoder accepted for status {code:#x}"
2318                    );
2319                }
2320                None => {
2321                    for (site, result) in [
2322                        ("subgroup read_object", read.map(|_| ())),
2323                        ("subgroup read_object_meta", meta.map(|_| ())),
2324                        ("status datagram", decoded.map(|_| ())),
2325                    ] {
2326                        match result {
2327                            Ok(()) => panic!(
2328                                "{site} accepted status {code:#x}, which the draft does not assign"
2329                            ),
2330                            Err(error) => assert!(
2331                                matches!(error, CodecError::InvalidField),
2332                                "{site} refused status {code:#x} with {error:?}, not InvalidField"
2333                            ),
2334                        }
2335                    }
2336                }
2337            }
2338        }
2339    }
2340}