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