Skip to main content

moqtap_client/draft11/
connection.rs

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