Skip to main content

moqtap_codec/draft19/
data_stream.rs

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