Skip to main content

moqtap_client/draft14/
connection.rs

1use bytes::{Buf, Bytes, BytesMut};
2
3use crate::draft14::endpoint::{Endpoint, EndpointError};
4use crate::draft14::event::{ClientEvent, Direction, StreamKind};
5use crate::draft14::observer::ConnectionObserver;
6use crate::draft14::session::request_id::Role;
7use crate::draft14::session::setup;
8use crate::forwarding_preference::ObjectForwardingPreference;
9use crate::malformed_tracks::MalformedTrackCondition;
10use crate::track_locations::{ObjectLocation, ObjectRole, TrackObjects};
11use crate::transport::{RecvStream, SendStream, Transport, TransportError};
12use moqtap_codec::dispatch::{
13    AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
14};
15use moqtap_codec::draft14::data_stream::{FetchObject, SubgroupObject, SubgroupObjectReader};
16use moqtap_codec::draft14::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 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 (e.g. object before header).
57    #[error("data stream state error: {0}")]
58    DataStreamState(&'static str),
59    /// A control message this build decoded for draft-14 and then could not
60    /// narrow to draft-14'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 per-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-14 did not narrow to draft-14: 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 MoQT client connection.
144///
145/// Both `draft` and `transport` are required — there is no `Default` impl.
146pub struct ClientConfig {
147    /// The MoQT draft version to use (primary, determines codec/framing).
148    pub draft: DraftVersion,
149    /// Additional draft versions to offer in CLIENT_SETUP.
150    /// The primary `draft` is always included first.
151    pub additional_versions: Vec<DraftVersion>,
152    /// The transport type (QUIC or WebTransport).
153    pub transport: TransportType,
154    /// Whether to skip TLS certificate verification (for testing).
155    pub skip_cert_verification: bool,
156    /// Custom CA certificates to trust (DER-encoded).
157    pub ca_certs: Vec<Vec<u8>>,
158    /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
159    pub setup_parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
160}
161
162impl ClientConfig {
163    /// Returns the MoQT version varints for the CLIENT_SETUP message.
164    /// Primary draft first, then any additional versions.
165    pub fn supported_versions(&self) -> Vec<VarInt> {
166        let mut versions = vec![self.draft.version_varint()];
167        for v in &self.additional_versions {
168            let varint = v.version_varint();
169            if !versions.contains(&varint) {
170                versions.push(varint);
171            }
172        }
173        versions
174    }
175
176    /// Returns the ALPN protocol identifiers for the transport.
177    pub fn alpn(&self) -> Vec<Vec<u8>> {
178        match &self.transport {
179            TransportType::Quic => vec![self.draft.quic_alpn().to_vec()],
180            TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
181        }
182    }
183}
184
185/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
186pub struct FramedSendStream {
187    inner: SendStream,
188    draft: DraftVersion,
189    /// Stateful subgroup object encoder. Initialized by
190    /// [`FramedSendStream::write_subgroup_header`] and used by
191    /// [`FramedSendStream::write_subgroup_object`] to track the delta-encoded
192    /// object ID state.
193    subgroup_io: Option<SubgroupObjectReader>,
194}
195
196impl FramedSendStream {
197    /// Create a new framed send stream for the given draft version.
198    pub fn new(inner: SendStream, draft: DraftVersion) -> Self {
199        Self { inner, draft, subgroup_io: None }
200    }
201
202    /// Get the transport-level stream ID.
203    pub fn stream_id(&self) -> u64 {
204        self.inner.stream_id()
205    }
206
207    /// Write a control message to the stream with type+length framing.
208    /// Returns the raw bytes that were written (for event capture).
209    pub async fn write_control(
210        &mut self,
211        msg: &AnyControlMessage,
212    ) -> Result<Vec<u8>, ConnectionError> {
213        let mut buf = Vec::new();
214        msg.encode(&mut buf)?;
215        self.inner.write_all(&buf).await?;
216        Ok(buf)
217    }
218
219    /// Write a subgroup stream header. Also initializes the internal
220    /// delta-encoding state used by [`FramedSendStream::write_subgroup_object`].
221    ///
222    /// The header is refused, and nothing is written, if its fields disagree
223    /// with its own stream type. That check has to happen here rather than at
224    /// the first object: the type is what every object after it is framed
225    /// against, so a header that went out saying the wrong thing cannot be
226    /// taken back.
227    pub async fn write_subgroup_header(
228        &mut self,
229        header: &AnySubgroupHeader,
230    ) -> Result<(), ConnectionError> {
231        let mut buf = Vec::new();
232        header.encode_stream_checked(&mut buf)?;
233        self.inner.write_all(&buf).await?;
234        // Clippy would rather see these two arms as an `if let`, and rustc rejects
235        // that in a single-draft build, where the pattern is irrefutable. Only a
236        // `match` satisfies both.
237        #[allow(clippy::single_match)]
238        match header {
239            AnySubgroupHeader::Draft14(ref d14) => {
240                self.subgroup_io = Some(SubgroupObjectReader::new(d14));
241            }
242            // Only this draft's header seeds the object reader. With draft 14 the only enabled
243            // draft `AnySubgroupHeader` has a single variant, the arm above is exhaustive and this
244            // one unreachable. Compiled in every configuration with the lint allowed, rather than
245            // gated on a `cfg` naming the other drafts: such a list has to be edited in
246            // every draft module whenever a draft is added, and a copy that omits one leaves this
247            // match non-exhaustive.
248            #[allow(unreachable_patterns)]
249            _ => {}
250        }
251        Ok(())
252    }
253
254    /// Write a fetch response header.
255    pub async fn write_fetch_header(
256        &mut self,
257        header: &AnyFetchHeader,
258    ) -> Result<(), ConnectionError> {
259        let mut buf = Vec::new();
260        header.encode_stream(&mut buf);
261        self.inner.write_all(&buf).await?;
262        Ok(())
263    }
264
265    /// Append a draft-14 subgroup object to the stream. Uses the stateful
266    /// reader installed by [`FramedSendStream::write_subgroup_header`] to
267    /// produce the correct delta-encoded object ID. Returns an error if
268    /// called before a subgroup header was written.
269    pub async fn write_subgroup_object(
270        &mut self,
271        object: &SubgroupObject,
272    ) -> Result<(), ConnectionError> {
273        let writer = self
274            .subgroup_io
275            .as_mut()
276            .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
277        let mut buf = Vec::new();
278        writer.write_object(object, &mut buf)?;
279        self.inner.write_all(&buf).await?;
280        Ok(())
281    }
282
283    /// Append a draft-14 fetch object to the stream. Fetch objects are
284    /// self-contained (each carries its own group/subgroup/object IDs and
285    /// priority), so no prior `write_fetch_header` bookkeeping is required.
286    /// Append a fetch object to the stream.
287    ///
288    /// Draft-14 derives the declared length from the payload itself rather than
289    /// holding it as a field, so there is nothing here for the two to disagree
290    /// about - unlike the subgroup object, whose header carries the length and
291    /// whose writer has to overwrite it.
292    ///
293    /// # Errors
294    ///
295    /// Whatever the checked encoder refuses: a payload beside a status that
296    /// forbids one, or a status the draft leaves unassigned.
297    pub async fn write_fetch_object(
298        &mut self,
299        object: &FetchObject,
300    ) -> Result<(), ConnectionError> {
301        let mut buf = Vec::new();
302        object.encode_checked(&mut buf)?;
303        self.inner.write_all(&buf).await?;
304        Ok(())
305    }
306
307    /// Finish the stream (send FIN).
308    pub async fn finish(&mut self) -> Result<(), ConnectionError> {
309        self.inner.finish()?;
310        Ok(())
311    }
312
313    /// Returns the draft version this stream is framed for.
314    pub fn draft(&self) -> DraftVersion {
315        self.draft
316    }
317}
318
319/// What an Object Status makes of an object here.
320///
321/// Two answers where drafts 08 through 13 have three, and the missing one is
322/// the point: the end-of-track status settles where the track ended and is
323/// judged against nothing, because the rule about where one may be placed is
324/// not in this draft. `a_track_may_end_where_it_has_already_been.rs` asserts
325/// that acceptance.
326///
327/// Every other status is a statement about objects rather than one of them.
328fn object_role(status: Option<u64>) -> ObjectRole {
329    match status {
330        None | Some(0x0) => ObjectRole::Produced,
331        Some(0x4) => ObjectRole::EndsTrack(None),
332        _ => ObjectRole::Neither,
333    }
334}
335
336/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
337pub struct FramedRecvStream {
338    inner: RecvStream,
339    buf: BytesMut,
340    draft: DraftVersion,
341    /// Stateful subgroup object decoder. Initialized by
342    /// [`FramedRecvStream::read_subgroup_header`] and used by
343    /// [`FramedRecvStream::read_subgroup_object`] to track delta-encoded
344    /// object IDs and whether extension headers are present.
345    subgroup_io: Option<SubgroupObjectReader>,
346    /// The record this stream's objects are measured against, and the Group ID
347    /// its header named.
348    ///
349    /// One group for the whole stream: a subgroup header names it once and no
350    /// object header repeats it. `None` on a stream that was never given one -
351    /// a stream for an alias no live binding names, and every stream built
352    /// outside [`Connection::accept_subgroup_stream`] - and such a stream reads
353    /// without being measured, because `note_subgroup_object` has nothing to
354    /// measure it against.
355    tracking: Option<(TrackObjects, u64)>,
356}
357
358impl FramedRecvStream {
359    /// Create a new framed receive stream for the given draft version.
360    pub fn new(inner: RecvStream, draft: DraftVersion) -> Self {
361        Self { inner, buf: BytesMut::with_capacity(4096), draft, subgroup_io: None, tracking: None }
362    }
363
364    /// Get the transport-level stream ID.
365    pub fn stream_id(&self) -> u64 {
366        self.inner.stream_id()
367    }
368
369    /// Measure this stream's objects against `objects`, all of them in `group`.
370    ///
371    /// Called by [`Connection::accept_subgroup_stream`] once the header has
372    /// been read, which is the only point at which both the track and the group
373    /// are known.
374    fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
375        self.tracking = Some((objects, group));
376    }
377
378    /// Judge one object this stream carried against where its track ended.
379    ///
380    /// The object's Group ID is the stream's and its Object ID is its own,
381    /// already resolved from the delta the wire carries; what they are measured
382    /// against is the end an end-of-track object settled on any stream.
383    fn note_subgroup_object(
384        &self,
385        object: u64,
386        status: Option<u64>,
387    ) -> Result<(), ConnectionError> {
388        let Some((objects, group)) = &self.tracking else { return Ok(()) };
389        let at = ObjectLocation { group: *group, object };
390        objects.note_past_final(at, object_role(status)).map_err(|end| {
391            ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject {
392                alias: objects.alias(),
393                group: at.group,
394                object: at.object,
395                final_group: end.group,
396                final_object: end.object,
397            })
398        })
399    }
400
401    /// Read more data from the stream into the internal buffer.
402    async fn fill(&mut self) -> Result<bool, ConnectionError> {
403        let mut tmp = [0u8; 4096];
404        match self.inner.read(&mut tmp).await {
405            Ok(Some(n)) => {
406                self.buf.extend_from_slice(&tmp[..n]);
407                Ok(true)
408            }
409            Ok(None) => Ok(false),
410            Err(e) => Err(ConnectionError::Transport(e)),
411        }
412    }
413
414    /// Ensure at least `n` bytes are available in the buffer.
415    async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
416        while self.buf.len() < n {
417            if !self.fill().await? {
418                return Err(ConnectionError::UnexpectedEnd);
419            }
420        }
421        Ok(())
422    }
423
424    /// Read a control message from the stream.
425    ///
426    /// When `capture_raw` is true, the returned tuple includes a clone of the
427    /// framed wire bytes (for observer emission). When false, the second
428    /// element is `None` and the payload clone is skipped.
429    pub async fn read_control(
430        &mut self,
431        capture_raw: bool,
432    ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
433        // Read type ID varint
434        self.ensure(1).await?;
435        let type_len = varint_len(self.buf[0]);
436        self.ensure(type_len).await?;
437
438        let mut cursor = &self.buf[..type_len];
439        let _type_id = VarInt::decode(&mut cursor)?;
440
441        // Read payload length (16-bit BE for draft-11+, varint for earlier drafts)
442        let (payload_len, len_field_size) = if self.draft.uses_fixed_length_framing() {
443            self.ensure(type_len + 2).await?;
444            let hi = self.buf[type_len] as usize;
445            let lo = self.buf[type_len + 1] as usize;
446            ((hi << 8) | lo, 2)
447        } else {
448            self.ensure(type_len + 1).await?;
449            let payload_len_start = type_len;
450            let payload_len_varint_len = varint_len(self.buf[payload_len_start]);
451            self.ensure(type_len + payload_len_varint_len).await?;
452            let mut cursor = &self.buf[payload_len_start..type_len + payload_len_varint_len];
453            let payload_len = VarInt::decode(&mut cursor)?.into_inner() as usize;
454            (payload_len, payload_len_varint_len)
455        };
456
457        // Read full payload
458        let total = type_len + len_field_size + payload_len;
459        self.ensure(total).await?;
460
461        // Capture raw bytes only if requested (observer attached).
462        let raw = capture_raw.then(|| self.buf[..total].to_vec());
463
464        // Now decode the whole message
465        let mut frame = &self.buf[..total];
466        let msg = AnyControlMessage::decode(self.draft, &mut frame)?;
467        self.buf.advance(total);
468        Ok((msg, raw))
469    }
470
471    /// Read a subgroup stream header. Also initializes the internal
472    /// delta-decoding state used by
473    /// [`FramedRecvStream::read_subgroup_object`].
474    pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
475        self.ensure(1).await?;
476        loop {
477            let mut cursor = &self.buf[..];
478            match AnySubgroupHeader::decode(self.draft, &mut cursor) {
479                Ok(header) => {
480                    let consumed = self.buf.len() - cursor.remaining();
481                    self.buf.advance(consumed);
482                    // Clippy would rather see these two arms as an `if let`, and rustc rejects
483                    // that in a single-draft build, where the pattern is irrefutable. Only a
484                    // `match` satisfies both.
485                    #[allow(clippy::single_match)]
486                    match header {
487                        AnySubgroupHeader::Draft14(ref d14) => {
488                            self.subgroup_io = Some(SubgroupObjectReader::new(d14));
489                        }
490                        // Only this draft's header seeds the object reader. With draft 14 the only
491                        // enabled draft `AnySubgroupHeader` has a single variant, the arm above is
492                        // exhaustive and this one unreachable. Compiled in every configuration with
493                        // the lint allowed, rather than gated on a `cfg` naming the other thirteen
494                        // drafts: such a list has to be edited in every draft module whenever a
495                        // draft is added, and a copy that omits one leaves this match
496                        // non-exhaustive.
497                        #[allow(unreachable_patterns)]
498                        _ => {}
499                    }
500                    return Ok(header);
501                }
502                Err(e) if e.is_incomplete() => {
503                    if !self.fill().await? {
504                        return Err(ConnectionError::UnexpectedEnd);
505                    }
506                }
507                Err(e) => return Err(ConnectionError::Codec(e)),
508            }
509        }
510    }
511
512    /// Read a fetch response header.
513    pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
514        self.ensure(1).await?;
515        loop {
516            let mut cursor = &self.buf[..];
517            match AnyFetchHeader::decode(self.draft, &mut cursor) {
518                Ok(header) => {
519                    let consumed = self.buf.len() - cursor.remaining();
520                    self.buf.advance(consumed);
521                    return Ok(header);
522                }
523                Err(e) if e.is_incomplete() => {
524                    if !self.fill().await? {
525                        return Err(ConnectionError::UnexpectedEnd);
526                    }
527                }
528                Err(e) => return Err(ConnectionError::Codec(e)),
529            }
530        }
531    }
532
533    /// Read the next draft-14 subgroup object from this stream. Uses the
534    /// stateful reader installed by
535    /// [`FramedRecvStream::read_subgroup_header`] to decode the delta-
536    /// encoded object ID and handle extension headers per the stream type.
537    /// Returns an error if called before a subgroup header was read.
538    pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
539        if self.subgroup_io.is_none() {
540            return Err(ConnectionError::DataStreamState("subgroup header not read yet"));
541        }
542        loop {
543            let reader = self.subgroup_io.as_mut().unwrap();
544            let mut probe = reader.clone();
545            let mut cursor = &self.buf[..];
546            match probe.read_object(&mut cursor) {
547                Ok(obj) => {
548                    let consumed = self.buf.len() - cursor.remaining();
549                    self.buf.advance(consumed);
550                    // Commit state from the successful probe decode.
551                    *reader = probe;
552                    self.note_subgroup_object(
553                        obj.object_id.into_inner(),
554                        obj.status.map(|s| s as u64),
555                    )?;
556                    return Ok(obj);
557                }
558                Err(e) if e.is_incomplete() => {
559                    if !self.fill().await? {
560                        return Err(ConnectionError::UnexpectedEnd);
561                    }
562                }
563                Err(e) => return Err(ConnectionError::Codec(e)),
564            }
565        }
566    }
567
568    /// Read the next draft-14 fetch object from this stream. Fetch objects
569    /// are self-describing, so no prior `read_fetch_header` state is needed
570    /// to decode each object.
571    pub async fn read_fetch_object(&mut self) -> Result<FetchObject, ConnectionError> {
572        loop {
573            let mut cursor = &self.buf[..];
574            match FetchObject::decode(&mut cursor) {
575                Ok(obj) => {
576                    let consumed = self.buf.len() - cursor.remaining();
577                    self.buf.advance(consumed);
578                    return Ok(obj);
579                }
580                Err(e) if e.is_incomplete() => {
581                    if !self.fill().await? {
582                        return Err(ConnectionError::UnexpectedEnd);
583                    }
584                }
585                Err(e) => return Err(ConnectionError::Codec(e)),
586            }
587        }
588    }
589
590    /// Returns the draft version this stream is framed for.
591    pub fn draft(&self) -> DraftVersion {
592        self.draft
593    }
594}
595
596/// A live MoQT connection over QUIC or WebTransport, combining the endpoint
597/// state machine with actual network I/O.
598pub struct Connection {
599    transport: Transport,
600    endpoint: Endpoint,
601    draft: DraftVersion,
602    /// Behind a lock because a control message is written from two kinds of
603    /// place. Most of them are the caller's own request, made through `&mut
604    /// self`. The messages that answer a Malformed Track are not: the
605    /// conditions that make a track malformed are detected on the data plane,
606    /// where this connection is reached through a shared reference. The lock
607    /// also makes one message the unit of writing, so two of them cannot
608    /// interleave on the stream.
609    control_send: Option<tokio::sync::Mutex<FramedSendStream>>,
610    control_recv: Option<FramedRecvStream>,
611    observer: Option<Box<dyn ConnectionObserver>>,
612    /// Setup events buffered during `connect()` and replayed when an
613    /// observer attaches via `set_observer` — without this, an observer
614    /// attached after `connect` returns would never see the handshake.
615    pending_events: Vec<ClientEvent>,
616    /// The server's half of the setup handshake, kept whole.
617    ///
618    /// The endpoint acts on the parameters it recognises and retains none of
619    /// them, and which parameters a server sends — in what order, with what
620    /// values — is the sharpest thing a session says about the implementation
621    /// behind it.
622    server_setup: AnyControlMessage,
623    /// The framed wire bytes of [`Self::server_setup`].
624    server_setup_raw: Option<Vec<u8>>,
625}
626
627impl Connection {
628    /// Connect to a MoQT server as a client.
629    ///
630    /// Establishes a QUIC or WebTransport connection (based on `config.transport`),
631    /// opens a bidirectional control stream, performs the CLIENT_SETUP /
632    /// SERVER_SETUP handshake, and returns a ready-to-use connection.
633    pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
634        // PATH is for native QUIC only, and the transport is known here and
635        // nowhere further in. Refusing before dialling means a session that
636        // the server would close on sight is never opened.
637        setup::validate_client_path_transport(
638            &config.setup_parameters,
639            matches!(config.transport, TransportType::WebTransport { .. }),
640        )
641        .map_err(EndpointError::from)?;
642
643        let transport = match &config.transport {
644            TransportType::Quic => Self::connect_quic(addr, &config).await?,
645            TransportType::WebTransport { url } => {
646                let url = url.clone();
647                Self::connect_webtransport(&url, &config).await?
648            }
649        };
650
651        Self::adopt(transport, config).await
652    }
653
654    /// Run the MoQT setup handshake over a transport somebody else established.
655    ///
656    /// For choosing the draft from what the server selected: dial once through
657    /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
658    /// connection to the module its answer names. [`Self::connect`] cannot do
659    /// this — it derives its single ALPN from the draft it was given.
660    ///
661    /// `config.draft` must match this module. The transport is adopted as
662    /// given; nothing here re-checks the ALPN it was negotiated with.
663    pub async fn adopt(
664        transport: Transport,
665        config: ClientConfig,
666    ) -> Result<Self, ConnectionError> {
667        Self::adopt_offering(transport, config, None).await
668    }
669
670    /// [`Self::adopt`], offering exactly `versions` in CLIENT_SETUP.
671    ///
672    /// `None` offers what `config` implies, which is what [`Self::adopt`]
673    /// passes. `Some` replaces the list outright, and takes raw varints rather
674    /// than [`DraftVersion`]s because the reason to reach for this is to offer
675    /// a version no draft assigns — which an enum of drafts cannot name.
676    ///
677    /// A server MUST answer with a version the client offered and MUST
678    /// otherwise close the session; from draft-11 the code for that is
679    /// `VERSION_NEGOTIATION_FAILED` (0x15). How a relay spells the refusal is
680    /// a conformance measurement, and offering a version deliberately outside
681    /// the negotiable set is the only way to ask for it.
682    pub async fn adopt_offering(
683        transport: Transport,
684        config: ClientConfig,
685        versions: Option<Vec<VarInt>>,
686    ) -> Result<Self, ConnectionError> {
687        let draft = config.draft;
688        // PATH is for native QUIC only, and the transport is known here and
689        // nowhere further in. Refusing before dialling means a session that
690        // the server would close on sight is never opened.
691        setup::validate_client_path_transport(
692            &config.setup_parameters,
693            matches!(config.transport, TransportType::WebTransport { .. }),
694        )
695        .map_err(EndpointError::from)?;
696
697        // Open bidirectional control stream
698        let (send, recv) = transport.open_bi().await?;
699        let mut control_send = FramedSendStream::new(send, draft);
700        let mut control_recv = FramedRecvStream::new(recv, draft);
701
702        // Perform setup handshake
703        let mut endpoint = Endpoint::new(Role::Client);
704        endpoint.connect()?;
705        let setup_msg = endpoint.send_client_setup(
706            versions.unwrap_or_else(|| config.supported_versions()),
707            config.setup_parameters.clone(),
708        )?;
709        let any_setup = AnyControlMessage::Draft14(setup_msg);
710        let raw_setup = control_send.write_control(&any_setup).await?;
711
712        let (server_setup, raw_server_setup) = control_recv.read_control(true).await?;
713        // Unwrap to draft-14 for the endpoint (which is draft-14 only)
714        match &server_setup {
715            AnyControlMessage::Draft14(ControlMessage::ServerSetup(ref ss)) => {
716                endpoint.receive_server_setup(ss)?;
717            }
718            _ => {
719                return Err(ConnectionError::Endpoint(EndpointError::NotActive));
720            }
721        }
722
723        let mut pending_events = Vec::with_capacity(3);
724        pending_events.push(ClientEvent::ControlMessage {
725            direction: Direction::Send,
726            message: any_setup,
727            raw: Some(raw_setup),
728        });
729        pending_events.push(ClientEvent::ControlMessage {
730            direction: Direction::Receive,
731            message: server_setup.clone(),
732            raw: raw_server_setup.clone(),
733        });
734        if let Some(v) = endpoint.negotiated_version() {
735            pending_events.push(ClientEvent::SetupComplete { negotiated_version: v.into_inner() });
736        }
737
738        Ok(Self {
739            transport,
740            endpoint,
741            draft,
742            control_send: Some(tokio::sync::Mutex::new(control_send)),
743            control_recv: Some(control_recv),
744            observer: None,
745            pending_events,
746            server_setup,
747            server_setup_raw: raw_server_setup,
748        })
749    }
750
751    /// Establish a raw QUIC connection.
752    ///
753    /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
754    /// TLS and endpoint setup.
755    async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
756        let (transport, _negotiated) = crate::transport::dial_quic(
757            addr,
758            &crate::transport::QuicDialOptions {
759                skip_cert_verification: config.skip_cert_verification,
760                ca_certs: config.ca_certs.clone(),
761                ..crate::transport::QuicDialOptions::new(config.alpn())
762            },
763        )
764        .await?;
765        Ok(transport)
766    }
767
768    /// Establish a WebTransport connection.
769    ///
770    /// [`crate::transport::dial_webtransport`] holds the TLS and endpoint
771    /// setup, exactly as `connect_quic` above defers its own. That is not
772    /// only deduplication: both dials must trust the same roots. Settling trust
773    /// at this call site instead — from `wtransport`'s own builder settings, or
774    /// from a second config of this draft's own — puts the decision in two
775    /// places, where it can stop matching what the QUIC dial trusts, so one
776    /// relay would pass on one transport and fail on the other and a caller's
777    /// private CA would reach only the dials whose call site installed it.
778    /// Both ask the same function what to trust.
779    #[cfg(feature = "webtransport")]
780    async fn connect_webtransport(
781        url: &str,
782        config: &ClientConfig,
783    ) -> Result<Transport, ConnectionError> {
784        Ok(crate::transport::dial_webtransport(
785            url,
786            &crate::transport::QuicDialOptions {
787                skip_cert_verification: config.skip_cert_verification,
788                ca_certs: config.ca_certs.clone(),
789                ..crate::transport::QuicDialOptions::new(config.alpn())
790            },
791        )
792        .await?)
793    }
794
795    /// Stub for when the webtransport feature is not enabled.
796    #[cfg(not(feature = "webtransport"))]
797    async fn connect_webtransport(
798        _url: &str,
799        _config: &ClientConfig,
800    ) -> Result<Transport, ConnectionError> {
801        Err(ConnectionError::Transport(TransportError::Connect(
802            "webtransport feature not enabled".into(),
803        )))
804    }
805
806    // ── Observer ───────────────────────────────────────────────
807
808    /// Attach an observer. Buffered handshake events from `connect()` are
809    /// flushed in arrival order before this returns.
810    pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
811        self.observer = Some(observer);
812        for event in self.pending_events.drain(..) {
813            if let Some(ref obs) = self.observer {
814                obs.on_event_owned(event);
815            }
816        }
817    }
818
819    /// Remove the observer.
820    pub fn clear_observer(&mut self) {
821        self.observer = None;
822    }
823
824    /// Emit an event to the observer, if one is attached.
825    fn emit(&self, event: ClientEvent) {
826        if let Some(ref obs) = self.observer {
827            obs.on_event_owned(event);
828        }
829    }
830
831    // ── Control message I/O ─────────────────────────────────
832
833    /// Send a control message on the control stream.
834    ///
835    /// Wraps the draft-14 message in `AnyControlMessage::Draft14` for framing.
836    pub async fn send_control(&self, msg: &ControlMessage) -> Result<(), ConnectionError> {
837        let any = AnyControlMessage::Draft14(msg.clone());
838        let mut send =
839            self.control_send.as_ref().ok_or(ConnectionError::NoControlStream)?.lock().await;
840        let raw = send.write_control(&any).await?;
841        drop(send);
842        self.emit(ClientEvent::ControlMessage {
843            direction: Direction::Send,
844            message: any,
845            raw: Some(raw),
846        });
847        Ok(())
848    }
849
850    /// Read the next control message from the control stream.
851    ///
852    /// Returns the `AnyControlMessage` and also extracts the draft-14
853    /// `ControlMessage` for internal endpoint dispatch.
854    pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
855        let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
856        let capture_raw = self.observer.is_some();
857        let (any, raw) = match recv.read_control(capture_raw).await {
858            Ok(v) => v,
859            Err(e) => return Err(self.close_for_codec(e)),
860        };
861        if capture_raw {
862            self.emit(ClientEvent::ControlMessage {
863                direction: Direction::Receive,
864                message: any.clone(),
865                raw,
866            });
867        }
868        // Unwrap to draft-14 for the endpoint
869        match any {
870            AnyControlMessage::Draft14(msg) => Ok(msg),
871            // `AnyControlMessage` carries one variant per enabled draft feature. With draft 14 the
872            // only one enabled the arm above is exhaustive and this rejection arm unreachable.
873            // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
874            // naming the other drafts: such a list has to be edited in every draft module
875            // whenever a draft is added, and a copy that omits one leaves this match
876            // non-exhaustive.
877            #[allow(unreachable_patterns)]
878            _ => Err(ConnectionError::ControlMessageNarrowing),
879        }
880    }
881
882    /// Read and dispatch the next incoming control message through the endpoint
883    /// state machine. Returns the decoded message for inspection.
884    pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
885        let msg = self.recv_control().await?;
886        self.endpoint.receive_message(msg.clone()).map_err(|e| self.close_if_session_fatal(e))?;
887
888        // Emit draining event if this was a GoAway
889        if let ControlMessage::GoAway(ref ga) = msg {
890            self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
891        }
892
893        Ok(msg)
894    }
895
896    // ── Subscribe flow ──────────────────────────────────────
897
898    /// Send a SUBSCRIBE and return the allocated request ID.
899    pub async fn subscribe(
900        &mut self,
901        track_namespace: TrackNamespace,
902        track_name: Vec<u8>,
903        subscriber_priority: u8,
904        group_order: GroupOrder,
905        filter_type: FilterType,
906        parameters: Vec<KeyValuePair>,
907    ) -> Result<VarInt, ConnectionError> {
908        let (req_id, msg) = self.endpoint.subscribe(
909            track_namespace,
910            track_name,
911            subscriber_priority,
912            group_order,
913            filter_type,
914            parameters,
915        )?;
916        self.send_control(&msg).await?;
917        Ok(req_id)
918    }
919
920    /// Send a SUBSCRIBE for a range of the track, starting at a given
921    /// location.
922    ///
923    /// The Filter Type is derived from the range, so the message cannot name a
924    /// filter whose fields it does not carry.
925    #[allow(clippy::too_many_arguments)]
926    pub async fn subscribe_range(
927        &mut self,
928        track_namespace: TrackNamespace,
929        track_name: Vec<u8>,
930        subscriber_priority: u8,
931        group_order: GroupOrder,
932        start_location: Location,
933        end_group: Option<VarInt>,
934        parameters: Vec<KeyValuePair>,
935    ) -> Result<VarInt, ConnectionError> {
936        let (req_id, msg) = self.endpoint.subscribe_range(
937            track_namespace,
938            track_name,
939            subscriber_priority,
940            group_order,
941            start_location,
942            end_group,
943            parameters,
944        )?;
945        self.send_control(&msg).await?;
946        Ok(req_id)
947    }
948
949    /// Send an UNSUBSCRIBE for the given request ID.
950    pub async fn unsubscribe(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
951        let msg = self.endpoint.unsubscribe(request_id)?;
952        self.send_control(&msg).await
953    }
954
955    /// Accept a subscription the peer opened, sending SUBSCRIBE_OK and giving
956    /// its track a Track Alias.
957    ///
958    /// The endpoint refuses an alias a live track of its own already holds and
959    /// refuses a second answer to one SUBSCRIBE, so nothing is written on the
960    /// wire when it does either.
961    pub async fn subscribe_ok(
962        &mut self,
963        request_id: VarInt,
964        track_alias: VarInt,
965        expires: VarInt,
966        group_order: GroupOrder,
967        parameters: Vec<KeyValuePair>,
968    ) -> Result<(), ConnectionError> {
969        let msg = self.endpoint.send_subscribe_ok(
970            request_id,
971            track_alias,
972            expires,
973            group_order,
974            parameters,
975        )?;
976        self.send_control(&msg).await
977    }
978
979    /// Reject a subscription the peer opened, sending SUBSCRIBE_ERROR.
980    ///
981    /// The endpoint refuses a second answer to one SUBSCRIBE, so nothing is
982    /// written on the wire when it does.
983    pub async fn subscribe_error(
984        &mut self,
985        request_id: VarInt,
986        error_code: VarInt,
987        reason_phrase: Vec<u8>,
988    ) -> Result<(), ConnectionError> {
989        let msg = self.endpoint.send_subscribe_error(request_id, error_code, reason_phrase)?;
990        self.send_control(&msg).await
991    }
992
993    /// Accept a PUBLISH the peer sent, which establishes the subscription it
994    /// opened.
995    ///
996    /// The endpoint refuses a second answer to one PUBLISH, so nothing is
997    /// written on the wire when it does.
998    #[allow(clippy::too_many_arguments)]
999    pub async fn publish_ok(
1000        &mut self,
1001        request_id: VarInt,
1002        forward: Forward,
1003        subscriber_priority: u8,
1004        group_order: GroupOrder,
1005        filter_type: FilterType,
1006        start_location: Option<Location>,
1007        end_group: Option<VarInt>,
1008    ) -> Result<(), ConnectionError> {
1009        let msg = self.endpoint.send_publish_ok(
1010            request_id,
1011            forward,
1012            subscriber_priority,
1013            group_order,
1014            filter_type,
1015            start_location,
1016            end_group,
1017        )?;
1018        self.send_control(&msg).await
1019    }
1020
1021    /// Reject a PUBLISH the peer sent, which ends the subscription it opened
1022    /// before it was established.
1023    ///
1024    /// The endpoint refuses a second answer to one PUBLISH, so nothing is
1025    /// written on the wire when it does.
1026    pub async fn publish_error(
1027        &mut self,
1028        request_id: VarInt,
1029        error_code: VarInt,
1030        reason_phrase: Vec<u8>,
1031    ) -> Result<(), ConnectionError> {
1032        let msg = self.endpoint.send_publish_error(request_id, error_code, reason_phrase)?;
1033        self.send_control(&msg).await
1034    }
1035
1036    /// Send a SUBSCRIBE_UPDATE for an active subscription. Returns the
1037    /// allocated request ID of the update message.
1038    pub async fn subscribe_update(
1039        &mut self,
1040        subscription_request_id: VarInt,
1041        start_location: Location,
1042        end_group: VarInt,
1043        subscriber_priority: u8,
1044        forward: Forward,
1045        parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
1046    ) -> Result<VarInt, ConnectionError> {
1047        let (req_id, msg) = self.endpoint.subscribe_update(
1048            subscription_request_id,
1049            start_location,
1050            end_group,
1051            subscriber_priority,
1052            forward,
1053            parameters,
1054        )?;
1055        self.send_control(&msg).await?;
1056        Ok(req_id)
1057    }
1058
1059    // ── Fetch flow ──────────────────────────────────────────
1060
1061    /// Send a FETCH and return the allocated request ID.
1062    #[allow(clippy::too_many_arguments)]
1063    pub async fn fetch(
1064        &mut self,
1065        track_namespace: TrackNamespace,
1066        track_name: Vec<u8>,
1067        subscriber_priority: u8,
1068        group_order: GroupOrder,
1069        start_group: VarInt,
1070        start_object: VarInt,
1071        end_group: VarInt,
1072        end_object: VarInt,
1073        parameters: Vec<KeyValuePair>,
1074    ) -> Result<VarInt, ConnectionError> {
1075        let (req_id, msg) = self.endpoint.fetch(
1076            track_namespace,
1077            track_name,
1078            subscriber_priority,
1079            group_order,
1080            start_group,
1081            start_object,
1082            end_group,
1083            end_object,
1084            parameters,
1085        )?;
1086        self.send_control(&msg).await?;
1087        Ok(req_id)
1088    }
1089
1090    /// Send a Relative Joining Fetch and return the allocated request ID.
1091    ///
1092    /// `joining_start` is a count of groups back from the subscription's live
1093    /// edge. For the form that names the group outright, see
1094    /// [`absolute_joining_fetch`](Self::absolute_joining_fetch).
1095    pub async fn joining_fetch(
1096        &mut self,
1097        subscriber_priority: u8,
1098        group_order: GroupOrder,
1099        joining_request_id: VarInt,
1100        joining_start: VarInt,
1101        parameters: Vec<KeyValuePair>,
1102    ) -> Result<VarInt, ConnectionError> {
1103        let (req_id, msg) = self.endpoint.joining_fetch(
1104            subscriber_priority,
1105            group_order,
1106            joining_request_id,
1107            joining_start,
1108            parameters,
1109        )?;
1110        self.send_control(&msg).await?;
1111        Ok(req_id)
1112    }
1113
1114    /// Send an Absolute Joining Fetch and return the allocated request ID.
1115    ///
1116    /// `joining_start` is the group to begin at.
1117    pub async fn absolute_joining_fetch(
1118        &mut self,
1119        subscriber_priority: u8,
1120        group_order: GroupOrder,
1121        joining_request_id: VarInt,
1122        joining_start: VarInt,
1123        parameters: Vec<KeyValuePair>,
1124    ) -> Result<VarInt, ConnectionError> {
1125        let (req_id, msg) = self.endpoint.absolute_joining_fetch(
1126            subscriber_priority,
1127            group_order,
1128            joining_request_id,
1129            joining_start,
1130            parameters,
1131        )?;
1132        self.send_control(&msg).await?;
1133        Ok(req_id)
1134    }
1135
1136    /// Send a FETCH_CANCEL for the given request ID.
1137    pub async fn fetch_cancel(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1138        let msg = self.endpoint.fetch_cancel(request_id)?;
1139        self.send_control(&msg).await
1140    }
1141
1142    /// Accept a fetch the peer opened, sending FETCH_OK.
1143    ///
1144    /// The endpoint refuses a Joining Fetch naming a subscription this session
1145    /// cannot join and refuses a second answer to one FETCH, so nothing is
1146    /// written on the wire when it does either.
1147    pub async fn fetch_ok(
1148        &mut self,
1149        request_id: VarInt,
1150        group_order: GroupOrder,
1151        end_of_track: u8,
1152        end_location: Location,
1153        parameters: Vec<KeyValuePair>,
1154    ) -> Result<(), ConnectionError> {
1155        let msg = self.endpoint.send_fetch_ok(
1156            request_id,
1157            group_order,
1158            end_of_track,
1159            end_location,
1160            parameters,
1161        )?;
1162        self.send_control(&msg).await
1163    }
1164
1165    /// Refuse a fetch the peer opened, sending FETCH_ERROR.
1166    ///
1167    /// The endpoint refuses a second answer to one FETCH, and refuses a
1168    /// Joining Fetch's refusal under any code but the one the draft names for
1169    /// it, so nothing is written on the wire when it does either.
1170    pub async fn fetch_error(
1171        &mut self,
1172        request_id: VarInt,
1173        error_code: VarInt,
1174        reason_phrase: Vec<u8>,
1175    ) -> Result<(), ConnectionError> {
1176        let msg = self.endpoint.send_fetch_error(request_id, error_code, reason_phrase)?;
1177        self.send_control(&msg).await
1178    }
1179
1180    // ── Namespace flows ─────────────────────────────────────
1181
1182    /// Send a SUBSCRIBE_NAMESPACE and return the request ID.
1183    pub async fn subscribe_namespace(
1184        &mut self,
1185        track_namespace: TrackNamespace,
1186        parameters: Vec<KeyValuePair>,
1187    ) -> Result<VarInt, ConnectionError> {
1188        let (req_id, msg) = self.endpoint.subscribe_namespace(track_namespace, parameters)?;
1189        self.send_control(&msg).await?;
1190        Ok(req_id)
1191    }
1192
1193    /// Accept a namespace subscription the peer made, sending SUBSCRIBE_NAMESPACE_OK.
1194    ///
1195    /// The endpoint refuses a second answer to one SUBSCRIBE_NAMESPACE, so nothing is
1196    /// written on the wire when it does.
1197    pub async fn subscribe_namespace_ok(
1198        &mut self,
1199        request_id: VarInt,
1200    ) -> Result<(), ConnectionError> {
1201        let msg = self.endpoint.send_subscribe_namespace_ok(request_id)?;
1202        self.send_control(&msg).await
1203    }
1204
1205    /// Refuse a namespace subscription the peer made, sending SUBSCRIBE_NAMESPACE_ERROR.
1206    ///
1207    /// The other half of the same sentence: one answer, and this is the other
1208    /// one it can be.
1209    pub async fn subscribe_namespace_error(
1210        &mut self,
1211        request_id: VarInt,
1212        error_code: VarInt,
1213        reason_phrase: Vec<u8>,
1214    ) -> Result<(), ConnectionError> {
1215        let msg =
1216            self.endpoint.send_subscribe_namespace_error(request_id, error_code, reason_phrase)?;
1217        self.send_control(&msg).await
1218    }
1219
1220    /// Send a PUBLISH_NAMESPACE and return the request ID.
1221    pub async fn publish_namespace(
1222        &mut self,
1223        track_namespace: TrackNamespace,
1224        parameters: Vec<KeyValuePair>,
1225    ) -> Result<VarInt, ConnectionError> {
1226        let (req_id, msg) = self.endpoint.publish_namespace(track_namespace, parameters)?;
1227        self.send_control(&msg).await?;
1228        Ok(req_id)
1229    }
1230
1231    /// Accept an announcement the peer made, sending PUBLISH_NAMESPACE_OK.
1232    ///
1233    /// The endpoint refuses a second answer to one PUBLISH_NAMESPACE, so
1234    /// nothing is written on the wire when it does.
1235    pub async fn publish_namespace_ok(
1236        &mut self,
1237        request_id: VarInt,
1238    ) -> Result<(), ConnectionError> {
1239        let msg = self.endpoint.send_publish_namespace_ok(request_id)?;
1240        self.send_control(&msg).await
1241    }
1242
1243    /// Refuse an announcement the peer made, sending PUBLISH_NAMESPACE_ERROR.
1244    ///
1245    /// The other half of the same sentence: one answer, and this is the other
1246    /// one it can be.
1247    pub async fn publish_namespace_error(
1248        &mut self,
1249        request_id: VarInt,
1250        error_code: VarInt,
1251        reason_phrase: Vec<u8>,
1252    ) -> Result<(), ConnectionError> {
1253        let msg =
1254            self.endpoint.send_publish_namespace_error(request_id, error_code, reason_phrase)?;
1255        self.send_control(&msg).await
1256    }
1257
1258    /// Revoke an acceptance, sending PUBLISH_NAMESPACE_CANCEL.
1259    ///
1260    /// The endpoint refuses one for an announcement it never accepted, so
1261    /// nothing is written on the wire when it does.
1262    pub async fn publish_namespace_cancel(
1263        &mut self,
1264        track_namespace: TrackNamespace,
1265        error_code: VarInt,
1266        reason_phrase: Vec<u8>,
1267    ) -> Result<(), ConnectionError> {
1268        let msg =
1269            self.endpoint.publish_namespace_cancel(track_namespace, error_code, reason_phrase)?;
1270        self.send_control(&msg).await
1271    }
1272
1273    /// Withdraw an announcement this endpoint made, sending
1274    /// PUBLISH_NAMESPACE_DONE.
1275    ///
1276    /// The mirror of [`Self::publish_namespace`], and the counterpart of
1277    /// [`Self::publish_namespace_cancel`]: this one ends an announcement of
1278    /// this endpoint's, that one revokes the acceptance of one the peer made.
1279    pub async fn publish_namespace_done(
1280        &mut self,
1281        track_namespace: TrackNamespace,
1282    ) -> Result<(), ConnectionError> {
1283        let msg = self.endpoint.publish_namespace_done(track_namespace)?;
1284        self.send_control(&msg).await
1285    }
1286    // ── Track Status flow ────────────────────────────────────
1287
1288    /// Send a TRACK_STATUS and return the allocated request ID.
1289    #[allow(clippy::too_many_arguments)]
1290    pub async fn track_status(
1291        &mut self,
1292        track_namespace: TrackNamespace,
1293        track_name: Vec<u8>,
1294        subscriber_priority: u8,
1295        group_order: GroupOrder,
1296        forward: Forward,
1297        filter_type: FilterType,
1298        parameters: Vec<KeyValuePair>,
1299    ) -> Result<VarInt, ConnectionError> {
1300        let (req_id, msg) = self.endpoint.track_status(
1301            track_namespace,
1302            track_name,
1303            subscriber_priority,
1304            group_order,
1305            forward,
1306            filter_type,
1307            parameters,
1308        )?;
1309        self.send_control(&msg).await?;
1310        Ok(req_id)
1311    }
1312
1313    /// Accept a track status the peer asked for, sending TRACK_STATUS_OK.
1314    ///
1315    /// The endpoint refuses a second answer to one request, so nothing is
1316    /// written on the wire when it does. It also chooses the Track Alias the
1317    /// message carries, because the draft leaves only one value open.
1318    pub async fn track_status_ok(
1319        &mut self,
1320        request_id: VarInt,
1321        expires: VarInt,
1322        group_order: GroupOrder,
1323        parameters: Vec<KeyValuePair>,
1324    ) -> Result<(), ConnectionError> {
1325        let msg =
1326            self.endpoint.send_track_status_ok(request_id, expires, group_order, parameters)?;
1327        self.send_control(&msg).await
1328    }
1329
1330    /// Refuse a track status the peer asked for, sending TRACK_STATUS_ERROR.
1331    ///
1332    /// The other answer the request can have, and the endpoint holds it to the
1333    /// same count of one.
1334    pub async fn track_status_error(
1335        &mut self,
1336        request_id: VarInt,
1337        error_code: VarInt,
1338        reason_phrase: Vec<u8>,
1339    ) -> Result<(), ConnectionError> {
1340        let msg = self.endpoint.send_track_status_error(request_id, error_code, reason_phrase)?;
1341        self.send_control(&msg).await
1342    }
1343
1344    // ── Publish flow (publisher side) ───────────────────────
1345
1346    /// Offer the peer a subscription to a track this endpoint publishes, and
1347    /// return the Request ID the offer was allocated.
1348    ///
1349    /// Nothing is written on the wire when the endpoint refuses to build the
1350    /// offer, which it does when the Track Alias is one another live track of
1351    /// this session already holds.
1352    #[allow(clippy::too_many_arguments)]
1353    pub async fn publish(
1354        &mut self,
1355        track_namespace: TrackNamespace,
1356        track_name: Vec<u8>,
1357        track_alias: VarInt,
1358        group_order: GroupOrder,
1359        largest_location: Option<Location>,
1360        forward: Forward,
1361        parameters: Vec<KeyValuePair>,
1362    ) -> Result<VarInt, ConnectionError> {
1363        let (req_id, msg) = self.endpoint.publish(
1364            track_namespace,
1365            track_name,
1366            track_alias,
1367            group_order,
1368            largest_location,
1369            forward,
1370            parameters,
1371        )?;
1372        self.send_control(&msg).await?;
1373        Ok(req_id)
1374    }
1375
1376    /// Send a PUBLISH_DONE for the given request ID.
1377    pub async fn publish_done(
1378        &mut self,
1379        request_id: VarInt,
1380        status_code: VarInt,
1381        reason_phrase: Vec<u8>,
1382    ) -> Result<(), ConnectionError> {
1383        let msg = self.endpoint.send_publish_done(request_id, status_code, reason_phrase)?;
1384        self.send_control(&msg).await
1385    }
1386
1387    // ── Malformed Tracks ────────────────────────────────────
1388
1389    /// Send what Section 2.5 asks for when this endpoint finds a track
1390    /// malformed.
1391    ///
1392    /// "it MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for
1393    /// that Track from that publisher" — one message per request, in Request
1394    /// ID order, and the endpoint decides which message each request takes.
1395    ///
1396    /// A write that fails is not reported. The caller is on its way to
1397    /// returning an error that says what went wrong with the track, and a
1398    /// control stream that will not take an UNSUBSCRIBE is a session on its
1399    /// way out for a reason of its own; replacing the condition's report with
1400    /// a transport error would lose the only account of why the track was
1401    /// withdrawn. The rest of the withdrawal is abandoned, because a stream
1402    /// that refused one message will refuse the next.
1403    async fn withdraw_malformed_track(&self, alias: u64, condition: MalformedTrackCondition) {
1404        for msg in self.endpoint.withdraw_malformed_track(alias, condition) {
1405            if self.send_control(&msg).await.is_err() {
1406                break;
1407            }
1408        }
1409    }
1410
1411    /// Record the framing an arriving object was sent with, and withdraw from
1412    /// the track when it is the second framing that track has been sent.
1413    ///
1414    /// The receiving half of a pair. The two writing paths call the endpoint
1415    /// directly and answer a mixed track by refusing to write it, because
1416    /// Section 2.5's sentence is a subscriber's: an endpoint about to send an
1417    /// object is that object's Original Publisher, and a publisher has no
1418    /// subscription of its own to withdraw and no fetch of its own to cancel.
1419    async fn note_received_framing(
1420        &self,
1421        alias: u64,
1422        seen: ObjectForwardingPreference,
1423    ) -> Result<(), ConnectionError> {
1424        let Err(err) = self.endpoint.note_object_forwarding_preference(alias, seen) else {
1425            return Ok(());
1426        };
1427        self.withdraw_malformed_track(alias, MalformedTrackCondition::MixedForwardingPreference)
1428            .await;
1429        Err(err.into())
1430    }
1431
1432    // ── Data streams ────────────────────────────────────────
1433
1434    /// Open a new unidirectional stream for sending subgroup data.
1435    pub async fn open_subgroup_stream(
1436        &self,
1437        header: &AnySubgroupHeader,
1438    ) -> Result<FramedSendStream, ConnectionError> {
1439        // Before the stream is opened: the Original Publisher is who the rule
1440        // binds, so a header that would mix this track's framing is refused
1441        // here rather than written and answered by the peer.
1442        self.endpoint.note_object_forwarding_preference(
1443            header.track_alias(),
1444            ObjectForwardingPreference::Subgroup,
1445        )?;
1446        let send = self.transport.open_uni().await?;
1447        let mut framed = FramedSendStream::new(send, self.draft);
1448        let sid = framed.stream_id();
1449        framed.write_subgroup_header(header).await?;
1450        self.emit(ClientEvent::StreamOpened {
1451            direction: Direction::Send,
1452            stream_kind: StreamKind::Subgroup,
1453            stream_id: sid,
1454        });
1455        self.emit(ClientEvent::DataStreamHeader {
1456            stream_id: sid,
1457            direction: Direction::Send,
1458            header: header.clone(),
1459        });
1460        Ok(framed)
1461    }
1462
1463    /// Open a new unidirectional stream for sending a FETCH's objects.
1464    ///
1465    /// The objects answering a FETCH do not go on the request's own stream:
1466    /// they go on a unidirectional stream of their own, which opens with a
1467    /// FETCH_HEADER naming the request they belong to. This writes that header
1468    /// and hands back the stream, the same way
1469    /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
1470    /// subgroup.
1471    ///
1472    /// The caller owns the stream that comes back. Nothing here remembers
1473    /// which request it belongs to, so an endpoint serving several fetches at
1474    /// once keeps its own map from Request ID to stream.
1475    pub async fn open_fetch_stream(
1476        &self,
1477        header: &AnyFetchHeader,
1478    ) -> Result<FramedSendStream, ConnectionError> {
1479        let send = self.transport.open_uni().await?;
1480        let mut framed = FramedSendStream::new(send, self.draft);
1481        let sid = framed.stream_id();
1482        framed.write_fetch_header(header).await?;
1483        self.emit(ClientEvent::StreamOpened {
1484            direction: Direction::Send,
1485            stream_kind: StreamKind::Fetch,
1486            stream_id: sid,
1487        });
1488        Ok(framed)
1489    }
1490
1491    /// Accept the next unidirectional stream and read its fetch header.
1492    ///
1493    /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
1494    /// are separate because the header decides how every object after it is
1495    /// framed, so a caller has to know which it is expecting before the first
1496    /// byte is read.
1497    ///
1498    /// Objects come off the returned stream with
1499    /// [`FramedRecvStream::read_fetch_object`].
1500    pub async fn accept_fetch_stream(
1501        &self,
1502    ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
1503        let recv = self.transport.accept_uni().await?;
1504        let mut framed = FramedRecvStream::new(recv, self.draft);
1505        let sid = framed.stream_id();
1506        let header = framed.read_fetch_header().await?;
1507        self.emit(ClientEvent::StreamOpened {
1508            direction: Direction::Receive,
1509            stream_kind: StreamKind::Fetch,
1510            stream_id: sid,
1511        });
1512        self.emit(ClientEvent::FetchStreamHeader {
1513            stream_id: sid,
1514            direction: Direction::Receive,
1515            header: header.clone(),
1516        });
1517        // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
1518        // carries an `AnySubgroupHeader` and cannot express one. What
1519        // `accept_subgroup_stream` does beyond this - the forwarding-preference
1520        // note, the object measurement - is about a subgroup and has no
1521        // counterpart on a fetch stream.
1522        Ok((header, framed))
1523    }
1524
1525    /// Accept an incoming unidirectional data stream and read its subgroup
1526    /// header.
1527    pub async fn accept_subgroup_stream(
1528        &self,
1529    ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
1530        let recv = self.transport.accept_uni().await?;
1531        let mut framed = FramedRecvStream::new(recv, self.draft);
1532        let sid = framed.stream_id();
1533        let header = framed.read_subgroup_header().await?;
1534        self.emit(ClientEvent::StreamOpened {
1535            direction: Direction::Receive,
1536            stream_kind: StreamKind::Subgroup,
1537            stream_id: sid,
1538        });
1539        self.emit(ClientEvent::DataStreamHeader {
1540            stream_id: sid,
1541            direction: Direction::Receive,
1542            header: header.clone(),
1543        });
1544        // Every object on a subgroup stream has the Subgroup preference, so
1545        // the header settles the track's framing before a single object is
1546        // read.
1547        self.note_received_framing(header.track_alias(), ObjectForwardingPreference::Subgroup)
1548            .await?;
1549        // The track is resolved here and not inside the stream: it takes the
1550        // endpoint's alias table, which a stream handle has no way back to.
1551        if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
1552            framed.measure_objects_against(objects, header.group_id());
1553        }
1554        Ok((header, framed))
1555    }
1556
1557    /// Send an object via datagram.
1558    ///
1559    /// The header goes through `AnyDatagramHeader::encode`, which refuses a
1560    /// header whose Object Status the framing it names cannot carry. Such a
1561    /// header errors here and nothing is sent, rather than going out as an
1562    /// ordinary payload datagram with the status quietly dropped.
1563    pub fn send_datagram(
1564        &self,
1565        header: &AnyDatagramHeader,
1566        payload: &[u8],
1567    ) -> Result<(), ConnectionError> {
1568        // Before anything is encoded, for the reason `open_subgroup_stream`
1569        // gives.
1570        self.endpoint.note_object_forwarding_preference(
1571            header.meta().track_alias,
1572            ObjectForwardingPreference::Datagram,
1573        )?;
1574        let mut buf = Vec::new();
1575        header.encode(&mut buf)?;
1576        buf.extend_from_slice(payload);
1577        self.emit(ClientEvent::DatagramReceived {
1578            direction: Direction::Send,
1579            header: header.clone(),
1580            payload_len: payload.len(),
1581        });
1582        self.transport.send_datagram(bytes::Bytes::from(buf))?;
1583        Ok(())
1584    }
1585
1586    /// Receive a datagram and decode its header.
1587    pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
1588        let data = self.transport.recv_datagram().await?;
1589        let mut cursor = &data[..];
1590        let header = AnyDatagramHeader::decode(self.draft, &mut cursor)?;
1591        let consumed = data.len() - cursor.len();
1592        let payload = data.slice(consumed..);
1593        self.emit(ClientEvent::DatagramReceived {
1594            direction: Direction::Receive,
1595            header: header.clone(),
1596            payload_len: payload.len(),
1597        });
1598        // A datagram is the other framing, and it settles the track's just as a
1599        // subgroup header does.
1600        self.note_received_framing(header.meta().track_alias, ObjectForwardingPreference::Datagram)
1601            .await?;
1602        // A datagram is a whole object, so the connection can measure it
1603        // without help from the caller - and answer the condition itself,
1604        // because an UNSUBSCRIBE takes the connection an object on a stream
1605        // cannot reach.
1606        let meta = header.meta();
1607        if let Err(err) = self.endpoint.note_received_object(
1608            meta.track_alias,
1609            ObjectLocation { group: meta.group_id, object: meta.object_id },
1610            object_role(meta.status),
1611        ) {
1612            self.withdraw_malformed_track(
1613                meta.track_alias,
1614                MalformedTrackCondition::ObjectPastFinalObject,
1615            )
1616            .await;
1617            return Err(err.into());
1618        }
1619        Ok((header, payload))
1620    }
1621
1622    // ── Accessors ───────────────────────────────────────────
1623
1624    /// Access the underlying endpoint state machine.
1625    pub fn endpoint(&self) -> &Endpoint {
1626        &self.endpoint
1627    }
1628
1629    /// Mutable access to the endpoint state machine.
1630    pub fn endpoint_mut(&mut self) -> &mut Endpoint {
1631        &mut self.endpoint
1632    }
1633
1634    /// The SETUP message the server answered the handshake with.
1635    ///
1636    /// `SERVER_SETUP` through draft-16, the server's half of the unified
1637    /// `SETUP` from draft-17. [`AnyControlMessage::fields`] renders it under
1638    /// this draft's own parameter names, in the order they arrived.
1639    pub fn server_setup(&self) -> &AnyControlMessage {
1640        &self.server_setup
1641    }
1642
1643    /// The framed wire bytes of [`Self::server_setup`], as they arrived.
1644    ///
1645    /// Kept beside the decoded form because the encoding is evidence the
1646    /// decoding discards: two relays sending the same parameter can still
1647    /// disagree on how wide a varint they wrote it in.
1648    pub fn server_setup_raw(&self) -> Option<&[u8]> {
1649        self.server_setup_raw.as_deref()
1650    }
1651
1652    /// Get the negotiated MoQT version.
1653    pub fn negotiated_version(&self) -> Option<VarInt> {
1654        self.endpoint.negotiated_version()
1655    }
1656
1657    /// Returns the draft version this connection is using.
1658    pub fn draft(&self) -> DraftVersion {
1659        self.draft
1660    }
1661
1662    /// Which of this draft's *own* `ConnectionError` variants this error is,
1663    /// and which kind of thing it says.
1664    ///
1665    /// Draft-14 adds none. Every variant of its [`ConnectionError`] is one of the
1666    /// ten every draft carries, and [`AnyConnectionError`] classifies those
1667    /// itself — so `None` here is this draft's answer rather than a stub, and it
1668    /// stays right for exactly as long as that list does.
1669    ///
1670    /// Matched exhaustively, with no wildcard arm and deliberately so: a
1671    /// variant added to this draft's error type has to arrive here as a compile
1672    /// error, beside the doc comment that quotes the sentence it enforces,
1673    /// rather than as a silent [`ErrorCause::Unclassified`] in the facade.
1674    ///
1675    /// [`AnyConnectionError`]: crate::dispatch::AnyConnectionError
1676    /// [`ErrorCause::Unclassified`]: crate::dispatch::ErrorCause::Unclassified
1677    pub fn draft_specific_cause(
1678        err: &ConnectionError,
1679    ) -> Option<crate::above_codec_rules::DraftSpecificCause> {
1680        match err {
1681            ConnectionError::Endpoint(_)
1682            | ConnectionError::Codec(_)
1683            | ConnectionError::Transport(_)
1684            | ConnectionError::VarInt(_)
1685            | ConnectionError::NoControlStream
1686            | ConnectionError::UnexpectedEnd
1687            | ConnectionError::StreamFinished
1688            | ConnectionError::InvalidAddress(_)
1689            | ConnectionError::TlsConfig(_)
1690            | ConnectionError::DataStreamState(_) => None,
1691            // This build decoding a message and then failing to narrow it to
1692            // its own draft. Nothing reached the wire and no peer is
1693            // implicated, which is the whole reason it is not
1694            // `ConnectionError::Codec`: under that name it would carry
1695            // `Some(PROTOCOL_VIOLATION)` out of `codec_session_error_code` and
1696            // publish a relay for this build's defect. See the variant's own
1697            // doc.
1698            ConnectionError::ControlMessageNarrowing => {
1699                Some(crate::above_codec_rules::DraftSpecificCause::LocalRefusal)
1700            }
1701        }
1702    }
1703
1704    /// The code to close the session with when a message could not be decoded
1705    /// because the peer broke a rule draft-14 answers with a close.
1706    ///
1707    /// Every variant listed here comes from a sentence in this draft that names
1708    /// the consequence, and the list is per draft: answering a bound this draft
1709    /// does not state would close a session over traffic a conforming peer may
1710    /// send.
1711    ///
1712    ///   - Reason Phrase, maximum 1024 bytes: "If an endpoint receives a length
1713    ///     exceeding the maximum, it MUST close the session with a
1714    ///     PROTOCOL_VIOLATION."
1715    ///   - GOAWAY New Session URI, maximum 8,192 bytes, with the same sentence.
1716    ///     Drafts 11 through 19 state it; 07 through 10 state no maximum for
1717    ///     the field at all.
1718    ///   - Key-Value-Pair value, maximum 2^16-1 bytes, with the same sentence.
1719    ///   - Track Namespace tuple size: "If an endpoint receives a Track
1720    ///     Namespace tuple with an N of 0 or more than 32, it MUST close the
1721    ///     session with a Protocol Violation." Note the lower bound - an empty
1722    ///     tuple is refused here, where drafts 17 and later permit one.
1723    ///   - Full Track Name, maximum 4,096 bytes, "computed as the sum of the
1724    ///     lengths of each Track Namespace tuple field and the Track Name
1725    ///     length field". This draft bounds the pair and not the namespace
1726    ///     alone; draft-16 widened it.
1727    ///   - Duplicate parameters, a SHOULD rather than a MUST: "Receivers SHOULD
1728    ///     check that there are no unauthorized duplicate parameters and close
1729    ///     the session as a PROTOCOL_VIOLATION if found." The rule is
1730    ///     asymmetric here - "Receivers MUST allow duplicates of unknown
1731    ///     parameters", and one known type is granted repeats - and the codec
1732    ///     reports only the repeats this draft actually forbids.
1733    ///   - Unknown control message type: "An endpoint that receives an unknown
1734    ///     message type MUST close the session."
1735    ///   - Extension headers on an Object whose status is Object Does Not
1736    ///     Exist, Section 10.2.1.2. That one arrives on a data stream or a
1737    ///     datagram, so [`Connection::close_for_data_stream`] is what carries
1738    ///     it.
1739    ///
1740    /// **Not** the zero-length Track Namespace Field, the delta-encoded
1741    /// parameter type overflow, or the Object ID delta wrap. Those enter the
1742    /// specification at drafts 16 and 18, and this draft states none of them.
1743    ///
1744    /// **Not** the 2^16-1 control message length either. This draft states the
1745    /// limit - "the total length of a control message is limited to 2^16-1
1746    /// bytes" - and states no consequence for exceeding it, so an oversized
1747    /// message is refused by the decoder and stops there.
1748    ///
1749    /// **Not** [`CodecError::UnexpectedEnd`], which reports no rule at all: the
1750    /// reader raises it whenever a message is still arriving, and
1751    /// `read_control` loops on it. Closing over it would end a session on an
1752    /// ordinary short read.
1753    ///
1754    /// **Not** the unknown Message Parameter rule. Drafts 16 through 19 require
1755    /// a close for a Message Parameter whose type the negotiated version does
1756    /// not define. This draft states the opposite and states it about the same
1757    /// parameters: "Receivers MUST allow duplicates of unknown parameters",
1758    /// which presumes an unknown parameter arrives and is carried. Refusing one
1759    /// here would close a session over an extension this draft leaves room for.
1760    ///
1761    /// `None` for everything else, including [`CodecError::InvalidField`]. That
1762    /// variant is shared by a dozen unrelated malformations, only some of which
1763    /// the draft answers with a close, so a session cannot be ended on it
1764    /// without ending sessions the draft does not ask to be ended. Splitting it
1765    /// is the way to bring the rest of those rules under this function;
1766    /// widening the match is not - the extension-header rule above is in this
1767    /// table because it has a variant of its own.
1768    pub fn codec_session_error_code(
1769        err: &CodecError,
1770    ) -> Option<moqtap_codec::draft14::error_codes::SessionErrorCode> {
1771        use moqtap_codec::draft14::error_codes::SessionErrorCode;
1772        use moqtap_codec::kvp::KvpError;
1773        match err {
1774            // The declared Length disagreeing with the fields, which every
1775            // draft answers with a close. Drafts 07 through 10 name no code for
1776            // it, so it takes the one their other unnamed rules take.
1777            // A Filter Type outside the four this draft assigns, Section 9.7:
1778            // "An endpoint that receives a filter type other than the above MUST
1779            // be close the session with PROTOCOL_VIOLATION" — the missing word
1780            // is the draft's. This is the draft the rule gained a consequence
1781            // on. Drafts 07 through 13 write the same sentence as "MUST be
1782            // treated as error", which names none - draft-13 Section 8.7 among
1783            // them - and their tables leave it unanswered.
1784            CodecError::InvalidFilterType(_) => Some(SessionErrorCode::ProtocolViolation),
1785            // A Fetch Type outside the three this draft assigns, Section
1786            // 9.16: "An endpoint that receives a Fetch Type other than 0x1,
1787            // 0x2 or 0x3 MUST be close the session with a PROTOCOL_VIOLATION."
1788            // The missing word is the draft's, as it is for the filter type
1789            // above. The value decides which
1790            // fields follow it — a Standalone fetch carries a track name and a
1791            // range where a joining fetch carries a Request ID and an offset —
1792            // so a reader that cannot name the type cannot find the end of the
1793            // message.
1794            CodecError::InvalidFetchType(_) => Some(SessionErrorCode::ProtocolViolation),
1795            CodecError::ControlMessageLengthMismatch { .. } => {
1796                Some(SessionErrorCode::ProtocolViolation)
1797            }
1798            CodecError::ReasonPhraseTooLong
1799            | CodecError::GoAwayUriTooLong
1800            | CodecError::InvalidNamespaceTupleSize(_)
1801            | CodecError::TrackNameTooLong
1802            | CodecError::DuplicateParameter(_)
1803            | CodecError::UnknownMessageType(_)
1804            | CodecError::ExtensionsOnNonExistentObject(_)
1805            | CodecError::Kvp(KvpError::ValueTooLong(_)) => {
1806                Some(SessionErrorCode::ProtocolViolation)
1807            }
1808            // An unknown data-plane type, Section 10: "An endpoint that
1809            // receives an unknown stream or datagram type MUST close the
1810            // session." One sentence covering two tables, which is why both
1811            // variants sit here.
1812            // A Content Exists field that is neither zero nor one, Sections 9.8
1813            // and 9.13: "Any other value is a protocol error and MUST terminate
1814            // the session with a PROTOCOL_VIOLATION".
1815            CodecError::InvalidContentExists(_) => Some(SessionErrorCode::ProtocolViolation),
1816            // A Forward field that is neither zero nor one. Sections 9.7 and
1817            // 9.10 use the sentence above; Section 9.13 says "Any value other
1818            // than 0 or 1 is a PROTOCOL_VIOLATION". Section 9.14 names the two
1819            // legal values without a consequence and is answered the same way,
1820            // for the reason given on the variant. Draft-15 replaces the field
1821            // with the FORWARD parameter, which carries the same rule in a
1822            // different shape and reports it as `ParameterValueOutOfRange`.
1823            CodecError::InvalidForward(_) => Some(SessionErrorCode::ProtocolViolation),
1824            CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
1825                Some(SessionErrorCode::ProtocolViolation)
1826            }
1827            // A key-value pair whose value is not the serialization its own
1828            // Type defines, Section 1.4.2: "If a receiver understands a Type,
1829            // and the following Value or Length/Value does not match the
1830            // serialization defined by that Type, the receiver MUST terminate the
1831            // session with error code KEY_VALUE_FORMATTING_ERROR."
1832            //
1833            // Section 9.2.1.1 states the same answer for the one structure this
1834            // draft spells out: "If the Token structure cannot be decoded, the
1835            // receiver MUST close the Session with Key-Value Formatting error."
1836            //
1837            // The one rule in this table that names a code other than Protocol
1838            // Violation.
1839            CodecError::KeyValueFormatting { .. } => {
1840                Some(SessionErrorCode::KeyValueFormattingError)
1841            }
1842            // Everything this draft does not answer, named rather than swept up
1843            // by a wildcard. The arm is exhaustive deliberately: a new
1844            // `CodecError` variant will not compile until it has been placed on
1845            // one side or the other, on this draft, which is the decision a `_`
1846            // arm makes silently and invisibly in every draft module at once.
1847            //
1848            // Adding one variant to `CodecError` produces an `E0004` in every
1849            // draft module that matches it exhaustively, each naming the
1850            // variant that has nowhere to go. That is the whole mechanism.
1851            //
1852            // The nesting stops at `VarInt`, whose variants report how the bytes
1853            // ran out rather than a rule an endpoint states, so there is nothing
1854            // in it for a draft to answer. `Kvp` is spelled out because it does
1855            // carry one.
1856            // Not `ParameterValueOutOfRange`: no parameter this draft defines
1857            // restricts its value's range. Forwarding is a message field here
1858            // and is answered above.
1859            CodecError::ParameterValueOutOfRange { .. }
1860            | CodecError::UnexpectedEnd
1861            | CodecError::MessageTooLong(_)
1862            | CodecError::VarInt(_)
1863            | CodecError::InvalidField
1864            | CodecError::EmptyNamespaceField
1865            | CodecError::InvalidRange(..)
1866            | CodecError::ParameterLengthMismatch(_)
1867            | CodecError::EndOfTrackObjectId(_)
1868            | CodecError::KeyDeltaOverflow(..)
1869            // Not `TrackPropertyValueOutOfRange`: this draft has neither
1870            // namespace the variant is about. Draft-16 opens an extension header
1871            // registry with value rules of its own, and draft-17 renames it to
1872            // the Track Property registry. Before that, everything with a
1873            // restricted range is either a message field or a Message Parameter.
1874            | CodecError::TrackPropertyValueOutOfRange { .. }
1875            | CodecError::ParametersOutOfOrder(..)
1876            | CodecError::ObjectIdOverflow(..)
1877            | CodecError::InvalidRequiredRequestIdDelta(..)
1878            | CodecError::InvalidStreamTypeValue { .. }
1879            | CodecError::InvalidDatagramTypeValue { .. }
1880            | CodecError::UnknownMessageParameter(_)
1881            // Not `ParameterOutOfScope`: this draft states the scope rule and
1882            // answers it the other way. Section 9.2.1 Version Specific Parameters: "Each
1883            // version-specific parameter definition indicates the message types in which it can
1884            // appear. If it appears in some other type of message, it MUST be
1885            // ignored." The codec carries such a parameter on this draft and never
1886            // raises the variant, so this arm records a rule this draft has and
1887            // does not close over, not one it is missing. Draft-17 is where the
1888            // second sentence becomes a close.
1889            | CodecError::ParameterOutOfScope { .. }
1890            // Both belong to the parameter form of the filter. Draft-15 moved
1891            // the Filter Type, Start Location and End Group out of SUBSCRIBE and
1892            // into one length-prefixed parameter; this draft still carries them
1893            // as fields, where a value that runs short is a truncated frame and
1894            // an End Group is written out rather than added to anything.
1895            | CodecError::SubscriptionFilterMalformed { .. }
1896            | CodecError::FilterEndGroupOverflow { .. }
1897            // The object payload rule, Section 10.2.1.1: "Any object with a status
1898            // code other than zero MUST have an empty payload." A MUST on the
1899            // sender with no receiver action named anywhere — the "SHOULD be
1900            // treated as a protocol error" in the same paragraph belongs to the
1901            // sentence before it, which is about a status value this draft does
1902            // not assign — so an object carrying a payload it may not is refused
1903            // and the session stays open.
1904            | CodecError::PayloadNotPermitted { .. }
1905            | CodecError::UnsupportedDraft(_)
1906            | CodecError::Kvp(
1907                KvpError::MissingLength | KvpError::UnexpectedEnd | KvpError::VarInt(_),
1908            ) => None,
1909        }
1910    }
1911
1912    /// Close the session on the wire when a decode failure is one draft-14
1913    /// answers with a close, and hand the error back unchanged.
1914    /// Without it every bound the decoder enforces would stop at *this endpoint
1915    /// refused the frame* while the peer, which is the one that broke the rule,
1916    /// saw a session that was still open and went on sending. "MUST close the
1917    /// session" is a statement about the wire.
1918    fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
1919        if let ConnectionError::Codec(inner) = &err {
1920            if let Some(code) = Self::codec_session_error_code(inner) {
1921                // QUIC application error codes are 62-bit; every code in this
1922                // registry is far below `u32::MAX`, and saturating rather than
1923                // truncating means a future code that is not could never be
1924                // reported as a different, assigned one.
1925                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1926                self.close(wire_code, inner.to_string().as_bytes());
1927            }
1928        }
1929        err
1930    }
1931
1932    /// Close the session on the wire when the endpoint says a violation is
1933    /// fatal to it, and hand the error back unchanged.
1934    ///
1935    /// [`EndpointError::session_error_code`] answers `Some` for exactly the
1936    /// errors this draft ends the session over, and the endpoint has already
1937    /// moved its own state machine to Closed by the time this runs. Without
1938    /// this step that move is purely internal: the local endpoint refuses to
1939    /// start anything new while the peer, which is the one that broke the
1940    /// rule, sees a session that is still open and goes on sending. A rule
1941    /// that names a session termination code is a statement about the wire,
1942    /// so it takes a CONNECTION_CLOSE to satisfy it.
1943    ///
1944    /// The reason phrase is the error's own `Display` text, which names the
1945    /// rule rather than repeating the numeric code the close already carries.
1946    ///
1947    /// Errors that answer `None` are recoverable and nothing is sent.
1948    fn close_for(&self, err: &EndpointError) {
1949        if let Some(code) = err.session_error_code() {
1950            // QUIC application error codes are 62-bit; every code in this
1951            // registry is far below `u32::MAX`, and saturating rather than
1952            // truncating means a future code that is not could never be
1953            // reported as a different, assigned one.
1954            let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1955            self.close(wire_code, err.to_string().as_bytes());
1956        }
1957    }
1958
1959    /// [`close_for`](Self::close_for), then the error unchanged, for the
1960    /// common case where the endpoint's error is also what the caller returns.
1961    fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
1962        self.close_for(&err);
1963        ConnectionError::Endpoint(err)
1964    }
1965
1966    /// Withdraw from a track a data stream found malformed, reporting whether
1967    /// it did.
1968    ///
1969    /// The Malformed Track twin of [`Connection::close_for_data_stream`], and
1970    /// separate from it for the same reason and one more. The same one: a
1971    /// [`FramedRecvStream`] holds no connection, so the reader that finds the
1972    /// fault is not the object that can send an UNSUBSCRIBE. The one more: the
1973    /// two answers are opposites - that call ends the session, this one gives
1974    /// up a track and leaves it running - and a single entry point would have
1975    /// to decide between them from the error alone, which is exactly the
1976    /// decision a caller reproducing a capture wants to make itself.
1977    ///
1978    /// The datagram path needs none of this. It is read through the connection,
1979    /// so [`Connection::recv_datagram`] answers the condition where it finds
1980    /// it, and this is only for the objects that arrive on a stream the caller
1981    /// holds.
1982    pub async fn withdraw_for_data_stream(&self, err: &ConnectionError) -> bool {
1983        let ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject { alias, .. }) = err
1984        else {
1985            return false;
1986        };
1987        self.withdraw_malformed_track(*alias, MalformedTrackCondition::ObjectPastFinalObject).await;
1988        true
1989    }
1990
1991    /// Close the session over a rule broken on a data stream, reporting whether
1992    /// it did.
1993    ///
1994    /// A data stream cannot close for itself the way `recv_control` does:
1995    /// [`Connection::accept_subgroup_stream`] hands the caller a
1996    /// [`FramedRecvStream`] holding no connection, so the reader that finds the
1997    /// violation is not the object that can act on it. Keeping it a separate
1998    /// call is deliberate as well - a permissive caller, one reproducing a
1999    /// capture, can read a violating stream and report it without tearing the
2000    /// session down.
2001    ///
2002    /// The rule this draft answers here is extension headers on an Object whose
2003    /// status is Object Does Not Exist, which reaches subgroup streams, fetch
2004    /// streams and status datagrams alike. It shares `codec_session_error_code`
2005    /// with the control path, so a rule is answered with one code whichever
2006    /// stream carried it.
2007    ///
2008    /// Not every rule that reaches here is the decoder's. A track whose objects
2009    /// mix forwarding preferences is the endpoint's to notice — it takes the
2010    /// alias table to know which track an object belongs to — and it arrives on
2011    /// exactly these streams. Both kinds are asked for a code the same way, and
2012    /// a rule with no code is declined rather than guessed at.
2013    pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
2014        match err {
2015            ConnectionError::Codec(inner) => {
2016                let Some(code) = Self::codec_session_error_code(inner) else { return false };
2017                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2018                self.close(wire_code, inner.to_string().as_bytes());
2019                true
2020            }
2021            // A rule the endpoint raises rather than the decoder. The two
2022            // reach their codes through different tables and mean the same
2023            // thing here: `Some` is a rule this draft ends the session over.
2024            ConnectionError::Endpoint(inner) => {
2025                let Some(code) = inner.session_error_code() else { return false };
2026                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2027                self.close(wire_code, inner.to_string().as_bytes());
2028                true
2029            }
2030            _ => false,
2031        }
2032    }
2033
2034    /// Close the connection.
2035    pub fn close(&self, code: u32, reason: &[u8]) {
2036        self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
2037        self.transport.close(code, reason);
2038    }
2039}
2040
2041/// Determine the encoded length of a varint from its first byte.
2042fn varint_len(first_byte: u8) -> usize {
2043    1 << (first_byte >> 6)
2044}
2045
2046#[cfg(test)]
2047mod tests {
2048    use super::*;
2049
2050    /// This build failing to narrow a message it decoded is never a finding
2051    /// about the peer.
2052    ///
2053    /// The arm that raises `ControlMessageNarrowing` is unreachable — this
2054    /// draft's decoder can only hand back this draft's variant — and nothing
2055    /// pins that. What is pinned here is the half that matters.
2056    /// `CodecError::UnknownMessageType(0)` is what the arm must not raise:
2057    /// `codec_session_error_code` answers it `Some(PROTOCOL_VIOLATION)` on
2058    /// every draft in range, so the day the narrowing failed a conformance
2059    /// probe would publish a relay for sending a control message type this
2060    /// draft does not assign — with `0x00` attached as the codepoint that
2061    /// proved it, which is an accusation better evidenced than any real one
2062    /// this build makes. The section stating that rule is renumbered several
2063    /// times across the series, and the point does not turn on the
2064    /// number.
2065    ///
2066    /// Ablated by putting the arm back to
2067    /// `ConnectionError::Codec(CodecError::UnknownMessageType(0))`: this test
2068    /// reddens on the cause, and so does the probe's own
2069    /// `violation::a_message_this_build_could_not_narrow_names_nobody`.
2070    #[test]
2071    fn a_message_this_build_could_not_narrow_names_nobody() {
2072        use crate::dispatch::{AnyConnectionError, ErrorCause};
2073
2074        let err: AnyConnectionError = ConnectionError::ControlMessageNarrowing.into();
2075        assert!(err.is_local(), "a narrowing this build could not do is this build's");
2076        assert_eq!(
2077            err.cause(),
2078            &ErrorCause::Facade,
2079            "nothing reached the wire, so there is no rule and no close code to read"
2080        );
2081    }
2082
2083    #[test]
2084    fn varint_len_single_byte() {
2085        // 0b00xxxxxx -> 1 byte
2086        assert_eq!(varint_len(0x00), 1);
2087        assert_eq!(varint_len(0x3F), 1);
2088    }
2089
2090    #[test]
2091    fn varint_len_two_bytes() {
2092        // 0b01xxxxxx -> 2 bytes
2093        assert_eq!(varint_len(0x40), 2);
2094        assert_eq!(varint_len(0x7F), 2);
2095    }
2096
2097    #[test]
2098    fn varint_len_four_bytes() {
2099        // 0b10xxxxxx -> 4 bytes
2100        assert_eq!(varint_len(0x80), 4);
2101        assert_eq!(varint_len(0xBF), 4);
2102    }
2103
2104    #[test]
2105    fn varint_len_eight_bytes() {
2106        // 0b11xxxxxx -> 8 bytes
2107        assert_eq!(varint_len(0xC0), 8);
2108        assert_eq!(varint_len(0xFF), 8);
2109    }
2110
2111    #[test]
2112    fn client_config_supported_versions_draft14() {
2113        let config = ClientConfig {
2114            draft: DraftVersion::Draft14,
2115            additional_versions: Vec::new(),
2116            transport: TransportType::Quic,
2117            skip_cert_verification: false,
2118            ca_certs: Vec::new(),
2119            setup_parameters: Vec::new(),
2120        };
2121        let versions = config.supported_versions();
2122        assert_eq!(versions.len(), 1);
2123        assert_eq!(versions[0].into_inner(), 0xff000000 + 14);
2124    }
2125
2126    #[test]
2127    fn client_config_supported_versions_draft07() {
2128        let config = ClientConfig {
2129            draft: DraftVersion::Draft07,
2130            additional_versions: Vec::new(),
2131            transport: TransportType::Quic,
2132            skip_cert_verification: false,
2133            ca_certs: Vec::new(),
2134            setup_parameters: Vec::new(),
2135        };
2136        let versions = config.supported_versions();
2137        assert_eq!(versions.len(), 1);
2138        assert_eq!(versions[0].into_inner(), 0xff000000 + 7);
2139    }
2140
2141    #[test]
2142    fn client_config_alpn_quic() {
2143        let config = ClientConfig {
2144            draft: DraftVersion::Draft14,
2145            additional_versions: Vec::new(),
2146            transport: TransportType::Quic,
2147            skip_cert_verification: false,
2148            ca_certs: Vec::new(),
2149            setup_parameters: Vec::new(),
2150        };
2151        assert_eq!(config.alpn(), vec![b"moq-00".to_vec()]);
2152    }
2153
2154    #[test]
2155    fn client_config_alpn_webtransport() {
2156        let config = ClientConfig {
2157            draft: DraftVersion::Draft14,
2158            additional_versions: Vec::new(),
2159            transport: TransportType::WebTransport { url: "https://example.com".to_string() },
2160            skip_cert_verification: false,
2161            ca_certs: Vec::new(),
2162            setup_parameters: Vec::new(),
2163        };
2164        assert_eq!(config.alpn(), vec![b"h3".to_vec()]);
2165    }
2166
2167    #[test]
2168    fn moqt_alpn_value() {
2169        assert_eq!(MOQT_ALPN, b"moq-00");
2170    }
2171
2172    #[test]
2173    fn transport_type_debug() {
2174        let quic = TransportType::Quic;
2175        assert!(format!("{quic:?}").contains("Quic"));
2176
2177        let wt = TransportType::WebTransport { url: "https://example.com".to_string() };
2178        assert!(format!("{wt:?}").contains("WebTransport"));
2179    }
2180}