Skip to main content

moqtap_client/draft13/
connection.rs

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