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