Skip to main content

moqtap_client/draft07/
connection.rs

1use bytes::{Buf, Bytes, BytesMut};
2
3use crate::draft07::endpoint::{Endpoint, EndpointError, Role};
4use crate::draft07::event::{ClientEvent, Direction, FetchObject, StreamKind, SubgroupObject};
5use crate::draft07::observer::ConnectionObserver;
6use crate::draft07::session::setup;
7use crate::forwarding_preference::ObjectForwardingPreference;
8use crate::transport::{RecvStream, SendStream, Transport, TransportError};
9use moqtap_codec::dispatch::{
10    AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
11};
12use moqtap_codec::draft07::data_stream::{FetchObjectHeader, ObjectHeader};
13use moqtap_codec::draft07::message::ControlMessage;
14use moqtap_codec::error::CodecError;
15use moqtap_codec::kvp::KeyValuePair;
16use moqtap_codec::types::*;
17use moqtap_codec::varint::VarInt;
18use moqtap_codec::version::DraftVersion;
19
20/// MoQT ALPN identifier (used by raw QUIC transport).
21pub const MOQT_ALPN: &[u8] = b"moq-00";
22
23/// Errors from the draft-07 connection layer.
24#[derive(Debug, thiserror::Error)]
25pub enum ConnectionError {
26    /// Endpoint state machine error.
27    #[error("endpoint error: {0}")]
28    Endpoint(#[from] EndpointError),
29    /// Wire codec error.
30    #[error("codec error: {0}")]
31    Codec(#[from] CodecError),
32    /// Transport-level error.
33    #[error("transport error: {0}")]
34    Transport(#[from] TransportError),
35    /// Variable-length integer decoding error.
36    #[error("varint error: {0}")]
37    VarInt(#[from] moqtap_codec::varint::VarIntError),
38    /// Control stream was not opened.
39    #[error("control stream not open")]
40    NoControlStream,
41    /// Stream ended before a complete message was read.
42    #[error("unexpected end of stream")]
43    UnexpectedEnd,
44    /// Stream was finished by the peer.
45    #[error("stream finished")]
46    StreamFinished,
47    /// Invalid server address string.
48    #[error("invalid server address: {0}")]
49    InvalidAddress(String),
50    /// TLS configuration error.
51    #[error("TLS config error: {0}")]
52    TlsConfig(String),
53    /// Data stream used out of order: an object before its header, or an
54    /// Object ID that does not advance on the last one written.
55    #[error("data stream state error: {0}")]
56    DataStreamState(&'static str),
57}
58
59impl From<crate::transport::DialError> for ConnectionError {
60    /// Preserves the variants this error had when the dial was inlined here,
61    /// so a caller matching on `InvalidAddress` or `TlsConfig` sees no change.
62    fn from(e: crate::transport::DialError) -> Self {
63        match e {
64            crate::transport::DialError::InvalidAddress(s) => ConnectionError::InvalidAddress(s),
65            crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
66            crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
67        }
68    }
69}
70
71/// Transport type for the connection.
72#[derive(Debug, Clone)]
73pub enum TransportType {
74    /// Raw QUIC via quinn. The `addr` field should be `host:port`.
75    Quic,
76    /// WebTransport via wtransport. The `url` field is the WebTransport URL.
77    WebTransport {
78        /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
79        url: String,
80    },
81}
82
83/// Configuration for a draft-07 MoQT client connection.
84pub struct ClientConfig {
85    /// Additional draft versions to offer in CLIENT_SETUP (draft-07 is always
86    /// offered first).
87    pub additional_versions: Vec<DraftVersion>,
88    /// The transport type (QUIC or WebTransport).
89    pub transport: TransportType,
90    /// Whether to skip TLS certificate verification (for testing).
91    pub skip_cert_verification: bool,
92    /// Custom CA certificates to trust (DER-encoded).
93    pub ca_certs: Vec<Vec<u8>>,
94    /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
95    pub setup_parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
96}
97
98impl ClientConfig {
99    /// Returns the MoQT version varints for the CLIENT_SETUP message.
100    /// Draft-07 first, then any additional versions.
101    pub fn supported_versions(&self) -> Vec<VarInt> {
102        let mut versions = vec![DraftVersion::Draft07.version_varint()];
103        for v in &self.additional_versions {
104            let varint = v.version_varint();
105            if !versions.contains(&varint) {
106                versions.push(varint);
107            }
108        }
109        versions
110    }
111
112    /// Returns the ALPN protocol identifiers for the transport.
113    pub fn alpn(&self) -> Vec<Vec<u8>> {
114        match &self.transport {
115            TransportType::Quic => vec![DraftVersion::Draft07.quic_alpn().to_vec()],
116            TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
117        }
118    }
119}
120
121/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
122pub struct FramedSendStream {
123    inner: SendStream,
124    /// The last Object ID written on this subgroup stream, once one has been.
125    ///
126    /// `None` before the first object; the outer `Option` is `None` until a
127    /// subgroup header has been written, which is what makes an object sent
128    /// before its header answerable rather than unframed bytes.
129    subgroup_objects: Option<Option<u64>>,
130}
131
132impl FramedSendStream {
133    /// Create a new framed send stream.
134    pub fn new(inner: SendStream) -> Self {
135        Self { inner, subgroup_objects: None }
136    }
137
138    /// Get the transport-level stream ID.
139    pub fn stream_id(&self) -> u64 {
140        self.inner.stream_id()
141    }
142
143    /// Write a control message to the stream with type+length framing.
144    /// Returns the raw bytes that were written (for event capture).
145    pub async fn write_control(
146        &mut self,
147        msg: &AnyControlMessage,
148    ) -> Result<Vec<u8>, ConnectionError> {
149        let mut buf = Vec::new();
150        msg.encode(&mut buf)?;
151        self.inner.write_all(&buf).await?;
152        Ok(buf)
153    }
154
155    /// Write a subgroup stream header. Also opens the Object ID bookkeeping
156    /// [`FramedSendStream::write_subgroup_object`] holds the stream to.
157    ///
158    /// Written through the checked encoder, which on this draft refuses
159    /// nothing: SUBGROUP_HEADER has one shape here, every field goes out every
160    /// time, and no type byte selects between them. The drafts that gained a
161    /// header type table need the refusal, and one call site for all thirteen
162    /// is what keeps this from being the draft where it was forgotten.
163    pub async fn write_subgroup_header(
164        &mut self,
165        header: &AnySubgroupHeader,
166    ) -> Result<(), ConnectionError> {
167        let mut buf = Vec::new();
168        header.encode_stream_checked(&mut buf)?;
169        self.inner.write_all(&buf).await?;
170        self.subgroup_objects = Some(None);
171        Ok(())
172    }
173
174    /// Write a fetch response header.
175    pub async fn write_fetch_header(
176        &mut self,
177        header: &AnyFetchHeader,
178    ) -> Result<(), ConnectionError> {
179        let mut buf = Vec::new();
180        header.encode_stream(&mut buf);
181        self.inner.write_all(&buf).await?;
182        Ok(())
183    }
184
185    /// Append a draft-07 subgroup object (header + payload) to the stream.
186    ///
187    /// Section 7.3.1: "A publisher MUST NOT send an Object on a stream if its
188    /// Object ID is less than a previously sent Object ID within a given group
189    /// in that stream." A subgroup stream carries one group, so the Object IDs
190    /// written here are exactly the ones that sentence compares, and the
191    /// comparison needs the object before - which no per-header check can see.
192    /// The state advances only once the object has been written, so declining to
193    /// write an object leaves the next one measured against the last one kept.
194    ///
195    /// An equal Object ID is refused as well as a smaller one. The draft's own
196    /// sentence forbids only "less than", but an Object ID names an Object
197    /// within a Group: writing one twice on a stream describes the same Object
198    /// with two different payloads, and a reader has no way to choose. The
199    /// dispatch-level writer in the codec draws the line in the same place, and
200    /// two writers that disagreed about it would be worse than either answer.
201    ///
202    /// # Errors
203    ///
204    /// [`ConnectionError::DataStreamState`] if no subgroup header has been
205    /// written yet, or if `object` does not advance past the last one written.
206    pub async fn write_subgroup_object(
207        &mut self,
208        object: &SubgroupObject,
209    ) -> Result<(), ConnectionError> {
210        let previous = self
211            .subgroup_objects
212            .as_mut()
213            .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
214        let object_id = object.header.object_id.into_inner();
215        if matches!(*previous, Some(prev) if object_id <= prev) {
216            return Err(ConnectionError::DataStreamState(
217                "object id does not advance on the last one written to this stream",
218            ));
219        }
220        // The declared length comes from the payload rather than from the
221        // caller's field: a header that disagrees with the bytes beside it
222        // desynchronises every object after it on the stream, and nothing
223        // downstream can recover.
224        let mut header = object.header.clone();
225        header.payload_length = VarInt::from_usize(object.payload.len());
226        let mut buf = Vec::new();
227        header.encode_checked(&mut buf)?;
228        buf.extend_from_slice(&object.payload);
229        self.inner.write_all(&buf).await?;
230        *previous = Some(object_id);
231        Ok(())
232    }
233
234    /// Append a draft-07 fetch object (header + payload) to the stream.
235    pub async fn write_fetch_object(
236        &mut self,
237        object: &FetchObject,
238    ) -> Result<(), ConnectionError> {
239        // The declared length comes from the payload rather than from the
240        // caller's field: a header that disagrees with the bytes beside it
241        // desynchronises every object after it on the stream, and nothing
242        // downstream can recover.
243        let mut header = object.header.clone();
244        header.payload_length = VarInt::from_usize(object.payload.len());
245        let mut buf = Vec::new();
246        header.encode_checked(&mut buf)?;
247        buf.extend_from_slice(&object.payload);
248        self.inner.write_all(&buf).await?;
249        Ok(())
250    }
251
252    /// Finish the stream (send FIN).
253    pub async fn finish(&mut self) -> Result<(), ConnectionError> {
254        self.inner.finish()?;
255        Ok(())
256    }
257}
258
259/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
260pub struct FramedRecvStream {
261    inner: RecvStream,
262    buf: BytesMut,
263}
264
265impl FramedRecvStream {
266    /// Create a new framed receive stream.
267    pub fn new(inner: RecvStream) -> Self {
268        Self { inner, buf: BytesMut::with_capacity(4096) }
269    }
270
271    /// Get the transport-level stream ID.
272    pub fn stream_id(&self) -> u64 {
273        self.inner.stream_id()
274    }
275
276    /// Read more data from the stream into the internal buffer.
277    async fn fill(&mut self) -> Result<bool, ConnectionError> {
278        let mut tmp = [0u8; 4096];
279        match self.inner.read(&mut tmp).await {
280            Ok(Some(n)) => {
281                self.buf.extend_from_slice(&tmp[..n]);
282                Ok(true)
283            }
284            Ok(None) => Ok(false),
285            Err(e) => Err(ConnectionError::Transport(e)),
286        }
287    }
288
289    /// Ensure at least `n` bytes are available in the buffer.
290    async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
291        while self.buf.len() < n {
292            if !self.fill().await? {
293                return Err(ConnectionError::UnexpectedEnd);
294            }
295        }
296        Ok(())
297    }
298
299    /// Read a control message from the stream.
300    ///
301    /// When `capture_raw` is true, the returned tuple includes a clone of the
302    /// framed wire bytes (for observer emission). When false, the second
303    /// element is `None` and the payload clone is skipped.
304    pub async fn read_control(
305        &mut self,
306        capture_raw: bool,
307    ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
308        // Read type ID varint
309        self.ensure(1).await?;
310        let type_len = varint_len(self.buf[0]);
311        self.ensure(type_len).await?;
312
313        let mut cursor = &self.buf[..type_len];
314        let _type_id = VarInt::decode(&mut cursor)?;
315
316        // Draft-07 uses varint length framing.
317        self.ensure(type_len + 1).await?;
318        let payload_len_start = type_len;
319        let payload_len_varint_len = varint_len(self.buf[payload_len_start]);
320        self.ensure(type_len + payload_len_varint_len).await?;
321        let mut cursor = &self.buf[payload_len_start..type_len + payload_len_varint_len];
322        let payload_len = VarInt::decode(&mut cursor)?.into_inner() as usize;
323        let len_field_size = payload_len_varint_len;
324
325        // Read full payload
326        let total = type_len + len_field_size + payload_len;
327        self.ensure(total).await?;
328
329        // Capture raw bytes only if requested (observer attached).
330        let raw = capture_raw.then(|| self.buf[..total].to_vec());
331
332        // Now decode the whole message using the draft-07 dispatcher
333        let mut frame = &self.buf[..total];
334        let msg = AnyControlMessage::decode(DraftVersion::Draft07, &mut frame)?;
335        self.buf.advance(total);
336        Ok((msg, raw))
337    }
338
339    /// Read a subgroup stream header.
340    pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
341        self.ensure(1).await?;
342        loop {
343            let mut cursor = &self.buf[..];
344            match AnySubgroupHeader::decode_stream(DraftVersion::Draft07, &mut cursor) {
345                Ok(header) => {
346                    let consumed = self.buf.len() - cursor.remaining();
347                    self.buf.advance(consumed);
348                    return Ok(header);
349                }
350                Err(CodecError::UnexpectedEnd) => {
351                    if !self.fill().await? {
352                        return Err(ConnectionError::UnexpectedEnd);
353                    }
354                }
355                Err(e) => return Err(ConnectionError::Codec(e)),
356            }
357        }
358    }
359
360    /// Read a fetch response header.
361    pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
362        self.ensure(1).await?;
363        loop {
364            let mut cursor = &self.buf[..];
365            match AnyFetchHeader::decode_stream(DraftVersion::Draft07, &mut cursor) {
366                Ok(header) => {
367                    let consumed = self.buf.len() - cursor.remaining();
368                    self.buf.advance(consumed);
369                    return Ok(header);
370                }
371                Err(CodecError::UnexpectedEnd) => {
372                    if !self.fill().await? {
373                        return Err(ConnectionError::UnexpectedEnd);
374                    }
375                }
376                Err(e) => return Err(ConnectionError::Codec(e)),
377            }
378        }
379    }
380
381    /// Read the next draft-07 subgroup object (header + payload). Since
382    /// draft-07 subgroup objects are stateless, this does not require any
383    /// prior header-decoding state.
384    pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
385        loop {
386            let mut cursor = &self.buf[..];
387            match ObjectHeader::decode(&mut cursor) {
388                Ok(header) => {
389                    let header_consumed = self.buf.len() - cursor.remaining();
390                    let payload_len = header.payload_length.into_inner() as usize;
391                    let total = header_consumed + payload_len;
392                    if self.buf.len() < total {
393                        if !self.fill().await? {
394                            return Err(ConnectionError::UnexpectedEnd);
395                        }
396                        continue;
397                    }
398                    let payload = self.buf[header_consumed..total].to_vec();
399                    self.buf.advance(total);
400                    return Ok(SubgroupObject { header, payload });
401                }
402                Err(CodecError::UnexpectedEnd) => {
403                    if !self.fill().await? {
404                        return Err(ConnectionError::UnexpectedEnd);
405                    }
406                }
407                Err(e) => return Err(ConnectionError::Codec(e)),
408            }
409        }
410    }
411
412    /// Read the next draft-07 fetch object (header + payload).
413    pub async fn read_fetch_object(&mut self) -> Result<FetchObject, ConnectionError> {
414        loop {
415            let mut cursor = &self.buf[..];
416            match FetchObjectHeader::decode(&mut cursor) {
417                Ok(header) => {
418                    let header_consumed = self.buf.len() - cursor.remaining();
419                    let payload_len = header.payload_length.into_inner() as usize;
420                    let total = header_consumed + payload_len;
421                    if self.buf.len() < total {
422                        if !self.fill().await? {
423                            return Err(ConnectionError::UnexpectedEnd);
424                        }
425                        continue;
426                    }
427                    let payload = self.buf[header_consumed..total].to_vec();
428                    self.buf.advance(total);
429                    return Ok(FetchObject { header, payload });
430                }
431                Err(CodecError::UnexpectedEnd) => {
432                    if !self.fill().await? {
433                        return Err(ConnectionError::UnexpectedEnd);
434                    }
435                }
436                Err(e) => return Err(ConnectionError::Codec(e)),
437            }
438        }
439    }
440}
441
442/// A live draft-07 MoQT connection over QUIC or WebTransport.
443pub struct Connection {
444    transport: Transport,
445    endpoint: Endpoint,
446    control_send: Option<FramedSendStream>,
447    control_recv: Option<FramedRecvStream>,
448    observer: Option<Box<dyn ConnectionObserver>>,
449    /// Setup events buffered during `connect()` and replayed when an
450    /// observer attaches via `set_observer` — without this, an observer
451    /// attached after `connect` returns would never see the handshake.
452    pending_events: Vec<ClientEvent>,
453}
454
455impl Connection {
456    /// Connect to a draft-07 MoQT server as a client.
457    pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
458        // PATH is for native QUIC only, and the transport is known here and
459        // nowhere further in. Refusing before dialling means a session that
460        // the server would close on sight is never opened.
461        setup::validate_client_path_transport(
462            &config.setup_parameters,
463            matches!(config.transport, TransportType::WebTransport { .. }),
464        )
465        .map_err(EndpointError::from)?;
466
467        let transport = match &config.transport {
468            TransportType::Quic => Self::connect_quic(addr, &config).await?,
469            TransportType::WebTransport { url } => {
470                let url = url.clone();
471                Self::connect_webtransport(&url, &config).await?
472            }
473        };
474
475        Self::adopt(transport, config).await
476    }
477
478    /// Run the MoQT setup handshake over a transport somebody else established.
479    ///
480    /// For choosing the draft from what the server selected: dial once through
481    /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
482    /// connection to the module its answer names. [`Self::connect`] cannot do
483    /// this — it derives its single ALPN from the draft it was given.
484    ///
485    /// `config.draft` must match this module. The transport is adopted as
486    /// given; nothing here re-checks the ALPN it was negotiated with.
487    pub async fn adopt(
488        transport: Transport,
489        config: ClientConfig,
490    ) -> Result<Self, ConnectionError> {
491        // PATH is for native QUIC only, and the transport is known here and
492        // nowhere further in. Refusing before dialling means a session that
493        // the server would close on sight is never opened.
494        setup::validate_client_path_transport(
495            &config.setup_parameters,
496            matches!(config.transport, TransportType::WebTransport { .. }),
497        )
498        .map_err(EndpointError::from)?;
499
500        // Open bidirectional control stream
501        let (send, recv) = transport.open_bi().await?;
502        let mut control_send = FramedSendStream::new(send);
503        let mut control_recv = FramedRecvStream::new(recv);
504
505        // Perform setup handshake
506        let mut endpoint = Endpoint::new(Role::Client);
507        endpoint.connect()?;
508        let setup_msg = endpoint
509            .send_client_setup(config.supported_versions(), config.setup_parameters.clone())?;
510        let any_setup = AnyControlMessage::Draft07(setup_msg);
511        let raw_setup = control_send.write_control(&any_setup).await?;
512
513        let (server_setup, raw_server_setup) = control_recv.read_control(true).await?;
514        match &server_setup {
515            AnyControlMessage::Draft07(ControlMessage::ServerSetup(ref ss)) => {
516                endpoint.receive_server_setup(ss)?;
517            }
518            _ => {
519                return Err(ConnectionError::Endpoint(EndpointError::NotActive));
520            }
521        }
522
523        let mut pending_events = Vec::with_capacity(3);
524        pending_events.push(ClientEvent::ControlMessage {
525            direction: Direction::Send,
526            message: any_setup,
527            raw: Some(raw_setup),
528        });
529        pending_events.push(ClientEvent::ControlMessage {
530            direction: Direction::Receive,
531            message: server_setup,
532            raw: raw_server_setup,
533        });
534        if let Some(v) = endpoint.negotiated_version() {
535            pending_events.push(ClientEvent::SetupComplete { negotiated_version: v.into_inner() });
536        }
537
538        Ok(Self {
539            transport,
540            endpoint,
541            control_send: Some(control_send),
542            control_recv: Some(control_recv),
543            observer: None,
544            pending_events,
545        })
546    }
547
548    /// Establish a raw QUIC connection.
549    ///
550    /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
551    /// TLS and endpoint setup.
552    async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
553        let (transport, _negotiated) = crate::transport::dial_quic(
554            addr,
555            &crate::transport::QuicDialOptions {
556                skip_cert_verification: config.skip_cert_verification,
557                ca_certs: config.ca_certs.clone(),
558                alpn: config.alpn(),
559            },
560        )
561        .await?;
562        Ok(transport)
563    }
564
565    /// Establish a WebTransport connection.
566    #[cfg(feature = "webtransport")]
567    async fn connect_webtransport(
568        url: &str,
569        config: &ClientConfig,
570    ) -> Result<Transport, ConnectionError> {
571        use crate::transport::webtransport::WebTransportTransport;
572
573        let wt_config = if config.skip_cert_verification {
574            wtransport::ClientConfig::builder()
575                .with_bind_default()
576                .with_no_cert_validation()
577                .build()
578        } else {
579            wtransport::ClientConfig::builder().with_bind_default().with_native_certs().build()
580        };
581
582        let endpoint = wtransport::Endpoint::client(wt_config)
583            .map_err(|e| ConnectionError::Transport(TransportError::Connect(e.to_string())))?;
584
585        let connection = endpoint
586            .connect(url)
587            .await
588            .map_err(|e| ConnectionError::Transport(TransportError::Connect(e.to_string())))?;
589
590        Ok(Transport::WebTransport(WebTransportTransport::new(connection)))
591    }
592
593    /// Stub for when the webtransport feature is not enabled.
594    #[cfg(not(feature = "webtransport"))]
595    async fn connect_webtransport(
596        _url: &str,
597        _config: &ClientConfig,
598    ) -> Result<Transport, ConnectionError> {
599        Err(ConnectionError::Transport(TransportError::Connect(
600            "webtransport feature not enabled".into(),
601        )))
602    }
603
604    // ── Observer ───────────────────────────────────────────────
605
606    /// Attach an observer. Buffered handshake events from `connect()` are
607    /// flushed in arrival order before this returns.
608    pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
609        self.observer = Some(observer);
610        for event in self.pending_events.drain(..) {
611            if let Some(ref obs) = self.observer {
612                obs.on_event_owned(event);
613            }
614        }
615    }
616
617    /// Remove the observer.
618    pub fn clear_observer(&mut self) {
619        self.observer = None;
620    }
621
622    /// Emit an event to the observer, if one is attached.
623    fn emit(&self, event: ClientEvent) {
624        if let Some(ref obs) = self.observer {
625            obs.on_event_owned(event);
626        }
627    }
628
629    // ── Control message I/O ─────────────────────────────────
630
631    /// Send a control message on the control stream.
632    pub async fn send_control(&mut self, msg: &ControlMessage) -> Result<(), ConnectionError> {
633        let any = AnyControlMessage::Draft07(msg.clone());
634        let send = self.control_send.as_mut().ok_or(ConnectionError::NoControlStream)?;
635        let raw = send.write_control(&any).await?;
636        self.emit(ClientEvent::ControlMessage {
637            direction: Direction::Send,
638            message: any,
639            raw: Some(raw),
640        });
641        Ok(())
642    }
643
644    /// Read the next control message from the control stream.
645    pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
646        let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
647        let capture_raw = self.observer.is_some();
648        let (any, raw) = match recv.read_control(capture_raw).await {
649            Ok(v) => v,
650            Err(e) => return Err(self.close_for_codec(e)),
651        };
652        if capture_raw {
653            self.emit(ClientEvent::ControlMessage {
654                direction: Direction::Receive,
655                message: any.clone(),
656                raw,
657            });
658        }
659        match any {
660            AnyControlMessage::Draft07(msg) => Ok(msg),
661            // `AnyControlMessage` carries one variant per enabled draft feature. With draft 07 the
662            // only one enabled the arm above is exhaustive and this rejection arm unreachable.
663            // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
664            // naming the other thirteen drafts: that list had to be edited in every draft module
665            // whenever a draft was added, and a copy that omitted one left this match
666            // non-exhaustive.
667            #[allow(unreachable_patterns)]
668            _ => Err(ConnectionError::Codec(CodecError::UnknownMessageType(0))),
669        }
670    }
671
672    /// Read and dispatch the next incoming control message through the endpoint
673    /// state machine. Returns the decoded message for inspection.
674    pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
675        let msg = self.recv_control().await?;
676        self.endpoint.receive_message(msg.clone()).map_err(|e| self.close_if_session_fatal(e))?;
677
678        if let ControlMessage::GoAway(ref ga) = msg {
679            self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
680        }
681
682        Ok(msg)
683    }
684
685    // ── Subscribe flow ──────────────────────────────────────
686
687    /// Send a SUBSCRIBE and return the allocated subscribe ID.
688    #[allow(clippy::too_many_arguments)]
689    pub async fn subscribe(
690        &mut self,
691        track_alias: VarInt,
692        track_namespace: TrackNamespace,
693        track_name: Vec<u8>,
694        subscriber_priority: u8,
695        group_order: GroupOrder,
696        filter_type: FilterType,
697    ) -> Result<VarInt, ConnectionError> {
698        let (sub_id, msg) = self.endpoint.subscribe(
699            track_alias,
700            track_namespace,
701            track_name,
702            subscriber_priority,
703            group_order,
704            filter_type,
705        )?;
706        self.send_control(&msg).await?;
707        Ok(sub_id)
708    }
709
710    /// Send a SUBSCRIBE for a range of the track and return the allocated ID.
711    ///
712    /// The Filter Type comes from the arguments, so the message cannot name a
713    /// filter whose fields it does not carry.
714    #[allow(clippy::too_many_arguments)]
715    pub async fn subscribe_range(
716        &mut self,
717        track_alias: VarInt,
718        track_namespace: TrackNamespace,
719        track_name: Vec<u8>,
720        subscriber_priority: u8,
721        group_order: GroupOrder,
722        start_location: Location,
723        end_location: Option<Location>,
724    ) -> Result<VarInt, ConnectionError> {
725        let (sub_id, msg) = self.endpoint.subscribe_range(
726            track_alias,
727            track_namespace,
728            track_name,
729            subscriber_priority,
730            group_order,
731            start_location,
732            end_location,
733        )?;
734        self.send_control(&msg).await?;
735        Ok(sub_id)
736    }
737
738    /// Send an UNSUBSCRIBE for the given subscribe ID.
739    pub async fn unsubscribe(&mut self, subscribe_id: VarInt) -> Result<(), ConnectionError> {
740        let msg = self.endpoint.unsubscribe(subscribe_id)?;
741        self.send_control(&msg).await
742    }
743
744    /// Accept a subscription the peer opened, sending SUBSCRIBE_OK.
745    pub async fn subscribe_ok(
746        &mut self,
747        subscribe_id: VarInt,
748        expires: VarInt,
749        group_order: GroupOrder,
750        parameters: Vec<KeyValuePair>,
751    ) -> Result<(), ConnectionError> {
752        let msg =
753            self.endpoint.send_subscribe_ok(subscribe_id, expires, group_order, parameters)?;
754        self.send_control(&msg).await
755    }
756
757    /// Reject a subscription the peer opened, sending SUBSCRIBE_ERROR.
758    ///
759    /// The Track Alias travels back with the refusal: under the 'Retry Track
760    /// Alias' code it is the alias the peer should try again with, and under
761    /// any other code it is ignored.
762    pub async fn subscribe_error(
763        &mut self,
764        subscribe_id: VarInt,
765        error_code: VarInt,
766        reason_phrase: Vec<u8>,
767        track_alias: VarInt,
768    ) -> Result<(), ConnectionError> {
769        let msg = self.endpoint.send_subscribe_error(
770            subscribe_id,
771            error_code,
772            reason_phrase,
773            track_alias,
774        )?;
775        self.send_control(&msg).await
776    }
777
778    /// End a subscription this endpoint accepted, sending SUBSCRIBE_DONE.
779    pub async fn subscribe_done(
780        &mut self,
781        subscribe_id: VarInt,
782        status_code: VarInt,
783        reason_phrase: Vec<u8>,
784    ) -> Result<(), ConnectionError> {
785        let msg = self.endpoint.send_subscribe_done(subscribe_id, status_code, reason_phrase)?;
786        self.send_control(&msg).await
787    }
788
789    // ── Fetch flow ──────────────────────────────────────────
790
791    /// Send a FETCH and return the allocated subscribe ID.
792    #[allow(clippy::too_many_arguments)]
793    pub async fn fetch(
794        &mut self,
795        track_namespace: TrackNamespace,
796        track_name: Vec<u8>,
797        subscriber_priority: u8,
798        group_order: GroupOrder,
799        start_group: VarInt,
800        start_object: VarInt,
801        end_group: VarInt,
802        end_object: VarInt,
803    ) -> Result<VarInt, ConnectionError> {
804        let (sub_id, msg) = self.endpoint.fetch(
805            track_namespace,
806            track_name,
807            subscriber_priority,
808            group_order,
809            start_group,
810            start_object,
811            end_group,
812            end_object,
813        )?;
814        self.send_control(&msg).await?;
815        Ok(sub_id)
816    }
817
818    /// Send a FETCH_CANCEL for the given subscribe ID.
819    pub async fn fetch_cancel(&mut self, subscribe_id: VarInt) -> Result<(), ConnectionError> {
820        let msg = self.endpoint.fetch_cancel(subscribe_id)?;
821        self.send_control(&msg).await
822    }
823
824    /// Accept a fetch the peer opened, sending FETCH_OK.
825    ///
826    /// The endpoint refuses a Joining Fetch naming a subscription this session
827    /// cannot join and refuses a second answer to one FETCH, so nothing is
828    /// written on the wire when it does either.
829    pub async fn fetch_ok(
830        &mut self,
831        subscribe_id: VarInt,
832        group_order: GroupOrder,
833        end_of_track: u8,
834        largest_group_id: Option<VarInt>,
835        largest_object_id: Option<VarInt>,
836        parameters: Vec<KeyValuePair>,
837    ) -> Result<(), ConnectionError> {
838        let msg = self.endpoint.send_fetch_ok(
839            subscribe_id,
840            group_order,
841            end_of_track,
842            largest_group_id,
843            largest_object_id,
844            parameters,
845        )?;
846        self.send_control(&msg).await
847    }
848
849    /// Refuse a fetch the peer opened, sending FETCH_ERROR.
850    ///
851    /// The endpoint refuses a second answer to one FETCH, so nothing is
852    /// written on the wire when it does.
853    pub async fn fetch_error(
854        &mut self,
855        subscribe_id: VarInt,
856        error_code: VarInt,
857        reason_phrase: Vec<u8>,
858    ) -> Result<(), ConnectionError> {
859        let msg = self.endpoint.send_fetch_error(subscribe_id, error_code, reason_phrase)?;
860        self.send_control(&msg).await
861    }
862
863    // ── Namespace flows ─────────────────────────────────────
864
865    /// Send a SUBSCRIBE_ANNOUNCES.
866    pub async fn subscribe_announces(
867        &mut self,
868        track_namespace_prefix: TrackNamespace,
869    ) -> Result<(), ConnectionError> {
870        let msg = self.endpoint.subscribe_announces(track_namespace_prefix)?;
871        self.send_control(&msg).await
872    }
873
874    /// Accept a namespace subscription the peer made, sending SUBSCRIBE_ANNOUNCES_OK.
875    ///
876    /// The endpoint refuses a second answer to one SUBSCRIBE_ANNOUNCES, so nothing is
877    /// written on the wire when it does.
878    pub async fn subscribe_announces_ok(
879        &mut self,
880        track_namespace_prefix: TrackNamespace,
881    ) -> Result<(), ConnectionError> {
882        let msg = self.endpoint.send_subscribe_announces_ok(track_namespace_prefix)?;
883        self.send_control(&msg).await
884    }
885
886    /// Refuse a namespace subscription the peer made, sending SUBSCRIBE_ANNOUNCES_ERROR.
887    ///
888    /// The other half of the same sentence: one answer, and this is the other
889    /// one it can be.
890    pub async fn subscribe_announces_error(
891        &mut self,
892        track_namespace_prefix: TrackNamespace,
893        error_code: VarInt,
894        reason_phrase: Vec<u8>,
895    ) -> Result<(), ConnectionError> {
896        let msg = self.endpoint.send_subscribe_announces_error(
897            track_namespace_prefix,
898            error_code,
899            reason_phrase,
900        )?;
901        self.send_control(&msg).await
902    }
903
904    /// Send an ANNOUNCE.
905    pub async fn announce(
906        &mut self,
907        track_namespace: TrackNamespace,
908    ) -> Result<(), ConnectionError> {
909        let msg = self.endpoint.announce(track_namespace)?;
910        self.send_control(&msg).await
911    }
912
913    /// Send an UNANNOUNCE.
914    pub async fn unannounce(
915        &mut self,
916        track_namespace: TrackNamespace,
917    ) -> Result<(), ConnectionError> {
918        let msg = self.endpoint.unannounce(track_namespace)?;
919        self.send_control(&msg).await
920    }
921
922    /// Accept an announcement the peer made, sending ANNOUNCE_OK.
923    ///
924    /// The endpoint refuses a second answer to one ANNOUNCE, so nothing is
925    /// written on the wire when it does.
926    pub async fn announce_ok(
927        &mut self,
928        track_namespace: TrackNamespace,
929    ) -> Result<(), ConnectionError> {
930        let msg = self.endpoint.send_announce_ok(track_namespace)?;
931        self.send_control(&msg).await
932    }
933
934    /// Refuse an announcement the peer made, sending ANNOUNCE_ERROR.
935    ///
936    /// The other half of the same sentence: one answer, and this is the other
937    /// one it can be.
938    pub async fn announce_error(
939        &mut self,
940        track_namespace: TrackNamespace,
941        error_code: VarInt,
942        reason_phrase: Vec<u8>,
943    ) -> Result<(), ConnectionError> {
944        let msg = self.endpoint.send_announce_error(track_namespace, error_code, reason_phrase)?;
945        self.send_control(&msg).await
946    }
947
948    /// Revoke an acceptance, sending ANNOUNCE_CANCEL.
949    ///
950    /// The endpoint refuses one for an announcement it never accepted, so
951    /// nothing is written on the wire when it does.
952    pub async fn announce_cancel(
953        &mut self,
954        track_namespace: TrackNamespace,
955        error_code: VarInt,
956        reason_phrase: Vec<u8>,
957    ) -> Result<(), ConnectionError> {
958        let msg = self.endpoint.announce_cancel(track_namespace, error_code, reason_phrase)?;
959        self.send_control(&msg).await
960    }
961    // ── Track Status flow ────────────────────────────────────
962
963    /// Send a TRACK_STATUS_REQUEST.
964    pub async fn track_status_request(
965        &mut self,
966        track_namespace: TrackNamespace,
967        track_name: Vec<u8>,
968    ) -> Result<(), ConnectionError> {
969        let msg = self.endpoint.track_status_request(track_namespace, track_name)?;
970        self.send_control(&msg).await
971    }
972
973    /// Answer a TRACK_STATUS_REQUEST the peer sent, sending TRACK_STATUS.
974    ///
975    /// The endpoint refuses a second answer to one request, so nothing is
976    /// written on the wire when it does.
977    pub async fn track_status(
978        &mut self,
979        track_namespace: TrackNamespace,
980        track_name: Vec<u8>,
981        status_code: VarInt,
982        last_group_id: VarInt,
983        last_object_id: VarInt,
984    ) -> Result<(), ConnectionError> {
985        let msg = self.endpoint.send_track_status(
986            track_namespace,
987            track_name,
988            status_code,
989            last_group_id,
990            last_object_id,
991        )?;
992        self.send_control(&msg).await
993    }
994
995    // ── Data streams ────────────────────────────────────────
996
997    /// Open a new unidirectional stream for sending subgroup data.
998    pub async fn open_subgroup_stream(
999        &self,
1000        header: &AnySubgroupHeader,
1001    ) -> Result<FramedSendStream, ConnectionError> {
1002        // Before the stream is opened: the Original Publisher is who the rule
1003        // binds, so a header that would mix this track's framing is refused
1004        // here rather than written and answered by the peer.
1005        self.endpoint.note_object_forwarding_preference(
1006            header.track_alias(),
1007            ObjectForwardingPreference::Subgroup,
1008        )?;
1009        let send = self.transport.open_uni().await?;
1010        let mut framed = FramedSendStream::new(send);
1011        let sid = framed.stream_id();
1012        framed.write_subgroup_header(header).await?;
1013        self.emit(ClientEvent::StreamOpened {
1014            direction: Direction::Send,
1015            stream_kind: StreamKind::Subgroup,
1016            stream_id: sid,
1017        });
1018        self.emit(ClientEvent::DataStreamHeader {
1019            stream_id: sid,
1020            direction: Direction::Send,
1021            header: header.clone(),
1022        });
1023        Ok(framed)
1024    }
1025
1026    /// Open a new unidirectional stream for sending a FETCH's objects.
1027    ///
1028    /// The objects answering a FETCH do not go on the request's own stream:
1029    /// they go on a unidirectional stream of their own, which opens with a
1030    /// FETCH_HEADER naming the request they belong to. This writes that header
1031    /// and hands back the stream, the same way
1032    /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
1033    /// subgroup.
1034    ///
1035    /// The caller owns the stream that comes back. Nothing here remembers
1036    /// which request it belongs to, so an endpoint serving several fetches at
1037    /// once keeps its own map from Request ID to stream.
1038    pub async fn open_fetch_stream(
1039        &self,
1040        header: &AnyFetchHeader,
1041    ) -> Result<FramedSendStream, ConnectionError> {
1042        let send = self.transport.open_uni().await?;
1043        let mut framed = FramedSendStream::new(send);
1044        let sid = framed.stream_id();
1045        framed.write_fetch_header(header).await?;
1046        self.emit(ClientEvent::StreamOpened {
1047            direction: Direction::Send,
1048            stream_kind: StreamKind::Fetch,
1049            stream_id: sid,
1050        });
1051        Ok(framed)
1052    }
1053
1054    /// Accept the next unidirectional stream and read its fetch header.
1055    ///
1056    /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
1057    /// are separate because the header decides how every object after it is
1058    /// framed, so a caller has to know which it is expecting before the first
1059    /// byte is read.
1060    ///
1061    /// Objects come off the returned stream with
1062    /// [`FramedRecvStream::read_fetch_object`].
1063    pub async fn accept_fetch_stream(
1064        &self,
1065    ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
1066        let recv = self.transport.accept_uni().await?;
1067        let mut framed = FramedRecvStream::new(recv);
1068        let sid = framed.stream_id();
1069        let header = framed.read_fetch_header().await?;
1070        self.emit(ClientEvent::StreamOpened {
1071            direction: Direction::Receive,
1072            stream_kind: StreamKind::Fetch,
1073            stream_id: sid,
1074        });
1075        self.emit(ClientEvent::FetchStreamHeader {
1076            stream_id: sid,
1077            direction: Direction::Receive,
1078            header: header.clone(),
1079        });
1080        // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
1081        // carries an `AnySubgroupHeader` and cannot express one. What
1082        // `accept_subgroup_stream` does beyond this - the forwarding-preference
1083        // note, the object measurement - is about a subgroup and has no
1084        // counterpart on a fetch stream.
1085        Ok((header, framed))
1086    }
1087
1088    /// Accept an incoming unidirectional data stream and read its subgroup
1089    /// header.
1090    pub async fn accept_subgroup_stream(
1091        &self,
1092    ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
1093        let recv = self.transport.accept_uni().await?;
1094        let mut framed = FramedRecvStream::new(recv);
1095        let sid = framed.stream_id();
1096        let header = framed.read_subgroup_header().await?;
1097        self.emit(ClientEvent::StreamOpened {
1098            direction: Direction::Receive,
1099            stream_kind: StreamKind::Subgroup,
1100            stream_id: sid,
1101        });
1102        self.emit(ClientEvent::DataStreamHeader {
1103            stream_id: sid,
1104            direction: Direction::Receive,
1105            header: header.clone(),
1106        });
1107        // Every object on a subgroup stream has the Subgroup preference, so
1108        // the header settles the track's framing before a single object is
1109        // read.
1110        self.endpoint.note_object_forwarding_preference(
1111            header.track_alias(),
1112            ObjectForwardingPreference::Subgroup,
1113        )?;
1114        Ok((header, framed))
1115    }
1116
1117    /// Send an object via datagram.
1118    ///
1119    /// The header goes through `AnyDatagramHeader::encode`, which refuses a
1120    /// header whose Object Status the framing it names cannot carry. Such a
1121    /// header errors here and nothing is sent, rather than going out as an
1122    /// ordinary payload datagram with the status quietly dropped.
1123    pub fn send_datagram(
1124        &self,
1125        header: &AnyDatagramHeader,
1126        payload: &[u8],
1127    ) -> Result<(), ConnectionError> {
1128        // Before anything is encoded, for the reason `open_subgroup_stream`
1129        // gives.
1130        self.endpoint.note_object_forwarding_preference(
1131            header.meta().track_alias,
1132            ObjectForwardingPreference::Datagram,
1133        )?;
1134        let mut buf = Vec::new();
1135        header.encode(&mut buf)?;
1136        buf.extend_from_slice(payload);
1137        self.emit(ClientEvent::DatagramReceived {
1138            direction: Direction::Send,
1139            header: header.clone(),
1140            payload_len: payload.len(),
1141        });
1142        self.transport.send_datagram(bytes::Bytes::from(buf))?;
1143        Ok(())
1144    }
1145
1146    /// Receive a datagram and decode its header.
1147    pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
1148        let data = self.transport.recv_datagram().await?;
1149        let mut cursor = &data[..];
1150        let header = AnyDatagramHeader::decode(DraftVersion::Draft07, &mut cursor)?;
1151        let consumed = data.len() - cursor.len();
1152        let payload = data.slice(consumed..);
1153        self.emit(ClientEvent::DatagramReceived {
1154            direction: Direction::Receive,
1155            header: header.clone(),
1156            payload_len: payload.len(),
1157        });
1158        // A datagram is the other framing, and it settles the track's just as a
1159        // subgroup header does.
1160        self.endpoint.note_object_forwarding_preference(
1161            header.meta().track_alias,
1162            ObjectForwardingPreference::Datagram,
1163        )?;
1164        Ok((header, payload))
1165    }
1166
1167    // ── Accessors ───────────────────────────────────────────
1168
1169    /// Access the underlying endpoint state machine.
1170    pub fn endpoint(&self) -> &Endpoint {
1171        &self.endpoint
1172    }
1173
1174    /// Mutable access to the endpoint state machine.
1175    pub fn endpoint_mut(&mut self) -> &mut Endpoint {
1176        &mut self.endpoint
1177    }
1178
1179    /// Get the negotiated MoQT version.
1180    pub fn negotiated_version(&self) -> Option<VarInt> {
1181        self.endpoint.negotiated_version()
1182    }
1183
1184    /// The code to close the session with when a message could not be decoded
1185    /// because the peer broke a rule draft-07 answers with a close.
1186    ///
1187    /// Every variant listed here comes from a sentence in this draft that names
1188    /// the consequence, and the list is per draft: answering a bound this draft
1189    /// does not state would close a session over traffic a conforming peer may
1190    /// send. This draft's list is the shortest of the thirteen and shares only
1191    /// two entries with draft-08's, which sits next to it.
1192    ///
1193    ///   - Duplicate parameters, a SHOULD rather than a MUST: "Receivers SHOULD
1194    ///     check that there are no duplicate parameters and close the session as
1195    ///     a 'Protocol Violation' if found." Unqualified here, as on drafts 08
1196    ///     through 10: there is no carve-out for repeats a message authorizes
1197    ///     and none for duplicates of unknown parameters, both of which arrive
1198    ///     at draft-11 and make the rule asymmetric there.
1199    ///   - Unknown control message type: "An endpoint that receives an unknown
1200    ///     message type MUST close the session." The sentence names no code, so
1201    ///     Protocol Violation is what carries it, as on every other draft.
1202    ///   - A parameter whose value does not match the length its type implies —
1203    ///     the one rule here **not** answered with a Protocol Violation: "If a
1204    ///     receiver understands a parameter type, and the parameter length
1205    ///     implied by that type does not match the Parameter Length field, the
1206    ///     receiver MUST terminate the session with error code 'Parameter Length
1207    ///     Mismatch'." Drafts 08, 09 and 10 carry the same sentence; drafts 11
1208    ///     and later drop it along with the Parameter framing it describes.
1209    ///
1210    /// **Not** the Track Namespace tuple size, and this is where draft-07 parts
1211    /// company with every draft above it. Section 2.3 states the range — "an
1212    /// ordered N-tuple of bytes where N can be between 1 and 32" — and stops
1213    /// there. It is draft-08 Section 2.4.1 that adds the consequence: "If an
1214    /// endpoint receives a Track Namespace tuple with an N of 0 or more than
1215    /// 32, it MUST close the session with a Protocol Violation." A tuple of 40
1216    /// fields is malformed on this draft and the session survives it, so the
1217    /// codec does not raise the variant here and this table would have nothing
1218    /// to answer if it did.
1219    ///
1220    /// **Not** the Reason Phrase maximum, the GOAWAY New Session URI maximum,
1221    /// the Full Track Name maximum or the parameter value maximum. Those enter
1222    /// the specification at draft-11 and this draft states none of them.
1223    ///
1224    /// **Not** the end-of-track Object ID rule either, which drafts 08, 09 and
1225    /// 10 do state: this draft assigns no Object Status 0x5 for it to be about.
1226    ///
1227    /// **Not** [`CodecError::UnexpectedEnd`], which reports no rule at all: the
1228    /// reader raises it whenever a message is still arriving, and
1229    /// `read_control` loops on it. Closing over it would end a session on an
1230    /// ordinary short read.
1231    ///
1232    /// **Not** the key-value pair serialization rule. Drafts 11 and later
1233    /// require a close with KEY_VALUE_FORMATTING_ERROR when a value does not
1234    /// match the serialization its Type defines; this draft has no Key-Value
1235    /// Pair at all. What it states instead is the Parameter Length Mismatch
1236    /// rule above, over the Parameter framing it has in its place.
1237    ///
1238    /// **Not** the unknown Message Parameter rule, which enters at draft-16 and
1239    /// requires a close for a Message Parameter type the negotiated version does
1240    /// not define. This draft states nothing of the kind, and its parameters are
1241    /// not Key-Value-Pairs at all.
1242    ///
1243    /// `None` for everything else, including [`CodecError::InvalidField`]. That
1244    /// variant is shared by a dozen unrelated malformations, only some of which
1245    /// the draft answers with a close, so a session cannot be ended on it
1246    /// without ending sessions the draft does not ask to be ended. Two of this
1247    /// draft's own rules are stuck behind it — an unknown data stream type, and
1248    /// a ContentExists field holding anything but 0 or 1, both of which this
1249    /// draft calls a protocol error. Splitting it is the way to bring them
1250    /// under this function; widening the match is not.
1251    fn codec_session_error_code(
1252        err: &CodecError,
1253    ) -> Option<moqtap_codec::draft07::error_codes::SessionErrorCode> {
1254        use moqtap_codec::draft07::error_codes::SessionErrorCode;
1255        use moqtap_codec::kvp::KvpError;
1256        match err {
1257            // The declared Length disagreeing with the fields, which every
1258            // draft answers with a close. Drafts 07 through 10 name no code for
1259            // it, so it takes the one their other unnamed rules take.
1260            CodecError::ControlMessageLengthMismatch { .. } => {
1261                Some(SessionErrorCode::ProtocolViolation)
1262            }
1263            // A Content Exists field that is neither zero nor one, Sections
1264            // 6.15 and 6.19: "Any other value is a protocol error and MUST
1265            // terminate the session with a Protocol Violation". Drafts 08
1266            // through 14 carry the field and the same sentence; draft-15 is
1267            // where it goes.
1268            CodecError::InvalidContentExists(_) => Some(SessionErrorCode::ProtocolViolation),
1269            CodecError::DuplicateParameter(_) | CodecError::UnknownMessageType(_) => {
1270                Some(SessionErrorCode::ProtocolViolation)
1271            }
1272            CodecError::ParameterLengthMismatch(_) => {
1273                Some(SessionErrorCode::ParameterLengthMismatch)
1274            }
1275            // An unknown data-plane type, Section 7: "An endpoint that
1276            // receives an unknown stream type MUST close the session." Draft-07
1277            // numbers its streams and its datagrams in one table, so the stream
1278            // variant carries both and the datagram variant never arrives here.
1279            CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
1280                Some(SessionErrorCode::ProtocolViolation)
1281            }
1282            // Everything this draft does not answer, named rather than swept up
1283            // by a wildcard. The arm is exhaustive deliberately: a new
1284            // `CodecError` variant will not compile until it has been placed on
1285            // one side or the other, on this draft, which is the decision a `_`
1286            // arm makes silently and invisibly on all thirteen at once.
1287            //
1288            // Adding one variant to `CodecError` was tried, and produces
1289            // thirteen `E0004`s, one per draft, each naming the variant that has
1290            // nowhere to go. That is the whole mechanism.
1291            //
1292            // The nesting stops at `VarInt`, whose variants report how the bytes
1293            // ran out rather than a rule an endpoint states, so there is nothing
1294            // in it for a draft to answer. `Kvp` is spelled out because it does
1295            // carry one.
1296            // Not `InvalidForward`: draft-07 has no Forward field. Not
1297            // `ParameterValueOutOfRange`: no parameter this draft defines
1298            // restricts its value's range. Both arrive with drafts above.
1299            CodecError::InvalidForward(_)
1300            | CodecError::ParameterValueOutOfRange { .. }
1301            | CodecError::UnexpectedEnd
1302            | CodecError::MessageTooLong(_)
1303            | CodecError::VarInt(_)
1304            | CodecError::InvalidField
1305            | CodecError::InvalidNamespaceTupleSize(_)
1306            | CodecError::EmptyNamespaceField
1307            | CodecError::TrackNameTooLong
1308            | CodecError::InvalidRange(..)
1309            | CodecError::EndOfTrackObjectId(_)
1310            | CodecError::KeyDeltaOverflow(..)
1311            // Not `TrackPropertyValueOutOfRange`: this draft has neither
1312            // namespace the variant is about. Draft-16 opens an extension header
1313            // registry with value rules of its own, and draft-17 renames it to
1314            // the Track Property registry. Before that, everything with a
1315            // restricted range is either a message field or a Message Parameter.
1316            | CodecError::TrackPropertyValueOutOfRange { .. }
1317            | CodecError::ParametersOutOfOrder(..)
1318            | CodecError::ObjectIdOverflow(..)
1319            | CodecError::ExtensionsOnNonExistentObject(_)
1320            | CodecError::InvalidRequiredRequestIdDelta(..)
1321            | CodecError::InvalidTypeValue { .. }
1322            | CodecError::ReasonPhraseTooLong
1323            | CodecError::GoAwayUriTooLong
1324            | CodecError::KeyValueFormatting { .. }
1325            | CodecError::UnknownMessageParameter(_)
1326            // Not `ParameterOutOfScope`: this draft states the scope rule and
1327            // answers it the other way. Section 6.1.1 Version Specific Parameters: "Each
1328            // version-specific parameter definition indicates the message types in which it can
1329            // appear. If it appears in some other type of message, it MUST be
1330            // ignored." The codec carries such a parameter on this draft and never
1331            // raises the variant, so this arm records a rule this draft has and
1332            // does not close over, not one it is missing. Draft-17 is where the
1333            // second sentence becomes a close.
1334            | CodecError::ParameterOutOfScope { .. }
1335            // A Filter Type outside the set this draft assigns. Section 6.4
1336            // states the rule and stops there: "A filter type other than the
1337            // above MUST be treated as error." No code, no close, and no
1338            // sentence elsewhere in the draft that turns an error into one — so
1339            // the message is refused and the session stays open.
1340            // Draft-14 Section 9.7 is where the same sentence gained "MUST be
1341            // close the session with PROTOCOL_VIOLATION", and it is answered
1342            // there.
1343            //
1344            // The assigned set is not the same on every draft either: 07 and 08
1345            // assign 0x1 as Latest Group, 09 and 10 withdraw it, and 11 and
1346            // later reinstate it as Next Group Start. The decoder holds each
1347            // draft to its own list; this arm only decides what a refusal does
1348            // to the session.
1349            //
1350            // The two rules below belong to the parameter form of the filter,
1351            // which arrives at draft-15. This draft carries the Filter Type as a
1352            // field of SUBSCRIBE, so there is no parameter for either to be
1353            // about.
1354            | CodecError::InvalidFilterType(_)
1355            | CodecError::SubscriptionFilterMalformed { .. }
1356            | CodecError::FilterEndGroupOverflow { .. }
1357            // Draft-07's FETCH (Section 6.7) carries no Fetch Type field at all:
1358            // every fetch on this draft is the standalone form, with a start and
1359            // an end and nothing to select between them. Draft-08 introduced the
1360            // field and the rule, and drafts 14 and later answer it with a close.
1361            | CodecError::InvalidFetchType(_)
1362            // The object payload rule, Section 7.1.1.1: "Any object with a status
1363            // code other than zero MUST have an empty payload." A MUST on the
1364            // sender with no receiver action named anywhere — the "SHOULD be
1365            // treated as a protocol error" in the same paragraph belongs to the
1366            // sentence before it, which is about a status value this draft does
1367            // not assign — so an object carrying a payload it may not is refused
1368            // and the session stays open.
1369            //
1370            // That was already the answer. The bytes used to arrive as
1371            // `InvalidField`, which is on this side too; naming the rule changes
1372            // nothing a peer can observe and makes the decision legible.
1373            | CodecError::PayloadNotPermitted { .. }
1374            | CodecError::UnsupportedDraft(_)
1375            | CodecError::Kvp(
1376                KvpError::ValueTooLong(_)
1377                | KvpError::MissingLength
1378                | KvpError::UnexpectedEnd
1379                | KvpError::VarInt(_),
1380            ) => None,
1381        }
1382    }
1383
1384    /// Close the session on the wire when a decode failure is one draft-07
1385    /// answers with a close, and hand the error back unchanged.
1386    /// Without it every bound the decoder enforces would stop at *this endpoint
1387    /// refused the frame* while the peer, which is the one that broke the rule,
1388    /// saw a session that was still open and went on sending. "MUST close the
1389    /// session" is a statement about the wire.
1390    fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
1391        if let ConnectionError::Codec(inner) = &err {
1392            if let Some(code) = Self::codec_session_error_code(inner) {
1393                // QUIC application error codes are 62-bit; every code in this
1394                // registry is far below `u32::MAX`, and saturating rather than
1395                // truncating means a future code that is not could never be
1396                // reported as a different, assigned one.
1397                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1398                self.close(wire_code, inner.to_string().as_bytes());
1399            }
1400        }
1401        err
1402    }
1403
1404    /// Close the session on the wire when the endpoint says a violation is
1405    /// fatal to it, and hand the error back unchanged.
1406    ///
1407    /// [`EndpointError::session_error_code`] answers `Some` for exactly the
1408    /// errors this draft ends the session over, and the endpoint has already
1409    /// moved its own state machine to Closed by the time this runs. Without
1410    /// this step that move is purely internal: the local endpoint refuses to
1411    /// start anything new while the peer, which is the one that broke the
1412    /// rule, sees a session that is still open and goes on sending. A rule
1413    /// that names a session termination code is a statement about the wire,
1414    /// so it takes a CONNECTION_CLOSE to satisfy it.
1415    ///
1416    /// The reason phrase is the error's own `Display` text, which names the
1417    /// rule rather than repeating the numeric code the close already carries.
1418    ///
1419    /// Errors that answer `None` are recoverable and nothing is sent.
1420    fn close_for(&self, err: &EndpointError) {
1421        if let Some(code) = err.session_error_code() {
1422            // QUIC application error codes are 62-bit; every code in this
1423            // registry is far below `u32::MAX`, and saturating rather than
1424            // truncating means a future code that is not could never be
1425            // reported as a different, assigned one.
1426            let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1427            self.close(wire_code, err.to_string().as_bytes());
1428        }
1429    }
1430
1431    /// [`close_for`](Self::close_for), then the error unchanged, for the
1432    /// common case where the endpoint's error is also what the caller returns.
1433    fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
1434        self.close_for(&err);
1435        ConnectionError::Endpoint(err)
1436    }
1437
1438    /// Close the session over a rule broken on a data stream, reporting whether
1439    /// it did.
1440    ///
1441    /// A data stream cannot close for itself the way `recv_control` does:
1442    /// [`Connection::accept_subgroup_stream`] hands the caller a
1443    /// [`FramedRecvStream`] holding no connection, so the reader that finds the
1444    /// violation is not the object that can act on it. Keeping it a separate
1445    /// call is deliberate as well — a permissive caller, one reproducing a
1446    /// capture, can read a violating stream and report it without tearing the
1447    /// session down.
1448    ///
1449    /// The rule this draft answers here is the unknown stream type, Section 7,
1450    /// which arrives as the very first varint on a unidirectional stream and
1451    /// nowhere else. Every draft from 07 to 19 states it, in one of two
1452    /// phrasings — this one names streams alone because draft-07 numbers its
1453    /// datagrams in the same table, and drafts 08 through 16,
1454    /// draft-08 Section 8 among them, say "an unknown stream or datagram type"
1455    /// for the two tables they split it into. It shares
1456    /// `codec_session_error_code` with the control path, so a rule is answered
1457    /// with one code whichever stream carried it.
1458    ///
1459    /// Not every rule that reaches here is the decoder's. A track whose objects
1460    /// mix forwarding preferences is the endpoint's to notice — it takes the
1461    /// alias table to know which track an object belongs to — and it arrives on
1462    /// exactly these streams. Both kinds are asked for a code the same way, and
1463    /// a rule with no code is declined rather than guessed at.
1464    pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
1465        match err {
1466            ConnectionError::Codec(inner) => {
1467                let Some(code) = Self::codec_session_error_code(inner) else { return false };
1468                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1469                self.close(wire_code, inner.to_string().as_bytes());
1470                true
1471            }
1472            // A rule the endpoint raises rather than the decoder. The two
1473            // reach their codes through different tables and mean the same
1474            // thing here: `Some` is a rule this draft ends the session over.
1475            ConnectionError::Endpoint(inner) => {
1476                let Some(code) = inner.session_error_code() else { return false };
1477                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1478                self.close(wire_code, inner.to_string().as_bytes());
1479                true
1480            }
1481            _ => false,
1482        }
1483    }
1484
1485    /// Close the connection.
1486    pub fn close(&self, code: u32, reason: &[u8]) {
1487        self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
1488        self.transport.close(code, reason);
1489    }
1490}
1491
1492/// Determine the encoded length of a varint from its first byte.
1493fn varint_len(first_byte: u8) -> usize {
1494    1 << (first_byte >> 6)
1495}
1496
1497#[cfg(test)]
1498mod tests {
1499    use super::*;
1500
1501    #[test]
1502    fn client_config_supported_versions_default() {
1503        let config = ClientConfig {
1504            additional_versions: Vec::new(),
1505            transport: TransportType::Quic,
1506            skip_cert_verification: false,
1507            ca_certs: Vec::new(),
1508            setup_parameters: Vec::new(),
1509        };
1510        let versions = config.supported_versions();
1511        assert_eq!(versions.len(), 1);
1512        assert_eq!(versions[0].into_inner(), 0xff000000 + 7);
1513    }
1514
1515    #[test]
1516    fn client_config_alpn_quic() {
1517        let config = ClientConfig {
1518            additional_versions: Vec::new(),
1519            transport: TransportType::Quic,
1520            skip_cert_verification: false,
1521            ca_certs: Vec::new(),
1522            setup_parameters: Vec::new(),
1523        };
1524        assert_eq!(config.alpn(), vec![DraftVersion::Draft07.quic_alpn().to_vec()]);
1525    }
1526
1527    #[test]
1528    fn moqt_alpn_value() {
1529        assert_eq!(MOQT_ALPN, b"moq-00");
1530    }
1531}