Skip to main content

moqtap_client/draft11/
connection.rs

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