Skip to main content

moqtap_codec/
dispatch.rs

1//! Unified types and version-aware decode/encode for runtime draft dispatch.
2//!
3//! This module provides wrapper enums (`Any*`) that hold any enabled draft's
4//! types and dispatch encoding/decoding based on
5//! [`DraftVersion`](crate::version::DraftVersion).
6//!
7//! Each enum variant is gated on its draft feature flag. Enable multiple draft
8//! features (e.g. `draft07` + `draft14`) for runtime dispatch between drafts.
9
10use bytes::{Buf, BufMut};
11
12use crate::error::CodecError;
13use crate::version::DraftVersion;
14
15pub use crate::data_dispatch::{
16    reemit_subgroup_object, AnyFetchEndOfRange, AnyFetchFrame, AnyFetchGroupOrder, AnyFetchObject,
17    AnyFetchObjectMeta, AnyFetchObjectReader, AnyFetchObjectWriter, AnySubgroupObject,
18    AnySubgroupObjectMeta, AnySubgroupObjectReader, AnySubgroupObjectWriter, FetchReemit, Reemit,
19};
20
21/// Generates a dispatch enum with one variant per enabled draft feature.
22///
23/// Each variant wraps the draft-specific type and delegates encode/decode
24/// to the appropriate draft module.
25macro_rules! dispatch_enum {
26    (
27        $(#[$meta:meta])*
28        $vis:vis enum $name:ident {
29            $(
30                #[cfg(feature = $feat:literal)]
31                $variant:ident => $module:path,
32            )+
33        }
34        decode($decode_fn:ident);
35        encode($encode_fn:ident -> $encode_ret:ty);
36    ) => {
37        $(#[$meta])*
38        $vis enum $name {
39            $(
40                #[cfg(feature = $feat)]
41                #[doc = concat!("Draft-", $feat, " variant.")]
42                $variant($module),
43            )+
44        }
45
46        impl $name {
47            /// Decode from wire using the specified draft version.
48            #[allow(unused_variables)]
49            pub fn decode(
50                version: DraftVersion,
51                buf: &mut impl Buf,
52            ) -> Result<Self, CodecError> {
53                match version {
54                    $(
55                        #[cfg(feature = $feat)]
56                        DraftVersion::$variant => {
57                            <$module>::$decode_fn(buf).map($name::$variant)
58                        }
59                    )+
60                    #[allow(unreachable_patterns)]
61                    _ => Err(CodecError::UnsupportedDraft(
62                        format!("draft {:?} not enabled via feature flag", version),
63                    )),
64                }
65            }
66
67            /// Encode to wire using the appropriate draft's format.
68            #[allow(unused_variables, unreachable_code)]
69            pub fn encode(&self, buf: &mut impl BufMut) -> $encode_ret {
70                match self {
71                    $(
72                        #[cfg(feature = $feat)]
73                        $name::$variant(inner) => inner.$encode_fn(buf),
74                    )+
75                    #[allow(unreachable_patterns)]
76                    _ => unreachable!("AnyXxx enum has no enabled variants"),
77                }
78            }
79
80            /// Returns the draft version this value belongs to.
81            #[allow(unreachable_code)]
82            pub fn draft(&self) -> DraftVersion {
83                match self {
84                    $(
85                        #[cfg(feature = $feat)]
86                        $name::$variant(_) => DraftVersion::$variant,
87                    )+
88                    #[allow(unreachable_patterns)]
89                    _ => unreachable!("AnyXxx enum has no enabled variants"),
90                }
91            }
92        }
93    };
94}
95
96/// Generates one uniform [`AnySubgroupHeader`] accessor.
97///
98/// Bodies are written once per group of drafts that share one; every arm is
99/// `#[cfg]`-gated on its own draft feature and a catch-all closes the match,
100/// so a single-draft build and a zero-draft build both compile — the same
101/// shape [`dispatch_enum!`] generates for `draft()`.
102macro_rules! subgroup_header_accessor {
103    (
104        $(#[$meta:meta])*
105        $name:ident -> $ret:ty;
106        $(
107            [ $( $variant:ident @ $feat:literal ),+ $(,)? ] => |$h:ident| $body:expr
108        ),+ $(,)?
109    ) => {
110        $(#[$meta])*
111        #[allow(unreachable_code)]
112        pub fn $name(&self) -> $ret {
113            match self {
114                $($(
115                    #[cfg(feature = $feat)]
116                    AnySubgroupHeader::$variant($h) => $body,
117                )+)+
118                #[allow(unreachable_patterns)]
119                _ => unreachable!("AnySubgroupHeader has no enabled variants"),
120            }
121        }
122    };
123}
124
125// ── Control messages ────────────────────────────────────────
126
127dispatch_enum! {
128    /// A control message from any enabled draft.
129    #[derive(Debug, Clone)]
130    pub enum AnyControlMessage {
131        #[cfg(feature = "draft07")]
132        Draft07 => crate::draft07::message::ControlMessage,
133        #[cfg(feature = "draft08")]
134        Draft08 => crate::draft08::message::ControlMessage,
135        #[cfg(feature = "draft09")]
136        Draft09 => crate::draft09::message::ControlMessage,
137        #[cfg(feature = "draft10")]
138        Draft10 => crate::draft10::message::ControlMessage,
139        #[cfg(feature = "draft11")]
140        Draft11 => crate::draft11::message::ControlMessage,
141        #[cfg(feature = "draft12")]
142        Draft12 => crate::draft12::message::ControlMessage,
143        #[cfg(feature = "draft13")]
144        Draft13 => crate::draft13::message::ControlMessage,
145        #[cfg(feature = "draft14")]
146        Draft14 => crate::draft14::message::ControlMessage,
147        #[cfg(feature = "draft15")]
148        Draft15 => crate::draft15::message::ControlMessage,
149        #[cfg(feature = "draft16")]
150        Draft16 => crate::draft16::message::ControlMessage,
151        #[cfg(feature = "draft17")]
152        Draft17 => crate::draft17::message::ControlMessage,
153        #[cfg(feature = "draft18")]
154        Draft18 => crate::draft18::message::ControlMessage,
155        #[cfg(feature = "draft19")]
156        Draft19 => crate::draft19::message::ControlMessage,
157    }
158    decode(decode);
159    encode(encode -> Result<(), CodecError>);
160}
161
162impl AnyControlMessage {
163    /// Returns `true` if this is a CLIENT_SETUP or SERVER_SETUP message.
164    pub fn is_setup(&self) -> bool {
165        match self {
166            #[cfg(feature = "draft07")]
167            AnyControlMessage::Draft07(m) => matches!(
168                m,
169                crate::draft07::message::ControlMessage::ClientSetup(_)
170                    | crate::draft07::message::ControlMessage::ServerSetup(_)
171            ),
172            #[cfg(feature = "draft08")]
173            AnyControlMessage::Draft08(m) => matches!(
174                m,
175                crate::draft08::message::ControlMessage::ClientSetup(_)
176                    | crate::draft08::message::ControlMessage::ServerSetup(_)
177            ),
178            #[cfg(feature = "draft09")]
179            AnyControlMessage::Draft09(m) => matches!(
180                m,
181                crate::draft09::message::ControlMessage::ClientSetup(_)
182                    | crate::draft09::message::ControlMessage::ServerSetup(_)
183            ),
184            #[cfg(feature = "draft10")]
185            AnyControlMessage::Draft10(m) => matches!(
186                m,
187                crate::draft10::message::ControlMessage::ClientSetup(_)
188                    | crate::draft10::message::ControlMessage::ServerSetup(_)
189            ),
190            #[cfg(feature = "draft11")]
191            AnyControlMessage::Draft11(m) => matches!(
192                m,
193                crate::draft11::message::ControlMessage::ClientSetup(_)
194                    | crate::draft11::message::ControlMessage::ServerSetup(_)
195            ),
196            #[cfg(feature = "draft12")]
197            AnyControlMessage::Draft12(m) => matches!(
198                m,
199                crate::draft12::message::ControlMessage::ClientSetup(_)
200                    | crate::draft12::message::ControlMessage::ServerSetup(_)
201            ),
202            #[cfg(feature = "draft13")]
203            AnyControlMessage::Draft13(m) => matches!(
204                m,
205                crate::draft13::message::ControlMessage::ClientSetup(_)
206                    | crate::draft13::message::ControlMessage::ServerSetup(_)
207            ),
208            #[cfg(feature = "draft14")]
209            AnyControlMessage::Draft14(m) => matches!(
210                m,
211                crate::draft14::message::ControlMessage::ClientSetup(_)
212                    | crate::draft14::message::ControlMessage::ServerSetup(_)
213            ),
214            #[cfg(feature = "draft15")]
215            AnyControlMessage::Draft15(m) => matches!(
216                m,
217                crate::draft15::message::ControlMessage::ClientSetup(_)
218                    | crate::draft15::message::ControlMessage::ServerSetup(_)
219            ),
220            #[cfg(feature = "draft16")]
221            AnyControlMessage::Draft16(m) => matches!(
222                m,
223                crate::draft16::message::ControlMessage::ClientSetup(_)
224                    | crate::draft16::message::ControlMessage::ServerSetup(_)
225            ),
226            #[cfg(feature = "draft17")]
227            AnyControlMessage::Draft17(m) => {
228                matches!(m, crate::draft17::message::ControlMessage::Setup(_))
229            }
230            #[cfg(feature = "draft18")]
231            AnyControlMessage::Draft18(m) => {
232                matches!(m, crate::draft18::message::ControlMessage::Setup(_))
233            }
234            #[cfg(feature = "draft19")]
235            AnyControlMessage::Draft19(m) => {
236                matches!(m, crate::draft19::message::ControlMessage::Setup(_))
237            }
238            #[allow(unreachable_patterns)]
239            _ => false,
240        }
241    }
242
243    /// The Request ID and Group Order of a FETCH, on the drafts where the
244    /// FETCH settles the order by itself.
245    ///
246    /// A fetch response's Objects arrive in the order the request asked for.
247    /// Draft-19 Section 10.12.3: "The publisher responding to a FETCH is
248    /// responsible for delivering all available Objects in the requested
249    /// range in the requested order (see Section 10.2.8)." Draft-19 Section
250    /// 10.2.8 carries the order itself, as the GROUP_ORDER parameter, and states
251    /// what its absence means: "If omitted from FETCH, the receiver uses
252    /// Ascending (0x1)." So on those drafts one message answers the question
253    /// outright, whether or not it carries the parameter, and that is what
254    /// this returns.
255    ///
256    /// The answer matters most on drafts 18 and 19, whose fetch Objects write
257    /// a Group ID as a difference from the Object before and leave the order
258    /// to decide its sign — see
259    /// [`AnyFetchObjectReader::new`]. Drafts 15, 16 and 17
260    /// state the same rule about the same parameter and their fetch streams
261    /// resolve without it, so this answers for them too rather than for the
262    /// two that happen to need it.
263    ///
264    /// # What answers `None`
265    ///
266    /// Any message that is not a FETCH, and **every FETCH on drafts 07-14**.
267    /// Those drafts carry Group Order as a field of the FETCH rather than as
268    /// a parameter, and its value 0x0 means the subscriber expressed no
269    /// preference — which leaves the order to the publisher, who states it in
270    /// the FETCH_OK. That is a two-message negotiation, and a function handed
271    /// one message cannot answer it. Answering Ascending there would be a
272    /// guess wearing the same return type as a fact.
273    ///
274    /// Also `None` for a GROUP_ORDER value that is neither Ascending (0x1)
275    /// nor Descending (0x2), which drafts 15-19 make a session-closing
276    /// PROTOCOL_VIOLATION and this crate's decoder refuses before building a
277    /// message. Defensive, and deliberately not the Ascending default: an
278    /// out-of-range value is not an omitted one.
279    #[allow(unreachable_patterns, unused_variables)]
280    pub fn fetch_group_order(&self) -> Option<(u64, AnyFetchGroupOrder)> {
281        /// GROUP_ORDER, Parameter Type 0x22 on every draft that has it.
282        ///
283        /// Both of these go unused in a build compiling none of drafts 15-19,
284        /// which is the honest report: no draft in such a build carries a
285        /// fetch's Group Order as a parameter, so every arm below is gated
286        /// out and the match is the `None` arm alone.
287        #[allow(dead_code)]
288        const GROUP_ORDER: u64 = 0x22;
289
290        #[allow(dead_code)]
291        fn fetch_group_order(
292            request_id: crate::varint::VarInt,
293            parameters: &[crate::kvp::KeyValuePair],
294        ) -> Option<(u64, AnyFetchGroupOrder)> {
295            // The first, because drafts 15-19 refuse a repeated parameter
296            // before a message is built, so there is never a second.
297            let order = match parameters.iter().find(|p| p.key.into_inner() == GROUP_ORDER) {
298                None => AnyFetchGroupOrder::Ascending,
299                Some(p) => match &p.value {
300                    crate::kvp::KvpValue::Varint(v) => match v.into_inner() {
301                        0x1 => AnyFetchGroupOrder::Ascending,
302                        0x2 => AnyFetchGroupOrder::Descending,
303                        _ => return None,
304                    },
305                    // An even key type carries a varint, so this shape does
306                    // not survive decoding either.
307                    crate::kvp::KvpValue::Bytes(_) => return None,
308                },
309            };
310            Some((request_id.into_inner(), order))
311        }
312
313        match self {
314            #[cfg(feature = "draft15")]
315            AnyControlMessage::Draft15(crate::draft15::message::ControlMessage::Fetch(f)) => {
316                fetch_group_order(f.request_id, &f.parameters)
317            }
318            #[cfg(feature = "draft16")]
319            AnyControlMessage::Draft16(crate::draft16::message::ControlMessage::Fetch(f)) => {
320                fetch_group_order(f.request_id, &f.parameters)
321            }
322            #[cfg(feature = "draft17")]
323            AnyControlMessage::Draft17(crate::draft17::message::ControlMessage::Fetch(f)) => {
324                fetch_group_order(f.request_id, &f.parameters)
325            }
326            #[cfg(feature = "draft18")]
327            AnyControlMessage::Draft18(crate::draft18::message::ControlMessage::Fetch(f)) => {
328                fetch_group_order(f.request_id, &f.parameters)
329            }
330            #[cfg(feature = "draft19")]
331            AnyControlMessage::Draft19(crate::draft19::message::ControlMessage::Fetch(f)) => {
332                fetch_group_order(f.request_id, &f.parameters)
333            }
334            _ => None,
335        }
336    }
337}
338
339// ── Data stream headers ─────────────────────────────────────
340
341dispatch_enum! {
342    /// A subgroup header from any enabled draft.
343    #[derive(Debug, Clone)]
344    pub enum AnySubgroupHeader {
345        #[cfg(feature = "draft07")]
346        Draft07 => crate::draft07::data_stream::SubgroupHeader,
347        #[cfg(feature = "draft08")]
348        Draft08 => crate::draft08::data_stream::SubgroupHeader,
349        #[cfg(feature = "draft09")]
350        Draft09 => crate::draft09::data_stream::SubgroupHeader,
351        #[cfg(feature = "draft10")]
352        Draft10 => crate::draft10::data_stream::SubgroupHeader,
353        #[cfg(feature = "draft11")]
354        Draft11 => crate::draft11::data_stream::SubgroupHeader,
355        #[cfg(feature = "draft12")]
356        Draft12 => crate::draft12::data_stream::SubgroupHeader,
357        #[cfg(feature = "draft13")]
358        Draft13 => crate::draft13::data_stream::SubgroupHeader,
359        #[cfg(feature = "draft14")]
360        Draft14 => crate::draft14::data_stream::SubgroupHeader,
361        #[cfg(feature = "draft15")]
362        Draft15 => crate::draft15::data_stream::SubgroupHeader,
363        #[cfg(feature = "draft16")]
364        Draft16 => crate::draft16::data_stream::SubgroupHeader,
365        #[cfg(feature = "draft17")]
366        Draft17 => crate::draft17::data_stream::SubgroupHeader,
367        #[cfg(feature = "draft18")]
368        Draft18 => crate::draft18::data_stream::SubgroupHeader,
369        #[cfg(feature = "draft19")]
370        Draft19 => crate::draft19::data_stream::SubgroupHeader,
371    }
372    decode(decode);
373    encode(encode -> ());
374}
375
376impl AnySubgroupHeader {
377    /// Decode a subgroup stream header including its leading stream-type
378    /// field, for any enabled draft.
379    ///
380    /// Drafts 07-13 encode the stream type as a varint ahead of the header
381    /// body; drafts 14-19 fold it into the header itself. This entry point
382    /// hides that difference: callers hand it the stream's first byte onwards
383    /// and it consumes exactly the header, type field included.
384    ///
385    /// On drafts 11-13 the stream type also selects the header layout and
386    /// fixes whether objects carry extension headers, which
387    /// [`Self::decode`] cannot know; prefer this entry point whenever the
388    /// stream's first byte is available.
389    #[allow(unused_variables)]
390    pub fn decode_stream(version: DraftVersion, buf: &mut impl Buf) -> Result<Self, CodecError> {
391        match version {
392            #[cfg(feature = "draft07")]
393            DraftVersion::Draft07 => {
394                crate::draft07::data_stream::SubgroupHeader::decode_stream(buf)
395                    .map(AnySubgroupHeader::Draft07)
396            }
397            #[cfg(feature = "draft08")]
398            DraftVersion::Draft08 => {
399                crate::draft08::data_stream::SubgroupHeader::decode_stream(buf)
400                    .map(AnySubgroupHeader::Draft08)
401            }
402            #[cfg(feature = "draft09")]
403            DraftVersion::Draft09 => {
404                crate::draft09::data_stream::SubgroupHeader::decode_stream(buf)
405                    .map(AnySubgroupHeader::Draft09)
406            }
407            #[cfg(feature = "draft10")]
408            DraftVersion::Draft10 => {
409                crate::draft10::data_stream::SubgroupHeader::decode_stream(buf)
410                    .map(AnySubgroupHeader::Draft10)
411            }
412            #[cfg(feature = "draft11")]
413            DraftVersion::Draft11 => {
414                crate::draft11::data_stream::SubgroupHeader::decode_stream(buf)
415                    .map(AnySubgroupHeader::Draft11)
416            }
417            #[cfg(feature = "draft12")]
418            DraftVersion::Draft12 => {
419                crate::draft12::data_stream::SubgroupHeader::decode_stream(buf)
420                    .map(AnySubgroupHeader::Draft12)
421            }
422            #[cfg(feature = "draft13")]
423            DraftVersion::Draft13 => {
424                crate::draft13::data_stream::SubgroupHeader::decode_stream(buf)
425                    .map(AnySubgroupHeader::Draft13)
426            }
427            #[cfg(feature = "draft14")]
428            DraftVersion::Draft14 => crate::draft14::data_stream::SubgroupHeader::decode(buf)
429                .map(AnySubgroupHeader::Draft14),
430            #[cfg(feature = "draft15")]
431            DraftVersion::Draft15 => crate::draft15::data_stream::SubgroupHeader::decode(buf)
432                .map(AnySubgroupHeader::Draft15),
433            #[cfg(feature = "draft16")]
434            DraftVersion::Draft16 => crate::draft16::data_stream::SubgroupHeader::decode(buf)
435                .map(AnySubgroupHeader::Draft16),
436            #[cfg(feature = "draft17")]
437            DraftVersion::Draft17 => crate::draft17::data_stream::SubgroupHeader::decode(buf)
438                .map(AnySubgroupHeader::Draft17),
439            #[cfg(feature = "draft18")]
440            DraftVersion::Draft18 => crate::draft18::data_stream::SubgroupHeader::decode(buf)
441                .map(AnySubgroupHeader::Draft18),
442            #[cfg(feature = "draft19")]
443            DraftVersion::Draft19 => crate::draft19::data_stream::SubgroupHeader::decode(buf)
444                .map(AnySubgroupHeader::Draft19),
445            #[allow(unreachable_patterns)]
446            _ => Err(CodecError::UnsupportedDraft(format!(
447                "draft {version:?} not enabled via feature flag"
448            ))),
449        }
450    }
451
452    /// Encode a subgroup stream header including its leading stream-type
453    /// field, the inverse of [`Self::decode_stream`].
454    ///
455    /// [`Self::encode`] is not that inverse on drafts 07-13 and never was:
456    /// it writes the header body alone, so bytes written with it and read
457    /// back with [`Self::decode_stream`] lose their first field and shift
458    /// every field after it. Use this for a stream's first write and
459    /// [`Self::encode`] only once the stream is already open.
460    // A build with no draft feature compiles this match to no arms at all,
461    // which leaves the parameter read by nothing. That is the same shape
462    // `unreachable_code` is allowed for here, and it is a real configuration
463    // — CI checks it — rather than a hypothetical one.
464    #[allow(unreachable_code, unused_variables)]
465    pub fn encode_stream(&self, buf: &mut impl BufMut) {
466        match self {
467            #[cfg(feature = "draft07")]
468            AnySubgroupHeader::Draft07(h) => h.encode_stream(buf),
469            #[cfg(feature = "draft08")]
470            AnySubgroupHeader::Draft08(h) => h.encode_stream(buf),
471            #[cfg(feature = "draft09")]
472            AnySubgroupHeader::Draft09(h) => h.encode_stream(buf),
473            #[cfg(feature = "draft10")]
474            AnySubgroupHeader::Draft10(h) => h.encode_stream(buf),
475            #[cfg(feature = "draft11")]
476            AnySubgroupHeader::Draft11(h) => h.encode_stream(buf),
477            #[cfg(feature = "draft12")]
478            AnySubgroupHeader::Draft12(h) => h.encode_stream(buf),
479            #[cfg(feature = "draft13")]
480            AnySubgroupHeader::Draft13(h) => h.encode_stream(buf),
481            // Drafts 14-19 fold the stream type into the header, so their
482            // `encode` already writes it and `decode_stream` already reads
483            // it back.
484            #[cfg(feature = "draft14")]
485            AnySubgroupHeader::Draft14(h) => h.encode(buf),
486            #[cfg(feature = "draft15")]
487            AnySubgroupHeader::Draft15(h) => h.encode(buf),
488            #[cfg(feature = "draft16")]
489            AnySubgroupHeader::Draft16(h) => h.encode(buf),
490            #[cfg(feature = "draft17")]
491            AnySubgroupHeader::Draft17(h) => h.encode(buf),
492            #[cfg(feature = "draft18")]
493            AnySubgroupHeader::Draft18(h) => h.encode(buf),
494            #[cfg(feature = "draft19")]
495            AnySubgroupHeader::Draft19(h) => h.encode(buf),
496            #[allow(unreachable_patterns)]
497            _ => unreachable!("AnySubgroupHeader has no enabled variants"),
498        }
499    }
500
501    /// Encode the header body, refusing a value the stream type will not carry,
502    /// and write the type field in front of it.
503    ///
504    /// The checked form of [`Self::encode_stream`]. Every draft from 11 on has
505    /// a header type table with a column the value can disagree with - a
506    /// Subgroup ID the type does not write, an `Option` that does not match
507    /// what the type says is present - and disagreeing does not produce a
508    /// malformed stream. It produces a well-formed stream for a different
509    /// subgroup, or with a different priority, which the peer has no way to
510    /// question. Each draft's own `encode_checked` says no to that; this is the
511    /// one entry point that reaches all of them.
512    ///
513    /// Drafts 07 through 10 have nothing to refuse: their SUBGROUP_HEADER has
514    /// one shape, every field is written every time, and no type byte selects
515    /// between them. They are written unchanged.
516    ///
517    /// # Errors
518    ///
519    /// [`CodecError::InvalidField`] if the header's fields disagree with its
520    /// own type. A refused header leaves `buf` untouched.
521    #[allow(unreachable_code, unused_variables, unused_mut)]
522    pub fn encode_stream_checked(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
523        let mut body = Vec::with_capacity(32);
524        match self {
525            #[cfg(feature = "draft07")]
526            AnySubgroupHeader::Draft07(h) => h.encode_stream(&mut body),
527            #[cfg(feature = "draft08")]
528            AnySubgroupHeader::Draft08(h) => h.encode_stream(&mut body),
529            #[cfg(feature = "draft09")]
530            AnySubgroupHeader::Draft09(h) => h.encode_stream(&mut body),
531            #[cfg(feature = "draft10")]
532            AnySubgroupHeader::Draft10(h) => h.encode_stream(&mut body),
533            // Drafts 11-13 write the stream type ahead of a body their
534            // `encode_checked` produces on its own.
535            #[cfg(feature = "draft11")]
536            AnySubgroupHeader::Draft11(h) => {
537                crate::varint::VarInt::from_usize(h.stream_type as usize).encode(&mut body);
538                h.encode_checked(&mut body)?;
539            }
540            #[cfg(feature = "draft12")]
541            AnySubgroupHeader::Draft12(h) => {
542                crate::varint::VarInt::from_usize(h.stream_type as usize).encode(&mut body);
543                h.encode_checked(&mut body)?;
544            }
545            #[cfg(feature = "draft13")]
546            AnySubgroupHeader::Draft13(h) => {
547                crate::varint::VarInt::from_usize(h.stream_type as usize).encode(&mut body);
548                h.encode_checked(&mut body)?;
549            }
550            // Drafts 14-19 fold the type into the header, so their
551            // `encode_checked` already writes it.
552            #[cfg(feature = "draft14")]
553            AnySubgroupHeader::Draft14(h) => h.encode_checked(&mut body)?,
554            #[cfg(feature = "draft15")]
555            AnySubgroupHeader::Draft15(h) => h.encode_checked(&mut body)?,
556            #[cfg(feature = "draft16")]
557            AnySubgroupHeader::Draft16(h) => h.encode_checked(&mut body)?,
558            #[cfg(feature = "draft17")]
559            AnySubgroupHeader::Draft17(h) => h.encode_checked(&mut body)?,
560            #[cfg(feature = "draft18")]
561            AnySubgroupHeader::Draft18(h) => h.encode_checked(&mut body)?,
562            #[cfg(feature = "draft19")]
563            AnySubgroupHeader::Draft19(h) => h.encode_checked(&mut body)?,
564            #[allow(unreachable_patterns)]
565            _ => unreachable!("AnySubgroupHeader has no enabled variants"),
566        }
567        buf.put_slice(&body);
568        Ok(())
569    }
570
571    subgroup_header_accessor! {
572        /// The Track Alias every object on this stream belongs to.
573        track_alias -> u64;
574        [
575            Draft07 @ "draft07", Draft08 @ "draft08", Draft09 @ "draft09",
576            Draft10 @ "draft10", Draft11 @ "draft11", Draft12 @ "draft12",
577            Draft13 @ "draft13", Draft14 @ "draft14", Draft15 @ "draft15",
578            Draft16 @ "draft16", Draft17 @ "draft17", Draft18 @ "draft18",
579            Draft19 @ "draft19",
580        ] => |h| h.track_alias.into_inner(),
581    }
582
583    subgroup_header_accessor! {
584        /// The Group ID every object on this stream belongs to.
585        group_id -> u64;
586        [
587            Draft07 @ "draft07", Draft08 @ "draft08", Draft09 @ "draft09",
588            Draft10 @ "draft10", Draft11 @ "draft11", Draft12 @ "draft12",
589            Draft13 @ "draft13", Draft14 @ "draft14", Draft15 @ "draft15",
590            Draft16 @ "draft16", Draft17 @ "draft17", Draft18 @ "draft18",
591            Draft19 @ "draft19",
592        ] => |h| h.group_id.into_inner(),
593    }
594
595    subgroup_header_accessor! {
596        /// The Publisher Priority, or `None` when the header set a
597        /// default-priority flag and omitted the field (drafts 15+).
598        publisher_priority -> Option<u8>;
599        [
600            Draft07 @ "draft07", Draft08 @ "draft08", Draft09 @ "draft09",
601            Draft10 @ "draft10", Draft11 @ "draft11", Draft12 @ "draft12",
602            Draft13 @ "draft13", Draft14 @ "draft14",
603        ] => |h| Some(h.publisher_priority),
604        [
605            Draft15 @ "draft15", Draft16 @ "draft16", Draft17 @ "draft17",
606            Draft18 @ "draft18", Draft19 @ "draft19",
607        ] => |h| h.publisher_priority,
608    }
609
610    subgroup_header_accessor! {
611        /// The Subgroup ID this header fixes for its objects, or `None` when
612        /// the header does not determine one.
613        ///
614        /// `None` covers two cases. The first is the *subgroup ID is the first
615        /// object's ID* stream, which **every draft from 11 on** defines and
616        /// this codec never resolves — nine of the thirteen, not the eight
617        /// this said, and the miscount is worth naming because draft-15 spent
618        /// a long time excluded from lists elsewhere on exactly that reading.
619        /// The second is a header whose type the draft does not assign at all:
620        /// drafts 17-19 mode 3, and the same fourth combination of the `0x06`
621        /// bits on drafts 15 and 16. In every one of them the codec stores a
622        /// placeholder zero that a caller must not report.
623        ///
624        /// Imposes draft-14's `!has_subgroup_id_field()` guard uniformly. Every
625        /// per-draft accessor it reaches through now reads the Subgroup ID
626        /// carrier the way that draft's own decoder does, so there is no longer
627        /// a disagreement here for this accessor to paper over. Draft-16 used to
628        /// read its two mode bits one at a time and so reported a first-object
629        /// carrier for a Type whose mode is reserved, which made reaching for
630        /// its per-draft accessor directly a way to resolve such a header to the
631        /// wrong subgroup.
632        subgroup_id -> Option<u64>;
633        [
634            Draft07 @ "draft07", Draft08 @ "draft08", Draft09 @ "draft09",
635            Draft10 @ "draft10",
636        ] => |h| Some(h.subgroup_id.into_inner()),
637        [Draft11 @ "draft11"] => |h| {
638            use crate::draft11::data_stream::StreamType;
639            match h.stream_type {
640                StreamType::SubgroupFirstObj | StreamType::SubgroupFirstObjExt => None,
641                _ => Some(h.subgroup_id.into_inner()),
642            }
643        },
644        [Draft12 @ "draft12"] => |h| {
645            use crate::draft12::data_stream::StreamType;
646            match h.stream_type {
647                StreamType::SubgroupFirstObj
648                | StreamType::SubgroupFirstObjExt
649                | StreamType::SubgroupFirstObjEog
650                | StreamType::SubgroupFirstObjEogExt => None,
651                _ => Some(h.subgroup_id.into_inner()),
652            }
653        },
654        [Draft13 @ "draft13"] => |h| {
655            use crate::draft13::data_stream::StreamType;
656            match h.stream_type {
657                StreamType::SubgroupFirstObj
658                | StreamType::SubgroupFirstObjExt
659                | StreamType::SubgroupFirstObjEog
660                | StreamType::SubgroupFirstObjEogExt => None,
661                _ => Some(h.subgroup_id.into_inner()),
662            }
663        },
664        [Draft14 @ "draft14"] => |h| {
665            if h.stream_type.has_subgroup_id_field() {
666                Some(h.subgroup_id.map_or(0, |id| id.into_inner()))
667            } else if h.stream_type.subgroup_id_is_first_object() {
668                None
669            } else {
670                Some(0)
671            }
672        },
673        // Drafts 15 and 16 read the same three carriers out of the same two
674        // bits, so they share an answer — draft-16 naming them a
675        // SUBGROUP_ID_MODE and draft-15 giving them as a pair of table
676        // columns, which is a difference in wording and not in bytes.
677        //
678        // `None` is the first-object carrier: the ID is not on the wire and
679        // only the stream reader, which has seen the first object, can supply
680        // it. Answering `Some(0)` there — which the draft-15 arm used to do —
681        // collapses every first-object subgroup onto subgroup zero, and two
682        // subgroups of one group must never share a stream.
683        //
684        // `None` is also the fourth combination, which neither draft assigns:
685        // draft-16 reserves those type values by name, draft-15 reaches the
686        // same eight by leaving them out of Table 6. No such header decodes,
687        // so reaching this arm with one means a caller built it rather than
688        // read it, and that caller is the one this accessor exists to protect.
689        // `Some(0)` would hand it subgroup zero for a stream no draft defines;
690        // `None` says the header determines no Subgroup ID, which is true.
691        // Drafts 17-19 already answer `None` for their mode 3, so this is the
692        // same rule stated once for all five.
693        [Draft15 @ "draft15", Draft16 @ "draft16"] => |h| {
694            // The unassigned combination has to be tested somewhere. With the
695            // mode reserved neither carrier predicate answers `true`, so the
696            // fall-through would report subgroup zero for a stream no draft
697            // defines. It is tested first for legibility only: the ordering was
698            // load-bearing while draft-16 read its two mode bits one at a time
699            // and claimed an explicit Subgroup ID here, and is not any more.
700            if h.header_type & 0x06 == 0x06 {
701                None
702            } else if h.has_explicit_subgroup_id() {
703                Some(h.subgroup_id.into_inner())
704            } else if h.subgroup_id_from_first_object() {
705                None
706            } else {
707                Some(0)
708            }
709        },
710        [Draft17 @ "draft17", Draft18 @ "draft18", Draft19 @ "draft19"] => |h| {
711            match h.subgroup_id_mode() {
712                0 => Some(0),
713                2 => Some(h.subgroup_id.into_inner()),
714                // Mode 1 is *the first object's ID* and mode 3 is reserved; the
715                // decoder stores a placeholder zero for each.
716                _ => None,
717            }
718        },
719    }
720
721    subgroup_header_accessor! {
722        /// The two-bit subgroup-ID mode, on the five drafts that put one in
723        /// the header type, or `None` on the eight that do not.
724        ///
725        /// `0` = the header carries no subgroup ID and it is zero; `1` = the
726        /// subgroup ID is the first object's ID; `2` = an explicit ID
727        /// follows; `3` = the fourth combination, which no draft assigns.
728        ///
729        /// Exists because on those five drafts [`Self::subgroup_id`] returns
730        /// `None` for **both** mode 1 and mode 3 — the decoder stores a
731        /// placeholder zero for each — and the two mean different things to a
732        /// caller deciding whether an object may be elided. Without it,
733        /// eliding index 0 of a reserved-mode stream is indistinguishable
734        /// from eliding it on a stream whose subgroup ID the first object
735        /// defines.
736        ///
737        /// **Reported wherever that ambiguity exists, and that is what picks
738        /// the five.** Drafts 16 through 19 name a SUBGROUP_ID_MODE field;
739        /// draft-15 does not, and spells the same three carriers out as a
740        /// Subgroup ID Field Present column beside a Subgroup ID Value one,
741        /// reaching the fourth combination by leaving it out of the table
742        /// rather than by reserving it. That is a difference in wording and
743        /// not in bytes — same mask, same shift, same four values — so the
744        /// question this accessor asks has one answer on both. It is named
745        /// for the question and not for any draft's field, as
746        /// [`Self::carries_extension_block`] is, and answering it here adds
747        /// nothing to `draft15`, which goes on describing its own bits in its
748        /// own words.
749        ///
750        /// `None` on drafts 07 through 14 means the ambiguity is absent, not
751        /// the carrier. Drafts 07-10 always put the subgroup ID on the wire.
752        /// Drafts 11 through 14 give each carrier a stream type of its own and
753        /// assign every type they define, so [`Self::subgroup_id`] answers
754        /// `None` for the first-object carrier and for nothing else, and there
755        /// is no second reading for a mode to resolve.
756        subgroup_id_mode -> Option<u8>;
757        [
758            Draft07 @ "draft07", Draft08 @ "draft08", Draft09 @ "draft09",
759            Draft10 @ "draft10", Draft11 @ "draft11", Draft12 @ "draft12",
760            Draft13 @ "draft13", Draft14 @ "draft14",
761        ] => |_h| None,
762        // Read off the type byte rather than through a per-draft accessor.
763        // Draft-15 has no name for the field and gains no method for one;
764        // draft-16's would have exactly this caller. The mask is the same
765        // literal the arm two accessors above tests, and both headers expose
766        // `header_type` directly.
767        [Draft15 @ "draft15", Draft16 @ "draft16"] => |h| {
768            Some((h.header_type & 0x06) >> 1)
769        },
770        [Draft17 @ "draft17", Draft18 @ "draft18", Draft19 @ "draft19"] => |h| {
771            Some(h.subgroup_id_mode())
772        },
773    }
774
775    subgroup_header_accessor! {
776        /// Whether every object on this stream writes a length-prefixed
777        /// extension block — the field drafts 17-19 renamed Properties.
778        ///
779        /// A property of the *stream*, not of any object on it. The header's
780        /// type settles it once, and an object with nothing to put in the
781        /// block still writes a length of zero on a stream that carries one.
782        /// So a writer cannot work the answer out from the object in its hand,
783        /// and one that guesses puts a stream on the wire that no reader can
784        /// follow: the missing length is read out of the next field along, and
785        /// every object after it is misframed.
786        ///
787        /// Answered `false` on draft-07, which has no such block at all, and
788        /// `true` on drafts 08 through 10, where every object carries one and
789        /// no header type can say otherwise. From draft-11 on it is the
790        /// header's own answer.
791        ///
792        /// Exists because nothing else exposed it. `subgroup_id` and
793        /// `publisher_priority` report what the header *holds*; this reports
794        /// what the objects after it must *write*, and only the first kind was
795        /// reachable without matching on the concrete per-draft variant.
796        carries_extension_block -> bool;
797        [Draft07 @ "draft07"] => |_h| false,
798        [Draft08 @ "draft08", Draft09 @ "draft09", Draft10 @ "draft10"] => |_h| true,
799        [Draft11 @ "draft11", Draft12 @ "draft12", Draft13 @ "draft13"] => |h| {
800            h.stream_type.has_extensions()
801        },
802        [Draft14 @ "draft14"] => |h| h.stream_type.extensions_present(),
803        [Draft15 @ "draft15", Draft16 @ "draft16"] => |h| h.has_extensions(),
804        [Draft17 @ "draft17", Draft18 @ "draft18", Draft19 @ "draft19"] => |h| {
805            h.has_properties()
806        },
807    }
808}
809
810dispatch_enum! {
811    /// An object header from any enabled draft.
812    #[derive(Debug, Clone)]
813    pub enum AnyObjectHeader {
814        #[cfg(feature = "draft07")]
815        Draft07 => crate::draft07::data_stream::ObjectHeader,
816        #[cfg(feature = "draft08")]
817        Draft08 => crate::draft08::data_stream::ObjectHeader,
818        #[cfg(feature = "draft09")]
819        Draft09 => crate::draft09::data_stream::ObjectHeader,
820        #[cfg(feature = "draft10")]
821        Draft10 => crate::draft10::data_stream::ObjectHeader,
822        #[cfg(feature = "draft11")]
823        Draft11 => crate::draft11::data_stream::ObjectHeader,
824        #[cfg(feature = "draft12")]
825        Draft12 => crate::draft12::data_stream::ObjectHeader,
826        #[cfg(feature = "draft13")]
827        Draft13 => crate::draft13::data_stream::ObjectHeader,
828        // NOTE: drafts 14-19 have no standalone ObjectHeader — their
829        // subgroup objects are delta-encoded against the previous object
830        // on the stream. Use [`AnySubgroupObjectReader`], which covers
831        // every draft 07-19 and also consumes object payloads.
832    }
833    decode(decode);
834    encode(encode -> ());
835}
836
837dispatch_enum! {
838    /// A datagram header from any enabled draft.
839    ///
840    /// [`encode`](Self::encode) is fallible on every draft. It dispatches to
841    /// each draft's `DatagramHeader::encode_checked` (draft-14's
842    /// `DatagramObject::encode_checked`), which refuses a header whose Object
843    /// Status the framing it names cannot carry rather than writing the bytes
844    /// and dropping the status. Every draft 07-18 says "Any object with a
845    /// status code other than zero MUST have an empty payload"; draft-19
846    /// replaces that blanket rule with a per-status Payload column in the
847    /// Object Status registry of its Section 15.9. Either way there is no
848    /// datagram that states End of Group and carries a payload, so a value
849    /// asking for one is answered with [`CodecError::InvalidField`] and
850    /// nothing is written.
851    ///
852    /// The per-draft `encode` methods are unchanged and still infallible; they
853    /// take the framing the value names as the authority and silently discard
854    /// whatever does not fit it. Reach for one of those only when that is what
855    /// you want.
856    #[derive(Debug, Clone)]
857    pub enum AnyDatagramHeader {
858        #[cfg(feature = "draft07")]
859        Draft07 => crate::draft07::data_stream::Datagram,
860        #[cfg(feature = "draft08")]
861        Draft08 => crate::draft08::data_stream::Datagram,
862        #[cfg(feature = "draft09")]
863        Draft09 => crate::draft09::data_stream::Datagram,
864        #[cfg(feature = "draft10")]
865        Draft10 => crate::draft10::data_stream::Datagram,
866        #[cfg(feature = "draft11")]
867        Draft11 => crate::draft11::data_stream::Datagram,
868        #[cfg(feature = "draft12")]
869        Draft12 => crate::draft12::data_stream::Datagram,
870        #[cfg(feature = "draft13")]
871        Draft13 => crate::draft13::data_stream::Datagram,
872        #[cfg(feature = "draft14")]
873        Draft14 => crate::draft14::data_stream::DatagramObject,
874        #[cfg(feature = "draft15")]
875        Draft15 => crate::draft15::data_stream::DatagramHeader,
876        #[cfg(feature = "draft16")]
877        Draft16 => crate::draft16::data_stream::DatagramHeader,
878        #[cfg(feature = "draft17")]
879        Draft17 => crate::draft17::data_stream::DatagramHeader,
880        #[cfg(feature = "draft18")]
881        Draft18 => crate::draft18::data_stream::DatagramHeader,
882        #[cfg(feature = "draft19")]
883        Draft19 => crate::draft19::data_stream::DatagramHeader,
884    }
885    decode(decode);
886    encode(encode_checked -> Result<(), CodecError>);
887}
888
889/// One datagram's identity, resolved, without its payload.
890///
891/// The five fields a caller keys on, taken off whichever of the thirteen
892/// per-draft datagram shapes this value holds. Produced by
893/// [`AnyDatagramHeader::meta`], and the reason it exists is that the shapes
894/// disagree about far more than their field order: drafts 07 through 13 split
895/// a payload datagram and a status datagram into two structs, draft-14 merges
896/// them behind an optional status, and drafts 15 through 19 hang both the
897/// status and the priority off bits in a type byte.
898///
899/// Every field is a primitive, so keying on a datagram never means naming a
900/// per-draft codec type — the same contract
901/// [`AnySubgroupObjectMeta`] holds for a subgroup object.
902#[derive(Debug, Clone, Copy, PartialEq, Eq)]
903pub struct AnyDatagramMeta {
904    /// Track alias identifying the subscription this datagram answers.
905    pub track_alias: u64,
906    /// Group ID.
907    pub group_id: u64,
908    /// Object ID.
909    ///
910    /// Always a value, on every draft, including the six whose type byte can
911    /// leave the field off the wire. Drafts 14 through 19 give the omission a
912    /// meaning rather than making the field absent — draft-16 Section 10.3.1:
913    /// "The ZERO_OBJECT_ID bit (0x04) indicates when the Object ID field is
914    /// present. When set to 1, the Object ID field is omitted and the Object
915    /// ID is 0." So the zero behind an omitted field is the Object's ID and
916    /// not a placeholder standing in for one, which is the opposite of what a
917    /// fetch object's absent Subgroup ID means and is why this field is not an
918    /// `Option`.
919    pub object_id: u64,
920    /// Publisher priority, or `None` where the datagram states none.
921    ///
922    /// Absent only on drafts 15 through 19, whose type byte carries a
923    /// default-priority bit; an Object that leaves it clear takes the priority
924    /// the control message that established the subscription specified, which
925    /// is not on this datagram and not knowable from it. Drafts 07 through 14
926    /// always carry the field.
927    pub publisher_priority: Option<u8>,
928    /// The Object Status this datagram states, or `None` when it carries a
929    /// payload instead.
930    ///
931    /// The framing decides which, and each cohort frames it differently: a
932    /// declared payload length of zero on drafts 07 and 08, a separate status
933    /// datagram on 08 through 13, an optional field on 14, and a status bit in
934    /// the type byte from 15 on. Draft-08 appears in that list twice because it
935    /// states a status both ways — it kept draft-07's optional status field
936    /// under a zero payload length and added OBJECT_DATAGRAM_STATUS beside it,
937    /// and draft-09 is where the first of the two goes away. The code is always
938    /// one the draft assigns, because every draft's decoder refuses the values
939    /// it does not.
940    pub status: Option<u64>,
941}
942
943impl AnyDatagramHeader {
944    /// This datagram's identity, without its payload.
945    ///
946    /// One call in place of thirteen match arms. A caller that wants a track
947    /// alias, a Location or a priority off a datagram has otherwise to
948    /// destructure the concrete per-draft variant — and on drafts 07 through 13
949    /// to destructure again, because those carry a payload datagram and a
950    /// status datagram as two different structs behind one enum.
951    ///
952    /// See [`AnyDatagramMeta::object_id`] for the one field whose absence from
953    /// the wire is not an absence of the value.
954    #[allow(unreachable_patterns)]
955    pub fn meta(&self) -> AnyDatagramMeta {
956        /// Drafts 09 through 13, which are the same two-struct shape five
957        /// times: the payload form carries no status field at all, so the
958        /// enum arm is the whole of the answer.
959        ///
960        /// Gated on the five it serves. A build carrying none of them has no
961        /// caller for it, and an ungated definition would be a `-D warnings`
962        /// error on every such per-draft row rather than on the all-features
963        /// build a reviewer runs.
964        #[cfg(any(
965            feature = "draft09",
966            feature = "draft10",
967            feature = "draft11",
968            feature = "draft12",
969            feature = "draft13"
970        ))]
971        macro_rules! split_datagram {
972            ($module:ident, $value:expr) => {
973                match $value {
974                    crate::$module::data_stream::Datagram::Payload(h) => AnyDatagramMeta {
975                        track_alias: h.track_alias.into_inner(),
976                        group_id: h.group_id.into_inner(),
977                        object_id: h.object_id.into_inner(),
978                        publisher_priority: Some(h.publisher_priority),
979                        status: None,
980                    },
981                    crate::$module::data_stream::Datagram::Status(h) => AnyDatagramMeta {
982                        track_alias: h.track_alias.into_inner(),
983                        group_id: h.group_id.into_inner(),
984                        object_id: h.object_id.into_inner(),
985                        publisher_priority: Some(h.publisher_priority),
986                        status: Some(h.object_status.as_u64()),
987                    },
988                }
989            };
990        }
991
992        match self {
993            // Drafts 07 and 08 are the two that hang a status off a declared
994            // payload length of zero, so on both the payload form can state one
995            // and the enum arm is not the whole of the answer. Draft-07's
996            // OBJECT_DATAGRAM is `… Object Payload Length (i), [Object Status
997            // (i)], Object Payload (..)` and it is the only datagram that draft
998            // has; draft-08 keeps that layout and adds OBJECT_DATAGRAM_STATUS
999            // beside it, so it says the same thing two ways. Draft-09 dropped
1000            // both the length and the status from the payload form, which is
1001            // why every draft from there on can read the arm alone.
1002            #[cfg(feature = "draft07")]
1003            AnyDatagramHeader::Draft07(d) => {
1004                let states_status = d.is_status();
1005                match d {
1006                    crate::draft07::data_stream::Datagram::Payload(h) => AnyDatagramMeta {
1007                        track_alias: h.track_alias.into_inner(),
1008                        group_id: h.group_id.into_inner(),
1009                        object_id: h.object_id.into_inner(),
1010                        publisher_priority: Some(h.publisher_priority),
1011                        status: states_status.then(|| h.object_status.as_u64()),
1012                    },
1013                }
1014            }
1015            #[cfg(feature = "draft08")]
1016            AnyDatagramHeader::Draft08(d) => {
1017                let states_status = d.is_status();
1018                match d {
1019                    crate::draft08::data_stream::Datagram::Payload(h) => AnyDatagramMeta {
1020                        track_alias: h.track_alias.into_inner(),
1021                        group_id: h.group_id.into_inner(),
1022                        object_id: h.object_id.into_inner(),
1023                        publisher_priority: Some(h.publisher_priority),
1024                        status: states_status.then(|| h.object_status.as_u64()),
1025                    },
1026                    crate::draft08::data_stream::Datagram::Status(h) => AnyDatagramMeta {
1027                        track_alias: h.track_alias.into_inner(),
1028                        group_id: h.group_id.into_inner(),
1029                        object_id: h.object_id.into_inner(),
1030                        publisher_priority: Some(h.publisher_priority),
1031                        status: Some(h.object_status.as_u64()),
1032                    },
1033                }
1034            }
1035            #[cfg(feature = "draft09")]
1036            AnyDatagramHeader::Draft09(d) => split_datagram!(draft09, d),
1037            #[cfg(feature = "draft10")]
1038            AnyDatagramHeader::Draft10(d) => split_datagram!(draft10, d),
1039            #[cfg(feature = "draft11")]
1040            AnyDatagramHeader::Draft11(d) => split_datagram!(draft11, d),
1041            #[cfg(feature = "draft12")]
1042            AnyDatagramHeader::Draft12(d) => split_datagram!(draft12, d),
1043            #[cfg(feature = "draft13")]
1044            AnyDatagramHeader::Draft13(d) => split_datagram!(draft13, d),
1045            #[cfg(feature = "draft14")]
1046            AnyDatagramHeader::Draft14(d) => AnyDatagramMeta {
1047                track_alias: d.track_alias.into_inner(),
1048                group_id: d.group_id.into_inner(),
1049                object_id: d.object_id.into_inner(),
1050                publisher_priority: Some(d.publisher_priority),
1051                status: d.status.map(|s| s.as_u64()),
1052            },
1053            // Drafts 15 and 16 write the status field whenever the type byte's
1054            // status bit is set, and an unset value under a set bit encodes as
1055            // Normal — so the bit is the authority on presence and the field is
1056            // the authority on nothing else.
1057            #[cfg(feature = "draft15")]
1058            AnyDatagramHeader::Draft15(d) => AnyDatagramMeta {
1059                track_alias: d.track_alias.into_inner(),
1060                group_id: d.group_id.into_inner(),
1061                object_id: d.object_id.into_inner(),
1062                publisher_priority: d.publisher_priority,
1063                status: d.is_status().then(|| {
1064                    d.object_status.unwrap_or(crate::draft15::types::ObjectStatus::Normal).as_u64()
1065                }),
1066            },
1067            #[cfg(feature = "draft16")]
1068            AnyDatagramHeader::Draft16(d) => AnyDatagramMeta {
1069                track_alias: d.track_alias.into_inner(),
1070                group_id: d.group_id.into_inner(),
1071                object_id: d.object_id.into_inner(),
1072                publisher_priority: d.publisher_priority,
1073                status: d.is_status().then(|| {
1074                    d.object_status.unwrap_or(crate::draft16::types::ObjectStatus::Normal).as_u64()
1075                }),
1076            },
1077            // Drafts 17 through 19 resolve the same pair themselves.
1078            #[cfg(feature = "draft17")]
1079            AnyDatagramHeader::Draft17(d) => AnyDatagramMeta {
1080                track_alias: d.track_alias.into_inner(),
1081                group_id: d.group_id.into_inner(),
1082                object_id: d.object_id.into_inner(),
1083                publisher_priority: d.publisher_priority,
1084                status: d.has_status().then(|| d.status().as_u64()),
1085            },
1086            #[cfg(feature = "draft18")]
1087            AnyDatagramHeader::Draft18(d) => AnyDatagramMeta {
1088                track_alias: d.track_alias.into_inner(),
1089                group_id: d.group_id.into_inner(),
1090                object_id: d.object_id.into_inner(),
1091                publisher_priority: d.publisher_priority,
1092                status: d.has_status().then(|| d.status().as_u64()),
1093            },
1094            #[cfg(feature = "draft19")]
1095            AnyDatagramHeader::Draft19(d) => AnyDatagramMeta {
1096                track_alias: d.track_alias.into_inner(),
1097                group_id: d.group_id.into_inner(),
1098                object_id: d.object_id.into_inner(),
1099                publisher_priority: d.publisher_priority,
1100                status: d.has_status().then(|| d.status().as_u64()),
1101            },
1102            _ => unreachable!("AnyDatagramHeader has no enabled variants"),
1103        }
1104    }
1105
1106    /// Whether this datagram may carry a non-empty payload.
1107    ///
1108    /// Drafts 07 through 18 state one blanket rule — "Any object with a status
1109    /// code other than zero MUST have an empty payload" — and draft-19 replaces
1110    /// it with a Payload column in the Object Status registry of its Section
1111    /// 15.9, which grants a payload to the same one status the blanket rule
1112    /// did. The answer is therefore the same shape on all thirteen, and it is
1113    /// the framing that gives it: every draft either splits payload and status
1114    /// datagrams into separate types (08 through 14, and the type byte on 15
1115    /// and 16) or hangs the status off a declared length of zero (07), so a
1116    /// datagram that states a status is one that has no payload to carry.
1117    ///
1118    /// Note this asks what the framing *permits*, not what the value holds. A
1119    /// datagram permitted a payload may still carry none; a zero-length Normal
1120    /// object is legal everywhere.
1121    ///
1122    /// Before this, a caller had to match the concrete per-draft variant to ask
1123    /// at all, which is why the client carries thirteen arms to do it.
1124    #[allow(unreachable_patterns)]
1125    pub fn permits_payload(&self) -> bool {
1126        match self {
1127            #[cfg(feature = "draft07")]
1128            AnyDatagramHeader::Draft07(d) => !d.is_status(),
1129            #[cfg(feature = "draft08")]
1130            AnyDatagramHeader::Draft08(d) => !d.is_status(),
1131            #[cfg(feature = "draft09")]
1132            AnyDatagramHeader::Draft09(d) => !d.is_status(),
1133            #[cfg(feature = "draft10")]
1134            AnyDatagramHeader::Draft10(d) => !d.is_status(),
1135            #[cfg(feature = "draft11")]
1136            AnyDatagramHeader::Draft11(d) => !d.is_status(),
1137            #[cfg(feature = "draft12")]
1138            AnyDatagramHeader::Draft12(d) => !d.is_status(),
1139            #[cfg(feature = "draft13")]
1140            AnyDatagramHeader::Draft13(d) => !d.is_status(),
1141            #[cfg(feature = "draft14")]
1142            AnyDatagramHeader::Draft14(d) => !d.datagram_type.is_status(),
1143            #[cfg(feature = "draft15")]
1144            AnyDatagramHeader::Draft15(d) => !d.is_status(),
1145            #[cfg(feature = "draft16")]
1146            AnyDatagramHeader::Draft16(d) => !d.is_status(),
1147            // Drafts 17-19 answer the per-status question directly, which on 19
1148            // is the registry column rather than the blanket rule.
1149            #[cfg(feature = "draft17")]
1150            AnyDatagramHeader::Draft17(d) => d.permits_payload(),
1151            #[cfg(feature = "draft18")]
1152            AnyDatagramHeader::Draft18(d) => d.permits_payload(),
1153            #[cfg(feature = "draft19")]
1154            AnyDatagramHeader::Draft19(d) => d.permits_payload(),
1155            _ => unreachable!("AnyDatagramHeader has no enabled variants"),
1156        }
1157    }
1158
1159    /// Whether this datagram's status is allowed to carry the extension headers
1160    /// it has, or `None` where the draft states no such rule.
1161    ///
1162    /// The rule enters the specification twice, in two different widths, and a
1163    /// draft-neutral caller must not apply either one outside its range:
1164    ///
1165    /// - **Drafts 07 through 10 state nothing.** Draft-07's datagram has no
1166    ///   extension block at all, and drafts 08, 09 and 10 have one with no rule
1167    ///   attached. These answer `None` rather than `true`, because "permitted"
1168    ///   would imply a rule was consulted.
1169    /// - **Drafts 11 through 14 state the narrow form**, in the section naming
1170    ///   the Object Extension Header: "Any Object may have extension headers
1171    ///   except those with Object Status 'Object Does Not Exist'." One status,
1172    ///   and End of Group and End of Track may carry extensions freely.
1173    /// - **Drafts 15 through 19 state the general form**: "Any Object with
1174    ///   status Normal can have extension headers. If an endpoint receives
1175    ///   extension headers on Objects with status that is not Normal, it MUST
1176    ///   close the session with a PROTOCOL_VIOLATION." Draft-16 also dropped
1177    ///   the Object Does Not Exist status, so the narrow form's subject no
1178    ///   longer exists there.
1179    ///
1180    /// Drafts 17 and later call the block Properties rather than Extensions;
1181    /// the name here follows [`AnySubgroupObject::extension_headers`], which
1182    /// spans the same rename.
1183    ///
1184    /// This reports rather than refuses, on all thirteen. A frame carrying
1185    /// extensions beside a status is well formed — every length is honest and
1186    /// every field parses — so a decoder hands it back intact and a tool that
1187    /// reproduces a capture can re-emit it. Refusing on decode would make a
1188    /// captured violation unreadable, which loses the one artifact anybody
1189    /// debugging it needs.
1190    #[allow(unreachable_patterns)]
1191    pub fn extensions_permitted(&self) -> Option<bool> {
1192        match self {
1193            // No rule stated: see above.
1194            #[cfg(feature = "draft07")]
1195            AnyDatagramHeader::Draft07(_) => None,
1196            #[cfg(feature = "draft08")]
1197            AnyDatagramHeader::Draft08(_) => None,
1198            #[cfg(feature = "draft09")]
1199            AnyDatagramHeader::Draft09(_) => None,
1200            #[cfg(feature = "draft10")]
1201            AnyDatagramHeader::Draft10(_) => None,
1202            // The narrow form. A payload datagram's status is Normal, so only
1203            // the status form can state the violation.
1204            #[cfg(feature = "draft11")]
1205            AnyDatagramHeader::Draft11(d) => Some(match d {
1206                crate::draft11::data_stream::Datagram::Payload(_) => true,
1207                crate::draft11::data_stream::Datagram::Status(s) => {
1208                    s.extensions.is_empty()
1209                        || s.object_status
1210                            != crate::draft11::types::ObjectStatus::ObjectDoesNotExist
1211                }
1212            }),
1213            #[cfg(feature = "draft12")]
1214            AnyDatagramHeader::Draft12(d) => Some(match d {
1215                crate::draft12::data_stream::Datagram::Payload(_) => true,
1216                crate::draft12::data_stream::Datagram::Status(s) => {
1217                    s.extensions.is_empty()
1218                        || s.object_status
1219                            != crate::draft12::types::ObjectStatus::ObjectDoesNotExist
1220                }
1221            }),
1222            #[cfg(feature = "draft13")]
1223            AnyDatagramHeader::Draft13(d) => Some(match d {
1224                crate::draft13::data_stream::Datagram::Payload(_) => true,
1225                crate::draft13::data_stream::Datagram::Status(s) => {
1226                    s.extensions.is_empty()
1227                        || s.object_status
1228                            != crate::draft13::types::ObjectStatus::ObjectDoesNotExist
1229                }
1230            }),
1231            // Draft-14 folds both forms into one value, so an absent status
1232            // means Normal rather than *no status field here*.
1233            #[cfg(feature = "draft14")]
1234            AnyDatagramHeader::Draft14(d) => Some(
1235                d.extension_headers.is_empty()
1236                    || d.status != Some(crate::draft14::types::ObjectStatus::ObjectDoesNotExist),
1237            ),
1238            // The general form, already answered per draft.
1239            #[cfg(feature = "draft15")]
1240            AnyDatagramHeader::Draft15(d) => Some(d.extensions_permitted()),
1241            #[cfg(feature = "draft16")]
1242            AnyDatagramHeader::Draft16(d) => Some(d.extensions_permitted()),
1243            #[cfg(feature = "draft17")]
1244            AnyDatagramHeader::Draft17(d) => Some(d.properties_permitted()),
1245            #[cfg(feature = "draft18")]
1246            AnyDatagramHeader::Draft18(d) => Some(d.properties_permitted()),
1247            #[cfg(feature = "draft19")]
1248            AnyDatagramHeader::Draft19(d) => Some(d.properties_permitted()),
1249            _ => unreachable!("AnyDatagramHeader has no enabled variants"),
1250        }
1251    }
1252}
1253
1254dispatch_enum! {
1255    /// A fetch header from any enabled draft.
1256    ///
1257    /// Note: Header structure varies significantly across drafts.
1258    /// Draft-07 has a minimal fetch header, Draft-14 has a full header.
1259    #[derive(Debug, Clone)]
1260    pub enum AnyFetchHeader {
1261        #[cfg(feature = "draft07")]
1262        Draft07 => crate::draft07::data_stream::FetchHeader,
1263        #[cfg(feature = "draft08")]
1264        Draft08 => crate::draft08::data_stream::FetchHeader,
1265        #[cfg(feature = "draft09")]
1266        Draft09 => crate::draft09::data_stream::FetchHeader,
1267        #[cfg(feature = "draft10")]
1268        Draft10 => crate::draft10::data_stream::FetchHeader,
1269        #[cfg(feature = "draft11")]
1270        Draft11 => crate::draft11::data_stream::FetchHeader,
1271        #[cfg(feature = "draft12")]
1272        Draft12 => crate::draft12::data_stream::FetchHeader,
1273        #[cfg(feature = "draft13")]
1274        Draft13 => crate::draft13::data_stream::FetchHeader,
1275        #[cfg(feature = "draft14")]
1276        Draft14 => crate::draft14::data_stream::FetchHeader,
1277        #[cfg(feature = "draft15")]
1278        Draft15 => crate::draft15::data_stream::FetchHeader,
1279        #[cfg(feature = "draft16")]
1280        Draft16 => crate::draft16::data_stream::FetchHeader,
1281        #[cfg(feature = "draft17")]
1282        Draft17 => crate::draft17::data_stream::FetchHeader,
1283        #[cfg(feature = "draft18")]
1284        Draft18 => crate::draft18::data_stream::FetchHeader,
1285        #[cfg(feature = "draft19")]
1286        Draft19 => crate::draft19::data_stream::FetchHeader,
1287    }
1288    decode(decode);
1289    encode(encode -> ());
1290}
1291
1292impl AnyFetchHeader {
1293    /// The id of the request this fetch stream answers.
1294    ///
1295    /// Every draft puts it in the header and nothing else: drafts 07-10 call
1296    /// it the Subscribe ID and drafts 11-19 the Request ID, and it names the
1297    /// request the publisher is responding to either way. Draft-19 Section
1298    /// 11.4.4: "When a stream begins with FETCH_HEADER, all objects on the
1299    /// stream belong to the track requested in the Fetch message identified by
1300    /// Request ID."
1301    ///
1302    /// It is what ties a fetch data stream back to the control exchange that
1303    /// opened it, which is the only route by which anything the stream does
1304    /// not state — on drafts 18 and 19, the Group Order its Group ID Deltas
1305    /// resolve against — can reach a reader.
1306    #[allow(unreachable_patterns)]
1307    pub fn request_id(&self) -> u64 {
1308        match self {
1309            #[cfg(feature = "draft07")]
1310            AnyFetchHeader::Draft07(h) => h.subscribe_id.into_inner(),
1311            #[cfg(feature = "draft08")]
1312            AnyFetchHeader::Draft08(h) => h.subscribe_id.into_inner(),
1313            #[cfg(feature = "draft09")]
1314            AnyFetchHeader::Draft09(h) => h.subscribe_id.into_inner(),
1315            #[cfg(feature = "draft10")]
1316            AnyFetchHeader::Draft10(h) => h.subscribe_id.into_inner(),
1317            #[cfg(feature = "draft11")]
1318            AnyFetchHeader::Draft11(h) => h.request_id.into_inner(),
1319            #[cfg(feature = "draft12")]
1320            AnyFetchHeader::Draft12(h) => h.request_id.into_inner(),
1321            #[cfg(feature = "draft13")]
1322            AnyFetchHeader::Draft13(h) => h.request_id.into_inner(),
1323            #[cfg(feature = "draft14")]
1324            AnyFetchHeader::Draft14(h) => h.request_id.into_inner(),
1325            #[cfg(feature = "draft15")]
1326            AnyFetchHeader::Draft15(h) => h.request_id.into_inner(),
1327            #[cfg(feature = "draft16")]
1328            AnyFetchHeader::Draft16(h) => h.request_id.into_inner(),
1329            #[cfg(feature = "draft17")]
1330            AnyFetchHeader::Draft17(h) => h.request_id.into_inner(),
1331            #[cfg(feature = "draft18")]
1332            AnyFetchHeader::Draft18(h) => h.request_id.into_inner(),
1333            #[cfg(feature = "draft19")]
1334            AnyFetchHeader::Draft19(h) => h.request_id.into_inner(),
1335            _ => unreachable!("AnyFetchHeader has no enabled variants"),
1336        }
1337    }
1338
1339    /// As [`AnySubgroupHeader::encode_stream`], for fetch streams.
1340    ///
1341    /// Fetch was the carrier this pair was missing: `decode_stream` has
1342    /// existed here all along with nothing on the other side of it, so a
1343    /// fetch stream the codec wrote could not be read back by the codec.
1344    #[allow(unreachable_code, unused_variables)]
1345    pub fn encode_stream(&self, buf: &mut impl BufMut) {
1346        match self {
1347            #[cfg(feature = "draft07")]
1348            AnyFetchHeader::Draft07(h) => h.encode_stream(buf),
1349            #[cfg(feature = "draft08")]
1350            AnyFetchHeader::Draft08(h) => h.encode_stream(buf),
1351            #[cfg(feature = "draft09")]
1352            AnyFetchHeader::Draft09(h) => h.encode_stream(buf),
1353            #[cfg(feature = "draft10")]
1354            AnyFetchHeader::Draft10(h) => h.encode_stream(buf),
1355            #[cfg(feature = "draft11")]
1356            AnyFetchHeader::Draft11(h) => h.encode_stream(buf),
1357            #[cfg(feature = "draft12")]
1358            AnyFetchHeader::Draft12(h) => h.encode_stream(buf),
1359            #[cfg(feature = "draft13")]
1360            AnyFetchHeader::Draft13(h) => h.encode_stream(buf),
1361            // Drafts 14-19 fold the stream type into the header, so their
1362            // `encode` already writes it and `decode_stream` already reads
1363            // it back.
1364            #[cfg(feature = "draft14")]
1365            AnyFetchHeader::Draft14(h) => h.encode(buf),
1366            #[cfg(feature = "draft15")]
1367            AnyFetchHeader::Draft15(h) => h.encode(buf),
1368            #[cfg(feature = "draft16")]
1369            AnyFetchHeader::Draft16(h) => h.encode(buf),
1370            #[cfg(feature = "draft17")]
1371            AnyFetchHeader::Draft17(h) => h.encode(buf),
1372            #[cfg(feature = "draft18")]
1373            AnyFetchHeader::Draft18(h) => h.encode(buf),
1374            #[cfg(feature = "draft19")]
1375            AnyFetchHeader::Draft19(h) => h.encode(buf),
1376            #[allow(unreachable_patterns)]
1377            _ => unreachable!("AnyFetchHeader has no enabled variants"),
1378        }
1379    }
1380
1381    /// As [`AnySubgroupHeader::decode_stream`], for fetch streams.
1382    #[allow(unused_variables)]
1383    pub fn decode_stream(version: DraftVersion, buf: &mut impl Buf) -> Result<Self, CodecError> {
1384        match version {
1385            #[cfg(feature = "draft07")]
1386            DraftVersion::Draft07 => crate::draft07::data_stream::FetchHeader::decode_stream(buf)
1387                .map(AnyFetchHeader::Draft07),
1388            #[cfg(feature = "draft08")]
1389            DraftVersion::Draft08 => crate::draft08::data_stream::FetchHeader::decode_stream(buf)
1390                .map(AnyFetchHeader::Draft08),
1391            #[cfg(feature = "draft09")]
1392            DraftVersion::Draft09 => crate::draft09::data_stream::FetchHeader::decode_stream(buf)
1393                .map(AnyFetchHeader::Draft09),
1394            #[cfg(feature = "draft10")]
1395            DraftVersion::Draft10 => crate::draft10::data_stream::FetchHeader::decode_stream(buf)
1396                .map(AnyFetchHeader::Draft10),
1397            #[cfg(feature = "draft11")]
1398            DraftVersion::Draft11 => crate::draft11::data_stream::FetchHeader::decode_stream(buf)
1399                .map(AnyFetchHeader::Draft11),
1400            #[cfg(feature = "draft12")]
1401            DraftVersion::Draft12 => crate::draft12::data_stream::FetchHeader::decode_stream(buf)
1402                .map(AnyFetchHeader::Draft12),
1403            #[cfg(feature = "draft13")]
1404            DraftVersion::Draft13 => crate::draft13::data_stream::FetchHeader::decode_stream(buf)
1405                .map(AnyFetchHeader::Draft13),
1406            #[cfg(feature = "draft14")]
1407            DraftVersion::Draft14 => {
1408                crate::draft14::data_stream::FetchHeader::decode(buf).map(AnyFetchHeader::Draft14)
1409            }
1410            #[cfg(feature = "draft15")]
1411            DraftVersion::Draft15 => {
1412                crate::draft15::data_stream::FetchHeader::decode(buf).map(AnyFetchHeader::Draft15)
1413            }
1414            #[cfg(feature = "draft16")]
1415            DraftVersion::Draft16 => {
1416                crate::draft16::data_stream::FetchHeader::decode(buf).map(AnyFetchHeader::Draft16)
1417            }
1418            #[cfg(feature = "draft17")]
1419            DraftVersion::Draft17 => {
1420                crate::draft17::data_stream::FetchHeader::decode(buf).map(AnyFetchHeader::Draft17)
1421            }
1422            #[cfg(feature = "draft18")]
1423            DraftVersion::Draft18 => {
1424                crate::draft18::data_stream::FetchHeader::decode(buf).map(AnyFetchHeader::Draft18)
1425            }
1426            #[cfg(feature = "draft19")]
1427            DraftVersion::Draft19 => {
1428                crate::draft19::data_stream::FetchHeader::decode(buf).map(AnyFetchHeader::Draft19)
1429            }
1430            #[allow(unreachable_patterns)]
1431            _ => Err(CodecError::UnsupportedDraft(format!(
1432                "draft {version:?} not enabled via feature flag"
1433            ))),
1434        }
1435    }
1436}
1437
1438// The one test below drives drafts 07, 12 and 18, each standing for one of the
1439// three framing shapes. Under a feature set naming none of them every arm
1440// compiles away, leaving the import with no user, so the module is gated on the
1441// same three rather than on `test` alone.
1442#[cfg(all(test, any(feature = "draft07", feature = "draft12", feature = "draft18")))]
1443mod tests {
1444    use super::*;
1445
1446    /// The draft-neutral entry point carries each draft's refusal out to the
1447    /// caller instead of resolving it the way the per-draft `encode` does.
1448    ///
1449    /// This is what changed for a caller holding an [`AnyDatagramHeader`]:
1450    /// [`AnyDatagramHeader::encode`] used to return `()` on all thirteen
1451    /// drafts, so a header whose Object Status its framing could not carry went
1452    /// out with the status quietly removed. It now dispatches to each draft's
1453    /// `encode_checked` and answers [`CodecError::InvalidField`] without
1454    /// writing a byte.
1455    ///
1456    /// Three drafts are driven here, one per shape the thirteen fall into.
1457    /// Draft-07 hangs the status field off a zero Object Payload Length;
1458    /// draft-18 hangs it off the STATUS bit in the type byte; draft-12 has no
1459    /// status field on this message at all, its statuses travelling on a
1460    /// separate OBJECT_DATAGRAM_STATUS, and so must keep accepting every header
1461    /// a publisher may send. A build with only some drafts enabled compiles
1462    /// only the arms it has.
1463    ///
1464    /// # What this catches, observed by making the change and running it
1465    ///
1466    /// Dropping the check from draft-07's `DatagramHeader::encode_checked`, so
1467    /// the dispatch layer has nothing to carry out:
1468    ///
1469    /// ```text
1470    /// draft-07 must refuse a status its framing cannot carry; got Ok(())
1471    /// ```
1472    #[test]
1473    fn any_datagram_header_encode_refuses_what_the_framing_cannot_carry() {
1474        #[cfg(feature = "draft07")]
1475        {
1476            let header =
1477                AnyDatagramHeader::Draft07(crate::draft07::data_stream::Datagram::Payload(
1478                    crate::draft07::data_stream::DatagramHeader {
1479                        track_alias: crate::varint::VarInt::from_usize(1),
1480                        group_id: crate::varint::VarInt::from_usize(0),
1481                        object_id: crate::varint::VarInt::from_usize(0),
1482                        publisher_priority: 128,
1483                        object_status: crate::draft07::types::ObjectStatus::EndOfGroup,
1484                        payload_length: crate::varint::VarInt::from_usize(4),
1485                    },
1486                ));
1487            let mut buf = Vec::new();
1488            let result = header.encode(&mut buf);
1489            assert!(
1490                matches!(result, Err(CodecError::InvalidField)),
1491                "draft-07 must refuse a status its framing cannot carry; got {result:?}"
1492            );
1493            assert!(buf.is_empty(), "draft-07 wrote {buf:?} for a header it refused");
1494        }
1495
1496        #[cfg(feature = "draft18")]
1497        {
1498            let header = AnyDatagramHeader::Draft18(crate::draft18::data_stream::DatagramHeader {
1499                // Type 0x00: every flag clear, so the STATUS bit is clear and
1500                // a payload follows the header.
1501                datagram_type: 0x00,
1502                track_alias: crate::varint::VarInt::from_usize(1),
1503                group_id: crate::varint::VarInt::from_usize(0),
1504                object_id: crate::varint::VarInt::from_usize(0),
1505                publisher_priority: Some(128),
1506                properties: Vec::new(),
1507                object_status: Some(crate::draft18::types::ObjectStatus::EndOfGroup),
1508            });
1509            let mut buf = Vec::new();
1510            let result = header.encode(&mut buf);
1511            assert!(
1512                matches!(result, Err(CodecError::InvalidField)),
1513                "draft-18 must refuse a status its framing cannot carry; got {result:?}"
1514            );
1515            assert!(buf.is_empty(), "draft-18 wrote {buf:?} for a header it refused");
1516        }
1517
1518        #[cfg(feature = "draft12")]
1519        {
1520            let header =
1521                AnyDatagramHeader::Draft12(crate::draft12::data_stream::Datagram::Payload(
1522                    crate::draft12::data_stream::DatagramHeader {
1523                        track_alias: crate::varint::VarInt::from_usize(1),
1524                        group_id: crate::varint::VarInt::from_usize(0),
1525                        object_id: crate::varint::VarInt::from_usize(7),
1526                        publisher_priority: 128,
1527                        extension_headers_length: crate::varint::VarInt::from_usize(0),
1528                        extensions: Vec::new(),
1529                        end_of_group: false,
1530                    },
1531                ));
1532            let mut buf = Vec::new();
1533            header
1534                .encode(&mut buf)
1535                .expect("draft-12's payload datagram carries no status to refuse");
1536            let mut cursor = &buf[..];
1537            let decoded = AnyDatagramHeader::decode(DraftVersion::Draft12, &mut cursor)
1538                .expect("the bytes the dispatch layer wrote must parse back");
1539            assert_eq!(decoded.draft(), DraftVersion::Draft12);
1540            assert!(!cursor.has_remaining(), "draft-12 left {cursor:?} unread");
1541        }
1542    }
1543
1544    /// The draft-neutral predicates answer the two questions that previously
1545    /// required matching the concrete per-draft variant.
1546    ///
1547    /// The same three drafts stand for the three eras of the extensions rule.
1548    /// Draft-07 has no extension block and no rule, and must answer `None`
1549    /// rather than `true` — reporting "permitted" would claim a rule was
1550    /// consulted. Draft-12 states the narrow form, so an extension block is a
1551    /// violation beside Object Does Not Exist and legal beside End of Group.
1552    /// Draft-18 states the general form, where both are violations.
1553    ///
1554    /// # What this catches, observed by making the change and running it
1555    ///
1556    /// Widening draft-12's arm to the general form, by comparing its status
1557    /// against `Normal` instead of against `ObjectDoesNotExist`:
1558    ///
1559    /// ```text
1560    /// draft-12 states the narrow form, which leaves End of Group free to
1561    /// carry extensions: expected Some(true), got Some(false)
1562    /// ```
1563    #[test]
1564    fn any_datagram_header_reports_payload_and_extension_permission() {
1565        #[cfg(feature = "draft07")]
1566        {
1567            let status =
1568                AnyDatagramHeader::Draft07(crate::draft07::data_stream::Datagram::Payload(
1569                    crate::draft07::data_stream::DatagramHeader {
1570                        track_alias: crate::varint::VarInt::from_usize(1),
1571                        group_id: crate::varint::VarInt::from_usize(0),
1572                        object_id: crate::varint::VarInt::from_usize(0),
1573                        publisher_priority: 128,
1574                        object_status: crate::draft07::types::ObjectStatus::EndOfGroup,
1575                        // Draft-07 has one datagram layout and hangs the status
1576                        // off a zero length, so this is what makes it a status.
1577                        payload_length: crate::varint::VarInt::from_usize(0),
1578                    },
1579                ));
1580            assert!(
1581                !status.permits_payload(),
1582                "draft-07 declares no payload bytes, so it may not carry any",
1583            );
1584            assert_eq!(
1585                status.extensions_permitted(),
1586                None,
1587                "draft-07 has no extension block and states no rule about one",
1588            );
1589        }
1590
1591        #[cfg(feature = "draft12")]
1592        {
1593            let with_extensions = |object_status| {
1594                AnyDatagramHeader::Draft12(crate::draft12::data_stream::Datagram::Status(
1595                    crate::draft12::data_stream::DatagramStatusHeader {
1596                        track_alias: crate::varint::VarInt::from_usize(1),
1597                        group_id: crate::varint::VarInt::from_usize(0),
1598                        object_id: crate::varint::VarInt::from_usize(0),
1599                        publisher_priority: 128,
1600                        extension_headers_length: crate::varint::VarInt::from_usize(2),
1601                        extensions: vec![0x3c, 0x01],
1602                        object_status,
1603                    },
1604                ))
1605            };
1606
1607            let absent = with_extensions(crate::draft12::types::ObjectStatus::ObjectDoesNotExist);
1608            assert!(!absent.permits_payload(), "a status datagram carries no payload");
1609            assert_eq!(
1610                absent.extensions_permitted(),
1611                Some(false),
1612                "Object Does Not Exist is the one status draft-12 bars extensions from",
1613            );
1614
1615            let end_of_group = with_extensions(crate::draft12::types::ObjectStatus::EndOfGroup);
1616            assert_eq!(
1617                end_of_group.extensions_permitted(),
1618                Some(true),
1619                "draft-12 states the narrow form, which leaves End of Group free to \
1620                 carry extensions: expected Some(true), got {:?}",
1621                end_of_group.extensions_permitted(),
1622            );
1623        }
1624
1625        #[cfg(feature = "draft18")]
1626        {
1627            // Type 0x21: the STATUS bit and the properties bit both set.
1628            let header = |object_status| {
1629                AnyDatagramHeader::Draft18(crate::draft18::data_stream::DatagramHeader {
1630                    datagram_type: 0x21,
1631                    track_alias: crate::varint::VarInt::from_usize(1),
1632                    group_id: crate::varint::VarInt::from_usize(0),
1633                    object_id: crate::varint::VarInt::from_usize(0),
1634                    publisher_priority: Some(128),
1635                    properties: vec![0x3c, 0x01],
1636                    object_status: Some(object_status),
1637                })
1638            };
1639
1640            let end_of_group = header(crate::draft18::types::ObjectStatus::EndOfGroup);
1641            assert!(!end_of_group.permits_payload(), "a status datagram carries no payload");
1642            assert_eq!(
1643                end_of_group.extensions_permitted(),
1644                Some(false),
1645                "draft-18 states the general form, which bars properties beside any \
1646                 status that is not Normal",
1647            );
1648        }
1649    }
1650}