Skip to main content

moqtap_codec/draft20/
data_stream.rs

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