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