Skip to main content

moqtap_client/draft13/
connection.rs

1use bytes::{Buf, Bytes, BytesMut};
2
3use crate::draft13::endpoint::{Endpoint, EndpointError};
4use crate::draft13::event::{ClientEvent, Direction, FetchObject, StreamKind, SubgroupObject};
5use crate::draft13::observer::ConnectionObserver;
6use crate::draft13::session::request_id::Role;
7use crate::draft13::session::setup;
8use crate::forwarding_preference::ObjectForwardingPreference;
9use crate::malformed_tracks::MalformedTrackCondition;
10use crate::track_locations::{EndOfTrackForm, ObjectLocation, ObjectRole, TrackObjects};
11use crate::transport::{RecvStream, SendStream, Transport, TransportError};
12use moqtap_codec::dispatch::{
13    AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
14};
15use moqtap_codec::draft13::data_stream::{FetchObjectHeader, ObjectHeader};
16use moqtap_codec::draft13::message::ControlMessage;
17use moqtap_codec::error::CodecError;
18use moqtap_codec::kvp::KeyValuePair;
19use moqtap_codec::types::*;
20use moqtap_codec::varint::VarInt;
21use moqtap_codec::version::DraftVersion;
22
23/// MoQT ALPN identifier (used by raw QUIC transport).
24pub const MOQT_ALPN: &[u8] = b"moq-00";
25
26/// Errors from the draft-13 connection layer.
27#[derive(Debug, thiserror::Error)]
28pub enum ConnectionError {
29    /// Endpoint state machine error.
30    #[error("endpoint error: {0}")]
31    Endpoint(#[from] EndpointError),
32    /// Wire codec error.
33    #[error("codec error: {0}")]
34    Codec(#[from] CodecError),
35    /// Transport-level error.
36    #[error("transport error: {0}")]
37    Transport(#[from] TransportError),
38    /// Variable-length integer decoding error.
39    #[error("varint error: {0}")]
40    VarInt(#[from] moqtap_codec::varint::VarIntError),
41    /// Control stream was not opened.
42    #[error("control stream not open")]
43    NoControlStream,
44    /// Stream ended before a complete message was read.
45    #[error("unexpected end of stream")]
46    UnexpectedEnd,
47    /// Stream was finished by the peer.
48    #[error("stream finished")]
49    StreamFinished,
50    /// Invalid server address string.
51    #[error("invalid server address: {0}")]
52    InvalidAddress(String),
53    /// TLS configuration error.
54    #[error("TLS config error: {0}")]
55    TlsConfig(String),
56    /// Data stream used out of order: an object before its header, or an
57    /// Object ID that does not advance on the last one written.
58    #[error("data stream state error: {0}")]
59    DataStreamState(&'static str),
60}
61
62impl From<crate::transport::DialError> for ConnectionError {
63    /// Preserves the variants this error had when the dial was inlined here,
64    /// so a caller matching on `InvalidAddress` or `TlsConfig` sees no change.
65    fn from(e: crate::transport::DialError) -> Self {
66        match e {
67            crate::transport::DialError::InvalidAddress(s) => ConnectionError::InvalidAddress(s),
68            crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
69            crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
70        }
71    }
72}
73
74/// Transport type for the connection.
75#[derive(Debug, Clone)]
76pub enum TransportType {
77    /// Raw QUIC via quinn. The `addr` field should be `host:port`.
78    Quic,
79    /// WebTransport via wtransport. The `url` field is the WebTransport URL.
80    WebTransport {
81        /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
82        url: String,
83    },
84}
85
86/// Configuration for a draft-13 MoQT client connection.
87pub struct ClientConfig {
88    /// Additional draft versions to offer in CLIENT_SETUP (draft-13 is always
89    /// offered first).
90    pub additional_versions: Vec<DraftVersion>,
91    /// The transport type (QUIC or WebTransport).
92    pub transport: TransportType,
93    /// Whether to skip TLS certificate verification (for testing).
94    pub skip_cert_verification: bool,
95    /// Custom CA certificates to trust (DER-encoded).
96    pub ca_certs: Vec<Vec<u8>>,
97    /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
98    pub setup_parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
99}
100
101impl ClientConfig {
102    /// Returns the MoQT version varints for the CLIENT_SETUP message.
103    /// Draft-12 first, then any additional versions.
104    pub fn supported_versions(&self) -> Vec<VarInt> {
105        let mut versions = vec![DraftVersion::Draft13.version_varint()];
106        for v in &self.additional_versions {
107            let varint = v.version_varint();
108            if !versions.contains(&varint) {
109                versions.push(varint);
110            }
111        }
112        versions
113    }
114
115    /// Returns the ALPN protocol identifiers for the transport.
116    pub fn alpn(&self) -> Vec<Vec<u8>> {
117        match &self.transport {
118            TransportType::Quic => vec![DraftVersion::Draft13.quic_alpn().to_vec()],
119            TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
120        }
121    }
122}
123
124/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
125pub struct FramedSendStream {
126    inner: SendStream,
127    /// What the subgroup header opened on this stream settled, once one has
128    /// been written.
129    ///
130    /// `None` until a subgroup header has been written, which is what makes an
131    /// object sent before its header answerable rather than unframed bytes.
132    subgroup_objects: Option<SubgroupStreamState>,
133}
134
135/// What a subgroup header fixes for every object written after it.
136///
137/// Two facts, and neither is recoverable from an object on its own: the
138/// Object ID it has to advance past, and whether the stream's type puts an
139/// extension block on each object. The second is the one that used to be
140/// guessed - always "absent" - which wrote a stream a reader could not follow
141/// whenever the header said otherwise.
142#[derive(Debug, Clone, Copy)]
143struct SubgroupStreamState {
144    /// The last Object ID written, or `None` before the first object.
145    previous_object_id: Option<u64>,
146    /// Whether each object writes an Extension Headers Length field.
147    carries_extension_block: bool,
148}
149
150impl FramedSendStream {
151    /// Create a new framed send stream.
152    pub fn new(inner: SendStream) -> Self {
153        Self { inner, subgroup_objects: None }
154    }
155
156    /// Get the transport-level stream ID.
157    pub fn stream_id(&self) -> u64 {
158        self.inner.stream_id()
159    }
160
161    /// Write a control message to the stream with type+length framing.
162    /// Returns the raw bytes that were written (for event capture).
163    pub async fn write_control(
164        &mut self,
165        msg: &AnyControlMessage,
166    ) -> Result<Vec<u8>, ConnectionError> {
167        let mut buf = Vec::new();
168        msg.encode(&mut buf)?;
169        self.inner.write_all(&buf).await?;
170        Ok(buf)
171    }
172
173    /// Write a subgroup stream header. Also opens the Object ID bookkeeping
174    /// [`FramedSendStream::write_subgroup_object`] holds the stream to.
175    ///
176    /// The header is refused, and nothing is written, if its fields disagree
177    /// with its own stream type. That check has to happen here rather than at
178    /// the first object: the type is what every object after it is framed
179    /// against, so a header that went out saying the wrong thing cannot be
180    /// taken back.
181    pub async fn write_subgroup_header(
182        &mut self,
183        header: &AnySubgroupHeader,
184    ) -> Result<(), ConnectionError> {
185        let mut buf = Vec::new();
186        header.encode_stream_checked(&mut buf)?;
187        self.inner.write_all(&buf).await?;
188        // The framing is read off the header now, while the header is in hand.
189        // An object arriving later cannot say whether the stream carries an
190        // extension block, and writing one either way is not a recoverable
191        // mistake: the reader takes the next field along as the missing length
192        // and misframes every object after it.
193        self.subgroup_objects = Some(SubgroupStreamState {
194            previous_object_id: None,
195            carries_extension_block: header.carries_extension_block(),
196        });
197        Ok(())
198    }
199
200    /// Write a fetch response header.
201    pub async fn write_fetch_header(
202        &mut self,
203        header: &AnyFetchHeader,
204    ) -> Result<(), ConnectionError> {
205        let mut buf = Vec::new();
206        header.encode_stream(&mut buf);
207        self.inner.write_all(&buf).await?;
208        Ok(())
209    }
210
211    /// Append a draft-13 subgroup object (header + payload) to the stream.
212    ///
213    /// Section 9.4.2: "A publisher MUST NOT send an Object on a stream if its
214    /// Object ID is less than a previously sent Object ID within a given group
215    /// in that stream." A subgroup stream carries one group, so the Object IDs
216    /// written here are exactly the ones that sentence compares, and the
217    /// comparison needs the object before - which no per-header check can see.
218    /// The state advances only once the object has been written, so declining to
219    /// write an object leaves the next one measured against the last one kept.
220    ///
221    /// An equal Object ID is refused as well as a smaller one. The draft's own
222    /// sentence forbids only "less than", but an Object ID names an Object
223    /// within a Group: writing one twice on a stream describes the same Object
224    /// with two different payloads, and a reader has no way to choose. The
225    /// dispatch-level writer in the codec draws the line in the same place, and
226    /// two writers that disagreed about it would be worse than either answer.
227    ///
228    /// # Errors
229    ///
230    /// [`ConnectionError::DataStreamState`] if no subgroup header has been
231    /// written yet, or if `object` does not advance past the last one written.
232    pub async fn write_subgroup_object(
233        &mut self,
234        object: &SubgroupObject,
235    ) -> Result<(), ConnectionError> {
236        let state = self
237            .subgroup_objects
238            .as_mut()
239            .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
240        let object_id = object.header.object_id.into_inner();
241        if matches!(state.previous_object_id, Some(prev) if object_id <= prev) {
242            return Err(ConnectionError::DataStreamState(
243                "object id does not advance on the last one written to this stream",
244            ));
245        }
246        // The declared length comes from the payload rather than from the
247        // caller's field: a header that disagrees with the bytes beside it
248        // desynchronises every object after it on the stream, and nothing
249        // downstream can recover.
250        let mut header = object.header.clone();
251        header.payload_length = VarInt::from_usize(object.payload.len());
252        let mut buf = Vec::new();
253        header.encode_checked_with_extensions(state.carries_extension_block, &mut buf)?;
254        buf.extend_from_slice(&object.payload);
255        self.inner.write_all(&buf).await?;
256        state.previous_object_id = Some(object_id);
257        Ok(())
258    }
259
260    /// Append a draft-13 fetch object (header + payload) to the stream.
261    pub async fn write_fetch_object(
262        &mut self,
263        object: &FetchObject,
264    ) -> Result<(), ConnectionError> {
265        // The declared length comes from the payload rather than from the
266        // caller's field: a header that disagrees with the bytes beside it
267        // desynchronises every object after it on the stream, and nothing
268        // downstream can recover.
269        let mut header = object.header.clone();
270        header.payload_length = VarInt::from_usize(object.payload.len());
271        let mut buf = Vec::new();
272        header.encode_checked(&mut buf)?;
273        buf.extend_from_slice(&object.payload);
274        self.inner.write_all(&buf).await?;
275        Ok(())
276    }
277
278    /// Finish the stream (send FIN).
279    pub async fn finish(&mut self) -> Result<(), ConnectionError> {
280        self.inner.finish()?;
281        Ok(())
282    }
283}
284
285/// What an Object Status makes of an object, for Section 9.2.1.1's table.
286///
287/// `None` is Normal and not "no status": a datagram carrying a payload has no
288/// status field at all, and an object with a payload is an ordinary object.
289fn object_role(status: Option<u64>) -> ObjectRole {
290    match status {
291        None | Some(0x0) => ObjectRole::Produced,
292        // 0x4, end of Track. This draft folded drafts 08 through 10's second
293        // end-of-track status into this one and kept the looser of the two
294        // conditions, so the Group ID may equal the largest group seen.
295        Some(0x4) => ObjectRole::EndsTrack(Some(EndOfTrackForm::LastGroup)),
296        // Every other status is a statement about objects rather than one of
297        // them. Two of them name an Object ID one past the largest on purpose,
298        // so counting one as produced would refuse the end-of-track object the
299        // draft goes on to define.
300        _ => ObjectRole::Neither,
301    }
302}
303
304/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
305pub struct FramedRecvStream {
306    inner: RecvStream,
307    buf: BytesMut,
308    /// The record this stream's objects are measured against, and the Group ID
309    /// its header named.
310    ///
311    /// One group for the whole stream: a subgroup header names it once and no
312    /// object header repeats it. `None` on a stream that was never given one —
313    /// a stream for an alias no live binding names, and every stream built
314    /// outside [`Connection::accept_subgroup_stream`] — and such a stream reads
315    /// exactly as it did before this existed.
316    tracking: Option<(TrackObjects, u64)>,
317    /// Whether the subgroup stream this reader is on carries an extension block
318    /// on every object.
319    ///
320    /// The stream's Type says so and nothing in an object header repeats it, so
321    /// a reader that does not remember the Type cannot parse the objects at all:
322    /// on an extensions-bearing stream it reads the Extension Headers Length as
323    /// the Object Payload Length and every field after it is nonsense.
324    ///
325    /// Set by [`FramedRecvStream::read_subgroup_header`] and read by
326    /// [`FramedRecvStream::read_subgroup_object`]. `false` until a subgroup
327    /// header has been read, which is the only order those two may be called
328    /// in.
329    subgroup_has_extensions: bool,
330}
331
332impl FramedRecvStream {
333    /// Create a new framed receive stream.
334    pub fn new(inner: RecvStream) -> Self {
335        Self {
336            inner,
337            buf: BytesMut::with_capacity(4096),
338            subgroup_has_extensions: false,
339            tracking: None,
340        }
341    }
342
343    /// Measure this stream's objects against `objects`, all of them in `group`.
344    ///
345    /// Called by [`Connection::accept_subgroup_stream`] once the header has been
346    /// read, which is the only point at which both the track and the group are
347    /// known.
348    fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
349        self.tracking = Some((objects, group));
350    }
351
352    /// Record or judge one object this stream carried.
353    ///
354    /// The whole of Section 9.2.1.1's rule that a single header cannot settle: the
355    /// object's Group ID is the stream's, its Object ID is its own, and what
356    /// they are measured against is everything the track has carried on any
357    /// stream.
358    fn note_subgroup_object(&self, header: &ObjectHeader) -> Result<(), ConnectionError> {
359        let Some((objects, group)) = &self.tracking else { return Ok(()) };
360        let at = ObjectLocation { group: *group, object: header.object_id.into_inner() };
361        objects.note_with_final_object(at, object_role(Some(header.object_status as u64))).map_err(
362            |fault| {
363                ConnectionError::Endpoint(EndpointError::for_track_fault(
364                    objects.alias(),
365                    at,
366                    fault,
367                ))
368            },
369        )
370    }
371
372    /// Get the transport-level stream ID.
373    pub fn stream_id(&self) -> u64 {
374        self.inner.stream_id()
375    }
376
377    /// Read more data from the stream into the internal buffer.
378    async fn fill(&mut self) -> Result<bool, ConnectionError> {
379        let mut tmp = [0u8; 4096];
380        match self.inner.read(&mut tmp).await {
381            Ok(Some(n)) => {
382                self.buf.extend_from_slice(&tmp[..n]);
383                Ok(true)
384            }
385            Ok(None) => Ok(false),
386            Err(e) => Err(ConnectionError::Transport(e)),
387        }
388    }
389
390    /// Ensure at least `n` bytes are available in the buffer.
391    async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
392        while self.buf.len() < n {
393            if !self.fill().await? {
394                return Err(ConnectionError::UnexpectedEnd);
395            }
396        }
397        Ok(())
398    }
399
400    /// Read a control message from the stream.
401    ///
402    /// When `capture_raw` is true, the returned tuple includes a clone of the
403    /// framed wire bytes (for observer emission). When false, the second
404    /// element is `None` and the payload clone is skipped.
405    pub async fn read_control(
406        &mut self,
407        capture_raw: bool,
408    ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
409        // Read type ID varint
410        self.ensure(1).await?;
411        let type_len = varint_len(self.buf[0]);
412        self.ensure(type_len).await?;
413
414        let mut cursor = &self.buf[..type_len];
415        let _type_id = VarInt::decode(&mut cursor)?;
416
417        // Draft-12 uses a fixed 16-bit length after the type id.
418        let len_field_size = 2;
419        self.ensure(type_len + len_field_size).await?;
420        let payload_len = u16::from_be_bytes([self.buf[type_len], self.buf[type_len + 1]]) as usize;
421
422        // Read full payload
423        let total = type_len + len_field_size + payload_len;
424        self.ensure(total).await?;
425
426        // Capture raw bytes only if requested (observer attached).
427        let raw = capture_raw.then(|| self.buf[..total].to_vec());
428
429        // Now decode the whole message using the draft-13 dispatcher
430        let mut frame = &self.buf[..total];
431        let msg = AnyControlMessage::decode(DraftVersion::Draft13, &mut frame)?;
432        self.buf.advance(total);
433        Ok((msg, raw))
434    }
435
436    /// Read a subgroup stream header.
437    pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
438        self.ensure(1).await?;
439        loop {
440            let mut cursor = &self.buf[..];
441            match AnySubgroupHeader::decode_stream(DraftVersion::Draft13, &mut cursor) {
442                Ok(header) => {
443                    let consumed = self.buf.len() - cursor.remaining();
444                    self.buf.advance(consumed);
445                    // Clippy would rather see these two arms as an `if let`, and rustc rejects
446                    // that in a single-draft build, where the pattern is irrefutable. Only a
447                    // `match` satisfies both.
448                    #[allow(clippy::single_match)]
449                    match header {
450                        AnySubgroupHeader::Draft13(ref d) => {
451                            self.subgroup_has_extensions = d.stream_type.has_extensions();
452                        }
453                        // Only this draft's header sets the flag. With draft 13 the only enabled
454                        // draft `AnySubgroupHeader` has a single variant, the arm above is
455                        // exhaustive and this one unreachable. Compiled in every configuration with
456                        // the lint allowed, rather than gated on a `cfg` naming the other thirteen
457                        // drafts: that list had to be edited in every draft module whenever a draft
458                        // was added, and a copy that omitted one left this match non-exhaustive.
459                        #[allow(unreachable_patterns)]
460                        _ => {}
461                    }
462                    return Ok(header);
463                }
464                Err(CodecError::UnexpectedEnd) => {
465                    if !self.fill().await? {
466                        return Err(ConnectionError::UnexpectedEnd);
467                    }
468                }
469                Err(e) => return Err(ConnectionError::Codec(e)),
470            }
471        }
472    }
473
474    /// Read a fetch response header.
475    pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
476        self.ensure(1).await?;
477        loop {
478            let mut cursor = &self.buf[..];
479            match AnyFetchHeader::decode_stream(DraftVersion::Draft13, &mut cursor) {
480                Ok(header) => {
481                    let consumed = self.buf.len() - cursor.remaining();
482                    self.buf.advance(consumed);
483                    return Ok(header);
484                }
485                Err(CodecError::UnexpectedEnd) => {
486                    if !self.fill().await? {
487                        return Err(ConnectionError::UnexpectedEnd);
488                    }
489                }
490                Err(e) => return Err(ConnectionError::Codec(e)),
491            }
492        }
493    }
494
495    /// Read the next draft-13 subgroup object (header + payload).
496    ///
497    /// Whether an object carries an extension block is a property of the
498    /// stream's Type, not of the object, so this reads it from the header
499    /// [`FramedRecvStream::read_subgroup_header`] recorded rather than assuming
500    /// either answer. Assuming "no extensions" is not a conservative default: on
501    /// a stream whose Type announces them the Extension Headers Length is read
502    /// as the Object Payload Length, and every object on the stream comes back
503    /// wrong instead of being refused.
504    ///
505    /// It is also what puts the rule binding extension headers to Object Status
506    /// within reach on a subgroup stream. A reader that never reads the block
507    /// cannot notice that a non-existent Object carried one.
508    pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
509        loop {
510            let mut cursor = &self.buf[..];
511            match ObjectHeader::decode_with_extensions(self.subgroup_has_extensions, &mut cursor) {
512                Ok(header) => {
513                    let header_consumed = self.buf.len() - cursor.remaining();
514                    let payload_len = header.payload_length.into_inner() as usize;
515                    let total = header_consumed + payload_len;
516                    if self.buf.len() < total {
517                        if !self.fill().await? {
518                            return Err(ConnectionError::UnexpectedEnd);
519                        }
520                        continue;
521                    }
522                    let payload = self.buf[header_consumed..total].to_vec();
523                    self.buf.advance(total);
524                    self.note_subgroup_object(&header)?;
525                    return Ok(SubgroupObject { header, payload });
526                }
527                Err(CodecError::UnexpectedEnd) => {
528                    if !self.fill().await? {
529                        return Err(ConnectionError::UnexpectedEnd);
530                    }
531                }
532                Err(e) => return Err(ConnectionError::Codec(e)),
533            }
534        }
535    }
536
537    /// Read the next draft-13 fetch object (header + payload).
538    pub async fn read_fetch_object(&mut self) -> Result<FetchObject, ConnectionError> {
539        loop {
540            let mut cursor = &self.buf[..];
541            match FetchObjectHeader::decode(&mut cursor) {
542                Ok(header) => {
543                    let header_consumed = self.buf.len() - cursor.remaining();
544                    let payload_len = header.payload_length.into_inner() as usize;
545                    let total = header_consumed + payload_len;
546                    if self.buf.len() < total {
547                        if !self.fill().await? {
548                            return Err(ConnectionError::UnexpectedEnd);
549                        }
550                        continue;
551                    }
552                    let payload = self.buf[header_consumed..total].to_vec();
553                    self.buf.advance(total);
554                    return Ok(FetchObject { header, payload });
555                }
556                Err(CodecError::UnexpectedEnd) => {
557                    if !self.fill().await? {
558                        return Err(ConnectionError::UnexpectedEnd);
559                    }
560                }
561                Err(e) => return Err(ConnectionError::Codec(e)),
562            }
563        }
564    }
565}
566
567/// A live draft-13 MoQT connection over QUIC or WebTransport.
568pub struct Connection {
569    transport: Transport,
570    endpoint: Endpoint,
571    /// Behind a lock because a control message is written from two kinds of
572    /// place. Most of them are the caller's own request, made through `&mut
573    /// self`. The UNSUBSCRIBE that answers a Malformed Track is not: the
574    /// conditions that make a track malformed are detected on the data plane,
575    /// where this connection is reached through a shared reference. The lock
576    /// also makes one message the unit of writing, so two of them cannot
577    /// interleave on the stream.
578    control_send: Option<tokio::sync::Mutex<FramedSendStream>>,
579    control_recv: Option<FramedRecvStream>,
580    observer: Option<Box<dyn ConnectionObserver>>,
581    /// Setup events buffered during `connect()` and replayed when an
582    /// observer attaches via `set_observer` — without this, an observer
583    /// attached after `connect` returns would never see the handshake.
584    pending_events: Vec<ClientEvent>,
585}
586
587impl Connection {
588    /// Connect to a draft-13 MoQT server as a client.
589    pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
590        // PATH is for native QUIC only, and the transport is known here and
591        // nowhere further in. Refusing before dialling means a session that
592        // the server would close on sight is never opened.
593        setup::validate_client_path_transport(
594            &config.setup_parameters,
595            matches!(config.transport, TransportType::WebTransport { .. }),
596        )
597        .map_err(EndpointError::from)?;
598
599        let transport = match &config.transport {
600            TransportType::Quic => Self::connect_quic(addr, &config).await?,
601            TransportType::WebTransport { url } => {
602                let url = url.clone();
603                Self::connect_webtransport(&url, &config).await?
604            }
605        };
606
607        Self::adopt(transport, config).await
608    }
609
610    /// Run the MoQT setup handshake over a transport somebody else established.
611    ///
612    /// For choosing the draft from what the server selected: dial once through
613    /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
614    /// connection to the module its answer names. [`Self::connect`] cannot do
615    /// this — it derives its single ALPN from the draft it was given.
616    ///
617    /// `config.draft` must match this module. The transport is adopted as
618    /// given; nothing here re-checks the ALPN it was negotiated with.
619    pub async fn adopt(
620        transport: Transport,
621        config: ClientConfig,
622    ) -> Result<Self, ConnectionError> {
623        // PATH is for native QUIC only, and the transport is known here and
624        // nowhere further in. Refusing before dialling means a session that
625        // the server would close on sight is never opened.
626        setup::validate_client_path_transport(
627            &config.setup_parameters,
628            matches!(config.transport, TransportType::WebTransport { .. }),
629        )
630        .map_err(EndpointError::from)?;
631
632        // Open bidirectional control stream
633        let (send, recv) = transport.open_bi().await?;
634        let mut control_send = FramedSendStream::new(send);
635        let mut control_recv = FramedRecvStream::new(recv);
636
637        // Perform setup handshake
638        let mut endpoint = Endpoint::new(Role::Client);
639        endpoint.connect()?;
640        let setup_msg = endpoint
641            .send_client_setup(config.supported_versions(), config.setup_parameters.clone())?;
642        let any_setup = AnyControlMessage::Draft13(setup_msg);
643        let raw_setup = control_send.write_control(&any_setup).await?;
644
645        let (server_setup, raw_server_setup) = control_recv.read_control(true).await?;
646        match &server_setup {
647            AnyControlMessage::Draft13(ControlMessage::ServerSetup(ref ss)) => {
648                endpoint.receive_server_setup(ss)?;
649            }
650            _ => {
651                return Err(ConnectionError::Endpoint(EndpointError::NotActive));
652            }
653        }
654
655        let mut pending_events = Vec::with_capacity(3);
656        pending_events.push(ClientEvent::ControlMessage {
657            direction: Direction::Send,
658            message: any_setup,
659            raw: Some(raw_setup),
660        });
661        pending_events.push(ClientEvent::ControlMessage {
662            direction: Direction::Receive,
663            message: server_setup,
664            raw: raw_server_setup,
665        });
666        if let Some(v) = endpoint.negotiated_version() {
667            pending_events.push(ClientEvent::SetupComplete { negotiated_version: v.into_inner() });
668        }
669
670        Ok(Self {
671            transport,
672            endpoint,
673            control_send: Some(tokio::sync::Mutex::new(control_send)),
674            control_recv: Some(control_recv),
675            observer: None,
676            pending_events,
677        })
678    }
679
680    /// Establish a raw QUIC connection.
681    ///
682    /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
683    /// TLS and endpoint setup.
684    async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
685        let (transport, _negotiated) = crate::transport::dial_quic(
686            addr,
687            &crate::transport::QuicDialOptions {
688                skip_cert_verification: config.skip_cert_verification,
689                ca_certs: config.ca_certs.clone(),
690                alpn: config.alpn(),
691            },
692        )
693        .await?;
694        Ok(transport)
695    }
696
697    /// Establish a WebTransport connection.
698    #[cfg(feature = "webtransport")]
699    async fn connect_webtransport(
700        url: &str,
701        config: &ClientConfig,
702    ) -> Result<Transport, ConnectionError> {
703        use crate::transport::webtransport::WebTransportTransport;
704
705        let wt_config = if config.skip_cert_verification {
706            wtransport::ClientConfig::builder()
707                .with_bind_default()
708                .with_no_cert_validation()
709                .build()
710        } else {
711            wtransport::ClientConfig::builder().with_bind_default().with_native_certs().build()
712        };
713
714        let endpoint = wtransport::Endpoint::client(wt_config)
715            .map_err(|e| ConnectionError::Transport(TransportError::Connect(e.to_string())))?;
716
717        let connection = endpoint
718            .connect(url)
719            .await
720            .map_err(|e| ConnectionError::Transport(TransportError::Connect(e.to_string())))?;
721
722        Ok(Transport::WebTransport(WebTransportTransport::new(connection)))
723    }
724
725    /// Stub for when the webtransport feature is not enabled.
726    #[cfg(not(feature = "webtransport"))]
727    async fn connect_webtransport(
728        _url: &str,
729        _config: &ClientConfig,
730    ) -> Result<Transport, ConnectionError> {
731        Err(ConnectionError::Transport(TransportError::Connect(
732            "webtransport feature not enabled".into(),
733        )))
734    }
735
736    // ── Observer ───────────────────────────────────────────────
737
738    /// Attach an observer. Buffered handshake events from `connect()` are
739    /// flushed in arrival order before this returns.
740    pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
741        self.observer = Some(observer);
742        for event in self.pending_events.drain(..) {
743            if let Some(ref obs) = self.observer {
744                obs.on_event_owned(event);
745            }
746        }
747    }
748
749    /// Remove the observer.
750    pub fn clear_observer(&mut self) {
751        self.observer = None;
752    }
753
754    /// Emit an event to the observer, if one is attached.
755    fn emit(&self, event: ClientEvent) {
756        if let Some(ref obs) = self.observer {
757            obs.on_event_owned(event);
758        }
759    }
760
761    // ── Control message I/O ─────────────────────────────────
762
763    /// Send a control message on the control stream.
764    pub async fn send_control(&self, msg: &ControlMessage) -> Result<(), ConnectionError> {
765        let any = AnyControlMessage::Draft13(msg.clone());
766        let mut send =
767            self.control_send.as_ref().ok_or(ConnectionError::NoControlStream)?.lock().await;
768        let raw = send.write_control(&any).await?;
769        drop(send);
770        self.emit(ClientEvent::ControlMessage {
771            direction: Direction::Send,
772            message: any,
773            raw: Some(raw),
774        });
775        Ok(())
776    }
777
778    /// Read the next control message from the control stream.
779    pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
780        let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
781        let capture_raw = self.observer.is_some();
782        let (any, raw) = match recv.read_control(capture_raw).await {
783            Ok(v) => v,
784            Err(e) => return Err(self.close_for_codec(e)),
785        };
786        if capture_raw {
787            self.emit(ClientEvent::ControlMessage {
788                direction: Direction::Receive,
789                message: any.clone(),
790                raw,
791            });
792        }
793        match any {
794            AnyControlMessage::Draft13(msg) => Ok(msg),
795            // `AnyControlMessage` carries one variant per enabled draft feature. With draft 13 the
796            // only one enabled the arm above is exhaustive and this rejection arm unreachable.
797            // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
798            // naming the other thirteen drafts: that list had to be edited in every draft module
799            // whenever a draft was added, and a copy that omitted one left this match
800            // non-exhaustive.
801            #[allow(unreachable_patterns)]
802            _ => Err(ConnectionError::Codec(CodecError::UnknownMessageType(0))),
803        }
804    }
805
806    /// Read and dispatch the next incoming control message through the endpoint
807    /// state machine. Returns the decoded message for inspection.
808    pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
809        let msg = self.recv_control().await?;
810        self.endpoint.receive_message(msg.clone()).map_err(|e| self.close_if_session_fatal(e))?;
811
812        if let ControlMessage::GoAway(ref ga) = msg {
813            self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
814        }
815
816        Ok(msg)
817    }
818
819    // ── Subscribe flow ──────────────────────────────────────
820
821    /// Send a SUBSCRIBE and return the allocated request ID.
822    ///
823    /// Draft-12: the subscriber no longer chooses a track alias. The alias is
824    /// returned by the publisher in SUBSCRIBE_OK and can be retrieved later
825    /// from the endpoint via `endpoint().track_alias_for(request_id)`.
826    pub async fn subscribe(
827        &mut self,
828        track_namespace: TrackNamespace,
829        track_name: Vec<u8>,
830        subscriber_priority: u8,
831        group_order: GroupOrder,
832        filter_type: FilterType,
833        parameters: Vec<KeyValuePair>,
834    ) -> Result<VarInt, ConnectionError> {
835        let (req_id, msg) = self.endpoint.subscribe(
836            track_namespace,
837            track_name,
838            subscriber_priority,
839            group_order,
840            filter_type,
841            parameters,
842        )?;
843        self.send_control(&msg).await?;
844        Ok(req_id)
845    }
846
847    /// Send a SUBSCRIBE for a range of the track, starting at a given
848    /// location.
849    ///
850    /// The Filter Type is derived from the range, so the message cannot name a
851    /// filter whose fields it does not carry.
852    #[allow(clippy::too_many_arguments)]
853    pub async fn subscribe_range(
854        &mut self,
855        track_namespace: TrackNamespace,
856        track_name: Vec<u8>,
857        subscriber_priority: u8,
858        group_order: GroupOrder,
859        start_location: Location,
860        end_group: Option<VarInt>,
861        parameters: Vec<KeyValuePair>,
862    ) -> Result<VarInt, ConnectionError> {
863        let (req_id, msg) = self.endpoint.subscribe_range(
864            track_namespace,
865            track_name,
866            subscriber_priority,
867            group_order,
868            start_location,
869            end_group,
870            parameters,
871        )?;
872        self.send_control(&msg).await?;
873        Ok(req_id)
874    }
875
876    /// Send an UNSUBSCRIBE for the given request ID.
877    pub async fn unsubscribe(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
878        let msg = self.endpoint.unsubscribe(request_id)?;
879        self.send_control(&msg).await
880    }
881
882    /// Accept a subscription the peer opened, sending SUBSCRIBE_OK and giving
883    /// its track a Track Alias.
884    ///
885    /// The endpoint refuses an alias a live track of its own already holds and
886    /// refuses a second answer to one SUBSCRIBE, so nothing is written on the
887    /// wire when it does either.
888    pub async fn subscribe_ok(
889        &mut self,
890        request_id: VarInt,
891        track_alias: VarInt,
892        expires: VarInt,
893        group_order: GroupOrder,
894        parameters: Vec<KeyValuePair>,
895    ) -> Result<(), ConnectionError> {
896        let msg = self.endpoint.send_subscribe_ok(
897            request_id,
898            track_alias,
899            expires,
900            group_order,
901            parameters,
902        )?;
903        self.send_control(&msg).await
904    }
905
906    /// Reject a subscription the peer opened, sending SUBSCRIBE_ERROR.
907    ///
908    /// The endpoint refuses a second answer to one SUBSCRIBE, so nothing is
909    /// written on the wire when it does.
910    pub async fn subscribe_error(
911        &mut self,
912        request_id: VarInt,
913        error_code: VarInt,
914        reason_phrase: Vec<u8>,
915    ) -> Result<(), ConnectionError> {
916        let msg = self.endpoint.send_subscribe_error(request_id, error_code, reason_phrase)?;
917        self.send_control(&msg).await
918    }
919
920    /// End a subscription this endpoint accepted, sending SUBSCRIBE_DONE.
921    pub async fn subscribe_done(
922        &mut self,
923        request_id: VarInt,
924        status_code: VarInt,
925        stream_count: VarInt,
926        reason_phrase: Vec<u8>,
927    ) -> Result<(), ConnectionError> {
928        let msg = self.endpoint.send_subscribe_done(
929            request_id,
930            status_code,
931            stream_count,
932            reason_phrase,
933        )?;
934        self.send_control(&msg).await
935    }
936
937    /// Narrow a subscription this endpoint opened, sending SUBSCRIBE_UPDATE.
938    ///
939    /// No Request ID is spent: on this draft the message's one identifier
940    /// field names the subscription it modifies.
941    #[allow(clippy::too_many_arguments)]
942    pub async fn subscribe_update(
943        &mut self,
944        request_id: VarInt,
945        start_group: VarInt,
946        start_object: VarInt,
947        end_group: VarInt,
948        subscriber_priority: u8,
949        forward: Forward,
950        parameters: Vec<KeyValuePair>,
951    ) -> Result<(), ConnectionError> {
952        let msg = self.endpoint.subscribe_update(
953            request_id,
954            start_group,
955            start_object,
956            end_group,
957            subscriber_priority,
958            forward,
959            parameters,
960        )?;
961        self.send_control(&msg).await
962    }
963
964    /// Offer the peer a subscription to a track this endpoint publishes, and
965    /// return the Request ID the offer was allocated.
966    ///
967    /// Nothing is written on the wire when the endpoint refuses to build the
968    /// offer, which it does when the Track Alias is one another live track of
969    /// this session already holds.
970    #[allow(clippy::too_many_arguments)]
971    pub async fn publish(
972        &mut self,
973        track_namespace: TrackNamespace,
974        track_name: Vec<u8>,
975        track_alias: VarInt,
976        group_order: GroupOrder,
977        largest_location: Option<Location>,
978        forward: Forward,
979        parameters: Vec<KeyValuePair>,
980    ) -> Result<VarInt, ConnectionError> {
981        let (req_id, msg) = self.endpoint.publish(
982            track_namespace,
983            track_name,
984            track_alias,
985            group_order,
986            largest_location,
987            forward,
988            parameters,
989        )?;
990        self.send_control(&msg).await?;
991        Ok(req_id)
992    }
993
994    /// Accept a PUBLISH the peer sent, which establishes the subscription it
995    /// opened.
996    ///
997    /// The endpoint refuses a second answer to one PUBLISH, so nothing is
998    /// written on the wire when it does.
999    #[allow(clippy::too_many_arguments)]
1000    pub async fn publish_ok(
1001        &mut self,
1002        request_id: VarInt,
1003        forward: Forward,
1004        subscriber_priority: u8,
1005        group_order: GroupOrder,
1006        filter_type: FilterType,
1007        start_group: Option<VarInt>,
1008        start_object: Option<VarInt>,
1009        end_group: Option<VarInt>,
1010    ) -> Result<(), ConnectionError> {
1011        let msg = self.endpoint.send_publish_ok(
1012            request_id,
1013            forward,
1014            subscriber_priority,
1015            group_order,
1016            filter_type,
1017            start_group,
1018            start_object,
1019            end_group,
1020        )?;
1021        self.send_control(&msg).await
1022    }
1023
1024    /// Reject a PUBLISH the peer sent, which ends the subscription it opened
1025    /// before it was established.
1026    ///
1027    /// The endpoint refuses a second answer to one PUBLISH, so nothing is
1028    /// written on the wire when it does.
1029    pub async fn publish_error(
1030        &mut self,
1031        request_id: VarInt,
1032        error_code: VarInt,
1033        reason_phrase: Vec<u8>,
1034    ) -> Result<(), ConnectionError> {
1035        let msg = self.endpoint.send_publish_error(request_id, error_code, reason_phrase)?;
1036        self.send_control(&msg).await
1037    }
1038
1039    // ── Fetch flow ──────────────────────────────────────────
1040
1041    /// Send a FETCH and return the allocated request ID.
1042    #[allow(clippy::too_many_arguments)]
1043    pub async fn fetch(
1044        &mut self,
1045        track_namespace: TrackNamespace,
1046        track_name: Vec<u8>,
1047        subscriber_priority: u8,
1048        group_order: GroupOrder,
1049        start_group: VarInt,
1050        start_object: VarInt,
1051        end_group: VarInt,
1052        end_object: VarInt,
1053        parameters: Vec<KeyValuePair>,
1054    ) -> Result<VarInt, ConnectionError> {
1055        let (req_id, msg) = self.endpoint.fetch(
1056            track_namespace,
1057            track_name,
1058            subscriber_priority,
1059            group_order,
1060            start_group,
1061            start_object,
1062            end_group,
1063            end_object,
1064            parameters,
1065        )?;
1066        self.send_control(&msg).await?;
1067        Ok(req_id)
1068    }
1069
1070    /// Send a Relative Joining Fetch and return the allocated request ID.
1071    ///
1072    /// `joining_start` counts groups back from the live edge of the
1073    /// subscription it names. Section 8.16.1 computes the range from "the
1074    /// Preceding Group Offset", which is this field under the name it carried
1075    /// two drafts ago; the field list calls it Joining Start and says that for
1076    /// a Relative Joining Fetch "a value of 0 indicates the Fetch starts at
1077    /// the beginning of the Current Group". For the form that names the group
1078    /// outright, see
1079    /// [`absolute_joining_fetch`](Self::absolute_joining_fetch).
1080    pub async fn joining_fetch(
1081        &mut self,
1082        subscriber_priority: u8,
1083        group_order: GroupOrder,
1084        joining_request_id: VarInt,
1085        joining_start: VarInt,
1086        parameters: Vec<KeyValuePair>,
1087    ) -> Result<VarInt, ConnectionError> {
1088        let (req_id, msg) = self.endpoint.joining_fetch(
1089            subscriber_priority,
1090            group_order,
1091            joining_request_id,
1092            joining_start,
1093            parameters,
1094        )?;
1095        self.send_control(&msg).await?;
1096        Ok(req_id)
1097    }
1098
1099    /// Send an Absolute Joining Fetch and return the allocated request ID.
1100    ///
1101    /// `joining_start` is the group to begin at. Section 8.16 has an Absolute
1102    /// Joining Fetch "Identical to a Relative Joining Fetch except that the
1103    /// Start Group is determined by an absolute Group value rather than a
1104    /// relative offset to the subscription", so a subscriber that knows the
1105    /// group it wants can ask for it without first being told a Largest
1106    /// Location to count back from.
1107    ///
1108    /// Two calls rather than one taking a Fetch Type, because these are the
1109    /// only two the joining form admits. The endpoint is named the same way,
1110    /// so no call between an application and the wire has a Fetch Type in it
1111    /// to be given a wrong one.
1112    pub async fn absolute_joining_fetch(
1113        &mut self,
1114        subscriber_priority: u8,
1115        group_order: GroupOrder,
1116        joining_request_id: VarInt,
1117        joining_start: VarInt,
1118        parameters: Vec<KeyValuePair>,
1119    ) -> Result<VarInt, ConnectionError> {
1120        let (req_id, msg) = self.endpoint.absolute_joining_fetch(
1121            subscriber_priority,
1122            group_order,
1123            joining_request_id,
1124            joining_start,
1125            parameters,
1126        )?;
1127        self.send_control(&msg).await?;
1128        Ok(req_id)
1129    }
1130
1131    /// Send a FETCH_CANCEL for the given request ID.
1132    pub async fn fetch_cancel(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1133        let msg = self.endpoint.fetch_cancel(request_id)?;
1134        self.send_control(&msg).await
1135    }
1136
1137    /// Accept a fetch the peer opened, sending FETCH_OK.
1138    ///
1139    /// The endpoint refuses a Joining Fetch naming a subscription this session
1140    /// cannot join and refuses a second answer to one FETCH, so nothing is
1141    /// written on the wire when it does either.
1142    pub async fn fetch_ok(
1143        &mut self,
1144        request_id: VarInt,
1145        group_order: GroupOrder,
1146        end_of_track: u8,
1147        end_location: Location,
1148        parameters: Vec<KeyValuePair>,
1149    ) -> Result<(), ConnectionError> {
1150        let msg = self.endpoint.send_fetch_ok(
1151            request_id,
1152            group_order,
1153            end_of_track,
1154            end_location,
1155            parameters,
1156        )?;
1157        self.send_control(&msg).await
1158    }
1159
1160    /// Refuse a fetch the peer opened, sending FETCH_ERROR.
1161    ///
1162    /// The endpoint refuses a second answer to one FETCH, and refuses a
1163    /// Joining Fetch's refusal under any code but the one the draft names for
1164    /// it, so nothing is written on the wire when it does either.
1165    pub async fn fetch_error(
1166        &mut self,
1167        request_id: VarInt,
1168        error_code: VarInt,
1169        reason_phrase: Vec<u8>,
1170    ) -> Result<(), ConnectionError> {
1171        let msg = self.endpoint.send_fetch_error(request_id, error_code, reason_phrase)?;
1172        self.send_control(&msg).await
1173    }
1174
1175    // ── Namespace flows ─────────────────────────────────────
1176
1177    /// Send a SUBSCRIBE_NAMESPACE. Returns the allocated request ID.
1178    pub async fn subscribe_namespace(
1179        &mut self,
1180        track_namespace_prefix: TrackNamespace,
1181        parameters: Vec<KeyValuePair>,
1182    ) -> Result<VarInt, ConnectionError> {
1183        let (req_id, msg) =
1184            self.endpoint.subscribe_namespace(track_namespace_prefix, parameters)?;
1185        self.send_control(&msg).await?;
1186        Ok(req_id)
1187    }
1188
1189    /// Accept a namespace subscription the peer made, sending SUBSCRIBE_NAMESPACE_OK.
1190    ///
1191    /// The endpoint refuses a second answer to one SUBSCRIBE_NAMESPACE, so nothing is
1192    /// written on the wire when it does.
1193    pub async fn subscribe_namespace_ok(
1194        &mut self,
1195        request_id: VarInt,
1196    ) -> Result<(), ConnectionError> {
1197        let msg = self.endpoint.send_subscribe_namespace_ok(request_id)?;
1198        self.send_control(&msg).await
1199    }
1200
1201    /// Refuse a namespace subscription the peer made, sending SUBSCRIBE_NAMESPACE_ERROR.
1202    ///
1203    /// The other half of the same sentence: one answer, and this is the other
1204    /// one it can be.
1205    pub async fn subscribe_namespace_error(
1206        &mut self,
1207        request_id: VarInt,
1208        error_code: VarInt,
1209        reason_phrase: Vec<u8>,
1210    ) -> Result<(), ConnectionError> {
1211        let msg =
1212            self.endpoint.send_subscribe_namespace_error(request_id, error_code, reason_phrase)?;
1213        self.send_control(&msg).await
1214    }
1215
1216    /// Send an ANNOUNCE. Returns the allocated request ID.
1217    pub async fn announce(
1218        &mut self,
1219        track_namespace: TrackNamespace,
1220        parameters: Vec<KeyValuePair>,
1221    ) -> Result<VarInt, ConnectionError> {
1222        let (req_id, msg) = self.endpoint.announce(track_namespace, parameters)?;
1223        self.send_control(&msg).await?;
1224        Ok(req_id)
1225    }
1226
1227    /// Send an UNANNOUNCE.
1228    pub async fn unannounce(
1229        &mut self,
1230        track_namespace: TrackNamespace,
1231    ) -> Result<(), ConnectionError> {
1232        let msg = self.endpoint.unannounce(track_namespace)?;
1233        self.send_control(&msg).await
1234    }
1235
1236    /// Accept an announcement the peer made, sending ANNOUNCE_OK.
1237    ///
1238    /// The endpoint refuses a second answer to one ANNOUNCE, so nothing is
1239    /// written on the wire when it does.
1240    pub async fn announce_ok(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1241        let msg = self.endpoint.send_announce_ok(request_id)?;
1242        self.send_control(&msg).await
1243    }
1244
1245    /// Refuse an announcement the peer made, sending ANNOUNCE_ERROR.
1246    ///
1247    /// The other half of the same sentence: one answer, and this is the other
1248    /// one it can be.
1249    pub async fn announce_error(
1250        &mut self,
1251        request_id: VarInt,
1252        error_code: VarInt,
1253        reason_phrase: Vec<u8>,
1254    ) -> Result<(), ConnectionError> {
1255        let msg = self.endpoint.send_announce_error(request_id, error_code, reason_phrase)?;
1256        self.send_control(&msg).await
1257    }
1258
1259    /// Revoke an acceptance, sending ANNOUNCE_CANCEL.
1260    ///
1261    /// The endpoint refuses one for an announcement it never accepted, so
1262    /// nothing is written on the wire when it does.
1263    pub async fn announce_cancel(
1264        &mut self,
1265        track_namespace: TrackNamespace,
1266        error_code: VarInt,
1267        reason_phrase: Vec<u8>,
1268    ) -> Result<(), ConnectionError> {
1269        let msg = self.endpoint.announce_cancel(track_namespace, error_code, reason_phrase)?;
1270        self.send_control(&msg).await
1271    }
1272    // ── Track Status flow ────────────────────────────────────
1273
1274    /// Send a TRACK_STATUS (draft-13 subscribe-like request).
1275    /// Returns the allocated request ID.
1276    #[allow(clippy::too_many_arguments)]
1277    pub async fn track_status(
1278        &mut self,
1279        track_namespace: TrackNamespace,
1280        track_name: Vec<u8>,
1281        subscriber_priority: u8,
1282        group_order: GroupOrder,
1283        forward: Forward,
1284        filter_type: FilterType,
1285        parameters: Vec<KeyValuePair>,
1286    ) -> Result<VarInt, ConnectionError> {
1287        let (req_id, msg) = self.endpoint.track_status(
1288            track_namespace,
1289            track_name,
1290            subscriber_priority,
1291            group_order,
1292            forward,
1293            filter_type,
1294            parameters,
1295        )?;
1296        self.send_control(&msg).await?;
1297        Ok(req_id)
1298    }
1299
1300    /// Accept a track status the peer asked for, sending TRACK_STATUS_OK.
1301    ///
1302    /// The endpoint refuses a second answer to one request, so nothing is
1303    /// written on the wire when it does. It also chooses the Track Alias the
1304    /// message carries, because the draft leaves only one value open.
1305    pub async fn track_status_ok(
1306        &mut self,
1307        request_id: VarInt,
1308        expires: VarInt,
1309        group_order: GroupOrder,
1310        parameters: Vec<KeyValuePair>,
1311    ) -> Result<(), ConnectionError> {
1312        let msg =
1313            self.endpoint.send_track_status_ok(request_id, expires, group_order, parameters)?;
1314        self.send_control(&msg).await
1315    }
1316
1317    /// Refuse a track status the peer asked for, sending TRACK_STATUS_ERROR.
1318    ///
1319    /// The other answer the request can have, and the endpoint holds it to the
1320    /// same count of one.
1321    pub async fn track_status_error(
1322        &mut self,
1323        request_id: VarInt,
1324        error_code: VarInt,
1325        reason_phrase: Vec<u8>,
1326    ) -> Result<(), ConnectionError> {
1327        let msg = self.endpoint.send_track_status_error(request_id, error_code, reason_phrase)?;
1328        self.send_control(&msg).await
1329    }
1330
1331    // ── Malformed Tracks ────────────────────────────────────
1332
1333    /// Answer Section 2.5 for the track `alias` names: withdraw from it, and
1334    /// send the UNSUBSCRIBE that withdrawal is.
1335    ///
1336    /// "When a subscriber detects a Malformed Track, it MUST UNSUBSCRIBE from
1337    /// the Track and SHOULD deliver an error to the application." The caller
1338    /// delivers the error by returning it; this is the other half, and the
1339    /// caller does not have to remember to ask for it.
1340    ///
1341    /// Nothing here ends the session, and that is the point of it. The
1342    /// conditions in Section 2.5 are answered by giving up one track.
1343    ///
1344    /// # Why a write that fails is not reported
1345    ///
1346    /// The error the caller is about to receive is the one that says what
1347    /// went wrong with the track. A control stream that will not take an
1348    /// UNSUBSCRIBE is a session on its way out for a reason of its own, and
1349    /// replacing the condition's report with a transport error would lose the
1350    /// only account of why the track was withdrawn. The rest of the
1351    /// withdrawal is abandoned, because a stream that refused one message
1352    /// will refuse the next.
1353    async fn withdraw_malformed_track(&self, alias: u64, condition: MalformedTrackCondition) {
1354        for msg in self.endpoint.withdraw_malformed_track(alias, condition) {
1355            if self.send_control(&msg).await.is_err() {
1356                break;
1357            }
1358        }
1359    }
1360
1361    /// Record the framing an arriving object was sent with, and withdraw from
1362    /// the track when it is the second framing that track has been sent.
1363    ///
1364    /// The receiving half of a pair. The two writing paths call the endpoint
1365    /// directly and answer a mixed track by refusing to write it, because
1366    /// Section 2.5's sentence is a subscriber's: an endpoint about to send an
1367    /// object is that object's Original Publisher, and a publisher has no
1368    /// subscription of its own to withdraw.
1369    async fn note_received_framing(
1370        &self,
1371        alias: u64,
1372        seen: ObjectForwardingPreference,
1373    ) -> Result<(), ConnectionError> {
1374        let Err(err) = self.endpoint.note_object_forwarding_preference(alias, seen) else {
1375            return Ok(());
1376        };
1377        self.withdraw_malformed_track(alias, MalformedTrackCondition::MixedForwardingPreference)
1378            .await;
1379        Err(err.into())
1380    }
1381
1382    // ── Data streams ────────────────────────────────────────
1383
1384    /// Open a new unidirectional stream for sending subgroup data.
1385    pub async fn open_subgroup_stream(
1386        &self,
1387        header: &AnySubgroupHeader,
1388    ) -> Result<FramedSendStream, ConnectionError> {
1389        // Before the stream is opened: the Original Publisher is who the rule
1390        // binds, so a header that would mix this track's framing is refused
1391        // here rather than written and answered by the peer.
1392        self.endpoint.note_object_forwarding_preference(
1393            header.track_alias(),
1394            ObjectForwardingPreference::Subgroup,
1395        )?;
1396        let send = self.transport.open_uni().await?;
1397        let mut framed = FramedSendStream::new(send);
1398        let sid = framed.stream_id();
1399        framed.write_subgroup_header(header).await?;
1400        self.emit(ClientEvent::StreamOpened {
1401            direction: Direction::Send,
1402            stream_kind: StreamKind::Subgroup,
1403            stream_id: sid,
1404        });
1405        self.emit(ClientEvent::DataStreamHeader {
1406            stream_id: sid,
1407            direction: Direction::Send,
1408            header: header.clone(),
1409        });
1410        Ok(framed)
1411    }
1412
1413    /// Open a new unidirectional stream for sending a FETCH's objects.
1414    ///
1415    /// The objects answering a FETCH do not go on the request's own stream:
1416    /// they go on a unidirectional stream of their own, which opens with a
1417    /// FETCH_HEADER naming the request they belong to. This writes that header
1418    /// and hands back the stream, the same way
1419    /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
1420    /// subgroup.
1421    ///
1422    /// The caller owns the stream that comes back. Nothing here remembers
1423    /// which request it belongs to, so an endpoint serving several fetches at
1424    /// once keeps its own map from Request ID to stream.
1425    pub async fn open_fetch_stream(
1426        &self,
1427        header: &AnyFetchHeader,
1428    ) -> Result<FramedSendStream, ConnectionError> {
1429        let send = self.transport.open_uni().await?;
1430        let mut framed = FramedSendStream::new(send);
1431        let sid = framed.stream_id();
1432        framed.write_fetch_header(header).await?;
1433        self.emit(ClientEvent::StreamOpened {
1434            direction: Direction::Send,
1435            stream_kind: StreamKind::Fetch,
1436            stream_id: sid,
1437        });
1438        Ok(framed)
1439    }
1440
1441    /// Accept the next unidirectional stream and read its fetch header.
1442    ///
1443    /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
1444    /// are separate because the header decides how every object after it is
1445    /// framed, so a caller has to know which it is expecting before the first
1446    /// byte is read.
1447    ///
1448    /// Objects come off the returned stream with
1449    /// [`FramedRecvStream::read_fetch_object`].
1450    pub async fn accept_fetch_stream(
1451        &self,
1452    ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
1453        let recv = self.transport.accept_uni().await?;
1454        let mut framed = FramedRecvStream::new(recv);
1455        let sid = framed.stream_id();
1456        let header = framed.read_fetch_header().await?;
1457        self.emit(ClientEvent::StreamOpened {
1458            direction: Direction::Receive,
1459            stream_kind: StreamKind::Fetch,
1460            stream_id: sid,
1461        });
1462        self.emit(ClientEvent::FetchStreamHeader {
1463            stream_id: sid,
1464            direction: Direction::Receive,
1465            header: header.clone(),
1466        });
1467        // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
1468        // carries an `AnySubgroupHeader` and cannot express one. What
1469        // `accept_subgroup_stream` does beyond this - the forwarding-preference
1470        // note, the object measurement - is about a subgroup and has no
1471        // counterpart on a fetch stream.
1472        Ok((header, framed))
1473    }
1474
1475    /// Accept an incoming unidirectional data stream and read its subgroup
1476    /// header.
1477    pub async fn accept_subgroup_stream(
1478        &self,
1479    ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
1480        let recv = self.transport.accept_uni().await?;
1481        let mut framed = FramedRecvStream::new(recv);
1482        let sid = framed.stream_id();
1483        let header = framed.read_subgroup_header().await?;
1484        self.emit(ClientEvent::StreamOpened {
1485            direction: Direction::Receive,
1486            stream_kind: StreamKind::Subgroup,
1487            stream_id: sid,
1488        });
1489        self.emit(ClientEvent::DataStreamHeader {
1490            stream_id: sid,
1491            direction: Direction::Receive,
1492            header: header.clone(),
1493        });
1494        // Every object on a subgroup stream has the Subgroup preference, so
1495        // the header settles the track's framing before a single object is
1496        // read.
1497        self.note_received_framing(header.track_alias(), ObjectForwardingPreference::Subgroup)
1498            .await?;
1499        // The track is resolved here and not inside the stream: it takes the
1500        // endpoint's alias table, which a stream handle has no way back to.
1501        if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
1502            framed.measure_objects_against(objects, header.group_id());
1503        }
1504        Ok((header, framed))
1505    }
1506
1507    /// Send an object via datagram.
1508    ///
1509    /// The header goes through `AnyDatagramHeader::encode`, which refuses a
1510    /// header whose Object Status the framing it names cannot carry. Such a
1511    /// header errors here and nothing is sent, rather than going out as an
1512    /// ordinary payload datagram with the status quietly dropped.
1513    pub fn send_datagram(
1514        &self,
1515        header: &AnyDatagramHeader,
1516        payload: &[u8],
1517    ) -> Result<(), ConnectionError> {
1518        // Before anything is encoded, for the reason `open_subgroup_stream`
1519        // gives.
1520        self.endpoint.note_object_forwarding_preference(
1521            header.meta().track_alias,
1522            ObjectForwardingPreference::Datagram,
1523        )?;
1524        let mut buf = Vec::new();
1525        header.encode(&mut buf)?;
1526        buf.extend_from_slice(payload);
1527        self.emit(ClientEvent::DatagramReceived {
1528            direction: Direction::Send,
1529            header: header.clone(),
1530            payload_len: payload.len(),
1531        });
1532        self.transport.send_datagram(bytes::Bytes::from(buf))?;
1533        Ok(())
1534    }
1535
1536    /// Receive a datagram and decode its header.
1537    pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
1538        let data = self.transport.recv_datagram().await?;
1539        let mut cursor = &data[..];
1540        let header = AnyDatagramHeader::decode(DraftVersion::Draft13, &mut cursor)?;
1541        let consumed = data.len() - cursor.len();
1542        let payload = data.slice(consumed..);
1543        self.emit(ClientEvent::DatagramReceived {
1544            direction: Direction::Receive,
1545            header: header.clone(),
1546            payload_len: payload.len(),
1547        });
1548        // A datagram is the other framing, and it settles the track's just as a
1549        // subgroup header does.
1550        self.note_received_framing(header.meta().track_alias, ObjectForwardingPreference::Datagram)
1551            .await?;
1552        // A datagram is a whole object, so the connection can measure it
1553        // without help from the caller.
1554        let meta = header.meta();
1555        if let Err(err) = self.endpoint.note_received_object(
1556            meta.track_alias,
1557            ObjectLocation { group: meta.group_id, object: meta.object_id },
1558            object_role(meta.status),
1559        ) {
1560            // One of the two rules that record answers is a Malformed Track,
1561            // and this path can give the answer itself: a datagram is read
1562            // through the connection, which is what an UNSUBSCRIBE takes. The
1563            // other rule ends the session and is the close table's.
1564            if matches!(err, EndpointError::ObjectPastFinalObject { .. }) {
1565                self.withdraw_malformed_track(
1566                    meta.track_alias,
1567                    MalformedTrackCondition::ObjectPastFinalObject,
1568                )
1569                .await;
1570            }
1571            return Err(err.into());
1572        }
1573        Ok((header, payload))
1574    }
1575
1576    // ── Accessors ───────────────────────────────────────────
1577
1578    /// Access the underlying endpoint state machine.
1579    pub fn endpoint(&self) -> &Endpoint {
1580        &self.endpoint
1581    }
1582
1583    /// Mutable access to the endpoint state machine.
1584    pub fn endpoint_mut(&mut self) -> &mut Endpoint {
1585        &mut self.endpoint
1586    }
1587
1588    /// Get the negotiated MoQT version.
1589    pub fn negotiated_version(&self) -> Option<VarInt> {
1590        self.endpoint.negotiated_version()
1591    }
1592
1593    /// The code to close the session with when a message could not be decoded
1594    /// because the peer broke a rule draft-13 answers with a close.
1595    ///
1596    /// Every variant listed here comes from a sentence in this draft that names
1597    /// the consequence, and the list is per draft: answering a bound this draft
1598    /// does not state would close a session over traffic a conforming peer may
1599    /// send.
1600    ///
1601    ///   - Reason Phrase, maximum 1024 bytes: "If an endpoint receives a length
1602    ///     exceeding the maximum, it MUST close the session with a Protocol
1603    ///     Violation."
1604    ///   - GOAWAY New Session URI, maximum 8,192 bytes, with the same sentence.
1605    ///     Drafts 11 through 19 state it; 07 through 10 state no maximum for
1606    ///     the field at all.
1607    ///   - Key-Value-Pair value, maximum 2^16-1 bytes, with the same sentence.
1608    ///   - Track Namespace tuple size: "If an endpoint receives a Track
1609    ///     Namespace tuple with an N of 0 or more than 32, it MUST close the
1610    ///     session with a Protocol Violation." Note the lower bound - an empty
1611    ///     tuple is refused here, where drafts 17 and later permit one.
1612    ///   - Full Track Name, maximum 4,096 bytes, "computed as the sum of the
1613    ///     lengths of each Track Namespace tuple field and the Track Name
1614    ///     length field". This draft bounds the pair and not the namespace
1615    ///     alone; draft-16 widened it.
1616    ///   - Duplicate parameters, a SHOULD rather than a MUST: "Receivers SHOULD
1617    ///     check that there are no unauthorized duplicate parameters and close
1618    ///     the session as a 'Protocol Violation' if found." The rule is
1619    ///     asymmetric here - "Receivers MUST allow duplicates of unknown
1620    ///     parameters", and one known type is granted repeats - and the codec
1621    ///     reports only the repeats this draft actually forbids.
1622    ///   - Unknown control message type: "An endpoint that receives an unknown
1623    ///     message type MUST close the session."
1624    ///   - Extension headers on an Object whose status is Object Does Not
1625    ///     Exist, Section 9.2.1.2. That one arrives on a data stream or a
1626    ///     datagram, so [`Connection::close_for_data_stream`] is what carries
1627    ///     it.
1628    ///
1629    /// **Not** the zero-length Track Namespace Field, the delta-encoded
1630    /// parameter type overflow, or the Object ID delta wrap. Those enter the
1631    /// specification at drafts 16 and 18, and this draft states none of them.
1632    ///
1633    /// **Not** the 2^16-1 control message length either. This draft states the
1634    /// limit - "the total length of a control message is limited to 2^16-1
1635    /// bytes" - and states no consequence for exceeding it, so an oversized
1636    /// message is refused by the decoder and stops there.
1637    ///
1638    /// **Not** [`CodecError::UnexpectedEnd`], which reports no rule at all: the
1639    /// reader raises it whenever a message is still arriving, and
1640    /// `read_control` loops on it. Closing over it would end a session on an
1641    /// ordinary short read.
1642    ///
1643    /// **Not** the unknown Message Parameter rule. Drafts 16 through 19 require
1644    /// a close for a Message Parameter whose type the negotiated version does
1645    /// not define. This draft states the opposite and states it about the same
1646    /// parameters: "Receivers MUST allow duplicates of unknown parameters",
1647    /// which presumes an unknown parameter arrives and is carried. Refusing one
1648    /// here would close a session over an extension this draft leaves room for.
1649    ///
1650    /// `None` for everything else, including [`CodecError::InvalidField`]. That
1651    /// variant is shared by a dozen unrelated malformations, only some of which
1652    /// the draft answers with a close, so a session cannot be ended on it
1653    /// without ending sessions the draft does not ask to be ended. Splitting it
1654    /// is the way to bring the rest of those rules under this function;
1655    /// widening the match is not - the extension-header rule above reached this
1656    /// table by being split out of it.
1657    fn codec_session_error_code(
1658        err: &CodecError,
1659    ) -> Option<moqtap_codec::draft13::error_codes::SessionErrorCode> {
1660        use moqtap_codec::draft13::error_codes::SessionErrorCode;
1661        use moqtap_codec::kvp::KvpError;
1662        match err {
1663            // The declared Length disagreeing with the fields, which every
1664            // draft answers with a close. Drafts 07 through 10 name no code for
1665            // it, so it takes the one their other unnamed rules take.
1666            CodecError::ControlMessageLengthMismatch { .. } => {
1667                Some(SessionErrorCode::ProtocolViolation)
1668            }
1669            CodecError::ReasonPhraseTooLong
1670            | CodecError::GoAwayUriTooLong
1671            | CodecError::InvalidNamespaceTupleSize(_)
1672            | CodecError::TrackNameTooLong
1673            | CodecError::DuplicateParameter(_)
1674            | CodecError::UnknownMessageType(_)
1675            | CodecError::ExtensionsOnNonExistentObject(_)
1676            | CodecError::Kvp(KvpError::ValueTooLong(_)) => {
1677                Some(SessionErrorCode::ProtocolViolation)
1678            }
1679            // An unknown data-plane type, Section 9: "An endpoint that
1680            // receives an unknown stream or datagram type MUST close the
1681            // session." One sentence covering two tables, which is why both
1682            // variants sit here.
1683            // A Content Exists field that is neither zero nor one,
1684            // Sections 8.8 and 8.13: "Any other value is a protocol error and
1685            // MUST terminate the session with a Protocol Violation".
1686            CodecError::InvalidContentExists(_) => Some(SessionErrorCode::ProtocolViolation),
1687            // A Forward field that is neither zero nor one. Sections 8.7 and
1688            // 8.10: "Any other value is a protocol error and MUST terminate the
1689            // session with a Protocol Violation". Section 8.13 says "Any value
1690            // other than 0 or 1 is a Protocol Violation". Section 8.14 names the
1691            // two legal values without a consequence and is answered the same
1692            // way, for the reason given on the variant.
1693            CodecError::InvalidForward(_) => Some(SessionErrorCode::ProtocolViolation),
1694            CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
1695                Some(SessionErrorCode::ProtocolViolation)
1696            }
1697            // A key-value pair whose value is not the serialization its own
1698            // Type defines, Section 1.3.2: "If a receiver understands a Type,
1699            // and the following Value or Length/Value does not match the
1700            // serialization defined by that Type, the receiver MUST terminate the
1701            // session with error code 'Key-Value Formatting Error'."
1702            // Section 8.2.1.1 states the same answer for the one structure this
1703            // draft spells out: "If the Token structure cannot be decoded, the
1704            // receiver MUST close the Session with Key-Value Formatting error."
1705            //
1706            // The one rule in this table that names a code other than Protocol
1707            // Violation.
1708            CodecError::KeyValueFormatting { .. } => {
1709                Some(SessionErrorCode::KeyValueFormattingError)
1710            }
1711            // Everything this draft does not answer, named rather than swept up
1712            // by a wildcard. The arm is exhaustive deliberately: a new
1713            // `CodecError` variant will not compile until it has been placed on
1714            // one side or the other, on this draft, which is the decision a `_`
1715            // arm makes silently and invisibly on all thirteen at once.
1716            //
1717            // Adding one variant to `CodecError` was tried, and produces
1718            // thirteen `E0004`s, one per draft, each naming the variant that has
1719            // nowhere to go. That is the whole mechanism.
1720            //
1721            // The nesting stops at `VarInt`, whose variants report how the bytes
1722            // ran out rather than a rule an endpoint states, so there is nothing
1723            // in it for a draft to answer. `Kvp` is spelled out because it does
1724            // carry one.
1725            // Not `ParameterValueOutOfRange`: no parameter this draft defines
1726            // restricts its value's range. Forwarding is a message field here
1727            // and is answered above.
1728            CodecError::ParameterValueOutOfRange { .. }
1729            | CodecError::UnexpectedEnd
1730            | CodecError::MessageTooLong(_)
1731            | CodecError::VarInt(_)
1732            | CodecError::InvalidField
1733            | CodecError::EmptyNamespaceField
1734            | CodecError::InvalidRange(..)
1735            | CodecError::ParameterLengthMismatch(_)
1736            | CodecError::EndOfTrackObjectId(_)
1737            | CodecError::KeyDeltaOverflow(..)
1738            // Not `TrackPropertyValueOutOfRange`: this draft has neither
1739            // namespace the variant is about. Draft-16 opens an extension header
1740            // registry with value rules of its own, and draft-17 renames it to
1741            // the Track Property registry. Before that, everything with a
1742            // restricted range is either a message field or a Message Parameter.
1743            | CodecError::TrackPropertyValueOutOfRange { .. }
1744            | CodecError::ParametersOutOfOrder(..)
1745            | CodecError::ObjectIdOverflow(..)
1746            | CodecError::InvalidRequiredRequestIdDelta(..)
1747            | CodecError::InvalidTypeValue { .. }
1748            | CodecError::UnknownMessageParameter(_)
1749            // Not `ParameterOutOfScope`: this draft states the scope rule and
1750            // answers it the other way. Section 8.2.1 Version Specific Parameters: "Each
1751            // version-specific parameter definition indicates the message types in which it can
1752            // appear. If it appears in some other type of message, it MUST be
1753            // ignored." The codec carries such a parameter on this draft and never
1754            // raises the variant, so this arm records a rule this draft has and
1755            // does not close over, not one it is missing. Draft-17 is where the
1756            // second sentence becomes a close.
1757            | CodecError::ParameterOutOfScope { .. }
1758            // A Filter Type outside the set this draft assigns. Section 8.7
1759            // states the rule and stops there: "A filter type other than the
1760            // above MUST be treated as error." No code, no close, and no
1761            // sentence elsewhere in the draft that turns an error into one — so
1762            // the message is refused and the session stays open.
1763            // Draft-14 Section 9.7 is where the same sentence gained "MUST be
1764            // close the session with PROTOCOL_VIOLATION", and it is answered
1765            // there.
1766            //
1767            // The assigned set is not the same on every draft either: 07 and 08
1768            // assign 0x1 as Latest Group, 09 and 10 withdraw it, and 11 and
1769            // later reinstate it as Next Group Start. The decoder holds each
1770            // draft to its own list; this arm only decides what a refusal does
1771            // to the session.
1772            //
1773            // The two rules below belong to the parameter form of the filter,
1774            // which arrives at draft-15. This draft carries the Filter Type as a
1775            // field of SUBSCRIBE, so there is no parameter for either to be
1776            // about.
1777            | CodecError::InvalidFilterType(_)
1778            | CodecError::SubscriptionFilterMalformed { .. }
1779            | CodecError::FilterEndGroupOverflow { .. }
1780            // A Fetch Type outside the set this draft assigns. Section 8.16
1781            // states the rule and stops there: "A Fetch Type other than 0x1,
1782            // 0x2 or 0x3 MUST be treated as an error", naming no code and no
1783            // close, exactly as this draft's Filter Type sentence does.
1784            // Draft-14 Section 9.16 is where both gained "MUST be close the
1785            // session with a PROTOCOL_VIOLATION", and it answers them there.
1786            | CodecError::InvalidFetchType(_)
1787            // The object payload rule, Section 9.2.1.1: "Any object with a status
1788            // code other than zero MUST have an empty payload." A MUST on the
1789            // sender with no receiver action named anywhere — the "SHOULD be
1790            // treated as a protocol error" in the same paragraph belongs to the
1791            // sentence before it, which is about a status value this draft does
1792            // not assign — so an object carrying a payload it may not is refused
1793            // and the session stays open.
1794            //
1795            // That was already the answer. The bytes used to arrive as
1796            // `InvalidField`, which is on this side too; naming the rule changes
1797            // nothing a peer can observe and makes the decision legible.
1798            | CodecError::PayloadNotPermitted { .. }
1799            | CodecError::UnsupportedDraft(_)
1800            | CodecError::Kvp(
1801                KvpError::MissingLength | KvpError::UnexpectedEnd | KvpError::VarInt(_),
1802            ) => None,
1803        }
1804    }
1805
1806    /// Close the session on the wire when a decode failure is one draft-13
1807    /// answers with a close, and hand the error back unchanged.
1808    /// Without it every bound the decoder enforces would stop at *this endpoint
1809    /// refused the frame* while the peer, which is the one that broke the rule,
1810    /// saw a session that was still open and went on sending. "MUST close the
1811    /// session" is a statement about the wire.
1812    fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
1813        if let ConnectionError::Codec(inner) = &err {
1814            if let Some(code) = Self::codec_session_error_code(inner) {
1815                // QUIC application error codes are 62-bit; every code in this
1816                // registry is far below `u32::MAX`, and saturating rather than
1817                // truncating means a future code that is not could never be
1818                // reported as a different, assigned one.
1819                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1820                self.close(wire_code, inner.to_string().as_bytes());
1821            }
1822        }
1823        err
1824    }
1825
1826    /// Close the session on the wire when the endpoint says a violation is
1827    /// fatal to it, and hand the error back unchanged.
1828    ///
1829    /// [`EndpointError::session_error_code`] answers `Some` for exactly the
1830    /// errors this draft ends the session over, and the endpoint has already
1831    /// moved its own state machine to Closed by the time this runs. Without
1832    /// this step that move is purely internal: the local endpoint refuses to
1833    /// start anything new while the peer, which is the one that broke the
1834    /// rule, sees a session that is still open and goes on sending. A rule
1835    /// that names a session termination code is a statement about the wire,
1836    /// so it takes a CONNECTION_CLOSE to satisfy it.
1837    ///
1838    /// The reason phrase is the error's own `Display` text, which names the
1839    /// rule rather than repeating the numeric code the close already carries.
1840    ///
1841    /// Errors that answer `None` are recoverable and nothing is sent.
1842    fn close_for(&self, err: &EndpointError) {
1843        if let Some(code) = err.session_error_code() {
1844            // QUIC application error codes are 62-bit; every code in this
1845            // registry is far below `u32::MAX`, and saturating rather than
1846            // truncating means a future code that is not could never be
1847            // reported as a different, assigned one.
1848            let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1849            self.close(wire_code, err.to_string().as_bytes());
1850        }
1851    }
1852
1853    /// [`close_for`](Self::close_for), then the error unchanged, for the
1854    /// common case where the endpoint's error is also what the caller returns.
1855    fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
1856        self.close_for(&err);
1857        ConnectionError::Endpoint(err)
1858    }
1859
1860    /// Withdraw from a track a data stream found malformed, reporting whether
1861    /// it did.
1862    ///
1863    /// The Malformed Track twin of [`Connection::close_for_data_stream`], and
1864    /// separate from it for the same reason and one more. The same one: a
1865    /// [`FramedRecvStream`] holds no connection, so the reader that finds the
1866    /// fault is not the object that can send an UNSUBSCRIBE. The one more: the
1867    /// two answers are opposites — that call ends the session, this one gives
1868    /// up a track and leaves it running — and a single entry point would have
1869    /// to decide between them from the error alone, which is exactly the
1870    /// decision a caller reproducing a capture wants to make itself.
1871    ///
1872    /// The datagram path needs none of this. It is read through the connection,
1873    /// so [`Connection::recv_datagram`] answers the condition where it finds
1874    /// it, and this is only for the objects that arrive on a stream the caller
1875    /// holds.
1876    pub async fn withdraw_for_data_stream(&self, err: &ConnectionError) -> bool {
1877        let ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject { alias, .. }) = err
1878        else {
1879            return false;
1880        };
1881        self.withdraw_malformed_track(*alias, MalformedTrackCondition::ObjectPastFinalObject).await;
1882        true
1883    }
1884
1885    /// Close the session over a rule broken on a data stream, reporting whether
1886    /// it did.
1887    ///
1888    /// A data stream cannot close for itself the way `recv_control` does:
1889    /// [`Connection::accept_subgroup_stream`] hands the caller a
1890    /// [`FramedRecvStream`] holding no connection, so the reader that finds the
1891    /// violation is not the object that can act on it. Keeping it a separate
1892    /// call is deliberate as well - a permissive caller, one reproducing a
1893    /// capture, can read a violating stream and report it without tearing the
1894    /// session down.
1895    ///
1896    /// The rule this draft answers here is extension headers on an Object whose
1897    /// status is Object Does Not Exist, which reaches subgroup streams, fetch
1898    /// streams and status datagrams alike. It shares `codec_session_error_code`
1899    /// with the control path, so a rule is answered with one code whichever
1900    /// stream carried it.
1901    ///
1902    /// Not every rule that reaches here is the decoder's. A track whose objects
1903    /// mix forwarding preferences is the endpoint's to notice — it takes the
1904    /// alias table to know which track an object belongs to — and it arrives on
1905    /// exactly these streams. Both kinds are asked for a code the same way, and
1906    /// a rule with no code is declined rather than guessed at.
1907    pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
1908        match err {
1909            ConnectionError::Codec(inner) => {
1910                let Some(code) = Self::codec_session_error_code(inner) else { return false };
1911                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1912                self.close(wire_code, inner.to_string().as_bytes());
1913                true
1914            }
1915            // A rule the endpoint raises rather than the decoder. The two
1916            // reach their codes through different tables and mean the same
1917            // thing here: `Some` is a rule this draft ends the session over.
1918            ConnectionError::Endpoint(inner) => {
1919                let Some(code) = inner.session_error_code() else { return false };
1920                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1921                self.close(wire_code, inner.to_string().as_bytes());
1922                true
1923            }
1924            _ => false,
1925        }
1926    }
1927
1928    /// Close the connection.
1929    pub fn close(&self, code: u32, reason: &[u8]) {
1930        self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
1931        self.transport.close(code, reason);
1932    }
1933}
1934
1935/// Determine the encoded length of a varint from its first byte.
1936fn varint_len(first_byte: u8) -> usize {
1937    1 << (first_byte >> 6)
1938}
1939
1940#[cfg(test)]
1941mod tests {
1942    use super::*;
1943
1944    #[test]
1945    fn client_config_supported_versions_default() {
1946        let config = ClientConfig {
1947            additional_versions: Vec::new(),
1948            transport: TransportType::Quic,
1949            skip_cert_verification: false,
1950            ca_certs: Vec::new(),
1951            setup_parameters: Vec::new(),
1952        };
1953        let versions = config.supported_versions();
1954        assert_eq!(versions.len(), 1);
1955        assert_eq!(versions[0].into_inner(), 0xff000000 + 13);
1956    }
1957
1958    #[test]
1959    fn client_config_alpn_quic() {
1960        let config = ClientConfig {
1961            additional_versions: Vec::new(),
1962            transport: TransportType::Quic,
1963            skip_cert_verification: false,
1964            ca_certs: Vec::new(),
1965            setup_parameters: Vec::new(),
1966        };
1967        assert_eq!(config.alpn(), vec![DraftVersion::Draft13.quic_alpn().to_vec()]);
1968    }
1969
1970    #[test]
1971    fn moqt_alpn_value() {
1972        assert_eq!(MOQT_ALPN, b"moq-00");
1973    }
1974}