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