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