Skip to main content

moqtap_client/draft13/
connection.rs

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