moqtap_proxy/framer.rs
1//! Byte-exact object framing for MoQT data streams.
2//!
3//! [`ObjectFramer`] turns the raw bytes of one unidirectional data stream
4//! into individually addressable objects without altering them: every item
5//! it yields is a slice of the bytes that were fed in, and concatenating
6//! those items reproduces the stream exactly.
7//!
8//! Framing is opt-in because it costs latency and memory — an object is
9//! only emitted once it is buffered whole. The proxy pays that cost only
10//! when something is observing; see `session::pipe_data`.
11
12use std::collections::HashMap;
13use std::sync::{Arc, Mutex};
14
15use bytes::{Buf, Bytes, BytesMut};
16
17use moqtap_codec::dispatch::{
18 reemit_subgroup_object, AnyFetchFrame, AnyFetchGroupOrder, AnyFetchHeader, AnyFetchObjectMeta,
19 AnyFetchObjectReader, AnyFetchObjectWriter, AnySubgroupHeader, AnySubgroupObjectMeta,
20 AnySubgroupObjectReader, FetchReemit,
21};
22use moqtap_codec::version::DraftVersion;
23
24use crate::capability::fetch_group_order_is_needed;
25use crate::event::DataStreamHeaderKind;
26use crate::instrument::Recorder;
27use crate::parser::data::is_incomplete_error;
28use crate::types::DataStreamType;
29
30pub use crate::types::{BypassReason, ObjectMeta};
31
32/// Every fetch this session has learned a Group Order for, keyed by the
33/// Request ID that names it on both the control plane and the data stream.
34///
35/// # Why a fetch stream needs something from outside itself
36///
37/// Drafts 18, 19 and 20 write a fetch Object's Group ID as a difference from the
38/// Object before it, and the fetch's Group Order decides whether the
39/// difference counts up or down — draft-19 Section 11.4.4.1. Nothing on the
40/// data stream states the order, so a framer handed only the stream cannot
41/// reach an absolute Group ID, and the wrong choice decodes every Object
42/// without an error under Group IDs walking the wrong way.
43///
44/// The order is on the FETCH. Draft-19 Section 10.12.3: "The publisher
45/// responding to a FETCH is responsible for delivering all available Objects
46/// in the requested range in the requested order (see Section 10.2.8)." The
47/// session's control pipes read it off each FETCH they carry and file it
48/// here; [`ObjectFramer`] takes it out again when the response stream opens.
49///
50/// # Why an entry is taken rather than read
51///
52/// One FETCH opens one response stream, so an entry has exactly one reader
53/// and is spent by it. Taking bounds the table to the fetches that have been
54/// asked for and not yet answered, on a session that may run for hours, and
55/// it needs no rule about when to forget: the reader is the rule.
56///
57/// What that leaves is a fetch the publisher answered with an error rather
58/// than a stream, whose entry no reader ever comes for. A cap is the
59/// bound on those, and it fails closed — past it nothing is recorded, so the
60/// affected streams are bypassed and say so rather than being read against
61/// somebody else's order.
62#[derive(Debug, Default)]
63pub struct FetchGroupOrders {
64 known: Mutex<HashMap<u64, AnyFetchGroupOrder>>,
65}
66
67impl FetchGroupOrders {
68 /// How many asked-for-but-unanswered fetches one session may hold.
69 ///
70 /// Reached only by a peer that sends FETCHes whose responses never open a
71 /// stream, since every answered one takes its own entry away again.
72 const CAP: usize = 1024;
73
74 /// File the order a FETCH asked for, under the Request ID it asked under.
75 ///
76 /// Call with the order the *peer will act on*, which on a session whose
77 /// control frames a hook may rewrite is the one leaving the proxy rather
78 /// than the one that arrived.
79 pub fn record(&self, request_id: u64, order: AnyFetchGroupOrder) {
80 let mut known = self.known.lock().expect("no task holds the fetch orders across a panic");
81 if known.len() >= Self::CAP && !known.contains_key(&request_id) {
82 return;
83 }
84 known.insert(request_id, order);
85 }
86
87 /// Take the order filed for `request_id`, if one was.
88 #[must_use]
89 pub fn take(&self, request_id: u64) -> Option<AnyFetchGroupOrder> {
90 self.known
91 .lock()
92 .expect("no task holds the fetch orders across a panic")
93 .remove(&request_id)
94 }
95}
96
97/// Padding width that puts every wire length a varint can express within
98/// measuring reach.
99///
100/// Half of `usize::MAX`, so adding the buffered bytes cannot overflow
101/// [`PaddedBuf::remaining`]; a varint tops out at `2^62 - 1`, well inside
102/// it on a 64-bit target.
103const UNBOUNDED_MEASURING_PAD: usize = usize::MAX / 2;
104
105/// Configuration for [`ObjectFramer`].
106///
107/// Construct with [`FramerConfig::default`] and adjust fields, or with
108/// [`FramerConfig::new`] and the builder setters. The struct is
109/// `#[non_exhaustive]` so later releases can add knobs without a break;
110/// that also means a struct literal no longer compiles from outside this
111/// crate.
112#[derive(Debug, Clone)]
113#[non_exhaustive]
114pub struct FramerConfig {
115 /// Largest object the framer will buffer whole, in bytes. Objects
116 /// larger than this are streamed through as
117 /// [`FramerOut::Passthrough`] and are not individually addressable.
118 /// Default 4 MiB.
119 pub max_buffered_object_bytes: usize,
120}
121
122impl Default for FramerConfig {
123 fn default() -> Self {
124 Self { max_buffered_object_bytes: 4 * 1024 * 1024 }
125 }
126}
127
128impl FramerConfig {
129 /// A config with default values.
130 #[must_use]
131 pub fn new() -> Self {
132 Self::default()
133 }
134
135 /// Set the largest object the framer will buffer whole.
136 #[must_use]
137 pub fn with_max_buffered_object_bytes(mut self, bytes: usize) -> Self {
138 self.max_buffered_object_bytes = bytes;
139 self
140 }
141}
142
143/// One item produced by [`ObjectFramer::poll`].
144#[derive(Debug)]
145#[non_exhaustive]
146pub enum FramerOut {
147 /// The stream's header, with its exact wire bytes (stream-type field
148 /// included).
149 Header {
150 /// The decoded header.
151 header: DataStreamHeaderKind,
152 /// The header's wire bytes, including the stream-type field.
153 raw: Bytes,
154 },
155 /// A complete object, with its exact wire bytes.
156 Object {
157 /// The object's framing, without its payload.
158 meta: ObjectMeta,
159 /// The object's complete wire bytes, framing and payload.
160 raw: Bytes,
161 },
162 /// Bytes the framer is forwarding without interpreting them: an object
163 /// larger than the buffer cap, a fetch stream on a draft whose fetch
164 /// objects are not addressed, or any stream the framer has stopped
165 /// parsing. These bytes are not individually addressable.
166 Passthrough(Bytes),
167 /// The framer has stopped parsing this stream.
168 ///
169 /// Carries **no bytes**, so [`ObjectFramer::poll`]'s concatenation
170 /// invariant is untouched: this item contributes nothing to the
171 /// reconstruction. Emitted exactly once per stream.
172 ///
173 /// **Ordering: immediately after the item the bypass was decided
174 /// during, not in place of it.** Every one of `latch_bypass`'s call
175 /// sites returns a *different* `FramerOut` from the same `poll()` —
176 /// the two header sites fall through to [`Self::Header`], the
177 /// measuring-reach sites return [`Self::Passthrough`], the decode and
178 /// fix-up sites return [`Self::Error`] — and `poll()` returns one
179 /// item. The variant is therefore **deferred**: `latch_bypass` stores
180 /// a `pending_bypass: Option<BypassReason>`, and `poll()` drains it at
181 /// the top of its next call, before anything else.
182 Bypassed {
183 /// Why parsing stopped.
184 reason: BypassReason,
185 /// Whether an elide fix-up was still owed when parsing stopped.
186 ///
187 /// `true` means the remainder of this stream cannot be renumbered
188 /// and the destination must be reset rather than forwarded — the
189 /// bytes still in flight decode to Object IDs one ahead of what
190 /// was actually delivered.
191 fixup_owed: bool,
192 },
193 /// Not enough buffered bytes to produce another item.
194 NeedMore,
195 /// The stream could not be parsed. The framer switches to
196 /// [`Passthrough`](Self::Passthrough) for the remainder of the stream
197 /// and never reports a second error.
198 Error(String),
199}
200
201/// The state an elide leaves behind on a subgroup stream.
202///
203/// Read it with [`ObjectFramer::elide_cursor`]. The framer owns this state
204/// rather than the caller because it forwards objects the caller never
205/// sees an [`ObjectMeta`] for — an object one byte over
206/// [`FramerConfig::max_buffered_object_bytes`] leaves as
207/// [`FramerOut::Passthrough`], and a cursor kept outside the framer would
208/// let its stale delta go out on the wire.
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub struct ElideCursor {
211 /// Absolute Object ID of the last object actually forwarded, or `None`
212 /// when none has been.
213 pub last_forwarded_id: Option<u64>,
214 /// Whether the next object the framer emits will have its leading ID
215 /// field rewritten. Always `false` on drafts 07-13, where IDs are
216 /// absolute, and on fetch streams.
217 pub fixup_pending: bool,
218}
219
220/// The object the framer emitted most recently, whose forward-or-elide
221/// disposition has not been committed to the cursor yet.
222#[derive(Debug, Clone, Copy)]
223struct PendingObject {
224 object_id: u64,
225 index_in_stream: u64,
226}
227
228/// Identity fields the stream header contributes to every object on a
229/// subgroup stream.
230#[derive(Debug, Clone, Copy, Default)]
231struct SubgroupContext {
232 track_alias: u64,
233 group_id: u64,
234 subgroup_id: Option<u64>,
235 publisher_priority: Option<u8>,
236}
237
238/// What the framer is currently decoding.
239#[derive(Debug)]
240enum Stage {
241 /// Nothing decoded yet; the next bytes are the stream header.
242 AwaitingHeader,
243 /// Subgroup objects, with the per-stream delta state.
244 Subgroup(AnySubgroupObjectReader),
245 /// Fetch objects, with the state on both sides of the framer: what the
246 /// stream said, and what has actually been forwarded from it.
247 ///
248 /// The two are the same value until something is elided, and every frame
249 /// goes through the writer regardless. A frame it never saw would leave
250 /// it a frame behind, and a survivor re-encoded against a stale
251 /// predecessor is a renumbered stream rather than a decode error.
252 Fetch { reader: AnyFetchObjectReader, writer: AnyFetchObjectWriter },
253}
254
255/// Frames a unidirectional MoQT data stream into individually addressable
256/// objects while preserving the exact wire bytes.
257///
258/// Feed it the raw bytes of one stream, in order, from the stream's first
259/// byte. Drain [`poll`](Self::poll) after each [`feed`](Self::feed) until
260/// it returns [`FramerOut::NeedMore`]. The concatenation of every `raw`
261/// and `Passthrough` payload it yields equals the concatenation of every
262/// chunk fed to it, unless [`note_elided`](Self::note_elided) has been
263/// called — see [`Self::poll`].
264#[derive(Debug)]
265pub struct ObjectFramer {
266 stream_type: DataStreamType,
267 draft: DraftVersion,
268 config: FramerConfig,
269 buf: BytesMut,
270 stage: Stage,
271 context: SubgroupContext,
272 /// Bytes still owed to an oversized object that is being streamed
273 /// through uninterpreted. Denominated in **source** bytes — see
274 /// [`Self::poll_oversized`].
275 passthrough_remaining: u64,
276 /// Set once parsing has been abandoned for the rest of the stream.
277 bypassed: bool,
278 /// Why parsing was abandoned, waiting for [`Self::poll`] to drain it
279 /// as a [`FramerOut::Bypassed`].
280 ///
281 /// `fixup_owed` is read off [`Self::fixup_pending`] at drain time
282 /// rather than being captured here, and the two are the same value:
283 /// once `bypassed` is set, `poll` never re-enters `poll_object`, which
284 /// is the only thing that clears the flag, and no `Object` item can be
285 /// emitted between the latch and the drain for `note_elided` to set it.
286 pending_bypass: Option<BypassReason>,
287 index_in_stream: u64,
288 /// Absolute Object ID of the last object actually forwarded.
289 last_forwarded_id: Option<u64>,
290 /// Whether the next object emitted still owes a fix-up: on a subgroup
291 /// stream the leading Object ID varint rewritten against
292 /// [`Self::last_forwarded_id`], on a fetch stream a survivor re-encoded
293 /// against the frame that is now in front of it.
294 ///
295 /// Both are settled by emitting one object, after which the writing
296 /// cursor is level with the reading one again.
297 fixup_pending: bool,
298 /// The fetch writer as it stood before the object emitted most recently
299 /// was re-emitted through it.
300 ///
301 /// Re-emitting advances the writer, and the caller's verdict on that
302 /// object does not land until the next poll, so an elide has to put the
303 /// writer back where it was — otherwise the survivor after it is encoded
304 /// against an object nobody received.
305 fetch_rollback: Option<AnyFetchObjectWriter>,
306 /// The object emitted most recently, awaiting the caller's verdict.
307 /// Committed lazily at the top of [`Self::poll_object`].
308 pending_disposition: Option<PendingObject>,
309 /// Slow-path counters for the session this framer belongs to.
310 counters: Arc<Recorder>,
311 /// The session's answered fetches, on the drafts where a fetch stream
312 /// cannot be read without one. `None` for a framer built outside a
313 /// session, and on every draft that needs no order.
314 fetch_orders: Option<Arc<FetchGroupOrders>>,
315}
316
317impl ObjectFramer {
318 /// A framer whose slow-path counters go to `counters`.
319 ///
320 /// **The only constructor `session.rs` may use.** The counters are
321 /// session-scoped, and a framer that cannot reach its session's
322 /// [`Recorder`] would leave `framers_created`, `framer_header_polls`,
323 /// `framer_object_polls`, `objects_not_addressable` and
324 /// `object_ids_rewritten` at zero for every real session — which is a
325 /// *passing* `Interest::NONE` proof obtained by measuring nothing.
326 ///
327 /// [`Self::new`] is kept, unchanged, for tests and downstream callers
328 /// that construct a framer to parse bytes rather than to forward them;
329 /// it is equivalent to passing a fresh `Recorder` whose counts nobody
330 /// reads. That is why this is an **addition** and not a signature
331 /// change.
332 pub fn with_recorder(
333 stream_type: DataStreamType,
334 draft: DraftVersion,
335 config: FramerConfig,
336 counters: Arc<Recorder>,
337 ) -> Self {
338 counters.note_framer_created();
339 Self {
340 stream_type,
341 draft,
342 config,
343 buf: BytesMut::with_capacity(4096),
344 stage: Stage::AwaitingHeader,
345 context: SubgroupContext::default(),
346 passthrough_remaining: 0,
347 bypassed: false,
348 pending_bypass: None,
349 index_in_stream: 0,
350 last_forwarded_id: None,
351 fixup_pending: false,
352 fetch_rollback: None,
353 pending_disposition: None,
354 counters,
355 fetch_orders: None,
356 }
357 }
358
359 /// Read this stream's fetch Objects against the order its FETCH asked for.
360 ///
361 /// Only drafts 18, 19 and 20 need it — see [`FetchGroupOrders`] — and only a
362 /// fetch stream consults it; a subgroup framer given one ignores it. A
363 /// framer built without it on a draft that needs one reports
364 /// [`BypassReason::FetchGroupOrderUnknown`] and forwards the stream
365 /// uninterpreted, which is what every caller outside a session gets and
366 /// what the session itself gets for a stream naming a request it never
367 /// saw asked for.
368 #[must_use]
369 pub fn with_fetch_group_orders(mut self, orders: Arc<FetchGroupOrders>) -> Self {
370 self.fetch_orders = Some(orders);
371 self
372 }
373
374 /// Create a framer for a stream of the given kind on the given draft,
375 /// discarding its counters.
376 ///
377 /// Increments go to a private [`Recorder`] nothing can read, so a
378 /// caller that wants a session's counters to move must use
379 /// [`Self::with_recorder`]. Retained for callers that parse bytes
380 /// rather than forward them, where the counts are not the point.
381 pub fn new(stream_type: DataStreamType, draft: DraftVersion, config: FramerConfig) -> Self {
382 Self::with_recorder(stream_type, draft, config, Arc::new(Recorder::new()))
383 }
384
385 /// The state an elide has left behind on this stream.
386 ///
387 /// Read-only, and read-anytime: the framer applies the fix-up itself,
388 /// so nothing outside has to act on this.
389 #[must_use]
390 pub fn elide_cursor(&self) -> ElideCursor {
391 ElideCursor { last_forwarded_id: self.last_forwarded_id, fixup_pending: self.fixup_pending }
392 }
393
394 /// Record that the object the framer emitted **most recently** was not
395 /// forwarded.
396 ///
397 /// Call exactly once, immediately after deciding to drop an object,
398 /// and before the next [`Self::poll`]. `meta` must be the meta the
399 /// framer handed out for that object; it is `debug_assert`ed against
400 /// the framer's own record, because calling this out of order is the
401 /// one way to corrupt a stream silently.
402 ///
403 /// On drafts 07-13 subgroup streams and on drafts 07-14 fetch streams
404 /// this only suppresses the cursor advance — those Object IDs are
405 /// absolute, so the bytes of every later object already say the truth.
406 /// Elsewhere it also arms a fix-up: the leading Object ID varint on a
407 /// drafts 14-20 subgroup stream, and the whole framing of the next frame
408 /// on a drafts 15-20 fetch stream, where it additionally puts the fetch
409 /// writer back to where the last forwarded frame left it.
410 pub fn note_elided(&mut self, meta: &ObjectMeta) {
411 let pending = self.pending_disposition.take();
412 debug_assert!(
413 matches!(
414 pending,
415 Some(p) if p.object_id == meta.object_id
416 && p.index_in_stream == meta.index_in_stream
417 ),
418 "note_elided must name the object the framer emitted most recently; \
419 got object {} at index {}, framer holds {:?}",
420 meta.object_id,
421 meta.index_in_stream,
422 pending.map(|p| (p.object_id, p.index_in_stream)),
423 );
424 // Taking `pending` is the elide: the cursor simply does not
425 // advance to it. Arming the fix-up on a `None` would renumber an
426 // object whose predecessor was already committed as forwarded.
427 if pending.is_none() {
428 return;
429 }
430 if self.elide_owes_a_fixup() {
431 self.fixup_pending = true;
432 }
433 // The frame was re-emitted through the writer before the caller saw
434 // it, so the writer is one frame ahead of what the destination
435 // received. Nothing else can restore it: the per-draft state is what
436 // the *forwarded* frames left behind, and this frame was not one.
437 if let Some(rollback) = self.fetch_rollback.take() {
438 if let Stage::Fetch { writer, .. } = &mut self.stage {
439 *writer = rollback;
440 }
441 }
442 }
443
444 /// Buffer a chunk of stream bytes.
445 pub fn feed(&mut self, chunk: &[u8]) {
446 self.buf.extend_from_slice(chunk);
447 }
448
449 /// Bytes buffered but not yet emitted. Non-zero only mid-object.
450 pub fn buffered(&self) -> usize {
451 self.buf.len()
452 }
453
454 /// `true` once the framer has stopped parsing this stream and is
455 /// forwarding bytes uninterpreted.
456 pub fn is_bypassed(&self) -> bool {
457 self.bypassed
458 }
459
460 /// Flush any buffered bytes at end of stream.
461 ///
462 /// Called when the source signals FIN. Returns whatever the framer
463 /// still holds — a truncated final object, or bytes buffered behind an
464 /// incomplete framing — so the caller can forward them before
465 /// finishing the destination stream. Forgetting this call turns a
466 /// clean FIN into silent truncation.
467 pub fn finish(&mut self) -> Option<Bytes> {
468 self.passthrough_remaining = 0;
469 if self.buf.is_empty() {
470 None
471 } else {
472 Some(self.buf.split().freeze())
473 }
474 }
475
476 /// Produce the next item, or [`FramerOut::NeedMore`].
477 ///
478 /// # Invariant
479 ///
480 /// Concatenating the `raw` field of every [`Header`](FramerOut::Header)
481 /// and [`Object`](FramerOut::Object) and the payload of every
482 /// [`Passthrough`](FramerOut::Passthrough), in the order produced,
483 /// reproduces the fed bytes exactly — on every draft, including
484 /// streams that fall back to bypass — **unless**
485 /// [`Self::note_elided`] has been called, which deliberately removes
486 /// an object's bytes and may rewrite one leading varint.
487 ///
488 /// That exception is the *only* one, and it is opt-in per stream: a
489 /// framer used as a pure observer never calls `note_elided`, so its
490 /// output stays byte-identical to its input. When `note_elided` has
491 /// been called on a drafts 14-19 subgroup stream, the next object the
492 /// framer emits has its leading Object ID varint re-encoded against
493 /// the last object actually forwarded — one field, in one object, and
494 /// every byte after it copied verbatim. Everything else, on every
495 /// other stream, still concatenates back to the source exactly.
496 ///
497 /// [`FramerOut::Bypassed`] and [`FramerOut::NeedMore`] carry no bytes
498 /// and so contribute nothing to the reconstruction either way.
499 pub fn poll(&mut self) -> FramerOut {
500 // Drained before anything else. `latch_bypass` cannot return this
501 // item itself — each of its call sites returns a different
502 // `FramerOut` from the same `poll()` — so the bypass is reported
503 // on the next call, immediately after the item it was decided
504 // during. Zero bytes, so the invariant above is untouched.
505 if let Some(reason) = self.pending_bypass.take() {
506 return FramerOut::Bypassed { reason, fixup_owed: self.fixup_pending };
507 }
508 if self.passthrough_remaining > 0 {
509 let owed = self.passthrough_remaining;
510 return self.emit_passthrough(owed);
511 }
512 if self.bypassed {
513 return self.emit_passthrough(u64::MAX);
514 }
515 match self.stage {
516 Stage::AwaitingHeader => self.poll_header(),
517 Stage::Subgroup(_) | Stage::Fetch { .. } => self.poll_object(),
518 }
519 }
520
521 /// Hand out up to `limit` buffered bytes without interpreting them.
522 fn emit_passthrough(&mut self, limit: u64) -> FramerOut {
523 if self.buf.is_empty() {
524 return FramerOut::NeedMore;
525 }
526 let take = usize::try_from(limit).unwrap_or(usize::MAX).min(self.buf.len());
527 self.passthrough_remaining = self.passthrough_remaining.saturating_sub(take as u64);
528 FramerOut::Passthrough(self.buf.split_to(take).freeze())
529 }
530
531 /// Stop parsing this stream for good, record why, and release
532 /// everything buffered.
533 ///
534 /// Draining here is what keeps memory bounded: without it a stream the
535 /// framer cannot parse grows the buffer for as long as the peer keeps
536 /// writing.
537 ///
538 /// `reason` is stored rather than returned, because every call site
539 /// returns a different [`FramerOut`] from the same
540 /// [`poll`](Self::poll); the next `poll` drains it. Guarded on
541 /// `bypassed` so a second latch — which today cannot happen, since
542 /// `poll` short-circuits once the flag is set — could never overwrite
543 /// the first reason or double-count
544 /// `Counters::streams_not_shapeable`, whose whole job is to be one per
545 /// stream.
546 fn latch_bypass(&mut self, reason: BypassReason) {
547 if !self.bypassed {
548 self.bypassed = true;
549 self.pending_bypass = Some(reason);
550 self.counters.note_stream_not_shapeable();
551 }
552 self.passthrough_remaining = 0;
553 }
554
555 fn poll_header(&mut self) -> FramerOut {
556 self.counters.note_framer_header_poll();
557 if self.buf.is_empty() {
558 return FramerOut::NeedMore;
559 }
560
561 let snapshot: &[u8] = &self.buf[..];
562 let mut cursor: &[u8] = snapshot;
563 let decoded = match self.stream_type {
564 DataStreamType::Subgroup => AnySubgroupHeader::decode_stream(self.draft, &mut cursor)
565 .map(DataStreamHeaderKind::Subgroup),
566 DataStreamType::Fetch => AnyFetchHeader::decode_stream(self.draft, &mut cursor)
567 .map(DataStreamHeaderKind::Fetch),
568 };
569
570 match decoded {
571 Ok(header) => {
572 let consumed = snapshot.len() - cursor.remaining();
573 match &header {
574 DataStreamHeaderKind::Subgroup(h) => match AnySubgroupObjectReader::new(h) {
575 Ok(reader) => {
576 self.context = subgroup_context(h);
577 self.stage = Stage::Subgroup(reader);
578 }
579 // A subgroup stream type the object reader
580 // rejects. The header still decoded, so report it,
581 // then forward the rest uninterpreted.
582 Err(_) => self.latch_bypass(BypassReason::UnsupportedSubgroupStreamType),
583 },
584 // Whether this stream's Objects can be addressed is
585 // asked of `fetch_group_order_is_needed` and of the
586 // session's own record of what each FETCH asked for,
587 // rather than inferred from the reader refusing. A codec
588 // able to decode a layout is not on its own enough: on
589 // drafts 18, 19 and 20 it decodes under a Group Order nothing
590 // on this stream states, and the wrong one decodes as
591 // willingly as the right one.
592 DataStreamHeaderKind::Fetch(h) => match self.fetch_stage(h) {
593 Ok(stage) => self.stage = stage,
594 Err(reason) => self.latch_bypass(reason),
595 },
596 }
597 let raw = self.buf.split_to(consumed).freeze();
598 FramerOut::Header { header, raw }
599 }
600 Err(e) if is_incomplete_error(&e) => {
601 if self.buf.len() >= self.config.max_buffered_object_bytes {
602 self.latch_bypass(BypassReason::ObjectBeyondMeasuringReach);
603 self.emit_passthrough(u64::MAX)
604 } else {
605 FramerOut::NeedMore
606 }
607 }
608 Err(e) => {
609 self.latch_bypass(BypassReason::DecodeError);
610 FramerOut::Error(format!("data stream header decode: {e}"))
611 }
612 }
613 }
614
615 /// The reader and writer a fetch stream is parsed with, or why it is not.
616 ///
617 /// Two drafts need an answer from off the stream and the other eleven do
618 /// not, which is [`fetch_group_order_is_needed`]. Where one is needed it
619 /// comes from the FETCH the session carried, filed under the Request ID
620 /// this header names; a stream naming a request that was never asked for
621 /// is bypassed rather than guessed at, because the guess would decode.
622 ///
623 /// The reader and writer are built with the same order. They are the two
624 /// halves of one stream — the writer re-encodes a survivor after an elide
625 /// against the frames now in front of it — so a pair built against
626 /// different orders would renumber the stream it was meant to preserve.
627 fn fetch_stage(&mut self, h: &AnyFetchHeader) -> Result<Stage, BypassReason> {
628 let order = if fetch_group_order_is_needed(self.draft) {
629 let orders = self.fetch_orders.as_ref().ok_or(BypassReason::FetchGroupOrderUnknown)?;
630 Some(orders.take(h.request_id()).ok_or(BypassReason::FetchGroupOrderUnknown)?)
631 } else {
632 None
633 };
634 // Ascending where the draft needs no answer: the branch above supplies
635 // one for every draft that does, so the fallback is only ever reached
636 // where nothing on the stream is signed and the value cannot be wrong.
637 let order = order.unwrap_or(AnyFetchGroupOrder::Ascending);
638 let built = (AnyFetchObjectReader::new(h, order), AnyFetchObjectWriter::new(h, order));
639 match built {
640 (Ok(reader), Ok(writer)) => Ok(Stage::Fetch { reader, writer }),
641 // A draft this build did not compile. Not an error — the stream
642 // is simply not addressable. The header decode above would
643 // already have failed on such a draft, so this is a second line
644 // rather than the first.
645 _ => Err(BypassReason::NoFetchObjectCodec),
646 }
647 }
648
649 fn poll_object(&mut self) -> FramerOut {
650 self.counters.note_framer_object_poll();
651
652 // Lazy commit, once, here. The pipe loop is sequential — the
653 // caller's verdict on object *N* always lands before `poll`
654 // produces object *N+1* — so an object still pending at the top of
655 // this call was forwarded. `poll_oversized` deliberately does
656 // **not** repeat this: it is only ever reached from inside this
657 // function, and a second commit would consume a disposition this
658 // one already took. If it ever gains an entry path of its own,
659 // make the commit idempotent rather than adding a second site.
660 self.commit_disposition();
661
662 if self.buf.is_empty() {
663 return FramerOut::NeedMore;
664 }
665
666 let snapshot: &[u8] = &self.buf[..];
667 let mut cursor: &[u8] = snapshot;
668
669 // Probe against a clone: a reader mutated before the object is
670 // known to be complete carries a corrupt delta state into every
671 // later object on the stream.
672 let probed = match &self.stage {
673 Stage::Subgroup(reader) => {
674 let mut probe = reader.clone();
675 probe
676 .read_object_meta(&mut cursor)
677 .map(|m| (Probe::Subgroup(probe), Meta::Sub(m), None))
678 }
679 // `read_object_frame` rather than `read_object_meta`: it reports
680 // the same framing and additionally keeps the shape the frame
681 // arrived in, which is what re-encoding it against a different
682 // predecessor takes.
683 Stage::Fetch { reader, .. } => {
684 let mut probe = reader.clone();
685 probe
686 .read_object_frame(&mut cursor)
687 .map(|frame| (Probe::Fetch(probe), Meta::Fetch(frame.meta), Some(frame)))
688 }
689 Stage::AwaitingHeader => return FramerOut::NeedMore,
690 };
691
692 match probed {
693 Ok((probe, meta, frame)) => {
694 let consumed = snapshot.len() - cursor.remaining();
695 let object_id = meta.object_id();
696 // Before the source bytes are split off, so a failure
697 // leaves them in the buffer for the bypassed poll that
698 // follows to forward verbatim rather than dropping them.
699 let rewritten = match self.apply_elide_fixup(consumed, object_id, frame.as_ref()) {
700 Ok(rewritten) => rewritten,
701 Err(e) => {
702 self.latch_bypass(BypassReason::DecodeError);
703 return FramerOut::Error(e);
704 }
705 };
706 let source = self.buf.split_to(consumed).freeze();
707 let raw = rewritten.unwrap_or(source);
708 self.commit(probe);
709 // An object that arrives in one read completes before
710 // buffering ever reaches the cap, so the incomplete-decode
711 // path below never sees it. Judging it by its own wire
712 // length too is what makes the cap a property of the object
713 // rather than of the caller's read size. Judged on the
714 // *source* length, so an elide fix-up that widens the ID
715 // varint cannot move an object across the boundary.
716 let out = if consumed > self.config.max_buffered_object_bytes {
717 // A `Passthrough` that is a whole object: the meta was
718 // decoded and is about to be discarded, so nothing
719 // outside the framer can address it.
720 self.counters.note_object_not_addressable();
721 FramerOut::Passthrough(raw)
722 } else {
723 FramerOut::Object { meta: self.object_meta(meta), raw }
724 };
725 // Both branches emitted an object, so both arm the cursor:
726 // the oversized one has no `ObjectMeta` for the caller to
727 // name, which is exactly why the framer keeps the record.
728 self.pending_disposition =
729 Some(PendingObject { object_id, index_in_stream: self.index_in_stream });
730 self.index_in_stream += 1;
731 out
732 }
733 Err(e) if is_incomplete_error(&e) => {
734 if self.buf.len() < self.config.max_buffered_object_bytes {
735 FramerOut::NeedMore
736 } else {
737 self.poll_oversized()
738 }
739 }
740 Err(e) => {
741 self.latch_bypass(BypassReason::DecodeError);
742 FramerOut::Error(format!("object decode: {e}"))
743 }
744 }
745 }
746
747 /// The buffer hit its cap without completing an object. Decide whether
748 /// the object's *framing* is understood — in which case its bytes can
749 /// be streamed through and framing resumes afterwards — or whether the
750 /// stream has to be abandoned.
751 ///
752 /// The framing is read against the buffered bytes followed by padding,
753 /// so the decoder can advance past a payload that has not arrived. How
754 /// wide that padding may be is [`Self::measuring_pad`]'s call.
755 fn poll_oversized(&mut self) -> FramerOut {
756 let pad = self.measuring_pad();
757 let mut padded = PaddedBuf::new(&self.buf[..], pad);
758 let probed = match &self.stage {
759 Stage::Subgroup(reader) => {
760 let mut probe = reader.clone();
761 probe.read_object_meta(&mut padded).map(|m| {
762 (Probe::Subgroup(probe), m.object_id, m.wire_len, m.payload_length, None)
763 })
764 }
765 Stage::Fetch { reader, .. } => {
766 let mut probe = reader.clone();
767 probe.read_object_frame(&mut padded).map(|frame| {
768 let meta = frame.meta;
769 (
770 Probe::Fetch(probe),
771 meta.object_id,
772 meta.wire_len,
773 meta.payload_length,
774 Some(frame),
775 )
776 })
777 }
778 Stage::AwaitingHeader => return FramerOut::NeedMore,
779 };
780
781 let Ok((probe, object_id, wire_len, payload_length, frame)) = probed else {
782 self.latch_bypass(BypassReason::ObjectBeyondMeasuringReach);
783 return self.emit_passthrough(u64::MAX);
784 };
785
786 // Only trust the framing when every field ahead of the payload
787 // came from real bytes rather than padding.
788 let framing_len = wire_len - payload_length;
789 let buffered = self.buf.len() as u64;
790 if framing_len > buffered || wire_len <= buffered {
791 self.latch_bypass(BypassReason::ObjectBeyondMeasuringReach);
792 return self.emit_passthrough(u64::MAX);
793 }
794
795 // `framing_len <= buffered` is what makes the fix-up reachable
796 // here: the whole framing, leading Object ID varint included, is
797 // in the chunk about to be emitted. The chunk is a *prefix* of the
798 // object, which `reemit_subgroup_object` accepts — it decodes the
799 // ID field and copies the rest verbatim without validating any
800 // length.
801 let chunk = self.buf.len();
802 let rewritten = match self.apply_elide_fixup(chunk, object_id, frame.as_ref()) {
803 Ok(rewritten) => rewritten,
804 Err(e) => {
805 self.latch_bypass(BypassReason::DecodeError);
806 return FramerOut::Error(e);
807 }
808 };
809
810 self.commit(probe);
811 // The second `Passthrough`-as-an-object path. Counted once here,
812 // not once per emitted chunk: this function is entered once per
813 // oversized object, and the chunks that follow come from `poll`'s
814 // `passthrough_remaining` branch.
815 self.counters.note_object_not_addressable();
816 // Denominated in **source** bytes, and deliberately not adjusted
817 // by the fix-up's `id_bytes_after - id_bytes_before`. Its job is
818 // to say how many more bytes of the *incoming* stream belong to
819 // this object; the emitted stream is a byte or two shorter or
820 // longer for exactly one object, which is what an elide fix-up is.
821 // Correcting it here would make the framer stop consuming this
822 // object early or late and resynchronise mid-way through the next
823 // one — a silent corruption that only fires when the new delta
824 // needs a wider varint.
825 self.passthrough_remaining = wire_len - buffered;
826 self.pending_disposition =
827 Some(PendingObject { object_id, index_in_stream: self.index_in_stream });
828 self.index_in_stream += 1;
829 let source = self.buf.split().freeze();
830 FramerOut::Passthrough(rewritten.unwrap_or(source))
831 }
832
833 /// Commit the disposition of the object emitted most recently.
834 ///
835 /// An object still pending was forwarded; one the caller elided was
836 /// taken out of `pending_disposition` by [`Self::note_elided`], so the
837 /// cursor never advances to it.
838 fn commit_disposition(&mut self) {
839 if let Some(pending) = self.pending_disposition.take() {
840 self.last_forwarded_id = Some(pending.object_id);
841 // Forwarded, so the writer's advance stands and there is nothing
842 // to put back. Cleared rather than left for the next re-emit to
843 // overwrite, so that a rollback can only ever undo the frame it
844 // was taken for.
845 self.fetch_rollback = None;
846 }
847 }
848
849 /// `true` when this stream's Object IDs are written as `id - prev - 1`
850 /// rather than absolutely, so eliding one renumbers every later object.
851 ///
852 /// Subgroup streams on drafts 14-20, and no fetch stream on any draft:
853 /// this is the predicate for the *varint rewrite*, and a fetch frame is
854 /// paid for by [`Self::reemit_fetch_frame`] instead. Drafts 07-13 write
855 /// subgroup Object IDs absolutely and need neither.
856 ///
857 /// The draft half is an exhaustive match rather than a `matches!`: a draft
858 /// left off the list answers "written absolutely", the framer then forwards
859 /// the object after an elide without rewriting its leading varint, and
860 /// every later Object ID on the stream is off by one with nothing to say
861 /// so. The stream-kind half stays a `matches!` — `DataStreamType` is not a
862 /// draft list and its two variants are both named here.
863 fn delta_encodes_object_ids(&self) -> bool {
864 matches!(self.stream_type, DataStreamType::Subgroup)
865 && match self.draft {
866 DraftVersion::Draft07
867 | DraftVersion::Draft08
868 | DraftVersion::Draft09
869 | DraftVersion::Draft10
870 | DraftVersion::Draft11
871 | DraftVersion::Draft12
872 | DraftVersion::Draft13 => false,
873 DraftVersion::Draft14
874 | DraftVersion::Draft15
875 | DraftVersion::Draft16
876 | DraftVersion::Draft17
877 | DraftVersion::Draft18
878 | DraftVersion::Draft19
879 | DraftVersion::Draft20 => true,
880 }
881 }
882
883 /// `true` when eliding an object from this stream leaves the next one
884 /// encoded against something that is no longer on the wire, so a fix-up
885 /// is owed before another object may be forwarded.
886 ///
887 /// The two stream kinds owe it for different reasons and pay it in
888 /// different ways, and the debt itself is the same: until one more object
889 /// has been emitted, the bytes the destination would receive decode to
890 /// Locations nobody sent. `fixup_owed` on a `Bypassed` is what a session
891 /// resets its destination over, and it reads this.
892 ///
893 /// Fetch streams on drafts 15-20, where a Serialization Flags field lets
894 /// a frame take any of its Group ID, Subgroup ID, Object ID and Priority
895 /// from the frame before it — draft-17 Section 10.4.4.1, Table 7: "Object
896 /// ID is the prior Object's ID plus one". Not drafts 07-14, whose fetch
897 /// objects state all four outright.
898 ///
899 /// Exhaustive rather than a `matches!` for the same reason as
900 /// [`Self::delta_encodes_object_ids`], and the consequence is larger here:
901 /// `false` means no fix-up is owed, so the session never resets the
902 /// destination and the receiver keeps a stream whose frames decode to
903 /// Locations nobody sent.
904 fn elide_owes_a_fixup(&self) -> bool {
905 match self.stream_type {
906 DataStreamType::Subgroup => self.delta_encodes_object_ids(),
907 DataStreamType::Fetch => match self.draft {
908 DraftVersion::Draft07
909 | DraftVersion::Draft08
910 | DraftVersion::Draft09
911 | DraftVersion::Draft10
912 | DraftVersion::Draft11
913 | DraftVersion::Draft12
914 | DraftVersion::Draft13
915 | DraftVersion::Draft14 => false,
916 DraftVersion::Draft15
917 | DraftVersion::Draft16
918 | DraftVersion::Draft17
919 | DraftVersion::Draft18
920 | DraftVersion::Draft19
921 | DraftVersion::Draft20 => true,
922 },
923 }
924 }
925
926 /// Rewrite the leading Object ID varint of `self.buf[..len]` when an
927 /// elide has left the wire's delta chain one object ahead of what was
928 /// actually forwarded.
929 ///
930 /// `len` may be a prefix of the object rather than the whole of it —
931 /// the oversized path calls this with the first chunk. Returns
932 /// `Ok(None)` when no fix-up was owed, in which case the caller emits
933 /// the source bytes untouched; the borrow of `self.buf` ends before
934 /// this returns, so the caller is free to split it afterwards.
935 ///
936 /// The error is unreachable in practice — Object IDs are strictly
937 /// increasing within a stream and the caller has already established
938 /// that the ID field is whole in the buffer — but it is reported
939 /// rather than swallowed, because the alternative is emitting bytes
940 /// known to decode to the wrong ID.
941 fn apply_elide_fixup(
942 &mut self,
943 len: usize,
944 object_id: u64,
945 frame: Option<&AnyFetchFrame>,
946 ) -> Result<Option<Bytes>, String> {
947 // `frame` is `Some` exactly on a fetch stream, where the payment is a
948 // re-encode of the whole framing rather than a rewrite of one varint,
949 // and where it is the writer rather than a flag that decides whether
950 // anything is owed.
951 if let Some(frame) = frame {
952 return self.reemit_fetch_frame(len, frame);
953 }
954 if !self.fixup_pending || !self.delta_encodes_object_ids() {
955 return Ok(None);
956 }
957 let mut out = BytesMut::with_capacity(len + 8);
958 let outcome = reemit_subgroup_object(
959 self.draft,
960 self.last_forwarded_id,
961 object_id,
962 &self.buf[..len],
963 &mut out,
964 );
965 if let Err(e) = outcome {
966 // Leaves `fixup_pending` set, so the cursor keeps reporting
967 // that this stream still owes a fix-up.
968 return Err(format!("elide fix-up: {e}"));
969 }
970 self.fixup_pending = false;
971 self.counters.note_object_id_rewritten();
972 Ok(Some(out.freeze()))
973 }
974
975 /// Re-emit one fetch frame through this stream's writer.
976 ///
977 /// Called for **every** fetch frame the framer emits, not only after an
978 /// elide. The writer is what a survivor is re-encoded against, and it
979 /// only moves when it is shown a frame, so skipping the frames nothing
980 /// was owed for would leave it as many frames behind as were skipped.
981 ///
982 /// Answers `Ok(None)` when the frame's own bytes still say what the frame
983 /// says, which is every frame on a stream nothing has been removed from.
984 /// The writer is cloned first so that [`Self::note_elided`] can put it
985 /// back: this runs before the caller's verdict on the frame, and a frame
986 /// the caller drops must leave no trace on the writing side.
987 ///
988 /// `len` may be a prefix of the frame — the oversized path calls this
989 /// with the first chunk — and the codec asks only that the whole framing
990 /// be inside it, which is what the caller has already established.
991 fn reemit_fetch_frame(
992 &mut self,
993 len: usize,
994 frame: &AnyFetchFrame,
995 ) -> Result<Option<Bytes>, String> {
996 let mut out = BytesMut::with_capacity(len + 16);
997 let (outcome, rollback) = {
998 let Stage::Fetch { writer, .. } = &mut self.stage else {
999 return Ok(None);
1000 };
1001 let rollback = writer.clone();
1002 (writer.reemit_object(frame, &self.buf[..len], &mut out), rollback)
1003 };
1004 self.fetch_rollback = Some(rollback);
1005
1006 match outcome {
1007 Ok(FetchReemit::Unchanged) => {
1008 // The debt such as it was is settled either way. A removal
1009 // whose survivor happened to state every field it needed
1010 // costs nothing, and leaving the flag set would have the
1011 // stream report an unpaid fix-up at every later bypass.
1012 self.fixup_pending = false;
1013 Ok(None)
1014 }
1015 Ok(FetchReemit::Reframed { .. }) => {
1016 self.fixup_pending = false;
1017 self.counters.note_object_id_rewritten();
1018 Ok(Some(out.freeze()))
1019 }
1020 // Leaves `fixup_pending` set, as the subgroup path does, so the
1021 // stream keeps reporting that it still owes one.
1022 Err(e) => Err(format!("elide fix-up: {e}")),
1023 }
1024 }
1025
1026 /// How much padding [`Self::poll_oversized`] may put behind the
1027 /// buffered bytes when measuring an object that has not arrived whole.
1028 ///
1029 /// The pad costs no memory by itself — it is never materialised — but
1030 /// every copying read in the codec checks `Buf::remaining()` before it
1031 /// allocates, so the pad is exactly the bound on the allocation a
1032 /// hostile length field can provoke. That makes the choice per layout
1033 /// rather than global:
1034 ///
1035 /// * Subgroup objects on drafts 14-20, and fetch objects on draft-14,
1036 /// are measured without a single copy — their `read_object_meta`
1037 /// advances past the extension block and the payload rather than
1038 /// reading them. Nothing can be talked into allocating, so the pad is
1039 /// effectively unbounded and an object of *any* declared size stays
1040 /// measurable: it streams through and framing resumes after it.
1041 /// * Every other object layout decodes its extension block by copying
1042 /// it out of the buffer. Those keep a pad of one cap, which bounds
1043 /// the copy at the price of reach: an object whose wire length
1044 /// exceeds `buffered + cap` cannot be measured and the stream falls
1045 /// back to passthrough for its remainder.
1046 ///
1047 /// Widening the second case needs the codec to reject an extension
1048 /// length larger than the bytes actually present, which is not this
1049 /// crate's to change.
1050 ///
1051 /// **"Every other" includes the newest fetch layouts, and that is not an
1052 /// omission.** Drafts 15 through 20 all read a fetch frame through
1053 /// `FetchObjectReader::read_object_header`, which materialises the frame's
1054 /// properties — `data_dispatch.rs` `fo15`..`fo20` each carry the block out
1055 /// of the buffer before `read_object_frame` skips the payload — so a
1056 /// declared extension length is still an allocation those drafts can be
1057 /// talked into. Only draft-14's `FetchObject::decode_meta` advances past
1058 /// the block instead. Both arms are exhaustive matches rather than
1059 /// `matches!` so that this stays a decision: a new draft answering `false`
1060 /// by omission would be *safe* here, which is precisely why nothing would
1061 /// ever notice that no one had looked at its fetch layout.
1062 fn measuring_pad(&self) -> usize {
1063 let copy_free = match self.stage {
1064 Stage::Subgroup(_) => match self.draft {
1065 DraftVersion::Draft07
1066 | DraftVersion::Draft08
1067 | DraftVersion::Draft09
1068 | DraftVersion::Draft10
1069 | DraftVersion::Draft11
1070 | DraftVersion::Draft12
1071 | DraftVersion::Draft13 => false,
1072 DraftVersion::Draft14
1073 | DraftVersion::Draft15
1074 | DraftVersion::Draft16
1075 | DraftVersion::Draft17
1076 | DraftVersion::Draft18
1077 | DraftVersion::Draft19
1078 | DraftVersion::Draft20 => true,
1079 },
1080 Stage::Fetch { .. } => match self.draft {
1081 DraftVersion::Draft14 => true,
1082 DraftVersion::Draft07
1083 | DraftVersion::Draft08
1084 | DraftVersion::Draft09
1085 | DraftVersion::Draft10
1086 | DraftVersion::Draft11
1087 | DraftVersion::Draft12
1088 | DraftVersion::Draft13
1089 | DraftVersion::Draft15
1090 | DraftVersion::Draft16
1091 | DraftVersion::Draft17
1092 | DraftVersion::Draft18
1093 | DraftVersion::Draft19
1094 | DraftVersion::Draft20 => false,
1095 },
1096 Stage::AwaitingHeader => false,
1097 };
1098 if copy_free {
1099 UNBOUNDED_MEASURING_PAD
1100 } else {
1101 self.config.max_buffered_object_bytes
1102 }
1103 }
1104
1105 /// Adopt a probe reader whose decode succeeded.
1106 fn commit(&mut self, probe: Probe) {
1107 match (&mut self.stage, probe) {
1108 (Stage::Subgroup(reader), Probe::Subgroup(probe)) => *reader = probe,
1109 (Stage::Fetch { reader, .. }, Probe::Fetch(probe)) => *reader = probe,
1110 // `probe` was cloned from `stage` a few lines earlier, so the
1111 // pairing always matches.
1112 _ => {}
1113 }
1114 }
1115
1116 fn object_meta(&self, meta: Meta) -> ObjectMeta {
1117 match meta {
1118 Meta::Sub(m) => ObjectMeta {
1119 draft: self.draft,
1120 stream_kind: DataStreamType::Subgroup,
1121 track_alias: Some(self.context.track_alias),
1122 group_id: self.context.group_id,
1123 subgroup_id: self.context.subgroup_id,
1124 object_id: m.object_id,
1125 publisher_priority: self.context.publisher_priority,
1126 index_in_stream: self.index_in_stream,
1127 payload_len: m.payload_length,
1128 status: m.status,
1129 end_of_range: None,
1130 },
1131 Meta::Fetch(m) => ObjectMeta {
1132 draft: self.draft,
1133 stream_kind: DataStreamType::Fetch,
1134 track_alias: None,
1135 group_id: m.group_id,
1136 // Not `Some(m.subgroup_id)`: from draft-15 a fetch frame
1137 // may carry no Subgroup ID at all — an object forwarded
1138 // over a datagram has no subgroup, and an End of Range
1139 // indicator names a Location rather than an object — and
1140 // the codec leaves a placeholder in the field when it says
1141 // so. Forwarding that placeholder would key a matcher on a
1142 // subgroup the publisher never named.
1143 subgroup_id: m.has_subgroup_id.then_some(m.subgroup_id),
1144 object_id: m.object_id,
1145 publisher_priority: Some(m.publisher_priority),
1146 index_in_stream: self.index_in_stream,
1147 payload_len: m.payload_length,
1148 status: m.status,
1149 end_of_range: m.end_of_range,
1150 },
1151 }
1152 }
1153}
1154
1155/// A reader clone whose decode succeeded and is ready to be adopted.
1156enum Probe {
1157 Subgroup(AnySubgroupObjectReader),
1158 Fetch(AnyFetchObjectReader),
1159}
1160
1161/// The codec-level framing of one object, either stream kind.
1162#[derive(Clone, Copy)]
1163enum Meta {
1164 Sub(AnySubgroupObjectMeta),
1165 Fetch(AnyFetchObjectMeta),
1166}
1167
1168impl Meta {
1169 /// The object's absolute Object ID, delta encoding already resolved.
1170 fn object_id(self) -> u64 {
1171 match self {
1172 Meta::Sub(m) => m.object_id,
1173 Meta::Fetch(m) => m.object_id,
1174 }
1175 }
1176}
1177
1178/// The identity fields a subgroup header contributes to its objects.
1179///
1180/// Every field comes from an [`AnySubgroupHeader`] accessor, so the
1181/// per-draft knowledge behind them — which stream types leave the subgroup
1182/// ID to the first object, which drafts fold a mode field into the
1183/// header-type octet, which drafts may omit the publisher priority — stays
1184/// in the codec beside the decoders that define it. This crate keeps no
1185/// copy of it, which is the point: the copy it used to keep (a mask
1186/// constant and a thirteen-arm match) had already drifted from draft-16's
1187/// own decoder.
1188///
1189/// Where a draft encodes the subgroup ID implicitly as the first object's
1190/// ID, the codec stores zero and [`AnySubgroupHeader::subgroup_id`]
1191/// reports `None` rather than that zero.
1192fn subgroup_context(header: &AnySubgroupHeader) -> SubgroupContext {
1193 SubgroupContext {
1194 track_alias: header.track_alias(),
1195 group_id: header.group_id(),
1196 subgroup_id: header.subgroup_id(),
1197 publisher_priority: header.publisher_priority(),
1198 }
1199}
1200
1201/// A slice followed by a run of zero bytes.
1202///
1203/// Lets a decoder walk an object's framing and advance past a payload that
1204/// has not arrived yet, so the framer can learn an object's total wire
1205/// length without buffering it. Only fields decoded from the real prefix
1206/// are trustworthy; the caller checks that before using the result, and
1207/// picks the pad width from what the decode path may copy — see
1208/// `ObjectFramer::measuring_pad`.
1209struct PaddedBuf<'a> {
1210 real: &'a [u8],
1211 pad: usize,
1212}
1213
1214/// Backing bytes for [`PaddedBuf`]'s padded region.
1215const ZEROS: [u8; 1024] = [0u8; 1024];
1216
1217impl<'a> PaddedBuf<'a> {
1218 fn new(real: &'a [u8], pad: usize) -> Self {
1219 Self { real, pad }
1220 }
1221}
1222
1223impl Buf for PaddedBuf<'_> {
1224 fn remaining(&self) -> usize {
1225 self.real.len() + self.pad
1226 }
1227
1228 fn chunk(&self) -> &[u8] {
1229 if self.real.is_empty() {
1230 &ZEROS[..self.pad.min(ZEROS.len())]
1231 } else {
1232 self.real
1233 }
1234 }
1235
1236 fn advance(&mut self, cnt: usize) {
1237 let from_real = cnt.min(self.real.len());
1238 self.real = &self.real[from_real..];
1239 self.pad = self.pad.saturating_sub(cnt - from_real);
1240 }
1241}
1242
1243// Every test below builds its input with a codec writer and reads it back
1244// with a codec reader, so every one of them needs at least one draft
1245// compiled in. With none, `AnySubgroupHeader` and friends are uninhabited
1246// enums and the helpers stop type-checking — `-D warnings` reports the
1247// `decode_stream` call as an unreachable definition. Gating the module is
1248// the honest shape: with no draft there is no framing to test, so the
1249// module vanishes rather than being kept alive by `#[allow]`.
1250#[cfg(test)]
1251#[cfg(any(
1252 feature = "draft07",
1253 feature = "draft08",
1254 feature = "draft09",
1255 feature = "draft10",
1256 feature = "draft11",
1257 feature = "draft12",
1258 feature = "draft13",
1259 feature = "draft14",
1260 feature = "draft15",
1261 feature = "draft16",
1262 feature = "draft17",
1263 feature = "draft18",
1264 feature = "draft19",
1265 feature = "draft20"
1266))]
1267mod tests {
1268 //! The elide cursor, on the wire.
1269 //! Everything here builds its input with the codec's *writer* and checks
1270 //! the framer's output by *decoding* it again — so the claim under test
1271 //! (*after eliding object N the stream still decodes to the IDs that
1272 //! survived*) is settled by the codec's readers rather than by the rewrite
1273 //! logic being tested. Byte identity in the absence of an elide stays the
1274 //! property `tests/framer_tests.rs` and
1275 //! `tests/object_framing_acceptance.rs` own; the fence here only pins that
1276 //! the new code does not fire when nothing asked it to.
1277
1278 use super::*;
1279
1280 use moqtap_codec::dispatch::{AnySubgroupObject, AnySubgroupObjectWriter};
1281
1282 /// The drafts this build actually compiled.
1283 ///
1284 /// Each element carries its own `#[cfg]`, so the sweep is the enabled
1285 /// set rather than a hardcoded fourteen: a `--features draft14` build
1286 /// sweeps one draft and does not try to decode thirteen headers whose
1287 /// decoders were not compiled.
1288 const DRAFTS: &[DraftVersion] = &[
1289 #[cfg(feature = "draft07")]
1290 DraftVersion::Draft07,
1291 #[cfg(feature = "draft08")]
1292 DraftVersion::Draft08,
1293 #[cfg(feature = "draft09")]
1294 DraftVersion::Draft09,
1295 #[cfg(feature = "draft10")]
1296 DraftVersion::Draft10,
1297 #[cfg(feature = "draft11")]
1298 DraftVersion::Draft11,
1299 #[cfg(feature = "draft12")]
1300 DraftVersion::Draft12,
1301 #[cfg(feature = "draft13")]
1302 DraftVersion::Draft13,
1303 #[cfg(feature = "draft14")]
1304 DraftVersion::Draft14,
1305 #[cfg(feature = "draft15")]
1306 DraftVersion::Draft15,
1307 #[cfg(feature = "draft16")]
1308 DraftVersion::Draft16,
1309 #[cfg(feature = "draft17")]
1310 DraftVersion::Draft17,
1311 #[cfg(feature = "draft18")]
1312 DraftVersion::Draft18,
1313 #[cfg(feature = "draft19")]
1314 DraftVersion::Draft19,
1315 #[cfg(feature = "draft20")]
1316 DraftVersion::Draft20,
1317 ];
1318
1319 /// The drafts that write an Object ID as `id - prev - 1`, and so are
1320 /// the only ones where eliding an object corrupts its successors —
1321 /// intersected with the ones this build compiled.
1322 const DELTA_DRAFTS: &[DraftVersion] = &[
1323 #[cfg(feature = "draft14")]
1324 DraftVersion::Draft14,
1325 #[cfg(feature = "draft15")]
1326 DraftVersion::Draft15,
1327 #[cfg(feature = "draft16")]
1328 DraftVersion::Draft16,
1329 #[cfg(feature = "draft17")]
1330 DraftVersion::Draft17,
1331 #[cfg(feature = "draft18")]
1332 DraftVersion::Draft18,
1333 #[cfg(feature = "draft19")]
1334 DraftVersion::Draft19,
1335 #[cfg(feature = "draft20")]
1336 DraftVersion::Draft20,
1337 ];
1338
1339 /// The stream-type field opening a subgroup stream that carries an
1340 /// explicit subgroup ID and no extensions, per draft.
1341 fn subgroup_stream_type(draft: DraftVersion) -> u8 {
1342 match draft {
1343 DraftVersion::Draft07
1344 | DraftVersion::Draft08
1345 | DraftVersion::Draft09
1346 | DraftVersion::Draft10 => 0x04,
1347 DraftVersion::Draft11 => 0x0C,
1348 _ => 0x14,
1349 }
1350 }
1351
1352 /// Wire bytes of a subgroup header: track alias 1, group 0, subgroup 0,
1353 /// publisher priority 128, no extensions.
1354 fn header_bytes(draft: DraftVersion) -> Vec<u8> {
1355 vec![subgroup_stream_type(draft), 0x01, 0x00, 0x00, 0x80]
1356 }
1357
1358 fn object(object_id: u64, payload_len: usize) -> AnySubgroupObject {
1359 AnySubgroupObject {
1360 object_id,
1361 extension_headers: Vec::new(),
1362 extension_count: None,
1363 status: None,
1364 payload: vec![0xAB; payload_len],
1365 }
1366 }
1367
1368 /// Encode a whole subgroup stream: header plus `objects`, on `draft`.
1369 fn build_stream(draft: DraftVersion, objects: &[AnySubgroupObject]) -> Vec<u8> {
1370 let head = header_bytes(draft);
1371 let mut cursor: &[u8] = &head;
1372 let header = AnySubgroupHeader::decode_stream(draft, &mut cursor)
1373 .unwrap_or_else(|e| panic!("[{draft}] header decode: {e}"));
1374 let mut writer = AnySubgroupObjectWriter::new(&header)
1375 .unwrap_or_else(|e| panic!("[{draft}] writer: {e}"));
1376
1377 let mut out = head;
1378 for obj in objects {
1379 writer
1380 .write_object(obj, &mut out)
1381 .unwrap_or_else(|e| panic!("[{draft}] write object {}: {e}", obj.object_id));
1382 }
1383 out
1384 }
1385
1386 /// A subgroup framer on `draft`, reporting into `counters`.
1387 ///
1388 /// Every framer in this module goes through
1389 /// [`ObjectFramer::with_recorder`] rather than [`ObjectFramer::new`]:
1390 /// it keeps `src/` free of the string standing gate 7 greps for
1391 /// without the gate having to filter comments, and it gives the
1392 /// counter assertions below something to read.
1393 fn framer_for(
1394 draft: DraftVersion,
1395 config: FramerConfig,
1396 counters: &Arc<Recorder>,
1397 ) -> ObjectFramer {
1398 ObjectFramer::with_recorder(DataStreamType::Subgroup, draft, config, Arc::clone(counters))
1399 }
1400
1401 /// What one run of the framer produced.
1402 struct Run {
1403 /// Every byte the framer emitted, in order.
1404 bytes: Vec<u8>,
1405 /// The Object IDs the framer itself resolved while producing them.
1406 ///
1407 /// Objects that left as `Passthrough` are absent — they were never
1408 /// addressable — so this is also the record of *where framing
1409 /// resumed* after one of them.
1410 framed_ids: Vec<u64>,
1411 /// The run's own counters.
1412 counters: Arc<Recorder>,
1413 }
1414
1415 /// Feed `stream` to a framer in `chunk`-sized pieces, dropping every
1416 /// object whose ID is in `elide`, and return what the framer produced.
1417 ///
1418 /// A bypass is a panic rather than a recorded outcome: every test that
1419 /// uses this helper is about an elide surviving, and a stream that
1420 /// quietly stopped being parsed would satisfy the byte assertions
1421 /// while proving nothing.
1422 fn run_eliding(
1423 draft: DraftVersion,
1424 stream: &[u8],
1425 chunk: usize,
1426 config: FramerConfig,
1427 elide: &[u64],
1428 ) -> Run {
1429 let counters = Arc::new(Recorder::new());
1430 let mut framer = framer_for(draft, config, &counters);
1431 let mut run = Run { bytes: Vec::new(), framed_ids: Vec::new(), counters };
1432 for piece in stream.chunks(chunk.max(1)) {
1433 framer.feed(piece);
1434 loop {
1435 match framer.poll() {
1436 FramerOut::NeedMore => break,
1437 FramerOut::Header { raw, .. } => run.bytes.extend_from_slice(&raw),
1438 FramerOut::Object { meta, raw } => {
1439 run.framed_ids.push(meta.object_id);
1440 if elide.contains(&meta.object_id) {
1441 framer.note_elided(&meta);
1442 } else {
1443 run.bytes.extend_from_slice(&raw);
1444 }
1445 }
1446 FramerOut::Passthrough(raw) => run.bytes.extend_from_slice(&raw),
1447 FramerOut::Bypassed { reason, fixup_owed } => {
1448 panic!("[{draft}] unexpected bypass: {reason:?}, fixup_owed {fixup_owed}")
1449 }
1450 FramerOut::Error(e) => panic!("[{draft}] framer error: {e}"),
1451 }
1452 }
1453 }
1454 if let Some(tail) = framer.finish() {
1455 run.bytes.extend_from_slice(&tail);
1456 }
1457 run
1458 }
1459
1460 /// Decode `bytes` as a subgroup stream and report the Object IDs the
1461 /// codec's readers resolve from it.
1462 ///
1463 /// The framer used here has the default cap, so every object is framed
1464 /// individually no matter how the producing run chose to emit it.
1465 fn framed_object_ids(draft: DraftVersion, bytes: &[u8]) -> Vec<u64> {
1466 let counters = Arc::new(Recorder::new());
1467 let mut framer = framer_for(draft, FramerConfig::default(), &counters);
1468 framer.feed(bytes);
1469 let mut ids = Vec::new();
1470 loop {
1471 match framer.poll() {
1472 FramerOut::NeedMore => break,
1473 FramerOut::Header { .. } => {}
1474 FramerOut::Object { meta, .. } => ids.push(meta.object_id),
1475 FramerOut::Passthrough(raw) => {
1476 panic!("[{draft}] re-decode fell back to passthrough after {} bytes", raw.len())
1477 }
1478 FramerOut::Bypassed { reason, .. } => {
1479 panic!("[{draft}] re-decode bypassed: {reason:?}")
1480 }
1481 FramerOut::Error(e) => panic!("[{draft}] re-decode: {e}"),
1482 }
1483 }
1484 assert_eq!(framer.buffered(), 0, "[{draft}] re-decode left bytes buffered");
1485 ids
1486 }
1487
1488 /// Eliding an object leaves a stream that still decodes to exactly the
1489 /// objects that survived, on every draft.
1490 ///
1491 /// On drafts 14-19 that is only true because the framer rewrites the
1492 /// next object's leading Object ID varint; on 07-13 the IDs are
1493 /// absolute and the surviving bytes already say the truth.
1494 ///
1495 /// *Ablation:* returning `Ok(None)` unconditionally from
1496 /// `apply_elide_fixup` (never rewriting) fails this on the six delta
1497 /// drafts, `[0, 2, 3]` decoding as `[0, 1, 2]` — the whole tail of the
1498 /// stream shifted by one — and leaves 07-13 passing, which is exactly
1499 /// the blast radius the fix-up is scoped to.
1500 #[test]
1501 fn eliding_an_object_renumbers_the_rest_of_the_stream() {
1502 for &draft in DRAFTS {
1503 let objects: Vec<_> = (0..4).map(|id| object(id, 16)).collect();
1504 let stream = build_stream(draft, &objects);
1505
1506 let run = run_eliding(draft, &stream, stream.len(), FramerConfig::default(), &[1]);
1507
1508 assert_eq!(
1509 framed_object_ids(draft, &run.bytes),
1510 vec![0, 2, 3],
1511 "[{draft}] elided stream must decode to the surviving IDs"
1512 );
1513 }
1514 }
1515
1516 /// The elide survives an object the framer cannot address
1517 /// individually.
1518 ///
1519 /// Object 65 is over the buffer cap, so it leaves as `Passthrough` with
1520 /// no `ObjectMeta` for anyone outside the framer to name. A cursor
1521 /// owned by the caller could not renumber it, and its stale delta would
1522 /// shift every later object for the rest of the stream. The IDs are
1523 /// chosen so the correction changes the varint's *width*:
1524 /// `65 - 1 - 1 = 63` is one byte, `65 - 0 - 1 = 64` is two.
1525 ///
1526 /// *Ablation:* returning `Ok(None)` from `apply_elide_fixup` fails this
1527 /// on all six delta drafts, decoding `[0, 65, 66]` as `[0, 64, 65]`.
1528 #[test]
1529 fn an_oversized_object_after_an_elide_is_still_renumbered() {
1530 for &draft in DRAFTS {
1531 let objects = vec![object(0, 8), object(1, 8), object(65, 4096), object(66, 8)];
1532 let stream = build_stream(draft, &objects);
1533 let config = FramerConfig::new().with_max_buffered_object_bytes(64);
1534
1535 // Whole stream in one feed: the oversized object decodes
1536 // completely and `poll_object` routes it by its own wire length.
1537 let run = run_eliding(draft, &stream, stream.len(), config, &[1]);
1538 assert_eq!(
1539 run.framed_ids,
1540 vec![0, 1, 66],
1541 "[{draft}] object 65 left as passthrough and framing resumed on 66"
1542 );
1543 assert_eq!(
1544 framed_object_ids(draft, &run.bytes),
1545 vec![0, 65, 66],
1546 "[{draft}] oversized successor of an elide, decoded whole"
1547 );
1548 }
1549 }
1550
1551 /// The same, through `poll_oversized`: the object never arrives whole,
1552 /// so it is measured against padding and streamed out in chunks.
1553 ///
1554 /// This is the path where `passthrough_remaining` matters. It is
1555 /// computed from the *source* wire length while the emitted chunk is a
1556 /// byte longer, and the objects are picked so that gap is real.
1557 ///
1558 /// Restricted to the six delta drafts because they are the only ones
1559 /// whose object layout the codec can measure past an unarrived payload;
1560 /// on 07-13 a 4 KiB object with a 64-byte cap is out of measuring reach
1561 /// and the stream bypasses instead, which is a different property.
1562 ///
1563 /// *Ablation:* setting `passthrough_remaining = wire_len - emitted`
1564 /// instead of `wire_len - buffered` — the plausible-looking
1565 /// "correction" that redenominates the counter in emitted bytes when it
1566 /// has to stay in source bytes — fails this on all six drafts with
1567 /// `framed_ids == [0, 1]`: the
1568 /// framer leaves one payload byte of object 65 in the buffer, reads it
1569 /// as the head of the next object, and never frames object 66 at all.
1570 /// It fails **nothing else**, including this file's byte-level
1571 /// assertions, because the framer still emits every byte it was fed —
1572 /// just partitioned wrongly. That is why `Run` reports `framed_ids`:
1573 /// the first draft of this test asserted only on bytes and the
1574 /// ablation walked straight through it.
1575 ///
1576 /// Returning `Ok(None)` from `apply_elide_fixup` fails it earlier,
1577 /// with the re-decode reading `[0, 64, 65]`.
1578 #[test]
1579 fn an_elide_survives_an_object_streamed_through_in_chunks() {
1580 for &draft in DELTA_DRAFTS {
1581 let objects = vec![object(0, 8), object(1, 8), object(65, 4096), object(66, 8)];
1582 let stream = build_stream(draft, &objects);
1583 let config = FramerConfig::new().with_max_buffered_object_bytes(64);
1584
1585 let run = run_eliding(draft, &stream, 32, config, &[1]);
1586 // The accounting claim: `passthrough_remaining` is denominated
1587 // in source bytes, so the framer consumes object 65 exactly and
1588 // resynchronises on object 66's first byte — even though what
1589 // it emitted for object 65 is a byte longer than what it read.
1590 assert_eq!(
1591 run.framed_ids,
1592 vec![0, 1, 66],
1593 "[{draft}] framing must resume exactly on object 66"
1594 );
1595 assert_eq!(
1596 framed_object_ids(draft, &run.bytes),
1597 vec![0, 65, 66],
1598 "[{draft}] oversized successor of an elide, streamed through"
1599 );
1600 }
1601 }
1602
1603 /// A framer nobody elides on emits its input back, byte for byte.
1604 ///
1605 /// The regression fence for the fix-up code: it must be inert until
1606 /// [`ObjectFramer::note_elided`] is called. This is the property the
1607 /// acceptance suite rests on, restated where the code that could break
1608 /// it lives.
1609 ///
1610 /// *Ablation:* dropping the `self.fixup_pending` guard from
1611 /// `apply_elide_fixup` (rewriting every object unconditionally) still
1612 /// passes here, and passes `tests/framer_tests.rs` and
1613 /// `tests/object_framing_acceptance.rs` too — a minimally encoded delta
1614 /// re-encodes to itself, so the rewrite is invisible on any stream
1615 /// these build. `a_widened_object_id_field_is_not_re_encoded` below is
1616 /// the fence for that line; this test is a pin.
1617 #[test]
1618 fn a_stream_with_no_elide_is_emitted_byte_for_byte() {
1619 for &draft in DRAFTS {
1620 let objects: Vec<_> = (0..4).map(|id| object(id, 16)).collect();
1621 let stream = build_stream(draft, &objects);
1622
1623 for chunk in [1usize, 7, stream.len()] {
1624 let run = run_eliding(draft, &stream, chunk, FramerConfig::default(), &[]);
1625 assert_eq!(run.bytes, stream, "[{draft}] chunk {chunk}: byte identity");
1626 }
1627 }
1628 }
1629
1630 /// A non-minimally encoded Object ID field survives untouched when
1631 /// nothing was elided.
1632 ///
1633 /// QUIC varints may be written wider than they need to be, and a
1634 /// producer that does so is still emitting a legal stream. Re-encoding
1635 /// such a field would change bytes the proxy was asked to forward
1636 /// unchanged — so the fix-up must be reached only when an elide
1637 /// actually armed it, not merely when the draft delta-encodes.
1638 ///
1639 /// *Ablation:* dropping the `self.fixup_pending` guard from
1640 /// `apply_elide_fixup` fails this on all six delta drafts, and fails
1641 /// **nothing else** — not this file's other tests, not
1642 /// `tests/framer_tests.rs`, not `tests/object_framing_acceptance.rs` — because
1643 /// a minimally encoded field re-encodes to itself. This test is the
1644 /// only fence that line has.
1645 #[test]
1646 fn a_widened_object_id_field_is_not_re_encoded() {
1647 for &draft in DELTA_DRAFTS {
1648 let stream = build_stream(draft, &[object(0, 8), object(1, 8)]);
1649
1650 // Object 0 starts right after the five header bytes, and its
1651 // leading field is the one-byte varint 0. The two-byte form of the
1652 // same value differs by draft: `0x40 0x00` under RFC 9000, and
1653 // `0x80 0x00` from draft-17, where `0x40` is the one-byte 64.
1654 let head = header_bytes(draft).len();
1655 assert_eq!(stream[head], 0x00, "[{draft}] object 0's ID field is not where expected");
1656 let two_byte_zero: [u8; 2] =
1657 if draft.uses_moqt_varint() { [0x80, 0x00] } else { [0x40, 0x00] };
1658 let mut widened = stream[..head].to_vec();
1659 widened.extend_from_slice(&two_byte_zero);
1660 widened.extend_from_slice(&stream[head + 1..]);
1661
1662 let run = run_eliding(draft, &widened, widened.len(), FramerConfig::default(), &[]);
1663 assert_eq!(run.framed_ids, vec![0, 1], "[{draft}] widened field must still decode");
1664 assert_eq!(run.bytes, widened, "[{draft}] widened field must be forwarded verbatim");
1665 }
1666 }
1667
1668 /// A draft-16 subgroup header cannot set both the explicit-subgroup-ID
1669 /// bit and the first-object bit, so the proxy never has to choose
1670 /// between them.
1671 ///
1672 /// This test used to pin the choice. Setting `0x04` (explicit subgroup
1673 /// ID) and `0x02` (subgroup ID is the first object's) together puts
1674 /// bits one and two at `0b11`, and draft-16 Section 10.4.2 reserves
1675 /// that Subgroup ID mode. The decoder now refuses all eight bytes that
1676 /// spell it, so a header asking the question can no longer be built.
1677 ///
1678 /// What the old test recorded is still worth keeping, because it
1679 /// explains why the proxy reads the field through the uniform
1680 /// accessor: the header's own `subgroup_id_from_first_object`
1681 /// predicate used to look only at `0x02`, so a match asking only that
1682 /// question reported `None` for a subgroup ID sitting on the wire.
1683 /// That predicate reads the whole two-bit mode now, and the accessor
1684 /// already let the explicit mode win, matching the decoder. That
1685 /// behaviour is unchanged and is exercised by every valid
1686 /// explicit-mode header; only the contradictory input is gone.
1687 ///
1688 /// The fence moved rather than vanished — this now sweeps the whole
1689 /// reserved mode instead of pinning one byte of it.
1690 ///
1691 /// *Ablation:* accepting the reserved mode again — dropping the
1692 /// Subgroup ID mode arm from the draft-16 `validate_subgroup_type` —
1693 /// lets all eight headers decode, and this fails on the first:
1694 ///
1695 /// ```text
1696 /// thread 'framer::tests::draft16_refuses_the_reserved_subgroup_id_mode'
1697 /// panicked at crates\moqtap-proxy\src\framer.rs:1436:13:
1698 /// type 0x16 sets the reserved Subgroup ID mode and must be refused
1699 /// ```
1700 #[test]
1701 #[cfg(feature = "draft16")]
1702 fn draft16_refuses_the_reserved_subgroup_id_mode() {
1703 let draft = DraftVersion::Draft16;
1704 // Bits one and two are the Subgroup ID mode; 0b11 is reserved.
1705 // These are the eight subgroup types that set it, with and without
1706 // the extensions and priority bits.
1707 for ty in [0x16u8, 0x17, 0x1E, 0x1F, 0x36, 0x37, 0x3E, 0x3F] {
1708 // Track alias 1, group 0, subgroup 42, publisher priority 128 —
1709 // a complete body, so a refusal is of the type and never of a
1710 // short buffer.
1711 let head = vec![ty, 0x01, 0x00, 0x2A, 0x80];
1712 let mut cursor: &[u8] = &head;
1713 assert!(
1714 AnySubgroupHeader::decode_stream(draft, &mut cursor).is_err(),
1715 "type {ty:#04x} sets the reserved Subgroup ID mode and must be refused",
1716 );
1717 }
1718 }
1719
1720 /// The cursor advances lazily, one object behind the framer.
1721 ///
1722 /// An object's disposition is not known when it is emitted — the caller
1723 /// decides afterwards — so `last_forwarded_id` moves at the top of the
1724 /// *next* `poll_object`. The fix-up flag is the part that must be
1725 /// visible immediately, because it is what a bypass has to report as
1726 /// still owed.
1727 ///
1728 /// *Ablation:* committing eagerly (adding
1729 /// `self.last_forwarded_id = Some(object_id)` where
1730 /// `pending_disposition` is assigned) makes the elided object count as
1731 /// forwarded. This test fails at its first cursor assertion, on
1732 /// draft-07 — `Some(0)` where `None` is owed — and the three elide
1733 /// tests fail alongside it from draft-14 on, reading `[0, 1, 2]` and
1734 /// `[0, 64, 65]`.
1735 #[test]
1736 fn the_elide_cursor_tracks_the_last_object_actually_forwarded() {
1737 for &draft in DRAFTS {
1738 let delta = DELTA_DRAFTS.contains(&draft);
1739 let objects: Vec<_> = (0..3).map(|id| object(id, 8)).collect();
1740 let stream = build_stream(draft, &objects);
1741
1742 let counters = Arc::new(Recorder::new());
1743 let mut framer = framer_for(draft, FramerConfig::default(), &counters);
1744 framer.feed(&stream);
1745
1746 assert_eq!(
1747 framer.elide_cursor(),
1748 ElideCursor { last_forwarded_id: None, fixup_pending: false },
1749 "[{draft}] fresh framer"
1750 );
1751
1752 assert!(matches!(framer.poll(), FramerOut::Header { .. }));
1753
1754 // Object 0, forwarded.
1755 let FramerOut::Object { meta, .. } = framer.poll() else {
1756 panic!("[{draft}] expected object 0");
1757 };
1758 assert_eq!(meta.object_id, 0);
1759 assert_eq!(
1760 framer.elide_cursor().last_forwarded_id,
1761 None,
1762 "[{draft}] object 0's disposition is not known yet"
1763 );
1764
1765 // Object 1, elided. Polling for it commits object 0 first.
1766 let FramerOut::Object { meta, .. } = framer.poll() else {
1767 panic!("[{draft}] expected object 1");
1768 };
1769 assert_eq!(meta.object_id, 1);
1770 assert_eq!(
1771 framer.elide_cursor().last_forwarded_id,
1772 Some(0),
1773 "[{draft}] object 0 committed as forwarded"
1774 );
1775 framer.note_elided(&meta);
1776 assert_eq!(
1777 framer.elide_cursor(),
1778 ElideCursor { last_forwarded_id: Some(0), fixup_pending: delta },
1779 "[{draft}] the elide is visible at once, and only delta drafts owe a fix-up"
1780 );
1781
1782 // Object 2 clears the fix-up as it is emitted, and the cursor
1783 // still names object 0 until the object after it is asked for.
1784 let FramerOut::Object { meta, .. } = framer.poll() else {
1785 panic!("[{draft}] expected object 2");
1786 };
1787 assert_eq!(meta.object_id, 2);
1788 assert_eq!(
1789 framer.elide_cursor(),
1790 ElideCursor { last_forwarded_id: Some(0), fixup_pending: false },
1791 "[{draft}] the fix-up is spent on the object that carried it"
1792 );
1793
1794 assert!(matches!(framer.poll(), FramerOut::NeedMore));
1795 assert_eq!(
1796 framer.elide_cursor().last_forwarded_id,
1797 Some(2),
1798 "[{draft}] object 2 committed as forwarded"
1799 );
1800 }
1801 }
1802
1803 /// A bypass is reported once, as a zero-byte item, on the poll *after*
1804 /// the one that decided it.
1805 ///
1806 /// A draft-19 fetch stream is the cleanest case: the header decodes,
1807 /// so it is emitted, and only then is the stream found to be one whose
1808 /// objects are not addressed. `latch_bypass` cannot return the
1809 /// `Bypassed` item from that poll — the poll already owes a `Header` —
1810 /// so it is deferred.
1811 ///
1812 /// *Ablation:* dropping the `pending_bypass` drain from the top of
1813 /// `poll` fails this at the second poll with a `Passthrough`, and the
1814 /// stream's whole reason for not being addressable becomes
1815 /// unreportable — which is the state `BypassReason` was in before this
1816 /// unit: declared, and never constructed.
1817 #[test]
1818 #[cfg(feature = "draft19")]
1819 fn a_bypass_is_reported_once_on_the_poll_after_the_item_that_decided_it() {
1820 let draft = DraftVersion::Draft19;
1821 // Fetch stream type 0x05, request ID 42, then four bytes of
1822 // whatever the publisher was sending.
1823 let stream = vec![0x05u8, 0x2A, 0xDE, 0xAD, 0xBE, 0xEF];
1824
1825 let counters = Arc::new(Recorder::new());
1826 let mut framer = ObjectFramer::with_recorder(
1827 DataStreamType::Fetch,
1828 draft,
1829 FramerConfig::default(),
1830 Arc::clone(&counters),
1831 );
1832 framer.feed(&stream);
1833
1834 let FramerOut::Header { raw, .. } = framer.poll() else {
1835 panic!("expected the fetch header");
1836 };
1837 assert_eq!(&raw[..], &stream[..2], "the header is still emitted in full");
1838 assert!(framer.is_bypassed(), "the bypass was decided before the header was handed out");
1839
1840 assert!(
1841 matches!(
1842 framer.poll(),
1843 FramerOut::Bypassed {
1844 reason: BypassReason::FetchGroupOrderUnknown,
1845 fixup_owed: false
1846 }
1847 ),
1848 "the bypass follows the header rather than replacing it"
1849 );
1850
1851 let FramerOut::Passthrough(rest) = framer.poll() else {
1852 panic!("expected the remainder to be forwarded uninterpreted");
1853 };
1854 assert_eq!(&rest[..], &stream[2..], "no byte is lost to the bypass");
1855
1856 assert!(matches!(framer.poll(), FramerOut::NeedMore));
1857 assert!(matches!(framer.poll(), FramerOut::NeedMore), "reported exactly once");
1858
1859 let c = counters.snapshot();
1860 assert_eq!(c.framers_created, 1);
1861 assert_eq!(c.framer_header_polls, 1);
1862 assert_eq!(c.framer_object_polls, 0, "a bypassed stream never enters the object path");
1863 assert_eq!(c.streams_not_shapeable, 1);
1864 assert_eq!(c.objects_not_addressable, 0, "no object was ever decoded");
1865 }
1866
1867 /// A bypass while a fix-up is owed says so, because the bytes still in
1868 /// flight decode to Object IDs one ahead of what was delivered.
1869 ///
1870 /// Objects 0 and 1 are written normally and object 1 is elided, which
1871 /// arms the fix-up. The third object is a hand-written all-ones varint —
1872 /// `2^62 - 1` in eight bytes under RFC 9000, `2^64 - 1` in nine from
1873 /// draft-17. Either is legal and its delta resolves to an Object ID above
1874 /// the ceiling, so `read_object_meta` returns `InvalidField` rather than
1875 /// an incomplete-input error and the framer abandons the stream with the
1876 /// fix-up unspent.
1877 ///
1878 /// *Ablation:* reporting `fixup_owed: false` unconditionally passes
1879 /// every other test in this file — nothing else reads the flag — and
1880 /// leaves the session forwarding a tail that renumbers itself.
1881 #[test]
1882 fn a_bypass_that_strands_a_fix_up_reports_it_as_owed() {
1883 for &draft in DELTA_DRAFTS {
1884 let mut stream = build_stream(draft, &[object(0, 8), object(1, 8)]);
1885 // All-ones is the largest varint the draft's encoding can express:
1886 // eight bytes under RFC 9000, nine from draft-17.
1887 if draft.uses_moqt_varint() {
1888 stream.extend_from_slice(&[0xFFu8; 9]);
1889 } else {
1890 stream.extend_from_slice(&[0xFFu8; 8]);
1891 }
1892
1893 let counters = Arc::new(Recorder::new());
1894 let mut framer = framer_for(draft, FramerConfig::default(), &counters);
1895 framer.feed(&stream);
1896
1897 assert!(matches!(framer.poll(), FramerOut::Header { .. }));
1898 assert!(matches!(framer.poll(), FramerOut::Object { .. }), "[{draft}] object 0");
1899 let FramerOut::Object { meta, .. } = framer.poll() else {
1900 panic!("[{draft}] expected object 1");
1901 };
1902 framer.note_elided(&meta);
1903 assert!(framer.elide_cursor().fixup_pending, "[{draft}] the fix-up is armed");
1904
1905 assert!(
1906 matches!(framer.poll(), FramerOut::Error(_)),
1907 "[{draft}] the third object must fail to decode outright"
1908 );
1909 assert!(
1910 matches!(
1911 framer.poll(),
1912 FramerOut::Bypassed { reason: BypassReason::DecodeError, fixup_owed: true }
1913 ),
1914 "[{draft}] the stranded fix-up must be reported"
1915 );
1916
1917 assert_eq!(counters.snapshot().streams_not_shapeable, 1, "[{draft}] one latch");
1918 assert_eq!(
1919 counters.snapshot().object_ids_rewritten,
1920 0,
1921 "[{draft}] the fix-up never got the chance to be written"
1922 );
1923 }
1924 }
1925
1926 /// The counters move where — and only where — the slow path ran.
1927 ///
1928 /// This is the falsifiable half of the `Interest::NONE` claim: the
1929 /// framer is the slow path, so a framer that counts nothing makes the
1930 /// proof pass by measuring nothing.
1931 ///
1932 /// *Ablation:* passing a throwaway `Recorder` from `with_recorder`
1933 /// instead of the caller's — the shape `ObjectFramer::new` has by
1934 /// design — leaves every assertion here reading 0.
1935 #[test]
1936 fn the_framer_counts_the_slow_path_work_it_did() {
1937 for &draft in DRAFTS {
1938 let delta = DELTA_DRAFTS.contains(&draft);
1939 let objects: Vec<_> = (0..4).map(|id| object(id, 16)).collect();
1940 let stream = build_stream(draft, &objects);
1941
1942 let run = run_eliding(draft, &stream, stream.len(), FramerConfig::default(), &[1]);
1943 let c = run.counters.snapshot();
1944
1945 assert_eq!(c.framers_created, 1, "[{draft}]");
1946 assert_eq!(c.framer_header_polls, 1, "[{draft}] one poll decoded the header");
1947 // Four objects plus the poll that returned `NeedMore`.
1948 assert_eq!(c.framer_object_polls, 5, "[{draft}]");
1949 assert_eq!(
1950 c.object_ids_rewritten,
1951 u64::from(delta),
1952 "[{draft}] one rewrite, and only where IDs are deltas"
1953 );
1954 assert_eq!(c.objects_not_addressable, 0, "[{draft}] every object was framed");
1955 assert_eq!(c.streams_not_shapeable, 0, "[{draft}] nothing was bypassed");
1956 }
1957 }
1958
1959 /// An object too big to buffer is counted as unaddressable exactly
1960 /// once, on both of the paths that produce one.
1961 ///
1962 /// The whole-stream feed routes object 65 through `poll_object`'s
1963 /// oversized branch; the 32-byte feed routes it through
1964 /// `poll_oversized`, which emits it as several `Passthrough` chunks
1965 /// and must still count *one* object.
1966 ///
1967 /// *Ablation:* counting in `poll`'s `passthrough_remaining` branch
1968 /// instead — the obvious place, since that is where most of the bytes
1969 /// leave — reports 4 for the chunked run and 0 for the whole-stream
1970 /// one.
1971 #[test]
1972 fn an_unaddressable_object_is_counted_once_per_object_not_per_chunk() {
1973 for &draft in DELTA_DRAFTS {
1974 let objects = vec![object(0, 8), object(1, 8), object(65, 4096), object(66, 8)];
1975 let stream = build_stream(draft, &objects);
1976 let config = FramerConfig::new().with_max_buffered_object_bytes(64);
1977
1978 for chunk in [stream.len(), 32] {
1979 let run = run_eliding(draft, &stream, chunk, config.clone(), &[1]);
1980 let c = run.counters.snapshot();
1981 assert_eq!(
1982 c.objects_not_addressable, 1,
1983 "[{draft}] chunk {chunk}: one object, however many chunks it left in"
1984 );
1985 assert_eq!(c.object_ids_rewritten, 1, "[{draft}] chunk {chunk}");
1986 assert_eq!(c.streams_not_shapeable, 0, "[{draft}] chunk {chunk}: no bypass");
1987 }
1988 }
1989 }
1990}