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