Skip to main content

moqtap_codec/draft15/
data_stream.rs

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