Skip to main content

moqtap_client/draft15/
connection.rs

1use bytes::{Buf, Bytes, BytesMut};
2
3use crate::draft15::endpoint::{Endpoint, EndpointError};
4use crate::draft15::event::{ClientEvent, Direction, StreamKind};
5use crate::draft15::observer::ConnectionObserver;
6use crate::draft15::session::request_id::Role;
7use crate::draft15::session::setup;
8use crate::forwarding_preference::ObjectForwardingPreference;
9use crate::malformed_tracks::MalformedTrackCondition;
10use crate::track_locations::{ObjectLocation, ObjectRole, TrackObjects};
11use crate::transport::{RecvStream, SendStream, Transport, TransportError};
12use moqtap_codec::dispatch::{
13    AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
14};
15use moqtap_codec::draft15::data_stream::{
16    FetchHeader, FetchObjectHeader, FetchObjectReader, SubgroupObject, SubgroupObjectReader,
17};
18use moqtap_codec::draft15::message::ControlMessage;
19use moqtap_codec::error::CodecError;
20use moqtap_codec::kvp::KeyValuePair;
21use moqtap_codec::types::*;
22use moqtap_codec::varint::VarInt;
23use moqtap_codec::version::DraftVersion;
24
25/// The ALPN identifier draft-15 uses on raw QUIC, `moqt-15`.
26///
27/// Drafts 07 to 14 share one ALPN, `moq-00`, and a peer that offers it has
28/// said nothing about which of the eight it speaks. Draft-15 ended that:
29/// from there each draft has an ALPN of its own, so the version is settled
30/// by the TLS handshake before a byte of MoQT is written.
31///
32/// This is [`DraftVersion::Draft15`]'s own
33/// [`quic_alpn`](DraftVersion::quic_alpn), which is what
34/// [`ClientConfig::alpn`] offers; the test below holds the two together.
35pub const MOQT_ALPN: &[u8] = b"moqt-15";
36
37/// Errors from the connection layer.
38#[derive(Debug, thiserror::Error)]
39pub enum ConnectionError {
40    /// Endpoint state machine error.
41    #[error("endpoint error: {0}")]
42    Endpoint(#[from] EndpointError),
43    /// Wire codec error.
44    #[error("codec error: {0}")]
45    Codec(#[from] CodecError),
46    /// Transport-level error.
47    #[error("transport error: {0}")]
48    Transport(#[from] TransportError),
49    /// Variable-length integer decoding error.
50    #[error("varint error: {0}")]
51    VarInt(#[from] moqtap_codec::varint::VarIntError),
52    /// Control stream was not opened.
53    #[error("control stream not open")]
54    NoControlStream,
55    /// Stream ended before a complete message was read.
56    #[error("unexpected end of stream")]
57    UnexpectedEnd,
58    /// Stream was finished by the peer.
59    #[error("stream finished")]
60    StreamFinished,
61    /// Invalid server address string.
62    #[error("invalid server address: {0}")]
63    InvalidAddress(String),
64    /// TLS configuration error.
65    #[error("TLS config error: {0}")]
66    TlsConfig(String),
67    /// Data stream used out of order (e.g. object before header).
68    #[error("data stream state error: {0}")]
69    DataStreamState(&'static str),
70    /// A control message this build decoded for draft-15 and then could not
71    /// narrow to draft-15's own message type.
72    ///
73    /// Unreachable, and that is not the same as harmless. `read_control`
74    /// decodes with this connection's own draft, so the `AnyControlMessage` it
75    /// hands back can only carry this draft's variant — but the narrowing arm
76    /// is compiled in every configuration anyway, under
77    /// `#[allow(unreachable_patterns)]` rather than a `cfg` naming the other
78    /// drafts, because such a list has to be edited in every per-draft
79    /// module whenever a draft is added and a copy that omits one leaves the
80    /// match non-exhaustive.
81    ///
82    /// Spelled as `CodecError::UnknownMessageType(0)` it would not stay inert:
83    /// every draft's
84    /// [`codec_session_error_code`](Connection::codec_session_error_code)
85    /// answers that variant `Some(PROTOCOL_VIOLATION)`. So the day the
86    /// narrowing did fail, this build's own defect would reach a caller as *the
87    /// peer sent a control message type this draft does not assign, and the
88    /// session must be closed with a Protocol Violation* — carrying `0x00` as
89    /// the codepoint that proved it. A conformance report reading that
90    /// publishes a named, well-evidenced accusation against a relay for
91    /// something no relay did.
92    ///
93    /// A variant of its own is what stops that.
94    /// [`draft_specific_cause`](Connection::draft_specific_cause) answers it
95    /// [`LocalRefusal`], the facade turns that into [`ErrorCause::Facade`], and
96    /// nothing downstream can read a rule out of a cause that says nothing
97    /// reached the wire. What is pinned is the consequence rather than the
98    /// unreachability: nothing pins the arm's reachability, which is exactly
99    /// why the consequence must not be an accusation.
100    ///
101    /// [`LocalRefusal`]: crate::above_codec_rules::DraftSpecificCause::LocalRefusal
102    /// [`ErrorCause::Facade`]: crate::dispatch::ErrorCause::Facade
103    #[error(
104        "a control message decoded for draft-15 did not narrow to draft-15: a defect in this build, and evidence about nothing the peer did"
105    )]
106    ControlMessageNarrowing,
107    /// An Object arrived carrying extension headers on a status that is not
108    /// Normal.
109    ///
110    /// Draft-15 Section 10.2.1.2: "Any Object with status Normal can have
111    /// extension headers. If an endpoint receives extension headers on Objects
112    /// with status that is not Normal, it MUST close the session with a
113    /// PROTOCOL_VIOLATION."
114    ///
115    /// The codec's `read_object` refuses such an Object as a bare
116    /// `InvalidField`. Its `read_object_meta` frames it, which is how the
117    /// receive path names the rule here and stays in step with the wire.
118    ///
119    /// [`Connection::close_for_data_stream`] performs the close the sentence
120    /// above requires. It is a separate call because the reader that raises
121    /// this holds no connection, and because a deliberately permissive caller
122    /// should be able to read a violating stream and report it without tearing
123    /// the session down.
124    #[error(
125        "object {object_id} carries {extensions_len} bytes of extension headers on status {status:?}, which is not Normal"
126    )]
127    ExtensionsOnNonNormalStatus {
128        /// The Object ID the extension headers arrived on.
129        object_id: u64,
130        /// Length in bytes of the extension-header block.
131        extensions_len: usize,
132        /// The Object's status, resolved through the encoding's elision rule.
133        ///
134        /// Spelled out in full because the glob import of `moqtap_codec::types`
135        /// brings a different `ObjectStatus` into this module.
136        status: moqtap_codec::draft15::types::ObjectStatus,
137    },
138    /// A datagram whose Type announces extension headers arrived with an
139    /// Extension Headers Length of 0.
140    ///
141    /// draft-15 Section 10.3.1: "If an endpoint receives a datagram with
142    /// Extensions Present as "Yes" and a Extension Headers Length of 0, it MUST
143    /// close the session with a PROTOCOL_VIOLATION."
144    ///
145    /// The codec's header decoder refuses such a datagram as a bare
146    /// `InvalidField`. [`Connection::recv_datagram`] names it here, and
147    /// [`Connection::close_for_data_stream`] closes the session over it.
148    #[error(
149        "the datagram for object {object_id} announces extension headers and has an Extension Headers Length of 0"
150    )]
151    EmptyDatagramExtensions {
152        /// The Object ID the datagram named.
153        object_id: u64,
154    },
155}
156
157/// [`ConnectionError::EmptyDatagramExtensions`] for a datagram the header
158/// decoder refused as `InvalidField`, when its empty extension block was the
159/// fault.
160///
161/// draft-15 Section 10.3.1 gives a Type's fields by bit: 0x01 Extensions
162/// present, 0x04 no Object ID, 0x08 no Publisher Priority.
163fn empty_datagram_extensions(data: &[u8]) -> Option<ConnectionError> {
164    use crate::datagram_prefix::{empty_extension_block, Prefix};
165    empty_extension_block(data, |datagram_type, _| {
166        (datagram_type & 0x01 != 0).then_some(Prefix {
167            object_id: datagram_type & 0x04 == 0,
168            priority: datagram_type & 0x08 == 0,
169        })
170    })
171    .map(|object_id| ConnectionError::EmptyDatagramExtensions { object_id })
172}
173
174impl From<crate::transport::DialError> for ConnectionError {
175    /// Maps a dial failure onto the variants this error already has, so a
176    /// caller matches `InvalidAddress` or `TlsConfig`.
177    ///
178    /// A `LocalSocket` failure maps to `InvalidAddress`, which the facade reads
179    /// as this side's failure
180    /// ([`ErrorCause::Facade`](crate::dispatch::ErrorCause::Facade)). The dial
181    /// phase stays on [`DialError::phase`](crate::transport::DialError::phase).
182    fn from(e: crate::transport::DialError) -> Self {
183        match e {
184            // Two variants, one arm, deliberately — see above.
185            crate::transport::DialError::InvalidAddress(s)
186            | crate::transport::DialError::LocalSocket(s) => ConnectionError::InvalidAddress(s),
187            crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
188            crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
189        }
190    }
191}
192
193/// Transport type for the connection.
194#[derive(Debug, Clone)]
195pub enum TransportType {
196    /// Raw QUIC via quinn. The `addr` field should be `host:port`.
197    Quic,
198    /// WebTransport via wtransport. The `url` field is the WebTransport URL.
199    WebTransport {
200        /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
201        url: String,
202    },
203}
204
205/// Configuration for a MoQT client connection.
206///
207/// Both `draft` and `transport` are required -- there is no `Default` impl.
208pub struct ClientConfig {
209    /// The MoQT draft version to use (primary, determines codec/framing).
210    pub draft: DraftVersion,
211    /// The transport type (QUIC or WebTransport).
212    pub transport: TransportType,
213    /// Whether to skip TLS certificate verification (for testing).
214    pub skip_cert_verification: bool,
215    /// Custom CA certificates to trust (DER-encoded).
216    pub ca_certs: Vec<Vec<u8>>,
217    /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
218    pub setup_parameters: Vec<KeyValuePair>,
219}
220
221impl ClientConfig {
222    /// Returns the ALPN protocol identifiers for the transport.
223    pub fn alpn(&self) -> Vec<Vec<u8>> {
224        match &self.transport {
225            TransportType::Quic => vec![self.draft.quic_alpn().to_vec()],
226            TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
227        }
228    }
229}
230
231/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
232pub struct FramedSendStream {
233    inner: SendStream,
234    draft: DraftVersion,
235    /// Stateful subgroup object writer (tracks delta encoding state and
236    /// extension-presence flag, seeded from the stream's
237    /// `SubgroupHeader`).
238    subgroup_io: Option<SubgroupObjectReader>,
239    /// Stateful fetch object writer, seeded from the stream's `FetchHeader`.
240    /// A fetch object inherits fields from the object before it, so the writer
241    /// has to have seen that object; without this the first one has nothing to
242    /// inherit from and is refused.
243    fetch_io: Option<FetchObjectReader>,
244}
245
246impl FramedSendStream {
247    /// Create a new framed send stream for the given draft version.
248    pub fn new(inner: SendStream, draft: DraftVersion) -> Self {
249        Self { inner, draft, subgroup_io: None, fetch_io: None }
250    }
251
252    /// Get the transport-level stream ID.
253    pub fn stream_id(&self) -> u64 {
254        self.inner.stream_id()
255    }
256
257    /// Write a control message to the stream with type+length framing.
258    /// Returns the raw bytes that were written (for event capture).
259    pub async fn write_control(
260        &mut self,
261        msg: &AnyControlMessage,
262    ) -> Result<Vec<u8>, ConnectionError> {
263        let mut buf = Vec::new();
264        msg.encode(&mut buf)?;
265        self.inner.write_all(&buf).await?;
266        Ok(buf)
267    }
268
269    /// Write a subgroup stream header. Also initializes the internal
270    /// delta-encoding state used by
271    /// [`FramedSendStream::write_subgroup_object`].
272    ///
273    /// The header is refused, and nothing is written, if its fields disagree
274    /// with its own stream type. That check has to happen here rather than at
275    /// the first object: the type is what every object after it is framed
276    /// against, so a header that went out saying the wrong thing cannot be
277    /// taken back.
278    pub async fn write_subgroup_header(
279        &mut self,
280        header: &AnySubgroupHeader,
281    ) -> Result<(), ConnectionError> {
282        let mut buf = Vec::new();
283        header.encode_stream_checked(&mut buf)?;
284        self.inner.write_all(&buf).await?;
285        // Clippy would rather see these two arms as an `if let`, and rustc rejects
286        // that in a single-draft build, where the pattern is irrefutable. Only a
287        // `match` satisfies both.
288        #[allow(clippy::single_match)]
289        match header {
290            AnySubgroupHeader::Draft15(ref d15) => {
291                self.subgroup_io = Some(SubgroupObjectReader::new(d15));
292            }
293            // Only this draft's header seeds the object reader. With draft 15 the only enabled
294            // draft `AnySubgroupHeader` has a single variant, the arm above is exhaustive and this
295            // one unreachable. Compiled in every configuration with the lint allowed, rather than
296            // gated on a `cfg` naming the other drafts: such a list has to be edited in
297            // every draft module whenever a draft is added, and a copy that omits one leaves this
298            // match non-exhaustive.
299            #[allow(unreachable_patterns)]
300            _ => {}
301        }
302        Ok(())
303    }
304
305    /// Write a fetch response header.
306    pub async fn write_fetch_header(
307        &mut self,
308        header: &AnyFetchHeader,
309    ) -> Result<(), ConnectionError> {
310        let mut buf = Vec::new();
311        header.encode_stream(&mut buf);
312        self.inner.write_all(&buf).await?;
313        self.fetch_io = Some(FetchObjectReader::new());
314        Ok(())
315    }
316
317    /// Append a draft-15 subgroup object to the stream. Uses the
318    /// stateful writer seeded from
319    /// [`FramedSendStream::write_subgroup_header`] to produce correct
320    /// delta-encoded object IDs.
321    pub async fn write_subgroup_object(
322        &mut self,
323        object: &SubgroupObject,
324    ) -> Result<(), ConnectionError> {
325        let writer = self
326            .subgroup_io
327            .as_mut()
328            .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
329        let mut buf = Vec::new();
330        writer.write_object(object, &mut buf)?;
331        self.inner.write_all(&buf).await?;
332        Ok(())
333    }
334
335    /// Append a fetch object to the stream.
336    ///
337    /// The fetch stream had a header writer and no object writer, so a caller
338    /// could open one and put nothing on it through this type.
339    ///
340    /// Draft-15 delta-encodes a fetch object against the one before it, so this
341    /// needs the same stream state the subgroup path keeps, seeded by
342    /// [`write_fetch_header`](Self::write_fetch_header). An object that inherits
343    /// a field from a previous object that does not exist is refused there
344    /// rather than written as a zero.
345    ///
346    /// The declared length comes from the payload rather than from the caller's
347    /// field: a header that disagrees with the bytes beside it desynchronises
348    /// every object after it on the stream, and nothing downstream can recover.
349    ///
350    /// # Errors
351    ///
352    /// [`ConnectionError::DataStreamState`] if no fetch header has been written
353    /// on this stream, and [`ConnectionError::Codec`] for a header the writer
354    /// refuses.
355    pub async fn write_fetch_object(
356        &mut self,
357        header: &FetchObjectHeader,
358        payload: &[u8],
359    ) -> Result<(), ConnectionError> {
360        let writer = self
361            .fetch_io
362            .as_mut()
363            .ok_or(ConnectionError::DataStreamState("fetch header not written yet"))?;
364        let mut header = header.clone();
365        header.payload_length = VarInt::from_usize(payload.len());
366        let mut buf = Vec::new();
367        writer.write_object_header(&header, &mut buf)?;
368        buf.extend_from_slice(payload);
369        self.inner.write_all(&buf).await?;
370        Ok(())
371    }
372
373    /// Finish the stream (send FIN).
374    pub async fn finish(&mut self) -> Result<(), ConnectionError> {
375        self.inner.finish()?;
376        Ok(())
377    }
378
379    /// Returns the draft version this stream is framed for.
380    pub fn draft(&self) -> DraftVersion {
381        self.draft
382    }
383}
384
385/// What an Object Status makes of an object here.
386///
387/// Two answers where drafts 08 through 13 have three, and the missing one is
388/// the point: the end-of-track status settles where the track ended and is
389/// judged against nothing, because the rule about where one may be placed is
390/// not in this draft. `a_track_may_end_where_it_has_already_been.rs` asserts
391/// that acceptance.
392///
393/// Every other status is a statement about objects rather than one of them.
394fn object_role(status: Option<u64>) -> ObjectRole {
395    match status {
396        None | Some(0x0) => ObjectRole::Produced,
397        Some(0x4) => ObjectRole::EndsTrack(None),
398        _ => ObjectRole::Neither,
399    }
400}
401
402/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
403pub struct FramedRecvStream {
404    inner: RecvStream,
405    buf: BytesMut,
406    draft: DraftVersion,
407    /// Stateful subgroup object reader (tracks delta-decode state and
408    /// extension-presence flag).
409    subgroup_io: Option<SubgroupObjectReader>,
410    /// Stateful fetch object reader, holding the Object each following Object
411    /// may inherit its Group ID, Subgroup ID, Object ID and Priority from.
412    /// Seeded by [`FramedRecvStream::read_fetch_header`].
413    fetch_io: Option<FetchObjectReader>,
414    /// The record this stream's objects are measured against, and the Group ID
415    /// its header named.
416    ///
417    /// One group for the whole stream: a subgroup header names it once and no
418    /// object header repeats it. `None` on a stream that was never given one -
419    /// a stream for an alias no live binding names, and every stream built
420    /// outside [`Connection::accept_subgroup_stream`] - and such a stream reads
421    /// without being measured, because `note_subgroup_object` has nothing to
422    /// measure it against.
423    tracking: Option<(TrackObjects, u64)>,
424    /// The bytes of the last control frame a capturing read refused.
425    refused: Option<Vec<u8>>,
426}
427
428impl FramedRecvStream {
429    /// Create a new framed receive stream for the given draft version.
430    pub fn new(inner: RecvStream, draft: DraftVersion) -> Self {
431        Self {
432            inner,
433            buf: BytesMut::with_capacity(4096),
434            draft,
435            subgroup_io: None,
436            fetch_io: None,
437            tracking: None,
438            refused: None,
439        }
440    }
441
442    /// Get the transport-level stream ID.
443    pub fn stream_id(&self) -> u64 {
444        self.inner.stream_id()
445    }
446
447    /// The bytes of the control frame the last capturing read refused.
448    pub(crate) fn take_refused(&mut self) -> Option<Vec<u8>> {
449        self.refused.take()
450    }
451
452    /// Keep the first `len` buffered bytes as a refused frame when the read is
453    /// capturing, and hand `err` back.
454    fn refuse(&mut self, capture_raw: bool, len: usize, err: ConnectionError) -> ConnectionError {
455        if capture_raw {
456            self.refused = Some(self.buf[..len].to_vec());
457        }
458        err
459    }
460
461    /// Measure this stream's objects against `objects`, all of them in `group`.
462    ///
463    /// Called by [`Connection::accept_subgroup_stream`] once the header has
464    /// been read, which is the only point at which both the track and the group
465    /// are known.
466    fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
467        self.tracking = Some((objects, group));
468    }
469
470    /// Judge one object this stream carried against where its track ended.
471    ///
472    /// The object's Group ID is the stream's and its Object ID is its own,
473    /// already resolved from the delta the wire carries; what they are measured
474    /// against is the end an end-of-track object settled on any stream.
475    fn note_subgroup_object(
476        &self,
477        object: u64,
478        status: Option<u64>,
479    ) -> Result<(), ConnectionError> {
480        let Some((objects, group)) = &self.tracking else { return Ok(()) };
481        let at = ObjectLocation { group: *group, object };
482        objects.note_past_final(at, object_role(status)).map_err(|end| {
483            ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject {
484                alias: objects.alias(),
485                group: at.group,
486                object: at.object,
487                final_group: end.group,
488                final_object: end.object,
489            })
490        })
491    }
492
493    /// Read more data from the stream into the internal buffer.
494    async fn fill(&mut self) -> Result<bool, ConnectionError> {
495        let mut tmp = [0u8; 4096];
496        match self.inner.read(&mut tmp).await {
497            Ok(Some(n)) => {
498                self.buf.extend_from_slice(&tmp[..n]);
499                Ok(true)
500            }
501            Ok(None) => Ok(false),
502            Err(e) => Err(ConnectionError::Transport(e)),
503        }
504    }
505
506    /// Ensure at least `n` bytes are available in the buffer.
507    async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
508        while self.buf.len() < n {
509            if !self.fill().await? {
510                return Err(ConnectionError::UnexpectedEnd);
511            }
512        }
513        Ok(())
514    }
515
516    /// Read a control message from the stream.
517    ///
518    /// When `capture_raw` is true, the returned tuple includes a clone of the
519    /// framed wire bytes (for observer emission). When false, the second
520    /// element is `None` and the payload clone is skipped.
521    pub async fn read_control(
522        &mut self,
523        capture_raw: bool,
524    ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
525        self.refused = None;
526        // Read type ID varint
527        self.ensure(1).await?;
528        let type_len = varint_len(self.buf[0]);
529        self.ensure(type_len).await?;
530
531        let mut cursor = &self.buf[..type_len];
532        if let Err(e) = VarInt::decode(&mut cursor) {
533            return Err(self.refuse(capture_raw, type_len, e.into()));
534        }
535
536        // Draft-15: 16-bit BE payload length
537        let (payload_len, len_field_size) = if self.draft.uses_fixed_length_framing() {
538            self.ensure(type_len + 2).await?;
539            let hi = self.buf[type_len] as usize;
540            let lo = self.buf[type_len + 1] as usize;
541            ((hi << 8) | lo, 2)
542        } else {
543            self.ensure(type_len + 1).await?;
544            let payload_len_start = type_len;
545            let payload_len_varint_len = varint_len(self.buf[payload_len_start]);
546            self.ensure(type_len + payload_len_varint_len).await?;
547            let header_len = type_len + payload_len_varint_len;
548            let mut cursor = &self.buf[payload_len_start..header_len];
549            let payload_len = match VarInt::decode(&mut cursor) {
550                Ok(length) => length.into_inner() as usize,
551                Err(e) => return Err(self.refuse(capture_raw, header_len, e.into())),
552            };
553            (payload_len, payload_len_varint_len)
554        };
555
556        // Read full payload
557        let total = type_len + len_field_size + payload_len;
558        self.ensure(total).await?;
559
560        // Capture raw bytes only if requested (observer attached).
561        let raw = capture_raw.then(|| self.buf[..total].to_vec());
562
563        // Now decode the whole message
564        let mut frame = &self.buf[..total];
565        let msg = match AnyControlMessage::decode(self.draft, &mut frame) {
566            Ok(msg) => msg,
567            Err(e) => return Err(self.refuse(capture_raw, total, e.into())),
568        };
569        self.buf.advance(total);
570        Ok((msg, raw))
571    }
572
573    /// Read a subgroup stream header. Also initializes the internal
574    /// delta-decoding state.
575    pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
576        self.ensure(1).await?;
577        loop {
578            let mut cursor = &self.buf[..];
579            match AnySubgroupHeader::decode(self.draft, &mut cursor) {
580                Ok(header) => {
581                    let consumed = self.buf.len() - cursor.remaining();
582                    self.buf.advance(consumed);
583                    // Clippy would rather see these two arms as an `if let`, and rustc rejects
584                    // that in a single-draft build, where the pattern is irrefutable. Only a
585                    // `match` satisfies both.
586                    #[allow(clippy::single_match)]
587                    match header {
588                        AnySubgroupHeader::Draft15(ref d15) => {
589                            self.subgroup_io = Some(SubgroupObjectReader::new(d15));
590                        }
591                        // Only this draft's header seeds the object reader. With draft 15 the only
592                        // enabled draft `AnySubgroupHeader` has a single variant, the arm above is
593                        // exhaustive and this one unreachable. Compiled in every configuration with
594                        // the lint allowed, rather than gated on a `cfg` naming every other
595                        // draft: such a list has to be edited in every draft module whenever a
596                        // draft is added, and a copy that omits one leaves this match
597                        // non-exhaustive.
598                        #[allow(unreachable_patterns)]
599                        _ => {}
600                    }
601                    return Ok(header);
602                }
603                Err(e) if e.is_incomplete() => {
604                    if !self.fill().await? {
605                        return Err(ConnectionError::UnexpectedEnd);
606                    }
607                }
608                Err(e) => return Err(ConnectionError::Codec(e)),
609            }
610        }
611    }
612
613    /// Read a fetch response header, and seed the object reader that follows it.
614    ///
615    /// The seeding is what makes [`FramedRecvStream::read_fetch_object`] usable:
616    /// a draft-15 fetch object may leave out fields and take the prior Object's,
617    /// so the objects of one stream have to be read through one reader and the
618    /// header is where that reader begins.
619    pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
620        self.ensure(1).await?;
621        loop {
622            let mut cursor = &self.buf[..];
623            match AnyFetchHeader::decode(self.draft, &mut cursor) {
624                Ok(header) => {
625                    let consumed = self.buf.len() - cursor.remaining();
626                    self.buf.advance(consumed);
627                    self.fetch_io = Some(FetchObjectReader::new());
628                    return Ok(header);
629                }
630                Err(e) if e.is_incomplete() => {
631                    if !self.fill().await? {
632                        return Err(ConnectionError::UnexpectedEnd);
633                    }
634                }
635                Err(e) => return Err(ConnectionError::Codec(e)),
636            }
637        }
638    }
639
640    /// Read the next draft-15 subgroup object from this stream. Uses
641    /// the stateful reader seeded by
642    /// [`FramedRecvStream::read_subgroup_header`] to decode the
643    /// delta-encoded object ID and (when the stream type says so) the
644    /// extension block. Returns an error if called before a subgroup
645    /// header was read.
646    ///
647    /// Errors with [`ConnectionError::ExtensionsOnNonNormalStatus`] on an
648    /// Object that carries extension headers on a status other than Normal,
649    /// which draft-15 Section 10.2.1.2 answers with a session close. The Object
650    /// is consumed from the stream before the check, so the reader stays in
651    /// step with the wire and a caller that reports the violation and reads on
652    /// sees the following Object rather than a re-parse of this one.
653    pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
654        if self.subgroup_io.is_none() {
655            return Err(ConnectionError::DataStreamState("subgroup header not read yet"));
656        }
657        loop {
658            let reader = self.subgroup_io.as_mut().unwrap();
659            let mut probe = reader.clone();
660            let mut cursor = &self.buf[..];
661            match probe.read_object(&mut cursor) {
662                Ok(obj) => {
663                    let consumed = self.buf.len() - cursor.remaining();
664                    self.buf.advance(consumed);
665                    *reader = probe;
666                    if !obj.extensions_permitted() {
667                        return Err(ConnectionError::ExtensionsOnNonNormalStatus {
668                            object_id: obj.object_id.into_inner(),
669                            extensions_len: obj.extension_headers.len(),
670                            status: obj.status(),
671                        });
672                    }
673                    self.note_subgroup_object(
674                        obj.object_id.into_inner(),
675                        obj.object_status.map(|s| s as u64),
676                    )?;
677                    return Ok(obj);
678                }
679                Err(e) if e.is_incomplete() => {
680                    if !self.fill().await? {
681                        return Err(ConnectionError::UnexpectedEnd);
682                    }
683                }
684                // The codec refuses extension headers on a non-Normal status as
685                // `InvalidField`, one of many readings of that variant. Its
686                // framing-only reader accepts them, which tells this case apart
687                // from the others and keeps the stream in step.
688                Err(CodecError::InvalidField) => {
689                    let mut framing = reader.clone();
690                    let mut cursor = &self.buf[..];
691                    if let Ok(meta) = framing.read_object_meta(&mut cursor) {
692                        let status = meta
693                            .status
694                            .and_then(moqtap_codec::draft15::types::ObjectStatus::from_u64);
695                        if let Some(status) = status.filter(|s| {
696                            meta.extension_headers_len > 0
697                                && *s != moqtap_codec::draft15::types::ObjectStatus::Normal
698                        }) {
699                            let consumed = self.buf.len() - cursor.remaining();
700                            self.buf.advance(consumed);
701                            *reader = framing;
702                            return Err(ConnectionError::ExtensionsOnNonNormalStatus {
703                                object_id: meta.object_id,
704                                extensions_len: meta.extension_headers_len as usize,
705                                status,
706                            });
707                        }
708                    }
709                    return Err(ConnectionError::Codec(CodecError::InvalidField));
710                }
711                Err(e) => return Err(ConnectionError::Codec(e)),
712            }
713        }
714    }
715
716    /// Read the next draft-15 fetch header from this stream.
717    ///
718    /// The typed twin of [`read_fetch_header`](Self::read_fetch_header), and it
719    /// has to do the same two things that one does.
720    ///
721    /// It seeds the object reader, because the two consume the same bytes: a
722    /// version that left `fetch_io` unset would put the stream in a state no
723    /// caller can leave, with the next
724    /// [`read_fetch_object`](Self::read_fetch_object) returning its
725    /// `fetch header not read yet` refusal about a header this method has just
726    /// read, and the bytes it would need already spent.
727    ///
728    /// It fills before decoding, and treats a varint that ran out of buffer as
729    /// a short read rather than a malformed header. `FetchHeader::decode`
730    /// reports that as `CodecError::VarInt(VarIntError::UnexpectedEnd)` where
731    /// [`AnyFetchHeader`] reports a bare `CodecError::UnexpectedEnd`, so a loop
732    /// matching only the latter never reaches its own `fill` — and since the
733    /// buffer starts empty, that is every first call on a fresh stream.
734    pub async fn read_fetch_stream_header(&mut self) -> Result<FetchHeader, ConnectionError> {
735        self.ensure(1).await?;
736        loop {
737            let mut cursor = &self.buf[..];
738            match FetchHeader::decode(&mut cursor) {
739                Ok(hdr) => {
740                    let consumed = self.buf.len() - cursor.remaining();
741                    self.buf.advance(consumed);
742                    self.fetch_io = Some(FetchObjectReader::new());
743                    return Ok(hdr);
744                }
745                Err(CodecError::UnexpectedEnd)
746                | Err(CodecError::VarInt(moqtap_codec::varint::VarIntError::UnexpectedEnd)) => {
747                    if !self.fill().await? {
748                        return Err(ConnectionError::UnexpectedEnd);
749                    }
750                }
751                Err(e) => return Err(ConnectionError::Codec(e)),
752            }
753        }
754    }
755
756    /// Read the next draft-15 fetch object's header and payload.
757    ///
758    /// The mirror of [`FramedSendStream::write_fetch_object`], and it needs the
759    /// same state that one needs: draft-15 lets an Object leave out its Group
760    /// ID, Subgroup ID, Object ID and Priority and take the prior Object's, so
761    /// the reader carries the prior Object and this method is refused before
762    /// [`FramedRecvStream::read_fetch_header`] has seeded it.
763    ///
764    /// The header that comes back is resolved — every field is a value rather
765    /// than an inheritance — which is what makes draft-15 and draft-18
766    /// different from the three drafts either side of them.
767    ///
768    /// The payload comes back with the header because `payload_length` says how
769    /// many bytes follow it, and a reader that takes the wrong number of them
770    /// desynchronises every later object on the stream.
771    ///
772    /// # Errors
773    ///
774    /// [`ConnectionError::DataStreamState`] when no fetch header has been read,
775    /// [`ConnectionError::UnexpectedEnd`] when the stream ends inside the header
776    /// or inside the payload it declared, and [`ConnectionError::Codec`] on
777    /// every rule Section 10.4.4 states about the flags and about an Object that
778    /// inherits from one that does not exist.
779    ///
780    /// A Prior Group ID Gap or Prior Object ID Gap fault the stream shows is
781    /// returned as [`CodecError::MalformedTrack`], and an extension block that
782    /// does not parse as [`CodecError::MalformedObjectProperties`];
783    /// [`Connection::withdraw_for_fetch_stream`] withdraws from the track.
784    pub async fn read_fetch_object(
785        &mut self,
786    ) -> Result<(FetchObjectHeader, Vec<u8>), ConnectionError> {
787        if self.fetch_io.is_none() {
788            return Err(ConnectionError::DataStreamState("fetch header not read yet"));
789        }
790        let header = loop {
791            let reader = self.fetch_io.as_mut().unwrap();
792            // The reader carries the prior Object, so it is advanced on a probe
793            // and committed only once the whole header was there to read. A
794            // reader advanced by a short read would resolve the next Object
795            // against a half-read one.
796            let mut probe = reader.clone();
797            let mut cursor = &self.buf[..];
798            match probe.read_object_header(&mut cursor) {
799                Ok(header) => {
800                    let consumed = self.buf.len() - cursor.remaining();
801                    self.buf.advance(consumed);
802                    *reader = probe;
803                    break header;
804                }
805                Err(e) if e.is_incomplete() => {
806                    if !self.fill().await? {
807                        return Err(ConnectionError::UnexpectedEnd);
808                    }
809                }
810                Err(e) => return Err(ConnectionError::Codec(e)),
811            }
812        };
813        let payload = self.read_object_payload(&header.payload_length).await?;
814        Ok((header, payload))
815    }
816
817    /// Take the `length` payload bytes that follow a fetch object's header.
818    ///
819    /// Separate from the header read because the header is decoded from a probe
820    /// cursor that may have to be retried after a fill, and the payload is a
821    /// flat byte count that never is.
822    async fn read_object_payload(&mut self, length: &VarInt) -> Result<Vec<u8>, ConnectionError> {
823        let length = length.into_inner() as usize;
824        self.ensure(length).await?;
825        let payload = self.buf[..length].to_vec();
826        self.buf.advance(length);
827        Ok(payload)
828    }
829
830    /// Returns the draft version this stream is framed for.
831    pub fn draft(&self) -> DraftVersion {
832        self.draft
833    }
834}
835
836/// A live MoQT connection over QUIC or WebTransport, combining the endpoint
837/// state machine with actual network I/O.
838pub struct Connection {
839    transport: Transport,
840    endpoint: Endpoint,
841    draft: DraftVersion,
842    /// Behind a lock because a control message is written from two kinds of
843    /// place. Most of them are the caller's own request, made through `&mut
844    /// self`. The messages that answer a Malformed Track are not: the
845    /// conditions that make a track malformed are detected on the data plane,
846    /// where this connection is reached through a shared reference. The lock
847    /// also makes one message the unit of writing, so two of them cannot
848    /// interleave on the stream.
849    control_send: Option<tokio::sync::Mutex<FramedSendStream>>,
850    control_recv: Option<FramedRecvStream>,
851    observer: Option<Box<dyn ConnectionObserver>>,
852    /// Setup events buffered during `connect()` and replayed when an
853    /// observer attaches via `set_observer` — without this, an observer
854    /// attached after `connect` returns would never see the handshake.
855    pending_events: Vec<ClientEvent>,
856    /// The server's half of the setup handshake, kept whole.
857    ///
858    /// The endpoint acts on the parameters it recognises and retains none of
859    /// them, and which parameters a server sends — in what order, with what
860    /// values — is the sharpest thing a session says about the implementation
861    /// behind it.
862    server_setup: AnyControlMessage,
863    /// The framed wire bytes of [`Self::server_setup`].
864    server_setup_raw: Option<Vec<u8>>,
865}
866
867impl Connection {
868    /// Connect to a MoQT server as a client.
869    ///
870    /// Establishes a QUIC or WebTransport connection (based on
871    /// `config.transport`), opens a bidirectional control stream,
872    /// performs the CLIENT_SETUP / SERVER_SETUP handshake, and returns
873    /// a ready-to-use connection.
874    pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
875        // PATH is for native QUIC only, and the transport is known here and
876        // nowhere further in. Refusing before dialling means a session that
877        // the server would close on sight is never opened.
878        setup::validate_client_path_transport(
879            &config.setup_parameters,
880            matches!(config.transport, TransportType::WebTransport { .. }),
881        )
882        .map_err(EndpointError::from)?;
883
884        let transport = match &config.transport {
885            TransportType::Quic => Self::connect_quic(addr, &config).await?,
886            TransportType::WebTransport { url } => {
887                let url = url.clone();
888                Self::connect_webtransport(&url, &config).await?
889            }
890        };
891
892        Self::adopt(transport, config).await
893    }
894
895    /// Run the MoQT setup handshake over a transport somebody else established.
896    ///
897    /// For choosing the draft from what the server selected: dial once through
898    /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
899    /// connection to the module its answer names. [`Self::connect`] cannot do
900    /// this — it derives its single ALPN from the draft it was given.
901    ///
902    /// `config.draft` must match this module. The transport is adopted as
903    /// given; nothing here re-checks the ALPN it was negotiated with.
904    pub async fn adopt(
905        transport: Transport,
906        config: ClientConfig,
907    ) -> Result<Self, ConnectionError> {
908        Self::adopt_with(transport, config, None).await
909    }
910
911    /// [`Self::adopt`] with `observer` attached before the handshake.
912    ///
913    /// The SETUP this side sends is reported as it is written, and a SETUP from
914    /// the peer that this side refuses as [`ClientEvent::ControlMessageRefused`].
915    /// [`Self::set_observer`] after `adopt` sees the same handshake events, but
916    /// only once the handshake has succeeded.
917    pub async fn adopt_observed(
918        transport: Transport,
919        config: ClientConfig,
920        observer: Box<dyn ConnectionObserver>,
921    ) -> Result<Self, ConnectionError> {
922        Self::adopt_with(transport, config, Some(observer)).await
923    }
924
925    /// [`Self::adopt`], reporting to `observer` from the first byte written.
926    pub(crate) async fn adopt_with(
927        transport: Transport,
928        config: ClientConfig,
929        observer: Option<Box<dyn ConnectionObserver>>,
930    ) -> Result<Self, ConnectionError> {
931        let draft = config.draft;
932        // PATH is for native QUIC only, and the transport is known here and
933        // nowhere further in. Refusing before dialling means a session that
934        // the server would close on sight is never opened.
935        setup::validate_client_path_transport(
936            &config.setup_parameters,
937            matches!(config.transport, TransportType::WebTransport { .. }),
938        )
939        .map_err(EndpointError::from)?;
940
941        // Open bidirectional control stream
942        let (send, recv) = transport.open_bi().await?;
943        let mut control_send = FramedSendStream::new(send, draft);
944        let mut control_recv = FramedRecvStream::new(recv, draft);
945
946        // Perform setup handshake (draft-15: no versions)
947        let mut endpoint = Endpoint::new(Role::Client);
948        endpoint.connect()?;
949        let setup_msg = endpoint.send_client_setup(config.setup_parameters.clone())?;
950        let any_setup = AnyControlMessage::Draft15(setup_msg);
951        let raw_setup = control_send.write_control(&any_setup).await?;
952        // Reported as written, so a peer that never answers still leaves it
953        // observed.
954        if let Some(obs) = &observer {
955            obs.on_event_owned(ClientEvent::ControlMessage {
956                direction: Direction::Send,
957                message: any_setup.clone(),
958                raw: Some(raw_setup.clone()),
959            });
960        }
961
962        let (server_setup, raw_server_setup) = match control_recv.read_control(true).await {
963            Ok(read) => read,
964            Err(e) => {
965                if let Some(obs) = &observer {
966                    if let Some(event) = Self::refused_event(&e, control_recv.take_refused()) {
967                        obs.on_event_owned(event);
968                    }
969                }
970                return Err(e);
971            }
972        };
973        // Unwrap to draft-15 for the endpoint
974        match &server_setup {
975            AnyControlMessage::Draft15(ControlMessage::ServerSetup(ref ss)) => {
976                if let Err(e) = endpoint.receive_server_setup(ss) {
977                    let err = ConnectionError::from(e);
978                    Self::refuse_setup(observer.as_deref(), &err, &server_setup, &raw_server_setup);
979                    return Err(err);
980                }
981            }
982            _ => {
983                return Err(ConnectionError::Endpoint(EndpointError::NotActive));
984            }
985        }
986
987        let mut pending_events = vec![
988            ClientEvent::ControlMessage {
989                direction: Direction::Send,
990                message: any_setup,
991                raw: Some(raw_setup),
992            },
993            ClientEvent::ControlMessage {
994                direction: Direction::Receive,
995                message: server_setup.clone(),
996                raw: raw_server_setup.clone(),
997            },
998            ClientEvent::SetupComplete { negotiated_version: 0xff000000 + 15 },
999        ];
1000
1001        if let Some(obs) = &observer {
1002            // The SETUP this side sent was reported as it was written.
1003            for event in pending_events.drain(..).skip(1) {
1004                obs.on_event_owned(event);
1005            }
1006        }
1007
1008        Ok(Self {
1009            transport,
1010            endpoint,
1011            draft,
1012            control_send: Some(tokio::sync::Mutex::new(control_send)),
1013            control_recv: Some(control_recv),
1014            observer,
1015            pending_events,
1016            server_setup,
1017            server_setup_raw: raw_server_setup,
1018        })
1019    }
1020
1021    /// Establish a raw QUIC connection.
1022    ///
1023    /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
1024    /// TLS and endpoint setup.
1025    async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
1026        let (transport, _negotiated) = crate::transport::dial_quic(
1027            addr,
1028            &crate::transport::QuicDialOptions {
1029                skip_cert_verification: config.skip_cert_verification,
1030                ca_certs: config.ca_certs.clone(),
1031                ..crate::transport::QuicDialOptions::new(config.alpn())
1032            },
1033        )
1034        .await?;
1035        Ok(transport)
1036    }
1037
1038    /// Establish a WebTransport connection.
1039    ///
1040    /// [`crate::transport::dial_webtransport`] holds the TLS and endpoint
1041    /// setup, exactly as `connect_quic` above defers its own. That is not
1042    /// only deduplication: both dials must trust the same roots. Settling trust
1043    /// at this call site instead — from `wtransport`'s own builder settings, or
1044    /// from a second config of this draft's own — puts the decision in two
1045    /// places, where it can stop matching what the QUIC dial trusts, so one
1046    /// relay would pass on one transport and fail on the other and a caller's
1047    /// private CA would reach only the dials whose call site installed it.
1048    /// Both ask the same function what to trust.
1049    #[cfg(feature = "webtransport")]
1050    async fn connect_webtransport(
1051        url: &str,
1052        config: &ClientConfig,
1053    ) -> Result<Transport, ConnectionError> {
1054        Ok(crate::transport::dial_webtransport(
1055            url,
1056            &crate::transport::QuicDialOptions {
1057                skip_cert_verification: config.skip_cert_verification,
1058                ca_certs: config.ca_certs.clone(),
1059                // The draft's own protocol identifier. Section 3 gives this
1060                // draft two version-negotiation channels and one of them per
1061                // transport: an ALPN over QUIC, and the WT-Available-Protocols
1062                // header over WebTransport. `config.alpn()` above is `h3`,
1063                // which is the HTTP/3 name and settles no version, so without
1064                // this the draft would be named nowhere.
1065                wt_protocols: vec![config.draft.quic_alpn().to_vec()],
1066                ..crate::transport::QuicDialOptions::new(config.alpn())
1067            },
1068        )
1069        .await?)
1070    }
1071
1072    /// Stub for when the webtransport feature is not enabled.
1073    #[cfg(not(feature = "webtransport"))]
1074    async fn connect_webtransport(
1075        _url: &str,
1076        _config: &ClientConfig,
1077    ) -> Result<Transport, ConnectionError> {
1078        Err(ConnectionError::Transport(TransportError::Connect(
1079            "webtransport feature not enabled".into(),
1080        )))
1081    }
1082
1083    // -- Observer ---------------------------------------------------
1084
1085    /// Attach an observer. Buffered handshake events from `connect()` are
1086    /// flushed in arrival order before this returns.
1087    pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
1088        self.observer = Some(observer);
1089        for event in self.pending_events.drain(..) {
1090            if let Some(ref obs) = self.observer {
1091                obs.on_event_owned(event);
1092            }
1093        }
1094    }
1095
1096    /// Remove the observer.
1097    pub fn clear_observer(&mut self) {
1098        self.observer = None;
1099    }
1100
1101    /// How the QUIC connection was closed, once it has been. See
1102    /// [`TransportClose`](crate::transport::TransportClose).
1103    pub fn transport_close(&self) -> Option<crate::transport::TransportClose> {
1104        self.transport.transport_close()
1105    }
1106
1107    /// Emit an event to the observer, if one is attached.
1108    fn emit(&self, event: ClientEvent) {
1109        if let Some(ref obs) = self.observer {
1110            obs.on_event_owned(event);
1111        }
1112    }
1113
1114    /// Report a received frame this endpoint refused, when an observer is
1115    /// attached and the refusal is the peer's: see [`Self::refused_event`].
1116    fn emit_refused(&self, err: &ConnectionError, raw: Option<Vec<u8>>) {
1117        if self.observer.is_some() {
1118            if let Some(event) = Self::refused_event(err, raw) {
1119                self.emit(event);
1120            }
1121        }
1122    }
1123
1124    /// The [`ClientEvent::ControlMessageRefused`] for `raw`, when `err` refuses
1125    /// what the peer sent: the decoder refused it, or it broke a rule
1126    /// [`EndpointError::fault`] or [`Self::draft_specific_cause`] lays on the
1127    /// peer.
1128    fn refused_event(err: &ConnectionError, raw: Option<Vec<u8>>) -> Option<ClientEvent> {
1129        use crate::above_codec_rules::{DraftSpecificCause, EndpointFault};
1130        let peers = match err {
1131            ConnectionError::Codec(_) | ConnectionError::VarInt(_) => true,
1132            ConnectionError::Endpoint(e) => matches!(e.fault(), EndpointFault::Peer(_)),
1133            other => matches!(
1134                Self::draft_specific_cause(other),
1135                Some(DraftSpecificCause::PeerViolation { .. } | DraftSpecificCause::Codec { .. })
1136            ),
1137        };
1138        let raw = raw.filter(|_| peers)?;
1139        Some(ClientEvent::ControlMessageRefused { raw })
1140    }
1141
1142    /// Report a peer's SETUP the handshake refused to an observer attached
1143    /// before it: the message as received, then the refusal.
1144    fn refuse_setup(
1145        observer: Option<&dyn ConnectionObserver>,
1146        err: &ConnectionError,
1147        setup: &AnyControlMessage,
1148        raw: &Option<Vec<u8>>,
1149    ) {
1150        let Some(obs) = observer else { return };
1151        obs.on_event_owned(ClientEvent::ControlMessage {
1152            direction: Direction::Receive,
1153            message: setup.clone(),
1154            raw: raw.clone(),
1155        });
1156        if let Some(event) = Self::refused_event(err, raw.clone()) {
1157            obs.on_event_owned(event);
1158        }
1159    }
1160
1161    // -- Control message I/O ----------------------------------------
1162
1163    /// Send a control message on the control stream.
1164    ///
1165    /// Wraps the draft-15 message in `AnyControlMessage::Draft15` for
1166    /// framing.
1167    pub async fn send_control(&self, msg: &ControlMessage) -> Result<(), ConnectionError> {
1168        let any = AnyControlMessage::Draft15(msg.clone());
1169        let mut send =
1170            self.control_send.as_ref().ok_or(ConnectionError::NoControlStream)?.lock().await;
1171        let raw = send.write_control(&any).await?;
1172        drop(send);
1173        self.emit(ClientEvent::ControlMessage {
1174            direction: Direction::Send,
1175            message: any,
1176            raw: Some(raw),
1177        });
1178        Ok(())
1179    }
1180
1181    /// Read the next control message from the control stream.
1182    ///
1183    /// Returns the `AnyControlMessage` and also extracts the draft-15
1184    /// `ControlMessage` for internal endpoint dispatch.
1185    pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
1186        self.recv_control_raw().await.map(|(msg, _)| msg)
1187    }
1188
1189    /// [`Self::recv_control`], keeping the frame's wire bytes when an observer
1190    /// is attached.
1191    async fn recv_control_raw(
1192        &mut self,
1193    ) -> Result<(ControlMessage, Option<Vec<u8>>), ConnectionError> {
1194        let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
1195        let capture_raw = self.observer.is_some();
1196        let (any, raw) = match recv.read_control(capture_raw).await {
1197            Ok(v) => v,
1198            Err(e) => {
1199                let raw = recv.take_refused();
1200                let err = self.close_for_codec(e);
1201                self.emit_refused(&err, raw);
1202                return Err(err);
1203            }
1204        };
1205        if capture_raw {
1206            self.emit(ClientEvent::ControlMessage {
1207                direction: Direction::Receive,
1208                message: any.clone(),
1209                raw: raw.clone(),
1210            });
1211        }
1212        // Unwrap to draft-15 for the endpoint
1213        match any {
1214            AnyControlMessage::Draft15(msg) => Ok((msg, raw)),
1215            // `AnyControlMessage` carries one variant per enabled draft feature. With draft 15 the
1216            // only one enabled the arm above is exhaustive and this rejection arm unreachable.
1217            // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
1218            // naming the other drafts: such a list has to be edited in every draft module
1219            // whenever a draft is added, and a copy that omits one leaves this match
1220            // non-exhaustive.
1221            #[allow(unreachable_patterns)]
1222            _ => Err(ConnectionError::ControlMessageNarrowing),
1223        }
1224    }
1225
1226    /// Read and dispatch the next incoming control message through the
1227    /// endpoint state machine. Returns the decoded message for inspection.
1228    pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
1229        let (msg, raw) = self.recv_control_raw().await?;
1230        if let Err(e) = self.endpoint.receive_message(msg.clone()) {
1231            let err = self.close_if_session_fatal(e);
1232            self.emit_refused(&err, raw);
1233            return Err(err);
1234        }
1235
1236        // Emit draining event if this was a GoAway
1237        if let ControlMessage::GoAway(ref ga) = msg {
1238            self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
1239        }
1240
1241        Ok(msg)
1242    }
1243
1244    // -- Subscribe flow ---------------------------------------------
1245
1246    /// Send a SUBSCRIBE and return the allocated request ID.
1247    pub async fn subscribe(
1248        &mut self,
1249        track_namespace: TrackNamespace,
1250        track_name: Vec<u8>,
1251        parameters: Vec<KeyValuePair>,
1252    ) -> Result<VarInt, ConnectionError> {
1253        let (req_id, msg) = self.endpoint.subscribe(track_namespace, track_name, parameters)?;
1254        self.send_control(&msg).await?;
1255        Ok(req_id)
1256    }
1257
1258    /// Send an UNSUBSCRIBE for the given request ID.
1259    pub async fn unsubscribe(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1260        let msg = self.endpoint.unsubscribe(request_id)?;
1261        self.send_control(&msg).await
1262    }
1263
1264    /// Accept a subscription the peer opened, sending SUBSCRIBE_OK and giving
1265    /// its track a Track Alias.
1266    ///
1267    /// The endpoint refuses an alias a live track of its own already holds and
1268    /// refuses a second answer to one SUBSCRIBE, so nothing is written on the
1269    /// wire when it does either.
1270    pub async fn subscribe_ok(
1271        &mut self,
1272        request_id: VarInt,
1273        track_alias: VarInt,
1274        parameters: Vec<KeyValuePair>,
1275    ) -> Result<(), ConnectionError> {
1276        let msg = self.endpoint.send_subscribe_ok(request_id, track_alias, parameters)?;
1277        self.send_control(&msg).await
1278    }
1279
1280    /// Refuse a request the peer opened, sending REQUEST_ERROR.
1281    ///
1282    /// One message refuses a SUBSCRIBE, a FETCH, an announcement, a track
1283    /// status or a namespace subscription, and the endpoint finds which by the
1284    /// identifier. It refuses a second answer to any of them, and
1285    /// refuses a Joining Fetch's refusal under any code but the one the draft
1286    /// names for it, so nothing is written on the wire when it does.
1287    pub async fn request_error(
1288        &mut self,
1289        request_id: VarInt,
1290        error_code: VarInt,
1291        reason_phrase: Vec<u8>,
1292    ) -> Result<(), ConnectionError> {
1293        let msg = self.endpoint.send_request_error(request_id, error_code, reason_phrase)?;
1294        self.send_control(&msg).await
1295    }
1296
1297    /// Narrow a subscription this endpoint opened, sending SUBSCRIBE_UPDATE, and
1298    /// return the Request ID the update itself spent.
1299    pub async fn subscribe_update(
1300        &mut self,
1301        subscription_request_id: VarInt,
1302        parameters: Vec<KeyValuePair>,
1303    ) -> Result<VarInt, ConnectionError> {
1304        let (request_id, msg) =
1305            self.endpoint.subscribe_update(subscription_request_id, parameters)?;
1306        self.send_control(&msg).await?;
1307        Ok(request_id)
1308    }
1309
1310    /// Accept a PUBLISH the peer sent, which establishes the subscription it
1311    /// opened.
1312    ///
1313    /// The endpoint refuses a second answer to one PUBLISH, so nothing is
1314    /// written on the wire when it does.
1315    pub async fn publish_ok(
1316        &mut self,
1317        request_id: VarInt,
1318        parameters: Vec<KeyValuePair>,
1319    ) -> Result<(), ConnectionError> {
1320        let msg = self.endpoint.send_publish_ok(request_id, parameters)?;
1321        self.send_control(&msg).await
1322    }
1323
1324    /// Reject a PUBLISH the peer sent, which ends the subscription it opened
1325    /// before it was established.
1326    ///
1327    /// The endpoint refuses a second answer to one PUBLISH, so nothing is
1328    /// written on the wire when it does.
1329    pub async fn publish_error(
1330        &mut self,
1331        request_id: VarInt,
1332        error_code: VarInt,
1333        reason_phrase: Vec<u8>,
1334    ) -> Result<(), ConnectionError> {
1335        let msg = self.endpoint.send_publish_error(request_id, error_code, reason_phrase)?;
1336        self.send_control(&msg).await
1337    }
1338
1339    // -- Fetch flow -------------------------------------------------
1340
1341    /// Send a standalone FETCH and return the allocated request ID.
1342    #[allow(clippy::too_many_arguments)]
1343    pub async fn fetch(
1344        &mut self,
1345        track_namespace: TrackNamespace,
1346        track_name: Vec<u8>,
1347        start_group: VarInt,
1348        start_object: VarInt,
1349        end_group: VarInt,
1350        end_object: VarInt,
1351        parameters: Vec<KeyValuePair>,
1352    ) -> Result<VarInt, ConnectionError> {
1353        let (req_id, msg) = self.endpoint.fetch(
1354            track_namespace,
1355            track_name,
1356            start_group,
1357            start_object,
1358            end_group,
1359            end_object,
1360            parameters,
1361        )?;
1362        self.send_control(&msg).await?;
1363        Ok(req_id)
1364    }
1365
1366    /// Send a Relative Joining Fetch and return the allocated request ID.
1367    ///
1368    /// `joining_start` counts groups back from the subscription's largest
1369    /// group. To name the starting group outright, use
1370    /// [`absolute_joining_fetch`](Self::absolute_joining_fetch).
1371    pub async fn joining_fetch(
1372        &mut self,
1373        joining_request_id: VarInt,
1374        joining_start: VarInt,
1375        parameters: Vec<KeyValuePair>,
1376    ) -> Result<VarInt, ConnectionError> {
1377        let (req_id, msg) =
1378            self.endpoint.joining_fetch(joining_request_id, joining_start, parameters)?;
1379        self.send_control(&msg).await?;
1380        Ok(req_id)
1381    }
1382
1383    /// Send an Absolute Joining Fetch and return the allocated request ID.
1384    ///
1385    /// Here `joining_start` is the group to begin at rather than an offset,
1386    /// which is what an application that knows the group it wants has: draft-15
1387    /// Section 9.16.2.1 has the publisher set the Start Location to
1388    /// {Joining Start, 0}.
1389    pub async fn absolute_joining_fetch(
1390        &mut self,
1391        joining_request_id: VarInt,
1392        joining_start: VarInt,
1393        parameters: Vec<KeyValuePair>,
1394    ) -> Result<VarInt, ConnectionError> {
1395        let (req_id, msg) =
1396            self.endpoint.absolute_joining_fetch(joining_request_id, joining_start, parameters)?;
1397        self.send_control(&msg).await?;
1398        Ok(req_id)
1399    }
1400
1401    /// Send a FETCH_CANCEL for the given request ID.
1402    pub async fn fetch_cancel(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1403        let msg = self.endpoint.fetch_cancel(request_id)?;
1404        self.send_control(&msg).await
1405    }
1406
1407    /// Accept a fetch the peer opened, sending FETCH_OK.
1408    ///
1409    /// The endpoint refuses a Joining Fetch naming a subscription this session
1410    /// cannot join and refuses a second answer to one FETCH, so nothing is
1411    /// written on the wire when it does either.
1412    pub async fn fetch_ok(
1413        &mut self,
1414        request_id: VarInt,
1415        end_of_track: u8,
1416        end_group: VarInt,
1417        end_object: VarInt,
1418        parameters: Vec<KeyValuePair>,
1419    ) -> Result<(), ConnectionError> {
1420        let msg = self.endpoint.send_fetch_ok(
1421            request_id,
1422            end_of_track,
1423            end_group,
1424            end_object,
1425            parameters,
1426        )?;
1427        self.send_control(&msg).await
1428    }
1429
1430    // -- Namespace flows --------------------------------------------
1431
1432    /// Send a SUBSCRIBE_NAMESPACE and return the request ID.
1433    pub async fn subscribe_namespace(
1434        &mut self,
1435        namespace_prefix: TrackNamespace,
1436        parameters: Vec<KeyValuePair>,
1437    ) -> Result<VarInt, ConnectionError> {
1438        let (req_id, msg) = self.endpoint.subscribe_namespace(namespace_prefix, parameters)?;
1439        self.send_control(&msg).await?;
1440        Ok(req_id)
1441    }
1442
1443    /// Send a PUBLISH_NAMESPACE and return the request ID.
1444    pub async fn publish_namespace(
1445        &mut self,
1446        track_namespace: TrackNamespace,
1447        parameters: Vec<KeyValuePair>,
1448    ) -> Result<VarInt, ConnectionError> {
1449        let (req_id, msg) = self.endpoint.publish_namespace(track_namespace, parameters)?;
1450        self.send_control(&msg).await?;
1451        Ok(req_id)
1452    }
1453
1454    /// Accept a request the peer opened, sending REQUEST_OK.
1455    ///
1456    /// The endpoint refuses a second answer to one request, so nothing is
1457    /// written on the wire when it does. On this draft an announcement, a track
1458    /// status and a namespace subscription are the requests REQUEST_OK
1459    /// accepts; a subscription, a publication and a fetch each have an
1460    /// acceptance of their own that carries more than this one can.
1461    pub async fn request_ok(
1462        &mut self,
1463        request_id: VarInt,
1464        parameters: Vec<KeyValuePair>,
1465    ) -> Result<(), ConnectionError> {
1466        let msg = self.endpoint.send_request_ok(request_id, parameters)?;
1467        self.send_control(&msg).await
1468    }
1469
1470    /// Revoke an acceptance, sending PUBLISH_NAMESPACE_CANCEL.
1471    ///
1472    /// The endpoint refuses one for an announcement it never accepted, so
1473    /// nothing is written on the wire when it does.
1474    pub async fn publish_namespace_cancel(
1475        &mut self,
1476        track_namespace: TrackNamespace,
1477        error_code: VarInt,
1478        reason_phrase: Vec<u8>,
1479    ) -> Result<(), ConnectionError> {
1480        let msg =
1481            self.endpoint.publish_namespace_cancel(track_namespace, error_code, reason_phrase)?;
1482        self.send_control(&msg).await
1483    }
1484
1485    /// Withdraw an announcement this endpoint made, sending
1486    /// PUBLISH_NAMESPACE_DONE.
1487    ///
1488    /// The mirror of [`Self::publish_namespace`], and the counterpart of
1489    /// [`Self::publish_namespace_cancel`]: this one ends an announcement of
1490    /// this endpoint's, that one revokes the acceptance of one the peer made.
1491    pub async fn publish_namespace_done(
1492        &mut self,
1493        track_namespace: TrackNamespace,
1494    ) -> Result<(), ConnectionError> {
1495        let msg = self.endpoint.publish_namespace_done(track_namespace)?;
1496        self.send_control(&msg).await
1497    }
1498    // -- Track Status flow ------------------------------------------
1499
1500    /// Send a TRACK_STATUS and return the allocated request ID.
1501    pub async fn track_status(
1502        &mut self,
1503        track_namespace: TrackNamespace,
1504        track_name: Vec<u8>,
1505        parameters: Vec<KeyValuePair>,
1506    ) -> Result<VarInt, ConnectionError> {
1507        let (req_id, msg) = self.endpoint.track_status(track_namespace, track_name, parameters)?;
1508        self.send_control(&msg).await?;
1509        Ok(req_id)
1510    }
1511
1512    // -- Publish flow (publisher side) ------------------------------
1513
1514    /// Send a PUBLISH and return the allocated request ID.
1515    pub async fn publish(
1516        &mut self,
1517        track_namespace: TrackNamespace,
1518        track_name: Vec<u8>,
1519        track_alias: VarInt,
1520        parameters: Vec<KeyValuePair>,
1521    ) -> Result<VarInt, ConnectionError> {
1522        let (req_id, msg) =
1523            self.endpoint.publish(track_namespace, track_name, track_alias, parameters)?;
1524        self.send_control(&msg).await?;
1525        Ok(req_id)
1526    }
1527
1528    /// Send a PUBLISH_DONE for the given request ID.
1529    pub async fn publish_done(
1530        &mut self,
1531        request_id: VarInt,
1532        status_code: VarInt,
1533        stream_count: VarInt,
1534        reason_phrase: Vec<u8>,
1535    ) -> Result<(), ConnectionError> {
1536        let msg = self.endpoint.send_publish_done(
1537            request_id,
1538            status_code,
1539            stream_count,
1540            reason_phrase,
1541        )?;
1542        self.send_control(&msg).await
1543    }
1544
1545    // -- Malformed Tracks -------------------------------------------
1546
1547    /// Send what Section 2.4.2 asks for when this endpoint finds a track
1548    /// malformed.
1549    ///
1550    /// "it MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for
1551    /// that Track from that publisher" — one message per request, in Request
1552    /// ID order, and the endpoint decides which message each request takes.
1553    ///
1554    /// A write that fails is not reported. The caller is on its way to
1555    /// returning an error that says what went wrong with the track, and a
1556    /// control stream that will not take an UNSUBSCRIBE is a session on its
1557    /// way out for a reason of its own; replacing the condition's report with
1558    /// a transport error would lose the only account of why the track was
1559    /// withdrawn. The rest of the withdrawal is abandoned, because a stream
1560    /// that refused one message will refuse the next.
1561    async fn withdraw_malformed_track(&self, alias: u64, condition: MalformedTrackCondition) {
1562        for msg in self.endpoint.withdraw_malformed_track(alias, condition) {
1563            if self.send_control(&msg).await.is_err() {
1564                break;
1565            }
1566        }
1567    }
1568
1569    /// [`Self::withdraw_malformed_track`] for the track the FETCH `request_id`
1570    /// asked for.
1571    async fn withdraw_malformed_fetch_track(
1572        &self,
1573        request_id: u64,
1574        condition: MalformedTrackCondition,
1575    ) {
1576        for msg in self.endpoint.withdraw_malformed_fetch_track(request_id, condition) {
1577            if self.send_control(&msg).await.is_err() {
1578                break;
1579            }
1580        }
1581    }
1582
1583    /// Record the framing an arriving object was sent with, and withdraw from
1584    /// the track when it is the second framing that track has been sent.
1585    ///
1586    /// The receiving half of a pair. The two writing paths call the endpoint
1587    /// directly and answer a mixed track by refusing to write it, because
1588    /// Section 2.4.2's sentence is a subscriber's: an endpoint about to send an
1589    /// object is that object's Original Publisher, and a publisher has no
1590    /// subscription of its own to withdraw and no fetch of its own to cancel.
1591    async fn note_received_framing(
1592        &self,
1593        alias: u64,
1594        seen: ObjectForwardingPreference,
1595    ) -> Result<(), ConnectionError> {
1596        let Err(err) = self.endpoint.note_object_forwarding_preference(alias, seen) else {
1597            return Ok(());
1598        };
1599        self.withdraw_malformed_track(alias, MalformedTrackCondition::MixedForwardingPreference)
1600            .await;
1601        Err(err.into())
1602    }
1603
1604    // -- Data streams -----------------------------------------------
1605
1606    /// Open a new unidirectional stream for sending subgroup data.
1607    pub async fn open_subgroup_stream(
1608        &self,
1609        header: &AnySubgroupHeader,
1610    ) -> Result<FramedSendStream, ConnectionError> {
1611        // Before the stream is opened: the Original Publisher is who the rule
1612        // binds, so a header that would mix this track's framing is refused
1613        // here rather than written and answered by the peer.
1614        self.endpoint.note_object_forwarding_preference(
1615            header.track_alias(),
1616            ObjectForwardingPreference::Subgroup,
1617        )?;
1618        let send = self.transport.open_uni().await?;
1619        let mut framed = FramedSendStream::new(send, self.draft);
1620        let sid = framed.stream_id();
1621        framed.write_subgroup_header(header).await?;
1622        self.emit(ClientEvent::StreamOpened {
1623            direction: Direction::Send,
1624            stream_kind: StreamKind::Subgroup,
1625            stream_id: sid,
1626        });
1627        self.emit(ClientEvent::DataStreamHeader {
1628            stream_id: sid,
1629            direction: Direction::Send,
1630            header: header.clone(),
1631        });
1632        Ok(framed)
1633    }
1634
1635    /// Open a new unidirectional stream for sending a FETCH's objects.
1636    ///
1637    /// The objects answering a FETCH do not go on the request's own stream:
1638    /// they go on a unidirectional stream of their own, which opens with a
1639    /// FETCH_HEADER naming the request they belong to. This writes that header
1640    /// and hands back the stream, the same way
1641    /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
1642    /// subgroup.
1643    ///
1644    /// The caller owns the stream that comes back. Nothing here remembers
1645    /// which request it belongs to, so an endpoint serving several fetches at
1646    /// once keeps its own map from Request ID to stream.
1647    pub async fn open_fetch_stream(
1648        &self,
1649        header: &AnyFetchHeader,
1650    ) -> Result<FramedSendStream, ConnectionError> {
1651        let send = self.transport.open_uni().await?;
1652        let mut framed = FramedSendStream::new(send, self.draft);
1653        let sid = framed.stream_id();
1654        framed.write_fetch_header(header).await?;
1655        self.emit(ClientEvent::StreamOpened {
1656            direction: Direction::Send,
1657            stream_kind: StreamKind::Fetch,
1658            stream_id: sid,
1659        });
1660        Ok(framed)
1661    }
1662
1663    /// Accept the next unidirectional stream and read its fetch header.
1664    ///
1665    /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
1666    /// are separate because the header decides how every object after it is
1667    /// framed, so a caller has to know which it is expecting before the first
1668    /// byte is read.
1669    ///
1670    /// Objects come off the returned stream with
1671    /// [`FramedRecvStream::read_fetch_object`].
1672    pub async fn accept_fetch_stream(
1673        &self,
1674    ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
1675        let recv = self.transport.accept_uni().await?;
1676        let mut framed = FramedRecvStream::new(recv, self.draft);
1677        let sid = framed.stream_id();
1678        let header = framed.read_fetch_header().await?;
1679        self.emit(ClientEvent::StreamOpened {
1680            direction: Direction::Receive,
1681            stream_kind: StreamKind::Fetch,
1682            stream_id: sid,
1683        });
1684        self.emit(ClientEvent::FetchStreamHeader {
1685            stream_id: sid,
1686            direction: Direction::Receive,
1687            header: header.clone(),
1688        });
1689        // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
1690        // carries an `AnySubgroupHeader` and cannot express one. What
1691        // `accept_subgroup_stream` does beyond this - the forwarding-preference
1692        // note, the object measurement - is about a subgroup and has no
1693        // counterpart on a fetch stream.
1694        Ok((header, framed))
1695    }
1696
1697    /// Accept an incoming unidirectional data stream and read its subgroup
1698    /// header.
1699    pub async fn accept_subgroup_stream(
1700        &self,
1701    ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
1702        let recv = self.transport.accept_uni().await?;
1703        let mut framed = FramedRecvStream::new(recv, self.draft);
1704        let sid = framed.stream_id();
1705        let header = framed.read_subgroup_header().await?;
1706        self.emit(ClientEvent::StreamOpened {
1707            direction: Direction::Receive,
1708            stream_kind: StreamKind::Subgroup,
1709            stream_id: sid,
1710        });
1711        self.emit(ClientEvent::DataStreamHeader {
1712            stream_id: sid,
1713            direction: Direction::Receive,
1714            header: header.clone(),
1715        });
1716        // Every object on a subgroup stream has the Subgroup preference, so
1717        // the header settles the track's framing before a single object is
1718        // read.
1719        self.note_received_framing(header.track_alias(), ObjectForwardingPreference::Subgroup)
1720            .await?;
1721        // The track is resolved here and not inside the stream: it takes the
1722        // endpoint's alias table, which a stream handle has no way back to.
1723        if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
1724            framed.measure_objects_against(objects, header.group_id());
1725        }
1726        Ok((header, framed))
1727    }
1728
1729    /// Send an object via datagram.
1730    ///
1731    /// The header goes through `AnyDatagramHeader::encode`, which refuses a
1732    /// header whose Object Status the framing it names cannot carry. Such a
1733    /// header errors here and nothing is sent, rather than going out as an
1734    /// ordinary payload datagram with the status quietly dropped.
1735    pub fn send_datagram(
1736        &self,
1737        header: &AnyDatagramHeader,
1738        payload: &[u8],
1739    ) -> Result<(), ConnectionError> {
1740        // Before anything is encoded, for the reason `open_subgroup_stream`
1741        // gives.
1742        self.endpoint.note_object_forwarding_preference(
1743            header.meta().track_alias,
1744            ObjectForwardingPreference::Datagram,
1745        )?;
1746        let mut buf = Vec::new();
1747        header.encode(&mut buf)?;
1748        buf.extend_from_slice(payload);
1749        self.emit(ClientEvent::DatagramReceived {
1750            direction: Direction::Send,
1751            header: header.clone(),
1752            payload_len: payload.len(),
1753        });
1754        self.transport.send_datagram(bytes::Bytes::from(buf))?;
1755        Ok(())
1756    }
1757
1758    /// Receive a datagram and decode it whole.
1759    ///
1760    /// An extension block announced with a length of 0 is returned as
1761    /// [`ConnectionError::EmptyDatagramExtensions`], which
1762    /// [`Connection::close_for_data_stream`] closes the session over. Extension
1763    /// headers on a status other than Normal are returned as
1764    /// [`ConnectionError::ExtensionsOnNonNormalStatus`], which it also closes over.
1765    ///
1766    /// The codec's `DatagramHeader::decode_object` then reads the whole datagram,
1767    /// and its refusals are returned as [`ConnectionError::Codec`]: an extension
1768    /// block that does not parse, a payload after a status, and a Prior Group ID
1769    /// Gap or Prior Object ID Gap fault. A gap fault,
1770    /// [`CodecError::MalformedTrack`], and an extension block that does not
1771    /// parse, [`CodecError::MalformedObjectProperties`] (draft-15 Section 11.1),
1772    /// make the track malformed, and this withdraws from it before returning,
1773    /// as draft-15 Section 2.4.2 requires: "When a subscriber detects a
1774    /// Malformed Track, it MUST UNSUBSCRIBE any subscription and FETCH_CANCEL
1775    /// any fetch for that Track from that publisher, and SHOULD deliver an
1776    /// error to the application." The session stays open.
1777    pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
1778        let data = self.transport.recv_datagram().await?;
1779        let mut cursor = &data[..];
1780        let header = match AnyDatagramHeader::decode(self.draft, &mut cursor) {
1781            Ok(header) => header,
1782            Err(CodecError::InvalidField) => {
1783                return Err(empty_datagram_extensions(&data)
1784                    .unwrap_or(ConnectionError::Codec(CodecError::InvalidField)));
1785            }
1786            Err(err) => return Err(err.into()),
1787        };
1788        let consumed = data.len() - cursor.len();
1789        let payload = data.slice(consumed..);
1790        self.emit(ClientEvent::DatagramReceived {
1791            direction: Direction::Receive,
1792            header: header.clone(),
1793            payload_len: payload.len(),
1794        });
1795        // Refutable only in a build with more than one draft enabled.
1796        #[allow(irrefutable_let_patterns)]
1797        if let AnyDatagramHeader::Draft15(h) = &header {
1798            // draft-15 Section 10.2.1.2: "If an endpoint receives extension
1799            // headers on Objects with status that is not Normal, it MUST close
1800            // the session with a PROTOCOL_VIOLATION." The header decoder
1801            // reports it through `extensions_permitted` and does not refuse it.
1802            if !h.extensions_permitted() {
1803                return Err(ConnectionError::ExtensionsOnNonNormalStatus {
1804                    object_id: h.object_id.into_inner(),
1805                    extensions_len: h.extension_headers.len(),
1806                    // Not permitted only beside a status that is present.
1807                    status: h
1808                        .object_status
1809                        .unwrap_or(moqtap_codec::draft15::types::ObjectStatus::Normal),
1810                });
1811            }
1812            let whole =
1813                moqtap_codec::draft15::data_stream::DatagramHeader::decode_object(&mut &data[..]);
1814            if let Err(err) = whole {
1815                if let Some(condition) =
1816                    malformed_track_condition(&ConnectionError::Codec(err.clone()))
1817                {
1818                    self.withdraw_malformed_track(header.meta().track_alias, condition).await;
1819                }
1820                return Err(err.into());
1821            }
1822        }
1823        // A datagram is the other framing, and it settles the track's just as a
1824        // subgroup header does.
1825        self.note_received_framing(header.meta().track_alias, ObjectForwardingPreference::Datagram)
1826            .await?;
1827        // A datagram is a whole object, so the connection can measure it
1828        // without help from the caller - and answer the condition itself,
1829        // because an UNSUBSCRIBE takes the connection an object on a stream
1830        // cannot reach.
1831        let meta = header.meta();
1832        if let Err(err) = self.endpoint.note_received_object(
1833            meta.track_alias,
1834            ObjectLocation { group: meta.group_id, object: meta.object_id },
1835            object_role(meta.status),
1836        ) {
1837            self.withdraw_malformed_track(
1838                meta.track_alias,
1839                MalformedTrackCondition::ObjectPastFinalObject,
1840            )
1841            .await;
1842            return Err(err.into());
1843        }
1844        Ok((header, payload))
1845    }
1846
1847    /// Close the session on the wire when the endpoint says a violation is
1848    /// fatal to it, and hand the error back unchanged.
1849    ///
1850    /// [`EndpointError::session_error_code`] answers `Some` for exactly the
1851    /// errors this draft ends the session over, and the endpoint has already
1852    /// moved its own state machine to Closed by the time this runs. Without
1853    /// this step that move is purely internal: the local endpoint refuses to
1854    /// start anything new while the peer, which is the one that broke the
1855    /// rule, sees a session that is still open and goes on sending. A rule
1856    /// that names a session termination code is a statement about the wire,
1857    /// so it takes a CONNECTION_CLOSE to satisfy it.
1858    ///
1859    /// The reason phrase is the error's own `Display` text, which names the
1860    /// rule rather than repeating the numeric code the close already carries.
1861    ///
1862    /// Errors that answer `None` are recoverable and nothing is sent.
1863    fn close_for(&self, err: &EndpointError) {
1864        if let Some(code) = err.session_error_code() {
1865            // QUIC application error codes are 62-bit; every code in this
1866            // registry is far below `u32::MAX`, and saturating rather than
1867            // truncating means a future code that is not could never be
1868            // reported as a different, assigned one.
1869            let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1870            self.close(wire_code, err.to_string().as_bytes());
1871        }
1872    }
1873
1874    /// [`close_for`](Self::close_for), then the error unchanged, for the
1875    /// common case where the endpoint's error is also what the caller returns.
1876    fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
1877        self.close_for(&err);
1878        ConnectionError::Endpoint(err)
1879    }
1880
1881    /// Withdraw from a track a data stream found malformed, reporting whether
1882    /// it did.
1883    ///
1884    /// The Malformed Track twin of [`Connection::close_for_data_stream`], and
1885    /// separate from it for the same reason and one more. The same one: a
1886    /// [`FramedRecvStream`] holds no connection, so the reader that finds the
1887    /// fault is not the object that can send an UNSUBSCRIBE. The one more: the
1888    /// two answers are opposites - that call ends the session, this one gives
1889    /// up a track and leaves it running - and a single entry point would have
1890    /// to decide between them from the error alone, which is exactly the
1891    /// decision a caller reproducing a capture wants to make itself.
1892    ///
1893    /// The datagram path needs none of this. It is read through the connection,
1894    /// so [`Connection::recv_datagram`] answers the condition where it finds
1895    /// it, and this is only for the objects that arrive on a stream the caller
1896    /// holds.
1897    pub async fn withdraw_for_data_stream(&self, err: &ConnectionError) -> bool {
1898        let ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject { alias, .. }) = err
1899        else {
1900            return false;
1901        };
1902        self.withdraw_malformed_track(*alias, MalformedTrackCondition::ObjectPastFinalObject).await;
1903        true
1904    }
1905
1906    /// [`Connection::withdraw_for_data_stream`] for an error read from the
1907    /// subgroup stream `header` opened, which also withdraws from the track
1908    /// over the [`CodecError::MalformedTrack`] that
1909    /// [`FramedRecvStream::read_subgroup_object`] returns for a Prior Group ID
1910    /// Gap or Prior Object ID Gap fault, and over the
1911    /// [`CodecError::MalformedObjectProperties`] it returns for an extension
1912    /// block that does not parse (draft-15 Section 11.1). Those errors name no
1913    /// track, so the header supplies its alias.
1914    ///
1915    /// draft-15 Section 2.4.2: "When a subscriber detects a Malformed Track, it
1916    /// MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for that
1917    /// Track from that publisher, and SHOULD deliver an error to the
1918    /// application."
1919    pub async fn withdraw_for_subgroup_stream(
1920        &self,
1921        header: &AnySubgroupHeader,
1922        err: &ConnectionError,
1923    ) -> bool {
1924        let Some(condition) = malformed_track_condition(err) else {
1925            return self.withdraw_for_data_stream(err).await;
1926        };
1927        self.withdraw_malformed_track(header.track_alias(), condition).await;
1928        true
1929    }
1930
1931    /// [`Connection::withdraw_for_data_stream`] for an error read from the fetch
1932    /// stream `header` opened, which also withdraws from the track the FETCH
1933    /// asked for over the [`CodecError::MalformedTrack`] that
1934    /// [`FramedRecvStream::read_fetch_object`] returns for a Prior Group ID Gap
1935    /// or Prior Object ID Gap fault, and over the
1936    /// [`CodecError::MalformedObjectProperties`] it returns for an extension
1937    /// block that does not parse (draft-15 Section 11.1). Those errors name no
1938    /// track, so the header's Request ID supplies it.
1939    ///
1940    /// draft-15 Section 2.4.2: "When a subscriber detects a Malformed Track, it
1941    /// MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for that
1942    /// Track from that publisher, and SHOULD deliver an error to the
1943    /// application."
1944    pub async fn withdraw_for_fetch_stream(
1945        &self,
1946        header: &AnyFetchHeader,
1947        err: &ConnectionError,
1948    ) -> bool {
1949        let Some(condition) = malformed_track_condition(err) else {
1950            return self.withdraw_for_data_stream(err).await;
1951        };
1952        self.withdraw_malformed_fetch_track(header.request_id(), condition).await;
1953        true
1954    }
1955
1956    /// Close the session when a failure raised while reading a *data* stream is
1957    /// one draft-15 answers with a close. Reports whether it closed.
1958    ///
1959    /// [`accept_subgroup_stream`](Self::accept_subgroup_stream) hands the caller
1960    /// a [`FramedRecvStream`], which holds no connection and so cannot close
1961    /// one, and the read that raises this failure happens there. The caller is
1962    /// the only party holding both halves, which is what this is for.
1963    ///
1964    /// Splitting it this way rather than closing inside the reader keeps a
1965    /// caller that is deliberately permissive — a tool reproducing a capture,
1966    /// say — able to read a violating stream and report it without tearing the
1967    /// session down. The rule is stated at endpoints, and this is where an
1968    /// endpoint decides it is one.
1969    ///
1970    /// Answers the extension-header rule of Section 10.2.1.2, and any decode
1971    /// failure `codec_session_error_code` recognises, so a rule is answered
1972    /// with one code whichever stream carried it.
1973    ///
1974    /// Not every rule that reaches here is the decoder's. A track whose objects
1975    /// mix forwarding preferences is the endpoint's to notice — it takes the
1976    /// alias table to know which track an object belongs to — and it arrives on
1977    /// exactly these streams. Both kinds are asked for a code the same way, and
1978    /// a rule with no code is declined rather than guessed at.
1979    pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
1980        use crate::above_codec_rules::DraftSpecificCause;
1981
1982        match err {
1983            ConnectionError::Codec(inner) => {
1984                let Some(code) = Self::codec_session_error_code(inner) else { return false };
1985                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1986                self.close(wire_code, inner.to_string().as_bytes());
1987                true
1988            }
1989            // Not a `Codec` failure: the codec refuses such an Object as a bare
1990            // `InvalidField`, which `read_subgroup_object` narrows to this
1991            // crate's own variant, so the mapping table above never sees it.
1992            //
1993            // The code comes from `draft_specific_cause` rather than from a
1994            // constant here, so this draft's reading of its own sentence is
1995            // written down once and a caller who reads the error as a value
1996            // sees the same code the peer was sent.
1997            ConnectionError::ExtensionsOnNonNormalStatus { .. } => {
1998                let Some(DraftSpecificCause::PeerViolation { close: Some(code), .. }) =
1999                    Self::draft_specific_cause(err)
2000                else {
2001                    return false;
2002                };
2003                // Saturate rather than truncate, so a future code above
2004                // `u32::MAX` is never reported as a different assigned one.
2005                self.close(u32::try_from(code).unwrap_or(u32::MAX), err.to_string().as_bytes());
2006                true
2007            }
2008            // A rule the endpoint raises rather than the decoder. The two
2009            // reach their codes through different tables and mean the same
2010            // thing here: `Some` is a rule this draft ends the session over.
2011            ConnectionError::Endpoint(inner) => {
2012                let Some(code) = inner.session_error_code() else { return false };
2013                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2014                self.close(wire_code, inner.to_string().as_bytes());
2015                true
2016            }
2017            // Not a `Codec` failure: the codec refuses the datagram as a bare
2018            // `InvalidField`, which `recv_datagram` names, so the code comes
2019            // from `draft_specific_cause`.
2020            ConnectionError::EmptyDatagramExtensions { .. } => {
2021                let Some(crate::above_codec_rules::DraftSpecificCause::Codec {
2022                    close: Some(code),
2023                    ..
2024                }) = Self::draft_specific_cause(err)
2025                else {
2026                    return false;
2027                };
2028                self.close(u32::try_from(code).unwrap_or(u32::MAX), err.to_string().as_bytes());
2029                true
2030            }
2031            _ => false,
2032        }
2033    }
2034
2035    // -- Accessors --------------------------------------------------
2036
2037    /// Access the underlying endpoint state machine.
2038    pub fn endpoint(&self) -> &Endpoint {
2039        &self.endpoint
2040    }
2041
2042    /// Mutable access to the endpoint state machine.
2043    pub fn endpoint_mut(&mut self) -> &mut Endpoint {
2044        &mut self.endpoint
2045    }
2046
2047    /// The SETUP message the server answered the handshake with.
2048    ///
2049    /// `SERVER_SETUP` through draft-16, the server's half of the unified
2050    /// `SETUP` from draft-17. [`AnyControlMessage::fields`] renders it under
2051    /// this draft's own parameter names, in the order they arrived.
2052    pub fn server_setup(&self) -> &AnyControlMessage {
2053        &self.server_setup
2054    }
2055
2056    /// The framed wire bytes of [`Self::server_setup`], as they arrived.
2057    ///
2058    /// Kept beside the decoded form because the encoding is evidence the
2059    /// decoding discards: two relays sending the same parameter can still
2060    /// disagree on how wide a varint they wrote it in.
2061    pub fn server_setup_raw(&self) -> Option<&[u8]> {
2062        self.server_setup_raw.as_deref()
2063    }
2064
2065    /// Returns the draft version this connection is using.
2066    pub fn draft(&self) -> DraftVersion {
2067        self.draft
2068    }
2069
2070    /// Which of this draft's *own* `ConnectionError` variants this error is,
2071    /// and which kind of thing it says.
2072    ///
2073    /// The ten every draft carries answer `None` here: [`AnyConnectionError`]
2074    /// classifies those itself, once, and never asks a draft about them. What
2075    /// is left splits two ways, and the split is the reason this function
2076    /// exists — before it, both halves reached a caller as a sentence and read
2077    /// exactly alike. A [`LocalRefusal`] is this endpoint declining to write
2078    /// something, so nothing reached the wire and no relay is implicated; a
2079    /// [`PeerViolation`] is a peer having done something draft-15 forbids, and
2080    /// carries the session error code draft-15's own text answers it with.
2081    ///
2082    /// Matched exhaustively, with no wildcard arm and deliberately so: a
2083    /// variant added to this draft's error type has to arrive here as a compile
2084    /// error, beside the doc comment quoting the sentence it enforces, rather
2085    /// than as a silent [`ErrorCause::Unclassified`] in the facade.
2086    ///
2087    /// [`AnyConnectionError`]: crate::dispatch::AnyConnectionError
2088    /// [`ErrorCause::Unclassified`]: crate::dispatch::ErrorCause::Unclassified
2089    /// [`LocalRefusal`]: crate::above_codec_rules::DraftSpecificCause::LocalRefusal
2090    /// [`PeerViolation`]: crate::above_codec_rules::DraftSpecificCause::PeerViolation
2091    pub fn draft_specific_cause(
2092        err: &ConnectionError,
2093    ) -> Option<crate::above_codec_rules::DraftSpecificCause> {
2094        use crate::above_codec_rules::{AboveCodecRule, DraftSpecificCause};
2095        use moqtap_codec::draft15::error_codes::SessionErrorCode;
2096
2097        match err {
2098            ConnectionError::Endpoint(_)
2099            | ConnectionError::Codec(_)
2100            | ConnectionError::Transport(_)
2101            | ConnectionError::VarInt(_)
2102            | ConnectionError::NoControlStream
2103            | ConnectionError::UnexpectedEnd
2104            | ConnectionError::StreamFinished
2105            | ConnectionError::InvalidAddress(_)
2106            | ConnectionError::TlsConfig(_)
2107            | ConnectionError::DataStreamState(_) => None,
2108            // This build decoding a message and then failing to narrow it to
2109            // its own draft. Nothing reached the wire and no peer is
2110            // implicated, which is the whole reason it is not
2111            // `ConnectionError::Codec`: under that name it would carry
2112            // `Some(PROTOCOL_VIOLATION)` out of `codec_session_error_code` and
2113            // publish a relay for this build's defect. See the variant's own
2114            // doc.
2115            ConnectionError::ControlMessageNarrowing => {
2116                Some(crate::above_codec_rules::DraftSpecificCause::LocalRefusal)
2117            }
2118            // Section 10.2.1.2 states the rule and names the code in the same
2119            // sentence. The codec refuses such an Object only as a bare
2120            // `InvalidField`, so this layer is the one that names the rule,
2121            // and `close_for_data_stream` performs the close by
2122            // reading this same answer.
2123            ConnectionError::ExtensionsOnNonNormalStatus { .. } => {
2124                Some(DraftSpecificCause::PeerViolation {
2125                    rule: AboveCodecRule::PropertiesOnNonNormalStatus,
2126                    close: Some(SessionErrorCode::ProtocolViolation.as_u64()),
2127                })
2128            }
2129            // The codec refuses the datagram as a bare `InvalidField`, and
2130            // `recv_datagram` names the rule, so `close_for_data_stream` reads
2131            // the close here. See the variant's own doc.
2132            ConnectionError::EmptyDatagramExtensions { .. } => {
2133                Some(crate::above_codec_rules::DraftSpecificCause::Codec {
2134                    error: CodecError::InvalidField,
2135                    close: Some(
2136                        moqtap_codec::draft15::error_codes::SessionErrorCode::ProtocolViolation
2137                            .as_u64(),
2138                    ),
2139                })
2140            }
2141        }
2142    }
2143
2144    /// The code to close the session with when a control message could not be
2145    /// decoded because the peer broke a rule draft-15 answers with a close.
2146    ///
2147    /// Every variant listed here comes from a sentence in this draft that names
2148    /// the consequence, and the list is deliberately shorter than draft-17's:
2149    /// the bounds are per draft, and answering one this draft does not state
2150    /// would close a session over traffic a conforming peer may send.
2151    ///
2152    ///   - Reason Phrase, maximum 1024 bytes: "If an endpoint receives a length
2153    ///     exceeding the maximum, it MUST close the session with a
2154    ///     PROTOCOL_VIOLATION."
2155    ///   - KVP value, maximum 2^16-1 bytes, with the same sentence.
2156    ///   - Track Namespace field count: "If an endpoint receives a Track
2157    ///     Namespace consisting of 0 or greater than 32 Track Namespace Fields,
2158    ///     it MUST close the session with a PROTOCOL_VIOLATION." Note the lower
2159    ///     bound — an empty tuple is refused here, where drafts 17 and later
2160    ///     permit it.
2161    ///   - Full Track Name, maximum 4,096 bytes. Draft-15 states this of the Full Track
2162    ///     Name alone; draft-16 widened it to a Track Namespace on its own.
2163    ///   - Duplicate parameters, a SHOULD rather than a MUST: "Receivers SHOULD
2164    ///     check that there are no unauthorized duplicate parameters and close the
2165    ///     session as a PROTOCOL_VIOLATION"
2166    ///
2167    ///   - GOAWAY New Session URI, maximum 8,192 bytes: "If an endpoint
2168    ///     receives a length exceeding the maximum, it MUST close the session
2169    ///     with a PROTOCOL_VIOLATION." Every draft from 11 to 19 states it; 07
2170    ///     through 10 state no maximum for the field at all.
2171    ///   - Unknown control message type: "An endpoint that receives an unknown
2172    ///     message type MUST close the session." All the drafts state it,
2173    ///     in the same words, and the sentence names no code, so Protocol
2174    ///     Violation is what carries it.
2175    ///
2176    /// **Not** the unknown Message Parameter rule. Drafts 16 through 19 require
2177    /// a close for a Message Parameter whose type the negotiated version does
2178    /// not define. This draft states the opposite and states it about the same
2179    /// parameters: "Receivers MUST allow duplicates of unknown parameters",
2180    /// which presumes an unknown parameter arrives and is carried. Refusing one
2181    /// here would close a session over an extension this draft leaves room for.
2182    ///
2183    /// `None` for everything else, including [`CodecError::InvalidField`]. That
2184    /// variant is shared by a dozen unrelated malformations, only some of which
2185    /// the draft answers with a close, so treating it as fatal would close
2186    /// sessions the draft does not ask to be closed. Splitting it is the way to
2187    /// bring the rest of those rules under this function; widening the match is
2188    /// not.
2189    pub fn codec_session_error_code(
2190        err: &CodecError,
2191    ) -> Option<moqtap_codec::draft15::error_codes::SessionErrorCode> {
2192        use moqtap_codec::draft15::error_codes::SessionErrorCode;
2193        use moqtap_codec::kvp::KvpError;
2194        match err {
2195            // The declared Length disagreeing with the fields, which every
2196            // draft answers with a close. Drafts 07 through 10 name no code for
2197            // it, so it takes the one their other unnamed rules take.
2198            // A Filter Type outside the four this draft assigns, Section 5.1.2:
2199            // "An endpoint that receives a filter type other than the above MUST
2200            // close the session with PROTOCOL_VIOLATION."
2201            //
2202            // Drafts 07 through 14 carried the Filter Type as a field of
2203            // SUBSCRIBE. From draft-15 it is the first field inside the
2204            // length-prefixed filter parameter, where a codec that carries the
2205            // value as opaque bytes never reads it — the rule did not change and
2206            // the place it has to be enforced did.
2207            CodecError::InvalidFilterType(_) => Some(SessionErrorCode::ProtocolViolation),
2208            // A filter parameter whose value is not a filter, Section 9.2.1.7:
2209            // "It is a length-prefixed Subscription Filter... If the length of
2210            // the Subscription Filter does not match the parameter length, the
2211            // publisher MUST close the session with PROTOCOL_VIOLATION."
2212            //
2213            // The one key-value malformation this draft answers with something
2214            // other than KEY_VALUE_FORMATTING_ERROR. The general rule covers the
2215            // same bytes and names that code; the sentence above is the specific
2216            // one, so it governs. Drafts 17 and later drop it and leave only the
2217            // general rule, which is why the same malformation ends a session
2218            // there under a different code.
2219            CodecError::SubscriptionFilterMalformed { .. } => {
2220                Some(SessionErrorCode::ProtocolViolation)
2221            }
2222            // A Fetch Type outside the three this draft assigns: "An endpoint
2223            // that receives a Fetch Type other than 0x1, 0x2 or 0x3 MUST close
2224            // the session with a PROTOCOL_VIOLATION." The value decides which
2225            // fields follow it — a Standalone fetch carries a track name and a
2226            // range where a joining fetch carries a Request ID and an offset —
2227            // so a reader that cannot name the type cannot find the end of the
2228            // message.
2229            CodecError::InvalidFetchType(_) => Some(SessionErrorCode::ProtocolViolation),
2230            CodecError::ControlMessageLengthMismatch { .. } => {
2231                Some(SessionErrorCode::ProtocolViolation)
2232            }
2233            CodecError::DuplicateParameter(_)
2234            | CodecError::TrackNameTooLong
2235            | CodecError::InvalidNamespaceTupleSize(_)
2236            | CodecError::ReasonPhraseTooLong
2237            | CodecError::GoAwayUriTooLong
2238            | CodecError::UnknownMessageType(_)
2239            | CodecError::Kvp(KvpError::ValueTooLong(_)) => {
2240                Some(SessionErrorCode::ProtocolViolation)
2241            }
2242            // An unknown data-plane type, Section 10: "An endpoint that
2243            // receives an unknown stream or datagram type MUST close the
2244            // session." One sentence covering two tables, which is why both
2245            // variants sit here.
2246            // A Message Parameter whose value is outside the range its type
2247            // allows: FORWARD in Section 9.2.1.10, GROUP_ORDER in Section 9.2.1.6,
2248            // SUBSCRIBER_PRIORITY in Section 9.2.1.5 and DYNAMIC_GROUPS in
2249            // Section 9.2.1.11.
2250            // Each states that a receiver "MUST close the session with
2251            // PROTOCOL_VIOLATION".
2252            CodecError::ParameterValueOutOfRange { .. } => {
2253                Some(SessionErrorCode::ProtocolViolation)
2254            }
2255            CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
2256                Some(SessionErrorCode::ProtocolViolation)
2257            }
2258            // A key-value pair whose value is not the serialization its own
2259            // Type defines, Section 1.4.2: "If a receiver understands a Type,
2260            // and the following Value or Length/Value does not match the
2261            // serialization defined by that Type, the receiver MUST terminate the
2262            // session with error code KEY_VALUE_FORMATTING_ERROR."
2263            //
2264            // Section 9.2.1.1 states the same answer for the one structure this
2265            // draft spells out: "If the Token structure cannot be decoded, the
2266            // receiver MUST close the Session with KEY_VALUE_FORMATTING_ERROR."
2267            //
2268            // The one rule in this table that names a code other than Protocol
2269            // Violation.
2270            CodecError::KeyValueFormatting { .. } => {
2271                Some(SessionErrorCode::KeyValueFormattingError)
2272            }
2273            // Everything this draft does not answer, named rather than swept up
2274            // by a wildcard. The arm is exhaustive deliberately: a new
2275            // `CodecError` variant will not compile until it has been placed on
2276            // one side or the other, on this draft, which is the decision a `_`
2277            // arm makes silently and invisibly in every draft module at once.
2278            //
2279            // Adding one variant to `CodecError` produces an `E0004` in every
2280            // draft module that matches it exhaustively, each naming the
2281            // variant that has nowhere to go. That is the whole mechanism.
2282            //
2283            // The nesting stops at `VarInt`, whose variants report how the bytes
2284            // ran out rather than a rule an endpoint states, so there is nothing
2285            // in it for a draft to answer. `Kvp` is spelled out because it does
2286            // carry one.
2287            // Neither field exists from draft-15 on. Forwarding became the
2288            // FORWARD parameter, which carries the same rule in a different
2289            // shape and is answered above under its own variant; Content Exists
2290            // became the presence or absence of a LARGEST_OBJECT parameter.
2291            CodecError::InvalidForward(_)
2292            | CodecError::InvalidContentExists(_)
2293            | CodecError::UnexpectedEnd
2294            | CodecError::MessageTooLong(_)
2295            | CodecError::VarInt(_)
2296            | CodecError::InvalidField
2297            | CodecError::EmptyNamespaceField
2298            | CodecError::InvalidRange(..)
2299            | CodecError::ParameterLengthMismatch(_)
2300            | CodecError::EndOfTrackObjectId(_)
2301            | CodecError::KeyDeltaOverflow(..)
2302            // Not `TrackPropertyValueOutOfRange`: draft-15 has no extension
2303            // header or Track Property registry. DYNAMIC_GROUPS, which draft-16
2304            // moves into one, is a Message Parameter here - Section 9.2.1.11,
2305            // "Values larger than 1 are a Protocol Violation" - and arrives as
2306            // `ParameterValueOutOfRange` above.
2307            | CodecError::TrackPropertyValueOutOfRange { .. }
2308            | CodecError::ParametersOutOfOrder(..)
2309            | CodecError::ObjectIdOverflow(..)
2310            | CodecError::ExtensionsOnNonExistentObject(_)
2311            | CodecError::InvalidRequiredRequestIdDelta(..)
2312            | CodecError::InvalidStreamTypeValue { .. }
2313            | CodecError::InvalidDatagramTypeValue { .. }
2314            | CodecError::UnknownMessageParameter(_)
2315            // Not `ParameterOutOfScope`: this draft states the scope rule and
2316            // answers it the other way. Section 9.2.1 Version Specific Parameters: "Each
2317            // version-specific parameter definition indicates the message types in which it can
2318            // appear. If it appears in some other type of message, it MUST be
2319            // ignored." The codec carries such a parameter on this draft and never
2320            // raises the variant, so this arm records a rule this draft has and
2321            // does not close over, not one it is missing. Draft-17 is where the
2322            // second sentence becomes a close.
2323            | CodecError::ParameterOutOfScope { .. }
2324            // The End Group is written out in full on this draft, so there is
2325            // nothing to add and nothing to overflow. Drafts 17 and later
2326            // replaced it with a delta measured from the Start Location's Group,
2327            // and 18 and 19 close the session when the sum leaves the range.
2328            | CodecError::FilterEndGroupOverflow { .. }
2329            // The object payload rule, Section 10.2.1.1: "Any object with a status
2330            // code other than zero MUST have an empty payload." A MUST on the
2331            // sender with no receiver action named anywhere — the "SHOULD be
2332            // treated as a protocol error" in the same paragraph belongs to the
2333            // sentence before it, which is about a status value this draft does
2334            // not assign — so an object carrying a payload it may not is refused
2335            // and the session stays open.
2336            | CodecError::PayloadNotPermitted { .. }
2337            // A property block that does not parse makes the Track malformed,
2338            // Section 11.1.
2339            | CodecError::MalformedObjectProperties { .. }
2340            // A Malformed Track (Section 2.4.2) costs the track and not the
2341            // session.
2342            | CodecError::MalformedTrack { .. }
2343            // Raised only by the draft-23 codec.
2344            | CodecError::ControlMessageExceedsLimit { .. }
2345            | CodecError::MissingErrorDetail { .. }
2346            | CodecError::MalformedErrorDetail { .. }
2347            | CodecError::TokenAliasInSetup { .. }
2348            | CodecError::EmptyTrackName
2349            | CodecError::MalformedAuthToken { .. }
2350            | CodecError::TrackPropertiesNotPermitted
2351            | CodecError::DeltaDecodingOverflow { .. }
2352            | CodecError::MalformedObject { .. }
2353            | CodecError::UnknownObjectStatus(_)
2354            | CodecError::UnsupportedDraft(_)
2355            | CodecError::Kvp(
2356                KvpError::MissingLength | KvpError::UnexpectedEnd | KvpError::VarInt(_),
2357            ) => None,
2358        }
2359    }
2360
2361    /// Close the session on the wire when a decode failure is one draft-15
2362    /// answers with a close, and hand the error back unchanged.
2363    /// Without it every bound the decoder enforces would stop at *this endpoint
2364    /// refused the frame* while the peer, which is the one that broke the rule,
2365    /// saw a session that was still open and went on sending. "MUST close the
2366    /// session with a PROTOCOL_VIOLATION" is a statement about the wire.
2367    fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
2368        if let ConnectionError::Codec(inner) = &err {
2369            if let Some(code) = Self::codec_session_error_code(inner) {
2370                // QUIC application error codes are 62-bit; every code in this
2371                // registry is far below `u32::MAX`, and saturating rather than
2372                // truncating means a future code that is not could never be
2373                // reported as a different, assigned one.
2374                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2375                self.close(wire_code, inner.to_string().as_bytes());
2376            }
2377        }
2378        err
2379    }
2380
2381    /// Close the connection.
2382    pub fn close(&self, code: u32, reason: &[u8]) {
2383        self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
2384        self.transport.close(code, reason);
2385    }
2386}
2387
2388/// The Malformed Track condition a codec refusal on a data path is, on this
2389/// draft, or `None` for a refusal that is not one.
2390fn malformed_track_condition(err: &ConnectionError) -> Option<MalformedTrackCondition> {
2391    match err {
2392        ConnectionError::Codec(CodecError::MalformedTrack { .. }) => {
2393            Some(MalformedTrackCondition::PriorGap)
2394        }
2395        // draft-15 Section 11.1: "A Key-Value-Pair cannot be parsed."
2396        ConnectionError::Codec(CodecError::MalformedObjectProperties { .. }) => {
2397            Some(MalformedTrackCondition::MalformedPropertyBlock)
2398        }
2399        _ => None,
2400    }
2401}
2402
2403/// Determine the encoded length of a varint from its first byte.
2404fn varint_len(first_byte: u8) -> usize {
2405    1 << (first_byte >> 6)
2406}
2407
2408#[cfg(test)]
2409mod tests {
2410    use super::*;
2411
2412    /// Only a refusal of what the peer sent is reported as a refused frame,
2413    /// and only with the bytes it was about.
2414    #[test]
2415    fn only_a_refusal_of_the_peers_frame_is_reported() {
2416        let raw = Some(vec![0x01]);
2417        let decoder = ConnectionError::Codec(CodecError::UnexpectedEnd);
2418        assert!(Connection::refused_event(&decoder, raw.clone()).is_some());
2419        assert!(Connection::refused_event(&decoder, None).is_none());
2420        let local = ConnectionError::Endpoint(EndpointError::NotActive);
2421        assert!(Connection::refused_event(&local, raw).is_none());
2422    }
2423
2424    /// This build failing to narrow a message it decoded is never a finding
2425    /// about the peer.
2426    ///
2427    /// The arm that raises `ControlMessageNarrowing` is unreachable — this
2428    /// draft's decoder can only hand back this draft's variant — and nothing
2429    /// pins that. What is pinned here is the half that matters.
2430    /// `CodecError::UnknownMessageType(0)` is what the arm must not raise:
2431    /// `codec_session_error_code` answers it `Some(PROTOCOL_VIOLATION)` on
2432    /// every draft in range, so the day the narrowing failed a conformance
2433    /// probe would publish a relay for sending a control message type this
2434    /// draft does not assign — with `0x00` attached as the codepoint that
2435    /// proved it, which is an accusation better evidenced than any real one
2436    /// this build makes. The section stating that rule is numbered differently
2437    /// on every draft, and the point does not turn on the number.
2438    #[test]
2439    fn a_message_this_build_could_not_narrow_names_nobody() {
2440        use crate::dispatch::{AnyConnectionError, ErrorCause};
2441
2442        let err: AnyConnectionError = ConnectionError::ControlMessageNarrowing.into();
2443        assert!(err.is_local(), "a narrowing this build could not do is this build's");
2444        assert_eq!(
2445            err.cause(),
2446            &ErrorCause::Facade,
2447            "nothing reached the wire, so there is no rule and no close code to read"
2448        );
2449    }
2450
2451    #[test]
2452    fn varint_len_single_byte() {
2453        assert_eq!(varint_len(0x00), 1);
2454        assert_eq!(varint_len(0x3F), 1);
2455    }
2456
2457    #[test]
2458    fn varint_len_two_bytes() {
2459        assert_eq!(varint_len(0x40), 2);
2460        assert_eq!(varint_len(0x7F), 2);
2461    }
2462
2463    #[test]
2464    fn varint_len_four_bytes() {
2465        assert_eq!(varint_len(0x80), 4);
2466        assert_eq!(varint_len(0xBF), 4);
2467    }
2468
2469    #[test]
2470    fn varint_len_eight_bytes() {
2471        assert_eq!(varint_len(0xC0), 8);
2472        assert_eq!(varint_len(0xFF), 8);
2473    }
2474
2475    #[test]
2476    fn client_config_alpn_quic_draft15() {
2477        let config = ClientConfig {
2478            draft: DraftVersion::Draft15,
2479            transport: TransportType::Quic,
2480            skip_cert_verification: false,
2481            ca_certs: Vec::new(),
2482            setup_parameters: Vec::new(),
2483        };
2484        assert_eq!(config.alpn(), vec![b"moqt-15".to_vec()]);
2485    }
2486
2487    #[test]
2488    fn client_config_alpn_webtransport() {
2489        let config = ClientConfig {
2490            draft: DraftVersion::Draft15,
2491            transport: TransportType::WebTransport { url: "https://example.com".to_string() },
2492            skip_cert_verification: false,
2493            ca_certs: Vec::new(),
2494            setup_parameters: Vec::new(),
2495        };
2496        assert_eq!(config.alpn(), vec![b"h3".to_vec()]);
2497    }
2498
2499    /// `MOQT_ALPN` is the ALPN a client configured for this draft offers.
2500    #[test]
2501    fn moqt_alpn_is_the_one_a_client_offers() {
2502        // Checked against what a client configured for this draft puts on the
2503        // wire, and only then against the literal.
2504        let config = ClientConfig {
2505            draft: DraftVersion::Draft15,
2506            transport: TransportType::Quic,
2507            skip_cert_verification: false,
2508            ca_certs: Vec::new(),
2509            setup_parameters: Vec::new(),
2510        };
2511        assert_eq!(
2512            config.alpn(),
2513            vec![MOQT_ALPN.to_vec()],
2514            "MOQT_ALPN is {:?}; a draft-{} client offers {:?}",
2515            String::from_utf8_lossy(MOQT_ALPN),
2516            15,
2517            config
2518                .alpn()
2519                .iter()
2520                .map(|a| String::from_utf8_lossy(a).into_owned())
2521                .collect::<Vec<_>>(),
2522        );
2523        assert_eq!(MOQT_ALPN, b"moqt-15");
2524    }
2525
2526    #[test]
2527    fn transport_type_debug() {
2528        let quic = TransportType::Quic;
2529        assert!(format!("{quic:?}").contains("Quic"));
2530
2531        let wt = TransportType::WebTransport { url: "https://example.com".to_string() };
2532        assert!(format!("{wt:?}").contains("WebTransport"));
2533    }
2534}