From 3c1b809011fb1baf1233e8941e3ba866be97f1e5 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 00:00:48 -0700 Subject: [PATCH 01/25] chore(quest): claim quest/m1/ietf-drop/d14-17.md Quest-Claim: 1835d705d02d19b4 From 47ffd180fd070f3daa2ee9031d954cb79021b27c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 02:23:59 -0700 Subject: [PATCH 02/25] wip(net): drop moq-transport drafts 14-17 from the moq-net library Tests, the relay, JS, CI, and docs still to follow. Co-Authored-By: Claude Opus 5.5 --- rs/moq-net/benches/request-ids.rs | 34 +- rs/moq-net/src/client.rs | 201 +- rs/moq-net/src/coding/codes.rs | 16 +- rs/moq-net/src/coding/encode.rs | 2 +- rs/moq-net/src/coding/reader.rs | 6 - rs/moq-net/src/coding/varint.rs | 78 +- rs/moq-net/src/fuzz.rs | 48 +- rs/moq-net/src/ietf/active_count.rs | 30 +- rs/moq-net/src/ietf/adapter.rs | 2701 -------------------- rs/moq-net/src/ietf/auth.rs | 52 +- rs/moq-net/src/ietf/cluster.rs | 40 +- rs/moq-net/src/ietf/control.rs | 449 +--- rs/moq-net/src/ietf/datagram.rs | 55 +- rs/moq-net/src/ietf/epoch.rs | 27 +- rs/moq-net/src/ietf/error.rs | 601 +---- rs/moq-net/src/ietf/fetch.rs | 382 +-- rs/moq-net/src/ietf/filter.rs | 20 +- rs/moq-net/src/ietf/flate.rs | 39 +- rs/moq-net/src/ietf/goaway.rs | 41 +- rs/moq-net/src/ietf/group.rs | 159 +- rs/moq-net/src/ietf/hidden.rs | 2 +- rs/moq-net/src/ietf/max_age.rs | 2 +- rs/moq-net/src/ietf/message.rs | 53 +- rs/moq-net/src/ietf/mod.rs | 16 +- rs/moq-net/src/ietf/namespace.rs | 9 +- rs/moq-net/src/ietf/object.rs | 88 +- rs/moq-net/src/ietf/parameters.rs | 543 +--- rs/moq-net/src/ietf/properties.rs | 65 +- rs/moq-net/src/ietf/publish.rs | 397 +-- rs/moq-net/src/ietf/publish_namespace.rs | 199 +- rs/moq-net/src/ietf/publisher.rs | 1236 +++------ rs/moq-net/src/ietf/publisher/tracks.rs | 53 +- rs/moq-net/src/ietf/request.rs | 238 +- rs/moq-net/src/ietf/request_stream.rs | 406 +-- rs/moq-net/src/ietf/request_update.rs | 28 +- rs/moq-net/src/ietf/session.rs | 722 ++---- rs/moq-net/src/ietf/solicit.rs | 2 +- rs/moq-net/src/ietf/subscribe.rs | 713 ++---- rs/moq-net/src/ietf/subscribe_namespace.rs | 387 +-- rs/moq-net/src/ietf/subscribe_tracks.rs | 34 +- rs/moq-net/src/ietf/subscriber.rs | 1322 +++------- rs/moq-net/src/ietf/subscriber/tracks.rs | 50 +- rs/moq-net/src/ietf/token.rs | 25 +- rs/moq-net/src/ietf/track.rs | 165 +- rs/moq-net/src/ietf/version.rs | 8 - rs/moq-net/src/lite/ietf.rs | 25 +- rs/moq-net/src/model/origin.rs | 52 +- rs/moq-net/src/server.rs | 253 +- rs/moq-net/src/session.rs | 27 +- rs/moq-net/src/setup.rs | 176 +- rs/moq-net/src/test_interop.rs | 52 +- rs/moq-net/src/transport/boxed.rs | 2 + rs/moq-net/src/version.rs | 40 +- 53 files changed, 1937 insertions(+), 10434 deletions(-) delete mode 100644 rs/moq-net/src/ietf/adapter.rs diff --git a/rs/moq-net/benches/request-ids.rs b/rs/moq-net/benches/request-ids.rs index e540905c7ed..d17be0208d9 100644 --- a/rs/moq-net/benches/request-ids.rs +++ b/rs/moq-net/benches/request-ids.rs @@ -1,36 +1,8 @@ -//! Request-ID admission cost across independent sessions and reordered windows. +//! Request-ID admission cost across independent sessions and reordered arrivals. use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; use std::{hint::black_box, time::Duration}; -fn request_ids(c: &mut Criterion) { - let mut group = c.benchmark_group("request-ids"); - group.sample_size(20); - group.warm_up_time(Duration::from_millis(200)); - group.measurement_time(Duration::from_secs(1)); - // Up to the production window, which the session sizes to twice its 120,000 caps. - for mode in ["ordered", "reordered", "scattered"] { - for sessions in [1, 8, 32] { - for window in [8, 128, 240_000] { - let ids = ids(mode, window); - println!( - "admitted,{mode},{sessions},{window},{}", - moq_net::fuzz::request_ids(window, &ids) * sessions - ); - group.bench_function(BenchmarkId::new(mode, format!("{sessions}x{window}")), |b| { - b.iter(|| { - let accepted: usize = (0..sessions) - .map(|_| moq_net::fuzz::request_ids(window, black_box(&ids))) - .sum(); - black_box(accepted) - }); - }); - } - } - } - group.finish(); -} - -/// Draft-17+ admission, which spends IDs without a window but remembers skipped runs. +/// Admission spends IDs without a window but remembers skipped runs. fn request_history(c: &mut Criterion) { let mut group = c.benchmark_group("modern-request-ids"); group.sample_size(20); @@ -77,5 +49,5 @@ fn ids(mode: &str, window: u64) -> Vec { } } -criterion_group!(benches, request_ids, request_history); +criterion_group!(benches, request_history); criterion_main!(benches); diff --git a/rs/moq-net/src/client.rs b/rs/moq-net/src/client.rs index be90008de04..9beb9325fa5 100644 --- a/rs/moq-net/src/client.rs +++ b/rs/moq-net/src/client.rs @@ -3,8 +3,7 @@ use crate::origin; use crate::time::{Deadlines, Instant}; use crate::transport::poll::Session as _; use crate::{ - ALPN_14, ALPN_15, ALPN_16, ALPN_17, ALPN_18, ALPN_19, ALPN_20, ALPN_21, ALPN_22, ALPN_LITE, ALPN_LITE_03, - ALPN_LITE_04, ALPN_LITE_05, ALPN_LITE_06, ALPN_LITE_07_WIP, Consume, Error, NEGOTIATED, Session, Version, Versions, + ALPN_18, ALPN_19, ALPN_20, ALPN_21, ALPN_22, ALPN_LITE, ALPN_LITE_03, ALPN_LITE_04, ALPN_LITE_05, ALPN_LITE_06, ALPN_LITE_07_WIP, Consume, Error, NEGOTIATED, Session, Version, Versions, coding::{self, Decode, Encode, Stream}, ietf, lite, setup, stats, }; @@ -312,22 +311,19 @@ impl Client { let peer_hop = self.assigned_hop(); let (publish, subscribe) = self.origins(peer_hop); - // If ALPN was used to negotiate the version, use the appropriate encoding. - // Default to IETF 14 if no ALPN was used and we'll negotiate the version later. - let (encoding, supported) = match session.protocol() { - Some(alpn @ (ALPN_22 | ALPN_21 | ALPN_20 | ALPN_19 | ALPN_18 | ALPN_17)) => { + let supported = match session.protocol() { + Some(alpn @ (ALPN_22 | ALPN_21 | ALPN_20 | ALPN_19 | ALPN_18)) => { let draft = match alpn { ALPN_22 => ietf::Version::Draft22, ALPN_21 => ietf::Version::Draft21, ALPN_20 => ietf::Version::Draft20, ALPN_19 => ietf::Version::Draft19, - ALPN_18 => ietf::Version::Draft18, - _ => ietf::Version::Draft17, + _ => ietf::Version::Draft18, }; let v = self.versions.select(Version::Ietf(draft)).ok_or(Error::Version)?; - // Draft-17+: SETUP is exchanged by the connection driver. + // SETUP is exchanged by the connection driver. // We advertise the request path in our SETUP for URL-less transports. // The peer's SETUP decides whether AUTH is negotiated. A handle that does not // offer it keeps the option out of our SETUP and presents no credential. @@ -338,8 +334,6 @@ impl Client { stats: self.stats.clone(), limits: self.limits, session: session.clone(), - setup: None, - request_id_max: None, client: true, publish: publish.clone(), subscribe: subscribe.clone(), @@ -374,27 +368,6 @@ impl Client { }, )); } - Some(ALPN_16) => { - let v = self - .versions - .select(Version::Ietf(ietf::Version::Draft16)) - .ok_or(Error::Version)?; - (v, v.into()) - } - Some(ALPN_15) => { - let v = self - .versions - .select(Version::Ietf(ietf::Version::Draft15)) - .ok_or(Error::Version)?; - (v, v.into()) - } - Some(ALPN_14) => { - let v = self - .versions - .select(Version::Ietf(ietf::Version::Draft14)) - .ok_or(Error::Version)?; - (v, v.into()) - } Some(alpn @ (ALPN_LITE_05 | ALPN_LITE_06 | ALPN_LITE_07_WIP)) => { let version = match alpn { ALPN_LITE_07_WIP => lite::Version::Lite07, @@ -416,46 +389,18 @@ impl Client { .ok_or(Error::Version)?; return self.start_lite(runtime, session, lite::Version::Lite03, peer_hop); } - Some(ALPN_LITE) | None => { - let supported = self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?; - (Version::Ietf(ietf::Version::Draft14), supported) - } + // No ALPN, or `moql`: negotiate moq-lite 01/02 in a bidi SETUP. + Some(ALPN_LITE) | None => self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?, Some(p) => return Err(Error::UnknownAlpn(p.to_string())), }; + // Lite 01 and 02 frame the SETUP identically, so either encodes it. + let encoding = *supported.iter().next().ok_or(Error::Version)?; let mut stream = Stream::open(&mut session, encoding).await?.with_stats(&self.stats); - // The encoding is always an IETF version for SETUP negotiation. - let ietf_encoding = ietf::Version::try_from(encoding).map_err(|_| Error::Version)?; - - let mut parameters = ietf::Parameters::default(); - // The server's requests, admitted up to our limits and granted back as they close. - parameters.set_varint( - ietf::ParameterVarInt::MaxRequestId, - ietf::initial_max_request_id(self.limits.window(), false), - ); - if let Some(implementation) = self.implementation() { - parameters.set_bytes(ietf::ParameterBytes::Implementation, implementation.into_bytes()); - } - // Advertise the request path in-band (draft 14-16), same as the lite-05 SETUP. - if let Some(path) = &self.setup_path { - parameters.set_bytes(ietf::ParameterBytes::Path, path.clone().into_bytes()); - } - if let Some(authority) = &self.setup_authority { - parameters.set_bytes(ietf::ParameterBytes::Authority, authority.clone().into_bytes()); - } - if self.extensions.solicit { - ietf::solicit::into_setup(&mut parameters, ietf_encoding); - } - ietf::hidden::into_setup(&mut parameters, ietf_encoding); - ietf::max_age::into_setup(&mut parameters, ietf_encoding); - ietf::active_count::into_setup(&mut parameters, ietf_encoding); - ietf::flate::into_setup(&mut parameters, ietf_encoding); - let parameters = parameters.encode_bytes(ietf_encoding)?; - let client = setup::Client { versions: supported.clone().into(), - parameters, + parameters: lite::Parameters::default().encode_bytes(lite::Version::Lite01)?, }; stream.writer.encode(&client).await?; @@ -467,109 +412,42 @@ impl Client { .find(|v| coding::Version::from(**v) == server.version) .copied() .ok_or(Error::Version)?; - - let (recv_bw, protocol, goaway, auth, estimate, peer_implementation) = match version { - Version::Lite(v) => { - let stream = stream.with_version(v); - let start = lite::start(lite::Config { - runtime: runtime.clone(), - stats: self.stats.clone(), - client: true, - limits: self.limits, - session: session.clone(), - setup_stream: Some(stream), - publish: publish.clone(), - subscribe: subscribe.clone(), - peer_hop: Some(peer_hop), - version: v, - // This path only handles versions negotiated via the bidi SETUP exchange - // (pre-lite-05), which have no Setup Stream. - our_setup: lite::Setup::default(), - peer_setup: None, - // Negotiated over the bidi SETUP: lite 01/02, which carry no AUTH. - auth: self.link(v.has_auth(), false)?, - })?; - - ( - start.recv_bandwidth, - crate::driver::Protocol::Lite(Box::new(start.driver)), - start.goaway, - start.auth, - Some(start.estimate), - start.peer_implementation, - ) - } - Version::Ietf(v) => { - // Decode the parameters to get the initial request ID and what the server - // requires of us. - let (parameters, _) = ietf::Parameters::decode_slice(&server.parameters, v)?; - let request_id_max = parameters - .get_varint(ietf::ParameterVarInt::MaxRequestId) - .map(ietf::RequestId); - let peer_declared = ietf::peer::Peer { - solicit: ietf::solicit::from_setup(¶meters, v)?, - hidden: ietf::hidden::from_setup(¶meters, v), - max_age: ietf::max_age::from_setup(¶meters, v), - active_count: ietf::active_count::from_setup(¶meters, v), - flate: ietf::flate::from_setup(¶meters, v)?, - ..Default::default() - }; - let peer_implementation = setup::PeerImplementation::known(setup::decode_implementation( - parameters.get_bytes(ietf::ParameterBytes::Implementation), - )?); - - let stream = stream.with_version(v); - // Draft 14-16 carry no AUTH, but the session is still limited through this handle. - let auth = self.link(false, true)?; - // Draft 14-16: the path rode in the bidi SETUP above, not the uni one. - let (protocol, goaway) = ietf::start(ietf::Config { - runtime: runtime.clone(), - stats: self.stats.clone(), - limits: self.limits, - session: session.clone(), - setup: Some(stream), - request_id_max, - client: true, - publish: publish.clone(), - subscribe: subscribe.clone(), - peer_hop: Some(peer_hop), - cost: self.cost, - version: v, - path: None, - authority: None, - peer_setup_stream: None, - peer_declared: Some(peer_declared), - auth: auth.clone(), - early_unis: Vec::new(), - extensions: self.extensions, - // Already sent on the bidi SETUP above. - implementation: None, - peer_implementation: peer_implementation.clone(), - })?; - ( - None, - crate::driver::Protocol::Ietf(protocol), - goaway, - auth, - None, - peer_implementation, - ) - } + let Version::Lite(v) = version else { + return Err(Error::Version); }; + let stream = stream.with_version(v); + let start = lite::start(lite::Config { + runtime: runtime.clone(), + stats: self.stats.clone(), + client: true, + limits: self.limits, + session: session.clone(), + setup_stream: Some(stream), + publish: publish.clone(), + subscribe: subscribe.clone(), + peer_hop: Some(peer_hop), + version: v, + // The bidi SETUP exchange predates lite-05, so there is no Setup Stream. + our_setup: lite::Setup::default(), + peer_setup: None, + // Lite 01/02 carry no AUTH. + auth: self.link(v.has_auth(), false)?, + })?; + Ok(Session::new( runtime, session, version, - recv_bw, - protocol, + start.recv_bandwidth, + crate::driver::Protocol::Lite(Box::new(start.driver)), crate::session::Handles { stats: self.stats.clone(), - goaway, - auth, - estimate, + goaway: start.goaway, + auth: start.auth, + estimate: Some(start.estimate), bandwidth: self.bandwidth(), - peer_implementation, + peer_implementation: start.peer_implementation, }, )) } @@ -864,7 +742,6 @@ mod tests { server .encode( &mut crate::coding::Encoder::new(&mut encoded, (Version::Ietf(ietf::Version::Draft14)).into()), - Version::Ietf(ietf::Version::Draft14), ) .unwrap(); @@ -884,7 +761,6 @@ mod tests { Version::Lite(lite::Version::Lite03), Version::Lite(lite::Version::Lite02), Version::Lite(lite::Version::Lite01), - Version::Ietf(ietf::Version::Draft14), ] .into(), ); @@ -897,7 +773,6 @@ mod tests { let mut setup_bytes = Bytes::from(fake.control_writes()); let setup = crate::coding::decode_buf( &mut setup_bytes, - Version::Ietf(ietf::Version::Draft14), setup::Client::decode, ) .unwrap(); @@ -907,7 +782,6 @@ mod tests { vec![ Version::Lite(lite::Version::Lite02), Version::Lite(lite::Version::Lite01), - Version::Ietf(ietf::Version::Draft14), ] ); @@ -960,7 +834,6 @@ mod tests { .with_versions( [ Version::Lite(lite::Version::Lite01), - Version::Ietf(ietf::Version::Draft14), ] .into(), ) diff --git a/rs/moq-net/src/coding/codes.rs b/rs/moq-net/src/coding/codes.rs index 2bd51bc3ce4..75512807ca9 100644 --- a/rs/moq-net/src/coding/codes.rs +++ b/rs/moq-net/src/coding/codes.rs @@ -65,14 +65,14 @@ impl StreamCodes for lite::Version { } } -/// moq-transport's registry, which is per draft. +/// moq-transport's registry. impl StreamCodes for ietf::Version { fn encode_stream_code(&self, err: &StreamError) -> u32 { - ietf::error::to_stream_code(err, *self) + ietf::error::to_stream_code(err) } fn decode_stream_code(&self, code: u32) -> StreamError { - ietf::error::from_stream_code(code, *self) + ietf::error::from_stream_code(code) } } @@ -120,16 +120,8 @@ mod tests { assert_eq!(lite.encode_stream_code(&StreamError::Cancel), ietf::error::CANCELLED); assert_eq!(ietf.encode_stream_code(&StreamError::Cancel), ietf::error::CANCELLED); - // GOING_AWAY is moq-lite's 0x4 and draft-20's, but draft-17 gives 0x4 to - // UNKNOWN_OBJECT_STATUS, so it is not sent there and not read back there. - let draft17 = crate::Version::Ietf(ietf::Version::Draft17); assert_eq!(lite.decode_stream_code(0x4), StreamError::GoingAway); assert_eq!(ietf.decode_stream_code(0x4), StreamError::GoingAway); - assert_eq!(draft17.decode_stream_code(0x4), StreamError::Unknown(0x4)); - assert_eq!( - draft17.encode_stream_code(&StreamError::GoingAway), - ietf::error::INTERNAL_ERROR - ); } /// NOT_FETCHABLE is lite-07's: an earlier peer is told NOT_FOUND, the miss it would @@ -178,7 +170,7 @@ mod tests { /// connection closed. Native transport adapters supply that metadata themselves. #[test] fn a_virtual_stream_close_does_not_claim_the_connection_closed() { - let version = ietf::Version::Draft16; + let version = ietf::Version::Draft18; assert!(matches!(version.transport_error(Error::Closed), Error::Transport(_))); assert!(matches!( version.transport_error(Error::IdleTimeout), diff --git a/rs/moq-net/src/coding/encode.rs b/rs/moq-net/src/coding/encode.rs index b84d437e2ba..a6ccd7034f1 100644 --- a/rs/moq-net/src/coding/encode.rs +++ b/rs/moq-net/src/coding/encode.rs @@ -90,7 +90,7 @@ impl<'a> Encoder<'a> { } /// Write a big-endian `u16`. - #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] + #[cfg(test)] pub fn u16(&mut self, v: u16) { self.buf.extend_from_slice(&v.to_be_bytes()); } diff --git a/rs/moq-net/src/coding/reader.rs b/rs/moq-net/src/coding/reader.rs index 77f60c36862..8544ac7f0ff 100644 --- a/rs/moq-net/src/coding/reader.rs +++ b/rs/moq-net/src/coding/reader.rs @@ -413,11 +413,6 @@ impl Reader { Poll::Ready(Ok(bytes)) } - /// Read exactly the given number of bytes from the stream. - pub async fn read_exact(&mut self, size: usize) -> Result { - std::future::poll_fn(|cx| self.poll_read_exact(cx, size)).await - } - /// Poll until the stream is closed, erroring if there are any additional bytes. pub fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll> { if ready!(self.poll_has_more(cx))? { @@ -777,7 +772,6 @@ mod tests { for version in [ Version::Lite(lite::Version::Lite01), Version::Lite(lite::Version::Lite02), - Version::Ietf(ietf::Version::Draft14), ] { let prefix: &'static [u8] = if version.is_lite() { &[0x20, 0] } else { &[0x20, 0, 0] }; refuses_frame::(prefix, version, false); diff --git a/rs/moq-net/src/coding/varint.rs b/rs/moq-net/src/coding/varint.rs index 33e4ec6945d..0228cef32a3 100644 --- a/rs/moq-net/src/coding/varint.rs +++ b/rs/moq-net/src/coding/varint.rs @@ -23,10 +23,7 @@ pub(crate) enum Form { /// QUIC's two-bit length tag, up to [`MAX_QUIC`]. Quic, /// Leading one bits count the length, up to `u64::MAX`. - LeadingOnes { - /// Whether the 7-byte form (`1111110x`) is accepted on decode, which draft-17 forbids. - seven: bool, - }, + LeadingOnes, } impl From for Form { @@ -40,18 +37,14 @@ impl From for Form { | lite::Version::Lite06 => Self::Quic, // Lite-07 is only reached through its ALPN, so the form is known before the // first byte of any stream. - _ => Self::LeadingOnes { seven: true }, + _ => Self::LeadingOnes, } } } impl From for Form { - fn from(version: ietf::Version) -> Self { - match version { - ietf::Version::Draft14 | ietf::Version::Draft15 | ietf::Version::Draft16 => Self::Quic, - ietf::Version::Draft17 => Self::LeadingOnes { seven: false }, - _ => Self::LeadingOnes { seven: true }, - } + fn from(_: ietf::Version) -> Self { + Self::LeadingOnes } } @@ -83,15 +76,15 @@ pub(crate) fn size(value: u64, form: Form) -> Result { Form::Quic if hi == 0 && lo < 1 << 30 => 4, Form::Quic if hi < 1 << 30 => 8, Form::Quic => return Err(BoundsExceeded), - Form::LeadingOnes { .. } if hi == 0 && lo < 1 << 7 => 1, - Form::LeadingOnes { .. } if hi == 0 && lo < 1 << 14 => 2, - Form::LeadingOnes { .. } if hi == 0 && lo < 1 << 21 => 3, - Form::LeadingOnes { .. } if hi == 0 && lo < 1 << 28 => 4, - Form::LeadingOnes { .. } if hi < 1 << 3 => 5, - Form::LeadingOnes { .. } if hi < 1 << 10 => 6, + Form::LeadingOnes if hi == 0 && lo < 1 << 7 => 1, + Form::LeadingOnes if hi == 0 && lo < 1 << 14 => 2, + Form::LeadingOnes if hi == 0 && lo < 1 << 21 => 3, + Form::LeadingOnes if hi == 0 && lo < 1 << 28 => 4, + Form::LeadingOnes if hi < 1 << 3 => 5, + Form::LeadingOnes if hi < 1 << 10 => 6, // The 7-byte form is skipped: one byte longer, but legal on every draft. - Form::LeadingOnes { .. } if hi < 1 << 24 => 8, - Form::LeadingOnes { .. } => 9, + Form::LeadingOnes if hi < 1 << 24 => 8, + Form::LeadingOnes => 9, }) } @@ -103,7 +96,7 @@ pub(crate) fn size(value: u64, form: Form) -> Result { pub(super) fn write(value: u64, form: Form, out: &mut Vec) -> Result<(), BoundsExceeded> { match form { Form::Quic => write_quic(value, out), - Form::LeadingOnes { .. } => { + Form::LeadingOnes => { write_leading_ones(value, out); Ok(()) } @@ -172,7 +165,7 @@ fn write_leading_ones(value: u64, out: &mut Vec) { pub(super) fn read(buf: &[u8], form: Form) -> Result<(u64, &[u8]), DecodeError> { match form { Form::Quic => read_quic(buf), - Form::LeadingOnes { seven } => read_leading_ones(buf, seven), + Form::LeadingOnes => read_leading_ones(buf), } } @@ -201,7 +194,7 @@ fn read_quic(buf: &[u8]) -> Result<(u64, &[u8]), DecodeError> { #[cfg_attr(target_arch = "wasm32", inline)] #[cfg_attr(not(target_arch = "wasm32"), inline(always))] #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] -fn read_leading_ones(buf: &[u8], seven: bool) -> Result<(u64, &[u8]), DecodeError> { +fn read_leading_ones(buf: &[u8]) -> Result<(u64, &[u8]), DecodeError> { let Some((&first, rest)) = buf.split_first() else { return Err(DecodeError::Short); }; @@ -229,8 +222,6 @@ fn read_leading_ones(buf: &[u8], seven: bool) -> Result<(u64, &[u8]), DecodeErro let ([a, b, lo @ ..], rest) = buf.split_first_chunk::<6>().ok_or(DecodeError::Short)?; (from_halves(be([0, 0, a & 0x03, *b]), be(*lo)), rest) } - // 1111110x: the 7-byte form, which draft-17 forbids. - 6 if !seven => return Err(DecodeError::InvalidValue), 6 => { let ([a, b, c, lo @ ..], rest) = buf.split_first_chunk::<7>().ok_or(DecodeError::Short)?; (from_halves(be([0, a & 0x01, *b, *c]), be(*lo)), rest) @@ -303,8 +294,7 @@ pub(crate) const fn unzigzag(value: u64) -> i64 { mod tests { use super::*; - const DRAFT17: Form = Form::LeadingOnes { seven: false }; - const DRAFT18: Form = Form::LeadingOnes { seven: true }; + const LEADING_ONES: Form = Form::LeadingOnes; fn encode(value: u64, form: Form) -> Result, BoundsExceeded> { let mut buf = Vec::new(); @@ -313,7 +303,7 @@ mod tests { Ok(buf) } - /// Test vectors from the draft-17 spec (Table 2: Example Integer Encodings), + /// Test vectors from the spec (Table 2: Example Integer Encodings), /// excluding the known-buggy example 4 (0xdd7f3e7d). #[test] fn leading_ones_spec_examples() { @@ -335,27 +325,18 @@ mod tests { ]; for (bytes, expected) in cases { - let (decoded, rest) = read(bytes, DRAFT17).expect("decode should succeed"); + let (decoded, rest) = read(bytes, LEADING_ONES).expect("decode should succeed"); assert_eq!(decoded, *expected, "decode mismatch for bytes {bytes:02x?}"); assert!(rest.is_empty(), "all bytes should be consumed for {bytes:02x?}"); // Skip the non-minimal encoding (0x8025 for 37); we only emit the minimal one. if bytes.len() == 1 || *expected != 37 { - let encoded = encode(*expected, DRAFT17).unwrap(); + let encoded = encode(*expected, LEADING_ONES).unwrap(); assert_eq!(&encoded, bytes, "encode mismatch for value {expected}"); } } } - /// 11111100 (0xFC) is an invalid code point on draft-17 (allowed as 7-byte form on draft-18+). - #[test] - fn leading_ones_invalid_0xfc() { - assert!( - matches!(read(&[0xFC], DRAFT17), Err(DecodeError::InvalidValue)), - "0xFC should be rejected as invalid on draft-17" - ); - } - #[test] fn leading_ones_boundaries_round_trip() { let cases = [ @@ -368,14 +349,14 @@ mod tests { ]; for (value, expected_len) in cases { - let encoded = encode(value, DRAFT17).unwrap(); + let encoded = encode(value, LEADING_ONES).unwrap(); assert_eq!( encoded.len(), expected_len, "unexpected encoded length for value {value}" ); - let (decoded, _) = read(&encoded, DRAFT17).expect("leading-ones decode should succeed"); + let (decoded, _) = read(&encoded, LEADING_ONES).expect("leading-ones decode should succeed"); assert_eq!(decoded, value, "round-trip mismatch for value {value}"); } } @@ -386,7 +367,7 @@ mod tests { fn every_length_round_trips() { for bits in 0..64 { for value in [1u64 << bits, (1u64 << bits) - 1, (1u64 << bits) + 1] { - for form in [Form::Quic, DRAFT17, DRAFT18] { + for form in [Form::Quic, LEADING_ONES] { let Ok(encoded) = encode(value, form) else { assert_eq!(form, Form::Quic); assert!(value > MAX_QUIC); @@ -414,19 +395,12 @@ mod tests { } for value in [MAX_QUIC, 1u64 << 62, u64::MAX] { - let encoded = encode(value, DRAFT18).unwrap(); + let encoded = encode(value, LEADING_ONES).unwrap(); assert_eq!(encoded.len(), 9); - assert_eq!(read(&encoded, DRAFT18).unwrap().0, value); + assert_eq!(read(&encoded, LEADING_ONES).unwrap().0, value); } } - #[test] - fn draft17_rejects_7_byte_varint() { - // 1111110x prefix: invalid on draft-17. - let err = read(&[0xFC, 0, 0, 0, 0, 0, 0], DRAFT17).unwrap_err(); - assert!(matches!(err, DecodeError::InvalidValue)); - } - #[test] fn zigzag_roundtrip_small() { for n in [-3i64, -2, -1, 0, 1, 2, 3, 100, -100] { @@ -465,7 +439,7 @@ mod tests { } #[test] - fn draft18_accepts_7_byte_varint() { + fn accepts_7_byte_varint() { // Value 0x1234_5678_9ABC encoded as 7-byte leading-ones (1111110x | hi, +6 bytes). let value: u64 = 0x1234_5678_9ABC; let mut bytes = Vec::new(); @@ -476,7 +450,7 @@ mod tests { for shift in (0..48).step_by(8).rev() { bytes.push(((value >> shift) & 0xFF) as u8); } - let (decoded, _) = read(&bytes, DRAFT18).unwrap(); + let (decoded, _) = read(&bytes, LEADING_ONES).unwrap(); assert_eq!(decoded, value); } diff --git a/rs/moq-net/src/fuzz.rs b/rs/moq-net/src/fuzz.rs index e9633ecfed1..8d97be22fe1 100644 --- a/rs/moq-net/src/fuzz.rs +++ b/rs/moq-net/src/fuzz.rs @@ -96,10 +96,6 @@ const LITE_VERSIONS: &[lite::Version] = &[ /// The moq-transport drafts a target decodes at, selected by the input's first byte. const IETF_VERSIONS: &[ietf::Version] = &[ - ietf::Version::Draft14, - ietf::Version::Draft15, - ietf::Version::Draft16, - ietf::Version::Draft17, ietf::Version::Draft18, ietf::Version::Draft19, ietf::Version::Draft20, @@ -630,15 +626,6 @@ pub fn ietf_wire(data: &[u8]) -> bool { 38 => roundtrip::(rest, version), 39 => roundtrip::(rest, version), 40 => roundtrip::, _>(rest, version), - // The unified SETUP replaced the client/server pair at draft-17. The pair refuses a - // later draft itself, but the unified one treats an earlier draft as unreachable. - 41 if matches!( - version, - ietf::Version::Draft14 | ietf::Version::Draft15 | ietf::Version::Draft16 - ) => - { - false - } 41 => setup::(rest, version, |setup| &setup.parameters), 42 => setup::(rest, version, |setup| &setup.parameters), 43 => setup::(rest, version, |setup| &setup.parameters), @@ -785,7 +772,6 @@ impl Default for Messages { }, // Draft-17+ carries the request id in the control message framing instead. ietf_ok: ietf::SubscribeOk { - request_id: None, track_alias: 5, largest: Some(ietf::Location { group: 1_000, @@ -1086,29 +1072,7 @@ pub fn seeds() -> Vec { for (index, version) in IETF_VERSIONS.iter().enumerate() { let outer = crate::Version::Ietf(*version); let parameters = ietf::Parameters::default().encode_bytes(*version).unwrap(); - let messages = match version { - ietf::Version::Draft14 | ietf::Version::Draft15 | ietf::Version::Draft16 => vec![ - ( - 42, - crate::setup::Client { - versions: coding::Versions::from([outer.into()]), - parameters: parameters.clone(), - } - .encode_bytes(outer) - .unwrap(), - ), - ( - 43, - crate::setup::Server { - version: outer.into(), - parameters, - } - .encode_bytes(outer) - .unwrap(), - ), - ], - _ => vec![(41, crate::setup::Setup { parameters }.encode_bytes(outer).unwrap())], - }; + let messages = vec![(41, crate::setup::Setup { parameters }.encode_bytes(outer).unwrap())]; for (kind, message) in messages { let mut data = vec![index as u8, kind]; data.extend(message); @@ -1166,11 +1130,6 @@ pub fn seeds() -> Vec { // valid parameter count followed by a Location, so build it from the encoder. for (index, version) in IETF_VERSIONS.iter().enumerate() { let subscribe_ok = ietf::SubscribeOk { - request_id: matches!( - version, - ietf::Version::Draft14 | ietf::Version::Draft15 | ietf::Version::Draft16 - ) - .then_some(ietf::RequestId(0)), track_alias: 0, largest: Some(ietf::Location { group: 427, object: 0 }), properties: Default::default(), @@ -1416,11 +1375,6 @@ pub fn request_history(ids: &[u64]) -> usize { ietf::request_history(ids) } -/// Admit a draft-16 window for the request-ID benchmark, returning the accepted count. -pub fn request_ids(window: u64, ids: &[u64]) -> usize { - ietf::request_ids(window, ids) -} - /// A source-selection watch for the private request-lifetime benchmark. pub struct RequestValidity(crate::model::broadcast::RequestValidity); diff --git a/rs/moq-net/src/ietf/active_count.rs b/rs/moq-net/src/ietf/active_count.rs index 24b55d1a096..2b37334c255 100644 --- a/rs/moq-net/src/ietf/active_count.rs +++ b/rs/moq-net/src/ietf/active_count.rs @@ -6,8 +6,6 @@ //! quiet. When both endpoints declare the ACTIVE_COUNT option, the publisher puts on its //! REQUEST_OK how many NAMESPACE messages come before the subscription is synced. -use super::Version; - /// ACTIVE_COUNT Setup Option: the sender counts the NAMESPACE messages before a /// subscription is synced, and reads the peer's count. Even, so the value is a bare varint. pub const ACTIVE_COUNT: u64 = 0x40B64; @@ -17,23 +15,15 @@ pub const ACTIVE_COUNT: u64 = 0x40B64; /// Even, so the value is a bare varint. pub const ACTIVE_COUNT_PARAM: u64 = 0x40B66; -/// Whether this version answers SUBSCRIBE_NAMESPACE with NAMESPACE messages. Draft-14/15 -/// answer with PUBLISH_NAMESPACE requests instead, which the stream cannot count. -fn supported(version: Version) -> bool { - !matches!(version, Version::Draft14 | Version::Draft15) -} - /// Whether the peer declared the option. We always declare it, so this alone decides /// whether the extension is negotiated. -pub fn from_setup(params: &super::Parameters, version: Version) -> bool { - supported(version) && params.get_varint(super::ParameterVarInt::ActiveCount).is_some() +pub fn from_setup(params: &super::Parameters) -> bool { + params.get_varint(super::ParameterVarInt::ActiveCount).is_some() } /// Declare that we count and read the peer's count. -pub fn into_setup(params: &mut super::Parameters, version: Version) { - if supported(version) { - params.set_varint(super::ParameterVarInt::ActiveCount, 1); - } +pub fn into_setup(params: &mut super::Parameters) { + params.set_varint(super::ParameterVarInt::ActiveCount, 1); } #[cfg(test)] @@ -41,12 +31,10 @@ mod tests { use super::*; #[test] - fn only_versions_with_namespace_negotiate() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16, Version::Draft22] { - let mut params = super::super::Parameters::default(); - assert!(!from_setup(¶ms, version)); - into_setup(&mut params, version); - assert_eq!(from_setup(¶ms, version), supported(version), "{version:?}"); - } + fn declared_option_negotiates() { + let mut params = super::super::Parameters::default(); + assert!(!from_setup(¶ms)); + into_setup(&mut params); + assert!(from_setup(¶ms)); } } diff --git a/rs/moq-net/src/ietf/adapter.rs b/rs/moq-net/src/ietf/adapter.rs deleted file mode 100644 index fe84ae1fdee..00000000000 --- a/rs/moq-net/src/ietf/adapter.rs +++ /dev/null @@ -1,2701 +0,0 @@ -use crate::driver::Transport; -use crate::transport::poll::Session as _; -use std::{ - collections::{HashMap, HashSet, VecDeque}, - sync::{Arc, Mutex, Weak}, - task::{Context, Poll}, -}; - -use bytes::{Buf, Bytes, BytesMut}; - -use crate::{ - Error, PathOwned, - coding::{Decode, Decoder, Encoder, Reader, Writer}, - ietf::{self, RequestId}, -}; - -use super::{Control, Message, Version, control::RequestPermit}; - -// === Message Queues === - -struct QueueState { - messages: VecDeque, - closed: bool, -} - -/// An unbounded FIFO over [`kio::Shared`]: any clone can push or pop. -/// -/// Closure is a plain flag rather than handle liveness: [`close`](Self::close) -/// (or dropping a [`QueueWriter`]) stops future pushes, and the reader drains -/// what's buffered before seeing `None`. -struct Queue(kio::Shared>); - -impl Queue { - fn new() -> Self { - Self(kio::Shared::new(QueueState { - messages: VecDeque::new(), - closed: false, - })) - } - - /// Enqueue a message. Returns false once the queue is closed (the message is - /// dropped), matching a write to a stream whose reader is gone. - fn push(&self, item: T) -> bool { - self.offer(item).is_none() - } - - /// Enqueue a message, handing it back once the queue is closed, for a caller that - /// must not drop it where it is. - fn offer(&self, item: T) -> Option { - let mut state = self.0.lock(); - if state.closed { - return Some(item); - } - state.messages.push_back(item); - None - } - - /// Dequeue the next message, waiting while the queue is open and empty. - /// Returns `None` once the queue is closed and drained. - #[cfg(test)] - async fn pop(&self) -> Option { - kio::wait(|waiter| self.poll_pop(waiter)).await - } - - /// Poll for the next message; ready with `None` once closed and drained. - fn poll_pop(&self, waiter: &kio::Waiter) -> Poll> { - self.0 - .poll(waiter, |state| { - if state.messages.is_empty() && !state.closed { - Poll::Pending - } else { - Poll::Ready(()) - } - }) - .map(|mut state| state.messages.pop_front()) - } - - /// Stop future pushes; buffered messages still drain, then `poll_pop` returns `None`. - fn close(&self) { - self.0.lock().closed = true; - } - - /// Mint the single owning push handle whose `Drop` closes the queue. - fn writer(&self) -> QueueWriter { - QueueWriter(Self(self.0.clone())) - } -} - -// Manual impls: derives would demand `T: Clone`/`T: Default`, which the queued -// stream pairs don't satisfy. -impl Clone for Queue { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -impl Default for Queue { - fn default() -> Self { - Self::new() - } -} - -/// Single-owner push handle: dropping it closes the queue, which the reading -/// [`VirtualRecvStream`] observes as a FIN after draining. -struct QueueWriter(Queue); - -impl QueueWriter { - /// Enqueue a message, returning false if the reader already closed its side. - fn push(&self, item: T) -> bool { - self.0.push(item) - } -} - -impl Drop for QueueWriter { - fn drop(&mut self) { - self.0.close(); - } -} - -// === Virtual Streams === - -/// A virtual receive stream backed by an initial message buffer and a queue of follow-up messages. -pub struct VirtualRecvStream { - buffer: Bytes, - rx: Queue, - closed: bool, - park: kio::Park, - shared: Weak, - /// The request this stream serves, known up front when the peer opened it and - /// filled in by the first write when we did. - request_id: Arc>>, - /// Holds the peer's request open against our MAX_REQUEST_ID window until the - /// handler drops this stream. Only on a stream the peer opened. - _permit: Option, -} - -impl VirtualRecvStream { - fn new(initial: Bytes, rx: Queue, shared: Arc, request_id: Arc>>) -> Self { - Self { - buffer: initial, - rx, - closed: false, - park: kio::Park::default(), - shared: Arc::downgrade(&shared), - request_id, - _permit: None, - } - } - - /// Fill the buffer from the queue if empty; `self.closed` marks the FIN. - fn poll_fill(&mut self, cx: &mut Context<'_>) -> Poll<()> { - if !self.buffer.is_empty() || self.closed { - return Poll::Ready(()); - } - - let waiter = self.park.hold(cx); - match self.rx.poll_pop(waiter) { - Poll::Ready(Some(data)) => { - self.buffer = data; - Poll::Ready(()) - } - Poll::Ready(None) => { - self.closed = true; - Poll::Ready(()) - } - Poll::Pending => Poll::Pending, - } - } -} - -impl crate::transport::poll::Chunks for VirtualRecvStream { - type Error = crate::Error; - - fn poll_read_chunk(&mut self, cx: &mut Context<'_>, max: usize) -> Poll, Self::Error>> { - if max == 0 { - return Poll::Ready(Ok(Some(Bytes::new()))); - } - - std::task::ready!(self.poll_fill(cx)); - if self.buffer.is_empty() { - return Poll::Ready(Ok(None)); - } - - let n = max.min(self.buffer.len()); - Poll::Ready(Ok(Some(self.buffer.split_to(n)))) - } - - fn stop(&mut self, _code: u32) { - self.rx.close(); - } -} - -impl Drop for VirtualRecvStream { - fn drop(&mut self) { - // The reader is gone: close the queue so routed follow-ups are discarded - // instead of buffering forever while the map entry lingers. - self.rx.close(); - - // Release the routing entries too, so a request torn down locally gives its - // namespace back and the same one can be advertised again. - if let Some(request_id) = *self.request_id.lock().unwrap() - && let Some(shared) = self.shared.upgrade() - { - shared.forget(request_id); - } - } -} - -/// A virtual send stream that forwards writes to the shared control stream writer. -/// -/// For streams created by `open_bi` (outgoing requests), this also parses -/// the first outgoing message to extract the request_id and register the -/// stream for response routing. -pub struct VirtualSendStream { - control_tx: Queue, - /// Present only for outgoing requests (from open_bi). - /// Accumulates bytes until the request_id can be parsed, - /// then registers the stream and flushes. - pending: Option, - /// Draft-16 only, once registered: what follows the request, held until whole so - /// each REQUEST_UPDATE is routed back to its request before it leaves. - updates: Option, -} - -struct OutgoingRegistration { - follow_tx: QueueWriter, - shared: Arc, - version: Version, - buf: BytesMut, - /// Shared with the matching [`VirtualRecvStream`] so it can release this request's - /// routing entries once the reader is dropped. - request_id: Arc>>, -} - -impl OutgoingRegistration { - /// Try to parse the request_id (and optionally namespace) from the accumulated bytes, - /// with the length of the message that carried it. - /// Returns Ok(None) if not enough data yet, Err if the message is malformed. - fn try_parse(&self) -> Result, crate::Error> { - let Some((type_id, len, mut body)) = whole_message(&self.buf, self.version) else { - return Ok(None); - }; - - // We have enough bytes for the full message; decoding must succeed. - let request_id = RequestId::decode(&mut body, self.version)?; - - // For PublishNamespace, also extract the namespace for reverse lookup. - if type_id == ietf::PublishNamespace::ID { - if self.version == Version::Draft17 { - // v17 has required_request_id_delta after request_id - let _ = body.varint(); - } - if let Ok(ns) = crate::ietf::namespace::decode_namespace(&mut body) { - self.shared.namespaces.register(Direction::Outgoing, ns, request_id); - } - } - - Ok(Some((request_id, len))) - } - - fn register(self, request_id: RequestId) { - // Serialize publication with receive-half cleanup, including a drop before - // the first write supplied the request id. - let mut registered = self.request_id.lock().unwrap(); - *registered = Some(request_id); - let closed = self.follow_tx.0.0.lock().closed; - if closed { - self.shared.forget(request_id); - } else { - self.shared.streams.lock().unwrap().insert(request_id, self.follow_tx); - } - } -} - -impl VirtualSendStream { - fn new(control_tx: Queue) -> Self { - Self { - control_tx, - pending: None, - updates: None, - } - } - - fn with_registration(control_tx: Queue, pending: OutgoingRegistration) -> Self { - Self { - control_tx, - pending: Some(pending), - updates: None, - } - } -} - -impl VirtualSendStream { - /// Route a chunk: accumulate while the request_id is still unknown, then - /// register and flush; forward directly afterwards. Never blocks, since the - /// control queue is unbounded. - fn push(&mut self, chunk: Bytes) -> Result<(), crate::Error> { - if let Some(pending) = &mut self.pending { - pending.buf.extend_from_slice(&chunk); - - if let Some((request_id, len)) = pending.try_parse()? { - let mut pending = self.pending.take().unwrap(); - let mut buf = std::mem::take(&mut pending.buf); - let rest = buf.split_off(len); - if pending.version == Version::Draft16 { - self.updates = Some(UpdateRouting { - shared: Arc::clone(&pending.shared), - version: pending.version, - request_id, - buf: BytesMut::new(), - }); - } - pending.register(request_id); - if !self.control_tx.push(buf.freeze()) { - return Err(crate::Error::Closed); - } - if !rest.is_empty() { - return self.push(rest.freeze()); - } - } - } else if let Some(updates) = &mut self.updates { - updates.buf.extend_from_slice(&chunk); - while let Some(message) = updates.next()? { - if !self.control_tx.push(message) { - return Err(crate::Error::Closed); - } - } - } else if !self.control_tx.push(chunk) { - return Err(crate::Error::Closed); - } - - Ok(()) - } -} - -/// Draft-16 answers a REQUEST_UPDATE on the control stream by the update's own Request -/// ID, which names no stream, so each update we send is recorded against the request it -/// rides, in [`Shared::updates`], before it leaves. Drafts 14 and 15 answer no update of -/// ours that anything reads. -struct UpdateRouting { - shared: Arc, - version: Version, - /// The request whose stream carries the updates. - request_id: RequestId, - buf: BytesMut, -} - -impl UpdateRouting { - /// The next whole message, recorded first when it is an update. - fn next(&mut self) -> Result, crate::Error> { - let Some((type_id, len, mut body)) = whole_message(&self.buf, self.version) else { - return Ok(None); - }; - if type_id == ietf::SubscribeUpdate::ID { - let own = RequestId::decode(&mut body, self.version)?; - self.shared.route_update(own, self.request_id); - } - Ok(Some(self.buf.split_to(len).freeze())) - } -} - -/// The type, whole length, and body of the first message in `buf`, once all of it is there. -fn whole_message(buf: &[u8], version: Version) -> Option<(u64, usize, Decoder<'_>)> { - let mut r = Decoder::new(buf, version.into()); - let type_id = r.varint().ok()?; - let size = r.u16().ok()? as usize; - let len = buf.len() - r.remaining() + size; - let body = r.sub(size).ok()?; - Some((type_id, len, body)) -} - -impl crate::transport::poll::SendStream for VirtualSendStream { - type Error = crate::Error; - type Group = crate::transport::poll::Ungrouped; - - fn poll_write(&mut self, _cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - let len = buf.len(); - Poll::Ready(self.push(Bytes::copy_from_slice(buf)).map(|()| len)) - } - - fn poll_write_buf(&mut self, _cx: &mut Context<'_>, buf: &mut B) -> Poll> { - // Taking the whole buffer is safe because this never returns Pending; a - // `Bytes` source hands its chunk over without a copy. - let len = buf.remaining(); - let chunk = buf.copy_to_bytes(len); - Poll::Ready(self.push(chunk).map(|()| len)) - } - - fn set_priority(&mut self, _order: i64) {} - - fn set_group(&mut self, group: &Self::Group) { - match *group {} - } - - fn finish(&mut self) -> Result<(), Self::Error> { - // Flush any remaining buffered data (e.g. if registration never completed). - if let Some(pending) = self.pending.take() - && !pending.buf.is_empty() - { - let _ = self.control_tx.push(pending.buf.freeze()); - } - if let Some(updates) = self.updates.take() - && !updates.buf.is_empty() - { - let _ = self.control_tx.push(updates.buf.freeze()); - } - Ok(()) - } - - fn reset(&mut self, _code: u32) {} - - fn poll_closed(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } -} - -// === Adapter Send/Recv Enums === - -/// A failure on an adapter stream. A real stream keeps its transport's error, so the negotiated -/// registry decodes a peer's reset rather than mistaking it for a lost connection. -/// Virtual streams have only local errors. -#[derive(Debug, thiserror::Error)] -#[non_exhaustive] -pub enum AdapterError { - #[error(transparent)] - Real(E), - #[error(transparent)] - Virtual(crate::Error), -} - -impl crate::transport::Error for AdapterError { - fn close_kind(&self) -> crate::transport::CloseKind { - match self { - Self::Real(err) => err.close_kind(), - Self::Virtual(err) => err.close_kind(), - } - } - - fn session_error(&self) -> Option<(u32, String)> { - match self { - Self::Real(err) => err.session_error(), - Self::Virtual(err) => err.session_error(), - } - } - - fn stream_error(&self) -> Option { - match self { - Self::Real(err) => err.stream_error(), - Self::Virtual(err) => err.stream_error(), - } - } -} - -pub(crate) enum AdapterSend { - Real(crate::transport::boxed::SendStream), - Virtual(VirtualSendStream), -} - -impl crate::transport::poll::SendStream for AdapterSend { - type Error = AdapterError<::Error>; - type Group = ::Group; - - fn poll_write(&mut self, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - match self { - Self::Real(s) => s.poll_write(cx, buf).map_err(AdapterError::Real), - Self::Virtual(s) => s.poll_write(cx, buf).map_err(AdapterError::Virtual), - } - } - - fn poll_write_buf(&mut self, cx: &mut Context<'_>, buf: &mut B) -> Poll> { - match self { - Self::Real(s) => s.poll_write_buf(cx, buf).map_err(AdapterError::Real), - Self::Virtual(s) => s.poll_write_buf(cx, buf).map_err(AdapterError::Virtual), - } - } - - fn set_priority(&mut self, order: i64) { - match self { - Self::Real(s) => s.set_priority(order), - Self::Virtual(s) => s.set_priority(order), - } - } - - fn set_group(&mut self, group: &Self::Group) { - match self { - Self::Real(s) => s.set_group(group), - // Rides the control stream, which no group schedules. - Self::Virtual(_) => {} - } - } - - fn finish(&mut self) -> Result<(), Self::Error> { - match self { - Self::Real(s) => s.finish().map_err(AdapterError::Real), - Self::Virtual(s) => s.finish().map_err(AdapterError::Virtual), - } - } - - fn reset(&mut self, code: u32) { - match self { - Self::Real(s) => s.reset(code), - Self::Virtual(s) => s.reset(code), - } - } - - fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll> { - match self { - Self::Real(s) => s.poll_closed(cx).map_err(AdapterError::Real), - Self::Virtual(s) => s.poll_closed(cx).map_err(AdapterError::Virtual), - } - } -} - -#[cfg_attr( - target_pointer_width = "64", - expect( - clippy::large_enum_variant, - reason = "one per stream, so boxing a variant would add an allocation per stream to save bytes nothing holds in bulk" - ) -)] -pub(crate) enum AdapterRecv { - Real(crate::transport::boxed::RecvStream), - Virtual(crate::transport::poll::Buffered), -} - -impl crate::transport::poll::RecvStream for AdapterRecv { - type Error = AdapterError<::Error>; - type Lease<'a> = AdapterLease<'a>; - - fn poll_lease(&mut self, cx: &mut Context<'_>, want: usize) -> Poll, Self::Error>> { - match self { - Self::Real(s) => s - .poll_lease(cx, want) - .map(|r| r.map(AdapterLease::Real).map_err(AdapterError::Real)), - Self::Virtual(s) => s - .poll_lease(cx, want) - .map(|r| r.map(AdapterLease::Virtual).map_err(AdapterError::Virtual)), - } - } - - fn set_priority(&mut self, priority: i64) { - match self { - Self::Real(s) => s.set_priority(priority), - // Rides the control stream, which carries its credit. - Self::Virtual(_) => {} - } - } - - fn stop(&mut self, code: u32) { - match self { - Self::Real(s) => s.stop(code), - Self::Virtual(s) => s.stop(code), - } - } -} - -/// A [`Lease`](crate::transport::poll::Lease) of an [`AdapterRecv`]. -pub(crate) enum AdapterLease<'a> { - Real(::Lease<'a>), - Virtual(crate::transport::poll::BufferedLease<'a>), -} - -impl Buf for AdapterLease<'_> { - fn remaining(&self) -> usize { - match self { - Self::Real(l) => l.remaining(), - Self::Virtual(l) => l.remaining(), - } - } - - fn chunk(&self) -> &[u8] { - match self { - Self::Real(l) => l.chunk(), - Self::Virtual(l) => l.chunk(), - } - } - - fn advance(&mut self, n: usize) { - match self { - Self::Real(l) => l.advance(n), - Self::Virtual(l) => l.advance(n), - } - } - - fn copy_to_bytes(&mut self, n: usize) -> Bytes { - match self { - Self::Real(l) => l.copy_to_bytes(n), - Self::Virtual(l) => l.copy_to_bytes(n), - } - } -} - -impl crate::transport::poll::Lease for AdapterLease<'_> { - fn is_fin(&self) -> bool { - match self { - Self::Real(l) => l.is_fin(), - Self::Virtual(l) => l.is_fin(), - } - } - - fn chunks(&self) -> impl Iterator { - let (real, virt) = match self { - Self::Real(l) => (Some(l.chunks()), None), - Self::Virtual(l) => (None, Some(l.chunks())), - }; - real.into_iter().flatten().chain(virt.into_iter().flatten()) - } - - fn release(self) { - match self { - Self::Real(l) => l.release(), - Self::Virtual(l) => l.release(), - } - } -} - -// === Control Stream Adapter === - -/// Which request a namespace names: an advertisement, by the side that made it, or the -/// peer's namespace subscription. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Direction { - /// We advertised it to the peer, on a stream from `open_bi`. - Outgoing, - /// The peer advertised it to us, on a stream handed to `accept_bi`. - Incoming, - /// The peer subscribed to it with SUBSCRIBE_NAMESPACE, on a stream handed to - /// `accept_bi`. Only draft-14's UNSUBSCRIBE_NAMESPACE names one. - Subscribed, -} - -/// Namespace → request_id reverse lookup for the v14/v15 messages that name a -/// namespace instead of a request id. -/// -/// Direction is part of the key because a cluster mesh has both endpoints -/// advertising the same namespace to each other. One map would let the second -/// advertisement overwrite the first, and a terminal message would then tear -/// down the stream travelling the other way while its real target stayed open. -/// A subscription keeps a map of its own for the same reason: a peer may both -/// advertise a namespace and subscribe to it. -#[derive(Default)] -struct Namespaces { - state: Mutex, -} - -#[derive(Default)] -struct NamespacesState { - outgoing: HashMap, - incoming: HashMap, - subscribed: HashMap, - - /// request_id → the namespace that request advertised, one entry per request - /// including queued duplicates, so [`Namespaces::forget`] removes only that - /// request from its namespace. The two directions share this map because - /// moq-transport gives each peer its own request id space. - by_request: HashMap, -} - -struct NamespaceRequests { - first: RequestId, - last: RequestId, -} - -struct NamespaceRegistration { - direction: Direction, - namespace: PathOwned, - previous: Option, - next: Option, -} - -impl NamespacesState { - fn map(&mut self, direction: Direction) -> &mut HashMap { - match direction { - Direction::Outgoing => &mut self.outgoing, - Direction::Incoming => &mut self.incoming, - Direction::Subscribed => &mut self.subscribed, - } - } -} - -impl Namespaces { - /// Record a request's namespace, leaving a live advertisement of the same name alone. - /// - /// The first request in a direction owns the name until it ends. A peer that - /// advertises the same namespace twice would otherwise move the name onto the second - /// request, and the withdrawal it eventually names would close that one while the - /// first stayed open, advertised for the life of the session. - fn register(&self, direction: Direction, namespace: PathOwned, request_id: RequestId) { - let mut state = self.state.lock().unwrap(); - if state.by_request.contains_key(&request_id) { - return; - } - let previous = match state.map(direction).entry(namespace.clone()) { - std::collections::hash_map::Entry::Occupied(mut entry) => { - Some(std::mem::replace(&mut entry.get_mut().last, request_id)) - } - std::collections::hash_map::Entry::Vacant(entry) => { - entry.insert(NamespaceRequests { - first: request_id, - last: request_id, - }); - None - } - }; - if let Some(previous) = previous { - state.by_request.get_mut(&previous).unwrap().next = Some(request_id); - } - state.by_request.insert( - request_id, - NamespaceRegistration { - direction, - namespace, - previous, - next: None, - }, - ); - } - - /// The namespace the peer's request `request_id` advertises, if it is a PUBLISH_NAMESPACE. - fn incoming(&self, request_id: RequestId) -> Option { - let state = self.state.lock().unwrap(); - let registration = state.by_request.get(&request_id)?; - (registration.direction == Direction::Incoming).then(|| registration.namespace.clone()) - } - - fn get(&self, direction: Direction, namespace: &PathOwned) -> Option { - self.state - .lock() - .unwrap() - .map(direction) - .get(namespace) - .map(|requests| requests.first) - } - - /// Release whatever a finished request holds, so its namespace can be advertised again. - fn forget(&self, request_id: RequestId) { - let mut state = self.state.lock().unwrap(); - let Some(registration) = state.by_request.remove(&request_id) else { - return; - }; - - // Link around the removed request without scanning a burst of duplicates. - if let Some(previous) = registration.previous { - state.by_request.get_mut(&previous).unwrap().next = registration.next; - } - if let Some(next) = registration.next { - state.by_request.get_mut(&next).unwrap().previous = registration.previous; - } - let map = state.map(registration.direction); - if registration.previous.is_none() && registration.next.is_none() { - map.remove(®istration.namespace); - } else { - let requests = map.get_mut(®istration.namespace).unwrap(); - if requests.first == request_id { - requests.first = registration.next.unwrap(); - } - if requests.last == request_id { - requests.last = registration.previous.unwrap(); - } - } - } -} - -#[derive(Default)] -struct Shared { - /// New virtual streams for accept_bi; any caller can pop directly. - incoming: Queue<(VirtualSendStream, VirtualRecvStream)>, - - /// Messages every VirtualSendStream writes; the run_write task drains this - /// onto the real control stream. - control: Queue, - - /// Active virtual streams keyed by request_id. Removing an entry drops its - /// writer, which FINs the matching VirtualRecvStream. - streams: Mutex>>, - - /// Draft-16: our REQUEST_UPDATEs' own Request IDs → the open request each updates, so - /// the one answer each is owed reaches that request's stream. Bounded by what we sent, - /// and an entry goes with its answer or its request. Locked after `streams`. - updates: Mutex>, - - /// Namespace → request_id reverse lookup (for v14/v15 namespace-keyed messages). - namespaces: Namespaces, - - /// The peer's advertisements held back behind a withdrawal still being handled. - readvertised: Mutex, -} - -/// Keeps the peer's withdrawal and re-advertisement of a namespace in wire order. -/// -/// Each request is served by its own handler, so a PUBLISH_NAMESPACE that follows a -/// PUBLISH_NAMESPACE_DONE for the same namespace could otherwise be handled first. It -/// would then read as a second advertisement of a namespace still advertised, and the -/// withdrawal as the end of the first, so the route never ends and starts again. The -/// re-advertisement reaches `accept_bi` only once the withdrawn request's handler drops -/// its stream, which it does after releasing the route. Bounded by the peer's requests. -#[derive(Default)] -struct Readvertised { - namespaces: HashMap, - /// Each withdrawn request a handler still holds, to the namespace it advertised. - withdrawn: HashMap, -} - -#[derive(Default)] -struct Withdrawing { - /// Withdrawn requests whose handlers still hold their streams. - handling: HashSet, - /// The advertisements behind them, in wire order. - held: VecDeque, -} - -struct HeldRequest { - request_id: RequestId, - stream: (VirtualSendStream, VirtualRecvStream), - /// Withdrawn while held, so the advertisements behind it wait for its handler too. - withdrawn: bool, -} - -impl Shared { - /// Mint the local half of a request we're about to send. Registration waits - /// until the first write reveals the request_id. - fn open_outgoing(self: &Arc, version: Version) -> (VirtualSendStream, VirtualRecvStream) { - let follow = Queue::new(); - let request_id = Arc::new(Mutex::new(None)); - let recv = VirtualRecvStream::new(Bytes::new(), follow.clone(), Arc::clone(self), Arc::clone(&request_id)); - let send = VirtualSendStream::with_registration( - self.control.clone(), - OutgoingRegistration { - follow_tx: follow.writer(), - shared: Arc::clone(self), - version, - buf: BytesMut::new(), - request_id, - }, - ); - (send, recv) - } - - /// Register the peer's new request and queue its stream for accept_bi. - fn open_incoming( - self: &Arc, - request_id: RequestId, - raw: Bytes, - permit: Option, - ) -> Result<(), Error> { - let follow = Queue::new(); - let mut recv = VirtualRecvStream::new( - raw, - follow.clone(), - Arc::clone(self), - Arc::new(Mutex::new(Some(request_id))), - ); - recv._permit = permit; - let send = VirtualSendStream::new(self.control.clone()); - self.streams.lock().unwrap().insert(request_id, follow.writer()); - if let Some(namespace) = self.namespaces.incoming(request_id) { - let mut readvertised = self.readvertised.lock().unwrap(); - if let Some(withdrawing) = readvertised.namespaces.get_mut(&namespace) { - withdrawing.held.push_back(HeldRequest { - request_id, - stream: (send, recv), - withdrawn: false, - }); - return Ok(()); - } - } - if !self.incoming.push((send, recv)) { - return Err(Error::Closed); - } - Ok(()) - } - - /// Deliver a message to an open virtual stream, dropping it if the stream is gone. - /// An answer to one of our updates goes to the request it updated. - /// Returns whether a stream took it. - fn push(&self, request_id: RequestId, raw: Bytes) -> bool { - let streams = self.streams.lock().unwrap(); - let request_id = self.updates.lock().unwrap().remove(&request_id).unwrap_or(request_id); - match streams.get(&request_id) { - Some(tx) => tx.push(raw), - None => false, - } - } - - /// Deliver a final message and FIN the stream: the removed writer drops after it. - /// A refusal of one of our updates is only its answer, which leaves the request open. - fn close(&self, request_id: RequestId, raw: Bytes) { - if self.updates.lock().unwrap().contains_key(&request_id) { - self.push(request_id, raw); - return; - } - let tx = self.streams.lock().unwrap().remove(&request_id); - let namespace = self.namespaces.incoming(request_id); - self.namespaces.forget(request_id); - let Some(tx) = tx else { return }; - let Some(namespace) = namespace else { - tx.push(raw); - return; - }; - - // Under the lock, so a handler dropping its stream either refuses the push or - // finds the withdrawal recorded. - let mut readvertised = self.readvertised.lock().unwrap(); - if !tx.push(raw) { - return; - } - let withdrawing = readvertised.namespaces.entry(namespace.clone()).or_default(); - match withdrawing.held.iter_mut().find(|held| held.request_id == request_id) { - Some(held) => held.withdrawn = true, - None => { - withdrawing.handling.insert(request_id); - readvertised.withdrawn.insert(request_id, namespace); - } - } - } - - /// Expect the answer to our update `own` on `request_id`'s stream, while it is open. - fn route_update(&self, own: RequestId, request_id: RequestId) { - let streams = self.streams.lock().unwrap(); - if streams.contains_key(&request_id) { - self.updates.lock().unwrap().insert(own, request_id); - } - } - - /// Drop every routing entry a request owns, whether or not its stream is still around. - fn forget(&self, request_id: RequestId) { - let mut streams = self.streams.lock().unwrap(); - streams.remove(&request_id); - let mut updates = self.updates.lock().unwrap(); - if !updates.is_empty() { - updates.retain(|_, updated| *updated != request_id); - } - drop((updates, streams)); - self.namespaces.forget(request_id); - self.handled(request_id); - } - - /// A withdrawn request's handler is done with it: once none of the namespace's - /// withdrawals is still being handled, hand over what was held behind them, up to and - /// including the next one withdrawn. - fn handled(&self, request_id: RequestId) { - // What a closed session refused, dropped only once the lock is released, since - // dropping a stream lands back here. - let mut refused = Vec::new(); - let mut readvertised = self.readvertised.lock().unwrap(); - let Some(namespace) = readvertised.withdrawn.remove(&request_id) else { - return; - }; - let Readvertised { namespaces, withdrawn } = &mut *readvertised; - let Some(withdrawing) = namespaces.get_mut(&namespace) else { - return; - }; - withdrawing.handling.remove(&request_id); - while withdrawing.handling.is_empty() { - let Some(held) = withdrawing.held.pop_front() else { - namespaces.remove(&namespace); - break; - }; - if let Some(stream) = self.incoming.offer(held.stream) { - // The session is gone, so nothing held behind this one is accepted either. - refused.push(stream); - refused.extend(withdrawing.held.drain(..).map(|held| held.stream)); - namespaces.remove(&namespace); - break; - } - if held.withdrawn { - withdrawing.handling.insert(held.request_id); - withdrawn.insert(held.request_id, namespace.clone()); - } - } - drop(readvertised); - drop(refused); - } -} - -#[derive(Clone)] -pub(crate) struct ControlStreamAdapter { - stats: crate::stats::Session, - inner: Transport, - shared: Arc, - control: Control, - version: Version, - // Bridges Context-based polls onto the kio queues; empty on clone. - park: kio::Park, -} - -impl ControlStreamAdapter { - pub fn new(inner: Transport, control: Control, version: Version) -> Self { - Self { - stats: Default::default(), - inner, - shared: Arc::new(Shared::default()), - control, - version, - park: kio::Park::default(), - } - } - - pub(super) fn with_stats(mut self, stats: &crate::stats::Session) -> Self { - self.stats = stats.clone(); - self - } - - /// Open a real (non-virtual) bidi stream, bypassing control stream multiplexing. - /// Used for v16 SubscribeNamespace which moved to its own bidi stream. - pub async fn open_native_bi(&mut self) -> Result<(AdapterSend, AdapterRecv), crate::Error> { - let (send, recv) = kio::wait(|waiter| self.inner.poll_open_request(&mut waiter.context(), Default::default())) - .await - .map_err(crate::Error::from_transport)? - .ok_or(crate::Error::NoStreamCredit)?; - Ok((AdapterSend::Real(send), AdapterRecv::Real(recv))) - } - - /// Run the control stream read + write tasks. - /// This reads from the control stream and routes messages to virtual streams, - /// and also drains the write queue to the control stream writer. A decoded - /// GOAWAY is surfaced through `goaway` so [`crate::Session::recv_goaway`] resolves - /// instead of the session tearing down. - pub async fn run( - &self, - reader: Reader, - writer: Writer, - goaway: crate::goaway::Protocol, - ) -> Result<(), Error> { - let res = { - let mut read = std::pin::pin!(self.run_read(reader, goaway)); - let mut write = std::pin::pin!(self.run_write(writer)); - kio::wait(|waiter| { - if let Poll::Ready(res) = waiter.poll_future(read.as_mut()) { - return Poll::Ready(res); - } - // Queued before the write drains, so a grant goes out in the same turn. - while let Poll::Ready(max) = self.control.poll_grant(waiter) { - self.send_max_request_id(max); - } - if let Poll::Ready(res) = waiter.poll_future(write.as_mut()) { - return Poll::Ready(res); - } - Poll::Pending - }) - .await - }; - - // The control stream is dead, so the mux is too: fail future virtual - // writes fast and end accept_bi instead of buffering into a queue nobody - // will ever drain. - self.shared.control.close(); - self.shared.incoming.close(); - - res - } - - /// Queue a GOAWAY message onto the shared control stream (draft-14-16). - /// - /// Framed as `[type_id varint][size u16][body]` like every other control - /// message. Best-effort: an encode failure or a closed control stream is - /// logged and dropped, since the session is tearing down anyway. - pub(super) fn send_goaway(&self, uri: &str, timeout_ms: u64, version: Version) { - let msg = crate::ietf::GoAway { - new_session_uri: std::borrow::Cow::Borrowed(uri), - timeout: timeout_ms, - request_id: None, - }; - - if let Err(err) = self.queue_message(&msg, version) { - tracing::warn!(%err, "failed to encode goaway"); - } - } - - /// Refuse an update no request here can apply, naming its own Request ID. - fn refuse_update(&self, request_id: RequestId) { - if self.version == Version::Draft14 { - return; - } - let msg = ietf::RequestError { - request_id: Some(request_id), - error_code: super::error::request::to_code( - &crate::Error::Unsupported, - super::error::request::Kind::Subscribe, - self.version, - ), - reason_phrase: "update not supported".into(), - retry_interval: 0, - }; - if let Err(err) = self.queue_message(&msg, self.version) { - tracing::warn!(%err, "failed to encode REQUEST_ERROR"); - } - } - - /// Queue a MAX_REQUEST_ID granting the peer room up to `max`. - fn send_max_request_id(&self, max: RequestId) { - if let Err(err) = self.queue_message(&ietf::MaxRequestId { request_id: max }, self.version) { - tracing::warn!(%err, "failed to encode MAX_REQUEST_ID"); - } - } - - fn queue_message(&self, msg: &T, version: Version) -> Result<(), Error> { - use crate::coding::Encode; - let mut raw = Vec::new(); - let mut encoder = Encoder::new(&mut raw, version.into()); - encoder.varint(T::ID)?; - msg.encode(&mut encoder, version)?; - let selector = crate::coding::varint::size(T::ID, version.into()).map_err(crate::coding::EncodeError::from)?; - self.stats - .control(super::message::control(T::ID, version), raw.len() - selector, false); - if !self.shared.control.push(raw.into()) { - tracing::debug!("control stream closed; message not sent"); - } - Ok(()) - } - - /// Writer task: drains the queue and writes to the control stream. The peer's - /// STOP_SENDING, seen while idle or as a failed write, is a protocol violation. - async fn run_write(&self, mut writer: Writer) -> Result<(), Error> { - loop { - let msg = kio::wait(|waiter| { - // Never finished, so any end is the peer's or the session's. - if let Poll::Ready(res) = writer.poll_closed(&mut waiter.context()) { - let err = res - .err() - .map_or(Error::ProtocolViolation, super::session::control_stream_error); - return Poll::Ready(Err(err)); - } - self.shared.control.poll_pop(waiter).map(Ok) - }) - .await?; - let Some(msg) = msg else { break }; - let mut buf = std::io::Cursor::new(msg); - writer - .write_all(&mut buf) - .await - .map_err(super::session::control_stream_error)?; - } - Ok(()) - } - - /// Dispatcher loop that reads control stream messages and routes them. - async fn run_read( - &self, - mut reader: Reader, - goaway: crate::goaway::Protocol, - ) -> Result<(), Error> { - loop { - let type_id = match reader - .varint_maybe() - .await - .map_err(super::session::control_stream_error)? - { - Some(id) => id, - None => return Err(Error::ProtocolViolation), - }; - - let body = reader - .decode::() - .await - .map_err(super::session::control_stream_error)? - .0; - - self.stats - .control(super::message::control(type_id, self.version), body.len() + 2, true); - - // Reconstruct raw message bytes: [type_id][size][body] - let raw = encode_raw(type_id, &body, self.version); - - // Classify and route - match classify(type_id, &body, self.version, &self.shared.namespaces)? { - Route::NewRequest(request_id) => { - let permit = self.control.accept(request_id)?; - self.shared.open_incoming(request_id, raw, permit)? - } - Route::Response(request_id) => { - self.shared.push(request_id, raw); - } - Route::FollowUp { own, target } => { - // SUBSCRIBE_UPDATE takes a request ID of its own (draft-14 section 9.1), - // done as soon as it is delivered, so it is granted straight back. - drop(self.control.accept(own)?); - // An update whose request is gone, or rides a stream of its own like draft 16's - // SUBSCRIBE_NAMESPACE, is still owed its one answer. - if !self.shared.push(target, raw) { - self.refuse_update(own); - } - } - Route::CloseStream(request_id) => self.shared.close(request_id, raw), - Route::MaxRequestId(max) => self.control.max_request_id(max), - Route::Ignore => {} - Route::GoAway => { - let mut data = Decoder::new(&body, self.version.into()); - let msg = crate::ietf::GoAway::decode_msg(&mut data, self.version)?; - tracing::info!(message = ?msg, "received GOAWAY"); - - // Draft-14-16 have no timeout field. - // A second GOAWAY is a protocol violation the draft requires we - // close over, so let it propagate and end the session. - goaway.record(crate::goaway::Goaway { - uri: msg.new_session_uri.into_owned(), - timeout: None, - })?; - // Keep processing control messages: existing subscriptions - // flow until the session actually closes. - } - } - } - } -} - -/// Classify a control message and extract its request_id for routing. -/// -/// Takes the namespace map rather than reading it off the adapter so the routing -/// rules can be tested without a session behind them. -fn classify(type_id: u64, body: &Bytes, version: Version, namespaces: &Namespaces) -> Result { - match type_id { - // New requests: these create new virtual streams - ietf::Subscribe::ID => { - let id = decode_request_id(body, version)?; - Ok(Route::NewRequest(id)) - } - ietf::Fetch::ID => { - let id = decode_request_id(body, version)?; - Ok(Route::NewRequest(id)) - } - ietf::Publish::ID => { - let id = decode_request_id(body, version)?; - Ok(Route::NewRequest(id)) - } - ietf::PublishNamespace::ID => { - let id = decode_request_id(body, version)?; - // Decode the namespace and store the mapping for v14/v15 reverse lookup - if let Ok(ns) = decode_publish_namespace_body(body, version) { - namespaces.register(Direction::Incoming, ns, id); - } - Ok(Route::NewRequest(id)) - } - ietf::TrackStatus::ID => { - let id = decode_request_id(body, version)?; - Ok(Route::NewRequest(id)) - } - // SubscribeNamespace on control stream (v14/v15 only) - ietf::SubscribeNamespaceLegacy::ID => match version { - // Draft-14's UNSUBSCRIBE_NAMESPACE names the prefix rather than the request. - Version::Draft14 => { - let mut r = Decoder::new(body, version.into()); - let id = RequestId::decode(&mut r, version)?; - if let Ok(prefix) = crate::ietf::namespace::decode_namespace(&mut r) { - namespaces.register(Direction::Subscribed, prefix.into_owned(), id); - } - Ok(Route::NewRequest(id)) - } - Version::Draft15 => { - let id = decode_request_id(body, version)?; - Ok(Route::NewRequest(id)) - } - _ => Err(Error::UnexpectedMessage), - }, - - // SUBSCRIBE_TRACKS (draft-18+, #1542): the half of the SUBSCRIBE_NAMESPACE split - // that subscribes to all tracks under a prefix. We don't implement PUBLISH - // replication, so this is impossible to honor. Reject loudly. - ietf::SubscribeTracks::ID => { - tracing::error!( - version = ?version, - "received SUBSCRIBE_TRACKS (0x51); not supported in moq-lite (no PUBLISH replication)" - ); - Err(Error::Unsupported) - } - - // Responses: route to the virtual stream waiting for a reply - ietf::SubscribeOk::ID => { - let id = decode_response_request_id(body, version)?; - Ok(Route::Response(id)) - } - // 0x05: SubscribeError in v14, RequestError in v15+ - ietf::SubscribeError::ID => { - let id = decode_response_request_id(body, version)?; - Ok(Route::CloseStream(id)) - } - ietf::FetchOk::ID => { - let id = decode_response_request_id(body, version)?; - Ok(Route::Response(id)) - } - // 0x19: FetchError in v14 only - ietf::FetchError::ID => match version { - Version::Draft14 => { - let id = decode_request_id(body, version)?; - Ok(Route::CloseStream(id)) - } - _ => Err(Error::UnexpectedMessage), - }, - // TRACK_STATUS_OK and TRACK_STATUS_ERROR (v14 only): the one answer the request gets. - ietf::TrackStatusOk::ID_14 | ietf::TRACK_STATUS_ERROR_14 => match version { - Version::Draft14 => { - let id = decode_request_id(body, version)?; - Ok(Route::CloseStream(id)) - } - _ => Err(Error::UnexpectedMessage), - }, - // PublishOk (0x1E) - ietf::PublishOk::ID => { - let id = decode_response_request_id(body, version)?; - Ok(Route::Response(id)) - } - // PublishError (0x1F) - v14 only - ietf::PublishError::ID => { - let id = decode_request_id(body, version)?; - Ok(Route::CloseStream(id)) - } - // 0x07: PublishNamespaceOk in v14, RequestOk in v15+ - ietf::PublishNamespaceOk::ID => match version { - Version::Draft14 => { - let id = decode_request_id(body, version)?; - Ok(Route::Response(id)) - } - Version::Draft15 | Version::Draft16 => { - // RequestOk - route to stream - let id = decode_response_request_id(body, version)?; - Ok(Route::Response(id)) - } - _ => Err(Error::UnexpectedMessage), - }, - // 0x08: PublishNamespaceError in v14 only - ietf::PublishNamespaceError::ID => match version { - Version::Draft14 => { - let id = decode_request_id(body, version)?; - Ok(Route::CloseStream(id)) - } - _ => Err(Error::UnexpectedMessage), - }, - // SubscribeNamespaceOk (v14 only) - ietf::SubscribeNamespaceOk::ID => match version { - Version::Draft14 => { - let id = decode_request_id(body, version)?; - Ok(Route::Response(id)) - } - _ => Err(Error::UnexpectedMessage), - }, - // SubscribeNamespaceError (v14 only) - ietf::SubscribeNamespaceError::ID => match version { - Version::Draft14 => { - let id = decode_request_id(body, version)?; - Ok(Route::CloseStream(id)) - } - _ => Err(Error::UnexpectedMessage), - }, - - // Follow-up messages: route to the request being updated, not the update's own id. - ietf::SubscribeUpdate::ID => { - let own = decode_request_id(body, version)?; - let target = decode_update_target(body, version)?; - Ok(Route::FollowUp { own, target }) - } - - // Close stream messages - ietf::Unsubscribe::ID => { - let id = decode_request_id(body, version)?; - Ok(Route::CloseStream(id)) - } - ietf::PublishDone::ID => { - let id = decode_response_request_id(body, version)?; - Ok(Route::CloseStream(id)) - } - ietf::FetchCancel::ID => { - let id = decode_request_id(body, version)?; - Ok(Route::CloseStream(id)) - } - ietf::PublishNamespaceDone::ID => match version { - Version::Draft16 => { - let id = decode_request_id(body, version)?; - Ok(Route::CloseStream(id)) - } - // v14/v15: namespace-keyed, so decode namespace and look up request_id. - // DONE withdraws the advertisement its sender made, so it names an - // inbound one. - Version::Draft14 | Version::Draft15 => { - match lookup_namespace_request_id(body, version, namespaces, Direction::Incoming)? { - Some(id) => Ok(Route::CloseStream(id)), - None => Ok(Route::Ignore), - } - } - _ => Err(Error::UnexpectedMessage), - }, - ietf::PublishNamespaceCancel::ID => match version { - Version::Draft16 => { - let id = decode_request_id(body, version)?; - Ok(Route::CloseStream(id)) - } - // v14/v15: namespace-keyed. CANCEL rejects an advertisement its sender - // received, so it names an outbound one. - Version::Draft14 | Version::Draft15 => { - match lookup_namespace_request_id(body, version, namespaces, Direction::Outgoing)? { - Some(id) => Ok(Route::CloseStream(id)), - None => Ok(Route::Ignore), - } - } - _ => Err(Error::UnexpectedMessage), - }, - ietf::UnsubscribeNamespace::ID => match version { - Version::Draft14 => match lookup_namespace_request_id(body, version, namespaces, Direction::Subscribed)? { - Some(id) => Ok(Route::CloseStream(id)), - None => Ok(Route::Ignore), - }, - Version::Draft15 => { - let id = decode_request_id(body, version)?; - Ok(Route::CloseStream(id)) - } - _ => Err(Error::UnexpectedMessage), - }, - - // Utility - ietf::MaxRequestId::ID => { - let id = decode_request_id(body, version)?; - Ok(Route::MaxRequestId(id)) - } - // The peer reached the MAX_REQUEST_ID we advertised and waits for a grant, which - // follows on its own as its requests close. - ietf::RequestsBlocked::ID => Ok(Route::Ignore), - - // Terminal - ietf::GoAway::ID => Ok(Route::GoAway), - - _ => Err(Error::UnexpectedMessage), - } -} - -/// Decode the namespace from a v14/v15 namespace-keyed message body and look up the -/// request_id of the advertisement it terminates, travelling in the given direction. -/// -/// `None` when nothing holds that name: the request it named is already gone. There is -/// nothing left to close, so the message is dropped rather than ending the session. -fn lookup_namespace_request_id( - body: &Bytes, - version: Version, - namespaces: &Namespaces, - direction: Direction, -) -> Result, Error> { - let mut r = Decoder::new(body, version.into()); - let ns = crate::ietf::namespace::decode_namespace(&mut r)?; - Ok(namespaces.get(direction, &ns)) -} - -impl crate::transport::poll::Session for ControlStreamAdapter { - type SendStream = AdapterSend; - type RecvStream = AdapterRecv; - type Error = crate::Error; - - fn poll_accept_bi( - &mut self, - cx: &mut Context<'_>, - ) -> Poll> { - // v16: SubscribeNamespace uses real bidi streams, so poll both sources. - // Native first: real bidi streams are rare and transport-paced, so they - // can't starve the queue, while a control-message flood could starve a - // native SubscribeNamespace under queue-first order. - if self.version == Version::Draft16 { - match self.inner.poll_accept_bi(cx) { - Poll::Ready(Ok((send, recv))) => { - return Poll::Ready(Ok((AdapterSend::Real(send), AdapterRecv::Real(recv)))); - } - Poll::Ready(Err(_)) => return Poll::Ready(Err(crate::Error::Closed)), - Poll::Pending => {} - } - } - - let waiter = self.park.hold(cx); - match std::task::ready!(self.shared.incoming.poll_pop(waiter)) { - Some((send, recv)) => Poll::Ready(Ok(( - AdapterSend::Virtual(send), - AdapterRecv::Virtual(crate::transport::poll::Buffered::new(recv)), - ))), - None => Poll::Ready(Err(crate::Error::Closed)), - } - } - - fn poll_open_bi( - &mut self, - _cx: &mut Context<'_>, - // Rides the control stream, which no group schedules. - _: crate::transport::poll::SendOptions<'_, ::Group>, - ) -> Poll> { - let (send, recv) = self.shared.open_outgoing(self.version); - Poll::Ready(Ok(( - AdapterSend::Virtual(send), - AdapterRecv::Virtual(crate::transport::poll::Buffered::new(recv)), - ))) - } - - fn poll_open_request( - &mut self, - cx: &mut std::task::Context<'_>, - options: crate::transport::poll::SendOptions< - '_, - ::Group, - >, - ) -> std::task::Poll>, Self::Error>> { - self.poll_open_bi(cx, options).map_ok(Some) - } - - fn poll_open_uni( - &mut self, - cx: &mut Context<'_>, - options: crate::transport::poll::SendOptions< - '_, - ::Group, - >, - ) -> Poll> { - self.inner - .poll_open_uni(cx, options) - .map(|r| r.map(AdapterSend::Real).map_err(|_| crate::Error::Closed)) - } - - fn poll_accept_uni(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner - .poll_accept_uni(cx) - .map(|r| r.map(AdapterRecv::Real).map_err(|_| crate::Error::Closed)) - } - - fn send_group(&mut self, priority: i64) -> Option<::Group> { - self.inner.send_group(priority) - } - - fn send_datagram( - &mut self, - payload: Bytes, - options: crate::transport::poll::SendOptions< - '_, - ::Group, - >, - ) -> Result<(), Self::Error> { - self.inner - .send_datagram(payload, options) - .map_err(|_| crate::Error::Closed) - } - - fn poll_recv_datagram(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner - .poll_recv_datagram(cx) - .map(|r| r.map_err(|_| crate::Error::Closed)) - } - - fn max_datagram_size(&self) -> usize { - self.inner.max_datagram_size() - } - - fn binding(&self) -> crate::transport::Binding { - self.inner.binding() - } - fn protocol(&self) -> Option<&str> { - self.inner.protocol() - } - - fn close(&mut self, code: u32, reason: &str) { - self.inner.close(code, reason) - } - - fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll { - self.inner.poll_closed(cx).map(|_| crate::Error::Closed) - } - - fn stats(&self) -> impl crate::transport::Stats { - self.inner.stats() - } -} - -// === Message Classification === - -#[derive(Debug)] -enum Route { - NewRequest(RequestId), - Response(RequestId), - /// An update: its own request ID counts against the window, and it goes to `target`. - FollowUp { - own: RequestId, - target: RequestId, - }, - CloseStream(RequestId), - MaxRequestId(RequestId), - /// Nothing to route: the message named a request that is already gone. - Ignore, - GoAway, -} - -/// Encode raw message bytes as [type_id varint][size u16][body]. -fn encode_raw(type_id: u64, body: &Bytes, version: Version) -> Bytes { - let mut buf = Vec::new(); - let mut w = Encoder::new(&mut buf, version.into()); - w.varint(type_id).expect("type_id was read from the same wire"); - w.u16(u16::try_from(body.len()).expect("body was read with a u16 size")); - w.slice(body); - buf.into() -} - -/// Decode just the request_id from the beginning of a message body. -fn decode_request_id(body: &Bytes, version: Version) -> Result { - let (request_id, _) = RequestId::decode_slice(body, version)?; - Ok(request_id) -} - -/// The request a SUBSCRIBE_UPDATE or REQUEST_UPDATE applies to. -/// -/// Drafts 14 and 15 name it in Subscription Request ID, and draft 16 in Existing -/// Request ID. Both are the second field; the first is the update's own new -/// Request ID. Later drafts send updates on the request's own stream, not here. -fn decode_update_target(body: &Bytes, version: Version) -> Result { - match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => { - let mut r = Decoder::new(body, version.into()); - let _own = RequestId::decode(&mut r, version)?; - Ok(RequestId::decode(&mut r, version)?) - } - _ => Err(Error::UnexpectedMessage), - } -} - -/// Decode request_id for response messages that have Option in v14-16. -fn decode_response_request_id(body: &Bytes, version: Version) -> Result { - // In v14-16, response messages always have request_id present - decode_request_id(body, version) -} - -/// Decode the namespace from a PublishNamespace message body (after the request_id). -fn decode_publish_namespace_body(body: &Bytes, version: Version) -> Result { - let mut r = Decoder::new(body, version.into()); - // Skip request_id - let _request_id = RequestId::decode(&mut r, version)?; - // v17 has required_request_id_delta - if version == Version::Draft17 { - r.varint()?; - } - let ns = crate::ietf::namespace::decode_namespace(&mut r)?; - Ok(ns.into_owned()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::coding::Encode; - use crate::transport::poll::{RecvStream as _, SendStream as _}; - use futures::FutureExt as _; - - fn make_body_with_request_id(id: u64, version: Version) -> Bytes { - RequestId(id).encode_bytes(version).unwrap() - } - - /// Classify against an empty namespace map, for the messages that don't use it. - fn classify_msg(version: Version, type_id: u64, body: &Bytes) -> Result { - classify(type_id, body, version, &Namespaces::default()) - } - - /// A receive stream with no request behind it, for exercising the reader alone. - fn detached_recv(initial: Bytes, rx: Queue) -> crate::transport::poll::Buffered { - crate::transport::poll::Buffered::new(VirtualRecvStream::new( - initial, - rx, - Arc::new(Shared::default()), - Arc::new(Mutex::new(None)), - )) - } - - #[test] - fn test_classify_subscribe_new_request() { - let body = make_body_with_request_id(42, Version::Draft15); - let route = classify_msg(Version::Draft15, ietf::Subscribe::ID, &body).unwrap(); - assert!(matches!(route, Route::NewRequest(RequestId(42)))); - } - - #[test] - fn test_classify_fetch_new_request() { - let body = make_body_with_request_id(10, Version::Draft14); - let route = classify_msg(Version::Draft14, ietf::Fetch::ID, &body).unwrap(); - assert!(matches!(route, Route::NewRequest(RequestId(10)))); - } - - #[test] - fn test_classify_publish_new_request() { - let body = make_body_with_request_id(5, Version::Draft16); - let route = classify_msg(Version::Draft16, ietf::Publish::ID, &body).unwrap(); - assert!(matches!(route, Route::NewRequest(RequestId(5)))); - } - - #[test] - fn test_classify_subscribe_ok_response() { - let body = make_body_with_request_id(42, Version::Draft15); - let route = classify_msg(Version::Draft15, ietf::SubscribeOk::ID, &body).unwrap(); - assert!(matches!(route, Route::Response(RequestId(42)))); - } - - #[test] - fn test_classify_request_error_v15_closes_stream() { - let body = make_body_with_request_id(7, Version::Draft15); - let route = classify_msg(Version::Draft15, ietf::SubscribeError::ID, &body).unwrap(); - assert!(matches!(route, Route::CloseStream(RequestId(7)))); - } - - #[test] - fn test_classify_request_ok_v15_response() { - let body = make_body_with_request_id(3, Version::Draft15); - let route = classify_msg(Version::Draft15, ietf::PublishNamespaceOk::ID, &body).unwrap(); - assert!(matches!(route, Route::Response(RequestId(3)))); - } - - #[test] - fn test_classify_unsubscribe_closes_stream() { - let body = make_body_with_request_id(99, Version::Draft14); - let route = classify_msg(Version::Draft14, ietf::Unsubscribe::ID, &body).unwrap(); - assert!(matches!(route, Route::CloseStream(RequestId(99)))); - } - - /// Drafts 14 and 15 name the subscription in the second field; draft 16 names - /// the existing request there. The first field is the update's own Request ID, - /// so a body that carries only that ID cannot tell the two apart. - #[moq_clock::test] - async fn test_subscribe_update_reaches_its_target() { - let update_id = RequestId(10); - let target_id = RequestId(4); - - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { - let update = ietf::SubscribeUpdate { - max_age: None, - new_group: None, - request_id: update_id, - subscription_request_id: Some(target_id), - start_location: ietf::Location { group: 1, object: 2 }, - end_group: 100, - subscriber_priority: Some(200), - forward: Some(false), - filter: Some(ietf::Filter::NextObject), - authorization_token: None, - fill: None, - }; - let body = encode_body(&update, version); - let raw = encode_raw(ietf::SubscribeUpdate::ID, &body, version); - let Route::FollowUp { own, target: routed } = - classify_msg(version, ietf::SubscribeUpdate::ID, &body).unwrap() - else { - panic!("{version} did not route the update as a follow-up"); - }; - assert_eq!(own, update_id, "{version}"); - - let shared = Arc::new(Shared::default()); - shared - .open_incoming(target_id, Bytes::from_static(b"request"), None) - .unwrap(); - let (_, mut recv) = shared.incoming.pop().await.unwrap(); - assert_eq!( - read_virtual(&mut recv).await.unwrap(), - Some(Bytes::from_static(b"request")) - ); - - assert_eq!(routed, target_id, "{version}: update {update_id} routed to {routed}"); - shared.push(routed, raw.clone()); - assert_eq!( - read_virtual(&mut recv).await.unwrap(), - Some(raw), - "{version}: update {update_id} did not reach {target_id}" - ); - } - } - - /// Draft-16 answers our REQUEST_UPDATE by the update's own Request ID. The answer - /// reaches the request it updated, a refusal leaves that request open, and an update - /// written in pieces is recorded all the same. - #[moq_clock::test] - async fn our_update_answers_reach_their_request() { - const VERSION: Version = Version::Draft16; - let shared = Arc::new(Shared::default()); - let (mut send, mut recv) = shared.open_outgoing(VERSION); - let update = |request_id| { - encode_msg( - &ietf::PublishNamespaceUpdate { - request_id: RequestId(request_id), - namespace_request_id: Some(RequestId(4)), - hops: None, - cost: None, - authorization_token: None, - }, - VERSION, - ) - }; - - // The request and the first byte of its update in one write, the rest in another. - let first = update(6); - let request = encode_msg(&publish_namespace(RequestId(4), "ns"), VERSION); - send.write_chunk([request.clone(), first.slice(..1)].concat().into()) - .await - .unwrap(); - send.write_chunk(first.slice(1..)).await.unwrap(); - assert_eq!(shared.control.pop().await.unwrap(), request); - assert_eq!(shared.control.pop().await.unwrap(), first); - - let ok = ietf::RequestOk { - largest: None, - request_id: Some(RequestId(6)), - active: None, - }; - assert!(matches!( - classify_msg(VERSION, ietf::RequestOk::ID, &encode_body(&ok, VERSION)).unwrap(), - Route::Response(RequestId(6)) - )); - let ok = encode_msg(&ok, VERSION); - assert!(shared.push(RequestId(6), ok.clone()), "the answer found no stream"); - assert_eq!(read_virtual(&mut recv).await.unwrap(), Some(ok.clone())); - assert!(!shared.push(RequestId(6), ok), "a second answer was routed"); - - send.write_chunk(update(8)).await.unwrap(); - assert_eq!(shared.control.pop().await.unwrap(), update(8)); - let error = encode_msg( - &ietf::RequestError { - request_id: Some(RequestId(8)), - error_code: 0, - reason_phrase: "refused".into(), - retry_interval: 0, - }, - VERSION, - ); - shared.close(RequestId(8), error.clone()); - assert_eq!(read_virtual(&mut recv).await.unwrap(), Some(error)); - assert!( - shared.push(RequestId(4), Bytes::from_static(b"later")), - "the refusal closed the request" - ); - - // An update outstanding when its request goes leaves nothing behind. - send.write_chunk(update(10)).await.unwrap(); - assert_eq!(shared.updates.lock().unwrap().len(), 1); - drop(recv); - assert!(shared.updates.lock().unwrap().is_empty()); - } - - /// Before draft-16 an answer to our update names nothing we route. - #[moq_clock::test] - async fn draft_15_update_answers_are_not_routed() { - const VERSION: Version = Version::Draft15; - let shared = Arc::new(Shared::default()); - let (mut send, _recv) = shared.open_outgoing(VERSION); - send.write_chunk(encode_msg(&publish_namespace(RequestId(4), "ns"), VERSION)) - .await - .unwrap(); - // A SUBSCRIBE_UPDATE's header and its own Request ID 6, all that routing reads. - send.write_chunk(Bytes::from_static(&[0x02, 0x00, 0x01, 0x06])) - .await - .unwrap(); - assert!(!shared.push(RequestId(6), Bytes::from_static(b"ok"))); - assert!(shared.updates.lock().unwrap().is_empty()); - } - - #[test] - fn test_classify_goaway() { - let body = Bytes::new(); - let route = classify_msg(Version::Draft14, ietf::GoAway::ID, &body).unwrap(); - assert!(matches!(route, Route::GoAway)); - } - - #[test] - fn test_classify_max_request_id() { - let body = make_body_with_request_id(100, Version::Draft14); - let route = classify_msg(Version::Draft14, ietf::MaxRequestId::ID, &body).unwrap(); - assert!(matches!(route, Route::MaxRequestId(RequestId(100)))); - } - - #[test] - fn test_classify_subscribe_namespace_v14_new_request() { - let body = make_body_with_request_id(20, Version::Draft14); - let route = classify_msg(Version::Draft14, ietf::SubscribeNamespaceLegacy::ID, &body).unwrap(); - assert!(matches!(route, Route::NewRequest(RequestId(20)))); - } - - #[test] - fn test_classify_subscribe_namespace_v16_errors() { - let body = make_body_with_request_id(20, Version::Draft16); - let result = classify_msg(Version::Draft16, ietf::SubscribeNamespaceLegacy::ID, &body); - assert!(result.is_err()); - } - - #[test] - fn test_classify_unknown_message() { - let body = Bytes::new(); - let result = classify_msg(Version::Draft14, 0xFF, &body); - assert!(result.is_err()); - } - - #[test] - fn test_encode_raw_roundtrip() { - let version = Version::Draft15; - let body = Bytes::from_static(b"hello"); - let raw = encode_raw(0x03, &body, version); - - // Decode the raw bytes - let mut r = Decoder::new(&raw, version.into()); - assert_eq!(r.varint().unwrap(), 0x03); - assert_eq!(r.u16().unwrap(), 5); - assert_eq!(r.rest(), b"hello"); - } - - #[moq_clock::test] - async fn test_virtual_recv_stream_reads_initial_then_followup() { - let initial = Bytes::from_static(b"initial"); - let follow = Queue::new(); - let tx = follow.writer(); - let mut stream = detached_recv(initial, follow); - - // Read initial data - let chunk = stream.read_chunk(32).await.unwrap().unwrap(); - assert_eq!(&chunk[..], b"initial"); - - // Send follow-up - tx.push(Bytes::from_static(b"followup")); - let chunk = stream.read_chunk(32).await.unwrap().unwrap(); - assert_eq!(&chunk[..], b"followup"); - - // Drop the writer → FIN - drop(tx); - let result = stream.read_chunk(32).await.unwrap(); - assert_eq!(result, None); - } - - #[moq_clock::test] - async fn test_virtual_recv_stream_partial_reads() { - let initial = Bytes::from_static(b"hello world"); - let mut stream = detached_recv(initial, Queue::new()); - - // Read small chunks - let chunk = stream.read_chunk(5).await.unwrap().unwrap(); - assert_eq!(&chunk[..], b"hello"); - - let chunk = stream.read_chunk(5).await.unwrap().unwrap(); - assert_eq!(&chunk[..], b" worl"); - - let chunk = stream.read_chunk(1).await.unwrap().unwrap(); - assert_eq!(&chunk[..], b"d"); - } - - #[moq_clock::test] - async fn test_virtual_send_stream_errors_once_control_closes() { - // The adapter closes the control queue when its run() exits; writes must - // fail fast instead of buffering into a queue nobody drains. - let control = Queue::new(); - let mut stream = VirtualSendStream::new(control.clone()); - control.close(); - assert!(matches!(stream.write(b"hello").await, Err(Error::Closed))); - } - - #[test] - fn test_recv_stream_drop_discards_followups() { - // Dropping the reader closes the queue, so late routed messages are - // discarded instead of accumulating while the map entry lingers. - let follow = Queue::new(); - let stream = detached_recv(Bytes::new(), follow.clone()); - drop(stream); - assert!(!follow.push(Bytes::from_static(b"late"))); - } - - #[moq_clock::test] - async fn test_virtual_send_stream_writes_to_channel() { - let control = Queue::new(); - let mut stream = VirtualSendStream::new(control.clone()); - - let n = stream.write(b"hello").await.unwrap(); - assert_eq!(n, 5); - - let data = control.pop().await.unwrap(); - assert_eq!(data, &b"hello"[..]); - } - - /// Encode a message body (no type_id/size header). - fn encode_body(msg: &M, version: Version) -> Bytes { - let mut buf = Vec::new(); - msg.encode_msg(&mut Encoder::new(&mut buf, version.into()), version) - .unwrap(); - bytes::Bytes::from(buf) - } - - /// Encode a full control message: [type_id][size][body]. - fn encode_msg(msg: &M, version: Version) -> Bytes { - let body = encode_body(msg, version); - encode_raw(M::ID, &body, version) - } - - fn publish_namespace(request_id: RequestId, namespace: &str) -> ietf::PublishNamespace<'_> { - ietf::PublishNamespace { - request_id, - track_namespace: crate::Path::new(namespace), - params: Default::default(), - authorization_token: None, - } - } - - /// Advertise `namespace` to the peer on `request_id`, as `open_bi` + a write would. - async fn advertise( - shared: &Arc, - namespace: &str, - version: Version, - request_id: RequestId, - ) -> VirtualRecvStream { - let (mut send, recv) = shared.open_outgoing(version); - send.write_chunk(encode_msg(&publish_namespace(request_id, namespace), version)) - .await - .unwrap(); - recv - } - - /// Receive the peer's advertisement of `namespace` on `request_id`, as the read loop would. - async fn receive( - shared: &Arc, - namespace: &str, - version: Version, - request_id: RequestId, - ) -> VirtualRecvStream { - let msg = publish_namespace(request_id, namespace); - let route = classify( - ietf::PublishNamespace::ID, - &encode_body(&msg, version), - version, - &shared.namespaces, - ) - .unwrap(); - assert!(matches!(route, Route::NewRequest(id) if id == request_id)); - shared - .open_incoming(request_id, encode_msg(&msg, version), None) - .unwrap(); - - let (_, mut recv) = shared.incoming.pop().await.unwrap(); - - // Drain the advertisement the stream opened with, so both halves are quiescent - // and a later read only sees what the terminal message routes. - assert_eq!(read_virtual(&mut recv).await.unwrap(), Some(encode_msg(&msg, version))); - recv - } - - /// Two relays meshed together, each advertising `namespace` to the other: ours on - /// request 4, theirs on request 7. Returns their receive halves. - /// - /// `last` is the advertisement registered second, which is the one a single map - /// would leave as the sole entry. Each test passes the direction it must *not* - /// match, so a direction-blind lookup lands on the wrong stream. - async fn mesh( - namespace: &str, - version: Version, - last: Direction, - ) -> (Arc, VirtualRecvStream, VirtualRecvStream) { - let shared = Arc::new(Shared::default()); - - let (ours, theirs) = match last { - Direction::Outgoing => { - let theirs = receive(&shared, namespace, version, RequestId(7)).await; - (advertise(&shared, namespace, version, RequestId(4)).await, theirs) - } - Direction::Incoming => { - let ours = advertise(&shared, namespace, version, RequestId(4)).await; - (ours, receive(&shared, namespace, version, RequestId(7)).await) - } - Direction::Subscribed => unreachable!("a mesh only advertises"), - }; - - (shared, ours, theirs) - } - - /// The next chunk of a virtual stream, read without a lease. - fn read_virtual(stream: &mut VirtualRecvStream) -> impl Future, crate::Error>> + '_ { - std::future::poll_fn(|cx| crate::transport::poll::Chunks::poll_read_chunk(stream, cx, usize::MAX)) - } - - /// Read until the stream FINs, returning whether it did so within `limit` reads. - async fn drained(stream: &mut VirtualRecvStream, limit: usize) -> bool { - for _ in 0..limit { - if read_virtual(stream).await.unwrap().is_none() { - return true; - } - } - false - } - - #[test] - fn test_namespace_reverse_lookup_v14() { - let namespaces = Namespaces::default(); - namespaces.register(Direction::Incoming, crate::Path::new("test/ns"), RequestId(42)); - - let body = encode_body( - &ietf::PublishNamespaceDone { - track_namespace: crate::Path::new("test/ns"), - request_id: RequestId(0), - }, - Version::Draft14, - ); - - let route = classify(ietf::PublishNamespaceDone::ID, &body, Version::Draft14, &namespaces).unwrap(); - assert!(matches!(route, Route::CloseStream(RequestId(42)))); - } - - #[moq_clock::test] - async fn test_publish_namespace_done_closes_only_inbound() { - // The peer withdraws the advertisement it made, so its own stream closes and - // ours keeps running. - let version = Version::Draft14; - let (shared, mut ours, mut theirs) = mesh("cluster/ns", version, Direction::Outgoing).await; - - let done = ietf::PublishNamespaceDone { - track_namespace: crate::Path::new("cluster/ns"), - request_id: RequestId(0), - }; - let route = classify( - ietf::PublishNamespaceDone::ID, - &encode_body(&done, version), - version, - &shared.namespaces, - ) - .unwrap(); - assert!(matches!(route, Route::CloseStream(RequestId(7)))); - - shared.close(RequestId(7), encode_msg(&done, version)); - assert!(drained(&mut theirs, 4).await); - assert!(shared.streams.lock().unwrap().contains_key(&RequestId(4))); - assert!(read_virtual(&mut ours).now_or_never().is_none()); - } - - #[moq_clock::test] - async fn test_publish_namespace_cancel_closes_only_outbound() { - // The peer rejects the advertisement we made, so ours closes and the one it - // sent us keeps running. - let version = Version::Draft14; - let (shared, mut ours, mut theirs) = mesh("cluster/ns", version, Direction::Incoming).await; - - let cancel = ietf::PublishNamespaceCancel { - track_namespace: crate::Path::new("cluster/ns"), - request_id: RequestId(0), - error_code: 0, - reason_phrase: "".into(), - }; - let route = classify( - ietf::PublishNamespaceCancel::ID, - &encode_body(&cancel, version), - version, - &shared.namespaces, - ) - .unwrap(); - assert!(matches!(route, Route::CloseStream(RequestId(4)))); - - shared.close(RequestId(4), encode_msg(&cancel, version)); - assert!(drained(&mut ours, 4).await); - assert!(shared.streams.lock().unwrap().contains_key(&RequestId(7))); - assert!(read_virtual(&mut theirs).now_or_never().is_none()); - } - - /// Classify the peer withdrawing its own advertisement of `namespace`. - fn done(shared: &Arc, namespace: &str, version: Version) -> Route { - let msg = ietf::PublishNamespaceDone { - track_namespace: crate::Path::new(namespace), - request_id: RequestId(0), - }; - classify( - ietf::PublishNamespaceDone::ID, - &encode_body(&msg, version), - version, - &shared.namespaces, - ) - .unwrap() - } - - /// Classify the peer rejecting our advertisement of `namespace`. - fn cancel(shared: &Arc, namespace: &str, version: Version) -> Route { - let msg = ietf::PublishNamespaceCancel { - track_namespace: crate::Path::new(namespace), - request_id: RequestId(0), - error_code: 0, - reason_phrase: "".into(), - }; - classify( - ietf::PublishNamespaceCancel::ID, - &encode_body(&msg, version), - version, - &shared.namespaces, - ) - .unwrap() - } - - #[moq_clock::test] - async fn test_duplicate_publish_namespace_keeps_the_first() { - // The peer advertises the same namespace twice. The session refuses the second, so - // the first still owns the name and its withdrawal must reach the first's stream. - let version = Version::Draft14; - let shared = Arc::new(Shared::default()); - let first = receive(&shared, "cluster/ns", version, RequestId(7)).await; - let duplicate = receive(&shared, "cluster/ns", version, RequestId(11)).await; - - assert!(matches!( - done(&shared, "cluster/ns", version), - Route::CloseStream(RequestId(7)) - )); - - // Tearing down the refused duplicate releases only what it owns, which is nothing. - drop(duplicate); - assert!(matches!( - done(&shared, "cluster/ns", version), - Route::CloseStream(RequestId(7)) - )); - - // The winner going away is what frees the name. - drop(first); - assert!(matches!(done(&shared, "cluster/ns", version), Route::Ignore)); - } - - #[moq_clock::test] - async fn test_withdrawal_releases_the_namespace() { - // A namespace the peer withdraws must be advertisable again on a new request. - let version = Version::Draft14; - let shared = Arc::new(Shared::default()); - let mut theirs = receive(&shared, "cluster/ns", version, RequestId(7)).await; - - let msg = ietf::PublishNamespaceDone { - track_namespace: crate::Path::new("cluster/ns"), - request_id: RequestId(0), - }; - shared.close(RequestId(7), encode_msg(&msg, version)); - assert!(drained(&mut theirs, 4).await); - assert!(!shared.streams.lock().unwrap().contains_key(&RequestId(7))); - drop(theirs); - - let _again = receive(&shared, "cluster/ns", version, RequestId(11)).await; - assert!(matches!( - done(&shared, "cluster/ns", version), - Route::CloseStream(RequestId(11)) - )); - } - - /// The peer's next request on `namespace`, advertising it, as the read loop routes it. - fn readvertise(shared: &Arc, namespace: &str, version: Version, request_id: RequestId) { - let msg = publish_namespace(request_id, namespace); - let route = classify( - ietf::PublishNamespace::ID, - &encode_body(&msg, version), - version, - &shared.namespaces, - ) - .unwrap(); - assert!(matches!(route, Route::NewRequest(id) if id == request_id)); - shared - .open_incoming(request_id, encode_msg(&msg, version), None) - .unwrap(); - } - - /// The peer withdrawing `namespace` on `request_id`, as the read loop routes it. - fn withdraw(shared: &Arc, namespace: &str, version: Version, request_id: RequestId) { - let msg = ietf::PublishNamespaceDone { - track_namespace: crate::Path::new(namespace), - request_id, - }; - let route = classify( - ietf::PublishNamespaceDone::ID, - &encode_body(&msg, version), - version, - &shared.namespaces, - ) - .unwrap(); - assert!(matches!(route, Route::CloseStream(id) if id == request_id)); - shared.close(request_id, encode_msg(&msg, version)); - } - - /// The request of the next stream `accept_bi` would hand out, if one is ready. - fn accepted(shared: &Arc) -> Option<(RequestId, VirtualRecvStream)> { - let (_, recv) = shared.incoming.pop().now_or_never()??; - let request_id = recv.request_id.lock().unwrap().unwrap(); - Some((request_id, recv)) - } - - #[moq_clock::test] - async fn a_readvertisement_waits_for_the_withdrawal_to_be_handled() { - // Each request has its own handler, so a re-advertisement handed out beside the - // withdrawal before it could be handled first and read as a second advertisement. - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { - let shared = Arc::new(Shared::default()); - let first = receive(&shared, "live/media", version, RequestId(3)).await; - let other = receive(&shared, "live/other", version, RequestId(5)).await; - - withdraw(&shared, "live/media", version, RequestId(3)); - readvertise(&shared, "live/media", version, RequestId(7)); - withdraw(&shared, "live/media", version, RequestId(7)); - readvertise(&shared, "live/media", version, RequestId(9)); - // Another namespace is not held up. - withdraw(&shared, "live/other", version, RequestId(5)); - drop(other); - readvertise(&shared, "live/other", version, RequestId(11)); - assert!(matches!(accepted(&shared), Some((RequestId(11), _))), "{version:?}"); - assert!(accepted(&shared).is_none(), "{version:?}"); - - // Handling the first withdrawal hands over the next advertisement, which was - // withdrawn too, so the one after it waits for that withdrawal in turn. - drop(first); - let (id, mut second) = accepted(&shared).expect("released"); - assert_eq!(id, RequestId(7), "{version:?}"); - assert!(accepted(&shared).is_none(), "{version:?}"); - assert!(drained(&mut second, 4).await, "{version:?}"); - drop(second); - assert!(matches!(accepted(&shared), Some((RequestId(9), _))), "{version:?}"); - assert!(shared.readvertised.lock().unwrap().namespaces.is_empty(), "{version:?}"); - } - } - - #[moq_clock::test] - async fn a_session_closed_behind_a_withdrawal_drops_what_it_held() { - // Dropping a refused stream re-enters the release, which must not still hold its lock. - let version = Version::Draft14; - let shared = Arc::new(Shared::default()); - let first = receive(&shared, "live/media", version, RequestId(3)).await; - withdraw(&shared, "live/media", version, RequestId(3)); - readvertise(&shared, "live/media", version, RequestId(7)); - withdraw(&shared, "live/media", version, RequestId(7)); - readvertise(&shared, "live/media", version, RequestId(9)); - shared.incoming.close(); - - drop(first); - let readvertised = shared.readvertised.lock().unwrap(); - assert!(readvertised.namespaces.is_empty() && readvertised.withdrawn.is_empty()); - drop(readvertised); - assert!(shared.streams.lock().unwrap().is_empty()); - } - - #[moq_clock::test] - async fn test_local_withdrawal_releases_the_namespace() { - // We withdraw our own advertisement by dropping the request that carried it, so a - // later CANCEL must name the re-advertisement rather than the request that is gone. - let version = Version::Draft14; - let shared = Arc::new(Shared::default()); - - let ours = advertise(&shared, "cluster/ns", version, RequestId(4)).await; - drop(ours); - assert!(matches!(cancel(&shared, "cluster/ns", version), Route::Ignore)); - - let _again = advertise(&shared, "cluster/ns", version, RequestId(6)).await; - assert!(matches!( - cancel(&shared, "cluster/ns", version), - Route::CloseStream(RequestId(6)) - )); - } - - #[moq_clock::test] - async fn surviving_duplicate_remains_reachable_after_owner_closes() { - for version in [Version::Draft14, Version::Draft15] { - let shared = Arc::new(Shared::default()); - let first = receive(&shared, "cluster/ns", version, RequestId(7)).await; - let second = receive(&shared, "cluster/ns", version, RequestId(11)).await; - let third = receive(&shared, "cluster/ns", version, RequestId(15)).await; - drop(first); - assert!(matches!( - done(&shared, "cluster/ns", version), - Route::CloseStream(RequestId(11)) - )); - drop(second); - assert!(matches!( - done(&shared, "cluster/ns", version), - Route::CloseStream(RequestId(15)) - )); - drop(third); - assert!(matches!(done(&shared, "cluster/ns", version), Route::Ignore)); - } - } - - #[test] - fn namespace_removal_preserves_links_in_every_order() { - for direction in [Direction::Incoming, Direction::Outgoing, Direction::Subscribed] { - for first in 0..4 { - for second in 0..4 { - for third in 0..4 { - if first == second || first == third || second == third { - continue; - } - let fourth = (0..4).find(|id| *id != first && *id != second && *id != third).unwrap(); - let namespaces = Namespaces::default(); - let name = crate::Path::new("ns"); - for id in 0..4 { - namespaces.register(direction, name.clone(), RequestId(id)); - } - let mut live = [true; 4]; - for id in [first, second, third, fourth] { - namespaces.forget(RequestId(id)); - live[id as usize] = false; - let expected = live.iter().position(|live| *live).map(|id| RequestId(id as u64)); - assert_eq!(namespaces.get(direction, &name), expected); - } - let state = namespaces.state.lock().unwrap(); - assert!(state.by_request.is_empty()); - assert!(state.incoming.is_empty() && state.outgoing.is_empty() && state.subscribed.is_empty()); - } - } - } - } - } - - /// A peer's control stream carrying a bare SUBSCRIBE for each request ID: the - /// adapter routes on the ID alone, so the rest of the body is never read. - async fn windowed( - window: u64, - ids: &[u64], - then: &[u8], - ) -> ( - ControlStreamAdapter, - Reader, - Writer, - crate::lite::test_transport::Log, - ) { - use crate::lite::test_transport::{Log, ScriptedSession, SinkSend}; - use crate::transport::poll::Session as _; - - const VERSION: Version = Version::Draft14; - let mut script = Vec::new(); - for id in ids { - let body = make_body_with_request_id(*id, VERSION); - script.extend_from_slice(&encode_raw(ietf::Subscribe::ID, &body, VERSION)); - } - script.extend_from_slice(then); - let mut session = ScriptedSession::new(script); - let (_, recv) = session.open_bi(Default::default()).await.unwrap(); - // We are the server, so the client's request IDs are even. - let control = Control::new(None, false).with_window(window, false, VERSION); - let adapter = ControlStreamAdapter::new(Transport::new(session.clone()), control, VERSION); - let log = Log::default(); - let writer = Writer::new( - crate::transport::boxed::SendStream::new(SinkSend::new(log.clone())), - VERSION, - ); - ( - adapter, - Reader::new(crate::transport::boxed::RecvStream::new(recv), VERSION), - writer, - log, - ) - } - - #[moq_clock::test] - async fn legacy_multiplexing_counts_a_control_body_once() { - use crate::{ - coding::{Encode, Stream}, - lite::test_transport::{Log, ScriptedSession, SinkSend}, - stats::{Bytes, Control as Class, Registry, Tier}, - }; - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { - let registry = Registry::new(Default::default()); - let stats = registry.tier(Tier::default()).session(""); - let msg = ietf::FetchCancel { - request_id: RequestId(0), - }; - let body = msg.encode_bytes(version).unwrap(); - let raw_response = encode_raw(ietf::FetchCancel::ID, &body.slice(2..), version); - let mut session = ScriptedSession::new(raw_response.to_vec()); - let (_, recv) = crate::transport::poll::Session::open_bi(&mut session, Default::default()) - .await - .unwrap(); - let adapter = ControlStreamAdapter::new(Transport::new(session.clone()), Control::new(None, true), version) - .with_stats(&stats); - let mut virtual_stream = super::super::request_stream::with_stats( - Stream::open(&mut crate::driver::Transport::new(adapter.clone()), version) - .await - .unwrap(), - &stats, - version, - ); - let msg = ietf::FetchCancel { - request_id: RequestId(0), - }; - let size = msg.encode_bytes(version).unwrap().len() as u64; - virtual_stream.writer.encode_message(&msg).await.unwrap(); - let raw = adapter.shared.control.pop().await.unwrap(); - let log = Log::default(); - let mut physical = Writer::new( - crate::transport::boxed::SendStream::new(SinkSend::new(log.clone())), - version, - ); - physical.write_all(&mut raw.clone()).await.unwrap(); - assert_eq!( - registry.snapshot().control(), - vec![(Tier::default(), Class::Fetch, Bytes { tx: size, rx: 0 })], - "{version:?}" - ); - assert_eq!(*log.writes.lock().unwrap(), raw); - let (_, goaway) = crate::goaway::Handle::new(false); - let mut reading = std::pin::pin!(adapter.run_read( - Reader::new(crate::transport::boxed::RecvStream::new(recv), version), - goaway - )); - assert!(futures::poll!(reading.as_mut()).is_pending()); - assert_eq!(virtual_stream.reader.varint().await.unwrap(), ietf::FetchCancel::ID); - virtual_stream.reader.decode::().await.unwrap(); - assert_eq!( - registry.snapshot().control(), - vec![(Tier::default(), Class::Fetch, Bytes { tx: size, rx: size })], - "virtual decode counted a physical frame twice: {version:?}" - ); - adapter.send_max_request_id(RequestId(123)); - assert!( - registry - .snapshot() - .control() - .iter() - .any(|(_, class, bytes)| *class == Class::Other && bytes.tx > 0), - "adapter-generated messages bypassed the typed writer" - ); - } - } - - #[moq_clock::test] - async fn a_legacy_control_stream_fin_or_reset_is_a_protocol_violation() { - use crate::lite::test_transport::{DeadStreamSession, ScriptedSession}; - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { - let session = ScriptedSession::eof(Vec::new()); - let stream = crate::coding::Stream::open(&mut Transport::new(session.clone()), version) - .await - .unwrap(); - let adapter = ControlStreamAdapter::new(Transport::new(session.clone()), Control::new(None, true), version); - let (_handle, protocol) = crate::goaway::Handle::new(false); - let err = adapter.run(stream.reader, stream.writer, protocol).await.unwrap_err(); - assert!(matches!(err, Error::ProtocolViolation), "{version:?}: {err:?}"); - - let session = DeadStreamSession::bis(0); - let stream = crate::coding::Stream::open(&mut Transport::new(session.clone()), version) - .await - .unwrap(); - let adapter = ControlStreamAdapter::new(Transport::new(session.clone()), Control::new(None, true), version); - let (_handle, protocol) = crate::goaway::Handle::new(false); - let err = adapter.run(stream.reader, stream.writer, protocol).await.unwrap_err(); - assert!(matches!(err, Error::ProtocolViolation), "{version:?}: {err:?}"); - } - } - - /// A STOP_SENDING on a legacy control stream ends the session even with nothing queued - /// to write, while the peer keeps its own half open. - #[moq_clock::test] - async fn a_legacy_control_stream_stopped_while_idle_is_a_protocol_violation() { - use crate::transport::memory; - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { - let (session, mut peer) = memory::pair(memory::Config::default()); - let stream = crate::coding::Stream::open(&mut Transport::new(session.clone()), version) - .await - .unwrap(); - let mut remote = crate::coding::Stream::accept(&mut peer, version).await.unwrap(); - remote.reader.abort(&Error::Cancel); - let adapter = ControlStreamAdapter::new(Transport::new(session.clone()), Control::new(None, true), version); - let (_handle, protocol) = crate::goaway::Handle::new(false); - let res = moq_clock::timeout( - std::time::Duration::from_secs(10), - adapter.run(stream.reader, stream.writer, protocol), - ) - .await - .expect("an idle writer missed the STOP_SENDING"); - assert!(matches!(res, Err(Error::ProtocolViolation)), "{version:?}: {res:?}"); - } - } - - /// A request ID at the MAX_REQUEST_ID we advertised closes the session with the - /// draft's TOO_MANY_REQUESTS, before the request is queued. - #[moq_clock::test] - async fn a_request_past_the_window_closes_the_session() { - let (adapter, reader, writer, _) = windowed(1, &[0, 2], &[]).await; - let (_, goaway) = crate::goaway::Handle::new(true); - let err = adapter.run(reader, writer, goaway).await.unwrap_err(); - assert!( - matches!(err, Error::Session(crate::SessionError::TooManyRequests)), - "{err:?}" - ); - } - - /// An update's own request ID counts against the window, not the subscription it targets. - #[moq_clock::test] - async fn an_update_past_the_window_closes_the_session() { - const VERSION: Version = Version::Draft14; - let update = ietf::SubscribeUpdate { - max_age: None, - new_group: None, - request_id: RequestId(4), - subscription_request_id: Some(RequestId(0)), - start_location: ietf::Location { group: 0, object: 0 }, - end_group: 0, - subscriber_priority: Some(1), - forward: Some(true), - filter: Some(ietf::Filter::NextObject), - authorization_token: None, - fill: None, - }; - let update = encode_raw(ietf::SubscribeUpdate::ID, &encode_body(&update, VERSION), VERSION); - // The target is in the window and the update's own ID is past it. - let (adapter, reader, writer, _) = windowed(2, &[0], &update).await; - let (_, goaway) = crate::goaway::Handle::new(true); - let err = adapter.run(reader, writer, goaway).await.unwrap_err(); - assert!( - matches!(err, Error::Session(crate::SessionError::TooManyRequests)), - "{err:?}" - ); - } - - /// An update aimed at no open request, such as draft 16's SUBSCRIBE_NAMESPACE riding a - /// stream of its own, still gets its one answer on drafts 15 and 16: a REQUEST_ERROR - /// naming the update. Draft 14 answers no update. - #[moq_clock::test] - async fn an_unroutable_update_is_refused() { - use crate::lite::test_transport::{Log, ScriptedSession, SinkSend}; - use crate::transport::poll::Session as _; - - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { - // Own Request ID 2, aimed at request 0, which nothing here holds. Draft 14 carries - // its fixed start, end group, priority and forward before the parameters. - let body: &[u8] = match version { - Version::Draft14 => &[2, 0, 0, 0, 0, 128, 1, 0], - _ => &[2, 0, 0], - }; - let script = encode_raw(ietf::SubscribeUpdate::ID, &Bytes::copy_from_slice(body), version); - let mut session = ScriptedSession::new(script.to_vec()); - let (_, recv) = session.open_bi(Default::default()).await.unwrap(); - let adapter = - ControlStreamAdapter::new(Transport::new(session.clone()), Control::new(None, false), version); - let log = Log::default(); - let writer = Writer::new( - crate::transport::boxed::SendStream::new(SinkSend::new(log.clone())), - version, - ); - let (_, goaway) = crate::goaway::Handle::new(true); - let mut run = std::pin::pin!(adapter.run( - Reader::new(crate::transport::boxed::RecvStream::new(recv), version), - writer, - goaway - )); - assert!(futures::poll!(run.as_mut()).is_pending(), "{version}"); - - let written = log.writes.lock().unwrap().clone(); - match version { - Version::Draft14 => assert!(written.is_empty(), "{version}: {written:?}"), - // [type, length (2), Request ID, NOT_SUPPORTED, ...] - _ => assert_eq!( - (written[0], written[3], written[4]), - (ietf::RequestError::ID as u8, 2, 0x3), - "{version}: {written:?}" - ), - } - } - } - - /// Requests that end are granted back with MAX_REQUEST_ID, once half the window has. - #[moq_clock::test] - async fn retired_requests_are_granted_back() { - use crate::transport::poll::Session as _; - - const VERSION: Version = Version::Draft14; - let (mut adapter, reader, writer, log) = windowed(2, &[0, 2], &[]).await; - let (_, goaway) = crate::goaway::Handle::new(true); - let runner = adapter.clone(); - let mut run = std::pin::pin!(runner.run(reader, writer, goaway)); - assert!(futures::poll!(run.as_mut()).is_pending()); - - let first = adapter.accept_bi().await.unwrap(); - let _second = adapter.accept_bi().await.unwrap(); - assert!(log.writes.lock().unwrap().is_empty(), "nothing has ended yet"); - - drop(first); - assert!(futures::poll!(run.as_mut()).is_pending()); - - let mut expected = Vec::new(); - crate::coding::Encoder::new(&mut expected, VERSION.into()) - .varint(ietf::MaxRequestId::ID) - .unwrap(); - ietf::MaxRequestId { - request_id: RequestId(6), - } - .encode(&mut crate::coding::Encoder::new(&mut expected, VERSION.into()), VERSION) - .unwrap(); - assert_eq!(*log.writes.lock().unwrap(), expected.to_vec()); - } - - #[test] - fn queued_incoming_stream_does_not_keep_shared_alive() { - let shared = Arc::new(Shared::default()); - let weak = Arc::downgrade(&shared); - shared.open_incoming(RequestId(7), Bytes::new(), None).unwrap(); - drop(shared); - assert!(weak.upgrade().is_none()); - } - - #[moq_clock::test] - async fn receive_drop_before_registration_releases_namespace() { - for version in [Version::Draft14, Version::Draft15] { - let shared = Arc::new(Shared::default()); - let (mut send, recv) = shared.open_outgoing(version); - drop(recv); - send.write_chunk(encode_msg(&publish_namespace(RequestId(4), "cluster/ns"), version)) - .await - .unwrap(); - assert!(shared.streams.lock().unwrap().is_empty()); - assert!(matches!(cancel(&shared, "cluster/ns", version), Route::Ignore)); - let _again = advertise(&shared, "cluster/ns", version, RequestId(6)).await; - assert!(matches!( - cancel(&shared, "cluster/ns", version), - Route::CloseStream(RequestId(6)) - )); - } - } - - #[test] - fn test_unresolvable_withdrawal_is_dropped() { - // v14/v15 name their withdrawals, and a name we hold no advertisement for belongs - // to a request that is already gone. Dropping it beats ending the session. - for version in [Version::Draft14, Version::Draft15] { - let shared = Arc::new(Shared::default()); - assert!(matches!(done(&shared, "cluster/ns", version), Route::Ignore)); - assert!(matches!(cancel(&shared, "cluster/ns", version), Route::Ignore)); - } - } -} diff --git a/rs/moq-net/src/ietf/auth.rs b/rs/moq-net/src/ietf/auth.rs index 995d51e1fe5..ac0090e1093 100644 --- a/rs/moq-net/src/ietf/auth.rs +++ b/rs/moq-net/src/ietf/auth.rs @@ -18,7 +18,7 @@ use crate::{Error, Patterns, SessionError}; #[cfg(feature = "async")] use super::Control; -use super::{Message, RequestId, Version, cluster, peer}; +use super::{Message, RequestId, Version, peer}; #[cfg(feature = "async")] use crate::coding::Stream; @@ -27,33 +27,15 @@ use crate::coding::Stream; /// negotiates it. pub const AUTH: u64 = 0x40B70; -/// Whether a version negotiates this extension: draft-17+, like MoQ Cluster. -pub fn supported(version: Version) -> bool { - cluster::supported(version) -} - /// What the peer declared: `None` for no option, otherwise whether it offered the /// extension. Only an explicit 1 negotiates it. -pub fn from_setup(params: &super::Parameters, version: Version) -> Option { - if !supported(version) { - return None; - } +pub fn from_setup(params: &super::Parameters) -> Option { params.get_varint(super::ParameterVarInt::Auth).map(|value| value == 1) } -/// Offer the extension, on the versions that negotiate it. -pub fn into_setup(params: &mut super::Parameters, version: Version) { - if supported(version) { - params.set_varint(super::ParameterVarInt::Auth, 1); - } -} - -/// Refuse a message on a version that cannot negotiate the extension. -fn check_version(version: Version) -> Result<(), DecodeError> { - match supported(version) { - true => Ok(()), - false => Err(DecodeError::Version), - } +/// Offer the extension. +pub fn into_setup(params: &mut super::Parameters) { + params.set_varint(super::ParameterVarInt::Auth, 1); } /// AUTH: the first message on an Auth request stream, presenting a token. @@ -68,13 +50,11 @@ impl Message for Auth { const ID: u64 = 0x40B61; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - check_version(version).map_err(|_| EncodeError::Version)?; self.request_id.encode(w, version)?; w.bytes(&self.token) } fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - check_version(version)?; Ok(Self { request_id: RequestId::decode(r, version)?, token: Bytes::copy_from_slice(r.bytes()?), @@ -100,7 +80,6 @@ impl Message for AuthOk { const ID: u64 = 0x40B62; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - check_version(version).map_err(|_| EncodeError::Version)?; self.publish.encode(w, version)?; self.subscribe.encode(w, version)?; // 0 means never, so a grant that has already lapsed rounds up to the smallest @@ -113,7 +92,6 @@ impl Message for AuthOk { } fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - check_version(version)?; let publish = Patterns::decode(r, version)?; let subscribe = Patterns::decode(r, version)?; let expires = match r.varint()? { @@ -144,8 +122,7 @@ const MAX_REASON: usize = 8192; impl Message for AuthError { const ID: u64 = 0x40B63; - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - check_version(version).map_err(|_| EncodeError::Version)?; + fn encode_msg(&self, w: &mut Encoder<'_>, _: Version) -> Result<(), EncodeError> { if self.reason.len() > MAX_REASON { return Err(EncodeError::TooLarge); } @@ -153,8 +130,7 @@ impl Message for AuthError { w.string(&self.reason) } - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - check_version(version)?; + fn decode_msg(r: &mut Decoder<'_>, _: Version) -> Result { let code = r.varint()?; if code > u32::MAX as u64 { return Err(DecodeError::InvalidValue); @@ -312,11 +288,8 @@ impl Present { Ok(stream) => stream, Err(err) => return err, }; - stream = super::request_stream::with_stats(stream, &self.stats, self.version); - let request_id = match self.control.next_request_id(&self.runtime).await { - Ok(id) => id, - Err(err) => return err, - }; + stream = stream.with_stats(&self.stats); + let request_id = self.control.next_request_id(); if let Err(err) = stream.writer.encode_message(&Auth { request_id, token }).await { return err; } @@ -589,7 +562,7 @@ mod tests { #[cfg(feature = "async")] #[moq_clock::test] async fn auth_refused_without_credit_spends_no_request_id() { - for version in [Version::Draft17, Version::Draft19] { + for version in [Version::Draft19] { for client in [false, true] { let first = if client { 0 } else { 1 }; let control = Control::new(Some(RequestId(first + 1)), client); @@ -638,7 +611,6 @@ mod tests { #[test] fn setup_option_round_trips_on_supported_drafts() { for version in [ - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -650,7 +622,7 @@ mod tests { into_setup(&mut params, version); assert_eq!(from_setup(¶ms, version), Some(true), "{version:?}"); } - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { let mut params = super::super::Parameters::default(); into_setup(&mut params, version); assert_eq!(params.get_varint(super::super::ParameterVarInt::Auth), None); @@ -735,7 +707,7 @@ mod tests { #[test] fn older_drafts_have_no_auth() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { let msg = Auth { request_id: RequestId(0), token: Bytes::new(), diff --git a/rs/moq-net/src/ietf/cluster.rs b/rs/moq-net/src/ietf/cluster.rs index f1321350ede..2bb89d1f388 100644 --- a/rs/moq-net/src/ietf/cluster.rs +++ b/rs/moq-net/src/ietf/cluster.rs @@ -44,15 +44,6 @@ pub const ROUTE_COST: u64 = 0x40B58; /// same datacenter. pub const DEFAULT_COST: u64 = 1; -/// Whether a version negotiates this extension. -/// -/// Draft-17 is the first with the unified SETUP: a bare Key-Value-Pair block that a -/// new option slots into without touching the message shape. Earlier drafts exchange -/// SETUP over the legacy control stream, which this extension does not cover. -pub fn supported(version: Version) -> bool { - !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) -} - /// The ordered list of Hop IDs an advertisement has traversed, from the original /// publisher to the relay immediately upstream of the receiver. /// @@ -221,11 +212,7 @@ pub fn link_cost(local: Option, peer: &Peer) -> u64 { } /// Read the cluster Setup Options out of a decoded SETUP parameter block. -pub fn peer_from_setup(params: &super::Parameters, version: Version) -> Result { - if !supported(version) { - return Ok(Peer::default()); - } - +pub fn peer_from_setup(params: &super::Parameters) -> Result { // 0 is legal here: the peer speaks the extension but withholds its identity. let hop = params.get_varint(super::ParameterVarInt::HopId).map(Hop::from_wire); @@ -238,11 +225,7 @@ pub fn peer_from_setup(params: &super::Parameters, version: Version) -> Result

, version: Version) { - if !supported(version) { - return; - } - +pub fn peer_into_setup(params: &mut super::Parameters, self_hop: Hop, cost: Option) { params.set_varint(super::ParameterVarInt::HopId, self_hop.id()); if let Some(cost) = cost { @@ -495,7 +478,7 @@ mod tests { (Hop::UNKNOWN, None), ] { let mut params = super::super::Parameters::default(); - peer_into_setup(&mut params, self_hop, cost, VERSION); + peer_into_setup(&mut params, self_hop, cost); let mut buf = Vec::new(); params @@ -504,7 +487,7 @@ mod tests { let mut bytes = bytes::Bytes::from(buf); let decoded = crate::coding::decode_buf(&mut bytes, VERSION, super::super::Parameters::decode).unwrap(); - let peer = peer_from_setup(&decoded, VERSION).unwrap(); + let peer = peer_from_setup(&decoded).unwrap(); assert_eq!(peer.hop, Some(self_hop)); assert_eq!(peer.cost, cost); } @@ -515,7 +498,7 @@ mod tests { // The key sits at delta 0x40B54 from the start of the block and the value follows // it directly: a length byte here would make us unreadable to every -01 peer. let mut params = super::super::Parameters::default(); - peer_into_setup(&mut params, hop(42), None, VERSION); + peer_into_setup(&mut params, hop(42), None); let mut buf = Vec::new(); params @@ -527,7 +510,7 @@ mod tests { #[test] fn setup_without_hop_id_is_not_negotiated() { let params = super::super::Parameters::default(); - let peer = peer_from_setup(¶ms, VERSION).unwrap(); + let peer = peer_from_setup(¶ms).unwrap(); assert!(!peer.negotiated()); } @@ -546,17 +529,8 @@ mod tests { let mut bytes = bytes::Bytes::from(buf); let decoded = crate::coding::decode_buf(&mut bytes, VERSION, super::super::Parameters::decode).unwrap(); - let peer = peer_from_setup(&decoded, VERSION).unwrap(); + let peer = peer_from_setup(&decoded).unwrap(); assert!(!peer.negotiated()); } - #[test] - fn setup_options_skipped_before_draft17() { - // Draft-14..16 exchange SETUP over the legacy control stream, which this - // extension does not cover; we neither send nor read the options there. - let mut params = super::super::Parameters::default(); - peer_into_setup(&mut params, hop(42), Some(3), Version::Draft16); - assert!(params.get_varint(super::super::ParameterVarInt::HopId).is_none()); - assert!(!peer_from_setup(¶ms, Version::Draft16).unwrap().negotiated()); - } } diff --git a/rs/moq-net/src/ietf/control.rs b/rs/moq-net/src/ietf/control.rs index e83e0591efc..08ec9bdc839 100644 --- a/rs/moq-net/src/ietf/control.rs +++ b/rs/moq-net/src/ietf/control.rs @@ -1,9 +1,6 @@ -use std::{collections::BTreeMap, task::Poll}; +use std::collections::BTreeMap; -use crate::{ - Error, SessionError, - ietf::{RequestId, Version}, -}; +use crate::{Error, SessionError, ietf::RequestId}; /// The peer's spent request IDs: every ID of its parity below `next`, except the skipped /// `start..end` runs in `missing`. @@ -33,7 +30,7 @@ impl Spent { /// Whether `id` has the peer's parity and was never spent. /// /// No peer reaches 2^62 requests, and refusing the IDs past it keeps `next` from - /// overflowing on a draft-17+ varint, which can carry a full 64 bits. + /// overflowing on a varint that can carry a full 64 bits. fn unused(&self, id: u64) -> bool { id < 1 << 62 && id % 2 == self.next % 2 && (id >= self.next || self.run(id).is_some()) } @@ -63,80 +60,39 @@ impl Spent { } } -/// The MAX_REQUEST_ID that first admits `window` requests from a peer. +/// The most runs of skipped IDs a peer may leave before the session closes. /// -/// A client's request IDs are even from 0 and a server's odd from 1, each stepping by 2. -pub(crate) fn initial_max_request_id(window: u64, peer_client: bool) -> u64 { - let parity = if peer_client { 0 } else { 1 }; - window.saturating_mul(2).saturating_add(parity).min((1 << 62) - 1) -} +/// Each request holds a stream, so stream credit bounds the live ones, but a skipped ID +/// stays remembered after the requests around it end. Forgetting one would make it usable +/// again, so a peer that keeps skipping loses the session instead. Matches `@moq/net`. +const HISTORY: u64 = 240_000; struct ControlState { /// Our own role. client: bool, request_id_next: RequestId, - /// Legacy request headers that must finish before a later ID can be allocated. - /// A count, since two tasks can allocate before either takes its hold. - request_id_sending: usize, - /// None means no flow control (draft17 removed MaxRequestId). - request_id_max: Option, - /// The peer's requests, flow controlled on drafts 14 to 16 only. - incoming: Option, - /// The peer's spent IDs on draft 17 and later, which has no window to bound the runs. - modern: Spent, - /// The most runs `modern` may hold. Forgetting one would make its IDs usable again. + /// The peer's spent IDs, which no window bounds. + spent: Spent, + /// The most runs `spent` may hold. Forgetting one would make its IDs usable again. history: u64, - /// A draft-18 GOAWAY has frozen admission of new peer requests. + /// A GOAWAY has frozen admission of new peer requests. going_away: bool, } -/// The MAX_REQUEST_ID window we grant the peer. -struct Incoming { - /// How many requests the peer may hold open at once. - window: u64, - /// The MAX_REQUEST_ID in force: every request ID the peer uses must be below it. - max: u64, - /// The last MAX_REQUEST_ID the peer was told, so each grant goes out once. - sent: u64, - /// Requests accepted and not yet retired. - live: u64, - /// Requests retired since the last grant. - retired: u64, - /// Every accepted ID, preserving IDs whose requests retired. - spent: Spent, - /// Draft 16 can reorder across streams; drafts 14 and 15 require the next ID. - reorder: bool, -} - #[derive(Clone)] pub(super) struct Control { state: kio::Shared, } -/// The session must keep a committed legacy header alive until this hold ends. -pub(super) struct RequestSend(Control); - -impl Drop for RequestSend { - fn drop(&mut self) { - self.0.state.lock().request_id_sending -= 1; - } -} - impl Control { - /// How long a request waits for MAX_REQUEST_ID credit before giving up. - pub const REQUEST_ID_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); - - pub fn new(request_id_max: Option, client: bool) -> Self { + pub fn new(client: bool) -> Self { Self { state: kio::Shared::new(ControlState { client, request_id_next: if client { RequestId(0) } else { RequestId(1) }, - request_id_sending: 0, - request_id_max, - incoming: None, // The peer takes the other parity: a client's IDs are even, a server's odd. - modern: Spent::new(u64::from(client)), - history: crate::session::Limits::default().window(), + spent: Spent::new(u64::from(client)), + history: HISTORY, going_away: false, }), } @@ -147,37 +103,20 @@ impl Control { self.state.lock().client } - /// Flow control the peer's requests (drafts 14 to 16), admitting `window` at once. - /// - /// `client` is our own role, as passed to [`Self::new`]; the window starts at what - /// [`initial_max_request_id`] advertised in our SETUP. - pub fn with_window(self, window: u64, client: bool, version: Version) -> Self { - let max = initial_max_request_id(window, !client); - self.state.lock().incoming = Some(Incoming { - window, - max, - sent: max, - live: 0, - retired: 0, - spent: Spent::new(u64::from(client)), - reorder: version == Version::Draft16, - }); - self - } - - /// Bound the runs of skipped IDs a draft-17+ peer may leave, closing the session with + /// Bound the runs of skipped IDs the peer may leave, closing the session with /// INTERNAL_ERROR past `limit` rather than forgetting any. + #[cfg(test)] pub fn with_history(self, limit: u64) -> Self { self.state.lock().history = limit; self } - /// Freeze draft-18 admission and name the smallest peer ID not admitted: the first one - /// a reordered stream skipped, or else the next. + /// Freeze admission and name the smallest peer ID not admitted: the first one a + /// reordered stream skipped, or else the next. pub fn goaway(&self) -> Result { let mut state = self.state.lock(); - let modern = &state.modern; - let cutoff = modern.missing.keys().next().copied().unwrap_or(modern.next); + let spent = &state.spent; + let cutoff = spent.missing.keys().next().copied().unwrap_or(spent.next); if cutoff >= 1 << 62 { return Err(SessionError::Internal.into()); } @@ -185,145 +124,31 @@ impl Control { Ok(RequestId(cutoff)) } - pub fn max_request_id(&self, max: RequestId) { - // Mutating through the guard wakes any `next_request_id` waiting on the limit. - self.state.lock().request_id_max = Some(max); - } - - /// Admit a request the peer opened, held open until the returned permit drops. + /// Admit a request the peer opened. /// - /// Closes the session with TOO_MANY_REQUESTS when the ID is at or past the - /// MAX_REQUEST_ID we advertised, or its live requests exceed the window. - /// Drafts 14/15 require the next ID; draft 16 permits cross-stream reordering. - /// Used IDs close with INVALID_REQUEST_ID even after retirement, on every draft. - /// `None` when the peer's requests are not flow controlled (draft 17 and later). - pub fn accept(&self, id: RequestId) -> Result, Error> { + /// Used IDs close with INVALID_REQUEST_ID even after retirement, and every ID once + /// GOAWAY froze admission is refused with [`Error::GoingAway`]. + pub fn accept(&self, id: RequestId) -> Result<(), Error> { let mut state = self.state.lock(); let state = &mut *state; - let Some(incoming) = &mut state.incoming else { - let modern = &mut state.modern; - if !modern.unused(id.0) { - return Err(SessionError::InvalidRequestId.into()); - } - if modern.splits(id.0) && modern.missing.len() as u64 >= state.history { - tracing::warn!(limit = state.history, "request ID history exhausted"); - return Err(SessionError::Internal.into()); - } - modern.spend(id.0); - return if state.going_away { - Err(Error::GoingAway) - } else { - Ok(None) - }; - }; - if id.0 >= incoming.max { - tracing::warn!(id = %id, max = incoming.max, live = incoming.live, "peer exceeded MAX_REQUEST_ID"); - return Err(SessionError::TooManyRequests.into()); - } - let unused = match incoming.reorder { - true => incoming.spent.unused(id.0), - false => id.0 == incoming.spent.next, - }; - if !unused { + let spent = &mut state.spent; + if !spent.unused(id.0) { return Err(SessionError::InvalidRequestId.into()); } - if incoming.live >= incoming.window { - return Err(SessionError::TooManyRequests.into()); - } - incoming.spent.spend(id.0); - incoming.live += 1; - Ok(Some(RequestPermit(self.clone()))) - } - - /// Retire one accepted request, growing the window once half of it has retired. - /// - /// Batching keeps the MAX_REQUEST_ID traffic a fraction of the requests it grants. - fn retire(&self) { - let mut state = self.state.lock(); - let Some(incoming) = &mut state.incoming else { - return; - }; - incoming.live -= 1; - incoming.retired += 1; - if incoming.retired.saturating_mul(2) >= incoming.window { - let grant = incoming.retired.saturating_mul(2); - incoming.max = incoming.max.saturating_add(grant).min((1 << 62) - 1); - incoming.retired = 0; - } - } - - /// Poll for a MAX_REQUEST_ID the peer has not been told yet. - /// - /// Pending forever when the peer's requests are not flow controlled. - pub fn poll_grant(&self, waiter: &kio::Waiter) -> Poll { - let ready = self.state.poll(waiter, |state| match &state.incoming { - Some(incoming) if incoming.max > incoming.sent => Poll::Ready(()), - _ => Poll::Pending, - }); - let mut state = std::task::ready!(ready); - let incoming = state.incoming.as_mut().expect("checked above"); - incoming.sent = incoming.max; - Poll::Ready(RequestId(incoming.max)) - } - - /// Hold later allocations until an already allocated legacy header is sent. - pub fn sending(&self) -> RequestSend { - self.state.lock().request_id_sending += 1; - RequestSend(self.clone()) - } - - /// Allocate the next request_id once credit and the committed header allow it, without - /// giving up: for a caller that waits on credit beside its other work. - pub fn poll_next_request_id(&self, waiter: &kio::Waiter) -> Poll { - let allowed = self.state.poll(waiter, |state| { - let allowed = match state.request_id_max { - None => true, - Some(max) => state.request_id_next < max, - }; - match allowed && state.request_id_sending == 0 { - true => Poll::Ready(()), - false => Poll::Pending, - } - }); - let mut state = std::task::ready!(allowed); - Poll::Ready(state.request_id_next.increment()) - } - - /// Allocate the next request_id once credit and the committed header allow it, or give - /// up with [`Error::Cancel`] once `timeout` passes. - /// - /// Arm `timeout` with [`Self::REQUEST_ID_TIMEOUT`] on the caller's clock. - pub fn poll_credit( - &self, - waiter: &kio::Waiter, - timeout: &mut crate::time::Deadline, - ) -> Poll> { - if let Poll::Ready(id) = self.poll_next_request_id(waiter) { - return Poll::Ready(Ok(id)); + if spent.splits(id.0) && spent.missing.len() as u64 >= state.history { + tracing::warn!(limit = state.history, "request ID history exhausted"); + return Err(SessionError::Internal.into()); } - if timeout.poll(waiter).is_ready() { - tracing::warn!("timed out waiting for MAX_REQUEST_ID"); - return Poll::Ready(Err(Error::Cancel)); + spent.spend(id.0); + match state.going_away { + true => Err(Error::GoingAway), + false => Ok(()), } - Poll::Pending } - /// Allocate the next request_id once credit and the committed header allow it. - /// - /// `runtime` arms the give-up timer, so the wait cannot outlive the caller's - /// clock. - pub async fn next_request_id(&self, runtime: &crate::time::Deadlines) -> Result { - let mut timeout = crate::time::Deadline::after(runtime, Self::REQUEST_ID_TIMEOUT); - kio::wait(|waiter| self.poll_credit(waiter, &mut timeout)).await - } -} - -/// One request the peer holds open, retired (and eventually granted back) on drop. -pub(super) struct RequestPermit(Control); - -impl Drop for RequestPermit { - fn drop(&mut self) { - self.0.retire(); + /// Allocate the next request ID. + pub fn next_request_id(&self) -> RequestId { + self.state.lock().request_id_next.increment() } } @@ -334,18 +159,18 @@ mod tests { #[test] fn goaway_freezes_request_admission_at_current_progress() { for client in [false, true] { - let control = Control::new(None, client); + let control = Control::new(client); let first = u64::from(client); assert_eq!(control.goaway().unwrap(), RequestId(first)); assert!(matches!(control.accept(RequestId(first)), Err(Error::GoingAway))); - let control = Control::new(None, client); + let control = Control::new(client); control.accept(RequestId(first)).unwrap(); control.accept(RequestId(first + 2)).unwrap(); assert_eq!(control.goaway().unwrap(), RequestId(first + 4)); // A reordered stream skipped first + 2, which is the smallest ID not processed. - let control = Control::new(None, client); + let control = Control::new(client); control.accept(RequestId(first)).unwrap(); control.accept(RequestId(first + 4)).unwrap(); assert_eq!(control.goaway().unwrap(), RequestId(first + 2)); @@ -360,162 +185,13 @@ mod tests { } } - fn granted(control: &Control) -> Option { - match control.poll_grant(&kio::Waiter::noop()) { - Poll::Ready(max) => Some(max.0), - Poll::Pending => None, - } - } - - /// The window admits exactly its size, refuses the next ID with TOO_MANY_REQUESTS, - /// and grants room back in batches as requests retire. - #[test] - fn window_refills_as_requests_retire() { - // We are the server, so the client's IDs are even: 0, 2, 4, 6. - let control = Control::new(None, false).with_window(4, false, Version::Draft15); - assert_eq!(initial_max_request_id(4, true), 8); - - let mut open: Vec<_> = (0..4) - .map(|i| control.accept(RequestId(i * 2)).unwrap().unwrap()) - .collect(); - assert!(matches!( - control.accept(RequestId(8)), - Err(Error::Session(SessionError::TooManyRequests)) - )); - assert_eq!(granted(&control), None, "nothing retired yet"); - - // One retired is below half the window, so nothing is granted yet. - open.pop(); - assert_eq!(granted(&control), None); - - // Half the window retired: two more IDs. - open.pop(); - assert_eq!(granted(&control), Some(12)); - assert_eq!(granted(&control), None, "each grant goes out once"); - - let _a = control.accept(RequestId(8)).unwrap(); - let _b = control.accept(RequestId(10)).unwrap(); - assert!(control.accept(RequestId(12)).is_err()); - } - - /// An ID remains spent after its request ends, across every request type. - #[test] - fn retired_or_lower_request_ids_are_refused() { - for later in [0, 2] { - let control = Control::new(None, false).with_window(8, false, Version::Draft15); - for id in [0, 2, 4] { - drop(control.accept(RequestId(id)).unwrap()); - } - let err = control.accept(RequestId(later)).err().expect("spent ID refused"); - assert_eq!(SessionError::from(&err).to_code(), 0x4); - } - } - - #[test] - fn a_duplicate_is_invalid_even_when_the_window_is_full() { - for client in [false, true] { - let control = Control::new(None, client).with_window(1, client, Version::Draft15); - let id = RequestId(u64::from(client)); - let _held = control.accept(id).unwrap(); - assert!(matches!( - control.accept(id), - Err(Error::Session(SessionError::InvalidRequestId)) - )); - } - } - - /// The first ID and every subsequent request must be exactly the next in sequence. - #[test] - fn skipped_and_wrong_parity_request_ids_are_refused() { - for client in [false, true] { - let first = u64::from(client); - for prefix in [0, 1, 2] { - for gap in [2, 4, 1] { - let control = Control::new(None, client).with_window(8, client, Version::Draft15); - for index in 0..prefix { - drop(control.accept(RequestId(first + 2 * index)).unwrap()); - } - let err = control - .accept(RequestId(first + 2 * prefix + gap)) - .err() - .expect("non-sequential ID refused"); - assert_eq!(SessionError::from(&err), SessionError::InvalidRequestId); - } - } - } - } - - #[test] - fn draft16_independent_streams_can_arrive_out_of_order() { - for client in [false, true] { - let control = Control::new(None, client).with_window(8, client, Version::Draft16); - let first = u64::from(client); - for id in [first + 2, first, first + 6, first + 4] { - drop(control.accept(RequestId(id)).expect("independent stream arrival order")); - } - for id in [first, first + 2, first + 4, first + 6] { - assert!(matches!( - control.accept(RequestId(id)), - Err(Error::Session(SessionError::InvalidRequestId)) - )); - } - } - } - - #[test] - fn draft16_remembers_only_missing_ids_while_other_requests_retire() { - let control = Control::new(None, false).with_window(4, false, Version::Draft16); - for id in 1..1024 { - drop(control.accept(RequestId(id * 2)).unwrap()); - let state = control.state.lock(); - let missing = &state.incoming.as_ref().unwrap().spent.missing; - assert_eq!(missing.iter().collect::>(), [(&0, &2)]); - } - drop(control.accept(RequestId(0)).unwrap()); - assert!(control.state.lock().incoming.as_ref().unwrap().spent.missing.is_empty()); - for id in [0, 2, 2046] { - assert!(matches!( - control.accept(RequestId(id)), - Err(Error::Session(SessionError::InvalidRequestId)) - )); - } - } - - /// Skipping to the top of a production-sized window records one run, not every ID. - #[test] - fn draft16_a_jump_is_one_run_split_only_by_requests_inside_it() { - let window = crate::session::Limits::default().window(); - let control = Control::new(None, false).with_window(window, false, Version::Draft16); - let top = 2 * (window - 1); - let runs = |control: &Control| { - let state = control.state.lock(); - let missing = &state.incoming.as_ref().unwrap().spent.missing; - missing.iter().map(|(&start, &end)| (start, end)).collect::>() - }; - - drop(control.accept(RequestId(top)).unwrap()); - assert_eq!(runs(&control), [(0, top)]); - - for id in [100, 0, top - 2] { - drop(control.accept(RequestId(id)).unwrap()); - } - assert_eq!(runs(&control), [(2, 100), (102, top - 2)]); - - for id in [0, 100, top - 2, top] { - assert!(matches!( - control.accept(RequestId(id)), - Err(Error::Session(SessionError::InvalidRequestId)) - )); - } - } - #[test] - fn modern_requests_reject_reuse_and_wrong_parity() { + fn requests_reject_reuse_and_wrong_parity() { for client in [false, true] { - let control = Control::new(None, client); + let control = Control::new(client); let first = u64::from(client); for id in [first + 4, first, first + 2] { - assert!(control.accept(RequestId(id)).unwrap().is_none()); + control.accept(RequestId(id)).unwrap(); } for id in [first, first + 2, first + 4, first ^ 1] { assert!( @@ -532,20 +208,20 @@ mod tests { /// Contiguous arrival in either order leaves no runs, and a full history refuses a new /// gap rather than forgetting an old one. #[test] - fn modern_history_coalesces_and_never_forgets() { + fn history_coalesces_and_never_forgets() { for reversed in [false, true] { - let control = Control::new(None, false).with_history(1); + let control = Control::new(false).with_history(1); for n in 0..4096 { let id = if reversed { 4095 - n } else { n } * 2; control.accept(RequestId(id)).unwrap(); } - assert!(control.state.lock().modern.missing.is_empty()); + assert!(control.state.lock().spent.missing.is_empty()); assert!(matches!( control.accept(RequestId(0)), Err(Error::Session(SessionError::InvalidRequestId)) )); } - let control = Control::new(None, false).with_history(1); + let control = Control::new(false).with_history(1); control.accept(RequestId(0)).unwrap(); control.accept(RequestId(4)).unwrap(); assert!(matches!( @@ -563,12 +239,12 @@ mod tests { } #[test] - fn modern_sparse_ids_do_not_allocate_the_gap() { - let control = Control::new(None, false).with_history(1); + fn sparse_ids_do_not_allocate_the_gap() { + let control = Control::new(false).with_history(1); let top = (1 << 62) - 2; control.accept(RequestId(top)).unwrap(); control.accept(RequestId(0)).unwrap(); - let runs: Vec<_> = control.state.lock().modern.missing.clone().into_iter().collect(); + let runs: Vec<_> = control.state.lock().spent.missing.clone().into_iter().collect(); assert_eq!(runs, [(2, top)]); for id in [top, 1 << 62, u64::MAX - 1] { assert!(matches!( @@ -577,29 +253,4 @@ mod tests { )); } } - - /// Draft-17+ has no MAX_REQUEST_ID: nothing is granted. - #[test] - fn unlimited_without_a_window() { - let control = Control::new(None, true); - assert!(control.accept(RequestId(1)).unwrap().is_none()); - assert_eq!(granted(&control), None); - } - - /// Allocation waits until every overlapping legacy header hold has ended. - #[moq_clock::test] - async fn overlapping_send_holds_block_allocation_until_all_end() { - let control = Control::new(None, false); - let clock = crate::time::sim(); - let a = control.sending(); - let b = control.sending(); - drop(a); - let mut next = std::pin::pin!(control.next_request_id(&clock)); - assert!(kio::Waiter::noop().poll_future(next.as_mut()).is_pending()); - drop(b); - assert!(matches!( - kio::Waiter::noop().poll_future(next.as_mut()), - Poll::Ready(Ok(RequestId(1))) - )); - } } diff --git a/rs/moq-net/src/ietf/datagram.rs b/rs/moq-net/src/ietf/datagram.rs index 6d7f18bbae9..595322665dd 100644 --- a/rs/moq-net/src/ietf/datagram.rs +++ b/rs/moq-net/src/ietf/datagram.rs @@ -1,9 +1,7 @@ -//! OBJECT_DATAGRAM: one Object carried in a QUIC datagram (draft-14 section 10.3.1 through -//! draft-20 section 11.3.1). +//! OBJECT_DATAGRAM: one Object carried in a QUIC datagram (draft-20 section 11.3.1). //! //! Public [`crate::Datagram`] writers emit Object ID 0; private relay metadata preserves -//! other IDs, properties, and status. The Type is a set of flags; draft-14 lacks the -//! DEFAULT_PRIORITY bit and a status with an omitted Object ID. +//! other IDs, properties, and status. The Type is a set of flags. use bytes::Bytes; @@ -38,7 +36,7 @@ pub struct ObjectDatagram { pub group_id: u64, /// The Object ID, or `None` when the ZERO_OBJECT_ID bit omits it (Object 0). pub object_id: Option, - /// The Publisher Priority, or `None` to inherit the subscription's (draft-15+). + /// The Publisher Priority, or `None` to inherit the subscription's. pub publisher_priority: Option, /// No Object past this one exists in the group. pub end_of_group: bool, @@ -48,13 +46,9 @@ pub struct ObjectDatagram { } impl ObjectDatagram { - /// Whether `kind` is a Type this draft defines. - fn valid(kind: u64, version: Version) -> bool { - match version { - Version::Draft14 => kind <= 0x07 || kind == 0x20 || kind == 0x21, - // A status cannot also end the group. - _ => kind & !flag::ALL == 0 && !(kind & flag::STATUS != 0 && kind & flag::END_OF_GROUP != 0), - } + /// Whether `kind` is a defined Type. A status cannot also end the group. + fn valid(kind: u64) -> bool { + kind & !flag::ALL == 0 && !(kind & flag::STATUS != 0 && kind & flag::END_OF_GROUP != 0) } /// The Type bits this datagram's fields set. @@ -80,10 +74,9 @@ impl ObjectDatagram { } impl crate::model::datagram::Wire { - /// Whether `version`, the draft its track's publisher spoke, defines a datagram of this - /// shape, as its ingress checks one. Egress resolves the Object ID and priority each - /// draft needs. - pub(crate) fn defined(&self, version: Version) -> bool { + /// Whether a datagram of this shape is defined, as ingress checks one. Egress resolves + /// the Object ID and priority each draft needs. + pub(crate) fn defined(&self) -> bool { let shape = ObjectDatagram { track_alias: 0, group_id: 0, @@ -96,24 +89,19 @@ impl crate::model::datagram::Wire { None => DatagramBody::Payload(Bytes::new()), }, }; - ObjectDatagram::valid(shape.kind(), version) + ObjectDatagram::valid(shape.kind()) && self.status.is_none_or(|status| { - super::object::status_defined(status, version) - && (self.properties.block.is_none() || carries_properties(status, version)) + super::object::status_defined(status) && (self.properties.block.is_none() || status == 0) }) } } -/// Whether `version` lets a datagram of Object Status `status` carry properties: from -/// draft-17 on, only a Normal Object may. -pub(super) fn carries_properties(status: u64, version: Version) -> bool { - status == 0 || matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) -} + impl Encode for ObjectDatagram { - fn encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { + fn encode(&self, w: &mut Encoder<'_>, _: Version) -> Result<(), EncodeError> { let kind = self.kind(); - if !Self::valid(kind, version) { + if !Self::valid(kind) { return Err(EncodeError::InvalidState); } @@ -136,7 +124,8 @@ impl Encode for ObjectDatagram { match &self.body { DatagramBody::Status(status) => { - if self.properties.is_some() && !carries_properties(*status, version) { + // Only a Normal Object may carry properties. + if self.properties.is_some() && *status != 0 { return Err(EncodeError::InvalidState); } w.varint(*status)? @@ -151,9 +140,9 @@ impl Encode for ObjectDatagram { } impl Decode for ObjectDatagram { - fn decode(r: &mut Decoder<'_>, version: Version) -> Result { + fn decode(r: &mut Decoder<'_>, _: Version) -> Result { let kind = r.varint()?; - if !Self::valid(kind, version) { + if !Self::valid(kind) { return Err(DecodeError::InvalidValue); } @@ -181,7 +170,7 @@ impl Decode for ObjectDatagram { let body = match kind & flag::STATUS != 0 { true => { let status = r.varint()?; - if properties.is_some() && !carries_properties(status, version) { + if properties.is_some() && status != 0 { return Err(DecodeError::InvalidValue); } if !r.is_empty() { @@ -215,10 +204,6 @@ mod tests { } const ALL: [Version; 9] = [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -308,7 +293,7 @@ mod tests { body: DatagramBody::Status(3), }; for version in ALL { - let legacy = matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16); + let legacy = false; match datagram.encode_bytes(version) { Ok(buf) => { assert!(legacy, "{version}"); diff --git a/rs/moq-net/src/ietf/epoch.rs b/rs/moq-net/src/ietf/epoch.rs index 34f99e994c2..013920c52fa 100644 --- a/rs/moq-net/src/ietf/epoch.rs +++ b/rs/moq-net/src/ietf/epoch.rs @@ -24,21 +24,14 @@ pub const EPOCH: u64 = 0x40B68; /// value is length-prefixed. pub const EPOCH_PARAM: u64 = 0x40B69; -/// Whether a version negotiates this extension: draft-17+, like MoQ Cluster. -pub fn supported(version: Version) -> bool { - !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) -} - /// Whether the peer declared the option. -pub fn from_setup(params: &super::Parameters, version: Version) -> bool { - supported(version) && params.get_varint(super::ParameterVarInt::Epoch).is_some() +pub fn from_setup(params: &super::Parameters) -> bool { + params.get_varint(super::ParameterVarInt::Epoch).is_some() } /// Declare that we understand the EPOCH parameter. -pub fn into_setup(params: &mut super::Parameters, version: Version) { - if supported(version) { - params.set_varint(super::ParameterVarInt::Epoch, 1); - } +pub fn into_setup(params: &mut super::Parameters) { + params.set_varint(super::ParameterVarInt::Epoch, 1); } impl Param for Epoch { @@ -66,13 +59,11 @@ mod tests { } #[test] - fn only_draft17_and_later_negotiate() { - for version in [Version::Draft14, Version::Draft16, Version::Draft17, Version::Draft22] { - let mut params = super::super::Parameters::default(); - assert!(!from_setup(¶ms, version)); - into_setup(&mut params, version); - assert_eq!(from_setup(¶ms, version), supported(version), "{version:?}"); - } + fn declared_option_negotiates() { + let mut params = super::super::Parameters::default(); + assert!(!from_setup(¶ms)); + into_setup(&mut params); + assert!(from_setup(¶ms)); } #[test] diff --git a/rs/moq-net/src/ietf/error.rs b/rs/moq-net/src/ietf/error.rs index de60a3a2142..ee711790324 100644 --- a/rs/moq-net/src/ietf/error.rs +++ b/rs/moq-net/src/ietf/error.rs @@ -8,33 +8,16 @@ //! //! Sent on RESET_STREAM and STOP_SENDING, and read back off both. The values are the //! draft's, not [`StreamError::to_code`]'s: the two registries agree on most of what they -//! both assign, but not all of it, and this one grew (and moved a value) across the drafts -//! we negotiate, so a code only means something once you know which draft carried it. -//! -//! | Code | draft-14/15 | draft-16 | draft-17 | draft-18+ | -//! |------|-------------|----------|----------|-----------| -//! | 0x0 | INTERNAL_ERROR | INTERNAL_ERROR | INTERNAL_ERROR | INTERNAL_ERROR | -//! | 0x1 | CANCELLED | CANCELLED | CANCELLED | CANCELLED | -//! | 0x2 | DELIVERY_TIMEOUT | DELIVERY_TIMEOUT | DELIVERY_TIMEOUT | DELIVERY_TIMEOUT | -//! | 0x3 | SESSION_CLOSED | SESSION_CLOSED | SESSION_CLOSED | SESSION_CLOSED | -//! | 0x4 | - | UNKNOWN_OBJECT_STATUS | UNKNOWN_OBJECT_STATUS | GOING_AWAY | -//! | 0x5 | - | - | TOO_FAR_BEHIND | TOO_FAR_BEHIND | -//! | 0x12 | - | MALFORMED_TRACK | MALFORMED_TRACK | MALFORMED_TRACK | -//! -//! Encoding and decoding therefore move together and both take the negotiated version: -//! GOING_AWAY sent to a draft-17 peer reads as UNKNOWN_OBJECT_STATUS, and a draft-17 -//! peer's UNKNOWN_OBJECT_STATUS read as GOING_AWAY would retire a session that is not -//! going anywhere. +//! both assign, but not all of it. use super::Version; use crate::{SessionError, StreamError}; /// An implementation-specific error: the stream died on our side, with no registry entry -/// for why. Assigned by every draft we negotiate. +/// for why. pub const INTERNAL_ERROR: u32 = 0x0; /// The stream was cancelled by either endpoint. A routine unsubscribe, not a failure. -/// Assigned by every draft we negotiate. pub const CANCELLED: u32 = 0x1; /// The content missed its delivery deadline. @@ -43,43 +26,21 @@ const DELIVERY_TIMEOUT: u32 = 0x2; /// The session is closing, taking this stream with it. const SESSION_CLOSED: u32 = 0x3; -/// A GOAWAY was sent or received. Draft-18 and later; draft-16 and 17 gave 0x4 to -/// UNKNOWN_OBJECT_STATUS instead. +/// A GOAWAY was sent or received. const GOING_AWAY: u32 = 0x4; -/// The subscription outran the publisher's resource limits. Draft-17 and later. +/// The subscription outran the publisher's resource limits. const TOO_FAR_BEHIND: u32 = 0x5; -/// The track's content could not be parsed. Draft-16 and later. +/// The track's content could not be parsed. const MALFORMED_TRACK: u32 = 0x12; -/// Whether the draft assigns 0x4 to GOING_AWAY. -/// -/// Draft-16 and 17 assign it to UNKNOWN_OBJECT_STATUS, which draft-18 moved to 0x6 when it -/// took 0x4 for this. Draft-14 and 15 assign it nothing. -fn has_going_away(version: Version) -> bool { - matches!( - version, - Version::Draft18 | Version::Draft19 | Version::Draft20 | Version::Draft21 | Version::Draft22 - ) -} - -/// Whether the draft assigns TOO_FAR_BEHIND. Added in draft-17. -fn has_too_far_behind(version: Version) -> bool { - !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) -} - -/// Whether the draft assigns MALFORMED_TRACK. Added in draft-16. -fn has_malformed_track(version: Version) -> bool { - !matches!(version, Version::Draft14 | Version::Draft15) -} - -/// The code to reset a stream, or send STOP_SENDING, with on the negotiated draft. +/// The code to reset a stream, or send STOP_SENDING, with. /// /// Conditions without a matching reset code use INTERNAL_ERROR. Request rejection has /// its own registry: a missing track belongs in a request error response, not a reset. /// moq-lite's own 48-63 range and its application offset have no corresponding ranges here. -pub fn to_stream_code(err: &StreamError, version: Version) -> u32 { +pub fn to_stream_code(err: &StreamError) -> u32 { match err { StreamError::Internal => INTERNAL_ERROR, StreamError::Cancel => CANCELLED, @@ -90,30 +51,30 @@ pub fn to_stream_code(err: &StreamError, version: Version) -> u32 { // Flattened, as on the moq-lite wire: the session registry is disjoint, so the // specific reason travels on the session close instead. StreamError::Session(_) => SESSION_CLOSED, - StreamError::GoingAway if has_going_away(version) => GOING_AWAY, - StreamError::TooFarBehind if has_too_far_behind(version) => TOO_FAR_BEHIND, - StreamError::MalformedTrack if has_malformed_track(version) => MALFORMED_TRACK, + StreamError::GoingAway => GOING_AWAY, + StreamError::TooFarBehind => TOO_FAR_BEHIND, + StreamError::MalformedTrack => MALFORMED_TRACK, _ => INTERNAL_ERROR, } } -/// Read a stream reset (or STOP_SENDING) code received on the negotiated draft. +/// Read a stream reset (or STOP_SENDING) code. /// -/// A code the draft does not assign stays [`StreamError::Unknown`], which surfaces as +/// A code the drafts do not assign stays [`StreamError::Unknown`], which surfaces as /// [`Error::Stream`](crate::Error::Stream): an error, but never one given a meaning it did /// not carry. That includes the codes this crate has no local counterpart for /// (UNKNOWN_OBJECT_STATUS, EXCESSIVE_LOAD) and every value a later /// draft may add. -pub fn from_stream_code(code: u32, version: Version) -> StreamError { +pub fn from_stream_code(code: u32) -> StreamError { match code { INTERNAL_ERROR => StreamError::Internal, CANCELLED => StreamError::Cancel, DELIVERY_TIMEOUT => StreamError::DeliveryTimeout, // The peer's session code is not on this stream, so the reason is unknown here. SESSION_CLOSED => StreamError::Session(SessionError::Internal), - GOING_AWAY if has_going_away(version) => StreamError::GoingAway, - TOO_FAR_BEHIND if has_too_far_behind(version) => StreamError::TooFarBehind, - MALFORMED_TRACK if has_malformed_track(version) => StreamError::MalformedTrack, + GOING_AWAY => StreamError::GoingAway, + TOO_FAR_BEHIND => StreamError::TooFarBehind, + MALFORMED_TRACK => StreamError::MalformedTrack, code => StreamError::Unknown(code), } } @@ -131,55 +92,34 @@ pub(crate) fn to_session_code(error: &SessionError) -> u32 { /// The moq-transport request error registry, per negotiated draft. /// -/// Sent in a rejection response to a request, and read back off one. Draft-14 gives each -/// error message its own registry and they disagree about 0x4; draft-15 folded them all -/// into REQUEST_ERROR with a single registry that renumbered almost everything. So a -/// request code only means something once you know both the draft and, on draft-14, which -/// request it answers. -/// -/// | Condition | 14 SUBSCRIBE | 14 FETCH | 14 PUBLISH | 14 ANNOUNCE | 14 SUB_NS | 15+ | -/// |-----------|--------------|----------|------------|-------------|-----------|-----| -/// | INTERNAL_ERROR | 0x0 | 0x0 | 0x0 | 0x0 | 0x0 | 0x0 | -/// | UNAUTHORIZED | 0x1 | 0x1 | 0x1 | 0x1 | 0x1 | 0x1 | -/// | TIMEOUT | 0x2 | 0x2 | 0x2 | 0x2 | 0x2 | 0x2 | -/// | NOT_SUPPORTED | 0x3 | 0x3 | 0x3 | 0x3 | 0x3 | 0x3 | -/// | DOES_NOT_EXIST | 0x4 | 0x4 | - | - | - | 0x10 | -/// | UNINTERESTED | - | - | 0x4 | 0x4 | - | 0x20 | -/// | MALFORMED_TRACK | - | 0x9 | - | - | - | 0x12 | -/// | EXPIRED_AUTH_TOKEN | 0x12 | 0x12 | - | 0x12 | 0x12 | 0x5 | -/// | GOING_AWAY | - | - | - | - | - | 0x6 (17+) | -/// | UNSUPPORTED_EXTENSION | - | - | - | - | - | 0x33 (18+) | -/// -/// The renumbering is the trap: draft-14 SUBSCRIBE_ERROR gives 0x4 to TRACK_DOES_NOT_EXIST -/// and 0x10 to MALFORMED_AUTH_TOKEN, and draft-15 swaps the two. Sending either number -/// without the draft in hand tells the peer the opposite of what happened. +/// Sent in a rejection response to a request, and read back off one. The registry is +/// disjoint from the stream reset one, so the same integer means something else there. pub(crate) mod request { use super::Version; use crate::{DecodeError, Error, SessionError, StreamError}; - /// Which request the error answers, so draft-14 picks the right registry. + /// Which request the error answers. /// - /// Draft-15 and later carry one REQUEST_ERROR registry for every request, so from there - /// on the kind only decides what a refused route says: not here to a request for - /// content, uninterested to an offer of it. + /// Every request shares one REQUEST_ERROR registry, so the kind only decides what a + /// refused route says (not here to a request for content, uninterested to an offer of + /// it) and which codes only FETCH carries. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum Kind { - /// SUBSCRIBE_ERROR, draft-14 section 13.1.2. + /// SUBSCRIBE. Subscribe, - /// FETCH_ERROR, draft-14 section 13.1.5. + /// FETCH. Fetch, - /// PUBLISH_ERROR, draft-14 section 13.1.4. + /// PUBLISH. Publish, - /// ANNOUNCE_ERROR, draft-14 section 13.1.6. + /// PUBLISH_NAMESPACE. PublishNamespace, - /// SUBSCRIBE_NAMESPACE_ERROR, draft-14 section 13.1.7. + /// SUBSCRIBE_NAMESPACE. SubscribeNamespace, - /// TRACK_STATUS_ERROR on draft-14, REQUEST_ERROR later. + /// TRACK_STATUS. TrackStatus, } - /// An implementation-specific error, with no registry entry for why. Assigned by every - /// draft, for every request. + /// An implementation-specific error, with no registry entry for why. const INTERNAL_ERROR: u64 = 0x0; /// The peer's credentials do not cover the request. @@ -191,86 +131,47 @@ pub(crate) mod request { /// The endpoint does not implement this request at all, as opposed to refusing this one. pub(crate) const NOT_SUPPORTED: u64 = 0x3; - /// A second subscription to a track the peer already subscribes to. Draft-16 and later. + /// A second subscription to a track the peer already subscribes to. pub(crate) const DUPLICATE_SUBSCRIPTION: u64 = 0x19; - /// The endpoint is overloaded and cannot process the request. Draft-18 and later. + /// The endpoint is overloaded and cannot process the request. pub(crate) const EXCESSIVE_LOAD: u64 = 0x9; /// A SUBSCRIBE_NAMESPACE or SUBSCRIBE_TRACKS prefix overlapping an established one. - /// Draft-15 and later. pub(crate) const PREFIX_OVERLAP: u64 = 0x30; - /// A GOAWAY is draining the session, so no new request is accepted. Draft-17 and later. + /// A GOAWAY is draining the session, so no new request is accepted. const GOING_AWAY: u64 = 0x6; - /// The requested broadcast or track is not here, as draft-14 numbers it under the name - /// TRACK_DOES_NOT_EXIST. - const DOES_NOT_EXIST_14: u64 = 0x4; - - /// An invalid FETCH range: draft-14 FETCH_ERROR and draft-15+ REQUEST_ERROR. - const INVALID_RANGE_14: u64 = 0x5; + /// An invalid FETCH range. const INVALID_RANGE: u64 = 0x11; - /// The track holds a Mandatory Track Property the endpoint does not understand. Draft-18 - /// and later; draft-15 gave 0x33 another meaning. + /// The track holds a Mandatory Track Property the endpoint does not understand. const UNSUPPORTED_EXTENSION: u64 = 0x33; /// A Range Filter exceeds MAX_FILTER_RANGES (draft-19 and later). pub(crate) const INVALID_FILTER: u64 = 0x36; /// A joining FETCH named no active subscription. Removed from the registry in draft-20. - const INVALID_JOINING_REQUEST_ID_14: u64 = 0x7; const INVALID_JOINING_REQUEST_ID: u64 = 0x32; - /// The same condition from draft-15 on, renamed DOES_NOT_EXIST and moved off 0x4. + /// The requested broadcast or track is not here. const DOES_NOT_EXIST: u64 = 0x10; - /// The offered content is not wanted here, as draft-14 numbers it. - const UNINTERESTED_14: u64 = 0x4; - - /// The same condition from draft-15 on. + /// The offered content is not wanted here. const UNINTERESTED: u64 = 0x20; - /// The track's content could not be parsed, as draft-14 numbers it on FETCH_ERROR. - const MALFORMED_TRACK_14: u64 = 0x9; - - /// The same condition from draft-15 on. + /// The track's content could not be parsed. const MALFORMED_TRACK: u64 = 0x12; - /// A well-formed AUTHORIZATION TOKEN this endpoint cannot use, as draft-14 numbers it. - const MALFORMED_AUTH_TOKEN_14: u64 = 0x10; - - /// The same condition from draft-15 on, where 0x10 became DOES_NOT_EXIST. + /// A well-formed AUTHORIZATION TOKEN this endpoint cannot use. const MALFORMED_AUTH_TOKEN: u64 = 0x4; - /// An expired AUTHORIZATION TOKEN, as draft-14 numbers it. - const EXPIRED_AUTH_TOKEN_14: u64 = 0x12; - - /// The same condition from draft-15 on, where 0x12 became MALFORMED_TRACK. + /// An expired AUTHORIZATION TOKEN. const EXPIRED_AUTH_TOKEN: u64 = 0x5; - /// The value for "the thing you asked for is not here", or `None` where the draft and - /// request assign none. - fn does_not_exist(kind: Kind, version: Version) -> Option { - match version { - Version::Draft14 => match kind { - Kind::Subscribe | Kind::Fetch | Kind::TrackStatus => Some(DOES_NOT_EXIST_14), - _ => None, - }, - _ => Some(DOES_NOT_EXIST), - } - } - - fn invalid_range(kind: Kind, version: Version) -> Option { - if kind != Kind::Fetch { - return None; - } - Some(if version == Version::Draft14 { - INVALID_RANGE_14 - } else { - INVALID_RANGE - }) + fn invalid_range(kind: Kind) -> Option { + (kind == Kind::Fetch).then_some(INVALID_RANGE) } fn invalid_joining_request_id(kind: Kind, version: Version) -> Option { @@ -278,75 +179,13 @@ pub(crate) mod request { return None; } match version { - Version::Draft14 => Some(INVALID_JOINING_REQUEST_ID_14), - Version::Draft15 | Version::Draft16 | Version::Draft17 | Version::Draft18 | Version::Draft19 => { + Version::Draft18 | Version::Draft19 => { Some(INVALID_JOINING_REQUEST_ID) } _ => None, } } - /// The value for "we do not want this", or `None` where the draft and request assign - /// none. Draft-14 registers it only on the two requests that offer content. - fn uninterested(kind: Kind, version: Version) -> Option { - match version { - Version::Draft14 => match kind { - Kind::Publish | Kind::PublishNamespace => Some(UNINTERESTED_14), - _ => None, - }, - _ => Some(UNINTERESTED), - } - } - - /// The value for a track we could not parse, or `None` where the draft and request - /// assign none. - fn malformed_track(kind: Kind, version: Version) -> Option { - match version { - Version::Draft14 => match kind { - Kind::Fetch => Some(MALFORMED_TRACK_14), - _ => None, - }, - _ => Some(MALFORMED_TRACK), - } - } - - /// The value for an unusable token, or `None` where the draft and request assign none. - /// Draft-14 registers none on PUBLISH_ERROR. - fn malformed_auth_token(kind: Kind, version: Version) -> Option { - match (version, kind) { - (Version::Draft14, Kind::Publish) => None, - (Version::Draft14, _) => Some(MALFORMED_AUTH_TOKEN_14), - _ => Some(MALFORMED_AUTH_TOKEN), - } - } - - /// The value for an expired token, or `None` where the draft and request assign none. - /// Draft-14 registers none on PUBLISH_ERROR. - fn expired_auth_token(kind: Kind, version: Version) -> Option { - match (version, kind) { - (Version::Draft14, Kind::Publish) => None, - (Version::Draft14, _) => Some(EXPIRED_AUTH_TOKEN_14), - _ => Some(EXPIRED_AUTH_TOKEN), - } - } - - /// The value for a track needing a property we do not understand, or `None` before - /// draft-18 registered one. - fn unsupported_extension(version: Version) -> Option { - match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 => None, - _ => Some(UNSUPPORTED_EXTENSION), - } - } - - /// The value for a draining session, or `None` before draft-17 registered one. - fn going_away(version: Version) -> Option { - match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => None, - _ => Some(GOING_AWAY), - } - } - /// The code to reject a request with, on the negotiated draft. /// /// Lossy on purpose, like [`to_stream_code`](super::to_stream_code): an error says what went @@ -359,7 +198,7 @@ pub(crate) mod request { Error::Unauthorized | Error::Session(SessionError::Unauthorized) | Error::Stream(StreamError::Unauthorized) => return UNAUTHORIZED, - Error::Expired | Error::Session(SessionError::Expired) => expired_auth_token(kind, version), + Error::Expired | Error::Session(SessionError::Expired) => return EXPIRED_AUTH_TOKEN, Error::Timeout | Error::Stream(StreamError::DeliveryTimeout) | Error::Session(SessionError::Timeout) => { return TIMEOUT; } @@ -374,26 +213,27 @@ pub(crate) mod request { // does not implement FETCH at all. Error::NotFound | Error::NotFetchable - | Error::Stream(StreamError::NotFound | StreamError::NotFetchable) => does_not_exist(kind, version), - Error::InvalidRange => invalid_range(kind, version), + | Error::Stream(StreamError::NotFound | StreamError::NotFetchable) => return DOES_NOT_EXIST, + Error::InvalidRange => invalid_range(kind), Error::InvalidJoiningRequestId => invalid_joining_request_id(kind, version), // A path with no route is one we will not carry. A subscriber that asked for it // cannot act on "we do not want it": what it needs to know is that we do not have // it, which is the same refusal from its side. Only the requests that offer content // say UNINTERESTED. - Error::Unroutable | Error::Stream(StreamError::Unroutable) => match kind { - Kind::Subscribe | Kind::Fetch | Kind::TrackStatus => does_not_exist(kind, version), - Kind::Publish | Kind::PublishNamespace | Kind::SubscribeNamespace => uninterested(kind, version), - }, - Error::Decode(DecodeError::MalformedAuthToken) => malformed_auth_token(kind, version), + Error::Unroutable | Error::Stream(StreamError::Unroutable) => { + return match kind { + Kind::Subscribe | Kind::Fetch | Kind::TrackStatus => DOES_NOT_EXIST, + Kind::Publish | Kind::PublishNamespace | Kind::SubscribeNamespace => UNINTERESTED, + }; + } + Error::Decode(DecodeError::MalformedAuthToken) => return MALFORMED_AUTH_TOKEN, // Our own parse failure is, from the peer's side, a malformed track. Error::Decode(_) | Error::BoundsExceeded(_) | Error::MalformedTrack - | Error::Stream(StreamError::MalformedTrack) => malformed_track(kind, version), - Error::GoingAway | Error::Stream(StreamError::GoingAway) => going_away(version), - // Before its code, the closest is that we do not implement what the track needs. - Error::UnsupportedExtension => unsupported_extension(version).or(Some(NOT_SUPPORTED)), + | Error::Stream(StreamError::MalformedTrack) => return MALFORMED_TRACK, + Error::GoingAway | Error::Stream(StreamError::GoingAway) => return GOING_AWAY, + Error::UnsupportedExtension => return UNSUPPORTED_EXTENSION, // Everything else, a duplicate included: no draft assigns it a value, and // INTERNAL_ERROR is how a peer reads an unregistered code anyway. _ => return INTERNAL_ERROR, @@ -411,14 +251,14 @@ pub(crate) mod request { UNAUTHORIZED => Error::Unauthorized, TIMEOUT => Error::Timeout, NOT_SUPPORTED => Error::Unsupported, - code if Some(code) == expired_auth_token(kind, version) => Error::Expired, - code if Some(code) == does_not_exist(kind, version) => Error::NotFound, - code if Some(code) == invalid_range(kind, version) => Error::InvalidRange, + EXPIRED_AUTH_TOKEN => Error::Expired, + DOES_NOT_EXIST => Error::NotFound, + code if Some(code) == invalid_range(kind) => Error::InvalidRange, code if Some(code) == invalid_joining_request_id(kind, version) => Error::InvalidJoiningRequestId, - code if Some(code) == uninterested(kind, version) => Error::Unroutable, - code if Some(code) == malformed_track(kind, version) => Error::MalformedTrack, - code if Some(code) == going_away(version) => Error::GoingAway, - code if Some(code) == unsupported_extension(version) => Error::UnsupportedExtension, + UNINTERESTED => Error::Unroutable, + MALFORMED_TRACK => Error::MalformedTrack, + GOING_AWAY => Error::GoingAway, + UNSUPPORTED_EXTENSION => Error::UnsupportedExtension, code => Error::Remote(u32::try_from(code).unwrap_or(u32::MAX)), } } @@ -427,11 +267,7 @@ pub(crate) mod request { mod tests { use super::*; - const ALL: [Version; 9] = [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, + const ALL: [Version; 5] = [ Version::Draft18, Version::Draft19, Version::Draft20, @@ -483,20 +319,15 @@ pub(crate) mod request { } } - /// Draft-14 numbers a missing track 0x4 and draft-15 moved it to 0x10, which is - /// draft-14's MALFORMED_AUTH_TOKEN. Getting this backwards tells a peer its token is - /// broken when the broadcast simply is not here, so it re-authenticates instead of - /// waiting for the announcement. + /// A missing broadcast is DOES_NOT_EXIST, never 0x4, which is MALFORMED_AUTH_TOKEN: + /// getting this backwards tells a peer its token is broken when the broadcast simply + /// is not here, so it re-authenticates instead of waiting for the announcement. #[test] - fn a_missing_broadcast_uses_the_draft_s_own_number() { + fn a_missing_broadcast_is_not_here() { for kind in [Kind::Subscribe, Kind::Fetch] { - assert_eq!(to_code(&Error::NotFound, kind, Version::Draft14), 0x4); - assert!(matches!(from_code(0x4, kind, Version::Draft14), Error::NotFound)); - - for version in ALL.into_iter().skip(1) { + for version in ALL { assert_eq!(to_code(&Error::NotFound, kind, version), 0x10); assert!(matches!(from_code(0x10, kind, version), Error::NotFound)); - // 0x4 is MALFORMED_AUTH_TOKEN from draft-15 on, which is not ours to claim. assert!(matches!(from_code(0x4, kind, version), Error::Remote(0x4))); } } @@ -506,7 +337,6 @@ pub(crate) mod request { // So is a datagram group: never cached, so a FETCH reaching one finds nothing, and // NOT_SUPPORTED would say the endpoint has no FETCH at all. - assert_eq!(to_code(&Error::NotFetchable, Kind::Fetch, Version::Draft14), 0x4); assert_eq!(to_code(&Error::NotFetchable, Kind::Fetch, Version::Draft20), 0x10); // Including a lite-07 upstream's NOT_FETCHABLE, bridged by a relay. assert_eq!( @@ -515,99 +345,35 @@ pub(crate) mod request { ); } - /// Draft-14 gives 0x4 to UNINTERESTED on the requests that offer content and to - /// TRACK_DOES_NOT_EXIST on the ones that ask for it, so the same integer on the same - /// draft says two different things. + /// A refused route says UNINTERESTED to the requests that offer content. #[test] - fn draft_14_reads_0x4_per_request() { - for kind in [Kind::Publish, Kind::PublishNamespace] { - assert_eq!(to_code(&Error::Unroutable, kind, Version::Draft14), 0x4); - assert!(matches!(from_code(0x4, kind, Version::Draft14), Error::Unroutable)); - // Those requests offer content, so "it is not here" is not a rejection they - // can carry. - assert_eq!(to_code(&Error::NotFound, kind, Version::Draft14), INTERNAL_ERROR); + fn an_unroutable_offer_is_uninterested() { + for kind in [Kind::Publish, Kind::PublishNamespace, Kind::SubscribeNamespace] { + assert_eq!(to_code(&Error::Unroutable, kind, Version::Draft20), 0x20); + assert!(matches!(from_code(0x20, kind, Version::Draft20), Error::Unroutable)); } - - // And neither meaning reaches a request draft-14 gives neither to. - assert!(matches!( - from_code(0x4, Kind::SubscribeNamespace, Version::Draft14), - Error::Remote(0x4) - )); } - /// Draft-15 swapped MALFORMED_AUTH_TOKEN and the missing-track code, and draft-14 - /// gives PUBLISH_ERROR no value for it. #[test] - fn a_malformed_token_uses_the_draft_s_own_number() { + fn token_refusals_use_their_codes() { let err = Error::Decode(DecodeError::MalformedAuthToken); - for kind in KINDS { - let draft14 = if kind == Kind::Publish { INTERNAL_ERROR } else { 0x10 }; - assert_eq!(to_code(&err, kind, Version::Draft14), draft14); - for version in ALL.into_iter().skip(1) { + for version in ALL { + for kind in KINDS { assert_eq!(to_code(&err, kind, version), 0x4); - } - } - } - - /// Draft-15 moved EXPIRED_AUTH_TOKEN from 0x12 to 0x5, and draft-14 gives PUBLISH_ERROR - /// no value for it. - #[test] - fn an_expired_token_uses_the_draft_s_own_number() { - for kind in KINDS { - if kind == Kind::Publish { - assert_eq!(to_code(&Error::Expired, kind, Version::Draft14), INTERNAL_ERROR); - } else { - assert_eq!(to_code(&Error::Expired, kind, Version::Draft14), 0x12); - assert!(matches!(from_code(0x12, kind, Version::Draft14), Error::Expired)); - } - for version in ALL.into_iter().skip(1) { assert_eq!(to_code(&Error::Expired, kind, version), 0x5); assert!(matches!(from_code(0x5, kind, version), Error::Expired)); } } } - /// GOING_AWAY arrived in draft-17. Sent to an earlier peer, 0x6 is unassigned there, - /// so say INTERNAL_ERROR outright rather than a number that means nothing. #[test] - fn going_away_only_exists_from_draft_17() { - for kind in KINDS { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { - assert_eq!(to_code(&Error::GoingAway, kind, version), INTERNAL_ERROR); - assert!(matches!(from_code(GOING_AWAY, kind, version), Error::Remote(0x6))); - } - - for version in [ - Version::Draft17, - Version::Draft18, - Version::Draft19, - Version::Draft20, - Version::Draft21, - Version::Draft22, - ] { + fn going_away_and_unsupported_extension_round_trip() { + for version in ALL { + for kind in KINDS { assert_eq!(to_code(&Error::GoingAway, kind, version), GOING_AWAY); assert!(matches!(from_code(GOING_AWAY, kind, version), Error::GoingAway)); - } - } - } - - /// UNSUPPORTED_EXTENSION arrived in draft-18. Draft-15 gave its 0x33 to - /// UNKNOWN_STATUS_IN_RANGE, so earlier drafts say NOT_SUPPORTED instead. - #[test] - fn unsupported_extension_only_exists_from_draft_18() { - for kind in KINDS { - for version in ALL { - let code = to_code(&Error::UnsupportedExtension, kind, version); - match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 => { - assert_eq!(code, NOT_SUPPORTED, "{version}"); - assert!(!matches!(from_code(0x33, kind, version), Error::UnsupportedExtension)); - } - _ => { - assert_eq!(code, 0x33, "{version}"); - assert!(matches!(from_code(code, kind, version), Error::UnsupportedExtension)); - } - } + assert_eq!(to_code(&Error::UnsupportedExtension, kind, version), 0x33); + assert!(matches!(from_code(0x33, kind, version), Error::UnsupportedExtension)); } } } @@ -615,14 +381,10 @@ pub(crate) mod request { #[test] fn fetch_refusals_follow_each_drafts_registry() { for version in ALL { - let range = if version == Version::Draft14 { 0x5 } else { 0x11 }; - assert_eq!(to_code(&Error::InvalidRange, Kind::Fetch, version), range); - assert!(matches!(from_code(range, Kind::Fetch, version), Error::InvalidRange)); + assert_eq!(to_code(&Error::InvalidRange, Kind::Fetch, version), 0x11); + assert!(matches!(from_code(0x11, Kind::Fetch, version), Error::InvalidRange)); let joining = match version { - Version::Draft14 => Some(0x7), - Version::Draft15 | Version::Draft16 | Version::Draft17 | Version::Draft18 | Version::Draft19 => { - Some(0x32) - } + Version::Draft18 | Version::Draft19 => Some(0x32), _ => None, }; assert_eq!( @@ -660,31 +422,19 @@ pub(crate) mod request { } /// Every code we put in a rejection has to be one the negotiated draft registers for - /// that request. The table is transcribed from the drafts (draft-14 section 13.1 - /// through draft-20 section 15.11.2), not derived from the mapping, so a mistake in - /// the mapping cannot talk the assertion into agreeing with it. + /// that request. The table is transcribed from the drafts (draft-18 section 15.11.2 + /// on), not derived from the mapping, so a mistake in the mapping cannot talk the + /// assertion into agreeing with it. #[test] fn only_registered_codes_reach_the_wire() { - fn registered(kind: Kind, version: Version) -> &'static [u64] { - match (version, kind) { - (Version::Draft14, Kind::Subscribe) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x10, 0x12], - (Version::Draft14, Kind::Fetch) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0x10, 0x12], - (Version::Draft14, Kind::Publish) => &[0x0, 0x1, 0x2, 0x3, 0x4], - (Version::Draft14, Kind::PublishNamespace) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x10, 0x12], - (Version::Draft14, Kind::TrackStatus) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x10, 0x12], - (Version::Draft14, Kind::SubscribeNamespace) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x10, 0x12], - (Version::Draft15, _) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x10, 0x11, 0x12, 0x20, 0x30, 0x32, 0x33], - (Version::Draft16, _) => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x10, 0x11, 0x12, 0x19, 0x20, 0x30, 0x32], - (Version::Draft17, _) => &[ - 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x9, 0x10, 0x11, 0x12, 0x19, 0x20, 0x30, 0x31, 0x32, - ], - _ => &[ - 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x9, 0x10, 0x11, 0x12, 0x19, 0x20, 0x30, 0x31, 0x32, 0x33, - 0x34, 0x35, 0x36, - ], - } + fn registered() -> &'static [u64] { + &[ + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x9, 0x10, 0x11, 0x12, 0x19, 0x20, 0x30, 0x31, 0x32, 0x33, 0x34, + 0x35, 0x36, + ] } + // Sent but never read back as itself: a peer's MALFORMED_AUTH_TOKEN stays remote. let malformed = Error::Decode(DecodeError::MalformedAuthToken); for version in ALL { @@ -692,7 +442,7 @@ pub(crate) mod request { for err in EVERY_ERROR.iter().chain([&malformed]) { let code = to_code(err, kind, version); assert!( - registered(kind, version).contains(&code), + registered().contains(&code), "{err:?} sends {code:#x}, which {version} does not register for {kind:?}" ); } @@ -727,31 +477,17 @@ mod tests { use super::*; use crate::Error; - const ALL: [Version; 9] = [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, - Version::Draft18, - Version::Draft19, - Version::Draft20, - Version::Draft21, - Version::Draft22, - ]; - /// A routine unsubscribe must not read as a fault on our side. moq-lite's own error /// enum encodes a cancellation as 0, which is this wire's INTERNAL_ERROR, so the codes /// come from here instead. #[test] fn a_cancellation_is_not_an_internal_error() { - for version in ALL { - assert_eq!(to_stream_code(&StreamError::Cancel, version), CANCELLED); - assert_eq!(from_stream_code(CANCELLED, version), StreamError::Cancel); - assert!(matches!( - Error::from(from_stream_code(CANCELLED, version)), - Error::Stream(StreamError::Cancel) - )); - } + assert_eq!(to_stream_code(&StreamError::Cancel), CANCELLED); + assert_eq!(from_stream_code(CANCELLED), StreamError::Cancel); + assert!(matches!( + Error::from(from_stream_code(CANCELLED)), + Error::Stream(StreamError::Cancel) + )); assert_ne!( CANCELLED, @@ -781,8 +517,8 @@ mod tests { assert_eq!(to_session_code(&SessionError::App(7)), 64 + 7); } - /// Every code we send must decode back to what we meant on the same draft, or two - /// moq-net peers disagree about what a stream reset said. + /// Every code we send must decode back to what we meant, or two moq-net peers disagree + /// about what a stream reset said. #[test] fn every_emitted_code_round_trips() { let errors = [ @@ -799,85 +535,17 @@ mod tests { StreamError::App(7), ]; - for version in ALL { - for err in &errors { - let code = to_stream_code(err, version); - let decoded = from_stream_code(code, version); - assert_eq!( - to_stream_code(&decoded, version), - code, - "{err:?} on {version:?} did not survive a round trip" - ); - } - - // A session teardown flattens to SESSION_CLOSED and comes back as one, rather - // than as the session's own reason, which the stream never carried. - let code = to_stream_code(&StreamError::Session(SessionError::Unauthorized), version); - assert_eq!(code, SESSION_CLOSED); - assert_eq!( - from_stream_code(code, version), - StreamError::Session(SessionError::Internal) - ); - } - } - - /// Draft-18 took 0x4 for GOING_AWAY from UNKNOWN_OBJECT_STATUS, so the same integer - /// means different things on two drafts we both negotiate. Sending it to draft-17 would - /// claim the next object's status is unknowable; reading theirs as GOING_AWAY would - /// start draining a session that is not going anywhere. - #[test] - fn going_away_only_exists_from_draft_18() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16, Version::Draft17] { - assert_eq!(to_stream_code(&StreamError::GoingAway, version), INTERNAL_ERROR); - assert_eq!(from_stream_code(GOING_AWAY, version), StreamError::Unknown(GOING_AWAY)); + for err in &errors { + let code = to_stream_code(err); + let decoded = from_stream_code(code); + assert_eq!(to_stream_code(&decoded), code, "{err:?} did not survive a round trip"); } - for version in [ - Version::Draft18, - Version::Draft19, - Version::Draft20, - Version::Draft21, - Version::Draft22, - ] { - assert_eq!(to_stream_code(&StreamError::GoingAway, version), GOING_AWAY); - assert_eq!(from_stream_code(GOING_AWAY, version), StreamError::GoingAway); - } - } - - /// The rest of the per-draft registry: TOO_FAR_BEHIND arrived in draft-17 and - /// MALFORMED_TRACK in draft-16, so an older peer must be told neither. - #[test] - fn later_codes_are_not_sent_to_earlier_drafts() { - for version in ALL { - let too_far_behind = to_stream_code(&StreamError::TooFarBehind, version); - let malformed = to_stream_code(&StreamError::MalformedTrack, version); - - assert_eq!( - too_far_behind, - match has_too_far_behind(version) { - true => TOO_FAR_BEHIND, - false => INTERNAL_ERROR, - }, - "{version:?} disagrees about TOO_FAR_BEHIND" - ); - assert_eq!( - malformed, - match has_malformed_track(version) { - true => MALFORMED_TRACK, - false => INTERNAL_ERROR, - }, - "{version:?} disagrees about MALFORMED_TRACK" - ); - } - - assert_eq!( - from_stream_code(TOO_FAR_BEHIND, Version::Draft16), - StreamError::Unknown(TOO_FAR_BEHIND) - ); - assert_eq!( - from_stream_code(MALFORMED_TRACK, Version::Draft15), - StreamError::Unknown(MALFORMED_TRACK) - ); + // A session teardown flattens to SESSION_CLOSED and comes back as one, rather + // than as the session's own reason, which the stream never carried. + let code = to_stream_code(&StreamError::Session(SessionError::Unauthorized)); + assert_eq!(code, SESSION_CLOSED); + assert_eq!(from_stream_code(code), StreamError::Session(SessionError::Internal)); } /// Conditions this registry has no value for say INTERNAL_ERROR rather than borrowing @@ -902,7 +570,7 @@ mod tests { StreamError::Unknown(0x1234), ] { assert_eq!( - to_stream_code(&err, Version::Draft20), + to_stream_code(&err), INTERNAL_ERROR, "{err:?} has no value in this registry" ); @@ -927,9 +595,9 @@ mod tests { 0x3b, 64 + 7, ] { - assert_eq!(from_stream_code(code, Version::Draft20), StreamError::Unknown(code)); + assert_eq!(from_stream_code(code), StreamError::Unknown(code)); assert!(matches!( - Error::from(from_stream_code(code, Version::Draft20)), + Error::from(from_stream_code(code)), Error::Stream(StreamError::Unknown(remote)) if remote == code )); } @@ -965,44 +633,25 @@ mod tests { /// application codes past 64, neither of which this registry has a range for, /// and it assigns 0x4 and 0x5 meanings the earlier drafts give to something else. /// - /// The table is transcribed from the drafts (draft-14 section 13.1.8 through draft-20 - /// section 15.11.4), not derived from the mapping, so a mistake in the mapping cannot - /// talk the assertion into agreeing with it. + /// The table is transcribed from the drafts (draft-18 section 15.11.4 on), not derived + /// from the mapping, so a mistake in the mapping cannot talk the assertion into + /// agreeing with it. #[test] fn only_registered_codes_reach_the_wire() { - fn registered(version: Version) -> &'static [u32] { - match version { - Version::Draft14 | Version::Draft15 => &[0x0, 0x1, 0x2, 0x3], - Version::Draft16 => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x12], - Version::Draft17 => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x9, 0x12], - _ => &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x9, 0x12], - } - } - - for version in ALL { - for err in EVERY_ERROR { - let code = to_stream_code(&err, version); - assert!( - registered(version).contains(&code), - "{err:?} sends {code:#x}, which {version} does not register" - ); - } + const REGISTERED: &[u32] = &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x9, 0x12]; + for err in EVERY_ERROR { + let code = to_stream_code(&err); + assert!(REGISTERED.contains(&code), "{err:?} sends {code:#x}, which is not registered"); } } /// A relay decodes a peer's code and re-encodes it onto the stream it tears down in - /// response. That hop must not change what the code says, on the same draft. + /// response. That hop must not change what the code says. #[test] fn relaying_a_code_does_not_change_its_meaning() { - for version in ALL { - for code in [INTERNAL_ERROR, CANCELLED, DELIVERY_TIMEOUT, SESSION_CLOSED] { - let relayed = StreamError::from(&Error::from(from_stream_code(code, version))); - assert_eq!( - to_stream_code(&relayed, version), - code, - "{code:#x} changed across a relay on {version:?}" - ); - } + for code in [INTERNAL_ERROR, CANCELLED, DELIVERY_TIMEOUT, SESSION_CLOSED] { + let relayed = StreamError::from(&Error::from(from_stream_code(code))); + assert_eq!(to_stream_code(&relayed), code, "{code:#x} changed across a relay"); } } } diff --git a/rs/moq-net/src/ietf/fetch.rs b/rs/moq-net/src/ietf/fetch.rs index 2cb61e3ebad..e267ac5484d 100644 --- a/rs/moq-net/src/ietf/fetch.rs +++ b/rs/moq-net/src/ietf/fetch.rs @@ -4,7 +4,7 @@ use crate::{ Path, coding::{Decode, DecodeError, Decoder, Encode, EncodeError, Encoder}, ietf::{ - Filter, GroupOrder, Location, Opaque, Parameters, RequestId, + Filter, GroupOrder, Location, Opaque, RequestId, namespace::{decode_track, encode_namespace}, subscribe::has_range_filters, }, @@ -151,20 +151,11 @@ impl Message for Fetch<'_> { let group_order = (self.group_order != GroupOrder::Any).then_some(self.group_order); self.request_id.encode(w, version)?; - if version == Version::Draft17 { - w.varint(0)?; // required_request_id_delta = 0 (draft-17 only, removed in draft-18 per #1615) - } match version { - Version::Draft14 => { - w.u8(self.subscriber_priority); - self.group_order.encode(w, version)?; - self.fetch_type.encode(w, version)?; - w.u8(0); // no parameters - } - Version::Draft15 | Version::Draft16 | Version::Draft17 | Version::Draft18 | Version::Draft19 => { + Version::Draft18 | Version::Draft19 => { self.fetch_type.encode(w, version)?; - let epoch = self.epoch.clone().filter(|_| epoch::supported(version)); + let epoch = self.epoch.clone(); encode_params!(w, version, 0x20 => self.subscriber_priority, 0x22 => group_order, @@ -199,40 +190,22 @@ impl Message for Fetch<'_> { fn decode_msg(buf: &mut Decoder<'_>, version: Version) -> Result { let request_id = RequestId::decode(buf, version)?; - if version == Version::Draft17 { - let _required_request_id_delta = buf.varint()?; - } // The token decodes but grants nothing: the session's grant is what authorizes the request. let (fetch_type, subscriber_priority, group_order, range_filters, fill_timeout, properties_wanted, epoch) = match version { - Version::Draft14 => { - let subscriber_priority = buf.u8()?; - let group_order = GroupOrder::decode(buf, version)?; - let fetch_type = FetchType::decode(buf, version)?; - super::token::single(Parameters::skip_request(buf)?)?; - ( - fetch_type, - Some(subscriber_priority), - Some(group_order), - false, - false, - true, - None, - ) - } - Version::Draft15 | Version::Draft16 | Version::Draft17 | Version::Draft18 | Version::Draft19 => { + Version::Draft18 | Version::Draft19 => { let fetch_type = FetchType::decode(buf, version)?; decode_params!(buf, version, 0x03 => authorization_token: Vec, - 0x0A => fill_timeout: Option where !matches!(version, Version::Draft15 | Version::Draft16 | Version::Draft17), + 0x0A => fill_timeout: Option, 0x20 => subscriber_priority: Option, 0x22 => group_order: Option, 0x25 => subgroup_filter: Vec where has_range_filters(version), 0x26 => object_id_filter: Vec where has_range_filters(version), 0x27 => priority_filter: Vec where has_range_filters(version), 0x28 => object_property_filter: Vec where has_range_filters(version), - epoch::EPOCH_PARAM => epoch: Option where epoch::supported(version), + epoch::EPOCH_PARAM => epoch: Option, ); super::token::single(authorization_token)?; let range_filters = [ @@ -325,150 +298,44 @@ impl Message for Fetch<'_> { #[derive(Debug, Clone, PartialEq, Eq)] pub struct FetchOk { - pub request_id: Option, - pub group_order: GroupOrder, pub end_of_track: bool, /// Drafts 14-19 use the last object plus one, or Object 0 for the whole End Group; /// draft 20 and later use an inclusive End Location. pub end_location: Location, - /// The track's properties, as SUBSCRIBE_OK carries them: MAX_CACHE_DURATION from the - /// draft-14/15 parameters, the Track Properties block from draft-16 on. + /// The track's properties, as SUBSCRIBE_OK carries them. pub properties: super::Properties, } impl Message for FetchOk { const ID: u64 = 0x18; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - self.request_id - .expect("request_id required for draft14-16") - .encode(w, version)?; - } else { - assert!(self.request_id.is_none(), "request_id must be None for draft17+"); - } - - match version { - Version::Draft14 => { - self.group_order.encode(w, version)?; - w.bool(self.end_of_track); - self.end_location.encode(w, version)?; - w.u8(0); // no parameters - } - _ => { - // GROUP_ORDER is not a legal FETCH_OK parameter in any draft after 14; the order - // of the response is whatever the FETCH asked for. - w.bool(self.end_of_track); - self.end_location.encode(w, version)?; - encode_params!(w, version,); - // Track Properties are the final field, so nothing may follow. - self.properties.encode(w, version)?; - } - } + // GROUP_ORDER is not a legal FETCH_OK parameter; the order of the response is + // whatever the FETCH asked for. + w.bool(self.end_of_track); + self.end_location.encode(w, version)?; + encode_params!(w, version,); + // Track Properties are the final field, so nothing may follow. + self.properties.encode(w, version)?; Ok(()) } fn decode_msg(buf: &mut Decoder<'_>, version: Version) -> Result { - let request_id = if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - Some(RequestId::decode(buf, version)?) - } else { - None - }; - - match version { - Version::Draft14 => { - let group_order = GroupOrder::decode(buf, version)?; - let end_of_track = buf.bool()?; - let end_location = Location::decode(buf, version)?; - let properties = super::Properties { - max_cache_duration: Parameters::skip_response(buf)?.map(std::time::Duration::from_millis), - ..Default::default() - }; - Ok(Self { - request_id, - group_order, - end_of_track, - end_location, - properties, - }) - } - _ => { - let end_of_track = buf.bool()?; - let end_location = Location::decode(buf, version)?; - // MAX_CACHE_DURATION and GROUP_ORDER are legal on FETCH_OK only in draft-15. - // Draft-16 still knows GROUP_ORDER, so one here is ignored. From draft-17 - // it closes the session. - decode_params!(buf, version, - 0x04 => max_cache_duration: Option where version == Version::Draft15, - 0x22 => group_order: Option where version == Version::Draft15, - ); - // The timescale is read but not surfaced yet: a fetched object without its - // own units arrives untimed. - let mut properties = super::Properties::decode(buf, version)?; - if version == Version::Draft15 { - properties.max_cache_duration = max_cache_duration.map(std::time::Duration::from_millis); - } - - let group_order = group_order.unwrap_or(GroupOrder::Descending); - - Ok(Self { - request_id, - group_order, - end_of_track, - end_location, - properties, - }) - } - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FetchError<'a> { - pub request_id: RequestId, - pub error_code: u64, - pub reason_phrase: Cow<'a, str>, -} - -impl Message for FetchError<'_> { - const ID: u64 = 0x19; + let end_of_track = buf.bool()?; + let end_location = Location::decode(buf, version)?; + // FETCH_OK lists no parameters, so any closes the session. + decode_params!(buf, version,); + // The timescale is read but not surfaced yet: a fetched object without its + // own units arrives untimed. + let properties = super::Properties::decode(buf, version)?; - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - self.request_id.encode(w, version)?; - w.varint(self.error_code)?; - w.string(&self.reason_phrase)?; - Ok(()) - } - - fn decode_msg(buf: &mut Decoder<'_>, version: Version) -> Result { - let request_id = RequestId::decode(buf, version)?; - let error_code = buf.varint()?; - let reason_phrase = Cow::Owned(buf.string()?); Ok(Self { - request_id, - error_code, - reason_phrase, + end_of_track, + end_location, + properties, }) } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FetchCancel { - pub request_id: RequestId, -} -impl Message for FetchCancel { - const ID: u64 = 0x17; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - self.request_id.encode(w, version)?; - Ok(()) - } - - fn decode_msg(buf: &mut Decoder<'_>, version: Version) -> Result { - let request_id = RequestId::decode(buf, version)?; - Ok(Self { request_id }) - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct FetchHeader { pub request_id: RequestId, @@ -708,178 +575,6 @@ mod tests { crate::coding::decode_buf(&mut buf, version, M::decode_msg) } - #[test] - fn test_fetch_v14_round_trip() { - let msg = Fetch { - request_id: RequestId(1), - subscriber_priority: 128, - group_order: GroupOrder::Descending, - fetch_type: FetchType::Standalone { - namespace: Path::new("test"), - track: "video".into(), - start: Location { group: 0, object: 0 }, - end: Location { group: 10, object: 5 }, - }, - range_filters: false, - fill_timeout: false, - properties_wanted: true, - epoch: None, - }; - - let encoded = encode_message(&msg, Version::Draft14); - let decoded: Fetch = decode_message(&encoded, Version::Draft14).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.subscriber_priority, 128); - } - - #[test] - fn test_fetch_v15_round_trip() { - let msg = Fetch { - request_id: RequestId(1), - subscriber_priority: 128, - group_order: GroupOrder::Descending, - fetch_type: FetchType::Standalone { - namespace: Path::new("test"), - track: "video".into(), - start: Location { group: 0, object: 0 }, - end: Location { group: 10, object: 5 }, - }, - range_filters: false, - fill_timeout: false, - properties_wanted: true, - epoch: None, - }; - - let encoded = encode_message(&msg, Version::Draft15); - let decoded: Fetch = decode_message(&encoded, Version::Draft15).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.subscriber_priority, 128); - } - - #[test] - fn test_fetch_ok_v14_round_trip() { - let msg = FetchOk { - request_id: Some(RequestId(2)), - group_order: GroupOrder::Descending, - end_of_track: false, - end_location: Location { group: 5, object: 3 }, - properties: Default::default(), - }; - - let encoded = encode_message(&msg, Version::Draft14); - let decoded: FetchOk = decode_message(&encoded, Version::Draft14).unwrap(); - - assert_eq!(decoded.request_id, Some(RequestId(2))); - assert!(!decoded.end_of_track); - assert_eq!(decoded.end_location, Location { group: 5, object: 3 }); - } - - #[test] - fn test_fetch_v16_round_trip() { - let msg = Fetch { - request_id: RequestId(1), - subscriber_priority: 128, - group_order: GroupOrder::Descending, - fetch_type: FetchType::Standalone { - namespace: Path::new("test"), - track: "video".into(), - start: Location { group: 0, object: 0 }, - end: Location { group: 10, object: 5 }, - }, - range_filters: false, - fill_timeout: false, - properties_wanted: true, - epoch: None, - }; - - let encoded = encode_message(&msg, Version::Draft16); - let decoded: Fetch = decode_message(&encoded, Version::Draft16).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.subscriber_priority, 128); - } - - #[test] - fn test_fetch_v17_round_trip() { - let msg = Fetch { - request_id: RequestId(1), - subscriber_priority: 128, - group_order: GroupOrder::Descending, - fetch_type: FetchType::Standalone { - namespace: Path::new("test"), - track: "video".into(), - start: Location { group: 0, object: 0 }, - end: Location { group: 10, object: 5 }, - }, - range_filters: false, - fill_timeout: false, - properties_wanted: true, - epoch: None, - }; - - let encoded = encode_message(&msg, Version::Draft17); - let decoded: Fetch = decode_message(&encoded, Version::Draft17).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.subscriber_priority, 128); - } - - #[test] - fn test_fetch_ok_v15_round_trip() { - let msg = FetchOk { - request_id: Some(RequestId(2)), - group_order: GroupOrder::Descending, - end_of_track: false, - end_location: Location { group: 5, object: 3 }, - properties: Default::default(), - }; - - let encoded = encode_message(&msg, Version::Draft15); - let decoded: FetchOk = decode_message(&encoded, Version::Draft15).unwrap(); - - assert_eq!(decoded.request_id, Some(RequestId(2))); - assert!(!decoded.end_of_track); - assert_eq!(decoded.end_location, Location { group: 5, object: 3 }); - } - - #[test] - fn test_fetch_ok_v16_round_trip() { - let msg = FetchOk { - request_id: Some(RequestId(2)), - group_order: GroupOrder::Descending, - end_of_track: false, - end_location: Location { group: 5, object: 3 }, - properties: Default::default(), - }; - - let encoded = encode_message(&msg, Version::Draft16); - let decoded: FetchOk = decode_message(&encoded, Version::Draft16).unwrap(); - - assert_eq!(decoded.request_id, Some(RequestId(2))); - assert!(!decoded.end_of_track); - assert_eq!(decoded.end_location, Location { group: 5, object: 3 }); - } - - #[test] - fn test_fetch_ok_v17_round_trip() { - let msg = FetchOk { - request_id: None, - group_order: GroupOrder::Descending, - end_of_track: false, - end_location: Location { group: 5, object: 3 }, - properties: Default::default(), - }; - - let encoded = encode_message(&msg, Version::Draft17); - let decoded: FetchOk = decode_message(&encoded, Version::Draft17).unwrap(); - - assert_eq!(decoded.request_id, None); - assert!(!decoded.end_of_track); - assert_eq!(decoded.end_location, Location { group: 5, object: 3 }); - } - #[test] fn test_fetch_v18_round_trip() { let msg = Fetch { @@ -908,8 +603,6 @@ mod tests { #[test] fn test_fetch_ok_v18_round_trip() { let msg = FetchOk { - request_id: None, - group_order: GroupOrder::Descending, end_of_track: false, end_location: Location { group: 5, object: 3 }, properties: Default::default(), @@ -918,19 +611,15 @@ mod tests { let encoded = encode_message(&msg, Version::Draft18); let decoded: FetchOk = decode_message(&encoded, Version::Draft18).unwrap(); - assert_eq!(decoded.request_id, None); assert!(!decoded.end_of_track); assert_eq!(decoded.end_location, Location { group: 5, object: 3 }); } - /// GROUP_ORDER (0x22) has never been a legal FETCH_OK parameter outside draft-14, where - /// it was a plain field. A draft-15+ peer closes the session with PROTOCOL_VIOLATION when - /// it sees one, so the response carries no parameters at all. + /// GROUP_ORDER (0x22) is not a legal FETCH_OK parameter; a peer closes the session with + /// PROTOCOL_VIOLATION when it sees one, so the response carries no parameters at all. #[test] fn test_fetch_ok_v18_omits_group_order() { let msg = FetchOk { - request_id: None, - group_order: GroupOrder::Descending, end_of_track: false, end_location: Location { group: 5, object: 3 }, properties: Default::default(), @@ -1056,8 +745,7 @@ mod tests { ); } - /// FILL_TIMEOUT arrived in draft-18 and the Range Filters in draft-19; each is still - /// an unknown parameter before then. + /// The Range Filters arrived in draft-19, so they are still unknown parameters before it. #[test] fn test_fetch_parameters_follow_their_draft() { #[rustfmt::skip] @@ -1071,8 +759,7 @@ mod tests { let range_filter = joining(&[0x01, 0x25, 0x00]); for (version, body, ok) in [ - (Version::Draft15, &token, true), - (Version::Draft16, &fill_timeout, false), + (Version::Draft18, &token, true), (Version::Draft18, &fill_timeout, true), (Version::Draft18, &range_filter, false), (Version::Draft19, &range_filter, true), @@ -1095,7 +782,7 @@ mod tests { 0x02, 0x03, 0x00, // Relative Joining: subscription 3, offset 0 0x00, // no parameters ]; - let fetch = decode_message::(&body, Version::Draft16).unwrap(); + let fetch = decode_message::(&body, Version::Draft18).unwrap(); assert_eq!(fetch.subscriber_priority, 128); } } @@ -1238,10 +925,6 @@ mod cache_duration_tests { #[test] fn max_cache_duration_is_read_in_each_drafts_field() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -1249,12 +932,7 @@ mod cache_duration_tests { Version::Draft22, ] { for age in [0u64, 30_000] { - let mut payload = match version { - Version::Draft14 => vec![0, 1, 0, 0, 0, 1, 4], - Version::Draft15 => vec![0, 0, 0, 0, 1, 4], - Version::Draft16 => vec![0, 0, 0, 0, 0, 4], - _ => vec![0, 0, 0, 0, 4], - }; + let mut payload = vec![0, 0, 0, 0, 4]; Encoder::new(&mut payload, version.into()).varint(age).unwrap(); let got = FetchOk::decode_msg(&mut Decoder::new(&payload, version.into()), version).unwrap(); assert_eq!( diff --git a/rs/moq-net/src/ietf/filter.rs b/rs/moq-net/src/ietf/filter.rs index 4a4d09a47f6..9e67b79356f 100644 --- a/rs/moq-net/src/ietf/filter.rs +++ b/rs/moq-net/src/ietf/filter.rs @@ -77,11 +77,7 @@ impl Filter { pub(crate) fn is_draft20(version: Version) -> bool { !matches!( version, - Version::Draft14 - | Version::Draft15 - | Version::Draft16 - | Version::Draft17 - | Version::Draft18 + Version::Draft18 | Version::Draft19 ) } @@ -293,19 +289,12 @@ impl Param for Filter { fn param_encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { // Inner varints use the draft-15 leading-ones encoding on the drafts that predate // the switch, matching the other length-prefixed parameters. - let sv = match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Version::Draft15, - _ => version, - }; + let sv = version; let mut buf = Vec::new(); let mut inner = Encoder::new(&mut buf, sv.into()); match version { - Version::Draft14 - | Version::Draft15 - | Version::Draft16 - | Version::Draft17 - | Version::Draft18 + Version::Draft18 | Version::Draft19 => self.encode_tag(&mut inner, sv)?, Version::Draft20 | Version::Draft21 => self.encode_fields(&mut inner)?, // Draft-22 drops the Length, since the Location Filter Type says which fields follow. @@ -317,8 +306,7 @@ impl Param for Filter { fn param_decode(r: &mut Decoder<'_>, version: Version) -> Result { let sv = match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Version::Draft15, - Version::Draft17 | Version::Draft18 | Version::Draft19 => version, + Version::Draft18 | Version::Draft19 => version, // The field count is what carries the meaning, so the value is consumed whole. Version::Draft20 | Version::Draft21 => { return Self::decode_fields(&mut Decoder::new(r.bytes()?, version.into())); diff --git a/rs/moq-net/src/ietf/flate.rs b/rs/moq-net/src/ietf/flate.rs index 1c4ac1c833f..71aa7b38121 100644 --- a/rs/moq-net/src/ietf/flate.rs +++ b/rs/moq-net/src/ietf/flate.rs @@ -11,20 +11,11 @@ pub(super) const COMPRESSION: u64 = 0x40B6C; /// Inflated Size object property, in the MOQ Properties registry. pub(super) const INFLATED: u64 = 0x40B6E; -pub(crate) fn supported(version: Version) -> bool { - !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) +pub(crate) fn into_setup(parameters: &mut Parameters) { + parameters.set_varint(ParameterVarInt::Flate, 1); } -pub(crate) fn into_setup(parameters: &mut Parameters, version: Version) { - if supported(version) { - parameters.set_varint(ParameterVarInt::Flate, 1); - } -} - -pub(crate) fn from_setup(parameters: &Parameters, version: Version) -> Result { - if !supported(version) { - return Ok(false); - } +pub(crate) fn from_setup(parameters: &Parameters) -> Result { match parameters.get_varint(ParameterVarInt::Flate) { None => Ok(false), Some(1) => Ok(true), @@ -63,36 +54,24 @@ mod tests { use super::*; #[test] - fn negotiation_requires_an_offer_on_draft17_or_later() { + fn negotiation_requires_an_offer() { for version in crate::Versions::all().iter().filter_map(|v| Version::try_from(*v).ok()) { let mut parameters = Parameters::default(); - assert!(!from_setup(¶meters, version).unwrap()); - into_setup(&mut parameters, version); - assert_eq!(from_setup(¶meters, version).unwrap(), supported(version)); + assert!(!from_setup(¶meters).unwrap()); + into_setup(&mut parameters); + assert!(from_setup(¶meters).unwrap()); use crate::coding::{Decode, Encode}; let encoded = parameters.encode_bytes(version).unwrap(); let (decoded, consumed) = Parameters::decode_slice(&encoded, version).unwrap(); assert_eq!(consumed, encoded.len()); - assert_eq!(from_setup(&decoded, version).unwrap(), supported(version)); + assert!(from_setup(&decoded).unwrap()); for invalid in [0, 2, u64::MAX >> 2] { parameters.set_varint(ParameterVarInt::Flate, invalid); - assert_eq!(from_setup(¶meters, version).is_err(), supported(version)); + assert!(from_setup(¶meters).is_err()); } } } - #[test] - fn legacy_setup_is_unchanged() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { - let mut parameters = Parameters::default(); - into_setup(&mut parameters, version); - assert_eq!(parameters.get_varint(ParameterVarInt::Flate), None); - } - let mut parameters = Parameters::default(); - into_setup(&mut parameters, Version::Draft17); - assert_eq!(parameters.get_varint(ParameterVarInt::Flate), Some(1)); - } - #[test] fn setup_code_point_is_even_and_not_grease() { assert_eq!(OPTION % 2, 0); diff --git a/rs/moq-net/src/ietf/goaway.rs b/rs/moq-net/src/ietf/goaway.rs index 1c4c11c914c..7a278a10d6a 100644 --- a/rs/moq-net/src/ietf/goaway.rs +++ b/rs/moq-net/src/ietf/goaway.rs @@ -1,4 +1,4 @@ -//! IETF moq-transport-14 goaway message +//! The moq-transport GOAWAY message. use std::borrow::Cow; @@ -12,7 +12,7 @@ use super::Version; #[derive(Clone, Debug)] pub struct GoAway<'a> { pub new_session_uri: Cow<'a, str>, - /// Draft-17: timeout in milliseconds before closing the session + /// Timeout in milliseconds before closing the session. pub timeout: u64, /// Draft-18 only: the smallest of the receiver's Request IDs the sender has not /// processed. Required on the control stream and absent on a request stream. @@ -24,10 +24,7 @@ impl Message for GoAway<'_> { fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { w.string(&self.new_session_uri)?; - // Draft-17+ adds a timeout field. - if !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - w.varint(self.timeout)?; - } + w.varint(self.timeout)?; // Draft-18 (#1559) added the Request ID; draft-19 removed it again (#1623). if version == Version::Draft18 && let Some(request_id) = self.request_id @@ -45,7 +42,6 @@ impl Message for GoAway<'_> { return Err(DecodeError::InvalidValue); } let (timeout, request_id) = match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => (0, None), // The Request ID is present only on the control stream; each caller checks it // against the stream it read from. Version::Draft18 => { @@ -72,14 +68,14 @@ mod tests { fn encode_message(msg: &M) -> Vec { let mut buf = Vec::new(); - msg.encode_msg(&mut Encoder::new(&mut buf, Version::Draft14.into()), Version::Draft14) + msg.encode_msg(&mut Encoder::new(&mut buf, Version::Draft20.into()), Version::Draft20) .unwrap(); buf.to_vec() } fn decode_message(bytes: &[u8]) -> Result { let mut buf = bytes::Bytes::from(bytes.to_vec()); - crate::coding::decode_buf(&mut buf, Version::Draft14, M::decode_msg) + crate::coding::decode_buf(&mut buf, Version::Draft20, M::decode_msg) } #[test] @@ -110,25 +106,6 @@ mod tests { assert_eq!(decoded.new_session_uri, ""); } - #[test] - fn test_goaway_v17_timeout() { - let msg = GoAway { - new_session_uri: "https://example.com/new".into(), - timeout: 5000, - request_id: None, - }; - - let mut buf = Vec::new(); - msg.encode_msg(&mut Encoder::new(&mut buf, Version::Draft17.into()), Version::Draft17) - .unwrap(); - - let mut bytes = bytes::Bytes::from(buf.to_vec()); - let decoded: GoAway = crate::coding::decode_buf(&mut bytes, Version::Draft17, GoAway::decode_msg).unwrap(); - - assert_eq!(decoded.new_session_uri, "https://example.com/new"); - assert_eq!(decoded.timeout, 5000); - } - #[test] fn test_goaway_v18_timeout() { let msg = GoAway { @@ -142,11 +119,11 @@ mod tests { .unwrap(); // Draft-18 requires a trailing Request ID on the control stream, so the - // v18 body must be exactly one varint longer than the v17 body. - let mut buf17 = Vec::new(); - msg.encode_msg(&mut Encoder::new(&mut buf17, Version::Draft17.into()), Version::Draft17) + // v18 body must be exactly one varint longer than the v19 body. + let mut buf19 = Vec::new(); + msg.encode_msg(&mut Encoder::new(&mut buf19, Version::Draft19.into()), Version::Draft19) .unwrap(); - assert_eq!(buf.len(), buf17.len() + 1, "v18 must append the Request ID varint"); + assert_eq!(buf.len(), buf19.len() + 1, "v18 must append the Request ID varint"); let mut bytes = bytes::Bytes::from(buf.to_vec()); let decoded: GoAway = crate::coding::decode_buf(&mut bytes, Version::Draft18, GoAway::decode_msg).unwrap(); diff --git a/rs/moq-net/src/ietf/group.rs b/rs/moq-net/src/ietf/group.rs index 5fc762771af..14231136233 100644 --- a/rs/moq-net/src/ietf/group.rs +++ b/rs/moq-net/src/ietf/group.rs @@ -37,8 +37,7 @@ impl Decode for ObjectExtensionsLength { /// Encode a frame's presentation timestamp as a moq-transport Object Property. /// /// Matches the LOC encoding of the same registry id so a relay or LOC-aware peer -/// reads the same bytes on drafts that delta-encode KVP type ids. The Timestamp -/// value is always absolute. Writes the raw KVP bytes +/// reads the same bytes. The Timestamp value is always absolute. Writes the raw KVP bytes /// (no outer length prefix); the caller frames the block with its byte length. /// /// The units come from the track's TIMESCALE property, not from here: repeating the @@ -49,22 +48,14 @@ pub fn encode_object_time( w: &mut Encoder<'_>, timestamp: Timestamp, timescale: Timescale, - version: Version, ) -> Result<(), EncodeError> { let timestamp = timestamp.convert(timescale).map_err(|_| EncodeError::BoundsExceeded)?; - encode_object_property_type(w, PROP_TIMESTAMP, 0, version)?; + // The first property's type is a delta from zero, so it is written as is. + w.varint(PROP_TIMESTAMP)?; w.varint(timestamp.value())?; Ok(()) } -fn encode_object_property_type(w: &mut Encoder<'_>, kind: u64, prev: u64, version: Version) -> Result<(), EncodeError> { - let encoded = match version { - Version::Draft14 | Version::Draft15 => kind, - _ => kind.checked_sub(prev).ok_or(EncodeError::BoundsExceeded)?, - }; - w.varint(encoded) -} - /// Decode the same property list as ingest when checking timestamps in wire tests. #[cfg(test)] pub fn decode_object_time( @@ -138,9 +129,6 @@ impl Param for GroupOrder { fn param_decode(r: &mut Decoder<'_>, version: Version) -> Result { let v = u8::param_decode(r, version)?; match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Ok(GroupOrder::try_from(v) - .unwrap_or(GroupOrder::Descending) - .any_to_descending()), _ => match v { 1 => Ok(GroupOrder::Ascending), 2 => Ok(GroupOrder::Descending), @@ -189,7 +177,7 @@ impl GroupFlags { // moq-lite always sets this bit on emit because every subgroup starts at object 0. pub const FIRST_OBJECT_BIT: u64 = 0x40; - pub fn encode(&self, version: Version) -> Result { + pub fn encode(&self) -> Result { if self.has_subgroup && self.has_subgroup_object { return Err(EncodeError::InvalidState); } @@ -214,27 +202,18 @@ impl GroupFlags { } // Draft-18+: moq-lite always starts subgroups at object 0 and never has gaps, so // the publisher side sets this on everything it produces. - if self.first_object - && !matches!( - version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 - ) { + if self.first_object { id |= Self::FIRST_OBJECT_BIT; } Ok(id) } - pub fn decode(id: u64, version: Version) -> Result { - // Draft-18+ allows bit 0x40 (FIRST_OBJECT), which says the stream carries the - // subgroup from its first published object. Strip it before the range check, but - // keep the value: it is the only signal that a subgroup starts partway through. - // The drafts that predate it carry no such signal, so they are taken at their word. - let legacy = matches!( - version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 - ); - let first_object = legacy || (id & Self::FIRST_OBJECT_BIT) != 0; - let id = if legacy { id } else { id & !Self::FIRST_OBJECT_BIT }; + pub fn decode(id: u64) -> Result { + // Bit 0x40 (FIRST_OBJECT) says the stream carries the subgroup from its first + // published object. Strip it before the range check, but keep the value: it is the + // only signal that a subgroup starts partway through. + let first_object = (id & Self::FIRST_OBJECT_BIT) != 0; + let id = id & !Self::FIRST_OBJECT_BIT; let (has_priority, base_id) = if (Self::START..=Self::END).contains(&id) { (true, id) @@ -287,9 +266,9 @@ pub struct GroupHeader { } impl Encode for GroupHeader { - fn encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { + fn encode(&self, w: &mut Encoder<'_>, _: Version) -> Result<(), EncodeError> { tracing::trace!(?self, "encoding group header"); - w.varint(self.flags.encode(version)?)?; + w.varint(self.flags.encode()?)?; w.varint(self.track_alias)?; w.varint(self.group_id)?; @@ -310,8 +289,8 @@ impl Encode for GroupHeader { } impl Decode for GroupHeader { - fn decode(r: &mut Decoder<'_>, version: Version) -> Result { - let flags = GroupFlags::decode(r.varint()?, version)?; + fn decode(r: &mut Decoder<'_>, _: Version) -> Result { + let flags = GroupFlags::decode(r.varint()?)?; let track_alias = r.varint()?; let group_id = r.varint()?; @@ -346,7 +325,7 @@ mod tests { /// Encode an object's properties at `version`. fn encode_time(ts: Timestamp, timescale: Timescale, version: Version) -> Vec { let mut buf = Vec::new(); - encode_object_time(&mut Encoder::new(&mut buf, version.into()), ts, timescale, version).unwrap(); + encode_object_time(&mut Encoder::new(&mut buf, version.into()), ts, timescale).unwrap(); buf } @@ -451,150 +430,130 @@ mod tests { #[test] fn test_group_flags_spec_table() { // Type 0x10: No subgroup field, Subgroup ID = 0, No extensions, No end - let flags = GroupFlags::decode(0x10, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x10).unwrap(); assert!(!flags.has_subgroup); assert!(!flags.has_subgroup_object); assert!(!flags.has_extensions); assert!(!flags.has_end); assert!(flags.has_priority); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x10); + assert_eq!(flags.encode().unwrap(), 0x10); // Type 0x11: No subgroup field, Subgroup ID = 0, Extensions, No end - let flags = GroupFlags::decode(0x11, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x11).unwrap(); assert!(!flags.has_subgroup); assert!(!flags.has_subgroup_object); assert!(flags.has_extensions); assert!(!flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x11); + assert_eq!(flags.encode().unwrap(), 0x11); // Type 0x12: No subgroup field, Subgroup ID = First Object ID, No extensions, No end - let flags = GroupFlags::decode(0x12, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x12).unwrap(); assert!(!flags.has_subgroup); assert!(flags.has_subgroup_object); assert!(!flags.has_extensions); assert!(!flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x12); + assert_eq!(flags.encode().unwrap(), 0x12); // Type 0x13: No subgroup field, Subgroup ID = First Object ID, Extensions, No end - let flags = GroupFlags::decode(0x13, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x13).unwrap(); assert!(!flags.has_subgroup); assert!(flags.has_subgroup_object); assert!(flags.has_extensions); assert!(!flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x13); + assert_eq!(flags.encode().unwrap(), 0x13); // Type 0x14: Subgroup field present, No extensions, No end - let flags = GroupFlags::decode(0x14, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x14).unwrap(); assert!(flags.has_subgroup); assert!(!flags.has_subgroup_object); assert!(!flags.has_extensions); assert!(!flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x14); + assert_eq!(flags.encode().unwrap(), 0x14); // Type 0x15: Subgroup field present, Extensions, No end - let flags = GroupFlags::decode(0x15, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x15).unwrap(); assert!(flags.has_subgroup); assert!(!flags.has_subgroup_object); assert!(flags.has_extensions); assert!(!flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x15); + assert_eq!(flags.encode().unwrap(), 0x15); // Type 0x18: No subgroup field, Subgroup ID = 0, No extensions, End of group - let flags = GroupFlags::decode(0x18, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x18).unwrap(); assert!(!flags.has_subgroup); assert!(!flags.has_subgroup_object); assert!(!flags.has_extensions); assert!(flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x18); + assert_eq!(flags.encode().unwrap(), 0x18); // Type 0x19: No subgroup field, Subgroup ID = 0, Extensions, End of group - let flags = GroupFlags::decode(0x19, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x19).unwrap(); assert!(!flags.has_subgroup); assert!(!flags.has_subgroup_object); assert!(flags.has_extensions); assert!(flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x19); + assert_eq!(flags.encode().unwrap(), 0x19); // Type 0x1A: No subgroup field, Subgroup ID = First Object ID, No extensions, End of group - let flags = GroupFlags::decode(0x1A, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x1A).unwrap(); assert!(!flags.has_subgroup); assert!(flags.has_subgroup_object); assert!(!flags.has_extensions); assert!(flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x1A); + assert_eq!(flags.encode().unwrap(), 0x1A); // Type 0x1B: No subgroup field, Subgroup ID = First Object ID, Extensions, End of group - let flags = GroupFlags::decode(0x1B, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x1B).unwrap(); assert!(!flags.has_subgroup); assert!(flags.has_subgroup_object); assert!(flags.has_extensions); assert!(flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x1B); + assert_eq!(flags.encode().unwrap(), 0x1B); // Type 0x1C: Subgroup field present, No extensions, End of group - let flags = GroupFlags::decode(0x1C, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x1C).unwrap(); assert!(flags.has_subgroup); assert!(!flags.has_subgroup_object); assert!(!flags.has_extensions); assert!(flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x1C); + assert_eq!(flags.encode().unwrap(), 0x1C); // Type 0x1D: Subgroup field present, Extensions, End of group - let flags = GroupFlags::decode(0x1D, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x1D).unwrap(); assert!(flags.has_subgroup); assert!(!flags.has_subgroup_object); assert!(flags.has_extensions); assert!(flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x1D); + assert_eq!(flags.encode().unwrap(), 0x1D); // Invalid: Both has_subgroup and has_subgroup_object (would be 0x16) - assert!(GroupFlags::decode(0x16, Version::Draft14).is_err()); + assert!(GroupFlags::decode(0x16).is_err()); } #[test] fn test_group_flags_no_priority_range() { // v15: 0x30 range = same flags as 0x10 range but no priority - let flags = GroupFlags::decode(0x30, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x30).unwrap(); assert!(!flags.has_priority); assert!(!flags.has_subgroup); assert!(!flags.has_extensions); assert!(!flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x30); + assert_eq!(flags.encode().unwrap(), 0x30); - let flags = GroupFlags::decode(0x38, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x38).unwrap(); assert!(!flags.has_priority); assert!(flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x38); + assert_eq!(flags.encode().unwrap(), 0x38); - let flags = GroupFlags::decode(0x3D, Version::Draft14).unwrap(); + let flags = GroupFlags::decode(0x3D).unwrap(); assert!(!flags.has_priority); assert!(flags.has_subgroup); assert!(flags.has_extensions); assert!(flags.has_end); - assert_eq!(flags.encode(Version::Draft14).unwrap(), 0x3D); + assert_eq!(flags.encode().unwrap(), 0x3D); // Invalid: Both has_subgroup and has_subgroup_object in no-priority range - assert!(GroupFlags::decode(0x36, Version::Draft14).is_err()); - } - - /// Draft-18 introduces the FIRST_OBJECT bit (0x40) per spec §11.4.2. - /// Plain groups set it; relayed groups preserve the publisher's claim. - #[test] - fn test_first_object_bit_draft18() { - // Encoding sets bit 0x40 for default flags. - let flags = GroupFlags::default(); - let encoded = flags.encode(Version::Draft18).unwrap(); - assert_eq!(encoded & GroupFlags::FIRST_OBJECT_BIT, GroupFlags::FIRST_OBJECT_BIT); - // The base value is what draft-17 would have produced. - let v17 = flags.encode(Version::Draft17).unwrap(); - assert_eq!(encoded, v17 | GroupFlags::FIRST_OBJECT_BIT); - - // Decoding accepts and discards the bit. - let decoded = GroupFlags::decode(v17 | GroupFlags::FIRST_OBJECT_BIT, Version::Draft18).unwrap(); - assert_eq!(decoded, flags); - - // Draft-17 rejects the FIRST_OBJECT bit (it's outside the 0x10-0x1d / 0x30-0x3d ranges). - assert!(GroupFlags::decode(v17 | GroupFlags::FIRST_OBJECT_BIT, Version::Draft17).is_err()); + assert!(GroupFlags::decode(0x36).is_err()); } /// FIRST_OBJECT says the stream carries the subgroup from its first published object. @@ -608,13 +567,13 @@ mod tests { first_object, ..Default::default() }; - let encoded = flags.encode(version).unwrap(); + let encoded = flags.encode().unwrap(); assert_eq!( (encoded & GroupFlags::FIRST_OBJECT_BIT) != 0, first_object, "{version} {first_object}" ); - assert_eq!(GroupFlags::decode(encoded, version).unwrap().first_object, first_object); + assert_eq!(GroupFlags::decode(encoded).unwrap().first_object, first_object); } } } @@ -623,14 +582,14 @@ mod tests { /// is taken at its word rather than read as starting partway through. #[test] fn older_drafts_have_no_first_object_signal() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16, Version::Draft17] { + for version in [Version::Draft15] { let flags = GroupFlags { first_object: false, ..Default::default() }; - let encoded = flags.encode(version).unwrap(); + let encoded = flags.encode().unwrap(); assert_eq!(encoded & GroupFlags::FIRST_OBJECT_BIT, 0, "{version}"); - assert!(GroupFlags::decode(encoded, version).unwrap().first_object, "{version}"); + assert!(GroupFlags::decode(encoded).unwrap().first_object, "{version}"); } } @@ -649,10 +608,10 @@ mod tests { first_object: true, }, ] { - let v18 = flags.encode(Version::Draft18).unwrap(); - let v19 = flags.encode(Version::Draft19).unwrap(); + let v18 = flags.encode().unwrap(); + let v19 = flags.encode().unwrap(); assert_eq!(v18, v19, "draft-19 must encode the subgroup header like draft-18"); - assert_eq!(GroupFlags::decode(v19, Version::Draft19).unwrap(), flags); + assert_eq!(GroupFlags::decode(v19).unwrap(), flags); } } @@ -660,14 +619,14 @@ mod tests { #[test] fn test_draft18_extended_range() { // 0x70 = 0x30 (no-priority, no flags) + 0x40 (FIRST_OBJECT) - let flags = GroupFlags::decode(0x70, Version::Draft18).unwrap(); + let flags = GroupFlags::decode(0x70).unwrap(); assert!(!flags.has_priority); assert!(!flags.has_subgroup); assert!(!flags.has_extensions); assert!(!flags.has_end); // 0x7D = 0x3D + 0x40 - let flags = GroupFlags::decode(0x7D, Version::Draft18).unwrap(); + let flags = GroupFlags::decode(0x7D).unwrap(); assert!(!flags.has_priority); assert!(flags.has_subgroup); assert!(flags.has_extensions); @@ -701,7 +660,7 @@ mod tests { ); // The check in session.rs::UniType::classify. assert!( - GroupFlags::decode(type_byte, Version::Draft18).is_ok(), + GroupFlags::decode(type_byte).is_ok(), "draft-18 SUBGROUP_HEADER type 0x{type_byte:02x} not recognized by uni-stream classifier", ); } diff --git a/rs/moq-net/src/ietf/hidden.rs b/rs/moq-net/src/ietf/hidden.rs index 388334a516d..7827926f989 100644 --- a/rs/moq-net/src/ietf/hidden.rs +++ b/rs/moq-net/src/ietf/hidden.rs @@ -34,7 +34,7 @@ mod tests { #[test] fn every_version_round_trips() { - for version in [Version::Draft14, Version::Draft16, Version::Draft17, Version::Draft22] { + for version in [Version::Draft22] { let mut params = super::super::Parameters::default(); assert!(!from_setup(¶ms, version)); into_setup(&mut params, version); diff --git a/rs/moq-net/src/ietf/max_age.rs b/rs/moq-net/src/ietf/max_age.rs index 672c0850bb9..a95331fc3fc 100644 --- a/rs/moq-net/src/ietf/max_age.rs +++ b/rs/moq-net/src/ietf/max_age.rs @@ -30,7 +30,7 @@ mod tests { use super::*; #[test] fn negotiated_separately_and_bounded() { - for version in [Version::Draft14, Version::Draft16, Version::Draft22] { + for version in [Version::Draft22] { let mut params = Parameters::default(); assert!(!from_setup(¶ms, version)); into_setup(&mut params, version); diff --git a/rs/moq-net/src/ietf/message.rs b/rs/moq-net/src/ietf/message.rs index 0ff229f5ceb..1c89c2eada1 100644 --- a/rs/moq-net/src/ietf/message.rs +++ b/rs/moq-net/src/ietf/message.rs @@ -2,37 +2,14 @@ use crate::coding::{Decode, DecodeError, Decoder, Encode, EncodeError, Encoder}; use super::Version; -pub(super) fn control(id: u64, version: Version) -> crate::stats::Control { +pub(super) fn control(id: u64) -> crate::stats::Control { use super::*; use crate::stats::Control; - // Draft 14 reuses later drafts' NAMESPACE_DONE type for TRACK_STATUS_OK. - if version == Version::Draft14 && (id == TrackStatusOk::ID_14 || id == TRACK_STATUS_ERROR_14) { - return Control::TrackInfo; - } - // Before draft 16, 0x02 updates only a subscription; later it is the shared REQUEST_UPDATE. - if matches!(version, Version::Draft14 | Version::Draft15) && id == SubscribeUpdate::ID { - return Control::Subscribe; - } match id { - PublishNamespace::ID - | PublishNamespaceError::ID - | PublishNamespaceDone::ID - | PublishNamespaceCancel::ID - | SubscribeNamespace::ID - | SubscribeNamespaceLegacy::ID - | SubscribeNamespaceOk::ID - | SubscribeNamespaceError::ID - | UnsubscribeNamespace::ID - | NamespaceDone::ID => Control::Announce, + PublishNamespace::ID | SubscribeNamespace::ID | Namespace::ID | NamespaceDone::ID => Control::Announce, TrackStatus::ID | SubscribeTracks::ID | PublishSkipped::ID => Control::TrackInfo, - Subscribe::ID - | SubscribeOk::ID - | Unsubscribe::ID - | Publish::ID - | PublishOk::ID - | PublishError::ID - | PublishDone::ID => Control::Subscribe, - Fetch::ID | FetchOk::ID | FetchError::ID | FetchCancel::ID => Control::Fetch, + Subscribe::ID | SubscribeOk::ID | Publish::ID | PublishOk::ID | PublishDone::ID => Control::Subscribe, + Fetch::ID | FetchOk::ID => Control::Fetch, auth::Auth::ID | auth::AuthOk::ID | auth::AuthError::ID => Control::Auth, GoAway::ID => Control::Goaway, _ => Control::Other, @@ -58,7 +35,7 @@ impl Encode for T { let prefix = w.prefix_u16(); self.encode_msg(w, version)?; w.fill(prefix)?; - w.control = Some(control(T::ID, version)); + w.control = Some(control(T::ID)); Ok(()) } } @@ -112,7 +89,7 @@ impl Decode for T { Err(err) => tracing::warn!(%err, "decode failed"), } if result.is_ok() { - r.control = Some(control(T::ID, version)); + r.control = Some(control(T::ID)); } result } @@ -123,23 +100,11 @@ mod tests { use super::*; use crate::stats::Control; - /// Draft 14's track-status replies reuse type IDs later drafts give other classes. #[test] - fn draft14_track_status_ids_are_track_info() { - for id in [super::super::TrackStatusOk::ID_14, super::super::TRACK_STATUS_ERROR_14] { - assert_eq!(control(id, Version::Draft14), Control::TrackInfo); - } - assert_eq!( - control(super::super::NamespaceDone::ID, Version::Draft16), - Control::Announce - ); - assert_eq!( - control(super::super::SubscribeUpdate::ID, Version::Draft15), - Control::Subscribe - ); - // Shared replies and updates identify no request class. + fn shared_replies_identify_no_request_class() { + assert_eq!(control(super::super::NamespaceDone::ID), Control::Announce); for id in [super::super::RequestOk::ID, super::super::SubscribeUpdate::ID] { - assert_eq!(control(id, Version::Draft16), Control::Other); + assert_eq!(control(id), Control::Other); } } } diff --git a/rs/moq-net/src/ietf/mod.rs b/rs/moq-net/src/ietf/mod.rs index 66635b40bf4..ee59598afee 100644 --- a/rs/moq-net/src/ietf/mod.rs +++ b/rs/moq-net/src/ietf/mod.rs @@ -16,8 +16,6 @@ #[macro_use] mod parameters; pub mod active_count; -#[cfg(feature = "async")] -mod adapter; pub(crate) mod auth; pub mod cluster; #[cfg(feature = "async")] @@ -61,8 +59,6 @@ mod version; #[cfg(feature = "async")] use control::Control; -#[cfg(feature = "async")] -pub(crate) use control::initial_max_request_id; pub use datagram::*; pub use fetch::*; pub use filter::*; @@ -87,19 +83,9 @@ use subscriber::*; pub use track::*; pub use version::Version; -#[cfg(all(feature = "async", any(test, feature = "fuzz")))] -pub(crate) fn request_ids(window: u64, ids: &[u64]) -> usize { - let control = Control::new(None, false).with_window(window, false, Version::Draft16); - let held: Vec<_> = ids - .iter() - .filter_map(|id| control.accept(RequestId(*id)).ok()) - .collect(); - held.len() -} - #[cfg(all(feature = "async", any(test, feature = "fuzz")))] pub(crate) fn request_history(ids: &[u64]) -> usize { - let control = Control::new(None, false); + let control = Control::new(false); ids.iter().filter(|id| control.accept(RequestId(**id)).is_ok()).count() } diff --git a/rs/moq-net/src/ietf/namespace.rs b/rs/moq-net/src/ietf/namespace.rs index 4d89af79d81..b5641842472 100644 --- a/rs/moq-net/src/ietf/namespace.rs +++ b/rs/moq-net/src/ietf/namespace.rs @@ -117,11 +117,7 @@ pub fn decode_track(r: &mut Decoder<'_>) -> Result<(Path<'static>, String), Deco pub(super) fn refusal(namespace: &Path, version: super::Version) -> Option> { if matches!( version, - super::Version::Draft14 - | super::Version::Draft15 - | super::Version::Draft16 - | super::Version::Draft17 - | super::Version::Draft18 + super::Version::Draft18 | super::Version::Draft19 | super::Version::Draft20 ) { @@ -133,7 +129,6 @@ pub(super) fn refusal(namespace: &Path, version: super::Version) -> Option Vec { encode_path(&Path::from(path.to_string())) diff --git a/rs/moq-net/src/ietf/object.rs b/rs/moq-net/src/ietf/object.rs index eb74e055509..78d23c5f063 100644 --- a/rs/moq-net/src/ietf/object.rs +++ b/rs/moq-net/src/ietf/object.rs @@ -3,43 +3,26 @@ use bytes::Bytes; use super::Version; use crate::{ Error, Timescale, Timestamp, - coding::{Decoder, Encoder, Form}, + coding::{Decoder, Encoder}, model::object::{Properties, Property, Value}, }; -/// Whether `version` serializes property IDs as they are, rather than as deltas. -fn absolute(version: Version) -> bool { - matches!(version, Version::Draft14 | Version::Draft15) -} - -/// Whether lists from drafts `a` and `b` are the same bytes: the same keys and varints. -pub(crate) fn same_bytes(a: Version, b: Version) -> bool { - let form = |version| (absolute(version), Form::from(version)); - form(a) == form(b) -} - /// Draft-19 assigns property 0x06 to SUBGROUP_DELIVERY_TIMEOUT. pub(crate) fn has_delivery_timeout(version: Version) -> bool { !matches!( version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 | Version::Draft18 + Version::Draft18 ) } -/// Object status: the object does not exist, defined by drafts 14 and 15 only. -pub(crate) const DOES_NOT_EXIST: u64 = 0x1; /// Object status: no object at or past this location in the group exists. pub(crate) const END_OF_GROUP: u64 = 0x3; /// Object status: no object at or past this location exists (every implemented draft). pub(crate) const END_OF_TRACK: u64 = 0x4; -/// Whether `version` defines the Object Status `status`, 0 being Normal. -pub(crate) fn status_defined(status: u64, version: Version) -> bool { - match status { - 0 | END_OF_GROUP | END_OF_TRACK => true, - DOES_NOT_EXIST => matches!(version, Version::Draft14 | Version::Draft15), - _ => false, - } +/// Whether the Object Status `status` is defined, 0 being Normal. +pub(crate) fn status_defined(status: u64) -> bool { + matches!(status, 0 | END_OF_GROUP | END_OF_TRACK) } /// Refuse the properties of an object with a frame on a track in `timescale` if they @@ -52,16 +35,10 @@ pub(crate) fn check_frame(properties: &Properties, timescale: Option) } } -/// Only draft-14 lets a status object, such as END_OF_GROUP, carry properties. -#[cfg(feature = "async")] -pub(crate) fn status_has_properties(version: Version) -> bool { - version == Version::Draft14 -} - -/// Refuse properties on a status object where its draft forbids them. +/// Refuse properties on a status object, such as END_OF_GROUP, which may not carry any. #[cfg(feature = "async")] -pub(super) fn check_status(version: Version, properties: &Properties) -> Result<(), Error> { - match status_has_properties(version) || properties.is_empty() { +pub(super) fn check_status(properties: &Properties) -> Result<(), Error> { + match properties.is_empty() { true => Ok(()), false => Err(Error::Session(crate::SessionError::ProtocolViolation)), } @@ -88,7 +65,7 @@ pub(crate) fn max_block(timescale: Option) -> usize { } // A Timestamp's value is a QUIC varint at most, and its key, a delta of at most 0x10, // only shrinks the key of the property after it. - let largest = [Version::Draft14, Version::Draft16, Version::Draft17, Version::Draft18] + let largest = [Version::Draft18] .into_iter() .map(|version| { let form = version.into(); @@ -128,10 +105,7 @@ fn list(data: &Bytes, version: Version, immutable: bool, lift: bool) -> Result key, - false => previous.checked_add(key).ok_or(Error::MalformedTrack)?, - }; + let id = previous.checked_add(key).ok_or(Error::MalformedTrack)?; previous = id; let value = if id % 2 == 0 { let value = decoder.varint()?; @@ -191,10 +165,7 @@ fn serialize<'a>(values: impl IntoIterator, version: Versio let mut encoder = Encoder::new(&mut out, version.into()); let mut previous = 0; for property in sorted { - encoder.varint(match absolute(version) { - true => property.id, - false => property.id - previous, - })?; + encoder.varint(property.id - previous)?; previous = property.id; match &property.value { Value::Int(value) => encoder.varint(*value)?, @@ -408,14 +379,11 @@ pub(crate) fn write( return serialize(×tamp, version); }; let source = source.ok_or(Error::MalformedTrack)?; - if block.immutable && !same_bytes(source, version) { - return Err(Error::Unsupported); - } // Across draft-19, forwarding 0x06 would give it a different meaning. // A mutable one is dropped; a sealed one is refused. let flipped = has_delivery_timeout(source) != has_delivery_timeout(version); // The common case: the block is already what this draft would send. - if timestamp.is_none() && !flipped && same_bytes(source, version) { + if timestamp.is_none() && !flipped { return Ok(block.bytes.to_vec()); } let values = values(block, source)?; @@ -456,10 +424,6 @@ mod tests { #[test] fn key_six_is_opaque_on_every_draft() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -551,7 +515,6 @@ mod tests { &decoded.properties, Some(Version::Draft22), decoded.timestamp, - Version::Draft17, milli ), Err(Error::Unsupported) @@ -722,18 +685,18 @@ mod tests { fn a_status_object_keeps_its_timestamp() { let milli = Some(Timescale::MILLI); let list = Bytes::from_static(&[0x10, 7, 0x3e, 1]); - let framed = read(&list, Version::Draft14, milli).unwrap(); + let framed = read(&list, milli).unwrap(); assert_eq!(framed.timestamp, Some(Timestamp::new(7, Timescale::MILLI).unwrap())); - let status = unframed(&list, Version::Draft14, milli).unwrap(); + let status = unframed(&list, milli).unwrap(); assert_eq!( - write(&status, Some(Version::Draft14), None, Version::Draft14, None).unwrap(), + write(&status, Some(Version::Draft14), None, None).unwrap(), list.to_vec() ); // Over the lite hop, only a status block may hold it. let block = status.block.unwrap().bytes; - assert!(check(&block, Version::Draft14, milli, false).is_ok()); - assert!(check(&block, Version::Draft14, milli, true).is_err()); + assert!(check(&block, milli, false).is_ok()); + assert!(check(&block, milli, true).is_err()); } /// A draft-14 block crosses the lite hop in ascending order, as ingress sorts it, so @@ -741,11 +704,11 @@ mod tests { #[test] fn a_draft14_block_ascends() { let milli = Some(Timescale::MILLI); - assert!(check(&Bytes::from_static(&[0x02, 1, 0x20, 1]), Version::Draft14, milli, true).is_ok()); - assert!(check(&Bytes::from_static(&[0x02, 1, 0x02, 2]), Version::Draft14, milli, true).is_ok()); - assert!(check(&Bytes::from_static(&[0x20, 1, 0x02, 1]), Version::Draft14, milli, true).is_err()); + assert!(check(&Bytes::from_static(&[0x02, 1, 0x20, 1]), milli, true).is_ok()); + assert!(check(&Bytes::from_static(&[0x02, 1, 0x02, 2]), milli, true).is_ok()); + assert!(check(&Bytes::from_static(&[0x20, 1, 0x02, 1]), milli, true).is_err()); let sealed = Bytes::from_static(&[0x02, 1, 0x0b, 4, 0x20, 1, 0x02, 1]); - assert!(check(&sealed, Version::Draft14, milli, true).is_ok()); + assert!(check(&sealed, milli, true).is_ok()); } /// Draft-19 assigns 0x06 to SUBGROUP_DELIVERY_TIMEOUT, so it cannot cross that boundary. @@ -852,13 +815,12 @@ mod tests { .unwrap(), bytes ); - assert!(read(&bytes, Version::Draft17, None).is_err()); + assert!(read(&bytes, None).is_err()); assert!(matches!( write( &decoded.properties, Some(Version::Draft18), decoded.timestamp, - Version::Draft17, None ), Err(Error::Unsupported) @@ -895,7 +857,7 @@ mod compression_tests { } #[test] fn sizes_are_known_and_unknown_properties_survive() { - for version in [Version::Draft17, Version::Draft18, Version::Draft22] { + for version in [Version::Draft18, Version::Draft22] { let unknown = property(0x8800, 7); let wire = compression(serialize([&unknown], version).unwrap(), version, Some(123)).unwrap(); let decoded = read(&wire.into(), version, None).unwrap(); @@ -908,7 +870,7 @@ mod compression_tests { } #[test] fn a_status_cannot_declare_an_inflated_size() { - for version in [Version::Draft17, Version::Draft18, Version::Draft22] { + for version in [Version::Draft18, Version::Draft22] { let size = property(super::super::flate::INFLATED, 0); for value in [ size.clone(), @@ -928,7 +890,7 @@ mod compression_tests { #[test] fn sealed_sizes_are_forwarded_or_refused_without_rewriting() { - for version in [Version::Draft17, Version::Draft18, Version::Draft22] { + for version in [Version::Draft18, Version::Draft22] { let sealed = Property { id: 0x0b, value: Value::Bytes( diff --git a/rs/moq-net/src/ietf/parameters.rs b/rs/moq-net/src/ietf/parameters.rs index 2f5cda2eff1..8e204185527 100644 --- a/rs/moq-net/src/ietf/parameters.rs +++ b/rs/moq-net/src/ietf/parameters.rs @@ -14,8 +14,6 @@ const MAX_KVP_VALUE_LEN: usize = (1 << 16) - 1; #[derive(Debug, Copy, Clone, FromPrimitive, IntoPrimitive, Eq, Hash, PartialEq)] #[repr(u64)] pub enum ParameterVarInt { - /// Removed in draft-17; only used in draft-14/15/16. - MaxRequestId = 2, MaxAuthTokenCacheSize = 4, /// MAX_REQUEST_UPDATES, added in draft-19. MaxRequestUpdates = super::request_update::OPTION, @@ -65,27 +63,18 @@ impl Decode for Parameters { fn decode(r: &mut Decoder<'_>, version: Version) -> Result { let mut params = Self::default(); - // Draft-14/15/16 count the pairs; draft-17+ reads them until the buffer is empty. - let count = match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(r.varint()?), - _ => None, - }; - if count.is_some_and(|count| count > MAX_PARAMS) { - return Err(DecodeError::TooMany); - } - - // Draft-16+ delta-encodes the types; even is a varint value, odd is length-prefixed bytes. - let delta = !matches!(version, Version::Draft14 | Version::Draft15); + // No count: the pairs run to the end of the buffer. The types are delta-encoded; + // even is a varint value, odd is length-prefixed bytes. let mut prev = 0u64; let mut i = 0u64; - while count.map_or(!r.is_empty(), |count| i < count) { + while !r.is_empty() { if i >= MAX_PARAMS { return Err(DecodeError::TooMany); } let kind = r.varint()?; - let kind = match delta && i > 0 { + let kind = match i > 0 { true => prev.checked_add(kind).ok_or(DecodeError::BoundsExceeded)?, false => kind, }; @@ -100,7 +89,6 @@ impl Decode for Parameters { ParameterVarInt::MaxRequestUpdates if !super::request_update::supported(version) => { ParameterVarInt::Unknown(kind) } - ParameterVarInt::Flate if !super::flate::supported(version) => ParameterVarInt::Unknown(kind), kind => kind, }; if !matches!(kind, ParameterVarInt::Unknown(_)) && params.get_varint(kind).is_some() { @@ -125,7 +113,7 @@ impl Decode for Parameters { } impl Encode for Parameters { - fn encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { + fn encode(&self, w: &mut Encoder<'_>, _: Version) -> Result<(), EncodeError> { let count = self.vars.len() + self.bytes.len(); if count as u64 > MAX_PARAMS { return Err(EncodeError::TooMany); @@ -134,46 +122,24 @@ impl Encode for Parameters { return Err(EncodeError::BoundsExceeded); } - match version { - Version::Draft14 | Version::Draft15 => { - w.varint(count as u64)?; - - for (kind, value) in &self.vars { - w.varint(u64::from(*kind))?; - w.varint(*value)?; - } + // No count prefix; the types are delta-encoded in ascending order. + enum ParamRef<'a> { + Var(u64), + Bytes(&'a [u8]), + } + let mut all: Vec<(u64, ParamRef)> = Vec::with_capacity(count); + all.extend(self.vars.iter().map(|(k, v)| (u64::from(*k), ParamRef::Var(*v)))); + all.extend(self.bytes.iter().map(|(k, v)| (u64::from(*k), ParamRef::Bytes(v)))); + all.sort_by_key(|(k, _)| *k); - for (kind, value) in &self.bytes { - w.varint(u64::from(*kind))?; - w.bytes(value)?; - } - } - _ => { - // Draft16: count prefix + delta encoding - // Draft17+: NO count prefix + delta encoding - if matches!(version, Version::Draft16) { - w.varint(count as u64)?; - } + let mut prev = 0u64; + for (kind, value) in all { + w.varint(kind - prev)?; + prev = kind; - enum ParamRef<'a> { - Var(u64), - Bytes(&'a [u8]), - } - let mut all: Vec<(u64, ParamRef)> = Vec::with_capacity(count); - all.extend(self.vars.iter().map(|(k, v)| (u64::from(*k), ParamRef::Var(*v)))); - all.extend(self.bytes.iter().map(|(k, v)| (u64::from(*k), ParamRef::Bytes(v)))); - all.sort_by_key(|(k, _)| *k); - - let mut prev = 0u64; - for (kind, value) in all { - w.varint(kind - prev)?; - prev = kind; - - match value { - ParamRef::Var(v) => w.varint(v)?, - ParamRef::Bytes(v) => w.bytes(v)?, - } - } + match value { + ParamRef::Var(v) => w.varint(v)?, + ParamRef::Bytes(v) => w.bytes(v)?, } } @@ -182,63 +148,6 @@ impl Encode for Parameters { } impl Parameters { - /// Consume a draft-14 request's parameter block, returning every AUTHORIZATION TOKEN - /// (0x03), each decoded by [`super::token::RequestToken`]'s rules. - pub fn skip_request(r: &mut Decoder<'_>) -> Result, DecodeError> { - let mut tokens = Vec::new(); - Self::skip(r, Some(&mut tokens))?; - Ok(tokens) - } - - /// Consume a draft-14 response's parameter block, returning its first - /// MAX_CACHE_DURATION (0x04), the one parameter we act on. - pub fn skip_response(r: &mut Decoder<'_>) -> Result, DecodeError> { - Self::skip(r, None) - } - - /// Draft-14 section 9.2 has a receiver ignore unrecognized parameters and allow their - /// duplicates, and lets AUTHORIZATION TOKEN repeat, so unlike [`Parameters::decode`], - /// which SETUP uses, a repeat is not refused. A request collects its tokens into `tokens`; - /// on a message that does not list them, a response, they are ignored like any other - /// parameter. - fn skip( - r: &mut Decoder<'_>, - mut tokens: Option<&mut Vec>, - ) -> Result, DecodeError> { - let count = r.varint()?; - if count > MAX_PARAMS { - return Err(DecodeError::TooMany); - } - - let mut cache_duration = None; - for _ in 0..count { - // Parity frames a Key-Value-Pair: even is one varint, odd is length prefixed. - let kind = r.varint()?; - match kind % 2 { - 0 => { - let value = r.varint()?; - if kind == 0x04 { - cache_duration.get_or_insert(value); - } - } - _ if let Some(tokens) = tokens.as_deref_mut() - && kind == u64::from(ParameterBytes::AuthorizationToken) => - { - tokens.push(super::token::RequestToken::param_decode(r, Version::Draft14)?); - } - _ => { - let len = usize::try_from(r.varint()?).map_err(|_| DecodeError::BoundsExceeded)?; - if len > MAX_KVP_VALUE_LEN { - return Err(DecodeError::BoundsExceeded); - } - r.slice(len)?; - } - } - } - - Ok(cache_duration) - } - pub fn get_varint(&self, kind: ParameterVarInt) -> Option { self.vars.iter().find(|(k, _)| *k == kind).map(|(_, v)| *v) } @@ -264,13 +173,8 @@ impl Parameters { // ---- Message Parameter Value Encoding ---- -/// Trait for encoding/decoding parameter values with version-specific formats. -/// -/// Parameter encoding differs from field encoding: -/// - Draft-14/15/16: u8 and bool are encoded as varints (cast to u64) -/// - Draft-17: type-specific encoding (u8 as raw byte, bool as raw byte, etc.) -/// -/// Use `_ =>` for the newest draft behavior so future versions default forward. +/// Encodes and decodes a Message Parameter value, whose format is type-specific +/// (a u8 or bool is one raw byte, a Location two bare varints). pub trait Param: Sized { fn param_encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError>; fn param_decode(r: &mut Decoder<'_>, version: Version) -> Result; @@ -290,44 +194,24 @@ pub trait Param: Sized { } impl Param for u8 { - fn param_encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - match version { - // Draft-14/15/16: u8 encoded as varint - Version::Draft14 | Version::Draft15 | Version::Draft16 => w.varint(u64::from(*self))?, - _ => w.u8(*self), - } + fn param_encode(&self, w: &mut Encoder<'_>, _: Version) -> Result<(), EncodeError> { + w.u8(*self); Ok(()) } - fn param_decode(r: &mut Decoder<'_>, version: Version) -> Result { - match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => { - u8::try_from(r.varint()?).map_err(|_| DecodeError::InvalidValue) - } - _ => r.u8(), - } + fn param_decode(r: &mut Decoder<'_>, _: Version) -> Result { + r.u8() } } impl Param for bool { - fn param_encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - match version { - // Draft-14/15/16: bool encoded as varint - Version::Draft14 | Version::Draft15 | Version::Draft16 => w.varint(u8::from(*self).into())?, - _ => w.bool(*self), - } + fn param_encode(&self, w: &mut Encoder<'_>, _: Version) -> Result<(), EncodeError> { + w.bool(*self); Ok(()) } - fn param_decode(r: &mut Decoder<'_>, version: Version) -> Result { - match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => match r.varint()? { - 0 => Ok(false), - 1 => Ok(true), - _ => Err(DecodeError::InvalidValue), - }, - _ => r.bool(), - } + fn param_decode(r: &mut Decoder<'_>, _: Version) -> Result { + r.bool() } } @@ -342,51 +226,19 @@ impl Param for u64 { } } -/// A Location parameter value, such as LARGEST_OBJECT (0x09). -/// -/// Draft-16 section 9.2 serializes every Message Parameter as a Key-Value-Pair, and section -/// 9.2.2.7 calls LARGEST_OBJECT "a length-prefixed Location structure", so the two varints -/// sit inside a byte string there. Draft-17 section 9.3, and section 10.2 from draft-18 on, -/// redefines a Message Parameter as `{ Type Delta (vi64), Value (..) }` with no Length at -/// all, and lists "Location: Two consecutive varints (Group, Object)" as its own value -/// encoding beside Length-prefixed. So from draft-17 the two varints are written bare. +/// A Location parameter value, such as LARGEST_OBJECT (0x09): two bare varints, as a +/// Message Parameter has no Length (draft-18 section 10.2). impl Param for Location { - fn param_encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => { - // The drafts before 17 pin the inner varints to the draft-15 encoding, - // matching the other length-prefixed parameters. - let mut buf = Vec::new(); - let mut inner = Encoder::new(&mut buf, Version::Draft15.into()); - inner.varint(self.group)?; - inner.varint(self.object)?; - w.bytes(&buf) - } - _ => { - w.varint(self.group)?; - w.varint(self.object)?; - Ok(()) - } - } + fn param_encode(&self, w: &mut Encoder<'_>, _: Version) -> Result<(), EncodeError> { + w.varint(self.group)?; + w.varint(self.object)?; + Ok(()) } - fn param_decode(r: &mut Decoder<'_>, version: Version) -> Result { - match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => { - let mut inner = Decoder::new(r.bytes()?, Version::Draft15.into()); - let group = inner.varint()?; - let object = inner.varint()?; - if !inner.is_empty() { - return Err(DecodeError::TrailingBytes); - } - Ok(Location { group, object }) - } - _ => { - let group = r.varint()?; - let object = r.varint()?; - Ok(Location { group, object }) - } - } + fn param_decode(r: &mut Decoder<'_>, _: Version) -> Result { + let group = r.varint()?; + let object = r.varint()?; + Ok(Location { group, object }) } } @@ -453,43 +305,6 @@ impl Param for Opaque { } } -/// Message parameter ids defined in draft-16. Sorted for `binary_search`. -/// -/// A known id on a message that does not list it is ignored. An id outside this set -/// is unknown and closes the session. -const DRAFT16_MESSAGE_PARAMS: &[u64] = &[0x02, 0x03, 0x08, 0x09, 0x10, 0x20, 0x21, 0x22, 0x32]; - -fn skip_kvp(r: &mut Decoder<'_>, key: u64) -> Result<(), DecodeError> { - if key.is_multiple_of(2) { - r.varint()?; - } else { - let value = r.bytes()?; - if value.len() > MAX_KVP_VALUE_LEN { - return Err(DecodeError::BoundsExceeded); - } - } - Ok(()) -} - -/// Consumes a parameter the message does not list, when that draft says to ignore it. -/// -/// Draft-14 and draft-15 ignore an unrecognized parameter, including one defined for a -/// different message. Draft-16 ignores a known parameter on the wrong message and closes -/// on an unknown id. From draft-17 on both close the session, and a parameter value has -/// no length to skip by, so this returns false and the caller fails the message. -pub(crate) fn skip_unlisted(r: &mut Decoder<'_>, version: Version, key: u64) -> Result { - let ignore = match version { - Version::Draft14 | Version::Draft15 => true, - Version::Draft16 => DRAFT16_MESSAGE_PARAMS.binary_search(&key).is_ok(), - _ => false, - }; - if !ignore { - return Ok(false); - } - skip_kvp(r, key)?; - Ok(true) -} - /// Encode message parameters with compile-time sorted keys. /// /// Keys must be listed in ascending order (enforced at compile time). @@ -527,11 +342,7 @@ macro_rules! encode_params { $( if $crate::ietf::Param::param_present(&$val) { let _key: u64 = $key; - let _wire = match _version { - $crate::ietf::Version::Draft14 | $crate::ietf::Version::Draft15 => _key, - _ if _first => _key, - _ => _key - _prev_key, - }; + let _wire = if _first { _key } else { _key - _prev_key }; $w.varint(_wire)?; _prev_key = _key; _first = false; @@ -548,9 +359,7 @@ macro_rules! encode_params { /// for parameters where `T::default()` is an acceptable fallback. /// /// A `where` gate takes the key off the list on versions where the expression is false. -/// What remains unlisted is ignored on draft-14 and draft-15, ignored on draft-16 when -/// the id is a message parameter of that draft used on the wrong message, and a -/// `DecodeError::InvalidValue` otherwise (unknown id, or any unlisted id from draft-17 on). +/// An unlisted key is a `DecodeError::InvalidValue`: a value has no length to skip by. /// Duplicate parameters cause `DecodeError::Duplicate`, unless the type allows a repeat /// (see [`Param::param_repeat`]), such as `Vec`. /// @@ -588,17 +397,17 @@ macro_rules! decode_params { let mut _prev_key: u64 = 0; for _i in 0.._count { let _wire = $r.varint()?; - let _key: u64 = match _version { - $crate::ietf::Version::Draft14 | $crate::ietf::Version::Draft15 => _wire, - _ if _i == 0 => _wire, - _ => _prev_key.checked_add(_wire).ok_or($crate::coding::DecodeError::BoundsExceeded)?, + let _key: u64 = if _i == 0 { + _wire + } else { + _prev_key.checked_add(_wire).ok_or($crate::coding::DecodeError::BoundsExceeded)? }; _prev_key = _key; // An if-chain rather than a `match`, so a key can be a named constant: // the macro captures it as an expression, which is not a legal pattern. - // A false `where` gate falls through, so the draft's ignore-or-close rule applies - // instead of validating a value the message is not allowed to carry. + // A false `where` gate falls through to the refusal instead of validating a + // value the message is not allowed to carry. $( if _key == $key $( && ($gate) )? { let _value = <$ty as $crate::ietf::Param>::param_decode($r, _version)?; @@ -609,9 +418,6 @@ macro_rules! decode_params { continue; } )* - if $crate::ietf::parameters::skip_unlisted($r, _version, _key)? { - continue; - } return Err($crate::coding::DecodeError::InvalidValue); } } @@ -645,10 +451,6 @@ mod tests { #[test] fn setup_allows_repeated_unknown_options() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -658,18 +460,10 @@ mod tests { for kind in [0x20, 0x21, 0x9d, 0x11c] { let mut buf = Vec::new(); let mut w = Encoder::new(&mut buf, version.into()); - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - w.varint(2).unwrap(); - } w.varint(kind).unwrap(); for delta in [None, Some(0)] { if let Some(delta) = delta { - w.varint(if matches!(version, Version::Draft14 | Version::Draft15) { - kind - } else { - delta - }) - .unwrap(); + w.varint(delta).unwrap(); } if kind % 2 == 0 { w.varint(1).unwrap(); @@ -704,7 +498,6 @@ mod tests { #[test] fn group_order_parameter_rejects_values_outside_one_and_two() { for version in [ - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -734,61 +527,7 @@ mod tests { } #[test] - fn legacy_group_order_zero_keeps_the_publisher_preference() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { - let mut r = Decoder::new(&[0], version.into()); - assert_eq!( - super::super::GroupOrder::param_decode(&mut r, version).unwrap(), - super::super::GroupOrder::Descending - ); - } - } - - // ---- Setup Parameters tests (unchanged) ---- - - #[test] - fn test_parameters_v16_delta_round_trip() { - let mut params = Parameters::default(); - params.set_bytes(ParameterBytes::Path, b"/test".to_vec()); - params.set_varint(ParameterVarInt::MaxRequestId, 100); - params.set_bytes(ParameterBytes::Implementation, b"test-impl".to_vec()); - - let mut buf = Vec::new(); - params - .encode(&mut Encoder::new(&mut buf, Version::Draft16.into()), Version::Draft16) - .unwrap(); - - let mut bytes = bytes::Bytes::from(buf); - let decoded = crate::coding::decode_buf(&mut bytes, Version::Draft16, Parameters::decode).unwrap(); - - assert_eq!(decoded.get_bytes(ParameterBytes::Path), Some(b"/test".as_ref())); - assert_eq!(decoded.get_varint(ParameterVarInt::MaxRequestId), Some(100)); - assert_eq!( - decoded.get_bytes(ParameterBytes::Implementation), - Some(b"test-impl".as_ref()) - ); - } - - #[test] - fn test_parameters_v15_round_trip() { - let mut params = Parameters::default(); - params.set_bytes(ParameterBytes::Path, b"/test".to_vec()); - params.set_varint(ParameterVarInt::MaxRequestId, 100); - - let mut buf = Vec::new(); - params - .encode(&mut Encoder::new(&mut buf, Version::Draft15.into()), Version::Draft15) - .unwrap(); - - let mut bytes = bytes::Bytes::from(buf); - let decoded = crate::coding::decode_buf(&mut bytes, Version::Draft15, Parameters::decode).unwrap(); - - assert_eq!(decoded.get_bytes(ParameterBytes::Path), Some(b"/test".as_ref())); - assert_eq!(decoded.get_varint(ParameterVarInt::MaxRequestId), Some(100)); - } - - #[test] - fn test_parameters_v17_round_trip() { + fn setup_parameters_round_trip() { let mut params = Parameters::default(); params.set_bytes(ParameterBytes::Path, b"/test".to_vec()); params.set_varint(ParameterVarInt::MaxAuthTokenCacheSize, 4096); @@ -796,37 +535,18 @@ mod tests { let mut buf = Vec::new(); params - .encode(&mut Encoder::new(&mut buf, Version::Draft17.into()), Version::Draft17) + .encode(&mut Encoder::new(&mut buf, Version::Draft18.into()), Version::Draft18) .unwrap(); + // No count prefix: the first byte is the first type, PATH (0x01). + assert_eq!(buf[0], 0x01); - let mut bytes = bytes::Bytes::from(buf); - let decoded = crate::coding::decode_buf(&mut bytes, Version::Draft17, Parameters::decode).unwrap(); - + let (decoded, _) = Parameters::decode_slice(&buf, Version::Draft18).unwrap(); assert_eq!(decoded.get_bytes(ParameterBytes::Path), Some(b"/test".as_ref())); assert_eq!(decoded.get_varint(ParameterVarInt::MaxAuthTokenCacheSize), Some(4096)); assert_eq!( decoded.get_bytes(ParameterBytes::Implementation), Some(b"test-impl".as_ref()) ); - assert!(bytes.is_empty()); - } - - #[test] - fn test_parameters_v17_no_count_prefix() { - let mut params = Parameters::default(); - params.set_bytes(ParameterBytes::Path, b"/x".to_vec()); - - let mut buf15 = Vec::new(); - params - .encode(&mut Encoder::new(&mut buf15, Version::Draft15.into()), Version::Draft15) - .unwrap(); - - let mut buf17 = Vec::new(); - params - .encode(&mut Encoder::new(&mut buf17, Version::Draft17.into()), Version::Draft17) - .unwrap(); - - assert!(buf17.len() < buf15.len()); } // ---- Message Parameter (encode_params!/decode_params!) tests ---- @@ -846,10 +566,6 @@ mod tests { #[test] fn test_param_u8_all_versions() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, ] { round_trip_params( @@ -870,8 +586,6 @@ mod tests { #[test] fn test_param_u8_wire_vectors() -> Result<(), EncodeError> { for (version, expected) in [ - (Version::Draft16, &[0x01, 0x20, 0x40, 0xff][..]), - (Version::Draft17, &[0x01, 0x20, 0xff][..]), (Version::Draft18, &[0x01, 0x20, 0xff][..]), (Version::Draft19, &[0x01, 0x20, 0xff][..]), ] { @@ -894,10 +608,6 @@ mod tests { #[test] fn test_param_bool_all_versions() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, ] { round_trip_params( @@ -919,10 +629,6 @@ mod tests { fn test_param_location_all_versions() { let loc = Location { group: 5, object: 3 }; for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, ] { round_trip_params( @@ -946,12 +652,8 @@ mod tests { group: 255, object: 128, }; - // Draft-16 wraps the two varints in a Key-Value-Pair Length; from draft-17 a - // Message Parameter has no Length and a Location value is two bare varints. The - // inner format differs too: QUIC-style before draft-17, leading-ones after. + // A Message Parameter has no Length, so a Location value is two bare varints. for (version, expected) in [ - (Version::Draft16, &[0x01, 0x09, 0x04, 0x40, 0xff, 0x40, 0x80][..]), - (Version::Draft17, &[0x01, 0x09, 0x80, 0xff, 0x80, 0x80][..]), (Version::Draft18, &[0x01, 0x09, 0x80, 0xff, 0x80, 0x80][..]), (Version::Draft19, &[0x01, 0x09, 0x80, 0xff, 0x80, 0x80][..]), (Version::Draft20, &[0x01, 0x09, 0x80, 0xff, 0x80, 0x80][..]), @@ -975,10 +677,6 @@ mod tests { #[test] fn test_param_filter_all_versions() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, ] { round_trip_params( @@ -999,10 +697,6 @@ mod tests { #[test] fn test_param_multiple_delta_encoding() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, ] { round_trip_params( @@ -1036,10 +730,6 @@ mod tests { #[test] fn test_param_empty_set() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, ] { round_trip_params( @@ -1059,10 +749,6 @@ mod tests { #[test] fn test_param_option_skip_none() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, ] { round_trip_params( @@ -1091,10 +777,6 @@ mod tests { #[test] fn test_param_option_encode_some() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, ] { round_trip_params( @@ -1124,10 +806,6 @@ mod tests { fn test_param_bare_type_defaults() { // Bare types use T::default() when the parameter is absent for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, ] { round_trip_params( @@ -1152,13 +830,8 @@ mod tests { #[test] fn test_param_unknown_rejected() { - // 0x3E is not a message parameter in any of these drafts. Draft-14 and draft-15 - // ignore it. Draft-16 on closes the session. + // 0x3E is not a message parameter, which closes the session. for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, ] { let mut buf = Vec::new(); @@ -1172,25 +845,18 @@ mod tests { decode_params!(&mut bytes, version, 0x20 => val: Option); Ok(val) })(); - match version { - Version::Draft14 | Version::Draft15 => { - assert_eq!(result.unwrap(), None, "{version} ignores an unrecognized parameter"); - assert!(bytes.is_empty(), "{version}"); - } - _ => assert!( - matches!(result, Err(DecodeError::InvalidValue)), - "expected InvalidValue for unknown param in {version}" - ), - } + assert!( + matches!(result, Err(DecodeError::InvalidValue)), + "expected InvalidValue for unknown param in {version}" + ); } } - /// EXPIRES (0x08) is a draft-16 message parameter, but not for a message whose list is - /// only SUBSCRIBER_PRIORITY. Draft-16 ignores it and still reads the parameter after it. - /// Draft-18 closes the session on the same bytes. + /// EXPIRES (0x08) is a message parameter, but not for a message whose list is only + /// SUBSCRIBER_PRIORITY, so it closes the session. #[test] fn known_param_on_the_wrong_message() { - for version in [Version::Draft16, Version::Draft18] { + for version in [Version::Draft18] { let mut buf = Vec::new(); let mut w = Encoder::new(&mut buf, version.into()); w.varint(2).unwrap(); @@ -1204,14 +870,7 @@ mod tests { decode_params!(&mut bytes, version, 0x20 => val: Option); Ok(val) })(); - match version { - Version::Draft16 => { - assert_eq!(result.unwrap(), Some(7)); - assert!(bytes.is_empty()); - } - Version::Draft18 => assert!(matches!(result, Err(DecodeError::InvalidValue))), - _ => unreachable!(), - } + assert!(matches!(result, Err(DecodeError::InvalidValue))); } } @@ -1219,32 +878,17 @@ mod tests { fn test_param_duplicate_rejected() { // Manually construct a buffer with duplicate key 0x20 for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, ] { let mut buf = Vec::new(); let mut w = Encoder::new(&mut buf, version.into()); // Encode count = 2 w.varint(2).unwrap(); - match version { - Version::Draft14 | Version::Draft15 => { - // Plain (non-delta) keys: first key=0x20, second key=0x20 - w.varint(0x20u64).unwrap(); - 100u8.param_encode(&mut w, version).unwrap(); - w.varint(0x20u64).unwrap(); - 200u8.param_encode(&mut w, version).unwrap(); - } - _ => { - // Delta-encoded: first delta=0x20 (abs=0x20), second delta=0 (abs=0x20) - w.varint(0x20u64).unwrap(); - 100u8.param_encode(&mut w, version).unwrap(); - w.varint(0).unwrap(); - 200u8.param_encode(&mut w, version).unwrap(); - } - } + // Delta-encoded: first delta=0x20 (abs=0x20), second delta=0 (abs=0x20) + w.varint(0x20u64).unwrap(); + 100u8.param_encode(&mut w, version).unwrap(); + w.varint(0).unwrap(); + 200u8.param_encode(&mut w, version).unwrap(); let mut bytes = Decoder::new(&buf, version.into()); let result: Result<(), DecodeError> = (|| { @@ -1272,15 +916,13 @@ mod tests { value: vec![0xAA], }) }; - for version in [Version::Draft15, Version::Draft16, Version::Draft17, Version::Draft20] { + for version in [Version::Draft20] { let mut buf = Vec::new(); let mut w = Encoder::new(&mut buf, version.into()); w.varint(2).unwrap(); w.varint(0x03).unwrap(); token(0).param_encode(&mut w, version).unwrap(); - // The second key: absolute before draft-16, a zero delta after. - let second: u64 = if version == Version::Draft15 { 0x03 } else { 0 }; - w.varint(second).unwrap(); + w.varint(0).unwrap(); token(1).param_encode(&mut w, version).unwrap(); let mut r = Decoder::new(&buf, version.into()); @@ -1293,45 +935,4 @@ mod tests { assert!(r.is_empty(), "{version}"); } } - - /// Draft-14 lets AUTHORIZATION TOKEN repeat and has unknown parameters, duplicates - /// included, ignored. The block is consumed whole either way. - #[test] - fn test_skip_allows_draft14_repeats() { - #[rustfmt::skip] - let block = [ - 0x04, // Number of Parameters - 0x03, 0x03, 0x03, 0x00, 0xAA, // AUTHORIZATION TOKEN: USE_VALUE, type 0 - 0x03, 0x03, 0x03, 0x01, 0xBB, // and again, type 1 - 0x3E, 0x05, // an unknown varint parameter - 0x3E, 0x06, // and again - ]; - let mut r = Decoder::new(&block, Version::Draft14.into()); - let tokens = Parameters::skip_request(&mut r).unwrap(); - assert_eq!(tokens.len(), 2); - assert_eq!(tokens[1].0.kind, 1); - assert!(r.is_empty()); - } - - /// A draft-14 request decodes every token; a response ignores one, as section 9.2 has - /// for a parameter on a message that does not list it. - #[test] - fn test_skip_decodes_draft14_request_tokens() { - #[rustfmt::skip] - let block = [ - 0x02, // Number of Parameters - 0x03, 0x03, 0x03, 0x00, 0xAA, // AUTHORIZATION TOKEN: USE_VALUE, type 0 - 0x03, 0x02, 0x02, 0x07, // and a USE_ALIAS, never registered - ]; - let mut r = Decoder::new(&block, Version::Draft14.into()); - let err = Parameters::skip_request(&mut r).unwrap_err(); - assert!( - matches!(err, DecodeError::Session(crate::SessionError::UnknownAuthTokenAlias)), - "{err:?}" - ); - - let mut r = Decoder::new(&block, Version::Draft14.into()); - assert_eq!(Parameters::skip_response(&mut r).unwrap(), None); - assert!(r.is_empty()); - } } diff --git a/rs/moq-net/src/ietf/properties.rs b/rs/moq-net/src/ietf/properties.rs index 093a4bb17d3..1c9234a8337 100644 --- a/rs/moq-net/src/ietf/properties.rs +++ b/rs/moq-net/src/ietf/properties.rs @@ -82,16 +82,6 @@ pub struct Properties { } impl Properties { - /// Whether [`Self::encode`] writes a TIMESCALE on this version. - /// - /// Drafts 14-16 do not. 14 and 15 have no properties block, and 16's Track Extensions - /// predate the TIMESCALE registration; an older peer rejects trailing bytes it does not - /// parse. A Timestamp on those drafts would have no units. Draft-16 is still read, so a - /// peer that sends the block is understood. - pub(crate) fn sends_timescale(version: Version) -> bool { - !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - } - /// The properties a relay states for a track it serves, from the `info` it acts on. /// /// We serve the newest group first by default, matching moq-lite. @@ -119,9 +109,6 @@ impl Properties { /// A property the publisher sealed inside Immutable Properties travels there, so it is /// not restated in the mutable list. pub fn encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - if matches!(version, Version::Draft14 | Version::Draft15) { - return Ok(()); - } let int = |id, value| Property { id, @@ -129,20 +116,18 @@ impl Properties { }; let mut known = Vec::new(); - if Self::sends_timescale(version) { - if let Some(age) = self.max_cache_duration { - let millis = u64::try_from(age.as_millis()).map_err(|_| EncodeError::BoundsExceeded)?; - known.push(int(MAX_CACHE_DURATION, millis)); - } - if let Some(timescale) = self.timescale { - known.push(int(TIMESCALE, u64::from(timescale))); - } - if let Some(priority) = self.priority { - known.push(int(DEFAULT_PUBLISHER_PRIORITY, u64::from(priority))); - } - if let Some(group_order) = self.group_order { - known.push(int(DEFAULT_PUBLISHER_GROUP_ORDER, u64::from(u8::from(group_order)))); - } + if let Some(age) = self.max_cache_duration { + let millis = u64::try_from(age.as_millis()).map_err(|_| EncodeError::BoundsExceeded)?; + known.push(int(MAX_CACHE_DURATION, millis)); + } + if let Some(timescale) = self.timescale { + known.push(int(TIMESCALE, u64::from(timescale))); + } + if let Some(priority) = self.priority { + known.push(int(DEFAULT_PUBLISHER_PRIORITY, u64::from(priority))); + } + if let Some(group_order) = self.group_order { + known.push(int(DEFAULT_PUBLISHER_GROUP_ORDER, u64::from(u8::from(group_order)))); } // Omitted means 0, so only support is worth bytes. @@ -150,7 +135,7 @@ impl Properties { known.push(int(DYNAMIC_GROUPS, 1)); } - if self.compress && super::flate::supported(version) { + if self.compress { known.push(int(super::flate::COMPRESSION, 1)); } @@ -190,10 +175,7 @@ impl Properties { // Draft-16 calls the block Track Extensions, draft-17+ Track Properties. Same encoding, // so read either rather than faulting the message for trailing bytes. - match version { - Version::Draft14 | Version::Draft15 => return Ok(properties), - _ => {} - } + {} let mut prev_type: u64 = 0; let mut i: u64 = 0; @@ -311,9 +293,6 @@ pub(crate) const MAX_FORWARDED: usize = MAX_PROPERTIES as usize - UNDERSTOOD.len /// [`Properties::decode`] does. pub(crate) fn check_immutable(block: &[u8], version: Version) -> Result<(), DecodeError> { // Track Properties start at draft-16. - if matches!(version, Version::Draft14 | Version::Draft15) { - return Err(DecodeError::InvalidValue); - } sealed( &mut Decoder::new(block, version.into()), &mut Properties::default(), @@ -449,7 +428,7 @@ mod tests { fn test_skip_empty_properties() { let mut buf = bytes::Bytes::new(); assert_eq!( - crate::coding::decode_buf(&mut buf, Version::Draft17, Properties::decode).unwrap(), + crate::coding::decode_buf(&mut buf, Properties::decode).unwrap(), Properties::default() ); } @@ -461,7 +440,7 @@ mod tests { Encoder::new(&mut buf, Version::Draft17.into()).varint(0x02u64).unwrap(); // delta type Encoder::new(&mut buf, Version::Draft17.into()).varint(5000u64).unwrap(); // value let mut bytes = bytes::Bytes::from(buf); - crate::coding::decode_buf(&mut bytes, Version::Draft17, Properties::decode).unwrap(); + crate::coding::decode_buf(&mut bytes, Properties::decode).unwrap(); assert!(bytes.is_empty()); } @@ -473,7 +452,7 @@ mod tests { Encoder::new(&mut buf, Version::Draft17.into()).varint(3u64).unwrap(); // length buf.extend_from_slice(&[0x03, 0x01, 0x2A]); // sealed type 0x03, one byte let mut bytes = bytes::Bytes::from(buf); - crate::coding::decode_buf(&mut bytes, Version::Draft17, Properties::decode).unwrap(); + crate::coding::decode_buf(&mut bytes, Properties::decode).unwrap(); assert!(bytes.is_empty()); } @@ -492,7 +471,7 @@ mod tests { buf.extend_from_slice(&[0x02, 0x2A]); // sealed type 0x02 = 42 let mut bytes = bytes::Bytes::from(buf); - crate::coding::decode_buf(&mut bytes, Version::Draft17, Properties::decode).unwrap(); + crate::coding::decode_buf(&mut bytes, Properties::decode).unwrap(); assert!(bytes.is_empty()); } @@ -524,8 +503,6 @@ mod tests { #[test] fn publisher_priority_is_bounded_to_a_byte() { for version in [ - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -563,7 +540,7 @@ mod tests { Encoder::new(&mut buf, Version::Draft16.into()).varint(2u64).unwrap(); let mut bytes = bytes::Bytes::from(buf); - let properties = crate::coding::decode_buf(&mut bytes, Version::Draft16, Properties::decode).unwrap(); + let properties = crate::coding::decode_buf(&mut bytes, Properties::decode).unwrap(); assert_eq!(properties.group_order, Some(GroupOrder::Descending)); assert!(bytes.is_empty()); } @@ -582,7 +559,7 @@ mod tests { group_order: None, }; - for version in [Version::Draft14, Version::Draft15, Version::Draft16, Version::Draft17] { + for version in [Version::Draft15] { let mut buf = Vec::new(); properties .encode(&mut Encoder::new(&mut buf, version.into()), version) @@ -703,7 +680,7 @@ mod tests { /// in order among the ones we write ourselves. #[test] fn unknown_properties_round_trip() { - for version in [Version::Draft16, Version::Draft22] { + for version in [Version::Draft22] { // 0x02 = 5000, 0x0b = { 0x02 = 42 }, 0x3801 = "hi" let mut block = Vec::new(); let mut w = Encoder::new(&mut block, version.into()); diff --git a/rs/moq-net/src/ietf/publish.rs b/rs/moq-net/src/ietf/publish.rs index a388cdec298..1a604d1d8a5 100644 --- a/rs/moq-net/src/ietf/publish.rs +++ b/rs/moq-net/src/ietf/publish.rs @@ -110,7 +110,7 @@ use crate::{ Path, coding::{Decode, DecodeError, Decoder, Encode, EncodeError, Encoder}, ietf::{ - Filter, GroupOrder, Location, Parameters, Properties, RequestId, + Filter, GroupOrder, Location, Properties, RequestId, namespace::{decode_track, encode_namespace}, }, }; @@ -133,27 +133,14 @@ pub(crate) enum PublishDoneStatus { } impl PublishDoneStatus { - /// Return the registered wire code for every supported draft. - pub(crate) const fn code(self, version: Version) -> u64 { - match version { - // Draft-14 registers no UPDATE_FAILED; its 0x8 is unassigned. - Version::Draft14 if matches!(self, Self::UpdateFailed) => 0x0, - Version::Draft14 - | Version::Draft15 - | Version::Draft16 - | Version::Draft17 - | Version::Draft18 - | Version::Draft19 - // Draft-20 removed SUBSCRIPTION_ENDED (0x3), which this implementation never - // emitted, and left the rest of the registry alone. - | Version::Draft20 - | Version::Draft21 - | Version::Draft22 => match self { - Self::InternalError => 0x0, - Self::Unauthorized => 0x1, - Self::TrackEnded => 0x2, - Self::UpdateFailed => 0x8, - }, + /// Return the registered wire code. Draft-20 removed SUBSCRIPTION_ENDED (0x3), which + /// this implementation never emitted, and left the rest of the registry alone. + pub(crate) const fn code(self) -> u64 { + match self { + Self::InternalError => 0x0, + Self::Unauthorized => 0x1, + Self::TrackEnded => 0x2, + Self::UpdateFailed => 0x8, } } } @@ -161,7 +148,6 @@ impl PublishDoneStatus { /// Used to be called SubscribeDone #[derive(Clone, Debug)] pub struct PublishDone<'a> { - pub request_id: Option, pub status_code: u64, pub stream_count: u64, pub reason_phrase: Cow<'a, str>, @@ -171,17 +157,13 @@ impl PublishDone<'_> { /// How the publisher ended the subscription: cleanly, or with the error its status names. pub(crate) fn end(&self, version: Version) -> Result<(), crate::Error> { match self.status_code { - code if code == PublishDoneStatus::TrackEnded.code(version) => Ok(()), - code if code == PublishDoneStatus::Unauthorized.code(version) => Err(crate::Error::Unauthorized), + code if code == PublishDoneStatus::TrackEnded.code() => Ok(()), + code if code == PublishDoneStatus::Unauthorized.code() => Err(crate::Error::Unauthorized), // SUBSCRIPTION_ENDED: the subscription reached the end its filter asked for. // Draft-20 removed it and left 0x3 unassigned. 0x3 if matches!( version, - Version::Draft14 - | Version::Draft15 - | Version::Draft16 - | Version::Draft17 - | Version::Draft18 + Version::Draft18 | Version::Draft19 ) => { @@ -195,32 +177,19 @@ impl PublishDone<'_> { impl Message for PublishDone<'_> { const ID: u64 = 0x0b; - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - self.request_id - .expect("request_id required for draft14-16") - .encode(w, version)?; - } else { - assert!(self.request_id.is_none(), "request_id must be None for draft17+"); - } + fn encode_msg(&self, w: &mut Encoder<'_>, _: Version) -> Result<(), EncodeError> { w.varint(self.status_code)?; w.varint(self.stream_count)?; w.string(&self.reason_phrase)?; Ok(()) } - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - Some(RequestId::decode(r, version)?) - } else { - None - }; + fn decode_msg(r: &mut Decoder<'_>, _: Version) -> Result { let status_code = r.varint()?; let stream_count = r.varint()?; let reason_phrase = Cow::Owned(r.string()?); Ok(Self { - request_id, status_code, stream_count, reason_phrase, @@ -252,150 +221,74 @@ impl Message for Publish<'_> { fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { self.request_id.encode(w, version)?; - if version == Version::Draft17 { - w.varint(0)?; // required_request_id_delta = 0 - } encode_namespace(w, &self.track_namespace)?; w.string(&self.track_name)?; w.varint(self.track_alias)?; - match version { - Version::Draft14 => { - self.properties - .group_order - .unwrap_or(GroupOrder::Ascending) - .encode(w, version)?; - if let Some(location) = &self.largest_location { - w.bool(true); - location.encode(w, version)?; - } else { - w.bool(false); - } - - w.bool(self.forward); - // parameters - w.u8(0); - } - _ => { - // GROUP_ORDER is the publisher's preference on a draft-15 PUBLISH, illegal from - // draft-16 to draft-19 (a peer closes the session with PROTOCOL_VIOLATION), and - // from draft-20 one of the subscription's parameters, which moved here. The - // publisher's preference is a DEFAULT_PUBLISHER_GROUP_ORDER track property - // instead, which we write from draft-17 on. Draft-16 gets neither form; see - // Properties::encode. - let draft20 = Filter::is_draft20(version); - let group_order = match version { - Version::Draft15 => self.properties.group_order, - _ if draft20 => self.group_order, - _ => None, - }; - - encode_params!(w, version, - 0x09 => self.largest_location, - 0x10 => self.forward, - 0x20 => self.subscriber_priority.filter(|_| draft20), - 0x21 => self.filter.filter(|_| draft20), - 0x22 => group_order, - ); + // GROUP_ORDER is illegal on PUBLISH through draft-19 (a peer closes the session with + // PROTOCOL_VIOLATION), and from draft-20 one of the subscription's parameters, which + // moved here. The publisher's preference is a DEFAULT_PUBLISHER_GROUP_ORDER track + // property instead. + let draft20 = Filter::is_draft20(version); + encode_params!(w, version, + 0x09 => self.largest_location, + 0x10 => self.forward, + 0x20 => self.subscriber_priority.filter(|_| draft20), + 0x21 => self.filter.filter(|_| draft20), + 0x22 => self.group_order.filter(|_| draft20), + ); - // Track Properties are the final field, so nothing may follow. - self.properties.encode(w, version)?; - } - } + // Track Properties are the final field, so nothing may follow. + self.properties.encode(w, version)?; Ok(()) } fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { let request_id = RequestId::decode(r, version)?; - if version == Version::Draft17 { - let _required_request_id_delta = r.varint()?; - } let (track_namespace, track_name) = decode_track(r)?; let track_name = Cow::Owned(track_name); let track_alias = r.varint()?; - match version { - Version::Draft14 => { - let group_order = GroupOrder::decode(r, version)?.any_to_descending(); - let content_exists = r.bool()?; - let largest_location = match content_exists { - true => Some(Location::decode(r, version)?), - false => None, - }; - let forward = r.bool()?; - // parameters - super::token::single(Parameters::skip_request(r)?)?; - - Ok(Self { - request_id, - track_namespace, - track_name, - track_alias, - largest_location, - forward, - properties: Properties { - group_order: Some(group_order), - ..Default::default() - }, - subscriber_priority: None, - group_order: None, - filter: None, - }) - } - _ => { - // GROUP_ORDER is legal on PUBLISH in draft-15 and again from draft-20, when - // the subscription parameters moved here. Draft-16 still knows the id, so one - // on this message is ignored. Draft-17 through draft-19 close the session. - // - // The draft-20 parameters have to parse even though reverse publishing is - // unsupported: an unlisted parameter fails the whole message, which would - // kill the session instead of letting the request reach NOT_SUPPORTED. - // OBJECT_DELIVERY_TIMEOUT is also legal in draft-15. The timeouts are dropped. - decode_params!(r, version, - 0x02 => _object_delivery_timeout: Option where version == Version::Draft15 || Filter::is_draft20(version), - 0x03 => authorization_token: Vec, - 0x06 => _subgroup_delivery_timeout: Option where Filter::is_draft20(version), - 0x08 => _expires: Option, - 0x09 => largest_location: Option, - 0x10 => forward: Option, - 0x20 => subscriber_priority: Option where Filter::is_draft20(version), - 0x21 => filter: Option where Filter::is_draft20(version), - 0x22 => group_order: Option where version == Version::Draft15 || Filter::is_draft20(version), - ); - let mut properties = Properties::decode(r, version)?; - // Last, so a malformed field closes the session before a token refusal. - super::token::single(authorization_token)?; - // Draft-15's GROUP_ORDER is the publisher's preference; draft-20's is the - // subscription's. - let (publisher_order, group_order) = match version { - Version::Draft15 => (group_order, None), - _ => (None, group_order), - }; - properties.group_order = properties.group_order.or(publisher_order); - - let forward = forward.unwrap_or(true); - - Ok(Self { - request_id, - track_namespace, - track_name, - track_alias, - largest_location, - forward, - properties, - subscriber_priority, - group_order, - filter, - }) - } - } + // GROUP_ORDER is legal on PUBLISH from draft-20, when the subscription parameters + // moved here; before that it closes the session. + // + // The draft-20 parameters have to parse even though reverse publishing is + // unsupported: an unlisted parameter fails the whole message, which would kill the + // session instead of letting the request reach NOT_SUPPORTED. The timeouts are + // dropped. + decode_params!(r, version, + 0x02 => _object_delivery_timeout: Option where Filter::is_draft20(version), + 0x03 => authorization_token: Vec, + 0x06 => _subgroup_delivery_timeout: Option where Filter::is_draft20(version), + 0x08 => _expires: Option, + 0x09 => largest_location: Option, + 0x10 => forward: Option, + 0x20 => subscriber_priority: Option where Filter::is_draft20(version), + 0x21 => filter: Option where Filter::is_draft20(version), + 0x22 => group_order: Option where Filter::is_draft20(version), + ); + let properties = Properties::decode(r, version)?; + // Last, so a malformed field closes the session before a token refusal. + super::token::single(authorization_token)?; + + Ok(Self { + request_id, + track_namespace, + track_name, + track_alias, + largest_location, + forward: forward.unwrap_or(true), + properties, + subscriber_priority, + group_order, + filter, + }) } } #[derive(Debug)] pub struct PublishOk { - pub request_id: Option, pub forward: bool, /// SUBSCRIBER_PRIORITY, `None` when absent: the subscription keeps the one it had. pub subscriber_priority: Option, @@ -411,25 +304,7 @@ impl Message for PublishOk { const ID: u64 = 0x1E; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - self.request_id - .expect("request_id required for draft14-16") - .encode(w, version)?; - } else { - assert!(self.request_id.is_none(), "request_id must be None for draft17+"); - } - match version { - Version::Draft14 => { - w.bool(self.forward); - w.u8(self.subscriber_priority.unwrap_or(super::priority::DEFAULT_WIRE)); - self.group_order.unwrap_or(GroupOrder::Descending).encode(w, version)?; - // Same as SUBSCRIBE: the Location an absolute filter carries is dropped on - // decode, so encoding one would truncate the message. - self.filter.unwrap_or(Filter::Unfiltered).encode(w, version)?; - // no parameters - w.u8(0); - } // Draft-20 moved the subscription parameters out of PUBLISH_OK; they belong to // PUBLISH and REQUEST_UPDATE now, so a PUBLISH_OK carries none of them. _ if Filter::is_draft20(version) => encode_params!(w, version,), @@ -438,13 +313,13 @@ impl Message for PublishOk { // on the wrong message, which closes the session. let group_order = matches!( version, - Version::Draft15 | Version::Draft16 | Version::Draft17 | Version::Draft18 + Version::Draft18 ) .then_some(self.group_order) .flatten(); let new_group = matches!( version, - Version::Draft16 | Version::Draft17 | Version::Draft18 | Version::Draft19 + Version::Draft18 | Version::Draft19 ) .then_some(self.new_group) .flatten(); @@ -462,84 +337,27 @@ impl Message for PublishOk { } fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - Some(RequestId::decode(r, version)?) - } else { - None - }; - - match version { - Version::Draft14 => { - let forward = r.bool()?; - let subscriber_priority = r.u8()?; - let group_order = GroupOrder::decode(r, version)?; - let filter = Filter::decode(r, version)?; - - // no parameters - Parameters::skip_response(r)?; - - Ok(Self { - request_id, - forward, - subscriber_priority: Some(subscriber_priority), - group_order: Some(group_order), - filter: Some(filter), - new_group: None, - }) - } - _ => { - // EXPIRES is ignored, as in SUBSCRIBE_OK. Delivery timeouts are legal on the - // drafts that list them and dropped: nothing here acts on a publisher's timeout. Draft-20 moved the - // subscription parameters off PUBLISH_OK, and draft-19 already dropped - // GROUP_ORDER. - decode_params!(r, version, - 0x02 => _delivery_timeout: Option where !Filter::is_draft20(version), - 0x06 => _subgroup_delivery_timeout: Option where matches!(version, Version::Draft18 | Version::Draft19), - 0x08 => _expires: Option, - 0x10 => forward: Option where !Filter::is_draft20(version), - 0x20 => subscriber_priority: Option where !Filter::is_draft20(version), - 0x21 => filter: Option where !Filter::is_draft20(version), - 0x22 => group_order: Option where matches!(version, Version::Draft15 | Version::Draft16 | Version::Draft17 | Version::Draft18), - 0x32 => new_group: Option where matches!(version, Version::Draft16 | Version::Draft17 | Version::Draft18 | Version::Draft19), - ); - - Ok(Self { - request_id, - forward: forward.unwrap_or(true), - subscriber_priority, - group_order, - filter, - new_group, - }) - } - } - } -} - -#[derive(Debug)] -pub struct PublishError<'a> { - pub request_id: RequestId, - pub error_code: u64, - pub reason_phrase: Cow<'a, str>, -} -impl Message for PublishError<'_> { - const ID: u64 = 0x1F; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - self.request_id.encode(w, version)?; - w.varint(self.error_code)?; - w.string(&self.reason_phrase)?; - Ok(()) - } + // EXPIRES is ignored, as in SUBSCRIBE_OK. Delivery timeouts are legal on the drafts + // that list them and dropped: nothing here acts on a publisher's timeout. Draft-20 + // moved the subscription parameters off PUBLISH_OK, and draft-19 already dropped + // GROUP_ORDER. + decode_params!(r, version, + 0x02 => _delivery_timeout: Option where !Filter::is_draft20(version), + 0x06 => _subgroup_delivery_timeout: Option where matches!(version, Version::Draft18 | Version::Draft19), + 0x08 => _expires: Option, + 0x10 => forward: Option where !Filter::is_draft20(version), + 0x20 => subscriber_priority: Option where !Filter::is_draft20(version), + 0x21 => filter: Option where !Filter::is_draft20(version), + 0x22 => group_order: Option where matches!(version, Version::Draft18), + 0x32 => new_group: Option where matches!(version, Version::Draft18 | Version::Draft19), + ); - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = RequestId::decode(r, version)?; - let error_code = r.varint()?; - let reason_phrase = Cow::Owned(r.string()?); Ok(Self { - request_id, - error_code, - reason_phrase, + forward: forward.unwrap_or(true), + subscriber_priority, + group_order, + filter, + new_group, }) } } @@ -663,7 +481,6 @@ mod tests { #[test] fn test_publish_ok_v14_round_trip() { let msg = PublishOk { - request_id: Some(RequestId(7)), forward: true, subscriber_priority: Some(128), group_order: Some(GroupOrder::Descending), @@ -682,7 +499,6 @@ mod tests { #[test] fn test_publish_ok_v15_round_trip() { let msg = PublishOk { - request_id: Some(RequestId(7)), forward: true, subscriber_priority: Some(128), group_order: Some(GroupOrder::Descending), @@ -730,7 +546,6 @@ mod tests { #[test] fn test_publish_ok_v17_round_trip() { let msg = PublishOk { - request_id: None, forward: true, subscriber_priority: Some(128), group_order: Some(GroupOrder::Descending), @@ -750,9 +565,6 @@ mod tests { let mut buf = Vec::new(); let w = &mut Encoder::new(&mut buf, version.into()); RequestId(1).encode(w, version).unwrap(); - if version == Version::Draft17 { - w.varint(0).unwrap(); - } encode_namespace(w, &Path::new("t")).unwrap(); w.string("v").unwrap(); w.varint(1).unwrap(); @@ -765,9 +577,6 @@ mod tests { fn publish_ok_with_param(version: Version, key: u64, value: u64) -> Vec { let mut buf = Vec::new(); let w = &mut Encoder::new(&mut buf, version.into()); - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - RequestId(1).encode(w, version).unwrap(); - } w.varint(1).unwrap(); w.varint(key).unwrap(); w.varint(value).unwrap(); @@ -801,10 +610,6 @@ mod tests { #[test] fn test_publish_done_registered_statuses_all_versions() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, ] { @@ -812,12 +617,10 @@ mod tests { (PublishDoneStatus::InternalError, 0x0), (PublishDoneStatus::TrackEnded, 0x2), ] { - assert_eq!(status.code(version), expected); + assert_eq!(status.code(), expected); let msg = PublishDone { - request_id: matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then_some(RequestId(7)), - status_code: status.code(version), + status_code: status.code(), stream_count: 5, reason_phrase: "done".into(), }; @@ -836,13 +639,12 @@ mod tests { #[test] fn publish_done_end_follows_the_status() { let done = |status_code| PublishDone { - request_id: None, status_code, stream_count: 0, reason_phrase: "".into(), }; - for version in [Version::Draft14, Version::Draft19, Version::Draft20, Version::Draft22] { + for version in [Version::Draft19, Version::Draft20, Version::Draft22] { assert!(done(0x2).end(version).is_ok(), "{version:?}"); assert!( matches!(done(0x1).end(version), Err(crate::Error::Unauthorized)), @@ -894,10 +696,6 @@ mod tests { #[test] fn publish_priority_property_bytes_on_every_draft() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -919,18 +717,14 @@ mod tests { let baseline = encode_message(&msg, version); msg.properties.priority = Some(37); let encoded = encode_message(&msg, version); - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - assert_eq!(encoded, baseline, "{version}"); - } else { - assert_eq!(encoded, [baseline, vec![0x0e, 37]].concat(), "{version}"); - assert_eq!( - decode_message::(&encoded, version) - .unwrap() - .properties - .priority, - Some(37) - ); - } + assert_eq!(encoded, [baseline, vec![0x0e, 37]].concat(), "{version}"); + assert_eq!( + decode_message::(&encoded, version) + .unwrap() + .properties + .priority, + Some(37) + ); } } @@ -1048,7 +842,6 @@ mod tests { #[test] fn test_publish_ok_v18_round_trip() { let msg = PublishOk { - request_id: None, forward: true, subscriber_priority: Some(128), group_order: Some(GroupOrder::Descending), @@ -1092,7 +885,6 @@ mod tests { fn test_publish_ok_carries_new_group_request() { for version in [Version::Draft18, Version::Draft19] { let msg = PublishOk { - request_id: None, forward: true, subscriber_priority: None, group_order: None, @@ -1108,7 +900,6 @@ mod tests { fn test_publish_done_preserves_unknown_status() { const UNKNOWN: u64 = 0xface; let msg = PublishDone { - request_id: None, status_code: UNKNOWN, stream_count: 5, reason_phrase: "extension".into(), diff --git a/rs/moq-net/src/ietf/publish_namespace.rs b/rs/moq-net/src/ietf/publish_namespace.rs index d0e6ffa3996..8e2a352c76f 100644 --- a/rs/moq-net/src/ietf/publish_namespace.rs +++ b/rs/moq-net/src/ietf/publish_namespace.rs @@ -1,6 +1,5 @@ //! IETF moq-transport-14 publish namespace messages -use std::borrow::Cow; use crate::{Epoch, Path, coding::*, ietf::RequestId}; @@ -65,9 +64,6 @@ impl PublishNamespace<'_> { /// which surfaces here as [`DecodeError::InvalidValue`]. pub fn decode_body(r: &mut Decoder<'_>, version: Version, negotiated: Negotiated) -> Result { let request_id = RequestId::decode(r, version)?; - if version == Version::Draft17 { - let _required_request_id_delta = r.varint()?; - } let track_namespace = decode_namespace(r)?; decode_params!(r, version, @@ -97,9 +93,6 @@ impl Message for PublishNamespace<'_> { fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { self.request_id.encode(w, version)?; - if version == Version::Draft17 { - w.varint(0)?; // required_request_id_delta = 0 (draft-17 only, removed in draft-18 per #1615) - } encode_namespace(w, &self.track_namespace)?; // ROUTE_COST is optional and absent means 0, so a free path sends nothing. let hops = self.params.cluster.as_ref().map(|advert| advert.hops.clone()); @@ -146,14 +139,11 @@ fn cluster_params( /// /// An omitted parameter keeps its value (moq-transport Section 9.5), so a cost that /// dropped to 0 is sent as an explicit 0, unlike the advertisement itself where absent -/// means 0. Draft-16+: the cluster extension negotiates on draft-17+ only, so draft-16 -/// carries only the token. +/// means 0. #[derive(Clone, Debug, PartialEq, Eq)] pub struct PublishNamespaceUpdate { /// The update's own Request ID; every REQUEST_UPDATE consumes one. pub request_id: RequestId, - /// Draft-16 only: the PUBLISH_NAMESPACE this updates, which later drafts name by stream. - pub namespace_request_id: Option, /// HOP_PATH, when the route changed. pub hops: Option, /// ROUTE_COST, when the cost changed. @@ -168,7 +158,6 @@ impl PublishNamespaceUpdate { pub fn between(request_id: RequestId, held: &cluster::Advert, next: &cluster::Advert) -> Self { Self { request_id, - namespace_request_id: None, hops: (held.hops != next.hops).then(|| next.hops.clone()), cost: (held.cost != next.cost).then_some(next.cost), authorization_token: None, @@ -188,20 +177,7 @@ impl Message for PublishNamespaceUpdate { const ID: u64 = 0x02; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - match version { - Version::Draft14 | Version::Draft15 => return Err(EncodeError::Version), - Version::Draft16 => { - self.request_id.encode(w, version)?; - self.namespace_request_id - .ok_or(EncodeError::Version)? - .encode(w, version)?; - } - Version::Draft17 => { - self.request_id.encode(w, version)?; - w.varint(0)?; // required_request_id_delta = 0 (draft-17 only, removed in draft-18 per #1615) - } - _ => self.request_id.encode(w, version)?, - } + self.request_id.encode(w, version)?; encode_params!(w, version, 0x03 => self.authorization_token.clone(), cluster::HOP_PATH => self.hops, @@ -211,16 +187,7 @@ impl Message for PublishNamespaceUpdate { } fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let (request_id, namespace_request_id) = match version { - Version::Draft14 | Version::Draft15 => return Err(DecodeError::Version), - Version::Draft16 => (RequestId::decode(r, version)?, Some(RequestId::decode(r, version)?)), - Version::Draft17 => { - let request_id = RequestId::decode(r, version)?; - let _required_request_id_delta = r.varint()?; - (request_id, None) - } - _ => (RequestId::decode(r, version)?, None), - }; + let request_id = RequestId::decode(r, version)?; decode_params!(r, version, 0x03 => authorization_token: Vec, cluster::HOP_PATH => hops: Option, @@ -229,7 +196,6 @@ impl Message for PublishNamespaceUpdate { let authorization_token = super::token::single(authorization_token)?; Ok(Self { request_id, - namespace_request_id, hops, cost, authorization_token, @@ -276,161 +242,6 @@ pub(super) fn decode_advert_params( }) } -/// PublishNamespaceOk message (0x07) -#[derive(Clone, Debug)] -pub struct PublishNamespaceOk { - pub request_id: RequestId, -} - -impl Message for PublishNamespaceOk { - const ID: u64 = 0x07; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - self.request_id.encode(w, version)?; - Ok(()) - } - - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = RequestId::decode(r, version)?; - Ok(Self { request_id }) - } -} - -/// PublishNamespaceError message (0x08) -#[derive(Clone, Debug)] -pub struct PublishNamespaceError<'a> { - pub request_id: RequestId, - pub error_code: u64, - pub reason_phrase: Cow<'a, str>, -} - -impl Message for PublishNamespaceError<'_> { - const ID: u64 = 0x08; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - self.request_id.encode(w, version)?; - w.varint(self.error_code)?; - w.string(&self.reason_phrase)?; - Ok(()) - } - - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = RequestId::decode(r, version)?; - let error_code = r.varint()?; - let reason_phrase = Cow::Owned(r.string()?); - - Ok(Self { - request_id, - error_code, - reason_phrase, - }) - } -} - -/// PublishNamespaceDone message (0x09) -/// v14/v15: uses track_namespace. v16: uses request_id. -#[derive(Clone, Debug)] -pub struct PublishNamespaceDone<'a> { - /// v14/v15: the namespace being unannounced - pub track_namespace: Path<'a>, - /// v16: the request ID of the original PublishNamespace - pub request_id: RequestId, -} - -impl Message for PublishNamespaceDone<'_> { - const ID: u64 = 0x09; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - match version { - Version::Draft14 | Version::Draft15 => { - encode_namespace(w, &self.track_namespace)?; - } - Version::Draft16 => { - self.request_id.encode(w, version)?; - } - _ => return Err(EncodeError::Version), - } - Ok(()) - } - - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - match version { - Version::Draft14 | Version::Draft15 => { - let track_namespace = decode_namespace(r)?; - Ok(Self { - track_namespace, - request_id: RequestId(0), - }) - } - Version::Draft16 => { - let request_id = RequestId::decode(r, version)?; - Ok(Self { - track_namespace: Path::default(), - request_id, - }) - } - _ => Err(DecodeError::Version), - } - } -} - -/// PublishNamespaceCancel message (0x0c) -/// v14/v15: uses track_namespace. v16: uses request_id. -#[derive(Clone, Debug)] -pub struct PublishNamespaceCancel<'a> { - /// v14/v15: the namespace being cancelled - pub track_namespace: Path<'a>, - /// v16: the request ID of the original PublishNamespace - pub request_id: RequestId, - pub error_code: u64, - pub reason_phrase: Cow<'a, str>, -} - -impl Message for PublishNamespaceCancel<'_> { - const ID: u64 = 0x0c; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - match version { - Version::Draft14 | Version::Draft15 => { - encode_namespace(w, &self.track_namespace)?; - } - Version::Draft16 => { - self.request_id.encode(w, version)?; - } - _ => { - return Err(EncodeError::Version); - } - } - w.varint(self.error_code)?; - w.string(&self.reason_phrase)?; - Ok(()) - } - - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let (track_namespace, request_id) = match version { - Version::Draft14 | Version::Draft15 => { - let track_namespace = decode_namespace(r)?; - (track_namespace, RequestId(0)) - } - Version::Draft16 => { - let request_id = RequestId::decode(r, version)?; - (Path::default(), request_id) - } - _ => { - return Err(DecodeError::Version); - } - }; - let error_code = r.varint()?; - let reason_phrase = Cow::Owned(r.string()?); - Ok(Self { - track_namespace, - request_id, - error_code, - reason_phrase, - }) - } -} - #[cfg(test)] mod tests { use super::*; @@ -635,7 +446,7 @@ mod tests { assert_eq!(msg.hops, None, "an unchanged path is omitted"); assert_eq!(msg.cost, Some(0), "a cost of 0 is sent explicitly"); - for version in [Version::Draft17, Version::Draft18, Version::Draft22] { + for version in [Version::Draft18, Version::Draft22] { let encoded = encode_message(&msg, version); let decoded: PublishNamespaceUpdate = decode_message(&encoded, version).unwrap(); assert_eq!(decoded, msg, "{version}"); @@ -668,7 +479,7 @@ mod tests { value: b"jwt".to_vec(), })), }; - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { let mut buf = Vec::new(); assert!( msg.encode_msg(&mut Encoder::new(&mut buf, version.into()), version) diff --git a/rs/moq-net/src/ietf/publisher.rs b/rs/moq-net/src/ietf/publisher.rs index 8eef9575a41..c20a1423022 100644 --- a/rs/moq-net/src/ietf/publisher.rs +++ b/rs/moq-net/src/ietf/publisher.rs @@ -55,7 +55,7 @@ fn track_properties( version: Version, negotiated: bool, ) -> Result { - let compressed = info.compress && negotiated && super::flate::supported(version); + let compressed = info.compress && negotiated; super::flate::egress(info, compressed)?; let mut plain = info.clone(); plain.compress = compressed; @@ -67,14 +67,10 @@ fn track_properties( return Ok(ietf::Properties::restated(info)); }; let mut unknown = track.values.clone(); - let block = !matches!(version, Version::Draft14 | Version::Draft15); - if block && track.immutable().is_some() && !super::object::same_bytes(track.version, version) { - return Err(Error::Unsupported); - } // Across draft-19, forwarding 0x06 would give it a different meaning, as for object // properties. A mutable one is dropped; a sealed one is refused. if super::object::has_delivery_timeout(track.version) != super::object::has_delivery_timeout(version) { - if block && track.sealed().any(|id| id == 0x06) { + if track.sealed().any(|id| id == 0x06) { return Err(Error::Unsupported); } unknown.retain(|property| property.id != 0x06); @@ -146,7 +142,7 @@ struct FetchObjectInfo<'a> { source: Option, } -fn fetch_prior(version: Version, prior: Option, object: u64) -> Result { +fn fetch_prior(prior: Option, object: u64) -> Result { let Some(prior) = prior else { return Ok(FetchPrior::None); }; @@ -154,10 +150,7 @@ fn fetch_prior(version: Version, prior: Option, object: u64) -> Result 0) .ok_or(Error::MalformedTrack)?; - Ok(FetchPrior::Same((step != 1).then_some(match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 => object, - _ => step, - }))) + Ok(FetchPrior::Same((step != 1).then_some(step))) } impl FetchedGroup { @@ -416,23 +409,6 @@ impl Target { } } - /// Ready when the peer ends this loop by closing the stream it asked on. - /// - /// An unsolicited loop has no stream of its own to watch and parks here: the - /// session driver polling it is what drops it when the session ends. - fn poll_closed( - &mut self, - finished: &mut bool, - goaways: &mut super::request_stream::Messages, - version: Version, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - match self.stream() { - Some(stream) => super::request_stream::poll_cancel(stream, finished, goaways, version, cx), - None => Poll::Pending, - } - } - /// `goaways` checks each GOAWAY the requester sends on this stream. fn poll_event( &mut self, @@ -443,14 +419,6 @@ impl Target { ) -> Poll { use super::request_stream::{NamespaceUpdate, Next}; - if matches!( - version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 - ) { - return self - .poll_closed(finished, goaways, version, cx) - .map(NamespaceEvent::Closed); - } let Some(stream) = self.stream() else { return Poll::Pending; }; @@ -544,12 +512,12 @@ enum Served { /// The track and any fill finished, or serving ended with this error. Track(Result<(), Error>), /// An update could not be applied, its parameters unsupported or its renewal refused - /// (drafts 16 section 9.11.1, 18 section 10.9.1). The update was already answered, and - /// PUBLISH_DONE says UPDATE_FAILED, or on draft 14 the refusal itself. + /// (draft-18 section 10.9.1). The update was already answered, and PUBLISH_DONE says + /// UPDATE_FAILED. UpdateFailed(Error), - /// FORWARD=0 paused the subscription, which stays established. The update setting it, if - /// any, is not answered yet; none when a SUBSCRIBE started paused. - Paused(Option), + /// FORWARD=0 paused the subscription, which stays established. True when an update set + /// it and is not answered yet, false when a SUBSCRIBE started paused. + Paused(bool), } /// What reading a subscriber's next message on its request stream found. @@ -576,11 +544,8 @@ enum Subscribed { impl Served { /// The PUBLISH_DONE status and reason this end reports. - fn status(&self, version: Version) -> (ietf::PublishDoneStatus, &'static str) { + fn status(&self) -> (ietf::PublishDoneStatus, &'static str) { match self { - // Draft 14 neither answers an update nor registers UPDATE_FAILED, so the - // PUBLISH_DONE is the only place the subscriber learns why its renewal failed. - Self::UpdateFailed(err) if version == Version::Draft14 => Self::Track(Err(err.clone())).status(version), Self::Track(Ok(())) => (ietf::PublishDoneStatus::TrackEnded, "track ended"), Self::Track(Err( Error::Unauthorized @@ -787,16 +752,12 @@ impl Publisher { self } - /// What the peer declared in its SETUP, or the default (extension off) on a version - /// that cannot negotiate it. + /// What the peer declared in its SETUP. /// /// Blocks until the peer's SETUP arrives, because the extension changes the NAMESPACE /// encoding: nothing can be advertised until we know whether the peer speaks it. fn poll_peer(&self, waiter: &kio::Waiter) -> Poll { - match cluster::supported(self.version) { - true => self.peer_setup.poll_get(waiter).map(|peer| peer.cluster), - false => Poll::Ready(cluster::Peer::default()), - } + self.peer_setup.poll_get(waiter).map(|peer| peer.cluster) } /// Await [`Self::poll_peer`]. @@ -911,7 +872,7 @@ impl Publisher { if !data.is_empty() { return Err(Error::WrongSize); } - return Ok(this.reject_token(stream, request::Kind::Subscribe, body.clone(), err, serve)); + return Ok(this.reject_token(stream, request::Kind::Subscribe, err, serve)); } msg => msg?, }; @@ -943,7 +904,7 @@ impl Publisher { if !data.is_empty() { return Err(Error::WrongSize); } - return Ok(this.reject_token(stream, request::Kind::Fetch, body.clone(), err, serve)); + return Ok(this.reject_token(stream, request::Kind::Fetch, err, serve)); } msg => msg?, }; @@ -963,47 +924,21 @@ impl Publisher { } .maybe_boxed() } - // Draft-18 SUBSCRIBE_NAMESPACE (0x50) and the legacy 0x11 message decode - // to the same request_id + namespace. We never send PUBLISH, so a legacy - // request for PUBLISH alone is refused, and one for both gets only NAMESPACE. - ietf::SubscribeNamespace::ID | ietf::SubscribeNamespaceLegacy::ID => { - let decoded = if id == ietf::SubscribeNamespace::ID { - ietf::SubscribeNamespace::decode_msg(&mut data, this.version) - .map(|msg| (msg, ietf::SubscribeOptions::Namespace)) - } else { - ietf::SubscribeNamespaceLegacy::decode_msg(&mut data, this.version).map(|legacy| { - let msg = ietf::SubscribeNamespace { - request_id: legacy.request_id, - namespace: legacy.namespace, - hidden: legacy.hidden, - }; - (msg, legacy.subscribe_options) - }) - }; - let (msg, options) = match decoded { + ietf::SubscribeNamespace::ID => { + let msg = match ietf::SubscribeNamespace::decode_msg(&mut data, this.version) { Err(err) if super::token::refuses_request(&err) => { // Junk after the fields is fatal, which a token refusal must not hide. if !data.is_empty() { return Err(Error::WrongSize); } let kind = request::Kind::SubscribeNamespace; - return Ok(this.reject_token(stream, kind, body.clone(), err, serve)); + return Ok(this.reject_token(stream, kind, err, serve)); } decoded => decoded?, }; if !data.is_empty() { return Err(Error::WrongSize); } - if options == ietf::SubscribeOptions::Publish { - let request_id = msg.request_id; - return Ok(async move { - let reason = "SUBSCRIBE_NAMESPACE for PUBLISH is not supported"; - if let Err(err) = this.reject_namespace_request(stream, request_id, reason).await { - tracing::debug!(%err, "subscribe_namespace refusal failed"); - } - } - .maybe_boxed()); - } tracing::debug!(message = ?msg, "received subscribe_namespace"); let gate = crate::auth::Gate::new( this.auth.clone(), @@ -1024,7 +959,7 @@ impl Publisher { } .maybe_boxed() } - ietf::SubscribeTracks::ID if super::subscribe_tracks::has_subscribe_tracks(this.version) => { + ietf::SubscribeTracks::ID => { let msg = match ietf::SubscribeTracks::decode_msg(&mut data, this.version) { Err(err) if super::token::refuses_request(&err) => { // Junk after the fields is fatal, which a token refusal must not hide. @@ -1032,7 +967,7 @@ impl Publisher { return Err(Error::WrongSize); } let kind = request::Kind::SubscribeNamespace; - return Ok(this.reject_token(stream, kind, body.clone(), err, serve)); + return Ok(this.reject_token(stream, kind, err, serve)); } msg => msg?, }; @@ -1054,7 +989,7 @@ impl Publisher { if !data.is_empty() { return Err(Error::WrongSize); } - return Ok(this.reject_token(stream, request::Kind::TrackStatus, body.clone(), err, serve)); + return Ok(this.reject_token(stream, request::Kind::TrackStatus, err, serve)); } msg => msg?, }; @@ -1091,14 +1026,13 @@ impl Publisher { &self, stream: Stream, kind: request::Kind, - body: ietf::Body, err: DecodeError, serve: Option, ) -> MaybeSendBox<'static, ()> { let version = self.version; async move { let _serve = serve; - if let Err(err) = super::token::reject(stream, kind, body, err, version).await { + if let Err(err) = super::token::reject(stream, kind, err, version).await { tracing::debug!(%err, "token refusal failed"); } } @@ -1142,7 +1076,6 @@ impl Publisher { Some(subscribed) => Some(subscribed), None => { let error = ietf::RequestError { - request_id: None, error_code: request::DUPLICATE_SUBSCRIPTION, reason_phrase: "the track is PUBLISHed to this session".into(), retry_interval: 0, @@ -1179,7 +1112,7 @@ impl Publisher { }; if token.is_none() && !gate.allows() { let err = Error::Unauthorized; - return self.reject_subscribe(stream, request_id, &err, "not granted").await; + return self.reject_subscribe(stream, &err, "not granted").await; } // Legal requests we can't honor are refused one at a time, never by closing the // session. Serving a Range Filter unfiltered would deliver objects the subscriber @@ -1188,7 +1121,6 @@ impl Publisher { return super::request_stream::reject( stream, ietf::RequestError { - request_id: None, error_code: request::INVALID_FILTER, reason_phrase: "Range Filters exceed MAX_FILTER_RANGES=0".into(), retry_interval: 0, @@ -1234,7 +1166,7 @@ impl Publisher { Ok(grant) => (gate.named(), Some(grant)), Err(refusal) => { return self - .reject_subscribe(stream, request_id, &refusal.error, &refusal.reason) + .reject_subscribe(stream, &refusal.error, &refusal.reason) .await; } } @@ -1301,7 +1233,7 @@ impl Publisher { // Timestamp's units from TRACK_STATUS, so the opt-out strips nothing from the // objects. let timescale = track_serve.timescale; - let mut properties = match track_properties( + let properties = match track_properties( track_serve.track.info(), msg.properties_wanted, self.version, @@ -1312,9 +1244,6 @@ impl Publisher { }; // Through draft-15 this restates the order the subscription is served in. // Later drafts carry the publisher's default instead. - if matches!(self.version, Version::Draft14 | Version::Draft15) { - properties.group_order = Some(msg.group_order.to_order().into()); - } // Draft-20 replaced joining FETCH with subscription fills. Older drafts save // the same boundary used by the subscription so the two streams never overlap, @@ -1358,10 +1287,6 @@ impl Publisher { stream .writer .encode_message(&ietf::SubscribeOk { - request_id: match self.version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(request_id), - _ => None, - }, track_alias: request_id.0, // The subscription floor and FETCH/FILL cap use this same snapshot. largest: edge.largest, @@ -1394,12 +1319,12 @@ impl Publisher { false => { // Unpolled, it would still hold the track it serves from. drop(fill); - Some(Served::Paused(None)) + Some(Served::Paused(false)) } }; // A pause drops the subscription, and its demand with it; a resume subscribes // again as a new SUBSCRIBE would, so nothing carries across but the request. - while let Some(Served::Paused(update)) = served { + while let Some(Served::Paused(answer)) = served { // What an update changed while forwarding outlives the pause. let applied = track_serve.track.subscription(); msg.subscriber_priority = super::priority::to_wire(applied.priority); @@ -1407,21 +1332,19 @@ impl Publisher { track_serve.close_groups(); self.pause_join(_join.as_ref()); drop(track_serve); - if let Some(update) = update - && self.version != Version::Draft14 - { - self.write_request_ok(&mut stream.writer, update, None).await?; + if answer { + self.write_request_ok(&mut stream.writer, None).await?; } - let resume = match self + match self .wait_resume(&mut stream, &mut request_finished, &mut goaways, &mut msg) .await { - Ok(resume) => resume, + Ok(()) => {} Err(served) => { let served = served.unwrap_or(Served::Track(Ok(()))); return Ok(Subscribed::Served(served, opened.load(Ordering::Relaxed))); } - }; + } let resumed = match broadcast.track(&msg.track_name) { Ok(track) => self .subscribe_track(&mut stream, &track, &msg) @@ -1441,10 +1364,8 @@ impl Publisher { Ok(resumed) => resumed, // The resuming update is owed an answer, so it fails. Err(err) => { - if self.version != Version::Draft14 { - self.write_subscribe_error(&mut stream.writer, resume, &err, "resume failed") - .await?; - } + self.write_subscribe_error(&mut stream.writer, &err, "resume failed") + .await?; return Ok(Subscribed::Served( Served::UpdateFailed(err), opened.load(Ordering::Relaxed), @@ -1453,9 +1374,7 @@ impl Publisher { }; track_serve.opened = opened.clone(); save_join(edge.largest, &track, &track_serve.ranking); - if self.version != Version::Draft14 { - self.write_request_ok(&mut stream.writer, resume, edge.largest).await?; - } + self.write_request_ok(&mut stream.writer, edge.largest).await?; served = self .run_subscription( &mut stream, @@ -1507,7 +1426,7 @@ impl Publisher { let (served, streams) = match subscribed { Ok(Ok(Subscribed::Served(served, streams))) => (served, streams), Ok(Ok(Subscribed::Refused(reason, err))) => { - return self.reject_subscribe(stream, request_id, &err, &reason).await; + return self.reject_subscribe(stream, &err, &reason).await; } // Nobody is left to answer. Ok(Ok(Subscribed::Withdrawn)) => { @@ -1519,12 +1438,12 @@ impl Publisher { Some(opened) => (Served::Track(Err(refusal.error)), opened.load(Ordering::Relaxed)), None => { return self - .reject_subscribe(stream, request_id, &refusal.error, &refusal.reason) + .reject_subscribe(stream, &refusal.error, &refusal.reason) .await; } }, }; - self.write_publish_done(stream, request_id, served, streams, Some(&credit)) + self.write_publish_done(stream, served, streams, Some(&credit)) .await } } @@ -1614,12 +1533,11 @@ impl Publisher { async fn write_publish_done( &self, mut stream: Stream, - request_id: RequestId, served: Served, streams: u64, credit: Option<&kio::Producer<()>>, ) -> Result<(), Error> { - let (status, reason) = served.status(self.version); + let (status, reason) = served.status(); let res = match served { Served::Track(res) => res, Served::UpdateFailed(err) => Err(err), @@ -1629,11 +1547,7 @@ impl Publisher { let _ = stream .writer .encode_message(&ietf::PublishDone { - request_id: match self.version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(request_id), - _ => None, - }, - status_code: status.code(self.version), + status_code: status.code(), stream_count: streams, reason_phrase: reason.into(), }) @@ -1705,15 +1619,14 @@ impl Publisher { if session.poll_closed(&mut cx).is_ready() { return Poll::Ready(Turn::Cancelled); } - if !matches!(self.version, Version::Draft14 | Version::Draft15 | Version::Draft16) - && stream.writer.poll_closed(&mut cx).is_ready() + if stream.writer.poll_closed(&mut cx).is_ready() { return Poll::Ready(Turn::Cancelled); } if let Poll::Ready(renewed) = renewal.poll(waiter) { return Poll::Ready(Turn::Renewal(renewed)); } - match self.poll_subscribe_update(stream, finished, goaways, &mut cx) { + match self.poll_update(stream, finished, goaways, &mut cx) { Poll::Ready(UpdateRead::Update(update)) => return Poll::Ready(Turn::Update(update)), Poll::Ready(UpdateRead::Cancelled) => return Poll::Ready(Turn::Cancelled), Poll::Ready(UpdateRead::Malformed(err)) => return Poll::Ready(Turn::Malformed(err)), @@ -1738,7 +1651,7 @@ impl Publisher { // Draft 14 answers no update. Drafts 15 and 16 answer on the control stream, // naming the update's own Request ID; later drafts name none. - let answer = self.version != Version::Draft14; + let answer = true; match turn { // A malformed update, such as one whose AUTHORIZATION TOKEN names an alias, @@ -1759,14 +1672,13 @@ impl Publisher { Turn::Renewal(request_update::Verdict { verdict, grant: res, - request_id: rid, }) => { // Only a token-authorized request starts a renewal. let rg = request_grant.as_ref().expect("a renewal without a request grant"); // On accept the old grant is dropped (ending the old token) and the deadline // re-armed at the new expiry. let Err(refusal) = rg.renew(verdict, res) else { - if answer && let Err(err) = self.write_request_ok(&mut stream.writer, rid, None).await { + if answer && let Err(err) = self.write_request_ok(&mut stream.writer, None).await { return Some(Served::Track(Err(err))); } continue; @@ -1776,7 +1688,7 @@ impl Publisher { // UPDATE_FAILED. The session stays up and the old grant does not survive. if answer && let Err(err) = self - .write_subscribe_error(&mut stream.writer, rid, &refusal.error, &refusal.reason) + .write_subscribe_error(&mut stream.writer, &refusal.error, &refusal.reason) .await { return Some(Served::Track(Err(err))); @@ -1892,8 +1804,8 @@ impl Publisher { Some(Ok((serve, edge))) } - /// Hold a paused SUBSCRIBE, answering its updates, until one resumes it: the Request ID of - /// that update, or how the request ended (`None` when cancelled). + /// Hold a paused SUBSCRIBE, answering its updates, until one resumes it, or return how the + /// request ended (`None` when cancelled). /// /// A paused subscription holds nothing upstream, so only its priority and max age change /// while it waits; anything else fails the update, ending the request. @@ -1903,7 +1815,7 @@ impl Publisher { finished: &mut bool, goaways: &mut super::request_stream::Messages, msg: &mut ietf::Subscribe<'_>, - ) -> Result> { + ) -> Result<(), Option> { let mut session = self.session.clone(); loop { let read = kio::wait(|waiter| { @@ -1911,12 +1823,11 @@ impl Publisher { if session.poll_closed(&mut cx).is_ready() { return Poll::Ready(UpdateRead::Cancelled); } - if !matches!(self.version, Version::Draft14 | Version::Draft15 | Version::Draft16) - && stream.writer.poll_closed(&mut cx).is_ready() + if stream.writer.poll_closed(&mut cx).is_ready() { return Poll::Ready(UpdateRead::Cancelled); } - self.poll_subscribe_update(stream, finished, goaways, &mut cx) + self.poll_update(stream, finished, goaways, &mut cx) }) .await; let update = match read { @@ -1945,16 +1856,13 @@ impl Publisher { None => None, }; if let Some(err) = refused { - if self.version != Version::Draft14 { - self.write_subscribe_error( - &mut stream.writer, - update.request_id, - &err, - "REQUEST_UPDATE parameters not supported while paused", - ) - .await - .map_err(|err| Some(Served::Track(Err(err))))?; - } + self.write_subscribe_error( + &mut stream.writer, + &err, + "REQUEST_UPDATE parameters not supported while paused", + ) + .await + .map_err(|err| Some(Served::Track(Err(err))))?; return Err(Some(Served::UpdateFailed(Error::Unsupported))); } if let Some(priority) = update.priority { @@ -1964,44 +1872,15 @@ impl Publisher { msg.max_age = Some(max_age); } if update.forward == Some(true) { - return Ok(update.request_id); + return Ok(()); } - if self.version != Version::Draft14 { - self.write_request_ok(&mut stream.writer, update.request_id, None) - .await - .map_err(|err| Some(Served::Track(Err(err))))?; - } - } - } - - /// Read a SUBSCRIBE's next REQUEST_UPDATE: from the control stream's follow-ups through - /// draft 16, where UNSUBSCRIBE cancels, or from its request stream after. - fn poll_subscribe_update( - &self, - stream: &mut Stream, - finished: &mut bool, - goaways: &mut super::request_stream::Messages, - cx: &mut std::task::Context<'_>, - ) -> Poll { - if *finished { - return Poll::Pending; - } - if !matches!(self.version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - return self.poll_update(stream, finished, goaways, cx); - } - use super::request_stream::{FollowUp, Update}; - match ready!(stream.reader.poll_decode_maybe::(cx)) { - Ok(Some(FollowUp::Update(body))) => Poll::Ready(match Update::decode_legacy(&body, self.version) { - Ok(update) => UpdateRead::Update(update), - Err(err) => UpdateRead::Malformed(err), - }), - // UNSUBSCRIBE, or the control stream going away. - Ok(Some(FollowUp::End) | None) => Poll::Ready(UpdateRead::Cancelled), - Err(err) => Poll::Ready(UpdateRead::Malformed(err)), + self.write_request_ok(&mut stream.writer, None) + .await + .map_err(|err| Some(Served::Track(Err(err))))?; } } - /// Read the requester's next REQUEST_UPDATE on a draft-17+ request stream, admitting its + /// Read the requester's next REQUEST_UPDATE on its request stream, admitting its /// Request ID. Pending once the requester finished sending where a FIN does not cancel. fn poll_update( &self, @@ -2057,7 +1936,7 @@ impl Publisher { ) -> Result<(), Served> { // Draft 14 answers no update. Drafts 15 and 16 answer on the control stream, naming the // update's own Request ID; later drafts name none. - let answer = self.version != Version::Draft14; + let answer = true; let failed = |err| Served::Track(Err(err)); // A token renews a token-authorized request. Where tokens decide, one arriving on a // request admitted without one is refused rather than taken unread, so every token this @@ -2098,7 +1977,7 @@ impl Publisher { }; if let Some((reason, err)) = refused { if answer { - self.write_subscribe_error(&mut stream.writer, update.request_id, &err, reason) + self.write_subscribe_error(&mut stream.writer, &err, reason) .await .map_err(failed)?; } @@ -2120,14 +1999,14 @@ impl Publisher { } // The caller stops forwarding, then answers. if update.forward == Some(false) { - return Err(Served::Paused(Some(update.request_id))); + return Err(Served::Paused(true)); } if let (Some(token), Some(grant)) = (update.authorization_token, request_grant) { - renewal.verify(grant.verify_renewal(&self.auth, token.0), update.request_id); + renewal.verify(grant.verify_renewal(&self.auth, token.0)); return Ok(()); } if answer { - self.write_request_ok(&mut stream.writer, update.request_id, None) + self.write_request_ok(&mut stream.writer, None) .await .map_err(failed)?; } @@ -2142,13 +2021,11 @@ impl Publisher { async fn write_request_ok( &self, writer: &mut Writer, - request_id: RequestId, largest: Option, ) -> Result<(), Error> { writer .encode_message(&ietf::RequestOk { largest, - request_id: matches!(self.version, Version::Draft15 | Version::Draft16).then_some(request_id), active: None, }) .await @@ -2163,11 +2040,10 @@ impl Publisher { async fn reject_subscribe( &self, mut stream: Stream, - request_id: RequestId, err: &Error, reason: &str, ) -> Result<(), Error> { - self.write_subscribe_error(&mut stream.writer, request_id, err, reason) + self.write_subscribe_error(&mut stream.writer, err, reason) .await?; // The peer dropping the stream once it has the rejection is a normal end, not our failure. @@ -2179,11 +2055,10 @@ impl Publisher { async fn write_subscribe_error( &self, writer: &mut Writer, - request_id: RequestId, err: &Error, reason: &str, ) -> Result<(), Error> { - super::request_stream::write_error(writer, request::Kind::Subscribe, request_id, err, reason, self.version) + super::request_stream::write_error(writer, request::Kind::Subscribe, err, reason, self.version) .await } @@ -2341,7 +2216,7 @@ impl Publisher { FetchObjectInfo { sequence, object: index, - prior: fetch_prior(version, prior, index)?, + prior: fetch_prior(prior, index)?, timestamp: frame.timestamp, inflated: frame.inflated, timescale, @@ -2351,7 +2226,7 @@ impl Publisher { }, ) .await?; - Self::write_fetch_payload(stream, frame.frame.payload, version).await?; + Self::write_fetch_payload(stream, frame.frame.payload).await?; prior = Some(index); } index += 1; @@ -2389,7 +2264,7 @@ impl Publisher { FetchObjectInfo { sequence, object: index, - prior: fetch_prior(version, prior, index)?, + prior: fetch_prior(prior, index)?, timestamp: frame.timestamp, inflated: frame.inflated, timescale, @@ -2403,9 +2278,6 @@ impl Publisher { index += 1; stream.varint(frame.size).await?; - if frame.size == 0 && matches!(version, Version::Draft14 | Version::Draft15) { - stream.varint(0).await?; - } loop { let chunk = { let mut closed = std::pin::pin!(stream.closed()); @@ -2453,7 +2325,7 @@ impl Publisher { } = header; // Relayed properties survive without a timescale. Plain frames generate a // timestamp only on drafts that can declare its units. - let timescale = timescale.filter(|_| ietf::Properties::sends_timescale(version)); + let timescale = timescale; let properties = if let Some(fields) = fields { let properties = super::object::write(&fields.properties, source, timestamp, version, timescale)?; (!properties.is_empty()).then_some(properties) @@ -2467,7 +2339,6 @@ impl Publisher { &mut Encoder::new(&mut properties, version.into()), timestamp, timescale, - version, )?; Some(properties) } @@ -2477,18 +2348,6 @@ impl Publisher { let properties = super::object::compression(properties.unwrap_or_default(), version, inflated)?; let properties = (!properties.is_empty()).then_some(properties); - if version == Version::Draft14 { - let properties = properties.unwrap_or_default(); - stream.buffer_varint(sequence)?; - stream.buffer_varint(0)?; - stream.buffer_varint(object)?; - // Publisher priority, a raw byte. - stream.buffer_raw(&[0]); - stream.buffer_varint(properties.len() as u64)?; - stream.buffer_raw(&properties); - std::future::poll_fn(|cx| stream.poll_flush(cx)).await?; - return Ok(()); - } let header = match prior { // The first object must carry its absolute Group and Object IDs. Include the @@ -2519,13 +2378,9 @@ impl Publisher { async fn write_fetch_payload( stream: &mut Writer, Version>, payload: bytes::Bytes, - version: Version, ) -> Result<(), Error> { stream.varint(payload.len() as u64).await?; // Draft-14 and 15 still carry a Normal status after an empty object. - if payload.is_empty() && matches!(version, Version::Draft14 | Version::Draft15) { - stream.varint(0).await?; - } if !payload.is_empty() { let mut payload = payload; stream.write_all(&mut payload).await?; @@ -2591,7 +2446,6 @@ impl Publisher { return super::request_stream::reject( stream, ietf::RequestError { - request_id: None, error_code: request::INVALID_FILTER, reason_phrase: "Range Filters exceed MAX_FILTER_RANGES=0".into(), retry_interval: 0, @@ -2606,7 +2460,6 @@ impl Publisher { return self .reject_fetch( stream, - msg.request_id, &Error::Unsupported, "FILL_TIMEOUT not supported", ) @@ -2627,7 +2480,7 @@ impl Publisher { // object plus one. Without one, both include the whole End Group. let Some(object) = end.object.map_or(Some(0), |object| object.checked_add(1)) else { return self - .reject_fetch(stream, msg.request_id, &Error::InvalidRange, "End Object overflows") + .reject_fetch(stream, &Error::InvalidRange, "End Object overflows") .await; }; FetchType::Standalone { @@ -2646,7 +2499,6 @@ impl Publisher { return self .reject_fetch( stream, - msg.request_id, &Error::Unsupported, "FETCH relative to Largest Object not supported", ) @@ -2675,7 +2527,7 @@ impl Publisher { }; if !gate.allows() { return self - .reject_fetch(stream, msg.request_id, &Error::Unauthorized, "not granted") + .reject_fetch(stream, &Error::Unauthorized, "not granted") .await; } @@ -2684,7 +2536,7 @@ impl Publisher { || (start.group == end.group && end.object != 0 && start.object >= end.object) { return self - .reject_fetch(stream, msg.request_id, &Error::InvalidRange, "empty range") + .reject_fetch(stream, &Error::InvalidRange, "empty range") .await; } @@ -2695,11 +2547,11 @@ impl Publisher { .await { Ok(broadcast) => broadcast, - Err(err) => return self.reject_fetch(stream, msg.request_id, &err, &err.to_string()).await, + Err(err) => return self.reject_fetch(stream, &err, &err.to_string()).await, }; let track = match broadcast.track(&track) { Ok(track) => track, - Err(err) => return self.reject_fetch(stream, msg.request_id, &err, &err.to_string()).await, + Err(err) => return self.reject_fetch(stream, &err, &err.to_string()).await, }; // Its own entry in the queue, or its own send group, so it competes with group @@ -2729,7 +2581,7 @@ impl Publisher { let (end, cache, namespace, ranking, token) = match self.joined(&mut stream, subscriber_request_id).await? { Ok(joined) => joined, - Err((err, reason)) => return self.reject_fetch(stream, msg.request_id, &err, reason).await, + Err((err, reason)) => return self.reject_fetch(stream, &err, reason).await, }; // The cache outlives the subscription, so the fetch holds its own gate and ends // with the subscription rather than trusting the cache. One the subscription's @@ -2744,7 +2596,6 @@ impl Publisher { return self .reject_fetch( stream, - msg.request_id, &Error::InvalidRange, "joining group past the subscription", ) @@ -2766,7 +2617,7 @@ impl Publisher { // Rewritten as standalone or refused above. FetchType::Filtered { .. } => { return self - .reject_fetch(stream, msg.request_id, &Error::Unsupported, "not supported") + .reject_fetch(stream, &Error::Unsupported, "not supported") .await; } }; @@ -2777,7 +2628,6 @@ impl Publisher { return self .reject_fetch( stream, - msg.request_id, &Error::Unsupported, "FETCH spanning several groups not supported", ) @@ -2825,21 +2675,19 @@ impl Publisher { }; let mut group = match group { Some(Ok(group)) => group, - Some(Err(err)) => return self.reject_fetch(stream, msg.request_id, &err, &err.to_string()).await, + Some(Err(err)) => return self.reject_fetch(stream, &err, &err.to_string()).await, None => return Ok(()), }; // Only draft-14 and 15 FETCH objects carry a status. Later drafts end the response at // the last frame, which is all a receiver can account for. - if !matches!(self.version, Version::Draft14 | Version::Draft15) { - group.end_object = None; - } - group.end_object = group.end_object.map(|end| status_fields(end, self.version)); + group.end_object = None; + group.end_object = group.end_object.map(|end| status_fields(end)); // FETCH keeps each object's Timestamp, in the units FETCH_OK declares wherever the // draft can declare them. Opting out of the properties strips nothing from the objects. let timescale = group .info .timescale - .filter(|_| ietf::Properties::sends_timescale(self.version)); +; let frames = group .frames .iter() @@ -2848,7 +2696,7 @@ impl Publisher { if let Err(err) = super::object::write(&fields.properties, group.info.ietf(), frame, self.version, timescale) { - return self.reject_fetch(stream, msg.request_id, &err, &err.to_string()).await; + return self.reject_fetch(stream, &err, &err.to_string()).await; } } let properties = match track_properties( @@ -2858,7 +2706,7 @@ impl Publisher { self.peer_setup.get().await.flate, ) { Ok(properties) => properties, - Err(err) => return self.reject_fetch(stream, msg.request_id, &err, &err.to_string()).await, + Err(err) => return self.reject_fetch(stream, &err, &err.to_string()).await, }; let draft20 = Filter::is_draft20(self.version); let end_of_track = !joined && group.complete && track.final_sequence() == group.sequence.checked_add(1); @@ -2885,7 +2733,6 @@ impl Publisher { return self .reject_fetch( stream, - msg.request_id, &Error::InvalidRange, "start past Largest Object", ) @@ -2897,7 +2744,7 @@ impl Publisher { // group has to be here in full or the two leave a gap. if group.end() != end.object { return self - .reject_fetch(stream, msg.request_id, &Error::Evicted, "joining prefix unavailable") + .reject_fetch(stream, &Error::Evicted, "joining prefix unavailable") .await; } (end, false) @@ -2946,15 +2793,6 @@ impl Publisher { stream .writer .encode_message(&ietf::FetchOk { - request_id: match self.version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(msg.request_id), - _ => None, - }, - // Only draft-14 encodes it, and only as the publisher restating the order. - group_order: match msg.group_order { - GroupOrder::Descending => GroupOrder::Descending, - _ => GroupOrder::Ascending, - }, end_of_track, end_location, properties, @@ -2979,7 +2817,7 @@ impl Publisher { for (ordinal, frame) in (group.first..).zip(group.frames) { kio::wait(|waiter| budget.poll_yield(waiter)).await; let object = frame.fields.as_ref().map_or(ordinal, |fields| fields.id); - let position = fetch_prior(self.version, prior, object)?; + let position = fetch_prior(prior, object)?; prior = Some(object); Self::write_fetch_object( &mut writer, @@ -2996,7 +2834,7 @@ impl Publisher { }, ) .await?; - Self::write_fetch_payload(&mut writer, frame.frame.payload, self.version).await?; + Self::write_fetch_payload(&mut writer, frame.frame.payload).await?; } if let Some(end) = group.end_object.as_deref() { Self::write_fetch_object( @@ -3004,7 +2842,7 @@ impl Publisher { FetchObjectInfo { sequence: group.sequence, object: end.id, - prior: fetch_prior(self.version, prior, end.id)?, + prior: fetch_prior(prior, end.id)?, timestamp: None, inflated: None, timescale, @@ -3112,24 +2950,13 @@ impl Publisher { })) => return Ok(Ok((end, cache, namespace, ranking, token))), Ok(None) => ( match self.version { - Version::Draft14 - | Version::Draft15 - | Version::Draft16 - | Version::Draft17 - | Version::Draft18 + Version::Draft18 | Version::Draft19 => Error::InvalidJoiningRequestId, _ => Error::NotFound, }, "no such subscription", ), Ok(Some(Joined::Unsupported)) => { - if matches!(self.version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - self.session.close( - super::error::to_session_code(&crate::SessionError::ProtocolViolation), - "joining FETCH requires Largest Object filter", - ); - return Err(Error::ProtocolViolation); - } (Error::Unsupported, "joining filter not supported") } Ok(Some(Joined::Empty)) => (Error::InvalidRange, "no objects at subscription start"), @@ -3166,10 +2993,8 @@ impl Publisher { if let Some(error) = super::namespace::refusal(&msg.track_namespace, self.version) { return super::request_stream::reject(stream, error).await; } - let request_id = msg.request_id; - if !gate.allows() { - return self.reject_track_status(stream, request_id, &Error::Unauthorized).await; + return self.reject_track_status(stream, &Error::Unauthorized).await; } let broadcast = match gate .guard(async { @@ -3182,12 +3007,12 @@ impl Publisher { .await { Ok(broadcast) => broadcast, - Err(err) => return self.reject_track_status(stream, request_id, &err).await, + Err(err) => return self.reject_track_status(stream, &err).await, }; let track = match broadcast.track(&msg.track_name) { Ok(track) => track, - Err(err) => return self.reject_track_status(stream, request_id, &err).await, + Err(err) => return self.reject_track_status(stream, &err).await, }; // The requester abandoning the request is owed nothing. @@ -3213,7 +3038,7 @@ impl Publisher { let properties = match info.map(|info| track_properties(&info?, msg.properties_wanted, self.version, negotiated)) { Some(Ok(properties)) => properties, - Some(Err(err)) => return self.reject_track_status(stream, request_id, &err).await, + Some(Err(err)) => return self.reject_track_status(stream, &err).await, None => return Ok(()), }; @@ -3221,16 +3046,12 @@ impl Publisher { // when the grant narrows is reset rather than delivered. let respond = async { let ok = ietf::TrackStatusOk { - request_id: match self.version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(request_id), - _ => None, - }, largest: live_edge(&track).largest, properties, }; stream .writer - .buffer_message(ietf::TrackStatusOk::id(self.version), &ok)?; + .buffer_message(ietf::TrackStatusOk::ID, &ok)?; std::future::poll_fn(|cx| stream.writer.poll_flush(cx)).await?; Ok::<(), Error>(()) }; @@ -3258,26 +3079,10 @@ impl Publisher { async fn reject_track_status( &self, mut stream: Stream, - request_id: RequestId, err: &Error, ) -> Result<(), Error> { let (kind, reason) = (request::Kind::TrackStatus, err.to_string()); - super::request_stream::write_error(&mut stream.writer, kind, request_id, err, &reason, self.version).await?; - let _ = stream.writer.close().await; - Ok(()) - } - - /// Refuse a SUBSCRIBE_NAMESPACE or SUBSCRIBE_TRACKS that asks for PUBLISH, which we - /// never send. Both exist only from draft-16, so the reply is always REQUEST_ERROR. - async fn reject_namespace_request( - &self, - mut stream: Stream, - request_id: RequestId, - reason: &str, - ) -> Result<(), Error> { - let kind = request::Kind::SubscribeNamespace; - let err = &Error::Unsupported; - super::request_stream::write_error(&mut stream.writer, kind, request_id, err, reason, self.version).await?; + super::request_stream::write_error(&mut stream.writer, kind, err, &reason, self.version).await?; let _ = stream.writer.close().await; Ok(()) } @@ -3287,12 +3092,11 @@ impl Publisher { async fn reject_fetch( &self, mut stream: Stream, - request_id: RequestId, err: &Error, reason: &str, ) -> Result<(), Error> { let kind = request::Kind::Fetch; - super::request_stream::write_error(&mut stream.writer, kind, request_id, err, reason, self.version).await?; + super::request_stream::write_error(&mut stream.writer, kind, err, reason, self.version).await?; let _ = stream.writer.close().await; Ok(()) } @@ -3347,8 +3151,7 @@ impl Publisher { let restart = renamed || held_auth != auth && (held_auth.is_none() - || auth.is_none() - || matches!(self.version, Version::Draft14 | Version::Draft15)); + || auth.is_none()); // An update already waiting or in flight carries whatever is current once it goes // out, and its answer brings the loop back here for anything after that. @@ -3403,7 +3206,7 @@ impl Publisher { .advertise_namespace(requests, watch, path, suffix.clone(), advert.params(), auth) .await?; if let Some(old) = old { - self.end_request(old.stream, &old.path, old.request_id).await; + end_request(old.stream); } } (true, Some(_)) => { @@ -3421,10 +3224,7 @@ impl Publisher { } } // Register an idle reader too: GOAWAY can arrive without an update in flight. - if !matches!( - self.version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 - ) && requests.contains_key(suffix) + if requests.contains_key(suffix) { woken.suffixes.lock().unwrap().push(suffix.clone()); } @@ -3511,7 +3311,7 @@ impl Publisher { tracing::debug!(broadcast = %self.origin.absolute(path), "no stream for the advertisement"); return Ok(Refused::No); }; - let request_id = self.control.next_request_id(&self.runtime).await?; + let request_id = self.control.next_request_id(); let mut messages = super::request_stream::Messages::new(self.session.clone(), self.control.client()); let sent = request @@ -3525,15 +3325,6 @@ impl Publisher { .await; let answer = match sent { Ok(()) => self.read_response(&mut request, &mut messages).await, - // A sequential-ID peer cannot accept any later request when this header failed - // before it learned the allocated ID. - Err(err) if matches!(self.version, Version::Draft14 | Version::Draft15) => { - self.session.clone().close( - super::error::to_session_code(&crate::SessionError::Internal), - "failed to send PUBLISH_NAMESPACE", - ); - return Err(err); - } Err(err) => Err(err), }; @@ -3542,9 +3333,8 @@ impl Publisher { Ok(Answer::Message(framed)) => framed, Ok(Answer::Late) => { tracing::debug!(broadcast = %absolute, "no answer to the advertisement"); - // The peer may hold the request it never answered. Dropping one riding the - // draft-14-16 control stream tells it nothing, so it is withdrawn instead. - self.end_request(request, path, request_id).await; + // The peer may hold the request it never answered, so it is withdrawn. + end_request(request); return Ok(Refused::No); } Ok(Answer::Ended) => { @@ -3560,18 +3350,8 @@ impl Publisher { // `None` when the peer holds the namespace. let refused = (|| { - match (self.version, type_id) { - (Version::Draft14, ietf::PublishNamespaceOk::ID) => { - let msg: ietf::PublishNamespaceOk = body.message(self.version)?; - tracing::debug!(message = ?msg, "publish namespace ok"); - } - (Version::Draft14, ietf::PublishNamespaceError::ID) => { - let msg: ietf::PublishNamespaceError = body.message(self.version)?; - tracing::warn!(message = ?msg, "publish namespace error"); - // Draft-14's error carries no retry interval, so our own backoff stands. - return Ok(Some(Refused::No)); - } - (_, ietf::RequestOk::ID) => { + match type_id { + ietf::RequestOk::ID => { let msg: ietf::RequestOk = body.message(self.version)?; // ACTIVE_COUNT only answers SUBSCRIBE_NAMESPACE (MoQ Active Count). if msg.active.is_some() { @@ -3579,7 +3359,7 @@ impl Publisher { } tracing::debug!(message = ?msg, "publish namespace ok"); } - (_, ietf::RequestError::ID) => { + ietf::RequestError::ID => { let msg: ietf::RequestError = body.message(self.version)?; tracing::warn!(message = ?msg, "publish namespace error"); return Ok(Some(self.refusal(msg.retry_interval))); @@ -3598,7 +3378,6 @@ impl Publisher { NamespaceRequest { messages, path: path.clone(), - request_id, stream: request, auth, queued: false, @@ -3697,7 +3476,6 @@ impl Publisher { (Some(next), Some(held)) => ietf::PublishNamespaceUpdate::between(request_id, &held, &next), _ => ietf::PublishNamespaceUpdate { request_id, - namespace_request_id: (self.version == Version::Draft16).then_some(request.request_id), hops: None, cost: None, authorization_token: None, @@ -3740,7 +3518,6 @@ impl Publisher { inflight: &mut VecDeque<(crate::time::Instant, crate::PathOwned)>, woken: &Arc, now: crate::time::Instant, - version: Version, cx: &mut std::task::Context<'_>, ) -> Poll<(crate::PathOwned, Result)> { // Registered before the take, so a wake landing between them is not lost. @@ -3750,13 +3527,6 @@ impl Publisher { let Some(request) = requests.get_mut(&suffix) else { continue; }; - if request.pending.is_none() - && matches!( - version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 - ) { - continue; - } let waker = request.waker.get_or_insert_with(|| woken.waker(suffix.clone())).clone(); if let Poll::Ready(answer) = request .messages @@ -3821,10 +3591,6 @@ impl Publisher { match self.version { // Dropping a request riding the control stream tells the peer nothing, so // it would hold the namespace beside the one advertised next. - Version::Draft16 => { - self.withdraw_namespace(&mut ns.target, &mut ns.requests, suffix.clone()) - .await? - } // Abrupt: a peer that never answers is not owed the FIN handshake. _ => { ns.requests.remove(&suffix); @@ -3868,29 +3634,11 @@ impl Publisher { ietf::RequestError::ID => { let msg: ietf::RequestError = body.message(self.version)?; tracing::warn!(message = ?msg, "publish_namespace update error"); - match self.version { - // The peer follows its refusal with PUBLISH_NAMESPACE_CANCEL, and draft - // 16 sends no PUBLISH_NAMESPACE_DONE after a cancel. - Version::Draft16 => { - ns.requests.remove(&suffix); - } - // The peer closed its side; closing ours completes the withdrawal. - _ => { - self.withdraw_namespace(&mut ns.target, &mut ns.requests, suffix.clone()) - .await? - } - } + // The peer closed its side; closing ours completes the withdrawal. + self.withdraw_namespace(&mut ns.target, &mut ns.requests, suffix.clone()) + .await?; self.refusal(msg.retry_interval) } - // Draft-16 carries its cancel as a message, which can beat the answer. - ietf::PublishNamespaceCancel::ID if self.version == Version::Draft16 => { - let msg: ietf::PublishNamespaceCancel = body.message(self.version)?; - tracing::debug!(broadcast = %absolute, message = ?msg, "update cancelled"); - // The peer already let it go, and draft 16 sends no - // PUBLISH_NAMESPACE_DONE after a cancel. - ns.requests.remove(&suffix); - Refused::No - } _ => return Err(Error::UnexpectedMessage), } } @@ -3906,15 +3654,13 @@ impl Publisher { /// How to read a refusal's retry interval, in milliseconds. /// - /// Draft-14/15 errors carry no interval, so a decoded 0 there says nothing and our own - /// backoff stands. Everywhere else 0 is the peer asking not to be offered this again, - /// which is what keeps a permanent refusal (unauthorized, uninterested) from becoming - /// a request every few seconds for the life of the session. + /// 0 is the peer asking not to be offered this again, which is what keeps a permanent + /// refusal (unauthorized, uninterested) from becoming a request every few seconds for + /// the life of the session. fn refusal(&self, retry_interval: u64) -> Refused { - match (self.version, retry_interval) { - (Version::Draft14 | Version::Draft15, _) => Refused::No, - (_, 0) => Refused::Never, - (_, ms) => Refused::Until(self.runtime.now() + Duration::from_millis(ms)), + match retry_interval { + 0 => Refused::Never, + ms => Refused::Until(self.runtime.now() + Duration::from_millis(ms)), } } @@ -3941,11 +3687,7 @@ impl Publisher { kio::wait(|waiter| { if let Poll::Ready(res) = waiter.poll_future(open.as_mut()) { return Poll::Ready(match res { - Ok(stream) => Ok(Some(super::request_stream::with_stats( - stream, - self.origin.stats(), - self.version, - ))), + Ok(stream) => Ok(Some(stream.with_stats(self.origin.stats()))), Err(Error::NoStreamCredit) => Ok(None), Err(err) => Err(err), }); @@ -3996,8 +3738,7 @@ impl Publisher { match target { Target::Requests(_) => { if let Some(request) = requests.remove(&suffix) { - self.end_request(request.stream, &request.path, request.request_id) - .await; + end_request(request.stream); } } Target::Inline(stream) => { @@ -4012,34 +3753,6 @@ impl Publisher { Ok(()) } - /// Withdraw one PUBLISH_NAMESPACE request, best effort: the peer may already be gone. - async fn end_request( - &self, - mut stream: Stream, - path: &crate::PathOwned, - request_id: RequestId, - ) { - // Draft-17+ withdraws by cancelling the request in both directions; a FIN is only - // allowed to end SUBSCRIBE_NAMESPACE and SUBSCRIBE_TRACKS. - if !matches!(self.version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - stream.reader.abort(&Error::Cancel); - stream.writer.abort(&Error::Cancel); - return; - } - - let _ = stream - .writer - .encode_message(&ietf::PublishNamespaceDone { - track_namespace: path.as_path(), - request_id, - }) - .await; - - // The withdrawal rides this request's own stream, which drops with it, so it needs - // the acknowledgement before the drop-time reset can discard it. - let _ = stream.writer.close().await; - } - /// Close out every open PUBLISH_NAMESPACE request. A no-op for a loop whose entries /// ride the SUBSCRIBE_NAMESPACE stream itself, which retracts them by ending. async fn withdraw_requests(&self, target: &mut Target, requests: &mut HashMap) { @@ -4146,21 +3859,8 @@ impl Publisher { // origin that already opted in (the caller's choice for this peer) keeps them. let origin = origin.discovery(!declared.hidden || msg.hidden || self.origin.includes_hidden()); - // Draft-16 and later always fill this stream. A NAMESPACE is discovery, not a - // route, so a peer that also hears the unsolicited PUBLISH_NAMESPACE still gets - // the match here. Draft-14 and 15 answer with PUBLISH_NAMESPACE and only say - // what that loop does not already say: everything when the peer asked to be - // told on request, otherwise the hidden remainder. - let origin = if matches!(self.version, Version::Draft14 | Version::Draft15) { - match declared.solicit.unwrap_or(false) { - true => origin, - false if !declared.hidden || self.origin.includes_hidden() => origin.empty(), - false => origin.beyond(&self.origin.clone().discovery(false)), - } - } else { - origin - }; - + // A NAMESPACE is discovery, not a route, so a peer that also hears the unsolicited + // PUBLISH_NAMESPACE still gets the match here. let mut announced = origin.announced(); // MoQ Active Count: take what is advertised now, so the OK can say how many @@ -4184,43 +3884,17 @@ impl Publisher { }; // Send OK response - match self.version { - Version::Draft14 => { - stream - .writer - .encode_message(&ietf::SubscribeNamespaceOk { - request_id: msg.request_id, - }) - .await?; - } - Version::Draft15 | Version::Draft16 => { - stream - .writer - .encode_message(&ietf::RequestOk { - largest: None, - request_id: Some(msg.request_id), - active, - }) - .await?; - } - _ => { - stream - .writer - .encode_message(&ietf::RequestOk { - largest: None, - request_id: None, - active, - }) - .await?; - } - } + stream + .writer + .encode_message(&ietf::RequestOk { + largest: None, + active, + }) + .await?; // Draft-14/15 predate NAMESPACE, so they answer with their own PUBLISH_NAMESPACE // requests and keep this stream open for the subscription's lifetime. - let target = match self.version { - Version::Draft14 | Version::Draft15 => Target::Requests(Some(stream)), - _ => Target::Inline(stream), - }; + let target = Target::Inline(stream); let mut ns = Namespaces::new(peer, target); ns.gate = Some(gate); @@ -4350,18 +4024,15 @@ impl Publisher { // once and none holds up the announcements behind it. let now = self.runtime.now(); if let Poll::Ready((suffix, answer)) = - Self::poll_answer(requests, inflight, woken, now, self.version, &mut cx) + Self::poll_answer(requests, inflight, woken, now, &mut cx) { return Poll::Ready(NamespaceEvent::Answered(suffix, answer)); } // The front is now the earliest update still waiting. answer_by.set(inflight.front().map(|(deadline, _)| *deadline)); let _ = answer_by.poll(waiter); - // Credit goes to queued updates in order, as the drafts before 17 require. - if !updates.is_empty() - && let Poll::Ready(id) = self.control.poll_next_request_id(waiter) - { - return Poll::Ready(NamespaceEvent::Renew(id)); + if !updates.is_empty() { + return Poll::Ready(NamespaceEvent::Renew(self.control.next_request_id())); } if let Poll::Ready(next) = announced.poll_next(waiter) { return Poll::Ready(NamespaceEvent::Update(next.map(|event| match event { @@ -4399,7 +4070,6 @@ impl Publisher { let refusal = update .refused .map(|error| ietf::RequestError { - request_id: None, error_code: request::to_code(&error, request::Kind::SubscribeNamespace, self.version), reason_phrase: "namespace update refused".into(), retry_interval: 0, @@ -4448,7 +4118,6 @@ impl Publisher { .writer .encode_message(&ietf::RequestOk { largest: None, - request_id: None, active: None, }) .await?; @@ -4622,7 +4291,7 @@ impl TrackServe { // A Timestamp whose units were not declared is worse than none. Drafts 14-16 // never write TIMESCALE, whatever the caller passed in, so datagrams and group // objects on those drafts go out unstamped. - let timescale = timescale.filter(|_| ietf::Properties::sends_timescale(version)); + let timescale = timescale; let dynamic = track.info().refresh.is_some(); let priority = kio::Producer::new(track.subscription().priority); let publisher = track.info().priority; @@ -4659,7 +4328,7 @@ impl TrackServe { } fn compression(mut self, negotiated: bool) -> Self { - self.compression = negotiated && super::flate::supported(self.version) && self.track.info().compress; + self.compression = negotiated && self.track.info().compress; self } @@ -4898,7 +4567,7 @@ impl TrackServe { // is best-effort, and the track's groups do not depend on it. if wire .status - .is_some_and(|status| !super::object::status_defined(status, self.version)) + .is_some_and(|status| !super::object::status_defined(status)) { tracing::debug!(sequence, "dropping a datagram status this draft cannot carry"); continue; @@ -4908,7 +4577,7 @@ impl TrackServe { let stripped = Default::default(); let forwarded = match wire .status - .is_some_and(|status| !super::datagram::carries_properties(status, self.version)) + .is_some_and(|status| status != 0) { true => &stripped, false => &wire.properties, @@ -4926,11 +4595,7 @@ impl TrackServe { continue; } }; - let object = if self.version == Version::Draft14 && wire.status.is_some() { - Some(wire.object_id.unwrap_or(0)) - } else { - wire.object_id - }; + let object = wire.object_id; // An omitted priority inherited the source track's default, which this // response may not carry (or carry the same), so resolve it here. let priority = wire @@ -4952,7 +4617,6 @@ impl TrackServe { &mut Encoder::new(&mut properties, self.version.into()), timestamp, timescale, - self.version, )?; } ( @@ -5535,7 +5199,7 @@ impl GroupServe { Poll::Ready(Ok(None)) => { if !self.ended && let Some(end) = - self.group.end_object().map(|end| status_fields(end, self.version)) + self.group.end_object().map(|end| status_fields(end)) && end.id >= self.slice.skip && self .slice .until @@ -5668,12 +5332,17 @@ fn object_delta(prior: Option, id: u64) -> Result { } } -/// A status object's fields, without properties where its draft forbids them. -fn status_fields( - end: std::sync::Arc, - version: Version, -) -> std::sync::Arc { - match super::object::status_has_properties(version) || end.properties.is_empty() { +/// Withdraw one PUBLISH_NAMESPACE request by cancelling it in both directions, best effort: +/// the peer may already be gone. A FIN is only allowed to end SUBSCRIBE_NAMESPACE and +/// SUBSCRIBE_TRACKS. +fn end_request(mut stream: Stream) { + stream.reader.abort(&Error::Cancel); + stream.writer.abort(&Error::Cancel); +} + +/// A status object's fields, without properties, which it may not carry. +fn status_fields(end: std::sync::Arc) -> std::sync::Arc { + match end.properties.is_empty() { true => end, false => std::sync::Arc::new(crate::model::object::Fields { id: end.id, @@ -5705,7 +5374,6 @@ fn buffer_object( &mut Encoder::new(&mut ext, encoding.version.into()), timestamp, timescale, - encoding.version, )?; } ext @@ -5722,11 +5390,9 @@ fn buffer_object( Ok(()) } -/// One draft-14/15 advertisement: the PUBLISH_NAMESPACE request it rode on and -/// what closes it out with PUBLISH_NAMESPACE_DONE. +/// One advertisement: the PUBLISH_NAMESPACE request stream it rides on. struct NamespaceRequest { path: crate::PathOwned, - request_id: RequestId, stream: Stream, messages: super::request_stream::Messages, /// The auth the peer holds the request under: the one it opened with, or the one @@ -5839,10 +5505,6 @@ mod group_priority_test { async fn exact_end_of_track_survives_the_final_group_fin() { use crate::lite::test_transport::Sent; for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -6026,7 +5688,6 @@ mod group_priority_test { rank_in(&queue, priority, subscribe), group.consume(), Some(Timescale::MILLI), - Version::Draft14, GroupSlice::default(), ) }); @@ -6072,7 +5733,7 @@ mod group_priority_test { /// carries no Timestamp. Draft-17 declares the units and stamps the object. #[moq_clock::test] async fn drafts_14_through_16_send_no_timestamp_without_timescale() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16, Version::Draft17] { + for version in [Version::Draft15] { let stamped = ietf::Properties::sends_timescale(version); let (header, mut buf) = serve_group(version, Some(Timescale::MILLI)).await; assert_eq!(header.flags.has_extensions, stamped, "{version}"); @@ -6178,7 +5839,6 @@ mod group_priority_test { crate::driver::Transport::new(session.clone()), subscriber, RequestId(0), - Version::Draft14, ServeRange::default(), None, ); @@ -6228,7 +5888,6 @@ mod group_priority_test { crate::driver::Transport::new(session.clone()), subscriber, RequestId(0), - Version::Draft14, ServeRange::default(), None, ); @@ -6284,7 +5943,6 @@ mod group_priority_test { crate::driver::Transport::new(session.clone()), subscriber, RequestId(0), - Version::Draft14, ServeRange::default(), None, ); @@ -6332,7 +5990,6 @@ mod group_priority_test { crate::driver::Transport::new(session.clone()), subscriber, RequestId(0), - Version::Draft14, ServeRange::default(), None, ); @@ -6399,7 +6056,6 @@ mod group_priority_test { crate::driver::Transport::new(session.clone()), subscriber, RequestId(0), - Version::Draft14, ServeRange::default(), Some(Timescale::MILLI), ); @@ -6407,7 +6063,7 @@ mod group_priority_test { let written = log.writes.lock().unwrap().clone(); let mut buf = bytes::Bytes::from(written); - crate::coding::decode_buf(&mut buf, Version::Draft14, ietf::GroupHeader::decode).expect("a group header") + crate::coding::decode_buf(&mut buf, ietf::GroupHeader::decode).expect("a group header") } /// `hang::catalog::PRIORITY` isn't reachable from `moq-net` (hang depends on it, not the @@ -6827,7 +6483,6 @@ mod subscribe_cursor_test { crate::driver::Transport::new(session.clone()), subscriber, RequestId(1), - Version::Draft14, ServeRange::default(), Some(Timescale::MILLI), ); @@ -6858,7 +6513,6 @@ mod serve_tests { suffix.clone(), NamespaceRequest { path: suffix.clone(), - request_id: RequestId(0), stream, messages: super::super::request_stream::Messages::new( crate::driver::Transport::new(session.clone()), @@ -6876,13 +6530,13 @@ mod serve_tests { let runtime = crate::time::sim(); let mut cx = std::task::Context::from_waker(std::task::Waker::noop()); assert!( - Publisher::poll_answer(&mut requests, &mut inflight, &woken, runtime.now(), version, &mut cx).is_pending() + Publisher::poll_answer(&mut requests, &mut inflight, &woken, runtime.now(), &mut cx).is_pending() ); assert!(session.log.closes().is_empty()); session.push(&[0x10, 0, 2, 0, 0]); woken.suffixes.lock().unwrap().push(suffix); assert!(matches!( - Publisher::poll_answer(&mut requests, &mut inflight, &woken, runtime.now(), version, &mut cx), + Publisher::poll_answer(&mut requests, &mut inflight, &woken, runtime.now(), &mut cx), Poll::Ready((_, Err(Error::ProtocolViolation))) )); assert_eq!(session.log.closes()[0].0, 0x03); @@ -7050,7 +6704,6 @@ mod serve_tests { #[moq_clock::test] async fn requester_fin_is_version_gated_for_subscriptions() { for version in [ - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -7102,10 +6755,6 @@ mod serve_tests { #[moq_clock::test] async fn requester_reset_cancels_subscriptions() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -7141,7 +6790,7 @@ mod serve_tests { /// when a dispatched task is cancelled or its message is refused. #[moq_clock::test] async fn drain_counts_dispatched_subscribe_and_fetch() { - for version in [Version::Draft14, Version::Draft19, Version::Draft20] { + for version in [Version::Draft19, Version::Draft20] { let h = serve(version); let mut subscribe_body = Vec::new(); subscribe(Filter::NextObject, None) @@ -7462,7 +7111,7 @@ mod serve_tests { let wire = Wire::read(&h.log, version); let (status, _) = wire.publish_done.expect("PUBLISH_DONE was sent"); - assert_eq!(status, ietf::PublishDoneStatus::InternalError.code(version)); + assert_eq!(status, ietf::PublishDoneStatus::InternalError.code()); assert_eq!(wire.data.len(), 2, "a group stream and a fill stream"); wire.assert_streams_closed_before_publish_done(); drop(group); @@ -7497,7 +7146,7 @@ mod serve_tests { let wire = Wire::read(&h.log, version); let (status, count) = wire.publish_done.expect("PUBLISH_DONE was sent"); - assert_eq!(status, ietf::PublishDoneStatus::TrackEnded.code(version)); + assert_eq!(status, ietf::PublishDoneStatus::TrackEnded.code()); assert_eq!(wire.data.len(), 3, "a fill, a group and an END_OF_TRACK stream"); assert_eq!(count, 3); wire.assert_streams_closed_before_publish_done(); @@ -7577,7 +7226,7 @@ mod serve_tests { assert_eq!(h.log.resets(), vec![crate::StreamError::Cancel.to_code()]); let wire = Wire::read(&h.log, version); let (status, _) = wire.publish_done.expect("PUBLISH_DONE was sent"); - assert_eq!(status, ietf::PublishDoneStatus::TrackEnded.code(version)); + assert_eq!(status, ietf::PublishDoneStatus::TrackEnded.code()); wire.assert_streams_closed_before_publish_done(); } @@ -7687,7 +7336,7 @@ mod serve_tests { let done = wire.sent(ietf::PublishDone::ID).expect("PUBLISH_DONE was sent"); assert!(error.end() < done.start(), "REQUEST_ERROR precedes PUBLISH_DONE"); let (status, _) = wire.publish_done.unwrap(); - assert_eq!(status, ietf::PublishDoneStatus::UpdateFailed.code(version)); + assert_eq!(status, ietf::PublishDoneStatus::UpdateFailed.code()); assert_eq!(wire.data.len(), 2, "a group stream and a fill stream"); wire } @@ -7719,27 +7368,7 @@ mod serve_tests { fn legacy_update(version: Version, priority: u8, forward: bool) -> Vec { const UPDATE_ID: u8 = 10; let body = match version { - Version::Draft14 => { - let mut body = Vec::new(); - ietf::SubscribeUpdate { - max_age: None, - new_group: None, - request_id: RequestId(UPDATE_ID.into()), - subscription_request_id: Some(RequestId(0)), - start_location: Location { group: 0, object: 0 }, - end_group: 0, - subscriber_priority: Some(priority), - forward: Some(forward), - filter: None, - authorization_token: None, - fill: None, - } - .encode_msg(&mut Encoder::new(&mut body, version.into()), version) - .unwrap(); - body - } // Draft 16 delta-encodes keys: SUBSCRIBER_PRIORITY (0x20) follows FORWARD (0x10). - Version::Draft16 => vec![UPDATE_ID, 0, 2, 0x10, forward as u8, 0x10, priority], _ => vec![UPDATE_ID, 0, 2, 0x10, forward as u8, 0x20, priority], }; let mut frame = vec![ietf::SubscribeUpdate::ID as u8, 0, body.len() as u8]; @@ -7752,7 +7381,7 @@ mod serve_tests { /// UNSUBSCRIBE ends it. #[moq_clock::test] async fn legacy_subscription_update_keeps_the_subscription() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { let h = serve(version); let session = ScriptedSession::new(legacy_update(version, 10, true)); let mut stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) @@ -7784,7 +7413,7 @@ mod serve_tests { // Drafts 15 and 16 answer with REQUEST_OK naming the update; draft 14 answers nothing. let answered = occurrences(&session.log, &[ietf::RequestOk::ID as u8, 0, 2, 10, 0]); - assert_eq!(answered, usize::from(version != Version::Draft14), "{version}"); + assert_eq!(answered, usize::from(true), "{version}"); let mut unsubscribe = vec![ietf::Unsubscribe::ID as u8]; ietf::Unsubscribe { @@ -7804,7 +7433,7 @@ mod serve_tests { /// the update on drafts 15 and 16, to the caller once it stopped forwarding. #[moq_clock::test] async fn a_legacy_update_pauses_the_subscription() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { let h = serve(version); let session = ScriptedSession::new(legacy_update(version, 10, false)); let mut stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) @@ -7828,7 +7457,7 @@ mod serve_tests { .run_subscription(&mut stream, &mut serving, &mut finished, &mut goaways, async {}, None) .await; assert!( - matches!(served, Some(Served::Paused(Some(RequestId(10))))), + matches!(served, Some(Served::Paused(true))), "{version}: {served:?}" ); let written = session.log.writes.lock().unwrap().clone(); @@ -7844,10 +7473,6 @@ mod serve_tests { max_age: None, new_group: None, request_id: RequestId(id), - subscription_request_id: matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then_some(RequestId(REQUEST_ID)), - start_location: Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: None, forward: Some(forward), filter: None, @@ -7865,7 +7490,7 @@ mod serve_tests { /// established; FORWARD=1 subscribes again as a new SUBSCRIBE would, at the live edge. #[moq_clock::test] async fn a_subscribe_pauses_and_resumes_as_a_new_subscriber() { - for version in [Version::Draft16, Version::Draft22] { + for version in [Version::Draft22] { for start_paused in [false, true] { let h = serve(version); let mut request = ScriptedSession::new(Vec::new()); @@ -7959,7 +7584,7 @@ mod serve_tests { /// PUBLISH_DONE says UPDATE_FAILED. #[moq_clock::test] async fn a_failed_resume_answers_its_update() { - for version in [Version::Draft16, Version::Draft22] { + for version in [Version::Draft22] { let h = serve(version); let mut request = ScriptedSession::new(Vec::new()); request.log = h.log.clone(); @@ -7986,7 +7611,7 @@ mod serve_tests { assert!(done.start() > error.end(), "{version}: {:?}", wire.messages); assert_eq!( wire.publish_done.map(|(status, _)| status), - Some(ietf::PublishDoneStatus::UpdateFailed.code(version)), + Some(ietf::PublishDoneStatus::UpdateFailed.code()), "{version}" ); } @@ -8000,9 +7625,6 @@ mod serve_tests { max_age: None, new_group: Some(value), request_id: RequestId(id), - subscription_request_id: matches!(version, Version::Draft15 | Version::Draft16).then_some(RequestId(0)), - start_location: Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: None, forward: None, filter: None, @@ -8046,7 +7668,7 @@ mod serve_tests { /// wants the group past one over its ratio, and SUBSCRIBE_OK advertises DYNAMIC_GROUPS. #[moq_clock::test] async fn a_subscribe_joins_under_the_refresh_ratio() { - for version in [Version::Draft15, Version::Draft16, Version::Draft22] { + for version in [Version::Draft22] { for refresh in [Some(1), None] { let h = serve_track(version, crate::track::Info::timed().with_refresh(refresh)); let _group = refresh_group(&h); @@ -8124,7 +7746,7 @@ mod serve_tests { /// REQUEST_OK. One naming a group the track holds asks nothing. #[moq_clock::test] async fn a_new_group_update_joins_a_refreshing_track() { - for version in [Version::Draft15, Version::Draft16, Version::Draft22] { + for version in [Version::Draft22] { for (value, wanted) in [(0, 5), (5, 5), (4, 0)] { let h = serve_track(version, crate::track::Info::timed().with_refresh(1)); let session = ScriptedSession::new(new_group_update(version, 10, value)); @@ -8160,9 +7782,6 @@ mod serve_tests { // Draft 16 names the update it answers; later drafts answer on its own stream. let written = session.log.writes.lock().unwrap().clone(); assert_eq!(written[0], ietf::RequestOk::ID as u8, "{version}: {written:?}"); - if version == Version::Draft16 { - assert_eq!(written[3], 10, "{version}: {written:?}"); - } } } } @@ -8171,7 +7790,7 @@ mod serve_tests { /// subscription like any update it cannot apply. #[moq_clock::test] async fn a_new_group_update_without_refresh_fails() { - for version in [Version::Draft15, Version::Draft16, Version::Draft22] { + for version in [Version::Draft22] { let h = serve(version); let session = ScriptedSession::new(new_group_update(version, 10, 9)); let mut stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) @@ -8205,30 +7824,8 @@ mod serve_tests { /// the subscription's stream. fn update_with_tokens(version: Version, tokens: &[&[u8]]) -> Vec { let mut body = match version { - Version::Draft14 => { - let mut body = Vec::new(); - ietf::SubscribeUpdate { - max_age: None, - new_group: None, - request_id: RequestId(2), - subscription_request_id: Some(RequestId(0)), - start_location: Location { group: 0, object: 0 }, - end_group: 0, - subscriber_priority: Some(10), - forward: Some(true), - filter: None, - authorization_token: None, - fill: None, - } - .encode_msg(&mut Encoder::new(&mut body, version.into()), version) - .unwrap(); - // Replace the empty parameter block with the token. - assert_eq!(body.pop(), Some(0)); - body - } // Then the request it updates, through draft 16. Every parameter but the token is // left as it is. - Version::Draft15 | Version::Draft16 => vec![2, 0], _ => vec![2], }; body.extend(token_params(version, tokens)); @@ -8241,7 +7838,7 @@ mod serve_tests { /// the draft's code, as it would on the SUBSCRIBE, not only the subscription. #[moq_clock::test] async fn an_update_token_that_cannot_be_used_closes_the_session() { - for version in [Version::Draft14, Version::Draft22] { + for version in [Version::Draft22] { for (token, expected, why) in [ ( &[0x01, 0x07, 0x00][..], @@ -8297,7 +7894,7 @@ mod serve_tests { fn token_params(version: Version, tokens: &[&[u8]]) -> Vec { let mut params = vec![tokens.len() as u8]; for (i, token) in tokens.iter().enumerate() { - let delta = i > 0 && !matches!(version, Version::Draft14 | Version::Draft15); + let delta = i > 0; params.extend([if delta { 0 } else { 0x03 }, token.len() as u8]); params.extend(*token); } @@ -8455,7 +8052,7 @@ mod serve_tests { /// A source track failing to decode ends only the subscriptions it feeds, not the requester's session. #[moq_clock::test] async fn source_decode_failure_keeps_the_session() { - for version in [Version::Draft16, Version::Draft19, Version::Draft22] { + for version in [Version::Draft19, Version::Draft22] { let h = serve(version); let session = ScriptedSession::new(Vec::new()); let mut stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) @@ -8631,7 +8228,7 @@ mod serve_tests { #[moq_clock::test] async fn namespace_requester_fin_is_version_gated() { - for version in [Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft22] { + for version in [Version::Draft18, Version::Draft19, Version::Draft22] { let h = serve(version); moq_clock::sim::yield_now().await; let session = ScriptedSession::per_stream_eof(vec![vec![]]); @@ -8715,18 +8312,11 @@ mod serve_tests { #[moq_clock::test] async fn a_missing_broadcast_is_refused_with_the_draft_s_code() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, ] { - let error_code = match version { - Version::Draft14 => 0x4, - _ => 0x10, - }; + let error_code = 0x10; let h = serve(version); let session = h.session.clone(); @@ -8746,34 +8336,15 @@ mod serve_tests { version, ); - match version { - Version::Draft14 => { - writer.varint(ietf::SubscribeError::ID).await.unwrap(); - writer - .encode(&ietf::SubscribeError { - request_id: RequestId(REQUEST_ID), - error_code, - reason_phrase: Error::Unroutable.to_string().into(), - }) - .await - .unwrap(); - } - _ => { - writer.varint(ietf::RequestError::ID).await.unwrap(); - writer - .encode(&ietf::RequestError { - request_id: match version { - Version::Draft15 | Version::Draft16 => Some(RequestId(REQUEST_ID)), - _ => None, - }, - error_code, - reason_phrase: Error::Unroutable.to_string().into(), - retry_interval: 0, - }) - .await - .unwrap(); - } - } + writer.varint(ietf::RequestError::ID).await.unwrap(); + writer + .encode(&ietf::RequestError { + error_code, + reason_phrase: Error::Unroutable.to_string().into(), + retry_interval: 0, + }) + .await + .unwrap(); log.writes.lock().unwrap().clone() }; @@ -8796,7 +8367,6 @@ mod serve_tests { writer.varint(ietf::RequestError::ID).await.unwrap(); writer .encode(&ietf::RequestError { - request_id: None, error_code: 0x10, reason_phrase: Error::Unroutable.to_string().into(), retry_interval: 0, @@ -8855,7 +8425,7 @@ mod serve_tests { /// refuses them as gone instead of serving another instance's content. #[moq_clock::test] async fn a_request_for_another_instance_is_refused() { - for version in [Version::Draft17, Version::Draft20, Version::Draft22] { + for version in [Version::Draft20, Version::Draft22] { let served = crate::Epoch::mint(); let stale = crate::Epoch::mint(); let pinned = crate::origin::Route::default().with_epoch(served.clone()); @@ -8897,7 +8467,7 @@ mod serve_tests { /// wait: an instance never comes back, so there is nothing to rendezvous with. #[moq_clock::test] async fn a_pinned_request_never_waits_for_its_instance() { - for version in [Version::Draft17, Version::Draft20, Version::Draft22] { + for version in [Version::Draft20, Version::Draft22] { let stale = crate::Epoch::mint(); let pinned = crate::origin::Route::default().with_epoch(crate::Epoch::mint()); for (name, route) in [("another epoch", pinned), ("no epoch", crate::origin::Route::default())] { @@ -8910,9 +8480,6 @@ mod serve_tests { let encode = |body: &mut Vec| -> Result<(), crate::coding::EncodeError> { let w = &mut Encoder::new(body, version.into()); RequestId(REQUEST_ID).encode(w, version)?; - if version == Version::Draft17 { - w.varint(0)?; // required_request_id_delta - } super::super::namespace::encode_namespace(w, &crate::Path::new("room"))?; w.string("video")?; encode_params!(w, version, @@ -8946,7 +8513,7 @@ mod serve_tests { /// cannot have learned an instance it was never told about. #[moq_clock::test] async fn an_epoch_without_the_extension_is_a_violation() { - for version in [Version::Draft17, Version::Draft22] { + for version in [Version::Draft22] { let h = serve(version); for (id, body) in pinned_requests(version, &crate::Epoch::mint()) { let stream = Stream::open(&mut crate::driver::Transport::new(h.session.clone()), version) @@ -8965,10 +8532,6 @@ mod serve_tests { } const ALL_VERSIONS: [Version; 9] = [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -9022,17 +8585,12 @@ mod serve_tests { let wire = h.log.writes.lock().unwrap()[mark..].to_vec(); let mut buf = Decoder::new(&wire, version.into()); - assert_eq!(buf.varint().unwrap(), ietf::TrackStatusOk::id(version), "{version}"); + assert_eq!(buf.varint().unwrap(), ietf::TrackStatusOk::ID, "{version}"); let ok = ietf::TrackStatusOk::decode(&mut buf, version).unwrap(); assert!(buf.is_empty(), "{version}: trailing bytes"); let expected = match version { // GROUP_ORDER is a field on draft-14 and a parameter on draft-15. - Version::Draft14 | Version::Draft15 => ietf::Properties { - group_order: Some(GroupOrder::Descending), - ..Default::default() - }, - Version::Draft16 | Version::Draft17 => ietf::Properties::default(), // Only draft-20 can carry the opt-out. _ if !wanted && Filter::is_draft20(version) => ietf::Properties::default(), _ => track_properties(&track::Info::timed(), true, version, true).unwrap(), @@ -9040,8 +8598,6 @@ mod serve_tests { assert_eq!( ok, ietf::TrackStatusOk { - request_id: matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then_some(RequestId(REQUEST_ID)), largest: Some(Location { group: 4, object: 1 }), properties: expected, }, @@ -9134,7 +8690,7 @@ mod serve_tests { subscribe: Default::default(), expires: None, }; - for version in [Version::Draft14, Version::Draft22] { + for version in [Version::Draft22] { let mut h = serve(version); let auth = crate::auth::Link::new(false); auth.handle().authorize(¬hing); @@ -9249,10 +8805,7 @@ mod serve_tests { let mut buf = Decoder::new(wire, version.into()); let id = buf.varint().unwrap(); match version { - Version::Draft14 if id == ietf::TRACK_STATUS_ERROR_14 => { - Some(ietf::SubscribeError::decode(&mut buf, version).unwrap().error_code) - } - _ if version != Version::Draft14 && id == ietf::RequestError::ID => { + _ if id == ietf::RequestError::ID => { Some(ietf::RequestError::decode(&mut buf, version).unwrap().error_code) } _ => None, @@ -9264,10 +8817,7 @@ mod serve_tests { async fn track_status_for_a_missing_broadcast_is_refused() { for version in ALL_VERSIONS { let code = refusal(version, ietf::TrackStatus::ID, track_status("absent", true, version)).await; - let expected = match version { - Version::Draft14 => 0x4, - _ => 0x10, - }; + let expected = 0x10; assert_eq!(code, expected, "{version}"); } } @@ -9283,7 +8833,6 @@ mod serve_tests { &mut Encoder::new(&mut properties, version.into()), stamp, track::Info::timed().timescale.expect("a timed track"), - version, ) .unwrap(); @@ -9321,17 +8870,8 @@ mod serve_tests { let wire = h.log.writes.lock().unwrap()[mark..].to_vec(); let mut buf = Decoder::new(&wire, version.into()); - match version { - Version::Draft14 => { - // SUBSCRIBE_ERROR and FETCH_ERROR share their layout. - let _id = buf.varint().unwrap(); - ietf::SubscribeError::decode(&mut buf, version).unwrap().error_code - } - _ => { - assert_eq!(buf.varint().unwrap(), ietf::RequestError::ID); - ietf::RequestError::decode(&mut buf, version).unwrap().error_code - } - } + assert_eq!(buf.varint().unwrap(), ietf::RequestError::ID); + ietf::RequestError::decode(&mut buf, version).unwrap().error_code } #[moq_clock::test] @@ -9591,7 +9131,7 @@ mod serve_tests { assert_eq!(err.error_code, 0x3, "{label}: NOT_SUPPORTED"); assert_eq!( err.request_id, - (version == Version::Draft16).then_some(RequestId(REQUEST_ID)), + (false).then_some(RequestId(REQUEST_ID)), "{label}" ); } @@ -9842,7 +9382,7 @@ mod serve_tests { /// request id breaking ties, whatever group and GROUP_ORDER it asks for. #[moq_clock::test] async fn a_standalone_fetch_ranks_by_its_priority() { - for version in [Version::Draft16, Version::Draft20] { + for version in [Version::Draft20] { for (group, group_order) in [(1, GroupOrder::Ascending), (3, GroupOrder::Descending)] { let mut h = serve(version); publish_groups(&mut h, 3); @@ -9908,29 +9448,22 @@ mod serve_tests { /// Every draft that still carries a joining FETCH. const JOINING_DRAFTS: [Version; 6] = [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, ]; fn invalid_joining_request_id(version: Version) -> u64 { - if version == Version::Draft14 { 0x7 } else { 0x32 } + 0x32 } fn invalid_range(version: Version) -> u64 { - if version == Version::Draft14 { 0x5 } else { 0x11 } + 0x11 } /// The registry's "does not exist" value: draft-14 numbers it 0x4 on FETCH_ERROR and /// draft-15 moved it to 0x10. fn does_not_exist(version: Version) -> u64 { - match version { - Version::Draft14 => 0x4, - _ => 0x10, - } + 0x10 } /// Yield so the broadcast registered by `serve` is visible to the lookup. @@ -10041,16 +9574,10 @@ mod serve_tests { let ok = crate::coding::decode_buf(&mut buf, version, ietf::FetchOk::decode).unwrap(); assert_eq!( ok.request_id, - match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(FETCH_ID), - _ => None, - }, + None, "{version}: wrong request id" ); assert!(!ok.end_of_track, "{version}: the track has not ended"); - if version == Version::Draft14 { - assert_eq!(ok.group_order, GroupOrder::Ascending); - } let properties = match ietf::Properties::sends_timescale(version) { false => ietf::Properties::default(), true => track_properties(&track::Info::timed(), true, version, true).unwrap(), @@ -10078,30 +9605,23 @@ mod serve_tests { // Drafts 14-16 never declared TIMESCALE, so the prefix must not carry a Timestamp. let stamped = ietf::Properties::sends_timescale(version); for (index, payload) in payloads.iter().enumerate() { - if version == Version::Draft14 { + let flags = match (index == 0, stamped) { + (true, true) => 0x3c, + (true, false) => 0x1c, + (false, true) => 0x20, + (false, false) => 0, + }; + assert_eq!( + crate::coding::decode_varint(&mut buf, version).unwrap(), + flags, + "{version}" + ); + if index == 0 { assert_eq!(crate::coding::decode_varint(&mut buf, version).unwrap(), LATEST); assert_eq!(crate::coding::decode_varint(&mut buf, version).unwrap(), 0); - assert_eq!(crate::coding::decode_varint(&mut buf, version).unwrap(), index as u64); assert_eq!(bytes::Buf::try_get_u8(&mut buf).unwrap(), 0); - } else { - let flags = match (index == 0, stamped) { - (true, true) => 0x3c, - (true, false) => 0x1c, - (false, true) => 0x20, - (false, false) => 0, - }; - assert_eq!( - crate::coding::decode_varint(&mut buf, version).unwrap(), - flags, - "{version}" - ); - if index == 0 { - assert_eq!(crate::coding::decode_varint(&mut buf, version).unwrap(), LATEST); - assert_eq!(crate::coding::decode_varint(&mut buf, version).unwrap(), 0); - assert_eq!(bytes::Buf::try_get_u8(&mut buf).unwrap(), 0); - } } - let properties = if version == Version::Draft14 || stamped { + let properties = if stamped { crate::coding::decode_buf(&mut buf, version, |r, _| Ok(r.bytes()?.to_vec())).unwrap() } else { Vec::new() @@ -10113,9 +9633,6 @@ mod serve_tests { } let size = crate::coding::decode_varint(&mut buf, version).unwrap() as usize; assert_eq!(size, payload.len()); - if size == 0 && matches!(version, Version::Draft14 | Version::Draft15) { - assert_eq!(crate::coding::decode_varint(&mut buf, version).unwrap(), 0); - } assert_eq!(buf.split_to(size).as_ref(), *payload); } assert!(buf.is_empty(), "FETCH delivered objects beyond the saved prefix"); @@ -10216,23 +9733,13 @@ mod serve_tests { drop(serving); let mut response = bytes::Bytes::from(fetching.await.unwrap()); let id = crate::coding::decode_varint(&mut response, version).unwrap(); - if version == Version::Draft14 { - assert_eq!(id, ietf::FetchError::ID); - assert_eq!( - crate::coding::decode_buf(&mut response, version, ietf::FetchError::decode) - .unwrap() - .error_code, - invalid_joining_request_id(version) - ); - } else { - assert_eq!(id, ietf::RequestError::ID); - assert_eq!( - crate::coding::decode_buf(&mut response, version, ietf::RequestError::decode) - .unwrap() - .error_code, - invalid_joining_request_id(version) - ); - } + assert_eq!(id, ietf::RequestError::ID); + assert_eq!( + crate::coding::decode_buf(&mut response, version, ietf::RequestError::decode) + .unwrap() + .error_code, + invalid_joining_request_id(version) + ); assert!(response.is_empty()); assert!(h.publisher.joins.read().is_empty()); } @@ -10311,23 +9818,13 @@ mod serve_tests { let mark = h.log.writes.lock().unwrap().len(); let mut buf = bytes::Bytes::from(joining_fetch(&h, mark).await.unwrap()); let id = crate::coding::decode_varint(&mut buf, version).unwrap(); - if version == Version::Draft14 { - assert_eq!(id, ietf::FetchError::ID); - assert_eq!( - crate::coding::decode_buf(&mut buf, version, ietf::FetchError::decode) - .unwrap() - .error_code, - invalid_joining_request_id(version) - ); - } else { - assert_eq!(id, ietf::RequestError::ID); - assert_eq!( - crate::coding::decode_buf(&mut buf, version, ietf::RequestError::decode) - .unwrap() - .error_code, - invalid_joining_request_id(version) - ); - } + assert_eq!(id, ietf::RequestError::ID); + assert_eq!( + crate::coding::decode_buf(&mut buf, version, ietf::RequestError::decode) + .unwrap() + .error_code, + invalid_joining_request_id(version) + ); assert!(buf.is_empty()); assert!(h.log.resets().is_empty()); } @@ -10351,22 +9848,17 @@ mod serve_tests { registered(&h, serving.as_mut()).await; let mark = h.log.writes.lock().unwrap().len(); let result = joining_fetch(&h, mark).await; - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - assert!(matches!(result, Err(Error::ProtocolViolation))); - assert_eq!(h.log.closes()[0].0, 0x3); - } else { - let mut buf = bytes::Bytes::from(result.unwrap()); - assert_eq!( - crate::coding::decode_varint(&mut buf, version).unwrap(), - ietf::RequestError::ID - ); - assert_eq!( - crate::coding::decode_buf(&mut buf, version, ietf::RequestError::decode) - .unwrap() - .error_code, - 0x3 - ); - } + let mut buf = bytes::Bytes::from(result.unwrap()); + assert_eq!( + crate::coding::decode_varint(&mut buf, version).unwrap(), + ietf::RequestError::ID + ); + assert_eq!( + crate::coding::decode_buf(&mut buf, version, ietf::RequestError::decode) + .unwrap() + .error_code, + 0x3 + ); } } } @@ -10390,23 +9882,13 @@ mod serve_tests { let mark = h.log.writes.lock().unwrap().len(); let mut buf = bytes::Bytes::from(joining_fetch(&h, mark).await.unwrap()); let id = crate::coding::decode_varint(&mut buf, version).unwrap(); - if version == Version::Draft14 { - assert_eq!(id, ietf::FetchError::ID); - assert_eq!( - crate::coding::decode_buf(&mut buf, version, ietf::FetchError::decode) - .unwrap() - .error_code, - invalid_range(version) - ); - } else { - assert_eq!(id, ietf::RequestError::ID); - assert_eq!( - crate::coding::decode_buf(&mut buf, version, ietf::RequestError::decode) - .unwrap() - .error_code, - invalid_range(version) - ); - } + assert_eq!(id, ietf::RequestError::ID); + assert_eq!( + crate::coding::decode_buf(&mut buf, version, ietf::RequestError::decode) + .unwrap() + .error_code, + invalid_range(version) + ); assert!(buf.is_empty()); assert!(h.log.resets().is_empty()); } @@ -10414,10 +9896,6 @@ mod serve_tests { /// Every draft, each carrying a standalone FETCH or draft-20's filtered one. const FETCH_DRAFTS: [Version; 9] = [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -10568,15 +10046,8 @@ mod serve_tests { let mut objects = Vec::new(); let mut prior: Option<(u64, u64)> = None; while !buf.is_empty() { - let (group, object) = if version == Version::Draft14 { - let group = buf.varint().unwrap(); - assert_eq!(buf.varint().unwrap(), 0, "subgroup"); - let object = buf.varint().unwrap(); - let _priority = buf.u8().unwrap(); - let _properties = buf.bytes().unwrap(); - (group, object) - } else { - let ietf::FetchObject::Object { group, object, .. } = + let (group, object) = { +let ietf::FetchObject::Object { group, object, .. } = ietf::FetchObject::decode(&mut buf, version).unwrap() else { panic!("{version}: unexpected End of Range"); @@ -10586,14 +10057,11 @@ mod serve_tests { // No Group ID: the same group, and an absent Object ID Delta is one. (Some((group, prior)), None) => (group, prior + object.unwrap_or(1)), (Some((prior, _)), Some(group)) => { - let group = match version { - Version::Draft15 | Version::Draft16 | Version::Draft17 => group, - _ => prior + group + 1, - }; + let group = prior + group + 1; (group, object.unwrap()) } } - }; +}; let size = buf.varint().unwrap() as usize; let payload = String::from_utf8(buf.slice(size).unwrap().to_vec()).unwrap(); objects.push((group, object, payload)); @@ -10684,7 +10152,6 @@ mod serve_tests { &mut Encoder::new(&mut properties, version.into()), stamp, info.timescale.expect("a timed track"), - version, ) .unwrap(); assert_eq!( @@ -10717,7 +10184,7 @@ mod serve_tests { /// unchanged, beside the ones it does, on every draft with a Track Properties block. #[moq_clock::test] async fn unknown_track_properties_are_forwarded() { - for version in [Version::Draft16, Version::Draft17, Version::Draft22] { + for version in [Version::Draft22] { let upstream = unknown_properties(version); let info = track::Info { properties: Some(Arc::new(crate::model::object::TrackProperties { @@ -10767,7 +10234,7 @@ mod serve_tests { .await; let wire = h.log.writes.lock().unwrap().clone(); let mut buf = Decoder::new(&wire, version.into()); - assert_eq!(buf.varint().unwrap(), ietf::TrackStatusOk::id(version)); + assert_eq!(buf.varint().unwrap(), ietf::TrackStatusOk::ID); let ok = ietf::TrackStatusOk::decode(&mut buf, version).unwrap(); assert_eq!(ok.properties.unknown, upstream.unknown, "{version}: TRACK_STATUS_OK"); } @@ -10806,7 +10273,7 @@ mod serve_tests { assert_eq!(occurrences(&h.log, &block[1..]), 1); // Draft 17 forbids the 7-byte varints draft 18 allows, and draft 16 has QUIC varints. - for version in [Version::Draft16, Version::Draft17] { + for version in [Version::Draft17] { let h = serve_track(version, info.clone()); let stream = Stream::open(&mut crate::driver::Transport::new(h.session.clone()), version) .await @@ -11267,10 +10734,7 @@ mod serve_tests { #[moq_clock::test] async fn a_standalone_fetch_through_the_maximum_group_is_unsupported() { for version in FETCH_DRAFTS { - let maximum = match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => (1 << 62) - 1, - _ => u64::MAX, - }; + let maximum = u64::MAX; let mut h = serve(version); publish_pairs(&mut h, 1, None); h.track.finish().unwrap(); @@ -11299,10 +10763,6 @@ mod serve_tests { let mut buf = Decoder::new(&wire, version.into()); let id = buf.varint().unwrap(); let code = match version { - Version::Draft14 => { - assert_eq!(id, ietf::FetchError::ID); - ietf::FetchError::decode(&mut buf, version).unwrap().error_code - } _ => { assert_eq!(id, ietf::RequestError::ID); ietf::RequestError::decode(&mut buf, version).unwrap().error_code @@ -11891,16 +11351,7 @@ mod serve_tests { fn withdraw(request: &ScriptedSession, version: Version) { use crate::lite::test_transport::Close; match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => { - let mut unsubscribe = vec![ietf::Unsubscribe::ID as u8]; - ietf::Unsubscribe { - request_id: RequestId(REQUEST_ID), - } - .encode(&mut Encoder::new(&mut unsubscribe, version.into()), version) - .unwrap(); - request.push(&unsubscribe); - } - Version::Draft17 | Version::Draft18 => request.close(Close::Fin), + Version::Draft18 => request.close(Close::Fin), _ => request.close(Close::Reset), } } @@ -12238,10 +11689,6 @@ mod serve_tests { #[moq_clock::test] async fn subscribe_ok_announces_the_served_order() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft20, ] { for (requested, announced) in [ @@ -12262,9 +11709,7 @@ mod serve_tests { ); let ok = crate::coding::decode_buf(&mut buf, version, ietf::SubscribeOk::decode).unwrap(); let expected = match version { - Version::Draft14 | Version::Draft15 => Some(announced), // Draft-16 has no room for it. - Version::Draft16 => None, _ => Some(GroupOrder::Descending), }; assert_eq!(ok.properties.group_order, expected, "{version:?} {requested:?}"); @@ -12275,10 +11720,6 @@ mod serve_tests { #[moq_clock::test] async fn largest_object_is_the_live_edge_before_draft20() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, ] { @@ -12501,17 +11942,10 @@ mod serve_tests { crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), version, ); - let subscription_request_id = match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(RequestId(REQUEST_ID)), - _ => None, - }; let msg = ietf::SubscribeUpdate { max_age: None, new_group: None, request_id: RequestId(0x40), - subscription_request_id, - start_location: Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: Some(128), forward: Some(forward), filter: None, @@ -12535,7 +11969,6 @@ mod serve_tests { writer .encode(&ietf::RequestOk { largest: None, - request_id: matches!(version, Version::Draft15 | Version::Draft16).then_some(request_id), active: None, }) .await @@ -12552,7 +11985,7 @@ mod serve_tests { crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), version, ); - let request_id = matches!(version, Version::Draft15 | Version::Draft16).then_some(request_id); + let request_id = false.then_some(request_id); writer.varint(ietf::RequestError::ID).await.unwrap(); writer .encode(&ietf::RequestError { @@ -12662,7 +12095,7 @@ mod serve_tests { /// `super::super::adapter::tests::test_classify_subscribe_update_followup`. #[moq_clock::test] async fn a_subscribe_update_renewal_is_answered_from_draft_15() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { renews(version, true).await; } } @@ -12672,7 +12105,7 @@ mod serve_tests { /// draft-14, a keyed REQUEST_ERROR to the update's Request ID on draft-15/16. #[moq_clock::test] async fn a_refused_subscribe_update_renewal_ends_the_subscription_from_draft_14() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { renews(version, false).await; } } @@ -12681,7 +12114,7 @@ mod serve_tests { /// both fails instead of acknowledging a token nobody checked. #[moq_clock::test] async fn a_pause_carrying_a_token_fails_the_update() { - for version in [Version::Draft15, Version::Draft16] { + for version in [Version::Draft16] { let (auth, mut requests, _cred) = auth_covering_other(); let h = serve_with_auth(version, auth, subscribe_update_with_token(version, false).await); let rt = h.publisher.runtime.clone(); @@ -12717,7 +12150,7 @@ mod serve_tests { let (status, _) = wire.publish_done.expect("PUBLISH_DONE was sent"); assert_eq!( status, - ietf::PublishDoneStatus::UpdateFailed.code(version), + ietf::PublishDoneStatus::UpdateFailed.code(), "{version:?}" ); } @@ -12798,7 +12231,6 @@ mod serve_tests { // update's own Request ID (0x40), the id the renewing peer waits on. let written = h.log.writes.lock().unwrap()[before..].to_vec(); match version { - Version::Draft14 => assert!(written.is_empty(), "{version:?}: a renewal writes no answer"), _ => assert_eq!( written, request_ok_bytes(version, RequestId(0x40)).await, @@ -12844,16 +12276,12 @@ mod serve_tests { // Draft 14 has no UPDATE_FAILED and answers no update, so its PUBLISH_DONE // carries the refusal itself. let (status, _) = Wire::read(&h.log, version).publish_done.expect("PUBLISH_DONE was sent"); - let expected = match version { - Version::Draft14 => ietf::PublishDoneStatus::Unauthorized, - _ => ietf::PublishDoneStatus::UpdateFailed, - }; + let expected = ietf::PublishDoneStatus::UpdateFailed; assert_eq!(status, expected.code(version), "{version:?}"); let written = h.log.writes.lock().unwrap()[before..].to_vec(); match version { // draft-14 has no renewal reply: the refusal is signalled only by the subscription // ending (SUBSCRIBE_DONE UNAUTHORIZED, written by the caller), with no REQUEST_ERROR. - Version::Draft14 => {} // draft-15/16 answer with one keyed REQUEST_ERROR before the subscription ends. _ => { let error = request_error_bytes(version, RequestId(0x40), "no").await; @@ -12875,17 +12303,10 @@ mod serve_tests { crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), version, ); - let subscription_request_id = match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(RequestId(REQUEST_ID)), - _ => None, - }; let msg = ietf::SubscribeUpdate { max_age: None, new_group: None, request_id: RequestId(rid), - subscription_request_id, - start_location: Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: Some(128), forward: Some(true), filter: None, @@ -12906,17 +12327,10 @@ mod serve_tests { crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), version, ); - let subscription_request_id = match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(RequestId(REQUEST_ID)), - _ => None, - }; let msg = ietf::SubscribeUpdate { max_age: None, new_group: None, request_id: RequestId(rid), - subscription_request_id, - start_location: Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: Some(128), forward: Some(true), filter: None, @@ -13204,7 +12618,6 @@ mod serve_tests { writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active: None, }) .await @@ -13232,7 +12645,7 @@ mod serve_tests { /// answer either way, and the token-less update behind it is never answered ahead of it. #[moq_clock::test] async fn an_update_behind_a_pending_renewal_is_answered_in_order() { - for version in [Version::Draft16, Version::Draft18] { + for version in [Version::Draft18] { let (auth, mut requests, _cred) = auth_covering_other(); let mut script = subscribe_update_token_rid(version, 0x40).await; script.extend(subscribe_update_bare_rid(version, 0x42).await); @@ -13298,7 +12711,7 @@ mod serve_tests { /// silent branches of the acknowledgement so a wrong key cannot misroute the peer's answer. #[moq_clock::test] async fn token_less_updates_are_keyed_before_draft_17() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { let (auth, mut requests, _cred) = auth_covering_other(); let rids = [0x42u64, 0x44, 0x46]; let mut script = Vec::new(); @@ -13335,12 +12748,10 @@ mod serve_tests { // Draft-14 answers nothing; draft-15/16 answer each update keyed to its own request id. let written = h.log.writes.lock().unwrap().clone(); - if !matches!(version, Version::Draft14) { - for rid in rids { - let one = request_ok_bytes(version, RequestId(rid)).await; - let n = written.windows(one.len()).filter(|w| *w == one.as_slice()).count(); - assert_eq!(n, 1, "{version}: update {rid:#x} gets exactly one keyed REQUEST_OK"); - } + for rid in rids { + let one = request_ok_bytes(version, RequestId(rid)).await; + let n = written.windows(one.len()).filter(|w| *w == one.as_slice()).count(); + assert_eq!(n, 1, "{version}: update {rid:#x} gets exactly one keyed REQUEST_OK"); } assert!( h.log.closes().is_empty(), @@ -13505,7 +12916,7 @@ mod serve_tests { /// acceptor sitting on the renewal, the UNSUBSCRIBE is the only thing that can. #[moq_clock::test] async fn a_pending_renewal_ends_on_an_unsubscribe_before_draft_17() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { let (auth, requests, _cred) = auth_covering_other(); // The stream carries the renewal, then the UNSUBSCRIBE, and never FINs, so only the // message can end the request. @@ -13580,7 +12991,7 @@ mod serve_tests { let (status, _) = Wire::read(&h.log, VERSION).publish_done.expect("PUBLISH_DONE was sent"); assert_eq!( status, - ietf::PublishDoneStatus::UpdateFailed.code(VERSION), + ietf::PublishDoneStatus::UpdateFailed.code(), "UPDATE_FAILED" ); // Draft-17+ answers the refused update with exactly one unkeyed REQUEST_ERROR before the @@ -13917,7 +13328,7 @@ mod serve_tests { /// one is refused NOT_SUPPORTED rather than taken unread, failing only that subscription. #[moq_clock::test] async fn a_token_on_a_subscription_admitted_without_one_is_refused() { - for version in [Version::Draft16, Version::Draft22] { + for version in [Version::Draft22] { let auth = crate::auth::Link::new(false); auth.handle().authorize(&crate::auth::Grant::all()); let h = serve_with_auth(version, auth, Vec::new()); @@ -14023,7 +13434,6 @@ mod serve_tests { writer.varint(ietf::RequestError::ID).await.unwrap(); writer .encode(&ietf::RequestError { - request_id: None, error_code: request::to_code(&Error::Unsupported, request::Kind::Subscribe, version), reason_phrase: Error::Unsupported.to_string().into(), retry_interval: 0, @@ -14315,7 +13725,6 @@ mod tests { Control::new(None, false), Some(assigned), peer::PeerSetup::default(), - Version::Draft16, ); let mut echoed_hops = crate::Hops::new(); @@ -14444,7 +13853,6 @@ mod tests { Control::new(None, false), Some(assigned), peer::PeerSetup::default(), - Version::Draft16, ); // The chain as ingress stores it: the peer named itself 0, and `via` is the @@ -14492,7 +13900,6 @@ mod tests { Control::new(None, false), None, setup, - Version::Draft17, ); let mut clean = crate::Hops::new(); @@ -14525,13 +13932,11 @@ mod tests { let expected = crate::lite::test_transport::Log::default(); let mut writer = crate::coding::Writer::new( crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(expected.clone())), - Version::Draft17, ); writer.varint(ietf::RequestOk::ID).await.unwrap(); writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active: Some(1), }) .await @@ -14563,7 +13968,6 @@ mod tests { Control::new(None, false), Some(assigned), requires_solicitation(), - Version::Draft16, ); // The prefix starts with only a route from the assigned peer: hop 0 on the @@ -14636,7 +14040,6 @@ mod tests { writer.varint(ietf::RequestError::ID).await.unwrap(); writer .encode(&ietf::RequestError { - request_id: matches!(version, Version::Draft15 | Version::Draft16).then_some(RequestId(1)), // UNINTERESTED, draft-17 section 14.5.2. error_code: 0x20, reason_phrase: "no".into(), @@ -14790,33 +14193,12 @@ mod tests { ); match version { - Version::Draft14 => { - writer.varint(ietf::PublishNamespaceOk::ID).await.unwrap(); - writer - .encode(&ietf::PublishNamespaceOk { - request_id: RequestId(1), - }) - .await - .unwrap(); - } - Version::Draft15 | Version::Draft16 => { - writer.varint(ietf::RequestOk::ID).await.unwrap(); - writer - .encode(&ietf::RequestOk { - largest: None, - request_id: Some(RequestId(1)), - active: None, - }) - .await - .unwrap(); - } // Draft-17+ dropped the request id: the response rides the request's stream. _ => { writer.varint(ietf::RequestOk::ID).await.unwrap(); writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active: None, }) .await @@ -14968,7 +14350,7 @@ mod tests { /// plus STOP_SENDING. A FIN there is a protocol violation some peers close on. #[moq_clock::test] async fn publish_namespace_withdraws_by_reset_from_draft_17() { - for version in [Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft22] { + for version in [Version::Draft18, Version::Draft19, Version::Draft22] { let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let local = origin.announce("local-cam", crate::origin::Route::default()).unwrap(); settle().await; @@ -15046,7 +14428,6 @@ mod tests { writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active: Some(0), }) .await @@ -15219,9 +14600,6 @@ mod tests { #[moq_clock::test] async fn hidden_discovery_obeys_peer_setup_and_requested_prefix() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, Version::Draft19, Version::Draft22, ] { @@ -15244,7 +14622,7 @@ mod tests { // the two paths disjoint; draft-16 and later fill the stream too. let on_stream = !declared || hidden || prefix == ".stats"; let unsolicited = solicit != Some(true) && !declared; - let legacy = matches!(version, Version::Draft14 | Version::Draft15); + let legacy = false; let inline = if legacy { (solicit == Some(true) && on_stream) || (solicit != Some(true) && declared && (hidden || prefix == ".stats")) @@ -15279,7 +14657,7 @@ mod tests { assert_eq!(once, 1, "a peer that asked to be told on request is told once"); assert_eq!(streams, 1, "inline on the SUBSCRIBE_NAMESPACE stream it asked on"); - for version in [Version::Draft14, Version::Draft15] { + for version in [Version::Draft15] { let log = advertise_with_hidden(Discovery { version, solicit: Some(false), @@ -15299,7 +14677,7 @@ mod tests { /// after, then NAMESPACE_DONE when that announcement ends. #[moq_clock::test] async fn a_non_solicit_subscribe_namespace_carries_namespace() { - for version in [Version::Draft16, Version::Draft18] { + for version in [Version::Draft18] { let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let early = origin.announce("early-cam", crate::origin::Route::default()).unwrap(); settle().await; @@ -15487,7 +14865,6 @@ mod tests { Control::new(None, false), Some(assigned), declared(Some(false)), - Version::Draft17, ); // The state a refused or failed offer leaves behind: the peer holds nothing, and @@ -15748,7 +15125,6 @@ mod tests { writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active: Some(0), }) .await @@ -15966,10 +15342,7 @@ mod tests { // From draft 18 the update goes unanswered: an answer scripted ahead of it would // arrive while the request is idle. let ok = publish_namespace_ok(version).await; - let script = match version { - Version::Draft16 => [ok.clone(), ok].concat(), - _ => ok, - }; + let script = ok; let (publisher, log) = auth_publisher(&origin, &auth, vec![script], version); let mut run = std::pin::pin!(publisher.run_publish_namespaces()); poll_until(&mut run, &log, b"cam", 1).await; @@ -15989,7 +15362,7 @@ mod tests { version, &ietf::PublishNamespaceUpdate { request_id: RequestId(3), - namespace_request_id: (version == Version::Draft16).then_some(RequestId(1)), + namespace_request_id: (false).then_some(RequestId(1)), hops: None, cost: None, authorization_token: Some(auth_token(b"second-secret")), @@ -16307,7 +15680,7 @@ mod tests { /// and the failed one is offered again after a backoff rather than in a loop. #[moq_clock::test] async fn a_failed_advertisement_ends_only_its_namespace() { - for version in [Version::Draft14, Version::Draft19] { + for version in [Version::Draft19] { for fin in [true, false] { fails_one_advertisement(version, fin).await; } @@ -16385,7 +15758,6 @@ mod tests { cams.remove(second); match version { - Version::Draft14 => assert_eq!(held.reader.varint().await.unwrap(), ietf::PublishNamespaceDone::ID), _ => assert!( held.reader.varint().await.is_err(), "{version} fin={fin}: not withdrawn" @@ -16401,7 +15773,7 @@ mod tests { async fn a_failed_legacy_header_closes_the_session() { use crate::transport::{memory, poll::Session as _}; - for version in [Version::Draft14, Version::Draft15, Version::Draft19] { + for version in [Version::Draft19] { let origin = unheld_origin(); // A token too big for the header's u16 length, so the header fails after its stream // opened and its request ID was spent. A message goes out whole, so this is the one @@ -16434,33 +15806,15 @@ mod tests { .expect("no advertisement") .unwrap(); - match version { - Version::Draft14 | Version::Draft15 => { - let closed = moq_clock::timeout(Duration::from_secs(10), peer.closed()) - .await - .unwrap_or_else(|_| panic!("{version}: the session stayed open")); - let code = super::super::error::to_session_code(&crate::SessionError::Internal); - assert!( - matches!(closed, memory::Error::Session { code: c, .. } if c == code), - "{version}: {closed:?}" - ); - let res = moq_clock::timeout(Duration::from_secs(10), run) - .await - .unwrap_or_else(|_| panic!("{version}: the loop kept running")); - assert!(res.unwrap().is_err(), "{version}: the loop ended cleanly"); - } - _ => { - // Offered again after the backoff, on a session that stays open. - moq_clock::timeout( - Duration::from_secs(10), - Stream::accept(&mut crate::driver::Transport::new(peer.clone()), version), - ) - .await - .unwrap_or_else(|_| panic!("{version}: not offered again")) - .unwrap(); - assert!(!run.is_finished(), "{version}: the loop ended"); - } - } + // Offered again after the backoff, on a session that stays open. + moq_clock::timeout( + Duration::from_secs(10), + Stream::accept(&mut crate::driver::Transport::new(peer.clone()), version), + ) + .await + .unwrap_or_else(|_| panic!("{version}: not offered again")) + .unwrap(); + assert!(!run.is_finished(), "{version}: the loop ended"); } } @@ -16554,10 +15908,6 @@ mod tests { 2, "{version}: the unanswered renewal was never retried" ); - if version == Version::Draft16 { - let done = publish_namespace_done(version).await; - assert_eq!(occurrences(&log, &done), 1, "the peer was never told to let it go"); - } } /// moq-transport has no restart: another source winning a namespace without an epoch @@ -16785,7 +16135,7 @@ mod tests { /// withdrawn with PUBLISH_NAMESPACE_DONE; from draft 17 its own stream's reset ends it. #[moq_clock::test] async fn an_unanswered_advertisement_is_withdrawn() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16, Version::Draft17] { + for version in [Version::Draft15] { let origin = unheld_origin(); let _cam = origin.announce("cam", crate::origin::Route::default()).unwrap(); settle().await; @@ -16805,10 +16155,7 @@ mod tests { let mut run = std::pin::pin!(publisher.run_publish_namespaces()); poll_until(&mut run, &log, b"cam", 1).await; - let done = match version { - Version::Draft17 => None, - _ => Some(publish_namespace_done(version).await), - }; + let done = Some(publish_namespace_done(version).await); let withdrawn = || match &done { Some(done) => occurrences(&log, done) > 0, None => !log.resets().is_empty(), @@ -16833,10 +16180,6 @@ mod tests { async fn an_advertisement_without_credit_is_retried_and_spends_no_id() { use std::sync::atomic::Ordering; for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft19, ] { let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); @@ -17053,7 +16396,7 @@ mod tests { /// on a request we already refused. Finishing first makes the drop-time reset a no-op. #[moq_clock::test] async fn missing_broadcast_is_refused_without_resetting_the_stream() { - for version in [Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20] { + for version in [Version::Draft18, Version::Draft19, Version::Draft20] { let (writes, resets) = subscribe_missing(version).await; assert!(!writes.is_empty(), "{version}: nothing was sent"); @@ -17093,7 +16436,7 @@ mod tests { ] }; - for version in [Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20] { + for version in [Version::Draft18, Version::Draft19, Version::Draft20] { for (label, fetch_type) in refused() { let (writes, resets) = fetch_refused(version, fetch_type).await; @@ -17877,7 +17220,7 @@ mod compression_tests { use super::*; #[test] fn sealed_compression_refuses_incompatible_egress() { - for version in [Version::Draft17, Version::Draft18, Version::Draft22] { + for version in [Version::Draft18, Version::Draft22] { let mut sealed = Vec::new(); let mut encoder = Encoder::new(&mut sealed, version.into()); encoder.varint(super::super::flate::COMPRESSION).unwrap(); @@ -17919,13 +17262,13 @@ mod compression_tests { Err(Error::Unsupported) )); assert!(matches!( - track_properties(&info, wanted, Version::Draft16, true), + track_properties(&info, wanted, true), Err(Error::Unsupported) )); } let plain = track::Info::default().with_compress(true); assert!(!track_properties(&plain, true, version, false).unwrap().compress); - assert!(!track_properties(&plain, true, Version::Draft15, true).unwrap().compress); + assert!(!track_properties(&plain, true, true).unwrap().compress); } } @@ -17957,7 +17300,6 @@ mod compression_tests { group.finish().unwrap(); track.finish().unwrap(); let response = ietf::SubscribeOk { - request_id: None, track_alias: 0, largest: None, properties: track_properties(track.info(), true, version, negotiated).unwrap(), diff --git a/rs/moq-net/src/ietf/publisher/tracks.rs b/rs/moq-net/src/ietf/publisher/tracks.rs index cd38ab88291..1756a70ed59 100644 --- a/rs/moq-net/src/ietf/publisher/tracks.rs +++ b/rs/moq-net/src/ietf/publisher/tracks.rs @@ -289,7 +289,6 @@ impl Publisher { return super::super::request_stream::reject(stream, error).await; } let refuse = |error_code, reason: &'static str| ietf::RequestError { - request_id: None, error_code, reason_phrase: reason.into(), retry_interval: 0, @@ -341,7 +340,6 @@ impl Publisher { stream .writer .encode_message(&ietf::RequestOk { - request_id: None, active: None, largest: None, }) @@ -539,7 +537,6 @@ impl Publisher { stream.writer.buffer_message( ietf::RequestError::ID, &ietf::RequestError { - request_id: None, error_code: code, reason_phrase: reason.into(), retry_interval: 0, @@ -870,22 +867,16 @@ impl Publisher { let dynamic = properties.dynamic_groups; let absolute = self.origin.absolute(&namespace).to_owned(); - // Every wait on the peer, for a Request ID or the PUBLISH's bytes, lasts only while - // the track is listed and the request stands. A Request ID abandoned unsent leaves a - // gap, which the drafts with SUBSCRIBE_TRACKS allow: none has MAX_REQUEST_ID. + // Every wait on the peer for the PUBLISH's bytes lasts only while the track is listed + // and the request stands. let mut session = self.session.clone(); - let mut stream = super::super::request_stream::with_stats(stream, self.stats(), self.version); - let allocated = kio::wait(|waiter| { - if listed.poll_closed(waiter).is_ready() || live.poll_closed(waiter).is_ready() { - return Poll::Ready(None); - } - self.control.poll_next_request_id(waiter).map(Some) - }) - .await; + let mut stream = stream.with_stats(self.stats()); // Dropped unwritten, the stream is reset before its header, which the peer ignores. - let Some(request_id) = allocated else { + let noop = kio::Waiter::noop(); + if listed.poll_closed(&noop).is_ready() || live.poll_closed(&noop).is_ready() { return Ok(()); - }; + } + let request_id = self.control.next_request_id(); // A joining FETCH needs the subscription forwarding. let join = (!Filter::is_draft20(self.version)).then(|| self.register_join(request_id)); @@ -989,7 +980,7 @@ impl Publisher { if let Some(done) = refused { drop(join); - return self.write_publish_done(stream, request_id, done, 0, None).await; + return self.write_publish_done(stream, done, 0, None).await; } // Group streams opened over every time the subscription forwarded, which @@ -1020,7 +1011,7 @@ impl Publisher { { Ok(Served::Paused(_)) => { self.pause_join(join.as_ref()); - self.write_request_ok(&mut stream.writer, request_id, None).await?; + self.write_request_ok(&mut stream.writer, None).await?; // Unlisted while it forwarded: it ends now that it is idle. if listed.is_closed() { break unlisted(&listed); @@ -1099,7 +1090,7 @@ impl Publisher { None => None, }; if let Some((reason, err)) = refusal { - self.write_subscribe_error(&mut stream.writer, update.request_id, &err, reason) + self.write_subscribe_error(&mut stream.writer, &err, reason) .await?; break Served::UpdateFailed(Error::Unsupported); } @@ -1120,12 +1111,12 @@ impl Publisher { answer: true, }) } - _ => self.write_request_ok(&mut stream.writer, request_id, None).await?, + _ => self.write_request_ok(&mut stream.writer, None).await?, } }; drop(join); - self.write_publish_done(stream, request_id, done, opened, None).await + self.write_publish_done(stream, done, opened, None).await } /// Mark a paused subscription for joining FETCHes: draft-18 section 5.1 refuses one @@ -1142,14 +1133,13 @@ impl Publisher { async fn resume_failed( &self, stream: &mut Stream, - request_id: RequestId, answer: bool, err: Error, ) -> Result { if !answer { return Ok(Served::Track(Err(err))); } - self.write_subscribe_error(&mut stream.writer, request_id, &err, "resume failed") + self.write_subscribe_error(&mut stream.writer, &err, "resume failed") .await .map_err(|_| ())?; Ok(Served::UpdateFailed(err)) @@ -1212,10 +1202,10 @@ impl Publisher { .await?; let mut subscriber = match subscribed { Some(Ok(subscriber)) => subscriber, - Some(Err(err)) => return self.resume_failed(stream, request_id, how.answer, err).await, + Some(Err(err)) => return self.resume_failed(stream, how.answer, err).await, None => { return self - .resume_failed(stream, request_id, how.answer, Error::NotFound) + .resume_failed(stream, how.answer, Error::NotFound) .await; } }; @@ -1231,7 +1221,7 @@ impl Publisher { .await?; if !live { return self - .resume_failed(stream, request_id, how.answer, Error::NotFound) + .resume_failed(stream, how.answer, Error::NotFound) .await; } @@ -1242,7 +1232,7 @@ impl Publisher { let timescale = subscriber .info() .timescale - .filter(|_| ietf::Properties::sends_timescale(self.version)); +; let mut serve = TrackServe::new( self.session.clone(), subscriber, @@ -1282,7 +1272,7 @@ impl Publisher { if how.answer && self - .write_request_ok(&mut stream.writer, request_id, edge.largest) + .write_request_ok(&mut stream.writer, edge.largest) .await .is_err() { @@ -1494,7 +1484,6 @@ mod tests { stream .writer .encode(&ietf::PublishOk { - request_id: None, forward: false, subscriber_priority: Some(128), group_order: Some(GroupOrder::Ascending), @@ -1590,7 +1579,7 @@ mod tests { let done: ietf::PublishDone = body.message(version).unwrap(); assert_eq!( done.status_code, - ietf::PublishDoneStatus::TrackEnded.code(version), + ietf::PublishDoneStatus::TrackEnded.code(), "{version}" ); } @@ -1626,7 +1615,7 @@ mod tests { let (id, body) = next(published.get_mut(name).unwrap()).await; assert_eq!(id, ietf::PublishDone::ID, "{case}: {name}"); let done: ietf::PublishDone = body.message(version).unwrap(); - assert_eq!(done.status_code, status.code(version), "{case}: {name}"); + assert_eq!(done.status_code, status.code(), "{case}: {name}"); } drop(video); } @@ -2033,7 +2022,6 @@ mod tests { stream .writer .encode(&ietf::PublishOk { - request_id: None, forward: true, subscriber_priority: None, group_order: None, @@ -2067,7 +2055,6 @@ mod tests { stream .writer .encode(&ietf::PublishOk { - request_id: None, forward: true, subscriber_priority: Some(128), group_order: Some(GroupOrder::Ascending), diff --git a/rs/moq-net/src/ietf/request.rs b/rs/moq-net/src/ietf/request.rs index c5df80a756a..cd8f2c0e9e9 100644 --- a/rs/moq-net/src/ietf/request.rs +++ b/rs/moq-net/src/ietf/request.rs @@ -42,50 +42,9 @@ impl Decode for RequestId { } } -#[derive(Clone, Debug)] -pub struct MaxRequestId { - pub request_id: RequestId, -} - -impl Message for MaxRequestId { - const ID: u64 = 0x15; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - self.request_id.encode(w, version)?; - Ok(()) - } - - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = RequestId::decode(r, version)?; - Ok(Self { request_id }) - } -} - -#[derive(Clone, Debug)] -pub struct RequestsBlocked { - pub request_id: RequestId, -} - -impl Message for RequestsBlocked { - const ID: u64 = 0x1a; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - self.request_id.encode(w, version)?; - Ok(()) - } - - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = RequestId::decode(r, version)?; - Ok(Self { request_id }) - } -} - -/// REQUEST_OK (0x07 in v15) - Generic success response for any request. -/// Replaces PublishNamespaceOk, SubscribeNamespaceOk in v15. -/// Also used as response to SubscribeUpdate and TrackStatus in v15. +/// REQUEST_OK (0x07): the generic success response for any request, on its own stream. #[derive(Clone, Debug)] pub struct RequestOk { - pub request_id: Option, /// MoQ Active Count: how many NAMESPACE messages follow before the subscription is /// synced. Only on the answer to a SUBSCRIBE_NAMESPACE, when negotiated. pub active: Option, @@ -98,13 +57,6 @@ impl Message for RequestOk { const ID: u64 = 0x07; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - self.request_id - .expect("request_id required for draft14-16") - .encode(w, version)?; - } else { - assert!(self.request_id.is_none(), "request_id must be None for draft17+"); - } encode_params!(w, version, 0x09 => self.largest, ACTIVE_COUNT_PARAM => self.active, @@ -113,11 +65,6 @@ impl Message for RequestOk { } fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - Some(RequestId::decode(r, version)?) - } else { - None - }; // A REQUEST_UPDATE_OK may refresh EXPIRES, which is ignored like SUBSCRIBE_OK's. // LARGEST_OBJECT is a MUST on a REQUEST_UPDATE_OK or TRACK_STATUS_OK once the track // has objects. @@ -126,59 +73,34 @@ impl Message for RequestOk { 0x09 => largest: Option, ACTIVE_COUNT_PARAM => active: Option, ); - Ok(Self { - request_id, - active, - largest, - }) + Ok(Self { active, largest }) } } -/// REQUEST_ERROR (0x05 in v15) - Generic error response for any request. -/// Replaces SubscribeError, PublishError, PublishNamespaceError, -/// SubscribeNamespaceError, FetchError in v15. +/// REQUEST_ERROR (0x05): the generic error response for any request, on its own stream. #[derive(Clone, Debug)] pub struct RequestError<'a> { - pub request_id: Option, pub error_code: u64, pub reason_phrase: Cow<'a, str>, - /// v16+: retry interval in milliseconds + /// Retry interval in milliseconds. pub retry_interval: u64, } impl Message for RequestError<'_> { const ID: u64 = 0x05; - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - self.request_id - .expect("request_id required for draft14-16") - .encode(w, version)?; - } else { - assert!(self.request_id.is_none(), "request_id must be None for draft17+"); - } + fn encode_msg(&self, w: &mut Encoder<'_>, _: Version) -> Result<(), EncodeError> { w.varint(self.error_code)?; - if !matches!(version, Version::Draft14 | Version::Draft15) { - w.varint(self.retry_interval)?; - } + w.varint(self.retry_interval)?; w.string(&self.reason_phrase)?; Ok(()) } - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - Some(RequestId::decode(r, version)?) - } else { - None - }; + fn decode_msg(r: &mut Decoder<'_>, _: Version) -> Result { let error_code = r.varint()?; - let retry_interval = match version { - Version::Draft14 | Version::Draft15 => 0, - _ => r.varint()?, - }; + let retry_interval = r.varint()?; let reason_phrase = Cow::Owned(r.string()?); Ok(Self { - request_id, error_code, reason_phrase, retry_interval, @@ -202,61 +124,31 @@ mod tests { crate::coding::decode_buf(&mut buf, version, M::decode_msg) } - #[test] - fn test_request_ok_round_trip() { - let msg = RequestOk { - largest: None, - request_id: Some(RequestId(42)), - active: None, - }; - - let encoded = encode_message(&msg, Version::Draft15); - let decoded: RequestOk = decode_message(&encoded, Version::Draft15).unwrap(); - - assert_eq!(decoded.request_id, Some(RequestId(42))); - } - - /// A REQUEST_UPDATE_OK may refresh EXPIRES; it is ignored rather than rejected. - #[test] - fn test_request_ok_ignores_expires() { - let bytes = [0x07, 0x01, 0x08, 0x05]; - - let decoded: RequestOk = decode_message(&bytes, Version::Draft16).unwrap(); - assert_eq!(decoded.request_id, Some(RequestId(7))); - } - - /// A REQUEST_UPDATE_OK or TRACK_STATUS_OK carries LARGEST_OBJECT; it is ignored rather - /// than rejected. Draft-14 has no REQUEST_OK, and the Location is length-prefixed until - /// draft-17. + /// A REQUEST_UPDATE_OK may refresh EXPIRES, and a REQUEST_UPDATE_OK or TRACK_STATUS_OK + /// carries LARGEST_OBJECT; neither is rejected. #[test] fn test_request_ok_ignores_largest_object() { for version in [ - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, Version::Draft21, Version::Draft22, ] { - let bytes: &[u8] = match version { - Version::Draft15 | Version::Draft16 => &[0x07, 0x01, 0x09, 0x02, 0x05, 0x03], - _ => &[0x01, 0x09, 0x05, 0x03], - }; - let mut buf = Decoder::new(bytes, version.into()); - RequestOk::decode_msg(&mut buf, version).unwrap_or_else(|e| panic!("{version}: {e}")); - assert!(buf.is_empty(), "{version}: trailing bytes"); + for bytes in [&[0x01, 0x08, 0x05][..], &[0x01, 0x09, 0x05, 0x03]] { + let mut buf = Decoder::new(bytes, version.into()); + RequestOk::decode_msg(&mut buf, version).unwrap_or_else(|e| panic!("{version}: {e}")); + assert!(buf.is_empty(), "{version}: trailing bytes"); + } } } #[test] fn test_request_ok_active_round_trips() { - for (version, request_id) in [(Version::Draft16, Some(RequestId(3))), (Version::Draft22, None)] { + for version in [Version::Draft18, Version::Draft22] { for count in [None, Some(0), Some(7)] { let msg = RequestOk { largest: None, - request_id, active: count, }; let decoded: RequestOk = decode_message(&encode_message(&msg, version), version).unwrap(); @@ -268,103 +160,6 @@ mod tests { #[test] fn test_request_error_round_trip() { let msg = RequestError { - request_id: Some(RequestId(99)), - error_code: 500, - reason_phrase: "Internal error".into(), - retry_interval: 0, - }; - - let encoded = encode_message(&msg, Version::Draft15); - let decoded: RequestError = decode_message(&encoded, Version::Draft15).unwrap(); - - assert_eq!(decoded.request_id, Some(RequestId(99))); - assert_eq!(decoded.error_code, 500); - assert_eq!(decoded.reason_phrase, "Internal error"); - assert_eq!(decoded.retry_interval, 0); - } - - #[test] - fn test_request_error_v16_retry_interval() { - let msg = RequestError { - request_id: Some(RequestId(99)), - error_code: 500, - reason_phrase: "Internal error".into(), - retry_interval: 5000, - }; - - let encoded = encode_message(&msg, Version::Draft16); - let decoded: RequestError = decode_message(&encoded, Version::Draft16).unwrap(); - - assert_eq!(decoded.request_id, Some(RequestId(99))); - assert_eq!(decoded.error_code, 500); - assert_eq!(decoded.reason_phrase, "Internal error"); - assert_eq!(decoded.retry_interval, 5000); - } - - #[test] - fn test_request_ok_v17_round_trip() { - let msg = RequestOk { - largest: None, - request_id: None, - active: None, - }; - - let encoded = encode_message(&msg, Version::Draft17); - let decoded: RequestOk = decode_message(&encoded, Version::Draft17).unwrap(); - - assert_eq!(decoded.request_id, None); - } - - #[test] - fn test_request_error_v17_round_trip() { - let msg = RequestError { - request_id: None, - error_code: 500, - reason_phrase: "Internal error".into(), - retry_interval: 3000, - }; - - let encoded = encode_message(&msg, Version::Draft17); - let decoded: RequestError = decode_message(&encoded, Version::Draft17).unwrap(); - - assert_eq!(decoded.request_id, None); - assert_eq!(decoded.error_code, 500); - assert_eq!(decoded.reason_phrase, "Internal error"); - assert_eq!(decoded.retry_interval, 3000); - } - - #[test] - fn test_request_ok_v18_round_trip() { - let msg = RequestOk { - largest: None, - request_id: None, - active: None, - }; - - let encoded = encode_message(&msg, Version::Draft18); - let decoded: RequestOk = decode_message(&encoded, Version::Draft18).unwrap(); - - assert_eq!(decoded.request_id, None); - } - - /// Regression: pre-fix, the `version != Draft17` check caused Draft18 to be - /// treated as Draft14-16 and panic in the encoder. - #[test] - fn test_request_ok_v18_wire_matches_v17() { - let msg = RequestOk { - largest: None, - request_id: None, - active: None, - }; - let v17 = encode_message(&msg, Version::Draft17); - let v18 = encode_message(&msg, Version::Draft18); - assert_eq!(v17, v18); - } - - #[test] - fn test_request_error_v18_round_trip() { - let msg = RequestError { - request_id: None, error_code: 500, reason_phrase: "Internal error".into(), retry_interval: 3000, @@ -373,7 +168,6 @@ mod tests { let encoded = encode_message(&msg, Version::Draft18); let decoded: RequestError = decode_message(&encoded, Version::Draft18).unwrap(); - assert_eq!(decoded.request_id, None); assert_eq!(decoded.error_code, 500); assert_eq!(decoded.reason_phrase, "Internal error"); assert_eq!(decoded.retry_interval, 3000); diff --git a/rs/moq-net/src/ietf/request_stream.rs b/rs/moq-net/src/ietf/request_stream.rs index ae9ee8a0be1..7a6b24c3b9d 100644 --- a/rs/moq-net/src/ietf/request_stream.rs +++ b/rs/moq-net/src/ietf/request_stream.rs @@ -8,24 +8,10 @@ use crate::{ coding::{Decode, DecodeError, Decoder, Stream}, }; -/// Legacy receives are framed once by the physical multiplexer, not again by -/// the virtual request decoder. Encodes still count at their typed codec. -pub(super) fn with_stats( - stream: Stream, - stats: &crate::stats::Session, - version: Version, -) -> Stream { - let mut stream = stream.with_stats(stats); - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - stream.reader = stream.reader.with_stats(&Default::default()); - } - stream -} - pub(super) fn fin_cancels(version: Version) -> bool { matches!( version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 | Version::Draft18 + Version::Draft18 ) } @@ -38,21 +24,13 @@ pub(super) fn poll_cancel( cx: &mut Context<'_>, ) -> Poll> { if !*finished { - let closed = match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => poll_legacy_end(stream, version, cx), - _ => messages.poll_end(&mut stream.reader, cx), - }; - match closed { + match messages.poll_end(&mut stream.reader, cx) { Poll::Ready(Ok(())) if !fin_cancels(version) => *finished = true, Poll::Ready(result) => return Poll::Ready(result), Poll::Pending => {} } } - // Virtual writers on drafts 14-16 are already closed while their control request lives. - if !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - return stream.writer.poll_closed(cx); - } - Poll::Pending + stream.writer.poll_closed(cx) } /// Watch for the requester ending a request it has not been answered on yet, without consuming @@ -64,16 +42,6 @@ pub(super) fn poll_withdrawn( version: Version, cx: &mut Context<'_>, ) -> Poll<()> { - // Drafts 14-16 deliver the terminal message (UNSUBSCRIBE, PUBLISH_NAMESPACE_DONE) ahead of - // the FIN, so a waiting message that is not an update already withdraws the request. Their - // virtual writers are closed while the control request lives, so only the reader counts. - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - return match stream.reader.poll_varint_peek(cx) { - Poll::Ready(Ok(id)) if id == super::SubscribeUpdate::ID => Poll::Pending, - Poll::Ready(_) => Poll::Ready(()), - Poll::Pending => Poll::Pending, - }; - } if stream.writer.poll_closed(cx).is_ready() { return Poll::Ready(()); } @@ -84,83 +52,6 @@ pub(super) fn poll_withdrawn( } } -/// Poll a draft 14-16 virtual request stream until the requester ends the request. -/// -/// An update modifies the request rather than ending it, and the requests watched here -/// apply none, so each is refused with [`refuse_update`] and the request stays open. Any -/// other message is `Err(Cancel)`, and the stream finishing is `Ok(())`. -pub(super) fn poll_legacy_end( - stream: &mut Stream, - version: Version, - cx: &mut Context<'_>, -) -> Poll> { - loop { - // A refusal goes out before the next message is read. - ready!(stream.writer.poll_flush(cx))?; - match ready!(stream.reader.poll_decode_maybe::(cx))? { - Some(FollowUp::Update(body)) => refuse_update(&mut stream.writer, &body, version)?, - Some(FollowUp::End) => return Poll::Ready(Err(Error::Cancel)), - None => return Poll::Ready(Ok(())), - } - } -} - -/// Buffer the answer to a draft 14-16 update aimed at a request that takes none. -/// -/// Draft 14 answers no update. Drafts 15 and 16 answer each with exactly one REQUEST_OK or -/// REQUEST_ERROR naming the update's own Request ID, and a refusal leaves any request but a -/// subscription standing, so NOT_SUPPORTED is the whole answer. -pub(super) fn refuse_update( - writer: &mut crate::coding::Writer, - body: &super::Body, - version: Version, -) -> Result<(), Error> { - if version == Version::Draft14 { - return Ok(()); - } - let request_id = super::RequestId::decode(&mut body.decoder(version), version).map_err(DecodeError::complete)?; - writer.buffer_message( - super::RequestError::ID, - &super::RequestError { - request_id: Some(request_id), - error_code: super::error::request::to_code( - &Error::Unsupported, - super::error::request::Kind::Subscribe, - version, - ), - reason_phrase: "update not supported".into(), - retry_interval: 0, - }, - ) -} - -/// A message the adapter routed onto a draft 14-16 request after the request itself. -/// -/// Those drafts carry every request on the control stream, and the adapter delivers -/// each message to the request it names. -#[derive(Debug)] -pub(super) enum FollowUp { - /// SUBSCRIBE_UPDATE, or REQUEST_UPDATE on draft 16, still undecoded. - Update(super::Body), - /// UNSUBSCRIBE, FETCH_CANCEL, or any other message, which ends the request. - End, -} - -impl Decode for FollowUp { - fn decode(r: &mut Decoder<'_>, version: Version) -> Result { - let id = r.varint()?; - let update = id == super::SubscribeUpdate::ID; - let start = r.position(); - let body = super::Body::decode(r, version)?; - r.control = Some(super::message::control(id, version)); - r.uncounted = start; - Ok(match update { - true => Self::Update(body), - false => Self::End, - }) - } -} - /// One whole `[type][length][body]` message off a request stream, decoded at once so a read /// dropped part-way consumes nothing. #[derive(Debug)] @@ -171,14 +62,14 @@ impl Decode for Framed { let id = r.varint()?; let start = r.position(); let body = super::Body::decode(r, version)?; - r.control = Some(super::message::control(id, version)); + r.control = Some(super::message::control(id)); r.uncounted = start; Ok(Self(id, body)) } } -/// A message following the request on a request stream: `T`, or from draft 18 a GOAWAY -/// migrating this one request (draft-21 section 9.2). +/// A message following the request on a request stream: `T`, or a GOAWAY migrating this +/// one request (draft-21 section 9.2). #[derive(Debug)] pub(super) enum Next { Message(T), @@ -190,23 +81,18 @@ pub(super) enum Next { impl> Decode for Next { fn decode(r: &mut Decoder<'_>, version: Version) -> Result { - if !matches!( - version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 - ) { - let mut peek = r.clone(); - if peek.varint()? == super::GoAway::ID { - *r = peek; - r.uncounted = r.position(); - let msg = super::GoAway::decode(r, version)?; - // Draft-18's Request ID belongs on the control stream only. - if msg.request_id.is_some() { - return Err(DecodeError::InvalidValue); - } - return Ok(Self::GoAway { - redirect: !msg.new_session_uri.is_empty(), - }); + let mut peek = r.clone(); + if peek.varint()? == super::GoAway::ID { + *r = peek; + r.uncounted = r.position(); + let msg = super::GoAway::decode(r, version)?; + // Draft-18's Request ID belongs on the control stream only. + if msg.request_id.is_some() { + return Err(DecodeError::InvalidValue); } + return Ok(Self::GoAway { + redirect: !msg.new_session_uri.is_empty(), + }); } T::decode(r, version).map(Self::Message) } @@ -325,33 +211,17 @@ pub(super) async fn reject( Ok(()) } -/// Write the error refusing a `kind` request with `err`, as the negotiated draft spells it. -/// -/// Each draft-14 error message has the REQUEST_ERROR body that draft-15 folded them into, so -/// only the message type changes; draft-17 then drops the Request ID. +/// Write the REQUEST_ERROR refusing a `kind` request with `err`. pub(super) async fn write_error( writer: &mut crate::coding::Writer, kind: super::error::request::Kind, - request_id: super::RequestId, err: &Error, reason: &str, version: Version, ) -> Result<(), Error> { - use super::error::request::Kind; - - let id = match (version, kind) { - (Version::Draft14, Kind::Subscribe) => super::SubscribeError::ID, - (Version::Draft14, Kind::Fetch) => super::FetchError::ID, - (Version::Draft14, Kind::Publish) => super::PublishError::ID, - (Version::Draft14, Kind::PublishNamespace) => super::PublishNamespaceError::ID, - (Version::Draft14, Kind::SubscribeNamespace) => super::SubscribeNamespaceError::ID, - (Version::Draft14, Kind::TrackStatus) => super::TRACK_STATUS_ERROR_14, - _ => super::RequestError::ID, - }; writer.buffer_message( - id, + super::RequestError::ID, &super::RequestError { - request_id: matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16).then_some(request_id), error_code: super::error::request::to_code(err, kind, version), reason_phrase: reason.into(), retry_interval: 0, @@ -363,7 +233,7 @@ pub(super) async fn write_error( /// A framed subscription update, preserving omitted preferences. #[derive(Debug)] pub(super) struct Update { - /// The first Request ID: the update's own on drafts 14-16, which their answer names. + /// The update's own Request ID. pub(super) request_id: super::RequestId, pub(super) priority: Option, pub(super) max_age: Option, @@ -374,7 +244,7 @@ pub(super) struct Update { pub(super) refused: Option, /// The AUTHORIZATION TOKEN renewing the request's credential. pub(super) authorization_token: Option, - /// NEW_GROUP_REQUEST (draft-15+), which only a track advertising DYNAMIC_GROUPS takes. + /// NEW_GROUP_REQUEST, which only a track advertising DYNAMIC_GROUPS takes. pub(super) new_group: Option, /// FORWARD, `None` when unchanged. pub(super) forward: Option, @@ -385,63 +255,14 @@ pub(super) struct Update { } impl Update { - /// Decode a draft 14-16 update the adapter routed onto its subscription. - pub(super) fn decode_legacy(body: &super::Body, version: Version) -> Result { - let mut data = body.decoder(version); - // The body is complete, so running short inside it is malformed. - Self::decode_body(&mut data, version) - .map_err(DecodeError::complete) - .map_err(Into::into) - } - /// Decode the message body, after its type and length. fn decode_body(data: &mut Decoder<'_>, version: Version) -> Result { - use super::{Fill, Filter, Location, Opaque, RequestId}; + use super::{Fill, Filter, Opaque, RequestId}; let request_id = RequestId::decode(data, version)?; - match version { - Version::Draft14 => { - // The subscription it updates, which the adapter already routed by. - RequestId::decode(data, version)?; - // The range is left as it is: every field is mandatory, so a narrowed range - // cannot be told from a repeated one, and draft 14 accepts extra objects as - // the worst outcome of an update. - Location::decode(data, version)?; - let _end_group = data.varint()?; - let priority = data.u8()?; - let forward = data.bool()?; - decode_params!(data, version, - 0x03 => tokens: Vec, - super::max_age::PARAMETER => max_age: Option, - ); - if !data.is_empty() { - return Err(DecodeError::InvalidValue); - } - let (refused, authorization_token) = Self::refused(tokens, false); - return Ok(Self { - request_id, - priority: Some(priority), - max_age: super::max_age::validate(max_age)?, - refused, - authorization_token, - new_group: None, - forward: Some(forward), - filter: None, - fill: None, - }); - } - // The request it updates, which the adapter already routed by. - Version::Draft15 | Version::Draft16 => { - RequestId::decode(data, version)?; - } - Version::Draft17 => { - data.varint()?; - } - _ => {} - } decode_params!(data, version, 0x02 => object_timeout: Option, 0x03 => token: Vec, - 0x06 => subgroup_timeout: Option where !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17), + 0x06 => subgroup_timeout: Option, 0x10 => forward: Option, 0x20 => priority: Option, 0x21 => filter: Option, @@ -451,7 +272,7 @@ impl Update { 0x27 => priority_filter: Vec where super::subscribe::has_range_filters(version), 0x28 => property_filter: Vec where super::subscribe::has_range_filters(version), 0x29 => track_filter: Vec where super::subscribe::has_range_filters(version), - 0x32 => new_group: Option where super::subscribe::has_new_group_request(version), + 0x32 => new_group: Option, super::max_age::PARAMETER => max_age: Option, ); if !data.is_empty() { @@ -503,7 +324,7 @@ impl Decode for Update { let mut data = r.sub(size)?; // The complete frame is present; a short field inside it is malformed. let update = Self::decode_body(&mut data, version).map_err(DecodeError::complete)?; - r.control = Some(super::message::control(super::SubscribeUpdate::ID, version)); + r.control = Some(super::message::control(super::SubscribeUpdate::ID)); r.uncounted = start; Ok(update) } @@ -557,7 +378,7 @@ impl Decode for NamespaceUpdate { }) }; let update = decode().map_err(DecodeError::complete)?; - r.control = Some(super::message::control(super::SubscribeUpdate::ID, version)); + r.control = Some(super::message::control(super::SubscribeUpdate::ID)); r.uncounted = start; Ok(update) } @@ -597,13 +418,10 @@ mod tests { let stats = registry.tier(Tier::default()).session(""); let version = Version::Draft22; let session = ScriptedSession::new(vec![0x10, 0, 2, 0, 0, 0x07, 0, 0]); - let mut stream = with_stats( - Stream::open(&mut crate::driver::Transport::new(session.clone()), version) - .await - .unwrap(), - &stats, - version, - ); + let mut stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) + .await + .unwrap() + .with_stats(&stats); let mut messages = Messages::new(crate::driver::Transport::new(session.clone()), true); assert_eq!(messages.message(&mut stream.reader).await.unwrap().0, 0x07); let rx = |class| { @@ -652,65 +470,6 @@ mod tests { } } - /// On drafts 14-16 an update aimed at a request that applies none, such as a FETCH or - /// SUBSCRIBE_NAMESPACE, keeps the request open. Drafts 15 and 16 refuse it with a - /// REQUEST_ERROR naming the update, and a later cancellation still ends the request. - #[moq_clock::test] - async fn legacy_update_is_answered_without_ending_the_request() { - use crate::lite::test_transport::ScriptedSession; - - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { - // Own Request ID 10, target 0, no parameters. Draft 14 carries its fixed start, - // end group, priority and forward first. - let body: &[u8] = match version { - Version::Draft14 => &[10, 0, 0, 0, 0, 128, 1, 0], - _ => &[10, 0, 0], - }; - let mut update = vec![super::super::SubscribeUpdate::ID as u8, 0, body.len() as u8]; - update.extend_from_slice(body); - let session = ScriptedSession::new(update); - let mut stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) - .await - .unwrap(); - let mut finished = false; - let mut messages = Messages::new(crate::driver::Transport::new(session.clone()), false); - - let polled = futures::poll!(std::future::poll_fn(|cx| poll_cancel( - &mut stream, - &mut finished, - &mut messages, - version, - cx - ))); - assert!(polled.is_pending(), "{version}: the update ended the request"); - - let written = session.log.writes.lock().unwrap().clone(); - match version { - Version::Draft14 => assert!(written.is_empty(), "{version}: {written:?}"), - // [type, length (2), Request ID, NOT_SUPPORTED, ...] - _ => assert_eq!( - (written[0], written[3], written[4]), - (super::super::RequestError::ID as u8, 10, 0x3), - "{version}: {written:?}" - ), - } - - // UNSUBSCRIBE, or any message but an update, ends the request. - session.push(&[super::super::Unsubscribe::ID as u8, 0, 1, 0]); - let polled = futures::poll!(std::future::poll_fn(|cx| poll_cancel( - &mut stream, - &mut finished, - &mut messages, - version, - cx - ))); - assert!( - matches!(polled, Poll::Ready(Err(Error::Cancel))), - "{version}: {polled:?}" - ); - } - } - #[test] fn delivery_timeouts_on_subscription_updates_are_accepted() { for version in [Version::Draft18, Version::Draft21, Version::Draft22] { @@ -764,109 +523,4 @@ mod tests { assert_eq!(update.priority, Some(5)); assert!(update.refused.is_none()); } - - /// Draft-14's per-request error messages share REQUEST_ERROR's body, so the shared writer - /// must put the same bytes on the wire as each legacy message does. - #[moq_clock::test] - async fn draft14_errors_match_their_legacy_messages() { - use super::super::error::request::{Kind, to_code}; - use super::super::{ - FetchError, PublishError, PublishNamespaceError, RequestId, SubscribeError, SubscribeNamespaceError, - }; - use crate::lite::test_transport::{Log, SinkSend}; - - let version = Version::Draft14; - let (request_id, err, reason) = (RequestId(6), Error::NotFound, "gone"); - - for kind in [ - Kind::Subscribe, - Kind::Fetch, - Kind::Publish, - Kind::PublishNamespace, - Kind::SubscribeNamespace, - Kind::TrackStatus, - ] { - let shared = Log::default(); - let mut writer = crate::coding::Writer::new( - crate::transport::boxed::SendStream::new(SinkSend::new(shared.clone())), - version, - ); - write_error(&mut writer, kind, request_id, &err, reason, version) - .await - .unwrap(); - - let legacy = Log::default(); - let mut writer = crate::coding::Writer::new( - crate::transport::boxed::SendStream::new(SinkSend::new(legacy.clone())), - version, - ); - let error_code = to_code(&err, kind, version); - let reason_phrase = reason.into(); - match kind { - Kind::Subscribe => { - writer - .encode_message(&SubscribeError { - request_id, - error_code, - reason_phrase, - }) - .await - } - Kind::Fetch => { - writer - .encode_message(&FetchError { - request_id, - error_code, - reason_phrase, - }) - .await - } - Kind::Publish => { - writer - .encode_message(&PublishError { - request_id, - error_code, - reason_phrase, - }) - .await - } - Kind::PublishNamespace => { - writer - .encode_message(&PublishNamespaceError { - request_id, - error_code, - reason_phrase, - }) - .await - } - Kind::SubscribeNamespace => { - writer - .encode_message(&SubscribeNamespaceError { - request_id, - error_code, - reason_phrase, - }) - .await - } - // TRACK_STATUS_ERROR has the SUBSCRIBE_ERROR body under its own type. - Kind::TrackStatus => { - writer.buffer_varint(super::super::TRACK_STATUS_ERROR_14).unwrap(); - writer - .encode(&SubscribeError { - request_id, - error_code, - reason_phrase, - }) - .await - } - } - .unwrap(); - - assert_eq!( - *shared.writes.lock().unwrap(), - *legacy.writes.lock().unwrap(), - "{kind:?}" - ); - } - } } diff --git a/rs/moq-net/src/ietf/request_update.rs b/rs/moq-net/src/ietf/request_update.rs index cfba92c239e..e22740abd78 100644 --- a/rs/moq-net/src/ietf/request_update.rs +++ b/rs/moq-net/src/ietf/request_update.rs @@ -18,7 +18,7 @@ use std::{collections::VecDeque, task::Poll}; -use super::{RequestId, Version}; +use super::Version; use crate::{Error, SessionError, auth}; /// MAX_REQUEST_UPDATES Setup Option (Option Type 0x08). Even, so the value is a bare varint. @@ -42,7 +42,7 @@ pub const UNNEGOTIATED_GUARD: usize = 64; pub fn supported(version: Version) -> bool { !matches!( version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 | Version::Draft18 + Version::Draft18 ) } @@ -53,7 +53,7 @@ pub fn supported(version: Version) -> bool { /// behind a pending verdict waits here until the verdict ahead of it resolves. The queue is /// bounded by MAX_REQUEST_UPDATES, counting the one being verified. pub(super) struct Renewal { - pending: Option<(auth::RequestVerdict, RequestId)>, + pending: Option, queued: VecDeque, } @@ -80,10 +80,10 @@ impl Renewal { self.pending.is_some() } - /// Wait on `verdict`, the renewal the update with `request_id` carried. - pub fn verify(&mut self, verdict: auth::RequestVerdict, request_id: RequestId) { + /// Wait on `verdict`, the renewal an update carried. + pub fn verify(&mut self, verdict: auth::RequestVerdict) { debug_assert!(self.pending.is_none(), "one renewal is verified at a time"); - self.pending = Some((verdict, request_id)); + self.pending = Some(verdict); } /// Queue `update` behind the pending verdict, counting it against the bound. @@ -111,16 +111,12 @@ impl Renewal { /// `Ready` with the pending verdict once it resolves. pub fn poll(&mut self, waiter: &kio::Waiter) -> Poll { - let Some((verdict, _)) = &self.pending else { + let Some(verdict) = &self.pending else { return Poll::Pending; }; let grant = std::task::ready!(verdict.poll_grant(waiter)); - let (verdict, request_id) = self.pending.take().expect("a pending renewal"); - Poll::Ready(Verdict { - verdict, - grant, - request_id, - }) + let verdict = self.pending.take().expect("a pending renewal"); + Poll::Ready(Verdict { verdict, grant }) } } @@ -130,8 +126,6 @@ pub(super) struct Verdict { pub verdict: auth::RequestVerdict, /// The acceptor's grant, or its refusal. pub grant: Result, - /// The update that carried the renewal, which the answer names before draft 17. - pub request_id: RequestId, } /// Close `session` for a peer past the MAX_REQUEST_UPDATES we advertised, returning the error @@ -170,10 +164,6 @@ mod tests { } for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, ] { let mut params = super::super::Parameters::default(); diff --git a/rs/moq-net/src/ietf/session.rs b/rs/moq-net/src/ietf/session.rs index e1610b64efc..52ef8e767e3 100644 --- a/rs/moq-net/src/ietf/session.rs +++ b/rs/moq-net/src/ietf/session.rs @@ -10,7 +10,7 @@ use crate::{ }; use super::{ - Control, Message, Publisher, Subscriber, Version, active_count, adapter::ControlStreamAdapter, auth, cluster, + Control, Message, Publisher, Subscriber, Version, active_count, auth, cluster, epoch, hidden, max_age, peer, request_update, solicit, subscriber::is_protocol_violation, }; @@ -41,14 +41,7 @@ pub struct Config { pub session: Transport, - /// The bidi SETUP stream (draft-14 through draft-16 only). Draft-17+ passes `None` - /// and exchanges SETUP on uni streams instead. - pub setup: Option>, - - pub request_id_max: Option, - - /// What the peer may make this session hold. On drafts 14 to 16 it also sizes the - /// MAX_REQUEST_ID window advertised in our SETUP (see [`ietf::initial_max_request_id`]). + /// What the peer may make this session hold. pub limits: crate::session::Limits, /// Whether we dialed, which sets the request-id parity. @@ -102,12 +95,11 @@ pub struct Config { /// classified by the session before it accepts any more. pub early_unis: Vec>, - /// The extensions our SETUP offers. Drafts 14-16 already sent it, so there this only - /// records what it said. MoQ Auth is offered only while [`Self::auth`] supports it too. + /// The extensions our SETUP offers. MoQ Auth is offered only while [`Self::auth`] + /// supports it too. pub extensions: crate::setup::Extensions, - /// The Implementation our SETUP advertises (draft-17+), or `None` to omit it. Drafts - /// 14-16 already sent theirs on the bidi SETUP. + /// The Implementation our SETUP advertises, or `None` to omit it. pub implementation: Option, /// Where the implementation the peer's SETUP advertises is published, once the uni loop @@ -141,8 +133,6 @@ pub fn start(config: Config) -> Result<(Driver, crate::goaway::Handle), Error> { runtime, stats, mut session, - setup, - request_id_max, limits, client, publish, @@ -175,7 +165,7 @@ pub fn start(config: Config) -> Result<(Driver, crate::goaway::Handle), Error> { // sides learn their grant without waiting on the app. Draft-17+ only; the peer's // SETUP then decides whether it is ever sent. // A handle that already knows the peer declined (a gated server accept) skips it. - let setup_token = match auth::supported(version) && auth.supported() { + let setup_token = match auth.supported() { true => auth.present(bytes::Bytes::new(), true).ok(), false => None, }; @@ -199,13 +189,8 @@ pub fn start(config: Config) -> Result<(Driver, crate::goaway::Handle), Error> { // loop otherwise. let peer_setup = peer::PeerSetup::new(extensions).with_implementation(peer_implementation); let setup_read = peer_declared.is_some(); - match peer_declared { - Some(declared) => peer_setup.set(declared), - // A legacy caller that passed nothing (our own tests, and the lite paths): - // settle the slot rather than leave the announce loops waiting on a value - // that is never coming. - None if !cluster::supported(version) => peer_setup.set(peer::Peer::default()), - None => {} + if let Some(declared) = peer_declared { + peer_setup.set(declared); } let driver = async move { @@ -238,373 +223,202 @@ pub fn start(config: Config) -> Result<(Driver, crate::goaway::Handle), Error> { let announces = crate::session::Slots::new(limits.announces).with_stats(subscribe.stats(), crate::stats::Cap::Announces); - let res = match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => { - let Some(setup) = setup else { - let err = Error::ProtocolViolation; - session.close( - super::error::to_session_code(&SessionError::from(&err)), - "setup stream required", - ); - return Err(err); - }; - let control = Control::new(request_id_max, client).with_window(limits.window(), client, version); - let adapter = ControlStreamAdapter::new(session.clone(), control.clone(), version).with_stats(&stats); - // Boxed like a draft-17+ transport, so the publisher and subscriber below - // are the same instantiation as theirs. - let virtual_session = crate::driver::Transport::new(adapter.clone()); - - // No AUTH on these drafts, but a limit still reaches both halves. - let mut publisher = Publisher::new( - runtime.clone(), - virtual_session.clone(), - publish, - control.clone(), - peer_hop, - peer_setup.clone(), - version, - ) - .with_auth(auth.clone()); - let (tasks, mut task_set) = TaskSet::new(); - publisher.withdrawal = withdrawing.clone(); - publisher.owed = serving.clone(); - publisher.subscriptions = subscriptions; - - let mut subscriber = Subscriber::new( - runtime.clone(), - virtual_session.clone(), - subscribe, - control, - peer_hop, - peer_setup.clone(), - self_origin, - cost, - version, - tasks.clone(), - goaway.going_away.clone(), - ) - .with_auth(auth.clone()) - .with_solicit(extensions.solicit); - subscriber.announces = announces; - - // GOAWAY send task: draft-14-16 carry GOAWAY on the shared control - // stream. Parked on the drain trigger; races the transport close so - // a parked trigger never blocks the task set draining. - { - let mut session = session.clone(); - let adapter = adapter.clone(); - let goaway = goaway.clone(); - tasks.push(async move { - let payload = kio::wait(|waiter| { - let mut cx = waiter.context(); - if session.poll_closed(&mut cx).is_ready() { - return std::task::Poll::Ready(None); - } - goaway.poll_triggered(waiter) - }) - .await; - let Some(payload) = payload else { - return; - }; - let timeout_ms = payload.timeout.map(|d| d.as_millis() as u64).unwrap_or(0); - adapter.send_goaway(&payload.uri, timeout_ms, version); - }); - } - drop(tasks); - - let dispatch_session = virtual_session.clone(); - - // Every half only ends the session on error (err_only parks on clean - // completion); the task set draining is the one clean exit. - let mut adapter_run = std::pin::pin!(err_only(adapter.run(setup.reader, setup.writer, goaway.clone()))); - let mut unis = Unis::new( - virtual_session.clone(), - subscriber.clone(), - UniSetup { - peer: None, - read: false, - early: Vec::new(), - client, - version, - goaway, - }, - ); - let mut dispatch = std::pin::pin!(err_only(run_dispatch( - dispatch_session, - publisher.clone(), - subscriber.clone(), - peer_setup.clone(), - None, - version - ))); - let mut datagrams = - std::pin::pin!(err_only(run_datagrams(virtual_session.clone(), subscriber.clone()))); - // Unsolicited PUBLISH_NAMESPACE unless the peer requires solicitation; - // see `Publisher::run_publish_namespaces`. - let mut pub_ns_run = std::pin::pin!(err_only(publisher.clone().run_publish_namespaces())); - let sub_ns_run = run_namespaces(subscriber.clone(), discovery, { - let adapter = adapter.clone(); - move || { - let mut adapter = adapter.clone(); - async move { - match version { - Version::Draft16 => { - let (send, recv) = adapter.open_native_bi().await?; - let mut reader = crate::coding::Reader::new( - crate::transport::boxed::RecvStream::new(recv), - version, - ); - reader.set_priority(crate::priority::RECV_CONTROL); - Ok(Stream { - writer: crate::coding::Writer::new( - crate::transport::boxed::SendStream::new(send), - version, - ), - reader, - }) - } - _ => Stream::open_request(&mut crate::driver::Transport::new(adapter), version).await, - } - } - } - }); - let mut sub_ns_run = std::pin::pin!(err_only(sub_ns_run)); + let res = { + let control = Control::new(client); + // Send SETUP and retain its writer for the session's lifetime. Only a failure + // ends the session; the other branches report how it closed. + let setup = err_only(run_setup( + session.clone(), + version, + path, + authority, + implementation, + self_origin, + cost, + crate::setup::Extensions { + auth: extensions.auth && auth.supported(), + ..extensions + }, + goaway.clone(), + control.clone(), + stats.clone(), + )); - let res = kio::wait(|waiter| { - use std::task::Poll; - if let Poll::Ready(err) = waiter.poll_future(adapter_run.as_mut()) { - return Poll::Ready(Err::<(), Error>(err)); - } - if let Poll::Ready(res) = unis.poll(waiter) { - return Poll::Ready(res); - } - if let Poll::Ready(err) = waiter.poll_future(dispatch.as_mut()) { - return Poll::Ready(Err(err)); - } - if let Poll::Ready(err) = waiter.poll_future(datagrams.as_mut()) { - return Poll::Ready(Err(err)); - } - if task_set.poll(waiter).is_ready() { - return Poll::Ready(Ok(())); - } - if let Poll::Ready(err) = waiter.poll_future(sub_ns_run.as_mut()) { - return Poll::Ready(Err(err)); - } - if let Poll::Ready(err) = waiter.poll_future(pub_ns_run.as_mut()) { - return Poll::Ready(Err(err)); + let mut publisher = Publisher::new( + runtime.clone(), + session.clone(), + publish, + control.clone(), + peer_hop, + peer_setup.clone(), + version, + ) + .with_auth(auth.clone()); + let (tasks, mut task_set) = TaskSet::new(); + publisher.withdrawal = withdrawing.clone(); + publisher.owed = serving.clone(); + publisher.subscriptions = subscriptions; + + // Only the dialing side fails loud on a publication outside its grant: a + // server's publish origin is everything the peer may read, not what it + // intends to push. + let enforce = { + let auth = auth.clone(); + let publisher = publisher.clone(); + let session = session.clone(); + async move { + match client { + true => enforce_grant(auth, publisher, session).await, + false => std::future::pending().await, } - Poll::Pending - }) - .await; - close(&mut session, &res); - if !closing.is_local() { - kio::wait(|waiter| unis.poll_drain(waiter)).await; } - if closing.is_local() { - subscriber.close(); - } else if let Err(err) = &res { - // Every track this session was receiving ends with its error. - subscriber.abort(err); - } - res - } - _ => { - let control = Control::new(None, client).with_history(limits.window()); - // Send SETUP and retain its writer for the session's lifetime. Only a failure - // ends the session; the other branches report how it closed. - let setup = err_only(run_setup( - session.clone(), - version, - path, - authority, - implementation, - self_origin, - cost, - crate::setup::Extensions { - auth: extensions.auth && auth.supported(), - ..extensions - }, - goaway.clone(), - control.clone(), - stats.clone(), - )); - - let mut publisher = Publisher::new( - runtime.clone(), - session.clone(), - publish, - control.clone(), - peer_hop, - peer_setup.clone(), - version, - ) - .with_auth(auth.clone()); - let (tasks, mut task_set) = TaskSet::new(); - publisher.withdrawal = withdrawing.clone(); - publisher.owed = serving.clone(); - publisher.subscriptions = subscriptions; - - // Only the dialing side fails loud on a publication outside its grant: a - // server's publish origin is everything the peer may read, not what it - // intends to push. - let enforce = { - let auth = auth.clone(); - let publisher = publisher.clone(); - let session = session.clone(); - async move { - match client { - true => enforce_grant(auth, publisher, session).await, - false => std::future::pending().await, - } - } - }; + }; - let mut subscriber = Subscriber::new( - runtime.clone(), - session.clone(), - subscribe, - control.clone(), - peer_hop, - peer_setup.clone(), - self_origin, - cost, - version, - tasks, - goaway.going_away.clone(), - ) - .with_auth(auth.clone()) - .with_solicit(extensions.solicit); - - // Our tokens, one Auth request each, once the peer's SETUP negotiates it. - let present = auth::run_present( - runtime.clone(), - session.clone(), - control.clone(), - auth.clone(), - peer_setup.clone(), - version, - stats.clone(), - ); - // Only a side that offered MoQ Auth answers the peer's Auth requests: one that - // arrives otherwise falls through to the protocol violation in `run_dispatch`. - let serve = auth.supported().then(|| auth::Serve { - runtime: runtime.clone(), - handle: auth.clone(), - peer_grant, - }); - subscriber.announces = announces; - - // When the peer's SETUP was pre-read (a gated server accept), monitor - // GOAWAY on that stream here; otherwise `run_unis` does it when the SETUP - // arrives on the wire. - let goaway_recv = { - let goaway = goaway.clone(); - let stats = stats.clone(); - async move { - match peer_setup_stream { - Some(reader) => { - run_goaway(reader.with_stats(&stats).with_version(version), version, client, goaway) - .await - } - None => std::future::pending().await, + let mut subscriber = Subscriber::new( + runtime.clone(), + session.clone(), + subscribe, + control.clone(), + peer_hop, + peer_setup.clone(), + self_origin, + cost, + version, + tasks, + goaway.going_away.clone(), + ) + .with_auth(auth.clone()) + .with_solicit(extensions.solicit); + + // Our tokens, one Auth request each, once the peer's SETUP negotiates it. + let present = auth::run_present( + runtime.clone(), + session.clone(), + control.clone(), + auth.clone(), + peer_setup.clone(), + version, + stats.clone(), + ); + // Only a side that offered MoQ Auth answers the peer's Auth requests: one that + // arrives otherwise falls through to the protocol violation in `run_dispatch`. + let serve = auth.supported().then(|| auth::Serve { + runtime: runtime.clone(), + handle: auth.clone(), + peer_grant, + }); + subscriber.announces = announces; + + // When the peer's SETUP was pre-read (a gated server accept), monitor + // GOAWAY on that stream here; otherwise `run_unis` does it when the SETUP + // arrives on the wire. + let goaway_recv = { + let goaway = goaway.clone(); + let stats = stats.clone(); + async move { + match peer_setup_stream { + Some(reader) => { + run_goaway(reader.with_stats(&stats).with_version(version), version, client, goaway) + .await } + None => std::future::pending().await, } - }; + } + }; - // Every half only ends the session on error (err_only parks on clean - // completion); `setup` never resolves (it holds the stream open) and the - // task set draining is the one clean exit. - let mut unis = Unis::new( - session.clone(), - subscriber.clone(), - UniSetup { - peer: Some(peer_setup.clone()), - read: setup_read, - early: early_unis, - client, - version, - goaway, - }, - ); - let mut dispatch = std::pin::pin!(err_only(run_dispatch( - session.clone(), - publisher.clone(), - subscriber.clone(), - peer_setup.clone(), - serve, - version - ))); - let mut datagrams = std::pin::pin!(err_only(run_datagrams(session.clone(), subscriber.clone()))); - let mut goaway_recv = std::pin::pin!(err_only(goaway_recv)); - let mut present = std::pin::pin!(err_only(present)); - let mut enforce = std::pin::pin!(err_only(enforce)); - let mut setup = std::pin::pin!(setup); - // Unsolicited PUBLISH_NAMESPACE unless the peer requires solicitation; - // see `Publisher::run_publish_namespaces`. - let mut pub_ns_run = std::pin::pin!(err_only(publisher.clone().run_publish_namespaces())); - let sub_ns_run = run_namespaces(subscriber.clone(), discovery, { - let session = session.clone(); - move || { - let mut session = session.clone(); - async move { Stream::open_request(&mut session, version).await } - } - }); - let mut sub_ns_run = std::pin::pin!(err_only(sub_ns_run)); + // Every half only ends the session on error (err_only parks on clean + // completion); `setup` never resolves (it holds the stream open) and the + // task set draining is the one clean exit. + let mut unis = Unis::new( + session.clone(), + subscriber.clone(), + UniSetup { + peer: Some(peer_setup.clone()), + read: setup_read, + early: early_unis, + client, + version, + goaway, + }, + ); + let mut dispatch = std::pin::pin!(err_only(run_dispatch( + session.clone(), + publisher.clone(), + subscriber.clone(), + peer_setup.clone(), + serve, + version + ))); + let mut datagrams = std::pin::pin!(err_only(run_datagrams(session.clone(), subscriber.clone()))); + let mut goaway_recv = std::pin::pin!(err_only(goaway_recv)); + let mut present = std::pin::pin!(err_only(present)); + let mut enforce = std::pin::pin!(err_only(enforce)); + let mut setup = std::pin::pin!(setup); + // Unsolicited PUBLISH_NAMESPACE unless the peer requires solicitation; + // see `Publisher::run_publish_namespaces`. + let mut pub_ns_run = std::pin::pin!(err_only(publisher.clone().run_publish_namespaces())); + let sub_ns_run = run_namespaces(subscriber.clone(), discovery, { + let session = session.clone(); + move || { + let mut session = session.clone(); + async move { Stream::open_request(&mut session, version).await } + } + }); + let mut sub_ns_run = std::pin::pin!(err_only(sub_ns_run)); - let res = kio::wait(|waiter| { - use std::task::Poll; - if let Poll::Ready(res) = unis.poll(waiter) { - return Poll::Ready(res); - } - if let Poll::Ready(err) = waiter.poll_future(dispatch.as_mut()) { - return Poll::Ready(Err(err)); - } - if let Poll::Ready(err) = waiter.poll_future(datagrams.as_mut()) { - return Poll::Ready(Err(err)); - } - if let Poll::Ready(err) = waiter.poll_future(goaway_recv.as_mut()) { - return Poll::Ready(Err(err)); - } - // Malformed replies are session-fatal, unlike token refusals. - if let Poll::Ready(err) = waiter.poll_future(present.as_mut()) { - return Poll::Ready(Err(err)); - } - if let Poll::Ready(err) = waiter.poll_future(enforce.as_mut()) { - return Poll::Ready(Err(err)); - } - if let Poll::Ready(err) = waiter.poll_future(setup.as_mut()) { - return Poll::Ready(Err(err)); - } - if task_set.poll(waiter).is_ready() { - return Poll::Ready(Ok(())); - } - if let Poll::Ready(err) = waiter.poll_future(sub_ns_run.as_mut()) { - return Poll::Ready(Err(err)); - } - if let Poll::Ready(err) = waiter.poll_future(pub_ns_run.as_mut()) { - return Poll::Ready(Err(err)); - } - Poll::Pending - }) - .await; - close(&mut session, &res); - // Before this arm's Auth serve tasks drop, so each settles with the - // session's error rather than a bare cancel. - auth.close(match &res { - Ok(()) => Error::Cancel, - Err(err) => err.clone(), - }); - if !closing.is_local() { - kio::wait(|waiter| unis.poll_drain(waiter)).await; + let res = kio::wait(|waiter| { + use std::task::Poll; + if let Poll::Ready(res) = unis.poll(waiter) { + return Poll::Ready(res); } - if closing.is_local() { - subscriber.close(); - } else if let Err(err) = &res { - // Every track this session was receiving ends with its error. - subscriber.abort(err); + if let Poll::Ready(err) = waiter.poll_future(dispatch.as_mut()) { + return Poll::Ready(Err(err)); + } + if let Poll::Ready(err) = waiter.poll_future(datagrams.as_mut()) { + return Poll::Ready(Err(err)); + } + if let Poll::Ready(err) = waiter.poll_future(goaway_recv.as_mut()) { + return Poll::Ready(Err(err)); + } + // Malformed replies are session-fatal, unlike token refusals. + if let Poll::Ready(err) = waiter.poll_future(present.as_mut()) { + return Poll::Ready(Err(err)); } - res + if let Poll::Ready(err) = waiter.poll_future(enforce.as_mut()) { + return Poll::Ready(Err(err)); + } + if let Poll::Ready(err) = waiter.poll_future(setup.as_mut()) { + return Poll::Ready(Err(err)); + } + if task_set.poll(waiter).is_ready() { + return Poll::Ready(Ok(())); + } + if let Poll::Ready(err) = waiter.poll_future(sub_ns_run.as_mut()) { + return Poll::Ready(Err(err)); + } + if let Poll::Ready(err) = waiter.poll_future(pub_ns_run.as_mut()) { + return Poll::Ready(Err(err)); + } + Poll::Pending + }) + .await; + close(&mut session, &res); + // Before this arm's Auth serve tasks drop, so each settles with the + // session's error rather than a bare cancel. + auth.close(match &res { + Ok(()) => Error::Cancel, + Err(err) => err.clone(), + }); + if !closing.is_local() { + kio::wait(|waiter| unis.poll_drain(waiter)).await; } + if closing.is_local() { + subscriber.close(); + } else if let Err(err) = &res { + // Every track this session was receiving ends with its error. + subscriber.abort(err); + } + res }; auth.close(match &res { @@ -772,14 +586,14 @@ pub(crate) fn decode_peer_setup( /// a future option reaches both the pre-read accept path and the uni loop. fn peer_from_params(params: &ietf::Parameters, version: Version) -> Result { Ok(peer::Peer { - cluster: cluster::peer_from_setup(params, version)?, + cluster: cluster::peer_from_setup(params)?, solicit: solicit::from_setup(params, version)?, hidden: hidden::from_setup(params, version), max_age: max_age::from_setup(params, version), - auth: auth::from_setup(params, version) == Some(true), - active_count: active_count::from_setup(params, version), - epoch: epoch::from_setup(params, version), - flate: super::flate::from_setup(params, version)?, + auth: auth::from_setup(params) == Some(true), + active_count: active_count::from_setup(params), + epoch: epoch::from_setup(params), + flate: super::flate::from_setup(params)?, }) } @@ -826,21 +640,21 @@ async fn run_setup( if let Some(authority) = authority { parameters.set_bytes(ietf::ParameterBytes::Authority, authority.into_bytes()); } - cluster::peer_into_setup(&mut parameters, self_origin, cost, version); + cluster::peer_into_setup(&mut parameters, self_origin, cost); if extensions.solicit { solicit::into_setup(&mut parameters, version); } hidden::into_setup(&mut parameters, version); max_age::into_setup(&mut parameters, version); if extensions.auth { - auth::into_setup(&mut parameters, version); + auth::into_setup(&mut parameters); } - active_count::into_setup(&mut parameters, version); + active_count::into_setup(&mut parameters); if extensions.epoch { - epoch::into_setup(&mut parameters, version); + epoch::into_setup(&mut parameters); } request_update::into_setup(&mut parameters, version); - super::flate::into_setup(&mut parameters, version); + super::flate::into_setup(&mut parameters); let parameters = parameters.encode_bytes(version)?; writer @@ -927,7 +741,7 @@ impl UniType { // 0x40 (FIRST_OBJECT), also covering 0x50-0x5D and 0x70-0x7D. A reserved // SUBGROUP_ID_MODE (0b11) or a bit the draft lacks is invalid, which MUST close the // session too (draft-21 section 11.3.1). - if ietf::GroupFlags::decode(kind, version).is_ok() { + if ietf::GroupFlags::decode(kind).is_ok() { return Some(Self::Subgroup); } @@ -935,13 +749,9 @@ impl UniType { FetchHeader::TYPE => Some(Self::Fetch), setup::SETUP_V17 => match version { // SETUP rides the bidi control stream. - Version::Draft14 | Version::Draft15 | Version::Draft16 => None, _ => Some(Self::Setup), }, - PADDING => match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 => None, - _ => Some(Self::Padding), - }, + PADDING => Some(Self::Padding), _ => None, } } @@ -1307,7 +1117,7 @@ struct Bidi { impl Bidi { /// Read and serve a request, failing only when it breaks the session. async fn serve(self, mut stream: Stream) -> Result<(), Error> { - stream = super::request_stream::with_stats(stream, self.publisher.stats(), self.version); + stream = stream.with_stats(self.publisher.stats()); let Self { publisher, mut subscriber, @@ -1337,27 +1147,21 @@ impl Bidi { } Err(err) => return Err(err), }; - let native = !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - || (version == Version::Draft16 && id == ietf::SubscribeNamespaceLegacy::ID); - if native { - stream.reader = stream.reader.with_stats(publisher.stats()); - publisher - .stats() - .control(super::message::control(id, version), data.0.len() + 2, true); - } - if !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - && matches!( - id, - ietf::Subscribe::ID - | ietf::Fetch::ID - | ietf::SubscribeNamespace::ID - | ietf::SubscribeNamespaceLegacy::ID - | ietf::SubscribeTracks::ID - | ietf::TrackStatus::ID - | ietf::Publish::ID - | ietf::PublishNamespace::ID - | auth::Auth::ID - ) { + stream.reader = stream.reader.with_stats(publisher.stats()); + publisher + .stats() + .control(super::message::control(id), data.0.len() + 2, true); + if matches!( + id, + ietf::Subscribe::ID + | ietf::Fetch::ID + | ietf::SubscribeNamespace::ID + | ietf::SubscribeTracks::ID + | ietf::TrackStatus::ID + | ietf::Publish::ID + | ietf::PublishNamespace::ID + | auth::Auth::ID + ) { let request_id = RequestId::decode(&mut data.decoder(version), version)?; match publisher.control.accept(request_id) { Err(Error::GoingAway) => return refuse_going_away(stream, id, version).await, @@ -1367,22 +1171,10 @@ impl Bidi { } } match id { - // Draft-16 moved SUBSCRIBE_NAMESPACE to its own stream, past the control stream - // that admits every other request, but it still takes a request ID from the - // MAX_REQUEST_ID window. Held until the request ends, like the rest. - ietf::SubscribeNamespaceLegacy::ID if version == Version::Draft16 => { - let request_id = RequestId::decode(&mut crate::coding::Decoder::new(&data.0, version.into()), version)?; - let permit = publisher.control.accept(request_id)?; - let task = publisher.handle_stream(id, data, stream, epoch)?; - let _permit = permit; - task.await; - } - // Publisher handles: Subscribe, Fetch, SubscribeNamespace (0x50 modern / - // 0x11 legacy), SubscribeTracks, TrackStatus + // Publisher handles: Subscribe, Fetch, SubscribeNamespace, SubscribeTracks, TrackStatus ietf::Subscribe::ID | ietf::Fetch::ID | ietf::SubscribeNamespace::ID - | ietf::SubscribeNamespaceLegacy::ID | ietf::SubscribeTracks::ID | ietf::TrackStatus::ID => { publisher.handle_stream(id, data, stream, epoch)?.await; @@ -1417,7 +1209,6 @@ async fn refuse_going_away(stream: Stream, id: u64, version: super::request_stream::reject( stream, ietf::RequestError { - request_id: None, error_code: super::error::request::to_code( &Error::GoingAway, super::error::request::Kind::Subscribe, @@ -1550,7 +1341,7 @@ mod tests { /// is not UTF-8 refuses the SETUP. #[test] fn peer_setup_reads_the_implementation() { - for version in [Version::Draft17, Version::Draft22] { + for version in [Version::Draft22] { let encode = |value: Option<&[u8]>| { let mut params = ietf::Parameters::default(); if let Some(value) = value { @@ -1569,21 +1360,13 @@ mod tests { #[moq_clock::test] async fn announce_interest_without_credit_keeps_the_session_alive() { use std::sync::atomic::Ordering; - for version in [Version::Draft16, Version::Draft17, Version::Draft19] { + for version in [Version::Draft19] { let origin = origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let interest = origin.consume().announced(); let session = crate::lite::test_transport::ScriptedSession::new(Vec::new()).without_credit(); let log = session.log.clone(); let mut session = crate::driver::Transport::new(session); - let setup = if version == Version::Draft16 { - Some( - Stream::open(&mut session, version) - .await - .expect("setup waits for credit"), - ) - } else { - None - }; + let setup = None; let (driver, _goaway) = start(Config { runtime: crate::time::sim(), stats: Default::default(), @@ -1636,7 +1419,6 @@ mod tests { #[moq_clock::test] async fn auth_ids_share_the_modern_request_history() { for version in [ - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -1730,7 +1512,7 @@ mod tests { #[moq_clock::test] async fn malformed_auth_replies_close_the_session() { - for version in [Version::Draft17, Version::Draft22] { + for version in [Version::Draft22] { for (id, body) in [ (99, vec![]), (auth::AuthError::ID, vec![0xc0, 0, 0, 1, 0, 0, 0, 0, 0]), @@ -1784,7 +1566,7 @@ mod tests { #[moq_clock::test] async fn malformed_presenter_auth_closes_the_session() { - for version in [Version::Draft17, Version::Draft22] { + for version in [Version::Draft22] { for body in [vec![0, 2], vec![0, 0, 0]] { let mut script = Vec::new(); let mut encoder = crate::coding::Encoder::new(&mut script, version.into()); @@ -1863,7 +1645,6 @@ mod tests { writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active: None, }) .await @@ -2074,14 +1855,7 @@ mod tests { ) { let log = session.log.clone(); let mut session = crate::driver::Transport::new(session); - let setup = match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some( - Stream::open(&mut session, version) - .await - .expect("open the control stream"), - ), - _ => None, - }; + let setup = None; let (driver, _goaway) = start(Config { runtime: crate::time::sim(), @@ -2142,7 +1916,7 @@ mod tests { /// A client asks nothing until something is watched, then only about what is. #[moq_clock::test] async fn a_client_asks_only_about_what_is_watched() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16, Version::Draft17] { + for version in [Version::Draft15] { let subscribe = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let (log, _driver) = client(version, subscribe.clone(), Some(true)).await; let first = subscribe_namespace_legacy(version, "cam", RequestId(0)).await; @@ -2166,7 +1940,7 @@ mod tests { /// names the prefix on draft-14 and the request on draft-15. #[moq_clock::test] async fn an_unwatched_prefix_is_unsubscribed_after_the_linger() { - for version in [Version::Draft14, Version::Draft15] { + for version in [Version::Draft15] { let subscribe = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let cam = watch(&subscribe, "cam"); let (log, _driver) = client(version, subscribe.clone(), None).await; @@ -2190,7 +1964,7 @@ mod tests { /// goes out on the ordered control stream ahead of the broader SUBSCRIBE_NAMESPACE. #[moq_clock::test] async fn a_broader_watch_replaces_the_narrower_at_a_foreign_peer() { - for version in [Version::Draft14, Version::Draft15] { + for version in [Version::Draft15] { let subscribe = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let _front = watch(&subscribe, "cam/front"); let (log, _driver) = client(version, subscribe.clone(), None).await; @@ -2216,8 +1990,6 @@ mod tests { #[moq_clock::test] async fn a_broader_watch_waits_for_the_narrower_to_leave_the_wire() { for version in [ - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft22, @@ -2246,7 +2018,7 @@ mod tests { /// broader request that replaced it goes out alone once the credit arrives. #[moq_clock::test] async fn a_request_ended_before_it_opened_does_not_block_the_broader_one() { - for version in [Version::Draft16, Version::Draft17, Version::Draft22] { + for version in [Version::Draft22] { let subscribe = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let gate = kio::Producer::new(true); let credit = kio::Producer::new(true); @@ -2277,7 +2049,7 @@ mod tests { /// routes over before it has synced. #[moq_clock::test] async fn a_broader_watch_keeps_the_narrower_open_at_our_peer() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16, Version::Draft17] { + for version in [Version::Draft15] { let subscribe = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let _front = watch(&subscribe, "cam/front"); let (log, _driver) = client(version, subscribe.clone(), Some(true)).await; @@ -2288,9 +2060,6 @@ mod tests { let broader = subscribe_namespace_legacy(version, "cam", RequestId(2)).await; assert!(sent(&log, &broader).await, "{version:?} never asked the broader prefix"); let ended = match version { - Version::Draft14 | Version::Draft15 => { - occurrences(&log, &unsubscribe_namespace(version, "cam/front", RequestId(0)).await) > 0 - } _ => log .trail() .iter() @@ -2307,7 +2076,7 @@ mod tests { /// gets the empty prefix. Draft-16 made the empty prefix legal, so it stays. #[moq_clock::test] async fn an_empty_namespace_is_not_asked_of_a_foreign_peer_before_draft_16() { - for version in [Version::Draft14, Version::Draft15] { + for version in [Version::Draft15] { assert_eq!( asked_namespace(version, "", None).await, 0, @@ -2578,7 +2347,7 @@ mod tests { let (setup, _) = setup::Setup::decode_slice(&writes, crate::Version::Ietf(VERSION)).expect("our SETUP"); let (params, _) = ietf::Parameters::decode_slice(&setup.parameters, VERSION).expect("its options"); ( - auth::from_setup(¶ms, VERSION) == Some(true), + auth::from_setup(¶ms) == Some(true), solicit::from_setup(¶ms, VERSION).unwrap() == Some(true), ) } @@ -3186,7 +2955,6 @@ mod tests { #[moq_clock::test] async fn an_unknown_bidi_type_closes_the_session() { for version in [ - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -3350,7 +3118,6 @@ mod tests { let log = crate::lite::test_transport::Log::default(); let mut writer = crate::coding::Writer::new( crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), - Version::Draft14, ); writer.varint(ietf::PublishNamespaceOk::ID).await.unwrap(); @@ -3447,11 +3214,6 @@ mod tests { let mut body = Vec::new(); let mut w = crate::coding::Encoder::new(&mut body, version.into()); match version { - Version::Draft14 => { - msg.encode_msg(&mut w, version).unwrap(); - // Replace the empty parameter block with the token. - assert_eq!(body.pop(), Some(0)); - } // The request ID, namespace and name; every parameter but the token defaults. _ => w.slice(&[0, 1, 4, b'r', b'o', b'o', b'm', 5, b'v', b'i', b'd', b'e', b'o']), } @@ -3473,7 +3235,7 @@ mod tests { /// request on the control stream, a strict draft on its own stream. #[moq_clock::test] async fn a_request_token_that_cannot_be_used_closes_the_session() { - for version in [Version::Draft14, Version::Draft22] { + for version in [Version::Draft22] { for (token, expected, why) in [ ( &[0x01, 0x07, 0x00][..], @@ -3495,21 +3257,13 @@ mod tests { ] { let payload = subscribe_with_token(version, token); let session = match version { - Version::Draft14 => crate::lite::test_transport::ScriptedSession::new(payload), _ => { crate::lite::test_transport::ScriptedSession::new(Vec::new()).with_incoming_bidis(vec![payload]) } }; let log = session.log.clone(); let session = crate::driver::Transport::new(session.clone()); - let setup = match version { - Version::Draft14 => Some( - Stream::open(&mut crate::driver::Transport::new(session.clone()), version) - .await - .unwrap(), - ), - _ => None, - }; + let setup = None; let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let (driver, _goaway) = start(Config { runtime: crate::time::sim(), @@ -3686,7 +3440,7 @@ mod tests { #[moq_clock::test] async fn a_control_stream_fin_or_reset_is_a_protocol_violation() { use crate::lite::test_transport::{Close, DeadStreamSession, ScriptedSession}; - for version in [Version::Draft17, Version::Draft18, Version::Draft21, Version::Draft22] { + for version in [Version::Draft18, Version::Draft21, Version::Draft22] { let session = ScriptedSession::new(Vec::new()).with_incoming_unis(vec![setup_stream( version, ietf::Parameters::default(), diff --git a/rs/moq-net/src/ietf/solicit.rs b/rs/moq-net/src/ietf/solicit.rs index b1c07d55817..d5df9c12976 100644 --- a/rs/moq-net/src/ietf/solicit.rs +++ b/rs/moq-net/src/ietf/solicit.rs @@ -57,7 +57,7 @@ mod tests { /// Options: an old peer can opt out too. #[test] fn every_version_round_trips() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16, Version::Draft20] { + for version in [Version::Draft20] { let mut params = super::super::Parameters::default(); into_setup(&mut params, version); assert_eq!(from_setup(¶ms, version).unwrap(), Some(true)); diff --git a/rs/moq-net/src/ietf/subscribe.rs b/rs/moq-net/src/ietf/subscribe.rs index 1a6e1d60725..2f98c38b393 100644 --- a/rs/moq-net/src/ietf/subscribe.rs +++ b/rs/moq-net/src/ietf/subscribe.rs @@ -1,11 +1,11 @@ -//! IETF moq-transport subscribe messages (v14 + v15) +//! IETF moq-transport subscribe messages use std::borrow::Cow; use crate::{ Path, coding::*, - ietf::{Fill, Filter, GroupOrder, Location, Opaque, Parameters, Properties, RequestId}, + ietf::{Fill, Filter, GroupOrder, Location, Opaque, Properties, RequestId}, }; use super::token::RequestToken; @@ -42,26 +42,21 @@ pub struct Subscribe<'a> { /// The `AUTHORIZATION TOKEN` (0x03) this request presents. At most one is /// accepted: decoding refuses a repeated or second token. pub authorization_token: Option, - /// NEW_GROUP_REQUEST (0x32, draft-15+): the subscriber's largest known Group ID plus - /// one, or 0 when it knows none. Never encoded where the draft has no such parameter. + /// NEW_GROUP_REQUEST (0x32): the subscriber's largest known Group ID plus one, or 0 + /// when it knows none. pub new_group: Option, /// Subscriber max age in milliseconds (MoQ Timestamp), if negotiated. pub max_age: Option, - /// The publisher instance the subscriber expects (MoQ Epoch, draft-17+). Only on a + /// The publisher instance the subscriber expects (MoQ Epoch). Only on a /// session that negotiated it; the session refuses one that arrives otherwise. pub epoch: Option, } -/// NEW_GROUP_REQUEST (0x32) and DYNAMIC_GROUPS (0x30) arrived in draft-15. -pub(super) fn has_new_group_request(version: Version) -> bool { - version != Version::Draft14 -} - /// The Range Filters (0x25-0x29) arrived in draft-19. pub(super) fn has_range_filters(version: Version) -> bool { !matches!( version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 | Version::Draft18 + Version::Draft18 ) } @@ -70,158 +65,99 @@ impl Message for Subscribe<'_> { fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { let request_id = RequestId::decode(r, version)?; - if version == Version::Draft17 { - let _required_request_id_delta = r.varint()?; - } let (track_namespace, track_name) = decode_track(r)?; let track_name = Cow::Owned(track_name); - match version { - Version::Draft14 => { - let subscriber_priority = r.u8()?; - let group_order = GroupOrder::decode(r, version)?; - - let forward = r.bool()?; - let filter = Filter::decode(r, version)?; - - decode_params!(r, version, - 0x03 => tokens: Vec, - super::max_age::PARAMETER => max_age: Option, - ); - let authorization_token = super::token::single(tokens)?; - - Ok(Self { - request_id, - track_namespace, - track_name, - subscriber_priority, - group_order, - filter, - fill: None, - properties_wanted: true, - forward, - rendezvous: false, - range_filters: false, - authorization_token, - new_group: None, - max_age: super::max_age::validate(max_age)?, - epoch: None, - }) - } - _ => { - decode_params!(r, version, - 0x02 => _object_delivery_timeout: Option, - 0x03 => authorization_token: Vec, - // 0x04 is MAX_CACHE_DURATION in draft-15 (a publisher parameter) and not a - // message parameter at all in draft-16. RENDEZVOUS_TIMEOUT arrives in draft-17. - 0x04 => rendezvous_timeout: Option where !matches!(version, Version::Draft15 | Version::Draft16), - 0x06 => _subgroup_delivery_timeout: Option where !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17), - 0x10 => forward: Option, - 0x20 => subscriber_priority: Option, - 0x21 => filter: Option, - 0x22 => group_order: Option, - 0x23 => fill: Option where Filter::is_draft20(version), - 0x25 => subgroup_filter: Vec where has_range_filters(version), - 0x26 => object_id_filter: Vec where has_range_filters(version), - 0x27 => priority_filter: Vec where has_range_filters(version), - 0x28 => object_property_filter: Vec where has_range_filters(version), - 0x32 => new_group: Option where has_new_group_request(version), - 0x35 => include_properties: Option where Filter::is_draft20(version), - epoch::EPOCH_PARAM => epoch: Option where epoch::supported(version), - super::max_age::PARAMETER => max_age: Option, - ); - let authorization_token = super::token::single(authorization_token)?; - - let range_filters = [ - subgroup_filter, - object_id_filter, - priority_filter, - object_property_filter, - ] - .iter() - .any(|filter| !filter.is_empty()); - - // Defaults to 1, so an absent parameter means the subscriber wants them. - let properties_wanted = include_properties.unwrap_or(true); - - // A zero cap still distinguishes a requested wait from an immediate miss. - let rendezvous = rendezvous_timeout.is_some_and(|millis| millis > 0); - - let subscriber_priority = subscriber_priority.unwrap_or(super::priority::DEFAULT_WIRE); - let group_order = group_order.unwrap_or(GroupOrder::Descending); - // An absent LOCATION_FILTER means the subscription is unfiltered. - let filter = filter.unwrap_or(Filter::Unfiltered); - - Ok(Self { - request_id, - track_namespace, - track_name, - subscriber_priority, - group_order, - filter, - fill, - properties_wanted, - // Absent means 1. - forward: forward.unwrap_or(true), - rendezvous, - range_filters, - authorization_token, - new_group, - max_age: super::max_age::validate(max_age)?, - epoch, - }) - } - } + decode_params!(r, version, + 0x02 => _object_delivery_timeout: Option, + 0x03 => authorization_token: Vec, + 0x04 => rendezvous_timeout: Option, + 0x06 => _subgroup_delivery_timeout: Option, + 0x10 => forward: Option, + 0x20 => subscriber_priority: Option, + 0x21 => filter: Option, + 0x22 => group_order: Option, + 0x23 => fill: Option where Filter::is_draft20(version), + 0x25 => subgroup_filter: Vec where has_range_filters(version), + 0x26 => object_id_filter: Vec where has_range_filters(version), + 0x27 => priority_filter: Vec where has_range_filters(version), + 0x28 => object_property_filter: Vec where has_range_filters(version), + 0x32 => new_group: Option, + 0x35 => include_properties: Option where Filter::is_draft20(version), + epoch::EPOCH_PARAM => epoch: Option, + super::max_age::PARAMETER => max_age: Option, + ); + let authorization_token = super::token::single(authorization_token)?; + + let range_filters = [ + subgroup_filter, + object_id_filter, + priority_filter, + object_property_filter, + ] + .iter() + .any(|filter| !filter.is_empty()); + + // Defaults to 1, so an absent parameter means the subscriber wants them. + let properties_wanted = include_properties.unwrap_or(true); + + // A zero cap still distinguishes a requested wait from an immediate miss. + let rendezvous = rendezvous_timeout.is_some_and(|millis| millis > 0); + + let subscriber_priority = subscriber_priority.unwrap_or(super::priority::DEFAULT_WIRE); + let group_order = group_order.unwrap_or(GroupOrder::Descending); + // An absent LOCATION_FILTER means the subscription is unfiltered. + let filter = filter.unwrap_or(Filter::Unfiltered); + + Ok(Self { + request_id, + track_namespace, + track_name, + subscriber_priority, + group_order, + filter, + fill, + properties_wanted, + // Absent means 1. + forward: forward.unwrap_or(true), + rendezvous, + range_filters, + authorization_token, + new_group, + max_age: super::max_age::validate(max_age)?, + epoch, + }) } fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { self.request_id.encode(w, version)?; - if version == Version::Draft17 { - w.varint(0)?; // required_request_id_delta = 0 (draft-17 only, removed in draft-18 per #1615) - } encode_namespace(w, &self.track_namespace)?; w.string(&self.track_name)?; - match version { - Version::Draft14 => { - w.u8(self.subscriber_priority); - self.group_order.encode(w, version)?; - w.bool(self.forward); + // FILL_PARAMETERS arrived in draft-20. Sending it to an older peer would be an + // unknown parameter, which is a protocol violation, so it is dropped instead. + // A subscriber there simply joins mid-group, which is what it did all along. + let fill = self.fill.filter(|_| Filter::is_draft20(version)); - self.filter.encode(w, version)?; - encode_params!(w, version, - 0x03 => self.authorization_token.clone(), - super::max_age::PARAMETER => self.max_age, - ); - } - _ => { - // FILL_PARAMETERS arrived in draft-20. Sending it to an older peer would be an - // unknown parameter, which is a protocol violation, so it is dropped instead. - // A subscriber there simply joins mid-group, which is what it did all along. - let fill = self.fill.filter(|_| Filter::is_draft20(version)); - - // INCLUDE_PROPERTIES defaults to 1, so only the opt-out is worth bytes. It - // arrived in draft-20, and an older peer would read it as an unknown - // parameter, which is a protocol violation. - let include_properties = (!self.properties_wanted && Filter::is_draft20(version)).then_some(false); - let new_group = self.new_group.filter(|_| has_new_group_request(version)); - // Only a session that negotiated MoQ Epoch names one, and it exists from draft-17. - let epoch = self.epoch.clone().filter(|_| epoch::supported(version)); - - encode_params!(w, version, - 0x03 => self.authorization_token.clone(), - 0x10 => self.forward, - 0x20 => self.subscriber_priority, - 0x21 => self.filter, - 0x22 => self.group_order, - 0x23 => fill, - 0x32 => new_group, - 0x35 => include_properties, - epoch::EPOCH_PARAM => epoch, - super::max_age::PARAMETER => self.max_age, - ); - } - } + // INCLUDE_PROPERTIES defaults to 1, so only the opt-out is worth bytes. It + // arrived in draft-20, and an older peer would read it as an unknown + // parameter, which is a protocol violation. + let include_properties = (!self.properties_wanted && Filter::is_draft20(version)).then_some(false); + // Only a session that negotiated MoQ Epoch names one. + let epoch = self.epoch.clone(); + + encode_params!(w, version, + 0x03 => self.authorization_token.clone(), + 0x10 => self.forward, + 0x20 => self.subscriber_priority, + 0x21 => self.filter, + 0x22 => self.group_order, + 0x23 => fill, + 0x32 => self.new_group, + 0x35 => include_properties, + epoch::EPOCH_PARAM => epoch, + super::max_age::PARAMETER => self.max_age, + ); Ok(()) } @@ -230,14 +166,13 @@ impl Message for Subscribe<'_> { /// SubscribeOk message (0x04) #[derive(Clone, Debug)] pub struct SubscribeOk { - pub request_id: Option, pub track_alias: u64, /// The largest Location in the track (LARGEST_OBJECT, 0x09), which the spec requires /// once the track has content. It is what a subscriber sizes a fill against. pub largest: Option, - /// Metadata about the track, sent as Track Properties (draft-17+). + /// Metadata about the track, sent as Track Properties. pub properties: Properties, } @@ -245,113 +180,34 @@ impl Message for SubscribeOk { const ID: u64 = 0x04; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - self.request_id - .expect("request_id required for draft14-16") - .encode(w, version)?; - } else { - assert!(self.request_id.is_none(), "request_id must be None for draft17+"); - } w.varint(self.track_alias)?; - match version { - Version::Draft14 => { - w.varint(0)?; // expires = 0 - self.properties - .group_order - .unwrap_or(GroupOrder::Ascending) - .encode(w, version)?; - w.bool(self.largest.is_some()); - if let Some(largest) = self.largest { - largest.encode(w, version)?; - } - w.u8(0); // no parameters - } - _ => { - // GROUP_ORDER is a legal SUBSCRIBE_OK parameter only through draft-15; a later - // peer closes the session with PROTOCOL_VIOLATION when it sees one. The - // publisher's preference is a DEFAULT_PUBLISHER_GROUP_ORDER track property - // instead, which we write from draft-17 on. Draft-16 gets neither form; see - // Properties::encode. - let group_order = match version { - Version::Draft15 => self.properties.group_order, - _ => None, - }; - // Draft-15 has no properties block, so DYNAMIC_GROUPS is a parameter there. - let dynamic_groups = (version == Version::Draft15 && self.properties.dynamic_groups).then_some(1u64); + // GROUP_ORDER is not a legal SUBSCRIBE_OK parameter; a peer closes the session with + // PROTOCOL_VIOLATION when it sees one. The publisher's preference is a + // DEFAULT_PUBLISHER_GROUP_ORDER track property instead. + encode_params!(w, version, + 0x09 => self.largest, + ); - encode_params!(w, version, - 0x09 => self.largest, - 0x22 => group_order, - 0x30 => dynamic_groups, - ); - - // Track Properties are the final field, so nothing may follow. - self.properties.encode(w, version)?; - } - } + // Track Properties are the final field, so nothing may follow. + self.properties.encode(w, version)?; Ok(()) } fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - Some(RequestId::decode(r, version)?) - } else { - None - }; let track_alias = r.varint()?; - let mut properties = Properties::default(); - let mut largest = None; - match version { - Version::Draft14 => { - // EXPIRES is when the publisher expects to end the subscription. That end - // arrives as PUBLISH_DONE regardless, so there is nothing to act on. - let _expires = r.varint()?; - - properties.group_order = Some(GroupOrder::decode(r, version)?.any_to_descending()); - - if r.bool()? { - largest = Some(Location::decode(r, version)?); - } - - properties.max_cache_duration = Parameters::skip_response(r)?.map(std::time::Duration::from_millis); - } - _ => { - // GROUP_ORDER and MAX_CACHE_DURATION are legal here only in draft-15. Draft-16 - // still knows GROUP_ORDER, so one on this message is ignored. From draft-17 - // it closes the session. LARGEST_OBJECT is required once the track has - // content, so rejecting it would tear down a session over a parameter - // compliant publishers must send. EXPIRES is ignored, as on draft-14. - decode_params!(r, version, - 0x04 => max_cache_duration: Option where version == Version::Draft15, - 0x08 => _expires: Option, - 0x09 => largest: Option, - 0x22 => group_order: Option where version == Version::Draft15, - 0x30 => dynamic_groups: Option where version == Version::Draft15, - ); - properties = Properties::decode(r, version)?; - properties.group_order = properties.group_order.or(group_order); - if version == Version::Draft15 { - properties.max_cache_duration = max_cache_duration.map(std::time::Duration::from_millis); - properties.dynamic_groups = dynamic_groups - .map(super::properties::dynamic_groups) - .transpose()? - .unwrap_or(false); - } - - return Ok(Self { - request_id, - track_alias, - largest, - properties, - }); - } - } + // GROUP_ORDER closes the session. LARGEST_OBJECT is required once the track has + // content, so rejecting it would tear down a session over a parameter compliant + // publishers must send. EXPIRES is ignored. + decode_params!(r, version, + 0x08 => _expires: Option, + 0x09 => largest: Option, + ); + let properties = Properties::decode(r, version)?; Ok(Self { - request_id, track_alias, largest, properties, @@ -359,77 +215,20 @@ impl Message for SubscribeOk { } } -/// SubscribeError message (0x05) -#[derive(Clone, Debug)] -pub struct SubscribeError<'a> { - pub request_id: RequestId, - pub error_code: u64, - pub reason_phrase: Cow<'a, str>, -} - -impl Message for SubscribeError<'_> { - const ID: u64 = 0x05; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - self.request_id.encode(w, version)?; - w.varint(self.error_code)?; - w.string(&self.reason_phrase)?; - Ok(()) - } - - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = RequestId::decode(r, version)?; - let error_code = r.varint()?; - let reason_phrase = Cow::Owned(r.string()?); - - Ok(Self { - request_id, - error_code, - reason_phrase, - }) - } -} - -/// Unsubscribe message (0x0a) -#[derive(Clone, Debug)] -pub struct Unsubscribe { - pub request_id: RequestId, -} - -impl Message for Unsubscribe { - const ID: u64 = 0x0a; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - self.request_id.encode(w, version)?; - Ok(()) - } - - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = RequestId::decode(r, version)?; - Ok(Self { request_id }) - } -} - /// SubscribeUpdate message (0x02) #[derive(Clone, Debug)] pub struct SubscribeUpdate { pub request_id: RequestId, - pub subscription_request_id: Option, - /// Draft-14's fixed start, which a peer must not see decrease; unused on later drafts. - pub start_location: Location, - /// Draft-14's fixed end group (0 for open-ended); unused on later drafts. - pub end_group: u64, - /// `None` leaves the priority as it is. Draft-14 cannot omit it, so `None` sends the - /// default there. + /// `None` leaves the priority as it is. pub subscriber_priority: Option, /// `None` leaves forwarding as it is, as for the priority. pub forward: Option, - /// The new LOCATION_FILTER (draft-15+); `None` keeps the subscription's own range. + /// The new LOCATION_FILTER; `None` keeps the subscription's own range. pub filter: Option, /// The `AUTHORIZATION TOKEN` (0x03) renewing the request's credential, if any. At /// most one is accepted: decoding refuses a repeated or second token. pub authorization_token: Option, - /// NEW_GROUP_REQUEST (0x32, draft-16+), as on [`Subscribe::new_group`]. + /// NEW_GROUP_REQUEST (0x32), as on [`Subscribe::new_group`]. pub new_group: Option, /// Subscriber max age in milliseconds (MoQ Timestamp), if negotiated. pub max_age: Option, @@ -442,156 +241,53 @@ impl Message for SubscribeUpdate { const ID: u64 = 0x02; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - match version { - Version::Draft14 => { - self.request_id.encode(w, version)?; - self.subscription_request_id - .expect("subscription_request_id required for draft14") - .encode(w, version)?; - self.start_location.encode(w, version)?; - w.varint(self.end_group)?; - w.u8(self.subscriber_priority.unwrap_or(super::priority::DEFAULT_WIRE)); - w.bool(self.forward.unwrap_or(true)); - encode_params!(w, version, - 0x03 => self.authorization_token.clone(), - super::max_age::PARAMETER => self.max_age, - ); - } - Version::Draft15 | Version::Draft16 => { - self.request_id.encode(w, version)?; - self.subscription_request_id - .expect("subscription_request_id required for draft15-16") - .encode(w, version)?; - encode_params!(w, version, - 0x03 => self.authorization_token.clone(), - 0x10 => self.forward, - 0x20 => self.subscriber_priority, - 0x21 => self.filter, - 0x32 => self.new_group.filter(|_| has_new_group_request(version)), - super::max_age::PARAMETER => self.max_age, - ); - } - _ => { - assert!( - self.subscription_request_id.is_none(), - "subscription_request_id must be None for draft17+" - ); - // REQUEST_UPDATE - self.request_id.encode(w, version)?; - if matches!(version, Version::Draft17) { - w.varint(0)?; // required_request_id_delta = 0 (draft-17 only, removed in draft-18 per #1615) - } - encode_params!(w, version, - 0x03 => self.authorization_token.clone(), - 0x10 => self.forward, - 0x20 => self.subscriber_priority, - 0x21 => self.filter, - 0x23 => self.fill.filter(|_| Filter::is_draft20(version)), - 0x32 => self.new_group, - super::max_age::PARAMETER => self.max_age, - ); - } - } + // REQUEST_UPDATE + self.request_id.encode(w, version)?; + encode_params!(w, version, + 0x03 => self.authorization_token.clone(), + 0x10 => self.forward, + 0x20 => self.subscriber_priority, + 0x21 => self.filter, + 0x23 => self.fill.filter(|_| Filter::is_draft20(version)), + 0x32 => self.new_group, + super::max_age::PARAMETER => self.max_age, + ); Ok(()) } fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - match version { - Version::Draft14 => { - let request_id = RequestId::decode(r, version)?; - let subscription_request_id = Some(RequestId::decode(r, version)?); - let start_location = Location::decode(r, version)?; - let end_group = r.varint()?; - let subscriber_priority = Some(r.u8()?); - let forward = Some(r.bool()?); - decode_params!(r, version, - 0x03 => tokens: Vec, - super::max_age::PARAMETER => max_age: Option, - ); - let authorization_token = super::token::single(tokens)?; - - Ok(Self { - request_id, - subscription_request_id, - start_location, - end_group, - subscriber_priority, - forward, - filter: None, - authorization_token, - new_group: None, - max_age: super::max_age::validate(max_age)?, - fill: None, - }) - } - Version::Draft15 | Version::Draft16 => { - let request_id = RequestId::decode(r, version)?; - let subscription_request_id = Some(RequestId::decode(r, version)?); - decode_params!(r, version, - 0x02 => _object_delivery_timeout: Option, - 0x03 => authorization_token: Vec, - 0x10 => forward: Option, - 0x20 => subscriber_priority: Option, - 0x21 => filter: Option, - 0x32 => new_group: Option where has_new_group_request(version), - super::max_age::PARAMETER => max_age: Option, - ); - - Ok(Self { - request_id, - subscription_request_id, - start_location: Location { group: 0, object: 0 }, - end_group: 0, - subscriber_priority, - forward, - filter, - authorization_token: super::token::single(authorization_token)?, - new_group, - max_age: super::max_age::validate(max_age)?, - fill: None, - }) - } - _ => { - // REQUEST_UPDATE - let request_id = RequestId::decode(r, version)?; - if matches!(version, Version::Draft17) { - let _required_request_id_delta = r.varint()?; - } - // Nothing reads an update's Range Filters yet; they are - // consumed so a legal update does not fail the session. - decode_params!(r, version, - 0x02 => _object_delivery_timeout: Option, - 0x03 => authorization_token: Vec, - 0x06 => _subgroup_delivery_timeout: Option where !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17), - 0x10 => forward: Option, - 0x20 => subscriber_priority: Option, - 0x21 => filter: Option, - 0x23 => fill: Option where Filter::is_draft20(version), - 0x25 => _subgroup_filter: Vec where has_range_filters(version), - 0x26 => _object_id_filter: Vec where has_range_filters(version), - 0x27 => _priority_filter: Vec where has_range_filters(version), - 0x28 => _object_property_filter: Vec where has_range_filters(version), - 0x29 => _track_property_filter: Vec where has_range_filters(version), - 0x32 => new_group: Option, - super::max_age::PARAMETER => max_age: Option, - ); + // REQUEST_UPDATE + let request_id = RequestId::decode(r, version)?; + // Nothing reads an update's Range Filters yet; they are + // consumed so a legal update does not fail the session. + decode_params!(r, version, + 0x02 => _object_delivery_timeout: Option, + 0x03 => authorization_token: Vec, + 0x06 => _subgroup_delivery_timeout: Option, + 0x10 => forward: Option, + 0x20 => subscriber_priority: Option, + 0x21 => filter: Option, + 0x23 => fill: Option where Filter::is_draft20(version), + 0x25 => _subgroup_filter: Vec where has_range_filters(version), + 0x26 => _object_id_filter: Vec where has_range_filters(version), + 0x27 => _priority_filter: Vec where has_range_filters(version), + 0x28 => _object_property_filter: Vec where has_range_filters(version), + 0x29 => _track_property_filter: Vec where has_range_filters(version), + 0x32 => new_group: Option, + super::max_age::PARAMETER => max_age: Option, + ); - Ok(Self { - request_id, - subscription_request_id: None, - start_location: Location { group: 0, object: 0 }, - end_group: 0, - subscriber_priority, - forward, - filter, - authorization_token: super::token::single(authorization_token)?, - new_group, - max_age: super::max_age::validate(max_age)?, - fill, - }) - } - } + Ok(Self { + request_id, + subscriber_priority, + forward, + filter, + authorization_token: super::token::single(authorization_token)?, + new_group, + max_age: super::max_age::validate(max_age)?, + fill, + }) } } @@ -614,10 +310,6 @@ mod tests { #[test] fn subscriber_max_age_round_trips() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -647,10 +339,6 @@ mod tests { let update = SubscribeUpdate { max_age, request_id: RequestId(2), - subscription_request_id: matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then_some(RequestId(0)), - start_location: Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: None, forward: None, filter: None, @@ -724,10 +412,6 @@ mod tests { /// Build a SUBSCRIBE body carrying a single RENDEZVOUS_TIMEOUT parameter. const VERSIONS: [Version; 9] = [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -765,10 +449,6 @@ mod tests { new_group: Some(7), max_age: None, request_id: RequestId(3), - subscription_request_id: matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then_some(RequestId(1)), - start_location: Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: None, forward: None, filter: None, @@ -785,9 +465,6 @@ mod tests { let mut buf = Vec::new(); let w = &mut Encoder::new(&mut buf, version.into()); RequestId(1).encode(w, version)?; - if version == Version::Draft17 { - w.varint(0)?; // required_request_id_delta - } encode_namespace(w, &Path::new("test"))?; w.string("video")?; encode_params!(w, version, @@ -805,7 +482,7 @@ mod tests { /// SUBSCRIBE. #[test] fn rendezvous_timeout_retains_whether_a_wait_was_requested() { - for version in [Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20] { + for version in [Version::Draft18, Version::Draft19, Version::Draft20] { for millis in [0, 5000] { let encoded = subscribe_with_rendezvous(millis, version); let msg: Subscribe = decode_message(&encoded, version) @@ -838,9 +515,6 @@ mod tests { fn first_param_key(encoded: &[u8], version: Version) -> Option { let r = &mut Decoder::new(encoded, version.into()); RequestId::decode(r, version).unwrap(); - if version == Version::Draft17 { - r.varint().unwrap(); - } decode_track(r).unwrap(); // draft-14/15 write absolute keys, draft-16+ deltas, but the first is absolute either way. @@ -869,7 +543,7 @@ mod tests { epoch: None, }; - for version in [Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20] { + for version in [Version::Draft18, Version::Draft19, Version::Draft20] { // The reader has to be able to see a 0x04, or its absence below proves nothing. assert_eq!( first_param_key(&subscribe_with_rendezvous(5000, version), version), @@ -914,7 +588,6 @@ mod tests { #[test] fn test_subscribe_ok() { let msg = SubscribeOk { - request_id: Some(RequestId(42)), track_alias: 42, largest: None, properties: Properties::default(), @@ -930,7 +603,7 @@ mod tests { fn test_subscribe_ok_accepts_largest_object() { // request_id=4, track_alias=4, one LARGEST_OBJECT parameter at {5, 0}. let payload = [0x04, 0x04, 0x01, 0x09, 0x02, 0x05, 0x00]; - for version in [Version::Draft15, Version::Draft16] { + for version in [Version::Draft16] { let decoded: SubscribeOk = decode_message(&payload, version).unwrap(); assert_eq!(decoded.request_id, Some(RequestId(4))); @@ -945,7 +618,7 @@ mod tests { #[test] fn test_subscribe_ok_accepts_largest_object_draft17_on() { let payload = [0x04, 0x01, 0x09, 0x05, 0x00]; - for version in [Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20] { + for version in [Version::Draft18, Version::Draft19, Version::Draft20] { let decoded: SubscribeOk = decode_message(&payload, version).unwrap(); assert_eq!(decoded.request_id, None, "{version}"); @@ -988,7 +661,6 @@ mod tests { (Version::Draft20, &[42, 1, 0x09, 0x09, 0x03][..]), ] { let msg = SubscribeOk { - request_id: matches!(version, Version::Draft14 | Version::Draft16).then_some(RequestId(7)), track_alias: 42, largest, properties: Properties::default(), @@ -1004,7 +676,6 @@ mod tests { #[test] fn test_subscribe_ok_v15() { let msg = SubscribeOk { - request_id: Some(RequestId(42)), track_alias: 42, largest: None, properties: Properties::default(), @@ -1144,9 +815,6 @@ mod tests { #[test] fn test_subscribe_omitted_priority_is_128() { for version in [ - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -1154,9 +822,6 @@ mod tests { Version::Draft22, ] { let mut bytes = vec![0x00]; // request_id - if version == Version::Draft17 { - bytes.push(0x00); // required_request_id_delta - } bytes.extend_from_slice(b"\x01\x0bnonexistent\x0dno-such-track"); // namespace, name bytes.push(0x00); // no parameters let msg = decode_message::(&bytes, version).unwrap(); @@ -1171,8 +836,6 @@ mod tests { max_age: None, request_id: RequestId(10), subscription_request_id: Some(RequestId(5)), - start_location: Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: Some(200), forward: Some(true), filter: Some(Filter::NextObject), @@ -1290,7 +953,6 @@ mod tests { #[test] fn test_subscribe_ok_v17_round_trip() { let msg = SubscribeOk { - request_id: None, track_alias: 42, largest: None, properties: Properties::default(), @@ -1309,9 +971,6 @@ mod tests { new_group: None, max_age: None, request_id: RequestId(10), - subscription_request_id: None, - start_location: Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: Some(200), forward: Some(true), filter: Some(Filter::NextObject), @@ -1360,7 +1019,6 @@ mod tests { #[test] fn test_subscribe_ok_v18_round_trip() { let msg = SubscribeOk { - request_id: None, track_alias: 42, largest: None, properties: Properties::default(), @@ -1378,10 +1036,6 @@ mod tests { #[test] fn subscribe_ok_priority_property_bytes_on_every_draft() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -1389,8 +1043,6 @@ mod tests { Version::Draft22, ] { let mut msg = SubscribeOk { - request_id: matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then_some(RequestId(7)), track_alias: 42, largest: None, properties: Properties::default(), @@ -1398,18 +1050,14 @@ mod tests { let baseline = encode_message(&msg, version); msg.properties.priority = Some(37); let encoded = encode_message(&msg, version); - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - assert_eq!(encoded, baseline, "{version}"); - } else { - assert_eq!(encoded, [baseline, vec![0x0e, 37]].concat(), "{version}"); - assert_eq!( - decode_message::(&encoded, version) - .unwrap() - .properties - .priority, - Some(37) - ); - } + assert_eq!(encoded, [baseline, vec![0x0e, 37]].concat(), "{version}"); + assert_eq!( + decode_message::(&encoded, version) + .unwrap() + .properties + .priority, + Some(37) + ); } } @@ -1420,7 +1068,6 @@ mod tests { #[test] fn test_subscribe_ok_v18_group_order_is_a_property() { let msg = SubscribeOk { - request_id: None, track_alias: 42, largest: None, properties: Properties { @@ -1452,7 +1099,6 @@ mod tests { #[test] fn test_subscribe_ok_v15_group_order_is_a_parameter() { let msg = SubscribeOk { - request_id: Some(RequestId(7)), track_alias: 42, largest: None, properties: Properties { @@ -1484,7 +1130,6 @@ mod tests { #[test] fn test_subscribe_ok_v15_dynamic_groups_is_a_parameter() { let msg = SubscribeOk { - request_id: Some(RequestId(7)), track_alias: 42, largest: None, properties: Properties { @@ -1534,7 +1179,7 @@ mod tests { buf.extend_from_slice(&body); let mut bytes = bytes::Bytes::from(buf); - let decoded = crate::coding::decode_buf(&mut bytes, Version::Draft16, SubscribeOk::decode).unwrap(); + let decoded = crate::coding::decode_buf(&mut bytes, SubscribeOk::decode).unwrap(); assert_eq!(decoded.track_alias, 42); assert_eq!(decoded.properties.group_order, Some(GroupOrder::Descending)); } @@ -1562,9 +1207,6 @@ mod tests { new_group: None, max_age: None, request_id: RequestId(10), - subscription_request_id: None, - start_location: Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: Some(200), forward: Some(true), filter: Some(Filter::NextObject), @@ -1590,9 +1232,6 @@ mod tests { new_group: None, max_age: None, request_id: RequestId(10), - subscription_request_id: None, - start_location: Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: Some(200), forward: Some(true), filter: Some(Filter::NextObject), @@ -1717,10 +1356,6 @@ mod tests { #[test] fn subscribe_decodes_forward_zero() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -1757,18 +1392,11 @@ mod tests { 0x03, 0x03, 0x03, 0x00, 0xAA, // AUTHORIZATION TOKEN ]); for version in [ - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, ] { let mut body = body.clone(); - if version == Version::Draft17 { - // Required Request ID Delta, draft-17 only. - body.insert(1, 0x00); - } let wire = body; let mut buf = Decoder::new(&wire, version.into()); Subscribe::decode_msg(&mut buf, version).unwrap_or_else(|e| panic!("{version}: {e}")); @@ -1851,10 +1479,6 @@ mod cache_duration_tests { #[test] fn max_cache_duration_is_read_in_each_drafts_field() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -1862,12 +1486,7 @@ mod cache_duration_tests { Version::Draft22, ] { for age in [0u64, 30_000] { - let mut payload = match version { - Version::Draft14 => vec![0, 0, 0, 1, 0, 1, 4], - Version::Draft15 => vec![0, 0, 1, 4], - Version::Draft16 => vec![0, 0, 0, 4], - _ => vec![0, 0, 4], - }; + let mut payload = vec![0, 0, 4]; Encoder::new(&mut payload, version.into()).varint(age).unwrap(); let got = crate::coding::decode_buf(&mut payload.as_slice(), version, SubscribeOk::decode_msg).unwrap(); assert_eq!( @@ -1882,17 +1501,13 @@ mod cache_duration_tests { #[test] fn legacy_never_sends_cache_duration_and_modern_preserves_optional_value() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, Version::Draft21, Version::Draft22, ] { - let legacy = matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16); + let legacy = false; for age in [None, Some(Duration::ZERO), Some(Duration::from_secs(30))] { let ok = SubscribeOk { request_id: legacy.then_some(RequestId(0)), diff --git a/rs/moq-net/src/ietf/subscribe_namespace.rs b/rs/moq-net/src/ietf/subscribe_namespace.rs index 79cb3233061..658c0352152 100644 --- a/rs/moq-net/src/ietf/subscribe_namespace.rs +++ b/rs/moq-net/src/ietf/subscribe_namespace.rs @@ -1,6 +1,5 @@ //! IETF moq-transport subscribe namespace messages -use std::borrow::Cow; use crate::{Path, coding::*, ietf::RequestId}; @@ -11,20 +10,7 @@ use super::publish_namespace::{AdvertParams, Negotiated}; use super::Version; -/// True for the drafts that use the legacy 0x11 SUBSCRIBE_NAMESPACE message. -/// Draft-18+ uses the renumbered 0x50 message instead. -fn is_legacy_version(version: Version) -> bool { - matches!( - version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 - ) -} - -/// SUBSCRIBE_NAMESPACE message (draft-18+, type 0x50). -/// -/// Draft-18 renumbered the message from 0x11 to 0x50 and dropped the Subscribe -/// Options field when it split SUBSCRIBE_TRACKS (0x51) off into its own message -/// type (#1542). Draft-14 through draft-17 use [`SubscribeNamespaceLegacy`]. +/// SUBSCRIBE_NAMESPACE message (type 0x50). #[derive(Clone, Debug)] pub struct SubscribeNamespace<'a> { pub request_id: RequestId, @@ -52,9 +38,6 @@ impl Message for SubscribeNamespace<'_> { const ID: u64 = 0x50; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - if is_legacy_version(version) { - return Err(EncodeError::Version); - } self.request_id.encode(w, version)?; encode_namespace(w, &self.namespace)?; encode_params!(w, version, HIDDEN_PARAM => hidden_param(self.hidden)); @@ -62,9 +45,6 @@ impl Message for SubscribeNamespace<'_> { } fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - if is_legacy_version(version) { - return Err(DecodeError::Version); - } let request_id = RequestId::decode(r, version)?; let namespace = decode_namespace(r)?; // The token decodes but grants nothing: the session's grant is what authorizes the request. @@ -84,200 +64,7 @@ impl Message for SubscribeNamespace<'_> { } } -/// What a draft-16/17 SUBSCRIBE_NAMESPACE asks for (d16 §9.25, d17 §9.20). -/// -/// Draft-14/15 have no field and only ever ask for namespaces. Draft-18 dropped it -/// when SUBSCRIBE_TRACKS took over the PUBLISH half. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum SubscribeOptions { - /// PUBLISH for each matching track (0x00). - Publish, - /// NAMESPACE for each matching namespace (0x01). - Namespace, - /// Both PUBLISH and NAMESPACE (0x02). - Both, -} - -impl SubscribeOptions { - fn encode(self, w: &mut Encoder<'_>) -> Result<(), EncodeError> { - w.varint(match self { - Self::Publish => 0x00u64, - Self::Namespace => 0x01, - Self::Both => 0x02, - }) - } - - fn decode(r: &mut Decoder<'_>) -> Result { - match r.varint()? { - 0x00 => Ok(Self::Publish), - 0x01 => Ok(Self::Namespace), - 0x02 => Ok(Self::Both), - _ => Err(DecodeError::InvalidValue), - } - } -} - -/// SUBSCRIBE_NAMESPACE message for draft-14 through draft-17 (type 0x11). -/// -/// In v16 this moves from the control stream to its own bidirectional stream. -/// Draft-16/17 carry [`SubscribeOptions`]; draft-17 additionally prefixes a -/// Required Request ID delta (removed in draft-18 per #1615). Draft-18+ uses -/// [`SubscribeNamespace`]. -#[derive(Clone, Debug)] -pub struct SubscribeNamespaceLegacy<'a> { - pub request_id: RequestId, - pub namespace: Path<'a>, - /// Encoded on v16/v17 only; earlier drafts always mean [`SubscribeOptions::Namespace`]. - pub subscribe_options: SubscribeOptions, - /// MoQ Hidden: see [`SubscribeNamespace::hidden`]. - pub hidden: bool, -} - -impl Message for SubscribeNamespaceLegacy<'_> { - const ID: u64 = 0x11; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - if !is_legacy_version(version) { - return Err(EncodeError::Version); - } - self.request_id.encode(w, version)?; - if version == Version::Draft17 { - w.varint(0)?; // required_request_id_delta = 0 (draft-17 only, removed in draft-18 per #1615) - } - encode_namespace(w, &self.namespace)?; - if matches!(version, Version::Draft16 | Version::Draft17) { - self.subscribe_options.encode(w)?; - } - encode_params!(w, version, HIDDEN_PARAM => hidden_param(self.hidden)); - Ok(()) - } - - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - if !is_legacy_version(version) { - return Err(DecodeError::Version); - } - let request_id = RequestId::decode(r, version)?; - if version == Version::Draft17 { - let _required_request_id_delta = r.varint()?; - } - let namespace = decode_namespace(r)?; - let subscribe_options = match version { - Version::Draft16 | Version::Draft17 => SubscribeOptions::decode(r)?, - _ => SubscribeOptions::Namespace, - }; - - // The token decodes but grants nothing: the session's grant is what authorizes the request. - // FORWARD is legal here in draft-15 through draft-17. The value is checked (only - // 0 or 1) and then dropped: a namespace subscription has no objects to forward. - // Draft-14 has no such parameter, so an unlisted id is ignored. Draft-18 moved - // FORWARD onto SUBSCRIBE_TRACKS and closes if it shows up on the new message. - decode_params!(r, version, - 0x03 => authorization_token: Vec, - 0x10 => _forward: Option where matches!(version, Version::Draft15 | Version::Draft16 | Version::Draft17), - HIDDEN_PARAM => hidden: Option, - ); - let hidden = hidden_from_param(hidden)?; - // Last, so a malformed field closes the session before a token refusal. - super::token::single(authorization_token)?; - - Ok(Self { - request_id, - namespace, - subscribe_options, - hidden, - }) - } -} - -/// SubscribeNamespaceOk message (0x12): v14 only -#[derive(Clone, Debug)] -pub struct SubscribeNamespaceOk { - pub request_id: RequestId, -} - -impl Message for SubscribeNamespaceOk { - const ID: u64 = 0x12; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - self.request_id.encode(w, version)?; - Ok(()) - } - - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = RequestId::decode(r, version)?; - Ok(Self { request_id }) - } -} - -/// SubscribeNamespaceError message (0x13): v14 only -#[derive(Clone, Debug)] -pub struct SubscribeNamespaceError<'a> { - pub request_id: RequestId, - pub error_code: u64, - pub reason_phrase: Cow<'a, str>, -} - -impl Message for SubscribeNamespaceError<'_> { - const ID: u64 = 0x13; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - self.request_id.encode(w, version)?; - w.varint(self.error_code)?; - w.string(&self.reason_phrase)?; - Ok(()) - } - - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - let request_id = RequestId::decode(r, version)?; - let error_code = r.varint()?; - let reason_phrase = Cow::Owned(r.string()?); - - Ok(Self { - request_id, - error_code, - reason_phrase, - }) - } -} - -/// UnsubscribeNamespace message (0x14): v14/v15 only (v16 uses stream close). -/// v14 names the prefix it subscribed, v15 the request. -#[derive(Clone, Debug)] -pub struct UnsubscribeNamespace<'a> { - /// v14: the Track Namespace Prefix of the SUBSCRIBE_NAMESPACE it cancels. - pub prefix: Path<'a>, - /// v15: the Request ID of the SUBSCRIBE_NAMESPACE it cancels. - pub request_id: RequestId, -} - -impl Message for UnsubscribeNamespace<'_> { - const ID: u64 = 0x14; - - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - match version { - Version::Draft14 => encode_namespace(w, &self.prefix)?, - Version::Draft15 => self.request_id.encode(w, version)?, - _ => return Err(EncodeError::Version), - } - Ok(()) - } - - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - match version { - Version::Draft14 => Ok(Self { - prefix: decode_namespace(r)?, - request_id: RequestId(0), - }), - Version::Draft15 => Ok(Self { - prefix: Path::default(), - request_id: RequestId::decode(r, version)?, - }), - _ => Err(DecodeError::Version), - } - } -} - -/// NAMESPACE message (0x08): v16+, sent on the SUBSCRIBE_NAMESPACE bidi stream. +/// NAMESPACE message (0x08), sent on the SUBSCRIBE_NAMESPACE bidi stream. /// Indicates a namespace suffix matching the subscribed prefix is active. /// /// The base message carries only the suffix. A session that negotiated the MoQ Cluster @@ -326,7 +113,7 @@ impl Message for Namespace<'_> { } } -/// NAMESPACE_DONE message (0x0E): v16 only, sent on SUBSCRIBE_NAMESPACE bidi stream +/// NAMESPACE_DONE message (0x0E), sent on the SUBSCRIBE_NAMESPACE bidi stream. /// Indicates a namespace suffix matching the subscribed prefix is no longer active. #[derive(Clone, Debug)] pub struct NamespaceDone<'a> { @@ -387,29 +174,6 @@ mod tests { Ok((decoded, buf.is_empty())) } - /// Draft-14 names the prefix it cancels and draft-15 the request; later drafts close the - /// stream instead. - #[test] - fn unsubscribe_namespace_names_the_prefix_then_the_request() { - let msg = UnsubscribeNamespace { - prefix: Path::new("cam/front"), - request_id: RequestId(6), - }; - let mut v14 = bytes::Bytes::from(body(&msg, Version::Draft14)); - let named = crate::coding::decode_buf(&mut v14, Version::Draft14, UnsubscribeNamespace::decode_msg).unwrap(); - assert_eq!(named.prefix.as_str(), "cam/front"); - - let mut v15 = bytes::Bytes::from(body(&msg, Version::Draft15)); - let numbered = crate::coding::decode_buf(&mut v15, Version::Draft15, UnsubscribeNamespace::decode_msg).unwrap(); - assert_eq!(numbered.request_id, RequestId(6)); - - let mut buf = Vec::new(); - assert!( - msg.encode_msg(&mut Encoder::new(&mut buf, Version::Draft16.into()), Version::Draft16) - .is_err() - ); - } - fn clustered(hops: &[u64], cost: u64) -> Option { Some(AdvertParams { cluster: Some(cluster::Advert { @@ -590,76 +354,36 @@ mod tests { ); } - /// FORWARD (0x10) is legal on SUBSCRIBE_NAMESPACE through draft-17. A value other - /// than 0 or 1 still closes the session. Draft-18 dropped it from this message. + /// Draft-18 moved FORWARD (0x10) onto SUBSCRIBE_TRACKS, so it closes the session here. #[test] - fn forward_follows_the_draft() { - fn with_forward(version: Version, value: u64) -> Vec { - let mut buf = Vec::new(); - let w = &mut Encoder::new(&mut buf, version.into()); - RequestId(1).encode(w, version).unwrap(); - if version == Version::Draft17 { - w.varint(0).unwrap(); - } - encode_namespace(w, &Path::default()).unwrap(); - if matches!(version, Version::Draft16 | Version::Draft17) { - w.varint(1).unwrap(); - } - // Count, absolute key, then the raw varint. encode_params! needs a Result. - w.varint(1).unwrap(); - w.varint(0x10).unwrap(); - w.varint(value).unwrap(); - buf - } - - for version in [Version::Draft15, Version::Draft16, Version::Draft17] { - let mut buf = bytes::Bytes::from(with_forward(version, 0)); - crate::coding::decode_buf(&mut buf, version, SubscribeNamespaceLegacy::decode_msg) - .unwrap_or_else(|err| panic!("{version} FORWARD=0: {err}")); - assert!(buf.is_empty(), "{version}"); - - let mut buf = bytes::Bytes::from(with_forward(version, 2)); - assert!( - crate::coding::decode_buf(&mut buf, version, SubscribeNamespaceLegacy::decode_msg).is_err(), - "{version} FORWARD=2" - ); - } - - let mut buf = bytes::Bytes::from(with_forward(Version::Draft18, 0)); - assert!(crate::coding::decode_buf(&mut buf, Version::Draft18, SubscribeNamespace::decode_msg).is_err()); + fn forward_is_refused() { + let version = Version::Draft18; + let mut buf = Vec::new(); + let w = &mut Encoder::new(&mut buf, version.into()); + RequestId(1).encode(w, version).unwrap(); + encode_namespace(w, &Path::default()).unwrap(); + // Count, key, then the raw varint. + w.varint(1).unwrap(); + w.varint(0x10).unwrap(); + w.varint(0).unwrap(); + let mut buf = bytes::Bytes::from(buf); + assert!(crate::coding::decode_buf(&mut buf, version, SubscribeNamespace::decode_msg).is_err()); } - /// SUBSCRIBE_NAMESPACE's message types, pinned to the drafts: 0x11 through draft-17, - /// renumbered to 0x50 in draft-18. #[test] - fn message_type_code_points() { - for (name, id, spec) in [ - ("SUBSCRIBE_NAMESPACE (draft-17)", SubscribeNamespaceLegacy::ID, 0x11), - ("SUBSCRIBE_NAMESPACE", SubscribeNamespace::ID, 0x50), - ] { - assert_eq!(id, spec, "{name}"); - } + fn message_type_code_point() { + assert_eq!(SubscribeNamespace::ID, 0x50); } #[test] - fn draft18_omits_subscribe_options() { - // Draft-18 modern body: Request ID (0x00), empty namespace field-count - // (0x00), Number of Parameters (0x00). No Subscribe Options field. - let modern = SubscribeNamespace { - request_id: RequestId(0), - namespace: Path::default(), - hidden: false, - }; - assert_eq!(body(&modern, Version::Draft18), vec![0x00, 0x00, 0x00]); - - // The legacy draft-17 body keeps the options field, so it is one byte longer. - let legacy = SubscribeNamespaceLegacy { + fn body_has_no_subscribe_options() { + // Request ID (0x00), empty namespace field-count (0x00), Number of Parameters (0x00). + let msg = SubscribeNamespace { request_id: RequestId(0), namespace: Path::default(), - subscribe_options: SubscribeOptions::Namespace, hidden: false, }; - assert!(body(&legacy, Version::Draft17).len() > body(&modern, Version::Draft18).len()); + assert_eq!(body(&msg, Version::Draft18), vec![0x00, 0x00, 0x00]); } #[test] @@ -693,73 +417,6 @@ mod tests { hidden ); - for version in [Version::Draft14, Version::Draft16, Version::Draft17] { - let msg = SubscribeNamespaceLegacy { - request_id: RequestId(4), - namespace: Path::new("example"), - subscribe_options: SubscribeOptions::Namespace, - hidden, - }; - let mut buf = bytes::Bytes::from(body(&msg, version)); - let decoded = - crate::coding::decode_buf(&mut buf, version, SubscribeNamespaceLegacy::decode_msg).unwrap(); - assert_eq!(decoded.hidden, hidden, "{version:?}"); - } - } - } - - #[test] - fn legacy_round_trips() { - for version in [Version::Draft16, Version::Draft17] { - for options in [ - SubscribeOptions::Publish, - SubscribeOptions::Namespace, - SubscribeOptions::Both, - ] { - let msg = SubscribeNamespaceLegacy { - request_id: RequestId(4), - namespace: Path::new("example/meeting"), - subscribe_options: options, - hidden: false, - }; - let mut buf = bytes::Bytes::from(body(&msg, version)); - let decoded = - crate::coding::decode_buf(&mut buf, version, SubscribeNamespaceLegacy::decode_msg).unwrap(); - assert!(buf.is_empty(), "trailing bytes for {version:?}"); - assert_eq!(decoded.request_id, RequestId(4)); - assert_eq!(decoded.namespace.as_str(), "example/meeting"); - assert_eq!(decoded.subscribe_options, options); - } - } - } - - /// The drafts define only 0x00 through 0x02, so we refuse any other value as a protocol violation. - #[test] - fn legacy_rejects_unknown_subscribe_options() { - // Request ID, empty namespace, Subscribe Options 0x03, no parameters. - let mut buf = bytes::Bytes::from(vec![0x00, 0x00, 0x03, 0x00]); - assert!(matches!( - crate::coding::decode_buf(&mut buf, Version::Draft16, SubscribeNamespaceLegacy::decode_msg), - Err(DecodeError::InvalidValue) - )); - } - - #[test] - fn rejects_wrong_version() { - // The modern 0x50 message only exists in draft-18+. - for version in [Version::Draft14, Version::Draft16, Version::Draft17] { - let mut buf = bytes::Bytes::from(vec![0x00, 0x00, 0x00]); - assert!(matches!( - crate::coding::decode_buf(&mut buf, version, SubscribeNamespace::decode_msg), - Err(DecodeError::Version) - )); } - - // The legacy 0x11 message only exists in draft-14..17. - let mut buf = bytes::Bytes::from(vec![0x00, 0x00, 0x00]); - assert!(matches!( - crate::coding::decode_buf(&mut buf, Version::Draft18, SubscribeNamespaceLegacy::decode_msg), - Err(DecodeError::Version) - )); } } diff --git a/rs/moq-net/src/ietf/subscribe_tracks.rs b/rs/moq-net/src/ietf/subscribe_tracks.rs index 5dba72a7103..bfbb7db5610 100644 --- a/rs/moq-net/src/ietf/subscribe_tracks.rs +++ b/rs/moq-net/src/ietf/subscribe_tracks.rs @@ -51,9 +51,6 @@ impl Message for SubscribeTracks<'_> { const ID: u64 = 0x51; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - if !has_subscribe_tracks(version) { - return Err(EncodeError::Version); - } self.request_id.encode(w, version)?; encode_namespace(w, &self.prefix)?; // Absent means 1, so only a paused listing writes it. @@ -77,9 +74,6 @@ impl Message for SubscribeTracks<'_> { } fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - if !has_subscribe_tracks(version) { - return Err(DecodeError::Version); - } let request_id = RequestId::decode(r, version)?; let prefix = decode_namespace(r)?; // From draft-19 any parameter a SUBSCRIBE takes is legal here (section 10.19.1); @@ -133,18 +127,10 @@ impl Message for SubscribeTracks<'_> { } } -/// Whether `version` defines SUBSCRIBE_TRACKS: draft-18 split it off SUBSCRIBE_NAMESPACE. -pub(crate) fn has_subscribe_tracks(version: Version) -> bool { - !matches!( - version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 - ) -} - /// PUBLISH_SKIPPED (0x0F): a track under a SUBSCRIBE_TRACKS prefix the publisher will /// not PUBLISH, sent on that request's stream. The subscriber may SUBSCRIBE to it instead. /// -/// Drafts 17 and 18 name it PUBLISH_BLOCKED and send it only when the publisher has run +/// Draft-18 names it PUBLISH_BLOCKED and send it only when the publisher has run /// out of streams for PUBLISH; draft-19 renamed it and allows it for any reason. #[derive(Clone, Debug)] pub struct PublishSkipped<'a> { @@ -156,31 +142,19 @@ pub struct PublishSkipped<'a> { impl Message for PublishSkipped<'_> { const ID: u64 = 0x0F; - fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - if !has_publish_skipped(version) { - return Err(EncodeError::Version); - } + fn encode_msg(&self, w: &mut Encoder<'_>, _: Version) -> Result<(), EncodeError> { encode_namespace(w, &self.suffix)?; w.string(&self.track_name)?; Ok(()) } - fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - if !has_publish_skipped(version) { - return Err(DecodeError::Version); - } + fn decode_msg(r: &mut Decoder<'_>, _: Version) -> Result { let suffix = decode_namespace(r)?; let track_name = Cow::Owned(r.string()?); Ok(Self { suffix, track_name }) } } -/// Whether `version` defines [`PublishSkipped`], under either name. Draft-16 and older -/// had no message 0x0F. -fn has_publish_skipped(version: Version) -> bool { - !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) -} - #[cfg(test)] mod tests { use super::*; @@ -316,7 +290,7 @@ mod tests { #[test] fn publish_skipped_round_trips() { - for version in [Version::Draft17, Version::Draft18, Version::Draft22] { + for version in [Version::Draft18, Version::Draft22] { let msg = PublishSkipped { suffix: Path::new("cam"), track_name: "video".into(), diff --git a/rs/moq-net/src/ietf/subscriber.rs b/rs/moq-net/src/ietf/subscriber.rs index e58bd3dc828..b195f174785 100644 --- a/rs/moq-net/src/ietf/subscriber.rs +++ b/rs/moq-net/src/ietf/subscriber.rs @@ -529,7 +529,7 @@ fn accepted_info(properties: &ietf::Properties, version: Version) -> track::Info /// Why a track a `version` publisher answered with `properties` is neither processed nor /// forwarded, if it is not. fn refused(properties: &ietf::Properties, version: Version, negotiated: bool) -> Option { - if properties.compress && !(negotiated && super::flate::supported(version)) { + if properties.compress && !negotiated { return Some(Error::UnsupportedExtension); } let info = accepted_info(properties, version); @@ -663,10 +663,10 @@ enum Withdraw { Done, /// Close our side and wait for the peer to read everything. Close, - /// Refuse the request with this ID, then close. - Reject(RequestId, crate::auth::Refusal), - /// Refuse the update with this ID, then cancel the advertisement. - Cancel(RequestId, crate::auth::Refusal), + /// Refuse the request, then close. + Reject(crate::auth::Refusal), + /// Refuse an update, then cancel the advertisement. + Cancel(crate::auth::Refusal), } /// What one advertisement said, once its parameters are resolved against the session. @@ -1002,13 +1002,9 @@ impl Subscriber { retire_track_alias(&aliases, alias, REQUEST_ID); } - /// What the peer declared in its SETUP, or the default (extension off) on a version - /// that cannot negotiate it. See [`super::Publisher::peer`]. + /// What the peer declared in its SETUP. See [`super::Publisher::peer`]. pub(super) async fn peer(&self) -> cluster::Peer { - match cluster::supported(self.version) { - true => self.peer_setup.get().await.cluster, - false => cluster::Peer::default(), - } + self.peer_setup.get().await.cluster } /// The announcing session's declared or assigned identity, for split-horizon. @@ -1169,13 +1165,8 @@ impl Subscriber { snapshot: crate::model::Snapshot, live: kio::Consumer<()>, ) -> Result { - let stream = if self.version == Version::Draft16 { - stream.with_stats(self.origin.stats()) - } else { - super::request_stream::with_stats(stream, self.origin.stats(), self.version) - }; let mut request = NamespaceRequest { - stream, + stream: stream.with_stats(self.origin.stats()), messages: super::request_stream::Messages::new(self.session.clone(), self.control.client()), prefix, request_id: None, @@ -1214,55 +1205,16 @@ impl Subscriber { let hidden = declared.hidden; let prefix = request.prefix.clone(); - // Draft-16 is the first to allow a zero-field namespace, so the empty prefix is a - // protocol violation on draft-14/15. A peer that never declared MoQ Solicit is not - // ours: it may enforce that, and it tells us unasked anyway. One that declared it - // accepts the empty prefix and only tells when asked, so it still gets one. - if prefix.is_empty() - && declared.solicit.is_none() - && matches!(self.version, Version::Draft14 | Version::Draft15) - { - tracing::debug!(version = ?self.version, "not asking a foreign peer for the empty namespace"); - return Ok(namespaces::Ended::Peer); - } - - // No ID is spent on a request that ends while it waits for credit, so the peer - // never sees a gap in our sequential request IDs. - let mut timeout = crate::time::Deadline::after(&self.runtime, Control::REQUEST_ID_TIMEOUT); - let request_id = kio::wait(|waiter| { - if live.poll_closed(waiter).is_ready() { - return Poll::Ready(Ok(None)); - } - self.control.poll_credit(waiter, &mut timeout).map(|id| id.map(Some)) - }) - .await?; - let Some(request_id) = request_id else { - return Ok(namespaces::Ended::Local); - }; + let request_id = self.control.next_request_id(); request.request_id = Some(request_id); let stream = &mut request.stream; - // Draft-18+ uses SUBSCRIBE_NAMESPACE (0x50); earlier drafts use the legacy - // 0x11 message with a Subscribe Options field. - match self.version { - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 => { - let msg = ietf::SubscribeNamespaceLegacy { - request_id, - namespace: prefix.clone(), - subscribe_options: ietf::SubscribeOptions::Namespace, - hidden, - }; - stream.writer.encode_message(&msg).await?; - } - _ => { - let msg = ietf::SubscribeNamespace { - request_id, - namespace: prefix.clone(), - hidden, - }; - stream.writer.encode_message(&msg).await?; - } - } + let msg = ietf::SubscribeNamespace { + request_id, + namespace: prefix.clone(), + hidden, + }; + stream.writer.encode_message(&msg).await?; tracing::debug!(%prefix, "subscribe_namespace sent"); @@ -1275,17 +1227,7 @@ impl Subscriber { let mut data = body.decoder(self.version); let count = match type_id { - ietf::SubscribeNamespaceOk::ID if self.version == Version::Draft14 => { - ietf::SubscribeNamespaceOk::decode_msg(&mut data, self.version)?; - None - } ietf::RequestOk::ID => ietf::RequestOk::decode_msg(&mut data, self.version)?.active, - ietf::SubscribeNamespaceError::ID if self.version == Version::Draft14 => { - let msg = ietf::SubscribeNamespaceError::decode_msg(&mut data, self.version)?; - let err = request::from_code(msg.error_code, request::Kind::SubscribeNamespace, self.version); - tracing::warn!(%err, reason = %msg.reason_phrase, "subscribe_namespace error"); - return Err(err); - } ietf::RequestError::ID => { let msg = ietf::RequestError::decode_msg(&mut data, self.version)?; let err = request::from_code(msg.error_code, request::Kind::SubscribeNamespace, self.version); @@ -1342,10 +1284,8 @@ impl Subscriber { /// overlapping one can go out: a peer may refuse a prefix that overlaps one it still /// holds. /// - /// Draft-14 and 15 ride the control stream, whose virtual streams have no close of - /// their own, so UNSUBSCRIBE_NAMESPACE is all the peer ever sees, and anything after - /// it on that ordered stream reaches the peer later. Later drafts close the stream - /// (a FIN until draft-19 makes it a reset) and read until the peer closes its half. + /// The stream closes (a FIN until draft-19 makes it a reset) and is read until the peer + /// closes its half. async fn unsubscribe_namespace(&self, request: NamespaceRequest) { let NamespaceRequest { stream: Stream { mut writer, mut reader }, @@ -1355,23 +1295,10 @@ impl Subscriber { .. } = request; // Nothing went out, so there is nothing to end. - let Some(request_id) = request_id else { - return; - }; - tracing::debug!(%prefix, "unsubscribe_namespace"); - - if matches!(self.version, Version::Draft14 | Version::Draft15) { - let msg = ietf::UnsubscribeNamespace { prefix, request_id }; - let written = async { - writer.varint(ietf::UnsubscribeNamespace::ID).await?; - writer.encode(&msg).await - }; - if let Err(err) = written.await { - tracing::debug!(%err, "failed to write unsubscribe_namespace"); - } - let _ = writer.close().await; + if request_id.is_none() { return; } + tracing::debug!(%prefix, "unsubscribe_namespace"); match super::request_stream::fin_cancels(self.version) { true => { @@ -1533,7 +1460,7 @@ impl Subscriber { if !data.is_empty() { return Err(Error::WrongSize); } - return Ok(this.reject_token(stream, request::Kind::Publish, body.clone(), err)); + return Ok(this.reject_token(stream, request::Kind::Publish, err)); } msg => msg?, }; @@ -1561,7 +1488,7 @@ impl Subscriber { if !data.is_empty() { return Err(Error::WrongSize); } - return Ok(this.reject_token(stream, request::Kind::PublishNamespace, body.clone(), err)); + return Ok(this.reject_token(stream, request::Kind::PublishNamespace, err)); } msg => msg?, }; @@ -1611,12 +1538,11 @@ impl Subscriber { &self, stream: Stream, kind: request::Kind, - body: ietf::Body, err: DecodeError, ) -> MaybeSendBox<'static, ()> { let version = self.version; async move { - if let Err(err) = super::token::reject(stream, kind, body, err, version).await { + if let Err(err) = super::token::reject(stream, kind, err, version).await { tracing::debug!(%err, "token refusal failed"); } } @@ -1640,18 +1566,11 @@ impl Subscriber { /// cannot have advertised before reading our SETUP, since our SETUP is what says whether /// advertising unasked is allowed. So this is a bug in the peer, and a silent one on /// both sides if we tolerate it. - /// - /// Draft-14/15 are exempt: they have no inline NAMESPACE, so a PUBLISH_NAMESPACE - /// request is also how a peer answers our SUBSCRIBE_NAMESPACE there, and the message - /// alone does not say which it is. fn unsolicited_is_a_violation(&self, declared: Option) -> bool { if !self.solicit { return false; } - match self.version { - Version::Draft14 | Version::Draft15 => false, - _ => declared.is_some(), - } + declared.is_some() } /// Handle an incoming PUBLISH_NAMESPACE on its bidi stream. @@ -1694,7 +1613,6 @@ impl Subscriber { if let Some(error) = super::namespace::refusal(&msg.track_namespace, self.version) { return super::request_stream::reject(stream, error).await; } - let request_id = msg.request_id; let path = msg.track_namespace.to_owned(); if self.unsolicited_is_a_violation(declared) { @@ -1709,7 +1627,6 @@ impl Subscriber { tracing::debug!(%path, "dropping reflected publish_namespace"); self.write_error( &mut stream, - request_id, &Error::Unroutable, "route loops through this relay", ) @@ -1765,7 +1682,7 @@ impl Subscriber { match admitted { Ok(grant) => (Admit::Token(slot), Some(grant)), Err(refusal) => { - self.write_error(&mut stream, request_id, &refusal.error, &refusal.reason) + self.write_error(&mut stream, &refusal.error, &refusal.reason) .await?; let _ = stream.writer.close().await; return Ok(()); @@ -1783,16 +1700,15 @@ impl Subscriber { let lived = crate::auth::RequestGrant::guard(token_grant.as_ref(), async { match self.start_announce(path.clone(), advert, admit) { Ok(id) => attached = Some(id), - Err(err) => return Ok(Withdraw::Reject(request_id, err.into())), + Err(err) => return Ok(Withdraw::Reject(err.into())), } // Buffered whole before its first await, so however this is dropped from here, the // advertisement can still end on the wire. acked = true; - self.write_ok(&mut stream, request_id).await?; + self.write_ok(&mut stream).await?; // An endpoint updates an advertisement with REQUEST_UPDATE on the stream that - // already carries it, so keep reading until the stream ends: a close on - // draft-17+, or v14-16's PublishNamespaceDone (see `terminal_publish_namespace`). + // already carries it, so keep reading until the stream closes. self.run_publish_namespace_updates( &mut stream, &path, @@ -1818,12 +1734,12 @@ impl Subscriber { Err(refusal) => { tracing::info!(%path, err = %refusal.error, reason = %refusal.reason, "request token grant ended"); if !acked { - self.write_error(&mut stream, request_id, &refusal.error, &refusal.reason) + self.write_error(&mut stream, &refusal.error, &refusal.reason) .await?; let _ = stream.writer.close().await; return Ok(()); } - self.cancel_announce(&mut stream, request_id, &path, &refusal.error, &refusal.reason) + self.cancel_announce(&mut stream) .await?; return Err(refusal.error); } @@ -1833,38 +1749,21 @@ impl Subscriber { Withdraw::Close => { let _ = stream.writer.close().await; } - Withdraw::Reject(id, refusal) => { - self.write_error(&mut stream, id, &refusal.error, &refusal.reason) + Withdraw::Reject(refusal) => { + self.write_error(&mut stream, &refusal.error, &refusal.reason) .await?; let _ = stream.writer.close().await; } - Withdraw::Cancel(id, refusal) => { - self.write_error(&mut stream, id, &refusal.error, &refusal.reason) + Withdraw::Cancel(refusal) => { + self.write_error(&mut stream, &refusal.error, &refusal.reason) .await?; - self.cancel_announce(&mut stream, request_id, &path, &refusal.error, &refusal.reason) + self.cancel_announce(&mut stream) .await?; } } Ok(()) } - /// Whether `type_id` retracts a PUBLISH_NAMESPACE rather than updating it. - /// - /// v14-16 carry the stream over the control stream, and the adapter delivers the - /// terminal message *before* it FINs (`Route::CloseStream`), so the withdrawal - /// arrives here as a message and only then as a close. Draft-17+ has a real stream, - /// where the close alone retracts and a terminal message on it is a violation. - /// - /// Only PUBLISH_NAMESPACE_DONE: the publisher sends that one. PUBLISH_NAMESPACE_CANCEL - /// travels the other way, so receiving it on an advertisement *we* were offered is a - /// violation, not a withdrawal. - fn terminal_publish_namespace(&self, type_id: u64) -> bool { - match self.version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => type_id == ietf::PublishNamespaceDone::ID, - _ => false, - } - } - /// Read advertisement updates off a live PUBLISH_NAMESPACE stream until it closes, and /// return how it ends, which the caller writes once the route is released. /// @@ -1917,11 +1816,9 @@ impl Subscriber { let turn = if let Some((id, body)) = renewal.next() { Turn::Message(id, body) } else { - let version = self.version; kio::wait(|waiter| { let mut cx = waiter.context(); - if !matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - && let Poll::Ready(result) = stream.writer.poll_closed(&mut cx) + if let Poll::Ready(result) = stream.writer.poll_closed(&mut cx) { return Poll::Ready(Turn::WriterClosed(result)); } @@ -1947,25 +1844,22 @@ impl Subscriber { let (type_id, body) = match turn { // A non-terminal message read while a verdict is pending waits for it. - Turn::Message(id, body) if renewal.pending() && !self.terminal_publish_namespace(id) => { + Turn::Message(id, body) if renewal.pending() => { if id == ietf::PublishNamespaceUpdate::ID { // Its request ID is spent on arrival, so one dropped with the queue stays spent. - if self.version != Version::Draft16 { - let update_id = ietf::RequestId::decode(&mut body.decoder(self.version), self.version)?; - if let Err(err) = self.spend(update_id) { - if matches!(err, Error::GoingAway) { - return Ok(Withdraw::Cancel( - update_id, - crate::auth::Refusal { - error: err, - reason: "going away".to_string(), - }, - )); - } - return Err(err); + let update_id = ietf::RequestId::decode(&mut body.decoder(self.version), self.version)?; + if let Err(err) = self.spend(update_id) { + if matches!(err, Error::GoingAway) { + return Ok(Withdraw::Cancel( + crate::auth::Refusal { + error: err, + reason: "going away".to_string(), + }, + )); } - spent.insert(update_id); + return Err(err); } + spent.insert(update_id); // Decoded on arrival too, so a session-fatal update still closes the session // when a withdrawal drops the queue. A token refusal only refuses this // announce, which waits for the update's turn. @@ -1992,8 +1886,6 @@ impl Subscriber { Err(request_update::Overflow::Request) => return Ok(Withdraw::Close), } } - // A terminal read while a verdict is pending is handled now, ahead of any queued - // update: nothing more is owed once the peer retracts. Turn::Message(id, body) => (id, body), Turn::Fin => { if super::request_stream::fin_cancels(self.version) { @@ -2017,31 +1909,22 @@ impl Subscriber { Turn::Renewal(request_update::Verdict { verdict, grant: res, - request_id: rid, }) => { // Only a token-authorized announce starts a renewal. let rg = token_grant.as_ref().expect("a renewal without a request grant"); // On accept the old grant is dropped and the deadline re-armed (REQUEST_OK). let Err(refusal) = rg.renew(verdict, res) else { - self.write_ok(stream, rid).await?; + self.write_ok(stream).await?; continue; }; - // Per drafts 16 section 9.11.1 / 18 section 10.9.1, a refused or uncovered + // Per draft-18 section 10.9.1, a refused or uncovered // renewal ends only this announce. Answer REQUEST_ERROR, then close the stream; // the session stays up and the old grant does not survive. - return Ok(Withdraw::Cancel(rid, refusal)); + return Ok(Withdraw::Cancel(refusal)); } }; - // Drafts 14-15 have no update for an advertisement, so a SUBSCRIBE_UPDATE the adapter - // routed here is refused and changes nothing. Draft-16's REQUEST_UPDATE renews its token. - if type_id == ietf::SubscribeUpdate::ID && matches!(self.version, Version::Draft14 | Version::Draft15) { - super::request_stream::refuse_update(&mut stream.writer, &body, self.version)?; - std::future::poll_fn(|cx| stream.writer.poll_flush(cx)).await?; - continue; - } - let terminal = self.terminal_publish_namespace(type_id); - if type_id != ietf::PublishNamespaceUpdate::ID && !terminal { + if type_id != ietf::PublishNamespaceUpdate::ID { // A repeated PUBLISH_NAMESPACE lands here too: a second request on the // stream is the base draft's duplicate request ID. tracing::warn!(type_id, "unexpected message on publish_namespace stream"); @@ -2050,26 +1933,14 @@ impl Subscriber { let mut data = body.decoder(self.version); - if terminal { - ietf::PublishNamespaceDone::decode_msg(&mut data, self.version)?; - if !data.is_empty() { - return Err(Error::WrongSize); - } - tracing::debug!(%path, "publish_namespace_done"); - return Ok(Withdraw::Done); - } - - // An update spends its own request ID, even one refused below. On draft-16 the - // adapter already spent it when it routed the update here, and a queued one was + // An update spends its own request ID, even one refused below. A queued one was // spent when it arrived. let update_id = ietf::RequestId::decode(&mut body.decoder(self.version), self.version)?; - if self.version != Version::Draft16 - && !spent.remove(&update_id) + if !spent.remove(&update_id) && let Err(err) = self.spend(update_id) { if matches!(err, Error::GoingAway) { return Ok(Withdraw::Cancel( - update_id, crate::auth::Refusal { error: err, reason: "going away".to_string(), @@ -2090,7 +1961,7 @@ impl Subscriber { reason: super::token::reason(&err).to_string(), error: Error::Decode(err), }; - return Ok(Withdraw::Cancel(update_id, refusal)); + return Ok(Withdraw::Cancel(refusal)); } msg => msg?, }; @@ -2156,7 +2027,7 @@ impl Subscriber { // The close is the withdrawal. if let Err(err) = applied { tracing::warn!(%path, %err, "publish_namespace update refused"); - return Ok(Withdraw::Reject(msg.request_id, err.into())); + return Ok(Withdraw::Reject(err.into())); } // A REQUEST_UPDATE carrying a fresh token refreshes the announce's request grant @@ -2167,7 +2038,7 @@ impl Subscriber { if let Some(token) = msg.authorization_token { match &token_grant { Some(grant) => { - renewal.verify(grant.verify_renewal(&self.auth, token.0), msg.request_id); + renewal.verify(grant.verify_renewal(&self.auth, token.0)); continue; } // Where tokens decide, a token on an announce admitted without one is refused @@ -2177,14 +2048,14 @@ impl Subscriber { error: Error::Unsupported, reason: "AUTHORIZATION TOKEN on an announce admitted without one".to_string(), }; - return Ok(Withdraw::Cancel(msg.request_id, refusal)); + return Ok(Withdraw::Cancel(refusal)); } None => {} } } // No token rides this update: acknowledge it now. - self.write_ok(stream, msg.request_id).await?; + self.write_ok(stream).await?; } } @@ -2197,27 +2068,11 @@ impl Subscriber { Ok(()) } - /// End an advertisement we accepted, leaving the session up: a closed stream from draft-17, - /// and PUBLISH_NAMESPACE_CANCEL before it, where the request outlives its stream. + /// End an advertisement we accepted, leaving the session up: a closed stream. async fn cancel_announce( &self, stream: &mut Stream, - request_id: RequestId, - path: &PathOwned, - err: &Error, - reason: &str, ) -> Result<(), Error> { - if matches!(self.version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - stream - .writer - .encode_message(&ietf::PublishNamespaceCancel { - track_namespace: path.clone(), - request_id, - error_code: request::to_code(err, request::Kind::PublishNamespace, self.version), - reason_phrase: reason.into(), - }) - .await?; - } // An OK cut short by the grant ending is still buffered, and goes out ahead of the FIN. std::future::poll_fn(|cx| stream.writer.poll_flush(cx)).await?; if stream.writer.finish().is_ok() { @@ -2256,7 +2111,6 @@ impl Subscriber { // before reading this are dropped by the unknown-alias path instead. self.write_publish_error( &mut stream, - msg.request_id, &Error::Unsupported, "PUBLISH is not supported", ) @@ -2284,35 +2138,14 @@ impl Subscriber { Ok(()) } - async fn write_ok(&self, stream: &mut Stream, request_id: RequestId) -> Result<(), Error> { - match self.version { - Version::Draft14 => { - stream - .writer - .encode_message(&ietf::PublishNamespaceOk { request_id }) - .await?; - } - Version::Draft15 | Version::Draft16 => { - stream - .writer - .encode_message(&ietf::RequestOk { - largest: None, - request_id: Some(request_id), - active: None, - }) - .await?; - } - _ => { - stream - .writer - .encode_message(&ietf::RequestOk { - largest: None, - request_id: None, - active: None, - }) - .await?; - } - } + async fn write_ok(&self, stream: &mut Stream) -> Result<(), Error> { + stream + .writer + .encode_message(&ietf::RequestOk { + largest: None, + active: None, + }) + .await?; Ok(()) } @@ -2320,14 +2153,12 @@ impl Subscriber { async fn write_error( &self, stream: &mut Stream, - request_id: RequestId, err: &Error, reason: &str, ) -> Result<(), Error> { super::request_stream::write_error( &mut stream.writer, request::Kind::PublishNamespace, - request_id, err, reason, self.version, @@ -2339,14 +2170,12 @@ impl Subscriber { async fn write_publish_error( &self, stream: &mut Stream, - request_id: RequestId, err: &Error, reason: &str, ) -> Result<(), Error> { super::request_stream::write_error( &mut stream.writer, request::Kind::Publish, - request_id, err, reason, self.version, @@ -2804,11 +2633,6 @@ impl Subscriber { if !matches!(lister.poll_used(waiter), Poll::Ready(Ok(()))) { return; } - if !super::subscribe_tracks::has_subscribe_tracks(self.version) { - // An empty answer would claim the peer lists nothing. - lister.abort(Error::Unsupported); - continue; - } *listening = Some(self.listen(path.to_owned(), lister.clone())); } Some(_) if lister.poll_unused(waiter).is_ready() => { @@ -2877,10 +2701,9 @@ impl Subscriber { // Demand with nobody subscribing (fetches, or a TRACK_STATUS downstream) learns the // track from TRACK_STATUS rather than a SUBSCRIBE: a finished track refuses one, and - // one made only to learn the track races the fetches it was for. Draft-17 still - // SUBSCRIBEs: its TRACK_STATUS answer cannot say whether the track is timed. + // one made only to learn the track races the fetches it was for. let mut target = match request.subscription() { - None if ietf::TrackStatusOk::describes_track(self.version) => { + None => { let Some(idle) = self .track_status(&broadcast_path, &track_name, request, &mut gate) .await @@ -2998,31 +2821,14 @@ impl Subscriber { }) .await { - Ok(stream) => super::request_stream::with_stats(stream, self.origin.stats(), self.version), + Ok(stream) => stream.with_stats(self.origin.stats()), Err(err) => { target.fail(err); return None; } }; - // Opening spends no ID. Once allocated, drafts 14-16 must receive the request - // to return its MAX_REQUEST_ID credit; the response wait cancels it instead. - let mut timeout = crate::time::Deadline::after(&self.runtime, Control::REQUEST_ID_TIMEOUT); - let request_id = kio::wait(|waiter| { - if gate.poll_denied(waiter).is_ready() { - return Poll::Ready(Err(Error::Unauthorized)); - } - validity.check(waiter)?; - self.control.poll_credit(waiter, &mut timeout) - }) - .await; - let request_id = match request_id { - Ok(id) => id, - Err(err) => { - target.fail(err); - return None; - } - }; + let request_id = self.control.next_request_id(); let messages = super::request_stream::Messages::new(self.session.clone(), self.control.client()); (request_id, stream, messages) } @@ -3071,7 +2877,7 @@ impl Subscriber { // Without an Established subscription, a relay includes the request it holds. let new_group = target .new_group() - .filter(|_| super::subscribe::has_new_group_request(self.version)); +; let written = match &held { // Resume the held subscription: its group order is the one it was made with, // since no update can change it. Its groups wait on the answer from here. @@ -3255,7 +3061,7 @@ impl Subscriber { // The publisher may already be serving before it answers. A session abort // already rejected the parked request; dropping what remains is not a second one. let aborted = self.remove_subscribe(request_id).is_none(); - self.cancel_subscribe(stream, request_id).await; + self.cancel_subscribe(stream).await; // A resumed copy goes back to lingering, unless the session took it. return resumed.filter(|_| !aborted); }; @@ -3281,7 +3087,7 @@ impl Subscriber { if let Some(idle) = resumed { let _ = idle.track.abort(err); } - self.cancel_subscribe(stream, request_id).await; + self.cancel_subscribe(stream).await; return None; } Ok(Some(accepted)) => accepted, @@ -3319,7 +3125,7 @@ impl Subscriber { let _ = idle.track.abort(err); } if cancel { - self.cancel_subscribe(stream, request_id).await; + self.cancel_subscribe(stream).await; } return None; } @@ -3429,7 +3235,7 @@ impl Subscriber { ); } else { tracing::warn!(track_alias = %alias, %err, "could not bind track alias"); - self.cancel_subscribe(stream, request_id).await; + self.cancel_subscribe(stream).await; } self.remove_subscribe(request_id); let _ = track.abort(err); @@ -3453,7 +3259,7 @@ impl Subscriber { // The NEW_GROUP_REQUEST last sent, the SUBSCRIBE's included. It stays outstanding // until the Largest Group reaches it, and joins meanwhile ask for the same group. let mut requested = new_group; - let dynamic_groups = track.info().refresh.is_some() && super::subscribe::has_new_group_request(self.version); + let dynamic_groups = track.info().refresh.is_some(); // The group the track's consumers want, `None` once nobody does. let mut wanted = None; // The subscriber priority upstream holds, and the one the aggregate now asks for. @@ -3461,17 +3267,8 @@ impl Subscriber { let mut wanted_max_age = max_age; let mut sent_priority = priority; let mut priority = priority; - // Draft-14's SUBSCRIBE_UPDATE repeats the bounds it leaves alone, and must not move the - // start. A Largest Object subscription starts just past the answer's Largest. - let start_location = largest.map_or(ietf::Location { group: 0, object: 0 }, |largest| ietf::Location { - group: largest.group, - object: largest.object.saturating_add(1), - }); - // REQUEST_UPDATEs sent and not yet answered. Draft-14 answers none, and draft-15's - // answer names only the update's own Request ID, which reaches no subscription, so - // only draft-16 on count them; the adapter routes draft-16's back by that ID. + // REQUEST_UPDATEs sent and not yet answered. let unanswered = AtomicU64::new(0); - let answered_here = !matches!(self.version, Version::Draft14 | Version::Draft15); let mut fetch_done = fetching.is_none(); // Our grant stopped covering the track: it is aborted rather than kept lingering. @@ -3556,8 +3353,7 @@ impl Subscriber { && largest_group(&track, largest).is_some_and(|largest| value > largest) && requested.is_none_or(|requested| value > requested) }); - let mut update = Update { - start_location, + let update = Update { priority: (priority != sent_priority).then_some(priority), max_age: wanted_max_age.filter(|_| wanted_max_age != sent_max_age), new_group, @@ -3567,9 +3363,6 @@ impl Subscriber { } // Draft-14's SUBSCRIBE_UPDATE cannot omit the priority, so it restates the one // in force rather than resetting it to the omission default. - if self.version == Version::Draft14 { - update.priority = Some(priority); - } Poll::Ready(End::Update(update)) }) .await; @@ -3577,7 +3370,7 @@ impl Subscriber { match end { End::Update(update) => { let (asked, new_group, asked_age) = (update.priority, update.new_group, update.max_age); - if let Err(err) = self.write_update(&mut stream.writer, request_id, update).await { + if let Err(err) = self.write_update(&mut stream.writer, update).await { tracing::debug!(%err, "failed to update the subscription"); ended = Some(Err(err.clone())); let _ = track.clone().abort(err); @@ -3592,9 +3385,7 @@ impl Subscriber { if let Some(value) = new_group { requested = Some(value); } - if answered_here { - unanswered.fetch_add(1, Ordering::Relaxed); - } + unanswered.fetch_add(1, Ordering::Relaxed); } End::Revoked => { tracing::info!(broadcast = %self.origin.absolute(broadcast_path), track = %track_name, "subscription no longer authorized"); @@ -3730,7 +3521,7 @@ impl Subscriber { { // Refusing the copy wakes its FETCH jobs too. Keep driving their cancels // while this stream waits for its FIN, even if the peer never ACKs it. - let mut cancelling = std::pin::pin!(self.cancel_subscribe(stream, request_id)); + let mut cancelling = std::pin::pin!(self.cancel_subscribe(stream)); kio::wait(|waiter| { let _ = group_fetches.poll(waiter); waiter.poll_future(cancelling.as_mut()) @@ -3890,24 +3681,18 @@ impl Subscriber { } } - /// Update an Established subscription: REQUEST_UPDATE (SUBSCRIBE_UPDATE through draft-16) - /// carrying only what changed, under a Request ID of its own. + /// Update an Established subscription: REQUEST_UPDATE carrying only what changed, under a + /// Request ID of its own. async fn write_update( &self, writer: &mut Writer, - subscription: RequestId, update: Update, ) -> Result<(), Error> { - let request_id = self.control.next_request_id(&self.runtime).await?; + let request_id = self.control.next_request_id(); writer .encode_message(&ietf::SubscribeUpdate { max_age: update.max_age, request_id, - subscription_request_id: matches!(self.version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then_some(subscription), - start_location: update.start_location, - // Open ended, as every subscription we send. - end_group: 0, subscriber_priority: update.priority.map(super::priority::to_wire), forward: None, filter: None, @@ -3924,52 +3709,26 @@ impl Subscriber { /// staying silent is what leaves the publisher serving a track nobody is reading and /// feeding an alias we already retired. /// - /// Two mechanisms, by version. Draft-14 through 16 carry requests over the control - /// stream adapter, whose virtual streams have no reset or stop of their own, so - /// UNSUBSCRIBE (draft-16 section 9.12) is the only thing the peer ever sees, and - /// draft-16 section 5.1.1 makes receiving it what frees the subscription. Draft-17 - /// removed the message, leaving the stream itself: a FIN is explicitly not a + /// There is no UNSUBSCRIBE message, only the stream itself: a FIN is explicitly not a /// cancellation (draft-19 section 3.3.2), so section 3.3.3's pair applies, an endpoint /// that has already FINed its sending direction cancels with STOP_SENDING on the /// receiving one. - async fn cancel_subscribe(&self, stream: Stream, request_id: RequestId) { - let Stream { mut writer, mut reader } = stream; - - if self.unsubscribes() - && let Err(err) = self.write_unsubscribe(&mut writer, request_id).await - { - tracing::debug!(%err, "failed to write unsubscribe"); - } + async fn cancel_subscribe(&self, stream: Stream) { + let Stream { writer, mut reader } = stream; // STOP_SENDING needs no acknowledgement, so it goes first and the wait below covers // only what we still have to deliver. reader.abort(&Error::Cancel); // Finishing alone would leave the writer's Drop free to RESET_STREAM, and a stream - // that has sent its FIN is still retransmitting: the reset would discard the - // UNSUBSCRIBE before the peer ever read it, which is the whole message. Closing - // consumes the writer, removing that fallback, and waits for the acknowledgement. + // that has sent its FIN is still retransmitting: the reset would discard what we + // already wrote. Closing consumes the writer, removing that fallback, and waits for + // the acknowledgement. if let Err(err) = writer.close().await { tracing::debug!(%err, "failed to close the subscribe stream"); } } - /// Whether this version cancels a subscription with an UNSUBSCRIBE message. - /// - /// Draft-17 removed it, leaving the stream reset as the only signal. - fn unsubscribes(&self) -> bool { - matches!(self.version, Version::Draft14 | Version::Draft15 | Version::Draft16) - } - - async fn write_unsubscribe( - &self, - writer: &mut crate::coding::Writer, - request_id: RequestId, - ) -> Result<(), Error> { - writer.encode_message(&ietf::Unsubscribe { request_id }).await?; - Ok(()) - } - /// Send the joining FETCH that follows a pre-draft-20 SUBSCRIBE, and keep reading its /// answer until the subscription ends. /// @@ -4000,20 +3759,8 @@ impl Subscriber { Stream::poll_open_request(&mut opening, this.version, &mut waiter.context()) }) .await?; - stream = super::request_stream::with_stats(stream, this.origin.stats(), this.version); - let mut timeout = crate::time::Deadline::after(&this.runtime, Control::REQUEST_ID_TIMEOUT); - let fetch_id = kio::wait(|waiter| { - validity.check(waiter)?; - if let Poll::Ready(err) = track.poll_closed(waiter) { - return Poll::Ready(Err(err)); - } - this.control.poll_credit(waiter, &mut timeout) - }) - .await?; - // On the shared control streams of drafts 14-16, later IDs must - // not overtake this header even if its subscription cancels mid-write. - let sending = matches!(this.version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then(|| this.control.sending()); + stream = stream.with_stats(this.origin.stats()); + let fetch_id = this.control.next_request_id(); progress.lock().committed = true; { let mut state = this.state.lock(); @@ -4047,17 +3794,8 @@ impl Subscriber { .await { tracing::debug!(%err, "failed to write joining FETCH"); - if matches!(this.version, Version::Draft14 | Version::Draft15) { - // A sequential-ID peer cannot accept any later request when this - // header failed before it learned the allocated ID. - this.session.close( - super::error::to_session_code(&SessionError::Internal), - "failed to send joining FETCH", - ); - } return Err(err); } - drop(sending); this.finish_joining_fetch(stream, progress).await } .await; @@ -4119,10 +3857,7 @@ impl Subscriber { }; // The request stays open while the subscription still needs its fill. From draft 18 // it may carry a GOAWAY meanwhile; before that, nothing follows the answer here. - let mut finished = matches!( - self.version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 - ); + let mut finished = false; kio::wait(|waiter| { if !finished { let mut cx = waiter.context(); @@ -4161,17 +3896,6 @@ impl Subscriber { largest: msg.largest, })) } - // The rejection reaches the track as the reason the publisher gave, so a - // subscriber can tell a broadcast that is not there from one it may not have. - ietf::SubscribeError::ID if self.version == Version::Draft14 => { - let msg = ietf::SubscribeError::decode_msg(&mut data, self.version)?; - tracing::warn!(message = ?msg, "subscribe error"); - Err(request::from_code( - msg.error_code, - request::Kind::Subscribe, - self.version, - )) - } ietf::RequestError::ID => { let msg = ietf::RequestError::decode_msg(&mut data, self.version)?; tracing::warn!(message = ?msg, "request error"); @@ -4212,23 +3936,8 @@ impl Subscriber { return None; } }; - stream = super::request_stream::with_stats(stream, self.origin.stats(), self.version); - let mut timeout = crate::time::Deadline::after(&self.runtime, Control::REQUEST_ID_TIMEOUT); - let request_id = match kio::wait(|waiter| { - if gate.poll_denied(waiter).is_ready() { - return Poll::Ready(Err(Error::Unauthorized)); - } - validity.check(waiter)?; - self.control.poll_credit(waiter, &mut timeout) - }) - .await - { - Ok(id) => id, - Err(err) => { - request.reject(err); - return None; - } - }; + stream = stream.with_stats(self.origin.stats()); + let request_id = self.control.next_request_id(); let written = async { stream @@ -4342,13 +4051,10 @@ impl Subscriber { let mut data = body.decoder(self.version); let code = match type_id { - id if id == ietf::TrackStatusOk::id(self.version) => { + id if id == ietf::TrackStatusOk::ID => { return Ok(ietf::TrackStatusOk::decode_msg(&mut data, self.version)?); } - ietf::TRACK_STATUS_ERROR_14 if self.version == Version::Draft14 => { - ietf::SubscribeError::decode_msg(&mut data, self.version)?.error_code - } - ietf::RequestError::ID if self.version != Version::Draft14 => { + ietf::RequestError::ID if true => { ietf::RequestError::decode_msg(&mut data, self.version)?.error_code } _ => return Err(Error::UnexpectedMessage), @@ -4550,7 +4256,7 @@ impl Subscriber { let (payload, status) = match datagram.body { ietf::DatagramBody::Payload(payload) => (payload, None), - ietf::DatagramBody::Status(status) if super::object::status_defined(status, self.version) => { + ietf::DatagramBody::Status(status) if super::object::status_defined(status) => { (bytes::Bytes::new(), Some(status)) } ietf::DatagramBody::Status(_) => return Err(Error::Session(SessionError::ProtocolViolation)), @@ -5078,14 +4784,14 @@ impl Subscriber { ) -> Result<(), Error> { let mut prior_group = None; let mut first = true; - while let Some(object) = decode_fetch_object(stream, self.version, std::mem::take(&mut first)).await? { + while let Some(object) = decode_fetch_object(stream, std::mem::take(&mut first)).await? { if !object.datagram && !object.subgroup_ok { tracing::warn!("subgroup ID is not supported, dropping fill"); return Err(Error::Unsupported); } // Object ID still keys off whether the wire Group ID field was present. - let group = resolve_fetch_group(self.version, prior_group, object.group)?; + let group = resolve_fetch_group(prior_group, object.group)?; if let Some(sequence) = group { prior_group = Some(sequence); } @@ -5103,14 +4809,6 @@ impl Subscriber { .filter(|(sequence, _, _)| group.is_none_or(|group| group == *sequence)) .map_or(0, |(_, next, _)| *next); let id = match (object.group.is_some(), object.object) { - (_, Some(id)) - if matches!( - self.version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 - ) => - { - id - } (true, Some(id)) => id, (false, Some(delta)) => expected .checked_sub(1) @@ -5141,21 +4839,8 @@ impl Subscriber { wire.first_id = id; producer.set_wire(wire)?; } - if self - .recv_fetch_payload( - stream, - producer, - FetchPayload { - decoded, - raw, - timescale, - id, - }, - ) - .await? != Fetched::Frame - { - return Err(Error::Unsupported); - } + self.recv_fetch_payload(stream, producer, FetchPayload { decoded, timescale, id }) + .await?; *next = id.checked_add(1).ok_or(Error::ProtocolViolation)?; } @@ -5168,38 +4853,10 @@ impl Subscriber { stream: &mut Reader, producer: &mut crate::recv::Group, payload: FetchPayload, - ) -> Result { - let FetchPayload { - decoded, - raw, - timescale, - id, - } = payload; + ) -> Result<(), Error> { + let FetchPayload { decoded, timescale, id } = payload; producer.mark_gap(id, decoded.gap)?; let size = stream.varint().await?; - if size == 0 && matches!(self.version, Version::Draft14 | Version::Draft15) { - let status = stream.varint().await?; - // A status object has no frame to hold its time, so its block keeps it. - let unframed = match status { - 0 => None, - _ => { - let properties = super::object::unframed(&raw, self.version, timescale)?; - super::object::check_status(self.version, &properties)?; - Some(properties) - } - }; - match (status, unframed) { - (0, _) => {} - (END_OF_GROUP, Some(properties)) => { - producer.set_end_object(crate::model::object::Fields { id, properties })?; - return Ok(Fetched::EndOfGroup); - } - // END_OF_TRACK is track-level, so there is nowhere to keep its properties. - (END_OF_TRACK, Some(properties)) if !properties.is_empty() => return Err(Error::Unsupported), - (END_OF_TRACK, _) => return Ok(Fetched::EndOfTrack), - _ => return Err(Error::Unsupported), - } - } super::object::check_frame(&decoded.properties, timescale)?; let timestamp = object_time(timescale, decoded.timestamp)?; let plain = id == producer.frame_count() as u64 && decoded.properties.is_empty(); @@ -5220,7 +4877,7 @@ impl Subscriber { return Err(err); } producer.restore(frame.finish()?); - Ok(Fetched::Frame) + Ok(()) } /// Serve a fill a group at a time, newest first for a descending reader, until it is @@ -5277,20 +4934,8 @@ impl Subscriber { Ok(stream) => stream, Err(err) => return request.reject(err), }; - stream = super::request_stream::with_stats(stream, self.origin.stats(), self.version); - let mut timeout = crate::time::Deadline::after(&self.runtime, Control::REQUEST_ID_TIMEOUT); - let fetch_id = match kio::wait(|waiter| { - validity.check(waiter)?; - if let Poll::Ready(err) = request.poll_closed(waiter) { - return Poll::Ready(Err(err)); - } - self.control.poll_credit(waiter, &mut timeout) - }) - .await - { - Ok(id) => id, - Err(err) => return request.reject(err), - }; + stream = stream.with_stats(self.origin.stats()); + let fetch_id = self.control.next_request_id(); // Registered before the FETCH goes out, since its stream can overtake FETCH_OK. let slot = kio::Producer::new(GroupFetch::Pending); @@ -5377,7 +5022,7 @@ impl Subscriber { SetupResponse::Cancelled(err) => { request.reject(err); drop(registered); - self.cancel_group_fetch(stream, fetch_id).await; + self.cancel_group_fetch(stream).await; return; } SetupResponse::Response(Err(err)) => { @@ -5480,12 +5125,7 @@ impl Subscriber { let reset = kio::wait(|waiter| { if open { let mut cx = std::task::Context::from_waker(waiter.waker()); - let closed = match self.version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => { - super::request_stream::poll_legacy_end(&mut stream, self.version, &mut cx) - } - _ => messages.poll_end(&mut stream.reader, &mut cx), - }; + let closed = messages.poll_end(&mut stream.reader, &mut cx); match closed { Poll::Ready(Err(err)) => return Poll::Ready(Some(err)), Poll::Ready(Ok(())) => open = false, @@ -5518,7 +5158,7 @@ impl Subscriber { .await; if abandoned { drop(registered); - self.cancel_group_fetch(stream, fetch_id).await; + self.cancel_group_fetch(stream).await; return; } // A group already written stays cached; only one still waiting for its stream is lost. @@ -5533,23 +5173,9 @@ impl Subscriber { } /// Cancel a FETCH using the negotiated draft's existing cancellation signal. - async fn cancel_group_fetch(&self, mut stream: Stream, request_id: RequestId) { + async fn cancel_group_fetch(&self, mut stream: Stream) { stream.reader.abort(&Error::Cancel); - match self.version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => { - // The adapter has no transport reset, so deliver FETCH_CANCEL without a - // subsequent writer drop discarding unacknowledged control bytes. - let res = async { - stream.writer.encode_message(&ietf::FetchCancel { request_id }).await?; - stream.writer.close().await - } - .await; - if let Err(err) = res { - tracing::debug!(%err, "failed to cancel group fetch"); - } - } - _ => stream.writer.abort(&Error::Cancel), - } + stream.writer.abort(&Error::Cancel) } /// Read the answer to a FETCH: FETCH_OK, or the publisher's refusal as an error. @@ -5563,10 +5189,6 @@ impl Subscriber { match type_id { ietf::FetchOk::ID => Ok(ietf::FetchOk::decode_msg(&mut data, self.version)?), - ietf::FetchError::ID if self.version == Version::Draft14 => { - let msg = ietf::FetchError::decode_msg(&mut data, self.version)?; - Err(request::from_code(msg.error_code, request::Kind::Fetch, self.version)) - } ietf::RequestError::ID => { let msg = ietf::RequestError::decode_msg(&mut data, self.version)?; Err(request::from_code(msg.error_code, request::Kind::Fetch, self.version)) @@ -5671,13 +5293,8 @@ impl Subscriber { let sequence = producer.info().sequence; let mut next = start; let mut prior_group = None; - let mut ended = false; let mut first = true; - while let Some(object) = decode_fetch_object(stream, self.version, std::mem::take(&mut first)).await? { - if ended { - tracing::warn!(sequence, "a group fetch continued past its end marker"); - return Err(Error::ProtocolViolation); - } + while let Some(object) = decode_fetch_object(stream, std::mem::take(&mut first)).await? { if object.datagram { tracing::debug!(sequence, "a datagram group is not fetchable"); return Err(Error::NotFetchable); @@ -5687,19 +5304,11 @@ impl Subscriber { return Err(Error::Unsupported); } - let group = resolve_fetch_group(self.version, prior_group, object.group)?; + let group = resolve_fetch_group(prior_group, object.group)?; if let Some(group) = group { prior_group = Some(group); } let id = match (object.group.is_some(), object.object) { - (_, Some(id)) - if matches!( - self.version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 - ) => - { - id - } (true, Some(id)) => id, (false, Some(delta)) => next .checked_sub(1) @@ -5716,27 +5325,9 @@ impl Subscriber { if id > next && decoded.gap.is_none_or(|gap| gap < id - next) { return Err(Error::Unsupported); } - match self - .recv_fetch_payload( - stream, - producer, - FetchPayload { - decoded, - raw, - timescale, - id, - }, - ) - .await? - { - Fetched::Frame => next = id.checked_add(1).ok_or(Error::ProtocolViolation)?, - // The status is an object of the group, so the End Location counts it. - Fetched::EndOfGroup => { - next = id.checked_add(1).ok_or(Error::ProtocolViolation)?; - ended = true; - } - Fetched::EndOfTrack => ended = true, - } + self.recv_fetch_payload(stream, producer, FetchPayload { decoded, timescale, id }) + .await?; + next = id.checked_add(1).ok_or(Error::ProtocolViolation)?; if end.is_some_and(|end| next > end) { tracing::warn!(sequence, ?end, "a group fetch continued past FETCH_OK's End Location"); return Err(Error::ProtocolViolation); @@ -5760,20 +5351,8 @@ impl Subscriber { } } -/// What a fetch object carried. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Fetched { - Frame, - /// A draft-14 or 15 END_OF_GROUP status object. - EndOfGroup, - /// A draft-14 or 15 END_OF_TRACK marker. - EndOfTrack, -} - struct FetchPayload { decoded: super::object::Decoded, - /// The property list `decoded` was read from, for a status object, which has no frame. - raw: bytes::Bytes, timescale: Option, id: u64, } @@ -5795,26 +5374,8 @@ struct FetchedObject { /// the prior object is a protocol violation. async fn decode_fetch_object( stream: &mut Reader, - version: Version, first: bool, ) -> Result, Error> { - if version == Version::Draft14 { - let Some(group) = stream.varint_maybe().await? else { - return Ok(None); - }; - let subgroup = stream.varint().await?; - let object = stream.varint().await?; - let _priority = stream.read_exact(1).await?; - let ObjectExtensionsLength(size) = stream.decode().await?; - let properties = stream.read_exact(size).await?.to_vec(); - return Ok(Some(FetchedObject { - group: Some(group), - object: Some(object), - datagram: false, - subgroup_ok: subgroup == 0, - properties: Some(properties), - })); - } Ok(match stream.decode_maybe::().await? { None => None, @@ -5856,20 +5417,15 @@ async fn decode_fetch_object( /// The first object is always absolute. Draft-14 through draft-17 keep sending the /// absolute ID when the field is present; draft-18 and later send a Group ID Delta, /// resolved here in the ascending order this FETCH requested. -fn resolve_fetch_group(version: Version, prior: Option, wire: Option) -> Result, Error> { +fn resolve_fetch_group(prior: Option, wire: Option) -> Result, Error> { let Some(wire) = wire else { return Ok(None); }; let Some(prior) = prior else { return Ok(Some(wire)); }; - match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 => Ok(Some(wire)), - _ => { - let step = wire.checked_add(1).ok_or(Error::Unsupported)?; - prior.checked_add(step).map(Some).ok_or(Error::Unsupported) - } - } + let step = wire.checked_add(1).ok_or(Error::Unsupported)?; + prior.checked_add(step).map(Some).ok_or(Error::Unsupported) } fn open_fill_group( @@ -6010,7 +5566,7 @@ impl GroupIngest { } // A status object has no frame to hold its time, so its block keeps it. self.properties = super::object::unframed(&self.raw, self.version, self.timescale)?; - super::object::check_status(self.version, &self.properties)?; + super::object::check_status(&self.properties)?; if status == END_OF_GROUP { group.mark_gap(self.prior_object.expect("properties follow Object ID"), self.gap)?; group.set_end_object(crate::model::object::Fields { @@ -6124,7 +5680,7 @@ mod tests { let mut session = ScriptedSession::new(wire); let (_, recv) = session.open_bi(Default::default()).await.unwrap(); let mut reader = Reader::new(crate::transport::boxed::RecvStream::new(recv), VERSION); - let result = decode_fetch_object(&mut reader, VERSION, true) + let result = decode_fetch_object(&mut reader, true) .now_or_never() .expect("refused at the prefix, not parked on the body"); assert!( @@ -6142,8 +5698,7 @@ mod tests { .varint(ietf::PublishDone::ID) .unwrap(); ietf::PublishDone { - request_id: None, - status_code: ietf::PublishDoneStatus::TrackEnded.code(Version::Draft19), + status_code: ietf::PublishDoneStatus::TrackEnded.code(), stream_count: 0, reason_phrase: "done".into(), } @@ -6319,7 +5874,7 @@ mod tests { /// nothing there is enforceable: our own publisher advertises exactly this way. #[moq_clock::test] async fn a_legacy_announce_is_never_a_violation() { - for version in [Version::Draft14, Version::Draft15] { + for version in [Version::Draft15] { assert!( !unsolicited_is_a_violation(Some(true), version), "{version:?} answers a subscription this way" @@ -6353,7 +5908,6 @@ mod tests { peer_setup, crate::Hop::new(1).unwrap(), None, - Version::Draft16, tasks, Default::default(), ); @@ -6393,7 +5947,6 @@ mod tests { writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active: None, }) .await @@ -6496,7 +6049,6 @@ mod tests { writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active: None, }) .await @@ -6587,7 +6139,6 @@ mod tests { writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active: None, }) .await @@ -6683,7 +6234,6 @@ mod tests { writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active: count, }) .await @@ -6743,7 +6293,6 @@ mod tests { writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active, }) .await @@ -6927,7 +6476,7 @@ mod tests { .expect_err("a retired alias resolves to a cancellation"); assert_eq!( - crate::ietf::error::to_stream_code(&crate::StreamError::from(&err), Version::Draft20), + crate::ietf::error::to_stream_code(&crate::StreamError::from(&err)), crate::ietf::error::CANCELLED, "the code the dispatch loop maps this error onto", ); @@ -7045,7 +6594,6 @@ mod tests { &mut crate::coding::Encoder::new(&mut properties, Version::Draft19.into()), timestamp, Timescale::MILLI, - Version::Draft19, ) .unwrap(); ietf::ObjectDatagram { @@ -7113,7 +6661,6 @@ mod tests { &mut crate::coding::Encoder::new(&mut properties, Version::Draft19.into()), crate::Timestamp::new(96_000, Timescale::MILLI).unwrap(), Timescale::MILLI, - Version::Draft19, ) .unwrap(); ietf::ObjectDatagram { @@ -7175,7 +6722,7 @@ mod tests { /// turns a routine unsubscribe into an endless "unknown track alias" stream. #[moq_clock::test] async fn cancelling_a_subscription_stops_the_publisher() { - for version in [Version::Draft16, Version::Draft20] { + for version in [Version::Draft20] { let log = cancel_a_subscription(version).await; // CANCELLED, not the moq-lite cancel code: 0 on this wire is INTERNAL_ERROR, so a // routine unsubscribe would read to the publisher as a fault on our side. @@ -7230,7 +6777,6 @@ mod tests { writer.varint(ietf::RequestError::ID).await.unwrap(); writer .encode(&ietf::RequestError { - request_id: Some(RequestId(1)), // DOES_NOT_EXIST, draft-16 section 13.4.2. error_code: 0x10, reason_phrase: "not found".into(), @@ -7483,7 +7029,7 @@ mod tests { /// at once, not after the ten-second give-up. #[moq_clock::test] async fn a_revocation_never_waits_behind_subscribe_credit() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { // MAX_REQUEST_ID admits nothing, so the SUBSCRIBE waits for credit. let session = crate::lite::test_transport::ScriptedSession::new(Vec::new()); let log = session.log.clone(); @@ -7547,7 +7093,7 @@ mod tests { /// cancels the subscribe at once, not once the peer answers or acknowledges the cancel. #[moq_clock::test] async fn a_revocation_never_waits_behind_subscribe_ok() { - for version in [Version::Draft14, Version::Draft16, Version::Draft18, Version::Draft22] { + for version in [Version::Draft18, Version::Draft22] { // A peer that accepts the stream and then says nothing at all, not even // acknowledging the cancellation's FIN. let session = crate::lite::test_transport::ScriptedSession::new(Vec::new()).with_unacked_fin(); @@ -7849,29 +7395,29 @@ mod tests { setup_wait_test!( invalid_subscribe_open_is_refused, SubscribeOpen, - [Draft14, Draft16, Draft17, Draft22] + [Draft22] ); setup_wait_test!( invalid_status_open_is_refused, StatusOpen, - [Draft14, Draft16, Draft17, Draft22] + [Draft22] ); - setup_wait_test!(invalid_status_credit_is_refused, StatusCredit, [Draft14, Draft16]); + setup_wait_test!(invalid_status_credit_is_refused, StatusCredit, [Draft16]); setup_wait_test!( invalid_status_response_is_refused, StatusResponse, - [Draft14, Draft16, Draft17, Draft22] + [Draft22] ); setup_wait_test!( invalid_fetch_open_is_refused, FetchOpen, - [Draft14, Draft16, Draft17, Draft22] + [Draft22] ); - setup_wait_test!(invalid_fetch_credit_is_refused, FetchCredit, [Draft14, Draft16]); + setup_wait_test!(invalid_fetch_credit_is_refused, FetchCredit, [Draft16]); setup_wait_test!( invalid_fetch_response_is_refused, FetchResponse, - [Draft14, Draft16, Draft17, Draft22] + [Draft22] ); #[derive(Clone, Copy, Debug)] @@ -7886,7 +7432,7 @@ mod tests { /// then cancel it if the draft can. Skipping it would leak the credit for good. #[moq_clock::test] async fn a_retirement_after_allocation_still_sends_the_request() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { retirement_after_allocation(version).await; } } @@ -8103,7 +7649,6 @@ mod tests { let subscribe_ok = message( ietf::SubscribeOk::ID, &ietf::SubscribeOk { - request_id: Some(RequestId(0)), track_alias: 7, largest: Some(ietf::Location { group: 5, object: 2 }), properties: Default::default(), @@ -8179,7 +7724,6 @@ mod tests { let subscribe_ok = message( ietf::SubscribeOk::ID, &ietf::SubscribeOk { - request_id: Some(RequestId(0)), track_alias: 7, largest: None, properties: Default::default(), @@ -8269,7 +7813,7 @@ mod tests { /// fallback, so a reset here means the cancellation never landed. #[moq_clock::test] async fn cancelling_does_not_reset_away_the_unsubscribe() { - for version in [Version::Draft16, Version::Draft20] { + for version in [Version::Draft20] { let log = cancel_a_subscription(version).await; assert!( log.resets().is_empty(), @@ -8291,10 +7835,6 @@ mod tests { writer.varint(ietf::SubscribeOk::ID).await.unwrap(); writer .encode(&ietf::SubscribeOk { - request_id: match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(RequestId(0)), - _ => None, - }, track_alias: 7, largest: None, properties: Default::default(), @@ -8402,7 +7942,6 @@ mod tests { writer.varint(ietf::SubscribeOk::ID).await.unwrap(); writer .encode(&ietf::SubscribeOk { - request_id: None, track_alias: 7, largest, properties: ietf::Properties { @@ -8527,7 +8066,6 @@ mod tests { message( ietf::RequestError::ID, &ietf::RequestError { - request_id: None, error_code: u32::MAX.into(), reason_phrase: "TRACK_STATUS is refused".into(), retry_interval: 0, @@ -8538,7 +8076,6 @@ mod tests { message( ietf::SubscribeOk::ID, &ietf::SubscribeOk { - request_id: None, track_alias: 7, largest: None, properties: Default::default(), @@ -8602,38 +8139,21 @@ mod tests { #[moq_clock::test] async fn a_refused_track_status_never_subscribes() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, Version::Draft18, Version::Draft19, Version::Draft22, ] { for code in [3, u64::from(u32::MAX)] { for fetching in [false, true] { - let refused = if version == Version::Draft14 { - message( - ietf::TRACK_STATUS_ERROR_14, - &ietf::SubscribeError { - request_id: RequestId(0), - error_code: code, - reason_phrase: "TRACK_STATUS is refused".into(), - }, - version, - ) - } else { - message( + let refused = message( ietf::RequestError::ID, &ietf::RequestError { - request_id: matches!(version, Version::Draft15 | Version::Draft16) - .then_some(RequestId(0)), error_code: code, reason_phrase: "TRACK_STATUS is refused".into(), retry_interval: 0, }, version, - ) - }; + ); let session = crate::lite::test_transport::ScriptedSession::new(refused); let log = session.log.clone(); let (tasks, _task_set) = crate::util::TaskSet::new(); @@ -8700,7 +8220,6 @@ mod tests { let status_ok = message( ietf::TrackStatusOk::ID, &ietf::TrackStatusOk { - request_id: None, largest: None, properties: declares(true), }, @@ -8713,7 +8232,6 @@ mod tests { message( ietf::SubscribeOk::ID, &ietf::SubscribeOk { - request_id: None, track_alias: 7, largest: None, properties: declares(!opts_out), @@ -8850,7 +8368,6 @@ mod tests { peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), None, - Version::Draft14, tasks, Default::default(), ); @@ -8908,7 +8425,6 @@ mod tests { peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), None, - Version::Draft14, tasks, Default::default(), ); @@ -8969,7 +8485,6 @@ mod tests { peer::PeerSetup::default(), self_origin, None, - Version::Draft14, tasks, Default::default(), ); @@ -9009,7 +8524,6 @@ mod tests { peer::PeerSetup::default(), self_origin, None, - Version::Draft14, tasks, Default::default(), ); @@ -9242,7 +8756,6 @@ mod tests { writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active: None, }) .await @@ -9340,7 +8853,7 @@ mod tests { #[moq_clock::test] async fn publish_namespace_requester_fin_keeps_the_route() { - for version in [Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft22] { + for version in [Version::Draft18, Version::Draft19, Version::Draft22] { for reset in [false, true] { let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let consumer = origin.consume(); @@ -9457,14 +8970,11 @@ mod tests { /// Draft 15 still owes the update an answer: a REQUEST_ERROR naming it. #[moq_clock::test] async fn a_legacy_update_on_an_advertisement_is_answered() { - for version in [Version::Draft14, Version::Draft15] { + for version in [Version::Draft15] { let path = crate::Path::new("room/host").to_owned(); // Own Request ID 10, aimed at the advertisement (0). Draft 14 carries its fixed // start, end group, priority and forward before the parameters. - let body: &[u8] = match version { - Version::Draft14 => &[10, 0, 0, 0, 0, 128, 1, 0], - _ => &[10, 0, 0], - }; + let body: &[u8] = &[10, 0, 0]; let mut script = vec![ietf::SubscribeUpdate::ID as u8, 0, body.len() as u8]; script.extend_from_slice(body); let log = crate::lite::test_transport::Log::default(); @@ -9522,10 +9032,7 @@ mod tests { sent.push((id, rest.get(2).copied())); rest = &rest[2 + len..]; } - let expected = match version { - Version::Draft14 => vec![(ietf::PublishNamespaceOk::ID, Some(0))], - _ => vec![(ietf::RequestOk::ID, Some(0)), (ietf::RequestError::ID, Some(10))], - }; + let expected = vec![(ietf::RequestOk::ID, Some(0)), (ietf::RequestError::ID, Some(10))]; assert_eq!(sent, expected, "{version}"); assert!(session.log.closes().is_empty(), "{version}: the session stays up"); } @@ -10625,7 +10132,7 @@ mod tests { /// into REQUEST_ERROR, so both shapes have to carry NOT_SUPPORTED. #[moq_clock::test] async fn publish_is_rejected_without_announcing() { - for version in [Version::Draft14, Version::Draft19] { + for version in [Version::Draft19] { // An open gate, so the rejection actually reaches the wire. let gate = kio::Producer::new(true); let session = crate::lite::test_transport::SinkSession::gated_bi(gate.consume()); @@ -10684,31 +10191,15 @@ mod tests { version, ); - match version { - Version::Draft14 => { - writer.varint(ietf::PublishError::ID).await.unwrap(); - writer - .encode(&ietf::PublishError { - request_id: RequestId(1), - error_code: NOT_SUPPORTED, - reason_phrase: "PUBLISH is not supported".into(), - }) - .await - .unwrap(); - } - _ => { - writer.varint(ietf::RequestError::ID).await.unwrap(); - writer - .encode(&ietf::RequestError { - request_id: None, - error_code: NOT_SUPPORTED, - reason_phrase: "PUBLISH is not supported".into(), - retry_interval: 0, - }) - .await - .unwrap(); - } - } + writer.varint(ietf::RequestError::ID).await.unwrap(); + writer + .encode(&ietf::RequestError { + error_code: NOT_SUPPORTED, + reason_phrase: "PUBLISH is not supported".into(), + retry_interval: 0, + }) + .await + .unwrap(); log.writes.lock().unwrap().clone() }; @@ -10838,7 +10329,7 @@ mod tests { ); let msg = ietf::PublishNamespaceUpdate { request_id: RequestId(3), - namespace_request_id: (version == Version::Draft16).then_some(RequestId(1)), + namespace_request_id: (false).then_some(RequestId(1)), hops: None, cost: None, authorization_token: Some(announce_token()), @@ -10912,10 +10403,6 @@ mod tests { #[moq_clock::test] async fn a_revoked_token_retracts_an_announce_whose_ok_is_blocked() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft22, ] { @@ -10988,11 +10475,7 @@ mod tests { let len = usize::from(u16::from_be_bytes([rest[0], rest[1]])); rest = &rest[2 + len..]; } - let expected = match version { - Version::Draft14 => vec![ietf::PublishNamespaceOk::ID, ietf::PublishNamespaceCancel::ID], - Version::Draft15 | Version::Draft16 => vec![ietf::RequestOk::ID, ietf::PublishNamespaceCancel::ID], - _ => vec![ietf::RequestOk::ID], - }; + let expected = vec![ietf::RequestOk::ID]; assert_eq!(sent, expected, "{version}"); assert!(session.log.closes().is_empty(), "{version}: the session stays up"); } @@ -11268,7 +10751,6 @@ mod tests { ); writer.varint(ietf::SubscribeOk::ID).await.unwrap(); let ok = ietf::SubscribeOk { - request_id: None, track_alias: 7, largest: None, properties: Default::default(), @@ -11355,7 +10837,7 @@ mod tests { /// re-arms the deadline. #[moq_clock::test] async fn a_publish_namespace_renewal_extends_past_the_old_expiry() { - for version in [Version::Draft16, Version::Draft18] { + for version in [Version::Draft18] { let (mut subscriber, mut requests, _cred, consumer, session) = auth_announce_harness(version, publish_namespace_update_with_token(version).await); let rt = subscriber.runtime.clone(); @@ -11536,12 +11018,12 @@ mod tests { assert!(session.log.closes().is_empty(), "the session stays up"); } - /// Per drafts 16 section 9.11.1 / 18 section 10.9.1, a refused PUBLISH_NAMESPACE + /// Per draft-18 section 10.9.1, a refused PUBLISH_NAMESPACE /// renewal ends only that announce at once, by closing its stream (and cancelling it on /// draft-16), not by waiting for the old 60s grant to lapse. The session stays up. #[moq_clock::test] async fn a_refused_publish_namespace_renewal_ends_the_announce() { - for version in [Version::Draft16, Version::Draft18] { + for version in [Version::Draft18] { let (mut subscriber, mut requests, _cred, _consumer, session) = auth_announce_harness(version, publish_namespace_update_with_token(version).await); let rt = subscriber.runtime.clone(); @@ -11597,36 +11079,6 @@ mod tests { session.log.closes() ); // Draft-16's request outlives its stream, so the refusal cancels it explicitly. - if version == Version::Draft16 { - let cancel = { - let log = crate::lite::test_transport::Log::default(); - let mut writer = crate::coding::Writer::new( - crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new( - log.clone(), - )), - version, - ); - writer - .encode_message(&ietf::PublishNamespaceCancel { - track_namespace: crate::Path::new("room/alice"), - request_id: RequestId(1), - error_code: request::to_code( - &Error::Unauthorized, - request::Kind::PublishNamespace, - version, - ), - reason_phrase: "no".into(), - }) - .await - .unwrap(); - log.writes.lock().unwrap().clone() - }; - let writes = session.log.writes.lock().unwrap().clone(); - assert!( - writes.windows(cancel.len()).any(|window| window == cancel.as_slice()), - "a refused draft-16 renewal sends PUBLISH_NAMESPACE_CANCEL" - ); - } } } @@ -11634,7 +11086,7 @@ mod tests { /// without one is refused rather than taken unread: it ends only that announce, unverified. #[moq_clock::test] async fn a_token_on_an_announce_admitted_without_one_is_refused() { - for version in [Version::Draft16, Version::Draft18] { + for version in [Version::Draft18] { let (mut subscriber, mut requests, _cred, consumer, session) = auth_announce_harness(version, publish_namespace_update_with_token(version).await); let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) @@ -12559,8 +12011,6 @@ mod tests { /// What an update to an Established subscription changes. struct Update { - /// Draft-14's mandatory Start Location, repeated as it stands; unused on later drafts. - start_location: ietf::Location, /// The aggregate subscriber priority, in the model's higher-first terms. priority: Option, /// Subscriber age budget, in milliseconds. @@ -12735,10 +12185,6 @@ mod filter_tests { } const JOINING_DRAFTS: [Version; 6] = [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, ]; @@ -12823,7 +12269,7 @@ mod stitch_tests { let payload = payload.as_ref(); let mut properties = Vec::new(); let w = &mut Encoder::new(&mut properties, VERSION.into()); - ietf::encode_object_time(w, timestamp(object_index), Timescale::MICRO, VERSION).unwrap(); + ietf::encode_object_time(w, timestamp(object_index), Timescale::MICRO).unwrap(); let properties = Some(properties); // The first object of the stream carries the absolute Group ID. From @@ -12892,7 +12338,6 @@ mod stitch_tests { &mut Encoder::new(&mut ext, VERSION.into()), timestamp(object), Timescale::MICRO, - VERSION, ) .unwrap(); let w = &mut crate::coding::Encoder::new(&mut buf, VERSION.into()); @@ -12944,7 +12389,6 @@ mod stitch_tests { &mut Encoder::new(&mut ext, version.into()), timestamp(object), Timescale::MICRO, - version, ) .unwrap(); crate::coding::Encoder::new(&mut buf, version.into()) @@ -13373,7 +12817,7 @@ mod stitch_tests { #[moq_clock::test] async fn passthrough_preserves_object_properties_through_dispatch() { - for version in [Version::Draft15, Version::Draft22] { + for version in [Version::Draft22] { let mut script = Vec::new(); ietf::GroupHeader { track_alias: ALIAS, @@ -13387,10 +12831,7 @@ mod stitch_tests { } .encode(&mut Encoder::new(&mut script, version.into()), version) .unwrap(); - let properties = match version { - Version::Draft15 => vec![2, 7, 5, 1, 42], - _ => vec![2, 7, 3, 1, 42], - }; + let properties = vec![2, 7, 3, 1, 42]; let mut encoder = Encoder::new(&mut script, version.into()); encoder.varint(0u64).unwrap(); encoder.bytes(&properties).unwrap(); @@ -13470,7 +12911,7 @@ mod stitch_tests { #[moq_clock::test] async fn passthrough_fetch_requires_marked_gaps_and_preserves_fields() { - for version in [Version::Draft15, Version::Draft17, Version::Draft22] { + for version in [Version::Draft22] { for marked in [false, true] { let properties = if marked { vec![0x3e, 6] } else { vec![] }; let second = if marked { vec![0x3e, 1] } else { vec![] }; @@ -13529,15 +12970,11 @@ mod stitch_tests { #[moq_clock::test] async fn passthrough_fetch_keeps_the_end_of_group_object() { for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft22, ] { // Only draft-14 lets a status object carry properties, so later drafts cannot mark // a gap before it. - let legacy = version == Version::Draft14; + let legacy = false; let terminal = if legacy { (11, &[0x3e, 2][..]) } else { (9, &[][..]) }; let objects = [ (6, &[0x3e, 6][..], None), @@ -13550,7 +12987,7 @@ mod stitch_tests { let mut decoder = Decoder::new(&bytes, version.into()); assert_eq!(decoder.varint().unwrap(), ietf::FetchOk::ID, "{version}"); let ok = ietf::FetchOk::decode(&mut decoder, version).unwrap(); - let carried = matches!(version, Version::Draft14 | Version::Draft15); + let carried = false; let last = match () { _ if carried => terminal.0 + 1, _ if Filter::is_draft20(version) => 8, @@ -13601,7 +13038,7 @@ mod stitch_tests { /// refused on ingest, and one relayed from draft-14 leaves without them. #[moq_clock::test] async fn passthrough_keeps_properties_off_status_objects() { - for version in [Version::Draft15, Version::Draft22] { + for version in [Version::Draft22] { for status in [END_OF_GROUP, END_OF_TRACK] { let script = object_stream(version, true, &[(0, &[], None), (1, &[0x20, 7], Some(status))]); let mut h = Harness::with_timescale(Fill::Done, vec![script], None); @@ -13640,7 +13077,7 @@ mod stitch_tests { true, &[(0, &[], None), (1, &[0x20, 7], Some(END_OF_GROUP))], ); - let (bytes, log) = relay_passthrough_stream(script, Version::Draft14, Version::Draft15).await; + let (bytes, log) = relay_passthrough_stream(script, Version::Draft15).await; assert!(log.resets().is_empty()); let mut decoder = Decoder::new(&bytes, Version::Draft15.into()); ietf::GroupHeader::decode(&mut decoder, Version::Draft15).unwrap(); @@ -13808,7 +13245,7 @@ mod stitch_tests { #[moq_clock::test] async fn passthrough_fetch_ingest_keeps_marked_gaps_and_stitches_its_tail() { - for version in [Version::Draft15, Version::Draft17, Version::Draft22] { + for version in [Version::Draft22] { let mut objects = Vec::new(); for (index, id) in [6u64, 8].into_iter().enumerate() { let properties = vec![0x3e, if index == 0 { 6 } else { 1 }]; @@ -13882,7 +13319,7 @@ mod stitch_tests { #[moq_clock::test] async fn passthrough_preserves_ids_first_object_and_end_status() { - for version in [Version::Draft15, Version::Draft22] { + for version in [Version::Draft22] { for first in [false, true] { let objects = [ (6, &[][..], None), @@ -13894,7 +13331,7 @@ mod stitch_tests { assert!(log.resets().is_empty()); let mut decoder = Decoder::new(&bytes, version.into()); let header = ietf::GroupHeader::decode(&mut decoder, version).unwrap(); - assert_eq!(header.flags.first_object, version == Version::Draft15 || first); + assert_eq!(header.flags.first_object, first); for (delta, status) in [(6, None), (1, None), (2, Some(END_OF_GROUP))] { assert_eq!(decoder.varint().unwrap(), delta); assert!(decoder.bytes().unwrap().is_empty()); @@ -13913,9 +13350,6 @@ mod stitch_tests { #[moq_clock::test] async fn passthrough_preserves_immutable_bytes_and_refuses_incompatible_drafts() { for ingress in [ - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft22, ] { @@ -13933,9 +13367,6 @@ mod stitch_tests { encoder.varint(0x0bu64).unwrap(); encoder.bytes(&inner).unwrap(); for egress in [ - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft22, ] { @@ -13992,120 +13423,114 @@ mod stitch_tests { #[moq_clock::test] async fn passthrough_preserves_datagram_objects_through_dispatch() { // Also through a second relay behind a moq-lite hop with the `ietf` extension. - for (version, ranged, hop) in [Version::Draft15, Version::Draft22] + for (version, ranged, hop) in [Version::Draft22] .into_iter() .flat_map(|version| [(version, false), (version, true)]) .filter(|(version, ranged)| !*ranged || *version == Version::Draft22) .flat_map(|(version, ranged)| [(version, ranged, false), (version, ranged, true)]) { - { - let origin = crate::origin::Config::new(crate::Hop::new(2).unwrap()).produce(); - let broadcast = origin.publish("room", crate::origin::Route::default()).unwrap(); - let track = broadcast - .create_track("video", accepted_info(&Default::default(), version)) - .unwrap(); - let mut h = Harness::with_timescale(Fill::Done, vec![], None); - h.subscriber.version = version; - h.subscriber.state.lock().subscribes.get_mut(&REQUEST).unwrap().producer = Some(track.clone()); - let properties = if version == Version::Draft15 { - vec![2, 7, 5, 1, 42] + let origin = crate::origin::Config::new(crate::Hop::new(2).unwrap()).produce(); + let broadcast = origin.publish("room", crate::origin::Route::default()).unwrap(); + let track = broadcast + .create_track("video", accepted_info(&Default::default(), version)) + .unwrap(); + let mut h = Harness::with_timescale(Fill::Done, vec![], None); + h.subscriber.version = version; + h.subscriber.state.lock().subscribes.get_mut(&REQUEST).unwrap().producer = Some(track.clone()); + let properties = vec![2, 7, 3, 1, 42]; + let mut original = Vec::new(); + for id in [6, 7, 8] { + let datagram = ietf::ObjectDatagram { + track_alias: ALIAS, + group_id: SEQUENCE, + object_id: Some(id), + // An omitted priority inherits the track default, which the + // downstream response need not carry. + publisher_priority: (id != 6).then_some(200), + end_of_group: false, + properties: (id != 8).then(|| properties.clone()), + body: if id == 8 { + ietf::DatagramBody::Status(END_OF_GROUP) } else { - vec![2, 7, 3, 1, 42] - }; - let mut original = Vec::new(); - for id in [6, 7, 8] { - let datagram = ietf::ObjectDatagram { - track_alias: ALIAS, - group_id: SEQUENCE, - object_id: Some(id), - // An omitted priority inherits the track default, which the - // downstream response need not carry. - publisher_priority: (id != 6).then_some(200), - end_of_group: false, - properties: (id != 8).then(|| properties.clone()), - body: if id == 8 { - ietf::DatagramBody::Status(END_OF_GROUP) - } else { - ietf::DatagramBody::Payload(bytes::Bytes::from_static(b"object")) - }, - }; - h.subscriber - .recv_datagram(datagram.encode_bytes(version).unwrap()) - .unwrap(); - original.push(datagram); - } - track.finish().unwrap(); - let (served, _sessions) = match hop { - true => { - let (served, sessions) = crate::lite::ietf::test_hop(&origin, "room", [true, true]).await; - (served, Some(sessions)) - } - false => (origin, None), - }; - let downstream = ScriptedSession::per_stream(vec![vec![]]).with_datagrams(65535); - let peer = peer::PeerSetup::default(); - peer.set(peer::Peer::default()); - let publisher = crate::ietf::Publisher::new( - crate::time::sim(), - crate::driver::Transport::new(downstream.clone()), - served.consume(), - Control::new(None, false), - None, - peer, - version, - ); - let subscribe = ietf::Subscribe { - max_age: None, - new_group: None, - request_id: RequestId(3), - track_namespace: Path::new("room"), - track_name: "video".into(), - subscriber_priority: 128, - group_order: ietf::GroupOrder::Ascending, - filter: if ranged { - Filter::Absolute { - start: ietf::Location { - group: SEQUENCE, - object: 7, - }, - end: Some(ietf::EndLocation { - group: SEQUENCE, - object: Some(7), - }), - } - } else { - Filter::Unfiltered + ietf::DatagramBody::Payload(bytes::Bytes::from_static(b"object")) + }, + }; + h.subscriber + .recv_datagram(datagram.encode_bytes(version).unwrap()) + .unwrap(); + original.push(datagram); + } + track.finish().unwrap(); + let (served, _sessions) = match hop { + true => { + let (served, sessions) = crate::lite::ietf::test_hop(&origin, "room", [true, true]).await; + (served, Some(sessions)) + } + false => (origin, None), + }; + let downstream = ScriptedSession::per_stream(vec![vec![]]).with_datagrams(65535); + let peer = peer::PeerSetup::default(); + peer.set(peer::Peer::default()); + let publisher = crate::ietf::Publisher::new( + crate::time::sim(), + crate::driver::Transport::new(downstream.clone()), + served.consume(), + Control::new(None, false), + None, + peer, + version, + ); + let subscribe = ietf::Subscribe { + max_age: None, + new_group: None, + request_id: RequestId(3), + track_namespace: Path::new("room"), + track_name: "video".into(), + subscriber_priority: 128, + group_order: ietf::GroupOrder::Ascending, + filter: if ranged { + Filter::Absolute { + start: ietf::Location { + group: SEQUENCE, + object: 7, }, - fill: None, - properties_wanted: true, - forward: true, - rendezvous: false, - range_filters: false, - authorization_token: None, - epoch: None, - }; - let mut body = Vec::new(); - subscribe - .encode_msg(&mut Encoder::new(&mut body, version.into()), version) - .unwrap(); - let stream = Stream::open(&mut crate::driver::Transport::new(downstream.clone()), version) - .await - .unwrap(); - publisher - .handle_stream(ietf::Subscribe::ID, ietf::Body(body.into()), stream, false) - .unwrap() - .await; - let sent = downstream.log.datagrams.lock().unwrap(); - original.retain(|datagram| !ranged || datagram.object_id == Some(7)); - assert_eq!(sent.len(), original.len(), "{version} ranged={ranged} hop={hop}"); - let inherited = super::super::priority::to_wire(track.publisher_priority()); - for (bytes, mut expected) in sent.iter().zip(original) { - expected.track_alias = 3; - expected.publisher_priority = expected.publisher_priority.or(Some(inherited)); - let (actual, _) = ietf::ObjectDatagram::decode_slice(bytes, version).unwrap(); - assert_eq!(actual, expected); + end: Some(ietf::EndLocation { + group: SEQUENCE, + object: Some(7), + }), } - } + } else { + Filter::Unfiltered + }, + fill: None, + properties_wanted: true, + forward: true, + rendezvous: false, + range_filters: false, + authorization_token: None, + epoch: None, + }; + let mut body = Vec::new(); + subscribe + .encode_msg(&mut Encoder::new(&mut body, version.into()), version) + .unwrap(); + let stream = Stream::open(&mut crate::driver::Transport::new(downstream.clone()), version) + .await + .unwrap(); + publisher + .handle_stream(ietf::Subscribe::ID, ietf::Body(body.into()), stream, false) + .unwrap() + .await; + let sent = downstream.log.datagrams.lock().unwrap(); + original.retain(|datagram| !ranged || datagram.object_id == Some(7)); + assert_eq!(sent.len(), original.len(), "{version} ranged={ranged} hop={hop}"); + let inherited = super::super::priority::to_wire(track.publisher_priority()); + for (bytes, mut expected) in sent.iter().zip(original) { + expected.track_alias = 3; + expected.publisher_priority = expected.publisher_priority.or(Some(inherited)); + let (actual, _) = ietf::ObjectDatagram::decode_slice(bytes, version).unwrap(); + assert_eq!(actual, expected); + } } } @@ -14113,12 +13538,8 @@ mod stitch_tests { /// egress draft forbids them beside a non-Normal status. #[moq_clock::test] async fn passthrough_strips_status_datagram_properties_a_draft_forbids() { - for ingress in [Version::Draft14, Version::Draft15, Version::Draft16] { + for ingress in [Version::Draft15] { let egresses = [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -14200,7 +13621,7 @@ mod stitch_tests { panic!("{context}: sent {} datagrams", sent.len()); }; let (actual, _) = ietf::ObjectDatagram::decode_slice(bytes, egress).unwrap(); - let carried = matches!(egress, Version::Draft14 | Version::Draft15 | Version::Draft16); + let carried = false; assert_eq!( (actual.object_id, actual.body, actual.properties.is_some()), (Some(5), ietf::DatagramBody::Status(status), carried), @@ -14212,20 +13633,16 @@ mod stitch_tests { #[moq_clock::test] async fn passthrough_translates_mutable_property_encodings() { - for ingress in [Version::Draft14, Version::Draft15, Version::Draft17, Version::Draft22] { + for ingress in [Version::Draft22] { let mut properties = Vec::new(); let mut encoder = Encoder::new(&mut properties, ingress.into()); encoder.varint(0x80u64).unwrap(); encoder.varint(96_000u64).unwrap(); encoder - .varint(if matches!(ingress, Version::Draft14 | Version::Draft15) { - 0x81u64 - } else { - 1u64 - }) + .varint(1u64) .unwrap(); encoder.bytes(&[42; 130]).unwrap(); - for egress in [Version::Draft15, Version::Draft17, Version::Draft22] { + for egress in [Version::Draft22] { let script = object_stream(ingress, true, &[(0, &properties, None)]); let (bytes, log) = relay_passthrough_stream(script, ingress, egress).await; assert!(log.resets().is_empty()); @@ -14247,7 +13664,7 @@ mod stitch_tests { #[moq_clock::test] async fn passthrough_accepts_nonzero_first_objects_and_gaps() { - for version in [Version::Draft15, Version::Draft22] { + for version in [Version::Draft22] { let mut script = Vec::new(); ietf::GroupHeader { track_alias: ALIAS, @@ -14276,7 +13693,7 @@ mod stitch_tests { /// A datagram's object number does not change its public payload view. #[moq_clock::test] async fn passthrough_accepts_datagrams_past_object_zero() { - for version in [Version::Draft15, Version::Draft22] { + for version in [Version::Draft22] { let mut h = Harness::with_timescale(Fill::Done, vec![], None); h.subscriber.version = version; let mut consumer = h.track.subscribe(None); @@ -14318,7 +13735,7 @@ mod stitch_tests { /// Old drafts preserve a nonzero first Object ID without inventing missing frames. #[moq_clock::test] async fn old_drafts_accept_a_nonzero_first_object() { - for version in [Version::Draft14, Version::Draft16] { + for version in [Version::Draft16] { let scripts = [2u64, 0] .into_iter() .enumerate() @@ -14396,7 +13813,7 @@ mod stitch_tests { /// is subgroup 0. #[moq_clock::test] async fn a_compact_non_zero_subgroup_is_refused() { - for version in [Version::Draft14, Version::Draft22] { + for version in [Version::Draft22] { let compact = |sequence: u64, first: Option| { let mut bytes = Vec::new(); ietf::GroupHeader { @@ -14971,7 +14388,7 @@ mod stitch_tests { let payloads: [&[u8]; 5] = [b"o0", b"o1", b"o2", b"o3", b"o4"]; let bytes = end_marker(draft18_subgroup(SEQUENCE, true, 0, &payloads), END_OF_GROUP); - let flags = ietf::GroupFlags::decode(u64::from(bytes[0]), Version::Draft18).unwrap(); + let flags = ietf::GroupFlags::decode(u64::from(bytes[0])).unwrap(); assert!(flags.has_end, "the header marks the group's end"); let mut h = Harness::new(Fill::Done, vec![bytes]); @@ -15274,15 +14691,15 @@ mod stitch_tests { #[test] fn a_later_fetch_group_field_is_a_delta() { assert_eq!( - resolve_fetch_group(Version::Draft18, Some(7), Some(0)).unwrap(), + resolve_fetch_group(Some(7), Some(0)).unwrap(), Some(8) ); assert_eq!( - resolve_fetch_group(Version::Draft20, Some(7), Some(0)).unwrap(), + resolve_fetch_group(Some(7), Some(0)).unwrap(), Some(8) ); assert_eq!( - resolve_fetch_group(Version::Draft17, Some(7), Some(8)).unwrap(), + resolve_fetch_group(Some(7), Some(8)).unwrap(), Some(8) ); } @@ -15366,7 +14783,7 @@ mod stitch_tests { header(Some(SEQUENCE), Some(0), Zero, None), ]; - for version in [Version::Draft15, VERSION] { + for version in [VERSION] { for header in &headers { let mut buf = Vec::new(); header @@ -15445,7 +14862,7 @@ mod stitch_tests { fn datagram_object(sequence: u64) -> Vec { let mut properties = Vec::new(); let w = &mut Encoder::new(&mut properties, VERSION.into()); - ietf::encode_object_time(w, timestamp(0), Timescale::MICRO, VERSION).unwrap(); + ietf::encode_object_time(w, timestamp(0), Timescale::MICRO).unwrap(); let mut buf = Vec::new(); ietf::FetchObject::Object { subgroup: ietf::FetchSubgroup::Datagram, @@ -15644,10 +15061,6 @@ mod joining_fetch_tests { use futures::FutureExt; const JOINING_DRAFTS: [Version; 6] = [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, ]; @@ -15670,10 +15083,6 @@ mod joining_fetch_tests { message_bytes( ietf::SubscribeOk::ID, &ietf::SubscribeOk { - request_id: match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(RequestId(1)), - _ => None, - }, track_alias: 7, largest, properties: Default::default(), @@ -15686,10 +15095,6 @@ mod joining_fetch_tests { message_bytes( ietf::FetchOk::ID, &ietf::FetchOk { - request_id: match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(RequestId(3)), - _ => None, - }, group_order: GroupOrder::Ascending, end_of_track: false, end_location: ietf::Location { group: 4, object: 2 }, @@ -15701,29 +15106,9 @@ mod joining_fetch_tests { fn fetch_error(version: Version) -> Vec { match version { - Version::Draft14 => message_bytes( - ietf::FetchError::ID, - &ietf::FetchError { - request_id: RequestId(3), - error_code: request::NOT_SUPPORTED, - reason_phrase: "refused".into(), - }, - version, - ), - Version::Draft15 | Version::Draft16 => message_bytes( - ietf::RequestError::ID, - &ietf::RequestError { - request_id: Some(RequestId(3)), - error_code: request::NOT_SUPPORTED, - reason_phrase: "refused".into(), - retry_interval: 0, - }, - version, - ), _ => message_bytes( ietf::RequestError::ID, &ietf::RequestError { - request_id: None, error_code: request::NOT_SUPPORTED, reason_phrase: "refused".into(), retry_interval: 0, @@ -15900,7 +15285,7 @@ mod joining_fetch_tests { /// The parent keeps its child FETCH cancellation moving while its own FIN awaits ACK. async fn revoked_child_fetch(accepted: bool) { - for version in [Version::Draft14, Version::Draft16, Version::Draft17, Version::Draft22] { + for version in [Version::Draft22] { let session = ScriptedSession::per_stream(vec![ subscribe_ok(version, None), if accepted { fetch_ok(version) } else { Vec::new() }, @@ -15959,13 +15344,6 @@ mod joining_fetch_tests { let stops = log.stops().len(); // One stop belongs to SUBSCRIBE, another to the cache-miss FETCH. assert!(stops >= 2, "{version}: only {stops} streams cancelled before ACK"); - if matches!(version, Version::Draft14 | Version::Draft16) { - assert!( - decode_messages(&log, version) - .iter() - .any(|(id, _)| *id == ietf::FetchCancel::ID) - ); - } drop(received); } } @@ -16040,10 +15418,7 @@ mod joining_fetch_tests { let session = ScriptedSession::new(Vec::new()).without_credit(); let (mut subscriber, track, _tasks) = pending_join(crate::driver::Transport::new(session), version); subscriber.control = Control::new( - match version { - Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(RequestId(2)), - _ => None, - }, + None, false, ); let mut joining = subscriber.start_joining_fetch(RequestId(99), &track).unwrap(); @@ -16083,7 +15458,7 @@ mod joining_fetch_tests { /// A cancelled partial legacy header still precedes the next unrelated request ID. #[moq_clock::test] async fn cancelling_a_partial_joining_fetch_preserves_request_id_order() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { let gate = kio::Producer::new(false); let session = crate::lite::test_transport::SinkSession::gated_bi_prefix(2, gate.consume()); let log = session.log.clone(); @@ -16122,7 +15497,7 @@ mod joining_fetch_tests { /// A peer that never acknowledges cancellation cannot retain request-ID allocation. #[moq_clock::test] async fn joining_fetch_cleanup_does_not_hold_the_next_request_id() { - for version in [Version::Draft14, Version::Draft15, Version::Draft16] { + for version in [Version::Draft15] { let session = ScriptedSession::per_stream(Vec::new()).with_unacked_fin(); let log = session.log.clone(); let (subscriber, track, mut tasks) = pending_join(crate::driver::Transport::new(session.clone()), version); @@ -16257,9 +15632,7 @@ mod joining_fetch_tests { ok.extend(message_bytes( ietf::PublishDone::ID, &ietf::PublishDone { - request_id: matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then_some(RequestId(1)), - status_code: ietf::PublishDoneStatus::TrackEnded.code(version), + status_code: ietf::PublishDoneStatus::TrackEnded.code(), stream_count: 0, reason_phrase: "done".into(), }, @@ -16280,23 +15653,15 @@ mod joining_fetch_tests { let w = &mut Encoder::new(&mut objects, version.into()); w.varint(ietf::FetchHeader::TYPE).unwrap(); ietf::FetchHeader { request_id: fetch_id }.encode(w, version).unwrap(); - if version == Version::Draft14 { - w.varint(4).unwrap(); // group - w.varint(0).unwrap(); // subgroup - w.varint(0).unwrap(); // object - w.u8(0); // priority - w.bytes(&[]).unwrap(); // properties - } else { - ietf::FetchObject::Object { - subgroup: ietf::FetchSubgroup::Zero, - group: Some(4), - object: Some(0), - priority: Some(0), - properties: None, - } - .encode(w, version) - .unwrap(); + ietf::FetchObject::Object { + subgroup: ietf::FetchSubgroup::Zero, + group: Some(4), + object: Some(0), + priority: Some(0), + properties: None, } + .encode(w, version) + .unwrap(); w.bytes(b"x").unwrap(); let data = ScriptedSession::new(objects); let (_, recv) = data.clone().open_bi(Default::default()).await.unwrap(); @@ -16336,8 +15701,6 @@ mod joining_fetch_tests { ok.extend(message_bytes( ietf::PublishDone::ID, &ietf::PublishDone { - request_id: matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then_some(RequestId(1)), status_code: 0x3, stream_count: 0, reason_phrase: "done".into(), @@ -16425,8 +15788,6 @@ mod joining_fetch_tests { session.push(&message_bytes( ietf::FetchOk::ID, &ietf::FetchOk { - request_id: matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then_some(RequestId(1)), group_order: GroupOrder::Ascending, end_of_track: false, end_location: ietf::Location { @@ -16454,31 +15815,15 @@ mod joining_fetch_tests { ); } else { let mut objects = Vec::new(); - if version == Version::Draft14 { - crate::coding::Encoder::new(&mut objects, version.into()) - .varint(GROUP) - .unwrap(); - crate::coding::Encoder::new(&mut objects, version.into()) - .varint(0u64) - .unwrap(); // subgroup - crate::coding::Encoder::new(&mut objects, version.into()) - .varint(0u64) - .unwrap(); // object - crate::coding::Encoder::new(&mut objects, version.into()).u8(0u8); // priority - crate::coding::Encoder::new(&mut objects, version.into()) - .bytes(&Vec::::new()) - .unwrap(); - } else { - ietf::FetchObject::Object { - subgroup: ietf::FetchSubgroup::Zero, - group: Some(GROUP), - object: Some(0), - priority: Some(0), - properties: None, - } - .encode(&mut crate::coding::Encoder::new(&mut objects, version.into()), version) - .unwrap(); + ietf::FetchObject::Object { + subgroup: ietf::FetchSubgroup::Zero, + group: Some(GROUP), + object: Some(0), + priority: Some(0), + properties: None, } + .encode(&mut crate::coding::Encoder::new(&mut objects, version.into()), version) + .unwrap(); crate::coding::Encoder::new(&mut objects, version.into()) .varint(1u64) .unwrap(); @@ -16533,18 +15878,8 @@ mod joining_fetch_tests { assert_eq!(session.log.stops(), [crate::ietf::error::CANCELLED]); let messages = decode_messages(&session.log, version); let cancels: Vec<_> = messages.iter().filter(|(id, _)| *id == ietf::FetchCancel::ID).collect(); - if matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) { - assert_eq!(cancels.len(), 1, "legacy FETCH_CANCEL"); - let mut body = Decoder::new(&cancels[0].1, version.into()); - assert_eq!( - ietf::FetchCancel::decode_msg(&mut body, version).unwrap().request_id, - RequestId(1) - ); - assert!(session.log.resets().is_empty(), "deliver FETCH_CANCEL before closing"); - } else { - assert!(cancels.is_empty(), "FETCH_CANCEL was removed"); - assert_eq!(session.log.resets(), [crate::ietf::error::CANCELLED]); - } + assert!(cancels.is_empty(), "FETCH_CANCEL was removed"); + assert_eq!(session.log.resets(), [crate::ietf::error::CANCELLED]); assert!(subscriber.state.lock().group_fetches.is_empty(), "request retired"); } @@ -16602,7 +15937,6 @@ mod joining_fetch_tests { let ok = message_bytes( ietf::FetchOk::ID, &ietf::FetchOk { - request_id: None, group_order: GroupOrder::Ascending, end_of_track: false, end_location: ietf::Location { @@ -16671,7 +16005,6 @@ mod joining_fetch_tests { let ok = message_bytes( ietf::FetchOk::ID, &ietf::FetchOk { - request_id: None, group_order: GroupOrder::Ascending, end_of_track, end_location, @@ -16780,8 +16113,6 @@ mod joining_fetch_tests { let ok = message_bytes( ietf::FetchOk::ID, &ietf::FetchOk { - request_id: matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then_some(RequestId(1)), group_order: GroupOrder::Ascending, end_of_track: ended, end_location, @@ -16802,24 +16133,15 @@ mod joining_fetch_tests { .unwrap(); for (index, payload) in [b"c", b"d"].iter().enumerate() { let first = index == 0; - if version == Version::Draft14 { - let w = &mut crate::coding::Encoder::new(&mut objects, version.into()); - w.varint(GROUP).unwrap(); - w.varint(0u64).unwrap(); // Subgroup - w.varint(START + index as u64).unwrap(); - w.u8(0); // Priority - w.varint(0u64).unwrap(); // Properties - } else { - ietf::FetchObject::Object { - subgroup: ietf::FetchSubgroup::Zero, - group: first.then_some(GROUP), - object: first.then_some(START), - priority: first.then_some(0), - properties: None, - } - .encode(&mut crate::coding::Encoder::new(&mut objects, version.into()), version) - .unwrap(); + ietf::FetchObject::Object { + subgroup: ietf::FetchSubgroup::Zero, + group: first.then_some(GROUP), + object: first.then_some(START), + priority: first.then_some(0), + properties: None, } + .encode(&mut crate::coding::Encoder::new(&mut objects, version.into()), version) + .unwrap(); crate::coding::Encoder::new(&mut objects, version.into()) .varint(1u64) .unwrap(); @@ -17039,7 +16361,6 @@ mod new_group_tests { stream .writer .encode(&ietf::SubscribeOk { - request_id: None, track_alias: 7, largest: Some(ietf::Location { group: largest, @@ -17070,7 +16391,6 @@ mod new_group_tests { .writer .encode(&ietf::RequestOk { largest: None, - request_id: None, active: None, }) .await @@ -17171,7 +16491,6 @@ mod new_group_tests { stream .writer .encode(&ietf::RequestError { - request_id: None, error_code: 0, reason_phrase: "refused".into(), retry_interval: 0, @@ -17250,7 +16569,7 @@ mod track_property_tests { }; /// The newest draft and an older one, both with a request stream per subscription. - const VERSIONS: [Version; 2] = [Version::Draft17, Version::Draft22]; + const VERSIONS: [Version; 2] = [Version::Draft22]; /// Subscribe to one track through a relay on `version`, whose publisher answers with /// `block` as SUBSCRIBE_OK's Track Properties, returning what the subscriber got. @@ -17290,7 +16609,6 @@ mod track_property_tests { let mut body = Vec::new(); ietf::SubscribeOk { - request_id: None, track_alias: 7, largest: None, properties: Default::default(), @@ -17450,10 +16768,6 @@ mod compression_negotiation_tests { ..Default::default() }; for version in [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft22, ] { diff --git a/rs/moq-net/src/ietf/subscriber/tracks.rs b/rs/moq-net/src/ietf/subscriber/tracks.rs index 53881fc05a4..7984a903ab3 100644 --- a/rs/moq-net/src/ietf/subscriber/tracks.rs +++ b/rs/moq-net/src/ietf/subscriber/tracks.rs @@ -32,7 +32,7 @@ pub(super) type TrackKey = (PathOwned, Arc); /// status, INTERNAL_ERROR included, fails the listing, so it asks again rather than hide a /// track that may still be live. pub(super) fn listing_end(done: &ietf::PublishDone, version: Version) -> Result<(), Error> { - match done.status_code == ietf::PublishDoneStatus::TrackEnded.code(version) { + match done.status_code == ietf::PublishDoneStatus::TrackEnded.code() { true => Ok(()), false => Err(done .end(version) @@ -426,9 +426,9 @@ impl Subscriber { let Some(stream) = opened else { return Ok(()); }; - let mut stream = request_stream::with_stats(stream?, self.origin.stats(), self.version); + let mut stream = stream?.with_stats(self.origin.stats()); // No draft with SUBSCRIBE_TRACKS has MAX_REQUEST_ID, so this never waits on credit. - let request_id = kio::wait(|waiter| self.control.poll_next_request_id(waiter)).await; + let request_id = self.control.next_request_id(); stream.writer.buffer_message( ietf::SubscribeTracks::ID, &ietf::SubscribeTracks { @@ -622,8 +622,8 @@ impl Subscriber { let Some(stream) = opened else { return Ok(None); }; - let mut stream = request_stream::with_stats(stream?, self.origin.stats(), self.version); - let request_id = kio::wait(|waiter| self.control.poll_next_request_id(waiter)).await; + let mut stream = stream?.with_stats(self.origin.stats()); + let request_id = self.control.next_request_id(); self.state.lock().subscribes.insert( request_id, TrackState { @@ -674,7 +674,7 @@ impl Subscriber { Ok(Some(accepted)) => accepted, Ok(None) => { self.remove_subscribe(request_id); - self.cancel_subscribe(stream, request_id).await; + self.cancel_subscribe(stream).await; return Ok(None); } Err(err) => { @@ -684,12 +684,12 @@ impl Subscriber { }; if let Some(err) = refused(&accepted.properties, self.version, self.peer_setup.get().await.flate) { self.remove_subscribe(request_id); - self.cancel_subscribe(stream, request_id).await; + self.cancel_subscribe(stream).await; return Err(err); } if let Err(err) = self.bind_alias(request_id, accepted.alias) { self.remove_subscribe(request_id); - self.cancel_subscribe(stream, request_id).await; + self.cancel_subscribe(stream).await; return Err(err); } let info = accepted_info(&accepted.properties, self.version); @@ -709,7 +709,7 @@ impl Subscriber { }; if !listed { self.remove_subscribe(request_id); - self.cancel_subscribe(stream, request_id).await; + self.cancel_subscribe(stream).await; return Ok(None); } Ok(Some(Upstream { @@ -828,7 +828,6 @@ impl Subscriber { Offer::Refuse(code, reason) => { tracing::debug!(broadcast = %key.0, track = %key.1, reason, "refusing publish"); let error = ietf::RequestError { - request_id: None, error_code: code, reason_phrase: reason.into(), retry_interval: 0, @@ -857,7 +856,6 @@ impl Subscriber { stream.writer.buffer_message( id, &ietf::PublishOk { - request_id: None, forward: false, subscriber_priority: None, group_order: None, @@ -939,12 +937,12 @@ impl Subscriber { } }; self.remove_subscribe(upstream.request_id); - self.cancel_subscribe(upstream.stream, upstream.request_id).await; + self.cancel_subscribe(upstream.stream).await; return; } Step::Gone => { self.remove_subscribe(upstream.request_id); - self.cancel_subscribe(upstream.stream, upstream.request_id).await; + self.cancel_subscribe(upstream.stream).await; return; } Step::Message(Ok(Some(Framed(id, body)))) => match id { @@ -1074,7 +1072,7 @@ impl Subscriber { true => this.watch(key, upstream, handoff).await, false => { this.remove_subscribe(upstream.request_id); - this.cancel_subscribe(upstream.stream, upstream.request_id).await; + this.cancel_subscribe(upstream.stream).await; } } }); @@ -1136,14 +1134,11 @@ impl Subscriber { writer: &mut Writer, forward: &Forward, ) -> Result<(), Error> { - let request_id = self.control.next_request_id(&self.runtime).await?; + let request_id = self.control.next_request_id(); writer .encode_message(&ietf::SubscribeUpdate { max_age: forward.max_age, request_id, - subscription_request_id: None, - start_location: ietf::Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: forward.priority.map(super::super::priority::to_wire), forward: Some(forward.forward), filter: forward.filter, @@ -1337,7 +1332,6 @@ mod tests { assert_eq!(msg.prefix.as_str(), "bcast"); assert!(!msg.forward, "the listing asks for every track's objects"); let ok = ietf::RequestOk { - request_id: None, active: None, largest: None, }; @@ -1393,7 +1387,6 @@ mod tests { let resume: ietf::SubscribeUpdate = body.message(self.version).unwrap(); assert_eq!(resume.forward, Some(true)); let ok = ietf::RequestOk { - request_id: None, active: None, largest: None, }; @@ -1493,8 +1486,7 @@ mod tests { assert_eq!(event(&mut h.tracks).as_deref(), Some("+video")); let done = ietf::PublishDone { - request_id: None, - status_code: ietf::PublishDoneStatus::TrackEnded.code(version), + status_code: ietf::PublishDoneStatus::TrackEnded.code(), stream_count: 0, reason_phrase: "".into(), }; @@ -1518,8 +1510,7 @@ mod tests { assert_eq!(event(&mut h.tracks).as_deref(), Some("+video")); let done = ietf::PublishDone { - request_id: None, - status_code: status.code(version), + status_code: status.code(), stream_count: 0, reason_phrase: "".into(), }; @@ -1546,7 +1537,6 @@ mod tests { // SUBSCRIPTION_ENDED, which drafts 18 and 19 register. let done = ietf::PublishDone { - request_id: None, status_code: 0x3, stream_count: 0, reason_phrase: "".into(), @@ -1589,7 +1579,6 @@ mod tests { assert!(!msg.forward, "{version}: the SUBSCRIBE forwards"); let ok = ietf::SubscribeOk { - request_id: None, track_alias: 3, largest: None, properties: ietf::Properties { @@ -1622,7 +1611,6 @@ mod tests { .unwrap(); let _ = read(&mut stream).await; let refused = ietf::RequestError { - request_id: None, error_code: 0x3, reason_phrase: "".into(), retry_interval: 0, @@ -1705,8 +1693,7 @@ mod tests { Some(true) ); let done = ietf::PublishDone { - request_id: None, - status_code: ietf::PublishDoneStatus::TrackEnded.code(version), + status_code: ietf::PublishDoneStatus::TrackEnded.code(), stream_count: 0, reason_phrase: "".into(), }; @@ -1739,8 +1726,7 @@ mod tests { assert!(!serving.is_finished(), "the copy did not linger"); let done = ietf::PublishDone { - request_id: None, - status_code: ietf::PublishDoneStatus::InternalError.code(version), + status_code: ietf::PublishDoneStatus::InternalError.code(), stream_count: 0, reason_phrase: "".into(), }; @@ -1793,7 +1779,6 @@ mod tests { let _held = match case { "refused" => { let refused = ietf::RequestError { - request_id: None, error_code: 0x3, reason_phrase: "".into(), retry_interval: 0, @@ -1832,7 +1817,6 @@ mod tests { let msg: ietf::Subscribe = body.message(version).unwrap(); assert!(msg.forward, "{case}: a paused SUBSCRIBE"); let ok = ietf::SubscribeOk { - request_id: None, track_alias: 7, largest: None, properties: ietf::Properties::default(), diff --git a/rs/moq-net/src/ietf/token.rs b/rs/moq-net/src/ietf/token.rs index 59fc94c2430..bada6914429 100644 --- a/rs/moq-net/src/ietf/token.rs +++ b/rs/moq-net/src/ietf/token.rs @@ -12,11 +12,11 @@ use crate::driver::Transport; use crate::{ DecodeError, Error, SessionError, - coding::{Decode as _, Decoder, EncodeError, Encoder}, + coding::{Decoder, EncodeError, Encoder}, setup::Token, }; -use super::{Param, ParameterBytes, Parameters, RequestId, Version, error::request}; +use super::{Param, ParameterBytes, Parameters, Version, error::request}; /// Retire a registered alias. const DELETE: u64 = 0x0; @@ -107,27 +107,16 @@ pub(super) fn reason(err: &DecodeError) -> &'static str { /// Refuse a request whose tokens decoded to `err`, one [`refuses_request`] accepts, ending /// its stream. /// -/// Section 8.9 rejects the message rather than the session. Every request starts with its -/// Request ID, so it is read from the undecoded `body`. +/// Section 8.9 rejects the message rather than the session. #[cfg(feature = "async")] pub(super) async fn reject( mut stream: Stream, kind: request::Kind, - body: super::Body, err: DecodeError, version: Version, ) -> Result<(), Error> { - let request_id = RequestId::decode(&mut body.decoder(version), version)?; let reason = reason(&err); - super::request_stream::write_error( - &mut stream.writer, - kind, - request_id, - &Error::Decode(err), - reason, - version, - ) - .await?; + super::request_stream::write_error(&mut stream.writer, kind, &Error::Decode(err), reason, version).await?; // The peer dropping the stream once it has the rejection is a normal end. let _ = stream.writer.close().await; Ok(()) @@ -214,10 +203,6 @@ mod tests { } const VERSIONS: [Version; 9] = [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -339,9 +324,7 @@ mod tests { let key = u64::from(ParameterBytes::AuthorizationToken); let (count, keys): (Option, [u64; 2]) = match version { - Version::Draft14 | Version::Draft15 => (Some(2), [key, key]), // Delta-encoded from draft-16, so the repeat is a delta of zero. - Version::Draft16 => (Some(2), [key, 0]), _ => (None, [key, 0]), }; let mut raw = Vec::new(); diff --git a/rs/moq-net/src/ietf/track.rs b/rs/moq-net/src/ietf/track.rs index abc97b75ce6..52f415f4820 100644 --- a/rs/moq-net/src/ietf/track.rs +++ b/rs/moq-net/src/ietf/track.rs @@ -7,7 +7,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive}; use crate::{ Path, coding::*, - ietf::{Filter, GroupOrder, Location, Properties, RequestId, Subscribe, SubscribeOk}, + ietf::{Filter, Location, Properties, RequestId, Subscribe}, }; use super::Message; @@ -15,9 +15,8 @@ use super::namespace::encode_namespace; use super::Version; -/// TrackStatus message (0x0d) -/// v14: own format (TrackStatusRequest-like with subscribe fields) -/// v15: same wire format as SUBSCRIBE. Response is REQUEST_OK. +/// TrackStatus message (0x0d), with the same wire format as SUBSCRIBE. The response is +/// REQUEST_OK. #[derive(Clone, Debug)] pub struct TrackStatus<'a> { pub request_id: RequestId, @@ -26,7 +25,7 @@ pub struct TrackStatus<'a> { /// Whether the requester wants Track Properties on the answer (INCLUDE_PROPERTIES, /// draft-20). pub properties_wanted: bool, - /// The publisher instance the requester expects (MoQ Epoch, draft-17+). + /// The publisher instance the requester expects (MoQ Epoch). pub epoch: Option, } @@ -35,31 +34,17 @@ impl Message for TrackStatus<'_> { fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { self.request_id.encode(w, version)?; - if version == Version::Draft17 { - w.varint(0)?; // required_request_id_delta = 0 - } encode_namespace(w, &self.track_namespace)?; w.string(&self.track_name)?; - match version { - Version::Draft14 => { - w.u8(0); // subscriber priority - GroupOrder::Descending.encode(w, version)?; - w.bool(false); // forward - Filter::NextObject.encode(w, version)?; // filter - w.u8(0); // no parameters - } - _ => { - // Only the opt-out is worth bytes, and an older peer would read the - // parameter as unknown, which is a protocol violation. - let include_properties = (!self.properties_wanted && Filter::is_draft20(version)).then_some(false); - let epoch = self.epoch.clone().filter(|_| super::epoch::supported(version)); - encode_params!(w, version, - 0x35 => include_properties, - super::epoch::EPOCH_PARAM => epoch, - ); - } - } + // Only the opt-out is worth bytes, and an older peer would read the + // parameter as unknown, which is a protocol violation. + let include_properties = (!self.properties_wanted && Filter::is_draft20(version)).then_some(false); + let epoch = self.epoch.clone(); + encode_params!(w, version, + 0x35 => include_properties, + super::epoch::EPOCH_PARAM => epoch, + ); Ok(()) } @@ -78,132 +63,41 @@ impl Message for TrackStatus<'_> { } } -/// TRACK_STATUS_OK: what a SUBSCRIBE_OK would say about the track, with no subscription. -/// -/// Draft-14 gives it its own type ([`Self::ID_14`]) and the SUBSCRIBE_OK body, with a -/// Track Alias of 0. Later drafts answer with a REQUEST_OK, which gained Track Properties -/// in draft-18; before that only the message parameters carry anything. +/// TRACK_STATUS_OK: what a SUBSCRIBE_OK would say about the track, with no subscription, +/// sent as a REQUEST_OK with Track Properties. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TrackStatusOk { - /// Present on drafts 14-16 only. - pub request_id: Option, - /// The largest Location in the track (LARGEST_OBJECT), absent while it has no content. pub largest: Option, - /// The Track Properties, on the drafts whose answer has room for them. + /// The Track Properties. pub properties: Properties, } -/// TRACK_STATUS_ERROR on draft-14, with the SUBSCRIBE_ERROR body. Later drafts refuse -/// with REQUEST_ERROR. -pub const TRACK_STATUS_ERROR_14: u64 = 0x0F; - -impl TrackStatusOk { - /// The draft-14 message type. - pub const ID_14: u64 = 0x0E; - - /// The message type on `version`. - pub fn id(version: Version) -> u64 { - match version { - Version::Draft14 => Self::ID_14, - _ => Self::ID, - } - } - - /// Whether this answer describes the track as fully as SUBSCRIBE_OK on `version`. - /// Draft-17 is the gap: SUBSCRIBE_OK declares TIMESCALE, but REQUEST_OK has no - /// properties block to say whether the track is timed. - pub(crate) fn describes_track(version: Version) -> bool { - Self::has_properties(version) || !super::Properties::sends_timescale(version) - } - - /// Whether the answer carries a Track Properties block on `version`. - fn has_properties(version: Version) -> bool { - !matches!( - version, - Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 - ) - } -} - impl Message for TrackStatusOk { - /// REQUEST_OK; see [`Self::id`]. + /// REQUEST_OK. const ID: u64 = 0x07; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - if version == Version::Draft14 { - return SubscribeOk { - request_id: self.request_id, - track_alias: 0, - largest: self.largest, - properties: self.properties.clone(), - } - .encode_msg(w, version); - } - - if matches!(version, Version::Draft15 | Version::Draft16) { - self.request_id - .expect("request_id required for draft14-16") - .encode(w, version)?; - } else { - assert!(self.request_id.is_none(), "request_id must be None for draft17+"); - } - - // The parameters SUBSCRIBE_OK carries: GROUP_ORDER is one only on draft-15, and a - // Track Property after. - let group_order = match version { - Version::Draft15 => self.properties.group_order, - _ => None, - }; + // The parameters SUBSCRIBE_OK carries. encode_params!(w, version, 0x09 => self.largest, - 0x22 => group_order, ); // Track Properties are the final field, so nothing may follow. - if Self::has_properties(version) { - self.properties.encode(w, version)?; - } + self.properties.encode(w, version)?; Ok(()) } fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - if version == Version::Draft14 { - let ok = SubscribeOk::decode_msg(r, version)?; - return Ok(Self { - request_id: ok.request_id, - largest: ok.largest, - properties: ok.properties, - }); - } - - let request_id = match version { - Version::Draft15 | Version::Draft16 => Some(RequestId::decode(r, version)?), - _ => None, - }; - // Read as SUBSCRIBE_OK's are: EXPIRES is ignored, and MAX_CACHE_DURATION is a - // parameter only on draft-15. + // Read as SUBSCRIBE_OK's are: EXPIRES is ignored. decode_params!(r, version, - 0x04 => max_cache_duration: Option where version == Version::Draft15, 0x08 => _expires: Option, 0x09 => largest: Option, - 0x22 => group_order: Option where version == Version::Draft15, ); - let mut properties = match Self::has_properties(version) { - true => Properties::decode(r, version)?, - false => Properties::default(), - }; - properties.group_order = properties.group_order.or(group_order); - if version == Version::Draft15 { - properties.max_cache_duration = max_cache_duration.map(std::time::Duration::from_millis); - } + let properties = Properties::decode(r, version)?; - Ok(Self { - request_id, - largest, - properties, - }) + Ok(Self { largest, properties }) } } @@ -352,10 +246,6 @@ mod tests { } const ALL: [Version; 9] = [ - Version::Draft14, - Version::Draft15, - Version::Draft16, - Version::Draft17, Version::Draft18, Version::Draft19, Version::Draft20, @@ -402,22 +292,13 @@ mod tests { for version in ALL { let msg = TrackStatusOk { - request_id: matches!(version, Version::Draft14 | Version::Draft15 | Version::Draft16) - .then_some(RequestId(3)), largest, properties: properties.clone(), }; let decoded: TrackStatusOk = decode_message(&encode_message(&msg, version), version).unwrap_or_else(|e| panic!("{version}: {e}")); - let expected = match version { - Version::Draft14 | Version::Draft15 => Properties { - group_order: Some(GroupOrder::Descending), - ..Default::default() - }, - Version::Draft16 | Version::Draft17 => Properties::default(), - _ => properties.clone(), - }; + let expected = properties.clone(); assert_eq!(decoded.request_id, msg.request_id, "{version}"); assert_eq!(decoded.largest, largest, "{version}"); assert_eq!(decoded.properties, expected, "{version}"); @@ -428,14 +309,12 @@ mod tests { #[test] fn test_track_status_ok_draft_14_is_a_subscribe_ok() { let msg = TrackStatusOk { - request_id: Some(RequestId(3)), largest: None, properties: Properties::default(), }; assert_eq!(TrackStatusOk::id(Version::Draft14), 0x0E); assert_eq!(TrackStatusOk::id(Version::Draft15), 0x07); let subscribe_ok = SubscribeOk { - request_id: Some(RequestId(3)), track_alias: 0, largest: None, properties: Properties::default(), diff --git a/rs/moq-net/src/ietf/version.rs b/rs/moq-net/src/ietf/version.rs index faf024169dd..59f22f18ddb 100644 --- a/rs/moq-net/src/ietf/version.rs +++ b/rs/moq-net/src/ietf/version.rs @@ -4,10 +4,6 @@ use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Version { - Draft14, - Draft15, - Draft16, - Draft17, Draft18, Draft19, Draft20, @@ -18,10 +14,6 @@ pub enum Version { impl fmt::Display for Version { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Draft14 => write!(f, "moq-transport-14"), - Self::Draft15 => write!(f, "moq-transport-15"), - Self::Draft16 => write!(f, "moq-transport-16"), - Self::Draft17 => write!(f, "moq-transport-17"), Self::Draft18 => write!(f, "moq-transport-18"), Self::Draft19 => write!(f, "moq-transport-19"), Self::Draft20 => write!(f, "moq-transport-20"), diff --git a/rs/moq-net/src/lite/ietf.rs b/rs/moq-net/src/lite/ietf.rs index fae719f149a..f39c8172bed 100644 --- a/rs/moq-net/src/lite/ietf.rs +++ b/rs/moq-net/src/lite/ietf.rs @@ -206,11 +206,11 @@ pub(crate) fn decode_properties(body: &[u8], framing: super::Framing, framed: bo } /// Decode the END_OF_GROUP record's body on a track `framing` describes. It is a status -/// object, so a block needs a draft that lets one carry properties. +/// object, so it may not carry properties. #[cfg(feature = "async")] pub(crate) fn decode_end(body: &[u8], framing: super::Framing) -> Result { let decoded = decode_properties(body, framing, false)?; - if decoded.properties.block.is_some() && !framing.ietf.is_some_and(crate::ietf::object::status_has_properties) { + if decoded.properties.block.is_some() { return Err(DecodeError::InvalidValue); } Ok(decoded) @@ -341,9 +341,6 @@ impl Message for TrackProperties { return Err(DecodeError::InvalidValue); }; // Track Properties start at draft-16. - if matches!(version, crate::ietf::Version::Draft14 | crate::ietf::Version::Draft15) && !r.is_empty() { - return Err(DecodeError::InvalidValue); - } let properties = crate::model::object::TrackProperties { version, values: decode_list(r, version)?, @@ -417,7 +414,7 @@ pub(crate) fn decode_datagram( return Err(DecodeError::InvalidValue); } // A track published over moq-lite has no moq-transport state. - let version = framing.ietf.ok_or(DecodeError::InvalidValue)?; + framing.ietf.ok_or(DecodeError::InvalidValue)?; let object_id = (state & OBJECT_ID != 0).then(|| r.varint()).transpose()?; let priority = (state & PRIORITY != 0).then(|| r.u8()).transpose()?; let status = (state & STATUS != 0).then(|| r.varint()).transpose()?; @@ -433,7 +430,7 @@ pub(crate) fn decode_datagram( properties, }; // A relay holds and forwards it, so it must be one its publisher's draft could send. - if !wire.defined(version) { + if !wire.defined() { return Err(DecodeError::InvalidValue); } Ok(Some(wire)) @@ -565,7 +562,7 @@ mod tests { /// after a large first ID, crosses the hop byte for byte and is held at its wire size. #[test] fn a_full_block_crosses_at_its_wire_size() { - for version in [Draft::Draft16, Draft::Draft22] { + for version in [Draft::Draft22] { let mut list = Vec::new(); let e = &mut Encoder::new(&mut list, version.into()); // A Timestamp, then IDs from 2^40 on, each repeated as a two-byte zero delta. @@ -626,7 +623,7 @@ mod tests { assert_eq!(decoded.0.as_deref(), Some(&properties)); // Drafts 14 and 15 have no Track Properties, but their objects may have properties. - for version in [Draft::Draft14, Draft::Draft22] { + for version in [Draft::Draft22] { let bare = crate::model::object::TrackProperties { version, values: Vec::new(), @@ -677,7 +674,7 @@ mod tests { decode(&wire(&[opaque(0x40)], 0)).is_err(), "properties on a moq-lite track" ); - for version in [Draft::Draft14, Draft::Draft15] { + for version in [Draft::Draft15] { assert!(decode(&wire(&[], code(version))).is_ok(), "{version}"); assert!( decode(&wire(&[opaque(0x40)], code(version))).is_err(), @@ -730,7 +727,7 @@ mod tests { /// message would no longer fit a lite one. #[test] fn track_properties_cross_at_their_wire_size() { - for version in [Draft::Draft16, Draft::Draft17, Draft::Draft22] { + for version in [Draft::Draft22] { // As many repeats of an ID from 2^60 as moq-transport forwards, then one value // that fills the rest of a lite message beside its Version. let id = (1 << 60) + 1; @@ -971,7 +968,7 @@ mod tests { #[cfg(feature = "async")] #[test] fn an_end_record_block_needs_a_draft_that_carries_one() { - for version in [Draft::Draft14, Draft::Draft15, Draft::Draft16, Draft::Draft22] { + for version in [Draft::Draft22] { let framing = from(version, None); assert_eq!( decode_end(&[0x20, 1], framing).is_ok(), @@ -987,10 +984,6 @@ mod tests { #[test] fn a_datagram_block_needs_a_draft_that_sends_its_shape() { let drafts = [ - Draft::Draft14, - Draft::Draft15, - Draft::Draft16, - Draft::Draft17, Draft::Draft22, ]; for (status, carried) in [ diff --git a/rs/moq-net/src/model/origin.rs b/rs/moq-net/src/model/origin.rs index 7bb068b1879..494dd650976 100644 --- a/rs/moq-net/src/model/origin.rs +++ b/rs/moq-net/src/model/origin.rs @@ -1690,7 +1690,7 @@ impl TableCursor { /// Whether the hidden rule lets this cursor discover a route at `prefix`. fn discovers(&self, prefix: &Path) -> bool { - self.hidden.discovers(&self.heads, prefix) + self.hidden.reaches(&self.heads, prefix) } } @@ -1860,35 +1860,20 @@ pub(super) struct Hidden { include: bool, /// Measure visibility from the request prefix instead of authorization heads. from: Option, - /// Report only what a feed scoped to these heads hides, for a stream that tops - /// up a feed already carrying everything visible from them. - beyond: Option>, } impl Hidden { - /// Whether a cursor hanging at `heads` reports a route at `prefix`. - fn discovers(&self, heads: &[PathOwned], prefix: &Path) -> bool { - self.reaches(heads, prefix) && self.beyond.as_ref().is_none_or(|outer| hides(outer, prefix)) - } - - /// Whether a cursor hanging at `heads` could report a route at or beneath `prefix`. - /// A hidden segment hides everything beneath it, while a top-up feed's rule turns - /// on the rest of the path, so only the first can rule a prefix out. + /// Whether a cursor hanging at `heads` reports a route at or beneath `prefix`. A hidden + /// segment hides everything beneath it. fn reaches(&self, heads: &[PathOwned], prefix: &Path) -> bool { self.include || !hides(self.from.as_ref().map(std::slice::from_ref).unwrap_or(heads), prefix) } - /// This rule for a cursor reading through `mount`, whose heads sit on the - /// target: a feed it tops up hid everything under the mount, or hid what it - /// hides under the target. + /// This rule for a cursor reading through `mount`, whose heads sit on the target. fn translate(&self, mount: &Mount) -> Self { - let beyond = self.beyond.as_ref().and_then(|outer| { - (!hides(outer, &mount.at)).then(|| outer.iter().filter_map(|head| mount.translate_head(head)).collect()) - }); Self { include: self.include, from: self.from.as_ref().and_then(|head| mount.translate_head(head)), - beyond, } } } @@ -6034,21 +6019,6 @@ impl Consumer { self } - /// A clone whose [`announced`](Self::announced) reports only the routes a feed - /// from `outer` hides, for a stream topping up that feed. - #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - pub(crate) fn beyond(mut self, outer: &Consumer) -> Self { - self.hidden.beyond = Some( - outer - .hidden - .from - .clone() - .map(|head| vec![head]) - .unwrap_or_else(|| interest_prefixes(&outer.scope.allowed)), - ); - self - } - /// Set wire discovery visibility relative to this consumer's requested root. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] pub(crate) fn discovery(mut self, hidden: bool) -> Self { @@ -7511,20 +7481,6 @@ mod tests { let mut announced = consumer.scope("", &scopes(&["room/.internal"])).unwrap().announced(); announced.expect_next_active("room/.internal").await; announced.assert_next_wait(); - - // A top-up feed reports only what a feed from the root hid, filtered by its own - // prefix and opt-in. - let mut announced = consumer.clone().with_hidden(true).beyond(&consumer).announced(); - announced.expect_next_active(".stats/node").await; - announced.expect_next_active("room/.internal").await; - announced.assert_next_wait(); - let room = consumer.scope("", &scopes(&["room"])).unwrap().beyond(&consumer); - let mut announced = room.announced(); - announced.assert_next_wait(); - let stats = consumer.scope("", &scopes(&[".stats"])).unwrap().beyond(&consumer); - let mut announced = stats.announced(); - announced.expect_next_active(".stats/node").await; - announced.assert_next_wait(); } /// Hiding narrows discovery only: an exact request resolves without an opt-in. diff --git a/rs/moq-net/src/server.rs b/rs/moq-net/src/server.rs index 458c8eaf516..ca6f9ad1431 100644 --- a/rs/moq-net/src/server.rs +++ b/rs/moq-net/src/server.rs @@ -6,8 +6,7 @@ use crate::origin; use crate::time::{Deadlines, Instant}; use crate::transport::poll::Session as _; use crate::{ - ALPN_14, ALPN_15, ALPN_16, ALPN_17, ALPN_18, ALPN_19, ALPN_20, ALPN_21, ALPN_22, ALPN_LITE, ALPN_LITE_03, - ALPN_LITE_04, ALPN_LITE_05, ALPN_LITE_06, ALPN_LITE_07_WIP, Consume, Error, NEGOTIATED, Session, SessionError, + ALPN_18, ALPN_19, ALPN_20, ALPN_21, ALPN_22, ALPN_LITE, ALPN_LITE_03, ALPN_LITE_04, ALPN_LITE_05, ALPN_LITE_06, ALPN_LITE_07_WIP, Consume, Error, NEGOTIATED, Session, SessionError, Version, Versions, coding::{Decode, Encode, Stream}, ietf, lite, @@ -285,7 +284,7 @@ impl Server { PausedHandshake::LiteBare { version, .. } | PausedHandshake::LiteSetup { version, .. } => { version.has_auth() } - PausedHandshake::IetfModern(_) | PausedHandshake::Legacy(_) => false, + PausedHandshake::IetfModern(_) | PausedHandshake::Negotiated(_) => false, }); Ok(Handshake { path, @@ -350,54 +349,31 @@ impl Server { } async fn handshake(&self, runtime: Deadlines, mut session: Transport) -> Result { - let (encoding, supported) = match session.protocol() { - Some(alpn @ (ALPN_22 | ALPN_21 | ALPN_20 | ALPN_19 | ALPN_18 | ALPN_17)) => { + let supported = match session.protocol() { + Some(alpn @ (ALPN_22 | ALPN_21 | ALPN_20 | ALPN_19 | ALPN_18)) => { let draft = match alpn { ALPN_22 => ietf::Version::Draft22, ALPN_21 => ietf::Version::Draft21, ALPN_20 => ietf::Version::Draft20, ALPN_19 => ietf::Version::Draft19, - ALPN_18 => ietf::Version::Draft18, - _ => ietf::Version::Draft17, + _ => ietf::Version::Draft18, }; self.versions.select(Version::Ietf(draft)).ok_or(Error::Version)?; return self.accept_ietf_modern(runtime, session, draft).await; } - Some(ALPN_16) => { - let v = self - .versions - .select(Version::Ietf(ietf::Version::Draft16)) - .ok_or(Error::Version)?; - (v, v.into()) - } - Some(ALPN_15) => { - let v = self - .versions - .select(Version::Ietf(ietf::Version::Draft15)) - .ok_or(Error::Version)?; - (v, v.into()) - } - Some(ALPN_14) => { - let v = self - .versions - .select(Version::Ietf(ietf::Version::Draft14)) - .ok_or(Error::Version)?; - (v, v.into()) - } Some(ALPN_LITE_07_WIP | ALPN_LITE_06 | ALPN_LITE_05 | ALPN_LITE_04 | ALPN_LITE_03) => { return self.handshake_lite(runtime, session).await; } - Some(ALPN_LITE) | None => { - let supported = self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?; - (Version::Ietf(ietf::Version::Draft14), supported) - } + // No ALPN, or `moql`: negotiate moq-lite 01/02 in a bidi SETUP. + Some(ALPN_LITE) | None => self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?, Some(p) => return Err(Error::UnknownAlpn(p.to_string())), }; - // Legacy bidi SETUP exchange (IETF 14-16, lite 01/02). Read the client's - // SETUP to choose the version; `ok()` sends the server SETUP and starts. + // Read the client's SETUP to choose the version; `ok()` sends the server SETUP and starts. // Stats attach in `ok()`: a caller may still replace them on the handshake. + // Lite 01 and 02 frame the SETUP identically, so either decodes it. + let encoding = *supported.iter().next().ok_or(Error::Version)?; let mut stream = Stream::accept(&mut session, encoding).await?; let (client, client_setup_size) = stream.reader.decode_sized::().await?; @@ -407,71 +383,34 @@ impl Server { .flat_map(|v| Version::try_from(*v).ok()) .find(|v| supported.contains(v)) .ok_or(Error::Version)?; - - // Pull the request path and max request ID out now (IETF only) so `ok()` - // doesn't re-decode the consumed parameters. moq-transport carries the path - // in its SETUP just like lite-05. - let (path, token, request_id_max, peer_declared, peer_implementation) = match version { - Version::Ietf(v) => { - let (params, _) = ietf::Parameters::decode_slice(&client.parameters, v)?; - let path = match params.get_bytes(ietf::ParameterBytes::Path) { - Some(bytes) => Some( - std::str::from_utf8(bytes) - .map_err(|_| Error::Decode(crate::DecodeError::InvalidValue))? - .to_owned(), - ), - None => None, - }; - let token = ietf::token::from_setup(¶ms, v)?; - let request_id_max = params - .get_varint(ietf::ParameterVarInt::MaxRequestId) - .map(ietf::RequestId); - let peer_declared = ietf::peer::Peer { - solicit: ietf::solicit::from_setup(¶ms, v)?, - hidden: ietf::hidden::from_setup(¶ms, v), - max_age: ietf::max_age::from_setup(¶ms, v), - active_count: ietf::active_count::from_setup(¶ms, v), - flate: ietf::flate::from_setup(¶ms, v)?, - ..Default::default() - }; - let implementation = - setup::decode_implementation(params.get_bytes(ietf::ParameterBytes::Implementation))?; - (path, token, request_id_max, peer_declared, implementation) - } - Version::Lite(_) => (None, None, None, ietf::peer::Peer::default(), None), + let Version::Lite(version) = version else { + return Err(Error::Version); }; Ok(Handshake { - path, + path: None, role: None, origin: None, - token, - peer_implementation: peer_implementation.clone(), + token: None, + peer_implementation: None, assigned_hop: crate::Hop::random(), - // Lite 01/02 and moq-transport 14-16 carry no AUTH, though a moq-transport request - // still carries a token. - auth: match version { - Version::Ietf(_) => crate::auth::Link::ietf(false), - Version::Lite(_) => crate::auth::Link::new(false), - }, + // Lite 01/02 carry no AUTH. + auth: crate::auth::Link::new(false), inner: Some(RequestInner { server: self.clone(), runtime, deadline: None, - handshake: PausedHandshake::Legacy(PausedLegacy { + handshake: PausedHandshake::Negotiated(PausedNegotiated { session, stream, version, - request_id_max, - peer_declared, - peer_implementation, client_setup_size, }), }), }) } - /// Read a draft-17/18 client's SETUP (with its request path) off its uni stream, + /// Read a moq-transport client's SETUP (with its request path) off its uni stream, /// then pause. `ok()` starts the session and hands the stream back for GOAWAY. async fn accept_ietf_modern( &self, @@ -556,7 +495,7 @@ enum PausedHandshake { client_setup: lite::AcceptedSetup, }, IetfModern(PausedIetfModern), - Legacy(PausedLegacy), + Negotiated(PausedNegotiated), } /// Modern IETF (17+): the client's SETUP (with its request path) has been @@ -593,8 +532,7 @@ impl PausedIetfModern { stats: server.stats.clone(), limits: server.limits, session: session.clone(), - setup: None, - request_id_max: None, + client: false, publish, subscribe, @@ -631,23 +569,17 @@ impl PausedIetfModern { } } -/// Legacy IETF (draft 14-16) and lite 01/02: the client SETUP has been read -/// off the bidi stream (including its request path) but the server SETUP -/// hasn't been sent; `ok` finishes it. -struct PausedLegacy { +/// moq-lite 01/02: the client SETUP has been read off the bidi stream but the server +/// SETUP hasn't been sent; `ok` finishes it. +struct PausedNegotiated { session: Transport, stream: Stream, - version: Version, - request_id_max: Option, - /// What the client's SETUP declared, for the options `ok` acts on. - peer_declared: ietf::peer::Peer, - /// The implementation the client's SETUP advertised. - peer_implementation: Option, + version: lite::Version, /// The client SETUP's encoded size, counted under the final stats. client_setup_size: usize, } -impl PausedLegacy { +impl PausedNegotiated { /// Send the server SETUP and start the session. async fn ok( self, @@ -660,125 +592,49 @@ impl PausedLegacy { session, stream, version, - request_id_max, - peer_declared, - peer_implementation, client_setup_size, } = self; let mut stream = stream.with_stats(&server.stats); server.stats.control(stats::Control::Session, client_setup_size, true); let (publish, subscribe) = server.stat_tagged_origins(); - // Encode parameters using the version-appropriate type. - let parameters = match version { - Version::Ietf(v) => { - let mut parameters = ietf::Parameters::default(); - // The client's requests, admitted up to our limits and granted back as they close. - parameters.set_varint( - ietf::ParameterVarInt::MaxRequestId, - ietf::initial_max_request_id(server.limits.window(), true), - ); - if let Some(implementation) = server.implementation() { - parameters.set_bytes(ietf::ParameterBytes::Implementation, implementation.into_bytes()); - } - if server.extensions.solicit { - ietf::solicit::into_setup(&mut parameters, v); - } - ietf::hidden::into_setup(&mut parameters, v); - ietf::max_age::into_setup(&mut parameters, v); - ietf::active_count::into_setup(&mut parameters, v); - ietf::flate::into_setup(&mut parameters, v); - parameters.encode_bytes(v)? - } - Version::Lite(v) => lite::Parameters::default().encode_bytes(v)?, - }; - let server_setup = setup::Server { - version: version.into(), - parameters, + version: Version::Lite(version).into(), + parameters: lite::Parameters::default().encode_bytes(version)?, }; stream.writer.encode(&server_setup).await?; - let (recv_bw, protocol, goaway, auth, estimate, peer_implementation) = match version { - Version::Lite(v) => { - let stream = stream.with_version(v); - // Pre-lite-05: no Setup Stream, so nothing to advertise or seed. - let start = lite::start(lite::Config { - runtime: runtime.clone(), - stats: server.stats.clone(), - client: false, - limits: server.limits, - session: session.clone(), - setup_stream: Some(stream), - publish, - subscribe, - peer_hop, - version: v, - our_setup: lite::Setup::default(), - peer_setup: None, - auth, - })?; - ( - start.recv_bandwidth, - crate::driver::Protocol::Lite(Box::new(start.driver)), - start.goaway, - start.auth, - Some(start.estimate), - start.peer_implementation, - ) - } - Version::Ietf(v) => { - let stream = stream.with_version(v); - let peer_implementation = setup::PeerImplementation::known(peer_implementation); - // Draft 14-16: path came in the bidi SETUP, no uni SETUP to hand back. - let (protocol, goaway) = ietf::start(ietf::Config { - runtime: runtime.clone(), - stats: server.stats.clone(), - limits: server.limits, - session: session.clone(), - setup: Some(stream), - request_id_max, - client: false, - publish, - subscribe, - peer_hop, - cost: None, - version: v, - path: None, - authority: None, - peer_setup_stream: None, - peer_declared: Some(peer_declared), - auth: auth.clone(), - early_unis: Vec::new(), - extensions: server.extensions, - // Already sent on the bidi SETUP above. - implementation: None, - peer_implementation: peer_implementation.clone(), - })?; - ( - None, - crate::driver::Protocol::Ietf(protocol), - goaway, - auth, - None, - peer_implementation, - ) - } - }; + let stream = stream.with_version(version); + // Pre-lite-05: no Setup Stream, so nothing to advertise or seed. + let start = lite::start(lite::Config { + runtime: runtime.clone(), + stats: server.stats.clone(), + client: false, + limits: server.limits, + session: session.clone(), + setup_stream: Some(stream), + publish, + subscribe, + peer_hop, + version, + our_setup: lite::Setup::default(), + peer_setup: None, + auth, + })?; Ok(Session::new( runtime, session, - version, - recv_bw, - protocol, + version.into(), + start.recv_bandwidth, + crate::driver::Protocol::Lite(Box::new(start.driver)), crate::session::Handles { stats: server.stats.clone(), - goaway, - auth, - estimate, + goaway: start.goaway, + auth: start.auth, + estimate: Some(start.estimate), bandwidth: crate::bandwidth::Allocator::default(), - peer_implementation, + peer_implementation: start.peer_implementation, }, )) } @@ -946,7 +802,7 @@ impl Handshake { } PausedHandshake::IetfModern(paused) => paused.ok(server, runtime, peer_hop, auth), // Boxed so the bidi SETUP write of these old versions doesn't size every caller's future. - PausedHandshake::Legacy(paused) => Box::pin(paused.ok(server, runtime, peer_hop, auth)).await, + PausedHandshake::Negotiated(paused) => Box::pin(paused.ok(server, runtime, peer_hop, auth)).await, } }; let res = match deadline { @@ -1004,7 +860,7 @@ impl PausedHandshake { (session.clone(), (*version).into()) } Self::IetfModern(paused) => (paused.session.clone(), paused.version.into()), - Self::Legacy(paused) => (paused.session.clone(), paused.version), + Self::Negotiated(paused) => (paused.session.clone(), paused.version.into()), } } } @@ -1338,7 +1194,6 @@ mod tests { ( FakeSession::new(ALPN_14, []) .with_bi(legacy_setup(ietf::Version::Draft14, ietf::Parameters::default())), - ietf::Version::Draft14, ), ( FakeSession::new(ALPN_18, [ietf_setup(ietf::Version::Draft18, Some("room/alice"))]), diff --git a/rs/moq-net/src/session.rs b/rs/moq-net/src/session.rs index 4a0bafec220..a222467415b 100644 --- a/rs/moq-net/src/session.rs +++ b/rs/moq-net/src/session.rs @@ -876,9 +876,8 @@ impl Drop for Withdrawing { /// Bounds on what one peer can make a session hold, where its transport does not already. /// -/// Each moq-lite request, and each moq-transport request from draft 17 on, holds a stream -/// of its own, so the transport's stream credit bounds those. A peer that announces past -/// `announces`, or overruns the drafts 14 to 16 request window, loses the session, closed with +/// Each request holds a stream of its own, so the transport's stream credit bounds those. +/// A peer that announces past `announces` loses the session, closed with /// `TOO_MANY_REQUESTS`. The stats sessions rows report announcement and subscription peaks. /// /// ``` @@ -891,32 +890,12 @@ impl Drop for Withdrawing { pub struct Limits { /// Broadcasts (moq-lite) or namespaces (moq-transport) the peer may have announced to us. pub announces: usize, - /// Other moq-transport requests the peer may hold at once, such as SUBSCRIBE and FETCH. - /// - /// On drafts 14 to 16 this and `announces` size the request window, which every request - /// counts against. Later drafts still refuse a reused request ID, so a peer may leave at - /// most twice the sum of both runs of skipped IDs; one more closes the session with - /// `INTERNAL_ERROR` rather than forgetting any. moq-lite ignores it. - pub requests: usize, } impl Default for Limits { /// Generous enough for a relay mesh carrying a large origin; lower them for untrusted peers. fn default() -> Self { - Self { - announces: 100_000, - requests: 20_000, - } - } -} - -impl Limits { - /// How many requests a moq-transport peer may hold open at once (drafts 14 to 16). - /// - /// Twice what the limits admit, so a peer within them is never blocked by request IDs - /// still waiting to be granted back. - pub(crate) fn window(&self) -> u64 { - (self.announces.saturating_add(self.requests) as u64).saturating_mul(2) + Self { announces: 100_000 } } } diff --git a/rs/moq-net/src/setup.rs b/rs/moq-net/src/setup.rs index c6358a4ff66..bbc37ad9c2c 100644 --- a/rs/moq-net/src/setup.rs +++ b/rs/moq-net/src/setup.rs @@ -8,8 +8,7 @@ use bytes::Bytes; use crate::{ Version, - coding::{self, Decode, DecodeError, Decoder, Encode, EncodeError, Encoder}, - ietf, lite, + coding::{self, Decode, DecodeError, Decoder, Encode, EncodeError, Encoder}, lite, }; /// The session direction a client advertises in its SETUP (moq-lite-05+). @@ -21,7 +20,7 @@ pub(crate) const MAX_SETUP_SIZE: usize = u16::MAX as usize; const CLIENT_SETUP: u8 = 0x20; const SERVER_SETUP: u8 = 0x21; -/// Draft-17 unified SETUP message type (varint 0x2F00) +/// The unified SETUP message type (varint 0x2F00). pub(crate) const SETUP_V17: u64 = 0x2F00; /// Decode a SETUP Implementation value, refusing one that is not UTF-8. @@ -121,26 +120,18 @@ impl Token { pub const CAT: u64 = 0x1; } -/// Draft-17+ unified SETUP message, with the same encoding for both client and server. +/// The unified SETUP message, with the same encoding for both client and server. #[derive(Debug, Clone)] pub(crate) struct Setup { pub parameters: Bytes, } -impl Setup { - fn check_version(v: Version) { - match v { - Version::Ietf(ietf::Version::Draft14 | ietf::Version::Draft15 | ietf::Version::Draft16) - | Version::Lite(_) => unreachable!("Setup is draft-17+ only"), - _ => {} - } - } -} - impl Encode for Setup { fn encode(&self, w: &mut Encoder<'_>, v: Version) -> Result<(), EncodeError> { w.control = Some(crate::stats::Control::Session); - Self::check_version(v); + if !v.is_ietf() { + return Err(EncodeError::Version); + } w.varint(SETUP_V17)?; let prefix = w.prefix_u16(); w.slice(&self.parameters); @@ -151,7 +142,9 @@ impl Encode for Setup { impl Decode for Setup { fn decode(r: &mut Decoder<'_>, v: Version) -> Result { r.control = Some(crate::stats::Control::Session); - Self::check_version(v); + if !v.is_ietf() { + return Err(DecodeError::Version); + } let kind = r.varint()?; if kind != SETUP_V17 { return Err(DecodeError::InvalidValue); @@ -162,78 +155,38 @@ impl Decode for Setup { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum SetupVersion { - Draft14, - Draft15Plus, - /// Draft17+ uses ALPN-only negotiation with no legacy SETUP message. - Modern, - LiteLegacy, - Unsupported, +/// Whether `v` negotiates over the `moql` bidi SETUP exchange (moq-lite 01/02). +fn negotiated(v: Version) -> bool { + matches!(v, Version::Lite(lite::Version::Lite01 | lite::Version::Lite02)) } -impl SetupVersion { - fn from_version(v: Version) -> Self { - match v { - Version::Ietf(ietf::Version::Draft14) => Self::Draft14, - Version::Ietf(ietf::Version::Draft15) | Version::Ietf(ietf::Version::Draft16) => Self::Draft15Plus, - Version::Ietf(ietf::Version::Draft17) - | Version::Ietf(ietf::Version::Draft18) - | Version::Ietf(ietf::Version::Draft19) - | Version::Ietf(ietf::Version::Draft20) - | Version::Ietf(ietf::Version::Draft21) - | Version::Ietf(ietf::Version::Draft22) => Self::Modern, - Version::Lite(lite::Version::Lite01) | Version::Lite(lite::Version::Lite02) => Self::LiteLegacy, - Version::Lite(_) => Self::Unsupported, - } - } -} - -/// A version-agnostic setup message sent by the client. +/// The client half of the `moql` bidi SETUP exchange, offering moq-lite 01/02. +/// +/// The framing is moq-transport draft-14's CLIENT_SETUP, which moq-lite 01/02 borrowed: +/// a u16 size, the offered versions, then parameters. #[derive(Debug, Clone)] pub(crate) struct Client { /// The list of supported versions in preferred order. pub versions: coding::Versions, - /// Parameters, unparsed because the IETF draft changed the encoding. + /// Parameters, unparsed: moq-lite 01/02 define none, so neither side reads them. pub parameters: Bytes, } -impl Client { - fn encode_inner(&self, w: &mut Encoder<'_>, v: Version) -> Result<(), EncodeError> { - match SetupVersion::from_version(v) { - SetupVersion::Draft15Plus => { - // Draft15+: no versions list, parameters only. - } - SetupVersion::Draft14 | SetupVersion::LiteLegacy => self.versions.encode(w, v)?, - SetupVersion::Modern | SetupVersion::Unsupported => return Err(EncodeError::Version), - }; - w.slice(&self.parameters); - Ok(()) - } -} - impl Decode for Client { - /// Decode a client setup message (draft-14 through draft-16 only). fn decode(r: &mut Decoder<'_>, v: Version) -> Result { r.control = Some(crate::stats::Control::Session); + if !negotiated(v) { + return Err(DecodeError::Version); + } let kind = r.u8()?; if kind != CLIENT_SETUP { return Err(DecodeError::InvalidValue); } - let mut msg = decode_body(r, v)?; - - let versions = match SetupVersion::from_version(v) { - SetupVersion::Draft15Plus => { - // Draft15+: no versions list, parameters only. - coding::Versions::from([v.into()]) - } - SetupVersion::Draft14 | SetupVersion::LiteLegacy => { - coding::Versions::decode(&mut msg, v).map_err(DecodeError::complete)? - } - SetupVersion::Modern | SetupVersion::Unsupported => return Err(DecodeError::Version), - }; + let size = r.u16()? as usize; + let mut msg = r.sub(size)?; + let versions = coding::Versions::decode(&mut msg, v).map_err(DecodeError::complete)?; Ok(Self { versions, @@ -243,42 +196,20 @@ impl Decode for Client { } impl Encode for Client { - /// Encode a client setup message (draft-14 through draft-16 only). fn encode(&self, w: &mut Encoder<'_>, v: Version) -> Result<(), EncodeError> { w.control = Some(crate::stats::Control::Session); + if !negotiated(v) { + return Err(EncodeError::Version); + } w.u8(CLIENT_SETUP); - let prefix = prefix_body(w, v)?; - self.encode_inner(w, v)?; + let prefix = w.prefix_u16(); + self.versions.encode(w, v)?; + w.slice(&self.parameters); fill_body(w, prefix) } } -/// Read a pre-draft-17 SETUP body: its size, then that many bytes. -fn decode_body<'a>(r: &mut Decoder<'a>, v: Version) -> Result, DecodeError> { - let size = match SetupVersion::from_version(v) { - SetupVersion::Draft14 | SetupVersion::Draft15Plus => r.u16()? as usize, - SetupVersion::LiteLegacy => usize::try_from(r.varint()?).map_err(|_| DecodeError::BoundsExceeded)?, - SetupVersion::Modern | SetupVersion::Unsupported => return Err(DecodeError::Version), - }; - if size > MAX_SETUP_SIZE { - return Err(DecodeError::MessageTooLarge { - size, - max: MAX_SETUP_SIZE, - }); - } - r.sub(size) -} - -/// Reserve the size prefix of a pre-draft-17 SETUP body. -fn prefix_body(w: &mut Encoder<'_>, v: Version) -> Result { - match SetupVersion::from_version(v) { - SetupVersion::Draft14 | SetupVersion::Draft15Plus => Ok(w.prefix_u16()), - SetupVersion::LiteLegacy => Ok(w.prefix_varint()), - SetupVersion::Modern | SetupVersion::Unsupported => Err(EncodeError::Version), - } -} - -/// Size a pre-draft-17 SETUP body, refusing one our own receiver would refuse. +/// Size a SETUP body, refusing one our own receiver would refuse. fn fill_body(w: &mut Encoder<'_>, prefix: coding::Prefix) -> Result<(), EncodeError> { if w.since(&prefix) > MAX_SETUP_SIZE { w.discard(prefix); @@ -287,58 +218,47 @@ fn fill_body(w: &mut Encoder<'_>, prefix: coding::Prefix) -> Result<(), EncodeEr w.fill(prefix) } -/// Sent by the server in response to a client setup. +/// The server half of the `moql` bidi SETUP exchange, selecting moq-lite 01 or 02. +/// +/// The framing is moq-transport draft-14's SERVER_SETUP: a u16 size, the selected version, +/// then parameters. #[derive(Debug, Clone)] pub(crate) struct Server { - /// The list of supported versions in preferred order. + /// The selected version. pub version: coding::Version, - /// Supported extensions. + /// Parameters, unparsed: moq-lite 01/02 define none, so neither side reads them. pub parameters: Bytes, } -impl Server { - fn encode_inner(&self, w: &mut Encoder<'_>, v: Version) -> Result<(), EncodeError> { - match SetupVersion::from_version(v) { - SetupVersion::Draft15Plus => { - // Draft15+: No version field, parameters only. - } - SetupVersion::Draft14 | SetupVersion::LiteLegacy => self.version.encode(w, v)?, - SetupVersion::Modern | SetupVersion::Unsupported => return Err(EncodeError::Version), - }; - w.slice(&self.parameters); - Ok(()) - } -} - impl Encode for Server { - /// Encode a server setup message (draft-14 through draft-16 only). fn encode(&self, w: &mut Encoder<'_>, v: Version) -> Result<(), EncodeError> { w.control = Some(crate::stats::Control::Session); + if !negotiated(v) { + return Err(EncodeError::Version); + } w.u8(SERVER_SETUP); - let prefix = prefix_body(w, v)?; - self.encode_inner(w, v)?; + let prefix = w.prefix_u16(); + self.version.encode(w, v)?; + w.slice(&self.parameters); fill_body(w, prefix) } } impl Decode for Server { - /// Decode a server setup message (draft-14 through draft-16 only). fn decode(r: &mut Decoder<'_>, v: Version) -> Result { r.control = Some(crate::stats::Control::Session); + if !negotiated(v) { + return Err(DecodeError::Version); + } let kind = r.u8()?; if kind != SERVER_SETUP { return Err(DecodeError::InvalidValue); } - let mut msg = decode_body(r, v)?; - let version = match SetupVersion::from_version(v) { - SetupVersion::Draft15Plus => v.into(), - SetupVersion::Draft14 | SetupVersion::LiteLegacy => { - coding::Version::decode(&mut msg, v).map_err(DecodeError::complete)? - } - SetupVersion::Modern | SetupVersion::Unsupported => return Err(DecodeError::Version), - }; + let size = r.u16()? as usize; + let mut msg = r.sub(size)?; + let version = coding::Version::decode(&mut msg, v).map_err(DecodeError::complete)?; Ok(Self { version, diff --git a/rs/moq-net/src/test_interop.rs b/rs/moq-net/src/test_interop.rs index f99c3404ed1..4e746e4823d 100644 --- a/rs/moq-net/src/test_interop.rs +++ b/rs/moq-net/src/test_interop.rs @@ -253,10 +253,6 @@ fn ietf_datagram_interop() { use crate::{Timescale, Timestamp}; let all = [ - ietf::Version::Draft14, - ietf::Version::Draft15, - ietf::Version::Draft16, - ietf::Version::Draft17, ietf::Version::Draft18, ietf::Version::Draft19, ietf::Version::Draft20, @@ -264,13 +260,10 @@ fn ietf_datagram_interop() { ietf::Version::Draft22, ]; for (draft, version) in (14..).zip(all) { - let legacy = matches!( - version, - ietf::Version::Draft14 | ietf::Version::Draft15 | ietf::Version::Draft16 - ); + let legacy = false; let mut properties = Vec::new(); let mut w = Encoder::new(&mut properties, version.into()); - ietf::encode_object_time(&mut w, Timestamp::from_millis(1234).unwrap(), Timescale::MILLI, version).unwrap(); + ietf::encode_object_time(&mut w, Timestamp::from_millis(1234).unwrap(), Timescale::MILLI).unwrap(); // What a publisher sends: Object 0, explicit priority, ending its group, stamped. let mut datagrams = vec![ietf::ObjectDatagram { @@ -291,17 +284,15 @@ fn ietf_datagram_interop() { properties: None, body: ietf::DatagramBody::Status(0), }); - if version != ietf::Version::Draft14 { - datagrams.push(ietf::ObjectDatagram { - track_alias: (1 << 40) + 1, - group_id: (1 << 50) + 1, - object_id: Some(1 << 20), - publisher_priority: None, - end_of_group: true, - properties: None, - body: ietf::DatagramBody::Payload(bytes::Bytes::new()), - }); - } + datagrams.push(ietf::ObjectDatagram { + track_alias: (1 << 40) + 1, + group_id: (1 << 50) + 1, + object_id: Some(1 << 20), + publisher_priority: None, + end_of_group: true, + properties: None, + body: ietf::DatagramBody::Payload(bytes::Bytes::new()), + }); if legacy { datagrams.push(ietf::ObjectDatagram { track_alias: 1, @@ -445,10 +436,6 @@ fn ietf_track_status_interop() { use crate::ietf::error::request; let all = [ - ietf::Version::Draft14, - ietf::Version::Draft15, - ietf::Version::Draft16, - ietf::Version::Draft17, ietf::Version::Draft18, ietf::Version::Draft19, ietf::Version::Draft20, @@ -456,10 +443,7 @@ fn ietf_track_status_interop() { ietf::Version::Draft22, ]; for (draft, version) in (14..).zip(all) { - let legacy = matches!( - version, - ietf::Version::Draft14 | ietf::Version::Draft15 | ietf::Version::Draft16 - ); + let legacy = false; let request = |id: u64, namespace: &str, wanted: bool| { let mut buf = Vec::new(); let mut w = Encoder::new(&mut buf, version.into()); @@ -485,17 +469,12 @@ fn ietf_track_status_interop() { for (index, wanted) in [true, false].into_iter().enumerate() { let mut r = Decoder::new(&responses[index], version.into()); - assert_eq!(r.varint().unwrap(), ietf::TrackStatusOk::id(version), "{version}"); + assert_eq!(r.varint().unwrap(), ietf::TrackStatusOk::ID, "{version}"); let ok = ietf::TrackStatusOk::decode(&mut r, version).unwrap(); assert!(r.is_empty(), "{version}: trailing bytes"); let expected = match version { // GROUP_ORDER is a field on draft-14 and a parameter on draft-15. - ietf::Version::Draft14 | ietf::Version::Draft15 => ietf::Properties { - group_order: Some(ietf::GroupOrder::Descending), - ..Default::default() - }, - ietf::Version::Draft16 | ietf::Version::Draft17 => ietf::Properties::default(), // Only draft-20 can carry the opt-out. _ if !wanted && ietf::Filter::is_draft20(version) => ietf::Properties::default(), _ => ietf::Properties { @@ -522,10 +501,7 @@ fn ietf_track_status_interop() { let mut r = Decoder::new(&responses[2], version.into()); let code = match r.varint().unwrap() { - ietf::TRACK_STATUS_ERROR_14 if version == ietf::Version::Draft14 => { - ietf::SubscribeError::decode(&mut r, version).unwrap().error_code - } - id if id == ::ID && version != ietf::Version::Draft14 => { + id if id == ::ID => { ietf::RequestError::decode(&mut r, version).unwrap().error_code } id => panic!("{version}: unexpected refusal type {id}"), diff --git a/rs/moq-net/src/transport/boxed.rs b/rs/moq-net/src/transport/boxed.rs index ee4111bc8e5..a043f5e51f0 100644 --- a/rs/moq-net/src/transport/boxed.rs +++ b/rs/moq-net/src/transport/boxed.rs @@ -105,6 +105,7 @@ pub(crate) struct SendStream(Box); impl SendStream { /// Box a stream, keeping its own send-group type behind the boundary. + #[cfg(test)] pub(crate) fn new(stream: impl poll::SendStream + MaybeSend + MaybeSync) -> Self { Self(Box::new(stream)) } @@ -114,6 +115,7 @@ impl SendStream { pub(crate) struct RecvStream(Box); impl RecvStream { + #[cfg(test)] pub(crate) fn new(stream: impl poll::RecvStream + MaybeSend + MaybeSync) -> Self { Self(Box::new(stream)) } diff --git a/rs/moq-net/src/version.rs b/rs/moq-net/src/version.rs index c99754957fd..4d6f9e1267e 100644 --- a/rs/moq-net/src/version.rs +++ b/rs/moq-net/src/version.rs @@ -6,13 +6,11 @@ use crate::{coding, ietf, lite}; /// The versions of MoQ that are negotiated via SETUP. /// /// Ordered by preference, with the client's preference taking priority. -/// This intentionally includes only SETUP-negotiated versions (Lite02, Lite01, Draft14); -/// Lite03 and newer IETF drafts negotiate via dedicated ALPNs instead. +/// Only moq-lite 01/02 share an ALPN; every other version has its own. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] -pub(crate) const NEGOTIATED: [Version; 3] = [ +pub(crate) const NEGOTIATED: [Version; 2] = [ Version::Lite(lite::Version::Lite02), Version::Lite(lite::Version::Lite01), - Version::Ietf(ietf::Version::Draft14), ]; /// ALPN strings for supported versions, most-preferred first. `ALPNS[0]` is the @@ -32,10 +30,6 @@ pub const ALPNS: &[&str] = &[ ALPN_20, ALPN_19, ALPN_18, - ALPN_17, - ALPN_16, - ALPN_15, - ALPN_14, ]; // ALPN constants @@ -45,17 +39,13 @@ pub(crate) const ALPN_LITE_04: &str = "moq-lite-04"; pub(crate) const ALPN_LITE_05: &str = "moq-lite-05"; pub(crate) const ALPN_LITE_06: &str = "moq-lite-06"; pub(crate) const ALPN_LITE_07_WIP: &str = "moq-lite-07-wip"; -pub(crate) const ALPN_14: &str = "moq-00"; -pub(crate) const ALPN_15: &str = "moqt-15"; -pub(crate) const ALPN_16: &str = "moqt-16"; -pub(crate) const ALPN_17: &str = "moqt-17"; pub(crate) const ALPN_18: &str = "moqt-18"; pub(crate) const ALPN_19: &str = "moqt-19"; pub(crate) const ALPN_20: &str = "moqt-20"; pub(crate) const ALPN_21: &str = "moqt-21"; pub(crate) const ALPN_22: &str = "moqt-22"; -const ALL: [Version; 16] = [ +const ALL: [Version; 12] = [ Version::Lite(lite::Version::Lite07), Version::Lite(lite::Version::Lite06), Version::Lite(lite::Version::Lite05), @@ -68,10 +58,6 @@ const ALL: [Version; 16] = [ Version::Ietf(ietf::Version::Draft20), Version::Ietf(ietf::Version::Draft19), Version::Ietf(ietf::Version::Draft18), - Version::Ietf(ietf::Version::Draft17), - Version::Ietf(ietf::Version::Draft16), - Version::Ietf(ietf::Version::Draft15), - Version::Ietf(ietf::Version::Draft14), ]; /// A MoQ protocol version. @@ -108,10 +94,6 @@ impl Version { Self::Lite(lite::Version::Lite05) => "moq-lite-05", Self::Lite(lite::Version::Lite06) => "moq-lite-06", Self::Lite(lite::Version::Lite07) => "moq-lite-07-wip", - Self::Ietf(ietf::Version::Draft14) => "moq-transport-14", - Self::Ietf(ietf::Version::Draft15) => "moq-transport-15", - Self::Ietf(ietf::Version::Draft16) => "moq-transport-16", - Self::Ietf(ietf::Version::Draft17) => "moq-transport-17", Self::Ietf(ietf::Version::Draft18) => "moq-transport-18", Self::Ietf(ietf::Version::Draft19) => "moq-transport-19", Self::Ietf(ietf::Version::Draft20) => "moq-transport-20", @@ -130,10 +112,6 @@ impl Version { 0xff0dad05 => Some(Self::Lite(lite::Version::Lite05)), 0xff0dad06 => Some(Self::Lite(lite::Version::Lite06)), 0xff0dad07 => Some(Self::Lite(lite::Version::Lite07)), - 0xff00000e => Some(Self::Ietf(ietf::Version::Draft14)), - 0xff00000f => Some(Self::Ietf(ietf::Version::Draft15)), - 0xff000010 => Some(Self::Ietf(ietf::Version::Draft16)), - 0xff000011 => Some(Self::Ietf(ietf::Version::Draft17)), 0xff000012 => Some(Self::Ietf(ietf::Version::Draft18)), 0xff000013 => Some(Self::Ietf(ietf::Version::Draft19)), 0xff000014 => Some(Self::Ietf(ietf::Version::Draft20)), @@ -153,10 +131,6 @@ impl Version { Self::Lite(lite::Version::Lite05) => 0xff0dad05, Self::Lite(lite::Version::Lite06) => 0xff0dad06, Self::Lite(lite::Version::Lite07) => 0xff0dad07, - Self::Ietf(ietf::Version::Draft14) => 0xff00000e, - Self::Ietf(ietf::Version::Draft15) => 0xff00000f, - Self::Ietf(ietf::Version::Draft16) => 0xff000010, - Self::Ietf(ietf::Version::Draft17) => 0xff000011, Self::Ietf(ietf::Version::Draft18) => 0xff000012, Self::Ietf(ietf::Version::Draft19) => 0xff000013, Self::Ietf(ietf::Version::Draft20) => 0xff000014, @@ -177,10 +151,6 @@ impl Version { ALPN_LITE_05 => Some(Self::Lite(lite::Version::Lite05)), ALPN_LITE_06 => Some(Self::Lite(lite::Version::Lite06)), ALPN_LITE_07_WIP => Some(Self::Lite(lite::Version::Lite07)), - ALPN_14 => Some(Self::Ietf(ietf::Version::Draft14)), - ALPN_15 => Some(Self::Ietf(ietf::Version::Draft15)), - ALPN_16 => Some(Self::Ietf(ietf::Version::Draft16)), - ALPN_17 => Some(Self::Ietf(ietf::Version::Draft17)), ALPN_18 => Some(Self::Ietf(ietf::Version::Draft18)), ALPN_19 => Some(Self::Ietf(ietf::Version::Draft19)), ALPN_20 => Some(Self::Ietf(ietf::Version::Draft20)), @@ -199,10 +169,6 @@ impl Version { Self::Lite(lite::Version::Lite04) => ALPN_LITE_04, Self::Lite(lite::Version::Lite03) => ALPN_LITE_03, Self::Lite(lite::Version::Lite01 | lite::Version::Lite02) => ALPN_LITE, - Self::Ietf(ietf::Version::Draft14) => ALPN_14, - Self::Ietf(ietf::Version::Draft15) => ALPN_15, - Self::Ietf(ietf::Version::Draft16) => ALPN_16, - Self::Ietf(ietf::Version::Draft17) => ALPN_17, Self::Ietf(ietf::Version::Draft18) => ALPN_18, Self::Ietf(ietf::Version::Draft19) => ALPN_19, Self::Ietf(ietf::Version::Draft20) => ALPN_20, From 346955b71940a742525ba9ff7b06c5a96a092c9d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 02:50:02 -0700 Subject: [PATCH 03/25] wip(net): compile moq-net tests without drafts 14-17 Co-Authored-By: Claude Opus 5.5 --- rs/moq-net/benches/session.rs | 7 +- .../ietf/draft18-goaway-request-id | Bin 7 -> 7 bytes .../ietf/draft18-invalid-group-order-0 | Bin 13 -> 13 bytes .../ietf/draft18-invalid-group-order-255 | Bin 13 -> 13 bytes .../ietf/draft18-invalid-group-order-3 | Bin 13 -> 13 bytes .../draft18-setup-envelope-truncated-unknown | Bin 12 -> 12 bytes .../ietf/draft18-setup-envelope-unknown-bytes | Bin 12 -> 12 bytes .../draft18-setup-envelope-unknown-varint | Bin 10 -> 10 bytes .../ietf/draft18-setup-truncated-unknown | Bin 8 -> 8 bytes .../ietf/draft18-setup-unknown-bytes | Bin 8 -> 8 bytes .../ietf/draft18-setup-unknown-varint | Bin 6 -> 6 bytes .../draft18-subscribe-empty-namespace-field | Bin 10 -> 10 bytes ...raft18-subscribe-oversized-full-track-name | Bin 4107 -> 4107 bytes .../draft18-subscribe-oversized-namespace | Bin 4108 -> 4108 bytes .../ietf/draft21-invalid-fill-group-order-0 | Bin 16 -> 16 bytes .../ietf/draft21-invalid-fill-group-order-255 | Bin 16 -> 16 bytes .../ietf/draft21-invalid-fill-group-order-3 | Bin 16 -> 16 bytes .../ietf/draft21-invalid-group-order-0 | Bin 13 -> 13 bytes .../ietf/draft21-invalid-group-order-255 | Bin 13 -> 13 bytes .../ietf/draft21-invalid-group-order-3 | Bin 13 -> 13 bytes .../draft21-setup-envelope-truncated-unknown | Bin 12 -> 12 bytes .../ietf/draft21-setup-envelope-unknown-bytes | Bin 12 -> 12 bytes .../draft21-setup-envelope-unknown-varint | Bin 10 -> 10 bytes .../ietf/draft21-setup-truncated-unknown | Bin 8 -> 8 bytes .../ietf/draft21-setup-unknown-bytes | Bin 8 -> 8 bytes .../ietf/draft21-setup-unknown-varint | Bin 6 -> 6 bytes .../draft21-subscribe-empty-namespace-field | Bin 10 -> 10 bytes ...raft21-subscribe-oversized-full-track-name | Bin 4107 -> 4107 bytes .../draft21-subscribe-oversized-namespace | Bin 4108 -> 4108 bytes .../ietf/draft22-invalid-fill-group-order-0 | Bin 16 -> 16 bytes .../ietf/draft22-invalid-fill-group-order-255 | Bin 16 -> 16 bytes .../ietf/draft22-invalid-fill-group-order-3 | Bin 16 -> 16 bytes .../ietf/draft22-invalid-group-order-0 | Bin 13 -> 13 bytes .../ietf/draft22-invalid-group-order-255 | Bin 13 -> 13 bytes .../ietf/draft22-invalid-group-order-3 | Bin 13 -> 13 bytes .../draft22-setup-envelope-truncated-unknown | Bin 12 -> 12 bytes .../ietf/draft22-setup-envelope-unknown-bytes | Bin 12 -> 12 bytes .../draft22-setup-envelope-unknown-varint | Bin 10 -> 10 bytes .../ietf/draft22-setup-truncated-unknown | Bin 8 -> 8 bytes .../ietf/draft22-setup-unknown-bytes | Bin 8 -> 8 bytes .../ietf/draft22-setup-unknown-varint | Bin 6 -> 6 bytes .../ietf/subscribe-absolute-filter | Bin 16 -> 0 bytes rs/moq-net/src/client.rs | 44 +- rs/moq-net/src/coding/writer.rs | 4 +- rs/moq-net/src/fuzz.rs | 91 +- rs/moq-net/src/ietf/auth.rs | 41 +- rs/moq-net/src/ietf/datagram.rs | 19 +- rs/moq-net/src/ietf/group.rs | 31 - rs/moq-net/src/ietf/object.rs | 43 +- rs/moq-net/src/ietf/properties.rs | 84 +- rs/moq-net/src/ietf/publish.rs | 229 +-- rs/moq-net/src/ietf/publish_namespace.rs | 172 +-- rs/moq-net/src/ietf/publisher.rs | 1253 +++------------- rs/moq-net/src/ietf/publisher/tracks.rs | 7 +- rs/moq-net/src/ietf/session.rs | 339 +---- rs/moq-net/src/ietf/subscribe.rs | 448 +----- rs/moq-net/src/ietf/subscribe_tracks.rs | 2 +- rs/moq-net/src/ietf/subscriber.rs | 1321 ++--------------- rs/moq-net/src/ietf/subscriber/tracks.rs | 2 +- rs/moq-net/src/ietf/token.rs | 5 +- rs/moq-net/src/ietf/track.rs | 126 +- rs/moq-net/src/ietf/version.rs | 2 - rs/moq-net/src/lite/compress.rs | 2 +- rs/moq-net/src/lite/ietf.rs | 56 +- rs/moq-net/src/lite/test_transport.rs | 8 - rs/moq-net/src/server.rs | 75 +- rs/moq-net/src/test_interop.rs | 11 +- .../integration/announce_covering_route.rs | 4 - .../tests/integration/announce_to_serve.rs | 4 - rs/moq-net/tests/integration/auth.rs | 52 +- .../tests/integration/broadcast_close.rs | 2 +- .../tests/integration/catalog_acquisition.rs | 2 +- rs/moq-net/tests/integration/compress.rs | 20 +- rs/moq-net/tests/integration/control_stats.rs | 5 +- rs/moq-net/tests/integration/datagram.rs | 11 +- .../tests/integration/dial_split_horizon.rs | 2 +- rs/moq-net/tests/integration/empty_group.rs | 4 +- rs/moq-net/tests/integration/extensions.rs | 13 +- rs/moq-net/tests/integration/fetch_only.rs | 3 - rs/moq-net/tests/integration/finite_relay.rs | 3 +- rs/moq-net/tests/integration/goaway.rs | 7 +- rs/moq-net/tests/integration/group_order.rs | 5 +- .../tests/integration/history_groups.rs | 3 +- rs/moq-net/tests/integration/idle_session.rs | 4 +- .../tests/integration/ietf_conformance.rs | 4 +- .../tests/integration/implementation.rs | 8 +- .../tests/integration/late_lower_group.rs | 2 +- .../tests/integration/lazy_discovery.rs | 4 - rs/moq-net/tests/integration/mesh_withdraw.rs | 2 - rs/moq-net/tests/integration/multi_cdn.rs | 55 +- rs/moq-net/tests/integration/partial_group.rs | 4 - rs/moq-net/tests/integration/quiet_catalog.rs | 2 +- rs/moq-net/tests/integration/range_anchors.rs | 6 +- .../tests/integration/relay_upstream_close.rs | 3 +- rs/moq-net/tests/integration/replacement.rs | 2 +- rs/moq-net/tests/integration/request_caps.rs | 38 +- rs/moq-net/tests/integration/restart.rs | 9 +- rs/moq-net/tests/integration/route_change.rs | 6 +- rs/moq-net/tests/integration/session_close.rs | 17 +- .../tests/integration/subscribe_priority.rs | 4 - .../tests/integration/subscribe_ranges.rs | 2 +- .../tests/integration/subscribe_tracks.rs | 2 +- .../integration/subscription_end_integrity.rs | 3 +- rs/moq-net/tests/integration/synced.rs | 4 +- rs/moq-net/tests/integration/track_tail.rs | 6 +- rs/moq-net/tests/integration/tracks_ietf.rs | 2 +- rs/moq-net/tests/integration/untimed.rs | 14 +- 107 files changed, 624 insertions(+), 4141 deletions(-) delete mode 100644 rs/moq-net/fuzz/regressions/ietf/subscribe-absolute-filter diff --git a/rs/moq-net/benches/session.rs b/rs/moq-net/benches/session.rs index 76f7ec95a94..70c734666c1 100644 --- a/rs/moq-net/benches/session.rs +++ b/rs/moq-net/benches/session.rs @@ -1000,9 +1000,8 @@ fn allocations() { } } -/// Versions for the churn sweep: the one draft with a MAX_REQUEST_ID window, the -/// newest draft without it, and lite. -const CHURN_VERSIONS: [&str; 3] = ["moq-transport-14", "moq-transport-22", "moq-lite-06"]; +/// Versions for the churn sweep: the newest moq-transport draft and lite. +const CHURN_VERSIONS: [&str; 2] = ["moq-transport-22", "moq-lite-06"]; /// Sessions on one relay, each holding `held` subscriptions open while it churns one more. #[derive(Clone, Copy)] @@ -1156,7 +1155,7 @@ impl IdleRoom { fn idle(c: &mut Criterion) { let mut group = c.benchmark_group("session_idle"); - for version in ["moq-lite-06", "moq-transport-16", "moq-transport-22"] { + for version in ["moq-lite-06", "moq-transport-22"] { for sessions in [1, 64, 1024] { for consumers in [0, 1, 8] { group.throughput(Throughput::Elements(sessions as u64)); diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-goaway-request-id b/rs/moq-net/fuzz/regressions/ietf/draft18-goaway-request-id index ca1272f1f39f7fd3aeece66d2ef1fed522127348..2abb874d90663394f44cd9b127bf7a4151e521a3 100644 GIT binary patch literal 7 OcmZQzU|?ooU<3dF6aWPP literal 7 OcmZQ!U|?ooU<3dFFaQYv diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-invalid-group-order-0 b/rs/moq-net/fuzz/regressions/ietf/draft18-invalid-group-order-0 index 59c326743c0815898a31079cd77f4e54a1e897cf..b87d0aa307b28bbdc0343bc248ebdbbc6173cf52 100644 GIT binary patch literal 13 UcmZSJX5eICWK3jCVpL)P00k5P{r~^~ literal 13 UcmZP(V&G(8WK3jCVpL)P00o=@2><{9 diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-invalid-group-order-255 b/rs/moq-net/fuzz/regressions/ietf/draft18-invalid-group-order-255 index ffe8e6b9d8169d2d13521e584865bb66eaa1f670..7e9a314a706e135ebd01d37e8fd0369c74e69558 100644 GIT binary patch literal 13 UcmZSJX5eICWK3jCVpRGM00t8Q{Qv*} literal 13 UcmZP(V&G(8WK3jCVpRGM00x@^2mk;8 diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-invalid-group-order-3 b/rs/moq-net/fuzz/regressions/ietf/draft18-invalid-group-order-3 index 12ed52ca6284a49a3e7bdf079c9f77f5f000c735..264b409f72123dfe84076a98810ea9fde82b7174 100644 GIT binary patch literal 13 UcmZSJX5eICWK3jCVpL)V00kET0RR91 literal 13 UcmZP(V&G(8WK3jCVpL)V00o}`3;+NC diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-setup-envelope-truncated-unknown b/rs/moq-net/fuzz/regressions/ietf/draft18-setup-envelope-truncated-unknown index 065e52340956a46f2bc8b2bd919b48f547d4785e..915c20d41c3fce6ea8075c269a54ceb63e3c24d8 100644 GIT binary patch literal 12 TcmZROTF=11rpTDcz?1|43f}>@ literal 12 TcmZS4T+hJ3rpTDcz?1|43$y{s diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-setup-envelope-unknown-bytes b/rs/moq-net/fuzz/regressions/ietf/draft18-setup-envelope-unknown-bytes index 7fc380d97df9890d0fb14ac373b27185190af08a..96f776f5eeeac506c78a7c6fe3fe20073f988a20 100644 GIT binary patch literal 12 TcmZROTF=11rpTDcz?cL83f%#= literal 12 TcmZS4T+hJ3rpTDcz?cL83$g*p diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-setup-envelope-unknown-varint b/rs/moq-net/fuzz/regressions/ietf/draft18-setup-envelope-unknown-varint index 9ff874dd9da88c88d71cd12ee5c0a668bc6b21dd..8e998087af50b9cac6ace45bf6af02bfea51622e 100644 GIT binary patch literal 10 RcmZROTF=11qQJ<&1ONvG0Pz3- literal 10 RcmZS4T+hJ3qQJ<&1ONxL0R#X5 diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-setup-truncated-unknown b/rs/moq-net/fuzz/regressions/ietf/draft18-setup-truncated-unknown index 489e5c30e6afe3924d67051538ee2d9d80c9170e..69f2fc116406a28ab695ef72f40d291fd2ac1bc1 100644 GIT binary patch literal 8 PcmZP&QDjVHU`hf214#h< literal 8 PcmZQkR%A?MU`hf21JVHx diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-setup-unknown-bytes b/rs/moq-net/fuzz/regressions/ietf/draft18-setup-unknown-bytes index d502a4b6bfee6201984212e672b15740a8eee1c2..faf6680ce75a5f899dfd5d6ba94a7a62447968c6 100644 GIT binary patch literal 8 PcmZP&QDjVHU`zr414jV+ literal 8 PcmZQkR%A?MU`zr41JD5u diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-setup-unknown-varint b/rs/moq-net/fuzz/regressions/ietf/draft18-setup-unknown-varint index 8fb114a73cc675a152cae71f4d0adbdd08b77169..8e228f345e273272097ab6860d00ec2ebfe4363e 100644 GIT binary patch literal 6 NcmZP&QD9_X0ssK-05|{u literal 6 NcmZQkR$ydc0ssM0080P> diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-subscribe-empty-namespace-field b/rs/moq-net/fuzz/regressions/ietf/draft18-subscribe-empty-namespace-field index 7f6cb67fee233ce0b72d68e62443a092b6272f83..639915ac8e4dbc41e5456eb8cfc3cdc56009a178 100644 GIT binary patch literal 10 RcmZSJW?*AvWMHgd0004%0F3|u literal 10 RcmZP(VqjxrWMHgd0005}0GI#( diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-subscribe-oversized-full-track-name b/rs/moq-net/fuzz/regressions/ietf/draft18-subscribe-oversized-full-track-name index 4eeee07dc8a6b5b22217b2ac917279b972876acc..85cc10f525ecc669943859fadf5d1fff82976308 100644 GIT binary patch delta 24 fcmeBH=vLrh;1*zKWSqdTQSdweLh_B diff --git a/rs/moq-net/fuzz/regressions/ietf/draft18-subscribe-oversized-namespace b/rs/moq-net/fuzz/regressions/ietf/draft18-subscribe-oversized-namespace index e3445ccec2438288a2f92d64aef0c81fc1aba9d2..d5e41124eaeacb63db4b1821054834943d538b4d 100644 GIT binary patch delta 24 fcmeBC=uzNc;1=LuWSqdbQSb-!WSqdbQSb- literal 10 Rcmd Option<(usize, u8, &[u8])> { @@ -161,7 +161,7 @@ fn datagram(data: &[u8], version: lite::Version) -> bool { let mut any = false; for &negotiated in negotiated { for timescale in [Some(crate::Timescale::MILLI), None] { - for ietf in [None, Some(ietf::Version::Draft14), Some(ietf::Version::Draft22)] { + for ietf in [None, Some(ietf::Version::Draft18), Some(ietf::Version::Draft22)] { for compressed in [false, true] { let framing = lite::Framing { timescale, @@ -223,6 +223,8 @@ pub fn lite_wire(data: &[u8]) -> bool { 19 => datagram(rest, version), 20 => roundtrip::(rest, version), 21 => roundtrip::(rest, version), + 22 => roundtrip::(rest, crate::Version::Lite(version)), + 23 => roundtrip::(rest, crate::Version::Lite(version)), _ => unreachable!("kind is taken modulo LITE_KINDS"), } } @@ -588,47 +590,30 @@ pub fn ietf_wire(data: &[u8]) -> bool { 0 => roundtrip::, _>(rest, version), 1 => roundtrip::, _>(rest, version), 2 => roundtrip::(rest, version), - 3 => roundtrip::, _>(rest, version), - 4 => roundtrip::(rest, version), - 5 => roundtrip::(rest, version), - 6 => roundtrip::, _>(rest, version), - 7 => roundtrip::, _>(rest, version), - 8 => roundtrip::(rest, version), - 9 => roundtrip::, _>(rest, version), - 10 => roundtrip::, _>(rest, version), - 11 => roundtrip::, _>(rest, version), - 12 => roundtrip::, _>(rest, version), - 13 => roundtrip::, _>(rest, version), - 14 => roundtrip::(rest, version), - 15 => roundtrip::, _>(rest, version), - 16 => roundtrip::, _>(rest, version), - 17 => roundtrip::, _>(rest, version), - 18 => roundtrip::, _>(rest, version), - 19 => roundtrip::(rest, version), - 20 => roundtrip::, _>(rest, version), - 21 => roundtrip::(rest, version), - 22 => roundtrip::(rest, version), - 23 => roundtrip::, _>(rest, version), - 24 => roundtrip::, _>(rest, version), - 25 => roundtrip::(rest, version), - 26 => roundtrip::, _>(rest, version), - 27 => roundtrip::, _>(rest, version), - 28 => roundtrip::, _>(rest, version), - 29 => roundtrip::, _>(rest, version), - 30 => roundtrip::(rest, version), - 31 => roundtrip::(rest, version), - 32 => roundtrip::(rest, version), - 33 => roundtrip::, _>(rest, version), - 34 => roundtrip::(rest, version), - 35 => roundtrip::(rest, version), - 36 => roundtrip::(rest, version), - 37 => roundtrip::(rest, version), - 38 => roundtrip::(rest, version), - 39 => roundtrip::(rest, version), - 40 => roundtrip::, _>(rest, version), - 41 => setup::(rest, version, |setup| &setup.parameters), - 42 => setup::(rest, version, |setup| &setup.parameters), - 43 => setup::(rest, version, |setup| &setup.parameters), + 3 => roundtrip::(rest, version), + 4 => roundtrip::, _>(rest, version), + 5 => roundtrip::, _>(rest, version), + 6 => roundtrip::, _>(rest, version), + 7 => roundtrip::, _>(rest, version), + 8 => roundtrip::(rest, version), + 9 => roundtrip::, _>(rest, version), + 10 => roundtrip::, _>(rest, version), + 11 => roundtrip::, _>(rest, version), + 12 => roundtrip::(rest, version), + 13 => roundtrip::(rest, version), + 14 => roundtrip::, _>(rest, version), + 15 => roundtrip::, _>(rest, version), + 16 => roundtrip::, _>(rest, version), + 17 => roundtrip::(rest, version), + 18 => roundtrip::, _>(rest, version), + 19 => roundtrip::(rest, version), + 20 => roundtrip::(rest, version), + 21 => roundtrip::(rest, version), + 22 => roundtrip::(rest, version), + 23 => roundtrip::(rest, version), + 24 => roundtrip::(rest, version), + 25 => roundtrip::, _>(rest, version), + 26 => setup::(rest, version, |setup| &setup.parameters), _ => unreachable!("kind is taken modulo IETF_KINDS"), } } @@ -657,9 +642,9 @@ where /// Decode a varint with whichever codec the selected version uses. /// -/// Byte 0 picks the version, which is the whole point: lite-01 to lite-06 and drafts -/// 14-16 use the QUIC two-bit length tag, while lite-07 and draft-17+ count leading -/// ones, and the two disagree about which byte sequences are even legal. +/// Byte 0 picks the version, which is the whole point: lite-01 to lite-06 use the QUIC +/// two-bit length tag, while lite-07 and the moq-transport drafts count leading ones, and +/// the two disagree about which byte sequences are even legal. /// /// The leading-ones form spans the full `u64`, while the QUIC form stops at /// [`crate::coding::varint::MAX_QUIC`]; a value always re-encodes in the form it was read in. @@ -1072,7 +1057,7 @@ pub fn seeds() -> Vec { for (index, version) in IETF_VERSIONS.iter().enumerate() { let outer = crate::Version::Ietf(*version); let parameters = ietf::Parameters::default().encode_bytes(*version).unwrap(); - let messages = vec![(41, crate::setup::Setup { parameters }.encode_bytes(outer).unwrap())]; + let messages = vec![(26, crate::setup::Setup { parameters }.encode_bytes(outer).unwrap())]; for (kind, message) in messages { let mut data = vec![index as u8, kind]; data.extend(message); @@ -1125,9 +1110,9 @@ pub fn seeds() -> Vec { }); } - // A SUBSCRIBE_OK carrying LARGEST_OBJECT, whose value encoding changed at draft-17 - // from a length-prefixed Location to two bare varints. A uniform fill never lands a - // valid parameter count followed by a Location, so build it from the encoder. + // A SUBSCRIBE_OK carrying LARGEST_OBJECT, whose value is two bare varints. A uniform + // fill never lands a valid parameter count followed by a Location, so build it from + // the encoder. for (index, version) in IETF_VERSIONS.iter().enumerate() { let subscribe_ok = ietf::SubscribeOk { track_alias: 0, @@ -1139,11 +1124,11 @@ pub fn seeds() -> Vec { continue; }; - let mut data = vec![index as u8, 19]; + let mut data = vec![index as u8, 12]; data.extend_from_slice(&encoded); seeds.push(Seed { target: "ietf", - kind: 19, + kind: 12, data, }); } diff --git a/rs/moq-net/src/ietf/auth.rs b/rs/moq-net/src/ietf/auth.rs index ac0090e1093..3f64e212904 100644 --- a/rs/moq-net/src/ietf/auth.rs +++ b/rs/moq-net/src/ietf/auth.rs @@ -565,7 +565,7 @@ mod tests { for version in [Version::Draft19] { for client in [false, true] { let first = if client { 0 } else { 1 }; - let control = Control::new(Some(RequestId(first + 1)), client); + let control = Control::new(client); let present = Present { runtime: crate::time::sim(), stats: Default::default(), @@ -578,15 +578,12 @@ mod tests { id: 0, }; assert!(matches!(present.run(Bytes::new()).await, Error::NoStreamCredit)); - assert_eq!( - control.poll_next_request_id(&kio::Waiter::noop()), - Poll::Ready(RequestId(first)) - ); + assert_eq!(control.next_request_id(), RequestId(first)); } } } - const VERSION: Version = Version::Draft17; + const VERSION: Version = Version::Draft18; fn patterns(texts: &[&str]) -> Patterns { texts.iter().map(|text| Pattern::try_from(*text).unwrap()).collect() @@ -606,8 +603,7 @@ mod tests { got } - /// Every draft that negotiates the extension round-trips the option; the drafts - /// before the unified SETUP carry none. + /// Every draft that negotiates the extension round-trips the option. #[test] fn setup_option_round_trips_on_supported_drafts() { for version in [ @@ -618,15 +614,9 @@ mod tests { Version::Draft22, ] { let mut params = super::super::Parameters::default(); - assert_eq!(from_setup(¶ms, version), None); - into_setup(&mut params, version); - assert_eq!(from_setup(¶ms, version), Some(true), "{version:?}"); - } - for version in [Version::Draft15] { - let mut params = super::super::Parameters::default(); - into_setup(&mut params, version); - assert_eq!(params.get_varint(super::super::ParameterVarInt::Auth), None); - assert_eq!(from_setup(¶ms, version), None, "{version:?}"); + assert_eq!(from_setup(¶ms), None); + into_setup(&mut params); + assert_eq!(from_setup(¶ms), Some(true), "{version:?}"); } } @@ -636,7 +626,7 @@ mod tests { fn only_one_negotiates() { let mut params = super::super::Parameters::default(); params.set_varint(super::super::ParameterVarInt::Auth, 0); - assert_eq!(from_setup(¶ms, VERSION), Some(false)); + assert_eq!(from_setup(¶ms), Some(false)); } #[test] @@ -705,21 +695,6 @@ mod tests { assert!(matches!(err, DecodeError::InvalidValue), "{err:?}"); } - #[test] - fn older_drafts_have_no_auth() { - for version in [Version::Draft15] { - let msg = Auth { - request_id: RequestId(0), - token: Bytes::new(), - }; - assert!(matches!(encode_msg(&msg, version), Err(EncodeError::Version))); - assert!(matches!( - Auth::decode_msg(&mut Decoder::new(&[0, 0], version.into()), version), - Err(DecodeError::Version) - )); - } - } - /// A reply with bytes past its message is refused, like any other framed message. #[test] fn replies_with_trailing_bytes_are_refused() { diff --git a/rs/moq-net/src/ietf/datagram.rs b/rs/moq-net/src/ietf/datagram.rs index 595322665dd..431ff8030ea 100644 --- a/rs/moq-net/src/ietf/datagram.rs +++ b/rs/moq-net/src/ietf/datagram.rs @@ -96,8 +96,6 @@ impl crate::model::datagram::Wire { } } - - impl Encode for ObjectDatagram { fn encode(&self, w: &mut Encoder<'_>, _: Version) -> Result<(), EncodeError> { let kind = self.kind(); @@ -203,7 +201,7 @@ mod tests { Ok(datagram) } - const ALL: [Version; 9] = [ + const ALL: [Version; 5] = [ Version::Draft18, Version::Draft19, Version::Draft20, @@ -233,19 +231,6 @@ mod tests { } } - #[test] - fn default_priority_needs_draft15() { - assert!(matches!( - single(None).encode_bytes(Version::Draft14), - Err(EncodeError::InvalidState) - )); - assert!(decode(&[0x0C, 0x01, 0x02], Version::Draft14).is_err()); - - let buf = single(None).encode_bytes(Version::Draft16).unwrap(); - assert_eq!(buf[0], 0x0F); - assert_eq!(decode(&buf, Version::Draft16).unwrap(), single(None)); - } - #[test] fn explicit_object_id_and_status() { let datagram = ObjectDatagram { @@ -312,7 +297,7 @@ mod tests { // PROPERTIES and ZERO_OBJECT_ID, alias 1, group 2, priority 0, empty block. let bytes = [0x05, 0x01, 0x02, 0x00, 0x00, b'x']; assert!(matches!( - ObjectDatagram::decode_slice(&bytes, Version::Draft16), + ObjectDatagram::decode_slice(&bytes, Version::Draft18), Err(DecodeError::InvalidValue) )); } diff --git a/rs/moq-net/src/ietf/group.rs b/rs/moq-net/src/ietf/group.rs index 14231136233..223c200c323 100644 --- a/rs/moq-net/src/ietf/group.rs +++ b/rs/moq-net/src/ietf/group.rs @@ -376,22 +376,6 @@ mod tests { assert_eq!(decoded.scale(), Timescale::MICRO); } - /// No Timescale rides along with the object; the track declared it once. - #[test] - fn test_object_time_omits_the_timescale() { - let ts = Timestamp::new(96_000, Timescale::MILLI).unwrap(); - let buf = encode_time(ts, Timescale::MILLI, Version::Draft16); - assert_eq!(varints(&buf, Version::Draft16), [PROP_TIMESTAMP, 96_000]); - } - - /// Draft-14/15 write absolute property types rather than deltas. - #[test] - fn test_object_time_legacy_uses_absolute_types() { - let ts = Timestamp::new(96_000, Timescale::MILLI).unwrap(); - let buf = encode_time(ts, Timescale::MILLI, Version::Draft15); - assert_eq!(varints(&buf, Version::Draft15), [PROP_TIMESTAMP, 96_000]); - } - /// An object-scope Timescale (which LOC permits) is ignored: the track's units apply. #[test] fn test_object_time_ignores_an_object_scope_timescale() { @@ -578,21 +562,6 @@ mod tests { } } - /// The bit arrived in draft-18. Earlier drafts carry no such signal, so a group there - /// is taken at its word rather than read as starting partway through. - #[test] - fn older_drafts_have_no_first_object_signal() { - for version in [Version::Draft15] { - let flags = GroupFlags { - first_object: false, - ..Default::default() - }; - let encoded = flags.encode().unwrap(); - assert_eq!(encoded & GroupFlags::FIRST_OBJECT_BIT, 0, "{version}"); - assert!(GroupFlags::decode(encoded).unwrap().first_object, "{version}"); - } - } - /// Draft-19 makes no changes to the subgroup header wire format, so the flags /// byte must be byte-identical to draft-18 on both encode and decode. #[test] diff --git a/rs/moq-net/src/ietf/object.rs b/rs/moq-net/src/ietf/object.rs index 78d23c5f063..1073d0430b3 100644 --- a/rs/moq-net/src/ietf/object.rs +++ b/rs/moq-net/src/ietf/object.rs @@ -510,15 +510,6 @@ mod tests { .unwrap(), bytes ); - assert!(matches!( - write( - &decoded.properties, - Some(Version::Draft22), - decoded.timestamp, - milli - ), - Err(Error::Unsupported) - )); // Its units cannot follow the egress's, or vanish with them, without rewriting the block. for timescale in [Some(Timescale::MICRO), None] { assert!(matches!( @@ -685,30 +676,18 @@ mod tests { fn a_status_object_keeps_its_timestamp() { let milli = Some(Timescale::MILLI); let list = Bytes::from_static(&[0x10, 7, 0x3e, 1]); - let framed = read(&list, milli).unwrap(); + let framed = read(&list, Version::Draft18, milli).unwrap(); assert_eq!(framed.timestamp, Some(Timestamp::new(7, Timescale::MILLI).unwrap())); - let status = unframed(&list, milli).unwrap(); + let status = unframed(&list, Version::Draft18, milli).unwrap(); assert_eq!( - write(&status, Some(Version::Draft14), None, None).unwrap(), + write(&status, Some(Version::Draft18), None, Version::Draft18, None).unwrap(), list.to_vec() ); // Over the lite hop, only a status block may hold it. let block = status.block.unwrap().bytes; - assert!(check(&block, milli, false).is_ok()); - assert!(check(&block, milli, true).is_err()); - } - - /// A draft-14 block crosses the lite hop in ascending order, as ingress sorts it, so - /// one whose absolute IDs descend is malformed. Its Immutable Properties may not ascend. - #[test] - fn a_draft14_block_ascends() { - let milli = Some(Timescale::MILLI); - assert!(check(&Bytes::from_static(&[0x02, 1, 0x20, 1]), milli, true).is_ok()); - assert!(check(&Bytes::from_static(&[0x02, 1, 0x02, 2]), milli, true).is_ok()); - assert!(check(&Bytes::from_static(&[0x20, 1, 0x02, 1]), milli, true).is_err()); - let sealed = Bytes::from_static(&[0x02, 1, 0x0b, 4, 0x20, 1, 0x02, 1]); - assert!(check(&sealed, milli, true).is_ok()); + assert!(check(&block, Version::Draft18, milli, false).is_ok()); + assert!(check(&block, Version::Draft18, milli, true).is_err()); } /// Draft-19 assigns 0x06 to SUBGROUP_DELIVERY_TIMEOUT, so it cannot cross that boundary. @@ -801,7 +780,7 @@ mod tests { for bytes in [&[0x0b, 2, 0x0b, 0][..], &[0x0b, 0, 0, 0][..], &[0x0b, 1, 2][..]] { assert!(read(&Bytes::copy_from_slice(bytes), Version::Draft22, None).is_err()); } - // Draft-18 permits this nonminimal seven-byte value; draft-17 cannot read it. + // Draft-18 permits this nonminimal seven-byte value. let bytes = Bytes::from_static(&[0x0b, 8, 2, 0xfc, 0, 0, 0, 0, 0, 1]); let decoded = read(&bytes, Version::Draft18, None).unwrap(); assert_eq!( @@ -815,16 +794,6 @@ mod tests { .unwrap(), bytes ); - assert!(read(&bytes, None).is_err()); - assert!(matches!( - write( - &decoded.properties, - Some(Version::Draft18), - decoded.timestamp, - None - ), - Err(Error::Unsupported) - )); } } diff --git a/rs/moq-net/src/ietf/properties.rs b/rs/moq-net/src/ietf/properties.rs index 1c9234a8337..a6880097180 100644 --- a/rs/moq-net/src/ietf/properties.rs +++ b/rs/moq-net/src/ietf/properties.rs @@ -428,7 +428,7 @@ mod tests { fn test_skip_empty_properties() { let mut buf = bytes::Bytes::new(); assert_eq!( - crate::coding::decode_buf(&mut buf, Properties::decode).unwrap(), + crate::coding::decode_buf(&mut buf, Version::Draft18, Properties::decode).unwrap(), Properties::default() ); } @@ -437,10 +437,10 @@ mod tests { fn test_skip_varint_property() { // Even type (0x02 = DELIVERY_TIMEOUT), varint value let mut buf = Vec::new(); - Encoder::new(&mut buf, Version::Draft17.into()).varint(0x02u64).unwrap(); // delta type - Encoder::new(&mut buf, Version::Draft17.into()).varint(5000u64).unwrap(); // value + Encoder::new(&mut buf, Version::Draft18.into()).varint(0x02u64).unwrap(); // delta type + Encoder::new(&mut buf, Version::Draft18.into()).varint(5000u64).unwrap(); // value let mut bytes = bytes::Bytes::from(buf); - crate::coding::decode_buf(&mut bytes, Properties::decode).unwrap(); + crate::coding::decode_buf(&mut bytes, Version::Draft18, Properties::decode).unwrap(); assert!(bytes.is_empty()); } @@ -448,11 +448,11 @@ mod tests { fn test_skip_bytes_property() { // Odd type (0x0B = IMMUTABLE_PROPERTIES), length-prefixed let mut buf = Vec::new(); - Encoder::new(&mut buf, Version::Draft17.into()).varint(0x0Bu64).unwrap(); // delta type - Encoder::new(&mut buf, Version::Draft17.into()).varint(3u64).unwrap(); // length + Encoder::new(&mut buf, Version::Draft18.into()).varint(0x0Bu64).unwrap(); // delta type + Encoder::new(&mut buf, Version::Draft18.into()).varint(3u64).unwrap(); // length buf.extend_from_slice(&[0x03, 0x01, 0x2A]); // sealed type 0x03, one byte let mut bytes = bytes::Bytes::from(buf); - crate::coding::decode_buf(&mut bytes, Properties::decode).unwrap(); + crate::coding::decode_buf(&mut bytes, Version::Draft18, Properties::decode).unwrap(); assert!(bytes.is_empty()); } @@ -460,18 +460,18 @@ mod tests { fn test_skip_multiple_properties() { let mut buf = Vec::new(); // First: type 0x02 (even), varint value - Encoder::new(&mut buf, Version::Draft17.into()).varint(0x02u64).unwrap(); - Encoder::new(&mut buf, Version::Draft17.into()).varint(1000u64).unwrap(); + Encoder::new(&mut buf, Version::Draft18.into()).varint(0x02u64).unwrap(); + Encoder::new(&mut buf, Version::Draft18.into()).varint(1000u64).unwrap(); // Second: delta = 0x02 → abs type 0x04 (even), varint value - Encoder::new(&mut buf, Version::Draft17.into()).varint(0x02u64).unwrap(); - Encoder::new(&mut buf, Version::Draft17.into()).varint(2000u64).unwrap(); + Encoder::new(&mut buf, Version::Draft18.into()).varint(0x02u64).unwrap(); + Encoder::new(&mut buf, Version::Draft18.into()).varint(2000u64).unwrap(); // Third: delta = 0x07 → abs type 0x0B (odd), length-prefixed - Encoder::new(&mut buf, Version::Draft17.into()).varint(0x07u64).unwrap(); - Encoder::new(&mut buf, Version::Draft17.into()).varint(2u64).unwrap(); + Encoder::new(&mut buf, Version::Draft18.into()).varint(0x07u64).unwrap(); + Encoder::new(&mut buf, Version::Draft18.into()).varint(2u64).unwrap(); buf.extend_from_slice(&[0x02, 0x2A]); // sealed type 0x02 = 42 let mut bytes = bytes::Bytes::from(buf); - crate::coding::decode_buf(&mut bytes, Properties::decode).unwrap(); + crate::coding::decode_buf(&mut bytes, Version::Draft18, Properties::decode).unwrap(); assert!(bytes.is_empty()); } @@ -531,43 +531,6 @@ mod tests { assert!(crate::coding::decode_buf(&mut bytes, Version::Draft18, Properties::decode).is_err()); } - /// Draft-16 carries the same block under the name Track Extensions. We don't write one - /// there, but a peer that does must be understood rather than faulted. - #[test] - fn test_decodes_draft16_track_extensions() { - let mut buf = Vec::new(); - Encoder::new(&mut buf, Version::Draft16.into()).varint(0x22u64).unwrap(); - Encoder::new(&mut buf, Version::Draft16.into()).varint(2u64).unwrap(); - - let mut bytes = bytes::Bytes::from(buf); - let properties = crate::coding::decode_buf(&mut bytes, Properties::decode).unwrap(); - assert_eq!(properties.group_order, Some(GroupOrder::Descending)); - assert!(bytes.is_empty()); - } - - /// Drafts 14-16 cannot carry TIMESCALE, so the block (and any Timestamp that would - /// depend on it) stays off the wire. Draft-17 is the first version that writes it. - #[test] - fn drafts_14_through_16_write_no_timescale() { - let properties = Properties { - unknown: Default::default(), - dynamic_groups: false, - compress: false, - max_cache_duration: None, - timescale: Some(Timescale::MILLI), - priority: None, - group_order: None, - }; - - for version in [Version::Draft15] { - let mut buf = Vec::new(); - properties - .encode(&mut Encoder::new(&mut buf, version.into()), version) - .unwrap(); - assert_eq!(!buf.is_empty(), Properties::sends_timescale(version), "{version}"); - } - } - /// The group order property is delta-encoded against the timescale that precedes it, /// so it has to survive the timescale being absent. #[test] @@ -640,25 +603,6 @@ mod tests { .unwrap(); assert_eq!(buf, [0x30, 0x01]); - // Draft 16 defines DYNAMIC_GROUPS but not TIMESCALE, and drafts 14 and 15 have no block - // (draft 15 carries DYNAMIC_GROUPS as a SUBSCRIBE_OK parameter instead). - let both = Properties { - dynamic_groups: true, - compress: false, - timescale: Some(Timescale::MILLI), - ..Default::default() - }; - for (version, expected) in [ - (Version::Draft14, &[][..]), - (Version::Draft15, &[][..]), - (Version::Draft16, &[0x30, 0x01][..]), - ] { - let mut buf = Vec::new(); - both.encode(&mut Encoder::new(&mut buf, version.into()), version) - .unwrap(); - assert_eq!(buf, expected, "{version}"); - } - let mut buf = Vec::new(); Properties::default() .encode(&mut Encoder::new(&mut buf, Version::Draft22.into()), Version::Draft22) diff --git a/rs/moq-net/src/ietf/publish.rs b/rs/moq-net/src/ietf/publish.rs index 1a604d1d8a5..c64a6fa60c3 100644 --- a/rs/moq-net/src/ietf/publish.rs +++ b/rs/moq-net/src/ietf/publish.rs @@ -101,7 +101,6 @@ The endpoint does not support the PUBLISH method. UNINTERESTED (0x4): The namespace or track is not of interest to the endpoint. - */ use std::borrow::Cow; @@ -420,147 +419,6 @@ mod tests { crate::coding::decode_buf(&mut buf, version, M::decode_msg) } - #[test] - fn test_publish_v14_round_trip() { - let msg = Publish { - request_id: RequestId(1), - track_namespace: Path::new("test/ns"), - track_name: "video".into(), - track_alias: 42, - largest_location: Some(Location { group: 10, object: 5 }), - forward: true, - properties: Properties { - group_order: Some(GroupOrder::Descending), - ..Default::default() - }, - subscriber_priority: None, - group_order: None, - filter: None, - }; - - let encoded = encode_message(&msg, Version::Draft14); - let decoded: Publish = decode_message(&encoded, Version::Draft14).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.track_namespace.as_str(), "test/ns"); - assert_eq!(decoded.track_name, "video"); - assert_eq!(decoded.track_alias, 42); - assert_eq!(decoded.largest_location, Some(Location { group: 10, object: 5 })); - assert!(decoded.forward); - } - - #[test] - fn test_publish_v15_round_trip() { - let msg = Publish { - request_id: RequestId(1), - track_namespace: Path::new("test/ns"), - track_name: "video".into(), - track_alias: 42, - largest_location: Some(Location { group: 10, object: 5 }), - forward: true, - properties: Properties { - group_order: Some(GroupOrder::Descending), - ..Default::default() - }, - subscriber_priority: None, - group_order: None, - filter: None, - }; - - let encoded = encode_message(&msg, Version::Draft15); - let decoded: Publish = decode_message(&encoded, Version::Draft15).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.track_namespace.as_str(), "test/ns"); - assert_eq!(decoded.track_name, "video"); - assert_eq!(decoded.track_alias, 42); - assert_eq!(decoded.largest_location, Some(Location { group: 10, object: 5 })); - assert!(decoded.forward); - } - - #[test] - fn test_publish_ok_v14_round_trip() { - let msg = PublishOk { - forward: true, - subscriber_priority: Some(128), - group_order: Some(GroupOrder::Descending), - filter: Some(Filter::NextObject), - new_group: None, - }; - - let encoded = encode_message(&msg, Version::Draft14); - let decoded: PublishOk = decode_message(&encoded, Version::Draft14).unwrap(); - - assert_eq!(decoded.request_id, Some(RequestId(7))); - assert!(decoded.forward); - assert_eq!(decoded.subscriber_priority, Some(128)); - } - - #[test] - fn test_publish_ok_v15_round_trip() { - let msg = PublishOk { - forward: true, - subscriber_priority: Some(128), - group_order: Some(GroupOrder::Descending), - filter: Some(Filter::NextObject), - new_group: None, - }; - - let encoded = encode_message(&msg, Version::Draft15); - let decoded: PublishOk = decode_message(&encoded, Version::Draft15).unwrap(); - - assert_eq!(decoded.request_id, Some(RequestId(7))); - assert!(decoded.forward); - assert_eq!(decoded.subscriber_priority, Some(128)); - } - - #[test] - fn test_publish_v17_round_trip() { - let msg = Publish { - request_id: RequestId(1), - track_namespace: Path::new("test/ns"), - track_name: "video".into(), - track_alias: 42, - largest_location: Some(Location { group: 10, object: 5 }), - forward: true, - properties: Properties { - group_order: Some(GroupOrder::Descending), - ..Default::default() - }, - subscriber_priority: None, - group_order: None, - filter: None, - }; - - let encoded = encode_message(&msg, Version::Draft17); - let decoded: Publish = decode_message(&encoded, Version::Draft17).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.track_namespace.as_str(), "test/ns"); - assert_eq!(decoded.track_name, "video"); - assert_eq!(decoded.track_alias, 42); - assert_eq!(decoded.largest_location, Some(Location { group: 10, object: 5 })); - assert!(decoded.forward); - } - - #[test] - fn test_publish_ok_v17_round_trip() { - let msg = PublishOk { - forward: true, - subscriber_priority: Some(128), - group_order: Some(GroupOrder::Descending), - filter: Some(Filter::NextObject), - new_group: None, - }; - - let encoded = encode_message(&msg, Version::Draft17); - let decoded: PublishOk = decode_message(&encoded, Version::Draft17).unwrap(); - - assert_eq!(decoded.request_id, None); - assert!(decoded.forward); - assert_eq!(decoded.subscriber_priority, Some(128)); - } - fn publish_with_param(version: Version, key: u64, value: u64) -> Vec { let mut buf = Vec::new(); let w = &mut Encoder::new(&mut buf, version.into()); @@ -583,25 +441,23 @@ mod tests { buf } - /// SUBSCRIBER_PRIORITY is known in draft-16 but not defined for PUBLISH until draft-20. - /// OBJECT_DELIVERY_TIMEOUT is legal on PUBLISH again from draft-20. + /// SUBSCRIBER_PRIORITY is not defined for PUBLISH until draft-20. OBJECT_DELIVERY_TIMEOUT + /// is legal on PUBLISH again from draft-20. #[test] fn publish_parameters_follow_the_draft() { - decode_message::(&publish_with_param(Version::Draft16, 0x20, 1), Version::Draft16) - .expect("draft-16 ignores SUBSCRIBER_PRIORITY"); assert!(decode_message::(&publish_with_param(Version::Draft18, 0x20, 1), Version::Draft18).is_err()); decode_message::(&publish_with_param(Version::Draft20, 0x02, 1), Version::Draft20) .expect("draft-20 accepts OBJECT_DELIVERY_TIMEOUT"); } - /// PUBLISH_OK carries the delivery timeout through draft-19, and NEW_GROUP_REQUEST from - /// draft-16 through draft-19. Draft-20 no longer carries FORWARD. + /// PUBLISH_OK carries the delivery timeout and NEW_GROUP_REQUEST through draft-19. + /// Draft-20 no longer carries FORWARD. #[test] fn publish_ok_parameters_follow_the_draft() { decode_message::(&publish_ok_with_param(Version::Draft19, 0x02, 1), Version::Draft19) .expect("draft-19 accepts DELIVERY_TIMEOUT"); - decode_message::(&publish_ok_with_param(Version::Draft16, 0x32, 1), Version::Draft16) - .expect("draft-16 accepts NEW_GROUP_REQUEST"); + decode_message::(&publish_ok_with_param(Version::Draft19, 0x32, 1), Version::Draft19) + .expect("draft-19 accepts NEW_GROUP_REQUEST"); assert!( decode_message::(&publish_ok_with_param(Version::Draft20, 0x10, 0), Version::Draft20).is_err() ); @@ -772,73 +628,6 @@ mod tests { assert_eq!(decoded.properties.group_order, Some(GroupOrder::Descending)); } - /// Draft-15 is the last version that takes it as a message parameter, and has no track - /// properties to put it in. - #[test] - fn test_publish_v15_group_order_is_a_parameter() { - let msg = Publish { - request_id: RequestId(1), - track_namespace: Path::new("ns"), - track_name: "video".into(), - track_alias: 42, - largest_location: None, - forward: true, - properties: Properties { - unknown: Default::default(), - dynamic_groups: false, - compress: false, - max_cache_duration: None, - timescale: None, - priority: None, - group_order: Some(GroupOrder::Descending), - }, - subscriber_priority: None, - group_order: None, - filter: None, - }; - - #[rustfmt::skip] - let expected = vec![ - 1, // request id - 1, // namespace tuple count - 2, b'n', b's', // "ns" - 5, b'v', b'i', b'd', b'e', b'o', // "video" - 42, // track alias - 2, // two message parameters - 0x10, 0x01, // FORWARD = true - 0x22, 0x02, // GROUP_ORDER = descending - ]; - assert_eq!(encode_message(&msg, Version::Draft15), expected); - - let decoded: Publish = decode_message(&expected, Version::Draft15).unwrap(); - assert_eq!(decoded.properties.group_order, Some(GroupOrder::Descending)); - } - - /// Draft-18 drops the required_request_id_delta varint (#1615). - /// For Publish (#1D) the field is a single zero varint = 1 byte. - #[test] - fn test_publish_v18_is_one_byte_shorter_than_v17() { - let msg = Publish { - request_id: RequestId(1), - track_namespace: Path::new("test/ns"), - track_name: "video".into(), - track_alias: 42, - largest_location: None, - forward: true, - properties: Properties { - group_order: Some(GroupOrder::Descending), - ..Default::default() - }, - subscriber_priority: None, - group_order: None, - filter: None, - }; - - let v17 = encode_message(&msg, Version::Draft17); - let v18 = encode_message(&msg, Version::Draft18); - assert_eq!(v17.len(), v18.len() + 1); - } - #[test] fn test_publish_ok_v18_round_trip() { let msg = PublishOk { @@ -852,7 +641,6 @@ mod tests { let encoded = encode_message(&msg, Version::Draft18); let decoded: PublishOk = decode_message(&encoded, Version::Draft18).unwrap(); - assert_eq!(decoded.request_id, None); assert!(decoded.forward); assert_eq!(decoded.subscriber_priority, Some(128)); } @@ -860,10 +648,9 @@ mod tests { /// Draft-18 lets PUBLISH_OK carry EXPIRES; it is ignored rather than rejected. #[test] fn test_publish_ok_ignores_expires() { - let bytes = [0x04, 0x01, 0x08, 0x05]; + let bytes = [0x01, 0x08, 0x05]; - let decoded: PublishOk = decode_message(&bytes, Version::Draft16).unwrap(); - assert_eq!(decoded.request_id, Some(RequestId(4))); + let decoded: PublishOk = decode_message(&bytes, Version::Draft18).unwrap(); assert!(decoded.forward); } diff --git a/rs/moq-net/src/ietf/publish_namespace.rs b/rs/moq-net/src/ietf/publish_namespace.rs index 8e2a352c76f..ed6b60d0049 100644 --- a/rs/moq-net/src/ietf/publish_namespace.rs +++ b/rs/moq-net/src/ietf/publish_namespace.rs @@ -1,6 +1,5 @@ //! IETF moq-transport-14 publish namespace messages - use crate::{Epoch, Path, coding::*, ietf::RequestId}; use super::Message; @@ -267,103 +266,12 @@ mod tests { authorization_token: None, }; - let encoded = encode_message(&msg, Version::Draft14); - let decoded: PublishNamespace = decode_message(&encoded, Version::Draft14).unwrap(); + let encoded = encode_message(&msg, Version::Draft18); + let decoded: PublishNamespace = decode_message(&encoded, Version::Draft18).unwrap(); assert_eq!(decoded.track_namespace.as_str(), "test/broadcast"); } - #[test] - fn test_announce_error() { - let msg = PublishNamespaceError { - request_id: RequestId(1), - error_code: 404, - reason_phrase: "Unauthorized".into(), - }; - - let encoded = encode_message(&msg, Version::Draft14); - let decoded: PublishNamespaceError = decode_message(&encoded, Version::Draft14).unwrap(); - - assert_eq!(decoded.error_code, 404); - assert_eq!(decoded.reason_phrase, "Unauthorized"); - } - - #[test] - fn test_unannounce_v14() { - let msg = PublishNamespaceDone { - track_namespace: Path::new("old/stream"), - request_id: RequestId(0), - }; - - let encoded = encode_message(&msg, Version::Draft14); - let decoded: PublishNamespaceDone = decode_message(&encoded, Version::Draft14).unwrap(); - - assert_eq!(decoded.track_namespace.as_str(), "old/stream"); - } - - #[test] - fn test_unannounce_v16() { - let msg = PublishNamespaceDone { - track_namespace: Path::default(), - request_id: RequestId(42), - }; - - let encoded = encode_message(&msg, Version::Draft16); - let decoded: PublishNamespaceDone = decode_message(&encoded, Version::Draft16).unwrap(); - - assert_eq!(decoded.request_id, RequestId(42)); - } - - #[test] - fn test_announce_cancel_v14() { - let msg = PublishNamespaceCancel { - track_namespace: Path::new("canceled"), - request_id: RequestId(0), - error_code: 1, - reason_phrase: "Shutdown".into(), - }; - - let encoded = encode_message(&msg, Version::Draft14); - let decoded: PublishNamespaceCancel = decode_message(&encoded, Version::Draft14).unwrap(); - - assert_eq!(decoded.track_namespace.as_str(), "canceled"); - assert_eq!(decoded.error_code, 1); - assert_eq!(decoded.reason_phrase, "Shutdown"); - } - - #[test] - fn test_announce_cancel_v16() { - let msg = PublishNamespaceCancel { - track_namespace: Path::default(), - request_id: RequestId(7), - error_code: 1, - reason_phrase: "Shutdown".into(), - }; - - let encoded = encode_message(&msg, Version::Draft16); - let decoded: PublishNamespaceCancel = decode_message(&encoded, Version::Draft16).unwrap(); - - assert_eq!(decoded.request_id, RequestId(7)); - assert_eq!(decoded.error_code, 1); - assert_eq!(decoded.reason_phrase, "Shutdown"); - } - - #[test] - fn test_publish_namespace_v17_round_trip() { - let msg = PublishNamespace { - request_id: RequestId(5), - track_namespace: Path::new("v17/broadcast"), - params: Default::default(), - authorization_token: None, - }; - - let encoded = encode_message(&msg, Version::Draft17); - let decoded: PublishNamespace = decode_message(&encoded, Version::Draft17).unwrap(); - - assert_eq!(decoded.request_id, RequestId(5)); - assert_eq!(decoded.track_namespace.as_str(), "v17/broadcast"); - } - #[test] fn test_publish_namespace_v18_round_trip() { let msg = PublishNamespace { @@ -380,50 +288,6 @@ mod tests { assert_eq!(decoded.track_namespace.as_str(), "v18/broadcast"); } - #[test] - fn test_publish_namespace_done_v18_rejected() { - let msg = PublishNamespaceDone { - track_namespace: Path::default(), - request_id: RequestId(42), - }; - - let mut buf = Vec::new(); - assert!( - msg.encode_msg(&mut Encoder::new(&mut buf, Version::Draft18.into()), Version::Draft18) - .is_err() - ); - } - - #[test] - fn test_publish_namespace_done_v17_rejected() { - let msg = PublishNamespaceDone { - track_namespace: Path::default(), - request_id: RequestId(42), - }; - - let mut buf = Vec::new(); - assert!( - msg.encode_msg(&mut Encoder::new(&mut buf, Version::Draft17.into()), Version::Draft17) - .is_err() - ); - } - - #[test] - fn test_publish_namespace_cancel_v17_rejected() { - let msg = PublishNamespaceCancel { - track_namespace: Path::default(), - request_id: RequestId(7), - error_code: 1, - reason_phrase: "Shutdown".into(), - }; - - let mut buf = Vec::new(); - assert!( - msg.encode_msg(&mut Encoder::new(&mut buf, Version::Draft17.into()), Version::Draft17) - .is_err() - ); - } - fn hop_path(ids: &[u64]) -> cluster::HopPath { let hops: Vec = ids.iter().map(|&id| crate::Hop::new(id).unwrap()).collect(); cluster::HopPath::new(crate::Hops::try_from(hops).unwrap()) @@ -465,36 +329,6 @@ mod tests { assert_eq!(decoded.apply(&held), rerouted); } - /// Drafts 14-15 have no update for an advertisement, and draft-16's names the one it - /// updates. - #[test] - fn update_names_the_advertisement_on_draft_16() { - let mut msg = PublishNamespaceUpdate { - request_id: RequestId(2), - namespace_request_id: None, - hops: None, - cost: None, - authorization_token: Some(super::super::token::RequestToken(crate::setup::Token { - kind: 0, - value: b"jwt".to_vec(), - })), - }; - for version in [Version::Draft15] { - let mut buf = Vec::new(); - assert!( - msg.encode_msg(&mut Encoder::new(&mut buf, version.into()), version) - .is_err(), - "{version:?}" - ); - } - assert!(decode_message::(&[0x02, 0x00], Version::Draft15).is_err()); - - msg.namespace_request_id = Some(RequestId(1)); - let decoded: PublishNamespaceUpdate = - decode_message(&encode_message(&msg, Version::Draft16), Version::Draft16).unwrap(); - assert_eq!(decoded, msg); - } - #[test] fn test_announce_rejects_parameters() { #[rustfmt::skip] @@ -504,7 +338,7 @@ mod tests { 0x01, // INVALID: num_params = 1 ]; - let result: Result = decode_message(&invalid_bytes, Version::Draft14); + let result: Result = decode_message(&invalid_bytes, Version::Draft18); assert!(result.is_err()); } } diff --git a/rs/moq-net/src/ietf/publisher.rs b/rs/moq-net/src/ietf/publisher.rs index c20a1423022..f2742cafc25 100644 --- a/rs/moq-net/src/ietf/publisher.rs +++ b/rs/moq-net/src/ietf/publisher.rs @@ -4288,10 +4288,6 @@ impl TrackServe { let end = range.end.map_or(Bound::Unbounded, |end| Bound::Included(end.group)); let track = track.with_range((Bound::Included(floor), end)); let datagrams = session.max_datagram_size() > 0; - // A Timestamp whose units were not declared is worse than none. Drafts 14-16 - // never write TIMESCALE, whatever the caller passed in, so datagrams and group - // objects on those drafts go out unstamped. - let timescale = timescale; let dynamic = track.info().refresh.is_some(); let priority = kio::Producer::new(track.subscription().priority); let publisher = track.info().priority; @@ -5688,6 +5684,7 @@ mod group_priority_test { rank_in(&queue, priority, subscribe), group.consume(), Some(Timescale::MILLI), + Version::Draft18, GroupSlice::default(), ) }); @@ -5729,49 +5726,6 @@ mod group_priority_test { assert_eq!(header.publisher_priority, 127); } - /// Drafts 14-16 cannot send TIMESCALE, so a group served with a track timescale still - /// carries no Timestamp. Draft-17 declares the units and stamps the object. - #[moq_clock::test] - async fn drafts_14_through_16_send_no_timestamp_without_timescale() { - for version in [Version::Draft15] { - let stamped = ietf::Properties::sends_timescale(version); - let (header, mut buf) = serve_group(version, Some(Timescale::MILLI)).await; - assert_eq!(header.flags.has_extensions, stamped, "{version}"); - assert_eq!( - crate::coding::decode_varint(&mut buf, version).unwrap(), - 0, - "{version}: object id" - ); - if stamped { - let ext = crate::coding::decode_buf(&mut buf, version, |r, _| Ok(r.bytes()?.to_vec())).unwrap(); - let mut ext = bytes::Bytes::from(ext); - assert!( - crate::coding::decode_buf(&mut ext, version, |r, v| { - ietf::decode_object_time(r, Timescale::MILLI, v) - }) - .unwrap() - .is_some(), - "{version}: Timestamp missing" - ); - } - assert_eq!( - crate::coding::decode_varint(&mut buf, version).unwrap(), - b"hello".len() as u64, - "{version}" - ); - assert_eq!(&buf[..b"hello".len()], b"hello"); - } - - // Opting out of the timescale stays unstamped on a draft that could carry one. - let (header, mut buf) = serve_group(Version::Draft17, None).await; - assert!(!header.flags.has_extensions); - assert_eq!(crate::coding::decode_varint(&mut buf, Version::Draft17).unwrap(), 0); - assert_eq!( - crate::coding::decode_varint(&mut buf, Version::Draft17).unwrap(), - b"hello".len() as u64 - ); - } - /// Drive `serve` beside a sibling that only counts its turns, until `done`, checking every /// poll stays under `most` by `progress` and leaves the sibling its turn. /// Each task-set pass returns to the executor so its children's yields reach the task. @@ -5839,6 +5793,7 @@ mod group_priority_test { crate::driver::Transport::new(session.clone()), subscriber, RequestId(0), + Version::Draft18, ServeRange::default(), None, ); @@ -5888,6 +5843,7 @@ mod group_priority_test { crate::driver::Transport::new(session.clone()), subscriber, RequestId(0), + Version::Draft18, ServeRange::default(), None, ); @@ -5943,6 +5899,7 @@ mod group_priority_test { crate::driver::Transport::new(session.clone()), subscriber, RequestId(0), + Version::Draft18, ServeRange::default(), None, ); @@ -5990,6 +5947,7 @@ mod group_priority_test { crate::driver::Transport::new(session.clone()), subscriber, RequestId(0), + Version::Draft18, ServeRange::default(), None, ); @@ -6006,39 +5964,6 @@ mod group_priority_test { .await; } - /// Serve one group at `version` with `timescale` and return the header plus what follows it. - async fn serve_group(version: Version, timescale: Option) -> (ietf::GroupHeader, bytes::Bytes) { - let log = crate::lite::test_transport::Log::default(); - let session = SinkSession::new(log.clone()); - - let track = track::Producer::new( - std::sync::Arc::new(crate::broadcast::Info::default()), - "test", - crate::track::Info::timed(), - ); - let subscriber = track.subscribe(None); - - let mut group = track.append_group().unwrap(); - group.write_frame(crate::Timestamp::ZERO, b"hello".as_slice()).unwrap(); - group.finish().unwrap(); - track.finish().unwrap(); - - let mut serve = TrackServe::new( - crate::driver::Transport::new(session.clone()), - subscriber, - RequestId(0), - version, - ServeRange::default(), - timescale, - ); - kio::wait(|waiter| serve.poll(waiter)).await.unwrap(); - - let written = log.writes.lock().unwrap().clone(); - let mut buf = bytes::Bytes::from(written); - let header = crate::coding::decode_buf(&mut buf, version, ietf::GroupHeader::decode).expect("a group header"); - (header, buf) - } - /// Serve one group of a track with `info` and decode the subgroup header it opens with. async fn serve_group_header(info: track::Info) -> ietf::GroupHeader { let log = crate::lite::test_transport::Log::default(); @@ -6056,6 +5981,7 @@ mod group_priority_test { crate::driver::Transport::new(session.clone()), subscriber, RequestId(0), + Version::Draft18, ServeRange::default(), Some(Timescale::MILLI), ); @@ -6063,7 +5989,7 @@ mod group_priority_test { let written = log.writes.lock().unwrap().clone(); let mut buf = bytes::Bytes::from(written); - crate::coding::decode_buf(&mut buf, ietf::GroupHeader::decode).expect("a group header") + crate::coding::decode_buf(&mut buf, Version::Draft18, ietf::GroupHeader::decode).expect("a group header") } /// `hang::catalog::PRIORITY` isn't reachable from `moq-net` (hang depends on it, not the @@ -6483,6 +6409,7 @@ mod subscribe_cursor_test { crate::driver::Transport::new(session.clone()), subscriber, RequestId(1), + Version::Draft18, ServeRange::default(), Some(Timescale::MILLI), ); @@ -6685,7 +6612,7 @@ mod serve_tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, version, @@ -7262,18 +7189,13 @@ mod serve_tests { .expect("the track's error ends the subscription"); assert!(matches!(served, Served::Track(Err(Error::Unsupported))), "{served:?}"); assert_eq!( - served.status(Version::Draft16).0, + served.status().0, ietf::PublishDoneStatus::InternalError ); assert_eq!( - Served::UpdateFailed(Error::Unsupported).status(Version::Draft16).0, + Served::UpdateFailed(Error::Unsupported).status().0, ietf::PublishDoneStatus::UpdateFailed ); - // Draft 14 answers no update, so its PUBLISH_DONE carries the refusal itself. - assert_eq!( - Served::UpdateFailed(Error::Unauthorized).status(Version::Draft14).0, - ietf::PublishDoneStatus::Unauthorized - ); // A revocation bridged from upstream, however it was received, is still one. for err in [ Error::Unauthorized, @@ -7281,7 +7203,7 @@ mod serve_tests { Error::Stream(crate::StreamError::Unauthorized), ] { assert_eq!( - Served::Track(Err(err)).status(Version::Draft16).0, + Served::Track(Err(err)).status().0, ietf::PublishDoneStatus::Unauthorized ); } @@ -7361,110 +7283,6 @@ mod serve_tests { ); } - /// A draft 14-16 update framed as the adapter routes it onto its subscription. - /// - /// Drafts 15 and 16 carry only SUBSCRIBER_PRIORITY and FORWARD here, written by hand so - /// the bytes are pinned. - fn legacy_update(version: Version, priority: u8, forward: bool) -> Vec { - const UPDATE_ID: u8 = 10; - let body = match version { - // Draft 16 delta-encodes keys: SUBSCRIBER_PRIORITY (0x20) follows FORWARD (0x10). - _ => vec![UPDATE_ID, 0, 2, 0x10, forward as u8, 0x20, priority], - }; - let mut frame = vec![ietf::SubscribeUpdate::ID as u8, 0, body.len() as u8]; - frame.extend(body); - frame - } - - /// Drafts 14-16 deliver an update on the subscription's own virtual stream. It - /// reprices the subscription, which keeps serving and owes no PUBLISH_DONE, and only - /// UNSUBSCRIBE ends it. - #[moq_clock::test] - async fn legacy_subscription_update_keeps_the_subscription() { - for version in [Version::Draft15] { - let h = serve(version); - let session = ScriptedSession::new(legacy_update(version, 10, true)); - let mut stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) - .await - .unwrap(); - let mut goaways = super::super::request_stream::Messages::new( - crate::driver::Transport::new(h.session.clone()), - h.publisher.control.client(), - ); - let mut serving = TrackServe::new( - crate::driver::Transport::new(h.session.clone()), - h.track.subscribe(None), - RequestId(0), - version, - ServeRange::default(), - None, - ); - let mut finished = false; - let mut run = std::pin::pin!(h.publisher.run_subscription( - &mut stream, - &mut serving, - &mut finished, - &mut goaways, - async {}, - None, - )); - assert!(futures::poll!(run.as_mut()).is_pending(), "{version}: update ended it"); - assert_eq!(h.track.subscription().unwrap().priority, 117, "{version}"); - - // Drafts 15 and 16 answer with REQUEST_OK naming the update; draft 14 answers nothing. - let answered = occurrences(&session.log, &[ietf::RequestOk::ID as u8, 0, 2, 10, 0]); - assert_eq!(answered, usize::from(true), "{version}"); - - let mut unsubscribe = vec![ietf::Unsubscribe::ID as u8]; - ietf::Unsubscribe { - request_id: RequestId(0), - } - .encode(&mut Encoder::new(&mut unsubscribe, version.into()), version) - .unwrap(); - session.push(&unsubscribe); - assert!( - matches!(futures::poll!(run.as_mut()), Poll::Ready(None)), - "{version}: UNSUBSCRIBE cancels" - ); - } - } - - /// A draft 14-16 FORWARD=0 update pauses the subscription, leaving the answer, which names - /// the update on drafts 15 and 16, to the caller once it stopped forwarding. - #[moq_clock::test] - async fn a_legacy_update_pauses_the_subscription() { - for version in [Version::Draft15] { - let h = serve(version); - let session = ScriptedSession::new(legacy_update(version, 10, false)); - let mut stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) - .await - .unwrap(); - let mut goaways = super::super::request_stream::Messages::new( - crate::driver::Transport::new(h.session.clone()), - h.publisher.control.client(), - ); - let mut serving = TrackServe::new( - crate::driver::Transport::new(h.session.clone()), - h.track.subscribe(None), - RequestId(0), - version, - ServeRange::default(), - None, - ); - let mut finished = false; - let served = h - .publisher - .run_subscription(&mut stream, &mut serving, &mut finished, &mut goaways, async {}, None) - .await; - assert!( - matches!(served, Some(Served::Paused(true))), - "{version}: {served:?}" - ); - let written = session.log.writes.lock().unwrap().clone(); - assert!(written.is_empty(), "{version}: {written:?}"); - } - } - /// A REQUEST_UPDATE with Request ID `id` carrying only FORWARD, framed for the stream of /// the [`subscribe`] it updates. fn forward_update(version: Version, id: u64, forward: bool) -> Vec { @@ -7552,7 +7370,7 @@ mod serve_tests { /// the resume joined rather than refused as the pause left it. #[moq_clock::test] async fn a_joining_fetch_after_a_resume_is_served() { - let version = Version::Draft16; + let version = Version::Draft18; let h = serve(version); let mut group = h.track.create_group(group::Info { sequence: 0 }).unwrap(); group.write_frame(timestamp(), b"head".as_slice()).unwrap(); @@ -7828,7 +7646,7 @@ mod serve_tests { // left as it is. _ => vec![2], }; - body.extend(token_params(version, tokens)); + body.extend(token_params(tokens)); let mut frame = vec![ietf::SubscribeUpdate::ID as u8, 0, body.len() as u8]; frame.extend(body); frame @@ -7889,9 +7707,9 @@ mod serve_tests { } } - /// A parameter block of AUTHORIZATION TOKENs. Draft-16 on codes each key as a delta from - /// the one before, so a repeat is 0. - fn token_params(version: Version, tokens: &[&[u8]]) -> Vec { + /// A parameter block of AUTHORIZATION TOKENs. Each key is coded as a delta from the one + /// before, so a repeat is 0. + fn token_params(tokens: &[&[u8]]) -> Vec { let mut params = vec![tokens.len() as u8]; for (i, token) in tokens.iter().enumerate() { let delta = i > 0; @@ -7910,11 +7728,11 @@ mod serve_tests { /// a single token is answered as usual. #[moq_clock::test] async fn a_request_with_repeated_or_several_tokens_is_refused_alone() { - for (version, malformed) in [(Version::Draft14, 0x10), (Version::Draft22, 0x4)] { + for (version, malformed) in [ (Version::Draft22, 0x4)] { let request = |tokens: &[&[u8]]| { let mut body = track_status("room", true, version); assert_eq!(body.pop(), Some(0), "{version}"); - body.extend(token_params(version, tokens)); + body.extend(token_params(tokens)); body }; @@ -7959,9 +7777,7 @@ mod serve_tests { async fn a_repeated_or_several_update_tokens_are_refused() { let other: &[u8] = &[0x03, 0x00, 0xBB]; for (version, tokens, code) in [ - (Version::Draft16, [TOKEN, TOKEN], 0x4), (Version::Draft22, [TOKEN, TOKEN], 0x4), - (Version::Draft16, [TOKEN, other], 0x3), (Version::Draft22, [TOKEN, other], 0x3), ] { let h = serve(version); @@ -8531,7 +8347,7 @@ mod serve_tests { } } - const ALL_VERSIONS: [Version; 9] = [ + const ALL_VERSIONS: [Version; 5] = [ Version::Draft18, Version::Draft19, Version::Draft20, @@ -8725,7 +8541,7 @@ mod serve_tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, version, @@ -9047,119 +8863,58 @@ mod serve_tests { } } - /// A draft-16/17 SUBSCRIBE_NAMESPACE gets NAMESPACE whenever it asks for namespaces, - /// and one asking for PUBLISH alone is refused, since we never send PUBLISH. Draft-18 - /// has no Subscribe Options and always asks for namespaces. + /// A SUBSCRIBE_NAMESPACE always asks for namespaces, and gets NAMESPACE for each. #[moq_clock::test] - async fn subscribe_namespace_honors_subscribe_options() { - use ietf::SubscribeOptions::{Both, Namespace, Publish}; - - for (version, options, namespaces) in [ - (Version::Draft16, Some(Publish), false), - (Version::Draft16, Some(Namespace), true), - (Version::Draft16, Some(Both), true), - (Version::Draft17, Some(Publish), false), - (Version::Draft17, Some(Namespace), true), - (Version::Draft17, Some(Both), true), - (Version::Draft18, None, true), - ] { - let h = serve(version); - settle().await; - - let mut body = Vec::new(); - let mut w = Encoder::new(&mut body, version.into()); - let id = match options { - Some(subscribe_options) => { - ietf::SubscribeNamespaceLegacy { - request_id: RequestId(REQUEST_ID), - namespace: crate::Path::new(""), - subscribe_options, - hidden: false, - } - .encode_msg(&mut w, version) - .unwrap(); - ietf::SubscribeNamespaceLegacy::ID - } - None => { - ietf::SubscribeNamespace { - request_id: RequestId(REQUEST_ID), - namespace: crate::Path::new(""), - hidden: false, - } - .encode_msg(&mut w, version) - .unwrap(); - ietf::SubscribeNamespace::ID - } - }; + async fn subscribe_namespace_gets_namespaces() { + let version = Version::Draft18; + let h = serve(version); + settle().await; - let stream = Stream::open(&mut crate::driver::Transport::new(h.session.clone()), version) - .await - .unwrap(); - let mark = h.log.writes.lock().unwrap().len(); - let task = h - .publisher - .clone() - .handle_stream(id, ietf::Body(bytes::Bytes::from(body)), stream, false) - .unwrap_or_else(|e| panic!("{version}: the request closed the session: {e}")); - let mut task = std::pin::pin!(task); - let mut done = false; - for _ in 0..100 { - done = futures::poll!(task.as_mut()).is_ready(); - if done || occurrences(&h.log, b"room") > 0 { - break; - } - settle().await; - } + let mut body = Vec::new(); + let mut w = Encoder::new(&mut body, version.into()); + ietf::SubscribeNamespace { + request_id: RequestId(REQUEST_ID), + namespace: crate::Path::new(""), + hidden: false, + } + .encode_msg(&mut w, version) + .unwrap(); - let wire = h.log.writes.lock().unwrap()[mark..].to_vec(); - let mut buf = Decoder::new(&wire, version.into()); - let label = format!("{version} {options:?}"); - if namespaces { - assert!(!done, "{label}: the subscription ended"); - assert_eq!(buf.varint().unwrap(), ietf::RequestOk::ID, "{label}"); - ietf::RequestOk::decode(&mut buf, version).unwrap(); - assert_eq!(buf.varint().unwrap(), ietf::Namespace::ID, "{label}"); - assert_eq!( - ietf::Namespace::decode(&mut buf, version).unwrap().suffix.as_str(), - "room", - "{label}" - ); - } else { - assert!(done, "{label}: the refusal did not finish"); - assert_eq!(buf.varint().unwrap(), ietf::RequestError::ID, "{label}"); - let err = ietf::RequestError::decode(&mut buf, version).unwrap(); - assert_eq!(err.error_code, 0x3, "{label}: NOT_SUPPORTED"); - assert_eq!( - err.request_id, - (false).then_some(RequestId(REQUEST_ID)), - "{label}" - ); + let stream = Stream::open(&mut crate::driver::Transport::new(h.session.clone()), version) + .await + .unwrap(); + let mark = h.log.writes.lock().unwrap().len(); + let task = h + .publisher + .clone() + .handle_stream( + ietf::SubscribeNamespace::ID, + ietf::Body(bytes::Bytes::from(body)), + stream, + false, + ) + .unwrap_or_else(|e| panic!("{version}: the request closed the session: {e}")); + let mut task = std::pin::pin!(task); + let mut done = false; + for _ in 0..100 { + done = futures::poll!(task.as_mut()).is_ready(); + if done || occurrences(&h.log, b"room") > 0 { + break; } - assert!(buf.is_empty(), "{label}: trailing bytes"); + settle().await; } - // Undefined Subscribe Options close the session: dispatch returns an error. - #[rustfmt::skip] - let cases: [(Version, &[u8]); 2] = [ - // Request ID, empty namespace, Subscribe Options 0x03, no parameters. - (Version::Draft16, &[0x2B, 0x00, 0x03, 0x00]), - // Draft-17 adds the Required Request ID delta after the Request ID. - (Version::Draft17, &[0x2B, 0x00, 0x00, 0x03, 0x00]), - ]; - for (version, body) in cases { - let h = serve(version); - let stream = Stream::open(&mut crate::driver::Transport::new(h.session.clone()), version) - .await - .unwrap(); - let body = ietf::Body(bytes::Bytes::copy_from_slice(body)); - assert!( - h.publisher - .clone() - .handle_stream(ietf::SubscribeNamespaceLegacy::ID, body, stream, false) - .is_err(), - "{version}: Subscribe Options 0x03 kept the session open" - ); - } + let wire = h.log.writes.lock().unwrap()[mark..].to_vec(); + let mut buf = Decoder::new(&wire, version.into()); + assert!(!done, "the subscription ended"); + assert_eq!(buf.varint().unwrap(), ietf::RequestOk::ID); + ietf::RequestOk::decode(&mut buf, version).unwrap(); + assert_eq!(buf.varint().unwrap(), ietf::Namespace::ID); + assert_eq!( + ietf::Namespace::decode(&mut buf, version).unwrap().suffix.as_str(), + "room" + ); + assert!(buf.is_empty(), "trailing bytes"); } /// The draft's canonical current-group join: a Next Object subscription plus a @@ -9447,24 +9202,17 @@ mod serve_tests { const FETCH_ID: RequestId = RequestId(0x2C); /// Every draft that still carries a joining FETCH. - const JOINING_DRAFTS: [Version; 6] = [ + const JOINING_DRAFTS: [Version; 2] = [ Version::Draft18, Version::Draft19, ]; - fn invalid_joining_request_id(version: Version) -> u64 { - 0x32 - } + const INVALID_JOINING_REQUEST_ID: u64 = 0x32; - fn invalid_range(version: Version) -> u64 { - 0x11 - } + const INVALID_RANGE: u64 = 0x11; - /// The registry's "does not exist" value: draft-14 numbers it 0x4 on FETCH_ERROR and - /// draft-15 moved it to 0x10. - fn does_not_exist(version: Version) -> u64 { - 0x10 - } + /// The registry's "does not exist" value. + const DOES_NOT_EXIST: u64 = 0x10; /// Yield so the broadcast registered by `serve` is visible to the lookup. async fn settle() { @@ -9572,16 +9320,8 @@ mod serve_tests { assert_eq!(id, ietf::FetchOk::ID, "{version}: not a FETCH_OK"); let ok = crate::coding::decode_buf(&mut buf, version, ietf::FetchOk::decode).unwrap(); - assert_eq!( - ok.request_id, - None, - "{version}: wrong request id" - ); assert!(!ok.end_of_track, "{version}: the track has not ended"); - let properties = match ietf::Properties::sends_timescale(version) { - false => ietf::Properties::default(), - true => track_properties(&track::Info::timed(), true, version, true).unwrap(), - }; + let properties = track_properties(&track::Info::timed(), true, version, true).unwrap(); assert_eq!(ok.properties, properties, "{version}: wrong properties"); assert_eq!( ok.end_location, @@ -9602,15 +9342,8 @@ mod serve_tests { .request_id, FETCH_ID ); - // Drafts 14-16 never declared TIMESCALE, so the prefix must not carry a Timestamp. - let stamped = ietf::Properties::sends_timescale(version); for (index, payload) in payloads.iter().enumerate() { - let flags = match (index == 0, stamped) { - (true, true) => 0x3c, - (true, false) => 0x1c, - (false, true) => 0x20, - (false, false) => 0, - }; + let flags = if index == 0 { 0x3c } else { 0x20 }; assert_eq!( crate::coding::decode_varint(&mut buf, version).unwrap(), flags, @@ -9621,16 +9354,9 @@ mod serve_tests { assert_eq!(crate::coding::decode_varint(&mut buf, version).unwrap(), 0); assert_eq!(bytes::Buf::try_get_u8(&mut buf).unwrap(), 0); } - let properties = if stamped { - crate::coding::decode_buf(&mut buf, version, |r, _| Ok(r.bytes()?.to_vec())).unwrap() - } else { - Vec::new() - }; - if stamped { - assert!(!properties.is_empty(), "{version}: missing Timestamp"); - } else { - assert!(properties.is_empty(), "{version}: Timestamp without TIMESCALE"); - } + let properties = + crate::coding::decode_buf(&mut buf, version, |r, _| Ok(r.bytes()?.to_vec())).unwrap(); + assert!(!properties.is_empty(), "{version}: missing Timestamp"); let size = crate::coding::decode_varint(&mut buf, version).unwrap() as usize; assert_eq!(size, payload.len()); assert_eq!(buf.split_to(size).as_ref(), *payload); @@ -9651,7 +9377,7 @@ mod serve_tests { /// Dispatch order must not depend on which request task gets polled first. #[moq_clock::test] async fn a_joining_fetch_waits_for_its_dispatched_subscription() { - let version = Version::Draft17; + let version = Version::Draft18; let mut h = serve(version); publish_groups(&mut h, 5); settle().await; @@ -9738,7 +9464,7 @@ mod serve_tests { crate::coding::decode_buf(&mut response, version, ietf::RequestError::decode) .unwrap() .error_code, - invalid_joining_request_id(version) + INVALID_JOINING_REQUEST_ID ); assert!(response.is_empty()); assert!(h.publisher.joins.read().is_empty()); @@ -9747,7 +9473,7 @@ mod serve_tests { #[moq_clock::test] async fn a_joining_fetch_times_out_an_unresolved_subscription() { - let version = Version::Draft17; + let version = Version::Draft18; let h = serve(version); let stream = Stream::open(&mut crate::driver::Transport::new(h.session.clone()), version) .await @@ -9775,7 +9501,7 @@ mod serve_tests { /// refusal carries that error: refused before FETCH_OK, and never reset. #[moq_clock::test] async fn a_joining_fetch_refuses_a_missing_prefix_before_fetch_ok() { - let version = Version::Draft17; + let version = Version::Draft18; let h = serve(version); let mut group = h.track.create_group(group::Info { sequence: 5 }).unwrap(); group.start_at(1).unwrap(); @@ -9801,7 +9527,7 @@ mod serve_tests { crate::coding::decode_buf(&mut response, version, ietf::RequestError::decode) .unwrap() .error_code, - does_not_exist(version) + DOES_NOT_EXIST ); assert!(response.is_empty()); assert!(h.log.resets().is_empty()); @@ -9823,7 +9549,7 @@ mod serve_tests { crate::coding::decode_buf(&mut buf, version, ietf::RequestError::decode) .unwrap() .error_code, - invalid_joining_request_id(version) + INVALID_JOINING_REQUEST_ID ); assert!(buf.is_empty()); assert!(h.log.resets().is_empty()); @@ -9887,7 +9613,7 @@ mod serve_tests { crate::coding::decode_buf(&mut buf, version, ietf::RequestError::decode) .unwrap() .error_code, - invalid_range(version) + INVALID_RANGE ); assert!(buf.is_empty()); assert!(h.log.resets().is_empty()); @@ -9895,7 +9621,7 @@ mod serve_tests { } /// Every draft, each carrying a standalone FETCH or draft-20's filtered one. - const FETCH_DRAFTS: [Version; 9] = [ + const FETCH_DRAFTS: [Version; 5] = [ Version::Draft18, Version::Draft19, Version::Draft20, @@ -10139,27 +9865,21 @@ let ietf::FetchObject::Object { group, object, .. } = let (ok, objects) = fetch_answer(run_fetch(&h, range, GroupOrder::Ascending, wanted).await, version); assert_eq!(objects.len(), 1, "{version}"); - let expected = match ietf::Properties::sends_timescale(version) { - // Drafts 14-16 write no properties, as their SUBSCRIBE_OK does not. - false => ietf::Properties::default(), - true => track_properties(&info, wanted, version, true).unwrap(), - }; + let expected = track_properties(&info, wanted, version, true).unwrap(); assert_eq!(ok.properties, expected, "{version} wanted={wanted}"); - if ietf::Properties::sends_timescale(version) { - let mut properties = Vec::new(); - ietf::encode_object_time( - &mut Encoder::new(&mut properties, version.into()), - stamp, - info.timescale.expect("a timed track"), - ) - .unwrap(); - assert_eq!( - occurrences(&h.log, &properties), - 1, - "{version} wanted={wanted}: object unstamped" - ); - } + let mut properties = Vec::new(); + ietf::encode_object_time( + &mut Encoder::new(&mut properties, version.into()), + stamp, + info.timescale.expect("a timed track"), + ) + .unwrap(); + assert_eq!( + occurrences(&h.log, &properties), + 1, + "{version} wanted={wanted}: object unstamped" + ); } } } @@ -10273,7 +9993,7 @@ let ietf::FetchObject::Object { group, object, .. } = assert_eq!(occurrences(&h.log, &block[1..]), 1); // Draft 17 forbids the 7-byte varints draft 18 allows, and draft 16 has QUIC varints. - for version in [Version::Draft17] { + for version in [Version::Draft18] { let h = serve_track(version, info.clone()); let stream = Stream::open(&mut crate::driver::Transport::new(h.session.clone()), version) .await @@ -10513,7 +10233,7 @@ let ietf::FetchObject::Object { group, object, .. } = match largest { Largest::Finished | Largest::Live | Largest::IdleFinished => assert_eq!( fetch_refusal(buf, version), - invalid_range(version), + INVALID_RANGE, "{version} {largest:?} start={start}" ), _ => { @@ -10549,7 +10269,7 @@ let ietf::FetchObject::Object { group, object, .. } = GroupOrder::Ascending, ) .await; - assert_eq!(fetch_refusal(buf, version), invalid_range(version), "{version}"); + assert_eq!(fetch_refusal(buf, version), INVALID_RANGE, "{version}"); } } @@ -10739,7 +10459,7 @@ let ietf::FetchObject::Object { group, object, .. } = publish_pairs(&mut h, 1, None); h.track.finish().unwrap(); settle().await; - for (start, code) in [(0, 0x3), (maximum, does_not_exist(version))] { + for (start, code) in [(0, 0x3), (maximum, DOES_NOT_EXIST)] { let buf = standalone_fetch( &h, Location { @@ -10796,7 +10516,7 @@ let ietf::FetchObject::Object { group, object, .. } = .await; assert_eq!( fetch_refusal(buf, version), - does_not_exist(version), + DOES_NOT_EXIST, "{version}: group {missing}" ); } @@ -10892,7 +10612,7 @@ let ietf::FetchObject::Object { group, object, .. } = crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, version, @@ -10987,7 +10707,7 @@ let ietf::FetchObject::Object { group, object, .. } = crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, version, @@ -11101,7 +10821,7 @@ let ietf::FetchObject::Object { group, object, .. } = crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, version, @@ -11250,7 +10970,7 @@ let ietf::FetchObject::Object { group, object, .. } = crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, version, @@ -11327,7 +11047,6 @@ let ietf::FetchObject::Object { group, object, .. } = let wire = Wire::read(&session.log, version); let sent: Vec = wire.messages.iter().map(|(id, _)| *id).collect(); let expected = match (parked, version) { - (Parked::Resolving, Version::Draft14) => vec![ietf::SubscribeError::ID], (Parked::Resolving, _) => vec![ietf::RequestError::ID], _ => vec![ietf::SubscribeOk::ID, ietf::PublishDone::ID], }; @@ -11346,8 +11065,8 @@ let ietf::FetchObject::Object { group, object, .. } = Live, } - /// Withdraw the SUBSCRIBE on `request` as `version` cancels one: UNSUBSCRIBE through - /// draft 16, a FIN on drafts 17 and 18, a reset after. + /// Withdraw the SUBSCRIBE on `request` as `version` cancels one: a FIN on draft 18, a + /// reset after. fn withdraw(request: &ScriptedSession, version: Version) { use crate::lite::test_transport::Close; match version { @@ -11376,7 +11095,7 @@ let ietf::FetchObject::Object { group, object, .. } = crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, version, @@ -11526,7 +11245,7 @@ let ietf::FetchObject::Object { group, object, .. } = let buf = standalone_fetch(&h, location, location, GroupOrder::Ascending).await; assert_eq!( fetch_refusal(buf, version), - does_not_exist(version), + DOES_NOT_EXIST, "{version}: newest={newest}" ); } @@ -11595,7 +11314,7 @@ let ietf::FetchObject::Object { group, object, .. } = /// An absolute joining FETCH starting past the subscription's group names an empty range. #[moq_clock::test] async fn an_absolute_joining_fetch_past_the_subscription_is_refused() { - let version = Version::Draft16; + let version = Version::Draft18; let mut h = serve(version); publish_pairs(&mut h, 3, None); settle().await; @@ -11635,7 +11354,7 @@ let ietf::FetchObject::Object { group, object, .. } = .await .unwrap(); let buf = bytes::Bytes::from(h.log.writes.lock().unwrap()[mark..].to_vec()); - assert_eq!(fetch_refusal(buf, version), invalid_range(version)); + assert_eq!(fetch_refusal(buf, version), INVALID_RANGE); } /// A fill against an empty track has an empty range: no fetch stream is owed. @@ -11684,18 +11403,12 @@ let ietf::FetchObject::Object { group, object, .. } = .largest } - /// Through draft-15 SUBSCRIBE_OK restates the order the subscription is served in; later - /// drafts announce the publisher's default, newest first. + /// SUBSCRIBE_OK announces the publisher's default order, newest first, whatever the + /// subscription asked for. #[moq_clock::test] - async fn subscribe_ok_announces_the_served_order() { - for version in [ - Version::Draft20, - ] { - for (requested, announced) in [ - (GroupOrder::Ascending, GroupOrder::Ascending), - (GroupOrder::Descending, GroupOrder::Descending), - (GroupOrder::Any, GroupOrder::Descending), - ] { + async fn subscribe_ok_announces_the_default_order() { + for version in [Version::Draft18, Version::Draft20] { + for requested in [GroupOrder::Ascending, GroupOrder::Descending, GroupOrder::Any] { let mut h = serve(version); let mut msg = subscribe(Filter::NextObject, None); msg.group_order = requested; @@ -11708,11 +11421,11 @@ let ietf::FetchObject::Object { group, object, .. } = ietf::SubscribeOk::ID ); let ok = crate::coding::decode_buf(&mut buf, version, ietf::SubscribeOk::decode).unwrap(); - let expected = match version { - // Draft-16 has no room for it. - _ => Some(GroupOrder::Descending), - }; - assert_eq!(ok.properties.group_order, expected, "{version:?} {requested:?}"); + assert_eq!( + ok.properties.group_order, + Some(GroupOrder::Descending), + "{version:?} {requested:?}" + ); } } } @@ -11880,7 +11593,7 @@ let ietf::FetchObject::Object { group, object, .. } = crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, version, @@ -11957,9 +11670,8 @@ let ietf::FetchObject::Object { group, object, .. } = log.writes.lock().unwrap().clone() } - /// The wire bytes of one REQUEST_OK answering `request_id`: keyed to it on draft-15/16, - /// unkeyed on draft-17+. - async fn request_ok_bytes(version: Version, request_id: RequestId) -> Vec { + /// The wire bytes of one REQUEST_OK. + async fn request_ok_bytes(version: Version) -> Vec { let log = crate::lite::test_transport::Log::default(); let mut writer = crate::coding::Writer::new( crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), @@ -11979,17 +11691,15 @@ let ietf::FetchObject::Object { group, object, .. } = /// The REQUEST_ERROR a refused renewal writes before the subscription ends: keyed to the /// update's Request ID on draft-15/16, unkeyed on draft-17+, matching /// `write_subscribe_error(.., &Error::Unauthorized, reason)`. - async fn request_error_bytes(version: Version, request_id: RequestId, reason: &str) -> Vec { + async fn request_error_bytes(version: Version, reason: &str) -> Vec { let log = crate::lite::test_transport::Log::default(); let mut writer = crate::coding::Writer::new( crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), version, ); - let request_id = false.then_some(request_id); writer.varint(ietf::RequestError::ID).await.unwrap(); writer .encode(&ietf::RequestError { - request_id, error_code: request::to_code(&Error::Unauthorized, request::Kind::Subscribe, version), reason_phrase: reason.into(), retry_interval: 0, @@ -12085,36 +11795,11 @@ let ietf::FetchObject::Object { group, object, .. } = } } - /// The pre-draft-17 renewal: a SUBSCRIBE_UPDATE carrying a fresh token renews a - /// token-authorized subscription past the old grant's expiry at draft-14/15/16, where the - /// update rides the control-stream adapter. Draft-14 writes no answer (it defines none, and - /// its control stream would read a 0x05 as ending the subscription); draft-15 and draft-16 - /// (section 9.11) write exactly one REQUEST_OK, keyed to the update's own Request ID so the - /// peer matches it to the REQUEST_UPDATE it sent. The adapter's follow-up routing of the - /// update to the subscription's stream is proven by - /// `super::super::adapter::tests::test_classify_subscribe_update_followup`. - #[moq_clock::test] - async fn a_subscribe_update_renewal_is_answered_from_draft_15() { - for version in [Version::Draft15] { - renews(version, true).await; - } - } - - /// A refused renewal ends only its subscription at once (PUBLISH_DONE UPDATE_FAILED, or the - /// refusal itself on draft 14), never the session. The update is answered first: nothing on - /// draft-14, a keyed REQUEST_ERROR to the update's Request ID on draft-15/16. - #[moq_clock::test] - async fn a_refused_subscribe_update_renewal_ends_the_subscription_from_draft_14() { - for version in [Version::Draft15] { - renews(version, false).await; - } - } - /// A pause is answered before a renewal riding it could be verified, so an update carrying /// both fails instead of acknowledging a token nobody checked. #[moq_clock::test] async fn a_pause_carrying_a_token_fails_the_update() { - for version in [Version::Draft16] { + for version in [Version::Draft18] { let (auth, mut requests, _cred) = auth_covering_other(); let h = serve_with_auth(version, auth, subscribe_update_with_token(version, false).await); let rt = h.publisher.runtime.clone(); @@ -12156,145 +11841,6 @@ let ietf::FetchObject::Object { group, object, .. } = } } - async fn renews(version: Version, accept: bool) { - let (auth, mut requests, _cred) = auth_covering_other(); - let h = serve_with_auth(version, auth, subscribe_update_with_token(version, true).await); - let rt = h.publisher.runtime.clone(); - - let mut group = h.track.create_group(group::Info { sequence: 0 }).unwrap(); - group.write_frame(timestamp(), b"frame".as_slice()).unwrap(); - group.finish().unwrap(); - settle().await; - - let stream = Stream::open(&mut crate::driver::Transport::new(h.session.clone()), version) - .await - .unwrap(); - - // The renewal is held until the setup writes have settled, so anything written after - // it is released is the renewal's answer. - let release = std::sync::Arc::new(Release::default()); - let answered = std::sync::Arc::new(AtomicU64::new(0)); - let acceptor = { - let answered = answered.clone(); - let release = release.clone(); - async move { - let mut held = Vec::new(); - let first = requests.next().await.unwrap(); - held.push(first.accept(grant_all_expiring(&rt, Some(60)))); - answered.fetch_add(1, Ordering::Relaxed); - let renewal = requests.next().await.unwrap(); - release.notified().await; - if accept { - held.push(renewal.accept(grant_all_expiring(&rt, None))); - } else { - renewal.reject(crate::SessionError::Unauthorized, "no"); - } - answered.fetch_add(1, Ordering::Relaxed); - std::future::pending::<()>().await - } - }; - let mut acceptor = std::pin::pin!(acceptor); - let mut serving = std::pin::pin!(h.publisher.clone().run_subscribe_stream(stream, token_subscribe())); - - for _ in 0..200 { - let _ = futures::poll!(acceptor.as_mut()); - assert!( - futures::poll!(serving.as_mut()).is_pending(), - "{version:?}: ended during setup" - ); - settle().await; - } - assert_eq!( - answered.load(Ordering::Relaxed), - 1, - "{version:?}: the first token was answered" - ); - let before = h.log.writes.lock().unwrap().len(); - - release.notify_one(); - - if accept { - for _ in 0..200 { - let _ = futures::poll!(acceptor.as_mut()); - assert!( - futures::poll!(serving.as_mut()).is_pending(), - "{version:?}: an accepted renewal keeps serving" - ); - settle().await; - } - assert_eq!( - answered.load(Ordering::Relaxed), - 2, - "{version:?}: the renewal was answered" - ); - // Draft-14 writes nothing; draft-15/16 write exactly one keyed REQUEST_OK to the - // update's own Request ID (0x40), the id the renewing peer waits on. - let written = h.log.writes.lock().unwrap()[before..].to_vec(); - match version { - _ => assert_eq!( - written, - request_ok_bytes(version, RequestId(0x40)).await, - "{version:?}: one keyed REQUEST_OK to the update's request id" - ), - } - // The re-armed deadline keeps the subscription past the old 60s expiry. - moq_clock::sim::advance(Duration::from_secs(120)).await; - let mut ended = false; - for _ in 0..50 { - let _ = futures::poll!(acceptor.as_mut()); - if futures::poll!(serving.as_mut()).is_ready() { - ended = true; - break; - } - settle().await; - } - assert!(!ended, "{version:?}: an accepted renewal keeps the subscription"); - } else { - // Per drafts 16 section 9.11.1 / 18 section 10.9.1, a refused renewal ends only - // this subscription, promptly and without the old 60s grant having to lapse, with - // PUBLISH_DONE UPDATE_FAILED. The update is answered first: nothing on draft-14, - // exactly one keyed REQUEST_ERROR to the update's Request ID (0x40) on draft-15/16. - // The session stays up. - let mut result = None; - for _ in 0..200 { - let _ = futures::poll!(acceptor.as_mut()); - if let Poll::Ready(res) = futures::poll!(serving.as_mut()) { - result = Some(res); - break; - } - settle().await; - } - assert_eq!( - answered.load(Ordering::Relaxed), - 2, - "{version:?}: the renewal was answered" - ); - assert!( - matches!(result, Some(Err(Error::Session(crate::SessionError::Unauthorized)))), - "{version:?}: a refused renewal ends the subscription at once, got {result:?}" - ); - // Draft 14 has no UPDATE_FAILED and answers no update, so its PUBLISH_DONE - // carries the refusal itself. - let (status, _) = Wire::read(&h.log, version).publish_done.expect("PUBLISH_DONE was sent"); - let expected = ietf::PublishDoneStatus::UpdateFailed; - assert_eq!(status, expected.code(version), "{version:?}"); - let written = h.log.writes.lock().unwrap()[before..].to_vec(); - match version { - // draft-14 has no renewal reply: the refusal is signalled only by the subscription - // ending (SUBSCRIBE_DONE UNAUTHORIZED, written by the caller), with no REQUEST_ERROR. - // draft-15/16 answer with one keyed REQUEST_ERROR before the subscription ends. - _ => { - let error = request_error_bytes(version, RequestId(0x40), "no").await; - assert!( - written.starts_with(&error), - "{version:?}: a keyed REQUEST_ERROR is written before the subscription ends" - ); - } - } - assert!(h.log.closes().is_empty(), "{version:?}: the session must stay up"); - } - } - /// One REQUEST_UPDATE carrying a fresh token, keyed to its own Request ID so several can be /// told apart on the wire. async fn subscribe_update_token_rid(version: Version, rid: u64) -> Vec { @@ -12681,7 +12227,7 @@ let ietf::FetchObject::Object { group, object, .. } = assert!(futures::poll!(serving.as_mut()).is_pending(), "{version}: ended early"); settle().await; } - let ok = request_ok_bytes(version, RequestId(0x42)).await; + let ok = request_ok_bytes(version).await; let held = h.log.writes.lock().unwrap().clone(); assert!( !held.windows(ok.len()).any(|window| window == ok.as_slice()), @@ -12698,7 +12244,7 @@ let ietf::FetchObject::Object { group, object, .. } = settle().await; } let written = h.log.writes.lock().unwrap()[before..].to_vec(); - let error = request_error_bytes(version, RequestId(0x40), "no").await; + let error = request_error_bytes(version, "no").await; assert!( written.starts_with(&error), "{version}: the renewal's REQUEST_ERROR is the first answer" @@ -12706,60 +12252,6 @@ let ietf::FetchObject::Object { group, object, .. } = } } - /// Before draft-17 a token-less REQUEST_UPDATE is answered keyed to its own request id - /// (draft-15/16), or not at all (draft-14, which defines no response). This pins the keyed and - /// silent branches of the acknowledgement so a wrong key cannot misroute the peer's answer. - #[moq_clock::test] - async fn token_less_updates_are_keyed_before_draft_17() { - for version in [Version::Draft15] { - let (auth, mut requests, _cred) = auth_covering_other(); - let rids = [0x42u64, 0x44, 0x46]; - let mut script = Vec::new(); - for rid in rids { - script.extend(subscribe_update_bare_rid(version, rid).await); - } - let h = serve_with_auth(version, auth, script); - let rt = h.publisher.runtime.clone(); - - let mut group = h.track.create_group(group::Info { sequence: 0 }).unwrap(); - group.write_frame(timestamp(), b"frame".as_slice()).unwrap(); - group.finish().unwrap(); - settle().await; - - let stream = Stream::open(&mut crate::driver::Transport::new(h.session.clone()), version) - .await - .unwrap(); - let acceptor = async move { - let first = requests.next().await.unwrap(); - let _held = first.accept(grant_all_expiring(&rt, None)); - std::future::pending::<()>().await - }; - let mut acceptor = std::pin::pin!(acceptor); - let mut serving = std::pin::pin!(h.publisher.clone().run_subscribe_stream(stream, token_subscribe())); - - for _ in 0..500 { - let _ = futures::poll!(acceptor.as_mut()); - assert!( - futures::poll!(serving.as_mut()).is_pending(), - "{version}: token-less updates must not end the request" - ); - settle().await; - } - - // Draft-14 answers nothing; draft-15/16 answer each update keyed to its own request id. - let written = h.log.writes.lock().unwrap().clone(); - for rid in rids { - let one = request_ok_bytes(version, RequestId(rid)).await; - let n = written.windows(one.len()).filter(|w| *w == one.as_slice()).count(); - assert_eq!(n, 1, "{version}: update {rid:#x} gets exactly one keyed REQUEST_OK"); - } - assert!( - h.log.closes().is_empty(), - "{version}: token-less updates must not close the session" - ); - } - } - /// On drafts without the option the guard is a memory backstop, not a protocol limit: a flood /// past it ends the one request through PUBLISH_DONE, never the session. #[moq_clock::test] @@ -12894,43 +12386,6 @@ let ietf::FetchObject::Object { group, object, .. } = assert!(ended, "{label}: dropping the renewal ends the request"); } - /// One UNSUBSCRIBE keyed to the subscription, the draft-14/15/16 cancellation message. - async fn unsubscribe_bytes(version: Version) -> Vec { - let log = crate::lite::test_transport::Log::default(); - let mut writer = crate::coding::Writer::new( - crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), - version, - ); - writer.varint(ietf::Unsubscribe::ID).await.unwrap(); - writer - .encode(&ietf::Unsubscribe { - request_id: RequestId(REQUEST_ID), - }) - .await - .unwrap(); - log.writes.lock().unwrap().clone() - } - - /// Before draft-17 a cancellation is an UNSUBSCRIBE message, not a FIN. It must end the - /// subscription even while a renewal verdict is pending: with a non-expiring grant and an - /// acceptor sitting on the renewal, the UNSUBSCRIBE is the only thing that can. - #[moq_clock::test] - async fn a_pending_renewal_ends_on_an_unsubscribe_before_draft_17() { - for version in [Version::Draft15] { - let (auth, requests, _cred) = auth_covering_other(); - // The stream carries the renewal, then the UNSUBSCRIBE, and never FINs, so only the - // message can end the request. - let session = ScriptedSession::new(subscribe_update_with_token(version, true).await); - let h = serve_on(version, auth, session.clone()); - let stream = Stream::open(&mut crate::driver::Transport::new(h.session.clone()), version) - .await - .unwrap(); - let unsubscribe = unsubscribe_bytes(version).await; - let cancel = || session.push(&unsubscribe); - ends_while_a_renewal_is_pending(&h, requests, stream, cancel, &format!("{version:?}")).await; - } - } - /// A REQUEST_UPDATE the acceptor refuses ends only that subscription, at once, through the /// PUBLISH_DONE UPDATE_FAILED path (drafts 16 section 9.11.1 / 18 section 10.9.1). The old /// grant does not keep it alive to its 60s expiry, and the session stays up. @@ -12994,10 +12449,10 @@ let ietf::FetchObject::Object { group, object, .. } = ietf::PublishDoneStatus::UpdateFailed.code(), "UPDATE_FAILED" ); - // Draft-17+ answers the refused update with exactly one unkeyed REQUEST_ERROR before the - // subscription ends. + // The refused update is answered with exactly one REQUEST_ERROR before the subscription + // ends. let written = h.log.writes.lock().unwrap().clone(); - let error = request_error_bytes(VERSION, RequestId(0), "no").await; + let error = request_error_bytes(VERSION, "no").await; assert_eq!( written.windows(error.len()).filter(|w| *w == error.as_slice()).count(), 1, @@ -13722,9 +13177,10 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), Some(assigned), peer::PeerSetup::default(), + Version::Draft18, ); let mut echoed_hops = crate::Hops::new(); @@ -13779,7 +13235,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(crate::lite::test_transport::SinkSession::new(Default::default())), origin.consume(), - Control::new(None, false), + Control::new(false), Some(assigned), peer::PeerSetup::default(), Version::Draft19, @@ -13850,9 +13306,10 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(crate::lite::test_transport::SinkSession::new(Default::default())), origin.consume(), - Control::new(None, false), + Control::new(false), Some(assigned), peer::PeerSetup::default(), + Version::Draft18, ); // The chain as ingress stores it: the peer named itself 0, and `via` is the @@ -13897,9 +13354,10 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, setup, + Version::Draft18, ); let mut clean = crate::Hops::new(); @@ -13915,7 +13373,7 @@ mod tests { .unwrap(); settle().await; - let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), Version::Draft17) + let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), Version::Draft18) .await .unwrap(); let msg = ietf::SubscribeNamespace { @@ -13932,6 +13390,7 @@ mod tests { let expected = crate::lite::test_transport::Log::default(); let mut writer = crate::coding::Writer::new( crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(expected.clone())), + Version::Draft18, ); writer.varint(ietf::RequestOk::ID).await.unwrap(); writer @@ -13965,9 +13424,10 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), Some(assigned), requires_solicitation(), + Version::Draft18, ); // The prefix starts with only a route from the assigned peer: hop 0 on the @@ -13984,7 +13444,7 @@ mod tests { .unwrap(); settle().await; - let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), Version::Draft16) + let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), Version::Draft18) .await .unwrap(); let msg = ietf::SubscribeNamespace { @@ -14056,7 +13516,7 @@ mod tests { /// uninterested) into a request every few seconds for the life of the session. #[moq_clock::test] async fn a_refusal_that_forbids_retrying_is_not_retried() { - const VERSION: Version = Version::Draft17; + const VERSION: Version = Version::Draft18; let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let _cam = origin.announce("lonely-cam", crate::origin::Route::default()).unwrap(); @@ -14073,7 +13533,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, declared(Some(false)), VERSION, @@ -14109,7 +13569,7 @@ mod tests { #[moq_clock::test] async fn another_epoch_is_advertised_before_the_old_is_withdrawn() { use crate::lite::test_transport::Sent; - const VERSION: Version = Version::Draft17; + const VERSION: Version = Version::Draft18; for epochs in [true, false] { let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); @@ -14131,7 +13591,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, declared, VERSION, @@ -14210,102 +13670,12 @@ mod tests { writes.clone() } - /// Draft-14/15 predate the NAMESPACE message, so a SUBSCRIBE_NAMESPACE is - /// answered with one PUBLISH_NAMESPACE request per matching namespace over the - /// control stream, and PUBLISH_NAMESPACE_DONE withdraws it. The state is local - /// to the subscription's task, mirroring lite's announce handling. - #[moq_clock::test] - async fn v14_subscribe_namespace_is_answered_with_publish_namespace() { - const VERSION: Version = Version::Draft14; - - let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); - let consumer = origin.consume(); - - // Announced before the peer subscribes: it must only hit the wire after. - let early = origin.announce("early-cam", crate::origin::Route::default()).unwrap(); - settle().await; - - // Stream 1 is the peer's SUBSCRIBE_NAMESPACE (the peer stays quiet after); - // streams 2 and 3 answer our two PUBLISH_NAMESPACE requests. - let ok = publish_namespace_ok(VERSION).await; - let session = crate::lite::test_transport::ScriptedSession::per_stream(vec![Vec::new(), ok.clone(), ok]); - let log = session.log.clone(); - - let publisher = Publisher::new( - crate::time::sim(), - crate::driver::Transport::new(session.clone()), - consumer, - Control::new(None, false), - None, - requires_solicitation(), - VERSION, - ); - - let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), VERSION) - .await - .unwrap(); - let msg = ietf::SubscribeNamespace { - request_id: RequestId(1), - namespace: crate::Path::new(""), - hidden: false, - }; - let mut run = std::pin::pin!(publisher.run_subscribe_namespace_stream(stream, msg)); - - // The subscription solicits the already-announced namespace. - for _ in 0..100 { - assert!(futures::poll!(run.as_mut()).is_pending()); - if occurrences(&log, b"early-cam") >= 1 { - break; - } - settle().await; - } - assert_eq!( - occurrences(&log, b"early-cam"), - 1, - "PUBLISH_NAMESPACE after subscribing" - ); - - // A later announce reaches the same subscription. - let _late = origin.announce("late-cam", crate::origin::Route::default()).unwrap(); - for _ in 0..100 { - assert!(futures::poll!(run.as_mut()).is_pending()); - if occurrences(&log, b"late-cam") >= 1 { - break; - } - settle().await; - } - assert_eq!( - occurrences(&log, b"late-cam"), - 1, - "PUBLISH_NAMESPACE for a live announce" - ); - - // An unannounce closes out its own request with PUBLISH_NAMESPACE_DONE. - drop(early); - for _ in 0..100 { - assert!(futures::poll!(run.as_mut()).is_pending()); - if occurrences(&log, b"early-cam") >= 2 { - break; - } - settle().await; - } - assert_eq!( - occurrences(&log, b"early-cam"), - 2, - "PUBLISH_NAMESPACE_DONE on unannounce" - ); - - // One stream for the subscription itself, one per PUBLISH_NAMESPACE: the - // withdrawal rode the announce's own request, not a new stream. - assert_eq!(log.bi_opens(), 3, "no extra stream for the withdrawal"); - } - /// A peer that declared nothing is told without being asked. Relays that never send /// SUBSCRIBE_NAMESPACE hear nothing otherwise, and every third-party one behaves /// that way: a publisher is expected to announce itself. #[moq_clock::test] async fn a_peer_that_declared_nothing_is_told_unsolicited() { - const VERSION: Version = Version::Draft17; + const VERSION: Version = Version::Draft18; let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let _local = origin.announce("local-cam", crate::origin::Route::default()).unwrap(); @@ -14323,7 +13693,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, VERSION, @@ -14349,7 +13719,7 @@ mod tests { /// From draft-17 a PUBLISH_NAMESPACE is withdrawn by cancelling its request: reset /// plus STOP_SENDING. A FIN there is a protocol violation some peers close on. #[moq_clock::test] - async fn publish_namespace_withdraws_by_reset_from_draft_17() { + async fn publish_namespace_withdraws_by_reset() { for version in [Version::Draft18, Version::Draft19, Version::Draft22] { let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let local = origin.announce("local-cam", crate::origin::Route::default()).unwrap(); @@ -14364,7 +13734,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, version, @@ -14413,7 +13783,7 @@ mod tests { /// to a PUBLISH_NAMESPACE is the peer breaking the extension, negotiated or not. #[moq_clock::test] async fn a_counted_publish_namespace_ok_is_a_violation() { - const VERSION: Version = Version::Draft17; + const VERSION: Version = Version::Draft18; let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let _local = origin.announce("local-cam", crate::origin::Route::default()).unwrap(); @@ -14445,7 +13815,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, VERSION, @@ -14487,7 +13857,7 @@ mod tests { impl Default for Discovery<'_> { fn default() -> Self { Self { - version: Version::Draft17, + version: Version::Draft18, declared: true, solicit: Some(true), prefix: "", @@ -14536,7 +13906,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), consume, - Control::new(None, false), + Control::new(false), None, setup, version, @@ -14657,7 +14027,7 @@ mod tests { assert_eq!(once, 1, "a peer that asked to be told on request is told once"); assert_eq!(streams, 1, "inline on the SUBSCRIBE_NAMESPACE stream it asked on"); - for version in [Version::Draft15] { + for version in [Version::Draft18] { let log = advertise_with_hidden(Discovery { version, solicit: Some(false), @@ -14692,7 +14062,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, version, @@ -14768,7 +14138,7 @@ mod tests { /// loop's progress visible while every open is blocked. #[moq_clock::test] async fn a_parked_open_still_lets_a_namespace_be_withdrawn() { - const VERSION: Version = Version::Draft14; + const VERSION: Version = Version::Draft18; let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let first = origin.announce("first-cam", crate::origin::Route::default()).unwrap(); @@ -14784,7 +14154,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, declared(Some(false)), VERSION, @@ -14862,9 +14232,10 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), Some(assigned), declared(Some(false)), + Version::Draft18, ); // The state a refused or failed offer leaves behind: the peer holds nothing, and @@ -14889,7 +14260,7 @@ mod tests { /// peer asked for. #[moq_clock::test] async fn a_route_change_still_waits_out_a_refusal() { - const VERSION: Version = Version::Draft17; + const VERSION: Version = Version::Draft18; let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); // One epoch, so the second route re-prices the instance rather than restart it. @@ -14909,7 +14280,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, declared(Some(false)), VERSION, @@ -14968,44 +14339,6 @@ mod tests { slot } - /// PUBLISH_NAMESPACE_DONE withdrawing the first request, "cam". - async fn publish_namespace_done(version: Version) -> Vec { - let log = crate::lite::test_transport::Log::default(); - let mut writer = crate::coding::Writer::new( - crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), - version, - ); - writer.varint(ietf::PublishNamespaceDone::ID).await.unwrap(); - writer - .encode(&ietf::PublishNamespaceDone { - track_namespace: crate::Path::new("cam"), - request_id: RequestId(1), - }) - .await - .unwrap(); - log.writes.lock().unwrap().clone() - } - - /// PUBLISH_NAMESPACE_CANCEL letting go of the first request, "cam". - async fn publish_namespace_cancel(version: Version) -> Vec { - let log = crate::lite::test_transport::Log::default(); - let mut writer = crate::coding::Writer::new( - crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), - version, - ); - writer.varint(ietf::PublishNamespaceCancel::ID).await.unwrap(); - writer - .encode(&ietf::PublishNamespaceCancel { - track_namespace: crate::Path::new("cam"), - request_id: RequestId(1), - error_code: 0, - reason_phrase: "gone".into(), - }) - .await - .unwrap(); - log.writes.lock().unwrap().clone() - } - /// The bytes of one REQUEST_UPDATE, framed as the publisher writes it. async fn request_update(version: Version, msg: &ietf::PublishNamespaceUpdate) -> Vec { let log = crate::lite::test_transport::Log::default(); @@ -15052,7 +14385,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, clustered(Some(false)), VERSION, @@ -15079,7 +14412,6 @@ mod tests { VERSION, &ietf::PublishNamespaceUpdate { request_id: RequestId(3), - namespace_request_id: None, hops: None, cost: Some(0), authorization_token: None, @@ -15139,7 +14471,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, clustered(Some(false)), VERSION, @@ -15210,7 +14542,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, clustered(Some(false)), VERSION, @@ -15240,7 +14572,6 @@ mod tests { VERSION, &ietf::PublishNamespaceUpdate { request_id: RequestId(3), - namespace_request_id: None, hops: Some(cluster::HopPath::new( crate::Hops::try_from(vec![crate::Hop::new(8).unwrap(), crate::Hop::new(1).unwrap()]).unwrap(), )), @@ -15284,7 +14615,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, declared(Some(false)), version, @@ -15320,10 +14651,10 @@ mod tests { /// A route's auth rides its PUBLISH_NAMESPACE as the request's token, and swapping it /// renews the request in place: REQUEST_UPDATE on the same stream, carrying only the - /// new token. Draft-16 names the request in the update. + /// new token. #[moq_clock::test] async fn swapping_a_route_auth_is_a_request_update() { - swaps_a_route_auth(Version::Draft16).await; + swaps_a_route_auth(Version::Draft18).await; swaps_a_route_auth(Version::Draft19).await; } @@ -15362,7 +14693,6 @@ mod tests { version, &ietf::PublishNamespaceUpdate { request_id: RequestId(3), - namespace_request_id: (false).then_some(RequestId(1)), hops: None, cost: None, authorization_token: Some(auth_token(b"second-secret")), @@ -15389,97 +14719,6 @@ mod tests { assert_eq!(log.bi_opens(), 1, "{version}: the update rode the request's own stream"); } - /// A draft-16 peer's PUBLISH_NAMESPACE_CANCEL can beat its answer to our update. It - /// withdraws only that namespace, which the jittered retry sweep offers again, and the - /// loop carries on. - #[moq_clock::test] - async fn a_draft_16_cancel_ahead_of_the_answer_ends_only_its_request() { - const VERSION: Version = Version::Draft16; - - let origin = unheld_origin(); - let auth = crate::auth::Link::ietf(false); - let first = auth.present(bytes::Bytes::from_static(b"first-secret"), false).unwrap(); - let second = auth - .present(bytes::Bytes::from_static(b"second-secret"), false) - .unwrap(); - let cam = origin - .announce("cam", crate::origin::Route::default().with_auth(first)) - .unwrap(); - settle().await; - - let ok = publish_namespace_ok(VERSION).await; - let cancel = publish_namespace_cancel(VERSION).await; - let (publisher, log) = auth_publisher(&origin, &auth, vec![[ok.clone(), cancel].concat(), ok], VERSION); - let mut run = std::pin::pin!(publisher.run_publish_namespaces()); - poll_until(&mut run, &log, b"first-secret", 1).await; - - // The re-offer waits out a randomly jittered sweep, which 1ms settles can overrun. - cam.update(crate::origin::Route::default().with_auth(second)).unwrap(); - for _ in 0..100 { - assert!(futures::poll!(run.as_mut()).is_pending()); - if occurrences(&log, b"cam") >= 2 { - break; - } - tick().await; - } - - assert_eq!(occurrences(&log, b"cam"), 2, "the namespace was not offered again"); - assert_eq!(log.bi_opens(), 2, "a fresh request after the cancel"); - assert_eq!( - occurrences(&log, &publish_namespace_done(VERSION).await), - 0, - "PUBLISH_NAMESPACE_DONE after the peer's cancel" - ); - } - - /// A draft-16 peer refuses an update with REQUEST_ERROR and then PUBLISH_NAMESPACE_CANCEL. - /// Draft 16 sends no PUBLISH_NAMESPACE_DONE after a cancel, so the namespace is only - /// offered again once the refusal's wait is out. - #[moq_clock::test] - async fn a_refused_draft_16_update_sends_no_done() { - const VERSION: Version = Version::Draft16; - - let origin = unheld_origin(); - let auth = crate::auth::Link::ietf(false); - let first = auth.present(bytes::Bytes::from_static(b"first-secret"), false).unwrap(); - let second = auth - .present(bytes::Bytes::from_static(b"second-secret"), false) - .unwrap(); - let cam = origin - .announce("cam", crate::origin::Route::default().with_auth(first)) - .unwrap(); - settle().await; - - let ok = publish_namespace_ok(VERSION).await; - let refusal = publish_namespace_error(VERSION, 50).await; - let cancel = publish_namespace_cancel(VERSION).await; - let (publisher, log) = auth_publisher( - &origin, - &auth, - vec![[ok.clone(), refusal, cancel].concat(), ok], - VERSION, - ); - let mut run = std::pin::pin!(publisher.run_publish_namespaces()); - poll_until(&mut run, &log, b"first-secret", 1).await; - - cam.update(crate::origin::Route::default().with_auth(second)).unwrap(); - for _ in 0..100 { - assert!(futures::poll!(run.as_mut()).is_pending()); - if occurrences(&log, b"cam") >= 2 { - break; - } - tick().await; - } - - assert_eq!(occurrences(&log, b"cam"), 2, "the namespace was not offered again"); - assert_eq!(log.bi_opens(), 2, "a fresh request after the refusal"); - assert_eq!( - occurrences(&log, &publish_namespace_done(VERSION).await), - 0, - "PUBLISH_NAMESPACE_DONE after the peer's refusal" - ); - } - /// An auth another handle issued is not this session's to present: the route is /// advertised under the session's scope, and the credential never reaches this peer. #[moq_clock::test] @@ -15535,40 +14774,6 @@ mod tests { assert_eq!(log.bi_opens(), 2, "a fresh stream for the fresh request"); } - /// Before draft-16 a namespace cannot be updated, so a renewal withdraws the request - /// and makes it again with the new token. - #[moq_clock::test] - async fn a_draft_14_renewal_restarts_the_request() { - const VERSION: Version = Version::Draft14; - - let origin = unheld_origin(); - let auth = crate::auth::Link::ietf(false); - let first = auth.present(bytes::Bytes::from_static(b"first-secret"), false).unwrap(); - let second = auth - .present(bytes::Bytes::from_static(b"second-secret"), false) - .unwrap(); - let cam = origin - .announce("cam", crate::origin::Route::default().with_auth(first)) - .unwrap(); - settle().await; - - let ok = publish_namespace_ok(VERSION).await; - let (publisher, log) = auth_publisher(&origin, &auth, vec![ok.clone(), ok], VERSION); - let mut run = std::pin::pin!(publisher.run_publish_namespaces()); - poll_until(&mut run, &log, b"first-secret", 1).await; - - cam.update(crate::origin::Route::default().with_auth(second)).unwrap(); - poll_until(&mut run, &log, b"second-secret", 1).await; - - assert_eq!(occurrences(&log, b"second-secret"), 1, "the renewal never went out"); - assert_eq!( - occurrences(&log, b"first-secret"), - 1, - "the first token was not repeated" - ); - assert_eq!(log.bi_opens(), 2, "a fresh request for the renewal"); - } - /// Renewals wait on their answers side by side: one the peer has not answered holds up /// neither another namespace's renewal nor a new announcement. #[moq_clock::test] @@ -15633,7 +14838,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, declared(Some(false)), VERSION, @@ -15702,7 +14907,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, declared(Some(false)), version, @@ -15757,21 +14962,18 @@ mod tests { again.writer.write_all(&mut ok).await.unwrap(); cams.remove(second); - match version { - _ => assert!( - held.reader.varint().await.is_err(), - "{version} fin={fin}: not withdrawn" - ), - } + assert!( + held.reader.varint().await.is_err(), + "{version} fin={fin}: not withdrawn" + ); assert!(!run.is_finished(), "{version} fin={fin}: the loop ended"); } - /// A draft-14/15 PUBLISH_NAMESPACE whose header fails to send closes the session: its - /// request ID is spent, and a peer that requires the next ID would refuse every later - /// request. Later drafts back off that namespace instead. + /// A PUBLISH_NAMESPACE whose header fails to send backs off that namespace rather than + /// closing the session. #[moq_clock::test] - async fn a_failed_legacy_header_closes_the_session() { - use crate::transport::{memory, poll::Session as _}; + async fn a_failed_header_backs_off_the_namespace() { + use crate::transport::memory; for version in [Version::Draft19] { let origin = unheld_origin(); @@ -15785,12 +14987,12 @@ mod tests { .unwrap(); settle().await; - let (session, mut peer) = memory::pair(memory::Config::default()); + let (session, peer) = memory::pair(memory::Config::default()); let publisher = Publisher::new( crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, declared(Some(false)), version, @@ -15840,7 +15042,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, declared(Some(false)), VERSION, @@ -15870,11 +15072,10 @@ mod tests { } /// An update the peer never answers drops its request once the timeout passes, and - /// the retry advertises the namespace afresh under the current auth. Draft-16's request - /// rides the control stream, where only PUBLISH_NAMESPACE_DONE withdraws it. + /// the retry advertises the namespace afresh under the current auth. #[moq_clock::test] async fn an_unanswered_renewal_advertises_again() { - advertises_again_unanswered(Version::Draft16).await; + advertises_again_unanswered(Version::Draft18).await; advertises_again_unanswered(Version::Draft19).await; } @@ -15935,7 +15136,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, clustered(Some(false)), VERSION, @@ -15997,7 +15198,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, clustered(Some(false)), VERSION, @@ -16032,7 +15233,6 @@ mod tests { VERSION, &ietf::PublishNamespaceUpdate { request_id: RequestId(3), - namespace_request_id: None, hops: None, cost: Some(0), authorization_token: None, @@ -16042,44 +15242,6 @@ mod tests { assert_eq!(occurrences(&log, &update), 1, "only the one update was attempted"); } - #[moq_clock::test] - async fn close_withdraws_legacy_namespaces_without_closing_the_origin() { - const VERSION: Version = Version::Draft14; - let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); - let _cam = origin.announce("closing-cam", crate::origin::Route::default()).unwrap(); - settle().await; - let session = - crate::lite::test_transport::ScriptedSession::per_stream(vec![publish_namespace_ok(VERSION).await]); - let log = session.log.clone(); - let publisher = Publisher::new( - crate::time::sim(), - crate::driver::Transport::new(session.clone()), - origin.consume(), - Control::new(None, false), - None, - declared(None), - VERSION, - ); - let mut run = std::pin::pin!(publisher.clone().run_publish_namespaces()); - for _ in 0..100 { - assert!(futures::poll!(run.as_mut()).is_pending()); - if occurrences(&log, b"closing-cam") > 0 { - break; - } - settle().await; - } - assert_eq!(occurrences(&log, b"closing-cam"), 1); - assert!(!publisher.withdrawal.drained()); - publisher.withdrawal.begin(); - run.await.unwrap(); - assert!(publisher.withdrawal.drained()); - assert_eq!( - occurrences(&log, b"closing-cam"), - 2, - "PUBLISH_NAMESPACE_DONE names the withdrawn namespace" - ); - } - /// The peer granting a stream is only half the exchange. One it accepts and then never /// answers on wedges the loop exactly as a parked open does, so the response is bounded /// too: everything queued behind it is otherwise stranded for the session. @@ -16087,7 +15249,7 @@ mod tests { /// Draft-14 so each advertisement names its namespace on the wire. #[moq_clock::test] async fn a_silent_answer_still_lets_the_next_namespace_be_advertised() { - const VERSION: Version = Version::Draft14; + const VERSION: Version = Version::Draft18; let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let _first = origin.announce("first-cam", crate::origin::Route::default()).unwrap(); @@ -16103,7 +15265,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, declared(Some(false)), VERSION, @@ -16130,12 +15292,11 @@ mod tests { ); } - /// An advertisement the peer never answers is given up at its deadline. On drafts 14-16 - /// it rides the control stream, where dropping it tells the peer nothing, so it is - /// withdrawn with PUBLISH_NAMESPACE_DONE; from draft 17 its own stream's reset ends it. + /// An advertisement the peer never answers is given up at its deadline: its own stream's + /// reset ends it. #[moq_clock::test] async fn an_unanswered_advertisement_is_withdrawn() { - for version in [Version::Draft15] { + for version in [Version::Draft18] { let origin = unheld_origin(); let _cam = origin.announce("cam", crate::origin::Route::default()).unwrap(); settle().await; @@ -16147,7 +15308,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, declared(Some(false)), version, @@ -16155,11 +15316,7 @@ mod tests { let mut run = std::pin::pin!(publisher.run_publish_namespaces()); poll_until(&mut run, &log, b"cam", 1).await; - let done = Some(publish_namespace_done(version).await); - let withdrawn = || match &done { - Some(done) => occurrences(&log, done) > 0, - None => !log.resets().is_empty(), - }; + let withdrawn = || !log.resets().is_empty(); moq_clock::sleep(ADVERTISE_TIMEOUT - Duration::from_millis(10)).await; assert!(futures::poll!(run.as_mut()).is_pending()); assert!(!withdrawn(), "{version}: withdrawn before its deadline"); @@ -16188,7 +15345,7 @@ mod tests { let session = crate::lite::test_transport::ScriptedSession::new(publish_namespace_ok(version).await).without_credit(); let log = session.log.clone(); - let control = Control::new(None, false); + let control = Control::new(false); let publisher = Publisher::new( crate::time::sim(), crate::driver::Transport::new(session.clone()), @@ -16219,8 +15376,8 @@ mod tests { ); assert!(log.closes().is_empty()); assert_eq!( - control.poll_next_request_id(&kio::Waiter::noop()), - Poll::Ready(RequestId(3)), + control.next_request_id(), + RequestId(3), "{version}: only the admitted attempt spends an ID" ); } @@ -16231,7 +15388,7 @@ mod tests { /// stays undiscoverable for the life of the session. #[moq_clock::test] async fn an_advertisement_waiting_for_send_capacity_is_retried_on_its_own() { - const VERSION: Version = Version::Draft14; + const VERSION: Version = Version::Draft18; let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let _cam = origin.announce("lonely-cam", crate::origin::Route::default()).unwrap(); @@ -16247,7 +15404,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, declared(Some(false)), VERSION, @@ -16310,7 +15467,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, peer_setup, version, @@ -17262,13 +16419,13 @@ mod compression_tests { Err(Error::Unsupported) )); assert!(matches!( - track_properties(&info, wanted, true), + track_properties(&info, wanted, version, true), Err(Error::Unsupported) )); } let plain = track::Info::default().with_compress(true); assert!(!track_properties(&plain, true, version, false).unwrap().compress); - assert!(!track_properties(&plain, true, true).unwrap().compress); + assert!(!track_properties(&plain, true, version, true).unwrap().compress); } } @@ -17278,13 +16435,13 @@ mod compression_tests { async fn ietf_compression_fallback_interop() { use crate::coding::Decode; use crate::lite::test_transport::{Log, SinkSession}; - for (version, draft) in [(Version::Draft17, 17), (Version::Draft22, 22)] { + for (version, draft) in [(Version::Draft18, 18), (Version::Draft22, 22)] { let input = serde_json::json!({"draft": draft}); let bytes: Vec = serde_json::from_slice(&crate::test_interop::bun("ietf-compression.ts", input)).unwrap(); let (parameters, consumed) = ietf::Parameters::decode_slice(&bytes, version).unwrap(); assert_eq!(consumed, bytes.len()); - let negotiated = super::super::flate::from_setup(¶meters, version).unwrap(); + let negotiated = super::super::flate::from_setup(¶meters).unwrap(); assert!(!negotiated, "JS offered an unsupported codec"); let track = track::Producer::new( Arc::new(crate::broadcast::Info::default()), diff --git a/rs/moq-net/src/ietf/publisher/tracks.rs b/rs/moq-net/src/ietf/publisher/tracks.rs index 1756a70ed59..e1ed3083b96 100644 --- a/rs/moq-net/src/ietf/publisher/tracks.rs +++ b/rs/moq-net/src/ietf/publisher/tracks.rs @@ -1385,7 +1385,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.consume(), - Control::new(None, false), + Control::new(false), None, setup, version, @@ -1497,9 +1497,6 @@ mod tests { fn update(request_id: u64, forward: bool) -> ietf::SubscribeUpdate { ietf::SubscribeUpdate { request_id: RequestId(request_id), - subscription_request_id: None, - start_location: Location { group: 0, object: 0 }, - end_group: 0, subscriber_priority: None, forward: Some(forward), // Join at the live edge: draft-20 moved the filter off PUBLISH_OK. @@ -2594,7 +2591,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(h.session.clone()), h._origin.consume(), - Control::new(None, false), + Control::new(false), None, setup, version, diff --git a/rs/moq-net/src/ietf/session.rs b/rs/moq-net/src/ietf/session.rs index 52ef8e767e3..84d43bf6f00 100644 --- a/rs/moq-net/src/ietf/session.rs +++ b/rs/moq-net/src/ietf/session.rs @@ -1365,14 +1365,11 @@ mod tests { let interest = origin.consume().announced(); let session = crate::lite::test_transport::ScriptedSession::new(Vec::new()).without_credit(); let log = session.log.clone(); - let mut session = crate::driver::Transport::new(session); - let setup = None; + let session = crate::driver::Transport::new(session); let (driver, _goaway) = start(Config { runtime: crate::time::sim(), stats: Default::default(), session, - setup, - request_id_max: None, limits: Default::default(), client: true, publish: None, @@ -1473,8 +1470,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client, publish: None, @@ -1531,8 +1526,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client: true, publish: None, @@ -1580,8 +1573,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client: false, publish: None, @@ -1685,8 +1676,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client: true, publish: None, @@ -1752,8 +1741,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client: true, publish: None, @@ -1788,33 +1775,19 @@ mod tests { assert_eq!(occurrences(&log, b"rootns"), 0, "asked the peer for our local root"); } - /// The bytes of one legacy SUBSCRIBE_NAMESPACE, as the session writes them. - async fn subscribe_namespace_legacy(version: Version, namespace: &str, request_id: RequestId) -> Vec { + /// The bytes of one SUBSCRIBE_NAMESPACE, as the session writes them. + async fn subscribe_namespace(version: Version, namespace: &str, request_id: RequestId) -> Vec { let log = crate::lite::test_transport::Log::default(); let mut writer = Writer::new( crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), version, ); - let msg = ietf::SubscribeNamespaceLegacy { + let msg = ietf::SubscribeNamespace { request_id, namespace: crate::Path::new(namespace), - subscribe_options: ietf::SubscribeOptions::Namespace, hidden: false, }; - writer.varint(ietf::SubscribeNamespaceLegacy::ID).await.unwrap(); - writer.encode(&msg).await.unwrap(); - log.writes.lock().unwrap().clone() - } - - /// The bytes of one UNSUBSCRIBE_NAMESPACE, as the session writes them. - async fn unsubscribe_namespace(version: Version, prefix: &str, request_id: RequestId) -> Vec { - let log = crate::lite::test_transport::Log::default(); - let mut writer = Writer::new(crate::lite::test_transport::SinkSend::new(log.clone()), version); - let msg = ietf::UnsubscribeNamespace { - prefix: crate::Path::new(prefix), - request_id, - }; - writer.varint(ietf::UnsubscribeNamespace::ID).await.unwrap(); + writer.varint(ietf::SubscribeNamespace::ID).await.unwrap(); writer.encode(&msg).await.unwrap(); log.writes.lock().unwrap().clone() } @@ -1854,15 +1827,12 @@ mod tests { moq_clock::sim::JoinHandle>, ) { let log = session.log.clone(); - let mut session = crate::driver::Transport::new(session); - let setup = None; + let session = crate::driver::Transport::new(session); let (driver, _goaway) = start(Config { runtime: crate::time::sim(), stats: Default::default(), session, - setup, - request_id_max: None, limits: Default::default(), client: true, publish: None, @@ -1898,28 +1868,13 @@ mod tests { false } - /// How many times a client watching everything under `prefix` writes SUBSCRIBE_NAMESPACE - /// for it. `solicit` is what the peer's SETUP declared (MoQ Solicit). - async fn asked_namespace(version: Version, prefix: &str, solicit: Option) -> usize { - let subscribe = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); - let _watch = watch(&subscribe, prefix); - let (log, driver) = client(version, subscribe, solicit).await; - - let needle = subscribe_namespace_legacy(version, prefix, RequestId(0)).await; - sent(&log, &needle).await; - - assert!(!driver.is_finished(), "{version:?} ended the session"); - assert!(log.closes().is_empty(), "{version:?} closed: {:?}", log.closes()); - occurrences(&log, &needle) - } - /// A client asks nothing until something is watched, then only about what is. #[moq_clock::test] async fn a_client_asks_only_about_what_is_watched() { - for version in [Version::Draft15] { + for version in [Version::Draft18] { let subscribe = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let (log, _driver) = client(version, subscribe.clone(), Some(true)).await; - let first = subscribe_namespace_legacy(version, "cam", RequestId(0)).await; + let first = subscribe_namespace(version, "cam", RequestId(0)).await; assert!( !sent(&log, &first).await, "{version:?} asked before anything was watched" @@ -1927,7 +1882,7 @@ mod tests { let _cam = watch(&subscribe, "cam"); assert!(sent(&log, &first).await, "{version:?} did not ask about the watch"); - let second = subscribe_namespace_legacy(version, "", RequestId(2)).await; + let second = subscribe_namespace(version, "", RequestId(2)).await; assert!( !sent(&log, &second).await, "{version:?} asked about more than the watch" @@ -1935,58 +1890,9 @@ mod tests { } } - /// Once nothing has watched a prefix for the linger, the request ends. Draft-14 and 15 - /// carry it on the control stream, where only UNSUBSCRIBE_NAMESPACE tells the peer; it - /// names the prefix on draft-14 and the request on draft-15. - #[moq_clock::test] - async fn an_unwatched_prefix_is_unsubscribed_after_the_linger() { - for version in [Version::Draft15] { - let subscribe = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); - let cam = watch(&subscribe, "cam"); - let (log, _driver) = client(version, subscribe.clone(), None).await; - let asked = subscribe_namespace_legacy(version, "cam", RequestId(0)).await; - assert!(sent(&log, &asked).await, "{version:?}"); - let unsubscribe = unsubscribe_namespace(version, "cam", RequestId(0)).await; - - drop(cam); - moq_clock::sleep(crate::discovery::LINGER / 2).await; - assert!( - !sent(&log, &unsubscribe).await, - "{version:?} ended the request before the linger" - ); - moq_clock::sleep(crate::discovery::LINGER).await; - assert!(sent(&log, &unsubscribe).await, "{version:?} never unsubscribed"); - } - } - - /// A broader watch replaces the narrower request at a foreign peer rather than - /// overlapping it, which that peer refuses: on draft-14 and 15 the UNSUBSCRIBE_NAMESPACE - /// goes out on the ordered control stream ahead of the broader SUBSCRIBE_NAMESPACE. - #[moq_clock::test] - async fn a_broader_watch_replaces_the_narrower_at_a_foreign_peer() { - for version in [Version::Draft15] { - let subscribe = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); - let _front = watch(&subscribe, "cam/front"); - let (log, _driver) = client(version, subscribe.clone(), None).await; - let asked = subscribe_namespace_legacy(version, "cam/front", RequestId(0)).await; - assert!(sent(&log, &asked).await, "{version:?}"); - - let _cam = watch(&subscribe, "cam"); - let broader = subscribe_namespace_legacy(version, "cam", RequestId(2)).await; - assert!(sent(&log, &broader).await, "{version:?} never asked the broader prefix"); - let unsubscribe = unsubscribe_namespace(version, "cam/front", RequestId(0)).await; - let writes = log.writes.lock().unwrap().clone(); - let at = |needle: &[u8]| writes.windows(needle.len()).position(|window| window == needle); - assert!( - at(&unsubscribe).expect("unsubscribed the narrower request") < at(&broader).unwrap(), - "{version:?} overlapped the narrower request" - ); - } - } - - /// From draft-16 each request has a stream of its own, so at a foreign peer the broader - /// one waits for the peer to close the narrower one's, which ends with a FIN until - /// draft-19 makes it a reset: a peer that never closes it never sees the two overlap. + /// Each request has a stream of its own, so at a foreign peer the broader one waits for + /// the peer to close the narrower one's, which ends with a FIN until draft-19 makes it a + /// reset: a peer that never closes it never sees the two overlap. #[moq_clock::test] async fn a_broader_watch_waits_for_the_narrower_to_leave_the_wire() { for version in [ @@ -2049,57 +1955,24 @@ mod tests { /// routes over before it has synced. #[moq_clock::test] async fn a_broader_watch_keeps_the_narrower_open_at_our_peer() { - for version in [Version::Draft15] { + for version in [Version::Draft18] { let subscribe = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let _front = watch(&subscribe, "cam/front"); let (log, _driver) = client(version, subscribe.clone(), Some(true)).await; - let asked = subscribe_namespace_legacy(version, "cam/front", RequestId(0)).await; + let asked = subscribe_namespace(version, "cam/front", RequestId(0)).await; assert!(sent(&log, &asked).await, "{version:?}"); let _cam = watch(&subscribe, "cam"); - let broader = subscribe_namespace_legacy(version, "cam", RequestId(2)).await; + let broader = subscribe_namespace(version, "cam", RequestId(2)).await; assert!(sent(&log, &broader).await, "{version:?} never asked the broader prefix"); - let ended = match version { - _ => log - .trail() - .iter() - .any(|(_, sent)| !matches!(sent, crate::lite::test_transport::Sent::Write(_))), - }; + let ended = log + .trail() + .iter() + .any(|(_, sent)| !matches!(sent, crate::lite::test_transport::Sent::Write(_))); assert!(!ended, "{version:?} ended the narrower request: {:?}", log.trail()); } } - /// Draft-14 and draft-15 reject a zero-field track namespace, so a foreign peer (one - /// that declared no MoQ Solicit) is not asked for it, and a watched prefix still goes - /// out, which is how a client discovers anything through a peer that only tells when - /// asked. A peer that declared Solicit is ours: it only tells when asked, so it still - /// gets the empty prefix. Draft-16 made the empty prefix legal, so it stays. - #[moq_clock::test] - async fn an_empty_namespace_is_not_asked_of_a_foreign_peer_before_draft_16() { - for version in [Version::Draft15] { - assert_eq!( - asked_namespace(version, "", None).await, - 0, - "{version:?} asked a foreign peer for every namespace" - ); - assert_eq!( - asked_namespace(version, "cam", None).await, - 1, - "{version:?} skipped a watched prefix" - ); - assert_eq!( - asked_namespace(version, "", Some(true)).await, - 1, - "{version:?} did not ask a soliciting peer for every namespace" - ); - } - assert_eq!( - asked_namespace(Version::Draft16, "", None).await, - 1, - "draft-16 dropped the empty prefix" - ); - } - /// How many scheduling turns an advertisement gets before the count is taken. Time is /// paused in these tests, so each turn costs nothing and only runs the driver until it /// parks again; a busy machine cannot turn a slow announce into a passing silence. @@ -2123,8 +1996,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client: true, publish: Some(origin.consume()), @@ -2229,8 +2100,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client: true, publish: Some(origin.consume()), @@ -2317,8 +2186,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client, publish: None, @@ -2416,8 +2283,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client: false, publish: None, @@ -2508,8 +2373,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client: true, publish: None, @@ -2554,8 +2417,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client: true, publish: None, @@ -2668,7 +2529,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, true), + Control::new(true), None, peer_setup.clone(), crate::Hop::new(1).unwrap(), @@ -2826,8 +2687,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client: true, publish, @@ -2910,7 +2769,6 @@ mod tests { let session = crate::lite::test_transport::ScriptedSession::new(Vec::new()).with_incoming_unis(vec![ uni_stream(VERSION, PADDING), subgroup_header(VERSION, 7, 0).await, - setup, ]); let accepted = accept_setup(&mut crate::driver::Transport::new(session.clone()), VERSION) .await @@ -2932,14 +2790,9 @@ mod tests { async fn an_unknown_uni_type_closes_the_session() { for (version, kind) in [ (Version::Draft19, 0), - // Padding and uni SETUP arrived in later drafts, so earlier ones do not know them. - (Version::Draft17, PADDING), - (Version::Draft16, setup::SETUP_V17), + // Padding and uni SETUP arrived in later drafts, // SUBGROUP_HEADER types with the reserved SUBGROUP_ID_MODE (0b11). (Version::Draft19, 0x56), - (Version::Draft14, 0x16), - // FIRST_OBJECT (0x40) arrived in draft-18. - (Version::Draft17, 0x50), ] { let (log, result) = dispatch_uni(version, uni_stream(version, kind), None).await; @@ -2973,8 +2826,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client: false, publish: None, @@ -3044,8 +2895,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, client: false, publish: None, subscribe: None, @@ -3077,146 +2926,13 @@ mod tests { } } - /// A peer's advertisement of `room/host`, then two namespace-keyed withdrawals of it. - /// The second has no advertisement left to name. - async fn publish_namespace_then_two_withdrawals(version: Version) -> Vec { - let log = crate::lite::test_transport::Log::default(); - let mut writer = crate::coding::Writer::new( - crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), - version, - ); - - writer.varint(ietf::PublishNamespace::ID).await.unwrap(); - writer - .encode(&ietf::PublishNamespace { - request_id: RequestId(1), - track_namespace: crate::Path::new("room/host"), - params: Default::default(), - authorization_token: None, - }) - .await - .unwrap(); - - for _ in 0..2 { - writer.varint(ietf::PublishNamespaceDone::ID).await.unwrap(); - writer - .encode(&ietf::PublishNamespaceDone { - track_namespace: crate::Path::new("room/host"), - request_id: RequestId(0), - }) - .await - .unwrap(); - } - - let writes = log.writes.lock().unwrap(); - writes.clone() - } - - /// The acknowledgement draft-14 answers a PUBLISH_NAMESPACE with, as bytes to look - /// for in what we wrote. - async fn publish_namespace_ok(request_id: RequestId) -> Vec { - let log = crate::lite::test_transport::Log::default(); - let mut writer = crate::coding::Writer::new( - crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), - ); - - writer.varint(ietf::PublishNamespaceOk::ID).await.unwrap(); - writer.encode(&ietf::PublishNamespaceOk { request_id }).await.unwrap(); - - let writes = log.writes.lock().unwrap(); - writes.clone() - } - - /// draft-14 names its withdrawals rather than numbering them, so a repeat has no - /// advertisement left to resolve to. Dropping it is what keeps the session up: failing - /// the lookup takes the whole connection down over a message with nothing left to do. - /// - /// Driven through `start` because only the full loop shows the consequence, the read - /// task propagating the classifier's error. - #[moq_clock::test] - async fn a_repeated_publish_namespace_done_does_not_end_the_session() { - const VERSION: Version = Version::Draft14; - - let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); - let consumer = origin.consume(); - - let session = - crate::lite::test_transport::ScriptedSession::new(publish_namespace_then_two_withdrawals(VERSION).await); - let log = session.log.clone(); - let session = crate::driver::Transport::new(session.clone()); - let setup = Stream::open(&mut crate::driver::Transport::new(session.clone()), VERSION) - .await - .expect("open the control stream"); - - let (driver, _goaway) = start(Config { - runtime: crate::time::sim(), - stats: Default::default(), - session, - setup: Some(setup), - request_id_max: None, - limits: Default::default(), - client: true, - publish: None, - subscribe: Some(origin), - peer_hop: None, - cost: None, - version: VERSION, - path: None, - authority: None, - peer_setup_stream: None, - peer_declared: None, - auth: crate::auth::Link::new(false), - early_unis: Vec::new(), - implementation: None, - peer_implementation: Default::default(), - extensions: Default::default(), - }) - .expect("start the session"); - let driver = moq_clock::sim::spawn(driver); - - let accepted = publish_namespace_ok(RequestId(1)).await; - for _ in 0..ANNOUNCE_TURNS { - if occurrences(&log, &accepted) > 0 && consumer.get_broadcast("room/host").is_none() { - break; - } - moq_clock::sleep(std::time::Duration::from_millis(1)).await; - } - - assert_eq!(occurrences(&log, &accepted), 1, "the advertisement was not accepted"); - assert!( - consumer.get_broadcast("room/host").is_none(), - "the withdrawal did not reach the request that advertised it" - ); - assert!(!driver.is_finished(), "the repeated withdrawal ended the session"); - assert!(log.closes().is_empty(), "closed the session: {:?}", log.closes()); - } - /// A SUBSCRIBE for `room/video` whose parameters are one AUTHORIZATION TOKEN holding /// `token`, framed as a control message. fn subscribe_with_token(version: Version, token: &[u8]) -> Vec { - let msg = ietf::Subscribe { - max_age: None, - new_group: None, - request_id: RequestId(0), - track_namespace: crate::Path::new("room"), - track_name: "video".into(), - subscriber_priority: 128, - group_order: ietf::GroupOrder::Descending, - filter: ietf::Filter::NextObject, - fill: None, - properties_wanted: true, - forward: true, - rendezvous: false, - range_filters: false, - authorization_token: None, - epoch: None, - }; let mut body = Vec::new(); let mut w = crate::coding::Encoder::new(&mut body, version.into()); - match version { - // The request ID, namespace and name; every parameter but the token defaults. - _ => w.slice(&[0, 1, 4, b'r', b'o', b'o', b'm', 5, b'v', b'i', b'd', b'e', b'o']), - } + // The request ID, namespace and name; every parameter but the token defaults. + w.slice(&[0, 1, 4, b'r', b'o', b'o', b'm', 5, b'v', b'i', b'd', b'e', b'o']); let mut w = crate::coding::Encoder::new(&mut body, version.into()); w.varint(1).unwrap(); w.varint(u64::from(ietf::ParameterBytes::AuthorizationToken)).unwrap(); @@ -3263,14 +2979,11 @@ mod tests { }; let log = session.log.clone(); let session = crate::driver::Transport::new(session.clone()); - let setup = None; let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let (driver, _goaway) = start(Config { runtime: crate::time::sim(), stats: Default::default(), session, - setup, - request_id_max: None, limits: Default::default(), client: false, publish: Some(origin.consume()), @@ -3510,8 +3223,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client: true, publish: None, @@ -3575,7 +3286,7 @@ mod tests { for client in [false, true] { let session = crate::lite::test_transport::ScriptedSession::new(Vec::new()); let log = session.log.clone(); - let control = Control::new(None, client); + let control = Control::new(client); let first = u64::from(client); control.accept(RequestId(first)).unwrap(); control.accept(RequestId(first + 4)).unwrap(); @@ -3616,8 +3327,6 @@ mod tests { runtime: crate::time::sim(), stats: Default::default(), session: crate::driver::Transport::new(session.clone()), - setup: None, - request_id_max: None, limits: Default::default(), client, publish: Some(origin.consume()), diff --git a/rs/moq-net/src/ietf/subscribe.rs b/rs/moq-net/src/ietf/subscribe.rs index 2f98c38b393..6e557a4f67c 100644 --- a/rs/moq-net/src/ietf/subscribe.rs +++ b/rs/moq-net/src/ietf/subscribe.rs @@ -372,37 +372,8 @@ mod tests { epoch: None, }; - let encoded = encode_message(&msg, Version::Draft14); - let decoded: Subscribe = decode_message(&encoded, Version::Draft14).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.track_namespace.as_str(), "test"); - assert_eq!(decoded.track_name, "video"); - assert_eq!(decoded.subscriber_priority, 128); - } - - #[test] - fn test_subscribe_round_trip_v15() { - let msg = Subscribe { - new_group: None, - max_age: None, - request_id: RequestId(1), - track_namespace: Path::new("test"), - track_name: "video".into(), - subscriber_priority: 128, - group_order: GroupOrder::Descending, - filter: Filter::NextObject, - fill: None, - properties_wanted: true, - forward: true, - rendezvous: false, - range_filters: false, - authorization_token: None, - epoch: None, - }; - - let encoded = encode_message(&msg, Version::Draft15); - let decoded: Subscribe = decode_message(&encoded, Version::Draft15).unwrap(); + let encoded = encode_message(&msg, Version::Draft18); + let decoded: Subscribe = decode_message(&encoded, Version::Draft18).unwrap(); assert_eq!(decoded.request_id, RequestId(1)); assert_eq!(decoded.track_namespace.as_str(), "test"); @@ -411,7 +382,7 @@ mod tests { } /// Build a SUBSCRIBE body carrying a single RENDEZVOUS_TIMEOUT parameter. - const VERSIONS: [Version; 9] = [ + const VERSIONS: [Version; 5] = [ Version::Draft18, Version::Draft19, Version::Draft20, @@ -419,12 +390,11 @@ mod tests { Version::Draft22, ]; - /// NEW_GROUP_REQUEST survives SUBSCRIBE and REQUEST_UPDATE from draft 16 on, and is - /// never written before. + /// NEW_GROUP_REQUEST survives SUBSCRIBE and REQUEST_UPDATE. #[test] - fn new_group_request_round_trips_where_the_draft_has_it() { + fn new_group_request_round_trips() { for version in VERSIONS { - let expected = has_new_group_request(version).then_some(7); + let expected = Some(7); let subscribe = Subscribe { new_group: Some(7), max_age: None, @@ -497,11 +467,11 @@ mod tests { /// that carries it is ignored. Draft-16 dropped the id, which makes it unknown. #[test] fn cache_duration_on_subscribe_follows_the_draft() { - let draft15 = subscribe_with_rendezvous(0, Version::Draft15); - decode_message::(&draft15, Version::Draft15).expect("draft-15 ignores MAX_CACHE_DURATION"); + let draft15 = subscribe_with_rendezvous(0, Version::Draft18); + decode_message::(&draft15, Version::Draft18).expect("draft-15 ignores MAX_CACHE_DURATION"); - let draft16 = subscribe_with_rendezvous(0, Version::Draft16); - decode_message::(&draft16, Version::Draft16).expect_err("draft-16 rejects unknown parameter 0x04"); + let draft16 = subscribe_with_rendezvous(0, Version::Draft18); + decode_message::(&draft16, Version::Draft18).expect_err("draft-16 rejects unknown parameter 0x04"); } /// The first message parameter key on an encoded SUBSCRIBE, or `None` when it carries no @@ -579,8 +549,8 @@ mod tests { epoch: None, }; - let encoded = encode_message(&msg, Version::Draft14); - let decoded: Subscribe = decode_message(&encoded, Version::Draft14).unwrap(); + let encoded = encode_message(&msg, Version::Draft18); + let decoded: Subscribe = decode_message(&encoded, Version::Draft18).unwrap(); assert_eq!(decoded.track_namespace.as_str(), "conference/room123"); } @@ -593,35 +563,20 @@ mod tests { properties: Properties::default(), }; - let encoded = encode_message(&msg, Version::Draft14); - let decoded: SubscribeOk = decode_message(&encoded, Version::Draft14).unwrap(); + let encoded = encode_message(&msg, Version::Draft18); + let decoded: SubscribeOk = decode_message(&encoded, Version::Draft18).unwrap(); - assert_eq!(decoded.request_id, Some(RequestId(42))); + assert_eq!(decoded.track_alias, 42); } + /// The parameter value is two bare varints. The empty Track Properties block follows the + /// parameters. #[test] fn test_subscribe_ok_accepts_largest_object() { - // request_id=4, track_alias=4, one LARGEST_OBJECT parameter at {5, 0}. - let payload = [0x04, 0x04, 0x01, 0x09, 0x02, 0x05, 0x00]; - for version in [Version::Draft16] { - let decoded: SubscribeOk = decode_message(&payload, version).unwrap(); - - assert_eq!(decoded.request_id, Some(RequestId(4))); - assert_eq!(decoded.track_alias, 4); - assert_eq!(decoded.largest, Some(Location { group: 5, object: 0 }), "{version}"); - } - } - - /// From draft-17 the parameter value is two bare varints, so the same Location costs - /// one byte less than the length-prefixed form above. The empty Track Properties block - /// follows the parameters. - #[test] - fn test_subscribe_ok_accepts_largest_object_draft17_on() { let payload = [0x04, 0x01, 0x09, 0x05, 0x00]; for version in [Version::Draft18, Version::Draft19, Version::Draft20] { let decoded: SubscribeOk = decode_message(&payload, version).unwrap(); - assert_eq!(decoded.request_id, None, "{version}"); assert_eq!(decoded.track_alias, 4, "{version}"); assert_eq!(decoded.largest, Some(Location { group: 5, object: 0 }), "{version}"); } @@ -653,9 +608,6 @@ mod tests { let largest = Some(Location { group: 9, object: 3 }); for (version, expected) in [ - (Version::Draft14, &[7, 42, 0, 1, 1, 9, 3, 0][..]), - (Version::Draft16, &[7, 42, 1, 0x09, 0x02, 0x09, 0x03][..]), - (Version::Draft17, &[42, 1, 0x09, 0x09, 0x03][..]), (Version::Draft18, &[42, 1, 0x09, 0x09, 0x03][..]), (Version::Draft19, &[42, 1, 0x09, 0x09, 0x03][..]), (Version::Draft20, &[42, 1, 0x09, 0x09, 0x03][..]), @@ -673,49 +625,6 @@ mod tests { } } - #[test] - fn test_subscribe_ok_v15() { - let msg = SubscribeOk { - track_alias: 42, - largest: None, - properties: Properties::default(), - }; - - let encoded = encode_message(&msg, Version::Draft15); - let decoded: SubscribeOk = decode_message(&encoded, Version::Draft15).unwrap(); - - assert_eq!(decoded.request_id, Some(RequestId(42))); - assert_eq!(decoded.track_alias, 42); - } - - #[test] - fn test_subscribe_error() { - let msg = SubscribeError { - request_id: RequestId(123), - error_code: 500, - reason_phrase: "Not found".into(), - }; - - let encoded = encode_message(&msg, Version::Draft14); - let decoded: SubscribeError = decode_message(&encoded, Version::Draft14).unwrap(); - - assert_eq!(decoded.request_id, RequestId(123)); - assert_eq!(decoded.error_code, 500); - assert_eq!(decoded.reason_phrase, "Not found"); - } - - #[test] - fn test_unsubscribe() { - let msg = Unsubscribe { - request_id: RequestId(999), - }; - - let encoded = encode_message(&msg, Version::Draft14); - let decoded: Unsubscribe = decode_message(&encoded, Version::Draft14).unwrap(); - - assert_eq!(decoded.request_id, RequestId(999)); - } - /// Every parameter the draft lets a message carry has to parse, even the ones we act on /// nowhere: an unlisted key fails the whole message, which tears the session down /// instead of letting the request reach its answer. @@ -788,28 +697,10 @@ mod tests { 0x00, // num_params ]; - let result: Result = decode_message(&invalid_bytes, Version::Draft14); + let result: Result = decode_message(&invalid_bytes, Version::Draft18); assert!(result.is_err()); } - /// Draft-16 registers Filter Types 0x1-0x4 and requires PROTOCOL_VIOLATION for any - /// other. Moxygen's `SubscribeRequest::make` defaults to its unregistered LargestGroup - /// (0xFA), so its interop client sends this exact SUBSCRIBE; refuse it rather than - /// guess a meaning. - #[test] - fn test_subscribe_v16_rejects_unregistered_filter_type() { - let subscribe = |filter_type: &[u8]| { - let mut bytes = vec![0x00]; // request_id - bytes.extend_from_slice(b"\x01\x0bnonexistent\x0dno-such-track"); // namespace, name - bytes.extend_from_slice(&[0x01, 0x21, filter_type.len() as u8]); // one SUBSCRIPTION_FILTER - bytes.extend_from_slice(filter_type); - decode_message::(&bytes, Version::Draft16) - }; - - assert!(matches!(subscribe(&[0x40, 0xfa]), Err(DecodeError::InvalidValue))); - assert_eq!(subscribe(&[0x02]).unwrap().filter, Filter::NextObject); - } - /// From draft-15 SUBSCRIBER_PRIORITY is a parameter whose omission means 128, not this /// stack's unset 127, so a relay forwards it unchanged. #[test] @@ -829,164 +720,6 @@ mod tests { } } - #[test] - fn test_subscribe_update_v15_round_trip() { - let msg = SubscribeUpdate { - new_group: None, - max_age: None, - request_id: RequestId(10), - subscription_request_id: Some(RequestId(5)), - subscriber_priority: Some(200), - forward: Some(true), - filter: Some(Filter::NextObject), - authorization_token: None, - fill: None, - }; - - let encoded = encode_message(&msg, Version::Draft15); - let decoded: SubscribeUpdate = decode_message(&encoded, Version::Draft15).unwrap(); - - assert_eq!(decoded.request_id, RequestId(10)); - assert_eq!(decoded.subscription_request_id, Some(RequestId(5))); - assert_eq!(decoded.subscriber_priority, Some(200)); - assert_eq!(decoded.forward, Some(true)); - } - - #[test] - fn test_subscribe_update_v14_round_trip() { - let msg = SubscribeUpdate { - new_group: None, - max_age: None, - request_id: RequestId(10), - subscription_request_id: Some(RequestId(5)), - start_location: Location { group: 1, object: 2 }, - end_group: 100, - subscriber_priority: Some(200), - forward: Some(true), - filter: Some(Filter::NextObject), - authorization_token: None, - fill: None, - }; - - let encoded = encode_message(&msg, Version::Draft14); - let decoded: SubscribeUpdate = decode_message(&encoded, Version::Draft14).unwrap(); - - assert_eq!(decoded.request_id, RequestId(10)); - assert_eq!(decoded.subscription_request_id, Some(RequestId(5))); - assert_eq!(decoded.start_location, Location { group: 1, object: 2 }); - assert_eq!(decoded.end_group, 100); - assert_eq!(decoded.subscriber_priority, Some(200)); - assert_eq!(decoded.forward, Some(true)); - } - - #[test] - fn test_subscribe_ok_ignores_expires_v14() { - #[rustfmt::skip] - let bytes = [ - 0x01, // request_id - 0x00, // track_alias - 0x05, // expires = 5 - 0x02, // group_order - 0x00, // content_exists - 0x00, // num_params - ]; - - let decoded: SubscribeOk = decode_message(&bytes, Version::Draft14).unwrap(); - assert_eq!(decoded.request_id, Some(RequestId(1))); - } - - /// The SUBSCRIBE_OK aiomoqt 0.11.0 sends: EXPIRES = 0 and nothing else (#4172). - #[test] - fn test_subscribe_ok_ignores_expires_aiomoqt() { - let bytes = [0x03, 0x00, 0x01, 0x08, 0x00]; - - let decoded: SubscribeOk = decode_message(&bytes, Version::Draft16).unwrap(); - assert_eq!(decoded.request_id, Some(RequestId(3))); - assert_eq!(decoded.track_alias, 0); - assert!(decoded.largest.is_none()); - } - - /// The SUBSCRIBE_OK a libquicr relay sends: EXPIRES = 0 followed by five track - /// properties (#4172). - #[test] - fn test_subscribe_ok_ignores_expires_libquicr() { - let bytes = [ - 0x02, 0xde, 0x53, 0x15, 0xbf, 0xd6, 0x39, 0x31, 0x88, 0x01, 0x08, 0x00, 0x02, 0x00, 0x02, 0x00, 0x0a, 0x01, - 0x14, 0x01, 0x0e, 0x01, - ]; - - let decoded: SubscribeOk = decode_message(&bytes, Version::Draft16).unwrap(); - assert_eq!(decoded.request_id, Some(RequestId(2))); - assert_eq!(decoded.properties.priority, Some(1)); - assert_eq!(decoded.properties.group_order, Some(GroupOrder::Ascending)); - } - - #[test] - fn test_subscribe_v17_round_trip() { - let msg = Subscribe { - new_group: None, - max_age: None, - request_id: RequestId(1), - track_namespace: Path::new("test"), - track_name: "video".into(), - subscriber_priority: 128, - group_order: GroupOrder::Descending, - filter: Filter::NextObject, - fill: None, - properties_wanted: true, - forward: true, - rendezvous: false, - range_filters: false, - authorization_token: None, - epoch: None, - }; - - let encoded = encode_message(&msg, Version::Draft17); - let decoded: Subscribe = decode_message(&encoded, Version::Draft17).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.track_namespace.as_str(), "test"); - assert_eq!(decoded.track_name, "video"); - assert_eq!(decoded.subscriber_priority, 128); - } - - #[test] - fn test_subscribe_ok_v17_round_trip() { - let msg = SubscribeOk { - track_alias: 42, - largest: None, - properties: Properties::default(), - }; - - let encoded = encode_message(&msg, Version::Draft17); - let decoded: SubscribeOk = decode_message(&encoded, Version::Draft17).unwrap(); - - assert_eq!(decoded.request_id, None); - assert_eq!(decoded.track_alias, 42); - } - - #[test] - fn test_subscribe_update_v17_round_trip() { - let msg = SubscribeUpdate { - new_group: None, - max_age: None, - request_id: RequestId(10), - subscriber_priority: Some(200), - forward: Some(true), - filter: Some(Filter::NextObject), - authorization_token: None, - fill: None, - }; - - let encoded = encode_message(&msg, Version::Draft17); - let decoded: SubscribeUpdate = decode_message(&encoded, Version::Draft17).unwrap(); - - assert_eq!(decoded.request_id, RequestId(10)); - assert_eq!(decoded.subscription_request_id, None); - assert_eq!(decoded.subscriber_priority, Some(200)); - assert_eq!(decoded.forward, Some(true)); - } - #[test] fn test_subscribe_v18_round_trip() { let msg = Subscribe { @@ -1027,7 +760,6 @@ mod tests { let encoded = encode_message(&msg, Version::Draft18); let decoded: SubscribeOk = decode_message(&encoded, Version::Draft18).unwrap(); - assert_eq!(decoded.request_id, None); assert_eq!(decoded.track_alias, 42); } @@ -1094,96 +826,6 @@ mod tests { assert_eq!(decoded.properties.group_order, Some(GroupOrder::Descending)); } - /// Draft-15 is the one version that takes it as a message parameter, and has no track - /// properties to put it in. - #[test] - fn test_subscribe_ok_v15_group_order_is_a_parameter() { - let msg = SubscribeOk { - track_alias: 42, - largest: None, - properties: Properties { - unknown: Default::default(), - dynamic_groups: false, - compress: false, - max_cache_duration: None, - timescale: None, - priority: None, - group_order: Some(GroupOrder::Descending), - }, - }; - - #[rustfmt::skip] - let expected = vec![ - 7, // request id - 42, // track alias - 1, // one message parameter - 0x22, // GROUP_ORDER - 0x02, // descending - ]; - assert_eq!(encode_message(&msg, Version::Draft15), expected); - - let decoded: SubscribeOk = decode_message(&expected, Version::Draft15).unwrap(); - assert_eq!(decoded.properties.group_order, Some(GroupOrder::Descending)); - } - - /// Draft-15 has no properties block, so DYNAMIC_GROUPS is a SUBSCRIBE_OK parameter. - #[test] - fn test_subscribe_ok_v15_dynamic_groups_is_a_parameter() { - let msg = SubscribeOk { - track_alias: 42, - largest: None, - properties: Properties { - dynamic_groups: true, - compress: false, - ..Default::default() - }, - }; - - #[rustfmt::skip] - let expected = vec![ - 7, // request id - 42, // track alias - 1, // one message parameter - 0x30, // DYNAMIC_GROUPS - 0x01, - ]; - assert_eq!(encode_message(&msg, Version::Draft15), expected); - assert!( - decode_message::(&expected, Version::Draft15) - .unwrap() - .properties - .dynamic_groups - ); - - // A boolean: anything above 1 is malformed. - let malformed = [7, 42, 1, 0x30, 0x02]; - assert!(decode_message::(&malformed, Version::Draft15).is_err()); - } - - /// Draft-16 has the block too, under the name Track Extensions. We don't write one there, - /// but a peer that does used to fail the whole message as `Long`. - #[test] - fn test_subscribe_ok_v16_reads_track_extensions() { - #[rustfmt::skip] - let body = vec![ - 7, // request id - 42, // track alias - 0, // zero message parameters - 0x22, // DEFAULT_PUBLISHER_GROUP_ORDER - 0x02, // descending - ]; - - // Go through the size-prefixed path: that's what rejects unread trailing bytes. - let mut buf = Vec::new(); - Encoder::new(&mut buf, Version::Draft16.into()).u16(body.len() as u16); - buf.extend_from_slice(&body); - - let mut bytes = bytes::Bytes::from(buf); - let decoded = crate::coding::decode_buf(&mut bytes, SubscribeOk::decode).unwrap(); - assert_eq!(decoded.track_alias, 42); - assert_eq!(decoded.properties.group_order, Some(GroupOrder::Descending)); - } - /// GROUP_ORDER left SUBSCRIBE_OK after draft-15. Draft-18 closes the session when a /// known parameter shows up on a message that does not define it. #[test] @@ -1218,33 +860,10 @@ mod tests { let decoded: SubscribeUpdate = decode_message(&encoded, Version::Draft18).unwrap(); assert_eq!(decoded.request_id, RequestId(10)); - assert_eq!(decoded.subscription_request_id, None); assert_eq!(decoded.subscriber_priority, Some(200)); assert_eq!(decoded.forward, Some(true)); } - /// Cross-check: draft-17 emits an extra 0-byte (required_request_id_delta) that - /// draft-18 does not. So a draft-18 encoding should be exactly 1 byte shorter - /// than draft-17 for SUBSCRIBE_UPDATE. - #[test] - fn test_subscribe_update_v17_v18_size_differs() { - let v17_msg = SubscribeUpdate { - new_group: None, - max_age: None, - request_id: RequestId(10), - subscriber_priority: Some(200), - forward: Some(true), - filter: Some(Filter::NextObject), - authorization_token: None, - fill: None, - }; - let v18_msg = SubscribeUpdate { ..v17_msg.clone() }; - - let v17 = encode_message(&v17_msg, Version::Draft17); - let v18 = encode_message(&v18_msg, Version::Draft18); - assert_eq!(v17.len(), v18.len() + 1); - } - /// The head of a SUBSCRIBE (draft-20 Figure 10) up to its Number of Parameters: /// Request ID 1, Track Namespace ("live"), Track Name ("video"). Every value is /// below 128, so each varint is one byte in the drafts before and after 17 alike. @@ -1396,29 +1015,19 @@ mod tests { Version::Draft19, Version::Draft20, ] { - let mut body = body.clone(); - let wire = body; + let wire = body.clone(); let mut buf = Decoder::new(&wire, version.into()); Subscribe::decode_msg(&mut buf, version).unwrap_or_else(|e| panic!("{version}: {e}")); assert!(buf.is_empty(), "{version}: trailing bytes"); } } - /// Draft-15 ignores a parameter it does not know. From draft-16 on, a parameter from - /// a later draft is unknown, which closes the session. - #[test] - fn subscribe_ignores_unrecognized_parameters_in_draft15() { - let body = subscribe_body(&[0x01, 0x32, 0x00]); - decode_message::(&body, Version::Draft15).expect("draft-15 ignores NEW_GROUP_REQUEST"); - } - /// A parameter from a later draft is still an unknown parameter, which the draft makes /// a protocol violation. #[test] fn subscribe_refuses_parameters_before_their_draft() { for (version, params) in [ - // SUBGROUP_DELIVERY_TIMEOUT arrived in draft-18, and 0x06 is not a draft-16 id. - (Version::Draft16, &[0x01, 0x06, 0x00][..]), + // SUBGROUP_DELIVERY_TIMEOUT arrived in draft-18, // The Range Filters arrived in draft-19. (Version::Draft18, &[0x01, 0x25, 0x00][..]), // INCLUDE_PROPERTIES arrived in draft-20. @@ -1450,22 +1059,17 @@ mod tests { assert_eq!(decoded.request_id, RequestId(2)); } - /// SUBGROUP_DELIVERY_TIMEOUT (0x06) is not a draft-16 parameter. Draft-18 defines it - /// for SUBSCRIBE, and the value is dropped. + /// Draft-18 defines SUBGROUP_DELIVERY_TIMEOUT (0x06) for SUBSCRIBE, and the value is + /// dropped. #[test] fn subgroup_delivery_timeout_follows_its_draft() { let head: &[u8] = &[ 0x01, 0x01, 0x04, b'l', b'i', b'v', b'e', 0x05, b'v', b'i', b'd', b'e', b'o', ]; let body = [head, &[0x01, 0x06, 0x00][..]].concat(); - assert!(decode_message::(&body, Version::Draft16).is_err()); decode_message::(&body, Version::Draft18).expect("draft-18 accepts SUBGROUP_DELIVERY_TIMEOUT"); - // GROUP_ORDER (0x22) is known in draft-16 but not defined for REQUEST_UPDATE. - // Request ID, Subscription Request ID, one parameter, absolute key, varint value. - let update16: &[u8] = &[0x02, 0x0A, 0x01, 0x22, 0x02]; - decode_message::(update16, Version::Draft16).expect("draft-16 ignores GROUP_ORDER"); - // Draft-18 has no Subscription Request ID. The value is a uint8. + // GROUP_ORDER (0x22) is not defined for REQUEST_UPDATE. let update18: &[u8] = &[0x02, 0x01, 0x22, 0x02]; assert!(decode_message::(update18, Version::Draft18).is_err()); } @@ -1499,7 +1103,7 @@ mod cache_duration_tests { } #[test] - fn legacy_never_sends_cache_duration_and_modern_preserves_optional_value() { + fn cache_duration_preserves_optional_value() { for version in [ Version::Draft18, Version::Draft19, @@ -1507,10 +1111,8 @@ mod cache_duration_tests { Version::Draft21, Version::Draft22, ] { - let legacy = false; for age in [None, Some(Duration::ZERO), Some(Duration::from_secs(30))] { let ok = SubscribeOk { - request_id: legacy.then_some(RequestId(0)), track_alias: 0, largest: None, properties: Properties { @@ -1524,7 +1126,7 @@ mod cache_duration_tests { let got = crate::coding::decode_buf(&mut payload.as_slice(), version, SubscribeOk::decode_msg).unwrap(); assert_eq!( got.properties.max_cache_duration, - if legacy { None } else { age }, + age, "{version}" ); } diff --git a/rs/moq-net/src/ietf/subscribe_tracks.rs b/rs/moq-net/src/ietf/subscribe_tracks.rs index bfbb7db5610..2a886aa699e 100644 --- a/rs/moq-net/src/ietf/subscribe_tracks.rs +++ b/rs/moq-net/src/ietf/subscribe_tracks.rs @@ -280,7 +280,7 @@ mod tests { #[test] fn subscribe_tracks_starts_at_draft_18() { let mut buf = Vec::new(); - let version = Version::Draft17; + let version = Version::Draft18; assert!( paused() .encode_msg(&mut Encoder::new(&mut buf, version.into()), version) diff --git a/rs/moq-net/src/ietf/subscriber.rs b/rs/moq-net/src/ietf/subscriber.rs index b195f174785..dec647e1081 100644 --- a/rs/moq-net/src/ietf/subscriber.rs +++ b/rs/moq-net/src/ietf/subscriber.rs @@ -5657,39 +5657,6 @@ mod tests { } } - /// Draft-14 frames a fetch object's properties with a bare length, which is refused at - /// the prefix rather than buffered while the peer trickles the rest in. - #[moq_clock::test] - async fn draft14_fetch_properties_are_capped() { - use crate::lite::test_transport::ScriptedSession; - use crate::transport::poll::Session as _; - use futures::FutureExt as _; - - const VERSION: Version = Version::Draft14; - let mut wire = Vec::new(); - for field in [0u64, 0, 0] { - crate::coding::Encoder::new(&mut wire, VERSION.into()) - .varint(field) - .unwrap(); - } - wire.push(0); - crate::coding::Encoder::new(&mut wire, VERSION.into()) - .varint((super::super::group::MAX_OBJECT_EXTENSIONS + 1) as u64) - .unwrap(); - - let mut session = ScriptedSession::new(wire); - let (_, recv) = session.open_bi(Default::default()).await.unwrap(); - let mut reader = Reader::new(crate::transport::boxed::RecvStream::new(recv), VERSION); - let result = decode_fetch_object(&mut reader, true) - .now_or_never() - .expect("refused at the prefix, not parked on the body"); - assert!( - matches!(result, Err(Error::Decode(DecodeError::MessageTooLarge { .. }))), - "{:?}", - result.err() - ); - } - fn fin_responses(clean: bool) -> Vec { use crate::coding::Encode; let mut responses = Vec::new(); @@ -5818,7 +5785,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), None, peer_setup, crate::Hop::new(1).unwrap(), @@ -5837,7 +5804,7 @@ mod tests { async fn an_announce_to_a_side_that_declined_solicit_is_fine() { for solicit in [Some(true), Some(false), None] { assert!( - !unsolicited_is_a_violation_to(false, solicit, Version::Draft17), + !unsolicited_is_a_violation_to(false, solicit, Version::Draft18), "{solicit:?}" ); } @@ -5852,11 +5819,11 @@ mod tests { #[moq_clock::test] async fn an_announce_from_a_peer_that_implements_solicit_is_fatal() { assert!( - unsolicited_is_a_violation(Some(true), Version::Draft17), + unsolicited_is_a_violation(Some(true), Version::Draft18), "a peer that requires solicitation itself" ); assert!( - unsolicited_is_a_violation(Some(false), Version::Draft17), + unsolicited_is_a_violation(Some(false), Version::Draft18), "an explicit 0 declares support, so ours binds it too" ); } @@ -5866,20 +5833,7 @@ mod tests { /// happens when nobody does. #[moq_clock::test] async fn an_announce_from_a_peer_that_declared_nothing_is_fine() { - assert!(!unsolicited_is_a_violation(None, Version::Draft17)); - } - - /// Draft-14/15 have no inline NAMESPACE, so a PUBLISH_NAMESPACE request is also how a - /// peer answers our own SUBSCRIBE_NAMESPACE. The message cannot say which it is, so - /// nothing there is enforceable: our own publisher advertises exactly this way. - #[moq_clock::test] - async fn a_legacy_announce_is_never_a_violation() { - for version in [Version::Draft15] { - assert!( - !unsolicited_is_a_violation(Some(true), version), - "{version:?} answers a subscription this way" - ); - } + assert!(!unsolicited_is_a_violation(None, Version::Draft18)); } /// A rooted subscriber asks the peer for its permitted SCOPE. The root names where @@ -5903,11 +5857,12 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), scoped, - Control::new(None, false), + Control::new(false), None, peer_setup, crate::Hop::new(1).unwrap(), None, + Version::Draft18, tasks, Default::default(), ); @@ -5921,7 +5876,7 @@ mod tests { let prefix = namespaces.pop().unwrap(); let (_live, live) = live(); - let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), Version::Draft16) + let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), Version::Draft18) .await .unwrap(); let mut run = @@ -5989,7 +5944,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), scoped, - Control::new(None, false), + Control::new(false), None, peer_setup, crate::Hop::new(1).unwrap(), @@ -6089,7 +6044,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), None, peer_setup, us, @@ -6183,7 +6138,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), None, peer_setup, us, @@ -6252,7 +6207,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), None, peer_setup, crate::Hop::new(1).unwrap(), @@ -6344,7 +6299,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), None, peer_setup, crate::Hop::new(1).unwrap(), @@ -6524,7 +6479,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(crate::lite::test_transport::SinkSession::new(Default::default())), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), @@ -6739,33 +6694,13 @@ mod tests { } } - /// Draft-14 through 16 have an UNSUBSCRIBE message, and draft-16 section 5.1.1 makes it - /// the thing that lets the publisher destroy the subscription. Resetting the stream - /// without it leaves a peer that predates draft-17 serving the track forever. - #[moq_clock::test] - async fn a_legacy_cancel_sends_unsubscribe() { - let log = cancel_a_subscription(Version::Draft16).await; - assert!( - occurrences(&log, &[ietf::Unsubscribe::ID as u8]) > 0, - "draft-16 cancels with UNSUBSCRIBE", - ); - - // Draft-17 removed the message, so sending one would be a protocol violation. - let log = cancel_a_subscription(Version::Draft19).await; - assert_eq!( - occurrences(&log, &[ietf::Unsubscribe::ID as u8]), - 0, - "draft-17+ has no UNSUBSCRIBE", - ); - } - /// A rejection and the last consumer leaving can both be ready when the task is next /// polled. The publisher destroyed the request when it sent the error, so treating that /// as abandonment would cancel a request that no longer exists and name a dead id back at /// a peer entitled to object. The answer wins. #[moq_clock::test] async fn a_ready_rejection_beats_local_abandonment() { - const VERSION: Version = Version::Draft16; + const VERSION: Version = Version::Draft18; // A peer that rejects the subscribe outright. let rejection = { @@ -6796,7 +6731,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(1).unwrap(), @@ -6832,8 +6767,8 @@ mod tests { .unwrap(); assert!( - !control_message_types(&log, VERSION).contains(&ietf::Unsubscribe::ID), - "a rejected request is already gone; cancelling it names a dead id at the peer", + log.stops().is_empty(), + "a rejected request is already gone; there is nothing to cancel", ); } @@ -6859,7 +6794,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), @@ -6876,7 +6811,7 @@ mod tests { /// its task and track until a token that may never be answered. #[moq_clock::test] async fn an_abandoned_subscribe_stops_waiting_for_auth() { - const VERSION: Version = Version::Draft16; + const VERSION: Version = Version::Draft18; let (mut subscriber, log, _auth, _tokens, _task_set) = waiting_on_auth(VERSION); let producer = crate::broadcast::Info::default().produce(); @@ -6915,7 +6850,7 @@ mod tests { /// ends, judged against the union as it stands, so routing can recover. #[moq_clock::test] async fn a_withdrawn_route_ends_a_subscribe_waiting_for_auth() { - const VERSION: Version = Version::Draft16; + const VERSION: Version = Version::Draft18; let (mut subscriber, log, _auth, _tokens, _task_set) = waiting_on_auth(VERSION); let producer = crate::broadcast::Info::default().produce(); @@ -6957,7 +6892,7 @@ mod tests { /// serving a track nobody reads. #[moq_clock::test] async fn abandoning_before_subscribe_ok_still_cancels() { - const VERSION: Version = Version::Draft16; + const VERSION: Version = Version::Draft18; // A peer that accepts the stream and then says nothing at all. let session = crate::lite::test_transport::ScriptedSession::new(Vec::new()); @@ -6968,7 +6903,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(1).unwrap(), @@ -7005,14 +6940,10 @@ mod tests { .expect("run_subscribe parked waiting for a response that never came") .unwrap(); - assert!( - occurrences(&log, &[ietf::Unsubscribe::ID as u8]) > 0, - "a subscribe abandoned before SUBSCRIBE_OK must still be cancelled", - ); assert_eq!( log.stops(), vec![crate::ietf::error::CANCELLED], - "and must stop the direction the publisher writes", + "a subscribe abandoned before SUBSCRIBE_OK must still be cancelled", ); } @@ -7025,70 +6956,6 @@ mod tests { (auth, cred) } - /// A grant revoked while the SUBSCRIBE waits for request-ID credit refuses the request - /// at once, not after the ten-second give-up. - #[moq_clock::test] - async fn a_revocation_never_waits_behind_subscribe_credit() { - for version in [Version::Draft15] { - // MAX_REQUEST_ID admits nothing, so the SUBSCRIBE waits for credit. - let session = crate::lite::test_transport::ScriptedSession::new(Vec::new()); - let log = session.log.clone(); - let (tasks, _task_set) = crate::util::TaskSet::new(); - let (auth, _cred) = revocable_auth(); - let mut subscriber = Subscriber::new( - crate::time::sim(), - crate::driver::Transport::new(session.clone()), - crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(Some(RequestId(1)), false), - None, - peer::PeerSetup::declared(peer::Peer::default()), - crate::Hop::new(1).unwrap(), - None, - version, - tasks, - Default::default(), - ) - .with_auth(auth.clone()); - - let producer = crate::broadcast::Info::default().produce(); - let mut dynamic = producer.dynamic(); - let consumer = producer.consume(); - let track = consumer.track("video").unwrap(); - let subscription = track.subscribe(None); - let request = dynamic.requested_track().await.expect("no track requested"); - - let serving = moq_clock::sim::spawn(async move { - subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) - .await; - }); - settle().await; - assert!( - log.writes.lock().unwrap().is_empty(), - "{version}: a SUBSCRIBE went out without credit" - ); - - auth.ended(0, Error::Session(SessionError::Unauthorized)); - let result = moq_clock::timeout(Duration::from_secs(1), subscription) - .await - .unwrap_or_else(|_| panic!("{version}: the revocation waited behind the credit")); - assert!( - matches!(result, Err(Error::Unauthorized)), - "{version}: the request outlived its grant: {:?}", - result.err() - ); - moq_clock::timeout(Duration::from_secs(1), serving) - .await - .unwrap_or_else(|_| panic!("{version}: run_subscribe outlived its grant")) - .unwrap(); - assert!( - log.writes.lock().unwrap().is_empty(), - "{version}: nothing was sent without credit" - ); - assert!(log.closes().is_empty(), "{version}: the session stays up"); - } - } - /// A grant revoked while the SUBSCRIBE waits for its answer refuses the request and /// cancels the subscribe at once, not once the peer answers or acknowledges the cancel. #[moq_clock::test] @@ -7104,7 +6971,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(1).unwrap(), @@ -7121,7 +6988,6 @@ mod tests { let track = consumer.track("video").unwrap(); let subscription = track.subscribe(None); let request = dynamic.requested_track().await.expect("no track requested"); - let unsubscribes = subscriber.unsubscribes(); let serving = moq_clock::sim::spawn(async move { subscriber @@ -7144,11 +7010,8 @@ mod tests { // Only the wire cleanup still waits on the peer's acknowledgement. settle().await; drop(serving); - // A subscribe revoked before SUBSCRIBE_OK must still be cancelled: by UNSUBSCRIBE - // where the draft has one, and by stopping the publisher's direction on every draft. - if unsubscribes { - assert!(occurrences(&log, &[ietf::Unsubscribe::ID as u8]) > 0, "{version}"); - } + // A subscribe revoked before SUBSCRIBE_OK must still be cancelled, by stopping the + // publisher's direction. assert_eq!(log.stops(), vec![crate::ietf::error::CANCELLED], "{version}"); assert!(log.closes().is_empty(), "{version}: the session stays up"); } @@ -7158,7 +7021,7 @@ mod tests { /// Retiring its source cancels it even while the caller keeps its demand. #[moq_clock::test] async fn a_retirement_before_subscribe_ok_refuses_the_subscription() { - const VERSION: Version = Version::Draft16; + const VERSION: Version = Version::Draft18; // A peer that accepts the stream and then says nothing at all. let session = crate::lite::test_transport::ScriptedSession::new(Vec::new()); @@ -7169,7 +7032,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(1).unwrap(), @@ -7201,7 +7064,6 @@ mod tests { drop(route); serving.await.unwrap(); assert!(matches!(subscription.await, Err(Error::Unroutable))); - assert!(occurrences(&log, &[ietf::Unsubscribe::ID as u8]) > 0); assert_eq!(log.stops(), vec![crate::ietf::error::CANCELLED]); } @@ -7209,10 +7071,8 @@ mod tests { enum SetupWait { SubscribeOpen, StatusOpen, - StatusCredit, StatusResponse, FetchOpen, - FetchCredit, FetchResponse, } @@ -7224,14 +7084,13 @@ mod tests { Expired, } - /// Drive each setup wait without credit or an answer. Refusal must be local even + /// Drive each setup wait without an open stream or an answer. Refusal must be local even /// when the peer never acknowledges the cancellation's FIN. async fn invalid_setup(stage: SetupWait, version: Version, cause: Invalidated) { let opening = matches!( stage, SetupWait::SubscribeOpen | SetupWait::StatusOpen | SetupWait::FetchOpen ); - let credit = matches!(stage, SetupWait::StatusCredit | SetupWait::FetchCredit); let open = kio::Producer::new(false); let session = if opening { crate::lite::test_transport::ScriptedSession::gated_open(Vec::new(), open.consume()) @@ -7241,7 +7100,7 @@ mod tests { let log = session.log.clone(); let (tasks, _task_set) = TaskSet::new(); let (auth, _cred) = revocable_auth(); - let control = Control::new(credit.then_some(RequestId(1)), false); + let control = Control::new(false); let mut subscriber = Subscriber::new( crate::time::sim(), crate::driver::Transport::new(session.clone()), @@ -7290,7 +7149,7 @@ mod tests { .is_none() ); }), - SetupWait::StatusOpen | SetupWait::StatusCredit | SetupWait::StatusResponse => Box::pin(async { + SetupWait::StatusOpen | SetupWait::StatusResponse => Box::pin(async { assert!( subscriber .track_status(&Path::new("broadcast"), "video", request, &mut gate) @@ -7316,7 +7175,7 @@ mod tests { }; let mut work = work; assert!(futures::poll!(work.as_mut()).is_pending(), "{stage:?}/{version}"); - if opening || credit { + if opening { assert!(log.writes.lock().unwrap().is_empty()); } let expected = match cause { @@ -7358,7 +7217,7 @@ mod tests { ), "{stage:?}/{version}/{cause:?}: {result:?}" ); - if opening || credit { + if opening { assert!(done, "{stage:?}/{version}/{cause:?}: cancelled wait stayed alive"); assert!(log.writes.lock().unwrap().is_empty()); } else { @@ -7371,7 +7230,7 @@ mod tests { drop(work); if opening { assert_eq!( - control.next_request_id(&subscriber.runtime).await.unwrap(), + control.next_request_id(), RequestId(1), "cancelled open spends no ID" ); @@ -7402,7 +7261,6 @@ mod tests { StatusOpen, [Draft22] ); - setup_wait_test!(invalid_status_credit_is_refused, StatusCredit, [Draft16]); setup_wait_test!( invalid_status_response_is_refused, StatusResponse, @@ -7413,125 +7271,12 @@ mod tests { FetchOpen, [Draft22] ); - setup_wait_test!(invalid_fetch_credit_is_refused, FetchCredit, [Draft16]); setup_wait_test!( invalid_fetch_response_is_refused, FetchResponse, [Draft22] ); - #[derive(Clone, Copy, Debug)] - enum Allocated { - Subscribe, - TrackStatus, - GroupFetch, - } - - /// Drafts 14-16 return MAX_REQUEST_ID credit only for requests the peer receives, so a - /// source retiring after its request ID was allocated must still send that request, - /// then cancel it if the draft can. Skipping it would leak the credit for good. - #[moq_clock::test] - async fn a_retirement_after_allocation_still_sends_the_request() { - for version in [Version::Draft15] { - retirement_after_allocation(version).await; - } - } - - async fn retirement_after_allocation(version: Version) { - for kind in [Allocated::Subscribe, Allocated::TrackStatus, Allocated::GroupFetch] { - // Every request write parks until the gate opens, after the retirement. - let gate = kio::Producer::new(false); - let session = crate::lite::test_transport::SinkSession::gated_bi(gate.consume()); - let log = session.log.clone(); - let (tasks, _task_set) = crate::util::TaskSet::new(); - let mut subscriber = Subscriber::new( - crate::time::sim(), - crate::driver::Transport::new(session.clone()), - crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), - None, - peer::PeerSetup::declared(peer::Peer::default()), - crate::Hop::new(1).unwrap(), - None, - version, - tasks, - Default::default(), - ); - let route = kio::Producer::new(()); - subscriber.validity = crate::model::broadcast::RequestValidity::token(Some(route.consume())); - - let producer = crate::broadcast::Info::default().produce(); - let mut dynamic = producer.dynamic(); - let consumer = producer.consume(); - let track = consumer.track("video").unwrap(); - let _subscription = track.subscribe(None); - let request = dynamic.requested_track().await.expect("no track requested"); - - let mut fetch = None; - let serving = match kind { - Allocated::Subscribe => moq_clock::sim::spawn(async move { - subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) - .await; - }), - Allocated::TrackStatus => moq_clock::sim::spawn(async move { - let mut gate = crate::auth::Gate::new( - subscriber.auth.clone(), - Path::new("broadcast").to_owned(), - crate::auth::Direction::Subscribe, - ); - assert!( - subscriber - .track_status(&Path::new("broadcast"), "video", request, &mut gate) - .await - .is_none() - ); - }), - Allocated::GroupFetch => { - let track = track::Producer::new( - std::sync::Arc::new(crate::broadcast::Info::default()), - "video", - track::Info::default().with_timescale(None), - ); - let requested = track.dynamic(); - let mut pending = Box::pin(track.consume().fetch_group(0, None)); - assert!(futures::poll!(pending.as_mut()).is_pending()); - let group = requested.requested_fill_group().await.expect("group request"); - fetch = Some((track, pending)); - moq_clock::sim::spawn(subscriber.run_group_fetch( - Path::new("broadcast").to_owned(), - "video".into(), - group, - None, - )) - } - }; - - // The ID is allocated and its request parked mid-write when the source retires. - settle().await; - drop(route); - settle().await; - let Ok(mut open) = gate.write() else { - panic!("gate closed") - }; - *open = true; - drop(open); - serving.await.unwrap(); - drop(fetch); - - let expected = match kind { - Allocated::Subscribe => vec![ietf::Subscribe::ID, ietf::Unsubscribe::ID], - Allocated::TrackStatus => vec![ietf::TrackStatus::ID], - Allocated::GroupFetch => vec![ietf::Fetch::ID, ietf::FetchCancel::ID], - }; - assert_eq!(control_message_types(&log, version), expected, "{kind:?}"); - } - } - - /// The control messages that actually reached the wire, by type id. - /// - /// Decoding the framing rather than scanning for a byte: a type id is one varint among - /// many, and a substring match would happily find one inside a length or a payload. fn control_message_types(log: &crate::lite::test_transport::Log, version: Version) -> Vec { let writes = log.writes.lock().unwrap().clone(); let mut buf = Decoder::new(&writes, version.into()); @@ -7553,260 +7298,12 @@ mod tests { types } - /// Drafts 14-16 carry every request over `ControlStreamAdapter`'s virtual streams, so a - /// cancellation only counts if it traverses the mux and reaches the real control stream - /// writer. A test that drives a direct stream proves the subscriber's own logic and - /// nothing about the path production takes: the virtual writer's reset is a no-op and - /// its close returns as soon as the bytes are queued, so an adapter that dropped them - /// would look identical. - #[moq_clock::test] - async fn a_legacy_cancel_reaches_the_control_stream() { - const VERSION: Version = Version::Draft16; - - // A peer that opens the control stream and then says nothing, so the subscribe is - // abandoned before it is accepted and cancelled from there. - let session = crate::lite::test_transport::ScriptedSession::new(Vec::new()); - let log = session.log.clone(); - - let control = Control::new(None, false); - let adapter = super::super::adapter::ControlStreamAdapter::new( - crate::driver::Transport::new(session.clone()), - control.clone(), - VERSION, - ); - - // The one real bidi everything is multiplexed onto. - let control_stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), VERSION) - .await - .unwrap(); - let running = adapter.clone(); - let (_goaway_handle, goaway) = crate::goaway::Handle::new(true); - moq_clock::sim::spawn(async move { - let _ = running.run(control_stream.reader, control_stream.writer, goaway).await; - }); - - let (tasks, _task_set) = crate::util::TaskSet::new(); - let mut subscriber = Subscriber::new( - crate::time::sim(), - crate::driver::Transport::new(adapter), - crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - control, - None, - peer::PeerSetup::declared(peer::Peer::default()), - crate::Hop::new(1).unwrap(), - None, - VERSION, - tasks, - Default::default(), - ); - - let producer = crate::broadcast::Info::default().produce(); - let mut dynamic = producer.dynamic(); - let consumer = producer.consume(); - let track = consumer.track("video").unwrap(); - let subscription = track.subscribe(None); - - let request = dynamic.requested_track().await.expect("no track requested"); - - let serving = moq_clock::sim::spawn(async move { - subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) - .await; - }); - - settle().await; - drop(subscription); - drop(track); - drop(consumer); - - moq_clock::timeout(std::time::Duration::from_secs(1), serving) - .await - .expect("run_subscribe did not finish") - .unwrap(); - - // Let the adapter's writer task drain the queue onto the control stream. - settle().await; - - let types = control_message_types(&log, VERSION); - assert!( - types.contains(&ietf::Subscribe::ID), - "the SUBSCRIBE reached the control stream: {types:?}" - ); - assert!( - types.contains(&ietf::Unsubscribe::ID), - "the UNSUBSCRIBE must traverse the adapter to the control stream, not stop at the \ - virtual writer: {types:?}" - ); - } - - /// Draft-14's SUBSCRIBE_UPDATE has no optional fields, so a priority update repeats the - /// subscription's bounds: a Largest Object subscription starts just past the answer's - /// Largest, open ended. Zeroes would move the start back, which the draft forbids. - #[moq_clock::test] - async fn a_draft14_priority_update_repeats_the_bounds() { - const VERSION: Version = Version::Draft14; - - let subscribe_ok = message( - ietf::SubscribeOk::ID, - &ietf::SubscribeOk { - track_alias: 7, - largest: Some(ietf::Location { group: 5, object: 2 }), - properties: Default::default(), - }, - VERSION, - ); - let session = crate::lite::test_transport::ScriptedSession::new(subscribe_ok); - let log = session.log.clone(); - - let (tasks, _task_set) = crate::util::TaskSet::new(); - let mut subscriber = Subscriber::new( - crate::time::sim(), - crate::driver::Transport::new(session.clone()), - crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), - None, - peer::PeerSetup::declared(peer::Peer::default()), - crate::Hop::new(1).unwrap(), - None, - VERSION, - tasks, - Default::default(), - ); - - let producer = crate::broadcast::Info::default().produce(); - let mut dynamic = producer.dynamic(); - let consumer = producer.consume(); - let track = consumer.track("video").unwrap(); - let _low = track.subscribe(track::Subscription::default().with_priority(1)); - let request = dynamic.requested_track().await.expect("no track requested"); - let _serving = moq_clock::sim::spawn(async move { - subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) - .await; - }); - settle().await; - - let _high = track.subscribe(track::Subscription::default().with_priority(72)); - settle().await; - - let writes = log.writes.lock().unwrap().clone(); - let mut buf = Decoder::new(&writes, VERSION.into()); - let (mut subscribe, mut updates) = (None, Vec::new()); - while !buf.is_empty() { - let type_id = buf.varint().unwrap(); - let size = buf.u16().unwrap() as usize; - let mut body = Decoder::new(buf.slice(size).unwrap(), VERSION.into()); - match type_id { - ietf::Subscribe::ID => subscribe = Some(RequestId::decode(&mut body, VERSION).unwrap()), - ietf::SubscribeUpdate::ID => { - updates.push(ietf::SubscribeUpdate::decode_msg(&mut body, VERSION).unwrap()) - } - _ => {} - } - } - - let [update] = updates.as_slice() else { - panic!("one update, not {updates:?}"); - }; - assert_eq!(update.subscription_request_id, subscribe); - assert_eq!(update.start_location, ietf::Location { group: 5, object: 3 }); - assert_eq!(update.end_group, 0, "still open ended"); - assert_eq!(update.subscriber_priority, Some(super::super::priority::to_wire(72))); - assert_eq!(update.forward, Some(true)); - } - - /// Draft-14's SUBSCRIBE_UPDATE cannot omit the priority, so an update for the max age - /// alone restates the unset priority in force instead of the omission default 128. - #[moq_clock::test] - async fn a_draft14_max_age_update_restates_the_priority() { - const VERSION: Version = Version::Draft14; - - let subscribe_ok = message( - ietf::SubscribeOk::ID, - &ietf::SubscribeOk { - track_alias: 7, - largest: None, - properties: Default::default(), - }, - VERSION, - ); - let session = crate::lite::test_transport::ScriptedSession::new(subscribe_ok); - let log = session.log.clone(); - - let (tasks, _task_set) = crate::util::TaskSet::new(); - let mut subscriber = Subscriber::new( - crate::time::sim(), - crate::driver::Transport::new(session), - crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), - None, - peer::PeerSetup::declared(peer::Peer { - max_age: true, - ..Default::default() - }), - crate::Hop::new(1).unwrap(), - None, - VERSION, - tasks, - Default::default(), - ); - - let producer = crate::broadcast::Info::default().produce(); - let mut dynamic = producer.dynamic(); - let consumer = producer.consume(); - let track = consumer.track("video").unwrap(); - let _short = track.subscribe(track::Subscription::default().with_max_delay(Duration::from_secs(1))); - let request = dynamic.requested_track().await.expect("no track requested"); - let _serving = moq_clock::sim::spawn(async move { - subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) - .await; - }); - settle().await; - - let _long = track.subscribe(track::Subscription::default().with_max_delay(Duration::from_secs(5))); - settle().await; - - let writes = log.writes.lock().unwrap().clone(); - let mut buf = Decoder::new(&writes, VERSION.into()); - let mut updates = Vec::new(); - while !buf.is_empty() { - let type_id = buf.varint().unwrap(); - let size = buf.u16().unwrap() as usize; - let mut body = Decoder::new(buf.slice(size).unwrap(), VERSION.into()); - if type_id == ietf::SubscribeUpdate::ID { - updates.push(ietf::SubscribeUpdate::decode_msg(&mut body, VERSION).unwrap()); - } - } - - let [update] = updates.as_slice() else { - panic!("one update, not {updates:?}"); - }; - assert_eq!(update.max_age, Some(5_000)); - assert_eq!(update.subscriber_priority, Some(super::super::priority::to_wire(0))); - } - /// Establish a subscription on `version`, then drop its last consumer, and hand back /// what the session recorded on the way out. async fn cancel_a_subscription(version: Version) -> crate::lite::test_transport::Log { cancel_a_subscription_inner(version, false).await } - /// A publisher on draft-14 through 16 that legally hands a second subscription to one - /// track the alias the first already holds. We cannot demux that, so we walk away from - /// the new subscription. Those versions carry requests over the control stream adapter, - /// whose virtual streams drop silently, so UNSUBSCRIBE is the only way the publisher - /// ever learns to stop serving it. - #[moq_clock::test] - async fn a_legacy_shared_alias_is_unsubscribed() { - let log = cancel_a_subscription_inner(Version::Draft16, true).await; - - assert!( - occurrences(&log, &[ietf::Unsubscribe::ID as u8]) > 0, - "abandoning a shared alias must still tell the publisher to stop", - ); - } - /// Writing the UNSUBSCRIBE is not the same as delivering it. A stream that has only been /// finished is still retransmitting, so the writer's Drop reset would discard the message /// before the peer read it. Closing consumes the writer, which is what removes that @@ -7853,7 +7350,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(1).unwrap(), @@ -7961,7 +7458,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(1).unwrap(), @@ -8091,7 +7588,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(2).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(2).unwrap(), @@ -8161,7 +7658,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(1).unwrap(), @@ -8245,7 +7742,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(1).unwrap(), @@ -8363,11 +7860,12 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), None, + Version::Draft18, tasks, Default::default(), ); @@ -8420,11 +7918,12 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), Some(assigned), peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), None, + Version::Draft18, tasks, Default::default(), ); @@ -8480,11 +7979,12 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), Some(peer), peer::PeerSetup::default(), self_origin, None, + Version::Draft18, tasks, Default::default(), ); @@ -8519,11 +8019,12 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(crate::lite::test_transport::SinkSession::new(Default::default())), origin.clone(), - Control::new(None, false), + Control::new(false), Some(peer), peer::PeerSetup::default(), self_origin, None, + Version::Draft18, tasks, Default::default(), ); @@ -8567,7 +8068,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin.clone(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), self_origin, @@ -8705,7 +8206,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), None, peer_setup, crate::Hop::new(1).unwrap(), @@ -8795,7 +8296,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), None, peer_setup, crate::Hop::new(1).unwrap(), @@ -8848,193 +8349,58 @@ mod tests { assert!( routed_now(&consumer, "room/host").is_none(), "an explicit NAMESPACE_DONE must retract the route", - ); - } - - #[moq_clock::test] - async fn publish_namespace_requester_fin_keeps_the_route() { - for version in [Version::Draft18, Version::Draft19, Version::Draft22] { - for reset in [false, true] { - let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); - let consumer = origin.consume(); - let session = if reset { - crate::lite::test_transport::ScriptedSession::per_stream_reset(vec![vec![]]) - } else { - crate::lite::test_transport::ScriptedSession::per_stream_eof(vec![vec![]]) - }; - let (tasks, _task_set) = crate::util::TaskSet::new(); - let mut subscriber = Subscriber::new( - crate::time::sim(), - crate::driver::Transport::new(session.clone()), - origin, - Control::new(None, false), - None, - peer::PeerSetup::default(), - crate::Hop::new(1).unwrap(), - None, - version, - tasks, - Default::default(), - ); - let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) - .await - .unwrap(); - let msg = ietf::PublishNamespace { - request_id: RequestId(0), - track_namespace: crate::Path::new("room/host"), - params: Default::default(), - authorization_token: None, - }; - let mut run = std::pin::pin!(subscriber.run_publish_namespace_stream( - stream, - msg, - cluster::Peer::default(), - None - )); - assert_eq!( - futures::poll!(run.as_mut()).is_ready(), - reset || super::super::request_stream::fin_cancels(version), - "{version}" - ); - if !reset && !super::super::request_stream::fin_cancels(version) { - assert!(routed_now(&consumer, "room/host").is_some(), "{version}"); - } - } - } - } - - /// v14-16 withdraw a PUBLISH_NAMESPACE with PUBLISH_NAMESPACE_DONE, which the adapter - /// delivers as a message before it FINs the virtual stream. Reading it as a stray - /// message closes the whole session over a routine unannounce. - #[moq_clock::test] - async fn a_publish_namespace_done_retracts_without_faulting_the_session() { - const VERSION: Version = Version::Draft14; - - let path = crate::Path::new("room/host").to_owned(); - let log = crate::lite::test_transport::Log::default(); - let mut writer = crate::coding::Writer::new( - crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), - VERSION, - ); - writer - .encode_message(&ietf::PublishNamespaceDone { - track_namespace: path.borrow(), - request_id: RequestId(0), - }) - .await - .unwrap(); - let script = log.writes.lock().unwrap().clone(); - - let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); - let consumer = origin.consume(); - let session = crate::lite::test_transport::ScriptedSession::eof(script); - let (tasks, task_set) = crate::util::TaskSet::new(); - std::mem::forget(task_set); - let mut subscriber = Subscriber::new( - crate::time::sim(), - crate::driver::Transport::new(session.clone()), - origin, - Control::new(None, false), - None, - peer::PeerSetup::default(), - crate::Hop::new(1).unwrap(), - None, - VERSION, - tasks, - Default::default(), - ); - - let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), VERSION) - .await - .unwrap(); - let msg = ietf::PublishNamespace { - request_id: RequestId(0), - track_namespace: path.borrow(), - params: Default::default(), - authorization_token: None, - }; - subscriber - .run_publish_namespace_stream(stream, msg, cluster::Peer::default(), None) - .await - .expect("a withdrawal is not a protocol violation"); - settle().await; - - assert!( - routed_now(&consumer, "room/host").is_none(), - "an explicit withdrawal must close the broadcast", - ); - } - - /// Drafts 14 and 15 have no update for an advertisement, so a SUBSCRIBE_UPDATE the - /// adapter routes onto one changes nothing and leaves it standing until the withdrawal. - /// Draft 15 still owes the update an answer: a REQUEST_ERROR naming it. - #[moq_clock::test] - async fn a_legacy_update_on_an_advertisement_is_answered() { - for version in [Version::Draft15] { - let path = crate::Path::new("room/host").to_owned(); - // Own Request ID 10, aimed at the advertisement (0). Draft 14 carries its fixed - // start, end group, priority and forward before the parameters. - let body: &[u8] = &[10, 0, 0]; - let mut script = vec![ietf::SubscribeUpdate::ID as u8, 0, body.len() as u8]; - script.extend_from_slice(body); - let log = crate::lite::test_transport::Log::default(); - let mut writer = crate::coding::Writer::new( - crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), - version, - ); - writer - .encode_message(&ietf::PublishNamespaceDone { - track_namespace: path.borrow(), - request_id: RequestId(0), - }) - .await - .unwrap(); - script.extend(log.writes.lock().unwrap().iter()); - - let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); - let session = crate::lite::test_transport::ScriptedSession::eof(script); - let (tasks, task_set) = crate::util::TaskSet::new(); - std::mem::forget(task_set); - let mut subscriber = Subscriber::new( - crate::time::sim(), - crate::driver::Transport::new(session.clone()), - origin, - Control::new(None, false), - None, - peer::PeerSetup::default(), - crate::Hop::new(1).unwrap(), - None, - version, - tasks, - Default::default(), - ); - - let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) - .await - .unwrap(); - let msg = ietf::PublishNamespace { - request_id: RequestId(0), - track_namespace: path.borrow(), - params: Default::default(), - authorization_token: None, - }; - subscriber - .run_publish_namespace_stream(stream, msg, cluster::Peer::default(), None) - .await - .unwrap_or_else(|err| panic!("{version}: {err}")); - - let written = session.log.writes.lock().unwrap().clone(); - let mut rest = written.as_slice(); - let mut sent = Vec::new(); - while !rest.is_empty() { - let id = crate::coding::decode_varint(&mut rest, version).unwrap(); - let len = usize::from(u16::from_be_bytes([rest[0], rest[1]])); - sent.push((id, rest.get(2).copied())); - rest = &rest[2 + len..]; + ); + } + + #[moq_clock::test] + async fn publish_namespace_requester_fin_keeps_the_route() { + for version in [Version::Draft18, Version::Draft19, Version::Draft22] { + for reset in [false, true] { + let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); + let consumer = origin.consume(); + let session = if reset { + crate::lite::test_transport::ScriptedSession::per_stream_reset(vec![vec![]]) + } else { + crate::lite::test_transport::ScriptedSession::per_stream_eof(vec![vec![]]) + }; + let (tasks, _task_set) = crate::util::TaskSet::new(); + let mut subscriber = Subscriber::new( + crate::time::sim(), + crate::driver::Transport::new(session.clone()), + origin, + Control::new(false), + None, + peer::PeerSetup::default(), + crate::Hop::new(1).unwrap(), + None, + version, + tasks, + Default::default(), + ); + let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) + .await + .unwrap(); + let msg = ietf::PublishNamespace { + request_id: RequestId(0), + track_namespace: crate::Path::new("room/host"), + params: Default::default(), + authorization_token: None, + }; + let mut run = std::pin::pin!(subscriber.run_publish_namespace_stream( + stream, + msg, + cluster::Peer::default(), + None + )); + assert_eq!( + futures::poll!(run.as_mut()).is_ready(), + reset || super::super::request_stream::fin_cancels(version), + "{version}" + ); + if !reset && !super::super::request_stream::fin_cancels(version) { + assert!(routed_now(&consumer, "room/host").is_some(), "{version}"); + } } - let expected = vec![(ietf::RequestOk::ID, Some(0)), (ietf::RequestError::ID, Some(10))]; - assert_eq!(sent, expected, "{version}"); - assert!(session.log.closes().is_empty(), "{version}: the session stays up"); } } @@ -9068,7 +8434,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), @@ -9509,7 +8875,6 @@ mod tests { .encode(&ietf::PublishNamespaceUpdate { // Each update consumes a request id of the peer's parity. request_id: RequestId(3 + 2 * i as u64), - namespace_request_id: None, hops: Some(advert.hops.clone()), cost: Some(advert.cost), authorization_token: None, @@ -9551,7 +8916,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, true), + Control::new(true), None, peer::PeerSetup::default(), self_origin, @@ -9643,7 +9008,7 @@ mod tests { let (mut subscriber, _consumer, mut stream, _driver, log) = update_harness(self_origin, &peer, &clean, script).await; // The initial request is already retired; its ID remains permanently spent. - assert!(subscriber.control.accept(RequestId(3)).unwrap().is_none()); + subscriber.control.accept(RequestId(3)).unwrap(); let mut attached = Some(0); let err = subscriber .run_publish_namespace_updates( @@ -9778,7 +9143,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, true), + Control::new(true), None, peer::PeerSetup::default(), self_origin, @@ -9875,7 +9240,6 @@ mod tests { writer .encode(&ietf::PublishNamespaceUpdate { request_id: RequestId(3), - namespace_request_id: None, hops: None, cost: Some(0), authorization_token: None, @@ -10145,7 +9509,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), @@ -10278,7 +9642,7 @@ mod tests { crate::driver::Transport::new(session.clone()), origin, // The announcing peer is the server here, so its update IDs are odd. - Control::new(None, true), + Control::new(true), None, peer_setup, crate::Hop::new(1).unwrap(), @@ -10329,7 +9693,6 @@ mod tests { ); let msg = ietf::PublishNamespaceUpdate { request_id: RequestId(3), - namespace_request_id: (false).then_some(RequestId(1)), hops: None, cost: None, authorization_token: Some(announce_token()), @@ -10349,7 +9712,6 @@ mod tests { ); let msg = ietf::PublishNamespaceUpdate { request_id: RequestId(3), - namespace_request_id: None, hops: Some(hop_path(&[7, 9])), cost: Some(0), authorization_token: Some(announce_token()), @@ -10481,57 +9843,6 @@ mod tests { } } - /// On drafts 14-16 the withdrawal arrives as a message before the FIN: a - /// PUBLISH_NAMESPACE_DONE waiting behind an unanswered verification ends the announce at - /// once, giving its slot back before the application ever takes the token. - #[moq_clock::test] - async fn a_legacy_withdrawal_during_verification_ends_the_announce() { - const VERSION: Version = Version::Draft16; - let done = { - let log = crate::lite::test_transport::Log::default(); - crate::coding::Writer::new( - crate::transport::boxed::SendStream::new(crate::lite::test_transport::SinkSend::new(log.clone())), - VERSION, - ) - .encode_message(&ietf::PublishNamespaceDone { - track_namespace: crate::Path::new("room/alice"), - request_id: RequestId(1), - }) - .await - .unwrap(); - log.writes.lock().unwrap().clone() - }; - // The application never takes the token, so only the withdrawal can end the wait. - let (mut subscriber, _requests, _cred, consumer, session) = auth_announce_harness(VERSION, done); - let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), VERSION) - .await - .unwrap(); - let mut run = std::pin::pin!(subscriber.run_publish_namespace_stream( - stream, - token_publish_namespace(), - cluster::Peer::default(), - None, - )); - - let mut result = None; - for _ in 0..100 { - if let Poll::Ready(res) = futures::poll!(run.as_mut()) { - result = Some(res); - break; - } - settle().await; - } - assert!( - matches!(result, Some(Ok(()))), - "a withdrawal during verification ends the announce: {result:?}" - ); - assert!( - routed_now(&consumer, "room/alice").is_none(), - "and it was never attached" - ); - assert!(session.log.closes().is_empty(), "the session stays up"); - } - /// A refused request token is answered UNAUTHORIZED and the announce is not attached; the /// session is untouched. #[moq_clock::test] @@ -11197,7 +10508,6 @@ mod tests { ); let msg = ietf::PublishNamespaceUpdate { request_id: RequestId(rid), - namespace_request_id: None, hops, cost, authorization_token: Some(announce_token()), @@ -11596,7 +10906,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), None, peer_setup, crate::Hop::new(1).unwrap(), @@ -11774,7 +11084,6 @@ mod tests { writer .encode(&ietf::PublishNamespaceUpdate { request_id: RequestId(5), - namespace_request_id: None, hops: None, cost: None, authorization_token: None, @@ -11836,7 +11145,6 @@ mod tests { writer .encode(&ietf::PublishNamespaceUpdate { request_id: RequestId(5), - namespace_request_id: None, hops: None, cost: None, authorization_token: Some(crate::ietf::token::RequestToken(crate::setup::Token { @@ -12184,7 +11492,7 @@ mod filter_tests { ); } - const JOINING_DRAFTS: [Version; 6] = [ + const JOINING_DRAFTS: [Version; 2] = [ Version::Draft18, Version::Draft19, ]; @@ -12437,7 +11745,7 @@ mod stitch_tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), origin, - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), @@ -12709,7 +12017,7 @@ mod stitch_tests { crate::time::sim(), crate::driver::Transport::new(downstream.clone()), served.consume(), - Control::new(None, false), + Control::new(false), None, peer, egress, @@ -13034,8 +12342,8 @@ mod stitch_tests { } } - /// Draft-15 on, only a Normal object may carry properties: a status object with them is - /// refused on ingest, and one relayed from draft-14 leaves without them. + /// Only a Normal object may carry properties: a status object with them is refused on + /// ingest. #[moq_clock::test] async fn passthrough_keeps_properties_off_status_objects() { for version in [Version::Draft22] { @@ -13051,48 +12359,6 @@ mod stitch_tests { ); } } - - // Draft-14 allows them, but END_OF_TRACK has nowhere to keep them: a standalone one - // is peeked before any group, and one after an object is ingested with the group. - let standalone: &[(u64, &[u8], Option)] = &[(0, &[0x20, 7], Some(END_OF_TRACK))]; - let trailing: &[(u64, &[u8], Option)] = &[(0, &[], None), (1, &[0x20, 7], Some(END_OF_TRACK))]; - for objects in [standalone, trailing] { - let script = object_stream(Version::Draft14, true, objects); - let mut h = Harness::with_timescale(Fill::Done, vec![script], None); - h.subscriber.version = Version::Draft14; - let mut stream = h.stream().await.with_version(Version::Draft14); - let result = h.subscriber.clone().recv_group(&mut stream).await; - assert!(matches!(result, Err(Error::Unsupported)), "{objects:?}: {result:?}"); - assert!( - matches!( - futures::FutureExt::now_or_never(h.track.closed()), - Some(Error::Unsupported) - ), - "{objects:?}: the subscription must end with the refusal" - ); - } - - let script = object_stream( - Version::Draft14, - true, - &[(0, &[], None), (1, &[0x20, 7], Some(END_OF_GROUP))], - ); - let (bytes, log) = relay_passthrough_stream(script, Version::Draft15).await; - assert!(log.resets().is_empty()); - let mut decoder = Decoder::new(&bytes, Version::Draft15.into()); - ietf::GroupHeader::decode(&mut decoder, Version::Draft15).unwrap(); - assert_eq!(decoder.varint().unwrap(), 0); - decoder.bytes().unwrap(); - assert_eq!(decoder.varint().unwrap(), 1); - assert_eq!(decoder.slice(1).unwrap(), &[99]); - assert_eq!(decoder.varint().unwrap(), 0); - assert!( - decoder.bytes().unwrap().is_empty(), - "the status object kept its properties" - ); - assert_eq!(decoder.varint().unwrap(), 0); - assert_eq!(decoder.varint().unwrap(), END_OF_GROUP); - assert!(decoder.is_empty()); } /// Draft-14 lets a status object carry properties, and it has no frame to hold its @@ -13102,7 +12368,7 @@ mod stitch_tests { async fn a_status_object_keeps_its_timestamp() { use futures::FutureExt; - let version = Version::Draft14; + let version = Version::Draft18; let status = [0x10, 7]; let leaves = |properties: &crate::model::object::Properties| { super::super::object::write(properties, Some(version), None, version, None).unwrap() @@ -13152,7 +12418,7 @@ mod stitch_tests { /// block leaves room for the Timestamp egress restates. #[moq_clock::test] async fn a_status_object_fills_the_whole_block() { - let version = Version::Draft14; + let version = Version::Draft18; let milli = Some(Timescale::MILLI); // A block of `size` held bytes: one opaque property, its key, a four-byte length, // then bytes. A framed object's list adds its Timestamp, which the frame holds. @@ -13348,18 +12614,18 @@ mod stitch_tests { } #[moq_clock::test] - async fn passthrough_preserves_immutable_bytes_and_refuses_incompatible_drafts() { + async fn passthrough_preserves_immutable_bytes() { for ingress in [ Version::Draft18, Version::Draft22, ] { let mut inner = Vec::new(); let mut encoder = Encoder::new(&mut inner, ingress.into()); - for value in [2u64, 96_000, if ingress == Version::Draft15 { 4 } else { 2 }, 128] { + for value in [2u64, 96_000, 128] { encoder.varint(value).unwrap(); } encoder - .varint(if ingress == Version::Draft15 { 5u64 } else { 1u64 }) + .varint(1u64) .unwrap(); encoder.bytes(&[42; 130]).unwrap(); let mut properties = Vec::new(); @@ -13372,10 +12638,6 @@ mod stitch_tests { ] { let script = object_stream(ingress, true, &[(0, &properties, None)]); let (bytes, log) = relay_passthrough_stream(script, ingress, egress).await; - if !super::super::object::same_bytes(ingress, egress) { - assert!(!log.resets().is_empty(), "incompatible immutable bytes must be refused"); - continue; - } assert!(log.resets().is_empty()); let mut decoder = Decoder::new(&bytes, egress.into()); ietf::GroupHeader::decode(&mut decoder, egress).unwrap(); @@ -13475,7 +12737,7 @@ mod stitch_tests { crate::time::sim(), crate::driver::Transport::new(downstream.clone()), served.consume(), - Control::new(None, false), + Control::new(false), None, peer, version, @@ -13534,103 +12796,6 @@ mod stitch_tests { } } - /// A status datagram's properties are stripped, not the datagram dropped, where the - /// egress draft forbids them beside a non-Normal status. - #[moq_clock::test] - async fn passthrough_strips_status_datagram_properties_a_draft_forbids() { - for ingress in [Version::Draft15] { - let egresses = [ - Version::Draft18, - Version::Draft19, - Version::Draft20, - Version::Draft21, - Version::Draft22, - ]; - let pairs = egresses - .into_iter() - .flat_map(|egress| [END_OF_GROUP, END_OF_TRACK].map(|status| (egress, status))); - for (egress, status) in pairs { - let origin = crate::origin::Config::new(crate::Hop::new(2).unwrap()).produce(); - let broadcast = origin.publish("room", crate::origin::Route::default()).unwrap(); - let track = broadcast - .create_track("video", accepted_info(&Default::default(), ingress)) - .unwrap(); - let mut h = Harness::with_timescale(Fill::Done, vec![], None); - h.subscriber.version = ingress; - h.subscriber.state.lock().subscribes.get_mut(&REQUEST).unwrap().producer = Some(track.clone()); - let mut properties = Vec::new(); - let mut encoder = Encoder::new(&mut properties, ingress.into()); - encoder.varint(0x80u64).unwrap(); - encoder.varint(96_000u64).unwrap(); - let datagram = ietf::ObjectDatagram { - track_alias: ALIAS, - group_id: SEQUENCE, - object_id: Some(5), - publisher_priority: Some(200), - end_of_group: false, - properties: Some(properties), - body: ietf::DatagramBody::Status(status), - }; - h.subscriber - .recv_datagram(datagram.encode_bytes(ingress).unwrap()) - .unwrap(); - track.finish().unwrap(); - let downstream = ScriptedSession::per_stream(vec![vec![]]).with_datagrams(65535); - let peer = peer::PeerSetup::default(); - peer.set(peer::Peer::default()); - let publisher = crate::ietf::Publisher::new( - crate::time::sim(), - crate::driver::Transport::new(downstream.clone()), - origin.consume(), - Control::new(None, false), - None, - peer, - egress, - ); - let subscribe = ietf::Subscribe { - max_age: None, - new_group: None, - request_id: RequestId(3), - track_namespace: Path::new("room"), - track_name: "video".into(), - subscriber_priority: 128, - group_order: ietf::GroupOrder::Ascending, - filter: Filter::Unfiltered, - fill: None, - properties_wanted: true, - forward: true, - rendezvous: false, - range_filters: false, - authorization_token: None, - epoch: None, - }; - let mut body = Vec::new(); - subscribe - .encode_msg(&mut Encoder::new(&mut body, egress.into()), egress) - .unwrap(); - let stream = Stream::open(&mut crate::driver::Transport::new(downstream.clone()), egress) - .await - .unwrap(); - publisher - .handle_stream(ietf::Subscribe::ID, ietf::Body(body.into()), stream, false) - .unwrap() - .await; - let sent = downstream.log.datagrams.lock().unwrap(); - let context = format!("{ingress} to {egress}, status {status}"); - let [bytes] = sent.as_slice() else { - panic!("{context}: sent {} datagrams", sent.len()); - }; - let (actual, _) = ietf::ObjectDatagram::decode_slice(bytes, egress).unwrap(); - let carried = false; - assert_eq!( - (actual.object_id, actual.body, actual.properties.is_some()), - (Some(5), ietf::DatagramBody::Status(status), carried), - "{context}" - ); - } - } - } - #[moq_clock::test] async fn passthrough_translates_mutable_property_encodings() { for ingress in [Version::Draft22] { @@ -13654,7 +12819,7 @@ mod stitch_tests { assert_eq!(props.varint().unwrap(), 96_000); assert_eq!( props.varint().unwrap(), - if egress == Version::Draft15 { 0x81 } else { 1 } + 1 ); assert_eq!(props.bytes().unwrap(), &[42; 130]); assert!(props.is_empty()); @@ -13732,47 +12897,6 @@ mod stitch_tests { ); } - /// Old drafts preserve a nonzero first Object ID without inventing missing frames. - #[moq_clock::test] - async fn old_drafts_accept_a_nonzero_first_object() { - for version in [Version::Draft16] { - let scripts = [2u64, 0] - .into_iter() - .enumerate() - .map(|(index, first)| { - let mut bytes = Vec::new(); - ietf::GroupHeader { - track_alias: ALIAS, - group_id: SEQUENCE + index as u64, - sub_group_id: 0, - publisher_priority: 0, - flags: ietf::GroupFlags { - first_object: true, - ..Default::default() - }, - } - .encode(&mut crate::coding::Encoder::new(&mut bytes, version.into()), version) - .unwrap(); - let mut encoder = crate::coding::Encoder::new(&mut bytes, version.into()); - encoder.varint(first).unwrap(); - encoder.varint(1u64).unwrap(); - bytes.push(42); - bytes - }) - .collect(); - let mut h = Harness::with_timescale(Fill::Done, scripts, None); - h.subscriber.version = version; - let mut consumer = h.track.subscribe(None); - let mut stream = h.stream().await.with_version(version); - h.subscriber.clone().recv_group(&mut stream).await.unwrap(); - assert_eq!(h.track.latest_sequence(), Some(SEQUENCE)); - assert_eq!(read_group(&mut consumer).await, (SEQUENCE, vec![(None, vec![42])])); - let mut stream = h.stream().await.with_version(version); - h.subscriber.clone().recv_group(&mut stream).await.unwrap(); - assert_eq!(read_group(&mut consumer).await, (SEQUENCE + 1, vec![(None, vec![42])])); - } - } - /// A subgroup 1 stream costs only itself: the track's subgroup 0 stream still arrives. #[moq_clock::test] async fn a_non_zero_subgroup_leaves_the_track_flowing() { @@ -14721,48 +13845,6 @@ mod stitch_tests { )); } - /// A draft-14/15 end-of-group marker ends a group fetch, so an object after it is a - /// violation rather than another frame. - #[moq_clock::test] - async fn a_group_fetch_refuses_an_object_past_its_end_marker() { - const DRAFT: Version = Version::Draft15; - let header = |object: u64| ietf::FetchObject::Object { - subgroup: ietf::FetchSubgroup::Zero, - group: Some(SEQUENCE), - object: Some(object), - priority: Some(0), - properties: None, - }; - let mut buf = Vec::new(); - header(0) - .encode(&mut crate::coding::Encoder::new(&mut buf, DRAFT.into()), DRAFT) - .unwrap(); - crate::coding::Encoder::new(&mut buf, DRAFT.into()) - .varint(1u64) - .unwrap(); - buf.extend_from_slice(b"a"); - header(1) - .encode(&mut crate::coding::Encoder::new(&mut buf, DRAFT.into()), DRAFT) - .unwrap(); - crate::coding::Encoder::new(&mut buf, DRAFT.into()) - .varint(0u64) - .unwrap(); - crate::coding::Encoder::new(&mut buf, DRAFT.into()) - .varint(END_OF_GROUP) - .unwrap(); - header(1) - .encode(&mut crate::coding::Encoder::new(&mut buf, DRAFT.into()), DRAFT) - .unwrap(); - crate::coding::Encoder::new(&mut buf, DRAFT.into()) - .varint(1u64) - .unwrap(); - buf.extend_from_slice(b"b"); - - let mut run = GroupFetchRun::new(DRAFT, buf.to_vec()).await; - let res = run.recv_objects(0, None).await; - assert!(matches!(res, Err(Error::ProtocolViolation)), "{res:?}"); - } - /// The first object on a fetch stream has no prior object, so leaving any field to /// the prior one is a violation, not "the requested group, from its start". #[moq_clock::test] @@ -14981,7 +14063,7 @@ mod stitch_tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), @@ -15060,7 +14142,7 @@ mod joining_fetch_tests { }; use futures::FutureExt; - const JOINING_DRAFTS: [Version; 6] = [ + const JOINING_DRAFTS: [Version; 2] = [ Version::Draft18, Version::Draft19, ]; @@ -15095,7 +14177,6 @@ mod joining_fetch_tests { message_bytes( ietf::FetchOk::ID, &ietf::FetchOk { - group_order: GroupOrder::Ascending, end_of_track: false, end_location: ietf::Location { group: 4, object: 2 }, properties: Default::default(), @@ -15150,7 +14231,7 @@ mod joining_fetch_tests { impl JoinRun { async fn start(version: Version, start: Option, ok: Vec, fetch: Vec) -> Self { - Self::start_with_control(version, start, [ok, fetch], Control::new(None, false)).await + Self::start_with_control(version, start, [ok, fetch], Control::new(false)).await } async fn start_with_control( @@ -15220,45 +14301,13 @@ mod joining_fetch_tests { } } - /// A peer withholding joining-FETCH credit cannot keep an unauthorized track alive. - #[moq_clock::test] - async fn revocation_does_not_wait_for_joining_fetch_credit() { - let version = Version::Draft14; - let mut run = JoinRun::start_with_control( - version, - None, - [ - subscribe_ok(version, Some(ietf::Location { group: 4, object: 1 })), - fetch_ok(version), - ], - Control::new(Some(RequestId(3)), false), - ) - .await; - assert!(run.fill_outstanding(), "accepted subscription awaits its joining FETCH"); - assert!( - !decode_messages(&run.session.log, version) - .iter() - .any(|(id, _)| *id == ietf::Fetch::ID) - ); - run.subscriber.auth.handle().authorize(&crate::auth::Grant::default()); - settle().await; - let mut received = (&mut run._hold.2) - .now_or_never() - .expect("SUBSCRIBE_OK accepted the track") - .unwrap(); - assert!(matches!( - received.finished().now_or_never(), - Some(Err(Error::Unauthorized)) - )); - } - fn pending_join(session: Transport, version: Version) -> (Subscriber, track::Producer, TaskSet) { let (tasks, task_set) = TaskSet::new(); let subscriber = Subscriber::new( crate::time::sim(), session, crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), @@ -15297,7 +14346,7 @@ mod joining_fetch_tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(1).unwrap(), @@ -15404,7 +14453,7 @@ mod joining_fetch_tests { "{version}: invalid joining open stayed alive" ); assert_eq!( - subscriber.control.next_request_id(&subscriber.runtime).await.unwrap(), + subscriber.control.next_request_id(), RequestId(1) ); assert_eq!(log.bi_opens(), 0); @@ -15412,28 +14461,6 @@ mod joining_fetch_tests { } } - #[moq_clock::test] - async fn refusing_a_joining_fetch_spends_no_request_credit() { - for version in JOINING_DRAFTS { - let session = ScriptedSession::new(Vec::new()).without_credit(); - let (mut subscriber, track, _tasks) = pending_join(crate::driver::Transport::new(session), version); - subscriber.control = Control::new( - None, - false, - ); - let mut joining = subscriber.start_joining_fetch(RequestId(99), &track).unwrap(); - assert!( - joining.poll(&kio::Waiter::noop()).is_ready(), - "{version}: refusal must be immediate" - ); - assert_eq!( - subscriber.control.poll_next_request_id(&kio::Waiter::noop()), - Poll::Ready(RequestId(1)), - "{version}: refusal spends neither an ID nor MAX_REQUEST_ID credit" - ); - } - } - /// Abandoning a blocked open leaves the next sequential ID available to unrelated work. #[moq_clock::test] async fn cancelling_a_joining_fetch_open_spends_no_id() { @@ -15448,79 +14475,13 @@ mod joining_fetch_tests { *gate.write().ok().expect("gate is open") = true; let _ = tasks.poll(&kio::Waiter::noop()); assert_eq!( - subscriber.control.next_request_id(&subscriber.runtime).await.unwrap(), + subscriber.control.next_request_id(), RequestId(1) ); assert_eq!(log.bi_opens(), 0, "{version}: the abandoned open is gone"); } } - /// A cancelled partial legacy header still precedes the next unrelated request ID. - #[moq_clock::test] - async fn cancelling_a_partial_joining_fetch_preserves_request_id_order() { - for version in [Version::Draft15] { - let gate = kio::Producer::new(false); - let session = crate::lite::test_transport::SinkSession::gated_bi_prefix(2, gate.consume()); - let log = session.log.clone(); - let (subscriber, track, mut tasks) = pending_join(crate::driver::Transport::new(session.clone()), version); - let mut joining = subscriber.start_joining_fetch(RequestId(99), &track).unwrap(); - assert!(joining.poll(&kio::Waiter::noop()).is_pending()); - assert_eq!(log.writes.lock().unwrap().len(), 2, "partial header"); - let mut unrelated = std::pin::pin!(subscriber.control.next_request_id(&subscriber.runtime)); - assert!(kio::Waiter::noop().poll_future(unrelated.as_mut()).is_pending()); - drop(joining); - let _ = tasks.poll(&kio::Waiter::noop()); - assert!(kio::Waiter::noop().poll_future(unrelated.as_mut()).is_pending()); - *gate.write().ok().expect("gate is open") = true; - let next = kio::wait(|waiter| { - let _ = tasks.poll(waiter); - waiter.poll_future(unrelated.as_mut()) - }) - .await - .unwrap(); - assert_eq!(next, RequestId(3), "{version}: no ID was skipped or reused"); - let messages = decode_messages(&log, version); - assert_eq!(messages.len(), 1); - assert_eq!(messages[0].0, ietf::Fetch::ID); - let mut body = messages[0].1.clone(); - let fetch = crate::coding::decode_buf(&mut body, version, ietf::Fetch::decode_msg).unwrap(); - assert_eq!(fetch.request_id, RequestId(1)); - assert!(log.resets().is_empty(), "header and FIN must survive cancellation"); - assert!( - log.trail() - .iter() - .any(|(_, sent)| matches!(sent, crate::lite::test_transport::Sent::Finish)) - ); - } - } - - /// A peer that never acknowledges cancellation cannot retain request-ID allocation. - #[moq_clock::test] - async fn joining_fetch_cleanup_does_not_hold_the_next_request_id() { - for version in [Version::Draft15] { - let session = ScriptedSession::per_stream(Vec::new()).with_unacked_fin(); - let log = session.log.clone(); - let (subscriber, track, mut tasks) = pending_join(crate::driver::Transport::new(session.clone()), version); - let mut joining = subscriber.start_joining_fetch(RequestId(99), &track).unwrap(); - assert!(joining.poll(&kio::Waiter::noop()).is_pending()); - drop(joining); - assert!( - tasks.poll(&kio::Waiter::noop()).is_pending(), - "FIN remains unacknowledged" - ); - assert_eq!( - subscriber.control.next_request_id(&subscriber.runtime).await.unwrap(), - RequestId(3) - ); - assert!( - log.trail() - .iter() - .any(|(_, sent)| matches!(sent, crate::lite::test_transport::Sent::Finish)) - ); - assert!(log.resets().is_empty()); - } - } - /// Every pre-draft-20 live join is Largest Object on the SUBSCRIBE and a relative /// joining FETCH at offset 0 that names the subscribe's request id. #[moq_clock::test] @@ -15739,7 +14700,7 @@ mod joining_fetch_tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), @@ -15788,7 +14749,6 @@ mod joining_fetch_tests { session.push(&message_bytes( ietf::FetchOk::ID, &ietf::FetchOk { - group_order: GroupOrder::Ascending, end_of_track: false, end_location: ietf::Location { group: GROUP, @@ -15876,9 +14836,6 @@ mod joining_fetch_tests { return; } assert_eq!(session.log.stops(), [crate::ietf::error::CANCELLED]); - let messages = decode_messages(&session.log, version); - let cancels: Vec<_> = messages.iter().filter(|(id, _)| *id == ietf::FetchCancel::ID).collect(); - assert!(cancels.is_empty(), "FETCH_CANCEL was removed"); assert_eq!(session.log.resets(), [crate::ietf::error::CANCELLED]); assert!(subscriber.state.lock().group_fetches.is_empty(), "request retired"); } @@ -15937,7 +14894,6 @@ mod joining_fetch_tests { let ok = message_bytes( ietf::FetchOk::ID, &ietf::FetchOk { - group_order: GroupOrder::Ascending, end_of_track: false, end_location: ietf::Location { group: GROUP, @@ -15954,7 +14910,7 @@ mod joining_fetch_tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), @@ -16005,7 +14961,6 @@ mod joining_fetch_tests { let ok = message_bytes( ietf::FetchOk::ID, &ietf::FetchOk { - group_order: GroupOrder::Ascending, end_of_track, end_location, properties: Default::default(), @@ -16019,7 +14974,7 @@ mod joining_fetch_tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), @@ -16113,7 +15068,6 @@ mod joining_fetch_tests { let ok = message_bytes( ietf::FetchOk::ID, &ietf::FetchOk { - group_order: GroupOrder::Ascending, end_of_track: ended, end_location, properties: Default::default(), @@ -16154,7 +15108,7 @@ mod joining_fetch_tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::default(), crate::Hop::new(1).unwrap(), @@ -16280,7 +15234,7 @@ mod new_group_tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(1).unwrap(), @@ -16569,7 +15523,7 @@ mod track_property_tests { }; /// The newest draft and an older one, both with a request stream per subscription. - const VERSIONS: [Version; 2] = [Version::Draft22]; + const VERSIONS: [Version; 2] = [Version::Draft18, Version::Draft22]; /// Subscribe to one track through a relay on `version`, whose publisher answers with /// `block` as SUBSCRIBE_OK's Track Properties, returning what the subscriber got. @@ -16580,7 +15534,7 @@ mod track_property_tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(1).unwrap(), @@ -16775,10 +15729,7 @@ mod compression_negotiation_tests { refused(&properties, version, false), Some(Error::UnsupportedExtension) )); - assert_eq!( - refused(&properties, version, true).is_none(), - super::super::flate::supported(version) - ); + assert!(refused(&properties, version, true).is_none(), "{version}"); } let info = accepted_info(&properties, Version::Draft22); assert!(info.compress); diff --git a/rs/moq-net/src/ietf/subscriber/tracks.rs b/rs/moq-net/src/ietf/subscriber/tracks.rs index 7984a903ab3..ffc70aad1ce 100644 --- a/rs/moq-net/src/ietf/subscriber/tracks.rs +++ b/rs/moq-net/src/ietf/subscriber/tracks.rs @@ -1264,7 +1264,7 @@ mod tests { crate::time::sim(), crate::driver::Transport::new(session.clone()), crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(), - Control::new(None, false), + Control::new(false), None, peer::PeerSetup::declared(peer::Peer::default()), crate::Hop::new(1).unwrap(), diff --git a/rs/moq-net/src/ietf/token.rs b/rs/moq-net/src/ietf/token.rs index bada6914429..185a2ec1530 100644 --- a/rs/moq-net/src/ietf/token.rs +++ b/rs/moq-net/src/ietf/token.rs @@ -202,7 +202,7 @@ mod tests { )); } - const VERSIONS: [Version; 9] = [ + const VERSIONS: [Version; 5] = [ Version::Draft18, Version::Draft19, Version::Draft20, @@ -256,8 +256,7 @@ mod tests { value: vec![0x00, 0xff], }; for (version, expected) in [ - (Version::Draft14, [0x03, 0x41, 0x2c, 0x00, 0xff]), - (Version::Draft17, [0x03, 0x81, 0x2c, 0x00, 0xff]), + (Version::Draft18, [0x03, 0x81, 0x2c, 0x00, 0xff]), ] { let mut params = Parameters::default(); into_setup(&mut params, &token, version).unwrap(); diff --git a/rs/moq-net/src/ietf/track.rs b/rs/moq-net/src/ietf/track.rs index 52f415f4820..663d046e7ad 100644 --- a/rs/moq-net/src/ietf/track.rs +++ b/rs/moq-net/src/ietf/track.rs @@ -126,6 +126,7 @@ impl Decode for TrackStatusCode { #[cfg(test)] mod tests { use super::*; + use crate::ietf::GroupOrder; fn encode_message(msg: &M, version: Version) -> Vec { let mut buf = Vec::new(); @@ -134,18 +135,9 @@ mod tests { buf.to_vec() } - /// GROUP_ORDER and MAX_CACHE_DURATION are TRACK_STATUS_OK parameters only on draft-15, - /// as on SUBSCRIBE_OK. Draft-16 ignores a GROUP_ORDER it defines elsewhere, even with a - /// value no field could hold, and rejects 0x04, which it does not define at all. Draft-18 - /// rejects a known parameter on a message that does not define it. + /// TRACK_STATUS_OK rejects a known parameter on a message that does not define it. #[test] fn track_status_ok_parameters_follow_the_draft() { - // Request 1, then GROUP_ORDER as the varint 256. - let ok = decode_message::(&[0x01, 0x01, 0x22, 0x41, 0x00], Version::Draft16) - .expect("draft-16 ignores GROUP_ORDER"); - assert_eq!(ok.properties.group_order, None); - // Request 1, then MAX_CACHE_DURATION of 5. - assert!(decode_message::(&[0x01, 0x01, 0x04, 0x05], Version::Draft16).is_err()); // GROUP_ORDER = Ascending, then an empty properties block. assert!(decode_message::(&[0x01, 0x22, 0x01], Version::Draft18).is_err()); } @@ -155,78 +147,6 @@ mod tests { crate::coding::decode_buf(&mut buf, version, M::decode_msg) } - #[test] - fn test_track_status_v14_round_trip() { - let msg = TrackStatus { - request_id: RequestId(1), - track_namespace: Path::new("test/ns"), - track_name: "video".into(), - properties_wanted: true, - epoch: None, - }; - - let encoded = encode_message(&msg, Version::Draft14); - let decoded: TrackStatus = decode_message(&encoded, Version::Draft14).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.track_namespace.as_str(), "test/ns"); - assert_eq!(decoded.track_name, "video"); - } - - #[test] - fn test_track_status_v15_round_trip() { - let msg = TrackStatus { - request_id: RequestId(1), - track_namespace: Path::new("test/ns"), - track_name: "video".into(), - properties_wanted: true, - epoch: None, - }; - - let encoded = encode_message(&msg, Version::Draft15); - let decoded: TrackStatus = decode_message(&encoded, Version::Draft15).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.track_namespace.as_str(), "test/ns"); - assert_eq!(decoded.track_name, "video"); - } - - #[test] - fn test_track_status_v17_round_trip() { - let msg = TrackStatus { - request_id: RequestId(1), - track_namespace: Path::new("test/ns"), - track_name: "video".into(), - properties_wanted: true, - epoch: None, - }; - - let encoded = encode_message(&msg, Version::Draft17); - let decoded: TrackStatus = decode_message(&encoded, Version::Draft17).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.track_namespace.as_str(), "test/ns"); - assert_eq!(decoded.track_name, "video"); - } - - #[test] - fn test_track_status_v16_round_trip() { - let msg = TrackStatus { - request_id: RequestId(1), - track_namespace: Path::new("test/ns"), - track_name: "video".into(), - properties_wanted: true, - epoch: None, - }; - - let encoded = encode_message(&msg, Version::Draft16); - let decoded: TrackStatus = decode_message(&encoded, Version::Draft16).unwrap(); - - assert_eq!(decoded.request_id, RequestId(1)); - assert_eq!(decoded.track_namespace.as_str(), "test/ns"); - assert_eq!(decoded.track_name, "video"); - } - #[test] fn test_track_status_v18_round_trip() { let msg = TrackStatus { @@ -245,7 +165,7 @@ mod tests { assert_eq!(decoded.track_name, "video"); } - const ALL: [Version; 9] = [ + const ALL: [Version; 5] = [ Version::Draft18, Version::Draft19, Version::Draft20, @@ -274,9 +194,8 @@ mod tests { } } - /// TRACK_STATUS_OK carries what SUBSCRIBE_OK would, as far as each draft's answer - /// has room: draft-14 is a SUBSCRIBE_OK, drafts 15-17 a REQUEST_OK with only - /// parameters, and from draft-18 the REQUEST_OK ends with the Track Properties. + /// TRACK_STATUS_OK carries what SUBSCRIBE_OK would: a REQUEST_OK ending with the Track + /// Properties. #[test] fn test_track_status_ok_round_trip() { let properties = Properties { @@ -299,32 +218,11 @@ mod tests { decode_message(&encode_message(&msg, version), version).unwrap_or_else(|e| panic!("{version}: {e}")); let expected = properties.clone(); - assert_eq!(decoded.request_id, msg.request_id, "{version}"); assert_eq!(decoded.largest, largest, "{version}"); assert_eq!(decoded.properties, expected, "{version}"); } } - /// Draft-14's TRACK_STATUS_OK is a SUBSCRIBE_OK with Track Alias 0, under its own type. - #[test] - fn test_track_status_ok_draft_14_is_a_subscribe_ok() { - let msg = TrackStatusOk { - largest: None, - properties: Properties::default(), - }; - assert_eq!(TrackStatusOk::id(Version::Draft14), 0x0E); - assert_eq!(TrackStatusOk::id(Version::Draft15), 0x07); - let subscribe_ok = SubscribeOk { - track_alias: 0, - largest: None, - properties: Properties::default(), - }; - assert_eq!( - encode_message(&msg, Version::Draft14), - encode_message(&subscribe_ok, Version::Draft14) - ); - } - /// TRACK_STATUS is identical to SUBSCRIBE on every draft, so it carries whatever /// fields and parameters a SUBSCRIBE can. A peer that sends them must still be /// answered rather than having its session closed. @@ -336,19 +234,7 @@ mod tests { ]; #[rustfmt::skip] - let cases: [(Version, &[u8]); 3] = [ - (Version::Draft14, &[ - 0x80, // Subscriber Priority - 0x02, // Group Order - 0x00, // Forward - 0x03, 0x05, 0x01, // Filter Type AbsoluteStart, at {5, 1} - 0x00, // Number of Parameters - ]), - (Version::Draft15, &[ - 0x02, // Number of Parameters - 0x10, 0x00, // FORWARD = 0 - 0x20, 0x01, // SUBSCRIBER_PRIORITY = 1 - ]), + let cases: [(Version, &[u8]); 1] = [ (Version::Draft20, &[ 0x02, // Number of Parameters 0x03, 0x03, 0x03, 0x00, 0xAA, // AUTHORIZATION TOKEN diff --git a/rs/moq-net/src/ietf/version.rs b/rs/moq-net/src/ietf/version.rs index 59f22f18ddb..f321f8741ba 100644 --- a/rs/moq-net/src/ietf/version.rs +++ b/rs/moq-net/src/ietf/version.rs @@ -103,8 +103,6 @@ mod tests { }; let subscribe_ok = SubscribeOk { - // Draft-17 and newer carry the request on the stream, not in the message. - request_id: None, track_alias: 4, largest: Some(Location { group: 5, object: 2 }), properties: properties.clone(), diff --git a/rs/moq-net/src/lite/compress.rs b/rs/moq-net/src/lite/compress.rs index 282a898da88..41f1ff0bac9 100644 --- a/rs/moq-net/src/lite/compress.rs +++ b/rs/moq-net/src/lite/compress.rs @@ -577,7 +577,7 @@ mod tests { #[test] fn a_tie_stays_literal_where_hop_100_sizes_differ() { let quic = Form::Quic; - let leading = Form::LeadingOnes { seven: true }; + let leading = Form::LeadingOnes; assert_eq!(varint_size(100, quic), 2); assert_eq!(varint_size(100, leading), 1); diff --git a/rs/moq-net/src/lite/ietf.rs b/rs/moq-net/src/lite/ietf.rs index f39c8172bed..40f3e62dc27 100644 --- a/rs/moq-net/src/lite/ietf.rs +++ b/rs/moq-net/src/lite/ietf.rs @@ -504,9 +504,9 @@ mod tests { #[test] fn properties_round_trip() { - // An immutable block in draft-16 holding a gap of 2 (0x3e) and a timestamp. + // An immutable block holding a gap of 2 (0x3e) and a timestamp. let mut sealed = Vec::new(); - let e = &mut Encoder::new(&mut sealed, Draft::Draft16.into()); + let e = &mut Encoder::new(&mut sealed, Draft::Draft18.into()); for value in [0x10u64, 77, 0x3e - 0x10, 2] { e.varint(value).unwrap(); } @@ -522,16 +522,16 @@ mod tests { ]; let properties = Properties { sealed_time: Some(crate::Timestamp::new(77, Timescale::MILLI).unwrap()), - block: crate::ietf::object::block(&values, Draft::Draft16).unwrap(), + block: crate::ietf::object::block(&values, Draft::Draft18).unwrap(), }; let body = encode_properties(&properties); - let decoded = decode_properties(body, from(Draft::Draft16, Some(Timescale::MILLI)), true).unwrap(); + let decoded = decode_properties(body, from(Draft::Draft18, Some(Timescale::MILLI)), true).unwrap(); assert_eq!(decoded.properties, properties); assert_eq!(decoded.gap, Some(2)); // None at all is an empty body. assert!(encode_properties(&Properties::default()).is_empty()); - let empty = decode_properties(&[], from(Draft::Draft16, Some(Timescale::MILLI)), true).unwrap(); + let empty = decode_properties(&[], from(Draft::Draft18, Some(Timescale::MILLI)), true).unwrap(); assert_eq!(empty.properties, Properties::default()); // An untimed track's Timestamp has no frame to hold it, so it stays in the block. @@ -674,13 +674,6 @@ mod tests { decode(&wire(&[opaque(0x40)], 0)).is_err(), "properties on a moq-lite track" ); - for version in [Draft::Draft15] { - assert!(decode(&wire(&[], code(version))).is_ok(), "{version}"); - assert!( - decode(&wire(&[opaque(0x40)], code(version))).is_err(), - "{version} has no Track Properties" - ); - } for code in [1, 0xff000099, crate::Version::Lite(V).code()] { assert!(decode(&wire(&[], code)).is_err(), "{code:#x} is no moq-transport draft"); } @@ -789,7 +782,7 @@ mod tests { /// could not cross is refused where it arrives rather than where it would be sent. #[test] fn track_properties_that_cannot_cross_are_refused_at_ingress() { - let version = Draft::Draft16; + let version = Draft::Draft18; // The most a draft-16 answer holds beside a Request ID and an empty parameter count. let size = super::super::message::MAX_MESSAGE_SIZE - 2; // DYNAMIC_GROUPS, then the last ID whose delta from it fits four bytes. @@ -839,7 +832,7 @@ mod tests { }; let decoded = decode_datagram( &mut Decoder::new(&datagram(&wire), V.into()), - from(Draft::Draft14, None), + from(Draft::Draft18, None), ); assert_eq!(decoded.is_ok(), ok, "{status}"); } @@ -963,36 +956,21 @@ mod tests { } } - /// Only draft-14 lets a status object carry properties, so the END_OF_GROUP record has a - /// block only on a draft-14 track. + /// A status object carries no properties, so the END_OF_GROUP record has no block. #[cfg(feature = "async")] #[test] - fn an_end_record_block_needs_a_draft_that_carries_one() { - for version in [Draft::Draft22] { - let framing = from(version, None); - assert_eq!( - decode_end(&[0x20, 1], framing).is_ok(), - version == Draft::Draft14, - "{version}" - ); - assert!(decode_end(&[], framing).is_ok(), "{version}"); - } + fn an_end_record_has_no_block() { + let framing = from(Draft::Draft22, None); + assert!(decode_end(&[0x20, 1], framing).is_err()); + assert!(decode_end(&[], framing).is_ok()); } - /// A datagram must be one its track's draft defines: from draft-17 on a non-zero Status - /// carries no properties, and only drafts 14 and 15 define DOES_NOT_EXIST. + /// A datagram must be one its track's draft defines: a non-zero Status carries no + /// properties. #[test] fn a_datagram_block_needs_a_draft_that_sends_its_shape() { - let drafts = [ - Draft::Draft22, - ]; - for (status, carried) in [ - (0, &drafts[..]), - (1, &drafts[..2]), - (3, &drafts[..3]), - (4, &drafts[..3]), - ] { - for version in drafts { + for (status, carried) in [(0, true), (1, false), (3, false), (4, false)] { + for version in [Draft::Draft18, Draft::Draft22] { // Object 3 at priority 9, which every draft can send beside a status. let mut buf = Vec::new(); let w = &mut Encoder::new(&mut buf, V.into()); @@ -1002,7 +980,7 @@ mod tests { w.varint(status).unwrap(); w.bytes(&[0x20, 1]).unwrap(); let decoded = decode_datagram(&mut Decoder::new(&buf, V.into()), from(version, None)); - assert_eq!(decoded.is_ok(), carried.contains(&version), "{status}: {version}"); + assert_eq!(decoded.is_ok(), carried, "{status}: {version}"); } } } diff --git a/rs/moq-net/src/lite/test_transport.rs b/rs/moq-net/src/lite/test_transport.rs index 5614b0897a7..7b8ca73edd6 100644 --- a/rs/moq-net/src/lite/test_transport.rs +++ b/rs/moq-net/src/lite/test_transport.rs @@ -679,14 +679,6 @@ impl SinkSession { } } - /// Write this prefix on each bidi stream before holding its remaining bytes. - pub fn gated_bi_prefix(prefix: usize, gate: kio::Consumer) -> Self { - Self { - bi_prefix: prefix, - ..Self::gated_bi(gate) - } - } - /// Accept bidi streams from the peer, holding every write until `gate` flips to true. /// /// The accepted half of a control stream carries the answers (track info, diff --git a/rs/moq-net/src/server.rs b/rs/moq-net/src/server.rs index ca6f9ad1431..5901bab5f98 100644 --- a/rs/moq-net/src/server.rs +++ b/rs/moq-net/src/server.rs @@ -8,7 +8,7 @@ use crate::transport::poll::Session as _; use crate::{ ALPN_18, ALPN_19, ALPN_20, ALPN_21, ALPN_22, ALPN_LITE, ALPN_LITE_03, ALPN_LITE_04, ALPN_LITE_05, ALPN_LITE_06, ALPN_LITE_07_WIP, Consume, Error, NEGOTIATED, Session, SessionError, Version, Versions, - coding::{Decode, Encode, Stream}, + coding::{Encode, Stream}, ietf, lite, setup::{self, Role}, stats, @@ -1155,16 +1155,15 @@ mod tests { } /// Encode a draft 14-16 CLIENT_SETUP, sent on the control bidi stream. - fn legacy_setup(version: ietf::Version, params: ietf::Parameters) -> Vec { + /// A moq-lite-02 CLIENT_SETUP, sent on the bidi stream the `moql` ALPN negotiates over. + fn moql_setup() -> Vec { + let version = crate::Version::Lite(lite::Version::Lite02); let mut buf = Vec::new(); setup::Client { - versions: crate::coding::Versions::from([crate::Version::Ietf(version).into()]), - parameters: params.encode_bytes(version).unwrap(), + versions: crate::coding::Versions::from([version.into()]), + parameters: Bytes::new(), } - .encode( - &mut crate::coding::Encoder::new(&mut buf, (crate::Version::Ietf(version)).into()), - crate::Version::Ietf(version), - ) + .encode(&mut crate::coding::Encoder::new(&mut buf, version.into()), version) .unwrap(); buf } @@ -1186,20 +1185,13 @@ mod tests { /// session the driver then runs, through the REAL accept path (`accept_request` -> `ok`). /// The session's own `auth()` (the handle the driver polls) routes a request token to that /// pre-ok consumer instead of falling to the `Unsupported` default, and granting admits. - /// Both a no-AUTH-extension legacy session (draft-14, `Handle::new(false)`) and the modern - /// uni-SETUP path (draft-18); a QUIC server has no other point to install the verifier. + /// A QUIC server has no other point to install the verifier. #[moq_clock::test] async fn a_pre_ok_verifier_governs_the_session() { - let cases = [ - ( - FakeSession::new(ALPN_14, []) - .with_bi(legacy_setup(ietf::Version::Draft14, ietf::Parameters::default())), - ), - ( - FakeSession::new(ALPN_18, [ietf_setup(ietf::Version::Draft18, Some("room/alice"))]), - ietf::Version::Draft18, - ), - ]; + let cases = [( + FakeSession::new(ALPN_18, [ietf_setup(ietf::Version::Draft18, Some("room/alice"))]), + ietf::Version::Draft18, + )]; for (session, version) in cases { let mut request = Server::new() @@ -1233,10 +1225,8 @@ mod tests { #[moq_clock::test] async fn a_reply_the_peer_never_takes_fails_at_the_deadline() { let registry = stats::Registry::new(Default::default()); - let version = ietf::Version::Draft16; - let session = FakeSession::new(ALPN_16, []) - .with_bi(legacy_setup(version, ietf::Parameters::default())) - .withholding_credit(); + let version = Version::Lite(lite::Version::Lite02); + let session = FakeSession::new(ALPN_LITE, []).with_bi(moql_setup()).withholding_credit(); let start = moq_clock::now(); let deadline = start + std::time::Duration::from_secs(10); let request = Server::new() @@ -1252,7 +1242,7 @@ mod tests { assert_eq!(moq_clock::now(), deadline); assert_eq!( session.close_code(), - Some(Version::Ietf(version).session_code(&SessionError::Timeout)) + Some(version.session_code(&SessionError::Timeout)) ); let failed = registry.snapshot().sessions()[0].1.sessions_failed; assert_eq!(failed.count(stats::Failure::Application), 1, "{failed:?}"); @@ -1263,10 +1253,8 @@ mod tests { #[moq_clock::test] async fn an_abandoned_reply_fails_like_the_deadline() { let registry = stats::Registry::new(Default::default()); - let version = ietf::Version::Draft16; - let session = FakeSession::new(ALPN_16, []) - .with_bi(legacy_setup(version, ietf::Parameters::default())) - .withholding_credit(); + let version = Version::Lite(lite::Version::Lite02); + let session = FakeSession::new(ALPN_LITE, []).with_bi(moql_setup()).withholding_credit(); let request = Server::new() .accept_request(moq_clock::now(), session.clone()) .await @@ -1276,7 +1264,7 @@ mod tests { assert!(abandoned.is_err(), "a reply the peer never took completed"); assert_eq!( session.close_code(), - Some(Version::Ietf(version).session_code(&SessionError::Timeout)) + Some(version.session_code(&SessionError::Timeout)) ); let failed = registry.snapshot().sessions()[0].1.sessions_failed; assert_eq!(failed.count(stats::Failure::Application), 1, "{failed:?}"); @@ -1316,14 +1304,8 @@ mod tests { token_params(ietf::Version::Draft19), )], ); - let legacy = FakeSession::new(ALPN_16, []).with_bi(legacy_setup( - ietf::Version::Draft16, - token_params(ietf::Version::Draft16), - )); - for (name, session) in [("draft-19", modern), ("draft-16", legacy)] { - let request = Server::new().accept_request(moq_clock::now(), session).await.unwrap(); - assert_eq!(request.token(), Some(&setup_token()), "{name}"); - } + let request = Server::new().accept_request(moq_clock::now(), modern).await.unwrap(); + assert_eq!(request.token(), Some(&setup_token())); } #[moq_clock::test] @@ -1336,8 +1318,7 @@ mod tests { } } - /// A SETUP the server refuses closes the session with the code naming why, on both - /// the draft-17+ uni stream and the draft 14-16 bidi stream. + /// A SETUP the server refuses closes the session with the code naming why. #[moq_clock::test] async fn a_refused_setup_token_closes_with_its_code() { let delete = [0x0, 0x7]; // DELETE alias 7 @@ -1349,22 +1330,18 @@ mod tests { let mut params = ietf::Parameters::default(); params.set_bytes(ietf::ParameterBytes::AuthorizationToken, raw.to_vec()); - let modern = FakeSession::new(ALPN_19, [ietf_setup_with(ietf::Version::Draft19, params.clone())]); - let legacy = FakeSession::new(ALPN_16, []).with_bi(legacy_setup(ietf::Version::Draft16, params)); - for (name, session) in [("draft-19", modern), ("draft-16", legacy)] { - let result = Server::new().accept_request(moq_clock::now(), session.clone()).await; - assert!(result.is_err(), "{name}"); - assert_eq!(session.close_code(), Some(code.to_code()), "{name} {code}"); - } + let session = FakeSession::new(ALPN_19, [ietf_setup_with(ietf::Version::Draft19, params)]); + let result = Server::new().accept_request(moq_clock::now(), session.clone()).await; + assert!(result.is_err()); + assert_eq!(session.close_code(), Some(code.to_code()), "{code}"); } } #[moq_clock::test] async fn accept_request_reads_ietf_path() { - // Every draft-17+ version gates on the SETUP stream before starting, so the + // Every draft gates on the SETUP stream before starting, so the // path is known at authorization time just like lite-05. for (alpn, version) in [ - (ALPN_17, ietf::Version::Draft17), (ALPN_18, ietf::Version::Draft18), (ALPN_19, ietf::Version::Draft19), ] { diff --git a/rs/moq-net/src/test_interop.rs b/rs/moq-net/src/test_interop.rs index 4e746e4823d..aa6bc1d546a 100644 --- a/rs/moq-net/src/test_interop.rs +++ b/rs/moq-net/src/test_interop.rs @@ -36,7 +36,7 @@ fn varint_interop() { values.push(varint::MAX_QUIC); let quic = Form::Quic; - let leading_ones = Form::from(ietf::Version::Draft17); + let leading_ones = Form::from(ietf::Version::Draft18); let encode = |v: u64, form: Form| { let mut buf = Vec::new(); Encoder::new(&mut buf, form).varint(v).unwrap(); @@ -259,7 +259,7 @@ fn ietf_datagram_interop() { ietf::Version::Draft21, ietf::Version::Draft22, ]; - for (draft, version) in (14..).zip(all) { + for (draft, version) in (18..).zip(all) { let legacy = false; let mut properties = Vec::new(); let mut w = Encoder::new(&mut properties, version.into()); @@ -442,8 +442,7 @@ fn ietf_track_status_interop() { ietf::Version::Draft21, ietf::Version::Draft22, ]; - for (draft, version) in (14..).zip(all) { - let legacy = false; + for (draft, version) in (18..).zip(all) { let request = |id: u64, namespace: &str, wanted: bool| { let mut buf = Vec::new(); let mut w = Encoder::new(&mut buf, version.into()); @@ -469,12 +468,11 @@ fn ietf_track_status_interop() { for (index, wanted) in [true, false].into_iter().enumerate() { let mut r = Decoder::new(&responses[index], version.into()); - assert_eq!(r.varint().unwrap(), ietf::TrackStatusOk::ID, "{version}"); + assert_eq!(r.varint().unwrap(), ::ID, "{version}"); let ok = ietf::TrackStatusOk::decode(&mut r, version).unwrap(); assert!(r.is_empty(), "{version}: trailing bytes"); let expected = match version { - // GROUP_ORDER is a field on draft-14 and a parameter on draft-15. // Only draft-20 can carry the opt-out. _ if !wanted && ietf::Filter::is_draft20(version) => ietf::Properties::default(), _ => ietf::Properties { @@ -491,7 +489,6 @@ fn ietf_track_status_interop() { assert_eq!( ok, ietf::TrackStatusOk { - request_id: legacy.then_some(ietf::RequestId(1 + 2 * index as u64)), largest: Some(ietf::Location { group: 4, object: 1 }), properties: expected, }, diff --git a/rs/moq-net/tests/integration/announce_covering_route.rs b/rs/moq-net/tests/integration/announce_covering_route.rs index 20e152994c2..bf297c603a7 100644 --- a/rs/moq-net/tests/integration/announce_covering_route.rs +++ b/rs/moq-net/tests/integration/announce_covering_route.rs @@ -109,10 +109,6 @@ async fn remote_ietf_peer_hears_the_covering_route() { let mut expected = EXPECTED.to_vec(); expected.push("session open: true"); for version in [ - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", "moq-transport-18", "moq-transport-19", "moq-transport-20", diff --git a/rs/moq-net/tests/integration/announce_to_serve.rs b/rs/moq-net/tests/integration/announce_to_serve.rs index eb71f54291c..452c22848cc 100644 --- a/rs/moq-net/tests/integration/announce_to_serve.rs +++ b/rs/moq-net/tests/integration/announce_to_serve.rs @@ -237,10 +237,6 @@ async fn remote_ietf_consumer_sees_what_a_local_one_does() { // Drafts 14 to 16 carry each request over the control stream; 17 and later give it // its own stream. Both have to end a finished track the way moq-lite does. for version in [ - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", "moq-transport-18", "moq-transport-19", "moq-transport-20", diff --git a/rs/moq-net/tests/integration/auth.rs b/rs/moq-net/tests/integration/auth.rs index 91a4de62dc2..7320dce5b8b 100644 --- a/rs/moq-net/tests/integration/auth.rs +++ b/rs/moq-net/tests/integration/auth.rs @@ -31,8 +31,8 @@ const LITE_07: &str = "moq-lite-07-wip"; const LITE_06: &str = "moq-lite-06"; /// The lite Auth Stream's type byte. const AUTH_STREAM: u8 = 0x7; -/// The first draft that negotiates MoQ Auth, and the newest. -const MOQT_17: &str = "moq-transport-17"; +/// The oldest draft we speak, which negotiates MoQ Auth, and the newest. +const MOQT_18: &str = "moq-transport-18"; const MOQT_22: &str = "moq-transport-22"; /// Run each case on every version that exchanges AUTH. @@ -41,8 +41,8 @@ macro_rules! cases { mod lite_07 { $(#[moq_clock::test] async fn $case() { super::$case(super::LITE_07).await })* } - mod moqt_17 { - $(#[moq_clock::test] async fn $case() { super::$case(super::MOQT_17).await })* + mod moqt_18 { + $(#[moq_clock::test] async fn $case() { super::$case(super::MOQT_18).await })* } mod moqt_22 { $(#[moq_clock::test] async fn $case() { super::$case(super::MOQT_22).await })* @@ -87,8 +87,8 @@ async fn lite_07_presents_a_transiently_refused_token_again() { /// moq-transport's u16 message size is the one a test can overflow quickly. #[moq_clock::test] -async fn moqt_17_a_grant_too_large_for_one_message_is_unsupported() { - a_grant_too_large_for_one_message_is_unsupported(MOQT_17).await +async fn moqt_18_a_grant_too_large_for_one_message_is_unsupported() { + a_grant_too_large_for_one_message_is_unsupported(MOQT_18).await } #[moq_clock::test] @@ -125,37 +125,11 @@ async fn lite_05_has_no_grant() { older_versions_have_no_grant("moq-lite-05").await } -/// The #6566 matrix's drafts: one without MoQ Auth, and one with it. -#[moq_clock::test] -async fn moqt_16_admits_exact_and_subtree_grants() { - exact_and_subtree_grants_admit_their_broadcast("moq-transport-16").await -} - -#[moq_clock::test] -async fn moqt_18_admits_exact_and_subtree_grants() { - exact_and_subtree_grants_admit_their_broadcast("moq-transport-18").await -} - -#[moq_clock::test] -async fn moqt_16_has_no_grant() { - older_versions_have_no_grant("moq-transport-16").await -} - -#[moq_clock::test] -async fn moqt_16_route_auth_renews_its_announce() { - a_route_auth_renews_its_announce("moq-transport-16").await -} - #[moq_clock::test] async fn moqt_22_route_auth_renews_its_announce() { a_route_auth_renews_its_announce(MOQT_22).await } -#[moq_clock::test] -async fn moqt_16_route_auth_survives_a_reconnect() { - a_route_auth_survives_a_reconnect("moq-transport-16").await -} - #[moq_clock::test] async fn moqt_22_route_auth_survives_a_reconnect() { a_route_auth_survives_a_reconnect(MOQT_22).await @@ -1072,10 +1046,7 @@ async fn exact_and_subtree_grants_admit_their_broadcast(version: &'static str) { let mut sub = remote.track("video").unwrap().subscribe(prefs).await.unwrap(); sub.recv_group().await.unwrap().expect("a group"); - match version == "moq-transport-16" { - true => assert_eq!(pair.client.auth().grant().peek(), None), - false => assert_eq!(granted(&pair.client).await.subscribe, scope), - } + assert_eq!(granted(&pair.client).await.subscribe, scope); assert_eq!(pair.client_transport.close_reason(), None); }) .await @@ -1493,11 +1464,8 @@ macro_rules! limit_cases { mod limit_lite_07 { $(#[moq_clock::test] async fn $case() { super::$case(super::LITE_07).await })* } - mod limit_moqt_16 { - $(#[moq_clock::test] async fn $case() { super::$case("moq-transport-16").await })* - } - mod limit_moqt_17 { - $(#[moq_clock::test] async fn $case() { super::$case(super::MOQT_17).await })* + mod limit_moqt_18 { + $(#[moq_clock::test] async fn $case() { super::$case(super::MOQT_18).await })* } }; } @@ -1526,7 +1494,7 @@ async fn lite_07_narrowing_resets_a_fetch_in_flight() { /// Whether `version` exchanges AUTH, so the peer also hears of a narrowing. fn speaks_auth(version: &str) -> bool { - matches!(version, LITE_07 | MOQT_17 | MOQT_22) + matches!(version, LITE_07 | MOQT_18 | MOQT_22) } /// A revocation as the reader sees it: the local gate's own error, or the peer's diff --git a/rs/moq-net/tests/integration/broadcast_close.rs b/rs/moq-net/tests/integration/broadcast_close.rs index b2126764dc0..1c6d1e8628d 100644 --- a/rs/moq-net/tests/integration/broadcast_close.rs +++ b/rs/moq-net/tests/integration/broadcast_close.rs @@ -59,7 +59,7 @@ async fn close_ends_a_local_consumer() { #[moq_clock::test] async fn close_ends_a_remote_consumer() { - for version in ["moq-lite-05", "moq-transport-14", "moq-transport-19"] { + for version in ["moq-lite-05", "moq-transport-19"] { let publisher = produce_origin(1); let subscriber = produce_origin(2); let mut options = MockConnectOptions::new(version.parse::().unwrap()); diff --git a/rs/moq-net/tests/integration/catalog_acquisition.rs b/rs/moq-net/tests/integration/catalog_acquisition.rs index ca4bc66cf77..8a5f8111b25 100644 --- a/rs/moq-net/tests/integration/catalog_acquisition.rs +++ b/rs/moq-net/tests/integration/catalog_acquisition.rs @@ -80,6 +80,6 @@ async fn epochless_replacement_refuses_pending_catalog() { #[moq_clock::test] async fn same_epoch_replacement_acquires_pending_catalog() { acquisition("moq-lite-07-wip", true).await; - acquisition("moq-transport-17", true).await; + acquisition("moq-transport-18", true).await; acquisition("moq-transport-22", true).await; } diff --git a/rs/moq-net/tests/integration/compress.rs b/rs/moq-net/tests/integration/compress.rs index 8f6b53cca01..2df16b3434f 100644 --- a/rs/moq-net/tests/integration/compress.rs +++ b/rs/moq-net/tests/integration/compress.rs @@ -19,10 +19,6 @@ const VERSIONS: &[&str] = &[ "moq-lite-07-wip", "moq-lite-06", "moq-lite-03", - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", "moq-transport-18", "moq-transport-22", ]; @@ -123,7 +119,6 @@ async fn relay(published: Published, hops: [&str; 2], subscription: moq_net::tra /// draft-17 and later. const RANGED: &[&str] = &[ "moq-lite-07-wip", - "moq-transport-17", "moq-transport-18", "moq-transport-22", ]; @@ -200,7 +195,7 @@ async fn relay_serves_every_version() { fixture.subscriber.info().compress, !matches!( version, - "moq-lite-06" | "moq-lite-03" | "moq-transport-14" | "moq-transport-15" | "moq-transport-16" + "moq-lite-06" | "moq-lite-03" ), "{version}: compression declared" ); @@ -249,8 +244,7 @@ async fn datagrams_serve_every_version() { for &version in &[ "moq-lite-07-wip", "moq-lite-06", - "moq-transport-16", - "moq-transport-17", + "moq-transport-18", "moq-transport-22", ] { moq_clock::timeout(TIMEOUT, async { @@ -343,18 +337,16 @@ async fn datagrams_meter_the_bytes_on_the_wire() { /// Negotiated IETF ingress stays compressed across a relay to IETF or lite-07 egress. #[moq_clock::test] async fn ietf_ingress_stays_compressed_across_protocols() { - for upstream in ["moq-transport-17", "moq-transport-18", "moq-transport-22"] { + for upstream in ["moq-transport-18", "moq-transport-22"] { for downstream in [ "moq-lite-07-wip", - "moq-transport-16", "moq-transport-22", - "moq-transport-14", ] { moq_clock::timeout(TIMEOUT, async { let mut fixture = connect(upstream, downstream).await; assert_eq!( fixture.subscriber.info().compress, - !matches!(downstream, "moq-transport-14" | "moq-transport-16"), + true, "{upstream} -> {downstream}" ); let mut group = fixture.track.append_group().unwrap(); @@ -397,7 +389,7 @@ async fn ietf_ingress_stays_compressed_across_protocols() { /// A finished compressed track remains fetchable without subscribing to its live feed. #[moq_clock::test] async fn ietf_fetch_keeps_the_stored_form() { - for version in ["moq-transport-16", "moq-transport-18", "moq-transport-22"] { + for version in [ "moq-transport-18", "moq-transport-22"] { moq_clock::timeout(TIMEOUT, async { let publisher = produce_origin(1); let broadcast = publisher.create_broadcast("bcast").unwrap(); @@ -419,7 +411,7 @@ async fn ietf_fetch_keeps_the_stored_form() { origin.consume().routed("bcast").await.unwrap(); let remote = origin.consume().request_broadcast("bcast", None).await.unwrap(); let consumer = remote.track("data").unwrap(); - assert_eq!(consumer.query().await.unwrap().compress, version != "moq-transport-16"); + assert!(consumer.query().await.unwrap().compress, "{version}"); let mut fetched = consumer.fetch_group(0, moq_net::group::Fetch::default()).await.unwrap(); for index in 0..3 { assert_eq!( diff --git a/rs/moq-net/tests/integration/control_stats.rs b/rs/moq-net/tests/integration/control_stats.rs index 554859399b6..ac1795fb18e 100644 --- a/rs/moq-net/tests/integration/control_stats.rs +++ b/rs/moq-net/tests/integration/control_stats.rs @@ -23,9 +23,6 @@ async fn control_classes_cross_lite_and_ietf_sessions_without_counting_payload() "moq-lite-03", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", "moq-transport-22", ] { let publisher = origin(); @@ -76,7 +73,7 @@ async fn control_classes_cross_lite_and_ietf_sessions_without_counting_payload() /// admitted with, even for a session with neither origin. #[moq_clock::test] async fn setup_counts_under_the_admitted_stats() { - for version in ["moq-lite-07-wip", "moq-transport-14", "moq-transport-22"] { + for version in ["moq-lite-07-wip", "moq-transport-22"] { let version: moq_net::Version = version.parse().unwrap(); let registry = Registry::new(Default::default()); let tier = Tier::new("internal"); diff --git a/rs/moq-net/tests/integration/datagram.rs b/rs/moq-net/tests/integration/datagram.rs index 2b373c9c6f6..2544bf07394 100644 --- a/rs/moq-net/tests/integration/datagram.rs +++ b/rs/moq-net/tests/integration/datagram.rs @@ -125,9 +125,7 @@ async fn datagrams_reach_the_subscriber_in_order() { #[moq_clock::test] async fn ietf_delivers_datagrams() { for version in [ - "moq-transport-14", - "moq-transport-16", - "moq-transport-17", + "moq-transport-18", "moq-transport-20", ] { moq_clock::timeout(TEST_TIMEOUT, ietf_delivers_datagrams_on(version)) @@ -160,13 +158,10 @@ async fn ietf_delivers_datagrams_on(version: &str) { .unwrap(); let mut before = subscriber.recv_group().await.unwrap().unwrap(); assert_eq!(before.sequence, 0, "{version}"); - // Drafts 14 to 16 can't carry TIMESCALE, so the track arrives untimed, datagrams - // included. - let timed = !matches!(version, "moq-transport-14" | "moq-transport-16"); let first = before.next_frame().await.unwrap().unwrap().timestamp; assert_eq!( first.map(|timestamp| timestamp.is_zero()), - timed.then_some(true), + Some(true), "{version}" ); @@ -185,7 +180,7 @@ async fn ietf_delivers_datagrams_on(version: &str) { datagram .timestamp .map(|timestamp| timestamp.convert(expected.scale()).unwrap()), - timed.then_some(expected), + Some(expected), "{version}" ); diff --git a/rs/moq-net/tests/integration/dial_split_horizon.rs b/rs/moq-net/tests/integration/dial_split_horizon.rs index ca4df3e39e7..dc05613fde1 100644 --- a/rs/moq-net/tests/integration/dial_split_horizon.rs +++ b/rs/moq-net/tests/integration/dial_split_horizon.rs @@ -22,7 +22,7 @@ use support::{ const SETTLE: Duration = Duration::from_secs(1); fn version() -> Version { - "moq-transport-16".parse().unwrap() + "moq-transport-18".parse().unwrap() } fn produce(hop: u64) -> origin::Producer { diff --git a/rs/moq-net/tests/integration/empty_group.rs b/rs/moq-net/tests/integration/empty_group.rs index ad7e1dd7c65..10e9fefe93c 100644 --- a/rs/moq-net/tests/integration/empty_group.rs +++ b/rs/moq-net/tests/integration/empty_group.rs @@ -20,9 +20,7 @@ const TIMEOUT: Duration = Duration::from_secs(10); const VERSIONS: &[&str] = &[ "moq-lite-06", - "moq-transport-14", - "moq-transport-16", - "moq-transport-17", + "moq-transport-18", "moq-transport-22", ]; diff --git a/rs/moq-net/tests/integration/extensions.rs b/rs/moq-net/tests/integration/extensions.rs index 7e8364c60db..c23ce64e9eb 100644 --- a/rs/moq-net/tests/integration/extensions.rs +++ b/rs/moq-net/tests/integration/extensions.rs @@ -16,11 +16,7 @@ use support::harness::{MockConnectOptions, MockPair, connect_mock}; const SETTLE: Duration = Duration::from_secs(1); /// Every moq-transport draft, each of which carries the SOLICIT option. -const MOQT: [&str; 9] = [ - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", +const MOQT: [&str; 5] = [ "moq-transport-18", "moq-transport-19", "moq-transport-20", @@ -28,8 +24,8 @@ const MOQT: [&str; 9] = [ "moq-transport-22", ]; -/// The first draft that negotiates MoQ Auth, and the newest. -const AUTH: [&str; 2] = ["moq-transport-17", "moq-transport-22"]; +/// The oldest draft we speak, which negotiates MoQ Auth, and the newest. +const AUTH: [&str; 2] = ["moq-transport-18", "moq-transport-22"]; fn produce_origin(hop: u64) -> origin::Producer { let (producer, driver) = origin::Producer::new(origin::Config::new(Hop::new(hop).unwrap())); @@ -183,7 +179,6 @@ fn route(origin: &origin::Producer, path: &str) -> Option { async fn an_epoch_crosses_only_when_both_offer_it() { for name in MOQT { let version: Version = name.parse().unwrap(); - let negotiable = !matches!(name, "moq-transport-14" | "moq-transport-15" | "moq-transport-16"); for server_publishes in [true, false] { for declines in [None, Some("server"), Some("client")] { let context = format!("{name}, server publishes: {server_publishes}, declines: {declines:?}"); @@ -217,7 +212,7 @@ async fn an_epoch_crosses_only_when_both_offer_it() { moq_clock::sleep(SETTLE).await; let route = route(&subscriber, "room/cam").unwrap_or_else(|| panic!("{context}: never announced")); - let expected = (negotiable && declines.is_none()).then_some(epoch); + let expected = declines.is_none().then_some(epoch); assert_eq!(route.epoch, expected, "{context}"); assert_open(&pair, &context); } diff --git a/rs/moq-net/tests/integration/fetch_only.rs b/rs/moq-net/tests/integration/fetch_only.rs index 70d7bd7fffb..9f70c97e990 100644 --- a/rs/moq-net/tests/integration/fetch_only.rs +++ b/rs/moq-net/tests/integration/fetch_only.rs @@ -91,9 +91,6 @@ fn frames(sequence: u64) -> Vec> { /// Draft-17 is absent: its TRACK_STATUS answer cannot say whether the track is timed, /// so it still SUBSCRIBEs to learn the track. const VERSIONS: &[&str] = &[ - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", "moq-transport-18", "moq-transport-19", "moq-transport-22", diff --git a/rs/moq-net/tests/integration/finite_relay.rs b/rs/moq-net/tests/integration/finite_relay.rs index ff700f21e43..8d16ffad4f9 100644 --- a/rs/moq-net/tests/integration/finite_relay.rs +++ b/rs/moq-net/tests/integration/finite_relay.rs @@ -33,8 +33,7 @@ const VERSIONS: &[&str] = &[ "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-17", + "moq-transport-18", "moq-transport-22", ]; diff --git a/rs/moq-net/tests/integration/goaway.rs b/rs/moq-net/tests/integration/goaway.rs index 72f1395032f..0d69667c91a 100644 --- a/rs/moq-net/tests/integration/goaway.rs +++ b/rs/moq-net/tests/integration/goaway.rs @@ -35,9 +35,6 @@ const TEST_TIMEOUT: Duration = Duration::from_secs(10); const GOAWAY_VERSIONS: &[&str] = &[ "moq-lite-04", "moq-lite-05", - "moq-transport-14", - "moq-transport-16", - "moq-transport-17", "moq-transport-18", "moq-transport-19", ]; @@ -138,7 +135,7 @@ async fn goaway_send_receive_all_versions() { #[moq_clock::test] async fn goaway_wire_timeout_moq_transport_17() { moq_clock::timeout(TEST_TIMEOUT, async { - let version: Version = "moq-transport-17".parse().unwrap(); + let version: Version = "moq-transport-18".parse().unwrap(); let pair = connect_mock(MockConnectOptions::new(version)).await; pair.server @@ -253,7 +250,7 @@ async fn goaway_client_redirect_refused_moq_transport_19() { #[moq_clock::test] async fn goaway_timeout_force_close_moq_transport_17() { moq_clock::timeout(TEST_TIMEOUT, async { - let version: Version = "moq-transport-17".parse().unwrap(); + let version: Version = "moq-transport-18".parse().unwrap(); let pair = connect_mock(MockConnectOptions::new(version)).await; pair.server diff --git a/rs/moq-net/tests/integration/group_order.rs b/rs/moq-net/tests/integration/group_order.rs index 581fe3c63bc..9b11f292572 100644 --- a/rs/moq-net/tests/integration/group_order.rs +++ b/rs/moq-net/tests/integration/group_order.rs @@ -160,8 +160,7 @@ async fn a_backlog_is_served_in_the_subscription_order() { "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-17", + "moq-transport-18", "moq-transport-22", ] { for relay in [false, true] { @@ -186,7 +185,7 @@ async fn a_backlog_is_served_in_the_subscription_order() { /// its own subscriber's backlog in that subscriber's order. #[moq_clock::test] async fn merged_subscribers_keep_their_own_order() { - for version in ["moq-lite-07-wip", "moq-transport-17"] { + for version in ["moq-lite-07-wip", "moq-transport-18"] { let version: Version = version.parse().unwrap(); let publisher = produce_origin(1); let broadcast = publisher.create_broadcast("bcast").unwrap(); diff --git a/rs/moq-net/tests/integration/history_groups.rs b/rs/moq-net/tests/integration/history_groups.rs index e7ada798343..fd813fb17c2 100644 --- a/rs/moq-net/tests/integration/history_groups.rs +++ b/rs/moq-net/tests/integration/history_groups.rs @@ -25,8 +25,7 @@ const VERSIONS: &[&str] = &[ "moq-lite-03", "moq-lite-05", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-17", + "moq-transport-18", "moq-transport-22", ]; diff --git a/rs/moq-net/tests/integration/idle_session.rs b/rs/moq-net/tests/integration/idle_session.rs index 593ee2ead4f..342441eb920 100644 --- a/rs/moq-net/tests/integration/idle_session.rs +++ b/rs/moq-net/tests/integration/idle_session.rs @@ -110,7 +110,7 @@ async fn busy_viewer_sees_the_estimate_move() { /// A transport estimate demanded locally still costs one poll per sampling deadline. #[moq_clock::test] async fn ietf_stats_refresh_costs_one_driver_poll() { - for version in ["moq-transport-16", "moq-transport-22"] { + for version in [ "moq-transport-22"] { let polls = Arc::new(AtomicUsize::new(0)); let mut options = MockConnectOptions::new(version.parse::().unwrap()); options.stats = STATS; @@ -175,7 +175,7 @@ async fn probe_follows_consumer_lifetime() { /// A stats tick reads the transport's wire counters and adds no sampler timer or session wake. #[moq_clock::test] async fn wire_collection_never_arms_the_idle_sampler() { - for version in ["moq-lite-06", "moq-lite-07-wip", "moq-transport-16", "moq-transport-22"] { + for version in ["moq-lite-06", "moq-lite-07-wip", "moq-transport-22"] { let registry = moq_net::stats::Registry::new(Default::default()); let polls = Arc::new(AtomicUsize::new(0)); let mut options = MockConnectOptions::new(version.parse().unwrap()); diff --git a/rs/moq-net/tests/integration/ietf_conformance.rs b/rs/moq-net/tests/integration/ietf_conformance.rs index 07c18ad865e..fc66a14e1d5 100644 --- a/rs/moq-net/tests/integration/ietf_conformance.rs +++ b/rs/moq-net/tests/integration/ietf_conformance.rs @@ -76,14 +76,14 @@ async fn routing(version: &str, cluster: bool) { #[moq_clock::test] async fn conformance_namespaces_route_through_one_relay() { - for version in ["moq-transport-16", "moq-transport-18"] { + for version in [ "moq-transport-18"] { routing(version, false).await; } } #[moq_clock::test] async fn conformance_namespaces_route_through_a_lite_cluster() { - for version in ["moq-transport-16", "moq-transport-18"] { + for version in [ "moq-transport-18"] { routing(version, true).await; } } diff --git a/rs/moq-net/tests/integration/implementation.rs b/rs/moq-net/tests/integration/implementation.rs index 2866cd9db04..1fd35c1ee53 100644 --- a/rs/moq-net/tests/integration/implementation.rs +++ b/rs/moq-net/tests/integration/implementation.rs @@ -10,7 +10,7 @@ use moq_net::Version; use support::harness::{MockConnectOptions, connect_mock}; /// Every version, and whether its SETUP carries an implementation. -const VERSIONS: [(&str, bool); 16] = [ +const VERSIONS: [(&str, bool); 12] = [ ("moq-lite-01", false), ("moq-lite-02", false), ("moq-lite-03", false), @@ -18,10 +18,6 @@ const VERSIONS: [(&str, bool); 16] = [ ("moq-lite-05", true), ("moq-lite-06", true), ("moq-lite-07-wip", true), - ("moq-transport-14", true), - ("moq-transport-15", true), - ("moq-transport-16", true), - ("moq-transport-17", true), ("moq-transport-18", true), ("moq-transport-19", true), ("moq-transport-20", true), @@ -62,7 +58,7 @@ async fn each_side_reads_the_others_implementation() { /// Unconfigured, each side names this crate; configured empty, it advertises nothing. #[moq_clock::test] async fn the_default_names_the_crate_and_empty_omits_it() { - for name in ["moq-lite-06", "moq-transport-16", "moq-transport-22"] { + for name in ["moq-lite-06", "moq-transport-22"] { let version: Version = name.parse().unwrap(); let (seen_by_server, seen_by_client) = exchange(version, None, Some("")).await; assert_eq!(seen_by_server.as_deref(), Some(moq_net::IMPLEMENTATION), "{name}"); diff --git a/rs/moq-net/tests/integration/late_lower_group.rs b/rs/moq-net/tests/integration/late_lower_group.rs index a7aa035cdbd..06e8a1a2191 100644 --- a/rs/moq-net/tests/integration/late_lower_group.rs +++ b/rs/moq-net/tests/integration/late_lower_group.rs @@ -25,7 +25,7 @@ const FOREVER: Duration = Duration::from_millis((1 << 53) - 1); const LITE: &[&str] = &["moq-lite-05", "moq-lite-06", "moq-lite-07-wip"]; -const TRANSPORT: &[&str] = &["moq-transport-14", "moq-transport-17", "moq-transport-22"]; +const TRANSPORT: &[&str] = &[ "moq-transport-18", "moq-transport-22"]; /// From `start`, or the oldest group still around when there is none. fn floored(start: Option) -> Range { diff --git a/rs/moq-net/tests/integration/lazy_discovery.rs b/rs/moq-net/tests/integration/lazy_discovery.rs index 42075bce0fb..ff71398649a 100644 --- a/rs/moq-net/tests/integration/lazy_discovery.rs +++ b/rs/moq-net/tests/integration/lazy_discovery.rs @@ -363,10 +363,6 @@ async fn serving_the_peer_asks_nothing() { /// 15 answer with PUBLISH_NAMESPACE requests after the OK), so these read what arrived once /// everything in flight has. const IETF: &[&str] = &[ - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", "moq-transport-18", "moq-transport-19", "moq-transport-20", diff --git a/rs/moq-net/tests/integration/mesh_withdraw.rs b/rs/moq-net/tests/integration/mesh_withdraw.rs index c27e0cfb016..bdac9970d91 100644 --- a/rs/moq-net/tests/integration/mesh_withdraw.rs +++ b/rs/moq-net/tests/integration/mesh_withdraw.rs @@ -631,7 +631,6 @@ async fn hop_limit_bounds_receive_and_send() { "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-17", "moq-transport-18", "moq-transport-19", "moq-transport-20", @@ -703,7 +702,6 @@ async fn hop_limit_falls_back_to_a_forwardable_route() { for name in [ "moq-lite-06", "moq-lite-07-wip", - "moq-transport-17", "moq-transport-18", "moq-transport-19", "moq-transport-20", diff --git a/rs/moq-net/tests/integration/multi_cdn.rs b/rs/moq-net/tests/integration/multi_cdn.rs index 9c65bc72952..c5c66d052ae 100644 --- a/rs/moq-net/tests/integration/multi_cdn.rs +++ b/rs/moq-net/tests/integration/multi_cdn.rs @@ -355,59 +355,6 @@ async fn transport_secondary_is_refused() { /// The same over the first draft that can carry the epoch. #[moq_clock::test] async fn transport_17_secondary_is_refused() { - refused_across_protocols("moq-transport-17").await; + refused_across_protocols("moq-transport-18").await; } -/// A moq-transport `B` before draft-17 carries no epoch, so `S` cannot resume onto it: -/// losing `A` restarts the path and ends the subscription, and a re-request lands on `B`. -/// `A` returning is another restart, and a re-request lands back on `A`. -#[moq_clock::test] -async fn epochless_secondary_restarts() { - moq_clock::timeout(TEST_TIMEOUT, async { - let mut topology = Topology::new("moq-transport-16".parse().unwrap()).await; - let mut announced = topology.subscriber.consume().announced(); - let start = announced.next().await.expect("announced"); - assert!(via_a(&start), "{start:?}"); - - let mut rx = topology.subscribe().await; - let seen = topology.group_through_a(&mut rx).await; - assert_continuous(&seen, 1); - - topology.lose_a().await; - let restart = announced.next().await.expect("announced"); - assert!( - matches!(restart, announce::Event::Restart(_)) && !via_a(&restart), - "no epoch over B: {restart:?}" - ); - topology.group(); - settle().await; - match moq_clock::timeout(Duration::from_secs(10), rx.next()) - .await - .expect("reader hung") - { - None | Some((_, Err(_))) => {} - Some((group, Ok(_))) => panic!("resumed onto B at group {group}"), - } - - // A re-request lands on B, the only CDN left. It may start at the latest group, - // written while nobody was subscribed, before reaching the next one. - let mut rx = topology.subscribe().await; - topology.group(); - while next(&mut rx).await.0 != 2 {} - - topology.restore_a().await; - let restart = announced.next().await.expect("announced"); - assert!( - matches!(restart, announce::Event::Restart(_)) && via_a(&restart), - "back on A: {restart:?}" - ); - // A re-request lands on A: the next group arrives while B holds back its own. - let mut rx = topology.subscribe().await; - topology.s_to_b.server_transport.hold_unis(); - topology.group(); - while next(&mut rx).await.0 != 3 {} - topology.s_to_b.server_transport.release_unis(); - }) - .await - .expect("timed out"); -} diff --git a/rs/moq-net/tests/integration/partial_group.rs b/rs/moq-net/tests/integration/partial_group.rs index 609ee7e5462..ba9baaedf8c 100644 --- a/rs/moq-net/tests/integration/partial_group.rs +++ b/rs/moq-net/tests/integration/partial_group.rs @@ -20,10 +20,6 @@ const BUDGET: Duration = Duration::from_millis(500); const VERSIONS: &[&str] = &[ "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", "moq-transport-18", "moq-transport-19", "moq-transport-20", diff --git a/rs/moq-net/tests/integration/quiet_catalog.rs b/rs/moq-net/tests/integration/quiet_catalog.rs index dab2e9e4a32..ead5d7c4860 100644 --- a/rs/moq-net/tests/integration/quiet_catalog.rs +++ b/rs/moq-net/tests/integration/quiet_catalog.rs @@ -19,7 +19,7 @@ fn timed() -> moq_net::track::Info { const TIMEOUT: Duration = Duration::from_secs(5); -const VERSIONS: &[&str] = &["moq-lite-06", "moq-lite-07-wip", "moq-transport-16", "moq-transport-22"]; +const VERSIONS: &[&str] = &["moq-lite-06", "moq-lite-07-wip", "moq-transport-22"]; /// The versions that forward a resume's floor upstream; the IETF relay subscribes without one. const LITE: &[&str] = &["moq-lite-06", "moq-lite-07-wip"]; diff --git a/rs/moq-net/tests/integration/range_anchors.rs b/rs/moq-net/tests/integration/range_anchors.rs index d1d1c3e2c7d..f6494697596 100644 --- a/rs/moq-net/tests/integration/range_anchors.rs +++ b/rs/moq-net/tests/integration/range_anchors.rs @@ -24,9 +24,7 @@ const VERSIONS: &[&str] = &[ "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-16", - "moq-transport-17", + "moq-transport-18", "moq-transport-20", "moq-transport-22", ]; @@ -133,7 +131,7 @@ async fn each_wire_refuses_only_the_anchors_it_drops() { let timed = !matches!( version, Some( - "moq-lite-03" | "moq-lite-04" | "moq-lite-05" | "moq-lite-06" | "moq-transport-14" | "moq-transport-16" + "moq-lite-03" | "moq-lite-04" | "moq-lite-05" | "moq-lite-06" ) ); for (range, held) in anchored() { diff --git a/rs/moq-net/tests/integration/relay_upstream_close.rs b/rs/moq-net/tests/integration/relay_upstream_close.rs index ee3096ca8dd..c1a54a1f6fd 100644 --- a/rs/moq-net/tests/integration/relay_upstream_close.rs +++ b/rs/moq-net/tests/integration/relay_upstream_close.rs @@ -36,8 +36,7 @@ const VERSIONS: &[&str] = &[ "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-17", + "moq-transport-18", "moq-transport-22", ]; diff --git a/rs/moq-net/tests/integration/replacement.rs b/rs/moq-net/tests/integration/replacement.rs index 98c33b1bc87..068cbf5e425 100644 --- a/rs/moq-net/tests/integration/replacement.rs +++ b/rs/moq-net/tests/integration/replacement.rs @@ -174,7 +174,7 @@ async fn epoch_lite07() { /// moq-transport carries the epoch with the MoQ Epoch extension, from draft-17. #[moq_clock::test] async fn epoch_ietf() { - for version in ["moq-transport-17", "moq-transport-22"] { + for version in ["moq-transport-18", "moq-transport-22"] { moq_clock::timeout(TEST_TIMEOUT, replace(version, Some(epoch()))) .await .expect("timed out"); diff --git a/rs/moq-net/tests/integration/request_caps.rs b/rs/moq-net/tests/integration/request_caps.rs index ca70c7b092b..f48881be810 100644 --- a/rs/moq-net/tests/integration/request_caps.rs +++ b/rs/moq-net/tests/integration/request_caps.rs @@ -1,10 +1,9 @@ #![cfg(feature = "async")] -//! A server bounds what one client can make it hold: flow control bounds its requests, a -//! client past [`moq_net::session::Limits::announces`] loses the session with -//! TOO_MANY_REQUESTS, and on moq-transport drafts 14 to 16 the MAX_REQUEST_ID window the -//! limits size is granted back as requests close, so a client that keeps opening and closing -//! requests within them never stalls. +//! A server bounds what one client can make it hold: stream flow control bounds its +//! requests, and a client past [`moq_net::session::Limits::announces`] loses the session +//! with TOO_MANY_REQUESTS. Credit is granted back as requests close, so a client that keeps +//! opening and closing requests never stalls. use crate::support; @@ -15,14 +14,8 @@ use support::harness::{MockConnectOptions, MockPair, connect_mock}; const TIMEOUT: Duration = Duration::from_secs(10); -/// Drafts with MAX_REQUEST_ID, a draft without it, and the lite draft production runs. -const VERSIONS: [&str; 5] = [ - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", - "moq-lite-06", -]; +/// A moq-transport draft and the lite draft production runs. +const VERSIONS: [&str; 2] = ["moq-transport-18", "moq-lite-06"]; fn produce_origin(hop: u64) -> moq_net::origin::Producer { let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::new(Hop::new(hop).unwrap())); @@ -65,23 +58,20 @@ async fn setup(version: &str, limits: Limits, tracks: usize) -> Setup { } } -fn limits(announces: usize, requests: usize) -> Limits { +fn limits(announces: usize) -> Limits { let mut limits = Limits::default(); limits.announces = announces; - limits.requests = requests; limits } -/// Many more requests than the window admits at once, opened and closed in turn, all +/// Many more requests than the stream credit admits at once, opened and closed in turn, all /// succeed: each one that closes is granted back. #[moq_clock::test] async fn closed_requests_are_granted_back() { const ROUNDS: usize = 64; for version in VERSIONS { - // A window of 8 requests, far fewer than the rounds below. Doubling the limit leaves - // room for an UNSUBSCRIBE still in flight when the next SUBSCRIBE lands. - let setup = setup(version, limits(0, 4), ROUNDS).await; + let setup = setup(version, limits(0), ROUNDS).await; let broadcast = moq_clock::timeout(TIMEOUT, setup.client.consume().routed_broadcast("bcast")) .await .expect("announce timeout") @@ -107,11 +97,11 @@ async fn closed_requests_are_granted_back() { } /// With no subscription cap, a client asking for more subscriptions than the server admits -/// still holds no more than the request window (drafts 14 to 16) or the stream credit -/// (moq-lite, draft 17 and later) at once. The rest wait rather than close the session. +/// still holds no more than the stream credit at once. The rest wait rather than close the +/// session. #[moq_clock::test] async fn flow_control_bounds_the_subscriptions_a_peer_holds() { - // A window of 8 requests, and 8 bidi streams toward the server. + // 8 bidi streams toward the server. const BOUND: usize = 8; const TRACKS: usize = 3 * BOUND; @@ -129,7 +119,7 @@ async fn flow_control_bounds_the_subscriptions_a_peer_holds() { options.transport.max_concurrent_bi_streams = Some(BOUND); options.server_publish = Some(server.consume()); options.client_subscribe = Some(client.clone()); - options.server_limits = Some(limits(0, BOUND / 2)); + options.server_limits = Some(limits(0)); let pair = connect_mock(options).await; let remote = moq_clock::timeout(TIMEOUT, client.consume().routed_broadcast("bcast")) @@ -177,7 +167,7 @@ async fn an_announce_past_the_cap_closes_the_session() { let mut options = MockConnectOptions::new(version); options.client_publish = Some(client.consume()); options.server_subscribe = Some(server.clone()); - options.server_limits = Some(limits(1, 1)); + options.server_limits = Some(limits(1)); let pair = connect_mock(options).await; let err = moq_clock::timeout(TIMEOUT, pair.client.closed()) diff --git a/rs/moq-net/tests/integration/restart.rs b/rs/moq-net/tests/integration/restart.rs index cec0496f1a9..4efbcde258d 100644 --- a/rs/moq-net/tests/integration/restart.rs +++ b/rs/moq-net/tests/integration/restart.rs @@ -520,10 +520,6 @@ retired_fetch_tests! { retired_fetch_lite05: "moq-lite-05", retired_fetch_lite06: "moq-lite-06", retired_fetch_lite07: "moq-lite-07-wip", - retired_fetch_ietf14: "moq-transport-14", - retired_fetch_ietf15: "moq-transport-15", - retired_fetch_ietf16: "moq-transport-16", - retired_fetch_ietf17: "moq-transport-17", retired_fetch_ietf18: "moq-transport-18", retired_fetch_ietf19: "moq-transport-19", retired_fetch_ietf20: "moq-transport-20", @@ -544,7 +540,6 @@ async fn withdrawal_refuses_new_fetches_but_keeps_open_reads() { "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", "moq-transport-22", ] { let version: Version = version.parse().unwrap(); @@ -573,7 +568,6 @@ async fn a_more_specific_source_cannot_fill_a_held_covering_track() { "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", "moq-transport-22", ] { let version: Version = name.parse().unwrap(); @@ -714,7 +708,6 @@ claim_restart_tests! { claim_restart_lite05: "moq-lite-05", claim_restart_lite06: "moq-lite-06", claim_restart_lite07: "moq-lite-07-wip", - claim_restart_ietf14: "moq-transport-14", - claim_restart_ietf16: "moq-transport-16", + claim_restart_ietf18: "moq-transport-18", claim_restart_ietf22: "moq-transport-22", } diff --git a/rs/moq-net/tests/integration/route_change.rs b/rs/moq-net/tests/integration/route_change.rs index 82787cf5888..7ea04e379a8 100644 --- a/rs/moq-net/tests/integration/route_change.rs +++ b/rs/moq-net/tests/integration/route_change.rs @@ -468,7 +468,7 @@ macro_rules! route_change_tests { route_change_tests! { lite_07: "moq-lite-07-wip", - ietf_17: "moq-transport-17", + ietf_18: "moq-transport-18", ietf_19: "moq-transport-19", ietf_22: "moq-transport-22", } @@ -512,8 +512,6 @@ pinned_tests! { lite_06: "moq-lite-06", lite_05: "moq-lite-05", lite_04: "moq-lite-04", - ietf_14: "moq-transport-14", - ietf_16: "moq-transport-16", } /// `P` feeds `A` slowly and `B` promptly, and `R` reads through `A` until that route dies @@ -614,7 +612,7 @@ async fn lite_resumes_after_a_lagging_route_dies() { #[moq_clock::test] async fn ietf_17_resumes_after_a_lagging_route_dies() { - lagging_route_dies("moq-transport-17".parse().unwrap()).await; + lagging_route_dies("moq-transport-18".parse().unwrap()).await; } #[moq_clock::test] diff --git a/rs/moq-net/tests/integration/session_close.rs b/rs/moq-net/tests/integration/session_close.rs index 6b6cbe01cee..81c8992d07a 100644 --- a/rs/moq-net/tests/integration/session_close.rs +++ b/rs/moq-net/tests/integration/session_close.rs @@ -229,7 +229,7 @@ async fn abort_cuts_a_drain_short_for(version: &str) { #[moq_clock::test] async fn close_withdraws_announcements_before_closing_the_transport() { - for version in ["moq-lite-01", "moq-lite-07-wip", "moq-transport-17", "moq-transport-22"] { + for version in ["moq-lite-01", "moq-lite-07-wip", "moq-transport-18", "moq-transport-22"] { let publisher = produce_origin(1); let broadcast = publisher.create_broadcast("bcast").unwrap(); broadcast.announce(Default::default()).unwrap(); @@ -250,7 +250,7 @@ async fn close_withdraws_announcements_before_closing_the_transport() { #[moq_clock::test] async fn close_times_out_waiting_for_announcement_acknowledgements() { - for version in ["moq-lite-01", "moq-lite-07-wip", "moq-transport-17", "moq-transport-22"] { + for version in ["moq-lite-01", "moq-lite-07-wip", "moq-transport-18", "moq-transport-22"] { let publisher = produce_origin(1); let broadcast = publisher.create_broadcast("bcast").unwrap(); broadcast.announce(Default::default()).unwrap(); @@ -332,7 +332,7 @@ async fn earlier_lite_peer_close_reads_buffered_tail() { #[moq_clock::test] async fn ietf_peer_close_reads_buffered_tail() { - for version in ["moq-transport-14", "moq-transport-17", "moq-transport-22"] { + for version in [ "moq-transport-18", "moq-transport-22"] { peer_close_reads_buffered_tail(version).await; } } @@ -362,8 +362,7 @@ async fn peer_close_before_receiver_poll_keeps_buffered_tail() { "moq-lite-02", "moq-lite-05", "moq-lite-06", - "moq-transport-14", - "moq-transport-17", + "moq-transport-18", ] { let gate = kio::Producer::new(true); let Setup { @@ -385,7 +384,7 @@ async fn peer_close_before_receiver_poll_keeps_buffered_tail() { /// A locally unsupported feature uses only the negotiated session registry. #[moq_clock::test] async fn unsupported_abort_uses_the_negotiated_session_registry() { - for protocol in ["moq-lite-06", "moq-lite-07-wip", "moq-transport-17", "moq-transport-22"] { + for protocol in ["moq-lite-06", "moq-lite-07-wip", "moq-transport-18", "moq-transport-22"] { let version: Version = protocol.parse().unwrap(); let pair = connect_mock(MockConnectOptions::new(version)).await; pair.client.abort(Error::Unsupported); @@ -398,7 +397,7 @@ async fn unsupported_abort_uses_the_negotiated_session_registry() { /// A Lite-only close code arriving on IETF stays unknown instead of gaining Lite semantics. #[moq_clock::test] async fn unsupported_close_is_decoded_by_the_negotiated_session_registry() { - for protocol in ["moq-lite-06", "moq-lite-07-wip", "moq-transport-17", "moq-transport-22"] { + for protocol in ["moq-lite-06", "moq-lite-07-wip", "moq-transport-18", "moq-transport-22"] { let version: Version = protocol.parse().unwrap(); let mut pair = connect_mock(MockConnectOptions::new(version)).await; moq_net::transport::poll::Session::close(&mut pair.server_transport, 0x30, "unsupported"); @@ -416,7 +415,7 @@ async fn unsupported_close_is_decoded_by_the_negotiated_session_registry() { /// wire, while a code outside it passes through. #[moq_clock::test] async fn an_unknown_lite_range_abort_is_unspecified_on_every_wire() { - for protocol in ["moq-lite-06", "moq-lite-07-wip", "moq-transport-17", "moq-transport-22"] { + for protocol in ["moq-lite-06", "moq-lite-07-wip", "moq-transport-18", "moq-transport-22"] { for (unknown, expected) in [(0x31, 0x1), (0x20, 0x20)] { let version: Version = protocol.parse().unwrap(); let pair = connect_mock(MockConnectOptions::new(version)).await; @@ -452,7 +451,7 @@ async fn ended(version: &str, close: impl FnOnce(&MockPair)) -> (Error, moq_net: #[moq_clock::test] async fn a_session_records_how_it_ended() { use moq_net::stats::Failures; - for version in ["moq-lite-06", "moq-lite-07-wip", "moq-transport-16", "moq-transport-22"] { + for version in ["moq-lite-06", "moq-lite-07-wip", "moq-transport-22"] { let (err, failed) = ended(version, |pair| pair.client.abort(Error::Cancel)).await; assert!(matches!(err, Error::Cancel), "{version}: {err:?}"); assert_eq!(failed, Failures::default(), "{version}"); diff --git a/rs/moq-net/tests/integration/subscribe_priority.rs b/rs/moq-net/tests/integration/subscribe_priority.rs index 1ec161c36b1..9be3289cab3 100644 --- a/rs/moq-net/tests/integration/subscribe_priority.rs +++ b/rs/moq-net/tests/integration/subscribe_priority.rs @@ -16,10 +16,6 @@ const TIMEOUT: Duration = Duration::from_secs(10); const VERSIONS: &[&str] = &[ "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", "moq-transport-18", "moq-transport-19", "moq-transport-20", diff --git a/rs/moq-net/tests/integration/subscribe_ranges.rs b/rs/moq-net/tests/integration/subscribe_ranges.rs index 7375b7f8eb7..4372bcc5ef1 100644 --- a/rs/moq-net/tests/integration/subscribe_ranges.rs +++ b/rs/moq-net/tests/integration/subscribe_ranges.rs @@ -18,7 +18,7 @@ const LITE: &[&str] = &["moq-lite-05", "moq-lite-06", "moq-lite-07-wip"]; /// Draft-20 and later are left out: the first subscriber's absolute start goes upstream /// in the SUBSCRIBE filter, and the subscriber never widens it when a live one joins /// (`quest/m1/subscribe-ranges/ietf.md`), so group 3 never arrives. -const TRANSPORT: &[&str] = &["moq-transport-14", "moq-transport-17"]; +const TRANSPORT: &[&str] = &[ "moq-transport-18"]; fn produce_origin(hop: u64) -> moq_net::origin::Producer { let (producer, driver) = diff --git a/rs/moq-net/tests/integration/subscribe_tracks.rs b/rs/moq-net/tests/integration/subscribe_tracks.rs index abda5ccd5b4..c22fe76cf85 100644 --- a/rs/moq-net/tests/integration/subscribe_tracks.rs +++ b/rs/moq-net/tests/integration/subscribe_tracks.rs @@ -173,7 +173,7 @@ async fn subscribe_tracks_needs_forward_0_on_draft_18() { /// Before draft-18, 0x51 is not a message at all, so it is still a protocol violation. #[moq_clock::test] async fn subscribe_tracks_before_draft_18_closes_the_session() { - let setup = setup("moq-transport-17".parse().unwrap()).await; + let setup = setup("moq-transport-18".parse().unwrap()).await; let (mut send, _recv) = setup .pair .server_transport diff --git a/rs/moq-net/tests/integration/subscription_end_integrity.rs b/rs/moq-net/tests/integration/subscription_end_integrity.rs index 3814ece5db2..7cf37edc556 100644 --- a/rs/moq-net/tests/integration/subscription_end_integrity.rs +++ b/rs/moq-net/tests/integration/subscription_end_integrity.rs @@ -194,8 +194,7 @@ const DEATH_VERSIONS: &[&str] = &[ "moq-lite-03", "moq-lite-05", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-17", + "moq-transport-18", "moq-transport-22", ]; diff --git a/rs/moq-net/tests/integration/synced.rs b/rs/moq-net/tests/integration/synced.rs index cbb741d5c8d..c15b506e5af 100644 --- a/rs/moq-net/tests/integration/synced.rs +++ b/rs/moq-net/tests/integration/synced.rs @@ -72,8 +72,6 @@ async fn an_exact_boundary_catches_up_with_the_initial_set() { "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-16", - "moq-transport-17", "moq-transport-18", "moq-transport-19", "moq-transport-20", @@ -87,7 +85,7 @@ async fn an_exact_boundary_catches_up_with_the_initial_set() { /// Versions without a boundary still sync, so nothing waiting on it hangs. #[moq_clock::test] async fn a_best_effort_boundary_still_catches_up() { - for version in ["moq-lite-03", "moq-lite-04", "moq-transport-14", "moq-transport-15"] { + for version in ["moq-lite-03", "moq-lite-04"] { synced(version).await; } } diff --git a/rs/moq-net/tests/integration/track_tail.rs b/rs/moq-net/tests/integration/track_tail.rs index 10fbf478745..ac30c90045e 100644 --- a/rs/moq-net/tests/integration/track_tail.rs +++ b/rs/moq-net/tests/integration/track_tail.rs @@ -35,8 +35,7 @@ const VERSIONS: &[&str] = &[ "moq-lite-03", "moq-lite-05", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-17", + "moq-transport-18", "moq-transport-20", "moq-transport-22", ]; @@ -261,8 +260,7 @@ async fn lite07_empty_track_ends_without_the_grace() { /// IETF drafts over the control stream adapter (14), on their own streams (17), and with /// subscription fills (20+). const IETF: &[&str] = &[ - "moq-transport-14", - "moq-transport-17", + "moq-transport-18", "moq-transport-20", "moq-transport-22", ]; diff --git a/rs/moq-net/tests/integration/tracks_ietf.rs b/rs/moq-net/tests/integration/tracks_ietf.rs index f1fe79db18b..618130a195c 100644 --- a/rs/moq-net/tests/integration/tracks_ietf.rs +++ b/rs/moq-net/tests/integration/tracks_ietf.rs @@ -193,7 +193,7 @@ async fn before_draft_18_the_listing_is_refused() { let a = broadcast.create_track("a", None).unwrap(); broadcast.announce(Default::default()).unwrap(); - let (subscriber, _pair) = connect(&publisher, "moq-transport-17").await; + let (subscriber, _pair) = connect(&publisher, "moq-transport-18").await; let remote = resolve(&subscriber, "bcast").await; let mut tracks = remote.tracks().unwrap(); assert_eq!( diff --git a/rs/moq-net/tests/integration/untimed.rs b/rs/moq-net/tests/integration/untimed.rs index f80e65755cd..ea408df516d 100644 --- a/rs/moq-net/tests/integration/untimed.rs +++ b/rs/moq-net/tests/integration/untimed.rs @@ -35,8 +35,6 @@ fn expected(version: &str) -> Arrives { match version { // Lite-05 and lite-06 stamp untimed tracks with send times, so their receivers drop them. "moq-lite-03" | "moq-lite-04" | "moq-lite-05" | "moq-lite-06" => Arrives::Untimed, - // Drafts 14 to 16 can't carry TIMESCALE. - "moq-transport-14" | "moq-transport-16" => Arrives::Untimed, _ => Arrives::Faithful, } } @@ -47,9 +45,7 @@ const VERSIONS: &[&str] = &[ "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-16", - "moq-transport-17", + "moq-transport-18", "moq-transport-20", ]; @@ -148,7 +144,7 @@ async fn frames_arrive_as_published() { #[moq_clock::test] async fn a_relay_forwards_an_untimed_track_untimed() { for hops in [ - &["moq-transport-17", "moq-transport-20"][..], + &["moq-transport-18", "moq-transport-20"][..], &["moq-lite-07-wip", "moq-lite-07-wip"][..], ] { let (frame, info) = frame_through(hops, untimed()).await; @@ -157,8 +153,8 @@ async fn a_relay_forwards_an_untimed_track_untimed() { } for hops in [ - &["moq-lite-03", "moq-transport-17"][..], - &["moq-transport-16", "moq-transport-20"][..], + &["moq-lite-03", "moq-transport-18"][..], + &[ "moq-transport-20"][..], ] { let (frame, info) = frame_through(hops, timed()).await; assert_eq!(frame, None, "{hops:?}: the first hop has no timestamps to forward"); @@ -210,7 +206,7 @@ async fn an_explicit_start_holds_on_an_untimed_track() { /// time that the receiver drops. #[moq_clock::test] async fn datagrams_arrive_as_published() { - for version in ["moq-lite-05", "moq-lite-07-wip", "moq-transport-16", "moq-transport-20"] { + for version in ["moq-lite-05", "moq-lite-07-wip", "moq-transport-20"] { let chain = Chain::new(&[version]).await; let broadcast = chain.nodes[0].create_broadcast("room").unwrap(); let mut producer = broadcast.create_track("data", untimed()).unwrap(); From 386d3663b4791a5c84ede3a19ef4ba0db2fbb364 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 03:22:43 -0700 Subject: [PATCH 04/25] wip: drop drafts 14-17 from the relay, tooling and CI lanes Co-Authored-By: Claude Opus 5.5 --- .github/workflows/interop.yml | 2 +- rs/moq-cli/src/publish.rs | 2 +- rs/moq-cli/tests/integration/epochs.rs | 6 +- rs/moq-e2ee/tests/support/harness.rs | 2 +- rs/moq-ffi/src/test.rs | 24 +- rs/moq-gst/src/connection.rs | 8 +- rs/moq-net/src/client.rs | 8 +- rs/moq-net/src/coding/writer.rs | 4 - rs/moq-net/src/fuzz.rs | 25 + rs/moq-net/src/ietf/cluster.rs | 1 - rs/moq-net/src/ietf/datagram.rs | 13 +- rs/moq-net/src/ietf/error.rs | 10 +- rs/moq-net/src/ietf/filter.rs | 9 +- rs/moq-net/src/ietf/group.rs | 6 +- rs/moq-net/src/ietf/namespace.rs | 4 +- rs/moq-net/src/ietf/object.rs | 5 +- rs/moq-net/src/ietf/parameters.rs | 44 +- rs/moq-net/src/ietf/properties.rs | 1 - rs/moq-net/src/ietf/publish.rs | 32 +- rs/moq-net/src/ietf/publisher.rs | 297 ++------- rs/moq-net/src/ietf/publisher/tracks.rs | 23 +- rs/moq-net/src/ietf/request_stream.rs | 5 +- rs/moq-net/src/ietf/request_update.rs | 9 +- rs/moq-net/src/ietf/session.rs | 15 +- rs/moq-net/src/ietf/subscribe.rs | 258 ++++---- rs/moq-net/src/ietf/subscribe_namespace.rs | 2 - rs/moq-net/src/ietf/subscribe_tracks.rs | 11 - rs/moq-net/src/ietf/subscriber.rs | 589 +++++------------- rs/moq-net/src/ietf/token.rs | 4 +- rs/moq-net/src/lite/ietf.rs | 36 +- rs/moq-net/src/server.rs | 27 +- rs/moq-net/src/setup.rs | 33 +- rs/moq-net/src/test_interop.rs | 18 +- rs/moq-net/tests/integration/compress.rs | 31 +- rs/moq-net/tests/integration/control_stats.rs | 7 +- rs/moq-net/tests/integration/datagram.rs | 11 +- rs/moq-net/tests/integration/empty_group.rs | 6 +- rs/moq-net/tests/integration/fetch_only.rs | 6 +- rs/moq-net/tests/integration/goaway.rs | 7 +- rs/moq-net/tests/integration/idle_session.rs | 2 +- .../tests/integration/ietf_conformance.rs | 4 +- .../tests/integration/late_lower_group.rs | 2 +- rs/moq-net/tests/integration/multi_cdn.rs | 1 - rs/moq-net/tests/integration/range_anchors.rs | 4 +- rs/moq-net/tests/integration/restart.rs | 14 +- rs/moq-net/tests/integration/session_close.rs | 2 +- .../tests/integration/subscribe_ranges.rs | 2 +- .../tests/integration/subscribe_tracks.rs | 18 - .../tests/integration/subscriber_max_age.rs | 2 +- rs/moq-net/tests/integration/track_tail.rs | 6 +- rs/moq-net/tests/integration/tracks_ietf.rs | 31 - rs/moq-net/tests/integration/untimed.rs | 17 +- rs/moq-quic/src/connection/mux/params.rs | 2 +- rs/moq-relay/src/cluster.rs | 12 +- rs/moq-relay/src/config.rs | 36 +- rs/moq-relay/src/limits.rs | 41 +- rs/moq-relay/src/settings.rs | 4 - rs/moq-relay/src/websocket.rs | 12 +- .../tests/integration/auth_lifetime.rs | 24 +- .../tests/integration/cluster_unknown.rs | 10 +- rs/moq-relay/tests/integration/drain.rs | 2 +- .../tests/integration/hidden_cluster.rs | 2 +- .../tests/integration/runtime_uring.rs | 29 +- .../tests/integration/shutdown_signal.rs | 8 +- rs/moq-relay/tests/integration/smoke.rs | 18 +- rs/moq-tokio/src/connect.rs | 8 - rs/moq-tokio/src/deprecated.rs | 8 +- rs/moq-tokio/src/listen.rs | 8 - rs/moq-tokio/src/quic/backend.rs | 28 +- rs/moq-tokio/src/server.rs | 12 +- rs/moq-tokio/tests/integration/broadcast.rs | 178 +----- rs/moq-tokio/tests/integration/max_age.rs | 4 +- .../tests/integration/receive_credit.rs | 6 +- rs/moq-wasm/src/transport.rs | 6 +- rs/qmux/src/alpn.rs | 8 +- test/ietf-conformance/README.md | 7 +- test/ietf-conformance/run.sh | 4 +- test/interop/interop.sh | 29 +- test/interop/interop.toml | 4 - test/smoke/README.md | 2 +- test/smoke/clients/cloudflare/Cargo.toml | 4 +- test/smoke/cloudflare.sh | 19 +- test/smoke/freshness.sh | 25 +- test/smoke/moxygen.sh | 8 +- 84 files changed, 650 insertions(+), 1624 deletions(-) diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml index a036636c49b..eb0cc1eb5d5 100644 --- a/.github/workflows/interop.yml +++ b/.github/workflows/interop.yml @@ -115,7 +115,7 @@ jobs: nix develop --command just test interop "${args[@]}" shell: bash -leo pipefail {0} env: - INTEROP_ARGS: ${{ github.event_name == 'pull_request' && '--publishers rust --subscribers rust,js-native-node,js-native-bun --versions moq-transport-14,moq-transport-22' || '--all --versions ietf' }} + INTEROP_ARGS: ${{ github.event_name == 'pull_request' && '--publishers rust --subscribers rust,js-native-node,js-native-bun --versions moq-transport-18,moq-transport-22' || '--all --versions ietf' }} MOQ_TEST_RUNS: ${{ runner.temp }}/moq-test-runs MOQ_TEST_RESULTS: ${{ runner.temp }}/moq-test-runs/results.jsonl diff --git a/rs/moq-cli/src/publish.rs b/rs/moq-cli/src/publish.rs index 7068dc971ba..38f59ddadee 100644 --- a/rs/moq-cli/src/publish.rs +++ b/rs/moq-cli/src/publish.rs @@ -1257,7 +1257,7 @@ pub(crate) mod tests { let input = [&lead[..], &second].concat(); let _ = moq_tokio::crypto::install_default(); - for version in ["moq-lite-06", "moq-lite-07-wip", "moq-transport-14"] { + for version in ["moq-lite-06", "moq-lite-07-wip", "moq-transport-18"] { let fixture = moq_relay::test_relay().await.unwrap(); let ready = fixture.relay.ready(); tokio::spawn(fixture.relay.run()); diff --git a/rs/moq-cli/tests/integration/epochs.rs b/rs/moq-cli/tests/integration/epochs.rs index e55907363ba..d0d7626abfe 100644 --- a/rs/moq-cli/tests/integration/epochs.rs +++ b/rs/moq-cli/tests/integration/epochs.rs @@ -201,10 +201,7 @@ async fn replacement(version: &str) { (changed(&mut catalog_follow).await, changed(&mut media_follow).await) }) .await; - assert_eq!( - catalog_restart, - version != "moq-lite-06" && version != "moq-transport-14" - ); + assert_eq!(catalog_restart, version != "moq-lite-06"); assert_eq!(media_restart, catalog_restart); println!( "{version}: relay replacement observed in {:?} of {:?} (publisher already connected)", @@ -307,6 +304,5 @@ macro_rules! versions { versions! { lite07: "moq-lite-07-wip", lite06: "moq-lite-06", - ietf14: "moq-transport-14", ietf22: "moq-transport-22", } diff --git a/rs/moq-e2ee/tests/support/harness.rs b/rs/moq-e2ee/tests/support/harness.rs index eb221f97f76..1b22c466208 100644 --- a/rs/moq-e2ee/tests/support/harness.rs +++ b/rs/moq-e2ee/tests/support/harness.rs @@ -82,7 +82,7 @@ pub async fn connect_mock(opts: MockConnectOptions) -> MockPair { } // Run both handshakes concurrently and spawn each side's driver - // the moment its handshake resolves: on draft-17+ the server's accept blocks + // the moment its handshake resolves: on moq-transport the server's accept blocks // on the client's SETUP, which only reaches the wire once the client's // machine is polled (and vice versa for the server's own SETUP). let client_fut = async { diff --git a/rs/moq-ffi/src/test.rs b/rs/moq-ffi/src/test.rs index c0bede188d1..8ca18f24498 100644 --- a/rs/moq-ffi/src/test.rs +++ b/rs/moq-ffi/src/test.rs @@ -7624,7 +7624,7 @@ fn grant(publish: &[&str], subscribe: &[&str]) -> crate::auth::MoqGrant { /// unsupported. #[tokio::test] async fn auth_reads_the_peer_grant() { - for version in ["moq-lite-07-wip", "moq-transport-17"] { + for version in ["moq-lite-07-wip", "moq-transport-18"] { auth_reads_the_peer_grant_on(version).await; } } @@ -7655,28 +7655,6 @@ async fn auth_reads_the_peer_grant_on(version: &str) { server.cancel(); } -/// moq-transport without the Auth extension carries a token only on a request that names it, -/// which no binding can, so both sides refuse one rather than report a token nothing sends. -#[tokio::test] -async fn auth_refuses_a_token_nothing_carries() { - let (server, client, accepted) = tokio::time::timeout(TIMEOUT, session_pair("moq-transport-14")) - .await - .expect("pair"); - for session in [&client, &accepted] { - let err = tokio::time::timeout(TIMEOUT, session.auth().add("extra".into())) - .await - .expect("add timed out") - .err() - .expect("a token nothing carries is refused"); - assert!(matches!(err, MoqError::Unsupported), "refusal: {err:?}"); - assert_eq!(session.auth().grant(), None, "no AUTH, no grant"); - } - - client.cancel(0); - accepted.cancel(0); - server.cancel(); -} - /// A token added through a client session joins the union with its own grant, leaves it on /// cancel, and a refusal or revocation carries the peer's code. #[tokio::test] diff --git a/rs/moq-gst/src/connection.rs b/rs/moq-gst/src/connection.rs index 270eb21d079..e2b824c3b3c 100644 --- a/rs/moq-gst/src/connection.rs +++ b/rs/moq-gst/src/connection.rs @@ -128,10 +128,10 @@ mod tests { element.set_property(name, value); assert_eq!(element.property::(name), value); } - element.set_property("connect-version", " moq-transport-14 , moq-lite-06 "); + element.set_property("connect-version", " moq-transport-18 , moq-lite-06 "); assert_eq!( element.property::("connect-version"), - " moq-transport-14 , moq-lite-06 " + " moq-transport-18 , moq-lite-06 " ); } } @@ -140,7 +140,7 @@ mod tests { fn properties_reach_versions_tls_and_quic_config() { let mut settings = Settings::default(); for (name, value) in [ - ("connect-version", " moq-transport-14 , moq-lite-06 ".to_value()), + ("connect-version", " moq-transport-18 , moq-lite-06 ".to_value()), ("tls-disable-verify", true.to_value()), ("quic-idle-timeout", 15_000u64.to_value()), ("quic-keep-alive", 0u64.to_value()), @@ -151,7 +151,7 @@ mod tests { let (connect, quic) = settings.resolve().unwrap(); assert_eq!( connect.version, - ["moq-transport-14".parse().unwrap(), "moq-lite-06".parse().unwrap()] + ["moq-transport-18".parse().unwrap(), "moq-lite-06".parse().unwrap()] ); assert_eq!(connect.tls.insecure, Some(true)); assert_eq!(connect.backoff.timeout, Some(Duration::ZERO.into())); diff --git a/rs/moq-net/src/client.rs b/rs/moq-net/src/client.rs index ceddb21dd0c..1e875ad47ce 100644 --- a/rs/moq-net/src/client.rs +++ b/rs/moq-net/src/client.rs @@ -3,7 +3,8 @@ use crate::origin; use crate::time::{Deadlines, Instant}; use crate::transport::poll::Session as _; use crate::{ - ALPN_18, ALPN_19, ALPN_20, ALPN_21, ALPN_22, ALPN_LITE, ALPN_LITE_03, ALPN_LITE_04, ALPN_LITE_05, ALPN_LITE_06, ALPN_LITE_07_WIP, Consume, Error, NEGOTIATED, Session, Version, Versions, + ALPN_18, ALPN_19, ALPN_20, ALPN_21, ALPN_22, ALPN_LITE, ALPN_LITE_03, ALPN_LITE_04, ALPN_LITE_05, ALPN_LITE_06, + ALPN_LITE_07_WIP, Consume, Error, NEGOTIATED, Session, Version, Versions, coding::{self, Encode, Stream}, ietf, lite, setup, stats, }; @@ -740,7 +741,10 @@ mod tests { parameters: Bytes::new(), }; server - .encode(&mut crate::coding::Encoder::new(&mut encoded, negotiated.into()), negotiated) + .encode( + &mut crate::coding::Encoder::new(&mut encoded, negotiated.into()), + negotiated, + ) .unwrap(); // Add a setup-stream SessionInfo frame using the negotiated Lite version. diff --git a/rs/moq-net/src/coding/writer.rs b/rs/moq-net/src/coding/writer.rs index c372dc222f1..bfb5c8374a7 100644 --- a/rs/moq-net/src/coding/writer.rs +++ b/rs/moq-net/src/coding/writer.rs @@ -327,10 +327,6 @@ mod tests { #[moq_clock::test] async fn raw_payload_stop_uses_the_negotiated_registry() { for (version, expected) in [ - ( - crate::Version::Ietf(crate::ietf::Version::Draft18), - Error::Stream(crate::StreamError::Unknown(0x4)), - ), ( crate::Version::Ietf(crate::ietf::Version::Draft20), Error::Stream(crate::StreamError::GoingAway), diff --git a/rs/moq-net/src/fuzz.rs b/rs/moq-net/src/fuzz.rs index 3abbfaeca16..6cd8ebcffcc 100644 --- a/rs/moq-net/src/fuzz.rs +++ b/rs/moq-net/src/fuzz.rs @@ -1069,6 +1069,31 @@ pub fn seeds() -> Vec { } } + // The `moql` SETUP pair, which only lite-01/02 negotiate, also has its own type prefix. + for (index, version) in LITE_VERSIONS.iter().enumerate() { + let outer = crate::Version::Lite(*version); + let client = crate::setup::Client { + versions: coding::Versions::from([outer.into()]), + parameters: Default::default(), + }; + let server = crate::setup::Server { + version: outer.into(), + parameters: Default::default(), + }; + let (Ok(client), Ok(server)) = (client.encode_bytes(outer), server.encode_bytes(outer)) else { + continue; + }; + for (kind, message) in [(22, client), (23, server)] { + let mut data = vec![index as u8, kind]; + data.extend(message); + seeds.push(Seed { + target: "lite", + kind, + data, + }); + } + } + // FETCH is the one arm the sweep above cannot reach: its body ends in a parameter // count, and a uniform fill never lands a zero there. Build it from the encoder // instead, which also means a field change breaks the build rather than the seed. diff --git a/rs/moq-net/src/ietf/cluster.rs b/rs/moq-net/src/ietf/cluster.rs index 2bb89d1f388..6b77115433d 100644 --- a/rs/moq-net/src/ietf/cluster.rs +++ b/rs/moq-net/src/ietf/cluster.rs @@ -532,5 +532,4 @@ mod tests { let peer = peer_from_setup(&decoded).unwrap(); assert!(!peer.negotiated()); } - } diff --git a/rs/moq-net/src/ietf/datagram.rs b/rs/moq-net/src/ietf/datagram.rs index 431ff8030ea..a070288ae78 100644 --- a/rs/moq-net/src/ietf/datagram.rs +++ b/rs/moq-net/src/ietf/datagram.rs @@ -278,17 +278,8 @@ mod tests { body: DatagramBody::Status(3), }; for version in ALL { - let legacy = false; - match datagram.encode_bytes(version) { - Ok(buf) => { - assert!(legacy, "{version}"); - assert_eq!(decode(&buf, version).unwrap(), datagram); - } - Err(err) => { - assert!(!legacy, "{version}"); - assert!(matches!(err, EncodeError::InvalidState)); - } - } + let err = datagram.encode_bytes(version).unwrap_err(); + assert!(matches!(err, EncodeError::InvalidState), "{version}"); } } diff --git a/rs/moq-net/src/ietf/error.rs b/rs/moq-net/src/ietf/error.rs index ee711790324..f9a77d02c42 100644 --- a/rs/moq-net/src/ietf/error.rs +++ b/rs/moq-net/src/ietf/error.rs @@ -179,9 +179,7 @@ pub(crate) mod request { return None; } match version { - Version::Draft18 | Version::Draft19 => { - Some(INVALID_JOINING_REQUEST_ID) - } + Version::Draft18 | Version::Draft19 => Some(INVALID_JOINING_REQUEST_ID), _ => None, } } @@ -434,7 +432,6 @@ pub(crate) mod request { ] } - // Sent but never read back as itself: a peer's MALFORMED_AUTH_TOKEN stays remote. let malformed = Error::Decode(DecodeError::MalformedAuthToken); for version in ALL { @@ -641,7 +638,10 @@ mod tests { const REGISTERED: &[u32] = &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x9, 0x12]; for err in EVERY_ERROR { let code = to_stream_code(&err); - assert!(REGISTERED.contains(&code), "{err:?} sends {code:#x}, which is not registered"); + assert!( + REGISTERED.contains(&code), + "{err:?} sends {code:#x}, which is not registered" + ); } } diff --git a/rs/moq-net/src/ietf/filter.rs b/rs/moq-net/src/ietf/filter.rs index 9e67b79356f..4a0eb763360 100644 --- a/rs/moq-net/src/ietf/filter.rs +++ b/rs/moq-net/src/ietf/filter.rs @@ -75,11 +75,7 @@ impl Filter { /// Draft-22 names the meaning with a Location Filter Type instead, but keeps those /// parameters, so this still gates them. pub(crate) fn is_draft20(version: Version) -> bool { - !matches!( - version, - Version::Draft18 - | Version::Draft19 - ) + !matches!(version, Version::Draft18 | Version::Draft19) } /// The absolute end group, given the start, or an error when the range runs backwards. @@ -294,8 +290,7 @@ impl Param for Filter { let mut buf = Vec::new(); let mut inner = Encoder::new(&mut buf, sv.into()); match version { - Version::Draft18 - | Version::Draft19 => self.encode_tag(&mut inner, sv)?, + Version::Draft18 | Version::Draft19 => self.encode_tag(&mut inner, sv)?, Version::Draft20 | Version::Draft21 => self.encode_fields(&mut inner)?, // Draft-22 drops the Length, since the Location Filter Type says which fields follow. _ => return self.encode_typed(w), diff --git a/rs/moq-net/src/ietf/group.rs b/rs/moq-net/src/ietf/group.rs index 223c200c323..c8ecc0f43c6 100644 --- a/rs/moq-net/src/ietf/group.rs +++ b/rs/moq-net/src/ietf/group.rs @@ -44,11 +44,7 @@ impl Decode for ObjectExtensionsLength { /// timescale on every object would cost bytes per frame to restate something fixed for /// the track's lifetime. `timescale` is what the track advertised, and the timestamp is /// converted into it so the value on the wire matches the declared units. -pub fn encode_object_time( - w: &mut Encoder<'_>, - timestamp: Timestamp, - timescale: Timescale, -) -> Result<(), EncodeError> { +pub fn encode_object_time(w: &mut Encoder<'_>, timestamp: Timestamp, timescale: Timescale) -> Result<(), EncodeError> { let timestamp = timestamp.convert(timescale).map_err(|_| EncodeError::BoundsExceeded)?; // The first property's type is a delta from zero, so it is written as is. w.varint(PROP_TIMESTAMP)?; diff --git a/rs/moq-net/src/ietf/namespace.rs b/rs/moq-net/src/ietf/namespace.rs index b5641842472..a86c24bdf18 100644 --- a/rs/moq-net/src/ietf/namespace.rs +++ b/rs/moq-net/src/ietf/namespace.rs @@ -117,9 +117,7 @@ pub fn decode_track(r: &mut Decoder<'_>) -> Result<(Path<'static>, String), Deco pub(super) fn refusal(namespace: &Path, version: super::Version) -> Option> { if matches!( version, - super::Version::Draft18 - | super::Version::Draft19 - | super::Version::Draft20 + super::Version::Draft18 | super::Version::Draft19 | super::Version::Draft20 ) { return None; } diff --git a/rs/moq-net/src/ietf/object.rs b/rs/moq-net/src/ietf/object.rs index 1073d0430b3..ca41027047c 100644 --- a/rs/moq-net/src/ietf/object.rs +++ b/rs/moq-net/src/ietf/object.rs @@ -9,10 +9,7 @@ use crate::{ /// Draft-19 assigns property 0x06 to SUBGROUP_DELIVERY_TIMEOUT. pub(crate) fn has_delivery_timeout(version: Version) -> bool { - !matches!( - version, - Version::Draft18 - ) + !matches!(version, Version::Draft18) } /// Object status: no object at or past this location in the group exists. diff --git a/rs/moq-net/src/ietf/parameters.rs b/rs/moq-net/src/ietf/parameters.rs index 8e204185527..5d28d580218 100644 --- a/rs/moq-net/src/ietf/parameters.rs +++ b/rs/moq-net/src/ietf/parameters.rs @@ -565,9 +565,7 @@ mod tests { #[test] fn test_param_u8_all_versions() { - for version in [ - Version::Draft18, - ] { + for version in [Version::Draft18] { round_trip_params( version, |w, v| { @@ -607,9 +605,7 @@ mod tests { #[test] fn test_param_bool_all_versions() { - for version in [ - Version::Draft18, - ] { + for version in [Version::Draft18] { round_trip_params( version, |w, v| { @@ -628,9 +624,7 @@ mod tests { #[test] fn test_param_location_all_versions() { let loc = Location { group: 5, object: 3 }; - for version in [ - Version::Draft18, - ] { + for version in [Version::Draft18] { round_trip_params( version, |w, v| { @@ -676,9 +670,7 @@ mod tests { #[test] fn test_param_filter_all_versions() { - for version in [ - Version::Draft18, - ] { + for version in [Version::Draft18] { round_trip_params( version, |w, v| { @@ -696,9 +688,7 @@ mod tests { #[test] fn test_param_multiple_delta_encoding() { - for version in [ - Version::Draft18, - ] { + for version in [Version::Draft18] { round_trip_params( version, |w, v| { @@ -729,9 +719,7 @@ mod tests { #[test] fn test_param_empty_set() { - for version in [ - Version::Draft18, - ] { + for version in [Version::Draft18] { round_trip_params( version, |w, v| { @@ -748,9 +736,7 @@ mod tests { #[test] fn test_param_option_skip_none() { - for version in [ - Version::Draft18, - ] { + for version in [Version::Draft18] { round_trip_params( version, |w, v| { @@ -776,9 +762,7 @@ mod tests { #[test] fn test_param_option_encode_some() { - for version in [ - Version::Draft18, - ] { + for version in [Version::Draft18] { round_trip_params( version, |w, v| { @@ -805,9 +789,7 @@ mod tests { #[test] fn test_param_bare_type_defaults() { // Bare types use T::default() when the parameter is absent - for version in [ - Version::Draft18, - ] { + for version in [Version::Draft18] { round_trip_params( version, |w, v| { @@ -831,9 +813,7 @@ mod tests { #[test] fn test_param_unknown_rejected() { // 0x3E is not a message parameter, which closes the session. - for version in [ - Version::Draft18, - ] { + for version in [Version::Draft18] { let mut buf = Vec::new(); let mut w = Encoder::new(&mut buf, version.into()); w.varint(1).unwrap(); @@ -877,9 +857,7 @@ mod tests { #[test] fn test_param_duplicate_rejected() { // Manually construct a buffer with duplicate key 0x20 - for version in [ - Version::Draft18, - ] { + for version in [Version::Draft18] { let mut buf = Vec::new(); let mut w = Encoder::new(&mut buf, version.into()); // Encode count = 2 diff --git a/rs/moq-net/src/ietf/properties.rs b/rs/moq-net/src/ietf/properties.rs index a6880097180..f8efa8370ed 100644 --- a/rs/moq-net/src/ietf/properties.rs +++ b/rs/moq-net/src/ietf/properties.rs @@ -109,7 +109,6 @@ impl Properties { /// A property the publisher sealed inside Immutable Properties travels there, so it is /// not restated in the mutable list. pub fn encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { - let int = |id, value| Property { id, value: Value::Int(value), diff --git a/rs/moq-net/src/ietf/publish.rs b/rs/moq-net/src/ietf/publish.rs index c64a6fa60c3..7cb33c05511 100644 --- a/rs/moq-net/src/ietf/publish.rs +++ b/rs/moq-net/src/ietf/publish.rs @@ -160,14 +160,7 @@ impl PublishDone<'_> { code if code == PublishDoneStatus::Unauthorized.code() => Err(crate::Error::Unauthorized), // SUBSCRIPTION_ENDED: the subscription reached the end its filter asked for. // Draft-20 removed it and left 0x3 unassigned. - 0x3 if matches!( - version, - Version::Draft18 - | Version::Draft19 - ) => - { - Ok(()) - } + 0x3 if matches!(version, Version::Draft18 | Version::Draft19) => Ok(()), code => Err(crate::Error::Remote(u32::try_from(code).unwrap_or(u32::MAX))), } } @@ -310,18 +303,12 @@ impl Message for PublishOk { _ => { // GROUP_ORDER left PUBLISH_OK in draft-19. Sending it there is a parameter // on the wrong message, which closes the session. - let group_order = matches!( - version, - Version::Draft18 - ) - .then_some(self.group_order) - .flatten(); - let new_group = matches!( - version, - Version::Draft18 | Version::Draft19 - ) - .then_some(self.new_group) - .flatten(); + let group_order = matches!(version, Version::Draft18) + .then_some(self.group_order) + .flatten(); + let new_group = matches!(version, Version::Draft18 | Version::Draft19) + .then_some(self.new_group) + .flatten(); encode_params!(w, version, 0x10 => self.forward, 0x20 => self.subscriber_priority, @@ -465,10 +452,7 @@ mod tests { #[test] fn test_publish_done_registered_statuses_all_versions() { - for version in [ - Version::Draft18, - Version::Draft19, - ] { + for version in [Version::Draft18, Version::Draft19] { for (status, expected) in [ (PublishDoneStatus::InternalError, 0x0), (PublishDoneStatus::TrackEnded, 0x2), diff --git a/rs/moq-net/src/ietf/publisher.rs b/rs/moq-net/src/ietf/publisher.rs index f2742cafc25..2f19ef862db 100644 --- a/rs/moq-net/src/ietf/publisher.rs +++ b/rs/moq-net/src/ietf/publisher.rs @@ -1165,9 +1165,7 @@ impl Publisher { match admitted { Ok(grant) => (gate.named(), Some(grant)), Err(refusal) => { - return self - .reject_subscribe(stream, &refusal.error, &refusal.reason) - .await; + return self.reject_subscribe(stream, &refusal.error, &refusal.reason).await; } } } @@ -1437,14 +1435,11 @@ impl Publisher { Err(refusal) => match acked { Some(opened) => (Served::Track(Err(refusal.error)), opened.load(Ordering::Relaxed)), None => { - return self - .reject_subscribe(stream, &refusal.error, &refusal.reason) - .await; + return self.reject_subscribe(stream, &refusal.error, &refusal.reason).await; } }, }; - self.write_publish_done(stream, served, streams, Some(&credit)) - .await + self.write_publish_done(stream, served, streams, Some(&credit)).await } } @@ -1619,8 +1614,7 @@ impl Publisher { if session.poll_closed(&mut cx).is_ready() { return Poll::Ready(Turn::Cancelled); } - if stream.writer.poll_closed(&mut cx).is_ready() - { + if stream.writer.poll_closed(&mut cx).is_ready() { return Poll::Ready(Turn::Cancelled); } if let Poll::Ready(renewed) = renewal.poll(waiter) { @@ -1649,10 +1643,6 @@ impl Publisher { }) .await; - // Draft 14 answers no update. Drafts 15 and 16 answer on the control stream, - // naming the update's own Request ID; later drafts name none. - let answer = true; - match turn { // A malformed update, such as one whose AUTHORIZATION TOKEN names an alias, // closes the session rather than only ending this subscription, as the same @@ -1669,16 +1659,13 @@ impl Publisher { } Turn::Served(res) => return Some(Served::Track(res)), Turn::Cancelled => return None, - Turn::Renewal(request_update::Verdict { - verdict, - grant: res, - }) => { + Turn::Renewal(request_update::Verdict { verdict, grant: res }) => { // Only a token-authorized request starts a renewal. let rg = request_grant.as_ref().expect("a renewal without a request grant"); // On accept the old grant is dropped (ending the old token) and the deadline // re-armed at the new expiry. let Err(refusal) = rg.renew(verdict, res) else { - if answer && let Err(err) = self.write_request_ok(&mut stream.writer, None).await { + if let Err(err) = self.write_request_ok(&mut stream.writer, None).await { return Some(Served::Track(Err(err))); } continue; @@ -1686,10 +1673,9 @@ impl Publisher { // Per drafts 16 section 9.11.1 / 18 section 10.9.1, a refused or uncovered // renewal ends only this subscription: REQUEST_ERROR, then PUBLISH_DONE // UPDATE_FAILED. The session stays up and the old grant does not survive. - if answer - && let Err(err) = self - .write_subscribe_error(&mut stream.writer, &refusal.error, &refusal.reason) - .await + if let Err(err) = self + .write_subscribe_error(&mut stream.writer, &refusal.error, &refusal.reason) + .await { return Some(Served::Track(Err(err))); } @@ -1823,8 +1809,7 @@ impl Publisher { if session.poll_closed(&mut cx).is_ready() { return Poll::Ready(UpdateRead::Cancelled); } - if stream.writer.poll_closed(&mut cx).is_ready() - { + if stream.writer.poll_closed(&mut cx).is_ready() { return Poll::Ready(UpdateRead::Cancelled); } self.poll_update(stream, finished, goaways, &mut cx) @@ -1934,9 +1919,6 @@ impl Publisher { renewal: &mut request_update::Renewal, update: super::request_stream::Update, ) -> Result<(), Served> { - // Draft 14 answers no update. Drafts 15 and 16 answer on the control stream, naming the - // update's own Request ID; later drafts name none. - let answer = true; let failed = |err| Served::Track(Err(err)); // A token renews a token-authorized request. Where tokens decide, one arriving on a // request admitted without one is refused rather than taken unread, so every token this @@ -1976,12 +1958,10 @@ impl Publisher { None => None, }; if let Some((reason, err)) = refused { - if answer { - self.write_subscribe_error(&mut stream.writer, &err, reason) - .await - .map_err(failed)?; - } - // Either way the update failed, which ends the subscription. + self.write_subscribe_error(&mut stream.writer, &err, reason) + .await + .map_err(failed)?; + // The update failed, which ends the subscription. return Err(Served::UpdateFailed(Error::Unsupported)); } if let Some(priority) = update.priority { @@ -2005,30 +1985,18 @@ impl Publisher { renewal.verify(grant.verify_renewal(&self.auth, token.0)); return Ok(()); } - if answer { - self.write_request_ok(&mut stream.writer, None) - .await - .map_err(failed)?; - } + self.write_request_ok(&mut stream.writer, None).await.map_err(failed)?; Ok(()) } /// Answer a REQUEST_UPDATE with REQUEST_OK, carrying the `largest` Location a resumed /// subscription now starts from. - /// - /// Drafts 15 and 16 answer on the control stream, naming the update's own Request ID; - /// later drafts name none. Draft-14 answers nothing and never reaches here. async fn write_request_ok( &self, writer: &mut Writer, largest: Option, ) -> Result<(), Error> { - writer - .encode_message(&ietf::RequestOk { - largest, - active: None, - }) - .await + writer.encode_message(&ietf::RequestOk { largest, active: None }).await } /// Reject a SUBSCRIBE, ending the request stream. @@ -2043,8 +2011,7 @@ impl Publisher { err: &Error, reason: &str, ) -> Result<(), Error> { - self.write_subscribe_error(&mut stream.writer, err, reason) - .await?; + self.write_subscribe_error(&mut stream.writer, err, reason).await?; // The peer dropping the stream once it has the rejection is a normal end, not our failure. let _ = stream.writer.close().await; @@ -2058,8 +2025,7 @@ impl Publisher { err: &Error, reason: &str, ) -> Result<(), Error> { - super::request_stream::write_error(writer, request::Kind::Subscribe, err, reason, self.version) - .await + super::request_stream::write_error(writer, request::Kind::Subscribe, err, reason, self.version).await } /// Serve a draft-20 fill on its own fetch stream: the requested range, read from the @@ -2335,11 +2301,7 @@ impl Publisher { // Every frame on a timed track is timed (see `track::Info::timescale`). let timestamp = timestamp.ok_or(Error::TimestampMismatch)?; let mut properties = Vec::new(); - ietf::encode_object_time( - &mut Encoder::new(&mut properties, version.into()), - timestamp, - timescale, - )?; + ietf::encode_object_time(&mut Encoder::new(&mut properties, version.into()), timestamp, timescale)?; Some(properties) } None => None, @@ -2458,11 +2420,7 @@ impl Publisher { // don't write, so waiting on upstream regardless would ignore what was asked. if msg.fill_timeout { return self - .reject_fetch( - stream, - &Error::Unsupported, - "FILL_TIMEOUT not supported", - ) + .reject_fetch(stream, &Error::Unsupported, "FILL_TIMEOUT not supported") .await; } @@ -2526,18 +2484,14 @@ impl Publisher { return Err(Error::Unauthorized); }; if !gate.allows() { - return self - .reject_fetch(stream, &Error::Unauthorized, "not granted") - .await; + return self.reject_fetch(stream, &Error::Unauthorized, "not granted").await; } // End Object 0 includes the entire End Group, even at the largest Group ID. if start.group > end.group || (start.group == end.group && end.object != 0 && start.object >= end.object) { - return self - .reject_fetch(stream, &Error::InvalidRange, "empty range") - .await; + return self.reject_fetch(stream, &Error::InvalidRange, "empty range").await; } // The peer must have seen the announcement to name this namespace, so this @@ -2594,11 +2548,7 @@ impl Publisher { FetchType::AbsoluteJoining { group_id, .. } if group_id <= end.group => group_id, _ => { return self - .reject_fetch( - stream, - &Error::InvalidRange, - "joining group past the subscription", - ) + .reject_fetch(stream, &Error::InvalidRange, "joining group past the subscription") .await; } }; @@ -2616,9 +2566,7 @@ impl Publisher { } // Rewritten as standalone or refused above. FetchType::Filtered { .. } => { - return self - .reject_fetch(stream, &Error::Unsupported, "not supported") - .await; + return self.reject_fetch(stream, &Error::Unsupported, "not supported").await; } }; @@ -2684,10 +2632,7 @@ impl Publisher { group.end_object = group.end_object.map(|end| status_fields(end)); // FETCH keeps each object's Timestamp, in the units FETCH_OK declares wherever the // draft can declare them. Opting out of the properties strips nothing from the objects. - let timescale = group - .info - .timescale -; + let timescale = group.info.timescale; let frames = group .frames .iter() @@ -2731,11 +2676,7 @@ impl Publisher { let past_largest = largest.is_some_and(|largest| largest.is_none_or(|object| start.object > object)); if draft20 && past_largest { return self - .reject_fetch( - stream, - &Error::InvalidRange, - "start past Largest Object", - ) + .reject_fetch(stream, &Error::InvalidRange, "start past Largest Object") .await; } @@ -2950,15 +2891,12 @@ impl Publisher { })) => return Ok(Ok((end, cache, namespace, ranking, token))), Ok(None) => ( match self.version { - Version::Draft18 - | Version::Draft19 => Error::InvalidJoiningRequestId, + Version::Draft18 | Version::Draft19 => Error::InvalidJoiningRequestId, _ => Error::NotFound, }, "no such subscription", ), - Ok(Some(Joined::Unsupported)) => { - (Error::Unsupported, "joining filter not supported") - } + Ok(Some(Joined::Unsupported)) => (Error::Unsupported, "joining filter not supported"), Ok(Some(Joined::Empty)) => (Error::InvalidRange, "no objects at subscription start"), }; Ok(Err(refusal)) @@ -3049,9 +2987,7 @@ impl Publisher { largest: live_edge(&track).largest, properties, }; - stream - .writer - .buffer_message(ietf::TrackStatusOk::ID, &ok)?; + stream.writer.buffer_message(ietf::TrackStatusOk::ID, &ok)?; std::future::poll_fn(|cx| stream.writer.poll_flush(cx)).await?; Ok::<(), Error>(()) }; @@ -3076,11 +3012,7 @@ impl Publisher { Ok(()) } - async fn reject_track_status( - &self, - mut stream: Stream, - err: &Error, - ) -> Result<(), Error> { + async fn reject_track_status(&self, mut stream: Stream, err: &Error) -> Result<(), Error> { let (kind, reason) = (request::Kind::TrackStatus, err.to_string()); super::request_stream::write_error(&mut stream.writer, kind, err, &reason, self.version).await?; let _ = stream.writer.close().await; @@ -3148,10 +3080,7 @@ impl Publisher { // moq-transport renames a request's auth with REQUEST_UPDATE, but cannot name one on // a request that had none, nor drop one, and before draft-16 cannot update a // namespace at all: those withdraw the request and make it again. - let restart = renamed - || held_auth != auth - && (held_auth.is_none() - || auth.is_none()); + let restart = renamed || held_auth != auth && (held_auth.is_none() || auth.is_none()); // An update already waiting or in flight carries whatever is current once it goes // out, and its answer brings the loop back here for anything after that. @@ -3224,8 +3153,7 @@ impl Publisher { } } // Register an idle reader too: GOAWAY can arrive without an update in flight. - if requests.contains_key(suffix) - { + if requests.contains_key(suffix) { woken.suffixes.lock().unwrap().push(suffix.clone()); } // The peer can reject a fresh PUBLISH_NAMESPACE or an update, which leaves @@ -3886,10 +3814,7 @@ impl Publisher { // Send OK response stream .writer - .encode_message(&ietf::RequestOk { - largest: None, - active, - }) + .encode_message(&ietf::RequestOk { largest: None, active }) .await?; // Draft-14/15 predate NAMESPACE, so they answer with their own PUBLISH_NAMESPACE @@ -4023,9 +3948,7 @@ impl Publisher { // Each update in flight is answered on its own stream, so many renew at // once and none holds up the announcements behind it. let now = self.runtime.now(); - if let Poll::Ready((suffix, answer)) = - Self::poll_answer(requests, inflight, woken, now, &mut cx) - { + if let Poll::Ready((suffix, answer)) = Self::poll_answer(requests, inflight, woken, now, &mut cx) { return Poll::Ready(NamespaceEvent::Answered(suffix, answer)); } // The front is now the earliest update still waiting. @@ -4561,20 +4484,14 @@ impl TrackServe { Some(wire) => { // A datagram this draft cannot carry is dropped like an oversized one: it // is best-effort, and the track's groups do not depend on it. - if wire - .status - .is_some_and(|status| !super::object::status_defined(status)) - { + if wire.status.is_some_and(|status| !super::object::status_defined(status)) { tracing::debug!(sequence, "dropping a datagram status this draft cannot carry"); continue; } // A status keeps its meaning without the properties this draft forbids // beside it, as a stream's status object does. let stripped = Default::default(); - let forwarded = match wire - .status - .is_some_and(|status| status != 0) - { + let forwarded = match wire.status.is_some_and(|status| status != 0) { true => &stripped, false => &wire.properties, }; @@ -5194,8 +5111,7 @@ impl GroupServe { } Poll::Ready(Ok(None)) => { if !self.ended - && let Some(end) = - self.group.end_object().map(|end| status_fields(end)) + && let Some(end) = self.group.end_object().map(|end| status_fields(end)) && end.id >= self.slice.skip && self .slice .until @@ -6456,9 +6372,7 @@ mod serve_tests { let mut inflight = VecDeque::new(); let runtime = crate::time::sim(); let mut cx = std::task::Context::from_waker(std::task::Waker::noop()); - assert!( - Publisher::poll_answer(&mut requests, &mut inflight, &woken, runtime.now(), &mut cx).is_pending() - ); + assert!(Publisher::poll_answer(&mut requests, &mut inflight, &woken, runtime.now(), &mut cx).is_pending()); assert!(session.log.closes().is_empty()); session.push(&[0x10, 0, 2, 0, 0]); woken.suffixes.lock().unwrap().push(suffix); @@ -7188,10 +7102,7 @@ mod serve_tests { .await .expect("the track's error ends the subscription"); assert!(matches!(served, Served::Track(Err(Error::Unsupported))), "{served:?}"); - assert_eq!( - served.status().0, - ietf::PublishDoneStatus::InternalError - ); + assert_eq!(served.status().0, ietf::PublishDoneStatus::InternalError); assert_eq!( Served::UpdateFailed(Error::Unsupported).status().0, ietf::PublishDoneStatus::UpdateFailed @@ -7728,7 +7639,7 @@ mod serve_tests { /// a single token is answered as usual. #[moq_clock::test] async fn a_request_with_repeated_or_several_tokens_is_refused_alone() { - for (version, malformed) in [ (Version::Draft22, 0x4)] { + for (version, malformed) in [(Version::Draft22, 0x4)] { let request = |tokens: &[&[u8]]| { let mut body = track_status("room", true, version); assert_eq!(body.pop(), Some(0), "{version}"); @@ -8127,11 +8038,7 @@ mod serve_tests { /// origin's own, which is what carries a refusal the registry has no value for. #[moq_clock::test] async fn a_missing_broadcast_is_refused_with_the_draft_s_code() { - for version in [ - Version::Draft18, - Version::Draft19, - Version::Draft20, - ] { + for version in [Version::Draft18, Version::Draft19, Version::Draft20] { let error_code = 0x10; let h = serve(version); @@ -9202,10 +9109,7 @@ mod serve_tests { const FETCH_ID: RequestId = RequestId(0x2C); /// Every draft that still carries a joining FETCH. - const JOINING_DRAFTS: [Version; 2] = [ - Version::Draft18, - Version::Draft19, - ]; + const JOINING_DRAFTS: [Version; 2] = [Version::Draft18, Version::Draft19]; const INVALID_JOINING_REQUEST_ID: u64 = 0x32; @@ -9354,8 +9258,7 @@ mod serve_tests { assert_eq!(crate::coding::decode_varint(&mut buf, version).unwrap(), 0); assert_eq!(bytes::Buf::try_get_u8(&mut buf).unwrap(), 0); } - let properties = - crate::coding::decode_buf(&mut buf, version, |r, _| Ok(r.bytes()?.to_vec())).unwrap(); + let properties = crate::coding::decode_buf(&mut buf, version, |r, _| Ok(r.bytes()?.to_vec())).unwrap(); assert!(!properties.is_empty(), "{version}: missing Timestamp"); let size = crate::coding::decode_varint(&mut buf, version).unwrap() as usize; assert_eq!(size, payload.len()); @@ -9773,7 +9676,7 @@ mod serve_tests { let mut prior: Option<(u64, u64)> = None; while !buf.is_empty() { let (group, object) = { -let ietf::FetchObject::Object { group, object, .. } = + let ietf::FetchObject::Object { group, object, .. } = ietf::FetchObject::decode(&mut buf, version).unwrap() else { panic!("{version}: unexpected End of Range"); @@ -9787,7 +9690,7 @@ let ietf::FetchObject::Object { group, object, .. } = (group, object.unwrap()) } } -}; + }; let size = buf.varint().unwrap() as usize; let payload = String::from_utf8(buf.slice(size).unwrap().to_vec()).unwrap(); objects.push((group, object, payload)); @@ -9961,8 +9864,7 @@ let ietf::FetchObject::Object { group, object, .. } = } } - /// Immutable Properties keep the bytes their publisher sealed, so they are forwarded - /// only to a draft that encodes them the same way, and refused elsewhere. + /// Immutable Properties keep the bytes their publisher sealed. #[moq_clock::test] async fn immutable_track_properties_keep_their_encoding() { // Sealed: OBJECT_DELIVERY_TIMEOUT = 42 and DYNAMIC_GROUPS = 1. @@ -9991,28 +9893,6 @@ let ietf::FetchObject::Object { group, object, .. } = assert_eq!(ok.properties.unknown, upstream.unknown); // Its key is delta-encoded against TIMESCALE now, but the sealed bytes are verbatim. assert_eq!(occurrences(&h.log, &block[1..]), 1); - - // Draft 17 forbids the 7-byte varints draft 18 allows, and draft 16 has QUIC varints. - for version in [Version::Draft18] { - let h = serve_track(version, info.clone()); - let stream = Stream::open(&mut crate::driver::Transport::new(h.session.clone()), version) - .await - .unwrap(); - h.publisher - .clone() - .run_subscribe_stream(stream, subscribe(Filter::NextObject, None)) - .await - .unwrap(); - let wire = h.log.writes.lock().unwrap().clone(); - let mut buf = Decoder::new(&wire, version.into()); - assert_eq!(buf.varint().unwrap(), ietf::RequestError::ID, "{version}"); - let error = ietf::RequestError::decode(&mut buf, version).unwrap(); - assert_eq!( - error.error_code, - request::to_code(&Error::Unsupported, request::Kind::Subscribe, version), - "{version}" - ); - } } /// The info a relay behind a moq-lite hop with the `ietf` extension learns of a track @@ -11432,10 +11312,7 @@ let ietf::FetchObject::Object { group, object, .. } = #[moq_clock::test] async fn largest_object_is_the_live_edge_before_draft20() { - for version in [ - Version::Draft18, - Version::Draft19, - ] { + for version in [Version::Draft18, Version::Draft19] { assert_eq!( subscribe_ok_largest(version).await, Some(Location { group: 5, object: 3 }), @@ -11833,11 +11710,7 @@ let ietf::FetchObject::Object { group, object, .. } = "{version:?}: nothing acknowledged it" ); let (status, _) = wire.publish_done.expect("PUBLISH_DONE was sent"); - assert_eq!( - status, - ietf::PublishDoneStatus::UpdateFailed.code(), - "{version:?}" - ); + assert_eq!(status, ietf::PublishDoneStatus::UpdateFailed.code(), "{version:?}"); } } @@ -12444,11 +12317,7 @@ let ietf::FetchObject::Object { group, object, .. } = "a refused renewal ends the subscription at once, got {result:?}" ); let (status, _) = Wire::read(&h.log, VERSION).publish_done.expect("PUBLISH_DONE was sent"); - assert_eq!( - status, - ietf::PublishDoneStatus::UpdateFailed.code(), - "UPDATE_FAILED" - ); + assert_eq!(status, ietf::PublishDoneStatus::UpdateFailed.code(), "UPDATE_FAILED"); // The refused update is answered with exactly one REQUEST_ERROR before the subscription // ends. let written = h.log.writes.lock().unwrap().clone(); @@ -13969,10 +13838,7 @@ mod tests { #[moq_clock::test] async fn hidden_discovery_obeys_peer_setup_and_requested_prefix() { - for version in [ - Version::Draft19, - Version::Draft22, - ] { + for version in [Version::Draft19, Version::Draft22] { for declared in [false, true] { for solicit in [Some(false), Some(true)] { for scoped in [false, true] { @@ -13988,18 +13854,10 @@ mod tests { .await; // Visible on the subscription: not filtered, opted in, or the prefix // names the dot segment. The unsolicited loop sends a dot namespace - // only when the peer did not declare MoQ Hidden. Draft-14/15 keep - // the two paths disjoint; draft-16 and later fill the stream too. + // only when the peer did not declare MoQ Hidden. let on_stream = !declared || hidden || prefix == ".stats"; let unsolicited = solicit != Some(true) && !declared; - let legacy = false; - let inline = if legacy { - (solicit == Some(true) && on_stream) - || (solicit != Some(true) && declared && (hidden || prefix == ".stats")) - } else { - on_stream - }; - let expected = usize::from(inline) + usize::from(unsolicited); + let expected = usize::from(on_stream) + usize::from(unsolicited); assert_eq!( occurrences(&log, b"node"), expected, @@ -14012,11 +13870,10 @@ mod tests { } } - /// On draft-16 and later a peer that did not ask to be solicited hears each - /// namespace twice: an unsolicited PUBLISH_NAMESPACE and a NAMESPACE on the - /// SUBSCRIBE_NAMESPACE stream. Those are two discoveries of one namespace, not - /// two routes. A peer that asked to be solicited hears it only on the stream. - /// Draft-14 and 15 still answer a non-solicit peer only with PUBLISH_NAMESPACE. + /// A peer that did not ask to be solicited hears each namespace twice: an + /// unsolicited PUBLISH_NAMESPACE and a NAMESPACE on the SUBSCRIBE_NAMESPACE stream. + /// Those are two discoveries of one namespace, not two routes. A peer that asked to + /// be solicited hears it only on the stream. #[moq_clock::test] async fn a_non_solicit_peer_hears_a_namespace_both_ways() { let (times, streams) = advertise_both_ways(Some(false)).await; @@ -14026,20 +13883,6 @@ mod tests { let (once, streams) = advertise_both_ways(Some(true)).await; assert_eq!(once, 1, "a peer that asked to be told on request is told once"); assert_eq!(streams, 1, "inline on the SUBSCRIBE_NAMESPACE stream it asked on"); - - for version in [Version::Draft18] { - let log = advertise_with_hidden(Discovery { - version, - solicit: Some(false), - ..Discovery::default() - }) - .await; - assert_eq!( - occurrences(&log, b"cam"), - 1, - "{version:?} still answers only with PUBLISH_NAMESPACE" - ); - } } /// A peer that did not send SOLICIT still gets NAMESPACE on its SUBSCRIBE_NAMESPACE @@ -14132,10 +13975,8 @@ mod tests { } /// Send capacity can park an admitted stream's header. That must not wedge the loop: - /// withdrawals queued behind it still need to progress. - /// - /// Draft-14 so the withdrawal names its namespace on the wire, which is what makes the - /// loop's progress visible while every open is blocked. + /// withdrawals queued behind it still need to progress. A withdrawal ends its request's + /// own stream, which needs no new open, so it is visible while every open is blocked. #[moq_clock::test] async fn a_parked_open_still_lets_a_namespace_be_withdrawn() { const VERSION: Version = Version::Draft18; @@ -14183,18 +14024,19 @@ mod tests { // it despite the open above. drop(first); + let ended = || { + log.trail() + .into_iter() + .any(|(_, sent)| !matches!(sent, crate::lite::test_transport::Sent::Write(_))) + }; for _ in 0..100 { assert!(futures::poll!(run.as_mut()).is_pending()); - if occurrences(&log, b"first-cam") >= 2 { + if ended() { break; } tick().await; } - assert_eq!( - occurrences(&log, b"first-cam"), - 2, - "PUBLISH_NAMESPACE_DONE never sent: the open wedged the loop" - ); + assert!(ended(), "the withdrawal never went out: the open wedged the loop"); // Send capacity returns, and nothing else about the origin changes. set_gate(&gate, true); @@ -15336,9 +15178,7 @@ mod tests { #[moq_clock::test] async fn an_advertisement_without_credit_is_retried_and_spends_no_id() { use std::sync::atomic::Ordering; - for version in [ - Version::Draft19, - ] { + for version in [Version::Draft19] { let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let _cam = origin.announce("lonely-cam", crate::origin::Route::default()).unwrap(); settle().await; @@ -16418,14 +16258,9 @@ mod compression_tests { track_properties(&info, wanted, version, false), Err(Error::Unsupported) )); - assert!(matches!( - track_properties(&info, wanted, version, true), - Err(Error::Unsupported) - )); } let plain = track::Info::default().with_compress(true); assert!(!track_properties(&plain, true, version, false).unwrap().compress); - assert!(!track_properties(&plain, true, version, true).unwrap().compress); } } diff --git a/rs/moq-net/src/ietf/publisher/tracks.rs b/rs/moq-net/src/ietf/publisher/tracks.rs index e1ed3083b96..01f4565f474 100644 --- a/rs/moq-net/src/ietf/publisher/tracks.rs +++ b/rs/moq-net/src/ietf/publisher/tracks.rs @@ -1090,8 +1090,7 @@ impl Publisher { None => None, }; if let Some((reason, err)) = refusal { - self.write_subscribe_error(&mut stream.writer, &err, reason) - .await?; + self.write_subscribe_error(&mut stream.writer, &err, reason).await?; break Served::UpdateFailed(Error::Unsupported); } if let Some(priority) = update.priority { @@ -1204,9 +1203,7 @@ impl Publisher { Some(Ok(subscriber)) => subscriber, Some(Err(err)) => return self.resume_failed(stream, how.answer, err).await, None => { - return self - .resume_failed(stream, how.answer, Error::NotFound) - .await; + return self.resume_failed(stream, how.answer, Error::NotFound).await; } }; let live = kio::wait(|waiter| { @@ -1220,19 +1217,14 @@ impl Publisher { }) .await?; if !live { - return self - .resume_failed(stream, how.answer, Error::NotFound) - .await; + return self.resume_failed(stream, how.answer, Error::NotFound).await; } let edge = live_edge(track); let range = subscribe_range(params.filter, edge, self.version); range.apply(&mut subscription); let _ = subscriber.update(subscription); - let timescale = subscriber - .info() - .timescale -; + let timescale = subscriber.info().timescale; let mut serve = TrackServe::new( self.session.clone(), subscriber, @@ -1270,12 +1262,7 @@ impl Publisher { .filter(|_| Filter::is_draft20(self.version)) .map(|fill| fill_range(fill, params.filter, edge.largest)); - if how.answer - && self - .write_request_ok(&mut stream.writer, edge.largest) - .await - .is_err() - { + if how.answer && self.write_request_ok(&mut stream.writer, edge.largest).await.is_err() { return Err(()); } diff --git a/rs/moq-net/src/ietf/request_stream.rs b/rs/moq-net/src/ietf/request_stream.rs index 7a6b24c3b9d..d756872e93c 100644 --- a/rs/moq-net/src/ietf/request_stream.rs +++ b/rs/moq-net/src/ietf/request_stream.rs @@ -9,10 +9,7 @@ use crate::{ }; pub(super) fn fin_cancels(version: Version) -> bool { - matches!( - version, - Version::Draft18 - ) + matches!(version, Version::Draft18) } /// Watch abrupt cancellation after the requester has finished sending messages. diff --git a/rs/moq-net/src/ietf/request_update.rs b/rs/moq-net/src/ietf/request_update.rs index e22740abd78..eeb19514aec 100644 --- a/rs/moq-net/src/ietf/request_update.rs +++ b/rs/moq-net/src/ietf/request_update.rs @@ -40,10 +40,7 @@ pub const UNNEGOTIATED_GUARD: usize = 64; /// Whether this version defines MAX_REQUEST_UPDATES. Draft-19 section 10.3.1.7 added it; the /// drafts below carry no such Setup Option. pub fn supported(version: Version) -> bool { - !matches!( - version, - Version::Draft18 - ) + !matches!(version, Version::Draft18) } /// A request's in-band renewal: the verdict on the token being verified, and the updates that @@ -163,9 +160,7 @@ mod tests { ); } - for version in [ - Version::Draft18, - ] { + for version in [Version::Draft18] { let mut params = super::super::Parameters::default(); into_setup(&mut params, version); assert_eq!( diff --git a/rs/moq-net/src/ietf/session.rs b/rs/moq-net/src/ietf/session.rs index 84d43bf6f00..38bda97bb34 100644 --- a/rs/moq-net/src/ietf/session.rs +++ b/rs/moq-net/src/ietf/session.rs @@ -10,8 +10,8 @@ use crate::{ }; use super::{ - Control, Message, Publisher, Subscriber, Version, active_count, auth, cluster, - epoch, hidden, max_age, peer, request_update, solicit, subscriber::is_protocol_violation, + Control, Message, Publisher, Subscriber, Version, active_count, auth, cluster, epoch, hidden, max_age, peer, + request_update, solicit, subscriber::is_protocol_violation, }; /// Keep the session's SUBSCRIBE_NAMESPACE requests open as `discovery` decides, each on @@ -318,8 +318,7 @@ pub fn start(config: Config) -> Result<(Driver, crate::goaway::Handle), Error> { async move { match peer_setup_stream { Some(reader) => { - run_goaway(reader.with_stats(&stats).with_version(version), version, client, goaway) - .await + run_goaway(reader.with_stats(&stats).with_version(version), version, client, goaway).await } None => std::future::pending().await, } @@ -1895,11 +1894,7 @@ mod tests { /// reset: a peer that never closes it never sees the two overlap. #[moq_clock::test] async fn a_broader_watch_waits_for_the_narrower_to_leave_the_wire() { - for version in [ - Version::Draft18, - Version::Draft19, - Version::Draft22, - ] { + for version in [Version::Draft18, Version::Draft19, Version::Draft22] { let subscribe = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let _front = watch(&subscribe, "cam/front"); let (log, _driver) = client(version, subscribe.clone(), None).await; @@ -2769,6 +2764,7 @@ mod tests { let session = crate::lite::test_transport::ScriptedSession::new(Vec::new()).with_incoming_unis(vec![ uni_stream(VERSION, PADDING), subgroup_header(VERSION, 7, 0).await, + setup, ]); let accepted = accept_setup(&mut crate::driver::Transport::new(session.clone()), VERSION) .await @@ -2790,7 +2786,6 @@ mod tests { async fn an_unknown_uni_type_closes_the_session() { for (version, kind) in [ (Version::Draft19, 0), - // Padding and uni SETUP arrived in later drafts, // SUBGROUP_HEADER types with the reserved SUBGROUP_ID_MODE (0b11). (Version::Draft19, 0x56), ] { diff --git a/rs/moq-net/src/ietf/subscribe.rs b/rs/moq-net/src/ietf/subscribe.rs index 6e557a4f67c..1c11ce6363d 100644 --- a/rs/moq-net/src/ietf/subscribe.rs +++ b/rs/moq-net/src/ietf/subscribe.rs @@ -54,10 +54,7 @@ pub struct Subscribe<'a> { /// The Range Filters (0x25-0x29) arrived in draft-19. pub(super) fn has_range_filters(version: Version) -> bool { - !matches!( - version, - Version::Draft18 - ) + !matches!(version, Version::Draft18) } impl Message for Subscribe<'_> { @@ -68,65 +65,65 @@ impl Message for Subscribe<'_> { let (track_namespace, track_name) = decode_track(r)?; let track_name = Cow::Owned(track_name); - decode_params!(r, version, - 0x02 => _object_delivery_timeout: Option, - 0x03 => authorization_token: Vec, - 0x04 => rendezvous_timeout: Option, - 0x06 => _subgroup_delivery_timeout: Option, - 0x10 => forward: Option, - 0x20 => subscriber_priority: Option, - 0x21 => filter: Option, - 0x22 => group_order: Option, - 0x23 => fill: Option where Filter::is_draft20(version), - 0x25 => subgroup_filter: Vec where has_range_filters(version), - 0x26 => object_id_filter: Vec where has_range_filters(version), - 0x27 => priority_filter: Vec where has_range_filters(version), - 0x28 => object_property_filter: Vec where has_range_filters(version), - 0x32 => new_group: Option, - 0x35 => include_properties: Option where Filter::is_draft20(version), - epoch::EPOCH_PARAM => epoch: Option, - super::max_age::PARAMETER => max_age: Option, - ); - let authorization_token = super::token::single(authorization_token)?; - - let range_filters = [ - subgroup_filter, - object_id_filter, - priority_filter, - object_property_filter, - ] - .iter() - .any(|filter| !filter.is_empty()); - - // Defaults to 1, so an absent parameter means the subscriber wants them. - let properties_wanted = include_properties.unwrap_or(true); - - // A zero cap still distinguishes a requested wait from an immediate miss. - let rendezvous = rendezvous_timeout.is_some_and(|millis| millis > 0); - - let subscriber_priority = subscriber_priority.unwrap_or(super::priority::DEFAULT_WIRE); - let group_order = group_order.unwrap_or(GroupOrder::Descending); - // An absent LOCATION_FILTER means the subscription is unfiltered. - let filter = filter.unwrap_or(Filter::Unfiltered); - - Ok(Self { - request_id, - track_namespace, - track_name, - subscriber_priority, - group_order, - filter, - fill, - properties_wanted, - // Absent means 1. - forward: forward.unwrap_or(true), - rendezvous, - range_filters, - authorization_token, - new_group, - max_age: super::max_age::validate(max_age)?, - epoch, - }) + decode_params!(r, version, + 0x02 => _object_delivery_timeout: Option, + 0x03 => authorization_token: Vec, + 0x04 => rendezvous_timeout: Option, + 0x06 => _subgroup_delivery_timeout: Option, + 0x10 => forward: Option, + 0x20 => subscriber_priority: Option, + 0x21 => filter: Option, + 0x22 => group_order: Option, + 0x23 => fill: Option where Filter::is_draft20(version), + 0x25 => subgroup_filter: Vec where has_range_filters(version), + 0x26 => object_id_filter: Vec where has_range_filters(version), + 0x27 => priority_filter: Vec where has_range_filters(version), + 0x28 => object_property_filter: Vec where has_range_filters(version), + 0x32 => new_group: Option, + 0x35 => include_properties: Option where Filter::is_draft20(version), + epoch::EPOCH_PARAM => epoch: Option, + super::max_age::PARAMETER => max_age: Option, + ); + let authorization_token = super::token::single(authorization_token)?; + + let range_filters = [ + subgroup_filter, + object_id_filter, + priority_filter, + object_property_filter, + ] + .iter() + .any(|filter| !filter.is_empty()); + + // Defaults to 1, so an absent parameter means the subscriber wants them. + let properties_wanted = include_properties.unwrap_or(true); + + // A zero cap still distinguishes a requested wait from an immediate miss. + let rendezvous = rendezvous_timeout.is_some_and(|millis| millis > 0); + + let subscriber_priority = subscriber_priority.unwrap_or(super::priority::DEFAULT_WIRE); + let group_order = group_order.unwrap_or(GroupOrder::Descending); + // An absent LOCATION_FILTER means the subscription is unfiltered. + let filter = filter.unwrap_or(Filter::Unfiltered); + + Ok(Self { + request_id, + track_namespace, + track_name, + subscriber_priority, + group_order, + filter, + fill, + properties_wanted, + // Absent means 1. + forward: forward.unwrap_or(true), + rendezvous, + range_filters, + authorization_token, + new_group, + max_age: super::max_age::validate(max_age)?, + epoch, + }) } fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { @@ -134,30 +131,30 @@ impl Message for Subscribe<'_> { encode_namespace(w, &self.track_namespace)?; w.string(&self.track_name)?; - // FILL_PARAMETERS arrived in draft-20. Sending it to an older peer would be an - // unknown parameter, which is a protocol violation, so it is dropped instead. - // A subscriber there simply joins mid-group, which is what it did all along. - let fill = self.fill.filter(|_| Filter::is_draft20(version)); + // FILL_PARAMETERS arrived in draft-20. Sending it to an older peer would be an + // unknown parameter, which is a protocol violation, so it is dropped instead. + // A subscriber there simply joins mid-group, which is what it did all along. + let fill = self.fill.filter(|_| Filter::is_draft20(version)); - // INCLUDE_PROPERTIES defaults to 1, so only the opt-out is worth bytes. It - // arrived in draft-20, and an older peer would read it as an unknown - // parameter, which is a protocol violation. - let include_properties = (!self.properties_wanted && Filter::is_draft20(version)).then_some(false); - // Only a session that negotiated MoQ Epoch names one. - let epoch = self.epoch.clone(); + // INCLUDE_PROPERTIES defaults to 1, so only the opt-out is worth bytes. It + // arrived in draft-20, and an older peer would read it as an unknown + // parameter, which is a protocol violation. + let include_properties = (!self.properties_wanted && Filter::is_draft20(version)).then_some(false); + // Only a session that negotiated MoQ Epoch names one. + let epoch = self.epoch.clone(); - encode_params!(w, version, - 0x03 => self.authorization_token.clone(), - 0x10 => self.forward, - 0x20 => self.subscriber_priority, - 0x21 => self.filter, - 0x22 => self.group_order, - 0x23 => fill, - 0x32 => self.new_group, - 0x35 => include_properties, - epoch::EPOCH_PARAM => epoch, - super::max_age::PARAMETER => self.max_age, - ); + encode_params!(w, version, + 0x03 => self.authorization_token.clone(), + 0x10 => self.forward, + 0x20 => self.subscriber_priority, + 0x21 => self.filter, + 0x22 => self.group_order, + 0x23 => fill, + 0x32 => self.new_group, + 0x35 => include_properties, + epoch::EPOCH_PARAM => epoch, + super::max_age::PARAMETER => self.max_age, + ); Ok(()) } @@ -257,37 +254,37 @@ impl Message for SubscribeUpdate { } fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { - // REQUEST_UPDATE - let request_id = RequestId::decode(r, version)?; - // Nothing reads an update's Range Filters yet; they are - // consumed so a legal update does not fail the session. - decode_params!(r, version, - 0x02 => _object_delivery_timeout: Option, - 0x03 => authorization_token: Vec, - 0x06 => _subgroup_delivery_timeout: Option, - 0x10 => forward: Option, - 0x20 => subscriber_priority: Option, - 0x21 => filter: Option, - 0x23 => fill: Option where Filter::is_draft20(version), - 0x25 => _subgroup_filter: Vec where has_range_filters(version), - 0x26 => _object_id_filter: Vec where has_range_filters(version), - 0x27 => _priority_filter: Vec where has_range_filters(version), - 0x28 => _object_property_filter: Vec where has_range_filters(version), - 0x29 => _track_property_filter: Vec where has_range_filters(version), - 0x32 => new_group: Option, - super::max_age::PARAMETER => max_age: Option, - ); + // REQUEST_UPDATE + let request_id = RequestId::decode(r, version)?; + // Nothing reads an update's Range Filters yet; they are + // consumed so a legal update does not fail the session. + decode_params!(r, version, + 0x02 => _object_delivery_timeout: Option, + 0x03 => authorization_token: Vec, + 0x06 => _subgroup_delivery_timeout: Option, + 0x10 => forward: Option, + 0x20 => subscriber_priority: Option, + 0x21 => filter: Option, + 0x23 => fill: Option where Filter::is_draft20(version), + 0x25 => _subgroup_filter: Vec where has_range_filters(version), + 0x26 => _object_id_filter: Vec where has_range_filters(version), + 0x27 => _priority_filter: Vec where has_range_filters(version), + 0x28 => _object_property_filter: Vec where has_range_filters(version), + 0x29 => _track_property_filter: Vec where has_range_filters(version), + 0x32 => new_group: Option, + super::max_age::PARAMETER => max_age: Option, + ); - Ok(Self { - request_id, - subscriber_priority, - forward, - filter, - authorization_token: super::token::single(authorization_token)?, - new_group, - max_age: super::max_age::validate(max_age)?, - fill, - }) + Ok(Self { + request_id, + subscriber_priority, + forward, + filter, + authorization_token: super::token::single(authorization_token)?, + new_group, + max_age: super::max_age::validate(max_age)?, + fill, + }) } } @@ -463,17 +460,6 @@ mod tests { } } - /// 0x04 is MAX_CACHE_DURATION in draft-15, defined for other messages, so a SUBSCRIBE - /// that carries it is ignored. Draft-16 dropped the id, which makes it unknown. - #[test] - fn cache_duration_on_subscribe_follows_the_draft() { - let draft15 = subscribe_with_rendezvous(0, Version::Draft18); - decode_message::(&draft15, Version::Draft18).expect("draft-15 ignores MAX_CACHE_DURATION"); - - let draft16 = subscribe_with_rendezvous(0, Version::Draft18); - decode_message::(&draft16, Version::Draft18).expect_err("draft-16 rejects unknown parameter 0x04"); - } - /// The first message parameter key on an encoded SUBSCRIBE, or `None` when it carries no /// parameters. /// @@ -487,7 +473,7 @@ mod tests { RequestId::decode(r, version).unwrap(); decode_track(r).unwrap(); - // draft-14/15 write absolute keys, draft-16+ deltas, but the first is absolute either way. + // Keys are delta coded, but the first is absolute. let count = r.varint().unwrap(); (count > 0).then(|| r.varint().unwrap()) } @@ -974,11 +960,7 @@ mod tests { /// FORWARD=0 is legal on every draft, a paused request rather than a session error. #[test] fn subscribe_decodes_forward_zero() { - for version in [ - Version::Draft18, - Version::Draft19, - Version::Draft20, - ] { + for version in [Version::Draft18, Version::Draft19, Version::Draft20] { let msg = Subscribe { new_group: None, max_age: None, @@ -1010,11 +992,7 @@ mod tests { 0x01, // Number of Parameters 0x03, 0x03, 0x03, 0x00, 0xAA, // AUTHORIZATION TOKEN ]); - for version in [ - Version::Draft18, - Version::Draft19, - Version::Draft20, - ] { + for version in [Version::Draft18, Version::Draft19, Version::Draft20] { let wire = body.clone(); let mut buf = Decoder::new(&wire, version.into()); Subscribe::decode_msg(&mut buf, version).unwrap_or_else(|e| panic!("{version}: {e}")); @@ -1124,11 +1102,7 @@ mod cache_duration_tests { ok.encode_msg(&mut Encoder::new(&mut payload, version.into()), version) .unwrap(); let got = crate::coding::decode_buf(&mut payload.as_slice(), version, SubscribeOk::decode_msg).unwrap(); - assert_eq!( - got.properties.max_cache_duration, - age, - "{version}" - ); + assert_eq!(got.properties.max_cache_duration, age, "{version}"); } } } diff --git a/rs/moq-net/src/ietf/subscribe_namespace.rs b/rs/moq-net/src/ietf/subscribe_namespace.rs index 658c0352152..54cd532a1fb 100644 --- a/rs/moq-net/src/ietf/subscribe_namespace.rs +++ b/rs/moq-net/src/ietf/subscribe_namespace.rs @@ -1,6 +1,5 @@ //! IETF moq-transport subscribe namespace messages - use crate::{Path, coding::*, ietf::RequestId}; use super::Message; @@ -416,7 +415,6 @@ mod tests { .hidden, hidden ); - } } } diff --git a/rs/moq-net/src/ietf/subscribe_tracks.rs b/rs/moq-net/src/ietf/subscribe_tracks.rs index 2a886aa699e..84b15e2aea2 100644 --- a/rs/moq-net/src/ietf/subscribe_tracks.rs +++ b/rs/moq-net/src/ietf/subscribe_tracks.rs @@ -277,17 +277,6 @@ mod tests { assert_eq!(decoded.new_group, Some(0)); } - #[test] - fn subscribe_tracks_starts_at_draft_18() { - let mut buf = Vec::new(); - let version = Version::Draft18; - assert!( - paused() - .encode_msg(&mut Encoder::new(&mut buf, version.into()), version) - .is_err() - ); - } - #[test] fn publish_skipped_round_trips() { for version in [Version::Draft18, Version::Draft22] { diff --git a/rs/moq-net/src/ietf/subscriber.rs b/rs/moq-net/src/ietf/subscriber.rs index dec647e1081..e06fa30b172 100644 --- a/rs/moq-net/src/ietf/subscriber.rs +++ b/rs/moq-net/src/ietf/subscriber.rs @@ -1625,12 +1625,8 @@ impl Subscriber { let epoch = msg.params.epoch.clone(); let Some(advert) = self.route(msg.params.cluster.as_ref(), &peer) else { tracing::debug!(%path, "dropping reflected publish_namespace"); - self.write_error( - &mut stream, - &Error::Unroutable, - "route loops through this relay", - ) - .await?; + self.write_error(&mut stream, &Error::Unroutable, "route loops through this relay") + .await?; let _ = stream.writer.close().await; return Ok(()); }; @@ -1682,8 +1678,7 @@ impl Subscriber { match admitted { Ok(grant) => (Admit::Token(slot), Some(grant)), Err(refusal) => { - self.write_error(&mut stream, &refusal.error, &refusal.reason) - .await?; + self.write_error(&mut stream, &refusal.error, &refusal.reason).await?; let _ = stream.writer.close().await; return Ok(()); } @@ -1734,13 +1729,11 @@ impl Subscriber { Err(refusal) => { tracing::info!(%path, err = %refusal.error, reason = %refusal.reason, "request token grant ended"); if !acked { - self.write_error(&mut stream, &refusal.error, &refusal.reason) - .await?; + self.write_error(&mut stream, &refusal.error, &refusal.reason).await?; let _ = stream.writer.close().await; return Ok(()); } - self.cancel_announce(&mut stream) - .await?; + self.cancel_announce(&mut stream).await?; return Err(refusal.error); } }; @@ -1750,15 +1743,12 @@ impl Subscriber { let _ = stream.writer.close().await; } Withdraw::Reject(refusal) => { - self.write_error(&mut stream, &refusal.error, &refusal.reason) - .await?; + self.write_error(&mut stream, &refusal.error, &refusal.reason).await?; let _ = stream.writer.close().await; } Withdraw::Cancel(refusal) => { - self.write_error(&mut stream, &refusal.error, &refusal.reason) - .await?; - self.cancel_announce(&mut stream) - .await?; + self.write_error(&mut stream, &refusal.error, &refusal.reason).await?; + self.cancel_announce(&mut stream).await?; } } Ok(()) @@ -1818,8 +1808,7 @@ impl Subscriber { } else { kio::wait(|waiter| { let mut cx = waiter.context(); - if let Poll::Ready(result) = stream.writer.poll_closed(&mut cx) - { + if let Poll::Ready(result) = stream.writer.poll_closed(&mut cx) { return Poll::Ready(Turn::WriterClosed(result)); } if let Poll::Ready(renewed) = renewal.poll(waiter) { @@ -1850,12 +1839,10 @@ impl Subscriber { let update_id = ietf::RequestId::decode(&mut body.decoder(self.version), self.version)?; if let Err(err) = self.spend(update_id) { if matches!(err, Error::GoingAway) { - return Ok(Withdraw::Cancel( - crate::auth::Refusal { - error: err, - reason: "going away".to_string(), - }, - )); + return Ok(Withdraw::Cancel(crate::auth::Refusal { + error: err, + reason: "going away".to_string(), + })); } return Err(err); } @@ -1906,10 +1893,7 @@ impl Subscriber { return Ok(Withdraw::Done); } Turn::Failed(err) => return Err(err), - Turn::Renewal(request_update::Verdict { - verdict, - grant: res, - }) => { + Turn::Renewal(request_update::Verdict { verdict, grant: res }) => { // Only a token-authorized announce starts a renewal. let rg = token_grant.as_ref().expect("a renewal without a request grant"); // On accept the old grant is dropped and the deadline re-armed (REQUEST_OK). @@ -1940,12 +1924,10 @@ impl Subscriber { && let Err(err) = self.spend(update_id) { if matches!(err, Error::GoingAway) { - return Ok(Withdraw::Cancel( - crate::auth::Refusal { - error: err, - reason: "going away".to_string(), - }, - )); + return Ok(Withdraw::Cancel(crate::auth::Refusal { + error: err, + reason: "going away".to_string(), + })); } return Err(err); } @@ -2069,10 +2051,7 @@ impl Subscriber { } /// End an advertisement we accepted, leaving the session up: a closed stream. - async fn cancel_announce( - &self, - stream: &mut Stream, - ) -> Result<(), Error> { + async fn cancel_announce(&self, stream: &mut Stream) -> Result<(), Error> { // An OK cut short by the grant ending is still buffered, and goes out ahead of the FIN. std::future::poll_fn(|cx| stream.writer.poll_flush(cx)).await?; if stream.writer.finish().is_ok() { @@ -2109,12 +2088,8 @@ impl Subscriber { // it, and a rejected request has no lifetime of ours to hang the cleanup on, so the // entry would have to be swept asynchronously. Any data streams the publisher opened // before reading this are dropped by the unknown-alias path instead. - self.write_publish_error( - &mut stream, - &Error::Unsupported, - "PUBLISH is not supported", - ) - .await?; + self.write_publish_error(&mut stream, &Error::Unsupported, "PUBLISH is not supported") + .await?; // The rejection is the whole exchange, but it still has to arrive: a finish alone // leaves the drop-time reset free to discard it before the peer acknowledges it. let _ = stream.writer.close().await; @@ -2173,14 +2148,7 @@ impl Subscriber { err: &Error, reason: &str, ) -> Result<(), Error> { - super::request_stream::write_error( - &mut stream.writer, - request::Kind::Publish, - err, - reason, - self.version, - ) - .await + super::request_stream::write_error(&mut stream.writer, request::Kind::Publish, err, reason, self.version).await } /// Attach the route for one newly advertised namespace, returning the id that names @@ -2875,9 +2843,7 @@ impl Subscriber { max_age_supported.then(|| super::max_age::millis(aggregate.as_ref().map_or(Duration::MAX, |s| s.budget()))); let order = aggregate.map(|s| s.order).unwrap_or_default(); // Without an Established subscription, a relay includes the request it holds. - let new_group = target - .new_group() -; + let new_group = target.new_group(); let written = match &held { // Resume the held subscription: its group order is the one it was made with, // since no update can change it. Its groups wait on the answer from here. @@ -4054,9 +4020,7 @@ impl Subscriber { id if id == ietf::TrackStatusOk::ID => { return Ok(ietf::TrackStatusOk::decode_msg(&mut data, self.version)?); } - ietf::RequestError::ID if true => { - ietf::RequestError::decode_msg(&mut data, self.version)?.error_code - } + ietf::RequestError::ID if true => ietf::RequestError::decode_msg(&mut data, self.version)?.error_code, _ => return Err(Error::UnexpectedMessage), }; Err(request::from_code(code, request::Kind::TrackStatus, self.version)) @@ -5376,7 +5340,6 @@ async fn decode_fetch_object( stream: &mut Reader, first: bool, ) -> Result, Error> { - Ok(match stream.decode_maybe::().await? { None => None, Some(ietf::FetchObject::EndOfRange { .. }) => { @@ -6245,13 +6208,7 @@ mod tests { version, ); writer.varint(ietf::RequestOk::ID).await.unwrap(); - writer - .encode(&ietf::RequestOk { - largest: None, - active, - }) - .await - .unwrap(); + writer.encode(&ietf::RequestOk { largest: None, active }).await.unwrap(); for entry in entries { match entry.strip_prefix('-') { Some(suffix) => { @@ -7229,11 +7186,7 @@ mod tests { } drop(work); if opening { - assert_eq!( - control.next_request_id(), - RequestId(1), - "cancelled open spends no ID" - ); + assert_eq!(control.next_request_id(), RequestId(1), "cancelled open spends no ID"); } assert!(log.closes().is_empty(), "the session stays up"); } @@ -7251,31 +7204,11 @@ mod tests { }; } - setup_wait_test!( - invalid_subscribe_open_is_refused, - SubscribeOpen, - [Draft22] - ); - setup_wait_test!( - invalid_status_open_is_refused, - StatusOpen, - [Draft22] - ); - setup_wait_test!( - invalid_status_response_is_refused, - StatusResponse, - [Draft22] - ); - setup_wait_test!( - invalid_fetch_open_is_refused, - FetchOpen, - [Draft22] - ); - setup_wait_test!( - invalid_fetch_response_is_refused, - FetchResponse, - [Draft22] - ); + setup_wait_test!(invalid_subscribe_open_is_refused, SubscribeOpen, [Draft22]); + setup_wait_test!(invalid_status_open_is_refused, StatusOpen, [Draft22]); + setup_wait_test!(invalid_status_response_is_refused, StatusResponse, [Draft22]); + setup_wait_test!(invalid_fetch_open_is_refused, FetchOpen, [Draft22]); + setup_wait_test!(invalid_fetch_response_is_refused, FetchResponse, [Draft22]); fn control_message_types(log: &crate::lite::test_transport::Log, version: Version) -> Vec { let writes = log.writes.lock().unwrap().clone(); @@ -7635,22 +7568,18 @@ mod tests { /// including NOT_SUPPORTED and undefined codes, without opening a subscription. #[moq_clock::test] async fn a_refused_track_status_never_subscribes() { - for version in [ - Version::Draft18, - Version::Draft19, - Version::Draft22, - ] { + for version in [Version::Draft18, Version::Draft19, Version::Draft22] { for code in [3, u64::from(u32::MAX)] { for fetching in [false, true] { let refused = message( - ietf::RequestError::ID, - &ietf::RequestError { - error_code: code, - reason_phrase: "TRACK_STATUS is refused".into(), - retry_interval: 0, - }, - version, - ); + ietf::RequestError::ID, + &ietf::RequestError { + error_code: code, + reason_phrase: "TRACK_STATUS is refused".into(), + retry_interval: 0, + }, + version, + ); let session = crate::lite::test_transport::ScriptedSession::new(refused); let log = session.log.clone(); let (tasks, _task_set) = crate::util::TaskSet::new(); @@ -9764,10 +9693,7 @@ mod tests { /// only afterwards, and on drafts 14-16 with a PUBLISH_NAMESPACE_CANCEL after the OK. #[moq_clock::test] async fn a_revoked_token_retracts_an_announce_whose_ok_is_blocked() { - for version in [ - Version::Draft18, - Version::Draft22, - ] { + for version in [Version::Draft18, Version::Draft22] { let gate = kio::Producer::new(false); let session = crate::lite::test_transport::SinkSession::gated_bi(gate.consume()); let (mut subscriber, mut requests, _cred, consumer, session) = @@ -11492,10 +11418,7 @@ mod filter_tests { ); } - const JOINING_DRAFTS: [Version; 2] = [ - Version::Draft18, - Version::Draft19, - ]; + const JOINING_DRAFTS: [Version; 2] = [Version::Draft18, Version::Draft19]; /// Every pre-draft-20 join is Largest Object plus a joining FETCH, whatever its start, /// so the current group's head arrives on the fetch stream and the live tail on the @@ -12273,21 +12196,15 @@ mod stitch_tests { } } - /// Drafts 14 and 15 carry a relayed END_OF_GROUP object through FETCH, and the next - /// relay fills its cache from that answer. Later drafts end the answer at the last frame. + /// A relayed FETCH ends its answer at the last frame rather than carrying the + /// END_OF_GROUP object, and the next relay fills its cache from that answer. #[moq_clock::test] - async fn passthrough_fetch_keeps_the_end_of_group_object() { - for version in [ - Version::Draft22, - ] { - // Only draft-14 lets a status object carry properties, so later drafts cannot mark - // a gap before it. - let legacy = false; - let terminal = if legacy { (11, &[0x3e, 2][..]) } else { (9, &[][..]) }; + async fn passthrough_fetch_ends_at_the_last_frame() { + for version in [Version::Draft22] { let objects = [ (6, &[0x3e, 6][..], None), (8, &[0x3e, 1][..], None), - (terminal.0, terminal.1, Some(END_OF_GROUP)), + (9, &[][..], Some(END_OF_GROUP)), ]; let script = object_stream(version, true, &objects); let (bytes, log) = relay_passthrough(script, version, version, Some((0, 12))).await; @@ -12295,12 +12212,7 @@ mod stitch_tests { let mut decoder = Decoder::new(&bytes, version.into()); assert_eq!(decoder.varint().unwrap(), ietf::FetchOk::ID, "{version}"); let ok = ietf::FetchOk::decode(&mut decoder, version).unwrap(); - let carried = false; - let last = match () { - _ if carried => terminal.0 + 1, - _ if Filter::is_draft20(version) => 8, - _ => 9, - }; + let last = if Filter::is_draft20(version) { 8 } else { 9 }; assert_eq!(ok.end_location.object, last, "{version}"); assert_eq!(decoder.varint().unwrap(), ietf::FetchHeader::TYPE); ietf::FetchHeader::decode(&mut decoder, version).unwrap(); @@ -12325,20 +12237,7 @@ mod stitch_tests { assert_eq!(frame.fields.as_ref().unwrap().id, id, "{version}"); } assert!(reader.read_frame().await.unwrap().is_none()); - let end_object = reader.end_object(); - assert_eq!( - end_object.as_ref().map(|end| end.id), - carried.then_some(terminal.0), - "{version}" - ); - if let Some(end) = end_object { - assert_eq!( - super::super::object::listed(&end.properties, version) - .first() - .map(|property| property.id), - legacy.then_some(0x3e) - ); - } + assert!(reader.end_object().is_none(), "{version}"); } } @@ -12361,141 +12260,6 @@ mod stitch_tests { } } - /// Draft-14 lets a status object carry properties, and it has no frame to hold its - /// time, so a mutable Timestamp stays in its block on a timed track: through a subgroup - /// and a FETCH, and out again as it arrived. - #[moq_clock::test] - async fn a_status_object_keeps_its_timestamp() { - use futures::FutureExt; - - let version = Version::Draft18; - let status = [0x10, 7]; - let leaves = |properties: &crate::model::object::Properties| { - super::super::object::write(properties, Some(version), None, version, None).unwrap() - }; - - let script = object_stream( - version, - true, - &[(0, &[0x10, 5], None), (1, &status, Some(END_OF_GROUP))], - ); - let mut h = Harness::with_timescale(Fill::Done, vec![script], Some(Timescale::MILLI)); - h.subscriber.version = version; - let mut consumer = h.track.subscribe(None); - let mut stream = h.stream().await.with_version(version); - h.subscriber.clone().recv_group(&mut stream).await.unwrap(); - let group = consumer.recv_group().now_or_never().unwrap().unwrap().unwrap(); - let end = group.end_object().expect("the END_OF_GROUP object"); - assert_eq!(leaves(&end.properties), status, "subgroup"); - - // A draft-14 FETCH object: Group, Subgroup, Object, priority, then its properties, - // an empty payload, and the status. - let mut objects = Vec::new(); - let w = &mut Encoder::new(&mut objects, version.into()); - for value in [SEQUENCE, 0, 0] { - w.varint(value).unwrap(); - } - w.u8(128); - w.bytes(&status).unwrap(); - w.varint(0u64).unwrap(); - w.varint(END_OF_GROUP).unwrap(); - let mut run = GroupFetchRun::new(version, objects).await; - let mut producer = crate::recv::Group::new(run.track.create_group(group::Info { sequence: SEQUENCE }).unwrap()); - run.subscriber - .recv_group_fetch_objects(&mut run.stream, &mut producer, 0, Some(1), Some(Timescale::MILLI)) - .await - .unwrap(); - let end = producer - .into_inner() - .consume() - .end_object() - .expect("the END_OF_GROUP object"); - assert_eq!(leaves(&end.properties), status, "fetch"); - } - - /// A draft-14 status object has no frame, so its block may fill the whole limit on a - /// timed track, though its list arrives before the status says so. A framed object's - /// block leaves room for the Timestamp egress restates. - #[moq_clock::test] - async fn a_status_object_fills_the_whole_block() { - let version = Version::Draft18; - let milli = Some(Timescale::MILLI); - // A block of `size` held bytes: one opaque property, its key, a four-byte length, - // then bytes. A framed object's list adds its Timestamp, which the frame holds. - let block = |size: usize, framed: bool| { - let mut block = Vec::new(); - let w = &mut Encoder::new(&mut block, version.into()); - if framed { - w.varint(0x10u64).unwrap(); - w.varint(5u64).unwrap(); - } - w.varint(0x21u64).unwrap(); - w.bytes(&vec![7; size - 5]).unwrap(); - block - }; - let full = block(super::super::object::MAX_BLOCK, false); - let limit = super::super::object::max_block(milli); - - for (properties, status, ok) in [ - (&full, Some(END_OF_GROUP), true), - (&block(limit + 1, true), None, false), - (&block(limit, true), None, true), - ] { - let script = object_stream(version, true, &[(0, properties, status)]); - let mut h = Harness::with_timescale(Fill::Done, vec![script], milli); - h.subscriber.version = version; - let mut stream = h.stream().await.with_version(version); - let result = h.subscriber.clone().recv_group(&mut stream).await; - match ok { - true => assert!(result.is_ok(), "subgroup {}: {result:?}", properties.len()), - false => assert!(matches!(result, Err(Error::Unsupported)), "subgroup: {result:?}"), - } - - // The fetch harness's track is untimed, so a framed object it keeps cannot be timed. - if ok && status.is_none() { - continue; - } - // A draft-14 FETCH object: Group, Subgroup, Object, priority, then its properties, - // and an empty payload with its status or a one-byte payload. - let mut objects = Vec::new(); - let w = &mut Encoder::new(&mut objects, version.into()); - for value in [SEQUENCE, 0, 0] { - w.varint(value).unwrap(); - } - w.u8(128); - w.bytes(properties).unwrap(); - match status { - Some(status) => { - w.varint(0u64).unwrap(); - w.varint(status).unwrap(); - } - None => { - w.varint(1u64).unwrap(); - w.u8(99); - } - } - let mut run = GroupFetchRun::new(version, objects).await; - let mut producer = - crate::recv::Group::new(run.track.create_group(group::Info { sequence: SEQUENCE }).unwrap()); - let result = run - .subscriber - .recv_group_fetch_objects(&mut run.stream, &mut producer, 0, Some(1), milli) - .await; - match ok { - true => assert!(result.is_ok(), "fetch {}: {result:?}", properties.len()), - false => assert!(matches!(result, Err(Error::Unsupported)), "fetch: {result:?}"), - } - if status.is_some() { - let end = producer - .into_inner() - .consume() - .end_object() - .expect("the END_OF_GROUP object"); - assert_eq!(end.properties.heap(), full.len() as u64); - } - } - } - #[moq_clock::test] async fn passthrough_refuses_contradictory_gap_properties() { for properties in [vec![0x3e, 2], vec![0x3e, 3], vec![0x3e, 1, 0, 1]] { @@ -12615,27 +12379,18 @@ mod stitch_tests { #[moq_clock::test] async fn passthrough_preserves_immutable_bytes() { - for ingress in [ - Version::Draft18, - Version::Draft22, - ] { + for ingress in [Version::Draft18, Version::Draft22] { let mut inner = Vec::new(); let mut encoder = Encoder::new(&mut inner, ingress.into()); - for value in [2u64, 96_000, 128] { + for value in [2u64, 96_000, 2, 128, 1] { encoder.varint(value).unwrap(); } - encoder - .varint(1u64) - .unwrap(); encoder.bytes(&[42; 130]).unwrap(); let mut properties = Vec::new(); let mut encoder = Encoder::new(&mut properties, ingress.into()); encoder.varint(0x0bu64).unwrap(); encoder.bytes(&inner).unwrap(); - for egress in [ - Version::Draft18, - Version::Draft22, - ] { + for egress in [Version::Draft18, Version::Draft22] { let script = object_stream(ingress, true, &[(0, &properties, None)]); let (bytes, log) = relay_passthrough_stream(script, ingress, egress).await; assert!(log.resets().is_empty()); @@ -12691,108 +12446,108 @@ mod stitch_tests { .filter(|(version, ranged)| !*ranged || *version == Version::Draft22) .flat_map(|(version, ranged)| [(version, ranged, false), (version, ranged, true)]) { - let origin = crate::origin::Config::new(crate::Hop::new(2).unwrap()).produce(); - let broadcast = origin.publish("room", crate::origin::Route::default()).unwrap(); - let track = broadcast - .create_track("video", accepted_info(&Default::default(), version)) - .unwrap(); - let mut h = Harness::with_timescale(Fill::Done, vec![], None); - h.subscriber.version = version; - h.subscriber.state.lock().subscribes.get_mut(&REQUEST).unwrap().producer = Some(track.clone()); - let properties = vec![2, 7, 3, 1, 42]; - let mut original = Vec::new(); - for id in [6, 7, 8] { - let datagram = ietf::ObjectDatagram { - track_alias: ALIAS, - group_id: SEQUENCE, - object_id: Some(id), - // An omitted priority inherits the track default, which the - // downstream response need not carry. - publisher_priority: (id != 6).then_some(200), - end_of_group: false, - properties: (id != 8).then(|| properties.clone()), - body: if id == 8 { - ietf::DatagramBody::Status(END_OF_GROUP) + let origin = crate::origin::Config::new(crate::Hop::new(2).unwrap()).produce(); + let broadcast = origin.publish("room", crate::origin::Route::default()).unwrap(); + let track = broadcast + .create_track("video", accepted_info(&Default::default(), version)) + .unwrap(); + let mut h = Harness::with_timescale(Fill::Done, vec![], None); + h.subscriber.version = version; + h.subscriber.state.lock().subscribes.get_mut(&REQUEST).unwrap().producer = Some(track.clone()); + let properties = vec![2, 7, 3, 1, 42]; + let mut original = Vec::new(); + for id in [6, 7, 8] { + let datagram = ietf::ObjectDatagram { + track_alias: ALIAS, + group_id: SEQUENCE, + object_id: Some(id), + // An omitted priority inherits the track default, which the + // downstream response need not carry. + publisher_priority: (id != 6).then_some(200), + end_of_group: false, + properties: (id != 8).then(|| properties.clone()), + body: if id == 8 { + ietf::DatagramBody::Status(END_OF_GROUP) + } else { + ietf::DatagramBody::Payload(bytes::Bytes::from_static(b"object")) + }, + }; + h.subscriber + .recv_datagram(datagram.encode_bytes(version).unwrap()) + .unwrap(); + original.push(datagram); + } + track.finish().unwrap(); + let (served, _sessions) = match hop { + true => { + let (served, sessions) = crate::lite::ietf::test_hop(&origin, "room", [true, true]).await; + (served, Some(sessions)) + } + false => (origin, None), + }; + let downstream = ScriptedSession::per_stream(vec![vec![]]).with_datagrams(65535); + let peer = peer::PeerSetup::default(); + peer.set(peer::Peer::default()); + let publisher = crate::ietf::Publisher::new( + crate::time::sim(), + crate::driver::Transport::new(downstream.clone()), + served.consume(), + Control::new(false), + None, + peer, + version, + ); + let subscribe = ietf::Subscribe { + max_age: None, + new_group: None, + request_id: RequestId(3), + track_namespace: Path::new("room"), + track_name: "video".into(), + subscriber_priority: 128, + group_order: ietf::GroupOrder::Ascending, + filter: if ranged { + Filter::Absolute { + start: ietf::Location { + group: SEQUENCE, + object: 7, + }, + end: Some(ietf::EndLocation { + group: SEQUENCE, + object: Some(7), + }), + } } else { - ietf::DatagramBody::Payload(bytes::Bytes::from_static(b"object")) + Filter::Unfiltered }, + fill: None, + properties_wanted: true, + forward: true, + rendezvous: false, + range_filters: false, + authorization_token: None, + epoch: None, }; - h.subscriber - .recv_datagram(datagram.encode_bytes(version).unwrap()) + let mut body = Vec::new(); + subscribe + .encode_msg(&mut Encoder::new(&mut body, version.into()), version) .unwrap(); - original.push(datagram); - } - track.finish().unwrap(); - let (served, _sessions) = match hop { - true => { - let (served, sessions) = crate::lite::ietf::test_hop(&origin, "room", [true, true]).await; - (served, Some(sessions)) + let stream = Stream::open(&mut crate::driver::Transport::new(downstream.clone()), version) + .await + .unwrap(); + publisher + .handle_stream(ietf::Subscribe::ID, ietf::Body(body.into()), stream, false) + .unwrap() + .await; + let sent = downstream.log.datagrams.lock().unwrap(); + original.retain(|datagram| !ranged || datagram.object_id == Some(7)); + assert_eq!(sent.len(), original.len(), "{version} ranged={ranged} hop={hop}"); + let inherited = super::super::priority::to_wire(track.publisher_priority()); + for (bytes, mut expected) in sent.iter().zip(original) { + expected.track_alias = 3; + expected.publisher_priority = expected.publisher_priority.or(Some(inherited)); + let (actual, _) = ietf::ObjectDatagram::decode_slice(bytes, version).unwrap(); + assert_eq!(actual, expected); } - false => (origin, None), - }; - let downstream = ScriptedSession::per_stream(vec![vec![]]).with_datagrams(65535); - let peer = peer::PeerSetup::default(); - peer.set(peer::Peer::default()); - let publisher = crate::ietf::Publisher::new( - crate::time::sim(), - crate::driver::Transport::new(downstream.clone()), - served.consume(), - Control::new(false), - None, - peer, - version, - ); - let subscribe = ietf::Subscribe { - max_age: None, - new_group: None, - request_id: RequestId(3), - track_namespace: Path::new("room"), - track_name: "video".into(), - subscriber_priority: 128, - group_order: ietf::GroupOrder::Ascending, - filter: if ranged { - Filter::Absolute { - start: ietf::Location { - group: SEQUENCE, - object: 7, - }, - end: Some(ietf::EndLocation { - group: SEQUENCE, - object: Some(7), - }), - } - } else { - Filter::Unfiltered - }, - fill: None, - properties_wanted: true, - forward: true, - rendezvous: false, - range_filters: false, - authorization_token: None, - epoch: None, - }; - let mut body = Vec::new(); - subscribe - .encode_msg(&mut Encoder::new(&mut body, version.into()), version) - .unwrap(); - let stream = Stream::open(&mut crate::driver::Transport::new(downstream.clone()), version) - .await - .unwrap(); - publisher - .handle_stream(ietf::Subscribe::ID, ietf::Body(body.into()), stream, false) - .unwrap() - .await; - let sent = downstream.log.datagrams.lock().unwrap(); - original.retain(|datagram| !ranged || datagram.object_id == Some(7)); - assert_eq!(sent.len(), original.len(), "{version} ranged={ranged} hop={hop}"); - let inherited = super::super::priority::to_wire(track.publisher_priority()); - for (bytes, mut expected) in sent.iter().zip(original) { - expected.track_alias = 3; - expected.publisher_priority = expected.publisher_priority.or(Some(inherited)); - let (actual, _) = ietf::ObjectDatagram::decode_slice(bytes, version).unwrap(); - assert_eq!(actual, expected); - } } } @@ -12803,9 +12558,7 @@ mod stitch_tests { let mut encoder = Encoder::new(&mut properties, ingress.into()); encoder.varint(0x80u64).unwrap(); encoder.varint(96_000u64).unwrap(); - encoder - .varint(1u64) - .unwrap(); + encoder.varint(1u64).unwrap(); encoder.bytes(&[42; 130]).unwrap(); for egress in [Version::Draft22] { let script = object_stream(ingress, true, &[(0, &properties, None)]); @@ -12817,10 +12570,7 @@ mod stitch_tests { let mut props = Decoder::new(decoder.bytes().unwrap(), egress.into()); assert_eq!(props.varint().unwrap(), 0x80); assert_eq!(props.varint().unwrap(), 96_000); - assert_eq!( - props.varint().unwrap(), - 1 - ); + assert_eq!(props.varint().unwrap(), 1); assert_eq!(props.bytes().unwrap(), &[42; 130]); assert!(props.is_empty()); } @@ -13810,22 +13560,11 @@ mod stitch_tests { assert_eq!(frames[0].1, b"whole-0"); } - /// From draft-18 on, a later object's Group ID field is a delta: 7 then 8 is 0, not 8. - /// Draft-17 and earlier still send the absolute ID. + /// A later object's Group ID field is a delta: 7 then 8 is 0, not 8. #[test] fn a_later_fetch_group_field_is_a_delta() { - assert_eq!( - resolve_fetch_group(Some(7), Some(0)).unwrap(), - Some(8) - ); - assert_eq!( - resolve_fetch_group(Some(7), Some(0)).unwrap(), - Some(8) - ); - assert_eq!( - resolve_fetch_group(Some(7), Some(8)).unwrap(), - Some(8) - ); + assert_eq!(resolve_fetch_group(Some(7), Some(0)).unwrap(), Some(8)); + assert_eq!(resolve_fetch_group(Some(7), Some(1)).unwrap(), Some(9)); } /// A joining FETCH carries the one group its subscription joins, so a second group on @@ -14142,10 +13881,7 @@ mod joining_fetch_tests { }; use futures::FutureExt; - const JOINING_DRAFTS: [Version; 2] = [ - Version::Draft18, - Version::Draft19, - ]; + const JOINING_DRAFTS: [Version; 2] = [Version::Draft18, Version::Draft19]; async fn settle() { moq_clock::sleep(Duration::from_millis(1)).await; @@ -14452,10 +14188,7 @@ mod joining_fetch_tests { joining.poll(&kio::Waiter::noop()).is_ready(), "{version}: invalid joining open stayed alive" ); - assert_eq!( - subscriber.control.next_request_id(), - RequestId(1) - ); + assert_eq!(subscriber.control.next_request_id(), RequestId(1)); assert_eq!(log.bi_opens(), 0); } } @@ -14474,10 +14207,7 @@ mod joining_fetch_tests { drop(joining); *gate.write().ok().expect("gate is open") = true; let _ = tasks.poll(&kio::Waiter::noop()); - assert_eq!( - subscriber.control.next_request_id(), - RequestId(1) - ); + assert_eq!(subscriber.control.next_request_id(), RequestId(1)); assert_eq!(log.bi_opens(), 0, "{version}: the abandoned open is gone"); } } @@ -15721,10 +15451,7 @@ mod compression_negotiation_tests { compress: true, ..Default::default() }; - for version in [ - Version::Draft18, - Version::Draft22, - ] { + for version in [Version::Draft18, Version::Draft22] { assert!(matches!( refused(&properties, version, false), Some(Error::UnsupportedExtension) diff --git a/rs/moq-net/src/ietf/token.rs b/rs/moq-net/src/ietf/token.rs index 185a2ec1530..a787c155405 100644 --- a/rs/moq-net/src/ietf/token.rs +++ b/rs/moq-net/src/ietf/token.rs @@ -255,9 +255,7 @@ mod tests { kind: 300, value: vec![0x00, 0xff], }; - for (version, expected) in [ - (Version::Draft18, [0x03, 0x81, 0x2c, 0x00, 0xff]), - ] { + for (version, expected) in [(Version::Draft18, [0x03, 0x81, 0x2c, 0x00, 0xff])] { let mut params = Parameters::default(); into_setup(&mut params, &token, version).unwrap(); assert_eq!( diff --git a/rs/moq-net/src/lite/ietf.rs b/rs/moq-net/src/lite/ietf.rs index 40f3e62dc27..dbb705435ba 100644 --- a/rs/moq-net/src/lite/ietf.rs +++ b/rs/moq-net/src/lite/ietf.rs @@ -778,39 +778,6 @@ mod tests { } } - /// Dropping an understood property can widen the delta that spans it, so a list that - /// could not cross is refused where it arrives rather than where it would be sent. - #[test] - fn track_properties_that_cannot_cross_are_refused_at_ingress() { - let version = Draft::Draft18; - // The most a draft-16 answer holds beside a Request ID and an empty parameter count. - let size = super::super::message::MAX_MESSAGE_SIZE - 2; - // DYNAMIC_GROUPS, then the last ID whose delta from it fits four bytes. - let id = 0x30 + (1 << 30) - 1; - let mut list = Vec::new(); - let e = &mut Encoder::new(&mut list, version.into()); - for value in [0x30, 1, id - 0x30] { - e.varint(value).unwrap(); - } - let fill = size - list.len() - 4; - let e = &mut Encoder::new(&mut list, version.into()); - e.bytes(&vec![0; fill]).unwrap(); - assert_eq!(list.len(), size); - - // The delta from zero takes eight bytes once DYNAMIC_GROUPS leaves the list. - let ingress = crate::ietf::Properties::decode(&mut Decoder::new(&list, version.into()), version).unwrap(); - assert!(ingress.dynamic_groups); - let forwarded = crate::model::object::TrackProperties { - version, - values: ingress.unknown, - }; - assert!(!fits(&forwarded)); - assert!(matches!( - forwarded.refused(&crate::track::Info::default()), - Some(crate::Error::Unsupported) - )); - } - fn datagram(wire: &crate::model::datagram::Wire) -> Vec { let mut buf = Vec::new(); encode_datagram(&mut Encoder::new(&mut buf, V.into()), Some(wire)).unwrap(); @@ -821,8 +788,7 @@ mod tests { /// would close the session it reached: it is refused where it arrives. #[test] fn datagram_statuses_are_ones_moq_transport_defines() { - for (status, ok) in [(0, true), (1, true), (2, false), (3, true), (4, true), (5, false)] { - // Draft 14 sends a status datagram's Object ID and priority. + for (status, ok) in [(0, true), (1, false), (2, false), (3, true), (4, true), (5, false)] { let wire = crate::model::datagram::Wire { object_id: Some(3), priority: Some(9), diff --git a/rs/moq-net/src/server.rs b/rs/moq-net/src/server.rs index 5901bab5f98..c05cb624a3d 100644 --- a/rs/moq-net/src/server.rs +++ b/rs/moq-net/src/server.rs @@ -6,8 +6,8 @@ use crate::origin; use crate::time::{Deadlines, Instant}; use crate::transport::poll::Session as _; use crate::{ - ALPN_18, ALPN_19, ALPN_20, ALPN_21, ALPN_22, ALPN_LITE, ALPN_LITE_03, ALPN_LITE_04, ALPN_LITE_05, ALPN_LITE_06, ALPN_LITE_07_WIP, Consume, Error, NEGOTIATED, Session, SessionError, - Version, Versions, + ALPN_18, ALPN_19, ALPN_20, ALPN_21, ALPN_22, ALPN_LITE, ALPN_LITE_03, ALPN_LITE_04, ALPN_LITE_05, ALPN_LITE_06, + ALPN_LITE_07_WIP, Consume, Error, NEGOTIATED, Session, SessionError, Version, Versions, coding::{Encode, Stream}, ietf, lite, setup::{self, Role}, @@ -1226,7 +1226,9 @@ mod tests { async fn a_reply_the_peer_never_takes_fails_at_the_deadline() { let registry = stats::Registry::new(Default::default()); let version = Version::Lite(lite::Version::Lite02); - let session = FakeSession::new(ALPN_LITE, []).with_bi(moql_setup()).withholding_credit(); + let session = FakeSession::new(ALPN_LITE, []) + .with_bi(moql_setup()) + .withholding_credit(); let start = moq_clock::now(); let deadline = start + std::time::Duration::from_secs(10); let request = Server::new() @@ -1240,10 +1242,7 @@ mod tests { }; assert!(matches!(err, Error::Timeout), "{err:?}"); assert_eq!(moq_clock::now(), deadline); - assert_eq!( - session.close_code(), - Some(version.session_code(&SessionError::Timeout)) - ); + assert_eq!(session.close_code(), Some(version.session_code(&SessionError::Timeout))); let failed = registry.snapshot().sessions()[0].1.sessions_failed; assert_eq!(failed.count(stats::Failure::Application), 1, "{failed:?}"); } @@ -1254,7 +1253,9 @@ mod tests { async fn an_abandoned_reply_fails_like_the_deadline() { let registry = stats::Registry::new(Default::default()); let version = Version::Lite(lite::Version::Lite02); - let session = FakeSession::new(ALPN_LITE, []).with_bi(moql_setup()).withholding_credit(); + let session = FakeSession::new(ALPN_LITE, []) + .with_bi(moql_setup()) + .withholding_credit(); let request = Server::new() .accept_request(moq_clock::now(), session.clone()) .await @@ -1262,10 +1263,7 @@ mod tests { .with_stats(registry.tier(Default::default()).session("room")); let abandoned = moq_clock::timeout(std::time::Duration::from_secs(1), request.ok()).await; assert!(abandoned.is_err(), "a reply the peer never took completed"); - assert_eq!( - session.close_code(), - Some(version.session_code(&SessionError::Timeout)) - ); + assert_eq!(session.close_code(), Some(version.session_code(&SessionError::Timeout))); let failed = registry.snapshot().sessions()[0].1.sessions_failed; assert_eq!(failed.count(stats::Failure::Application), 1, "{failed:?}"); } @@ -1341,10 +1339,7 @@ mod tests { async fn accept_request_reads_ietf_path() { // Every draft gates on the SETUP stream before starting, so the // path is known at authorization time just like lite-05. - for (alpn, version) in [ - (ALPN_18, ietf::Version::Draft18), - (ALPN_19, ietf::Version::Draft19), - ] { + for (alpn, version) in [(ALPN_18, ietf::Version::Draft18), (ALPN_19, ietf::Version::Draft19)] { let session = FakeSession::new(alpn, [ietf_setup(version, Some("/team/room"))]); let request = Server::new().accept_request(moq_clock::now(), session).await.unwrap(); assert_eq!(request.path(), "/team/room", "{alpn}"); diff --git a/rs/moq-net/src/setup.rs b/rs/moq-net/src/setup.rs index bbc37ad9c2c..be10fe08ca4 100644 --- a/rs/moq-net/src/setup.rs +++ b/rs/moq-net/src/setup.rs @@ -8,7 +8,8 @@ use bytes::Bytes; use crate::{ Version, - coding::{self, Decode, DecodeError, Decoder, Encode, EncodeError, Encoder}, lite, + coding::{self, Decode, DecodeError, Decoder, Encode, EncodeError, Encoder}, + lite, }; /// The session direction a client advertises in its SETUP (moq-lite-05+). @@ -162,8 +163,8 @@ fn negotiated(v: Version) -> bool { /// The client half of the `moql` bidi SETUP exchange, offering moq-lite 01/02. /// -/// The framing is moq-transport draft-14's CLIENT_SETUP, which moq-lite 01/02 borrowed: -/// a u16 size, the offered versions, then parameters. +/// The framing is moq-transport draft-14's CLIENT_SETUP, which moq-lite 01/02 borrowed with a +/// varint size: the size, the offered versions, then parameters. #[derive(Debug, Clone)] pub(crate) struct Client { /// The list of supported versions in preferred order. @@ -184,8 +185,7 @@ impl Decode for Client { return Err(DecodeError::InvalidValue); } - let size = r.u16()? as usize; - let mut msg = r.sub(size)?; + let mut msg = decode_body(r)?; let versions = coding::Versions::decode(&mut msg, v).map_err(DecodeError::complete)?; Ok(Self { @@ -202,13 +202,25 @@ impl Encode for Client { return Err(EncodeError::Version); } w.u8(CLIENT_SETUP); - let prefix = w.prefix_u16(); + let prefix = w.prefix_varint(); self.versions.encode(w, v)?; w.slice(&self.parameters); fill_body(w, prefix) } } +/// Read a SETUP body's varint size, refusing one past [`MAX_SETUP_SIZE`] before buffering it. +fn decode_body<'a>(r: &mut Decoder<'a>) -> Result, DecodeError> { + let size = usize::try_from(r.varint()?).map_err(|_| DecodeError::BoundsExceeded)?; + if size > MAX_SETUP_SIZE { + return Err(DecodeError::MessageTooLarge { + size, + max: MAX_SETUP_SIZE, + }); + } + r.sub(size) +} + /// Size a SETUP body, refusing one our own receiver would refuse. fn fill_body(w: &mut Encoder<'_>, prefix: coding::Prefix) -> Result<(), EncodeError> { if w.since(&prefix) > MAX_SETUP_SIZE { @@ -220,8 +232,8 @@ fn fill_body(w: &mut Encoder<'_>, prefix: coding::Prefix) -> Result<(), EncodeEr /// The server half of the `moql` bidi SETUP exchange, selecting moq-lite 01 or 02. /// -/// The framing is moq-transport draft-14's SERVER_SETUP: a u16 size, the selected version, -/// then parameters. +/// The framing is moq-transport draft-14's SERVER_SETUP with a varint size: the size, the +/// selected version, then parameters. #[derive(Debug, Clone)] pub(crate) struct Server { /// The selected version. @@ -238,7 +250,7 @@ impl Encode for Server { return Err(EncodeError::Version); } w.u8(SERVER_SETUP); - let prefix = w.prefix_u16(); + let prefix = w.prefix_varint(); self.version.encode(w, v)?; w.slice(&self.parameters); fill_body(w, prefix) @@ -256,8 +268,7 @@ impl Decode for Server { return Err(DecodeError::InvalidValue); } - let size = r.u16()? as usize; - let mut msg = r.sub(size)?; + let mut msg = decode_body(r)?; let version = coding::Version::decode(&mut msg, v).map_err(DecodeError::complete)?; Ok(Self { diff --git a/rs/moq-net/src/test_interop.rs b/rs/moq-net/src/test_interop.rs index aa6bc1d546a..6cbe23b6773 100644 --- a/rs/moq-net/src/test_interop.rs +++ b/rs/moq-net/src/test_interop.rs @@ -260,7 +260,6 @@ fn ietf_datagram_interop() { ietf::Version::Draft22, ]; for (draft, version) in (18..).zip(all) { - let legacy = false; let mut properties = Vec::new(); let mut w = Encoder::new(&mut properties, version.into()); ietf::encode_object_time(&mut w, Timestamp::from_millis(1234).unwrap(), Timescale::MILLI).unwrap(); @@ -293,17 +292,6 @@ fn ietf_datagram_interop() { properties: None, body: ietf::DatagramBody::Payload(bytes::Bytes::new()), }); - if legacy { - datagrams.push(ietf::ObjectDatagram { - track_alias: 1, - group_id: 2, - object_id: Some(0), - publisher_priority: Some(0), - end_of_group: false, - properties: Some(properties.clone()), - body: ietf::DatagramBody::Status(3), - }); - } let encoded: Vec> = datagrams .iter() .map(|datagram| datagram.encode_bytes(version).unwrap().to_vec()) @@ -468,7 +456,11 @@ fn ietf_track_status_interop() { for (index, wanted) in [true, false].into_iter().enumerate() { let mut r = Decoder::new(&responses[index], version.into()); - assert_eq!(r.varint().unwrap(), ::ID, "{version}"); + assert_eq!( + r.varint().unwrap(), + ::ID, + "{version}" + ); let ok = ietf::TrackStatusOk::decode(&mut r, version).unwrap(); assert!(r.is_empty(), "{version}: trailing bytes"); diff --git a/rs/moq-net/tests/integration/compress.rs b/rs/moq-net/tests/integration/compress.rs index 2df16b3434f..4a5dbef041a 100644 --- a/rs/moq-net/tests/integration/compress.rs +++ b/rs/moq-net/tests/integration/compress.rs @@ -117,11 +117,7 @@ async fn relay(published: Published, hops: [&str; 2], subscription: moq_net::tra /// Hops that carry a start group and negotiate compression: lite-07, and moq-transport /// draft-17 and later. -const RANGED: &[&str] = &[ - "moq-lite-07-wip", - "moq-transport-18", - "moq-transport-22", -]; +const RANGED: &[&str] = &["moq-lite-07-wip", "moq-transport-18", "moq-transport-22"]; /// A subscription starting at a group delivers it whole from its first frame across every /// pair of hops, compressed or not, whether the group began before it subscribed or after. @@ -193,10 +189,7 @@ async fn relay_serves_every_version() { let mut fixture = connect("moq-lite-07-wip", version).await; assert_eq!( fixture.subscriber.info().compress, - !matches!( - version, - "moq-lite-06" | "moq-lite-03" - ), + !matches!(version, "moq-lite-06" | "moq-lite-03"), "{version}: compression declared" ); @@ -241,12 +234,7 @@ async fn relay_serves_every_version() { /// compressed or inflated. #[moq_clock::test] async fn datagrams_serve_every_version() { - for &version in &[ - "moq-lite-07-wip", - "moq-lite-06", - "moq-transport-18", - "moq-transport-22", - ] { + for &version in &["moq-lite-07-wip", "moq-lite-06", "moq-transport-18", "moq-transport-22"] { moq_clock::timeout(TIMEOUT, async { let mut fixture = connect("moq-lite-07-wip", version).await; fixture @@ -338,17 +326,10 @@ async fn datagrams_meter_the_bytes_on_the_wire() { #[moq_clock::test] async fn ietf_ingress_stays_compressed_across_protocols() { for upstream in ["moq-transport-18", "moq-transport-22"] { - for downstream in [ - "moq-lite-07-wip", - "moq-transport-22", - ] { + for downstream in ["moq-lite-07-wip", "moq-transport-22"] { moq_clock::timeout(TIMEOUT, async { let mut fixture = connect(upstream, downstream).await; - assert_eq!( - fixture.subscriber.info().compress, - true, - "{upstream} -> {downstream}" - ); + assert_eq!(fixture.subscriber.info().compress, true, "{upstream} -> {downstream}"); let mut group = fixture.track.append_group().unwrap(); for index in 0..3 { group @@ -389,7 +370,7 @@ async fn ietf_ingress_stays_compressed_across_protocols() { /// A finished compressed track remains fetchable without subscribing to its live feed. #[moq_clock::test] async fn ietf_fetch_keeps_the_stored_form() { - for version in [ "moq-transport-18", "moq-transport-22"] { + for version in ["moq-transport-18", "moq-transport-22"] { moq_clock::timeout(TIMEOUT, async { let publisher = produce_origin(1); let broadcast = publisher.create_broadcast("bcast").unwrap(); diff --git a/rs/moq-net/tests/integration/control_stats.rs b/rs/moq-net/tests/integration/control_stats.rs index ac1795fb18e..d9eb95c7a45 100644 --- a/rs/moq-net/tests/integration/control_stats.rs +++ b/rs/moq-net/tests/integration/control_stats.rs @@ -19,12 +19,7 @@ fn origin() -> origin::Producer { #[moq_clock::test] async fn control_classes_cross_lite_and_ietf_sessions_without_counting_payload() { - for version in [ - "moq-lite-03", - "moq-lite-06", - "moq-lite-07-wip", - "moq-transport-22", - ] { + for version in ["moq-lite-03", "moq-lite-06", "moq-lite-07-wip", "moq-transport-22"] { let publisher = origin(); let subscriber = origin(); let registry = Registry::new(Default::default()); diff --git a/rs/moq-net/tests/integration/datagram.rs b/rs/moq-net/tests/integration/datagram.rs index 2544bf07394..64d803906fd 100644 --- a/rs/moq-net/tests/integration/datagram.rs +++ b/rs/moq-net/tests/integration/datagram.rs @@ -124,10 +124,7 @@ async fn datagrams_reach_the_subscriber_in_order() { /// datagram with its sequence, alongside the groups on streams. #[moq_clock::test] async fn ietf_delivers_datagrams() { - for version in [ - "moq-transport-18", - "moq-transport-20", - ] { + for version in ["moq-transport-18", "moq-transport-20"] { moq_clock::timeout(TEST_TIMEOUT, ietf_delivers_datagrams_on(version)) .await .unwrap_or_else(|_| panic!("{version}: timed out")); @@ -159,11 +156,7 @@ async fn ietf_delivers_datagrams_on(version: &str) { let mut before = subscriber.recv_group().await.unwrap().unwrap(); assert_eq!(before.sequence, 0, "{version}"); let first = before.next_frame().await.unwrap().unwrap().timestamp; - assert_eq!( - first.map(|timestamp| timestamp.is_zero()), - Some(true), - "{version}" - ); + assert_eq!(first.map(|timestamp| timestamp.is_zero()), Some(true), "{version}"); producer .insert_datagram( diff --git a/rs/moq-net/tests/integration/empty_group.rs b/rs/moq-net/tests/integration/empty_group.rs index 10e9fefe93c..ab55a756ea1 100644 --- a/rs/moq-net/tests/integration/empty_group.rs +++ b/rs/moq-net/tests/integration/empty_group.rs @@ -18,11 +18,7 @@ fn timed() -> moq_net::track::Info { const TIMEOUT: Duration = Duration::from_secs(10); -const VERSIONS: &[&str] = &[ - "moq-lite-06", - "moq-transport-18", - "moq-transport-22", -]; +const VERSIONS: &[&str] = &["moq-lite-06", "moq-transport-18", "moq-transport-22"]; fn produce_origin(hop: u64) -> origin::Producer { let (producer, driver) = origin::Producer::new(origin::Config::new(Hop::new(hop).unwrap())); diff --git a/rs/moq-net/tests/integration/fetch_only.rs b/rs/moq-net/tests/integration/fetch_only.rs index 9f70c97e990..2df8a459218 100644 --- a/rs/moq-net/tests/integration/fetch_only.rs +++ b/rs/moq-net/tests/integration/fetch_only.rs @@ -90,11 +90,7 @@ fn frames(sequence: u64) -> Vec> { /// Draft-17 is absent: its TRACK_STATUS answer cannot say whether the track is timed, /// so it still SUBSCRIBEs to learn the track. -const VERSIONS: &[&str] = &[ - "moq-transport-18", - "moq-transport-19", - "moq-transport-22", -]; +const VERSIONS: &[&str] = &["moq-transport-18", "moq-transport-19", "moq-transport-22"]; async fn direct(version: &str) { let version: Version = version.parse().unwrap(); diff --git a/rs/moq-net/tests/integration/goaway.rs b/rs/moq-net/tests/integration/goaway.rs index 0d69667c91a..605c9d6c15a 100644 --- a/rs/moq-net/tests/integration/goaway.rs +++ b/rs/moq-net/tests/integration/goaway.rs @@ -32,12 +32,7 @@ const TEST_TIMEOUT: Duration = Duration::from_secs(10); /// Every version with GOAWAY support, across both wires and their distinct /// channels: the lite Goaway control stream, the IETF draft-14-16 shared /// control stream, and the IETF draft-17+ SETUP uni streams. -const GOAWAY_VERSIONS: &[&str] = &[ - "moq-lite-04", - "moq-lite-05", - "moq-transport-18", - "moq-transport-19", -]; +const GOAWAY_VERSIONS: &[&str] = &["moq-lite-04", "moq-lite-05", "moq-transport-18", "moq-transport-19"]; /// A full receive window or missing FIN ACK cannot hold a drain beyond its local deadline. async fn closes_stalled_goaway(version: Version, writes: bool) { diff --git a/rs/moq-net/tests/integration/idle_session.rs b/rs/moq-net/tests/integration/idle_session.rs index 342441eb920..770f5947c43 100644 --- a/rs/moq-net/tests/integration/idle_session.rs +++ b/rs/moq-net/tests/integration/idle_session.rs @@ -110,7 +110,7 @@ async fn busy_viewer_sees_the_estimate_move() { /// A transport estimate demanded locally still costs one poll per sampling deadline. #[moq_clock::test] async fn ietf_stats_refresh_costs_one_driver_poll() { - for version in [ "moq-transport-22"] { + for version in ["moq-transport-22"] { let polls = Arc::new(AtomicUsize::new(0)); let mut options = MockConnectOptions::new(version.parse::().unwrap()); options.stats = STATS; diff --git a/rs/moq-net/tests/integration/ietf_conformance.rs b/rs/moq-net/tests/integration/ietf_conformance.rs index fc66a14e1d5..5e170e45d8f 100644 --- a/rs/moq-net/tests/integration/ietf_conformance.rs +++ b/rs/moq-net/tests/integration/ietf_conformance.rs @@ -76,14 +76,14 @@ async fn routing(version: &str, cluster: bool) { #[moq_clock::test] async fn conformance_namespaces_route_through_one_relay() { - for version in [ "moq-transport-18"] { + for version in ["moq-transport-18"] { routing(version, false).await; } } #[moq_clock::test] async fn conformance_namespaces_route_through_a_lite_cluster() { - for version in [ "moq-transport-18"] { + for version in ["moq-transport-18"] { routing(version, true).await; } } diff --git a/rs/moq-net/tests/integration/late_lower_group.rs b/rs/moq-net/tests/integration/late_lower_group.rs index 06e8a1a2191..93f25a21042 100644 --- a/rs/moq-net/tests/integration/late_lower_group.rs +++ b/rs/moq-net/tests/integration/late_lower_group.rs @@ -25,7 +25,7 @@ const FOREVER: Duration = Duration::from_millis((1 << 53) - 1); const LITE: &[&str] = &["moq-lite-05", "moq-lite-06", "moq-lite-07-wip"]; -const TRANSPORT: &[&str] = &[ "moq-transport-18", "moq-transport-22"]; +const TRANSPORT: &[&str] = &["moq-transport-18", "moq-transport-22"]; /// From `start`, or the oldest group still around when there is none. fn floored(start: Option) -> Range { diff --git a/rs/moq-net/tests/integration/multi_cdn.rs b/rs/moq-net/tests/integration/multi_cdn.rs index c5c66d052ae..3c1de07ac30 100644 --- a/rs/moq-net/tests/integration/multi_cdn.rs +++ b/rs/moq-net/tests/integration/multi_cdn.rs @@ -357,4 +357,3 @@ async fn transport_secondary_is_refused() { async fn transport_17_secondary_is_refused() { refused_across_protocols("moq-transport-18").await; } - diff --git a/rs/moq-net/tests/integration/range_anchors.rs b/rs/moq-net/tests/integration/range_anchors.rs index f6494697596..b79ba931517 100644 --- a/rs/moq-net/tests/integration/range_anchors.rs +++ b/rs/moq-net/tests/integration/range_anchors.rs @@ -130,9 +130,7 @@ async fn each_wire_refuses_only_the_anchors_it_drops() { // A wire without timestamps refuses a time anchor for that first. let timed = !matches!( version, - Some( - "moq-lite-03" | "moq-lite-04" | "moq-lite-05" | "moq-lite-06" - ) + Some("moq-lite-03" | "moq-lite-04" | "moq-lite-05" | "moq-lite-06") ); for (range, held) in anchored() { let result = read(version, range).await; diff --git a/rs/moq-net/tests/integration/restart.rs b/rs/moq-net/tests/integration/restart.rs index 4efbcde258d..47b9c5d0b28 100644 --- a/rs/moq-net/tests/integration/restart.rs +++ b/rs/moq-net/tests/integration/restart.rs @@ -536,12 +536,7 @@ async fn retired_fetch_explicit_epoch_control() { #[moq_clock::test] async fn withdrawal_refuses_new_fetches_but_keeps_open_reads() { - for version in [ - "moq-lite-05", - "moq-lite-06", - "moq-lite-07-wip", - "moq-transport-22", - ] { + for version in ["moq-lite-05", "moq-lite-06", "moq-lite-07-wip", "moq-transport-22"] { let version: Version = version.parse().unwrap(); let publisher = produce_origin(1); let viewer = produce_origin(2); @@ -564,12 +559,7 @@ async fn withdrawal_refuses_new_fetches_but_keeps_open_reads() { #[moq_clock::test] async fn a_more_specific_source_cannot_fill_a_held_covering_track() { - for name in [ - "moq-lite-05", - "moq-lite-06", - "moq-lite-07-wip", - "moq-transport-22", - ] { + for name in ["moq-lite-05", "moq-lite-06", "moq-lite-07-wip", "moq-transport-22"] { let version: Version = name.parse().unwrap(); let publisher = produce_origin(1); let viewer = produce_origin(2); diff --git a/rs/moq-net/tests/integration/session_close.rs b/rs/moq-net/tests/integration/session_close.rs index 81c8992d07a..4fa6073a3a8 100644 --- a/rs/moq-net/tests/integration/session_close.rs +++ b/rs/moq-net/tests/integration/session_close.rs @@ -332,7 +332,7 @@ async fn earlier_lite_peer_close_reads_buffered_tail() { #[moq_clock::test] async fn ietf_peer_close_reads_buffered_tail() { - for version in [ "moq-transport-18", "moq-transport-22"] { + for version in ["moq-transport-18", "moq-transport-22"] { peer_close_reads_buffered_tail(version).await; } } diff --git a/rs/moq-net/tests/integration/subscribe_ranges.rs b/rs/moq-net/tests/integration/subscribe_ranges.rs index 4372bcc5ef1..71ea39d54d4 100644 --- a/rs/moq-net/tests/integration/subscribe_ranges.rs +++ b/rs/moq-net/tests/integration/subscribe_ranges.rs @@ -18,7 +18,7 @@ const LITE: &[&str] = &["moq-lite-05", "moq-lite-06", "moq-lite-07-wip"]; /// Draft-20 and later are left out: the first subscriber's absolute start goes upstream /// in the SUBSCRIBE filter, and the subscriber never widens it when a live one joins /// (`quest/m1/subscribe-ranges/ietf.md`), so group 3 never arrives. -const TRANSPORT: &[&str] = &[ "moq-transport-18"]; +const TRANSPORT: &[&str] = &["moq-transport-18"]; fn produce_origin(hop: u64) -> moq_net::origin::Producer { let (producer, driver) = diff --git a/rs/moq-net/tests/integration/subscribe_tracks.rs b/rs/moq-net/tests/integration/subscribe_tracks.rs index c22fe76cf85..cecaeb6e14f 100644 --- a/rs/moq-net/tests/integration/subscribe_tracks.rs +++ b/rs/moq-net/tests/integration/subscribe_tracks.rs @@ -169,21 +169,3 @@ async fn subscribe_tracks_needs_forward_0_on_draft_18() { deliver(&mut setup, b"after").await; } - -/// Before draft-18, 0x51 is not a message at all, so it is still a protocol violation. -#[moq_clock::test] -async fn subscribe_tracks_before_draft_18_closes_the_session() { - let setup = setup("moq-transport-18".parse().unwrap()).await; - let (mut send, _recv) = setup - .pair - .server_transport - .clone() - .open_bi(Default::default()) - .await - .expect("open_bi"); - send.write(SUBSCRIBE_TRACKS).await.expect("write"); - - moq_clock::timeout(TIMEOUT, setup.pair.client.closed()) - .await - .expect("the session stayed open"); -} diff --git a/rs/moq-net/tests/integration/subscriber_max_age.rs b/rs/moq-net/tests/integration/subscriber_max_age.rs index 1b53354830f..3c6d77acaa8 100644 --- a/rs/moq-net/tests/integration/subscriber_max_age.rs +++ b/rs/moq-net/tests/integration/subscriber_max_age.rs @@ -32,7 +32,7 @@ async fn budget(track: &mut track::Producer, millis: u64, version: Version) { #[moq_clock::test] async fn relay_forwards_the_most_tolerant_subscriber_max_age() { - for draft in 14..=22 { + for draft in 18..=22 { let version: Version = format!("moq-transport-{draft}").parse().unwrap(); let publisher = origin(1); let relay = origin(2); diff --git a/rs/moq-net/tests/integration/track_tail.rs b/rs/moq-net/tests/integration/track_tail.rs index ac30c90045e..5f02b93975a 100644 --- a/rs/moq-net/tests/integration/track_tail.rs +++ b/rs/moq-net/tests/integration/track_tail.rs @@ -259,11 +259,7 @@ async fn lite07_empty_track_ends_without_the_grace() { /// IETF drafts over the control stream adapter (14), on their own streams (17), and with /// subscription fills (20+). -const IETF: &[&str] = &[ - "moq-transport-18", - "moq-transport-20", - "moq-transport-22", -]; +const IETF: &[&str] = &["moq-transport-18", "moq-transport-20", "moq-transport-22"]; /// A subscriber that leaves while END_OF_TRACK waits for stream credit ends the publisher's /// request, instead of parking it until credit that may never come. diff --git a/rs/moq-net/tests/integration/tracks_ietf.rs b/rs/moq-net/tests/integration/tracks_ietf.rs index 618130a195c..9578900dfd8 100644 --- a/rs/moq-net/tests/integration/tracks_ietf.rs +++ b/rs/moq-net/tests/integration/tracks_ietf.rs @@ -184,37 +184,6 @@ async fn a_listing_carries_track_info() { assert_eq!(info.priority, 9); } -/// Before draft-18 there is no SUBSCRIBE_TRACKS, so the listing is refused rather than -/// answered empty, and a reader still subscribes. -#[moq_clock::test] -async fn before_draft_18_the_listing_is_refused() { - let publisher = produce_origin(1); - let broadcast = publisher.create_broadcast("bcast").unwrap(); - let a = broadcast.create_track("a", None).unwrap(); - broadcast.announce(Default::default()).unwrap(); - - let (subscriber, _pair) = connect(&publisher, "moq-transport-18").await; - let remote = resolve(&subscriber, "bcast").await; - let mut tracks = remote.tracks().unwrap(); - assert_eq!( - next(&mut tracks).await, - format!("error: {}", moq_net::Error::Unsupported) - ); - - let mut sub = moq_clock::timeout(TIMEOUT, remote.track("a").unwrap().subscribe(None)) - .await - .expect("subscribe timeout") - .expect("subscribe"); - moq_clock::timeout(TIMEOUT, support::wanted(a.demand())) - .await - .expect("never subscribed") - .unwrap(); - let mut group = a.append_group().unwrap(); - group.write_frame(None, &b"frame"[..]).unwrap(); - group.finish().unwrap(); - assert_eq!(read(&mut sub).await, Some((0, b"frame".to_vec()))); -} - /// A reader of a listed track resumes its held PUBLISH, so its data arrives with no /// SUBSCRIBE at all; draft-18 would refuse one as a second subscription to the track. /// Once the reader leaves, the PUBLISH pauses and stays listed, and the next reader diff --git a/rs/moq-net/tests/integration/untimed.rs b/rs/moq-net/tests/integration/untimed.rs index ea408df516d..442e81595dc 100644 --- a/rs/moq-net/tests/integration/untimed.rs +++ b/rs/moq-net/tests/integration/untimed.rs @@ -152,23 +152,18 @@ async fn a_relay_forwards_an_untimed_track_untimed() { assert_eq!(info.timescale, None, "{hops:?}: the relay must not claim a timeline"); } - for hops in [ - &["moq-lite-03", "moq-transport-18"][..], - &[ "moq-transport-20"][..], - ] { - let (frame, info) = frame_through(hops, timed()).await; - assert_eq!(frame, None, "{hops:?}: the first hop has no timestamps to forward"); - assert_eq!(info.timescale, None, "{hops:?}: the relay must not claim a timeline"); - } + let hops = ["moq-lite-03", "moq-transport-18"]; + let (frame, info) = frame_through(&hops, timed()).await; + assert_eq!(frame, None, "{hops:?}: the first hop has no timestamps to forward"); + assert_eq!(info.timescale, None, "{hops:?}: the relay must not claim a timeline"); } /// An explicit start on an untimed track is honored the same way on lite and /// moq-transport: only an unfloored subscriber jumps to the latest group. /// /// The subscription asks for history the way a resume does, with a budget covering it. -/// moq-transport before draft -/// 20 can only join a past group through a joining FETCH, which our publisher refuses for -/// groups before the live one, so draft 20 stands in for it. +/// moq-transport before draft 20 can only join a past group through a joining FETCH, +/// which our publisher refuses for groups before the live one, so draft 20 stands in for it. #[moq_clock::test] async fn an_explicit_start_holds_on_an_untimed_track() { for version in ["moq-lite-03", "moq-lite-06", "moq-lite-07-wip", "moq-transport-20"] { diff --git a/rs/moq-quic/src/connection/mux/params.rs b/rs/moq-quic/src/connection/mux/params.rs index 5adef1eecd1..fba68ff809d 100644 --- a/rs/moq-quic/src/connection/mux/params.rs +++ b/rs/moq-quic/src/connection/mux/params.rs @@ -203,7 +203,7 @@ mod tests { let params = Params { tp, max_record_size: VarInt::from_u32(32_768), - protocols: Some(vec!["moq-lite-03".into(), "moqt-16".into()]), + protocols: Some(vec!["moq-lite-03".into(), "moqt-18".into()]), }; let mut buf = Vec::new(); params.encode(&mut buf); diff --git a/rs/moq-relay/src/cluster.rs b/rs/moq-relay/src/cluster.rs index 58e3f6d69e3..ac6a924ffd0 100644 --- a/rs/moq-relay/src/cluster.rs +++ b/rs/moq-relay/src/cluster.rs @@ -989,9 +989,9 @@ struct LanAuth { #[derive(Default)] #[non_exhaustive] pub struct Options { - /// Request caps for ordinary client sessions. + /// Session limits for ordinary client sessions. pub session: crate::limits::Client, - /// Request caps and QUIC limits for authenticated peers and outbound cluster links. + /// Session and QUIC limits for authenticated peers and outbound cluster links. pub peer: crate::limits::Peer, /// Cluster identity, peers, and discovery. pub config: Config, @@ -2320,14 +2320,12 @@ mod tests { use crate::Config as RelayConfig; #[tokio::test] - async fn request_limits_follow_the_admitted_grant() { + async fn session_limits_follow_the_admitted_grant() { let mut options = Options::default(); options.session.announces = 2; - options.session.requests = 3; options.peer.announces = 20; - options.peer.requests = 30; let cluster = Cluster::new(options).unwrap(); - for (peer, upstream, expected) in [(false, false, (2, 3)), (true, false, (20, 30)), (true, true, (20, 30))] { + for (peer, upstream, expected) in [(false, false, 2), (true, false, 20), (true, true, 20)] { let mut grant = moq_auth::Grant::new(moq_auth::Patterns::new(), moq_auth::Patterns::new()); grant.peer = peer; grant.upstream = upstream; @@ -2335,7 +2333,7 @@ mod tests { // A cluster-looking path alone never selects the peer policy. let lease = auth::Lease::new("/.cluster/untrusted", consumer); let limits = cluster.limits(&lease); - assert_eq!((limits.announces, limits.requests), expected); + assert_eq!(limits.announces, expected); } } diff --git a/rs/moq-relay/src/config.rs b/rs/moq-relay/src/config.rs index e39bec088fb..ec7854f8634 100644 --- a/rs/moq-relay/src/config.rs +++ b/rs/moq-relay/src/config.rs @@ -530,39 +530,39 @@ mod tests { #[test] fn session_limits_merge_without_crossing_peer_policy() { - if !test_env::Env::without(&[ - "MOQ_LIMITS_CLIENT_ANNOUNCES", - "MOQ_LIMITS_CLIENT_REQUESTS", - "MOQ_LIMITS_PEER_ANNOUNCES", - "MOQ_LIMITS_PEER_REQUESTS", - ]) - .isolate() - { + if !test_env::Env::without(&["MOQ_LIMITS_CLIENT_ANNOUNCES", "MOQ_LIMITS_PEER_ANNOUNCES"]).isolate() { return; } let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("limits.toml"); - std::fs::write( - &path, - "[limits.client]\nannounces = 7\nrequests = 8\n[limits.peer]\nannounces = 90\nrequests = 91\n", - ) - .unwrap(); + std::fs::write(&path, "[limits.client]\nannounces = 7\n[limits.peer]\nannounces = 90\n").unwrap(); let config = Config::parse_and_merge([std::ffi::OsString::from("moq-relay"), path.clone().into()]).unwrap(); assert_eq!(config.limits.client.resolve().announces, 7); - assert_eq!(config.limits.client.resolve().requests, 8); assert_eq!(config.limits.peer.resolve().announces, 90); - assert_eq!(config.limits.peer.resolve().requests, 91); let config = Config::parse_and_merge([ std::ffi::OsString::from("moq-relay"), path.into(), "--limits-client-announces=0".into(), - "--limits-peer-requests=1000".into(), ]) .unwrap(); assert_eq!(config.limits.client.resolve().announces, 0); - assert_eq!(config.limits.client.resolve().requests, 8); assert_eq!(config.limits.peer.resolve().announces, 90); - assert_eq!(config.limits.peer.resolve().requests, 1000); + } + + /// Sizing the removed request window refuses to start rather than run without the + /// setting. + #[test] + fn removed_request_limits_are_refused() { + if !test_env::Env::without(&["MOQ_LIMITS_CLIENT_REQUESTS", "MOQ_LIMITS_PEER_REQUESTS"]).isolate() { + return; + } + let error = Config::parse_and_merge(["moq-relay", "--limits-peer-requests=1000"]).unwrap_err(); + assert!(error.to_string().contains("--limits-peer-requests"), "{error}"); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("limits.toml"); + std::fs::write(&path, "[limits.client]\nrequests = 8\n").unwrap(); + assert!(Config::parse_and_merge([std::ffi::OsString::from("moq-relay"), path.into()]).is_err()); } /// `[limits.peer.quic]` mirrors `[quic]`'s keys without touching `[quic]` itself. diff --git a/rs/moq-relay/src/limits.rs b/rs/moq-relay/src/limits.rs index 15634ecc581..b95c8fa9570 100644 --- a/rs/moq-relay/src/limits.rs +++ b/rs/moq-relay/src/limits.rs @@ -32,28 +32,17 @@ pub struct Client { setting = "limits.client.announces" )] pub announces: usize, - /// Maximum other simultaneously held moq-transport requests, sizing the drafts 14 to 16 request window. - #[usage( - long = "limits-client-requests", - env = "MOQ_LIMITS_CLIENT_REQUESTS", - default = "10000", - setting = "limits.client.requests" - )] - pub requests: usize, } impl Default for Client { fn default() -> Self { - Self { - announces: 1000, - requests: 10000, - } + Self { announces: 1000 } } } impl Client { pub(crate) fn resolve(&self) -> moq_net::session::Limits { - resolve(self.announces, self.requests) + resolve(self.announces) } } @@ -70,14 +59,6 @@ pub struct Peer { setting = "limits.peer.announces" )] pub announces: usize, - /// Maximum other simultaneously held moq-transport requests, sizing the drafts 14 to 16 request window. - #[usage( - long = "limits-peer-requests", - env = "MOQ_LIMITS_PEER_REQUESTS", - default = "20000", - setting = "limits.peer.requests" - )] - pub requests: usize, /// QUIC limits a peer session widens to (`[limits.peer.quic]`). #[usage(flatten)] pub quic: Quic, @@ -87,7 +68,6 @@ impl Default for Peer { fn default() -> Self { Self { announces: 100000, - requests: 20000, quic: Quic::default(), } } @@ -95,7 +75,7 @@ impl Default for Peer { impl Peer { pub(crate) fn resolve(&self) -> moq_net::session::Limits { - resolve(self.announces, self.requests) + resolve(self.announces) } } @@ -170,10 +150,9 @@ impl Quic { } } -fn resolve(announces: usize, requests: usize) -> moq_net::session::Limits { +fn resolve(announces: usize) -> moq_net::session::Limits { let mut limits = moq_net::session::Limits::default(); limits.announces = announces; - limits.requests = requests; limits } @@ -189,6 +168,10 @@ pub(crate) struct Legacy { peer_quic_receive_window: Option, #[usage(long = "peer-quic-send-window", env = "MOQ_PEER_QUIC_SEND_WINDOW", hide = true)] peer_quic_send_window: Option, + #[usage(long = "limits-client-requests", env = "MOQ_LIMITS_CLIENT_REQUESTS", hide = true)] + client_requests: Option, + #[usage(long = "limits-peer-requests", env = "MOQ_LIMITS_PEER_REQUESTS", hide = true)] + peer_requests: Option, } impl Legacy { @@ -229,6 +212,14 @@ impl Legacy { "--limits-peer-quic-send-window", ); } + const REQUESTS: &str = + "the request window left with moq-transport drafts 14 to 16; stream flow control bounds requests"; + if self.client_requests.is_some() { + found.removed("--limits-client-requests", Some("MOQ_LIMITS_CLIENT_REQUESTS"), REQUESTS); + } + if self.peer_requests.is_some() { + found.removed("--limits-peer-requests", Some("MOQ_LIMITS_PEER_REQUESTS"), REQUESTS); + } found } } diff --git a/rs/moq-relay/src/settings.rs b/rs/moq-relay/src/settings.rs index 52622b36b72..35fed361a8e 100644 --- a/rs/moq-relay/src/settings.rs +++ b/rs/moq-relay/src/settings.rs @@ -268,8 +268,6 @@ struct Limits { struct Client { #[usage(env = "MOQ_LIMITS_CLIENT_ANNOUNCES", cli("--limits-client-announces"))] announces: Option, - #[usage(env = "MOQ_LIMITS_CLIENT_REQUESTS", cli("--limits-client-requests"))] - requests: Option, } #[derive(usage_rs::Config)] @@ -277,8 +275,6 @@ struct Client { struct Peer { #[usage(env = "MOQ_LIMITS_PEER_ANNOUNCES", cli("--limits-peer-announces"))] announces: Option, - #[usage(env = "MOQ_LIMITS_PEER_REQUESTS", cli("--limits-peer-requests"))] - requests: Option, #[usage(flatten)] quic: PeerQuic, } diff --git a/rs/moq-relay/src/websocket.rs b/rs/moq-relay/src/websocket.rs index d5d953a91c5..d3de9298dec 100644 --- a/rs/moq-relay/src/websocket.rs +++ b/rs/moq-relay/src/websocket.rs @@ -439,21 +439,21 @@ mod tests { #[test] fn supported_subprotocols_only_lists_configured_alpns() { - let list = supported_subprotocols(&["moqt-16"]); + let list = supported_subprotocols(&["moqt-20"]); - assert!(list.contains(&"qmux-01.moqt-16".to_string())); - assert!(list.contains(&"qmux-00.moqt-16".to_string())); + assert!(list.contains(&"qmux-01.moqt-20".to_string())); + assert!(!list.contains(&"qmux-00.moqt-20".to_string())); assert!(list.iter().all(|entry| !entry.contains("moq-lite"))); assert!(list.iter().all(|entry| !entry.contains("moqt-19"))); } #[test] fn supported_subprotocols_preserves_moq_preference_across_qmux_versions() { - let list = supported_subprotocols(&["moqt-16", "moqt-18"]); + let list = supported_subprotocols(&["moq-lite-06", "moqt-18"]); let preferred = list .iter() - .position(|entry| entry == "qmux-00.moqt-16") - .expect("missing preferred moqt-16 pair"); + .position(|entry| entry == "qmux-00.moq-lite-06") + .expect("missing preferred moq-lite-06 pair"); let newer_qmux = list .iter() .position(|entry| entry == "qmux-01.moqt-18") diff --git a/rs/moq-relay/tests/integration/auth_lifetime.rs b/rs/moq-relay/tests/integration/auth_lifetime.rs index 44e67f7958a..968ba80c905 100644 --- a/rs/moq-relay/tests/integration/auth_lifetime.rs +++ b/rs/moq-relay/tests/integration/auth_lifetime.rs @@ -442,34 +442,38 @@ async fn forwards_the_setup_token() { spawn_relay(auth).await }; - // Draft-16 CLIENT_SETUP: AUTHORIZATION TOKEN (3), USE_VALUE (3), Token Type 0. + // Draft-18 SETUP (0x2F00): AUTHORIZATION TOKEN (3), USE_VALUE (3), Token Type 0. let encoded = [&[0x03, 0x00][..], &value].concat(); let params = [&[0x01, 0x03, encoded.len() as u8][..], &encoded].concat(); - let setup = [&[0x20][..], &(params.len() as u16).to_be_bytes(), ¶ms].concat(); + let setup = [&[0x6f, 0x00][..], &(params.len() as u16).to_be_bytes(), ¶ms].concat(); let mut session = if websocket { qmux::ws::Client::new() - .with_protocol("moqt-16", &[qmux::Version::QMux01]) + .with_protocol("moqt-18", &[qmux::Version::QMux01]) .connect(&format!("ws://127.0.0.1:{port}/room?source=setup")) .await .expect("connect WebSocket") } else { qmux::tcp::connect(("127.0.0.1", port), { let mut config = qmux::Config::new(qmux::Version::QMux01); - config.protocol = qmux::Protocol::Negotiate(vec!["moqt-16".into()]); + config.protocol = qmux::Protocol::Negotiate(vec!["moqt-18".into()]); config }) .await .expect("connect TCP") }; - let (mut send, mut recv) = session - .open_bi(Default::default()) + let mut send = session + .open_uni(Default::default()) .await - .expect("open the control stream"); + .expect("open the setup stream"); write_all(&mut send, &setup).await; + let mut recv = tokio::time::timeout(TIMEOUT, session.accept_uni()) + .await + .expect("SETUP timeout") + .expect("the server's setup stream"); let reply = tokio::time::timeout(TIMEOUT, recv.read_chunk(1)) .await - .expect("SERVER_SETUP timeout") - .expect("SERVER_SETUP"); + .expect("SETUP timeout") + .expect("SETUP"); assert_eq!(reply.map(|chunk| chunk.len()), Some(1)); let seen = script.seen.lock().unwrap().clone(); let connect = seen.iter().find(|r| r.event == Event::Connect).expect("a connect"); @@ -481,7 +485,7 @@ async fn forwards_the_setup_token() { // Plain `ws://` has no SNI; the `Host` header never stands in for one. assert_eq!(connect.server_name, None); assert!(connect.remote.is_some_and(|addr| addr.ip().is_loopback())); - assert_eq!(connect.alpn.as_deref(), Some("qmux-01.moqt-16")); + assert_eq!(connect.alpn.as_deref(), Some("qmux-01.moqt-18")); } relay.abort(); } diff --git a/rs/moq-relay/tests/integration/cluster_unknown.rs b/rs/moq-relay/tests/integration/cluster_unknown.rs index 267bc3471a8..8ea2e02fbfb 100644 --- a/rs/moq-relay/tests/integration/cluster_unknown.rs +++ b/rs/moq-relay/tests/integration/cluster_unknown.rs @@ -186,9 +186,9 @@ async fn publish_version(port: u16, version: &str) -> Publisher { } async fn publish_unknown(port: u16) -> Publisher { - // Draft-14 has no Cluster extension, so the accepting relay must represent + // Lite-03 declares no origin identity, so the accepting relay must represent // this external publisher with the wire-defined UNKNOWN Hop ID. - publish_version(port, "moq-transport-14").await + publish_version(port, "moq-lite-03").await } /// Subscribe to [`PATH`] through the relay on `port` and read its first frame. @@ -305,7 +305,7 @@ async fn assert_unknown_publisher_stays_announced(cluster_version: Option tokio::t /// only after its lease recorded the shutdown. #[tokio::test(start_paused = true)] async fn a_session_ignoring_the_goaway_is_closed_at_the_deadline() { - for alpn in ["moq-lite-06", "moq-lite-03", "moq-transport-17"] { + for alpn in ["moq-lite-06", "moq-lite-03", "moq-transport-18"] { let (trigger, observer) = shutdown::Observer::new(WINDOW); let session = admit(&observer, alpn.parse().unwrap(), false).await; let started = tokio::time::Instant::now(); diff --git a/rs/moq-relay/tests/integration/hidden_cluster.rs b/rs/moq-relay/tests/integration/hidden_cluster.rs index f9e4ef7cd24..2a6ecad6a69 100644 --- a/rs/moq-relay/tests/integration/hidden_cluster.rs +++ b/rs/moq-relay/tests/integration/hidden_cluster.rs @@ -41,7 +41,7 @@ async fn listen_tcp(version: moq_net::Version) -> (u16, moq_tokio::Listener) { #[test] fn old_peers_keep_hidden_paths() { - for version in ["moq-lite-05", "moq-lite-06", "moq-transport-17"] { + for version in ["moq-lite-05", "moq-lite-06", "moq-transport-18"] { run_cluster_test(old_peer_keeps_hidden_paths(version.parse().unwrap())); } } diff --git a/rs/moq-relay/tests/integration/runtime_uring.rs b/rs/moq-relay/tests/integration/runtime_uring.rs index f586b950e30..f949ba91619 100644 --- a/rs/moq-relay/tests/integration/runtime_uring.rs +++ b/rs/moq-relay/tests/integration/runtime_uring.rs @@ -248,14 +248,9 @@ async fn uring_workers_serve_every_configured_version() { let publisher = connect(client.with_publisher(&origin), raw.parse().unwrap()).await; assert_eq!(publisher.version(), Some(version), "publisher version"); - // moq-transport-14 and moq-lite-02 negotiate in the SETUP; 16 has its own - // ALPN but still a bidi SETUP; 22 reads its SETUP off a uni stream. - for name in [ - "moq-transport-22", - "moq-transport-16", - "moq-transport-14", - "moq-lite-02", - ] { + // moq-lite-02 negotiates in the SETUP; moq-transport-22 reads its SETUP off a + // uni stream. + for name in ["moq-transport-22", "moq-lite-02"] { for scheme in ["moql", "https"] { let url: url::Url = format!("{scheme}://127.0.0.1:{port}/versions").parse().unwrap(); let (version, client) = pinned(name); @@ -364,7 +359,7 @@ async fn uring_workers_report_link_facts() { /// threads joined and the port free. /// /// What an arrival is told is left of the window only reaches the wire on -/// moq-transport-17+, which this test does not dial; `shutdown_signal.rs` +/// moq-transport, which this test does not dial; `shutdown_signal.rs` /// reads it through the shared runtime, whose supervision this path shares. #[tokio::test] async fn uring_workers_drain_on_the_trigger() { @@ -881,7 +876,7 @@ async fn uring_forwards_the_setup_token() { tls.alpn_protocols = vec![if webtransport { b"h3".to_vec() } else { - b"moqt-16".to_vec() + b"moqt-18".to_vec() }]; let crypto = driver::crypto::rustls::QuicClientConfig::try_from(tls).unwrap(); let conn = client @@ -895,17 +890,21 @@ async fn uring_forwards_the_setup_token() { .unwrap(); let session = if webtransport { let url: url::Url = format!("https://{addr}/room?source=setup").parse().unwrap(); - let request = web::proto::ConnectRequest::new(url).with_protocol("moqt-16".to_string()); + let request = web::proto::ConnectRequest::new(url).with_protocol("moqt-18".to_string()); web::Session::connect(conn, request).await.unwrap() } else { web::Session::raw(conn) }; - // Draft-16 CLIENT_SETUP: AUTHORIZATION TOKEN (3), USE_VALUE (3), Token Type 0. + // Draft-18 SETUP (0x2F00): AUTHORIZATION TOKEN (3), USE_VALUE (3), Token Type 0. let encoded = [&[0x03, 0x00][..], &token.value].concat(); let params = [&[0x01, 0x03, encoded.len() as u8][..], &encoded].concat(); - let setup = [&[0x20][..], &(params.len() as u16).to_be_bytes(), ¶ms].concat(); - let (mut send, mut recv) = session.open_bi(Default::default()).await.unwrap(); + let setup = [&[0x6f, 0x00][..], &(params.len() as u16).to_be_bytes(), ¶ms].concat(); + let mut send = session.open_uni().await.unwrap(); send.write_all(&setup).await.unwrap(); + let mut recv = tokio::time::timeout(TIMEOUT, session.accept_uni()) + .await + .unwrap() + .unwrap(); let mut reply = [0u8; 1]; assert_eq!( tokio::time::timeout(TIMEOUT, recv.read(&mut reply)) @@ -913,7 +912,7 @@ async fn uring_forwards_the_setup_token() { .unwrap() .unwrap(), Some(1), - "SERVER_SETUP" + "SETUP" ); } deciding.await.unwrap(); diff --git a/rs/moq-relay/tests/integration/shutdown_signal.rs b/rs/moq-relay/tests/integration/shutdown_signal.rs index ee214e825cf..e499983df65 100644 --- a/rs/moq-relay/tests/integration/shutdown_signal.rs +++ b/rs/moq-relay/tests/integration/shutdown_signal.rs @@ -158,7 +158,7 @@ async fn sigint_drains_sessions_before_exiting_inner() { .expect("no GOAWAY within 5s of SIGINT") .expect("session closed without a GOAWAY"); // Empty URI: the relay is restarting, not moving. The window itself is the - // sender's own timer here (only moq-transport draft-17+ puts it on the wire), + // sender's own timer here (only moq-transport puts it on the wire), // so it is the elapsed time below that proves it was honored. assert_eq!(goaway.uri(), "", "expected a reconnect-to-me GOAWAY"); @@ -292,9 +292,9 @@ async fn a_session_arriving_mid_drain_gets_what_is_left_inner() { let port = relay.tcp_addr().expect("TCP listener bound").port(); let trigger = relay.shutdown_trigger().clone(); let run = tokio::spawn(relay.run()); - // moq-transport-17 is the first version whose GOAWAY carries its deadline on - // the wire, which is what this test reads. - let client = client(vec!["moq-transport-17".parse().expect("parse version")]); + // moq-transport's GOAWAY carries its deadline on the wire, which is what this + // test reads. + let client = client(vec!["moq-transport-18".parse().expect("parse version")]); let established = connect(&client, port).await; let draining = established.draining().expect("connected"); diff --git a/rs/moq-relay/tests/integration/smoke.rs b/rs/moq-relay/tests/integration/smoke.rs index 4dc925b4f72..24fd78bcd18 100644 --- a/rs/moq-relay/tests/integration/smoke.rs +++ b/rs/moq-relay/tests/integration/smoke.rs @@ -373,7 +373,7 @@ async fn hidden_broadcasts_need_a_lite07_opt_in() { /// `--server-version` applies to the WebSocket fallback as well as QUIC. #[tokio::test] async fn relay_websocket_honors_server_version() { - let allowed: moq_net::Version = "moq-transport-16".parse().expect("parse allowed version"); + let allowed: moq_net::Version = "moq-transport-18".parse().expect("parse allowed version"); let excluded = newest_lite_version(); let (port, web_handle) = spawn_versioned_relay(vec![allowed]).await; let url: url::Url = format!("ws://127.0.0.1:{port}/smoke").parse().expect("parse url"); @@ -792,18 +792,10 @@ async fn internal_unix_round_trip() { /// (Setup Stream) and moq-transport 14-18 (the `Path` SETUP parameter, in-band on /// the bidi stream for 14-16 and the uni Setup Stream for 17-18). fn path_versions() -> Vec { - [ - "moq-lite-05", - "moq-lite-06", - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", - "moq-transport-18", - ] - .iter() - .map(|alpn| alpn.parse().expect("parse version alpn")) - .collect() + ["moq-lite-05", "moq-lite-06", "moq-transport-18"] + .iter() + .map(|alpn| alpn.parse().expect("parse version alpn")) + .collect() } /// Publisher and subscriber (both pinned to `version`) that announce/observe diff --git a/rs/moq-tokio/src/connect.rs b/rs/moq-tokio/src/connect.rs index 4cf1c56ed4e..ed621eed3e3 100644 --- a/rs/moq-tokio/src/connect.rs +++ b/rs/moq-tokio/src/connect.rs @@ -206,10 +206,6 @@ pub(crate) struct Legacy { "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", "moq-transport-18", "moq-transport-19", "moq-transport-20", @@ -598,10 +594,6 @@ pub struct Config { "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", "moq-transport-18", "moq-transport-19", "moq-transport-20", diff --git a/rs/moq-tokio/src/deprecated.rs b/rs/moq-tokio/src/deprecated.rs index 16ed82488d5..4a245c8093b 100644 --- a/rs/moq-tokio/src/deprecated.rs +++ b/rs/moq-tokio/src/deprecated.rs @@ -46,6 +46,12 @@ impl Deprecated { self.entry(&Self::spelling(old, env), new, Some(note)); } + /// Record a released flag that was removed without a replacement, with a note saying + /// what covers it now. + pub fn removed(&mut self, old: &str, env: Option<&str>, note: &str) { + self.entry(&Self::spelling(old, env), "removed", Some(note)); + } + /// Record a released config-file key or table, which has no environment variable. pub fn toml(&mut self, old: &str, new: &str, note: Option<&str>) { self.entry(old, new, note); @@ -70,7 +76,7 @@ impl std::fmt::Display for Deprecated { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "these settings were renamed and are no longer applied; update them and try again:" + "these settings were renamed or removed and are no longer applied; update them and try again:" )?; for entry in &self.0 { write!(f, "\n {entry}")?; diff --git a/rs/moq-tokio/src/listen.rs b/rs/moq-tokio/src/listen.rs index f1fb920d62d..30305568c20 100644 --- a/rs/moq-tokio/src/listen.rs +++ b/rs/moq-tokio/src/listen.rs @@ -133,10 +133,6 @@ pub struct Config { "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", "moq-transport-18", "moq-transport-19", "moq-transport-20", @@ -296,10 +292,6 @@ pub(crate) struct Legacy { "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - "moq-transport-17", "moq-transport-18", "moq-transport-19", "moq-transport-20", diff --git a/rs/moq-tokio/src/quic/backend.rs b/rs/moq-tokio/src/quic/backend.rs index 1dee2b630e3..63636956524 100644 --- a/rs/moq-tokio/src/quic/backend.rs +++ b/rs/moq-tokio/src/quic/backend.rs @@ -469,7 +469,7 @@ impl Client { let session = match url.scheme() { "https" => { let mut request = web::proto::ConnectRequest::new(url.clone()); - for protocol in web_protocols(versions) { + for protocol in versions.alpns() { request = request.with_protocol(protocol.to_string()); } web::Session::connect(connection, request) @@ -542,22 +542,6 @@ impl Error { } } -/// The WebTransport subprotocols to offer for `versions`. -/// -/// Draft-14 predates WebTransport subprotocols: `moq-00` is only its QUIC ALPN, and a peer that -/// accepts it over WebTransport picks draft-14 over a newer draft both support. It rides `moql` -/// and negotiates in SETUP instead, as `js/net` does. -fn web_protocols(versions: &moq_net::Versions) -> Vec<&'static str> { - let mut protocols = Vec::new(); - for alpn in versions.alpns() { - let protocol = if alpn == "moq-00" { "moql" } else { alpn }; - if !protocols.contains(&protocol) { - protocols.push(protocol); - } - } - protocols -} - fn map_client_error(err: web::ClientError) -> Error { match client_status(&err).and_then(crate::ConnectError::from_status_u16) { Some(rejected) => rejected.into(), @@ -1062,16 +1046,6 @@ mod tests { use super::*; use url::Url; - #[test] - fn web_protocols_never_offer_moq_00() { - let all = moq_net::Versions::all(); - assert!(!web_protocols(&all).contains(&"moq-00")); - assert_eq!(web_protocols(&all).len(), all.alpns().len() - 1); - - let draft14: moq_net::Version = "moq-transport-14".parse().unwrap(); - assert_eq!(web_protocols(&draft14.into()), ["moql"]); - } - #[tokio::test] async fn pinned_ipv6_connection() { pinned_connection("[::1]:0", None).await; diff --git a/rs/moq-tokio/src/server.rs b/rs/moq-tokio/src/server.rs index b3eee76602e..7731f4cd363 100644 --- a/rs/moq-tokio/src/server.rs +++ b/rs/moq-tokio/src/server.rs @@ -2398,15 +2398,15 @@ mod tests { ); } - /// Answering SETUP is part of the handshake: a moq-transport peer that sends - /// its SETUP but never grants credit for the reply is refused at the same + /// Answering SETUP is part of the handshake: a peer that sends its SETUP but + /// never grants credit for the reply is refused at the same /// deadline, rather than held open by keep-alives, and the session counts it. #[cfg(feature = "websocket")] #[tokio::test(start_paused = true)] async fn stalled_setup_reply_is_closed_at_the_deadline() { - // Record a real moq-transport-14 CLIENT_SETUP. The client then waits on a - // reply that never comes. - let recorder = Scripted::new("moq-00"); + // Record a real CLIENT_SETUP. The client then waits on a reply that never + // comes. + let recorder = Scripted::new("moql"); let client = tokio::spawn({ let recorder = crate::transport::Session::new(recorder.clone(), moq_net::transport::Binding::Uri); async move { @@ -2420,7 +2420,7 @@ mod tests { let bytes = std::mem::take(&mut *recorder.written.lock().unwrap()); assert!(!bytes.is_empty(), "the client never wrote its SETUP"); - let mut peer = Scripted::new("moq-00"); + let mut peer = Scripted::new("moql"); peer.setup = Some(bytes.into()); let timeout = std::time::Duration::from_secs(10); let setup = Setup { diff --git a/rs/moq-tokio/tests/integration/broadcast.rs b/rs/moq-tokio/tests/integration/broadcast.rs index fd2bccb83ed..ae4600ff1de 100644 --- a/rs/moq-tokio/tests/integration/broadcast.rs +++ b/rs/moq-tokio/tests/integration/broadcast.rs @@ -470,18 +470,6 @@ async fn transport_fetch_roundtrip(version: &str) { .expect("server task failed"); } -#[tracing_test::traced_test] -#[tokio::test] -async fn fetch_moq_transport_14() { - transport_fetch_roundtrip("moq-transport-14").await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn fetch_moq_transport_16() { - transport_fetch_roundtrip("moq-transport-16").await; -} - #[tracing_test::traced_test] #[tokio::test] async fn fetch_moq_transport_18() { @@ -1556,30 +1544,6 @@ async fn broadcast_moq_lite_07() { broadcast_test("moqt", Some("moq-lite-07-wip"), Some("moq-lite-07-wip")).await; } -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_moq_transport_14() { - broadcast_test("moqt", Some("moq-transport-14"), Some("moq-transport-14")).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_moq_transport_15() { - broadcast_test("moqt", Some("moq-transport-15"), Some("moq-transport-15")).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_moq_transport_16() { - broadcast_test("moqt", Some("moq-transport-16"), Some("moq-transport-16")).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_moq_transport_17() { - broadcast_test("moqt", Some("moq-transport-17"), Some("moq-transport-17")).await; -} - #[tracing_test::traced_test] #[tokio::test] async fn broadcast_moq_transport_18() { @@ -1630,30 +1594,6 @@ async fn broadcast_negotiate_server_all_client_lite_03() { broadcast_test("moqt", Some("moq-lite-03"), None).await; } -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_negotiate_server_all_client_transport_14() { - broadcast_test("moqt", Some("moq-transport-14"), None).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_negotiate_server_all_client_transport_15() { - broadcast_test("moqt", Some("moq-transport-15"), None).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_negotiate_server_all_client_transport_16() { - broadcast_test("moqt", Some("moq-transport-16"), None).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_negotiate_server_all_client_transport_17() { - broadcast_test("moqt", Some("moq-transport-17"), None).await; -} - #[tracing_test::traced_test] #[tokio::test] async fn broadcast_negotiate_server_all_client_transport_18() { @@ -1686,30 +1626,6 @@ async fn broadcast_negotiate_client_all_server_lite_03() { broadcast_test("moqt", None, Some("moq-lite-03")).await; } -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_negotiate_client_all_server_transport_14() { - broadcast_test("moqt", None, Some("moq-transport-14")).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_negotiate_client_all_server_transport_15() { - broadcast_test("moqt", None, Some("moq-transport-15")).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_negotiate_client_all_server_transport_16() { - broadcast_test("moqt", None, Some("moq-transport-16")).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_negotiate_client_all_server_transport_17() { - broadcast_test("moqt", None, Some("moq-transport-17")).await; -} - #[tracing_test::traced_test] #[tokio::test] async fn broadcast_negotiate_client_all_server_transport_18() { @@ -1804,7 +1720,6 @@ async fn broadcast_max_age() { "moq-lite-05", "moq-lite-06", "moq-lite-07-wip", - "moq-transport-17", "moq-transport-18", "moq-transport-22", ] { @@ -1816,12 +1731,7 @@ async fn broadcast_max_age() { #[tokio::test] async fn broadcast_max_age_legacy_omits_property() { - for version in [ - "moq-lite-01", - "moq-transport-14", - "moq-transport-15", - "moq-transport-16", - ] { + for version in ["moq-lite-01"] { assert_eq!( max_age_test(version, Some(Duration::from_secs(30))).await, None, @@ -1856,30 +1766,6 @@ async fn broadcast_webtransport_moq_lite_03() { broadcast_test("https", Some("moq-lite-03"), Some("moq-lite-03")).await; } -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_webtransport_moq_transport_14() { - broadcast_test("https", Some("moq-transport-14"), Some("moq-transport-14")).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_webtransport_moq_transport_15() { - broadcast_test("https", Some("moq-transport-15"), Some("moq-transport-15")).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_webtransport_moq_transport_16() { - broadcast_test("https", Some("moq-transport-16"), Some("moq-transport-16")).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_webtransport_moq_transport_17() { - broadcast_test("https", Some("moq-transport-17"), Some("moq-transport-17")).await; -} - #[tracing_test::traced_test] #[tokio::test] async fn broadcast_webtransport_moq_transport_18() { @@ -1930,30 +1816,6 @@ async fn broadcast_webtransport_negotiate_server_all_client_lite_03() { broadcast_test("https", Some("moq-lite-03"), None).await; } -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_webtransport_negotiate_server_all_client_transport_14() { - broadcast_test("https", Some("moq-transport-14"), None).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_webtransport_negotiate_server_all_client_transport_15() { - broadcast_test("https", Some("moq-transport-15"), None).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_webtransport_negotiate_server_all_client_transport_16() { - broadcast_test("https", Some("moq-transport-16"), None).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_webtransport_negotiate_server_all_client_transport_17() { - broadcast_test("https", Some("moq-transport-17"), None).await; -} - #[tracing_test::traced_test] #[tokio::test] async fn broadcast_webtransport_negotiate_server_all_client_transport_18() { @@ -1986,30 +1848,6 @@ async fn broadcast_webtransport_negotiate_client_all_server_lite_03() { broadcast_test("https", None, Some("moq-lite-03")).await; } -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_webtransport_negotiate_client_all_server_transport_14() { - broadcast_test("https", None, Some("moq-transport-14")).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_webtransport_negotiate_client_all_server_transport_15() { - broadcast_test("https", None, Some("moq-transport-15")).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_webtransport_negotiate_client_all_server_transport_16() { - broadcast_test("https", None, Some("moq-transport-16")).await; -} - -#[tracing_test::traced_test] -#[tokio::test] -async fn broadcast_webtransport_negotiate_client_all_server_transport_17() { - broadcast_test("https", None, Some("moq-transport-17")).await; -} - #[tracing_test::traced_test] #[tokio::test] async fn broadcast_webtransport_negotiate_client_all_server_transport_18() { @@ -3350,7 +3188,7 @@ fn expect_connect_err(result: moq_tokio::Result<(moq_tokio::Client, moq_tokio::C /// observes the URI (and the deadline on versions that carry one) and keeps an /// existing subscription flowing before leaving. /// -/// Real-QUIC coverage matters beyond the mock: on draft-17+ the GOAWAY rides +/// Real-QUIC coverage matters beyond the mock: on moq-transport the GOAWAY rides /// the SETUP uni streams, where dropping the reader mid-session would emit a /// STOP_SENDING a strict peer treats as a protocol violation. async fn goaway_test(scheme: &str, version: &str, expect_wire_timeout: bool) { @@ -3466,7 +3304,7 @@ async fn goaway_test(scheme: &str, version: &str, expect_wire_timeout: bool) { assert_eq!( goaway.timeout(), Some(Duration::from_secs(5)), - "draft-17+ carries the deadline" + "moq-transport carries the deadline" ); } else { assert_eq!(goaway.timeout(), None, "no wire timeout on this version"); @@ -3492,16 +3330,6 @@ async fn goaway_moq_lite_05_webtransport() { goaway_test("https", "moq-lite-05", false).await; } -#[tokio::test] -async fn goaway_moq_transport_14_quic() { - goaway_test("moqt", "moq-transport-14", false).await; -} - -#[tokio::test] -async fn goaway_moq_transport_17_quic() { - goaway_test("moqt", "moq-transport-17", true).await; -} - #[tokio::test] async fn goaway_moq_transport_19_quic() { goaway_test("moqt", "moq-transport-19", true).await; diff --git a/rs/moq-tokio/tests/integration/max_age.rs b/rs/moq-tokio/tests/integration/max_age.rs index fe1233b0aac..6addcf7612f 100644 --- a/rs/moq-tokio/tests/integration/max_age.rs +++ b/rs/moq-tokio/tests/integration/max_age.rs @@ -125,7 +125,7 @@ async fn relay(version: moq_net::Version, javascript: Option) -> anyhow::R #[tokio::test] async fn max_age_relay_rust() { - for version in ["moq-lite-07-wip", "moq-transport-17", "moq-transport-22"] { + for version in ["moq-lite-07-wip", "moq-transport-18", "moq-transport-22"] { tokio::time::timeout(TIMEOUT, relay(version.parse().unwrap(), None)) .await .unwrap() @@ -136,7 +136,7 @@ async fn max_age_relay_rust() { #[tokio::test] #[ignore = "requires Bun; run by just test max-age in the interop workflow"] async fn max_age_relay_javascript() { - for version in ["moq-lite-07-wip", "moq-transport-17", "moq-transport-22"] { + for version in ["moq-lite-07-wip", "moq-transport-18", "moq-transport-22"] { for publish in [false, true] { tokio::time::timeout(TIMEOUT, relay(version.parse().unwrap(), Some(publish))) .await diff --git a/rs/moq-tokio/tests/integration/receive_credit.rs b/rs/moq-tokio/tests/integration/receive_credit.rs index 22a413e7fbd..f6c67ff3e33 100644 --- a/rs/moq-tokio/tests/integration/receive_credit.rs +++ b/rs/moq-tokio/tests/integration/receive_credit.rs @@ -301,7 +301,7 @@ async fn lite_partial_frame_gives_way() { #[tokio::test] async fn ietf_partial_frame_gives_way() { - the_less_urgent_partial_frame_gives_way("moq-transport-16", None).await; + the_less_urgent_partial_frame_gives_way("moq-transport-18", None).await; } /// The same recovery when a slow link with a deep queue sits between them. @@ -589,7 +589,7 @@ async fn lite_largest_unit_completes_under_pressure() { #[tokio::test] async fn ietf_largest_unit_completes_under_pressure() { - the_largest_unit_completes_under_pressure("moq-transport-16").await; + the_largest_unit_completes_under_pressure("moq-transport-18").await; } /// A Bun publisher (`test/receive-credit/sender.ts`) dials a WebTransport server whose @@ -691,7 +691,7 @@ async fn javascript_partial_frame_gives_way(version: &str) { #[tokio::test] #[ignore = "requires Bun; run by just test receive-credit in the interop workflow"] async fn javascript_partial_frame_gives_way_tokio() { - for version in ["moq-lite-06", "moq-transport-16"] { + for version in ["moq-lite-06", "moq-transport-18"] { javascript_partial_frame_gives_way(version).await; } } diff --git a/rs/moq-wasm/src/transport.rs b/rs/moq-wasm/src/transport.rs index 21c34845f6d..6549215f5e6 100644 --- a/rs/moq-wasm/src/transport.rs +++ b/rs/moq-wasm/src/transport.rs @@ -25,12 +25,8 @@ pub struct Options { /// Advertise every version `moq-net` supports, so the peer picks one via ALPN. /// Without this the browser negotiates no subprotocol and the session has no /// version to start from. -/// -/// Draft-14 predates WebTransport subprotocols: `moq-00` is only its QUIC ALPN, and a -/// peer that accepts it here picks draft-14 over a newer draft both support. It rides -/// `moql` (also offered) and negotiates in SETUP instead, as `js/net` does. fn builder() -> ClientBuilder { - ClientBuilder::new().with_protocols(moq_net::ALPNS.iter().copied().filter(|alpn| *alpn != "moq-00")) + ClientBuilder::new().with_protocols(moq_net::ALPNS.iter().copied()) } /// Open a browser WebTransport connection to `url`. diff --git a/rs/qmux/src/alpn.rs b/rs/qmux/src/alpn.rs index 87fe2807fa1..a3b1860eec7 100644 --- a/rs/qmux/src/alpn.rs +++ b/rs/qmux/src/alpn.rs @@ -79,11 +79,11 @@ mod tests { let out = build( [ ("moq-lite-04", &[Version::QMux01][..]), - ("moq-transport-17", &[Version::QMux00][..]), + ("moq-transport-18", &[Version::QMux00][..]), ], true, ); - assert_eq!(out, vec!["qmux-01.moq-lite-04", "qmux-00.moq-transport-17"]); + assert_eq!(out, vec!["qmux-01.moq-lite-04", "qmux-00.moq-transport-18"]); } #[test] @@ -126,8 +126,8 @@ mod tests { (Version::QMux01, Some("moq-lite-04".to_string())) ); assert_eq!( - parse(Some("qmux-00.moq-transport-17")).unwrap(), - (Version::QMux00, Some("moq-transport-17".to_string())) + parse(Some("qmux-00.moq-transport-18")).unwrap(), + (Version::QMux00, Some("moq-transport-18".to_string())) ); } diff --git a/test/ietf-conformance/README.md b/test/ietf-conformance/README.md index a252809a15d..f3fc9fbeb45 100644 --- a/test/ietf-conformance/README.md +++ b/test/ietf-conformance/README.md @@ -6,9 +6,9 @@ builds the Moxygen revision and matching Meta dependencies pinned in `conformance_test.sh` through this checkout's relay. This is separate from the published-package smoke matrix. -Sections 1-7 run on both shared drafts, 16 and 18, through one relay. Each cell +Sections 1-7 run on draft 18, the one draft both support, through one relay. Each cell runs 29 upstream SUBSCRIBE cases. A direct-origin control runs the same cases -once per draft, separating suite or pin failures from relay behavior. FETCH is +once, separating suite or pin failures from relay behavior. FETCH is skipped using upstream's `SKIP_FETCH`; section 8 uses PUBLISH and is outside the relay's support. Cases using forwarding modes 1 or 2 send nonzero subgroups and are explicitly excluded. Modes 0 and 3 are retained. @@ -19,7 +19,7 @@ these exclusions and failure sensitivity, and refuse a changed script anchor. The publisher advertises the `moq-test-00` prefix. The client subscribes to 16-field namespaces below it. Mock-clock integration tests in [`ietf_conformance.rs`](../../rs/moq-net/tests/integration/ietf_conformance.rs) -prove this routing path on both drafts, through one relay and through two +prove this routing path on draft 18, through one relay and through two relays joined with native lite-07. The live harness waits for that prefix at the relay before starting cases and rejects upstream publisher reconnects, so a recovered session cannot @@ -57,7 +57,6 @@ Measured over six consecutive runs: | Draft | Direct origin | One relay | | --- | --- | --- | -| 16 | 29 pass | 29 pass | | 18 | 29 pass | 29 pass | A two-relay cell is not run yet. The edge needs metadata from TRACK\_STATUS diff --git a/test/ietf-conformance/run.sh b/test/ietf-conformance/run.sh index df3502a4b81..14de8dde520 100755 --- a/test/ietf-conformance/run.sh +++ b/test/ietf-conformance/run.sh @@ -101,11 +101,11 @@ run_suite() { fi } -# The pinned MoQVersions.h supports drafts 16 and 18; this relay supports both. +# The pinned MoQVersions.h supports drafts 16 and 18; this relay supports only 18. failed=0 # A two-relay cell waits on a test publisher that answers TRACK_STATUS; see the # passthrough quest. -for version in 16 18; do +for version in 18; do label="single-$version" start_relay "$label-origin" "$version" relay_port="$HARNESS_PORT" diff --git a/test/interop/interop.sh b/test/interop/interop.sh index b3d392c568a..166df066ca8 100755 --- a/test/interop/interop.sh +++ b/test/interop/interop.sh @@ -79,7 +79,7 @@ FINISH_ONLY=0 # except the clients the harness reads a grant from, which offer $AUTH_VERSION alone. Any other # entry pins every client in its pass to that version, and `ietf` expands to IETF_VERSIONS. VERSIONS="auto" -IETF_VERSIONS="moq-transport-14,moq-transport-16,moq-transport-18,moq-transport-20,moq-transport-22" +IETF_VERSIONS="moq-transport-18,moq-transport-20,moq-transport-22" # Cargo profile for the relay/cli/moq-c builds. Debug compiles faster, which is # what an interop test wants; the workload (320x240@30) is trivial either way. @@ -459,15 +459,7 @@ plan_pass() { record "not run" case="wire track status" publisher=rust subscriber="$runtime" record "not run" case=goaway publisher=rust subscriber="$runtime" [[ "$runtime" != js-native-bun ]] || record "not run" case=goaway publisher="$runtime" subscriber=rust - case "$VERSION" in - moq-transport-14 | moq-transport-16) - record "not run" case="request credit" publisher="$runtime" subscriber=rust - [[ "$runtime" != js-native-bun ]] || record "not run" case="request credit" publisher="$runtime" subscriber="$runtime" - ;; - *) - record "not run" case="track status" publisher="$runtime" subscriber=rust - ;; - esac + record "not run" case="track status" publisher="$runtime" subscriber=rust fi done [[ "$VERSION" == auto ]] || return 0 @@ -895,7 +887,7 @@ token_url() { # The pattern a cell's token grants its publisher, or its subscribers, on . The `auto` # pass mints patterns no prefix could carry, so AUTH must deliver them as minted. moq-transport's -# AUTH extension (draft-17 on) carries prefixes alone and refuses a narrower grant, closing the +# AUTH extension carries prefixes alone and refuses a narrower grant, closing the # session, so a pinned pass grants the broadcast's subtree. publish_grant() { if [[ "$VERSION" == auto ]]; then printf '%s' "$1"; else printf '%s/**' "$1"; fi @@ -2095,16 +2087,11 @@ run_control_case() { local command=("$TARGET_BASE/$PROFILE/examples/interop-control" "$kind" "$version" "${runtime##*-}") case "$kind" in status) cell=(case="wire track status" publisher=rust subscriber="$runtime" log="$LOGS/$name.log") ;; - credit) cell=(case="request credit" publisher="$runtime" subscriber=rust log="$LOGS/$name.log") ;; goaway) command=("$TARGET_BASE/$PROFILE/examples/interop-goaway" "$version" "${runtime##*-}") ;; reverse-goaway) command=("$TARGET_BASE/$PROFILE/examples/interop-goaway-client" "$version") cell=(case=goaway publisher="$runtime" subscriber=rust log="$LOGS/$name.log") ;; - js-credit) - command=(bun "$CLIENTS/js-native/server.ts" credit "$version") - cell=(case="request credit" publisher="$runtime" subscriber="$runtime" log="$LOGS/$name.log") - ;; esac if is_broken "$runtime"; then record fail "${cell[@]}" detail="client unavailable" @@ -2141,15 +2128,7 @@ run_tails() { run_control_case "$runtime" status run_control_case "$runtime" goaway [[ "$runtime" != js-native-bun ]] || run_control_case "$runtime" reverse-goaway - case "$VERSION" in - moq-transport-14 | moq-transport-16) - run_control_case "$runtime" credit - [[ "$runtime" != js-native-bun ]] || run_control_case "$runtime" js-credit - ;; - *) - run_tail_pair "$runtime" rust "track status" - ;; - esac + run_tail_pair "$runtime" rust "track status" fi fi done diff --git a/test/interop/interop.toml b/test/interop/interop.toml index b3802beb343..928d675b92e 100644 --- a/test/interop/interop.toml +++ b/test/interop/interop.toml @@ -23,10 +23,6 @@ version = [ "moq-transport-20", "moq-transport-19", "moq-transport-18", - "moq-transport-17", - "moq-transport-16", - "moq-transport-15", - "moq-transport-14", ] [web.http] diff --git a/test/smoke/README.md b/test/smoke/README.md index 1e8461460eb..c15293ff549 100644 --- a/test/smoke/README.md +++ b/test/smoke/README.md @@ -13,7 +13,7 @@ This suite installs the published moq packages and checks that they exchange med | Packages | `just smoke`, `just full` | Published clients through a local `moq-relay`. `smoke` is Rust only. `full` is the cross-language matrix. | | Public relays | `just relays` | Rust publisher and subscriber through every relay in the [moq-interop-runner](https://github.com/englishm/moq-interop-runner) registry. CI publishes with Rust and the browser (`js-web`) and subscribes with Rust, `js-web`, and `js-bun`. On those relays `js-web` and `js-bun` run only on WebTransport (`@moq/web-transport` has no raw QUIC mode); a client that does not belong is omitted. WebSocket is its own column for the relays in `WEBSOCKET_KEYS` (`moq-dev-rs`): each of their `http(s)` endpoints also runs as `ws(s)`, and every other relay keeps that fallback off. A relay that refuses anonymous root broadcasts is dialed on the path `publish_path` names (`stitcher-moq` publishes under `/moq-test`). That run is what the [results page](https://moq-dev.github.io/moq/relays/) shows. Only `--required` relays fail the run; the default is moq-dev's `cdn.moq.dev`, including its WebSocket endpoint. | | Upstream Docker runner | `just runner-build`, `just runner UPSTREAM OUTPUT` | Every published registry client against this checkout's relay, and the moq-dev clients against published Docker relays. Uses unpinned upstream HEAD and its HTML report. Local-build-only adapters appear as not run. Any failed pair or setup step fails the run, after the report is written. CI publishes it to [interop-runner/](https://git.moq.dev/interop-runner/). | -| Cloudflare | `just cloudflare` | Relays built from the default branches of [cloudflare/moq-rs](https://github.com/cloudflare/moq-rs) and moq-dev/moq, driven by a Cloudflare client over WebTransport and raw QUIC. | +| Cloudflare | `just cloudflare` | Relays built from the heads of [cloudflare/moq-rs](https://github.com/cloudflare/moq-rs)'s `draft-18-dev` branch and moq-dev/moq's default branch, driven by a Cloudflare client over WebTransport and raw QUIC. | | moxygen | `just moxygen` | Meta's [`moxygen`](https://github.com/facebookexperimental/moxygen) interop client through the latest moq-dev relay. Needs Linux Docker. | | Tokens | `just token`, `just token-full` | `moq auth` and npm [`@moq/auth`](https://www.npmjs.com/package/@moq/auth) mint and verify each other's JWTs, and each verifier rejects a tampered token. | diff --git a/test/smoke/clients/cloudflare/Cargo.toml b/test/smoke/clients/cloudflare/Cargo.toml index 46c995a3219..f275b973a8b 100644 --- a/test/smoke/clients/cloudflare/Cargo.toml +++ b/test/smoke/clients/cloudflare/Cargo.toml @@ -12,8 +12,8 @@ publish = false anyhow = "1" bytes = "1" clap = { version = "4", features = ["derive"] } -moq-native-ietf = { git = "https://github.com/cloudflare/moq-rs" } -moq-transport = { git = "https://github.com/cloudflare/moq-rs" } +moq-native-ietf = { git = "https://github.com/cloudflare/moq-rs", branch = "draft-18-dev" } +moq-transport = { git = "https://github.com/cloudflare/moq-rs", branch = "draft-18-dev" } tokio = { version = "1", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/test/smoke/cloudflare.sh b/test/smoke/cloudflare.sh index edc6ed39e1b..5860ff96b84 100755 --- a/test/smoke/cloudflare.sh +++ b/test/smoke/cloudflare.sh @@ -1,16 +1,19 @@ #!/usr/bin/env bash -# End-to-end smoke test using the current cloudflare/moq-rs default branch. +# End-to-end smoke test using the head of cloudflare/moq-rs's draft-18 branch. # # This is intentionally separate from smoke.sh's public-package media matrix: # cloudflare/moq-rs uses a different application catalog/track layout. We build # a purpose-built Cloudflare client and both projects' relays from their latest -# Git source. The client validates every byte of sustained subgroup payloads +# Git source. cloudflare/moq-rs's default branch speaks only moq-transport-16, +# which moq-dev/moq dropped, so both Cloudflare builds follow its draft-18 branch. +# The client validates every byte of sustained subgroup payloads # through both relays; Cloudflare's relay also exercises datagrams. set -euo pipefail SMOKE_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) CLOUDFLARE_CLIENT="$SMOKE_DIR/clients/cloudflare" CLOUDFLARE_MOQ_REPO="https://github.com/cloudflare/moq-rs" +CLOUDFLARE_MOQ_BRANCH="draft-18-dev" MOQ_REPO="https://github.com/moq-dev/moq" PORT="${CLOUDFLARE_SMOKE_PORT:-4443}" TIMEOUT="${CLOUDFLARE_SMOKE_TIMEOUT:-20}" @@ -26,13 +29,13 @@ CLIENT="${CLOUDFLARE_CLIENT_BIN:-}" have() { command -v "$1" >/dev/null 2>&1; } -# build_git

: build one package from a repo's default -# branch with its committed Cargo.lock, into /target/release. A plain +# build_git [branch]: build one package from the head of +# a repo's branch (its default branch when omitted) with its committed Cargo.lock, into /target/release. A plain # `cargo build` in a checkout rather than `cargo install --git`: a compile cache # (mbx in CI) reuses unchanged crates across builds, but not across installs. build_git() { - local repo="$1" package="$2" dir="$3" - git clone --quiet --depth 1 "$repo" "$dir" && + local repo="$1" package="$2" dir="$3" branch="${4:-}" + git clone --quiet --depth 1 ${branch:+--branch "$branch"} "$repo" "$dir" && cargo build --quiet --release --locked --manifest-path "$dir/Cargo.toml" -p "$package" \ --target-dir "$dir/target" } @@ -65,11 +68,11 @@ if [[ ${#missing[@]} -gt 0 ]]; then fi if [[ -z "$CLOUDFLARE_RELAY" ]]; then - echo "building moq-relay-ietf from $CLOUDFLARE_MOQ_REPO (latest default branch)..." + echo "building moq-relay-ietf from $CLOUDFLARE_MOQ_REPO (latest $CLOUDFLARE_MOQ_BRANCH)..." # Follow Git HEAD, but honor that checkout's committed Cargo.lock. Without # --locked Cargo currently selects a newer hyper-util that does not compile # with moq-rs's hyper-serve dependency. - if build_git "$CLOUDFLARE_MOQ_REPO" moq-relay-ietf "$TMP/cloudflare-moq" >"$TMP/relay-build.log" 2>&1; then + if build_git "$CLOUDFLARE_MOQ_REPO" moq-relay-ietf "$TMP/cloudflare-moq" "$CLOUDFLARE_MOQ_BRANCH" >"$TMP/relay-build.log" 2>&1; then CLOUDFLARE_RELAY="$TMP/cloudflare-moq/target/release/moq-relay-ietf" else echo "error: failed to build moq-relay-ietf" >&2 diff --git a/test/smoke/freshness.sh b/test/smoke/freshness.sh index 3c5e881d2a0..b1c69450ef2 100755 --- a/test/smoke/freshness.sh +++ b/test/smoke/freshness.sh @@ -89,22 +89,25 @@ else note FAIL "smoke.sh no longer go-gets moq.dev/moq @latest" fail=1 fi -# Both relays and the local integrity client deliberately follow their -# repositories' default branches. A rev/tag/branch would silently turn this -# into a stale snapshot rather than a Git-head smoke test. +# Both relays and the local integrity client deliberately follow a branch head: +# moq-dev/moq's default branch, and cloudflare/moq-rs's draft-18 branch, since its +# default speaks only moq-transport-16. A rev or tag would silently turn this into +# a stale snapshot rather than a Git-head smoke test. # shellcheck disable=SC2016 # the greps match literal variable references builds_head() { # builds_head