Skip to main content

moqtap_client/draft15/
connection.rs

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